First draft working
This commit is contained in:
parent
83675c25e6
commit
7ce439d9b1
@ -44,6 +44,8 @@ enum ArtistCommand {
|
||||
Add(ArtistValue),
|
||||
#[structopt(about = "Remove an artist from the collection")]
|
||||
Remove(ArtistValue),
|
||||
#[structopt(about = "Edit the artist's sort name")]
|
||||
Sort(SortCommand),
|
||||
#[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")]
|
||||
MusicBrainz(UrlCommand<SingleUrlValue>),
|
||||
#[structopt(
|
||||
@ -57,12 +59,28 @@ enum ArtistCommand {
|
||||
Qobuz(UrlCommand<SingleUrlValue>),
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum SortCommand {
|
||||
#[structopt(help = "Set the provided name as the artist's sort name")]
|
||||
Set(ArtistSortValue),
|
||||
#[structopt(help = "Clear the artist's sort name")]
|
||||
Clear(ArtistValue),
|
||||
}
|
||||
|
||||
#[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")]
|
||||
sort: String,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
enum UrlCommand<T: StructOpt> {
|
||||
#[structopt(about = "Add the provided URL(s) without overwriting existing values")]
|
||||
@ -137,6 +155,9 @@ impl ArtistCommand {
|
||||
ArtistCommand::Remove(artist_value) => {
|
||||
music_hoard.remove_artist(ArtistId::new(artist_value.artist));
|
||||
}
|
||||
ArtistCommand::Sort(sort_command) => {
|
||||
sort_command.handle(music_hoard);
|
||||
}
|
||||
ArtistCommand::MusicBrainz(url_command) => {
|
||||
single_url_command_dispatch!(url_command, music_hoard, musicbrainz)
|
||||
}
|
||||
@ -153,6 +174,22 @@ impl ArtistCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl SortCommand {
|
||||
fn handle(self, music_hoard: &mut MH) {
|
||||
match self {
|
||||
SortCommand::Set(artist_sort_value) => music_hoard
|
||||
.set_artist_sort(
|
||||
ArtistId::new(artist_sort_value.artist),
|
||||
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))
|
||||
.expect("failed to clear artist sort name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_args();
|
||||
|
||||
|
@ -225,9 +225,7 @@ mod tests {
|
||||
fn save_errors() {
|
||||
let mut object = HashMap::<ArtistId, String>::new();
|
||||
object.insert(
|
||||
ArtistId {
|
||||
name: String::from("artist"),
|
||||
},
|
||||
ArtistId::new(String::from("artist")),
|
||||
String::from("string"),
|
||||
);
|
||||
let serde_err = serde_json::to_string(&object);
|
||||
|
64
src/lib.rs
64
src/lib.rs
@ -380,6 +380,7 @@ impl Merge for ArtistProperties {
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct Artist {
|
||||
pub id: ArtistId,
|
||||
pub sort: Option<ArtistId>,
|
||||
pub properties: ArtistProperties,
|
||||
pub albums: Vec<Album>,
|
||||
}
|
||||
@ -433,11 +434,33 @@ impl Artist {
|
||||
pub fn new<ID: Into<ArtistId>>(id: ID) -> Self {
|
||||
Artist {
|
||||
id: id.into(),
|
||||
sort: None,
|
||||
properties: ArtistProperties::default(),
|
||||
albums: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_sort<ID: Into<ArtistId>, SORT: Into<ArtistId>>(id: ID, sort: SORT) -> Self {
|
||||
Artist {
|
||||
id: id.into(),
|
||||
sort: Some(sort.into()),
|
||||
properties: ArtistProperties::default(),
|
||||
albums: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sort_key(&self) -> &ArtistId {
|
||||
self.sort.as_ref().unwrap_or(&self.id)
|
||||
}
|
||||
|
||||
fn set_sort_key<SORT: Into<ArtistId>>(&mut self, sort: SORT) {
|
||||
self.sort = Some(sort.into());
|
||||
}
|
||||
|
||||
fn clear_sort_key(&mut self) {
|
||||
_ = self.sort.take();
|
||||
}
|
||||
|
||||
fn add_unique_url<S: AsRef<str>, T: for<'a> TryFrom<&'a str, Error = Error> + Eq + Display>(
|
||||
container: &mut Option<T>,
|
||||
url: S,
|
||||
@ -552,7 +575,7 @@ impl PartialOrd for Artist {
|
||||
|
||||
impl Ord for Artist {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
self.get_sort_key().cmp(other.get_sort_key())
|
||||
}
|
||||
}
|
||||
|
||||
@ -823,6 +846,21 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>(
|
||||
&mut self,
|
||||
artist_id: ID,
|
||||
artist_sort: SORT,
|
||||
) -> Result<(), Error> {
|
||||
self.get_artist_or_err(artist_id.as_ref())?
|
||||
.set_sort_key(artist_sort);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
||||
self.get_artist_or_err(artist_id.as_ref())?.clear_sort_key();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
music_hoard_unique_url_dispatch!(musicbrainz);
|
||||
|
||||
music_hoard_multi_url_dispatch!(musicbutler);
|
||||
@ -845,7 +883,7 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||
MergeSorted::new(primary.into_iter(), secondary.into_iter()).collect()
|
||||
}
|
||||
|
||||
fn items_to_artists(items: Vec<Item>) -> Vec<Artist> {
|
||||
fn items_to_artists(items: Vec<Item>) -> Result<Vec<Artist>, Error> {
|
||||
let mut artists: Vec<Artist> = vec![];
|
||||
let mut album_ids = HashMap::<ArtistId, HashSet<AlbumId>>::new();
|
||||
|
||||
@ -854,6 +892,8 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||
name: item.album_artist,
|
||||
};
|
||||
|
||||
let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s });
|
||||
|
||||
let album_id = AlbumId {
|
||||
year: item.album_year,
|
||||
title: item.album_title,
|
||||
@ -886,6 +926,21 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||
artists.last_mut().unwrap()
|
||||
};
|
||||
|
||||
if artist.sort.is_some() {
|
||||
if artist_sort.is_some() && (artist.sort != artist_sort) {
|
||||
return Err(Error::CollectionError(format!(
|
||||
"multiple album_artist_sort found for artist '{}': '{}' != '{}'",
|
||||
artist.id,
|
||||
artist.sort.as_ref().unwrap(),
|
||||
artist_sort.as_ref().unwrap()
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
if artist_sort.is_some() {
|
||||
artist.sort = artist_sort;
|
||||
}
|
||||
}
|
||||
|
||||
if album_ids[&artist_id].contains(&album_id) {
|
||||
// Assume results are in some order which means they will likely be grouped by
|
||||
// album. Therefore, we look from the back since the last inserted album is most
|
||||
@ -909,7 +964,7 @@ impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||
}
|
||||
}
|
||||
|
||||
artists
|
||||
Ok(artists)
|
||||
}
|
||||
|
||||
fn get_artist(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> {
|
||||
@ -927,7 +982,7 @@ impl<LIB: ILibrary, DB> MusicHoard<LIB, DB> {
|
||||
/// Rescan the library and merge with the in-memory collection.
|
||||
pub fn rescan_library(&mut self) -> Result<(), Error> {
|
||||
let items = self.library.list(&Query::new())?;
|
||||
let mut library_collection = Self::items_to_artists(items);
|
||||
let mut library_collection = Self::items_to_artists(items)?;
|
||||
Self::sort(&mut library_collection);
|
||||
|
||||
let collection = mem::take(&mut self.collection);
|
||||
@ -1036,6 +1091,7 @@ mod tests {
|
||||
for track in album.tracks.iter() {
|
||||
items.push(Item {
|
||||
album_artist: artist.id.name.clone(),
|
||||
album_artist_sort: artist.sort.as_ref().map(|s| s.name.clone()),
|
||||
album_year: album.id.year,
|
||||
album_title: album.id.title.clone(),
|
||||
track_number: track.id.number,
|
||||
|
@ -22,6 +22,8 @@ const LIST_FORMAT_ARG: &str = concat!(
|
||||
"--format=",
|
||||
"$albumartist",
|
||||
list_format_separator!(),
|
||||
"$albumartist_sort",
|
||||
list_format_separator!(),
|
||||
"$year",
|
||||
list_format_separator!(),
|
||||
"$album",
|
||||
@ -52,6 +54,7 @@ impl ToBeetsArg for Field {
|
||||
let negate = if include { "" } else { "^" };
|
||||
match self {
|
||||
Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
|
||||
Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"),
|
||||
Field::AlbumYear(ref u) => format!("{negate}year:{u}"),
|
||||
Field::AlbumTitle(ref s) => format!("{negate}album:{s}"),
|
||||
Field::TrackNumber(ref u) => format!("{negate}track:{u}"),
|
||||
@ -128,24 +131,30 @@ impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
|
||||
}
|
||||
|
||||
let album_artist = split[0].to_string();
|
||||
let album_year = split[1].parse::<u32>()?;
|
||||
let album_title = split[2].to_string();
|
||||
let track_number = split[3].parse::<u32>()?;
|
||||
let track_title = split[4].to_string();
|
||||
let track_artist = split[5]
|
||||
let album_artist_sort = if !split[1].is_empty() {
|
||||
Some(split[1].to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let album_year = split[2].parse::<u32>()?;
|
||||
let album_title = split[3].to_string();
|
||||
let track_number = split[4].parse::<u32>()?;
|
||||
let track_title = split[5].to_string();
|
||||
let track_artist = split[6]
|
||||
.to_string()
|
||||
.split("; ")
|
||||
.map(|s| s.to_owned())
|
||||
.collect();
|
||||
let track_format = match split[6].to_string().as_str() {
|
||||
let track_format = match split[7].to_string().as_str() {
|
||||
TRACK_FORMAT_FLAC => Format::Flac,
|
||||
TRACK_FORMAT_MP3 => Format::Mp3,
|
||||
_ => return Err(Error::Invalid(line.to_string())),
|
||||
};
|
||||
let track_bitrate = split[7].trim_end_matches("kbps").parse::<u32>()?;
|
||||
let track_bitrate = split[8].trim_end_matches("kbps").parse::<u32>()?;
|
||||
|
||||
items.push(Item {
|
||||
album_artist,
|
||||
album_artist_sort,
|
||||
album_year,
|
||||
album_title,
|
||||
track_number,
|
||||
@ -170,10 +179,15 @@ mod tests {
|
||||
|
||||
fn item_to_beets_string(item: &Item) -> String {
|
||||
format!(
|
||||
"{album_artist}{sep}{album_year}{sep}{album_title}{sep}\
|
||||
"{album_artist}{sep}{album_artist_sort}{sep}\
|
||||
{album_year}{sep}{album_title}{sep}\
|
||||
{track_number}{sep}{track_title}{sep}\
|
||||
{track_artist}{sep}{track_format}{sep}{track_bitrate}kbps",
|
||||
album_artist = item.album_artist,
|
||||
album_artist_sort = match item.album_artist_sort {
|
||||
Some(ref album_artist_sort) => album_artist_sort,
|
||||
None => "",
|
||||
},
|
||||
album_year = item.album_year,
|
||||
album_title = item.album_title,
|
||||
track_number = item.track_number,
|
||||
|
@ -30,6 +30,7 @@ impl ILibrary for NullLibrary {
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Item {
|
||||
pub album_artist: String,
|
||||
pub album_artist_sort: Option<String>,
|
||||
pub album_year: u32,
|
||||
pub album_title: String,
|
||||
pub track_number: u32,
|
||||
@ -43,6 +44,7 @@ pub struct Item {
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub enum Field {
|
||||
AlbumArtist(String),
|
||||
AlbumArtistSort(String),
|
||||
AlbumYear(u32),
|
||||
AlbumTitle(String),
|
||||
TrackNumber(u32),
|
||||
|
@ -5,6 +5,7 @@ macro_rules! collection {
|
||||
id: ArtistId {
|
||||
name: "album_artist a".to_string(),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
|
||||
@ -100,6 +101,7 @@ macro_rules! collection {
|
||||
id: ArtistId {
|
||||
name: "album_artist b".to_string(),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
@ -192,6 +194,7 @@ macro_rules! collection {
|
||||
id: ArtistId {
|
||||
name: "album_artist c".to_string(),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
|
File diff suppressed because one or more lines are too long
@ -41,6 +41,7 @@ fn artist_to_items(artist: &Artist) -> Vec<Item> {
|
||||
for track in album.tracks.iter() {
|
||||
items.push(Item {
|
||||
album_artist: artist.id.name.clone(),
|
||||
album_artist_sort: artist.sort.as_ref().map(|s| s.name.clone()),
|
||||
album_year: album.id.year,
|
||||
album_title: album.id.title.clone(),
|
||||
track_number: track.id.number,
|
||||
|
@ -10,6 +10,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
id: ArtistId {
|
||||
name: String::from("Eluveitie"),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38",
|
||||
@ -243,6 +244,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
id: ArtistId {
|
||||
name: String::from("Frontside"),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490",
|
||||
@ -389,6 +391,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
id: ArtistId {
|
||||
name: String::from("Heaven’s Basement"),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc",
|
||||
@ -509,6 +512,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
id: ArtistId {
|
||||
name: String::from("Metallica"),
|
||||
},
|
||||
sort: None,
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab",
|
||||
@ -863,6 +867,9 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
id: ArtistId {
|
||||
name: String::from("Аркона"),
|
||||
},
|
||||
sort: Some(ArtistId{
|
||||
name: String::from("Arkona")
|
||||
}),
|
||||
properties: ArtistProperties {
|
||||
musicbrainz: Some(MusicBrainz::new(
|
||||
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212",
|
||||
|
Loading…
Reference in New Issue
Block a user