Merge branch 'KotatsuApp:master' into master

pull/421/head
Deivid Gabriel Pereira de Oliveira 2 years ago committed by GitHub
commit dd0c6ff620
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -29,6 +29,7 @@
.idea/compiler.xml .idea/compiler.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/modules.xml .idea/modules.xml
.idea/ktlint-plugin.xml
.idea/*.iml .idea/*.iml
.idea/modules .idea/modules
*.iml *.iml
@ -76,4 +77,4 @@ build/
.idea/codeStyles/ .idea/codeStyles/
src/test/resources/cookies.txt src/test/resources/cookies.txt
local.properties local.properties

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.20" /> <option name="version" value="1.9.22" />
</component> </component>
</project> </project>

@ -2,8 +2,8 @@ import tasks.ReportGenerateTask
plugins { plugins {
id 'java-library' id 'java-library'
id 'org.jetbrains.kotlin.jvm' version '1.9.20' id 'org.jetbrains.kotlin.jvm' version '1.9.22'
id 'com.google.devtools.ksp' version '1.9.20-1.0.14' id 'com.google.devtools.ksp' version '1.9.22-1.0.16'
id 'maven-publish' id 'maven-publish'
} }
@ -55,16 +55,16 @@ afterEvaluate {
dependencies { dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okio:okio:3.6.0' implementation 'com.squareup.okio:okio:3.7.0'
api 'org.jsoup:jsoup:1.16.2' api 'org.jsoup:jsoup:1.17.2'
implementation 'org.json:json:20231013' implementation 'org.json:json:20231013'
implementation 'androidx.collection:collection-ktx:1.3.0' implementation 'androidx.collection:collection-ktx:1.3.0'
ksp project(':kotatsu-parsers-ksp') ksp project(':kotatsu-parsers-ksp')
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'io.webfolder:quickjs:1.1.0' testImplementation 'io.webfolder:quickjs:1.1.0'
} }

@ -3,5 +3,5 @@ plugins {
} }
dependencies { dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14' implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.16'
} }

@ -4,6 +4,8 @@ object ErrorMessages {
const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source" const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source"
const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source" const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source"
const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED =
"Multiple Content Rating are not supported by this source"
const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED = const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED =
"Filtering by both genres and locale is not supported by this source" "Filtering by both genres and locale is not supported by this source"
const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED = const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED =

@ -33,11 +33,25 @@ abstract class MangaParser @InternalParsersApi constructor(
open val availableStates: Set<MangaState> open val availableStates: Set<MangaState>
get() = emptySet() get() = emptySet()
open val availableContentRating: Set<ContentRating>
get() = emptySet()
/** /**
* Whether parser supports filtering by more than one tag * Whether parser supports filtering by more than one tag
*/ */
open val isMultipleTagsSupported: Boolean = true open val isMultipleTagsSupported: Boolean = true
/**
* Whether parser supports tagsExclude field in filter
*/
open val isTagsExclusionSupported: Boolean = false
/**
* Whether parser supports searching by string query using [MangaListFilter.Search]
*/
open val isSearchSupported: Boolean = true
@Deprecated( @Deprecated(
message = "Use availableSortOrders instead", message = "Use availableSortOrders instead",
replaceWith = ReplaceWith("availableSortOrders"), replaceWith = ReplaceWith("availableSortOrders"),
@ -95,6 +109,7 @@ abstract class MangaParser @InternalParsersApi constructor(
offset: Int, offset: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead") ): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead")
@ -130,18 +145,51 @@ abstract class MangaParser @InternalParsersApi constructor(
"org.koitharu.kotatsu.parsers.model.MangaListFilter", "org.koitharu.kotatsu.parsers.model.MangaListFilter",
), ),
) )
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> { open suspend fun getList(
offset: Int,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder?,
): List<Manga> {
return getList( return getList(
offset, offset,
MangaListFilter.Advanced(sortOrder ?: defaultSortOrder, tags.orEmpty(), null, emptySet()), MangaListFilter.Advanced(
sortOrder = sortOrder ?: defaultSortOrder,
tags = tags.orEmpty(),
tagsExclude = tagsExclude.orEmpty(),
locale = null,
states = emptySet(),
contentRating = emptySet(),
),
) )
} }
@Suppress("DEPRECATION")
open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) { return when (filter) {
is MangaListFilter.Advanced -> getList(offset, null, filter.tags, filter.sortOrder) is MangaListFilter.Advanced -> getList(
is MangaListFilter.Search -> getList(offset, filter.query, null, defaultSortOrder) offset = offset,
null -> getList(offset, null, null, defaultSortOrder) query = null,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
sortOrder = filter.sortOrder,
)
is MangaListFilter.Search -> getList(
offset = offset,
query = filter.query,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
null -> getList(
offset = offset,
query = null,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
} }
} }

@ -36,6 +36,7 @@ abstract class PagedMangaParser(
offset: Int, offset: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser") ): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
@ -43,14 +44,35 @@ abstract class PagedMangaParser(
page: Int, page: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead") ): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead")
open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) { return when (filter) {
is MangaListFilter.Advanced -> getListPage(page, null, filter.tags, filter.sortOrder) is MangaListFilter.Advanced -> getListPage(
is MangaListFilter.Search -> getListPage(page, filter.query, null, defaultSortOrder) page = page,
null -> getListPage(page, null, null, defaultSortOrder) query = null,
tags = filter.tags,
tagsExclude = filter.tagsExclude,
sortOrder = filter.sortOrder,
)
is MangaListFilter.Search -> getListPage(
page = page,
query = filter.query,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
null -> getListPage(
page = page,
query = null,
tags = null,
tagsExclude = null,
sortOrder = defaultSortOrder,
)
} }
} }

@ -0,0 +1,7 @@
package org.koitharu.kotatsu.parsers.model
enum class ContentRating {
SAFE,
SUGGESTIVE,
ADULT
}

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
import org.koitharu.kotatsu.parsers.MangaParser
import java.util.* import java.util.*
sealed interface MangaListFilter { sealed interface MangaListFilter {
@ -8,6 +9,16 @@ sealed interface MangaListFilter {
val sortOrder: SortOrder? val sortOrder: SortOrder?
fun isValid(parser: MangaParser): Boolean = when (this) {
is Advanced -> (sortOrder in parser.availableSortOrders) &&
(tags.size <= 1 || parser.isMultipleTagsSupported) &&
(tagsExclude.isEmpty() || parser.isTagsExclusionSupported) &&
(contentRating.isEmpty() || parser.availableContentRating.containsAll(contentRating)) &&
(states.isEmpty() || parser.availableStates.containsAll(states))
is Search -> parser.isSearchSupported
}
data class Search( data class Search(
@JvmField val query: String, @JvmField val query: String,
) : MangaListFilter { ) : MangaListFilter {
@ -20,10 +31,63 @@ sealed interface MangaListFilter {
data class Advanced( data class Advanced(
override val sortOrder: SortOrder, override val sortOrder: SortOrder,
@JvmField val tags: Set<MangaTag>, @JvmField val tags: Set<MangaTag>,
@JvmField val tagsExclude: Set<MangaTag>,
@JvmField val locale: Locale?, @JvmField val locale: Locale?,
@JvmField val states: Set<MangaState>, @JvmField val states: Set<MangaState>,
@JvmField val contentRating: Set<ContentRating>,
) : MangaListFilter { ) : MangaListFilter {
override fun isEmpty(): Boolean = tags.isEmpty() && locale == null && states.isEmpty() override fun isEmpty(): Boolean =
tags.isEmpty() && tagsExclude.isEmpty() && locale == null && states.isEmpty() && contentRating.isEmpty()
fun newBuilder() = Builder(sortOrder)
.tags(tags)
.tagsExclude(tagsExclude)
.locale(locale)
.states(states)
.contentRatings(contentRating)
class Builder(sortOrder: SortOrder) {
private var _sortOrder: SortOrder = sortOrder
private var _tags: Set<MangaTag>? = null
private var _tagsExclude: Set<MangaTag>? = null
private var _locale: Locale? = null
private var _states: Set<MangaState>? = null
private var _contentRating: Set<ContentRating>? = null
fun sortOrder(order: SortOrder) = apply {
_sortOrder = order
}
fun tags(tags: Set<MangaTag>?) = apply {
_tags = tags
}
fun tagsExclude(tags: Set<MangaTag>?) = apply {
_tagsExclude = tags
}
fun locale(locale: Locale?) = apply {
_locale = locale
}
fun states(states: Set<MangaState>?) = apply {
_states = states
}
fun contentRatings(rating: Set<ContentRating>?) = apply {
_contentRating = rating
}
fun build() = Advanced(
sortOrder = _sortOrder,
tags = _tags.orEmpty(),
tagsExclude = _tagsExclude.orEmpty(),
locale = _locale,
states = _states.orEmpty(),
contentRating = _contentRating.orEmpty(),
)
}
} }
} }

@ -1,5 +1,5 @@
package org.koitharu.kotatsu.parsers.model package org.koitharu.kotatsu.parsers.model
enum class MangaState { enum class MangaState {
ONGOING, FINISHED, ABANDONED, PAUSED ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING
} }

@ -5,5 +5,6 @@ enum class SortOrder {
POPULARITY, POPULARITY,
RATING, RATING,
NEWEST, NEWEST,
ALPHABETICAL ALPHABETICAL,
} ALPHABETICAL_DESC
}

@ -35,6 +35,10 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val isTagsExclusionSupported = true
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
override val configKeyDomain = ConfigKey.Domain( override val configKeyDomain = ConfigKey.Domain(
"bato.to", "bato.to",
"batocomic.com", "batocomic.com",
@ -93,6 +97,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
MangaState.FINISHED -> "completed" MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "cancelled" MangaState.ABANDONED -> "cancelled"
MangaState.PAUSED -> "hiatus" MangaState.PAUSED -> "hiatus"
MangaState.UPCOMING -> "pending"
}, },
) )
} }
@ -102,13 +107,29 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
append(it.language) append(it.language)
} }
append("&genres=")
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&genres=")
appendAll(filter.tags, ",") { it.key } appendAll(filter.tags, ",") { it.key }
} }
append("|")
if (filter.tagsExclude.isNotEmpty()) {
appendAll(filter.tagsExclude, ",") { it.key }
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.oneOrThrowIfMany()?.let {
append(
when (it) {
ContentRating.SAFE -> append(",gore,bloody,violence,ecchi,adult,mature,smut,hentai")
else -> append("")
},
)
}
}
append("&page=") append("&page=")
append(page) append(page.toString())
} }
return parseList(url, page) return parseList(url, page)
@ -120,7 +141,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
append(domain) append(domain)
append("/browse?sort=update.za") append("/browse?sort=update.za")
append("&page=") append("&page=")
append(page) append(page.toString())
} }
return parseList(url, page) return parseList(url, page)
} }

@ -31,7 +31,8 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
SortOrder.RATING, SortOrder.RATING,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
@Volatile @Volatile
private var cachedTags: SparseArrayCompat<MangaTag>? = null private var cachedTags: SparseArrayCompat<MangaTag>? = null
@ -75,6 +76,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
MangaState.FINISHED -> "2" MangaState.FINISHED -> "2"
MangaState.ABANDONED -> "3" MangaState.ABANDONED -> "3"
MangaState.PAUSED -> "4" MangaState.PAUSED -> "4"
else -> ""
}, },
) )
} }
@ -117,8 +119,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
var alt = "" var alt = ""
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " } comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
return manga.copy( return manga.copy(
title = comic.getString("title"), altTitle = alt.ifEmpty { comic.getStringOrNull("title") },
altTitle = alt,
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"), isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"), description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet { tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {
@ -186,7 +187,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
val chap = jo.getStringOrNull("chap") val chap = jo.getStringOrNull("chap")
val locale = Locale.forLanguageTag(jo.getString("lang")) val locale = Locale.forLanguageTag(jo.getString("lang"))
val group = jo.optJSONArray("group_name")?.joinToString(", ") val group = jo.optJSONArray("group_name")?.joinToString(", ")
val branch = locale.getDisplayName(locale).toTitleCase(locale) + group val branch = locale.getDisplayName(locale).toTitleCase(locale) + " (" + group + ")"
MangaChapter( MangaChapter(
id = generateUid(jo.getLong("id")), id = generateUid(jo.getLong("id")),
name = buildString { name = buildString {

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.parsers.site.all package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import androidx.collection.set import androidx.collection.set
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.internal.StringUtil
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@ -26,9 +26,8 @@ internal class ExHentaiParser(
context: MangaLoaderContext, context: MangaLoaderContext,
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider { ) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
override val availableSortOrders: Set<SortOrder> = Collections.singleton( override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
SortOrder.NEWEST, override val isTagsExclusionSupported: Boolean = true
)
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain( get() = ConfigKey.Domain(
@ -44,6 +43,7 @@ internal class ExHentaiParser(
private var updateDm = false private var updateDm = false
private val nextPages = SparseArrayCompat<Long>() private val nextPages = SparseArrayCompat<Long>()
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false) private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
private val tagsMap = SuspendLazy(::fetchTags)
override val isAuthorized: Boolean override val isAuthorized: Boolean
get() { get() {
@ -93,30 +93,25 @@ internal class ExHentaiParser(
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
append("&f_search=") filter.toSearchQuery()?.let { sq ->
append("&f_search=")
var fCats = 0 append(sq.urlEncoded())
if (filter.tags.isNotEmpty()) {
filter.tags.forEach {
if (it.title.startsWith("- ")) {
it.key.toIntOrNull()?.let { fCats = fCats or it } ?: run {
search += it.key + " "
}
} else {
append(" tag:".urlEncoded())
append(it.key)
}
}
} }
if (filter.locale != null) { val catsOn = filter.tags.mapNotNullToSet { it.key.toIntOrNull() }
append(" language:".urlEncoded()) val catsOff = filter.tagsExclude.mapNotNullToSet { it.key.toIntOrNull() }
append(filter.locale.toLanguagePath()) if (catsOff.size >= 10) {
return emptyList()
}
var fCats = catsOn.fold(0, Int::or)
if (fCats != 0) {
fCats = 1023 - fCats
} }
fCats = catsOff.fold(fCats, Int::or)
if (fCats != 0) { if (fCats != 0) {
append("&f_cats=") append("&f_cats=")
append(1023 - fCats) append(fCats)
} }
} }
@ -182,15 +177,14 @@ internal class ExHentaiParser(
val title = root.getElementById("gd2") val title = root.getElementById("gd2")
val tagList = root.getElementById("taglist") val tagList = root.getElementById("taglist")
val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr") val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
val lang = val lang = root.getElementById("gd3")
root.getElementById("gd3")?.selectFirst("tr:contains(Language)")?.selectFirst(".gdt2")?.text() ?: "Unknown" ?.selectFirst("tr:contains(Language)")
?.selectFirst(".gdt2")?.ownTextOrNull()
val tagMap = getOrCreateTagMap() val tagMap = tagsMap.get()
val tagF = val tags = ArraySet<MangaTag>()
tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullToSet { tagMap[it.text()] }.orEmpty() tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
val tagM = tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullToSet { tagMap[it.text()] }.orEmpty()
val tags = tagF + tagM
return manga.copy( return manga.copy(
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title, title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
@ -265,21 +259,17 @@ internal class ExHentaiParser(
"unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," + "unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," +
"tomgirl,tall man,oni,shotacon,prostate massage,policeman,males only,huge penis,fox boy,feminization,dog boy,dickgirl on male,big penis" "tomgirl,tall man,oni,shotacon,prostate massage,policeman,males only,huge penis,fox boy,feminization,dog boy,dickgirl on male,big penis"
private var tagCache: ArrayMap<String, MangaTag>? = null
private val mutex = Mutex()
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
return getOrCreateTagMap().values.toSet() return tagsMap.get().values.toSet()
} }
protected suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock { private suspend fun fetchTags(): Map<String, MangaTag> {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>() val tagMap = ArrayMap<String, MangaTag>()
val tagElements = tags.split(",") val tagElements = tags.split(",")
for (el in tagElements) { for (el in tagElements) {
if (el.isEmpty()) continue if (el.isEmpty()) continue
tagMap[el] = MangaTag( tagMap[el] = MangaTag(
title = el, title = el.toTitleCase(Locale.ENGLISH),
key = el, key = el,
source = source, source = source,
) )
@ -289,16 +279,14 @@ internal class ExHentaiParser(
val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table") val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
root.select("div.cs").mapNotNullToSet { div -> root.select("div.cs").mapNotNullToSet { div ->
val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null
val name = "- " + div.text().toTitleCase() val name = div.text().toTitleCase(Locale.ENGLISH)
tagMap[name] = MangaTag( tagMap[name] = MangaTag(
title = name, title = "Kind: $name",
key = id.toString(), key = id.toString(),
source = source, source = source,
) )
} }
return tagMap
tagCache = tagMap
return@withLock tagMap
} }
override suspend fun getAvailableLocales(): Set<Locale> = setOf( override suspend fun getAvailableLocales(): Set<Locale> = setOf(
@ -403,4 +391,32 @@ internal class ExHentaiParser(
?.queryParameter("next") ?.queryParameter("next")
?.toLongOrNull() ?: 1 ?.toLongOrNull() ?: 1
} }
private fun MangaListFilter.Advanced.toSearchQuery(): String? {
val joiner = StringUtil.StringJoiner(" ")
for (tag in tags) {
if (tag.key.isNumeric()) {
continue
}
joiner.add("tag:\"")
joiner.append(tag.key)
joiner.append("\"$")
}
for (tag in tagsExclude) {
if (tag.key.isNumeric()) {
continue
}
joiner.add("-tag:\"")
joiner.append(tag.key)
joiner.append("\"$")
}
locale?.let { lc ->
joiner.add("language:\"")
joiner.append(lc.toLanguagePath())
joiner.append("\"$")
}
return joiner.complete().takeUnless { it.isEmpty() }
}
private fun String.isNumeric() = all { c -> c.isDigit() }
} }

@ -21,8 +21,6 @@ private const val CHAPTERS_FIRST_PAGE_SIZE = 120
private const val CHAPTERS_MAX_PAGE_SIZE = 500 private const val CHAPTERS_MAX_PAGE_SIZE = 500
private const val CHAPTERS_PARALLELISM = 3 private const val CHAPTERS_PARALLELISM = 3
private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks like a bug private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks like a bug
private const val CONTENT_RATING =
"contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic"
private const val LOCALE_FALLBACK = "en" private const val LOCALE_FALLBACK = "en"
@MangaSourceParser("MANGADEX", "MangaDex") @MangaSourceParser("MANGADEX", "MangaDex")
@ -32,7 +30,12 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java) override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported = true
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
@ -44,28 +47,41 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(PAGE_SIZE) append(PAGE_SIZE)
append("&offset=") append("&offset=")
append(offset) append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist&") append("&includes[]=cover_art&includes[]=author&includes[]=artist")
when (filter) { when (filter) {
is MangaListFilter.Search -> { is MangaListFilter.Search -> {
append("title=") append("&title=")
append(filter.query) append(filter.query)
append('&')
} }
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
filter.tags.forEach { tag -> filter.tags.forEach {
append("includedTags[]=") append("&includedTags[]=")
append(tag.key) append(it.key)
append('&') }
filter.tagsExclude.forEach {
append("&excludedTags[]=")
append(it.key)
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.forEach {
when (it) {
ContentRating.SAFE -> append("&contentRating[]=safe")
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
ContentRating.ADULT -> append("&contentRating[]=pornographic")
}
}
} }
append(CONTENT_RATING)
append("&order") append("&order")
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.UPDATED -> "[latestUploadedChapter]=desc" SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
SortOrder.RATING -> "[rating]=desc" SortOrder.RATING -> "[rating]=desc"
SortOrder.ALPHABETICAL -> "[title]=asc" SortOrder.ALPHABETICAL -> "[title]=asc"
SortOrder.ALPHABETICAL_DESC -> "[title]=desc"
SortOrder.NEWEST -> "[createdAt]=desc" SortOrder.NEWEST -> "[createdAt]=desc"
SortOrder.POPULARITY -> "[followedCount]=desc" SortOrder.POPULARITY -> "[followedCount]=desc"
}, },
@ -77,6 +93,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
MangaState.FINISHED -> append("completed") MangaState.FINISHED -> append("completed")
MangaState.ABANDONED -> append("cancelled") MangaState.ABANDONED -> append("cancelled")
MangaState.PAUSED -> append("hiatus") MangaState.PAUSED -> append("hiatus")
else -> append("")
} }
} }
filter.locale?.let { filter.locale?.let {
@ -248,8 +265,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(limitedLimit) append(limitedLimit)
append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=") append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=")
append(offset) append(offset)
append('&') append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
append(CONTENT_RATING)
} }
val json = webClient.httpGet(url).parseJson() val json = webClient.httpGet(url).parseJson()
if (json.getString("result") == "ok") { if (json.getString("result") == "ok") {

@ -20,6 +20,10 @@ internal class MangaPark(context: MangaLoaderContext) :
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
override val isTagsExclusionSupported = true
override val configKeyDomain = ConfigKey.Domain("mangapark.net") override val configKeyDomain = ConfigKey.Domain("mangapark.net")
private val tagsMap = SuspendLazy(::parseTags) private val tagsMap = SuspendLazy(::parseTags)
@ -42,9 +46,25 @@ internal class MangaPark(context: MangaLoaderContext) :
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
append("&genres=")
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&genres=") appendAll(filter.tags, ",") { it.key }
append(filter.tags.joinToString(",") { it.key }) }
append("|")
if (filter.tagsExclude.isNotEmpty()) {
appendAll(filter.tagsExclude, ",") { it.key }
}
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.oneOrThrowIfMany()?.let {
append(
when (it) {
ContentRating.SAFE -> append(",gore,bloody,violence,ecchi,adult,mature,smut,hentai")
else -> append("")
},
)
}
} }
filter.states.oneOrThrowIfMany()?.let { filter.states.oneOrThrowIfMany()?.let {
@ -55,6 +75,7 @@ internal class MangaPark(context: MangaLoaderContext) :
MangaState.FINISHED -> "completed" MangaState.FINISHED -> "completed"
MangaState.PAUSED -> "hiatus" MangaState.PAUSED -> "hiatus"
MangaState.ABANDONED -> "cancelled" MangaState.ABANDONED -> "cancelled"
MangaState.UPCOMING -> "pending"
}, },
) )
} }
@ -67,6 +88,7 @@ internal class MangaPark(context: MangaLoaderContext) :
SortOrder.NEWEST -> "field_create" SortOrder.NEWEST -> "field_create"
SortOrder.ALPHABETICAL -> "field_name" SortOrder.ALPHABETICAL -> "field_name"
SortOrder.RATING -> "field_score" SortOrder.RATING -> "field_score"
else -> ""
}, },
) )

@ -9,30 +9,14 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.*
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.SuspendLazy
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
import org.koitharu.kotatsu.parsers.util.json.toJSONList import org.koitharu.kotatsu.parsers.util.json.toJSONList
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.toCamelCase
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.EnumSet import java.util.*
import java.util.Locale
@MangaSourceParser("NINENINENINEHENTAI", "999Hentai", type = ContentType.HENTAI) @MangaSourceParser("NINENINENINEHENTAI", "999Hentai", type = ContentType.HENTAI)
internal class NineNineNineHentaiParser(context: MangaLoaderContext) : internal class NineNineNineHentaiParser(context: MangaLoaderContext) :

@ -25,6 +25,7 @@ internal class PapScan(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.POPULARITY, SortOrder.POPULARITY,
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
) )
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
@ -50,7 +51,8 @@ internal class PapScan(context: MangaLoaderContext) :
append("&sortBy=") append("&sortBy=")
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL_DESC -> append("name&asc=false")
SortOrder.ALPHABETICAL -> append("name&asc=true")
else -> append("updated") else -> append("updated")
} }

@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
@MangaSourceParser("KOMIKZOID", "KomikzoId", "id") @MangaSourceParser("KOMIKZOID", "KomikzoId", "id")
internal class KomikzoId(context: MangaLoaderContext) : internal class KomikzoId(context: MangaLoaderContext) :
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.xyz") AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.id")

@ -59,6 +59,7 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
MangaState.FINISHED -> "completed" MangaState.FINISHED -> "completed"
MangaState.ABANDONED -> "droped" MangaState.ABANDONED -> "droped"
MangaState.PAUSED -> "onhold" MangaState.PAUSED -> "onhold"
MangaState.UPCOMING -> "soon"
}, },
) )
} }

@ -20,7 +20,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
override val availableStates: Set<MangaState> = override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED) EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
override val configKeyDomain = ConfigKey.Domain("team11x11.com") override val configKeyDomain = ConfigKey.Domain("team11x11.fun")
override val isMultipleTagsSupported = false override val isMultipleTagsSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {

@ -31,6 +31,7 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
offset: Int, offset: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
if (!query.isNullOrEmpty()) { if (!query.isNullOrEmpty()) {

@ -19,6 +19,7 @@ internal class FlixScansOrg(context: MangaLoaderContext) : PagedMangaParser(cont
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
override val configKeyDomain = ConfigKey.Domain("flixscans.org") override val configKeyDomain = ConfigKey.Domain("flixscans.org")
override val isSearchSupported = false
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {

@ -68,6 +68,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az" SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top" SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update" SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new" SortOrder.NEWEST -> "new"

@ -20,7 +20,9 @@ internal class ReaperComics(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("reapercomics.com") override val configKeyDomain = ConfigKey.Domain("reaperscans.com")
override val isSearchSupported = false
override val headers: Headers = Headers.Builder() override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)

@ -21,6 +21,8 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("templescanesp.net") override val configKeyDomain = ConfigKey.Domain("templescanesp.net")
override val isSearchSupported = false
override val headers: Headers = Headers.Builder() override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()

@ -26,6 +26,7 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.NEWEST, SortOrder.NEWEST,
SortOrder.POPULARITY, SortOrder.POPULARITY,
@ -48,15 +49,14 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
append("?order_item=") append("?order_item=")
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> "likes_count" SortOrder.POPULARITY -> "likes_count&order_dir=desc"
SortOrder.UPDATED -> "release_date" SortOrder.UPDATED -> "release_date&order_dir=desc"
SortOrder.NEWEST -> "creation" SortOrder.NEWEST -> "creation&order_dir=desc"
SortOrder.ALPHABETICAL -> "alphabetically" SortOrder.ALPHABETICAL -> "alphabetically&order_dir=asc"
SortOrder.RATING -> "score" SortOrder.ALPHABETICAL_DESC -> "alphabetically&order_dir=desc"
SortOrder.RATING -> "score&order_dir=desc"
}, },
) )
append("&order_dir=desc")
append("&filter_by=title") append("&filter_by=title")
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
for (tag in filter.tags) { for (tag in filter.tags) {

@ -25,6 +25,7 @@ internal abstract class FmreaderParser(
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.POPULARITY, SortOrder.POPULARITY,
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
) )
override val availableStates: Set<MangaState> = EnumSet.of( override val availableStates: Set<MangaState> = EnumSet.of(
@ -85,7 +86,8 @@ internal abstract class FmreaderParser(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update") SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL -> append("name&sort_type=ASC")
SortOrder.ALPHABETICAL_DESC -> append("name&sort_type=DESC")
else -> append("last_update") else -> append("last_update")
} }

@ -48,6 +48,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "az" SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
SortOrder.POPULARITY -> "top" SortOrder.POPULARITY -> "top"
SortOrder.UPDATED -> "update" SortOrder.UPDATED -> "update"
SortOrder.NEWEST -> "new" SortOrder.NEWEST -> "new"

@ -44,7 +44,8 @@ internal class OlimpoScans(context: MangaLoaderContext) :
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("last_update") SortOrder.UPDATED -> append("last_update")
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL -> append("name&sort_type=ASC")
SortOrder.ALPHABETICAL_DESC -> append("name&sort_type=DESC")
else -> append("last_update") else -> append("last_update")
} }
} }

@ -24,11 +24,15 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
SortOrder.RATING, SortOrder.RATING,
SortOrder.NEWEST, SortOrder.NEWEST,
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
) )
override val configKeyDomain = ConfigKey.Domain("bentomanga.com", "www.bentomanga.com") override val configKeyDomain = ConfigKey.Domain("bentomanga.com", "www.bentomanga.com")
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported = true
init { init {
paginator.firstPage = 0 paginator.firstPage = 0
@ -47,21 +51,34 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
url.addQueryParameter( when (filter.sortOrder) {
"order_by", SortOrder.UPDATED -> url.addQueryParameter("order_by", "update")
when (filter.sortOrder) { .addQueryParameter("order", "desc")
SortOrder.UPDATED -> "update"
SortOrder.POPULARITY -> "views" SortOrder.POPULARITY -> url.addQueryParameter("order_by", "views")
SortOrder.RATING -> "top" .addQueryParameter("order", "desc")
SortOrder.NEWEST -> "create"
SortOrder.ALPHABETICAL -> "name" SortOrder.RATING -> url.addQueryParameter("order_by", "top")
}, .addQueryParameter("order", "desc")
)
SortOrder.NEWEST -> url.addQueryParameter("order_by", "create")
.addQueryParameter("order", "desc")
SortOrder.ALPHABETICAL -> url.addQueryParameter("order_by", "name")
.addQueryParameter("order", "asc")
SortOrder.ALPHABETICAL_DESC -> url.addQueryParameter("order_by", "name")
.addQueryParameter("order", "desc")
}
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key }) url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key })
} }
if (filter.tagsExclude.isNotEmpty()) {
url.addQueryParameter("withoutCategories", filter.tagsExclude.joinToString(",") { it.key })
}
filter.states.oneOrThrowIfMany()?.let { filter.states.oneOrThrowIfMany()?.let {
url.addQueryParameter( url.addQueryParameter(
"state", "state",
@ -70,6 +87,7 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
MangaState.FINISHED -> "2" MangaState.FINISHED -> "2"
MangaState.PAUSED -> "3" MangaState.PAUSED -> "3"
MangaState.ABANDONED -> "5" MangaState.ABANDONED -> "5"
else -> "1"
}, },
) )
} }

@ -23,6 +23,8 @@ internal class FuryoSociety(context: MangaLoaderContext) :
override val configKeyDomain = ConfigKey.Domain("furyosociety.com") override val configKeyDomain = ConfigKey.Domain("furyosociety.com")
override val isSearchSupported = false
override val headers: Headers = Headers.Builder() override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
.build() .build()

@ -22,7 +22,10 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
SortOrder.UPDATED, SortOrder.UPDATED,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isSearchSupported = false
override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com") override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com")
@ -68,6 +71,7 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
MangaState.FINISHED -> append("1") MangaState.FINISHED -> append("1")
MangaState.PAUSED -> append("4") MangaState.PAUSED -> append("4")
MangaState.ABANDONED -> append("3") MangaState.ABANDONED -> append("3")
else -> append("")
} }
} }

@ -182,8 +182,7 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
override suspend fun getAvailableTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml() val doc = webClient.httpGet("https://$domain/").parseHtml()
val body = doc.body() val body = doc.body()
val root = body.select(".asp_gochosen")[1] val list = body.select(".asp_gochosen")[1].select("option").orEmpty()
val list = root?.select("option").orEmpty()
return list.mapToSet { li -> return list.mapToSet { li ->
MangaTag( MangaTag(
key = li.text().lowercase().replace(" ", "-"), key = li.text().lowercase().replace(" ", "-"),

@ -1,88 +0,0 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.koitharu.kotatsu.parsers.ErrorMessages
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("NHENTAIUK", "NHentai.uk", type = ContentType.HENTAI)
internal class NHentaiUk(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.NHENTAIUK, "nhentai.uk", pageSize = 50) {
override val selectGallery = ".gallery"
override val selectGalleryLink = "a"
override val selectGalleryTitle = ".caption"
override val pathTagUrl = "/tags/popular?p="
override val selectTags = "#tag-container"
override val selectTag = "div.tag-container:contains(Tags:) span.tags"
override val selectAuthor = "div.tag-container:contains(Artists:) a"
override val selectLanguageChapter = "div.tag-container:contains(Languages:) a"
override val idImg = "image-container"
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
Locale.ENGLISH,
Locale.FRENCH,
Locale.JAPANESE,
Locale.CHINESE,
Locale("es"),
Locale("ru"),
Locale("ko"),
Locale.GERMAN,
Locale("pt"),
Locale.ITALIAN,
Locale("tr"),
)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val url = buildString {
append("https://")
append(domain)
when (filter) {
is MangaListFilter.Search -> {
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
}
is MangaListFilter.Advanced -> {
when {
filter.locale != null && filter.tags.isNotEmpty() -> {
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
}
filter.locale != null -> {
append("/language")
append(filter.locale.toLanguagePath())
append("/?p=")
}
filter.tags.isNotEmpty() -> {
filter.tags.oneOrThrowIfMany()?.let {
append("/tag/")
append(it.key)
}
append("/?p=")
}
else -> {
append("/home?p=")
}
}
}
null -> append("/?")
}
append(page.toString())
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.requireElementById(idImg).selectFirstOrThrow("img").src() ?: root.parseFailed("Image src not found")
}
}

@ -27,7 +27,8 @@ internal abstract class HeanCms(
SortOrder.POPULARITY, SortOrder.POPULARITY,
) )
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java) override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val headers: Headers = Headers.Builder() override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP) .add("User-Agent", UserAgents.CHROME_DESKTOP)
@ -56,18 +57,19 @@ internal abstract class HeanCms(
MangaState.FINISHED -> "Completed" MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped" MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus" MangaState.PAUSED -> "Hiatus"
else -> ""
}, },
) )
} }
append("&order=desc")
append("&orderBy=") append("&orderBy=")
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views") SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at") SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title") SortOrder.ALPHABETICAL -> append("title&order=desc")
else -> append("latest") SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
else -> append("latest&order=desc")
} }
append("&series_type=Comic&perPage=12") append("&series_type=Comic&perPage=12")
append("&tags_ids=") append("&tags_ids=")

@ -33,18 +33,19 @@ internal class YugenMangasEs(context: MangaLoaderContext) :
MangaState.FINISHED -> "Completed" MangaState.FINISHED -> "Completed"
MangaState.ABANDONED -> "Dropped" MangaState.ABANDONED -> "Dropped"
MangaState.PAUSED -> "Hiatus" MangaState.PAUSED -> "Hiatus"
else -> ""
}, },
) )
} }
append("&order=desc")
append("&orderBy=") append("&orderBy=")
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.POPULARITY -> append("total_views") SortOrder.POPULARITY -> append("total_views&order=desc")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest&order=desc")
SortOrder.NEWEST -> append("created_at") SortOrder.NEWEST -> append("created_at&order=desc")
SortOrder.ALPHABETICAL -> append("title") SortOrder.ALPHABETICAL -> append("title&order=desc")
else -> append("latest") SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
else -> append("latest&order=desc")
} }
append("&series_type=Comic&perPage=12") append("&series_type=Comic&perPage=12")
append("&tags_ids=") append("&tags_ids=")

@ -23,6 +23,8 @@ internal abstract class HeanCmsAlt(
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val isSearchSupported = false
protected open val listUrl = "/comics" protected open val listUrl = "/comics"
protected open val datePattern = "MMMM d, yyyy" protected open val datePattern = "MMMM d, yyyy"

@ -151,6 +151,15 @@ internal abstract class MadaraParser(
"En attente", "En attente",
) )
@JvmField
protected val upcoming: Set<String> = hashSetOf(
"Upcoming",
"upcoming",
"لم تُنشَر بعد",
"Prochainement",
"À venir",
)
// Change these values only if the site does not support manga listings via ajax // Change these values only if the site does not support manga listings via ajax
protected open val withoutAjax = false protected open val withoutAjax = false
@ -203,6 +212,7 @@ internal abstract class MadaraParser(
MangaState.FINISHED -> append("end") MangaState.FINISHED -> append("end")
MangaState.ABANDONED -> append("canceled") MangaState.ABANDONED -> append("canceled")
MangaState.PAUSED -> append("on-hold") MangaState.PAUSED -> append("on-hold")
MangaState.UPCOMING -> append("upcoming")
} }
} }
append("&") append("&")
@ -215,6 +225,7 @@ internal abstract class MadaraParser(
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("latest")
} }
} }
@ -255,6 +266,7 @@ internal abstract class MadaraParser(
} }
SortOrder.RATING -> {} SortOrder.RATING -> {}
else -> payload["vars[meta_key]"] = "_latest_update"
} }
filter.states.forEach { filter.states.forEach {
@ -264,6 +276,7 @@ internal abstract class MadaraParser(
MangaState.FINISHED -> "end" MangaState.FINISHED -> "end"
MangaState.ABANDONED -> "canceled" MangaState.ABANDONED -> "canceled"
MangaState.PAUSED -> "on-hold" MangaState.PAUSED -> "on-hold"
MangaState.UPCOMING -> "upcoming"
} }
} }
} }
@ -310,6 +323,7 @@ internal abstract class MadaraParser(
in finished -> MangaState.FINISHED in finished -> MangaState.FINISHED
in abandoned -> MangaState.ABANDONED in abandoned -> MangaState.ABANDONED
in paused -> MangaState.PAUSED in paused -> MangaState.PAUSED
in upcoming -> MangaState.UPCOMING
else -> null else -> null
}, },
source = source, source = source,

@ -1,13 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import java.util.Locale
@MangaSourceParser("AKUMANGA", "AkuManga", "ar")
internal class AkuManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.AKUMANGA, "akumanga.com") {
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("FALCONMANGA", "FalconManga", "ar")
internal class FalconManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.FALCONMANGA, "falconmanga.com") {
override val datePattern = "d MMMM، yyyy"
}

@ -1,13 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGALEKS", "MangaLeks", "ar")
internal class MangaLeks(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGALEKS, "mangaleks.com") {
override val datePattern = "yyyy/MM/dd"
override val postReq = true
}

@ -5,8 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("SHADOWXMANGA", "Shadow X Manga", "ar") @MangaSourceParser("SHADOWXMANGA", "ShadowXManga", "ar")
internal class ShadowxManga(context: MangaLoaderContext) : internal class ShadowxManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.SHADOWXMANGA, "shadowxmanga.com") { MadaraParser(context, MangaSource.SHADOWXMANGA, "www.shadowxmanga.com") {
override val datePattern = "yyyy/MM/dd" override val datePattern = "yyyy/MM/dd"
} }

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.madara.de
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGALESEN", "MangaLesen", "de")
internal class MangaLesen(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGALESEN, "mangalesen.net")

@ -1,9 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("CM_READER", "CmReader", "en")
internal class CmReader(context: MangaLoaderContext) : MadaraParser(context, MangaSource.CM_READER, "cmreader.info")

@ -75,7 +75,7 @@ internal class FireScans(context: MangaLoaderContext) :
} }
} }
fun String.decodeHex(): ByteArray { private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" } check(length % 2 == 0) { "Must have an even length" }
return chunked(2).map { it.toInt(16).toByte() }.toByteArray() return chunked(2).map { it.toInt(16).toByte() }.toByteArray()

@ -68,6 +68,7 @@ internal class Hentai4Free(context: MangaLoaderContext) :
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("latest")
} }
} }

@ -61,6 +61,7 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
MangaState.FINISHED -> append("end") MangaState.FINISHED -> append("end")
MangaState.ABANDONED -> append("canceled") MangaState.ABANDONED -> append("canceled")
MangaState.PAUSED -> append("on-hold") MangaState.PAUSED -> append("on-hold")
MangaState.UPCOMING -> append("upcoming")
} }
} }
append("&") append("&")
@ -73,6 +74,7 @@ internal class IsekaiScanEuParser(context: MangaLoaderContext) :
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("latest")
} }
} }

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("JAIMINISBOX", "JaiminisBox", "en")
internal class Jaiminisbox(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.JAIMINISBOX, "jaiminisbox.net")

@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("JIMANGA", "JiManga", "en") @MangaSourceParser("JIMANGA", "S2Manga.io", "en")
internal class JiManga(context: MangaLoaderContext) : internal class JiManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.JIMANGA, "jimanga.com") MadaraParser(context, MangaSource.JIMANGA, "s2manga.io")

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANGA_3S", "Manga3s", "en")
internal class Manga3s(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGA_3S, "manga3s.com") {
override val tagPrefix = "manhwa-genre/"
override val datePattern = "MMMM dd, yyyy"
}

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANGACV", "MangaCv", "en")
internal class MangaCv(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGACV, "mangacv.com", pageSize = 10) {
override val datePattern = "MMMM dd, yyyy"
}

@ -62,6 +62,7 @@ internal class MangaDass(context: MangaLoaderContext) :
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("latest")
} }
} }

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANGA_ONLINE", "MangaOnline.team", "en")
internal class MangaOnline(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGA_ONLINE, "mangaonline.team", 18)

@ -5,8 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGAROSIE", "MangaRosie", "en") @MangaSourceParser("MANGAROSIE", "Toon69", "en")
internal class MangaRosie(context: MangaLoaderContext) : internal class MangaRosie(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGAROSIE, "mangarosie.in", pageSize = 16) { MadaraParser(context, MangaSource.MANGAROSIE, "toon69.com", pageSize = 16) {
override val datePattern = "MMMM dd, yyyy" override val datePattern = "MMMM dd, yyyy"
} }

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANGATX", "MangaTx", "en")
internal class MangaTx(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGATX, "mangatx.com") {
override val datePattern = "MMMM dd, yyyy"
}

@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGATXUNOFFICIAL", "Manga-Tx.com", "en") @MangaSourceParser("MANGATXUNOFFICIAL", "MangaEmpress", "en")
internal class MangaTxUnofficial(context: MangaLoaderContext) : internal class MangaTxUnofficial(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGATXUNOFFICIAL, "manga-tx.com") MadaraParser(context, MangaSource.MANGATXUNOFFICIAL, "mangaempress.com")

@ -1,11 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.en
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.site.madara.MadaraParser
@MangaSourceParser("MANGAGOYAOI", "MangaGoYaoi", "en", ContentType.HENTAI)
internal class Mangagoyaoi(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGAGOYAOI, "mangagoyaoi.com")

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANGANELO", "MangaNelo.biz", "en")
internal class Manganelo(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGANELO, "manganelo.biz", 10) {
override val postReq = true
}

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANGASTIC", "Mangastic", "en")
internal class Mangastic(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGASTIC, "mangastic.cc", 20)

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANHUADEX", "ManhuaDex", "en")
internal class ManhuaDex(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANHUADEX, "manhuadex.com")

@ -1,13 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANHUAMIX", "ManhuaMix", "en")
internal class Manhuamix(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANHUAMIX, "manhuamix.com", 20) {
override val tagPrefix = "manhua-genre/"
override val listUrl = "manhua/"
}

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("MANHWA2READ", "Manhwa2read", "en")
internal class Manhwa2Read(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANHWA2READ, "manhwa2read.com")

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("STMANHWA", "1stManhwa", "en")
internal class StManhwa(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.STMANHWA, "1stmanhwa.com")

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("STKISSMANGA_TV", "1stKissManga.tv", "en")
internal class StkissMangaTv(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.STKISSMANGA_TV, "1stkissmanga.tv", 20) {
override val postReq = true
}

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("TOONILYNET", "Toonily.net", "en")
internal class ToonilyNet(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.TOONILYNET, "toonily.net")

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("TOPMANHUA", "TopManhua", "en") @MangaSourceParser("TOPMANHUA", "TopManhua", "en")
internal class TopManhua(context: MangaLoaderContext) : internal class TopManhua(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.TOPMANHUA, "www.topmanhua.com") { MadaraParser(context, MangaSource.TOPMANHUA, "topmanhua.com") {
override val tagPrefix = "manhua-genre/" override val tagPrefix = "manhua-genre/"
override val datePattern = "MM/dd/yyyy" override val datePattern = "MM/dd/yyyy"
} }

@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("CONSEJODEMATONES", "ConsejoDeMatones", "es") @MangaSourceParser("KENHUAV2SCANK", "Kenhuav2Scan", "es")
internal class ConsejoDeMatones(context: MangaLoaderContext) : internal class Kenhuav2Scan(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.CONSEJODEMATONES, "www.consejodematones.xyz") MadaraParser(context, MangaSource.KENHUAV2SCANK, "kenhuav2scan.com")

@ -1,13 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("UNITOONOFICIAL", "UniToonOficial", "es")
internal class UniToonOficial(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.UNITOONOFICIAL, "unitoonoficial.com") {
override val datePattern = "dd/MM/yyyy"
override val tagPrefix = "generos/"
}

@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("ASTRALMANGA", "AstralManga", "fr") @MangaSourceParser("ASTRALMANGA", "AstralManga", "fr")
internal class AstralManga(context: MangaLoaderContext) : internal class AstralManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.ASTRALMANGA, "astral-manga.fr", pageSize = 12) { MadaraParser(context, MangaSource.ASTRALMANGA, "astral-manga.fr") {
override val datePattern = "dd/MM/yyyy" override val datePattern = "dd/MM/yyyy"
} }

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
@MangaSourceParser("KARATCAMSCANS", "KaratcamScans", "fr")
internal class KaratcamScans(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.KARATCAMSCANS, "karatcam-scans.fr") {
override val tagPrefix = "webtoon-genre/"
override val listUrl = "webtoon/"
override val datePattern = "dd/MM/yyyy"
}

@ -1,15 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
import java.util.Locale
@MangaSourceParser("HWAGO", "Hwago", "id")
internal class Hwago(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.HWAGO, "hwago.id", 10) {
override val listUrl = "komik/"
override val tagPrefix = "genre/"
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
import java.util.Locale
@MangaSourceParser("IMMORTALUPDATESID", "ImmortalUpdates", "id")
internal class ImmortalUpdatesId(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.IMMORTALUPDATESID, "immortalupdates.id") {
override val datePattern = "d MMMM yyyy"
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -1,16 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.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.site.madara.MadaraParser
import java.util.Locale
@MangaSourceParser("KOMIKSA", "KomikSay", "id")
internal class Komiksay(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.KOMIKSA, "komiksay.site") {
override val tagPrefix = "komik-genre/"
override val listUrl = "komik/"
override val datePattern = "MMMM d"
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -2,14 +2,24 @@ package org.koitharu.kotatsu.parsers.site.madara.id
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import java.util.Locale import java.util.*
@MangaSourceParser("SHINIGAMI", "Shinigami", "id") @MangaSourceParser("SHINIGAMI", "Shinigami", "id")
internal class Shinigami(context: MangaLoaderContext) : internal class Shinigami(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.SHINIGAMI, "shinigami.moe", 10) { MadaraParser(context, MangaSource.SHINIGAMI, "shinigami.moe", 10) {
private val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_MOBILE)
override val tagPrefix = "genre/" override val tagPrefix = "genre/"
override val listUrl = "series/" override val listUrl = "series/"
override val sourceLocale: Locale = Locale.ENGLISH override val sourceLocale: Locale = Locale.ENGLISH
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
} }

@ -5,9 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("ZEROSCAN", "ZeroScan", "pt") @MangaSourceParser("BURNINGSCANS", "BurningScans", "pt")
internal class ZeroScan(context: MangaLoaderContext) : internal class BurningScans(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.ZEROSCAN, "zeroscan.com.br") { MadaraParser(context, MangaSource.BURNINGSCANS, "burningscans.com") {
override val postReq = true override val datePattern = "dd/MM/yyyy"
override val datePattern: String = "dd/MM/yyyy"
} }

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("LADYESTELARSCAN", "Lady Estelar Scan", "pt")
internal class LadyestelarScan(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.LADYESTELARSCAN, "ladyestelarscan.com.br", 10) {
override val datePattern: String = "dd/MM/yyyy"
}

@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("VORTCESCAN", "VortceScan", "pt")
internal class VortceScan(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.VORTCESCAN, "vortcescan.com.br", pageSize = 10) {
override val datePattern: String = "d 'de' MMMMM 'de' yyyy"
}

@ -1,15 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.tr
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("CIZGIROMANARSIVI", "Cizgiromanarsivi", "tr")
internal class Cizgiromanarsivi(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.CIZGIROMANARSIVI, "cizgiromanarsivi.com", 24) {
override val stylePage = ""
override val tagPrefix = "kategori/"
override val listUrl = "seri/"
override val datePattern = "dd/MM/yyyy"
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("DOMALFANSB", "DomalFansub", "tr") @MangaSourceParser("DOMALFANSB", "DomalFansub", "tr")
internal class DomalFansb(context: MangaLoaderContext) : internal class DomalFansb(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.DOMALFANSB, "domalfansb.com") { MadaraParser(context, MangaSource.DOMALFANSB, "domalfansub.com.tr") {
override val datePattern = "d MMMM yyyy" override val datePattern = "d MMMM yyyy"
override val tagPrefix = "manga-turleri/" override val tagPrefix = "manga-turleri/"
} }

@ -7,5 +7,5 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("GHOSTFANSUB", "GhostFansub", "tr") @MangaSourceParser("GHOSTFANSUB", "GhostFansub", "tr")
internal class GhostFansub(context: MangaLoaderContext) : internal class GhostFansub(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.GHOSTFANSUB, "ghostfansub.online", 18) MadaraParser(context, MangaSource.GHOSTFANSUB, "ghostfansub.co", 18)
// you now need to log in to access content // you now need to log in to access content

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.tr
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGADIYARI", "MangaDiyari", "tr")
internal class MangaDiyari(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGADIYARI, "manga-diyari.com", 10)

@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGAWOW", "MangaWow", "tr") @MangaSourceParser("MANGAWOW", "MangaWow", "tr")
internal class MangaWow(context: MangaLoaderContext) : internal class MangaWow(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGAWOW, "mangawow.com", 18) { MadaraParser(context, MangaSource.MANGAWOW, "mangawow.org", 18) {
override val datePattern = "d MMMM yyyy" override val datePattern = "d MMMM yyyy"
} }

@ -6,6 +6,8 @@ import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("TONIZUTOON", "TonizuToon", "tr", ContentType.HENTAI) @MangaSourceParser("TONIZUTOON", "ToniZu.com", "tr", ContentType.HENTAI)
internal class Tonizutoon(context: MangaLoaderContext) : internal class Tonizutoon(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.TONIZUTOON, "tonizutoon.com") MadaraParser(context, MangaSource.TONIZUTOON, "tonizu.com") {
override val datePattern = "dd/mm/yyyy"
}

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.tr
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("WEBTOONEVRENI", "WebtoonEvreni", "tr")
internal class Webtoonevreni(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.WEBTOONEVRENI, "webtoonevreni.net", 10)

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.parsers.site.madara.vi
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MI2MANGA", "Mi2Manga", "vi")
internal class Mi2Manga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MI2MANGA, "www.mi2manga2.com") {
override val listUrl = "truyen-tranh/"
override val tagPrefix = "the-loai/"
override val datePattern = "d MMMM, yyyy"
}

@ -9,9 +9,9 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet import java.util.EnumSet
@MangaSourceParser("SAYTRUYENHAY", "Saytruyenhay", "vi") @MangaSourceParser("SAYTRUYENHAY", "PheTruyen", "vi")
internal class Saytruyenhay(context: MangaLoaderContext) : internal class Saytruyenhay(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.SAYTRUYENHAY, "saytruyenhay.com", 40) { MadaraParser(context, MangaSource.SAYTRUYENHAY, "phetruyen.pro", 40) {
override val tagPrefix = "genre/" override val tagPrefix = "genre/"
override val withoutAjax = true override val withoutAjax = true

@ -76,6 +76,7 @@ internal abstract class MadthemeParser(
SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty. SortOrder.ALPHABETICAL -> append("name") // On some sites without tags or searches, the alphabetical option is empty.
SortOrder.NEWEST -> append("created_at") SortOrder.NEWEST -> append("created_at")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("updated_at")
} }
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.forEach { filter.tags.forEach {
@ -196,7 +197,10 @@ internal abstract class MadthemeParser(
protected open suspend fun getChapters(doc: Document): List<MangaChapter> { protected open suspend fun getChapters(doc: Document): List<MangaChapter> {
val dateFormat = SimpleDateFormat(datePattern, sourceLocale) val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li -> val slug = doc.selectFirstOrThrow("script:containsData(bookSlug)").data().substringAfter("bookSlug = \"")
.substringBefore("\";")
val docChapter = webClient.httpGet("https://$domain/api/manga/$slug/chapters?source=detail").parseHtml()
return docChapter.select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a") val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href") val href = a.attrAsRelativeUrl("href")
val dateText = li.selectFirst(selectDate)?.text() val dateText = li.selectFirst(selectDate)?.text()

@ -1,22 +1,51 @@
package org.koitharu.kotatsu.parsers.site.madtheme.en package org.koitharu.kotatsu.parsers.site.madtheme.en
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import java.text.SimpleDateFormat
@MangaSourceParser("MANGAJINX", "MangaJinx", "en") @MangaSourceParser("MANGAJINX", "MangaJinx", "en")
internal class MangaJinx(context: MangaLoaderContext) : internal class MangaJinx(context: MangaLoaderContext) :
MadthemeParser(context, MangaSource.MANGAJINX, "mangajinx.com") { MadthemeParser(context, MangaSource.MANGAJINX, "mangajinx.com") {
override val listUrl = "search" override val listUrl = "search"
override suspend fun getChapters(doc: Document): List<MangaChapter> {
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
val id = doc.selectFirstOrThrow("script:containsData(bookId)").data().substringAfter("bookId = ")
.substringBefore(";")
val docChapter = webClient.httpGet("https://$domain/service/backend/chaplist/?manga_id=$id").parseHtml()
return docChapter.select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
val dateText = li.selectFirst(selectDate)?.text()
MangaChapter(
id = generateUid(href),
name = li.selectFirstOrThrow(".chapter-title").text(),
number = i + 1,
url = href,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
source = source,
scanlator = null,
branch = null,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain) val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val docs = webClient.httpGet(chapterUrl).parseHtml() val docs = webClient.httpGet(chapterUrl).parseHtml()

@ -1,10 +1,12 @@
package org.koitharu.kotatsu.parsers.site.madtheme.en package org.koitharu.kotatsu.parsers.site.madtheme.en
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser import org.koitharu.kotatsu.parsers.site.madtheme.MadthemeParser
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@MangaSourceParser("MANHUASCAN", "ManhuaScan.io", "en") @MangaSourceParser("MANHUASCAN", "ManhuaScan.io", "en")
@ -35,6 +37,7 @@ internal class ManhuaScan(context: MangaLoaderContext) :
SortOrder.ALPHABETICAL -> append("name") SortOrder.ALPHABETICAL -> append("name")
SortOrder.NEWEST -> append("created_at") SortOrder.NEWEST -> append("created_at")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("updated_at")
} }
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.forEach { filter.tags.forEach {
@ -93,6 +96,31 @@ internal class ManhuaScan(context: MangaLoaderContext) :
} }
} }
override suspend fun getChapters(doc: Document): List<MangaChapter> {
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
val id = doc.selectFirstOrThrow("script:containsData(bookId)").data().substringAfter("bookId = ")
.substringBefore(";")
val docChapter = webClient.httpGet("https://$domain/service/backend/chaplist/?manga_id=$id").parseHtml()
return docChapter.select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
val dateText = li.selectFirst(selectDate)?.text()
MangaChapter(
id = generateUid(href),
name = li.selectFirstOrThrow(".chapter-title").text(),
number = i + 1,
url = href,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
source = source,
scanlator = null,
branch = null,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val chapterUrl = chapter.url.toAbsoluteUrl(domain) val chapterUrl = chapter.url.toAbsoluteUrl(domain)
val docs = webClient.httpGet(chapterUrl).parseHtml() val docs = webClient.httpGet(chapterUrl).parseHtml()

@ -14,7 +14,7 @@ import java.util.EnumSet
internal class MangakakalotTv(context: MangaLoaderContext) : internal class MangakakalotTv(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.MANGAKAKALOTTV) { MangaboxParser(context, MangaSource.MANGAKAKALOTTV) {
override val configKeyDomain = ConfigKey.Domain("ww6.mangakakalot.tv") override val configKeyDomain = ConfigKey.Domain("ww7.mangakakalot.tv")
override val searchUrl = "/search/" override val searchUrl = "/search/"
override val listUrl = "/manga_list" override val listUrl = "/manga_list"
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(

@ -30,7 +30,13 @@ internal abstract class MangaReaderParser(
override val configKeyDomain = ConfigKey.Domain(domain) override val configKeyDomain = ConfigKey.Domain(domain)
override val availableSortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST) get() = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
SortOrder.NEWEST,
)
override val availableStates: Set<MangaState> override val availableStates: Set<MangaState>
get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED) get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
@ -63,6 +69,7 @@ internal abstract class MangaReaderParser(
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "title" SortOrder.ALPHABETICAL -> "title"
SortOrder.ALPHABETICAL_DESC -> "titlereverse"
SortOrder.NEWEST -> "latest" SortOrder.NEWEST -> "latest"
SortOrder.POPULARITY -> "popular" SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update" SortOrder.UPDATED -> "update"

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("GALAXYACTION", "GalaxyAction", "ar")
internal class GalaxyAction(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.GALAXYACTION, "galaxyaction.site", pageSize = 20, searchPageSize = 10)

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("MANGAPROTM", "MangaPro", "ar")
internal class MangaProtm(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANGAPROTM, "mangapro.co", pageSize = 20, searchPageSize = 20)

@ -1,89 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.WordSet
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("OXAPK", "Oxapk", "ar")
internal class Oxapk(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.OXAPK, "oxapk.com", pageSize = 24, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
val chapters = docs.select("#chapterlist > ul > li").mapChapters(reversed = true) { index, element ->
val url = element.selectFirst("a")?.attrAsRelativeUrl("href") ?: return@mapChapters null
MangaChapter(
id = generateUid(url),
name = docs.selectFirst("a.chapter-link-item")?.ownText().orEmpty(),
url = url,
number = index + 1,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
element.selectFirst("div.chapter-link-time")?.text(),
),
branch = null,
source = source,
)
}
return parseInfo(docs, manga, chapters)
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
date ?: return 0
return when {
date.endsWith("منذ ", ignoreCase = true) -> {
parseRelativeDate(date)
}
else -> dateFormat.tryParse(date)
}
}
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
WordSet("أيام").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("hour", "hours").anyWordIn(date) -> cal.apply {
add(
Calendar.HOUR,
-number,
)
}.timeInMillis
WordSet(
"mins",
).anyWordIn(date) -> cal.apply {
add(
Calendar.MINUTE,
-number,
)
}.timeInMillis
WordSet("second").anyWordIn(date) -> cal.apply {
add(
Calendar.SECOND,
-number,
)
}.timeInMillis
WordSet("أشهر").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
}

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("OZULSHOJO", "OzulShojo", "ar")
internal class OzulShojo(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.OZULSHOJO, "ozulshojo.com", pageSize = 20, searchPageSize = 10)

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.ar
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("SPIDERSCANS", "SpiderScans", "ar")
internal class SpiderScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.SPIDERSCANS, "spiderscans.com", pageSize = 20, searchPageSize = 10)

@ -36,6 +36,7 @@ internal class SwaTeam(context: MangaLoaderContext) :
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> "a-z" SortOrder.ALPHABETICAL -> "a-z"
SortOrder.ALPHABETICAL_DESC -> "z-a"
SortOrder.NEWEST -> "added" SortOrder.NEWEST -> "added"
SortOrder.POPULARITY -> "popular" SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update" SortOrder.UPDATED -> "update"

@ -1,10 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.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.site.mangareader.MangaReaderParser
@MangaSourceParser("AQUAMANGA_LIVE", "AquaManga.live", "en")
internal class AquaManga(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.AQUAMANGA_LIVE, "aquamanga.live", pageSize = 30, searchPageSize = 10)

@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("ARVENSCANS", "ArvenScans", "en") @MangaSourceParser("ARVENSCANS", "ArvenScans", "en")
internal class ArvenScans(context: MangaLoaderContext) : internal class ArvenScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.ARVENSCANS, "arvenscans.com", pageSize = 20, searchPageSize = 10) { MangaReaderParser(context, MangaSource.ARVENSCANS, "arvenscans.org", pageSize = 20, searchPageSize = 10) {
override val listUrl = "/series" override val listUrl = "/series"
} }

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

Loading…
Cancel
Save