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<Box<str>>,
48    pub artists: Option<Box<[Box<str>]>>,
49    pub genres: Option<Box<[Box<str>]>>,
50    /// Name of the album the track belongs to.
51    pub album: Option<Box<str>>,
52    pub lyrics: Option<Lyrics>,
53    pub comment: Option<Box<str>>,
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(Box::new(["Unknown".to_string().into_boxed_str()]))
71                .join(", "),
72            self.title
73                .clone()
74                .unwrap_or("Unknown".to_string().into_boxed_str()),
75        )
76    }
77}
78
79impl Track {
80    pub(crate) fn new(
81        path: PathBuf,
82        tagged_file: &TaggedFile,
83        config: &music_lib::Config,
84        covers_lookup_set: &RwLock<HashSet<PathBuf>>,
85    ) -> Result<Self, String> {
86        let tag = match tagged_file.primary_tag() {
87            Some(tag) => tag,
88            None => {
89                return Err(String::from("The provided tagged file had no tags"));
90            }
91        };
92
93        let lyrics = match tag.get(lofty::tag::ItemKey::Lyrics) {
94            Some(tag_item) => match tag_item.value() {
95                ItemValue::Text(lyrics) => Some(lyrics),
96                _ => None,
97            },
98            None => None,
99        };
100
101        let lyrics = if let Some(lyrics) = lyrics {
102            match SyncedLyrics::parse(lyrics) {
103                Ok(synced_lyrics) => Some(Lyrics::Synced(Box::new(synced_lyrics))),
104                Err(e) => {
105                    let lyrics = if let Ok(sanitized) = lrc_rs::strip_tags(lyrics) {
106                        Lyrics::Unsynced {
107                            reason: e,
108                            lyrics: sanitized.to_string(),
109                        }
110                    } else {
111                        Lyrics::Unsynced {
112                            reason: e,
113                            lyrics: lyrics.to_string(),
114                        }
115                    };
116                    Some(lyrics)
117                }
118            }
119        } else {
120            None
121        };
122
123        let artists = {
124            let artists: Vec<_> = tag.get_strings(lofty::tag::ItemKey::TrackArtists).collect();
125            if artists.len() <= 1 {
126                tag.artist().map(|a| {
127                    a.split(|c| {
128                        config
129                            .value_separators
130                            .artist
131                            .iter()
132                            .any(|s| s.chars().any(|sc| sc == c))
133                    })
134                    .map(|s| s.trim().to_string().into_boxed_str())
135                    .filter(|s| !s.is_empty())
136                    .collect()
137                })
138            } else {
139                Some(
140                    artists
141                        .into_iter()
142                        .map(|s| s.trim().to_string().into_boxed_str())
143                        .collect(),
144                )
145            }
146        };
147
148        let genres: Option<Vec<_>> = tag.genre().map(|genre| {
149            genre
150                .split(|c| {
151                    config
152                        .value_separators
153                        .genre
154                        .iter()
155                        .any(|s| s.chars().any(|sc| sc == c))
156                })
157                .map(|s| s.trim().to_string().into_boxed_str())
158                .filter(|s| !s.is_empty())
159                .collect()
160        });
161
162        let cover = {
163            #[cfg(not(test))]
164            match get_track_cover(tag, config.indexer, covers_lookup_set) {
165                Ok(cover) => cover,
166                Err(e) => {
167                    warn!("Could not get the cover image: {e}");
168                    Cover::none()
169                }
170            }
171            #[cfg(test)]
172            if config.indexer.cache_mode != indexer::CacheMode::Disabled {
173                match get_track_cover(tag, config.indexer, covers_lookup_set) {
174                    Ok(cover) => cover,
175                    Err(e) => {
176                        warn!("Could not get the cover image: {e}");
177                        Cover::none()
178                    }
179                }
180            } else {
181                Cover::none()
182            }
183        };
184
185        let track = Track {
186            path,
187            cover,
188            duration: tagged_file.properties().duration(),
189            artists,
190            title: tag.title().map(|title| title.into()),
191            album: tag.album().map(|album| album.into()),
192            genres: genres.map(|g| g.into_boxed_slice()),
193            lyrics,
194            comment: tag.comment().map(|comment| comment.into()),
195            track: tag.track(),
196            track_total: tag.track_total(),
197            disc: tag.disk(),
198            disc_total: tag.disk_total(),
199            date: tag.date(),
200        };
201
202        Ok(track)
203    }
204
205    pub fn hash_self(&self) -> u64 {
206        let mut hasher = DefaultHasher::new();
207        self.hash(&mut hasher);
208        hasher.finish()
209    }
210
211    pub fn hash_track(track: &Track) -> u64 {
212        let mut hasher = DefaultHasher::new();
213        track.hash(&mut hasher);
214        hasher.finish()
215    }
216
217    pub fn hash_tracks<I, T>(tracks: I) -> Vec<u64>
218    where
219        I: IntoIterator<Item = T>,
220        T: Deref<Target = Track>,
221    {
222        tracks
223            .into_iter()
224            .map(|track| Self::hash_track(track.deref()))
225            .collect()
226    }
227
228    // TODO: Am I doing this right???
229    #[cfg(feature = "mpris")]
230    pub fn track_id(&self, position: usize) -> TrackId {
231        let mut hasher = DefaultHasher::new();
232        self.hash(&mut hasher);
233        position.hash(&mut hasher);
234        let hash = hasher.finish();
235        TrackId::try_from(format!("/org/mpris/MediaPlayer2/TrackList/queue/{hash}")).unwrap()
236    }
237
238    pub fn open_file(&self) -> std::io::Result<File> {
239        match File::open(&self.path) {
240            Ok(file) => Ok(file),
241            Err(e) => Err(e),
242        }
243    }
244
245    pub fn open_source(&self) -> Result<Decoder<BufReader<File>>, String> {
246        let file = match self.open_file() {
247            Ok(file) => file,
248            Err(e) => return Err(e.to_string()),
249        };
250        match Decoder::try_from(file) {
251            Ok(source) => Ok(source),
252            Err(e) => Err(e.to_string()),
253        }
254    }
255
256    /// Returns a [Vec] of unique [Track] structs for testing.
257    #[cfg(test)]
258    pub fn unique_tracks(size: usize) -> Vec<Track> {
259        let mut tracks = Vec::new();
260        for i in 0..size {
261            let track = Track {
262                duration: Duration::from_secs(i.try_into().unwrap()),
263                path: format!("/test/path/{i}").into(),
264                ..Default::default()
265            };
266            tracks.push(track);
267        }
268        tracks
269    }
270}