mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
fail a download whose payload is a server error page
A Subsonic server answers a refused download with HTTP 200 and an XML error
document, which the platform downloader saves like any payload, so the row went
Completed and playback then tried to decode the error page. No audio container
starts with markup, so the leading bytes tell them apart: a Completed download
that starts with < or { is failed instead, and a stored Completed row is
re-checked on reconcile so existing bad files get cleaned up too.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package com.subtracks.data.download
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
class AudioFileTest {
|
||||
@Test
|
||||
fun anXmlErrorDocumentIsNotAudio() {
|
||||
assertFalse(file("<?xml version=\"1.0\"?><subsonic-response status=\"failed\"/>").looksLikeAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun leadingWhitespaceAndABomDoNotHideMarkup() {
|
||||
assertFalse(file("\uFEFF\n <html><body>Gateway timeout</body></html>").looksLikeAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aJsonErrorBodyIsNotAudio() {
|
||||
assertFalse(file("{\"subsonic-response\":{\"status\":\"failed\"}}").looksLikeAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEmptyFileIsNotAudio() {
|
||||
assertFalse(file("").looksLikeAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMissingFileIsNotAudio() {
|
||||
assertFalse(File.createTempFile("absent", ".tmp").apply { delete() }.looksLikeAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun audioHeadersPass() {
|
||||
assertTrue(file("audio").looksLikeAudio())
|
||||
assertTrue(file("ID3\u0004\u0000audio").looksLikeAudio())
|
||||
assertTrue(file("fLaC\u0000\u0000\u0000\u0022").looksLikeAudio())
|
||||
}
|
||||
|
||||
private fun file(contents: String): File = File.createTempFile("audio", ".tmp").apply { writeText(contents) }.also { it.deleteOnExit() }
|
||||
}
|
||||
@@ -920,6 +920,42 @@ class DownloadRepositoryTest {
|
||||
await { row(2, "o1")?.status == DownloadStatus.Completed }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aServerErrorSavedAsThePayloadIsNotTreatedAsADownload() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
repository.start()
|
||||
runBlocking { repository.download(1, "s1") }
|
||||
val id = engine.requests.single().first
|
||||
|
||||
engine.complete(id, total = 100)
|
||||
writeFile(
|
||||
1,
|
||||
"s1",
|
||||
"<?xml version=\"1.0\"?><subsonic-response status=\"failed\"><error code=\"70\"/></subsonic-response>",
|
||||
)
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
assertEquals(DownloadStatus.Failed, row(1, "s1")?.status)
|
||||
assertEquals("The server sent an error instead of the track", row(1, "s1")?.error)
|
||||
assertFalse(file(1, "s1").exists())
|
||||
assertNull(repository.localUri("s1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aCompletedRowLeftAsAnErrorPageIsFailedOnReconcile() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking { db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed, bytes = 10, total = 10)) }
|
||||
writeFile(1, "s1", "<subsonic-response status=\"failed\"/>")
|
||||
|
||||
runBlocking { repository.reconcile() }
|
||||
|
||||
assertEquals(DownloadStatus.Failed, row(1, "s1")?.status)
|
||||
assertFalse(file(1, "s1").exists())
|
||||
assertNull(repository.localUri("s1"))
|
||||
}
|
||||
|
||||
private fun requestedSongIds(): List<String> = engine.requests.map { it.second.path.substringAfterLast('/') }
|
||||
|
||||
private fun seedSongs(tracks: IntRange) {
|
||||
|
||||
Reference in New Issue
Block a user