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 {