Genre endpoints WIP

This commit is contained in:
Antoine Gersant 2024-09-29 12:06:39 -07:00
parent cb35ef0ebb
commit e06f79c500
9 changed files with 240 additions and 53 deletions

View file

@ -104,6 +104,8 @@ pub enum Error {
ArtistNotFound,
#[error("Album not found")]
AlbumNotFound,
#[error("Genre not found")]
GenreNotFound,
#[error("Song not found")]
SongNotFound,
#[error("Invalid search query syntax")]

View file

@ -20,8 +20,8 @@ mod search;
mod storage;
pub use browser::File;
pub use collection::{Album, AlbumHeader, Artist, ArtistHeader, Song};
use storage::{store_song, AlbumKey, ArtistKey, InternPath, SongKey};
pub use collection::{Album, AlbumHeader, Artist, ArtistHeader, Genre, GenreHeader, Song};
use storage::{store_song, AlbumKey, ArtistKey, GenreKey, InternPath, SongKey};
#[derive(Clone)]
pub struct Manager {
@ -108,6 +108,38 @@ impl Manager {
.unwrap()
}
pub async fn get_genres(&self) -> Vec<GenreHeader> {
spawn_blocking({
let index_manager = self.clone();
move || {
let index = index_manager.index.read().unwrap();
index.collection.get_genres(&index.strings)
}
})
.await
.unwrap()
}
pub async fn get_genre(&self, name: String) -> Result<Genre, Error> {
spawn_blocking({
let index_manager = self.clone();
move || {
let index = index_manager.index.read().unwrap();
let name = index
.strings
.get(&name)
.ok_or_else(|| Error::GenreNotFound)?;
let genre_key = GenreKey { name };
index
.collection
.get_genre(&index.strings, genre_key)
.ok_or_else(|| Error::GenreNotFound)
}
})
.await
.unwrap()
}
pub async fn get_albums(&self) -> Vec<AlbumHeader> {
spawn_blocking({
let index_manager = self.clone();
@ -137,12 +169,11 @@ impl Manager {
let index_manager = self.clone();
move || {
let index = index_manager.index.read().unwrap();
let artist_key = ArtistKey {
name: match name.as_str() {
"" => None,
s => index.strings.get(s),
},
};
let name = index
.strings
.get(name)
.ok_or_else(|| Error::ArtistNotFound)?;
let artist_key = ArtistKey { name };
index
.collection
.get_artist(&index.strings, artist_key)
@ -166,6 +197,7 @@ impl Manager {
artists: artists
.into_iter()
.filter_map(|a| index.strings.get(a))
.map(|k| ArtistKey { name: k })
.collect(),
name,
};

View file

@ -4,16 +4,27 @@ use std::{
path::PathBuf,
};
use lasso2::{RodeoReader, Spur};
use lasso2::RodeoReader;
use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
use serde::{Deserialize, Serialize};
use tinyvec::TinyVec;
use unicase::UniCase;
use crate::app::index::storage::{self, AlbumKey, ArtistKey, SongKey};
use crate::app::index::storage::{self, AlbumKey, ArtistKey, GenreKey, SongKey};
use super::storage::fetch_song;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct GenreHeader {
pub name: String,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Genre {
pub header: GenreHeader,
pub songs: Vec<Song>,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ArtistHeader {
pub name: UniCase<String>,
@ -70,6 +81,7 @@ pub struct Song {
pub struct Collection {
artists: HashMap<ArtistKey, storage::Artist>,
albums: HashMap<AlbumKey, storage::Album>,
genres: HashMap<GenreKey, storage::Genre>,
songs: HashMap<SongKey, storage::Song>,
recent_albums: Vec<AlbumKey>,
}
@ -178,6 +190,32 @@ impl Collection {
.collect()
}
pub fn get_genres(&self, strings: &RodeoReader) -> Vec<GenreHeader> {
let mut genres = self
.genres
.values()
.map(|a| make_genre_header(a, strings))
.collect::<Vec<_>>();
genres.sort_by(|a, b| a.name.cmp(&b.name));
genres
}
pub fn get_genre(&self, strings: &RodeoReader, genre_key: GenreKey) -> Option<Genre> {
self.genres.get(&genre_key).map(|genre| {
let songs = genre
.songs
.iter()
.filter_map(|k| self.get_song(strings, *k))
.collect::<Vec<_>>();
// TODO sort songs
Genre {
header: make_genre_header(genre, strings),
songs,
}
})
}
pub fn get_song(&self, strings: &RodeoReader, song_key: SongKey) -> Option<Song> {
self.songs.get(&song_key).map(|s| fetch_song(strings, s))
}
@ -194,7 +232,7 @@ fn make_album_header(album: &storage::Album, strings: &RodeoReader) -> AlbumHead
artists: album
.artists
.iter()
.map(|a| strings.resolve(a).to_string())
.map(|a| strings.resolve(&a.name).to_string())
.collect(),
year: album.year,
date_added: album.date_added,
@ -217,10 +255,17 @@ fn make_artist_header(artist: &storage::Artist, strings: &RodeoReader) -> Artist
}
}
fn make_genre_header(genre: &storage::Genre, strings: &RodeoReader) -> GenreHeader {
GenreHeader {
name: strings.resolve(&genre.name).to_string(),
}
}
#[derive(Default)]
pub struct Builder {
artists: HashMap<ArtistKey, storage::Artist>,
albums: HashMap<AlbumKey, storage::Album>,
genres: HashMap<GenreKey, storage::Genre>,
songs: HashMap<SongKey, storage::Song>,
}
@ -228,6 +273,7 @@ impl Builder {
pub fn add_song(&mut self, song: &storage::Song) {
self.add_song_to_album(&song);
self.add_song_to_artists(&song);
self.add_song_to_genres(&song);
self.songs.insert(
SongKey {
@ -249,6 +295,7 @@ impl Builder {
Collection {
artists: self.artists,
albums: self.albums,
genres: self.genres,
songs: self.songs,
recent_albums,
}
@ -257,39 +304,39 @@ impl Builder {
fn add_song_to_artists(&mut self, song: &storage::Song) {
let album_key = song.album_key();
let mut all_artists = TinyVec::<[Spur; 8]>::new();
let mut all_artists = TinyVec::<[ArtistKey; 8]>::new();
for name in &song.album_artists {
all_artists.push(*name);
for artist_key in &song.album_artists {
all_artists.push(*artist_key);
if let Some(album_key) = &album_key {
let artist = self.get_or_create_artist(*name);
let artist = self.get_or_create_artist(*artist_key);
artist.albums_as_performer.insert(album_key.clone());
}
}
for name in &song.composers {
all_artists.push(*name);
for artist_key in &song.composers {
all_artists.push(*artist_key);
if let Some(album_key) = &album_key {
let artist = self.get_or_create_artist(*name);
let artist = self.get_or_create_artist(*artist_key);
artist.albums_as_composer.insert(album_key.clone());
}
}
for name in &song.lyricists {
all_artists.push(*name);
for artist_key in &song.lyricists {
all_artists.push(*artist_key);
if let Some(album_key) = &album_key {
let artist = self.get_or_create_artist(*name);
let artist = self.get_or_create_artist(*artist_key);
artist.albums_as_lyricist.insert(album_key.clone());
}
}
for name in &song.artists {
all_artists.push(*name);
for artist_key in &song.artists {
all_artists.push(*artist_key);
if let Some(album_key) = &album_key {
let artist = self.get_or_create_artist(*name);
let artist = self.get_or_create_artist(*artist_key);
if song.album_artists.is_empty() {
artist.albums_as_performer.insert(album_key.clone());
} else if !song.album_artists.contains(name) {
} else if !song.album_artists.contains(artist_key) {
artist
.albums_as_additional_performer
.insert(album_key.clone());
@ -297,8 +344,8 @@ impl Builder {
}
}
for name in all_artists {
let artist = self.get_or_create_artist(name);
for artist_key in all_artists {
let artist = self.get_or_create_artist(artist_key);
artist.num_songs += 1;
if let Some(album_key) = &album_key {
artist.all_albums.insert(album_key.clone());
@ -313,12 +360,11 @@ impl Builder {
}
}
fn get_or_create_artist(&mut self, name: lasso2::Spur) -> &mut storage::Artist {
let artist_key = ArtistKey { name: Some(name) };
fn get_or_create_artist(&mut self, artist_key: ArtistKey) -> &mut storage::Artist {
self.artists
.entry(artist_key)
.or_insert_with(|| storage::Artist {
name,
name: artist_key.name,
all_albums: HashSet::new(),
albums_as_performer: HashSet::new(),
albums_as_additional_performer: HashSet::new(),
@ -359,6 +405,19 @@ impl Builder {
virtual_path: song.virtual_path,
});
}
fn add_song_to_genres(&mut self, song: &storage::Song) {
for name in &song.genres {
let genre_key = GenreKey { name: *name };
let genre = self.genres.entry(genre_key).or_insert(storage::Genre {
name: *name,
songs: Vec::new(),
});
genre.songs.push(SongKey {
virtual_path: song.virtual_path,
});
}
}
}
#[cfg(test)]
@ -721,7 +780,7 @@ mod test {
let artist = collection.get_artist(
&strings,
ArtistKey {
name: strings.get("Stratovarius"),
name: strings.get("Stratovarius").unwrap(),
},
);
@ -787,7 +846,9 @@ mod test {
let album = collection.get_album(
&strings,
AlbumKey {
artists: tiny_vec![strings.get("FSOL").unwrap()],
artists: tiny_vec![ArtistKey {
name: strings.get("FSOL").unwrap()
}],
name: strings.get("Lifeforms").unwrap(),
},
);

View file

@ -282,24 +282,24 @@ impl Builder {
self.text_fields[TextField::Album].insert(str, spur, song_key);
}
for (str, spur) in scanner_song
for (str, artist_key) in scanner_song
.album_artists
.iter()
.zip(storage_song.album_artists.iter())
{
self.text_fields[TextField::AlbumArtist].insert(str, *spur, song_key);
self.text_fields[TextField::AlbumArtist].insert(str, artist_key.name, song_key);
}
for (str, spur) in scanner_song.artists.iter().zip(storage_song.artists.iter()) {
self.text_fields[TextField::Artist].insert(str, *spur, song_key);
for (str, artist_key) in scanner_song.artists.iter().zip(storage_song.artists.iter()) {
self.text_fields[TextField::Artist].insert(str, artist_key.name, song_key);
}
for (str, spur) in scanner_song
for (str, artist_key) in scanner_song
.composers
.iter()
.zip(storage_song.composers.iter())
{
self.text_fields[TextField::Composer].insert(str, *spur, song_key);
self.text_fields[TextField::Composer].insert(str, artist_key.name, song_key);
}
if let Some(disc_number) = &scanner_song.disc_number {
@ -314,12 +314,12 @@ impl Builder {
self.text_fields[TextField::Label].insert(str, *spur, song_key);
}
for (str, spur) in scanner_song
for (str, artist_key) in scanner_song
.lyricists
.iter()
.zip(storage_song.lyricists.iter())
{
self.text_fields[TextField::Lyricist].insert(str, *spur, song_key);
self.text_fields[TextField::Lyricist].insert(str, artist_key.name, song_key);
}
self.text_fields[TextField::Path].insert(

View file

@ -16,6 +16,12 @@ pub enum File {
Song(PathKey),
}
#[derive(Serialize, Deserialize)]
pub struct Genre {
pub name: Spur,
pub songs: Vec<SongKey>,
}
#[derive(Serialize, Deserialize)]
pub struct Artist {
pub name: Spur,
@ -32,7 +38,7 @@ pub struct Artist {
pub struct Album {
pub name: Spur,
pub artwork: Option<PathKey>,
pub artists: TinyVec<[Spur; 1]>,
pub artists: TinyVec<[ArtistKey; 1]>,
pub year: Option<i64>,
pub date_added: i64,
pub songs: HashSet<SongKey>,
@ -45,14 +51,14 @@ pub struct Song {
pub track_number: Option<i64>,
pub disc_number: Option<i64>,
pub title: Option<Spur>,
pub artists: TinyVec<[Spur; 1]>,
pub album_artists: TinyVec<[Spur; 1]>,
pub artists: TinyVec<[ArtistKey; 1]>,
pub album_artists: TinyVec<[ArtistKey; 1]>,
pub year: Option<i64>,
pub album: Option<Spur>,
pub artwork: Option<PathKey>,
pub duration: Option<i64>,
pub lyricists: TinyVec<[Spur; 0]>,
pub composers: TinyVec<[Spur; 0]>,
pub lyricists: TinyVec<[ArtistKey; 0]>,
pub composers: TinyVec<[ArtistKey; 0]>,
pub genres: TinyVec<[Spur; 1]>,
pub labels: TinyVec<[Spur; 0]>,
pub date_added: i64,
@ -63,14 +69,19 @@ pub struct Song {
)]
pub struct PathKey(pub Spur);
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct GenreKey {
pub name: Spur,
}
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct ArtistKey {
pub name: Option<Spur>,
pub name: Spur,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct AlbumKey {
pub artists: TinyVec<[Spur; 4]>,
pub artists: TinyVec<[ArtistKey; 4]>,
pub name: Spur,
}
@ -151,11 +162,17 @@ pub fn store_song(
track_number: song.track_number,
disc_number: song.disc_number,
title: song.title.as_ref().and_then(&mut canonicalize),
artists: song.artists.iter().filter_map(&mut canonicalize).collect(),
artists: song
.artists
.iter()
.filter_map(&mut canonicalize)
.map(|k| ArtistKey { name: k })
.collect(),
album_artists: song
.album_artists
.iter()
.filter_map(&mut canonicalize)
.map(|k| ArtistKey { name: k })
.collect(),
year: song.year,
album: song.album.as_ref().and_then(&mut canonicalize),
@ -165,11 +182,13 @@ pub fn store_song(
.lyricists
.iter()
.filter_map(&mut canonicalize)
.map(|k| ArtistKey { name: k })
.collect(),
composers: song
.composers
.iter()
.filter_map(&mut canonicalize)
.map(|k| ArtistKey { name: k })
.collect(),
genres: song.genres.iter().filter_map(&mut canonicalize).collect(),
labels: song.labels.iter().filter_map(&mut canonicalize).collect(),
@ -187,12 +206,12 @@ pub fn fetch_song(strings: &RodeoReader, song: &Song) -> super::Song {
artists: song
.artists
.iter()
.map(|s| strings.resolve(&s).to_string())
.map(|k| strings.resolve(&k.name).to_string())
.collect(),
album_artists: song
.album_artists
.iter()
.map(|s| strings.resolve(&s).to_string())
.map(|k| strings.resolve(&k.name).to_string())
.collect(),
year: song.year,
album: song.album.map(|s| strings.resolve(&s).to_string()),
@ -201,12 +220,12 @@ pub fn fetch_song(strings: &RodeoReader, song: &Song) -> super::Song {
lyricists: song
.lyricists
.iter()
.map(|s| strings.resolve(&s).to_string())
.map(|k| strings.resolve(&k.name).to_string())
.collect(),
composers: song
.composers
.iter()
.map(|s| strings.resolve(&s).to_string())
.map(|k| strings.resolve(&k.name).to_string())
.collect(),
genres: song
.genres

View file

@ -59,6 +59,9 @@ pub fn router() -> Router<App> {
.route("/artists", get(get_artists))
.route("/artists/:artist", get(get_artist))
.route("/artists/:artists/albums/:name", get(get_album))
.route("/genres", get(get_genres))
.route("/genres/:genre", get(get_genre))
.route("/genres/:genre/songs", get(get_genre_songs))
.route("/random", get(get_random_albums)) // Deprecated
.route("/recent", get(get_recent_albums)) // Deprecated
// Search
@ -508,6 +511,45 @@ async fn get_recent_albums(
albums_to_response(albums, api_version)
}
async fn get_genres(
_auth: Auth,
State(index_manager): State<index::Manager>,
) -> Result<Json<Vec<dto::GenreHeader>>, APIError> {
Ok(Json(
index_manager
.get_genres()
.await
.into_iter()
.map(|g| g.into())
.collect(),
))
}
async fn get_genre(
_auth: Auth,
State(index_manager): State<index::Manager>,
Path(genre): Path<String>,
) -> Result<Json<dto::Genre>, APIError> {
Ok(Json(index_manager.get_genre(genre).await?.into()))
}
async fn get_genre_songs(
_auth: Auth,
State(index_manager): State<index::Manager>,
Path(genre): Path<String>,
) -> Result<Json<dto::SongList>, APIError> {
let songs = index_manager.get_genre(genre).await?.songs;
let song_list = dto::SongList {
paths: songs.iter().map(|s| s.virtual_path.clone()).collect(),
first_songs: songs
.into_iter()
.take(SONG_LIST_CAPACITY)
.map(|s| s.into())
.collect(),
};
Ok(Json(song_list))
}
async fn get_search(
_auth: Auth,
api_version: APIMajorVersion,

View file

@ -23,6 +23,7 @@ impl IntoResponse for APIError {
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
APIError::ArtistNotFound => StatusCode::NOT_FOUND,
APIError::AlbumNotFound => StatusCode::NOT_FOUND,
APIError::GenreNotFound => StatusCode::NOT_FOUND,
APIError::SongNotFound => StatusCode::NOT_FOUND,
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
APIError::EmptyPassword => StatusCode::BAD_REQUEST,

View file

@ -317,6 +317,33 @@ impl From<index::File> for BrowserEntry {
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenreHeader {
pub name: String,
}
impl From<index::GenreHeader> for GenreHeader {
fn from(g: index::GenreHeader) -> Self {
Self {
name: g.name.to_string(),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Genre {
#[serde(flatten)]
pub header: GenreHeader,
}
impl From<index::Genre> for Genre {
fn from(genre: index::Genre) -> Self {
Self {
header: GenreHeader::from(genre.header),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtistHeader {
pub name: String,

View file

@ -29,6 +29,8 @@ pub enum APIError {
ArtistNotFound,
#[error("Album not found")]
AlbumNotFound,
#[error("Genre not found")]
GenreNotFound,
#[error("Song not found")]
SongNotFound,
#[error("DDNS update query failed with HTTP status {0}")]
@ -137,6 +139,7 @@ impl From<app::Error> for APIError {
app::Error::DirectoryNotFound(d) => APIError::DirectoryNotFound(d),
app::Error::ArtistNotFound => APIError::ArtistNotFound,
app::Error::AlbumNotFound => APIError::AlbumNotFound,
app::Error::GenreNotFound => APIError::GenreNotFound,
app::Error::SongNotFound => APIError::SongNotFound,
app::Error::PlaylistNotFound => APIError::PlaylistNotFound,
app::Error::SearchQueryParseError => APIError::SearchQueryParseError,