Add initial (buggy) fetch implementation

main
mat ess 2023-04-20 22:00:54 -04:00
parent 0e8fcd0390
commit a91044c0a9
7 changed files with 1358 additions and 20 deletions

1199
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
musicbrainz_rs = "0.5.0"
nanoserde = "0.1.32"
pico-args = { version = "0.5.0", features = ["eq-separator", "short-space-opt", "combined-flags"] }
tokio = { version = "1.27.0", features = ["full"] }
[dev-dependencies]
criterion = "0.4.0"

View File

@ -15,10 +15,6 @@ fn bench_random_library(c: &mut Criterion) {
bench_random_library_with_sizes(c, "massive", 1000, 2500);
}
fn bench_supermassive_random_library(c: &mut Criterion) {
bench_random_library_with_sizes(c, "supermassive", 2500, 7500);
}
// benchmark helpers
fn bench_random_library_with_sizes(c: &mut Criterion, size: &str, artists: usize, songs: usize) {
@ -70,9 +66,4 @@ fn random_library(artists: usize, songs_per_artist: usize) -> Library {
// entrypoint
criterion_group!(benches, bench_random_library);
criterion_group! {
name = slow_benches;
config = Criterion::default().sample_size(20);
targets = bench_supermassive_random_library
}
criterion_main!(benches, slow_benches);
criterion_main!(benches);

11
justfile Normal file
View File

@ -0,0 +1,11 @@
bench:
cargo bench --profile release -- "#rnd"
bench-supermassive:
cargo bench --profile release -- "#spr"
bench-all:
cargo bench --profile release
flamegraph:
cargo flamegraph --bench bench_library --root --open -- --bench --profile-time 10

111
src/fetch.rs Normal file
View File

@ -0,0 +1,111 @@
use std::collections::HashSet;
use musicbrainz_rs::{
config,
entity::{
artist::{Artist, ArtistSearchQuery, ArtistType},
recording::Recording,
relations::{Relation, RelationContent},
},
prelude::*,
};
use crate::library;
const USER_AGENT: &str = "cover/0.1.0 ( mat@mat.services )";
pub async fn fetch_library(
names: impl IntoIterator<Item = String>,
) -> Result<library::Library, Error> {
config::set_user_agent(USER_AGENT);
let mut artists = vec![];
for name in names {
let artist = fetch_artist(name).await?;
println!("{artist:#?}");
artists.push(artist);
}
Ok(library::Library::new(artists))
}
async fn fetch_artist(name: String) -> Result<library::Artist, Error> {
let query = ArtistSearchQuery::query_builder().artist(&name).build();
let result = Artist::search(query)
.with_artist_relations()
.execute()
.await?;
let artist = result.entities.first().expect("Failed to load {name}");
let artist = Artist::fetch()
.id(&artist.id)
.with_artist_relations()
.execute()
.await?;
let members =
if artist.artist_type.is_some() && is_plural_artist(artist.artist_type.as_ref().unwrap()) {
artist
.relations
.as_ref()
.expect("Failed to load relations")
.iter()
.flat_map(member_from_relation)
.collect()
} else {
vec![artist.name.clone()]
};
let recordings = Recording::browse()
.by_artist(&artist.id)
.with_work_relations()
.with_work_level_relations()
.execute()
.await?;
let catalog = recordings
.entities
.into_iter()
.map(|recording| convert_recording(&artist, &members, recording));
Ok(library::Artist::new(name, members.clone(), catalog))
}
fn is_plural_artist(kind: &ArtistType) -> bool {
matches!(
kind,
ArtistType::Choir | ArtistType::Group | ArtistType::Orchestra
)
}
fn member_from_relation(relation: &Relation) -> Option<String> {
if relation.relation_type == "member of band" {
let artist = extract_artist(relation);
Some(artist.name.clone())
} else {
None
}
}
fn convert_recording(artist: &Artist, members: &[String], recording: Recording) -> library::Song {
let writers = get_writers(members, &recording);
library::Song {
title: recording.title,
performer: artist.name.clone(),
writers,
}
}
fn get_writers(members: &[String], recording: &Recording) -> HashSet<String> {
recording.relations.as_ref().map_or_else(
|| HashSet::from_iter(members.iter().cloned()),
|relations| relations.iter().flat_map(writer_from_relation).collect(),
)
}
fn writer_from_relation(relation: &Relation) -> Option<String> {
match relation.relation_type.as_str() {
"writer" | "composer" => Some(extract_artist(relation).name.clone()),
_ => None,
}
}
fn extract_artist(relation: &Relation) -> &Artist {
match &relation.content {
RelationContent::Artist(artist) => artist,
_ => unreachable!("Bad relation"),
}
}

View File

@ -1 +1,2 @@
pub mod fetch;
pub mod library;

View File

@ -1,16 +1,40 @@
use std::{env, fs};
use std::fs;
use std::process::exit;
use nanoserde::DeJson;
use pico_args::Arguments;
use cover::library::Library;
use cover::fetch::fetch_library;
fn main() {
let file_path = env::args()
.nth(1)
.expect("Usage: cover path/to/library.json");
let json = fs::read_to_string(file_path).expect("Failed to open library file");
let library: Library =
DeJson::deserialize_json(&json).expect("Failed to deserialize library file");
const USAGE: &str = "Usage:
# process library.json
$ cover json path/to/library.json
# fetch artists from musicbrainz
$ cover fetch 'Taylor Swift' 'black midi'";
#[tokio::main]
async fn main() {
let mut args = Arguments::from_env();
let library = match args.subcommand().unwrap().as_deref() {
Some("json") => {
let file_path = args
.finish()
.into_iter()
.next()
.unwrap()
.into_string()
.unwrap();
let json = fs::read_to_string(file_path).expect("Failed to open library file");
DeJson::deserialize_json(&json).expect("Failed to deserialize library file")
}
Some("fetch") => fetch_library(args.finish().into_iter().flat_map(|s| s.into_string()))
.await
.unwrap(),
_ => {
eprintln!("{USAGE}");
exit(1);
}
};
for cover in library.library_covers() {
println!("Found cover!");
println!(