diff --git a/src/subsonic.rs b/src/subsonic.rs index 82d0927..4ed3dce 100644 --- a/src/subsonic.rs +++ b/src/subsonic.rs @@ -1,5 +1,8 @@ pub mod schema; +mod album_list; +pub use album_list::AlbumListType; + use bytes::Bytes; use md5::Digest; use rand::Rng; @@ -49,12 +52,13 @@ impl From for Error { } } +#[derive(Clone)] pub struct Client { client: reqwest::Client, base_url: reqwest::Url, } -fn get_random_salt(length: usize) -> String { +fn random_salt(length: usize) -> String { let mut rng = rand::thread_rng(); std::iter::repeat(()) // 0.9: s/distributions/distr @@ -69,7 +73,7 @@ impl Client { const SALT_BYTES: usize = 8; // 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(); hasher.update(password); @@ -169,7 +173,7 @@ impl Client { self.get(&["rest", "ping"], &[]).await } - pub async fn get_random_songs(&self, size: u32) -> Result, Error> { + pub async fn random_songs(&self, size: u32) -> Result, Error> { self.get::( &["rest", "getRandomSongs"], &[("size", &size.to_string())], diff --git a/src/subsonic/album_list.rs b/src/subsonic/album_list.rs new file mode 100644 index 0000000..670500b --- /dev/null +++ b/src/subsonic/album_list.rs @@ -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, Error> { + match type_ { + AlbumListType::Newest => { + self.get::( + &["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> + 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() + } +} diff --git a/src/subsonic/schema.rs b/src/subsonic/schema.rs index e36c134..ef27b1f 100644 --- a/src/subsonic/schema.rs +++ b/src/subsonic/schema.rs @@ -1,3 +1,4 @@ +use chrono::{offset::Utc, DateTime}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -44,10 +45,40 @@ pub struct Child { pub artist: String, pub track: Option, pub year: Option, - pub starred: Option>, // TODO: check which is best + pub starred: Option>, // TODO: check which is best // applicable - pub duration: u64, + pub duration: u32, //pub play_count: Option, pub genre: Option, 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>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AlbumID3 { + pub id: String, + pub name: String, + pub artist: Option, + pub artist_id: Option, + pub cover_art: Option, + pub song_count: u32, + pub duration: u32, + pub play_count: Option, + pub created: DateTime, + pub starred: Option>, + pub year: Option, + pub genre: Option, + pub played: Option>, + // TODO: opensubsonic extensions? +} diff --git a/src/ui/window.rs b/src/ui/window.rs index b65593c..b339653 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -275,7 +275,7 @@ mod imp { let api = self.api.borrow(); 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); self.mpv .command(["loadfile", &song.stream_url(), "append-play"]) @@ -319,9 +319,7 @@ mod imp { } fn playlist_pos(&self) -> i64 { - self.mpv - .get_property("playlist-pos") - .unwrap() + self.mpv.get_property("playlist-pos").unwrap() } fn time_pos(&self) -> f64 { @@ -658,6 +656,19 @@ impl Window { pub fn setup_connected(&self, api: crate::subsonic::Client) { self.imp().api.replace(Some(Rc::new(api))); 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) {