From f87356c859502edfaadf85e9767c53aec7c472b9 Mon Sep 17 00:00:00 2001 From: djairoh Date: Fri, 5 May 2023 12:41:09 +0200 Subject: [PATCH] Main bulk of program built --- .gitignore | 3 ++ Cargo.toml | 17 +++++++ src/main.rs | 52 ++++++++++++++++++++++ src/print_text.rs | 50 +++++++++++++++++++++ src/structs/config.rs | 99 +++++++++++++++++++++++++++++++++++++++++ src/structs/data.rs | 17 +++++++ src/structs/mod.rs | 2 + src/update_message.rs | 100 ++++++++++++++++++++++++++++++++++++++++++ src/update_players.rs | 52 ++++++++++++++++++++++ 9 files changed, 392 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/print_text.rs create mode 100644 src/structs/config.rs create mode 100644 src/structs/data.rs create mode 100644 src/structs/mod.rs create mode 100644 src/update_message.rs create mode 100644 src/update_players.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0c260e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.env +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6bf5a8a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "polybar-now-playing-rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mpris = "2.0.*" +confy = "0.5.*" +serde = { version = "1.0.*", features = ["derive"] } +serde_json = "1.0.*" +string-builder = "0.2.*" +env_logger = "0.4.*" +dotenvy = "0.15.7" +log = "0.4" +signal-hook = "0.3.*" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e96aecf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::{self}; +use mpris::PlayerFinder; +use structs::{config::Config, data::Data}; +use crate::update_players::update_players; +use crate::update_message::update_message; +use crate::print_text::print_text; + +mod update_players; +mod update_message; +mod print_text; +mod structs; + +fn handle_signal(data: &Data, pf: &PlayerFinder) { + if data.current_player.is_some() { + if let Ok(p) = pf.find_by_name(data.current_player.as_ref().unwrap()) { + let _ = p.checked_play_pause(); + } + } +} + +fn main() { + dotenvy::dotenv().expect("Failed to read .env file"); + std::env::set_var("RUST_LOG", "debug"); + if let Err(e) = env_logger::init() { + panic!("{}", e); + } + + let mut cfg: Config = confy::load("polybar-now-playing", None).unwrap(); //TODO: error handling + cfg.priorities_to_lower(); + let mut data: Data = Data::default(); + let term = Arc::new(AtomicBool::new(false)); + + let pf: PlayerFinder = PlayerFinder::new() + .expect("Failed to connect to Dbus!"); + + if let Err(e) = signal_hook::flag::register(signal_hook::consts::SIGUSR1, Arc::clone(&term)) { + panic!("{}", e); + } + + loop { + thread::sleep(cfg.update_delay); + update_players(&pf, &cfg, &mut data); + update_message(&pf, &cfg, &mut data); + print_text(&cfg, &mut data); + if term.load(Ordering::Relaxed) { + handle_signal(&data, &pf); + term.swap(false, Ordering::Relaxed); + }; + } + } diff --git a/src/print_text.rs b/src/print_text.rs new file mode 100644 index 0000000..98eff50 --- /dev/null +++ b/src/print_text.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use log::info; +use string_builder::Builder; + +use crate::structs::{config::{Field, Config}, data::Data}; + +fn cutoff(fields: &Vec, brk: char, strings: &mut HashMap) { + for field in fields { + if !field.field.eq("xesam:userRating") && !field.field.eq("xesam:autoRating") { + let a = strings.get(&field.field); + if a.is_some() && a.unwrap().len() >= field.num_chars as usize { + let mut b = a.unwrap().clone(); + b.truncate(field.num_chars as usize); + b.push(brk); + println!("{}", b); + strings.insert(field.field.clone(), b); + } + } + } +} + +fn build_string(cfg: &Config, data: &mut Data) -> String { + let mut b = Builder::default(); + + if cfg.hide_output && data.current_player.is_none() { + b.append(' '); + } else { + cutoff(&cfg.metadata_fields, cfg.break_character, &mut data.display_text); + b.append(data.display_prefix.clone()); + b.append(' '); + b.append(format!(" %{{T{}}}", cfg.font_index)); + let mut idx = 0; let len = cfg.metadata_fields.len() as i32; + for string in &cfg.metadata_fields { + if let Some(string) = data.display_text.get(&string.field) { + idx += 1; + b.append(string.clone()); + if idx < len {b.append(format!(" {} ", cfg.metadata_separator))}; + } else { + info!("failed to get {} value!", string.field); + } + } + b.append("%{T-}"); + } + b.string().unwrap_or("Failed to unwrap stringBuilder!".to_owned()) +} + +pub fn print_text(cfg: &Config, data: &mut Data) { + println!("{}", build_string(cfg, data)); + +} \ No newline at end of file diff --git a/src/structs/config.rs b/src/structs/config.rs new file mode 100644 index 0000000..85594f7 --- /dev/null +++ b/src/structs/config.rs @@ -0,0 +1,99 @@ +use core::time; +use std::{collections::HashMap, time::Duration}; +use serde::{Serialize, Deserialize}; + + +#[derive(Serialize, Deserialize)] +pub struct Field { + pub field: String, + pub num_chars: u8 +} + +impl Field { + fn new(metadata_field: String, num_chars: u8) -> Self { + Field { + field: metadata_field, + num_chars + } + } + pub fn constructor(metadata_field: &str, num_chars: u8) -> Self { + Self::new(metadata_field.to_owned(), num_chars) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Rating { + pub nil: char, + pub half: char, + pub full: char +} + +impl Rating { + pub fn repeat(c: char, n: usize) -> String { + let mut s = c.to_string(); + s.push(' '); + s.repeat(n) + } +} + +impl Default for Rating { + fn default() -> Self { + Self { + nil: '-', + half: '/', + full: '+' + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub font_index: u8, + pub update_delay: Duration, + pub metadata_separator: char, + pub array_separator: char, + pub hide_output: bool, + pub metadata_fields: Vec, + pub player_prefixes: HashMap, + pub rating_icons: Rating, + pub player_priorities: Vec, + pub break_character: char, +} + +impl Default for Config { + fn default() -> Self { + Config { + font_index: 1, + update_delay: time::Duration::from_millis(300), + metadata_separator: '|', + array_separator: '+', + hide_output: true, + metadata_fields: vec![Field::constructor("xesam:title", 40), Field::constructor("xesam:artist", 20)], + player_prefixes: default_player_prefixes(), + rating_icons: Rating::default(), + player_priorities: vec![ms("clementine"), ms("spotify"), ms("deadbeef"), ms("mpv"), ms("vlc"), ms("firefox"), ms("chromium")], + break_character: '-', + } + } +} + +impl Config { + pub fn priorities_to_lower(&mut self) { + self.player_priorities = self.player_priorities.iter().map(|i| i.to_lowercase()).collect(); + } +} + +fn ms(str: &str) -> String { + str.to_string() +} + +fn default_player_prefixes() -> HashMap { + let mut out: HashMap = HashMap::new(); + + out.insert("clementine".to_owned(), 'c'); + out.insert("firefox".to_owned(), 'f'); + out.insert("spotify".to_owned(), 's'); + out.insert("default".to_owned(), '>'); + + out +} \ No newline at end of file diff --git a/src/structs/data.rs b/src/structs/data.rs new file mode 100644 index 0000000..a41b5f4 --- /dev/null +++ b/src/structs/data.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; + +pub struct Data { + pub current_player: Option, + pub display_text: HashMap, + pub display_prefix: char, +} + +impl Default for Data { + fn default() -> Self { + Self { + current_player: None, + display_text: HashMap::new(), + display_prefix: ' ', + } + } +} \ No newline at end of file diff --git a/src/structs/mod.rs b/src/structs/mod.rs new file mode 100644 index 0000000..8321105 --- /dev/null +++ b/src/structs/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod data; \ No newline at end of file diff --git a/src/update_message.rs b/src/update_message.rs new file mode 100644 index 0000000..0f5167f --- /dev/null +++ b/src/update_message.rs @@ -0,0 +1,100 @@ +use log::{debug, trace}; +use mpris::{PlayerFinder, MetadataValue}; + +use crate::structs::{config::{Rating, Config}, data::Data}; + + +fn update_prefix(cfg: &Config, data: &mut Data) { + if data.current_player.is_some() { + let c = cfg.player_prefixes.get(&data.current_player.as_ref().unwrap().to_ascii_lowercase()); + if let Some(char) = c { + data.display_prefix = char.clone(); + trace!("updated prefix to {}", data.display_prefix); + } else { + data.display_prefix = cfg.player_prefixes.get("default").unwrap().clone(); //todo: error handling + trace!("set prefix to default ({})", data.display_prefix); + } + } +} + +fn value_to_string(v: &MetadataValue, sep: char) -> String { + match v { + MetadataValue::String(v) => v.to_string(), + MetadataValue::I16(v) => v.to_string(), + MetadataValue::I32(v) => v.to_string(), + MetadataValue::I64(v) => v.to_string(), + MetadataValue::U8(v) => v.to_string(), + MetadataValue::U16(v) => v.to_string(), + MetadataValue::U32(v) => v.to_string(), + MetadataValue::U64(v) => v.to_string(), + MetadataValue::F64(v) => v.to_string(), + MetadataValue::Bool(v) => v.to_string(), + MetadataValue::Array(v) => { + let mut out = v.iter().map( |val| { + let mut str = value_to_string(val, sep); + str.push(sep); + str + }).collect::(); + out.pop(); + out + }, + MetadataValue::Map(_v) => panic!("unimplemented! TBH i have no clue when a metadataValue would even return this?"), + MetadataValue::Unsupported => panic!("Unsupported Metadata type detected!"), + } +} + +fn rating_to_string(r: Option<&MetadataValue>, map: &Rating) -> String { + match r { + Some(rating) => { + let f = (rating.as_f64().unwrap() * 10_f64).round() as i64; + match f { //todo: refactor + 0 => Rating::repeat(map.nil, 5), + 1 => format!("{}{}", Rating::repeat(map.half, 1), Rating::repeat(map.nil, 4)), + 2 => format!("{}{}", Rating::repeat(map.full, 1), Rating::repeat(map.nil, 4)), + 3 => format!("{}{}{}", Rating::repeat(map.full, 1), Rating::repeat(map.half, 1), Rating::repeat(map.nil, 3)), + 4 => format!("{}{}", Rating::repeat(map.full, 2), Rating::repeat(map.nil, 3)), + 5 => format!("{}{}{}", Rating::repeat(map.full, 2), Rating::repeat(map.half, 1), Rating::repeat(map.nil, 2)), + 6 => format!("{}{}", Rating::repeat(map.full, 3), Rating::repeat(map.nil, 2)), + 7 => format!("{}{}{}", Rating::repeat(map.full, 3), Rating::repeat(map.half, 1), Rating::repeat(map.nil, 1)), + 8 => format!("{}{}", Rating::repeat(map.full, 4), Rating::repeat(map.nil, 1)), + 9 => format!("{}{}", Rating::repeat(map.full, 4), Rating::repeat(map.half, 1)), + 10.. => Rating::repeat(map.full, 5), + _ => format!("Invalid rating!") + } + }, + None => { + Rating::repeat(map.nil, 5) + }, + } +} + +pub fn update_message(pf: &PlayerFinder, cfg: &Config, data: &mut Data) { + if data.current_player.is_some() { + update_prefix(cfg, data); + let name = &data.current_player.as_ref().unwrap(); + if let Ok(player) = pf.find_by_name(name) { + debug!("found player!"); + if let Ok(m) = player.get_metadata() { + debug!("got metadata!"); + for field in &cfg.metadata_fields { + if field.field.eq("xesam:userRating") || field.field.eq("xesam:autoRating") { + let key = field.field.clone(); + data.display_text.insert(field.field.clone(), rating_to_string(m.get(&key), &cfg.rating_icons)); + } else { + let key = field.field.clone(); + match m.get(&key) { + Some(value) => { + debug!("inserting {}: '{}'", key, value_to_string(value, cfg.array_separator)); + data.display_text.insert(key, value_to_string(value, cfg.array_separator)); + }, + None => { + debug!("field {} is empty!", key); + data.display_text.insert(key, format!("No {}", field.field.clone().trim_start_matches("xesam:"))); + }, + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/update_players.rs b/src/update_players.rs new file mode 100644 index 0000000..faa4b9a --- /dev/null +++ b/src/update_players.rs @@ -0,0 +1,52 @@ +use mpris::PlayerFinder; +use crate::structs::{data::Data, config::Config}; + +pub fn update_players( + pf: &PlayerFinder, + cfg: &Config, + mut data: &mut Data, +) { + let players = pf.find_all().unwrap(); //todo: error handling + if players.is_empty() { + data.current_player = None; + } else { + let mut active: Vec> = vec![Vec::new(), Vec::new(), Vec::new()]; + for player in players { + if cfg.player_priorities.contains(&player.identity().to_owned().to_ascii_lowercase()) { + let name = player.identity(); + let idx = cfg.player_priorities.iter().position(|x| x.to_ascii_lowercase().eq(&name.to_ascii_lowercase())).unwrap() as i32; //todo: move to function in config; error handling + let status = player.get_playback_status().unwrap(); //todo: error handling + match status { + mpris::PlaybackStatus::Playing => active[0].push((idx, name.to_owned())), + mpris::PlaybackStatus::Paused => active[1].push((idx, name.to_owned())), + mpris::PlaybackStatus::Stopped => active[2].push((idx, name.to_owned())), + }; + } + } + if !active[0].is_empty() { + data.current_player = Some(get_lowest(&active[0])); + } else if !active[1].is_empty() { + data.current_player = Some(get_lowest(&active[1])); + } else if !active[2].is_empty() { + data.current_player = Some(get_lowest(&active[2])); + } else { + if let Ok(player) = pf.find_active() { + data.current_player = Some(player.identity().to_owned()); + } else { + data.current_player = None; + } + } + } +} + +fn get_lowest(v: &Vec<(i32, String)>) -> String { + let mut out = String::new(); + let mut lowest_index = i32::MAX; + for (v_id, v_str) in v.iter() { + if v_id < &lowest_index { + out = v_str.to_owned(); + lowest_index = *v_id; + } + } + out +} \ No newline at end of file