From a980086fe00dcac139a2943157b064781546abb1 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 20 Sep 2024 21:00:43 +0200 Subject: [PATCH] Daemon draft --- .../lib/external/musicbrainz/daemon/mod.rs | 224 ++++++++++++++++++ src/tui/lib/external/musicbrainz/mod.rs | 2 + src/tui/lib/interface/musicbrainz/api/mod.rs | 2 +- 3 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 src/tui/lib/external/musicbrainz/daemon/mod.rs diff --git a/src/tui/lib/external/musicbrainz/daemon/mod.rs b/src/tui/lib/external/musicbrainz/daemon/mod.rs new file mode 100644 index 0000000..a235ac8 --- /dev/null +++ b/src/tui/lib/external/musicbrainz/daemon/mod.rs @@ -0,0 +1,224 @@ +use std::{collections::VecDeque, sync::mpsc, thread, time}; + +use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid}; + +use crate::tui::{ + app::MatchStateInfo, + event::{Event, EventSender}, + lib::interface::musicbrainz::api::{Error as ApiError, IMusicBrainz}, +}; + +pub enum Error { + RequestRecv(String), + Api(String), +} + +impl From for Error { + fn from(value: mpsc::RecvError) -> Self { + Error::RequestRecv(value.to_string()) + } +} + +impl From for Error { + fn from(value: ApiError) -> Self { + Error::Api(value.to_string()) + } +} + +pub struct MusicBrainzDaemon { + api: Box, + request_channel: RequestChannel, + job_queue: JobQueue, + events: EventSender, +} + +struct JobQueue { + foreground_queue: VecDeque, + background_queue: VecDeque, +} + +impl JobQueue { + fn new() -> Self { + JobQueue { + foreground_queue: VecDeque::new(), + background_queue: VecDeque::new(), + } + } + + fn is_empty(&self) -> bool { + self.foreground_queue.is_empty() && self.background_queue.is_empty() + } + + fn front_mut(&mut self) -> Option<&mut JobInstance> { + self.foreground_queue + .front_mut() + .or_else(|| self.background_queue.front_mut()) + } + + fn pop_front(&mut self) -> Option { + self.foreground_queue + .pop_front() + .or_else(|| self.background_queue.pop_front()) + } + + fn push_back(&mut self, job: Job) { + match job.priority { + JobPriority::Foreground => self.foreground_queue.push_back(job.instance), + JobPriority::Background => self.background_queue.push_back(job.instance), + } + } +} + +struct Job { + priority: JobPriority, + instance: JobInstance, +} + +enum JobPriority { + Foreground, + Background, +} + +type ReturnSender = mpsc::Sender>; +struct JobInstance { + return_sender: ReturnSender, + call_queue: VecDeque, +} + +enum ApiParams { + Search(SearchParams), +} + +enum SearchParams { + Artist(SearchArtistParams), + ReleaseGroup(SearchReleaseGroupParams), +} + +struct SearchArtistParams { + artist: ArtistMeta, +} + +struct SearchReleaseGroupParams { + arid: Mbid, + album: AlbumMeta, +} + +struct RequestChannel { + pub receiver: mpsc::Receiver, + pub sender: mpsc::Sender, +} + +impl RequestChannel { + fn new() -> Self { + let (sender, receiver) = mpsc::channel(); + RequestChannel { receiver, sender } + } +} + +impl MusicBrainzDaemon { + pub fn run(api: Box, events: EventSender) { + let daemon = MusicBrainzDaemon { + api, + request_channel: RequestChannel::new(), + job_queue: JobQueue::new(), + events, + }; + daemon.main(); + } + + fn wait_for_jobs(&mut self) -> Result<(), Error> { + if self.job_queue.is_empty() { + self.job_queue + .push_back(self.request_channel.receiver.recv()?); + } + Ok(()) + } + + fn enqueue_all_pending_jobs(&mut self) -> Result<(), Error> { + loop { + match self.request_channel.receiver.try_recv() { + Ok(job) => self.job_queue.push_back(job), + Err(mpsc::TryRecvError::Empty) => return Ok(()), + Err(mpsc::TryRecvError::Disconnected) => { + return Err(Error::RequestRecv( + mpsc::TryRecvError::Disconnected.to_string(), + )) + } + } + } + } + + fn execute_next_job(&mut self) -> Option<()> { + while let Some(instance) = self.job_queue.front_mut() { + match instance.execute_next(&mut self.api, &mut self.events) { + Some(()) => return Some(()), + None => { + self.job_queue.pop_front(); + } + } + } + None + } + + fn main(mut self) -> Result<(), Error> { + loop { + self.wait_for_jobs()?; + self.enqueue_all_pending_jobs()?; + if let Some(()) = self.execute_next_job() { + // Sleep for one second. Required by MB API rate limiting. Assume all other + // processing takes negligible time such that regardless of how much other + // processing there is to be done, this one second sleep is necessary. + thread::sleep(time::Duration::from_secs(1)); + } + } + } +} + +impl JobInstance { + fn execute_next( + &mut self, + api: &mut Box, + events: &mut EventSender, + ) -> Option<()> { + self.call_queue + .pop_front() + .map(|api_params| self.execute_call(api, events, api_params)) + } + + fn execute_call( + &mut self, + api: &mut Box, + events: &mut EventSender, + api_params: ApiParams, + ) { + match api_params { + ApiParams::Search(search) => match search { + SearchParams::Artist(params) => { + let result = api.search_artist(¶ms.artist); + let result = result.map(|list| MatchStateInfo::artist(params.artist, list)); + self.return_result(events, result).ok(); + } + SearchParams::ReleaseGroup(params) => { + let result = api.search_release_group(¶ms.arid, ¶ms.album); + let result = result.map(|list| MatchStateInfo::album(params.album, list)); + self.return_result(events, result).ok(); + } + }, + } + } + + fn return_result( + &mut self, + events: &mut EventSender, + result: Result, + ) -> Result<(), ()> { + // If receiver disconnects just drop the rest. + self.return_sender.send(result).map_err(|_| ())?; + + // If this send fails the event listener is dead. Don't panic as this function runs in a + // detached thread so this might be happening during normal shut down. + events.send(Event::FetchResultReady).map_err(|_| ())?; + + Ok(()) + } +} diff --git a/src/tui/lib/external/musicbrainz/mod.rs b/src/tui/lib/external/musicbrainz/mod.rs index e5fdf85..625f1ac 100644 --- a/src/tui/lib/external/musicbrainz/mod.rs +++ b/src/tui/lib/external/musicbrainz/mod.rs @@ -1 +1,3 @@ pub mod api; +pub mod daemon; + diff --git a/src/tui/lib/interface/musicbrainz/api/mod.rs b/src/tui/lib/interface/musicbrainz/api/mod.rs index 1bb540e..3c674e7 100644 --- a/src/tui/lib/interface/musicbrainz/api/mod.rs +++ b/src/tui/lib/interface/musicbrainz/api/mod.rs @@ -8,7 +8,7 @@ use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz:: /// Trait for interacting with the MusicBrainz API. #[cfg_attr(test, automock)] pub trait IMusicBrainz { - fn search_artist(&mut self, name: &ArtistMeta) -> Result>, Error>; + fn search_artist(&mut self, artist: &ArtistMeta) -> Result>, Error>; fn search_release_group( &mut self, arid: &Mbid,