Support resumable image downloads if supported by source (#3167)
Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- `Other` - for technical stuff.
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
||||
|
||||
## [v0.20.0] - 2026-06-27
|
||||
### Added
|
||||
|
||||
@@ -362,11 +362,6 @@ class Downloader(
|
||||
reIndexedPages
|
||||
}
|
||||
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.extension == "tmp" }
|
||||
?.forEach { it.delete() }
|
||||
|
||||
download.status = Download.State.DOWNLOADING
|
||||
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
@@ -441,14 +436,12 @@ class Downloader(
|
||||
|
||||
val digitCount = (download.pages?.size ?: 0).toString().length.coerceAtLeast(3)
|
||||
val filename = "%0${digitCount}d".format(Locale.ENGLISH, page.number)
|
||||
val tmpFile = tmpDir.findFile("$filename.tmp")
|
||||
|
||||
// Delete temp file if it exists
|
||||
tmpFile?.delete()
|
||||
|
||||
// Try to find the image file
|
||||
val imageFile = tmpDir.listFiles()?.firstOrNull {
|
||||
it.name!!.startsWith("$filename.") || it.name!!.startsWith("${filename}__001")
|
||||
val filename = it.name
|
||||
if (filename == null || filename.endsWith(".tmp")) return@firstOrNull false
|
||||
filename.startsWith("$filename.") || filename.startsWith("${filename}__001")
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -488,15 +481,27 @@ class Downloader(
|
||||
page.status = Page.State.DownloadImage
|
||||
page.progress = 0
|
||||
return flow {
|
||||
val response = source.getImage(page)
|
||||
val file = tmpDir.createFile("$filename.tmp")!!
|
||||
val file = tmpDir.findFile("$filename.tmp")
|
||||
?: tmpDir.createFile("$filename.tmp")!!
|
||||
|
||||
val response = source.getImage(page, file.length())
|
||||
|
||||
try {
|
||||
response.body.source().saveTo(file.openOutputStream())
|
||||
response.body
|
||||
.source()
|
||||
.saveTo(
|
||||
// If the server supports partial downloads (HTTP 206),
|
||||
// append to the existing file.
|
||||
// Otherwise, start from scratch and overwrite the file.
|
||||
file.openOutputStream(response.code == 206),
|
||||
)
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
response.close()
|
||||
file.delete()
|
||||
if (response.code == 416) {
|
||||
file.delete()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
emit(file)
|
||||
@@ -521,6 +526,8 @@ class Downloader(
|
||||
* @param filename the filename of the image.
|
||||
*/
|
||||
private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile {
|
||||
// Delete temp file if it exists
|
||||
tmpDir.findFile("$filename.tmp")?.delete()
|
||||
val tmpFile = tmpDir.createFile("$filename.tmp")!!
|
||||
cacheFile.inputStream().use { input ->
|
||||
tmpFile.openOutputStream().use { output ->
|
||||
|
||||
@@ -118,13 +118,26 @@ suspend fun Call.awaitSuccess(): Response {
|
||||
return response
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
fun OkHttpClient.newCachelessCallWithProgress(
|
||||
request: Request,
|
||||
listener: ProgressListener,
|
||||
existingSize: Long = 0L,
|
||||
): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
val request = chain.request()
|
||||
.newBuilder()
|
||||
.apply {
|
||||
if (existingSize > 0 && request.header("Range") == null) {
|
||||
header("Range", "bytes=$existingSize-")
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
val originalResponse = chain.proceed(request)
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body, listener))
|
||||
.body(ProgressResponseBody(originalResponse.body, listener, existingSize))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.io.IOException
|
||||
class ProgressResponseBody(
|
||||
private val responseBody: ResponseBody,
|
||||
private val progressListener: ProgressListener,
|
||||
private val existingSize: Long, // bytes already downloaded
|
||||
) : ResponseBody() {
|
||||
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
@@ -32,7 +33,7 @@ class ProgressResponseBody(
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
var totalBytesRead = 0L
|
||||
var totalBytesRead = existingSize
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
|
||||
@@ -404,8 +404,8 @@ abstract class HttpSource : CatalogueSource {
|
||||
)
|
||||
protected open fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
suspend fun getImage(page: Page): Response {
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
suspend fun getImage(page: Page, existingSize: Long = 0L): Response {
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page, existingSize)
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user