diff --git a/package-lock.json b/package-lock.json index d9fe69f43..e1dcb59d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "qbz-nix", - "version": "1.1.6", + "version": "1.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qbz-nix", - "version": "1.1.6", + "version": "1.1.7", "license": "MIT", "dependencies": { "@tauri-apps/api": "^2", diff --git a/src-tauri/src/api/client.rs b/src-tauri/src/api/client.rs index 101716b01..ee57133a5 100644 --- a/src-tauri/src/api/client.rs +++ b/src-tauri/src/api/client.rs @@ -590,11 +590,13 @@ impl QobuzClient { .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_default(); - // Validate that we got an actual URL +// 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::error!("Stream URL response missing 'url' field. Response: {:?}", json); - return Err(ApiError::ApiResponse("Stream URL response missing 'url' field".to_string())); + // 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 { @@ -622,6 +624,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 { @@ -637,6 +641,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; @@ -644,6 +653,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) } diff --git a/src-tauri/src/api/error.rs b/src-tauri/src/api/error.rs index 18f0cea8b..c11a40368 100644 --- a/src-tauri/src/api/error.rs +++ b/src-tauri/src/api/error.rs @@ -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), diff --git a/src-tauri/src/api/models.rs b/src-tauri/src/api/models.rs index 209926b68..8ee36530f 100644 --- a/src-tauri/src/api/models.rs +++ b/src-tauri/src/api/models.rs @@ -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" }) } } diff --git a/src/lib/app/bootstrap.ts b/src/lib/app/bootstrap.ts index e1171f5f2..182daf5c4 100644 --- a/src/lib/app/bootstrap.ts +++ b/src/lib/app/bootstrap.ts @@ -11,6 +11,7 @@ import { goBack, goForward } from '$lib/stores/navigationStore'; import { loadToastsPreference } from '$lib/stores/toastStore'; import { loadSystemNotificationsPreference, flushScrobbleQueue } from '$lib/services/playbackService'; import { initOfflineStore, cleanupOfflineStore, onOnlineTransition, syncPendingPlaylists } from '$lib/stores/offlineStore'; +import { loadUnavailableTracks } from '$lib/stores/unavailableTracksStore'; import { getNextZoomLevel } from '$lib/utils/zoom'; import { getZoom, setZoom } from '$lib/stores/zoomStore'; @@ -141,6 +142,9 @@ export function bootstrapApp(): BootstrapResult { // Initialize offline store (async, fire-and-forget) initOfflineStore(); + // Load unavailable tracks from localStorage + loadUnavailableTracks(); + // Register callback to flush scrobble queue and sync playlists when transitioning to online onOnlineTransition(() => { console.log('[Bootstrap] Online transition detected - flushing scrobble queue and syncing playlists'); diff --git a/src/lib/components/TrackMenu.svelte b/src/lib/components/TrackMenu.svelte index 9e7412800..773232c4b 100644 --- a/src/lib/components/TrackMenu.svelte +++ b/src/lib/components/TrackMenu.svelte @@ -16,7 +16,8 @@ CloudDownload, RefreshCw, Radio, - Info + Info, + Search } from 'lucide-svelte'; import { shouldHidePlaylistFeatures } from '$lib/utils/offlineHelpers'; import { @@ -37,6 +38,7 @@ onAddFavorite?: () => void; onAddToPlaylist?: () => void; onRemoveFromPlaylist?: () => void; + onFindReplacement?: () => void; onShareQobuz?: () => void; onShareSonglink?: () => void; onGoToAlbum?: () => void; @@ -58,6 +60,7 @@ onAddFavorite, onAddToPlaylist, onRemoveFromPlaylist, + onFindReplacement, onShareQobuz, onShareSonglink, onGoToAlbum, @@ -100,7 +103,7 @@ } const hasPlayback = $derived(!!(onPlayNow || onPlayTrackOnly || onPlayFromHere || onPlayNext || onPlayLater || onCreateRadio)); - const hasLibrary = $derived(!!(onAddFavorite || onAddToPlaylist || onRemoveFromPlaylist)); + const hasLibrary = $derived(!!(onAddFavorite || onAddToPlaylist || onRemoveFromPlaylist || onFindReplacement)); const hasShare = $derived(!!(onShareQobuz || onShareSonglink)); const hasDownload = $derived(!!onDownload || isTrackDownloaded); const hasNav = $derived(!!(onGoToAlbum || onGoToArtist || onShowInfo)); @@ -414,6 +417,12 @@ Remove from playlist {/if} + {#if onFindReplacement} + + {/if} {/if} {#if hasLibrary && (hasShare || hasNav)} diff --git a/src/lib/components/TrackReplacementModal.svelte b/src/lib/components/TrackReplacementModal.svelte new file mode 100644 index 000000000..c73bf0d9a --- /dev/null +++ b/src/lib/components/TrackReplacementModal.svelte @@ -0,0 +1,588 @@ + + +{#if isOpen} + +{/if} + + diff --git a/src/lib/components/TrackRow.svelte b/src/lib/components/TrackRow.svelte index 87d4b4466..34529d4af 100644 --- a/src/lib/components/TrackRow.svelte +++ b/src/lib/components/TrackRow.svelte @@ -1,6 +1,6 @@