scrobble: send now playing once per track change

A track transition resets the player position, and the scrobble policy's restart heuristic (same song, position drops to ~0) then re-announced the track we just left and, when the new track's position jittered back to zero, announced it twice. Suppress the now-playing when the restart is the same song; the submission logic still resets so a genuine replay is scrobbled again.
This commit is contained in:
austinried
2026-10-09 12:31:48 +00:00
parent ac1e62fc02
commit cfb663c250
2 changed files with 35 additions and 3 deletions
@@ -59,6 +59,7 @@ class ScrobblePolicy(
item?.id != songId ||
(item != null && item.id == lastItemId && positionMs < previousPosition && positionMs <= RESTART_POSITION_MS)
if (restarted) {
val sameSong = item != null && item.id == songId
songId = item?.id
lastItemId = null
playedMs = 0
@@ -68,7 +69,7 @@ class ScrobblePolicy(
if (item == null || !isPlaying) return null
nowPlayingSent = true
startedAt = now()
return Scrobble.NowPlaying(item.id)
return if (sameSong) null else Scrobble.NowPlaying(item.id)
}
lastPositionMs = positionMs
lastItemId = item?.id
@@ -130,15 +130,46 @@ class ScrobblePolicyTest {
}
@Test
fun restartingTheSameTrackScrobblesAgain() {
fun restartingTheSameTrackResubmitsWithoutReannouncing() {
val item = track(durationMs = 200_000)
play(item, toPositionMs = 100_000)
clock = 120_000L
val repeat = play(item, toPositionMs = 100_000)
assertEquals(Scrobble.NowPlaying("s1"), repeat.first())
assertEquals(emptyList<Scrobble>(), repeat.filterIsInstance<Scrobble.NowPlaying>())
assertEquals(Scrobble.Submission("s1", 120_000L), repeat.last())
}
@Test
fun doesNotReannounceTheTrackItJustLeftWhenItsPositionResets() {
val events = mutableListOf<Scrobble>()
events += play(track(id = "s1"), toPositionMs = 100_000)
events +=
listOfNotNull(
policy.advance(track(id = "s1"), 200_000, isPlaying = true, positionMs = 0),
policy.advance(track(id = "s2"), 200_000, isPlaying = true, positionMs = 0),
)
assertEquals(
listOf(Scrobble.NowPlaying("s1"), Scrobble.Submission("s1", 1_000L), Scrobble.NowPlaying("s2")),
events,
)
}
@Test
fun doesNotReannounceWhenTheNewTracksPositionJittersBackToZero() {
val events = mutableListOf<Scrobble>()
events += play(track(id = "s1"), toPositionMs = 100_000)
events +=
listOfNotNull(
policy.advance(track(id = "s2"), 200_000, isPlaying = true, positionMs = 0),
policy.advance(track(id = "s2"), 200_000, isPlaying = true, positionMs = 500),
policy.advance(track(id = "s2"), 200_000, isPlaying = true, positionMs = 0),
)
assertEquals(
listOf(Scrobble.NowPlaying("s1"), Scrobble.Submission("s1", 1_000L), Scrobble.NowPlaying("s2")),
events,
)
}
@Test
fun aTransitionDoesNotReannounceTheNewTrackOnAStaleLowPosition() {
val first = track(id = "s1")