react-native-nitro-player 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +2 -0
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +5 -3
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +4 -0
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +14 -13
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +31 -0
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +142 -95
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +7 -6
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +2 -1
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +1 -2
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +3 -2
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +25 -24
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +3 -2
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +20 -19
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +16 -9
  15. package/ios/HybridAudioRoutePicker.swift +1 -1
  16. package/ios/HybridDownloadManager.swift +3 -3
  17. package/ios/HybridEqualizer.swift +3 -3
  18. package/ios/HybridTrackPlayer.swift +8 -4
  19. package/ios/core/NitroPlayerLogger.swift +22 -0
  20. package/ios/core/TrackPlayerCore.swift +195 -256
  21. package/ios/download/DownloadDatabase.swift +72 -49
  22. package/ios/download/DownloadFileManager.swift +24 -17
  23. package/ios/download/DownloadManagerCore.swift +29 -33
  24. package/ios/equalizer/EqualizerCore.swift +25 -20
  25. package/ios/playlist/PlaylistManager.swift +19 -9
  26. package/ios/queue/QueueManager.swift +1 -1
  27. package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
  28. package/lib/types/PlayerQueue.d.ts +1 -1
  29. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
  30. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  31. package/nitrogen/generated/android/c++/JReason.hpp +3 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +2 -1
  34. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +12 -0
  35. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
  36. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  37. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
  38. package/nitrogen/generated/ios/swift/Reason.swift +4 -0
  39. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  40. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
  41. package/nitrogen/generated/shared/c++/Reason.hpp +4 -0
  42. package/package.json +1 -1
  43. package/src/specs/TrackPlayer.nitro.ts +1 -0
  44. package/src/types/PlayerQueue.ts +1 -1
@@ -41,7 +41,7 @@ final class DownloadDatabase {
41
41
  let record = DownloadedTrackRecord(
42
42
  trackId: track.trackId,
43
43
  originalTrack: self.trackItemToRecord(track.originalTrack),
44
- localPath: track.localPath,
44
+ localPath: URL(fileURLWithPath: track.localPath).lastPathComponent,
45
45
  localArtworkPath: self.variantToString(track.localArtworkPath),
46
46
  downloadedAt: track.downloadedAt,
47
47
  fileSize: track.fileSize,
@@ -67,17 +67,16 @@ final class DownloadDatabase {
67
67
  func isTrackDownloaded(trackId: String) -> Bool {
68
68
  return queue.sync {
69
69
  guard let record = downloadedTracks[trackId] else {
70
- print("🔍 DownloadDatabase: Track \(trackId) NOT found in database")
70
+ NitroPlayerLogger.log("DownloadDatabase", "🔍 Track \(trackId) NOT found in database")
71
71
  return false
72
72
  }
73
73
  // Verify file still exists
74
- let exists = FileManager.default.fileExists(atPath: record.localPath)
74
+ let absolutePath = resolveAbsolutePath(for: record)
75
+ let exists = FileManager.default.fileExists(atPath: absolutePath)
75
76
  if exists {
76
- print("✅ DownloadDatabase: Track \(trackId) IS downloaded at \(record.localPath)")
77
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Track \(trackId) IS downloaded at \(absolutePath)")
77
78
  } else {
78
- print(
79
- "❌ DownloadDatabase: Track \(trackId) record exists but file NOT found at \(record.localPath)"
80
- )
79
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Track \(trackId) record exists but file NOT found at \(absolutePath)")
81
80
  }
82
81
  return exists
83
82
  }
@@ -120,20 +119,21 @@ final class DownloadDatabase {
120
119
 
121
120
  func getDownloadedTrack(trackId: String) -> DownloadedTrack? {
122
121
  return queue.sync {
123
- print("🔍 DownloadDatabase.getDownloadedTrack() for trackId: \(trackId)")
124
- print(" Total records in memory: \(downloadedTracks.count)")
125
- print(" Available trackIds: \(Array(downloadedTracks.keys))")
122
+ NitroPlayerLogger.log("DownloadDatabase", "🔍 DownloadDatabase.getDownloadedTrack() for trackId: \(trackId)")
123
+ NitroPlayerLogger.log("DownloadDatabase", " Total records in memory: \(downloadedTracks.count)")
124
+ NitroPlayerLogger.log("DownloadDatabase", " Available trackIds: \(Array(downloadedTracks.keys))")
126
125
 
127
126
  guard let record = downloadedTracks[trackId] else {
128
- print(" ❌ No record found for trackId: \(trackId)")
127
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ No record found for trackId: \(trackId)")
129
128
  return nil
130
129
  }
131
130
 
132
- print(" Found record, checking file at: \(record.localPath)")
131
+ let absolutePath = resolveAbsolutePath(for: record)
132
+ NitroPlayerLogger.log("DownloadDatabase", " Found record, checking file at: \(absolutePath)")
133
133
 
134
134
  // Verify file still exists
135
- guard FileManager.default.fileExists(atPath: record.localPath) else {
136
- print(" ❌ File does NOT exist, cleaning up record")
135
+ guard FileManager.default.fileExists(atPath: absolutePath) else {
136
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ File does NOT exist, cleaning up record")
137
137
  // File was deleted externally, clean up record
138
138
  queue.async(flags: .barrier) {
139
139
  self.downloadedTracks.removeValue(forKey: trackId)
@@ -142,33 +142,33 @@ final class DownloadDatabase {
142
142
  return nil
143
143
  }
144
144
 
145
- print(" ✅ File exists, returning track")
145
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists, returning track")
146
146
  return recordToDownloadedTrack(record)
147
147
  }
148
148
  }
149
149
 
150
150
  func getAllDownloadedTracks() -> [DownloadedTrack] {
151
151
  return queue.sync {
152
- print(
153
- "🎯 DownloadDatabase: getAllDownloadedTracks called, have \(downloadedTracks.count) records")
152
+ NitroPlayerLogger.log("DownloadDatabase", "🎯 getAllDownloadedTracks called, have \(downloadedTracks.count) records")
154
153
 
155
154
  var validTracks: [DownloadedTrack] = []
156
155
  var invalidTrackIds: [String] = []
157
156
 
158
157
  for (trackId, record) in downloadedTracks {
159
- print(" Checking track \(trackId) at path: \(record.localPath)")
160
- if FileManager.default.fileExists(atPath: record.localPath) {
161
- print(" ✅ File exists")
158
+ let absolutePath = resolveAbsolutePath(for: record)
159
+ NitroPlayerLogger.log("DownloadDatabase", " Checking track \(trackId) at path: \(absolutePath)")
160
+ if FileManager.default.fileExists(atPath: absolutePath) {
161
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists")
162
162
  validTracks.append(recordToDownloadedTrack(record))
163
163
  } else {
164
- print(" ❌ File NOT found")
164
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ File NOT found")
165
165
  invalidTrackIds.append(trackId)
166
166
  }
167
167
  }
168
168
 
169
169
  // Clean up invalid records
170
170
  if !invalidTrackIds.isEmpty {
171
- print(" Cleaning up \(invalidTrackIds.count) invalid records")
171
+ NitroPlayerLogger.log("DownloadDatabase", " Cleaning up \(invalidTrackIds.count) invalid records")
172
172
  queue.async(flags: .barrier) {
173
173
  for trackId in invalidTrackIds {
174
174
  self.downloadedTracks.removeValue(forKey: trackId)
@@ -177,7 +177,7 @@ final class DownloadDatabase {
177
177
  }
178
178
  }
179
179
 
180
- print("🎯 DownloadDatabase: Returning \(validTracks.count) valid tracks")
180
+ NitroPlayerLogger.log("DownloadDatabase", "🎯 Returning \(validTracks.count) valid tracks")
181
181
  return validTracks
182
182
  }
183
183
  }
@@ -235,14 +235,15 @@ final class DownloadDatabase {
235
235
  /// Returns the number of orphaned records that were cleaned up
236
236
  func syncDownloads() -> Int {
237
237
  return queue.sync(flags: .barrier) {
238
- print("🔄 DownloadDatabase: syncDownloads called")
238
+ NitroPlayerLogger.log("DownloadDatabase", "🔄 syncDownloads called")
239
239
 
240
240
  var removedCount = 0
241
241
  var trackIdsToRemove: [String] = []
242
242
 
243
243
  for (trackId, record) in downloadedTracks {
244
- if !FileManager.default.fileExists(atPath: record.localPath) {
245
- print(" ❌ Missing file for track \(trackId): \(record.localPath)")
244
+ let absolutePath = resolveAbsolutePath(for: record)
245
+ if !FileManager.default.fileExists(atPath: absolutePath) {
246
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ Missing file for track \(trackId): \(absolutePath)")
246
247
  trackIdsToRemove.append(trackId)
247
248
  }
248
249
  }
@@ -267,9 +268,9 @@ final class DownloadDatabase {
267
268
 
268
269
  if removedCount > 0 {
269
270
  saveToDisk()
270
- print(" ✅ Cleaned up \(removedCount) orphaned records")
271
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ Cleaned up \(removedCount) orphaned records")
271
272
  } else {
272
- print(" ✅ All downloads are valid")
273
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ All downloads are valid")
273
274
  }
274
275
 
275
276
  return removedCount
@@ -283,7 +284,7 @@ final class DownloadDatabase {
283
284
  guard let record = self.downloadedTracks[trackId] else { return }
284
285
 
285
286
  // Delete the file
286
- DownloadFileManager.shared.deleteFile(at: record.localPath)
287
+ DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
287
288
 
288
289
  // Delete artwork if exists
289
290
  if let artworkPath = record.localArtworkPath {
@@ -314,7 +315,7 @@ final class DownloadDatabase {
314
315
  // Delete all tracks in the playlist
315
316
  for trackId in trackIds {
316
317
  if let record = self.downloadedTracks[trackId] {
317
- DownloadFileManager.shared.deleteFile(at: record.localPath)
318
+ DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
318
319
  if let artworkPath = record.localArtworkPath {
319
320
  DownloadFileManager.shared.deleteFile(at: artworkPath)
320
321
  }
@@ -333,7 +334,7 @@ final class DownloadDatabase {
333
334
  queue.async(flags: .barrier) {
334
335
  // Delete all files
335
336
  for record in self.downloadedTracks.values {
336
- DownloadFileManager.shared.deleteFile(at: record.localPath)
337
+ DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
337
338
  if let artworkPath = record.localArtworkPath {
338
339
  DownloadFileManager.shared.deleteFile(at: artworkPath)
339
340
  }
@@ -359,14 +360,14 @@ final class DownloadDatabase {
359
360
  let playlistData = try JSONEncoder().encode(playlistTracksDict)
360
361
  UserDefaults.standard.set(playlistData, forKey: Self.playlistTracksKey)
361
362
  } catch {
362
- print("[DownloadDatabase] Failed to save to disk: \(error)")
363
+ NitroPlayerLogger.log("DownloadDatabase", "Failed to save to disk: \(error)")
363
364
  }
364
365
  }
365
366
 
366
367
  private func loadFromDisk() {
367
- print("\n" + String(repeating: "📀", count: 40))
368
- print("📀 DownloadDatabase: LOADING FROM DISK")
369
- print(String(repeating: "📀", count: 40))
368
+ NitroPlayerLogger.log("DownloadDatabase", "\n" + String(repeating: "📀", count: 40))
369
+ NitroPlayerLogger.log("DownloadDatabase", "📀 LOADING FROM DISK")
370
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40))
370
371
 
371
372
  // Load synchronously to ensure data is available immediately
372
373
  // Load downloaded tracks
@@ -374,20 +375,38 @@ final class DownloadDatabase {
374
375
  do {
375
376
  self.downloadedTracks = try JSONDecoder().decode(
376
377
  [String: DownloadedTrackRecord].self, from: tracksData)
377
- print("✅ DownloadDatabase: Loaded \(self.downloadedTracks.count) tracks from disk")
378
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.downloadedTracks.count) tracks from disk")
379
+
380
+ // Migrate absolute paths → filenames (one-time, for existing installs)
381
+ var needsMigration = false
382
+ for (trackId, record) in self.downloadedTracks {
383
+ if record.localPath.contains("/") {
384
+ let filename = URL(fileURLWithPath: record.localPath).lastPathComponent
385
+ self.downloadedTracks[trackId] = DownloadedTrackRecord(
386
+ trackId: record.trackId,
387
+ originalTrack: record.originalTrack,
388
+ localPath: filename,
389
+ localArtworkPath: record.localArtworkPath,
390
+ downloadedAt: record.downloadedAt,
391
+ fileSize: record.fileSize,
392
+ storageLocation: record.storageLocation
393
+ )
394
+ needsMigration = true
395
+ }
396
+ }
397
+ if needsMigration { self.saveToDisk() }
378
398
 
379
399
  // Log each downloaded track
380
400
  for (trackId, record) in self.downloadedTracks {
381
- print(" 📥 \(trackId)")
382
- print(" Title: \(record.originalTrack.title)")
383
- print(" Path: \(record.localPath)")
384
- print(" Exists: \(FileManager.default.fileExists(atPath: record.localPath))")
401
+ NitroPlayerLogger.log("DownloadDatabase", " 📥 \(trackId)")
402
+ NitroPlayerLogger.log("DownloadDatabase", " Title: \(record.originalTrack.title)")
403
+ NitroPlayerLogger.log("DownloadDatabase", " Path (filename): \(record.localPath)")
385
404
  }
386
405
  } catch {
387
- print("❌ DownloadDatabase: Failed to load tracks from disk: \(error)")
406
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load tracks from disk: \(error)")
388
407
  }
389
408
  } else {
390
- print("⚠️ DownloadDatabase: No saved tracks found in UserDefaults")
409
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No saved tracks found in UserDefaults")
391
410
  }
392
411
 
393
412
  // Load playlist associations
@@ -396,25 +415,29 @@ final class DownloadDatabase {
396
415
  let playlistTracksDict = try JSONDecoder().decode(
397
416
  [String: [String]].self, from: playlistData)
398
417
  self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
399
- print(
400
- "✅ DownloadDatabase: Loaded \(self.playlistTracks.count) playlist associations from disk")
418
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.playlistTracks.count) playlist associations from disk")
401
419
 
402
420
  // Log playlist associations
403
421
  for (playlistId, trackIds) in self.playlistTracks {
404
- print(" 📋 Playlist \(playlistId): \(trackIds.count) tracks")
422
+ NitroPlayerLogger.log("DownloadDatabase", " 📋 Playlist \(playlistId): \(trackIds.count) tracks")
405
423
  }
406
424
  } catch {
407
- print("❌ DownloadDatabase: Failed to load playlist tracks from disk: \(error)")
425
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load playlist tracks from disk: \(error)")
408
426
  }
409
427
  } else {
410
- print("⚠️ DownloadDatabase: No playlist associations found")
428
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No playlist associations found")
411
429
  }
412
430
 
413
- print(String(repeating: "📀", count: 40) + "\n")
431
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
414
432
  }
415
433
 
416
434
  // MARK: - Conversion Helpers
417
435
 
436
+ private func resolveAbsolutePath(for record: DownloadedTrackRecord) -> String {
437
+ let location: StorageLocation = record.storageLocation == "private" ? .private : .public
438
+ return DownloadFileManager.shared.absolutePath(forFilename: record.localPath, storageLocation: location)
439
+ }
440
+
418
441
  /// Convert Variant_NullType_String? to String?
419
442
  private func variantToString(_ variant: Variant_NullType_String?) -> String? {
420
443
  guard let variant = variant else { return nil }
@@ -461,7 +484,7 @@ final class DownloadDatabase {
461
484
  return DownloadedTrack(
462
485
  trackId: record.trackId,
463
486
  originalTrack: recordToTrackItem(record.originalTrack),
464
- localPath: record.localPath,
487
+ localPath: resolveAbsolutePath(for: record),
465
488
  localArtworkPath: stringToVariant(record.localArtworkPath),
466
489
  downloadedAt: record.downloadedAt,
467
490
  fileSize: record.fileSize,
@@ -60,14 +60,14 @@ final class DownloadFileManager {
60
60
  suggestedFilename: String? = nil,
61
61
  httpResponse: HTTPURLResponse? = nil
62
62
  ) -> String? {
63
- print("🎯 DownloadFileManager: saveDownloadedFile called for trackId=\(trackId)")
64
- print(" From: \(temporaryLocation.path)")
65
- print(" Original URL: \(originalURL ?? "nil")")
66
- print(" Suggested Filename: \(suggestedFilename ?? "nil")")
63
+ NitroPlayerLogger.log("DownloadFileManager", "saveDownloadedFile called for trackId=\(trackId)")
64
+ NitroPlayerLogger.log("DownloadFileManager", " From: \(temporaryLocation.path)")
65
+ NitroPlayerLogger.log("DownloadFileManager", " Original URL: \(originalURL ?? "nil")")
66
+ NitroPlayerLogger.log("DownloadFileManager", " Suggested Filename: \(suggestedFilename ?? "nil")")
67
67
 
68
68
  let destinationDirectory =
69
69
  storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
70
- print(" Destination directory: \(destinationDirectory.path)")
70
+ NitroPlayerLogger.log("DownloadFileManager", " Destination directory: \(destinationDirectory.path)")
71
71
 
72
72
  // Determine file extension using headers first, then URL path, then default
73
73
  let fileExtension = Self.resolveFileExtension(
@@ -75,32 +75,32 @@ final class DownloadFileManager {
75
75
  suggestedFilename: suggestedFilename,
76
76
  originalURL: originalURL
77
77
  )
78
- print(" File extension: \(fileExtension)")
78
+ NitroPlayerLogger.log("DownloadFileManager", " File extension: \(fileExtension)")
79
79
 
80
80
  let fileName = "\(trackId).\(fileExtension)"
81
81
  let destinationURL = destinationDirectory.appendingPathComponent(fileName)
82
- print(" Destination: \(destinationURL.path)")
82
+ NitroPlayerLogger.log("DownloadFileManager", " Destination: \(destinationURL.path)")
83
83
 
84
84
  // Verify source file exists
85
85
  guard fileManager.fileExists(atPath: temporaryLocation.path) else {
86
- print("❌ DownloadFileManager: Source file does not exist at \(temporaryLocation.path)")
86
+ NitroPlayerLogger.log("DownloadFileManager", "❌ Source file does not exist at \(temporaryLocation.path)")
87
87
  return nil
88
88
  }
89
89
 
90
90
  do {
91
91
  // Remove existing file if present
92
92
  if fileManager.fileExists(atPath: destinationURL.path) {
93
- print(" Removing existing file at destination")
93
+ NitroPlayerLogger.log("DownloadFileManager", " Removing existing file at destination")
94
94
  try fileManager.removeItem(at: destinationURL)
95
95
  }
96
96
 
97
97
  // Move from temporary location to permanent location
98
98
  try fileManager.moveItem(at: temporaryLocation, to: destinationURL)
99
99
 
100
- print("✅ DownloadFileManager: File saved successfully")
100
+ NitroPlayerLogger.log("DownloadFileManager", "✅ File saved successfully")
101
101
  return destinationURL.path
102
102
  } catch {
103
- print("❌ DownloadFileManager: Failed to save file: \(error)")
103
+ NitroPlayerLogger.log("DownloadFileManager", "❌ Failed to save file: \(error)")
104
104
  return nil
105
105
  }
106
106
  }
@@ -133,7 +133,7 @@ final class DownloadFileManager {
133
133
  // 1. Content-Disposition: attachment; filename="track.mp3"
134
134
  if let disposition = httpResponse?.value(forHTTPHeaderField: "Content-Disposition") {
135
135
  if let ext = extensionFromContentDisposition(disposition), !ext.isEmpty {
136
- print(" [ExtResolve] Content-Disposition → .\(ext)")
136
+ NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] Content-Disposition → .\(ext)")
137
137
  return ext
138
138
  }
139
139
  }
@@ -142,7 +142,7 @@ final class DownloadFileManager {
142
142
  if let contentType = httpResponse?.value(forHTTPHeaderField: "Content-Type") {
143
143
  let mime = contentType.split(separator: ";").first.map(String.init)?.trimmingCharacters(in: .whitespaces) ?? contentType
144
144
  if let ext = mimeTypeToExtension[mime.lowercased()] {
145
- print(" [ExtResolve] Content-Type '\(mime)' → .\(ext)")
145
+ NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] Content-Type '\(mime)' → .\(ext)")
146
146
  return ext
147
147
  }
148
148
  }
@@ -151,7 +151,7 @@ final class DownloadFileManager {
151
151
  if let name = suggestedFilename, !name.isEmpty {
152
152
  let ext = URL(fileURLWithPath: name).pathExtension.lowercased()
153
153
  if !ext.isEmpty && isAudioExtension(ext) {
154
- print(" [ExtResolve] suggestedFilename → .\(ext)")
154
+ NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] suggestedFilename → .\(ext)")
155
155
  return ext
156
156
  }
157
157
  }
@@ -160,12 +160,12 @@ final class DownloadFileManager {
160
160
  if let urlString = originalURL, let url = URL(string: urlString) {
161
161
  let ext = url.pathExtension.lowercased()
162
162
  if !ext.isEmpty && isAudioExtension(ext) {
163
- print(" [ExtResolve] URL path ext → .\(ext)")
163
+ NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] URL path ext → .\(ext)")
164
164
  return ext
165
165
  }
166
166
  }
167
167
 
168
- print(" [ExtResolve] fallback → .mp3")
168
+ NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] fallback → .mp3")
169
169
  return "mp3"
170
170
  }
171
171
 
@@ -200,7 +200,7 @@ final class DownloadFileManager {
200
200
  try fileManager.removeItem(atPath: path)
201
201
  }
202
202
  } catch {
203
- print("[DownloadFileManager] Failed to delete file: \(error)")
203
+ NitroPlayerLogger.log("DownloadFileManager", "Failed to delete file: \(error)")
204
204
  }
205
205
  }
206
206
 
@@ -327,4 +327,11 @@ final class DownloadFileManager {
327
327
 
328
328
  return nil
329
329
  }
330
+
331
+ /// Reconstructs the current absolute path for a stored filename and storage location.
332
+ /// Always uses the current app container path, so it survives container UUID changes.
333
+ func absolutePath(forFilename filename: String, storageLocation: StorageLocation) -> String {
334
+ let dir = storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
335
+ return dir.appendingPathComponent(filename).path
336
+ }
330
337
  }
@@ -372,12 +372,12 @@ final class DownloadManagerCore: NSObject {
372
372
  }
373
373
 
374
374
  func getLocalPath(trackId: String) -> String? {
375
- print("🔍 DownloadManagerCore.getLocalPath() called for trackId: \(trackId)")
375
+ NitroPlayerLogger.log("DownloadManagerCore", "🔍 getLocalPath() called for trackId: \(trackId)")
376
376
  if let downloadedTrack = DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) {
377
- print(" ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
377
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
378
378
  return downloadedTrack.localPath
379
379
  } else {
380
- print(" ❌ No downloaded track found for trackId: \(trackId)")
380
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ No downloaded track found for trackId: \(trackId)")
381
381
  return nil
382
382
  }
383
383
  }
@@ -406,9 +406,7 @@ final class DownloadManagerCore: NSObject {
406
406
  func syncDownloads() -> Int {
407
407
  let removedFromDb = DownloadDatabase.shared.syncDownloads()
408
408
  let bytesFreed = DownloadFileManager.shared.cleanupOrphanedFiles()
409
- print(
410
- "🔄 DownloadManagerCore: syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes"
411
- )
409
+ NitroPlayerLogger.log("DownloadManagerCore", "🔄 syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes")
412
410
  return removedFromDb
413
411
  }
414
412
 
@@ -426,27 +424,27 @@ final class DownloadManagerCore: NSObject {
426
424
 
427
425
  func getEffectiveUrl(track: TrackItem) -> String {
428
426
  let preference = getPlaybackSourcePreference()
429
- print("🔍 DownloadManagerCore.getEffectiveUrl() for track: \(track.id)")
430
- print(" Playback preference: \(preference)")
427
+ NitroPlayerLogger.log("DownloadManagerCore", "🔍 getEffectiveUrl() for track: \(track.id)")
428
+ NitroPlayerLogger.log("DownloadManagerCore", " Playback preference: \(preference)")
431
429
 
432
430
  switch preference {
433
431
  case .network:
434
- print(" → Using network URL (preference=network)")
432
+ NitroPlayerLogger.log("DownloadManagerCore", " → Using network URL (preference=network)")
435
433
  return track.url
436
434
  case .download:
437
435
  if let localPath = getLocalPath(trackId: track.id) {
438
- print(" → Using local path: \(localPath)")
436
+ NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
439
437
  return localPath
440
438
  } else {
441
- print(" → Local path not found, falling back to network URL")
439
+ NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, falling back to network URL")
442
440
  return track.url
443
441
  }
444
442
  case .auto:
445
443
  if let localPath = getLocalPath(trackId: track.id) {
446
- print(" → Using local path: \(localPath)")
444
+ NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
447
445
  return localPath
448
446
  } else {
449
- print(" → Local path not found, using network URL")
447
+ NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, using network URL")
450
448
  return track.url
451
449
  }
452
450
  }
@@ -507,7 +505,7 @@ final class DownloadManagerCore: NSObject {
507
505
 
508
506
  /// Load persisted track metadata and playlist associations (survives app restart)
509
507
  private func loadPersistedMetadata() {
510
- print("📦 DownloadManagerCore: Loading persisted metadata...")
508
+ NitroPlayerLogger.log("DownloadManagerCore", "📦 Loading persisted metadata...")
511
509
 
512
510
  // Load track metadata
513
511
  if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
@@ -516,24 +514,24 @@ final class DownloadManagerCore: NSObject {
516
514
  for (trackId, record) in records {
517
515
  trackMetadata[trackId] = recordToTrackItem(record)
518
516
  }
519
- print(" ✅ Loaded \(trackMetadata.count) track metadata entries")
517
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(trackMetadata.count) track metadata entries")
520
518
  } catch {
521
- print(" ❌ Failed to load track metadata: \(error)")
519
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load track metadata: \(error)")
522
520
  }
523
521
  } else {
524
- print(" ⚠️ No persisted track metadata found")
522
+ NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted track metadata found")
525
523
  }
526
524
 
527
525
  // Load playlist associations
528
526
  if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
529
527
  do {
530
528
  playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
531
- print(" ✅ Loaded \(playlistAssociations.count) playlist associations")
529
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(playlistAssociations.count) playlist associations")
532
530
  } catch {
533
- print(" ❌ Failed to load playlist associations: \(error)")
531
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load playlist associations: \(error)")
534
532
  }
535
533
  } else {
536
- print(" ⚠️ No persisted playlist associations found")
534
+ NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted playlist associations found")
537
535
  }
538
536
  }
539
537
 
@@ -552,7 +550,7 @@ final class DownloadManagerCore: NSObject {
552
550
  let playlistData = try JSONEncoder().encode(playlistAssociations)
553
551
  UserDefaults.standard.set(playlistData, forKey: Self.playlistAssociationsKey)
554
552
  } catch {
555
- print("❌ DownloadManagerCore: Failed to save metadata: \(error)")
553
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save metadata: \(error)")
556
554
  }
557
555
  }
558
556
 
@@ -657,24 +655,22 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
657
655
  _ session: URLSession, downloadTask: URLSessionDownloadTask,
658
656
  didFinishDownloadingTo location: URL
659
657
  ) {
660
- print("🎯 DownloadManagerCore: didFinishDownloadingTo called")
658
+ NitroPlayerLogger.log("DownloadManagerCore", "🎯 didFinishDownloadingTo called")
661
659
 
662
660
  guard let description = downloadTask.taskDescription else {
663
- print("❌ DownloadManagerCore: No task description")
661
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ No task description")
664
662
  return
665
663
  }
666
664
  let parts = description.split(separator: "|")
667
665
  guard parts.count == 2 else {
668
- print("❌ DownloadManagerCore: Invalid task description format: \(description)")
666
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ Invalid task description format: \(description)")
669
667
  return
670
668
  }
671
669
 
672
670
  let downloadId = String(parts[0])
673
671
  let trackId = String(parts[1])
674
672
 
675
- print(
676
- "🎯 DownloadManagerCore: Processing completion for downloadId=\(downloadId), trackId=\(trackId)"
677
- )
673
+ NitroPlayerLogger.log("DownloadManagerCore", "🎯 Processing completion for downloadId=\(downloadId), trackId=\(trackId)")
678
674
 
679
675
  // IMPORTANT: Move file SYNCHRONOUSLY - the temp file is deleted after this method returns!
680
676
  // Get storage location and original URL from track metadata
@@ -698,7 +694,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
698
694
  // Now handle the rest asynchronously
699
695
  queue.async(flags: .barrier) {
700
696
  guard let destinationPath = destinationPath else {
701
- print("❌ DownloadManagerCore: Failed to save file for trackId=\(trackId)")
697
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save file for trackId=\(trackId)")
702
698
  self.taskMetadata[downloadId]?.state = .failed
703
699
  self.taskMetadata[downloadId]?.error = DownloadError(
704
700
  code: "FILE_MOVE_FAILED",
@@ -712,11 +708,11 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
712
708
  return
713
709
  }
714
710
 
715
- print("✅ DownloadManagerCore: File saved to \(destinationPath)")
711
+ NitroPlayerLogger.log("DownloadManagerCore", "✅ File saved to \(destinationPath)")
716
712
 
717
713
  guard let track = self.trackMetadata[trackId] else {
718
- print("❌ DownloadManagerCore: No track metadata for trackId=\(trackId)")
719
- print(" Available trackIds: \(Array(self.trackMetadata.keys))")
714
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ No track metadata for trackId=\(trackId)")
715
+ NitroPlayerLogger.log("DownloadManagerCore", " Available trackIds: \(Array(self.trackMetadata.keys))")
720
716
 
721
717
  // Still mark as completed even if we don't have metadata
722
718
  self.taskMetadata[downloadId]?.state = .completed
@@ -747,7 +743,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
747
743
  // Save to database
748
744
  DownloadDatabase.shared.saveDownloadedTrack(downloadedTrack, playlistId: playlistId)
749
745
 
750
- print("✅ DownloadManagerCore: Track saved to database")
746
+ NitroPlayerLogger.log("DownloadManagerCore", "✅ Track saved to database")
751
747
 
752
748
  // Clean up persisted metadata (no longer needed after completion)
753
749
  self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
@@ -760,7 +756,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
760
756
  self.activeTasks.removeValue(forKey: downloadId)
761
757
 
762
758
  // Notify
763
- print("✅ DownloadManagerCore: Notifying completion for trackId=\(trackId)")
759
+ NitroPlayerLogger.log("DownloadManagerCore", "✅ Notifying completion for trackId=\(trackId)")
764
760
  self.notifyStateChange(
765
761
  downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
766
762
  self.notifyComplete(downloadedTrack)