From 17a3e7dd4f6b1656eb8abc5ea738f635a547c7b1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 22 May 2026 13:28:54 -0400 Subject: [PATCH] feat(android): port cached_artists / cached_albums / cached_tracks (M8 4.2 slice 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors flutter_client/lib/cache/db.dart's CachedArtists / CachedAlbums / CachedTracks Drift tables. Library cache foundation — LibraryRepository (Phase 5.2) reads cache-first through these DAOs and refreshes from server via the sync controller (Phase 12.4). Column names follow Kotlin idiom (camelCase) instead of Drift's snake_case; the schema is internal to the native client and the wire JSON conversion happens in feature-level mappers. Each DAO carries: - observe* (Flow) for cache-first reads in ViewModels - getById/getByIds (suspend) for one-shot lookups - upsertAll (suspend, REPLACE) for sync writes - deleteByIds (suspend) for sync-driven deletes Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/cache/db/AppDatabase.kt | 18 +++++++++--- .../minstrel/cache/db/dao/CachedAlbumDao.kt | 29 +++++++++++++++++++ .../minstrel/cache/db/dao/CachedArtistDao.kt | 23 +++++++++++++++ .../minstrel/cache/db/dao/CachedTrackDao.kt | 29 +++++++++++++++++++ .../cache/db/entities/CachedAlbumEntity.kt | 22 ++++++++++++++ .../cache/db/entities/CachedArtistEntity.kt | 26 +++++++++++++++++ .../cache/db/entities/CachedTrackEntity.kt | 25 ++++++++++++++++ 7 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedAlbumDao.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedArtistDao.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedTrackDao.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedAlbumEntity.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedArtistEntity.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedTrackEntity.kt diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt index 6d572e36..f62ad44b 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt @@ -3,7 +3,13 @@ package com.fabledsword.minstrel.cache.db import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao +import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao +import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao +import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity +import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity +import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity /** @@ -16,14 +22,15 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity * change with explicit `Migration` entries; `fallbackToDestructiveMigration` * in `DatabaseModule` is the safety net while we're pre-v1. * - * Most entity families land in Task 4.2 (one commit per Drift table - * family). `SyncMetadataEntity` was pulled forward into Task 4.1 to - * satisfy Room's "at least one entity required" compile-time check; - * its consumer (SyncController) arrives in Phase 12.4. + * Entity families land in Task 4.2 slices. The set will grow until the + * whole Drift schema is mirrored. */ @Database( entities = [ SyncMetadataEntity::class, + CachedArtistEntity::class, + CachedAlbumEntity::class, + CachedTrackEntity::class, ], version = 1, exportSchema = true, @@ -31,4 +38,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity @TypeConverters(MinstrelTypeConverters::class) abstract class AppDatabase : RoomDatabase() { abstract fun syncMetadataDao(): SyncMetadataDao + abstract fun cachedArtistDao(): CachedArtistDao + abstract fun cachedAlbumDao(): CachedAlbumDao + abstract fun cachedTrackDao(): CachedTrackDao } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedAlbumDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedAlbumDao.kt new file mode 100644 index 00000000..3f84bc61 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedAlbumDao.kt @@ -0,0 +1,29 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CachedAlbumDao { + @Query("SELECT * FROM cached_albums ORDER BY sortTitle COLLATE NOCASE ASC") + fun observeAll(): Flow> + + @Query( + "SELECT * FROM cached_albums " + + "WHERE artistId = :artistId ORDER BY sortTitle COLLATE NOCASE ASC", + ) + fun observeByArtist(artistId: String): Flow> + + @Query("SELECT * FROM cached_albums WHERE id = :id") + suspend fun getById(id: String): CachedAlbumEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAll(rows: List) + + @Query("DELETE FROM cached_albums WHERE id IN (:ids)") + suspend fun deleteByIds(ids: List) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedArtistDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedArtistDao.kt new file mode 100644 index 00000000..2bd4cc03 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedArtistDao.kt @@ -0,0 +1,23 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CachedArtistDao { + @Query("SELECT * FROM cached_artists ORDER BY sortName COLLATE NOCASE ASC") + fun observeAll(): Flow> + + @Query("SELECT * FROM cached_artists WHERE id = :id") + suspend fun getById(id: String): CachedArtistEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAll(rows: List) + + @Query("DELETE FROM cached_artists WHERE id IN (:ids)") + suspend fun deleteByIds(ids: List) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedTrackDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedTrackDao.kt new file mode 100644 index 00000000..31dbc058 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedTrackDao.kt @@ -0,0 +1,29 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CachedTrackDao { + @Query( + "SELECT * FROM cached_tracks " + + "WHERE albumId = :albumId ORDER BY discNumber ASC, trackNumber ASC", + ) + fun observeByAlbum(albumId: String): Flow> + + @Query("SELECT * FROM cached_tracks WHERE id = :id") + suspend fun getById(id: String): CachedTrackEntity? + + @Query("SELECT * FROM cached_tracks WHERE id IN (:ids)") + suspend fun getByIds(ids: List): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAll(rows: List) + + @Query("DELETE FROM cached_tracks WHERE id IN (:ids)") + suspend fun deleteByIds(ids: List) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedAlbumEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedAlbumEntity.kt new file mode 100644 index 00000000..6255a20b --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedAlbumEntity.kt @@ -0,0 +1,22 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * Cache row for one album. Mirrors `flutter_client/lib/cache/db.dart`'s + * `CachedAlbums` Drift table. + */ +@Entity(tableName = "cached_albums") +data class CachedAlbumEntity( + @PrimaryKey val id: String, + val artistId: String, + val title: String, + val sortTitle: String, + val releaseDate: String? = null, + val coverPath: String? = null, + val mbid: String? = null, + val fetchedAt: Instant = Clock.System.now(), +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedArtistEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedArtistEntity.kt new file mode 100644 index 00000000..8601805c --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedArtistEntity.kt @@ -0,0 +1,26 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * Cache row for one artist. Mirrors `flutter_client/lib/cache/db.dart`'s + * `CachedArtists` Drift table. + * + * Column names follow Kotlin idiom (camelCase) rather than Drift's + * snake_case — the schema is purely internal to the native client; the + * sync controller exchanges snake_case JSON with the server and our + * mappers translate at the boundary. + */ +@Entity(tableName = "cached_artists") +data class CachedArtistEntity( + @PrimaryKey val id: String, + val name: String, + val sortName: String, + val mbid: String? = null, + val artistThumbPath: String? = null, + val artistFanartPath: String? = null, + val fetchedAt: Instant = Clock.System.now(), +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedTrackEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedTrackEntity.kt new file mode 100644 index 00000000..8653a60a --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedTrackEntity.kt @@ -0,0 +1,25 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * Cache row for one track. Mirrors `flutter_client/lib/cache/db.dart`'s + * `CachedTracks` Drift table. + */ +@Entity(tableName = "cached_tracks") +data class CachedTrackEntity( + @PrimaryKey val id: String, + val albumId: String, + val artistId: String, + val title: String, + val durationMs: Int = 0, + val trackNumber: Int? = null, + val discNumber: Int? = null, + val filePath: String? = null, + val fileFormat: String? = null, + val genre: String? = null, + val fetchedAt: Instant = Clock.System.now(), +)