bound artwork seeds and size the now-playing prefetch

This commit is contained in:
austinried
2026-10-04 00:29:58 +00:00
parent ca87a38276
commit a97a5e0687
10 changed files with 255 additions and 18 deletions
@@ -12,4 +12,16 @@ interface ArtworkSeedDao {
@Upsert
suspend fun upsert(seed: ArtworkSeed)
@Query("UPDATE artwork_seeds SET lastUsed = :lastUsed WHERE cacheKey = :cacheKey")
suspend fun touch(
cacheKey: String,
lastUsed: Long,
)
@Query(
"DELETE FROM artwork_seeds WHERE cacheKey NOT IN " +
"(SELECT cacheKey FROM artwork_seeds ORDER BY lastUsed DESC LIMIT :keep)",
)
suspend fun prune(keep: Int)
}
@@ -112,6 +112,21 @@ val MIGRATION_11_12 =
}
}
val MIGRATION_12_13 =
object : Migration(12, 13) {
override suspend fun migrate(connection: SQLiteConnection) {
connection.execSQL("DROP TABLE IF EXISTS `artwork_seeds`")
connection.execSQL(
"CREATE TABLE IF NOT EXISTS `artwork_seeds` (`cacheKey` TEXT NOT NULL, " +
"`primary` INTEGER NOT NULL, `secondary` INTEGER, `nameBusy` INTEGER, " +
"`sourceId` INTEGER NOT NULL, `lastUsed` INTEGER NOT NULL DEFAULT 0, " +
"PRIMARY KEY(`cacheKey`), " +
"FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
)
connection.execSQL("CREATE INDEX IF NOT EXISTS `index_artwork_seeds_sourceId` ON `artwork_seeds` (`sourceId`)")
}
}
val MIGRATIONS: Array<Migration> =
arrayOf(
MIGRATION_1_2,
@@ -125,4 +140,5 @@ val MIGRATIONS: Array<Migration> =
MIGRATION_9_10,
MIGRATION_10_11,
MIGRATION_11_12,
MIGRATION_12_13,
)
@@ -32,7 +32,7 @@ import com.subtracks.data.model.SubsonicSource
ShuffleOrder::class,
ArtworkSeed::class,
],
version = 12,
version = 13,
exportSchema = true,
)
@ColumnTypeConverters(QueueKindConverter::class)
@@ -237,12 +237,25 @@ data class ShuffleOrder(
val flatPosition: Long,
)
@Entity(tableName = "artwork_seeds")
@Entity(
tableName = "artwork_seeds",
foreignKeys = [
ForeignKey(
entity = Source::class,
parentColumns = ["id"],
childColumns = ["sourceId"],
onDelete = ForeignKey.CASCADE,
),
],
indices = [Index("sourceId")],
)
data class ArtworkSeed(
@PrimaryKey val cacheKey: String,
val primary: Int,
val secondary: Int?,
val nameBusy: Boolean? = null,
val sourceId: Long = 0,
@ColumnInfo(defaultValue = "0") val lastUsed: Long = 0,
)
class QueueKindConverter {
@@ -3,6 +3,8 @@ package com.subtracks.data.repo
import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.model.ArtworkSeed
private const val MAX_STORED_SEEDS = 1024
interface ArtworkSeedStore {
suspend fun seed(cacheKey: String): ArtworkSeed?
@@ -11,10 +13,19 @@ interface ArtworkSeedStore {
class ArtworkSeedRepository(
private val db: SubtracksDatabase,
private val maxStoredSeeds: Int = MAX_STORED_SEEDS,
) : ArtworkSeedStore {
override suspend fun seed(cacheKey: String): ArtworkSeed? = db.artworkSeedDao().seed(cacheKey)
override suspend fun seed(cacheKey: String): ArtworkSeed? {
val dao = db.artworkSeedDao()
val row = dao.seed(cacheKey) ?: return null
dao.touch(cacheKey, System.currentTimeMillis())
return row
}
override suspend fun save(seed: ArtworkSeed) {
db.artworkSeedDao().upsert(seed)
val sourceId = seed.cacheKey.substringBefore(':').toLongOrNull() ?: return
val dao = db.artworkSeedDao()
dao.upsert(seed.copy(sourceId = sourceId, lastUsed = System.currentTimeMillis()))
dao.prune(maxStoredSeeds)
}
}
@@ -580,11 +580,10 @@ class PlaybackController(
thumbnail: Boolean = false,
): CoverArtRef? = sourceRepository.coverArt(item?.coverArtId, thumbnail)
suspend fun upcomingCoverArt(thumbnail: Boolean = false): CoverArtRef? {
suspend fun upcomingItem(): QueueItem? {
val snap = snapshot ?: return null
val position = currentPosition() ?: return null
val next = queueRepository.itemAt(snap, position + 1) ?: return null
return sourceRepository.coverArt(next.toQueueItem().coverArtId, thumbnail)
return queueRepository.itemAt(snap, position + 1)?.toQueueItem()
}
private fun play(
@@ -98,8 +98,10 @@ fun NowPlayingRoute(
}
LaunchedEffect(state.item?.id, state.hasNext) {
if (!state.hasNext) return@LaunchedEffect
controller.upcomingCoverArt(thumbnail = true)?.let { ArtworkSeedCache.prefetch(context, it) }
controller.upcomingCoverArt()?.let { prefetchImage(context, it) }
controller.upcomingItem()?.let { next ->
controller.coverArt(next, thumbnail = true)?.let { ArtworkSeedCache.prefetch(context, it) }
controller.coverArt(next)?.let { prefetchImage(context, it) }
}
}
NowPlayingScreen(
state = state,
@@ -120,18 +122,26 @@ fun NowPlayingRoute(
)
}
private fun prefetchImage(
internal fun prefetchImage(
context: Context,
ref: CoverArtRef,
) {
SingletonImageLoader.get(context).enqueue(
ImageRequest
.Builder(context)
.data(ref.url)
.memoryCacheKey(ref.cacheKey)
.diskCacheKey(ref.cacheKey)
.build(),
)
SingletonImageLoader.get(context).enqueue(prefetchImageRequest(context, ref))
}
internal fun prefetchImageRequest(
context: Context,
ref: CoverArtRef,
): ImageRequest {
val metrics = context.resources.displayMetrics
val maxDimension = maxOf(metrics.widthPixels, metrics.heightPixels)
return ImageRequest
.Builder(context)
.data(ref.url)
.memoryCacheKey(ref.cacheKey)
.diskCacheKey(ref.cacheKey)
.size(maxDimension)
.build()
}
@OptIn(ExperimentalMaterial3Api::class)