selene-core 0.5.2

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{path::PathBuf, str::FromStr};

use chrono::{DateTime, Utc};
use lunar_lib::error;
use regex::Regex;
use serde::{Deserialize, Serialize};

use crate::{
    library::{
        album::AlbumId,
        artist::ArtistId,
        collection::rules::{EqOp, OrdOp, Rule, eq_many, eq_single, ord_single},
        track::{Track, TrackId, lyric_data::LyricData},
    },
    media_container::ContainerFormat,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TrackRule {
    /// Succeeds if the track id matches the [`EqOp`] of the input ids
    Id { id: Vec<TrackId>, op: EqOp },

    /// Succeeds if the track source dir matches the [`EqOp`] of the input paths
    SourceDir { source_dir: Vec<PathBuf>, op: EqOp },

    /// Succeeds if the track library dir matches the [`EqOp`] of the input paths
    LibraryDir { library_dir: Vec<PathBuf>, op: EqOp },

    /// Succeeds if the format matches the [`EqOp`] of the input formats
    Format {
        format: Vec<ContainerFormat>,
        op: EqOp,
    },

    /// Succeeds if the track duration matches the [`OrdOp`]
    Duration { duration: f64, op: OrdOp },

    /// Succeeds if the track channel count matches the [`OrdOp`]
    ChannelCount { count: usize, op: OrdOp },

    /// Succeeds if the track title DOES/DOES NOT match ALL/ANY of the input regexes
    Title { regex: Vec<String>, op: EqOp },

    /// Succeeds if the track has no album
    Single,

    /// Succeeds if the track album matches the [`EqOp`] of the input albums
    Album { albums: Vec<AlbumId>, op: EqOp },

    /// Succeeds if the track artists match the input artists using the [`EqOp`]
    Artist { artists: Vec<ArtistId>, op: EqOp },

    /// Succeeds if the track date matches the [`OrdOp`]
    Date { date: DateTime<Utc>, op: OrdOp },

    /// Succeeds if the track genres match the input genres using the [`EqOp`]
    Genre { genres: Vec<String>, op: EqOp },

    /// Succeeds if the track has any type of lyrics
    HasLyrics,

    /// Succeeds if the track has synced lyrics
    HasSyncedLyrics,

    /// Succeeds if the track has plain lyrics
    HasPlainLyrics,

    /// Succeeds if the track is instrumental
    Instrumental,

    /// Succeeds if the 'other' metadata at the given key matches the [`EqOp`] of the input value
    MetadataOther {
        key: String,
        value: Vec<String>,
        op: EqOp,
    },
}

impl Rule for TrackRule {
    type Item = Track;

    fn matches(&self, item: &Self::Item) -> bool {
        match self {
            TrackRule::Id { id, op } => eq_single(&item.id(), id, *op),
            TrackRule::SourceDir { source_dir, op } => {
                eq_single(&item.container().path().to_path_buf(), source_dir, *op)
            }
            TrackRule::LibraryDir { library_dir, op } => {
                eq_single(&item.container().path().to_path_buf(), library_dir, *op)
            }
            TrackRule::Format { format, op } => eq_single(item.container().format(), format, *op),
            TrackRule::Duration { duration, op } => {
                ord_single(item.container().stream().duration(), *duration, *op)
            }
            TrackRule::ChannelCount { count, op } => {
                ord_single(&item.container().stream().codec_params.channels, count, *op)
            }
            TrackRule::Title { regex, op } => {
                let regex: Vec<_> = match regex
                    .iter()
                    .map(|r| Regex::from_str(r))
                    .collect::<Result<Vec<_>, _>>()
                {
                    Ok(v) => v,
                    Err(err) => {
                        error!("Failed to create regex: {err}");
                        return false;
                    }
                };

                item.metadata.title.as_ref().is_some_and(|t| match op {
                    EqOp::EqAny => regex.iter().any(|r| r.is_match(t)),
                    EqOp::EqAll => regex.iter().all(|r| r.is_match(t)),
                    EqOp::NeqAny => !regex.iter().any(|r| r.is_match(t)),
                    EqOp::NeqAll => !regex.iter().all(|r| r.is_match(t)),
                })
            }
            TrackRule::Single => item.metadata.album.is_none(),
            TrackRule::Album { albums: album, op } => item
                .metadata
                .album
                .as_ref()
                .is_some_and(|a| eq_single(a, album, *op)),
            TrackRule::Artist {
                artists: artist,
                op,
            } => eq_many(item.metadata.artists.artist_ids(), artist, *op),
            TrackRule::Date { date, op } => item
                .metadata
                .date
                .is_some_and(|d| ord_single(&d, date, *op)),
            TrackRule::Genre { genres: genre, op } => eq_many(&item.metadata.genre, genre, *op),
            TrackRule::HasLyrics => item
                .metadata
                .lyric_data
                .as_ref()
                .is_some_and(LyricData::has_lyrics),
            TrackRule::HasSyncedLyrics => item
                .metadata
                .lyric_data
                .as_ref()
                .is_some_and(|l| matches!(l, LyricData::Synced(_))),
            TrackRule::HasPlainLyrics => item
                .metadata
                .lyric_data
                .as_ref()
                .is_some_and(|l| matches!(l, LyricData::Plain(_))),
            TrackRule::Instrumental => item
                .metadata
                .lyric_data
                .as_ref()
                .is_some_and(LyricData::instrumental),
            TrackRule::MetadataOther { key, value, op } => item
                .metadata
                .other
                .iter()
                .any(|(k, v)| k == key && eq_single(v, value, *op)),
        }
    }
}