Compare commits
11 Commits
a8f9423307
...
4a8c7fa36a
| Author | SHA1 | Date | |
|---|---|---|---|
|
4a8c7fa36a
|
|||
| 1251a6cd75 | |||
|
|
0ce8eae969 | ||
|
|
8b9c76fca5 | ||
|
52e4e6b802
|
|||
|
57c9d26916
|
|||
|
19d7003a9e
|
|||
|
b11e50ab22
|
|||
|
a03ab0e0c0
|
|||
|
05d4db00b3
|
|||
|
f4aaa14fc5
|
9
.github/workflows/test-parsers.yml
vendored
9
.github/workflows/test-parsers.yml
vendored
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user