Main bulk of program built
This commit is contained in:
commit
f87356c859
|
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
.env
|
||||
Cargo.lock
|
||||
|
|
@ -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.*"
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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: ' ',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
pub mod config;
|
||||
pub mod data;
|
||||
|
|
@ -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:")));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue