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 76 additions and 0 deletions
Showing only changes of commit c18ad19418 - Show all commits
@@ -8,6 +8,7 @@ import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao
import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity
@@ -15,6 +16,7 @@ import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity
import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
@@ -40,6 +42,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
CachedLikeEntity::class,
CachedPlaylistEntity::class,
CachedPlaylistTrackEntity::class,
CachedQuarantineEntity::class,
],
version = 1,
exportSchema = true,
@@ -53,4 +56,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedLikeDao(): CachedLikeDao
abstract fun cachedPlaylistDao(): CachedPlaylistDao
abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao
abstract fun cachedQuarantineDao(): CachedQuarantineDao
}
@@ -0,0 +1,36 @@
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.CachedQuarantineEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface CachedQuarantineDao {
/** Newest-first listing for the Quarantine screen. */
@Query("SELECT * FROM cached_quarantine_mine ORDER BY createdAt DESC")
fun observeAll(): Flow<List<CachedQuarantineEntity>>
/** Just the IDs — feed-filtering code reads this to hide tracks. */
@Query("SELECT trackId FROM cached_quarantine_mine")
fun observeFlaggedTrackIds(): Flow<List<String>>
@Query(
"SELECT EXISTS(SELECT 1 FROM cached_quarantine_mine WHERE trackId = :trackId)",
)
fun observeIsHidden(trackId: String): Flow<Boolean>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(row: CachedQuarantineEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(rows: List<CachedQuarantineEntity>)
@Query("DELETE FROM cached_quarantine_mine WHERE trackId = :trackId")
suspend fun deleteByTrackId(trackId: String)
@Query("DELETE FROM cached_quarantine_mine WHERE trackId IN (:trackIds)")
suspend fun deleteByTrackIds(trackIds: List<String>)
}
@@ -0,0 +1,36 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* The current user's quarantine flag for one track. Mirrors
* `flutter_client/lib/cache/db.dart`'s `CachedQuarantineMine` Drift
* table.
*
* The flat denormalized track/album/artist columns let the Quarantine
* screen render off this single table without a join — the server
* returns the full snapshot on /api/me/quarantine so we mirror that
* shape locally.
*
* `createdAt` is the server-issued timestamp preserved as an opaque
* ISO-8601 string (it's the canonical "when this was flagged" value);
* `fetchedAt` is our local sync-time marker.
*/
@Entity(tableName = "cached_quarantine_mine")
data class CachedQuarantineEntity(
@PrimaryKey val trackId: String,
val reason: String,
val notes: String? = null,
val createdAt: String,
val trackTitle: String,
val trackDurationMs: Int = 0,
val albumId: String,
val albumTitle: String,
val albumCoverArtPath: String? = null,
val artistId: String,
val artistName: String,
val fetchedAt: Instant = Clock.System.now(),
)