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"
|
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"
|
||||||
|
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::{
|
use musichoard::{
|
||||||
collection::musicbrainz::Mbid,
|
collection::musicbrainz::Mbid,
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
|
@ -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
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()),
|
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,
|
||||||
)]),
|
)]),
|
||||||
|
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,
|
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()),
|
||||||
|
@ -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)]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
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(),
|
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(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user