mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
download a whole album, playlist or artist at once
Third block of #65: bulk downloads and the list-level actions. - The hero's download button on albums and playlists is real now. It starts the songs the list is missing, cancels what is in flight, or deletes what is already downloaded, chosen from the list's own status. The artist has no hero button, so its bulk actions live in the context menu, which now carries the same status. - No new storage: a bulk job is the download rows for the list, and the status is counted from song_downloads joined to the list's songs, so cancel and delete stay well defined without anything to keep in sync. - downloadAll fetches artwork for the whole list under one shared budget, once per unique cover, and skips songs that are already downloaded so re-tapping Download resumes rather than restarting.
This commit is contained in:
@@ -12,13 +12,18 @@ import com.subtracks.data.download.ArtworkStore
|
||||
import com.subtracks.data.download.FakeDownloadEngine
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.DownloadList
|
||||
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.Song
|
||||
import com.subtracks.data.model.SongDownload
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.model.SubsonicSource
|
||||
import com.subtracks.data.model.coverArtKey
|
||||
import com.subtracks.data.prefs.fakeUserPreferences
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -418,6 +423,97 @@ class DownloadRepositoryTest {
|
||||
assertNotNull(artwork.uri(1, coverArtKey(1, ALBUM_ART, false)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadingAListQueuesEverySongAndStoresItsArtworkOnce() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
|
||||
runBlocking { repository.downloadAll(1, DownloadList.Album, "al1") }
|
||||
|
||||
assertEquals(2, engine.requests.size)
|
||||
assertEquals(4, requestedArt.size)
|
||||
assertNotNull(artwork.uri(1, coverArtKey(1, ALBUM_ART, true)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadingAListSkipsTheSongsItAlreadyHas() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking { repository.download(1, "s1") }
|
||||
val before = engine.requests.size
|
||||
|
||||
runBlocking { repository.downloadAll(1, DownloadList.Album, "al1") }
|
||||
|
||||
assertEquals(before + 1, engine.requests.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cancelAllStopsTheRunningSongsAndKeepsTheCompletedOnes() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
writeFile(1, "s1", "audio")
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, engineId = 11))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Running, engineId = 22))
|
||||
|
||||
runBlocking { repository.cancelAll(1, DownloadList.Album, "al1") }
|
||||
|
||||
assertNotNull(row(1, "s1"))
|
||||
assertNull(row(1, "s2"))
|
||||
assertTrue(engine.cancelled.contains(22L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllRemovesTheCompletedSongsAndKeepsTheRunningOnes() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
writeFile(1, "s1", "audio")
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, engineId = 11))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Running, engineId = 22))
|
||||
|
||||
runBlocking { repository.deleteAll(1, DownloadList.Album, "al1") }
|
||||
|
||||
assertNull(row(1, "s1"))
|
||||
assertFalse(file(1, "s1").exists())
|
||||
assertNotNull(row(1, "s2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theListStatusCountsTheSongsAndTracksProgress() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
writeFile(1, "s1", "audio")
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, engineId = 11))
|
||||
|
||||
assertEquals(ListDownloadStatus(total = 2, downloaded = 1, downloading = 0), statusOf(DownloadList.Album, "al1"))
|
||||
assertEquals(ListDownloadStatus(total = 2, downloaded = 1, downloading = 0), statusOf(DownloadList.Artist, "ar1"))
|
||||
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Queued, engineId = 22))
|
||||
|
||||
assertEquals(ListDownloadStatus(total = 2, downloaded = 1, downloading = 1), statusOf(DownloadList.Album, "al1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun thePlaylistStatusCountsASongThatAppearsTwiceOnce() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking {
|
||||
db.libraryDao().upsertPlaylists(listOf(Playlist(1, "pl1", "Playlist", null, null, 2, 0)))
|
||||
db.libraryDao().upsertPlaylistSongs(
|
||||
listOf(
|
||||
PlaylistSong(1, "pl1", "s1", 0),
|
||||
PlaylistSong(1, "pl1", "s1", 1),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(ListDownloadStatus(total = 1, downloaded = 0, downloading = 0), statusOf(DownloadList.Playlist, "pl1"))
|
||||
}
|
||||
|
||||
private fun statusOf(
|
||||
list: DownloadList,
|
||||
refId: String,
|
||||
): ListDownloadStatus = runBlocking { repository.status(1, list, refId).first() }
|
||||
|
||||
private fun album(coverArt: String?) =
|
||||
Album(
|
||||
sourceId = 1,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.subtracks.ui.components
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.model.ListDownloadStatus
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class HeroDownloadTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun anUndownloadedListOffersDownload() {
|
||||
assertEquals(BulkDownloadAction.Download, ListDownloadStatus(total = 3).action())
|
||||
assertEquals(BulkDownloadAction.Download, ListDownloadStatus().action())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aPartiallyDownloadedListStillOffersDownload() {
|
||||
assertEquals(BulkDownloadAction.Download, ListDownloadStatus(total = 3, downloaded = 1).action())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aListWithDownloadsInFlightOffersCancel() {
|
||||
assertEquals(BulkDownloadAction.Cancel, ListDownloadStatus(total = 3, downloaded = 1, downloading = 1).action())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFullyDownloadedListOffersDelete() {
|
||||
assertEquals(BulkDownloadAction.Delete, ListDownloadStatus(total = 3, downloaded = 3).action())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tappingTheButtonReportsTheActionItsStateOffers() {
|
||||
val actions = mutableListOf<BulkDownloadAction>()
|
||||
render(ListDownloadStatus(total = 3, downloaded = 1, downloading = 1)) { actions += it }
|
||||
|
||||
composeRule.onNodeWithContentDescription("Cancel download").performClick()
|
||||
|
||||
assertEquals(listOf(BulkDownloadAction.Cancel), actions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFullyDownloadedListTapsThroughToDelete() {
|
||||
val actions = mutableListOf<BulkDownloadAction>()
|
||||
render(ListDownloadStatus(total = 3, downloaded = 3)) { actions += it }
|
||||
|
||||
composeRule.onNodeWithContentDescription("Delete download").performClick()
|
||||
|
||||
assertEquals(listOf(BulkDownloadAction.Delete), actions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theButtonIsDisabledWithoutSongs() {
|
||||
render(ListDownloadStatus(), hasSongs = false) {}
|
||||
|
||||
composeRule.onNodeWithContentDescription("Download").assertIsNotEnabled()
|
||||
}
|
||||
|
||||
private fun render(
|
||||
status: ListDownloadStatus,
|
||||
hasSongs: Boolean = true,
|
||||
onAction: (BulkDownloadAction) -> Unit,
|
||||
) {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
HeroHeader(
|
||||
art = null,
|
||||
name = "Album",
|
||||
subtitle = "Artist",
|
||||
hasSongs = hasSongs,
|
||||
onPlay = {},
|
||||
onShuffle = {},
|
||||
downloadStatus = status,
|
||||
onDownloadAction = onAction,
|
||||
onMore = {},
|
||||
topInset = 0.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user