From 68a14796ea357032cd65e259f0180b6017f6e786 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 21 Jun 2022 22:56:35 +0200 Subject: [PATCH] Add stats for nerds (#33) --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 2 +- .../vimusic/services/PlayerService.kt | 26 +-- .../vfsfitvnm/vimusic/ui/views/PlayerView.kt | 164 +++++++++++++++++- .../it/vfsfitvnm/vimusic/utils/PlayerState.kt | 7 + 4 files changed, 177 insertions(+), 22 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index e247b20..6f2b4b6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -47,7 +47,7 @@ interface Database { fun songWithInfo(id: String): SongWithInfo? @Transaction - @Query("SELECT * FROM Song WHERE totalPlayTimeMs >= 15000 ORDER BY ROWID DESC") + @Query("SELECT * FROM Song ORDER BY ROWID DESC") fun history(): Flow> @Transaction diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt index b480ee5..460d8d9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/services/PlayerService.kt @@ -57,6 +57,7 @@ val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.E val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY) val GetCacheSizeCommand = SessionCommand("GetCacheSizeCommand", Bundle.EMPTY) +val GetSongCacheSizeCommand = SessionCommand("GetSongCacheSizeCommand", Bundle.EMPTY) val DeleteSongCacheCommand = SessionCommand("DeleteSongCacheCommand", Bundle.EMPTY) @@ -139,18 +140,6 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific .build() player.addListener(this) - - coroutineScope.launch(Dispatchers.IO) { - while (true) { - delay(1000) - withContext(Dispatchers.Main) { - println("volume: ${player.volume}") - } - songPendingLoudnessDb.forEach { (key, value) -> - println(" $key = $value") - } - } - } } override fun onDestroy() { @@ -173,6 +162,7 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific .add(StartArtistRadioCommand) .add(StopRadioCommand) .add(GetCacheSizeCommand) + .add(GetSongCacheSizeCommand) .add(DeleteSongCacheCommand) .add(SetSkipSilenceCommand) .add(GetAudioSessionIdCommand) @@ -217,6 +207,18 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific ) ) } + GetSongCacheSizeCommand -> { + return Futures.immediateFuture( + SessionResult( + SessionResult.RESULT_SUCCESS, + bundleOf("cacheSize" to cache.getCachedBytes( + args.getString("videoId") ?: "", + 0, + C.LENGTH_UNSET.toLong() + )) + ) + ) + } DeleteSongCacheCommand -> { args.getString("videoId")?.let { videoId -> cache.removeResource(videoId) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt index 5dc0b41..b58f762 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt @@ -1,27 +1,36 @@ package it.vfsfitvnm.vimusic.ui.views import android.text.format.DateUtils +import android.text.format.Formatter import androidx.compose.animation.* import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player @@ -29,16 +38,21 @@ import coil.compose.AsyncImage import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness +import it.vfsfitvnm.vimusic.services.GetSongCacheSizeCommand import it.vfsfitvnm.vimusic.ui.components.* import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.youtubemusic.Outcome +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.PlayerResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch +import kotlin.math.roundToInt @ExperimentalAnimationApi @@ -54,6 +68,7 @@ fun PlayerView( val density = LocalDensity.current val configuration = LocalConfiguration.current val player = LocalYoutubePlayer.current + val context = LocalContext.current val coroutineScope = rememberCoroutineScope() @@ -161,9 +176,15 @@ fun PlayerView( } ) { val song by remember(player.mediaItem?.mediaId) { - player.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() ?: flowOf(null) + player.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() ?: flowOf( + null + ) }.collectAsState(initial = null, context = Dispatchers.IO) + var isShowingStatsForNerds by rememberSaveable { + mutableStateOf(false) + } + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -219,20 +240,141 @@ fun PlayerView( .align(Alignment.CenterHorizontally) ) { val artworkUri = remember(it) { - player.mediaController.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(thumbnailSizePx) + player.mediaController.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail( + thumbnailSizePx + ) } - AsyncImage( - model = artworkUri, - contentDescription = null, - contentScale = ContentScale.Crop, + Box( modifier = Modifier .padding(bottom = 32.dp) .padding(horizontal = 32.dp) .aspectRatio(1f) .clip(ThumbnailRoundness.shape) .size(thumbnailSizeDp) - ) + ) { + AsyncImage( + model = artworkUri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { + isShowingStatsForNerds = true + } + ) + } + .fillMaxSize() + ) + + androidx.compose.animation.AnimatedVisibility( + visible = isShowingStatsForNerds, + enter = fadeIn(), + exit = fadeOut(), + ) { + val cachedPercentage = remember(song?.contentLength) { + song?.contentLength?.let { contentLength -> + player.mediaController.syncCommand( + GetSongCacheSizeCommand, + bundleOf("videoId" to song?.id) + ).extras.getLong("cacheSize").toFloat() / contentLength * 100 + }?.roundToInt() ?: 0 + } + + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures( + onPress = { + isShowingStatsForNerds = false + } + ) + } + .background(Color.Black.copy(alpha = 0.8f)) + .fillMaxSize() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(all = 16.dp) + ) { + Column { + BasicText( + text = "Volume", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + BasicText( + text = "Loudness", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + BasicText( + text = "Size", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + BasicText( + text = "Cached", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + } + + Column { + BasicText( + text = "${player.volume.times(100).roundToInt()}%", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + BasicText( + text = song?.loudnessDb?.let { loudnessDb -> + "%.2f dB".format(loudnessDb) + } ?: "Unknown", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + BasicText( + text = song?.contentLength?.let { contentLength -> + Formatter.formatShortFileSize(context, contentLength) + } ?: "Unknown", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + BasicText( + text = "$cachedPercentage%", + style = typography.xs.semiBold.color(BlackColorPalette.text) + ) + } + } + + if (song != null && (song?.contentLength == null || song?.loudnessDb == null)) { + BasicText( + text = "FILL MISSING DATA", + style = typography.xxs.semiBold.color(BlackColorPalette.text), + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + song?.let { song -> + coroutineScope.launch(Dispatchers.IO) { + YouTube.player(song.id).map { body -> + Database.update( + song.copy( + loudnessDb = body.playerConfig?.audioConfig?.loudnessDb?.toFloat(), + contentLength = body.streamingData?.adaptiveFormats?.findLast { format -> + format.itag == 251 || format.itag == 140 + }?.let(PlayerResponse.StreamingData.AdaptiveFormat::contentLength) + ) + ) + } + } + } + } + ) + .padding(all = 16.dp) + .align(Alignment.End) + ) + } + } + } + } } } else { Box( @@ -289,7 +431,9 @@ fun PlayerView( } }, onDragEnd = { - player.mediaController.seekTo(scrubbingPosition ?: player.mediaController.currentPosition) + player.mediaController.seekTo( + scrubbingPosition ?: player.mediaController.currentPosition + ) player.currentPosition = player.mediaController.currentPosition scrubbingPosition = null }, @@ -311,7 +455,9 @@ fun PlayerView( .padding(bottom = 16.dp) ) { BasicText( - text = DateUtils.formatElapsedTime((scrubbingPosition ?: player.currentPosition) / 1000), + text = DateUtils.formatElapsedTime( + (scrubbingPosition ?: player.currentPosition) / 1000 + ), style = typography.xxs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt index 7f832ec..e657bfa 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/PlayerState.kt @@ -46,6 +46,9 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener { var mediaItems by mutableStateOf(mediaController.currentTimeline.mediaItems) private set + var volume by mutableStateOf(mediaController.volume) + private set + init { handler.post(object : Runnable { override fun run() { @@ -56,6 +59,10 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener { }) } + override fun onVolumeChanged(volume: Float) { + this.volume = volume + } + override fun onPlaybackStateChanged(playbackState: Int) { this.playbackState = playbackState }