chilen_backend/music_lib/
mod.rs1pub mod indexer;
2
3mod 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};
16use serde::{Deserialize, Serialize};
17
18use crate::Error;
19
20pub use state::*;
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
26#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct ValueSeparators {
28 pub artist: Vec<String>,
29 pub genre: Vec<String>,
30}
31
32impl ValueSeparators {
33 pub fn new(separators: Vec<String>) -> Self {
34 Self {
35 artist: separators.clone(),
36 genre: separators,
37 }
38 }
39}
40
41#[cfg_attr(test, derive(Default))]
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub struct Config {
44 pub value_separators: ValueSeparators,
45 pub indexer: indexer::Config,
46}
47
48fn init_dir(dir: &PathBuf) -> Result<(), Error> {
49 if dir.is_dir() {
50 let perms = match dir.metadata() {
51 Ok(md) => md.permissions(),
52 Err(e) => {
53 error!("Could not read the metadata of {dir:?}: {e}");
54 return Err(Error::PathInaccessible(dir.to_path_buf()));
55 }
56 };
57 if perms.readonly() {
58 error!("The directory {dir:?} is readonly");
59 return Err(Error::PathReadonly(dir.to_path_buf()));
60 }
61 Ok(())
62 } else {
63 let exists = match dir.try_exists() {
64 Ok(exists) => exists,
65 Err(e) => {
66 error!("Can't check whether {dir:?} exists: {e}");
67 return Err(Error::PathInaccessible(dir.to_path_buf()));
68 }
69 };
70 if exists {
71 error!("The path is not a directory: {dir:?}");
72 Err(Error::NotADirectory(dir.to_path_buf()))
73 } else {
74 trace!("The directory at {dir:?} does not exist. Attempting to create a new one");
75 if let Err(e) = create_dir_all(dir) {
76 error!("Could not create the directory: {e}");
77 return Err(Error::DirectoryCreationFailed(
78 dir.to_path_buf(),
79 e.to_string(),
80 ));
81 }
82 trace!("Created a new directory at {dir:?}");
83 Ok(())
84 }
85 }
86}
87
88pub(crate) fn set_dirs(
89 data_dir: PathBuf,
90 cache_dir: PathBuf,
91 music_dir: PathBuf,
92) -> Result<(), Error> {
93 init_dir(&cache_dir)?;
94 init_dir(&data_dir)?;
95 if music_dir.is_dir() {
96 if let Err(e) = music_dir.metadata() {
97 error!("Could not read the metadata of {:?}: {e}", music_dir);
98 return Err(Error::PathInaccessible(music_dir));
99 }
100 } else {
101 error!("The music library is not a directory or does not exist: {music_dir:?}");
102 return Err(Error::NotADirectory(music_dir));
103 }
104 *DATA_DIR.write().unwrap() = Some(data_dir);
105 *CACHE_DIR.write().unwrap() = Some(cache_dir);
106 *MUSIC_DIR.write().unwrap() = Some(music_dir);
107
108 trace!("Successfully set paths");
109
110 Ok(())
111}
112
113pub(crate) fn tracks_from_paths(
126 track_paths: &[PathBuf],
127 allow_failure: bool,
129) -> Result<Vec<Arc<Track>>, Error> {
130 let guard = MUSIC_LIBRARY.read().unwrap();
131 let lib = unwrap_lib_ref(guard.as_ref())?;
132 let mut out = Vec::with_capacity(track_paths.len());
133
134 for path in track_paths {
135 if let Some(track) = lib.find_track_by_path(path) {
136 out.push(track);
137 } else if !allow_failure {
138 return Err(Error::UnknownTrackPath(path.to_path_buf()));
139 }
140 }
141
142 Ok(out)
143}
144
145pub(crate) fn tracks_from_hashes(hashes: Vec<u64>) -> Result<HashMatchingResult, Error> {
146 let guard = MUSIC_LIBRARY.read().unwrap();
147 let lib = unwrap_lib_ref(guard.as_ref())?;
148 Ok(lib.tracks_from_hashes(hashes))
149}
150
151pub(crate) fn tracks_from_m3u8(path: &PathBuf) -> Result<Vec<PathBuf>, Error> {
153 trace!("Loading an M3U8 playlist from {path:?}");
154 let data = match read(path) {
155 Ok(data) => data,
156 Err(e) => {
157 error!("Could not read the playlist file at {path:?}: {e}");
158 return Err(Error::PathDoesNotExist);
159 }
160 };
161 let content = String::from_utf8_lossy(&data);
162 match m3u8_rs::parser::parse_media_playlist(&content) {
163 Ok((_, pl)) => {
164 let base_path = path.parent().unwrap_or(Path::new("./"));
165 let track_paths: Vec<_> = pl
166 .segments
167 .into_iter()
168 .map(|s| {
169 let mut path = PathBuf::from(base_path);
170 path.push(s.uri.components());
171 path
172 })
173 .collect();
174 Ok(track_paths)
175 }
176 Err(e) => {
177 error!("Could not parse the M3U playlist: {e}");
178 Err(Error::PlaylistParsingError)
179 }
180 }
181}
182
183pub fn export_playlist_to_m3u8(name: &str, path: &Path) -> Result<(), Error> {
186 trace!("Exporting a playlist to an M3U8 file in {path:?}");
187 let guard = MUSIC_LIBRARY.read().unwrap();
188 let lib = unwrap_lib_ref(guard.as_ref())?;
189 let pl = match lib.find_playlist_by_name(name) {
190 Some(pl) => pl,
191 None => return Err(Error::UnknownPlaylist(name.to_string())),
192 };
193 let music_dir = MUSIC_DIR.read().unwrap().as_ref().unwrap().clone();
194 let segments = pl
195 .tracks
196 .iter()
197 .map(|t| MediaSegment {
198 uri: {
199 if path.parent() == Some(&music_dir) {
200 match t.path.strip_prefix(music_dir.clone()) {
201 Ok(path) => path.to_path_buf(),
202 Err(_) => path.to_path_buf(),
203 }
204 } else {
205 t.path.clone()
206 }
207 },
208 duration: t.duration,
209 title: t.title.clone(),
210 })
211 .collect();
212 drop(guard);
213 let media_playlist = MediaPlaylist { segments };
214 let mut file = match File::create(path) {
215 Ok(file) => file,
216 Err(e) => {
217 error!("Could not open the m3u8 export file for writing: {e}");
218 return Err(Error::PlaylistExportFailed);
219 }
220 };
221 match file.write_all(media_playlist.serialize().as_bytes()) {
222 Ok(_) => Ok(()),
223 Err(e) => {
224 error!("Could not write the playlist contents to a file: {e}");
225 Err(Error::PlaylistExportFailed)
226 }
227 }
228}
229
230pub fn create_playlist(name: String, track_paths: &Option<Vec<PathBuf>>) -> Result<(), Error> {
231 let mut guard = MUSIC_LIBRARY.write().unwrap();
232 let lib = unwrap_lib_mut(guard.as_mut())?;
233 let name = name.trim();
234 let name = if name.is_empty() {
235 lib.get_default_playlist_name()
236 } else {
237 name.to_string()
238 };
239 lib.create_playlist(name, track_paths)?;
240
241 drop(guard);
242 save_library()
243}
244
245pub fn rename_playlist(source: &str, target: &str) -> Result<(), Error> {
246 let mut guard = MUSIC_LIBRARY.write().unwrap();
247 let lib = unwrap_lib_mut(guard.as_mut())?;
248 lib.rename_playlist(source, target)?;
249 drop(guard);
250 save_library()
251}
252
253pub fn import_playlist_from_m3u8(name: Option<String>, file: &PathBuf) -> Result<(), Error> {
254 let mut guard = MUSIC_LIBRARY.write().unwrap();
255 let lib = unwrap_lib_mut(guard.as_mut())?;
256 lib.import_m3u8_playlist(file, name)?;
257
258 drop(guard);
259 save_library()
260}
261
262pub fn delete_playlists(playlists: Vec<String>) -> Result<(), Error> {
263 let mut guard = MUSIC_LIBRARY.write().unwrap();
264 let lib = unwrap_lib_mut(guard.as_mut())?;
265 lib.remove_playlists(playlists)?;
266
267 drop(guard);
268 save_library()
269}
270
271pub fn add_tracks(playlist: &str, tracks: Vec<PathBuf>) -> Result<(), Error> {
272 let mut guard = MUSIC_LIBRARY.write().unwrap();
273 let lib = unwrap_lib_mut(guard.as_mut())?;
274 lib.add_tracks(playlist, tracks)?;
275
276 drop(guard);
277 save_library()
278}
279
280pub fn remove_tracks(playlist: &str, tracks: Vec<usize>) -> Result<(), Error> {
282 let mut guard = MUSIC_LIBRARY.write().unwrap();
283 let lib = unwrap_lib_mut(guard.as_mut())?;
284 lib.remove_tracks(playlist, tracks)?;
285
286 drop(guard);
287 save_library()?;
288 Ok(())
289}