Skip to main content

selene_core/library/collection/rules/
track_rules.rs

1use std::{path::PathBuf, str::FromStr};
2
3use chrono::{DateTime, Utc};
4use lunar_lib::{error, iterator_ext::IteratorExtensions};
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    library::{
10        album::AlbumId,
11        artist::ArtistId,
12        collection::rules::{EqOp, OrdOp, Rule, eq_many, eq_single, ord_single},
13        track::{Track, TrackId, lyric_data::LyricData},
14    },
15    media_container::ContainerFormat,
16};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub enum TrackRule {
20    /// Succeeds if the track id matches the [`EqOp`] of the input ids
21    Id { id: Vec<TrackId>, op: EqOp },
22
23    /// Succeeds if the track source dir matches the [`EqOp`] of the input paths
24    SourceDir { source_dir: Vec<PathBuf>, op: EqOp },
25
26    /// Succeeds if the track library dir matches the [`EqOp`] of the input paths
27    LibraryDir { library_dir: Vec<PathBuf>, op: EqOp },
28
29    /// Succeeds if the format matches the [`EqOp`] of the input formats
30    Format {
31        format: Vec<ContainerFormat>,
32        op: EqOp,
33    },
34
35    /// Succeeds if the track duration matches the [`OrdOp`]
36    Duration { duration: f64, op: OrdOp },
37
38    /// Succeeds if the track channel count matches the [`OrdOp`]
39    ChannelCount { count: usize, op: OrdOp },
40
41    /// Succeeds if the track title DOES/DOES NOT match ALL/ANY of the input regexes
42    Title { regex: Vec<String>, op: EqOp },
43
44    /// Succeeds if the track has no album
45    Single,
46
47    /// Succeeds if the track album matches the [`EqOp`] of the input albums
48    Album { albums: Vec<AlbumId>, op: EqOp },
49
50    /// Succeeds if the track artists match the input artists using the [`EqOp`]
51    Artist { artists: Vec<ArtistId>, op: EqOp },
52
53    /// Succeeds if the track date matches the [`OrdOp`]
54    Date { date: DateTime<Utc>, op: OrdOp },
55
56    /// Succeeds if the track genres match the input genres using the [`EqOp`]
57    Genre { genres: Vec<String>, op: EqOp },
58
59    /// Succeeds if the track has any type of lyrics
60    HasLyrics,
61
62    /// Succeeds if the track has synced lyrics
63    HasSyncedLyrics,
64
65    /// Succeeds if the track has plain lyrics
66    HasPlainLyrics,
67
68    /// Succeeds if the track is instrumental
69    Instrumental,
70
71    /// Succeeds if the 'other' metadata at the given key matches the [`EqOp`] of the input value
72    MetadataOther {
73        key: String,
74        value: Vec<String>,
75        op: EqOp,
76    },
77}
78
79impl Rule for TrackRule {
80    type Item = Track;
81
82    fn matches(&self, item: &Self::Item) -> bool {
83        match self {
84            TrackRule::Id { id, op } => eq_single(&item.id(), id, *op),
85            TrackRule::SourceDir { source_dir, op } => {
86                eq_single(&item.container().path().to_path_buf(), source_dir, *op)
87            }
88            TrackRule::LibraryDir { library_dir, op } => {
89                eq_single(&item.container().path().to_path_buf(), library_dir, *op)
90            }
91            TrackRule::Format { format, op } => eq_single(item.container().format(), format, *op),
92            TrackRule::Duration { duration, op } => {
93                ord_single(item.container().stream().duration(), *duration, *op)
94            }
95            TrackRule::ChannelCount { count, op } => {
96                ord_single(&item.container().stream().codec_params.channels, count, *op)
97            }
98            TrackRule::Title { regex, op } => {
99                let regex: Vec<_> = match regex.iter().try_map(|r| Regex::from_str(r)) {
100                    Ok(v) => v.collect(),
101                    Err(err) => {
102                        error!("Failed to create regex: {err}");
103                        return false;
104                    }
105                };
106
107                item.metadata.title.as_ref().is_some_and(|t| match op {
108                    EqOp::EqAny => regex.iter().any(|r| r.is_match(t)),
109                    EqOp::EqAll => regex.iter().all(|r| r.is_match(t)),
110                    EqOp::NeqAny => !regex.iter().any(|r| r.is_match(t)),
111                    EqOp::NeqAll => !regex.iter().all(|r| r.is_match(t)),
112                })
113            }
114            TrackRule::Single => item.metadata.album.is_none(),
115            TrackRule::Album { albums: album, op } => item
116                .metadata
117                .album
118                .as_ref()
119                .is_some_and(|a| eq_single(a, album, *op)),
120            TrackRule::Artist {
121                artists: artist,
122                op,
123            } => eq_many(item.metadata.artists_raw(), artist, *op),
124            TrackRule::Date { date, op } => item
125                .metadata
126                .date
127                .is_some_and(|d| ord_single(&d, date, *op)),
128            TrackRule::Genre { genres: genre, op } => eq_many(&item.metadata.genre, genre, *op),
129            TrackRule::HasLyrics => item
130                .metadata
131                .lyric_data
132                .as_ref()
133                .is_some_and(LyricData::has_lyrics),
134            TrackRule::HasSyncedLyrics => item
135                .metadata
136                .lyric_data
137                .as_ref()
138                .is_some_and(|l| matches!(l, LyricData::Synced(_))),
139            TrackRule::HasPlainLyrics => item
140                .metadata
141                .lyric_data
142                .as_ref()
143                .is_some_and(|l| matches!(l, LyricData::Plain(_))),
144            TrackRule::Instrumental => item
145                .metadata
146                .lyric_data
147                .as_ref()
148                .is_some_and(LyricData::instrumental),
149            TrackRule::MetadataOther { key, value, op } => item
150                .metadata
151                .other
152                .iter()
153                .any(|(k, v)| k == key && eq_single(v, value, *op)),
154        }
155    }
156}