Add support for MusicBrainz's Browse API #228

Merged
wojtek merged 9 commits from 160---provide-a-keyboard-shortcut-to-pull-all-release-groups-of-an-artist into main 2024-09-29 21:33:43 +02:00
9 changed files with 223 additions and 12 deletions
Showing only changes of commit 3d447f324e - Show all commits

View File

@ -47,6 +47,11 @@ required-features = ["bin", "database-json", "library-beets", "library-beets-ssh
name = "musichoard-edit" name = "musichoard-edit"
required-features = ["bin", "database-json"] required-features = ["bin", "database-json"]
[[example]]
name = "musicbrainz-api---browse"
path = "examples/musicbrainz_api/browse.rs"
required-features = ["bin", "musicbrainz"]
[[example]] [[example]]
name = "musicbrainz-api---lookup" name = "musicbrainz-api---lookup"
path = "examples/musicbrainz_api/lookup.rs" path = "examples/musicbrainz_api/lookup.rs"

View File

@ -0,0 +1,98 @@
use std::{thread, time};
use musichoard::{
collection::musicbrainz::Mbid,
external::musicbrainz::{
api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient},
http::MusicBrainzHttp,
},
};
use structopt::StructOpt;
use uuid::Uuid;
const USER_AGENT: &str = concat!(
"MusicHoard---examples---musicbrainz-api---browse/",
env!("CARGO_PKG_VERSION"),
" ( musichoard@thenineworlds.net )"
);
#[derive(StructOpt)]
struct Opt {
#[structopt(subcommand)]
entity: OptEntity,
}
#[derive(StructOpt)]
enum OptEntity {
#[structopt(about = "Browse release groups")]
ReleaseGroup(OptReleaseGroup),
}
#[derive(StructOpt)]
enum OptReleaseGroup {
#[structopt(about = "Browse release groups of an artist")]
Artist(OptMbid),
}
#[derive(StructOpt)]
struct OptMbid {
#[structopt(help = "MBID of the entity")]
mbid: Uuid,
}
fn main() {
let opt = Opt::from_args();
println!("USER_AGENT: {USER_AGENT}");
let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client");
let mut client = MusicBrainzClient::new(http);
match opt.entity {
OptEntity::ReleaseGroup(opt_release_group) => match opt_release_group {
OptReleaseGroup::Artist(opt_mbid) => {
let mbid: Mbid = opt_mbid.mbid.into();
let mut request = BrowseReleaseGroupRequest::artist(&mbid).with_max_limit();
let mut response_counts: Vec<usize> = Vec::new();
loop {
let response = client
.browse_release_group(&request)
.expect("failed to make API call");
for rg in response.release_groups.iter() {
println!("{rg:?}\n");
}
let offset = response.release_group_offset;
let count = response.release_groups.len();
response_counts.push(count);
let total = response.release_group_count;
println!("Release group offset : {offset}");
println!("Release groups in this response: {count}");
println!("Release groups in total : {total}");
let next_offset = offset + count;
if next_offset == total {
break;
}
request.with_offset(next_offset);
thread::sleep(time::Duration::from_secs(1));
}
println!(
"Total: {}={} release groups",
response_counts
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join("+"),
response_counts.iter().sum::<usize>(),
);
}
},
}
}

View File

@ -1,5 +1,3 @@
#![allow(non_snake_case)]
use musichoard::{ use musichoard::{
collection::musicbrainz::Mbid, collection::musicbrainz::Mbid,
external::musicbrainz::{ external::musicbrainz::{

View File

@ -1,5 +1,3 @@
#![allow(non_snake_case)]
use std::{num::ParseIntError, str::FromStr}; use std::{num::ParseIntError, str::FromStr};
use musichoard::{ use musichoard::{

111
src/external/musicbrainz/api/browse.rs vendored Normal file
View File

@ -0,0 +1,111 @@
use std::fmt;
use serde::Deserialize;
use crate::{
collection::musicbrainz::Mbid,
external::musicbrainz::{
api::{Error, MusicBrainzClient, MB_BASE_URL},
IMusicBrainzHttp,
},
};
use super::{MbReleaseGroupMeta, SerdeMbReleaseGroupMeta};
const MB_MAX_BROWSE_LIMIT: usize = 100;
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
pub fn browse_release_group(
&mut self,
request: &BrowseReleaseGroupRequest,
) -> Result<BrowseReleaseGroupResponse, Error> {
let entity = &request.entity;
let mbid = request.mbid.uuid().as_hyphenated();
let limit = request
.limit
.map(|l| format!("&limit={l}"))
.unwrap_or_default();
let offset = request
.offset
.map(|o| format!("&offset={o}"))
.unwrap_or_default();
let url = format!("{MB_BASE_URL}/release-group?{entity}={mbid}{limit}{offset}");
let response: DeserializeBrowseReleaseGroupResponse = self.http.get(&url)?;
Ok(response.into())
}
}
pub struct BrowseReleaseGroupRequest<'a> {
entity: BrowseReleaseGroupRequestEntity,
mbid: &'a Mbid,
limit: Option<usize>,
offset: Option<usize>,
}
enum BrowseReleaseGroupRequestEntity {
Artist,
}
impl fmt::Display for BrowseReleaseGroupRequestEntity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BrowseReleaseGroupRequestEntity::Artist => write!(f, "artist"),
}
}
}
impl<'a> BrowseReleaseGroupRequest<'a> {
pub fn artist(mbid: &'a Mbid) -> Self {
BrowseReleaseGroupRequest {
entity: BrowseReleaseGroupRequestEntity::Artist,
mbid,
limit: None,
offset: None,
}
}
#[must_use]
pub const fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub const fn with_max_limit(self) -> Self {
self.with_limit(MB_MAX_BROWSE_LIMIT)
}
pub fn with_offset(&mut self, offset: usize) {
self.offset = Some(offset);
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct BrowseReleaseGroupResponse {
pub release_group_offset: usize,
pub release_group_count: usize,
pub release_groups: Vec<MbReleaseGroupMeta>,
}
#[derive(Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeBrowseReleaseGroupResponse {
release_group_offset: usize,
release_group_count: usize,
release_groups: Option<Vec<SerdeMbReleaseGroupMeta>>,
}
impl From<DeserializeBrowseReleaseGroupResponse> for BrowseReleaseGroupResponse {
fn from(value: DeserializeBrowseReleaseGroupResponse) -> Self {
BrowseReleaseGroupResponse {
release_group_offset: value.release_group_offset,
release_group_count: value.release_group_count,
release_groups: value
.release_groups
.map(|rgs| rgs.into_iter().map(Into::into).collect())
.unwrap_or_default(),
}
}
}

View File

@ -152,7 +152,7 @@ mod tests {
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
title: String::from("an album"), title: String::from("an album"),
first_release_date: SerdeAlbumDate((1986, 4).into()), first_release_date: SerdeAlbumDate((1986, 4).into()),
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: Some(vec![SerdeAlbumSecondaryType( secondary_types: Some(vec![SerdeAlbumSecondaryType(
AlbumSecondaryType::Compilation, AlbumSecondaryType::Compilation,
)]), )]),
@ -192,7 +192,7 @@ mod tests {
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
title: String::from("an album"), title: String::from("an album"),
first_release_date: SerdeAlbumDate((1986, 4).into()), first_release_date: SerdeAlbumDate((1986, 4).into()),
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: Some(vec![SerdeAlbumSecondaryType( secondary_types: Some(vec![SerdeAlbumSecondaryType(
AlbumSecondaryType::Compilation, AlbumSecondaryType::Compilation,
)]), )]),

View File

@ -11,6 +11,7 @@ use crate::{
external::musicbrainz::HttpError, external::musicbrainz::HttpError,
}; };
pub mod browse;
pub mod lookup; pub mod lookup;
pub mod search; pub mod search;
@ -91,7 +92,7 @@ pub struct MbReleaseGroupMeta {
pub id: Mbid, pub id: Mbid,
pub title: String, pub title: String,
pub first_release_date: AlbumDate, pub first_release_date: AlbumDate,
pub primary_type: AlbumPrimaryType, pub primary_type: Option<AlbumPrimaryType>,
pub secondary_types: Option<Vec<AlbumSecondaryType>>, pub secondary_types: Option<Vec<AlbumSecondaryType>>,
} }
@ -101,7 +102,7 @@ pub struct SerdeMbReleaseGroupMeta {
id: SerdeMbid, id: SerdeMbid,
title: String, title: String,
first_release_date: SerdeAlbumDate, first_release_date: SerdeAlbumDate,
primary_type: SerdeAlbumPrimaryType, primary_type: Option<SerdeAlbumPrimaryType>,
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>, secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
} }
@ -111,7 +112,7 @@ impl From<SerdeMbReleaseGroupMeta> for MbReleaseGroupMeta {
id: value.id.into(), id: value.id.into(),
title: value.title, title: value.title,
first_release_date: value.first_release_date.into(), first_release_date: value.first_release_date.into(),
primary_type: value.primary_type.into(), primary_type: value.primary_type.map(Into::into),
secondary_types: value secondary_types: value
.secondary_types .secondary_types
.map(|v| v.into_iter().map(Into::into).collect()), .map(|v| v.into_iter().map(Into::into).collect()),

View File

@ -115,7 +115,7 @@ mod tests {
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
title: String::from("an album"), title: String::from("an album"),
first_release_date: SerdeAlbumDate((1986, 4).into()), first_release_date: SerdeAlbumDate((1986, 4).into()),
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)),
secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]), secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]),
}, },
}; };

View File

@ -115,7 +115,7 @@ fn from_lookup_release_group_response(entity: LookupReleaseGroupResponse) -> Loo
seq: AlbumSeq::default(), seq: AlbumSeq::default(),
info: AlbumInfo { info: AlbumInfo {
musicbrainz: MbRefOption::Some(entity.meta.id.into()), musicbrainz: MbRefOption::Some(entity.meta.id.into()),
primary_type: Some(entity.meta.primary_type), primary_type: entity.meta.primary_type,
secondary_types: entity.meta.secondary_types.unwrap_or_default(), secondary_types: entity.meta.secondary_types.unwrap_or_default(),
}, },
}, },
@ -150,7 +150,7 @@ fn from_search_release_group_response_release_group(
seq: AlbumSeq::default(), seq: AlbumSeq::default(),
info: AlbumInfo { info: AlbumInfo {
musicbrainz: MbRefOption::Some(entity.meta.id.into()), musicbrainz: MbRefOption::Some(entity.meta.id.into()),
primary_type: Some(entity.meta.primary_type), primary_type: entity.meta.primary_type,
secondary_types: entity.meta.secondary_types.unwrap_or_default(), secondary_types: entity.meta.secondary_types.unwrap_or_default(),
}, },
}, },