Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c63edc664
|
||
|
|
83e7c698e1
|
||
|
|
d5f6d20e1f
|
||
|
|
008379a09a
|
||
|
|
897874169d
|
||
|
|
ffeaa412e8
|
||
|
|
24d71d9407
|
||
|
|
5f6c8cf69e
|
||
|
|
9e6d724865
|
||
|
|
bdb8b68c3e
|
No files matched your search
+34
-22
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- ".gitea/workflows/build.yml"
|
||||
- "scripts/**"
|
||||
- "patches/**"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -22,39 +23,24 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Futon Niadd debug APK
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: android-futon
|
||||
|
||||
steps:
|
||||
- name: Checkout CI repository
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: https://github.com/actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: https://github.com/android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android SDK 36
|
||||
- name: Verify Android build environment
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
yes | sdkmanager --licenses >/dev/null 2>&1 || true
|
||||
|
||||
sdkmanager \
|
||||
"platform-tools" \
|
||||
"platforms;android-${ANDROID_PLATFORM}" \
|
||||
"build-tools;${ANDROID_BUILD_TOOLS}"
|
||||
|
||||
echo "=== Android SDK ==="
|
||||
echo "ANDROID_HOME=${ANDROID_HOME}"
|
||||
echo "ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT:-}"
|
||||
|
||||
echo "=== Installed build tools ==="
|
||||
ls -la "${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS}"
|
||||
test -x "${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS}/aapt"
|
||||
test -x "${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS}/aapt2"
|
||||
|
||||
echo "=== Java ==="
|
||||
java -version
|
||||
@@ -72,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 \
|
||||
@@ -163,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: |
|
||||
@@ -246,6 +259,7 @@ jobs:
|
||||
shell: bash
|
||||
working-directory: work/Futon
|
||||
env:
|
||||
GRADLE_USER_HOME: /root/.gradle
|
||||
GRADLE_OPTS: >-
|
||||
-Dorg.gradle.daemon=false
|
||||
-Dorg.gradle.jvmargs=-Xmx4g
|
||||
@@ -254,14 +268,12 @@ jobs:
|
||||
|
||||
chmod +x gradlew
|
||||
|
||||
echo "=== Gradle version ==="
|
||||
./gradlew --version
|
||||
|
||||
echo "=== Build ==="
|
||||
|
||||
./gradlew \
|
||||
--no-daemon \
|
||||
--stacktrace \
|
||||
--max-workers=6 \
|
||||
:app:assembleDebug
|
||||
|
||||
- name: Verify APK identity
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
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
|
||||
--- 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
|
||||
|
||||
@@ -72,6 +77,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 +100,73 @@ 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", "close")
|
||||
+ .build()
|
||||
+
|
||||
+ okHttpClient.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"
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ 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) {
|
||||
+ "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 +184,11 @@ class MangaPageFetcher(
|
||||
return headers.build()
|
||||
}
|
||||
|
||||
+ companion object {
|
||||
+ private const val NIADD_RANGE_CHUNK_SIZE = 16L * 1024L
|
||||
+ private val niaddSemaphore = Semaphore(1)
|
||||
+ }
|
||||
+
|
||||
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/reader/domain/PageLoader.kt b/app/src/main/kotlin/io/github/landwarderer/futon/reader/domain/PageLoader.kt
|
||||
index 18d203d..6109efa 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
|
||||
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.download.ui.worker.DownloadSlowdownDispatche
|
||||
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.withLock
|
||||
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 @@ class PageLoader @Inject constructor(
|
||||
|
||||
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 @@ class PageLoader @Inject constructor(
|
||||
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(
|
||||
+ "niadd-page-",
|
||||
+ ".part",
|
||||
+ 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 @@ class PageLoader @Inject constructor(
|
||||
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
|
||||
|
||||
@@ -5,7 +5,10 @@ import sys
|
||||
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: {sys.argv[0]} <kotatsu-parsers-redo-dir>", file=sys.stderr)
|
||||
print(
|
||||
f"Usage: {sys.argv[0]} <kotatsu-parsers-redo-dir>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
@@ -17,7 +20,10 @@ parser_file = (
|
||||
)
|
||||
|
||||
if not parser_file.is_file():
|
||||
print(f"ERROR: parser file not found: {parser_file}", file=sys.stderr)
|
||||
print(
|
||||
f"ERROR: parser file not found: {parser_file}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -31,7 +37,8 @@ def replace_exact(name: str, old: str, new: str) -> None:
|
||||
|
||||
if count != 1:
|
||||
print(
|
||||
f"ERROR: patch '{name}' expected exactly 1 match, found {count}",
|
||||
f"ERROR: patch '{name}' expected exactly 1 match, "
|
||||
f"found {count}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
@@ -41,9 +48,26 @@ def replace_exact(name: str, old: str, new: str) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
text = text.replace(old, new, 1)
|
||||
|
||||
print(f"[OK] {name}")
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. CATEGORY PAGINATION
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Old NineManga:
|
||||
#
|
||||
# /category/index_1
|
||||
# /category/index_2
|
||||
#
|
||||
# Current Niadd:
|
||||
#
|
||||
# /category/
|
||||
# /category/index_2.html
|
||||
# /category/index_3.html
|
||||
#
|
||||
replace_exact(
|
||||
"Niadd category pagination",
|
||||
''' } else {
|
||||
@@ -67,6 +91,727 @@ replace_exact(
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. MANGA LIST
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Old NineManga expects:
|
||||
#
|
||||
# ul.direlist
|
||||
# li
|
||||
# dd
|
||||
# a.bookname
|
||||
#
|
||||
# Niadd no longer has ul.direlist.
|
||||
#
|
||||
# Manga detail links are still stable:
|
||||
#
|
||||
# /manga/<name>.html
|
||||
#
|
||||
# We therefore identify entries by their canonical /manga/ URL instead of
|
||||
# presentation CSS classes.
|
||||
#
|
||||
replace_exact(
|
||||
"Niadd manga list parser",
|
||||
''' val doc = webClient.httpGet(url).parseHtml()
|
||||
val root = doc.body().selectFirstOrThrow("ul.direlist")
|
||||
val baseHost = root.baseUri().toHttpUrl().host
|
||||
return root.select("li").map { node ->
|
||||
val href = node.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
|
||||
val relUrl = href.toRelativeUrl(baseHost)
|
||||
val dd = node.selectFirst("dd")
|
||||
Manga(
|
||||
id = generateUid(relUrl),
|
||||
url = relUrl,
|
||||
publicUrl = href,
|
||||
title = dd?.selectFirst("a.bookname")?.text()?.toCamelCase().orEmpty(),
|
||||
altTitles = emptySet(),
|
||||
coverUrl = node.selectFirst("img")?.src(),
|
||||
rating = RATING_UNKNOWN,
|
||||
authors = emptySet(),
|
||||
contentRating = null,
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
source = source,
|
||||
description = dd?.selectFirst("p")?.html(),
|
||||
)
|
||||
}
|
||||
''',
|
||||
''' val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
if (domain.endsWith("niadd.com")) {
|
||||
val mangaLinks = doc.select("a[href]").filter { link ->
|
||||
val href = link.attr("href")
|
||||
href.startsWith("/manga/") ||
|
||||
href.startsWith("https://${domain}/manga/")
|
||||
}
|
||||
|
||||
val linksByUrl = mangaLinks.groupBy { link ->
|
||||
link.attrAsAbsoluteUrl("href")
|
||||
}
|
||||
|
||||
return linksByUrl.mapNotNull { (href, links) ->
|
||||
val relUrl = href.toRelativeUrl(domain)
|
||||
|
||||
val title = links.asSequence()
|
||||
.mapNotNull { link ->
|
||||
link.selectFirst(
|
||||
"h1, h2, h3, h4, h5, h6, strong, b"
|
||||
)?.textOrNull()
|
||||
}
|
||||
.map { it.trim() }
|
||||
.firstOrNull { it.isNotEmpty() }
|
||||
?: links.asSequence()
|
||||
.map { it.ownText().trim() }
|
||||
.firstOrNull { it.isNotEmpty() }
|
||||
?: links.asSequence()
|
||||
.mapNotNull { link ->
|
||||
link.selectFirst("img[alt]")
|
||||
?.attr("alt")
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
.firstOrNull()
|
||||
?: return@mapNotNull null
|
||||
|
||||
val coverUrl = links.asSequence()
|
||||
.mapNotNull { link ->
|
||||
link.selectFirst("img")?.src()
|
||||
?: link.parent()
|
||||
?.selectFirst("img")
|
||||
?.src()
|
||||
?: link.parent()
|
||||
?.parent()
|
||||
?.selectFirst("img")
|
||||
?.src()
|
||||
}
|
||||
.firstOrNull()
|
||||
|
||||
val description = links.asSequence()
|
||||
.mapNotNull { link ->
|
||||
link.select("p")
|
||||
.mapNotNull { it.textOrNull() }
|
||||
.maxByOrNull { it.length }
|
||||
}
|
||||
.firstOrNull()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
||||
Manga(
|
||||
id = generateUid(relUrl),
|
||||
url = relUrl,
|
||||
publicUrl = href,
|
||||
title = title.toCamelCase(),
|
||||
altTitles = emptySet(),
|
||||
coverUrl = coverUrl,
|
||||
rating = RATING_UNKNOWN,
|
||||
authors = emptySet(),
|
||||
contentRating = null,
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
source = source,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val root = doc.body().selectFirstOrThrow("ul.direlist")
|
||||
val baseHost = root.baseUri().toHttpUrl().host
|
||||
|
||||
return root.select("li").map { node ->
|
||||
val href = node.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
|
||||
val relUrl = href.toRelativeUrl(baseHost)
|
||||
val dd = node.selectFirst("dd")
|
||||
|
||||
Manga(
|
||||
id = generateUid(relUrl),
|
||||
url = relUrl,
|
||||
publicUrl = href,
|
||||
title = dd?.selectFirst("a.bookname")
|
||||
?.text()
|
||||
?.toCamelCase()
|
||||
.orEmpty(),
|
||||
altTitles = emptySet(),
|
||||
coverUrl = node.selectFirst("img")?.src(),
|
||||
rating = RATING_UNKNOWN,
|
||||
authors = emptySet(),
|
||||
contentRating = null,
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
source = source,
|
||||
description = dd?.selectFirst("p")?.html(),
|
||||
)
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. DETAILS + CHAPTERS
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Old parser depends on:
|
||||
#
|
||||
# div.manga
|
||||
# div.bookintro
|
||||
# div.chapterbox
|
||||
# ul.sub_vol_ul
|
||||
# a.chapter_list_a
|
||||
#
|
||||
# Niadd no longer has that hierarchy.
|
||||
#
|
||||
# Current Niadd however has stable semantic URLs:
|
||||
#
|
||||
# Manga:
|
||||
# /manga/Foo.html
|
||||
#
|
||||
# Full chapter list:
|
||||
# /manga/Foo/chapters.html
|
||||
#
|
||||
# Chapter:
|
||||
# /chapter/3_81/5259607/
|
||||
# /chapter/2_80_2/5205144.html
|
||||
#
|
||||
# So for Niadd we parse by semantic links, not CSS classes.
|
||||
#
|
||||
replace_exact(
|
||||
"Niadd details and chapters parser",
|
||||
''' override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(
|
||||
manga.url.toAbsoluteUrl(domain) + "?waring=1",
|
||||
).parseHtml()
|
||||
val root = doc.body().selectFirstOrThrow("div.manga")
|
||||
val infoRoot = root.selectFirstOrThrow("div.bookintro")
|
||||
val tagMap = getOrCreateTagMap()
|
||||
val selectTag = infoRoot.getElementsByAttributeValue("itemprop", "genre").first()?.select("a")
|
||||
val tags = selectTag?.mapNotNullToSet { tagMap[it.text()] }
|
||||
val author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.textOrNull()
|
||||
return manga.copy(
|
||||
title = root.selectFirst("h1[itemprop=name]")?.textOrNull()?.removeSuffix("Manga")?.trimEnd()
|
||||
?: manga.title,
|
||||
tags = tags.orEmpty(),
|
||||
authors = setOfNotNull(author),
|
||||
state = parseStatus(infoRoot.select("li a.red").text()),
|
||||
description = infoRoot.getElementsByAttributeValue("itemprop", "description").first()?.html()
|
||||
?.substringAfter("</b>"),
|
||||
chapters = root.selectFirst("div.chapterbox")?.select("ul.sub_vol_ul > li")
|
||||
?.mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a.chapter_list_a")
|
||||
val href = a.attrAsRelativeUrl("href").replace("%20", " ")
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
title = a.textOrNull(),
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
url = href,
|
||||
uploadDate = parseChapterDateByLang(li.selectFirst("span")?.text().orEmpty()),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
''',
|
||||
''' override suspend fun getDetails(manga: Manga): Manga {
|
||||
if (domain.endsWith("niadd.com")) {
|
||||
val mangaPath = manga.url
|
||||
.substringBefore("?")
|
||||
.removeSuffix("/")
|
||||
|
||||
val chaptersPath = if (mangaPath.endsWith(".html")) {
|
||||
mangaPath.removeSuffix(".html") + "/chapters.html"
|
||||
} else {
|
||||
"$mangaPath/chapters.html"
|
||||
}
|
||||
|
||||
val chaptersDoc = webClient.httpGet(
|
||||
chaptersPath.toAbsoluteUrl(domain) + "?waring=1",
|
||||
).parseHtml()
|
||||
|
||||
/*
|
||||
* Current Niadd chapters page already contains
|
||||
* manga title, status, authors, genres and language,
|
||||
* so avoid a second HTTP request to the details page.
|
||||
*/
|
||||
val detailsDoc = chaptersDoc
|
||||
|
||||
val heading = detailsDoc.selectFirst("h1")
|
||||
?.textOrNull()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
|
||||
val title = heading
|
||||
.replace(
|
||||
Regex(
|
||||
"""\\s*\\((?:постоянный|завершенный)\\)\\s*$""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
"",
|
||||
)
|
||||
.trim()
|
||||
.ifEmpty { manga.title }
|
||||
|
||||
val tagMap = getOrCreateTagMap()
|
||||
|
||||
val tags = detailsDoc
|
||||
.select("a[href^=/category/]")
|
||||
.mapNotNullToSet { element ->
|
||||
val tagTitle = element.text()
|
||||
.trim()
|
||||
.removePrefix(",")
|
||||
.trim()
|
||||
|
||||
tagMap[tagTitle]
|
||||
}
|
||||
|
||||
val authors = detailsDoc
|
||||
.select("a[href*=author=]")
|
||||
.mapNotNullToSet { element ->
|
||||
element.textOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
val pageDescription = detailsDoc
|
||||
.selectFirst("meta[name=description]")
|
||||
?.attr("content")
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val chapterDateRegex = Regex(
|
||||
"""\\b[A-Z][a-z]{2}\\s+\\d{1,2},\\s+\\d{4}\\b"""
|
||||
)
|
||||
|
||||
val chapterLinks = chaptersDoc
|
||||
.select("a[href]")
|
||||
.filter { element ->
|
||||
val href = element.attr("href")
|
||||
|
||||
(
|
||||
href.startsWith("/chapter/") ||
|
||||
href.startsWith(
|
||||
"https://${domain}/chapter/"
|
||||
)
|
||||
) &&
|
||||
!href.contains("/manga_for_adults/")
|
||||
}
|
||||
.distinctBy { element ->
|
||||
element.attrAsAbsoluteUrl("href")
|
||||
}
|
||||
.reversed()
|
||||
|
||||
val chapters = chapterLinks.mapIndexed { i, a ->
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
.replace("%20", " ")
|
||||
|
||||
val anchorText = a.text().trim()
|
||||
val rowText = a.parent()?.text().orEmpty().trim()
|
||||
|
||||
val rawTitle = a.ownText()
|
||||
.trim()
|
||||
.ifEmpty {
|
||||
anchorText
|
||||
}
|
||||
|
||||
val anchorDate = chapterDateRegex
|
||||
.find(anchorText)
|
||||
?.value
|
||||
|
||||
val rowDate = chapterDateRegex
|
||||
.find(rowText)
|
||||
?.value
|
||||
|
||||
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 (
|
||||
dateText != null &&
|
||||
rawTitle.contains(dateText)
|
||||
) {
|
||||
rawTitle
|
||||
.substringBefore(dateText)
|
||||
.replace(
|
||||
Regex("""\\s+\\d+\\s*$"""),
|
||||
"",
|
||||
)
|
||||
.trim()
|
||||
} else {
|
||||
rawTitle
|
||||
}
|
||||
|
||||
val chapterUrl = if (
|
||||
pageCount != null &&
|
||||
pageCount > 0
|
||||
) {
|
||||
"$href#niadd-pages=$pageCount"
|
||||
} else {
|
||||
href
|
||||
}
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
title = chapterTitle.takeIf {
|
||||
it.isNotEmpty()
|
||||
},
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
url = chapterUrl,
|
||||
uploadDate = parseChapterDateByLang(
|
||||
dateText.orEmpty()
|
||||
),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
}
|
||||
|
||||
return manga.copy(
|
||||
title = title,
|
||||
tags = tags,
|
||||
authors = authors,
|
||||
state = parseStatus(
|
||||
detailsDoc.body().text()
|
||||
),
|
||||
description = manga.description
|
||||
?: pageDescription,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(
|
||||
manga.url.toAbsoluteUrl(domain) + "?waring=1",
|
||||
).parseHtml()
|
||||
|
||||
val root = doc.body().selectFirstOrThrow("div.manga")
|
||||
val infoRoot = root.selectFirstOrThrow("div.bookintro")
|
||||
|
||||
val tagMap = getOrCreateTagMap()
|
||||
|
||||
val selectTag = infoRoot
|
||||
.getElementsByAttributeValue(
|
||||
"itemprop",
|
||||
"genre",
|
||||
)
|
||||
.first()
|
||||
?.select("a")
|
||||
|
||||
val tags = selectTag
|
||||
?.mapNotNullToSet {
|
||||
tagMap[it.text()]
|
||||
}
|
||||
|
||||
val author = infoRoot
|
||||
.getElementsByAttributeValue(
|
||||
"itemprop",
|
||||
"author",
|
||||
)
|
||||
.first()
|
||||
?.textOrNull()
|
||||
|
||||
return manga.copy(
|
||||
title = root
|
||||
.selectFirst("h1[itemprop=name]")
|
||||
?.textOrNull()
|
||||
?.removeSuffix("Manga")
|
||||
?.trimEnd()
|
||||
?: manga.title,
|
||||
|
||||
tags = tags.orEmpty(),
|
||||
|
||||
authors = setOfNotNull(author),
|
||||
|
||||
state = parseStatus(
|
||||
infoRoot.select("li a.red").text()
|
||||
),
|
||||
|
||||
description = infoRoot
|
||||
.getElementsByAttributeValue(
|
||||
"itemprop",
|
||||
"description",
|
||||
)
|
||||
.first()
|
||||
?.html()
|
||||
?.substringAfter("</b>"),
|
||||
|
||||
chapters = root
|
||||
.selectFirst("div.chapterbox")
|
||||
?.select("ul.sub_vol_ul > li")
|
||||
?.mapChapters(
|
||||
reversed = true
|
||||
) { i, li ->
|
||||
val a = li.selectFirstOrThrow(
|
||||
"a.chapter_list_a"
|
||||
)
|
||||
|
||||
val href = a
|
||||
.attrAsRelativeUrl("href")
|
||||
.replace("%20", " ")
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
title = a.textOrNull(),
|
||||
number = i + 1f,
|
||||
volume = 0,
|
||||
url = href,
|
||||
uploadDate = parseChapterDateByLang(
|
||||
li.selectFirst("span")
|
||||
?.text()
|
||||
.orEmpty()
|
||||
),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. CHAPTER PAGE LIST
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Old NineManga:
|
||||
#
|
||||
# #page option
|
||||
#
|
||||
# Current Niadd has several <select>'s on the reader:
|
||||
# chapter selector
|
||||
# batch loading
|
||||
# image scale
|
||||
# page selector
|
||||
#
|
||||
# Instead of relying on id="page", select only options whose text is:
|
||||
#
|
||||
# 1/61
|
||||
# 2/61
|
||||
# ...
|
||||
#
|
||||
replace_exact(
|
||||
"Niadd page list parser",
|
||||
''' override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.body().requireElementById("page").select("option").map { option ->
|
||||
val url = option.attr("value")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
''',
|
||||
''' override suspend fun getPages(
|
||||
chapter: MangaChapter,
|
||||
): List<MangaPage> {
|
||||
if (domain.endsWith("niadd.com")) {
|
||||
val originalUrl = chapter.url
|
||||
.substringBefore("#")
|
||||
.substringBefore("?")
|
||||
|
||||
val totalPages = chapter.url
|
||||
.substringAfter(
|
||||
"#niadd-pages=",
|
||||
"",
|
||||
)
|
||||
.substringBefore("&")
|
||||
.toIntOrNull()
|
||||
?.takeIf { it > 0 }
|
||||
?: error(
|
||||
"Niadd page count is missing for ${chapter.url}. " +
|
||||
"Refresh manga details and try again."
|
||||
)
|
||||
|
||||
val normalizedUrl = originalUrl
|
||||
.removeSuffix("/")
|
||||
|
||||
val baseUrl = if (
|
||||
normalizedUrl.endsWith(".html")
|
||||
) {
|
||||
normalizedUrl.removeSuffix(".html")
|
||||
} else {
|
||||
normalizedUrl
|
||||
}
|
||||
|
||||
return (1..totalPages).map { pageNumber ->
|
||||
val url = if (pageNumber == 1) {
|
||||
originalUrl
|
||||
} else {
|
||||
"$baseUrl-$pageNumber.html"
|
||||
}
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient
|
||||
.httpGet(
|
||||
chapter.url.toAbsoluteUrl(domain)
|
||||
)
|
||||
.parseHtml()
|
||||
|
||||
return doc
|
||||
.body()
|
||||
.requireElementById("page")
|
||||
.select("option")
|
||||
.map { option ->
|
||||
val url = option.attr("value")
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. IMAGE URL
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Old NineManga:
|
||||
#
|
||||
# a.pic_download
|
||||
#
|
||||
# Current Niadd serves page images from an external image host.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# https://ruwebp.movietop.cc/comics/.../image.webp?acc=...&exp=...
|
||||
#
|
||||
# So find an image-looking URL instead of relying on CSS class.
|
||||
#
|
||||
replace_exact(
|
||||
"Niadd image URL parser",
|
||||
''' override suspend fun getPageUrl(page: MangaPage): String {
|
||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val root = doc.body()
|
||||
return root.selectFirstOrThrow("a.pic_download").attrAsAbsoluteUrl("href")
|
||||
}
|
||||
''',
|
||||
''' override suspend fun getPageUrl(
|
||||
page: MangaPage,
|
||||
): String {
|
||||
val doc = webClient
|
||||
.httpGet(
|
||||
page.url.toAbsoluteUrl(domain)
|
||||
)
|
||||
.parseHtml()
|
||||
|
||||
val root = doc.body()
|
||||
|
||||
if (domain.endsWith("niadd.com")) {
|
||||
val readerImageLinkRegex = Regex(
|
||||
"""^\\s*\\d+\\s+of\\s+\\d+\\s*$""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
val imageAnchor = root
|
||||
.select("a[href]")
|
||||
.firstOrNull { element ->
|
||||
readerImageLinkRegex.matches(
|
||||
element.text()
|
||||
)
|
||||
}
|
||||
|
||||
if (imageAnchor != null) {
|
||||
return imageAnchor.attrAsAbsoluteUrl(
|
||||
"href"
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
|
||||
error(
|
||||
"Cannot find Niadd reader image for ${page.url}"
|
||||
)
|
||||
}
|
||||
|
||||
return root
|
||||
.selectFirstOrThrow("a.pic_download")
|
||||
.attrAsAbsoluteUrl("href")
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. RUSSIAN STATUS
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Niadd currently uses e.g.:
|
||||
#
|
||||
# постоянный
|
||||
# Завершенный
|
||||
#
|
||||
replace_exact(
|
||||
"Russian status parsing",
|
||||
''' //ru
|
||||
@@ -74,12 +819,28 @@ replace_exact(
|
||||
status.contains("завершенный") -> MangaState.FINISHED
|
||||
''',
|
||||
''' //ru
|
||||
status.contains("постоянный", ignoreCase = true) -> MangaState.ONGOING
|
||||
status.contains("завершенный", ignoreCase = true) -> MangaState.FINISHED
|
||||
status.contains(
|
||||
"постоянный",
|
||||
ignoreCase = true,
|
||||
) -> MangaState.ONGOING
|
||||
|
||||
status.contains(
|
||||
"завершенный",
|
||||
ignoreCase = true,
|
||||
) -> MangaState.FINISHED
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. ENABLE RUSSIAN NIADD
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Keep NINEMANGA_RU unchanged intentionally.
|
||||
#
|
||||
# That preserves the existing source ID used by Futon database entries.
|
||||
#
|
||||
replace_exact(
|
||||
"NineManga RU to Niadd RU",
|
||||
''' @Broken
|
||||
@@ -91,7 +852,9 @@ replace_exact(
|
||||
)
|
||||
''',
|
||||
''' @MangaSourceParser("NINEMANGA_RU", "Niadd Русский", "ru")
|
||||
class Russian(context: MangaLoaderContext) : NineMangaParser(
|
||||
class Russian(
|
||||
context: MangaLoaderContext,
|
||||
) : NineMangaParser(
|
||||
context,
|
||||
MangaParserSource.NINEMANGA_RU,
|
||||
"ru.niadd.com",
|
||||
@@ -100,7 +863,21 @@ replace_exact(
|
||||
)
|
||||
|
||||
|
||||
parser_file.write_text(text, encoding="utf-8")
|
||||
parser_file.write_text(
|
||||
text,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
print()
|
||||
print(f"[OK] Patched: {parser_file}")
|
||||
print()
|
||||
print("[OK] Niadd domain")
|
||||
print("[OK] Niadd category pagination")
|
||||
print("[OK] Niadd manga list")
|
||||
print("[OK] Niadd details")
|
||||
print("[OK] Niadd chapter list")
|
||||
print("[OK] Niadd reader page list")
|
||||
print("[OK] Niadd image URL")
|
||||
print("[OK] Russian statuses")
|
||||
print("[OK] Russian source enabled")
|
||||
Reference in new issue
Block a user