v5
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 5m40s

This commit is contained in:
2026-08-11 22:15:12 +03:00
parent 7c63edc664
commit bbccd48bd6
2 changed files with 95 additions and 43 deletions
+92 -22
View File
@@ -1,5 +1,5 @@
diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/details/ui/pager/pages/MangaPageFetcher.kt b/app/src/main/kotlin/io/github/landwarderer/futon/details/ui/pager/pages/MangaPageFetcher.kt
index ae55a0f..ddadcc1 100644
index ae55a0f..9f4338b 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/details/ui/pager/pages/MangaPageFetcher.kt
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/details/ui/pager/pages/MangaPageFetcher.kt
@@ -15,21 +15,26 @@ import coil3.request.Options
@@ -29,7 +29,19 @@ index ae55a0f..ddadcc1 100644
import io.github.landwarderer.futon.reader.domain.PageLoader
import javax.inject.Inject
@@ -72,6 +77,12 @@ class MangaPageFetcher(
@@ -43,6 +48,11 @@ class MangaPageFetcher(
private val imageLoader: ImageLoader,
) : Fetcher {
+ private val niaddOkHttpClient = okHttpClient.newBuilder()
+ .followRedirects(false)
+ .followSslRedirects(false)
+ .build()
+
override suspend fun fetch(): FetchResult? {
if (!page.preview.isNullOrEmpty()) {
runCatchingCancellable {
@@ -72,6 +82,12 @@ class MangaPageFetcher(
}
private suspend fun fetchPage(pageUrl: String): FetchResult {
@@ -42,7 +54,7 @@ index ae55a0f..ddadcc1 100644
val request = PageLoader.createPageRequest(pageUrl, page.source)
return imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
if (!response.isSuccessful) {
@@ -89,6 +100,73 @@ class MangaPageFetcher(
@@ -89,6 +105,99 @@ class MangaPageFetcher(
}
}
@@ -64,18 +76,40 @@ index ae55a0f..ddadcc1 100644
+ .header("Connection", "close")
+ .build()
+
+ okHttpClient.newCall(request).execute().use { response ->
+ niaddOkHttpClient.newCall(request).execute().use { response ->
+ check(response.code == 206) {
+ "Niadd CDN ignored range request: HTTP ${response.code} for $pageUrl"
+ val location = response.header("Location")
+ "Niadd CDN range request returned HTTP ${response.code}" +
+ if (location.isNullOrBlank()) {
+ " for $pageUrl"
+ } else {
+ ", Location=$location for $pageUrl"
+ }
+ }
+
+ val contentType = response.header("Content-Type")
+ ?.substringBefore(';')
+ ?.trim()
+ .orEmpty()
+ check(contentType.startsWith("image/", ignoreCase = true)) {
+ "Niadd CDN returned non-image Content-Type '$contentType' for $pageUrl"
+ }
+ if (pageUrl.substringBefore('?').endsWith(".webp", ignoreCase = true)) {
+ check(contentType.equals("image/webp", ignoreCase = true)) {
+ "Niadd CDN replaced WebP with '$contentType' 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 }
+ val rangeMatch = NIADD_CONTENT_RANGE_REGEX.matchEntire(contentRange)
+ ?: error("Invalid Niadd Content-Range: $contentRange")
+ val responseStart = rangeMatch.groupValues[1].toLong()
+ val responseEnd = rangeMatch.groupValues[2].toLong()
+ val responseTotal = rangeMatch.groupValues[3].toLong()
+ check(responseTotal > 0L && responseStart == offset) {
+ "Unexpected Niadd Content-Range '$contentRange' for offset $offset ($pageUrl)"
+ }
+
+ if (totalBytes < 0L) {
+ totalBytes = responseTotal
@@ -85,11 +119,15 @@ index ae55a0f..ddadcc1 100644
+ }
+ }
+
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
+ check(responseEnd == expectedEnd) {
+ "Unexpected Niadd range end: expected $expectedEnd, got $responseEnd " +
+ "($contentRange, $pageUrl)"
+ }
+ if (mimeType == null) {
+ mimeType = response.mimeType?.toMimeTypeOrNull()
+ }
+
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
+ val expectedBytes = expectedEnd - offset + 1L
+ val receivedBytes = buffer.writeAll(response.requireBody().source())
+ check(receivedBytes == expectedBytes) {
@@ -116,12 +154,13 @@ index ae55a0f..ddadcc1 100644
private fun Response.toNetworkResponse() = NetworkResponse(
code = code,
requestMillis = sentRequestAtMillis,
@@ -106,6 +184,11 @@ class MangaPageFetcher(
@@ -106,6 +215,12 @@ class MangaPageFetcher(
return headers.build()
}
+ companion object {
+ private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L
+ private val NIADD_CONTENT_RANGE_REGEX = Regex("""bytes (\d+)-(\d+)/(\d+)""")
+ private val niaddSemaphore = Semaphore(1)
+ }
+
@@ -129,7 +168,7 @@ index ae55a0f..ddadcc1 100644
@MangaHttpClient private val okHttpClient: OkHttpClient,
@PageCache private val pagesCache: LocalStorageCache,
diff --git 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
index 18d203d..6109efa 100644
index 18d203d..02b1653 100644
--- 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.prefs.AppSettings
@@ -158,15 +197,19 @@ index 18d203d..6109efa 100644
import okio.use
import org.jetbrains.annotations.Blocking
import java.io.File
@@ -97,6 +102,7 @@ class PageLoader @Inject constructor(
@@ -97,6 +102,11 @@ class PageLoader @Inject constructor(
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
private val semaphore = Semaphore(3)
+ private val niaddSemaphore = Semaphore(1)
+ private val niaddOkHttp = okHttp.newBuilder()
+ .followRedirects(false)
+ .followSslRedirects(false)
+ .build()
private val convertLock = Mutex()
private val prefetchLock = Mutex()
@@ -303,13 +309,100 @@ class PageLoader @Inject constructor(
@@ -303,13 +313,126 @@ class PageLoader @Inject constructor(
if (isPrefetch) {
downloadSlowdownDispatcher.delay(page.source)
}
@@ -219,18 +262,40 @@ index 18d203d..6109efa 100644
+ .header("Connection", "close")
+ .build()
+
+ okHttp.newCall(request).execute().use { response ->
+ niaddOkHttp.newCall(request).execute().use { response ->
+ check(response.code == 206) {
+ "Niadd CDN ignored range request: HTTP ${response.code} for $pageUrl"
+ val location = response.header("Location")
+ "Niadd CDN range request returned HTTP ${response.code}" +
+ if (location.isNullOrBlank()) {
+ " for $pageUrl"
+ } else {
+ ", Location=$location for $pageUrl"
+ }
+ }
+
+ val contentType = response.header("Content-Type")
+ ?.substringBefore(';')
+ ?.trim()
+ .orEmpty()
+ check(contentType.startsWith("image/", ignoreCase = true)) {
+ "Niadd CDN returned non-image Content-Type '$contentType' for $pageUrl"
+ }
+ if (pageUrl.substringBefore('?').endsWith(".webp", ignoreCase = true)) {
+ check(contentType.equals("image/webp", ignoreCase = true)) {
+ "Niadd CDN replaced WebP with '$contentType' 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 }
+ val rangeMatch = NIADD_CONTENT_RANGE_REGEX.matchEntire(contentRange)
+ ?: error("Invalid Niadd Content-Range: $contentRange")
+ val responseStart = rangeMatch.groupValues[1].toLong()
+ val responseEnd = rangeMatch.groupValues[2].toLong()
+ val responseTotal = rangeMatch.groupValues[3].toLong()
+ check(responseTotal > 0L && responseStart == offset) {
+ "Unexpected Niadd Content-Range '$contentRange' for offset $offset ($pageUrl)"
+ }
+
+ if (totalBytes < 0L) {
+ totalBytes = responseTotal
@@ -240,12 +305,16 @@ index 18d203d..6109efa 100644
+ }
+ }
+
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
+ check(responseEnd == expectedEnd) {
+ "Unexpected Niadd range end: expected $expectedEnd, got $responseEnd " +
+ "($contentRange, $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) {
@@ -272,11 +341,12 @@ index 18d203d..6109efa 100644
}
}
@@ -340,6 +433,7 @@ class PageLoader @Inject constructor(
@@ -340,6 +463,8 @@ class PageLoader @Inject constructor(
companion object {
private const val PROGRESS_UNDEFINED = -1f
+ private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L
+ private val NIADD_CONTENT_RANGE_REGEX = Regex("""bytes (\d+)-(\d+)/(\d+)""")
private const val PREFETCH_LIMIT_DEFAULT = 6
private const val PREFETCH_MIN_RAM_MB = 80L
+3 -21
View File
@@ -767,27 +767,9 @@ replace_exact(
)
}
/*
* Fallback in case Niadd removes the "N of M"
* image link but leaves the actual image element.
*/
val imageUrlRegex = Regex(
"""(?i)\\.(?:jpe?g|png|gif|webp|avif)(?:\\?|$)"""
)
val image = root
.select("img[src]")
.firstOrNull { element ->
val src = element.attr("src")
imageUrlRegex.containsMatchIn(src) &&
!src.contains("logo", ignoreCase = true) &&
!src.contains("avatar", ignoreCase = true)
}
if (image != null) {
return image.attrAsAbsoluteUrl("src")
}
// Do not fall back to an arbitrary <img>. Niadd may return a
// service/notice page with a perfectly valid static image, which
// would otherwise be cached as every manga page.
error(
"Cannot find Niadd reader image for ${page.url}"