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
@@ -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)
@@ -16,11 +16,12 @@ class PlaylistManager {
16
16
  [:]
17
17
  private var currentPlaylistId: String?
18
18
  private let queue = DispatchQueue(label: "com.margelo.nitro.nitroplayer.playlist")
19
+ private var saveDebounceWorkItem: DispatchWorkItem?
19
20
 
20
21
  static let shared = PlaylistManager()
21
22
 
22
23
  private init() {
23
- loadPlaylistsFromUserDefaults()
24
+ loadFromFile()
24
25
  }
25
26
 
26
27
  /**
@@ -34,7 +35,7 @@ class PlaylistManager {
34
35
  playlists[id] = playlist
35
36
  }
36
37
 
37
- savePlaylistsToUserDefaults()
38
+ scheduleSave()
38
39
  notifyPlaylistsChanged(.add)
39
40
 
40
41
  return id
@@ -53,7 +54,7 @@ class PlaylistManager {
53
54
  currentPlaylistId = nil
54
55
  }
55
56
  playlistListeners.removeValue(forKey: playlistId)
56
- savePlaylistsToUserDefaults()
57
+ scheduleSave()
57
58
  notifyPlaylistsChanged(.remove)
58
59
  return true
59
60
  }
@@ -81,7 +82,7 @@ class PlaylistManager {
81
82
  )
82
83
  }
83
84
 
84
- savePlaylistsToUserDefaults()
85
+ scheduleSave()
85
86
  notifyPlaylistChanged(playlistId, .update)
86
87
  notifyPlaylistsChanged(.update)
87
88
 
@@ -130,7 +131,7 @@ class PlaylistManager {
130
131
  )
131
132
  }
132
133
 
133
- savePlaylistsToUserDefaults()
134
+ scheduleSave()
134
135
  notifyPlaylistChanged(playlistId, .add)
135
136
 
136
137
  // Update TrackPlayerCore if this is the current playlist
@@ -165,7 +166,7 @@ class PlaylistManager {
165
166
  )
166
167
  }
167
168
 
168
- savePlaylistsToUserDefaults()
169
+ scheduleSave()
169
170
  notifyPlaylistChanged(playlistId, .add)
170
171
 
171
172
  // Update TrackPlayerCore if this is the current playlist
@@ -204,7 +205,7 @@ class PlaylistManager {
204
205
  }
205
206
 
206
207
  if removed {
207
- savePlaylistsToUserDefaults()
208
+ scheduleSave()
208
209
  notifyPlaylistChanged(playlistId, .remove)
209
210
 
210
211
  // Update TrackPlayerCore if this is the current playlist
@@ -245,7 +246,7 @@ class PlaylistManager {
245
246
  )
246
247
  }
247
248
 
248
- savePlaylistsToUserDefaults()
249
+ scheduleSave()
249
250
  notifyPlaylistChanged(playlistId, .update)
250
251
 
251
252
  // Update TrackPlayerCore if this is the current playlist
@@ -349,13 +350,20 @@ class PlaylistManager {
349
350
  }
350
351
  }
351
352
 
352
- private func savePlaylistsToUserDefaults() {
353
- // Save playlists to UserDefaults for persistence
354
- // Implementation similar to Android SharedPreferences
353
+ private func scheduleSave() {
354
+ saveDebounceWorkItem?.cancel()
355
+ let work = DispatchWorkItem { [weak self] in self?.saveToFile() }
356
+ saveDebounceWorkItem = work
357
+ // Use global background queue — saveToFile calls queue.sync internally,
358
+ // which would deadlock if scheduled on queue itself.
359
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.3, execute: work)
360
+ }
361
+
362
+ // MARK: - Persistence
363
+
364
+ private func saveToFile() {
355
365
  do {
356
- let playlistsArray = queue.sync {
357
- return Array(playlists.values)
358
- }
366
+ let playlistsArray = queue.sync { Array(playlists.values) }
359
367
  let playlistsData = playlistsArray.map { playlist -> [String: Any] in
360
368
  return [
361
369
  "id": playlist.id,
@@ -371,13 +379,11 @@ class PlaylistManager {
371
379
  "duration": track.duration,
372
380
  "url": track.url,
373
381
  ]
374
- // Handle artwork - unwrap Variant_NullType_String
375
382
  if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
376
383
  trackDict["artwork"] = artworkUrl
377
384
  } else {
378
385
  trackDict["artwork"] = ""
379
386
  }
380
- // Serialize extraPayload to dictionary for persistence
381
387
  if let extraPayload = track.extraPayload {
382
388
  trackDict["extraPayload"] = extraPayload.toDictionary()
383
389
  }
@@ -385,93 +391,118 @@ class PlaylistManager {
385
391
  },
386
392
  ]
387
393
  }
388
- let data = try JSONSerialization.data(withJSONObject: playlistsData, options: [])
389
- UserDefaults.standard.set(data, forKey: "NitroPlayerPlaylists")
390
- UserDefaults.standard.set(currentPlaylistId, forKey: "NitroPlayerCurrentPlaylistId")
394
+ let wrapper: [String: Any] = [
395
+ "playlists": playlistsData,
396
+ "currentPlaylistId": currentPlaylistId as Any,
397
+ ]
398
+ let data = try JSONSerialization.data(withJSONObject: wrapper, options: [])
399
+ try NitroPlayerStorage.write(filename: "playlists.json", data: data)
391
400
  } catch {
392
- print("❌ PlaylistManager: Error saving playlists - \(error)")
401
+ NitroPlayerLogger.log("PlaylistManager", "❌ Error saving playlists - \(error)")
393
402
  }
394
403
  }
395
404
 
396
- private func loadPlaylistsFromUserDefaults() {
397
- guard let data = UserDefaults.standard.data(forKey: "NitroPlayerPlaylists") else {
405
+ private func loadFromFile() {
406
+ // 1. Try new JSON file (post-migration)
407
+ if let data = NitroPlayerStorage.read(filename: "playlists.json") {
408
+ do {
409
+ if let wrapper = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
410
+ let playlistsDict = wrapper["playlists"] as? [[String: Any]] ?? []
411
+ parsePlaylists(from: playlistsDict)
412
+ currentPlaylistId = wrapper["currentPlaylistId"] as? String
413
+ }
414
+ } catch {
415
+ NitroPlayerLogger.log("PlaylistManager", "❌ Error loading playlists - \(error)")
416
+ }
417
+ return
418
+ }
419
+
420
+ // 2. Migrate from UserDefaults (one-time, existing installs)
421
+ if let data = UserDefaults.standard.data(forKey: "NitroPlayerPlaylists") {
422
+ do {
423
+ let playlistsDict = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
424
+ parsePlaylists(from: playlistsDict)
425
+ currentPlaylistId = UserDefaults.standard.string(forKey: "NitroPlayerCurrentPlaylistId")
426
+ // Remove old keys to free UserDefaults space
427
+ UserDefaults.standard.removeObject(forKey: "NitroPlayerPlaylists")
428
+ UserDefaults.standard.removeObject(forKey: "NitroPlayerCurrentPlaylistId")
429
+ // Persist in new format
430
+ saveToFile()
431
+ } catch {
432
+ NitroPlayerLogger.log("PlaylistManager", "❌ Error migrating playlists - \(error)")
433
+ }
398
434
  return
399
435
  }
400
436
 
401
- do {
402
- let playlistsDict = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
437
+ // 3. Fresh install — nothing to load
438
+ }
403
439
 
404
- queue.sync {
405
- playlists.removeAll()
406
- for playlistDict in playlistsDict {
407
- guard let id = playlistDict["id"] as? String,
408
- let name = playlistDict["name"] as? String
440
+ private func parsePlaylists(from playlistsDict: [[String: Any]]) {
441
+ queue.sync {
442
+ playlists.removeAll()
443
+ for playlistDict in playlistsDict {
444
+ guard let id = playlistDict["id"] as? String,
445
+ let name = playlistDict["name"] as? String
446
+ else {
447
+ continue
448
+ }
449
+
450
+ let description = playlistDict["description"] as? String
451
+ let artwork = playlistDict["artwork"] as? String
452
+ let tracksArray = playlistDict["tracks"] as? [[String: Any]] ?? []
453
+
454
+ let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
455
+ guard let id = trackDict["id"] as? String,
456
+ let title = trackDict["title"] as? String,
457
+ let artist = trackDict["artist"] as? String,
458
+ let album = trackDict["album"] as? String,
459
+ let duration = trackDict["duration"] as? Double,
460
+ let url = trackDict["url"] as? String
409
461
  else {
410
- continue
462
+ return nil
411
463
  }
412
464
 
413
- let description = playlistDict["description"] as? String
414
- let artwork = playlistDict["artwork"] as? String
415
- let tracksArray = playlistDict["tracks"] as? [[String: Any]] ?? []
416
-
417
- let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
418
- guard let id = trackDict["id"] as? String,
419
- let title = trackDict["title"] as? String,
420
- let artist = trackDict["artist"] as? String,
421
- let album = trackDict["album"] as? String,
422
- let duration = trackDict["duration"] as? Double,
423
- let url = trackDict["url"] as? String
424
- else {
425
- return nil
426
- }
465
+ let artworkString = trackDict["artwork"] as? String
466
+ let artwork = artworkString.flatMap {
467
+ !$0.isEmpty ? Variant_NullType_String.second($0) : nil
468
+ }
427
469
 
428
- let artworkString = trackDict["artwork"] as? String
429
- let artwork = artworkString.flatMap {
430
- !$0.isEmpty ? Variant_NullType_String.second($0) : nil
431
- }
432
-
433
- // Deserialize extraPayload from dictionary
434
- var extraPayload: AnyMap? = nil
435
- if let extraPayloadDict = trackDict["extraPayload"] as? [String: Any] {
436
- extraPayload = AnyMap()
437
- for (key, value) in extraPayloadDict {
438
- if let stringValue = value as? String {
439
- extraPayload?.setString(key: key, value: stringValue)
440
- } else if let doubleValue = value as? Double {
441
- extraPayload?.setDouble(key: key, value: doubleValue)
442
- } else if let intValue = value as? Int {
443
- extraPayload?.setDouble(key: key, value: Double(intValue))
444
- } else if let boolValue = value as? Bool {
445
- extraPayload?.setBoolean(key: key, value: boolValue)
446
- }
470
+ var extraPayload: AnyMap? = nil
471
+ if let extraPayloadDict = trackDict["extraPayload"] as? [String: Any] {
472
+ extraPayload = AnyMap()
473
+ for (key, value) in extraPayloadDict {
474
+ if let stringValue = value as? String {
475
+ extraPayload?.setString(key: key, value: stringValue)
476
+ } else if let doubleValue = value as? Double {
477
+ extraPayload?.setDouble(key: key, value: doubleValue)
478
+ } else if let intValue = value as? Int {
479
+ extraPayload?.setDouble(key: key, value: Double(intValue))
480
+ } else if let boolValue = value as? Bool {
481
+ extraPayload?.setBoolean(key: key, value: boolValue)
447
482
  }
448
483
  }
449
-
450
- return TrackItem(
451
- id: id,
452
- title: title,
453
- artist: artist,
454
- album: album,
455
- duration: duration,
456
- url: url,
457
- artwork: artwork,
458
- extraPayload: extraPayload
459
- )
460
484
  }
461
485
 
462
- playlists[id] = PlaylistModel(
486
+ return TrackItem(
463
487
  id: id,
464
- name: name,
465
- description: description,
488
+ title: title,
489
+ artist: artist,
490
+ album: album,
491
+ duration: duration,
492
+ url: url,
466
493
  artwork: artwork,
467
- tracks: tracks
494
+ extraPayload: extraPayload
468
495
  )
469
496
  }
470
- }
471
497
 
472
- currentPlaylistId = UserDefaults.standard.string(forKey: "NitroPlayerCurrentPlaylistId")
473
- } catch {
474
- print("❌ PlaylistManager: Error loading playlists - \(error)")
498
+ playlists[id] = PlaylistModel(
499
+ id: id,
500
+ name: name,
501
+ description: description,
502
+ artwork: artwork,
503
+ tracks: tracks
504
+ )
505
+ }
475
506
  }
476
507
  }
477
508
  }
@@ -114,7 +114,7 @@ class QueueManager {
114
114
  wrapper.listener(currentTracks, operation)
115
115
  } catch {
116
116
  // Log error but don't break other listeners
117
- print("Error in queue change listener: \(error)")
117
+ NitroPlayerLogger.log("QueueManager", "Error in queue change listener: \(error)")
118
118
  }
119
119
  }
120
120
  }
@@ -0,0 +1,44 @@
1
+ //
2
+ // NitroPlayerStorage.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 19/02/26.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ enum NitroPlayerStorage {
11
+ /// Reads raw data from a file in the NitroPlayer storage directory.
12
+ /// Returns nil if the file does not exist or cannot be read.
13
+ static func read(filename: String) -> Data? {
14
+ let url = storageDirectory().appendingPathComponent(filename)
15
+ return try? Data(contentsOf: url)
16
+ }
17
+
18
+ /// Atomically writes data to a file in the NitroPlayer storage directory.
19
+ /// Writes to `<filename>.tmp` first, then renames to the final name —
20
+ /// leaving the prior file untouched if the write crashes mid-way.
21
+ static func write(filename: String, data: Data) throws {
22
+ let dir = storageDirectory()
23
+ try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
24
+ let dest = dir.appendingPathComponent(filename)
25
+ let tmp = dir.appendingPathComponent(filename + ".tmp")
26
+ try data.write(to: tmp)
27
+ if FileManager.default.fileExists(atPath: dest.path) {
28
+ _ = try FileManager.default.replaceItemAt(dest, withItemAt: tmp)
29
+ } else {
30
+ try FileManager.default.moveItem(at: tmp, to: dest)
31
+ }
32
+ }
33
+
34
+ /// Returns the NitroPlayer subdirectory inside Application Support.
35
+ /// Uses `FileManager` APIs — never hardcodes the UUID-based container path
36
+ /// so this resolves correctly regardless of which device or simulator the
37
+ /// app runs on.
38
+ private static func storageDirectory() -> URL {
39
+ let appSupport = FileManager.default.urls(
40
+ for: .applicationSupportDirectory, in: .userDomainMask
41
+ ).first!
42
+ return appSupport.appendingPathComponent("NitroPlayer", isDirectory: true)
43
+ }
44
+ }
@@ -35,6 +35,7 @@ export interface TrackPlayer extends HybridObject<{
35
35
  getActualQueue(): Promise<TrackItem[]>;
36
36
  getState(): Promise<PlayerState>;
37
37
  setRepeatMode(mode: RepeatMode): boolean;
38
+ getRepeatMode(): RepeatMode;
38
39
  configure(config: PlayerConfig): void;
39
40
  onChangeTrack(callback: (track: TrackItem, reason?: Reason) => void): void;
40
41
  onPlaybackStateChange(callback: (state: TrackPlayerState, reason?: Reason) => void): void;
@@ -19,7 +19,7 @@ export interface Playlist {
19
19
  }
20
20
  export type QueueOperation = 'add' | 'remove' | 'clear' | 'update';
21
21
  export type TrackPlayerState = 'playing' | 'paused' | 'stopped';
22
- export type Reason = 'user_action' | 'skip' | 'end' | 'error';
22
+ export type Reason = 'user_action' | 'skip' | 'end' | 'error' | 'repeat';
23
23
  export interface PlayerState {
24
24
  currentTrack: TrackItem | null;
25
25
  currentPosition: number;
@@ -215,6 +215,11 @@ namespace margelo::nitro::nitroplayer {
215
215
  auto __result = method(_javaPart, JRepeatMode::fromCpp(mode));
216
216
  return static_cast<bool>(__result);
217
217
  }
218
+ RepeatMode JHybridTrackPlayerSpec::getRepeatMode() {
219
+ static const auto method = javaClassStatic()->getMethod<jni::local_ref<JRepeatMode>()>("getRepeatMode");
220
+ auto __result = method(_javaPart);
221
+ return __result->toCpp();
222
+ }
218
223
  void JHybridTrackPlayerSpec::configure(const PlayerConfig& config) {
219
224
  static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JPlayerConfig> /* config */)>("configure");
220
225
  method(_javaPart, JPlayerConfig::fromCpp(config));
@@ -66,6 +66,7 @@ namespace margelo::nitro::nitroplayer {
66
66
  std::shared_ptr<Promise<std::vector<TrackItem>>> getActualQueue() override;
67
67
  std::shared_ptr<Promise<PlayerState>> getState() override;
68
68
  bool setRepeatMode(RepeatMode mode) override;
69
+ RepeatMode getRepeatMode() override;
69
70
  void configure(const PlayerConfig& config) override;
70
71
  void onChangeTrack(const std::function<void(const TrackItem& /* track */, std::optional<Reason> /* reason */)>& callback) override;
71
72
  void onPlaybackStateChange(const std::function<void(TrackPlayerState /* state */, std::optional<Reason> /* reason */)>& callback) override;
@@ -45,6 +45,7 @@ namespace margelo::nitro::nitroplayer {
45
45
  static const auto fieldSKIP = clazz->getStaticField<JReason>("SKIP");
46
46
  static const auto fieldEND = clazz->getStaticField<JReason>("END");
47
47
  static const auto fieldERROR = clazz->getStaticField<JReason>("ERROR");
48
+ static const auto fieldREPEAT = clazz->getStaticField<JReason>("REPEAT");
48
49
 
49
50
  switch (value) {
50
51
  case Reason::USER_ACTION:
@@ -55,6 +56,8 @@ namespace margelo::nitro::nitroplayer {
55
56
  return clazz->getStaticFieldValue(fieldEND);
56
57
  case Reason::ERROR:
57
58
  return clazz->getStaticFieldValue(fieldERROR);
59
+ case Reason::REPEAT:
60
+ return clazz->getStaticFieldValue(fieldREPEAT);
58
61
  default:
59
62
  std::string stringValue = std::to_string(static_cast<int>(value));
60
63
  throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
@@ -94,6 +94,10 @@ abstract class HybridTrackPlayerSpec: HybridObject() {
94
94
  @Keep
95
95
  abstract fun setRepeatMode(mode: RepeatMode): Boolean
96
96
 
97
+ @DoNotStrip
98
+ @Keep
99
+ abstract fun getRepeatMode(): RepeatMode
100
+
97
101
  @DoNotStrip
98
102
  @Keep
99
103
  abstract fun configure(config: PlayerConfig): Unit
@@ -19,5 +19,6 @@ enum class Reason(@DoNotStrip @Keep val value: Int) {
19
19
  USER_ACTION(0),
20
20
  SKIP(1),
21
21
  END(2),
22
- ERROR(3);
22
+ ERROR(3),
23
+ REPEAT(4);
23
24
  }
@@ -60,6 +60,8 @@ namespace margelo::nitro::nitroplayer { enum class PresetType; }
60
60
  namespace margelo::nitro::nitroplayer { enum class QueueOperation; }
61
61
  // Forward declaration of `Reason` to properly resolve imports.
62
62
  namespace margelo::nitro::nitroplayer { enum class Reason; }
63
+ // Forward declaration of `RepeatMode` to properly resolve imports.
64
+ namespace margelo::nitro::nitroplayer { enum class RepeatMode; }
63
65
  // Forward declaration of `StorageLocation` to properly resolve imports.
64
66
  namespace margelo::nitro::nitroplayer { enum class StorageLocation; }
65
67
  // Forward declaration of `TrackItem` to properly resolve imports.
@@ -106,6 +108,7 @@ namespace NitroPlayer { class HybridTrackPlayerSpec_cxx; }
106
108
  #include "PresetType.hpp"
107
109
  #include "QueueOperation.hpp"
108
110
  #include "Reason.hpp"
111
+ #include "RepeatMode.hpp"
109
112
  #include "StorageLocation.hpp"
110
113
  #include "TrackItem.hpp"
111
114
  #include "TrackPlayerState.hpp"
@@ -1452,5 +1455,14 @@ namespace margelo::nitro::nitroplayer::bridge::swift {
1452
1455
  inline Result_std__shared_ptr_Promise_PlayerState___ create_Result_std__shared_ptr_Promise_PlayerState___(const std::exception_ptr& error) noexcept {
1453
1456
  return Result<std::shared_ptr<Promise<PlayerState>>>::withError(error);
1454
1457
  }
1458
+
1459
+ // pragma MARK: Result<RepeatMode>
1460
+ using Result_RepeatMode_ = Result<RepeatMode>;
1461
+ inline Result_RepeatMode_ create_Result_RepeatMode_(RepeatMode value) noexcept {
1462
+ return Result<RepeatMode>::withValue(std::move(value));
1463
+ }
1464
+ inline Result_RepeatMode_ create_Result_RepeatMode_(const std::exception_ptr& error) noexcept {
1465
+ return Result<RepeatMode>::withError(error);
1466
+ }
1455
1467
 
1456
1468
  } // namespace margelo::nitro::nitroplayer::bridge::swift
@@ -173,6 +173,14 @@ namespace margelo::nitro::nitroplayer {
173
173
  auto __value = std::move(__result.value());
174
174
  return __value;
175
175
  }
176
+ inline RepeatMode getRepeatMode() override {
177
+ auto __result = _swiftPart.getRepeatMode();
178
+ if (__result.hasError()) [[unlikely]] {
179
+ std::rethrow_exception(__result.error());
180
+ }
181
+ auto __value = std::move(__result.value());
182
+ return __value;
183
+ }
176
184
  inline void configure(const PlayerConfig& config) override {
177
185
  auto __result = _swiftPart.configure(std::forward<decltype(config)>(config));
178
186
  if (__result.hasError()) [[unlikely]] {
@@ -26,6 +26,7 @@ public protocol HybridTrackPlayerSpec_protocol: HybridObject {
26
26
  func getActualQueue() throws -> Promise<[TrackItem]>
27
27
  func getState() throws -> Promise<PlayerState>
28
28
  func setRepeatMode(mode: RepeatMode) throws -> Bool
29
+ func getRepeatMode() throws -> RepeatMode
29
30
  func configure(config: PlayerConfig) throws -> Void
30
31
  func onChangeTrack(callback: @escaping (_ track: TrackItem, _ reason: Reason?) -> Void) throws -> Void
31
32
  func onPlaybackStateChange(callback: @escaping (_ state: TrackPlayerState, _ reason: Reason?) -> Void) throws -> Void