react-native-nitro-player 0.5.3 → 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/HybridAudioDevices.kt +5 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +4 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +14 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +31 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +142 -95
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +75 -29
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +2 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +1 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +25 -24
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +20 -19
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +119 -85
- package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +50 -0
- package/ios/HybridAudioRoutePicker.swift +1 -1
- package/ios/HybridDownloadManager.swift +3 -3
- package/ios/HybridEqualizer.swift +3 -3
- package/ios/HybridTrackPlayer.swift +8 -4
- package/ios/core/NitroPlayerLogger.swift +22 -0
- package/ios/core/TrackPlayerCore.swift +195 -256
- package/ios/download/DownloadDatabase.swift +92 -62
- package/ios/download/DownloadFileManager.swift +17 -17
- package/ios/download/DownloadManagerCore.swift +80 -44
- package/ios/equalizer/EqualizerCore.swift +25 -20
- package/ios/playlist/PlaylistManager.swift +113 -82
- package/ios/queue/QueueManager.swift +1 -1
- package/ios/storage/NitroPlayerStorage.swift +44 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
- package/lib/types/PlayerQueue.d.ts +1 -1
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JReason.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +2 -1
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +12 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
- package/nitrogen/generated/ios/swift/Reason.swift +4 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/Reason.hpp +4 -0
- package/package.json +1 -1
- package/src/specs/TrackPlayer.nitro.ts +1 -0
- package/src/types/PlayerQueue.ts +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
|
|
|
@@ -67,18 +67,16 @@ final class DownloadDatabase {
|
|
|
67
67
|
func isTrackDownloaded(trackId: String) -> Bool {
|
|
68
68
|
return queue.sync {
|
|
69
69
|
guard let record = downloadedTracks[trackId] else {
|
|
70
|
-
|
|
70
|
+
NitroPlayerLogger.log("DownloadDatabase", "🔍 Track \(trackId) NOT found in database")
|
|
71
71
|
return false
|
|
72
72
|
}
|
|
73
73
|
// Verify file still exists
|
|
74
74
|
let absolutePath = resolveAbsolutePath(for: record)
|
|
75
75
|
let exists = FileManager.default.fileExists(atPath: absolutePath)
|
|
76
76
|
if exists {
|
|
77
|
-
|
|
77
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Track \(trackId) IS downloaded at \(absolutePath)")
|
|
78
78
|
} else {
|
|
79
|
-
|
|
80
|
-
"❌ DownloadDatabase: Track \(trackId) record exists but file NOT found at \(absolutePath)"
|
|
81
|
-
)
|
|
79
|
+
NitroPlayerLogger.log("DownloadDatabase", "❌ Track \(trackId) record exists but file NOT found at \(absolutePath)")
|
|
82
80
|
}
|
|
83
81
|
return exists
|
|
84
82
|
}
|
|
@@ -121,21 +119,21 @@ final class DownloadDatabase {
|
|
|
121
119
|
|
|
122
120
|
func getDownloadedTrack(trackId: String) -> DownloadedTrack? {
|
|
123
121
|
return queue.sync {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
NitroPlayerLogger.log("DownloadDatabase", "🔍 DownloadDatabase.getDownloadedTrack() for trackId: \(trackId)")
|
|
123
|
+
NitroPlayerLogger.log("DownloadDatabase", " Total records in memory: \(downloadedTracks.count)")
|
|
124
|
+
NitroPlayerLogger.log("DownloadDatabase", " Available trackIds: \(Array(downloadedTracks.keys))")
|
|
127
125
|
|
|
128
126
|
guard let record = downloadedTracks[trackId] else {
|
|
129
|
-
|
|
127
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ No record found for trackId: \(trackId)")
|
|
130
128
|
return nil
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
let absolutePath = resolveAbsolutePath(for: record)
|
|
134
|
-
|
|
132
|
+
NitroPlayerLogger.log("DownloadDatabase", " Found record, checking file at: \(absolutePath)")
|
|
135
133
|
|
|
136
134
|
// Verify file still exists
|
|
137
135
|
guard FileManager.default.fileExists(atPath: absolutePath) else {
|
|
138
|
-
|
|
136
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ File does NOT exist, cleaning up record")
|
|
139
137
|
// File was deleted externally, clean up record
|
|
140
138
|
queue.async(flags: .barrier) {
|
|
141
139
|
self.downloadedTracks.removeValue(forKey: trackId)
|
|
@@ -144,34 +142,33 @@ final class DownloadDatabase {
|
|
|
144
142
|
return nil
|
|
145
143
|
}
|
|
146
144
|
|
|
147
|
-
|
|
145
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists, returning track")
|
|
148
146
|
return recordToDownloadedTrack(record)
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
func getAllDownloadedTracks() -> [DownloadedTrack] {
|
|
153
151
|
return queue.sync {
|
|
154
|
-
|
|
155
|
-
"🎯 DownloadDatabase: getAllDownloadedTracks called, have \(downloadedTracks.count) records")
|
|
152
|
+
NitroPlayerLogger.log("DownloadDatabase", "🎯 getAllDownloadedTracks called, have \(downloadedTracks.count) records")
|
|
156
153
|
|
|
157
154
|
var validTracks: [DownloadedTrack] = []
|
|
158
155
|
var invalidTrackIds: [String] = []
|
|
159
156
|
|
|
160
157
|
for (trackId, record) in downloadedTracks {
|
|
161
158
|
let absolutePath = resolveAbsolutePath(for: record)
|
|
162
|
-
|
|
159
|
+
NitroPlayerLogger.log("DownloadDatabase", " Checking track \(trackId) at path: \(absolutePath)")
|
|
163
160
|
if FileManager.default.fileExists(atPath: absolutePath) {
|
|
164
|
-
|
|
161
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists")
|
|
165
162
|
validTracks.append(recordToDownloadedTrack(record))
|
|
166
163
|
} else {
|
|
167
|
-
|
|
164
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ File NOT found")
|
|
168
165
|
invalidTrackIds.append(trackId)
|
|
169
166
|
}
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
// Clean up invalid records
|
|
173
170
|
if !invalidTrackIds.isEmpty {
|
|
174
|
-
|
|
171
|
+
NitroPlayerLogger.log("DownloadDatabase", " Cleaning up \(invalidTrackIds.count) invalid records")
|
|
175
172
|
queue.async(flags: .barrier) {
|
|
176
173
|
for trackId in invalidTrackIds {
|
|
177
174
|
self.downloadedTracks.removeValue(forKey: trackId)
|
|
@@ -180,7 +177,7 @@ final class DownloadDatabase {
|
|
|
180
177
|
}
|
|
181
178
|
}
|
|
182
179
|
|
|
183
|
-
|
|
180
|
+
NitroPlayerLogger.log("DownloadDatabase", "🎯 Returning \(validTracks.count) valid tracks")
|
|
184
181
|
return validTracks
|
|
185
182
|
}
|
|
186
183
|
}
|
|
@@ -238,7 +235,7 @@ final class DownloadDatabase {
|
|
|
238
235
|
/// Returns the number of orphaned records that were cleaned up
|
|
239
236
|
func syncDownloads() -> Int {
|
|
240
237
|
return queue.sync(flags: .barrier) {
|
|
241
|
-
|
|
238
|
+
NitroPlayerLogger.log("DownloadDatabase", "🔄 syncDownloads called")
|
|
242
239
|
|
|
243
240
|
var removedCount = 0
|
|
244
241
|
var trackIdsToRemove: [String] = []
|
|
@@ -246,7 +243,7 @@ final class DownloadDatabase {
|
|
|
246
243
|
for (trackId, record) in downloadedTracks {
|
|
247
244
|
let absolutePath = resolveAbsolutePath(for: record)
|
|
248
245
|
if !FileManager.default.fileExists(atPath: absolutePath) {
|
|
249
|
-
|
|
246
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ Missing file for track \(trackId): \(absolutePath)")
|
|
250
247
|
trackIdsToRemove.append(trackId)
|
|
251
248
|
}
|
|
252
249
|
}
|
|
@@ -271,9 +268,9 @@ final class DownloadDatabase {
|
|
|
271
268
|
|
|
272
269
|
if removedCount > 0 {
|
|
273
270
|
saveToDisk()
|
|
274
|
-
|
|
271
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ Cleaned up \(removedCount) orphaned records")
|
|
275
272
|
} else {
|
|
276
|
-
|
|
273
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ All downloads are valid")
|
|
277
274
|
}
|
|
278
275
|
|
|
279
276
|
return removedCount
|
|
@@ -356,32 +353,67 @@ final class DownloadDatabase {
|
|
|
356
353
|
private func saveToDisk() {
|
|
357
354
|
do {
|
|
358
355
|
let tracksData = try JSONEncoder().encode(downloadedTracks)
|
|
359
|
-
UserDefaults.standard.set(tracksData, forKey: Self.downloadedTracksKey)
|
|
360
|
-
|
|
361
356
|
// Convert Set to Array for encoding
|
|
362
357
|
let playlistTracksDict = playlistTracks.mapValues { Array($0) }
|
|
363
358
|
let playlistData = try JSONEncoder().encode(playlistTracksDict)
|
|
364
|
-
|
|
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)
|
|
365
371
|
} catch {
|
|
366
|
-
|
|
372
|
+
NitroPlayerLogger.log("DownloadDatabase", "Failed to save to disk: \(error)")
|
|
367
373
|
}
|
|
368
374
|
}
|
|
369
375
|
|
|
370
376
|
private func loadFromDisk() {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
NitroPlayerLogger.log("DownloadDatabase", "\n" + String(repeating: "📀", count: 40))
|
|
378
|
+
NitroPlayerLogger.log("DownloadDatabase", "📀 LOADING FROM DISK")
|
|
379
|
+
NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40))
|
|
380
|
+
|
|
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
|
|
374
408
|
|
|
375
|
-
|
|
376
|
-
// Load downloaded tracks
|
|
377
|
-
if let tracksData = UserDefaults.standard.data(forKey: Self.downloadedTracksKey) {
|
|
409
|
+
if let tracksData = UserDefaults.standard.data(forKey: Self.legacyDownloadedTracksKey) {
|
|
378
410
|
do {
|
|
379
411
|
self.downloadedTracks = try JSONDecoder().decode(
|
|
380
412
|
[String: DownloadedTrackRecord].self, from: tracksData)
|
|
381
|
-
|
|
413
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated \(self.downloadedTracks.count) tracks from UserDefaults")
|
|
382
414
|
|
|
383
|
-
// Migrate absolute paths → filenames (
|
|
384
|
-
var
|
|
415
|
+
// Migrate absolute paths → filenames (pre-existing migration)
|
|
416
|
+
var needsPathMigration = false
|
|
385
417
|
for (trackId, record) in self.downloadedTracks {
|
|
386
418
|
if record.localPath.contains("/") {
|
|
387
419
|
let filename = URL(fileURLWithPath: record.localPath).lastPathComponent
|
|
@@ -394,45 +426,43 @@ final class DownloadDatabase {
|
|
|
394
426
|
fileSize: record.fileSize,
|
|
395
427
|
storageLocation: record.storageLocation
|
|
396
428
|
)
|
|
397
|
-
|
|
429
|
+
needsPathMigration = true
|
|
398
430
|
}
|
|
399
431
|
}
|
|
400
|
-
if
|
|
401
|
-
|
|
402
|
-
// Log each downloaded track
|
|
403
|
-
for (trackId, record) in self.downloadedTracks {
|
|
404
|
-
print(" 📥 \(trackId)")
|
|
405
|
-
print(" Title: \(record.originalTrack.title)")
|
|
406
|
-
print(" Path (filename): \(record.localPath)")
|
|
432
|
+
if needsPathMigration {
|
|
433
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated absolute paths to filenames")
|
|
407
434
|
}
|
|
435
|
+
|
|
436
|
+
UserDefaults.standard.removeObject(forKey: Self.legacyDownloadedTracksKey)
|
|
437
|
+
didMigrate = true
|
|
408
438
|
} catch {
|
|
409
|
-
|
|
439
|
+
NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to migrate tracks from UserDefaults: \(error)")
|
|
410
440
|
}
|
|
411
441
|
} else {
|
|
412
|
-
|
|
442
|
+
NitroPlayerLogger.log("DownloadDatabase", "⚠️ No saved tracks found in UserDefaults")
|
|
413
443
|
}
|
|
414
444
|
|
|
415
|
-
|
|
416
|
-
if let playlistData = UserDefaults.standard.data(forKey: Self.playlistTracksKey) {
|
|
445
|
+
if let playlistData = UserDefaults.standard.data(forKey: Self.legacyPlaylistTracksKey) {
|
|
417
446
|
do {
|
|
418
447
|
let playlistTracksDict = try JSONDecoder().decode(
|
|
419
448
|
[String: [String]].self, from: playlistData)
|
|
420
449
|
self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Log playlist associations
|
|
425
|
-
for (playlistId, trackIds) in self.playlistTracks {
|
|
426
|
-
print(" 📋 Playlist \(playlistId): \(trackIds.count) tracks")
|
|
427
|
-
}
|
|
450
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Migrated \(self.playlistTracks.count) playlist associations from UserDefaults")
|
|
451
|
+
UserDefaults.standard.removeObject(forKey: Self.legacyPlaylistTracksKey)
|
|
452
|
+
didMigrate = true
|
|
428
453
|
} catch {
|
|
429
|
-
|
|
454
|
+
NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to migrate playlist tracks from UserDefaults: \(error)")
|
|
430
455
|
}
|
|
431
456
|
} else {
|
|
432
|
-
|
|
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()
|
|
433
463
|
}
|
|
434
464
|
|
|
435
|
-
|
|
465
|
+
NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
|
|
436
466
|
}
|
|
437
467
|
|
|
438
468
|
// MARK: - Conversion Helpers
|
|
@@ -60,14 +60,14 @@ final class DownloadFileManager {
|
|
|
60
60
|
suggestedFilename: String? = nil,
|
|
61
61
|
httpResponse: HTTPURLResponse? = nil
|
|
62
62
|
) -> String? {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
NitroPlayerLogger.log("DownloadFileManager", "saveDownloadedFile called for trackId=\(trackId)")
|
|
64
|
+
NitroPlayerLogger.log("DownloadFileManager", " From: \(temporaryLocation.path)")
|
|
65
|
+
NitroPlayerLogger.log("DownloadFileManager", " Original URL: \(originalURL ?? "nil")")
|
|
66
|
+
NitroPlayerLogger.log("DownloadFileManager", " Suggested Filename: \(suggestedFilename ?? "nil")")
|
|
67
67
|
|
|
68
68
|
let destinationDirectory =
|
|
69
69
|
storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
|
|
70
|
-
|
|
70
|
+
NitroPlayerLogger.log("DownloadFileManager", " Destination directory: \(destinationDirectory.path)")
|
|
71
71
|
|
|
72
72
|
// Determine file extension using headers first, then URL path, then default
|
|
73
73
|
let fileExtension = Self.resolveFileExtension(
|
|
@@ -75,32 +75,32 @@ final class DownloadFileManager {
|
|
|
75
75
|
suggestedFilename: suggestedFilename,
|
|
76
76
|
originalURL: originalURL
|
|
77
77
|
)
|
|
78
|
-
|
|
78
|
+
NitroPlayerLogger.log("DownloadFileManager", " File extension: \(fileExtension)")
|
|
79
79
|
|
|
80
80
|
let fileName = "\(trackId).\(fileExtension)"
|
|
81
81
|
let destinationURL = destinationDirectory.appendingPathComponent(fileName)
|
|
82
|
-
|
|
82
|
+
NitroPlayerLogger.log("DownloadFileManager", " Destination: \(destinationURL.path)")
|
|
83
83
|
|
|
84
84
|
// Verify source file exists
|
|
85
85
|
guard fileManager.fileExists(atPath: temporaryLocation.path) else {
|
|
86
|
-
|
|
86
|
+
NitroPlayerLogger.log("DownloadFileManager", "❌ Source file does not exist at \(temporaryLocation.path)")
|
|
87
87
|
return nil
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
do {
|
|
91
91
|
// Remove existing file if present
|
|
92
92
|
if fileManager.fileExists(atPath: destinationURL.path) {
|
|
93
|
-
|
|
93
|
+
NitroPlayerLogger.log("DownloadFileManager", " Removing existing file at destination")
|
|
94
94
|
try fileManager.removeItem(at: destinationURL)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Move from temporary location to permanent location
|
|
98
98
|
try fileManager.moveItem(at: temporaryLocation, to: destinationURL)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
NitroPlayerLogger.log("DownloadFileManager", "✅ File saved successfully")
|
|
101
101
|
return destinationURL.path
|
|
102
102
|
} catch {
|
|
103
|
-
|
|
103
|
+
NitroPlayerLogger.log("DownloadFileManager", "❌ Failed to save file: \(error)")
|
|
104
104
|
return nil
|
|
105
105
|
}
|
|
106
106
|
}
|
|
@@ -133,7 +133,7 @@ final class DownloadFileManager {
|
|
|
133
133
|
// 1. Content-Disposition: attachment; filename="track.mp3"
|
|
134
134
|
if let disposition = httpResponse?.value(forHTTPHeaderField: "Content-Disposition") {
|
|
135
135
|
if let ext = extensionFromContentDisposition(disposition), !ext.isEmpty {
|
|
136
|
-
|
|
136
|
+
NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] Content-Disposition → .\(ext)")
|
|
137
137
|
return ext
|
|
138
138
|
}
|
|
139
139
|
}
|
|
@@ -142,7 +142,7 @@ final class DownloadFileManager {
|
|
|
142
142
|
if let contentType = httpResponse?.value(forHTTPHeaderField: "Content-Type") {
|
|
143
143
|
let mime = contentType.split(separator: ";").first.map(String.init)?.trimmingCharacters(in: .whitespaces) ?? contentType
|
|
144
144
|
if let ext = mimeTypeToExtension[mime.lowercased()] {
|
|
145
|
-
|
|
145
|
+
NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] Content-Type '\(mime)' → .\(ext)")
|
|
146
146
|
return ext
|
|
147
147
|
}
|
|
148
148
|
}
|
|
@@ -151,7 +151,7 @@ final class DownloadFileManager {
|
|
|
151
151
|
if let name = suggestedFilename, !name.isEmpty {
|
|
152
152
|
let ext = URL(fileURLWithPath: name).pathExtension.lowercased()
|
|
153
153
|
if !ext.isEmpty && isAudioExtension(ext) {
|
|
154
|
-
|
|
154
|
+
NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] suggestedFilename → .\(ext)")
|
|
155
155
|
return ext
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -160,12 +160,12 @@ final class DownloadFileManager {
|
|
|
160
160
|
if let urlString = originalURL, let url = URL(string: urlString) {
|
|
161
161
|
let ext = url.pathExtension.lowercased()
|
|
162
162
|
if !ext.isEmpty && isAudioExtension(ext) {
|
|
163
|
-
|
|
163
|
+
NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] URL path ext → .\(ext)")
|
|
164
164
|
return ext
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
NitroPlayerLogger.log("DownloadFileManager", " [ExtResolve] fallback → .mp3")
|
|
169
169
|
return "mp3"
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -200,7 +200,7 @@ final class DownloadFileManager {
|
|
|
200
200
|
try fileManager.removeItem(atPath: path)
|
|
201
201
|
}
|
|
202
202
|
} catch {
|
|
203
|
-
|
|
203
|
+
NitroPlayerLogger.log("DownloadFileManager", "Failed to delete file: \(error)")
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -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
|
|
|
@@ -372,12 +373,12 @@ final class DownloadManagerCore: NSObject {
|
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
func getLocalPath(trackId: String) -> String? {
|
|
375
|
-
|
|
376
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🔍 getLocalPath() called for trackId: \(trackId)")
|
|
376
377
|
if let downloadedTrack = DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) {
|
|
377
|
-
|
|
378
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
|
|
378
379
|
return downloadedTrack.localPath
|
|
379
380
|
} else {
|
|
380
|
-
|
|
381
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ❌ No downloaded track found for trackId: \(trackId)")
|
|
381
382
|
return nil
|
|
382
383
|
}
|
|
383
384
|
}
|
|
@@ -406,9 +407,7 @@ final class DownloadManagerCore: NSObject {
|
|
|
406
407
|
func syncDownloads() -> Int {
|
|
407
408
|
let removedFromDb = DownloadDatabase.shared.syncDownloads()
|
|
408
409
|
let bytesFreed = DownloadFileManager.shared.cleanupOrphanedFiles()
|
|
409
|
-
|
|
410
|
-
"🔄 DownloadManagerCore: syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes"
|
|
411
|
-
)
|
|
410
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🔄 syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes")
|
|
412
411
|
return removedFromDb
|
|
413
412
|
}
|
|
414
413
|
|
|
@@ -426,27 +425,27 @@ final class DownloadManagerCore: NSObject {
|
|
|
426
425
|
|
|
427
426
|
func getEffectiveUrl(track: TrackItem) -> String {
|
|
428
427
|
let preference = getPlaybackSourcePreference()
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🔍 getEffectiveUrl() for track: \(track.id)")
|
|
429
|
+
NitroPlayerLogger.log("DownloadManagerCore", " Playback preference: \(preference)")
|
|
431
430
|
|
|
432
431
|
switch preference {
|
|
433
432
|
case .network:
|
|
434
|
-
|
|
433
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Using network URL (preference=network)")
|
|
435
434
|
return track.url
|
|
436
435
|
case .download:
|
|
437
436
|
if let localPath = getLocalPath(trackId: track.id) {
|
|
438
|
-
|
|
437
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
|
|
439
438
|
return localPath
|
|
440
439
|
} else {
|
|
441
|
-
|
|
440
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, falling back to network URL")
|
|
442
441
|
return track.url
|
|
443
442
|
}
|
|
444
443
|
case .auto:
|
|
445
444
|
if let localPath = getLocalPath(trackId: track.id) {
|
|
446
|
-
|
|
445
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
|
|
447
446
|
return localPath
|
|
448
447
|
} else {
|
|
449
|
-
|
|
448
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, using network URL")
|
|
450
449
|
return track.url
|
|
451
450
|
}
|
|
452
451
|
}
|
|
@@ -507,52 +506,91 @@ final class DownloadManagerCore: NSObject {
|
|
|
507
506
|
|
|
508
507
|
/// Load persisted track metadata and playlist associations (survives app restart)
|
|
509
508
|
private func loadPersistedMetadata() {
|
|
510
|
-
|
|
509
|
+
NitroPlayerLogger.log("DownloadManagerCore", "📦 Loading persisted metadata...")
|
|
510
|
+
|
|
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
|
|
511
536
|
|
|
512
|
-
|
|
513
|
-
if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
|
|
537
|
+
if let data = UserDefaults.standard.data(forKey: Self.legacyTrackMetadataKey) {
|
|
514
538
|
do {
|
|
515
539
|
let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: data)
|
|
516
540
|
for (trackId, record) in records {
|
|
517
541
|
trackMetadata[trackId] = recordToTrackItem(record)
|
|
518
542
|
}
|
|
519
|
-
|
|
543
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ✅ Migrated \(trackMetadata.count) track metadata entries from UserDefaults")
|
|
544
|
+
UserDefaults.standard.removeObject(forKey: Self.legacyTrackMetadataKey)
|
|
545
|
+
didMigrate = true
|
|
520
546
|
} catch {
|
|
521
|
-
|
|
547
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to migrate track metadata: \(error)")
|
|
522
548
|
}
|
|
523
549
|
} else {
|
|
524
|
-
|
|
550
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted track metadata found")
|
|
525
551
|
}
|
|
526
552
|
|
|
527
|
-
|
|
528
|
-
if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
|
|
553
|
+
if let data = UserDefaults.standard.data(forKey: Self.legacyPlaylistAssociationsKey) {
|
|
529
554
|
do {
|
|
530
555
|
playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
|
|
531
|
-
|
|
556
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ✅ Migrated \(playlistAssociations.count) playlist associations from UserDefaults")
|
|
557
|
+
UserDefaults.standard.removeObject(forKey: Self.legacyPlaylistAssociationsKey)
|
|
558
|
+
didMigrate = true
|
|
532
559
|
} catch {
|
|
533
|
-
|
|
560
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to migrate playlist associations: \(error)")
|
|
534
561
|
}
|
|
535
562
|
} else {
|
|
536
|
-
|
|
563
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted playlist associations found")
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if didMigrate {
|
|
567
|
+
savePersistedMetadata()
|
|
537
568
|
}
|
|
538
569
|
}
|
|
539
570
|
|
|
540
571
|
/// Persist track metadata and playlist associations to disk
|
|
541
572
|
private func savePersistedMetadata() {
|
|
542
|
-
// Convert TrackItem to TrackItemRecord for encoding
|
|
543
573
|
var records: [String: TrackItemRecord] = [:]
|
|
544
574
|
for (trackId, track) in trackMetadata {
|
|
545
575
|
records[trackId] = trackItemToRecord(track)
|
|
546
576
|
}
|
|
547
577
|
|
|
548
578
|
do {
|
|
549
|
-
let
|
|
550
|
-
UserDefaults.standard.set(trackData, forKey: Self.trackMetadataKey)
|
|
551
|
-
|
|
579
|
+
let tracksData = try JSONEncoder().encode(records)
|
|
552
580
|
let playlistData = try JSONEncoder().encode(playlistAssociations)
|
|
553
|
-
|
|
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)
|
|
554
592
|
} catch {
|
|
555
|
-
|
|
593
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save metadata: \(error)")
|
|
556
594
|
}
|
|
557
595
|
}
|
|
558
596
|
|
|
@@ -657,24 +695,22 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
657
695
|
_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
658
696
|
didFinishDownloadingTo location: URL
|
|
659
697
|
) {
|
|
660
|
-
|
|
698
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🎯 didFinishDownloadingTo called")
|
|
661
699
|
|
|
662
700
|
guard let description = downloadTask.taskDescription else {
|
|
663
|
-
|
|
701
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ No task description")
|
|
664
702
|
return
|
|
665
703
|
}
|
|
666
704
|
let parts = description.split(separator: "|")
|
|
667
705
|
guard parts.count == 2 else {
|
|
668
|
-
|
|
706
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ Invalid task description format: \(description)")
|
|
669
707
|
return
|
|
670
708
|
}
|
|
671
709
|
|
|
672
710
|
let downloadId = String(parts[0])
|
|
673
711
|
let trackId = String(parts[1])
|
|
674
712
|
|
|
675
|
-
|
|
676
|
-
"🎯 DownloadManagerCore: Processing completion for downloadId=\(downloadId), trackId=\(trackId)"
|
|
677
|
-
)
|
|
713
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🎯 Processing completion for downloadId=\(downloadId), trackId=\(trackId)")
|
|
678
714
|
|
|
679
715
|
// IMPORTANT: Move file SYNCHRONOUSLY - the temp file is deleted after this method returns!
|
|
680
716
|
// Get storage location and original URL from track metadata
|
|
@@ -698,7 +734,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
698
734
|
// Now handle the rest asynchronously
|
|
699
735
|
queue.async(flags: .barrier) {
|
|
700
736
|
guard let destinationPath = destinationPath else {
|
|
701
|
-
|
|
737
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save file for trackId=\(trackId)")
|
|
702
738
|
self.taskMetadata[downloadId]?.state = .failed
|
|
703
739
|
self.taskMetadata[downloadId]?.error = DownloadError(
|
|
704
740
|
code: "FILE_MOVE_FAILED",
|
|
@@ -712,11 +748,11 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
712
748
|
return
|
|
713
749
|
}
|
|
714
750
|
|
|
715
|
-
|
|
751
|
+
NitroPlayerLogger.log("DownloadManagerCore", "✅ File saved to \(destinationPath)")
|
|
716
752
|
|
|
717
753
|
guard let track = self.trackMetadata[trackId] else {
|
|
718
|
-
|
|
719
|
-
|
|
754
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ No track metadata for trackId=\(trackId)")
|
|
755
|
+
NitroPlayerLogger.log("DownloadManagerCore", " Available trackIds: \(Array(self.trackMetadata.keys))")
|
|
720
756
|
|
|
721
757
|
// Still mark as completed even if we don't have metadata
|
|
722
758
|
self.taskMetadata[downloadId]?.state = .completed
|
|
@@ -747,7 +783,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
747
783
|
// Save to database
|
|
748
784
|
DownloadDatabase.shared.saveDownloadedTrack(downloadedTrack, playlistId: playlistId)
|
|
749
785
|
|
|
750
|
-
|
|
786
|
+
NitroPlayerLogger.log("DownloadManagerCore", "✅ Track saved to database")
|
|
751
787
|
|
|
752
788
|
// Clean up persisted metadata (no longer needed after completion)
|
|
753
789
|
self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
|
|
@@ -760,7 +796,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
760
796
|
self.activeTasks.removeValue(forKey: downloadId)
|
|
761
797
|
|
|
762
798
|
// Notify
|
|
763
|
-
|
|
799
|
+
NitroPlayerLogger.log("DownloadManagerCore", "✅ Notifying completion for trackId=\(trackId)")
|
|
764
800
|
self.notifyStateChange(
|
|
765
801
|
downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
|
|
766
802
|
self.notifyComplete(downloadedTrack)
|