Integrate browse API into TUI MB daemon #230

Merged
wojtek merged 15 commits from 160---provide-a-keyboard-shortcut-to-pull-all-release-groups-of-an-artist into main 2024-10-06 15:32:46 +02:00
7 changed files with 45 additions and 96 deletions
Showing only changes of commit f7e215eedf - Show all commits

View File

@ -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, NextPage, PageSettings}, api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient, PageSettings},
http::MusicBrainzHttp, http::MusicBrainzHttp,
}, },
}; };
@ -71,10 +71,10 @@ fn main() {
println!("Release groups in this response: {count}"); println!("Release groups in this response: {count}");
match response.page.next_page_offset(count) { paging = match response.page.next_page(paging, count) {
NextPage::Offset(next_offset) => paging.set_offset(next_offset), Some(paging) => paging,
NextPage::Complete => break, None => break,
} };
thread::sleep(time::Duration::from_secs(1)); thread::sleep(time::Duration::from_secs(1));
} }

View File

@ -6,7 +6,7 @@ use crate::{
collection::musicbrainz::Mbid, collection::musicbrainz::Mbid,
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
ApiDisplay, Error, MbReleaseGroupMeta, MusicBrainzClient, NextPage, PageSettings, ApiDisplay, Error, MbReleaseGroupMeta, MusicBrainzClient, PageSettings,
SerdeMbReleaseGroupMeta, MB_BASE_URL, SerdeMbReleaseGroupMeta, MB_BASE_URL,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
@ -21,8 +21,8 @@ pub struct BrowseReleaseGroupPage {
} }
impl BrowseReleaseGroupPage { impl BrowseReleaseGroupPage {
pub fn next_page_offset(&self, page_count: usize) -> NextPage { pub fn next_page(&self, settings: PageSettings, page_count: usize) -> Option<PageSettings> {
NextPage::next_page_offset( settings.next_page(
self.release_group_offset, self.release_group_offset,
self.release_group_count, self.release_group_count,
page_count, page_count,
@ -125,7 +125,7 @@ mod tests {
release_group_count: 45, release_group_count: 45,
}; };
next_page_test(|val| page.next_page_offset(val)); next_page_test(|val| page.next_page(PageSettings::default(), val));
} }
#[test] #[test]

View File

@ -87,25 +87,13 @@ impl PageSettings {
pub fn set_offset(&mut self, offset: usize) { pub fn set_offset(&mut self, offset: usize) {
self.offset = Some(offset); self.offset = Some(offset);
} }
}
#[derive(Debug, PartialEq, Eq)] pub fn next_page(self, offset: usize, total_count: usize, page_count: usize) -> Option<Self> {
pub enum NextPage {
Offset(usize),
Complete,
}
impl NextPage {
pub fn new() -> NextPage {
NextPage::Offset(0)
}
pub fn next_page_offset(offset: usize, total_count: usize, page_count: usize) -> NextPage {
let next_offset = offset + page_count; let next_offset = offset + page_count;
if next_offset < total_count { if next_offset < total_count {
NextPage::Offset(next_offset) Some(self.with_offset(next_offset))
} else { } else {
NextPage::Complete None
} }
} }
} }
@ -370,21 +358,21 @@ mod tests {
pub fn next_page_test<Fn>(mut f: Fn) pub fn next_page_test<Fn>(mut f: Fn)
where where
Fn: FnMut(usize) -> NextPage, Fn: FnMut(usize) -> Option<PageSettings>,
{ {
let next = f(20); let next = f(20);
assert_eq!(next, NextPage::Offset(25)); assert_eq!(next.unwrap().offset, Some(25));
let next = f(40); let next = f(40);
assert_eq!(next, NextPage::Complete); assert!(next.is_none());
let next = f(100); let next = f(100);
assert_eq!(next, NextPage::Complete); assert!(next.is_none());
} }
#[test] #[test]
fn next_page() { fn next_page() {
next_page_test(|val| NextPage::next_page_offset(5, 45, val)); next_page_test(|val| PageSettings::default().next_page(5, 45, val));
} }
#[test] #[test]

View File

@ -23,8 +23,6 @@ use crate::external::musicbrainz::{
IMusicBrainzHttp, IMusicBrainzHttp,
}; };
use super::NextPage;
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all(deserialize = "kebab-case"))] #[serde(rename_all(deserialize = "kebab-case"))]
pub struct SearchPage { pub struct SearchPage {
@ -33,8 +31,8 @@ pub struct SearchPage {
} }
impl SearchPage { impl SearchPage {
pub fn next_page_offset(&self, page_count: usize) -> NextPage { pub fn next_page(&self, settings: PageSettings, page_count: usize) -> Option<PageSettings> {
NextPage::next_page_offset(self.offset, self.count, page_count) settings.next_page(self.offset, self.count, page_count)
} }
} }
@ -78,6 +76,6 @@ mod tests {
count: 45, count: 45,
}; };
next_page_test(|val| page.next_page_offset(val)); next_page_test(|val| page.next_page(PageSettings::default(), val));
} }
} }

View File

@ -25,7 +25,7 @@ use musichoard::{
}, },
}; };
use crate::tui::lib::interface::musicbrainz::api::{Entity, Error, IMusicBrainz, Paged}; use crate::tui::lib::interface::musicbrainz::api::{Entity, Error, IMusicBrainz};
// GRCOV_EXCL_START // GRCOV_EXCL_START
pub struct MusicBrainz<Http> { pub struct MusicBrainz<Http> {
@ -97,17 +97,17 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn browse_release_group( fn browse_release_group(
&mut self, &mut self,
artist: &Mbid, artist: &Mbid,
paging: &mut PageSettings, paging: &mut Option<PageSettings>,
) -> Result<Paged<Entity<AlbumMeta>>, Error> { ) -> Result<Vec<Entity<AlbumMeta>>, Error> {
let request = BrowseReleaseGroupRequest::artist(artist); let request = BrowseReleaseGroupRequest::artist(artist);
let mb_response = self.client.browse_release_group(&request, paging)?; let page = paging.take().unwrap_or_default();
let mb_response = self.client.browse_release_group(&request, &page)?;
let page_count = mb_response.release_groups.len(); let page_count = mb_response.release_groups.len();
let next = mb_response.page.next_page_offset(page_count); *paging = mb_response.page.next_page(page, page_count);
let items = from_browse_release_group_response(mb_response);
Ok(Paged { items, next }) Ok(from_browse_release_group_response(mb_response))
} }
} }

View File

@ -1,12 +1,12 @@
use std::{collections::VecDeque, sync::mpsc, thread, time}; use std::{collections::VecDeque, sync::mpsc, thread, time};
use musichoard::external::musicbrainz::api::{NextPage, PageSettings}; use musichoard::external::musicbrainz::api::PageSettings;
use crate::tui::{ use crate::tui::{
app::EntityMatches, app::EntityMatches,
event::IFetchCompleteEventSender, event::IFetchCompleteEventSender,
lib::interface::musicbrainz::{ lib::interface::musicbrainz::{
api::{Error as ApiError, IMusicBrainz, Paged}, api::{Error as ApiError, IMusicBrainz},
daemon::{ daemon::{
BrowseParams, EntityList, Error, IMbJobSender, LookupParams, MbParams, MbReturn, BrowseParams, EntityList, Error, IMbJobSender, LookupParams, MbParams, MbReturn,
ResultSender, SearchParams, ResultSender, SearchParams,
@ -228,13 +228,10 @@ impl JobInstance {
event_sender: &mut dyn IFetchCompleteEventSender, event_sender: &mut dyn IFetchCompleteEventSender,
) -> Result<JobInstanceStatus, JobInstanceError> { ) -> Result<JobInstanceStatus, JobInstanceError> {
// self.requests can be empty if the caller submits an empty job. // self.requests can be empty if the caller submits an empty job.
self.paging = match self.requests.front() { if let Some(params) = self.requests.front() {
Some(params) => { let result_sender = &mut self.result_sender;
let result_sender = &mut self.result_sender; let paging = &mut self.paging;
let paging = self.paging.take(); Self::execute(musicbrainz, result_sender, event_sender, &params, paging)?;
Self::execute(musicbrainz, result_sender, event_sender, &params, paging)?
}
None => None,
}; };
if self.paging.is_none() { if self.paging.is_none() {
@ -253,13 +250,11 @@ impl JobInstance {
result_sender: &mut ResultSender, result_sender: &mut ResultSender,
event_sender: &mut dyn IFetchCompleteEventSender, event_sender: &mut dyn IFetchCompleteEventSender,
api_params: &MbParams, api_params: &MbParams,
paging: Option<PageSettings>, paging: &mut Option<PageSettings>,
) -> Result<Option<PageSettings>, JobInstanceError> { ) -> Result<(), JobInstanceError> {
let mut paging = match paging { if paging.is_none() {
Some(paging) => paging, *paging = Some(PageSettings::with_max_limit());
None => PageSettings::with_max_limit(), }
};
let mut next_page = NextPage::Complete;
let result = match api_params { let result = match api_params {
MbParams::Lookup(lookup) => match lookup { MbParams::Lookup(lookup) => match lookup {
@ -285,22 +280,13 @@ impl JobInstance {
} }
.map(MbReturn::Match), .map(MbReturn::Match),
MbParams::Browse(browse) => match browse { MbParams::Browse(browse) => match browse {
BrowseParams::ReleaseGroup(params) => { BrowseParams::ReleaseGroup(params) => musicbrainz
let paged = musicbrainz.browse_release_group(&params.artist, &mut paging); .browse_release_group(&params.artist, paging)
let result = Paged::map_paged_result(paged, |rg| rg.entity, &mut next_page); .map(|rv| EntityList::Album(rv.into_iter().map(|rg| rg.entity).collect())),
result.map(EntityList::Album)
}
} }
.map(MbReturn::Fetch), .map(MbReturn::Fetch),
}; };
Self::return_result(result_sender, event_sender, result)?; Self::return_result(result_sender, event_sender, result)
let next_page_settings = match next_page {
NextPage::Offset(offset) => Some(paging.with_offset(offset)),
NextPage::Complete => None,
};
Ok(next_page_settings)
} }
fn return_result( fn return_result(

View File

@ -1,20 +1,17 @@
//! Module for accessing MusicBrainz metadata. //! Module for accessing MusicBrainz metadata.
use std::mem;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use musichoard::{ use musichoard::{
collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid}, collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid},
external::musicbrainz::api::{NextPage, PageSettings}, external::musicbrainz::api::PageSettings,
}; };
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IMusicBrainz { pub trait IMusicBrainz {
fn lookup_artist(&mut self, mbid: &Mbid) -> Result<Entity<ArtistMeta>, Error>; fn lookup_artist(&mut self, mbid: &Mbid) -> Result<Entity<ArtistMeta>, Error>;
fn lookup_release_group(&mut self, mbid: &Mbid) -> Result<Entity<AlbumMeta>, Error>; fn lookup_release_group(&mut self, mbid: &Mbid) -> Result<Entity<AlbumMeta>, Error>;
// TODO: Also make it out Paged
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Entity<ArtistMeta>>, Error>; fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Entity<ArtistMeta>>, Error>;
fn search_release_group( fn search_release_group(
&mut self, &mut self,
@ -24,8 +21,8 @@ pub trait IMusicBrainz {
fn browse_release_group( fn browse_release_group(
&mut self, &mut self,
artist: &Mbid, artist: &Mbid,
paging: &mut PageSettings, paging: &mut Option<PageSettings>,
) -> Result<Paged<Entity<AlbumMeta>>, Error>; ) -> Result<Vec<Entity<AlbumMeta>>, Error>;
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -45,24 +42,4 @@ impl<T> Entity<T> {
} }
} }
pub struct Paged<T> {
pub items: Vec<T>,
pub next: NextPage,
}
impl<T> Paged<T> {
pub fn map_paged_result<E, U, F: FnMut(T) -> U>(
result: Result<Paged<T>, E>,
op: F,
next: &mut NextPage,
) -> Result<Vec<U>, E> {
match result {
Ok(paged) => {
_ = mem::replace(next, paged.next);
Ok(paged.items.into_iter().map(op).collect())
}
Err(err) => Err(err),
}
}
}
pub type Error = musichoard::external::musicbrainz::api::Error; pub type Error = musichoard::external::musicbrainz::api::Error;