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::{
collections::{HashMap, HashSet},
hash::{DefaultHasher, Hash, Hasher},
sync::Arc,
};
@ -29,84 +30,198 @@ impl IndexManager {
&self,
count: usize,
) -> Result<Vec<collection::Album>, 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<Vec<collection::Album>, 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<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 {
pub artists: Vec<String>,
pub name: Option<String>,
}
#[derive(Default)]
pub(super) struct Index {
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());
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<collection::Album> {
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()
}
}

View file

@ -82,4 +82,5 @@ pub struct Album {
pub artists: Vec<String>,
pub year: Option<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 capacity = 500;
let mut index = Index::default();
let mut index_builder = IndexBuilder::default();
let mut buffer: Vec<Song> = 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?;

View file

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

View file

@ -302,7 +302,7 @@ impl From<collection::File> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -313,7 +313,7 @@ pub struct Album {
pub year: Option<i64>,
}
impl From<collection::Album> for Album {
impl From<collection::Album> for AlbumHeader {
fn from(a: collection::Album) -> Self {
Self {
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