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::{
|
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(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue