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<Box<str>>,
48 pub artists: Option<Box<[Box<str>]>>,
49 pub genres: Option<Box<[Box<str>]>>,
50 pub album: Option<Box<str>>,
52 pub lyrics: Option<Lyrics>,
53 pub comment: Option<Box<str>>,
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(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 #[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 #[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}