mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-17 23:01:04 +02:00
refactor: major client code changes
This does away with `lazy_static` singletons for all the clients, instead putting them all inside a `Clients` struct on the `Ironbar` struct. Client code has been refactored in places to accommodate this, and module code has been updated to get the clients the new way. The Wayland client has been re-written from the ground up to remove a lot of the needless complications, provide a nicer interface and reduce some duplicate data. The MPD music client has been overhauled to use the `mpd_utils` crate, which simplifies the code within Ironbar and should offer more robustness and better recovery when connection is lost to the server. The launcher module in particular has been affected by the refactor.
This commit is contained in:
parent
57b57ed002
commit
c702f6fffa
33 changed files with 1081 additions and 1063 deletions
|
@ -1,4 +1,5 @@
|
|||
use color_eyre::Result;
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
@ -19,8 +20,6 @@ pub enum PlayerUpdate {
|
|||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -56,7 +55,7 @@ pub struct ProgressTick {
|
|||
pub elapsed: Option<Duration>,
|
||||
}
|
||||
|
||||
pub trait MusicClient {
|
||||
pub trait MusicClient: Debug + Send + Sync {
|
||||
fn play(&self) -> Result<()>;
|
||||
fn pause(&self) -> Result<()>;
|
||||
fn next(&self) -> Result<()>;
|
||||
|
@ -68,18 +67,15 @@ pub trait MusicClient {
|
|||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||
}
|
||||
|
||||
pub enum ClientType<'a> {
|
||||
Mpd { host: &'a str, music_dir: PathBuf },
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ClientType {
|
||||
Mpd { host: String, music_dir: PathBuf },
|
||||
Mpris,
|
||||
}
|
||||
|
||||
pub async fn get_client(client_type: ClientType<'_>) -> Box<Arc<dyn MusicClient>> {
|
||||
pub fn create_client(client_type: ClientType) -> 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()),
|
||||
ClientType::Mpd { host, music_dir } => Arc::new(mpd::Client::new(host, music_dir)),
|
||||
ClientType::Mpris => Arc::new(mpris::Client::new()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,91 +1,72 @@
|
|||
use super::{
|
||||
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
|
||||
};
|
||||
use crate::{await_sync, send, spawn};
|
||||
use crate::{await_sync, send, spawn, Ironbar};
|
||||
use color_eyre::Report;
|
||||
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::client::{ConnectionEvent, Subsystem};
|
||||
use mpd_client::commands::{self, SeekMode};
|
||||
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 mpd_utils::{mpd_client, PersistentClient};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::{TcpStream, UnixStream};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::debug;
|
||||
|
||||
lazy_static! {
|
||||
static ref CONNECTIONS: Arc<Mutex<HashMap<String, Arc<MpdClient>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
macro_rules! command {
|
||||
($self:ident, $command:expr) => {
|
||||
await_sync(async move { $self.client.command($command).await.map_err(Report::new) })
|
||||
};
|
||||
}
|
||||
|
||||
pub struct MpdClient {
|
||||
client: Client,
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
client: Arc<PersistentClient>,
|
||||
music_dir: PathBuf,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MpdConnectionError {
|
||||
MaxRetries,
|
||||
ProtocolError(MpdProtocolError),
|
||||
}
|
||||
impl Client {
|
||||
pub fn new(host: String, music_dir: PathBuf) -> Self {
|
||||
let client = Arc::new(PersistentClient::new(host, Duration::from_secs(5)));
|
||||
let mut client_rx = client.subscribe();
|
||||
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
|
||||
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) = broadcast::channel(16);
|
||||
let _guard = Ironbar::runtime().enter();
|
||||
client.init();
|
||||
|
||||
{
|
||||
let music_dir = music_dir.clone();
|
||||
let tx = tx.clone();
|
||||
let client = client.clone();
|
||||
let music_dir = music_dir.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(change) = state_changes.next().await {
|
||||
debug!("Received state change: {:?}", change);
|
||||
Self::send_update(&client, &tx, &music_dir)
|
||||
.await
|
||||
.expect("Failed to send update");
|
||||
|
||||
while let Ok(change) = client_rx.recv().await {
|
||||
debug!("Received state change: {change:?}");
|
||||
if let ConnectionEvent::SubsystemChange(
|
||||
Subsystem::Player | Subsystem::Queue | Subsystem::Mixer,
|
||||
) = change
|
||||
) = *change
|
||||
{
|
||||
Self::send_update(&client, &tx, &music_dir)
|
||||
.await
|
||||
.expect("Failed to send update");
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = client.clone();
|
||||
let tx = tx.clone();
|
||||
let client = client.clone();
|
||||
|
||||
spawn(async move {
|
||||
loop {
|
||||
|
@ -95,16 +76,16 @@ impl MpdClient {
|
|||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
client,
|
||||
music_dir,
|
||||
tx,
|
||||
music_dir,
|
||||
_rx: rx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_update(
|
||||
client: &Client,
|
||||
client: &PersistentClient,
|
||||
tx: &broadcast::Sender<PlayerUpdate>,
|
||||
music_dir: &Path,
|
||||
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
|
@ -112,7 +93,7 @@ impl MpdClient {
|
|||
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 track = current_song.map(|s| convert_song(&s.song, music_dir));
|
||||
let status = Status::from(status);
|
||||
|
||||
let update = PlayerUpdate::Update(Box::new(track), status);
|
||||
|
@ -122,7 +103,7 @@ impl MpdClient {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
|
||||
async fn send_tick_update(client: &PersistentClient, tx: &broadcast::Sender<PlayerUpdate>) {
|
||||
let status = client.command(commands::Status).await;
|
||||
|
||||
if let Ok(status) = status {
|
||||
|
@ -136,183 +117,70 @@ impl MpdClient {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
!self.client.is_connection_closed()
|
||||
}
|
||||
|
||||
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||
info!("Connection to MPD server lost");
|
||||
self.tx.send(PlayerUpdate::Disconnect)?;
|
||||
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"),
|
||||
)
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.ok();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
impl MusicClient for Client {
|
||||
fn play(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::SetPause(false));
|
||||
Ok(())
|
||||
command!(self, commands::SetPause(false))
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::SetPause(true));
|
||||
Ok(())
|
||||
command!(self, commands::SetPause(true))
|
||||
}
|
||||
|
||||
fn next(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::Next);
|
||||
Ok(())
|
||||
command!(self, commands::Next)
|
||||
}
|
||||
|
||||
fn prev(&self) -> Result<()> {
|
||||
async_command!(self.client, commands::Previous);
|
||||
Ok(())
|
||||
command!(self, commands::Previous)
|
||||
}
|
||||
|
||||
fn set_volume_percent(&self, vol: u8) -> Result<()> {
|
||||
async_command!(self.client, commands::SetVolume(vol));
|
||||
Ok(())
|
||||
command!(self, commands::SetVolume(vol))
|
||||
}
|
||||
|
||||
fn seek(&self, duration: Duration) -> Result<()> {
|
||||
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
|
||||
Ok(())
|
||||
command!(self, commands::Seek(SeekMode::Absolute(duration)))
|
||||
}
|
||||
|
||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||
let rx = self.tx.subscribe();
|
||||
await_sync(async {
|
||||
await_sync(async move {
|
||||
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
||||
.await
|
||||
.expect("Failed to send player update");
|
||||
.expect("to be able to send 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) => {
|
||||
if client.is_connected() {
|
||||
Ok(Arc::clone(client))
|
||||
} else {
|
||||
client
|
||||
.send_disconnect_update()
|
||||
.expect("Failed to send disconnect update");
|
||||
fn convert_song(song: &Song, music_dir: &Path) -> Track {
|
||||
let (track, disc) = song.number();
|
||||
|
||||
let client = MpdClient::new(host, music_dir).await?;
|
||||
let client = Arc::new(client);
|
||||
connections.insert(host.to_string(), Arc::clone(&client));
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
let cover_path = music_dir
|
||||
.join(
|
||||
song.file_path()
|
||||
.parent()
|
||||
.expect("Song path should not be root")
|
||||
.join("cover.jpg"),
|
||||
)
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.ok();
|
||||
|
||||
Track {
|
||||
title: song.title().map(ToString::to_string),
|
||||
album: song.album().map(ToString::to_string),
|
||||
artist: Some(song.artists().join(", ")),
|
||||
date: try_get_first_tag(song, &Tag::Date).map(ToString::to_string),
|
||||
genre: try_get_first_tag(song, &Tag::Genre).map(ToString::to_string),
|
||||
disc: Some(disc),
|
||||
track: Some(track),
|
||||
cover_path,
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
|
|
|
@ -2,20 +2,16 @@ use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL
|
|||
use crate::clients::music::ProgressTick;
|
||||
use crate::{arc_mut, lock, send, spawn_blocking};
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||
use std::cmp;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{cmp, string};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
lazy_static! {
|
||||
static ref CLIENT: Arc<Client> = Arc::new(Client::new());
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
current_player: Arc<Mutex<Option<String>>>,
|
||||
tx: broadcast::Sender<PlayerUpdate>,
|
||||
|
@ -23,7 +19,7 @@ pub struct Client {
|
|||
}
|
||||
|
||||
impl Client {
|
||||
fn new() -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
|
||||
let current_player = arc_mut!(None);
|
||||
|
@ -289,10 +285,6 @@ impl MusicClient for Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_client() -> Arc<Client> {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
impl From<Metadata> for Track {
|
||||
fn from(value: Metadata) -> Self {
|
||||
const KEY_DATE: &str = "xesam:contentCreated";
|
||||
|
@ -301,11 +293,11 @@ impl From<Metadata> for Track {
|
|||
Self {
|
||||
title: value
|
||||
.title()
|
||||
.map(std::string::ToString::to_string)
|
||||
.map(ToString::to_string)
|
||||
.and_then(replace_empty_none),
|
||||
album: value
|
||||
.album_name()
|
||||
.map(std::string::ToString::to_string)
|
||||
.map(ToString::to_string)
|
||||
.and_then(replace_empty_none),
|
||||
artist: value
|
||||
.artists()
|
||||
|
@ -314,14 +306,14 @@ impl From<Metadata> for Track {
|
|||
date: value
|
||||
.get(KEY_DATE)
|
||||
.and_then(mpris::MetadataValue::as_string)
|
||||
.map(std::string::ToString::to_string),
|
||||
.map(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(string::ToString::to_string),
|
||||
cover_path: value.art_url().map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue