feat: M3 weighted shuffle v1 — real /api/radio with scoring #21

Merged
bvandeusen merged 262 commits from dev into main 2026-04-27 11:00:29 -04:00
3 changed files with 70 additions and 0 deletions
Showing only changes of commit 0b1ccd59b1 - Show all commits
@@ -7,6 +7,7 @@ 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.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedMutationDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
@@ -16,6 +17,7 @@ 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.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
@@ -46,6 +48,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
CachedPlaylistTrackEntity::class,
CachedQuarantineEntity::class,
AudioCacheIndexEntity::class,
CachedMutationEntity::class,
],
version = 1,
exportSchema = true,
@@ -61,4 +64,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao
abstract fun cachedQuarantineDao(): CachedQuarantineDao
abstract fun audioCacheIndexDao(): AudioCacheIndexDao
abstract fun cachedMutationDao(): CachedMutationDao
}
@@ -0,0 +1,34 @@
package com.fabledsword.minstrel.cache.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.Instant
@Dao
interface CachedMutationDao {
/** Pending count for the offline-indicator badge. */
@Query("SELECT COUNT(*) FROM cached_mutations")
fun observePendingCount(): Flow<Int>
/** FIFO drain order so the replayer processes oldest first. */
@Query("SELECT * FROM cached_mutations ORDER BY id ASC")
suspend fun getAll(): List<CachedMutationEntity>
@Insert
suspend fun insert(row: CachedMutationEntity): Long
@Query(
"UPDATE cached_mutations SET attempts = attempts + 1, lastAttemptAt = :at " +
"WHERE id = :id",
)
suspend fun recordAttempt(id: Long, at: Instant)
@Query("DELETE FROM cached_mutations WHERE id = :id")
suspend fun delete(id: Long)
@Query("DELETE FROM cached_mutations")
suspend fun clear()
}
@@ -0,0 +1,32 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* One row per pending offline-write. Mirrors
* `flutter_client/lib/cache/db.dart`'s `CachedMutations` Drift table.
*
* MutationQueue.enqueue() inserts a row when a server-write fails with
* an IOException; MutationReplayer.drain() pops and re-attempts each
* row when connectivity returns (Phase 12.2).
*
* `kind` is a stable string registered in `MutationKind` — the replayer
* looks up the handler in a kind→suspend-function map. Unknown kinds
* are dropped so a malformed row can't wedge the queue forever.
*
* `payload` is the JSON-serialized args needed to replay the REST call.
* Shape is kind-specific; the kind enum doubles as a tag for which
* decoder to apply at drain time.
*/
@Entity(tableName = "cached_mutations")
data class CachedMutationEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val kind: String,
val payload: String,
val createdAt: Instant = Clock.System.now(),
val lastAttemptAt: Instant? = null,
val attempts: Int = 0,
)