react-native-nitro-player 0.5.3 → 0.5.5

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 (45) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +5 -3
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +4 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +14 -13
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +31 -0
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +142 -95
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +75 -29
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +2 -1
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +1 -2
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +3 -2
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +25 -24
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +3 -2
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +20 -19
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +119 -85
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +50 -0
  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 +92 -62
  22. package/ios/download/DownloadFileManager.swift +17 -17
  23. package/ios/download/DownloadManagerCore.swift +80 -44
  24. package/ios/equalizer/EqualizerCore.swift +25 -20
  25. package/ios/playlist/PlaylistManager.swift +113 -82
  26. package/ios/queue/QueueManager.swift +1 -1
  27. package/ios/storage/NitroPlayerStorage.swift +44 -0
  28. package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
  29. package/lib/types/PlayerQueue.d.ts +1 -1
  30. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
  31. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  32. package/nitrogen/generated/android/c++/JReason.hpp +3 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +2 -1
  35. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +12 -0
  36. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
  37. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  38. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
  39. package/nitrogen/generated/ios/swift/Reason.swift +4 -0
  40. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  41. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
  42. package/nitrogen/generated/shared/c++/Reason.hpp +4 -0
  43. package/package.json +1 -1
  44. package/src/specs/TrackPlayer.nitro.ts +1 -0
  45. package/src/types/PlayerQueue.ts +1 -1
@@ -8,17 +8,17 @@
8
8
  import Foundation
9
9
  import NitroModules
10
10
 
11
- /// Manages persistence of downloaded track metadata using UserDefaults
11
+ /// Manages persistence of downloaded track metadata using file storage
12
12
  final class DownloadDatabase {
13
13
 
14
14
  // MARK: - Singleton
15
15
 
16
16
  static let shared = DownloadDatabase()
17
17
 
18
- // MARK: - Constants
18
+ // MARK: - Legacy UserDefaults Keys (migration only)
19
19
 
20
- private static let downloadedTracksKey = "NitroPlayerDownloadedTracks"
21
- private static let playlistTracksKey = "NitroPlayerPlaylistTracks"
20
+ private static let legacyDownloadedTracksKey = "NitroPlayerDownloadedTracks"
21
+ private static let legacyPlaylistTracksKey = "NitroPlayerPlaylistTracks"
22
22
 
23
23
  // MARK: - Properties
24
24
 
@@ -67,18 +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
74
  let absolutePath = resolveAbsolutePath(for: record)
75
75
  let exists = FileManager.default.fileExists(atPath: absolutePath)
76
76
  if exists {
77
- print("✅ DownloadDatabase: Track \(trackId) IS downloaded at \(absolutePath)")
77
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Track \(trackId) IS downloaded at \(absolutePath)")
78
78
  } else {
79
- print(
80
- "❌ DownloadDatabase: Track \(trackId) record exists but file NOT found at \(absolutePath)"
81
- )
79
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Track \(trackId) record exists but file NOT found at \(absolutePath)")
82
80
  }
83
81
  return exists
84
82
  }
@@ -121,21 +119,21 @@ final class DownloadDatabase {
121
119
 
122
120
  func getDownloadedTrack(trackId: String) -> DownloadedTrack? {
123
121
  return queue.sync {
124
- print("🔍 DownloadDatabase.getDownloadedTrack() for trackId: \(trackId)")
125
- print(" Total records in memory: \(downloadedTracks.count)")
126
- 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))")
127
125
 
128
126
  guard let record = downloadedTracks[trackId] else {
129
- print(" ❌ No record found for trackId: \(trackId)")
127
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ No record found for trackId: \(trackId)")
130
128
  return nil
131
129
  }
132
130
 
133
131
  let absolutePath = resolveAbsolutePath(for: record)
134
- print(" Found record, checking file at: \(absolutePath)")
132
+ NitroPlayerLogger.log("DownloadDatabase", " Found record, checking file at: \(absolutePath)")
135
133
 
136
134
  // Verify file still exists
137
135
  guard FileManager.default.fileExists(atPath: absolutePath) else {
138
- print(" ❌ File does NOT exist, cleaning up record")
136
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ File does NOT exist, cleaning up record")
139
137
  // File was deleted externally, clean up record
140
138
  queue.async(flags: .barrier) {
141
139
  self.downloadedTracks.removeValue(forKey: trackId)
@@ -144,34 +142,33 @@ final class DownloadDatabase {
144
142
  return nil
145
143
  }
146
144
 
147
- print(" ✅ File exists, returning track")
145
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists, returning track")
148
146
  return recordToDownloadedTrack(record)
149
147
  }
150
148
  }
151
149
 
152
150
  func getAllDownloadedTracks() -> [DownloadedTrack] {
153
151
  return queue.sync {
154
- print(
155
- "🎯 DownloadDatabase: getAllDownloadedTracks called, have \(downloadedTracks.count) records")
152
+ NitroPlayerLogger.log("DownloadDatabase", "🎯 getAllDownloadedTracks called, have \(downloadedTracks.count) records")
156
153
 
157
154
  var validTracks: [DownloadedTrack] = []
158
155
  var invalidTrackIds: [String] = []
159
156
 
160
157
  for (trackId, record) in downloadedTracks {
161
158
  let absolutePath = resolveAbsolutePath(for: record)
162
- print(" Checking track \(trackId) at path: \(absolutePath)")
159
+ NitroPlayerLogger.log("DownloadDatabase", " Checking track \(trackId) at path: \(absolutePath)")
163
160
  if FileManager.default.fileExists(atPath: absolutePath) {
164
- print(" ✅ File exists")
161
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists")
165
162
  validTracks.append(recordToDownloadedTrack(record))
166
163
  } else {
167
- print(" ❌ File NOT found")
164
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ File NOT found")
168
165
  invalidTrackIds.append(trackId)
169
166
  }
170
167
  }
171
168
 
172
169
  // Clean up invalid records
173
170
  if !invalidTrackIds.isEmpty {
174
- print(" Cleaning up \(invalidTrackIds.count) invalid records")
171
+ NitroPlayerLogger.log("DownloadDatabase", " Cleaning up \(invalidTrackIds.count) invalid records")
175
172
  queue.async(flags: .barrier) {
176
173
  for trackId in invalidTrackIds {
177
174
  self.downloadedTracks.removeValue(forKey: trackId)
@@ -180,7 +177,7 @@ final class DownloadDatabase {
180
177
  }
181
178
  }
182
179
 
183
- print("🎯 DownloadDatabase: Returning \(validTracks.count) valid tracks")
180
+ NitroPlayerLogger.log("DownloadDatabase", "🎯 Returning \(validTracks.count) valid tracks")
184
181
  return validTracks
185
182
  }
186
183
  }
@@ -238,7 +235,7 @@ final class DownloadDatabase {
238
235
  /// Returns the number of orphaned records that were cleaned up
239
236
  func syncDownloads() -> Int {
240
237
  return queue.sync(flags: .barrier) {
241
- print("🔄 DownloadDatabase: syncDownloads called")
238
+ NitroPlayerLogger.log("DownloadDatabase", "🔄 syncDownloads called")
242
239
 
243
240
  var removedCount = 0
244
241
  var trackIdsToRemove: [String] = []
@@ -246,7 +243,7 @@ final class DownloadDatabase {
246
243
  for (trackId, record) in downloadedTracks {
247
244
  let absolutePath = resolveAbsolutePath(for: record)
248
245
  if !FileManager.default.fileExists(atPath: absolutePath) {
249
- print(" ❌ Missing file for track \(trackId): \(absolutePath)")
246
+ NitroPlayerLogger.log("DownloadDatabase", " ❌ Missing file for track \(trackId): \(absolutePath)")
250
247
  trackIdsToRemove.append(trackId)
251
248
  }
252
249
  }
@@ -271,9 +268,9 @@ final class DownloadDatabase {
271
268
 
272
269
  if removedCount > 0 {
273
270
  saveToDisk()
274
- print(" ✅ Cleaned up \(removedCount) orphaned records")
271
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ Cleaned up \(removedCount) orphaned records")
275
272
  } else {
276
- print(" ✅ All downloads are valid")
273
+ NitroPlayerLogger.log("DownloadDatabase", " ✅ All downloads are valid")
277
274
  }
278
275
 
279
276
  return removedCount
@@ -356,32 +353,67 @@ final class DownloadDatabase {
356
353
  private func saveToDisk() {
357
354
  do {
358
355
  let tracksData = try JSONEncoder().encode(downloadedTracks)
359
- UserDefaults.standard.set(tracksData, forKey: Self.downloadedTracksKey)
360
-
361
356
  // Convert Set to Array for encoding
362
357
  let playlistTracksDict = playlistTracks.mapValues { Array($0) }
363
358
  let playlistData = try JSONEncoder().encode(playlistTracksDict)
364
- UserDefaults.standard.set(playlistData, forKey: Self.playlistTracksKey)
359
+
360
+ // Combine both into a single JSON wrapper object
361
+ guard let tracksJson = try JSONSerialization.jsonObject(with: tracksData) as? [String: Any],
362
+ let playlistJson = try JSONSerialization.jsonObject(with: playlistData) as? [String: Any]
363
+ else { return }
364
+
365
+ let wrapper: [String: Any] = [
366
+ "downloadedTracks": tracksJson,
367
+ "playlistTracks": playlistJson,
368
+ ]
369
+ let data = try JSONSerialization.data(withJSONObject: wrapper, options: [])
370
+ try NitroPlayerStorage.write(filename: "downloads.json", data: data)
365
371
  } catch {
366
- print("[DownloadDatabase] Failed to save to disk: \(error)")
372
+ NitroPlayerLogger.log("DownloadDatabase", "Failed to save to disk: \(error)")
367
373
  }
368
374
  }
369
375
 
370
376
  private func loadFromDisk() {
371
- print("\n" + String(repeating: "📀", count: 40))
372
- print("📀 DownloadDatabase: LOADING FROM DISK")
373
- print(String(repeating: "📀", count: 40))
377
+ NitroPlayerLogger.log("DownloadDatabase", "\n" + String(repeating: "📀", count: 40))
378
+ NitroPlayerLogger.log("DownloadDatabase", "📀 LOADING FROM DISK")
379
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40))
380
+
381
+ // 1. Try new JSON file (post-migration)
382
+ if let data = NitroPlayerStorage.read(filename: "downloads.json") {
383
+ do {
384
+ if let wrapper = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
385
+ if let tracksObj = wrapper["downloadedTracks"] as? [String: Any] {
386
+ let tracksData = try JSONSerialization.data(withJSONObject: tracksObj)
387
+ self.downloadedTracks = try JSONDecoder().decode(
388
+ [String: DownloadedTrackRecord].self, from: tracksData)
389
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.downloadedTracks.count) tracks from file")
390
+ }
391
+ if let playlistObj = wrapper["playlistTracks"] as? [String: Any] {
392
+ let playlistData = try JSONSerialization.data(withJSONObject: playlistObj)
393
+ let playlistTracksDict = try JSONDecoder().decode(
394
+ [String: [String]].self, from: playlistData)
395
+ self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
396
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.playlistTracks.count) playlist associations from file")
397
+ }
398
+ }
399
+ } catch {
400
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load from file: \(error)")
401
+ }
402
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
403
+ return
404
+ }
405
+
406
+ // 2. Migrate from UserDefaults (one-time, existing installs)
407
+ var didMigrate = false
374
408
 
375
- // Load synchronously to ensure data is available immediately
376
- // Load downloaded tracks
377
- if let tracksData = UserDefaults.standard.data(forKey: Self.downloadedTracksKey) {
409
+ if let tracksData = UserDefaults.standard.data(forKey: Self.legacyDownloadedTracksKey) {
378
410
  do {
379
411
  self.downloadedTracks = try JSONDecoder().decode(
380
412
  [String: DownloadedTrackRecord].self, from: tracksData)
381
- print("✅ DownloadDatabase: Loaded \(self.downloadedTracks.count) tracks from disk")
413
+ NitroPlayerLogger.log("DownloadDatabase", "Migrated \(self.downloadedTracks.count) tracks from UserDefaults")
382
414
 
383
- // Migrate absolute paths → filenames (one-time, for existing installs)
384
- var needsMigration = false
415
+ // Migrate absolute paths → filenames (pre-existing migration)
416
+ var needsPathMigration = false
385
417
  for (trackId, record) in self.downloadedTracks {
386
418
  if record.localPath.contains("/") {
387
419
  let filename = URL(fileURLWithPath: record.localPath).lastPathComponent
@@ -394,45 +426,43 @@ final class DownloadDatabase {
394
426
  fileSize: record.fileSize,
395
427
  storageLocation: record.storageLocation
396
428
  )
397
- needsMigration = true
429
+ needsPathMigration = true
398
430
  }
399
431
  }
400
- if needsMigration { self.saveToDisk() }
401
-
402
- // Log each downloaded track
403
- for (trackId, record) in self.downloadedTracks {
404
- print(" 📥 \(trackId)")
405
- print(" Title: \(record.originalTrack.title)")
406
- print(" Path (filename): \(record.localPath)")
432
+ if needsPathMigration {
433
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated absolute paths to filenames")
407
434
  }
435
+
436
+ UserDefaults.standard.removeObject(forKey: Self.legacyDownloadedTracksKey)
437
+ didMigrate = true
408
438
  } catch {
409
- print("❌ DownloadDatabase: Failed to load tracks from disk: \(error)")
439
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to migrate tracks from UserDefaults: \(error)")
410
440
  }
411
441
  } else {
412
- print("⚠️ DownloadDatabase: No saved tracks found in UserDefaults")
442
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No saved tracks found in UserDefaults")
413
443
  }
414
444
 
415
- // Load playlist associations
416
- if let playlistData = UserDefaults.standard.data(forKey: Self.playlistTracksKey) {
445
+ if let playlistData = UserDefaults.standard.data(forKey: Self.legacyPlaylistTracksKey) {
417
446
  do {
418
447
  let playlistTracksDict = try JSONDecoder().decode(
419
448
  [String: [String]].self, from: playlistData)
420
449
  self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
421
- print(
422
- "✅ DownloadDatabase: Loaded \(self.playlistTracks.count) playlist associations from disk")
423
-
424
- // Log playlist associations
425
- for (playlistId, trackIds) in self.playlistTracks {
426
- print(" 📋 Playlist \(playlistId): \(trackIds.count) tracks")
427
- }
450
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated \(self.playlistTracks.count) playlist associations from UserDefaults")
451
+ UserDefaults.standard.removeObject(forKey: Self.legacyPlaylistTracksKey)
452
+ didMigrate = true
428
453
  } catch {
429
- print("❌ DownloadDatabase: Failed to load playlist tracks from disk: \(error)")
454
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to migrate playlist tracks from UserDefaults: \(error)")
430
455
  }
431
456
  } else {
432
- print("⚠️ DownloadDatabase: No playlist associations found")
457
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No playlist associations found in UserDefaults")
458
+ }
459
+
460
+ if didMigrate {
461
+ // Persist migrated data in new file format
462
+ saveToDisk()
433
463
  }
434
464
 
435
- print(String(repeating: "📀", count: 40) + "\n")
465
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
436
466
  }
437
467
 
438
468
  // MARK: - Conversion Helpers
@@ -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
 
@@ -18,8 +18,9 @@ final class DownloadManagerCore: NSObject {
18
18
  // MARK: - Constants
19
19
 
20
20
  private static let backgroundSessionIdentifier = "com.nitroplayer.backgroundDownloads"
21
- private static let trackMetadataKey = "NitroPlayerTrackMetadata"
22
- private static let playlistAssociationsKey = "NitroPlayerPlaylistAssociations"
21
+ // Legacy UserDefaults keys (migration only)
22
+ private static let legacyTrackMetadataKey = "NitroPlayerTrackMetadata"
23
+ private static let legacyPlaylistAssociationsKey = "NitroPlayerPlaylistAssociations"
23
24
 
24
25
  // MARK: - Properties
25
26
 
@@ -372,12 +373,12 @@ final class DownloadManagerCore: NSObject {
372
373
  }
373
374
 
374
375
  func getLocalPath(trackId: String) -> String? {
375
- print("🔍 DownloadManagerCore.getLocalPath() called for trackId: \(trackId)")
376
+ NitroPlayerLogger.log("DownloadManagerCore", "🔍 getLocalPath() called for trackId: \(trackId)")
376
377
  if let downloadedTrack = DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) {
377
- print(" ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
378
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
378
379
  return downloadedTrack.localPath
379
380
  } else {
380
- print(" ❌ No downloaded track found for trackId: \(trackId)")
381
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ No downloaded track found for trackId: \(trackId)")
381
382
  return nil
382
383
  }
383
384
  }
@@ -406,9 +407,7 @@ final class DownloadManagerCore: NSObject {
406
407
  func syncDownloads() -> Int {
407
408
  let removedFromDb = DownloadDatabase.shared.syncDownloads()
408
409
  let bytesFreed = DownloadFileManager.shared.cleanupOrphanedFiles()
409
- print(
410
- "🔄 DownloadManagerCore: syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes"
411
- )
410
+ NitroPlayerLogger.log("DownloadManagerCore", "🔄 syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes")
412
411
  return removedFromDb
413
412
  }
414
413
 
@@ -426,27 +425,27 @@ final class DownloadManagerCore: NSObject {
426
425
 
427
426
  func getEffectiveUrl(track: TrackItem) -> String {
428
427
  let preference = getPlaybackSourcePreference()
429
- print("🔍 DownloadManagerCore.getEffectiveUrl() for track: \(track.id)")
430
- print(" Playback preference: \(preference)")
428
+ NitroPlayerLogger.log("DownloadManagerCore", "🔍 getEffectiveUrl() for track: \(track.id)")
429
+ NitroPlayerLogger.log("DownloadManagerCore", " Playback preference: \(preference)")
431
430
 
432
431
  switch preference {
433
432
  case .network:
434
- print(" → Using network URL (preference=network)")
433
+ NitroPlayerLogger.log("DownloadManagerCore", " → Using network URL (preference=network)")
435
434
  return track.url
436
435
  case .download:
437
436
  if let localPath = getLocalPath(trackId: track.id) {
438
- print(" → Using local path: \(localPath)")
437
+ NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
439
438
  return localPath
440
439
  } else {
441
- print(" → Local path not found, falling back to network URL")
440
+ NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, falling back to network URL")
442
441
  return track.url
443
442
  }
444
443
  case .auto:
445
444
  if let localPath = getLocalPath(trackId: track.id) {
446
- print(" → Using local path: \(localPath)")
445
+ NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
447
446
  return localPath
448
447
  } else {
449
- print(" → Local path not found, using network URL")
448
+ NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, using network URL")
450
449
  return track.url
451
450
  }
452
451
  }
@@ -507,52 +506,91 @@ final class DownloadManagerCore: NSObject {
507
506
 
508
507
  /// Load persisted track metadata and playlist associations (survives app restart)
509
508
  private func loadPersistedMetadata() {
510
- print("📦 DownloadManagerCore: Loading persisted metadata...")
509
+ NitroPlayerLogger.log("DownloadManagerCore", "📦 Loading persisted metadata...")
510
+
511
+ // 1. Try new JSON file (post-migration)
512
+ if let data = NitroPlayerStorage.read(filename: "download_metadata.json") {
513
+ do {
514
+ if let wrapper = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
515
+ if let tracksObj = wrapper["trackMetadata"] as? [String: Any] {
516
+ let tracksData = try JSONSerialization.data(withJSONObject: tracksObj)
517
+ let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: tracksData)
518
+ for (trackId, record) in records {
519
+ trackMetadata[trackId] = recordToTrackItem(record)
520
+ }
521
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(trackMetadata.count) track metadata entries from file")
522
+ }
523
+ if let assocObj = wrapper["playlistAssociations"] as? [String: String] {
524
+ playlistAssociations = assocObj
525
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(playlistAssociations.count) playlist associations from file")
526
+ }
527
+ }
528
+ } catch {
529
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load metadata from file: \(error)")
530
+ }
531
+ return
532
+ }
533
+
534
+ // 2. Migrate from UserDefaults (one-time, existing installs)
535
+ var didMigrate = false
511
536
 
512
- // Load track metadata
513
- if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
537
+ if let data = UserDefaults.standard.data(forKey: Self.legacyTrackMetadataKey) {
514
538
  do {
515
539
  let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: data)
516
540
  for (trackId, record) in records {
517
541
  trackMetadata[trackId] = recordToTrackItem(record)
518
542
  }
519
- print(" ✅ Loaded \(trackMetadata.count) track metadata entries")
543
+ NitroPlayerLogger.log("DownloadManagerCore", "Migrated \(trackMetadata.count) track metadata entries from UserDefaults")
544
+ UserDefaults.standard.removeObject(forKey: Self.legacyTrackMetadataKey)
545
+ didMigrate = true
520
546
  } catch {
521
- print(" ❌ Failed to load track metadata: \(error)")
547
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to migrate track metadata: \(error)")
522
548
  }
523
549
  } else {
524
- print(" ⚠️ No persisted track metadata found")
550
+ NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted track metadata found")
525
551
  }
526
552
 
527
- // Load playlist associations
528
- if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
553
+ if let data = UserDefaults.standard.data(forKey: Self.legacyPlaylistAssociationsKey) {
529
554
  do {
530
555
  playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
531
- print(" ✅ Loaded \(playlistAssociations.count) playlist associations")
556
+ NitroPlayerLogger.log("DownloadManagerCore", "Migrated \(playlistAssociations.count) playlist associations from UserDefaults")
557
+ UserDefaults.standard.removeObject(forKey: Self.legacyPlaylistAssociationsKey)
558
+ didMigrate = true
532
559
  } catch {
533
- print(" ❌ Failed to load playlist associations: \(error)")
560
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to migrate playlist associations: \(error)")
534
561
  }
535
562
  } else {
536
- print(" ⚠️ No persisted playlist associations found")
563
+ NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted playlist associations found")
564
+ }
565
+
566
+ if didMigrate {
567
+ savePersistedMetadata()
537
568
  }
538
569
  }
539
570
 
540
571
  /// Persist track metadata and playlist associations to disk
541
572
  private func savePersistedMetadata() {
542
- // Convert TrackItem to TrackItemRecord for encoding
543
573
  var records: [String: TrackItemRecord] = [:]
544
574
  for (trackId, track) in trackMetadata {
545
575
  records[trackId] = trackItemToRecord(track)
546
576
  }
547
577
 
548
578
  do {
549
- let trackData = try JSONEncoder().encode(records)
550
- UserDefaults.standard.set(trackData, forKey: Self.trackMetadataKey)
551
-
579
+ let tracksData = try JSONEncoder().encode(records)
552
580
  let playlistData = try JSONEncoder().encode(playlistAssociations)
553
- UserDefaults.standard.set(playlistData, forKey: Self.playlistAssociationsKey)
581
+
582
+ guard let tracksJson = try JSONSerialization.jsonObject(with: tracksData) as? [String: Any],
583
+ let assocJson = try JSONSerialization.jsonObject(with: playlistData) as? [String: Any]
584
+ else { return }
585
+
586
+ let wrapper: [String: Any] = [
587
+ "trackMetadata": tracksJson,
588
+ "playlistAssociations": assocJson,
589
+ ]
590
+ let data = try JSONSerialization.data(withJSONObject: wrapper, options: [])
591
+ try NitroPlayerStorage.write(filename: "download_metadata.json", data: data)
554
592
  } catch {
555
- print("❌ DownloadManagerCore: Failed to save metadata: \(error)")
593
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save metadata: \(error)")
556
594
  }
557
595
  }
558
596
 
@@ -657,24 +695,22 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
657
695
  _ session: URLSession, downloadTask: URLSessionDownloadTask,
658
696
  didFinishDownloadingTo location: URL
659
697
  ) {
660
- print("🎯 DownloadManagerCore: didFinishDownloadingTo called")
698
+ NitroPlayerLogger.log("DownloadManagerCore", "🎯 didFinishDownloadingTo called")
661
699
 
662
700
  guard let description = downloadTask.taskDescription else {
663
- print("❌ DownloadManagerCore: No task description")
701
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ No task description")
664
702
  return
665
703
  }
666
704
  let parts = description.split(separator: "|")
667
705
  guard parts.count == 2 else {
668
- print("❌ DownloadManagerCore: Invalid task description format: \(description)")
706
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ Invalid task description format: \(description)")
669
707
  return
670
708
  }
671
709
 
672
710
  let downloadId = String(parts[0])
673
711
  let trackId = String(parts[1])
674
712
 
675
- print(
676
- "🎯 DownloadManagerCore: Processing completion for downloadId=\(downloadId), trackId=\(trackId)"
677
- )
713
+ NitroPlayerLogger.log("DownloadManagerCore", "🎯 Processing completion for downloadId=\(downloadId), trackId=\(trackId)")
678
714
 
679
715
  // IMPORTANT: Move file SYNCHRONOUSLY - the temp file is deleted after this method returns!
680
716
  // Get storage location and original URL from track metadata
@@ -698,7 +734,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
698
734
  // Now handle the rest asynchronously
699
735
  queue.async(flags: .barrier) {
700
736
  guard let destinationPath = destinationPath else {
701
- print("❌ DownloadManagerCore: Failed to save file for trackId=\(trackId)")
737
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save file for trackId=\(trackId)")
702
738
  self.taskMetadata[downloadId]?.state = .failed
703
739
  self.taskMetadata[downloadId]?.error = DownloadError(
704
740
  code: "FILE_MOVE_FAILED",
@@ -712,11 +748,11 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
712
748
  return
713
749
  }
714
750
 
715
- print("✅ DownloadManagerCore: File saved to \(destinationPath)")
751
+ NitroPlayerLogger.log("DownloadManagerCore", "✅ File saved to \(destinationPath)")
716
752
 
717
753
  guard let track = self.trackMetadata[trackId] else {
718
- print("❌ DownloadManagerCore: No track metadata for trackId=\(trackId)")
719
- print(" Available trackIds: \(Array(self.trackMetadata.keys))")
754
+ NitroPlayerLogger.log("DownloadManagerCore", "❌ No track metadata for trackId=\(trackId)")
755
+ NitroPlayerLogger.log("DownloadManagerCore", " Available trackIds: \(Array(self.trackMetadata.keys))")
720
756
 
721
757
  // Still mark as completed even if we don't have metadata
722
758
  self.taskMetadata[downloadId]?.state = .completed
@@ -747,7 +783,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
747
783
  // Save to database
748
784
  DownloadDatabase.shared.saveDownloadedTrack(downloadedTrack, playlistId: playlistId)
749
785
 
750
- print("✅ DownloadManagerCore: Track saved to database")
786
+ NitroPlayerLogger.log("DownloadManagerCore", "✅ Track saved to database")
751
787
 
752
788
  // Clean up persisted metadata (no longer needed after completion)
753
789
  self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
@@ -760,7 +796,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
760
796
  self.activeTasks.removeValue(forKey: downloadId)
761
797
 
762
798
  // Notify
763
- print("✅ DownloadManagerCore: Notifying completion for trackId=\(trackId)")
799
+ NitroPlayerLogger.log("DownloadManagerCore", "✅ Notifying completion for trackId=\(trackId)")
764
800
  self.notifyStateChange(
765
801
  downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
766
802
  self.notifyComplete(downloadedTrack)