Artist list merges case divergences, excludes VA, reports more album info
This commit is contained in:
parent
309620a088
commit
ad37a14cfa
4 changed files with 122 additions and 32 deletions
|
@ -1,10 +1,11 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Borrow,
|
borrow::Borrow,
|
||||||
|
collections::HashMap,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use lasso2::{Rodeo, RodeoReader};
|
use lasso2::{Rodeo, RodeoReader, Spur};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
|
@ -251,6 +252,7 @@ impl Default for Index {
|
||||||
|
|
||||||
pub struct Builder {
|
pub struct Builder {
|
||||||
strings: Rodeo,
|
strings: Rodeo,
|
||||||
|
minuscules: HashMap<String, Spur>,
|
||||||
browser_builder: browser::Builder,
|
browser_builder: browser::Builder,
|
||||||
collection_builder: collection::Builder,
|
collection_builder: collection::Builder,
|
||||||
}
|
}
|
||||||
|
@ -259,6 +261,7 @@ impl Builder {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
strings: Rodeo::new(),
|
strings: Rodeo::new(),
|
||||||
|
minuscules: HashMap::default(),
|
||||||
browser_builder: browser::Builder::default(),
|
browser_builder: browser::Builder::default(),
|
||||||
collection_builder: collection::Builder::default(),
|
collection_builder: collection::Builder::default(),
|
||||||
}
|
}
|
||||||
|
@ -271,7 +274,8 @@ impl Builder {
|
||||||
|
|
||||||
pub fn add_song(&mut self, song: scanner::Song) {
|
pub fn add_song(&mut self, song: scanner::Song) {
|
||||||
self.browser_builder.add_song(&mut self.strings, &song);
|
self.browser_builder.add_song(&mut self.strings, &song);
|
||||||
self.collection_builder.add_song(&mut self.strings, &song);
|
self.collection_builder
|
||||||
|
.add_song(&mut self.strings, &mut self.minuscules, &song);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Index {
|
pub fn build(self) -> Index {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lasso2::{Rodeo, RodeoReader};
|
use lasso2::{Rodeo, RodeoReader, Spur};
|
||||||
use rand::{rngs::ThreadRng, seq::IteratorRandom};
|
use rand::{rngs::ThreadRng, seq::IteratorRandom};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
|
@ -16,8 +16,9 @@ use super::storage::fetch_song;
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq)]
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
pub struct ArtistHeader {
|
pub struct ArtistHeader {
|
||||||
pub name: Option<UniCase<String>>,
|
pub name: UniCase<String>,
|
||||||
pub num_albums: u32,
|
pub num_own_albums: u32,
|
||||||
|
pub num_appearances: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq)]
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
@ -67,10 +68,11 @@ pub struct Collection {
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn get_artists(&self, strings: &RodeoReader) -> Vec<ArtistHeader> {
|
pub fn get_artists(&self, strings: &RodeoReader) -> Vec<ArtistHeader> {
|
||||||
|
let exceptions = vec![strings.get("Various Artists"), strings.get("VA")];
|
||||||
let mut artists = self
|
let mut artists = self
|
||||||
.artists
|
.artists
|
||||||
.values()
|
.values()
|
||||||
.filter(|a| a.albums.len() > 0 || a.featured_on.len() > 1)
|
.filter(|a| !exceptions.contains(&Some(a.name)))
|
||||||
.map(|a| make_artist_header(a, strings))
|
.map(|a| make_artist_header(a, strings))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
artists.sort_by(|a, b| a.name.cmp(&b.name));
|
artists.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
@ -169,10 +171,9 @@ impl Collection {
|
||||||
|
|
||||||
fn make_artist_header(artist: &storage::Artist, strings: &RodeoReader) -> ArtistHeader {
|
fn make_artist_header(artist: &storage::Artist, strings: &RodeoReader) -> ArtistHeader {
|
||||||
ArtistHeader {
|
ArtistHeader {
|
||||||
name: artist
|
name: UniCase::new(strings.resolve(&artist.name).to_owned()),
|
||||||
.name
|
num_own_albums: artist.albums.len() as u32,
|
||||||
.map(|n| UniCase::new(strings.resolve(&n).to_owned())),
|
num_appearances: artist.featured_on.len() as u32,
|
||||||
num_albums: artist.albums.len() as u32 + artist.featured_on.len() as u32,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,8 +185,13 @@ pub struct Builder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Builder {
|
impl Builder {
|
||||||
pub fn add_song(&mut self, strings: &mut Rodeo, song: &scanner::Song) {
|
pub fn add_song(
|
||||||
let Some(song) = store_song(strings, song) else {
|
&mut self,
|
||||||
|
strings: &mut Rodeo,
|
||||||
|
minuscules: &mut HashMap<String, Spur>,
|
||||||
|
song: &scanner::Song,
|
||||||
|
) {
|
||||||
|
let Some(song) = store_song(strings, minuscules, song) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,7 +246,7 @@ impl Builder {
|
||||||
self.artists
|
self.artists
|
||||||
.entry(artist_key)
|
.entry(artist_key)
|
||||||
.or_insert_with(|| storage::Artist {
|
.or_insert_with(|| storage::Artist {
|
||||||
name: Some(name),
|
name,
|
||||||
albums: HashSet::new(),
|
albums: HashSet::new(),
|
||||||
featured_on: HashSet::new(),
|
featured_on: HashSet::new(),
|
||||||
})
|
})
|
||||||
|
@ -287,10 +293,11 @@ mod test {
|
||||||
|
|
||||||
fn setup_test(songs: Vec<scanner::Song>) -> (Collection, RodeoReader) {
|
fn setup_test(songs: Vec<scanner::Song>) -> (Collection, RodeoReader) {
|
||||||
let mut strings = Rodeo::new();
|
let mut strings = Rodeo::new();
|
||||||
|
let mut minuscules = HashMap::new();
|
||||||
let mut builder = Builder::default();
|
let mut builder = Builder::default();
|
||||||
|
|
||||||
for song in songs {
|
for song in songs {
|
||||||
builder.add_song(&mut strings, &song);
|
builder.add_song(&mut strings, &mut minuscules, &song);
|
||||||
}
|
}
|
||||||
|
|
||||||
let browser = builder.build();
|
let browser = builder.build();
|
||||||
|
@ -319,7 +326,7 @@ mod test {
|
||||||
let artists = collection
|
let artists = collection
|
||||||
.get_artists(&strings)
|
.get_artists(&strings)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| a.name.unwrap())
|
.map(|a| a.name)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -351,7 +358,7 @@ mod test {
|
||||||
let artists = collection
|
let artists = collection
|
||||||
.get_artists(&strings)
|
.get_artists(&strings)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| a.name.unwrap())
|
.map(|a| a.name)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -363,6 +370,66 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn artists_with_diverging_case_are_merged() {
|
||||||
|
let (collection, strings) = setup_test(Vec::from([
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("Rain of Fury.mp3"),
|
||||||
|
title: Some("Rain of Fury".to_owned()),
|
||||||
|
artists: vec!["Rhapsody Of Fire".to_owned()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("Emerald Sword.mp3"),
|
||||||
|
title: Some("Chains of Destiny".to_owned()),
|
||||||
|
artists: vec!["Rhapsody of Fire".to_owned()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
let artists = collection
|
||||||
|
.get_artists(&strings)
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(artists, vec![UniCase::new("Rhapsody of Fire".to_owned()),]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn artists_list_excludes_various_artists() {
|
||||||
|
let (collection, strings) = setup_test(Vec::from([
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("Rain of Fury.mp3"),
|
||||||
|
title: Some("Rain of Fury".to_owned()),
|
||||||
|
artists: vec!["Rhapsody Of Fire".to_owned()],
|
||||||
|
album_artists: vec!["Various Artists".to_owned()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scanner::Song {
|
||||||
|
virtual_path: PathBuf::from("Paradise.mp3"),
|
||||||
|
title: Some("Paradise".to_owned()),
|
||||||
|
artists: vec!["Stratovarius".to_owned()],
|
||||||
|
album_artists: vec!["Various Artists".to_owned()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
let artists = collection
|
||||||
|
.get_artists(&strings)
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
artists,
|
||||||
|
vec![
|
||||||
|
UniCase::new("Rhapsody of Fire".to_owned()),
|
||||||
|
UniCase::new("Stratovarius".to_owned()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_get_random_albums() {
|
fn can_get_random_albums() {
|
||||||
let (collection, strings) = setup_test(Vec::from([
|
let (collection, strings) = setup_test(Vec::from([
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::{HashMap, HashSet},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use lasso2::{Rodeo, RodeoReader};
|
use lasso2::{Rodeo, RodeoReader, Spur};
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tinyvec::TinyVec;
|
use tinyvec::TinyVec;
|
||||||
|
@ -18,7 +18,7 @@ pub enum File {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
pub name: Option<lasso2::Spur>,
|
pub name: lasso2::Spur,
|
||||||
pub albums: HashSet<AlbumKey>,
|
pub albums: HashSet<AlbumKey>,
|
||||||
pub featured_on: HashSet<AlbumKey>,
|
pub featured_on: HashSet<AlbumKey>,
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,11 @@ impl Song {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_song(strings: &mut Rodeo, song: &scanner::Song) -> Option<Song> {
|
pub fn store_song(
|
||||||
|
strings: &mut Rodeo,
|
||||||
|
minuscules: &mut HashMap<String, Spur>,
|
||||||
|
song: &scanner::Song,
|
||||||
|
) -> Option<Song> {
|
||||||
let Some(real_path) = (&song.real_path).get_or_intern(strings) else {
|
let Some(real_path) = (&song.real_path).get_or_intern(strings) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
@ -103,45 +107,58 @@ pub fn store_song(strings: &mut Rodeo, song: &scanner::Song) -> Option<Song> {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut canonicalize = |s: &String| {
|
||||||
|
minuscules
|
||||||
|
.entry(s.to_lowercase())
|
||||||
|
.or_insert_with(|| strings.get_or_intern(s))
|
||||||
|
.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
Some(Song {
|
Some(Song {
|
||||||
real_path,
|
real_path,
|
||||||
virtual_path,
|
virtual_path,
|
||||||
track_number: song.track_number,
|
track_number: song.track_number,
|
||||||
disc_number: song.disc_number,
|
disc_number: song.disc_number,
|
||||||
title: song.title.as_ref().map(|s| strings.get_or_intern(s)),
|
title: song.title.as_ref().map(&mut canonicalize),
|
||||||
artists: song
|
artists: song
|
||||||
.artists
|
.artists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.get_or_intern(s))
|
.filter(|s| s.len() > 0)
|
||||||
|
.map(&mut canonicalize)
|
||||||
.collect(),
|
.collect(),
|
||||||
album_artists: song
|
album_artists: song
|
||||||
.album_artists
|
.album_artists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.get_or_intern(s))
|
.filter(|s| s.len() > 0)
|
||||||
|
.map(&mut canonicalize)
|
||||||
.collect(),
|
.collect(),
|
||||||
year: song.year,
|
year: song.year,
|
||||||
album: song.album.as_ref().map(|s| strings.get_or_intern(s)),
|
album: song.album.as_ref().map(&mut canonicalize),
|
||||||
artwork: artwork,
|
artwork: artwork,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
lyricists: song
|
lyricists: song
|
||||||
.lyricists
|
.lyricists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.get_or_intern(s))
|
.filter(|s| s.len() > 0)
|
||||||
|
.map(&mut canonicalize)
|
||||||
.collect(),
|
.collect(),
|
||||||
composers: song
|
composers: song
|
||||||
.composers
|
.composers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.get_or_intern(s))
|
.filter(|s| s.len() > 0)
|
||||||
|
.map(&mut canonicalize)
|
||||||
.collect(),
|
.collect(),
|
||||||
genres: song
|
genres: song
|
||||||
.genres
|
.genres
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.get_or_intern(s))
|
.filter(|s| s.len() > 0)
|
||||||
|
.map(&mut canonicalize)
|
||||||
.collect(),
|
.collect(),
|
||||||
labels: song
|
labels: song
|
||||||
.labels
|
.labels
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.get_or_intern(s))
|
.filter(|s| s.len() > 0)
|
||||||
|
.map(&mut canonicalize)
|
||||||
.collect(),
|
.collect(),
|
||||||
date_added: song.date_added,
|
date_added: song.date_added,
|
||||||
})
|
})
|
||||||
|
|
|
@ -319,15 +319,17 @@ impl From<index::File> for BrowserEntry {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ArtistHeader {
|
pub struct ArtistHeader {
|
||||||
pub name: Option<String>,
|
pub name: String,
|
||||||
pub num_albums: u32,
|
pub num_own_albums: u32,
|
||||||
|
pub num_appearances: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<index::ArtistHeader> for ArtistHeader {
|
impl From<index::ArtistHeader> for ArtistHeader {
|
||||||
fn from(a: index::ArtistHeader) -> Self {
|
fn from(a: index::ArtistHeader) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: a.name.map(|s| s.into_inner()),
|
name: a.name.to_string(),
|
||||||
num_albums: a.num_albums,
|
num_own_albums: a.num_own_albums,
|
||||||
|
num_appearances: a.num_appearances,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue