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
@@ -2,6 +2,8 @@
2
2
 
3
3
  A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay.
4
4
 
5
+ Documentation: [https://nitroplayer.riteshshukla.in/](https://nitroplayer.riteshshukla.in/)
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -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 exists = FileManager.default.fileExists(atPath: record.localPath)
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 \(record.localPath)")
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 \(record.localPath)"
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
- print(" Found record, checking file at: \(record.localPath)")
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: record.localPath) else {
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
- print(" Checking track \(trackId) at path: \(record.localPath)")
160
- if FileManager.default.fileExists(atPath: record.localPath) {
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
- if !FileManager.default.fileExists(atPath: record.localPath) {
245
- print(" ❌ Missing file for track \(trackId): \(record.localPath)")
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.localPath)
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.localPath)
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.localPath)
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.localPath,
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
- var fileExtension = "mp3" // Default fallback
73
-
74
- if let suggestedFilename = suggestedFilename, !suggestedFilename.isEmpty {
75
- let url = URL(fileURLWithPath: suggestedFilename)
76
- let pathExtension = url.pathExtension.lowercased()
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.1",
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",