mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
address offline review: refresh, cursor, and cold-start fixes
This commit is contained in:
@@ -88,7 +88,7 @@ class SourceRepository(
|
|||||||
_quality.value = quality
|
_quality.value = quality
|
||||||
val sourceChanged = config?.id != activeSourceId
|
val sourceChanged = config?.id != activeSourceId
|
||||||
activeSourceId = config?.id
|
activeSourceId = config?.id
|
||||||
if (!_offline.value && sourceChanged && config != null && config.useTokenAuth) {
|
if (sourceChanged && config != null && config.useTokenAuth && !prefs.offlineMode().first()) {
|
||||||
scope.launch { runCatching { config.toClient().check("ping") } }
|
scope.launch { runCatching { config.toClient().check("ping") } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,7 +416,6 @@ class PlaybackController(
|
|||||||
if (offline) {
|
if (offline) {
|
||||||
queueRepository.setCursor(target)
|
queueRepository.setCursor(target)
|
||||||
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
||||||
refresh(target)
|
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
if (snapshot.upNextSize > 0L || updated.upNextSize > 0L || snapshot.shuffled) {
|
if (snapshot.upNextSize > 0L || updated.upNextSize > 0L || snapshot.shuffled) {
|
||||||
@@ -509,7 +508,6 @@ class PlaybackController(
|
|||||||
if (offline) {
|
if (offline) {
|
||||||
queueRepository.setCursor(target)
|
queueRepository.setCursor(target)
|
||||||
loadWindow(target, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
|
loadWindow(target, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
|
||||||
refresh(target)
|
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
@@ -564,9 +562,8 @@ class PlaybackController(
|
|||||||
val restoredId = queueRepository.itemAt(restored, target)?.song?.id
|
val restoredId = queueRepository.itemAt(restored, target)?.song?.id
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
if (offline) {
|
if (offline) {
|
||||||
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
|
||||||
queueRepository.setCursor(target)
|
queueRepository.setCursor(target)
|
||||||
refresh(target)
|
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
val canMirror =
|
val canMirror =
|
||||||
@@ -1054,6 +1051,10 @@ class PlaybackController(
|
|||||||
val snapshot = snapshot ?: return@withLock
|
val snapshot = snapshot ?: return@withLock
|
||||||
if (snapshot.size == 0L || player.isEnded) return@withLock
|
if (snapshot.size == 0L || player.isEnded) return@withLock
|
||||||
downloads.awaitLoaded()
|
downloads.awaitLoaded()
|
||||||
|
if (offline) {
|
||||||
|
reloadWindowLocked()
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
val position = (currentPosition() ?: return@withLock).coerceIn(0, snapshot.size - 1)
|
val position = (currentPosition() ?: return@withLock).coerceIn(0, snapshot.size - 1)
|
||||||
val window = queueRepository.window(snapshot, position, QUEUE_WINDOW_RADIUS)
|
val window = queueRepository.window(snapshot, position, QUEUE_WINDOW_RADIUS)
|
||||||
val uris = player.itemUris()
|
val uris = player.itemUris()
|
||||||
|
|||||||
@@ -252,24 +252,9 @@ class QueueViewModel(
|
|||||||
fun undo() {
|
fun undo() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
playbackController.undo()
|
playbackController.undo()
|
||||||
mutex.withLock { reload() }
|
mutex.withLock { reconcileLocked() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun reload() {
|
|
||||||
val snapshot = queueRepository.snapshot()
|
|
||||||
if (snapshot.size == 0L) {
|
|
||||||
rows.clear()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val start = (rows.minOfOrNull { it.position } ?: 0L).coerceIn(0, snapshot.size - 1)
|
|
||||||
val end = (rows.maxOfOrNull { it.position } ?: (start + QUEUE_CHUNK - 1)).coerceIn(start, snapshot.size - 1)
|
|
||||||
val loaded = queueRepository.range(snapshot, start, end).filter(::visible)
|
|
||||||
val reused = reusedIds()
|
|
||||||
val updated = loaded.map { rowFor(it, reused) }
|
|
||||||
rows.clear()
|
|
||||||
rows.addAll(updated)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -419,6 +419,46 @@ class LibraryDaoTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downloadedFilterSortsArtistsByTheirDownloadedAlbumCount() =
|
||||||
|
runTest {
|
||||||
|
val sourceId = source()
|
||||||
|
val dao = db.libraryDao()
|
||||||
|
dao.upsertArtists(
|
||||||
|
listOf(
|
||||||
|
artist(sourceId, "ar-1", "One", albumCount = 1),
|
||||||
|
artist(sourceId, "ar-2", "Two", albumCount = 3),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
dao.upsertAlbums(
|
||||||
|
listOf(
|
||||||
|
album(sourceId, "a1", "A1", year = null, starred = null),
|
||||||
|
album(sourceId, "a2", "A2", year = null, starred = null),
|
||||||
|
album(sourceId, "b1", "B1", year = null, starred = null).copy(artistId = "ar-2"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
dao.upsertSongs(
|
||||||
|
listOf(
|
||||||
|
song(sourceId, "s-a1", "A1", starred = null).copy(albumId = "a1"),
|
||||||
|
song(sourceId, "s-a2", "A2", starred = null).copy(albumId = "a2"),
|
||||||
|
song(sourceId, "s-b1", "B1", starred = null).copy(albumId = "b1", artistId = "ar-2"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
listOf("s-a1", "s-a2", "s-b1").forEach {
|
||||||
|
db.downloadDao().upsert(SongDownload(sourceId, it, DownloadStatus.Completed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ar-1 has two downloaded albums, ar-2 one; by synced count ar-2 (3) leads ar-1 (1).
|
||||||
|
assertEquals(
|
||||||
|
listOf("ar-1", "ar-2"),
|
||||||
|
dao.artistsByAlbumCount(sourceId, 0, "", downloadedFilter = 1).page().map { it.id },
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("ar-2", "ar-1"),
|
||||||
|
dao.artistsByAlbumCount(sourceId, 0, "").page().map { it.id },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun downloadedFilterReportsTheDownloadedSongCountForPlaylists() =
|
fun downloadedFilterReportsTheDownloadedSongCountForPlaylists() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -2497,6 +2497,74 @@ class PlaybackControllerTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlineRemovingThePlayingTrackPlaysTheNextDownloaded() {
|
||||||
|
seedAlbum(4, sourceId = 1)
|
||||||
|
markDownloaded(1, "s1")
|
||||||
|
markDownloaded(1, "s3")
|
||||||
|
setOffline()
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s1"
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking { controller.removeAt(0) }
|
||||||
|
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s3"
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
"s3",
|
||||||
|
controller.state.value.item
|
||||||
|
?.id,
|
||||||
|
)
|
||||||
|
assertEquals(1L, controller.state.value.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlineDeletingADownloadDropsItFromTheWindow() {
|
||||||
|
seedAlbum(3, sourceId = 1)
|
||||||
|
markDownloaded(1, "s1")
|
||||||
|
markDownloaded(1, "s2")
|
||||||
|
markDownloaded(1, "s3")
|
||||||
|
setOffline()
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await { handle.items.map { it.id } == listOf("s1", "s2", "s3") }
|
||||||
|
|
||||||
|
runBlocking { downloads.remove(1, "s2") }
|
||||||
|
controller.refreshMediaItems()
|
||||||
|
|
||||||
|
await { handle.items.none { it.id == "s2" } }
|
||||||
|
assertEquals(listOf("s1", "s3"), handle.items.map { it.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlinePreviousSkipsToThePreviousDownloaded() {
|
||||||
|
seedAlbum(5, sourceId = 1)
|
||||||
|
markDownloaded(1, "s1")
|
||||||
|
markDownloaded(1, "s4")
|
||||||
|
setOffline()
|
||||||
|
controller.playAlbum(1, "al1", 3)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s4"
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.previous()
|
||||||
|
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s1"
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
"s1",
|
||||||
|
controller.state.value.item
|
||||||
|
?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun offlinePlayStartsAtTheNearestDownloadedWhenTheTargetIsNotDownloaded() {
|
fun offlinePlayStartsAtTheNearestDownloadedWhenTheTargetIsNotDownloaded() {
|
||||||
seedAlbum(60, sourceId = 1)
|
seedAlbum(60, sourceId = 1)
|
||||||
@@ -2764,14 +2832,18 @@ class PlaybackControllerTest {
|
|||||||
songId: String,
|
songId: String,
|
||||||
) {
|
) {
|
||||||
val file = File(downloadsDir, "$sourceId/$songId")
|
val file = File(downloadsDir, "$sourceId/$songId")
|
||||||
|
val deadline = System.nanoTime() + 15_000_000_000L
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
downloads.reconcile()
|
|
||||||
db.downloadDao().upsert(SongDownload(sourceId, songId, DownloadStatus.Completed))
|
db.downloadDao().upsert(SongDownload(sourceId, songId, DownloadStatus.Completed))
|
||||||
file.parentFile?.mkdirs()
|
file.parentFile?.mkdirs()
|
||||||
file.writeBytes(byteArrayOf(1))
|
file.writeBytes(byteArrayOf(1))
|
||||||
downloads.reconcile()
|
downloads.reconcile()
|
||||||
}
|
}
|
||||||
await { downloads.localUri(songId) != null }
|
if (downloads.localUri(songId) != null) return
|
||||||
|
Thread.sleep(10)
|
||||||
|
}
|
||||||
|
assertTrue("Timed out marking $songId downloaded", downloads.localUri(songId) != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setOffline() {
|
private fun setOffline() {
|
||||||
|
|||||||
Reference in New Issue
Block a user