react-native-nitro-player 0.5.4 → 0.5.6

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.
@@ -21,7 +21,7 @@ class PlaylistManager {
21
21
  static let shared = PlaylistManager()
22
22
 
23
23
  private init() {
24
- loadPlaylistsFromUserDefaults()
24
+ loadFromFile()
25
25
  }
26
26
 
27
27
  /**
@@ -352,20 +352,18 @@ class PlaylistManager {
352
352
 
353
353
  private func scheduleSave() {
354
354
  saveDebounceWorkItem?.cancel()
355
- let work = DispatchWorkItem { [weak self] in self?.savePlaylistsToUserDefaults() }
355
+ let work = DispatchWorkItem { [weak self] in self?.saveToFile() }
356
356
  saveDebounceWorkItem = work
357
- // Use global background queue — savePlaylistsToUserDefaults calls queue.sync internally,
357
+ // Use global background queue — saveToFile calls queue.sync internally,
358
358
  // which would deadlock if scheduled on queue itself.
359
359
  DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.3, execute: work)
360
360
  }
361
361
 
362
- private func savePlaylistsToUserDefaults() {
363
- // Save playlists to UserDefaults for persistence
364
- // Implementation similar to Android SharedPreferences
362
+ // MARK: - Persistence
363
+
364
+ private func saveToFile() {
365
365
  do {
366
- let playlistsArray = queue.sync {
367
- return Array(playlists.values)
368
- }
366
+ let playlistsArray = queue.sync { Array(playlists.values) }
369
367
  let playlistsData = playlistsArray.map { playlist -> [String: Any] in
370
368
  return [
371
369
  "id": playlist.id,
@@ -381,13 +379,11 @@ class PlaylistManager {
381
379
  "duration": track.duration,
382
380
  "url": track.url,
383
381
  ]
384
- // Handle artwork - unwrap Variant_NullType_String
385
382
  if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
386
383
  trackDict["artwork"] = artworkUrl
387
384
  } else {
388
385
  trackDict["artwork"] = ""
389
386
  }
390
- // Serialize extraPayload to dictionary for persistence
391
387
  if let extraPayload = track.extraPayload {
392
388
  trackDict["extraPayload"] = extraPayload.toDictionary()
393
389
  }
@@ -395,93 +391,118 @@ class PlaylistManager {
395
391
  },
396
392
  ]
397
393
  }
398
- let data = try JSONSerialization.data(withJSONObject: playlistsData, options: [])
399
- UserDefaults.standard.set(data, forKey: "NitroPlayerPlaylists")
400
- 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)
401
400
  } catch {
402
401
  NitroPlayerLogger.log("PlaylistManager", "❌ Error saving playlists - \(error)")
403
402
  }
404
403
  }
405
404
 
406
- private func loadPlaylistsFromUserDefaults() {
407
- 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
+ }
408
417
  return
409
418
  }
410
419
 
411
- do {
412
- let playlistsDict = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
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
+ }
434
+ return
435
+ }
413
436
 
414
- queue.sync {
415
- playlists.removeAll()
416
- for playlistDict in playlistsDict {
417
- guard let id = playlistDict["id"] as? String,
418
- let name = playlistDict["name"] as? String
437
+ // 3. Fresh install — nothing to load
438
+ }
439
+
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
419
461
  else {
420
- continue
462
+ return nil
421
463
  }
422
464
 
423
- let description = playlistDict["description"] as? String
424
- let artwork = playlistDict["artwork"] as? String
425
- let tracksArray = playlistDict["tracks"] as? [[String: Any]] ?? []
426
-
427
- let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
428
- guard let id = trackDict["id"] as? String,
429
- let title = trackDict["title"] as? String,
430
- let artist = trackDict["artist"] as? String,
431
- let album = trackDict["album"] as? String,
432
- let duration = trackDict["duration"] as? Double,
433
- let url = trackDict["url"] as? String
434
- else {
435
- return nil
436
- }
465
+ let artworkString = trackDict["artwork"] as? String
466
+ let artwork = artworkString.flatMap {
467
+ !$0.isEmpty ? Variant_NullType_String.second($0) : nil
468
+ }
437
469
 
438
- let artworkString = trackDict["artwork"] as? String
439
- let artwork = artworkString.flatMap {
440
- !$0.isEmpty ? Variant_NullType_String.second($0) : nil
441
- }
442
-
443
- // Deserialize extraPayload from dictionary
444
- var extraPayload: AnyMap? = nil
445
- if let extraPayloadDict = trackDict["extraPayload"] as? [String: Any] {
446
- extraPayload = AnyMap()
447
- for (key, value) in extraPayloadDict {
448
- if let stringValue = value as? String {
449
- extraPayload?.setString(key: key, value: stringValue)
450
- } else if let doubleValue = value as? Double {
451
- extraPayload?.setDouble(key: key, value: doubleValue)
452
- } else if let intValue = value as? Int {
453
- extraPayload?.setDouble(key: key, value: Double(intValue))
454
- } else if let boolValue = value as? Bool {
455
- extraPayload?.setBoolean(key: key, value: boolValue)
456
- }
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)
457
482
  }
458
483
  }
459
-
460
- return TrackItem(
461
- id: id,
462
- title: title,
463
- artist: artist,
464
- album: album,
465
- duration: duration,
466
- url: url,
467
- artwork: artwork,
468
- extraPayload: extraPayload
469
- )
470
484
  }
471
485
 
472
- playlists[id] = PlaylistModel(
486
+ return TrackItem(
473
487
  id: id,
474
- name: name,
475
- description: description,
488
+ title: title,
489
+ artist: artist,
490
+ album: album,
491
+ duration: duration,
492
+ url: url,
476
493
  artwork: artwork,
477
- tracks: tracks
494
+ extraPayload: extraPayload
478
495
  )
479
496
  }
480
- }
481
497
 
482
- currentPlaylistId = UserDefaults.standard.string(forKey: "NitroPlayerCurrentPlaylistId")
483
- } catch {
484
- NitroPlayerLogger.log("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
+ }
485
506
  }
486
507
  }
487
508
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",