mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
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:
@@ -179,14 +179,20 @@ interface LibraryDao {
|
||||
recomputeArtistPlayData(sourceId)
|
||||
}
|
||||
|
||||
@Query("UPDATE songs SET playCount = playCount + 1, played = :at WHERE sourceId = :sourceId AND id = :songId")
|
||||
@Query(
|
||||
"UPDATE songs SET playCount = playCount + 1, played = MAX(COALESCE(played, :at), :at) " +
|
||||
"WHERE sourceId = :sourceId AND id = :songId",
|
||||
)
|
||||
suspend fun bumpSongPlay(
|
||||
sourceId: Long,
|
||||
songId: String,
|
||||
at: Long,
|
||||
)
|
||||
|
||||
@Query("UPDATE albums SET playCount = playCount + 1, played = :at WHERE sourceId = :sourceId AND id = :albumId")
|
||||
@Query(
|
||||
"UPDATE albums SET playCount = playCount + 1, played = MAX(COALESCE(played, :at), :at) " +
|
||||
"WHERE sourceId = :sourceId AND id = :albumId",
|
||||
)
|
||||
suspend fun bumpAlbumPlay(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
@@ -194,8 +200,8 @@ interface LibraryDao {
|
||||
)
|
||||
|
||||
@Query(
|
||||
"UPDATE artists SET playCount = playCount + 1, played = :at WHERE sourceId = :sourceId " +
|
||||
"AND id = (SELECT artistId FROM albums WHERE sourceId = :sourceId AND id = :albumId)",
|
||||
"UPDATE artists SET playCount = playCount + 1, played = MAX(COALESCE(played, :at), :at) " +
|
||||
"WHERE sourceId = :sourceId AND id = (SELECT artistId FROM albums WHERE sourceId = :sourceId AND id = :albumId)",
|
||||
)
|
||||
suspend fun bumpArtistPlay(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -126,17 +126,21 @@ class Scrobbler(
|
||||
}
|
||||
|
||||
private suspend fun submit(event: Scrobble) {
|
||||
try {
|
||||
when (event) {
|
||||
is Scrobble.NowPlaying -> {
|
||||
sink.nowPlaying(event.songId)
|
||||
}
|
||||
|
||||
is Scrobble.Submission -> {
|
||||
sink.scrobble(event.songId, event.time)
|
||||
recordPlay(event.songId, event.time / 1000)
|
||||
}
|
||||
when (event) {
|
||||
is Scrobble.NowPlaying -> {
|
||||
attempt { sink.nowPlaying(event.songId) }
|
||||
}
|
||||
|
||||
is Scrobble.Submission -> {
|
||||
attempt { recordPlay(event.songId, event.time / 1000) }
|
||||
attempt { sink.scrobble(event.songId, event.time) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun attempt(block: suspend () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (cancellation: CancellationException) {
|
||||
throw cancellation
|
||||
} catch (_: Exception) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user