Genre endpoints WIP
This commit is contained in:
parent
cb35ef0ebb
commit
e06f79c500
9 changed files with 240 additions and 53 deletions
|
@ -104,6 +104,8 @@ pub enum Error {
|
||||||
ArtistNotFound,
|
ArtistNotFound,
|
||||||
#[error("Album not found")]
|
#[error("Album not found")]
|
||||||
AlbumNotFound,
|
AlbumNotFound,
|
||||||
|
#[error("Genre not found")]
|
||||||
|
GenreNotFound,
|
||||||
#[error("Song not found")]
|
#[error("Song not found")]
|
||||||
SongNotFound,
|
SongNotFound,
|
||||||
#[error("Invalid search query syntax")]
|
#[error("Invalid search query syntax")]
|
||||||
|
|
|
@ -20,8 +20,8 @@ mod search;
|
||||||
mod storage;
|
mod storage;
|
||||||
|
|
||||||
pub use browser::File;
|
pub use browser::File;
|
||||||
pub use collection::{Album, AlbumHeader, Artist, ArtistHeader, Song};
|
pub use collection::{Album, AlbumHeader, Artist, ArtistHeader, Genre, GenreHeader, Song};
|
||||||
use storage::{store_song, AlbumKey, ArtistKey, InternPath, SongKey};
|
use storage::{store_song, AlbumKey, ArtistKey, GenreKey, InternPath, SongKey};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Manager {
|
pub struct Manager {
|
||||||
|
@ -108,6 +108,38 @@ impl Manager {
|
||||||
.unwrap()
|
.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> {
|
pub async fn get_albums(&self) -> Vec<AlbumHeader> {
|
||||||
spawn_blocking({
|
spawn_blocking({
|
||||||
let index_manager = self.clone();
|
let index_manager = self.clone();
|
||||||
|
@ -137,12 +169,11 @@ impl Manager {
|
||||||
let index_manager = self.clone();
|
let index_manager = self.clone();
|
||||||
move || {
|
move || {
|
||||||
let index = index_manager.index.read().unwrap();
|
let index = index_manager.index.read().unwrap();
|
||||||
let artist_key = ArtistKey {
|
let name = index
|
||||||
name: match name.as_str() {
|
.strings
|
||||||
"" => None,
|
.get(name)
|
||||||
s => index.strings.get(s),
|
.ok_or_else(|| Error::ArtistNotFound)?;
|
||||||
},
|
let artist_key = ArtistKey { name };
|
||||||
};
|
|
||||||
index
|
index
|
||||||
.collection
|
.collection
|
||||||
.get_artist(&index.strings, artist_key)
|
.get_artist(&index.strings, artist_key)
|
||||||
|
@ -166,6 +197,7 @@ impl Manager {
|
||||||
artists: artists
|
artists: artists
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|a| index.strings.get(a))
|
.filter_map(|a| index.strings.get(a))
|
||||||
|
.map(|k| ArtistKey { name: k })
|
||||||
.collect(),
|
.collect(),
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,16 +4,27 @@ use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lasso2::{RodeoReader, Spur};
|
use lasso2::RodeoReader;
|
||||||
use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
|
use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tinyvec::TinyVec;
|
use tinyvec::TinyVec;
|
||||||
use unicase::UniCase;
|
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;
|
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)]
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
pub struct ArtistHeader {
|
pub struct ArtistHeader {
|
||||||
pub name: UniCase<String>,
|
pub name: UniCase<String>,
|
||||||
|
@ -70,6 +81,7 @@ pub struct Song {
|
||||||
pub struct Collection {
|
pub struct Collection {
|
||||||
artists: HashMap<ArtistKey, storage::Artist>,
|
artists: HashMap<ArtistKey, storage::Artist>,
|
||||||
albums: HashMap<AlbumKey, storage::Album>,
|
albums: HashMap<AlbumKey, storage::Album>,
|
||||||
|
genres: HashMap<GenreKey, storage::Genre>,
|
||||||
songs: HashMap<SongKey, storage::Song>,
|
songs: HashMap<SongKey, storage::Song>,
|
||||||
recent_albums: Vec<AlbumKey>,
|
recent_albums: Vec<AlbumKey>,
|
||||||
}
|
}
|
||||||
|
@ -178,6 +190,32 @@ impl Collection {
|
||||||
.collect()
|
.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> {
|
pub fn get_song(&self, strings: &RodeoReader, song_key: SongKey) -> Option<Song> {
|
||||||
self.songs.get(&song_key).map(|s| fetch_song(strings, s))
|
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: album
|
||||||
.artists
|
.artists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| strings.resolve(a).to_string())
|
.map(|a| strings.resolve(&a.name).to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
year: album.year,
|
year: album.year,
|
||||||
date_added: album.date_added,
|
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)]
|
#[derive(Default)]
|
||||||
pub struct Builder {
|
pub struct Builder {
|
||||||
artists: HashMap<ArtistKey, storage::Artist>,
|
artists: HashMap<ArtistKey, storage::Artist>,
|
||||||
albums: HashMap<AlbumKey, storage::Album>,
|
albums: HashMap<AlbumKey, storage::Album>,
|
||||||
|
genres: HashMap<GenreKey, storage::Genre>,
|
||||||
songs: HashMap<SongKey, storage::Song>,
|
songs: HashMap<SongKey, storage::Song>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,6 +273,7 @@ impl Builder {
|
||||||
pub fn add_song(&mut self, song: &storage::Song) {
|
pub fn add_song(&mut self, song: &storage::Song) {
|
||||||
self.add_song_to_album(&song);
|
self.add_song_to_album(&song);
|
||||||
self.add_song_to_artists(&song);
|
self.add_song_to_artists(&song);
|
||||||
|
self.add_song_to_genres(&song);
|
||||||
|
|
||||||
self.songs.insert(
|
self.songs.insert(
|
||||||
SongKey {
|
SongKey {
|
||||||
|
@ -249,6 +295,7 @@ impl Builder {
|
||||||
Collection {
|
Collection {
|
||||||
artists: self.artists,
|
artists: self.artists,
|
||||||
albums: self.albums,
|
albums: self.albums,
|
||||||
|
genres: self.genres,
|
||||||
songs: self.songs,
|
songs: self.songs,
|
||||||
recent_albums,
|
recent_albums,
|
||||||
}
|
}
|
||||||
|
@ -257,39 +304,39 @@ impl Builder {
|
||||||
fn add_song_to_artists(&mut self, song: &storage::Song) {
|
fn add_song_to_artists(&mut self, song: &storage::Song) {
|
||||||
let album_key = song.album_key();
|
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 {
|
for artist_key in &song.album_artists {
|
||||||
all_artists.push(*name);
|
all_artists.push(*artist_key);
|
||||||
if let Some(album_key) = &album_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());
|
artist.albums_as_performer.insert(album_key.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in &song.composers {
|
for artist_key in &song.composers {
|
||||||
all_artists.push(*name);
|
all_artists.push(*artist_key);
|
||||||
if let Some(album_key) = &album_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());
|
artist.albums_as_composer.insert(album_key.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in &song.lyricists {
|
for artist_key in &song.lyricists {
|
||||||
all_artists.push(*name);
|
all_artists.push(*artist_key);
|
||||||
if let Some(album_key) = &album_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());
|
artist.albums_as_lyricist.insert(album_key.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in &song.artists {
|
for artist_key in &song.artists {
|
||||||
all_artists.push(*name);
|
all_artists.push(*artist_key);
|
||||||
if let Some(album_key) = &album_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() {
|
if song.album_artists.is_empty() {
|
||||||
artist.albums_as_performer.insert(album_key.clone());
|
artist.albums_as_performer.insert(album_key.clone());
|
||||||
} else if !song.album_artists.contains(name) {
|
} else if !song.album_artists.contains(artist_key) {
|
||||||
artist
|
artist
|
||||||
.albums_as_additional_performer
|
.albums_as_additional_performer
|
||||||
.insert(album_key.clone());
|
.insert(album_key.clone());
|
||||||
|
@ -297,8 +344,8 @@ impl Builder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in all_artists {
|
for artist_key in all_artists {
|
||||||
let artist = self.get_or_create_artist(name);
|
let artist = self.get_or_create_artist(artist_key);
|
||||||
artist.num_songs += 1;
|
artist.num_songs += 1;
|
||||||
if let Some(album_key) = &album_key {
|
if let Some(album_key) = &album_key {
|
||||||
artist.all_albums.insert(album_key.clone());
|
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 {
|
fn get_or_create_artist(&mut self, artist_key: ArtistKey) -> &mut storage::Artist {
|
||||||
let artist_key = ArtistKey { name: Some(name) };
|
|
||||||
self.artists
|
self.artists
|
||||||
.entry(artist_key)
|
.entry(artist_key)
|
||||||
.or_insert_with(|| storage::Artist {
|
.or_insert_with(|| storage::Artist {
|
||||||
name,
|
name: artist_key.name,
|
||||||
all_albums: HashSet::new(),
|
all_albums: HashSet::new(),
|
||||||
albums_as_performer: HashSet::new(),
|
albums_as_performer: HashSet::new(),
|
||||||
albums_as_additional_performer: HashSet::new(),
|
albums_as_additional_performer: HashSet::new(),
|
||||||
|
@ -359,6 +405,19 @@ impl Builder {
|
||||||
virtual_path: song.virtual_path,
|
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)]
|
#[cfg(test)]
|
||||||
|
@ -721,7 +780,7 @@ mod test {
|
||||||
let artist = collection.get_artist(
|
let artist = collection.get_artist(
|
||||||
&strings,
|
&strings,
|
||||||
ArtistKey {
|
ArtistKey {
|
||||||
name: strings.get("Stratovarius"),
|
name: strings.get("Stratovarius").unwrap(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -787,7 +846,9 @@ mod test {
|
||||||
let album = collection.get_album(
|
let album = collection.get_album(
|
||||||
&strings,
|
&strings,
|
||||||
AlbumKey {
|
AlbumKey {
|
||||||
artists: tiny_vec![strings.get("FSOL").unwrap()],
|
artists: tiny_vec![ArtistKey {
|
||||||
|
name: strings.get("FSOL").unwrap()
|
||||||
|
}],
|
||||||
name: strings.get("Lifeforms").unwrap(),
|
name: strings.get("Lifeforms").unwrap(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -282,24 +282,24 @@ impl Builder {
|
||||||
self.text_fields[TextField::Album].insert(str, spur, song_key);
|
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
|
.album_artists
|
||||||
.iter()
|
.iter()
|
||||||
.zip(storage_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()) {
|
for (str, artist_key) in scanner_song.artists.iter().zip(storage_song.artists.iter()) {
|
||||||
self.text_fields[TextField::Artist].insert(str, *spur, song_key);
|
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
|
.composers
|
||||||
.iter()
|
.iter()
|
||||||
.zip(storage_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 {
|
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);
|
self.text_fields[TextField::Label].insert(str, *spur, song_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (str, spur) in scanner_song
|
for (str, artist_key) in scanner_song
|
||||||
.lyricists
|
.lyricists
|
||||||
.iter()
|
.iter()
|
||||||
.zip(storage_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(
|
self.text_fields[TextField::Path].insert(
|
||||||
|
|
|
@ -16,6 +16,12 @@ pub enum File {
|
||||||
Song(PathKey),
|
Song(PathKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Genre {
|
||||||
|
pub name: Spur,
|
||||||
|
pub songs: Vec<SongKey>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
pub name: Spur,
|
pub name: Spur,
|
||||||
|
@ -32,7 +38,7 @@ pub struct Artist {
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
pub name: Spur,
|
pub name: Spur,
|
||||||
pub artwork: Option<PathKey>,
|
pub artwork: Option<PathKey>,
|
||||||
pub artists: TinyVec<[Spur; 1]>,
|
pub artists: TinyVec<[ArtistKey; 1]>,
|
||||||
pub year: Option<i64>,
|
pub year: Option<i64>,
|
||||||
pub date_added: i64,
|
pub date_added: i64,
|
||||||
pub songs: HashSet<SongKey>,
|
pub songs: HashSet<SongKey>,
|
||||||
|
@ -45,14 +51,14 @@ pub struct Song {
|
||||||
pub track_number: Option<i64>,
|
pub track_number: Option<i64>,
|
||||||
pub disc_number: Option<i64>,
|
pub disc_number: Option<i64>,
|
||||||
pub title: Option<Spur>,
|
pub title: Option<Spur>,
|
||||||
pub artists: TinyVec<[Spur; 1]>,
|
pub artists: TinyVec<[ArtistKey; 1]>,
|
||||||
pub album_artists: TinyVec<[Spur; 1]>,
|
pub album_artists: TinyVec<[ArtistKey; 1]>,
|
||||||
pub year: Option<i64>,
|
pub year: Option<i64>,
|
||||||
pub album: Option<Spur>,
|
pub album: Option<Spur>,
|
||||||
pub artwork: Option<PathKey>,
|
pub artwork: Option<PathKey>,
|
||||||
pub duration: Option<i64>,
|
pub duration: Option<i64>,
|
||||||
pub lyricists: TinyVec<[Spur; 0]>,
|
pub lyricists: TinyVec<[ArtistKey; 0]>,
|
||||||
pub composers: TinyVec<[Spur; 0]>,
|
pub composers: TinyVec<[ArtistKey; 0]>,
|
||||||
pub genres: TinyVec<[Spur; 1]>,
|
pub genres: TinyVec<[Spur; 1]>,
|
||||||
pub labels: TinyVec<[Spur; 0]>,
|
pub labels: TinyVec<[Spur; 0]>,
|
||||||
pub date_added: i64,
|
pub date_added: i64,
|
||||||
|
@ -63,14 +69,19 @@ pub struct Song {
|
||||||
)]
|
)]
|
||||||
pub struct PathKey(pub Spur);
|
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 struct ArtistKey {
|
||||||
pub name: Option<Spur>,
|
pub name: Spur,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct AlbumKey {
|
pub struct AlbumKey {
|
||||||
pub artists: TinyVec<[Spur; 4]>,
|
pub artists: TinyVec<[ArtistKey; 4]>,
|
||||||
pub name: Spur,
|
pub name: Spur,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,11 +162,17 @@ pub fn store_song(
|
||||||
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().and_then(&mut canonicalize),
|
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: song
|
||||||
.album_artists
|
.album_artists
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(&mut canonicalize)
|
.filter_map(&mut canonicalize)
|
||||||
|
.map(|k| ArtistKey { name: k })
|
||||||
.collect(),
|
.collect(),
|
||||||
year: song.year,
|
year: song.year,
|
||||||
album: song.album.as_ref().and_then(&mut canonicalize),
|
album: song.album.as_ref().and_then(&mut canonicalize),
|
||||||
|
@ -165,11 +182,13 @@ pub fn store_song(
|
||||||
.lyricists
|
.lyricists
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(&mut canonicalize)
|
.filter_map(&mut canonicalize)
|
||||||
|
.map(|k| ArtistKey { name: k })
|
||||||
.collect(),
|
.collect(),
|
||||||
composers: song
|
composers: song
|
||||||
.composers
|
.composers
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(&mut canonicalize)
|
.filter_map(&mut canonicalize)
|
||||||
|
.map(|k| ArtistKey { name: k })
|
||||||
.collect(),
|
.collect(),
|
||||||
genres: song.genres.iter().filter_map(&mut canonicalize).collect(),
|
genres: song.genres.iter().filter_map(&mut canonicalize).collect(),
|
||||||
labels: song.labels.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: song
|
||||||
.artists
|
.artists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.resolve(&s).to_string())
|
.map(|k| strings.resolve(&k.name).to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
album_artists: song
|
album_artists: song
|
||||||
.album_artists
|
.album_artists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.resolve(&s).to_string())
|
.map(|k| strings.resolve(&k.name).to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
year: song.year,
|
year: song.year,
|
||||||
album: song.album.map(|s| strings.resolve(&s).to_string()),
|
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: song
|
||||||
.lyricists
|
.lyricists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.resolve(&s).to_string())
|
.map(|k| strings.resolve(&k.name).to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
composers: song
|
composers: song
|
||||||
.composers
|
.composers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| strings.resolve(&s).to_string())
|
.map(|k| strings.resolve(&k.name).to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
genres: song
|
genres: song
|
||||||
.genres
|
.genres
|
||||||
|
|
|
@ -59,6 +59,9 @@ pub fn router() -> Router<App> {
|
||||||
.route("/artists", get(get_artists))
|
.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("/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("/random", get(get_random_albums)) // Deprecated
|
||||||
.route("/recent", get(get_recent_albums)) // Deprecated
|
.route("/recent", get(get_recent_albums)) // Deprecated
|
||||||
// Search
|
// Search
|
||||||
|
@ -508,6 +511,45 @@ async fn get_recent_albums(
|
||||||
albums_to_response(albums, api_version)
|
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(
|
async fn get_search(
|
||||||
_auth: Auth,
|
_auth: Auth,
|
||||||
api_version: APIMajorVersion,
|
api_version: APIMajorVersion,
|
||||||
|
|
|
@ -23,6 +23,7 @@ impl IntoResponse for APIError {
|
||||||
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
||||||
APIError::ArtistNotFound => StatusCode::NOT_FOUND,
|
APIError::ArtistNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::AlbumNotFound => StatusCode::NOT_FOUND,
|
APIError::AlbumNotFound => StatusCode::NOT_FOUND,
|
||||||
|
APIError::GenreNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::SongNotFound => StatusCode::NOT_FOUND,
|
APIError::SongNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
|
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
|
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
|
||||||
|
|
|
@ -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)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ArtistHeader {
|
pub struct ArtistHeader {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
@ -29,6 +29,8 @@ pub enum APIError {
|
||||||
ArtistNotFound,
|
ArtistNotFound,
|
||||||
#[error("Album not found")]
|
#[error("Album not found")]
|
||||||
AlbumNotFound,
|
AlbumNotFound,
|
||||||
|
#[error("Genre not found")]
|
||||||
|
GenreNotFound,
|
||||||
#[error("Song not found")]
|
#[error("Song not found")]
|
||||||
SongNotFound,
|
SongNotFound,
|
||||||
#[error("DDNS update query failed with HTTP status {0}")]
|
#[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::DirectoryNotFound(d) => APIError::DirectoryNotFound(d),
|
||||||
app::Error::ArtistNotFound => APIError::ArtistNotFound,
|
app::Error::ArtistNotFound => APIError::ArtistNotFound,
|
||||||
app::Error::AlbumNotFound => APIError::AlbumNotFound,
|
app::Error::AlbumNotFound => APIError::AlbumNotFound,
|
||||||
|
app::Error::GenreNotFound => APIError::GenreNotFound,
|
||||||
app::Error::SongNotFound => APIError::SongNotFound,
|
app::Error::SongNotFound => APIError::SongNotFound,
|
||||||
app::Error::PlaylistNotFound => APIError::PlaylistNotFound,
|
app::Error::PlaylistNotFound => APIError::PlaylistNotFound,
|
||||||
app::Error::SearchQueryParseError => APIError::SearchQueryParseError,
|
app::Error::SearchQueryParseError => APIError::SearchQueryParseError,
|
||||||
|
|
Loading…
Add table
Reference in a new issue