@roitium/expo-orpheus 0.3.0 → 0.4.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/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 +175 -34
- 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,10 +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
|
|
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
|
|
12
20
|
import androidx.media3.session.MediaController
|
|
13
21
|
import androidx.media3.session.SessionCommand
|
|
14
22
|
import androidx.media3.session.SessionResult
|
|
@@ -21,9 +29,11 @@ import expo.modules.kotlin.functions.Queues
|
|
|
21
29
|
import expo.modules.kotlin.modules.Module
|
|
22
30
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
23
31
|
import expo.modules.orpheus.models.TrackRecord
|
|
32
|
+
import expo.modules.orpheus.utils.DownloadUtil
|
|
24
33
|
import expo.modules.orpheus.utils.MediaItemStorer
|
|
25
34
|
import expo.modules.orpheus.utils.toMediaItem
|
|
26
35
|
|
|
36
|
+
@UnstableApi
|
|
27
37
|
class ExpoOrpheusModule : Module() {
|
|
28
38
|
private var controllerFuture: ListenableFuture<MediaController>? = null
|
|
29
39
|
|
|
@@ -31,13 +41,16 @@ class ExpoOrpheusModule : Module() {
|
|
|
31
41
|
|
|
32
42
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
33
43
|
|
|
44
|
+
private var downloadManager: DownloadManager? = null
|
|
45
|
+
|
|
34
46
|
// 记录上一首歌曲的 ID,用于在切歌时发送给 JS
|
|
35
47
|
private var lastMediaId: String? = null
|
|
36
48
|
|
|
37
|
-
private
|
|
49
|
+
private val durationCache = mutableMapOf<String, Long>()
|
|
38
50
|
|
|
39
51
|
val gson = Gson()
|
|
40
52
|
|
|
53
|
+
@OptIn(UnstableApi::class)
|
|
41
54
|
override fun definition() = ModuleDefinition {
|
|
42
55
|
Name("Orpheus")
|
|
43
56
|
|
|
@@ -47,7 +60,8 @@ class ExpoOrpheusModule : Module() {
|
|
|
47
60
|
"onPositionUpdate",
|
|
48
61
|
"onIsPlayingChanged",
|
|
49
62
|
"onTrackFinished",
|
|
50
|
-
"onTrackStarted"
|
|
63
|
+
"onTrackStarted",
|
|
64
|
+
"onDownloadUpdated"
|
|
51
65
|
)
|
|
52
66
|
|
|
53
67
|
OnCreate {
|
|
@@ -55,7 +69,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
55
69
|
MediaItemStorer.initialize(context)
|
|
56
70
|
val sessionToken = SessionToken(
|
|
57
71
|
context,
|
|
58
|
-
ComponentName(context,
|
|
72
|
+
ComponentName(context, OrpheusMusicService::class.java)
|
|
59
73
|
)
|
|
60
74
|
controllerFuture = MediaController.Builder(context, sessionToken)
|
|
61
75
|
.setApplicationLooper(Looper.getMainLooper()).buildAsync()
|
|
@@ -68,12 +82,17 @@ class ExpoOrpheusModule : Module() {
|
|
|
68
82
|
e.printStackTrace()
|
|
69
83
|
}
|
|
70
84
|
}, MoreExecutors.directExecutor())
|
|
85
|
+
|
|
86
|
+
downloadManager = DownloadUtil.getDownloadManager(context)
|
|
87
|
+
downloadManager?.addListener(downloadListener)
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
OnDestroy {
|
|
74
91
|
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
75
92
|
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
93
|
+
mainHandler.removeCallbacks(downloadProgressRunnable)
|
|
76
94
|
controllerFuture?.let { MediaController.releaseFuture(it) }
|
|
95
|
+
downloadManager?.removeListener(downloadListener)
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
Constant("restorePlaybackPositionEnabled") {
|
|
@@ -84,6 +103,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
84
103
|
OrpheusConfig.bilibiliCookie = cookie
|
|
85
104
|
}
|
|
86
105
|
|
|
106
|
+
|
|
87
107
|
Function("setRestorePlaybackPositionEnabled") { enabled: Boolean ->
|
|
88
108
|
MediaItemStorer.setRestoreEnabled(enabled)
|
|
89
109
|
}
|
|
@@ -291,41 +311,154 @@ class ExpoOrpheusModule : Module() {
|
|
|
291
311
|
if (player.playbackState == Player.STATE_IDLE) {
|
|
292
312
|
player.prepare()
|
|
293
313
|
}
|
|
294
|
-
}
|
|
314
|
+
}
|
|
295
315
|
|
|
296
|
-
AsyncFunction("
|
|
297
|
-
|
|
298
|
-
val
|
|
316
|
+
AsyncFunction("downloadTrack") { track: TrackRecord ->
|
|
317
|
+
val context = appContext.reactContext ?: return@AsyncFunction
|
|
318
|
+
val downloadRequest = DownloadRequest.Builder(track.id, track.url.toUri())
|
|
319
|
+
.setData(gson.toJson(track).toByteArray())
|
|
320
|
+
.build()
|
|
299
321
|
|
|
300
|
-
|
|
301
|
-
|
|
322
|
+
DownloadService.sendAddDownload(
|
|
323
|
+
context,
|
|
324
|
+
OrpheusDownloadService::class.java,
|
|
325
|
+
downloadRequest,
|
|
326
|
+
false
|
|
327
|
+
)
|
|
328
|
+
}
|
|
302
329
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
330
|
+
AsyncFunction("multiDownload") { tracks: List<TrackRecord> ->
|
|
331
|
+
val context = appContext.reactContext ?: return@AsyncFunction
|
|
332
|
+
tracks.forEach { track ->
|
|
333
|
+
val downloadRequest = DownloadRequest.Builder(track.id, track.url.toUri())
|
|
334
|
+
.setData(gson.toJson(track).toByteArray())
|
|
335
|
+
.build()
|
|
336
|
+
DownloadService.sendAddDownload(
|
|
337
|
+
context,
|
|
338
|
+
OrpheusDownloadService::class.java,
|
|
339
|
+
downloadRequest,
|
|
340
|
+
false
|
|
341
|
+
)
|
|
309
342
|
}
|
|
343
|
+
return@AsyncFunction
|
|
344
|
+
}
|
|
310
345
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
346
|
+
AsyncFunction("removeDownload") { id: String ->
|
|
347
|
+
val context = appContext.reactContext ?: return@AsyncFunction
|
|
348
|
+
DownloadService.sendRemoveDownload(
|
|
349
|
+
context,
|
|
350
|
+
OrpheusDownloadService::class.java,
|
|
351
|
+
id,
|
|
352
|
+
false
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
AsyncFunction("removeAllDownloads") {
|
|
357
|
+
val context = appContext.reactContext ?: return@AsyncFunction null
|
|
358
|
+
DownloadService.sendRemoveAllDownloads(
|
|
359
|
+
context,
|
|
360
|
+
OrpheusDownloadService::class.java,
|
|
361
|
+
false
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
AsyncFunction("getDownloads") {
|
|
366
|
+
val context =
|
|
367
|
+
appContext.reactContext ?: return@AsyncFunction emptyList<Map<String, Any>>()
|
|
368
|
+
val downloadManager = DownloadUtil.getDownloadManager(context)
|
|
369
|
+
val downloadIndex = downloadManager.downloadIndex
|
|
370
|
+
|
|
371
|
+
val cursor = downloadIndex.getDownloads()
|
|
372
|
+
val result = ArrayList<Map<String, Any>>()
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
while (cursor.moveToNext()) {
|
|
376
|
+
val download = cursor.download
|
|
377
|
+
result.add(getDownloadMap(download))
|
|
314
378
|
}
|
|
315
|
-
|
|
379
|
+
} finally {
|
|
380
|
+
cursor.close()
|
|
381
|
+
}
|
|
382
|
+
return@AsyncFunction result
|
|
383
|
+
}
|
|
316
384
|
|
|
317
|
-
|
|
385
|
+
AsyncFunction("getDownloadStatusByIds") { ids: List<String> ->
|
|
386
|
+
val context =
|
|
387
|
+
appContext.reactContext ?: return@AsyncFunction emptyMap<String, Int>()
|
|
388
|
+
val downloadManager = DownloadUtil.getDownloadManager(context)
|
|
389
|
+
val downloadIndex = downloadManager.downloadIndex
|
|
318
390
|
|
|
319
|
-
|
|
320
|
-
val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
|
|
391
|
+
val result = mutableMapOf<String, Int>()
|
|
321
392
|
|
|
322
|
-
|
|
393
|
+
for (id in ids) {
|
|
394
|
+
val download = downloadIndex.getDownload(id)
|
|
395
|
+
if (download != null) {
|
|
396
|
+
result[id] = download.state
|
|
397
|
+
}
|
|
323
398
|
}
|
|
399
|
+
return@AsyncFunction result
|
|
400
|
+
}
|
|
401
|
+
}
|
|
324
402
|
|
|
325
|
-
|
|
326
|
-
|
|
403
|
+
private fun getDownloadMap(download: Download): Map<String, Any> {
|
|
404
|
+
val trackJson = if (download.request.data.isNotEmpty()) {
|
|
405
|
+
String(download.request.data)
|
|
406
|
+
} else null
|
|
407
|
+
|
|
408
|
+
val map = mutableMapOf<String, Any>(
|
|
409
|
+
"id" to download.request.id,
|
|
410
|
+
"state" to download.state,
|
|
411
|
+
"percentDownloaded" to download.percentDownloaded,
|
|
412
|
+
"bytesDownloaded" to download.bytesDownloaded,
|
|
413
|
+
"contentLength" to download.contentLength
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if (trackJson != null) {
|
|
417
|
+
try {
|
|
418
|
+
val track = gson.fromJson(trackJson, TrackRecord::class.java)
|
|
419
|
+
map["track"] = track
|
|
420
|
+
} catch (e: Exception) {
|
|
421
|
+
e.printStackTrace()
|
|
327
422
|
}
|
|
328
|
-
}
|
|
423
|
+
}
|
|
424
|
+
return map
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private val downloadListener = object : DownloadManager.Listener {
|
|
428
|
+
override fun onDownloadChanged(
|
|
429
|
+
downloadManager: DownloadManager,
|
|
430
|
+
download: Download,
|
|
431
|
+
finalException: Exception?
|
|
432
|
+
) {
|
|
433
|
+
sendEvent("onDownloadUpdated", getDownloadMap(download))
|
|
434
|
+
updateDownloadProgressRunnerState()
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private val downloadProgressRunnable = object : Runnable {
|
|
439
|
+
override fun run() {
|
|
440
|
+
val manager = downloadManager ?: return
|
|
441
|
+
if (manager.currentDownloads.isNotEmpty()) {
|
|
442
|
+
for (download in manager.currentDownloads) {
|
|
443
|
+
if (download.state == Download.STATE_DOWNLOADING) {
|
|
444
|
+
sendEvent("onDownloadUpdated", getDownloadMap(download))
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
mainHandler.postDelayed(this, 500)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private fun updateDownloadProgressRunnerState() {
|
|
453
|
+
mainHandler.removeCallbacks(downloadProgressRunnable)
|
|
454
|
+
val manager = downloadManager ?: return
|
|
455
|
+
|
|
456
|
+
val hasActiveDownloads =
|
|
457
|
+
manager.currentDownloads.any { it.state == Download.STATE_DOWNLOADING }
|
|
458
|
+
|
|
459
|
+
if (hasActiveDownloads) {
|
|
460
|
+
mainHandler.post(downloadProgressRunnable)
|
|
461
|
+
}
|
|
329
462
|
}
|
|
330
463
|
|
|
331
464
|
private fun setupListeners() {
|
|
@@ -346,25 +479,38 @@ class ExpoOrpheusModule : Module() {
|
|
|
346
479
|
)
|
|
347
480
|
|
|
348
481
|
lastMediaId = newId
|
|
349
|
-
currentTrackDuration = 0L
|
|
350
482
|
saveCurrentPosition()
|
|
351
483
|
}
|
|
352
484
|
|
|
485
|
+
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
486
|
+
val player = controller ?: return
|
|
487
|
+
val currentItem = player.currentMediaItem ?: return
|
|
488
|
+
val mediaId = currentItem.mediaId
|
|
489
|
+
|
|
490
|
+
val duration = player.duration
|
|
491
|
+
|
|
492
|
+
if (duration != C.TIME_UNSET && duration > 0) {
|
|
493
|
+
durationCache[mediaId] = duration
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
353
497
|
override fun onPositionDiscontinuity(
|
|
354
498
|
oldPosition: Player.PositionInfo,
|
|
355
499
|
newPosition: Player.PositionInfo,
|
|
356
500
|
reason: Int
|
|
357
501
|
) {
|
|
358
|
-
Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT
|
|
359
502
|
if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) {
|
|
360
503
|
val lastMediaItem =
|
|
361
504
|
controller?.getMediaItemAt(oldPosition.mediaItemIndex) ?: return
|
|
362
505
|
|
|
506
|
+
// onPositionDiscontinuity 会被连续调用两次,且两次调用参数相同,很奇怪的行为,所以采用这种方式过滤.没值就直接返回,不发事件。
|
|
507
|
+
val duration = durationCache.remove(lastMediaItem.mediaId) ?: return
|
|
508
|
+
|
|
363
509
|
sendEvent(
|
|
364
510
|
"onTrackFinished", mapOf(
|
|
365
511
|
"trackId" to lastMediaItem.mediaId,
|
|
366
512
|
"finalPosition" to oldPosition.positionMs / 1000.0,
|
|
367
|
-
"duration" to
|
|
513
|
+
"duration" to duration / 1000.0,
|
|
368
514
|
)
|
|
369
515
|
)
|
|
370
516
|
}
|
|
@@ -381,11 +527,6 @@ class ExpoOrpheusModule : Module() {
|
|
|
381
527
|
)
|
|
382
528
|
)
|
|
383
529
|
|
|
384
|
-
if (state == Player.STATE_READY) {
|
|
385
|
-
val d = controller?.duration
|
|
386
|
-
if (d != C.TIME_UNSET && d != null) currentTrackDuration = d
|
|
387
|
-
}
|
|
388
|
-
|
|
389
530
|
updateProgressRunnerState()
|
|
390
531
|
}
|
|
391
532
|
|
|
@@ -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");
|