1use std::{
2 path::PathBuf,
3 sync::{Arc, LazyLock, RwLock, mpsc},
4 thread,
5};
6
7use icu::{
8 collator::{Collator, CollatorBorrowed, options::CollatorOptions},
9 locale::Locale,
10};
11use log::{error, info, warn};
12use serde::{Deserialize, Serialize};
13
14use crate::music_lib::{MusicLibrary, set_dirs};
15
16pub mod music_lib;
17pub mod playback;
18#[cfg(test)]
19mod tests;
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}
161
162impl std::error::Error for Error {}
163
164impl std::fmt::Display for Error {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 match self {
167 Self::PlayerNotConnected => write!(f, "The audio player is not connected"),
168 Self::StateNotInitialized => write!(f, "The playback state is not initialized"),
169 Self::QueueEmpty => write!(f, "The queue is empty"),
170 Self::SourceError => write!(
171 f,
172 "The audio file could not be opened, has an unsupported format or is corrupt"
173 ),
174 Self::PlayerPlaying => write!(f, "The player is already playing"),
175 Self::PlayerPaused => write!(f, "The player is already paused"),
176 Self::PlayerStopped => write!(f, "The player is stopped"),
177 Self::SeekNotSupported => write!(f, "Seek is not supported"),
178 Self::CannotGoPrevious => write!(f, "Cannot go to the previous track"),
179 Self::CannotGoNext => write!(f, "Cannot go to the next track"),
180 Self::NoTrackAtIndex(index) => write!(f, "No track was found at index {index}"),
181 Self::InvalidDuration => write!(
182 f,
183 "The player position could not be set because the duration was invalid"
184 ),
185 Self::DurationOverflow => {
186 write!(f, "Overflow detected while performing a seek operation")
187 }
188 Self::UnknownTrackPath(path) => {
189 write!(
190 f,
191 "The track track doesn't exist the music library: {path:?}"
192 )
193 }
194 Self::PlaylistExists => write!(f, "Playlist with this name already exists"),
195 Self::LibraryNotInitialized => write!(f, "The music library is not initialized"),
196 Self::UnknownPlaylist(name) => {
197 write!(f, "There is no playlist with the name \"{name}\"")
198 }
199 Self::IndexOutOfBounds => write!(f, "The item index was out of bounds"),
200 Self::DuplicateItems => write!(f, "The vector contained duplicate values"),
201 Self::StateNotReadable => {
202 write!(f, "Could not read the contents of the library state file")
203 }
204 Self::StateWriteFailed => write!(f, "Could not write the library state to a file"),
205 Self::StateNotAFile => write!(f, "The library state path is not a file"),
206 Self::PathDoesNotExist => {
207 write!(f, "The path does not exist or access to it was denied")
208 }
209 Self::DirectoryWithNoTracks => {
210 write!(f, "Could not find any audio files in the directory path")
211 }
212 Self::PlaylistParsingError => write!(f, "Could not parse the M3U8 playlist"),
213 Self::PlaylistExportFailed => write!(f, "Could not export the playlist to M3U8"),
214 Error::PathInaccessible(path_buf) => write!(f, "Path inaccessible: {path_buf:?}"),
215 Error::PathReadonly(path_buf) => write!(f, "Path is readonly: {path_buf:?}"),
216 Error::NotADirectory(path_buf) => {
217 write!(f, "Expected this path to be a directory: {path_buf:?}")
218 }
219 Error::DirectoryCreationFailed(path_buf, e) => {
220 write!(f, "Could not create a directory at {path_buf:?}: {e}")
221 }
222 Error::EmptyName => write!(f, "The name is empty"),
223 }
224 }
225}
226
227fn send_event(event: Event) {
228 if let Some(sender) = EVENT_SENDER.read().unwrap().as_ref() {
229 if let Err(e) = sender.send(event) {
230 error!("Could not send the event: {e}");
231 }
232 } else {
233 warn!(
234 "The event sender is not initialized - this is expected during testing, in production this would be a serious bug though"
235 )
236 }
237}
238
239pub(crate) static EVENT_SENDER: LazyLock<Arc<RwLock<Option<mpsc::Sender<Event>>>>> =
240 LazyLock::new(|| Arc::new(RwLock::new(None)));
241
242static CONFIG: RwLock<Option<Arc<Config>>> = RwLock::new(None);
243pub(crate) static COLLATOR: RwLock<Option<Arc<CollatorBorrowed<'_>>>> = RwLock::new(None);
244
245pub(crate) fn get_config() -> Arc<Config> {
246 let guard = CONFIG.read().unwrap();
247 guard.as_ref().unwrap().clone()
248}
249
250pub fn init(config: Config) -> Result<mpsc::Receiver<Event>, Error> {
252 let (sender, receiver) = mpsc::channel();
253 *EVENT_SENDER.write().unwrap() = Some(sender);
254
255 thread::spawn(|| {
256 *CONFIG.write().unwrap() = Some(Arc::new(config.clone()));
257 *COLLATOR.write().unwrap() =
258 match Collator::try_new(config.locale.clone().into(), CollatorOptions::default()) {
259 Ok(c) => Some(Arc::new(c)),
260 Err(e) => {
261 error!("Could not initialize the collator, things will be unordered: {e}");
262 None
263 }
264 };
265
266 if let Err(e) = set_dirs(config.data_dir, config.cache_dir, config.music_dir) {
267 error!("Could not set the initial directories: {e}");
268 send_event(Event::LoadProgressChanged(music_lib::Progress::Failed(e)));
269 return;
270 }
271 if let Err(e) = music_lib::load(config.library) {
272 error!("Could not load the music library: {e}");
273 send_event(Event::LoadProgressChanged(music_lib::Progress::Failed(e)));
274 return;
275 }
276 playback::init(
277 #[cfg(feature = "mpris")]
278 config.identity,
279 #[cfg(feature = "mpris")]
280 config.identifier,
281 )
282 });
283
284 Ok(receiver)
285}
286
287pub fn shutdown() -> Result<(), String> {
292 info!("Cleaning up on shutdown...");
293 let guard = playback::state::PLAYER_STATE.read().unwrap();
294 match guard.as_ref().cloned() {
295 Some(state) => {
296 if let Err(e) = playback::state::save_state(state) {
297 error!("Couldn't save playback state on quit: {e}");
298 return Err(e);
299 }
300 }
301 None => warn!("Player state is not initialized, maybe the daemon was closed too soon?"),
302 }
303 info!("Clean up completed");
304 Ok(())
305}