Add support for MusicBrainz's Browse API #228
@ -3,7 +3,7 @@ use std::{thread, time};
|
|||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::musicbrainz::Mbid,
|
collection::musicbrainz::Mbid,
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient, PageSettings},
|
api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient, NextPage, PageSettings},
|
||||||
http::MusicBrainzHttp,
|
http::MusicBrainzHttp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -66,20 +66,19 @@ fn main() {
|
|||||||
println!("{rg:?}\n");
|
println!("{rg:?}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = response.release_group_offset;
|
let offset = response.page.release_group_offset;
|
||||||
let count = response.release_groups.len();
|
let count = response.release_groups.len();
|
||||||
response_counts.push(count);
|
response_counts.push(count);
|
||||||
let total = response.release_group_count;
|
let total = response.page.release_group_count;
|
||||||
|
|
||||||
println!("Release group offset : {offset}");
|
println!("Release group offset : {offset}");
|
||||||
println!("Release groups in this response: {count}");
|
println!("Release groups in this response: {count}");
|
||||||
println!("Release groups in total : {total}");
|
println!("Release groups in total : {total}");
|
||||||
|
|
||||||
let next_offset = offset + count;
|
match response.page.next_page_offset(count) {
|
||||||
if next_offset == total {
|
NextPage::Offset(next_offset) => paging.with_offset(next_offset),
|
||||||
break;
|
NextPage::Complete => break,
|
||||||
}
|
}
|
||||||
paging.with_offset(next_offset);
|
|
||||||
|
|
||||||
thread::sleep(time::Duration::from_secs(1));
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
|
38
src/external/musicbrainz/api/browse.rs
vendored
38
src/external/musicbrainz/api/browse.rs
vendored
@ -6,14 +6,31 @@ use crate::{
|
|||||||
collection::musicbrainz::Mbid,
|
collection::musicbrainz::Mbid,
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{
|
||||||
Error, MbReleaseGroupMeta, MusicBrainzClient, PageSettings, SerdeMbReleaseGroupMeta,
|
ApiDisplay, Error, MbReleaseGroupMeta, MusicBrainzClient, NextPage, PageSettings,
|
||||||
MB_BASE_URL,
|
SerdeMbReleaseGroupMeta, MB_BASE_URL,
|
||||||
},
|
},
|
||||||
IMusicBrainzHttp,
|
IMusicBrainzHttp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ApiDisplay;
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
|
pub struct BrowseReleaseGroupPage {
|
||||||
|
pub release_group_offset: usize,
|
||||||
|
pub release_group_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowseReleaseGroupPage {
|
||||||
|
pub fn next_page_offset(&self, page_count: usize) -> NextPage {
|
||||||
|
NextPage::next_page_offset(
|
||||||
|
self.release_group_offset,
|
||||||
|
self.release_group_count,
|
||||||
|
page_count,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SerdeBrowseReleaseGroupPage = BrowseReleaseGroupPage;
|
||||||
|
|
||||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||||
pub fn browse_release_group(
|
pub fn browse_release_group(
|
||||||
@ -60,24 +77,22 @@ impl<'a> BrowseReleaseGroupRequest<'a> {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct BrowseReleaseGroupResponse {
|
pub struct BrowseReleaseGroupResponse {
|
||||||
pub release_group_offset: usize,
|
|
||||||
pub release_group_count: usize,
|
|
||||||
pub release_groups: Vec<MbReleaseGroupMeta>,
|
pub release_groups: Vec<MbReleaseGroupMeta>,
|
||||||
|
pub page: BrowseReleaseGroupPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
struct DeserializeBrowseReleaseGroupResponse {
|
struct DeserializeBrowseReleaseGroupResponse {
|
||||||
release_group_offset: usize,
|
|
||||||
release_group_count: usize,
|
|
||||||
release_groups: Option<Vec<SerdeMbReleaseGroupMeta>>,
|
release_groups: Option<Vec<SerdeMbReleaseGroupMeta>>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
page: SerdeBrowseReleaseGroupPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeBrowseReleaseGroupResponse> for BrowseReleaseGroupResponse {
|
impl From<DeserializeBrowseReleaseGroupResponse> for BrowseReleaseGroupResponse {
|
||||||
fn from(value: DeserializeBrowseReleaseGroupResponse) -> Self {
|
fn from(value: DeserializeBrowseReleaseGroupResponse) -> Self {
|
||||||
BrowseReleaseGroupResponse {
|
BrowseReleaseGroupResponse {
|
||||||
release_group_offset: value.release_group_offset,
|
page: value.page,
|
||||||
release_group_count: value.release_group_count,
|
|
||||||
release_groups: value
|
release_groups: value
|
||||||
.release_groups
|
.release_groups
|
||||||
.map(|rgs| rgs.into_iter().map(Into::into).collect())
|
.map(|rgs| rgs.into_iter().map(Into::into).collect())
|
||||||
@ -120,14 +135,15 @@ mod tests {
|
|||||||
)]),
|
)]),
|
||||||
};
|
};
|
||||||
let de_response = DeserializeBrowseReleaseGroupResponse {
|
let de_response = DeserializeBrowseReleaseGroupResponse {
|
||||||
|
page: SerdeBrowseReleaseGroupPage {
|
||||||
release_group_offset: de_release_group_offset,
|
release_group_offset: de_release_group_offset,
|
||||||
release_group_count: de_release_group_count,
|
release_group_count: de_release_group_count,
|
||||||
|
},
|
||||||
release_groups: Some(vec![de_meta.clone()]),
|
release_groups: Some(vec![de_meta.clone()]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = BrowseReleaseGroupResponse {
|
let response = BrowseReleaseGroupResponse {
|
||||||
release_group_offset: de_release_group_offset,
|
page: de_response.page,
|
||||||
release_group_count: de_release_group_count,
|
|
||||||
release_groups: vec![de_meta.clone().into()],
|
release_groups: vec![de_meta.clone().into()],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
16
src/external/musicbrainz/api/mod.rs
vendored
16
src/external/musicbrainz/api/mod.rs
vendored
@ -84,6 +84,22 @@ impl PageSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum NextPage {
|
||||||
|
Offset(usize),
|
||||||
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NextPage {
|
||||||
|
pub fn next_page_offset(offset: usize, total_count: usize, page_count: usize) -> NextPage {
|
||||||
|
let next_offset = offset + page_count;
|
||||||
|
if next_offset < total_count {
|
||||||
|
NextPage::Offset(next_offset)
|
||||||
|
} else {
|
||||||
|
NextPage::Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct MbArtistMeta {
|
pub struct MbArtistMeta {
|
||||||
pub id: Mbid,
|
pub id: Mbid,
|
||||||
|
16
src/external/musicbrainz/api/search/artist.rs
vendored
16
src/external/musicbrainz/api/search/artist.rs
vendored
@ -3,7 +3,10 @@ use std::fmt;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::external::musicbrainz::api::{
|
use crate::external::musicbrainz::api::{
|
||||||
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
search::{
|
||||||
|
query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
||||||
|
SearchPage, SerdeSearchPage,
|
||||||
|
},
|
||||||
MbArtistMeta, SerdeMbArtistMeta,
|
MbArtistMeta, SerdeMbArtistMeta,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,18 +29,22 @@ impl_term!(string, SearchArtist<'a>, String, &'a str);
|
|||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct SearchArtistResponse {
|
pub struct SearchArtistResponse {
|
||||||
pub artists: Vec<SearchArtistResponseArtist>,
|
pub artists: Vec<SearchArtistResponseArtist>,
|
||||||
|
pub page: SearchPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
pub struct DeserializeSearchArtistResponse {
|
pub struct DeserializeSearchArtistResponse {
|
||||||
artists: Vec<DeserializeSearchArtistResponseArtist>,
|
artists: Vec<DeserializeSearchArtistResponseArtist>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
page: SerdeSearchPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeSearchArtistResponse> for SearchArtistResponse {
|
impl From<DeserializeSearchArtistResponse> for SearchArtistResponse {
|
||||||
fn from(value: DeserializeSearchArtistResponse) -> Self {
|
fn from(value: DeserializeSearchArtistResponse) -> Self {
|
||||||
SearchArtistResponse {
|
SearchArtistResponse {
|
||||||
artists: value.artists.into_iter().map(Into::into).collect(),
|
artists: value.artists.into_iter().map(Into::into).collect(),
|
||||||
|
page: value.page,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,6 +84,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn de_response() -> DeserializeSearchArtistResponse {
|
fn de_response() -> DeserializeSearchArtistResponse {
|
||||||
|
let de_offset = 24;
|
||||||
|
let de_count = 124;
|
||||||
let de_artist = DeserializeSearchArtistResponseArtist {
|
let de_artist = DeserializeSearchArtistResponseArtist {
|
||||||
score: 67,
|
score: 67,
|
||||||
meta: SerdeMbArtistMeta {
|
meta: SerdeMbArtistMeta {
|
||||||
@ -88,6 +97,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
DeserializeSearchArtistResponse {
|
DeserializeSearchArtistResponse {
|
||||||
artists: vec![de_artist.clone()],
|
artists: vec![de_artist.clone()],
|
||||||
|
page: SerdeSearchPage {
|
||||||
|
offset: de_offset,
|
||||||
|
count: de_count,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +114,7 @@ mod tests {
|
|||||||
meta: a.meta.into(),
|
meta: a.meta.into(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
page: de_response.page,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
src/external/musicbrainz/api/search/mod.rs
vendored
18
src/external/musicbrainz/api/search/mod.rs
vendored
@ -9,6 +9,7 @@ pub use release_group::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
|
use serde::Deserialize;
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
use crate::external::musicbrainz::{
|
use crate::external::musicbrainz::{
|
||||||
@ -22,6 +23,23 @@ use crate::external::musicbrainz::{
|
|||||||
IMusicBrainzHttp,
|
IMusicBrainzHttp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::NextPage;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
|
pub struct SearchPage {
|
||||||
|
pub offset: usize,
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchPage {
|
||||||
|
pub fn next_page_offset(&self, page_count: usize) -> NextPage {
|
||||||
|
NextPage::next_page_offset(self.offset, self.count, page_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SerdeSearchPage = SearchPage;
|
||||||
|
|
||||||
macro_rules! impl_search_entity {
|
macro_rules! impl_search_entity {
|
||||||
($name:ident, $entity:literal) => {
|
($name:ident, $entity:literal) => {
|
||||||
paste! {
|
paste! {
|
||||||
|
@ -5,7 +5,10 @@ use serde::Deserialize;
|
|||||||
use crate::{
|
use crate::{
|
||||||
collection::{album::AlbumDate, musicbrainz::Mbid},
|
collection::{album::AlbumDate, musicbrainz::Mbid},
|
||||||
external::musicbrainz::api::{
|
external::musicbrainz::api::{
|
||||||
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
search::{
|
||||||
|
query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
|
||||||
|
SearchPage, SerdeSearchPage,
|
||||||
|
},
|
||||||
ApiDisplay, MbReleaseGroupMeta, SerdeMbReleaseGroupMeta,
|
ApiDisplay, MbReleaseGroupMeta, SerdeMbReleaseGroupMeta,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -50,18 +53,22 @@ impl_term!(rgid, SearchReleaseGroup<'a>, Rgid, &'a Mbid);
|
|||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct SearchReleaseGroupResponse {
|
pub struct SearchReleaseGroupResponse {
|
||||||
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
||||||
|
pub page: SearchPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||||
pub struct DeserializeSearchReleaseGroupResponse {
|
pub struct DeserializeSearchReleaseGroupResponse {
|
||||||
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
|
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
page: SerdeSearchPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
|
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
|
||||||
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
|
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
|
||||||
SearchReleaseGroupResponse {
|
SearchReleaseGroupResponse {
|
||||||
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
||||||
|
page: value.page,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,6 +116,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn de_response() -> DeserializeSearchReleaseGroupResponse {
|
fn de_response() -> DeserializeSearchReleaseGroupResponse {
|
||||||
|
let de_offset = 26;
|
||||||
|
let de_count = 126;
|
||||||
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
|
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
|
||||||
score: 67,
|
score: 67,
|
||||||
meta: SerdeMbReleaseGroupMeta {
|
meta: SerdeMbReleaseGroupMeta {
|
||||||
@ -121,6 +130,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
DeserializeSearchReleaseGroupResponse {
|
DeserializeSearchReleaseGroupResponse {
|
||||||
release_groups: vec![de_release_group.clone()],
|
release_groups: vec![de_release_group.clone()],
|
||||||
|
page: SerdeSearchPage {
|
||||||
|
offset: de_offset,
|
||||||
|
count: de_count,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +147,7 @@ mod tests {
|
|||||||
meta: rg.meta.into(),
|
meta: rg.meta.into(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
page: de_response.page,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
4
src/tui/lib/external/musicbrainz/api/mod.rs
vendored
@ -57,7 +57,7 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
|||||||
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
|
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
|
||||||
let query = SearchArtistRequest::new().string(&artist.id.name);
|
let query = SearchArtistRequest::new().string(&artist.id.name);
|
||||||
|
|
||||||
let paging = PageSettings::with_max_limit();
|
let paging = PageSettings::default();
|
||||||
let mb_response = self.client.search_artist(&query, &paging)?;
|
let mb_response = self.client.search_artist(&query, &paging)?;
|
||||||
|
|
||||||
Ok(mb_response
|
Ok(mb_response
|
||||||
@ -83,7 +83,7 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
|||||||
.and()
|
.and()
|
||||||
.release_group(&album.id.title);
|
.release_group(&album.id.title);
|
||||||
|
|
||||||
let paging = PageSettings::with_max_limit();
|
let paging = PageSettings::default();
|
||||||
let mb_response = self.client.search_release_group(&query, &paging)?;
|
let mb_response = self.client.search_release_group(&query, &paging)?;
|
||||||
|
|
||||||
Ok(mb_response
|
Ok(mb_response
|
||||||
|
Loading…
Reference in New Issue
Block a user