mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
publish the download map before signalling it is loaded
Second review round on the downloads block: - `loaded` was completed upstream of the state flow that publishes the map, so `awaitLoaded()` could return while the map was still the initial empty one and the first window after a cold start would stream a track that is already on disk. The collector now assigns the map before completing, and the map is a plain state flow instead of a lazy eager `stateIn` with a bare touch to start it. - A failed download now deletes its partial file, in both the engine-failure and the recovery paths. A failed row used to protect its own partial bytes from the sweep, and the UI offers no delete for it. - A queued row is left queued when the download URL cannot be resolved yet, instead of being failed. At launch the repository's reconcile can run before the source repository has built its client, and failing there turned a resumable download into a dead one. - The poller only considers the active source's rows, so a queued row belonging to another source no longer keeps it polling the database every second. - `SourceRepository.downloadUri` takes the expected source id and returns null unless it is the active one, so a download cannot be enqueued against the previous server's URL during a source switch. - Tests: a stream with no content length still gets its declared length through the production data source factory (the http half of the chain, which the first round left uncovered), and an engine failure is asserted to leave no partial file. The engine's status mapping now runs as a plain unit test.
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
package com.subtracks.data.download
|
||||
|
||||
import android.app.DownloadManager
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SystemDownloadEngineTest {
|
||||
@Test
|
||||
fun successfulRunningAndFailedStatusesAreMapped() {
|
||||
|
||||
@@ -188,17 +188,19 @@ class DownloadRepositoryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEngineFailureIsMirroredWithItsMessage() =
|
||||
fun anEngineFailureIsMirroredWithItsMessageAndLeavesNoPartialFile() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking { repository.download(1, "s1") }
|
||||
val id = engine.requests.single().first
|
||||
writeFile(1, "s1", "half")
|
||||
|
||||
engine.fail(id)
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
assertEquals(DownloadStatus.Failed, row(1, "s1")?.status)
|
||||
assertEquals("Download failed", row(1, "s1")?.error)
|
||||
assertFalse(file(1, "s1").exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -291,7 +293,7 @@ class DownloadRepositoryTest {
|
||||
),
|
||||
)
|
||||
}
|
||||
await { sources.downloadUri("s1") != null }
|
||||
await { sources.downloadUri(1, "s1") != null }
|
||||
}
|
||||
|
||||
private fun file(
|
||||
|
||||
@@ -6,7 +6,10 @@ import androidx.media3.datasource.DataSpec
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.source.streamLengthSuffix
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -14,11 +17,14 @@ import java.io.File
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MediaDataSourceTest {
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
private fun dataSource() = mediaDataSourceFactory(context, OkHttpDataSource.Factory(OkHttpClient())).createDataSource()
|
||||
|
||||
@Test
|
||||
fun aDownloadedFileIsReadThroughTheStreamingChain() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val file = File(context.cacheDir, "downloaded-${System.nanoTime()}").apply { writeBytes(byteArrayOf(1, 2, 3, 4)) }
|
||||
val dataSource = mediaDataSourceFactory(context, OkHttpDataSource.Factory(OkHttpClient())).createDataSource()
|
||||
val dataSource = dataSource()
|
||||
|
||||
val length = dataSource.open(DataSpec(Uri.fromFile(file)))
|
||||
val buffer = ByteArray(4)
|
||||
@@ -29,4 +35,19 @@ class MediaDataSourceTest {
|
||||
assertEquals(4, read)
|
||||
assertEquals(listOf<Byte>(1, 2, 3, 4), buffer.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aStreamWithoutAContentLengthGetsItsDeclaredLength() {
|
||||
val server = MockWebServer()
|
||||
server.enqueue(MockResponse().setChunkedBody("0123456789", 1))
|
||||
server.start()
|
||||
val url = "http://${server.hostName}:${server.port}/rest/stream" + streamLengthSuffix(1_000_000)
|
||||
val dataSource = dataSource()
|
||||
|
||||
val length = dataSource.open(DataSpec(Uri.parse(url)))
|
||||
dataSource.close()
|
||||
server.shutdown()
|
||||
|
||||
assertEquals(1_000_000L, length)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user