Migrate database (2)

This commit is contained in:
vfsfitvnm
2022-06-29 15:03:20 +02:00
parent a7bdda074b
commit 6f5bc02216
13 changed files with 545 additions and 145 deletions

View File

@@ -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<Info>): List<Long>
fun insert(info: List<Artist>): List<Long>
@Query("SELECT * FROM Song WHERE id = :id")
fun songFlow(id: String): Flow<Song?>
@@ -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<List<SongWithInfo>>
fun history(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC")
fun favorites(): Flow<List<SongWithInfo>>
fun favorites(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs >= 60000 ORDER BY totalPlayTimeMs DESC LIMIT 20")
fun mostPlayed(): Flow<List<SongWithInfo>>
fun mostPlayed(): Flow<List<DetailedSong>>
@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<List<String?>>
@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<List<SongWithInfo>>
// @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<List<DetailedSong>>
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insertQueue(queuedMediaItems: List<QueuedMediaItem>)
@@ -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;")
}
}
}

View File

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

View File

@@ -15,7 +15,7 @@ data class PlaylistWithSongs(
entityColumn = "songId"
)
)
val songs: List<SongWithInfo>
val songs: List<DetailedSong>
) {
companion object {
val Empty = PlaylistWithSongs(Playlist(-1, ""), emptyList())

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<DetailedSong>())
// Database.artistSongs(browseId)
}.collectAsState(initial = emptyList<DetailedSong>(), 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)

View File

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

View File

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

View File

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

View File

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