1#![feature(io_error_too_many_open_files)]
2
3use std::{
7 path::PathBuf,
8 sync::{Arc, LazyLock, RwLock, mpsc},
9 thread,
10};
11
12use log::{error, warn};
13
14use crate::{
15 music_lib::{covers::LoadMode, set_dirs, state::MusicLibrary},
16 playback::state::PlayerState,
17};
18
19pub mod music_lib;
20pub mod playback;
21#[cfg(test)]
22mod tests;
23
24pub const APP_NAME: &str = "Chilen";
25#[cfg(feature = "mpris")]
26const APP_ID: &str = "dev.tpaau.Chilen";
27
28pub struct Config {
29 pub cache_dir: PathBuf,
30 pub music_dir: PathBuf,
32 pub data_dir: PathBuf,
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub enum Event {
37 Raise,
38 Quit,
39 SetFullscreen(bool),
40 PlayerStateChanged(PlayerState),
41 LibraryLoadFailed(String),
42 LibraryChanged(Box<MusicLibrary>),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub enum Error {
48 PathInaccessible(PathBuf),
49 PathReadonly(PathBuf),
50 NotADirectory(PathBuf),
51 DirectoryCreationFailed(PathBuf, String),
52 EmptyName,
53 PlayerNotConnected,
58 StateNotInitialized,
60 QueueEmpty,
62 SourceError,
64 PlayerPlaying,
66 PlayerPaused,
68 PlayerStopped,
71 SeekNotSupported,
73 CannotGoPrevious,
78 CannotGoNext,
83 NoTrackAtIndex(usize),
85 InvalidDuration,
89 DurationOverflow,
91 PlaylistExists,
94 LibraryNotInitialized,
99 UnknownPlaylist(String),
102 IndexOutOfBounds,
104 DuplicateItems,
106 UnknownTrack,
108 StateNotReadable,
110 StateWriteFailed,
112 StateNotAFile,
114 PathDoesNotExist,
116 DirectoryWithNoTracks,
118 PlaylistParsingError,
122 PlaylistExportFailed,
126}
127
128impl std::error::Error for Error {}
129
130impl std::fmt::Display for Error {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 match self {
133 Self::PlayerNotConnected => write!(f, "The audio player is not connected"),
134 Self::StateNotInitialized => write!(f, "The playback state is not initialized"),
135 Self::QueueEmpty => write!(f, "The queue is empty"),
136 Self::SourceError => write!(
137 f,
138 "The audio file could not be opened, has an unsupported format or is corrupt"
139 ),
140 Self::PlayerPlaying => write!(f, "The player is already playing"),
141 Self::PlayerPaused => write!(f, "The player is already paused"),
142 Self::PlayerStopped => write!(f, "The player is stopped"),
143 Self::SeekNotSupported => write!(f, "Seek is not supported"),
144 Self::CannotGoPrevious => write!(f, "Cannot go to the previous track"),
145 Self::CannotGoNext => write!(f, "Cannot go to the next track"),
146 Self::NoTrackAtIndex(index) => write!(f, "No track was found at index {index}"),
147 Self::InvalidDuration => write!(
148 f,
149 "The player position could not be set because the duration provided was invalid"
150 ),
151 Self::DurationOverflow => {
152 write!(f, "Overflow detected while performing a seek operation")
153 }
154 Self::UnknownTrack => {
155 write!(f, "The provided track was not found in the music library")
156 }
157 Self::PlaylistExists => write!(f, "Playlist with this name already exists"),
158 Self::LibraryNotInitialized => write!(f, "The music library is not initialized"),
159 Self::UnknownPlaylist(name) => {
160 write!(f, "There is no playlist with the name \"{name}\"")
161 }
162 Self::IndexOutOfBounds => write!(f, "The provided item index was out of bounds"),
163 Self::DuplicateItems => write!(f, "The provided vector contained duplicate values"),
164 Self::StateNotReadable => {
165 write!(f, "Could not read the contents of the library state file")
166 }
167 Self::StateWriteFailed => write!(f, "Could not write the library state to a file"),
168 Self::StateNotAFile => write!(f, "The library state path is not a file"),
169 Self::PathDoesNotExist => write!(
170 f,
171 "The provided path does not exist or access to it was denied"
172 ),
173 Self::DirectoryWithNoTracks => write!(
174 f,
175 "Could not find any audio files in the provided directory path"
176 ),
177 Self::PlaylistParsingError => write!(f, "Could not parse the M3U8 playlist"),
178 Self::PlaylistExportFailed => write!(f, "Could not export the playlist to M3U8"),
179 Error::PathInaccessible(path_buf) => write!(f, "Path inaccessible: {path_buf:?}"),
180 Error::PathReadonly(path_buf) => write!(f, "Path is readonly: {path_buf:?}"),
181 Error::NotADirectory(path_buf) => {
182 write!(f, "Expected this path to be a directory: {path_buf:?}")
183 }
184 Error::DirectoryCreationFailed(path_buf, e) => {
185 write!(f, "Could not create a directory at {path_buf:?}: {e}")
186 }
187 Error::EmptyName => write!(f, "The provided name was empty"),
188 }
189 }
190}
191
192fn send_event(event: Event) {
193 if let Some(sender) = EVENT_SENDER.read().unwrap().as_ref() {
194 if let Err(e) = sender.send(event) {
195 error!("Could not send the event: {e}");
196 }
197 } else {
198 warn!(
199 "The event sender is not initialized - this is expected during testing, in production this would be a serious bug though"
200 )
201 }
202}
203
204pub(crate) static EVENT_SENDER: LazyLock<Arc<RwLock<Option<mpsc::Sender<Event>>>>> =
205 LazyLock::new(|| Arc::new(RwLock::new(None)));
206
207pub fn init(config: Config) -> Result<mpsc::Receiver<Event>, Error> {
209 let (sender, receiver) = mpsc::channel();
210 *EVENT_SENDER.write().unwrap() = Some(sender);
211
212 if let Err(e) = set_dirs(config.data_dir, config.cache_dir, config.music_dir) {
213 error!("Could not set the initial directories: {e}");
214 return Err(e);
215 }
216 if let Err(e) = music_lib::state::load(LoadMode::Load) {
217 error!("Could not load the music library: {e}");
218 return Err(e);
219 }
220 thread::spawn(playback::init);
221
222 Ok(receiver)
223}