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, 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")]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtistHeader { pub struct ArtistHeader {
pub name: String, pub name: String,

View file

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