1pub mod music_lib;
2pub mod playback;
3#[cfg(test)]
4mod tests;
5
6use std::{
7 path::PathBuf,
8 sync::{Arc, LazyLock, RwLock, mpsc},
9 thread,
10};
11
12use icu::{
13 collator::{Collator, options::CollatorOptions},
14 locale::Locale,
15};
16use log::{error, info, warn};
17use serde::{Deserialize, Serialize};
18
19use crate::music_lib::{MusicLibrary, set_dirs, sorting::COLLATOR};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct Config {
23 pub identity: String,
25 #[cfg(feature = "mpris")]
36 pub identifier: String,
37 pub cache_dir: PathBuf,
38 pub music_dir: PathBuf,
40 pub data_dir: PathBuf,
41 pub locale: Locale,
42 pub playback: playback::Config,
43 pub library: music_lib::Config,
44}
45
46#[cfg(test)]
47pub(crate) fn testing_init_config() {
48 use crate::music_lib::ValueSeparators;
49
50 *crate::CONFIG.write().unwrap() = Some(Arc::new(Config {
51 identity: "Chilen".to_string(),
52 #[cfg(feature = "mpris")]
53 identifier: "com.tpaau.Chilen".to_string(),
54 cache_dir: PathBuf::new(),
55 music_dir: PathBuf::new(),
56 data_dir: PathBuf::new(),
57 locale: "en-US".parse().unwrap(),
58 playback: playback::Config {
59 skip_previous_threshold: None,
60 },
61 library: crate::music_lib::Config {
62 value_separators: ValueSeparators::default(),
63 indexer: crate::music_lib::indexer::Config::default(),
64 },
65 }))
66}
67
68#[derive(Debug, Clone, PartialEq)]
69pub enum Event {
70 Raise,
71 Quit,
72 SetFullscreen(bool),
73 Playback(playback::Event),
74 LoadProgressChanged(music_lib::Progress),
75 LibraryLoadFailed(String),
76 LibraryChanged(Box<MusicLibrary>),
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub enum Error {
82 PathInaccessible(PathBuf),
83 PathReadonly(PathBuf),
84 NotADirectory(PathBuf),
85 DirectoryCreationFailed(PathBuf, String),
86 EmptyName,
87 PlayerNotConnected,
92 StateNotInitialized,
94 QueueEmpty,
96 SourceError,
98 PlayerPlaying,
100 PlayerPaused,
102 PlayerStopped,
105 SeekNotSupported,
107 CannotGoPrevious,
112 CannotGoNext,
117 NoTrackAtIndex(usize),
119 InvalidDuration,
123 DurationOverflow,
125 PlaylistExists,
128 LibraryNotInitialized,
133 UnknownPlaylist(String),
136 IndexOutOfBounds,
138 DuplicateItems,
140 UnknownTrackPath(PathBuf),
142 StateNotReadable,
144 StateWriteFailed,
146 StateNotAFile,
148 PathDoesNotExist,
150 DirectoryWithNoTracks,
152 PlaylistParsingError,
156 PlaylistExportFailed,
160 UnknownTrack {
162 hash: u64,
163 },
164}
165
166impl std::error::Error for Error {}
167
168impl std::fmt::Display for Error {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::PlayerNotConnected => write!(f, "The audio player is not connected"),
172 Self::StateNotInitialized => write!(f, "The playback state is not initialized"),
173 Self::QueueEmpty => write!(f, "The queue is empty"),
174 Self::SourceError => write!(
175 f,
176 "The audio file could not be opened, has an unsupported format or is corrupt"
177 ),
178 Self::PlayerPlaying => write!(f, "The player is already playing"),
179 Self::PlayerPaused => write!(f, "The player is already paused"),
180 Self::PlayerStopped => write!(f, "The player is stopped"),
181 Self::SeekNotSupported => write!(f, "Seek is not supported"),
182 Self::CannotGoPrevious => write!(f, "Cannot go to the previous track"),
183 Self::CannotGoNext => write!(f, "Cannot go to the next track"),
184 Self::NoTrackAtIndex(index) => write!(f, "No track was found at index {index}"),
185 Self::InvalidDuration => write!(
186 f,
187 "The player position could not be set because the duration was invalid"
188 ),
189 Self::DurationOverflow => {
190 write!(f, "Overflow detected while performing a seek operation")
191 }
192 Self::UnknownTrackPath(path) => {
193 write!(
194 f,
195 "The track track doesn't exist the music library: {path:?}"
196 )
197 }
198 Self::PlaylistExists => write!(f, "Playlist with this name already exists"),
199 Self::LibraryNotInitialized => write!(f, "The music library is not initialized"),
200 Self::UnknownPlaylist(name) => {
201 write!(f, "There is no playlist with the name \"{name}\"")
202 }
203 Self::IndexOutOfBounds => write!(f, "The item index was out of bounds"),
204 Self::DuplicateItems => write!(f, "The vector contained duplicate values"),
205 Self::StateNotReadable => {
206 write!(f, "Could not read the contents of the library state file")
207 }
208 Self::StateWriteFailed => write!(f, "Could not write the library state to a file"),
209 Self::StateNotAFile => write!(f, "The library state path is not a file"),
210 Self::PathDoesNotExist => {
211 write!(f, "The path does not exist or access to it was denied")
212 }
213 Self::DirectoryWithNoTracks => {
214 write!(f, "Could not find any audio files in the directory path")
215 }
216 Self::PlaylistParsingError => write!(f, "Could not parse the M3U8 playlist"),
217 Self::PlaylistExportFailed => write!(f, "Could not export the playlist to M3U8"),
218 Error::PathInaccessible(path_buf) => write!(f, "Path inaccessible: {path_buf:?}"),
219 Error::PathReadonly(path_buf) => write!(f, "Path is readonly: {path_buf:?}"),
220 Error::NotADirectory(path_buf) => {
221 write!(f, "Expected this path to be a directory: {path_buf:?}")
222 }
223 Error::DirectoryCreationFailed(path_buf, e) => {
224 write!(f, "Could not create a directory at {path_buf:?}: {e}")
225 }
226 Error::EmptyName => write!(f, "The name is empty"),
227 Self::UnknownTrack { hash } => {
228 write!(f, "Track with id {hash} does not exist in the library")
229 }
230 }
231 }
232}
233
234fn send_event(event: Event) {
235 if let Some(sender) = EVENT_SENDER.read().unwrap().as_ref() {
236 if let Err(e) = sender.send(event) {
237 error!("Could not send the event: {e}");
238 }
239 } else {
240 warn!(
241 "The event sender is not initialized - this is expected during testing, in production this would be a serious bug though"
242 )
243 }
244}
245
246pub(crate) static EVENT_SENDER: LazyLock<Arc<RwLock<Option<mpsc::Sender<Event>>>>> =
247 LazyLock::new(|| Arc::new(RwLock::new(None)));
248
249static CONFIG: RwLock<Option<Arc<Config>>> = RwLock::new(None);
250
251pub(crate) fn get_config() -> Arc<Config> {
252 let guard = CONFIG.read().unwrap();
253 guard.as_ref().unwrap().clone()
254}
255
256pub fn init(config: Config) -> Result<mpsc::Receiver<Event>, Error> {
258 let (sender, receiver) = mpsc::channel();
259 *EVENT_SENDER.write().unwrap() = Some(sender);
260
261 thread::spawn(|| {
262 *CONFIG.write().unwrap() = Some(Arc::new(config.clone()));
263 *COLLATOR.write().unwrap() =
264 match Collator::try_new(config.locale.clone().into(), CollatorOptions::default()) {
265 Ok(c) => Some(Arc::new(c)),
266 Err(e) => {
267 error!("Could not initialize the collator, things will be unordered: {e}");
268 None
269 }
270 };
271
272 if let Err(e) = set_dirs(config.data_dir, config.cache_dir, config.music_dir) {
273 error!("Could not set the initial directories: {e}");
274 send_event(Event::LoadProgressChanged(music_lib::Progress::Failed(e)));
275 return;
276 }
277 if let Err(e) = music_lib::load(config.library) {
278 error!("Could not load the music library: {e}");
279 send_event(Event::LoadProgressChanged(music_lib::Progress::Failed(e)));
280 return;
281 }
282 playback::init(
283 #[cfg(feature = "mpris")]
284 config.identity,
285 #[cfg(feature = "mpris")]
286 config.identifier,
287 )
288 });
289
290 Ok(receiver)
291}
292
293pub fn shutdown() -> Result<(), String> {
298 info!("Cleaning up on shutdown...");
299 let guard = playback::state::PLAYER_STATE.read().unwrap();
300 match guard.as_ref().cloned() {
301 Some(state) => {
302 if let Err(e) = playback::state::save_state(state) {
303 error!("Couldn't save playback state on quit: {e}");
304 return Err(e);
305 }
306 }
307 None => warn!("Player state is not initialized, maybe the daemon was closed too soon?"),
308 }
309 info!("Clean up completed");
310 Ok(())
311}