From accbfc47d0e9d248abfe9bda47e53b878dd2b189 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 29 Oct 2022 19:47:39 +0200 Subject: [PATCH] Move song lyrics to a separate database entity --- app/build.gradle.kts | 1 - .../23.json | 672 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 44 +- .../it/vfsfitvnm/vimusic/models/Lyrics.kt | 23 + .../it/vfsfitvnm/vimusic/models/Song.kt | 2 - .../vimusic/ui/components/ShimmerHost.kt | 3 + .../vimusic/ui/screens/player/Lyrics.kt | 232 +++--- .../vimusic/ui/screens/player/Thumbnail.kt | 23 +- 8 files changed, 859 insertions(+), 141 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/23.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Lyrics.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8ecb816..9e67b74 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -88,7 +88,6 @@ dependencies { implementation(libs.room) kapt(libs.room.compiler) - annotationProcessor(libs.room.compiler) implementation(projects.innertube) implementation(projects.kugou) diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/23.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/23.json new file mode 100644 index 0000000..7cc0ae4 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/23.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index a2d2d08..2e00f3f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -42,6 +42,7 @@ import it.vfsfitvnm.vimusic.models.SongWithContentLength import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.Format import it.vfsfitvnm.vimusic.models.Info +import it.vfsfitvnm.vimusic.models.Lyrics import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.PlaylistPreview import it.vfsfitvnm.vimusic.models.PlaylistWithSongs @@ -138,17 +139,8 @@ interface Database { @Query("UPDATE Song SET durationText = :durationText WHERE id = :songId") fun updateDurationText(songId: String, durationText: String): Int - @Query("SELECT lyrics FROM Song WHERE id = :songId") - fun lyrics(songId: String): Flow - - @Query("SELECT synchronizedLyrics FROM Song WHERE id = :songId") - fun synchronizedLyrics(songId: String): Flow - - @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 Lyrics WHERE songId = :songId") + fun lyrics(songId: String): Flow @Query("SELECT * FROM Artist WHERE id = :id") fun artist(id: String): Flow @@ -411,6 +403,9 @@ interface Database { @Update fun update(playlist: Playlist) + @Upsert + fun upsert(lyrics: Lyrics) + @Upsert fun upsert(album: Album, songAlbumMaps: List) @@ -450,11 +445,12 @@ interface Database { QueuedMediaItem::class, Format::class, Event::class, + Lyrics::class, ], views = [ SortedSongPlaylistMap::class ], - version = 22, + version = 23, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -492,7 +488,8 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() { .addMigrations( From8To9Migration(), From10To11Migration(), - From14To15Migration() + From14To15Migration(), + From22To23Migration() ) .build() } @@ -619,6 +616,27 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() { @DeleteColumn.Entries(DeleteColumn("Artist", "info")) 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 diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Lyrics.kt new file mode 100644 index 0000000..da6c770 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Lyrics.kt @@ -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?, +) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt index 909e239..3421956 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt @@ -12,8 +12,6 @@ data class Song( val artistsText: String? = null, val durationText: String?, val thumbnailUrl: String?, - val lyrics: String? = null, - val synchronizedLyrics: String? = null, val likedAt: Long? = null, val totalPlayTimeMs: Long = 0 ) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ShimmerHost.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ShimmerHost.kt index 1ae57c1..8f0f09b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ShimmerHost.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ShimmerHost.kt @@ -1,5 +1,6 @@ package it.vfsfitvnm.vimusic.ui.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable @@ -16,10 +17,12 @@ import com.valentinilk.shimmer.shimmer fun ShimmerHost( modifier: Modifier = Modifier, horizontalAlignment: Alignment.Horizontal = Alignment.Start, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, content: @Composable ColumnScope.() -> Unit ) { Column( horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArrangement, modifier = modifier .shimmer() .graphicsLayer(alpha = 0.99f) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt index 5eafe54..b0d7665 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -32,10 +33,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter 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.media3.common.C import androidx.media3.common.MediaMetadata +import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.innertube.Innertube import it.vfsfitvnm.innertube.models.bodies.NextBody import it.vfsfitvnm.innertube.requests.lyrics @@ -54,9 +56,9 @@ import it.vfsfitvnm.kugou.KuGou import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.Lyrics import it.vfsfitvnm.vimusic.query 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.MenuEntry 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext @@ -87,7 +88,7 @@ fun Lyrics( size: Dp, mediaMetadataProvider: () -> MediaMetadata, durationProvider: () -> Long, - onLyricsUpdate: (Boolean, String, String) -> Unit, + ensureSongInserted: () -> Unit, modifier: Modifier = Modifier ) { AnimatedVisibility( @@ -106,67 +107,84 @@ fun Lyrics( mutableStateOf(false) } - var lyrics by rememberSaveable { - mutableStateOf(".") + var lyrics by remember { + mutableStateOf(null) } - LaunchedEffect(mediaId, isShowingSynchronizedLyrics) { - if (isShowingSynchronizedLyrics) { - Database.synchronizedLyrics(mediaId) - } else { - Database.lyrics(mediaId) - }.distinctUntilChanged().collect { lyrics = it } - } + val text = if (isShowingSynchronizedLyrics) lyrics?.synced else lyrics?.fixed - var isError by remember(lyrics) { + var isError by remember(mediaId, isShowingSynchronizedLyrics) { mutableStateOf(false) } - LaunchedEffect(lyrics == null) { - if (lyrics != null) return@LaunchedEffect + LaunchedEffect(mediaId, isShowingSynchronizedLyrics) { + withContext(Dispatchers.IO) { + Database.lyrics(mediaId).collect { + if (isShowingSynchronizedLyrics && it?.synced == null) { + val mediaMetadata = mediaMetadataProvider() + var duration = withContext(Dispatchers.Main) { + durationProvider() + } - if (isShowingSynchronizedLyrics) { - val mediaMetadata = mediaMetadataProvider() - var duration = withContext(Dispatchers.Main) { - durationProvider() - } + while (duration == C.TIME_UNSET) { + delay(100) + duration = withContext(Dispatchers.Main) { + durationProvider() + } + } - while (duration == C.TIME_UNSET) { - delay(100) - duration = withContext(Dispatchers.Main) { - durationProvider() + KuGou.lyrics( + artist = mediaMetadata.artist?.toString() ?: "", + title = mediaMetadata.title?.toString() ?: "", + 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) { TextFieldDialog( hintText = "Enter the lyrics", - initialTextInput = lyrics ?: "", + initialTextInput = text ?: "", singleLine = false, maxLines = 10, isTextInputValid = { true }, onDismiss = { isEditing = false }, onDone = { query { - if (isShowingSynchronizedLyrics) { - Database.updateSynchronizedLyrics(mediaId, it) - } else { - Database.updateLyrics(mediaId, it) - } + ensureSongInserted() + Database.upsert( + Lyrics( + 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)) ) { AnimatedVisibility( - visible = isError && lyrics == null, + visible = isError && text == null, enter = slideInVertically { -it }, exit = slideOutVertically { -it }, modifier = Modifier @@ -210,7 +228,7 @@ fun Lyrics( } AnimatedVisibility( - visible = lyrics?.let(String::isEmpty) ?: false, + visible = text?.let(String::isEmpty) ?: false, enter = slideInVertically { -it }, exit = slideOutVertically { -it }, modifier = Modifier @@ -226,72 +244,78 @@ fun Lyrics( ) } - lyrics?.let { lyrics -> - if (lyrics.isNotEmpty() && lyrics != ".") { - if (isShowingSynchronizedLyrics) { - val density = LocalDensity.current - val player = LocalPlayerServiceBinder.current?.player - ?: return@AnimatedVisibility + if (text?.isNotEmpty() == true) { + if (isShowingSynchronizedLyrics) { + val density = LocalDensity.current + val player = LocalPlayerServiceBinder.current?.player + ?: return@AnimatedVisibility - val synchronizedLyrics = remember(lyrics) { - SynchronizedLyrics(KuGou.Lyrics(lyrics).sentences) { - player.currentPosition + 50 - } + val synchronizedLyrics = remember(text) { + SynchronizedLyrics(KuGou.Lyrics(text).sentences) { + player.currentPosition + 50 } + } - val lazyListState = rememberLazyListState( - synchronizedLyrics.index, - with(density) { size.roundToPx() } / 6) + val lazyListState = rememberLazyListState( + synchronizedLyrics.index, + with(density) { size.roundToPx() } / 6) - LaunchedEffect(synchronizedLyrics) { - val center = with(density) { size.roundToPx() } / 6 + LaunchedEffect(synchronizedLyrics) { + val center = with(density) { size.roundToPx() } / 6 - while (isActive) { - delay(50) - if (synchronizedLyrics.update()) { - lazyListState.animateScrollToItem( - synchronizedLyrics.index, - 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) + while (isActive) { + delay(50) + if (synchronizedLyrics.update()) { + lazyListState.animateScrollToItem( + synchronizedLyrics.index, + center ) } } - } 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) { - ShimmerHost(horizontalAlignment = Alignment.CenterHorizontally) { + if (text == null && !isError) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .shimmer() + ) { repeat(4) { - TextPlaceholder(color = colorPalette.onOverlayShimmer) + TextPlaceholder( + color = colorPalette.onOverlayShimmer, + modifier = Modifier + .alpha(1f - it * 0.2f) + ) } } } @@ -357,11 +381,13 @@ fun Lyrics( onClick = { menuState.hide() query { - if (isShowingSynchronizedLyrics) { - Database.updateSynchronizedLyrics(mediaId, null) - } else { - Database.updateLyrics(mediaId, null) - } + Database.upsert( + Lyrics( + songId = mediaId, + fixed = if (isShowingSynchronizedLyrics) lyrics?.fixed else null, + synced = if (isShowingSynchronizedLyrics) null else lyrics?.synced, + ) + ) } } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt index 6356562..2cc4e4d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt @@ -32,7 +32,6 @@ import androidx.media3.common.Player import coil.compose.AsyncImage import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder -import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.service.LoginRequiredException import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException import it.vfsfitvnm.vimusic.service.UnplayableException @@ -143,27 +142,7 @@ fun Thumbnail( mediaId = currentWindow.mediaItem.mediaId, isDisplayed = isShowingLyrics && error == null, onDismiss = { onShowLyrics(false) }, - onLyricsUpdate = { areSynchronized, mediaId, lyrics -> - 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) - } - } - } - } - } - }, + ensureSongInserted = { Database.insert(currentWindow.mediaItem) }, size = thumbnailSizeDp, mediaMetadataProvider = currentWindow.mediaItem::mediaMetadata, durationProvider = player::getDuration,