Add stats for nerds (#33)

This commit is contained in:
vfsfitvnm
2022-06-21 22:56:35 +02:00
parent 8a94c7e714
commit 68a14796ea
4 changed files with 177 additions and 22 deletions

View File

@@ -47,7 +47,7 @@ interface Database {
fun songWithInfo(id: String): SongWithInfo? fun songWithInfo(id: String): SongWithInfo?
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs >= 15000 ORDER BY ROWID DESC") @Query("SELECT * FROM Song ORDER BY ROWID DESC")
fun history(): Flow<List<SongWithInfo>> fun history(): Flow<List<SongWithInfo>>
@Transaction @Transaction

View File

@@ -57,6 +57,7 @@ val StartArtistRadioCommand = SessionCommand("StartArtistRadioCommand", Bundle.E
val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY) val StopRadioCommand = SessionCommand("StopRadioCommand", Bundle.EMPTY)
val GetCacheSizeCommand = SessionCommand("GetCacheSizeCommand", Bundle.EMPTY) val GetCacheSizeCommand = SessionCommand("GetCacheSizeCommand", Bundle.EMPTY)
val GetSongCacheSizeCommand = SessionCommand("GetSongCacheSizeCommand", Bundle.EMPTY)
val DeleteSongCacheCommand = SessionCommand("DeleteSongCacheCommand", Bundle.EMPTY) val DeleteSongCacheCommand = SessionCommand("DeleteSongCacheCommand", Bundle.EMPTY)
@@ -139,18 +140,6 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
.build() .build()
player.addListener(this) 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() { override fun onDestroy() {
@@ -173,6 +162,7 @@ class PlayerService : MediaSessionService(), MediaSession.Callback, MediaNotific
.add(StartArtistRadioCommand) .add(StartArtistRadioCommand)
.add(StopRadioCommand) .add(StopRadioCommand)
.add(GetCacheSizeCommand) .add(GetCacheSizeCommand)
.add(GetSongCacheSizeCommand)
.add(DeleteSongCacheCommand) .add(DeleteSongCacheCommand)
.add(SetSkipSilenceCommand) .add(SetSkipSilenceCommand)
.add(GetAudioSessionIdCommand) .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 -> { DeleteSongCacheCommand -> {
args.getString("videoId")?.let { videoId -> args.getString("videoId")?.let { videoId ->
cache.removeResource(videoId) cache.removeResource(videoId)

View File

@@ -1,27 +1,36 @@
package it.vfsfitvnm.vimusic.ui.views package it.vfsfitvnm.vimusic.ui.views
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.format.Formatter
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
@@ -29,16 +38,21 @@ import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.services.GetSongCacheSizeCommand
import it.vfsfitvnm.vimusic.ui.components.* import it.vfsfitvnm.vimusic.ui.components.*
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu 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.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@ExperimentalAnimationApi @ExperimentalAnimationApi
@@ -54,6 +68,7 @@ fun PlayerView(
val density = LocalDensity.current val density = LocalDensity.current
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val player = LocalYoutubePlayer.current val player = LocalYoutubePlayer.current
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -161,9 +176,15 @@ fun PlayerView(
} }
) { ) {
val song by remember(player.mediaItem?.mediaId) { 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) }.collectAsState(initial = null, context = Dispatchers.IO)
var isShowingStatsForNerds by rememberSaveable {
mutableStateOf(false)
}
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
@@ -219,20 +240,141 @@ fun PlayerView(
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
) { ) {
val artworkUri = remember(it) { val artworkUri = remember(it) {
player.mediaController.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(thumbnailSizePx) player.mediaController.getMediaItemAt(it).mediaMetadata.artworkUri.thumbnail(
thumbnailSizePx
)
} }
AsyncImage( Box(
model = artworkUri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.padding(bottom = 32.dp) .padding(bottom = 32.dp)
.padding(horizontal = 32.dp) .padding(horizontal = 32.dp)
.aspectRatio(1f) .aspectRatio(1f)
.clip(ThumbnailRoundness.shape) .clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp) .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 { } else {
Box( Box(
@@ -289,7 +431,9 @@ fun PlayerView(
} }
}, },
onDragEnd = { onDragEnd = {
player.mediaController.seekTo(scrubbingPosition ?: player.mediaController.currentPosition) player.mediaController.seekTo(
scrubbingPosition ?: player.mediaController.currentPosition
)
player.currentPosition = player.mediaController.currentPosition player.currentPosition = player.mediaController.currentPosition
scrubbingPosition = null scrubbingPosition = null
}, },
@@ -311,7 +455,9 @@ fun PlayerView(
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
) { ) {
BasicText( BasicText(
text = DateUtils.formatElapsedTime((scrubbingPosition ?: player.currentPosition) / 1000), text = DateUtils.formatElapsedTime(
(scrubbingPosition ?: player.currentPosition) / 1000
),
style = typography.xxs.semiBold, style = typography.xxs.semiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,

View File

@@ -46,6 +46,9 @@ open class PlayerState(val mediaController: MediaController) : Player.Listener {
var mediaItems by mutableStateOf(mediaController.currentTimeline.mediaItems) var mediaItems by mutableStateOf(mediaController.currentTimeline.mediaItems)
private set private set
var volume by mutableStateOf(mediaController.volume)
private set
init { init {
handler.post(object : Runnable { handler.post(object : Runnable {
override fun run() { 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) { override fun onPlaybackStateChanged(playbackState: Int) {
this.playbackState = playbackState this.playbackState = playbackState
} }