feat(tracks): add streamable field infrastructure for unavailable tracks
Adds streamable field throughout the stack to detect tracks that have been removed from Qobuz but still appear in playlists/favorites: Backend (Rust): - QueueTrack: add streamable field with default true - PersistedQueueTrack: add streamable for session persistence - radio.rs: log warning when non-streamable track is encountered Frontend: - BackendQueueTrack: add streamable? field - QobuzTrack, Track, PlaylistTrack types: add streamable - qobuzAdapters.ts: include streamable in track conversions - trackActions.ts: all queue builders now include streamable - +page.svelte: debug logging includes streamable field This prepares the infrastructure for graying out unavailable tracks in the UI (to be implemented when we can reproduce the issue). Refs: User report about "Failed to fetch audio" for removed tracks
This commit is contained in:
1 parent
7ce5b402c4
commit
10788f877f
8 files changed
+79
-5
No files matched your search
@@ -268,6 +268,14 @@ fn track_to_queue_track(track: &Track) -> QueueTrack {
|
||||
let album_id = track.album.as_ref().map(|a| a.id.clone());
|
||||
let artist_id = track.performer.as_ref().map(|p| p.id);
|
||||
|
||||
// Log if track is not streamable
|
||||
if !track.streamable {
|
||||
log::warn!(
|
||||
"[Radio] Track not streamable: {} - {} (ID: {})",
|
||||
artist, track.title, track.id
|
||||
);
|
||||
}
|
||||
|
||||
QueueTrack {
|
||||
id: track.id,
|
||||
title: track.title.clone(),
|
||||
@@ -281,6 +289,7 @@ fn track_to_queue_track(track: &Track) -> QueueTrack {
|
||||
is_local: false,
|
||||
album_id,
|
||||
artist_id,
|
||||
streamable: track.streamable,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ pub struct QueueTrack {
|
||||
pub album_id: Option<String>,
|
||||
/// Artist ID for navigation (Qobuz artist ID)
|
||||
pub artist_id: Option<u64>,
|
||||
/// Whether the track is streamable on Qobuz (false = removed/unavailable)
|
||||
#[serde(default = "default_streamable")]
|
||||
pub streamable: bool,
|
||||
}
|
||||
|
||||
fn default_streamable() -> bool {
|
||||
true // Default to true for backwards compatibility with existing queue data
|
||||
}
|
||||
|
||||
/// Repeat mode options
|
||||
|
||||
@@ -3,6 +3,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn default_streamable() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Represents a track in the persisted queue
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistedQueueTrack {
|
||||
@@ -20,6 +24,8 @@ pub struct PersistedQueueTrack {
|
||||
pub is_local: bool,
|
||||
pub album_id: Option<String>,
|
||||
pub artist_id: Option<u64>,
|
||||
#[serde(default = "default_streamable")]
|
||||
pub streamable: bool,
|
||||
}
|
||||
|
||||
/// Represents the full persisted session state
|
||||
@@ -261,6 +267,7 @@ impl SessionStore {
|
||||
is_local: row.get::<_, i64>(9).unwrap_or(0) != 0,
|
||||
album_id: row.get(10)?,
|
||||
artist_id: row.get::<_, Option<i64>>(11)?.map(|v| v as u64),
|
||||
streamable: true, // Default to true for persisted tracks
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Failed to query queue tracks: {}", e))?
|
||||
|
||||
@@ -346,7 +346,8 @@ export function convertQobuzAlbum(album: QobuzAlbum): AlbumDetail {
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
albumId: album.id,
|
||||
artistId: track.performer?.id ?? album.artist?.id,
|
||||
isrc: track.isrc
|
||||
isrc: track.isrc,
|
||||
streamable: track.streamable ?? true
|
||||
})) || [],
|
||||
upc: album.upc
|
||||
};
|
||||
|
||||
@@ -27,6 +27,12 @@ function showToast(message: string, type: ToastType): void {
|
||||
|
||||
export function buildQueueTrackFromQobuz(track: QobuzTrack): BackendQueueTrack {
|
||||
const artwork = track.album?.image?.large || track.album?.image?.thumbnail || track.album?.image?.small || '';
|
||||
|
||||
// Log if track is not streamable (for debugging unavailable tracks)
|
||||
if (track.streamable === false) {
|
||||
console.warn(`[Track] Non-streamable track: "${track.title}" by ${track.performer?.name} (ID: ${track.id})`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
@@ -39,7 +45,8 @@ export function buildQueueTrackFromQobuz(track: QobuzTrack): BackendQueueTrack {
|
||||
sample_rate: track.maximum_sampling_rate ?? null,
|
||||
is_local: false,
|
||||
album_id: track.album?.id || null,
|
||||
artist_id: track.performer?.id ?? null
|
||||
artist_id: track.performer?.id ?? null,
|
||||
streamable: track.streamable ?? true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +58,11 @@ export function buildQueueTrackFromAlbumTrack(
|
||||
albumId?: string,
|
||||
artistId?: number
|
||||
): BackendQueueTrack {
|
||||
// Log if track is not streamable
|
||||
if (track.streamable === false) {
|
||||
console.warn(`[Track] Non-streamable track: "${track.title}" (ID: ${track.id})`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
@@ -63,11 +75,17 @@ export function buildQueueTrackFromAlbumTrack(
|
||||
sample_rate: track.samplingRate ?? null,
|
||||
is_local: false,
|
||||
album_id: track.albumId || albumId || null,
|
||||
artist_id: track.artistId ?? artistId ?? null
|
||||
artist_id: track.artistId ?? artistId ?? null,
|
||||
streamable: track.streamable ?? true
|
||||
};
|
||||
}
|
||||
|
||||
export function buildQueueTrackFromPlaylistTrack(track: PlaylistTrack): BackendQueueTrack {
|
||||
// Log if track is not streamable
|
||||
if (track.streamable === false) {
|
||||
console.warn(`[Track] Non-streamable playlist track: "${track.title}" (ID: ${track.id})`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
@@ -80,7 +98,8 @@ export function buildQueueTrackFromPlaylistTrack(track: PlaylistTrack): BackendQ
|
||||
sample_rate: track.samplingRate ?? null,
|
||||
is_local: false,
|
||||
album_id: track.albumId || null,
|
||||
artist_id: track.artistId ?? null
|
||||
artist_id: track.artistId ?? null,
|
||||
streamable: track.streamable ?? true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +119,8 @@ export function buildQueueTrackFromLocalTrack(track: LocalLibraryTrack): Backend
|
||||
sample_rate: track.sample_rate ?? null,
|
||||
is_local: true,
|
||||
album_id: null, // Local tracks don't have Qobuz album IDs
|
||||
artist_id: null // Local tracks don't have Qobuz artist IDs
|
||||
artist_id: null, // Local tracks don't have Qobuz artist IDs
|
||||
streamable: true // Local tracks are always playable
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface BackendQueueTrack {
|
||||
is_local?: boolean;
|
||||
album_id?: string | null;
|
||||
artist_id?: number | null;
|
||||
/** Whether the track is streamable on Qobuz (false = removed/unavailable) */
|
||||
streamable?: boolean;
|
||||
}
|
||||
|
||||
interface BackendQueueState {
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface QobuzTrack {
|
||||
};
|
||||
performer?: { id?: number; name: string };
|
||||
hires_streamable?: boolean;
|
||||
/** Whether the track is streamable (false = removed/unavailable on Qobuz) */
|
||||
streamable?: boolean;
|
||||
maximum_bit_depth?: number;
|
||||
maximum_sampling_rate?: number;
|
||||
isrc?: string;
|
||||
@@ -153,6 +155,8 @@ export interface Track {
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
isrc?: string;
|
||||
/** Whether the track is streamable (false = unavailable on Qobuz) */
|
||||
streamable?: boolean;
|
||||
}
|
||||
|
||||
export interface AlbumDetail {
|
||||
@@ -277,6 +281,8 @@ export interface PlaylistTrack {
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
isrc?: string;
|
||||
/** Whether the track is streamable (false = unavailable on Qobuz) */
|
||||
streamable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1244,6 +1244,28 @@
|
||||
async function playQueueTrack(track: BackendQueueTrack, skippedIds = new Set<number>()) {
|
||||
const isLocal = isLocalTrack(track.id);
|
||||
|
||||
// DEBUG: Log track metadata from Qobuz
|
||||
console.log('[DEBUG] Track metadata:', {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
album: track.album,
|
||||
duration_secs: track.duration_secs,
|
||||
bit_depth: track.bit_depth,
|
||||
sample_rate: track.sample_rate,
|
||||
hires: track.hires,
|
||||
artwork_url: track.artwork_url,
|
||||
album_id: track.album_id,
|
||||
artist_id: track.artist_id,
|
||||
streamable: track.streamable,
|
||||
isLocal
|
||||
});
|
||||
|
||||
// Warn if trying to play a non-streamable track
|
||||
if (track.streamable === false && !isLocal) {
|
||||
console.warn(`[Player] Attempting to play non-streamable track: "${track.title}" (ID: ${track.id})`);
|
||||
}
|
||||
|
||||
// In offline mode, check if track is available
|
||||
if (offlineStatus.isOffline && !isLocal) {
|
||||
const available = await isTrackAvailable(track);
|
||||
|
||||
Reference in new issue
Block a user