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 69 additions and 0 deletions
Showing only changes of commit 7b55f586ac - Show all commits
@@ -5,10 +5,12 @@ 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.CachedLikeDao
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.CachedLikeEntity
import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity
import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
@@ -31,6 +33,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
CachedArtistEntity::class,
CachedAlbumEntity::class,
CachedTrackEntity::class,
CachedLikeEntity::class,
],
version = 1,
exportSchema = true,
@@ -41,4 +44,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cachedArtistDao(): CachedArtistDao
abstract fun cachedAlbumDao(): CachedAlbumDao
abstract fun cachedTrackDao(): CachedTrackDao
abstract fun cachedLikeDao(): CachedLikeDao
}
@@ -0,0 +1,39 @@
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.CachedLikeEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface CachedLikeDao {
/** All track IDs the user has liked. Audio-cache eviction uses this set. */
@Query(
"SELECT entityId FROM cached_likes " +
"WHERE userId = :userId AND entityType = 'track'",
)
fun observeLikedTrackIds(userId: String): Flow<List<String>>
@Query(
"SELECT entityId FROM cached_likes " +
"WHERE userId = :userId AND entityType = :entityType",
)
fun observeLikedIdsOfType(userId: String, entityType: String): Flow<List<String>>
@Query(
"SELECT EXISTS(SELECT 1 FROM cached_likes " +
"WHERE userId = :userId AND entityType = :entityType AND entityId = :entityId)",
)
fun observeIsLiked(userId: String, entityType: String, entityId: String): Flow<Boolean>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(rows: List<CachedLikeEntity>)
@Query(
"DELETE FROM cached_likes " +
"WHERE userId = :userId AND entityType = :entityType AND entityId = :entityId",
)
suspend fun delete(userId: String, entityType: String, entityId: String)
}
@@ -0,0 +1,26 @@
package com.fabledsword.minstrel.cache.db.entities
import androidx.room.Entity
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* Like membership row. Mirrors `flutter_client/lib/cache/db.dart`'s
* `CachedLikes` Drift table. Composite primary key — one user may
* independently like a track AND its album AND its artist; rows are
* disambiguated by the (userId, entityType, entityId) triple.
*
* `entityType` is one of "track" | "album" | "artist". Stored as a
* plain string for parity with the Drift schema and the server's wire
* format; an enum doesn't earn its keep here.
*/
@Entity(
tableName = "cached_likes",
primaryKeys = ["userId", "entityType", "entityId"],
)
data class CachedLikeEntity(
val userId: String,
val entityType: String,
val entityId: String,
val likedAt: Instant = Clock.System.now(),
)