react-native-nitro-player 0.5.2 → 0.5.4
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/README.md +2 -0
- 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 +7 -6
- 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 +16 -9
- 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 +72 -49
- package/ios/download/DownloadFileManager.swift +24 -17
- package/ios/download/DownloadManagerCore.swift +29 -33
- package/ios/equalizer/EqualizerCore.swift +25 -20
- package/ios/playlist/PlaylistManager.swift +19 -9
- package/ios/queue/QueueManager.swift +1 -1
- 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
|
@@ -41,7 +41,7 @@ final class DownloadDatabase {
|
|
|
41
41
|
let record = DownloadedTrackRecord(
|
|
42
42
|
trackId: track.trackId,
|
|
43
43
|
originalTrack: self.trackItemToRecord(track.originalTrack),
|
|
44
|
-
localPath: track.localPath,
|
|
44
|
+
localPath: URL(fileURLWithPath: track.localPath).lastPathComponent,
|
|
45
45
|
localArtworkPath: self.variantToString(track.localArtworkPath),
|
|
46
46
|
downloadedAt: track.downloadedAt,
|
|
47
47
|
fileSize: track.fileSize,
|
|
@@ -67,17 +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
|
-
let
|
|
74
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
75
|
+
let exists = FileManager.default.fileExists(atPath: absolutePath)
|
|
75
76
|
if exists {
|
|
76
|
-
|
|
77
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Track \(trackId) IS downloaded at \(absolutePath)")
|
|
77
78
|
} else {
|
|
78
|
-
|
|
79
|
-
"❌ DownloadDatabase: Track \(trackId) record exists but file NOT found at \(record.localPath)"
|
|
80
|
-
)
|
|
79
|
+
NitroPlayerLogger.log("DownloadDatabase", "❌ Track \(trackId) record exists but file NOT found at \(absolutePath)")
|
|
81
80
|
}
|
|
82
81
|
return exists
|
|
83
82
|
}
|
|
@@ -120,20 +119,21 @@ final class DownloadDatabase {
|
|
|
120
119
|
|
|
121
120
|
func getDownloadedTrack(trackId: String) -> DownloadedTrack? {
|
|
122
121
|
return queue.sync {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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))")
|
|
126
125
|
|
|
127
126
|
guard let record = downloadedTracks[trackId] else {
|
|
128
|
-
|
|
127
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ No record found for trackId: \(trackId)")
|
|
129
128
|
return nil
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
132
|
+
NitroPlayerLogger.log("DownloadDatabase", " Found record, checking file at: \(absolutePath)")
|
|
133
133
|
|
|
134
134
|
// Verify file still exists
|
|
135
|
-
guard FileManager.default.fileExists(atPath:
|
|
136
|
-
|
|
135
|
+
guard FileManager.default.fileExists(atPath: absolutePath) else {
|
|
136
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ File does NOT exist, cleaning up record")
|
|
137
137
|
// File was deleted externally, clean up record
|
|
138
138
|
queue.async(flags: .barrier) {
|
|
139
139
|
self.downloadedTracks.removeValue(forKey: trackId)
|
|
@@ -142,33 +142,33 @@ final class DownloadDatabase {
|
|
|
142
142
|
return nil
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists, returning track")
|
|
146
146
|
return recordToDownloadedTrack(record)
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
func getAllDownloadedTracks() -> [DownloadedTrack] {
|
|
151
151
|
return queue.sync {
|
|
152
|
-
|
|
153
|
-
"🎯 DownloadDatabase: getAllDownloadedTracks called, have \(downloadedTracks.count) records")
|
|
152
|
+
NitroPlayerLogger.log("DownloadDatabase", "🎯 getAllDownloadedTracks called, have \(downloadedTracks.count) records")
|
|
154
153
|
|
|
155
154
|
var validTracks: [DownloadedTrack] = []
|
|
156
155
|
var invalidTrackIds: [String] = []
|
|
157
156
|
|
|
158
157
|
for (trackId, record) in downloadedTracks {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
159
|
+
NitroPlayerLogger.log("DownloadDatabase", " Checking track \(trackId) at path: \(absolutePath)")
|
|
160
|
+
if FileManager.default.fileExists(atPath: absolutePath) {
|
|
161
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ File exists")
|
|
162
162
|
validTracks.append(recordToDownloadedTrack(record))
|
|
163
163
|
} else {
|
|
164
|
-
|
|
164
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ File NOT found")
|
|
165
165
|
invalidTrackIds.append(trackId)
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// Clean up invalid records
|
|
170
170
|
if !invalidTrackIds.isEmpty {
|
|
171
|
-
|
|
171
|
+
NitroPlayerLogger.log("DownloadDatabase", " Cleaning up \(invalidTrackIds.count) invalid records")
|
|
172
172
|
queue.async(flags: .barrier) {
|
|
173
173
|
for trackId in invalidTrackIds {
|
|
174
174
|
self.downloadedTracks.removeValue(forKey: trackId)
|
|
@@ -177,7 +177,7 @@ final class DownloadDatabase {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
NitroPlayerLogger.log("DownloadDatabase", "🎯 Returning \(validTracks.count) valid tracks")
|
|
181
181
|
return validTracks
|
|
182
182
|
}
|
|
183
183
|
}
|
|
@@ -235,14 +235,15 @@ final class DownloadDatabase {
|
|
|
235
235
|
/// Returns the number of orphaned records that were cleaned up
|
|
236
236
|
func syncDownloads() -> Int {
|
|
237
237
|
return queue.sync(flags: .barrier) {
|
|
238
|
-
|
|
238
|
+
NitroPlayerLogger.log("DownloadDatabase", "🔄 syncDownloads called")
|
|
239
239
|
|
|
240
240
|
var removedCount = 0
|
|
241
241
|
var trackIdsToRemove: [String] = []
|
|
242
242
|
|
|
243
243
|
for (trackId, record) in downloadedTracks {
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
245
|
+
if !FileManager.default.fileExists(atPath: absolutePath) {
|
|
246
|
+
NitroPlayerLogger.log("DownloadDatabase", " ❌ Missing file for track \(trackId): \(absolutePath)")
|
|
246
247
|
trackIdsToRemove.append(trackId)
|
|
247
248
|
}
|
|
248
249
|
}
|
|
@@ -267,9 +268,9 @@ final class DownloadDatabase {
|
|
|
267
268
|
|
|
268
269
|
if removedCount > 0 {
|
|
269
270
|
saveToDisk()
|
|
270
|
-
|
|
271
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ Cleaned up \(removedCount) orphaned records")
|
|
271
272
|
} else {
|
|
272
|
-
|
|
273
|
+
NitroPlayerLogger.log("DownloadDatabase", " ✅ All downloads are valid")
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
return removedCount
|
|
@@ -283,7 +284,7 @@ final class DownloadDatabase {
|
|
|
283
284
|
guard let record = self.downloadedTracks[trackId] else { return }
|
|
284
285
|
|
|
285
286
|
// Delete the file
|
|
286
|
-
DownloadFileManager.shared.deleteFile(at: record
|
|
287
|
+
DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
|
|
287
288
|
|
|
288
289
|
// Delete artwork if exists
|
|
289
290
|
if let artworkPath = record.localArtworkPath {
|
|
@@ -314,7 +315,7 @@ final class DownloadDatabase {
|
|
|
314
315
|
// Delete all tracks in the playlist
|
|
315
316
|
for trackId in trackIds {
|
|
316
317
|
if let record = self.downloadedTracks[trackId] {
|
|
317
|
-
DownloadFileManager.shared.deleteFile(at: record
|
|
318
|
+
DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
|
|
318
319
|
if let artworkPath = record.localArtworkPath {
|
|
319
320
|
DownloadFileManager.shared.deleteFile(at: artworkPath)
|
|
320
321
|
}
|
|
@@ -333,7 +334,7 @@ final class DownloadDatabase {
|
|
|
333
334
|
queue.async(flags: .barrier) {
|
|
334
335
|
// Delete all files
|
|
335
336
|
for record in self.downloadedTracks.values {
|
|
336
|
-
DownloadFileManager.shared.deleteFile(at: record
|
|
337
|
+
DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
|
|
337
338
|
if let artworkPath = record.localArtworkPath {
|
|
338
339
|
DownloadFileManager.shared.deleteFile(at: artworkPath)
|
|
339
340
|
}
|
|
@@ -359,14 +360,14 @@ final class DownloadDatabase {
|
|
|
359
360
|
let playlistData = try JSONEncoder().encode(playlistTracksDict)
|
|
360
361
|
UserDefaults.standard.set(playlistData, forKey: Self.playlistTracksKey)
|
|
361
362
|
} catch {
|
|
362
|
-
|
|
363
|
+
NitroPlayerLogger.log("DownloadDatabase", "Failed to save to disk: \(error)")
|
|
363
364
|
}
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
private func loadFromDisk() {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
NitroPlayerLogger.log("DownloadDatabase", "\n" + String(repeating: "📀", count: 40))
|
|
369
|
+
NitroPlayerLogger.log("DownloadDatabase", "📀 LOADING FROM DISK")
|
|
370
|
+
NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40))
|
|
370
371
|
|
|
371
372
|
// Load synchronously to ensure data is available immediately
|
|
372
373
|
// Load downloaded tracks
|
|
@@ -374,20 +375,38 @@ final class DownloadDatabase {
|
|
|
374
375
|
do {
|
|
375
376
|
self.downloadedTracks = try JSONDecoder().decode(
|
|
376
377
|
[String: DownloadedTrackRecord].self, from: tracksData)
|
|
377
|
-
|
|
378
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.downloadedTracks.count) tracks from disk")
|
|
379
|
+
|
|
380
|
+
// Migrate absolute paths → filenames (one-time, for existing installs)
|
|
381
|
+
var needsMigration = false
|
|
382
|
+
for (trackId, record) in self.downloadedTracks {
|
|
383
|
+
if record.localPath.contains("/") {
|
|
384
|
+
let filename = URL(fileURLWithPath: record.localPath).lastPathComponent
|
|
385
|
+
self.downloadedTracks[trackId] = DownloadedTrackRecord(
|
|
386
|
+
trackId: record.trackId,
|
|
387
|
+
originalTrack: record.originalTrack,
|
|
388
|
+
localPath: filename,
|
|
389
|
+
localArtworkPath: record.localArtworkPath,
|
|
390
|
+
downloadedAt: record.downloadedAt,
|
|
391
|
+
fileSize: record.fileSize,
|
|
392
|
+
storageLocation: record.storageLocation
|
|
393
|
+
)
|
|
394
|
+
needsMigration = true
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if needsMigration { self.saveToDisk() }
|
|
378
398
|
|
|
379
399
|
// Log each downloaded track
|
|
380
400
|
for (trackId, record) in self.downloadedTracks {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
print(" Exists: \(FileManager.default.fileExists(atPath: record.localPath))")
|
|
401
|
+
NitroPlayerLogger.log("DownloadDatabase", " 📥 \(trackId)")
|
|
402
|
+
NitroPlayerLogger.log("DownloadDatabase", " Title: \(record.originalTrack.title)")
|
|
403
|
+
NitroPlayerLogger.log("DownloadDatabase", " Path (filename): \(record.localPath)")
|
|
385
404
|
}
|
|
386
405
|
} catch {
|
|
387
|
-
|
|
406
|
+
NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load tracks from disk: \(error)")
|
|
388
407
|
}
|
|
389
408
|
} else {
|
|
390
|
-
|
|
409
|
+
NitroPlayerLogger.log("DownloadDatabase", "⚠️ No saved tracks found in UserDefaults")
|
|
391
410
|
}
|
|
392
411
|
|
|
393
412
|
// Load playlist associations
|
|
@@ -396,25 +415,29 @@ final class DownloadDatabase {
|
|
|
396
415
|
let playlistTracksDict = try JSONDecoder().decode(
|
|
397
416
|
[String: [String]].self, from: playlistData)
|
|
398
417
|
self.playlistTracks = playlistTracksDict.mapValues { Set($0) }
|
|
399
|
-
|
|
400
|
-
"✅ DownloadDatabase: Loaded \(self.playlistTracks.count) playlist associations from disk")
|
|
418
|
+
NitroPlayerLogger.log("DownloadDatabase", "✅ Loaded \(self.playlistTracks.count) playlist associations from disk")
|
|
401
419
|
|
|
402
420
|
// Log playlist associations
|
|
403
421
|
for (playlistId, trackIds) in self.playlistTracks {
|
|
404
|
-
|
|
422
|
+
NitroPlayerLogger.log("DownloadDatabase", " 📋 Playlist \(playlistId): \(trackIds.count) tracks")
|
|
405
423
|
}
|
|
406
424
|
} catch {
|
|
407
|
-
|
|
425
|
+
NitroPlayerLogger.log("DownloadDatabase", "❌ Failed to load playlist tracks from disk: \(error)")
|
|
408
426
|
}
|
|
409
427
|
} else {
|
|
410
|
-
|
|
428
|
+
NitroPlayerLogger.log("DownloadDatabase", "⚠️ No playlist associations found")
|
|
411
429
|
}
|
|
412
430
|
|
|
413
|
-
|
|
431
|
+
NitroPlayerLogger.log("DownloadDatabase", String(repeating: "📀", count: 40) + "\n")
|
|
414
432
|
}
|
|
415
433
|
|
|
416
434
|
// MARK: - Conversion Helpers
|
|
417
435
|
|
|
436
|
+
private func resolveAbsolutePath(for record: DownloadedTrackRecord) -> String {
|
|
437
|
+
let location: StorageLocation = record.storageLocation == "private" ? .private : .public
|
|
438
|
+
return DownloadFileManager.shared.absolutePath(forFilename: record.localPath, storageLocation: location)
|
|
439
|
+
}
|
|
440
|
+
|
|
418
441
|
/// Convert Variant_NullType_String? to String?
|
|
419
442
|
private func variantToString(_ variant: Variant_NullType_String?) -> String? {
|
|
420
443
|
guard let variant = variant else { return nil }
|
|
@@ -461,7 +484,7 @@ final class DownloadDatabase {
|
|
|
461
484
|
return DownloadedTrack(
|
|
462
485
|
trackId: record.trackId,
|
|
463
486
|
originalTrack: recordToTrackItem(record.originalTrack),
|
|
464
|
-
localPath: record
|
|
487
|
+
localPath: resolveAbsolutePath(for: record),
|
|
465
488
|
localArtworkPath: stringToVariant(record.localArtworkPath),
|
|
466
489
|
downloadedAt: record.downloadedAt,
|
|
467
490
|
fileSize: record.fileSize,
|
|
@@ -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
|
|
|
@@ -327,4 +327,11 @@ final class DownloadFileManager {
|
|
|
327
327
|
|
|
328
328
|
return nil
|
|
329
329
|
}
|
|
330
|
+
|
|
331
|
+
/// Reconstructs the current absolute path for a stored filename and storage location.
|
|
332
|
+
/// Always uses the current app container path, so it survives container UUID changes.
|
|
333
|
+
func absolutePath(forFilename filename: String, storageLocation: StorageLocation) -> String {
|
|
334
|
+
let dir = storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
|
|
335
|
+
return dir.appendingPathComponent(filename).path
|
|
336
|
+
}
|
|
330
337
|
}
|
|
@@ -372,12 +372,12 @@ final class DownloadManagerCore: NSObject {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
func getLocalPath(trackId: String) -> String? {
|
|
375
|
-
|
|
375
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🔍 getLocalPath() called for trackId: \(trackId)")
|
|
376
376
|
if let downloadedTrack = DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) {
|
|
377
|
-
|
|
377
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
|
|
378
378
|
return downloadedTrack.localPath
|
|
379
379
|
} else {
|
|
380
|
-
|
|
380
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ❌ No downloaded track found for trackId: \(trackId)")
|
|
381
381
|
return nil
|
|
382
382
|
}
|
|
383
383
|
}
|
|
@@ -406,9 +406,7 @@ final class DownloadManagerCore: NSObject {
|
|
|
406
406
|
func syncDownloads() -> Int {
|
|
407
407
|
let removedFromDb = DownloadDatabase.shared.syncDownloads()
|
|
408
408
|
let bytesFreed = DownloadFileManager.shared.cleanupOrphanedFiles()
|
|
409
|
-
|
|
410
|
-
"🔄 DownloadManagerCore: syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes"
|
|
411
|
-
)
|
|
409
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🔄 syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes")
|
|
412
410
|
return removedFromDb
|
|
413
411
|
}
|
|
414
412
|
|
|
@@ -426,27 +424,27 @@ final class DownloadManagerCore: NSObject {
|
|
|
426
424
|
|
|
427
425
|
func getEffectiveUrl(track: TrackItem) -> String {
|
|
428
426
|
let preference = getPlaybackSourcePreference()
|
|
429
|
-
|
|
430
|
-
|
|
427
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🔍 getEffectiveUrl() for track: \(track.id)")
|
|
428
|
+
NitroPlayerLogger.log("DownloadManagerCore", " Playback preference: \(preference)")
|
|
431
429
|
|
|
432
430
|
switch preference {
|
|
433
431
|
case .network:
|
|
434
|
-
|
|
432
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Using network URL (preference=network)")
|
|
435
433
|
return track.url
|
|
436
434
|
case .download:
|
|
437
435
|
if let localPath = getLocalPath(trackId: track.id) {
|
|
438
|
-
|
|
436
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
|
|
439
437
|
return localPath
|
|
440
438
|
} else {
|
|
441
|
-
|
|
439
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, falling back to network URL")
|
|
442
440
|
return track.url
|
|
443
441
|
}
|
|
444
442
|
case .auto:
|
|
445
443
|
if let localPath = getLocalPath(trackId: track.id) {
|
|
446
|
-
|
|
444
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Using local path: \(localPath)")
|
|
447
445
|
return localPath
|
|
448
446
|
} else {
|
|
449
|
-
|
|
447
|
+
NitroPlayerLogger.log("DownloadManagerCore", " → Local path not found, using network URL")
|
|
450
448
|
return track.url
|
|
451
449
|
}
|
|
452
450
|
}
|
|
@@ -507,7 +505,7 @@ final class DownloadManagerCore: NSObject {
|
|
|
507
505
|
|
|
508
506
|
/// Load persisted track metadata and playlist associations (survives app restart)
|
|
509
507
|
private func loadPersistedMetadata() {
|
|
510
|
-
|
|
508
|
+
NitroPlayerLogger.log("DownloadManagerCore", "📦 Loading persisted metadata...")
|
|
511
509
|
|
|
512
510
|
// Load track metadata
|
|
513
511
|
if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
|
|
@@ -516,24 +514,24 @@ final class DownloadManagerCore: NSObject {
|
|
|
516
514
|
for (trackId, record) in records {
|
|
517
515
|
trackMetadata[trackId] = recordToTrackItem(record)
|
|
518
516
|
}
|
|
519
|
-
|
|
517
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(trackMetadata.count) track metadata entries")
|
|
520
518
|
} catch {
|
|
521
|
-
|
|
519
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load track metadata: \(error)")
|
|
522
520
|
}
|
|
523
521
|
} else {
|
|
524
|
-
|
|
522
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted track metadata found")
|
|
525
523
|
}
|
|
526
524
|
|
|
527
525
|
// Load playlist associations
|
|
528
526
|
if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
|
|
529
527
|
do {
|
|
530
528
|
playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
|
|
531
|
-
|
|
529
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ✅ Loaded \(playlistAssociations.count) playlist associations")
|
|
532
530
|
} catch {
|
|
533
|
-
|
|
531
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ❌ Failed to load playlist associations: \(error)")
|
|
534
532
|
}
|
|
535
533
|
} else {
|
|
536
|
-
|
|
534
|
+
NitroPlayerLogger.log("DownloadManagerCore", " ⚠️ No persisted playlist associations found")
|
|
537
535
|
}
|
|
538
536
|
}
|
|
539
537
|
|
|
@@ -552,7 +550,7 @@ final class DownloadManagerCore: NSObject {
|
|
|
552
550
|
let playlistData = try JSONEncoder().encode(playlistAssociations)
|
|
553
551
|
UserDefaults.standard.set(playlistData, forKey: Self.playlistAssociationsKey)
|
|
554
552
|
} catch {
|
|
555
|
-
|
|
553
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save metadata: \(error)")
|
|
556
554
|
}
|
|
557
555
|
}
|
|
558
556
|
|
|
@@ -657,24 +655,22 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
657
655
|
_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
658
656
|
didFinishDownloadingTo location: URL
|
|
659
657
|
) {
|
|
660
|
-
|
|
658
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🎯 didFinishDownloadingTo called")
|
|
661
659
|
|
|
662
660
|
guard let description = downloadTask.taskDescription else {
|
|
663
|
-
|
|
661
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ No task description")
|
|
664
662
|
return
|
|
665
663
|
}
|
|
666
664
|
let parts = description.split(separator: "|")
|
|
667
665
|
guard parts.count == 2 else {
|
|
668
|
-
|
|
666
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ Invalid task description format: \(description)")
|
|
669
667
|
return
|
|
670
668
|
}
|
|
671
669
|
|
|
672
670
|
let downloadId = String(parts[0])
|
|
673
671
|
let trackId = String(parts[1])
|
|
674
672
|
|
|
675
|
-
|
|
676
|
-
"🎯 DownloadManagerCore: Processing completion for downloadId=\(downloadId), trackId=\(trackId)"
|
|
677
|
-
)
|
|
673
|
+
NitroPlayerLogger.log("DownloadManagerCore", "🎯 Processing completion for downloadId=\(downloadId), trackId=\(trackId)")
|
|
678
674
|
|
|
679
675
|
// IMPORTANT: Move file SYNCHRONOUSLY - the temp file is deleted after this method returns!
|
|
680
676
|
// Get storage location and original URL from track metadata
|
|
@@ -698,7 +694,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
698
694
|
// Now handle the rest asynchronously
|
|
699
695
|
queue.async(flags: .barrier) {
|
|
700
696
|
guard let destinationPath = destinationPath else {
|
|
701
|
-
|
|
697
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ Failed to save file for trackId=\(trackId)")
|
|
702
698
|
self.taskMetadata[downloadId]?.state = .failed
|
|
703
699
|
self.taskMetadata[downloadId]?.error = DownloadError(
|
|
704
700
|
code: "FILE_MOVE_FAILED",
|
|
@@ -712,11 +708,11 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
712
708
|
return
|
|
713
709
|
}
|
|
714
710
|
|
|
715
|
-
|
|
711
|
+
NitroPlayerLogger.log("DownloadManagerCore", "✅ File saved to \(destinationPath)")
|
|
716
712
|
|
|
717
713
|
guard let track = self.trackMetadata[trackId] else {
|
|
718
|
-
|
|
719
|
-
|
|
714
|
+
NitroPlayerLogger.log("DownloadManagerCore", "❌ No track metadata for trackId=\(trackId)")
|
|
715
|
+
NitroPlayerLogger.log("DownloadManagerCore", " Available trackIds: \(Array(self.trackMetadata.keys))")
|
|
720
716
|
|
|
721
717
|
// Still mark as completed even if we don't have metadata
|
|
722
718
|
self.taskMetadata[downloadId]?.state = .completed
|
|
@@ -747,7 +743,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
747
743
|
// Save to database
|
|
748
744
|
DownloadDatabase.shared.saveDownloadedTrack(downloadedTrack, playlistId: playlistId)
|
|
749
745
|
|
|
750
|
-
|
|
746
|
+
NitroPlayerLogger.log("DownloadManagerCore", "✅ Track saved to database")
|
|
751
747
|
|
|
752
748
|
// Clean up persisted metadata (no longer needed after completion)
|
|
753
749
|
self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
|
|
@@ -760,7 +756,7 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
760
756
|
self.activeTasks.removeValue(forKey: downloadId)
|
|
761
757
|
|
|
762
758
|
// Notify
|
|
763
|
-
|
|
759
|
+
NitroPlayerLogger.log("DownloadManagerCore", "✅ Notifying completion for trackId=\(trackId)")
|
|
764
760
|
self.notifyStateChange(
|
|
765
761
|
downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
|
|
766
762
|
self.notifyComplete(downloadedTrack)
|