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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +8 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +111 -33
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +114 -78
- package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +57 -0
- package/ios/download/DownloadDatabase.swift +145 -34
- package/ios/download/DownloadManagerCore.swift +136 -17
- package/ios/playlist/PlaylistManager.swift +97 -76
- package/ios/storage/NitroPlayerStorage.swift +44 -0
- package/package.json +1 -1
|
@@ -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(
|
|
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(
|
|
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 =
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
private const val
|
|
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(
|
|
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(
|
|
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(
|
|
287
|
+
NitroPlayerLogger.log(TAG, "Cleaned up ${trackIdsToRemove.size} orphaned records")
|
|
287
288
|
} else {
|
|
288
|
-
NitroPlayerLogger.log(
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
}
|
|
342
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
403
|
-
artwork = null,
|
|
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 {
|
|
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,120 @@ class PlaylistManager private constructor(
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
492
|
+
}
|
|
493
|
+
map
|
|
494
|
+
} else {
|
|
495
|
+
null
|
|
468
496
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
+
}
|