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
|
@@ -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.6",
|
|
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",
|