@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.
@@ -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,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, OrpheusService::class.java)
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
- }.runOnQueue(Queues.MAIN)
317
+ }
296
318
 
297
- AsyncFunction("playNext") { track: TrackRecord ->
298
- checkController()
299
- val player = controller ?: return@AsyncFunction
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
- val mediaItem = track.toMediaItem(gson)
302
- val targetIndex = player.currentMediaItemIndex + 1
325
+ DownloadService.sendAddDownload(
326
+ context,
327
+ OrpheusDownloadService::class.java,
328
+ downloadRequest,
329
+ false
330
+ )
331
+ }
303
332
 
304
- var existingIndex = -1
305
- for (i in 0 until player.mediaItemCount) {
306
- if (player.getMediaItemAt(i).mediaId == track.id) {
307
- existingIndex = i
308
- break
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
- if (existingIndex != -1) {
313
- if (existingIndex == player.currentMediaItemIndex) {
314
- return@AsyncFunction
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
- val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
382
+ } finally {
383
+ cursor.close()
384
+ }
385
+ return@AsyncFunction result
386
+ }
317
387
 
318
- player.moveMediaItem(existingIndex, safeTargetIndex)
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
- } else {
321
- val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
394
+ val result = mutableMapOf<String, Int>()
322
395
 
323
- player.addMediaItem(safeTargetIndex, mediaItem)
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
- if (player.playbackState == Player.STATE_IDLE) {
327
- player.prepare()
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
- }.runOnQueue(Queues.MAIN)
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
- if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) {
371
- val lastMediaItem =
372
- controller?.getMediaItemAt(oldPosition.mediaItemIndex) ?: return
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
- // onPositionDiscontinuity 会被连续调用两次,且两次调用参数相同,很奇怪的行为,所以采用这种方式过滤.没值就直接返回,不发事件。
375
- val duration = durationCache.remove(lastMediaItem.mediaId) ?: return
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
+ }
@@ -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.1",
3
+ "version": "0.4.1",
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");