Implement recent albums endpoint
This commit is contained in:
parent
64ef7cb21f
commit
93e8d7d94b
5 changed files with 191 additions and 58 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -82,4 +82,5 @@ pub struct Album {
|
|||
pub artists: Vec<String>,
|
||||
pub year: Option<i64>,
|
||||
pub date_added: i64,
|
||||
pub songs: Vec<Song>,
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue