Merge branch 'KotatsuApp:master' into master
commit
60ca36bb4e
@ -0,0 +1,318 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.en
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.json.JSONObject
|
||||||
|
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.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.*
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
internal abstract class LineWebtoonsParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaSource,
|
||||||
|
) : MangaParser(context, source) {
|
||||||
|
|
||||||
|
private val signer by lazy {
|
||||||
|
WebtoonsUrlSigner("gUtPzJFZch4ZyAGviiyH94P99lQ3pFdRTwpJWDlSGFfwgpr6ses5ALOxWHOIT7R1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't __really__ support changing this domain because:
|
||||||
|
// 1. I don't think other websites have this exact API
|
||||||
|
// 2. most communication is done with other domains (hosting API and static content), which are not configurable
|
||||||
|
// 3. we rely on the HTTP client setting the referer header to webtoons.com
|
||||||
|
//
|
||||||
|
// This effectively means that changing the domain will break the source. Yikes
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("webtoons.com")
|
||||||
|
|
||||||
|
private val apiDomain = "global.apis.naver.com"
|
||||||
|
private val staticDomain = "webtoon-phinf.pstatic.net"
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
// doesn't actually sort by rating, but by likes
|
||||||
|
// this should be fine though
|
||||||
|
SortOrder.RATING,
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
)
|
||||||
|
override val headers: Headers
|
||||||
|
get() = Headers.Builder()
|
||||||
|
.add("User-Agent", "nApps (Android 12;; linewebtoon; 3.1.0)")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(page: MangaPage): String {
|
||||||
|
return page.url.toAbsoluteUrl(staticDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// some language tags do not map perfectly to the ones used by the API
|
||||||
|
private val languageCode: String
|
||||||
|
get() = when (val tag = sourceLocale.toLanguageTag()) {
|
||||||
|
"in" -> "id"
|
||||||
|
"zh" -> "zh-hant"
|
||||||
|
else -> tag
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getChapters(titleNo: Long): List<MangaChapter> {
|
||||||
|
val firstResult = makeRequest(
|
||||||
|
url = "/lineWebtoon/webtoon/challengeEpisodeList.json?v=2&titleNo=$titleNo&startIndex=0&pageSize=30",
|
||||||
|
)
|
||||||
|
|
||||||
|
val totalEpisodeCount = firstResult
|
||||||
|
.getJSONObject("episodeList")
|
||||||
|
.getInt("totalServiceEpisodeCount")
|
||||||
|
|
||||||
|
val episodes = firstResult
|
||||||
|
.getJSONObject("episodeList")
|
||||||
|
.getJSONArray("episode")
|
||||||
|
.toJSONList()
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
while (episodes.count() < totalEpisodeCount) {
|
||||||
|
val page = makeRequest(
|
||||||
|
url = "/lineWebtoon/webtoon/challengeEpisodeList.json?v=2&titleNo=$titleNo&startIndex=${episodes.count()}&pageSize=30",
|
||||||
|
).getJSONObject("episodeList")
|
||||||
|
.getJSONArray("episode")
|
||||||
|
.toJSONList()
|
||||||
|
|
||||||
|
episodes.addAll(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodes.mapChapters { i, jo ->
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid("$titleNo-$i"),
|
||||||
|
name = jo.getString("episodeTitle"),
|
||||||
|
number = jo.getInt("episodeSeq"),
|
||||||
|
url = "$titleNo-${jo.get("episodeNo")}",
|
||||||
|
uploadDate = jo.getLong("modifyYmdt"),
|
||||||
|
branch = null,
|
||||||
|
scanlator = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}.sortedBy { it.number }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||||
|
val titleNo = manga.url.toLong()
|
||||||
|
val chaptersDeferred = async { getChapters(titleNo) }
|
||||||
|
|
||||||
|
makeRequest("/lineWebtoon/webtoon/challengeTitleInfo.json?v=2&titleNo=${titleNo}")
|
||||||
|
.getJSONObject("titleInfo")
|
||||||
|
.let { jo ->
|
||||||
|
Manga(
|
||||||
|
id = generateUid(titleNo),
|
||||||
|
title = jo.getString("title"),
|
||||||
|
altTitle = null,
|
||||||
|
url = "$titleNo",
|
||||||
|
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=${titleNo}",
|
||||||
|
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||||
|
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||||
|
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||||
|
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||||
|
tags = setOf(parseTag(jo.getJSONObject("genreInfo"))),
|
||||||
|
author = jo.getStringOrNull("writingAuthorName"),
|
||||||
|
description = jo.getString("synopsis"),
|
||||||
|
// I don't think the API provides this info
|
||||||
|
state = null,
|
||||||
|
chapters = chaptersDeferred.await(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getList(
|
||||||
|
offset: Int,
|
||||||
|
query: String?,
|
||||||
|
tags: Set<MangaTag>?,
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
): List<Manga> {
|
||||||
|
val genre = tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||||
|
|
||||||
|
val sortOrderStr = when (sortOrder) {
|
||||||
|
SortOrder.UPDATED -> "UPDATE"
|
||||||
|
SortOrder.POPULARITY -> "READ_COUNT"
|
||||||
|
SortOrder.RATING -> "LIKEIT"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder")
|
||||||
|
}
|
||||||
|
|
||||||
|
val manga = if (query != null) {
|
||||||
|
if (!tags.isNullOrEmpty()) {
|
||||||
|
throw IllegalArgumentException("This source does not support search with tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
|
||||||
|
.getJSONObject("challengeSearch")
|
||||||
|
.getJSONArray("titleList")
|
||||||
|
.mapJSON { jo ->
|
||||||
|
val titleNo = jo.getLong("titleNo")
|
||||||
|
|
||||||
|
Manga(
|
||||||
|
id = generateUid(titleNo),
|
||||||
|
title = jo.getString("title"),
|
||||||
|
altTitle = null,
|
||||||
|
url = titleNo.toString(),
|
||||||
|
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
isNsfw = isNsfwSource,
|
||||||
|
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||||
|
largeCoverUrl = null,
|
||||||
|
tags = emptySet(),
|
||||||
|
author = jo.getStringOrNull("writingAuthorName"),
|
||||||
|
description = null,
|
||||||
|
state = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val result =
|
||||||
|
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
|
||||||
|
|
||||||
|
val genres = result.getJSONObject("genreList")
|
||||||
|
.getJSONArray("challengeGenres")
|
||||||
|
.mapJSON { jo -> parseTag(jo) }
|
||||||
|
.associateBy { tag -> tag.key }
|
||||||
|
|
||||||
|
result
|
||||||
|
.getJSONObject("titleList")
|
||||||
|
.getJSONArray("titles")
|
||||||
|
.mapJSON { jo ->
|
||||||
|
val titleNo = jo.getLong("titleNo")
|
||||||
|
|
||||||
|
Manga(
|
||||||
|
id = generateUid(titleNo),
|
||||||
|
title = jo.getString("title"),
|
||||||
|
altTitle = null,
|
||||||
|
url = titleNo.toString(),
|
||||||
|
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||||
|
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||||
|
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||||
|
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||||
|
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||||
|
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||||
|
author = jo.getStringOrNull("writingAuthorName"),
|
||||||
|
description = jo.getString("synopsis"),
|
||||||
|
// I don't think the API provides this info
|
||||||
|
state = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val (titleNo, episodeNo) = requireNotNull(chapter.url.splitTwoParts('-'))
|
||||||
|
|
||||||
|
return makeRequest("/lineWebtoon/webtoon/challengeEpisodeInfo.json?v=2&titleNo=$titleNo&episodeNo=$episodeNo")
|
||||||
|
.getJSONObject("episodeInfo")
|
||||||
|
.getJSONArray("imageInfo")
|
||||||
|
.mapJSONIndexed { i, jo ->
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid("$titleNo-$episodeNo-$i"),
|
||||||
|
url = jo.getString("url"),
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTag(jo: JSONObject): MangaTag {
|
||||||
|
return MangaTag(
|
||||||
|
title = jo.getString("name"),
|
||||||
|
key = jo.getString("code"),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> {
|
||||||
|
return makeRequest("/lineWebtoon/webtoon/challengeGenreList.json")
|
||||||
|
.getJSONObject("genreList")
|
||||||
|
.getJSONArray("challengeGenres")
|
||||||
|
.mapJSONToSet { jo -> parseTag(jo) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun makeRequest(url: String): JSONObject {
|
||||||
|
val resp = webClient.httpGet(finalizeUrl(url))
|
||||||
|
val message: JSONObject? = resp.parseJson().optJSONObject("message")
|
||||||
|
return when (resp.code) {
|
||||||
|
in 200..299 -> checkNotNull(message).getJSONObject("result")
|
||||||
|
404 -> throw NotFoundException(message?.getStringOrNull("message").orEmpty(), url)
|
||||||
|
else -> {
|
||||||
|
val code = message?.getIntOrDefault("code", 0)
|
||||||
|
val errorMessage = message?.getStringOrNull("message")
|
||||||
|
throw ParseException("Api error (code=$code): $errorMessage", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finalizeUrl(url: String): HttpUrl {
|
||||||
|
val httpUrl = url.toAbsoluteUrl(apiDomain).toHttpUrl()
|
||||||
|
val builder = httpUrl.newBuilder()
|
||||||
|
.addQueryParameter("serviceZone", "GLOBAL")
|
||||||
|
if (httpUrl.queryParameter("v") == null) {
|
||||||
|
builder.addQueryParameter("v", "1")
|
||||||
|
}
|
||||||
|
builder.addQueryParameter("language", languageCode)
|
||||||
|
.addQueryParameter("locale", "languageCode")
|
||||||
|
.addQueryParameter("platform", "APP_ANDROID")
|
||||||
|
signer.makeEncryptUrl(builder)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_EN", "Line Webtoons English", "en", type = ContentType.MANGA)
|
||||||
|
class English(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_EN)
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_ZH", "Line Webtoons Chinese", "zh", type = ContentType.MANGA)
|
||||||
|
class Chinese(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ZH)
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_TH", "Line Webtoons Thai", "th", type = ContentType.MANGA)
|
||||||
|
class Thai(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_TH)
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_ID", "Line Webtoons Indonesian", "id", type = ContentType.MANGA)
|
||||||
|
class Indonesian(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ID)
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_ES", "Line Webtoons Spanish", "es", type = ContentType.MANGA)
|
||||||
|
class Spanish(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_ES)
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_FR", "Line Webtoons French", "fr", type = ContentType.MANGA)
|
||||||
|
class French(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_FR)
|
||||||
|
|
||||||
|
@MangaSourceParser("LINEWEBTOONS_DE", "Line Webtoons German", "de", type = ContentType.MANGA)
|
||||||
|
class German(context: MangaLoaderContext) : LineWebtoonsParser(context, MangaSource.LINEWEBTOONS_DE)
|
||||||
|
|
||||||
|
private inner class WebtoonsUrlSigner(private val secret: String) {
|
||||||
|
|
||||||
|
private val mac = Mac.getInstance("HmacSHA1").apply {
|
||||||
|
this.init(SecretKeySpec(secret.encodeToByteArray(), "HmacSHA1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMessage(url: String, msgpad: String): String {
|
||||||
|
return url.substring(0, 0xFF.coerceAtMost(url.length)) + msgpad
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMessageDigest(s: String): String {
|
||||||
|
val signedMessage = synchronized(mac) { mac.doFinal(s.toByteArray()) }
|
||||||
|
return context.encodeBase64(signedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeEncryptUrl(urlBuilder: HttpUrl.Builder) {
|
||||||
|
val msgpad = Calendar.getInstance().timeInMillis.toString()
|
||||||
|
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgpad))
|
||||||
|
urlBuilder
|
||||||
|
.addQueryParameter("msgpad", msgpad)
|
||||||
|
.addQueryParameter("md", digest)
|
||||||
|
// .addEncodedQueryParameter("md", digest.urlEncoded())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.fmreader.es
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
|
||||||
|
@MangaSourceParser("OLIMPOSCANS", "Olimpo Scans", "es")
|
||||||
|
internal class OlimpoScans(context: MangaLoaderContext) :
|
||||||
|
FmreaderParser(context, MangaSource.OLIMPOSCANS, "olimposcans.com") {
|
||||||
|
|
||||||
|
override val selectState = "ul.manga-info li:contains(Estado) a"
|
||||||
|
override val selectAlt = "ul.manga-info li:contains(Otros nombres)"
|
||||||
|
override val selectTag = "ul.manga-info li:contains(Género) a"
|
||||||
|
override val tagPrefix = "lista-de-comics-genero-"
|
||||||
|
|
||||||
|
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(listeurl)
|
||||||
|
append("?page=")
|
||||||
|
append(page.toString())
|
||||||
|
when {
|
||||||
|
!query.isNullOrEmpty() -> {
|
||||||
|
append("&name=")
|
||||||
|
append(query.urlEncoded())
|
||||||
|
}
|
||||||
|
|
||||||
|
!tags.isNullOrEmpty() -> {
|
||||||
|
append("&genre=")
|
||||||
|
append(tag?.key.orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append("&sort=")
|
||||||
|
when (sortOrder) {
|
||||||
|
SortOrder.POPULARITY -> append("views")
|
||||||
|
SortOrder.UPDATED -> append("last_update")
|
||||||
|
SortOrder.ALPHABETICAL -> append("name")
|
||||||
|
else -> append("last_update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val doc = webClient.httpGet(url).parseHtml()
|
||||||
|
val lastPage =
|
||||||
|
doc.selectLast(".pagination a")?.attr("href")?.substringAfterLast("page=")?.substringBeforeLast("&artist")
|
||||||
|
?.toInt() ?: 1
|
||||||
|
if (lastPage < page) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return doc.select("div.thumb-item-flow").map { div ->
|
||||||
|
val href = "/" + div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
|
||||||
|
Manga(
|
||||||
|
id = generateUid(href),
|
||||||
|
url = href,
|
||||||
|
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||||
|
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg").toAbsoluteUrl(domain),
|
||||||
|
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
|
||||||
|
altTitle = null,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
tags = emptySet(),
|
||||||
|
author = null,
|
||||||
|
state = null,
|
||||||
|
source = source,
|
||||||
|
isNsfw = isNsfwSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ("/proxy.php?link=" + img.src()).toRelativeUrl(domain)
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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("BARMANGA", "Bar Manga", "es")
|
||||||
|
internal class BarManga(context: MangaLoaderContext) :
|
||||||
|
MadaraParser(context, MangaSource.BARMANGA, "barmanga.com") {
|
||||||
|
override val datePattern = "MM/dd/yyyy"
|
||||||
|
}
|
||||||
@ -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("GANZOSCAN", "Ganzo Scan", "es")
|
||||||
|
internal class GanzoScan(context: MangaLoaderContext) :
|
||||||
|
MadaraParser(context, MangaSource.GANZOSCAN, "ganzoscan.com") {
|
||||||
|
override val datePattern: String = "dd 'de' MMMMM 'de' yyyy"
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||||
|
|
||||||
|
@MangaSourceParser("FLOWERMANGA", "Flower Manga", "pt")
|
||||||
|
internal class FlowerManga(context: MangaLoaderContext) :
|
||||||
|
MadaraParser(context, MangaSource.FLOWERMANGA, "flowermanga.com", 24) {
|
||||||
|
override val datePattern = "d MMMM yyyy"
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
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("KAKUSEIPROJECT", "Kakusei Project", "pt")
|
||||||
|
internal class KakuseiProject(context: MangaLoaderContext) :
|
||||||
|
MadaraParser(context, MangaSource.KAKUSEIPROJECT, "kakuseiproject.com.br", 10)
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||||
|
|
||||||
|
@MangaSourceParser("SWEETSCAN", "Sweet Scan", "pt")
|
||||||
|
internal class SweetScan(context: MangaLoaderContext) :
|
||||||
|
MadaraParser(context, MangaSource.SWEETSCAN, "sweetscan.net") {
|
||||||
|
override val datePattern: String = "dd 'de' MMMMM 'de' yyyy"
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TABERU", "Taberu", "pt")
|
||||||
|
internal class Taberu(context: MangaLoaderContext) :
|
||||||
|
MadaraParser(context, MangaSource.TABERU, "taberu.org", 10) {
|
||||||
|
override val datePattern: String = "dd/MM/yyyy"
|
||||||
|
}
|
||||||
@ -1,12 +1,39 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.mangareader.ar
|
package org.koitharu.kotatsu.parsers.site.mangareader.ar
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
@MangaSourceParser("POTATOMANGA", "Potato Manga", "ar")
|
@MangaSourceParser("POTATOMANGA", "Potato Manga", "ar")
|
||||||
internal class PotatoManga(context: MangaLoaderContext) :
|
internal class PotatoManga(context: MangaLoaderContext) :
|
||||||
MangaReaderParser(context, MangaSource.POTATOMANGA, "potatomanga.xyz", pageSize = 30, searchPageSize = 10) {
|
MangaReaderParser(context, MangaSource.POTATOMANGA, "potatomanga.xyz", pageSize = 30, searchPageSize = 10) {
|
||||||
override val listUrl = "/series"
|
override val listUrl = "/series"
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val chapterUrl = chapter.url.toAbsoluteUrl(domain)
|
||||||
|
val docs = webClient.httpGet(chapterUrl).parseHtml()
|
||||||
|
val script = docs.selectFirstOrThrow(selectTestScript)
|
||||||
|
val images = JSONObject(script.data().substringAfter('(').substringBeforeLast(')'))
|
||||||
|
.getJSONArray("sources")
|
||||||
|
.getJSONObject(0)
|
||||||
|
.getJSONArray("images")
|
||||||
|
val pages = ArrayList<MangaPage>(images.length())
|
||||||
|
for (i in 0 until images.length()) {
|
||||||
|
pages.add(
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(images.getString(i).replace("\\", "")),
|
||||||
|
url = images.getString(i).replace("\\", ""),
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
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("CYPHERSCANS", "Cypher Scans", "en")
|
||||||
|
internal class CypherScans(context: MangaLoaderContext) :
|
||||||
|
MangaReaderParser(context, MangaSource.CYPHERSCANS, "cypherscans.xyz", pageSize = 20, searchPageSize = 10)
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
@MangaSourceParser("WARUNGKOMIK", "Warung Komik", "id")
|
||||||
|
internal class WarungKomik(context: MangaLoaderContext) :
|
||||||
|
MangaReaderParser(context, MangaSource.WARUNGKOMIK, "warungkomik.com", pageSize = 20, searchPageSize = 10)
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.vi
|
||||||
|
|
||||||
|
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.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.parsers.util.domain
|
||||||
|
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.asIterable
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||||
|
import org.koitharu.kotatsu.parsers.util.requireElementById
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||||
|
import org.koitharu.kotatsu.parsers.util.tryParse
|
||||||
|
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@MangaSourceParser("YURINEKO", "Yurineko", "vi", ContentType.HENTAI)
|
||||||
|
class YurinekoParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.YURINEKO, 20) {
|
||||||
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
|
get() = ConfigKey.Domain("yurineko.net")
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder>
|
||||||
|
get() = EnumSet.of(SortOrder.UPDATED)
|
||||||
|
|
||||||
|
private val apiDomain
|
||||||
|
get() = "api.$domain"
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val response = webClient.httpGet(manga.url.toAbsoluteUrl(apiDomain)).parseJson()
|
||||||
|
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||||
|
return manga.copy(
|
||||||
|
chapters = response.getJSONArray("chapters")
|
||||||
|
.toJSONList()
|
||||||
|
.mapChapters(true) { i, jo ->
|
||||||
|
val mangaId = jo.getInt("mangaID")
|
||||||
|
val chapterId = jo.getInt("id")
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid(chapterId.toLong()),
|
||||||
|
name = jo.getString("name"),
|
||||||
|
number = i + 1,
|
||||||
|
scanlator = null,
|
||||||
|
url = "/read/$mangaId/$chapterId",
|
||||||
|
uploadDate = df.tryParse(jo.getString("date")),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListPage(
|
||||||
|
page: Int,
|
||||||
|
query: String?,
|
||||||
|
tags: Set<MangaTag>?,
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
): List<Manga> {
|
||||||
|
val listUrl = when {
|
||||||
|
!query.isNullOrEmpty() -> "/search?query=${query.urlEncoded()}&page=$page"
|
||||||
|
tags.isNullOrEmpty() -> "/lastest2?page=$page"
|
||||||
|
tags.size == 1 -> "/searchType?type=tag&id=${tags.first().key}&page=$page"
|
||||||
|
else -> {
|
||||||
|
// Sort order is different when filter with multiple tags
|
||||||
|
val tagKeys = tags.joinToString(separator = ",") { it.key }
|
||||||
|
"/advancedSearch?genre=$tagKeys¬Genre=&sort=7&minChapter=1&status=0&page=$page"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jsonResponse = webClient.httpGet(listUrl.toAbsoluteUrl(apiDomain)).parseJson()
|
||||||
|
return jsonResponse.getJSONArray("result")
|
||||||
|
.mapJSON { jo ->
|
||||||
|
val id = jo.getLong("id")
|
||||||
|
val relativeUrl = "/manga/$id"
|
||||||
|
Manga(
|
||||||
|
id = generateUid(id),
|
||||||
|
title = jo.getString("originalName"),
|
||||||
|
altTitle = jo.getStringOrNull("otherName"),
|
||||||
|
url = relativeUrl,
|
||||||
|
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
isNsfw = true,
|
||||||
|
coverUrl = jo.getString("thumbnail"),
|
||||||
|
tags = jo.getJSONArray("tag").mapJSONToSet { tag ->
|
||||||
|
MangaTag(
|
||||||
|
title = tag.getString("name"),
|
||||||
|
key = tag.getInt("id").toString(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
state = when (jo.getInt("status")) {
|
||||||
|
2 -> MangaState.FINISHED
|
||||||
|
1, 3, 4 -> MangaState.ONGOING
|
||||||
|
5, 6, 7 -> MangaState.ABANDONED
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
author = jo.getJSONArray("author")
|
||||||
|
.mapJSON { author -> author.getString("name") }
|
||||||
|
.joinToString { it },
|
||||||
|
description = jo.getStringOrNull("description"),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val jsonData = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||||
|
.requireElementById("__NEXT_DATA__")
|
||||||
|
.data()
|
||||||
|
return JSONObject(jsonData).getJSONObject("props")
|
||||||
|
.getJSONObject("pageProps")
|
||||||
|
.getJSONObject("chapterData")
|
||||||
|
.getJSONArray("url")
|
||||||
|
.asIterable<String>()
|
||||||
|
.map { url ->
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> {
|
||||||
|
return webClient.httpGet("https://$apiDomain/tag/find?query=")
|
||||||
|
.parseJsonArray()
|
||||||
|
.mapJSONToSet { jo ->
|
||||||
|
MangaTag(
|
||||||
|
key = jo.getInt("id").toString(),
|
||||||
|
title = jo.getString("name"),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue