Compare commits

...

67 Commits

Author SHA1 Message Date
dragonx943 7f98a7fb5c
chore (site/vi): Update sources domain 6 months ago
Draken a39721eaf7
DoujinDesu.tv: Fix search query, add author search support (#2326) 6 months ago
Draken 65c61c0c01
Mangafire: Get/generate vrf for ajax + Add referer, fix access denied (#2310)
Co-authored-by: Vicente <vicente@per>
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
Co-authored-by: Koitharu <nvasya95@gmail.com>
Co-authored-by: epikaigle444 <epikaigle444@gmail.com>
Co-authored-by: Max Bethmann <bethibande@gmail.com>
Co-authored-by: vtorres-t <73935362+vtorres-t@users.noreply.github.com>
Co-authored-by: epikaigle444 <epikaigle444@gmail.com>
Co-authored-by: Naga <94557604+NagaYZ@users.noreply.github.com>
7 months ago
dragonx943 ac977a6cbb
HentaiVN.su: Small fixes, remove broken annotation 7 months ago
Draken 8908031eee
Rawkuma: Update domain + Add broken annotation 7 months ago
Draken 4141a8f429
MimiHentai: Update API suffix, fix content not found or removed 7 months ago
Draken ad7f1eddca
Rawkuma: Use old domain, temporary fix nothing found 7 months ago
Draken 472ddab27c
GocTruyenTranhVui: Update token, fix chapters list 7 months ago
Draken 560cf63a5e
CManga: Fix missing content in getPages 7 months ago
dragonx943 d4e1acd515
Cứu Truyện: Allow to choose cover quality in source setting 7 months ago
Draken f66b97edc1
HentaiVN.su: Add source (#2295) 7 months ago
epikaigle444 a084909507
MangaMoins: Add source (#2284)
Co-authored-by: epikaigle444 <epikaigle444@gmail.com>
Co-authored-by: Koitharu <nvasya95@gmail.com>
Co-authored-by: dragonx943 <premieregirl26@gmail.com>
7 months ago
vtorres-t 58357a3745
chore: Add broken annotation for some sources (#2275)
Co-authored-by: Vicente <vicente@per>
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
Co-authored-by: Koitharu <nvasya95@gmail.com>
7 months ago
Max Bethmann 8a6a529023
Mangakakalot fix getListPage (#2231) (#2285) 7 months ago
UC-01-Zer0-ID 1b9ff47ab3
ScanHentai.Menu: Update domain (#2277) 7 months ago
Draken 229f321299
Cứu Truyện: Remove broken 7 months ago
Draken a5233ede48
MangaGeko: Fix missing images in getPages (#2267) 7 months ago
dragonx943 d9025dbf81
AtsuMoe: Delete source that were never usable 7 months ago
vtorres-t 2cc0af17d5
chore: Update domains (#2264)
Co-authored-by: Vicente <vicente@per>
7 months ago
Hoàng Phi Hùng a3f384e3b3
Ổ Truyện: Fix no value for _id in getListPage (#2259)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
7 months ago
nakrovati 5882a5d54d
chore: update domains (#2255)
Close #2249, #2234, #2228, #2127
7 months ago
Koitharu f62809407b
[Madaradex] Fixes 7 months ago
Draken 14c0eaf273
Truyện Tranh Full: Add source (#2252) 7 months ago
Draken 0817a82f36
MadaraDex: Fix access denied errors in getPages
Co-authored-by: Koitharu <nvasya95@gmail.com>
7 months ago
Draken 727e326379
Cứu Truyện: Add broken 7 months ago
Harsh Dev Jha ccaf52b74e
AtsuMoe: Add source (close #2187)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
7 months ago
Harsh Dev Jha fc988cdd1e
ReadManhua: Change source name to Resetscans + Update domain
Close #2201
7 months ago
Harsh Dev Jha cb2e9841ae
YuriLab: Update domain (close #2233) 7 months ago
Draken 775063d36f
MimiHentai: Fix intercept, scrambled images in getPages (#2237) 7 months ago
Naga 86b8642456
XoxoComics: Fixes (#2229) 7 months ago
Draken 185d9b6e6e
MyReadingManga: Fix order when query is empty 7 months ago
dragonx943 e3495fc41f
MadaraDex: Add referer 7 months ago
dragonx943 d8e47b2b52
HiveComic: Remove broken 7 months ago
dragonx943 be64bf2328
MyReadingManga: Fix locale query + tags in getDetails 7 months ago
dragonx943 d85bb64b1c
HiveComic: Use API to fix content not found in getListPage 7 months ago
dragonx943 de2289eb75
FreeMangaTop: Fix access denied in getPages 7 months ago
dragonx943 7cb069d108
MyReadingManga: Improve search query with more filters 7 months ago
dragonx943 8a18df148d
ComicK: Add exception + Fix build for ci 7 months ago
dragonx943 9007852d31
MyReadingManga: Add broken 7 months ago
dragonx943 60e451303e
MadaraParser: Add some values + Fix warn 7 months ago
dragonx943 170a11ae26
ci: Update dependencies for actions 7 months ago
dragonx943 642ac08338
MadaraDex: Fix access denied in getPages 7 months ago
Draken ff256cb3ec
CBHentai: Add state to manga details 7 months ago
Hoàng Phi Hùng 08d46988a3
LXManga: Add referer, fix not found error in getPages (#2220) 7 months ago
Draken a3abc81dfc
Dâm Cô Nương: Fix chapter list not found (#2210) 7 months ago
dragonx943 41a4c90e75
ComicK: Add broken 7 months ago
dragonx943 23fdfbbef9
Madtheme: Fix warns 8 months ago
dragonx943 a4f41e41f8
MTLParser: Fix warns 8 months ago
dragonx943 db586045d7
VioletScans: Fix warns 8 months ago
dragonx943 e636eda840
Remove unused import + Fix warns 8 months ago
dragonx943 406992a6c9
DragonTranslation.org: Add source + Fix warns 8 months ago
Koitharu 30f97c5c82
Improve OkHttp coroutines integration 8 months ago
Koitharu b9ecdb2db6
[Desu] Fixes 8 months ago
Koitharu e044463f91
[Grouple] Fixes 8 months ago
Koitharu a93c760133
Fix tests running 8 months ago
dragonx943 f2b6cab251
site/vi: Update sources domain 8 months ago
dragonx943 07225568ee
BatCave: Add referer, close #2176 8 months ago
dragonx943 860dd32a53
Góc Truyện Tranh Vui: Fix warns 8 months ago
dragonx943 74f5891ffe
Góc Truyện Tranh Vui: Update token, fix chapters list 8 months ago
Draken 7bae2602c7
Merge pull request #2174 from nakrovati/build/improve-buildsrc-gradle-config
Use libs.toml.versions and `kotlin-dsl` in buildSrc build.gradle.kts
8 months ago
Draken 4c697b6406
Jitpack: Add configuration file to use JVM 17 8 months ago
Daniil Zhuravlev b61698d405 chore: use libs.toml.versions in buildSrc 8 months ago
Draken 260e5acd5c
Cứu Truyện: Change pageSize to 24, remove unused state 8 months ago
Koitharu e599f431c5
Merge pull request #2172 from nakrovati/build/remove-gradle-from-buildsrc 8 months ago
Daniil Zhuravlev 0ec4fe42c1 build: migrate to gradle 9 8 months ago
Daniil Zhuravlev c9e4c7273e build: remove gradle from buildSrc dir 8 months ago
Draken 3e2515ac6a
Cứu Truyện: Improve search query with multiple tags (#2171) 8 months ago

@ -1 +1 @@
total: 1251
total: 1255

@ -13,15 +13,13 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up enviroment 🔧
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Set up Gradle 📦
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with:
cache-read-only: true
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
- name: Compile parsers 🚀
run: ./gradlew compileKotlin

@ -17,15 +17,13 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up enviroment 🔧
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: '17'
distribution: 'temurin'
java-version: '21'
distribution: 'temurin'
- name: Set up Gradle 📦
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
with:
cache-read-only: true
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
- name: Compile parsers 🚀
run: ./gradlew compileKotlin

4
.gitignore vendored

@ -92,3 +92,7 @@ local.properties
.idea/**/runConfigurations.xml
.idea/**/AndroidProjectSystem.xml
.idea/caches/deviceStreaming.xml
/.idea/copilot.data.migration.agent.xml
/.idea/copilot.data.migration.ask.xml
/.idea/copilot.data.migration.ask2agent.xml
/.idea/copilot.data.migration.edit.xml

@ -56,6 +56,7 @@ dependencies {
testImplementation(libs.junit.api)
testImplementation(libs.junit.engine)
testImplementation(libs.junit.params)
testRuntimeOnly(libs.junit.launcher)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.quickjs)
}

@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "2.2.10"
`kotlin-dsl`
}
repositories {
@ -11,8 +11,7 @@ kotlin {
}
dependencies {
implementation(gradleApi())
implementation("org.simpleframework:simple-xml:2.7.1")
implementation("com.soywiz.korlibs.korte:korte-jvm:4.0.10")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation(libs.korte)
implementation(libs.simplexml)
implementation(libs.kotlinx.coroutines.core)
}

Binary file not shown.

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

47
buildSrc/gradlew vendored

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@ -133,22 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

@ -0,0 +1,7 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

@ -1,9 +1,15 @@
# Following this blog:
## Following this blog:
# https://proandroiddev.com/how-we-reduced-our-gradle-build-times-by-over-80-51f2b6d6b05b
kotlin.code.style=official
systemProp.org.gradle.unsafe.configuration-cache=false
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=4096m -XX:+UseParallelGC
org.gradle.vfs.watch=true
org.gradle.configureondemand=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.unsafe.configuration-cache=true
org.gradle.configuration-cache.problems=warn
## Use these flags on local machine for faster build time
# org.gradle.caching=true
# org.gradle.configuration-cache=true
# org.gradle.vfs.watch=true
# org.gradle.parallel=true
# org.gradle.workers.max=8
# org.gradle.configuration-cache.max-problems=8

@ -9,6 +9,8 @@ json = "20240303"
androidx-collection = "1.5.0"
jsoup = "1.21.2"
quickjs = "1.1.0"
korte = "4.0.10"
simplexml = "2.7.1"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
@ -18,6 +20,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
@ -27,3 +30,5 @@ json = { module = "org.json:json", version.ref = "json" }
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
quickjs = { module = "io.webfolder:quickjs", version.ref = "quickjs" }
korte = { module = "com.soywiz.korlibs.korte:korte-jvm", version.ref = "korte" }
simplexml = { module = "org.simpleframework:simple-xml", version.ref = "simplexml" }

Binary file not shown.

@ -1,7 +1,7 @@
#Wed Aug 27 01:56:37 ICT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

49
gradlew vendored

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@ -133,22 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

41
gradlew.bat vendored

@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

@ -0,0 +1,2 @@
jdk:
- openjdk17

@ -5,6 +5,7 @@ import androidx.collection.SparseArrayCompat
import okhttp3.HttpUrl
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
@ -18,6 +19,7 @@ import java.util.*
private const val CHAPTERS_LIMIT = 99999
@Broken("Original site closed")
@MangaSourceParser("COMICK_FUN", "ComicK")
internal class ComickFunParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.COMICK_FUN, 20) {
@ -145,8 +147,13 @@ internal class ComickFunParser(context: MangaLoaderContext) :
)
}
val ja = webClient.httpGet(url.build()).parseJsonArray()
val tagsMap = tagsArray.get()
val ja = try {
webClient.httpGet(url.build()).parseJsonArray()
} catch (_: Exception) {
throw IllegalArgumentException("ComicK is down!")
}
val tagsMap = tagsArray.get()
return ja.mapJSON { jo ->
val slug = jo.getString("slug")
Manga(

@ -19,9 +19,7 @@ import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.util.*
import org.koitharu.kotatsu.parsers.Broken
@Broken("Need to fix getPages, most manga don't have chapter images due to faulty fetch logic")
@MangaSourceParser("KOHARU", "Schale.network", type = ContentType.HENTAI)
internal class Koharu(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.KOHARU, 24) {

@ -14,11 +14,40 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.bitmap.Rect
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.getCookies
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.ownTextOrNull
import org.koitharu.kotatsu.parsers.util.parseFailed
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.parseSafe
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.splitByWhitespace
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.text.SimpleDateFormat
import java.util.*
import java.util.Base64
import java.util.EnumSet
import java.util.Locale
import kotlin.math.min
private const val PIECE_SIZE = 200
@ -41,6 +70,10 @@ internal abstract class MangaFireParser(
SortOrder.RELEVANCE,
)
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.add("Referer", "https://$domain/")
.build()
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
@ -95,25 +128,30 @@ internal abstract class MangaFireParser(
addQueryParameter("page", page.toString())
addQueryParameter("language[]", siteLang)
when {
!filter.query.isNullOrEmpty() -> {
val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part ->
part.urlEncoded()
}
addEncodedQueryParameter("keyword", encodedQuery)
addQueryParameter(
name = "sort",
value = when (order) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
SortOrder.NEWEST -> "release_date"
SortOrder.ALPHABETICAL -> "title_az"
SortOrder.RELEVANCE -> "most_relevance"
else -> ""
},
)
}
when {
!filter.query.isNullOrEmpty() -> {
val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part ->
part.urlEncoded()
}
addEncodedQueryParameter("keyword", encodedQuery)
// Generate VRF for search query
val searchVrf = VrfGenerator.generate(filter.query.trim())
addQueryParameter("vrf", searchVrf)
addQueryParameter(
name = "sort",
value = when (order) {
SortOrder.UPDATED -> "recently_updated"
SortOrder.POPULARITY -> "most_viewed"
SortOrder.RATING -> "scores"
SortOrder.NEWEST -> "release_date"
SortOrder.ALPHABETICAL -> "title_az"
SortOrder.RELEVANCE -> "most_relevance"
else -> ""
},
)
}
else -> {
filter.tagsExclude.forEach { tag ->
@ -260,14 +298,18 @@ internal abstract class MangaFireParser(
}
}
private suspend fun getChaptersBranch(mangaId: String, branch: ChapterBranch): List<MangaChapter> {
val chapterElements = webClient
.httpGet("https://$domain/ajax/read/$mangaId/${branch.type}/${branch.langCode}")
.parseJson()
.getJSONObject("result")
.getString("html")
.let(Jsoup::parseBodyFragment)
.select("ul li a")
private suspend fun getChaptersBranch(mangaId: String, branch: ChapterBranch): List<MangaChapter> {
val readVrfInput = "$mangaId@${branch.type}@${branch.langCode}"
val readVrf = VrfGenerator.generate(readVrfInput)
val response = webClient
.httpGet("https://$domain/ajax/read/$mangaId/${branch.type}/${branch.langCode}?vrf=$readVrf")
val chapterElements = response.parseJson()
.getJSONObject("result")
.getString("html")
.let(Jsoup::parseBodyFragment)
.select("ul li a")
if (branch.type == "chapter") {
val doc = webClient
@ -276,31 +318,32 @@ internal abstract class MangaFireParser(
.getString("result")
.let(Jsoup::parseBodyFragment)
doc.select("ul li a").withIndex().forEach { (i, it) ->
val date = it.select("span")[1].ownText()
chapterElements[i].attr("upload-date", date)
chapterElements[i].attr("other-title", it.attr("title"))
}
}
return chapterElements.mapChapters(reversed = true) { _, it ->
MangaChapter(
id = generateUid(it.attr("href")),
title = it.attr("title").ifBlank {
"${branch.type.toTitleCase()} ${it.attr("data-number")}"
},
number = it.attr("data-number").toFloat(),
volume = it.attr("other-title").let {
volumeNumRegex.find(it)?.groupValues?.getOrNull(2)?.toInt() ?: 0
},
url = "${branch.type}/${it.attr("data-id")}",
scanlator = null,
uploadDate = dateFormat.parseSafe(it.attr("upload-date")),
branch = "${branch.langTitle} ${branch.type.toTitleCase()}",
source = source,
)
}
}
doc.select("ul li a").withIndex().forEach { (i, it) ->
val date = it.select("span").getOrNull(1)?.ownText() ?: ""
chapterElements[i].attr("upload-date", date)
chapterElements[i].attr("other-title", it.attr("title"))
}
}
return chapterElements.mapChapters(reversed = true) { _, it ->
val chapterId = it.attr("data-id")
MangaChapter(
id = generateUid(it.attr("href")),
title = it.attr("title").ifBlank {
"${branch.type.toTitleCase()} ${it.attr("data-number")}"
},
number = it.attr("data-number").toFloatOrNull() ?: -1f,
volume = it.attr("other-title").let { title ->
volumeNumRegex.find(title)?.groupValues?.getOrNull(2)?.toInt() ?: 0
},
url = "$mangaId/${branch.type}/${branch.langCode}/$chapterId",
scanlator = null,
uploadDate = dateFormat.parseSafe(it.attr("upload-date")),
branch = "${branch.langTitle} ${branch.type.toTitleCase()}",
source = source,
)
}
}
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)
private val volumeNumRegex = Regex("""vol(ume)?\s*(\d+)""", RegexOption.IGNORE_CASE)
@ -387,12 +430,15 @@ internal abstract class MangaFireParser(
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val images = webClient
.httpGet("https://$domain/ajax/read/${chapter.url}")
.parseJson()
.getJSONObject("result")
.getJSONArray("images")
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterId = chapter.url.substringAfterLast('/')
val vrf = VrfGenerator.generate("chapter@$chapterId")
val images = webClient
.httpGet("https://$domain/ajax/read/chapter/$chapterId?vrf=$vrf")
.parseJson()
.getJSONObject("result")
.getJSONArray("images")
val pages = ArrayList<MangaPage>(images.length())
@ -491,3 +537,186 @@ internal abstract class MangaFireParser(
class PortugueseBR(context: MangaLoaderContext) :
MangaFireParser(context, MangaParserSource.MANGAFIRE_PTBR, "pt-br")
}
private object VrfGenerator {
private fun atob(data: String): ByteArray = Base64.getDecoder().decode(data)
private fun btoa(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
private fun rc4(key: ByteArray, input: ByteArray): ByteArray {
val s = IntArray(256) { it }
var j = 0
// KSA
for (i in 0..255) {
j = (j + s[i] + key[i % key.size].toInt().and(0xFF)) and 0xFF
val temp = s[i]
s[i] = s[j]
s[j] = temp
}
// PRGA
val output = ByteArray(input.size)
var i = 0
j = 0
for (y in input.indices) {
i = (i + 1) and 0xFF
j = (j + s[i]) and 0xFF
val temp = s[i]
s[i] = s[j]
s[j] = temp
val k = s[(s[i] + s[j]) and 0xFF]
output[y] = (input[y].toInt() xor k).toByte()
}
return output
}
private fun transform(
input: ByteArray,
initSeedBytes: ByteArray,
prefixKeyBytes: ByteArray,
prefixLen: Int,
schedule: List<(Int) -> Int>,
): ByteArray {
val out = mutableListOf<Byte>()
for (i in input.indices) {
if (i < prefixLen) {
out.add(prefixKeyBytes[i])
}
val transformed = schedule[i % 10](
(input[i].toInt() xor initSeedBytes[i % 32].toInt()) and 0xFF,
) and 0xFF
out.add(transformed.toByte())
}
return out.toByteArray()
}
private val scheduleC = listOf<(Int) -> Int>(
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c xor 241) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c xor 8) and 0xFF },
)
private val scheduleY = listOf<(Int) -> Int>(
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c xor 163) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c xor 83) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
)
private val scheduleB = listOf<(Int) -> Int>(
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c - 48 + 256) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c xor 8) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c xor 163) and 0xFF },
)
private val scheduleJ = listOf<(Int) -> Int>(
{ c -> (c + 223) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c xor 83) and 0xFF },
{ c -> (c - 19 + 256) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c + 223) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c xor 83) and 0xFF },
)
private val scheduleE = listOf<(Int) -> Int>(
{ c -> (c + 82) and 0xFF },
{ c -> (c xor 83) and 0xFF },
{ c -> (c xor 163) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c - 170 + 256) and 0xFF },
{ c -> (c xor 8) and 0xFF },
{ c -> (c xor 241) and 0xFF },
{ c -> (c + 82) and 0xFF },
{ c -> (c + 176) and 0xFF },
{ c -> ((c shl 4) or (c ushr 4)) and 0xFF },
)
private val rc4Keys = mapOf(
"l" to "u8cBwTi1CM4XE3BkwG5Ble3AxWgnhKiXD9Cr279yNW0=",
"g" to "t00NOJ/Fl3wZtez1xU6/YvcWDoXzjrDHJLL2r/IWgcY=",
"B" to "S7I+968ZY4Fo3sLVNH/ExCNq7gjuOHjSRgSqh6SsPJc=",
"m" to "7D4Q8i8dApRj6UWxXbIBEa1UqvjI+8W0UvPH9talJK8=",
"F" to "0JsmfWZA1kwZeWLk5gfV5g41lwLL72wHbam5ZPfnOVE=",
)
private val seeds32 = mapOf(
"A" to "pGjzSCtS4izckNAOhrY5unJnO2E1VbrU+tXRYG24vTo=",
"V" to "dFcKX9Qpu7mt/AD6mb1QF4w+KqHTKmdiqp7penubAKI=",
"N" to "owp1QIY/kBiRWrRn9TLN2CdZsLeejzHhfJwdiQMjg3w=",
"P" to "H1XbRvXOvZAhyyPaO68vgIUgdAHn68Y6mrwkpIpEue8=",
"k" to "2Nmobf/mpQ7+Dxq1/olPSDj3xV8PZkPbKaucJvVckL0=",
)
private val prefixKeys = mapOf(
"O" to "Rowe+rg/0g==",
"v" to "8cULcnOMJVY8AA==",
"L" to "n2+Og2Gth8Hh",
"p" to "aRpvzH+yoA==",
"W" to "ZB4oBi0=",
)
fun generate(input: String): String {
var bytes = input.toByteArray()
// RC4 1
bytes = rc4(atob(rc4Keys["l"]!!), bytes)
// Step C1
bytes = transform(bytes, atob(seeds32["A"]!!), atob(prefixKeys["O"]!!), 7, scheduleC)
// RC4 2
bytes = rc4(atob(rc4Keys["g"]!!), bytes)
// Step Y
bytes = transform(bytes, atob(seeds32["V"]!!), atob(prefixKeys["v"]!!), 10, scheduleY)
// RC4 3
bytes = rc4(atob(rc4Keys["B"]!!), bytes)
// Step B
bytes = transform(bytes, atob(seeds32["N"]!!), atob(prefixKeys["L"]!!), 9, scheduleB)
// RC4 4
bytes = rc4(atob(rc4Keys["m"]!!), bytes)
// Step J
bytes = transform(bytes, atob(seeds32["P"]!!), atob(prefixKeys["p"]!!), 7, scheduleJ)
// RC4 5
bytes = rc4(atob(rc4Keys["F"]!!), bytes)
// Step E
bytes = transform(bytes, atob(seeds32["k"]!!), atob(prefixKeys["W"]!!), 5, scheduleE)
// Base64URL encode
return btoa(bytes)
.replace("+", "-")
.replace("/", "_")
.replace("=", "")
}
}

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.all
import okhttp3.Headers
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
@ -22,13 +23,15 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.splitByWhitespace
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
import java.util.regex.Pattern
import kotlin.collections.joinToString
@MangaSourceParser("MYREADINGMANGA", "MyReadingManga", type = ContentType.HENTAI)
internal class MyReadingManga(context: MangaLoaderContext) :
@ -36,19 +39,22 @@ internal class MyReadingManga(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("myreadingmanga.info")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36")
.add("X-Requested-With", randomString((1..20).random()))
.build()
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
isOriginalLocaleSupported = true,
isSearchWithFiltersSupported = true,
isMultipleTagsSupported = true,
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.RELEVANCE,
SortOrder.NEWEST,
SortOrder.NEWEST_ASC,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
@ -56,6 +62,8 @@ internal class MyReadingManga(context: MangaLoaderContext) :
availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
MangaState.PAUSED,
MangaState.ABANDONED,
),
availableContentRating = EnumSet.of(ContentRating.ADULT),
availableLocales = setOf(
@ -100,6 +108,8 @@ internal class MyReadingManga(context: MangaLoaderContext) :
private fun getLanguageSlug(locale: Locale?): String? {
return when {
locale?.language == "en" -> "english"
locale?.language == "ja" -> "japanese"
locale?.language == "fr" -> "french"
locale?.language == "ja" -> "jp"
locale?.language == "zh" && locale.country == "TW" -> "traditional-chinese"
@ -139,75 +149,56 @@ internal class MyReadingManga(context: MangaLoaderContext) :
}
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
// Add language path if specified
val langSlug = getLanguageSlug(filter.locale)
if (langSlug != null) {
append("/lang/")
append(langSlug)
}
when {
!filter.query.isNullOrEmpty() -> {
// Search with language: /lang/french/page/2/?s=example
if (page > 1) {
append("/page/")
append(page)
}
append("/?s=")
append(filter.query.urlEncoded())
}
filter.tags.isNotEmpty() -> {
// Genre filtering doesn't work with language, so we ignore language for genre
if (langSlug == null) {
append("/genre/")
append(filter.tags.first().key)
append("/page/")
append(page)
append("/")
} else {
// If both language and genre are selected, just use language
append("/page/")
append(page)
append("/")
}
}
filter.states.isNotEmpty() -> {
// Status filtering doesn't work with language either
if (langSlug == null) {
append("/status/")
append(
when (filter.states.first()) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
else -> "ongoing"
},
)
append("/page/")
append(page)
append("/")
} else {
// If both language and status are selected, just use language
append("/page/")
append(page)
append("/")
}
}
else -> {
// Regular browsing with or without language
append("/page/")
append(page)
append("/")
}
}
if (page > 1) {
append("/page/")
append(page)
}
// order
append("/?ep_sort=")
when (order) {
SortOrder.NEWEST -> append("date")
SortOrder.NEWEST_ASC -> append("date_asc")
else -> append("")
}
// fix order
append("&s=")
if (!filter.query.isNullOrEmpty()) {
append(filter.query.splitByWhitespace().joinToString(separator = "+"))
}
val langSlug = getLanguageSlug(filter.locale)
if (langSlug != null) {
append("&ep_filter_lang=")
append(langSlug)
}
if (filter.states.isNotEmpty()) {
append("&ep_filter_status=")
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "ongoing"
MangaState.FINISHED -> "completed"
MangaState.PAUSED -> "hiatus"
MangaState.ABANDONED -> "dropped"
else -> ""
}
)
}
}
if (filter.tags.isNotEmpty()) {
append("&ep_filter_genre=")
append(filter.tags.joinToString(",") { it.key })
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -287,10 +278,10 @@ internal class MyReadingManga(context: MangaLoaderContext) :
val genres = mutableSetOf<MangaTag>()
doc.select("span.entry-terms:has(span:contains(Genres)) a").forEach {
doc.select("span.entry-terms:has(span:contains(Genre:)) a").forEach {
genres.add(
MangaTag(
title = it.text(),
title = it.text().removeSuffix(",").trim(),
key = it.attr("href").substringAfterLast("/genre/").substringBefore("/"),
source = source,
),
@ -300,6 +291,8 @@ internal class MyReadingManga(context: MangaLoaderContext) :
val state = when (doc.select("a[href*=status]").firstOrNull()?.text()) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Hiatus" -> MangaState.PAUSED
"Dropped" -> MangaState.ABANDONED
else -> null
}
@ -356,15 +349,15 @@ internal class MyReadingManga(context: MangaLoaderContext) :
private fun findImageSrc(element: Element?): String? {
element ?: return null
return when {
element.hasAttr("data-src") && imgRegex.matcher(element.attr("data-src")).find() ->
element.hasAttr("data-src") && imgRegex.matcher(element.attr("data-src")).find() ->
element.absUrl("data-src")
element.hasAttr("data-cfsrc") && imgRegex.matcher(element.attr("data-cfsrc")).find() ->
element.hasAttr("data-cfsrc") && imgRegex.matcher(element.attr("data-cfsrc")).find() ->
element.absUrl("data-cfsrc")
element.hasAttr("src") && imgRegex.matcher(element.attr("src")).find() ->
element.hasAttr("src") && imgRegex.matcher(element.attr("src")).find() ->
element.absUrl("src")
element.hasAttr("data-lazy-src") ->
element.hasAttr("data-lazy-src") ->
element.absUrl("data-lazy-src")
else -> null
}
@ -447,5 +440,10 @@ internal class MyReadingManga(context: MangaLoaderContext) :
0L
}
}
private fun randomString(length: Int): String {
val charPool = ('a'..'z') + ('A'..'Z')
return List(length) { charPool.random() }.joinToString("")
}
}

@ -21,6 +21,10 @@ internal class BatCave(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("batcave.biz")
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.add("Referer", "https://$domain/")
.build()
private val availableTags = suspendLazy(initializer = ::fetchTags)
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -11,7 +11,7 @@ import org.koitharu.kotatsu.parsers.Broken
import java.text.SimpleDateFormat
import java.util.*
@Broken // The website is broken, it seems to be closed already.
@Broken("Original site closed")
@MangaSourceParser("COMICEXTRA", "ComicExtra", "en", ContentType.COMICS)
internal class ComicExtra(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.COMICEXTRA, 36) {

@ -156,18 +156,21 @@ internal class MangaGeko(context: MangaLoaderContext) :
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.requireElementById("chapter-reader").select("img").map { img ->
val url = img.requireSrc().toRelativeUrl(domain)
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select("center img")
.mapNotNull { it.attr("src").takeIf { src -> src.isNotBlank() } }
// remove all invaild images + credits
.filterNot { it.startsWith("data:image") || it.contains("credits-mgeko.png") }
.distinct().map { url ->
val finalUrl = url.toRelativeUrl(domain)
MangaPage(
id = generateUid(finalUrl),
url = finalUrl,
preview = null,
source = source,
)
}
}
}

@ -4,15 +4,8 @@ import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.FlexiblePagedMangaParser
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import org.koitharu.kotatsu.parsers.model.search.SearchCapability
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
import org.koitharu.kotatsu.parsers.model.search.SearchableField
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
@ -23,7 +16,12 @@ import java.util.Locale
@MangaSourceParser("VIOLETSCANS", "VioletScans", "en")
internal class VioletScans(context: MangaLoaderContext) :
FlexiblePagedMangaParser(context, MangaParserSource.VIOLETSCANS, 12) {
PagedMangaParser(context, MangaParserSource.VIOLETSCANS, 12) {
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("violetscans.com")
@ -34,37 +32,14 @@ internal class VioletScans(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val searchQueryCapabilities: MangaSearchQueryCapabilities
get() = MangaSearchQueryCapabilities(
SearchCapability(
field = TITLE_NAME,
criteriaTypes = setOf(Match::class),
isMultiple = false,
),
)
override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions()
override suspend fun getListPage(query: MangaSearchQuery, page: Int): List<Manga> {
var searchParameter = ""
query.criteria.forEach { criterion ->
when (criterion) {
is QueryCriteria.Match<*> -> {
if (criterion.field == SearchableField.TITLE_NAME) {
searchParameter = criterion.value.toString()
}
}
is QueryCriteria.Exclude<*> -> null
is QueryCriteria.Range<*> -> null
is QueryCriteria.Include<*> -> null
}
}
// scrapeNonSearchList has considerable less payload as response so this is a optimization
return when {
!searchParameter.isNullOrEmpty() -> scrapeSearchList(searchParameter, page)
else -> scrapeNonSearchList(page)
}
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
return if (!filter.query.isNullOrEmpty()) {
scrapeSearchList(filter.query, page)
} else {
scrapeNonSearchList(page)
}
}
private suspend fun scrapeNonSearchList(page: Int): List<Manga> {
@ -199,7 +174,7 @@ internal class VioletScans(context: MangaLoaderContext) :
}
val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
val date = dateFormat.parseSafe(dateString) ?: 0L
val date = dateFormat.parseSafe(dateString)
val chaptersList = root.selectFirstOrThrow("#chapterlist ul")
val chapters = chaptersList.select("li")

@ -1,15 +1,10 @@
package org.koitharu.kotatsu.parsers.site.en.MTL
package org.koitharu.kotatsu.parsers.site.en.mtl
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import org.koitharu.kotatsu.parsers.model.search.SearchCapability
import org.koitharu.kotatsu.parsers.model.search.SearchableField
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.core.FlexiblePagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.exception.ParseException
import java.text.SimpleDateFormat
import java.util.*
@ -18,7 +13,7 @@ internal abstract class MTLParser(
context: MangaLoaderContext,
source: MangaParserSource,
domain: String,
) : FlexiblePagedMangaParser(context, source, 24) {
) : PagedMangaParser(context, source, 24) {
override val configKeyDomain = ConfigKey.Domain(domain)
@ -30,21 +25,18 @@ internal abstract class MTLParser(
override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions()
override val searchQueryCapabilities = MangaSearchQueryCapabilities(
SearchCapability(
field = SearchableField.TITLE_NAME,
criteriaTypes = setOf(Match::class),
isMultiple = false,
),
)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
isSearchSupported = true,
)
override suspend fun getListPage(query: MangaSearchQuery, page: Int): List<Manga> {
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/search")
append("?")
when (query.order) {
when (order) {
SortOrder.POPULARITY -> append("sort_by=views")
SortOrder.UPDATED -> append("sort_by=recent")
else -> append("sort_by=recent")
@ -53,19 +45,10 @@ internal abstract class MTLParser(
append("&page=")
append(page)
}
query.criteria.find { it.field == SearchableField.TITLE_NAME }?.let { criteria ->
when (criteria) {
is Match -> {
append("&q=")
append(criteria.value.toString())
}
is Include,
is Exclude,
is Range,
-> Unit // Not supported for this field
}
}
append("&q=")
if (!filter.query.isNullOrEmpty()) {
append(filter.query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
@ -190,7 +173,7 @@ internal abstract class MTLParser(
val sdf = SimpleDateFormat("dd MMMM yyyy", Locale.ENGLISH)
sdf.timeZone = TimeZone.getTimeZone("UTC")
sdf.parse(dateString)?.time ?: 0L
} catch (e: Exception) {
} catch (_: Exception) {
0L
}
}

@ -1,11 +1,12 @@
package org.koitharu.kotatsu.parsers.site.en.MTL
package org.koitharu.kotatsu.parsers.site.en.mtl
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.site.en.MTL.MTLParser
@Broken
@MangaSourceParser("SNOWMTL", "SnowMTL", "en", type = ContentType.OTHER)
internal class SnowMTL(context: MangaLoaderContext):
MTLParser(context, source = MangaParserSource.SNOWMTL, "snowmtl.ru")
MTLParser(context, source = MangaParserSource.SNOWMTL, "snowmtl.ru")

@ -1,11 +1,12 @@
package org.koitharu.kotatsu.parsers.site.en.MTL
package org.koitharu.kotatsu.parsers.site.en.mtl
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.site.en.MTL.MTLParser
@Broken
@MangaSourceParser("SOLARMTL", "SolarMTL", "en", type = ContentType.MANGA)
internal class SolarMTL(context: MangaLoaderContext):
MTLParser(context, source = MangaParserSource.SOLARMTL, "solarmtl.com")
MTLParser(context, source = MangaParserSource.SOLARMTL, "solarmtl.com")

@ -9,7 +9,7 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@Broken // Website closed
@Broken("Redirect to DragonTranslation.org source")
@MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es")
internal class DragonTranslationParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.DRAGONTRANSLATION, 30) {

@ -0,0 +1,230 @@
package org.koitharu.kotatsu.parsers.site.fr
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.SinglePageMangaParser
import java.util.EnumSet
import java.util.Locale
@Broken
@MangaSourceParser("MANGAMOINS", "MangaMoins", "fr")
internal class MangaMoins(context: MangaLoaderContext) :
SinglePageMangaParser(context, MangaParserSource.MANGAMOINS) {
override val configKeyDomain = ConfigKey.Domain("mangamoins.com")
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val filterCapabilities = MangaListFilterCapabilities()
override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions()
override suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga> {
return listOf(
Manga(
id = generateUid("OP"),
title = "One Piece",
altTitles = emptySet(),
url = "OP",
publicUrl = "https://mangamoins.com/",
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = "https://mangamoins.com/files/scans/OP1161/thumbnail.png",
tags = emptySet(),
state = MangaState.ONGOING,
authors = setOf("Eiichiro Oda"),
source = source,
),
Manga(
id = generateUid("LCDL"),
title = "Les Carnets de l'Apothicaire",
altTitles = emptySet(),
url = "LCDL",
publicUrl = "https://mangamoins.com/",
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = "https://mangamoins.com/files/scans/LCDL76.2/thumbnail.png",
tags = emptySet(),
state = MangaState.ONGOING,
authors = setOf("Itsuki Nanao", "Nekokurage"),
source = source,
),
Manga(
id = generateUid("JKM"),
title = "Jujutsu Kaisen Modulo",
altTitles = emptySet(),
url = "JKM",
publicUrl = "https://mangamoins.com/",
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = "https://mangamoins.com/files/scans/JKM1/thumbnail.png",
tags = emptySet(),
state = MangaState.ONGOING,
authors = setOf("Gege Akutami", "Yuji Iwasaki"),
source = source,
),
Manga(
id = generateUid("OPC"),
title = "One Piece Colo",
altTitles = emptySet(),
url = "OPC",
publicUrl = "https://mangamoins.com/",
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = "https://mangamoins.com/files/scans/OPC1160/thumbnail.png",
tags = emptySet(),
state = MangaState.ONGOING,
authors = setOf("Eiichiro Oda"),
source = source,
),
Manga(
id = generateUid("LDS"),
title = "L'Atelier des Sorciers",
altTitles = emptySet(),
url = "LDS",
publicUrl = "https://mangamoins.com/",
rating = RATING_UNKNOWN,
contentRating = null,
coverUrl = "https://sceneario.com/wp-content/uploads/2023/05/9782811641344-1.jpg",
tags = emptySet(),
state = MangaState.ONGOING,
authors = setOf("Kamome Shirahama"),
source = source,
)
)
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet("https://$domain").parseHtml()
val prefix = manga.url
val latestSiteChapterNumber = doc.select("div.sortie a").mapNotNull { a ->
val href = a.attr("href")
if (href.startsWith("?scan=$prefix")) {
href.substringAfter(prefix).toFloatOrNull()
} else {
null
}
}.maxOrNull()
if (latestSiteChapterNumber == null) return manga
val cachedChapters = manga.chapters
val lastKnownChapterNumber = cachedChapters?.maxByOrNull { it.number }?.number
if (lastKnownChapterNumber != null && cachedChapters.isNotEmpty()) {
// INCREMENTAL SCAN (CACHE EXISTS)
if (latestSiteChapterNumber <= lastKnownChapterNumber) {
return manga.copy(chapters = cachedChapters.sortedBy { it.number })
}
val newChapters = mutableListOf<MangaChapter>()
var currentChapterInt = latestSiteChapterNumber.toInt()
while (currentChapterInt > lastKnownChapterNumber.toInt()) {
val chaptersForGroup = doChecks(prefix, currentChapterInt)
newChapters.addAll(chaptersForGroup)
currentChapterInt--
}
val combinedChapters = (cachedChapters + newChapters).distinctBy { it.number }
return manga.copy(chapters = combinedChapters.sortedBy { it.number })
} else {
// FULL SCAN (NO CACHE)
val allChapters = mutableListOf<MangaChapter>()
var currentChapterInt = latestSiteChapterNumber.toInt()
var misses = 0
while (currentChapterInt >= 1 && misses < 4) {
val chaptersForGroup = doChecks(prefix, currentChapterInt)
if (chaptersForGroup.isNotEmpty()) {
misses = 0
allChapters.addAll(chaptersForGroup)
} else {
misses++
}
currentChapterInt--
}
return manga.copy(chapters = allChapters.sortedBy { it.number })
}
}
private suspend fun doChecks(prefix: String, chapterInt: Int): List<MangaChapter> {
val foundChapters = mutableListOf<MangaChapter>()
// Check for integer chapter
val integerChapter = checkChapter(prefix, chapterInt.toString(), chapterInt.toFloat())
if (integerChapter != null) {
foundChapters.add(integerChapter)
}
// Conditionally and sequentially check for decimal chapters
if (prefix == "LCDL") {
// Start from 2 because .1 never exists
for (i in 2..9) {
val decimalNum = chapterInt + (i / 10.0f)
val decimalNumStr = String.format(Locale.US, "%.1f", decimalNum)
val decimalChapter = checkChapter(prefix, decimalNumStr, decimalNum)
if (decimalChapter != null) {
foundChapters.add(decimalChapter)
} else {
break // Stop if there's a gap
}
}
}
return foundChapters
}
private suspend fun checkChapter(prefix: String, chapterNumStr: String, chapterNumFloat: Float): MangaChapter? {
val thumbUrl = "https://mangamoins.com/files/scans/$prefix$chapterNumStr/thumbnail.png"
return try {
val response = webClient.httpHead(thumbUrl)
if (response.isSuccessful) {
response.close()
val chapterUrl = "https://mangamoins.com/?scan=$prefix$chapterNumStr"
MangaChapter(
id = generateUid(chapterUrl),
title = null,
number = chapterNumFloat,
volume = 0,
url = chapterUrl,
scanlator = null,
uploadDate = 0,
branch = null,
source = source
)
} else {
response.close()
null
}
} catch (_: Exception) {
null
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url).parseHtml()
return doc.select("link[rel=preload][as=image]").map { element ->
val url = element.attr("href").toAbsoluteUrl(domain)
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source
)
}
}
}

@ -6,12 +6,12 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
@Broken // Not dead, changed template
@Broken("Not dead, changed template")
@MangaSourceParser("TEMPLESCAN", "TempleScan", "en")
internal class TempleScan(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.TEMPLESCAN, "templetoons.com") {
override val pathManga = "comic"
override val apiPath: String
get() = "$domain/apiv1"
get() = "$domain/api"
}

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
@Broken // Not dead but changed template
@Broken("Not dead, changed template")
@MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI)
internal class YugenMangasEs(context: MangaLoaderContext) :
HeanCms(context, MangaParserSource.YUGEN_MANGAS_ES, "lectorikigai.acamu.net")

@ -8,7 +8,9 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.heancmsalt.HeanCmsAlt
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("BRAKEOUT", "Brakeout", "es")
internal class Brakeout(context: MangaLoaderContext) :
HeanCmsAlt(context, MangaParserSource.BRAKEOUT, "brakeout.xyz", 10) {

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.heancmsalt.HeanCmsAlt
import org.koitharu.kotatsu.parsers.Broken
@Broken("Not dead, changed template")
@MangaSourceParser("LEGIONSCANS", "CerberusSeries", "es")
internal class CerberuSeries(context: MangaLoaderContext) :
HeanCmsAlt(context, MangaParserSource.LEGIONSCANS, "cerberuseries.xyz")
HeanCmsAlt(context, MangaParserSource.LEGIONSCANS, "legionscans.com")

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.heancmsalt.HeanCmsAlt
@Broken // Not dead but changed template
@Broken("Not dead, changed template")
@MangaSourceParser("MANGAESP", "MangaEsp", "es")
internal class MangaEsp(context: MangaLoaderContext) :
HeanCmsAlt(context, MangaParserSource.MANGAESP, "mangaesp.topmanhuas.org", 15) {

@ -30,6 +30,7 @@ internal class DoujinDesuParser(context: MangaLoaderContext) :
isMultipleTagsSupported = true,
isSearchSupported = true,
isSearchWithFiltersSupported = true,
isAuthorSearchSupported = true,
)
override suspend fun getFilterOptions() = MangaListFilterOptions(
@ -49,17 +50,25 @@ internal class DoujinDesuParser(context: MangaLoaderContext) :
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = urlBuilder().apply {
addPathSegment("manga")
addPathSegment("page")
addPathSegment("$page/")
when {
page > 1 -> addPathSegments("manga/page/$page/")
else -> addPathSegment("manga/")
}
addQueryParameter(
addQueryParameter(
"title",
filter.query?.let {
filter.query
},
)
addQueryParameter(
name = "author",
value = filter.author?.let { it
space2plus(it).lowercase()
}
)
addQueryParameter(
"order",
when (order) {
@ -97,14 +106,6 @@ internal class DoujinDesuParser(context: MangaLoaderContext) :
},
)
}
// Author
// addQueryParameter("author",
// filter.author?.let {
// filter.author
// }
// )
}.build()
return webClient.httpGet(url).parseHtml()
@ -204,4 +205,6 @@ internal class DoujinDesuParser(context: MangaLoaderContext) :
)
}
}
private fun space2plus(input: String): String = input.replace(' ', '+')
}

@ -192,8 +192,8 @@ internal abstract class IkenParser(
val key = it.attrOrNull("value") ?: return@mapNotNullToSet null
MangaTag(
key = key,
title = (it.text() ?: key).toTitleCase(sourceLocale),
source = source,
title = it.text().ifBlank { key }.toTitleCase(sourceLocale),
source = source,
)
}
}
@ -201,7 +201,7 @@ internal abstract class IkenParser(
protected fun Document.getNextJson(key: String): String {
val scripts = select("script")
val scriptData = scripts.find { script ->
script.data()?.contains(key) == true
script.data().contains(key)
}?.data() ?: throw Exception("Unable to retrieve NEXT data")
val keyIndex = scriptData.indexOf(key)

@ -4,9 +4,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.iken.IkenParser
import org.koitharu.kotatsu.parsers.Broken
@Broken("Need to fix getPages")
@MangaSourceParser("HIVECOMIC", "HiveComic", "en")
internal class HiveComic(context: MangaLoaderContext) :
IkenParser(context, MangaParserSource.HIVECOMIC, "hivecomic.com", 18)
IkenParser(context, MangaParserSource.HIVECOMIC, "hivetoons.org", 18, true)

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.iken.IkenParser
@Broken // Redirect to @VORTEXSCANS
@Broken("Redirect to VortexScans")
@MangaSourceParser("MANGAGALAXY", "MangaGalaxy", "en")
internal class MangaGalaxyParser(context: MangaLoaderContext) :
IkenParser(context, MangaParserSource.MANGAGALAXY, "vortexscans.org", 18)
IkenParser(context, MangaParserSource.MANGAGALAXY, "vortexscans.org", 18)

@ -134,6 +134,7 @@ internal abstract class MadaraParser(
"en curso",
"ongoing",
"on going",
"OnGoing",
"ativo",
"en cours",
"en cours \uD83D\uDFE2",
@ -478,7 +479,7 @@ internal abstract class MadaraParser(
return elements.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary")
val author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText()
val author = summary?.selectFirst(".mg_author, .mg_artists")?.selectFirst("a")?.ownText()
Manga(
id = generateUid(href),
url = href,
@ -549,7 +550,7 @@ internal abstract class MadaraParser(
"div.post-content_item:contains(Status), div.post-content_item:contains(Statut), " +
"div.post-content_item:contains(État), div.post-content_item:contains(حالة العمل), div.post-content_item:contains(Estado), div.post-content_item:contains(สถานะ)," +
"div.post-content_item:contains(Stato), div.post-content_item:contains(Durum), div.post-content_item:contains(Statüsü), div.post-content_item:contains(Статус)," +
"div.post-content_item:contains(状态), div.post-content_item:contains(الحالة)"
"div.post-content_item:contains(状态), div.post-content_item:contains(الحالة), div.post-content_item:contains(Tình trạng)"
protected open val selectAlt =
".post-content_item:contains(Alt) .summary-content, .post-content_item:contains(Nomes alternativos: ) .summary-content"
@ -567,7 +568,7 @@ internal abstract class MadaraParser(
val href = doc.selectFirst("head meta[property='og:url']")?.attr("content")?.toRelativeUrl(domain) ?: manga.url
val testCheckAsync = doc.select(selectTestAsync)
val chaptersDeferred = if (testCheckAsync.isNullOrEmpty()) {
val chaptersDeferred = if (testCheckAsync.isEmpty()) {
async { loadChapters(href, doc) }
} else {
async { getChapters(manga, doc) }

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("COMICARAB", "ComicArab", "ar")
internal class ComicArab(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.COMICARAB, "comicarab.com", pageSize = 24) {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("GATEMANGA", "GateManga", "ar")
internal class GateManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.GATEMANGA, "gatemanga.com") {

@ -9,4 +9,8 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
internal class FreeMangaTop(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.FREEMANGATOP, "freemangatop.com") {
override val datePattern = "MM/dd/yyyy"
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.add("Referer", "https://$domain/")
.build()
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @hentai20
@Broken("Redirect to @hentai20")
@MangaSourceParser("HENTAI3Z", "Hentai3z", "en", ContentType.HENTAI)
internal class Hentai3z(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HENTAI3Z, "manga18h.xyz", pageSize = 20) {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @MortalsGroove
@Broken("Redirect to @MortalsGroove")
@MangaSourceParser("IMMORTALUPDATES", "ImmortalUpdates", "en")
internal class ImmortalUpdates(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.IMMORTALUPDATES, "immortalupdates.com") {

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import java.util.*
@Broken // Redirect to @XMANHWA
@Broken("Redirect to @XMANHWA")
@MangaSourceParser("INSTAMANHWA", "InstaManhwa", "en", ContentType.HENTAI)
internal class InstaManhwa(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.INSTAMANHWA, "www.manhwaden.com", 15) {

@ -1,15 +1,90 @@
package org.koitharu.kotatsu.parsers.site.madara.en
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
private const val F_URL = "fullUrl="
@MangaSourceParser("MADARADEX", "MadaraDex", "en", ContentType.HENTAI)
internal class MadaraDex(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MADARADEX, "madaradex.org") {
override val listUrl = "title/"
override val tagPrefix = "genre/"
override val postReq = true
MadaraParser(context, MangaParserSource.MADARADEX, "madaradex.org") {
init {
context.cookieJar.insertCookies(domain, "wpmanga-adault=1")
}
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.remove(userAgentKey)
}
override fun getRequestHeaders() = super.getRequestHeaders().newBuilder()
.set("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
override val authUrl: String
get() = "https://${domain}"
override suspend fun isAuthorized(): Boolean {
return context.cookieJar.getCookies(domain).any {
it.name.contains("cm_uaid")
}
}
override val listUrl = "title/"
override val tagPrefix = "genre/"
override val postReq = true
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val root = doc.body().selectFirst(selectBodyPage)
?: throw ParseException("No image found, try to log in", fullUrl)
return root.select(selectPage).flatMap { div ->
div.selectOrThrow("img").map { img ->
val fragUrl = img.requireSrc().toRelativeUrl(domain).toHttpUrl().newBuilder()
.fragment(F_URL + fullUrl)
.build()
val cleanUrl = fragUrl.newBuilder().fragment(null).build()
MangaPage(
id = generateUid(cleanUrl.toString()),
url = fragUrl.toString(),
preview = null,
source = source,
)
}
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
val fullUrl = url.fragment?.substringAfter(F_URL, "")
return if (!fullUrl.isNullOrEmpty()) {
copyCookies()
val cleanUrl = url.newBuilder().fragment(null).toString()
val newReq = request.newBuilder()
.header("Referer", fullUrl)
.url(cleanUrl)
.build()
chain.proceed(newReq)
} else {
super.intercept(chain)
}
}
private fun copyCookies() = context.cookieJar.copyCookies(domain, "cdn.$domain")
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @hentai20
@Broken("Redirect to @hentai20")
@MangaSourceParser("MANGA18H", "Manga18h", "en", ContentType.HENTAI)
internal class Manga18h(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MANGA18H, "manga18h.xyz", 20)

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @MANGAREAD
@Broken("Redirect to @MANGAREAD")
@MangaSourceParser("MANGAEFFECT", "MangaEffect", "en")
internal class MangaEffect(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MANGAEFFECT, "www.mangaread.org") {

@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("RESETSCANS", "ReadManhua", "en")
@MangaSourceParser("RESETSCANS", "Resetscans", "en")
internal class ResetScans(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.RESETSCANS, "reset-scans.co", 20)
MadaraParser(context, MangaParserSource.RESETSCANS, "reset-scans.org", 20)

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("SCANSRAW", "AquaScans.com", "en")
internal class Scansraw(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.SCANSRAW, "aquascans.com", 10)

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("STKISSMANGABLOG", "1StKissManga.net", "en")
internal class StkissMangaBlog(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.STKISSMANGABLOG, "1stkissmanga.org", 20) {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("STKISSMANGA_COM", "1stKissManga.com", "en")
internal class StkissMangaCom(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.STKISSMANGA_COM, "1stkissmanga.mom", 10) {

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("COCORIP", "Cocorip", "es")
internal class Cocorip(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.COCORIP, "cocorip.net", 16) {

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("DAPROB", "Daprob", "es")
internal class Daprob(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.DAPROB, "daprob.com") {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("DARKNEBULUS", "Darknebulus", "es")
internal class Darknebulus(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.DARKNEBULUS, "www.darknebulus.com")

@ -0,0 +1,12 @@
package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("DRAGONTRANSLATIONORG", "DragonTranslation.org", "es")
internal class DragonTranslationOrg(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.DRAGONTRANSLATIONORG, "dragontranslation.org", 16) {
override val datePattern = "dd/MM/yyyy"
}

@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("EMPERORSCAN", "EmperorScan", "es")
internal class EmperorScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.EMPERORSCAN, "zonaemperor.com")
MadaraParser(context, MangaParserSource.EMPERORSCAN, "emperorscan.mundoalterno.org")

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@Broken("Not dead, changed template")
@MangaSourceParser("HAREMSCANN", "HaremScann", "es")
internal class HaremScann(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HAREMSCANN, "haremscann.es") {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken
@Broken("Not dead, changed template")
@MangaSourceParser("HOUSEMANGAS", "HouseMangas", "es")
internal class HouseMangas(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HOUSEMANGAS, "housemangas.com")
MadaraParser(context, MangaParserSource.HOUSEMANGAS, "visormanga.com")

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("INMORALNOFANSUB", "InmoralNoFansub", "es")
internal class InmoralNoFansub(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.INMORALNOFANSUB, "inmoralnofansub.xyz") {

@ -5,8 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("JEAZTWOBLUESCANS", "Marcialhub", "es")
@MangaSourceParser("JEAZTWOBLUESCANS", "Lector HUB", "es")
internal class JeazTwoBlueScans(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.JEAZTWOBLUESCANS, "marcialhub.xyz") {
MadaraParser(context, MangaParserSource.JEAZTWOBLUESCANS, "lectorhub.j5z.xyz") {
override val datePattern = "d MMMM, yyyy"
}

@ -5,9 +5,9 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("KNIGHTNOSCANLATION", "TwoBlueScans", "es")
@MangaSourceParser("KNIGHTNOSCANLATION", "Lector KNS", "es")
internal class KnightnoScanlation(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.KNIGHTNOSCANLATION, "kns.cookni.net") {
MadaraParser(context, MangaParserSource.KNIGHTNOSCANLATION, "lectorknight.com") {
override val listUrl = "sr/"
override val tagPrefix = "generos/"
}

@ -6,9 +6,9 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Not dead but changed template
@Broken("Not dead, changed template")
@MangaSourceParser("KOINOBORISCAN", "KoinoboriScan", "es")
internal class KoinoboriScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.KOINOBORISCAN, "koinoboriscan.com") {
MadaraParser(context, MangaParserSource.KOINOBORISCAN, "visorkoi.com") {
override val postReq = true
}

@ -1,14 +1,12 @@
package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken
@MangaSourceParser("LEGENDSCANLATIONS", "LegendScanlations", "es")
internal class LegendScanlations(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.LEGENDSCANLATIONS, "legendscanlations.com", 10) {
MadaraParser(context, MangaParserSource.LEGENDSCANLATIONS, "escaneodeleyendas.com", 10) {
override val datePattern = "dd/MM/yyyy"
}

@ -6,9 +6,9 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken
@MangaSourceParser("JOBSIBE", "Jobsibe", "es")
internal class Jobsibe(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.JOBSIBE, "jobsibe.com") {
@Broken("Not dead, changed template")
@MangaSourceParser("LMTOS", "Lmtos", "es")
internal class Lmtos(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.LMTOS, "lmtos.com") {
override val datePattern = "dd/MM"
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGA_CRAB", "MangaCrab", "es")
internal class MangaCrab(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MANGA_CRAB, "mangacrab.topmanhuas.org") {
MadaraParser(context, MangaParserSource.MANGA_CRAB, "mangacrab.org") {
override val datePattern = "dd/MM/yyyy"
override val tagPrefix = "manga-genero/"
override val listUrl = "series/"

@ -1,15 +1,13 @@
package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken
@MangaSourceParser("MANTRAZSCAN", "MantrazScan", "es")
internal class MantrazScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MANTRAZSCAN, "artessupremas.com") {
MadaraParser(context, MangaParserSource.MANTRAZSCAN, "mantrazscan.org") {
override val datePattern = "dd/MM/yyyy"
override val tagPrefix = "generos-de-manga/"
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MHSCANS", "MhScans", "es")
internal class MhScans(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MHSCANS, "twobluescans.com") {
MadaraParser(context, MangaParserSource.MHSCANS, "mhscans.mundoalterno.org") {
override val datePattern = "d 'de' MMMMM 'de' yyyy"
override val listUrl = "series/"
}

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("MMDAOS", "Mmdaos", "es")
internal class Mmdaos(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MMDAOS, "mmdaos.com")

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("MONARCAMANGA", "MonarcaManga", "es")
internal class MonarcaManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.MONARCAMANGA, "visormonarca.com") {

@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("NOBLESSETRANSLATIONS", "NoblesseTranslations", "es")
internal class NoblesseTranslations(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.NOBLESSETRANSLATIONS, "swordalada.org")
MadaraParser(context, MangaParserSource.NOBLESSETRANSLATIONS, "nobledicion.yoveo.xyz")

@ -9,8 +9,8 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken
@MangaSourceParser("RAGNAROKSCAN", "RagnarokScan", "es")
internal class RagnarokScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.RAGNAROKSCAN, "ragnarokscan.com") {
override val stylePage = ""
override val listUrl = "series/"
override val tagPrefix = "genero/"
MadaraParser(context, MangaParserSource.RAGNAROKSCAN, "ragnarokscan.com") {
override val stylePage = ""
override val listUrl = "series/"
override val tagPrefix = "genero/"
}

@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("RAGNAROKSCANLATION", "RagnarokScanlation", "es")
internal class RagnarokScanlation(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.RAGNAROKSCANLATION, "ragnarokscanlation.net")
MadaraParser(context, MangaParserSource.RAGNAROKSCANLATION, "ragnarokscanlation.org")

@ -8,7 +8,7 @@ import java.util.*
@MangaSourceParser("RICHTOSCAN", "RichtoScan", "es")
internal class RichtoScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.RICHTOSCAN, "richtoscan.com") {
MadaraParser(context, MangaParserSource.RICHTOSCAN, "r1.richtoon.top") {
override val tagPrefix = "manga-generos/"
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("SAMURAISCAN", "SamuraiScan", "es")
internal class SamuraiScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.SAMURAISCAN, "latan.visorsmr.com", 10) {
MadaraParser(context, MangaParserSource.SAMURAISCAN, "samuraiscan.com", 10) {
override val listUrl = "read/"
}

@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("TAURUSMANGA", "TaurusManga", "es")
internal class TaurusManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.TAURUSMANGA, "taurusmanga.com") {
MadaraParser(context, MangaParserSource.TAURUSMANGA, "taurus.topmanhuas.org") {
override val datePattern = "dd/MM/yyyy"
}

@ -4,9 +4,11 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("TECNOPROJECTS", "TecnoProjects", "es")
internal class TecnoProjects(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.TECNOPROJECTS, "tecnoprojects.com") {
MadaraParser(context, MangaParserSource.TECNOPROJECTS, "tecnoprojects.xyz") {
override val datePattern = "dd 'de' MMMM 'de' yyyy"
}

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.Broken
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
@ -7,6 +8,7 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
@Broken
@MangaSourceParser("TMOMANGA", "TmoManga", "es")
internal class TmoManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.TMOMANGA, "tmomanga.com") {

@ -4,7 +4,9 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken
@MangaSourceParser("ZEVEP", "Zevep", "es")
internal class Zevep(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.ZEVEP, "zevep.com", 16)

@ -8,4 +8,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("SCANHENTAIMENU", "ScanHentai.Menu", "fr", ContentType.HENTAI)
internal class ScanHentaiMenu(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.SCANHENTAIMENU, "x-manga.net")
MadaraParser(context, MangaParserSource.SCANHENTAIMENU, "x-manga.org")

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Not dead, changed template
@Broken("Not dead, changed template")
@MangaSourceParser("CERISE_SCANS", "CeriseScans", "pt")
internal class CeriseScans(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.CERISE_SCANS, "cerise.leitorweb.com") {

@ -8,4 +8,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("CVNSCAN", "CvnScan", "pt", ContentType.HENTAI)
internal class CvnScan(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.CVNSCAN, "cvnscan.com")
MadaraParser(context, MangaParserSource.CVNSCAN, "covendasbruxonas.com")

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed
@Broken("Original site closed")
@MangaSourceParser("GALINHASAMURAI", "GalinhaSamurai", "pt")
internal class GalinhaSamurai(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.GALINHASAMURAI, "galinhasamurai.com") {

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.Broken
@Broken // Website closed and the domain has been sold
@Broken("Original site closed")
@MangaSourceParser("GOOFFANSUB", "GoofFansub", "pt", ContentType.HENTAI)
internal class GoofFansub(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.GOOFFANSUB, "gooffansub.com") {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Not dead, changed template
@Broken("Not dead, changed template")
@MangaSourceParser("SINENSISSCANS", "SinensisScans", "pt")
internal class SinensisScans(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.SINENSISSCANS, "sinensis.leitorweb.com") {

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @KINGS_MANGA
@Broken("Redirect to @KINGS_MANGA")
@MangaSourceParser("NEKOPOST", "NekoPost", "th", ContentType.HENTAI)
internal class NekoPost(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.NEKOPOST, "www.superdoujin.org") {

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @WEBTOONHATTI
@Broken("Redirect to @WEBTOONHATTI")
@MangaSourceParser("CLOVERMANGA", "CloverManga", "tr")
internal class CloverManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.CLOVERMANGA, "webtoonhatti.me", 20)

@ -6,8 +6,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @GRIMELEK
@Broken("Redirect to @GRIMELEK")
@MangaSourceParser("GHOSTFANSUB", "GhostFansub", "tr")
internal class GhostFansub(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.GHOSTFANSUB, "ghostfansub.co", 18)
// you now need to log in to access content
// you now need to log in to access content

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@Broken // Redirect to @MANGAGEZGINI
@Broken("Redirect to @MANGAGEZGINI")
@MangaSourceParser("GLORYMANGA", "GloryManga", "tr")
internal class GloryManga(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.GLORYMANGA, "mangagezgini.site", 18) {

@ -16,7 +16,7 @@ import org.koitharu.kotatsu.parsers.util.urlEncoded
@MangaSourceParser("HENTAIVNPLUS", "HentaiVN.plus", "vi", ContentType.HENTAI)
internal class HentaiVnPlus(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.HENTAIVNPLUS, "hentaivn.cx", 24) {
MadaraParser(context, MangaParserSource.HENTAIVNPLUS, "hentaivn.party", 24) {
override val listUrl = "truyen-hentai/"
override val tagPrefix = "the-loai/"
override val datePattern = "dd/MM/yyyy"

@ -9,7 +9,6 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
import org.koitharu.kotatsu.parsers.Broken
@Broken
@ -26,7 +25,7 @@ internal class HentaiZ(context: MangaLoaderContext) :
val href = doc.selectFirst("head meta[property='og:url']")?.attr("content")?.toRelativeUrl(domain) ?: manga.url
val testCheckAsync = doc.select(selectTestAsync)
val chaptersDeferred = if (testCheckAsync.isNullOrEmpty()) {
val chaptersDeferred = if (testCheckAsync.isEmpty()) {
async { loadChapters(href, doc) }
} else {
async { getChapters(manga, doc) }

@ -11,7 +11,7 @@ import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("QUAANHDAOCUTEO", "Quả Anh Đào Cuteo", "vi", ContentType.HENTAI)
internal class Quaanhdaocuteo(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.QUAANHDAOCUTEO, "qadcuteo.org") {
MadaraParser(context, MangaParserSource.QUAANHDAOCUTEO, "qadcuteo.cc") {
override val datePattern = "dd/MM/yyyy"
override val selectPage = "p img"

@ -0,0 +1,14 @@
package org.koitharu.kotatsu.parsers.site.madara.vi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("TRUYENTRANHFULL", "Truyện Tranh Full", "vi")
internal class TruyenTranhFull(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.TRUYENTRANHFULL, "truyentranhfull.net", 20) {
override val listUrl = "truyen-tranh/"
override val tagPrefix = "the-loai/"
override val datePattern = "dd/MM/yyyy"
}

@ -7,6 +7,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import java.text.SimpleDateFormat
@ -141,7 +142,7 @@ internal abstract class MadthemeParser(
private suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select("div.genres .checkbox").mapNotNullToSet { checkbox ->
val key = checkbox.selectFirstOrThrow("input").attr("value") ?: return@mapNotNullToSet null
val key = checkbox.selectFirstOrThrow("input").attr("value")
val name = checkbox.selectFirst("span.radio__label")?.text() ?: key
MangaTag(
key = key,
@ -190,7 +191,7 @@ internal abstract class MadthemeParser(
altTitles = setOfNotNull(alt.nullIfEmpty()),
state = state,
chapters = chaptersDeferred.await(),
contentRating = if (nsfw || manga.isNsfw) {
contentRating = if (nsfw || manga.contentRating == ContentRating.ADULT) {
ContentRating.ADULT
} else {
ContentRating.SAFE

@ -271,7 +271,7 @@ internal abstract class MangaboxParser(
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
if (doc.select(selectPage).isNullOrEmpty()) {
if (doc.select(selectPage).isEmpty()) {
val fullUrl2 = chapter.url.toAbsoluteUrl(domain).replace(domain, otherDomain)
val doc2 = webClient.httpGet(fullUrl2).parseHtml()

@ -19,12 +19,12 @@ import java.util.*
@MangaSourceParser("MANGAKAKALOT", "Mangakakalot.gg", "en")
internal class Mangakakalot(context: MangaLoaderContext) : MangaboxParser(context, MangaParserSource.MANGAKAKALOT) {
override val configKeyDomain = ConfigKey.Domain(
"www.mangakakalot.gg",
"mangakakalot.gg",
)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
@ -132,7 +132,7 @@ internal class Mangakakalot(context: MangaLoaderContext) : MangaboxParser(contex
val doc = webClient.httpGet(titleSearchUrl ?: url).parseHtml()
return doc.select("div.list-truyen-item-wrap").ifEmpty {
return doc.select("div.list-comic-item-wrap").ifEmpty {
doc.select("div.story_item")
}.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.parsers.site.mangabox.en
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
@ -17,6 +18,7 @@ import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@Broken("Connection refused")
@MangaSourceParser("MANGAKAKALOTTV", "Mangakakalot.tv", "en")
internal class MangakakalotTv(context: MangaLoaderContext) :
MangaboxParser(context, MangaParserSource.MANGAKAKALOTTV) {
@ -119,7 +121,7 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
val doc = webClient.httpGet(titleSearchUrl ?: url).parseHtml()
return doc.select("div.list-truyen-item-wrap").ifEmpty {
return doc.select("div.list-comic-item-wrap").ifEmpty {
doc.select("div.story_item")
}.map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
@ -128,7 +130,7 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img")?.src(),
title = div.selectFirstOrThrow("h3").text().orEmpty(),
title = div.selectFirstOrThrow("h3").text(),
altTitles = emptySet(),
rating = RATING_UNKNOWN,
tags = emptySet(),

@ -4,6 +4,7 @@ import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@ -111,7 +112,7 @@ internal class Normoyun(context: MangaLoaderContext) :
description = docs.selectFirst("span.desc")?.html(),
state = mangaState,
authors = setOfNotNull(author),
contentRating = if (manga.isNsfw || nsfw) {
contentRating = if (manga.contentRating == ContentRating.ADULT || nsfw) {
ContentRating.ADULT
} else {
ContentRating.SAFE

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save