@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.
@@ -1,21 +1,34 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
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=".OrpheusService"
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 cache: SimpleCache? = null
13
+ private var lruCache: SimpleCache? = null
14
+ private var stableCache: SimpleCache? = null
13
15
 
14
- @UnstableApi
15
- fun get(context: Context): SimpleCache {
16
- if (cache == null) {
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
- cache = SimpleCache(cacheDir, evictor, databaseProvider)
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 cache!!
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 var currentTrackDuration: Long = 0L
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, OrpheusService::class.java)
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
- }.runOnQueue(Queues.MAIN)
314
+ }
295
315
 
296
- AsyncFunction("playNext") { track: TrackRecord ->
297
- checkController()
298
- val player = controller ?: return@AsyncFunction
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
- val mediaItem = track.toMediaItem(gson)
301
- val targetIndex = player.currentMediaItemIndex + 1
322
+ DownloadService.sendAddDownload(
323
+ context,
324
+ OrpheusDownloadService::class.java,
325
+ downloadRequest,
326
+ false
327
+ )
328
+ }
302
329
 
303
- var existingIndex = -1
304
- for (i in 0 until player.mediaItemCount) {
305
- if (player.getMediaItemAt(i).mediaId == track.id) {
306
- existingIndex = i
307
- break
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
- if (existingIndex != -1) {
312
- if (existingIndex == player.currentMediaItemIndex) {
313
- return@AsyncFunction
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
- val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
379
+ } finally {
380
+ cursor.close()
381
+ }
382
+ return@AsyncFunction result
383
+ }
316
384
 
317
- player.moveMediaItem(existingIndex, safeTargetIndex)
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
- } else {
320
- val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
391
+ val result = mutableMapOf<String, Int>()
321
392
 
322
- player.addMediaItem(safeTargetIndex, mediaItem)
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
- if (player.playbackState == Player.STATE_IDLE) {
326
- player.prepare()
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
- }.runOnQueue(Queues.MAIN)
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 currentTrackDuration / 1000.0,
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
+ }
@@ -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.bilibili.BilibiliRepository
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 OrpheusService : MediaLibraryService() {
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 httpDataSourceFactory = DefaultHttpDataSource.Factory()
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(resolvingDataSourceFactory)
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
+ }
@@ -20,6 +20,7 @@ object MediaItemStorer {
20
20
  private const val KEY_SAVED_POSITION = "saved_position"
21
21
 
22
22
 
23
+ @Synchronized
23
24
  fun initialize(context: Context) {
24
25
  if (kv == null) {
25
26
  MMKV.initialize(context)
@@ -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":"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"}
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;AA6ID,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};\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\nexport const Orpheus = requireNativeModule<OrpheusModule>(\"Orpheus\");\n"]}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roitium/expo-orpheus",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A player for bbplayer",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -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");