Skip to main content

chilen_backend/
lib.rs

1#![feature(io_error_too_many_open_files)]
2
3// TODO: Add error handling for playback and library state restoration for when the indexer is
4// running in the background (too many open files)
5
6use 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    // TODO: Support for multiple music library directories
31    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/// Chilen error type.
46#[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    /// The audio player is not connected.
54    ///
55    /// This may happen if the device doesn't have an audio device or none of the audio devices are
56    /// marked as default.
57    PlayerNotConnected,
58    /// The playback state is not initialized.
59    StateNotInitialized,
60    /// The queue is empty.
61    QueueEmpty,
62    /// The audio file could not be opened, has an unsupported format or is corrupt.
63    SourceError,
64    /// The player is already playing.
65    PlayerPlaying,
66    /// The player is already paused.
67    PlayerPaused,
68    /// Thrown when a client attempts to stop the player when it was already stopped or when a
69    /// client attempts to seek while the player is stopped.
70    PlayerStopped,
71    /// Seek is not supported for the current audio source.
72    SeekNotSupported,
73    /// Cannot go to the previous track.
74    ///
75    /// This means that the current track is first in the queue and the
76    /// [loop state](playback::LoopState) is set to [`LoopState::Off`](playback::LoopState::Off).
77    CannotGoPrevious,
78    /// Cannot go to the next track.
79    ///
80    /// This means that the current track is last in the queue and the
81    /// [loop state](playback::LoopState) is set to [`LoopState::Off`](playback::LoopState).
82    CannotGoNext,
83    /// No track at this index.
84    NoTrackAtIndex(usize),
85    /// The player position could not be set because the duration provided was invalid.
86    ///
87    /// The player will additionally refuse to seek by 0s to prevent audio popping.
88    InvalidDuration,
89    /// Overflow detected while performing a seek operation.
90    DurationOverflow,
91    /// Could not complete the operation because a [playlist](music_lib::state::Playlist) with the
92    /// provided name already exists.
93    PlaylistExists,
94    /// Could not perform the operation because the [music library](music_lib::state::MusicLibrary)
95    /// is not initialized.
96    ///
97    /// This can happen if a command is sent to early and the music library is not yet initialized.
98    LibraryNotInitialized,
99    /// There is no [playlist](music_lib::state::Playlist) in the
100    /// [music library](music_lib::state::MusicLibrary) with the provided name.
101    UnknownPlaylist(String),
102    /// The provided item index was out of bounds.
103    IndexOutOfBounds,
104    /// The provided list contained duplicate values.
105    DuplicateItems,
106    /// The provided track is not registered in the library.
107    UnknownTrack,
108    /// Could not read the contents of the library state file.
109    StateNotReadable,
110    /// Could not write the library state to a file.
111    StateWriteFailed,
112    /// The library state path is not a file.
113    StateNotAFile,
114    /// The provided path does not exist or access to it was denied.
115    PathDoesNotExist,
116    /// Could not find any audio files in the provided directory path.
117    DirectoryWithNoTracks,
118    /// Could not parse the M3U8 playlist.
119    ///
120    /// Please make sure that the playlist has the correct format and is not corrupted.
121    PlaylistParsingError,
122    /// Could not export the playlist to M3U8.
123    ///
124    /// This likely either means that the specified file path doesn't exists or is not writable.
125    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
207// TODO: Unused cached cover art cleanup here somewhere
208pub 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}