Compare commits
4 Commits
21d0034b97
...
a67e6b937f
Author | SHA1 | Date | |
---|---|---|---|
a67e6b937f | |||
f4745b5d68 | |||
7d99219f44 | |||
993de9a38b |
@ -26,7 +26,7 @@ use musichoard::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use tui::{
|
use tui::{
|
||||||
App, EventChannel, EventHandler, EventListener, MusicBrainz, MusicBrainzDaemon, RequestChannel,
|
App, EventChannel, EventHandler, EventListener, JobChannel, MusicBrainz, MusicBrainzDaemon,
|
||||||
Tui, Ui,
|
Tui, Ui,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,13 +94,13 @@ fn with<Database: IDatabase + 'static, Library: ILibrary + 'static>(
|
|||||||
let listener = EventListener::new(listener_sender);
|
let listener = EventListener::new(listener_sender);
|
||||||
let handler = EventHandler::new(channel.receiver());
|
let handler = EventHandler::new(channel.receiver());
|
||||||
|
|
||||||
let mb_request_channel = RequestChannel::new();
|
let mb_job_channel = JobChannel::new();
|
||||||
thread::spawn(|| MusicBrainzDaemon::run(musicbrainz, mb_request_channel.receiver, app_sender));
|
|
||||||
|
|
||||||
let app = App::new(music_hoard, mb_request_channel.sender);
|
let app = App::new(music_hoard, mb_job_channel.sender());
|
||||||
let ui = Ui;
|
let ui = Ui;
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
|
thread::spawn(|| MusicBrainzDaemon::run(musicbrainz, mb_job_channel.receiver(), app_sender));
|
||||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
use std::{collections::VecDeque, sync::mpsc::{self, TryRecvError}};
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
sync::mpsc::{self, TryRecvError},
|
||||||
|
};
|
||||||
|
|
||||||
use musichoard::collection::{artist::Artist, musicbrainz::IMusicBrainzRef};
|
use musichoard::collection::{artist::Artist, musicbrainz::IMusicBrainzRef};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::{match_state::MatchState, App, AppInner, AppMachine},
|
machine::{match_state::MatchState, App, AppInner, AppMachine},
|
||||||
AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo,
|
AppPublicState, AppState, IAppEventFetch, IAppInteractFetch,
|
||||||
},
|
},
|
||||||
lib::{
|
lib::external::musicbrainz::daemon::{
|
||||||
external::musicbrainz::daemon::{
|
Error as DaemonError, IMbJobSender, MbApiResult, MbParams, ResultSender,
|
||||||
ApiParams, Job, JobInstance, JobPriority, ReturnSender, SearchArtistParams,
|
|
||||||
SearchParams, SearchReleaseGroupParams,
|
|
||||||
},
|
|
||||||
interface::musicbrainz::api::Error as MbError,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -20,16 +19,13 @@ pub struct FetchState {
|
|||||||
fetch_rx: FetchReceiver,
|
fetch_rx: FetchReceiver,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type FetchReceiver = mpsc::Receiver<MbApiResult>;
|
||||||
impl FetchState {
|
impl FetchState {
|
||||||
pub fn new(fetch_rx: FetchReceiver) -> Self {
|
pub fn new(fetch_rx: FetchReceiver) -> Self {
|
||||||
FetchState { fetch_rx }
|
FetchState { fetch_rx }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FetchError = MbError;
|
|
||||||
pub type FetchResult = Result<MatchStateInfo, FetchError>;
|
|
||||||
pub type FetchReceiver = mpsc::Receiver<FetchResult>;
|
|
||||||
|
|
||||||
impl AppMachine<FetchState> {
|
impl AppMachine<FetchState> {
|
||||||
fn fetch_state(inner: AppInner, state: FetchState) -> Self {
|
fn fetch_state(inner: AppInner, state: FetchState) -> Self {
|
||||||
AppMachine::new(inner, state)
|
AppMachine::new(inner, state)
|
||||||
@ -44,8 +40,10 @@ impl AppMachine<FetchState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (fetch_tx, fetch_rx) = mpsc::channel::<FetchResult>();
|
let (fetch_tx, fetch_rx) = mpsc::channel::<MbApiResult>();
|
||||||
Self::submit_fetch_job(&inner, artist, fetch_tx);
|
if let Err(err) = Self::submit_fetch_job(&inner.musicbrainz, fetch_tx, artist) {
|
||||||
|
return AppMachine::error_state(inner, err.to_string()).into();
|
||||||
|
}
|
||||||
|
|
||||||
let fetch = FetchState::new(fetch_rx);
|
let fetch = FetchState::new(fetch_rx);
|
||||||
AppMachine::app_fetch(inner, fetch, true)
|
AppMachine::app_fetch(inner, fetch, true)
|
||||||
@ -79,25 +77,22 @@ impl AppMachine<FetchState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn submit_fetch_job(inner: &AppInner, artist: &Artist, tx: ReturnSender) {
|
fn submit_fetch_job(
|
||||||
let mut queue = VecDeque::new();
|
musicbrainz: &Box<dyn IMbJobSender>,
|
||||||
match artist.meta.musicbrainz {
|
result_sender: ResultSender,
|
||||||
|
artist: &Artist,
|
||||||
|
) -> Result<(), DaemonError> {
|
||||||
|
let requests = match artist.meta.musicbrainz {
|
||||||
Some(ref arid) => {
|
Some(ref arid) => {
|
||||||
let arid = arid.mbid().clone();
|
let arid = arid.mbid();
|
||||||
for album in artist.albums.iter() {
|
let albums = artist.albums.iter();
|
||||||
queue.push_back(ApiParams::search(SearchParams::release_group(
|
albums
|
||||||
SearchReleaseGroupParams::new(arid.clone(), album.meta.clone()),
|
.map(|album| MbParams::search_release_group(arid.clone(), album.meta.clone()))
|
||||||
)));
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
None => VecDeque::from([MbParams::search_artist(artist.meta.clone())]),
|
||||||
None => {
|
};
|
||||||
queue.push_back(ApiParams::search(SearchParams::artist(
|
musicbrainz.submit_background_job(result_sender, requests)
|
||||||
SearchArtistParams::new(artist.meta.clone()),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let job = Job::new(JobPriority::Background, JobInstance::new(tx, queue));
|
|
||||||
inner.musicbrainz.send(job);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,14 +8,12 @@ mod match_state;
|
|||||||
mod reload_state;
|
mod reload_state;
|
||||||
mod search_state;
|
mod search_state;
|
||||||
|
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
selection::Selection, AppMode, AppPublic, AppPublicInner, AppPublicState, AppState, IApp,
|
selection::Selection, AppMode, AppPublic, AppPublicInner, AppPublicState, AppState, IApp,
|
||||||
IAppAccess, IAppBase, IAppState,
|
IAppAccess, IAppBase, IAppState,
|
||||||
},
|
},
|
||||||
lib::{external::musicbrainz::daemon::Job, IMusicHoard},
|
lib::{external::musicbrainz::daemon::IMbJobSender, IMusicHoard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use browse_state::BrowseState;
|
use browse_state::BrowseState;
|
||||||
@ -48,7 +46,7 @@ pub struct AppMachine<STATE> {
|
|||||||
pub struct AppInner {
|
pub struct AppInner {
|
||||||
running: bool,
|
running: bool,
|
||||||
music_hoard: Box<dyn IMusicHoard>,
|
music_hoard: Box<dyn IMusicHoard>,
|
||||||
musicbrainz: mpsc::Sender<Job>,
|
musicbrainz: Box<dyn IMbJobSender>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +81,9 @@ macro_rules! app_field_mut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new<MH: IMusicHoard + 'static>(
|
pub fn new<MH: IMusicHoard + 'static, MB: IMbJobSender + 'static>(
|
||||||
mut music_hoard: MH,
|
mut music_hoard: MH,
|
||||||
musicbrainz: mpsc::Sender<Job>,
|
musicbrainz: MB,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let init_result = Self::init(&mut music_hoard);
|
let init_result = Self::init(&mut music_hoard);
|
||||||
let inner = AppInner::new(music_hoard, musicbrainz);
|
let inner = AppInner::new(music_hoard, musicbrainz);
|
||||||
@ -171,12 +169,15 @@ impl IAppAccess for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppInner {
|
impl AppInner {
|
||||||
pub fn new<MH: IMusicHoard + 'static>(music_hoard: MH, musicbrainz: mpsc::Sender<Job>) -> Self {
|
pub fn new<MH: IMusicHoard + 'static, MB: IMbJobSender + 'static>(
|
||||||
|
music_hoard: MH,
|
||||||
|
musicbrainz: MB,
|
||||||
|
) -> Self {
|
||||||
let selection = Selection::new(music_hoard.get_collection());
|
let selection = Selection::new(music_hoard.get_collection());
|
||||||
AppInner {
|
AppInner {
|
||||||
running: true,
|
running: true,
|
||||||
music_hoard: Box::new(music_hoard),
|
music_hoard: Box::new(music_hoard),
|
||||||
musicbrainz,
|
musicbrainz: Box::new(musicbrainz),
|
||||||
selection,
|
selection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ impl From<mpsc::TryRecvError> for EventError {
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Key(KeyEvent),
|
Key(KeyEvent),
|
||||||
FetchResultReady,
|
FetchComplete,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventChannel {
|
pub struct EventChannel {
|
||||||
@ -50,6 +50,14 @@ pub struct EventChannel {
|
|||||||
receiver: mpsc::Receiver<Event>,
|
receiver: mpsc::Receiver<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait IKeyEventSender {
|
||||||
|
fn send_key(&self, key_event: KeyEvent) -> Result<(), EventError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IFetchCompleteEventSender {
|
||||||
|
fn send_fetch_complete(&self) -> Result<(), EventError>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EventSender {
|
pub struct EventSender {
|
||||||
sender: mpsc::Sender<Event>,
|
sender: mpsc::Sender<Event>,
|
||||||
@ -78,9 +86,15 @@ impl EventChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventSender {
|
impl IKeyEventSender for EventSender {
|
||||||
pub fn send(&self, event: Event) -> Result<(), EventError> {
|
fn send_key(&self, key_event: KeyEvent) -> Result<(), EventError> {
|
||||||
Ok(self.sender.send(event)?)
|
Ok(self.sender.send(Event::Key(key_event))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IFetchCompleteEventSender for EventSender {
|
||||||
|
fn send_fetch_complete(&self) -> Result<(), EventError> {
|
||||||
|
Ok(self.sender.send(Event::FetchComplete)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,13 +122,13 @@ mod tests {
|
|||||||
let channel = EventChannel::new();
|
let channel = EventChannel::new();
|
||||||
let sender = channel.sender();
|
let sender = channel.sender();
|
||||||
let receiver = channel.receiver();
|
let receiver = channel.receiver();
|
||||||
let event = Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::empty()));
|
let key_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
|
||||||
|
|
||||||
let result = sender.send(event);
|
let result = sender.send_key(key_event);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
drop(receiver);
|
drop(receiver);
|
||||||
let result = sender.send(event);
|
let result = sender.send_key(key_event);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,9 +137,9 @@ mod tests {
|
|||||||
let channel = EventChannel::new();
|
let channel = EventChannel::new();
|
||||||
let sender = channel.sender();
|
let sender = channel.sender();
|
||||||
let receiver = channel.receiver();
|
let receiver = channel.receiver();
|
||||||
let event = Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::empty()));
|
let key_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
|
||||||
|
|
||||||
sender.send(event).unwrap();
|
sender.send_key(key_event).unwrap();
|
||||||
let result = receiver.recv();
|
let result = receiver.recv();
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
@ -139,12 +153,11 @@ mod tests {
|
|||||||
let channel = EventChannel::new();
|
let channel = EventChannel::new();
|
||||||
let sender = channel.sender();
|
let sender = channel.sender();
|
||||||
let receiver = channel.receiver();
|
let receiver = channel.receiver();
|
||||||
let event = Event::FetchResultReady;
|
|
||||||
|
|
||||||
let result = receiver.try_recv();
|
let result = receiver.try_recv();
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
sender.send(event).unwrap();
|
sender.send_fetch_complete().unwrap();
|
||||||
let result = receiver.try_recv();
|
let result = receiver.try_recv();
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ trait IEventHandlerPrivate<APP: IApp> {
|
|||||||
fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP;
|
fn handle_error_key_event(app: <APP as IApp>::ErrorState, key_event: KeyEvent) -> APP;
|
||||||
fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP;
|
fn handle_critical_key_event(app: <APP as IApp>::CriticalState, key_event: KeyEvent) -> APP;
|
||||||
|
|
||||||
fn handle_fetch_result_ready_event(app: APP) -> APP;
|
fn handle_fetch_complete_event(app: APP) -> APP;
|
||||||
|
|
||||||
fn handle_input_key_event<Input: IAppInput<APP = APP>>(app: Input, key_event: KeyEvent) -> APP;
|
fn handle_input_key_event<Input: IAppInput<APP = APP>>(app: Input, key_event: KeyEvent) -> APP;
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ impl<APP: IApp> IEventHandler<APP> for EventHandler {
|
|||||||
fn handle_next_event(&self, app: APP) -> Result<APP, EventError> {
|
fn handle_next_event(&self, app: APP) -> Result<APP, EventError> {
|
||||||
Ok(match self.events.recv()? {
|
Ok(match self.events.recv()? {
|
||||||
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
||||||
Event::FetchResultReady => Self::handle_fetch_result_ready_event(app),
|
Event::FetchComplete => Self::handle_fetch_complete_event(app),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_fetch_result_ready_event(app: APP) -> APP {
|
fn handle_fetch_complete_event(app: APP) -> APP {
|
||||||
match app.state() {
|
match app.state() {
|
||||||
AppState::Browse(state) => state.no_op(),
|
AppState::Browse(state) => state.no_op(),
|
||||||
AppState::Info(state) => state.no_op(),
|
AppState::Info(state) => state.no_op(),
|
||||||
|
516
src/tui/lib/external/musicbrainz/daemon/mod.rs
vendored
516
src/tui/lib/external/musicbrainz/daemon/mod.rs
vendored
@ -1,35 +1,37 @@
|
|||||||
use std::{collections::VecDeque, sync::mpsc, thread, time};
|
use std::{collections::VecDeque, fmt, sync::mpsc, thread, time};
|
||||||
|
|
||||||
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
|
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::MatchStateInfo,
|
app::MatchStateInfo,
|
||||||
event::{Event, EventSender},
|
event::IFetchCompleteEventSender,
|
||||||
lib::interface::musicbrainz::api::{Error as ApiError, IMusicBrainz},
|
lib::interface::musicbrainz::api::{Error as ApiError, IMusicBrainz},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
RequestRecv(String),
|
EventChannelDisconnected,
|
||||||
Api(String),
|
JobChannelDisconnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<mpsc::RecvError> for Error {
|
impl fmt::Display for Error {
|
||||||
fn from(value: mpsc::RecvError) -> Self {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
Error::RequestRecv(value.to_string())
|
match self {
|
||||||
|
Error::EventChannelDisconnected => write!(f, "the event channel is disconnected"),
|
||||||
|
Error::JobChannelDisconnected => write!(f, "the job channel is disconnected"),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ApiError> for Error {
|
|
||||||
fn from(value: ApiError) -> Self {
|
|
||||||
Error::Api(value.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MusicBrainzDaemon {
|
pub struct MusicBrainzDaemon {
|
||||||
api: Box<dyn IMusicBrainz>,
|
musicbrainz: Box<dyn IMusicBrainz>,
|
||||||
request_receiver: mpsc::Receiver<Job>,
|
job_receiver: mpsc::Receiver<Job>,
|
||||||
job_queue: JobQueue,
|
job_queue: JobQueue,
|
||||||
events: EventSender,
|
event_sender: Box<dyn IFetchCompleteEventSender>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JobError {
|
||||||
|
JobQueueEmpty,
|
||||||
|
EventChannelDisconnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct JobQueue {
|
struct JobQueue {
|
||||||
@ -37,6 +39,286 @@ struct JobQueue {
|
|||||||
background_queue: VecDeque<JobInstance>,
|
background_queue: VecDeque<JobInstance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Job {
|
||||||
|
priority: JobPriority,
|
||||||
|
instance: JobInstance,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Job {
|
||||||
|
pub fn new(priority: JobPriority, instance: JobInstance) -> Self {
|
||||||
|
Job { priority, instance }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JobPriority {
|
||||||
|
Foreground,
|
||||||
|
Background,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MbApiResult = Result<MatchStateInfo, ApiError>;
|
||||||
|
pub type ResultSender = mpsc::Sender<MbApiResult>;
|
||||||
|
struct JobInstance {
|
||||||
|
result_sender: ResultSender,
|
||||||
|
requests: VecDeque<MbParams>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JobInstanceStatus {
|
||||||
|
Continue,
|
||||||
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JobInstanceError {
|
||||||
|
ReturnChannelDisconnected,
|
||||||
|
EventChannelDisconnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobInstance {
|
||||||
|
fn new(result_sender: ResultSender, requests: VecDeque<MbParams>) -> Self {
|
||||||
|
JobInstance {
|
||||||
|
result_sender,
|
||||||
|
requests,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MbParams {
|
||||||
|
Search(SearchParams),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MbParams {
|
||||||
|
pub fn search_artist(artist: ArtistMeta) -> Self {
|
||||||
|
MbParams::Search(SearchParams::Artist(SearchArtistParams { artist }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_release_group(arid: Mbid, album: AlbumMeta) -> Self {
|
||||||
|
MbParams::Search(SearchParams::ReleaseGroup(SearchReleaseGroupParams {
|
||||||
|
arid,
|
||||||
|
album,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SearchParams {
|
||||||
|
Artist(SearchArtistParams),
|
||||||
|
ReleaseGroup(SearchReleaseGroupParams),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SearchArtistParams {
|
||||||
|
artist: ArtistMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SearchReleaseGroupParams {
|
||||||
|
arid: Mbid,
|
||||||
|
album: AlbumMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JobChannel {
|
||||||
|
sender: mpsc::Sender<Job>,
|
||||||
|
receiver: mpsc::Receiver<Job>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JobSender {
|
||||||
|
sender: mpsc::Sender<Job>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JobReceiver {
|
||||||
|
receiver: mpsc::Receiver<Job>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobChannel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
JobChannel { receiver, sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sender(&self) -> JobSender {
|
||||||
|
JobSender {
|
||||||
|
sender: self.sender.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receiver(self) -> JobReceiver {
|
||||||
|
JobReceiver {
|
||||||
|
receiver: self.receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IMbJobSender {
|
||||||
|
fn submit_background_job(
|
||||||
|
&self,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
requests: VecDeque<MbParams>,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IMbJobSender for JobSender {
|
||||||
|
fn submit_background_job(
|
||||||
|
&self,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
requests: VecDeque<MbParams>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.send_background_job(result_sender, requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobSender {
|
||||||
|
fn send_background_job(
|
||||||
|
&self,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
requests: VecDeque<MbParams>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.send_job(JobPriority::Background, result_sender, requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_job(
|
||||||
|
&self,
|
||||||
|
priority: JobPriority,
|
||||||
|
result_sender: ResultSender,
|
||||||
|
requests: VecDeque<MbParams>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let instance = JobInstance::new(result_sender, requests);
|
||||||
|
let job = Job::new(priority, instance);
|
||||||
|
self.sender.send(job).or(Err(Error::JobChannelDisconnected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicBrainzDaemon {
|
||||||
|
pub fn run<MB: IMusicBrainz + 'static, ES: IFetchCompleteEventSender + 'static>(
|
||||||
|
musicbrainz: MB,
|
||||||
|
job_receiver: JobReceiver,
|
||||||
|
event_sender: ES,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let daemon = MusicBrainzDaemon {
|
||||||
|
musicbrainz: Box::new(musicbrainz),
|
||||||
|
job_receiver: job_receiver.receiver,
|
||||||
|
job_queue: JobQueue::new(),
|
||||||
|
event_sender: Box::new(event_sender),
|
||||||
|
};
|
||||||
|
daemon.main()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(mut self) -> Result<(), Error> {
|
||||||
|
loop {
|
||||||
|
self.enqueue_all_pending_jobs()?;
|
||||||
|
match self.execute_next_job() {
|
||||||
|
Ok(()) => {
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
Err(JobError::JobQueueEmpty) => {
|
||||||
|
self.wait_for_jobs()?;
|
||||||
|
}
|
||||||
|
Err(JobError::EventChannelDisconnected) => {
|
||||||
|
return Err(Error::EventChannelDisconnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enqueue_all_pending_jobs(&mut self) -> Result<(), Error> {
|
||||||
|
loop {
|
||||||
|
match self.job_receiver.try_recv() {
|
||||||
|
Ok(job) => self.job_queue.push_back(job),
|
||||||
|
Err(mpsc::TryRecvError::Empty) => return Ok(()),
|
||||||
|
Err(mpsc::TryRecvError::Disconnected) => {
|
||||||
|
return Err(Error::JobChannelDisconnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_next_job(&mut self) -> Result<(), JobError> {
|
||||||
|
if let Some(instance) = self.job_queue.front_mut() {
|
||||||
|
let result = instance.execute_next(&mut self.musicbrainz, &mut self.event_sender);
|
||||||
|
match result {
|
||||||
|
Ok(JobInstanceStatus::Continue) => {}
|
||||||
|
Ok(JobInstanceStatus::Complete)
|
||||||
|
| Err(JobInstanceError::ReturnChannelDisconnected) => {
|
||||||
|
self.job_queue.pop_front();
|
||||||
|
}
|
||||||
|
Err(JobInstanceError::EventChannelDisconnected) => {
|
||||||
|
return Err(JobError::EventChannelDisconnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(JobError::JobQueueEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_jobs(&mut self) -> Result<(), Error> {
|
||||||
|
assert!(self.job_queue.is_empty());
|
||||||
|
|
||||||
|
match self.job_receiver.recv() {
|
||||||
|
Ok(job) => self.job_queue.push_back(job),
|
||||||
|
Err(mpsc::RecvError) => return Err(Error::JobChannelDisconnected),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobInstance {
|
||||||
|
fn execute_next(
|
||||||
|
&mut self,
|
||||||
|
musicbrainz: &mut Box<dyn IMusicBrainz>,
|
||||||
|
event_sender: &mut Box<dyn IFetchCompleteEventSender>,
|
||||||
|
) -> Result<JobInstanceStatus, JobInstanceError> {
|
||||||
|
// self.requests can be empty if the caller submits an empty job.
|
||||||
|
if let Some(params) = self.requests.pop_front() {
|
||||||
|
self.execute(musicbrainz, event_sender, params)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.requests.is_empty() {
|
||||||
|
Ok(JobInstanceStatus::Complete)
|
||||||
|
} else {
|
||||||
|
Ok(JobInstanceStatus::Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
musicbrainz: &mut Box<dyn IMusicBrainz>,
|
||||||
|
event_sender: &mut Box<dyn IFetchCompleteEventSender>,
|
||||||
|
api_params: MbParams,
|
||||||
|
) -> Result<(), JobInstanceError> {
|
||||||
|
match api_params {
|
||||||
|
MbParams::Search(search) => match search {
|
||||||
|
SearchParams::Artist(params) => {
|
||||||
|
let result = musicbrainz.search_artist(¶ms.artist);
|
||||||
|
let result = result.map(|list| MatchStateInfo::artist(params.artist, list));
|
||||||
|
self.return_result(event_sender, result)
|
||||||
|
}
|
||||||
|
SearchParams::ReleaseGroup(params) => {
|
||||||
|
let result = musicbrainz.search_release_group(¶ms.arid, ¶ms.album);
|
||||||
|
let result = result.map(|list| MatchStateInfo::album(params.album, list));
|
||||||
|
self.return_result(event_sender, result)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn return_result(
|
||||||
|
&mut self,
|
||||||
|
event_sender: &mut Box<dyn IFetchCompleteEventSender>,
|
||||||
|
result: Result<MatchStateInfo, ApiError>,
|
||||||
|
) -> Result<(), JobInstanceError> {
|
||||||
|
self.result_sender
|
||||||
|
.send(result)
|
||||||
|
.map_err(|_| JobInstanceError::ReturnChannelDisconnected)?;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
event_sender
|
||||||
|
.send_fetch_complete()
|
||||||
|
.map_err(|_| JobInstanceError::EventChannelDisconnected)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl JobQueue {
|
impl JobQueue {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
JobQueue {
|
JobQueue {
|
||||||
@ -68,207 +350,3 @@ impl JobQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Job {
|
|
||||||
priority: JobPriority,
|
|
||||||
instance: JobInstance,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Job {
|
|
||||||
pub fn new(priority: JobPriority, instance: JobInstance) -> Self {
|
|
||||||
Job { priority, instance }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum JobPriority {
|
|
||||||
Foreground,
|
|
||||||
Background,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type ReturnSender = mpsc::Sender<Result<MatchStateInfo, ApiError>>;
|
|
||||||
pub struct JobInstance {
|
|
||||||
return_sender: ReturnSender,
|
|
||||||
call_queue: VecDeque<ApiParams>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JobInstance {
|
|
||||||
pub fn new(return_sender: ReturnSender, call_queue: VecDeque<ApiParams>) -> Self {
|
|
||||||
JobInstance {
|
|
||||||
return_sender,
|
|
||||||
call_queue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ApiParams {
|
|
||||||
Search(SearchParams),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiParams {
|
|
||||||
pub fn search(params: SearchParams) -> Self {
|
|
||||||
ApiParams::Search(params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum SearchParams {
|
|
||||||
Artist(SearchArtistParams),
|
|
||||||
ReleaseGroup(SearchReleaseGroupParams),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchParams {
|
|
||||||
pub fn artist(params: SearchArtistParams) -> Self {
|
|
||||||
SearchParams::Artist(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn release_group(params: SearchReleaseGroupParams) -> Self {
|
|
||||||
SearchParams::ReleaseGroup(params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SearchArtistParams {
|
|
||||||
artist: ArtistMeta,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchArtistParams {
|
|
||||||
pub fn new(artist: ArtistMeta) -> Self {
|
|
||||||
SearchArtistParams { artist }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SearchReleaseGroupParams {
|
|
||||||
arid: Mbid,
|
|
||||||
album: AlbumMeta,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchReleaseGroupParams {
|
|
||||||
pub fn new(arid: Mbid, album: AlbumMeta) -> Self {
|
|
||||||
SearchReleaseGroupParams { arid, album }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RequestChannel {
|
|
||||||
pub receiver: mpsc::Receiver<Job>,
|
|
||||||
pub sender: mpsc::Sender<Job>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestChannel {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
|
||||||
RequestChannel { receiver, sender }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MusicBrainzDaemon {
|
|
||||||
pub fn run<MB: IMusicBrainz + 'static>(
|
|
||||||
api: MB,
|
|
||||||
request_receiver: mpsc::Receiver<Job>,
|
|
||||||
events: EventSender,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let daemon = MusicBrainzDaemon {
|
|
||||||
api: Box::new(api),
|
|
||||||
request_receiver,
|
|
||||||
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_receiver.recv()?);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enqueue_all_pending_jobs(&mut self) -> Result<(), Error> {
|
|
||||||
loop {
|
|
||||||
match self.request_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<()> {
|
|
||||||
if let Some(instance) = self.job_queue.front_mut() {
|
|
||||||
let result = instance.execute_next(&mut self.api, &mut self.events);
|
|
||||||
if result.is_none() {
|
|
||||||
self.job_queue.pop_front();
|
|
||||||
}
|
|
||||||
return Some(());
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
if !self.call_queue.is_empty() {
|
|
||||||
Some(())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(¶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<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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,7 +4,7 @@ use std::thread;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::tui::event::{Event, EventError, EventSender};
|
use crate::tui::event::{EventError, IKeyEventSender};
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait IEventListener {
|
pub trait IEventListener {
|
||||||
@ -12,13 +12,15 @@ pub trait IEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventListener {
|
pub struct EventListener {
|
||||||
events: EventSender,
|
event_sender: Box<dyn IKeyEventSender + Send>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
// GRCOV_EXCL_START
|
||||||
impl EventListener {
|
impl EventListener {
|
||||||
pub fn new(events: EventSender) -> Self {
|
pub fn new<ES: IKeyEventSender + Send + 'static>(event_sender: ES) -> Self {
|
||||||
EventListener { events }
|
EventListener {
|
||||||
|
event_sender: Box::new(event_sender),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ impl IEventListener for EventListener {
|
|||||||
match event::read() {
|
match event::read() {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
if let Err(err) = match event {
|
if let Err(err) = match event {
|
||||||
CrosstermEvent::Key(e) => self.events.send(Event::Key(e)),
|
CrosstermEvent::Key(e) => self.event_sender.send_key(e),
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
} {
|
} {
|
||||||
return err;
|
return err;
|
||||||
|
@ -10,7 +10,7 @@ pub use event::EventChannel;
|
|||||||
pub use handler::EventHandler;
|
pub use handler::EventHandler;
|
||||||
pub use lib::external::musicbrainz::{
|
pub use lib::external::musicbrainz::{
|
||||||
api::MusicBrainz,
|
api::MusicBrainz,
|
||||||
daemon::{MusicBrainzDaemon, RequestChannel},
|
daemon::{JobChannel, MusicBrainzDaemon},
|
||||||
};
|
};
|
||||||
pub use listener::EventListener;
|
pub use listener::EventListener;
|
||||||
pub use ui::Ui;
|
pub use ui::Ui;
|
||||||
|
Loading…
Reference in New Issue
Block a user