Index artists
This commit is contained in:
parent
35736ee1d5
commit
72ec7b260a
7 changed files with 132 additions and 10 deletions
|
@ -46,6 +46,8 @@
|
|||
cargo-watch
|
||||
rust-analyzer
|
||||
sqlx-cli
|
||||
samply
|
||||
sqlitebrowser
|
||||
];
|
||||
|
||||
env = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::{
|
||||
borrow::BorrowMut,
|
||||
collections::{HashMap, HashSet},
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
sync::Arc,
|
||||
|
@ -65,6 +66,17 @@ impl IndexManager {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn get_artist(
|
||||
&self,
|
||||
artist_key: &ArtistKey,
|
||||
) -> Result<collection::Artist, collection::Error> {
|
||||
let index = self.index.read().await;
|
||||
let artist_id = artist_key.into();
|
||||
index
|
||||
.get_artist(artist_id)
|
||||
.ok_or_else(|| collection::Error::ArtistNotFound)
|
||||
}
|
||||
|
||||
pub async fn get_album(
|
||||
&self,
|
||||
album_key: &AlbumKey,
|
||||
|
@ -107,6 +119,7 @@ impl IndexManager {
|
|||
#[derive(Default)]
|
||||
pub(super) struct IndexBuilder {
|
||||
songs: HashMap<SongID, collection::Song>,
|
||||
artists: HashMap<ArtistID, Artist>,
|
||||
albums: HashMap<AlbumID, Album>,
|
||||
}
|
||||
|
||||
|
@ -114,20 +127,48 @@ impl IndexBuilder {
|
|||
pub fn add_song(&mut self, song: collection::Song) {
|
||||
let song_id: SongID = song.song_id();
|
||||
self.add_song_to_album(&song);
|
||||
self.add_album_to_artists(&song);
|
||||
self.songs.insert(song_id, song);
|
||||
}
|
||||
|
||||
fn add_album_to_artists(&mut self, song: &collection::Song) {
|
||||
let album_id: AlbumID = song.album_id();
|
||||
|
||||
for artist_name in &song.album_artists.0 {
|
||||
let artist = self.get_or_create_artist(artist_name);
|
||||
artist.albums.insert(album_id);
|
||||
}
|
||||
|
||||
for artist_name in &song.artists.0 {
|
||||
let artist = self.get_or_create_artist(artist_name);
|
||||
if song.album_artists.0.is_empty() {
|
||||
artist.albums.insert(album_id);
|
||||
} else if !song.album_artists.0.contains(artist_name) {
|
||||
artist.album_appearances.insert(album_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_artist(&mut self, name: &String) -> &mut Artist {
|
||||
let artist_key = ArtistKey {
|
||||
name: Some(name.clone()),
|
||||
};
|
||||
let artist_id: ArtistID = (&artist_key).into();
|
||||
self.artists
|
||||
.entry(artist_id)
|
||||
.or_insert_with(|| Artist {
|
||||
name: Some(name.clone()),
|
||||
albums: HashSet::new(),
|
||||
album_appearances: HashSet::new(),
|
||||
})
|
||||
.borrow_mut()
|
||||
}
|
||||
|
||||
fn add_song_to_album(&mut self, song: &collection::Song) {
|
||||
let song_id: SongID = song.song_id();
|
||||
let album_id: AlbumID = song.album_id();
|
||||
|
||||
let album = match self.albums.get_mut(&album_id) {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
self.albums.insert(album_id, Album::default());
|
||||
self.albums.get_mut(&album_id).unwrap()
|
||||
}
|
||||
};
|
||||
let album = self.albums.entry(album_id).or_default().borrow_mut();
|
||||
|
||||
if album.name.is_none() {
|
||||
album.name = song.album.clone();
|
||||
|
@ -163,6 +204,7 @@ impl IndexBuilder {
|
|||
|
||||
Index {
|
||||
songs: self.songs,
|
||||
artists: self.artists,
|
||||
albums: self.albums,
|
||||
recent_albums,
|
||||
}
|
||||
|
@ -172,11 +214,29 @@ impl IndexBuilder {
|
|||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub(super) struct Index {
|
||||
songs: HashMap<SongID, collection::Song>,
|
||||
artists: HashMap<ArtistID, Artist>,
|
||||
albums: HashMap<AlbumID, Album>,
|
||||
recent_albums: Vec<AlbumID>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub(self) fn get_artist(&self, artist_id: ArtistID) -> Option<collection::Artist> {
|
||||
self.artists.get(&artist_id).map(|a| {
|
||||
let mut albums = a
|
||||
.albums
|
||||
.iter()
|
||||
.filter_map(|album_id| self.get_album(*album_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
albums.sort_by(|a, b| (a.year, &a.name).partial_cmp(&(b.year, &b.name)).unwrap());
|
||||
|
||||
collection::Artist {
|
||||
name: a.name.clone(),
|
||||
albums: albums,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(self) fn get_album(&self, album_id: AlbumID) -> Option<collection::Album> {
|
||||
self.albums.get(&album_id).map(|a| {
|
||||
let mut songs = a
|
||||
|
@ -229,7 +289,28 @@ impl collection::Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Artist {
|
||||
pub name: Option<String>,
|
||||
pub albums: HashSet<AlbumID>,
|
||||
pub album_appearances: HashSet<AlbumID>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
struct ArtistID(u64);
|
||||
|
||||
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||
pub struct ArtistKey {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&ArtistKey> for ArtistID {
|
||||
fn from(key: &ArtistKey) -> Self {
|
||||
ArtistID(key.id())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
struct Album {
|
||||
pub name: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
|
|
|
@ -28,6 +28,8 @@ impl From<Option<String>> for MultiString {
|
|||
pub enum Error {
|
||||
#[error("Directory not found: {0}")]
|
||||
DirectoryNotFound(PathBuf),
|
||||
#[error("Artist not found")]
|
||||
ArtistNotFound,
|
||||
#[error("Album not found")]
|
||||
AlbumNotFound,
|
||||
#[error(transparent)]
|
||||
|
@ -36,9 +38,9 @@ pub enum Error {
|
|||
DatabaseConnection(#[from] db::Error),
|
||||
#[error(transparent)]
|
||||
Vfs(#[from] vfs::Error),
|
||||
#[error("Could not serialize collection")]
|
||||
IndexDeserializationError,
|
||||
#[error("Could not deserialize collection")]
|
||||
IndexDeserializationError,
|
||||
#[error("Could not serialize collection")]
|
||||
IndexSerializationError,
|
||||
#[error(transparent)]
|
||||
ThreadPoolBuilder(#[from] rayon::ThreadPoolBuildError),
|
||||
|
@ -82,6 +84,12 @@ pub struct Directory {
|
|||
pub virtual_parent: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct Artist {
|
||||
pub name: Option<String>,
|
||||
pub albums: Vec<Album>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct Album {
|
||||
pub name: Option<String>,
|
||||
|
|
|
@ -43,6 +43,7 @@ pub fn router() -> Router<App> {
|
|||
.route("/browse/*path", get(get_browse))
|
||||
.route("/flatten", get(get_flatten_root))
|
||||
.route("/flatten/*path", get(get_flatten))
|
||||
.route("/artists/:artist", get(get_artist))
|
||||
.route("/artists/:artists/albums/:name", get(get_album))
|
||||
.route("/random", get(get_random))
|
||||
.route("/recent", get(get_recent))
|
||||
|
@ -369,6 +370,17 @@ async fn get_flatten(
|
|||
songs_to_response(songs, api_version)
|
||||
}
|
||||
|
||||
async fn get_artist(
|
||||
_auth: Auth,
|
||||
State(index_manager): State<collection::IndexManager>,
|
||||
Path(artist): Path<String>,
|
||||
) -> Result<Json<dto::Artist>, APIError> {
|
||||
let artist_key = collection::ArtistKey {
|
||||
name: (!artist.is_empty()).then_some(artist),
|
||||
};
|
||||
Ok(Json(index_manager.get_artist(&artist_key).await?.into()))
|
||||
}
|
||||
|
||||
async fn get_album(
|
||||
_auth: Auth,
|
||||
State(index_manager): State<collection::IndexManager>,
|
||||
|
|
|
@ -21,6 +21,7 @@ impl IntoResponse for APIError {
|
|||
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
||||
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
||||
APIError::ArtistNotFound => StatusCode::NOT_FOUND,
|
||||
APIError::AlbumNotFound => StatusCode::NOT_FOUND,
|
||||
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
|
||||
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
|
||||
|
|
|
@ -301,6 +301,21 @@ impl From<collection::File> for BrowserEntry {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Artist {
|
||||
pub name: Option<String>,
|
||||
pub albums: Vec<AlbumHeader>,
|
||||
}
|
||||
|
||||
impl From<collection::Artist> for Artist {
|
||||
fn from(a: collection::Artist) -> Self {
|
||||
Self {
|
||||
name: a.name,
|
||||
albums: a.albums.into_iter().map(|a| a.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AlbumHeader {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
@ -26,6 +26,8 @@ pub enum APIError {
|
|||
Database(sqlx::Error),
|
||||
#[error("Directory not found: {0}")]
|
||||
DirectoryNotFound(PathBuf),
|
||||
#[error("Artist not found")]
|
||||
ArtistNotFound,
|
||||
#[error("Album not found")]
|
||||
AlbumNotFound,
|
||||
#[error("DDNS update query failed with HTTP status {0}")]
|
||||
|
@ -88,6 +90,7 @@ impl From<collection::Error> for APIError {
|
|||
fn from(error: collection::Error) -> APIError {
|
||||
match error {
|
||||
collection::Error::DirectoryNotFound(d) => APIError::DirectoryNotFound(d),
|
||||
collection::Error::ArtistNotFound => APIError::ArtistNotFound,
|
||||
collection::Error::AlbumNotFound => APIError::AlbumNotFound,
|
||||
collection::Error::Database(e) => APIError::Database(e),
|
||||
collection::Error::DatabaseConnection(e) => e.into(),
|
||||
|
|
Loading…
Add table
Reference in a new issue