react-native-nitro-player 0.5.4 → 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.
@@ -1,27 +1,27 @@
1
1
  package com.margelo.nitro.nitroplayer.download
2
2
 
3
3
  import android.content.Context
4
- import android.content.SharedPreferences
5
- import android.util.Log
6
4
  import com.margelo.nitro.core.NullType
7
5
  import com.margelo.nitro.nitroplayer.*
8
6
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
9
7
  import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
8
+ import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
10
9
  import org.json.JSONArray
11
10
  import org.json.JSONObject
12
11
  import java.io.File
13
12
 
14
13
  /**
15
- * Manages persistence of downloaded track metadata using SharedPreferences
14
+ * Manages persistence of downloaded track metadata using file storage
16
15
  */
17
16
  class DownloadDatabase private constructor(
18
17
  private val context: Context,
19
18
  ) {
20
19
  companion object {
21
20
  private const val TAG = "DownloadDatabase"
22
- private const val PREFS_NAME = "NitroPlayerDownloads"
23
- private const val KEY_DOWNLOADED_TRACKS = "downloaded_tracks"
24
- private const val KEY_PLAYLIST_TRACKS = "playlist_tracks"
21
+ // Legacy SharedPreferences keys (migration only)
22
+ private const val LEGACY_PREFS_NAME = "NitroPlayerDownloads"
23
+ private const val LEGACY_KEY_DOWNLOADED_TRACKS = "downloaded_tracks"
24
+ private const val LEGACY_KEY_PLAYLIST_TRACKS = "playlist_tracks"
25
25
 
26
26
  @Volatile
27
27
  private var instance: DownloadDatabase? = null
@@ -32,7 +32,6 @@ class DownloadDatabase private constructor(
32
32
  }
33
33
  }
34
34
 
35
- private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
36
35
  private val downloadedTracks = mutableMapOf<String, DownloadedTrackRecord>()
37
36
  private val playlistTracks = mutableMapOf<String, MutableSet<String>>()
38
37
  private val fileManager = DownloadFileManager.getInstance(context)
@@ -254,13 +253,13 @@ class DownloadDatabase private constructor(
254
253
  /** Validates all downloads and removes records for missing files */
255
254
  fun syncDownloads(): Int {
256
255
  synchronized(this) {
257
- NitroPlayerLogger.log("DownloadDatabase", "syncDownloads called")
256
+ NitroPlayerLogger.log(TAG, "syncDownloads called")
258
257
 
259
258
  val trackIdsToRemove = mutableListOf<String>()
260
259
 
261
260
  for ((trackId, record) in downloadedTracks) {
262
261
  if (!File(record.localPath).exists()) {
263
- NitroPlayerLogger.log("DownloadDatabase", "Missing file for track $trackId: ${record.localPath}")
262
+ NitroPlayerLogger.log(TAG, "Missing file for track $trackId: ${record.localPath}")
264
263
  trackIdsToRemove.add(trackId)
265
264
  }
266
265
  }
@@ -283,9 +282,9 @@ class DownloadDatabase private constructor(
283
282
 
284
283
  if (trackIdsToRemove.isNotEmpty()) {
285
284
  saveToDisk()
286
- NitroPlayerLogger.log("DownloadDatabase", "Cleaned up ${trackIdsToRemove.size} orphaned records")
285
+ NitroPlayerLogger.log(TAG, "Cleaned up ${trackIdsToRemove.size} orphaned records")
287
286
  } else {
288
- NitroPlayerLogger.log("DownloadDatabase", "All downloads are valid")
287
+ NitroPlayerLogger.log(TAG, "All downloads are valid")
289
288
  }
290
289
 
291
290
  return trackIdsToRemove.size
@@ -295,39 +294,80 @@ class DownloadDatabase private constructor(
295
294
  // Persistence
296
295
  private fun saveToDisk() {
297
296
  try {
298
- // Save downloaded tracks
299
297
  val tracksJson = JSONObject()
300
298
  for ((trackId, record) in downloadedTracks) {
301
299
  tracksJson.put(trackId, record.toJson())
302
300
  }
303
- prefs.edit().putString(KEY_DOWNLOADED_TRACKS, tracksJson.toString()).apply()
304
301
 
305
- // Save playlist associations
306
302
  val playlistJson = JSONObject()
307
303
  for ((playlistId, trackIds) in playlistTracks) {
308
304
  playlistJson.put(playlistId, JSONArray(trackIds.toList()))
309
305
  }
310
- prefs.edit().putString(KEY_PLAYLIST_TRACKS, playlistJson.toString()).apply()
306
+
307
+ val wrapper = JSONObject().apply {
308
+ put("downloadedTracks", tracksJson)
309
+ put("playlistTracks", playlistJson)
310
+ }
311
+ NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
311
312
  } catch (e: Exception) {
312
313
  e.printStackTrace()
313
314
  }
314
315
  }
315
316
 
316
317
  private fun loadFromDisk() {
317
- try {
318
- // Load downloaded tracks
319
- val tracksString = prefs.getString(KEY_DOWNLOADED_TRACKS, null)
320
- if (tracksString != null) {
318
+ // 1. Try new JSON file (post-migration)
319
+ val json = NitroPlayerStorage.read(context, "downloads.json")
320
+ if (json != null) {
321
+ try {
322
+ val wrapper = JSONObject(json)
323
+
324
+ val tracksJson = wrapper.optJSONObject("downloadedTracks")
325
+ if (tracksJson != null) {
326
+ for (trackId in tracksJson.keys()) {
327
+ val record = DownloadedTrackRecord.fromJson(tracksJson.getJSONObject(trackId))
328
+ downloadedTracks[trackId] = record
329
+ }
330
+ }
331
+
332
+ val playlistJson = wrapper.optJSONObject("playlistTracks")
333
+ if (playlistJson != null) {
334
+ for (playlistId in playlistJson.keys()) {
335
+ val trackIdsArray = playlistJson.getJSONArray(playlistId)
336
+ val trackIds = mutableSetOf<String>()
337
+ for (i in 0 until trackIdsArray.length()) {
338
+ trackIds.add(trackIdsArray.getString(i))
339
+ }
340
+ playlistTracks[playlistId] = trackIds
341
+ }
342
+ }
343
+ } catch (e: Exception) {
344
+ e.printStackTrace()
345
+ }
346
+ return
347
+ }
348
+
349
+ // 2. Migrate from SharedPreferences (one-time, existing installs)
350
+ val prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, Context.MODE_PRIVATE)
351
+ var didMigrate = false
352
+
353
+ val tracksString = prefs.getString(LEGACY_KEY_DOWNLOADED_TRACKS, null)
354
+ if (tracksString != null) {
355
+ try {
321
356
  val tracksJson = JSONObject(tracksString)
322
357
  for (trackId in tracksJson.keys()) {
323
358
  val record = DownloadedTrackRecord.fromJson(tracksJson.getJSONObject(trackId))
324
359
  downloadedTracks[trackId] = record
325
360
  }
361
+ prefs.edit().remove(LEGACY_KEY_DOWNLOADED_TRACKS).apply()
362
+ didMigrate = true
363
+ } catch (e: Exception) {
364
+ e.printStackTrace()
326
365
  }
366
+ }
327
367
 
328
- // Load playlist associations
329
- val playlistString = prefs.getString(KEY_PLAYLIST_TRACKS, null)
330
- if (playlistString != null) {
368
+ val playlistString = prefs.getString(LEGACY_KEY_PLAYLIST_TRACKS, null)
369
+ if (playlistString != null) {
370
+ try {
331
371
  val playlistJson = JSONObject(playlistString)
332
372
  for (playlistId in playlistJson.keys()) {
333
373
  val trackIdsArray = playlistJson.getJSONArray(playlistId)
@@ -337,9 +377,15 @@ class DownloadDatabase private constructor(
337
377
  }
338
378
  playlistTracks[playlistId] = trackIds
339
379
  }
380
+ prefs.edit().remove(LEGACY_KEY_PLAYLIST_TRACKS).apply()
381
+ didMigrate = true
382
+ } catch (e: Exception) {
383
+ e.printStackTrace()
340
384
  }
341
- } catch (e: Exception) {
342
- e.printStackTrace()
385
+ }
386
+
387
+ if (didMigrate) {
388
+ saveToDisk()
343
389
  }
344
390
  }
345
391
 
@@ -395,12 +441,11 @@ class DownloadDatabase private constructor(
395
441
  }
396
442
 
397
443
  private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist {
398
- // PlaylistManager already uses TrackItem from generated code with proper Variant types
399
444
  return Playlist(
400
445
  id = playlist.id,
401
446
  name = playlist.name,
402
- description = null, // PlaylistManager doesn't have description in Nitro Playlist
403
- artwork = null, // PlaylistManager doesn't have artwork in Nitro Playlist
447
+ description = null,
448
+ artwork = null,
404
449
  tracks = playlist.tracks.toTypedArray(),
405
450
  )
406
451
  }
@@ -1,14 +1,13 @@
1
1
  package com.margelo.nitro.nitroplayer.playlist
2
2
 
3
3
  import android.content.Context
4
- import android.content.SharedPreferences
5
4
  import com.margelo.nitro.core.AnyMap
6
- import com.margelo.nitro.core.NullType
7
5
  import com.margelo.nitro.nitroplayer.QueueOperation
8
6
  import com.margelo.nitro.nitroplayer.TrackItem
9
7
  import com.margelo.nitro.nitroplayer.Variant_NullType_String
10
8
  import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
11
9
  import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
10
+ import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
12
11
  import org.json.JSONArray
13
12
  import org.json.JSONObject
14
13
  import java.util.UUID
@@ -26,11 +25,8 @@ class PlaylistManager private constructor(
26
25
  private val playlistListeners = mutableMapOf<String, CopyOnWriteArrayList<(Playlist, QueueOperation?) -> Unit>>()
27
26
  private var currentPlaylistId: String? = null
28
27
 
29
- private val sharedPreferences: SharedPreferences =
30
- context.getSharedPreferences("NitroPlayerPlaylists", Context.MODE_PRIVATE)
31
-
32
28
  private val saveHandler = android.os.Handler(android.os.Looper.getMainLooper())
33
- private val saveRunnable = Runnable { savePlaylistsToPreferences() }
29
+ private val saveRunnable = Runnable { saveToFile() }
34
30
 
35
31
  private fun scheduleSave() {
36
32
  saveHandler.removeCallbacks(saveRunnable)
@@ -42,6 +38,9 @@ class PlaylistManager private constructor(
42
38
  @Suppress("ktlint:standard:property-naming")
43
39
  private var INSTANCE: PlaylistManager? = null
44
40
 
41
+ // Legacy SharedPreferences name (migration only)
42
+ private const val LEGACY_PREFS_NAME = "NitroPlayerPlaylists"
43
+
45
44
  @JvmStatic
46
45
  fun getInstance(context: Context): PlaylistManager =
47
46
  INSTANCE ?: synchronized(this) {
@@ -50,7 +49,7 @@ class PlaylistManager private constructor(
50
49
  }
51
50
 
52
51
  init {
53
- // Don't load from preferences on init - only load when Android Auto needs it
52
+ // Don't load from file on init - only load when Android Auto needs it
54
53
  }
55
54
 
56
55
  /**
@@ -368,7 +367,9 @@ class PlaylistManager private constructor(
368
367
  playlistListeners[playlistId]?.forEach { it(playlist, operation) }
369
368
  }
370
369
 
371
- private fun savePlaylistsToPreferences() {
370
+ // MARK: - Persistence
371
+
372
+ private fun saveToFile() {
372
373
  try {
373
374
  val jsonArray = JSONArray()
374
375
  synchronized(playlists) {
@@ -390,7 +391,6 @@ class PlaylistManager private constructor(
390
391
  put("duration", track.duration)
391
392
  put("url", track.url)
392
393
  track.artwork?.let { put("artwork", it) }
393
- // Serialize extraPayload to JSON for persistence
394
394
  track.extraPayload?.let { payload ->
395
395
  val extraPayloadMap = payload.toHashMap()
396
396
  val extraPayloadJson = JSONObject(extraPayloadMap)
@@ -405,84 +405,111 @@ class PlaylistManager private constructor(
405
405
  }
406
406
  }
407
407
 
408
- sharedPreferences
409
- .edit()
410
- .putString("playlists", jsonArray.toString())
411
- .putString("currentPlaylistId", currentPlaylistId)
412
- .apply()
408
+ val wrapper = JSONObject().apply {
409
+ put("playlists", jsonArray)
410
+ put("currentPlaylistId", currentPlaylistId)
411
+ }
412
+ NitroPlayerStorage.write(context, "playlists.json", wrapper.toString())
413
413
  } catch (e: Exception) {
414
414
  e.printStackTrace()
415
415
  }
416
416
  }
417
417
 
418
- private fun loadPlaylistsFromPreferences() {
419
- try {
420
- val jsonString = sharedPreferences.getString("playlists", null)
421
- if (jsonString != null) {
422
- val jsonArray = JSONArray(jsonString)
423
- synchronized(playlists) {
424
- playlists.clear()
425
- for (i in 0 until jsonArray.length()) {
426
- val jsonObject = jsonArray.getJSONObject(i)
427
- val tracks = mutableListOf<TrackItem>()
428
- val tracksArray = jsonObject.getJSONArray("tracks")
429
- for (j in 0 until tracksArray.length()) {
430
- val trackObj = tracksArray.getJSONObject(j)
431
- val artworkStr = trackObj.optString("artwork")
432
- val artwork: Variant_NullType_String? =
433
- if (!artworkStr.isNullOrEmpty()) {
434
- Variant_NullType_String.create(artworkStr)
435
- } else {
436
- null
437
- }
438
- // Deserialize extraPayload from JSON
439
- val extraPayload: AnyMap? =
440
- if (trackObj.has("extraPayload")) {
441
- val extraPayloadJson = trackObj.getJSONObject("extraPayload")
442
- val map = AnyMap()
443
- val keyIterator = extraPayloadJson.keys()
444
- while (keyIterator.hasNext()) {
445
- val key = keyIterator.next()
446
- when (val value = extraPayloadJson.get(key)) {
447
- is String -> map.setString(key, value)
448
- is Number -> map.setDouble(key, value.toDouble())
449
- is Boolean -> map.setBoolean(key, value)
450
- }
451
- }
452
- map
453
- } else {
454
- null
418
+ fun loadPlaylistsFromFile() {
419
+ // 1. Try new JSON file (post-migration)
420
+ val json = NitroPlayerStorage.read(context, "playlists.json")
421
+ if (json != null) {
422
+ try {
423
+ val wrapper = JSONObject(json)
424
+ val jsonArray = wrapper.optJSONArray("playlists") ?: JSONArray()
425
+ parseAndLoadPlaylists(jsonArray)
426
+ currentPlaylistId = if (wrapper.isNull("currentPlaylistId")) null
427
+ else wrapper.optString("currentPlaylistId", null.toString()).takeIf { it != "null" }
428
+ } catch (e: Exception) {
429
+ e.printStackTrace()
430
+ }
431
+ return
432
+ }
433
+
434
+ // 2. Migrate from SharedPreferences (one-time, existing installs)
435
+ val prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, Context.MODE_PRIVATE)
436
+ val legacyJson = prefs.getString("playlists", null)
437
+ if (legacyJson != null) {
438
+ try {
439
+ val jsonArray = JSONArray(legacyJson)
440
+ parseAndLoadPlaylists(jsonArray)
441
+ currentPlaylistId = prefs.getString("currentPlaylistId", null)
442
+ // Remove old SharedPreferences data to free space
443
+ prefs.edit().remove("playlists").remove("currentPlaylistId").apply()
444
+ // Persist in new format
445
+ saveToFile()
446
+ } catch (e: Exception) {
447
+ e.printStackTrace()
448
+ }
449
+ return
450
+ }
451
+
452
+ // 3. Fresh install — nothing to load
453
+ }
454
+
455
+ private fun parseAndLoadPlaylists(jsonArray: JSONArray) {
456
+ synchronized(playlists) {
457
+ playlists.clear()
458
+ for (i in 0 until jsonArray.length()) {
459
+ val jsonObject = jsonArray.getJSONObject(i)
460
+ val tracks = mutableListOf<TrackItem>()
461
+ val tracksArray = jsonObject.getJSONArray("tracks")
462
+ for (j in 0 until tracksArray.length()) {
463
+ val trackObj = tracksArray.getJSONObject(j)
464
+ val artworkStr = trackObj.optString("artwork")
465
+ val artwork: Variant_NullType_String? =
466
+ if (!artworkStr.isNullOrEmpty()) {
467
+ Variant_NullType_String.create(artworkStr)
468
+ } else {
469
+ null
470
+ }
471
+ val extraPayload: AnyMap? =
472
+ if (trackObj.has("extraPayload")) {
473
+ val extraPayloadJson = trackObj.getJSONObject("extraPayload")
474
+ val map = AnyMap()
475
+ val keyIterator = extraPayloadJson.keys()
476
+ while (keyIterator.hasNext()) {
477
+ val key = keyIterator.next()
478
+ when (val value = extraPayloadJson.get(key)) {
479
+ is String -> map.setString(key, value)
480
+ is Number -> map.setDouble(key, value.toDouble())
481
+ is Boolean -> map.setBoolean(key, value)
455
482
  }
456
- tracks.add(
457
- TrackItem(
458
- id = trackObj.getString("id"),
459
- title = trackObj.getString("title"),
460
- artist = trackObj.getString("artist"),
461
- album = trackObj.getString("album"),
462
- duration = trackObj.getDouble("duration"),
463
- url = trackObj.getString("url"),
464
- artwork = artwork,
465
- extraPayload = extraPayload,
466
- ),
467
- )
483
+ }
484
+ map
485
+ } else {
486
+ null
468
487
  }
469
- val descriptionStr = jsonObject.optString("description")
470
- val artworkStr = jsonObject.optString("artwork")
471
- val playlist =
472
- Playlist(
473
- id = jsonObject.getString("id"),
474
- name = jsonObject.getString("name"),
475
- description = if (!descriptionStr.isNullOrEmpty()) descriptionStr else null,
476
- artwork = if (!artworkStr.isNullOrEmpty()) artworkStr else null,
477
- tracks = tracks,
478
- )
479
- playlists[playlist.id] = playlist
480
- }
488
+ tracks.add(
489
+ TrackItem(
490
+ id = trackObj.getString("id"),
491
+ title = trackObj.getString("title"),
492
+ artist = trackObj.getString("artist"),
493
+ album = trackObj.getString("album"),
494
+ duration = trackObj.getDouble("duration"),
495
+ url = trackObj.getString("url"),
496
+ artwork = artwork,
497
+ extraPayload = extraPayload,
498
+ ),
499
+ )
481
500
  }
482
- currentPlaylistId = sharedPreferences.getString("currentPlaylistId", null)
501
+ val descriptionStr = jsonObject.optString("description")
502
+ val artworkStr = jsonObject.optString("artwork")
503
+ val playlist =
504
+ Playlist(
505
+ id = jsonObject.getString("id"),
506
+ name = jsonObject.getString("name"),
507
+ description = if (!descriptionStr.isNullOrEmpty()) descriptionStr else null,
508
+ artwork = if (!artworkStr.isNullOrEmpty()) artworkStr else null,
509
+ tracks = tracks,
510
+ )
511
+ playlists[playlist.id] = playlist
483
512
  }
484
- } catch (e: Exception) {
485
- e.printStackTrace()
486
513
  }
487
514
  }
488
515
  }
@@ -0,0 +1,50 @@
1
+ package com.margelo.nitro.nitroplayer.storage
2
+
3
+ import android.content.Context
4
+ import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
5
+ import java.io.File
6
+
7
+ object NitroPlayerStorage {
8
+ private const val TAG = "NitroPlayerStorage"
9
+ private const val DIR_NAME = "nitroplayer"
10
+
11
+ /** Reads the contents of [filename] from the NitroPlayer storage directory, or null if absent. */
12
+ fun read(context: Context, filename: String): String? {
13
+ val file = File(storageDirectory(context), filename)
14
+ return if (file.exists()) {
15
+ try {
16
+ file.readText()
17
+ } catch (e: Exception) {
18
+ NitroPlayerLogger.log(TAG, "read($filename) failed: $e")
19
+ null
20
+ }
21
+ } else {
22
+ null
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Atomically writes [json] to [filename] in the NitroPlayer storage directory.
28
+ * Writes to `<filename>.tmp` first, then renames — leaving the prior file
29
+ * untouched on failure (crash-safe).
30
+ */
31
+ fun write(context: Context, filename: String, json: String) {
32
+ try {
33
+ val dir = storageDirectory(context)
34
+ dir.mkdirs()
35
+ val tmp = File(dir, "$filename.tmp")
36
+ val dest = File(dir, filename)
37
+ tmp.writeText(json)
38
+ if (!tmp.renameTo(dest)) {
39
+ // renameTo can fail across mount points; copy then delete as fallback
40
+ dest.writeText(tmp.readText())
41
+ tmp.delete()
42
+ }
43
+ } catch (e: Exception) {
44
+ NitroPlayerLogger.log(TAG, "write($filename) failed: $e")
45
+ }
46
+ }
47
+
48
+ /** Returns the NitroPlayer subdirectory inside filesDir. */
49
+ private fun storageDirectory(context: Context): File = File(context.filesDir, DIR_NAME)
50
+ }
@@ -8,17 +8,17 @@
8
8
  import Foundation
9
9
  import NitroModules
10
10
 
11
- /// Manages persistence of downloaded track metadata using UserDefaults
11
+ /// Manages persistence of downloaded track metadata using file storage
12
12
  final class DownloadDatabase {
13
13
 
14
14
  // MARK: - Singleton
15
15
 
16
16
  static let shared = DownloadDatabase()
17
17
 
18
- // MARK: - Constants
18
+ // MARK: - Legacy UserDefaults Keys (migration only)
19
19
 
20
- private static let downloadedTracksKey = "NitroPlayerDownloadedTracks"
21
- private static let playlistTracksKey = "NitroPlayerPlaylistTracks"
20
+ private static let legacyDownloadedTracksKey = "NitroPlayerDownloadedTracks"
21
+ private static let legacyPlaylistTracksKey = "NitroPlayerPlaylistTracks"
22
22
 
23
23
  // MARK: - Properties
24
24
 
@@ -353,12 +353,21 @@ final class DownloadDatabase {
353
353
  private func saveToDisk() {
354
354
  do {
355
355
  let tracksData = try JSONEncoder().encode(downloadedTracks)
356
- UserDefaults.standard.set(tracksData, forKey: Self.downloadedTracksKey)
357
-
358
356
  // Convert Set to Array for encoding
359
357
  let playlistTracksDict = playlistTracks.mapValues { Array($0) }
360
358
  let playlistData = try JSONEncoder().encode(playlistTracksDict)
361
- UserDefaults.standard.set(playlistData, forKey: Self.playlistTracksKey)
359
+
360
+ // Combine both into a single JSON wrapper object
361
+ guard let tracksJson = try JSONSerialization.jsonObject(with: tracksData) as? [String: Any],
362
+ let playlistJson = try JSONSerialization.jsonObject(with: playlistData) as? [String: Any]
363
+ else { return }
364
+
365
+ let wrapper: [String: Any] = [
366
+ "downloadedTracks": tracksJson,
367
+ "playlistTracks": playlistJson,
368
+ ]
369
+ let data = try JSONSerialization.data(withJSONObject: wrapper, options: [])
370
+ try NitroPlayerStorage.write(filename: "downloads.json", data: data)
362
371
  } catch {
363
372
  NitroPlayerLogger.log("DownloadDatabase", "Failed to save to disk: \(error)")
364
373
  }
@@ -369,16 +378,42 @@ final class DownloadDatabase {
369
378
  NitroPlayerLogger.log("DownloadDatabase", "📀 LOADING FROM DISK")
370
379
  NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40))
371
380
 
372
- // Load synchronously to ensure data is available immediately
373
- // Load downloaded tracks
374
- if let tracksData = UserDefaults.standard.data(forKey: Self.downloadedTracksKey) {
381
+ // 1. Try new JSON file (post-migration)
382
+ if let data = NitroPlayerStorage.read(filename: "downloads.json") {
383
+ do {
384
+ if let wrapper = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
385
+ if let tracksObj = wrapper["downloadedTracks"] as? [String: Any] {
386
+ let tracksData = try JSONSerialization.data(withJSONObject: tracksObj)
387
+ self.downloadedTracks = try JSONDecoder().decode(
388
+ [String: DownloadedTrackRecord].self, from: tracksData)
389
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.downloadedTracks.count) tracks from file")
390
+ }
391
+ if let playlistObj = wrapper["playlistTracks"] as? [String: Any] {
392
+ let playlistData = try JSONSerialization.data(withJSONObject: playlistObj)
393
+ let playlistTracksDict = try JSONDecoder().decode(
394
+ [String: [String]].self, from: playlistData)
395
+ self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
396
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.playlistTracks.count) playlist associations from file")
397
+ }
398
+ }
399
+ } catch {
400
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load from file: \(error)")
401
+ }
402
+ NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
403
+ return
404
+ }
405
+
406
+ // 2. Migrate from UserDefaults (one-time, existing installs)
407
+ var didMigrate = false
408
+
409
+ if let tracksData = UserDefaults.standard.data(forKey: Self.legacyDownloadedTracksKey) {
375
410
  do {
376
411
  self.downloadedTracks = try JSONDecoder().decode(
377
412
  [String: DownloadedTrackRecord].self, from: tracksData)
378
- NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.downloadedTracks.count) tracks from disk")
413
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated \(self.downloadedTracks.count) tracks from UserDefaults")
379
414
 
380
- // Migrate absolute paths → filenames (one-time, for existing installs)
381
- var needsMigration = false
415
+ // Migrate absolute paths → filenames (pre-existing migration)
416
+ var needsPathMigration = false
382
417
  for (trackId, record) in self.downloadedTracks {
383
418
  if record.localPath.contains("/") {
384
419
  let filename = URL(fileURLWithPath: record.localPath).lastPathComponent
@@ -391,41 +426,40 @@ final class DownloadDatabase {
391
426
  fileSize: record.fileSize,
392
427
  storageLocation: record.storageLocation
393
428
  )
394
- needsMigration = true
429
+ needsPathMigration = true
395
430
  }
396
431
  }
397
- if needsMigration { self.saveToDisk() }
398
-
399
- // Log each downloaded track
400
- for (trackId, record) in self.downloadedTracks {
401
- NitroPlayerLogger.log("DownloadDatabase", " 📥 \(trackId)")
402
- NitroPlayerLogger.log("DownloadDatabase", " Title: \(record.originalTrack.title)")
403
- NitroPlayerLogger.log("DownloadDatabase", " Path (filename): \(record.localPath)")
432
+ if needsPathMigration {
433
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated absolute paths to filenames")
404
434
  }
435
+
436
+ UserDefaults.standard.removeObject(forKey: Self.legacyDownloadedTracksKey)
437
+ didMigrate = true
405
438
  } catch {
406
- NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load tracks from disk: \(error)")
439
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to migrate tracks from UserDefaults: \(error)")
407
440
  }
408
441
  } else {
409
442
  NitroPlayerLogger.log("DownloadDatabase", "⚠️ No saved tracks found in UserDefaults")
410
443
  }
411
444
 
412
- // Load playlist associations
413
- if let playlistData = UserDefaults.standard.data(forKey: Self.playlistTracksKey) {
445
+ if let playlistData = UserDefaults.standard.data(forKey: Self.legacyPlaylistTracksKey) {
414
446
  do {
415
447
  let playlistTracksDict = try JSONDecoder().decode(
416
448
  [String: [String]].self, from: playlistData)
417
449
  self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
418
- NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.playlistTracks.count) playlist associations from disk")
419
-
420
- // Log playlist associations
421
- for (playlistId, trackIds) in self.playlistTracks {
422
- NitroPlayerLogger.log("DownloadDatabase", " 📋 Playlist \(playlistId): \(trackIds.count) tracks")
423
- }
450
+ NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated \(self.playlistTracks.count) playlist associations from UserDefaults")
451
+ UserDefaults.standard.removeObject(forKey: Self.legacyPlaylistTracksKey)
452
+ didMigrate = true
424
453
  } catch {
425
- NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load playlist tracks from disk: \(error)")
454
+ NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to migrate playlist tracks from UserDefaults: \(error)")
426
455
  }
427
456
  } else {
428
- NitroPlayerLogger.log("DownloadDatabase", "⚠️ No playlist associations found")
457
+ NitroPlayerLogger.log("DownloadDatabase", "⚠️ No playlist associations found in UserDefaults")
458
+ }
459
+
460
+ if didMigrate {
461
+ // Persist migrated data in new file format
462
+ saveToDisk()
429
463
  }
430
464
 
431
465
  NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
@@ -18,8 +18,9 @@ final class DownloadManagerCore: NSObject {
18
18
  // MARK: - Constants
19
19
 
20
20
  private static let backgroundSessionIdentifier = "com.nitroplayer.backgroundDownloads"
21
- private static let trackMetadataKey = "NitroPlayerTrackMetadata"
22
- private static let playlistAssociationsKey = "NitroPlayerPlaylistAssociations"
21
+ // Legacy UserDefaults keys (migration only)
22
+ private static let legacyTrackMetadataKey = "NitroPlayerTrackMetadata"
23
+ private static let legacyPlaylistAssociationsKey = "NitroPlayerPlaylistAssociations"
23
24
 
24
25
  // MARK: - Properties
25
26
 
@@ -507,48 +508,87 @@ final class DownloadManagerCore: NSObject {
507
508
  private func loadPersistedMetadata() {
508
509
  NitroPlayerLogger.log("DownloadManagerCore", "📦 Loading persisted metadata...")
509
510
 
510
- // Load track metadata
511
- if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
511
+ // 1. Try new JSON file (post-migration)
512
+ if let data = NitroPlayerStorage.read(filename: "download_metadata.json") {
513
+ do {
514
+ if let wrapper = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
515
+ if let tracksObj = wrapper["trackMetadata"] as? [String: Any] {
516
+ let tracksData = try JSONSerialization.data(withJSONObject: tracksObj)
517
+ let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: tracksData)
518
+ for (trackId, record) in records {
519
+ trackMetadata[trackId] = recordToTrackItem(record)
520
+ }
521
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(trackMetadata.count) track metadata entries from file")
522
+ }
523
+ if let assocObj = wrapper["playlistAssociations"] as? [String: String] {
524
+ playlistAssociations = assocObj
525
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(playlistAssociations.count) playlist associations from file")
526
+ }
527
+ }
528
+ } catch {
529
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load metadata from file: \(error)")
530
+ }
531
+ return
532
+ }
533
+
534
+ // 2. Migrate from UserDefaults (one-time, existing installs)
535
+ var didMigrate = false
536
+
537
+ if let data = UserDefaults.standard.data(forKey: Self.legacyTrackMetadataKey) {
512
538
  do {
513
539
  let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: data)
514
540
  for (trackId, record) in records {
515
541
  trackMetadata[trackId] = recordToTrackItem(record)
516
542
  }
517
- NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(trackMetadata.count) track metadata entries")
543
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Migrated \(trackMetadata.count) track metadata entries from UserDefaults")
544
+ UserDefaults.standard.removeObject(forKey: Self.legacyTrackMetadataKey)
545
+ didMigrate = true
518
546
  } catch {
519
- NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load track metadata: \(error)")
547
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to migrate track metadata: \(error)")
520
548
  }
521
549
  } else {
522
550
  NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted track metadata found")
523
551
  }
524
552
 
525
- // Load playlist associations
526
- if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
553
+ if let data = UserDefaults.standard.data(forKey: Self.legacyPlaylistAssociationsKey) {
527
554
  do {
528
555
  playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
529
- NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(playlistAssociations.count) playlist associations")
556
+ NitroPlayerLogger.log("DownloadManagerCore", " ✅ Migrated \(playlistAssociations.count) playlist associations from UserDefaults")
557
+ UserDefaults.standard.removeObject(forKey: Self.legacyPlaylistAssociationsKey)
558
+ didMigrate = true
530
559
  } catch {
531
- NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load playlist associations: \(error)")
560
+ NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to migrate playlist associations: \(error)")
532
561
  }
533
562
  } else {
534
563
  NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted playlist associations found")
535
564
  }
565
+
566
+ if didMigrate {
567
+ savePersistedMetadata()
568
+ }
536
569
  }
537
570
 
538
571
  /// Persist track metadata and playlist associations to disk
539
572
  private func savePersistedMetadata() {
540
- // Convert TrackItem to TrackItemRecord for encoding
541
573
  var records: [String: TrackItemRecord] = [:]
542
574
  for (trackId, track) in trackMetadata {
543
575
  records[trackId] = trackItemToRecord(track)
544
576
  }
545
577
 
546
578
  do {
547
- let trackData = try JSONEncoder().encode(records)
548
- UserDefaults.standard.set(trackData, forKey: Self.trackMetadataKey)
549
-
579
+ let tracksData = try JSONEncoder().encode(records)
550
580
  let playlistData = try JSONEncoder().encode(playlistAssociations)
551
- UserDefaults.standard.set(playlistData, forKey: Self.playlistAssociationsKey)
581
+
582
+ guard let tracksJson = try JSONSerialization.jsonObject(with: tracksData) as? [String: Any],
583
+ let assocJson = try JSONSerialization.jsonObject(with: playlistData) as? [String: Any]
584
+ else { return }
585
+
586
+ let wrapper: [String: Any] = [
587
+ "trackMetadata": tracksJson,
588
+ "playlistAssociations": assocJson,
589
+ ]
590
+ let data = try JSONSerialization.data(withJSONObject: wrapper, options: [])
591
+ try NitroPlayerStorage.write(filename: "download_metadata.json", data: data)
552
592
  } catch {
553
593
  NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save metadata: \(error)")
554
594
  }
@@ -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.5",
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",