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
@@ -0,0 +1,103 @@
package com.subtracks.data.db
import android.content.Context
import androidx.room3.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.ArtworkSeed
import com.subtracks.data.model.Source
import com.subtracks.data.repo.ArtworkSeedRepository
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ArtworkSeedDaoTest {
private lateinit var db: SubtracksDatabase
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
db =
Room
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
.setDriver(BundledSQLiteDriver())
.build()
}
@After
fun tearDown() {
db.close()
}
@Test
fun deletingASourceCascadesToItsSeeds() =
runTest {
val sourceId = source()
val dao = db.artworkSeedDao()
dao.upsert(ArtworkSeed(cacheKey = "$sourceId:cover:false", sourceId = sourceId, primary = 1, secondary = null))
db.sourcesDao().deleteSource(sourceId)
assertNull(dao.seed("$sourceId:cover:false"))
}
@Test
fun pruningKeepsTheMostRecentlyUsedSeeds() =
runTest {
val sourceId = source()
val dao = db.artworkSeedDao()
(1..5).forEach { index ->
dao.upsert(
ArtworkSeed(
cacheKey = "$sourceId:cover$index:false",
sourceId = sourceId,
primary = index,
secondary = null,
lastUsed = index.toLong(),
),
)
}
dao.prune(keep = 2)
val remaining = (1..5).mapNotNull { dao.seed("$sourceId:cover$it:false")?.cacheKey }.toSet()
assertEquals(setOf("$sourceId:cover5:false", "$sourceId:cover4:false"), remaining)
}
@Test
fun savingDerivesTheSourceFromTheCacheKey() =
runTest {
val sourceId = source()
val repository = ArtworkSeedRepository(db)
repository.save("$sourceId:cover:true", 10 to 20)
val row = db.artworkSeedDao().seed("$sourceId:cover:true")
assertEquals(sourceId, row?.sourceId)
assertEquals(10, row?.primary)
assertEquals(20, row?.secondary)
}
@Test
fun savingBeyondTheCapPrunesTheOldestSeeds() =
runTest {
val sourceId = source()
val repository = ArtworkSeedRepository(db, maxStoredSeeds = 2)
repository.save("$sourceId:cover1:false", 1 to null)
repository.save("$sourceId:cover2:false", 2 to null)
repository.save("$sourceId:cover3:false", 3 to null)
val stored = (1..3).count { db.artworkSeedDao().seed("$sourceId:cover$it:false") != null }
assertEquals(2, stored)
}
private suspend fun source(): Long =
db.sourcesDao().insertSource(Source(name = "Navidrome", address = "http://localhost", isActive = true, createdAt = 0))
}
@@ -6,6 +6,7 @@ import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.model.Album
import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.Song
@@ -74,6 +75,39 @@ class PlaybackControllerTest {
assertEquals(listOf("prepare", "play"), handle.operations.takeLast(2))
}
@Test
fun upcomingItemIsTheNextQueueItemWithItsCoverArt() {
seedAlbum(3, sourceId = 1)
runBlocking {
db.libraryDao().upsertAlbums(
listOf(
Album(
sourceId = 1,
id = "al1",
artistId = "ar1",
name = "Album",
albumArtist = "Artist",
created = 0,
coverArt = "cover-al1",
genre = null,
year = null,
starred = null,
songCount = 3,
frequentRank = null,
recentRank = null,
),
),
)
}
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
assertEquals("cover-al1", runBlocking { controller.upcomingItem()?.coverArtId })
}
@Test
fun playingTheSongsListQueuesTheWholeLibrary() {
seedAlbum(60, sourceId = 1)
@@ -0,0 +1,39 @@
package com.subtracks.ui.playback
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import coil3.size.Dimension
import com.subtracks.data.model.CoverArtRef
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(DelicateCoilApi::class)
@RunWith(AndroidJUnit4::class)
class NowPlayingArtworkTest {
@After
fun resetImageLoader() {
SingletonImageLoader.reset()
}
@Test
fun prefetchingTheNextOriginalIsBoundedToTheScreen() =
runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
val ref = CoverArtRef(url = "http://example.test/art", cacheKey = "1:art:false")
val request = prefetchImageRequest(context, ref)
val resolved = request.sizeResolver.size()
val width = (resolved.width as Dimension.Pixels).px
val metrics = context.resources.displayMetrics
assertEquals(maxOf(metrics.widthPixels, metrics.heightPixels), width)
assertEquals(ref.cacheKey, request.memoryCacheKey)
assertEquals(ref.cacheKey, request.diskCacheKey)
}
}