diff --git a/docs/modules/Music.md b/docs/modules/Music.md index 959aad3..5536ee6 100644 --- a/docs/modules/Music.md +++ b/docs/modules/Music.md @@ -128,8 +128,6 @@ and will be replaced with values from the currently playing track: | `{track}` | Track number | | `{disc}` | Disc number | | `{genre}` | Genre | -| `{duration}` | Duration in `mm:ss` | -| `{elapsed}` | Time elapsed in `mm:ss` | ## Styling @@ -166,7 +164,10 @@ and will be replaced with values from the currently playing track: | `.popup-music .controls .btn-pause` | Pause button inside popup box | | `.popup-music .controls .btn-next` | Next button inside popup box | | `.popup-music .volume` | Volume container inside popup box | -| `.popup-music .volume .slider` | Volume slider popup box | -| `.popup-music .volume .icon` | Volume icon label inside popup box | +| `.popup-music .volume .slider` | Slider inside volume container | +| `.popup-music .volume .icon` | Icon inside volume container | +| `.popup-music .progress` | Progress (seek) bar container | +| `.popup-music .progress .slider` | Slider inside progress container | +| `.popup-music .progress .label` | Duration label inside progress container | For more information on styling, please see the [styling guide](styling-guide). \ No newline at end of file diff --git a/src/clients/music/mod.rs b/src/clients/music/mod.rs index b71777b..c2161d5 100644 --- a/src/clients/music/mod.rs +++ b/src/clients/music/mod.rs @@ -9,9 +9,17 @@ pub mod mpd; #[cfg(feature = "music+mpris")] pub mod mpris; +pub const TICK_INTERVAL_MS: u64 = 200; + #[derive(Clone, Debug)] pub enum PlayerUpdate { + /// Triggered when the track or player state notably changes, + /// such as a new track playing, the player being paused, or a volume change. Update(Box>, Status), + /// Triggered at regular intervals while a track is playing. + /// Used to keep track of the progress through the current track. + ProgressTick(ProgressTick), + /// Triggered when the client disconnects from the player. Disconnect, } @@ -27,23 +35,27 @@ pub struct Track { pub cover_path: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum PlayerState { Playing, Paused, Stopped, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct Status { pub state: PlayerState, - pub volume_percent: u8, - pub duration: Option, - pub elapsed: Option, + pub volume_percent: Option, pub playlist_position: u32, pub playlist_length: u32, } +#[derive(Clone, Copy, Debug)] +pub struct ProgressTick { + pub duration: Option, + pub elapsed: Option, +} + pub trait MusicClient { fn play(&self) -> Result<()>; fn pause(&self) -> Result<()>; @@ -51,6 +63,7 @@ pub trait MusicClient { fn prev(&self) -> Result<()>; fn set_volume_percent(&self, vol: u8) -> Result<()>; + fn seek(&self, duration: Duration) -> Result<()>; fn subscribe_change(&self) -> broadcast::Receiver; } diff --git a/src/clients/music/mpd.rs b/src/clients/music/mpd.rs index 6079b12..742b172 100644 --- a/src/clients/music/mpd.rs +++ b/src/clients/music/mpd.rs @@ -1,9 +1,11 @@ -use super::{MusicClient, Status, Track}; -use crate::await_sync; -use crate::clients::music::{PlayerState, PlayerUpdate}; +use super::{ + MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS, +}; +use crate::{await_sync, send}; use color_eyre::Result; use lazy_static::lazy_static; use mpd_client::client::{Connection, ConnectionEvent, Subsystem}; +use mpd_client::commands::SeekMode; use mpd_client::protocol::MpdProtocolError; use mpd_client::responses::{PlayState, Song}; use mpd_client::tag::Tag; @@ -16,7 +18,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::net::{TcpStream, UnixStream}; use tokio::spawn; -use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender}; +use tokio::sync::broadcast; use tokio::sync::Mutex; use tokio::time::sleep; use tracing::{debug, error, info}; @@ -29,8 +31,8 @@ lazy_static! { pub struct MpdClient { client: Client, music_dir: PathBuf, - tx: Sender, - _rx: Receiver, + tx: broadcast::Sender, + _rx: broadcast::Receiver, } #[derive(Debug)] @@ -57,7 +59,7 @@ impl MpdClient { let (client, mut state_changes) = wait_for_connection(host, Duration::from_secs(5), None).await?; - let (tx, rx) = channel(16); + let (tx, rx) = broadcast::channel(16); { let music_dir = music_dir.clone(); @@ -78,7 +80,19 @@ impl MpdClient { } } - Ok::<(), SendError<(Option, Status)>>(()) + Ok::<(), broadcast::error::SendError<(Option, Status)>>(()) + }); + } + + { + let client = client.clone(); + let tx = tx.clone(); + + spawn(async move { + loop { + Self::send_tick_update(&client, &tx).await; + sleep(Duration::from_millis(TICK_INTERVAL_MS)).await; + } }); } @@ -92,9 +106,9 @@ impl MpdClient { async fn send_update( client: &Client, - tx: &Sender, + tx: &broadcast::Sender, music_dir: &Path, - ) -> Result<(), SendError> { + ) -> Result<(), broadcast::error::SendError> { let current_song = client.command(commands::CurrentSong).await; let status = client.command(commands::Status).await; @@ -102,17 +116,33 @@ impl MpdClient { let track = current_song.map(|s| Self::convert_song(&s.song, music_dir)); let status = Status::from(status); - tx.send(PlayerUpdate::Update(Box::new(track), status))?; + let update = PlayerUpdate::Update(Box::new(track), status); + send!(tx, update); } Ok(()) } + async fn send_tick_update(client: &Client, tx: &broadcast::Sender) { + let status = client.command(commands::Status).await; + + if let Ok(status) = status { + if status.state == PlayState::Playing { + let update = PlayerUpdate::ProgressTick(ProgressTick { + duration: status.duration, + elapsed: status.elapsed, + }); + + send!(tx, update); + } + } + } + fn is_connected(&self) -> bool { !self.client.is_connection_closed() } - fn send_disconnect_update(&self) -> Result<(), SendError> { + fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError> { info!("Connection to MPD server lost"); self.tx.send(PlayerUpdate::Disconnect)?; Ok(()) @@ -182,7 +212,12 @@ impl MusicClient for MpdClient { Ok(()) } - fn subscribe_change(&self) -> Receiver { + fn seek(&self, duration: Duration) -> Result<()> { + async_command!(self.client, commands::Seek(SeekMode::Absolute(duration))); + Ok(()) + } + + fn subscribe_change(&self) -> broadcast::Receiver { let rx = self.tx.subscribe(); await_sync(async { Self::send_update(&self.client, &self.tx, &self.music_dir) @@ -291,9 +326,7 @@ impl From for Status { fn from(status: mpd_client::responses::Status) -> Self { Self { state: PlayerState::from(status.state), - volume_percent: status.volume, - duration: status.duration, - elapsed: status.elapsed, + volume_percent: Some(status.volume), playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32), playlist_length: status.playlist_length as u32, } diff --git a/src/clients/music/mpris.rs b/src/clients/music/mpris.rs index 0ca5388..2cbc5a2 100644 --- a/src/clients/music/mpris.rs +++ b/src/clients/music/mpris.rs @@ -1,15 +1,15 @@ -use super::{MusicClient, PlayerUpdate, Status, Track}; -use crate::clients::music::PlayerState; +use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS}; +use crate::clients::music::ProgressTick; use crate::{arc_mut, lock, send}; use color_eyre::Result; use lazy_static::lazy_static; use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder}; use std::collections::HashSet; -use std::string; use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; -use tokio::sync::broadcast::{channel, Receiver, Sender}; +use std::{cmp, string}; +use tokio::sync::broadcast; use tokio::task::spawn_blocking; use tracing::{debug, error, trace}; @@ -19,13 +19,13 @@ lazy_static! { pub struct Client { current_player: Arc>>, - tx: Sender, - _rx: Receiver, + tx: broadcast::Sender, + _rx: broadcast::Receiver, } impl Client { fn new() -> Self { - let (tx, rx) = channel(32); + let (tx, rx) = broadcast::channel(32); let current_player = arc_mut!(None); @@ -84,6 +84,20 @@ impl Client { }); } + { + let current_player = current_player.clone(); + let tx = tx.clone(); + + spawn_blocking(move || { + let player_finder = PlayerFinder::new().expect("to get new player finder"); + + loop { + Self::send_tick_update(&player_finder, ¤t_player, &tx); + sleep(Duration::from_millis(TICK_INTERVAL_MS)); + } + }); + } + Self { current_player, tx, @@ -95,7 +109,7 @@ impl Client { player_id: String, players: Arc>>, current_player: Arc>>, - tx: Sender, + tx: broadcast::Sender, ) { spawn_blocking(move || { let player_finder = PlayerFinder::new()?; @@ -138,7 +152,7 @@ impl Client { }); } - fn send_update(player: &Player, tx: &Sender) -> Result<()> { + fn send_update(player: &Player, tx: &broadcast::Sender) -> Result<()> { debug!("Sending update using '{}'", player.identity()); let metadata = player.get_metadata()?; @@ -148,10 +162,7 @@ impl Client { let track_list = player.get_track_list(); - let volume_percent = player - .get_volume() - .map(|vol| (vol * 100.0) as u8) - .unwrap_or(0); + let volume_percent = player.get_volume().map(|vol| (vol * 100.0) as u8).ok(); let status = Status { // MRPIS doesn't seem to provide playlist info reliably, @@ -159,8 +170,6 @@ impl Client { playlist_position: 1, playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX), state: PlayerState::from(playback_status), - elapsed: player.get_position().ok(), - duration: metadata.length(), volume_percent, }; @@ -181,6 +190,26 @@ impl Client { player_finder.find_by_name(player_name).ok() }) } + + fn send_tick_update( + player_finder: &PlayerFinder, + current_player: &Mutex>, + tx: &broadcast::Sender, + ) { + if let Some(player) = lock!(current_player) + .as_ref() + .and_then(|name| player_finder.find_by_name(name).ok()) + { + if let Ok(metadata) = player.get_metadata() { + let update = PlayerUpdate::ProgressTick(ProgressTick { + elapsed: player.get_position().ok(), + duration: metadata.length(), + }); + + send!(tx, update); + } + } + } } macro_rules! command { @@ -223,7 +252,23 @@ impl MusicClient for Client { Ok(()) } - fn subscribe_change(&self) -> Receiver { + fn seek(&self, duration: Duration) -> Result<()> { + if let Some(player) = Self::get_player(self) { + let pos = player.get_position().unwrap_or_default(); + + let duration = duration.as_micros() as i64; + let position = pos.as_micros() as i64; + + let seek = cmp::max(duration, 0) - position; + + player.seek(seek)?; + } else { + error!("Could not find player"); + } + Ok(()) + } + + fn subscribe_change(&self) -> broadcast::Receiver { debug!("Creating new subscription"); let rx = self.tx.subscribe(); @@ -236,9 +281,7 @@ impl MusicClient for Client { playlist_position: 0, playlist_length: 0, state: PlayerState::Stopped, - elapsed: None, - duration: None, - volume_percent: 0, + volume_percent: None, }; send!(self.tx, PlayerUpdate::Update(Box::new(None), status)); } @@ -257,9 +300,18 @@ impl From for Track { const KEY_GENRE: &str = "xesam:genre"; Self { - title: value.title().map(std::string::ToString::to_string), - album: value.album_name().map(std::string::ToString::to_string), - artist: value.artists().map(|artists| artists.join(", ")), + title: value + .title() + .map(std::string::ToString::to_string) + .and_then(replace_empty_none), + album: value + .album_name() + .map(std::string::ToString::to_string) + .and_then(replace_empty_none), + artist: value + .artists() + .map(|artists| artists.join(", ")) + .and_then(replace_empty_none), date: value .get(KEY_DATE) .and_then(mpris::MetadataValue::as_string) @@ -284,3 +336,11 @@ impl From for PlayerState { } } } + +fn replace_empty_none(string: String) -> Option { + if string.is_empty() { + None + } else { + Some(string) + } +} diff --git a/src/modules/music/mod.rs b/src/modules/music/mod.rs index 1fda33e..5b8ebc4 100644 --- a/src/modules/music/mod.rs +++ b/src/modules/music/mod.rs @@ -1,17 +1,20 @@ mod config; -use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track}; +use crate::clients::music::{ + self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, +}; use crate::gtk_helpers::add_class; use crate::image::{new_icon_button, new_icon_label, ImageProvider}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::popup::Popup; use crate::{send_async, try_send}; use color_eyre::Result; -use glib::Continue; +use glib::{Continue, PropertySet}; use gtk::prelude::*; use gtk::{Button, IconTheme, Label, Orientation, Scale}; use regex::Regex; use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::spawn; @@ -28,6 +31,7 @@ pub enum PlayerCommand { Pause, Next, Volume(u8), + Seek(Duration), } /// Formats a duration given in seconds @@ -47,6 +51,12 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec { .collect::>() } +#[derive(Clone, Debug)] +pub enum ControllerEvent { + Update(Option), + UpdateProgress(ProgressTick), +} + #[derive(Clone, Debug)] pub struct SongUpdate { song: Track, @@ -67,7 +77,7 @@ async fn get_client( } impl Module