downloads: reconcile bulk deletion as one batch

This commit is contained in:
austinried
2026-11-14 12:05:29 +00:00
parent 35f4f9178b
commit 9514bc501a
3 changed files with 70 additions and 1 deletions
@@ -291,6 +291,17 @@ class DownloadRepository(
} }
} }
suspend fun remove(
sourceId: Long,
songIds: Collection<String>,
) = withContext(dispatcher) {
val ids = songIds.toHashSet()
mutex.withLock {
removeRows(db.downloadDao().all().filter { it.sourceId == sourceId && it.songId in ids })
sweep(db.downloadDao().all())
}
}
suspend fun cancel( suspend fun cancel(
sourceId: Long, sourceId: Long,
songIds: Collection<String>, songIds: Collection<String>,
@@ -78,7 +78,7 @@ class DownloadsViewModel(
fun deleteSongs(songIds: List<String>) { fun deleteSongs(songIds: List<String>) {
viewModelScope.launch { viewModelScope.launch {
val source = sourceId.first() val source = sourceId.first()
songIds.forEach { downloadRepository.remove(source, it) } downloadRepository.remove(source, songIds)
playbackController.refreshMediaItems() playbackController.refreshMediaItems()
} }
} }
@@ -7,17 +7,23 @@ import com.subtracks.data.model.DownloadList
import com.subtracks.data.model.DownloadStatus import com.subtracks.data.model.DownloadStatus
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
import com.subtracks.data.model.SongDownload import com.subtracks.data.model.SongDownload
import com.subtracks.data.repo.QueueResetPreparation
import com.subtracks.data.repo.replace
import com.subtracks.ui.ViewModelRepositoryFixture import com.subtracks.ui.ViewModelRepositoryFixture
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DownloadsViewModelTest { class DownloadsViewModelTest {
@@ -81,6 +87,58 @@ class DownloadsViewModelTest {
assertNotNull("an unnamed song should survive", row("s2")) assertNotNull("an unnamed song should survive", row("s2"))
} }
@Test
fun deletingSeveralSongsUsesOneResetPointForTheWholeBatch() {
sourceId =
runBlocking {
val id = sourceRepository.addSource("nav", "http://localhost/", "u", "p", false)
db.libraryDao().upsertSongs((1..5).map { song(id, "s$it", "al1") })
(1..5).forEach { track ->
db.downloadDao().upsert(SongDownload(id, "s$track", DownloadStatus.Completed, downloadedAt = (6 - track).toLong()))
val file = File(fixture.downloadsDir, "$id/s$track")
file.parentFile?.mkdirs()
file.writeBytes(byteArrayOf(1))
}
id
}
downloadRepository.start()
await { (1..5).all { downloadRepository.localUri("s$it") != null } }
runBlocking {
fixture.queueRepository.replace(listOf(fixture.queueRepository.downloadedEntry(sourceId)))
fixture.queueRepository.setCursor(2)
}
val beforeReset = runBlocking { fixture.queueRepository.snapshot().resetVersion }
val firstResetStarted = CountDownLatch(1)
val allowFirstReset = CountDownLatch(1)
var resets = 0
fixture.queueRepository.setResetListener {
resets++
if (resets == 1) {
firstResetStarted.countDown()
allowFirstReset.await(5, TimeUnit.SECONDS)
}
QueueResetPreparation(it.snapshot)
}
viewModel.deleteSongs(listOf("s2", "s3"))
assertTrue(firstResetStarted.await(5, TimeUnit.SECONDS))
allowFirstReset.countDown()
await { runBlocking { fixture.queueRepository.snapshot().resetVersion > beforeReset } }
val snapshot = runBlocking { fixture.queueRepository.snapshot() }
assertEquals(2L, runBlocking { fixture.queueRepository.cursor() })
assertEquals(
"s5",
runBlocking {
fixture.queueRepository
.itemAt(snapshot, 2)
?.song
?.id
},
)
assertEquals(1, resets)
}
@Test @Test
fun cancelSongsRemovesOnlyTheActiveDownload() { fun cancelSongsRemovesOnlyTheActiveDownload() {
seedDownloads() seedDownloads()