package it.vfsfitvnm.vimusic import android.content.Context import android.database.Cursor import android.os.Parcel import androidx.media3.common.MediaItem import androidx.room.* import androidx.room.migration.AutoMigrationSpec import it.vfsfitvnm.vimusic.models.* import kotlinx.coroutines.flow.Flow @Dao interface Database { companion object : Database by DatabaseInitializer.Instance.database @Query("SELECT * FROM SearchQuery WHERE query LIKE :query ORDER BY id DESC") fun getRecentQueries(query: String): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(searchQuery: SearchQuery) @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(info: Info): Long @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(playlist: Playlist): Long @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(info: SongInPlaylist): Long @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(info: List): List @Query("SELECT * FROM Song WHERE id = :id") fun songFlow(id: String): Flow @Query("SELECT * FROM Song WHERE id = :id") fun song(id: String): Song? @Query("SELECT * FROM Playlist WHERE id = :id") fun playlist(id: Long): Playlist? @Query("SELECT * FROM Song") fun songs(): Flow> @Transaction @Query("SELECT * FROM Song WHERE id = :id") fun songWithInfo(id: String): SongWithInfo? @Transaction @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC") fun history(): Flow> @Transaction @Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC") fun favorites(): Flow> @Transaction @Query("SELECT * FROM Song WHERE totalPlayTimeMs >= 60000 ORDER BY totalPlayTimeMs DESC LIMIT 20") fun mostPlayed(): Flow> @Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id") fun incrementTotalPlayTimeMs(id: String, addition: Long) @Transaction @Query("SELECT * FROM Playlist WHERE id = :id") fun playlistWithSongs(id: Long): Flow @Query("SELECT COUNT(*) FROM SongInPlaylist WHERE playlistId = :id") fun playlistSongCount(id: Long): Int @Query("UPDATE SongInPlaylist SET position = position - 1 WHERE playlistId = :playlistId AND position >= :fromPosition") fun decrementSongPositions(playlistId: Long, fromPosition: Int) @Query("UPDATE SongInPlaylist SET position = position - 1 WHERE playlistId = :playlistId AND position >= :fromPosition AND position <= :toPosition") fun decrementSongPositions(playlistId: Long, fromPosition: Int, toPosition: Int) @Query("UPDATE SongInPlaylist SET position = position + 1 WHERE playlistId = :playlistId AND position >= :fromPosition AND position <= :toPosition") fun incrementSongPositions(playlistId: Long, fromPosition: Int, toPosition: Int) @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(songWithAuthors: SongWithAuthors): Long @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(song: Song): Long @Update fun update(song: Song) @Update fun update(songInPlaylist: SongInPlaylist) @Update fun update(playlist: Playlist) @Delete fun delete(searchQuery: SearchQuery) @Delete fun delete(playlist: Playlist) @Delete fun delete(song: Song) @Delete fun delete(songInPlaylist: SongInPlaylist) @Transaction @Query("SELECT id, name, (SELECT COUNT(*) FROM SongInPlaylist WHERE playlistId = id) as songCount FROM Playlist") fun playlistPreviews(): Flow> @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> @Insert(onConflict = OnConflictStrategy.ABORT) fun insertQueue(queuedMediaItems: List) @Query("SELECT * FROM QueuedMediaItem") fun queue(): List @Query("DELETE FROM QueuedMediaItem") fun clearQueue() } @androidx.room.Database( entities = [ Song::class, SongInPlaylist::class, Playlist::class, Info::class, SongWithAuthors::class, SearchQuery::class, QueuedMediaItem::class, ], views = [ SortedSongInPlaylist::class ], version = 6, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4, spec = DatabaseInitializer.From3To4Migration::class), AutoMigration(from = 4, to = 5), AutoMigration(from = 5, to = 6), ], ) @TypeConverters(Converters::class) abstract class DatabaseInitializer protected constructor() : RoomDatabase() { abstract val database: Database companion object { lateinit var Instance: DatabaseInitializer context(Context) operator fun invoke() { if (!::Instance.isInitialized) { Instance = Room .databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db") .build() } } } @DeleteTable.Entries(DeleteTable(tableName = "QueuedMediaItem")) class From3To4Migration : AutoMigrationSpec } @TypeConverters object Converters { @TypeConverter fun mediaItemFromByteArray(value: ByteArray?): MediaItem? { return value?.let { byteArray -> runCatching { val parcel = Parcel.obtain() parcel.unmarshall(byteArray, 0, byteArray.size) parcel.setDataPosition(0) val bundle = parcel.readBundle(MediaItem::class.java.classLoader) parcel.recycle() bundle?.let(MediaItem.CREATOR::fromBundle) }.getOrNull() } } @TypeConverter fun mediaItemToByteArray(mediaItem: MediaItem?): ByteArray? { return mediaItem?.toBundle()?.let { persistableBundle -> val parcel = Parcel.obtain() parcel.writeBundle(persistableBundle) val bytes = parcel.marshall() parcel.recycle() bytes } } } val Database.internal: RoomDatabase get() = DatabaseInitializer.Instance fun query(block: () -> Unit) = DatabaseInitializer.Instance.getQueryExecutor().execute(block) fun transaction(block: () -> Unit) = with(DatabaseInitializer.Instance) { getTransactionExecutor().execute { runInTransaction(block) } } val RoomDatabase.path: String get() = getOpenHelper().writableDatabase.path fun RoomDatabase.checkpoint() { getOpenHelper().writableDatabase.run { query("PRAGMA journal_mode").use { cursor -> if (cursor.moveToFirst()) { when (cursor.getString(0).lowercase()) { "wal" -> { query("PRAGMA wal_checkpoint").use(Cursor::moveToFirst) query("PRAGMA wal_checkpoint(TRUNCATE)").use(Cursor::moveToFirst) query("PRAGMA wal_checkpoint").use(Cursor::moveToFirst) } } } } } }