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;
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::{ChannelLayout, Codec, 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 { ids: Vec<TrackId>, op: EqOp },
22
23    /// Succeeds if the track source dir matches the [`EqOp`] of the input paths
24    SourceDir { source_dirs: Vec<PathBuf>, op: EqOp },
25
26    /// Succeeds if the track library dir matches the [`EqOp`] of the input paths
27    LibraryDir {
28        library_dirs: Vec<PathBuf>,
29        op: EqOp,
30    },
31
32    /// Succeeds if the format matches the [`EqOp`] of the input formats
33    Format {
34        formats: Vec<ContainerFormat>,
35        op: EqOp,
36    },
37
38    /// Succeeds if the codec matches the [`EqOp`] of the input codecs
39    Codec { codecs: Vec<Codec>, op: EqOp },
40
41    /// Succeeds if the track duration matches the [`OrdOp`]
42    Duration { duration: f64, op: OrdOp },
43
44    /// Succeeds if the track channel count matches the [`OrdOp`]
45    ChannelCount { count: usize, op: OrdOp },
46
47    /// Succeeds if the channel layout matches the [`EqOp`] of the input channel layouts
48    ChannelLayout {
49        layouts: Vec<ChannelLayout>,
50        op: EqOp,
51    },
52
53    /// Succeeds if the track title DOES/DOES NOT match ALL/ANY of the input regexes
54    Title { regex: Vec<String>, op: EqOp },
55
56    /// Succeeds if the track has no album
57    Single,
58
59    /// Succeeds if the track album matches the [`EqOp`] of the input albums
60    Album { albums: Vec<AlbumId>, op: EqOp },
61
62    /// Succeeds if the track artists match the input artists using the [`EqOp`]
63    Artist { artists: Vec<ArtistId>, op: EqOp },
64
65    /// Succeeds if the track date matches the [`OrdOp`]
66    Date { date: DateTime<Utc>, op: OrdOp },
67
68    /// Succeeds if the track genres match the input genres using the [`EqOp`]
69    Genre { genres: Vec<String>, op: EqOp },
70
71    /// Succeeds if the track has any type of lyrics
72    HasLyrics,
73
74    /// Succeeds if the track has synced lyrics
75    HasSyncedLyrics,
76
77    /// Succeeds if the track has plain lyrics
78    HasPlainLyrics,
79
80    /// Succeeds if the track is instrumental
81    Instrumental,
82
83    /// Succeeds if the 'other' metadata at the given key matches the [`EqOp`] of the input value
84    MetadataOther {
85        key: String,
86        value: Vec<String>,
87        op: EqOp,
88    },
89}
90
91impl Rule for TrackRule {
92    type Item = Track;
93
94    fn matches(&self, item: &Self::Item) -> bool {
95        match self {
96            TrackRule::Id { ids: id, op } => eq_single(&item.id(), id, *op),
97            TrackRule::SourceDir {
98                source_dirs: source_dir,
99                op,
100            } => eq_single(&item.src_container().path().to_path_buf(), source_dir, *op),
101            TrackRule::LibraryDir {
102                library_dirs: library_dir,
103                op,
104            } => item.lib_container().is_some_and(|container| {
105                eq_single(&container.path().to_path_buf(), library_dir, *op)
106            }),
107            TrackRule::Format {
108                formats: format,
109                op,
110            } => item
111                .lib_container()
112                .is_some_and(|container| eq_single(container.container(), format, *op)),
113            TrackRule::Codec { codecs: codec, op } => item
114                .lib_container()
115                .is_some_and(|container| eq_single(container.codec(), codec, *op)),
116            TrackRule::Duration { duration, op } => item
117                .lib_container()
118                .and_then(|c| c.stream().duration())
119                .is_some_and(|c| ord_single(c, *duration, *op)),
120            TrackRule::ChannelCount { count, op } => item
121                .lib_container()
122                .map(|c| c.stream().channel_count())
123                .is_some_and(|c| ord_single(c, *count, *op)),
124            TrackRule::ChannelLayout {
125                layouts: layout,
126                op,
127            } => item.lib_container().is_some_and(|container| {
128                container
129                    .stream()
130                    .channel_layout()
131                    .is_some_and(|channel_layout| eq_single(channel_layout, layout, *op))
132            }),
133            TrackRule::Title { regex, op } => {
134                let regex: Vec<_> = match regex
135                    .iter()
136                    .map(|r| Regex::from_str(r))
137                    .collect::<Result<Vec<_>, _>>()
138                {
139                    Ok(v) => v,
140                    Err(err) => {
141                        error!("Failed to create regex: {err}");
142                        return false;
143                    }
144                };
145
146                item.metadata.title.as_ref().is_some_and(|t| match op {
147                    EqOp::EqAny => regex.iter().any(|r| r.is_match(t)),
148                    EqOp::EqAll => regex.iter().all(|r| r.is_match(t)),
149                    EqOp::NeqAny => !regex.iter().any(|r| r.is_match(t)),
150                    EqOp::NeqAll => !regex.iter().all(|r| r.is_match(t)),
151                })
152            }
153            TrackRule::Single => item.metadata.album.is_none(),
154            TrackRule::Album { albums: album, op } => item
155                .metadata
156                .album
157                .as_ref()
158                .is_some_and(|a| eq_single(a, album, *op)),
159            TrackRule::Artist {
160                artists: artist,
161                op,
162            } => eq_many(item.metadata.artists.artist_ids(), artist, *op),
163            TrackRule::Date { date, op } => item
164                .metadata
165                .date
166                .is_some_and(|d| ord_single(&d, date, *op)),
167            TrackRule::Genre { genres: genre, op } => eq_many(&item.metadata.genre, genre, *op),
168            TrackRule::HasLyrics => item
169                .metadata
170                .lyric_data
171                .as_ref()
172                .is_some_and(LyricData::has_lyrics),
173            TrackRule::HasSyncedLyrics => item
174                .metadata
175                .lyric_data
176                .as_ref()
177                .is_some_and(|l| matches!(l, LyricData::Synced(_))),
178            TrackRule::HasPlainLyrics => item
179                .metadata
180                .lyric_data
181                .as_ref()
182                .is_some_and(|l| matches!(l, LyricData::Plain(_))),
183            TrackRule::Instrumental => item
184                .metadata
185                .lyric_data
186                .as_ref()
187                .is_some_and(LyricData::instrumental),
188            TrackRule::MetadataOther { key, value, op } => item
189                .metadata
190                .other
191                .iter()
192                .any(|(k, v)| k == key && eq_single(v, value, *op)),
193        }
194    }
195}