Add cover URL support in ICY metadata via StreamUrl field

Enhances ICY metadata streaming to include cover artwork URLs, enabling
media players to display album art while streaming.

Changes:
- Add cover_pk field to MetadataSnapshot (cache primary key)
- Extract cover_pk in update_metadata() alongside cover_url
- Format ICY metadata with StreamUrl field pointing to cover image:
  * If cover_pk exists: /covers/image/{pk}/256 (local cache, 256px)
  * Fallback to cover_url if no local cache (external URL)
- Use relative URLs for compatibility with same-origin streaming

ICY format example:
  StreamTitle='AC/DC - Highway to Hell';StreamUrl='/covers/image/abc123/256';

This works seamlessly with pmocovers which serves images at:
  GET /covers/image/{pk}       - Original WebP
  GET /covers/image/{pk}/256   - 256px variant (used in ICY)

Relative URLs are resolved correctly by VLC and other ICY-compatible players
when streaming from the same server that serves covers.
This commit is contained in:
Claude
2025-11-11 19:38:54 +00:00
parent 4884fddf0e
commit 941fbbed71

View File

@@ -101,9 +101,12 @@ pub struct MetadataSnapshot {
/// Track duration
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<Duration>,
/// Cover image URL
/// Cover image URL (external/original)
#[serde(skip_serializing_if = "Option::is_none")]
pub cover_url: Option<String>,
/// Cover primary key in local cache (for constructing server URL)
#[serde(skip_serializing_if = "Option::is_none")]
pub cover_pk: Option<String>,
/// Track number
#[serde(skip_serializing_if = "Option::is_none")]
pub track_number: Option<u32>,
@@ -287,10 +290,27 @@ impl IcyClientStream {
///
/// ICY format: StreamTitle='Artist - Title';StreamUrl='url';
/// Padded to multiple of 16 bytes, prefixed with length byte.
///
/// If cover_pk is available, constructs a URL for the cover image:
/// - If pmoserver is initialized: http://server/covers/image/{pk}/256
/// - Otherwise: relative URL /covers/image/{pk}/256
fn format_icy_metadata(meta: &MetadataSnapshot) -> Bytes {
let title = meta.title.as_deref().unwrap_or("Unknown");
let artist = meta.artist.as_deref().unwrap_or("Unknown Artist");
let metadata_str = format!("StreamTitle='{} - {}';", artist, title);
// Build ICY metadata string with cover URL if available
let mut metadata_str = format!("StreamTitle='{} - {}';", artist, title);
// Add cover URL if we have a cover_pk
if let Some(pk) = &meta.cover_pk {
// Use relative URL /covers/image/{pk}/256
// This works when streaming from the same server that serves covers
// VLC and other players will resolve relative URLs correctly
metadata_str.push_str(&format!("StreamUrl='/covers/image/{}/256';", pk));
} else if let Some(url) = &meta.cover_url {
// Fallback to external cover URL if no local pk
metadata_str.push_str(&format!("StreamUrl='{}';", url));
}
// ICY metadata is padded to multiple of 16 bytes
let metadata_bytes = metadata_str.as_bytes();
@@ -494,6 +514,7 @@ impl StreamingFlacSinkLogic {
snapshot.album = metadata.get_album().await.ok();
snapshot.duration = metadata.get_duration().await.ok();
snapshot.cover_url = metadata.get_cover_url().await.ok();
snapshot.cover_pk = metadata.get_cover_pk().await.ok();
snapshot.album_artist = metadata.get_album_artist().await.ok();
snapshot.year = metadata.get_year().await.ok();
@@ -509,11 +530,12 @@ impl StreamingFlacSinkLogic {
snapshot.version += 1;
debug!(
"Metadata updated: v{} @ {:.2}s - {} - {}",
"Metadata updated: v{} @ {:.2}s - {} - {} (cover_pk: {:?})",
snapshot.version,
timestamp_sec,
snapshot.artist.as_deref().unwrap_or("?"),
snapshot.title.as_deref().unwrap_or("?")
snapshot.title.as_deref().unwrap_or("?"),
snapshot.cover_pk
);
Ok(())