Compare commits

...

3 Commits

Author SHA1 Message Date
1578a1d1f8 Some clean up
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 1m57s
Cargo CI / Lint (pull_request) Failing after 1m2s
2024-08-26 19:34:53 +02:00
e9ca6ccdfe HTTP client error handling 2024-08-26 17:27:58 +02:00
3c29fda119 Move user agent to better place 2024-08-26 17:15:41 +02:00
6 changed files with 110 additions and 70 deletions

View File

@ -2,6 +2,8 @@
pub mod client;
use std::fmt;
use serde::{de::DeserializeOwned, Deserialize};
use url::form_urlencoded;
@ -42,6 +44,15 @@ impl From<ClientError> for Error {
}
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ClientError::Client(s) => write!(f, "the API client failed: {s}"),
ClientError::Status(u) => write!(f, "the API client failed with status: {u}"),
}
}
}
pub struct MusicBrainzApi<Mbc> {
client: Mbc,
}

View File

@ -8,11 +8,12 @@ use musichoard::{
use crate::tui::{
app::{
machine::{matches::AppMatchInfo, App, AppInner, AppMachine},
machine::{matches::AppMatchesInfo, App, AppInner, AppMachine},
selection::{Delta, ListSelection},
AppPublic, AppState, IAppInteractBrowse,
},
lib::IMusicHoard,
MUSICHOARD_TUI_HTTP_USER_AGENT,
};
pub struct AppBrowse;
@ -90,13 +91,13 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
}
fn fetch_musicbrainz(self) -> Self::APP {
const USER_AGENT: &str = concat!(
"MusicHoard/",
env!("CARGO_PKG_VERSION"),
" ( musichoard@thenineworlds.net )"
);
let client = MusicBrainzApiClient::new(USER_AGENT).expect("failed to create API client");
let client = match MusicBrainzApiClient::new(MUSICHOARD_TUI_HTTP_USER_AGENT) {
Ok(client) => client,
Err(err) => {
return AppMachine::error(self.inner, format!("cannot fetch: {}", err.to_string()))
.into()
}
};
let mut api = MusicBrainzApi::new(client);
let coll = self.inner.music_hoard.get_collection();
@ -114,10 +115,6 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
}
};
if artist.albums.is_empty() {
return AppMachine::error(self.inner, "cannot fetch: this artist has no albums").into();
}
let mut artist_album_matches = vec![];
let mut album_iter = artist.albums.iter().peekable();
while let Some(album) = album_iter.next() {
@ -126,7 +123,7 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
}
match api.search_release_group(arid, album) {
Ok(matches) => artist_album_matches.push(AppMatchInfo {
Ok(matches) => artist_album_matches.push(AppMatchesInfo {
matching: album.clone(),
matches,
}),

View File

@ -10,31 +10,33 @@ use crate::tui::{
lib::IMusicHoard,
};
pub struct AppMatchInfo {
pub struct AppMatchesInfo {
pub matching: Album,
pub matches: Vec<Match<Album>>,
}
pub struct AppMatches {
matches: Vec<AppMatchInfo>,
index: usize,
matches_info_vec: Vec<AppMatchesInfo>,
index: Option<usize>,
state: WidgetState,
}
impl<MH: IMusicHoard> AppMachine<MH, AppMatches> {
pub fn matches(inner: AppInner<MH>, matches: Vec<AppMatchInfo>) -> Self {
assert!(!matches.is_empty());
pub fn matches(inner: AppInner<MH>, matches_info_vec: Vec<AppMatchesInfo>) -> Self {
let mut index = None;
let mut state = WidgetState::default();
if !matches[0].matches.is_empty() {
state.list.select(Some(0));
if let Some(matches_info) = matches_info_vec.first() {
index = Some(0);
if !matches_info.matches.is_empty() {
state.list.select(Some(0));
}
}
AppMachine {
inner,
state: AppMatches {
matches,
index: 0,
matches_info_vec,
index,
state,
},
}
@ -49,11 +51,19 @@ impl<MH: IMusicHoard> From<AppMachine<MH, AppMatches>> for App<MH> {
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppMatches>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppMatches>) -> Self {
let (matching, matches) = match machine.state.index {
Some(index) => (
Some(&machine.state.matches_info_vec[index].matching),
Some(machine.state.matches_info_vec[index].matches.as_slice()),
),
None => (None, None),
};
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Matches(AppPublicMatches {
matching: &machine.state.matches[machine.state.index].matching,
matches: &machine.state.matches[machine.state.index].matches,
matching,
matches,
state: &mut machine.state.state,
}),
}
@ -64,8 +74,8 @@ impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
type APP = App<MH>;
fn prev_match(mut self) -> Self::APP {
if let Some(index) = self.state.state.list.selected() {
let result = index.saturating_sub(1);
if let Some(list_index) = self.state.state.list.selected() {
let result = list_index.saturating_sub(1);
self.state.state.list.select(Some(result));
}
@ -73,11 +83,14 @@ impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
}
fn next_match(mut self) -> Self::APP {
if let Some(index) = self.state.state.list.selected() {
let result = index.saturating_add(1);
if let Some(list_index) = self.state.state.list.selected() {
let result = list_index.saturating_add(1);
let to = cmp::min(
result,
self.state.matches[self.state.index].matches.len() - 1,
self.state.matches_info_vec[self.state.index.unwrap()]
.matches
.len()
.saturating_sub(1),
);
self.state.state.list.select(Some(to));
}
@ -86,16 +99,18 @@ impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
}
fn select(mut self) -> Self::APP {
self.state.index = self.state.index.saturating_add(1);
if self.state.index < self.state.matches.len() {
self.state.state = WidgetState::default();
if !self.state.matches[self.state.index].matches.is_empty() {
self.state.state.list.select(Some(0));
self.state.index = self.state.index.map(|i| i.saturating_add(1));
self.state.state = WidgetState::default();
if let Some(index) = self.state.index {
if let Some(matches_info) = self.state.matches_info_vec.get(index) {
if !matches_info.matches.is_empty() {
self.state.state.list.select(Some(0));
}
return self.into();
}
self.into()
} else {
AppMachine::browse(self.inner).into()
}
AppMachine::browse(self.inner).into()
}
fn abort(self) -> Self::APP {

View File

@ -131,8 +131,8 @@ pub struct AppPublicInner<'app> {
}
pub struct AppPublicMatches<'app> {
pub matching: &'app Album,
pub matches: &'app [Match<Album>],
pub matching: Option<&'app Album>,
pub matches: Option<&'app [Match<Album>]>,
pub state: &'app mut WidgetState,
}

View File

@ -26,6 +26,12 @@ use crate::tui::{
ui::IUi,
};
const MUSICHOARD_TUI_HTTP_USER_AGENT: &str = concat!(
"MusicHoard/",
env!("CARGO_PKG_VERSION"),
" ( musichoard@thenineworlds.net )"
);
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
Io(String),

View File

@ -531,11 +531,7 @@ impl Minibuffer<'_> {
},
AppState::Matches(public) => Minibuffer {
paragraphs: vec![
Paragraph::new(format!(
"Matching: {} | {}",
AlbumState::display_album_date(&public.matching.date),
&public.matching.id.title
)),
Paragraph::new(Minibuffer::display_matching_info(public.matching)),
Paragraph::new("q: abort"),
],
columns: 2,
@ -562,6 +558,17 @@ impl Minibuffer<'_> {
mb
}
fn display_matching_info(matching: Option<&Album>) -> String {
match matching {
Some(matching) => format!(
"Matching: {} | {}",
AlbumState::display_album_date(&matching.date),
&matching.id.title
),
None => String::from("Matching: nothing"),
}
}
}
struct ReloadMenu;
@ -819,34 +826,38 @@ impl Ui {
}
}
fn display_match_string(match_album: &Match<Album>) -> String {
format!(
"{:010} | {} [{}] ({}%)",
AlbumState::display_album_date(&match_album.item.date),
&match_album.item.id.title,
AlbumState::display_type(
&match_album.item.primary_type,
&match_album.item.secondary_types
),
match_album.score,
)
}
fn build_match_list(matches: &[Match<Album>]) -> List {
List::new(
matches
.iter()
.map(Ui::display_match_string)
.map(ListItem::new)
.collect::<Vec<ListItem>>(),
)
}
fn render_matches_overlay(
matching: &Album,
matches: &[Match<Album>],
matching: Option<&Album>,
matches: Option<&[Match<Album>]>,
state: &mut WidgetState,
frame: &mut Frame,
) {
let area = OverlayBuilder::default().build(frame.size());
let list = List::new(
matches
.iter()
.map(|m| {
ListItem::new(format!(
"{:010} | {} [{}] ({}%)",
AlbumState::display_album_date(&m.item.date),
&m.item.id.title,
AlbumState::display_type(&m.item.primary_type, &m.item.secondary_types),
m.score,
))
})
.collect::<Vec<ListItem>>(),
);
let matching_string = format!(
"Matching: {} | {}",
AlbumState::display_album_date(&matching.date),
&matching.id.title
);
let matching_string = Minibuffer::display_matching_info(matching);
let list = matches.map(|m| Ui::build_match_list(m)).unwrap_or_default();
Self::render_overlay_list_widget(&matching_string, list, state, true, area, frame)
}
@ -923,8 +934,8 @@ mod tests {
AppState::Search(s) => AppState::Search(s),
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
matching: m.matching,
matches: &m.matches,
state: &mut m.state,
matches: m.matches,
state: m.state,
}),
AppState::Error(s) => AppState::Error(s),
AppState::Critical(s) => AppState::Critical(s),