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
|
cargo-watch
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
sqlx-cli
|
sqlx-cli
|
||||||
|
samply
|
||||||
|
sqlitebrowser
|
||||||
];
|
];
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::BorrowMut,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -65,6 +66,17 @@ impl IndexManager {
|
||||||
Ok(true)
|
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(
|
pub async fn get_album(
|
||||||
&self,
|
&self,
|
||||||
album_key: &AlbumKey,
|
album_key: &AlbumKey,
|
||||||
|
@ -107,6 +119,7 @@ impl IndexManager {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(super) struct IndexBuilder {
|
pub(super) struct IndexBuilder {
|
||||||
songs: HashMap<SongID, collection::Song>,
|
songs: HashMap<SongID, collection::Song>,
|
||||||
|
artists: HashMap<ArtistID, Artist>,
|
||||||
albums: HashMap<AlbumID, Album>,
|
albums: HashMap<AlbumID, Album>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,20 +127,48 @@ impl IndexBuilder {
|
||||||
pub fn add_song(&mut self, song: collection::Song) {
|
pub fn add_song(&mut self, song: collection::Song) {
|
||||||
let song_id: SongID = song.song_id();
|
let song_id: SongID = song.song_id();
|
||||||
self.add_song_to_album(&song);
|
self.add_song_to_album(&song);
|
||||||
|
self.add_album_to_artists(&song);
|
||||||
self.songs.insert(song_id, 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) {
|
fn add_song_to_album(&mut self, song: &collection::Song) {
|
||||||
let song_id: SongID = song.song_id();
|
let song_id: SongID = song.song_id();
|
||||||
let album_id: AlbumID = song.album_id();
|
let album_id: AlbumID = song.album_id();
|
||||||
|
|
||||||
let album = match self.albums.get_mut(&album_id) {
|
let album = self.albums.entry(album_id).or_default().borrow_mut();
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
self.albums.insert(album_id, Album::default());
|
|
||||||
self.albums.get_mut(&album_id).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if album.name.is_none() {
|
if album.name.is_none() {
|
||||||
album.name = song.album.clone();
|
album.name = song.album.clone();
|
||||||
|
@ -163,6 +204,7 @@ impl IndexBuilder {
|
||||||
|
|
||||||
Index {
|
Index {
|
||||||
songs: self.songs,
|
songs: self.songs,
|
||||||
|
artists: self.artists,
|
||||||
albums: self.albums,
|
albums: self.albums,
|
||||||
recent_albums,
|
recent_albums,
|
||||||
}
|
}
|
||||||
|
@ -172,11 +214,29 @@ impl IndexBuilder {
|
||||||
#[derive(Default, Serialize, Deserialize)]
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
pub(super) struct Index {
|
pub(super) struct Index {
|
||||||
songs: HashMap<SongID, collection::Song>,
|
songs: HashMap<SongID, collection::Song>,
|
||||||
|
artists: HashMap<ArtistID, Artist>,
|
||||||
albums: HashMap<AlbumID, Album>,
|
albums: HashMap<AlbumID, Album>,
|
||||||
recent_albums: Vec<AlbumID>,
|
recent_albums: Vec<AlbumID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index {
|
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> {
|
pub(self) fn get_album(&self, album_id: AlbumID) -> Option<collection::Album> {
|
||||||
self.albums.get(&album_id).map(|a| {
|
self.albums.get(&album_id).map(|a| {
|
||||||
let mut songs = 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 {
|
struct Album {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub artwork: Option<String>,
|
pub artwork: Option<String>,
|
||||||
|
|
|
@ -28,6 +28,8 @@ impl From<Option<String>> for MultiString {
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Directory not found: {0}")]
|
#[error("Directory not found: {0}")]
|
||||||
DirectoryNotFound(PathBuf),
|
DirectoryNotFound(PathBuf),
|
||||||
|
#[error("Artist not found")]
|
||||||
|
ArtistNotFound,
|
||||||
#[error("Album not found")]
|
#[error("Album not found")]
|
||||||
AlbumNotFound,
|
AlbumNotFound,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -36,9 +38,9 @@ pub enum Error {
|
||||||
DatabaseConnection(#[from] db::Error),
|
DatabaseConnection(#[from] db::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Vfs(#[from] vfs::Error),
|
Vfs(#[from] vfs::Error),
|
||||||
#[error("Could not serialize collection")]
|
|
||||||
IndexDeserializationError,
|
|
||||||
#[error("Could not deserialize collection")]
|
#[error("Could not deserialize collection")]
|
||||||
|
IndexDeserializationError,
|
||||||
|
#[error("Could not serialize collection")]
|
||||||
IndexSerializationError,
|
IndexSerializationError,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ThreadPoolBuilder(#[from] rayon::ThreadPoolBuildError),
|
ThreadPoolBuilder(#[from] rayon::ThreadPoolBuildError),
|
||||||
|
@ -82,6 +84,12 @@ pub struct Directory {
|
||||||
pub virtual_parent: Option<String>,
|
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)]
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
|
@ -43,6 +43,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/: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))
|
||||||
.route("/recent", get(get_recent))
|
.route("/recent", get(get_recent))
|
||||||
|
@ -369,6 +370,17 @@ async fn get_flatten(
|
||||||
songs_to_response(songs, api_version)
|
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(
|
async fn get_album(
|
||||||
_auth: Auth,
|
_auth: Auth,
|
||||||
State(index_manager): State<collection::IndexManager>,
|
State(index_manager): State<collection::IndexManager>,
|
||||||
|
|
|
@ -21,6 +21,7 @@ impl IntoResponse for APIError {
|
||||||
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
APIError::DeletingOwnAccount => StatusCode::CONFLICT,
|
||||||
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
|
||||||
|
APIError::ArtistNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::AlbumNotFound => StatusCode::NOT_FOUND,
|
APIError::AlbumNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
|
APIError::EmbeddedArtworkNotFound => StatusCode::NOT_FOUND,
|
||||||
APIError::EmptyPassword => StatusCode::BAD_REQUEST,
|
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)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct AlbumHeader {
|
pub struct AlbumHeader {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
|
@ -26,6 +26,8 @@ pub enum APIError {
|
||||||
Database(sqlx::Error),
|
Database(sqlx::Error),
|
||||||
#[error("Directory not found: {0}")]
|
#[error("Directory not found: {0}")]
|
||||||
DirectoryNotFound(PathBuf),
|
DirectoryNotFound(PathBuf),
|
||||||
|
#[error("Artist not found")]
|
||||||
|
ArtistNotFound,
|
||||||
#[error("Album not found")]
|
#[error("Album not found")]
|
||||||
AlbumNotFound,
|
AlbumNotFound,
|
||||||
#[error("DDNS update query failed with HTTP status {0}")]
|
#[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 {
|
fn from(error: collection::Error) -> APIError {
|
||||||
match error {
|
match error {
|
||||||
collection::Error::DirectoryNotFound(d) => APIError::DirectoryNotFound(d),
|
collection::Error::DirectoryNotFound(d) => APIError::DirectoryNotFound(d),
|
||||||
|
collection::Error::ArtistNotFound => APIError::ArtistNotFound,
|
||||||
collection::Error::AlbumNotFound => APIError::AlbumNotFound,
|
collection::Error::AlbumNotFound => APIError::AlbumNotFound,
|
||||||
collection::Error::Database(e) => APIError::Database(e),
|
collection::Error::Database(e) => APIError::Database(e),
|
||||||
collection::Error::DatabaseConnection(e) => e.into(),
|
collection::Error::DatabaseConnection(e) => e.into(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue