Main bulk of program built

This commit is contained in:
Djairo Hougee 2023-05-05 12:41:09 +02:00
commit f87356c859
9 changed files with 392 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
.env
Cargo.lock

17
Cargo.toml Normal file
View File

@ -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.*"

52
src/main.rs Normal file
View File

@ -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);
};
}
}

50
src/print_text.rs Normal file
View File

@ -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<Field>, brk: char, strings: &mut HashMap<String, String>) {
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));
}

99
src/structs/config.rs Normal file
View File

@ -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<Field>,
pub player_prefixes: HashMap<String, char>,
pub rating_icons: Rating,
pub player_priorities: Vec<String>,
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<String, char> {
let mut out: HashMap<String, char> = 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
}

17
src/structs/data.rs Normal file
View File

@ -0,0 +1,17 @@
use std::collections::HashMap;
pub struct Data {
pub current_player: Option<String>,
pub display_text: HashMap<String, String>,
pub display_prefix: char,
}
impl Default for Data {
fn default() -> Self {
Self {
current_player: None,
display_text: HashMap::new(),
display_prefix: ' ',
}
}
}

2
src/structs/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod config;
pub mod data;

100
src/update_message.rs Normal file
View File

@ -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::<String>();
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:")));
},
}
}
}
}
}
}
}

52
src/update_players.rs Normal file
View File

@ -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<(i32, String)>> = 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
}