Add support for MusicBrainz's Browse API #228
@ -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"
|
||||
|
98
examples/musicbrainz_api/browse.rs
Normal file
98
examples/musicbrainz_api/browse.rs
Normal 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>(),
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use musichoard::{
|
||||
collection::musicbrainz::Mbid,
|
||||
external::musicbrainz::{
|
||||
|
@ -1,5 +1,3 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::{num::ParseIntError, str::FromStr};
|
||||
|
||||
use musichoard::{
|
||||
|
111
src/external/musicbrainz/api/browse.rs
vendored
Normal file
111
src/external/musicbrainz/api/browse.rs
vendored
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
4
src/external/musicbrainz/api/lookup.rs
vendored
4
src/external/musicbrainz/api/lookup.rs
vendored
@ -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,
|
||||
)]),
|
||||
|
7
src/external/musicbrainz/api/mod.rs
vendored
7
src/external/musicbrainz/api/mod.rs
vendored
@ -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<AlbumPrimaryType>,
|
||||
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
|
||||
}
|
||||
|
||||
@ -101,7 +102,7 @@ pub struct SerdeMbReleaseGroupMeta {
|
||||
id: SerdeMbid,
|
||||
title: String,
|
||||
first_release_date: SerdeAlbumDate,
|
||||
primary_type: SerdeAlbumPrimaryType,
|
||||
primary_type: Option<SerdeAlbumPrimaryType>,
|
||||
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
|
||||
}
|
||||
|
||||
@ -111,7 +112,7 @@ impl From<SerdeMbReleaseGroupMeta> 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()),
|
||||
|
@ -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)]),
|
||||
},
|
||||
};
|
||||
|
4
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
4
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
@ -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(),
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user