Sort by <field>_sort from tags if it is available #107

Merged
wojtek merged 5 commits from 73---sort-by-<field>_sort-from-tags-if-it-is-available into main 2024-01-13 15:42:05 +01:00
9 changed files with 134 additions and 16 deletions
Showing only changes of commit 7ce439d9b1 - Show all commits

View File

@ -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();

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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),

View File

@ -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

View File

@ -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,

View File

@ -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("Heavens 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",