@roitium/expo-orpheus 0.3.1 → 0.4.1
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/src/main/AndroidManifest.xml +16 -3
- package/android/src/main/java/expo/modules/orpheus/DownloadCache.kt +18 -6
- package/android/src/main/java/expo/modules/orpheus/ExpoOrpheusModule.kt +180 -30
- package/android/src/main/java/expo/modules/orpheus/OrpheusDownloadService.kt +76 -0
- package/android/src/main/java/expo/modules/orpheus/{OrpheusService.kt → OrpheusMusicService.kt} +31 -63
- package/android/src/main/java/expo/modules/orpheus/utils/DownloadUtil.kt +134 -0
- package/android/src/main/java/expo/modules/orpheus/utils/MediaItemStorer.kt +1 -0
- package/android/src/main/res/drawable/baseline_download_24.xml +5 -0
- package/build/ExpoOrpheusModule.d.ts +42 -0
- package/build/ExpoOrpheusModule.d.ts.map +1 -1
- package/build/ExpoOrpheusModule.js +10 -0
- package/build/ExpoOrpheusModule.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoOrpheusModule.ts +50 -0
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
<manifest xmlns:
|
|
1
|
+
<manifest xmlns:tools="http://schemas.android.com/tools"
|
|
2
|
+
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
3
|
|
|
3
4
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
4
5
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
5
6
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
7
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
8
|
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
6
9
|
|
|
7
10
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
8
11
|
|
|
9
12
|
<application>
|
|
10
13
|
<service
|
|
11
|
-
android:name=".
|
|
14
|
+
android:name=".OrpheusMusicService"
|
|
12
15
|
android:enabled="true"
|
|
13
16
|
android:exported="true"
|
|
14
|
-
android:foregroundServiceType="mediaPlayback"
|
|
17
|
+
android:foregroundServiceType="mediaPlayback"
|
|
18
|
+
tools:ignore="ExportedService">
|
|
15
19
|
<intent-filter>
|
|
16
20
|
<action android:name="androidx.media3.session.MediaLibraryService" />
|
|
17
21
|
<action android:name="android.media.browse.MediaBrowserService" />
|
|
18
22
|
</intent-filter>
|
|
19
23
|
</service>
|
|
24
|
+
<service
|
|
25
|
+
android:name=".OrpheusDownloadService"
|
|
26
|
+
android:exported="false"
|
|
27
|
+
android:foregroundServiceType="dataSync">
|
|
28
|
+
<intent-filter>
|
|
29
|
+
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
|
30
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
31
|
+
</intent-filter>
|
|
32
|
+
</service>
|
|
20
33
|
</application>
|
|
21
34
|
</manifest>
|
|
@@ -4,22 +4,34 @@ import android.content.Context
|
|
|
4
4
|
import androidx.media3.common.util.UnstableApi
|
|
5
5
|
import androidx.media3.database.StandaloneDatabaseProvider
|
|
6
6
|
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
|
7
|
+
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
|
7
8
|
import androidx.media3.datasource.cache.SimpleCache
|
|
8
9
|
import java.io.File
|
|
9
10
|
|
|
10
11
|
@UnstableApi
|
|
11
12
|
object DownloadCache {
|
|
12
|
-
private var
|
|
13
|
+
private var lruCache: SimpleCache? = null
|
|
14
|
+
private var stableCache: SimpleCache? = null
|
|
13
15
|
|
|
14
|
-
@
|
|
15
|
-
fun
|
|
16
|
-
if (
|
|
16
|
+
@Synchronized
|
|
17
|
+
fun getLruCache(context: Context): SimpleCache {
|
|
18
|
+
if (lruCache == null) {
|
|
17
19
|
val cacheDir = File(context.cacheDir, "media_cache")
|
|
18
20
|
val evictor = LeastRecentlyUsedCacheEvictor(256 * 1024 * 1024)
|
|
19
21
|
val databaseProvider = StandaloneDatabaseProvider(context)
|
|
22
|
+
lruCache = SimpleCache(cacheDir, evictor, databaseProvider)
|
|
23
|
+
}
|
|
24
|
+
return lruCache!!
|
|
25
|
+
}
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
@Synchronized
|
|
28
|
+
fun getStableCache(context: Context): SimpleCache {
|
|
29
|
+
if (stableCache == null) {
|
|
30
|
+
val cacheDir = File(context.filesDir, "media_download")
|
|
31
|
+
val evictor = NoOpCacheEvictor()
|
|
32
|
+
val databaseProvider = StandaloneDatabaseProvider(context)
|
|
33
|
+
stableCache = SimpleCache(cacheDir, evictor, databaseProvider)
|
|
22
34
|
}
|
|
23
|
-
return
|
|
35
|
+
return stableCache!!
|
|
24
36
|
}
|
|
25
37
|
}
|
|
@@ -5,11 +5,18 @@ import android.os.Bundle
|
|
|
5
5
|
import android.os.Handler
|
|
6
6
|
import android.os.Looper
|
|
7
7
|
import android.util.Log
|
|
8
|
+
import androidx.annotation.OptIn
|
|
9
|
+
import androidx.core.net.toUri
|
|
8
10
|
import androidx.media3.common.C
|
|
9
11
|
import androidx.media3.common.MediaItem
|
|
10
12
|
import androidx.media3.common.PlaybackException
|
|
11
13
|
import androidx.media3.common.Player
|
|
12
14
|
import androidx.media3.common.Timeline
|
|
15
|
+
import androidx.media3.common.util.UnstableApi
|
|
16
|
+
import androidx.media3.exoplayer.offline.Download
|
|
17
|
+
import androidx.media3.exoplayer.offline.DownloadManager
|
|
18
|
+
import androidx.media3.exoplayer.offline.DownloadRequest
|
|
19
|
+
import androidx.media3.exoplayer.offline.DownloadService
|
|
13
20
|
import androidx.media3.session.MediaController
|
|
14
21
|
import androidx.media3.session.SessionCommand
|
|
15
22
|
import androidx.media3.session.SessionResult
|
|
@@ -22,9 +29,11 @@ import expo.modules.kotlin.functions.Queues
|
|
|
22
29
|
import expo.modules.kotlin.modules.Module
|
|
23
30
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
24
31
|
import expo.modules.orpheus.models.TrackRecord
|
|
32
|
+
import expo.modules.orpheus.utils.DownloadUtil
|
|
25
33
|
import expo.modules.orpheus.utils.MediaItemStorer
|
|
26
34
|
import expo.modules.orpheus.utils.toMediaItem
|
|
27
35
|
|
|
36
|
+
@UnstableApi
|
|
28
37
|
class ExpoOrpheusModule : Module() {
|
|
29
38
|
private var controllerFuture: ListenableFuture<MediaController>? = null
|
|
30
39
|
|
|
@@ -32,13 +41,17 @@ class ExpoOrpheusModule : Module() {
|
|
|
32
41
|
|
|
33
42
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
34
43
|
|
|
44
|
+
private var downloadManager: DownloadManager? = null
|
|
45
|
+
|
|
35
46
|
// 记录上一首歌曲的 ID,用于在切歌时发送给 JS
|
|
36
47
|
private var lastMediaId: String? = null
|
|
48
|
+
private var lastTrackFinishedAt: Long = 0
|
|
37
49
|
|
|
38
50
|
private val durationCache = mutableMapOf<String, Long>()
|
|
39
51
|
|
|
40
52
|
val gson = Gson()
|
|
41
53
|
|
|
54
|
+
@OptIn(UnstableApi::class)
|
|
42
55
|
override fun definition() = ModuleDefinition {
|
|
43
56
|
Name("Orpheus")
|
|
44
57
|
|
|
@@ -48,7 +61,8 @@ class ExpoOrpheusModule : Module() {
|
|
|
48
61
|
"onPositionUpdate",
|
|
49
62
|
"onIsPlayingChanged",
|
|
50
63
|
"onTrackFinished",
|
|
51
|
-
"onTrackStarted"
|
|
64
|
+
"onTrackStarted",
|
|
65
|
+
"onDownloadUpdated"
|
|
52
66
|
)
|
|
53
67
|
|
|
54
68
|
OnCreate {
|
|
@@ -56,7 +70,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
56
70
|
MediaItemStorer.initialize(context)
|
|
57
71
|
val sessionToken = SessionToken(
|
|
58
72
|
context,
|
|
59
|
-
ComponentName(context,
|
|
73
|
+
ComponentName(context, OrpheusMusicService::class.java)
|
|
60
74
|
)
|
|
61
75
|
controllerFuture = MediaController.Builder(context, sessionToken)
|
|
62
76
|
.setApplicationLooper(Looper.getMainLooper()).buildAsync()
|
|
@@ -69,12 +83,17 @@ class ExpoOrpheusModule : Module() {
|
|
|
69
83
|
e.printStackTrace()
|
|
70
84
|
}
|
|
71
85
|
}, MoreExecutors.directExecutor())
|
|
86
|
+
|
|
87
|
+
downloadManager = DownloadUtil.getDownloadManager(context)
|
|
88
|
+
downloadManager?.addListener(downloadListener)
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
OnDestroy {
|
|
75
92
|
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
76
93
|
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
94
|
+
mainHandler.removeCallbacks(downloadProgressRunnable)
|
|
77
95
|
controllerFuture?.let { MediaController.releaseFuture(it) }
|
|
96
|
+
downloadManager?.removeListener(downloadListener)
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
Constant("restorePlaybackPositionEnabled") {
|
|
@@ -85,6 +104,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
85
104
|
OrpheusConfig.bilibiliCookie = cookie
|
|
86
105
|
}
|
|
87
106
|
|
|
107
|
+
|
|
88
108
|
Function("setRestorePlaybackPositionEnabled") { enabled: Boolean ->
|
|
89
109
|
MediaItemStorer.setRestoreEnabled(enabled)
|
|
90
110
|
}
|
|
@@ -154,6 +174,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
154
174
|
AsyncFunction("clear") {
|
|
155
175
|
checkController()
|
|
156
176
|
controller?.clearMediaItems()
|
|
177
|
+
durationCache.clear()
|
|
157
178
|
}.runOnQueue(Queues.MAIN)
|
|
158
179
|
|
|
159
180
|
AsyncFunction("skipTo") { index: Int ->
|
|
@@ -271,6 +292,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
271
292
|
val player = controller ?: return@AsyncFunction
|
|
272
293
|
if (clearQueue == true) {
|
|
273
294
|
player.clearMediaItems()
|
|
295
|
+
durationCache.clear()
|
|
274
296
|
}
|
|
275
297
|
val initialSize = player.mediaItemCount
|
|
276
298
|
player.addMediaItems(mediaItems)
|
|
@@ -292,41 +314,154 @@ class ExpoOrpheusModule : Module() {
|
|
|
292
314
|
if (player.playbackState == Player.STATE_IDLE) {
|
|
293
315
|
player.prepare()
|
|
294
316
|
}
|
|
295
|
-
}
|
|
317
|
+
}
|
|
296
318
|
|
|
297
|
-
AsyncFunction("
|
|
298
|
-
|
|
299
|
-
val
|
|
319
|
+
AsyncFunction("downloadTrack") { track: TrackRecord ->
|
|
320
|
+
val context = appContext.reactContext ?: return@AsyncFunction
|
|
321
|
+
val downloadRequest = DownloadRequest.Builder(track.id, track.url.toUri())
|
|
322
|
+
.setData(gson.toJson(track).toByteArray())
|
|
323
|
+
.build()
|
|
300
324
|
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
DownloadService.sendAddDownload(
|
|
326
|
+
context,
|
|
327
|
+
OrpheusDownloadService::class.java,
|
|
328
|
+
downloadRequest,
|
|
329
|
+
false
|
|
330
|
+
)
|
|
331
|
+
}
|
|
303
332
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
AsyncFunction("multiDownload") { tracks: List<TrackRecord> ->
|
|
334
|
+
val context = appContext.reactContext ?: return@AsyncFunction
|
|
335
|
+
tracks.forEach { track ->
|
|
336
|
+
val downloadRequest = DownloadRequest.Builder(track.id, track.url.toUri())
|
|
337
|
+
.setData(gson.toJson(track).toByteArray())
|
|
338
|
+
.build()
|
|
339
|
+
DownloadService.sendAddDownload(
|
|
340
|
+
context,
|
|
341
|
+
OrpheusDownloadService::class.java,
|
|
342
|
+
downloadRequest,
|
|
343
|
+
false
|
|
344
|
+
)
|
|
310
345
|
}
|
|
346
|
+
return@AsyncFunction
|
|
347
|
+
}
|
|
311
348
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
349
|
+
AsyncFunction("removeDownload") { id: String ->
|
|
350
|
+
val context = appContext.reactContext ?: return@AsyncFunction
|
|
351
|
+
DownloadService.sendRemoveDownload(
|
|
352
|
+
context,
|
|
353
|
+
OrpheusDownloadService::class.java,
|
|
354
|
+
id,
|
|
355
|
+
false
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
AsyncFunction("removeAllDownloads") {
|
|
360
|
+
val context = appContext.reactContext ?: return@AsyncFunction null
|
|
361
|
+
DownloadService.sendRemoveAllDownloads(
|
|
362
|
+
context,
|
|
363
|
+
OrpheusDownloadService::class.java,
|
|
364
|
+
false
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
AsyncFunction("getDownloads") {
|
|
369
|
+
val context =
|
|
370
|
+
appContext.reactContext ?: return@AsyncFunction emptyList<Map<String, Any>>()
|
|
371
|
+
val downloadManager = DownloadUtil.getDownloadManager(context)
|
|
372
|
+
val downloadIndex = downloadManager.downloadIndex
|
|
373
|
+
|
|
374
|
+
val cursor = downloadIndex.getDownloads()
|
|
375
|
+
val result = ArrayList<Map<String, Any>>()
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
while (cursor.moveToNext()) {
|
|
379
|
+
val download = cursor.download
|
|
380
|
+
result.add(getDownloadMap(download))
|
|
315
381
|
}
|
|
316
|
-
|
|
382
|
+
} finally {
|
|
383
|
+
cursor.close()
|
|
384
|
+
}
|
|
385
|
+
return@AsyncFunction result
|
|
386
|
+
}
|
|
317
387
|
|
|
318
|
-
|
|
388
|
+
AsyncFunction("getDownloadStatusByIds") { ids: List<String> ->
|
|
389
|
+
val context =
|
|
390
|
+
appContext.reactContext ?: return@AsyncFunction emptyMap<String, Int>()
|
|
391
|
+
val downloadManager = DownloadUtil.getDownloadManager(context)
|
|
392
|
+
val downloadIndex = downloadManager.downloadIndex
|
|
319
393
|
|
|
320
|
-
|
|
321
|
-
val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
|
|
394
|
+
val result = mutableMapOf<String, Int>()
|
|
322
395
|
|
|
323
|
-
|
|
396
|
+
for (id in ids) {
|
|
397
|
+
val download = downloadIndex.getDownload(id)
|
|
398
|
+
if (download != null) {
|
|
399
|
+
result[id] = download.state
|
|
400
|
+
}
|
|
324
401
|
}
|
|
402
|
+
return@AsyncFunction result
|
|
403
|
+
}
|
|
404
|
+
}
|
|
325
405
|
|
|
326
|
-
|
|
327
|
-
|
|
406
|
+
private fun getDownloadMap(download: Download): Map<String, Any> {
|
|
407
|
+
val trackJson = if (download.request.data.isNotEmpty()) {
|
|
408
|
+
String(download.request.data)
|
|
409
|
+
} else null
|
|
410
|
+
|
|
411
|
+
val map = mutableMapOf<String, Any>(
|
|
412
|
+
"id" to download.request.id,
|
|
413
|
+
"state" to download.state,
|
|
414
|
+
"percentDownloaded" to download.percentDownloaded,
|
|
415
|
+
"bytesDownloaded" to download.bytesDownloaded,
|
|
416
|
+
"contentLength" to download.contentLength
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if (trackJson != null) {
|
|
420
|
+
try {
|
|
421
|
+
val track = gson.fromJson(trackJson, TrackRecord::class.java)
|
|
422
|
+
map["track"] = track
|
|
423
|
+
} catch (e: Exception) {
|
|
424
|
+
e.printStackTrace()
|
|
328
425
|
}
|
|
329
|
-
}
|
|
426
|
+
}
|
|
427
|
+
return map
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private val downloadListener = object : DownloadManager.Listener {
|
|
431
|
+
override fun onDownloadChanged(
|
|
432
|
+
downloadManager: DownloadManager,
|
|
433
|
+
download: Download,
|
|
434
|
+
finalException: Exception?
|
|
435
|
+
) {
|
|
436
|
+
sendEvent("onDownloadUpdated", getDownloadMap(download))
|
|
437
|
+
updateDownloadProgressRunnerState()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private val downloadProgressRunnable = object : Runnable {
|
|
442
|
+
override fun run() {
|
|
443
|
+
val manager = downloadManager ?: return
|
|
444
|
+
if (manager.currentDownloads.isNotEmpty()) {
|
|
445
|
+
for (download in manager.currentDownloads) {
|
|
446
|
+
if (download.state == Download.STATE_DOWNLOADING) {
|
|
447
|
+
sendEvent("onDownloadUpdated", getDownloadMap(download))
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
mainHandler.postDelayed(this, 500)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private fun updateDownloadProgressRunnerState() {
|
|
456
|
+
mainHandler.removeCallbacks(downloadProgressRunnable)
|
|
457
|
+
val manager = downloadManager ?: return
|
|
458
|
+
|
|
459
|
+
val hasActiveDownloads =
|
|
460
|
+
manager.currentDownloads.any { it.state == Download.STATE_DOWNLOADING }
|
|
461
|
+
|
|
462
|
+
if (hasActiveDownloads) {
|
|
463
|
+
mainHandler.post(downloadProgressRunnable)
|
|
464
|
+
}
|
|
330
465
|
}
|
|
331
466
|
|
|
332
467
|
private fun setupListeners() {
|
|
@@ -356,6 +491,10 @@ class ExpoOrpheusModule : Module() {
|
|
|
356
491
|
val mediaId = currentItem.mediaId
|
|
357
492
|
|
|
358
493
|
val duration = player.duration
|
|
494
|
+
Log.d(
|
|
495
|
+
"Orpheus",
|
|
496
|
+
"onTimelineChanged: reason: $reason mediaId: $mediaId duration: $duration"
|
|
497
|
+
)
|
|
359
498
|
|
|
360
499
|
if (duration != C.TIME_UNSET && duration > 0) {
|
|
361
500
|
durationCache[mediaId] = duration
|
|
@@ -367,12 +506,23 @@ class ExpoOrpheusModule : Module() {
|
|
|
367
506
|
newPosition: Player.PositionInfo,
|
|
368
507
|
reason: Int
|
|
369
508
|
) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
509
|
+
val isAutoTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
510
|
+
val isIndexChanged = oldPosition.mediaItemIndex != newPosition.mediaItemIndex
|
|
511
|
+
val lastMediaItem = oldPosition.mediaItem ?: return
|
|
512
|
+
val currentTime = System.currentTimeMillis()
|
|
513
|
+
if ((currentTime - lastTrackFinishedAt) < 200) {
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
Log.d(
|
|
518
|
+
"Orpheus",
|
|
519
|
+
"onPositionDiscontinuity: isAutoTransition:$isAutoTransition isIndexChanged: $isIndexChanged durationCache:$durationCache"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if (isAutoTransition || isIndexChanged) {
|
|
373
523
|
|
|
374
|
-
|
|
375
|
-
|
|
524
|
+
val duration = durationCache[lastMediaItem.mediaId] ?: return
|
|
525
|
+
lastTrackFinishedAt = currentTime
|
|
376
526
|
|
|
377
527
|
sendEvent(
|
|
378
528
|
"onTrackFinished", mapOf(
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package expo.modules.orpheus
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Notification
|
|
5
|
+
import android.app.PendingIntent
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import androidx.annotation.RequiresPermission
|
|
8
|
+
import androidx.core.net.toUri
|
|
9
|
+
import androidx.media3.common.util.UnstableApi
|
|
10
|
+
import androidx.media3.exoplayer.offline.Download
|
|
11
|
+
import androidx.media3.exoplayer.offline.DownloadManager
|
|
12
|
+
import androidx.media3.exoplayer.offline.DownloadService
|
|
13
|
+
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
|
14
|
+
import androidx.media3.exoplayer.scheduler.Scheduler
|
|
15
|
+
import expo.modules.orpheus.utils.DownloadUtil
|
|
16
|
+
|
|
17
|
+
@UnstableApi
|
|
18
|
+
class OrpheusDownloadService : DownloadService(
|
|
19
|
+
FOREGROUND_NOTIFICATION_ID,
|
|
20
|
+
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
|
21
|
+
CHANNEL_ID,
|
|
22
|
+
androidx.media3.exoplayer.R.string.exo_download_notification_channel_name,
|
|
23
|
+
0
|
|
24
|
+
) {
|
|
25
|
+
companion object {
|
|
26
|
+
const val FOREGROUND_NOTIFICATION_ID = 114514
|
|
27
|
+
const val CHANNEL_ID = "orpheus_download_channel"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun getDownloadManager(): DownloadManager {
|
|
31
|
+
return DownloadUtil.getDownloadManager(this)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@RequiresPermission(Manifest.permission.RECEIVE_BOOT_COMPLETED)
|
|
35
|
+
override fun getScheduler(): Scheduler {
|
|
36
|
+
return PlatformScheduler(this, 114514)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun getForegroundNotification(
|
|
40
|
+
downloads: MutableList<Download>,
|
|
41
|
+
notMetRequirements: Int
|
|
42
|
+
): Notification {
|
|
43
|
+
var launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
44
|
+
|
|
45
|
+
if (launchIntent == null) {
|
|
46
|
+
launchIntent = Intent().apply {
|
|
47
|
+
setClassName(packageName, "$packageName.MainActivity")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
launchIntent.apply {
|
|
52
|
+
action = Intent.ACTION_VIEW
|
|
53
|
+
data = "orpheus://downloads".toUri()
|
|
54
|
+
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
val contentIntent = launchIntent.let {
|
|
58
|
+
PendingIntent.getActivity(
|
|
59
|
+
this,
|
|
60
|
+
0,
|
|
61
|
+
it,
|
|
62
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return DownloadUtil.getDownloadNotificationHelper(this)
|
|
67
|
+
.buildProgressNotification(
|
|
68
|
+
this,
|
|
69
|
+
R.drawable.baseline_download_24,
|
|
70
|
+
contentIntent,
|
|
71
|
+
null,
|
|
72
|
+
downloads,
|
|
73
|
+
notMetRequirements
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
package/android/src/main/java/expo/modules/orpheus/{OrpheusService.kt → OrpheusMusicService.kt}
RENAMED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package expo.modules.orpheus
|
|
2
2
|
|
|
3
|
+
import android.app.PendingIntent
|
|
4
|
+
import android.content.Intent
|
|
3
5
|
import android.os.Bundle
|
|
4
6
|
import androidx.annotation.OptIn
|
|
5
7
|
import androidx.core.net.toUri
|
|
@@ -9,10 +11,6 @@ import androidx.media3.common.Player
|
|
|
9
11
|
import androidx.media3.common.Timeline
|
|
10
12
|
import androidx.media3.common.util.Log
|
|
11
13
|
import androidx.media3.common.util.UnstableApi
|
|
12
|
-
import androidx.media3.datasource.DataSpec
|
|
13
|
-
import androidx.media3.datasource.DefaultHttpDataSource
|
|
14
|
-
import androidx.media3.datasource.ResolvingDataSource
|
|
15
|
-
import androidx.media3.datasource.cache.CacheDataSource
|
|
16
14
|
import androidx.media3.exoplayer.ExoPlayer
|
|
17
15
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
18
16
|
import androidx.media3.session.MediaLibraryService
|
|
@@ -21,12 +19,11 @@ import androidx.media3.session.SessionCommand
|
|
|
21
19
|
import androidx.media3.session.SessionResult
|
|
22
20
|
import com.google.common.util.concurrent.Futures
|
|
23
21
|
import com.google.common.util.concurrent.ListenableFuture
|
|
24
|
-
import expo.modules.orpheus.
|
|
22
|
+
import expo.modules.orpheus.utils.DownloadUtil
|
|
25
23
|
import expo.modules.orpheus.utils.MediaItemStorer
|
|
26
24
|
import expo.modules.orpheus.utils.SleepTimeController
|
|
27
|
-
import java.io.IOException
|
|
28
25
|
|
|
29
|
-
class
|
|
26
|
+
class OrpheusMusicService : MediaLibraryService() {
|
|
30
27
|
|
|
31
28
|
private var player: ExoPlayer? = null
|
|
32
29
|
private var mediaSession: MediaLibrarySession? = null
|
|
@@ -39,64 +36,11 @@ class OrpheusService : MediaLibraryService() {
|
|
|
39
36
|
MediaItemStorer.initialize(this)
|
|
40
37
|
|
|
41
38
|
|
|
42
|
-
val
|
|
43
|
-
.setUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
|
|
44
|
-
.setAllowCrossProtocolRedirects(true)
|
|
45
|
-
|
|
46
|
-
val cacheDataSourceFactory = CacheDataSource.Factory()
|
|
47
|
-
.setCache(DownloadCache.get(this))
|
|
48
|
-
.setUpstreamDataSourceFactory(httpDataSourceFactory)
|
|
49
|
-
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
50
|
-
|
|
51
|
-
val resolvingDataSourceFactory = ResolvingDataSource.Factory(
|
|
52
|
-
cacheDataSourceFactory,
|
|
53
|
-
object : ResolvingDataSource.Resolver {
|
|
54
|
-
// TODO: maybe we need to add a cache?
|
|
55
|
-
override fun resolveDataSpec(dataSpec: DataSpec): DataSpec {
|
|
56
|
-
val uri = dataSpec.uri
|
|
57
|
-
|
|
58
|
-
// orpheus://bilibili?bvid=bv123124&cid=114514&quality=30280&dolby=0&hires=0
|
|
59
|
-
if (uri.scheme == "orpheus" && uri.host == "bilibili") {
|
|
60
|
-
try {
|
|
61
|
-
val bvid = uri.getQueryParameter("bvid")
|
|
62
|
-
val cid = uri.getQueryParameter("cid")?.toLongOrNull()
|
|
63
|
-
val quality = uri.getQueryParameter("quality")?.toIntOrNull() ?: 30280
|
|
64
|
-
val enableDolby = uri.getQueryParameter("dolby") == "1"
|
|
65
|
-
val enableHiRes = uri.getQueryParameter("hires") == "1"
|
|
66
|
-
|
|
67
|
-
if (bvid == null) {
|
|
68
|
-
throw IOException("Invalid Bilibili Params: bvid=$bvid, cid=$cid")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
val realUrl = BilibiliRepository.resolveAudioUrl(
|
|
72
|
-
bvid = bvid,
|
|
73
|
-
cid = cid,
|
|
74
|
-
audioQuality = quality,
|
|
75
|
-
enableDolby = enableDolby,
|
|
76
|
-
enableHiRes = enableHiRes,
|
|
77
|
-
cookie = OrpheusConfig.bilibiliCookie
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
val headers = HashMap<String, String>()
|
|
81
|
-
headers["Referer"] = "https://www.bilibili.com/"
|
|
82
|
-
|
|
83
|
-
return dataSpec.buildUpon()
|
|
84
|
-
.setUri(realUrl.toUri())
|
|
85
|
-
.setHttpRequestHeaders(headers)
|
|
86
|
-
.setKey(uri.toString())
|
|
87
|
-
.build()
|
|
88
|
-
} catch (e: Exception) {
|
|
89
|
-
throw IOException("Resolve Url Failed: ${e.message}", e)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return dataSpec
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
)
|
|
39
|
+
val dataSourceFactory = DownloadUtil.getPlayerDataSourceFactory(this)
|
|
97
40
|
|
|
98
41
|
val mediaSourceFactory = DefaultMediaSourceFactory(this)
|
|
99
|
-
.setDataSourceFactory(
|
|
42
|
+
.setDataSourceFactory(dataSourceFactory)
|
|
43
|
+
|
|
100
44
|
|
|
101
45
|
player = ExoPlayer.Builder(this)
|
|
102
46
|
.setMediaSourceFactory(mediaSourceFactory)
|
|
@@ -111,8 +55,32 @@ class OrpheusService : MediaLibraryService() {
|
|
|
111
55
|
|
|
112
56
|
setupListeners()
|
|
113
57
|
|
|
58
|
+
var launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
59
|
+
|
|
60
|
+
if (launchIntent == null) {
|
|
61
|
+
launchIntent = Intent().apply {
|
|
62
|
+
setClassName(packageName, "$packageName.MainActivity")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
launchIntent.apply {
|
|
67
|
+
action = Intent.ACTION_VIEW
|
|
68
|
+
data = "orpheus://player".toUri()
|
|
69
|
+
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val contentIntent = launchIntent.let {
|
|
73
|
+
PendingIntent.getActivity(
|
|
74
|
+
this,
|
|
75
|
+
0,
|
|
76
|
+
it,
|
|
77
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
114
81
|
mediaSession = MediaLibrarySession.Builder(this, player!!, callback)
|
|
115
82
|
.setId("OrpheusSession")
|
|
83
|
+
.setSessionActivity(contentIntent)
|
|
116
84
|
.build()
|
|
117
85
|
|
|
118
86
|
restorePlayerState(MediaItemStorer.isRestoreEnabled())
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
package expo.modules.orpheus.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.core.net.toUri
|
|
5
|
+
import androidx.media3.common.util.UnstableApi
|
|
6
|
+
import androidx.media3.database.StandaloneDatabaseProvider
|
|
7
|
+
import androidx.media3.datasource.DataSource
|
|
8
|
+
import androidx.media3.datasource.DataSpec
|
|
9
|
+
import androidx.media3.datasource.DefaultHttpDataSource
|
|
10
|
+
import androidx.media3.datasource.ResolvingDataSource
|
|
11
|
+
import androidx.media3.datasource.cache.CacheDataSource
|
|
12
|
+
import androidx.media3.exoplayer.offline.DownloadManager
|
|
13
|
+
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
|
14
|
+
import androidx.media3.exoplayer.scheduler.Requirements
|
|
15
|
+
import expo.modules.orpheus.DownloadCache
|
|
16
|
+
import expo.modules.orpheus.OrpheusConfig
|
|
17
|
+
import expo.modules.orpheus.OrpheusDownloadService
|
|
18
|
+
import expo.modules.orpheus.bilibili.BilibiliRepository
|
|
19
|
+
import java.io.IOException
|
|
20
|
+
import java.util.concurrent.Executors
|
|
21
|
+
|
|
22
|
+
@UnstableApi
|
|
23
|
+
object DownloadUtil {
|
|
24
|
+
private var downloadManager: DownloadManager? = null
|
|
25
|
+
|
|
26
|
+
private var playerDataSourceFactory: DataSource.Factory? = null
|
|
27
|
+
private var downloadDataSourceFactory: DataSource.Factory? = null
|
|
28
|
+
|
|
29
|
+
private var downloadNotificationHelper: DownloadNotificationHelper? = null
|
|
30
|
+
|
|
31
|
+
@Synchronized
|
|
32
|
+
fun getDownloadManager(context: Context): DownloadManager {
|
|
33
|
+
if (downloadManager == null) {
|
|
34
|
+
val databaseProvider = StandaloneDatabaseProvider(context)
|
|
35
|
+
downloadManager = DownloadManager(
|
|
36
|
+
context,
|
|
37
|
+
databaseProvider,
|
|
38
|
+
DownloadCache.getStableCache(context),
|
|
39
|
+
getDownloadDataSourceFactory(),
|
|
40
|
+
Executors.newFixedThreadPool(6)
|
|
41
|
+
).apply {
|
|
42
|
+
maxParallelDownloads = 3
|
|
43
|
+
requirements = Requirements(0)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return downloadManager!!
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Synchronized
|
|
50
|
+
fun getPlayerDataSourceFactory(context: Context): DataSource.Factory {
|
|
51
|
+
if (playerDataSourceFactory == null) {
|
|
52
|
+
val upstreamFactory = getUpstreamFactory()
|
|
53
|
+
|
|
54
|
+
val lruCache = DownloadCache.getLruCache(context)
|
|
55
|
+
val downloadCache = DownloadCache.getStableCache(context)
|
|
56
|
+
|
|
57
|
+
val lruFactory = CacheDataSource.Factory()
|
|
58
|
+
.setCache(lruCache)
|
|
59
|
+
.setUpstreamDataSourceFactory(upstreamFactory)
|
|
60
|
+
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
61
|
+
|
|
62
|
+
val downloadFactory = CacheDataSource.Factory()
|
|
63
|
+
.setCache(downloadCache)
|
|
64
|
+
.setUpstreamDataSourceFactory(lruFactory)
|
|
65
|
+
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
66
|
+
.setCacheWriteDataSinkFactory(null)
|
|
67
|
+
|
|
68
|
+
playerDataSourceFactory = downloadFactory
|
|
69
|
+
}
|
|
70
|
+
return playerDataSourceFactory!!
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@Synchronized
|
|
75
|
+
private fun getDownloadDataSourceFactory(): DataSource.Factory {
|
|
76
|
+
if (downloadDataSourceFactory == null) {
|
|
77
|
+
downloadDataSourceFactory = getUpstreamFactory()
|
|
78
|
+
}
|
|
79
|
+
return downloadDataSourceFactory!!
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun getUpstreamFactory(): DataSource.Factory {
|
|
83
|
+
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
|
84
|
+
.setUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
|
|
85
|
+
.setAllowCrossProtocolRedirects(true)
|
|
86
|
+
|
|
87
|
+
return ResolvingDataSource.Factory(
|
|
88
|
+
httpDataSourceFactory,
|
|
89
|
+
BilibiliResolver()
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Synchronized
|
|
94
|
+
fun getDownloadNotificationHelper(context: Context): DownloadNotificationHelper {
|
|
95
|
+
if (downloadNotificationHelper == null) {
|
|
96
|
+
downloadNotificationHelper =
|
|
97
|
+
DownloadNotificationHelper(context, OrpheusDownloadService.CHANNEL_ID)
|
|
98
|
+
}
|
|
99
|
+
return downloadNotificationHelper!!
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private class BilibiliResolver : ResolvingDataSource.Resolver {
|
|
103
|
+
override fun resolveDataSpec(dataSpec: DataSpec): DataSpec {
|
|
104
|
+
val uri = dataSpec.uri
|
|
105
|
+
if (uri.scheme == "orpheus" && uri.host == "bilibili") {
|
|
106
|
+
try {
|
|
107
|
+
val bvid = uri.getQueryParameter("bvid")
|
|
108
|
+
val cid = uri.getQueryParameter("cid")?.toLongOrNull()
|
|
109
|
+
val quality = uri.getQueryParameter("quality")?.toIntOrNull() ?: 30280
|
|
110
|
+
val realUrl = BilibiliRepository.resolveAudioUrl(
|
|
111
|
+
bvid = bvid!!,
|
|
112
|
+
cid = cid,
|
|
113
|
+
audioQuality = quality,
|
|
114
|
+
enableDolby = uri.getQueryParameter("dolby") == "1",
|
|
115
|
+
enableHiRes = uri.getQueryParameter("hires") == "1",
|
|
116
|
+
cookie = OrpheusConfig.bilibiliCookie
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
val headers = HashMap<String, String>()
|
|
120
|
+
headers["Referer"] = "https://www.bilibili.com/"
|
|
121
|
+
|
|
122
|
+
return dataSpec.buildUpon()
|
|
123
|
+
.setUri(realUrl.toUri())
|
|
124
|
+
.setHttpRequestHeaders(headers)
|
|
125
|
+
.setKey(uri.toString())
|
|
126
|
+
.build()
|
|
127
|
+
} catch (e: Exception) {
|
|
128
|
+
throw IOException("Resolve Url Failed: ${e.message}", e)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return dataSpec
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
|
2
|
+
|
|
3
|
+
<path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
|
|
4
|
+
|
|
5
|
+
</vector>
|
|
@@ -53,6 +53,7 @@ export type OrpheusEvents = {
|
|
|
53
53
|
onIsPlayingChanged(event: {
|
|
54
54
|
status: boolean;
|
|
55
55
|
}): void;
|
|
56
|
+
onDownloadUpdated(event: DownloadTask): void;
|
|
56
57
|
};
|
|
57
58
|
declare class OrpheusModule extends NativeModule<OrpheusEvents> {
|
|
58
59
|
restorePlaybackPositionEnabled: boolean;
|
|
@@ -129,6 +130,47 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
|
|
|
129
130
|
*/
|
|
130
131
|
getSleepTimerEndTime(): Promise<number | null>;
|
|
131
132
|
cancelSleepTimer(): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* 下载单首歌曲
|
|
135
|
+
*/
|
|
136
|
+
downloadTrack(track: Track): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* 移除下载任务
|
|
139
|
+
*/
|
|
140
|
+
removeDownload(id: string): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* 批量下载歌曲
|
|
143
|
+
*/
|
|
144
|
+
multiDownload(tracks: Track[]): Promise<void>;
|
|
145
|
+
/**
|
|
146
|
+
* 移除所有下载任务
|
|
147
|
+
*/
|
|
148
|
+
removeAllDownloads(): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* 获取所有下载任务
|
|
151
|
+
*/
|
|
152
|
+
getDownloads(): Promise<DownloadTask[]>;
|
|
153
|
+
/**
|
|
154
|
+
* 批量返回指定 ID 的下载状态
|
|
155
|
+
*/
|
|
156
|
+
getDownloadStatusByIds(ids: string[]): Promise<Record<string, DownloadState>>;
|
|
157
|
+
}
|
|
158
|
+
export declare enum DownloadState {
|
|
159
|
+
QUEUED = 0,
|
|
160
|
+
STOPPED = 1,
|
|
161
|
+
DOWNLOADING = 2,
|
|
162
|
+
COMPLETED = 3,
|
|
163
|
+
FAILED = 4,
|
|
164
|
+
REMOVING = 5,
|
|
165
|
+
RESTARTING = 7
|
|
166
|
+
}
|
|
167
|
+
export interface DownloadTask {
|
|
168
|
+
id: string;
|
|
169
|
+
state: DownloadState;
|
|
170
|
+
percentDownloaded: number;
|
|
171
|
+
bytesDownloaded: number;
|
|
172
|
+
contentLength: number;
|
|
173
|
+
track?: Track;
|
|
132
174
|
}
|
|
133
175
|
export declare const Orpheus: OrpheusModule;
|
|
134
176
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoOrpheusModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtE,oBAAY,aAAa;IACvB,IAAI,IAAI;IACR,SAAS,IAAI;IACb,KAAK,IAAI;IACT,KAAK,IAAI;CACV;AAED,oBAAY,UAAU;IACpB,GAAG,IAAI;IACP,KAAK,IAAI;IACT,KAAK,IAAI;CACV;AAED,oBAAY,gBAAgB;IAC1B,MAAM,IAAI;IACV,IAAI,IAAI;IACR,IAAI,IAAI;IACR,gBAAgB,IAAI;CACrB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE;QACT,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAA;CACF;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,sBAAsB,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,cAAc,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,eAAe,CAAC,KAAK,EAAE;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,IAAI,CAAC;IACT,aAAa,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,gBAAgB,CAAC,KAAK,EAAE;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,IAAI,CAAC;IACT,kBAAkB,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"ExpoOrpheusModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,5 +19,15 @@ export var TransitionReason;
|
|
|
19
19
|
TransitionReason[TransitionReason["SEEK"] = 2] = "SEEK";
|
|
20
20
|
TransitionReason[TransitionReason["PLAYLIST_CHANGED"] = 3] = "PLAYLIST_CHANGED";
|
|
21
21
|
})(TransitionReason || (TransitionReason = {}));
|
|
22
|
+
export var DownloadState;
|
|
23
|
+
(function (DownloadState) {
|
|
24
|
+
DownloadState[DownloadState["QUEUED"] = 0] = "QUEUED";
|
|
25
|
+
DownloadState[DownloadState["STOPPED"] = 1] = "STOPPED";
|
|
26
|
+
DownloadState[DownloadState["DOWNLOADING"] = 2] = "DOWNLOADING";
|
|
27
|
+
DownloadState[DownloadState["COMPLETED"] = 3] = "COMPLETED";
|
|
28
|
+
DownloadState[DownloadState["FAILED"] = 4] = "FAILED";
|
|
29
|
+
DownloadState[DownloadState["REMOVING"] = 5] = "REMOVING";
|
|
30
|
+
DownloadState[DownloadState["RESTARTING"] = 7] = "RESTARTING";
|
|
31
|
+
})(DownloadState || (DownloadState = {}));
|
|
22
32
|
export const Orpheus = requireNativeModule("Orpheus");
|
|
23
33
|
//# sourceMappingURL=ExpoOrpheusModule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoOrpheusModule.js","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,2DAAa,CAAA;IACb,mDAAS,CAAA;IACT,mDAAS,CAAA;AACX,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,yCAAO,CAAA;IACP,6CAAS,CAAA;IACT,6CAAS,CAAA;AACX,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,2DAAU,CAAA;IACV,uDAAQ,CAAA;IACR,uDAAQ,CAAA;IACR,+EAAoB,CAAA;AACtB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;
|
|
1
|
+
{"version":3,"file":"ExpoOrpheusModule.js","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,2DAAa,CAAA;IACb,mDAAS,CAAA;IACT,mDAAS,CAAA;AACX,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,yCAAO,CAAA;IACP,6CAAS,CAAA;IACT,6CAAS,CAAA;AACX,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,2DAAU,CAAA;IACV,uDAAQ,CAAA;IACR,uDAAQ,CAAA;IACR,+EAAoB,CAAA;AACtB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AA4KD,MAAM,CAAN,IAAY,aAQX;AARD,WAAY,aAAa;IACvB,qDAAU,CAAA;IACV,uDAAW,CAAA;IACX,+DAAe,CAAA;IACf,2DAAa,CAAA;IACb,qDAAU,CAAA;IACV,yDAAY,CAAA;IACZ,6DAAc,CAAA;AAChB,CAAC,EARW,aAAa,KAAb,aAAa,QAQxB;AAWD,MAAM,CAAC,MAAM,OAAO,GAAG,mBAAmB,CAAgB,SAAS,CAAC,CAAC","sourcesContent":["import { requireNativeModule, NativeModule } from \"expo-modules-core\";\n\nexport enum PlaybackState {\n IDLE = 1,\n BUFFERING = 2,\n READY = 3,\n ENDED = 4,\n}\n\nexport enum RepeatMode {\n OFF = 0,\n TRACK = 1,\n QUEUE = 2,\n}\n\nexport enum TransitionReason {\n REPEAT = 0,\n AUTO = 1,\n SEEK = 2,\n PLAYLIST_CHANGED = 3,\n}\n\nexport interface Track {\n id: string;\n url: string;\n title?: string;\n artist?: string;\n artwork?: string;\n duration?: number;\n loudness?: {\n measured_i: number;\n target_i: number;\n }\n}\n\nexport type OrpheusEvents = {\n onPlaybackStateChanged(event: { state: PlaybackState }): void;\n onTrackStarted(event: { trackId: string; reason: TransitionReason }): void;\n onTrackFinished(event: {\n trackId: string;\n finalPosition: number;\n duration: number;\n }): void;\n onPlayerError(event: { code: string; message: string }): void;\n onPositionUpdate(event: {\n position: number;\n duration: number;\n buffered: number;\n }): void;\n onIsPlayingChanged(event: { status: boolean }): void;\n onDownloadUpdated(event: DownloadTask): void;\n};\n\ndeclare class OrpheusModule extends NativeModule<OrpheusEvents> {\n \n restorePlaybackPositionEnabled: boolean;\n\n /**\n * 获取当前进度(秒)\n */\n getPosition(): Promise<number>;\n\n /**\n * 获取总时长(秒)\n */\n getDuration(): Promise<number>;\n\n /**\n * 获取缓冲进度(秒)\n */\n getBuffered(): Promise<number>;\n\n /**\n * 获取是否正在播放\n */\n getIsPlaying(): Promise<boolean>;\n\n /**\n * 获取当前播放索引\n */\n getCurrentIndex(): Promise<number>;\n\n /**\n * 获取当前播放的 Track 对象\n */\n getCurrentTrack(): Promise<Track | null>;\n\n /**\n * 获取随机模式状态\n */\n getShuffleMode(): Promise<boolean>;\n\n /**\n * 获取指定索引的 Track\n */\n getIndexTrack(index: number): Promise<Track | null>;\n\n getRepeatMode(): Promise<RepeatMode>;\n\n setBilibiliCookie(cookie: string): void;\n \n setRestorePlaybackPositionEnabled(enabled: boolean): void;\n\n play(): Promise<void>;\n\n pause(): Promise<void>;\n\n clear(): Promise<void>;\n\n skipTo(index: number): Promise<void>;\n\n skipToNext(): Promise<void>;\n\n skipToPrevious(): Promise<void>;\n\n /**\n * 跳转进度\n * @param seconds 秒数\n */\n seekTo(seconds: number): Promise<void>;\n\n setRepeatMode(mode: RepeatMode): Promise<void>;\n\n setShuffleMode(enabled: boolean): Promise<void>;\n\n getQueue(): Promise<Track[]>;\n\n /**\n * 添加到队列末尾,且不去重。\n * @param tracks\n * @param startFromId 可选,添加后立即播放该 ID 的曲目\n * @param clearQueue 可选,是否清空当前队列\n */\n addToEnd(\n tracks: Track[],\n startFromId?: string,\n clearQueue?: boolean\n ): Promise<void>;\n\n /**\n * 播放下一首\n * @param track\n */\n playNext(track: Track): Promise<void>;\n\n removeTrack(index: number): Promise<void>;\n\n /**\n * 设置睡眠定时器\n * @param durationMs 单位毫秒\n */\n setSleepTimer(durationMs: number): Promise<void>;\n\n /**\n * 获取睡眠定时器结束时间\n * @returns 单位毫秒,如果没有设置则返回 null\n */\n getSleepTimerEndTime(): Promise<number | null>;\n\n cancelSleepTimer(): Promise<void>;\n\n /**\n * 下载单首歌曲\n */\n downloadTrack(track: Track): Promise<void>;\n\n /**\n * 移除下载任务\n */\n removeDownload(id: string): Promise<void>;\n\n /**\n * 批量下载歌曲\n */\n multiDownload(tracks: Track[]): Promise<void>;\n\n /**\n * 移除所有下载任务\n */\n removeAllDownloads(): Promise<void>;\n\n /**\n * 获取所有下载任务\n */\n getDownloads(): Promise<DownloadTask[]>;\n\n /**\n * 批量返回指定 ID 的下载状态\n */\n getDownloadStatusByIds(ids: string[]): Promise<Record<string, DownloadState>>;\n}\n\nexport enum DownloadState {\n QUEUED = 0,\n STOPPED = 1,\n DOWNLOADING = 2,\n COMPLETED = 3,\n FAILED = 4,\n REMOVING = 5,\n RESTARTING = 7,\n}\n\nexport interface DownloadTask {\n id: string;\n state: DownloadState;\n percentDownloaded: number;\n bytesDownloaded: number;\n contentLength: number;\n track?: Track;\n}\n\nexport const Orpheus = requireNativeModule<OrpheusModule>(\"Orpheus\");\n"]}
|
package/package.json
CHANGED
package/src/ExpoOrpheusModule.ts
CHANGED
|
@@ -48,6 +48,7 @@ export type OrpheusEvents = {
|
|
|
48
48
|
buffered: number;
|
|
49
49
|
}): void;
|
|
50
50
|
onIsPlayingChanged(event: { status: boolean }): void;
|
|
51
|
+
onDownloadUpdated(event: DownloadTask): void;
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
declare class OrpheusModule extends NativeModule<OrpheusEvents> {
|
|
@@ -157,6 +158,55 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
|
|
|
157
158
|
getSleepTimerEndTime(): Promise<number | null>;
|
|
158
159
|
|
|
159
160
|
cancelSleepTimer(): Promise<void>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 下载单首歌曲
|
|
164
|
+
*/
|
|
165
|
+
downloadTrack(track: Track): Promise<void>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 移除下载任务
|
|
169
|
+
*/
|
|
170
|
+
removeDownload(id: string): Promise<void>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 批量下载歌曲
|
|
174
|
+
*/
|
|
175
|
+
multiDownload(tracks: Track[]): Promise<void>;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 移除所有下载任务
|
|
179
|
+
*/
|
|
180
|
+
removeAllDownloads(): Promise<void>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 获取所有下载任务
|
|
184
|
+
*/
|
|
185
|
+
getDownloads(): Promise<DownloadTask[]>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 批量返回指定 ID 的下载状态
|
|
189
|
+
*/
|
|
190
|
+
getDownloadStatusByIds(ids: string[]): Promise<Record<string, DownloadState>>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export enum DownloadState {
|
|
194
|
+
QUEUED = 0,
|
|
195
|
+
STOPPED = 1,
|
|
196
|
+
DOWNLOADING = 2,
|
|
197
|
+
COMPLETED = 3,
|
|
198
|
+
FAILED = 4,
|
|
199
|
+
REMOVING = 5,
|
|
200
|
+
RESTARTING = 7,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface DownloadTask {
|
|
204
|
+
id: string;
|
|
205
|
+
state: DownloadState;
|
|
206
|
+
percentDownloaded: number;
|
|
207
|
+
bytesDownloaded: number;
|
|
208
|
+
contentLength: number;
|
|
209
|
+
track?: Track;
|
|
160
210
|
}
|
|
161
211
|
|
|
162
212
|
export const Orpheus = requireNativeModule<OrpheusModule>("Orpheus");
|