address review: count a play even when its scrobble fails

A submitted scrobble now counts locally regardless of the HTTP result
(offline already did), so a transient server error no longer drops the
play from the sorts. Last-played uses MAX(existing, at) so a backwards
clock step cannot move an album or artist back down the list.
This commit is contained in:
austinried
2026-10-04 00:30:05 +00:00
parent 95ac4ae6ce
commit 4d4eb7b758
5 changed files with 64 additions and 15 deletions
@@ -230,6 +230,13 @@ class LibraryDaoTest {
assertEquals(500L, dao.albumOnce(sourceId, "al-1")!!.played)
assertEquals(1L, dao.artistOnce(sourceId, "ar-1")!!.playCount)
assertEquals(500L, dao.artistOnce(sourceId, "ar-1")!!.played)
dao.recordPlay(sourceId, "s1", 100)
assertEquals(2L, dao.songOnce(sourceId, "s1")!!.playCount)
assertEquals(500L, dao.songOnce(sourceId, "s1")!!.played)
assertEquals(500L, dao.albumOnce(sourceId, "al-1")!!.played)
assertEquals(500L, dao.artistOnce(sourceId, "ar-1")!!.played)
}
@Test
@@ -12,6 +12,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.IOException
@OptIn(ExperimentalCoroutinesApi::class)
class ScrobblerTest {
@@ -28,6 +29,7 @@ class ScrobblerTest {
private class RecordingSink : ServerActionSink {
val calls = mutableListOf<Call>()
var failScrobble = false
override suspend fun nowPlaying(songId: String) {
calls += Call.NowPlaying(songId)
@@ -37,6 +39,7 @@ class ScrobblerTest {
songId: String,
time: Long,
) {
if (failScrobble) throw IOException("scrobble failed")
calls += Call.Scrobble(songId, time)
}
@@ -136,6 +139,35 @@ class ScrobblerTest {
assertEquals(listOf("s1" to 1L), recorded)
}
@Test
fun aFailedSubmissionStillRecordsThePlayLocally() =
runTest {
val sink = RecordingSink().apply { failScrobble = true }
val recorded = mutableListOf<Pair<String, Long>>()
val scrobbler =
Scrobbler(
sink = sink,
enabled = flowOf(true),
scope = testScope(),
policy = ScrobblePolicy(now = { 1_000L }),
recordPlay = { songId, at -> recorded += songId to at },
)
val state = MutableStateFlow(PlaybackState())
val position = MutableStateFlow(0L)
scrobbler.attach(state, position)
val item = QueueItem("s1", "Title", "Artist", "Album", null, durationMs = 30_000)
state.value = PlaybackState(item = item, isPlaying = true)
var tick = 500L
while (tick <= 15_000L) {
position.value = tick
tick += 500
}
assertEquals(emptyList<Call>(), sink.calls.filterIsInstance<Call.Scrobble>())
assertEquals(listOf("s1" to 1L), recorded)
}
@Test
fun disabledSendsNothing() =
runTest {