11 Commits

Author SHA1 Message Date
4a8c7fa36a Fix urlDecode usage 2024-02-07 09:33:33 +02:00
1251a6cd75 Merge pull request #548 from Ah3ron/patch-1
Fix tag filtering for Russian characters
2024-02-07 09:11:57 +02:00
Ah3ron
0ce8eae969 Fix tag filtering for Russian characters 2024-02-05 19:45:35 +03:00
devi
8b9c76fca5 LerYaoi #512
StellarSaber #533
Update Url ( #530 )
Remove dead sources
2024-02-03 11:00:02 +01:00
52e4e6b802 Update collections library 2024-01-31 15:04:00 +02:00
57c9d26916 Fix android compatibility 2024-01-31 14:45:39 +02:00
19d7003a9e [BatoTo] Fix pages parsing #521 #522 2024-01-31 14:31:06 +02:00
b11e50ab22 Fixes batch 2024-01-30 13:09:27 +02:00
a03ab0e0c0 Update tests 2024-01-30 11:16:28 +02:00
05d4db00b3 [ResetScans] Fix chapters names #518 2024-01-30 11:05:41 +02:00
f4aaa14fc5 Simplify workflow 2024-01-30 10:36:25 +02:00
55 changed files with 183 additions and 135 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
pull_request:
paths:
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/site/**'
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/**'
permissions:
contents: read
@@ -19,9 +19,4 @@ jobs:
java-version: '11'
distribution: 'temurin'
cache: 'gradle'
- run: ./gradlew :test --tests "org.koitharu.kotatsu.parsers.MangaParserTest" || true
- run: ./gradlew generateTestsReport
- uses: actions/upload-artifact@v3
with:
name: Report
path: build/test-results-html/TEST-org.koitharu.kotatsu.parsers.MangaParserTest.htm
- run: ./gradlew assemble

View File

@@ -3,7 +3,7 @@ import tasks.ReportGenerateTask
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
id 'com.google.devtools.ksp' version '1.9.22-1.0.16'
id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
id 'maven-publish'
}
@@ -58,7 +58,7 @@ dependencies {
implementation 'com.squareup.okio:okio:3.7.0'
api 'org.jsoup:jsoup:1.17.2'
implementation 'org.json:json:20231013'
implementation 'androidx.collection:collection-ktx:1.3.0'
implementation 'androidx.collection:collection:1.4.0'
ksp project(':kotatsu-parsers-ksp')

View File

@@ -1,10 +1,15 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.9.20'
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
}
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation gradleApi()
implementation 'org.simpleframework:simple-xml:2.7.1'

View File

@@ -2,6 +2,10 @@ plugins {
id 'org.jetbrains.kotlin.jvm'
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.16'
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.17'
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
@@ -81,7 +82,8 @@ abstract class MangaParser @InternalParsersApi constructor(
/**
* Used as fallback if value of `sortOrder` passed to [getList] is null
*/
protected open val defaultSortOrder: SortOrder
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
open val defaultSortOrder: SortOrder
get() {
val supported = availableSortOrders
return SortOrder.entries.first { it in supported }

View File

@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.isNullOrEmpty
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
@@ -205,7 +206,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
val url = images.getString(i)
result += MangaPage(
id = generateUid(url),
url = url + "?" + args.getString(i),
url = if (args.length() == 0) url else url + "?" + args.getString(i),
preview = null,
source = source,
)
@@ -226,7 +227,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
jo.keys().forEach { key ->
val item = jo.getJSONObject(key)
result += MangaTag(
title = item.getString("text").toTitleCase(),
title = item.getString("text").toTitleCase(Locale.ENGLISH),
key = item.getString("file"),
source = source,
)

View File

@@ -34,8 +34,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
@Volatile
private var cachedTags: SparseArrayCompat<MangaTag>? = null
private val tagsArray = SuspendLazy(::loadTags)
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val domain = domain
@@ -83,7 +82,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
}
}
val ja = webClient.httpGet(url.build()).parseJsonArray()
val tagsMap = cachedTags ?: loadTags()
val tagsMap = tagsArray.get()
return ja.mapJSON { jo ->
val slug = jo.getString("slug")
Manga(
@@ -151,7 +150,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
}
override suspend fun getAvailableTags(): Set<MangaTag> {
val sparseArray = cachedTags ?: loadTags()
val sparseArray = tagsArray.get()
val set = ArraySet<MangaTag>(sparseArray.size())
for (i in 0 until sparseArray.size()) {
set.add(sparseArray.valueAt(i))
@@ -166,13 +165,12 @@ internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(c
tags.append(
jo.getInt("id"),
MangaTag(
title = jo.getString("name"),
title = jo.getString("name").toTitleCase(Locale.ENGLISH),
key = jo.getString("slug"),
source = source,
),
)
}
cachedTags = tags
return tags
}

View File

@@ -103,7 +103,7 @@ internal class ImHentai(context: MangaLoaderContext) :
val href = it.attr("href").substringAfterLast("tag/").substringBeforeLast('/')
MangaTag(
key = href,
title = it.selectFirstOrThrow("h3.list_tag").text(),
title = it.selectFirstOrThrow("h3.list_tag").text().toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -200,7 +200,9 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
.getJSONArray("data")
return tags.mapJSONToSet { jo ->
MangaTag(
title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue().toTitleCase(),
title = jo.getJSONObject("attributes").getJSONObject("name")
.firstStringValue()
.toTitleCase(Locale.ENGLISH),
key = jo.getString("id"),
source = source,
)

View File

@@ -132,7 +132,7 @@ internal class MangaPark(context: MangaLoaderContext) :
.select("div.flex-col:contains(Genres) div.whitespace-nowrap")
val tagMap = ArrayMap<String, MangaTag>(tagElements.size)
for (el in tagElements) {
val name = el.selectFirstOrThrow("span.whitespace-nowrap").text()
val name = el.selectFirstOrThrow("span.whitespace-nowrap").text().toTitleCase(sourceLocale)
if (name.isEmpty()) continue
tagMap[name] = MangaTag(
title = name,

View File

@@ -191,7 +191,7 @@ internal abstract class NineMangaParser(
val cateId = el.attr("cate_id")
val a = el.selectFirstOrThrow("a")
tagMap[el.text()] = MangaTag(
title = a.text().toTitleCase(),
title = a.text().toTitleCase(sourceLocale),
key = cateId,
source = source,
)

View File

@@ -99,7 +99,7 @@ internal abstract class AnimeBootstrapParser(
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
return doc.select("div.product__page__filter div:contains(Genre:) option ").mapNotNullToSet { option ->
val key = option.attr("value") ?: return@mapNotNullToSet null
val name = option.text()
val name = option.text().toTitleCase(sourceLocale)
MangaTag(
key = key,
title = name,

View File

@@ -114,7 +114,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
return doc.requireElementById("select_genre").select("option").mapNotNullToSet {
MangaTag(
key = it.attr("value"),
title = it.text(),
title = it.text().toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -15,7 +15,7 @@ internal class BeeToon(context: MangaLoaderContext) :
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val configKeyDomain = ConfigKey.Domain("ww7.beetoon.net")
override val configKeyDomain = ConfigKey.Domain("manhwafull.net")
override val isMultipleTagsSupported = false

View File

@@ -147,7 +147,7 @@ internal abstract class FmreaderParser(
val href = a.attr("href").substringAfter(tagPrefix).substringBeforeLast(".html")
MangaTag(
key = href,
title = a.text(),
title = a.text().toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -98,7 +98,7 @@ internal abstract class HeanCms(
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = urlManga,
url = urlManga.toRelativeUrl(domain),
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
@@ -182,7 +182,7 @@ internal abstract class HeanCms(
return tags.mapNotNullToSet {
MangaTag(
key = it.substringAfter("id\":").substringBefore(",\""),
title = it.substringAfter("name\":\"").substringBefore("\"}]"),
title = it.substringAfter("name\":\"").substringBefore("\"}]").toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.likemanga
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.json.JSONObject
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
@@ -189,7 +190,8 @@ internal abstract class LikeMangaParser(
MangaChapter(
id = generateUid(url),
name = name,
number = chapNum.toInt(),
number = chapNum.toFloatOrNull() ?: 0f,
volume = 0,
url = url,
scanlator = null,
uploadDate = parseChapterDate(
@@ -239,9 +241,12 @@ internal abstract class LikeMangaParser(
val jsonData = JSONObject(context.decodeBase64(jsonRaw).toString(Charsets.UTF_8))
val jsonImg = context.decodeBase64(jsonData.getString("data")).toString(Charsets.UTF_8)
val images = jsonImg.replace("\\", "").replace("[", "").replace("]", "").replace("\"", "").split(",")
val cdn = doc.selectFirstOrThrow(".reading-detail img").src()?.substringBefore("manga/")
val baseUrl = doc.selectFirstOrThrow(".reading-detail img").src()
val cdn = baseUrl?.substringBefore("manga/", "")?.ifEmpty {
baseUrl.toHttpUrl().resolve("/").toString()
}
return images.map { img ->
val url = cdn + img
val url = concatUrl(cdn.orEmpty(), img)
MangaPage(
id = generateUid(url),
url = url,

View File

@@ -374,9 +374,9 @@ internal abstract class MadaraParser(
}
MangaTag(
key = href,
title = a.ownText().trim().ifEmpty {
a.selectFirst(".menu-image-title")?.text()?.trim() ?: return@mapNotNullToSet null
}.toTitleCase(),
title = a.ownText().ifEmpty {
a.selectFirst(".menu-image-title")?.textOrNull()
}?.toTitleCase(sourceLocale) ?: return@mapNotNullToSet null,
source = source,
)
}

View File

@@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGALEK", "MangaLeku", "ar")
internal class MangaLek(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGALEK, "mangaleku.com", pageSize = 10)
MadaraParser(context, MangaSource.MANGALEK, "manga-leko.com", pageSize = 10)

View File

@@ -0,0 +1,10 @@
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("STELLARSABER", "StellarSaber", "ar")
internal class StellarSaber(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.STELLARSABER, "stellarsaber.com")

View File

@@ -8,6 +8,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("HENTAI3Z", "Hentai3z", "en", ContentType.HENTAI)
internal class Hentai3z(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.HENTAI3Z, "hentai3z.xyz", pageSize = 20) {
MadaraParser(context, MangaSource.HENTAI3Z, "manga18h.xyz", pageSize = 20) {
override val withoutAjax = true
}

View File

@@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGAWEEBS", "MangaWeebs", "en")
internal class MangaWeebs(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGAWEEBS, "mangaweebs.in", pageSize = 20) {
MadaraParser(context, MangaSource.MANGAWEEBS, "mangaweebs.org", pageSize = 20) {
override val datePattern = "dd MMMM HH:mm"
}

View File

@@ -1,12 +1,53 @@
package org.koitharu.kotatsu.parsers.site.madara.en
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
@MangaSourceParser("RESETSCANS", "ResetScans", "en")
internal class ResetScans(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.RESETSCANS, "reset-scans.us", 18) {
override val datePattern = "MMM dd"
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
val doc = if (postReq) {
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
val url = "https://$domain/wp-admin/admin-ajax.php"
val postData = "action=manga_get_chapters&manga=$mangaId"
webClient.httpPost(url, postData).parseHtml()
} else {
val url = mangaUrl.toAbsoluteUrl(domain).removeSuffix('/') + "/ajax/chapters/"
webClient.httpPost(url, emptyMap()).parseHtml()
}
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.getElementsByTag("a").findWithText()
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
val link = href + stylePage
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
val name = a.text()
MangaChapter(
id = generateUid(href),
url = link,
name = name,
number = 0f,
volume = 0,
branch = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
scanlator = null,
source = source,
)
}
}
private fun Elements.findWithText() = firstOrNull { it.hasText() } ?: first()
}

View File

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

View File

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

View File

@@ -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("MANHUAFENIX", "ManhuaFenix", "es")
internal class ManhuaFenix(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANHUAFENIX, "manhua-fenix.com") {
override val postReq = true
}

View File

@@ -1,12 +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("MANGASCANTRADUNOFFICIAL", "MangaScantrad.fr", "fr")
internal class MangaScantradUnofficial(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGASCANTRADUNOFFICIAL, "www.mangascantrad.fr", 10) {
override val datePattern = "dd/MM/yyyy"
}

View File

@@ -1,10 +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("SHADOWTRAD", "ShadowTrad", "fr")
internal class ShadowTrad(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.SHADOWTRAD, "shadowtrad.net", 10)

View File

@@ -4,11 +4,11 @@ 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
import java.util.*
@MangaSourceParser("POJOKMANGA", "PojokManga", "id")
internal class PojokManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.POJOKMANGA, "pojokmanga.net") {
MadaraParser(context, MangaSource.POJOKMANGA, "pojokmanga.id") {
override val tagPrefix = "komik-genre/"
override val listUrl = "komik/"
override val datePattern = "MMM d, yyyy"

View File

@@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("FAYSCANS", "FayScans", "pt")
internal class FayScans(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.FAYSCANS, "fayscans.com.br") {
MadaraParser(context, MangaSource.FAYSCANS, "fayscans.net") {
override val datePattern: String = "dd/MM/yyyy"
}

View File

@@ -0,0 +1,15 @@
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.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("LERYAOI", "LerYaoi", "pt", ContentType.HENTAI)
internal class LerYaoi(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.LERYAOI, "leryaoi.com", 10) {
override val datePattern = "dd/MM/yyyy"
override val listUrl = "bl/"
override val tagPrefix = "genero/"
}

View File

@@ -2,12 +2,12 @@ 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.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.model.ContentType
@MangaSourceParser("NOCSUMMER", "NocturneSummer", "pt", ContentType.HENTAI)
internal class Nocsummer(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.NOCSUMMER, "nocsummer.com.br", 18) {
MadaraParser(context, MangaSource.NOCSUMMER, "nocfsb.com", 18) {
override val datePattern = "dd 'de' MMMMM 'de' yyyy"
}

View File

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

View File

@@ -6,16 +6,11 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.parseFailed
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("TRUYENTRANHDAMMYY", "TruyenTranhDamMyy", "vi")
internal class TruyenTranhDamMyy(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.TRUYENTRANHDAMMYY, "truyentranhdammyy.com") {
MadaraParser(context, MangaSource.TRUYENTRANHDAMMYY, "truyentranhdammyy.net") {
override val postReq = true
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")

View File

@@ -9,6 +9,6 @@ import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
@MangaSourceParser("MANGANATO", "Manganato", "en")
internal class Manganato(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.MANGANATO) {
override val configKeyDomain = ConfigKey.Domain("chapmanganato.com", "manganato.com")
override val otherDomain = "chapmanganato.com"
override val configKeyDomain = ConfigKey.Domain("chapmanganato.to", "chapmanganato.com", "manganato.com")
override val otherDomain = configKeyDomain.defaultValue
}

View File

@@ -5,8 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("ELITON", "Eliton", "ar")
@MangaSourceParser("ELITON", "ThunderScans", "ar")
internal class Eliton(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.ELITON, "eliton.lol", pageSize = 20, searchPageSize = 10) {
MangaReaderParser(context, MangaSource.ELITON, "thunderscans.com", pageSize = 20, searchPageSize = 10) {
override val isTagsExclusionSupported = false
}

View File

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

View File

@@ -8,7 +8,7 @@ import java.util.*
@MangaSourceParser("AINZSCANS", "AinzScans", "id")
internal class AinzScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.AINZSCANS, "ainzscans.site", pageSize = 20, searchPageSize = 10) {
MangaReaderParser(context, MangaSource.AINZSCANS, "ainzscans.net", pageSize = 20, searchPageSize = 10) {
override val listUrl = "/series"
override val datePattern = "MMM d, yyyy"
override val sourceLocale: Locale = Locale.ENGLISH

View File

@@ -8,7 +8,7 @@ import java.util.*
@MangaSourceParser("KOMIKLOKAL", "KomikMirror", "id")
internal class KomikLokalParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.KOMIKLOKAL, "komikmirror.sbs", pageSize = 20, searchPageSize = 10) {
MangaReaderParser(context, MangaSource.KOMIKLOKAL, "komikmirror.lol", pageSize = 20, searchPageSize = 10) {
override val datePattern = "MMM d, yyyy"
override val sourceLocale: Locale = Locale.ENGLISH
}

View File

@@ -7,8 +7,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
import java.util.*
@MangaSourceParser("KOMIKLOKALCFD", "KomikLokal.sbs", "id", ContentType.HENTAI)
@MangaSourceParser("KOMIKLOKALCFD", "KomikLokal.mom", "id", ContentType.HENTAI)
internal class KomiklokalCfd(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.KOMIKLOKALCFD, "komiklokal.sbs", pageSize = 30, searchPageSize = 10) {
MangaReaderParser(context, MangaSource.KOMIKLOKALCFD, "komiklokal.mom", pageSize = 30, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH
}

View File

@@ -7,6 +7,6 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("SEKAIKOMIK", "SekaiKomik", "id")
internal class SekaikomikParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.SEKAIKOMIK, "sekaikomik.bio", pageSize = 20, searchPageSize = 100) {
MangaReaderParser(context, MangaSource.SEKAIKOMIK, "sekaikomik.guru", pageSize = 20, searchPageSize = 100) {
override val isTagsExclusionSupported = false
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers.site.mangareader.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.mangareader.MangaReaderParser
@MangaSourceParser("ASURATR", "ArmoniScans", "tr")
internal class AsuraTRParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.ASURATR, "armoniscans.com", pageSize = 30, searchPageSize = 10) {
override val isTagsExclusionSupported = false
}

View File

@@ -190,7 +190,7 @@ internal abstract class MmrcmsParser(
val doc = webClient.httpGet(fullUrl).parseHtml()
val body = doc.body().selectFirstOrThrow("dl.dl-horizontal")
val chaptersDeferred = async { getChapters(doc) }
val desc = doc.selectFirstOrThrow(selectDesc).text()
val desc = doc.selectFirst(selectDesc)?.text().orEmpty()
val stateDiv = body.selectFirst(selectState)?.nextElementSibling()
val state = stateDiv?.let {
when (it.text()) {

View File

@@ -8,7 +8,7 @@ import java.util.Locale
@MangaSourceParser("MANGA_SCAN", "MangaScan", "fr")
internal class MangaScan(context: MangaLoaderContext) :
MmrcmsParser(context, MangaSource.MANGA_SCAN, "mangascan.cc") {
MmrcmsParser(context, MangaSource.MANGA_SCAN, "mangascan-fr.com") {
override val imgUpdated = ".jpg"
override val sourceLocale: Locale = Locale.ENGLISH
}

View File

@@ -18,7 +18,6 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.net.URLDecoder
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@@ -252,7 +251,7 @@ internal class RemangaParser(
val userCookie = context.cookieJar.getCookies(domain).find {
it.name == "user"
} ?: return baseHeaders
val jo = JSONObject(URLDecoder.decode(userCookie.value, Charsets.UTF_8.name()))
val jo = JSONObject(userCookie.value.urlDecode())
val accessToken = jo.getStringOrNull("access_token") ?: return baseHeaders
return baseHeaders.newBuilder().add("authorization", "bearer $accessToken").build()
}

View File

@@ -57,7 +57,7 @@ internal abstract class ChanParser(
row.selectFirst("div.genre")?.select("a")?.mapToSet {
MangaTag(
title = it.text().toTagName(),
key = it.attr("href").substringAfterLast('/').urlEncoded(),
key = it.attr("href").substringAfterLast('/').urlDecode(),
source = source,
)
}
@@ -133,7 +133,7 @@ internal abstract class ChanParser(
val a = li.children().lastOrNull() ?: li.parseFailed("a is null")
MangaTag(
title = a.text().toTagName(),
key = a.attr("href").substringAfterLast('/'),
key = a.attr("href").substringAfterLast('/').urlDecode(),
source = source,
)
}

View File

@@ -39,7 +39,7 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context,
val a = it.children().last() ?: doc.parseFailed("Invalid tag")
MangaTag(
title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('/'),
key = a.attr("href").substringAfterLast('/').urlDecode(),
source = source,
)
} ?: manga.tags,
@@ -48,7 +48,8 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context,
id = generateUid(readLink),
url = readLink,
source = source,
number = 1,
number = 0f,
volume = 0,
uploadDate = 0L,
name = manga.title,
scanlator = null,

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.util.*
internal class YaoiChanParser(context: MangaLoaderContext) : ChanParser(context, MangaSource.YAOICHAN) {
override val configKeyDomain = ConfigKey.Domain(
"v3.yaoi-chan.me",
"v2.yaoi-chan.me",
"v1.yaoi-chan.me",
"yaoi-chan.me",

View File

@@ -143,7 +143,7 @@ class MangaAy(context: MangaLoaderContext) : PagedMangaParser(context, MangaSour
if (option.text().isEmpty()) continue
tagMap[option.text()] = MangaTag(
key = option.attr("value"),
title = option.text(),
title = option.text().toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -14,7 +14,7 @@ class YaoiFlix(context: MangaLoaderContext) : PagedMangaParser(context, MangaSou
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("www.yaoiflix.live")
override val configKeyDomain = ConfigKey.Domain("www.yaoiflix.co")
override val isMultipleTagsSupported = false

View File

@@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
@MangaSourceParser("NETTRUYENMAX", "NettruyenBing", "vi")
internal class Nettruyenmax(context: MangaLoaderContext) :
WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenbing.com", 36)
WpComicsParser(context, MangaSource.NETTRUYENMAX, "www.nettruyenclub.com", 36)

View File

@@ -163,7 +163,7 @@ internal abstract class ZeistMangaParser(
}
Manga(
id = generateUid(href),
url = href,
url = href.toRelativeUrl(domain),
publicUrl = href,
coverUrl = urlImg.orEmpty(),
title = name,
@@ -183,7 +183,7 @@ internal abstract class ZeistMangaParser(
return doc.selectFirstOrThrow("div.filter").select("ul li").mapNotNullToSet {
MangaTag(
key = it.selectFirstOrThrow("input").attr("value"),
title = it.selectFirstOrThrow("label").text(),
title = it.selectFirstOrThrow("label").text().toTitleCase(sourceLocale),
source = source,
)
}

View File

@@ -3,6 +3,7 @@
package org.koitharu.kotatsu.parsers.util
import androidx.annotation.FloatRange
import androidx.collection.MutableIntList
import androidx.collection.arraySetOf
import java.math.BigInteger
import java.net.URLDecoder
@@ -84,7 +85,7 @@ fun String.ellipsize(maxLength: Int) = if (this.length > maxLength) {
} else this
fun String.splitTwoParts(delimiter: Char): Pair<String, String>? {
val indices = ArrayList<Int>(4)
val indices = MutableIntList(4)
for ((i, c) in this.withIndex()) {
if (c == delimiter) {
indices += i

View File

@@ -11,10 +11,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.medianOrNull
import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.test_util.isDistinct
import org.koitharu.kotatsu.test_util.isDistinctBy
import org.koitharu.kotatsu.test_util.isUrlAbsolute
import org.koitharu.kotatsu.test_util.maxDuplicates
import org.koitharu.kotatsu.test_util.*
import kotlin.time.Duration.Companion.minutes
@@ -28,7 +25,7 @@ internal class MangaParserTest {
@MangaSources
fun list(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source)
val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null, tagsExclude = null)
val list = parser.getList(0, null)
checkMangaList(list, "list")
assert(list.all { it.source == source })
}
@@ -93,10 +90,19 @@ internal class MangaParserTest {
val titles = tags.map { it.title }
assert(titles.isDistinct())
assert("" !in titles)
assert(titles.all { it.isCapitalized() }) {
val badTags = titles.filterNot { it.isCapitalized() }.joinToString()
"Not all tags are capitalized: $badTags"
}
assert(tags.all { it.source == source })
val tag = tags.last()
val list = parser.getList(offset = 0, tags = setOf(tag), null, sortOrder = null)
val list = parser.getList(
offset = 0,
MangaListFilter.Advanced.Builder(parser.defaultSortOrder)
.tags(setOf(tag))
.build(),
)
checkMangaList(list, "${tag.title} (${tag.key})")
assert(list.all { it.source == source })
}
@@ -142,7 +148,7 @@ internal class MangaParserTest {
@MangaSources
fun details(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source)
val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null, tagsExclude = null)
val list = parser.getList(0, null)
val manga = list[3]
parser.getDetails(manga).apply {
assert(!chapters.isNullOrEmpty()) { "Chapters are null or empty" }
@@ -156,7 +162,7 @@ internal class MangaParserTest {
assert(c.isDistinctBy { it.id }) {
"Chapters are not distinct by id: ${c.maxDuplicates { it.id }} for $publicUrl"
}
assert(c.isDistinctBy { it.number to it.branch }) {
assert(c.isDistinctByNotNull { if (it.number > 0f) it.number to it.branch else null }) {
"Chapters are not distinct by number: ${c.maxDuplicates { it.number to it.branch }} for $publicUrl"
}
assert(c.all { it.source == source })
@@ -171,7 +177,7 @@ internal class MangaParserTest {
@MangaSources
fun pages(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source)
val list = parser.getList(0, sortOrder = SortOrder.UPDATED, tags = null, tagsExclude = null)
val list = parser.getList(0, null)
val manga = list.first()
val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}")
val pages = parser.getPages(chapter)
@@ -256,4 +262,8 @@ internal class MangaParserTest {
}
}
}
private fun String.isCapitalized(): Boolean {
return !first().isLowerCase()
}
}

View File

@@ -30,6 +30,16 @@ internal fun <T, K> Collection<T>.isDistinctBy(selector: (T) -> K): Boolean {
return set.size == size
}
internal fun <T, K> Collection<T>.isDistinctByNotNull(selector: (T) -> K?): Boolean {
val set = ArraySet<K>(size)
for (item in this) {
if (!set.add(selector(item) ?: continue)) {
return false
}
}
return set.size == size
}
internal fun String.isUrlRelative() = matches(PATTERN_URL_RELATIVE)
internal fun String.isUrlAbsolute() = matches(PATTERN_URL_ABSOLUTE)