feat(android): port cached_home_index (M8 4.2 slice 9)

Mirrors flutter_client/lib/cache/db.dart's CachedHomeIndex — per-item
rows that drive the Home screen sections (Recently Added Albums,
Rediscover Albums/Artists, Most Played Tracks, Last Played Artists).

Composite PK (section, position) — exactly one row per slot per
section; sync replaces in-place via upsert. entityType ("album" /
"artist" / "track") dispatches per-tile hydration to the right
per-entity endpoint when the Home screen renders.

DAO surface fits the sync flow:
  - observeBySection (Flow) for the Home composables
  - getBySection (suspend) for one-shot sync reads
  - upsertAll for sync writes
  - deleteBySection for replace-all on a section sync

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 16:40:37 -04:00
parent f3a0c44460
commit e42f2bf525
3 changed files with 71 additions and 0 deletions
@@ -6,6 +6,7 @@ import androidx.room.TypeConverters
import com.fabledsword.minstrel.cache.db.dao.AudioCacheIndexDao
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedHomeIndexDao
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedMutationDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao
@@ -17,6 +18,7 @@ import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao
import com.fabledsword.minstrel.cache.db.entities.AudioCacheIndexEntity
import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity
import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedHomeIndexEntity
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
@@ -52,6 +54,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
AudioCacheIndexEntity::class,
CachedMutationEntity::class,
CachedResumeStateEntity::class,
CachedHomeIndexEntity::class,
],
version = 1,
exportSchema = true,
@@ -69,4 +72,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun audioCacheIndexDao(): AudioCacheIndexDao
abstract fun cachedMutationDao(): CachedMutationDao
abstract fun cachedResumeStateDao(): CachedResumeStateDao
abstract fun cachedHomeIndexDao(): CachedHomeIndexDao
}
@@ -0,0 +1,33 @@
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.CachedHomeIndexEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface CachedHomeIndexDao {
@Query(
"SELECT * FROM cached_home_index " +
"WHERE section = :section ORDER BY position ASC",
)
fun observeBySection(section: String): Flow<List<CachedHomeIndexEntity>>
@Query(
"SELECT * FROM cached_home_index " +
"WHERE section = :section ORDER BY position ASC",
)
suspend fun getBySection(section: String): List<CachedHomeIndexEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(rows: List<CachedHomeIndexEntity>)
/** Replace-all pattern; sync wipes a section then re-inserts. */
@Query("DELETE FROM cached_home_index WHERE section = :section")
suspend fun deleteBySection(section: String)
@Query("DELETE FROM cached_home_index")
suspend fun clear()
}
@@ -0,0 +1,34 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* Per-item row driving the Home screen sections. Mirrors
* `flutter_client/lib/cache/db.dart`'s `CachedHomeIndex` Drift table.
*
* `section` is one of (matching /api/home keys):
* - "recently_added_albums"
* - "rediscover_albums"
* - "rediscover_artists"
* - "most_played_tracks"
* - "last_played_artists"
*
* `entityType` is "album" | "artist" | "track" — dispatches hydration
* to the right per-entity endpoint when a tile is rendered.
*
* The composite PK (section, position) means there's exactly one row
* per slot per section; sync replaces in-place by upsert.
*/
@Entity(
tableName = "cached_home_index",
primaryKeys = ["section", "position"],
)
data class CachedHomeIndexEntity(
val section: String,
val position: Int,
val entityType: String,
val entityId: String,
val fetchedAt: Instant = Clock.System.now(),
)