From 6f5bc02216c0bbf4654c1ed28f328370a620a6cd Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 29 Jun 2022 15:03:20 +0200 Subject: [PATCH] Migrate database (2) --- .../9.json | 419 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 72 ++- .../it/vfsfitvnm/vimusic/models/Info.kt | 12 - .../vimusic/models/PlaylistWithSongs.kt | 2 +- .../it/vfsfitvnm/vimusic/models/Song.kt | 11 +- .../vimusic/models/SongWithAuthors.kt | 28 -- .../vfsfitvnm/vimusic/models/SongWithInfo.kt | 25 -- .../ui/components/themed/MediaItemMenu.kt | 8 +- .../vimusic/ui/screens/ArtistScreen.kt | 12 +- .../vimusic/ui/screens/HomeScreen.kt | 6 +- .../vimusic/ui/screens/LocalPlaylistScreen.kt | 10 +- .../it/vfsfitvnm/vimusic/ui/views/SongItem.kt | 6 +- .../it/vfsfitvnm/vimusic/utils/utils.kt | 79 ++-- 13 files changed, 545 insertions(+), 145 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/9.json delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Info.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithAuthors.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithInfo.kt diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/9.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/9.json new file mode 100644 index 0000000..a4013af --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/9.json @@ -0,0 +1,419 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "22e88f327e3340760100610939e9a158", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `albumId` TEXT, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, `loudnessDb` REAL, `contentLength` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongInPlaylist", + "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_SongInPlaylist_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongInPlaylist_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongInPlaylist_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongInPlaylist_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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "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 NOT NULL, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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": [] + } + ], + "views": [ + { + "viewName": "SortedSongInPlaylist", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongInPlaylist 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, '22e88f327e3340760100610939e9a158')" + ] + } +} \ 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 4fc6511..00b344a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -1,11 +1,14 @@ package it.vfsfitvnm.vimusic +import android.content.ContentValues import android.content.Context import android.database.Cursor +import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE import android.os.Parcel import androidx.media3.common.MediaItem import androidx.room.* import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SimpleSQLiteQuery import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import it.vfsfitvnm.vimusic.models.* @@ -22,8 +25,11 @@ interface Database { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(searchQuery: SearchQuery) - @Insert(onConflict = OnConflictStrategy.ABORT) - fun insert(info: Info): Long + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(info: Artist) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(info: Album) @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(playlist: Playlist): Long @@ -32,7 +38,7 @@ interface Database { fun insert(info: SongInPlaylist): Long @Insert(onConflict = OnConflictStrategy.ABORT) - fun insert(info: List): List + fun insert(info: List): List @Query("SELECT * FROM Song WHERE id = :id") fun songFlow(id: String): Flow @@ -48,19 +54,19 @@ interface Database { @Transaction @Query("SELECT * FROM Song WHERE id = :id") - fun songWithInfo(id: String): SongWithInfo? + fun songWithInfo(id: String): DetailedSong? @Transaction @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC") - fun history(): Flow> + fun history(): Flow> @Transaction @Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC") - fun favorites(): Flow> + fun favorites(): Flow> @Transaction @Query("SELECT * FROM Song WHERE totalPlayTimeMs >= 60000 ORDER BY totalPlayTimeMs DESC LIMIT 20") - fun mostPlayed(): Flow> + fun mostPlayed(): Flow> @Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id") fun incrementTotalPlayTimeMs(id: String, addition: Long) @@ -82,7 +88,7 @@ interface Database { fun incrementSongPositions(playlistId: Long, fromPosition: Int, toPosition: Int) @Insert(onConflict = OnConflictStrategy.ABORT) - fun insert(songWithAuthors: SongWithAuthors): Long + fun insert(songWithAuthors: SongArtistMap): Long @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(song: Song): Long @@ -115,10 +121,10 @@ interface Database { @Query("SELECT thumbnailUrl FROM Song JOIN SongInPlaylist ON id = songId WHERE playlistId = :id ORDER BY position LIMIT 4") fun playlistThumbnailUrls(id: Long): Flow> - @Transaction - @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM Info JOIN SongWithAuthors ON Info.id = SongWithAuthors.authorInfoId JOIN Song ON SongWithAuthors.songId = Song.id WHERE browseId = :artistId ORDER BY Song.ROWID DESC") - fun artistSongs(artistId: String): Flow> +// @Transaction +// @RewriteQueriesToDropUnusedColumns +// @Query("SELECT * FROM Info JOIN SongWithAuthors ON Info.id = SongWithAuthors.authorInfoId JOIN Song ON SongWithAuthors.songId = Song.id WHERE browseId = :artistId ORDER BY Song.ROWID DESC") +// fun artistSongs(artistId: String): Flow> @Insert(onConflict = OnConflictStrategy.ABORT) fun insertQueue(queuedMediaItems: List) @@ -135,11 +141,9 @@ interface Database { Song::class, SongInPlaylist::class, Playlist::class, - Info::class, - SongWithAuthors::class, - Album::class, Artist::class, SongArtistMap::class, + Album::class, SearchQuery::class, QueuedMediaItem::class, ], @@ -166,7 +170,7 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() { lateinit var Instance: DatabaseInitializer context(Context) - operator fun invoke() { + operator fun invoke() { if (!::Instance.isInitialized) { Instance = Room .databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db") @@ -183,8 +187,42 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() { class From7To8Migration : AutoMigrationSpec class From8To9Migration : Migration(8, 9) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(it: SupportSQLiteDatabase) { + it.query(SimpleSQLiteQuery("SELECT DISTINCT browseId, text, Info.id FROM Info JOIN Song ON Info.id = Song.albumId;")).use { cursor -> + val albumValues = ContentValues(2) + while (cursor.moveToNext()) { + albumValues.put("id", cursor.getString(0)) + albumValues.put("title", cursor.getString(1)) + it.insert("Album", CONFLICT_IGNORE, albumValues) + it.execSQL("UPDATE Song SET albumId = '${cursor.getString(0)}' WHERE albumId = ${cursor.getLong(2)}") + } + } + + it.query(SimpleSQLiteQuery("SELECT GROUP_CONCAT(text, ''), SongWithAuthors.songId FROM Info JOIN SongWithAuthors ON Info.id = SongWithAuthors.authorInfoId GROUP BY songId;")).use { cursor -> + val songValues = ContentValues(1) + while (cursor.moveToNext()) { + println("artistsText: ${cursor.getString(0)} (cursor.getString(1))") + songValues.put("artistsText", cursor.getString(0)) + it.update("Song", CONFLICT_IGNORE, songValues, "id = ?", arrayOf(cursor.getString(1))) + } + } + + it.query(SimpleSQLiteQuery("SELECT browseId, text, Info.id FROM Info JOIN SongWithAuthors ON Info.id = SongWithAuthors.authorInfoId WHERE browseId NOT NULL;")).use { cursor -> + val artistValues = ContentValues(2) + while (cursor.moveToNext()) { + artistValues.put("id", cursor.getString(0)) + artistValues.put("name", cursor.getString(1)) + it.insert("Artist", CONFLICT_IGNORE, artistValues) + + it.execSQL("UPDATE SongWithAuthors SET authorInfoId = '${cursor.getString(0)}' WHERE authorInfoId = ${cursor.getLong(2)}") + } + } + + it.execSQL("INSERT INTO SongArtistMap(songId, artistId) SELECT songId, authorInfoId FROM SongWithAuthors") + + it.execSQL("DROP TABLE Info;") + it.execSQL("DROP TABLE SongWithAuthors;") } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Info.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Info.kt deleted file mode 100644 index 9e7c601..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Info.kt +++ /dev/null @@ -1,12 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.room.Entity -import androidx.room.PrimaryKey - -// I know... -@Entity -data class Info( - @PrimaryKey(autoGenerate = true) val id: Long = 0, - val browseId: String?, - val text: String -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt index fa512c0..d40cad3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt @@ -15,7 +15,7 @@ data class PlaylistWithSongs( entityColumn = "songId" ) ) - val songs: List + val songs: List ) { companion object { val Empty = PlaylistWithSongs(Playlist(-1, ""), emptyList()) 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 d4f4743..a718ce6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt @@ -3,7 +3,16 @@ package it.vfsfitvnm.vimusic.models import androidx.room.* -@Entity +@Entity( +// foreignKeys = [ +// ForeignKey( +// entity = Album::class, +// parentColumns = ["id"], +// childColumns = ["albumId"], +// onDelete = ForeignKey.CASCADE +// ), +// ] +) data class Song( @PrimaryKey val id: String, val title: String, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithAuthors.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithAuthors.kt deleted file mode 100644 index e8216d0..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithAuthors.kt +++ /dev/null @@ -1,28 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.compose.runtime.Immutable -import androidx.room.* - - -@Immutable -@Entity( - primaryKeys = ["songId", "authorInfoId"], - foreignKeys = [ - ForeignKey( - entity = Song::class, - parentColumns = ["id"], - childColumns = ["songId"], - onDelete = ForeignKey.CASCADE - ), - ForeignKey( - entity = Info::class, - parentColumns = ["id"], - childColumns = ["authorInfoId"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class SongWithAuthors( - val songId: String, - @ColumnInfo(index = true) val authorInfoId: Long -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithInfo.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithInfo.kt deleted file mode 100644 index ae5adda..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SongWithInfo.kt +++ /dev/null @@ -1,25 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.room.Embedded -import androidx.room.Junction -import androidx.room.Relation - -open class SongWithInfo( - @Embedded val song: Song, - @Relation( - entity = Info::class, - parentColumn = "albumId", - entityColumn = "id" - ) val album: Info?, - @Relation( - entity = Info::class, - parentColumn = "id", - entityColumn = "id", - associateBy = Junction( - value = SongWithAuthors::class, - parentColumn = "songId", - entityColumn = "authorInfoId" - ) - ) - val authors: List? -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index 4869ebc..61281c9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -20,7 +20,7 @@ import it.vfsfitvnm.vimusic.* import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.SongInPlaylist -import it.vfsfitvnm.vimusic.models.SongWithInfo +import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute @@ -32,7 +32,7 @@ import kotlinx.coroutines.Dispatchers @ExperimentalAnimationApi @Composable fun InFavoritesMediaItemMenu( - song: SongWithInfo, + song: DetailedSong, modifier: Modifier = Modifier, onDismiss: (() -> Unit)? = null ) { @@ -51,7 +51,7 @@ fun InFavoritesMediaItemMenu( @ExperimentalAnimationApi @Composable fun InHistoryMediaItemMenu( - song: SongWithInfo, + song: DetailedSong, modifier: Modifier = Modifier, onDismiss: (() -> Unit)? = null ) { @@ -94,7 +94,7 @@ fun InHistoryMediaItemMenu( fun InPlaylistMediaItemMenu( playlistId: Long, positionInPlaylist: Int, - song: SongWithInfo, + song: DetailedSong, modifier: Modifier = Modifier, onDismiss: (() -> Unit)? = null ) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt index 820be8d..b20296b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt @@ -28,7 +28,7 @@ import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.models.SongWithInfo +import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.ui.components.OutcomeItem import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu @@ -40,6 +40,7 @@ import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.withContext @@ -96,8 +97,9 @@ fun ArtistScreen( } val songs by remember(browseId) { - Database.artistSongs(browseId) - }.collectAsState(initial = emptyList(), context = Dispatchers.IO) + flowOf(emptyList()) +// Database.artistSongs(browseId) + }.collectAsState(initial = emptyList(), context = Dispatchers.IO) LazyColumn( state = lazyListState, @@ -218,7 +220,7 @@ fun ArtistScreen( modifier = Modifier .clickable(enabled = songs.isNotEmpty()) { binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(songs.shuffled().map(SongWithInfo::asMediaItem)) + binder?.player?.forcePlayFromBeginning(songs.shuffled().map(DetailedSong::asMediaItem)) } .padding(horizontal = 8.dp, vertical = 8.dp) .size(20.dp) @@ -236,7 +238,7 @@ fun ArtistScreen( thumbnailSize = songThumbnailSizePx, onClick = { binder?.stopRadio() - binder?.player?.forcePlayAtIndex(songs.map(SongWithInfo::asMediaItem), index) + binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index) }, menuContent = { InHistoryMediaItemMenu(song = song) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index e9e6527..be4ec36 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -37,7 +37,7 @@ import it.vfsfitvnm.vimusic.enums.SongCollection import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.SearchQuery -import it.vfsfitvnm.vimusic.models.SongWithInfo +import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu @@ -358,7 +358,7 @@ fun HomeScreen() { modifier = Modifier .clickable(enabled = songCollection.isNotEmpty()) { binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(songCollection.shuffled().map(SongWithInfo::asMediaItem)) + binder?.player?.forcePlayFromBeginning(songCollection.shuffled().map(DetailedSong::asMediaItem)) } .padding(horizontal = 8.dp, vertical = 8.dp) .size(20.dp) @@ -378,7 +378,7 @@ fun HomeScreen() { thumbnailSize = thumbnailSize, onClick = { binder?.stopRadio() - binder?.player?.forcePlayAtIndex(songCollection.map(SongWithInfo::asMediaItem), index) + binder?.player?.forcePlayAtIndex(songCollection.map(DetailedSong::asMediaItem), index) }, menuContent = { when (preferences.homePageSongCollection) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt index 9736053..9ebd138 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt @@ -28,7 +28,7 @@ import it.vfsfitvnm.vimusic.* import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.PlaylistWithSongs import it.vfsfitvnm.vimusic.models.SongInPlaylist -import it.vfsfitvnm.vimusic.models.SongWithInfo +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.* @@ -160,7 +160,7 @@ fun LocalPlaylistScreen( enabled = playlistWithSongs.songs.isNotEmpty(), onClick = { menuState.hide() - binder?.player?.enqueue(playlistWithSongs.songs.map(SongWithInfo::asMediaItem)) + binder?.player?.enqueue(playlistWithSongs.songs.map(DetailedSong::asMediaItem)) } ) @@ -225,7 +225,7 @@ fun LocalPlaylistScreen( modifier = Modifier .clickable { binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem).shuffled()) + binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(DetailedSong::asMediaItem).shuffled()) } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -243,7 +243,7 @@ fun LocalPlaylistScreen( modifier = Modifier .clickable { binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(SongWithInfo::asMediaItem)) + binder?.player?.forcePlayFromBeginning(playlistWithSongs.songs.map(DetailedSong::asMediaItem)) } .shadow(elevation = 2.dp, shape = CircleShape) .background( @@ -267,7 +267,7 @@ fun LocalPlaylistScreen( thumbnailSize = thumbnailSize, onClick = { binder?.stopRadio() - binder?.player?.forcePlayAtIndex(playlistWithSongs.songs.map(SongWithInfo::asMediaItem), index) + binder?.player?.forcePlayAtIndex(playlistWithSongs.songs.map(DetailedSong::asMediaItem), index) }, menuContent = { InPlaylistMediaItemMenu( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt index 69a91cd..530db42 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt @@ -26,7 +26,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness -import it.vfsfitvnm.vimusic.models.SongWithInfo +import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography @@ -67,7 +67,7 @@ fun SongItem( @Composable @NonRestartableComposable fun SongItem( - song: SongWithInfo, + song: DetailedSong, thumbnailSize: Int, onClick: () -> Unit, menuContent: @Composable () -> Unit, @@ -78,7 +78,7 @@ fun SongItem( SongItem( thumbnailModel = song.song.thumbnailUrl?.thumbnail(thumbnailSize), title = song.song.title, - authors = song.authors?.joinToString("") { it.text } ?: "", + authors = song.song.artistsText ?: "", durationText = song.song.durationText, menuContent = menuContent, onClick = onClick, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt index 7268289..2a7efb7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt @@ -28,46 +28,43 @@ fun Database.insert(mediaItem: MediaItem): Song { return@runInTransaction it } - val albumInfo = mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> - Info( - text = mediaItem.mediaMetadata.albumTitle!!.toString(), - browseId = albumId - ) + val album = mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> + Album( + id = albumId, + title = mediaItem.mediaMetadata.albumTitle!!.toString(), + year = null, + authorsText = null, + thumbnailUrl = null + ).also(::insert) } - val albumInfoId = albumInfo?.let { insert(it) } - - val authorsInfo = - mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames -> - mediaItem.mediaMetadata.extras!!.getStringArrayList("artistIds")?.let { artistIds -> - artistNames.mapIndexed { index, artistName -> - Info( - text = artistName, - browseId = artistIds.getOrNull(index) - ) - } - } - } - val song = Song( id = mediaItem.mediaId, title = mediaItem.mediaMetadata.title!!.toString(), - albumId = albumInfoId.toString(), + artistsText = mediaItem.mediaMetadata.artist!!.toString(), + albumId = album?.id, durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!, thumbnailUrl = mediaItem.mediaMetadata.artworkUri!!.toString(), loudnessDb = mediaItem.mediaMetadata.extras?.getFloat("loudnessDb"), contentLength = mediaItem.mediaMetadata.extras?.getLong("contentLength"), - ) + ).also(::insert) - insert(song) - - val authorsInfoId = authorsInfo?.let { insert(authorsInfo) } - - authorsInfoId?.forEach { authorInfoId -> + mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames -> + mediaItem.mediaMetadata.extras!!.getStringArrayList("artistIds")?.let { artistIds -> + artistNames.mapIndexed { index, artistName -> + Artist( + id = artistIds[index], + name = artistName, + thumbnailUrl = null, + info = null + ).also(::insert) + } + } + }?.forEach { artist -> insert( - SongWithAuthors( - songId = mediaItem.mediaId, - authorInfoId = authorInfoId + SongArtistMap( + songId = song.id, + artistId = artist.id ) ) } @@ -92,8 +89,8 @@ val YouTube.Item.Song.asMediaItem: MediaItem "videoId" to info.endpoint!!.videoId, "albumId" to album?.endpoint?.browseId, "durationText" to durationText, - "artistNames" to authors.map { it.name }, - "artistIds" to authors.map { it.endpoint?.browseId }, + "artistNames" to authors.filter { it.endpoint != null }.map { it.name }, + "artistIds" to authors.mapNotNull { it.endpoint?.browseId }, ) ) .build() @@ -114,28 +111,28 @@ val YouTube.Item.Video.asMediaItem: MediaItem bundleOf( "videoId" to info.endpoint!!.videoId, "durationText" to durationText, - "artistNames" to if (isOfficialMusicVideo) authors.map { it.name } else null, - "artistIds" to if (isOfficialMusicVideo) authors.map { it.endpoint?.browseId } else null, + "artistNames" to if (isOfficialMusicVideo) authors.filter { it.endpoint != null }.map { it.name } else null, + "artistIds" to if (isOfficialMusicVideo) authors.mapNotNull { it.endpoint?.browseId } else null, ) ) .build() ) .build() -val SongWithInfo.asMediaItem: MediaItem +val DetailedSong.asMediaItem: MediaItem get() = MediaItem.Builder() .setMediaMetadata( MediaMetadata.Builder() .setTitle(song.title) - .setArtist(authors?.joinToString("") { it.text }) - .setAlbumTitle(album?.text) + .setArtist(song.artistsText) + .setAlbumTitle(album?.title) .setArtworkUri(song.thumbnailUrl?.toUri()) .setExtras( bundleOf( "videoId" to song.id, - "albumId" to album?.browseId, - "artistNames" to authors?.map { it.text }, - "artistIds" to authors?.map { it.browseId }, + "albumId" to album?.id, + "artistNames" to artists?.map { it.name }, + "artistIds" to artists?.map { it.id }, "durationText" to song.durationText, "loudnessDb" to song.loudnessDb ) @@ -166,8 +163,8 @@ fun YouTube.PlaylistOrAlbum.Item.toMediaItem( "playlistId" to info.endpoint?.playlistId, "albumId" to (if (isFromAlbum) albumId else album?.endpoint?.browseId), "durationText" to durationText, - "artistNames" to (authors ?: playlistOrAlbum.authors)?.map { it.name }, - "artistIds" to (authors ?: playlistOrAlbum.authors)?.map { it.endpoint?.browseId } + "artistNames" to (authors ?: playlistOrAlbum.authors)?.filter { it.endpoint != null }?.map { it.name }, + "artistIds" to (authors ?: playlistOrAlbum.authors)?.mapNotNull { it.endpoint?.browseId } ) ) .build()