#[macro_use] extern crate serde_derive; //#[macro_use] extern crate lazy_static; #[macro_use] extern crate log; //#[macro_use] extern crate failure; use std::env; use crate::config_file::ConfigFile; use std::collections::HashMap; use mpris::PlayerFinder; use std::time::Duration; use mpris::Event; use std::collections::HashSet; use failure::Error; use dbus::arg::Variant; use dbus::arg::RefArg; mod brainz; mod config_file; #[derive(Serialize, Deserialize, Debug)] #[serde(default)] pub struct BlacklistConf { /// Literal tags to reject pub tag: Vec, /// Tag sub-strings to reject (must be a whole word) pub tag_partial: Vec, /// Artists to reject pub artist: Vec, } impl Default for BlacklistConf { fn default() -> Self { Self { tag: vec![], artist: vec![], tag_partial: vec![ "hip-hop".to_owned(), "hip hop".to_owned(), "hiphop".to_owned(), "rap".to_owned(), ], } } } #[derive(Serialize, Deserialize, Debug)] #[serde(default)] pub struct WhiteList { /// Tags to allow despite e.g. a substring match pub tag: Vec, /// Artists to allow pub artist: Vec, } impl Default for WhiteList { fn default() -> Self { Self { tag: vec![], artist: vec![], } } } #[derive(Serialize, Deserialize, Debug)] #[serde(default)] pub struct Config { /// Logging - trace, debug, (info), warning, error pub logging: String, /// Players to handle (empty = all) pub allowed_players: Vec, /// Blacklists pub blacklist: BlacklistConf, /// Whitelist (overrides blacklist) pub whitelist: WhiteList, /// Min MusicBrainz search score for artist look-up pub artist_min_score: i32, /// Max nr of artists to check per track pub max_artists_per_track: u64, /// Interval in which the daemon probes DBUS for open MPRIS channels pub player_find_interval_ms: u64, /// Delay after a skip or allow, e.g. to prevent infinite skip chain when someone starts a rap playlist pub cooldown_ms: u64, /// Music Brainz API access timeout pub api_timeout_ms: u64, /// Allow playing songs from artists we couldn't verify pub allow_by_default: bool, } impl Default for Config { fn default() -> Self { Self { logging: "info".to_owned(), allowed_players: vec![], blacklist: BlacklistConf::default(), whitelist: WhiteList::default(), artist_min_score: 95, max_artists_per_track: 3, player_find_interval_ms: 2500, cooldown_ms: 1000, api_timeout_ms: 2000, allow_by_default: true, } } } const CONFIG_FILE: &'static str = "rapblock.json"; fn main() -> Result<(), Error> { // let config = match load_config() { // Ok(c) => c, // Err(e) => { // eprintln!("Could not load config from \"{}\": {}", CONFIG_FILE, e); // Config::default() // } // }; let config_file_path = env::current_dir()?.join(CONFIG_FILE); let mut cfg: ConfigFile = config_file::ConfigFile::new(config_file_path)?; let env = env_logger::Env::default().default_filter_or(&cfg.borrow().logging); env_logger::Builder::from_env(env).init(); let mut artist_cache = HashMap::::new(); info!("Waiting for players..."); 'main_loop: loop { // XXX this picks the first player, which isn't always ideal - see mpris/src/find.rs let player = PlayerFinder::new() .expect("Could not connect to D-Bus") .find_active(); let _ = cfg.update_if_needed(false); if let Ok(player) = player { let config = cfg.borrow(); let player_name = player.bus_name().to_string(); if !config.allowed_players.is_empty() && !config.allowed_players.contains(&player_name) { debug!("Ignoring player {}", player_name); ::std::thread::sleep(Duration::from_millis(config.player_find_interval_ms)); continue 'main_loop; } info!( "Connected to player: {}{}", player_name, player.unique_name() ); let events = player.events(); if events.is_err() { error!("Could not start event stream!"); // add a delay so we don't run too hot here ::std::thread::sleep(Duration::from_millis(config.player_find_interval_ms)); continue 'main_loop; } 'event_loop: for event in events.unwrap() { let _ = cfg.update_if_needed(false); let config = cfg.borrow(); match event { Ok(event) => { debug!("MPRIS event: {:#?}", event); match event { Event::PlayerShutDown => { info!("Player shut down"); break 'event_loop; } Event::TrackChanged(mut metadata) => { // we can't use the event metadata - it's incomplete ::std::thread::sleep(Duration::from_millis(250)); metadata = player.get_metadata().unwrap_or(metadata); let title = metadata.title().unwrap_or(""); info!("--- new track : {} ---", title); if title.is_empty() { warn!("Empty metadata! Wait for next track..."); continue 'event_loop; } let mut skip = !config.allow_by_default; let mut confidence = false; debug!("MPRIS meta: {:#?}", metadata); // try to get genre from the 'rest' map in the metadata struct #[allow(deprecated)] // 2.0.0 will provide some nicer API, hopefully let rest = metadata.rest(); let meta_genre : Option<&Variant>> = rest.get("xesam:genre"); match meta_genre { Some(variant) => { let b : &RefArg = variant.0.as_ref(); if let Some(s) = b.as_str() { info!("Using genre from MPRIS metadata: {}", s); skip = !brainz::check_genre(&config, &s.to_string().to_lowercase()); confidence = true; } else if let Some(list) = b.as_iter() { debug!("MPRIS contains array of genres"); for item in list { if let Some(s) = item.as_str() { info!("Using genre from MPRIS metadata: {}", s); skip = !brainz::check_genre(&config, &s.to_string().to_lowercase()); confidence = true; } } } }, None => { debug!("No MPRIS genre"); } }; if !confidence { let mut artists = HashSet::new(); if let Some(aa) = metadata.artists() { for a in aa { artists.insert(a); } } if let Some(aa) = metadata.album_artists() { for a in aa { artists.insert(a); } } let artists_e = artists .iter() .take(config.max_artists_per_track as usize) .enumerate(); 'artists_loop: for (an, a) in artists_e { info!("Checking artist #{}: {}", an + 1, a); if let Some(resolution) = artist_cache.get(*a) { confidence = true; if !resolution { info!("~ result cached: BAD"); skip = true; break 'artists_loop; } info!("~ result cached: GOOD"); continue 'artists_loop; } if config.whitelist.artist.contains(a) { info!("+ Whitelisted artist!"); // there may be other co-artists that spoil the song -> don't break yet artist_cache.insert(a.to_string(), true); confidence = true; continue 'artists_loop; } if config.blacklist.artist.contains(a) { info!("- Blacklisted artist!"); skip = true; artist_cache.insert(a.to_string(), false); confidence = true; break 'artists_loop; } let verdict = brainz::check_artist(&config, &a); match verdict { Ok(allow) => { confidence = true; artist_cache.insert(a.to_string(), allow); if allow { info!("Artist passed"); } else { skip = true; break 'artists_loop; } } Err(e) => { warn!("Something went wrong: {}", e); // probably no tags, or not found - use the default action artist_cache .insert(a.to_string(), config.allow_by_default); } } } } if skip || (!confidence && !config.allow_by_default) { info!(">>>>>> SKIP : {} >>>>>>\n", title); if player.next().is_err() { break 'event_loop; } } else { info!("Let it play...\n"); } ::std::thread::sleep(Duration::from_millis(config.cooldown_ms)); } _ => { debug!("Event not handled."); } } } Err(err) => { error!("D-Bus error: {}. Aborting.", err); break 'event_loop; } } } info!("Event stream ended - player likely shut down"); } else { debug!("No player found, waiting..."); } ::std::thread::sleep(Duration::from_millis(cfg.borrow().player_find_interval_ms)); } }