Skip to main content

chilen_backend/music_lib/state/
mod.rs

1mod library;
2mod playlist;
3mod track;
4
5use std::{
6    fs::{File, read},
7    io::Write,
8    path::PathBuf,
9    sync::{LazyLock, RwLock},
10    time::{Duration, SystemTime},
11};
12
13use log::{error, trace};
14use rmp_serde::{Deserializer, Serializer};
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    Error, Event,
19    music_lib::{
20        self, DATA_DIR,
21        indexer::{self},
22    },
23};
24
25pub use library::*;
26pub use playlist::*;
27pub use track::*;
28
29#[derive(Debug, Clone, PartialEq)]
30pub enum Progress {
31    /// First step of the loading process.
32    ///
33    /// The indexer finds files in the music library to index.
34    ///
35    /// This step usually takes under 100ms.
36    FindingTracks,
37
38    /// Second step of the loading process.
39    ///
40    /// The indexer goes through all of the found files and attempts to gather audio metadata from
41    /// them. During this process track covers are also cached.
42    ///
43    /// This is the longest step of the loading process. The time it takes to complete this step
44    /// heavily depends on on whether track covers are already cached (warm start) or not (cold
45    /// start), and how many tracks are in the library.
46    ///
47    /// Eg. for my 908-track music library, cold start takes about 4.24s, and warm start just 0.26s.
48    Indexing { progress: f32 },
49
50    /// Third step of the loading process.
51    ///
52    /// After all the tracks have been gathered, the backend rebuilds the virtual library from the
53    /// track tags. It rebuilds artists, albums and genres, as well as hash maps used to quickly
54    /// find items in the music library.
55    ///
56    /// Usually takes under a 100ms, depending on library size.
57    RebuildingLibrary,
58
59    /// Fourth step of the loading process.
60    ///
61    /// The playlists are restored from disk.
62    ///
63    /// **NOTE**: This step is skipped if the `playlists` file is not present.
64    RestoringState,
65
66    /// The loading process has finished.
67    Done,
68
69    /// The loading process has failed irrecoverably and will not restart.
70    Failed(Error),
71}
72
73static LIBRARY_FILE: LazyLock<PathBuf> = LazyLock::new(|| {
74    let mut data = DATA_DIR.read().unwrap().clone().unwrap();
75    data.push("playlists");
76    data
77});
78
79pub(crate) static MUSIC_LIBRARY: RwLock<Option<MusicLibrary>> = RwLock::new(None);
80
81pub(crate) fn unwrap_lib_ref(maybe_lib: Option<&MusicLibrary>) -> Result<&MusicLibrary, Error> {
82    match maybe_lib {
83        Some(lib) => Ok(lib),
84        None => Err(Error::LibraryNotInitialized),
85    }
86}
87
88pub(crate) fn unwrap_lib_mut(
89    maybe_lib: Option<&mut MusicLibrary>,
90) -> Result<&mut MusicLibrary, Error> {
91    match maybe_lib {
92        Some(lib) => Ok(lib),
93        None => Err(Error::LibraryNotInitialized),
94    }
95}
96
97#[cfg(test)]
98pub(crate) fn get_library() -> Result<MusicLibrary, Error> {
99    let guard = MUSIC_LIBRARY.read().unwrap();
100    unwrap_lib_ref(guard.as_ref()).cloned()
101}
102
103/// Save the library state to a file.
104pub(crate) fn save_library() -> Result<(), Error> {
105    trace!("Saving the library state");
106
107    let lib = MUSIC_LIBRARY.read().unwrap().clone();
108
109    if let Some(lib_data) = lib {
110        let lib = ConfMusicLibrary::from(lib_data.clone());
111
112        let library_state = LIBRARY_FILE.clone();
113
114        let mut data = Vec::new();
115        lib.serialize(&mut Serializer::new(&mut data)).unwrap();
116
117        let mut file = match File::create(library_state) {
118            Ok(file) => file,
119            Err(e) => {
120                error!("Could not open the library state in write-only mode: {e}");
121                return Err(Error::StateWriteFailed);
122            }
123        };
124
125        match file.write_all(&data) {
126            Ok(_) => Ok(()),
127            Err(e) => {
128                error!("Could not write to the library: {e}");
129                Err(Error::StateWriteFailed)
130            }
131        }
132    } else {
133        error!("Cannot save the library since it is uninitialized!");
134        Err(Error::LibraryNotInitialized)
135    }
136}
137
138// FIX: This function hangs if if the `MusicLibrary` struct changes (and can't be deserialized)
139/// Load the music library from the playlists file.
140pub(crate) fn load(config: music_lib::Config) -> Result<(), Error> {
141    trace!("Loading the music library");
142
143    let time_start = SystemTime::now();
144
145    let tracks = match indexer::index(config.clone()) {
146        Ok(tracks) => tracks,
147        Err(e) => {
148            crate::send_event(Event::LibraryLoadFailed(e.to_string()));
149            return Err(e);
150        }
151    };
152
153    let time_elapsed = time_start.elapsed().unwrap_or(Duration::from_secs(0));
154    trace!(
155        "Finished indexing the music directory in {:.2}s, found {} audio files",
156        time_elapsed.as_secs_f64(),
157        tracks.len()
158    );
159
160    let library_state = LIBRARY_FILE.clone();
161
162    trace!("Loading the library state from {library_state:?}");
163
164    let exists = match library_state.try_exists() {
165        Ok(exists) => exists,
166        Err(e) => {
167            error!("Could not check if the library exists: {e}");
168            crate::send_event(Event::LibraryLoadFailed(e.to_string()));
169            return Err(Error::StateNotReadable);
170        }
171    };
172
173    if exists {
174        trace!("Restoring the library state");
175
176        if !library_state.is_file() {
177            error!("The item at {library_state:?} must be a file!");
178            crate::send_event(Event::LibraryLoadFailed(format!(
179                "The item at {library_state:?} must be a file!"
180            )));
181            return Err(Error::StateNotAFile);
182        }
183
184        let data = match read(library_state) {
185            Ok(data) => data,
186            Err(e) => {
187                error!("Could not read the library: {e}");
188                crate::send_event(Event::LibraryLoadFailed(e.to_string()));
189                return Err(Error::StateNotReadable);
190            }
191        };
192
193        let lib = match ConfMusicLibrary::deserialize(&mut Deserializer::from_read_ref(&data)) {
194            Ok(lib) => {
195                MusicLibrary::load(lib, tracks.clone().into_iter().collect(), config.indexer)
196            }
197            Err(e) => {
198                error!("Could not decode the contents of the library state file: {e}");
199                trace!("Creating a new library");
200                MusicLibrary::new(tracks.into_iter().collect())
201            }
202        };
203
204        *MUSIC_LIBRARY.write().unwrap() = Some(lib);
205    } else {
206        trace!("The library file does not exist, creating a new library");
207        *MUSIC_LIBRARY.write().unwrap() = Some(MusicLibrary::new(tracks));
208    }
209    crate::send_event(Event::LibraryChanged(Box::new(
210        MUSIC_LIBRARY.read().unwrap().as_ref().unwrap().clone(),
211    )));
212    save_library()?;
213
214    let time_elapsed = time_start.elapsed().unwrap_or(Duration::from_secs(0));
215    trace!(
216        "Done loading the music library in {:.2}s",
217        time_elapsed.as_secs_f64()
218    );
219
220    crate::send_event(Event::LoadProgressChanged(Progress::Done));
221
222    Ok(())
223}