feat(radio): add pool builder and artist tracks endpoint
cx
This commit is contained in:
1 parent
da1384fbcc
commit
029e4a07df
3 files changed
+149
No files matched your search
@@ -269,6 +269,34 @@ impl QobuzClient {
|
|||||||
Ok(serde_json::from_value(artists.clone())?)
|
Ok(serde_json::from_value(artists.clone())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get an artist's tracks (public endpoint via artist/get?extra=tracks)
|
||||||
|
pub async fn get_artist_tracks(&self, artist_id: u64, limit: u32, offset: u32) -> Result<TracksContainer> {
|
||||||
|
let url = endpoints::build_url(paths::ARTIST_GET);
|
||||||
|
let locale = self.locale().await;
|
||||||
|
|
||||||
|
let response: Value = self
|
||||||
|
.http
|
||||||
|
.get(&url)
|
||||||
|
.header("X-App-Id", self.app_id().await?)
|
||||||
|
.query(&[
|
||||||
|
("artist_id", artist_id.to_string()),
|
||||||
|
("extra", "tracks".to_string()),
|
||||||
|
("lang", locale),
|
||||||
|
("limit", limit.to_string()),
|
||||||
|
("offset", offset.to_string()),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let tracks = response
|
||||||
|
.get("tracks")
|
||||||
|
.ok_or_else(|| ApiError::ApiResponse("No tracks in artist response".to_string()))?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_value(tracks.clone())?)
|
||||||
|
}
|
||||||
|
|
||||||
// === Get endpoints ===
|
// === Get endpoints ===
|
||||||
|
|
||||||
/// Get album by ID
|
/// Get album by ID
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
use super::db::{RadioDb, RadioSeed, RadioSession};
|
||||||
|
use crate::api::{QobuzClient, Track, TracksContainer};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BuildRadioOptions {
|
||||||
|
pub rng_seed: u64,
|
||||||
|
pub artist_spacing: u32,
|
||||||
|
pub reseed_every: u32,
|
||||||
|
|
||||||
|
pub seed_tracks_limit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BuildRadioOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
rng_seed: 0,
|
||||||
|
artist_spacing: 5,
|
||||||
|
reseed_every: 25,
|
||||||
|
seed_tracks_limit: 150,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RadioPoolBuilder<'a> {
|
||||||
|
pub db: &'a RadioDb,
|
||||||
|
pub client: &'a QobuzClient,
|
||||||
|
pub options: BuildRadioOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RadioPoolBuilder<'a> {
|
||||||
|
pub fn new(db: &'a RadioDb, client: &'a QobuzClient, options: BuildRadioOptions) -> Self {
|
||||||
|
Self { db, client, options }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_artist_id(track: &Track, fallback: u64) -> u64 {
|
||||||
|
if let Some(performer) = track.performer.as_ref() {
|
||||||
|
if performer.id != 0 {
|
||||||
|
return performer.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_music_track(track: &Track) -> bool {
|
||||||
|
track.streamable && track.duration > 0 && track.album.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_rng_seed(default_seed: u64, salt: u64) -> u64 {
|
||||||
|
if default_seed != 0 {
|
||||||
|
return default_seed;
|
||||||
|
}
|
||||||
|
(std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos() as u64)
|
||||||
|
.unwrap_or(0))
|
||||||
|
^ salt
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_artist_radio(&self, artist_id: u64) -> Result<RadioSession, String> {
|
||||||
|
let rng_seed = Self::derive_rng_seed(self.options.rng_seed, artist_id);
|
||||||
|
let session = self.db.create_session(
|
||||||
|
RadioSeed::Artist { artist_id },
|
||||||
|
rng_seed,
|
||||||
|
self.options.artist_spacing,
|
||||||
|
self.options.reseed_every,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.add_seed_artist_tracks(&session.id, artist_id).await?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_track_radio(&self, track_id: u64, artist_id: u64) -> Result<RadioSession, String> {
|
||||||
|
let rng_seed = Self::derive_rng_seed(self.options.rng_seed, track_id);
|
||||||
|
let session = self.db.create_session(
|
||||||
|
RadioSeed::Track { track_id, artist_id },
|
||||||
|
rng_seed,
|
||||||
|
self.options.artist_spacing,
|
||||||
|
self.options.reseed_every,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let track = self
|
||||||
|
.client
|
||||||
|
.get_track(track_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to fetch seed track: {}", e))?;
|
||||||
|
|
||||||
|
let track_artist_id = Self::track_artist_id(&track, artist_id);
|
||||||
|
if Self::is_music_track(&track) {
|
||||||
|
self.db
|
||||||
|
.insert_pool_track(&session.id, track.id, track_artist_id, "seed_track", 0)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.add_seed_artist_tracks(&session.id, artist_id).await?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_seed_artist_tracks(&self, session_id: &str, seed_artist_id: u64) -> Result<(), String> {
|
||||||
|
let tracks: TracksContainer = self
|
||||||
|
.client
|
||||||
|
.get_artist_tracks(seed_artist_id, self.options.seed_tracks_limit, 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to fetch seed artist tracks: {}", e))?;
|
||||||
|
|
||||||
|
for t in tracks.items {
|
||||||
|
if !Self::is_music_track(&t) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let artist_id = Self::track_artist_id(&t, seed_artist_id);
|
||||||
|
self.db
|
||||||
|
.insert_pool_track(session_id, t.id, artist_id, "seed_tracks", 0)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
pub mod builder;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
|
|
||||||
|
pub use builder::{BuildRadioOptions, RadioPoolBuilder};
|
||||||
pub use db::{RadioDb, RadioSeed, RadioSession, RadioTrackRef};
|
pub use db::{RadioDb, RadioSeed, RadioSession, RadioTrackRef};
|
||||||
pub use engine::RadioEngine;
|
pub use engine::RadioEngine;
|
||||||
|
|
||||||
Reference in new issue
Block a user