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
|
@@ -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")
|
|
@@ -456,6 +490,11 @@ final class DownloadDatabase {
|
|
|
456
490
|
}
|
|
457
491
|
|
|
458
492
|
private func trackItemToRecord(_ track: TrackItem) -> TrackItemRecord {
|
|
493
|
+
var extraPayloadDict: [String: Any]? = nil
|
|
494
|
+
if let extraPayload = track.extraPayload {
|
|
495
|
+
extraPayloadDict = extraPayload.toDictionary()
|
|
496
|
+
}
|
|
497
|
+
|
|
459
498
|
return TrackItemRecord(
|
|
460
499
|
id: track.id,
|
|
461
500
|
title: track.title,
|
|
@@ -463,11 +502,28 @@ final class DownloadDatabase {
|
|
|
463
502
|
album: track.album,
|
|
464
503
|
duration: track.duration,
|
|
465
504
|
url: track.url,
|
|
466
|
-
artwork: variantToString(track.artwork)
|
|
505
|
+
artwork: variantToString(track.artwork),
|
|
506
|
+
extraPayload: extraPayloadDict
|
|
467
507
|
)
|
|
468
508
|
}
|
|
469
509
|
|
|
470
510
|
private func recordToTrackItem(_ record: TrackItemRecord) -> TrackItem {
|
|
511
|
+
var extraPayload: AnyMap? = nil
|
|
512
|
+
if let extraPayloadDict = record.extraPayload {
|
|
513
|
+
extraPayload = AnyMap()
|
|
514
|
+
for (key, value) in extraPayloadDict {
|
|
515
|
+
if let stringValue = value as? String {
|
|
516
|
+
extraPayload?.setString(key: key, value: stringValue)
|
|
517
|
+
} else if let doubleValue = value as? Double {
|
|
518
|
+
extraPayload?.setDouble(key: key, value: doubleValue)
|
|
519
|
+
} else if let intValue = value as? Int {
|
|
520
|
+
extraPayload?.setDouble(key: key, value: Double(intValue))
|
|
521
|
+
} else if let boolValue = value as? Bool {
|
|
522
|
+
extraPayload?.setBoolean(key: key, value: boolValue)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
471
527
|
return TrackItem(
|
|
472
528
|
id: record.id,
|
|
473
529
|
title: record.title,
|
|
@@ -476,7 +532,7 @@ final class DownloadDatabase {
|
|
|
476
532
|
duration: record.duration,
|
|
477
533
|
url: record.url,
|
|
478
534
|
artwork: stringToVariant(record.artwork),
|
|
479
|
-
extraPayload:
|
|
535
|
+
extraPayload: extraPayload
|
|
480
536
|
)
|
|
481
537
|
}
|
|
482
538
|
|
|
@@ -513,4 +569,59 @@ private struct TrackItemRecord: Codable {
|
|
|
513
569
|
let duration: Double
|
|
514
570
|
let url: String
|
|
515
571
|
let artwork: String?
|
|
572
|
+
let extraPayload: [String: Any]?
|
|
573
|
+
|
|
574
|
+
enum CodingKeys: String, CodingKey {
|
|
575
|
+
case id, title, artist, album, duration, url, artwork, extraPayload
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Manual encoding to handle [String: Any]
|
|
579
|
+
func encode(to encoder: Encoder) throws {
|
|
580
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
581
|
+
try container.encode(id, forKey: .id)
|
|
582
|
+
try container.encode(title, forKey: .title)
|
|
583
|
+
try container.encode(artist, forKey: .artist)
|
|
584
|
+
try container.encode(album, forKey: .album)
|
|
585
|
+
try container.encode(duration, forKey: .duration)
|
|
586
|
+
try container.encode(url, forKey: .url)
|
|
587
|
+
try container.encodeIfPresent(artwork, forKey: .artwork)
|
|
588
|
+
|
|
589
|
+
if let extraPayload = extraPayload {
|
|
590
|
+
let jsonData = try JSONSerialization.data(withJSONObject: extraPayload)
|
|
591
|
+
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
592
|
+
try container.encode(jsonString, forKey: .extraPayload)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Manual decoding to handle [String: Any]
|
|
598
|
+
init(from decoder: Decoder) throws {
|
|
599
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
600
|
+
id = try container.decode(String.self, forKey: .id)
|
|
601
|
+
title = try container.decode(String.self, forKey: .title)
|
|
602
|
+
artist = try container.decode(String.self, forKey: .artist)
|
|
603
|
+
album = try container.decode(String.self, forKey: .album)
|
|
604
|
+
duration = try container.decode(Double.self, forKey: .duration)
|
|
605
|
+
url = try container.decode(String.self, forKey: .url)
|
|
606
|
+
artwork = try container.decodeIfPresent(String.self, forKey: .artwork)
|
|
607
|
+
|
|
608
|
+
if let jsonString = try? container.decodeIfPresent(String.self, forKey: .extraPayload),
|
|
609
|
+
let jsonData = jsonString.data(using: .utf8) {
|
|
610
|
+
extraPayload = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
|
611
|
+
} else {
|
|
612
|
+
extraPayload = nil
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Initializer for code creation
|
|
617
|
+
init(id: String, title: String, artist: String, album: String, duration: Double, url: String, artwork: String?, extraPayload: [String: Any]?) {
|
|
618
|
+
self.id = id
|
|
619
|
+
self.title = title
|
|
620
|
+
self.artist = artist
|
|
621
|
+
self.album = album
|
|
622
|
+
self.duration = duration
|
|
623
|
+
self.url = url
|
|
624
|
+
self.artwork = artwork
|
|
625
|
+
self.extraPayload = extraPayload
|
|
626
|
+
}
|
|
516
627
|
}
|
|
@@ -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
|
}
|
|
@@ -573,6 +613,12 @@ final class DownloadManagerCore: NSObject {
|
|
|
573
613
|
artworkString = value
|
|
574
614
|
}
|
|
575
615
|
}
|
|
616
|
+
|
|
617
|
+
var extraPayloadDict: [String: Any]? = nil
|
|
618
|
+
if let extraPayload = track.extraPayload {
|
|
619
|
+
extraPayloadDict = extraPayload.toDictionary()
|
|
620
|
+
}
|
|
621
|
+
|
|
576
622
|
return TrackItemRecord(
|
|
577
623
|
id: track.id,
|
|
578
624
|
title: track.title,
|
|
@@ -580,12 +626,30 @@ final class DownloadManagerCore: NSObject {
|
|
|
580
626
|
album: track.album,
|
|
581
627
|
duration: track.duration,
|
|
582
628
|
url: track.url,
|
|
583
|
-
artwork: artworkString
|
|
629
|
+
artwork: artworkString,
|
|
630
|
+
extraPayload: extraPayloadDict
|
|
584
631
|
)
|
|
585
632
|
}
|
|
586
633
|
|
|
587
634
|
private func recordToTrackItem(_ record: TrackItemRecord) -> TrackItem {
|
|
588
635
|
let artwork: Variant_NullType_String? = record.artwork.map { .second($0) }
|
|
636
|
+
|
|
637
|
+
var extraPayload: AnyMap? = nil
|
|
638
|
+
if let extraPayloadDict = record.extraPayload {
|
|
639
|
+
extraPayload = AnyMap()
|
|
640
|
+
for (key, value) in extraPayloadDict {
|
|
641
|
+
if let stringValue = value as? String {
|
|
642
|
+
extraPayload?.setString(key: key, value: stringValue)
|
|
643
|
+
} else if let doubleValue = value as? Double {
|
|
644
|
+
extraPayload?.setDouble(key: key, value: doubleValue)
|
|
645
|
+
} else if let intValue = value as? Int {
|
|
646
|
+
extraPayload?.setDouble(key: key, value: Double(intValue))
|
|
647
|
+
} else if let boolValue = value as? Bool {
|
|
648
|
+
extraPayload?.setBoolean(key: key, value: boolValue)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
589
653
|
return TrackItem(
|
|
590
654
|
id: record.id,
|
|
591
655
|
title: record.title,
|
|
@@ -594,7 +658,7 @@ final class DownloadManagerCore: NSObject {
|
|
|
594
658
|
duration: record.duration,
|
|
595
659
|
url: record.url,
|
|
596
660
|
artwork: artwork,
|
|
597
|
-
extraPayload:
|
|
661
|
+
extraPayload: extraPayload
|
|
598
662
|
)
|
|
599
663
|
}
|
|
600
664
|
|
|
@@ -923,4 +987,59 @@ private struct TrackItemRecord: Codable {
|
|
|
923
987
|
let duration: Double
|
|
924
988
|
let url: String
|
|
925
989
|
let artwork: String?
|
|
990
|
+
let extraPayload: [String: Any]?
|
|
991
|
+
|
|
992
|
+
enum CodingKeys: String, CodingKey {
|
|
993
|
+
case id, title, artist, album, duration, url, artwork, extraPayload
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Manual encoding to handle [String: Any]
|
|
997
|
+
func encode(to encoder: Encoder) throws {
|
|
998
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
999
|
+
try container.encode(id, forKey: .id)
|
|
1000
|
+
try container.encode(title, forKey: .title)
|
|
1001
|
+
try container.encode(artist, forKey: .artist)
|
|
1002
|
+
try container.encode(album, forKey: .album)
|
|
1003
|
+
try container.encode(duration, forKey: .duration)
|
|
1004
|
+
try container.encode(url, forKey: .url)
|
|
1005
|
+
try container.encodeIfPresent(artwork, forKey: .artwork)
|
|
1006
|
+
|
|
1007
|
+
if let extraPayload = extraPayload {
|
|
1008
|
+
let jsonData = try JSONSerialization.data(withJSONObject: extraPayload)
|
|
1009
|
+
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
1010
|
+
try container.encode(jsonString, forKey: .extraPayload)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Manual decoding to handle [String: Any]
|
|
1016
|
+
init(from decoder: Decoder) throws {
|
|
1017
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
1018
|
+
id = try container.decode(String.self, forKey: .id)
|
|
1019
|
+
title = try container.decode(String.self, forKey: .title)
|
|
1020
|
+
artist = try container.decode(String.self, forKey: .artist)
|
|
1021
|
+
album = try container.decode(String.self, forKey: .album)
|
|
1022
|
+
duration = try container.decode(Double.self, forKey: .duration)
|
|
1023
|
+
url = try container.decode(String.self, forKey: .url)
|
|
1024
|
+
artwork = try container.decodeIfPresent(String.self, forKey: .artwork)
|
|
1025
|
+
|
|
1026
|
+
if let jsonString = try? container.decodeIfPresent(String.self, forKey: .extraPayload),
|
|
1027
|
+
let jsonData = jsonString.data(using: .utf8) {
|
|
1028
|
+
extraPayload = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
|
1029
|
+
} else {
|
|
1030
|
+
extraPayload = nil
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Initializer for code creation
|
|
1035
|
+
init(id: String, title: String, artist: String, album: String, duration: Double, url: String, artwork: String?, extraPayload: [String: Any]?) {
|
|
1036
|
+
self.id = id
|
|
1037
|
+
self.title = title
|
|
1038
|
+
self.artist = artist
|
|
1039
|
+
self.album = album
|
|
1040
|
+
self.duration = duration
|
|
1041
|
+
self.url = url
|
|
1042
|
+
self.artwork = artwork
|
|
1043
|
+
self.extraPayload = extraPayload
|
|
1044
|
+
}
|
|
926
1045
|
}
|