Skip to main content

chilen_backend/music_lib/
state.rs

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/// Lyrics data, can be either synced or unsynced.
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum Lyrics {
36    /// Synced lyrics parsed from the LRC format.
37    Synced(Box<SyncedLyrics>),
38    /// Unsynced lyrics as a string.
39    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    /// Set the cover art from cache or extract it from the source file.
120    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    /// Extract the cover art from the source file discarding the cache contents.
126    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    // TODO: Am I doing this right???
152    #[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    /// Returns a [Vec] of unique [Track] structs for testing.
217    #[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    /// Returns the default playlist name ("New Playlist").
379    ///
380    /// If a playlist with the default name exists, then a number will be added to the end of the
381    /// playlist name so it's unique, eg. "New Playlist 1", "New Playlist 2", etc.
382    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    // TEST: Check if importing M3U8 files works correctly
543    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
616/// Save the library state to a file.
617pub(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
651// FIX: This function hangs if if the `MusicLibrary` struct changes (and can't be deserialized)
652/// Load the music library from the playlists file.
653pub(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}