feat(cast): v2_dlna_play_local_track for local-library tracks over DLNA
Mirrors v2_dlna_play_track but for library row ids. Reads the file from disk, infers content_type from AudioFormat (with extension fallback for Unknown), registers bytes with the DLNA media server, sends AVTransport URI + Play to the renderer. Without this command the cast path had no way to send local audio to a DLNA device — v2_dlna_play_track resolves the id as a Qobuz track and fails (404 in offline sessions, wrong-song match in online sessions where the library row id happens to collide with a real Qobuz track id). Issue #332 reported the symptom as "DLNA not operational"; logs showed the renderer connected successfully but never received a SOAP SetAVTransportURI. Frontend wiring in the cast dispatch + playbackService guard removal lands in follow-up commits. cc
This commit is contained in:
2 files changed
+112
-2
No files matched your search
@@ -7,8 +7,8 @@ use crate::config::download_settings::DownloadSettingsState;
|
||||
use crate::core_bridge::CoreBridgeState;
|
||||
use crate::integrations_v2::MusicBrainzV2State;
|
||||
use crate::library::{
|
||||
get_artwork_cache_dir, thumbnails, LibraryState, LocalAlbum, LocalTrack, MetadataExtractor,
|
||||
PlaylistLocalTrack, PlaylistSettings, PlaylistStats, ScanProgress,
|
||||
get_artwork_cache_dir, thumbnails, AudioFormat, LibraryState, LocalAlbum, LocalTrack,
|
||||
MetadataExtractor, PlaylistLocalTrack, PlaylistSettings, PlaylistStats, ScanProgress,
|
||||
};
|
||||
use crate::lyrics::LyricsState;
|
||||
use crate::offline::OfflineState;
|
||||
@@ -274,6 +274,115 @@ pub async fn v2_dlna_play_track(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Play a LOCAL library track on a DLNA renderer. Reads the file from disk,
|
||||
/// infers content_type from the track's format, registers bytes with the
|
||||
/// media server, and sends AVTransport URI + Play to the renderer.
|
||||
///
|
||||
/// Mirrors `v2_dlna_play_track` but for local-source tracks — that command
|
||||
/// resolves a Qobuz stream URL and would fail for a library row id. Without
|
||||
/// this command, casting any local-library track silently falls back to the
|
||||
/// app's local audio backend (issue #332).
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_dlna_play_local_track(
|
||||
trackId: i64,
|
||||
metadata: DlnaMetadata,
|
||||
dlna_state: State<'_, DlnaState>,
|
||||
library_state: State<'_, LibraryState>,
|
||||
) -> Result<(), String> {
|
||||
log::info!("DLNA: dlna_play_local_track called for track_id={}", trackId);
|
||||
|
||||
let track = {
|
||||
let guard = library_state.db.lock().await;
|
||||
let db = guard.as_ref().ok_or("No active session")?;
|
||||
db.get_track(trackId)
|
||||
.map_err(|e| format!("Library lookup failed: {}", e))?
|
||||
.ok_or_else(|| format!("Track {} not found in library", trackId))?
|
||||
};
|
||||
|
||||
let audio_data = std::fs::read(&track.file_path)
|
||||
.map_err(|e| format!("Failed to read {}: {}", track.file_path, e))?;
|
||||
let content_type = local_track_content_type(&track);
|
||||
|
||||
let target_ip = {
|
||||
let connection = dlna_state.connection.lock().await;
|
||||
connection.as_ref().map(|conn| conn.device_ip().to_string())
|
||||
};
|
||||
|
||||
dlna_state
|
||||
.ensure_media_server()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Library row ids are small positive autoincrement integers; using them
|
||||
// directly as MediaServer keys won't collide with Qobuz track ids within
|
||||
// a single cast session (the server is ephemeral per cast connection).
|
||||
let media_key = trackId as u64;
|
||||
let url = {
|
||||
let mut server_guard = dlna_state.media_server.lock().await;
|
||||
let server = server_guard
|
||||
.as_mut()
|
||||
.ok_or("Media server not initialized")?;
|
||||
server.register_audio(media_key, audio_data, &content_type);
|
||||
match target_ip.as_deref() {
|
||||
Some(ip) => server.get_audio_url_for_target(media_key, ip),
|
||||
None => server.get_audio_url(media_key),
|
||||
}
|
||||
.ok_or_else(|| "Failed to build media URL".to_string())?
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"DLNA: Playing local track {} ({}) via MediaServer URL: {}",
|
||||
trackId,
|
||||
content_type,
|
||||
url
|
||||
);
|
||||
|
||||
{
|
||||
let mut connection = dlna_state.connection.lock().await;
|
||||
let conn = connection.as_mut().ok_or("Not connected")?;
|
||||
conn.load_media(&url, &metadata, &content_type)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
{
|
||||
let mut connection = dlna_state.connection.lock().await;
|
||||
let conn = connection.as_mut().ok_or("Not connected")?;
|
||||
conn.play().await.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn local_track_content_type(track: &LocalTrack) -> String {
|
||||
match track.format {
|
||||
AudioFormat::Flac => "audio/flac".to_string(),
|
||||
AudioFormat::Alac => "audio/mp4".to_string(),
|
||||
AudioFormat::Wav => "audio/wav".to_string(),
|
||||
AudioFormat::Aiff => "audio/aiff".to_string(),
|
||||
AudioFormat::Ape => "audio/x-ape".to_string(),
|
||||
AudioFormat::Mp3 => "audio/mpeg".to_string(),
|
||||
AudioFormat::Unknown => {
|
||||
let ext = std::path::Path::new(&track.file_path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
match ext.as_str() {
|
||||
"flac" => "audio/flac",
|
||||
"mp3" => "audio/mpeg",
|
||||
"wav" => "audio/wav",
|
||||
"m4a" | "mp4" | "aac" => "audio/mp4",
|
||||
"aiff" | "aif" => "audio/aiff",
|
||||
"ogg" | "oga" => "audio/ogg",
|
||||
"opus" => "audio/opus",
|
||||
_ => "audio/octet-stream",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn v2_dlna_play(state: State<'_, DlnaState>) -> Result<(), String> {
|
||||
let mut connection = state.connection.lock().await;
|
||||
|
||||
Reference in new issue
Block a user