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.
@@ -12,7 +12,10 @@ object NitroPlayerLogger {
12
12
  * Use trailing lambda syntax: NitroPlayerLogger.log("Tag") { "msg $value" }
13
13
  * The lambda is inlined (no heap allocation) and skipped entirely when disabled.
14
14
  */
15
- inline fun log(header: String = "NitroPlayer", message: () -> String) {
15
+ inline fun log(
16
+ header: String = "NitroPlayer",
17
+ message: () -> String,
18
+ ) {
16
19
  if (isEnabled) {
17
20
  Log.d(header, message())
18
21
  }
@@ -23,7 +26,10 @@ object NitroPlayerLogger {
23
26
  * Note: the String is evaluated at the call site before this function runs.
24
27
  * Migrate to the lambda overload for hot paths.
25
28
  */
26
- fun log(header: String = "NitroPlayer", message: String) {
29
+ fun log(
30
+ header: String = "NitroPlayer",
31
+ message: String,
32
+ ) {
27
33
  if (isEnabled) {
28
34
  Log.d(header, message)
29
35
  }
@@ -698,10 +698,11 @@ class TrackPlayerCore private constructor(
698
698
  currentRepeatMode = mode
699
699
  if (::player.isInitialized) {
700
700
  handler.post {
701
- player.repeatMode = when (mode) {
702
- RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
703
- else -> Player.REPEAT_MODE_OFF
704
- }
701
+ player.repeatMode =
702
+ when (mode) {
703
+ RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
704
+ else -> Player.REPEAT_MODE_OFF
705
+ }
705
706
  }
706
707
  }
707
708
  NitroPlayerLogger.log("TrackPlayerCore", "🔁 setRepeatMode: $mode")
@@ -1,27 +1,29 @@
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
4
+ import com.margelo.nitro.core.AnyMap
6
5
  import com.margelo.nitro.core.NullType
7
6
  import com.margelo.nitro.nitroplayer.*
8
7
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
9
8
  import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
9
+ import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
10
10
  import org.json.JSONArray
11
11
  import org.json.JSONObject
12
12
  import java.io.File
13
13
 
14
14
  /**
15
- * Manages persistence of downloaded track metadata using SharedPreferences
15
+ * Manages persistence of downloaded track metadata using file storage
16
16
  */
17
17
  class DownloadDatabase private constructor(
18
18
  private val context: Context,
19
19
  ) {
20
20
  companion object {
21
21
  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"
22
+
23
+ // Legacy SharedPreferences keys (migration only)
24
+ private const val LEGACY_PREFS_NAME = "NitroPlayerDownloads"
25
+ private const val LEGACY_KEY_DOWNLOADED_TRACKS = "downloaded_tracks"
26
+ private const val LEGACY_KEY_PLAYLIST_TRACKS = "playlist_tracks"
25
27
 
26
28
  @Volatile
27
29
  private var instance: DownloadDatabase? = null
@@ -32,7 +34,6 @@ class DownloadDatabase private constructor(
32
34
  }
33
35
  }
34
36
 
35
- private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
36
37
  private val downloadedTracks = mutableMapOf<String, DownloadedTrackRecord>()
37
38
  private val playlistTracks = mutableMapOf<String, MutableSet<String>>()
38
39
  private val fileManager = DownloadFileManager.getInstance(context)
@@ -254,13 +255,13 @@ class DownloadDatabase private constructor(
254
255
  /** Validates all downloads and removes records for missing files */
255
256
  fun syncDownloads(): Int {
256
257
  synchronized(this) {
257
- NitroPlayerLogger.log("DownloadDatabase", "syncDownloads called")
258
+ NitroPlayerLogger.log(TAG, "syncDownloads called")
258
259
 
259
260
  val trackIdsToRemove = mutableListOf<String>()
260
261
 
261
262
  for ((trackId, record) in downloadedTracks) {
262
263
  if (!File(record.localPath).exists()) {
263
- NitroPlayerLogger.log("DownloadDatabase", "Missing file for track $trackId: ${record.localPath}")
264
+ NitroPlayerLogger.log(TAG, "Missing file for track $trackId: ${record.localPath}")
264
265
  trackIdsToRemove.add(trackId)
265
266
  }
266
267
  }
@@ -283,9 +284,9 @@ class DownloadDatabase private constructor(
283
284
 
284
285
  if (trackIdsToRemove.isNotEmpty()) {
285
286
  saveToDisk()
286
- NitroPlayerLogger.log("DownloadDatabase", "Cleaned up ${trackIdsToRemove.size} orphaned records")
287
+ NitroPlayerLogger.log(TAG, "Cleaned up ${trackIdsToRemove.size} orphaned records")
287
288
  } else {
288
- NitroPlayerLogger.log("DownloadDatabase", "All downloads are valid")
289
+ NitroPlayerLogger.log(TAG, "All downloads are valid")
289
290
  }
290
291
 
291
292
  return trackIdsToRemove.size
@@ -295,39 +296,81 @@ class DownloadDatabase private constructor(
295
296
  // Persistence
296
297
  private fun saveToDisk() {
297
298
  try {
298
- // Save downloaded tracks
299
299
  val tracksJson = JSONObject()
300
300
  for ((trackId, record) in downloadedTracks) {
301
301
  tracksJson.put(trackId, record.toJson())
302
302
  }
303
- prefs.edit().putString(KEY_DOWNLOADED_TRACKS, tracksJson.toString()).apply()
304
303
 
305
- // Save playlist associations
306
304
  val playlistJson = JSONObject()
307
305
  for ((playlistId, trackIds) in playlistTracks) {
308
306
  playlistJson.put(playlistId, JSONArray(trackIds.toList()))
309
307
  }
310
- prefs.edit().putString(KEY_PLAYLIST_TRACKS, playlistJson.toString()).apply()
308
+
309
+ val wrapper =
310
+ JSONObject().apply {
311
+ put("downloadedTracks", tracksJson)
312
+ put("playlistTracks", playlistJson)
313
+ }
314
+ NitroPlayerStorage.write(context, "downloads.json", wrapper.toString())
311
315
  } catch (e: Exception) {
312
316
  e.printStackTrace()
313
317
  }
314
318
  }
315
319
 
316
320
  private fun loadFromDisk() {
317
- try {
318
- // Load downloaded tracks
319
- val tracksString = prefs.getString(KEY_DOWNLOADED_TRACKS, null)
320
- if (tracksString != null) {
321
+ // 1. Try new JSON file (post-migration)
322
+ val json = NitroPlayerStorage.read(context, "downloads.json")
323
+ if (json != null) {
324
+ try {
325
+ val wrapper = JSONObject(json)
326
+
327
+ val tracksJson = wrapper.optJSONObject("downloadedTracks")
328
+ if (tracksJson != null) {
329
+ for (trackId in tracksJson.keys()) {
330
+ val record = DownloadedTrackRecord.fromJson(tracksJson.getJSONObject(trackId))
331
+ downloadedTracks[trackId] = record
332
+ }
333
+ }
334
+
335
+ val playlistJson = wrapper.optJSONObject("playlistTracks")
336
+ if (playlistJson != null) {
337
+ for (playlistId in playlistJson.keys()) {
338
+ val trackIdsArray = playlistJson.getJSONArray(playlistId)
339
+ val trackIds = mutableSetOf<String>()
340
+ for (i in 0 until trackIdsArray.length()) {
341
+ trackIds.add(trackIdsArray.getString(i))
342
+ }
343
+ playlistTracks[playlistId] = trackIds
344
+ }
345
+ }
346
+ } catch (e: Exception) {
347
+ e.printStackTrace()
348
+ }
349
+ return
350
+ }
351
+
352
+ // 2. Migrate from SharedPreferences (one-time, existing installs)
353
+ val prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, Context.MODE_PRIVATE)
354
+ var didMigrate = false
355
+
356
+ val tracksString = prefs.getString(LEGACY_KEY_DOWNLOADED_TRACKS, null)
357
+ if (tracksString != null) {
358
+ try {
321
359
  val tracksJson = JSONObject(tracksString)
322
360
  for (trackId in tracksJson.keys()) {
323
361
  val record = DownloadedTrackRecord.fromJson(tracksJson.getJSONObject(trackId))
324
362
  downloadedTracks[trackId] = record
325
363
  }
364
+ prefs.edit().remove(LEGACY_KEY_DOWNLOADED_TRACKS).apply()
365
+ didMigrate = true
366
+ } catch (e: Exception) {
367
+ e.printStackTrace()
326
368
  }
369
+ }
327
370
 
328
- // Load playlist associations
329
- val playlistString = prefs.getString(KEY_PLAYLIST_TRACKS, null)
330
- if (playlistString != null) {
371
+ val playlistString = prefs.getString(LEGACY_KEY_PLAYLIST_TRACKS, null)
372
+ if (playlistString != null) {
373
+ try {
331
374
  val playlistJson = JSONObject(playlistString)
332
375
  for (playlistId in playlistJson.keys()) {
333
376
  val trackIdsArray = playlistJson.getJSONArray(playlistId)
@@ -337,15 +380,27 @@ class DownloadDatabase private constructor(
337
380
  }
338
381
  playlistTracks[playlistId] = trackIds
339
382
  }
383
+ prefs.edit().remove(LEGACY_KEY_PLAYLIST_TRACKS).apply()
384
+ didMigrate = true
385
+ } catch (e: Exception) {
386
+ e.printStackTrace()
340
387
  }
341
- } catch (e: Exception) {
342
- e.printStackTrace()
388
+ }
389
+
390
+ if (didMigrate) {
391
+ saveToDisk()
343
392
  }
344
393
  }
345
394
 
346
395
  // Conversion Helpers
347
- private fun trackItemToRecord(track: TrackItem): TrackItemRecord =
348
- TrackItemRecord(
396
+ private fun trackItemToRecord(track: TrackItem): TrackItemRecord {
397
+ val extraPayloadJson =
398
+ track.extraPayload?.let { payload ->
399
+ val extraPayloadMap = payload.toHashMap()
400
+ JSONObject(extraPayloadMap)
401
+ }
402
+
403
+ return TrackItemRecord(
349
404
  id = track.id,
350
405
  title = track.title,
351
406
  artist = track.artist,
@@ -353,7 +408,9 @@ class DownloadDatabase private constructor(
353
408
  duration = track.duration,
354
409
  url = track.url,
355
410
  artwork = track.artwork?.asSecondOrNull(),
411
+ extraPayload = extraPayloadJson,
356
412
  )
413
+ }
357
414
 
358
415
  private fun recordToTrackItem(record: TrackItemRecord): TrackItem {
359
416
  val artworkVariant =
@@ -363,6 +420,21 @@ class DownloadDatabase private constructor(
363
420
  null
364
421
  }
365
422
 
423
+ val extraPayload: AnyMap? =
424
+ record.extraPayload?.let { extraPayloadJson ->
425
+ val map = AnyMap()
426
+ val keyIterator = extraPayloadJson.keys()
427
+ while (keyIterator.hasNext()) {
428
+ val key = keyIterator.next()
429
+ when (val value = extraPayloadJson.get(key)) {
430
+ is String -> map.setString(key, value)
431
+ is Number -> map.setDouble(key, value.toDouble())
432
+ is Boolean -> map.setBoolean(key, value)
433
+ }
434
+ }
435
+ map
436
+ }
437
+
366
438
  return TrackItem(
367
439
  id = record.id,
368
440
  title = record.title,
@@ -371,7 +443,7 @@ class DownloadDatabase private constructor(
371
443
  duration = record.duration,
372
444
  url = record.url,
373
445
  artwork = artworkVariant,
374
- extraPayload = null,
446
+ extraPayload = extraPayload,
375
447
  )
376
448
  }
377
449
 
@@ -394,16 +466,14 @@ class DownloadDatabase private constructor(
394
466
  )
395
467
  }
396
468
 
397
- private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist {
398
- // PlaylistManager already uses TrackItem from generated code with proper Variant types
399
- return Playlist(
469
+ private fun convertPlaylistManagerToNitro(playlist: com.margelo.nitro.nitroplayer.playlist.Playlist): Playlist =
470
+ Playlist(
400
471
  id = playlist.id,
401
472
  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
473
+ description = null,
474
+ artwork = null,
404
475
  tracks = playlist.tracks.toTypedArray(),
405
476
  )
406
- }
407
477
  }
408
478
 
409
479
  // Internal record classes
@@ -449,6 +519,7 @@ internal data class TrackItemRecord(
449
519
  val duration: Double,
450
520
  val url: String,
451
521
  val artwork: String?,
522
+ val extraPayload: JSONObject?,
452
523
  ) {
453
524
  fun toJson(): JSONObject =
454
525
  JSONObject().apply {
@@ -459,6 +530,7 @@ internal data class TrackItemRecord(
459
530
  put("duration", duration)
460
531
  put("url", url)
461
532
  put("artwork", artwork)
533
+ put("extraPayload", extraPayload)
462
534
  }
463
535
 
464
536
  companion object {
@@ -471,6 +543,12 @@ internal data class TrackItemRecord(
471
543
  duration = json.getDouble("duration"),
472
544
  url = json.getString("url"),
473
545
  artwork = if (json.isNull("artwork")) null else json.getString("artwork"),
546
+ extraPayload =
547
+ if (json.has("extraPayload") && !json.isNull("extraPayload")) {
548
+ json.getJSONObject("extraPayload")
549
+ } else {
550
+ null
551
+ },
474
552
  )
475
553
  }
476
554
  }
@@ -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,120 @@ 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 =
409
+ JSONObject().apply {
410
+ put("playlists", jsonArray)
411
+ put("currentPlaylistId", currentPlaylistId)
412
+ }
413
+ NitroPlayerStorage.write(context, "playlists.json", wrapper.toString())
413
414
  } catch (e: Exception) {
414
415
  e.printStackTrace()
415
416
  }
416
417
  }
417
418
 
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
419
+ fun loadPlaylistsFromFile() {
420
+ // 1. Try new JSON file (post-migration)
421
+ val json = NitroPlayerStorage.read(context, "playlists.json")
422
+ if (json != null) {
423
+ try {
424
+ val wrapper = JSONObject(json)
425
+ val jsonArray = wrapper.optJSONArray("playlists") ?: JSONArray()
426
+ parseAndLoadPlaylists(jsonArray)
427
+ currentPlaylistId =
428
+ if (wrapper.isNull("currentPlaylistId")) {
429
+ null
430
+ } else {
431
+ wrapper.optString("currentPlaylistId", null.toString()).takeIf { it != "null" }
432
+ }
433
+ } catch (e: Exception) {
434
+ e.printStackTrace()
435
+ }
436
+ return
437
+ }
438
+
439
+ // 2. Migrate from SharedPreferences (one-time, existing installs)
440
+ val prefs = context.getSharedPreferences(LEGACY_PREFS_NAME, Context.MODE_PRIVATE)
441
+ val legacyJson = prefs.getString("playlists", null)
442
+ if (legacyJson != null) {
443
+ try {
444
+ val jsonArray = JSONArray(legacyJson)
445
+ parseAndLoadPlaylists(jsonArray)
446
+ currentPlaylistId = prefs.getString("currentPlaylistId", null)
447
+ // Remove old SharedPreferences data to free space
448
+ prefs
449
+ .edit()
450
+ .remove("playlists")
451
+ .remove("currentPlaylistId")
452
+ .apply()
453
+ // Persist in new format
454
+ saveToFile()
455
+ } catch (e: Exception) {
456
+ e.printStackTrace()
457
+ }
458
+ return
459
+ }
460
+
461
+ // 3. Fresh install — nothing to load
462
+ }
463
+
464
+ private fun parseAndLoadPlaylists(jsonArray: JSONArray) {
465
+ synchronized(playlists) {
466
+ playlists.clear()
467
+ for (i in 0 until jsonArray.length()) {
468
+ val jsonObject = jsonArray.getJSONObject(i)
469
+ val tracks = mutableListOf<TrackItem>()
470
+ val tracksArray = jsonObject.getJSONArray("tracks")
471
+ for (j in 0 until tracksArray.length()) {
472
+ val trackObj = tracksArray.getJSONObject(j)
473
+ val artworkStr = trackObj.optString("artwork")
474
+ val artwork: Variant_NullType_String? =
475
+ if (!artworkStr.isNullOrEmpty()) {
476
+ Variant_NullType_String.create(artworkStr)
477
+ } else {
478
+ null
479
+ }
480
+ val extraPayload: AnyMap? =
481
+ if (trackObj.has("extraPayload")) {
482
+ val extraPayloadJson = trackObj.getJSONObject("extraPayload")
483
+ val map = AnyMap()
484
+ val keyIterator = extraPayloadJson.keys()
485
+ while (keyIterator.hasNext()) {
486
+ val key = keyIterator.next()
487
+ when (val value = extraPayloadJson.get(key)) {
488
+ is String -> map.setString(key, value)
489
+ is Number -> map.setDouble(key, value.toDouble())
490
+ is Boolean -> map.setBoolean(key, value)
455
491
  }
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
- )
492
+ }
493
+ map
494
+ } else {
495
+ null
468
496
  }
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
- }
497
+ tracks.add(
498
+ TrackItem(
499
+ id = trackObj.getString("id"),
500
+ title = trackObj.getString("title"),
501
+ artist = trackObj.getString("artist"),
502
+ album = trackObj.getString("album"),
503
+ duration = trackObj.getDouble("duration"),
504
+ url = trackObj.getString("url"),
505
+ artwork = artwork,
506
+ extraPayload = extraPayload,
507
+ ),
508
+ )
481
509
  }
482
- currentPlaylistId = sharedPreferences.getString("currentPlaylistId", null)
510
+ val descriptionStr = jsonObject.optString("description")
511
+ val artworkStr = jsonObject.optString("artwork")
512
+ val playlist =
513
+ Playlist(
514
+ id = jsonObject.getString("id"),
515
+ name = jsonObject.getString("name"),
516
+ description = if (!descriptionStr.isNullOrEmpty()) descriptionStr else null,
517
+ artwork = if (!artworkStr.isNullOrEmpty()) artworkStr else null,
518
+ tracks = tracks,
519
+ )
520
+ playlists[playlist.id] = playlist
483
521
  }
484
- } catch (e: Exception) {
485
- e.printStackTrace()
486
522
  }
487
523
  }
488
524
  }
@@ -0,0 +1,57 @@
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(
13
+ context: Context,
14
+ filename: String,
15
+ ): String? {
16
+ val file = File(storageDirectory(context), filename)
17
+ return if (file.exists()) {
18
+ try {
19
+ file.readText()
20
+ } catch (e: Exception) {
21
+ NitroPlayerLogger.log(TAG, "read($filename) failed: $e")
22
+ null
23
+ }
24
+ } else {
25
+ null
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Atomically writes [json] to [filename] in the NitroPlayer storage directory.
31
+ * Writes to `<filename>.tmp` first, then renames — leaving the prior file
32
+ * untouched on failure (crash-safe).
33
+ */
34
+ fun write(
35
+ context: Context,
36
+ filename: String,
37
+ json: String,
38
+ ) {
39
+ try {
40
+ val dir = storageDirectory(context)
41
+ dir.mkdirs()
42
+ val tmp = File(dir, "$filename.tmp")
43
+ val dest = File(dir, filename)
44
+ tmp.writeText(json)
45
+ if (!tmp.renameTo(dest)) {
46
+ // renameTo can fail across mount points; copy then delete as fallback
47
+ dest.writeText(tmp.readText())
48
+ tmp.delete()
49
+ }
50
+ } catch (e: Exception) {
51
+ NitroPlayerLogger.log(TAG, "write($filename) failed: $e")
52
+ }
53
+ }
54
+
55
+ /** Returns the NitroPlayer subdirectory inside filesDir. */
56
+ private fun storageDirectory(context: Context): File = File(context.filesDir, DIR_NAME)
57
+ }