Implement recent albums endpoint

This commit is contained in:
Antoine Gersant 2024-07-29 20:00:53 -07:00
parent 64ef7cb21f
commit 93e8d7d94b
5 changed files with 191 additions and 58 deletions

View file

@ -1,5 +1,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
hash::{DefaultHasher, Hash, Hasher},
sync::Arc, sync::Arc,
}; };
@ -29,84 +30,198 @@ impl IndexManager {
&self, &self,
count: usize, count: usize,
) -> Result<Vec<collection::Album>, collection::Error> { ) -> Result<Vec<collection::Album>, collection::Error> {
let lookups = self.index.read().await; let index = self.index.read().await;
Ok(lookups Ok(index
.songs_by_albums .albums
.keys() .keys()
.choose_multiple(&mut ThreadRng::default(), count) .choose_multiple(&mut ThreadRng::default(), count)
.iter() .into_iter()
.filter_map(|k| lookups.get_album(k)) .filter_map(|k| index.get_album(*k))
.collect()) .collect())
} }
pub async fn get_recent_albums( pub async fn get_recent_albums(
&self, &self,
count: i64, count: usize,
) -> Result<Vec<collection::Album>, collection::Error> { ) -> Result<Vec<collection::Album>, collection::Error> {
// TODO implement let index = self.index.read().await;
Ok(vec![]) Ok(index
.recent_albums
.iter()
.take(count)
.filter_map(|k| index.get_album(*k))
.collect())
} }
} }
// TODO how can clients refer to an album? #[derive(Default)]
#[derive(Clone, PartialEq, Eq, Hash)] pub(super) struct IndexBuilder {
songs: HashMap<SongID, collection::Song>,
albums: HashMap<AlbumID, Album>,
}
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::<Vec<_>>();
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<SongID, collection::Song>,
albums: HashMap<AlbumID, Album>,
recent_albums: Vec<AlbumID>,
}
impl Index {
pub fn get_album(&self, album_id: AlbumID) -> Option<collection::Album> {
self.albums.get(&album_id).map(|a| {
let songs = a
.songs
.iter()
.filter_map(|s| self.songs.get(s))
.cloned()
.collect::<Vec<_>>();
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<String>,
pub artwork: Option<String>,
pub artists: Vec<String>,
pub year: Option<i64>,
pub date_added: i64,
pub songs: HashSet<SongID>,
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct AlbumID(u64);
#[derive(Clone, Eq, Hash, PartialEq)]
struct AlbumKey { struct AlbumKey {
pub artists: Vec<String>, pub artists: Vec<String>,
pub name: Option<String>, pub name: Option<String>,
} }
#[derive(Default)] impl From<&collection::Song> for AlbumKey {
pub(super) struct Index { fn from(song: &collection::Song) -> Self {
all_songs: HashMap<String, collection::Song>,
songs_by_albums: HashMap<AlbumKey, HashSet<String>>, // 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());
let album_artists = match song.album_artists.0.is_empty() { let album_artists = match song.album_artists.0.is_empty() {
true => &song.artists.0, true => &song.artists.0,
false => &song.album_artists.0, false => &song.album_artists.0,
}; };
let album_key = AlbumKey { AlbumKey {
artists: album_artists.iter().cloned().collect(), artists: album_artists.iter().cloned().collect(),
name: song.album.clone(), 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<collection::Album> { impl From<&AlbumKey> for AlbumID {
let Some(songs) = self.songs_by_albums.get(key) else { fn from(key: &AlbumKey) -> Self {
return None; let mut hasher = DefaultHasher::default();
}; key.hash(&mut hasher);
AlbumID(hasher.finish())
}
}
let songs: Vec<&collection::Song> = impl From<&collection::Song> for AlbumID {
songs.iter().filter_map(|s| self.all_songs.get(s)).collect(); fn from(song: &collection::Song) -> Self {
let key: AlbumKey = song.into();
Some(collection::Album { (&key).into()
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(),
})
} }
} }

View file

@ -82,4 +82,5 @@ pub struct Album {
pub artists: Vec<String>, pub artists: Vec<String>,
pub year: Option<i64>, pub year: Option<i64>,
pub date_added: i64, pub date_added: i64,
pub songs: Vec<Song>,
} }

View file

@ -123,7 +123,7 @@ impl Updater {
let song_task = tokio::spawn(async move { let song_task = tokio::spawn(async move {
let capacity = 500; let capacity = 500;
let mut index = Index::default(); let mut index_builder = IndexBuilder::default();
let mut buffer: Vec<Song> = Vec::with_capacity(capacity); let mut buffer: Vec<Song> = Vec::with_capacity(capacity);
loop { loop {
@ -134,14 +134,14 @@ impl Updater {
0 => break, 0 => break,
_ => { _ => {
for song in buffer.drain(0..) { for song in buffer.drain(0..) {
index.add_song(&song); index_builder.add_song(&song);
song_inserter.insert(song).await; song_inserter.insert(song).await;
} }
} }
} }
} }
song_inserter.flush().await; song_inserter.flush().await;
index index_builder.build()
}); });
let index = tokio::join!(scanner.scan(), directory_task, song_task).2?; let index = tokio::join!(scanner.scan(), directory_task, song_task).2?;

View file

@ -307,7 +307,7 @@ fn albums_to_response(albums: Vec<collection::Album>, api_version: APIMajorVersi
albums albums
.into_iter() .into_iter()
.map(|f| f.into()) .map(|f| f.into())
.collect::<Vec<dto::Album>>(), .collect::<Vec<dto::AlbumHeader>>(),
) )
.into_response(), .into_response(),
} }

View file

@ -302,7 +302,7 @@ impl From<collection::File> for BrowserEntry {
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Album { pub struct AlbumHeader {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@ -313,7 +313,7 @@ pub struct Album {
pub year: Option<i64>, pub year: Option<i64>,
} }
impl From<collection::Album> for Album { impl From<collection::Album> for AlbumHeader {
fn from(a: collection::Album) -> Self { fn from(a: collection::Album) -> Self {
Self { Self {
name: a.name, name: a.name,
@ -324,4 +324,21 @@ impl From<collection::Album> for Album {
} }
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Album {
#[serde(flatten)]
pub header: AlbumHeader,
pub songs: Vec<Song>,
}
impl From<collection::Album> 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 // TODO: Preferences should have dto types