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?
@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>>
@Transaction

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
}