Skip to main content

chilen_backend/music_lib/
mod.rs

1pub(crate) mod covers;
2pub(crate) mod indexer;
3pub mod state;
4#[cfg(test)]
5mod tests;
6
7use std::{
8    fs::{File, create_dir_all, read},
9    io::Write,
10    path::{Path, PathBuf},
11    sync::{Arc, RwLock},
12};
13
14use log::{error, trace};
15use m3u8_rs::{MediaPlaylist, MediaSegment};
16
17use crate::{
18    Error,
19    music_lib::state::{MUSIC_LIBRARY, Track, save_library, unwrap_lib_mut, unwrap_lib_ref},
20};
21
22pub(crate) static DATA_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
23pub(crate) static MUSIC_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
24pub(crate) static CACHE_DIR: RwLock<Option<PathBuf>> = RwLock::new(None);
25
26fn init_dir(dir: &PathBuf) -> Result<(), Error> {
27    if dir.is_dir() {
28        let perms = match dir.metadata() {
29            Ok(md) => md.permissions(),
30            Err(e) => {
31                error!("Could not read the metadata of {dir:?}: {e}");
32                return Err(Error::PathInaccessible(dir.to_path_buf()));
33            }
34        };
35        if perms.readonly() {
36            error!("The directory {dir:?} is readonly");
37            return Err(Error::PathReadonly(dir.to_path_buf()));
38        }
39        Ok(())
40    } else {
41        let exists = match dir.try_exists() {
42            Ok(exists) => exists,
43            Err(e) => {
44                error!("Can't check whether {dir:?} exists: {e}");
45                return Err(Error::PathInaccessible(dir.to_path_buf()));
46            }
47        };
48        if exists {
49            error!("The path is not a directory: {dir:?}");
50            Err(Error::NotADirectory(dir.to_path_buf()))
51        } else {
52            trace!("The directory at {dir:?} does not exist. Attempting to create a new one");
53            if let Err(e) = create_dir_all(dir) {
54                error!("Could not create the directory: {e}");
55                return Err(Error::DirectoryCreationFailed(
56                    dir.to_path_buf(),
57                    e.to_string(),
58                ));
59            }
60            trace!("Created a new directory at {dir:?}");
61            Ok(())
62        }
63    }
64}
65
66pub(crate) fn set_dirs(
67    data_dir: PathBuf,
68    cache_dir: PathBuf,
69    music_dir: PathBuf,
70) -> Result<(), Error> {
71    init_dir(&cache_dir)?;
72    init_dir(&data_dir)?;
73    if music_dir.is_dir() {
74        if let Err(e) = music_dir.metadata() {
75            error!("Could not read the metadata of {:?}: {e}", music_dir);
76            return Err(Error::PathInaccessible(music_dir));
77        }
78    } else {
79        error!("The music library is not a directory or does not exist: {music_dir:?}");
80        return Err(Error::NotADirectory(music_dir));
81    }
82    *DATA_DIR.write().unwrap() = Some(data_dir);
83    *CACHE_DIR.write().unwrap() = Some(cache_dir);
84    *MUSIC_DIR.write().unwrap() = Some(music_dir);
85
86    trace!("Successfully set paths");
87
88    Ok(())
89}
90
91/// Find indexed tracks in the music library by their paths.
92///
93/// # Fails
94/// The function will fail if any of the provided paths are not present in the music library, and
95/// the `allow_failure` argument is set to `false`.
96///
97/// If the `allow_failure` argument is `true`, the function will iterate over all the provided paths
98/// and return only those that correspond to tracks in the music library. It might return an empty
99/// vector if no paths could be matched.
100///
101/// It might also fail if the music library is not initialized, so you should never run `unwrap`
102/// on the result of this function.
103pub(crate) fn tracks_from_paths(
104    track_paths: &[PathBuf],
105    // Whether nonexistent paths should result in a failure.
106    allow_failure: bool,
107) -> Result<Vec<Track>, Error> {
108    let guard = MUSIC_LIBRARY.read().unwrap();
109    let lib = unwrap_lib_ref(guard.as_ref())?;
110    let mut out = Vec::with_capacity(track_paths.len());
111
112    for path in track_paths {
113        if let Some(track) = lib.find_track_by_path(path) {
114            out.push(track.as_ref().clone());
115        } else if !allow_failure {
116            return Err(Error::UnknownTrack);
117        }
118    }
119
120    Ok(out)
121}
122
123pub(crate) fn tracks_from_hashes(hashes: Vec<u64>) -> Result<Vec<Arc<Track>>, Error> {
124    let guard = MUSIC_LIBRARY.read().unwrap();
125    let lib = unwrap_lib_ref(guard.as_ref())?;
126    lib.tracks_from_hashes(hashes)
127}
128
129// TEST: Check if import works correctly
130pub(crate) fn tracks_from_m3u8(path: &PathBuf) -> Result<Vec<PathBuf>, Error> {
131    trace!("Loading an M3U8 playlist from {path:?}");
132    let data = match read(path) {
133        Ok(data) => data,
134        Err(e) => {
135            error!("Could not read the playlist file at {path:?}: {e}");
136            return Err(Error::PathDoesNotExist);
137        }
138    };
139    let content = String::from_utf8_lossy(&data);
140    match m3u8_rs::parser::parse_media_playlist(&content) {
141        Ok((_, pl)) => {
142            let base_path = path.parent().unwrap_or(Path::new("./"));
143            let track_paths: Vec<_> = pl
144                .segments
145                .into_iter()
146                .map(|s| {
147                    let mut path = PathBuf::from(base_path);
148                    path.push(s.uri.components());
149                    path
150                })
151                .collect();
152            Ok(track_paths)
153        }
154        Err(e) => {
155            error!("Could not parse the M3U playlist: {e}");
156            Err(Error::PlaylistParsingError)
157        }
158    }
159}
160
161// TEST: Check if export work correctly
162pub fn export_playlist_to_m3u8(name: String, path: &Path) -> Result<(), Error> {
163    trace!("Exporting a playlist to an M3U8 file in {path:?}");
164    let guard = MUSIC_LIBRARY.read().unwrap();
165    let lib = unwrap_lib_ref(guard.as_ref())?;
166    let pl = match lib.find_playlist(&name) {
167        Some(pl) => pl,
168        None => return Err(Error::UnknownPlaylist(name)),
169    };
170    let music_dir = MUSIC_DIR.read().unwrap().as_ref().unwrap().clone();
171    let segments = pl
172        .tracks
173        .iter()
174        .map(|t| MediaSegment {
175            uri: {
176                if path.parent() == Some(&music_dir) {
177                    match t.path.strip_prefix(music_dir.clone()) {
178                        Ok(path) => path.to_path_buf(),
179                        Err(_) => path.to_path_buf(),
180                    }
181                } else {
182                    t.path.clone()
183                }
184            },
185            duration: t.duration,
186            title: t.title.clone(),
187        })
188        .collect();
189    drop(guard);
190    let media_playlist = MediaPlaylist { segments };
191    let mut file = match File::create(path) {
192        Ok(file) => file,
193        Err(e) => {
194            error!("Could not open the m3u8 export file for writing: {e}");
195            return Err(Error::PlaylistExportFailed);
196        }
197    };
198    match file.write_all(media_playlist.serialize().as_bytes()) {
199        Ok(_) => Ok(()),
200        Err(e) => {
201            error!("Could not write the playlist contents to a file: {e}");
202            Err(Error::PlaylistExportFailed)
203        }
204    }
205}
206
207pub fn create_playlist(name: String, track_paths: &Option<Vec<PathBuf>>) -> Result<(), Error> {
208    let mut guard = MUSIC_LIBRARY.write().unwrap();
209    let lib = unwrap_lib_mut(guard.as_mut())?;
210    let name = name.trim();
211    let name = if name.is_empty() {
212        lib.get_default_playlist_name()
213    } else {
214        name.to_string()
215    };
216    lib.create_playlist(name, track_paths)?;
217
218    drop(guard);
219    save_library()
220}
221
222pub fn rename_playlist(source: &str, target: &str) -> Result<(), Error> {
223    let mut guard = MUSIC_LIBRARY.write().unwrap();
224    let lib = unwrap_lib_mut(guard.as_mut())?;
225    lib.rename_playlist(source, target)?;
226    drop(guard);
227    save_library()
228}
229
230pub fn import_playlist_from_m3u8(name: Option<String>, file: &PathBuf) -> Result<(), Error> {
231    let mut guard = MUSIC_LIBRARY.write().unwrap();
232    let lib = unwrap_lib_mut(guard.as_mut())?;
233    lib.import_m3u8_playlist(file, name)?;
234
235    drop(guard);
236    save_library()
237}
238
239pub fn delete_playlists(playlists: Vec<String>) -> Result<(), Error> {
240    let mut guard = MUSIC_LIBRARY.write().unwrap();
241    let lib = unwrap_lib_mut(guard.as_mut())?;
242    lib.remove_playlists(playlists)?;
243
244    drop(guard);
245    save_library()
246}
247
248pub fn add_tracks(playlist: &str, tracks: Vec<PathBuf>) -> Result<(), Error> {
249    let mut guard = MUSIC_LIBRARY.write().unwrap();
250    let lib = unwrap_lib_mut(guard.as_mut())?;
251    lib.add_tracks(playlist, tracks)?;
252
253    drop(guard);
254    save_library()
255}
256
257/// Remove tracks by indices from a playlist.
258pub fn remove_tracks(playlist: &str, tracks: Vec<usize>) -> Result<(), Error> {
259    let mut guard = MUSIC_LIBRARY.write().unwrap();
260    let lib = unwrap_lib_mut(guard.as_mut())?;
261    lib.remove_tracks(playlist, tracks)?;
262
263    drop(guard);
264    save_library()?;
265    Ok(())
266}