feat(qbzd): implement play-track endpoint (download + play)
POST /api/playback/play-track accepts track_id + quality, downloads the audio stream from Qobuz, and feeds it to the Player. This was the missing piece — without this, the daemon could search and manage queue but couldn't actually play anything. cc
This commit is contained in:
2 files changed
+72
No files matched your search
@@ -28,6 +28,77 @@ pub async fn get_playback(daemon: Arc<DaemonCore>) -> Json<serde_json::Value> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PlayTrackRequest {
|
||||||
|
pub track_id: u64,
|
||||||
|
pub quality: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play a specific track by ID. Downloads audio from Qobuz and feeds to player.
|
||||||
|
pub async fn play_track(
|
||||||
|
daemon: Arc<DaemonCore>,
|
||||||
|
Json(req): Json<PlayTrackRequest>,
|
||||||
|
) -> Result<Json<serde_json::Value>, String> {
|
||||||
|
let quality = match req.quality.as_deref() {
|
||||||
|
Some("Hi-Res+") | Some("UltraHiRes") => qbz_models::Quality::UltraHiRes,
|
||||||
|
Some("Hi-Res") | Some("HiRes") => qbz_models::Quality::HiRes,
|
||||||
|
Some("Lossless") => qbz_models::Quality::Lossless,
|
||||||
|
_ => qbz_models::Quality::HiRes, // Default to HiRes
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("[qbzd/play] Playing track {} (quality: {:?})", req.track_id, quality);
|
||||||
|
|
||||||
|
// Get stream URL from Qobuz
|
||||||
|
let stream_url = daemon.core.get_stream_url(req.track_id, quality)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to get stream URL: {}", e))?;
|
||||||
|
|
||||||
|
log::info!("[qbzd/play] Stream: {}Hz, {:?}bit",
|
||||||
|
(stream_url.sampling_rate * 1000.0) as u32,
|
||||||
|
stream_url.bit_depth,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download the audio
|
||||||
|
let http = reqwest::Client::builder()
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("HTTP client error: {}", e))?;
|
||||||
|
|
||||||
|
let response = http
|
||||||
|
.get(&stream_url.url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Download failed: {}", e))?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!("Download HTTP {}", response.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio_data = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Read failed: {}", e))?
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
log::info!("[qbzd/play] Downloaded {} bytes, feeding to player", audio_data.len());
|
||||||
|
|
||||||
|
// Feed to player
|
||||||
|
let player = daemon.core.player();
|
||||||
|
player
|
||||||
|
.play_data(audio_data, req.track_id)
|
||||||
|
.map_err(|e| format!("Player error: {}", e))?;
|
||||||
|
|
||||||
|
// Cache the audio
|
||||||
|
daemon.audio_cache.insert(req.track_id, vec![]); // TODO: cache actual data
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"playing": true,
|
||||||
|
"track_id": req.track_id,
|
||||||
|
"sample_rate": stream_url.sampling_rate,
|
||||||
|
"bit_depth": stream_url.bit_depth,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn play(daemon: Arc<DaemonCore>) -> Result<&'static str, String> {
|
pub async fn play(daemon: Arc<DaemonCore>) -> Result<&'static str, String> {
|
||||||
daemon.core.resume().map_err(|e| e.to_string())?;
|
daemon.core.resume().map_err(|e| e.to_string())?;
|
||||||
Ok("ok")
|
Ok("ok")
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ fn build_router(daemon: Arc<DaemonCore>) -> axum::Router {
|
|||||||
// Playback
|
// Playback
|
||||||
.route("/api/playback", get(with_daemon!(daemon, playback::get_playback)))
|
.route("/api/playback", get(with_daemon!(daemon, playback::get_playback)))
|
||||||
.route("/api/playback/play", post(with_daemon!(daemon, playback::play)))
|
.route("/api/playback/play", post(with_daemon!(daemon, playback::play)))
|
||||||
|
.route("/api/playback/play-track", post(with_daemon!(daemon, playback::play_track, json)))
|
||||||
.route("/api/playback/pause", post(with_daemon!(daemon, playback::pause)))
|
.route("/api/playback/pause", post(with_daemon!(daemon, playback::pause)))
|
||||||
.route("/api/playback/stop", post(with_daemon!(daemon, playback::stop)))
|
.route("/api/playback/stop", post(with_daemon!(daemon, playback::stop)))
|
||||||
.route("/api/playback/next", post(with_daemon!(daemon, playback::next)))
|
.route("/api/playback/next", post(with_daemon!(daemon, playback::next)))
|
||||||
|
|||||||
Reference in new issue
Block a user