Sort albums by month if two releases of the same artist happen in the same year #155
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||
use structopt::{clap::AppSettings, StructOpt};
|
||||
|
||||
use musichoard::{
|
||||
collection::artist::ArtistId,
|
||||
collection::{album::AlbumId, artist::ArtistId},
|
||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||
MusicHoard, MusicHoardBuilder, NoLibrary,
|
||||
};
|
||||
@ -22,56 +22,54 @@ struct Opt {
|
||||
database_file_path: PathBuf,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
category: Category,
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum Category {
|
||||
#[structopt(about = "Edit artist information")]
|
||||
Artist(ArtistCommand),
|
||||
enum Command {
|
||||
#[structopt(about = "Modify artist information")]
|
||||
Artist(ArtistOpt),
|
||||
}
|
||||
|
||||
impl Category {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
match self {
|
||||
Category::Artist(artist_command) => artist_command.handle(music_hoard),
|
||||
}
|
||||
}
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct ArtistOpt {
|
||||
// For some reason, not specyfing the artist name with the `long` name makes StructOpt failed
|
||||
// for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and
|
||||
// will insteady try to process it as a command.
|
||||
#[structopt(long, help = "The name of the artist")]
|
||||
name: String,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
command: ArtistCommand,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum ArtistCommand {
|
||||
#[structopt(about = "Add a new artist to the collection")]
|
||||
Add(ArtistValue),
|
||||
Add,
|
||||
#[structopt(about = "Remove an artist from the collection")]
|
||||
Remove(ArtistValue),
|
||||
Remove,
|
||||
#[structopt(about = "Edit the artist's sort name")]
|
||||
Sort(SortCommand),
|
||||
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
|
||||
MusicBrainz(MusicBrainzCommand),
|
||||
#[structopt(name = "property", about = "Edit a property of an artist")]
|
||||
#[structopt(about = "Edit a property of an artist")]
|
||||
Property(PropertyCommand),
|
||||
#[structopt(about = "Modify the artist's album information")]
|
||||
Album(AlbumOpt),
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum SortCommand {
|
||||
#[structopt(about = "Set the provided name as the artist's sort name")]
|
||||
Set(ArtistSortValue),
|
||||
Set(SortValue),
|
||||
#[structopt(about = "Clear the artist's sort name")]
|
||||
Clear(ArtistValue),
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct ArtistValue {
|
||||
#[structopt(help = "The name of the artist")]
|
||||
artist: String,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct ArtistSortValue {
|
||||
#[structopt(help = "The name of the artist")]
|
||||
artist: String,
|
||||
#[structopt(help = "The sort name of the artist")]
|
||||
struct SortValue {
|
||||
#[structopt(help = "The sort name")]
|
||||
sort: String,
|
||||
}
|
||||
|
||||
@ -80,13 +78,11 @@ enum MusicBrainzCommand {
|
||||
#[structopt(about = "Set the MusicBrainz URL overwriting any existing value")]
|
||||
Set(MusicBrainzValue),
|
||||
#[structopt(about = "Clear the MusicBrainz URL)")]
|
||||
Clear(ArtistValue),
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct MusicBrainzValue {
|
||||
#[structopt(help = "The name of the artist")]
|
||||
artist: String,
|
||||
#[structopt(help = "The MusicBrainz URL")]
|
||||
url: String,
|
||||
}
|
||||
@ -105,8 +101,6 @@ enum PropertyCommand {
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct PropertyValue {
|
||||
#[structopt(help = "The name of the artist")]
|
||||
artist: String,
|
||||
#[structopt(help = "The name of the property")]
|
||||
property: String,
|
||||
#[structopt(help = "The list of values")]
|
||||
@ -115,101 +109,176 @@ struct PropertyValue {
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct PropertyName {
|
||||
#[structopt(help = "The name of the artist")]
|
||||
artist: String,
|
||||
#[structopt(help = "The name of the property")]
|
||||
property: String,
|
||||
}
|
||||
|
||||
impl ArtistCommand {
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct AlbumOpt {
|
||||
// Using `long` for consistency with `ArtistOpt`.
|
||||
#[structopt(long, help = "The title of the album")]
|
||||
title: String,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
command: AlbumCommand,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum AlbumCommand {
|
||||
#[structopt(about = "Edit the album's sequence value")]
|
||||
Seq(AlbumSeqCommand),
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum AlbumSeqCommand {
|
||||
#[structopt(about = "Set the sequence value overwriting any existing value")]
|
||||
Set(AlbumSeqValue),
|
||||
#[structopt(about = "Clear the sequence value")]
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct AlbumSeqValue {
|
||||
#[structopt(help = "The new sequence value")]
|
||||
value: u8,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
match self {
|
||||
ArtistCommand::Add(artist_value) => {
|
||||
Command::Artist(artist_opt) => artist_opt.handle(music_hoard),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtistOpt {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
self.command.handle(music_hoard, &self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtistCommand {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||
match self {
|
||||
ArtistCommand::Add => {
|
||||
music_hoard
|
||||
.add_artist(ArtistId::new(artist_value.artist))
|
||||
.add_artist(ArtistId::new(artist_name))
|
||||
.expect("failed to add artist");
|
||||
}
|
||||
ArtistCommand::Remove(artist_value) => {
|
||||
ArtistCommand::Remove => {
|
||||
music_hoard
|
||||
.remove_artist(ArtistId::new(artist_value.artist))
|
||||
.remove_artist(ArtistId::new(artist_name))
|
||||
.expect("failed to remove artist");
|
||||
}
|
||||
ArtistCommand::Sort(sort_command) => {
|
||||
sort_command.handle(music_hoard);
|
||||
sort_command.handle(music_hoard, artist_name);
|
||||
}
|
||||
ArtistCommand::MusicBrainz(musicbrainz_command) => {
|
||||
musicbrainz_command.handle(music_hoard)
|
||||
musicbrainz_command.handle(music_hoard, artist_name)
|
||||
}
|
||||
ArtistCommand::Property(property_command) => {
|
||||
property_command.handle(music_hoard);
|
||||
property_command.handle(music_hoard, artist_name);
|
||||
}
|
||||
ArtistCommand::Album(album_opt) => {
|
||||
album_opt.handle(music_hoard, artist_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SortCommand {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||
match self {
|
||||
SortCommand::Set(artist_sort_value) => music_hoard
|
||||
.set_artist_sort(
|
||||
ArtistId::new(artist_sort_value.artist),
|
||||
ArtistId::new(artist_name),
|
||||
ArtistId::new(artist_sort_value.sort),
|
||||
)
|
||||
.expect("faild to set artist sort name"),
|
||||
SortCommand::Clear(artist_value) => music_hoard
|
||||
.clear_artist_sort(ArtistId::new(artist_value.artist))
|
||||
SortCommand::Clear => music_hoard
|
||||
.clear_artist_sort(ArtistId::new(artist_name))
|
||||
.expect("failed to clear artist sort name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicBrainzCommand {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||
match self {
|
||||
MusicBrainzCommand::Set(musicbrainz_value) => music_hoard
|
||||
.set_musicbrainz_url(
|
||||
ArtistId::new(musicbrainz_value.artist),
|
||||
musicbrainz_value.url,
|
||||
)
|
||||
.set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url)
|
||||
.expect("failed to set MusicBrainz URL"),
|
||||
MusicBrainzCommand::Clear(artist_value) => music_hoard
|
||||
.clear_musicbrainz_url(ArtistId::new(artist_value.artist))
|
||||
MusicBrainzCommand::Clear => music_hoard
|
||||
.clear_artist_musicbrainz(ArtistId::new(artist_name))
|
||||
.expect("failed to clear MusicBrainz URL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyCommand {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||
match self {
|
||||
PropertyCommand::Add(property_value) => music_hoard
|
||||
.add_to_property(
|
||||
ArtistId::new(property_value.artist),
|
||||
.add_to_artist_property(
|
||||
ArtistId::new(artist_name),
|
||||
property_value.property,
|
||||
property_value.values,
|
||||
)
|
||||
.expect("failed to add values to property"),
|
||||
PropertyCommand::Remove(property_value) => music_hoard
|
||||
.remove_from_property(
|
||||
ArtistId::new(property_value.artist),
|
||||
.remove_from_artist_property(
|
||||
ArtistId::new(artist_name),
|
||||
property_value.property,
|
||||
property_value.values,
|
||||
)
|
||||
.expect("failed to remove values from property"),
|
||||
PropertyCommand::Set(property_value) => music_hoard
|
||||
.set_property(
|
||||
ArtistId::new(property_value.artist),
|
||||
.set_artist_property(
|
||||
ArtistId::new(artist_name),
|
||||
property_value.property,
|
||||
property_value.values,
|
||||
)
|
||||
.expect("failed to set property"),
|
||||
PropertyCommand::Clear(property_name) => music_hoard
|
||||
.clear_property(ArtistId::new(property_name.artist), property_name.property)
|
||||
.clear_artist_property(ArtistId::new(artist_name), property_name.property)
|
||||
.expect("failed to clear property"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlbumOpt {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str) {
|
||||
self.command.handle(music_hoard, artist_name, &self.title)
|
||||
}
|
||||
}
|
||||
|
||||
impl AlbumCommand {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
||||
match self {
|
||||
AlbumCommand::Seq(seq_command) => {
|
||||
seq_command.handle(music_hoard, artist_name, album_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlbumSeqCommand {
|
||||
fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) {
|
||||
match self {
|
||||
AlbumSeqCommand::Set(seq_value) => music_hoard
|
||||
.set_album_seq(
|
||||
ArtistId::new(artist_name),
|
||||
AlbumId::new(album_name),
|
||||
seq_value.value,
|
||||
)
|
||||
.expect("failed to set sequence value"),
|
||||
AlbumSeqCommand::Clear => music_hoard
|
||||
.clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name))
|
||||
.expect("failed to clear sequence value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_args();
|
||||
|
||||
@ -219,5 +288,5 @@ fn main() {
|
||||
.set_database(db)
|
||||
.build()
|
||||
.expect("failed to initialise MusicHoard");
|
||||
opt.category.handle(&mut music_hoard);
|
||||
opt.command.handle(&mut music_hoard);
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
use std::mem;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
mem,
|
||||
};
|
||||
|
||||
use crate::core::collection::{
|
||||
merge::{Merge, MergeSorted, WithId},
|
||||
@ -30,25 +33,15 @@ pub struct AlbumId {
|
||||
|
||||
// There are crates for handling dates, but we don't need much complexity beyond year-month-day.
|
||||
/// The album's release date.
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
pub struct AlbumDate {
|
||||
pub year: u32,
|
||||
pub month: AlbumMonth,
|
||||
pub day: u8,
|
||||
}
|
||||
|
||||
impl Default for AlbumDate {
|
||||
fn default() -> Self {
|
||||
AlbumDate {
|
||||
year: 0,
|
||||
month: AlbumMonth::None,
|
||||
day: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The album's sequence to determine order when two or more albums have the same release date.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
pub struct AlbumSeq(pub u8);
|
||||
|
||||
#[repr(u8)]
|
||||
@ -69,6 +62,12 @@ pub enum AlbumMonth {
|
||||
December = 12,
|
||||
}
|
||||
|
||||
impl Default for AlbumMonth {
|
||||
fn default() -> Self {
|
||||
AlbumMonth::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for AlbumMonth {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
@ -93,6 +92,14 @@ impl Album {
|
||||
pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) {
|
||||
(&self.date, &self.seq, &self.id)
|
||||
}
|
||||
|
||||
pub fn set_seq(&mut self, seq: AlbumSeq) {
|
||||
self.seq = seq;
|
||||
}
|
||||
|
||||
pub fn clear_seq(&mut self) {
|
||||
self.seq = AlbumSeq::default();
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Album {
|
||||
@ -116,6 +123,24 @@ impl Merge for Album {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AlbumId> for AlbumId {
|
||||
fn as_ref(&self) -> &AlbumId {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AlbumId {
|
||||
pub fn new<S: Into<String>>(name: S) -> AlbumId {
|
||||
AlbumId { title: name.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AlbumId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.title)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::testmod::FULL_COLLECTION;
|
||||
|
@ -171,6 +171,22 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
||||
})
|
||||
}
|
||||
|
||||
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
|
||||
artist.albums.iter_mut().find(|a| &a.id == album_id)
|
||||
}
|
||||
|
||||
fn get_album_mut_or_err<'a>(
|
||||
artist: &'a mut Artist,
|
||||
album_id: &AlbumId,
|
||||
) -> Result<&'a mut Album, Error> {
|
||||
Self::get_album_mut(artist, album_id).ok_or_else(|| {
|
||||
Error::CollectionError(format!(
|
||||
"album '{}' does not belong to the artist",
|
||||
album_id
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<LIB: ILibrary> MusicHoard<LIB, NoDatabase> {
|
||||
@ -243,38 +259,62 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_collection<F>(&mut self, func: F) -> Result<(), Error>
|
||||
fn update_collection<FN>(&mut self, func: FN) -> Result<(), Error>
|
||||
where
|
||||
F: FnOnce(&mut Collection),
|
||||
FN: FnOnce(&mut Collection),
|
||||
{
|
||||
func(&mut self.pre_commit);
|
||||
self.commit()
|
||||
}
|
||||
|
||||
fn update_artist_and<ID: AsRef<ArtistId>, F1, F2>(
|
||||
fn update_artist_and<ID: AsRef<ArtistId>, FNARTIST, FNCOLL>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
f1: F1,
|
||||
f2: F2,
|
||||
fn_artist: FNARTIST,
|
||||
fn_collection: FNCOLL,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
F1: FnOnce(&mut Artist),
|
||||
F2: FnOnce(&mut Collection),
|
||||
FNARTIST: FnOnce(&mut Artist),
|
||||
FNCOLL: FnOnce(&mut Collection),
|
||||
{
|
||||
f1(Self::get_artist_mut_or_err(
|
||||
fn_artist(Self::get_artist_mut_or_err(
|
||||
&mut self.pre_commit,
|
||||
artist_id.as_ref(),
|
||||
)?);
|
||||
self.update_collection(f2)
|
||||
self.update_collection(fn_collection)
|
||||
}
|
||||
|
||||
fn update_artist<ID: AsRef<ArtistId>, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error>
|
||||
fn update_artist<ID: AsRef<ArtistId>, FN>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
func: FN,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
F: FnOnce(&mut Artist),
|
||||
FN: FnOnce(&mut Artist),
|
||||
{
|
||||
self.update_artist_and(artist_id, func, |_| {})
|
||||
}
|
||||
|
||||
fn update_album_and<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>, FNALBUM, FNARTIST, FNCOLL>(
|
||||
&mut self,
|
||||
artist_id: ARTIST,
|
||||
album_id: ALBUM,
|
||||
fn_album: FNALBUM,
|
||||
fn_artist: FNARTIST,
|
||||
fn_collection: FNCOLL,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FNALBUM: FnOnce(&mut Album),
|
||||
FNARTIST: FnOnce(&mut Artist),
|
||||
FNCOLL: FnOnce(&mut Collection),
|
||||
{
|
||||
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id.as_ref())?;
|
||||
let album = Self::get_album_mut_or_err(artist, album_id.as_ref())?;
|
||||
fn_album(album);
|
||||
fn_artist(artist);
|
||||
self.update_collection(fn_collection)
|
||||
}
|
||||
|
||||
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
||||
let artist_id: ArtistId = artist_id.into();
|
||||
|
||||
@ -315,7 +355,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_musicbrainz_url<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
pub fn set_artist_musicbrainz<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
url: S,
|
||||
@ -324,14 +364,14 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url))
|
||||
}
|
||||
|
||||
pub fn clear_musicbrainz_url<ID: AsRef<ArtistId>>(
|
||||
pub fn clear_artist_musicbrainz<ID: AsRef<ArtistId>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url())
|
||||
}
|
||||
|
||||
pub fn add_to_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
pub fn add_to_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
property: S,
|
||||
@ -340,7 +380,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
self.update_artist(artist_id, |artist| artist.add_to_property(property, values))
|
||||
}
|
||||
|
||||
pub fn remove_from_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
pub fn remove_from_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
property: S,
|
||||
@ -351,7 +391,7 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
pub fn set_artist_property<ID: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
property: S,
|
||||
@ -360,13 +400,42 @@ impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
self.update_artist(artist_id, |artist| artist.set_property(property, values))
|
||||
}
|
||||
|
||||
pub fn clear_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
pub fn clear_artist_property<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
property: S,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id, |artist| artist.clear_property(property))
|
||||
}
|
||||
|
||||
pub fn set_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
|
||||
&mut self,
|
||||
artist_id: ARTIST,
|
||||
album_id: ALBUM,
|
||||
seq: u8,
|
||||
) -> Result<(), Error> {
|
||||
self.update_album_and(
|
||||
artist_id,
|
||||
album_id,
|
||||
|album| album.set_seq(AlbumSeq(seq)),
|
||||
|artist| artist.albums.sort_unstable(),
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clear_album_seq<ARTIST: AsRef<ArtistId>, ALBUM: AsRef<AlbumId>>(
|
||||
&mut self,
|
||||
artist_id: ARTIST,
|
||||
album_id: ALBUM,
|
||||
) -> Result<(), Error> {
|
||||
self.update_album_and(
|
||||
artist_id,
|
||||
album_id,
|
||||
|album| album.clear_seq(),
|
||||
|artist| artist.albums.sort_unstable(),
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||
@ -518,7 +587,7 @@ mod tests {
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
|
||||
let actual_err = music_hoard
|
||||
.set_musicbrainz_url(&artist_id, MUSICBUTLER)
|
||||
.set_artist_musicbrainz(&artist_id, MUSICBUTLER)
|
||||
.unwrap_err();
|
||||
let expected_err = Error::CollectionError(format!(
|
||||
"an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}"
|
||||
@ -544,23 +613,23 @@ mod tests {
|
||||
|
||||
// Setting a URL on an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.set_musicbrainz_url(&artist_id_2, MUSICBRAINZ)
|
||||
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
|
||||
.is_err());
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Setting a URL on an artist.
|
||||
assert!(music_hoard
|
||||
.set_musicbrainz_url(&artist_id, MUSICBRAINZ)
|
||||
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
|
||||
.is_ok());
|
||||
_ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap());
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Clearing URLs on an artist that does not exist is an error.
|
||||
assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err());
|
||||
assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Clearing URLs.
|
||||
assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok());
|
||||
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
|
||||
_ = expected.take();
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
}
|
||||
@ -582,13 +651,13 @@ mod tests {
|
||||
|
||||
// Adding URLs to an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.is_err());
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
||||
// Adding mutliple URLs without clashes.
|
||||
assert!(music_hoard
|
||||
.add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.is_ok());
|
||||
expected.push(MUSICBUTLER.to_owned());
|
||||
expected.push(MUSICBUTLER_2.to_owned());
|
||||
@ -599,7 +668,7 @@ mod tests {
|
||||
|
||||
// Removing URLs from an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
music_hoard.collection[0].properties.get("MusicButler"),
|
||||
@ -608,7 +677,11 @@ mod tests {
|
||||
|
||||
// Removing multiple URLs without clashes.
|
||||
assert!(music_hoard
|
||||
.remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.remove_from_artist_property(
|
||||
&artist_id,
|
||||
"MusicButler",
|
||||
vec![MUSICBUTLER, MUSICBUTLER_2]
|
||||
)
|
||||
.is_ok());
|
||||
expected.clear();
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
@ -631,13 +704,13 @@ mod tests {
|
||||
|
||||
// Seting URL on an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.is_err());
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
||||
// Set URLs.
|
||||
assert!(music_hoard
|
||||
.set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.is_ok());
|
||||
expected.clear();
|
||||
expected.push(MUSICBUTLER.to_owned());
|
||||
@ -649,12 +722,12 @@ mod tests {
|
||||
|
||||
// Clearing URLs on an artist that does not exist is an error.
|
||||
assert!(music_hoard
|
||||
.clear_property(&artist_id_2, "MusicButler")
|
||||
.clear_artist_property(&artist_id_2, "MusicButler")
|
||||
.is_err());
|
||||
|
||||
// Clear URLs.
|
||||
assert!(music_hoard
|
||||
.clear_property(&artist_id, "MusicButler")
|
||||
.clear_artist_property(&artist_id, "MusicButler")
|
||||
.is_ok());
|
||||
expected.clear();
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
@ -75,7 +75,12 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
||||
let ui = Ui;
|
||||
|
||||
// Run the TUI application.
|
||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||
let result = Tui::run(terminal, app, ui, handler, listener);
|
||||
if let Err(tui::Error::ListenerPanic(err)) = result {
|
||||
std::panic::resume_unwind(err);
|
||||
} else {
|
||||
result.expect("failed to run tui")
|
||||
};
|
||||
}
|
||||
|
||||
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
||||
|
@ -11,12 +11,16 @@ pub use handler::EventHandler;
|
||||
pub use listener::EventListener;
|
||||
pub use ui::Ui;
|
||||
|
||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use ratatui::backend::Backend;
|
||||
use ratatui::Terminal;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, EnableMouseCapture},
|
||||
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::Backend, Terminal};
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
ptr,
|
||||
{any::Any, io},
|
||||
};
|
||||
|
||||
use crate::tui::{
|
||||
app::{IAppAccess, IAppInteract},
|
||||
@ -26,11 +30,32 @@ use crate::tui::{
|
||||
ui::IUi,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(String),
|
||||
Event(String),
|
||||
ListenerPanic,
|
||||
ListenerPanic(Box<dyn Any + Send>),
|
||||
}
|
||||
|
||||
impl Eq for Error {}
|
||||
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match self {
|
||||
Error::Io(this) => match other {
|
||||
Error::Io(other) => this == other,
|
||||
_ => false,
|
||||
},
|
||||
Error::Event(this) => match other {
|
||||
Error::Event(other) => this == other,
|
||||
_ => false,
|
||||
},
|
||||
Error::ListenerPanic(this) => match other {
|
||||
Error::ListenerPanic(other) => ptr::eq(this.as_ref(), other.as_ref()),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
@ -114,7 +139,8 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
|
||||
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
||||
// will not produce an error message. The panic error message is printed at
|
||||
// the location of the panic which at the time is hidden by the TUI.
|
||||
Err(_) => return Err(Error::ListenerPanic),
|
||||
// Therefore, propagate the error for the caller to resume unwinding.
|
||||
Err(panic) => return Err(Error::ListenerPanic(panic)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,14 +327,14 @@ mod tests {
|
||||
|
||||
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
|
||||
assert!(matches!(result.unwrap_err(), Error::ListenerPanic(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
||||
let event_err: Error = EventError::Recv.into();
|
||||
let listener_err = Error::ListenerPanic;
|
||||
let listener_err = Error::ListenerPanic(Box::new("hello"));
|
||||
|
||||
assert!(!format!("{:?}", io_err).is_empty());
|
||||
assert!(!format!("{:?}", event_err).is_empty());
|
||||
|
Loading…
Reference in New Issue
Block a user