Skip to main content

chilen_backend/music_lib/
mod.rs

1pub mod indexer;
2pub mod sorting;
3
4mod state;
5#[cfg(test)]
6mod tests;
7
8use std::{
9    fs::{File, create_dir_all, read},
10    io::Write,
11    path::{Path, PathBuf},
12    sync::{Arc, RwLock},
13};
14
15use log::{error, trace};
16use m3u8_rs::{MediaPlaylist, MediaSegment};
17use serde::{Deserialize, Serialize};
18
19use crate::Error;
20
21pub use state::*;
22
23pub(crate) static DATA_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
24pub(crate) static MUSIC_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
25pub(crate) static CACHE_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
26
27pub trait AsTrack {
28    fn as_track(&self) -> &Track;
29}
30impl AsTrack for Arc<Track> {
31    fn as_track(&self) -> &Track {
32        self
33    }
34}
35impl AsTrack for &Arc<Track> {
36    fn as_track(&self) -> &Track {
37        self
38    }
39}
40impl AsTrack for &Track {
41    fn as_track(&self) -> &Track {
42        self
43    }
44}
45
46pub trait AsAlbum {
47    fn as_album(&self) -> &Album;
48}
49impl AsAlbum for Arc<Album> {
50    fn as_album(&self) -> &Album {
51        self
52    }
53}
54impl AsAlbum for &Arc<Album> {
55    fn as_album(&self) -> &Album {
56        self
57    }
58}
59impl AsAlbum for &Album {
60    fn as_album(&self) -> &Album {
61        self
62    }
63}
64
65pub trait AsArtist {
66    fn as_artist(&self) -> &Artist;
67}
68impl AsArtist for Arc<Artist> {
69    fn as_artist(&self) -> &Artist {
70        self
71    }
72}
73impl AsArtist for &Arc<Artist> {
74    fn as_artist(&self) -> &Artist {
75        self
76    }
77}
78impl AsArtist for &Artist {
79    fn as_artist(&self) -> &Artist {
80        self
81    }
82}
83
84pub trait AsPlaylist {
85    fn as_playlist(&self) -> &Playlist;
86}
87impl AsPlaylist for Arc<Playlist> {
88    fn as_playlist(&self) -> &Playlist {
89        self
90    }
91}
92impl AsPlaylist for &Arc<Playlist> {
93    fn as_playlist(&self) -> &Playlist {
94        self
95    }
96}
97impl AsPlaylist for &Playlist {
98    fn as_playlist(&self) -> &Playlist {
99        self
100    }
101}
102
103pub trait AsGenre {
104    fn as_genre(&self) -> &Genre;
105}
106impl AsGenre for Arc<Genre> {
107    fn as_genre(&self) -> &Genre {
108        self
109    }
110}
111impl AsGenre for &Arc<Genre> {
112    fn as_genre(&self) -> &Genre {
113        self
114    }
115}
116impl AsGenre for &Genre {
117    fn as_genre(&self) -> &Genre {
118        self
119    }
120}
121
122#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
123pub struct ValueSeparators {
124    pub artist: Vec<String>,
125    pub genre: Vec<String>,
126}
127
128impl ValueSeparators {
129    pub fn new(separators: Vec<String>) -> Self {
130        Self {
131            artist: separators.clone(),
132            genre: separators,
133        }
134    }
135}
136
137#[cfg_attr(test, derive(Default))]
138#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
139pub struct Config {
140    pub value_separators: ValueSeparators,
141    pub indexer: indexer::Config,
142}
143
144fn init_dir(dir: &PathBuf) -> Result<(), Error> {
145    if dir.is_dir() {
146        let perms = match dir.metadata() {
147            Ok(md) => md.permissions(),
148            Err(e) => {
149                error!("Could not read the metadata of {dir:?}: {e}");
150                return Err(Error::PathInaccessible(dir.to_path_buf()));
151            }
152        };
153        if perms.readonly() {
154            error!("The directory {dir:?} is readonly");
155            return Err(Error::PathReadonly(dir.to_path_buf()));
156        }
157        Ok(())
158    } else {
159        let exists = match dir.try_exists() {
160            Ok(exists) => exists,
161            Err(e) => {
162                error!("Can't check whether {dir:?} exists: {e}");
163                return Err(Error::PathInaccessible(dir.to_path_buf()));
164            }
165        };
166        if exists {
167            error!("The path is not a directory: {dir:?}");
168            Err(Error::NotADirectory(dir.to_path_buf()))
169        } else {
170            trace!("The directory at {dir:?} does not exist. Attempting to create a new one");
171            if let Err(e) = create_dir_all(dir) {
172                error!("Could not create the directory: {e}");
173                return Err(Error::DirectoryCreationFailed(
174                    dir.to_path_buf(),
175                    e.to_string(),
176                ));
177            }
178            trace!("Created a new directory at {dir:?}");
179            Ok(())
180        }
181    }
182}
183
184pub(crate) fn set_dirs(
185    data_dir: PathBuf,
186    cache_dir: PathBuf,
187    music_dir: PathBuf,
188) -> Result<(), Error> {
189    init_dir(&cache_dir)?;
190    init_dir(&data_dir)?;
191    if music_dir.is_dir() {
192        if let Err(e) = music_dir.metadata() {
193            error!("Could not read the metadata of {:?}: {e}", music_dir);
194            return Err(Error::PathInaccessible(music_dir));
195        }
196    } else {
197        error!("The music library is not a directory or does not exist: {music_dir:?}");
198        return Err(Error::NotADirectory(music_dir));
199    }
200    *DATA_DIR.write().unwrap() = Some(data_dir);
201    *CACHE_DIR.write().unwrap() = Some(cache_dir);
202    *MUSIC_DIR.write().unwrap() = Some(music_dir);
203
204    trace!("Successfully set paths");
205
206    Ok(())
207}
208
209/// Find indexed tracks in the music library by their paths.
210///
211/// # Fails
212/// The function will fail if any of the provided paths are not present in the music library, and
213/// the `allow_failure` argument is set to `false`.
214///
215/// If the `allow_failure` argument is `true`, the function will iterate over all the provided paths
216/// and return only those that correspond to tracks in the music library. It might return an empty
217/// vector if no paths could be matched.
218///
219/// It might also fail if the music library is not initialized, so you should never run `unwrap`
220/// on the result of this function.
221pub(crate) fn tracks_from_paths(
222    track_paths: &[PathBuf],
223    // Whether nonexistent paths should result in a failure.
224    allow_failure: bool,
225) -> Result<Vec<Arc<Track>>, Error> {
226    let guard = MUSIC_LIBRARY.read().unwrap();
227    let lib = unwrap_lib_ref(guard.as_ref())?;
228    let mut out = Vec::with_capacity(track_paths.len());
229
230    for path in track_paths {
231        if let Some(track) = lib.find_track_by_path(&path.to_string_lossy()) {
232            out.push(track);
233        } else if !allow_failure {
234            return Err(Error::UnknownTrackPath(path.to_path_buf()));
235        }
236    }
237
238    Ok(out)
239}
240
241pub(crate) fn find_tracks_by_hashes(hashes: Vec<u64>) -> Result<HashMatchingResult, Error> {
242    let guard = MUSIC_LIBRARY.read().unwrap();
243    let lib = unwrap_lib_ref(guard.as_ref())?;
244    Ok(lib.find_tracks_by_hashes(&hashes))
245}
246
247// TEST: Check if import works correctly
248pub(crate) fn tracks_from_m3u8(path: &PathBuf) -> Result<Vec<PathBuf>, Error> {
249    trace!("Loading an M3U8 playlist from {path:?}");
250    let data = match read(path) {
251        Ok(data) => data,
252        Err(e) => {
253            error!("Could not read the playlist file at {path:?}: {e}");
254            return Err(Error::PathDoesNotExist);
255        }
256    };
257    let content = String::from_utf8_lossy(&data);
258    match m3u8_rs::parser::parse_media_playlist(&content) {
259        Ok((_, pl)) => {
260            let base_path = path.parent().unwrap_or(Path::new("./"));
261            let track_paths: Vec<_> = pl
262                .segments
263                .into_iter()
264                .map(|s| {
265                    let mut path = PathBuf::from(base_path);
266                    path.push(s.uri.components());
267                    path
268                })
269                .collect();
270            Ok(track_paths)
271        }
272        Err(e) => {
273            error!("Could not parse the M3U playlist: {e}");
274            Err(Error::PlaylistParsingError)
275        }
276    }
277}
278
279// TODO: Reject empty playlists
280// TEST: Check if export work correctly
281pub fn export_playlist_to_m3u8(name: &str, path: &Path) -> Result<(), Error> {
282    trace!("Exporting a playlist to an M3U8 file in {path:?}");
283    let guard = MUSIC_LIBRARY.read().unwrap();
284    let lib = unwrap_lib_ref(guard.as_ref())?;
285    let pl = match lib.find_playlist_by_name(name) {
286        Some(pl) => pl,
287        None => return Err(Error::UnknownPlaylist(name.to_string())),
288    };
289    let music_dir = MUSIC_DIR.read().unwrap().as_ref().unwrap().clone();
290    let segments = pl
291        .tracks
292        .iter()
293        .map(|t| MediaSegment {
294            uri: {
295                if path.parent() == Some(&music_dir) {
296                    match t.path.strip_prefix(music_dir.clone()) {
297                        Ok(path) => path.to_path_buf(),
298                        Err(_) => path.to_path_buf(),
299                    }
300                } else {
301                    t.path.clone()
302                }
303            },
304            duration: t.duration,
305            title: t.title.clone().map(|t| t.into_string()),
306        })
307        .collect();
308    drop(guard);
309    let media_playlist = MediaPlaylist { segments };
310    let mut file = match File::create(path) {
311        Ok(file) => file,
312        Err(e) => {
313            error!("Could not open the m3u8 export file for writing: {e}");
314            return Err(Error::PlaylistExportFailed);
315        }
316    };
317    match file.write_all(media_playlist.serialize().as_bytes()) {
318        Ok(_) => Ok(()),
319        Err(e) => {
320            error!("Could not write the playlist contents to a file: {e}");
321            Err(Error::PlaylistExportFailed)
322        }
323    }
324}
325
326pub fn create_playlist(name: &str, track_paths: &Option<Vec<PathBuf>>) -> Result<(), Error> {
327    let mut guard = MUSIC_LIBRARY.write().unwrap();
328    let lib = unwrap_lib_mut(guard.as_mut())?;
329    let name = name.trim();
330    let name = if name.is_empty() {
331        lib.get_default_playlist_name()
332    } else {
333        name.to_string()
334    };
335    lib.create_playlist(&name, track_paths)?;
336
337    drop(guard);
338    save_library()
339}
340
341pub fn rename_playlist(source: &str, target: &str) -> Result<(), Error> {
342    let mut guard = MUSIC_LIBRARY.write().unwrap();
343    let lib = unwrap_lib_mut(guard.as_mut())?;
344    lib.rename_playlist(source, target)?;
345    drop(guard);
346    save_library()
347}
348
349pub fn import_playlist_from_m3u8(name: Option<String>, file: &PathBuf) -> Result<(), Error> {
350    let mut guard = MUSIC_LIBRARY.write().unwrap();
351    let lib = unwrap_lib_mut(guard.as_mut())?;
352    lib.import_m3u8_playlist(file, name)?;
353
354    drop(guard);
355    save_library()
356}
357
358pub fn delete_playlists(playlists: Vec<&str>) -> Result<(), Error> {
359    let mut guard = MUSIC_LIBRARY.write().unwrap();
360    let lib = unwrap_lib_mut(guard.as_mut())?;
361    lib.remove_playlists(playlists)?;
362
363    drop(guard);
364    save_library()
365}
366
367pub fn add_tracks(playlist: &str, tracks: &[u64]) -> Result<(), Error> {
368    let mut guard = MUSIC_LIBRARY.write().unwrap();
369    let lib = unwrap_lib_mut(guard.as_mut())?;
370    lib.add_tracks(playlist, tracks)?;
371
372    drop(guard);
373    save_library()
374}
375
376/// Remove tracks by indices from a playlist.
377pub fn remove_tracks_by_indices(playlist: &str, tracks: Vec<usize>) -> Result<(), Error> {
378    let mut guard = MUSIC_LIBRARY.write().unwrap();
379    let lib = unwrap_lib_mut(guard.as_mut())?;
380    lib.remove_tracks(playlist, tracks)?;
381
382    drop(guard);
383    save_library()?;
384    Ok(())
385}
386
387/// Adds a new favorite item to the library.
388///
389/// Fails if the library is not initialized.
390pub fn add_favorite(item: impl Into<LibraryItem>) -> Result<(), Error> {
391    trace!("Adding an item to favorites");
392
393    let mut guard = MUSIC_LIBRARY.write().unwrap();
394    let lib = unwrap_lib_mut(guard.as_mut())?;
395    lib.add_favorite(item.into())?;
396    drop(guard);
397    save_library()
398}
399
400/// Removes an item from the favorites list.
401///
402/// Fails if the library is not initialized.
403pub fn remove_favorite(item: &LibraryItem) -> Result<(), Error> {
404    trace!("Removing an item from favorites");
405
406    let mut guard = MUSIC_LIBRARY.write().unwrap();
407    let lib = unwrap_lib_mut(guard.as_mut())?;
408    lib.remove_favorite(item)?;
409
410    drop(guard);
411    save_library()
412}
413
414/// Adds an item to the favorites list if it's not present there, or removes it if it is.
415///
416/// Fails if the library is not initialized.
417pub fn toggle_favorite(item: impl Into<LibraryItem>) -> Result<(), Error> {
418    let item = item.into();
419
420    let mut guard = MUSIC_LIBRARY.write().unwrap();
421    let lib = unwrap_lib_mut(guard.as_mut())?;
422
423    if lib.favorites.contains(&item) {
424        lib.remove_favorite(&item)?;
425    } else {
426        lib.add_favorite(item)?;
427    }
428
429    drop(guard);
430    save_library()
431}
432
433/// Checks whether the given item is in the favorites list.
434///
435/// Fails if the library is not initialized.
436pub fn is_favorite(item: &LibraryItem) -> Result<bool, Error> {
437    let mut guard = MUSIC_LIBRARY.write().unwrap();
438    let lib = unwrap_lib_mut(guard.as_mut())?;
439    Ok(lib.favorites.contains(item))
440}