7 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
gandc 9ba7eed81a v5.1
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 5m24s
2026-08-11 22:38:20 +03:00
gandc bbccd48bd6 v5
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 5m40s
2026-08-11 22:15:12 +03:00
gandc 7c63edc664 fix patch
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 3m54s
2026-08-10 01:51:37 +03:00
gandc 83e7c698e1 fix SocketTimeoutException
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Failing after 26s
2026-08-10 01:48:15 +03:00
gandc d5f6d20e1f fix
Build Futon with Niadd patch / Build Futon Niadd debug APK (push) Successful in 4m3s
2026-08-10 01:35:30 +03:00
2 changed files with 450 additions and 112 deletions

No files matched your search

+385 -18
View File
@@ -1,6 +1,342 @@
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..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
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Response
+import okio.Buffer
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import io.github.landwarderer.futon.core.network.MangaHttpClient
import io.github.landwarderer.futon.core.network.imageproxy.ImageProxyInterceptor
import io.github.landwarderer.futon.core.parser.MangaRepository
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.fetch
import io.github.landwarderer.futon.core.util.ext.isNetworkUri
import io.github.landwarderer.futon.core.util.ext.toMimeTypeOrNull
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.util.mimeType
import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withPermit
import io.github.landwarderer.futon.reader.domain.PageLoader
import javax.inject.Inject
@@ -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 {
+ if (page.source == MangaParserSource.NINEMANGA_RU) {
+ return niaddSemaphore.withPermit {
+ fetchNiaddPageByRanges(pageUrl)
+ }
+ }
+
val request = PageLoader.createPageRequest(pageUrl, page.source)
return imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
if (!response.isSuccessful) {
@@ -89,6 +105,99 @@ class MangaPageFetcher(
}
}
+ private suspend fun fetchNiaddPageByRanges(pageUrl: String): FetchResult {
+ val buffer = Buffer()
+ var mimeType: MimeType? = null
+ var offset = 0L
+ var totalBytes = -1L
+
+ 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(pageUrl, page.source)
+ .newBuilder()
+ .header("Range", "bytes=$offset-$requestedEnd")
+ .header("Connection", "keep-alive")
+ .build()
+
+ niaddOkHttpClient.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 $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 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
+ } else {
+ check(totalBytes == responseTotal) {
+ "Niadd CDN changed content size from $totalBytes to $responseTotal for $pageUrl"
+ }
+ }
+
+ 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 expectedBytes = expectedEnd - offset + 1L
+ val receivedBytes = buffer.writeAll(response.requireBody().source())
+ check(receivedBytes == expectedBytes) {
+ "Short Niadd range: expected $expectedBytes bytes, got $receivedBytes " +
+ "for bytes=$offset-$expectedEnd ($pageUrl)"
+ }
+
+ offset += receivedBytes
+ }
+ }
+
+ check(totalBytes > 0L && buffer.size == totalBytes) {
+ "Incomplete Niadd image: expected $totalBytes bytes, got ${buffer.size} for $pageUrl"
+ }
+
+ val file = pagesCache.set(pageUrl, buffer, mimeType)
+ return SourceFetchResult(
+ source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
+ mimeType = mimeType?.toString(),
+ dataSource = DataSource.NETWORK,
+ )
+ }
+
private fun Response.toNetworkResponse() = NetworkResponse(
code = code,
requestMillis = sentRequestAtMillis,
@@ -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(2)
+ }
+
class Factory @Inject constructor(
@MangaHttpClient private val okHttpClient: OkHttpClient,
@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
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
+++ b/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 @@ @@ -30,6 +30,7 @@ import io.github.landwarderer.futon.core.prefs.AppSettings
import io.github.landwarderer.futon.core.ui.image.TrimTransformation import io.github.landwarderer.futon.core.ui.image.TrimTransformation
import io.github.landwarderer.futon.core.util.FileSize import io.github.landwarderer.futon.core.util.FileSize
import io.github.landwarderer.futon.core.util.MimeTypes import io.github.landwarderer.futon.core.util.MimeTypes
@@ -8,7 +344,7 @@
import io.github.landwarderer.futon.core.util.ext.URI_SCHEME_ZIP 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.cancelChildrenAndJoin
import io.github.landwarderer.futon.core.util.ext.compressToPNG import io.github.landwarderer.futon.core.util.ext.compressToPNG
@@ -52,6 +53,7 @@ @@ -52,6 +53,7 @@ import io.github.landwarderer.futon.download.ui.worker.DownloadSlowdownDispatche
import io.github.landwarderer.futon.local.data.LocalStorageCache import io.github.landwarderer.futon.local.data.LocalStorageCache
import io.github.landwarderer.futon.local.data.PageCache import io.github.landwarderer.futon.local.data.PageCache
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
@@ -16,7 +352,7 @@
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -70,6 +72,9 @@ @@ -70,6 +72,9 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -26,15 +362,19 @@
import okio.use import okio.use
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import java.io.File import java.io.File
@@ -97,6 +102,7 @@ @@ -97,6 +102,11 @@ class PageLoader @Inject constructor(
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()
+ .followRedirects(false)
+ .followSslRedirects(false)
+ .build()
private val convertLock = Mutex() private val convertLock = Mutex()
private val prefetchLock = Mutex() private val prefetchLock = Mutex()
@@ -303,13 +309,100 @@ @@ -303,13 +313,126 @@ class PageLoader @Inject constructor(
if (isPrefetch) { if (isPrefetch) {
downloadSlowdownDispatcher.delay(page.source) downloadSlowdownDispatcher.delay(page.source)
} }
@@ -65,9 +405,9 @@
+ progress: MutableStateFlow<Float>, + progress: MutableStateFlow<Float>,
+ ): Uri { + ): Uri {
+ val tempFile = File.createTempFile( + val tempFile = File.createTempFile(
+ prefix = "niadd-page-", + "niadd-page-",
+ suffix = ".part", + ".part",
+ directory = context.cacheDir, + context.cacheDir,
+ ) + )
+ var mimeType: MimeType? = null + var mimeType: MimeType? = null
+ try { + try {
@@ -84,21 +424,43 @@
+ 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()
+ +
+ okHttp.newCall(request).execute().use { response -> + niaddOkHttp.newCall(request).execute().use { response ->
+ check(response.code == 206) { + 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") + val contentRange = response.header("Content-Range")
+ ?: error("Niadd CDN response has no Content-Range for $pageUrl") + ?: error("Niadd CDN response has no Content-Range for $pageUrl")
+ val responseTotal = contentRange + val rangeMatch = NIADD_CONTENT_RANGE_REGEX.matchEntire(contentRange)
+ .substringAfterLast('/', "")
+ .toLongOrNull()
+ ?.takeIf { it > 0L }
+ ?: error("Invalid Niadd Content-Range: $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) { + if (totalBytes < 0L) {
+ totalBytes = responseTotal + totalBytes = responseTotal
@@ -108,12 +470,16 @@
+ } + }
+ } + }
+ +
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
+ check(responseEnd == expectedEnd) {
+ "Unexpected Niadd range end: expected $expectedEnd, got $responseEnd " +
+ "($contentRange, $pageUrl)"
+ }
+ val body = response.requireBody() + val body = response.requireBody()
+ if (mimeType == null) { + if (mimeType == null) {
+ mimeType = body.contentType()?.toMimeType() + mimeType = body.contentType()?.toMimeType()
+ } + }
+ +
+ val expectedEnd = minOf(requestedEnd, totalBytes - 1L)
+ val expectedBytes = expectedEnd - offset + 1L + val expectedBytes = expectedEnd - offset + 1L
+ val receivedBytes = sink.writeAll(body.source()) + val receivedBytes = sink.writeAll(body.source())
+ check(receivedBytes == expectedBytes) { + check(receivedBytes == expectedBytes) {
@@ -140,11 +506,12 @@
} }
} }
@@ -340,6 +433,7 @@ @@ -340,6 +463,8 @@ class PageLoader @Inject constructor(
companion object { companion object {
private const val PROGRESS_UNDEFINED = -1f private const val PROGRESS_UNDEFINED = -1f
+ 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 const val PREFETCH_LIMIT_DEFAULT = 6 private const val PREFETCH_LIMIT_DEFAULT = 6
private const val PREFETCH_MIN_RAM_MB = 80L private const val PREFETCH_MIN_RAM_MB = 80L
+65 -94
View File
@@ -425,38 +425,6 @@ replace_exact(
val dateText = anchorDate ?: rowDate val dateText = anchorDate ?: rowDate
val metadataText = when {
anchorDate != null -> anchorText
rowDate != null -> rowText
else -> ""
}
val textBeforeDate = if (dateText != null) {
metadataText
.substringBefore(dateText)
.trim()
} else {
metadataText
}
/*
* Niadd exposes the page count immediately before
* the chapter date, for example:
*
* Падение русала 1 - 42 41 Aug 02, 2024
* ^^
*
* Store that number in a URL fragment. Fragments
* are local metadata and are not sent to the server.
*/
val pageCount = Regex(
"""\\s+(\\d+)\\s*$"""
)
.find(textBeforeDate)
?.groupValues
?.getOrNull(1)
?.toIntOrNull()
val chapterTitle = if ( val chapterTitle = if (
dateText != null && dateText != null &&
rawTitle.contains(dateText) rawTitle.contains(dateText)
@@ -472,15 +440,6 @@ replace_exact(
rawTitle rawTitle
} }
val chapterUrl = if (
pageCount != null &&
pageCount > 0
) {
"$href#niadd-pages=$pageCount"
} else {
href
}
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
title = chapterTitle.takeIf { title = chapterTitle.takeIf {
@@ -488,7 +447,7 @@ replace_exact(
}, },
number = i + 1f, number = i + 1f,
volume = 0, volume = 0,
url = chapterUrl, url = href,
uploadDate = parseChapterDateByLang( uploadDate = parseChapterDateByLang(
dateText.orEmpty() dateText.orEmpty()
), ),
@@ -641,42 +600,78 @@ replace_exact(
''' override suspend fun getPages( ''' override suspend fun getPages(
chapter: MangaChapter, chapter: MangaChapter,
): List<MangaPage> { ): List<MangaPage> {
if (domain.endsWith("niadd.com")) { val chapterUrl = chapter.url
val originalUrl = chapter.url
.substringBefore("#") .substringBefore("#")
.substringBefore("?")
val totalPages = chapter.url val doc = webClient
.substringAfter( .httpGet(
"#niadd-pages=", chapterUrl.toAbsoluteUrl(domain)
"",
) )
.substringBefore("&") .parseHtml()
.toIntOrNull()
?.takeIf { it > 0 } if (domain.endsWith("niadd.com")) {
val pageLabelRegex = Regex(
"""^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$"""
)
val pageOptions = doc
.select("option[value]")
.mapNotNull { option ->
val match = pageLabelRegex.matchEntire(
option.text().trim()
) ?: return@mapNotNull null
val pageNumber = match
.groupValues
.getOrNull(1)
?.toIntOrNull()
?: return@mapNotNull null
val totalPages = match
.groupValues
.getOrNull(2)
?.toIntOrNull()
?: return@mapNotNull null
val url = option.attr("value").trim()
if (url.isEmpty()) {
return@mapNotNull null
}
Triple(pageNumber, totalPages, url)
}
.distinctBy { (pageNumber, _, _) ->
pageNumber
}
.sortedBy { (pageNumber, _, _) ->
pageNumber
}
val totalPages = pageOptions
.firstOrNull()
?.second
?: error( ?: error(
"Niadd page count is missing for ${chapter.url}. " + "Cannot find Niadd page selector for $chapterUrl"
"Refresh manga details and try again."
) )
val normalizedUrl = originalUrl check(
.removeSuffix("/") pageOptions.all { (_, total, _) ->
total == totalPages
val baseUrl = if ( }
normalizedUrl.endsWith(".html")
) { ) {
normalizedUrl.removeSuffix(".html") "Inconsistent Niadd page count for $chapterUrl"
} else {
normalizedUrl
} }
return (1..totalPages).map { pageNumber -> check(
val url = if (pageNumber == 1) { pageOptions.size == totalPages &&
originalUrl pageOptions.map { it.first } ==
} else { (1..totalPages).toList()
"$baseUrl-$pageNumber.html" ) {
"Incomplete Niadd page selector for $chapterUrl: " +
"found ${pageOptions.size} of $totalPages pages"
} }
return pageOptions.map { (_, _, url) ->
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
@@ -686,12 +681,6 @@ replace_exact(
} }
} }
val doc = webClient
.httpGet(
chapter.url.toAbsoluteUrl(domain)
)
.parseHtml()
return doc return doc
.body() .body()
.requireElementById("page") .requireElementById("page")
@@ -767,27 +756,9 @@ replace_exact(
) )
} }
/* // Do not fall back to an arbitrary <img>. Niadd may return a
* Fallback in case Niadd removes the "N of M" // service/notice page with a perfectly valid static image, which
* image link but leaves the actual image element. // would otherwise be cached as every manga page.
*/
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")
}
error( error(
"Cannot find Niadd reader image for ${page.url}" "Cannot find Niadd reader image for ${page.url}"