album list stream

This commit is contained in:
Erica Z 2024-11-07 19:53:57 +01:00
parent 62d9f74a39
commit 5d3c22aaad
4 changed files with 122 additions and 9 deletions

View file

@ -1,5 +1,8 @@
pub mod schema; pub mod schema;
mod album_list;
pub use album_list::AlbumListType;
use bytes::Bytes; use bytes::Bytes;
use md5::Digest; use md5::Digest;
use rand::Rng; use rand::Rng;
@ -49,12 +52,13 @@ impl From<reqwest::Error> for Error {
} }
} }
#[derive(Clone)]
pub struct Client { pub struct Client {
client: reqwest::Client, client: reqwest::Client,
base_url: reqwest::Url, base_url: reqwest::Url,
} }
fn get_random_salt(length: usize) -> String { fn random_salt(length: usize) -> String {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
std::iter::repeat(()) std::iter::repeat(())
// 0.9: s/distributions/distr // 0.9: s/distributions/distr
@ -69,7 +73,7 @@ impl Client {
const SALT_BYTES: usize = 8; const SALT_BYTES: usize = 8;
// subsonic docs say to generate a salt per request, but that's completely unnecessary // subsonic docs say to generate a salt per request, but that's completely unnecessary
let salt_hex = get_random_salt(SALT_BYTES); let salt_hex = random_salt(SALT_BYTES);
let mut hasher = md5::Md5::new(); let mut hasher = md5::Md5::new();
hasher.update(password); hasher.update(password);
@ -169,7 +173,7 @@ impl Client {
self.get(&["rest", "ping"], &[]).await self.get(&["rest", "ping"], &[]).await
} }
pub async fn get_random_songs(&self, size: u32) -> Result<Vec<schema::Child>, Error> { pub async fn random_songs(&self, size: u32) -> Result<Vec<schema::Child>, Error> {
self.get::<schema::RandomSongsOuter>( self.get::<schema::RandomSongsOuter>(
&["rest", "getRandomSongs"], &["rest", "getRandomSongs"],
&[("size", &size.to_string())], &[("size", &size.to_string())],

View file

@ -0,0 +1,67 @@
use super::{schema, Client, Error};
use futures::{Stream, TryStreamExt};
#[derive(Clone, Debug)]
pub enum AlbumListType {
Random,
Newest,
Highest,
Frequent,
Recent,
AlphabeticalByName,
AlphabeticalByArtist,
ByYear { from: u32, to: u32 },
ByGenre(String),
}
impl Client {
pub async fn album_list(
&self,
type_: &AlbumListType,
size: u32,
offset: u32,
) -> Result<Vec<schema::AlbumID3>, Error> {
match type_ {
AlbumListType::Newest => {
self.get::<schema::AlbumList2Outer>(
&["rest", "getAlbumList2"],
&[
("type", "newest"),
("size", &size.to_string()),
("offset", &offset.to_string()),
],
)
.await
}
_ => todo!("{type_:?}"),
}
.map(|response| match response.album_list2.album {
Some(albums) => albums,
None => vec![],
})
}
pub fn album_list_full(
&self,
type_: AlbumListType,
) -> impl Stream<Item = Result<schema::AlbumID3, Error>> + use<'_> {
futures::stream::try_unfold(0usize, move |offset| {
let type_ = type_.clone();
async move {
match self.album_list(&type_, 500, offset as u32).await {
Ok(albums) if albums.len() == 0 => Ok(None),
Ok(albums) => {
let next_offset = offset + albums.len();
Ok(Some((
futures::stream::iter(albums.into_iter().map(Result::Ok)),
next_offset,
)))
}
Err(err) => Err(err),
}
}
})
.try_flatten()
}
}

View file

@ -1,3 +1,4 @@
use chrono::{offset::Utc, DateTime};
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -44,10 +45,40 @@ pub struct Child {
pub artist: String, pub artist: String,
pub track: Option<u32>, pub track: Option<u32>,
pub year: Option<u32>, pub year: Option<u32>,
pub starred: Option<chrono::DateTime<chrono::offset::Utc>>, // TODO: check which is best pub starred: Option<DateTime<Utc>>, // TODO: check which is best
// applicable // applicable
pub duration: u64, pub duration: u32,
//pub play_count: Option<u32>, //pub play_count: Option<u32>,
pub genre: Option<String>, pub genre: Option<String>,
pub cover_art: String, pub cover_art: String,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AlbumList2Outer {
pub album_list2: AlbumList2,
}
#[derive(Debug, Deserialize)]
pub struct AlbumList2 {
pub album: Option<Vec<AlbumID3>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AlbumID3 {
pub id: String,
pub name: String,
pub artist: Option<String>,
pub artist_id: Option<String>,
pub cover_art: Option<String>,
pub song_count: u32,
pub duration: u32,
pub play_count: Option<u32>,
pub created: DateTime<Utc>,
pub starred: Option<DateTime<Utc>>,
pub year: Option<u32>,
pub genre: Option<String>,
pub played: Option<DateTime<Utc>>,
// TODO: opensubsonic extensions?
}

View file

@ -275,7 +275,7 @@ mod imp {
let api = self.api.borrow(); let api = self.api.borrow();
Rc::clone(api.as_ref().unwrap()) Rc::clone(api.as_ref().unwrap())
}; };
for song in api.get_random_songs(10).await.unwrap().into_iter() { for song in api.random_songs(10).await.unwrap().into_iter() {
let song = Song::from_child(&api, &song, true); let song = Song::from_child(&api, &song, true);
self.mpv self.mpv
.command(["loadfile", &song.stream_url(), "append-play"]) .command(["loadfile", &song.stream_url(), "append-play"])
@ -319,9 +319,7 @@ mod imp {
} }
fn playlist_pos(&self) -> i64 { fn playlist_pos(&self) -> i64 {
self.mpv self.mpv.get_property("playlist-pos").unwrap()
.get_property("playlist-pos")
.unwrap()
} }
fn time_pos(&self) -> f64 { fn time_pos(&self) -> f64 {
@ -658,6 +656,19 @@ impl Window {
pub fn setup_connected(&self, api: crate::subsonic::Client) { pub fn setup_connected(&self, api: crate::subsonic::Client) {
self.imp().api.replace(Some(Rc::new(api))); self.imp().api.replace(Some(Rc::new(api)));
self.set_can_click_shuffle_all(true); self.set_can_click_shuffle_all(true);
let api = Rc::clone(self.imp().api.borrow().as_ref().unwrap());
glib::spawn_future_local(async move {
use futures::TryStreamExt;
let mut count = 0;
let mut albums =
std::pin::pin!(api.album_list_full(crate::subsonic::AlbumListType::Newest));
while let Some(_) = albums.try_next().await.unwrap() {
count += 1;
}
println!("gathered {count} albums");
});
} }
pub fn playlist_next(&self) { pub fn playlist_next(&self) {