From 7657b5841eed7341997389913c596932f7310c0c Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Sat, 11 Apr 2026 17:33:13 -0700 Subject: [PATCH] =?UTF-8?q?The=20Albums=20tab=20was=20being=20populated=20?= =?UTF-8?q?with=20Companion=20API=20IDs=20(companion:Album=20Name|Artist?= =?UTF-8?q?=20Name)=20instead=20of=20real=20Navidrome=20IDs.=20Every=20tim?= =?UTF-8?q?e=20the=20Companion=20sync=20ran,=20it=20overwrote=20the=20vali?= =?UTF-8?q?d=20Subsonic=20album=20cache=20with=20these=20synthetic=20IDs.?= =?UTF-8?q?=20AlbumDetailView=20would=20detect=20the=20companion:=20prefix?= =?UTF-8?q?,=20load=20songs=20from=20the=20Companion=20API=20instead=20of?= =?UTF-8?q?=20Navidrome,=20and=20those=20songs=20have=20Companion=20song?= =?UTF-8?q?=20IDs=20that=20Navidrome=20can=E2=80=99t=20stream.=20The=20Art?= =?UTF-8?q?ist=20=E2=86=92=20Album=20path=20bypassed=20this=20entirely=20b?= =?UTF-8?q?ecause=20it=20navigates=20via=20artistId=20which=20fetches=20al?= =?UTF-8?q?bums=20fresh=20from=20Navidrome=20each=20time.=20After=20instal?= =?UTF-8?q?ling=20this=20and=20doing=20a=20pull-to-refresh,=20the=20Albums?= =?UTF-8?q?=20tab=20will=20use=20real=20Navidrome=20IDs=20again.=20You=20m?= =?UTF-8?q?ay=20need=20to=20clear=20the=20app=E2=80=99s=20cache=20once=20i?= =?UTF-8?q?f=20the=20stale=20Companion=20IDs=20are=20already=20persisted?= =?UTF-8?q?=20=E2=80=94=20Settings=20=E2=86=92=20clear=20library=20cache?= =?UTF-8?q?=20if=20that=20option=20exists,=20or=20just=20force-quit=20and?= =?UTF-8?q?=20relaunch=20after=20refreshing.=E2=80=8B=E2=80=8B=E2=80=8B?= =?UTF-8?q?=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B?= =?UTF-8?q?=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B=E2=80=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Shared/Audio/AudioPlayer.swift | 33 ++++++++++++++++++++++++++++- Shared/Storage/OfflineManager.swift | 2 +- iOS/Data/SyncEngine.swift | 11 +++++----- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Shared/Audio/AudioPlayer.swift b/Shared/Audio/AudioPlayer.swift index 8010fda..7a0d3c1 100644 --- a/Shared/Audio/AudioPlayer.swift +++ b/Shared/Audio/AudioPlayer.swift @@ -705,6 +705,13 @@ class AudioPlayer: NSObject, ObservableObject { // MARK: - AVPlayer Path (streams + fallback) private func playWithAVPlayer(_ url: URL) { + #if os(iOS) + let isStream = url.scheme == "http" || url.scheme == "https" + let display = isStream + ? "\(url.scheme ?? "")://\(url.host ?? "")...\(url.lastPathComponent)" + : url.lastPathComponent + DebugLogger.shared.log("▶ \(display)", category: "Audio") + #endif alog("AVPlayer path: \(url.scheme ?? "file")://...\(url.lastPathComponent)") stopAll() @@ -720,7 +727,30 @@ class AudioPlayer: NSObject, ObservableObject { let asset = AVURLAsset(url: url) playerItem = AVPlayerItem(asset: asset) - + + // Observe item status — catches stream 404s, auth failures, and codec + // errors that AVPlayer swallows silently otherwise + #if os(iOS) + let itemToObserve = playerItem! + Task { @MainActor [weak self] in + for await status in itemToObserve.publisher(for: \.status).values { + guard let self = self, self.playerItem === itemToObserve else { break } + switch status { + case .failed: + let err = itemToObserve.error?.localizedDescription ?? "unknown" + let urlStr = url.absoluteString.contains("stream") ? url.absoluteString : url.lastPathComponent + DebugLogger.shared.log("✗ Playback failed: \(err) | \(urlStr)", + category: "Audio", level: .error) + case .readyToPlay: + DebugLogger.shared.log("✓ Ready: \(url.lastPathComponent)", category: "Audio") + default: + break + } + if status == .failed { break } + } + } + #endif + if player == nil { player = AVPlayer(playerItem: playerItem) } else { @@ -1702,3 +1732,4 @@ class AudioPlayer: NSObject, ObservableObject { stop() } } + diff --git a/Shared/Storage/OfflineManager.swift b/Shared/Storage/OfflineManager.swift index cfe82a0..e6202c5 100644 --- a/Shared/Storage/OfflineManager.swift +++ b/Shared/Storage/OfflineManager.swift @@ -247,7 +247,7 @@ class OfflineManager: ObservableObject { let filename = (ds.localPath as NSString).lastPathComponent let url = downloadDirectory.appendingPathComponent(filename) if fileManager.fileExists(atPath: url.path) { - print("[OfflineManager] ID changed: \(ds.id) → \(song.id) (\(song.title))", flush: true) + print("[OfflineManager] ID changed: \(ds.id) → \(song.id) (\(song.title))") return url } } diff --git a/iOS/Data/SyncEngine.swift b/iOS/Data/SyncEngine.swift index b51c41f..34718de 100644 --- a/iOS/Data/SyncEngine.swift +++ b/iOS/Data/SyncEngine.swift @@ -321,14 +321,15 @@ class SyncEngine: ObservableObject { await setProgress("Syncing Companion library...") DebugLogger.shared.log("Companion sync started", category: "Companion") - // Fetch albums with correct sort order from Companion + // Fetch albums from Companion for sorting/metadata purposes let companionAlbums = try await service.fetchAllAlbums() cache.cacheCompanionAlbums(companionAlbums) - // Overwrite standard album cache with Companion-sorted data so - // MyMusicView and search results immediately reflect correct ordering - let standardAlbums = companionAlbums.map { $0.toAlbum() } - cache.save(standardAlbums, key: "all_albums") + // Do NOT overwrite "all_albums" with Companion IDs. + // Companion album IDs are synthetic ("companion:{name}|{artist}") and + // unknown to Navidrome — AlbumDetailView would fail to stream any song + // because getAlbum(id:) returns nothing for those IDs. + // The Subsonic-fetched all_albums (real Navidrome IDs) stays as-is. DebugLogger.shared.log("Companion sync: \(companionAlbums.count) albums", category: "Companion") await MainActor.run {