|
|
|
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
|
|
|
use failure::Error;
|
|
|
|
use std::io::Read;
|
|
|
|
use std::time::Duration;
|
|
|
|
use regex::Regex;
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
struct MBArtistQueryResult {
|
|
|
|
created : String,
|
|
|
|
count: i32,
|
|
|
|
offset: i32,
|
|
|
|
artists: Option<Vec<MBArtist>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
struct Tag {
|
|
|
|
count: i32, // negative doesn't make sense, but it sometimes occurs
|
|
|
|
name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
struct MBArtist {
|
|
|
|
id: String,
|
|
|
|
score: i32,
|
|
|
|
name: String,
|
|
|
|
#[serde(rename="sort-name")]
|
|
|
|
sort_name: String,
|
|
|
|
tags: Option<Vec<Tag>>
|
|
|
|
}
|
|
|
|
|
|
|
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
|
|
|
|
|
|
|
pub fn check_artist(config : &crate::Config, artist : &str) -> Result<bool, Error> {
|
|
|
|
let query = utf8_percent_encode(&format!("\"{}\"", artist), DEFAULT_ENCODE_SET).to_string();
|
|
|
|
let url = format!("https://musicbrainz.org/ws/2/artist?query={}&fmt=json&inc=tags", query);
|
|
|
|
|
|
|
|
info!("Querying MusicBrainz for artist \"{}\", query URL: {}", artist, url);
|
|
|
|
|
|
|
|
let mut resp = String::new();
|
|
|
|
|
|
|
|
let ua = format!("Bad Song Skipper / {v} ( https://git.ondrovo.com/MightyPork/rapblock )", v=VERSION);
|
|
|
|
|
|
|
|
debug!("Using UA: {}", ua);
|
|
|
|
|
|
|
|
reqwest::Client::builder()
|
|
|
|
.timeout(Duration::from_millis(config.api_timeout_ms))
|
|
|
|
.build()?
|
|
|
|
.get(&url)
|
|
|
|
.header(reqwest::header::USER_AGENT, ua.as_str())
|
|
|
|
.header(reqwest::header::ACCEPT, "text/json")
|
|
|
|
.send()?
|
|
|
|
.read_to_string(&mut resp)?;
|
|
|
|
|
|
|
|
debug!("Resp: {}", resp);
|
|
|
|
|
|
|
|
let result : MBArtistQueryResult = serde_json::from_str(&resp)?;
|
|
|
|
|
|
|
|
info!("Response OK");
|
|
|
|
debug!("{:#?}", result);
|
|
|
|
|
|
|
|
if result.count == 0 {
|
|
|
|
// not found, let's hope it's OK
|
|
|
|
warn!("No results!");
|
|
|
|
return Err(failure::err_msg("Artist not found"));
|
|
|
|
} else {
|
|
|
|
info!("Got {} results, checking where score >= {}", result.count, config.artist_min_score);
|
|
|
|
let artists = result.artists.as_ref().unwrap();
|
|
|
|
|
|
|
|
let mut confidence = false;
|
|
|
|
let mut passed = true;
|
|
|
|
|
|
|
|
'artists: for (an, a) in artists.iter().enumerate() {
|
|
|
|
if a.score >= config.artist_min_score {
|
|
|
|
if a.tags.is_some() {
|
|
|
|
let tags = a.tags.as_ref().unwrap();
|
|
|
|
let as_vec: Vec<&String> = tags.iter().map(|t| &t.name).collect();
|
|
|
|
|
|
|
|
info!("Artist #{} - \"{}\" has tags: {:?}", an+1, a.name, as_vec);
|
|
|
|
'tags: for tag in as_vec {
|
|
|
|
if config.whitelist.tag.contains(tag) {
|
|
|
|
info!("+ Whitelisted tag \"{}\"", tag);
|
|
|
|
continue 'tags;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut blacklisted = false;
|
|
|
|
if config.blacklist.tag.contains(&tag) {
|
|
|
|
info!("- Blacklisted tag \"{}\"", tag);
|
|
|
|
blacklisted = true;
|
|
|
|
} else {
|
|
|
|
'blacklist:
|
|
|
|
for t in config.blacklist.tag_partial.iter() {
|
|
|
|
let re = Regex::new(&format!(r"\b{}\b", regex::escape(t)));
|
|
|
|
|
|
|
|
let matches = match re {
|
|
|
|
Ok(re) => re.is_match(tag),
|
|
|
|
Err(_) => tag.contains(t)
|
|
|
|
};
|
|
|
|
|
|
|
|
if matches {
|
|
|
|
info!("- Blacklisted tag \"{}\" - due to substring \"{}\"", tag, t);
|
|
|
|
blacklisted = true;
|
|
|
|
break 'blacklist;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if blacklisted {
|
|
|
|
confidence = true;
|
|
|
|
passed = false;
|
|
|
|
break 'artists;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
info!("All tags OK");
|
|
|
|
confidence = true;
|
|
|
|
} else {
|
|
|
|
warn!("Artist #{} - \"{}\" has no tags, can't determine genre", an+1, a.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if confidence {
|
|
|
|
return Ok(passed);
|
|
|
|
} else {
|
|
|
|
return Err(failure::err_msg("Artist found, but has no tags"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|