react-native-nitro-player 0.5.1 → 0.5.3
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
CHANGED
|
@@ -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,
|
|
@@ -71,12 +71,13 @@ final class DownloadDatabase {
|
|
|
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
|
-
print("✅ DownloadDatabase: Track \(trackId) IS downloaded at \(
|
|
77
|
+
print("✅ DownloadDatabase: Track \(trackId) IS downloaded at \(absolutePath)")
|
|
77
78
|
} else {
|
|
78
79
|
print(
|
|
79
|
-
"❌ DownloadDatabase: Track \(trackId) record exists but file NOT found at \(
|
|
80
|
+
"❌ DownloadDatabase: Track \(trackId) record exists but file NOT found at \(absolutePath)"
|
|
80
81
|
)
|
|
81
82
|
}
|
|
82
83
|
return exists
|
|
@@ -129,10 +130,11 @@ final class DownloadDatabase {
|
|
|
129
130
|
return nil
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
134
|
+
print(" Found record, checking file at: \(absolutePath)")
|
|
133
135
|
|
|
134
136
|
// Verify file still exists
|
|
135
|
-
guard FileManager.default.fileExists(atPath:
|
|
137
|
+
guard FileManager.default.fileExists(atPath: absolutePath) else {
|
|
136
138
|
print(" ❌ File does NOT exist, cleaning up record")
|
|
137
139
|
// File was deleted externally, clean up record
|
|
138
140
|
queue.async(flags: .barrier) {
|
|
@@ -156,8 +158,9 @@ final class DownloadDatabase {
|
|
|
156
158
|
var invalidTrackIds: [String] = []
|
|
157
159
|
|
|
158
160
|
for (trackId, record) in downloadedTracks {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
162
|
+
print(" Checking track \(trackId) at path: \(absolutePath)")
|
|
163
|
+
if FileManager.default.fileExists(atPath: absolutePath) {
|
|
161
164
|
print(" ✅ File exists")
|
|
162
165
|
validTracks.append(recordToDownloadedTrack(record))
|
|
163
166
|
} else {
|
|
@@ -241,8 +244,9 @@ final class DownloadDatabase {
|
|
|
241
244
|
var trackIdsToRemove: [String] = []
|
|
242
245
|
|
|
243
246
|
for (trackId, record) in downloadedTracks {
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
let absolutePath = resolveAbsolutePath(for: record)
|
|
248
|
+
if !FileManager.default.fileExists(atPath: absolutePath) {
|
|
249
|
+
print(" ❌ Missing file for track \(trackId): \(absolutePath)")
|
|
246
250
|
trackIdsToRemove.append(trackId)
|
|
247
251
|
}
|
|
248
252
|
}
|
|
@@ -283,7 +287,7 @@ final class DownloadDatabase {
|
|
|
283
287
|
guard let record = self.downloadedTracks[trackId] else { return }
|
|
284
288
|
|
|
285
289
|
// Delete the file
|
|
286
|
-
DownloadFileManager.shared.deleteFile(at: record
|
|
290
|
+
DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
|
|
287
291
|
|
|
288
292
|
// Delete artwork if exists
|
|
289
293
|
if let artworkPath = record.localArtworkPath {
|
|
@@ -314,7 +318,7 @@ final class DownloadDatabase {
|
|
|
314
318
|
// Delete all tracks in the playlist
|
|
315
319
|
for trackId in trackIds {
|
|
316
320
|
if let record = self.downloadedTracks[trackId] {
|
|
317
|
-
DownloadFileManager.shared.deleteFile(at: record
|
|
321
|
+
DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
|
|
318
322
|
if let artworkPath = record.localArtworkPath {
|
|
319
323
|
DownloadFileManager.shared.deleteFile(at: artworkPath)
|
|
320
324
|
}
|
|
@@ -333,7 +337,7 @@ final class DownloadDatabase {
|
|
|
333
337
|
queue.async(flags: .barrier) {
|
|
334
338
|
// Delete all files
|
|
335
339
|
for record in self.downloadedTracks.values {
|
|
336
|
-
DownloadFileManager.shared.deleteFile(at: record
|
|
340
|
+
DownloadFileManager.shared.deleteFile(at: self.resolveAbsolutePath(for: record))
|
|
337
341
|
if let artworkPath = record.localArtworkPath {
|
|
338
342
|
DownloadFileManager.shared.deleteFile(at: artworkPath)
|
|
339
343
|
}
|
|
@@ -376,12 +380,30 @@ final class DownloadDatabase {
|
|
|
376
380
|
[String: DownloadedTrackRecord].self, from: tracksData)
|
|
377
381
|
print("✅ DownloadDatabase: Loaded \(self.downloadedTracks.count) tracks from disk")
|
|
378
382
|
|
|
383
|
+
// Migrate absolute paths → filenames (one-time, for existing installs)
|
|
384
|
+
var needsMigration = false
|
|
385
|
+
for (trackId, record) in self.downloadedTracks {
|
|
386
|
+
if record.localPath.contains("/") {
|
|
387
|
+
let filename = URL(fileURLWithPath: record.localPath).lastPathComponent
|
|
388
|
+
self.downloadedTracks[trackId] = DownloadedTrackRecord(
|
|
389
|
+
trackId: record.trackId,
|
|
390
|
+
originalTrack: record.originalTrack,
|
|
391
|
+
localPath: filename,
|
|
392
|
+
localArtworkPath: record.localArtworkPath,
|
|
393
|
+
downloadedAt: record.downloadedAt,
|
|
394
|
+
fileSize: record.fileSize,
|
|
395
|
+
storageLocation: record.storageLocation
|
|
396
|
+
)
|
|
397
|
+
needsMigration = true
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if needsMigration { self.saveToDisk() }
|
|
401
|
+
|
|
379
402
|
// Log each downloaded track
|
|
380
403
|
for (trackId, record) in self.downloadedTracks {
|
|
381
404
|
print(" 📥 \(trackId)")
|
|
382
405
|
print(" Title: \(record.originalTrack.title)")
|
|
383
|
-
print(" Path: \(record.localPath)")
|
|
384
|
-
print(" Exists: \(FileManager.default.fileExists(atPath: record.localPath))")
|
|
406
|
+
print(" Path (filename): \(record.localPath)")
|
|
385
407
|
}
|
|
386
408
|
} catch {
|
|
387
409
|
print("❌ DownloadDatabase: Failed to load tracks from disk: \(error)")
|
|
@@ -415,6 +437,11 @@ final class DownloadDatabase {
|
|
|
415
437
|
|
|
416
438
|
// MARK: - Conversion Helpers
|
|
417
439
|
|
|
440
|
+
private func resolveAbsolutePath(for record: DownloadedTrackRecord) -> String {
|
|
441
|
+
let location: StorageLocation = record.storageLocation == "private" ? .private : .public
|
|
442
|
+
return DownloadFileManager.shared.absolutePath(forFilename: record.localPath, storageLocation: location)
|
|
443
|
+
}
|
|
444
|
+
|
|
418
445
|
/// Convert Variant_NullType_String? to String?
|
|
419
446
|
private func variantToString(_ variant: Variant_NullType_String?) -> String? {
|
|
420
447
|
guard let variant = variant else { return nil }
|
|
@@ -461,7 +488,7 @@ final class DownloadDatabase {
|
|
|
461
488
|
return DownloadedTrack(
|
|
462
489
|
trackId: record.trackId,
|
|
463
490
|
originalTrack: recordToTrackItem(record.originalTrack),
|
|
464
|
-
localPath: record
|
|
491
|
+
localPath: resolveAbsolutePath(for: record),
|
|
465
492
|
localArtworkPath: stringToVariant(record.localArtworkPath),
|
|
466
493
|
downloadedAt: record.downloadedAt,
|
|
467
494
|
fileSize: record.fileSize,
|
|
@@ -57,7 +57,8 @@ final class DownloadFileManager {
|
|
|
57
57
|
func saveDownloadedFile(
|
|
58
58
|
from temporaryLocation: URL, trackId: String, storageLocation: StorageLocation,
|
|
59
59
|
originalURL: String? = nil,
|
|
60
|
-
suggestedFilename: String? = nil
|
|
60
|
+
suggestedFilename: String? = nil,
|
|
61
|
+
httpResponse: HTTPURLResponse? = nil
|
|
61
62
|
) -> String? {
|
|
62
63
|
print("🎯 DownloadFileManager: saveDownloadedFile called for trackId=\(trackId)")
|
|
63
64
|
print(" From: \(temporaryLocation.path)")
|
|
@@ -68,21 +69,12 @@ final class DownloadFileManager {
|
|
|
68
69
|
storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
|
|
69
70
|
print(" Destination directory: \(destinationDirectory.path)")
|
|
70
71
|
|
|
71
|
-
// Determine file extension
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if !pathExtension.isEmpty {
|
|
78
|
-
fileExtension = pathExtension
|
|
79
|
-
}
|
|
80
|
-
} else if let originalURL = originalURL, let url = URL(string: originalURL) {
|
|
81
|
-
let pathExtension = url.pathExtension.lowercased()
|
|
82
|
-
if !pathExtension.isEmpty {
|
|
83
|
-
fileExtension = pathExtension
|
|
84
|
-
}
|
|
85
|
-
}
|
|
72
|
+
// Determine file extension using headers first, then URL path, then default
|
|
73
|
+
let fileExtension = Self.resolveFileExtension(
|
|
74
|
+
httpResponse: httpResponse,
|
|
75
|
+
suggestedFilename: suggestedFilename,
|
|
76
|
+
originalURL: originalURL
|
|
77
|
+
)
|
|
86
78
|
print(" File extension: \(fileExtension)")
|
|
87
79
|
|
|
88
80
|
let fileName = "\(trackId).\(fileExtension)"
|
|
@@ -113,6 +105,95 @@ final class DownloadFileManager {
|
|
|
113
105
|
}
|
|
114
106
|
}
|
|
115
107
|
|
|
108
|
+
// MARK: - Extension Resolution
|
|
109
|
+
|
|
110
|
+
private static let mimeTypeToExtension: [String: String] = [
|
|
111
|
+
"audio/mpeg": "mp3",
|
|
112
|
+
"audio/mp3": "mp3",
|
|
113
|
+
"audio/mp4": "m4a",
|
|
114
|
+
"audio/m4a": "m4a",
|
|
115
|
+
"audio/x-m4a": "m4a",
|
|
116
|
+
"audio/aac": "aac",
|
|
117
|
+
"audio/ogg": "ogg",
|
|
118
|
+
"audio/flac": "flac",
|
|
119
|
+
"audio/x-flac": "flac",
|
|
120
|
+
"audio/wav": "wav",
|
|
121
|
+
"audio/x-wav": "wav",
|
|
122
|
+
"audio/webm": "webm",
|
|
123
|
+
"audio/opus": "opus",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
/// Resolves the audio file extension from HTTP response headers, suggested filename, or URL.
|
|
127
|
+
/// Priority: Content-Disposition filename → Content-Type MIME → suggestedFilename → URL path ext → "mp3"
|
|
128
|
+
private static func resolveFileExtension(
|
|
129
|
+
httpResponse: HTTPURLResponse?,
|
|
130
|
+
suggestedFilename: String?,
|
|
131
|
+
originalURL: String?
|
|
132
|
+
) -> String {
|
|
133
|
+
// 1. Content-Disposition: attachment; filename="track.mp3"
|
|
134
|
+
if let disposition = httpResponse?.value(forHTTPHeaderField: "Content-Disposition") {
|
|
135
|
+
if let ext = extensionFromContentDisposition(disposition), !ext.isEmpty {
|
|
136
|
+
print(" [ExtResolve] Content-Disposition → .\(ext)")
|
|
137
|
+
return ext
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Content-Type MIME type
|
|
142
|
+
if let contentType = httpResponse?.value(forHTTPHeaderField: "Content-Type") {
|
|
143
|
+
let mime = contentType.split(separator: ";").first.map(String.init)?.trimmingCharacters(in: .whitespaces) ?? contentType
|
|
144
|
+
if let ext = mimeTypeToExtension[mime.lowercased()] {
|
|
145
|
+
print(" [ExtResolve] Content-Type '\(mime)' → .\(ext)")
|
|
146
|
+
return ext
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Suggested filename from URLSession
|
|
151
|
+
if let name = suggestedFilename, !name.isEmpty {
|
|
152
|
+
let ext = URL(fileURLWithPath: name).pathExtension.lowercased()
|
|
153
|
+
if !ext.isEmpty && isAudioExtension(ext) {
|
|
154
|
+
print(" [ExtResolve] suggestedFilename → .\(ext)")
|
|
155
|
+
return ext
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 4. URL path extension (only if it looks like an audio format, not e.g. ".view")
|
|
160
|
+
if let urlString = originalURL, let url = URL(string: urlString) {
|
|
161
|
+
let ext = url.pathExtension.lowercased()
|
|
162
|
+
if !ext.isEmpty && isAudioExtension(ext) {
|
|
163
|
+
print(" [ExtResolve] URL path ext → .\(ext)")
|
|
164
|
+
return ext
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
print(" [ExtResolve] fallback → .mp3")
|
|
169
|
+
return "mp3"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private static func extensionFromContentDisposition(_ disposition: String) -> String? {
|
|
173
|
+
// Match: filename="foo.mp3" or filename=foo.mp3
|
|
174
|
+
let patterns = [
|
|
175
|
+
#"filename\*?=(?:UTF-8'')?\"?([^\";\r\n]+)\"?"#,
|
|
176
|
+
]
|
|
177
|
+
for pattern in patterns {
|
|
178
|
+
if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
|
|
179
|
+
let range = NSRange(disposition.startIndex..., in: disposition)
|
|
180
|
+
if let match = regex.firstMatch(in: disposition, range: range),
|
|
181
|
+
let filenameRange = Range(match.range(at: 1), in: disposition)
|
|
182
|
+
{
|
|
183
|
+
let filename = String(disposition[filenameRange]).trimmingCharacters(in: .whitespaces)
|
|
184
|
+
let ext = URL(fileURLWithPath: filename).pathExtension.lowercased()
|
|
185
|
+
if !ext.isEmpty { return ext }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return nil
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static func isAudioExtension(_ ext: String) -> Bool {
|
|
193
|
+
let audioExtensions: Set<String> = ["mp3", "m4a", "aac", "ogg", "flac", "wav", "webm", "opus", "mp4"]
|
|
194
|
+
return audioExtensions.contains(ext)
|
|
195
|
+
}
|
|
196
|
+
|
|
116
197
|
func deleteFile(at path: String) {
|
|
117
198
|
do {
|
|
118
199
|
if fileManager.fileExists(atPath: path) {
|
|
@@ -246,4 +327,11 @@ final class DownloadFileManager {
|
|
|
246
327
|
|
|
247
328
|
return nil
|
|
248
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
|
+
}
|
|
249
337
|
}
|
|
@@ -682,15 +682,17 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
|
682
682
|
(self.config.storageLocation ?? .private, self.trackMetadata[trackId]?.url)
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
-
// Get suggested filename from response
|
|
685
|
+
// Get suggested filename and HTTP headers from response
|
|
686
686
|
let suggestedFilename = downloadTask.response?.suggestedFilename
|
|
687
|
+
let httpResponse = downloadTask.response as? HTTPURLResponse
|
|
687
688
|
|
|
688
689
|
let destinationPath = DownloadFileManager.shared.saveDownloadedFile(
|
|
689
690
|
from: location,
|
|
690
691
|
trackId: trackId,
|
|
691
692
|
storageLocation: storageLocation,
|
|
692
693
|
originalURL: originalURL,
|
|
693
|
-
suggestedFilename: suggestedFilename
|
|
694
|
+
suggestedFilename: suggestedFilename,
|
|
695
|
+
httpResponse: httpResponse
|
|
694
696
|
)
|
|
695
697
|
|
|
696
698
|
// Now handle the rest asynchronously
|
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.3",
|
|
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",
|