Skip to main content

chilen_daemon/playback/
mod.rs

1#[cfg(feature = "mpris")]
2mod mpris;
3pub(crate) mod state;
4#[cfg(test)]
5mod tests;
6
7use std::{
8    path::PathBuf,
9    sync::{Arc, LazyLock, RwLock},
10    thread,
11    time::Duration,
12};
13
14use chilen_ipc::playback::{LoopState, PlaybackResponse};
15#[cfg(feature = "shuffle")]
16use chilen_ipc::{
17    Event, Response,
18    playback::{
19        PlaybackCommand, PlaybackRate, PlaybackState, PlayerVolume, ShuffleState, SignedDuration,
20    },
21};
22use log::{debug, error, info, trace, warn};
23use rodio::Player;
24use serde::{Deserialize, Serialize};
25use walkdir::WalkDir;
26
27use crate::{
28    music_lib::{
29        state::{Track, get_library},
30        tracks_from_m3u8, tracks_from_paths,
31    },
32    playback::state::{
33        PLAYER_STATE, PlayerState, background_save_state, restore_state_from_cache,
34        unwrap_state_mut, unwrap_state_ref,
35    },
36};
37
38/// Configuration options specific to the [`playback`](crate::playback) module.
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
40pub struct Config {
41    /// A friendly name to identify the media player to users (eg: “VLC media player”).
42    ///
43    /// This should usually match the name found in .desktop files.
44    #[cfg(any(feature = "mpris", doc))]
45    pub identity: String,
46    /// The bus name suffix to be used with MPRIS.
47    ///
48    /// The resulting bus name will be `org.mpris.MediaPlayer2.<bus_name_suffix>`, where
49    /// `<bus_name_suffix>` must be a unique identifier, such as one based on a UNIX process id.
50    /// For example, this could be:
51    ///
52    /// - `org.mpris.MediaPlayer2.vlc.instance7389`
53    ///
54    /// **Note:** According to the D-Bus specification, the unique identifier “must only contain
55    /// the ASCII characters \[A-Z\]\[a-z\]\[0-9\]_-” and “must not begin with a digit”.
56    #[cfg(any(feature = "mpris", doc))]
57    pub bus_name_suffix: String,
58    /// Whether to allow clients to modify the playback rate of the player.
59    ///
60    /// If set to true, the playback rate will be locked the value saved in cache the player state,
61    /// or set to the default value of `1.0` (regular speed) if it's not cached.
62    pub allow_rate_modification: bool,
63}
64
65#[derive(Debug, Clone)]
66pub(crate) enum Command {
67    Play(Option<usize>),
68    Pause,
69    Stop,
70    TogglePlaying,
71    GetPlaybackState,
72    SetQueue(Vec<Track>),
73    AppendToQueue(Vec<Track>),
74    GetCurrentTrack,
75    Next,
76    Previous,
77    SetLoopState(LoopState),
78    GetLoopState,
79    SetRate(f64),
80    GetRate,
81    #[cfg(feature = "shuffle")]
82    SetShuffleState(ShuffleState),
83    GetShuffleState,
84    #[cfg(not(feature = "shuffle"))]
85    SetShuffleState,
86    SetPlayerPosition(Duration),
87    Seek(SignedDuration),
88    GetPlayerPosition,
89    SetPlayerVolume(PlayerVolume),
90    GetPlayerVolume,
91    OpenURI(String),
92}
93
94impl TryFrom<PlaybackCommand> for Command {
95    type Error = chilen_ipc::Error;
96    fn try_from(value: PlaybackCommand) -> Result<Self, Self::Error> {
97        match value {
98            PlaybackCommand::Play(maybe_index) => Ok(Self::Play(maybe_index)),
99            PlaybackCommand::Pause => Ok(Self::Pause),
100            PlaybackCommand::Stop => Ok(Self::Stop),
101            PlaybackCommand::TogglePlaying => Ok(Self::TogglePlaying),
102            PlaybackCommand::GetPlaybackState => Ok(Self::GetPlaybackState),
103            PlaybackCommand::SetQueue(track_paths) => {
104                let tracks = tracks_from_paths(&track_paths, false)?;
105                Ok(Self::SetQueue(tracks))
106            }
107            PlaybackCommand::AppendToQueue(track_paths) => {
108                let tracks = tracks_from_paths(&track_paths, false)?;
109                Ok(Self::AppendToQueue(tracks))
110            }
111            PlaybackCommand::SetPlaylist(playlist) => {
112                let lib = get_library()?;
113                let playlist = match lib.find_playlist(&playlist) {
114                    Some(playlist) => playlist,
115                    None => return Err(chilen_ipc::Error::UnknownPlaylist),
116                };
117                Ok(Self::SetQueue(
118                    playlist.tracks.iter().map(|t| t.as_ref().clone()).collect(),
119                ))
120            }
121            PlaybackCommand::AppendPlaylist(playlist) => {
122                let lib = get_library()?;
123                let playlist = match lib.find_playlist(&playlist) {
124                    Some(playlist) => playlist,
125                    None => return Err(chilen_ipc::Error::UnknownPlaylist),
126                };
127                Ok(Self::AppendToQueue(
128                    playlist.tracks.iter().map(|t| t.as_ref().clone()).collect(),
129                ))
130            }
131            PlaybackCommand::GetCurrentTrack => Ok(Self::GetCurrentTrack),
132            PlaybackCommand::Next => Ok(Self::Next),
133            PlaybackCommand::Previous => Ok(Self::Previous),
134            PlaybackCommand::SetLoopState(loop_state) => Ok(Self::SetLoopState(loop_state)),
135            PlaybackCommand::GetLoopState => Ok(Self::GetLoopState),
136            PlaybackCommand::SetRate(rate) => Ok(Self::SetRate(rate)),
137            PlaybackCommand::GetRate => Ok(Self::GetRate),
138            #[cfg(feature = "shuffle")]
139            PlaybackCommand::SetShuffleState(shuffle_state) => {
140                Ok(Self::SetShuffleState(shuffle_state))
141            }
142            #[cfg(not(feature = "shuffle"))]
143            PlaybackCommand::SetShuffleState(_) => Ok(Self::SetShuffleState),
144            PlaybackCommand::GetShuffleState => Ok(Self::GetShuffleState),
145            PlaybackCommand::SetPlayerPosition(position) => Ok(Self::SetPlayerPosition(position)),
146            PlaybackCommand::Seek(delta) => Ok(Self::Seek(delta)),
147            PlaybackCommand::GetPlayerPosition => Ok(Self::GetPlayerPosition),
148            PlaybackCommand::SetPlayerVolume(volume) => Ok(Self::SetPlayerVolume(volume)),
149            PlaybackCommand::GetPlayerVolume => Ok(Self::GetPlayerVolume),
150            PlaybackCommand::OpenURI(uri) => Ok(Self::OpenURI(uri)),
151        }
152    }
153}
154
155static PLAYER_HANDLE: LazyLock<Arc<RwLock<Option<rodio::Player>>>> =
156    LazyLock::new(|| Arc::new(RwLock::new(None)));
157
158pub(crate) static CONFIG: LazyLock<Arc<RwLock<Option<Config>>>> =
159    LazyLock::new(|| Arc::new(RwLock::new(None)));
160
161pub(crate) fn cleanup() {
162    *PLAYER_HANDLE.write().unwrap() = None;
163    *CONFIG.write().unwrap() = None;
164}
165
166pub(crate) fn unwrap_player(
167    maybe_player: Option<&rodio::Player>,
168) -> Result<&rodio::Player, chilen_ipc::Error> {
169    match maybe_player {
170        Some(player) => Ok(player),
171        None => Err(chilen_ipc::Error::StateNotInitialized),
172    }
173}
174
175/// Set whether the daemon should allow rate modification by clients.
176///
177/// Will fail with [`Error::DaemonNotRunning`](super::Error::DaemonNotRunning) if the daemon isn't
178/// running.
179pub fn set_allow_rate_modification(allow_rate_modification: bool) -> Result<(), super::Error> {
180    let mut conf_guard = CONFIG.write().unwrap();
181    let conf = match conf_guard.as_mut() {
182        Some(conf) => conf,
183        None => return Err(super::Error::DaemonNotRunning),
184    };
185    conf.allow_rate_modification = allow_rate_modification;
186    Ok(())
187}
188
189/// Play the current track or a track at a specified index in the queue.
190pub(crate) fn play(index: Option<usize>) -> Result<(), chilen_ipc::Error> {
191    let player_guard = PLAYER_HANDLE.read().unwrap();
192    let player = unwrap_player(player_guard.as_ref())?;
193    let mut state_guard = PLAYER_STATE.write().unwrap();
194    let state = unwrap_state_mut(state_guard.as_mut())?;
195    if let Some(id) = index {
196        trace!("Playing a track at index {id}");
197        let track = match state.play_track(id) {
198            Some(track) => track,
199            None => {
200                error!("No track at index {id}");
201                return Err(chilen_ipc::Error::NoTrackAtIndex(id));
202            }
203        };
204        let source = match track.open_source() {
205            Ok(source) => source,
206            Err(e) => {
207                error!("Could not open audio source: {e}");
208                return Err(chilen_ipc::Error::SourceError);
209            }
210        };
211        player.stop();
212        player.append(source);
213        player.play();
214        state.set_playback_state(PlaybackState::Playing);
215        background_save_state(state.clone());
216        Ok(())
217    } else {
218        trace!("Playing the current media");
219        if !player.is_paused() && !player.empty() {
220            Err(chilen_ipc::Error::PlayerPlaying)
221        } else if player.empty() {
222            if let Some(track) = state.current() {
223                let source = match track.open_source() {
224                    Ok(source) => source,
225                    Err(e) => {
226                        error!("Could not open audio source: {e}");
227                        return Err(chilen_ipc::Error::SourceError);
228                    }
229                };
230                player.append(source);
231                state.set_playback_state(PlaybackState::Playing);
232                Ok(())
233            } else {
234                Err(chilen_ipc::Error::QueueEmpty)
235            }
236        } else {
237            player.play();
238            state.set_playback_state(PlaybackState::Playing);
239            Ok(())
240        }
241    }
242}
243
244pub(crate) fn pause() -> Result<(), chilen_ipc::Error> {
245    trace!("Pausing the current media");
246    let player_guard = PLAYER_HANDLE.read().unwrap();
247    let player = player_guard.as_ref().unwrap();
248    if player.is_paused() {
249        Err(chilen_ipc::Error::PlayerPaused)
250    } else {
251        // FIX: Audio popping by adding a 10ms fade out effect here
252        // NOTE: This is a missing feature in Rodio, see https://github.com/RustAudio/rodio/issues/889
253        player.pause();
254        let mut state_guard = PLAYER_STATE.write().unwrap();
255        let state = unwrap_state_mut(state_guard.as_mut())?;
256        state.set_playback_state(PlaybackState::Paused);
257        Ok(())
258    }
259}
260
261pub(crate) fn stop() -> Result<(), chilen_ipc::Error> {
262    trace!("Stopping the playback");
263    let player_guard = PLAYER_HANDLE.read().unwrap();
264    let player = player_guard.as_ref().unwrap();
265    if player.empty() {
266        Err(chilen_ipc::Error::PlayerStopped)
267    } else {
268        player.stop();
269        let mut state_guard = PLAYER_STATE.write().unwrap();
270        let state = unwrap_state_mut(state_guard.as_mut())?;
271        state.set_playback_state(PlaybackState::Stopped);
272        Ok(())
273    }
274}
275
276pub(crate) fn toggle_playing() -> Result<(), chilen_ipc::Error> {
277    trace!("Toggling playback state");
278    let state_guard = PLAYER_STATE.read().unwrap();
279    let state = unwrap_state_ref(state_guard.as_ref())?;
280    let playback_state = state.playback_state;
281    match playback_state {
282        PlaybackState::Paused => {
283            drop(state_guard);
284            play(None)
285        }
286        PlaybackState::Playing => {
287            drop(state_guard);
288            pause()
289        }
290        PlaybackState::Stopped => {
291            if state.current().is_some() {
292                let pos = state.position;
293                drop(state_guard);
294                play(Some(pos))
295            } else {
296                Err(chilen_ipc::Error::QueueEmpty)
297            }
298        }
299    }
300}
301
302pub(crate) fn get_playback_state() -> Result<PlaybackState, chilen_ipc::Error> {
303    let state_guard = PLAYER_STATE.read().unwrap();
304    let state = unwrap_state_ref(state_guard.as_ref())?;
305    Ok(state.playback_state)
306}
307
308// TEST: Add tests for this
309/// Opens a URI that can either be a track, a directory with tracks, or an M3U8 playlist file.
310pub(crate) fn open_uri(uri: PathBuf) -> Result<(), chilen_ipc::Error> {
311    trace!("Opening URI {uri:?}");
312    match uri.try_exists() {
313        Ok(exists) => {
314            if !exists {
315                return Err(chilen_ipc::Error::PathDoesNotExist);
316            }
317        }
318        Err(e) => {
319            error!("Could not check if the URI {uri:?} exists: {e}");
320            return Err(chilen_ipc::Error::PathExistenceUnknown);
321        }
322    }
323    if uri.is_dir() {
324        trace!("The provided URI {uri:?} is a directory, indexing it");
325
326        let mut files = Vec::new();
327        for result in WalkDir::new(uri).into_iter() {
328            let entry = match result {
329                Ok(entry) => entry,
330                Err(e) => {
331                    warn!("Error while trying to access the file: {e}");
332                    continue;
333                }
334            };
335
336            match entry.metadata() {
337                Ok(meta) => {
338                    if meta.is_file() {
339                        files.push(PathBuf::from(entry.path()));
340                    }
341                }
342                Err(e) => {
343                    warn!("Could not get path metadata: {e}");
344                    continue;
345                }
346            };
347        }
348        let tracks = tracks_from_paths(&files, true)?;
349        if tracks.is_empty() {
350            Err(chilen_ipc::Error::DirectoryWithNoTracks)
351        } else {
352            set_queue(tracks)
353        }
354    } else {
355        trace!(
356            "The provided URI {uri:?} is a file, so we're either opening an audio file or a playlist"
357        );
358        if let Ok(paths) = tracks_from_m3u8(&uri) {
359            trace!("Loaded M3U8 playlist {uri:?}");
360            let tracks = tracks_from_paths(&paths, true)?;
361            return set_queue(tracks);
362        } else {
363            trace!("The file does not appear to be an M3U8 playlist");
364        }
365        let track = tracks_from_paths(&[uri], false)?;
366        set_queue(track)
367    }
368}
369
370pub(crate) fn set_queue(queue: Vec<Track>) -> Result<(), chilen_ipc::Error> {
371    trace!("Setting a new queue");
372    let mut state_guard = PLAYER_STATE.write().unwrap();
373    let state = unwrap_state_mut(state_guard.as_mut())?;
374    state.set_tracks(queue);
375    #[cfg(feature = "shuffle")]
376    if state.shuffle_state == ShuffleState::On {
377        state.shuffle();
378    }
379    let player_guard = PLAYER_HANDLE.read().unwrap();
380    let player = unwrap_player(player_guard.as_ref())?;
381    player.stop();
382    if let Some(track) = state.current() {
383        let source = match track.open_source() {
384            Ok(source) => source,
385            Err(e) => {
386                error!("Could not open audio source: {e}");
387                return Err(chilen_ipc::Error::SourceError);
388            }
389        };
390        player.append(source);
391        player.pause();
392    }
393    background_save_state(state.clone());
394    Ok(())
395}
396
397pub(crate) fn append_to_queue(queue: &mut Vec<Track>) -> Result<(), chilen_ipc::Error> {
398    trace!("Appending tracks to queue");
399    let mut state_guard = PLAYER_STATE.write().unwrap();
400    let state = unwrap_state_mut(state_guard.as_mut())?;
401    state.append_tracks(queue);
402    background_save_state(state.clone());
403    Ok(())
404}
405
406pub(crate) fn get_current_track() -> Result<Option<Track>, chilen_ipc::Error> {
407    let state_guard = PLAYER_STATE.read().unwrap();
408    let state = unwrap_state_ref(state_guard.as_ref())?;
409    match state.current() {
410        Some(track) => Ok(Some(track.clone())),
411        None => Ok(None),
412    }
413}
414
415#[cfg(feature = "mpris")]
416pub(crate) fn get_current_meta() -> Result<Option<mpris_server::Metadata>, chilen_ipc::Error> {
417    let state_guard = PLAYER_STATE.read().unwrap();
418    let state = unwrap_state_ref(state_guard.as_ref())?;
419    match state.current() {
420        Some(track) => Ok(Some(track.get_meta(state.position))),
421        None => Ok(None),
422    }
423}
424
425pub(crate) fn skip_next() -> Result<(), chilen_ipc::Error> {
426    trace!("Skipping to the next track");
427    let mut state_guard = PLAYER_STATE.write().unwrap();
428    let state = unwrap_state_mut(state_guard.as_mut())?;
429    if state.can_go_next() {
430        let track = state.next().unwrap();
431        let source = match track.open_source() {
432            Ok(source) => source,
433            Err(e) => {
434                error!("Could not open audio source: {e}");
435                return Err(chilen_ipc::Error::SourceError);
436            }
437        };
438        background_save_state(state.clone());
439        let player_guard = PLAYER_HANDLE.read().unwrap();
440        let player = unwrap_player(player_guard.as_ref())?;
441        state.set_player_position(Duration::default());
442        state.set_playback_state(PlaybackState::Playing);
443        player.clear();
444        player.append(source);
445        player.play();
446        Ok(())
447    } else if state.is_empty() {
448        info!("Cannot skip to the next track, queue is empty");
449        Err(chilen_ipc::Error::QueueEmpty)
450    } else {
451        info!("Cannot skip to the next track");
452        Err(chilen_ipc::Error::CannotGoNext)
453    }
454}
455
456pub(crate) fn skip_previous() -> Result<(), chilen_ipc::Error> {
457    trace!("Skipping to the previous track");
458    let mut state_guard = PLAYER_STATE.write().unwrap();
459    let state = unwrap_state_mut(state_guard.as_mut())?;
460    if state.can_go_previous() {
461        let track = state.previous().unwrap();
462        let source = match track.open_source() {
463            Ok(source) => source,
464            Err(e) => {
465                error!("Could not open audio source: {e}");
466                return Err(chilen_ipc::Error::SourceError);
467            }
468        };
469        background_save_state(state.clone());
470        let player_guard = PLAYER_HANDLE.read().unwrap();
471        let player = unwrap_player(player_guard.as_ref())?;
472        state.set_player_position(Duration::default());
473        state.set_playback_state(PlaybackState::Playing);
474        player.clear();
475        player.append(source);
476        player.play();
477        Ok(())
478    } else if state.is_empty() {
479        info!("Cannot go to the previous track, queue is empty");
480        Err(chilen_ipc::Error::QueueEmpty)
481    } else {
482        info!("Cannot go to the previous track");
483        Err(chilen_ipc::Error::CannotGoPrevious)
484    }
485}
486
487pub(crate) fn set_loop_state(loop_state: LoopState) -> Result<(), chilen_ipc::Error> {
488    trace!("Setting loop state to {loop_state:?}");
489    let mut state_guard = PLAYER_STATE.write().unwrap();
490    let state = unwrap_state_mut(state_guard.as_mut())?;
491    state.set_loop_state(loop_state);
492    background_save_state(state.clone());
493    Ok(())
494}
495
496pub(crate) fn get_loop_state() -> Result<LoopState, chilen_ipc::Error> {
497    let state_guard = PLAYER_STATE.read().unwrap();
498    let state = unwrap_state_ref(state_guard.as_ref())?;
499    Ok(state.loop_state)
500}
501
502pub(crate) fn set_rate(rate: f64) -> Result<(), chilen_ipc::Error> {
503    let conf = CONFIG.read().unwrap();
504    if !conf.as_ref().unwrap().allow_rate_modification {
505        return Err(chilen_ipc::Error::FixedRate);
506    }
507    let mut state_guard = PLAYER_STATE.write().unwrap();
508    let state = unwrap_state_mut(state_guard.as_mut())?;
509    if !state.playback_rate.is_in_range(rate) {
510        Err(chilen_ipc::Error::RateOutOfRange)
511    } else {
512        let player_guard = PLAYER_HANDLE.read().unwrap();
513        let player = unwrap_player(player_guard.as_ref())?;
514        player.set_speed(PlaybackRate::from(rate).get_value_f32());
515        state.set_rate(rate);
516        background_save_state(state.clone());
517        Ok(())
518    }
519}
520
521pub(crate) fn get_rate() -> Result<PlaybackRate, chilen_ipc::Error> {
522    let state_guard = PLAYER_STATE.read().unwrap();
523    let state = unwrap_state_ref(state_guard.as_ref())?;
524    Ok(state.playback_rate)
525}
526
527pub(crate) fn set_shuffle_state(shuffle_state: ShuffleState) -> Result<(), chilen_ipc::Error> {
528    #[cfg(feature = "shuffle")]
529    {
530        trace!("Setting shuffle state to {shuffle_state:?}");
531        let mut state_guard = PLAYER_STATE.write().unwrap();
532        let state = unwrap_state_mut(state_guard.as_mut())?;
533        state.set_shuffle_state(shuffle_state);
534        state.shuffle();
535        background_save_state(state.clone());
536        Ok(())
537    }
538    #[cfg(not(feature = "shuffle"))]
539    {
540        info!("The daemon was built without shuffle support");
541        return Err(chilen_ipc::Error::ShuffleNotSupported);
542    }
543}
544
545pub(crate) fn get_shuffle_state() -> Result<ShuffleState, chilen_ipc::Error> {
546    let state_guard = PLAYER_STATE.read().unwrap();
547    let state = unwrap_state_ref(state_guard.as_ref())?;
548    Ok(state.shuffle_state)
549}
550
551pub(crate) fn set_player_position(position: Duration) -> Result<(), chilen_ipc::Error> {
552    trace!("Setting player position to {:?}", position.as_secs());
553    let player_guard = PLAYER_HANDLE.read().unwrap();
554    let mut state_guard = PLAYER_STATE.write().unwrap();
555    let state = unwrap_state_mut(state_guard.as_mut())?;
556    if state.playback_state == PlaybackState::Stopped {
557        return Err(chilen_ipc::Error::PlayerStopped);
558    }
559    let player = match player_guard.as_ref() {
560        Some(player) => player,
561        None => {
562            warn!("Cannot set player position, player is not connected");
563            return Err(chilen_ipc::Error::PlayerNotConnected);
564        }
565    };
566    if player.empty() {
567        Err(chilen_ipc::Error::QueueEmpty)
568    } else if let Some(track) = state.current()
569        && position > track.duration
570    {
571        skip_next()
572    } else if let Err(e) = player.try_seek(position) {
573        error!("Could not set player position: {e}");
574        // Prevent MPRIS from showing incorrect data if seek is not supported
575        #[cfg(feature = "mpris")]
576        mpris::set_position(state.player_position);
577        Err(chilen_ipc::Error::SeekNotSupported)
578    } else {
579        state.set_player_position(position);
580        Ok(())
581    }
582}
583
584/// When seeking, if the resulting position would result in a value less than this threshold, set
585/// the position to 0s.
586///
587/// Similarly, if the difference between the duration of the current track and the resulting
588/// position would be smaller than this value, skip to the next track.
589static SEEK_ROUND_THRESHOLD: LazyLock<Duration> = LazyLock::new(|| Duration::from_secs(1));
590
591// TODO: Add mime type(s) for M3U8 files (if it makes sense)
592/// Mime types supported by the music player.
593///
594/// Chilen uses the `rodio` crate for audio playback, which itself uses
595/// [Symphonia](https://github.com/pdeljanov/Symphonia) for decoding audio files. Chilen supports
596/// all audio formats Symphonia does.
597#[cfg(feature = "mpris")]
598static SUPPORTED_MIME_TYPES: LazyLock<Vec<String>> = LazyLock::new(|| {
599    let arr = [
600        "audio/aac",
601        "audio/32kadpcm",
602        "audio/aiff",
603        "audio/x-aiff",
604        "audio/x-caf",
605        "audio/flac",
606        "audio/matroska",
607        "audio/mpeg",
608        "audio/mp4",
609        "audio/MPA",
610        "audio/mpa-robust",
611        "audio/ogg",
612        "audio/vorbis",
613        "audio/vorbis-config",
614        "audio/vnd.wave",
615        "audio/wav",
616        "audio/wave",
617        "audio/x-wav",
618        "audio/webm",
619    ];
620    arr.into_iter().map(|t| t.to_string()).collect()
621});
622
623// FIX: Sometimes the MPRIS track metadata remains after the track switches in a seek operation
624pub(crate) fn seek(delta: SignedDuration) -> Result<(), chilen_ipc::Error> {
625    let player_guard = PLAYER_HANDLE.read().unwrap();
626    let player = match player_guard.as_ref() {
627        Some(player) => player,
628        None => {
629            warn!("Cannot seek, the player is not connected");
630            return Err(chilen_ipc::Error::PlayerNotConnected);
631        }
632    };
633    let mut state_guard = PLAYER_STATE.write().unwrap();
634    let state = unwrap_state_mut(state_guard.as_mut())?;
635    let pos = match delta {
636        SignedDuration::Positive(positive_dur) => {
637            if positive_dur == Duration::default() {
638                info!("Refusing to seek by 0s");
639                return Err(chilen_ipc::Error::InvalidDuration);
640            }
641            let sum = match positive_dur.checked_add(player.get_pos()) {
642                Some(sum) => sum,
643                None => {
644                    error!("Overflow detected while seeking, aborting");
645                    return Err(chilen_ipc::Error::DurationOverflow);
646                }
647            };
648            if let Some(track) = state.current()
649                && sum > track.duration - *SEEK_ROUND_THRESHOLD
650            {
651                drop(state_guard);
652                return skip_next();
653            } else {
654                trace!("Seeking player by {positive_dur:?}");
655                sum
656            }
657        }
658        SignedDuration::Negative(negative_dur) => {
659            if negative_dur == Duration::default() {
660                info!("Refusing to seek by 0s");
661                return Err(chilen_ipc::Error::InvalidDuration);
662            }
663            trace!("Seeking player by -{negative_dur:?}");
664            let sub = match player.get_pos().checked_sub(negative_dur) {
665                Some(sub) => sub,
666                None => {
667                    error!("Overflow detected while seeking");
668                    return Err(chilen_ipc::Error::DurationOverflow);
669                }
670            };
671            if sub < *SEEK_ROUND_THRESHOLD {
672                Duration::default()
673            } else {
674                sub
675            }
676        }
677    };
678    if let Err(e) = player.try_seek(pos) {
679        error!("Could not set player position: {e}");
680        Err(chilen_ipc::Error::SeekNotSupported)
681    } else {
682        state.set_player_position(pos);
683        Ok(())
684    }
685}
686
687pub(crate) fn get_player_position() -> Result<Duration, chilen_ipc::Error> {
688    let state_guard = PLAYER_STATE.read().unwrap();
689    let state = unwrap_state_ref(state_guard.as_ref())?;
690    Ok(state.player_position)
691}
692
693pub(crate) fn set_player_volume(volume: PlayerVolume) -> Result<(), chilen_ipc::Error> {
694    trace!("Setting player volume to {:?}", volume);
695    let player_guard = PLAYER_HANDLE.read().unwrap();
696    let player = unwrap_player(player_guard.as_ref())?;
697    player.set_volume(volume.get());
698    let mut state_guard = PLAYER_STATE.write().unwrap();
699    let state = unwrap_state_mut(state_guard.as_mut())?;
700    state.set_player_volume(volume);
701    background_save_state(state.clone());
702    Ok(())
703}
704
705pub(crate) fn get_player_volume() -> Result<PlayerVolume, chilen_ipc::Error> {
706    let player_guard = PLAYER_HANDLE.read().unwrap();
707    let player = unwrap_player(player_guard.as_ref())?;
708    Ok(PlayerVolume::new(player.volume()))
709}
710
711pub(crate) fn get_initial_events() -> Result<Vec<Event>, chilen_ipc::Error> {
712    let state_guard = PLAYER_STATE.read().unwrap();
713    let state = unwrap_state_ref(state_guard.as_ref())?;
714    Ok(state.get_initial_events())
715}
716
717pub(crate) fn init(config: Config) {
718    trace!("Initializing the playback module");
719
720    trace!("Initializing config");
721    *CONFIG.write().unwrap() = Some(config.clone());
722
723    let state = match restore_state_from_cache() {
724        Ok(state) => {
725            debug!("Restored player state from cache");
726            state
727        }
728        Err(e) => {
729            error!("Could not restore player state from cache: {e}");
730            let state = PlayerState::default();
731            debug!("Creating a new state and attempting to save it in cache");
732            background_save_state(state.clone());
733            state
734        }
735    };
736    trace!("Player state ready!");
737
738    state.send_initial_events();
739
740    let handle = match rodio::DeviceSinkBuilder::open_default_sink() {
741        Ok(sink) => sink,
742        Err(e) => {
743            error!("Could not open the default sink, audio playback will not work! Error: {e}");
744            return;
745        }
746    };
747    let player = Player::connect_new(handle.mixer());
748    player.set_volume(state.player_volume.get());
749
750    *PLAYER_STATE.write().unwrap() = Some(state);
751    *PLAYER_HANDLE.write().unwrap() = Some(player);
752
753    let mut state_guard = PLAYER_STATE.write().unwrap();
754    let state = state_guard.as_mut().unwrap();
755    if let Some(track) = state.current()
756        && let Ok(source) = track.open_source()
757    {
758        let player_guard = PLAYER_HANDLE.read().unwrap();
759        let player = player_guard.as_ref().unwrap();
760        state.player_position = Duration::default();
761        player.append(source);
762        player.pause();
763        player.set_speed(state.playback_rate.get_value_f32());
764        drop(player_guard);
765    }
766    drop(state_guard);
767
768    #[cfg(feature = "mpris")]
769    mpris::launch_server(config);
770
771    let mut initial_iter = true;
772    let sleep_duration = Duration::from_millis(100);
773    loop {
774        thread::sleep(sleep_duration);
775        let mut state_guard = PLAYER_STATE.write().unwrap();
776        let state = state_guard.as_mut().unwrap();
777        let player_guard = PLAYER_HANDLE.read().unwrap();
778        let player = player_guard.as_ref().unwrap();
779        if !player.is_paused() && !player.empty() {
780            state.increment_player_position(sleep_duration);
781        } else if player.empty() {
782            state.set_player_position(Duration::default());
783            if !state.can_go_next() {
784                state.set_playback_state(PlaybackState::Stopped);
785                continue;
786            }
787            let track = state.next().unwrap();
788            let source = match track.open_source() {
789                Ok(source) => source,
790                Err(e) => {
791                    error!("Could not open audio source: {e}");
792                    continue;
793                }
794            };
795            player.append(source);
796            if initial_iter {
797                player.pause();
798                initial_iter = false;
799                state.set_playback_state(PlaybackState::Stopped);
800            } else {
801                state.set_playback_state(PlaybackState::Playing);
802            }
803        }
804        drop(state_guard);
805        drop(player_guard);
806    }
807}
808
809#[cfg(feature = "mpris")]
810pub(crate) fn can_play() -> Result<bool, chilen_ipc::Error> {
811    let state_guard = PLAYER_STATE.read().unwrap();
812    let state = unwrap_state_ref(state_guard.as_ref())?;
813    Ok(state.can_play())
814}
815
816#[cfg(feature = "mpris")]
817pub(crate) fn can_pause() -> Result<bool, chilen_ipc::Error> {
818    let state_guard = PLAYER_STATE.read().unwrap();
819    let state = unwrap_state_ref(state_guard.as_ref())?;
820    Ok(state.playback_state == PlaybackState::Playing)
821}
822
823#[cfg(feature = "mpris")]
824pub(crate) fn can_go_next() -> Result<bool, chilen_ipc::Error> {
825    let state_guard = PLAYER_STATE.read().unwrap();
826    let state = unwrap_state_ref(state_guard.as_ref())?;
827    Ok(state.can_go_next())
828}
829
830#[cfg(feature = "mpris")]
831pub(crate) fn can_go_previous() -> Result<bool, chilen_ipc::Error> {
832    let state_guard = PLAYER_STATE.read().unwrap();
833    let state = unwrap_state_ref(state_guard.as_ref())?;
834    Ok(state.can_go_previous())
835}
836
837pub(crate) fn run_command(cmd: Command) -> Result<Response, chilen_ipc::Error> {
838    match cmd {
839        Command::Play(pos) => play(pos)?,
840        Command::Pause => pause()?,
841        Command::Stop => stop()?,
842        Command::TogglePlaying => toggle_playing()?,
843        Command::GetPlaybackState => {
844            return Ok(Response::Playback(PlaybackResponse::PlaybackState(
845                get_playback_state()?,
846            )));
847        }
848        Command::SetQueue(queue) => set_queue(queue)?,
849        Command::AppendToQueue(mut queue) => append_to_queue(&mut queue)?,
850        Command::GetCurrentTrack => {
851            return Ok(Response::Playback(match get_current_track()? {
852                Some(track) => PlaybackResponse::Track(Some(Box::new(track.into()))),
853                None => PlaybackResponse::Track(None),
854            }));
855        }
856        Command::Next => skip_next()?,
857        Command::Previous => skip_previous()?,
858        Command::SetLoopState(loop_state) => set_loop_state(loop_state)?,
859        Command::GetLoopState => {
860            return Ok(Response::Playback(PlaybackResponse::LoopState(
861                get_loop_state()?,
862            )));
863        }
864        Command::SetRate(rate) => set_rate(rate)?,
865        Command::GetRate => {
866            return Ok(Response::Playback(PlaybackResponse::PlaybackRate(
867                get_rate()?,
868            )));
869        }
870        Command::SetShuffleState(shuffle_state) => set_shuffle_state(shuffle_state)?,
871        #[cfg(feature = "shuffle")]
872        Command::GetShuffleState => {
873            return Ok(Response::Playback(PlaybackResponse::ShuffleState(
874                get_shuffle_state()?,
875            )));
876        }
877        Command::SetPlayerPosition(position) => set_player_position(position)?,
878        Command::Seek(delta) => seek(delta)?,
879        Command::GetPlayerPosition => {
880            return Ok(Response::Playback(PlaybackResponse::PlayerPosition(
881                get_player_position()?,
882            )));
883        }
884        Command::SetPlayerVolume(volume) => set_player_volume(volume)?,
885        Command::GetPlayerVolume => {
886            return Ok(Response::Playback(PlaybackResponse::PlayerVolume(
887                get_player_volume()?,
888            )));
889        }
890        Command::OpenURI(uri) => open_uri(uri.into())?,
891    }
892
893    Ok(Response::Ok)
894}