diff --git a/src/app/collection/index.rs b/src/app/collection/index.rs index 93a56c0..b9c3616 100644 --- a/src/app/collection/index.rs +++ b/src/app/collection/index.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + hash::{DefaultHasher, Hash, Hasher}, sync::Arc, }; @@ -29,84 +30,198 @@ impl IndexManager { &self, count: usize, ) -> Result, collection::Error> { - let lookups = self.index.read().await; - Ok(lookups - .songs_by_albums + let index = self.index.read().await; + Ok(index + .albums .keys() .choose_multiple(&mut ThreadRng::default(), count) - .iter() - .filter_map(|k| lookups.get_album(k)) + .into_iter() + .filter_map(|k| index.get_album(*k)) .collect()) } pub async fn get_recent_albums( &self, - count: i64, + count: usize, ) -> Result, collection::Error> { - // TODO implement - Ok(vec![]) + let index = self.index.read().await; + Ok(index + .recent_albums + .iter() + .take(count) + .filter_map(|k| index.get_album(*k)) + .collect()) } } -// TODO how can clients refer to an album? -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Default)] +pub(super) struct IndexBuilder { + songs: HashMap, + albums: HashMap, +} + +impl IndexBuilder { + pub fn add_song(&mut self, song: &collection::Song) { + self.songs.insert(song.into(), song.clone()); + self.add_song_to_album(song); + } + + fn add_song_to_album(&mut self, song: &collection::Song) { + let album_id: AlbumID = song.into(); + + 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() + } + }; + + if album.name.is_none() { + album.name = song.album.clone(); + } + + if album.artwork.is_none() { + album.artwork = song.artwork.clone(); + } + + if album.year.is_none() { + album.year = song.year.clone(); + } + + album.date_added = album.date_added.min(song.date_added); + + if !song.album_artists.0.is_empty() { + album.artists = song.album_artists.0.clone(); + } else if !song.album_artists.0.is_empty() { + album.artists = song.artists.0.clone(); + } + + album.songs.insert(song.into()); + } + + pub fn build(self) -> Index { + let mut recent_albums = self.albums.keys().cloned().collect::>(); + recent_albums.sort_by_key(|a| { + self.albums + .get(a) + .map(|a| -a.date_added) + .unwrap_or_default() + }); + + Index { + songs: self.songs, + albums: self.albums, + recent_albums, + } + } +} + +#[derive(Default)] +pub(super) struct Index { + songs: HashMap, + albums: HashMap, + recent_albums: Vec, +} + +impl Index { + pub fn get_album(&self, album_id: AlbumID) -> Option { + self.albums.get(&album_id).map(|a| { + let songs = a + .songs + .iter() + .filter_map(|s| self.songs.get(s)) + .cloned() + .collect::>(); + + collection::Album { + name: a.name.clone(), + artwork: a.artwork.clone(), + artists: a.artists.clone(), + year: a.year, + date_added: a.date_added, + songs, + } + }) + } +} + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct SongID(u64); + +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct SongKey { + pub virtual_path: String, +} + +impl From<&collection::Song> for SongKey { + fn from(song: &collection::Song) -> Self { + SongKey { + virtual_path: song.virtual_path.clone(), + } + } +} + +impl From<&SongKey> for SongID { + fn from(key: &SongKey) -> Self { + let mut hasher = DefaultHasher::default(); + key.hash(&mut hasher); + SongID(hasher.finish()) + } +} + +impl From<&collection::Song> for SongID { + fn from(song: &collection::Song) -> Self { + let key: SongKey = song.into(); + (&key).into() + } +} + +#[derive(Default)] +struct Album { + pub name: Option, + pub artwork: Option, + pub artists: Vec, + pub year: Option, + pub date_added: i64, + pub songs: HashSet, +} + +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct AlbumID(u64); + +#[derive(Clone, Eq, Hash, PartialEq)] struct AlbumKey { pub artists: Vec, pub name: Option, } -#[derive(Default)] -pub(super) struct Index { - all_songs: HashMap, - songs_by_albums: HashMap>, // TODO should this store collection::Album structs instead? -} - -impl Index { - pub fn add_song(&mut self, song: &collection::Song) { - self.all_songs - .insert(song.virtual_path.clone(), song.clone()); - +impl From<&collection::Song> for AlbumKey { + fn from(song: &collection::Song) -> Self { let album_artists = match song.album_artists.0.is_empty() { true => &song.artists.0, false => &song.album_artists.0, }; - let album_key = AlbumKey { + AlbumKey { artists: album_artists.iter().cloned().collect(), name: song.album.clone(), - }; - - let song_list = match self.songs_by_albums.get_mut(&album_key) { - Some(l) => l, - None => { - self.songs_by_albums - .insert(album_key.clone(), HashSet::new()); - self.songs_by_albums.get_mut(&album_key).unwrap() - } - }; - - song_list.insert(song.virtual_path.clone()); + } } +} - pub fn get_album(&self, key: &AlbumKey) -> Option { - let Some(songs) = self.songs_by_albums.get(key) else { - return None; - }; +impl From<&AlbumKey> for AlbumID { + fn from(key: &AlbumKey) -> Self { + let mut hasher = DefaultHasher::default(); + key.hash(&mut hasher); + AlbumID(hasher.finish()) + } +} - let songs: Vec<&collection::Song> = - songs.iter().filter_map(|s| self.all_songs.get(s)).collect(); - - Some(collection::Album { - name: key.name.clone(), - artwork: songs.iter().find_map(|s| s.artwork.clone()), - artists: key.artists.iter().cloned().collect(), - year: songs.iter().find_map(|s| s.year), - date_added: songs - .iter() - .min_by_key(|s| s.date_added) - .map(|s| s.date_added) - .unwrap_or_default(), - }) +impl From<&collection::Song> for AlbumID { + fn from(song: &collection::Song) -> Self { + let key: AlbumKey = song.into(); + (&key).into() } } diff --git a/src/app/collection/types.rs b/src/app/collection/types.rs index 26b083b..8278eeb 100644 --- a/src/app/collection/types.rs +++ b/src/app/collection/types.rs @@ -82,4 +82,5 @@ pub struct Album { pub artists: Vec, pub year: Option, pub date_added: i64, + pub songs: Vec, } diff --git a/src/app/collection/updater.rs b/src/app/collection/updater.rs index 5213095..bfc06c0 100644 --- a/src/app/collection/updater.rs +++ b/src/app/collection/updater.rs @@ -123,7 +123,7 @@ impl Updater { let song_task = tokio::spawn(async move { let capacity = 500; - let mut index = Index::default(); + let mut index_builder = IndexBuilder::default(); let mut buffer: Vec = Vec::with_capacity(capacity); loop { @@ -134,14 +134,14 @@ impl Updater { 0 => break, _ => { for song in buffer.drain(0..) { - index.add_song(&song); + index_builder.add_song(&song); song_inserter.insert(song).await; } } } } song_inserter.flush().await; - index + index_builder.build() }); let index = tokio::join!(scanner.scan(), directory_task, song_task).2?; diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index b1f83de..e291090 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -307,7 +307,7 @@ fn albums_to_response(albums: Vec, api_version: APIMajorVersi albums .into_iter() .map(|f| f.into()) - .collect::>(), + .collect::>(), ) .into_response(), } diff --git a/src/server/dto/v8.rs b/src/server/dto/v8.rs index a093045..939736b 100644 --- a/src/server/dto/v8.rs +++ b/src/server/dto/v8.rs @@ -302,7 +302,7 @@ impl From for BrowserEntry { } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Album { +pub struct AlbumHeader { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -313,7 +313,7 @@ pub struct Album { pub year: Option, } -impl From for Album { +impl From for AlbumHeader { fn from(a: collection::Album) -> Self { Self { name: a.name, @@ -324,4 +324,21 @@ impl From for Album { } } +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Album { + #[serde(flatten)] + pub header: AlbumHeader, + pub songs: Vec, +} + +impl From for Album { + fn from(mut a: collection::Album) -> Self { + let songs = a.songs.drain(..).map(|s| s.into()).collect(); + Self { + header: a.into(), + songs: songs, + } + } +} + // TODO: Preferences should have dto types