This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- ".gitea/workflows/build.yml"
|
||||
- "scripts/**"
|
||||
- "patches/**"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -57,6 +58,7 @@ jobs:
|
||||
|
||||
test -f scripts/patch_futon_niadd.py
|
||||
test -f scripts/patch_ninemanga_niadd.py
|
||||
test -f patches/futon-niadd-range-download.patch
|
||||
|
||||
python3 -m py_compile \
|
||||
scripts/patch_futon_niadd.py \
|
||||
@@ -148,6 +150,32 @@ jobs:
|
||||
|
||||
git -C work/Futon diff --check
|
||||
|
||||
- name: Apply Niadd range-download workaround
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
patch --dry-run -p1 \
|
||||
-d work/Futon \
|
||||
< patches/futon-niadd-range-download.patch
|
||||
|
||||
patch -p1 \
|
||||
-d work/Futon \
|
||||
< patches/futon-niadd-range-download.patch
|
||||
|
||||
PAGE_LOADER="work/Futon/app/src/main/kotlin/io/github/landwarderer/futon/reader/domain/PageLoader.kt"
|
||||
|
||||
grep -F 'NIADD_RANGE_CHUNK_SIZE = 16L * 1024L' \
|
||||
"${PAGE_LOADER}"
|
||||
|
||||
grep -F 'MangaParserSource.NINEMANGA_RU' \
|
||||
"${PAGE_LOADER}"
|
||||
|
||||
grep -F '.header("Range", "bytes=$offset-$requestedEnd")' \
|
||||
"${PAGE_LOADER}"
|
||||
|
||||
git -C work/Futon diff --check
|
||||
|
||||
- name: Apply Niadd parser modifications
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
--- a/app/src/main/kotlin/io/github/landwarderer/futon/reader/domain/PageLoader.kt
|
||||
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/reader/domain/PageLoader.kt
|
||||
@@ -30,6 +30,7 @@
|
||||
import io.github.landwarderer.futon.core.ui.image.TrimTransformation
|
||||
import io.github.landwarderer.futon.core.util.FileSize
|
||||
import io.github.landwarderer.futon.core.util.MimeTypes
|
||||
+import io.github.landwarderer.futon.core.util.ext.MimeType
|
||||
import io.github.landwarderer.futon.core.util.ext.URI_SCHEME_ZIP
|
||||
import io.github.landwarderer.futon.core.util.ext.cancelChildrenAndJoin
|
||||
import io.github.landwarderer.futon.core.util.ext.compressToPNG
|
||||
@@ -52,6 +53,7 @@
|
||||
import io.github.landwarderer.futon.local.data.LocalStorageCache
|
||||
import io.github.landwarderer.futon.local.data.PageCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
@@ -70,6 +72,9 @@
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
+import okio.buffer
|
||||
+import okio.sink
|
||||
+import okio.source
|
||||
import okio.use
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import java.io.File
|
||||
@@ -97,6 +102,7 @@
|
||||
|
||||
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
|
||||
private val semaphore = Semaphore(3)
|
||||
+ private val niaddSemaphore = Semaphore(1)
|
||||
private val convertLock = Mutex()
|
||||
private val prefetchLock = Mutex()
|
||||
|
||||
@@ -303,13 +309,100 @@
|
||||
if (isPrefetch) {
|
||||
downloadSlowdownDispatcher.delay(page.source)
|
||||
}
|
||||
- val request = createPageRequest(pageUrl, page.source)
|
||||
- imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||
- response.requireBody().withProgress(progress).use {
|
||||
- cache.set(pageUrl, it.source(), it.contentType()?.toMimeType())
|
||||
+ if (page.source == MangaParserSource.NINEMANGA_RU) {
|
||||
+ niaddSemaphore.withPermit {
|
||||
+ downloadNiaddPageByRanges(pageUrl, page.source, progress)
|
||||
}
|
||||
- }.toUri()
|
||||
+ } else {
|
||||
+ val request = createPageRequest(pageUrl, page.source)
|
||||
+ imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||
+ response.requireBody().withProgress(progress).use {
|
||||
+ cache.set(pageUrl, it.source(), it.contentType()?.toMimeType())
|
||||
+ }
|
||||
+ }.toUri()
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private suspend fun downloadNiaddPageByRanges(
|
||||
+ pageUrl: String,
|
||||
+ source: MangaSource,
|
||||
+ progress: MutableStateFlow<Float>,
|
||||
+ ): Uri {
|
||||
+ val tempFile = File.createTempFile(
|
||||
+ prefix = "niadd-page-",
|
||||
+ suffix = ".part",
|
||||
+ directory = context.cacheDir,
|
||||
+ )
|
||||
+ var mimeType: MimeType? = null
|
||||
+ try {
|
||||
+ var offset = 0L
|
||||
+ var totalBytes = -1L
|
||||
+
|
||||
+ tempFile.sink(append = false).buffer().use { sink ->
|
||||
+ while (totalBytes < 0L || offset < totalBytes) {
|
||||
+ val requestedEnd = if (totalBytes > 0L) {
|
||||
+ minOf(offset + NIADD_RANGE_CHUNK_SIZE - 1L, totalBytes - 1L)
|
||||
+ } else {
|
||||
+ offset + NIADD_RANGE_CHUNK_SIZE - 1L
|
||||
+ }
|
||||
+ val request = createPageRequest(pageUrl, source)
|
||||
+ .newBuilder()
|
||||
+ .header("Range", "bytes=$offset-$requestedEnd")
|
||||
+ .header("Connection", "close")
|
||||
+ .build()
|
||||
+
|
||||
+ okHttp.newCall(request).execute().use { response ->
|
||||
+ check(response.code == 206) {
|
||||
+ "Niadd CDN ignored range request: HTTP ${response.code} for $pageUrl"
|
||||
+ }
|
||||
+
|
||||
+ val contentRange = response.header("Content-Range")
|
||||
+ ?: error("Niadd CDN response has no Content-Range for $pageUrl")
|
||||
+ val responseTotal = contentRange
|
||||
+ .substringAfterLast('/', "")
|
||||
+ .toLongOrNull()
|
||||
+ ?.takeIf { it > 0L }
|
||||
+ ?: error("Invalid Niadd Content-Range: $contentRange")
|
||||
+
|
||||
+ if (totalBytes < 0L) {
|
||||
+ totalBytes = responseTotal
|
||||
+ } else {
|
||||
+ check(totalBytes == responseTotal) {
|
||||
+ "Niadd CDN changed content size from $totalBytes to $responseTotal for $pageUrl"
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ val body = response.requireBody()
|
||||
+ if (mimeType == null) {
|
||||
+ mimeType = body.contentType()?.toMimeType()
|
||||
+ }
|
||||
+
|
||||
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
|
||||
+ val expectedBytes = expectedEnd - offset + 1L
|
||||
+ val receivedBytes = sink.writeAll(body.source())
|
||||
+ check(receivedBytes == expectedBytes) {
|
||||
+ "Short Niadd range: expected $expectedBytes bytes, got $receivedBytes " +
|
||||
+ "for bytes=$offset-$expectedEnd ($pageUrl)"
|
||||
+ }
|
||||
+
|
||||
+ offset += receivedBytes
|
||||
+ progress.value = (offset.toFloat() / totalBytes.toFloat())
|
||||
+ .coerceIn(0f, 1f)
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ check(totalBytes > 0L && tempFile.length() == totalBytes) {
|
||||
+ "Incomplete Niadd image: expected $totalBytes bytes, got ${tempFile.length()} for $pageUrl"
|
||||
+ }
|
||||
+
|
||||
+ return tempFile.source().use { sourceStream ->
|
||||
+ cache.set(pageUrl, sourceStream, mimeType).toUri()
|
||||
}
|
||||
+ } finally {
|
||||
+ tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +433,7 @@
|
||||
companion object {
|
||||
|
||||
private const val PROGRESS_UNDEFINED = -1f
|
||||
+ private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L
|
||||
private const val PREFETCH_LIMIT_DEFAULT = 6
|
||||
private const val PREFETCH_MIN_RAM_MB = 80L
|
||||
|
||||
Reference in New Issue
Block a user