album list stream
This commit is contained in:
parent
62d9f74a39
commit
5d3c22aaad
4 changed files with 122 additions and 9 deletions
|
@ -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())],
|
||||||
|
|
67
src/subsonic/album_list.rs
Normal file
67
src/subsonic/album_list.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue