Merge branch 'source/devi'

pull/267/head
Koitharu 3 years ago
commit 0004be15ba
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -17,7 +17,7 @@ import java.util.*
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
override val configKeyDomain = ConfigKey.Domain("teamxnovel.com")
override val configKeyDomain = ConfigKey.Domain("team1x12.com")
override suspend fun getListPage(
page: Int,
@ -61,7 +61,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
return doc.select("div.listupd .bs .bsx").ifEmpty {
doc.select("div.post-body .box")
}.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.select(".tt, h3").text(),

@ -23,17 +23,17 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
if (query != null || offset > 0) {
return emptyList()
}
val link = "https://${domain}/viewer_landing.php"
val link = "https://$domain/viewer_landing.php"
val doc = webClient.httpGet(link).parseHtml()
val mangas = doc.getElementsByClass("comicPreviewContainer")
return mangas.mapNotNull { item ->
val background = item.selectFirstOrThrow(".comicPreview").styleValueOrNull("background")
val href = item.selectFirst("a")?.attrAsAbsoluteUrl("href") ?: return@mapNotNull null
val href = item.selectFirst("a")?.attrAsRelativeUrl("href") ?: return@mapNotNull null
val cover = background?.substring(background.indexOf("site/themes"), background.indexOf(")"))
Manga(
id = generateUid(href),
title = item.selectFirst("h3")?.text() ?: return@mapNotNull null,
coverUrl = "https://${domain}/$cover",
coverUrl = "https://$domain/$cover",
altTitle = null,
author = "Dan Kim",
rating = RATING_UNKNOWN,

@ -59,7 +59,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.movie-list-index div.cartoon-box").map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h3").text(),
@ -81,7 +81,6 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/popular-comic").parseHtml()
return doc.select("li.tag-item a").mapNotNullToSet { a ->
@ -146,5 +145,4 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
)
}
}
}

@ -49,7 +49,6 @@ internal class Comicastle(context: MangaLoaderContext) : PagedMangaParser(contex
}
val postdata = "submit=Submit&search=" + query.urlEncoded()
webClient.httpPost(url, postdata).parseHtml()
} else if (!tags.isNullOrEmpty()) {
val url = buildString {
append("https://$domain/library/search/genre/")
@ -57,7 +56,6 @@ internal class Comicastle(context: MangaLoaderContext) : PagedMangaParser(contex
}
val postdata = "submit=Submit&search=" + tag?.key.orEmpty()
webClient.httpPost(url, postdata).parseHtml()
} else {
val url = buildString {
append("https://$domain")
@ -75,7 +73,7 @@ internal class Comicastle(context: MangaLoaderContext) : PagedMangaParser(contex
return doc.select("div.card-body div.match-height div.col-6")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("p").text(),
@ -93,7 +91,6 @@ internal class Comicastle(context: MangaLoaderContext) : PagedMangaParser(contex
}
}
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/library/").parseHtml()
return doc.requireElementById("sidebar").selectFirstOrThrow(".card-body").select("button")
@ -156,5 +153,4 @@ internal class Comicastle(context: MangaLoaderContext) : PagedMangaParser(contex
)
}
}
}

@ -56,7 +56,7 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
if (!query.isNullOrEmpty()) {
return doc.select("dl.chapter-list dd")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("a").text(),
@ -81,7 +81,7 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
} else {
return doc.select("li.span2")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("div.caption").text(),
@ -98,7 +98,6 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
)
}
}
}
override suspend fun getTags(): Set<MangaTag> = emptySet()
@ -169,4 +168,3 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
return pages
}
}

@ -0,0 +1,165 @@
package org.koitharu.kotatsu.parsers.site.en
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("KSKMOE", "Ksk Moe", "en", ContentType.HENTAI)
internal class KskMoe(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.KSKMOE, 35) {
override val sortOrders: Set<SortOrder> =
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("ksk.moe")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/tags/")
append(tag?.key.orEmpty())
} else {
append("/browse")
}
if (page > 1) {
append("/page/")
append(page)
}
when (sortOrder) {
SortOrder.POPULARITY -> append("?sort=32")
SortOrder.UPDATED -> append("")
SortOrder.NEWEST -> append("?sort=16")
SortOrder.ALPHABETICAL -> append("?sort=1")
else -> append("")
}
if (!query.isNullOrEmpty()) {
append("?s=")
append(query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
if (!doc.html().contains("pagination") && page > 1) {
return emptyList()
}
return doc.requireElementById("galleries").select("article").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectLastOrThrow("h3 span").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(),
tags = div.select("footer span").mapNotNullToSet { span ->
MangaTag(
key = span.text().urlEncoded(),
title = span.text(),
source = source,
)
},
state = null,
author = null,
source = source,
)
}
}
override suspend fun getTags(): Set<MangaTag> {
return coroutineScope {
(1..2).map { page ->
async { getTags(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = if (page == 1) {
"https://$domain/tags"
} else {
"https://$domain/tags/page/$page"
}
val root = webClient.httpGet(url).parseHtml().body().getElementById("tags")
return root?.parseTags().orEmpty()
}
private fun Element.parseTags() = select("section.tags div a").mapToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("/tags/"),
title = a.selectFirstOrThrow("span").text(),
source = source,
)
}
private val date = SimpleDateFormat("dd.MM.yyyy hh:mm 'UTC'", Locale.US)
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
return manga.copy(
tags = doc.requireElementById("metadata").select("main div:contains(Tag) a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").substringAfterLast("/tags/"),
title = a.selectFirstOrThrow("span").text(),
source = source,
)
},
author = doc.requireElementById("metadata").selectFirstOrThrow("main div:contains(Artist) a span").text(),
chapters =
if ((doc.html().contains("previews"))) {
listOf(
MangaChapter(
id = generateUid(manga.id),
name = manga.title,
number = 1,
url = manga.url,
scanlator = null,
uploadDate = date.tryParse(doc.selectFirstOrThrow("time.updated").text()),
branch = null,
source = source,
),
)
} else {
emptyList()
},
)
}
// For the moment the pages are in poor quality.
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.requireElementById("previews").select("main div img").map { img ->
val url = img.src() ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}

@ -0,0 +1,223 @@
package org.koitharu.kotatsu.parsers.site.en
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("LIKEMANGA", "LikeManga", "en")
internal class LikeManga(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.LIKEMANGA, 36) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST)
override val configKeyDomain = ConfigKey.Domain("likemanga.io")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
append("/?act=search&f")
append("[sortby]".urlEncoded())
append("=")
when (sortOrder) {
SortOrder.POPULARITY -> append("hot")
SortOrder.UPDATED -> append("lastest-chap")
SortOrder.NEWEST -> append("lastest-manga")
else -> append("lastest-chap")
}
if (page > 1) {
append("&pageNum=")
append(page)
}
if (!tags.isNullOrEmpty()) {
append("&f")
append("[genres]".urlEncoded())
append("=")
append(tag?.key.orEmpty())
}
if (!query.isNullOrEmpty()) {
append("&f")
append("[keyword]".urlEncoded())
append("=")
append(query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.card-body div.video").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("p.title-manga").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/genres/").parseHtml()
return doc.select("ul.nav-genres li:not(.text-center) a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
title = a.text(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val mangaId = manga.url.toAbsoluteUrl(domain).removeSuffix("/").substringAfterLast("-").toInt()
val maxPageChapterSelect = doc.getElementById("nav_list_chapter_id_detail")?.select("a:not(.next)")
var maxPageChapter = 1
if (!maxPageChapterSelect.isNullOrEmpty()) {
maxPageChapterSelect.map {
val i = it.text().toInt()
if (i > maxPageChapter) {
maxPageChapter = i
}
}
}
return manga.copy(
altTitle = doc.selectFirstOrThrow(".list-info li.othername h2").text(),
state = null,
tags = doc.select("li.kind a").mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
title = a.text().toTitleCase(),
source = source,
)
},
author = doc.select("li.author p").last()?.text(),
description = doc.requireElementById("summary_shortened").text(),
chapters = run {
if (maxPageChapter == 1) {
parseChapters(doc)
} else {
coroutineScope {
val result = ArrayList(parseChapters(doc))
result.ensureCapacity(result.size * maxPageChapter)
(2..maxPageChapter).map { i ->
async {
loadChapters(mangaId, i)
}
}.awaitAll()
.flattenTo(result)
result
}
}
}.reversed(),
)
}
private suspend fun loadChapters(mangaId: Int, page: Int): List<MangaChapter> {
val json =
webClient.httpGet(
"https://$domain/?act=ajax&code=load_list_chapter&manga_id=$mangaId&page_num=$page&chap_id=0&keyword=",
)
.parseJson().getString("list_chap")
val chapters = json.split("wp-manga-chapter").drop(1)
return chapters.map { chapter ->
val url = chapter.substringAfter("href=\"").substringBefore("\">")
val name = chapter.substringAfter("/\">").substringBefore("</a>")
val chapNum = url.substringAfter("chapter-").substringBefore("-")
val d = chapter.substringAfter("<i>").substringBefore("</i>")
val dateText = if (d.contains("New")) {
"today"
} else {
d
}
MangaChapter(
id = generateUid(url),
name = name,
number = chapNum.toInt(),
url = url,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
branch = null,
source = source,
)
}
}
private val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
private fun parseChapters(root: Element): List<MangaChapter> {
return root.select("li.wp-manga-chapter")
.map { li ->
val url = li.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val dateText = if (li.selectFirstOrThrow(".chapter-release-date").text() == "New") {
"today"
} else {
li.selectFirstOrThrow(".chapter-release-date").text()
}
val chapNum = url.substringAfter("chapter-").substringBefore("-")
MangaChapter(
id = generateUid(url),
name = li.selectFirstOrThrow("a").text(),
number = chapNum.toInt(),
url = url,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
branch = null,
source = source,
)
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select(".reading-detail img").map { img ->
val url = img.src() ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
else -> dateFormat.tryParse(date)
}
}
}

@ -59,7 +59,7 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
val doc = webClient.httpGet(url).parseHtml()
return doc.select("li.novel-item").map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h4").text(),
@ -77,7 +77,6 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
}
}
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/browse-comics/").parseHtml()
return doc.select("label.checkbox-inline").mapNotNullToSet { label ->
@ -143,5 +142,4 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
)
}
}
}

@ -23,7 +23,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
override suspend fun getFavicons(): Favicons {
return Favicons(
listOf(
Favicon("https://${domain}/uploads/logos/logo-mini.png", 92, null),
Favicon("https://$domain/uploads/logos/logo-mini.png", 92, null),
),
domain,
)
@ -32,19 +32,19 @@ class Manhwa18Parser(context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val cardInfoElement = docs.selectFirst("div.series-information")
val author = cardInfoElement?.selectFirst(".info-name:contains(Author(s))")?.parent()
val author = cardInfoElement?.selectFirst(".info-name:contains(Author)")?.parent()
?.select("a")
?.joinToString(", ") { it.text() }
val availableTags = tagsMap.get()
val tags = cardInfoElement?.selectFirst(".info-name:contains(Genre(s))")?.parent()
val tags = cardInfoElement?.selectFirst(".info-name:contains(Genre)")?.parent()
?.select("a")
?.mapNotNullToSet { availableTags[it.text().lowercase(Locale.ENGLISH)] }
val state = cardInfoElement?.selectFirst(".info-name:contains(Status)")?.parent()
?.selectFirst("a")
?.let {
when (it.text()) {
"On going" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
when (it.text().lowercase()) {
"on going" -> MangaState.ONGOING
"completed" -> MangaState.FINISHED
else -> null
}
}
@ -56,7 +56,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
tags = tags.orEmpty(),
state = state,
chapters = docs.select(".card-body > .list-chapters > a").mapChapters(reversed = true) { index, element ->
// attrAsRelativeUrl only return page url without the '/'
val chapterUrl = element.attrAsAbsoluteUrlOrNull("href")?.toRelativeUrl(domain)
?: return@mapChapters null
val uploadDate = parseUploadDate(element.selectFirst(".chapter-time")?.text())

@ -0,0 +1,96 @@
package org.koitharu.kotatsu.parsers.site.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("PO2SCANS", "Po2Scans", "en")
internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, MangaSource.PO2SCANS) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("po2scans.com")
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
if (offset > 0) {
return emptyList()
}
val url = buildString {
append("https://$domain/series")
if (!query.isNullOrEmpty()) {
append("?search=")
append(query.urlEncoded())
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".series-list").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h2").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("dd MMM, yy", Locale.ENGLISH)
return manga.copy(
altTitle = null,
state = when (doc.select(".status span").last()?.text()) {
"Ongoing" -> MangaState.ONGOING
"Done" -> MangaState.FINISHED
else -> null
},
tags = emptySet(),
author = doc.select(".author span").last()?.text(),
description = doc.selectFirstOrThrow(".summary").text(),
chapters = doc.select(".chap-section .chap")
.mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val url = "/" + a.attrAsRelativeUrl("href").toAbsoluteUrl(domain)
MangaChapter(
id = generateUid(url),
name = a.text(),
number = i + 1,
url = url,
scanlator = null,
uploadDate = dateFormat.tryParse(div.select(".detail span").last()?.text()),
branch = null,
source = source,
)
},
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select(".swiper-slide img").map { img ->
val url = img.src()?.replace("./assets", "/assets")?.toRelativeUrl(domain)
?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}

@ -0,0 +1,150 @@
package org.koitharu.kotatsu.parsers.site.fr
import kotlinx.coroutines.coroutineScope
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("FMTEAM", "FmTeam", "fr")
internal class FmTeam(context: MangaLoaderContext) :
PagedMangaParser(context, MangaSource.FMTEAM, 0) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("fmteam.fr")
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (page > 1) {
return emptyList()
}
val jsonManga = if (!query.isNullOrEmpty()) {
//3 letters minimum
webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson().getJSONArray("comics")
} else {
webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics")
}
val manga = ArrayList<Manga>(jsonManga.length())
for (i in 0 until jsonManga.length()) {
val j = jsonManga.getJSONObject(i)
val href = "/api" + j.getString("url")
when {
!tags.isNullOrEmpty() -> {
val a = j.getJSONArray("genres").toString()
var found = true
tags.forEach {
if (!a.contains(it.key, ignoreCase = true)) {
found = false
}
}
if (found) {
manga.add(
addManga(href, j),
)
}
}
else -> {
manga.add(
addManga(href, j),
)
}
}
}
return manga
}
private fun addManga(href: String, j: JSONObject): Manga {
return Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(domain),
coverUrl = j.getString("thumbnail"),
title = j.getString("title"),
description = j.getString("description"),
altTitle = j.getJSONArray("alt_titles").toString()
.replace("[\"", "")
.replace("\"]", "")
.replace("\",\"", " , "),
rating = j.getString("rating").toFloatOrNull()?.div(10f)
?: RATING_UNKNOWN,
tags = emptySet(),
author = j.getString("author"),
state = when (j.getString("status").lowercase()) {
"en cours" -> MangaState.ONGOING
"terminé" -> MangaState.FINISHED
else -> null
},
source = source,
isNsfw = when (j.getString("adult").toInt()) {
0 -> false
1 -> true
else -> true
},
)
}
override suspend fun getTags(): Set<MangaTag> = emptySet()
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val json = webClient.httpGet(fullUrl).parseJson().getJSONObject("comic")
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
val chapters = JSONArray(json.getJSONArray("chapters").toJSONList().reversed())
manga.copy(
tags = json.getJSONArray("genres").toJSONList().mapNotNullToSet {
MangaTag(
key = it.getString("slug"),
title = it.getString("name"),
source = source,
)
},
chapters = chapters.mapJSONIndexed { i, j ->
val url = "/api" + j.getString("url").toRelativeUrl(domain)
val name = j.getString("full_title")
val date = j.getStringOrNull("updated_at")
MangaChapter(
id = generateUid(url),
name = name,
number = i + 1,
url = url,
scanlator = null,
uploadDate = dateFormat.tryParse(date),
branch = null,
source = source,
)
},
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val jsonPages = webClient.httpGet(fullUrl).parseJson().getJSONObject("chapter").getJSONArray("pages").toString()
val pages = jsonPages.replace("[", "").replace("]", "")
.replace("\\", "").split("\",\"").drop(1)
return pages.map { url ->
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
}

@ -0,0 +1,144 @@
package org.koitharu.kotatsu.parsers.site.fr
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("LIRESCAN", "Lire Scan", "fr")
internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.LIRESCAN, 20) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("lire-scan.me")
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val doc =
if (!query.isNullOrEmpty()) { // search only works with 4 or more letters
if (page > 1) {
return emptyList()
}
val q = query.urlEncoded().replace("%20", "+")
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
} else {
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/manga/")
append(tag?.key.orEmpty())
}
if (page > 1) {
append("/page/")
append(page)
append('/')
}
}
webClient.httpGet(url).parseHtml()
}
return doc.select("div.sect__content.grid-items div.item-poster").map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.select(".item-poster__title").text(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = div.selectFirstOrThrow(".item__rating").ownText().toFloatOrNull()?.div(10f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.FRANCE)
return manga.copy(
altTitle = root.select("ul.pmovie__list li:contains(Nom Alternatif:)").text()
.replace("Nom Alternatif:", ""),
state = when (root.select("ul.pmovie__list li:contains(Status:)").text()) {
"Status: OnGoing", "Status: En cours" -> MangaState.ONGOING
"Status: Fini" -> MangaState.FINISHED
else -> null
},
tags = root.select("ul.pmovie__list li:contains(Genre:)").text()
.replace("Genre:", "").split(" / ").mapNotNullToSet { tag ->
MangaTag(
key = tag.lowercase(),
title = tag,
source = source,
)
},
author = root.select("ul.pmovie__list li:contains(Artist(s):)").text().replace("Artist(s):", ""),
description = root.selectFirst("div.pmovie__text")?.html(),
chapters = root.select("ul li div.chapter")
.mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
val name = a.text()
val dateText = div.select("p").last()?.text()
MangaChapter(
id = generateUid(href),
name = name,
number = i,
url = href,
scanlator = null,
uploadDate = dateFormat.tryParse(dateText),
branch = null,
source = source,
)
},
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val pages = doc.selectFirstOrThrow("script:containsData(const manga = )").data()
.substringAfter("chapter1: [\"").substringBefore("\"]")
.split("\",\"")
return pages.map { img ->
MangaPage(
id = generateUid(img),
url = img,
preview = null,
source = source,
)
}
}
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.select(".nav-menu li a").mapNotNullToSet { a ->
val key = a.attr("href").removeSuffix('/').substringAfterLast("manga/", "")
MangaTag(
key = key,
title = a.text(),
source = source,
)
}
}
}

@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.SimpleDateFormat
import java.util.*
@ -49,110 +49,106 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
sortOrder: SortOrder,
): List<Manga> {
if (sortOrder == SortOrder.ALPHABETICAL) {
if (page > 1) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
if (sortOrder == SortOrder.ALPHABETICAL) {
append("/mangas/")
// just to stop the search of the ALPHABETICAL page because it contains all the manga and has no page function ( to change if there is a better method to stop the search )
if (page == 2) {
append(page.toString()) // juste for break
}
append("/api/get/catalog?page=0&filter=all")
}
val json = webClient.httpGet(url).parseJsonArray()
if (sortOrder == SortOrder.UPDATED) {
append("/api/manga/home/getlast/")
append(page.toString())
}
}
val doc = webClient.httpGet(url).parseHtml()
if (sortOrder == SortOrder.UPDATED) {
return doc.select(".last_chapters-element")
.map { div ->
val a = div.selectFirstOrThrow("a.last_chapters-title")
val href = a.attrAsAbsoluteUrl("href")
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
Manga(
id = generateUid(href),
title = a.text(),
id = generateUid(urlManga),
title = j.getString("title"),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = div.selectFirstOrThrow(".last_chapters-rate").ownText().toFloatOrNull()?.div(5f)
?: -1f,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow(".last_chapters-image img").attrAsAbsoluteUrl("src"),
coverUrl = img,
tags = setOf(),
state = null,
state = when (j.getString("status")) {
"0" -> MangaState.ONGOING
"1" -> MangaState.FINISHED
"3" -> MangaState.ABANDONED
else -> null
},
author = null,
source = source,
)
}
} else {
val root = doc.selectFirstOrThrow(".catalog")
return root.select("div.element")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val url = buildString {
append("https://")
append(domain)
append("/api/get/homegrid/")
append(page)
}
val json = webClient.httpGet(url).parseJsonArray()
return json.mapJSON { j ->
val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}"
val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}"
Manga(
id = generateUid(href),
title = div.select("a.title").text(),
id = generateUid(urlManga),
title = j.getString("manga_title"),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = div.selectFirstOrThrow("div.stats").lastElementChild()?.ownText()?.toFloatOrNull()
?.div(5f) ?: -1f,
url = urlManga,
publicUrl = urlManga.toAbsoluteUrl(domain),
rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
isNsfw = false,
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
coverUrl = img,
tags = setOf(),
state = null,
author = null,
source = source,
)
}
}
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.FRANCE)
val json = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseJson()
val jsonManga = json.getJSONObject("manga")
val chapters = json.getJSONObject("chapters").toString().split("{\"id\":").drop(1) // Possible improvement here
val slug = manga.url.substringAfterLast("/")
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.FRANCE)
return manga.copy(
altTitle = null,
state = when (root.select("div.manga-tags")[3].select("a").text()) {
"En Cours" -> MangaState.ONGOING
"Fini", "Abandonné", "Licencier" -> MangaState.FINISHED
state = when (jsonManga.getString("status")) {
"0" -> MangaState.ONGOING
"1" -> MangaState.FINISHED
"3" -> MangaState.ABANDONED
else -> null
},
author = jsonManga.getString("author"),
description = jsonManga.getString("description"),
chapters = chapters.mapChapters(reversed = true) { i, it ->
val id = it.substringAfter("\"chapter\":").substringBefore(",")
val url = "https://$domain/api/get/chapter/$slug/$id"
val date = getDateString(
it.substringAfter("\"date\":\"").substringBefore("\",").toLong(),
) // Possible improvement here
// Lists the tags but there is no search on the site so it will just come back to the a-z or last list.
tags = root.select("div.manga-tags")[1].select("a").mapNotNullToSet { a ->
MangaTag(
key = a.text(),
title = a.text().toTitleCase(),
source = source,
)
},
author = root.select("div.manga-staff").text(),
description = root.selectFirst("div.manga-description div")?.text(),
chapters = root.select("div.manga-chapters_wrapper div.manga-chapter")
.mapChapters(reversed = true) { i, div ->
val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
val name = a.text()
val dateText = div.select("span").last()?.text()
MangaChapter(
id = generateUid(href),
name = name,
id = generateUid(url),
name = "Chapitre : $id",
number = i,
url = href,
url = url,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
uploadDate = dateFormat.tryParse(date),
branch = null,
source = source,
)
@ -160,13 +156,23 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
)
}
private val simpleDateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.FRANCE)
private fun getDateString(time: Long): String = simpleDateFormat.format(time * 1000L)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val root = doc.body().requireElementById("forgen_reader")
return root.select("img").map { img ->
val url = img.attrAsRelativeUrlOrNull("data-src") ?: img.attrAsRelativeUrlOrNull("src")
?: img.parseFailed("Image src not found")
val jsonPage = webClient.httpGet(fullUrl).parseJson()
val idManga = jsonPage.getJSONObject("manga").getString("id")
val slugChapter = chapter.url.substringAfterLast("/")
val pages = jsonPage.getJSONObject("chapter").getJSONArray("files").toString()
.replace("[", "").replace("]", "").replace("\"", "")
.split(",") // Possible improvement here
return pages.map { img ->
val url = "https://$domain/upload/chapitre/$idManga/$slugChapter/$img"
MangaPage(
id = generateUid(url),
url = url,
@ -178,34 +184,4 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
override suspend fun getTags(): Set<MangaTag> = emptySet()
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.startsWith("il y a") -> 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("jour", "jours").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("heure", "heures").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("seconde", "secondes").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("mois").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("année", "années").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet("semaine", "semaines").anyWordIn(date) -> cal.apply {
add(
Calendar.WEEK_OF_MONTH,
-number,
)
}.timeInMillis
else -> 0
}
}
}

@ -41,7 +41,6 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
append(page.toString())
append("/?s=")
append(query.urlEncoded())
}
!tags.isNullOrEmpty() -> {
@ -55,7 +54,6 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
}
else -> {
if (sortOrder == SortOrder.ALPHABETICAL) {
append("/manga/")
append("/page/")
@ -65,7 +63,6 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
if (sortOrder == SortOrder.UPDATED) {
append("")
}
}
}
}
@ -74,7 +71,7 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
val root = doc.requireElementById("dernierschapitres")
return root.select("div.colonne")
.map { article ->
val href = article.selectFirstOrThrow("a.index-top4-a").attrAsAbsoluteUrl("href")
val href = article.selectFirstOrThrow("a.index-top4-a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = article.select(".carteinfos a").text(),
@ -94,7 +91,7 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
val root = doc.requireElementById("main")
return root.select("article.post-outer")
.map { article ->
val href = article.selectFirstOrThrow("a.thumb-link").attrAsAbsoluteUrl("href")
val href = article.selectFirstOrThrow("a.thumb-link").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = article.select(".index-post-header a").text(),
@ -111,8 +108,6 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
)
}
}
}
override suspend fun getDetails(manga: Manga): Manga {

@ -1,20 +1,24 @@
package org.koitharu.kotatsu.parsers.site.fr
package org.koitharu.kotatsu.parsers.site.heancms
import okhttp3.Headers
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("PERF_SCAN", "Perf Scan", "fr")
internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.PERF_SCAN, 12) {
internal abstract class HeanCms(
context: MangaLoaderContext,
source: MangaSource,
domain: String,
pageSize: Int = 20,
) : PagedMangaParser(context, source, pageSize) {
override val configKeyDomain = ConfigKey.Domain(domain)
override val sortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.ALPHABETICAL,
@ -23,12 +27,11 @@ internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context,
SortOrder.POPULARITY,
)
override val configKeyDomain = ConfigKey.Domain("perf-scan.fr")
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.build()
//For some sources, you need to send a json. For the moment, this part only works in Get. ( ex source need json gloriousscan.com , omegascans.org )
override suspend fun getListPage(
page: Int,
query: String?,
@ -36,8 +39,11 @@ internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context,
sortOrder: SortOrder,
): List<Manga> {
var firstTag = false
val url = buildString {
append("https://api.$domain/query?query_string=")
append("https://api.")
append(domain)
append("/query?query_string=")
if (!query.isNullOrEmpty()) {
append(query.urlEncoded())
@ -55,12 +61,29 @@ internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context,
append("&series_type=Comic&page=")
append(page)
append("&perPage=12&tags_ids=")
append("[]".urlEncoded())
append("[".urlEncoded())
if (!tags.isNullOrEmpty()) {
for (tag in tags) {
// Just to make it fit [1,2,44] ect
if (!firstTag) {
firstTag = true
} else {
append(",")
}
append(tag.key)
}
}
append("]".urlEncoded())
}
val json = webClient.httpGet(url).parseJson()
return json.getJSONArray("data").mapJSON { j ->
val slug = j.getString("series_slug")
val urlManga = "https://$domain/series/$slug"
val cover = if (j.getString("thumbnail").contains('/')) {
j.getString("thumbnail")
} else {
"https://api.$domain/${j.getString("thumbnail")}"
}
Manga(
id = generateUid(urlManga),
title = j.getString("title"),
@ -69,11 +92,12 @@ internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context,
publicUrl = urlManga,
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = j.getString("thumbnail"),
coverUrl = cover,
tags = setOf(),
state = when (j.getString("status")) {
"Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED
"Dropped" -> MangaState.ABANDONED
else -> null
},
author = null,
@ -82,31 +106,36 @@ internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context,
}
}
protected open val datePattern = "yyyy-MM-dd"
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("MM/DD/yyyy", Locale.ENGLISH)
val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH)
val slug = manga.url.substringAfterLast('/')
val chapter = root.selectFirstOrThrow("script:containsData(chapter_slug)").data()
.replace("\\", "")
.substringAfter("\"seasons\":")
.substringBefore("}]}],\"children\"")
.split("chapter_name")
.drop(1)
return manga.copy(
altTitle = root.selectFirstOrThrow("p.text-center.text-gray-400").text(),
tags = emptySet(),
author = root.select("div.flex.flex-col.gap-y-2 p:contains(Autor:) strong").text(),
description = root.selectFirst(".datas_synopsis")?.html(),
chapters = root.select("ul.grid a")
.mapChapters(reversed = true) { i, a ->
val href = a.attrAsRelativeUrl("href")
val name = a.selectFirstOrThrow("span").text()
val dateText = a.selectLast("span")?.text() ?: "0"
description = root.selectFirst("h5:contains(Desc) + .bg-gray-800")?.html(),
chapters = chapter.mapChapters(reversed = true) { i, it ->
val slugChapter = it.substringAfter("chapter_slug\":\"").substringBefore("\",\"")
val url = "https://$domain/series/$slug/$slugChapter"
val date = it.substringAfter("created_at\":\"").substringBefore("\",\"").substringBefore("T")
val name = slugChapter.replace("-", " ")
MangaChapter(
id = generateUid(href),
id = generateUid(url),
name = name,
number = i + 1,
url = href,
url = url,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
uploadDate = dateFormat.tryParse(date),
branch = null,
source = source,
)
@ -128,36 +157,21 @@ internal class PerfScan(context: MangaLoaderContext) : PagedMangaParser(context,
}
}
override suspend fun getTags(): Set<MangaTag> = emptySet()
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/comics").parseHtml()
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.endsWith(" ago") -> parseRelativeDate(date)
val tags = doc.selectFirstOrThrow("script:containsData(Genres)").data()
.replace("\\", "")
.substringAfterLast("\"Genres\"")
.split("\",{\"")
.drop(1)
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("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet("week").anyWordIn(date) -> cal.apply {
add(
Calendar.WEEK_OF_MONTH,
-number,
return tags.mapNotNullToSet {
MangaTag(
key = it.substringAfter("id\":").substringBefore(",\""),
title = it.substringAfter("name\":\"").substringBefore("\"}]"),
source = source,
)
}.timeInMillis
else -> 0
}
}
}

@ -0,0 +1,11 @@
package org.koitharu.kotatsu.parsers.site.heancms.es
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.heancms.HeanCms
@MangaSourceParser("YUGEN_MANGAS_ES", "Yugen Mangas Es", "es", ContentType.HENTAI)
internal class YugenMangasEs(context: MangaLoaderContext) :
HeanCms(context, MangaSource.YUGEN_MANGAS_ES, "yugenmangas.net")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.heancms.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.heancms.HeanCms
@MangaSourceParser("PERF_SCAN", "Perf Scan", "fr")
internal class PerfScan(context: MangaLoaderContext) :
HeanCms(context, MangaSource.PERF_SCAN, "perf-scan.fr")

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.heancms.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.heancms.HeanCms
@MangaSourceParser("REAPERSCANSPT", "ReaperScans Pt", "pt")
internal class ReaperScansPt(context: MangaLoaderContext) :
HeanCms(context, MangaSource.REAPERSCANSPT, "reaperscans.net")

@ -0,0 +1,159 @@
package org.koitharu.kotatsu.parsers.site.heancmsalt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
// Template similar to Heancms but with a different way of working
internal abstract class HeanCmsAlt(
context: MangaLoaderContext,
source: MangaSource,
domain: String,
pageSize: Int = 18,
) : PagedMangaParser(context, source, pageSize) {
override val configKeyDomain = ConfigKey.Domain(domain)
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
protected open val listUrl = "/comics"
protected open val datePattern = "MMMM d, yyyy"
init {
paginator.firstPage = 1
searchPaginator.firstPage = 1
}
protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)"
protected open val selectMangaTitle = "h5"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
// No search or tag
if (!query.isNullOrEmpty()) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append(listUrl)
if (page > 1) {
append("?page=")
append(page)
}
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(selectManga).map { div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirstOrThrow("img").src().orEmpty(),
title = div.selectFirstOrThrow(selectMangaTitle).text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),
author = null,
state = null,
source = source,
isNsfw = isNsfwSource,
)
}
}
override suspend fun getTags(): Set<MangaTag> = emptySet()
protected open val selectDesc = "div.description-container"
protected open val selectAlt = "div.series-alternative-names"
protected open val selectChapter = "ul.MuiList-root a"
protected open val selectChapterTitle = "div.MuiListItemText-multiline span"
protected open val selectChapterDate = "div.MuiListItemText-multiline p"
override suspend fun getDetails(manga: Manga): Manga {
val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
return manga.copy(
altTitle = doc.selectFirst(selectAlt)?.text().orEmpty(),
description = doc.selectFirstOrThrow(selectDesc).html(),
chapters = doc.select(selectChapter)
.mapChapters(reversed = true) { i, a ->
val dateText = a.selectFirstOrThrow(selectChapterDate).text()
val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain)
MangaChapter(
id = generateUid(url),
name = a.selectFirstOrThrow(selectChapterTitle).text(),
number = i + 1,
url = url,
scanlator = null,
uploadDate = parseChapterDate(
dateFormat,
dateText,
),
branch = null,
source = source,
)
},
)
}
protected open val selectPage = "p.flex-col.items-center img"
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml()
return doc.select(selectPage).map { img ->
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
MangaPage(
id = generateUid(url),
url = url,
preview = null,
source = source,
)
}
}
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0
return when {
d.startsWith("hace ") -> parseRelativeDate(date)
else -> dateFormat.tryParse(date)
}
}
// Parses dates in this form:
// 21 hours ago
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
WordSet("segundo").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("minutos", "minuto").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hora", "horas").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("días", "día").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("semana", "semanas").anyWordIn(date) -> cal.apply {
add(
Calendar.WEEK_OF_YEAR,
-number,
)
}.timeInMillis
WordSet("mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
}

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.heancmsalt.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.heancmsalt.HeanCmsAlt
@MangaSourceParser("LEGIONSCANS", "CerberuSeries", "es")
internal class CerberuSeries(context: MangaLoaderContext) :
HeanCmsAlt(context, MangaSource.LEGIONSCANS, "cerberuseries.xyz")

@ -0,0 +1,24 @@
package org.koitharu.kotatsu.parsers.site.heancmsalt.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.heancmsalt.HeanCmsAlt
@MangaSourceParser("MANGAESP", "MangaEsp", "es")
internal class MangaEsp(context: MangaLoaderContext) :
HeanCmsAlt(context, MangaSource.MANGAESP, "mangaesp.co", 15) {
override val listUrl = "/comic"
override val selectManga = "div.contenedor div.grid-5 .p-relative:not(.portada-contenedor)"
override val selectMangaTitle = "div.titulo-contenedor"
override val selectDesc = "div.project-sinopsis-contenido"
override val selectAlt = "div.project-info-opcion:contains(Altenativo) div.project-info-contenido"
override val selectChapter = "div.grid-capitulos div a"
override val selectChapterTitle = ".capitulo-info-titulo"
override val selectChapterDate = ".capitulo-info-fecha"
override val selectPage = ".grid-center img"
}

@ -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("MANGALIKE_ORG", "MangaLike Org", "ar")
internal class MangaLikeOrg(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGALIKE_ORG, "mangalike.org", pageSize = 10)

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MANGASTARZ", "Manga Starz", "ar")
internal class MangaStarz(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGASTARZ, "mangalike.org", pageSize = 10) {
MadaraParser(context, MangaSource.MANGASTARZ, "mangastarz.com", pageSize = 10) {
override val datePattern = "d MMMM، yyyy"
}

@ -0,0 +1,13 @@
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("SHADOWXMANGA", "Shadow X Manga", "ar")
internal class ShadowxManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.SHADOWXMANGA, "shadowxmanga.com") {
override val datePattern = "yyyy/MM/dd"
}

@ -5,9 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("STKISSMANGA", "Stkissmanga", "en")
internal class Stkissmanga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.STKISSMANGA, "1stkissmanga.me") {
override val datePattern = "MMMM dd, yyyy"
@MangaSourceParser("CREEPYSCANS", "CreepyScans", "en")
internal class CreepyScans(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.CREEPYSCANS, "creepyscans.com") {
override val stylepage = ""
}

@ -8,4 +8,4 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("HIPERDEX", "HiperDex", "en", ContentType.HENTAI)
internal class HiperDex(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.HIPERDEX, "hiperdex.com", 36)
MadaraParser(context, MangaSource.HIPERDEX, "hiperdex.xyz", 36)

@ -0,0 +1,13 @@
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("MANGA1K", "Manga1k", "en", ContentType.HENTAI)
internal class Manga1k(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MANGA1K, "manga1k.com", 20) {
override val withoutAjax = true
}

@ -9,5 +9,6 @@ import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
internal class MurimScan(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MURIMSCAN, "murimscan.run", 100) {
override val withoutAjax = true
override val postreq = true
}

@ -0,0 +1,14 @@
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("NVMANGA", "NvManga", "en")
internal class NvManga(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.NVMANGA, "nvmanga.com") {
override val datePattern = "dd/MM/yyyy"
override val tagPrefix = "genre/"
override val listUrl = "webtoon/"
}

@ -0,0 +1,12 @@
package org.koitharu.kotatsu.parsers.site.madara.es
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("MHSCANS", "MhScans", "es")
internal class MhScans(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.MHSCANS, "mhscans.com") {
override val datePattern = "d 'de' MMMMM 'de' yyyy"
}

@ -0,0 +1,13 @@
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("DIANXIATRADS", "Dianxia Trads", "pt")
internal class DianxiaTrads(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.DIANXIATRADS, "dianxiatrads.com", 10) {
override val datePattern: String = "dd/MM/yyyy"
}

@ -0,0 +1,12 @@
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)

@ -0,0 +1,13 @@
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("YAOITR", "Yaoi Tr", "tr")
internal class YaoiTr(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.YAOITR, "yaoitr.com", 16) {
override val datePattern = "d MMMM yyyy"
}

@ -0,0 +1,32 @@
package org.koitharu.kotatsu.parsers.site.manga18.zh
import org.jsoup.nodes.Document
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("HANMAN18", "Hanman18", "zh", ContentType.HENTAI)
internal class Hanman18(context: MangaLoaderContext) :
Manga18Parser(context, MangaSource.HANMAN18, "hanman18.com") {
override suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href")
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1,
url = href,
uploadDate = 0,
source = source,
scanlator = null,
branch = null,
)
}
}
override suspend fun getTags(): Set<MangaTag> = emptySet() // search by tag does not work
}

@ -0,0 +1,14 @@
package org.koitharu.kotatsu.parsers.site.mangabox.en
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.mangabox.MangaboxParser
@MangaSourceParser("MANGANELO_COM", "MangaNelo Com", "en")
internal class MangaNeloCom(context: MangaLoaderContext) :
MangaboxParser(context, MangaSource.MANGANELO_COM) {
override val configKeyDomain = ConfigKey.Domain("m.manganelo.com", "chapmanganelo.com")
override val otherDomain = "chapmanganelo.com"
}

@ -13,7 +13,6 @@ import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
internal abstract class MangaReaderParser(
context: MangaLoaderContext,
source: MangaSource,
@ -34,6 +33,84 @@ internal abstract class MangaReaderParser(
private val mutex = Mutex()
protected open var lastSearchPage = 1
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/page/")
append(page)
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""
}
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?order=")
append(sortQuery)
append(tagQuery)
append("&page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open val selectMangalist = ".postbody .listupd .bs .bsx"
protected open val selectMangaListImg = "img.ts-post-image"
protected open val selectMangaListTitle = "div.tt"
protected open fun parseMangaList(docs: Document): List<Manga> {
return docs.select(selectMangalist).mapNotNull {
val a = it.selectFirst("a") ?: return@mapNotNull null
val relativeUrl = a.attrAsRelativeUrl("href")
val rating = it.selectFirst(".numscore")?.text()
?.toFloatOrNull()?.div(10) ?: RATING_UNKNOWN
Manga(
id = generateUid(relativeUrl),
url = relativeUrl,
title = it.selectFirst(selectMangaListTitle)?.text() ?: a.attr("title"),
altTitle = null,
publicUrl = a.attrAsAbsoluteUrl("href"),
rating = rating,
isNsfw = isNsfwSource,
coverUrl = it.selectFirst(selectMangaListImg)?.src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
protected open val selectChapter = "#chapterlist > ul > li"
override suspend fun getDetails(manga: Manga): Manga {
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
@ -61,7 +138,6 @@ internal abstract class MangaReaderParser(
docs.selectFirst("div.seriestucontent > div.seriestucontentr") ?: docs.selectFirst("div.seriestucontentr")
?: docs.selectFirst("div.seriestucon")
val tagMap = getOrCreateTagMap()
val selectTag = if (tablemode != null) {
@ -72,7 +148,6 @@ internal abstract class MangaReaderParser(
val tags = selectTag.mapNotNullToSet { tagMap[it.text()] }
val stateSelect = if (tablemode != null) {
tablemode.selectFirst(".infotable td:contains(Status)")
?: tablemode.selectFirst(".infotable td:contains(Statut)")
@ -141,86 +216,6 @@ internal abstract class MangaReaderParser(
)
}
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {
if (page > lastSearchPage) {
return emptyList()
}
val url = buildString {
append("https://")
append(domain)
append("/page/")
append(page)
append("/?s=")
append(query.urlEncoded())
}
val docs = webClient.httpGet(url).parseHtml()
lastSearchPage = docs.selectFirst(".pagination .next")
?.previousElementSibling()
?.text()?.toIntOrNull() ?: 1
return parseMangaList(docs)
}
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""
}
val tagKey = "genre[]".urlEncoded()
val tagQuery =
if (tags.isNullOrEmpty()) "" else tags.joinToString(separator = "&", prefix = "&") { "$tagKey=${it.key}" }
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append("/?order=")
append(sortQuery)
append(tagQuery)
append("&page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open val selectMangalist = ".postbody .listupd .bs .bsx"
protected open val selectMangaListImg = "img.ts-post-image"
protected open val selectMangaListTitle = "div.tt"
protected open fun parseMangaList(docs: Document): List<Manga> {
return docs.select(selectMangalist).mapNotNull {
val a = it.selectFirst("a") ?: return@mapNotNull null
val relativeUrl = a.attrAsRelativeUrl("href")
val rating = it.selectFirst(".numscore")?.text()
?.toFloatOrNull()?.div(10) ?: RATING_UNKNOWN
Manga(
id = generateUid(relativeUrl),
url = relativeUrl,
title = it.selectFirst(selectMangaListTitle)?.text() ?: a.attr("title"),
altTitle = null,
publicUrl = a.attrAsAbsoluteUrl("href"),
rating = rating,
isNsfw = isNsfwSource,
coverUrl = it.selectFirst(selectMangaListImg)?.src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
source = source,
)
}
}
protected open val encodedSrc = false
protected open val selectScript = "div.wrapper script"
protected open val selectPage = "div#readerarea img"
@ -280,10 +275,7 @@ internal abstract class MangaReaderParser(
}
return pages
}
}
override suspend fun getTags(): Set<MangaTag> {
@ -293,22 +285,17 @@ internal abstract class MangaReaderParser(
protected suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val tagMap = ArrayMap<String, MangaTag>()
val url = listUrl.toAbsoluteUrl(domain)
val tagElements = webClient.httpGet(url).parseHtml().select("ul.genrez > li")
for (el in tagElements) {
if (el.text().isEmpty()) continue
tagMap[el.text()] = MangaTag(
title = el.text(),
key = el.selectFirst("input")?.attr("value") ?: continue,
source = source,
)
}
tagCache = tagMap
return@withLock tagMap
}
}

@ -9,4 +9,5 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
internal class EnAresManga(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.ENARESMANGA, "en-aresmanga.com", pageSize = 20, searchPageSize = 10) {
override val listUrl = "/series"
override val encodedSrc = true
}

@ -0,0 +1,10 @@
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)

@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
@MangaSourceParser("SWATEAM", "Swa Team", "ar")
internal class SwaTeam(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.SWATEAM, "swatmanga.co", pageSize = 42, searchPageSize = 39) {
MangaReaderParser(context, MangaSource.SWATEAM, "stmanga.me", pageSize = 42, searchPageSize = 39) {
override val datePattern = "MMMM dd, yyyy"
override val selectMangalist = ".listupd .bs .bsx"
@ -44,8 +44,8 @@ internal class SwaTeam(context: MangaLoaderContext) :
}
val sortQuery = when (sortOrder) {
SortOrder.ALPHABETICAL -> "title"
SortOrder.NEWEST -> "latest"
SortOrder.ALPHABETICAL -> "a-z"
SortOrder.NEWEST -> "added"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "update"
else -> ""

@ -7,8 +7,6 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("ANIGLISCANS", "Anigli Scans", "en")
internal class AnigliScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.ANIGLISCANS, "anigliscans.com", pageSize = 47, searchPageSize = 47) {
MangaReaderParser(context, MangaSource.ANIGLISCANS, "anigliscans.xyz", pageSize = 47, searchPageSize = 47) {
override val listUrl = "/series"
override val datePattern = "MMM d, yyyy"
}

@ -1,16 +1,14 @@
package org.koitharu.kotatsu.parsers.site.mangareader.id
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
import java.util.*
@MangaSourceParser("KOMIKLAB", "KomikLab", "id")
@MangaSourceParser("KOMIKLAB", "KomikLab", "en")
internal class KomikLabParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.KOMIKLAB, "komiklab.com", pageSize = 20, searchPageSize = 10) {
override val datePattern = "MMM d, yyyy"
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("NIGHTSCANS", "Night scans", "en")
internal class Nightscans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.NIGHTSCANS, "nightscans.org", pageSize = 20, searchPageSize = 20) {
override val datePattern = "MMM d, yyyy"
MangaReaderParser(context, MangaSource.NIGHTSCANS, "nightscans.net", pageSize = 20, searchPageSize = 10) {
override val listUrl = "/series"
override val selectMangaListImg = "img.ts-post-image, picture img"
}

@ -0,0 +1,12 @@
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("OZULSCANSEN", "OzulScans En", "en")
internal class OzulScansEn(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.OZULSCANSEN, "ozulscansen.com", pageSize = 30, searchPageSize = 10) {
override val listUrl = "/comics"
}

@ -4,14 +4,10 @@ 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
import java.util.*
import java.util.Locale
@MangaSourceParser("LEGIONSCANS", "Legion Scans", "es")
internal class LegionScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.LEGIONSCANS, "legionscans.com", pageSize = 20, searchPageSize = 20) {
override val datePattern = "MMM d, yyyy"
@MangaSourceParser("INARIMANGA", "Inari Manga", "es")
internal class InariManga(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.INARIMANGA, "inarimanga.com", pageSize = 20, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -8,7 +8,4 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("DOUJINDESURIP", "Doujin Desu Rip", "id", ContentType.HENTAI)
internal class DoujinDesuRip(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.DOUJINDESURIP, "doujindesu.rip", pageSize = 10, searchPageSize = 10) {
override val datePattern = "MMM d, yyyy"
}
MangaReaderParser(context, MangaSource.DOUJINDESURIP, "doujindesu.cfd", pageSize = 20, searchPageSize = 10)

@ -0,0 +1,13 @@
package org.koitharu.kotatsu.parsers.site.mangareader.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.mangareader.MangaReaderParser
import java.util.Locale
@MangaSourceParser("MANGASHIRO", "Manga Shiro", "id")
internal class MangaShiro(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANGASHIRO, "mangashiro.me", pageSize = 20, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -5,12 +5,10 @@ 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.mangareader.MangaReaderParser
import java.util.*
@MangaSourceParser("MIRRORDESU", "Mirrordesu", "id", ContentType.HENTAI)
internal class Mirrordesu(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MIRRORDESU, "mirrordesu.ink", pageSize = 20, searchPageSize = 20) {
override val listUrl = "/komik"
override val datePattern = "MMM d, yyyy"
@MangaSourceParser("MANHWALAND", "Manhwa Land", "id", ContentType.HENTAI)
internal class ManhwaLand(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANHWALAND, "manhwaland.lat", pageSize = 20, searchPageSize = 10) {
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -2,13 +2,12 @@ package org.koitharu.kotatsu.parsers.site.mangareader.id
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.mangareader.MangaReaderParser
@MangaSourceParser("MANHWADESU", "ManhwaDesu", "id")
@MangaSourceParser("MANHWADESU", "ManhwaDesu", "id", ContentType.HENTAI)
internal class ManhwadesuParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANHWADESU, "manhwadesu.top", pageSize = 20, searchPageSize = 10) {
MangaReaderParser(context, MangaSource.MANHWADESU, "manhwadesu.one", pageSize = 20, searchPageSize = 10) {
override val listUrl = "/komik"
override val datePattern = "MMM d, yyyy"
}

@ -0,0 +1,13 @@
package org.koitharu.kotatsu.parsers.site.mangareader.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.mangareader.MangaReaderParser
@MangaSourceParser("HENTAISSSSSCANLATOR", "Sssscanlator Hentai", "pt", type = ContentType.HENTAI)
internal class HentaiSsssscanlator(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.HENTAISSSSSCANLATOR, "hentais.sssscanlator.com", pageSize = 20, searchPageSize = 10,) {
override val datePattern = "MMM d, yyyy"
}

@ -0,0 +1,11 @@
package org.koitharu.kotatsu.parsers.site.mangareader.th
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.mangareader.MangaReaderParser
@MangaSourceParser("MANGA168", "Manga 168", "th", ContentType.HENTAI)
internal class Manga168(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANGA168, "manga168.com", pageSize = 40, searchPageSize = 30)

@ -5,9 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("ASURATR", "Asura Scans (tr)", "tr")
@MangaSourceParser("ASURATR", "Armoni Scans", "tr")
internal class AsuraTRParser(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.ASURATR, "asurascanstr.com", pageSize = 30, searchPageSize = 10) {
override val datePattern = "MMM d, yyyy"
}
MangaReaderParser(context, MangaSource.ASURATR, "armoniscans.com", pageSize = 30, searchPageSize = 10)

@ -0,0 +1,10 @@
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("MANGAGEZGINI", "Manga Gezgini", "tr")
internal class MangaGezgini(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANGAGEZGINI, "mangagezgini.com", pageSize = 20, searchPageSize = 10)

@ -0,0 +1,10 @@
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("MANGAEFENDISI", "Mangaefendisi", "tr")
internal class Mangaefendisi(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MANGAEFENDISI, "mangaefendisi.net", pageSize = 30, searchPageSize = 20)

@ -8,4 +8,4 @@ import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("MOONDAISY_SCANS", "MoonDaisyScans", "tr", ContentType.HENTAI)
internal class MoonDaisyScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.MOONDAISY_SCANS, "moondaisyscans.com", pageSize = 20, searchPageSize = 10)
MangaReaderParser(context, MangaSource.MOONDAISY_SCANS, "moondaisyscans.biz", pageSize = 20, searchPageSize = 10)

@ -0,0 +1,11 @@
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("TAROTSCANS", "Tarot Scans", "tr")
internal class TarotScans(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.TAROTSCANS, "www.tarotscans.com", pageSize = 20, searchPageSize = 10)

@ -114,7 +114,7 @@ internal abstract class MmrcmsParser(
url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = "https://$domain/uploads/manga/$deeplink$imgUpdated",
title = div.selectFirstOrThrow("a").text().orEmpty(),
title = div.selectFirstOrThrow("h3 a").text().orEmpty(),
altTitle = null,
rating = RATING_UNKNOWN,
tags = emptySet(),

@ -9,6 +9,5 @@ import java.util.Locale
@MangaSourceParser("FRSCANSCOM", "Fr Scans Com", "fr")
internal class FrScansCom(context: MangaLoaderContext) :
MmrcmsParser(context, MangaSource.FRSCANSCOM, "frscans.com") {
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -0,0 +1,16 @@
package org.koitharu.kotatsu.parsers.site.mmrcms.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.mmrcms.MmrcmsParser
import java.util.*
@MangaSourceParser("JPSCANVF", "JpScanVf", "fr")
internal class JpScanVf(context: MangaLoaderContext) :
MmrcmsParser(context, MangaSource.JPSCANVF, "jpscan-vf.net") {
//the search doesn't work on the source.
override val sourceLocale: Locale = Locale.ENGLISH
}

@ -53,23 +53,9 @@ internal abstract class NepnepParser(
!query.isNullOrEmpty() -> {
if (m.getString("i").contains(query.urlEncoded(), ignoreCase = true)) {
manga.add(
Manga(
id = generateUid(href),
title = m.getString("i").replace('-', ' '),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = imgUrl,
tags = emptySet(),
state = null,
author = null,
source = source,
),
addManga(href, imgUrl, m),
)
}
}
!tags.isNullOrEmpty() -> {
@ -82,27 +68,25 @@ internal abstract class NepnepParser(
}
if (found) {
manga.add(
Manga(
id = generateUid(href),
title = m.getString("i").replace('-', ' '),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = false,
coverUrl = imgUrl,
tags = emptySet(),
state = null,
author = null,
source = source,
),
addManga(href, imgUrl, m),
)
}
}
else -> {
manga.add(
Manga(
addManga(href, imgUrl, m),
)
}
}
}
return manga
}
private fun addManga(href: String, imgUrl: String, m: JSONObject): Manga {
return Manga(
id = generateUid(href),
title = m.getString("i").replace('-', ' '),
altTitle = null,
@ -115,16 +99,8 @@ internal abstract class NepnepParser(
state = null,
author = null,
source = source,
),
)
}
}
}
return manga
}
override suspend fun getTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/search/").parseHtml()

@ -27,7 +27,6 @@ internal class Bakai(context: MangaLoaderContext) : PagedMangaParser(context, Ma
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
@ -45,7 +44,6 @@ internal class Bakai(context: MangaLoaderContext) : PagedMangaParser(context, Ma
}
append("&quick=1&type=cms_records1&page=")
append(page.toString())
} else {
append("/hentai/")
append("page/")
@ -54,10 +52,9 @@ internal class Bakai(context: MangaLoaderContext) : PagedMangaParser(context, Ma
}
val doc = webClient.httpGet(url).parseHtml()
if (!tags.isNullOrEmpty() or !query.isNullOrEmpty()) {
return doc.select("ol.ipsStream li.ipsStreamItem")
.map { div ->
val href = div.selectFirstOrThrow("div.ipsStreamItem_snippet a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("div.ipsStreamItem_snippet a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h2.ipsStreamItem_title").text(),

@ -42,7 +42,6 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context,
}
append("/?s=")
append(query.urlEncoded())
} else {
when (sortOrder) {
SortOrder.POPULARITY -> append("/")
@ -64,7 +63,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context,
}
return item.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h2").text(),

@ -15,33 +15,27 @@ import java.util.*
internal class GoldenManga(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.GOLDENMANGA, 36) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
override val configKeyDomain = ConfigKey.Domain("goldenmanga.top")
override val headers: Headers = Headers.Builder()
.add("User-Agent", UserAgents.CHROME_DESKTOP)
.add("User-Agent", UserAgents.CHROME_MOBILE)
.build()
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/mangas")
append("?pagina=")
append(page.toString())
if (!query.isNullOrEmpty()) {
append("&search=")
append(query.urlEncoded())
}
if (!tags.isNullOrEmpty()) {
append("&genero=")
for (tag in tags) {
@ -53,7 +47,7 @@ internal class GoldenManga(context: MangaLoaderContext) : PagedMangaParser(conte
val doc = webClient.httpGet(url).parseHtml()
return doc.select("section.row div.mangas")
.map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("a h3").text(),
@ -107,7 +101,6 @@ internal class GoldenManga(context: MangaLoaderContext) : PagedMangaParser(conte
author = root.select("h5.cg_color a")[1].text(),
description = root.getElementById("manga_capitulo_descricao")?.html(),
chapters = root.requireElementById("capitulos").select("li")
.mapChapters(reversed = true) { i, div ->
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
val dateText = div.selectFirstOrThrow("div.col-sm-5 span").text()

@ -13,6 +13,7 @@ internal class MintMangaParser(
override val configKeyDomain = ConfigKey.Domain(
"mintmanga.live",
"mintmanga.com",
"m.mintmanga.live",
)
}

@ -28,7 +28,7 @@ internal class SadScans(context: MangaLoaderContext) : MangaParser(context, Mang
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".series-list").map { div ->
val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href")
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.selectFirstOrThrow("h2").text(),

Loading…
Cancel
Save