1use std::{
2 collections::{HashMap, HashSet},
3 fs::{File, read},
4 hash::{DefaultHasher, Hash, Hasher},
5 io::{BufReader, Write},
6 path::{Path, PathBuf},
7 sync::{Arc, LazyLock, RwLock},
8 time::{Duration, SystemTime},
9};
10
11use lofty::{
12 file::{AudioFile, TaggedFile, TaggedFileExt},
13 tag::{Accessor, ItemValue, Tag, items::Timestamp},
14};
15use log::{error, trace};
16use lrc_rs::SyncedLyrics;
17#[cfg(feature = "mpris")]
18use mpris_server::TrackId;
19use rmp_serde::{Deserializer, Serializer};
20use rodio::Decoder;
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 Error, Event,
25 music_lib::{
26 DATA_DIR,
27 covers::{CoverError, LoadMode, get_track_cover},
28 indexer::{self},
29 tracks_from_m3u8,
30 },
31};
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum Lyrics {
36 Synced(Box<SyncedLyrics>),
38 Unsynced(String),
40}
41
42#[cfg_attr(test, derive(Default))]
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct Track {
45 pub path: PathBuf,
46 pub cover_path: Option<PathBuf>,
47 pub duration: Duration,
48 pub artist: Option<String>,
49 pub title: Option<String>,
50 pub album: Option<String>,
51 pub genre: Option<String>,
52 pub lyrics: Option<Lyrics>,
53 pub comment: Option<String>,
54 pub track: Option<u32>,
55 pub track_total: Option<u32>,
56 pub disc: Option<u32>,
57 pub disc_total: Option<u32>,
58 pub date: Option<Timestamp>,
59}
60
61impl std::fmt::Display for Track {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(
64 f,
65 "{} - {}",
66 self.artist.clone().unwrap_or(String::from("Unknown")),
67 self.title.clone().unwrap_or(String::from("Unknown")),
68 )
69 }
70}
71
72impl TryFrom<&TaggedFile> for Track {
73 type Error = String;
74 fn try_from(value: &TaggedFile) -> Result<Self, Self::Error> {
75 let tag = match value.primary_tag() {
76 Some(tag) => tag,
77 None => {
78 return Err(String::from("The provided tagged file had no tags"));
79 }
80 };
81
82 let lyrics = match tag.get(lofty::tag::ItemKey::Lyrics) {
83 Some(tag_item) => match tag_item.value() {
84 ItemValue::Text(lyrics) => Some(lyrics),
85 _ => None,
86 },
87 None => None,
88 };
89
90 let lyrics = if let Some(lyrics) = lyrics {
91 match SyncedLyrics::parse(lyrics) {
92 Ok(synced_lyrics) => Some(Lyrics::Synced(Box::new(synced_lyrics))),
93 Err(_) => Some(Lyrics::Unsynced(lyrics.to_string())),
94 }
95 } else {
96 None
97 };
98
99 Ok(Track {
100 path: PathBuf::new(),
101 cover_path: None,
102 duration: value.properties().duration(),
103 artist: tag.artist().map(|artist| artist.into()),
104 title: tag.title().map(|title| title.into()),
105 album: tag.album().map(|album| album.into()),
106 genre: tag.genre().map(|genre| genre.into()),
107 lyrics,
108 comment: tag.comment().map(|comment| comment.into()),
109 track: tag.track(),
110 track_total: tag.track_total(),
111 disc: tag.disk(),
112 disc_total: tag.disk_total(),
113 date: tag.date(),
114 })
115 }
116}
117
118impl Track {
119 pub fn get_cover(&mut self, tag: &Tag) -> Result<(), CoverError> {
121 self.cover_path = Some(get_track_cover(self, tag, &LoadMode::Load)?);
122 Ok(())
123 }
124
125 pub fn extract_cover(&mut self, tag: &Tag) -> Result<(), CoverError> {
127 self.cover_path = Some(get_track_cover(self, tag, &LoadMode::Rebuild)?);
128 Ok(())
129 }
130
131 pub fn hash_self(&self) -> u64 {
132 let mut hasher = DefaultHasher::new();
133 self.hash(&mut hasher);
134 hasher.finish()
135 }
136
137 pub fn hash_track(track: &Track) -> u64 {
138 let mut hasher = DefaultHasher::new();
139 track.hash(&mut hasher);
140 hasher.finish()
141 }
142
143 pub fn hash_tracks(tracks: &Vec<Track>) -> Vec<u64> {
144 let mut hashes = Vec::new();
145 for track in tracks {
146 hashes.push(Self::hash_track(track));
147 }
148 hashes
149 }
150
151 #[cfg(feature = "mpris")]
153 pub fn track_id(&self, position: usize) -> TrackId {
154 let mut hasher = DefaultHasher::new();
155 self.hash(&mut hasher);
156 position.hash(&mut hasher);
157 let hash = hasher.finish();
158 TrackId::try_from(format!("/org/mpris/MediaPlayer2/TrackList/queue/{hash}")).unwrap()
159 }
160
161 #[cfg(feature = "mpris")]
162 pub fn get_meta(&self, position: usize) -> mpris_server::Metadata {
163 use mpris_server::{Time, builder::MetadataBuilder};
164
165 MetadataBuilder::default()
166 .length(Time::from_nanos(
167 self.duration.as_nanos().try_into().unwrap_or(i64::MAX),
168 ))
169 .url(self.path.to_string_lossy())
170 .art_url(
171 self.cover_path
172 .clone()
173 .unwrap_or_default()
174 .to_string_lossy(),
175 )
176 .artist(self.artist.clone())
177 .title(self.title.clone().unwrap_or_default())
178 .album(self.album.clone().unwrap_or_default())
179 .genre(self.genre.clone())
180 .comment(self.comment.clone())
181 .track_number(self.track.unwrap_or(0).try_into().unwrap_or(0))
182 .disc_number(self.disc.unwrap_or(0).try_into().unwrap_or(0))
183 .lyrics(match &self.lyrics {
184 Some(lyrics) => {
185 use lrc_rs::LyricsAccess;
186
187 match lyrics {
188 Lyrics::Synced(synced) => synced.clone().to_unsynced(),
189 Lyrics::Unsynced(unsynced) => unsynced.to_string(),
190 }
191 }
192 None => String::new(),
193 })
194 .trackid(self.track_id(position))
195 .build()
196 }
197
198 pub fn open_file(&self) -> std::io::Result<File> {
199 match File::open(&self.path) {
200 Ok(file) => Ok(file),
201 Err(e) => Err(e),
202 }
203 }
204
205 pub fn open_source(&self) -> Result<Decoder<BufReader<File>>, String> {
206 let file = match self.open_file() {
207 Ok(file) => file,
208 Err(e) => return Err(e.to_string()),
209 };
210 match Decoder::try_from(file) {
211 Ok(source) => Ok(source),
212 Err(e) => Err(e.to_string()),
213 }
214 }
215
216 #[cfg(test)]
218 pub fn unique_tracks(size: usize) -> Vec<Track> {
219 let mut tracks = Vec::new();
220 for i in 0..size {
221 let track = Track {
222 duration: Duration::from_secs(i.try_into().unwrap()),
223 path: format!("/test/path/{i}").into(),
224 ..Default::default()
225 };
226 tracks.push(track);
227 }
228 tracks
229 }
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Hash)]
233pub struct Playlist {
234 pub name: String,
235 pub tracks: Vec<Arc<Track>>,
236}
237
238impl Playlist {
239 fn try_from_loaded_playlist(lib: &MusicLibrary, loaded: ConfPlaylist) -> Result<Self, Error> {
240 Ok(Self {
241 name: loaded.name,
242 tracks: lib.tracks_from_hashes(loaded.track_hashes)?,
243 })
244 }
245
246 pub(crate) fn remove_tracks(&mut self, mut ids: Vec<usize>) -> Result<(), Error> {
247 ids.sort();
248 let mut unique = ids.clone();
249 unique.dedup();
250 if unique != ids {
251 return Err(Error::DuplicateItems);
252 }
253 if ids.iter().find(|i| i >= &&self.tracks.len()).is_some() {
254 return Err(Error::IndexOutOfBounds);
255 }
256 let remove_set: HashSet<usize> = ids.into_iter().collect();
257 self.tracks = self
258 .tracks
259 .drain(..)
260 .enumerate()
261 .filter_map(|(i, t)| {
262 if remove_set.contains(&i) {
263 None
264 } else {
265 Some(t)
266 }
267 })
268 .collect();
269 Ok(())
270 }
271}
272
273#[derive(Clone, Debug, Serialize, Deserialize)]
274struct ConfPlaylist {
275 name: String,
276 track_hashes: Vec<u64>,
277}
278
279impl From<Playlist> for ConfPlaylist {
280 fn from(value: Playlist) -> Self {
281 Self {
282 name: value.name,
283 track_hashes: value.tracks.iter().map(|t| t.hash_self()).collect(),
284 }
285 }
286}
287
288#[derive(Clone, Debug, PartialEq)]
289pub struct MusicLibrary {
290 pub playlists: HashSet<Arc<Playlist>>,
291 pub tracks: HashSet<Arc<Track>>,
292 tracks_by_path: HashMap<String, Arc<Track>>,
293 tracks_by_hash: HashMap<u64, Arc<Track>>,
294 playlists_by_name: HashMap<String, Arc<Playlist>>,
295}
296
297impl MusicLibrary {
298 fn try_from_loaded_lib(
299 loaded: ConfMusicLibrary,
300 tracks: HashSet<Track>,
301 ) -> Result<Self, Error> {
302 let tracks: HashSet<Arc<Track>> = tracks.into_iter().map(Arc::new).collect();
303 let playlists: HashSet<Arc<Playlist>> = HashSet::new();
304
305 let mut path_map: HashMap<_, _> = HashMap::with_capacity(tracks.len());
306 for t in tracks.iter() {
307 path_map.insert(t.path.to_string_lossy().to_string(), t.clone());
308 }
309
310 let mut hash_map: HashMap<_, _> = HashMap::with_capacity(tracks.len());
311 for t in tracks.iter() {
312 hash_map.insert(t.hash_self(), t.clone());
313 }
314
315 let mut lib = Self {
316 playlists,
317 tracks,
318 tracks_by_path: path_map,
319 tracks_by_hash: hash_map,
320 playlists_by_name: HashMap::new(),
321 };
322
323 for p in loaded.playlists {
324 let playlist = Arc::new(Playlist::try_from_loaded_playlist(&lib, p)?);
325 lib.playlists.insert(playlist.clone());
326 lib.playlists_by_name
327 .insert(playlist.name.clone(), playlist);
328 }
329
330 Ok(lib)
331 }
332
333 fn check_name(&self, name: &str) -> Result<(), Error> {
334 let name = name.trim();
335 if name.is_empty() {
336 return Err(Error::EmptyName);
337 }
338 if self.find_playlist(name).is_some() {
339 error!("A playlist with name \"{name}\" already exists");
340 return Err(Error::PlaylistExists);
341 }
342 Ok(())
343 }
344
345 pub(crate) fn new_from_tracks(tracks: Vec<Track>) -> Self {
346 let tracks: HashSet<Arc<Track>> = tracks.into_iter().map(Arc::new).collect();
347 let playlists: HashSet<Arc<Playlist>> = HashSet::new();
348
349 let mut path_map: HashMap<_, _> = HashMap::with_capacity(tracks.len());
350 for t in tracks.iter() {
351 path_map.insert(t.path.to_string_lossy().to_string(), t.clone());
352 }
353
354 let mut hash_map: HashMap<_, _> = HashMap::with_capacity(tracks.len());
355 for t in tracks.iter() {
356 hash_map.insert(t.hash_self(), t.clone());
357 }
358
359 Self {
360 playlists,
361 tracks,
362 tracks_by_path: path_map,
363 tracks_by_hash: hash_map,
364 playlists_by_name: HashMap::new(),
365 }
366 }
367
368 pub fn find_playlist(&self, name: &str) -> Option<&Arc<Playlist>> {
369 self.playlists_by_name.get(name)
370 }
371
372 pub fn find_track_by_path(&self, path: &Path) -> Option<Arc<Track>> {
373 self.tracks_by_path
374 .get(&path.to_string_lossy().to_string())
375 .cloned()
376 }
377
378 pub fn get_default_playlist_name(&self) -> String {
383 let mut i = 0;
384 let mut playlist_name = DEFAULT_PLAYLIST_NAME.to_string();
385 while self.find_playlist(&playlist_name).is_some() {
386 i += 1;
387 playlist_name = format!("{DEFAULT_PLAYLIST_NAME} {i}");
388 }
389 playlist_name
390 }
391
392 pub fn tracks_from_hashes(&self, hashes: Vec<u64>) -> Result<Vec<Arc<Track>>, Error> {
393 let mut tracks = Vec::with_capacity(hashes.len());
394 for hash in hashes {
395 match self.tracks_by_hash.get(&hash) {
396 Some(track) => tracks.push(track.clone()),
397 None => return Err(Error::UnknownTrack),
398 }
399 }
400
401 Ok(tracks)
402 }
403
404 pub fn remove_playlists(&mut self, mut playlists: Vec<String>) -> Result<(), Error> {
405 playlists.sort();
406 let mut unique = playlists.clone();
407 unique.dedup();
408 if unique != playlists {
409 return Err(Error::DuplicateItems);
410 }
411
412 for name in &playlists {
413 if let Some(playlist) = self.find_playlist(name).cloned() {
414 self.playlists.remove(&playlist);
415 self.playlists_by_name.remove(name);
416 } else {
417 return Err(Error::UnknownPlaylist(name.to_string()));
418 }
419 }
420
421 crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
422 Ok(())
423 }
424
425 pub fn create_playlist(
426 &mut self,
427 name: String,
428 track_paths: &Option<Vec<PathBuf>>,
429 ) -> Result<(), Error> {
430 trace!("Creating a new playlist \"{name}\" from a list of tracks");
431
432 let name = name.trim();
433 self.check_name(name)?;
434
435 let tracks = if let Some(tracks) = track_paths {
436 let mut out = Vec::with_capacity(tracks.len());
437 for path in tracks {
438 if let Some(track) = self.find_track_by_path(path) {
439 out.push(track);
440 } else {
441 error!("The track {path:?} was not found in the music library");
442 return Err(Error::UnknownTrack);
443 }
444 }
445 out
446 } else {
447 Vec::new()
448 };
449
450 let playlist = Arc::new(Playlist {
451 name: name.to_string(),
452 tracks,
453 });
454 self.playlists.insert(playlist.clone());
455 self.playlists_by_name.insert(name.to_string(), playlist);
456 crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
457 Ok(())
458 }
459
460 pub fn rename_playlist(&mut self, source: &str, target: &str) -> Result<(), Error> {
461 let source = source.trim();
462 let target = target.trim();
463 trace!("Renaming playlist \"{source}\" to \"{target}\"!");
464 self.check_name(target)?;
465
466 let source_playlist = if let Some(source) = self.find_playlist(source) {
467 source.clone()
468 } else {
469 return Err(Error::UnknownPlaylist(source.to_string()));
470 };
471 let mut playlist = source_playlist.as_ref().clone();
472 playlist.name = target.to_string();
473
474 self.playlists.remove(&source_playlist);
475 self.playlists_by_name.remove(source);
476 let playlist = Arc::new(playlist);
477 self.playlists.insert(playlist.clone());
478 self.playlists_by_name.insert(target.to_string(), playlist);
479
480 crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
481 Ok(())
482 }
483
484 pub fn add_tracks(&mut self, name: &str, tracks: Vec<PathBuf>) -> Result<(), Error> {
485 let name = name.trim();
486 trace!("Adding tracks to playlist \"{name}\"");
487
488 let mut playlist = match self.find_playlist(name) {
489 Some(playlist) => playlist.clone(),
490 None => return Err(Error::UnknownPlaylist(name.to_string())),
491 }
492 .as_ref()
493 .clone();
494
495 self.playlists.remove(&playlist);
496 self.playlists_by_name.remove(&playlist.name);
497
498 let mut out = Vec::with_capacity(tracks.len());
499 for path in tracks {
500 if let Some(track) = self.find_track_by_path(&path) {
501 out.push(track.clone());
502 } else {
503 return Err(Error::UnknownTrack);
504 }
505 }
506
507 playlist.tracks.append(&mut out);
508
509 let playlist = Arc::new(playlist);
510 self.playlists.insert(playlist.clone());
511 self.playlists_by_name
512 .insert(playlist.name.clone(), playlist);
513
514 crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
515 Ok(())
516 }
517
518 pub fn remove_tracks(&mut self, name: &str, tracks: Vec<usize>) -> Result<(), Error> {
519 let name = name.trim();
520 trace!("Removing tracks from playlist \"{name}\"");
521
522 let mut playlist = match self.find_playlist(name) {
523 Some(playlist) => playlist,
524 None => return Err(Error::UnknownPlaylist(name.to_string())),
525 }
526 .as_ref()
527 .clone();
528
529 self.playlists.remove(&playlist);
530 self.playlists_by_name.remove(&playlist.name);
531
532 playlist.remove_tracks(tracks)?;
533 let playlist = Arc::new(playlist);
534 self.playlists.insert(playlist.clone());
535 self.playlists_by_name
536 .insert(playlist.name.clone(), playlist);
537
538 crate::send_event(Event::LibraryChanged(Box::new(self.clone())));
539 Ok(())
540 }
541
542 pub fn import_m3u8_playlist(
544 &mut self,
545 path: &PathBuf,
546 name: Option<String>,
547 ) -> Result<(), Error> {
548 trace!("Importing a playlist from an M3U8 file at {path:?}");
549 let tracks = tracks_from_m3u8(path)?;
550 let name = match name {
551 Some(n) => n,
552 None => {
553 if let Some(path) = path.file_name() {
554 let path = path.to_string_lossy().to_string();
555 path.strip_suffix(".m3u8")
556 .unwrap_or(path.strip_suffix(".m3u").unwrap_or(&path))
557 .to_string()
558 } else {
559 self.get_default_playlist_name()
560 }
561 }
562 };
563 self.create_playlist(name, &Some(tracks))
564 }
565}
566
567#[derive(Clone, Debug, Serialize, Deserialize)]
568struct ConfMusicLibrary {
569 playlists: Vec<ConfPlaylist>,
570}
571
572impl From<MusicLibrary> for ConfMusicLibrary {
573 fn from(value: MusicLibrary) -> Self {
574 Self {
575 playlists: value
576 .playlists
577 .into_iter()
578 .map(|t| t.as_ref().clone().into())
579 .collect(),
580 }
581 }
582}
583
584const DEFAULT_PLAYLIST_NAME: &str = "New Playlist";
585
586static LIBRARY_FILE: LazyLock<PathBuf> = LazyLock::new(|| {
587 let mut data = DATA_DIR.read().unwrap().clone().unwrap();
588 data.push("playlists");
589 data
590});
591
592pub(crate) static MUSIC_LIBRARY: RwLock<Option<MusicLibrary>> = RwLock::new(None);
593
594pub(crate) fn unwrap_lib_ref(maybe_lib: Option<&MusicLibrary>) -> Result<&MusicLibrary, Error> {
595 match maybe_lib {
596 Some(lib) => Ok(lib),
597 None => Err(Error::LibraryNotInitialized),
598 }
599}
600
601pub(crate) fn unwrap_lib_mut(
602 maybe_lib: Option<&mut MusicLibrary>,
603) -> Result<&mut MusicLibrary, Error> {
604 match maybe_lib {
605 Some(lib) => Ok(lib),
606 None => Err(Error::LibraryNotInitialized),
607 }
608}
609
610#[cfg(test)]
611pub(crate) fn get_library() -> Result<MusicLibrary, Error> {
612 let guard = MUSIC_LIBRARY.read().unwrap();
613 unwrap_lib_ref(guard.as_ref()).cloned()
614}
615
616pub(crate) fn save_library() -> Result<(), Error> {
618 trace!("Saving the library state");
619
620 let lib = MUSIC_LIBRARY.read().unwrap().clone();
621
622 if let Some(lib_data) = lib {
623 let lib = ConfMusicLibrary::from(lib_data.clone());
624
625 let library_state = LIBRARY_FILE.clone();
626
627 let mut data = Vec::new();
628 lib.serialize(&mut Serializer::new(&mut data)).unwrap();
629
630 let mut file = match File::create(library_state) {
631 Ok(file) => file,
632 Err(e) => {
633 error!("Could not open the library state in write-only mode: {e}");
634 return Err(Error::StateWriteFailed);
635 }
636 };
637
638 match file.write_all(&data) {
639 Ok(_) => Ok(()),
640 Err(e) => {
641 error!("Could not write to the library: {e}");
642 Err(Error::StateWriteFailed)
643 }
644 }
645 } else {
646 error!("Cannot save the library since it is uninitialized!");
647 Err(Error::LibraryNotInitialized)
648 }
649}
650
651pub(crate) fn load(load_mode: LoadMode) -> Result<(), Error> {
654 trace!("Loading the music library");
655
656 let time_start = SystemTime::now();
657
658 let tracks = match indexer::index(load_mode) {
659 Ok(tracks) => tracks,
660 Err(e) => {
661 crate::send_event(Event::LibraryLoadFailed(e.to_string()));
662 return Err(e);
663 }
664 };
665
666 let time_elapsed = time_start.elapsed().unwrap_or(Duration::from_secs(0));
667 trace!(
668 "Finished indexing the music directory in {:.2}s, found {} audio files",
669 time_elapsed.as_secs_f64(),
670 tracks.len()
671 );
672
673 let library_state = LIBRARY_FILE.clone();
674
675 trace!("Loading the library state from {library_state:?}");
676
677 let exists = match library_state.try_exists() {
678 Ok(exists) => exists,
679 Err(e) => {
680 error!("Could not check if the library exists: {e}");
681 crate::send_event(Event::LibraryLoadFailed(e.to_string()));
682 return Err(Error::StateNotReadable);
683 }
684 };
685
686 if exists {
687 trace!("Restoring the library state");
688
689 if !library_state.is_file() {
690 error!("The item at {library_state:?} must be a file!");
691 crate::send_event(Event::LibraryLoadFailed(format!(
692 "The item at {library_state:?} must be a file!"
693 )));
694 return Err(Error::StateNotAFile);
695 }
696
697 let data = match read(library_state) {
698 Ok(data) => data,
699 Err(e) => {
700 error!("Could not read the library: {e}");
701 crate::send_event(Event::LibraryLoadFailed(e.to_string()));
702 return Err(Error::StateNotReadable);
703 }
704 };
705
706 let lib = match ConfMusicLibrary::deserialize(&mut Deserializer::from_read_ref(&data)) {
707 Ok(data) => {
708 match MusicLibrary::try_from_loaded_lib(data, tracks.clone().into_iter().collect())
709 {
710 Ok(lib) => lib,
711 Err(e) => {
712 error!("Could not open the music library: {e}");
713 MusicLibrary::new_from_tracks(tracks.into_iter().collect())
714 }
715 }
716 }
717 Err(e) => {
718 error!("Could not decode the contents of the library state file: {e}");
719 trace!("Creating a new library");
720 MusicLibrary::new_from_tracks(tracks.into_iter().collect())
721 }
722 };
723
724 *MUSIC_LIBRARY.write().unwrap() = Some(lib);
725 } else {
726 trace!("The library file does not exist, creating a new library");
727 *MUSIC_LIBRARY.write().unwrap() = Some(MusicLibrary::new_from_tracks(tracks));
728 }
729 crate::send_event(Event::LibraryChanged(Box::new(
730 MUSIC_LIBRARY.read().unwrap().as_ref().unwrap().clone(),
731 )));
732 save_library()?;
733
734 let time_elapsed = time_start.elapsed().unwrap_or(Duration::from_secs(0));
735 trace!(
736 "Done loading the music library in {:.2}s",
737 time_elapsed.as_secs_f64()
738 );
739
740 Ok(())
741}