From 3d447f324eff4dc66b0cb859c7d2e82d507f9f8e Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 29 Sep 2024 16:31:36 +0200 Subject: [PATCH] Browse API integration --- Cargo.toml | 5 + examples/musicbrainz_api/browse.rs | 98 ++++++++++++++++ examples/musicbrainz_api/lookup.rs | 2 - examples/musicbrainz_api/search.rs | 2 - src/external/musicbrainz/api/browse.rs | 111 ++++++++++++++++++ src/external/musicbrainz/api/lookup.rs | 4 +- src/external/musicbrainz/api/mod.rs | 7 +- .../musicbrainz/api/search/release_group.rs | 2 +- src/tui/lib/external/musicbrainz/api/mod.rs | 4 +- 9 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 examples/musicbrainz_api/browse.rs create mode 100644 src/external/musicbrainz/api/browse.rs diff --git a/Cargo.toml b/Cargo.toml index 19b269e..4582b62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,11 @@ required-features = ["bin", "database-json", "library-beets", "library-beets-ssh name = "musichoard-edit" required-features = ["bin", "database-json"] +[[example]] +name = "musicbrainz-api---browse" +path = "examples/musicbrainz_api/browse.rs" +required-features = ["bin", "musicbrainz"] + [[example]] name = "musicbrainz-api---lookup" path = "examples/musicbrainz_api/lookup.rs" diff --git a/examples/musicbrainz_api/browse.rs b/examples/musicbrainz_api/browse.rs new file mode 100644 index 0000000..397dd3a --- /dev/null +++ b/examples/musicbrainz_api/browse.rs @@ -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 = 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::>() + .join("+"), + response_counts.iter().sum::(), + ); + } + }, + } +} diff --git a/examples/musicbrainz_api/lookup.rs b/examples/musicbrainz_api/lookup.rs index 29e02ee..e3590b3 100644 --- a/examples/musicbrainz_api/lookup.rs +++ b/examples/musicbrainz_api/lookup.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - use musichoard::{ collection::musicbrainz::Mbid, external::musicbrainz::{ diff --git a/examples/musicbrainz_api/search.rs b/examples/musicbrainz_api/search.rs index 96fd893..7f4d2f6 100644 --- a/examples/musicbrainz_api/search.rs +++ b/examples/musicbrainz_api/search.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - use std::{num::ParseIntError, str::FromStr}; use musichoard::{ diff --git a/src/external/musicbrainz/api/browse.rs b/src/external/musicbrainz/api/browse.rs new file mode 100644 index 0000000..9a3ef17 --- /dev/null +++ b/src/external/musicbrainz/api/browse.rs @@ -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 MusicBrainzClient { + pub fn browse_release_group( + &mut self, + request: &BrowseReleaseGroupRequest, + ) -> Result { + 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, + offset: Option, +} + +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, +} + +#[derive(Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct DeserializeBrowseReleaseGroupResponse { + release_group_offset: usize, + release_group_count: usize, + release_groups: Option>, +} + +impl From 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(), + } + } +} diff --git a/src/external/musicbrainz/api/lookup.rs b/src/external/musicbrainz/api/lookup.rs index 46be188..3bfc5c7 100644 --- a/src/external/musicbrainz/api/lookup.rs +++ b/src/external/musicbrainz/api/lookup.rs @@ -152,7 +152,7 @@ mod tests { id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), title: String::from("an album"), first_release_date: SerdeAlbumDate((1986, 4).into()), - primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), secondary_types: Some(vec![SerdeAlbumSecondaryType( AlbumSecondaryType::Compilation, )]), @@ -192,7 +192,7 @@ mod tests { id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), title: String::from("an album"), first_release_date: SerdeAlbumDate((1986, 4).into()), - primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album), + primary_type: Some(SerdeAlbumPrimaryType(AlbumPrimaryType::Album)), secondary_types: Some(vec![SerdeAlbumSecondaryType( AlbumSecondaryType::Compilation, )]), diff --git a/src/external/musicbrainz/api/mod.rs b/src/external/musicbrainz/api/mod.rs index 70e9299..b269ee1 100644 --- a/src/external/musicbrainz/api/mod.rs +++ b/src/external/musicbrainz/api/mod.rs @@ -11,6 +11,7 @@ use crate::{ external::musicbrainz::HttpError, }; +pub mod browse; pub mod lookup; pub mod search; @@ -91,7 +92,7 @@ pub struct MbReleaseGroupMeta { pub id: Mbid, pub title: String, pub first_release_date: AlbumDate, - pub primary_type: AlbumPrimaryType, + pub primary_type: Option, pub secondary_types: Option>, } @@ -101,7 +102,7 @@ pub struct SerdeMbReleaseGroupMeta { id: SerdeMbid, title: String, first_release_date: SerdeAlbumDate, - primary_type: SerdeAlbumPrimaryType, + primary_type: Option, secondary_types: Option>, } @@ -111,7 +112,7 @@ impl From for MbReleaseGroupMeta { id: value.id.into(), title: value.title, 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 .map(|v| v.into_iter().map(Into::into).collect()), diff --git a/src/external/musicbrainz/api/search/release_group.rs b/src/external/musicbrainz/api/search/release_group.rs index d33d558..6e65c26 100644 --- a/src/external/musicbrainz/api/search/release_group.rs +++ b/src/external/musicbrainz/api/search/release_group.rs @@ -115,7 +115,7 @@ mod tests { id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()), title: String::from("an album"), 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)]), }, }; diff --git a/src/tui/lib/external/musicbrainz/api/mod.rs b/src/tui/lib/external/musicbrainz/api/mod.rs index f248f1d..b597cdb 100644 --- a/src/tui/lib/external/musicbrainz/api/mod.rs +++ b/src/tui/lib/external/musicbrainz/api/mod.rs @@ -115,7 +115,7 @@ fn from_lookup_release_group_response(entity: LookupReleaseGroupResponse) -> Loo seq: AlbumSeq::default(), info: AlbumInfo { 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(), }, }, @@ -150,7 +150,7 @@ fn from_search_release_group_response_release_group( seq: AlbumSeq::default(), info: AlbumInfo { 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(), }, },