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:
vicrodh committed 2026-01-31 16:10:26 -06:00
1 parent 3926209b0d
commit e68c3128b6
6 files changed
+62 -2

No files matched your search

+23 -1
View File
@@ -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)
}
+3
View File
@@ -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),
+2
View File
@@ -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"
})
}
}