react-native-mp3 0.1.0

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.
Files changed (64) hide show
  1. package/android/build.gradle +111 -0
  2. package/android/src/main/AndroidManifest.xml +44 -0
  3. package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
  4. package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
  5. package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
  6. package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
  7. package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
  8. package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
  9. package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
  10. package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
  11. package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
  12. package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
  13. package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
  14. package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
  15. package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
  16. package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
  17. package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
  18. package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
  19. package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
  20. package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
  21. package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
  22. package/android/src/main/res/drawable/ic_music_note.xml +11 -0
  23. package/android/src/main/res/drawable/ic_pause.xml +11 -0
  24. package/android/src/main/res/drawable/ic_play.xml +11 -0
  25. package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
  26. package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
  27. package/android/src/main/res/drawable/ic_stop.xml +11 -0
  28. package/lib/components/MusicList.d.ts +0 -0
  29. package/lib/components/MusicList.js +1 -0
  30. package/lib/components/MusicPlayerUI.d.ts +0 -0
  31. package/lib/components/MusicPlayerUI.js +1 -0
  32. package/lib/hooks/useMusicPlayer.d.ts +38 -0
  33. package/lib/hooks/useMusicPlayer.js +242 -0
  34. package/lib/hooks/useMusicScanner.d.ts +27 -0
  35. package/lib/hooks/useMusicScanner.js +217 -0
  36. package/lib/hooks/usePermissions.d.ts +9 -0
  37. package/lib/hooks/usePermissions.js +55 -0
  38. package/lib/index.d.ts +144 -0
  39. package/lib/index.js +148 -0
  40. package/lib/types/common.types.d.ts +79 -0
  41. package/lib/types/common.types.js +2 -0
  42. package/lib/types/index.d.ts +3 -0
  43. package/lib/types/index.js +2 -0
  44. package/lib/types/player.types.d.ts +35 -0
  45. package/lib/types/player.types.js +2 -0
  46. package/lib/types/scanner.types.d.ts +29 -0
  47. package/lib/types/scanner.types.js +2 -0
  48. package/lib/utils/constants.d.ts +31 -0
  49. package/lib/utils/constants.js +55 -0
  50. package/lib/utils/events.d.ts +0 -0
  51. package/lib/utils/events.js +1 -0
  52. package/package.json +62 -0
  53. package/src/components/MusicList.tsx +0 -0
  54. package/src/components/MusicPlayerUI.tsx +0 -0
  55. package/src/hooks/useMusicPlayer.ts +358 -0
  56. package/src/hooks/useMusicScanner.ts +286 -0
  57. package/src/hooks/usePermissions.ts +64 -0
  58. package/src/index.ts +214 -0
  59. package/src/types/common.types.ts +86 -0
  60. package/src/types/index.ts +4 -0
  61. package/src/types/player.types.ts +37 -0
  62. package/src/types/scanner.types.ts +31 -0
  63. package/src/utils/constants.ts +56 -0
  64. package/src/utils/events.ts +0 -0
@@ -0,0 +1,640 @@
1
+ package com.reactnativemp3.modules
2
+
3
+ import android.Manifest
4
+ import android.content.ContentUris
5
+ import android.content.pm.PackageManager
6
+ import android.database.Cursor
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import android.os.Environment
10
+ import android.provider.MediaStore
11
+ import android.provider.Settings
12
+ import androidx.core.content.ContextCompat
13
+ import com.facebook.react.bridge.*
14
+ import com.facebook.react.modules.core.DeviceEventManagerModule
15
+ import com.reactnativemp3.Mp3TurboModule
16
+ import com.reactnativemp3.database.MusicDatabase
17
+ import com.reactnativemp3.database.entities.SongEntity
18
+ import com.reactnativemp3.utils.MediaStoreUtils
19
+ import kotlinx.coroutines.*
20
+ import java.io.File
21
+ import java.util.*
22
+
23
+ @ReactModule(name = ScannerModule.NAME)
24
+ class ScannerModule(reactContext: ReactApplicationContext) :
25
+ Mp3TurboModule(reactContext, NAME) {
26
+
27
+ companion object {
28
+ const val NAME = "ScannerModule"
29
+
30
+ private const val PERMISSION_REQUEST_CODE = 1001
31
+ private const val EVENT_SCAN_PROGRESS = "SCAN_PROGRESS"
32
+ private const val EVENT_SCAN_COMPLETE = "SCAN_COMPLETE"
33
+ private const val EVENT_FILE_ADDED = "FILE_ADDED"
34
+ private const val EVENT_FILE_DELETED = "FILE_DELETED"
35
+ private const val EVENT_PERMISSION_STATUS = "PERMISSION_STATUS"
36
+ }
37
+
38
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
39
+ private lateinit var database: MusicDatabase
40
+
41
+ override fun initialize() {
42
+ super.initialize()
43
+ database = MusicDatabase.getInstance(reactApplicationContext)
44
+ }
45
+
46
+ @ReactMethod
47
+ fun requestStoragePermission(promise: Promise) {
48
+ val context = reactApplicationContext ?: run {
49
+ promise.reject("NO_CONTEXT", "React context not available")
50
+ return
51
+ }
52
+
53
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
54
+ // Android 11+ requires MANAGE_EXTERNAL_STORAGE
55
+ if (Environment.isExternalStorageManager()) {
56
+ promise.resolve(true)
57
+ emitPermissionStatus(true)
58
+ return
59
+ }
60
+
61
+ try {
62
+ val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
63
+ intent.addCategory("android.intent.category.DEFAULT")
64
+ intent.data = Uri.parse("package:${context.packageName}")
65
+
66
+ val activity = context.currentActivity
67
+ if (activity != null) {
68
+ activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
69
+ // We'll resolve the promise when permission is granted via callback
70
+ promise.resolve(null)
71
+ } else {
72
+ promise.reject("NO_ACTIVITY", "No activity available to request permission")
73
+ }
74
+ } catch (e: Exception) {
75
+ promise.reject("PERMISSION_ERROR", "Failed to request permission", e)
76
+ }
77
+ } else {
78
+ // Android 10 and below
79
+ val permissions = arrayOf(
80
+ Manifest.permission.READ_EXTERNAL_STORAGE,
81
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
82
+ )
83
+
84
+ val granted = permissions.all {
85
+ ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
86
+ }
87
+
88
+ if (granted) {
89
+ promise.resolve(true)
90
+ emitPermissionStatus(true)
91
+ } else {
92
+ val activity = context.currentActivity
93
+ if (activity != null) {
94
+ activity.requestPermissions(permissions, PERMISSION_REQUEST_CODE)
95
+ promise.resolve(null)
96
+ } else {
97
+ promise.reject("NO_ACTIVITY", "No activity available to request permission")
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ @ReactMethod
104
+ fun checkPermissionStatus(promise: Promise) {
105
+ val context = reactApplicationContext ?: run {
106
+ promise.reject("NO_CONTEXT", "React context not available")
107
+ return
108
+ }
109
+
110
+ val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
111
+ Environment.isExternalStorageManager()
112
+ } else {
113
+ ContextCompat.checkSelfPermission(
114
+ context,
115
+ Manifest.permission.READ_EXTERNAL_STORAGE
116
+ ) == PackageManager.PERMISSION_GRANTED &&
117
+ ContextCompat.checkSelfPermission(
118
+ context,
119
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
120
+ ) == PackageManager.PERMISSION_GRANTED
121
+ }
122
+
123
+ promise.resolve(hasPermission)
124
+ emitPermissionStatus(hasPermission)
125
+ }
126
+
127
+ @ReactMethod
128
+ fun scanMusicFiles(options: ReadableMap?, promise: Promise) {
129
+ val context = reactApplicationContext ?: run {
130
+ promise.reject("NO_CONTEXT", "React context not available")
131
+ return
132
+ }
133
+
134
+ scope.launch {
135
+ try {
136
+ val forceRescan = options?.getBoolean("forceRescan") ?: false
137
+ val scanPaths = if (options?.hasKey("paths") == true) {
138
+ val pathsArray = options.getArray("paths")
139
+ val paths = mutableListOf<String>()
140
+ for (i in 0 until pathsArray!!.size()) {
141
+ paths.add(pathsArray.getString(i)!!)
142
+ }
143
+ paths
144
+ } else {
145
+ getDefaultMusicPaths()
146
+ }
147
+
148
+ val totalSongs = scanAndSaveToDatabase(context, scanPaths, forceRescan)
149
+
150
+ withContext(Dispatchers.Main) {
151
+ val result = Arguments.createMap()
152
+ result.putInt("totalSongs", totalSongs)
153
+ result.putStringArray("scanPaths", scanPaths.toTypedArray())
154
+ promise.resolve(result)
155
+
156
+ // Emit completion event
157
+ emitEvent(EVENT_SCAN_COMPLETE, result)
158
+ }
159
+ } catch (e: Exception) {
160
+ withContext(Dispatchers.Main) {
161
+ promise.reject("SCAN_ERROR", "Failed to scan music files", e)
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ @ReactMethod
168
+ fun getCachedSongs(options: ReadableMap?, promise: Promise) {
169
+ scope.launch {
170
+ try {
171
+ val limit = options?.getInt("limit") ?: 0
172
+ val offset = options?.getInt("offset") ?: 0
173
+ val sortBy = options?.getString("sortBy") ?: "title"
174
+ val sortOrder = options?.getString("sortOrder") ?: "asc"
175
+ val filter = options?.getString("filter") // album, artist, genre, favorite
176
+ val filterValue = options?.getString("filterValue")
177
+
178
+ val songs = when (filter) {
179
+ "album" -> database.songDao().getSongsByAlbum(filterValue ?: "").first()
180
+ "artist" -> {
181
+ // Need to implement this in DAO
182
+ database.songDao().getAllSongs().first()
183
+ .filter { it.artist == filterValue }
184
+ }
185
+ "genre" -> {
186
+ database.songDao().getAllSongs().first()
187
+ .filter { it.genre == filterValue }
188
+ }
189
+ "favorite" -> database.songDao().getFavorites().first()
190
+ else -> database.songDao().getAllSongs().first()
191
+ }
192
+
193
+ // Apply sorting
194
+ val sortedSongs = when (sortBy) {
195
+ "title" -> if (sortOrder == "asc")
196
+ songs.sortedBy { it.title.lowercase(Locale.getDefault()) }
197
+ else
198
+ songs.sortedByDescending { it.title.lowercase(Locale.getDefault()) }
199
+ "artist" -> if (sortOrder == "asc")
200
+ songs.sortedBy { it.artist.lowercase(Locale.getDefault()) }
201
+ else
202
+ songs.sortedByDescending { it.artist.lowercase(Locale.getDefault()) }
203
+ "album" -> if (sortOrder == "asc")
204
+ songs.sortedBy { it.album.lowercase(Locale.getDefault()) }
205
+ else
206
+ songs.sortedByDescending { it.album.lowercase(Locale.getDefault()) }
207
+ "dateAdded" -> if (sortOrder == "asc")
208
+ songs.sortedBy { it.dateAdded }
209
+ else
210
+ songs.sortedByDescending { it.dateAdded }
211
+ "duration" -> if (sortOrder == "asc")
212
+ songs.sortedBy { it.duration }
213
+ else
214
+ songs.sortedByDescending { it.duration }
215
+ else -> songs
216
+ }
217
+
218
+ // Apply pagination
219
+ val paginatedSongs = if (limit > 0) {
220
+ sortedSongs.drop(offset).take(limit)
221
+ } else {
222
+ sortedSongs
223
+ }
224
+
225
+ val result = Arguments.createArray()
226
+ paginatedSongs.forEach { song ->
227
+ result.pushMap(convertSongToMap(song))
228
+ }
229
+
230
+ withContext(Dispatchers.Main) {
231
+ promise.resolve(result)
232
+ }
233
+ } catch (e: Exception) {
234
+ withContext(Dispatchers.Main) {
235
+ promise.reject("DB_ERROR", "Failed to get cached songs", e)
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ @ReactMethod
242
+ fun deleteSong(songId: String, promise: Promise) {
243
+ scope.launch {
244
+ try {
245
+ val song = database.songDao().getById(songId)
246
+ if (song == null) {
247
+ withContext(Dispatchers.Main) {
248
+ promise.reject("NOT_FOUND", "Song not found")
249
+ }
250
+ return@launch
251
+ }
252
+
253
+ val file = File(song.path)
254
+ val deleted = if (file.exists()) {
255
+ file.delete()
256
+ } else {
257
+ false
258
+ }
259
+
260
+ if (deleted) {
261
+ database.songDao().deleteById(songId)
262
+
263
+ // Emit deletion event
264
+ val eventParams = Arguments.createMap()
265
+ eventParams.putString("songId", songId)
266
+ eventParams.putString("path", song.path)
267
+ emitEvent(EVENT_FILE_DELETED, eventParams)
268
+
269
+ withContext(Dispatchers.Main) {
270
+ promise.resolve(true)
271
+ }
272
+ } else {
273
+ withContext(Dispatchers.Main) {
274
+ promise.reject("DELETE_FAILED", "Failed to delete file")
275
+ }
276
+ }
277
+ } catch (e: Exception) {
278
+ withContext(Dispatchers.Main) {
279
+ promise.reject("DELETE_ERROR", "Error deleting song", e)
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ @ReactMethod
286
+ fun startFileMonitoring() {
287
+ // Implementation for real-time file monitoring
288
+ // Will use FileObserver and ContentObserver
289
+ // This is a placeholder - actual implementation requires Service
290
+ }
291
+
292
+ @ReactMethod
293
+ fun stopFileMonitoring() {
294
+ // Stop monitoring
295
+ }
296
+
297
+ @ReactMethod
298
+ fun getAlbums(promise: Promise) {
299
+ scope.launch {
300
+ try {
301
+ val albums = database.songDao().getAllAlbums().first()
302
+ val result = Arguments.createArray()
303
+ albums.forEach { albumName ->
304
+ val albumSongs = database.songDao().getSongsByAlbum(albumName).first()
305
+ val albumMap = Arguments.createMap()
306
+ albumMap.putString("name", albumName)
307
+ albumMap.putInt("songCount", albumSongs.size)
308
+
309
+ // Get album art from first song
310
+ val albumArtUri = albumSongs.firstOrNull()?.albumArtUri
311
+ albumMap.putString("albumArtUri", albumArtUri)
312
+
313
+ result.pushMap(albumMap)
314
+ }
315
+
316
+ withContext(Dispatchers.Main) {
317
+ promise.resolve(result)
318
+ }
319
+ } catch (e: Exception) {
320
+ withContext(Dispatchers.Main) {
321
+ promise.reject("DB_ERROR", "Failed to get albums", e)
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ @ReactMethod
328
+ fun getArtists(promise: Promise) {
329
+ scope.launch {
330
+ try {
331
+ val artists = database.songDao().getAllArtists().first()
332
+ val result = Arguments.createArray()
333
+ artists.forEach { artistName ->
334
+ val artistSongs = database.songDao().getAllSongs().first()
335
+ .filter { it.artist == artistName }
336
+
337
+ val artistMap = Arguments.createMap()
338
+ artistMap.putString("name", artistName)
339
+ artistMap.putInt("songCount", artistSongs.size)
340
+ artistMap.putInt("albumCount", artistSongs.map { it.album }.distinct().size)
341
+ result.pushMap(artistMap)
342
+ }
343
+
344
+ withContext(Dispatchers.Main) {
345
+ promise.resolve(result)
346
+ }
347
+ } catch (e: Exception) {
348
+ withContext(Dispatchers.Main) {
349
+ promise.reject("DB_ERROR", "Failed to get artists", e)
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ @ReactMethod
356
+ fun searchSongs(query: String, promise: Promise) {
357
+ scope.launch {
358
+ try {
359
+ val songs = database.songDao().search("%$query%").first()
360
+ val result = Arguments.createArray()
361
+ songs.forEach { song ->
362
+ result.pushMap(convertSongToMap(song))
363
+ }
364
+
365
+ withContext(Dispatchers.Main) {
366
+ promise.resolve(result)
367
+ }
368
+ } catch (e: Exception) {
369
+ withContext(Dispatchers.Main) {
370
+ promise.reject("SEARCH_ERROR", "Failed to search songs", e)
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ @ReactMethod
377
+ fun addToFavorites(songId: String, promise: Promise) {
378
+ scope.launch {
379
+ try {
380
+ database.songDao().setFavorite(songId, true)
381
+ withContext(Dispatchers.Main) {
382
+ promise.resolve(true)
383
+ }
384
+ } catch (e: Exception) {
385
+ withContext(Dispatchers.Main) {
386
+ promise.reject("DB_ERROR", "Failed to add to favorites", e)
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ @ReactMethod
393
+ fun removeFromFavorites(songId: String, promise: Promise) {
394
+ scope.launch {
395
+ try {
396
+ database.songDao().setFavorite(songId, false)
397
+ withContext(Dispatchers.Main) {
398
+ promise.resolve(true)
399
+ }
400
+ } catch (e: Exception) {
401
+ withContext(Dispatchers.Main) {
402
+ promise.reject("DB_ERROR", "Failed to remove from favorites", e)
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ @ReactMethod
409
+ fun clearCache(promise: Promise) {
410
+ scope.launch {
411
+ try {
412
+ // Note: This only clears our database cache, not the actual files
413
+ // For actual file deletion, use deleteSong method
414
+ database.clearAllTables()
415
+ withContext(Dispatchers.Main) {
416
+ promise.resolve(true)
417
+ }
418
+ } catch (e: Exception) {
419
+ withContext(Dispatchers.Main) {
420
+ promise.reject("CLEAR_ERROR", "Failed to clear cache", e)
421
+ }
422
+ }
423
+ }
424
+ }
425
+
426
+ override fun onCatalystInstanceDestroy() {
427
+ super.onCatalystInstanceDestroy()
428
+ scope.cancel()
429
+ }
430
+
431
+ // Private helper methods
432
+
433
+ private suspend fun scanAndSaveToDatabase(
434
+ context: android.content.Context,
435
+ scanPaths: List<String>,
436
+ forceRescan: Boolean
437
+ ): Int {
438
+ val songs = mutableListOf<SongEntity>()
439
+
440
+ // Query MediaStore for audio files
441
+ val projection = arrayOf(
442
+ MediaStore.Audio.Media._ID,
443
+ MediaStore.Audio.Media.TITLE,
444
+ MediaStore.Audio.Media.ARTIST,
445
+ MediaStore.Audio.Media.ALBUM,
446
+ MediaStore.Audio.Media.DURATION,
447
+ MediaStore.Audio.Media.DATA,
448
+ MediaStore.Audio.Media.SIZE,
449
+ MediaStore.Audio.Media.MIME_TYPE,
450
+ MediaStore.Audio.Media.DATE_ADDED,
451
+ MediaStore.Audio.Media.DATE_MODIFIED,
452
+ MediaStore.Audio.Media.TRACK,
453
+ MediaStore.Audio.Media.YEAR,
454
+ MediaStore.Audio.Media.GENRE,
455
+ MediaStore.Audio.Media.ALBUM_ID,
456
+ MediaStore.Audio.Media.COMPOSER
457
+ )
458
+
459
+ val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0"
460
+ val sortOrder = "${MediaStore.Audio.Media.DATE_ADDED} DESC"
461
+
462
+ val cursor: Cursor? = context.contentResolver.query(
463
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
464
+ projection,
465
+ selection,
466
+ null,
467
+ sortOrder
468
+ )
469
+
470
+ cursor?.use { c ->
471
+ val idColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
472
+ val titleColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
473
+ val artistColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
474
+ val albumColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
475
+ val durationColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
476
+ val pathColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)
477
+ val sizeColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
478
+ val mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE)
479
+ val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED)
480
+ val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_MODIFIED)
481
+ val trackColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK)
482
+ val yearColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.YEAR)
483
+ val genreColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.GENRE)
484
+ val albumIdColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
485
+ val composerColumn = c.getColumnIndexOrThrow(MediaStore.Audio.Media.COMPOSER)
486
+
487
+ var processed = 0
488
+ val total = c.count
489
+
490
+ while (c.moveToNext()) {
491
+ val path = c.getString(pathColumn)
492
+
493
+ // Filter by scan paths if provided
494
+ if (scanPaths.isNotEmpty() && !isInScanPath(path, scanPaths)) {
495
+ continue
496
+ }
497
+
498
+ val id = c.getLong(idColumn)
499
+ val title = c.getString(titleColumn) ?: "Unknown"
500
+ val artist = c.getString(artistColumn) ?: "Unknown Artist"
501
+ val album = c.getString(albumColumn) ?: "Unknown Album"
502
+ val duration = c.getLong(durationColumn)
503
+ val size = c.getLong(sizeColumn)
504
+ val mimeType = c.getString(mimeTypeColumn) ?: "audio/mpeg"
505
+ val dateAdded = c.getLong(dateAddedColumn) * 1000 // Convert to milliseconds
506
+ val dateModified = c.getLong(dateModifiedColumn) * 1000
507
+ val trackNumber = c.getInt(trackColumn)
508
+ val year = if (!c.isNull(yearColumn)) c.getInt(yearColumn) else null
509
+ val genre = c.getString(genreColumn)
510
+ val albumId = c.getLong(albumIdColumn)
511
+ val composer = c.getString(composerColumn)
512
+
513
+ // Get album art URI
514
+ val albumArtUri = if (albumId > 0) {
515
+ val albumArtUri = Uri.parse("content://media/external/audio/albumart")
516
+ ContentUris.withAppendedId(albumArtUri, albumId).toString()
517
+ } else {
518
+ null
519
+ }
520
+
521
+ // Generate unique ID
522
+ val songId = SongEntity.generateId(path)
523
+
524
+ val song = SongEntity(
525
+ id = songId,
526
+ title = title,
527
+ artist = artist,
528
+ album = album,
529
+ duration = duration,
530
+ path = path,
531
+ uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
532
+ .buildUpon()
533
+ .appendPath(id.toString())
534
+ .build()
535
+ .toString(),
536
+ albumArtUri = albumArtUri,
537
+ size = size,
538
+ mimeType = mimeType,
539
+ trackNumber = trackNumber,
540
+ year = year,
541
+ genre = genre,
542
+ dateAdded = dateAdded,
543
+ lastModified = dateModified,
544
+ composer = composer
545
+ )
546
+
547
+ songs.add(song)
548
+ processed++
549
+
550
+ // Emit progress update every 10 songs
551
+ if (processed % 10 == 0) {
552
+ val progress = Arguments.createMap()
553
+ progress.putInt("processed", processed)
554
+ progress.putInt("total", total)
555
+ progress.putDouble("percentage", (processed.toDouble() / total) * 100)
556
+ emitEvent(EVENT_SCAN_PROGRESS, progress)
557
+ }
558
+ }
559
+ }
560
+
561
+ // Save to database
562
+ database.songDao().insertAll(songs)
563
+
564
+ return songs.size
565
+ }
566
+
567
+ private fun isInScanPath(filePath: String, scanPaths: List<String>): Boolean {
568
+ return scanPaths.any { filePath.startsWith(it) }
569
+ }
570
+
571
+ private fun getDefaultMusicPaths(): List<String> {
572
+ val paths = mutableListOf<String>()
573
+
574
+ // Common music directories
575
+ val musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
576
+ if (musicDir.exists()) {
577
+ paths.add(musicDir.absolutePath)
578
+ }
579
+
580
+ // Add Download directory (users often download music there)
581
+ val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
582
+ if (downloadDir.exists()) {
583
+ paths.add(downloadDir.absolutePath)
584
+ }
585
+
586
+ // Add SD card paths if available
587
+ val externalDirs = reactApplicationContext?.getExternalFilesDirs(null)
588
+ externalDirs?.forEach { dir ->
589
+ if (dir != null && dir.exists() && Environment.isExternalStorageRemovable(dir)) {
590
+ paths.add(dir.absolutePath)
591
+ }
592
+ }
593
+
594
+ return paths
595
+ }
596
+
597
+ private fun convertSongToMap(song: SongEntity): WritableMap {
598
+ val map = Arguments.createMap()
599
+ map.putString("id", song.id)
600
+ map.putString("title", song.title)
601
+ map.putString("artist", song.artist)
602
+ map.putString("album", song.album)
603
+ map.putDouble("duration", song.duration.toDouble())
604
+ map.putString("path", song.path)
605
+ map.putString("uri", song.uri)
606
+ map.putString("albumArtUri", song.albumArtUri)
607
+ map.putDouble("size", song.size.toDouble())
608
+ map.putString("mimeType", song.mimeType)
609
+ map.putInt("trackNumber", song.trackNumber)
610
+ map.putInt("year", song.year ?: 0)
611
+ map.putString("genre", song.genre)
612
+ map.putDouble("dateAdded", song.dateAdded.toDouble())
613
+ map.putDouble("lastModified", song.lastModified.toDouble())
614
+ map.putBoolean("isFavorite", song.isFavorite)
615
+ map.putInt("playCount", song.playCount)
616
+ map.putDouble("lastPlayed", song.lastPlayed?.toDouble() ?: 0.0)
617
+ map.putInt("bitrate", song.bitrate ?: 0)
618
+ map.putInt("sampleRate", song.sampleRate ?: 0)
619
+ map.putInt("channels", song.channels ?: 2)
620
+ map.putString("composer", song.composer)
621
+ map.putDouble("bookmark", song.bookmark.toDouble())
622
+ map.putBoolean("isPodcast", song.isPodcast)
623
+ map.putInt("rating", song.rating ?: 0)
624
+
625
+ return map
626
+ }
627
+
628
+ private fun emitPermissionStatus(granted: Boolean) {
629
+ val params = Arguments.createMap()
630
+ params.putBoolean("granted", granted)
631
+ params.putString("message", if (granted) "Permission granted" else "Permission denied")
632
+ emitEvent(EVENT_PERMISSION_STATUS, params)
633
+ }
634
+
635
+ private fun emitEvent(eventName: String, params: WritableMap?) {
636
+ reactApplicationContext
637
+ ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
638
+ ?.emit(eventName, params)
639
+ }
640
+ }