From f5024933c7a130a12fa0bbd009a256111baf8f4b Mon Sep 17 00:00:00 2001 From: maxrave-dev Date: Sun, 19 Jan 2025 12:08:21 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(YouTube):=20Improve=20YouTube?= =?UTF-8?q?=20Music=20scraping=20reliability=20=F0=9F=90=9B=20fix(YouTube)?= =?UTF-8?q?:=20Handle=20various=20client=20responses=20for=20signature=20?= =?UTF-8?q?=E2=9C=A8=20feat(UI):=20Add=20share=20button=20to=20modal=20bot?= =?UTF-8?q?tom=20sheet=20=F0=9F=90=9B=20fix(Player):=20Fix=20orientation?= =?UTF-8?q?=20issues=20in=20fullscreen=20mode=20=F0=9F=90=9B=20fix(Reposit?= =?UTF-8?q?ory):=20Update=20visitor=20data=20after=20cookie=20change=20?= =?UTF-8?q?=E2=9C=A8=20feat(version):=20Bump=20version=20to=200.2.9=20?= =?UTF-8?q?=F0=9F=90=9B=20fix(UI):=20Improve=20Video?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/proguard-rules.pro | 9 +- .../data/repository/MainRepository.kt | 3 + .../com/maxrave/simpmusic/extension/UIExt.kt | 31 +- .../service/SimpleMediaServiceHandler.kt | 1 - .../simpmusic/ui/component/LibraryItem.kt | 15 +- .../simpmusic/ui/component/MediaPlayerView.kt | 59 +-- .../ui/component/ModalBottomSheet.kt | 19 + .../ui/screen/player/FullscreenPlayer.kt | 429 ++++++++++-------- .../ui/screen/player/NowPlayingScreen.kt | 5 +- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 4 +- .../maxrave/kotlinytmusicscraper/YouTube.kt | 60 ++- .../maxrave/kotlinytmusicscraper/Ytmusic.kt | 4 +- .../models/YouTubeClient.kt | 25 +- 14 files changed, 380 insertions(+), 285 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index bc23d336..75bdda80 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -37,4 +37,11 @@ public static int i(...); public static int d(...); public static int v(...); -} \ No newline at end of file +} +# Please add these rules to your existing keep rules in order to suppress warning +# This is generated automatically by the Android Gradle plugin. +-dontwarn java.beans.BeanDescriptor +-dontwarn java.beans.BeanInfo +-dontwarn java.beans.IntrospectionException +-dontwarn java.beans.Introspector +-dontwarn java.beans.PropertyDescriptor \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index f8e9d03c..1b6ae7ed 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -178,6 +178,9 @@ class MainRepository( dataStoreManager.cookie.distinctUntilChanged().collectLatest { cookie -> if (cookie.isNotEmpty()) { youTube.cookie = cookie + youTube.visitorData()?.let { + youTube.visitorData = it + } } else { youTube.cookie = null } diff --git a/app/src/main/java/com/maxrave/simpmusic/extension/UIExt.kt b/app/src/main/java/com/maxrave/simpmusic/extension/UIExt.kt index 524a97ea..6c98736b 100644 --- a/app/src/main/java/com/maxrave/simpmusic/extension/UIExt.kt +++ b/app/src/main/java/com/maxrave/simpmusic/extension/UIExt.kt @@ -490,15 +490,17 @@ fun Context.findActivity(): ComponentActivity { } @Composable -fun PipListenerPreAPI12() { +fun PipListenerPreAPI12(isInFullscreen: Boolean) { // [START android_compose_pip_pre12_listener] if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { - context - .findActivity() - .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) + if (isInFullscreen) { + context + .findActivity() + .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) + } } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior, @@ -518,20 +520,25 @@ fun PipListenerPreAPI12() { /** * Android 12 and above Picture in Picture mode */ -fun Modifier.pipModifier(context: Context) = - this.onGloballyPositioned { layoutCoordinates -> - val builder = PictureInPictureParams.Builder() +fun Modifier.pipModifier( + context: Context, + isInFullscreen: Boolean, +) = this.onGloballyPositioned { layoutCoordinates -> + val builder = PictureInPictureParams.Builder() + Log.d("PiP", "isInFullscreen: $isInFullscreen") + if (isInFullscreen) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(16, 9), ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setAutoEnterEnabled(true) - } - Log.w("PiP info", "layoutCoordinates: $layoutCoordinates") - context.findActivity().setPictureInPictureParams(builder.build()) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(isInFullscreen) + } + Log.w("PiP info", "layoutCoordinates: $layoutCoordinates") + context.findActivity().setPictureInPictureParams(builder.build()) +} @RequiresOptIn( level = RequiresOptIn.Level.WARNING, diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt index 677a4244..133e7a0e 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt @@ -1050,7 +1050,6 @@ class SimpleMediaServiceHandler( } fun getRelated(videoId: String) { -// Queue.clear() coroutineScope.launch { mainRepository.getRelatedData(videoId).collect { response -> when (response) { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/LibraryItem.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/LibraryItem.kt index 00a73f89..3b31e610 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/LibraryItem.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/LibraryItem.kt @@ -3,8 +3,12 @@ package com.maxrave.simpmusic.ui.component import android.os.Bundle import android.widget.Toast import androidx.compose.animation.Crossfade +import androidx.compose.foundation.MarqueeAnimationMode +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth @@ -100,8 +104,9 @@ fun LibraryItem( ) } Column { - Box( + Row( modifier = Modifier.padding(top = 15.dp, start = 10.dp, end = 10.dp), + verticalAlignment = Alignment.CenterVertically, ) { Text( text = title, @@ -111,13 +116,17 @@ fun LibraryItem( Modifier .fillMaxWidth() .height(35.dp) - .align(Alignment.CenterStart), + .wrapContentHeight(align = Alignment.CenterVertically) + .weight(1f) + .basicMarquee( + iterations = Int.MAX_VALUE, + animationMode = MarqueeAnimationMode.Immediately, + ).focusable(), ) if (state.type is LibraryItemType.LocalPlaylist || state.type is LibraryItemType.YouTubePlaylist) { TextButton( modifier = Modifier - .align(Alignment.CenterEnd) .defaultMinSize(minWidth = 1.dp, minHeight = 1.dp), onClick = { if (state.type is LibraryItemType.LocalPlaylist) { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt index dba60405..d0676f45 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/MediaPlayerView.kt @@ -1,6 +1,5 @@ package com.maxrave.simpmusic.ui.component -import android.os.Build import android.util.Log import android.view.TextureView import androidx.compose.animation.Crossfade @@ -29,6 +28,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Tracks import androidx.media3.common.VideoSize import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultDataSource @@ -46,9 +46,7 @@ import coil3.request.crossfade import coil3.toCoilUri import com.maxrave.simpmusic.common.Config import com.maxrave.simpmusic.extension.KeepScreenOn -import com.maxrave.simpmusic.extension.PipListenerPreAPI12 import com.maxrave.simpmusic.extension.getScreenSizeInfo -import com.maxrave.simpmusic.extension.pipModifier import org.koin.compose.koinInject import org.koin.core.qualifier.named import kotlin.math.roundToInt @@ -169,9 +167,7 @@ fun MediaPlayerView( fun MediaPlayerView( player: ExoPlayer, modifier: Modifier = Modifier, - pipSupport: Boolean = false, ) { - val context = LocalContext.current var videoRatio by rememberSaveable { mutableFloatStateOf(16f / 9) } @@ -184,28 +180,30 @@ fun MediaPlayerView( mutableStateOf(false) } - if (pipSupport) { - PipListenerPreAPI12() - } - val playerListener = remember { object : Player.Listener { - override fun onVideoSizeChanged(videoSize: VideoSize) { - super.onVideoSizeChanged(videoSize) - Log.w("MediaPlayerView", "Video size changed: ${videoSize.width} / ${videoSize.height}") - if (videoSize.width != 0 && videoSize.height != 0) { - showArtwork = false - videoRatio = videoSize.width.toFloat() / videoSize.height.toFloat() - } else if (videoSize.width == 0) { - showArtwork = true + override fun onTracksChanged(tracks: Tracks) { + super.onTracksChanged(tracks) + if (!tracks.groups.isEmpty()) { + for (arrayIndex in 0 until tracks.groups.size) { + var done = false + for (groupIndex in 0 until tracks.groups[arrayIndex].length) { + val sampleMimeType = tracks.groups[arrayIndex].getTrackFormat(groupIndex).sampleMimeType + if (sampleMimeType != null && sampleMimeType.contains("video")) { + showArtwork = false + done = true + break + } else { + showArtwork = true + } + } + if (done) { + break + } + } } } - - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - keepScreenOn = isPlaying - } } } @@ -218,24 +216,9 @@ fun MediaPlayerView( LaunchedEffect(player) { player.addListener(playerListener) } - LaunchedEffect(true) { - player.videoSize.let { - if (it.width == 0) { - showArtwork = true - } else { - showArtwork = false - } - } - } Box( - modifier.then( - if (pipSupport && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - Modifier.pipModifier(context) - } else { - Modifier - }, - ), + modifier = modifier, contentAlignment = Alignment.Center, ) { if (keepScreenOn) { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt b/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt index 366f28b7..0541d28b 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/component/ModalBottomSheet.kt @@ -74,6 +74,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity import androidx.core.text.isDigitsOnly import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController @@ -1218,6 +1219,24 @@ fun LocalPlaylistBottomSheet( onDelete() hideModalBottomSheet() } + ActionButton( + icon = painterResource(id = R.drawable.baseline_share_24), + text = if (ytPlaylistId != null) R.string.share else R.string.sync_first, + enable = (ytPlaylistId != null), + ) { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "text/plain" + val url = "https://music.youtube.com/playlist?list=${ + ytPlaylistId?.replaceFirst( + "VL", + "", + ) + }" + shareIntent.putExtra(Intent.EXTRA_TEXT, url) + val chooserIntent = + Intent.createChooser(shareIntent, context.getString(R.string.share_url)) + context.startActivity(chooserIntent) + } EndOfModalBottomSheet() } } diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt index 65db6d7d..74403560 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/FullscreenPlayer.kt @@ -1,6 +1,7 @@ package com.maxrave.simpmusic.ui.screen.player import android.content.pm.ActivityInfo +import android.os.Build import android.view.View import androidx.compose.animation.Crossfade import androidx.compose.foundation.MarqueeAnimationMode @@ -88,8 +89,10 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.navigation.NavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.maxrave.simpmusic.R +import com.maxrave.simpmusic.extension.PipListenerPreAPI12 import com.maxrave.simpmusic.extension.findActivity import com.maxrave.simpmusic.extension.formatDuration +import com.maxrave.simpmusic.extension.pipModifier import com.maxrave.simpmusic.ui.component.MediaPlayerView import com.maxrave.simpmusic.ui.component.NowPlayingBottomSheet import com.maxrave.simpmusic.ui.component.RippleIconButton @@ -107,11 +110,14 @@ import org.koin.compose.koinInject fun FullscreenPlayer( navController: NavController, player: ExoPlayer = koinInject(), - sharedViewModel: SharedViewModel = viewModel() + sharedViewModel: SharedViewModel = viewModel(), ) { val context = LocalContext.current + var isFullScreen by remember { mutableStateOf(true) } DisposableEffect(true) { + isFullScreen = true + sharedViewModel.isFullScreen = true val activity = context.findActivity() val originalOrientation = activity.requestedOrientation activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE @@ -126,6 +132,7 @@ fun FullscreenPlayer( // restore original orientation when view disappears activity.requestedOrientation = originalOrientation sharedViewModel.isFullScreen = false + isFullScreen = false } } @@ -229,9 +236,9 @@ fun FullscreenPlayer( 0..( lines.getOrNull(0)?.startTimeMs ?: "0" - ).toLong() - ) + ).toLong() ) + ) ) { currentLineIndex = -1 } @@ -240,37 +247,48 @@ fun FullscreenPlayer( } } - Box { + Box( + modifier = + Modifier.then( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Modifier.pipModifier(context, isFullScreen) + } else { + Modifier + }, + ), + ) { + PipListenerPreAPI12(isFullScreen) MediaPlayerView( player = player, modifier = Modifier.fillMaxSize(), - pipSupport = true ) if (nowPlayingState.lyricsData != null && !context.findActivity().isInPictureInPictureMode && shouldShowSubtitle) { Crossfade(currentLineIndex != -1) { val lines = nowPlayingState.lyricsData?.lyrics?.lines ?: return@Crossfade if (it) { Box( - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .fillMaxHeight() .padding(bottom = 40.dp) .align(Alignment.BottomCenter), - contentAlignment = Alignment.BottomCenter + contentAlignment = Alignment.BottomCenter, ) { Box(Modifier.fillMaxWidth(0.7f)) { Column( Modifier.align(Alignment.BottomCenter), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = lines.getOrNull(currentLineIndex)?.words ?: return@Crossfade, style = typo.bodyLarge, color = Color.White, textAlign = TextAlign.Center, - modifier = Modifier - .padding(4.dp) - .background(Color.Black.copy(alpha = 0.5f)) - .wrapContentWidth() + modifier = + Modifier + .padding(4.dp) + .background(Color.Black.copy(alpha = 0.5f)) + .wrapContentWidth(), ) Crossfade(nowPlayingState.lyricsData?.translatedLyrics?.lines != null, label = "") { translate -> val translateLines = nowPlayingState.lyricsData?.translatedLyrics?.lines ?: return@Crossfade @@ -280,9 +298,10 @@ fun FullscreenPlayer( style = typo.bodyMedium, color = Color.Yellow, textAlign = TextAlign.Center, - modifier = Modifier - .background(Color.Black.copy(alpha = 0.5f)) - .wrapContentWidth() + modifier = + Modifier + .background(Color.Black.copy(alpha = 0.5f)) + .wrapContentWidth(), ) } } @@ -295,18 +314,18 @@ fun FullscreenPlayer( Row(Modifier.fillMaxSize()) { // Left side Box( - Modifier.fillMaxHeight().weight(1f) + Modifier + .fillMaxHeight() + .weight(1f) .clip( RoundedCornerShape( topEndPercent = 10, - bottomEndPercent = 10 - ) - ) - .indication( + bottomEndPercent = 10, + ), + ).indication( interactionSource = interactionSourceBackward, - indication = ripple() - ) - .pointerInput(Unit) { + indication = ripple(), + ).pointerInput(Unit) { detectTapGestures( onTap = { showHideFullscreenOverlay = !showHideFullscreenOverlay @@ -321,44 +340,44 @@ fun FullscreenPlayer( interactionSourceBackward.emit(PressInteraction.Release(press)) doubleBackwardTapped = false } - } + }, ) }, - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Crossfade(showBackwardText) { if (it) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon( Icons.Filled.KeyboardDoubleArrowLeft, "", - tint = Color.White + tint = Color.White, ) Spacer(Modifier.width(4.dp)) Text( stringResource(R.string.five_seconds), color = Color.White, - style = typo.bodyMedium + style = typo.bodyMedium, ) } } } } Box( - Modifier.fillMaxHeight().weight(1f) + Modifier + .fillMaxHeight() + .weight(1f) .clip( RoundedCornerShape( topStartPercent = 10, - bottomStartPercent = 10 - ) - ) - .indication( + bottomStartPercent = 10, + ), + ).indication( interactionSource = interactionSourceForward, - indication = ripple() - ) - .pointerInput(Unit) { + indication = ripple(), + ).pointerInput(Unit) { detectTapGestures( onTap = { showHideFullscreenOverlay = !showHideFullscreenOverlay @@ -373,26 +392,26 @@ fun FullscreenPlayer( interactionSourceForward.emit(PressInteraction.Release(press)) doubleForwardTapped = false } - } + }, ) }, - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Crossfade(showForwardText) { if (it) { Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( stringResource(R.string.five_seconds), color = Color.White, - style = typo.bodyMedium + style = typo.bodyMedium, ) Spacer(Modifier.width(4.dp)) Icon( Icons.Filled.KeyboardDoubleArrowRight, "", - tint = Color.White + tint = Color.White, ) } } @@ -402,29 +421,35 @@ fun FullscreenPlayer( Crossfade(showHideFullscreenOverlay) { if (it) { Box( - modifier = Modifier.fillMaxSize() - .background(overlay) + modifier = + Modifier + .fillMaxSize() + .background(overlay), ) { TopAppBar( - modifier = Modifier.align(Alignment.TopCenter) - .padding(horizontal = 12.dp) - .fillMaxWidth(), + modifier = + Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 12.dp) + .fillMaxWidth(), windowInsets = WindowInsets(0, 0, 0, 0), - colors = TopAppBarDefaults.topAppBarColors().copy( - containerColor = Color.Transparent - ), + colors = + TopAppBarDefaults.topAppBarColors().copy( + containerColor = Color.Transparent, + ), title = { Text( text = nowPlayingState.nowPlayingTitle, style = typo.titleMedium, maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(align = Alignment.CenterVertically) - .basicMarquee( - iterations = Int.MAX_VALUE, - animationMode = MarqueeAnimationMode.Immediately, - ).focusable() + modifier = + Modifier + .fillMaxWidth() + .wrapContentHeight(align = Alignment.CenterVertically) + .basicMarquee( + iterations = Int.MAX_VALUE, + animationMode = MarqueeAnimationMode.Immediately, + ).focusable(), ) }, navigationIcon = { @@ -443,29 +468,29 @@ fun FullscreenPlayer( RippleIconButton( R.drawable.baseline_more_vert_24, ) { - showBottom = true + showBottom = true } - } + }, ) Row( Modifier .align(Alignment.Center) .fillMaxWidth(0.3f), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(48.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), enabled = controllerState.isPreviousAvailable, onClick = { sharedViewModel.onUIEvent(UIEvent.Previous) @@ -476,22 +501,22 @@ fun FullscreenPlayer( tint = if (controllerState.isPreviousAvailable) Color.White else Color.DarkGray, contentDescription = "", modifier = - Modifier - .size(36.dp) + Modifier + .size(36.dp), ) } FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(48.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), onClick = { sharedViewModel.onUIEvent(UIEvent.Backward) }, @@ -501,22 +526,22 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(36.dp) + Modifier + .size(36.dp), ) } FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(64.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(64.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), onClick = { sharedViewModel.onUIEvent(UIEvent.PlayPause) }, @@ -528,8 +553,8 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(48.dp) + Modifier + .size(48.dp), ) } else { Icon( @@ -537,24 +562,24 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(48.dp) + Modifier + .size(48.dp), ) } } } FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(48.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), onClick = { sharedViewModel.onUIEvent(UIEvent.Forward) }, @@ -564,22 +589,22 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(36.dp) + Modifier + .size(36.dp), ) } FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(48.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(48.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), enabled = controllerState.isNextAvailable, onClick = { sharedViewModel.onUIEvent(UIEvent.Next) @@ -590,36 +615,42 @@ fun FullscreenPlayer( tint = if (controllerState.isNextAvailable) Color.White else Color.DarkGray, contentDescription = "", modifier = - Modifier - .size(36.dp) + Modifier + .size(36.dp), ) } } Column( - modifier = Modifier.align(Alignment.BottomCenter) - .fillMaxWidth() - .fillMaxHeight(0.3f) - .padding(horizontal = 40.dp), - verticalArrangement = Arrangement.Bottom + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .fillMaxHeight(0.3f) + .padding(horizontal = 40.dp), + verticalArrangement = Arrangement.Bottom, ) { Box( - modifier = Modifier.height(32.dp) - .fillMaxWidth(), + modifier = + Modifier + .height(32.dp) + .fillMaxWidth(), ) { Row( - modifier = Modifier.fillMaxHeight() - .wrapContentWidth() - .align(Alignment.CenterStart), + modifier = + Modifier + .fillMaxHeight() + .wrapContentWidth() + .align(Alignment.CenterStart), verticalAlignment = Alignment.CenterVertically, ) { Text( text = formatDuration(timelineState.current, context), - style = typo.labelSmall + style = typo.labelSmall, ) Spacer(Modifier.width(4.dp)) Text( text = " / ${formatDuration(timelineState.total, context)}", - style = typo.bodySmall + style = typo.bodySmall, ) } Row( @@ -629,16 +660,16 @@ fun FullscreenPlayer( ) { FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(32.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(32.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), onClick = { shouldShowSubtitle = !shouldShowSubtitle }, @@ -650,8 +681,8 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(24.dp) + Modifier + .size(24.dp), ) } else { Icon( @@ -659,8 +690,8 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(24.dp) + Modifier + .size(24.dp), ) } } @@ -668,16 +699,16 @@ fun FullscreenPlayer( Spacer(Modifier.width(8.dp)) FilledTonalIconButton( colors = - IconButtonDefaults.iconButtonColors().copy( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .size(32.dp) - .aspectRatio(1f) - .clip( - CircleShape, + IconButtonDefaults.iconButtonColors().copy( + containerColor = Color.Transparent, ), + modifier = + Modifier + .size(32.dp) + .aspectRatio(1f) + .clip( + CircleShape, + ), onClick = { navController.navigateUp() }, @@ -687,8 +718,8 @@ fun FullscreenPlayer( tint = Color.White, contentDescription = "", modifier = - Modifier - .size(24.dp) + Modifier + .size(24.dp), ) } } @@ -701,9 +732,9 @@ fun FullscreenPlayer( ) { Box( modifier = - Modifier - .fillMaxWidth() - .height(24.dp), + Modifier + .fillMaxWidth() + .height(24.dp), contentAlignment = Alignment.Center, ) { Crossfade(timelineState.loading) { @@ -711,14 +742,14 @@ fun FullscreenPlayer( CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { LinearProgressIndicator( modifier = - Modifier - .fillMaxWidth() - .height(4.dp) - .padding( - horizontal = 3.dp, - ).clip( - RoundedCornerShape(8.dp), - ), + Modifier + .fillMaxWidth() + .height(4.dp) + .padding( + horizontal = 3.dp, + ).clip( + RoundedCornerShape(8.dp), + ), color = Color.Gray, trackColor = Color.DarkGray, strokeCap = StrokeCap.Round, @@ -729,14 +760,14 @@ fun FullscreenPlayer( LinearProgressIndicator( progress = { timelineState.bufferedPercent.toFloat() / 100 }, modifier = - Modifier - .fillMaxWidth() - .height(4.dp) - .padding( - horizontal = 3.dp, - ).clip( - RoundedCornerShape(8.dp), - ), + Modifier + .fillMaxWidth() + .height(4.dp) + .padding( + horizontal = 3.dp, + ).clip( + RoundedCornerShape(8.dp), + ), color = Color.Gray, trackColor = Color.DarkGray, strokeCap = StrokeCap.Round, @@ -756,25 +787,25 @@ fun FullscreenPlayer( }, valueRange = 0f..100f, modifier = - Modifier - .fillMaxWidth() - .padding(top = 3.dp) - .align( - Alignment.TopCenter, - ), + Modifier + .fillMaxWidth() + .padding(top = 3.dp) + .align( + Alignment.TopCenter, + ), track = { sliderState -> SliderDefaults.Track( modifier = - Modifier - .height(5.dp), + Modifier + .height(5.dp), enabled = true, sliderState = sliderState, colors = - SliderDefaults.colors().copy( - thumbColor = Color.White, - activeTrackColor = Color.White, - inactiveTrackColor = Color.Transparent, - ), + SliderDefaults.colors().copy( + thumbColor = Color.White, + activeTrackColor = Color.White, + inactiveTrackColor = Color.Transparent, + ), thumbTrackGapSize = 0.dp, drawTick = { _, _ -> }, drawStopIndicator = null, @@ -783,23 +814,23 @@ fun FullscreenPlayer( thumb = { SliderDefaults.Thumb( modifier = - Modifier - .height(18.dp) - .width(8.dp) - .padding( - vertical = 4.dp, - ), + Modifier + .height(18.dp) + .width(8.dp) + .padding( + vertical = 4.dp, + ), thumbSize = DpSize(8.dp, 8.dp), interactionSource = - remember { - MutableInteractionSource() - }, + remember { + MutableInteractionSource() + }, colors = - SliderDefaults.colors().copy( - thumbColor = Color.White, - activeTrackColor = Color.White, - inactiveTrackColor = Color.Transparent, - ), + SliderDefaults.colors().copy( + thumbColor = Color.White, + activeTrackColor = Color.White, + inactiveTrackColor = Color.Transparent, + ), enabled = true, ) }, @@ -816,7 +847,7 @@ fun FullscreenPlayer( navController = navController, setSleepTimerEnable = true, changeMainLyricsProviderEnable = true, - song = null + song = null, ) } } diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt index 63025878..d75fde20 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt @@ -142,6 +142,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy +import org.koin.compose.koinInject @OptIn(ExperimentalFoundationApi::class) @UnstableApi @@ -222,7 +223,7 @@ fun NowPlayingScreen( } LaunchedEffect(true) { - val activity = context.findActivity() ?: return@LaunchedEffect + val activity = context.findActivity() val bottom = activity.findViewById(R.id.bottom_navigation_view) val miniplayer = activity.findViewById(R.id.miniplayer) if (bottom.visibility != View.GONE || miniplayer.visibility != View.GONE) { @@ -644,7 +645,7 @@ fun NowPlayingScreen( ) { // Player Box(Modifier.fillMaxSize()) { - MediaPlayerView(player = sharedViewModel.getPlayer(), modifier = Modifier.align(Alignment.Center)) + MediaPlayerView(player = koinInject(), modifier = Modifier.align(Alignment.Center)) } Box( modifier = diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fc3cda1..c5d4c59b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,4 +377,5 @@ Five seconds LRCLIB Lyrics provided by LRCLIB + Sync this playlist to YouTube Music first \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d04a30e..32646c83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # App version -version-name="0.2.8" -version-code="25" +version-name="0.2.9" +version-code="26" android = "8.8.0" kotlin = "2.1.0" diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt index 05879d7e..360ad500 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt @@ -19,6 +19,7 @@ import com.maxrave.kotlinytmusicscraper.models.SongItem import com.maxrave.kotlinytmusicscraper.models.VideoItem import com.maxrave.kotlinytmusicscraper.models.WatchEndpoint import com.maxrave.kotlinytmusicscraper.models.YTItemType +import com.maxrave.kotlinytmusicscraper.models.YouTubeClient.Companion.TVHTML5 import com.maxrave.kotlinytmusicscraper.models.YouTubeClient.Companion.WEB import com.maxrave.kotlinytmusicscraper.models.YouTubeClient.Companion.WEB_REMIX import com.maxrave.kotlinytmusicscraper.models.YouTubeLocale @@ -1163,7 +1164,6 @@ class YouTube { println("Visitor Data $visitorData") println("New Cookie $cookie") println("Playback Tracking $playbackTracking") - if (!visitorData.isNullOrEmpty()) this@YouTube.visitorData = visitorData return Triple(cookie, visitorData ?: this@YouTube.visitorData, playbackTracking) } catch (e: Exception) { e.printStackTrace() @@ -1187,38 +1187,70 @@ class YouTube { ] }.joinToString("") val sigTimestamp = getSignatureTimestamp(videoId) - val sigResponse = ytMusic.player(WEB_REMIX, videoId, playlistId, cpn, signatureTimestamp = sigTimestamp).body() + val listClients = listOf(TVHTML5) + var sigResponse: PlayerResponse? = null + for (client in listClients) { + Log.w("YouTube", "Client $client") + val tempRes = ytMusic.player(client, videoId, playlistId, cpn, signatureTimestamp = sigTimestamp).body() + Log.w("YouTube", "TempRes ${tempRes.playabilityStatus}") + if (tempRes.playabilityStatus.status != "OK") { + continue + } else { + sigResponse = tempRes + break + } + } val decodedSigResponse = - sigResponse.copy( + sigResponse?.copy( streamingData = sigResponse.streamingData?.copy( formats = - sigResponse.streamingData.formats?.map { format -> + sigResponse.streamingData?.formats?.map { format -> format.copy( url = format.signatureCipher?.let { decodeSignatureCipher(videoId, it) }, ) }, adaptiveFormats = - sigResponse.streamingData.adaptiveFormats.map { adaptiveFormats -> + sigResponse.streamingData?.adaptiveFormats?.map { adaptiveFormats -> adaptiveFormats.copy( url = adaptiveFormats.signatureCipher?.let { decodeSignatureCipher(videoId, it) }, ) - }, + } ?: emptyList(), ), ) val listUrlSig = ( - decodedSigResponse.streamingData + decodedSigResponse + ?.streamingData ?.adaptiveFormats ?.mapNotNull { it.url } ?.toMutableList() ?: mutableListOf() ).apply { - decodedSigResponse.streamingData + decodedSigResponse + ?.streamingData ?.formats ?.mapNotNull { it.url } ?.let { addAll(it) } } - if (listUrlSig.isEmpty()) { + Log.w("YouTube", "URL ${decodedSigResponse?.streamingData?.formats?.mapNotNull { it.url }}") + val listFormat = + ( + decodedSigResponse + ?.streamingData + ?.formats + ?.mapNotNull { Pair(it.itag, it.url) } + ?.toMutableList() ?: mutableListOf() + ).apply { + addAll( + decodedSigResponse?.streamingData?.adaptiveFormats?.map { + Pair(it.itag, it.url) + } ?: emptyList(), + ) + } + listFormat.forEach { + Log.d("YouTube", "Format ${it.first} ${it.second}") + } + if (listUrlSig.isEmpty() || decodedSigResponse == null) { val (tempCookie, visitorData, playbackTracking) = getVisitorData(videoId, playlistId) val now = System.currentTimeMillis() val poToken = @@ -1235,7 +1267,7 @@ class YouTube { }?.let { poTokenChallenge -> ytMusic.generatePoToken(poTokenChallenge).bodyAsText().getPoToken().also { poToken -> if (poToken != null) { - poTokenObject = Pair(poToken, now + 21600000) + poTokenObject = Pair(poToken, now + 3600) } } } @@ -1595,8 +1627,8 @@ class YouTube { } } - suspend fun visitorData(): Result = - runCatching { + suspend fun visitorData(): String? = + try { Json .parseToJsonElement(ytMusic.getSwJsData().bodyAsText().substring(5)) .jsonArray[0] @@ -1604,6 +1636,9 @@ class YouTube { .jsonArray .first { (it as? JsonPrimitive)?.content?.startsWith(VISITOR_DATA_PREFIX) == true } .jsonPrimitive.content + } catch (e: Exception) { + e.printStackTrace() + null } suspend fun accountInfo(): Result = @@ -1737,6 +1772,7 @@ class YouTube { */ private fun getSignatureTimestamp(videoId: String): Int? = try { + YoutubeJavaScriptPlayerManager.clearAllCaches() YoutubeJavaScriptPlayerManager.getSignatureTimestamp(videoId) } catch (e: Exception) { e.printStackTrace() diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt index a4ce10d7..888fdb1f 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt @@ -141,7 +141,7 @@ class Ytmusic { ) { contentType(ContentType.Application.Json) headers { -// append("X-Goog-Api-Format-Version", "1") + append("X-Goog-Api-Format-Version", "1") append("X-YouTube-Client-Name", "${client.xClientName ?: 1}") append("X-YouTube-Client-Version", client.clientVersion) append("x-origin", "https://music.youtube.com") @@ -150,8 +150,6 @@ class Ytmusic { } if (setLogin) { cookie?.let { cookie -> - append("X-Goog-Authuser", "0") - append("X-Goog-Visitor-Id", visitorData) append("Cookie", cookie) if ("SAPISID" !in cookieMap || "__Secure-3PAPISID" !in cookieMap) return@let val currentTime = System.currentTimeMillis() / 1000 diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/YouTubeClient.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/YouTubeClient.kt index 14255240..0575609c 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/YouTubeClient.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/YouTubeClient.kt @@ -57,7 +57,7 @@ data class YouTubeClient( userAgent = USER_AGENT_ANDROID, osName = "Android", osVersion = "11", - xClientName = 21 + xClientName = 21, ) val ANDROID = @@ -90,7 +90,7 @@ data class YouTubeClient( clientName = "TVHTML5_SIMPLY_EMBEDDED_PLAYER", clientVersion = "2.0", api_key = "AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8", - userAgent = "Mozilla/5.0 (PlayStation 4 5.55) AppleWebKit/601.2 (KHTML, like Gecko)", + userAgent = "Mozilla/5.0 (PlayStation; PlayStation 4/12.00) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15", ) val IOS = @@ -105,17 +105,18 @@ data class YouTubeClient( osVersion = "17.5.1.21F90", timeZone = "UTC", utcOffsetMinutes = 0, - xClientName = 5 + xClientName = 5, ) - val MWEB = YouTubeClient( - clientName = "MWEB", - clientVersion = "2.20241202.07.00", - api_key = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX3", - userAgent = USER_AGENT_MWEB, - timeZone = "UTC", - utcOffsetMinutes = 0, - xClientName = 2 - ) + val MWEB = + YouTubeClient( + clientName = "MWEB", + clientVersion = "2.20241202.07.00", + api_key = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX3", + userAgent = USER_AGENT_MWEB, + timeZone = "UTC", + utcOffsetMinutes = 0, + xClientName = 2, + ) } } \ No newline at end of file