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.
- package/android/build.gradle +111 -0
- package/android/src/main/AndroidManifest.xml +44 -0
- package/android/src/main/java/com/reactnativemp3/Mp3Package.kt +23 -0
- package/android/src/main/java/com/reactnativemp3/Mp3TurboModule.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/database/MusicDatabase.kt +48 -0
- package/android/src/main/java/com/reactnativemp3/database/dao/SongDao.kt +72 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/PlaylistEntity.kt +58 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/SongEntity.kt +104 -0
- package/android/src/main/java/com/reactnativemp3/database/entities/ThumbnailCacheEntity.kt +43 -0
- package/android/src/main/java/com/reactnativemp3/managers/CacheManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/managers/EqualizerManager.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/modules/MetadataModule.kt +330 -0
- package/android/src/main/java/com/reactnativemp3/modules/NotificationModule.kt +236 -0
- package/android/src/main/java/com/reactnativemp3/modules/PlayerModule.kt +710 -0
- package/android/src/main/java/com/reactnativemp3/modules/ScannerModule.kt +640 -0
- package/android/src/main/java/com/reactnativemp3/services/AudioFocusService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/FileObserverService.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/services/MusicService.kt +309 -0
- package/android/src/main/java/com/reactnativemp3/utils/MediaStoreUtils.kt +0 -0
- package/android/src/main/java/com/reactnativemp3/utils/PermissionUtils.kt +0 -0
- package/android/src/main/jni/Mp3TurboModule.cpp +29 -0
- package/android/src/main/res/drawable/ic_music_note.xml +11 -0
- package/android/src/main/res/drawable/ic_pause.xml +11 -0
- package/android/src/main/res/drawable/ic_play.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_next.xml +11 -0
- package/android/src/main/res/drawable/ic_skip_previous.xml +11 -0
- package/android/src/main/res/drawable/ic_stop.xml +11 -0
- package/lib/components/MusicList.d.ts +0 -0
- package/lib/components/MusicList.js +1 -0
- package/lib/components/MusicPlayerUI.d.ts +0 -0
- package/lib/components/MusicPlayerUI.js +1 -0
- package/lib/hooks/useMusicPlayer.d.ts +38 -0
- package/lib/hooks/useMusicPlayer.js +242 -0
- package/lib/hooks/useMusicScanner.d.ts +27 -0
- package/lib/hooks/useMusicScanner.js +217 -0
- package/lib/hooks/usePermissions.d.ts +9 -0
- package/lib/hooks/usePermissions.js +55 -0
- package/lib/index.d.ts +144 -0
- package/lib/index.js +148 -0
- package/lib/types/common.types.d.ts +79 -0
- package/lib/types/common.types.js +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +2 -0
- package/lib/types/player.types.d.ts +35 -0
- package/lib/types/player.types.js +2 -0
- package/lib/types/scanner.types.d.ts +29 -0
- package/lib/types/scanner.types.js +2 -0
- package/lib/utils/constants.d.ts +31 -0
- package/lib/utils/constants.js +55 -0
- package/lib/utils/events.d.ts +0 -0
- package/lib/utils/events.js +1 -0
- package/package.json +62 -0
- package/src/components/MusicList.tsx +0 -0
- package/src/components/MusicPlayerUI.tsx +0 -0
- package/src/hooks/useMusicPlayer.ts +358 -0
- package/src/hooks/useMusicScanner.ts +286 -0
- package/src/hooks/usePermissions.ts +64 -0
- package/src/index.ts +214 -0
- package/src/types/common.types.ts +86 -0
- package/src/types/index.ts +4 -0
- package/src/types/player.types.ts +37 -0
- package/src/types/scanner.types.ts +31 -0
- package/src/utils/constants.ts +56 -0
- 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
|
+
}
|
|
File without changes
|
|
File without changes
|