chilen_backend/music_lib/state/
track.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum Lyrics {
33 Synced(Box<SyncedLyrics>),
35 Unsynced { reason: LyricsError, lyrics: String },
37}
38
39#[cfg_attr(test, derive(Default))]
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub struct Track {
42 pub path: PathBuf,
44 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 pub album: Option<String>,
52 pub lyrics: Option<Lyrics>,
53 pub comment: Option<String>,
54 pub track: Option<u32>,
56 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 #[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 #[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}