fix: workaround for a bug where playing players are somtimes incorrectly reported as paused

This commit is contained in:
Djairo Hougee 2024-04-22 17:18:48 +02:00
parent c074b4279e
commit d34e842f3e
2 changed files with 136 additions and 97 deletions

View File

@ -1,4 +1,4 @@
//! This file deals with updating the actual message, including proper formatting. //! This file deals with updating the actual message, including proper formatting.
use log::{debug, trace}; use log::{debug, trace};
use mpris::MetadataValue; use mpris::MetadataValue;
@ -9,96 +9,126 @@ use crate::structs::{config::Config, data::Data};
/// Both the Map and Unsupported types currently lead the program to panic! /// Both the Map and Unsupported types currently lead the program to panic!
/// The HashMap because I honestly don't know when a metadata value would be encoded as such (and am too lazy to dig through the crate's source code), /// The HashMap because I honestly don't know when a metadata value would be encoded as such (and am too lazy to dig through the crate's source code),
/// The Unsupported type should be self-explanatory. /// The Unsupported type should be self-explanatory.
/// ///
/// Input: /// Input:
/// v: MetadataValue to convert. /// v: MetadataValue to convert.
/// sep: seperation character to insert between entries of a Vec. /// sep: seperation character to insert between entries of a Vec.
/// ///
/// Output: /// Output:
/// String representing the input MetadataValue. /// String representing the input MetadataValue.
fn value_to_string(v: &MetadataValue, sep: char) -> String { fn value_to_string(v: &MetadataValue, sep: char) -> String {
match v { match v {
MetadataValue::String(v) => v.to_string(), MetadataValue::String(v) => v.to_string(),
MetadataValue::I16(v) => v.to_string(), MetadataValue::I16(v) => v.to_string(),
MetadataValue::I32(v) => v.to_string(), MetadataValue::I32(v) => v.to_string(),
MetadataValue::I64(v) => v.to_string(), MetadataValue::I64(v) => v.to_string(),
MetadataValue::U8(v) => v.to_string(), MetadataValue::U8(v) => v.to_string(),
MetadataValue::U16(v) => v.to_string(), MetadataValue::U16(v) => v.to_string(),
MetadataValue::U32(v) => v.to_string(), MetadataValue::U32(v) => v.to_string(),
MetadataValue::U64(v) => v.to_string(), MetadataValue::U64(v) => v.to_string(),
MetadataValue::F64(v) => v.to_string(), MetadataValue::F64(v) => v.to_string(),
MetadataValue::Bool(v) => v.to_string(), MetadataValue::Bool(v) => v.to_string(),
MetadataValue::Array(v) => { MetadataValue::Array(v) => {
let mut out = v.iter().map( |val| { let mut out = v
let mut str = value_to_string(val, sep); .iter()
str.push(sep); .map(|val| {
str let mut str = value_to_string(val, sep);
}).collect::<String>(); str.push(sep);
out.pop(); str
out })
}, .collect::<String>();
MetadataValue::Map(_v) => panic!("unimplemented! TBH i have no clue when a metadataValue would even return this?"), out.pop();
MetadataValue::Unsupported => panic!("Unsupported Metadata type detected!"), 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!"),
}
} }
/// This function converts one specific instance of MetadataValue to an appropriate String. /// This function converts one specific instance of MetadataValue to an appropriate String.
/// It deals with the xesam:userRating type. This is a float (0.0 <= v <= 1.0), but should be represented on a scale fron 0 to 10 (according to me). /// It deals with the xesam:userRating type. This is a float (0.0 <= v <= 1.0), but should be represented on a scale fron 0 to 10 (according to me).
/// As such, it converts the float value to a visually appealing 5-symbol string. /// As such, it converts the float value to a visually appealing 5-symbol string.
/// ///
/// Input: /// Input:
/// r: MetadataValue, should be of the enum type f64 (unchecked). /// r: MetadataValue, should be of the enum type f64 (unchecked).
/// str: Vec containing precomputed rating strings to select from. /// str: Vec containing precomputed rating strings to select from.
/// ///
/// Output: /// Output:
/// Some(String) if a rating exists, None otherwise. /// Some(String) if a rating exists, None otherwise.
fn rating_to_string(r: Option<&MetadataValue>, str: &Vec<String>) -> Option<String> { fn rating_to_string(r: Option<&MetadataValue>, str: &Vec<String>) -> Option<String> {
match r { match r {
Some(rating) => { Some(rating) => {
if let Some(f) = rating.as_f64() { if let Some(f) = rating.as_f64() {
let mut i = (f * 10_f64).round() as i64; let mut i = (f * 10_f64).round() as i64;
if i > 10 {i = 10} if i > 10 {
if i < 0 {i = 0} i = 10
}
if i < 0 {
i = 0
}
Some(str[i as usize].to_owned()) //TODO: still inefficient. would be better to note the idx and load it in print_text Some(str[i as usize].to_owned()) //TODO: still inefficient. would be better to note the idx and load it in print_text
} else { } else {
debug!("failed to convert MetadataValue to f64!"); debug!("failed to convert MetadataValue to f64!");
None None
} }
}, }
None => { None => {
trace!("no userRating MetadataValue found!"); trace!("no userRating MetadataValue found!");
None None
}, }
} }
} }
/// This higher level function updates the to be output Hashmap of strings. /// This higher level function updates the to be output Hashmap of strings.
/// It does so by querying each metadata field in config to the current player, then updating the Hashmap in Data with the new value(s). /// It does so by querying each metadata field in config to the current player, then updating the Hashmap in Data with the new value(s).
/// "xesam:userRating" is treated separately, due to requiring a different output format. /// "xesam:userRating" is treated separately, due to requiring a different output format.
/// ///
/// Input: /// Input:
/// cfg: Config struct for the program. Contains the wanted metadata fields. /// cfg: Config struct for the program. Contains the wanted metadata fields.
/// data: mutable Data struct for the program. Its' Hashmap containing strings is updated. /// data: mutable Data struct for the program. Its' Hashmap containing strings is updated.
/// ratings: Vec of precomputed rating strings. /// ratings: Vec of precomputed rating strings.
pub fn update_message(cfg: &Config, data: &mut Data, ratings: &Vec<String>) { pub fn update_message(cfg: &Config, data: &mut Data, ratings: &Vec<String>) {
if let Some(player) = &data.current_player { if let Some(player) = &data.current_player {
if let Ok(meta) = player.get_metadata() { if let Ok(meta) = player.get_metadata() {
for field in &cfg.metadata_fields { for field in &cfg.metadata_fields {
let key: &str = field.field.as_ref(); let key: &str = field.field.as_ref();
if field.field.eq("xesam:userRating") { if field.field.eq("xesam:userRating") {
if let Some(rating_string) = rating_to_string(meta.get(key), ratings) { if let Some(rating_string) = rating_to_string(meta.get(key), ratings) {
data.field_text.insert(key.to_owned(), rating_string); data.field_text.insert(key.to_owned(), rating_string);
} else { } else {
data.field_text.remove(key); data.field_text.remove(key);
} }
} else {
match meta.get(&key) {
Some(value) => {
trace!(
"update_messages: field {} has value {}",
key,
value_to_string(value, cfg.array_separator)
);
data.field_text
.insert(key.to_owned(), value_to_string(value, cfg.array_separator))
}
None => {
trace!("update_messages: field {} has no value!", key);
data.field_text.insert(
key.to_owned(),
format!("No {}", key.trim_start_matches("xesam:")),
)
}
};
}
}
} else { } else {
match meta.get(&key) { debug!(
Some(value) => data.field_text.insert(key.to_owned(), value_to_string(value, cfg.array_separator)), "update_messages: Player {} has no metadata!",
None => data.field_text.insert(key.to_owned(), format!("No {}", key.trim_start_matches("xesam:"))), player.unique_name()
}; );
} }
} } else {
debug!("update_messages: No player found!");
} }
} }
}

View File

@ -1,57 +1,66 @@
//! This file deals with updating the active player. //! This file deals with updating the active player.
//! It also updates the prefix, which kind of breaks seperation of concerns, but this saves me a lot of headache so I'm not changing it. //! It also updates the prefix, which kind of breaks seperation of concerns, but this saves me a lot of headache so I'm not changing it.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use log::trace; use crate::structs::{config::Config, data::Data};
use mpris::{PlayerFinder, Player}; use log::{debug, trace};
use crate::structs::{data::Data, config::Config}; use mpris::PlayerFinder;
/// This function updates the current prefix. /// This function updates the current prefix.
/// If no entry is found in config containing the active player, a default value is used instead ('>'). /// If no entry is found in config containing the active player, a default value is used instead ('>').
/// ///
/// Input: /// Input:
/// cfg: Config struct for the program, containing the hashmap of prefixes. /// cfg: Config struct for the program, containing the hashmap of prefixes.
/// data: mutable char containing the active prefix. /// data: mutable char containing the active prefix.
/// name: name of active player, to fetch the appropriate prefix from cfg. /// name: name of active player, to fetch the appropriate prefix from cfg.
fn update_prefix(cfg: &Config, data: &mut char, name: &str) { fn update_prefix(cfg: &Config, data: &mut char, name: &str) {
if let Some(char) = cfg.player_prefixes.get(name) { if let Some(char) = cfg.player_prefixes.get(name) {
*data = *char; *data = *char;
trace!("updated prefix to {}", data); trace!("updated prefix to {}", data);
} else { } else {
*data = *cfg.player_prefixes.get("default").unwrap_or(&'>'); *data = *cfg.player_prefixes.get("default").unwrap_or(&'>');
trace!("set prefix to default ({})", data); trace!("set prefix to default ({})", data);
} }
} }
/// This function updates which player is selected as 'active'. /// This function updates which player is selected as 'active'.
/// It only considers players present in the config.player_priorities field to be valid candidates, then selects the active one with the highest rating. /// It only considers players present in the config.player_priorities field to be valid candidates, then selects the active one with the highest rating.
/// If none of the acceptable players are available, current_player is set to None instead. /// If none of the acceptable players are available, current_player is set to None instead.
/// ///
/// Input: /// Input:
/// pf: PlayerFinder instance of the program. /// pf: PlayerFinder instance of the program.
/// cfg: Config struct of the program, containing the list of acceptable players. /// cfg: Config struct of the program, containing the list of acceptable players.
/// data: mutable Data struct of the program, containing a marker for the currently active player. /// data: mutable Data struct of the program, containing a marker for the currently active player.
pub fn update_players(pf: &PlayerFinder, cfg: &Config, data: &mut Data) { pub fn update_players(pf: &PlayerFinder, cfg: &Config, data: &mut Data) {
// get all acceptable players // get all acceptable players
let players = pf.find_all().unwrap_or(Vec::new()); let players = pf.find_all().unwrap_or(Vec::new());
if players.is_empty() { if players.is_empty() {
data.current_player = None; data.current_player = None;
} else { debug!("update_players: no players found!")
let mut active: BTreeMap<u8, Player> = BTreeMap::new();
for player in players {
if let Ok(mpris::PlaybackStatus::Playing) = player.get_playback_status() {
let idx = cfg.find_player_priorities_idx(player.identity());
active.insert(idx, player);
}
}
// select the player with the highest priority.
if let Some((_, player)) = active.pop_first() {
update_prefix(cfg, &mut data.prefix, player.identity());
data.current_player = Some(player);
} else { } else {
data.current_player = None; let mut trees = vec![BTreeMap::new(), BTreeMap::new(), BTreeMap::new()];
for player in players {
if let Ok(status) = player.get_playback_status() {
let idx = cfg.find_player_priorities_idx(player.identity());
match status {
mpris::PlaybackStatus::Playing => trees[0].insert(idx, player),
mpris::PlaybackStatus::Paused => trees[1].insert(idx, player),
mpris::PlaybackStatus::Stopped => trees[2].insert(idx, player),
};
}
}
// select the player with the highest priority.
for mut tree in trees {
if let Some((_, player)) = tree.pop_first() {
update_prefix(cfg, &mut data.prefix, player.identity());
debug!("update_players: updated player to {}!", player.identity());
data.current_player = Some(player);
break;
} else {
data.current_player = None;
debug!("update_players: No acceptable player found!");
}
}
} }
} }
}