legacylisten 0.2.0

A simple CLI audio player with strange features.
Documentation
//! Handles song choosing and processing

use std::{cmp::Ordering, io::Write, sync::Arc};

use crossbeam_channel::Receiver;
use diskit::{diskit_extend::DiskitExt, walkdir::WalkDir, Diskit};
use legacytranslate::MessageHandler;
use rand::random;

use crate::{
    api::ApiResponder,
    config::{ArcConfig, Config},
    csv::Csv,
    err::Error,
    l10n::{
        messages::{LogLevel, Message},
        L10n,
    },
    matcher::BigAction,
};

/// Stores whether the song is repeated.
///
/// This enum stores whether the current song is repeated and if yes,
/// how.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Repeat
{
    Not,
    Once,
    Always,
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Song
{
    pub name: String,
    pub num: u32,
    pub loud: f32,
}

#[derive(Clone)]
pub(crate) struct L10nHelper
{
    l10n: L10n,
    // I don't want to do an failable action in such a high
    // sensitivity area; probably stupid
    err_msg: String,
}

#[derive(Clone)]
pub(crate) struct Songs<D>
where
    D: Diskit,
{
    pub songs: Vec<Song>,
    pub config: Arc<ArcConfig>,
    pub l10n_helper: L10nHelper,
    pub diskit: D,
}

impl L10nHelper
{
    pub fn new(l10n: L10n) -> Self
    {
        Self {
            l10n,
            err_msg: l10n.get(Message::SavingStateErr),
        }
    }
}

impl<D> Drop for Songs<D>
where
    D: Diskit,
{
    fn drop(&mut self)
    {
        let s = format!("{}", Csv::from(self));

        match Self::save(s.as_bytes(), &self.config, self.diskit.clone())
        {
            Ok(()) =>
            {
                self.l10n_helper.l10n.write(Message::StateSaved);
            }
            Err(e) =>
            {
                self.l10n_helper.l10n.write(Message::LiteralString(
                    format!("{}: {:?}", self.l10n_helper.err_msg, e),
                    LogLevel::Error,
                ));
                self.l10n_helper
                    .l10n
                    .write(Message::LiteralString(s, LogLevel::Error));
            }
        }
    }
}

impl<D> Songs<D>
where
    D: Diskit,
{
    #[must_use]
    pub fn total_likelihood(&self) -> u32
    {
        self.songs.iter().map(|x| x.num).sum()
    }

    // Look in lib.rs for justification.
    #[allow(clippy::needless_pass_by_value)]
    fn save(s: &[u8], config: &Arc<ArcConfig>, diskit: D) -> Result<(), Error>
    {
        let mut config_file = diskit.create(&config.config_dir.join("songs.csv"))?;

        config_file.write_all(s)?;

        Ok(())
    }

    pub fn read(config: Arc<ArcConfig>, l10n: L10n, diskit: D) -> Result<Self, Error>
    {
        let mut songs = Csv::new(config.config_dir.join("songs.csv"), diskit.clone())?
            .get_songs(config, l10n, diskit.clone())
            .ok_or(Error::MalformattedSongsCsv)?;

        for file in config_dir_handle(&songs.config, diskit)?
        {
            let file = file?;
            if file.file_type().is_dir()
            {
                // Don't want directories, since they can't be played.
                continue;
            }

            let filename = file
                .path()
                .strip_prefix(&songs.config.conffile.data_dir)?
                .to_string_lossy()
                .into_owned();

            if !songs.songs.iter().any(|x| x.name == filename)
            {
                l10n.write(Message::NewSongFound(filename.clone()));

                songs.songs.push(Song {
                    name: filename,
                    num: 10,
                    loud: 0.1,
                });
            }
        }

        Ok(songs)
    }

    pub fn choose_random<F>(
        &mut self,
        config: &mut Config,
        mut f: F,
        api: &ApiResponder,
        quit_request: Receiver<()>,
        l10n: L10n,
        diskit: D,
    ) -> BigAction
    where
        F: FnMut(&mut Song, &ApiResponder, Receiver<()>, &mut Config, D) -> BigAction,
    {
        let total = self.total_likelihood();

        l10n.write(Message::TotalPlayingLikelihood(total));

        if total == 0
        {
            l10n.write(Message::NoSongs);
            return BigAction::Quit;
        }

        if config.songlist.len() == config.song_index
        {
            let mut song_number = (random::<u64>() % total as u64) as _;

            for (pos, song) in self.songs.iter_mut().enumerate()
            {
                if song.num >= song_number
                {
                    config.songlist.push(pos);
                    break;
                }
                song_number -= song.num;
            }
        }

        let index = config.songlist[config.song_index];

        if config.repeat != Repeat::Not
        {
            self.songs[index].num = (self.songs[index].num as i64
                + config.arc_config.conffile.repeat_bonus)
                .clamp(0, u32::MAX as _)
                .try_into()
                .unwrap();

            match config.arc_config.conffile.repeat_bonus.cmp(&0)
            {
                Ordering::Greater => l10n.write(Message::PositiveBonus(self.songs[index].num)),
                Ordering::Less =>
                {
                    l10n.write(Message::NegativeBonus(self.songs[index].num));
                    if self.songs[index].num == 0
                    {
                        l10n.write(Message::SongNever);
                    }
                }
                Ordering::Equal =>
                {}
            }
        }

        match config.repeat
        {
            Repeat::Not => config.song_index += 1,
            Repeat::Once =>
            {
                config.repeat = Repeat::Not;
                config.song_index += 1;
            }
            Repeat::Always =>
            {}
        }

        f(&mut self.songs[index], api, quit_request, config, diskit)
    }
}

// Look in lib.rs for justification.
#[allow(clippy::needless_pass_by_value)]
fn config_dir_handle<D>(config: &Arc<ArcConfig>, diskit: D) -> Result<WalkDir<D>, Error>
where
    D: Diskit,
{
    let filename = &config.conffile.data_dir;
    let dir = diskit.open(filename).or_else(|_| {
        diskit.create_dir_all(filename)?;
        diskit.open(filename)
    })?;
    if !dir.metadata()?.is_dir()
    {
        diskit.trash_delete(filename)?;
        diskit.create_dir_all(filename)?;
    }

    Ok(diskit.walkdir(filename).set_follow_links(true))
}