Skip to main content

chilen_backend/music_lib/state/
track.rs

1use std::{
2    collections::HashSet,
3    fs::File,
4    hash::{DefaultHasher, Hash, Hasher},
5    io::BufReader,
6    ops::Deref,
7    path::PathBuf,
8    sync::RwLock,
9    time::Duration,
10};
11
12use lofty::{
13    file::{AudioFile, TaggedFile, TaggedFileExt},
14    tag::{Accessor, ItemValue, items::Timestamp},
15};
16use log::warn;
17pub use lrc_rs::Error as LyricsError;
18pub use lrc_rs::{LRCTool, LineTag, LyricsAccess, SyncedLyrics};
19#[cfg(feature = "mpris")]
20use mpris_server::TrackId;
21use rodio::Decoder;
22
23#[cfg(test)]
24use crate::music_lib::indexer::{self};
25use crate::music_lib::{
26    self,
27    indexer::covers::{Cover, get_track_cover},
28};
29
30/// Lyrics data, can be either synced or unsynced.
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum Lyrics {
33    /// Synced lyrics parsed from the LRC format.
34    Synced(Box<SyncedLyrics>),
35    /// Unsynced lyrics as a string.
36    Unsynced { reason: LyricsError, lyrics: String },
37}
38
39#[cfg_attr(test, derive(Default))]
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub struct Track {
42    /// Path to the audio file.
43    pub path: PathBuf,
44    /// Cover art for the track.
45    pub cover: Cover,
46    pub duration: Duration,
47    pub title: Option<String>,
48    pub artists: Option<Vec<String>>,
49    pub genres: Option<Vec<String>>,
50    /// Name of the album the track belongs to.
51    pub album: Option<String>,
52    pub lyrics: Option<Lyrics>,
53    pub comment: Option<String>,
54    /// Index of the track in the album.
55    pub track: Option<u32>,
56    /// Total number of tracks in the album.
57    pub track_total: Option<u32>,
58    pub disc: Option<u32>,
59    pub disc_total: Option<u32>,
60    pub date: Option<Timestamp>,
61}
62
63impl std::fmt::Display for Track {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(
66            f,
67            "{} - {}",
68            self.artists
69                .clone()
70                .unwrap_or(vec![String::from("Unknown")])
71                .join(", "),
72            self.title.clone().unwrap_or(String::from("Unknown")),
73        )
74    }
75}
76
77impl Track {
78    pub(crate) fn new(
79        path: PathBuf,
80        tagged_file: &TaggedFile,
81        config: &music_lib::Config,
82        covers_lookup_set: &RwLock<HashSet<PathBuf>>,
83    ) -> Result<Self, String> {
84        let tag = match tagged_file.primary_tag() {
85            Some(tag) => tag,
86            None => {
87                return Err(String::from("The provided tagged file had no tags"));
88            }
89        };
90
91        let lyrics = match tag.get(lofty::tag::ItemKey::Lyrics) {
92            Some(tag_item) => match tag_item.value() {
93                ItemValue::Text(lyrics) => Some(lyrics),
94                _ => None,
95            },
96            None => None,
97        };
98
99        let lyrics = if let Some(lyrics) = lyrics {
100            match SyncedLyrics::parse(lyrics) {
101                Ok(synced_lyrics) => Some(Lyrics::Synced(Box::new(synced_lyrics))),
102                Err(e) => {
103                    let lyrics = if let Ok(sanitized) = lrc_rs::strip_tags(lyrics) {
104                        Lyrics::Unsynced {
105                            reason: e,
106                            lyrics: sanitized.to_string(),
107                        }
108                    } else {
109                        Lyrics::Unsynced {
110                            reason: e,
111                            lyrics: lyrics.to_string(),
112                        }
113                    };
114                    Some(lyrics)
115                }
116            }
117        } else {
118            None
119        };
120
121        let artists = {
122            let artists: Vec<_> = tag.get_strings(lofty::tag::ItemKey::TrackArtists).collect();
123            if artists.len() <= 1 {
124                tag.artist().map(|a| {
125                    a.split(|c| {
126                        config
127                            .value_separators
128                            .artist
129                            .iter()
130                            .any(|s| s.chars().any(|sc| sc == c))
131                    })
132                    .map(|s| s.trim().to_string())
133                    .filter(|s| !s.is_empty())
134                    .collect()
135                })
136            } else {
137                Some(artists.into_iter().map(|s| s.trim().to_string()).collect())
138            }
139        };
140
141        let genres: Option<Vec<_>> = tag.genre().map(|genre| {
142            genre
143                .split(|c| {
144                    config
145                        .value_separators
146                        .genre
147                        .iter()
148                        .any(|s| s.chars().any(|sc| sc == c))
149                })
150                .map(|s| s.trim().to_string())
151                .filter(|s| !s.is_empty())
152                .collect()
153        });
154
155        let cover = {
156            #[cfg(not(test))]
157            match get_track_cover(tag, config.indexer, covers_lookup_set) {
158                Ok(cover) => cover,
159                Err(e) => {
160                    warn!("Could not get the cover image: {e}");
161                    Cover::none()
162                }
163            }
164            #[cfg(test)]
165            if config.indexer.cache_mode != indexer::CacheMode::Disabled {
166                match get_track_cover(tag, config.indexer, covers_lookup_set) {
167                    Ok(cover) => cover,
168                    Err(e) => {
169                        warn!("Could not get the cover image: {e}");
170                        Cover::none()
171                    }
172                }
173            } else {
174                Cover::none()
175            }
176        };
177
178        let track = Track {
179            path,
180            cover,
181            duration: tagged_file.properties().duration(),
182            artists,
183            title: tag.title().map(|title| title.into()),
184            album: tag.album().map(|album| album.into()),
185            genres,
186            lyrics,
187            comment: tag.comment().map(|comment| comment.into()),
188            track: tag.track(),
189            track_total: tag.track_total(),
190            disc: tag.disk(),
191            disc_total: tag.disk_total(),
192            date: tag.date(),
193        };
194
195        Ok(track)
196    }
197
198    pub fn hash_self(&self) -> u64 {
199        let mut hasher = DefaultHasher::new();
200        self.hash(&mut hasher);
201        hasher.finish()
202    }
203
204    pub fn hash_track(track: &Track) -> u64 {
205        let mut hasher = DefaultHasher::new();
206        track.hash(&mut hasher);
207        hasher.finish()
208    }
209
210    pub fn hash_tracks<I, T>(tracks: I) -> Vec<u64>
211    where
212        I: IntoIterator<Item = T>,
213        T: Deref<Target = Track>,
214    {
215        tracks
216            .into_iter()
217            .map(|track| Self::hash_track(track.deref()))
218            .collect()
219    }
220
221    // TODO: Am I doing this right???
222    #[cfg(feature = "mpris")]
223    pub fn track_id(&self, position: usize) -> TrackId {
224        let mut hasher = DefaultHasher::new();
225        self.hash(&mut hasher);
226        position.hash(&mut hasher);
227        let hash = hasher.finish();
228        TrackId::try_from(format!("/org/mpris/MediaPlayer2/TrackList/queue/{hash}")).unwrap()
229    }
230
231    #[cfg(feature = "mpris")]
232    pub fn get_meta(&self, position: usize) -> mpris_server::Metadata {
233        use mpris_server::{Time, builder::MetadataBuilder};
234
235        let builder = MetadataBuilder::default()
236            .length(Time::from_nanos(
237                self.duration.as_nanos().try_into().unwrap_or(i64::MAX),
238            ))
239            .url(self.path.to_string_lossy())
240            .artist(
241                self.artists
242                    .clone()
243                    .unwrap_or(vec!["Unknown artist".to_string()]),
244            )
245            .title(self.title.clone().unwrap_or_default())
246            .album(self.album.clone().unwrap_or_default())
247            .genre(
248                self.genres
249                    .clone()
250                    .unwrap_or(vec!["Unknown genre".to_string()]),
251            )
252            .comment(self.comment.clone())
253            .track_number(self.track.unwrap_or(0).try_into().unwrap_or(0))
254            .disc_number(self.disc.unwrap_or(0).try_into().unwrap_or(0))
255            .lyrics(match &self.lyrics {
256                Some(lyrics) => {
257                    use lrc_rs::LyricsAccess;
258
259                    match lyrics {
260                        Lyrics::Synced(synced) => synced.clone().to_unsynced(),
261                        Lyrics::Unsynced { lyrics, reason: _ } => lyrics.to_string(),
262                    }
263                }
264                None => String::new(),
265            })
266            .trackid(self.track_id(position));
267
268        let builder = match &self.cover.hires {
269            Some(cover) => builder.art_url(cover.to_string_lossy().to_string()),
270            None => builder,
271        };
272
273        builder.build()
274    }
275
276    pub fn open_file(&self) -> std::io::Result<File> {
277        match File::open(&self.path) {
278            Ok(file) => Ok(file),
279            Err(e) => Err(e),
280        }
281    }
282
283    pub fn open_source(&self) -> Result<Decoder<BufReader<File>>, String> {
284        let file = match self.open_file() {
285            Ok(file) => file,
286            Err(e) => return Err(e.to_string()),
287        };
288        match Decoder::try_from(file) {
289            Ok(source) => Ok(source),
290            Err(e) => Err(e.to_string()),
291        }
292    }
293
294    /// Returns a [Vec] of unique [Track] structs for testing.
295    #[cfg(test)]
296    pub fn unique_tracks(size: usize) -> Vec<Track> {
297        let mut tracks = Vec::new();
298        for i in 0..size {
299            let track = Track {
300                duration: Duration::from_secs(i.try_into().unwrap()),
301                path: format!("/test/path/{i}").into(),
302                ..Default::default()
303            };
304            tracks.push(track);
305        }
306        tracks
307    }
308}