mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
library: reconcile queues after starred and download changes
This commit is contained in:
@@ -21,6 +21,7 @@ import com.subtracks.data.model.DownloadStatus
|
||||
import com.subtracks.data.model.ListDownloadStatus
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.QueueKind
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.SongDownload
|
||||
import com.subtracks.data.model.Source
|
||||
@@ -61,6 +62,7 @@ class DownloadRepositoryTest {
|
||||
private lateinit var sources: SourceRepository
|
||||
private lateinit var engine: FakeDownloadEngine
|
||||
private lateinit var repository: DownloadRepository
|
||||
private lateinit var queues: QueueRepository
|
||||
private lateinit var artwork: ArtworkStore
|
||||
private lateinit var dir: File
|
||||
private lateinit var scope: CoroutineScope
|
||||
@@ -89,6 +91,7 @@ class DownloadRepositoryTest {
|
||||
sources = SourceRepository(db, OkHttpClient(), prefs, artwork, scope = scope)
|
||||
engine = FakeDownloadEngine()
|
||||
dir = File(context.cacheDir, "downloads-${System.nanoTime()}")
|
||||
queues = QueueRepository(db)
|
||||
repository =
|
||||
DownloadRepository(
|
||||
db,
|
||||
@@ -97,6 +100,7 @@ class DownloadRepositoryTest {
|
||||
dir,
|
||||
artworkStore = artwork,
|
||||
artworkFetcher = fetcher,
|
||||
queueRepository = queues,
|
||||
showMessage = { messages += it.resolve(resources) },
|
||||
scope = scope,
|
||||
)
|
||||
@@ -361,6 +365,34 @@ class DownloadRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllCapturesTheCurrentQueuePositionBeforeRemovingDownloads() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
seedSongs(3..3)
|
||||
runBlocking {
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, downloadedAt = 1))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 2))
|
||||
queues.replace(listOf(queues.downloadedEntry(1), queues.albumEntry(1, "al1")))
|
||||
}
|
||||
writeFile(1, "s1", "audio")
|
||||
writeFile(1, "s2", "audio")
|
||||
repository.start()
|
||||
await { repository.localUri("s1") != null && repository.localUri("s2") != null }
|
||||
runBlocking { repository.awaitLoaded() }
|
||||
runBlocking {
|
||||
queues.setCursor(4)
|
||||
queues.setPosition(42_000L)
|
||||
}
|
||||
val beforeReset = runBlocking { queues.snapshot().resetVersion }
|
||||
|
||||
runBlocking { repository.removeSource(1) }
|
||||
await { runBlocking { queues.snapshot().resetVersion > beforeReset } }
|
||||
|
||||
assertEquals(2L, runBlocking { queues.cursor() })
|
||||
assertEquals(42_000L, runBlocking { queues.cursorPositionMs() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reconcileDeletesFilesThatBelongToNoDownload() =
|
||||
runTest {
|
||||
@@ -613,6 +645,19 @@ class DownloadRepositoryTest {
|
||||
assertNotNull(artwork.uri(1, coverArtKey(1, ALBUM_ART, false)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun artworkSurvivesTheDownloadedSongGoingMissing() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking { repository.download(1, "s1") }
|
||||
assertNotNull(artwork.uri(1, coverArtKey(1, ALBUM_ART, false)))
|
||||
|
||||
runBlocking { db.libraryDao().deleteSongs(1, listOf("s1")) }
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
assertNotNull(artwork.uri(1, coverArtKey(1, ALBUM_ART, false)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removingTheLastDownloadOfAnAlbumRemovesItsArtwork() =
|
||||
runTest {
|
||||
@@ -1354,6 +1399,213 @@ class DownloadRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun switchingSourcesDoesNotResetAnEditedDownloadedQueue() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking {
|
||||
db.sourcesDao().upsertSource(Source(2, "other", "http://other/", isActive = false, createdAt = 1))
|
||||
db.libraryDao().upsertSongs(
|
||||
listOf(
|
||||
Song(
|
||||
2,
|
||||
"o1",
|
||||
"other-album",
|
||||
"other-artist",
|
||||
"Other",
|
||||
"Other Album",
|
||||
"Other Artist",
|
||||
100,
|
||||
track = 1,
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, downloadedAt = 1))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 2))
|
||||
db.downloadDao().upsert(SongDownload(2, "o1", DownloadStatus.Completed, downloadedAt = 1))
|
||||
}
|
||||
writeFile(1, "s1", "one")
|
||||
writeFile(1, "s2", "two")
|
||||
writeFile(2, "o1", "other")
|
||||
repository.start()
|
||||
await { repository.localUri("s1") != null }
|
||||
runBlocking {
|
||||
queues.replace(listOf(queues.downloadedEntry(1)))
|
||||
queues.removeAt(queues.snapshot(), 1)
|
||||
}
|
||||
val edited = runBlocking { queues.snapshot().entries.map { it.entry } }
|
||||
assertEquals(1, edited.size)
|
||||
assertNotNull(edited.single().rangeStart)
|
||||
|
||||
runBlocking { sources.selectSource(2) }
|
||||
await { repository.localUri("o1") != null }
|
||||
|
||||
assertEquals(edited, runBlocking { queues.snapshot().entries.map { it.entry } })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aDownloadCompletedWhileItsSourceIsInactiveResetsOnReturn() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking {
|
||||
db.sourcesDao().upsertSource(Source(2, "other", "http://other/", isActive = false, createdAt = 1))
|
||||
db.libraryDao().upsertSongs(
|
||||
listOf(
|
||||
Song(1, "s3", "al1", "ar1", "Song 3", "Album", "Artist", 100, track = 3, disc = 1, starred = null, genre = null),
|
||||
Song(
|
||||
2,
|
||||
"o1",
|
||||
"other-album",
|
||||
"other-artist",
|
||||
"Other",
|
||||
"Other Album",
|
||||
"Other Artist",
|
||||
100,
|
||||
track = 1,
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, downloadedAt = 1))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 2))
|
||||
db.downloadDao().upsert(SongDownload(2, "o1", DownloadStatus.Completed, downloadedAt = 1))
|
||||
}
|
||||
writeFile(1, "s1", "one")
|
||||
writeFile(1, "s2", "two")
|
||||
writeFile(2, "o1", "other")
|
||||
repository.start()
|
||||
await { repository.localUri("s1") != null }
|
||||
runBlocking {
|
||||
queues.replace(listOf(queues.downloadedEntry(1)))
|
||||
queues.removeAt(queues.snapshot(), 1)
|
||||
sources.selectSource(2)
|
||||
}
|
||||
await { repository.localUri("o1") != null }
|
||||
assertNotNull(
|
||||
runBlocking {
|
||||
queues
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart
|
||||
},
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
repository.captureForReset(1)
|
||||
db.downloadDao().upsert(SongDownload(1, "s3", DownloadStatus.Completed, downloadedAt = 3))
|
||||
writeFile(1, "s3", "three")
|
||||
repository.reconcile()
|
||||
sources.selectSource(1)
|
||||
}
|
||||
|
||||
await { repository.localUri("s3") != null }
|
||||
assertNull(
|
||||
runBlocking {
|
||||
queues
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun backfillingDownloadTimeResetsAnEditedDownloadedQueue() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking {
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, engineId = 42, downloadedAt = 0))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 1))
|
||||
}
|
||||
writeFile(1, "s1", "one")
|
||||
writeFile(1, "s2", "two")
|
||||
repository.start()
|
||||
await { repository.localUri("s1") != null && repository.localUri("s2") != null }
|
||||
runBlocking {
|
||||
queues.replace(listOf(queues.downloadedEntry(1)))
|
||||
queues.removeAt(queues.snapshot(), 0)
|
||||
}
|
||||
assertNotNull(
|
||||
runBlocking {
|
||||
queues
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart
|
||||
},
|
||||
)
|
||||
|
||||
engine.complete(42, total = 10)
|
||||
runBlocking { repository.reconcile() }
|
||||
await { runBlocking { (row(1, "s1")?.downloadedAt ?: 0L) > 0L } }
|
||||
Thread.sleep(200)
|
||||
|
||||
assertNull(
|
||||
runBlocking {
|
||||
queues
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMultiRowRecoveryKeepsTheResetPointFromBeforeTheWholeBatch() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
seedSongs(3..3)
|
||||
runBlocking {
|
||||
db.downloadDao().upsert(SongDownload(1, "s3", DownloadStatus.Completed, downloadedAt = 1))
|
||||
queues.replace(listOf(queues.downloadedEntry(1)))
|
||||
queues.setCursor(0)
|
||||
repository.captureForReset(1)
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, downloadedAt = 2))
|
||||
repository.captureForReset(1)
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 3))
|
||||
repository.captureForReset(1)
|
||||
}
|
||||
val field = DownloadRepository::class.java.getDeclaredField("pendingResetPoints")
|
||||
field.isAccessible = true
|
||||
val pending = field.get(repository) as Map<*, *>
|
||||
val point = pending[1L] as QueueResetPoint
|
||||
|
||||
assertEquals("s3", point.current?.songId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startupRecoveryResetsAnEditedDownloadedQueueWhenAFileIsMissing() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking {
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, downloadedAt = 1))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed, downloadedAt = 2))
|
||||
queues.replace(listOf(queues.downloadedEntry(1).copy(rangeStart = 1)))
|
||||
}
|
||||
writeFile(1, "s1", "one")
|
||||
|
||||
repository.start()
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
await {
|
||||
runBlocking {
|
||||
queues
|
||||
.snapshot()
|
||||
.entries
|
||||
.single()
|
||||
.entry.rangeStart == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun seedPlaylist(songIds: List<String>) {
|
||||
runBlocking {
|
||||
db.libraryDao().upsertPlaylists(listOf(Playlist(1, "pl1", "Playlist", null, null, songIds.size.toLong(), 0)))
|
||||
|
||||
@@ -72,6 +72,7 @@ class LibraryRepositoryTest {
|
||||
sourceRepository,
|
||||
NetworkServerActionSink(sourceRepository),
|
||||
fakeUserPreferences(),
|
||||
queueRepository = QueueRepository(db),
|
||||
showMessage = { messages += it.resolve(resources) },
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
@@ -538,6 +538,7 @@ class SyncManagerTest {
|
||||
downloadsDir,
|
||||
artworkStore = ArtworkStore(File(context.cacheDir, "art")),
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
queueRepository = queueRepository,
|
||||
scope = scope,
|
||||
)
|
||||
File(downloadsDir, "1").mkdirs()
|
||||
|
||||
@@ -119,6 +119,7 @@ class PlaybackControllerTest {
|
||||
downloadsDir,
|
||||
artworkStore = ArtworkStore(File(context.cacheDir, "art")),
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
queueRepository = queues,
|
||||
scope = repoScope,
|
||||
).also { it.start() }
|
||||
handle = FakePlayerHandle()
|
||||
@@ -3928,6 +3929,34 @@ class PlaybackControllerTest {
|
||||
assertEquals(2L, controller.state.value.position)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aDownloadInsertedBeforeCurrentKeepsTheActualCurrentSong() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
seedSong("s0", "al1")
|
||||
markDownloaded(1, "s1", downloadedAt = 200)
|
||||
markDownloaded(1, "s2", downloadedAt = 100)
|
||||
controller.playDownloaded(1, 1)
|
||||
await { handle.currentItem?.id == "s2" }
|
||||
|
||||
markDownloaded(1, "s0", downloadedAt = 300)
|
||||
|
||||
await { controller.state.value.position == 2L && handle.currentItem?.id == "s2" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deletingTheCurrentDownloadDetachesThatSong() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
markDownloaded(1, "s1", downloadedAt = 200)
|
||||
markDownloaded(1, "s2", downloadedAt = 100)
|
||||
controller.playDownloaded(1, 1)
|
||||
await { handle.currentItem?.id == "s2" }
|
||||
|
||||
runBlocking { downloads.remove(1, "s2") }
|
||||
|
||||
await { handle.items.take(2).map { it.id } == listOf("s2", "s1") }
|
||||
assertEquals("s2", handle.currentItem?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aDetachedTrackSurvivesMediaRefresh() {
|
||||
seedPlaylist(5)
|
||||
@@ -4369,6 +4398,7 @@ class PlaybackControllerTest {
|
||||
val deadline = System.nanoTime() + TEST_TIMEOUT_MS * 1_000_000
|
||||
while (System.nanoTime() < deadline) {
|
||||
runBlocking {
|
||||
downloads.captureForReset(sourceId)
|
||||
db.downloadDao().upsert(SongDownload(sourceId, songId, DownloadStatus.Completed, downloadedAt = downloadedAt))
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeBytes(byteArrayOf(1))
|
||||
|
||||
@@ -42,6 +42,7 @@ class ViewModelRepositoryFixture {
|
||||
val downloadEngine = FakeDownloadEngine()
|
||||
val downloadsDir = File(context.cacheDir, "downloads-${System.nanoTime()}")
|
||||
val sourceRepository = SourceRepository(db, OkHttpClient(), fakeUserPreferences(), artworkStore, scope = repoScope)
|
||||
val queueRepository = QueueRepository(db)
|
||||
val downloadRepository =
|
||||
DownloadRepository(
|
||||
db,
|
||||
@@ -50,9 +51,9 @@ class ViewModelRepositoryFixture {
|
||||
downloadsDir,
|
||||
artworkStore = artworkStore,
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
queueRepository = queueRepository,
|
||||
scope = repoScope,
|
||||
)
|
||||
val queueRepository = QueueRepository(db)
|
||||
val controller =
|
||||
PlaybackController(
|
||||
sourceRepository,
|
||||
@@ -68,6 +69,7 @@ class ViewModelRepositoryFixture {
|
||||
sourceRepository,
|
||||
NetworkServerActionSink(sourceRepository),
|
||||
fakeUserPreferences(),
|
||||
queueRepository = queueRepository,
|
||||
scope = repoScope,
|
||||
)
|
||||
|
||||
|
||||
@@ -91,9 +91,10 @@ class NowPlayingRouteTest {
|
||||
File(context.cacheDir, "downloads-${System.nanoTime()}"),
|
||||
artworkStore = artworkStore,
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
queueRepository = queues,
|
||||
scope = repoScope,
|
||||
).also { it.start() }
|
||||
library = LibraryRepository(db, sources, serverActions, prefs, scope = repoScope)
|
||||
library = LibraryRepository(db, sources, serverActions, prefs, queueRepository = queues, scope = repoScope)
|
||||
handle = FakePlayerHandle()
|
||||
controller =
|
||||
PlaybackController(
|
||||
|
||||
@@ -83,6 +83,7 @@ class QueueViewModelTest {
|
||||
downloadsDir,
|
||||
artworkStore = artwork,
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
queueRepository = queues,
|
||||
scope = repoScope,
|
||||
).also { it.start() }
|
||||
controller =
|
||||
|
||||
@@ -67,6 +67,7 @@ class AddSourceViewModelTest {
|
||||
File(context.cacheDir, "downloads-${System.nanoTime()}"),
|
||||
artworkStore = artwork,
|
||||
artworkFetcher = { ByteArray(0) },
|
||||
queueRepository = QueueRepository(db),
|
||||
scope = repoScope,
|
||||
)
|
||||
server = MockWebServer()
|
||||
|
||||
Reference in New Issue
Block a user