Implements artists/ endoint

This commit is contained in:
Antoine Gersant 2024-08-10 10:30:21 -07:00
parent bc3ed59382
commit 0afab8d634
7 changed files with 166 additions and 90 deletions

1
Cargo.lock generated
View file

@ -1649,6 +1649,7 @@ dependencies = [
"toml 0.8.19", "toml 0.8.19",
"tower-http", "tower-http",
"trie-rs", "trie-rs",
"unicase",
"ureq 2.10.0", "ureq 2.10.0",
"winres", "winres",
] ]

View file

@ -44,6 +44,7 @@ tokio-util = { version = "0.7.11", features = ["io"] }
toml = "0.8.19" toml = "0.8.19"
tower-http = { version = "0.5.2", features = ["fs"] } tower-http = { version = "0.5.2", features = ["fs"] }
trie-rs = { version = "0.4.2", features = ["serde"] } trie-rs = { version = "0.4.2", features = ["serde"] }
unicase = "2.7.0"
ureq = { version = "2.10.0", default-features = false, features = ["tls"] } ureq = { version = "2.10.0", default-features = false, features = ["tls"] }
[dependencies.axum] [dependencies.axum]

View file

@ -17,7 +17,7 @@ mod search;
mod storage; mod storage;
pub use browser::File; pub use browser::File;
pub use collection::{Album, Artist, Song}; pub use collection::{Album, Artist, ArtistHeader, Song};
use storage::{AlbumKey, ArtistKey, InternPath, SongKey}; use storage::{AlbumKey, ArtistKey, InternPath, SongKey};
#[derive(Clone)] #[derive(Clone)]
@ -105,6 +105,18 @@ impl Manager {
.unwrap() .unwrap()
} }
pub async fn get_artists(&self) -> Vec<ArtistHeader> {
spawn_blocking({
let index_manager = self.clone();
move || {
let index = index_manager.index.read().unwrap();
index.collection.get_artists(&index.strings)
}
})
.await
.unwrap()
}
pub async fn get_artist(&self, name: String) -> Result<Artist, Error> { pub async fn get_artist(&self, name: String) -> Result<Artist, Error> {
spawn_blocking({ spawn_blocking({
let index_manager = self.clone(); let index_manager = self.clone();

View file

@ -1,5 +1,6 @@
use std::{ use std::{
borrow::BorrowMut, borrow::BorrowMut,
cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::PathBuf, path::PathBuf,
}; };
@ -7,6 +8,7 @@ use std::{
use lasso2::{Rodeo, RodeoReader}; use lasso2::{Rodeo, RodeoReader};
use rand::{rngs::ThreadRng, seq::IteratorRandom}; use rand::{rngs::ThreadRng, seq::IteratorRandom};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicase::UniCase;
use crate::app::index::storage::{self, store_song, AlbumKey, ArtistKey, SongKey}; use crate::app::index::storage::{self, store_song, AlbumKey, ArtistKey, SongKey};
use crate::app::scanner; use crate::app::scanner;
@ -14,12 +16,16 @@ use crate::app::scanner;
use super::storage::fetch_song; use super::storage::fetch_song;
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq)]
pub struct Artist { pub struct ArtistHeader {
pub name: Option<String>, pub name: Option<String>,
pub num_albums: u32,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Artist {
pub header: ArtistHeader,
pub albums: Vec<Album>, pub albums: Vec<Album>,
pub song_credits: Vec<Album>, // Albums where this artist shows up as `artist` without being `album_artist` pub featured_on: Vec<Album>, // Albums where this artist shows up as `artist` without being `album_artist`
pub composer_credits: Vec<Album>, // Albums where this artist shows up as `composer` without being `artist` or `album_artist`
pub lyricist_credits: Vec<Album>, // Albums where this artist shows up as `lyricist` without being `artist` or `album_artist`
} }
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq)]
@ -61,44 +67,39 @@ pub struct Collection {
} }
impl Collection { impl Collection {
pub fn get_artists(&self, strings: &RodeoReader) -> Vec<ArtistHeader> {
let mut artists = self
.artists
.values()
.filter(|a| a.albums.len() > 0 || a.featured_on.len() > 1)
.map(|a| make_artist_header(a, strings))
.collect::<Vec<_>>();
artists.sort_by(|a, b| match (&a.name, &b.name) {
(Some(a), Some(b)) => UniCase::new(a).cmp(&UniCase::new(b)),
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
});
artists
}
pub fn get_artist(&self, strings: &RodeoReader, artist_key: ArtistKey) -> Option<Artist> { pub fn get_artist(&self, strings: &RodeoReader, artist_key: ArtistKey) -> Option<Artist> {
self.artists.get(&artist_key).map(|a| { self.artists.get(&artist_key).map(|a| {
let sort_albums = |a: &Album, b: &Album| (&a.year, &a.name).cmp(&(&b.year, &b.name));
let albums = { let albums = {
let mut albums = a let mut albums = a
.albums .albums
.iter() .iter()
.filter_map(|key| self.get_album(strings, key.clone())) .filter_map(|key| self.get_album(strings, key.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
albums.sort_by(|a, b| (a.year, &a.name).partial_cmp(&(b.year, &b.name)).unwrap()); albums.sort_by(sort_albums);
albums albums
}; };
let sort_albums = let featured_on = {
|a: &Album, b: &Album| (&a.year, &a.name).partial_cmp(&(&b.year, &b.name)).unwrap();
let song_credits = {
let mut albums = a let mut albums = a
.song_credits .featured_on
.iter()
.filter_map(|key| self.get_album(strings, key.clone()))
.collect::<Vec<_>>();
albums.sort_by(&sort_albums);
albums
};
let composer_credits = {
let mut albums = a
.composer_credits
.iter()
.filter_map(|key| self.get_album(strings, key.clone()))
.collect::<Vec<_>>();
albums.sort_by(&sort_albums);
albums
};
let lyricist_credits = {
let mut albums = a
.lyricist_credits
.iter() .iter()
.filter_map(|key| self.get_album(strings, key.clone())) .filter_map(|key| self.get_album(strings, key.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -107,11 +108,9 @@ impl Collection {
}; };
Artist { Artist {
name: a.name.map(|s| strings.resolve(&s).to_string()), header: make_artist_header(a, strings),
albums, albums,
song_credits, featured_on,
composer_credits,
lyricist_credits,
} }
}) })
} }
@ -174,6 +173,13 @@ impl Collection {
} }
} }
fn make_artist_header(artist: &storage::Artist, strings: &RodeoReader) -> ArtistHeader {
ArtistHeader {
name: artist.name.map(|n| strings.resolve(&n).to_owned()),
num_albums: artist.albums.len() as u32 + artist.featured_on.len() as u32,
}
}
#[derive(Default)] #[derive(Default)]
pub struct Builder { pub struct Builder {
artists: HashMap<ArtistKey, storage::Artist>, artists: HashMap<ArtistKey, storage::Artist>,
@ -228,25 +234,7 @@ impl Builder {
if song.album_artists.is_empty() { if song.album_artists.is_empty() {
artist.albums.insert(album_key.clone()); artist.albums.insert(album_key.clone());
} else if !song.album_artists.contains(name) { } else if !song.album_artists.contains(name) {
artist.song_credits.insert(album_key.clone()); artist.featured_on.insert(album_key.clone());
}
}
for name in &song.composers {
let is_also_artist = song.artists.contains(name);
let is_also_album_artist = song.artists.contains(name);
if !is_also_artist && !is_also_album_artist {
let artist = self.get_or_create_artist(*name);
artist.composer_credits.insert(album_key.clone());
}
}
for name in &song.lyricists {
let is_also_artist = song.artists.contains(name);
let is_also_album_artist = song.artists.contains(name);
if !is_also_artist && !is_also_album_artist {
let artist = self.get_or_create_artist(*name);
artist.lyricist_credits.insert(album_key.clone());
} }
} }
} }
@ -258,9 +246,7 @@ impl Builder {
.or_insert_with(|| storage::Artist { .or_insert_with(|| storage::Artist {
name: Some(name), name: Some(name),
albums: HashSet::new(), albums: HashSet::new(),
song_credits: HashSet::new(), featured_on: HashSet::new(),
composer_credits: HashSet::new(),
lyricist_credits: HashSet::new(),
}) })
.borrow_mut() .borrow_mut()
} }
@ -317,6 +303,61 @@ mod test {
(browser, strings) (browser, strings)
} }
#[tokio::test]
async fn can_list_artists() {
let (collection, strings) = setup_test(Vec::from([
scanner::Song {
virtual_path: PathBuf::from("Kai.mp3"),
title: Some("Kai".to_owned()),
artists: vec!["FSOL".to_owned()],
..Default::default()
},
scanner::Song {
virtual_path: PathBuf::from("Fantasy.mp3"),
title: Some("Fantasy".to_owned()),
artists: vec!["Stratovarius".to_owned()],
..Default::default()
},
]));
let artists = collection
.get_artists(&strings)
.into_iter()
.map(|a| a.name.unwrap())
.collect::<Vec<_>>();
assert_eq!(artists, vec!["FSOL".to_owned(), "Stratovarius".to_owned()]);
}
#[tokio::test]
async fn artist_list_is_sorted() {
let (collection, strings) = setup_test(Vec::from([
scanner::Song {
virtual_path: PathBuf::from("Destiny.mp3"),
title: Some("Destiny".to_owned()),
artists: vec!["Heavenly".to_owned()],
..Default::default()
},
scanner::Song {
virtual_path: PathBuf::from("Renegade.mp3"),
title: Some("Renegade".to_owned()),
artists: vec!["hammerfall".to_owned()], // Lower-case `h` to validate sorting is case-insensitive
..Default::default()
},
]));
let artists = collection
.get_artists(&strings)
.into_iter()
.map(|a| a.name.unwrap())
.collect::<Vec<_>>();
assert_eq!(
artists,
vec!["hammerfall".to_owned(), "Heavenly".to_owned()]
);
}
#[tokio::test] #[tokio::test]
async fn can_get_random_albums() { async fn can_get_random_albums() {
let (collection, strings) = setup_test(Vec::from([ let (collection, strings) = setup_test(Vec::from([
@ -381,10 +422,9 @@ mod test {
artists: Vec<String>, artists: Vec<String>,
composers: Vec<String>, composers: Vec<String>,
lyricists: Vec<String>, lyricists: Vec<String>,
expect_unlisted: bool,
expect_albums: bool, expect_albums: bool,
expect_song_credits: bool, expect_featured_on: bool,
expect_composer_credits: bool,
expect_lyricist_credits: bool,
} }
let test_cases = vec![ let test_cases = vec![
@ -407,7 +447,7 @@ mod test {
TestCase { TestCase {
album_artists: vec![other_artist_name.to_string()], album_artists: vec![other_artist_name.to_string()],
artists: vec![artist_name.to_string()], artists: vec![artist_name.to_string()],
expect_song_credits: true, expect_featured_on: true,
..Default::default() ..Default::default()
}, },
// Tagged as artist and within album artists // Tagged as artist and within album artists
@ -421,14 +461,14 @@ mod test {
TestCase { TestCase {
artists: vec![other_artist_name.to_string()], artists: vec![other_artist_name.to_string()],
composers: vec![artist_name.to_string()], composers: vec![artist_name.to_string()],
expect_composer_credits: true, expect_unlisted: true,
..Default::default() ..Default::default()
}, },
// Only tagged as lyricist // Only tagged as lyricist
TestCase { TestCase {
artists: vec![other_artist_name.to_string()], artists: vec![other_artist_name.to_string()],
lyricists: vec![artist_name.to_string()], lyricists: vec![artist_name.to_string()],
expect_lyricist_credits: true, expect_unlisted: true,
..Default::default() ..Default::default()
}, },
]; ];
@ -447,7 +487,15 @@ mod test {
let artist_key = ArtistKey { let artist_key = ArtistKey {
name: strings.get(artist_name), name: strings.get(artist_name),
}; };
let artist = collection.get_artist(&strings, artist_key).unwrap(); let artist = collection.get_artist(&strings, artist_key);
let artist = match test.expect_unlisted {
true => {
assert!(artist.is_none());
continue;
}
false => artist.unwrap(),
};
let names = |a: &Vec<Album>| { let names = |a: &Vec<Album>| {
a.iter() a.iter()
@ -461,22 +509,10 @@ mod test {
assert!(names(&artist.albums).is_empty()); assert!(names(&artist.albums).is_empty());
} }
if test.expect_song_credits { if test.expect_featured_on {
assert_eq!(names(&artist.song_credits), vec![album_name]); assert_eq!(names(&artist.featured_on), vec![album_name]);
} else { } else {
assert!(names(&artist.song_credits).is_empty()); assert!(names(&artist.featured_on).is_empty());
}
if test.expect_composer_credits {
assert_eq!(names(&artist.composer_credits), vec![album_name]);
} else {
assert!(names(&artist.composer_credits).is_empty());
}
if test.expect_lyricist_credits {
assert_eq!(names(&artist.lyricist_credits), vec![album_name]);
} else {
assert!(names(&artist.lyricist_credits).is_empty());
} }
} }
} }

View file

@ -20,9 +20,7 @@ pub enum File {
pub struct Artist { pub struct Artist {
pub name: Option<lasso2::Spur>, pub name: Option<lasso2::Spur>,
pub albums: HashSet<AlbumKey>, pub albums: HashSet<AlbumKey>,
pub song_credits: HashSet<AlbumKey>, pub featured_on: HashSet<AlbumKey>,
pub composer_credits: HashSet<AlbumKey>,
pub lyricist_credits: HashSet<AlbumKey>,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]

View file

@ -45,6 +45,7 @@ pub fn router() -> Router<App> {
.route("/browse/*path", get(get_browse)) .route("/browse/*path", get(get_browse))
.route("/flatten", get(get_flatten_root)) .route("/flatten", get(get_flatten_root))
.route("/flatten/*path", get(get_flatten)) .route("/flatten/*path", get(get_flatten))
.route("/artists", get(get_artists))
.route("/artists/:artist", get(get_artist)) .route("/artists/:artist", get(get_artist))
.route("/artists/:artists/albums/:name", get(get_album)) .route("/artists/:artists/albums/:name", get(get_album))
.route("/random", get(get_random)) .route("/random", get(get_random))
@ -364,6 +365,21 @@ async fn get_flatten(
songs_to_response(songs, api_version) songs_to_response(songs, api_version)
} }
async fn get_artists(
_auth: Auth,
State(index_manager): State<index::Manager>,
) -> Result<Json<Vec<dto::ArtistHeader>>, APIError> {
Ok(Json(
index_manager
.get_artists()
.await
.into_iter()
.map(|a| a.into())
.collect::<Vec<_>>()
.into(),
))
}
async fn get_artist( async fn get_artist(
_auth: Auth, _auth: Auth,
State(index_manager): State<index::Manager>, State(index_manager): State<index::Manager>,

View file

@ -307,22 +307,34 @@ impl From<index::File> for BrowserEntry {
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Artist { pub struct ArtistHeader {
pub name: Option<String>, pub name: Option<String>,
pub num_albums: u32,
}
impl From<index::ArtistHeader> for ArtistHeader {
fn from(a: index::ArtistHeader) -> Self {
Self {
name: a.name,
num_albums: a.num_albums,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Artist {
#[serde(flatten)]
pub header: ArtistHeader,
pub albums: Vec<AlbumHeader>, pub albums: Vec<AlbumHeader>,
pub song_credits: Vec<AlbumHeader>, pub featured_on: Vec<AlbumHeader>,
pub composer_credits: Vec<AlbumHeader>,
pub lyricist_credits: Vec<AlbumHeader>,
} }
impl From<index::Artist> for Artist { impl From<index::Artist> for Artist {
fn from(a: index::Artist) -> Self { fn from(a: index::Artist) -> Self {
Self { Self {
name: a.name, header: a.header.into(),
albums: a.albums.into_iter().map(|a| a.into()).collect(), albums: a.albums.into_iter().map(|a| a.into()).collect(),
song_credits: a.song_credits.into_iter().map(|a| a.into()).collect(), featured_on: a.featured_on.into_iter().map(|a| a.into()).collect(),
composer_credits: a.composer_credits.into_iter().map(|a| a.into()).collect(),
lyricist_credits: a.lyricist_credits.into_iter().map(|a| a.into()).collect(),
} }
} }
} }