1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 18:51:04 +02:00
ironbar/src/modules/music.rs

519 lines
16 KiB
Rust
Raw Normal View History

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;
use crate::try_send;
use color_eyre::Result;
use dirs::{audio_dir, home_dir};
2022-08-14 14:30:13 +01:00
use glib::Continue;
use gtk::gdk_pixbuf::Pixbuf;
2022-08-14 14:30:13 +01:00
use gtk::prelude::*;
2022-10-16 16:00:18 +01:00
use gtk::{Button, Image, Label, Orientation, Scale};
2022-08-14 14:30:13 +01:00
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
2022-08-14 14:30:13 +01:00
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
2022-08-14 14:30:13 +01:00
#[derive(Debug)]
pub enum PlayerCommand {
Previous,
Play,
Pause,
Next,
2022-10-16 16:00:18 +01:00
Volume(u8),
}
#[derive(Debug, Deserialize, Clone)]
pub struct Icons {
/// Icon to display when playing.
#[serde(default = "default_icon_play")]
2022-10-16 16:00:18 +01:00
play: String,
/// Icon to display when paused.
#[serde(default = "default_icon_pause")]
2022-10-16 16:00:18 +01:00
pause: String,
/// Icon to display under volume slider
#[serde(default = "default_icon_volume")]
volume: String,
}
impl Default for Icons {
fn default() -> Self {
Self {
pause: default_icon_pause(),
play: default_icon_play(),
2022-10-16 16:00:18 +01:00
volume: default_icon_volume(),
}
}
}
#[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
}
}
2022-08-14 14:30:13 +01:00
#[derive(Debug, Deserialize, Clone)]
pub struct MusicModule {
/// Type of player to connect to
#[serde(default)]
player_type: PlayerType,
/// Format of current song info to display on the bar.
2022-08-14 14:30:13 +01:00
#[serde(default = "default_format")]
format: String,
/// Player state icons
#[serde(default)]
icons: Icons,
2022-08-14 14:30:13 +01:00
// -- MPD --
/// TCP or Unix socket address.
#[serde(default = "default_socket")]
host: String,
/// Path to root of music directory.
2022-08-14 14:30:13 +01:00
#[serde(default = "default_music_dir")]
music_dir: PathBuf,
#[serde(flatten)]
pub common: Option<CommonConfig>,
2022-08-14 14:30:13 +01:00
}
fn default_socket() -> String {
String::from("localhost:6600")
}
fn default_format() -> String {
String::from("{icon} {title} / {artist}")
}
2022-10-16 16:00:18 +01:00
fn default_icon_play() -> String {
String::from("")
2022-08-14 14:30:13 +01:00
}
2022-10-16 16:00:18 +01:00
fn default_icon_pause() -> String {
String::from("")
}
fn default_icon_volume() -> String {
String::from("")
2022-08-14 14:30:13 +01:00
}
fn default_music_dir() -> PathBuf {
audio_dir().unwrap_or_else(|| home_dir().map(|dir| dir.join("Music")).unwrap_or_default())
2022-08-14 14:30:13 +01:00
}
/// Formats a duration given in seconds
/// in hh:mm format
fn format_time(duration: Duration) -> String {
let time = duration.as_secs();
2022-08-14 14:30:13 +01:00
let minutes = (time / 60) % 60;
let seconds = time % 60;
format!("{:0>2}:{:0>2}", minutes, seconds)
}
/// Extracts the formatting tokens from a formatting string
fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
re.captures_iter(format_string)
.map(|caps| caps[1].to_string())
.collect::<Vec<_>>()
}
#[derive(Clone, Debug)]
pub struct SongUpdate {
song: Track,
status: Status,
display_string: String,
2022-08-14 14:30:13 +01:00
}
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;
2022-08-14 14:30:13 +01:00
fn name() -> &'static str {
"music"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let format = self.format.clone();
let icons = self.icons.clone();
2022-08-14 14:30:13 +01:00
let re = Regex::new(r"\{([\w-]+)}")?;
let tokens = get_tokens(&re, self.format.as_str());
2022-08-14 14:30:13 +01:00
// receive player updates
{
let player_type = self.player_type;
let host = self.host.clone();
let music_dir = self.music_dir.clone();
spawn(async move {
loop {
let mut rx = {
let client = get_client(player_type, &host, music_dir.clone()).await;
client.subscribe_change()
};
while let Ok(update) = rx.recv().await {
match update {
PlayerUpdate::Update(track, status) => match *track {
Some(track) => {
let display_string = replace_tokens(
format.as_str(),
&tokens,
&track,
&status,
&icons,
);
let update = SongUpdate {
song: track,
status,
display_string,
};
tx.send(ModuleUpdateEvent::Update(Some(update)))
.await
.expect(ERR_CHANNEL_SEND);
}
None => tx
.send(ModuleUpdateEvent::Update(None))
.await
.expect(ERR_CHANNEL_SEND),
},
PlayerUpdate::Disconnect => break,
}
}
}
});
}
2022-08-14 14:30:13 +01:00
// 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);
}
2022-08-14 20:40:11 +01:00
}
});
}
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
let button = Button::new();
let label = Label::new(None);
label.set_angle(info.bar_position.get_angle());
button.add(&label);
let orientation = info.bar_position.get_orientation();
2022-10-16 16:00:18 +01:00
{
let tx = context.tx.clone();
button.connect_clicked(move |button| {
try_send!(
tx,
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation,))
);
2022-10-16 16:00:18 +01:00
});
}
2022-08-14 14:30:13 +01:00
{
let button = button.clone();
2022-10-16 16:00:18 +01:00
let tx = context.tx.clone();
2022-08-14 14:30:13 +01:00
context.widget_rx.attach(None, move |mut event| {
if let Some(event) = event.take() {
label.set_label(&event.display_string);
button.show();
} else {
button.hide();
try_send!(tx, ModuleUpdateEvent::ClosePopup);
}
Continue(true)
});
};
let popup = self.into_popup(context.controller_tx, context.popup_rx);
Ok(ModuleWidget {
widget: button,
popup,
})
}
fn into_popup(
self,
tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
) -> Option<gtk::Box> {
let container = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.spacing(10)
.name("popup-music")
.build();
let album_image = Image::builder()
.width_request(128)
.height_request(128)
.name("album-art")
.build();
let info_box = gtk::Box::new(Orientation::Vertical, 10);
let title_label = IconLabel::new("\u{f886}", None);
let album_label = IconLabel::new("\u{f524}", None);
let artist_label = IconLabel::new("\u{fd01}", None);
title_label.container.set_widget_name("title");
album_label.container.set_widget_name("album");
artist_label.container.set_widget_name("artist");
info_box.add(&title_label.container);
info_box.add(&album_label.container);
info_box.add(&artist_label.container);
let controls_box = gtk::Box::builder().name("controls").build();
let btn_prev = Button::builder().label("\u{f9ad}").name("btn-prev").build();
let btn_play_pause = Button::builder().label("").name("btn-play-pause").build();
let btn_next = Button::builder().label("\u{f9ac}").name("btn-next").build();
controls_box.add(&btn_prev);
controls_box.add(&btn_play_pause);
controls_box.add(&btn_next);
info_box.add(&controls_box);
2022-10-16 16:00:18 +01:00
let volume_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.spacing(5)
.name("volume")
.build();
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true);
volume_slider.set_widget_name("slider");
2022-10-16 16:00:18 +01:00
let volume_icon = Label::new(Some(&self.icons.volume));
volume_icon.style_context().add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0);
volume_box.pack_end(&volume_icon, false, false, 0);
container.add(&album_image);
container.add(&info_box);
2022-10-16 16:00:18 +01:00
container.add(&volume_box);
let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| {
try_send!(tx_prev, PlayerCommand::Previous);
});
2022-08-14 14:30:13 +01:00
let tx_toggle = tx.clone();
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);
}
});
2022-10-16 16:00:18 +01:00
let tx_next = tx.clone();
btn_next.connect_clicked(move |_| {
try_send!(tx_next, PlayerCommand::Next);
});
2022-10-16 16:00:18 +01:00
let tx_vol = tx;
volume_slider.connect_change_value(move |_, _, val| {
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
Inhibit(false)
2022-10-16 16:00:18 +01:00
});
container.show_all();
{
let mut prev_cover = None;
rx.attach(None, move |update| {
if let Some(update) = update {
// only update art when album changes
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(&update.song.album.unwrap_or_default());
artist_label
.label
.set_text(&update.song.artist.unwrap_or_default());
match update.status.state {
PlayerState::Stopped => {
btn_play_pause.set_sensitive(false);
}
PlayerState::Playing => {
btn_play_pause.set_sensitive(true);
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");
}
PlayerState::Paused => {
btn_play_pause.set_sensitive(true);
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");
2022-08-14 14:30:13 +01:00
}
}
let enable_prev = update.status.playlist_position > 0;
let enable_next =
update.status.playlist_position < update.status.playlist_length;
btn_prev.set_sensitive(enable_prev);
btn_next.set_sensitive(enable_next);
2022-10-16 16:00:18 +01:00
volume_slider.set_value(update.status.volume_percent as f64);
2022-08-14 14:30:13 +01:00
}
Continue(true)
});
}
2022-08-14 14:30:13 +01:00
Some(container)
2022-08-14 14:30:13 +01:00
}
}
/// Replaces each of the formatting tokens in the formatting string
/// with actual data pulled from the music player
fn replace_tokens(
format_string: &str,
tokens: &Vec<String>,
song: &Track,
status: &Status,
icons: &Icons,
) -> String {
let mut compiled_string = format_string.to_string();
for token in tokens {
let value = get_token_value(song, status, icons, token);
compiled_string =
compiled_string.replace(format!("{{{}}}", token).as_str(), value.as_str());
2022-08-14 14:30:13 +01:00
}
compiled_string
}
2022-08-14 14:30:13 +01:00
/// Converts a string format token value
/// 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),
}
.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)]
struct IconLabel {
label: Label,
container: gtk::Box,
}
impl IconLabel {
fn new(icon: &str, label: Option<&str>) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = Label::new(Some(icon));
let label = Label::new(label);
icon.style_context().add_class("icon");
label.style_context().add_class("label");
container.add(&icon);
container.add(&label);
Self { label, container }
2022-08-14 14:30:13 +01:00
}
2022-08-24 21:27:30 +01:00
}