Move song lyrics to a separate database entity

This commit is contained in:
vfsfitvnm 2022-10-29 19:47:39 +02:00
parent 33221746fc
commit accbfc47d0
8 changed files with 859 additions and 141 deletions

View file

@ -88,7 +88,6 @@ dependencies {
implementation(libs.room) implementation(libs.room)
kapt(libs.room.compiler) kapt(libs.room.compiler)
annotationProcessor(libs.room.compiler)
implementation(projects.innertube) implementation(projects.innertube)
implementation(projects.kugou) implementation(projects.kugou)

View file

@ -0,0 +1,672 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "205c24811149a247279bcbfdc2d6c396",
"entities": [
{
"tableName": "Song",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistsText",
"columnName": "artistsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durationText",
"columnName": "durationText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "likedAt",
"columnName": "likedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "totalPlayTimeMs",
"columnName": "totalPlayTimeMs",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongPlaylistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"playlistId"
]
},
"indices": [
{
"name": "index_SongPlaylistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongPlaylistMap_playlistId",
"unique": false,
"columnNames": [
"playlistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Playlist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"playlistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Playlist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "browseId",
"columnName": "browseId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Artist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `thumbnailUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkedAt",
"columnName": "bookmarkedAt",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongArtistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"artistId"
]
},
"indices": [
{
"name": "index_SongArtistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongArtistMap_artistId",
"unique": false,
"columnNames": [
"artistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Artist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"artistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Album",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorsText",
"columnName": "authorsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareUrl",
"columnName": "shareUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkedAt",
"columnName": "bookmarkedAt",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongAlbumMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"albumId"
]
},
"indices": [
{
"name": "index_SongAlbumMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongAlbumMap_albumId",
"unique": false,
"columnNames": [
"albumId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Album",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"albumId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "SearchQuery",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SearchQuery_query",
"unique": true,
"columnNames": [
"query"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
}
],
"foreignKeys": []
},
{
"tableName": "QueuedMediaItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaItem",
"columnName": "mediaItem",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Format",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itag",
"columnName": "itag",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentLength",
"columnName": "contentLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "lastModified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "loudnessDb",
"columnName": "loudnessDb",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Event",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "playTime",
"columnName": "playTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Event_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Lyrics",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `fixed` TEXT, `synced` TEXT, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fixed",
"columnName": "fixed",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "synced",
"columnName": "synced",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [
{
"viewName": "SortedSongPlaylistMap",
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '205c24811149a247279bcbfdc2d6c396')"
]
}
}

View file

@ -42,6 +42,7 @@ import it.vfsfitvnm.vimusic.models.SongWithContentLength
import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.Event
import it.vfsfitvnm.vimusic.models.Format import it.vfsfitvnm.vimusic.models.Format
import it.vfsfitvnm.vimusic.models.Info import it.vfsfitvnm.vimusic.models.Info
import it.vfsfitvnm.vimusic.models.Lyrics
import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.PlaylistPreview import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
@ -138,17 +139,8 @@ interface Database {
@Query("UPDATE Song SET durationText = :durationText WHERE id = :songId") @Query("UPDATE Song SET durationText = :durationText WHERE id = :songId")
fun updateDurationText(songId: String, durationText: String): Int fun updateDurationText(songId: String, durationText: String): Int
@Query("SELECT lyrics FROM Song WHERE id = :songId") @Query("SELECT * FROM Lyrics WHERE songId = :songId")
fun lyrics(songId: String): Flow<String?> fun lyrics(songId: String): Flow<Lyrics?>
@Query("SELECT synchronizedLyrics FROM Song WHERE id = :songId")
fun synchronizedLyrics(songId: String): Flow<String?>
@Query("UPDATE Song SET lyrics = :lyrics WHERE id = :songId")
fun updateLyrics(songId: String, lyrics: String?): Int
@Query("UPDATE Song SET synchronizedLyrics = :lyrics WHERE id = :songId")
fun updateSynchronizedLyrics(songId: String, lyrics: String?): Int
@Query("SELECT * FROM Artist WHERE id = :id") @Query("SELECT * FROM Artist WHERE id = :id")
fun artist(id: String): Flow<Artist?> fun artist(id: String): Flow<Artist?>
@ -411,6 +403,9 @@ interface Database {
@Update @Update
fun update(playlist: Playlist) fun update(playlist: Playlist)
@Upsert
fun upsert(lyrics: Lyrics)
@Upsert @Upsert
fun upsert(album: Album, songAlbumMaps: List<SongAlbumMap>) fun upsert(album: Album, songAlbumMaps: List<SongAlbumMap>)
@ -450,11 +445,12 @@ interface Database {
QueuedMediaItem::class, QueuedMediaItem::class,
Format::class, Format::class,
Event::class, Event::class,
Lyrics::class,
], ],
views = [ views = [
SortedSongPlaylistMap::class SortedSongPlaylistMap::class
], ],
version = 22, version = 23,
exportSchema = true, exportSchema = true,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 1, to = 2), AutoMigration(from = 1, to = 2),
@ -492,7 +488,8 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
.addMigrations( .addMigrations(
From8To9Migration(), From8To9Migration(),
From10To11Migration(), From10To11Migration(),
From14To15Migration() From14To15Migration(),
From22To23Migration()
) )
.build() .build()
} }
@ -619,6 +616,27 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
@DeleteColumn.Entries(DeleteColumn("Artist", "info")) @DeleteColumn.Entries(DeleteColumn("Artist", "info"))
class From21To22Migration : AutoMigrationSpec class From21To22Migration : AutoMigrationSpec
class From22To23Migration : Migration(22, 23) {
override fun migrate(it: SupportSQLiteDatabase) {
it.execSQL("CREATE TABLE IF NOT EXISTS Lyrics (`songId` TEXT NOT NULL, `fixed` TEXT, `synced` TEXT, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)")
it.query(SimpleSQLiteQuery("SELECT id, lyrics, synchronizedLyrics FROM Song;")).use { cursor ->
val lyricsValues = ContentValues(3)
while (cursor.moveToNext()) {
lyricsValues.put("songId", cursor.getString(0))
lyricsValues.put("fixed", cursor.getString(1))
lyricsValues.put("synced", cursor.getString(2))
it.insert("Lyrics", CONFLICT_IGNORE, lyricsValues)
}
}
it.execSQL("CREATE TABLE IF NOT EXISTS Song_new (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))")
it.execSQL("INSERT INTO Song_new(id, title, artistsText, durationText, thumbnailUrl, likedAt, totalPlayTimeMs) SELECT id, title, artistsText, durationText, thumbnailUrl, likedAt, totalPlayTimeMs FROM Song;")
it.execSQL("DROP TABLE Song;")
it.execSQL("ALTER TABLE Song_new RENAME TO Song;")
}
}
} }
@TypeConverters @TypeConverters

View file

@ -0,0 +1,23 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Immutable
@Entity(
foreignKeys = [
ForeignKey(
entity = Song::class,
parentColumns = ["id"],
childColumns = ["songId"],
onDelete = ForeignKey.CASCADE,
)
]
)
class Lyrics(
@PrimaryKey val songId: String,
val fixed: String?,
val synced: String?,
)

View file

@ -12,8 +12,6 @@ data class Song(
val artistsText: String? = null, val artistsText: String? = null,
val durationText: String?, val durationText: String?,
val thumbnailUrl: String?, val thumbnailUrl: String?,
val lyrics: String? = null,
val synchronizedLyrics: String? = null,
val likedAt: Long? = null, val likedAt: Long? = null,
val totalPlayTimeMs: Long = 0 val totalPlayTimeMs: Long = 0
) { ) {

View file

@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.components package it.vfsfitvnm.vimusic.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -16,10 +17,12 @@ import com.valentinilk.shimmer.shimmer
fun ShimmerHost( fun ShimmerHost(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
Column( Column(
horizontalAlignment = horizontalAlignment, horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
modifier = modifier modifier = modifier
.shimmer() .shimmer()
.graphicsLayer(alpha = 0.99f) .graphicsLayer(alpha = 0.99f)

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -32,10 +33,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
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.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@ -47,6 +48,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.innertube.Innertube import it.vfsfitvnm.innertube.Innertube
import it.vfsfitvnm.innertube.models.bodies.NextBody import it.vfsfitvnm.innertube.models.bodies.NextBody
import it.vfsfitvnm.innertube.requests.lyrics import it.vfsfitvnm.innertube.requests.lyrics
@ -54,9 +56,9 @@ import it.vfsfitvnm.kugou.KuGou
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Lyrics
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.Menu import it.vfsfitvnm.vimusic.ui.components.themed.Menu
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
@ -75,7 +77,6 @@ import it.vfsfitvnm.vimusic.utils.toast
import it.vfsfitvnm.vimusic.utils.verticalFadingEdge import it.vfsfitvnm.vimusic.utils.verticalFadingEdge
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -87,7 +88,7 @@ fun Lyrics(
size: Dp, size: Dp,
mediaMetadataProvider: () -> MediaMetadata, mediaMetadataProvider: () -> MediaMetadata,
durationProvider: () -> Long, durationProvider: () -> Long,
onLyricsUpdate: (Boolean, String, String) -> Unit, ensureSongInserted: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
AnimatedVisibility( AnimatedVisibility(
@ -106,67 +107,84 @@ fun Lyrics(
mutableStateOf(false) mutableStateOf(false)
} }
var lyrics by rememberSaveable { var lyrics by remember {
mutableStateOf<String?>(".") mutableStateOf<Lyrics?>(null)
} }
LaunchedEffect(mediaId, isShowingSynchronizedLyrics) { val text = if (isShowingSynchronizedLyrics) lyrics?.synced else lyrics?.fixed
if (isShowingSynchronizedLyrics) {
Database.synchronizedLyrics(mediaId)
} else {
Database.lyrics(mediaId)
}.distinctUntilChanged().collect { lyrics = it }
}
var isError by remember(lyrics) { var isError by remember(mediaId, isShowingSynchronizedLyrics) {
mutableStateOf(false) mutableStateOf(false)
} }
LaunchedEffect(lyrics == null) { LaunchedEffect(mediaId, isShowingSynchronizedLyrics) {
if (lyrics != null) return@LaunchedEffect withContext(Dispatchers.IO) {
Database.lyrics(mediaId).collect {
if (isShowingSynchronizedLyrics && it?.synced == null) {
val mediaMetadata = mediaMetadataProvider()
var duration = withContext(Dispatchers.Main) {
durationProvider()
}
if (isShowingSynchronizedLyrics) { while (duration == C.TIME_UNSET) {
val mediaMetadata = mediaMetadataProvider() delay(100)
var duration = withContext(Dispatchers.Main) { duration = withContext(Dispatchers.Main) {
durationProvider() durationProvider()
} }
}
while (duration == C.TIME_UNSET) { KuGou.lyrics(
delay(100) artist = mediaMetadata.artist?.toString() ?: "",
duration = withContext(Dispatchers.Main) { title = mediaMetadata.title?.toString() ?: "",
durationProvider() duration = duration / 1000
)?.onSuccess { syncedLyrics ->
Database.upsert(
Lyrics(
songId = mediaId,
fixed = it?.fixed,
synced = syncedLyrics?.value ?: ""
)
)
}?.onFailure {
isError = true
}
} else if (!isShowingSynchronizedLyrics && it?.fixed == null) {
Innertube.lyrics(NextBody(videoId = mediaId))?.onSuccess { fixedLyrics ->
Database.upsert(
Lyrics(
songId = mediaId,
fixed = fixedLyrics ?: "",
synced = it?.synced
)
)
}?.onFailure {
isError = true
}
} else {
lyrics = it
} }
} }
KuGou.lyrics(
artist = mediaMetadata.artist?.toString() ?: "",
title = mediaMetadata.title?.toString() ?: "",
duration = duration / 1000
)?.map { it?.value }
} else {
Innertube.lyrics(NextBody(videoId = mediaId))
}?.onSuccess { newLyrics ->
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
}?.onFailure {
isError = true
} }
} }
if (isEditing) { if (isEditing) {
TextFieldDialog( TextFieldDialog(
hintText = "Enter the lyrics", hintText = "Enter the lyrics",
initialTextInput = lyrics ?: "", initialTextInput = text ?: "",
singleLine = false, singleLine = false,
maxLines = 10, maxLines = 10,
isTextInputValid = { true }, isTextInputValid = { true },
onDismiss = { isEditing = false }, onDismiss = { isEditing = false },
onDone = { onDone = {
query { query {
if (isShowingSynchronizedLyrics) { ensureSongInserted()
Database.updateSynchronizedLyrics(mediaId, it) Database.upsert(
} else { Lyrics(
Database.updateLyrics(mediaId, it) songId = mediaId,
} fixed = if (isShowingSynchronizedLyrics) lyrics?.fixed else it,
synced = if (isShowingSynchronizedLyrics) it else lyrics?.synced,
)
)
} }
} }
) )
@ -193,7 +211,7 @@ fun Lyrics(
.background(Color.Black.copy(0.8f)) .background(Color.Black.copy(0.8f))
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isError && lyrics == null, visible = isError && text == null,
enter = slideInVertically { -it }, enter = slideInVertically { -it },
exit = slideOutVertically { -it }, exit = slideOutVertically { -it },
modifier = Modifier modifier = Modifier
@ -210,7 +228,7 @@ fun Lyrics(
} }
AnimatedVisibility( AnimatedVisibility(
visible = lyrics?.let(String::isEmpty) ?: false, visible = text?.let(String::isEmpty) ?: false,
enter = slideInVertically { -it }, enter = slideInVertically { -it },
exit = slideOutVertically { -it }, exit = slideOutVertically { -it },
modifier = Modifier modifier = Modifier
@ -226,72 +244,78 @@ fun Lyrics(
) )
} }
lyrics?.let { lyrics -> if (text?.isNotEmpty() == true) {
if (lyrics.isNotEmpty() && lyrics != ".") { if (isShowingSynchronizedLyrics) {
if (isShowingSynchronizedLyrics) { val density = LocalDensity.current
val density = LocalDensity.current val player = LocalPlayerServiceBinder.current?.player
val player = LocalPlayerServiceBinder.current?.player ?: return@AnimatedVisibility
?: return@AnimatedVisibility
val synchronizedLyrics = remember(lyrics) { val synchronizedLyrics = remember(text) {
SynchronizedLyrics(KuGou.Lyrics(lyrics).sentences) { SynchronizedLyrics(KuGou.Lyrics(text).sentences) {
player.currentPosition + 50 player.currentPosition + 50
}
} }
}
val lazyListState = rememberLazyListState( val lazyListState = rememberLazyListState(
synchronizedLyrics.index, synchronizedLyrics.index,
with(density) { size.roundToPx() } / 6) with(density) { size.roundToPx() } / 6)
LaunchedEffect(synchronizedLyrics) { LaunchedEffect(synchronizedLyrics) {
val center = with(density) { size.roundToPx() } / 6 val center = with(density) { size.roundToPx() } / 6
while (isActive) { while (isActive) {
delay(50) delay(50)
if (synchronizedLyrics.update()) { if (synchronizedLyrics.update()) {
lazyListState.animateScrollToItem( lazyListState.animateScrollToItem(
synchronizedLyrics.index, synchronizedLyrics.index,
center center
)
}
}
}
LazyColumn(
state = lazyListState,
userScrollEnabled = false,
contentPadding = PaddingValues(vertical = size / 2),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.verticalFadingEdge()
) {
itemsIndexed(items = synchronizedLyrics.sentences) { index, sentence ->
BasicText(
text = sentence.second,
style = typography.xs.center.medium.color(if (index == synchronizedLyrics.index) PureBlackColorPalette.text else PureBlackColorPalette.textDisabled),
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 32.dp)
) )
} }
} }
} else {
BasicText(
text = lyrics,
style = typography.xs.center.medium.color(PureBlackColorPalette.text),
modifier = Modifier
.verticalFadingEdge()
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(vertical = size / 4, horizontal = 32.dp)
)
} }
LazyColumn(
state = lazyListState,
userScrollEnabled = false,
contentPadding = PaddingValues(vertical = size / 2),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.verticalFadingEdge()
) {
itemsIndexed(items = synchronizedLyrics.sentences) { index, sentence ->
BasicText(
text = sentence.second,
style = typography.xs.center.medium.color(if (index == synchronizedLyrics.index) PureBlackColorPalette.text else PureBlackColorPalette.textDisabled),
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 32.dp)
)
}
}
} else {
BasicText(
text = text,
style = typography.xs.center.medium.color(PureBlackColorPalette.text),
modifier = Modifier
.verticalFadingEdge()
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(vertical = size / 4, horizontal = 32.dp)
)
} }
} }
if (lyrics == null && !isError) { if (text == null && !isError) {
ShimmerHost(horizontalAlignment = Alignment.CenterHorizontally) { Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.shimmer()
) {
repeat(4) { repeat(4) {
TextPlaceholder(color = colorPalette.onOverlayShimmer) TextPlaceholder(
color = colorPalette.onOverlayShimmer,
modifier = Modifier
.alpha(1f - it * 0.2f)
)
} }
} }
} }
@ -357,11 +381,13 @@ fun Lyrics(
onClick = { onClick = {
menuState.hide() menuState.hide()
query { query {
if (isShowingSynchronizedLyrics) { Database.upsert(
Database.updateSynchronizedLyrics(mediaId, null) Lyrics(
} else { songId = mediaId,
Database.updateLyrics(mediaId, null) fixed = if (isShowingSynchronizedLyrics) lyrics?.fixed else null,
} synced = if (isShowingSynchronizedLyrics) null else lyrics?.synced,
)
)
} }
} }
) )

View file

@ -32,7 +32,6 @@ import androidx.media3.common.Player
import coil.compose.AsyncImage import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.service.LoginRequiredException import it.vfsfitvnm.vimusic.service.LoginRequiredException
import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException
import it.vfsfitvnm.vimusic.service.UnplayableException import it.vfsfitvnm.vimusic.service.UnplayableException
@ -143,27 +142,7 @@ fun Thumbnail(
mediaId = currentWindow.mediaItem.mediaId, mediaId = currentWindow.mediaItem.mediaId,
isDisplayed = isShowingLyrics && error == null, isDisplayed = isShowingLyrics && error == null,
onDismiss = { onShowLyrics(false) }, onDismiss = { onShowLyrics(false) },
onLyricsUpdate = { areSynchronized, mediaId, lyrics -> ensureSongInserted = { Database.insert(currentWindow.mediaItem) },
query {
if (areSynchronized) {
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
if (mediaId == currentWindow.mediaItem.mediaId) {
Database.insert(currentWindow.mediaItem) { song ->
song.copy(synchronizedLyrics = lyrics)
}
}
}
} else {
if (Database.updateLyrics(mediaId, lyrics) == 0) {
if (mediaId == currentWindow.mediaItem.mediaId) {
Database.insert(currentWindow.mediaItem) { song ->
song.copy(lyrics = lyrics)
}
}
}
}
}
},
size = thumbnailSizeDp, size = thumbnailSizeDp,
mediaMetadataProvider = currentWindow.mediaItem::mediaMetadata, mediaMetadataProvider = currentWindow.mediaItem::mediaMetadata,
durationProvider = player::getDuration, durationProvider = player::getDuration,