2 Commits
Author SHA1 Message Date
gandc cdb84a2793 v5.2
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 3m32s
2026-08-11 23:19:21 +03:00
gandc 15c05f6bcd Fix Niadd range downloads in DownloadWorker
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 3m49s
2026-08-11 23:00:35 +03:00
+169 -4
View File
@@ -73,7 +73,7 @@ index ae55a0f..9f4338b 100644
+ val request = PageLoader.createPageRequest(pageUrl, page.source) + val request = PageLoader.createPageRequest(pageUrl, page.source)
+ .newBuilder() + .newBuilder()
+ .header("Range", "bytes=$offset-$requestedEnd") + .header("Range", "bytes=$offset-$requestedEnd")
+ .header("Connection", "close") + .header("Connection", "keep-alive")
+ .build() + .build()
+ +
+ niaddOkHttpClient.newCall(request).execute().use { response -> + niaddOkHttpClient.newCall(request).execute().use { response ->
@@ -161,12 +161,177 @@ index ae55a0f..9f4338b 100644
+ companion object { + companion object {
+ private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L + private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L
+ private val NIADD_CONTENT_RANGE_REGEX = Regex("""bytes (\d+)-(\d+)/(\d+)""") + private val NIADD_CONTENT_RANGE_REGEX = Regex("""bytes (\d+)-(\d+)/(\d+)""")
+ private val niaddSemaphore = Semaphore(1) + private val niaddSemaphore = Semaphore(2)
+ } + }
+ +
class Factory @Inject constructor( class Factory @Inject constructor(
@MangaHttpClient private val okHttpClient: OkHttpClient, @MangaHttpClient private val okHttpClient: OkHttpClient,
@PageCache private val pagesCache: LocalStorageCache, @PageCache private val pagesCache: LocalStorageCache,
diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/worker/DownloadWorker.kt
index 8227b2a..742341e 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/worker/DownloadWorker.kt
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/download/ui/worker/DownloadWorker.kt
@@ -91,6 +91,7 @@ import okio.use
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -125,6 +126,10 @@ class DownloadWorker @AssistedInject constructor(
private val notificationFactory = notificationFactoryFactory.create(uuid = params.id, isSilent = task.isSilent)
private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val bandwidthLimiter = BandwidthLimiter { settings.downloadBandwidthLimit * 1024 }
+ private val niaddOkHttp = okHttp.newBuilder()
+ .followRedirects(false)
+ .followSslRedirects(false)
+ .build()
@Volatile
private var lastPublishedState: DownloadState? = null
@@ -427,6 +432,13 @@ class DownloadWorker @AssistedInject constructor(
}
return file
}
+ if (isNiaddRangeDownload(url, source)) {
+ slowdownDispatcher.delay(source)
+ return NIADD_DOWNLOAD_SEMAPHORE.withPermit {
+ downloadNiaddFileByRanges(url, destination, source)
+ }
+ }
+
val request = PageLoader.createPageRequest(url, source)
slowdownDispatcher.delay(source)
return imageProxyInterceptor.interceptPageRequest(request, okHttp)
@@ -450,6 +462,117 @@ class DownloadWorker @AssistedInject constructor(
}
}
+ private fun isNiaddRangeDownload(url: String, source: MangaSource): Boolean {
+ return source == MangaParserSource.NINEMANGA_RU &&
+ url.substringBefore('?').endsWith(".webp", ignoreCase = true) &&
+ url.contains("acc=", ignoreCase = true) &&
+ url.contains("exp=", ignoreCase = true)
+ }
+
+ private suspend fun downloadNiaddFileByRanges(
+ url: String,
+ destination: File,
+ source: MangaSource,
+ ): File {
+ Log.d("DownloadWorker", "Using Niadd range download for $url")
+ var file: File? = null
+ var offset = 0L
+ var totalBytes = -1L
+
+ try {
+ 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 = PageLoader.createPageRequest(url, source)
+ .newBuilder()
+ .header("Range", "bytes=$offset-$requestedEnd")
+ .header("Connection", "keep-alive")
+ .build()
+
+ niaddOkHttp.newCall(request).execute().use { response ->
+ check(response.code == 206) {
+ val location = response.header("Location")
+ "Niadd CDN range request returned HTTP ${response.code}" +
+ if (location.isNullOrBlank()) {
+ " for $url"
+ } else {
+ ", Location=$location for $url"
+ }
+ }
+
+ val contentType = response.header("Content-Type")
+ ?.substringBefore(';')
+ ?.trim()
+ .orEmpty()
+ check(contentType.equals("image/webp", ignoreCase = true)) {
+ "Niadd CDN returned '$contentType' instead of image/webp for $url"
+ }
+
+ val contentRange = response.header("Content-Range")
+ ?: error("Niadd CDN response has no Content-Range for $url")
+ 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 ($url)"
+ }
+
+ if (totalBytes < 0L) {
+ totalBytes = responseTotal
+ } else {
+ check(totalBytes == responseTotal) {
+ "Niadd CDN changed content size from $totalBytes to $responseTotal for $url"
+ }
+ }
+
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
+ check(responseEnd == expectedEnd) {
+ "Unexpected Niadd range end: expected $expectedEnd, got $responseEnd " +
+ "($contentRange, $url)"
+ }
+
+ val body = response.requireBody()
+ if (file == null) {
+ file = destination.createTempFile(
+ ext = MimeTypes.getExtension(body.contentType()?.toMimeType()),
+ )
+ }
+ val outputFile = checkNotNull(file)
+ check(outputFile.length() == offset) {
+ "Niadd temporary file offset mismatch: expected $offset, got ${outputFile.length()} for $url"
+ }
+
+ val expectedBytes = expectedEnd - offset + 1L
+ outputFile.sink(append = offset > 0L).buffer().use { sink ->
+ sink.writeAllCancellable(body.source(), bandwidthLimiter)
+ }
+ val newLength = outputFile.length()
+ val receivedBytes = newLength - offset
+ check(receivedBytes == expectedBytes) {
+ "Short Niadd range: expected $expectedBytes bytes, got $receivedBytes " +
+ "for bytes=$offset-$expectedEnd ($url)"
+ }
+
+ offset = newLength
+ }
+ }
+
+ val result = checkNotNull(file)
+ check(totalBytes > 0L && result.length() == totalBytes) {
+ "Incomplete Niadd image: expected $totalBytes bytes, got ${result.length()} for $url"
+ }
+ return result
+ } catch (e: Exception) {
+ file?.delete()
+ throw e
+ }
+ }
+
private fun File.createTempFile(ext: String?) = File(
this,
buildString {
@@ -633,6 +756,9 @@ class DownloadWorker @AssistedInject constructor(
const val MAX_FAILSAFE_ATTEMPTS = 2
const val MAX_PAGES_PARALLELISM = 4
+ private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L
+ private val NIADD_CONTENT_RANGE_REGEX = Regex("""bytes (\d+)-(\d+)/(\d+)""")
+ private val NIADD_DOWNLOAD_SEMAPHORE = Semaphore(2)
const val DOWNLOAD_ERROR_DELAY = 2_000L
const val MAX_RETRY_DELAY = 7_200_000L // 2 hours
const val TAG = "download"
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 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..02b1653 100644 index 18d203d..02b1653 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/reader/domain/PageLoader.kt --- a/app/src/main/kotlin/io/github/landwarderer/futon/reader/domain/PageLoader.kt
@@ -201,7 +366,7 @@ index 18d203d..02b1653 100644
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>() private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
private val semaphore = Semaphore(3) private val semaphore = Semaphore(3)
+ private val niaddSemaphore = Semaphore(1) + private val niaddSemaphore = Semaphore(2)
+ private val niaddOkHttp = okHttp.newBuilder() + private val niaddOkHttp = okHttp.newBuilder()
+ .followRedirects(false) + .followRedirects(false)
+ .followSslRedirects(false) + .followSslRedirects(false)
@@ -259,7 +424,7 @@ index 18d203d..02b1653 100644
+ val request = createPageRequest(pageUrl, source) + val request = createPageRequest(pageUrl, source)
+ .newBuilder() + .newBuilder()
+ .header("Range", "bytes=$offset-$requestedEnd") + .header("Range", "bytes=$offset-$requestedEnd")
+ .header("Connection", "close") + .header("Connection", "keep-alive")
+ .build() + .build()
+ +
+ niaddOkHttp.newCall(request).execute().use { response -> + niaddOkHttp.newCall(request).execute().use { response ->