Skip to main content

chilen_backend/
lib.rs

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    /// A friendly name to identify the media player to users (eg: “VLC media player”).
24    pub identity: String,
25    /// The bus name suffix to be used with MPRIS.
26    ///
27    /// The resulting bus name will be `org.mpris.MediaPlayer2.<bus_name_suffix>`, where
28    /// `<bus_name_suffix>` must be a unique identifier, such as one based on a UNIX process id.
29    /// For example, this could be:
30    ///
31    /// - `org.mpris.MediaPlayer2.vlc.instance7389`
32    ///
33    /// **Note:** According to the D-Bus specification, the unique identifier “must only contain
34    /// the ASCII characters \[A-Z\]\[a-z\]\[0-9\]_-” and “must not begin with a digit”.
35    #[cfg(feature = "mpris")]
36    pub identifier: String,
37    pub cache_dir: PathBuf,
38    // TODO: Support for multiple music library directories
39    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/// Chilen error type.
80#[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    /// The audio player is not connected.
88    ///
89    /// This may happen if the device doesn't have an audio device or none of the audio devices are
90    /// marked as default.
91    PlayerNotConnected,
92    /// The playback state is not initialized.
93    StateNotInitialized,
94    /// The queue is empty.
95    QueueEmpty,
96    /// The audio file could not be opened, has an unsupported format or is corrupt.
97    SourceError,
98    /// The player is already playing.
99    PlayerPlaying,
100    /// The player is already paused.
101    PlayerPaused,
102    /// Thrown when a client attempts to stop the player when it was already stopped or when a
103    /// client attempts to seek while the player is stopped.
104    PlayerStopped,
105    /// Seek is not supported for the current audio source.
106    SeekNotSupported,
107    /// Cannot go to the previous track.
108    ///
109    /// This means that the current track is first in the queue and the
110    /// [loop state](playback::LoopState) is set to [`LoopState::Off`](playback::LoopState::Off).
111    CannotGoPrevious,
112    /// Cannot go to the next track.
113    ///
114    /// This means that the current track is last in the queue and the
115    /// [loop state](playback::LoopState) is set to [`LoopState::Off`](playback::LoopState).
116    CannotGoNext,
117    /// No track at this index.
118    NoTrackAtIndex(usize),
119    /// The player position could not be set because the duration provided was invalid.
120    ///
121    /// The player will additionally refuse to seek by 0s to prevent audio popping.
122    InvalidDuration,
123    /// Overflow detected while performing a seek operation.
124    DurationOverflow,
125    /// Could not complete the operation because a [playlist](music_lib::Playlist) with the
126    /// provided name already exists.
127    PlaylistExists,
128    /// Could not perform the operation because the [music library](music_lib::MusicLibrary)
129    /// is not initialized.
130    ///
131    /// This can happen if a command is sent to early and the music library is not yet initialized.
132    LibraryNotInitialized,
133    /// There is no [playlist](music_lib::Playlist) in the
134    /// [music library](music_lib::MusicLibrary) with the provided name.
135    UnknownPlaylist(String),
136    /// The item index was out of bounds.
137    IndexOutOfBounds,
138    /// The list contained duplicate values.
139    DuplicateItems,
140    /// The track was not found in the music library.
141    UnknownTrackPath(PathBuf),
142    /// Could not read the contents of the library state file.
143    StateNotReadable,
144    /// Could not write the library state to a file.
145    StateWriteFailed,
146    /// The library state path is not a file.
147    StateNotAFile,
148    /// The path does not exist or access to it was denied.
149    PathDoesNotExist,
150    /// Could not find any audio files in the directory path.
151    DirectoryWithNoTracks,
152    /// Could not parse the M3U8 playlist.
153    ///
154    /// Please make sure that the playlist has the correct format and is not corrupted.
155    PlaylistParsingError,
156    /// Could not export the playlist to M3U8.
157    ///
158    /// This likely either means that the specified file path doesn't exists or is not writable.
159    PlaylistExportFailed,
160    /// Track with the given id does not exist in the library
161    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
256// TODO: Unused cached cover art cleanup here somewhere
257pub 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
293// TODO: Should also acquire read locks to all important resources (eg. library) to make sure no
294// threads are writing anything to the drive.
295// It should also use a best-effort approach and close as much as possible
296/// Shut the daemon down gracefully.
297pub 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}