Skip to main content

chilen_backend/
lib.rs

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    /// 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}
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
250// TODO: Unused cached cover art cleanup here somewhere
251pub 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
287// TODO: Should also acquire read locks to all important resources (eg. library) to make sure no
288// threads are writing anything to the drive.
289// It should also use a best-effort approach and close as much as possible
290/// Shut the daemon down gracefully.
291pub 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}