Daemonize the musicbrainz thread #217

Merged
wojtek merged 15 commits from 188---add-option-for-manual-input-during-fetch into main 2024-09-21 23:03:47 +02:00
3 changed files with 227 additions and 1 deletions
Showing only changes of commit a980086fe0 - Show all commits

View File

@ -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<mpsc::RecvError> for Error {
fn from(value: mpsc::RecvError) -> Self {
Error::RequestRecv(value.to_string())
}
}
impl From<ApiError> for Error {
fn from(value: ApiError) -> Self {
Error::Api(value.to_string())
}
}
pub struct MusicBrainzDaemon {
api: Box<dyn IMusicBrainz>,
request_channel: RequestChannel,
job_queue: JobQueue,
events: EventSender,
}
struct JobQueue {
foreground_queue: VecDeque<JobInstance>,
background_queue: VecDeque<JobInstance>,
}
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<JobInstance> {
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<Result<MatchStateInfo, ApiError>>;
struct JobInstance {
return_sender: ReturnSender,
call_queue: VecDeque<ApiParams>,
}
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<Job>,
pub sender: mpsc::Sender<Job>,
}
impl RequestChannel {
fn new() -> Self {
let (sender, receiver) = mpsc::channel();
RequestChannel { receiver, sender }
}
}
impl MusicBrainzDaemon {
pub fn run(api: Box<dyn IMusicBrainz>, 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<dyn IMusicBrainz>,
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<dyn IMusicBrainz>,
events: &mut EventSender,
api_params: ApiParams,
) {
match api_params {
ApiParams::Search(search) => match search {
SearchParams::Artist(params) => {
let result = api.search_artist(&params.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(&params.arid, &params.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<MatchStateInfo, ApiError>,
) -> 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(())
}
}

View File

@ -1 +1,3 @@
pub mod api;
pub mod daemon;

View File

@ -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<Vec<Match<ArtistMeta>>, Error>;
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
fn search_release_group(
&mut self,
arid: &Mbid,