Add AComics, ReaperComics ( en )
This commit is contained in:
@@ -36,7 +36,7 @@ import java.util.UUID
|
||||
internal abstract class MangaPlusParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaSource,
|
||||
private val sourceLang: String
|
||||
private val sourceLang: String,
|
||||
) : MangaParser(context, source), Interceptor {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
|
||||
@@ -44,7 +44,7 @@ internal abstract class MangaPlusParser(
|
||||
override val availableSortOrders = setOf(
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.ALPHABETICAL
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
|
||||
@@ -65,6 +65,7 @@ internal abstract class MangaPlusParser(
|
||||
else -> getAllTitleList()
|
||||
}
|
||||
}
|
||||
|
||||
is MangaListFilter.Search -> getAllTitleList(filter.query)
|
||||
else -> getAllTitleList()
|
||||
}
|
||||
@@ -130,7 +131,7 @@ internal abstract class MangaPlusParser(
|
||||
rating = RATING_UNKNOWN,
|
||||
state = null,
|
||||
source = source,
|
||||
tags = emptySet()
|
||||
tags = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -161,7 +162,7 @@ internal abstract class MangaPlusParser(
|
||||
},
|
||||
chapters = parseChapters(
|
||||
json.getJSONArray("chapterListGroup"),
|
||||
title.getStringOrNull("language") ?: "ENGLISH"
|
||||
title.getStringOrNull("language") ?: "ENGLISH",
|
||||
),
|
||||
state = if (completed) {
|
||||
MangaState.FINISHED
|
||||
@@ -169,7 +170,7 @@ internal abstract class MangaPlusParser(
|
||||
MangaState.PAUSED
|
||||
} else {
|
||||
MangaState.ONGOING
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ internal abstract class MangaPlusParser(
|
||||
.toJSONList()
|
||||
.flatMap {
|
||||
it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() +
|
||||
it.optJSONArray("lastChapterList")?.toJSONList().orEmpty()
|
||||
it.optJSONArray("lastChapterList")?.toJSONList().orEmpty()
|
||||
}
|
||||
|
||||
return chapterList.mapNotNull { chapter ->
|
||||
@@ -199,7 +200,7 @@ internal abstract class MangaPlusParser(
|
||||
else -> language.lowercase().toTitleCase()
|
||||
},
|
||||
scanlator = null,
|
||||
source = source
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -216,9 +217,9 @@ internal abstract class MangaPlusParser(
|
||||
val encryptionKey = mangaPage.getStringOrNull("encryptionKey")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url + if (encryptionKey == null ) "" else "#$encryptionKey",
|
||||
url = url + if (encryptionKey == null) "" else "#$encryptionKey",
|
||||
preview = null,
|
||||
source = source
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -282,55 +283,55 @@ internal abstract class MangaPlusParser(
|
||||
class English(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_EN,
|
||||
"ENGLISH"
|
||||
"ENGLISH",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus Spanish", "es")
|
||||
class Spanish(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_ES,
|
||||
"SPANISH"
|
||||
"SPANISH",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus French", "fr")
|
||||
class French(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_FR,
|
||||
"FRENCH"
|
||||
"FRENCH",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus Indonesian", "id")
|
||||
class Indonesian(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_ID,
|
||||
"INDONESIAN"
|
||||
"INDONESIAN",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt")
|
||||
class Portuguese(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_PTBR,
|
||||
"PORTUGUESE_BR"
|
||||
"PORTUGUESE_BR",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus Russian", "ru")
|
||||
class Russian(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_RU,
|
||||
"RUSSIAN"
|
||||
"RUSSIAN",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus Thai", "th")
|
||||
class Thai(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_TH,
|
||||
"THAI"
|
||||
"THAI",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus Vietnamese", "vi")
|
||||
class Vietnamese(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_VI,
|
||||
"VIETNAMESE"
|
||||
"VIETNAMESE",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ class ManhwasMen(context: MangaLoaderContext) :
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
|
||||
return manga.copy(
|
||||
tags = doc.body().select(".genres a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
@@ -116,7 +117,7 @@ class ManhwasMen(context: MangaLoaderContext) :
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterDate(
|
||||
SimpleDateFormat("dd/MM/yyyy", sourceLocale),
|
||||
simpleDateFormat,
|
||||
li.selectLastOrThrow(".flex-grow-1 span").text(),
|
||||
),
|
||||
branch = null,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
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.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("REAPERCOMICS", "ReaperComics", "en")
|
||||
internal class ReaperComics(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.REAPERCOMICS, pageSize = 30) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("reapercomics.com")
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) // TODO
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("/")
|
||||
if (filter.sortOrder == SortOrder.UPDATED) {
|
||||
append("latest/")
|
||||
}
|
||||
append("comics?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/latest/comics?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
private fun parseMangaList(docs: Document): List<Manga> {
|
||||
|
||||
|
||||
return docs.select("main div.relative, main li.col-span-1").map {
|
||||
val a = it.selectFirstOrThrow("a")
|
||||
val url = a.attrAsAbsoluteUrl("href")
|
||||
Manga(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
title = (it.selectFirst("p a") ?: it.selectLast("a"))?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
publicUrl = url,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = it.selectFirstOrThrow("img").src().orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
|
||||
return manga.copy(
|
||||
description = doc.selectFirst("div.p-4 p.prose")?.html(),
|
||||
state = when (doc.selectFirst("dl.mt-2 div:contains(Status) dd")?.text()?.lowercase()) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"complete" -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
chapters = doc.select("div.p-2 div.pb-4 ul li").mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val chapterUrl = a.attrAsAbsoluteUrl("href").toRelativeUrl(domain)
|
||||
MangaChapter(
|
||||
id = generateUid(chapterUrl),
|
||||
name = li.selectFirst("div.truncate p.truncate")?.text().orEmpty(),
|
||||
number = i + 1,
|
||||
url = chapterUrl,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterDate(
|
||||
simpleDateFormat,
|
||||
li.selectFirst("div.truncate div.items-center")?.text(),
|
||||
),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
val d = date?.lowercase() ?: return 0
|
||||
return when {
|
||||
d.endsWith(" ago") -> 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("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
WordSet("week", "weeks").anyWordIn(date) -> cal.apply { add(Calendar.WEEK_OF_YEAR, -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
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.select("img.max-w-full").map { img ->
|
||||
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt
Normal file
174
src/main/kotlin/org/koitharu/kotatsu/parsers/site/ru/AComics.kt
Normal file
@@ -0,0 +1,174 @@
|
||||
package org.koitharu.kotatsu.parsers.site.ru
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.jsoup.nodes.Document
|
||||
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.util.*
|
||||
|
||||
@MangaSourceParser("ACOMICS", "AComics", "ru", ContentType.COMICS)
|
||||
internal class AComics(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.ACOMICS, pageSize = 10) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL, SortOrder.POPULARITY)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("acomics.ru")
|
||||
|
||||
init {
|
||||
paginator.firstPage = 0
|
||||
searchPaginator.firstPage = 0
|
||||
context.cookieJar.insertCookies(domain, "ageRestrict=18")
|
||||
}
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
append("/search?keyword=")
|
||||
append(filter.query)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("/comics?ratings[]=1&ratings[]=2&ratings[]=3&ratings[]=4&ratings[]=5&ratings[]=6&skip=")
|
||||
append((page * 10).toString())
|
||||
append("&sort=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "last_update"
|
||||
SortOrder.ALPHABETICAL -> "serial_name"
|
||||
SortOrder.POPULARITY -> "subscr_count"
|
||||
else -> "last_update"
|
||||
},
|
||||
)
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("&categories=")
|
||||
append(filter.tags.joinToString(separator = ",") { it.key })
|
||||
}
|
||||
|
||||
if (filter.states.isNotEmpty()) {
|
||||
append("&updatable=")
|
||||
append(
|
||||
filter.states.oneOrThrowIfMany().let {
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "yes"
|
||||
MangaState.FINISHED -> "no"
|
||||
else -> "0"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/comics?ratings[]=1&ratings[]=2&ratings[]=3&ratings[]=4&ratings[]=5&ratings[]=6&sort=last_update&skip=")
|
||||
append((page * 20).toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
private fun parseMangaList(docs: Document): List<Manga> {
|
||||
return docs.select("table.list-loadable").map {
|
||||
val a = it.selectFirstOrThrow("a")
|
||||
val url = a.attrAsAbsoluteUrl("href") + "/about"
|
||||
Manga(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
title = it.selectFirstOrThrow(".title").text(),
|
||||
altTitle = null,
|
||||
publicUrl = url,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = it.selectFirstOrThrow("img").src().orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var tagCache: ArrayMap<String, MangaTag>? = null
|
||||
private val mutex = Mutex()
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return getOrCreateTagMap().values.toSet()
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateTagMap(): Map<String, MangaTag> = mutex.withLock {
|
||||
tagCache?.let { return@withLock it }
|
||||
val tagMap = ArrayMap<String, MangaTag>()
|
||||
val tagElements =
|
||||
webClient.httpGet("https://$domain/comics").parseHtml().requireElementById("catalog").select(" a.button")
|
||||
for (el in tagElements) {
|
||||
val name = el.html().substringAfterLast("</span>")
|
||||
if (name.isEmpty()) continue
|
||||
tagMap[name] = MangaTag(
|
||||
title = name,
|
||||
key = el.attr("onclick").substringAfterLast("('").substringBefore("')"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
tagCache = tagMap
|
||||
return@withLock tagMap
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val tagMap = getOrCreateTagMap()
|
||||
val tags = doc.select("p.serial-about-badges .category").mapNotNullToSet { tagMap[it.text()] }
|
||||
return manga.copy(
|
||||
tags = tags,
|
||||
description = doc.selectFirst("section.serial-about-text p")?.text(),
|
||||
author = doc.selectFirst("p:contains(Автор оригинала:)")?.text()?.replace("Автор оригинала: ", ""),
|
||||
chapters = listOf(
|
||||
MangaChapter(
|
||||
id = manga.id,
|
||||
name = manga.title,
|
||||
number = 1,
|
||||
url = manga.url.replace("/about", "/"),
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url + "1").parseHtml()
|
||||
val totalPages = doc.selectFirstOrThrow("span.issueNumber").text().substringAfterLast('/').toInt()
|
||||
return (1..totalPages).map {
|
||||
val url = chapter.url + it
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String {
|
||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.requireElementById("mainImage").src() ?: doc.parseFailed("Image src not found")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user