This commit is contained in:
vfsfitvnm
2022-07-18 13:29:44 +02:00
parent 06b4fcdc63
commit 61eeeb07c0
13 changed files with 1355 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE
import android.os.Parcel
import androidx.core.database.getFloatOrNull
import androidx.media3.common.MediaItem
import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
@@ -14,8 +15,6 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.*
import it.vfsfitvnm.vimusic.utils.getFloatOrNull
import it.vfsfitvnm.vimusic.utils.getLongOrNull
import kotlinx.coroutines.flow.Flow
@@ -98,6 +97,14 @@ interface Database {
@RewriteQueriesToDropUnusedColumns
fun albumSongs(albumId: String): Flow<List<DetailedSong>>
@Query("SELECT * FROM Format WHERE songId = :songId")
fun format(songId: String): Flow<Format>
@Transaction
@Query("SELECT * FROM Song JOIN Format ON id = songId WHERE contentLength IS NOT NULL AND totalPlayTimeMs > 0 ORDER BY Song.ROWID DESC")
@RewriteQueriesToDropUnusedColumns
fun songsWithContentLength(): Flow<List<DetailedSongWithContentLength>>
@Query("UPDATE SongPlaylistMap SET position = position - 1 WHERE playlistId = :playlistId AND position >= :fromPosition")
fun decrementSongPositions(playlistId: Long, fromPosition: Int)
@@ -107,6 +114,9 @@ interface Database {
@Query("UPDATE SongPlaylistMap SET position = position + 1 WHERE playlistId = :playlistId AND position >= :fromPosition AND position <= :toPosition")
fun incrementSongPositions(playlistId: Long, fromPosition: Int, toPosition: Int)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(format: Format)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(searchQuery: SearchQuery)
@@ -141,9 +151,7 @@ interface Database {
title = mediaItem.mediaMetadata.title!!.toString(),
artistsText = mediaItem.mediaMetadata.artist?.toString(),
durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!,
thumbnailUrl = mediaItem.mediaMetadata.artworkUri?.toString(),
loudnessDb = mediaItem.mediaMetadata.extras?.getFloatOrNull("loudnessDb"),
contentLength = mediaItem.mediaMetadata.extras?.getLongOrNull("contentLength"),
thumbnailUrl = mediaItem.mediaMetadata.artworkUri?.toString()
).let(block).also { song ->
if (insert(song) == -1L) return
}
@@ -250,11 +258,12 @@ interface Database {
SongAlbumMap::class,
SearchQuery::class,
QueuedMediaItem::class,
Format::class,
],
views = [
SortedSongPlaylistMap::class
],
version = 13,
version = 15,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
@@ -267,6 +276,7 @@ interface Database {
AutoMigration(from = 9, to = 10),
AutoMigration(from = 11, to = 12, spec = DatabaseInitializer.From11To12Migration::class),
AutoMigration(from = 12, to = 13),
AutoMigration(from = 13, to = 14),
],
)
@TypeConverters(Converters::class)
@@ -281,7 +291,7 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
if (!::Instance.isInitialized) {
Instance = Room
.databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db")
.addMigrations(From8To9Migration(), From10To11Migration())
.addMigrations(From8To9Migration(), From10To11Migration(), From14To15Migration())
.build()
}
}
@@ -354,6 +364,26 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
@RenameTable("SongInPlaylist", "SongPlaylistMap")
@RenameTable("SortedSongInPlaylist", "SortedSongPlaylistMap")
class From11To12Migration : AutoMigrationSpec
class From14To15Migration : Migration(14, 15) {
override fun migrate(it: SupportSQLiteDatabase) {
it.query(SimpleSQLiteQuery("SELECT id, loudnessDb, contentLength FROM Song;")).use { cursor ->
val formatValues = ContentValues(3)
while (cursor.moveToNext()) {
formatValues.put("songId", cursor.getString(0))
formatValues.put("loudnessDb", cursor.getFloatOrNull(1))
formatValues.put("contentLength", cursor.getFloatOrNull(2))
it.insert("Format", CONFLICT_IGNORE, formatValues)
}
}
it.execSQL("CREATE TABLE IF NOT EXISTS `Song_new` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))")
it.execSQL("INSERT INTO Song_new(id, title, artistsText, durationText, thumbnailUrl, lyrics, likedAt, totalPlayTimeMs) SELECT id, title, artistsText, durationText, thumbnailUrl, lyrics, likedAt, totalPlayTimeMs FROM Song;")
it.execSQL("DROP TABLE Song;")
it.execSQL("ALTER TABLE Song_new RENAME TO Song;")
}
}
}
@TypeConverters

View File

@@ -5,7 +5,7 @@ import androidx.room.Junction
import androidx.room.Relation
data class DetailedSong(
open class DetailedSong(
@Embedded val song: Song,
@Relation(
entity = SongAlbumMap::class,

View File

@@ -0,0 +1,17 @@
package it.vfsfitvnm.vimusic.models
import androidx.room.Relation
class DetailedSongWithContentLength(
song: Song,
albumId: String?,
artists: List<Artist>?,
@Relation(
entity = Format::class,
entityColumn = "songId",
parentColumn = "id",
projection = ["contentLength"]
)
val contentLength: Long?
) : DetailedSong(song, albumId, artists)

View File

@@ -0,0 +1,26 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.*
@Immutable
@Entity(
foreignKeys = [
ForeignKey(
entity = Song::class,
parentColumns = ["id"],
childColumns = ["songId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Format(
@PrimaryKey val songId: String,
val itag: Int? = null,
val mimeType: String? = null,
val bitrate: Long? = null,
val contentLength: Long? = null,
val lastModified: Long? = null,
val loudnessDb: Float? = null
)

View File

@@ -12,9 +12,7 @@ data class Song(
val thumbnailUrl: String?,
val lyrics: String? = null,
val likedAt: Long? = null,
val totalPlayTimeMs: Long = 0,
val loudnessDb: Float? = null,
val contentLength: Long? = null,
val totalPlayTimeMs: Long = 0
) {
val formattedTotalPlayTime: String
get() {

View File

@@ -505,20 +505,20 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
player.findNextMediaItemById(videoId)
}
loudnessDb?.let { loudnessDb ->
mediaItem?.mediaMetadata?.extras
?.putFloat("loudnessDb", loudnessDb)
}
query {
mediaItem?.let(Database::insert)
format.contentLength?.let { contentLength ->
mediaItem?.mediaMetadata?.extras
?.putLong("contentLength", contentLength)
}
mediaItem?.let {
query {
Database.insert(it)
}
Database.insert(
it.vfsfitvnm.vimusic.models.Format(
songId = videoId,
itag = format.itag,
mimeType = format.mimeType,
bitrate = format.bitrate,
loudnessDb = loudnessDb,
contentLength = format.contentLength,
lastModified = format.lastModified
)
)
}
format.url

View File

@@ -368,7 +368,7 @@ fun MediaItemMenu(
)
}
onSetSleepTimer?.let { onSetSleepTimer ->
onSetSleepTimer?.let {
val binder = LocalPlayerServiceBinder.current
val (colorPalette, typography) = LocalAppearance.current

View File

@@ -28,8 +28,13 @@ import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.*
import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
import kotlinx.coroutines.Dispatchers
@@ -70,10 +75,10 @@ fun BuiltInPlaylistScreen(
val songs by remember(binder?.cache, builtInPlaylist) {
when (builtInPlaylist) {
BuiltInPlaylist.Favorites -> Database.favorites()
BuiltInPlaylist.Cached -> Database.songsByRowIdDesc().map { songs ->
BuiltInPlaylist.Cached -> Database.songsWithContentLength().map { songs ->
songs.filter { song ->
song.song.contentLength?.let { contentLength ->
binder?.cache?.isCached(song.song.id, 0, contentLength)
song.contentLength?.let {
binder?.cache?.isCached(song.song.id, 0, song.contentLength)
} ?: false
}
}

View File

@@ -13,10 +13,12 @@ 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.CircleShape
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
@@ -43,6 +45,7 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.Format
import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.*
@@ -50,8 +53,10 @@ import it.vfsfitvnm.vimusic.ui.components.themed.BaseMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.styling.*
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
import kotlin.math.roundToInt
@@ -186,7 +191,6 @@ fun PlayerView(
) {
Thumbnail(
playerState = playerState,
song = song,
modifier = Modifier
)
}
@@ -217,7 +221,6 @@ fun PlayerView(
) {
Thumbnail(
playerState = playerState,
song = song,
modifier = Modifier
)
}
@@ -314,7 +317,6 @@ fun PlayerView(
@Composable
private fun Thumbnail(
playerState: PlayerState,
song: Song?,
modifier: Modifier = Modifier
) {
val (_, typography) = LocalAppearance.current
@@ -378,25 +380,17 @@ private fun Thumbnail(
enter = fadeIn(),
exit = fadeOut(),
) {
var cachedBytes by remember(song?.id) {
mutableStateOf(binder.cache.getCachedBytes(playerState.mediaItem.mediaId, 0, -1))
val key = playerState.mediaItem.mediaId
var cachedBytes by remember(key) {
mutableStateOf(binder.cache.getCachedBytes(key, 0, -1))
}
val loudnessDb by remember {
derivedStateOf {
song?.loudnessDb ?: playerState.mediaMetadata.extras?.getFloatOrNull("loudnessDb")
}
}
val contentLength by remember {
derivedStateOf {
song?.contentLength ?: playerState.mediaMetadata.extras?.getLongOrNull("contentLength")
}
}
DisposableEffect(song?.id) {
val key = playerState.mediaItem.mediaId
val format by remember(key) {
Database.format(key)
}.collectAsState(initial = null, context = Dispatchers.IO)
DisposableEffect(key) {
val listener = object : Cache.Listener {
override fun onSpanAdded(cache: Cache, span: CacheSpan) {
cachedBytes += span.length
@@ -451,6 +445,10 @@ private fun Thumbnail(
text = "Loudness",
style = typography.xs.semiBold.color(BlackColorPalette.text)
)
BasicText(
text = "Bitrate",
style = typography.xs.semiBold.color(BlackColorPalette.text)
)
BasicText(
text = "Size",
style = typography.xs.semiBold.color(BlackColorPalette.text)
@@ -471,13 +469,19 @@ private fun Thumbnail(
style = typography.xs.semiBold.color(BlackColorPalette.text)
)
BasicText(
text = loudnessDb?.let { loudnessDb ->
text = format?.loudnessDb?.let { loudnessDb ->
"%.2f dB".format(loudnessDb)
} ?: "Unknown",
style = typography.xs.semiBold.color(BlackColorPalette.text)
)
BasicText(
text = contentLength?.let { contentLength ->
text = format?.bitrate?.let { bitrate ->
"${bitrate / 1000} kbps"
} ?: "Unknown",
style = typography.xs.semiBold.color(BlackColorPalette.text)
)
BasicText(
text = format?.contentLength?.let { contentLength ->
Formatter.formatShortFileSize(
context,
contentLength
@@ -489,7 +493,7 @@ private fun Thumbnail(
text = buildString {
append(Formatter.formatShortFileSize(context, cachedBytes))
contentLength?.let { contentLength ->
format?.contentLength?.let { contentLength ->
append(" (${(cachedBytes.toFloat() / contentLength * 100).roundToInt()}%)")
}
},
@@ -497,6 +501,41 @@ private fun Thumbnail(
)
}
}
if (format != null && format?.itag == null) {
BasicText(
text = "FETCH MISSING DATA",
style = typography.xxs.semiBold.color(BlackColorPalette.text),
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = {
query {
runBlocking(Dispatchers.IO) {
YouTube.player(key)?.map { response ->
response.streamingData?.adaptiveFormats?.findLast { format ->
format.itag == 251 || format.itag == 140
}?.let { format ->
Format(
songId = key,
itag = format.itag,
mimeType = format.mimeType,
bitrate = format.bitrate,
loudnessDb = response.playerConfig?.audioConfig?.loudnessDb?.toFloat(),
contentLength = format.contentLength,
lastModified = format.lastModified
)
}
}
}?.getOrNull()?.let(Database::insert)
}
}
)
.padding(all = 16.dp)
.align(Alignment.End)
)
}
}
}
}

View File

@@ -79,8 +79,7 @@ val DetailedSong.asMediaItem: MediaItem
"albumId" to albumId,
"artistNames" to artists?.map { it.name },
"artistIds" to artists?.map { it.id },
"durationText" to song.durationText,
"loudnessDb" to song.loudnessDb
"durationText" to song.durationText
)
)
.build()