diff --git a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt index deb915ea..f4debf21 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt @@ -19,9 +19,12 @@ import com.subtracks.data.model.DownloadList import com.subtracks.data.model.DownloadStatus import com.subtracks.data.model.DownloadedSong import com.subtracks.data.model.ListDownloadStatus +import com.subtracks.data.model.QueueKind +import com.subtracks.data.model.QueueOrigin import com.subtracks.data.model.SongDownload import com.subtracks.data.model.artistImageKey import com.subtracks.data.model.coverArtKey +import com.subtracks.data.repo.QueueResetPoint import com.subtracks.log.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred @@ -55,6 +58,7 @@ class DownloadRepository( private val downloadsDir: File, private val artworkStore: ArtworkStore, private val artworkFetcher: ArtworkFetcher, + private val queueRepository: QueueRepository, private val showMessage: (UiMessage) -> Unit = {}, private val dispatcher: CoroutineDispatcher = Dispatchers.IO, private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher), @@ -67,6 +71,9 @@ class DownloadRepository( private var started = false private val statesFlow = MutableStateFlow>(emptyMap()) + private val completedBySource = mutableMapOf>() + private val completedBaselineMutex = Mutex() + private val pendingResetPoints = mutableMapOf() private val attemptedBios = ConcurrentHashMap.newKeySet>() private val failedArtistImages = ConcurrentHashMap.newKeySet() @@ -76,8 +83,21 @@ class DownloadRepository( downloadsDir.mkdirs() File(downloadsDir, NO_MEDIA).createNewFile() scope.launch { - downloadStates().collect { rows -> + var resetPoint = queueRepository.captureResetPoint() + downloadStates().collect { (sourceId, rows) -> statesFlow.value = rows + if (sourceId != null) { + val completed = completedDownloads(rows) + completedBaselineMutex.withLock { + val previous = completedBySource[sourceId] + if (previous != null && completed != previous) { + val point = pendingResetPoints.remove(sourceId) ?: resetPoint + queueRepository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Downloaded, sourceId, "")), point) + completedBySource[sourceId] = completed + } + } + } + resetPoint = queueRepository.captureResetPoint() loaded.complete(Unit) } } @@ -96,6 +116,25 @@ class DownloadRepository( fun states(): StateFlow> = statesFlow + internal suspend fun captureForReset(sourceId: Long) = + completedBaselineMutex.withLock { + if (sourceId !in pendingResetPoints) pendingResetPoints[sourceId] = queueRepository.captureResetPoint() + } + + private suspend fun ensureCompletedBaselines(rows: List) = + completedBaselineMutex.withLock { + rows.groupBy { it.sourceId }.forEach { (sourceId, sourceRows) -> + if (sourceId !in completedBySource) { + completedBySource[sourceId] = completedDownloads(sourceRows.associateBy { it.songId }) + } + } + } + + private fun completedDownloads(rows: Map): Map = + rows.values + .filter { it.status == DownloadStatus.Completed } + .associate { it.songId to it.downloadedAt } + @OptIn(ExperimentalCoroutinesApi::class) fun activeDownloads(): Flow> = sourceRepository.activeSourceId().flatMapLatest { sourceId -> @@ -351,6 +390,7 @@ class DownloadRepository( val engineIds = rows.mapNotNull { it.engineId } if (engineIds.isNotEmpty()) engine.cancel(engineIds) rows.groupBy { it.sourceId }.forEach { (sourceId, group) -> + if (group.any { it.status == DownloadStatus.Completed }) captureForReset(sourceId) group.forEach { file(sourceId, it.songId).delete() } group.map { it.songId }.chunked(DELETE_CHUNK).forEach { db.downloadDao().deleteSongs(sourceId, it) } if (group.any { it.status == DownloadStatus.Completed }) { @@ -371,12 +411,9 @@ class DownloadRepository( suspend fun removeSource(sourceId: Long) = withContext(dispatcher) { mutex.withLock { - db - .downloadDao() - .all() - .filter { it.sourceId == sourceId } - .mapNotNull { it.engineId } - .let(engine::cancel) + val rows = db.downloadDao().all().filter { it.sourceId == sourceId } + if (rows.any { it.status == DownloadStatus.Completed }) captureForReset(sourceId) + rows.mapNotNull { it.engineId }.let(engine::cancel) db.downloadDao().deleteSource(sourceId) db.libraryDao().clearDownloadedArtistArt(sourceId) dir(sourceId).deleteRecursively() @@ -388,6 +425,7 @@ class DownloadRepository( val promoted = mutex.withLock { val rows = db.downloadDao().all() + ensureCompletedBaselines(rows) reconcileRows(rows) sweep(rows) ensurePolling() @@ -397,11 +435,16 @@ class DownloadRepository( } @OptIn(ExperimentalCoroutinesApi::class) - private fun downloadStates(): Flow> = + private fun downloadStates(): Flow>> = sourceRepository .activeSourceId() - .flatMapLatest { sourceId -> if (sourceId == null) flowOf(emptyList()) else db.downloadDao().downloads(sourceId) } - .map { rows -> rows.associateBy { it.songId } } + .flatMapLatest { sourceId -> + if (sourceId == null) { + flowOf(null to emptyMap()) + } else { + db.downloadDao().downloads(sourceId).map { rows -> sourceId to rows.associateBy { it.songId } } + } + } private suspend fun alreadyDownloadingOrDownloaded( sourceId: Long, @@ -585,6 +628,7 @@ class DownloadRepository( songId: String, ): Boolean { val row = db.downloadDao().find(sourceId, songId) ?: return false + if (row.status == DownloadStatus.Completed) captureForReset(sourceId) row.engineId?.let { engine.cancel(listOf(it)) } file(sourceId, songId).delete() db.downloadDao().delete(sourceId, songId) @@ -668,6 +712,14 @@ class DownloadRepository( row.downloadedAt }, ) + if (updated != row && + ( + updated.status == DownloadStatus.Completed || row.status == DownloadStatus.Completed || + updated.downloadedAt != row.downloadedAt + ) + ) { + captureForReset(row.sourceId) + } if (updated != row) db.downloadDao().upsert(updated) if (updated.status != row.status && (updated.status == DownloadStatus.Completed || row.status == DownloadStatus.Completed) @@ -688,6 +740,7 @@ class DownloadRepository( row: SongDownload, error: DownloadError, ) { + if (row.status == DownloadStatus.Completed) captureForReset(row.sourceId) row.engineId?.let { engine.cancel(listOf(it)) } file(row.sourceId, row.songId).delete() db.downloadDao().upsert(row.copy(status = DownloadStatus.Failed, engineId = null, error = error)) diff --git a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt index d7510757..4b6e8574 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -19,6 +19,8 @@ import com.subtracks.data.model.ArtistBio import com.subtracks.data.model.Disc import com.subtracks.data.model.Playlist import com.subtracks.data.model.PlaylistSongItem +import com.subtracks.data.model.QueueKind +import com.subtracks.data.model.QueueOrigin import com.subtracks.data.model.Song import com.subtracks.data.prefs.AlbumSort import com.subtracks.data.prefs.ArtistSort @@ -50,6 +52,7 @@ class LibraryRepository( private val sourceRepository: SourceRepository, private val serverActions: ServerActionSink, private val userPreferences: UserPreferences, + private val queueRepository: QueueRepository, private val showMessage: (UiMessage) -> Unit = {}, private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO), ) { @@ -452,10 +455,18 @@ class LibraryRepository( starred: Long?, ) { val dao = db.libraryDao() - when (type) { - StarType.Song -> dao.setSongStar(sourceId, id, starred) - StarType.Album -> dao.setAlbumStar(sourceId, id, starred) - StarType.Artist -> dao.setArtistStar(sourceId, id, starred) + if (type == StarType.Song) { + val previous = song(sourceId, id).first()?.starred + if (previous == starred) return + val point = queueRepository.captureResetPoint() + dao.setSongStar(sourceId, id, starred) + queueRepository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Starred, sourceId, "")), point) + } else { + when (type) { + StarType.Album -> dao.setAlbumStar(sourceId, id, starred) + StarType.Artist -> dao.setArtistStar(sourceId, id, starred) + StarType.Song -> Unit + } } } diff --git a/app/src/main/kotlin/com/subtracks/di/AppModule.kt b/app/src/main/kotlin/com/subtracks/di/AppModule.kt index dca08d10..8916de63 100644 --- a/app/src/main/kotlin/com/subtracks/di/AppModule.kt +++ b/app/src/main/kotlin/com/subtracks/di/AppModule.kt @@ -96,7 +96,7 @@ fun appModule( scope = get(), ) } - single { LibraryRepository(get(), get(), get(), get(), toast, scope = get(named("io"))) } + single { LibraryRepository(get(), get(), get(), get(), get(), toast, scope = get(named("io"))) } single { ArtworkSeedRepository(get()) } single { SyncManager(get(), get(), get(), get(), preferences = get(), showMessage = toast, scope = get()) } single { @@ -120,6 +120,7 @@ fun appModule( downloadsDir = downloadsRoot(context), artworkStore = get(), artworkFetcher = get(), + queueRepository = get(), showMessage = toast, dispatcher = Dispatchers.IO, scope = get(named("io")), diff --git a/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt b/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt index 8ff29905..e54a73bb 100644 --- a/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/repo/DownloadRepositoryTest.kt @@ -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) { runBlocking { db.libraryDao().upsertPlaylists(listOf(Playlist(1, "pl1", "Playlist", null, null, songIds.size.toLong(), 0))) diff --git a/app/src/test/kotlin/com/subtracks/data/repo/LibraryRepositoryTest.kt b/app/src/test/kotlin/com/subtracks/data/repo/LibraryRepositoryTest.kt index 8498ccde..7406dc5e 100644 --- a/app/src/test/kotlin/com/subtracks/data/repo/LibraryRepositoryTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/repo/LibraryRepositoryTest.kt @@ -72,6 +72,7 @@ class LibraryRepositoryTest { sourceRepository, NetworkServerActionSink(sourceRepository), fakeUserPreferences(), + queueRepository = QueueRepository(db), showMessage = { messages += it.resolve(resources) }, scope = scope, ) diff --git a/app/src/test/kotlin/com/subtracks/data/sync/SyncManagerTest.kt b/app/src/test/kotlin/com/subtracks/data/sync/SyncManagerTest.kt index 9178c49f..b09f54bb 100644 --- a/app/src/test/kotlin/com/subtracks/data/sync/SyncManagerTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/sync/SyncManagerTest.kt @@ -538,6 +538,7 @@ class SyncManagerTest { downloadsDir, artworkStore = ArtworkStore(File(context.cacheDir, "art")), artworkFetcher = { ByteArray(0) }, + queueRepository = queueRepository, scope = scope, ) File(downloadsDir, "1").mkdirs() diff --git a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt index b4c62731..a3bda73c 100644 --- a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt +++ b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt @@ -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)) diff --git a/app/src/test/kotlin/com/subtracks/ui/ViewModelRepositoryFixture.kt b/app/src/test/kotlin/com/subtracks/ui/ViewModelRepositoryFixture.kt index 8d4d62df..9a7e8106 100644 --- a/app/src/test/kotlin/com/subtracks/ui/ViewModelRepositoryFixture.kt +++ b/app/src/test/kotlin/com/subtracks/ui/ViewModelRepositoryFixture.kt @@ -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, ) diff --git a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingRouteTest.kt b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingRouteTest.kt index 29a7e1f3..27e8326a 100644 --- a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingRouteTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingRouteTest.kt @@ -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( diff --git a/app/src/test/kotlin/com/subtracks/ui/playback/QueueViewModelTest.kt b/app/src/test/kotlin/com/subtracks/ui/playback/QueueViewModelTest.kt index 1b6cd98b..9600974c 100644 --- a/app/src/test/kotlin/com/subtracks/ui/playback/QueueViewModelTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/playback/QueueViewModelTest.kt @@ -83,6 +83,7 @@ class QueueViewModelTest { downloadsDir, artworkStore = artwork, artworkFetcher = { ByteArray(0) }, + queueRepository = queues, scope = repoScope, ).also { it.start() } controller = diff --git a/app/src/test/kotlin/com/subtracks/ui/settings/AddSourceViewModelTest.kt b/app/src/test/kotlin/com/subtracks/ui/settings/AddSourceViewModelTest.kt index 53624377..4ae418e0 100644 --- a/app/src/test/kotlin/com/subtracks/ui/settings/AddSourceViewModelTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/settings/AddSourceViewModelTest.kt @@ -67,6 +67,7 @@ class AddSourceViewModelTest { File(context.cacheDir, "downloads-${System.nanoTime()}"), artworkStore = artwork, artworkFetcher = { ByteArray(0) }, + queueRepository = QueueRepository(db), scope = repoScope, ) server = MockWebServer()