react-native-nitro-player 0.5.3 → 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 (43) 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 +7 -6
  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 +16 -9
  14. package/ios/HybridAudioRoutePicker.swift +1 -1
  15. package/ios/HybridDownloadManager.swift +3 -3
  16. package/ios/HybridEqualizer.swift +3 -3
  17. package/ios/HybridTrackPlayer.swift +8 -4
  18. package/ios/core/NitroPlayerLogger.swift +22 -0
  19. package/ios/core/TrackPlayerCore.swift +195 -256
  20. package/ios/download/DownloadDatabase.swift +35 -39
  21. package/ios/download/DownloadFileManager.swift +17 -17
  22. package/ios/download/DownloadManagerCore.swift +29 -33
  23. package/ios/equalizer/EqualizerCore.swift +25 -20
  24. package/ios/playlist/PlaylistManager.swift +19 -9
  25. package/ios/queue/QueueManager.swift +1 -1
  26. package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
  27. package/lib/types/PlayerQueue.d.ts +1 -1
  28. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
  29. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  30. package/nitrogen/generated/android/c++/JReason.hpp +3 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +2 -1
  33. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +12 -0
  34. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
  35. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  36. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
  37. package/nitrogen/generated/ios/swift/Reason.swift +4 -0
  38. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  39. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
  40. package/nitrogen/generated/shared/c++/Reason.hpp +4 -0
  41. package/package.json +1 -1
  42. package/src/specs/TrackPlayer.nitro.ts +1 -0
  43. package/src/types/PlayerQueue.ts +1 -1
@@ -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
@@ -363,14 +360,14 @@ final class DownloadDatabase {
363
360
  let playlistData = try JSONEncoder().encode(playlistTracksDict)
364
361
  UserDefaults.standard.set(playlistData, forKey: Self.playlistTracksKey)
365
362
  } catch {
366
- print("[DownloadDatabase] Failed to save to disk: \(error)")
363
+ NitroPlayerLogger.log("DownloadDatabase", "Failed to save to disk: \(error)")
367
364
  }
368
365
  }
369
366
 
370
367
  private func loadFromDisk() {
371
- print("\n" + String(repeating: "📀", count: 40))
372
- print("📀 DownloadDatabase: LOADING FROM DISK")
373
- 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))
374
371
 
375
372
  // Load synchronously to ensure data is available immediately
376
373
  // Load downloaded tracks
@@ -378,7 +375,7 @@ final class DownloadDatabase {
378
375
  do {
379
376
  self.downloadedTracks = try JSONDecoder().decode(
380
377
  [String: DownloadedTrackRecord].self, from: tracksData)
381
- print("✅ DownloadDatabase: Loaded \(self.downloadedTracks.count) tracks from disk")
378
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.downloadedTracks.count) tracks from disk")
382
379
 
383
380
  // Migrate absolute paths → filenames (one-time, for existing installs)
384
381
  var needsMigration = false
@@ -401,15 +398,15 @@ final class DownloadDatabase {
401
398
 
402
399
  // Log each downloaded track
403
400
  for (trackId, record) in self.downloadedTracks {
404
- print(" 📥 \(trackId)")
405
- print(" Title: \(record.originalTrack.title)")
406
- print(" Path (filename): \(record.localPath)")
401
+ NitroPlayerLogger.log("DownloadDatabase", " 📥 \(trackId)")
402
+ NitroPlayerLogger.log("DownloadDatabase", " Title: \(record.originalTrack.title)")
403
+ NitroPlayerLogger.log("DownloadDatabase", " Path (filename): \(record.localPath)")
407
404
  }
408
405
  } catch {
409
- print("❌ DownloadDatabase: Failed to load tracks from disk: \(error)")
406
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load tracks from disk: \(error)")
410
407
  }
411
408
  } else {
412
- print("⚠️ DownloadDatabase: No saved tracks found in UserDefaults")
409
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No saved tracks found in UserDefaults")
413
410
  }
414
411
 
415
412
  // Load playlist associations
@@ -418,21 +415,20 @@ final class DownloadDatabase {
418
415
  let playlistTracksDict = try JSONDecoder().decode(
419
416
  [String: [String]].self, from: playlistData)
420
417
  self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
421
- print(
422
- "✅ DownloadDatabase: Loaded \(self.playlistTracks.count) playlist associations from disk")
418
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.playlistTracks.count) playlist associations from disk")
423
419
 
424
420
  // Log playlist associations
425
421
  for (playlistId, trackIds) in self.playlistTracks {
426
- print(" 📋 Playlist \(playlistId): \(trackIds.count) tracks")
422
+ NitroPlayerLogger.log("DownloadDatabase", " 📋 Playlist \(playlistId): \(trackIds.count) tracks")
427
423
  }
428
424
  } catch {
429
- print("❌ DownloadDatabase: Failed to load playlist tracks from disk: \(error)")
425
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load playlist tracks from disk: \(error)")
430
426
  }
431
427
  } else {
432
- print("⚠️ DownloadDatabase: No playlist associations found")
428
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No playlist associations found")
433
429
  }
434
430
 
435
- print(String(repeating: "📀", count: 40) + "\n")
431
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
436
432
  }
437
433
 
438
434
  // 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
 
@@ -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)
@@ -29,6 +29,9 @@ class EqualizerCore {
29
29
  // Current gains storage - internal so TapContext can access
30
30
  private(set) var currentGains: [Double] = [0, 0, 0, 0, 0]
31
31
 
32
+ // Dirty flag: set when gains change so TapContext only recalculates when needed
33
+ var gainsDirty: Bool = true
34
+
32
35
  // UserDefaults keys
33
36
  private let enabledKey = "eq_enabled"
34
37
  private let bandGainsKey = "eq_band_gains"
@@ -81,7 +84,7 @@ class EqualizerCore {
81
84
 
82
85
  private init() {
83
86
  restoreSettings()
84
- print("✅ EqualizerCore: Initialized with MTAudioProcessingTap support")
87
+ NitroPlayerLogger.log("EqualizerCore", "✅ Initialized with MTAudioProcessingTap support")
85
88
  }
86
89
 
87
90
  // MARK: - Audio Mix Creation for AVPlayerItem
@@ -98,19 +101,18 @@ class EqualizerCore {
98
101
  let status = asset.statusOfValue(forKey: "tracks", error: &error)
99
102
 
100
103
  if status == .failed {
101
- print(
102
- "⚠️ EqualizerCore: Failed to load tracks key: \(error?.localizedDescription ?? "unknown")")
104
+ NitroPlayerLogger.log("EqualizerCore", "⚠️ Failed to load tracks key: \(error?.localizedDescription ?? "unknown")")
103
105
  return
104
106
  }
105
107
 
106
108
  // Proceed only if loaded successfully
107
109
  guard status == .loaded else {
108
- print("⚠️ EqualizerCore: Tracks not loaded, status: \(status.rawValue)")
110
+ NitroPlayerLogger.log("EqualizerCore", "⚠️ Tracks not loaded, status: \(status.rawValue)")
109
111
  return
110
112
  }
111
113
 
112
114
  guard let audioTrack = asset.tracks(withMediaType: .audio).first else {
113
- print("⚠️ EqualizerCore: No audio track found in asset")
115
+ NitroPlayerLogger.log("EqualizerCore", "⚠️ No audio track found in asset")
114
116
  return
115
117
  }
116
118
 
@@ -137,7 +139,7 @@ class EqualizerCore {
137
139
  )
138
140
 
139
141
  guard createStatus == noErr, let audioTap = tap else {
140
- print("❌ EqualizerCore: Failed to create audio processing tap, status: \(createStatus)")
142
+ NitroPlayerLogger.log("EqualizerCore", "❌ Failed to create audio processing tap, status: \(createStatus)")
141
143
  return
142
144
  }
143
145
 
@@ -150,7 +152,7 @@ class EqualizerCore {
150
152
  // Apply to player item on main thread (AVPlayerItem properties should be accessed/modified on main thread or serial queue usually, but audioMix is thread safe - safely done on main to be sure)
151
153
  DispatchQueue.main.async {
152
154
  playerItem.audioMix = audioMix
153
- print("✅ EqualizerCore: Applied audio mix with EQ tap to player item (async)")
155
+ NitroPlayerLogger.log("EqualizerCore", "✅ Applied audio mix with EQ tap to player item (async)")
154
156
  }
155
157
  }
156
158
  }
@@ -163,7 +165,7 @@ class EqualizerCore {
163
165
  notifyEnabledChange(enabled)
164
166
  saveEnabled(enabled)
165
167
 
166
- print("🎚️ EqualizerCore: Equalizer \(enabled ? "enabled" : "disabled")")
168
+ NitroPlayerLogger.log("EqualizerCore", "🎚️ Equalizer \(enabled ? "enabled" : "disabled")")
167
169
  return true
168
170
  }
169
171
 
@@ -187,6 +189,7 @@ class EqualizerCore {
187
189
 
188
190
  let clampedGain = max(-12.0, min(12.0, gainDb))
189
191
  currentGains[bandIndex] = clampedGain
192
+ gainsDirty = true
190
193
 
191
194
  currentPresetName = nil
192
195
  notifyBandChange(getBands())
@@ -194,7 +197,7 @@ class EqualizerCore {
194
197
  saveBandGains(currentGains)
195
198
  saveCurrentPreset(nil)
196
199
 
197
- print("🎚️ EqualizerCore: Band \(bandIndex) gain set to \(clampedGain) dB")
200
+ NitroPlayerLogger.log("EqualizerCore", "🎚️ Band \(bandIndex) gain set to \(clampedGain) dB")
198
201
  return true
199
202
  }
200
203
 
@@ -204,11 +207,12 @@ class EqualizerCore {
204
207
  for i in 0..<5 {
205
208
  currentGains[i] = max(-12.0, min(12.0, gains[i]))
206
209
  }
210
+ gainsDirty = true
207
211
 
208
212
  notifyBandChange(getBands())
209
213
  saveBandGains(currentGains)
210
214
 
211
- print("🎚️ EqualizerCore: All band gains updated")
215
+ NitroPlayerLogger.log("EqualizerCore", "🎚️ All band gains updated")
212
216
  return true
213
217
  }
214
218
 
@@ -380,7 +384,7 @@ class EqualizerCore {
380
384
  currentPresetName = UserDefaults.standard.string(forKey: currentPresetKey)
381
385
  isEqualizerEnabled = enabled
382
386
 
383
- print("✅ EqualizerCore: Restored settings - enabled: \(enabled), gains: \(currentGains)")
387
+ NitroPlayerLogger.log("EqualizerCore", "✅ Restored settings - enabled: \(enabled), gains: \(currentGains)")
384
388
  }
385
389
 
386
390
  // MARK: - Callback Management
@@ -505,6 +509,7 @@ private class TapContext {
505
509
  sampleRate: Double(sampleRate)
506
510
  )
507
511
  }
512
+ eqCore.gainsDirty = false
508
513
  }
509
514
 
510
515
  /// Calculate biquad coefficients for a peaking EQ filter
@@ -556,13 +561,13 @@ private func tapInitCallback(
556
561
  let context = TapContext(eqCore: eqCore)
557
562
  tapStorageOut.pointee = Unmanaged.passRetained(context).toOpaque()
558
563
 
559
- print("🎛️ EqualizerCore: Tap initialized")
564
+ NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap initialized")
560
565
  }
561
566
 
562
567
  private func tapFinalizeCallback(tap: MTAudioProcessingTap) {
563
568
  let storage = MTAudioProcessingTapGetStorage(tap)
564
569
  Unmanaged<TapContext>.fromOpaque(storage).release()
565
- print("🎛️ EqualizerCore: Tap finalized")
570
+ NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap finalized")
566
571
  }
567
572
 
568
573
  private func tapPrepareCallback(
@@ -578,13 +583,11 @@ private func tapPrepareCallback(
578
583
  context.updateCoefficients()
579
584
  context.resetFilterStates()
580
585
 
581
- print(
582
- "🎛️ EqualizerCore: Tap prepared - sampleRate: \(context.sampleRate), channels: \(context.channelCount)"
583
- )
586
+ NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap prepared - sampleRate: \(context.sampleRate), channels: \(context.channelCount)")
584
587
  }
585
588
 
586
589
  private func tapUnprepareCallback(tap: MTAudioProcessingTap) {
587
- print("🎛️ EqualizerCore: Tap unprepared")
590
+ NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap unprepared")
588
591
  }
589
592
 
590
593
  private func tapProcessCallback(
@@ -610,7 +613,7 @@ private func tapProcessCallback(
610
613
  )
611
614
 
612
615
  guard status == noErr else {
613
- print("❌ EqualizerCore: Failed to get source audio: \(status)")
616
+ NitroPlayerLogger.log("EqualizerCore", "❌ Failed to get source audio: \(status)")
614
617
  return
615
618
  }
616
619
 
@@ -620,8 +623,10 @@ private func tapProcessCallback(
620
623
  return
621
624
  }
622
625
 
623
- // Update coefficients (in case gains changed)
624
- context.updateCoefficients()
626
+ // Update coefficients only when gains have changed
627
+ if context.eqCore?.gainsDirty == true {
628
+ context.updateCoefficients()
629
+ }
625
630
 
626
631
  // Process each buffer (channel)
627
632
  let bufferList = UnsafeMutableAudioBufferListPointer(bufferListInOut)