feat: auto-skip unavailable tracks with user-friendly error
Backend: - Add TrackUnavailable error type for tracks removed from Qobuz - Validate stream URL response - detect missing URL with restrictions - Add SampleRestrictedByRightHolders to restriction check - Return specific error when track is unavailable vs quality unavailable Frontend: - Detect track unavailable errors in playTrack() - Show friendly toast: Track title is no longer available - Auto-skip to next track in queue after 500ms delay - Add i18n translations (en/es) for trackUnavailable message
This commit is contained in:
1 parent
3926209b0d
commit
e68c3128b6
6 files changed
+62
-2
No files matched your search
@@ -589,8 +589,17 @@ impl QobuzClient {
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Validate that we got an actual URL (track may be unavailable)
|
||||
let url = json["url"].as_str().unwrap_or("").to_string();
|
||||
if url.is_empty() {
|
||||
// Log the restriction codes for debugging
|
||||
let restriction_codes: Vec<&str> = restrictions.iter().map(|r| r.code.as_str()).collect();
|
||||
log::warn!("Stream URL missing for track {} - restrictions: {:?}", track_id, restriction_codes);
|
||||
return Err(ApiError::TrackUnavailable(track_id));
|
||||
}
|
||||
|
||||
Ok(StreamUrl {
|
||||
url: json["url"].as_str().unwrap_or("").to_string(),
|
||||
url,
|
||||
format_id: json["format_id"].as_u64().unwrap_or(0) as u32,
|
||||
mime_type: json["mime_type"].as_str().unwrap_or("").to_string(),
|
||||
sampling_rate: json["sampling_rate"].as_f64().unwrap_or(0.0),
|
||||
@@ -614,6 +623,8 @@ impl QobuzClient {
|
||||
let qualities = Quality::fallback_order();
|
||||
let start_idx = qualities.iter().position(|q| *q == preferred).unwrap_or(0);
|
||||
|
||||
let mut track_unavailable = false;
|
||||
|
||||
for quality in &qualities[start_idx..] {
|
||||
log::info!("Trying quality: {:?}", quality);
|
||||
match self.get_stream_url(track_id, *quality).await {
|
||||
@@ -629,6 +640,11 @@ impl QobuzClient {
|
||||
log::error!("Invalid app secret");
|
||||
return Err(ApiError::InvalidAppSecret);
|
||||
},
|
||||
Err(ApiError::TrackUnavailable(_)) => {
|
||||
// Track is completely unavailable on Qobuz
|
||||
track_unavailable = true;
|
||||
continue;
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("Quality {:?} failed: {}, trying next", quality, e);
|
||||
continue;
|
||||
@@ -636,6 +652,12 @@ impl QobuzClient {
|
||||
}
|
||||
}
|
||||
|
||||
// If all quality levels reported track unavailable, return that specific error
|
||||
if track_unavailable {
|
||||
log::error!("Track {} is no longer available on Qobuz", track_id);
|
||||
return Err(ApiError::TrackUnavailable(track_id));
|
||||
}
|
||||
|
||||
log::error!("No quality available for track {}", track_id);
|
||||
Err(ApiError::NoQualityAvailable)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ pub enum ApiError {
|
||||
#[error("No valid quality available for this track")]
|
||||
NoQualityAvailable,
|
||||
|
||||
#[error("Track {0} is no longer available on Qobuz")]
|
||||
TrackUnavailable(u64),
|
||||
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(#[from] reqwest::Error),
|
||||
|
||||
|
||||
@@ -67,9 +67,11 @@ pub struct StreamUrl {
|
||||
}
|
||||
|
||||
impl StreamUrl {
|
||||
/// Check if the stream has restrictions that prevent playback
|
||||
pub fn has_restrictions(&self) -> bool {
|
||||
self.restrictions.iter().any(|r| {
|
||||
r.code == "FormatRestrictedByFormatAvailability"
|
||||
|| r.code == "SampleRestrictedByRightHolders"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@
|
||||
"lyrics": "Lyrics",
|
||||
"noLyrics": "No lyrics available",
|
||||
"fetchingLyrics": "Fetching lyrics...",
|
||||
"trackUnavailable": "is no longer available",
|
||||
"shuffle": "Shuffle",
|
||||
"repeat": "Repeat",
|
||||
"repeatAll": "Repeat All",
|
||||
|
||||
@@ -253,6 +253,7 @@
|
||||
"lyrics": "Letras",
|
||||
"noLyrics": "No hay letras disponibles",
|
||||
"fetchingLyrics": "Buscando letras...",
|
||||
"trackUnavailable": "ya no está disponible",
|
||||
"shuffle": "Aleatorio",
|
||||
"repeat": "Repetir",
|
||||
"repeatAll": "Repetir Todo",
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
setIsFavorite,
|
||||
type PlayingTrack
|
||||
} from '$lib/stores/playerStore';
|
||||
import { syncQueueState } from '$lib/stores/queueStore';
|
||||
import { syncQueueState, nextTrack } from '$lib/stores/queueStore';
|
||||
import { logRecoEvent } from '$lib/services/recoService';
|
||||
import {
|
||||
isCasting,
|
||||
@@ -199,6 +199,37 @@ export async function playTrack(
|
||||
} catch (err) {
|
||||
console.error('Failed to play track:', err);
|
||||
dismissBuffering();
|
||||
|
||||
// Check if track is unavailable on Qobuz
|
||||
const errorStr = String(err);
|
||||
if (errorStr.includes('no longer available') || errorStr.includes('TrackUnavailable')) {
|
||||
showToast(`"${track.title}" is no longer available`, 'error');
|
||||
|
||||
// Auto-skip to next track
|
||||
const next = await nextTrack();
|
||||
if (next) {
|
||||
console.log('Auto-skipping to next track:', next.title);
|
||||
// Small delay to let the toast be visible
|
||||
setTimeout(() => {
|
||||
playTrack({
|
||||
id: next.id,
|
||||
title: next.title,
|
||||
artist: next.artist,
|
||||
album: next.album,
|
||||
duration: next.duration_secs,
|
||||
artwork: next.artwork_url || undefined,
|
||||
albumId: next.album_id || undefined,
|
||||
artistId: next.artist_id || undefined,
|
||||
bitDepth: next.bit_depth || undefined,
|
||||
samplingRate: next.sample_rate || undefined
|
||||
}, { showLoadingToast: true, showSuccessToast: true });
|
||||
}, 500);
|
||||
} else {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
showToast(`Playback error: ${err}`, 'error');
|
||||
setIsPlaying(false);
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user