mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 10:41:03 +02:00
feat: mpris support
Resolves #25. Completely refactors the MPD module to be the 'music' module. This now supports both MPD and MPRIS with the same UI for both. BREAKING CHANGE: The `mpd` module has been renamed to `music`. You will need to update the `type` value in your config and add `player_type` to continue using MPD. You will also need to update your styles.
This commit is contained in:
parent
8076412bfc
commit
6d8e647f12
14 changed files with 1165 additions and 496 deletions
|
@ -195,7 +195,7 @@ fn add_modules(content: >k::Box, modules: Vec<ModuleConfig>, info: &ModuleInfo
|
|||
ModuleConfig::Focused(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Tray(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Mpd(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Music(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Launcher(mut module) => add_module!(module, id),
|
||||
ModuleConfig::Custom(mut module) => add_module!(module, id),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub mod mpd;
|
||||
pub mod music;
|
||||
pub mod sway;
|
||||
pub mod system_tray;
|
||||
pub mod wayland;
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
use lazy_static::lazy_static;
|
||||
use mpd_client::client::{CommandError, Connection, ConnectionEvent, Subsystem};
|
||||
use mpd_client::commands::Command;
|
||||
use mpd_client::protocol::MpdProtocolError;
|
||||
use mpd_client::responses::Status;
|
||||
use mpd_client::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::PathBuf;
|
||||
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::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tracing::debug;
|
||||
|
||||
lazy_static! {
|
||||
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub struct MpdClient {
|
||||
client: Client,
|
||||
tx: Sender<()>,
|
||||
_rx: Receiver<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MpdConnectionError {
|
||||
MaxRetries,
|
||||
ProtocolError(MpdProtocolError),
|
||||
}
|
||||
|
||||
impl Display for MpdConnectionError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MaxRetries => write!(f, "Reached max retries"),
|
||||
Self::ProtocolError(e) => write!(f, "{:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MpdConnectionError {}
|
||||
|
||||
impl MpdClient {
|
||||
async fn new(host: &str) -> Result<Self, MpdConnectionError> {
|
||||
debug!("Creating new MPD connection to {}", host);
|
||||
|
||||
let (client, mut state_changes) =
|
||||
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||
|
||||
let (tx, rx) = channel(16);
|
||||
let tx2 = tx.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(change) = state_changes.next().await {
|
||||
debug!("Received state change: {:?}", change);
|
||||
|
||||
if let ConnectionEvent::SubsystemChange(
|
||||
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
|
||||
) = change
|
||||
{
|
||||
tx2.send(())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), SendError<()>>(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
tx,
|
||||
_rx: rx,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> Receiver<()> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn command<C: Command>(&self, command: C) -> Result<C::Response, CommandError> {
|
||||
self.client.command(command).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client(host: &str) -> Result<Arc<MpdClient>, MpdConnectionError> {
|
||||
let mut connections = CONNECTIONS.lock().await;
|
||||
match connections.get(host) {
|
||||
None => {
|
||||
let client = MpdClient::new(host).await?;
|
||||
let client = Arc::new(client);
|
||||
connections.insert(host.to_string(), Arc::clone(&client));
|
||||
Ok(client)
|
||||
}
|
||||
Some(client) => Ok(Arc::clone(client)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_connection(
|
||||
host: &str,
|
||||
interval: Duration,
|
||||
max_retries: Option<usize>,
|
||||
) -> Result<Connection, MpdConnectionError> {
|
||||
let mut retries = 0;
|
||||
let max_retries = max_retries.unwrap_or(usize::MAX);
|
||||
|
||||
loop {
|
||||
if retries == max_retries {
|
||||
break Err(MpdConnectionError::MaxRetries);
|
||||
}
|
||||
|
||||
retries += 1;
|
||||
|
||||
match try_get_mpd_conn(host).await {
|
||||
Ok(conn) => break Ok(conn),
|
||||
Err(err) => {
|
||||
if retries == max_retries {
|
||||
break Err(MpdConnectionError::ProtocolError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycles through each MPD host and
|
||||
/// returns the first one which connects,
|
||||
/// or none if there are none
|
||||
async fn try_get_mpd_conn(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
if is_unix_socket(host) {
|
||||
connect_unix(host).await
|
||||
} else {
|
||||
connect_tcp(host).await
|
||||
}
|
||||
}
|
||||
|
||||
fn is_unix_socket(host: &str) -> bool {
|
||||
let path = PathBuf::from(host);
|
||||
path.exists()
|
||||
&& path
|
||||
.metadata()
|
||||
.map_or(false, |metadata| metadata.file_type().is_socket())
|
||||
}
|
||||
|
||||
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
let connection = UnixStream::connect(host).await?;
|
||||
Client::connect(connection).await
|
||||
}
|
||||
|
||||
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
let connection = TcpStream::connect(host).await?;
|
||||
Client::connect(connection).await
|
||||
}
|
||||
|
||||
/// Gets the duration of the current song
|
||||
pub fn get_duration(status: &Status) -> Option<u64> {
|
||||
status.duration.map(|duration| duration.as_secs())
|
||||
}
|
||||
|
||||
/// Gets the elapsed time of the current song
|
||||
pub fn get_elapsed(status: &Status) -> Option<u64> {
|
||||
status.elapsed.map(|duration| duration.as_secs())
|
||||
}
|
66
src/clients/music/mod.rs
Normal file
66
src/clients/music/mod.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use color_eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub mod mpd;
|
||||
pub mod mpris;
|
||||
|
||||
pub type PlayerUpdate = (Option<Track>, Status);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Track {
|
||||
pub title: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub date: Option<String>,
|
||||
pub disc: Option<u64>,
|
||||
pub genre: Option<String>,
|
||||
pub track: Option<u64>,
|
||||
pub cover_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlayerState {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Status {
|
||||
pub state: PlayerState,
|
||||
pub volume_percent: u8,
|
||||
pub duration: Option<Duration>,
|
||||
pub elapsed: Option<Duration>,
|
||||
pub playlist_position: u32,
|
||||
pub playlist_length: u32,
|
||||
}
|
||||
|
||||
pub trait MusicClient {
|
||||
fn play(&self) -> Result<()>;
|
||||
fn pause(&self) -> Result<()>;
|
||||
fn next(&self) -> Result<()>;
|
||||
fn prev(&self) -> Result<()>;
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||
}
|
||||
|
||||
pub enum ClientType<'a> {
|
||||
Mpd { host: &'a str, music_dir: PathBuf },
|
||||
Mpris,
|
||||
}
|
||||
|
||||
pub async fn get_client(client_type: ClientType<'_>) -> Box<Arc<dyn MusicClient>> {
|
||||
match client_type {
|
||||
ClientType::Mpd { host, music_dir } => Box::new(
|
||||
mpd::get_client(host, music_dir)
|
||||
.await
|
||||
.expect("Failed to connect to MPD client"),
|
||||
),
|
||||
ClientType::Mpris => Box::new(mpris::get_client()),
|
||||
}
|
||||
}
|
282
src/clients/music/mpd.rs
Normal file
282
src/clients/music/mpd.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
use super::{MusicClient, Status, Track};
|
||||
use crate::await_sync;
|
||||
use crate::clients::music::{PlayerState, PlayerUpdate};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
||||
use mpd_client::protocol::MpdProtocolError;
|
||||
use mpd_client::responses::{PlayState, Song};
|
||||
use mpd_client::tag::Tag;
|
||||
use mpd_client::{commands, Client};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
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::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error};
|
||||
|
||||
lazy_static! {
|
||||
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub struct MpdClient {
|
||||
client: Client,
|
||||
music_dir: PathBuf,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
_rx: Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MpdConnectionError {
|
||||
MaxRetries,
|
||||
ProtocolError(MpdProtocolError),
|
||||
}
|
||||
|
||||
impl Display for MpdConnectionError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MaxRetries => write!(f, "Reached max retries"),
|
||||
Self::ProtocolError(e) => write!(f, "{e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MpdConnectionError {}
|
||||
|
||||
impl MpdClient {
|
||||
async fn new(host: &str, music_dir: PathBuf) -> Result<Self, MpdConnectionError> {
|
||||
debug!("Creating new MPD connection to {}", host);
|
||||
|
||||
let (client, mut state_changes) =
|
||||
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||
|
||||
let (tx, rx) = channel(16);
|
||||
|
||||
{
|
||||
let music_dir = music_dir.clone();
|
||||
let tx = tx.clone();
|
||||
let client = client.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(change) = state_changes.next().await {
|
||||
debug!("Received state change: {:?}", change);
|
||||
|
||||
if let ConnectionEvent::SubsystemChange(
|
||||
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
|
||||
) = change
|
||||
{
|
||||
Self::send_update(&client, &tx, &music_dir).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), SendError<(Option<Track>, Status)>>(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
music_dir,
|
||||
tx,
|
||||
_rx: rx,
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_update(
|
||||
client: &Client,
|
||||
tx: &Sender<PlayerUpdate>,
|
||||
music_dir: &Path,
|
||||
) -> Result<(), SendError<(Option<Track>, Status)>> {
|
||||
let current_song = client.command(commands::CurrentSong).await;
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
if let (Ok(current_song), Ok(status)) = (current_song, status) {
|
||||
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
||||
let status = Status::from(status);
|
||||
|
||||
tx.send((track, status))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_song(song: &Song, music_dir: &Path) -> Track {
|
||||
let (track, disc) = song.number();
|
||||
|
||||
let cover_path = music_dir.join(
|
||||
song.file_path()
|
||||
.parent()
|
||||
.expect("Song path should not be root")
|
||||
.join("cover.jpg"),
|
||||
);
|
||||
|
||||
Track {
|
||||
title: song.title().map(std::string::ToString::to_string),
|
||||
album: song.album().map(std::string::ToString::to_string),
|
||||
artist: Some(song.artists().join(", ")),
|
||||
date: try_get_first_tag(song, &Tag::Date).map(std::string::ToString::to_string),
|
||||
genre: try_get_first_tag(song, &Tag::Genre).map(std::string::ToString::to_string),
|
||||
disc: Some(disc),
|
||||
track: Some(track),
|
||||
cover_path: Some(cover_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! async_command {
|
||||
($client:expr, $command:expr) => {
|
||||
await_sync(async {
|
||||
$client
|
||||
.command($command)
|
||||
.await
|
||||
.unwrap_or_else(|err| error!("Failed to send command: {err:?}"))
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
impl MusicClient for MpdClient {
|
||||
fn play(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::SetPause(false));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::SetPause(true));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::Next);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::Previous);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||
async_command!(self.client, commands::SetVolume(vol));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||
let rx = self.tx.subscribe();
|
||||
await_sync(async {
|
||||
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
||||
.await
|
||||
.expect("Failed to send player update");
|
||||
});
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client(
|
||||
host: &str,
|
||||
music_dir: PathBuf,
|
||||
) -> Result<Arc<MpdClient>, MpdConnectionError> {
|
||||
let mut connections = CONNECTIONS.lock().await;
|
||||
match connections.get(host) {
|
||||
None => {
|
||||
let client = MpdClient::new(host, music_dir).await?;
|
||||
let client = Arc::new(client);
|
||||
connections.insert(host.to_string(), Arc::clone(&client));
|
||||
Ok(client)
|
||||
}
|
||||
Some(client) => Ok(Arc::clone(client)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_connection(
|
||||
host: &str,
|
||||
interval: Duration,
|
||||
max_retries: Option<usize>,
|
||||
) -> Result<Connection, MpdConnectionError> {
|
||||
let mut retries = 0;
|
||||
let max_retries = max_retries.unwrap_or(usize::MAX);
|
||||
|
||||
loop {
|
||||
if retries == max_retries {
|
||||
break Err(MpdConnectionError::MaxRetries);
|
||||
}
|
||||
|
||||
retries += 1;
|
||||
|
||||
match try_get_mpd_conn(host).await {
|
||||
Ok(conn) => break Ok(conn),
|
||||
Err(err) => {
|
||||
if retries == max_retries {
|
||||
break Err(MpdConnectionError::ProtocolError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycles through each MPD host and
|
||||
/// returns the first one which connects,
|
||||
/// or none if there are none
|
||||
async fn try_get_mpd_conn(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
if is_unix_socket(host) {
|
||||
connect_unix(host).await
|
||||
} else {
|
||||
connect_tcp(host).await
|
||||
}
|
||||
}
|
||||
|
||||
fn is_unix_socket(host: &str) -> bool {
|
||||
let path = PathBuf::from(host);
|
||||
path.exists()
|
||||
&& path
|
||||
.metadata()
|
||||
.map_or(false, |metadata| metadata.file_type().is_socket())
|
||||
}
|
||||
|
||||
async fn connect_unix(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
let connection = UnixStream::connect(host).await?;
|
||||
Client::connect(connection).await
|
||||
}
|
||||
|
||||
async fn connect_tcp(host: &str) -> Result<Connection, MpdProtocolError> {
|
||||
let connection = TcpStream::connect(host).await?;
|
||||
Client::connect(connection).await
|
||||
}
|
||||
|
||||
/// Attempts to read the first value for a tag
|
||||
/// (since the MPD client returns a vector of tags, or None)
|
||||
pub fn try_get_first_tag<'a>(song: &'a Song, tag: &'a Tag) -> Option<&'a str> {
|
||||
song.tags
|
||||
.get(tag)
|
||||
.and_then(|vec| vec.first().map(String::as_str))
|
||||
}
|
||||
|
||||
impl From<mpd_client::responses::Status> 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,
|
||||
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
||||
playlist_length: status.playlist_length as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayState> for PlayerState {
|
||||
fn from(value: PlayState) -> Self {
|
||||
match value {
|
||||
PlayState::Stopped => Self::Stopped,
|
||||
PlayState::Playing => Self::Playing,
|
||||
PlayState::Paused => Self::Paused,
|
||||
}
|
||||
}
|
||||
}
|
283
src/clients/music/mpris.rs
Normal file
283
src/clients/music/mpris.rs
Normal file
|
@ -0,0 +1,283 @@
|
|||
use super::{MusicClient, PlayerUpdate, Status, Track};
|
||||
use crate::clients::music::PlayerState;
|
||||
use crate::error::ERR_MUTEX_LOCK;
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Client> = Arc::new(Client::new());
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
_rx: Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = channel(32);
|
||||
|
||||
let current_player = Arc::new(Mutex::new(None));
|
||||
|
||||
{
|
||||
let players_list = Arc::new(Mutex::new(HashSet::new()));
|
||||
let current_player = current_player.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus");
|
||||
|
||||
// D-Bus gives no event for new players,
|
||||
// so we have to keep polling the player list
|
||||
loop {
|
||||
let players = player_finder
|
||||
.find_all()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
let mut players_list_val = players_list.lock().expect(ERR_MUTEX_LOCK);
|
||||
for player in players {
|
||||
let identity = player.identity();
|
||||
|
||||
if !players_list_val.contains(identity) {
|
||||
debug!("Adding MPRIS player '{identity}'");
|
||||
players_list_val.insert(identity.to_string());
|
||||
|
||||
let status = player
|
||||
.get_playback_status()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
{
|
||||
let mut current_player =
|
||||
current_player.lock().expect(ERR_MUTEX_LOCK);
|
||||
|
||||
if status == PlaybackStatus::Playing || current_player.is_none() {
|
||||
debug!("Setting active player to '{identity}'");
|
||||
|
||||
current_player.replace(identity.to_string());
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::listen_player_events(
|
||||
identity.to_string(),
|
||||
players_list.clone(),
|
||||
current_player.clone(),
|
||||
tx.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// wait 1 second before re-checking players
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
current_player,
|
||||
tx,
|
||||
_rx: rx,
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_player_events(
|
||||
player_id: String,
|
||||
players: Arc<Mutex<HashSet<String>>>,
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: Sender<PlayerUpdate>,
|
||||
) {
|
||||
spawn_blocking(move || {
|
||||
let player_finder = PlayerFinder::new()?;
|
||||
|
||||
if let Ok(player) = player_finder.find_by_name(&player_id) {
|
||||
let identity = player.identity();
|
||||
|
||||
for event in player.events()? {
|
||||
trace!("Received player event from '{identity}': {event:?}");
|
||||
match event {
|
||||
Ok(Event::PlayerShutDown) => {
|
||||
current_player.lock().expect(ERR_MUTEX_LOCK).take();
|
||||
players.lock().expect(ERR_MUTEX_LOCK).remove(identity);
|
||||
break;
|
||||
}
|
||||
Ok(Event::Playing) => {
|
||||
current_player
|
||||
.lock()
|
||||
.expect(ERR_MUTEX_LOCK)
|
||||
.replace(identity.to_string());
|
||||
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
let current_player = current_player.lock().expect(ERR_MUTEX_LOCK);
|
||||
let current_player = current_player.as_ref();
|
||||
if let Some(current_player) = current_player {
|
||||
if current_player == identity {
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), DBusError>(())
|
||||
});
|
||||
}
|
||||
|
||||
fn send_update(player: &Player, tx: &Sender<PlayerUpdate>) -> Result<()> {
|
||||
debug!("Sending update using '{}'", player.identity());
|
||||
|
||||
let metadata = player.get_metadata()?;
|
||||
let playback_status = player
|
||||
.get_playback_status()
|
||||
.unwrap_or(PlaybackStatus::Stopped);
|
||||
|
||||
let track_list = player.get_track_list();
|
||||
|
||||
let volume_percent = player
|
||||
.get_volume()
|
||||
.map(|vol| (vol * 100.0) as u8)
|
||||
.unwrap_or(0);
|
||||
|
||||
let status = Status {
|
||||
playlist_position: 0,
|
||||
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(1),
|
||||
state: PlayerState::from(playback_status),
|
||||
elapsed: player.get_position().ok(),
|
||||
duration: metadata.length(),
|
||||
volume_percent,
|
||||
};
|
||||
|
||||
let track = Track::from(metadata);
|
||||
|
||||
let player_update: PlayerUpdate = (Some(track), status);
|
||||
|
||||
tx.send(player_update)
|
||||
.expect("Failed to send player update");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_player(&self) -> Option<Player> {
|
||||
let player_name = self.current_player.lock().expect(ERR_MUTEX_LOCK);
|
||||
let player_name = player_name.as_ref();
|
||||
|
||||
player_name.and_then(|player_name| {
|
||||
let player_finder = PlayerFinder::new().expect("Failed to connect to D-Bus");
|
||||
player_finder.find_by_name(player_name).ok()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! command {
|
||||
($self:ident, $func:ident) => {
|
||||
if let Some(player) = Self::get_player($self) {
|
||||
player.$func()?;
|
||||
} else {
|
||||
error!("Could not find player");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl MusicClient for Client {
|
||||
fn play(&self) -> Result<()> {
|
||||
command!(self, play);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<()> {
|
||||
command!(self, pause);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next(&self) -> Result<()> {
|
||||
command!(self, next);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&self) -> Result<()> {
|
||||
command!(self, previous);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||
if let Some(player) = Self::get_player(self) {
|
||||
player.set_volume(vol as f64 / 100.0)?;
|
||||
} else {
|
||||
error!("Could not find player");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
||||
debug!("Creating new subscription");
|
||||
let rx = self.tx.subscribe();
|
||||
|
||||
if let Some(player) = self.get_player() {
|
||||
if let Err(err) = Self::send_update(&player, &self.tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Client> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
impl From<Metadata> for Track {
|
||||
fn from(value: Metadata) -> Self {
|
||||
const KEY_DATE: &str = "xesam:contentCreated";
|
||||
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(", ")),
|
||||
date: value
|
||||
.get(KEY_DATE)
|
||||
.and_then(mpris::MetadataValue::as_string)
|
||||
.map(std::string::ToString::to_string),
|
||||
disc: value.disc_number().map(|disc| disc as u64),
|
||||
genre: value
|
||||
.get(KEY_GENRE)
|
||||
.and_then(mpris::MetadataValue::as_str_array)
|
||||
.and_then(|arr| arr.first().map(|val| (*val).to_string())),
|
||||
track: value.track_number().map(|track| track as u64),
|
||||
cover_path: value
|
||||
.art_url()
|
||||
.map(|path| path.replace("file://", ""))
|
||||
.map(PathBuf::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlaybackStatus> for PlayerState {
|
||||
fn from(value: PlaybackStatus) -> Self {
|
||||
match value {
|
||||
PlaybackStatus::Playing => Self::Playing,
|
||||
PlaybackStatus::Paused => Self::Paused,
|
||||
PlaybackStatus::Stopped => Self::Stopped,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ use crate::modules::clock::ClockModule;
|
|||
use crate::modules::custom::CustomModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
use crate::modules::mpd::MpdModule;
|
||||
use crate::modules::music::MusicModule;
|
||||
use crate::modules::script::ScriptModule;
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
use crate::modules::tray::TrayModule;
|
||||
|
@ -30,7 +30,7 @@ pub struct CommonConfig {
|
|||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ModuleConfig {
|
||||
Clock(ClockModule),
|
||||
Mpd(MpdModule),
|
||||
Music(MusicModule),
|
||||
Tray(TrayModule),
|
||||
Workspaces(WorkspacesModule),
|
||||
SysInfo(SysInfoModule),
|
||||
|
|
|
@ -8,7 +8,7 @@ pub mod clock;
|
|||
pub mod custom;
|
||||
pub mod focused;
|
||||
pub mod launcher;
|
||||
pub mod mpd;
|
||||
pub mod music;
|
||||
pub mod script;
|
||||
pub mod sysinfo;
|
||||
pub mod tray;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::clients::mpd::{get_client, get_duration, get_elapsed, MpdConnectionError};
|
||||
use crate::clients::music::{self, MusicClient, PlayerState, Status, Track};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::Popup;
|
||||
|
@ -9,12 +9,11 @@ use glib::Continue;
|
|||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Image, Label, Orientation, Scale};
|
||||
use mpd_client::commands;
|
||||
use mpd_client::responses::{PlayState, Song, Status};
|
||||
use mpd_client::tag::Tag;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
|
@ -23,7 +22,8 @@ use tracing::error;
|
|||
#[derive(Debug)]
|
||||
pub enum PlayerCommand {
|
||||
Previous,
|
||||
Toggle,
|
||||
Play,
|
||||
Pause,
|
||||
Next,
|
||||
Volume(u8),
|
||||
}
|
||||
|
@ -51,11 +51,26 @@ impl Default for Icons {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PlayerType {
|
||||
// Auto,
|
||||
Mpd,
|
||||
Mpris,
|
||||
}
|
||||
|
||||
impl Default for PlayerType {
|
||||
fn default() -> Self {
|
||||
Self::Mpris
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct MpdModule {
|
||||
/// TCP or Unix socket address.
|
||||
#[serde(default = "default_socket")]
|
||||
host: String,
|
||||
pub struct MusicModule {
|
||||
/// Type of player to connect to
|
||||
#[serde(default)]
|
||||
player_type: PlayerType,
|
||||
|
||||
/// Format of current song info to display on the bar.
|
||||
#[serde(default = "default_format")]
|
||||
format: String,
|
||||
|
@ -64,6 +79,10 @@ pub struct MpdModule {
|
|||
#[serde(default)]
|
||||
icons: Icons,
|
||||
|
||||
// -- MPD --
|
||||
/// TCP or Unix socket address.
|
||||
#[serde(default = "default_socket")]
|
||||
host: String,
|
||||
/// Path to root of music directory.
|
||||
#[serde(default = "default_music_dir")]
|
||||
music_dir: PathBuf,
|
||||
|
@ -96,15 +115,10 @@ fn default_music_dir() -> PathBuf {
|
|||
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Attempts to read the first value for a tag
|
||||
/// (since the MPD client returns a vector of tags, or None)
|
||||
pub fn try_get_first_tag(vec: Option<&Vec<String>>) -> Option<&str> {
|
||||
vec.and_then(|vec| vec.first().map(String::as_str))
|
||||
}
|
||||
|
||||
/// Formats a duration given in seconds
|
||||
/// in hh:mm format
|
||||
fn format_time(time: u64) -> String {
|
||||
fn format_time(duration: Duration) -> String {
|
||||
let time = duration.as_secs();
|
||||
let minutes = (time / 60) % 60;
|
||||
let seconds = time % 60;
|
||||
|
||||
|
@ -120,17 +134,29 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SongUpdate {
|
||||
song: Song,
|
||||
song: Track,
|
||||
status: Status,
|
||||
display_string: String,
|
||||
}
|
||||
|
||||
impl Module<Button> for MpdModule {
|
||||
async fn get_client(
|
||||
player_type: PlayerType,
|
||||
host: &str,
|
||||
music_dir: PathBuf,
|
||||
) -> Box<Arc<dyn MusicClient>> {
|
||||
match player_type {
|
||||
PlayerType::Mpd => music::get_client(music::ClientType::Mpd { host, music_dir }),
|
||||
PlayerType::Mpris => music::get_client(music::ClientType::Mpris {}),
|
||||
}
|
||||
.await
|
||||
}
|
||||
|
||||
impl Module<Button> for MusicModule {
|
||||
type SendMessage = Option<SongUpdate>;
|
||||
type ReceiveMessage = PlayerCommand;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"mpd"
|
||||
"music"
|
||||
}
|
||||
|
||||
fn spawn_controller(
|
||||
|
@ -139,73 +165,69 @@ impl Module<Button> for MpdModule {
|
|||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let host1 = self.host.clone();
|
||||
let host2 = self.host.clone();
|
||||
let format = self.format.clone();
|
||||
let icons = self.icons.clone();
|
||||
|
||||
let re = Regex::new(r"\{([\w-]+)}")?;
|
||||
let tokens = get_tokens(&re, self.format.as_str());
|
||||
|
||||
// poll mpd server
|
||||
spawn(async move {
|
||||
let client = get_client(&host1).await.expect("Failed to connect to MPD");
|
||||
let mut mpd_rx = client.subscribe();
|
||||
// receive player updates
|
||||
{
|
||||
let player_type = self.player_type;
|
||||
let host = self.host.clone();
|
||||
let music_dir = self.music_dir.clone();
|
||||
|
||||
loop {
|
||||
let current_song = client.command(commands::CurrentSong).await;
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
if let (Ok(Some(song)), Ok(status)) = (current_song, status) {
|
||||
let display_string =
|
||||
replace_tokens(format.as_str(), &tokens, &song.song, &status, &icons);
|
||||
|
||||
let update = SongUpdate {
|
||||
song: song.song,
|
||||
status,
|
||||
display_string,
|
||||
};
|
||||
|
||||
tx.send(ModuleUpdateEvent::Update(Some(update))).await?;
|
||||
} else {
|
||||
tx.send(ModuleUpdateEvent::Update(None)).await?;
|
||||
}
|
||||
|
||||
// wait for player state change
|
||||
if mpd_rx.recv().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
||||
});
|
||||
|
||||
// listen to ui events
|
||||
spawn(async move {
|
||||
let client = get_client(&host2).await?;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
let res = match event {
|
||||
PlayerCommand::Previous => client.command(commands::Previous).await,
|
||||
PlayerCommand::Toggle => match client.command(commands::Status).await {
|
||||
Ok(status) => match status.state {
|
||||
PlayState::Playing => client.command(commands::SetPause(true)).await,
|
||||
PlayState::Paused => client.command(commands::SetPause(false)).await,
|
||||
PlayState::Stopped => Ok(()),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
PlayerCommand::Next => client.command(commands::Next).await,
|
||||
PlayerCommand::Volume(vol) => client.command(commands::SetVolume(vol)).await,
|
||||
spawn(async move {
|
||||
let mut rx = {
|
||||
let client = get_client(player_type, &host, music_dir).await;
|
||||
client.subscribe_change()
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("Failed to send command to MPD server: {:?}", err);
|
||||
}
|
||||
}
|
||||
while let Ok((track, status)) = rx.recv().await {
|
||||
match track {
|
||||
Some(track) => {
|
||||
let display_string =
|
||||
replace_tokens(format.as_str(), &tokens, &track, &status, &icons);
|
||||
|
||||
Ok::<(), MpdConnectionError>(())
|
||||
});
|
||||
let update = SongUpdate {
|
||||
song: track,
|
||||
status,
|
||||
display_string,
|
||||
};
|
||||
|
||||
tx.send(ModuleUpdateEvent::Update(Some(update))).await?;
|
||||
}
|
||||
None => tx.send(ModuleUpdateEvent::Update(None)).await?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
||||
});
|
||||
}
|
||||
|
||||
// listen to ui events
|
||||
{
|
||||
let player_type = self.player_type;
|
||||
let host = self.host.clone();
|
||||
let music_dir = self.music_dir.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
let client = get_client(player_type, &host, music_dir.clone()).await;
|
||||
let res = match event {
|
||||
PlayerCommand::Previous => client.prev(),
|
||||
PlayerCommand::Play => client.play(),
|
||||
PlayerCommand::Pause => client.pause(),
|
||||
PlayerCommand::Next => client.next(),
|
||||
PlayerCommand::Volume(vol) => client.set_volume_percent(vol), // .unwrap_or_else(|_| error!("Failed to update player volume")),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("Failed to send command to server: {:?}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -266,7 +288,7 @@ impl Module<Button> for MpdModule {
|
|||
let container = gtk::Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(10)
|
||||
.name("popup-mpd")
|
||||
.name("popup-music")
|
||||
.build();
|
||||
|
||||
let album_image = Image::builder()
|
||||
|
@ -325,8 +347,12 @@ impl Module<Button> for MpdModule {
|
|||
});
|
||||
|
||||
let tx_toggle = tx.clone();
|
||||
btn_play_pause.connect_clicked(move |_| {
|
||||
try_send!(tx_toggle, PlayerCommand::Toggle);
|
||||
btn_play_pause.connect_clicked(move |button| {
|
||||
if button.style_context().has_class("playing") {
|
||||
try_send!(tx_toggle, PlayerCommand::Pause);
|
||||
} else {
|
||||
try_send!(tx_toggle, PlayerCommand::Play);
|
||||
}
|
||||
});
|
||||
|
||||
let tx_next = tx.clone();
|
||||
|
@ -343,70 +369,66 @@ impl Module<Button> for MpdModule {
|
|||
container.show_all();
|
||||
|
||||
{
|
||||
let music_dir = self.music_dir;
|
||||
|
||||
let mut prev_cover = None;
|
||||
rx.attach(None, move |update| {
|
||||
if let Some(update) = update {
|
||||
let prev_album = album_label.label.text();
|
||||
let curr_album = update.song.album().unwrap_or_default();
|
||||
|
||||
// only update art when album changes
|
||||
if prev_album != curr_album {
|
||||
let cover_path = music_dir.join(
|
||||
update
|
||||
.song
|
||||
.file_path()
|
||||
.parent()
|
||||
.expect("Song path should not be root")
|
||||
.join("cover.jpg"),
|
||||
);
|
||||
|
||||
Pixbuf::from_file_at_scale(cover_path, 128, 128, true).map_or_else(
|
||||
|_| {
|
||||
album_image.set_from_pixbuf(None);
|
||||
},
|
||||
|pixbuf| {
|
||||
album_image.set_from_pixbuf(Some(&pixbuf));
|
||||
},
|
||||
);
|
||||
let new_cover = update.song.cover_path;
|
||||
if prev_cover != new_cover {
|
||||
prev_cover = new_cover.clone();
|
||||
match new_cover.map(|cover_path| {
|
||||
Pixbuf::from_file_at_scale(cover_path, 128, 128, true)
|
||||
}) {
|
||||
Some(Ok(pixbuf)) => album_image.set_from_pixbuf(Some(&pixbuf)),
|
||||
Some(Err(err)) => {
|
||||
error!("{:?}", err);
|
||||
album_image.set_from_pixbuf(None)
|
||||
}
|
||||
None => album_image.set_from_pixbuf(None),
|
||||
};
|
||||
}
|
||||
|
||||
title_label
|
||||
.label
|
||||
.set_text(update.song.title().unwrap_or_default());
|
||||
album_label.label.set_text(curr_album);
|
||||
.set_text(&update.song.title.unwrap_or_default());
|
||||
album_label
|
||||
.label
|
||||
.set_text(&update.song.album.unwrap_or_default());
|
||||
artist_label
|
||||
.label
|
||||
.set_text(update.song.artists().first().unwrap_or(&String::new()));
|
||||
.set_text(&update.song.artist.unwrap_or_default());
|
||||
|
||||
match update.status.state {
|
||||
PlayState::Stopped => {
|
||||
PlayerState::Stopped => {
|
||||
btn_play_pause.set_sensitive(false);
|
||||
}
|
||||
PlayState::Playing => {
|
||||
PlayerState::Playing => {
|
||||
btn_play_pause.set_sensitive(true);
|
||||
btn_play_pause.set_label("");
|
||||
btn_play_pause.set_label(&self.icons.pause);
|
||||
|
||||
let style_context = btn_play_pause.style_context();
|
||||
style_context.add_class("playing");
|
||||
style_context.remove_class("paused");
|
||||
}
|
||||
PlayState::Paused => {
|
||||
PlayerState::Paused => {
|
||||
btn_play_pause.set_sensitive(true);
|
||||
btn_play_pause.set_label("");
|
||||
btn_play_pause.set_label(&self.icons.play);
|
||||
|
||||
let style_context = btn_play_pause.style_context();
|
||||
style_context.add_class("paused");
|
||||
style_context.remove_class("playing");
|
||||
}
|
||||
}
|
||||
|
||||
let enable_prev = match update.status.current_song {
|
||||
Some((pos, _)) => pos.0 > 0,
|
||||
None => false,
|
||||
};
|
||||
let enable_prev = update.status.playlist_position > 0;
|
||||
|
||||
let enable_next = match update.status.current_song {
|
||||
Some((pos, _)) => pos.0 < update.status.playlist_length,
|
||||
None => false,
|
||||
};
|
||||
let enable_next =
|
||||
update.status.playlist_position < update.status.playlist_length;
|
||||
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
volume_slider.set_value(update.status.volume as f64);
|
||||
volume_slider.set_value(update.status.volume_percent as f64);
|
||||
}
|
||||
|
||||
Continue(true)
|
||||
|
@ -418,11 +440,11 @@ impl Module<Button> for MpdModule {
|
|||
}
|
||||
|
||||
/// Replaces each of the formatting tokens in the formatting string
|
||||
/// with actual data pulled from MPD
|
||||
/// with actual data pulled from the music player
|
||||
fn replace_tokens(
|
||||
format_string: &str,
|
||||
tokens: &Vec<String>,
|
||||
song: &Song,
|
||||
song: &Track,
|
||||
status: &Status,
|
||||
icons: &Icons,
|
||||
) -> String {
|
||||
|
@ -436,30 +458,27 @@ fn replace_tokens(
|
|||
}
|
||||
|
||||
/// Converts a string format token value
|
||||
/// into its respective MPD value.
|
||||
fn get_token_value(song: &Song, status: &Status, icons: &Icons, token: &str) -> String {
|
||||
let s = match token {
|
||||
"icon" => {
|
||||
let icon = match status.state {
|
||||
PlayState::Stopped => None,
|
||||
PlayState::Playing => Some(&icons.play),
|
||||
PlayState::Paused => Some(&icons.pause),
|
||||
};
|
||||
icon.map(String::as_str)
|
||||
/// into its respective value.
|
||||
fn get_token_value(song: &Track, status: &Status, icons: &Icons, token: &str) -> String {
|
||||
match token {
|
||||
"icon" => match status.state {
|
||||
PlayerState::Stopped => None,
|
||||
PlayerState::Playing => Some(&icons.play),
|
||||
PlayerState::Paused => Some(&icons.pause),
|
||||
}
|
||||
"title" => song.title(),
|
||||
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
|
||||
"artist" => try_get_first_tag(song.tags.get(&Tag::Artist)),
|
||||
"date" => try_get_first_tag(song.tags.get(&Tag::Date)),
|
||||
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
|
||||
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
|
||||
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
|
||||
"duration" => return get_duration(status).map(format_time).unwrap_or_default(),
|
||||
|
||||
"elapsed" => return get_elapsed(status).map(format_time).unwrap_or_default(),
|
||||
_ => Some(token),
|
||||
};
|
||||
s.unwrap_or_default().to_string()
|
||||
.map(|s| s.to_string()),
|
||||
"title" => song.title.clone(),
|
||||
"album" => song.album.clone(),
|
||||
"artist" => song.artist.clone(),
|
||||
"date" => song.date.clone(),
|
||||
"disc" => song.disc.map(|x| x.to_string()),
|
||||
"genre" => song.genre.clone(),
|
||||
"track" => song.track.map(|x| x.to_string()),
|
||||
"duration" => status.duration.map(format_time),
|
||||
"elapsed" => status.elapsed.map(format_time),
|
||||
_ => Some(token.to_string()),
|
||||
}
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
Loading…
Add table
Add a link
Reference in a new issue