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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +72 -27
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +105 -78
- package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +50 -0
- package/ios/download/DownloadDatabase.swift +66 -32
- package/ios/download/DownloadManagerCore.swift +55 -15
- package/ios/playlist/PlaylistManager.swift +97 -76
- package/ios/storage/NitroPlayerStorage.swift +44 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
23
|
-
private const val
|
|
24
|
-
private const val
|
|
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(
|
|
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(
|
|
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(
|
|
285
|
+
NitroPlayerLogger.log(TAG, "Cleaned up ${trackIdsToRemove.size} orphaned records")
|
|
287
286
|
} else {
|
|
288
|
-
NitroPlayerLogger.log(
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
}
|
|
342
|
-
|
|
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,
|
|
403
|
-
artwork = null,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
|
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: -
|
|
18
|
+
// MARK: - Legacy UserDefaults Keys (migration only)
|
|
19
19
|
|
|
20
|
-
private static let
|
|
21
|
-
private static let
|
|
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
|
-
|
|
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
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
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", "✅
|
|
413
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated \(self.downloadedTracks.count) tracks from UserDefaults")
|
|
379
414
|
|
|
380
|
-
// Migrate absolute paths → filenames (
|
|
381
|
-
var
|
|
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
|
-
|
|
429
|
+
needsPathMigration = true
|
|
395
430
|
}
|
|
396
431
|
}
|
|
397
|
-
if
|
|
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
|
|
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
|
-
|
|
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", "✅
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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
|
-
|
|
22
|
-
private static let
|
|
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
|
-
//
|
|
511
|
-
if let data =
|
|
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", " ✅
|
|
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
|
|
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
|
-
|
|
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", " ✅
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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?.
|
|
355
|
+
let work = DispatchWorkItem { [weak self] in self?.saveToFile() }
|
|
356
356
|
saveDebounceWorkItem = work
|
|
357
|
-
// Use global background queue —
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
407
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
462
|
+
return nil
|
|
421
463
|
}
|
|
422
464
|
|
|
423
|
-
let
|
|
424
|
-
let artwork =
|
|
425
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
486
|
+
return TrackItem(
|
|
473
487
|
id: id,
|
|
474
|
-
|
|
475
|
-
|
|
488
|
+
title: title,
|
|
489
|
+
artist: artist,
|
|
490
|
+
album: album,
|
|
491
|
+
duration: duration,
|
|
492
|
+
url: url,
|
|
476
493
|
artwork: artwork,
|
|
477
|
-
|
|
494
|
+
extraPayload: extraPayload
|
|
478
495
|
)
|
|
479
496
|
}
|
|
480
|
-
}
|
|
481
497
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
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",
|