legacylisten 0.2.0

A simple CLI audio player with strange features.
Documentation
//! Entry point for `legacylisten`.
//!
//! This is the top level module and `legacylisten` is started by
//! calling the `run()` function.

use std::{io::Read, path::Path, sync::atomic::Ordering, thread, time::Duration};

use crossbeam_channel::{unbounded, Receiver};
use diskit::Diskit;
use id3::Tag;
use legacytranslate::MessageHandler;
use signal_hook::{
    consts::{SIGINT, SIGTERM, SIGUSR1, SIGUSR2},
    iterator::Signals,
};
use simple_logger::SimpleLogger;

use crate::{
    api::ApiResponder,
    audio::{AudioHandler, ChannelAudio},
    conffile::Conffile,
    config::Config,
    err::Error,
    helpers::take_error,
    l10n::{messages::Message, Writer},
    matcher::{main_match, BigAction},
    songs::{Song, Songs},
    threads::start_threads,
};

// Look in lib.rs for justification.
#[allow(clippy::needless_pass_by_value)]
// Called by songs::choose_random.
fn handle_song<D>(
    song: &mut Song,
    api: &ApiResponder,
    quit_request: Receiver<()>,
    config: &mut Config,
    diskit: D,
) -> BigAction
where
    D: Diskit + Send + 'static,
{
    let data_dir = &config.arc_config.conffile.data_dir;
    let song_path = data_dir.join(song.name.clone());
    let (tag, tag_option) = take_error(Tag::read_from_path(&song_path));
    config.tag = Some(tag);
    config.source = match ChannelAudio::new(&song_path, config.arc_config.clone(), diskit.clone())
    {
        Ok(source) => source,
        Err(e) =>
        {
            config
                .l10n
                .write(Message::ReadingSongProblem(song.name.clone(), e));

            if config.unsuccessful_tries == 255
            {
                config.l10n.write(Message::TooManyTries);
                return BigAction::Quit;
            }
            config.unsuccessful_tries += 1;

            config.l10n.write(Message::ChoosingNewSong);
            return BigAction::Skip;
        }
    };

    config.unsuccessful_tries = 0;

    config.num = song.num;
    config.loud = song.loud;

    config
        .audio_handler
        .append(config.source.inner.take().unwrap());
    config.audio_handler.set_volume(song.loud);

    if let Ok(s) = data_dir
        .join(song.name.clone())
        .into_os_string()
        .into_string()
    {
        config.l10n.write(Message::PlayingSong(s));
    }
    else
    {
        config.l10n.write(Message::PlayingSongUnknown);
    }

    config.l10n.write(Message::SongLikelihood(song.num));

    config.arc_config.update_dbus.store(true, Ordering::SeqCst);
    while !config.audio_handler.empty()
    {
        match main_match(config, diskit.clone())
        {
            BigAction::Nothing =>
            {
                song.num = config.num;
                song.loud = config.loud;
            }
            x => return x,
        }
        if quit_request.try_recv().is_ok()
        {
            return BigAction::Quit;
        }
        if config.rx_control.try_recv().is_ok()
        {
            let _ = config.tx_paused.send(config.paused);
            let _ = config.tx_path.send((song_path.clone(), tag_option.clone()));
        }
        if config.arc_config.reading_paused.load(Ordering::SeqCst)
        {
            config.l10n.write(Message::SignalPaused);
        }

        api.handle(config);
        song.num = config.num;
        song.loud = config.loud;

        // To prevent busy loop
        thread::sleep(Duration::from_micros(1));
    }

    BigAction::Nothing
}

fn handle_pausely(config: &mut Config) -> bool
{
    if config.quit_after_song
    {
        // Returns only `true` when legacylisten should quit.
        return true;
    }
    if config.pause_after_song
    {
        config.l10n.write(Message::RequestedPause);
        config.audio_handler.pause();
        config.paused = true;
        config.pause_after_song = false;
    }
    while config
        .arc_config
        .reading_paused
        .load(std::sync::atomic::Ordering::SeqCst)
    {
        // Is there any way to make that better than a poll loop?
        thread::sleep(Duration::from_millis(1));
    }

    false
}

/// Entry point for `legacylisten`
///
/// By calling this function `legacylisten` is started.
///
/// The first parameter allows to customize the configuration of
/// legacylisten.  If you don't want to change anything pass
/// [`Conffile::new`].  For more see the [documentation](Conffile).
///
/// The second and third parameters allow you to intercept the
/// terminal IO.  If `reader` is called it must yield an object from
/// which the desired input is [`read`](std::io::Read)-able, for
/// documentation of `writer` see [`Writer`](crate::l10n::Writer).
/// The default values are `standard_write_handler()` and `||
/// io::stdin().lock()` respectively.
///
/// The last parameter allows you to intercept disk IO, see [`diskit`]
/// for more information.
/// # Panics
/// It will panic if an fatal condition is encountered and it can't be
/// passed down as [`Error`](Error).
/// # Errors
/// It will return an error if an fatal condition occurs and it
/// actually can be passed down.
// Look in lib.rs for justification.
#[allow(clippy::needless_pass_by_value)]
pub fn run<C, A, D, R>(
    get_conffile: C,
    writer: Writer,
    reader: fn() -> R,
    api: ApiResponder,
    quit_request: Receiver<()>,
    diskit: D,
) -> Result<(), Error>
where
    C: Fn(&Path, D) -> Conffile,
    A: AudioHandler,
    D: Diskit + Send + 'static,
    R: Read + 'static,
{
    SimpleLogger::new().init().unwrap();

    // Initializing some channels for communication between some
    // far-away parts.  Better than the original globals, but still
    // not how I'd like it.
    let (tx_control, rx_control) = unbounded();
    let (tx_paused, rx_paused) = unbounded();
    let (tx_path, rx_path) = unbounded();
    // Initializing the configuration; nearly every function gets a
    // reference to that.
    let mut config = Config::new::<_, A, _>(
        rx_control,
        tx_paused,
        tx_path,
        get_conffile,
        writer,
        diskit.clone(),
    )?;
    // Reading the likelihoods and volumes of all songs.
    let mut songs = Songs::read(config.arc_config.clone(), config.l10n, diskit.clone())?;
    // Copied to make the borrowck happy.
    let l10n = config.l10n;

    // Starts a couple minor threads.
    start_threads(
        config.tx.clone(),
        tx_control,
        rx_paused,
        rx_path,
        Signals::new([SIGINT, SIGTERM, SIGUSR1, SIGUSR2])?,
        reader,
        config.arc_config.clone(),
        config.l10n,
        diskit.clone(),
    );

    loop
    {
        // There are multiple ways of pausing; handle all of them.
        if handle_pausely(&mut config)
        {
            break;
        }

        // A new song is about to be chosen, so notice dbus.
        config.arc_config.update_dbus.store(true, Ordering::SeqCst);

        // Choose a new song, play it and handle everthing else.  Name
        // might be a bit of a misnomer, since it does more than
        // choosing a song.  If through a command or something else,
        // we should quit the `break` handles that.
        match songs.choose_random(
            &mut config,
            handle_song,
            &api,
            quit_request.clone(),
            l10n,
            diskit.clone(),
        )
        {
            BigAction::Nothing | BigAction::Skip =>
            {}
            BigAction::Quit => break,
        }
    }

    config
        .l10n
        .write(Message::TotalPlayingLikelihood(songs.total_likelihood()));

    Ok(())
}