diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 822560e..079eb49 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -29,6 +29,13 @@ android {
"proguard-rules.pro"
)
}
+// debug {
+// isMinifyEnabled = true
+// proguardFiles(
+// getDefaultProguardFile("proguard-android-optimize.txt"),
+// "proguard-rules.pro"
+// )
+// }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb43..35463f0 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,6 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-dontwarn org.slf4j.impl.StaticLoggerBinder
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 410f502..d5ba17b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,9 @@
+
+
+
@@ -48,7 +51,10 @@
android:name=".activity.AboutActivity"
android:screenOrientation="portrait" />
+
+ if (body.videoDetails?.videoId != videoId) {
+ throw VideoIdMismatchException()
+ }
+ when (val status = body.playabilityStatus?.status) {
+ "OK" -> body.streamingData?.highestQualityFormat?.let { format ->
+ format.url
+ } ?: throw PlayableFormatNotFoundException()
+
+ "UNPLAYABLE" -> throw UnplayableException()
+ "LOGIN_REQUIRED" -> throw LoginRequiredException()
+ else -> throw PlaybackException(
+ status,
+ null,
+ PlaybackException.ERROR_CODE_REMOTE_ERROR
+ )
+ }
+ }
+ val url = urlResult?.getOrNull()
+ if (url != null) {
+ binding.playbackErrorLayout.visibility = View.GONE
+ binding.disableClicksLayout.visibility = View.GONE
+
+ binding.totalDurationTv.visibility = View.GONE
+
+ val newMediaItem =
+ MediaItem.Builder()
+ .setUri(url)
+ .setMediaMetadata(mediaItem.mediaMetadata)
+ .build()
+
+ MediaControllerManager.getController()?.let {
+ it.addListener(object : Player.Listener {
+ override fun onPlayerError(error: PlaybackException) {
+ Log.d(TAG, "onPlayerError = $error")
+ }
+
+ override fun onPlayWhenReadyChanged(
+ playWhenReady: Boolean,
+ reason: Int
+ ) {
+ Log.d(TAG, "onPlayWhenReadyChanged = $playWhenReady")
+ updateProgressState()
+ }
+
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ Log.d(TAG, "onPlaybackStateChanged = $playbackState")
+ if (playbackState == Player.STATE_READY) {
+ binding.totalDurationTv.visibility = View.VISIBLE
+ binding.totalDurationTv.text =
+ convertMillisToMinutesAndSecondsString(
+ MediaControllerManager.getDuration()
+ )
+ binding.sbProgress.max =
+ MediaControllerManager.getDuration().toInt()
+ }
+ updateProgressState()
+ }
+ })
+ it.setMediaItem(newMediaItem)
+ it.repeatMode = Player.REPEAT_MODE_ALL
+ it.prepare()
+ it.play()
+ }
+ } else {
+ binding.playbackErrorLayout.visibility = View.VISIBLE
+ binding.disableClicksLayout.visibility = View.VISIBLE
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateProgressState() {
+ val currentPlayer = MediaControllerManager.getController()
+ if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
+// updatePlayState(currentPlayer.isPlaying)
+ progressHandler.removeCallbacksAndMessages(null)
+ progressHandler.sendEmptyMessage(1)
+ } else {
+ progressHandler.removeCallbacksAndMessages(null)
+ }
+ }
+
+ private val progressHandler = object : Handler(Looper.myLooper()!!) {
+ override fun handleMessage(msg: Message) {
+ val currentPlayer = MediaControllerManager.getController()
+ if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
+ val currentPosition = currentPlayer.currentPosition
+ val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
+ binding.progressDurationTv.text = currentString
+
+ // 更新 SeekBar 的进度
+ binding.sbProgress.progress = currentPosition.toInt()
+
+ sendEmptyMessageDelayed(1, 1000)
+ }
+ }
+ }
+
+ private fun updatePlayState(b: Boolean) {
+ if (b) {
+ binding.playImg.setImageResource(R.drawable.playing_green_icon)
+ } else {
+ binding.playImg.setImageResource(R.drawable.play_green_icon)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt b/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt
index 201924e..6f13d16 100644
--- a/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt
+++ b/app/src/main/java/com/player/musicoo/adapter/ResponsiveListAdapter.kt
@@ -3,20 +3,13 @@ package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
-import com.player.musicoo.App
-import com.player.musicoo.R
-import com.player.musicoo.activity.PlayDetailsActivity
-import com.player.musicoo.bean.Audio
+import com.player.musicoo.activity.MoPlayDetailsActivity
import com.player.musicoo.databinding.MusicResponsiveItemBinding
-import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
-import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
-import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
-import com.player.musicoo.util.getAudioDurationFromAssets
+import com.player.musicoo.innertube.models.bodies.NextBody
class ResponsiveListAdapter(
private val context: Context,
@@ -32,7 +25,61 @@ class ResponsiveListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bean = list[position]
- holder.bind(bean)
+
+ val url = bean.musicResponsiveListItemRenderer
+ ?.thumbnail
+ ?.musicThumbnailRenderer
+ ?.thumbnail
+ ?.thumbnails
+ ?.let { it.getOrNull(1) ?: it.getOrNull(0) }
+ ?.url
+ val name = bean.musicResponsiveListItemRenderer
+ ?.flexColumns?.get(0)
+ ?.musicResponsiveListItemFlexColumnRenderer
+ ?.text
+ ?.runs
+ ?.firstOrNull()
+ ?.text
+ val desc = bean.musicResponsiveListItemRenderer
+ ?.flexColumns?.get(1)
+ ?.musicResponsiveListItemFlexColumnRenderer
+ ?.text
+ ?.runs
+ ?.firstOrNull()
+ ?.text
+
+ val watchEndpoint = bean.musicResponsiveListItemRenderer
+ ?.flexColumns
+ ?.firstOrNull()
+ ?.musicResponsiveListItemFlexColumnRenderer
+ ?.text
+ ?.runs
+ ?.firstOrNull()
+ ?.navigationEndpoint
+ ?.watchEndpoint
+
+ val videoId = watchEndpoint?.videoId
+ val playlistId = watchEndpoint?.playlistId
+ val playlistSetVideoId = watchEndpoint?.playlistSetVideoId
+ val params = watchEndpoint?.params
+
+ holder.bind(url, name, desc)
+
+ holder.itemView.setOnClickListener {
+ val intent = Intent(context, MoPlayDetailsActivity::class.java)
+ intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_VIDEO_ID, videoId)
+ intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_LIST_ID, playlistId)
+ if (playlistSetVideoId != null) {
+ intent.putExtra(
+ MoPlayDetailsActivity.PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID,
+ playlistSetVideoId
+ )
+ }
+ intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_PARAMS, params)
+ intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, name)
+ intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, desc)
+ context.startActivity(intent)
+ }
}
override fun getItemCount(): Int = list.size
@@ -40,28 +87,8 @@ class ResponsiveListAdapter(
inner class ViewHolder(private val binding: MusicResponsiveItemBinding) :
RecyclerView.ViewHolder(binding.root) {
- fun bind(content: MusicCarouselShelfRenderer.Content) {
- val url = content.musicResponsiveListItemRenderer
- ?.thumbnail
- ?.musicThumbnailRenderer
- ?.thumbnail
- ?.thumbnails
- ?.let { it.getOrNull(1) ?: it.getOrNull(0) }
- ?.url
- val name = content.musicResponsiveListItemRenderer
- ?.flexColumns?.get(0)
- ?.musicResponsiveListItemFlexColumnRenderer
- ?.text
- ?.runs
- ?.firstOrNull()
- ?.text
- val desc = content.musicResponsiveListItemRenderer
- ?.flexColumns?.get(1)
- ?.musicResponsiveListItemFlexColumnRenderer
- ?.text
- ?.runs
- ?.firstOrNull()
- ?.text
+ fun bind(url: String?, name: String?, desc: String?) {
+
binding.apply {
Glide.with(context)
.load(url)
diff --git a/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt b/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt
index cf42bd1..65b1251 100644
--- a/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt
+++ b/app/src/main/java/com/player/musicoo/adapter/TowRowListAdapter.kt
@@ -2,25 +2,13 @@ package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
-import android.util.Log
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
-import com.player.musicoo.App
-import com.player.musicoo.R
-import com.player.musicoo.activity.DetailsActivity
-import com.player.musicoo.activity.PlayDetailsActivity
-import com.player.musicoo.bean.Audio
-import com.player.musicoo.databinding.MusicResponsiveItemBinding
+import com.player.musicoo.activity.MoListDetailsActivity
import com.player.musicoo.databinding.MusicTowRowItemBinding
-import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
-import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
-import com.player.musicoo.util.LogTag
-import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
-import com.player.musicoo.util.getAudioDurationFromAssets
class TowRowListAdapter(
private val context: Context,
@@ -49,8 +37,8 @@ class TowRowListAdapter(
holder.bind(bean)
holder.itemView.setOnClickListener {
- val intent = Intent(context, DetailsActivity::class.java)
- intent.putExtra(DetailsActivity.PLAY_LIST_PAGE_BROWSE_ID, browseId)
+ val intent = Intent(context, MoListDetailsActivity::class.java)
+ intent.putExtra(MoListDetailsActivity.PLAY_LIST_PAGE_BROWSE_ID, browseId)
context.startActivity(intent)
}
}
diff --git a/app/src/main/java/com/player/musicoo/innertube/Innertube.kt b/app/src/main/java/com/player/musicoo/innertube/Innertube.kt
index 58d64f0..0570064 100644
--- a/app/src/main/java/com/player/musicoo/innertube/Innertube.kt
+++ b/app/src/main/java/com/player/musicoo/innertube/Innertube.kt
@@ -99,6 +99,7 @@ object Innertube {
val authors: List>?,
val album: Info?,
val durationText: String?,
+ val bigThumbnail: Thumbnail?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.videoId!!
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt
index 7a4f2ff..6df766d 100644
--- a/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/HomePage.kt
@@ -1,6 +1,5 @@
package com.player.musicoo.innertube.requests
-import android.util.Log
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.Context
diff --git a/app/src/main/java/com/player/musicoo/innertube/requests/MoNextPage.kt b/app/src/main/java/com/player/musicoo/innertube/requests/MoNextPage.kt
new file mode 100644
index 0000000..3187ea3
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/innertube/requests/MoNextPage.kt
@@ -0,0 +1,58 @@
+package com.player.musicoo.innertube.requests
+
+import io.ktor.client.call.body
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import com.player.musicoo.innertube.Innertube
+import com.player.musicoo.innertube.models.ContinuationResponse
+import com.player.musicoo.innertube.models.NextResponse
+import com.player.musicoo.innertube.models.bodies.ContinuationBody
+import com.player.musicoo.innertube.models.bodies.NextBody
+import com.player.musicoo.innertube.utils.from
+import com.player.musicoo.innertube.utils.runCatchingNonCancellable
+
+
+suspend fun Innertube.moNextPage(
+ videoId: String,
+ playlistId: String? = null,
+ params: String? = null,
+ playlistSetVideoId: String? = null
+) =
+ runCatchingNonCancellable {
+ val response = client.post(next) {
+ setBody(
+ NextBody(
+ videoId = videoId,
+ playlistId = playlistId,
+ playlistSetVideoId = playlistSetVideoId,
+ params = params
+ )
+ )
+ }.body()
+
+ val tabs = response
+ .contents
+ ?.singleColumnMusicWatchNextResultsRenderer
+ ?.tabbedRenderer
+ ?.watchNextTabbedResultsRenderer
+ ?.tabs
+
+ val playlistPanelRenderer = tabs
+ ?.getOrNull(0)
+ ?.tabRenderer
+ ?.content
+ ?.musicQueueRenderer
+ ?.content
+ ?.playlistPanelRenderer
+
+ val endpoint = playlistPanelRenderer
+ ?.contents
+ ?.lastOrNull()
+ ?.automixPreviewVideoRenderer
+ ?.content
+ ?.automixPlaylistVideoRenderer
+ ?.navigationEndpoint
+ ?.watchPlaylistEndpoint
+
+
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt
index e6c260b..743d6a0 100644
--- a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicResponsiveListItemRenderer.kt
@@ -39,6 +39,16 @@ fun Innertube.SongItem.Companion.from(renderer: MusicResponsiveListItemRenderer)
?.runs
?.firstOrNull()
?.let(Innertube::Info),
+ bigThumbnail = renderer
+ .thumbnail
+ ?.musicThumbnailRenderer
+ ?.thumbnail
+ ?.thumbnails
+ ?.let {
+ it.getOrNull(5) ?: it.getOrNull(4)
+ ?: it.getOrNull(3) ?: it.getOrNull(2)
+ ?: it.getOrNull(1) ?: it.getOrNull(0)
+ },
thumbnail = renderer
.thumbnail
?.musicThumbnailRenderer
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt
index 1396542..0b74b0d 100644
--- a/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromMusicShelfRendererContent.kt
@@ -35,6 +35,8 @@ fun Innertube.SongItem.Companion.from(content: MusicShelfRenderer.Content): Inne
durationText = otherRuns
.lastOrNull()
?.firstOrNull()?.text,
+ bigThumbnail = content
+ .thumbnail,
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.videoId != null }
diff --git a/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt b/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt
index 6a9fbd5..c8016f6 100644
--- a/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt
+++ b/app/src/main/java/com/player/musicoo/innertube/utils/FromPlaylistPanelVideoRenderer.kt
@@ -24,6 +24,14 @@ fun Innertube.SongItem.Companion.from(renderer: PlaylistPanelVideoRenderer): Inn
?.getOrNull(1)
?.getOrNull(0)
?.let(Innertube::Info),
+ bigThumbnail = renderer
+ .thumbnail
+ ?.thumbnails
+ ?.let {
+ it.getOrNull(5) ?: it.getOrNull(4)
+ ?: it.getOrNull(3) ?: it.getOrNull(2)
+ ?: it.getOrNull(1) ?: it.getOrNull(0)
+ },
thumbnail = renderer
.thumbnail
?.thumbnails
diff --git a/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt b/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt
index 16517ef..4f105bd 100644
--- a/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt
+++ b/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt
@@ -3,7 +3,6 @@ package com.player.musicoo.media
import android.content.ComponentName
import android.content.Context
import android.net.Uri
-import android.util.Log
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
@@ -44,6 +43,28 @@ object MediaControllerManager {
}
}
+ fun setupMedia(id: String, listener: Player.Listener) {
+ val mediaItem =
+ MediaItem.Builder()
+ .setUri(id)
+ .setMediaMetadata(
+ MediaMetadata.Builder()
+ .setArtist("测试")
+ .setTitle("测试")
+ .build()
+ )
+ .build()
+ if (isConnected()) {
+ mediaController?.let {
+ it.addListener(listener)
+ it.setMediaItem(mediaItem)
+ it.repeatMode = Player.REPEAT_MODE_ALL
+ it.prepare()
+ it.play()
+ }
+ }
+ }
+
fun setupMedia(context: Context, audio: Audio, listener: Player.Listener) {
if (currentAudioFile != audio.file) {
currentAudioFile = audio.file
@@ -77,7 +98,7 @@ object MediaControllerManager {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
- it.repeatMode = Player.REPEAT_MODE_ONE
+ it.repeatMode = Player.REPEAT_MODE_ALL
it.prepare()
it.play()
val currentPlayingAudio =
@@ -136,7 +157,7 @@ object MediaControllerManager {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
- it.repeatMode = Player.REPEAT_MODE_ONE
+ it.repeatMode = Player.REPEAT_MODE_ALL
it.prepare()
it.play()
val currentPlayingAudio =
@@ -174,4 +195,29 @@ object MediaControllerManager {
}
return false
}
+
+ fun play() {
+ mediaController?.play()
+ }
+
+ fun getMediaItemCount(): Int {
+ mediaController?.let {
+ return it.mediaItemCount
+ }
+ return 0
+ }
+
+ fun getCurrentMediaItem(): MediaItem? {
+ mediaController?.let {
+ return it.currentMediaItem
+ }
+ return null
+ }
+
+ fun getDuration(): Long {
+ mediaController?.let {
+ return it.duration
+ }
+ return 0
+ }
}
diff --git a/app/src/main/java/com/player/musicoo/media/MediaControllerUtils.kt b/app/src/main/java/com/player/musicoo/media/MediaControllerUtils.kt
new file mode 100644
index 0000000..f072b24
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/media/MediaControllerUtils.kt
@@ -0,0 +1,10 @@
+package com.player.musicoo.media
+
+import androidx.media3.common.MediaItem
+import androidx.media3.session.MediaController
+
+fun MediaController.forcePlay(mediaItem: MediaItem){
+ setMediaItem(mediaItem)
+ prepare()
+ play()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/media/SongRadio.kt b/app/src/main/java/com/player/musicoo/media/SongRadio.kt
new file mode 100644
index 0000000..9465462
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/media/SongRadio.kt
@@ -0,0 +1,50 @@
+package com.player.musicoo.media
+
+import com.player.musicoo.innertube.Innertube
+import com.player.musicoo.innertube.models.bodies.ContinuationBody
+import com.player.musicoo.innertube.models.bodies.NextBody
+import com.player.musicoo.innertube.requests.nextPage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+data class SongRadio(
+ private val videoId: String? = null,
+ private var playlistId: String? = null,
+ private var playlistSetVideoId: String? = null,
+ private var parameters: String? = null
+) {
+ private var nextContinuation: String? = null
+
+ suspend fun process(): List {
+ var songItems: List? = null
+
+ nextContinuation = withContext(Dispatchers.IO) {
+ val continuation = nextContinuation
+
+ if (continuation == null) {
+ Innertube.nextPage(
+ NextBody(
+ videoId = videoId,
+ playlistId = playlistId,
+ params = parameters,
+ playlistSetVideoId = playlistSetVideoId
+ )
+ )?.map { nextResult ->
+ playlistId = nextResult.playlistId
+ parameters = nextResult.params
+ playlistSetVideoId = nextResult.playlistSetVideoId
+
+ nextResult.itemsPage
+ }
+ } else {
+ Innertube.nextPage(ContinuationBody(continuation = continuation))
+ }?.getOrNull()?.let { songsPage ->
+ songItems = songsPage.items
+ songsPage.continuation?.takeUnless { nextContinuation == it }
+ }
+
+ }
+
+ return songItems ?: emptyList()
+ }
+}
diff --git a/app/src/main/java/com/player/musicoo/service/CustomMediaNotificationProvider.kt b/app/src/main/java/com/player/musicoo/service/CustomMediaNotificationProvider.kt
new file mode 100644
index 0000000..838297d
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/service/CustomMediaNotificationProvider.kt
@@ -0,0 +1,43 @@
+package com.player.musicoo.service
+
+import android.content.Context
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.*
+import com.google.common.collect.ImmutableList
+import com.player.musicoo.util.LogTag
+
+@UnstableApi
+class CustomMediaNotificationProvider(context: Context) :
+ DefaultMediaNotificationProvider(context) {
+
+ override fun addNotificationActions(
+ mediaSession: MediaSession,
+ mediaButtons: ImmutableList,
+ builder: NotificationCompat.Builder,
+ actionFactory: MediaNotification.ActionFactory
+ ): IntArray {
+
+ Log.d(LogTag.VO_API_LOG, "mediaButtons->$mediaButtons")
+
+ for (com: CommandButton in mediaButtons) {
+ Log.d(LogTag.VO_API_LOG, "displayName->${com.displayName}")
+ Log.d(LogTag.VO_API_LOG, "playerCommand->${com.playerCommand}")
+ Log.d(LogTag.VO_API_LOG, "------------------------------------------")
+ }
+
+ val notificationMediaButtons = ImmutableList.builder().apply {
+ add(NotificationCustomButton.SKIP_BACK.commandButton)
+ add(NotificationCustomButton.SKIP_FORWARD.commandButton)
+ }.build()
+
+ return super.addNotificationActions(
+ mediaSession,
+ notificationMediaButtons,
+ builder,
+ actionFactory
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/service/NotificationCustomButton.kt b/app/src/main/java/com/player/musicoo/service/NotificationCustomButton.kt
new file mode 100644
index 0000000..2be702d
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/service/NotificationCustomButton.kt
@@ -0,0 +1,30 @@
+package com.player.musicoo.service
+
+import android.os.Bundle
+import androidx.media3.session.CommandButton
+import androidx.media3.session.SessionCommand
+import com.player.musicoo.R
+
+private const val CUSTOM_COMMAND_SKIP_BACK_ACTION_ID = "Skip_back"
+private const val CUSTOM_COMMAND_SKIP_FORWARD_ACTION_ID = "Skip_Forward"
+
+enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
+ SKIP_BACK(
+ customAction = CUSTOM_COMMAND_SKIP_BACK_ACTION_ID,
+ commandButton = CommandButton.Builder()
+ .setDisplayName("SkipBack")
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_SKIP_BACK_ACTION_ID, Bundle()))
+ .setIconResId(R.drawable.play_skip_back)
+ .build()
+
+ ),
+ SKIP_FORWARD(
+ customAction = CUSTOM_COMMAND_SKIP_FORWARD_ACTION_ID,
+ commandButton = CommandButton.Builder()
+ .setDisplayName("SkipForward")
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_SKIP_FORWARD_ACTION_ID, Bundle()))
+ .setIconResId(R.drawable.play_skip_forward)
+ .build()
+
+ ),
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/service/PlaybackExceptions.kt b/app/src/main/java/com/player/musicoo/service/PlaybackExceptions.kt
new file mode 100644
index 0000000..007a69e
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/service/PlaybackExceptions.kt
@@ -0,0 +1,17 @@
+package com.player.musicoo.service
+
+import androidx.annotation.OptIn
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.util.UnstableApi
+
+@OptIn(UnstableApi::class)
+class PlayableFormatNotFoundException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
+
+@OptIn(UnstableApi::class)
+class UnplayableException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
+
+@OptIn(UnstableApi::class)
+class LoginRequiredException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
+
+@OptIn(UnstableApi::class)
+class VideoIdMismatchException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
diff --git a/app/src/main/java/com/player/musicoo/service/PlaybackService.kt b/app/src/main/java/com/player/musicoo/service/PlaybackService.kt
index 5321d9b..2f500ff 100644
--- a/app/src/main/java/com/player/musicoo/service/PlaybackService.kt
+++ b/app/src/main/java/com/player/musicoo/service/PlaybackService.kt
@@ -2,36 +2,53 @@ package com.player.musicoo.service
import android.content.Intent
import android.os.Bundle
+import android.util.Log
+import androidx.media3.common.AudioAttributes
import androidx.media3.common.Player
-import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
-import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
-import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
-import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.session.CommandButton
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
+import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
+import androidx.media3.session.SessionResult
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
import com.player.musicoo.R
+import com.player.musicoo.util.LogTag
@UnstableApi
class PlaybackService : MediaSessionService() {
-
+ private val TAG = LogTag.VO_SERVICE_LOG
private var mediaSession: MediaSession? = null
+ private val notificationCustomButtons =
+ NotificationCustomButton.entries.map { command -> command.commandButton }
- // Create your player and media session in the onCreate lifecycle event
override fun onCreate() {
super.onCreate()
- val player = ExoPlayer.Builder(this).build()
- mediaSession = MediaSession.Builder(this, player)
+ val player = ExoPlayer.Builder(this, createMediaSourceFactory())
+ .setAudioAttributes(AudioAttributes.DEFAULT, true)
+ .setHandleAudioBecomingNoisy(true)
.build()
+ mediaSession = MediaSession.Builder(this, player)
+// .setCallback(MyCallback())
+// .setCustomLayout(notificationCustomButtons)
+ .build()
-// setMediaNotificationProvider(MyMediaNotificationProvider(this))
+ val customMediaNotificationProvider = CustomMediaNotificationProvider(this).apply {
+ setSmallIcon(R.mipmap.musicoo_logo_img)
+ }
+// setMediaNotificationProvider(customMediaNotificationProvider)
+ }
+
+ private fun createMediaSourceFactory(): MediaSource.Factory {
+ Log.d(TAG, "createMediaSourceFactory")
+ return DefaultMediaSourceFactory(this)
}
// The user dismissed the app from the recent tasks
@@ -59,4 +76,49 @@ class PlaybackService : MediaSessionService() {
}
super.onDestroy()
}
+
+ private inner class MyCallback : MediaSession.Callback {
+ override fun onConnect(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo
+ ): MediaSession.ConnectionResult {
+ val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
+ if (session.isMediaNotificationController(controller)) {
+ val playerCommands =
+ MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
+ .removeAll()
+
+ val connectionResult = super.onConnect(session, controller)
+ val defaultPlayerCommands = connectionResult.availablePlayerCommands
+ Log.d(TAG, defaultPlayerCommands.toString())
+ notificationCustomButtons.forEach { commandButton ->
+ commandButton.sessionCommand?.let(sessionCommands::add)
+ }
+
+ return MediaSession.ConnectionResult.accept(
+ sessionCommands.build(),
+ playerCommands.build()
+ )
+ } else if (session.isAutoCompanionController(controller)) {
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
+ .setAvailableSessionCommands(sessionCommands.build())
+ .build()
+ }
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
+ }
+
+ override fun onCustomCommand(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo,
+ customCommand: SessionCommand,
+ args: Bundle
+ ): ListenableFuture {
+ when (customCommand.customAction) {
+ NotificationCustomButton.SKIP_BACK.customAction -> mediaSession?.player?.seekToPrevious()
+ NotificationCustomButton.SKIP_FORWARD.customAction -> mediaSession?.player?.seekToNext()
+ }
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/util/LogTag.kt b/app/src/main/java/com/player/musicoo/util/LogTag.kt
index 9fb6405..fdf7e58 100644
--- a/app/src/main/java/com/player/musicoo/util/LogTag.kt
+++ b/app/src/main/java/com/player/musicoo/util/LogTag.kt
@@ -4,4 +4,5 @@ object LogTag {
const val VO_ACT_LOG = "vo-act—log"
const val VO_FRAGMENT_LOG = "vo-fragment-log"
const val VO_API_LOG = "vo-api—log"
+ const val VO_SERVICE_LOG = "vo-service—log"
}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/util/Utils.kt b/app/src/main/java/com/player/musicoo/util/Utils.kt
new file mode 100644
index 0000000..903b4f6
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/util/Utils.kt
@@ -0,0 +1,77 @@
+package com.player.musicoo.util
+
+import android.net.Uri
+import android.os.Build
+import android.text.format.DateUtils
+import androidx.core.net.toUri
+import androidx.core.os.bundleOf
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
+import com.player.musicoo.innertube.Innertube
+import com.player.musicoo.innertube.models.bodies.ContinuationBody
+import com.player.musicoo.innertube.requests.playlistPage
+import com.player.musicoo.innertube.utils.plus
+
+val Innertube.SongItem.asMediaItem: MediaItem
+ get() = MediaItem.Builder()
+ .setMediaId(key)
+ .setMediaMetadata(
+ MediaMetadata.Builder()
+ .setTitle(info?.name)
+ .setArtist(authors?.joinToString("") { it.name ?: "" })
+ .setAlbumTitle(album?.name)
+ .setArtworkUri(bigThumbnail?.url?.toUri())
+ .setExtras(
+ bundleOf(
+ "albumId" to album?.endpoint?.browseId,
+ "durationText" to durationText,
+ "artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name },
+ "artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
+ )
+ )
+ .build()
+ )
+ .build()
+
+fun String?.thumbnail(size: Int): String? {
+ return when {
+ this?.startsWith("https://lh3.googleusercontent.com") == true -> "$this-w$size-h$size"
+ this?.startsWith("https://yt3.ggpht.com") == true -> "$this-w$size-h$size-s$size"
+ else -> this
+ }
+}
+
+fun Uri?.thumbnail(size: Int): Uri? {
+ return toString().thumbnail(size)?.toUri()
+}
+
+fun formatAsDuration(millis: Long) = DateUtils.formatElapsedTime(millis / 1000).removePrefix("0")
+
+suspend fun Result.completed(): Result? {
+ var playlistPage = getOrNull() ?: return null
+
+ while (playlistPage.songsPage?.continuation != null) {
+ val continuation = playlistPage.songsPage?.continuation!!
+ val otherPlaylistPageResult = Innertube.playlistPage(ContinuationBody(continuation = continuation)) ?: break
+
+ if (otherPlaylistPageResult.isFailure) break
+
+ otherPlaylistPageResult.getOrNull()?.let { otherSongsPage ->
+ playlistPage = playlistPage.copy(songsPage = playlistPage.songsPage + otherSongsPage)
+ }
+ }
+
+ return Result.success(playlistPage)
+}
+
+inline val isAtLeastAndroid6
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+
+inline val isAtLeastAndroid8
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+
+inline val isAtLeastAndroid12
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+
+inline val isAtLeastAndroid13
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
diff --git a/app/src/main/res/drawable/bg_playing_playback_progress.xml b/app/src/main/res/drawable/bg_playing_playback_progress.xml
new file mode 100644
index 0000000..ddd7c8d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_playing_playback_progress.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml b/app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml
new file mode 100644
index 0000000..c13cb1b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/music_list_icon.xml b/app/src/main/res/drawable/music_list_icon.xml
new file mode 100644
index 0000000..0a1b666
--- /dev/null
+++ b/app/src/main/res/drawable/music_list_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/pause.xml b/app/src/main/res/drawable/pause.xml
new file mode 100644
index 0000000..3280645
--- /dev/null
+++ b/app/src/main/res/drawable/pause.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml
new file mode 100644
index 0000000..4951da4
--- /dev/null
+++ b/app/src/main/res/drawable/play.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/play_mode_icon.xml b/app/src/main/res/drawable/play_mode_icon.xml
new file mode 100644
index 0000000..327283d
--- /dev/null
+++ b/app/src/main/res/drawable/play_mode_icon.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/play_skip_back.xml b/app/src/main/res/drawable/play_skip_back.xml
new file mode 100644
index 0000000..14602d8
--- /dev/null
+++ b/app/src/main/res/drawable/play_skip_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/play_skip_back_white_icon.xml b/app/src/main/res/drawable/play_skip_back_white_icon.xml
new file mode 100644
index 0000000..079f887
--- /dev/null
+++ b/app/src/main/res/drawable/play_skip_back_white_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/play_skip_forward.xml b/app/src/main/res/drawable/play_skip_forward.xml
new file mode 100644
index 0000000..24f14c4
--- /dev/null
+++ b/app/src/main/res/drawable/play_skip_forward.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/play_skip_forward_white_icon.xml b/app/src/main/res/drawable/play_skip_forward_white_icon.xml
new file mode 100644
index 0000000..fe9b57f
--- /dev/null
+++ b/app/src/main/res/drawable/play_skip_forward_white_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_mo_play_details.xml b/app/src/main/res/layout/activity_mo_play_details.xml
new file mode 100644
index 0000000..0014331
--- /dev/null
+++ b/app/src/main/res/layout/activity_mo_play_details.xml
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 905ca71..1701611 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,6 +3,7 @@
#FF000000
#99000000
#FFFFFFFF
+ #4DFFFFFF
#99FFFFFF
#CCFFFFFF
#151718
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b8fb464..0ce0f53 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -18,4 +18,5 @@
Resource Loading…
EXPAND
Description
+ An unknown playback error has occurred
\ No newline at end of file