Compare commits

...

4 Commits

Author SHA1 Message Date
9d685c1157 Fix multiline command
All checks were successful
Cargo CI / Lint (pull_request) Successful in 1m8s
Cargo CI / Build and Test (pull_request) Successful in 1m52s
2024-06-16 16:35:12 +02:00
ad573699a0 Exclude problematic derive code coverage 2024-06-16 16:35:12 +02:00
5d21655239 Update rust toolchain to 1.79 2024-06-16 16:35:12 +02:00
d9d5945422 Display all the extra album info (#173)
All checks were successful
Cargo CI / Build and Test (push) Successful in 1m58s
Cargo CI / Lint (push) Successful in 1m3s
Closes #172

Reviewed-on: #173
2024-03-17 20:17:41 +01:00
4 changed files with 182 additions and 25 deletions

View File

@ -1,4 +1,4 @@
FROM docker.io/library/rust:1.75 FROM docker.io/library/rust:1.79
RUN rustup component add \ RUN rustup component add \
clippy \ clippy \

View File

@ -13,7 +13,7 @@ env:
jobs: jobs:
build_and_test: build_and_test:
name: Build and Test name: Build and Test
container: docker.io/drrobot/musichoard-ci:rust-1.75 container: docker.io/drrobot/musichoard-ci:rust-1.79
env: env:
BEETSDIR: ./ BEETSDIR: ./
LLVM_PROFILE_FILE: target/debug/profraw/musichoard-%p-%m.profraw LLVM_PROFILE_FILE: target/debug/profraw/musichoard-%p-%m.profraw
@ -37,6 +37,7 @@ jobs:
--ignore "tests/*" --ignore "tests/*"
--ignore "src/main.rs" --ignore "src/main.rs"
--ignore "src/bin/musichoard-edit.rs" --ignore "src/bin/musichoard-edit.rs"
--excl-line "^#\[derive"
--excl-start "GRCOV_EXCL_START|mod tests \{" --excl-start "GRCOV_EXCL_START|mod tests \{"
--excl-stop "GRCOV_EXCL_STOP" --excl-stop "GRCOV_EXCL_STOP"
--output-path ./target/debug/coverage/ --output-path ./target/debug/coverage/

View File

@ -50,6 +50,7 @@ grcov codecov/debug/profraw \
--ignore "tests/*" \ --ignore "tests/*" \
--ignore "src/main.rs" \ --ignore "src/main.rs" \
--ignore "src/bin/musichoard-edit.rs" \ --ignore "src/bin/musichoard-edit.rs" \
--excl-line "^#\[derive" \
--excl-start "GRCOV_EXCL_START|mod tests \{" \ --excl-start "GRCOV_EXCL_START|mod tests \{" \
--excl-stop "GRCOV_EXCL_STOP" \ --excl-stop "GRCOV_EXCL_STOP" \
--output-path ./codecov/debug/coverage/ --output-path ./codecov/debug/coverage/

View File

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumDate, AlbumSeq, AlbumStatus}, album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
artist::Artist, artist::Artist,
musicbrainz::IMusicBrainzRef, musicbrainz::IMusicBrainzRef,
track::{Track, TrackFormat, TrackQuality}, track::{Track, TrackFormat, TrackQuality},
@ -198,15 +198,18 @@ impl<'a, 'b> ArtistState<'a, 'b> {
} }
} }
struct InfoOverlay;
impl InfoOverlay {
const ITEM_INDENT: &'static str = " ";
const LIST_INDENT: &'static str = " - ";
}
struct ArtistOverlay<'a> { struct ArtistOverlay<'a> {
properties: Paragraph<'a>, properties: Paragraph<'a>,
} }
impl<'a> ArtistOverlay<'a> { impl<'a> ArtistOverlay<'a> {
fn opt_opt_to_str<S: AsRef<str> + ?Sized>(opt: Option<Option<&S>>) -> &str {
opt.flatten().map(|item| item.as_ref()).unwrap_or("")
}
fn opt_hashmap_to_string<K: Ord + AsRef<str>, T: AsRef<str>>( fn opt_hashmap_to_string<K: Ord + AsRef<str>, T: AsRef<str>>(
opt_map: Option<&HashMap<K, Vec<T>>>, opt_map: Option<&HashMap<K, Vec<T>>>,
item_indent: &str, item_indent: &str,
@ -254,8 +257,8 @@ impl<'a> ArtistOverlay<'a> {
fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> { fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
let artist = state.selected().map(|i| &artists[i]); let artist = state.selected().map(|i| &artists[i]);
let item_indent = " "; let item_indent = InfoOverlay::ITEM_INDENT;
let list_indent = " - "; let list_indent = InfoOverlay::LIST_INDENT;
let double_item_indent = format!("{item_indent}{item_indent}"); let double_item_indent = format!("{item_indent}{item_indent}");
let double_list_indent = format!("{item_indent}{list_indent}"); let double_list_indent = format!("{item_indent}{list_indent}");
@ -265,7 +268,9 @@ impl<'a> ArtistOverlay<'a> {
MusicBrainz: {}\n{item_indent}\ MusicBrainz: {}\n{item_indent}\
Properties: {}", Properties: {}",
artist.map(|a| a.id.name.as_str()).unwrap_or(""), artist.map(|a| a.id.name.as_str()).unwrap_or(""),
Self::opt_opt_to_str(artist.map(|a| a.musicbrainz.as_ref().map(|mb| mb.url()))), artist
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""),
Self::opt_hashmap_to_string( Self::opt_hashmap_to_string(
artist.map(|a| &a.properties), artist.map(|a| &a.properties),
&double_item_indent, &double_item_indent,
@ -296,14 +301,15 @@ impl<'a, 'b> AlbumState<'a, 'b> {
let album = state.list.selected().map(|i| &albums[i]); let album = state.list.selected().map(|i| &albums[i]);
let info = Paragraph::new(format!( let info = Paragraph::new(format!(
"Title: {}\n\ "Title: {}\n\
Date: {}{}\n\ Date: {}\n\
Type: {}\n\
Status: {}", Status: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""), album.map(|a| a.id.title.as_str()).unwrap_or(""),
album album
.map(|a| Self::display_album_date(&a.date)) .map(|a| Self::display_date(&a.date, &a.seq))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| Self::display_album_seq(&a.seq)) .map(|a| Self::display_type(&a.primary_type, &a.secondary_types))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| Self::display_album_status(&a.get_status())) .map(|a| Self::display_album_status(&a.get_status()))
@ -329,6 +335,14 @@ impl<'a, 'b> AlbumState<'a, 'b> {
ListItem::new(line) ListItem::new(line)
} }
fn display_date(date: &AlbumDate, seq: &AlbumSeq) -> String {
if seq.0 > 0 {
format!("{} ({})", Self::display_album_date(date), seq.0)
} else {
Self::display_album_date(date)
}
}
fn display_album_date(date: &AlbumDate) -> String { fn display_album_date(date: &AlbumDate) -> String {
match date.year { match date.year {
Some(year) => match date.month { Some(year) => match date.month {
@ -342,14 +356,57 @@ impl<'a, 'b> AlbumState<'a, 'b> {
} }
} }
fn display_album_seq(seq: &AlbumSeq) -> String { fn display_type(
if seq.0 > 0 { primary: &Option<AlbumPrimaryType>,
format!(" ({})", seq.0) secondary: &Vec<AlbumSecondaryType>,
} else { ) -> String {
String::new() match primary {
Some(ref primary) => {
if secondary.is_empty() {
Self::display_primary_type(primary).to_string()
} else {
format!(
"{} ({})",
Self::display_primary_type(primary),
Self::display_secondary_types(secondary)
)
}
}
None => String::default(),
} }
} }
fn display_primary_type(value: &AlbumPrimaryType) -> &'static str {
match value {
AlbumPrimaryType::Album => "Album",
AlbumPrimaryType::Single => "Single",
AlbumPrimaryType::Ep => "EP",
AlbumPrimaryType::Broadcast => "Broadcast",
AlbumPrimaryType::Other => "Other",
}
}
fn display_secondary_types(values: &Vec<AlbumSecondaryType>) -> String {
let mut types: Vec<&'static str> = vec![];
for value in values {
match value {
AlbumSecondaryType::Compilation => types.push("Compilation"),
AlbumSecondaryType::Soundtrack => types.push("Soundtrack"),
AlbumSecondaryType::Spokenword => types.push("Spokenword"),
AlbumSecondaryType::Interview => types.push("Interview"),
AlbumSecondaryType::Audiobook => types.push("Audiobook"),
AlbumSecondaryType::AudioDrama => types.push("Audio drama"),
AlbumSecondaryType::Live => types.push("Live"),
AlbumSecondaryType::Remix => types.push("Remix"),
AlbumSecondaryType::DjMix => types.push("DJ-mix"),
AlbumSecondaryType::MixtapeStreet => types.push("Mixtape/Street"),
AlbumSecondaryType::Demo => types.push("Demo"),
AlbumSecondaryType::FieldRecording => types.push("Field recording"),
}
}
types.join(", ")
}
fn display_album_status(status: &AlbumStatus) -> &'static str { fn display_album_status(status: &AlbumStatus) -> &'static str {
match status { match status {
AlbumStatus::None => "None", AlbumStatus::None => "None",
@ -361,6 +418,29 @@ impl<'a, 'b> AlbumState<'a, 'b> {
} }
} }
struct AlbumOverlay<'a> {
properties: Paragraph<'a>,
}
impl<'a> AlbumOverlay<'a> {
fn new(albums: &'a [Album], state: &ListState) -> AlbumOverlay<'a> {
let album = state.selected().map(|i| &albums[i]);
let item_indent = InfoOverlay::ITEM_INDENT;
let properties = Paragraph::new(format!(
"Album: {}\n\n{item_indent}\
MusicBrainz: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""),
album
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""),
));
AlbumOverlay { properties }
}
}
struct TrackState<'a, 'b> { struct TrackState<'a, 'b> {
active: bool, active: bool,
list: List<'a>, list: List<'a>,
@ -698,9 +778,18 @@ impl Ui {
fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) { fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) {
let area = OverlayBuilder::default().build(frame.size()); let area = OverlayBuilder::default().build(frame.size());
let artist_overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list); if selection.category() == Category::Artist {
let artist_overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list);
Self::render_overlay_widget("Artist", artist_overlay.properties, area, false, frame); Self::render_overlay_widget("Artist", artist_overlay.properties, area, false, frame);
} else {
let no_albums: Vec<Album> = vec![];
let albums = selection
.state_album(artists)
.map(|st| st.list)
.unwrap_or_else(|| &no_albums);
let album_overlay = AlbumOverlay::new(albums, &selection.widget_state_album().list);
Self::render_overlay_widget("Album", album_overlay.properties, area, false, frame);
}
} }
fn render_reload_overlay(frame: &mut Frame) { fn render_reload_overlay(frame: &mut Frame) {
@ -818,10 +907,76 @@ mod tests {
} }
#[test] #[test]
fn display_album_seq() { fn display_date() {
assert_eq!(AlbumState::display_album_seq(&AlbumSeq::default()), ""); let date: AlbumDate = 1990.into();
assert_eq!(AlbumState::display_album_seq(&AlbumSeq(0)), ""); assert_eq!(
assert_eq!(AlbumState::display_album_seq(&AlbumSeq(5)), " (5)"); AlbumState::display_date(&date, &AlbumSeq::default()),
"1990"
);
assert_eq!(AlbumState::display_date(&date, &AlbumSeq(0)), "1990");
assert_eq!(AlbumState::display_date(&date, &AlbumSeq(5)), "1990 (5)");
}
#[test]
fn display_primary_type() {
assert_eq!(
AlbumState::display_primary_type(&AlbumPrimaryType::Album),
"Album"
);
assert_eq!(
AlbumState::display_primary_type(&AlbumPrimaryType::Single),
"Single"
);
assert_eq!(
AlbumState::display_primary_type(&AlbumPrimaryType::Ep),
"EP"
);
assert_eq!(
AlbumState::display_primary_type(&AlbumPrimaryType::Broadcast),
"Broadcast"
);
assert_eq!(
AlbumState::display_primary_type(&AlbumPrimaryType::Other),
"Other"
);
}
#[test]
fn display_secondary_types() {
assert_eq!(
AlbumState::display_secondary_types(&vec![
AlbumSecondaryType::Compilation,
AlbumSecondaryType::Soundtrack,
AlbumSecondaryType::Spokenword,
AlbumSecondaryType::Interview,
AlbumSecondaryType::Audiobook,
AlbumSecondaryType::AudioDrama,
AlbumSecondaryType::Live,
AlbumSecondaryType::Remix,
AlbumSecondaryType::DjMix,
AlbumSecondaryType::MixtapeStreet,
AlbumSecondaryType::Demo,
AlbumSecondaryType::FieldRecording,
]),
"Compilation, Soundtrack, Spokenword, Interview, Audiobook, Audio drama, Live, Remix, \
DJ-mix, Mixtape/Street, Demo, Field recording"
);
}
#[test]
fn display_type() {
assert_eq!(AlbumState::display_type(&None, &vec![]), "");
assert_eq!(
AlbumState::display_type(&Some(AlbumPrimaryType::Album), &vec![]),
"Album"
);
assert_eq!(
AlbumState::display_type(
&Some(AlbumPrimaryType::Album),
&vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation]
),
"Album (Live, Compilation)"
);
} }
#[test] #[test]