Merge pull request #31 from KotatsuApp/master

[pull] master from KotatsuApp:master
master
Naga 2 years ago committed by GitHub
commit 32eab42c26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -53,7 +53,7 @@ This library provides manga sources.
3. Usage in code
```kotlin
val parser = mangaLoaderContext.newParserInstance(MangaSource.MANGADEX)
val parser = mangaLoaderContext.newParserInstance(MangaParserSourceMANGADEX)
```
`mangaLoaderContext` is an implementation of the `MangaLoaderContext` class.
@ -62,7 +62,7 @@ This library provides manga sources.
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/jvmMain/kotlin/org/koitharu/kotatsu_dl/logic/MangaLoaderContextImpl.kt)
implementation.
Note that the `MangaSource.LOCAL` and `MangaSource.DUMMY` parsers cannot be instantiated.
Note that the `MangaParserSourceLOCAL` and `MangaParserSourceDUMMY` parsers cannot be instantiated.
## Contribution

@ -2,8 +2,8 @@ import tasks.ReportGenerateTask
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm' version '1.9.23'
id 'com.google.devtools.ksp' version '1.9.23-1.0.20'
id 'org.jetbrains.kotlin.jvm' version '2.0.10-RC'
id 'com.google.devtools.ksp' version '2.0.10-RC-1.0.23'
id 'maven-publish'
}
@ -53,12 +53,12 @@ afterEvaluate {
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okio:okio:3.9.0'
api 'org.jsoup:jsoup:1.17.2'
api 'org.jsoup:jsoup:1.18.1'
implementation 'org.json:json:20240303'
implementation 'androidx.collection:collection:1.4.0'
implementation 'androidx.collection:collection:1.4.1'
ksp project(':kotatsu-parsers-ksp')

@ -1,5 +1,5 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.9.23'
id 'org.jetbrains.kotlin.jvm' version '2.0.10-RC'
}
repositories {
@ -14,5 +14,5 @@ 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.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC'
}

@ -7,5 +7,5 @@ kotlin {
}
dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20'
implementation 'com.google.devtools.ksp:symbol-processing-api:2.0.10-RC-1.0.23'
}

@ -69,12 +69,9 @@ class ParserProcessor(
"""
package org.koitharu.kotatsu.parsers
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
@Suppress("DEPRECATION")
@InternalParsersApi
@Deprecated("", replaceWith = ReplaceWith("context.newParserInstance(this)"))
fun MangaSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
internal fun MangaParserSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
""".trimIndent(),
)
@ -83,14 +80,12 @@ class ParserProcessor(
"""
package org.koitharu.kotatsu.parsers.model
enum class MangaSource(
enum class MangaParserSource(
val title: String,
val locale: String,
val contentType: ContentType,
val isBroken: Boolean,
) {
LOCAL("Local", "", ContentType.OTHER, false),
UNKNOWN("Unknown", "", ContentType.OTHER, true),
): MangaSource {
""".trimIndent(),
)
@ -102,9 +97,7 @@ class ParserProcessor(
factoryWriter?.write(
"""
MangaSource.LOCAL,
MangaSource.UNKNOWN,
MangaSource.DUMMY -> throw NotImplementedError("Manga parser ${'$'}name cannot be instantiated")
MangaParserSource.DUMMY -> throw NotImplementedError("Manga parser ${'$'}name cannot be instantiated")
}.also {
require(it.source == this) {
"Cannot instantiate manga parser: ${'$'}name mapped to ${'$'}{it.source}"
@ -166,7 +159,7 @@ class ParserProcessor(
logger.warn("Source title duplication: \"$title\" is assigned to both $prevTitleClass and $className")
}
factoryWriter?.write("\tMangaSource.$name -> $className(context)\n")
factoryWriter?.write("\tMangaParserSource.$name -> $className(context)\n")
val deprecationString =
if (deprecation != null) {
val reason =

@ -5,6 +5,7 @@ import okhttp3.OkHttpClient
import okhttp3.Response
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.*
@ -14,8 +15,7 @@ abstract class MangaLoaderContext {
abstract val cookieJar: CookieJar
@Suppress("DEPRECATION")
fun newParserInstance(source: MangaSource): MangaParser = source.newParser(this)
fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this)
open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
@ -42,7 +42,7 @@ abstract class MangaLoaderContext {
*/
abstract fun redrawImageResponse(
response: Response,
redraw: (image: Bitmap) -> Bitmap
redraw: (image: Bitmap) -> Bitmap,
): Response
/**
@ -50,6 +50,6 @@ abstract class MangaLoaderContext {
*/
abstract fun createBitmap(
width: Int,
height: Int
height: Int,
): Bitmap
}

@ -15,7 +15,7 @@ import java.util.*
abstract class MangaParser @InternalParsersApi constructor(
@property:InternalParsersApi val context: MangaLoaderContext,
val source: MangaSource,
val source: MangaParserSource,
) {
/**
@ -62,7 +62,7 @@ abstract class MangaParser @InternalParsersApi constructor(
val config by lazy { context.getConfig(source) }
open val sourceLocale: Locale
get() = source.locale?.let { Locale(it) } ?: Locale.ROOT
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
val isNsfwSource = source.contentType == ContentType.HENTAI
@ -244,7 +244,7 @@ abstract class MangaParser @InternalParsersApi constructor(
return RelatedMangaFinder(listOf(this)).invoke(seed)
}
protected fun getParser(source: MangaSource) = if (this.source == source) {
protected fun getParser(source: MangaParserSource) = if (this.source == source) {
this
} else {
context.newParserInstance(source)

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.util.Paginator
@InternalParsersApi
abstract class PagedMangaParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField internal val pageSize: Int,
searchPageSize: Int = pageSize,
) : MangaParser(context, source) {

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.parsers.model
class Favicons internal constructor(
favicons: Collection<Favicon>,
@JvmField val referer: String,
@JvmField val referer: String?,
) : Collection<Favicon> {
private val icons = favicons.sortedDescending()
@ -47,4 +47,12 @@ class Favicons internal constructor(
}
return result
}
companion object {
@JvmStatic
fun empty() = Favicons(emptySet(), null)
fun single(url: String) = Favicons(setOf(Favicon(url, 0, null)), null)
}
}

@ -0,0 +1,6 @@
package org.koitharu.kotatsu.parsers.model
interface MangaSource {
val name: String
}

@ -21,7 +21,7 @@ import javax.crypto.spec.SecretKeySpec
@MangaSourceParser("BATOTO", "Bato.To")
internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
context = context,
source = MangaSource.BATOTO,
source = MangaParserSource.BATOTO,
pageSize = 60,
searchPageSize = 20,
) {
@ -104,7 +104,12 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
filter.locale?.let {
append("&langs=")
append(it.language)
if (it.language == "in") {
append("id")
} else {
append(it.language)
}
}
append("&genres=")

@ -21,7 +21,8 @@ import java.util.*
private const val CHAPTERS_LIMIT = 99999
@MangaSourceParser("COMICK_FUN", "ComicK")
internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.COMICK_FUN, 20) {
internal class ComickFunParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.COMICK_FUN, 20) {
override val configKeyDomain = ConfigKey.Domain("comick.io", "comick.cc")

@ -24,7 +24,7 @@ private const val DOMAIN_AUTHORIZED = "exhentai.org"
@MangaSourceParser("EXHENTAI", "ExHentai", type = ContentType.HENTAI)
internal class ExHentaiParser(
context: MangaLoaderContext,
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
) : PagedMangaParser(context, MangaParserSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val isTagsExclusionSupported: Boolean = true

@ -27,7 +27,7 @@ import kotlin.math.min
@OptIn(ExperimentalUnsignedTypes::class)
@MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI)
class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.HITOMILA) {
class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.HITOMILA) {
override val configKeyDomain = ConfigKey.Domain("hitomi.la")
private val ltnBaseUrl get() = "https://${getDomain("ltn")}"

@ -15,7 +15,7 @@ import java.util.*
@MangaSourceParser("IMHENTAI", "ImHentai", type = ContentType.HENTAI)
internal class ImHentai(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.IMHENTAI, pageSize = 20) {
PagedMangaParser(context, MangaParserSource.IMHENTAI, pageSize = 20) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING)

@ -21,7 +21,7 @@ import javax.crypto.spec.SecretKeySpec
internal abstract class LineWebtoonsParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
) : MangaParser(context, source) {
override val isMultipleTagsSupported = false
@ -308,25 +308,25 @@ internal abstract class LineWebtoonsParser(
}
@MangaSourceParser("LINEWEBTOONS_EN", "LineWebtoons English", "en", type = ContentType.MANGA)
class English(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_EN)
class English(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_EN)
@MangaSourceParser("LINEWEBTOONS_ZH", "LineWebtoons Chinese", "zh", type = ContentType.MANGA)
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ZH)
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_ZH)
@MangaSourceParser("LINEWEBTOONS_TH", "LineWebtoons Thai", "th", type = ContentType.MANGA)
class Thai(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_TH)
class Thai(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_TH)
@MangaSourceParser("LINEWEBTOONS_ID", "LineWebtoons Indonesian", "id", type = ContentType.MANGA)
class Indonesian(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ID)
class Indonesian(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_ID)
@MangaSourceParser("LINEWEBTOONS_ES", "LineWebtoons Spanish", "es", type = ContentType.MANGA)
class Spanish(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ES)
class Spanish(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_ES)
@MangaSourceParser("LINEWEBTOONS_FR", "LineWebtoons French", "fr", type = ContentType.MANGA)
class French(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_FR)
class French(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_FR)
@MangaSourceParser("LINEWEBTOONS_DE", "LineWebtoons German", "de", type = ContentType.MANGA)
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_DE)
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.LINEWEBTOONS_DE)
private inner class WebtoonsUrlSigner(private val secret: String) {

@ -24,7 +24,7 @@ private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks lik
private const val LOCALE_FALLBACK = "en"
@MangaSourceParser("MANGADEX", "MangaDex")
internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.MANGADEX) {
internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.MANGADEX) {
override val configKeyDomain = ConfigKey.Domain("mangadex.org")

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.parsers.site.all
import kotlinx.coroutines.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
@ -22,7 +24,7 @@ private const val MIN_SPLIT_COUNT = 5
internal abstract class MangaFireParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
private val siteLang: String,
) : PagedMangaParser(context, source, 30), Interceptor {
@ -425,23 +427,25 @@ internal abstract class MangaFireParser(
private fun Int.ceilDiv(other: Int) = (this + (other - 1)) / other
@MangaSourceParser("MANGAFIRE_EN", "MangaFire English", "en")
class English(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_EN, "en")
class English(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_EN, "en")
@MangaSourceParser("MANGAFIRE_ES", "MangaFire Spanish", "es")
class Spanish(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_ES, "es")
class Spanish(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_ES, "es")
@MangaSourceParser("MANGAFIRE_ESLA", "MangaFire Spanish (Latim)", "es")
class SpanishLatim(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_ESLA, "es-la")
class SpanishLatim(context: MangaLoaderContext) :
MangaFireParser(context, MangaParserSource.MANGAFIRE_ESLA, "es-la")
@MangaSourceParser("MANGAFIRE_FR", "MangaFire French", "fr")
class French(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_FR, "fr")
class French(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_FR, "fr")
@MangaSourceParser("MANGAFIRE_JA", "MangaFire Japanese", "ja")
class Japanese(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_JA, "ja")
class Japanese(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_JA, "ja")
@MangaSourceParser("MANGAFIRE_PT", "MangaFire Portuguese", "pt")
class Portuguese(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_PT, "pt")
class Portuguese(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_PT, "pt")
@MangaSourceParser("MANGAFIRE_PTBR", "MangaFire Portuguese (Brazil)", "pt")
class PortugueseBR(context: MangaLoaderContext) : MangaFireParser(context, MangaSource.MANGAFIRE_PTBR, "pt-br")
class PortugueseBR(context: MangaLoaderContext) :
MangaFireParser(context, MangaParserSource.MANGAFIRE_PTBR, "pt-br")
}

@ -14,7 +14,7 @@ import java.util.*
@MangaSourceParser("MANGAPARK", "MangaPark")
internal class MangaPark(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANGAPARK, pageSize = 36) {
PagedMangaParser(context, MangaParserSource.MANGAPARK, pageSize = 36) {
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
@ -257,7 +257,7 @@ internal class MangaPark(context: MangaLoaderContext) :
.findAll(script)
.mapNotNullTo(ArrayList()) {
val url = it.groupValues.getOrNull(1) ?: return@mapNotNullTo null
if (url.contains("/comic/") || url.contains("/manga/")) {
if (url.contains("/comic/") || url.contains("/manga/") || url.contains("/image/mpup/")) {
MangaPage(
id = generateUid(url),
url = url,

@ -22,7 +22,7 @@ import java.util.*
internal abstract class MangaPlusParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
private val sourceLang: String,
) : MangaParser(context, source), Interceptor {
@ -267,63 +267,63 @@ internal abstract class MangaPlusParser(
@MangaSourceParser("MANGAPLUSPARSER_EN", "MANGA Plus English", "en")
class English(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_EN,
MangaParserSource.MANGAPLUSPARSER_EN,
"ENGLISH",
)
@MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus Spanish", "es")
class Spanish(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_ES,
MangaParserSource.MANGAPLUSPARSER_ES,
"SPANISH",
)
@MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus French", "fr")
class French(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_FR,
MangaParserSource.MANGAPLUSPARSER_FR,
"FRENCH",
)
@MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus Indonesian", "id")
class Indonesian(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_ID,
MangaParserSource.MANGAPLUSPARSER_ID,
"INDONESIAN",
)
@MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt")
class Portuguese(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_PTBR,
MangaParserSource.MANGAPLUSPARSER_PTBR,
"PORTUGUESE_BR",
)
@MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus Russian", "ru")
class Russian(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_RU,
MangaParserSource.MANGAPLUSPARSER_RU,
"RUSSIAN",
)
@MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus Thai", "th")
class Thai(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_TH,
MangaParserSource.MANGAPLUSPARSER_TH,
"THAI",
)
@MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus Vietnamese", "vi")
class Vietnamese(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_VI,
MangaParserSource.MANGAPLUSPARSER_VI,
"VIETNAMESE",
)
@MangaSourceParser("MANGAPLUSPARSER_DE", "MANGA Plus German", "de")
class German(context: MangaLoaderContext) : MangaPlusParser(
context,
MangaSource.MANGAPLUSPARSER_DE,
MangaParserSource.MANGAPLUSPARSER_DE,
"GERMAN",
)
}

@ -20,7 +20,7 @@ import javax.crypto.spec.SecretKeySpec
import kotlin.math.min
@MangaSourceParser("MANGAREADERTO", "MangaReader.To")
class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGAREADERTO, 16),
class MangaReaderToParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGAREADERTO, 16),
Interceptor {
override val configKeyDomain = ConfigKey.Domain("mangareader.to")

@ -17,7 +17,7 @@ import java.util.*
internal abstract class NineMangaParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
defaultDomain: String,
) : PagedMangaParser(context, source, pageSize = 26), Interceptor {
@ -267,49 +267,49 @@ internal abstract class NineMangaParser(
@MangaSourceParser("NINEMANGA_EN", "NineManga English", "en")
class English(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_EN,
MangaParserSource.NINEMANGA_EN,
"www.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_ES", "NineManga Español", "es")
class Spanish(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_ES,
MangaParserSource.NINEMANGA_ES,
"es.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_RU", "NineManga Русский", "ru")
class Russian(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_RU,
MangaParserSource.NINEMANGA_RU,
"ru.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_DE", "NineManga Deutsch", "de")
class Deutsch(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_DE,
MangaParserSource.NINEMANGA_DE,
"de.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_BR", "NineManga Brasil", "pt")
class Brazil(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_BR,
MangaParserSource.NINEMANGA_BR,
"br.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_IT", "NineManga Italiano", "it")
class Italiano(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_IT,
MangaParserSource.NINEMANGA_IT,
"it.ninemanga.com",
)
@MangaSourceParser("NINEMANGA_FR", "NineManga Français", "fr")
class Francais(context: MangaLoaderContext) : NineMangaParser(
context,
MangaSource.NINEMANGA_FR,
MangaParserSource.NINEMANGA_FR,
"fr.ninemanga.com",
)
}

@ -18,11 +18,11 @@ import org.koitharu.kotatsu.parsers.util.json.toJSONList
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("NINENINENINEHENTAI", "999Hentai", type = ContentType.HENTAI)
@MangaSourceParser("NINENINENINEHENTAI", "AnimeH", type = ContentType.HENTAI)
internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.NINENINENINEHENTAI, size), Interceptor {
PagedMangaParser(context, MangaParserSource.NINENINENINEHENTAI, size), Interceptor {
override val configKeyDomain = ConfigKey.Domain("999hentai.net")
override val configKeyDomain = ConfigKey.Domain("animeh.to")
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
@ -377,7 +377,7 @@ internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
}
private suspend fun apiCall(query: String): JSONObject {
return webClient.graphQLQuery("https://hapi.$domain/api", query).getJSONObject("data")
return webClient.graphQLQuery("https://api.$domain/api", query).getJSONObject("data")
}
companion object {

@ -14,39 +14,16 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.ContentType
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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
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.SoftSuspendLazy
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.splitTwoParts
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.urlEncoded
import java.util.Calendar
import java.util.EnumSet
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
internal abstract class WebtoonsParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
) : MangaParser(context, source) {
override val isMultipleTagsSupported = false
@ -307,25 +284,25 @@ internal abstract class WebtoonsParser(
}
@MangaSourceParser("WEBTOONS_EN", "Webtoons English", "en", type = ContentType.MANGA)
class English(context: MangaLoaderContext) : WebtoonsParser(context, MangaSource.WEBTOONS_EN)
class English(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_EN)
@MangaSourceParser("WEBTOONS_ID", "Webtoons Indonesia", "id", type = ContentType.MANGA)
class Indonesian(context: MangaLoaderContext) : WebtoonsParser(context, MangaSource.WEBTOONS_ID)
class Indonesian(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_ID)
@MangaSourceParser("WEBTOONS_ES", "Webtoons Spanish", "es", type = ContentType.MANGA)
class Spanish(context: MangaLoaderContext) : WebtoonsParser(context, MangaSource.WEBTOONS_ES)
class Spanish(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_ES)
@MangaSourceParser("WEBTOONS_FR", "Webtoons French", "fr", type = ContentType.MANGA)
class French(context: MangaLoaderContext) : WebtoonsParser(context, MangaSource.WEBTOONS_FR)
class French(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_FR)
@MangaSourceParser("WEBTOONS_TH", "Webtoons Thai", "th", type = ContentType.MANGA)
class Thai(context: MangaLoaderContext) : WebtoonsParser(context, MangaSource.WEBTOONS_TH)
class Thai(context: MangaLoaderContext) : WebtoonsParser(context, MangaParserSource.WEBTOONS_TH)
@MangaSourceParser("WEBTOONS_ZH", "Webtoons Chinese", "zh", type = ContentType.MANGA)
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.WEBTOONS_ZH)
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.WEBTOONS_ZH)
@MangaSourceParser("WEBTOONS_DE", "Webtoons German", "de", type = ContentType.MANGA)
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.WEBTOONS_DE)
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaParserSource.WEBTOONS_DE)
private inner class WebtoonsUrlSigner(private val secret: String) {

@ -13,7 +13,7 @@ import java.util.*
internal abstract class AnimeBootstrapParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
domain: String,
pageSize: Int = 24,
) : PagedMangaParser(context, source, pageSize) {

@ -10,13 +10,12 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
import java.util.*
@Broken
@MangaSourceParser("PAPSCAN", "PapScan", "fr")
internal class PapScan(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") {
AnimeBootstrapParser(context, MangaParserSource.PAPSCAN, "papscan.com") {
override val sourceLocale: Locale = Locale.ENGLISH
override val isMultipleTagsSupported = false
override val listUrl = "/liste-manga"

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("KOMIKZOID", "KomikzoId", "id")
internal class KomikzoId(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.id")
AnimeBootstrapParser(context, MangaParserSource.KOMIKZOID, "komikzoid.id")

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("NEUMANGA", "NeuManga.xyz", "id")
internal class NeuManga(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.NEUMANGA, "neumanga.xyz")
AnimeBootstrapParser(context, MangaParserSource.NEUMANGA, "neumanga.xyz")

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.animebootstrap.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("SEKTEKOMIK", "SekteKomik", "id")
internal class SekteKomik(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.SEKTEKOMIK, "sektekomik.xyz")
AnimeBootstrapParser(context, MangaParserSource.SEKTEKOMIK, "sektekomik.xyz")

@ -16,7 +16,7 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("FLIXSCANS", "FlixScans.net", "ar")
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) {
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.FLIXSCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)

@ -11,7 +11,7 @@ import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("MANGASTORM", "MangaStorm", "ar")
internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGASTORM, 30) {
internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGASTORM, 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("mangastorm.org")

@ -14,13 +14,13 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("TEAMXNOVEL", "TeamXNovel", "ar")
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) {
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.TEAMXNOVEL, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("teamxnovel.com")
override val configKeyDomain = ConfigKey.Domain("teamoney.site")
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
@ -31,7 +31,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
when (filter) {
is MangaListFilter.Search -> {
append("/series?search=")
append("/?search=")
append(filter.query.urlEncoded())
if (page > 1) {
append("&page=")
@ -95,7 +95,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").src().orEmpty(),
coverUrl = div.selectFirstOrThrow("img").src()?.replace("thumbnail_", "").orEmpty(),
tags = emptySet(),
state = when (div.selectFirst(".status")?.text()) {
"مستمرة" -> MangaState.ONGOING

@ -21,7 +21,7 @@ import java.util.*
@Broken
@MangaSourceParser("ANIBEL", "Anibel", "be")
internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.ANIBEL) {
internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.ANIBEL) {
override val configKeyDomain = ConfigKey.Domain("anibel.net")

@ -0,0 +1,202 @@
package org.koitharu.kotatsu.parsers.site.cupfox
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
internal abstract class CupFoxParser(
context: MangaLoaderContext,
source: MangaParserSource,
domain: String,
pageSize: Int = 24,
) : PagedMangaParser(context, source, pageSize) {
override val configKeyDomain = ConfigKey.Domain(domain)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,
SortOrder.UPDATED,
)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
append("/search/")
append(filter.query.urlEncoded())
append('/')
append(page)
}
is MangaListFilter.Advanced -> {
append("/category/")
when (filter.sortOrder) {
SortOrder.POPULARITY -> append("order/hits/")
SortOrder.UPDATED -> append("order/addtime/")
else -> append("order/addtime/")
}
filter.states.oneOrThrowIfMany()?.let {
append(
when (it) {
MangaState.ONGOING -> "finish/1/"
MangaState.FINISHED -> "finish/2/"
else -> ""
},
)
}
if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let {
append("tags/")
append(it.key)
append('/')
}
}
append("page/")
append(page)
}
null -> {
append("/category/order/addtime/page/")
append(page)
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open val selectMangas =
"ul.row li, ul.stui-vodlist li, ul.clearfix li.dm-list, div.vod-list ul.row li, ul.ewave-vodlist li"
protected open val selectMangasCover =
"div.img-wrapper, div.stui-vodlist__thumb, a.stui-vodlist__thumb, div.ewave-vodlist__thumb, img.dm-thumb"
private fun parseMangaList(doc: Document): List<Manga> {
return doc.select(selectMangas).map { li ->
val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = li.selectFirst("h3, h4, p.dm-bn")?.text().orEmpty(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = li.selectFirst(selectMangasCover)
?.src().orEmpty(),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
protected open val selectMangaDetailsAltTitle =
"div.info span:contains(Autres noms), div.info span:contains(Biệt danh)"
protected open val selectMangaDetailsTags =
"div.info span a[href*=tags], p.data a[href*=tags], div.book-main-right p.info-text a[href*=tags]"
protected open val selectMangaDetailsAuthor =
"div.info span:contains(Auteur(s)), div.info span:contains(Tác giả), p.data span:contains(Auteur(s)), p.data span:contains(Autor), p.data span:contains(作者), div.book-main-right div.book-info:contains(作者) .info-text"
protected open val selectMangaDescription =
"div.vod-list:contains(Résumé) div.more-box, div.stui-pannel__head:contains(Résumé), div.book-desc div.info-text, div.info div.text:contains(Giới thiệu), #desc"
protected open val selectMangaChapters =
"div.episode-box ul li, ul.stui-content__playlist li a, ul.cnxh-ul li a, ul.ewave-content__playlist li a"
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
return manga.copy(
altTitle = doc.selectFirst(selectMangaDetailsAltTitle)?.text()?.substringAfter(""),
state = null,
tags = doc.select(selectMangaDetailsTags).mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
title = a.text().toTitleCase(),
source = source,
)
},
author = doc.selectFirst(selectMangaDetailsAuthor)?.text()?.substringAfter(""),
description = doc.selectFirst(selectMangaDescription)
?.html(),
chapters = doc.select(selectMangaChapters)
.mapChapters { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1f,
volume = 0,
url = href,
scanlator = null,
uploadDate = 0L,
branch = null,
source = source,
)
},
)
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val doc = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml()
return doc.select("div.vod-list div.more-box li, ul.stui-vodlist__bd li, ul.about-yxul li, ul.ewave-vodlist__bd li")
.map { li ->
val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = li.selectFirst("h3, h4, p.dm-bn")?.text().orEmpty(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = li.selectFirst(selectMangasCover)?.src().orEmpty(),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
protected open val selectPages = "div.more-box li img, ul.main li img"
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select(selectPages).map { img ->
val url = img.src() ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
protected open val selectAvailableTags = "div.swiper-wrapper a[href*=tags], ul.stui-screen__list li a[href*=tags]"
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/category/").parseHtml()
return doc.select(selectAvailableTags)
.mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
title = a.text().toTitleCase(),
source = source,
)
}
}
}

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.cupfox.de
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.cupfox.CupFoxParser
@MangaSourceParser("MANGAHAUS", "MangaHaus", "de")
internal class MangaHaus(context: MangaLoaderContext) :
CupFoxParser(context, MangaParserSource.MANGAHAUS, "www.mangahaus.com")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.cupfox.fr
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.cupfox.CupFoxParser
@MangaSourceParser("ENLIGNEMANGA", "EnLigneManga", "fr")
internal class EnLigneManga(context: MangaLoaderContext) :
CupFoxParser(context, MangaParserSource.ENLIGNEMANGA, "www.enlignemanga.com")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.cupfox.fr
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.cupfox.CupFoxParser
@MangaSourceParser("FRMANGA", "FrManga", "fr")
internal class FrManga(context: MangaLoaderContext) :
CupFoxParser(context, MangaParserSource.FRMANGA, "www.frmanga.com")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.cupfox.fr
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.cupfox.CupFoxParser
@MangaSourceParser("SEINEMANGA", "SeineManga", "fr")
internal class SeineManga(context: MangaLoaderContext) :
CupFoxParser(context, MangaParserSource.SEINEMANGA, "www.seinemanga.com")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.cupfox.ja
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.cupfox.CupFoxParser
@MangaSourceParser("MANGAKOINU", "MangaKoinu", "ja")
internal class MangaKoinu(context: MangaLoaderContext) :
CupFoxParser(context, MangaParserSource.MANGAKOINU, "www.mangakoinu.com")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.cupfox.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.cupfox.CupFoxParser
@MangaSourceParser("OIOIVN", "OioiVn", "vi")
internal class OioiVn(context: MangaLoaderContext) :
CupFoxParser(context, MangaParserSource.OIOIVN, "oioivn.com")

@ -0,0 +1,177 @@
package org.koitharu.kotatsu.parsers.site.en
import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.toJSONList
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("ASURASCANS", "AsuraComic", "en")
internal class AsuraScansParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.ASURASCANS, pageSize = 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.RATING,
SortOrder.UPDATED,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL_DESC,
SortOrder.ALPHABETICAL,
)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain("asuracomic.net")
override val isMultipleTagsSupported = true
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/series?page=")
append(page)
when (filter) {
is MangaListFilter.Search -> {
append("&name=")
append(filter.query.urlEncoded())
}
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty()) {
append("&genres=")
append(filter.tags.joinToString(separator = ",") { it.key })
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "3"
MangaState.ABANDONED -> "4"
MangaState.PAUSED -> "2"
MangaState.UPCOMING -> "6"
},
)
}
append("&types=-1&order=")
when (filter.sortOrder) {
SortOrder.RATING -> append("rating")
SortOrder.UPDATED -> append("update")
SortOrder.NEWEST -> append("latest")
SortOrder.ALPHABETICAL_DESC -> append("desc")
SortOrder.ALPHABETICAL -> append("asc")
else -> append("update")
}
}
null -> append("&genres=&status=-1&order=update&types=-1")
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.grid > a[href]").map { a ->
val href = "/" + a.attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = a.selectFirst("img")?.src().orEmpty(),
title = a.selectFirst("div.block > span.block")?.text().orEmpty(),
altTitle = null,
rating = a.selectFirst("div.block label.ml-1")?.text()?.toFloatOrNull()?.div(10f) ?: RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = when (a.selectLastOrThrow("span.status").text()) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Hiatus" -> MangaState.PAUSED
"Dropped" -> MangaState.ABANDONED
"Coming Soon" -> MangaState.UPCOMING
else -> null
},
source = source,
isNsfw = isNsfwSource,
)
}
}
private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex()
override suspend fun getAvailableTags(): Set<MangaTag> {
return getOrCreateTagMap().values.toSet()
}
private suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()
val json =
webClient.httpGet("https://gg.$domain/api/series/filters").parseJson().getJSONArray("genres").toJSONList()
for (el in json) {
if (el.getString("name").isEmpty()) continue
tagMap[el.getString("name")] = MangaTag(
key = el.getInt("id").toString(),
title = el.getString("name"),
source = source,
)
}
tagCache = tagMap
return@withLock tagMap
}
private val regexDate = """(\d+)(st|nd|rd|th)""".toRegex()
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val tagMap = getOrCreateTagMap()
val selectTag = doc.select("div[class^=space] > div.flex > button.text-white")
val tags = selectTag.mapNotNullToSet { tagMap[it.text()] }
return manga.copy(
description = doc.selectFirst("span.font-medium.text-sm")?.text().orEmpty(),
tags = tags,
author = doc.selectFirst("div.grid > div:has(h3:eq(0):containsOwn(Author)) > h3:eq(1)")?.text(),
chapters = doc.select("div.scrollbar-thumb-themecolor > div.group").mapChapters(reversed = true) { i, div ->
val a = div.selectLastOrThrow("a")
val urlRelative = "/series/" + a.attrAsRelativeUrl("href")
val url = urlRelative.toAbsoluteUrl(domain)
val date = div.selectFirst("h3:eq(1)")!!.ownText()
val cleanDate = date.replace(regexDate, "$1")
MangaChapter(
id = generateUid(url),
name = div.selectFirstOrThrow("h3:eq(0)").text(),
number = i + 1f,
volume = 0,
url = url,
scanlator = null,
uploadDate = SimpleDateFormat("MMMM d yyyy", Locale.US)
.tryParse(cleanDate),
branch = null,
source = source,
)
},
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
return doc.select("div > img[alt=chapter]").map { img ->
val urlPage = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(urlPage),
url = urlPage,
preview = null,
source = source,
)
}
}
}

@ -11,7 +11,7 @@ import java.util.*
@MangaSourceParser("BEETOON", "BeeToon.net", "en")
internal class BeeToon(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.BEETOON, pageSize = 30) {
PagedMangaParser(context, MangaParserSource.BEETOON, pageSize = 30) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)

@ -10,7 +10,7 @@ import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("CLONEMANGA", "CloneManga", "en")
internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.CLONEMANGA) {
internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.CLONEMANGA) {
override val availableSortOrders: Set<SortOrder> = Collections.singleton(
SortOrder.POPULARITY,

@ -13,14 +13,14 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("COMICEXTRA", "ComicExtra", "en")
internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.COMICEXTRA, 25) {
internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.COMICEXTRA, 25) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
override val configKeyDomain = ConfigKey.Domain("comicextra.org")
override val configKeyDomain = ConfigKey.Domain("comixextra.com")
override val isMultipleTagsSupported = false

@ -19,7 +19,8 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("DYNASTYSCANS", "DynastyScans", "en")
internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.DYNASTYSCANS, 117) {
internal class DynastyScans(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.DYNASTYSCANS, 117) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("dynasty-scans.com")
@ -38,8 +39,7 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
append("/search?q=")
append(filter.query.urlEncoded())
append("&")
append("classes[]".urlEncoded())
append("=Serie&page=")
append("classes[]=Series&page=")
append(page.toString())
}
return parseMangaListQuery(webClient.httpGet(url).parseHtml())
@ -154,6 +154,9 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val chapters = getChapters(doc)
val root = doc.requireElementById("main")
val licensedText = root.select("h4")
.find { it.ownText() == "This manga has been licensed" }
?.nextElementSibling()?.html()
return manga.copy(
altTitle = null,
state = when (root.select("h2.tag-title small").last()?.text()) {
@ -167,7 +170,7 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
.orEmpty(), // It is needed if the manga was found via the search.
tags = root.selectFirstOrThrow("div.tag-tags").parseTags(),
author = null,
description = null,
description = licensedText,
chapters = chapters,
)
}

@ -16,7 +16,8 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("FLIXSCANSORG", "FlixScans.org", "en")
internal class FlixScansOrg(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANSORG, 18) {
internal class FlixScansOrg(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.FLIXSCANSORG, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)

@ -2,30 +2,31 @@ package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANGAGEKO", "MangaGeko", "en")
internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGAGEKO, 30) {
internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGAGEKO, 30) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
override val configKeyDomain = ConfigKey.Domain("www.mgeko.com", "www.mangageko.com")
override val configKeyDomain = ConfigKey.Domain("www.mgeko.cc", "www.mgeko.com", "www.mangageko.com")
override val isMultipleTagsSupported = false
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
private val userAgentKey = ConfigKey.UserAgent(context.getDefaultUserAgent())
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.parsers.site.en
import org.koitharu.kotatsu.parsers.*
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
@ -9,7 +11,8 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANGATOWN", "MangaTown", "en")
internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGATOWN, 30) {
internal class MangaTownParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MANGATOWN, 30) {
override val configKeyDomain = ConfigKey.Domain("www.mangatown.com")
@ -102,7 +105,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
id = generateUid(href),
title = a.attr("title"),
coverUrl = a.selectFirst("img")?.absUrl("src").orEmpty(),
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
altTitle = null,
rating = li.selectFirst("p.score")?.selectFirst("b")
?.ownText()?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
@ -118,7 +121,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
MangaTag(
title = x.attr("title").toTitleCase(),
key = x.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
)
}.orEmpty(),
url = href,
@ -143,7 +146,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
MangaTag(
title = a.attr("title").toTitleCase(),
key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
)
}.orEmpty(),
description = info?.getElementById("show")?.ownText(),
@ -156,7 +159,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
MangaChapter(
id = generateUid(href),
url = href,
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
number = i + 1f,
volume = 0,
uploadDate = parseChapterDate(
@ -174,10 +177,10 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val root = doc.body().selectFirstOrThrow("div.page_select")
val isManga = root.select("select")
val root = doc.body().selectFirst("div.page_select")
val isManga = root?.select("select")
if (isManga.isEmpty()) {//Webtoon
if (isManga.isNullOrEmpty()) {//Webtoon
val imgElements = doc.select("div#viewer.read_img img.image")
return imgElements.map {
val href = it.attr("src")
@ -185,7 +188,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
id = generateUid(href),
url = href,
preview = null,
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
)
}
@ -199,7 +202,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
id = generateUid(href),
url = href,
preview = null,
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
)
}
}
@ -224,7 +227,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-")
MangaTag(
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
key = key,
title = a.text().toTitleCase(),
)
@ -253,7 +256,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : PagedMangaParser(c
MangaChapter(
id = generateUid(href),
url = href,
source = MangaSource.MANGATOWN,
source = MangaParserSource.MANGATOWN,
number = i + 1f,
volume = 0,
uploadDate = parseChapterDate(

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.coroutineScope
import okhttp3.Headers
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -14,9 +15,10 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat
import java.util.*
@Broken
@MangaSourceParser("MANGAOWL", "MangaOwl.to", "en")
internal class Mangaowl(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANGAOWL, pageSize = 24) {
PagedMangaParser(context, MangaParserSource.MANGAOWL, pageSize = 24) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY,

@ -11,7 +11,7 @@ import java.util.*
@MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI)
class Manhwa18Parser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
PagedMangaParser(context, MangaParserSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net")
@ -122,7 +122,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
author = null,
largeCoverUrl = null,
description = null,
source = MangaSource.MANHWA18,
source = MangaParserSource.MANHWA18,
)
}
}
@ -167,7 +167,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
scanlator = null,
uploadDate = uploadDate,
branch = null,
source = MangaSource.MANHWA18,
source = MangaParserSource.MANHWA18,
)
},
)
@ -210,7 +210,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
id = generateUid(url),
url = url,
preview = null,
source = MangaSource.MANHWA18,
source = MangaParserSource.MANHWA18,
)
}
}

@ -12,7 +12,7 @@ import java.util.*
@MangaSourceParser("MANHWASMEN", "ManhwasMen", "en", type = ContentType.HENTAI)
class ManhwasMen(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.MANHWASMEN, pageSize = 30, searchPageSize = 30) {
PagedMangaParser(context, MangaParserSource.MANHWASMEN, pageSize = 30, searchPageSize = 30) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwas.men")

@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("PO2SCANS", "Po2Scans", "en")
internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, MangaSource.PO2SCANS) {
internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.PO2SCANS) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("po2scans.com")

@ -15,7 +15,7 @@ import java.util.*
@MangaSourceParser("PURURIN", "Pururin", "en", ContentType.HENTAI)
internal class Pururin(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.PURURIN, pageSize = 20) {
PagedMangaParser(context, MangaParserSource.PURURIN, pageSize = 20) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.RATING, SortOrder.ALPHABETICAL)

@ -23,21 +23,18 @@ import java.text.SimpleDateFormat
import java.util.*
import kotlin.random.Random
private const val TOO_MANY_REQUESTS = 429
private const val MAX_RETRY_COUNT = 5
private val JSON_MEDIA_TYPE get() = "application/json; charset=utf-8".toMediaType()
@MangaSourceParser("REAPERCOMICS", "ReaperComics", "en")
internal class ReaperComics(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.REAPERCOMICS, pageSize = 32) {
PagedMangaParser(context, MangaParserSource.REAPERCOMICS, pageSize = 32) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("reaperscans.com")
private val userAgentKey = ConfigKey.UserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
)
private val userAgentKey = ConfigKey.UserAgent(context.getDefaultUserAgent())
private val baseHeaders: Headers
get() = Headers.Builder().add("User-Agent", config[userAgentKey]).build()
@ -159,9 +156,9 @@ internal class ReaperComics(context: MangaLoaderContext) :
keys.add(userAgentKey)
}
private fun chapterListNextPageSelector(): String = "button[wire:click*=nextPage]"
private val chapterListNextPageSelector: String = "button[wire:click*=nextPage]"
private fun chapterListSelector() = "div[wire:id] > div > ul[role=list] > li"
private val chapterListSelector: String = "div[wire:id] > div > ul[role=list] > li"
override suspend fun getDetails(manga: Manga): Manga {
val cachedChapters = chapterCache[manga.url]
@ -171,17 +168,18 @@ internal class ReaperComics(context: MangaLoaderContext) :
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
var totalChapters = (doc.selectFirst(selectTotalChapter)?.text()?.toIntOrNull() ?: 0) - 1
var totalChapters = (doc.selectFirst(selectTotalChapter)?.text()?.toFloatOrNull() ?: 0f) - 1f
val chapters = mutableSetOf<MangaChapter>()
var hasNextPage = doc.selectFirst(chapterListNextPageSelector()) != null
var hasNextPage = doc.selectFirst(chapterListNextPageSelector) != null
chapters.addAll(
doc.select(chapterListSelector()).mapChapters { _, li ->
doc.select(chapterListSelector).mapChapters { _, li ->
val a = li.selectFirstOrThrow("a")
val chapterUrl = a.attr("href").toRelativeUrl(domain)
MangaChapter(
id = generateUid(chapterUrl),
name = li.selectFirst("div.truncate p.truncate")?.text().orEmpty(),
number = totalChapters--,
volume = 0,
url = chapterUrl,
scanlator = null,
uploadDate = parseChapterDate(
@ -219,7 +217,7 @@ internal class ReaperComics(context: MangaLoaderContext) :
var pageToQuery = 2
// Javascript: (Math.random() + 1).toString(36).substring(8)
val generateId = { ->
val generateId = {
"1.${
Random.nextLong().toString(36)
}".substring(10)
@ -249,13 +247,14 @@ internal class ReaperComics(context: MangaLoaderContext) :
serverMemo = mergeLeft(serverMemo, responseData.serverMemo)
val chaptersHtml = Jsoup.parse(responseData.effects.html, "https://$domain")
chapters.addAll(
chaptersHtml.select(chapterListSelector()).mapChapters { _, li ->
chaptersHtml.select(chapterListSelector).mapChapters { _, li ->
val a = li.selectFirstOrThrow("a")
val chapterUrl = a.attr("href").toRelativeUrl(domain)
MangaChapter(
id = generateUid(chapterUrl),
name = li.selectFirst("div.truncate p.truncate")?.text().orEmpty(),
number = totalChapters--,
volume = 0,
url = chapterUrl,
scanlator = null,
uploadDate = parseChapterDate(
@ -267,7 +266,7 @@ internal class ReaperComics(context: MangaLoaderContext) :
)
},
)
hasNextPage = chaptersHtml.selectFirst(chapterListNextPageSelector()) != null
hasNextPage = chaptersHtml.selectFirst(chapterListNextPageSelector) != null
pageToQuery++
}

@ -15,7 +15,7 @@ import java.util.*
@MangaSourceParser("TEMPLESCANESP", "TempleScanEsp", "es", ContentType.HENTAI)
internal class TempleScanEsp(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.TEMPLESCANESP, pageSize = 15) {
PagedMangaParser(context, MangaParserSource.TEMPLESCANESP, pageSize = 15) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST, SortOrder.UPDATED)

@ -16,7 +16,7 @@ import java.util.*
@MangaSourceParser("TUMANGAONLINE", "TuMangaOnline", "es")
class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
context,
source = MangaSource.TUMANGAONLINE,
source = MangaParserSource.TUMANGAONLINE,
pageSize = 24,
) {
@ -147,7 +147,8 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
return MangaChapter(
id = generateUid(href),
name = "One Shot",
number = 1,
number = 1f,
volume = 0,
url = href,
scanlator = element.select("div.col-md-6.text-truncate").text(),
branch = null,
@ -163,7 +164,8 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
return MangaChapter(
id = generateUid(href),
name = chName,
number = number + 1,
number = number + 1f,
volume = 0,
url = href,
scanlator = element.select("div.col-md-6.text-truncate").text(),
branch = null,
@ -205,6 +207,9 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
private suspend fun redirectToReadingPage(document: Document): Document {
val script1 = document.selectFirst("script:containsData(uniqid)")
val script2 = document.selectFirst("script:containsData(window.location.replace)")
val script3 = document.selectFirst("script:containsData(redirectUrl)")
val script4 = document.selectFirst("input#redir")
val script5 = document.selectFirst("script:containsData(window.opener):containsData(location.replace)")
val redirectHeaders = Headers.Builder().set("Referer", document.baseUri()).build()
@ -226,15 +231,52 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
if (script2 != null) {
val data = script2.data()
val regexRedirect = """window\.location\.replace\('(.+)'\)""".toRegex()
val url = regexRedirect.find(data)!!.groupValues[1]
val regexRedirect = """window\.location\.replace\(['"](.+)['"]\)""".toRegex()
val url = regexRedirect.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (url != null) {
return redirectToReadingPage(webClient.httpGet(url, redirectHeaders).parseHtml())
}
}
if (script3 != null) {
val data = script3.data()
val regexRedirect = """redirectUrl\s*=\s*'(.+)'""".toRegex()
val url = regexRedirect.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (url != null) {
return redirectToReadingPage(webClient.httpGet(url, redirectHeaders).parseHtml())
}
}
if (script4 != null) {
val url = script4.attr("value").unescapeUrl()
return redirectToReadingPage(webClient.httpGet(url, redirectHeaders).parseHtml())
}
if (script5 != null) {
val data = script5.data()
val regexRedirect = """;[^.]location\.replace\(['"](.+)['"]\)""".toRegex()
val url = regexRedirect.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (url != null) {
return redirectToReadingPage(webClient.httpGet(url, redirectHeaders).parseHtml())
}
}
return document
}
private fun String.unescapeUrl(): String {
return if (this.startsWith("http:\\/\\/") || this.startsWith("https:\\/\\/")) {
this.replace("\\/", "/")
} else {
this
}
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/library", headers).parseHtml()
val elements = doc.body().select("div#books-genders > div > div")

@ -14,7 +14,7 @@ import java.util.*
internal abstract class FmreaderParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
domain: String,
pageSize: Int = 20,
) : PagedMangaParser(context, source, pageSize) {

@ -12,7 +12,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("MANHWA18COM", "Manhwa18.com", "en", ContentType.HENTAI)
internal class Manhwa18Com(context: MangaLoaderContext) :
FmreaderParser(context, MangaSource.MANHWA18COM, "manhwa18.com") {
FmreaderParser(context, MangaParserSource.MANHWA18COM, "manhwa18.com") {
override val listUrl = "/tim-kiem"
override val selectState = "div.info-item:contains(Status) span.info-value "

@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("OLIMPOSCANS", "OlimpoScans", "es")
internal class OlimpoScans(context: MangaLoaderContext) :
FmreaderParser(context, MangaSource.OLIMPOSCANS, "olimposcans.com") {
FmreaderParser(context, MangaParserSource.OLIMPOSCANS, "leerolimpo.com") {
override val selectState = "ul.manga-info li:contains(Estado) a"
override val selectAlt = "ul.manga-info li:contains(Otros nombres)"

@ -11,7 +11,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("KLZ9", "Klz9", "ja")
internal class Klz9(context: MangaLoaderContext) :
FmreaderParser(context, MangaSource.KLZ9, "klz9.com") {
FmreaderParser(context, MangaParserSource.KLZ9, "klz9.com") {
override val selectDesc = "div.row:contains(Description)"
override val selectState = "ul.manga-info li:contains(Status) a"

@ -5,14 +5,14 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@MangaSourceParser("WELOVEMANGA", "WeLoveManga", "ja")
internal class WeLoveManga(context: MangaLoaderContext) :
FmreaderParser(context, MangaSource.WELOVEMANGA, "welovemanga.one") {
FmreaderParser(context, MangaParserSource.WELOVEMANGA, "welovemanga.one") {
override suspend fun getChapters(doc: Document): List<MangaChapter> {
val mid = doc.selectFirstOrThrow("div.cmt input").attr("value")

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.fmreader.ja
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
@MangaSourceParser("WELOMA", "Weloma", "ja")
internal class Weloma(context: MangaLoaderContext) :
FmreaderParser(context, MangaSource.WELOMA, "weloma.art")
FmreaderParser(context, MangaParserSource.WELOMA, "weloma.art")

@ -13,7 +13,7 @@ import java.util.*
internal abstract class FoolSlideParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
domain: String,
pageSize: Int = 25,
) : PagedMangaParser(context, source, pageSize) {

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.foolslide.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("DEATHTOLLSCANS", "DeathTollScans", "en")
internal class Deathtollscans(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.DEATHTOLLSCANS, "reader.deathtollscans.net", 26)
FoolSlideParser(context, MangaParserSource.DEATHTOLLSCANS, "reader.deathtollscans.net", 26)

@ -2,11 +2,11 @@ package org.koitharu.kotatsu.parsers.site.foolslide.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("MANGATELLERS", "Mangatellers", "en")
internal class Mangatellers(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.MANGATELLERS, "reader.mangatellers.gr") {
FoolSlideParser(context, MangaParserSource.MANGATELLERS, "reader.mangatellers.gr") {
override val pagination = false
}

@ -1,10 +1,12 @@
package org.koitharu.kotatsu.parsers.site.foolslide.en
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@Broken
@MangaSourceParser("READER_EVILFLOWERS", "Evil Flowers", "en")
internal class ReaderEvilflowers(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.READER_EVILFLOWERS, "reader.evilflowers.com")
FoolSlideParser(context, MangaParserSource.READER_EVILFLOWERS, "reader.evilflowers.com")

@ -3,13 +3,14 @@ package org.koitharu.kotatsu.parsers.site.foolslide.en
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("SEINAGI", "Seinagi", "en")
internal class Seinagi(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.SEINAGI, "reader.seinagi.org.es") {
FoolSlideParser(context, MangaParserSource.SEINAGI, "reader.seinagi.org.es") {
override val pagination = false

@ -2,12 +2,12 @@ package org.koitharu.kotatsu.parsers.site.foolslide.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("MENUDO_FANSUB", "Menudo Fansub", "es")
internal class MenudoFansub(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.MENUDO_FANSUB, "www.menudo-fansub.com", 25) {
FoolSlideParser(context, MangaParserSource.MENUDO_FANSUB, "www.menudo-fansub.com", 25) {
override val searchUrl = "slide/search/"
override val listUrl = "slide/directory/"
}

@ -3,13 +3,15 @@ package org.koitharu.kotatsu.parsers.site.foolslide.es
import kotlinx.coroutines.coroutineScope
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("PZYKOSIS666HFANSUB", "Pzykosis666h Fansub", "es", ContentType.HENTAI)
internal class Pzykosis666hFansub(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.PZYKOSIS666HFANSUB, "lector.pzykosis666hfansub.com") {
FoolSlideParser(context, MangaParserSource.PZYKOSIS666HFANSUB, "lector.pzykosis666hfansub.com") {
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)

@ -5,17 +5,13 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.src
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("SEINAGIADULTO", "Seinagi Adulto", "es", ContentType.HENTAI)
internal class SeinagiAdulto(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.SEINAGIADULTO, "adulto.seinagi.org.es") {
FoolSlideParser(context, MangaParserSource.SEINAGIADULTO, "adulto.seinagi.org.es") {
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)

@ -2,13 +2,14 @@ package org.koitharu.kotatsu.parsers.site.foolslide.fr
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
// The source has changed template so for the moment it is dead.
@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr")
internal class HniScantrad(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.HNISCANTRAD, "hni-scantrad.net") {
FoolSlideParser(context, MangaParserSource.HNISCANTRAD, "hni-scantrad.net") {
override val pagination = false
override val searchUrl = "lel/search/"
override val listUrl = "lel/directory/"

@ -1,12 +1,14 @@
package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.Broken
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@Broken
@MangaSourceParser("POWERMANGA", "PowerManga", "it")
internal class PowerManga(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.POWERMANGA, "reader.powermanga.org") {
FoolSlideParser(context, MangaParserSource.POWERMANGA, "reader.powermanga.org") {
override val pagination = false
}

@ -2,12 +2,12 @@ package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("RAMAREADER", "RamaReader", "it")
internal class Ramareader(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.RAMAREADER, "www.ramareader.it") {
FoolSlideParser(context, MangaParserSource.RAMAREADER, "www.ramareader.it") {
override val searchUrl = "read/search/"
override val listUrl = "read/directory/"
}

@ -2,12 +2,12 @@ package org.koitharu.kotatsu.parsers.site.foolslide.it
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
@MangaSourceParser("READNIFTEAM", "ReadNifTeam", "it")
internal class ReadNifteam(context: MangaLoaderContext) :
FoolSlideParser(context, MangaSource.READNIFTEAM, "read-nifteam.info") {
FoolSlideParser(context, MangaParserSource.READNIFTEAM, "read-nifteam.info") {
override val searchUrl = "slide/search/"
override val listUrl = "slide/directory/"
}

@ -18,7 +18,8 @@ import java.util.*
@Broken
@MangaSourceParser("BENTOMANGA", "BentoManga", "fr")
internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.BENTOMANGA, 10) {
internal class BentomangaParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.BENTOMANGA, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
@ -228,7 +229,8 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
MangaChapter(
id = generateUid(href),
name = if (name != null && name != title) "$title: $name" else title,
number = href.substringAfterLast('/').toIntOrNull() ?: 0,
number = href.substringAfterLast('/').toFloatOrNull() ?: 0f,
volume = 0,
url = href,
scanlator = div.selectFirst(".team_link-name")?.textOrNull(),
uploadDate = div.selectFirst(".component-chapter-date")

@ -17,7 +17,7 @@ import java.util.*
@MangaSourceParser("FURYOSOCIETY", "FuryoSociety", "fr")
internal class FuryoSociety(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.FURYOSOCIETY, 0) {
PagedMangaParser(context, MangaParserSource.FURYOSOCIETY, 0) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.UPDATED)

@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("LEGACY_SCANS", "LegacyScans", "fr")
internal class LegacyScansParser(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.LEGACY_SCANS, 18) {
PagedMangaParser(context, MangaParserSource.LEGACY_SCANS, 18) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.parsers.site.fr
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
@ -11,7 +10,7 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("LIRESCAN", "LireScan", "fr")
internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.LIRESCAN, 20) {
internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.LIRESCAN, 20) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)

@ -16,7 +16,8 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("LUGNICASCANS", "LugnicaScans", "fr")
internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.LUGNICASCANS, 10) {
internal class LugnicaScans(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.LUGNICASCANS, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,

@ -0,0 +1,336 @@
package org.koitharu.kotatsu.parsers.site.fr
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.Headers
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("MANGAMANA", "MangaMana", "fr")
internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MANGAMANA, 25) {
override val availableSortOrders: Set<SortOrder> =
EnumSet.of(
SortOrder.UPDATED,
SortOrder.RATING,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.NEWEST,
)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("www.manga-mana.com")
override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val postData = buildString {
append("page=")
append(page)
when (filter) {
is MangaListFilter.Search -> {
if (page > 1) {
return emptyList()
}
val domainCdn = "cdn" + domain.removePrefix("www")
val json = webClient.httpGet("https://$domain/search-live?q=${filter.query}").parseJsonArray()
return json.mapJSON { jo ->
val slug = jo.getString("slug") ?: throw Exception("Missing Slug")
val url = "https://$domain/m/$slug"
val img = "https://$domainCdn/uploads/manga/$slug/cover/cover_thumb.jpg"
Manga(
id = generateUid(url),
title = jo.getString("name").orEmpty(),
coverUrl = img,
altTitle = jo.getString("otherNames").orEmpty(),
author = null,
isNsfw = when (jo.getIntOrDefault("caution", 0)) {
0 -> false
2 -> true
else -> false
},
rating = RATING_UNKNOWN,
url = url,
description = jo.getString("summary_old").orEmpty(),
publicUrl = url,
tags = emptySet(),
state = when (jo.getIntOrDefault("status_id_fr", 4)) {
1 -> MangaState.ONGOING
2 -> MangaState.FINISHED
3 -> MangaState.ABANDONED
else -> null
},
source = source,
)
}
}
is MangaListFilter.Advanced -> {
if (filter.sortOrder == SortOrder.UPDATED) {
if (filter.tags.isNotEmpty() or filter.states.isNotEmpty()) {
throw IllegalArgumentException("Le filtrage par « tri par : mis à jour » avec les genres ou les statuts n'est pas pris en charge par cette source.")
}
val doc = webClient.httpGet("https://$domain/?page=$page").parseHtml()
return doc.select("div.row div.col_home").map { div ->
val href = div.selectFirstOrThrow("h4 a").attrAsRelativeUrl("href")
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() ?: false
val img = if (isNsfw) {
div.selectFirst("img")?.attr("data-adult")
} else {
div.selectFirst("img")?.attr("data-src")?.replace(" ", "")
}
Manga(
id = generateUid(href),
title = div.select("h4").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = isNsfw,
coverUrl = img.orEmpty(),
description = null,
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
} else {
filter.tags.oneOrThrowIfMany()?.let {
append("&category=")
append(it.key)
}
filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "1"
MangaState.FINISHED -> "2"
MangaState.ABANDONED -> "3"
else -> ""
},
)
}
append("&sort_by=")
when (filter.sortOrder) {
SortOrder.RATING -> append("score&sort_dir=desc")
SortOrder.NEWEST -> append("updated_at&sort_dir=desc")
SortOrder.ALPHABETICAL -> append("name&sort_dir=asc")
SortOrder.ALPHABETICAL_DESC -> append("name&sort_dir=desc")
else -> append("updated_at&sort_dir=desc")
}
}
}
null -> append("&sort_by=updated_at&sort_dir=desc")
}
}
val url = "https://$domain/liste-mangas"
val token = webClient.httpGet(url).parseHtml().selectFirstOrThrow("meta[name=csrf-token]").attr("content")
val headers = Headers.Builder().add("X-CSRF-TOKEN", token).add("X-Requested-With", "XMLHttpRequest")
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build()
val doc = makeRequest(url, postData.toRequestBody(), headers)
return doc.select("div.p-2 div.col").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val isNsfw = div.selectFirst("img[data-adult]")?.attr("data-adult")?.isNotEmpty() ?: false
val img = if (isNsfw) {
div.selectFirst("img")?.attr("data-adult")
} else {
div.selectFirst("img")?.attr("data-src")?.replace(" ", "")
}
Manga(
id = generateUid(href),
title = div.select("h2.fs-6").text(),
altTitle = doc.selectFirst(".mangalist_item_othernames")?.text().orEmpty(),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = div.getElementById("avgrating")?.ownText()?.toFloat()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = isNsfw,
coverUrl = img.orEmpty(),
description = div.selectFirst(".mangalist_item_description")?.text().orEmpty(),
tags = div.select("div.mb-1 a").mapNotNullToSet {
val key = it.attr("href").substringAfterLast('=')
MangaTag(
key = key,
title = it.text(),
source = source,
)
},
state = null,
author = null,
source = source,
)
}
}
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
var retryCount = 0
val backoffDelay = 2000L // Initial delay (milliseconds)
val request = Request.Builder().url(url).post(payload).headers(headers).build()
while (true) {
try {
return Jsoup.parse(context.httpClient.newCall(request).execute().parseJson().getString("html"))
} catch (e: Exception) {
// Log or handle the exception as needed
if (++retryCount <= 5) {
withContext(Dispatchers.Default) {
delay(backoffDelay)
}
} else {
throw e
}
}
}
}
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val mangaUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(mangaUrl).parseHtml()
val maxPageChapterSelect = doc.select("ul.pagination a.page-link")
var maxPageChapter = 1
if (!maxPageChapterSelect.isNullOrEmpty()) {
maxPageChapterSelect.map {
val i = it.attr("href").substringAfterLast("=").toInt()
if (i > maxPageChapter) {
maxPageChapter = i
}
}
}
manga.copy(
state = when (doc.select("div.show_details div.d-flex:contains(Statut) span").text()) {
"En Cours" -> MangaState.ONGOING
"Terminé" -> MangaState.FINISHED
"Abandonné" -> MangaState.ABANDONED
else -> null
},
author = doc.selectFirst("div.show_details span[itemprop=author]")?.text().orEmpty(),
description = doc.selectFirst("dd[itemprop=description]")?.text(),
rating = doc.getElementById("avgrating")?.ownText()?.toFloat()?.div(5f) ?: RATING_UNKNOWN,
tags = doc.select("ul.list-unstyled li a.category").mapNotNullToSet {
val key = it.attr("href").substringAfterLast('=')
MangaTag(
key = key,
title = it.text(),
source = source,
)
},
chapters = run {
if (maxPageChapter == 1) {
parseChapters(doc)
} else {
coroutineScope {
val result = ArrayList(parseChapters(doc))
result.ensureCapacity(result.size * maxPageChapter)
(2..maxPageChapter).map { i ->
async {
loadChapters(mangaUrl, i)
}
}.awaitAll()
.flattenTo(result)
result
}
}
}.reversed(),
)
}
private suspend fun loadChapters(baseUrl: String, page: Int): List<MangaChapter> {
return parseChapters(webClient.httpGet("$baseUrl?page=$page").parseHtml().body())
}
private val dateFormat = SimpleDateFormat("d MMM yyyy", sourceLocale)
private fun parseChapters(doc: Element): List<MangaChapter> {
return doc.select("ul.list-unstyled li a.chapter_link")
.mapChapters { i, a ->
val href = a.attrAsRelativeUrl("href")
val name = a.selectFirst(".chapter div")?.html()?.substringBefore("<") ?: "Chapitre $i"
val dateText = a.selectFirst(".small")?.text()
val chapterN = href.substringAfterLast('/').replace("-", ".").replace("[^0-9.]".toRegex(), "").toFloat()
MangaChapter(
id = generateUid(href),
name = name,
number = chapterN,
volume = 0,
url = href,
scanlator = null,
uploadDate = dateFormat.tryParse(dateText),
branch = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val docs = webClient.httpGet(chapterUrl).parseHtml()
val cdn = docs.selectFirstOrThrow("script:containsData(var cdn = )").data().substringAfterLast("var cdn = \"")
.substringBefore('"')
val domainCdn = cdn + domain.removePrefix("www")
val slugManga = chapterUrl.substringAfter("/m/").substringBeforeLast('/')
val slugChapter = chapterUrl.substringAfterLast('/')
val script = docs.selectFirstOrThrow("script:containsData(var pages =)")
val json = JSONArray(script.data().substringAfter("pages = ").substringBefore("; var next_chapter"))
val pages = ArrayList<MangaPage>(json.length())
for (i in 0 until json.length()) {
val img = json.getJSONObject(i).getString("image")
val v = json.getJSONObject(i).getInt("version")
val url = "https://$domainCdn/uploads/manga/$slugManga/chapters_fr/$slugChapter/$img?$v"
pages.add(
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
),
)
}
return pages
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/liste-mangas").parseHtml()
return doc.select("select.selectpicker option").drop(1).mapNotNullToSet {
MangaTag(
key = it.attr("value"),
title = it.text(),
source = source,
)
}
}
}

@ -15,7 +15,7 @@ import java.util.*
@MangaSourceParser("SCANS_MANGAS_ME", "ScansMangas.me", "fr")
internal class ScansMangasMe(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.SCANS_MANGAS_ME, 0) {
PagedMangaParser(context, MangaParserSource.SCANS_MANGAS_ME, 0) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,
@ -132,7 +132,7 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
}
private fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().requireElementById("chapter_list").select("li").mapChapters(reversed = true) { i, li ->
return doc.body().requireElementById("chapter_list").select("li").mapChapters(reversed = true) { _, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
MangaChapter(

@ -13,7 +13,8 @@ import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("SCANTRADUNION", "ScantradUnion", "fr")
internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.SCANTRADUNION, 10) {
internal class ScantradUnion(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.SCANTRADUNION, 10) {
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,

@ -16,7 +16,7 @@ import java.util.*
internal abstract class GalleryAdultsParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
domain: String,
pageSize: Int = 20,
) : PagedMangaParser(context, source, pageSize) {
@ -158,7 +158,8 @@ internal abstract class GalleryAdultsParser(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
number = 1f,
volume = 0,
url = urlChapters,
scanlator = null,
uploadDate = 0,

@ -4,7 +4,7 @@ import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.mapToSet
@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("ASMHENTAI", "AsmHentai", type = ContentType.HENTAI)
internal class AsmHentai(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.ASMHENTAI, "asmhentai.com") {
GalleryAdultsParser(context, MangaParserSource.ASMHENTAI, "asmhentai.com") {
override val selectGallery = ".preview_item"
override val selectGalleryLink = ".image a"
override val selectGalleryImg = ".image img"

@ -10,7 +10,7 @@ import java.util.*
@MangaSourceParser("DOUJINDESUUK", "DoujinDesu.uk", type = ContentType.HENTAI)
internal class DoujinDesuUk(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.DOUJINDESUUK, "doujindesu.uk", 25) {
GalleryAdultsParser(context, MangaParserSource.DOUJINDESUUK, "doujindesu.uk", 25) {
override val selectGallery = ".gallery"
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".caption"

@ -10,7 +10,7 @@ import java.util.*
@MangaSourceParser("HENTAI3", "3Hentai", type = ContentType.HENTAI)
internal class Hentai3(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAI3, "3hentai.net", pageSize = 30) {
GalleryAdultsParser(context, MangaParserSource.HENTAI3, "3hentai.net", pageSize = 30) {
override val selectGallery = ".doujin "
override val selectGalleryLink = "a"

@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("HENTAIENVY", "HentaiEnvy", type = ContentType.HENTAI)
internal class HentaiEnvy(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIENVY, "hentaienvy.com", pageSize = 24) {
GalleryAdultsParser(context, MangaParserSource.HENTAIENVY, "hentaienvy.com", pageSize = 24) {
override val selectGalleryLink = "a"
override val selectGalleryTitle = "div.title"
override val selectTags = ".tags_items"

@ -10,7 +10,7 @@ import java.util.*
@MangaSourceParser("HENTAIERA", "HentaiEra", type = ContentType.HENTAI)
internal class HentaiEra(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIERA, "hentaiera.com", 25) {
GalleryAdultsParser(context, MangaParserSource.HENTAIERA, "hentaiera.com", 25) {
override val selectTags = ".tags_section"
override val selectTag = ".galleries_info li:contains(Tags) div.info_tags"
override val selectAuthor = ".galleries_info li:contains(Artists) span.item_name"
@ -131,7 +131,8 @@ internal class HentaiEra(context: MangaLoaderContext) :
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
number = 1f,
volume = 0,
url = urlChapters,
scanlator = null,
uploadDate = 0,

@ -10,7 +10,7 @@ import java.util.*
@MangaSourceParser("HENTAIFORCE", "HentaiForce", type = ContentType.HENTAI)
internal class HentaiForce(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIFORCE, "hentaiforce.net", pageSize = 30) {
GalleryAdultsParser(context, MangaParserSource.HENTAIFORCE, "hentaiforce.net", pageSize = 30) {
override val selectGallery = ".gallery"
override val selectGalleryLink = "a.gallery-thumb"
override val pathTagUrl = "/tags/popular/"

@ -11,7 +11,7 @@ import java.util.*
@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI)
internal class HentaiFox(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIFOX, "hentaifox.com") {
GalleryAdultsParser(context, MangaParserSource.HENTAIFOX, "hentaifox.com") {
override val selectGallery = ".lc_galleries .thumb, .related_galleries .thumb"
override val pathTagUrl = "/tags/popular/pag/"
override val selectTags = ".list_tags"

@ -4,7 +4,7 @@ import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.mapToSet
@ -13,7 +13,7 @@ import java.util.*
@MangaSourceParser("HENTAIROX", "HentaiRox", type = ContentType.HENTAI)
internal class HentaiRox(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIROX, "hentairox.com") {
GalleryAdultsParser(context, MangaParserSource.HENTAIROX, "hentairox.com") {
override val selectGalleryImg = ".inner_thumb img"
override val selectTags = ".gtags"
override val selectTag = "li:contains(Tags:)"

@ -14,7 +14,7 @@ import java.util.*
@MangaSourceParser("NHENTAI", "NHentai.net", type = ContentType.HENTAI)
internal class NHentaiParser(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.NHENTAI, "nhentai.net", 25) {
GalleryAdultsParser(context, MangaParserSource.NHENTAI, "nhentai.net", 25) {
override val selectGallery = "div.index-container:not(.index-popular) .gallery, #related-container .gallery"
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".caption"

@ -11,7 +11,7 @@ import java.util.*
internal abstract class GattsuParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
domain: String,
pageSize: Int = 20,
) : PagedMangaParser(context, source, pageSize) {
@ -115,7 +115,8 @@ internal abstract class GattsuParser(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
number = 1f,
volume = 0,
url = urlChapter,
scanlator = null,
uploadDate = 0,

@ -3,9 +3,9 @@ package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
@MangaSourceParser("HENTAISEASON", "HentaiSeason", "pt", ContentType.HENTAI)
internal class HentaiSeason(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.HENTAISEASON, "hentaiseason.com")
GattsuParser(context, MangaParserSource.HENTAISEASON, "hentaiseason.com")

@ -3,11 +3,11 @@ package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
@MangaSourceParser("HENTAITOKYO", "HentaiTokyo", "pt", ContentType.HENTAI)
internal class HentaiTokyo(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.HENTAITOKYO, "hentaitokyo.net") {
GattsuParser(context, MangaParserSource.HENTAITOKYO, "hentaitokyo.net") {
override val tagUrl = "tags"
}

@ -5,18 +5,14 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.selectLastOrThrow
import org.koitharu.kotatsu.parsers.util.src
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("MUNDOHENTAIOFICIAL", "MundoHentaiOficial", "pt", ContentType.HENTAI)
internal class MundoHentaiOficial(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.MUNDOHENTAIOFICIAL, "mundohentaioficial.com") {
GattsuParser(context, MangaParserSource.MUNDOHENTAIOFICIAL, "mundohentaioficial.com") {
override val tagUrl = "tags"

@ -3,17 +3,13 @@ package org.koitharu.kotatsu.parsers.site.gattsu.pt
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
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.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("UNIVERSOHENTAI", "UniversoHentai", "pt", ContentType.HENTAI)
internal class UniversoHentai(context: MangaLoaderContext) :
GattsuParser(context, MangaSource.UNIVERSOHENTAI, "universohentai.com") {
GattsuParser(context, MangaParserSource.UNIVERSOHENTAI, "universohentai.com") {
override val tagPrefix = "category"

@ -10,7 +10,7 @@ import java.util.*
internal abstract class GuyaParser(
context: MangaLoaderContext,
source: MangaSource,
source: MangaParserSource,
domain: String,
pageSize: Int = 0,
) : PagedMangaParser(context, source, pageSize) {
@ -91,7 +91,8 @@ internal abstract class GuyaParser(
MangaChapter(
id = generateUid(url),
name = chapter.getString("title"),
number = i,
number = i.toFloat(),
volume = 0,
url = url,
scanlator = null,
uploadDate = 0,

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.guya.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
@MangaSourceParser("MAHOUSHOUJOBU", "MahouShoujobu")
internal class MahouShoujobu(context: MangaLoaderContext) :
GuyaParser(context, MangaSource.MAHOUSHOUJOBU, "mahoushoujobu.com")
GuyaParser(context, MangaParserSource.MAHOUSHOUJOBU, "mahoushoujobu.com")

@ -2,9 +2,9 @@ package org.koitharu.kotatsu.parsers.site.guya.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
@MangaSourceParser("DANKE", "DankeFursLesen", "en")
internal class Danke(context: MangaLoaderContext) :
GuyaParser(context, MangaSource.DANKE, "danke.moe")
GuyaParser(context, MangaParserSource.DANKE, "danke.moe")

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

Loading…
Cancel
Save