Add support for MusicBrainz's Browse API #228
@ -3,7 +3,7 @@ use std::{thread, time};
|
||||
use musichoard::{
|
||||
collection::musicbrainz::Mbid,
|
||||
external::musicbrainz::{
|
||||
api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient},
|
||||
api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient, PageSettings},
|
||||
http::MusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
@ -52,13 +52,14 @@ fn main() {
|
||||
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 request = BrowseReleaseGroupRequest::artist(&mbid);
|
||||
let mut paging = PageSettings::with_max_limit();
|
||||
|
||||
let mut response_counts: Vec<usize> = Vec::new();
|
||||
|
||||
loop {
|
||||
let response = client
|
||||
.browse_release_group(&request)
|
||||
.browse_release_group(&request, &paging)
|
||||
.expect("failed to make API call");
|
||||
|
||||
for rg in response.release_groups.iter() {
|
||||
@ -78,7 +79,7 @@ fn main() {
|
||||
if next_offset == total {
|
||||
break;
|
||||
}
|
||||
request.with_offset(next_offset);
|
||||
paging.with_offset(next_offset);
|
||||
|
||||
thread::sleep(time::Duration::from_secs(1));
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use musichoard::{
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
search::{SearchArtistRequest, SearchReleaseGroupRequest},
|
||||
MusicBrainzClient,
|
||||
MusicBrainzClient, PageSettings,
|
||||
},
|
||||
http::MusicBrainzHttp,
|
||||
},
|
||||
@ -106,8 +106,9 @@ fn main() {
|
||||
|
||||
println!("Query: {query}");
|
||||
|
||||
let paging = PageSettings::default();
|
||||
let matches = client
|
||||
.search_artist(&query)
|
||||
.search_artist(&query, &paging)
|
||||
.expect("failed to make API call");
|
||||
|
||||
println!("{matches:#?}");
|
||||
@ -138,8 +139,9 @@ fn main() {
|
||||
|
||||
println!("Query: {query}");
|
||||
|
||||
let paging = PageSettings::default();
|
||||
let matches = client
|
||||
.search_release_group(&query)
|
||||
.search_release_group(&query, &paging)
|
||||
.expect("failed to make API call");
|
||||
|
||||
println!("{matches:#?}");
|
||||
|
81
src/external/musicbrainz/api/browse.rs
vendored
81
src/external/musicbrainz/api/browse.rs
vendored
@ -5,30 +5,27 @@ use serde::Deserialize;
|
||||
use crate::{
|
||||
collection::musicbrainz::Mbid,
|
||||
external::musicbrainz::{
|
||||
api::{Error, MbReleaseGroupMeta, MusicBrainzClient, SerdeMbReleaseGroupMeta, MB_BASE_URL},
|
||||
api::{
|
||||
Error, MbReleaseGroupMeta, MusicBrainzClient, PageSettings, SerdeMbReleaseGroupMeta,
|
||||
MB_BASE_URL,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
|
||||
const MB_MAX_BROWSE_LIMIT: usize = 100;
|
||||
use super::ApiDisplay;
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub fn browse_release_group(
|
||||
&mut self,
|
||||
request: &BrowseReleaseGroupRequest,
|
||||
paging: &PageSettings,
|
||||
) -> 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 page = ApiDisplay::format_page_settings(paging);
|
||||
|
||||
let url = format!("{MB_BASE_URL}/release-group?{entity}={mbid}{limit}{offset}");
|
||||
let url = format!("{MB_BASE_URL}/release-group?{entity}={mbid}{page}");
|
||||
|
||||
let response: DeserializeBrowseReleaseGroupResponse = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
@ -38,8 +35,6 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub struct BrowseReleaseGroupRequest<'a> {
|
||||
entity: BrowseReleaseGroupRequestEntity,
|
||||
mbid: &'a Mbid,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
}
|
||||
|
||||
enum BrowseReleaseGroupRequestEntity {
|
||||
@ -59,25 +54,8 @@ impl<'a> BrowseReleaseGroupRequest<'a> {
|
||||
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)]
|
||||
@ -110,12 +88,15 @@ impl From<DeserializeBrowseReleaseGroupResponse> for BrowseReleaseGroupResponse
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
use mockall::predicate;
|
||||
|
||||
use crate::{
|
||||
collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||
external::musicbrainz::{
|
||||
api::{SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid},
|
||||
api::{
|
||||
SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid,
|
||||
MB_MAX_PAGE_LIMIT,
|
||||
},
|
||||
MockIMusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
@ -150,34 +131,12 @@ mod tests {
|
||||
release_groups: vec![de_meta.clone().into()],
|
||||
};
|
||||
|
||||
let mut seq = Sequence::new();
|
||||
|
||||
let url = format!("https://musicbrainz.org/ws/2/release-group?artist={mbid}",);
|
||||
let expect_response = de_response.clone();
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(expect_response));
|
||||
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2/release-group?artist={mbid}&limit={MB_MAX_BROWSE_LIMIT}",
|
||||
"https://musicbrainz.org/ws/2/release-group?artist={mbid}&limit={MB_MAX_PAGE_LIMIT}",
|
||||
);
|
||||
let expect_response = de_response.clone();
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(expect_response));
|
||||
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2/release-group?artist={mbid}&limit={MB_MAX_BROWSE_LIMIT}&offset={offset}",
|
||||
offset = de_release_group_offset,
|
||||
);
|
||||
let expect_response = de_response.clone();
|
||||
http.expect_get()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.with(predicate::eq(url))
|
||||
.return_once(|_| Ok(expect_response));
|
||||
|
||||
@ -186,16 +145,8 @@ mod tests {
|
||||
let mbid: Mbid = mbid.try_into().unwrap();
|
||||
|
||||
let request = BrowseReleaseGroupRequest::artist(&mbid);
|
||||
let result = client.browse_release_group(&request).unwrap();
|
||||
assert_eq!(result, response);
|
||||
|
||||
let request = request.with_max_limit();
|
||||
let result = client.browse_release_group(&request).unwrap();
|
||||
assert_eq!(result, response);
|
||||
|
||||
let mut request = request;
|
||||
request.with_offset(de_release_group_offset);
|
||||
let result = client.browse_release_group(&request).unwrap();
|
||||
let paging = PageSettings::with_max_limit();
|
||||
let result = client.browse_release_group(&request, &paging).unwrap();
|
||||
assert_eq!(result, response);
|
||||
}
|
||||
}
|
||||
|
57
src/external/musicbrainz/api/mod.rs
vendored
57
src/external/musicbrainz/api/mod.rs
vendored
@ -17,6 +17,8 @@ pub mod search;
|
||||
|
||||
const MB_BASE_URL: &str = "https://musicbrainz.org/ws/2";
|
||||
const MB_RATE_LIMIT_CODE: u16 = 503;
|
||||
const MB_MAX_PAGE_LIMIT: usize = 100;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// The HTTP client failed.
|
||||
@ -59,6 +61,29 @@ impl<Http> MusicBrainzClient<Http> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PageSettings {
|
||||
pub limit: Option<usize>,
|
||||
pub offset: Option<usize>,
|
||||
}
|
||||
|
||||
impl PageSettings {
|
||||
pub fn with_limit(limit: usize) -> Self {
|
||||
PageSettings {
|
||||
limit: Some(limit),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_max_limit() -> Self {
|
||||
Self::with_limit(MB_MAX_PAGE_LIMIT)
|
||||
}
|
||||
|
||||
pub fn with_offset(&mut self, offset: usize) {
|
||||
self.offset = Some(offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MbArtistMeta {
|
||||
pub id: Mbid,
|
||||
@ -123,6 +148,18 @@ impl From<SerdeMbReleaseGroupMeta> for MbReleaseGroupMeta {
|
||||
pub struct ApiDisplay;
|
||||
|
||||
impl ApiDisplay {
|
||||
fn format_page_settings(paging: &PageSettings) -> String {
|
||||
let limit = paging
|
||||
.limit
|
||||
.map(|l| format!("&limit={l}"))
|
||||
.unwrap_or_default();
|
||||
let offset = paging
|
||||
.offset
|
||||
.map(|o| format!("&offset={o}"))
|
||||
.unwrap_or_default();
|
||||
format!("{limit}{offset}")
|
||||
}
|
||||
|
||||
fn format_album_date(date: &AlbumDate) -> String {
|
||||
match date.year {
|
||||
Some(year) => match date.month {
|
||||
@ -305,6 +342,26 @@ mod tests {
|
||||
assert!(!format!("{unk_err:?}").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_page_settings() {
|
||||
let paging = PageSettings::default();
|
||||
assert_eq!(ApiDisplay::format_page_settings(&paging), "");
|
||||
|
||||
let paging = PageSettings::with_max_limit();
|
||||
assert_eq!(ApiDisplay::format_page_settings(&paging), "&limit=100");
|
||||
|
||||
let mut paging = PageSettings::with_limit(45);
|
||||
paging.with_offset(145);
|
||||
assert_eq!(
|
||||
ApiDisplay::format_page_settings(&paging),
|
||||
"&limit=45&offset=145"
|
||||
);
|
||||
|
||||
let mut paging = PageSettings::default();
|
||||
paging.with_offset(26);
|
||||
assert_eq!(ApiDisplay::format_page_settings(&paging), "&offset=26");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_album_date() {
|
||||
assert_eq!(
|
||||
|
@ -70,7 +70,7 @@ mod tests {
|
||||
use mockall::predicate;
|
||||
|
||||
use crate::external::musicbrainz::{
|
||||
api::{MusicBrainzClient, SerdeMbid},
|
||||
api::{MusicBrainzClient, PageSettings, SerdeMbid},
|
||||
MockIMusicBrainzHttp,
|
||||
};
|
||||
|
||||
@ -128,7 +128,8 @@ mod tests {
|
||||
|
||||
let query = SearchArtistRequest::new().string(name);
|
||||
|
||||
let matches = client.search_artist(&query).unwrap();
|
||||
let paging = PageSettings::default();
|
||||
let matches = client.search_artist(&query, &paging).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
}
|
||||
|
8
src/external/musicbrainz/api/search/mod.rs
vendored
8
src/external/musicbrainz/api/search/mod.rs
vendored
@ -17,7 +17,7 @@ use crate::external::musicbrainz::{
|
||||
artist::DeserializeSearchArtistResponse,
|
||||
release_group::DeserializeSearchReleaseGroupResponse,
|
||||
},
|
||||
Error, MusicBrainzClient, MB_BASE_URL,
|
||||
ApiDisplay, Error, MusicBrainzClient, PageSettings, MB_BASE_URL,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
};
|
||||
@ -27,11 +27,13 @@ macro_rules! impl_search_entity {
|
||||
paste! {
|
||||
pub fn [<search_ $name:snake>](
|
||||
&mut self,
|
||||
query: &[<Search $name Request>]
|
||||
query: &[<Search $name Request>],
|
||||
paging: &PageSettings,
|
||||
) -> Result<[<Search $name Response>], Error> {
|
||||
let query: String =
|
||||
form_urlencoded::byte_serialize(format!("{query}").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/{entity}?query={query}", entity = $entity);
|
||||
let page = ApiDisplay::format_page_settings(paging);
|
||||
let url = format!("{MB_BASE_URL}/{entity}?query={query}{page}", entity = $entity);
|
||||
|
||||
let response: [<DeserializeSearch $name Response>] = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
|
@ -99,8 +99,8 @@ mod tests {
|
||||
collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType,
|
||||
SerdeMbid,
|
||||
MusicBrainzClient, PageSettings, SerdeAlbumDate, SerdeAlbumPrimaryType,
|
||||
SerdeAlbumSecondaryType, SerdeMbid,
|
||||
},
|
||||
MockIMusicBrainzHttp,
|
||||
},
|
||||
@ -161,7 +161,8 @@ mod tests {
|
||||
|
||||
let query = SearchReleaseGroupRequest::new().string(title);
|
||||
|
||||
let matches = client.search_release_group(&query).unwrap();
|
||||
let paging = PageSettings::default();
|
||||
let matches = client.search_release_group(&query, &paging).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
|
||||
@ -198,7 +199,8 @@ mod tests {
|
||||
.and()
|
||||
.first_release_date(&date);
|
||||
|
||||
let matches = client.search_release_group(&query).unwrap();
|
||||
let paging = PageSettings::default();
|
||||
let matches = client.search_release_group(&query, &paging).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
|
||||
@ -226,7 +228,8 @@ mod tests {
|
||||
|
||||
let query = SearchReleaseGroupRequest::new().rgid(&rgid);
|
||||
|
||||
let matches = client.search_release_group(&query).unwrap();
|
||||
let paging = PageSettings::default();
|
||||
let matches = client.search_release_group(&query, &paging).unwrap();
|
||||
assert_eq!(matches, response);
|
||||
}
|
||||
}
|
||||
|
8
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
8
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
@ -18,7 +18,7 @@ use musichoard::{
|
||||
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
|
||||
SearchReleaseGroupResponseReleaseGroup,
|
||||
},
|
||||
MusicBrainzClient,
|
||||
MusicBrainzClient, PageSettings,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
},
|
||||
@ -57,7 +57,8 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
|
||||
let query = SearchArtistRequest::new().string(&artist.id.name);
|
||||
|
||||
let mb_response = self.client.search_artist(&query)?;
|
||||
let paging = PageSettings::with_max_limit();
|
||||
let mb_response = self.client.search_artist(&query, &paging)?;
|
||||
|
||||
Ok(mb_response
|
||||
.artists
|
||||
@ -82,7 +83,8 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||
.and()
|
||||
.release_group(&album.id.title);
|
||||
|
||||
let mb_response = self.client.search_release_group(&query)?;
|
||||
let paging = PageSettings::with_max_limit();
|
||||
let mb_response = self.client.search_release_group(&query, &paging)?;
|
||||
|
||||
Ok(mb_response
|
||||
.release_groups
|
||||
|
Loading…
Reference in New Issue
Block a user