|
|
|
@ -1,19 +1,140 @@ |
|
|
|
|
#[macro_use] extern crate serde_derive; |
|
|
|
|
#[macro_use] extern crate lazy_static; |
|
|
|
|
//#[macro_use] extern crate lazy_static;
|
|
|
|
|
#[macro_use] extern crate log; |
|
|
|
|
#[macro_use] extern crate failure; |
|
|
|
|
|
|
|
|
|
mod brainz; |
|
|
|
|
|
|
|
|
|
use mpris::{PlayerFinder,Event}; |
|
|
|
|
use failure::Error; |
|
|
|
|
use std::time::Duration; |
|
|
|
|
use std::collections::HashSet; |
|
|
|
|
use std::env; |
|
|
|
|
use std::path::Path; |
|
|
|
|
use std::fs::File; |
|
|
|
|
use std::io::Read; |
|
|
|
|
use std::collections::HashMap; |
|
|
|
|
|
|
|
|
|
const DELAY_FIND_PLAYER : Duration = Duration::from_millis(1000); |
|
|
|
|
#[derive(Serialize, Deserialize, Debug)] |
|
|
|
|
#[serde(default)] |
|
|
|
|
pub struct BlacklistConf { |
|
|
|
|
pub tag: Vec<String>, |
|
|
|
|
pub tag_partial: Vec<String>, |
|
|
|
|
pub artist: Vec<String>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
pub tag: Vec<String>, |
|
|
|
|
pub artist: Vec<String>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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, |
|
|
|
|
/// 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(), |
|
|
|
|
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 load_config() -> Result<Config, Error> { |
|
|
|
|
let config_file_path = env::current_dir()?.join(CONFIG_FILE); |
|
|
|
|
let buf = read_file(config_file_path)?; |
|
|
|
|
|
|
|
|
|
let config : Config = serde_json::from_str(&buf)?; |
|
|
|
|
|
|
|
|
|
// Validations
|
|
|
|
|
match config.logging.as_ref() { |
|
|
|
|
"info" | "debug" | "trace" | "warning" | "error" => (), |
|
|
|
|
_ => bail!("Invalid value for \"logging\""), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
Ok(config) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String, Error> { |
|
|
|
|
let path = path.as_ref(); |
|
|
|
|
let mut file = File::open(path)?; |
|
|
|
|
|
|
|
|
|
let mut buf = String::new(); |
|
|
|
|
file.read_to_string(&mut buf)?; |
|
|
|
|
Ok(buf) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn main() -> Result<(), Error> { |
|
|
|
|
let env = env_logger::Env::default().default_filter_or("info"); |
|
|
|
|
let config = match load_config() { |
|
|
|
|
Ok(c) => c, |
|
|
|
|
Err(e) => { |
|
|
|
|
eprintln!("Could not load config from \"{}\": {}", CONFIG_FILE, e); |
|
|
|
|
Config::default() |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let env = env_logger::Env::default().default_filter_or(&config.logging); |
|
|
|
|
env_logger::Builder::from_env(env).init(); |
|
|
|
|
|
|
|
|
|
debug!("Loaded config:\n{}", serde_json::to_string_pretty(&config)?); |
|
|
|
|
|
|
|
|
|
let mut artist_cache = HashMap::<String, bool>::new(); |
|
|
|
|
|
|
|
|
|
'main_loop: |
|
|
|
|
loop { |
|
|
|
|
let player = PlayerFinder::new() |
|
|
|
@ -27,7 +148,7 @@ fn main() -> Result<(), Error> { |
|
|
|
|
if events.is_err() { |
|
|
|
|
error!("Could not start event stream!"); |
|
|
|
|
// add a delay so we don't run too hot here
|
|
|
|
|
::std::thread::sleep(DELAY_FIND_PLAYER); |
|
|
|
|
::std::thread::sleep(Duration::from_millis(config.player_find_interval_ms)); |
|
|
|
|
continue 'main_loop; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -52,39 +173,77 @@ fn main() -> Result<(), Error> { |
|
|
|
|
continue 'event_loop; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
'artists_loop: |
|
|
|
|
for ar in metadata.artists().into_iter().chain(metadata.artists()) { |
|
|
|
|
for a in ar { |
|
|
|
|
info!("Checking artist: {}", a); |
|
|
|
|
let mut artists = HashSet::new(); |
|
|
|
|
|
|
|
|
|
let verdict = brainz::artist_sucks(&a); |
|
|
|
|
match verdict { |
|
|
|
|
Ok(verdict) => { |
|
|
|
|
if verdict { |
|
|
|
|
if player.can_go_next().unwrap_or(false) { |
|
|
|
|
info!(">>>>>> SKIP >>>>>>"); |
|
|
|
|
if player.next().is_err() { |
|
|
|
|
break 'artists_loop; |
|
|
|
|
if let Some(aa) = metadata.artists() { |
|
|
|
|
for a in aa { artists.insert(a); } |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
info!("<><><> STOP <><><>"); |
|
|
|
|
if player.pause().is_err() { |
|
|
|
|
if let Some(aa) = metadata.album_artists() { |
|
|
|
|
for a in aa { artists.insert(a); } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let mut skip = false; |
|
|
|
|
let mut confidence = false; |
|
|
|
|
'artists_loop: |
|
|
|
|
for (an, a) in artists.iter().take(config.max_artists_per_track as usize).enumerate() { |
|
|
|
|
info!("Checking artist #{}: {}", an+1, a); |
|
|
|
|
if let Some(resolution) = artist_cache.get(a.as_str()) { |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
// we add a delay here to prevent going insane on rap playlists
|
|
|
|
|
::std::thread::sleep(Duration::from_millis(1000)); |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
info!("Good artist, let it play"); |
|
|
|
|
skip = true; |
|
|
|
|
break 'artists_loop; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
Err(e) => { |
|
|
|
|
error!("Something went wrong: {}", e); |
|
|
|
|
info!("Letting to play"); |
|
|
|
|
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."); |
|
|
|
@ -102,7 +261,7 @@ fn main() -> Result<(), Error> { |
|
|
|
|
debug!("No player found, waiting..."); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
::std::thread::sleep(DELAY_FIND_PLAYER); |
|
|
|
|
::std::thread::sleep(Duration::from_millis(config.player_find_interval_ms)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|