From 2a0d2abfea9ae83478d305ab1e4814ee385b9d13 Mon Sep 17 00:00:00 2001 From: ocean <503259349@qq.com> Date: Tue, 7 May 2024 16:06:37 +0800 Subject: [PATCH] update --- app/build.gradle.kts | 7 + app/proguard-rules.pro | 4 +- app/src/main/AndroidManifest.xml | 8 +- ...lsActivity.kt => MoListDetailsActivity.kt} | 2 +- .../musicoo/activity/MoPlayDetailsActivity.kt | 262 ++++++++++++++ .../musicoo/adapter/ResponsiveListAdapter.kt | 91 +++-- .../musicoo/adapter/TowRowListAdapter.kt | 18 +- .../com/player/musicoo/innertube/Innertube.kt | 1 + .../musicoo/innertube/requests/HomePage.kt | 1 - .../musicoo/innertube/requests/MoNextPage.kt | 58 ++++ .../FromMusicResponsiveListItemRenderer.kt | 10 + .../utils/FromMusicShelfRendererContent.kt | 2 + .../utils/FromPlaylistPanelVideoRenderer.kt | 8 + .../musicoo/media/MediaControllerManager.kt | 52 ++- .../musicoo/media/MediaControllerUtils.kt | 10 + .../com/player/musicoo/media/SongRadio.kt | 50 +++ .../CustomMediaNotificationProvider.kt | 43 +++ .../service/NotificationCustomButton.kt | 30 ++ .../musicoo/service/PlaybackExceptions.kt | 17 + .../player/musicoo/service/PlaybackService.kt | 82 ++++- .../java/com/player/musicoo/util/LogTag.kt | 1 + .../java/com/player/musicoo/util/Utils.kt | 77 +++++ .../drawable/bg_playing_playback_progress.xml | 25 ++ .../ic_playing_playback_progress_thumb.xml | 8 + app/src/main/res/drawable/music_list_icon.xml | 9 + app/src/main/res/drawable/pause.xml | 12 + app/src/main/res/drawable/play.xml | 9 + app/src/main/res/drawable/play_mode_icon.xml | 12 + app/src/main/res/drawable/play_skip_back.xml | 9 + .../drawable/play_skip_back_white_icon.xml | 9 + .../main/res/drawable/play_skip_forward.xml | 9 + .../drawable/play_skip_forward_white_icon.xml | 9 + .../res/layout/activity_mo_play_details.xml | 320 ++++++++++++++++++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 1 + 35 files changed, 1203 insertions(+), 64 deletions(-) rename app/src/main/java/com/player/musicoo/activity/{DetailsActivity.kt => MoListDetailsActivity.kt} (97%) create mode 100644 app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt create mode 100644 app/src/main/java/com/player/musicoo/innertube/requests/MoNextPage.kt create mode 100644 app/src/main/java/com/player/musicoo/media/MediaControllerUtils.kt create mode 100644 app/src/main/java/com/player/musicoo/media/SongRadio.kt create mode 100644 app/src/main/java/com/player/musicoo/service/CustomMediaNotificationProvider.kt create mode 100644 app/src/main/java/com/player/musicoo/service/NotificationCustomButton.kt create mode 100644 app/src/main/java/com/player/musicoo/service/PlaybackExceptions.kt create mode 100644 app/src/main/java/com/player/musicoo/util/Utils.kt create mode 100644 app/src/main/res/drawable/bg_playing_playback_progress.xml create mode 100644 app/src/main/res/drawable/ic_playing_playback_progress_thumb.xml create mode 100644 app/src/main/res/drawable/music_list_icon.xml create mode 100644 app/src/main/res/drawable/pause.xml create mode 100644 app/src/main/res/drawable/play.xml create mode 100644 app/src/main/res/drawable/play_mode_icon.xml create mode 100644 app/src/main/res/drawable/play_skip_back.xml create mode 100644 app/src/main/res/drawable/play_skip_back_white_icon.xml create mode 100644 app/src/main/res/drawable/play_skip_forward.xml create mode 100644 app/src/main/res/drawable/play_skip_forward_white_icon.xml create mode 100644 app/src/main/res/layout/activity_mo_play_details.xml 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