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
@@ -49,7 +49,7 @@ class TrackPlayerCore: NSObject {
49
49
  private var currentTrackIndex: Int = -1
50
50
  private var currentTracks: [TrackItem] = []
51
51
  private var isManuallySeeked = false
52
- private var repeatMode: RepeatMode = .off
52
+ private var currentRepeatMode: RepeatMode = .off
53
53
  private var boundaryTimeObserver: Any?
54
54
  private var currentItemObservers: [NSKeyValueObservation] = []
55
55
 
@@ -116,7 +116,7 @@ class TrackPlayerCore: NSObject {
116
116
  try audioSession.setCategory(.playback, mode: .default, options: [])
117
117
  try audioSession.setActive(true)
118
118
  } catch {
119
- print("❌ TrackPlayerCore: Failed to setup audio session - \(error)")
119
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Failed to setup audio session - \(error)")
120
120
  }
121
121
  }
122
122
 
@@ -138,8 +138,7 @@ class TrackPlayerCore: NSObject {
138
138
  player?.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
139
139
  }
140
140
 
141
- print(
142
- "🎵 TrackPlayerCore: Gapless playback configured - automaticallyWaitsToMinimizeStalling=true (flipped to false on first readyToPlay)")
141
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 Gapless playback configured - automaticallyWaitsToMinimizeStalling=true (flipped to false on first readyToPlay)")
143
142
 
144
143
  setupPlayerObservers()
145
144
  }
@@ -201,19 +200,19 @@ class TrackPlayerCore: NSObject {
201
200
  guard let player = player,
202
201
  let currentItem = player.currentItem
203
202
  else {
204
- print("⚠️ TrackPlayerCore: Cannot setup boundary observer - no player or item")
203
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Cannot setup boundary observer - no player or item")
205
204
  return
206
205
  }
207
206
 
208
207
  // Wait for duration to be available
209
208
  guard currentItem.status == .readyToPlay else {
210
- print("⚠️ TrackPlayerCore: Item not ready, will setup boundaries when ready")
209
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Item not ready, will setup boundaries when ready")
211
210
  return
212
211
  }
213
212
 
214
213
  let duration = currentItem.duration.seconds
215
214
  guard duration > 0 && !duration.isNaN && !duration.isInfinite else {
216
- print("⚠️ TrackPlayerCore: Invalid duration: \(duration), cannot setup boundaries")
215
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Invalid duration: \(duration), cannot setup boundaries")
217
216
  return
218
217
  }
219
218
 
@@ -236,9 +235,7 @@ class TrackPlayerCore: NSObject {
236
235
  time += interval
237
236
  }
238
237
 
239
- print(
240
- "⏱️ TrackPlayerCore: Setting up \(boundaryTimes.count) boundary observers (interval: \(interval)s, duration: \(Int(duration))s)"
241
- )
238
+ NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Setting up \(boundaryTimes.count) boundary observers (interval: \(interval)s, duration: \(Int(duration))s)")
242
239
 
243
240
  // Add boundary time observer
244
241
  boundaryTimeObserver = player.addBoundaryTimeObserver(forTimes: boundaryTimes, queue: .main) {
@@ -247,7 +244,7 @@ class TrackPlayerCore: NSObject {
247
244
  self.handleBoundaryTimeCrossed()
248
245
  }
249
246
 
250
- print("⏱️ TrackPlayerCore: Boundary time observer setup complete")
247
+ NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Boundary time observer setup complete")
251
248
  }
252
249
 
253
250
  private func handleBoundaryTimeCrossed() {
@@ -263,9 +260,7 @@ class TrackPlayerCore: NSObject {
263
260
 
264
261
  guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
265
262
 
266
- print(
267
- "⏱️ TrackPlayerCore: Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(!onPlaybackProgressChangeListeners.isEmpty)"
268
- )
263
+ NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(!onPlaybackProgressChangeListeners.isEmpty)")
269
264
 
270
265
  notifyPlaybackProgress(
271
266
  position,
@@ -278,100 +273,48 @@ class TrackPlayerCore: NSObject {
278
273
  // MARK: - Notification Handlers
279
274
 
280
275
  @objc private func playerItemDidPlayToEndTime(notification: Notification) {
281
- print("\n🏁 TrackPlayerCore: Track finished playing")
276
+ NitroPlayerLogger.log("TrackPlayerCore", "\n🏁 Track finished playing")
282
277
 
283
278
  guard let finishedItem = notification.object as? AVPlayerItem else {
284
- // Don't call skipToNext — AVQueuePlayer with actionAtItemEnd = .advance already auto-advances
285
279
  return
286
280
  }
287
281
 
288
- // Determine what type of track just finished and remove it from temporary lists
282
+ // 1. TRACK repeat handle FIRST, before any temp-track removal
283
+ if currentRepeatMode == .track {
284
+ NitroPlayerLogger.log("TrackPlayerCore", "🔁 TRACK repeat — seeking to zero and replaying")
285
+ player?.seek(to: .zero)
286
+ player?.play()
287
+ return // do not remove temp tracks, do not notify track change (same track looping)
288
+ }
289
+
290
+ // 2. Remove finished temp track from its list
289
291
  if let trackId = finishedItem.trackId {
290
292
  // Check if it was a playNext track
291
293
  if let index = playNextStack.firstIndex(where: { $0.id == trackId }) {
292
294
  let track = playNextStack.remove(at: index)
293
- print("🏁 Finished playNext track: \(track.title) - removed from stack")
295
+ NitroPlayerLogger.log("TrackPlayerCore", "🏁 Finished playNext track: \(track.title) - removed from stack")
294
296
  }
295
297
  // Check if it was an upNext track
296
298
  else if let index = upNextQueue.firstIndex(where: { $0.id == trackId }) {
297
299
  let track = upNextQueue.remove(at: index)
298
- print("🏁 Finished upNext track: \(track.title) - removed from queue")
300
+ NitroPlayerLogger.log("TrackPlayerCore", "🏁 Finished upNext track: \(track.title) - removed from queue")
299
301
  }
300
302
  // Otherwise it was from original playlist
301
303
  else if let track = currentTracks.first(where: { $0.id == trackId }) {
302
- print("🏁 Finished original track: \(track.title)")
304
+ NitroPlayerLogger.log("TrackPlayerCore", "🏁 Finished original track: \(track.title)")
303
305
  }
304
306
  }
305
307
 
306
- // Check remaining queue
308
+ // 3. Normal next-track advance happens via actionAtItemEnd = .advance
309
+ // The KVO observer (currentItemDidChange) will handle the track change notification
307
310
  if let player = player {
308
- print("📋 Remaining items in queue: \(player.items().count)")
311
+ NitroPlayerLogger.log("TrackPlayerCore", "📋 Remaining items in queue: \(player.items().count)")
309
312
  }
310
-
311
- // Handle repeat modes
312
- switch repeatMode {
313
- case .track:
314
- // Repeat current track - seek to beginning and play
315
- print("🔁 TrackPlayerCore: Repeat mode is TRACK - replaying current track")
316
- DispatchQueue.main.async { [weak self] in
317
- guard let self = self, let player = self.player else { return }
318
- // For temporary tracks, just seek to beginning
319
- if self.currentTemporaryType != .none {
320
- player.seek(to: .zero)
321
- player.play()
322
- } else {
323
- // For original tracks, recreate via playFromIndex
324
- self.playFromIndex(index: self.currentTrackIndex)
325
- }
326
- }
327
- return
328
-
329
- case .playlist:
330
- // Check if we're at the end of the ORIGINAL playlist (ignore temps)
331
- if currentTemporaryType == .none && currentTrackIndex >= currentTracks.count - 1 {
332
- // Check if there are still temporary tracks
333
- if !playNextStack.isEmpty || !upNextQueue.isEmpty {
334
- print("🔁 TrackPlayerCore: Temporary tracks remaining, continuing...")
335
- } else {
336
- print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - restarting from beginning")
337
- // Clear temps and restart
338
- playNextStack.removeAll()
339
- upNextQueue.removeAll()
340
- DispatchQueue.main.async { [weak self] in
341
- guard let self = self else { return }
342
- self.playFromIndex(index: 0)
343
- }
344
- return
345
- }
346
- } else {
347
- print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - continuing to next track")
348
- }
349
-
350
- case .off:
351
- // Default behavior - stop at end of playlist
352
- print("🔁 TrackPlayerCore: Repeat mode is OFF")
353
- }
354
-
355
- // Track ended naturally — notify with .end reason
356
- // AVQueuePlayer with actionAtItemEnd = .advance auto-advances to next item
357
- // The KVO observer (currentItemDidChange) will handle the track change notification
358
- notifyTrackChange(
359
- getCurrentTrack()
360
- ?? TrackItem(
361
- id: "",
362
- title: "",
363
- artist: "",
364
- album: "",
365
- duration: 0,
366
- url: "",
367
- artwork: nil,
368
- extraPayload: nil
369
- ), .end)
370
313
  }
371
314
 
372
315
  @objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
373
316
  if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
374
- print("❌ TrackPlayerCore: Playback failed - \(error)")
317
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Playback failed - \(error)")
375
318
  notifyPlaybackStateChange(.stopped, .error)
376
319
  }
377
320
  }
@@ -382,14 +325,12 @@ class TrackPlayerCore: NSObject {
382
325
  else { return }
383
326
 
384
327
  for event in errorLog.events ?? [] {
385
- print(
386
- "❌ TrackPlayerCore: Error log - \(event.errorComment ?? "Unknown error") - Code: \(event.errorStatusCode)"
387
- )
328
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Error log - \(event.errorComment ?? "Unknown error") - Code: \(event.errorStatusCode)")
388
329
  }
389
330
 
390
331
  // Also check item error
391
332
  if let error = item.error {
392
- print("❌ TrackPlayerCore: Item error - \(error.localizedDescription)")
333
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Item error - \(error.localizedDescription)")
393
334
  }
394
335
  }
395
336
 
@@ -401,7 +342,7 @@ class TrackPlayerCore: NSObject {
401
342
  let position = currentItem.currentTime().seconds
402
343
  let duration = currentItem.duration.seconds
403
344
 
404
- print("🎯 TrackPlayerCore: Time jumped (seek detected) - position: \(Int(position))s")
345
+ NitroPlayerLogger.log("TrackPlayerCore", "🎯 Time jumped (seek detected) - position: \(Int(position))s")
405
346
 
406
347
  // Call onSeek callback immediately
407
348
  notifySeek(position, duration)
@@ -421,24 +362,24 @@ class TrackPlayerCore: NSObject {
421
362
  ) {
422
363
  guard let player = player else { return }
423
364
 
424
- print("👀 TrackPlayerCore: KVO - keyPath: \(keyPath ?? "nil")")
365
+ NitroPlayerLogger.log("TrackPlayerCore", "👀 KVO - keyPath: \(keyPath ?? "nil")")
425
366
 
426
367
  if keyPath == "status" {
427
- print("👀 TrackPlayerCore: Player status changed to: \(player.status.rawValue)")
368
+ NitroPlayerLogger.log("TrackPlayerCore", "👀 Player status changed to: \(player.status.rawValue)")
428
369
  if player.status == .readyToPlay {
429
370
  emitStateChange()
430
371
  } else if player.status == .failed {
431
- print("❌ TrackPlayerCore: Player failed")
372
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Player failed")
432
373
  notifyPlaybackStateChange(.stopped, .error)
433
374
  }
434
375
  } else if keyPath == "rate" {
435
- print("👀 TrackPlayerCore: Rate changed to: \(player.rate)")
376
+ NitroPlayerLogger.log("TrackPlayerCore", "👀 Rate changed to: \(player.rate)")
436
377
  emitStateChange()
437
378
  } else if keyPath == "timeControlStatus" {
438
- print("👀 TrackPlayerCore: TimeControlStatus changed to: \(player.timeControlStatus.rawValue)")
379
+ NitroPlayerLogger.log("TrackPlayerCore", "👀 TimeControlStatus changed to: \(player.timeControlStatus.rawValue)")
439
380
  emitStateChange()
440
381
  } else if keyPath == "currentItem" {
441
- print("👀 TrackPlayerCore: Current item changed")
382
+ NitroPlayerLogger.log("TrackPlayerCore", "👀 Current item changed")
442
383
  currentItemDidChange()
443
384
  }
444
385
  }
@@ -453,43 +394,64 @@ class TrackPlayerCore: NSObject {
453
394
  guard let player = player,
454
395
  let currentItem = player.currentItem
455
396
  else {
456
- print("⚠️ TrackPlayerCore: Current item changed to nil")
397
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Current item changed to nil")
398
+ // Queue exhausted — handle PLAYLIST repeat
399
+ if currentRepeatMode == .playlist && !currentTracks.isEmpty, let player = player {
400
+ NitroPlayerLogger.log("TrackPlayerCore", "🔁 PLAYLIST repeat — rebuilding original queue and restarting")
401
+ playNextStack.removeAll()
402
+ upNextQueue.removeAll()
403
+ currentTemporaryType = .none
404
+
405
+ let allItems = currentTracks.compactMap { createGaplessPlayerItem(for: $0, isPreload: false) }
406
+ var lastItem: AVPlayerItem? = nil
407
+ for item in allItems {
408
+ player.insert(item, after: lastItem)
409
+ lastItem = item
410
+ }
411
+ currentTrackIndex = 0
412
+ player.play()
413
+
414
+ if let firstTrack = currentTracks.first {
415
+ notifyTrackChange(firstTrack, .repeat)
416
+ mediaSessionManager?.onTrackChanged()
417
+ }
418
+ }
457
419
  return
458
420
  }
459
421
 
460
- print("\n" + String(repeating: "▶", count: Constants.separatorLineLength))
461
- print("🔄 TrackPlayerCore: CURRENT ITEM CHANGED")
462
- print(String(repeating: "▶", count: Constants.separatorLineLength))
422
+ NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "▶", count: Constants.separatorLineLength))
423
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 CURRENT ITEM CHANGED")
424
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "▶", count: Constants.separatorLineLength))
463
425
 
464
426
  // Log current item details
465
427
  if let trackId = currentItem.trackId,
466
428
  let track = currentTracks.first(where: { $0.id == trackId })
467
429
  {
468
- print("▶️ NOW PLAYING: \(track.title) - \(track.artist) (ID: \(track.id))")
430
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ NOW PLAYING: \(track.title) - \(track.artist) (ID: \(track.id))")
469
431
  } else {
470
- print("⚠️ NOW PLAYING: Unknown track (trackId: \(currentItem.trackId ?? "nil"))")
432
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ NOW PLAYING: Unknown track (trackId: \(currentItem.trackId ?? "nil"))")
471
433
  }
472
434
 
473
435
  // Show remaining items in queue
474
436
  let remainingItems = player.items()
475
- print("\n📋 REMAINING ITEMS IN QUEUE: \(remainingItems.count)")
437
+ NitroPlayerLogger.log("TrackPlayerCore", "\n📋 REMAINING ITEMS IN QUEUE: \(remainingItems.count)")
476
438
  for (index, item) in remainingItems.enumerated() {
477
439
  if let trackId = item.trackId, let track = currentTracks.first(where: { $0.id == trackId }) {
478
440
  let marker = item == currentItem ? "▶️" : " "
479
- print("\(marker) [\(index + 1)] \(track.title) - \(track.artist)")
441
+ NitroPlayerLogger.log("TrackPlayerCore", "\(marker) [\(index + 1)] \(track.title) - \(track.artist)")
480
442
  } else {
481
- print(" [\(index + 1)] ⚠️ Unknown track")
443
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] ⚠️ Unknown track")
482
444
  }
483
445
  }
484
446
 
485
- print(String(repeating: "▶", count: Constants.separatorLineLength) + "\n")
447
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "▶", count: Constants.separatorLineLength) + "\n")
486
448
 
487
449
  // Log item status
488
- print("📱 TrackPlayerCore: Item status: \(currentItem.status.rawValue)")
450
+ NitroPlayerLogger.log("TrackPlayerCore", "📱 Item status: \(currentItem.status.rawValue)")
489
451
 
490
452
  // Check for errors
491
453
  if let error = currentItem.error {
492
- print("❌ TrackPlayerCore: Current item has error - \(error.localizedDescription)")
454
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Current item has error - \(error.localizedDescription)")
493
455
  }
494
456
 
495
457
  // Setup KVO observers for current item
@@ -497,12 +459,12 @@ class TrackPlayerCore: NSObject {
497
459
 
498
460
  // Update track index and determine temporary type
499
461
  if let trackId = currentItem.trackId {
500
- print("🔍 TrackPlayerCore: Looking up trackId '\(trackId)' in currentTracks...")
501
- print(" Current index BEFORE lookup: \(currentTrackIndex)")
462
+ NitroPlayerLogger.log("TrackPlayerCore", "🔍 Looking up trackId '\(trackId)' in currentTracks...")
463
+ NitroPlayerLogger.log("TrackPlayerCore", " Current index BEFORE lookup: \(currentTrackIndex)")
502
464
 
503
465
  // Update temporary type
504
466
  currentTemporaryType = determineCurrentTemporaryType()
505
- print(" 🎯 Track type: \(currentTemporaryType)")
467
+ NitroPlayerLogger.log("TrackPlayerCore", " 🎯 Track type: \(currentTemporaryType)")
506
468
 
507
469
  // If it's a temporary track, don't update currentTrackIndex
508
470
  if currentTemporaryType != .none {
@@ -515,38 +477,38 @@ class TrackPlayerCore: NSObject {
515
477
  }
516
478
 
517
479
  if let track = tempTrack {
518
- print(" 🎵 Temporary track: \(track.title) - \(track.artist)")
519
- print(" 📢 Emitting onChangeTrack for temporary track")
480
+ NitroPlayerLogger.log("TrackPlayerCore", " 🎵 Temporary track: \(track.title) - \(track.artist)")
481
+ NitroPlayerLogger.log("TrackPlayerCore", " 📢 Emitting onChangeTrack for temporary track")
520
482
  notifyTrackChange(track, .skip)
521
483
  mediaSessionManager?.onTrackChanged()
522
484
  }
523
485
  }
524
486
  // It's an original playlist track
525
487
  else if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
526
- print(" ✅ Found track at index: \(index)")
527
- print(" Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
488
+ NitroPlayerLogger.log("TrackPlayerCore", " ✅ Found track at index: \(index)")
489
+ NitroPlayerLogger.log("TrackPlayerCore", " Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
528
490
 
529
491
  let oldIndex = currentTrackIndex
530
492
  currentTrackIndex = index
531
493
 
532
494
  if let track = currentTracks[safe: index] {
533
- print(" 🎵 Track: \(track.title) - \(track.artist)")
495
+ NitroPlayerLogger.log("TrackPlayerCore", " 🎵 Track: \(track.title) - \(track.artist)")
534
496
 
535
497
  // Only emit onChangeTrack if index actually changed
536
498
  // This prevents duplicate emissions
537
499
  if oldIndex != index {
538
- print(" 📢 Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
500
+ NitroPlayerLogger.log("TrackPlayerCore", " 📢 Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
539
501
  notifyTrackChange(track, .skip)
540
502
  mediaSessionManager?.onTrackChanged()
541
503
  } else {
542
- print(" ⏭️ Skipping onChangeTrack emission (index unchanged)")
504
+ NitroPlayerLogger.log("TrackPlayerCore", " ⏭️ Skipping onChangeTrack emission (index unchanged)")
543
505
  }
544
506
  }
545
507
  } else {
546
- print(" ⚠️ Track ID '\(trackId)' NOT FOUND in currentTracks!")
547
- print(" Current tracks:")
508
+ NitroPlayerLogger.log("TrackPlayerCore", " ⚠️ Track ID '\(trackId)' NOT FOUND in currentTracks!")
509
+ NitroPlayerLogger.log("TrackPlayerCore", " Current tracks:")
548
510
  for (idx, track) in currentTracks.enumerated() {
549
- print(" [\(idx)] \(track.id) - \(track.title)")
511
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(idx)] \(track.id) - \(track.title)")
550
512
  }
551
513
  }
552
514
  }
@@ -563,19 +525,19 @@ class TrackPlayerCore: NSObject {
563
525
  }
564
526
 
565
527
  private func setupCurrentItemObservers(item: AVPlayerItem) {
566
- print("📱 TrackPlayerCore: Setting up item observers")
528
+ NitroPlayerLogger.log("TrackPlayerCore", "📱 Setting up item observers")
567
529
 
568
530
  // Observe status - recreate boundaries when ready and update now playing info
569
531
  let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
570
532
  if item.status == .readyToPlay {
571
- print("✅ TrackPlayerCore: Item ready, setting up boundaries")
533
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Item ready, setting up boundaries")
572
534
  self?.setupBoundaryTimeObserver()
573
535
  // First item is buffered and ready — disable stall waiting for gapless inter-track transitions
574
536
  self?.player?.automaticallyWaitsToMinimizeStalling = false
575
537
  // Update now playing info now that duration is available
576
538
  self?.mediaSessionManager?.updateNowPlayingInfo()
577
539
  } else if item.status == .failed {
578
- print("❌ TrackPlayerCore: Item failed")
540
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Item failed")
579
541
  self?.notifyPlaybackStateChange(.stopped, .error)
580
542
  }
581
543
  }
@@ -584,7 +546,7 @@ class TrackPlayerCore: NSObject {
584
546
  // Observe playback buffer
585
547
  let bufferEmptyObserver = item.observe(\.isPlaybackBufferEmpty, options: [.new]) { item, _ in
586
548
  if item.isPlaybackBufferEmpty {
587
- print("⏸️ TrackPlayerCore: Buffer empty (buffering)")
549
+ NitroPlayerLogger.log("TrackPlayerCore", "⏸️ Buffer empty (buffering)")
588
550
  }
589
551
  }
590
552
  currentItemObservers.append(bufferEmptyObserver)
@@ -592,7 +554,7 @@ class TrackPlayerCore: NSObject {
592
554
  let bufferKeepUpObserver = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) {
593
555
  item, _ in
594
556
  if item.isPlaybackLikelyToKeepUp {
595
- print("▶️ TrackPlayerCore: Buffer likely to keep up")
557
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Buffer likely to keep up")
596
558
  }
597
559
  }
598
560
  currentItemObservers.append(bufferKeepUpObserver)
@@ -611,32 +573,32 @@ class TrackPlayerCore: NSObject {
611
573
  }
612
574
 
613
575
  private func loadPlaylistInternal(playlistId: String) {
614
- print("\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
615
- print("📂 TrackPlayerCore: LOAD PLAYLIST REQUEST")
616
- print(" Playlist ID: \(playlistId)")
576
+ NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
577
+ NitroPlayerLogger.log("TrackPlayerCore", "📂 LOAD PLAYLIST REQUEST")
578
+ NitroPlayerLogger.log("TrackPlayerCore", " Playlist ID: \(playlistId)")
617
579
 
618
580
  // Clear temporary tracks when loading new playlist
619
581
  self.playNextStack.removeAll()
620
582
  self.upNextQueue.removeAll()
621
583
  self.currentTemporaryType = .none
622
- print(" 🧹 Cleared temporary tracks")
584
+ NitroPlayerLogger.log("TrackPlayerCore", " 🧹 Cleared temporary tracks")
623
585
 
624
586
  let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
625
587
  if let playlist = playlist {
626
- print(" ✅ Found playlist: \(playlist.name)")
627
- print(" 📋 Contains \(playlist.tracks.count) tracks:")
588
+ NitroPlayerLogger.log("TrackPlayerCore", " ✅ Found playlist: \(playlist.name)")
589
+ NitroPlayerLogger.log("TrackPlayerCore", " 📋 Contains \(playlist.tracks.count) tracks:")
628
590
  for (index, track) in playlist.tracks.enumerated() {
629
- print(" [\(index + 1)] \(track.title) - \(track.artist)")
591
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] \(track.title) - \(track.artist)")
630
592
  }
631
- print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
593
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
632
594
 
633
595
  self.currentPlaylistId = playlistId
634
596
  self.updatePlayerQueue(tracks: playlist.tracks)
635
597
  // Emit initial state (paused/stopped before play)
636
598
  self.emitStateChange()
637
599
  } else {
638
- print(" ❌ Playlist NOT FOUND")
639
- print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
600
+ NitroPlayerLogger.log("TrackPlayerCore", " ❌ Playlist NOT FOUND")
601
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
640
602
  }
641
603
  }
642
604
 
@@ -685,8 +647,8 @@ class TrackPlayerCore: NSObject {
685
647
  state = .stopped
686
648
  }
687
649
 
688
- print("🔔 TrackPlayerCore: Emitting state change: \(state)")
689
- print("🔔 TrackPlayerCore: Callback exists: \(!onPlaybackStateChangeListeners.isEmpty)")
650
+ NitroPlayerLogger.log("TrackPlayerCore", "🔔 Emitting state change: \(state)")
651
+ NitroPlayerLogger.log("TrackPlayerCore", "🔔 Callback exists: \(!onPlaybackStateChangeListeners.isEmpty)")
690
652
  notifyPlaybackStateChange(state, reason)
691
653
  mediaSessionManager?.onPlaybackStateChanged()
692
654
  }
@@ -706,19 +668,19 @@ class TrackPlayerCore: NSObject {
706
668
 
707
669
  if isLocal {
708
670
  // Local file - use fileURLWithPath
709
- print("📥 TrackPlayerCore: Using DOWNLOADED version for \(track.title)")
710
- print(" Local path: \(effectiveUrlString)")
671
+ NitroPlayerLogger.log("TrackPlayerCore", "📥 Using DOWNLOADED version for \(track.title)")
672
+ NitroPlayerLogger.log("TrackPlayerCore", " Local path: \(effectiveUrlString)")
711
673
 
712
674
  // Verify file exists
713
675
  if FileManager.default.fileExists(atPath: effectiveUrlString) {
714
676
  url = URL(fileURLWithPath: effectiveUrlString)
715
- print(" File URL: \(url.absoluteString)")
716
- print(" ✅ File verified to exist")
677
+ NitroPlayerLogger.log("TrackPlayerCore", " File URL: \(url.absoluteString)")
678
+ NitroPlayerLogger.log("TrackPlayerCore", " ✅ File verified to exist")
717
679
  } else {
718
- print(" ❌ Downloaded file does NOT exist at path!")
719
- print(" Falling back to remote URL: \(track.url)")
680
+ NitroPlayerLogger.log("TrackPlayerCore", " ❌ Downloaded file does NOT exist at path!")
681
+ NitroPlayerLogger.log("TrackPlayerCore", " Falling back to remote URL: \(track.url)")
720
682
  guard let remoteUrl = URL(string: track.url) else {
721
- print("❌ TrackPlayerCore: Invalid remote URL: \(track.url)")
683
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Invalid remote URL: \(track.url)")
722
684
  return nil
723
685
  }
724
686
  url = remoteUrl
@@ -726,18 +688,18 @@ class TrackPlayerCore: NSObject {
726
688
  } else {
727
689
  // Remote URL
728
690
  guard let remoteUrl = URL(string: effectiveUrlString) else {
729
- print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(effectiveUrlString)")
691
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Invalid URL for track: \(track.title) - \(effectiveUrlString)")
730
692
  return nil
731
693
  }
732
694
  url = remoteUrl
733
- print("🌐 TrackPlayerCore: Using REMOTE version for \(track.title)")
695
+ NitroPlayerLogger.log("TrackPlayerCore", "🌐 Using REMOTE version for \(track.title)")
734
696
  }
735
697
 
736
698
  // Check if we have a preloaded asset for this track
737
699
  let asset: AVURLAsset
738
700
  if let preloadedAsset = preloadedAssets[track.id] {
739
701
  asset = preloadedAsset
740
- print("🚀 TrackPlayerCore: Using preloaded asset for \(track.title)")
702
+ NitroPlayerLogger.log("TrackPlayerCore", "🚀 Using preloaded asset for \(track.title)")
741
703
  } else {
742
704
  // No AVURLAssetPreferPreciseDurationAndTimingKey — gapless playback is achieved via
743
705
  // AVQueuePlayer's internal audio buffer pre-roll, not timing metadata.
@@ -763,7 +725,7 @@ class TrackPlayerCore: NSObject {
763
725
  // Apply equalizer audio mix to the player item
764
726
  // This enables real-time EQ processing via MTAudioProcessingTap
765
727
  EqualizerCore.shared.applyAudioMix(to: item)
766
- print("🎛️ TrackPlayerCore: Requesting EQ audio mix application for \(track.title)")
728
+ NitroPlayerLogger.log("TrackPlayerCore", "🎛️ Requesting EQ audio mix application for \(track.title)")
767
729
 
768
730
  // If this is a preload request, start loading asset keys asynchronously
769
731
  if isPreload {
@@ -774,14 +736,12 @@ class TrackPlayerCore: NSObject {
774
736
  var error: NSError?
775
737
  let status = asset.statusOfValue(forKey: key, error: &error)
776
738
  if status == .failed {
777
- print(
778
- "⚠️ TrackPlayerCore: Failed to load key '\(key)' for \(track.title): \(error?.localizedDescription ?? "unknown")"
779
- )
739
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Failed to load key '\(key)' for \(track.title): \(error?.localizedDescription ?? "unknown")")
780
740
  allKeysLoaded = false
781
741
  }
782
742
  }
783
743
  if allKeysLoaded {
784
- print("✅ TrackPlayerCore: All asset keys preloaded for \(track.title)")
744
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ All asset keys preloaded for \(track.title)")
785
745
  }
786
746
  }
787
747
  }
@@ -826,7 +786,7 @@ class TrackPlayerCore: NSObject {
826
786
  if allKeysLoaded {
827
787
  DispatchQueue.main.async {
828
788
  self?.preloadedAssets[track.id] = asset
829
- print("🎯 TrackPlayerCore: Preloaded asset for upcoming track: \(track.title)")
789
+ NitroPlayerLogger.log("TrackPlayerCore", "🎯 Preloaded asset for upcoming track: \(track.title)")
830
790
  }
831
791
  }
832
792
  }
@@ -851,7 +811,7 @@ class TrackPlayerCore: NSObject {
851
811
  }
852
812
 
853
813
  if !assetsToRemove.isEmpty {
854
- print("🧹 TrackPlayerCore: Cleaned up \(assetsToRemove.count) preloaded assets")
814
+ NitroPlayerLogger.log("TrackPlayerCore", "🧹 Cleaned up \(assetsToRemove.count) preloaded assets")
855
815
  }
856
816
  }
857
817
  }
@@ -864,9 +824,7 @@ class TrackPlayerCore: NSObject {
864
824
  let box = WeakCallbackBox(owner: owner, callback: listener)
865
825
  listenersQueue.async(flags: .barrier) { [weak self] in
866
826
  self?.onChangeTrackListeners.append(box)
867
- print(
868
- "🎯 TrackPlayerCore: Added onChangeTrack listener (total: \(self?.onChangeTrackListeners.count ?? 0))"
869
- )
827
+ NitroPlayerLogger.log("TrackPlayerCore", "🎯 Added onChangeTrack listener (total: \(self?.onChangeTrackListeners.count ?? 0))")
870
828
  }
871
829
  }
872
830
 
@@ -877,9 +835,7 @@ class TrackPlayerCore: NSObject {
877
835
  let box = WeakCallbackBox(owner: owner, callback: listener)
878
836
  listenersQueue.async(flags: .barrier) { [weak self] in
879
837
  self?.onPlaybackStateChangeListeners.append(box)
880
- print(
881
- "🎯 TrackPlayerCore: Added onPlaybackStateChange listener (total: \(self?.onPlaybackStateChangeListeners.count ?? 0))"
882
- )
838
+ NitroPlayerLogger.log("TrackPlayerCore", "🎯 Added onPlaybackStateChange listener (total: \(self?.onPlaybackStateChangeListeners.count ?? 0))")
883
839
  }
884
840
  }
885
841
 
@@ -887,7 +843,7 @@ class TrackPlayerCore: NSObject {
887
843
  let box = WeakCallbackBox(owner: owner, callback: listener)
888
844
  listenersQueue.async(flags: .barrier) { [weak self] in
889
845
  self?.onSeekListeners.append(box)
890
- print("🎯 TrackPlayerCore: Added onSeek listener (total: \(self?.onSeekListeners.count ?? 0))")
846
+ NitroPlayerLogger.log("TrackPlayerCore", "🎯 Added onSeek listener (total: \(self?.onSeekListeners.count ?? 0))")
891
847
  }
892
848
  }
893
849
 
@@ -898,9 +854,7 @@ class TrackPlayerCore: NSObject {
898
854
  let box = WeakCallbackBox(owner: owner, callback: listener)
899
855
  listenersQueue.async(flags: .barrier) { [weak self] in
900
856
  self?.onPlaybackProgressChangeListeners.append(box)
901
- print(
902
- "🎯 TrackPlayerCore: Added onPlaybackProgressChange listener (total: \(self?.onPlaybackProgressChangeListeners.count ?? 0))"
903
- )
857
+ NitroPlayerLogger.log("TrackPlayerCore", "🎯 Added onPlaybackProgressChange listener (total: \(self?.onPlaybackProgressChangeListeners.count ?? 0))")
904
858
  }
905
859
  }
906
860
 
@@ -913,10 +867,8 @@ class TrackPlayerCore: NSObject {
913
867
  // Remove dead listeners
914
868
  self.onChangeTrackListeners.removeAll { !$0.isAlive }
915
869
 
916
- // Get live callbacks
917
- let liveCallbacks = self.onChangeTrackListeners.compactMap {
918
- $0.isAlive ? $0.callback : nil
919
- }
870
+ // Get live callbacks (all remaining are alive after removeAll)
871
+ let liveCallbacks = self.onChangeTrackListeners.map { $0.callback }
920
872
 
921
873
  // Call on main thread
922
874
  if !liveCallbacks.isEmpty {
@@ -935,9 +887,7 @@ class TrackPlayerCore: NSObject {
935
887
 
936
888
  self.onPlaybackStateChangeListeners.removeAll { !$0.isAlive }
937
889
 
938
- let liveCallbacks = self.onPlaybackStateChangeListeners.compactMap {
939
- $0.isAlive ? $0.callback : nil
940
- }
890
+ let liveCallbacks = self.onPlaybackStateChangeListeners.map { $0.callback }
941
891
 
942
892
  if !liveCallbacks.isEmpty {
943
893
  DispatchQueue.main.async {
@@ -955,9 +905,7 @@ class TrackPlayerCore: NSObject {
955
905
 
956
906
  self.onSeekListeners.removeAll { !$0.isAlive }
957
907
 
958
- let liveCallbacks = self.onSeekListeners.compactMap {
959
- $0.isAlive ? $0.callback : nil
960
- }
908
+ let liveCallbacks = self.onSeekListeners.map { $0.callback }
961
909
 
962
910
  if !liveCallbacks.isEmpty {
963
911
  DispatchQueue.main.async {
@@ -975,9 +923,7 @@ class TrackPlayerCore: NSObject {
975
923
 
976
924
  self.onPlaybackProgressChangeListeners.removeAll { !$0.isAlive }
977
925
 
978
- let liveCallbacks = self.onPlaybackProgressChangeListeners.compactMap {
979
- $0.isAlive ? $0.callback : nil
980
- }
926
+ let liveCallbacks = self.onPlaybackProgressChangeListeners.map { $0.callback }
981
927
 
982
928
  if !liveCallbacks.isEmpty {
983
929
  DispatchQueue.main.async {
@@ -994,29 +940,28 @@ class TrackPlayerCore: NSObject {
994
940
  // MARK: - Queue Management
995
941
 
996
942
  private func updatePlayerQueue(tracks: [TrackItem]) {
997
- print("\n" + String(repeating: "=", count: Constants.separatorLineLength))
998
- print("📋 TrackPlayerCore: UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
999
- print(String(repeating: "=", count: Constants.separatorLineLength))
943
+ NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "=", count: Constants.separatorLineLength))
944
+ NitroPlayerLogger.log("TrackPlayerCore", "📋 UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
945
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength))
1000
946
 
1001
947
  #if DEBUG
1002
948
  for (index, track) in tracks.enumerated() {
1003
949
  let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
1004
950
  let downloadStatus = isDownloaded ? "📥 DOWNLOADED" : "🌐 REMOTE"
1005
- print(
1006
- " [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
951
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
1007
952
  if isDownloaded {
1008
953
  if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
1009
- print(" Local path: \(localPath)")
954
+ NitroPlayerLogger.log("TrackPlayerCore", " Local path: \(localPath)")
1010
955
  }
1011
956
  }
1012
957
  }
1013
- print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
958
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength) + "\n")
1014
959
  #endif
1015
960
 
1016
961
  // Store tracks for index tracking
1017
962
  currentTracks = tracks
1018
963
  currentTrackIndex = 0
1019
- print("🔢 TrackPlayerCore: Reset currentTrackIndex to 0 (will be updated by KVO observer)")
964
+ NitroPlayerLogger.log("TrackPlayerCore", "🔢 Reset currentTrackIndex to 0 (will be updated by KVO observer)")
1020
965
 
1021
966
  // Remove old boundary observer if exists (this is safe)
1022
967
  if let boundaryObserver = boundaryTimeObserver, let currentPlayer = player {
@@ -1037,22 +982,20 @@ class TrackPlayerCore: NSObject {
1037
982
  return createGaplessPlayerItem(for: track, isPreload: isPreload)
1038
983
  }
1039
984
 
1040
- print("🎵 TrackPlayerCore: Created \(items.count) gapless-optimized player items")
985
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 Created \(items.count) gapless-optimized player items")
1041
986
 
1042
987
  guard !items.isEmpty else {
1043
- print("❌ TrackPlayerCore: No valid items to play")
988
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ No valid items to play")
1044
989
  return
1045
990
  }
1046
991
 
1047
992
  // Replace current queue (player should always exist after setupPlayer)
1048
993
  guard let existingPlayer = self.player else {
1049
- print("❌ TrackPlayerCore: No player available - this should never happen!")
994
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ No player available - this should never happen!")
1050
995
  return
1051
996
  }
1052
997
 
1053
- print(
1054
- "🔄 TrackPlayerCore: Updating queue - removing \(existingPlayer.items().count) items, adding \(items.count) new items"
1055
- )
998
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 Updating queue - removing \(existingPlayer.items().count) items, adding \(items.count) new items")
1056
999
 
1057
1000
  // Remove all existing items
1058
1001
  existingPlayer.removeAllItems()
@@ -1066,28 +1009,27 @@ class TrackPlayerCore: NSObject {
1066
1009
  lastItem = item
1067
1010
 
1068
1011
  if let trackId = item.trackId, let track = tracks.first(where: { $0.id == trackId }) {
1069
- print(" ➕ Added to player queue [\(index + 1)]: \(track.title)")
1012
+ NitroPlayerLogger.log("TrackPlayerCore", " ➕ Added to player queue [\(index + 1)]: \(track.title)")
1070
1013
  }
1071
1014
  }
1072
1015
 
1073
1016
  #if DEBUG
1074
1017
  let trackById = Dictionary(uniqueKeysWithValues: tracks.map { ($0.id, $0) })
1075
- print(
1076
- "\n🔍 TrackPlayerCore: VERIFICATION - Player now has \(existingPlayer.items().count) items:")
1018
+ NitroPlayerLogger.log("TrackPlayerCore", "\n🔍 VERIFICATION - Player now has \(existingPlayer.items().count) items:")
1077
1019
  for (index, item) in existingPlayer.items().enumerated() {
1078
1020
  if let trackId = item.trackId, let track = trackById[trackId] {
1079
- print(" [\(index + 1)] ✓ \(track.title) - \(track.artist) (ID: \(track.id))")
1021
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] ✓ \(track.title) - \(track.artist) (ID: \(track.id))")
1080
1022
  } else {
1081
- print(" [\(index + 1)] ⚠️ Unknown item (no trackId)")
1023
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] ⚠️ Unknown item (no trackId)")
1082
1024
  }
1083
1025
  }
1084
1026
  if let currentItem = existingPlayer.currentItem,
1085
1027
  let trackId = currentItem.trackId,
1086
1028
  let track = trackById[trackId]
1087
1029
  {
1088
- print("▶️ Current item: \(track.title)")
1030
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Current item: \(track.title)")
1089
1031
  }
1090
- print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
1032
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength) + "\n")
1091
1033
  #endif
1092
1034
 
1093
1035
  // Note: Boundary time observers will be set up automatically when item becomes ready
@@ -1095,8 +1037,8 @@ class TrackPlayerCore: NSObject {
1095
1037
 
1096
1038
  // Notify track change
1097
1039
  if let firstTrack = tracks.first {
1098
- print("🎵 TrackPlayerCore: Emitting track change: \(firstTrack.title)")
1099
- print("🎵 TrackPlayerCore: onChangeTrack callbacks count: \(onChangeTrackListeners.count)")
1040
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 Emitting track change: \(firstTrack.title)")
1041
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 onChangeTrack callbacks count: \(onChangeTrackListeners.count)")
1100
1042
  notifyTrackChange(firstTrack, nil)
1101
1043
  mediaSessionManager?.onTrackChanged()
1102
1044
  }
@@ -1104,7 +1046,7 @@ class TrackPlayerCore: NSObject {
1104
1046
  // Start preloading upcoming tracks for gapless playback
1105
1047
  preloadUpcomingTracks(from: 1)
1106
1048
 
1107
- print("✅ TrackPlayerCore: Queue updated with \(items.count) gapless-optimized tracks")
1049
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue updated with \(items.count) gapless-optimized tracks")
1108
1050
  }
1109
1051
 
1110
1052
  func getCurrentTrack() -> TrackItem? {
@@ -1143,6 +1085,7 @@ class TrackPlayerCore: NSObject {
1143
1085
 
1144
1086
  private func getActualQueueInternal() -> [TrackItem] {
1145
1087
  var queue: [TrackItem] = []
1088
+ queue.reserveCapacity(currentTracks.count + playNextStack.count + upNextQueue.count)
1146
1089
 
1147
1090
  // Add tracks before current (original playlist)
1148
1091
  // When a temp track is playing, include the original track at currentTrackIndex
@@ -1150,7 +1093,7 @@ class TrackPlayerCore: NSObject {
1150
1093
  let beforeEnd = currentTemporaryType != .none
1151
1094
  ? min(currentTrackIndex + 1, currentTracks.count) : currentTrackIndex
1152
1095
  if beforeEnd > 0 {
1153
- queue.append(contentsOf: Array(currentTracks[0..<beforeEnd]))
1096
+ queue.append(contentsOf: currentTracks[0..<beforeEnd])
1154
1097
  }
1155
1098
 
1156
1099
  // Add current track (temp or original)
@@ -1161,7 +1104,7 @@ class TrackPlayerCore: NSObject {
1161
1104
  // Add playNext stack (LIFO - most recently added plays first)
1162
1105
  // Skip index 0 if current track is from playNext (it's already added as current)
1163
1106
  if currentTemporaryType == .playNext && playNextStack.count > 1 {
1164
- queue.append(contentsOf: Array(playNextStack.dropFirst()))
1107
+ queue.append(contentsOf: playNextStack.dropFirst())
1165
1108
  } else if currentTemporaryType != .playNext {
1166
1109
  queue.append(contentsOf: playNextStack)
1167
1110
  }
@@ -1169,21 +1112,21 @@ class TrackPlayerCore: NSObject {
1169
1112
  // Add upNext queue (in order, FIFO)
1170
1113
  // Skip index 0 if current track is from upNext (it's already added as current)
1171
1114
  if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1172
- queue.append(contentsOf: Array(upNextQueue.dropFirst()))
1115
+ queue.append(contentsOf: upNextQueue.dropFirst())
1173
1116
  } else if currentTemporaryType != .upNext {
1174
1117
  queue.append(contentsOf: upNextQueue)
1175
1118
  }
1176
1119
 
1177
1120
  // Add remaining original tracks
1178
1121
  if currentTrackIndex + 1 < currentTracks.count {
1179
- queue.append(contentsOf: Array(currentTracks[(currentTrackIndex + 1)...]))
1122
+ queue.append(contentsOf: currentTracks[(currentTrackIndex + 1)...])
1180
1123
  }
1181
1124
 
1182
1125
  return queue
1183
1126
  }
1184
1127
 
1185
1128
  func play() {
1186
- print("▶️ TrackPlayerCore: play() called")
1129
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ play() called")
1187
1130
  if Thread.isMainThread {
1188
1131
  playInternal()
1189
1132
  } else {
@@ -1194,13 +1137,13 @@ class TrackPlayerCore: NSObject {
1194
1137
  }
1195
1138
 
1196
1139
  private func playInternal() {
1197
- print("▶️ TrackPlayerCore: Calling player.play()")
1140
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Calling player.play()")
1198
1141
  if let player = self.player {
1199
- print("▶️ TrackPlayerCore: Player status: \(player.status.rawValue)")
1142
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Player status: \(player.status.rawValue)")
1200
1143
  if let currentItem = player.currentItem {
1201
- print("▶️ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
1144
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Current item status: \(currentItem.status.rawValue)")
1202
1145
  if let error = currentItem.error {
1203
- print("❌ TrackPlayerCore: Current item error: \(error.localizedDescription)")
1146
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Current item error: \(error.localizedDescription)")
1204
1147
  }
1205
1148
  }
1206
1149
  player.play()
@@ -1211,12 +1154,12 @@ class TrackPlayerCore: NSObject {
1211
1154
  self?.emitStateChange()
1212
1155
  }
1213
1156
  } else {
1214
- print("❌ TrackPlayerCore: No player available")
1157
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ No player available")
1215
1158
  }
1216
1159
  }
1217
1160
 
1218
1161
  func pause() {
1219
- print("⏸️ TrackPlayerCore: pause() called")
1162
+ NitroPlayerLogger.log("TrackPlayerCore", "⏸️ pause() called")
1220
1163
  if Thread.isMainThread {
1221
1164
  pauseInternal()
1222
1165
  } else {
@@ -1245,31 +1188,31 @@ class TrackPlayerCore: NSObject {
1245
1188
  self.playNextStack.removeAll()
1246
1189
  self.upNextQueue.removeAll()
1247
1190
  self.currentTemporaryType = .none
1248
- print(" 🧹 Cleared temporary tracks")
1191
+ NitroPlayerLogger.log("TrackPlayerCore", " 🧹 Cleared temporary tracks")
1249
1192
 
1250
1193
  var targetPlaylistId: String?
1251
1194
  var songIndex: Int = -1
1252
1195
 
1253
1196
  // Case 1: If fromPlaylist is provided, use that playlist
1254
1197
  if let playlistId = fromPlaylist {
1255
- print("🎵 TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
1198
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 Looking for song in specified playlist: \(playlistId)")
1256
1199
  if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
1257
1200
  if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
1258
1201
  targetPlaylistId = playlistId
1259
1202
  songIndex = index
1260
- print("✅ Found song at index \(index) in playlist \(playlistId)")
1203
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Found song at index \(index) in playlist \(playlistId)")
1261
1204
  } else {
1262
- print("⚠️ Song \(songId) not found in specified playlist \(playlistId)")
1205
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Song \(songId) not found in specified playlist \(playlistId)")
1263
1206
  return
1264
1207
  }
1265
1208
  } else {
1266
- print("⚠️ Playlist \(playlistId) not found")
1209
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Playlist \(playlistId) not found")
1267
1210
  return
1268
1211
  }
1269
1212
  }
1270
1213
  // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
1271
1214
  else {
1272
- print("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
1215
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 No playlist specified, checking current playlist")
1273
1216
 
1274
1217
  // Check if song exists in currently loaded playlist
1275
1218
  if let currentId = self.currentPlaylistId,
@@ -1278,20 +1221,20 @@ class TrackPlayerCore: NSObject {
1278
1221
  if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
1279
1222
  targetPlaylistId = currentId
1280
1223
  songIndex = index
1281
- print("✅ Found song at index \(index) in current playlist \(currentId)")
1224
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Found song at index \(index) in current playlist \(currentId)")
1282
1225
  }
1283
1226
  }
1284
1227
 
1285
1228
  // If not found in current playlist, search in all playlists
1286
1229
  if songIndex == -1 {
1287
- print("🔍 Song not found in current playlist, searching all playlists...")
1230
+ NitroPlayerLogger.log("TrackPlayerCore", "🔍 Song not found in current playlist, searching all playlists...")
1288
1231
  let allPlaylists = self.playlistManager.getAllPlaylists()
1289
1232
 
1290
1233
  for playlist in allPlaylists {
1291
1234
  if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
1292
1235
  targetPlaylistId = playlist.id
1293
1236
  songIndex = index
1294
- print("✅ Found song at index \(index) in playlist \(playlist.id)")
1237
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Found song at index \(index) in playlist \(playlist.id)")
1295
1238
  break
1296
1239
  }
1297
1240
  }
@@ -1300,20 +1243,20 @@ class TrackPlayerCore: NSObject {
1300
1243
  if songIndex == -1 && !allPlaylists.isEmpty {
1301
1244
  targetPlaylistId = allPlaylists[0].id
1302
1245
  songIndex = 0
1303
- print("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
1246
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Song not found in any playlist, using first playlist and starting at index 0")
1304
1247
  }
1305
1248
  }
1306
1249
  }
1307
1250
 
1308
1251
  // Now play the song
1309
1252
  guard let playlistId = targetPlaylistId, songIndex >= 0 else {
1310
- print("❌ Could not determine playlist or song index")
1253
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Could not determine playlist or song index")
1311
1254
  return
1312
1255
  }
1313
1256
 
1314
1257
  // Load playlist if it's different from current
1315
1258
  if self.currentPlaylistId != playlistId {
1316
- print("🔄 Loading new playlist: \(playlistId)")
1259
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 Loading new playlist: \(playlistId)")
1317
1260
  if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
1318
1261
  self.currentPlaylistId = playlistId
1319
1262
  self.updatePlayerQueue(tracks: playlist.tracks)
@@ -1321,7 +1264,7 @@ class TrackPlayerCore: NSObject {
1321
1264
  }
1322
1265
 
1323
1266
  // Play from the found index
1324
- print("▶️ Playing from index: \(songIndex)")
1267
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Playing from index: \(songIndex)")
1325
1268
  self.playFromIndex(index: songIndex)
1326
1269
  }
1327
1270
 
@@ -1432,17 +1375,18 @@ class TrackPlayerCore: NSObject {
1432
1375
  // MARK: - Repeat Mode
1433
1376
 
1434
1377
  func setRepeatMode(mode: RepeatMode) -> Bool {
1435
- print("🔁 TrackPlayerCore: setRepeatMode called with mode: \(mode)")
1436
- if Thread.isMainThread {
1437
- self.repeatMode = mode
1438
- } else {
1439
- DispatchQueue.main.sync { [weak self] in
1440
- self?.repeatMode = mode
1441
- }
1378
+ currentRepeatMode = mode
1379
+ DispatchQueue.main.async { [weak self] in
1380
+ self?.player?.actionAtItemEnd = (mode == .track) ? .none : .advance
1442
1381
  }
1382
+ NitroPlayerLogger.log("TrackPlayerCore", "🔁 setRepeatMode: \(mode)")
1443
1383
  return true
1444
1384
  }
1445
1385
 
1386
+ func getRepeatMode() -> RepeatMode {
1387
+ return currentRepeatMode
1388
+ }
1389
+
1446
1390
  func getState() -> PlayerState {
1447
1391
  // Called from Promise.async background thread
1448
1392
  // Schedule on main thread and wait for result
@@ -1544,7 +1488,7 @@ class TrackPlayerCore: NSObject {
1544
1488
 
1545
1489
  func setVolume(volume: Double) -> Bool {
1546
1490
  guard let player = player else {
1547
- print("⚠️ TrackPlayerCore: Cannot set volume - no player available")
1491
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Cannot set volume - no player available")
1548
1492
  return false
1549
1493
  }
1550
1494
  DispatchQueue.main.async { [weak self] in
@@ -1556,8 +1500,7 @@ class TrackPlayerCore: NSObject {
1556
1500
  // Convert to 0.0-1.0 range for AVQueuePlayer
1557
1501
  let normalizedVolume = Float(clampedVolume / 100.0)
1558
1502
  currentPlayer.volume = normalizedVolume
1559
- print(
1560
- "🔊 TrackPlayerCore: Volume set to \(Int(clampedVolume))% (normalized: \(normalizedVolume))")
1503
+ NitroPlayerLogger.log("TrackPlayerCore", "🔊 Volume set to \(Int(clampedVolume))% (normalized: \(normalizedVolume))")
1561
1504
  }
1562
1505
  return true
1563
1506
  }
@@ -1688,21 +1631,19 @@ class TrackPlayerCore: NSObject {
1688
1631
 
1689
1632
  private func playFromIndexInternalWithResult(index: Int) -> Bool {
1690
1633
  guard index >= 0 && index < self.currentTracks.count else {
1691
- print(
1692
- "❌ TrackPlayerCore: playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)"
1693
- )
1634
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)")
1694
1635
  return false
1695
1636
  }
1696
1637
 
1697
- print("\n🎯 TrackPlayerCore: PLAY FROM INDEX \(index)")
1698
- print(" Total tracks in playlist: \(self.currentTracks.count)")
1699
- print(" Current index: \(self.currentTrackIndex), target index: \(index)")
1638
+ NitroPlayerLogger.log("TrackPlayerCore", "\n🎯 PLAY FROM INDEX \(index)")
1639
+ NitroPlayerLogger.log("TrackPlayerCore", " Total tracks in playlist: \(self.currentTracks.count)")
1640
+ NitroPlayerLogger.log("TrackPlayerCore", " Current index: \(self.currentTrackIndex), target index: \(index)")
1700
1641
 
1701
1642
  // Clear temporary tracks when jumping to specific index
1702
1643
  self.playNextStack.removeAll()
1703
1644
  self.upNextQueue.removeAll()
1704
1645
  self.currentTemporaryType = .none
1705
- print(" 🧹 Cleared temporary tracks")
1646
+ NitroPlayerLogger.log("TrackPlayerCore", " 🧹 Cleared temporary tracks")
1706
1647
 
1707
1648
  // Store the full playlist
1708
1649
  let fullPlaylist = self.currentTracks
@@ -1713,9 +1654,7 @@ class TrackPlayerCore: NSObject {
1713
1654
  // Recreate the queue starting from the target index
1714
1655
  // This ensures all remaining tracks are in the queue
1715
1656
  let tracksToPlay = Array(fullPlaylist[index...])
1716
- print(
1717
- " 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
1718
- )
1657
+ NitroPlayerLogger.log("TrackPlayerCore", " 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)")
1719
1658
 
1720
1659
  // Create gapless-optimized player items
1721
1660
  let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
@@ -1724,7 +1663,7 @@ class TrackPlayerCore: NSObject {
1724
1663
  }
1725
1664
 
1726
1665
  guard let player = self.player, !items.isEmpty else {
1727
- print("❌ No player or no items to play")
1666
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ No player or no items to play")
1728
1667
  return false
1729
1668
  }
1730
1669
 
@@ -1749,9 +1688,9 @@ class TrackPlayerCore: NSObject {
1749
1688
  // Restore the full playlist reference (don't slice it!)
1750
1689
  self.currentTracks = fullPlaylist
1751
1690
 
1752
- print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
1691
+ NitroPlayerLogger.log("TrackPlayerCore", " ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
1753
1692
  if let track = self.getCurrentTrack() {
1754
- print(" 🎵 Playing: \(track.title)")
1693
+ NitroPlayerLogger.log("TrackPlayerCore", " 🎵 Playing: \(track.title)")
1755
1694
  notifyTrackChange(track, .skip)
1756
1695
  self.mediaSessionManager?.onTrackChanged()
1757
1696
  }
@@ -1776,17 +1715,17 @@ class TrackPlayerCore: NSObject {
1776
1715
  }
1777
1716
 
1778
1717
  private func addToUpNextInternal(trackId: String) {
1779
- print("📋 TrackPlayerCore: addToUpNext(\(trackId))")
1718
+ NitroPlayerLogger.log("TrackPlayerCore", "📋 addToUpNext(\(trackId))")
1780
1719
 
1781
1720
  // Find the track from current playlist or all playlists
1782
1721
  guard let track = self.findTrackById(trackId) else {
1783
- print("❌ TrackPlayerCore: Track \(trackId) not found")
1722
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Track \(trackId) not found")
1784
1723
  return
1785
1724
  }
1786
1725
 
1787
1726
  // Add to end of upNext queue (FIFO)
1788
1727
  self.upNextQueue.append(track)
1789
- print(" ✅ Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
1728
+ NitroPlayerLogger.log("TrackPlayerCore", " ✅ Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
1790
1729
 
1791
1730
  // Rebuild the player queue if actively playing
1792
1731
  if self.player?.currentItem != nil {
@@ -1806,17 +1745,17 @@ class TrackPlayerCore: NSObject {
1806
1745
  }
1807
1746
 
1808
1747
  private func playNextInternal(trackId: String) {
1809
- print("⏭️ TrackPlayerCore: playNext(\(trackId))")
1748
+ NitroPlayerLogger.log("TrackPlayerCore", "⏭️ playNext(\(trackId))")
1810
1749
 
1811
1750
  // Find the track from current playlist or all playlists
1812
1751
  guard let track = self.findTrackById(trackId) else {
1813
- print("❌ TrackPlayerCore: Track \(trackId) not found")
1752
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Track \(trackId) not found")
1814
1753
  return
1815
1754
  }
1816
1755
 
1817
1756
  // Insert at beginning of playNext stack (LIFO)
1818
1757
  self.playNextStack.insert(track, at: 0)
1819
- print(" ✅ Added '\(track.title)' to playNext stack (position: 1)")
1758
+ NitroPlayerLogger.log("TrackPlayerCore", " ✅ Added '\(track.title)' to playNext stack (position: 1)")
1820
1759
 
1821
1760
  // Rebuild the player queue if actively playing
1822
1761
  if self.player?.currentItem != nil {
@@ -1921,7 +1860,7 @@ class TrackPlayerCore: NSObject {
1921
1860
  // MARK: - Cleanup
1922
1861
 
1923
1862
  deinit {
1924
- print("🧹 TrackPlayerCore: Cleaning up...")
1863
+ NitroPlayerLogger.log("TrackPlayerCore", "🧹 Cleaning up...")
1925
1864
 
1926
1865
  // Clear preloaded assets for gapless playback
1927
1866
  preloadedAssets.removeAll()
@@ -1940,12 +1879,12 @@ class TrackPlayerCore: NSObject {
1940
1879
  currentPlayer.removeObserver(self, forKeyPath: "rate")
1941
1880
  currentPlayer.removeObserver(self, forKeyPath: "timeControlStatus")
1942
1881
  currentPlayer.removeObserver(self, forKeyPath: "currentItem")
1943
- print("✅ TrackPlayerCore: Player observers removed")
1882
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Player observers removed")
1944
1883
  }
1945
1884
 
1946
1885
  // Remove all notification observers
1947
1886
  NotificationCenter.default.removeObserver(self)
1948
- print("✅ TrackPlayerCore: Cleanup complete")
1887
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Cleanup complete")
1949
1888
  }
1950
1889
  }
1951
1890