ReaperScans: Cleanup codes (#13871)
This commit is contained in:
parent
131ce10ccf
commit
0e9c86f901
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'Reaper Scans'
|
||||
pkgNameSuffix = 'en.reaperscans'
|
||||
extClass = '.ReaperScans'
|
||||
extVersionCode = 36
|
||||
extVersionCode = 37
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.tachiyomi.extension.en.reaperscans
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -8,17 +9,17 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.addJsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
@ -75,154 +76,27 @@ class ReaperScans : ParsedHttpSource() {
|
|||
title = it.text().trim()
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
thumbnail_url = element.select("img").attr("src")
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
thumbnail_url = document.select("div > img").first().attr("abs:src")
|
||||
title = document.select("h1").first().text()
|
||||
|
||||
status = when (document.select("dt:contains(Source Status)").next().text()) {
|
||||
"On hold" -> SManga.ON_HIATUS
|
||||
"Complete" -> SManga.COMPLETED
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Dropped" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
val genreList = mutableListOf<String>()
|
||||
val seriesType = when (document.select("dt:contains(Source Language)").next().text()) {
|
||||
"Korean" -> "Manhwa"
|
||||
"Chinese" -> "Manhua"
|
||||
"Japanese" -> "Manga"
|
||||
else -> null
|
||||
}
|
||||
seriesType?.let { genreList.add(it) }
|
||||
|
||||
genre = genreList.takeIf { genreList.isNotEmpty() }?.joinToString(",")
|
||||
description = document.select("section > div:nth-child(1) > div > p").first().text()
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListSelector() = "ul > li"
|
||||
|
||||
/**
|
||||
* Recursively merges j2 onto j1 in place
|
||||
* If j1 and j2 both contain keys whose values aren't both jsonObjects, j2's value overwrites j1's
|
||||
*
|
||||
*/
|
||||
private fun mergeLeft(j1: JsonObject, j2: JsonObject): JsonObject = buildJsonObject {
|
||||
j1.keys.forEach { put(it, j1[it]!!) }
|
||||
j2.keys.forEach { k ->
|
||||
when {
|
||||
j1[k] !is JsonObject -> put(k, j2[k]!!)
|
||||
j1[k] is JsonObject && j2[k] is JsonObject -> put(k, mergeLeft(j1[k]!!.jsonObject, j2[k]!!.jsonObject))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
var document = response.asJsoup()
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
document.select("div.pb-4 > div >" + chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||
|
||||
val csrfToken = document.selectFirst("meta[name=csrf-token]")?.attr("content")
|
||||
|
||||
val initialProps = document.selectFirst("div[wire:initial-data*=frontend.comic-chapters-list]")?.attr("wire:initial-data")?.let {
|
||||
json.parseToJsonElement(it)
|
||||
}
|
||||
|
||||
if (csrfToken != null && initialProps is JsonObject) {
|
||||
var serverMemo = initialProps["serverMemo"]!!.jsonObject
|
||||
val fingerprint = initialProps["fingerprint"]!!
|
||||
|
||||
var nextPage = 2
|
||||
while (document.select(popularMangaNextPageSelector()).isNotEmpty()) {
|
||||
val payload = buildJsonObject {
|
||||
put("fingerprint", fingerprint)
|
||||
put("serverMemo", serverMemo)
|
||||
// put("updates", json.parseToJsonElement("[{\"type\":\"callMethod\",\"payload\":{\"id\":\"9jhcg\",\"method\":\"gotoPage\",\"params\":[$nextPage,\"page\"]}}]"))
|
||||
putJsonArray("updates") {
|
||||
addJsonObject {
|
||||
put("type", "callMethod")
|
||||
putJsonObject("payload") {
|
||||
put("id", "9jhcg")
|
||||
put("method", "gotoPage")
|
||||
putJsonArray("params") {
|
||||
add(nextPage)
|
||||
add("page")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
|
||||
val request = Request.Builder().url("$baseUrl/livewire/message/frontend.comic-chapters-list").method("POST", payload).addHeader("x-csrf-token", csrfToken).addHeader("x-livewire", "true").build()
|
||||
|
||||
val response1 = client.newCall(request).execute()
|
||||
val responseText = response1.body!!.string()
|
||||
|
||||
val responseJson = json.parseToJsonElement(responseText).jsonObject
|
||||
|
||||
// response contains state that we need to preserve
|
||||
serverMemo = mergeLeft(serverMemo, responseJson["serverMemo"]!!.jsonObject)
|
||||
|
||||
document = Jsoup.parse(responseJson["effects"]!!.jsonObject.get("html")?.jsonPrimitive?.content)
|
||||
|
||||
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||
nextPage++
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
with(element) {
|
||||
select("a").first()?.let { urlElement ->
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("abs:href"))
|
||||
chapter.name = urlElement.select("p").first().text()
|
||||
urlElement.select("p").takeIf { it.size > 1 }?.let {
|
||||
chapter.date_upload = parseRelativeDate(it[1].text())
|
||||
}
|
||||
}
|
||||
}
|
||||
return chapter
|
||||
}
|
||||
|
||||
// Search
|
||||
override fun searchMangaSelector(): String = "a[href*=/comics/]"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
element.select("img").first()?.let {
|
||||
thumbnail_url = it.attr("abs:src")
|
||||
}
|
||||
title = element.select("p").first().text()
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException("Not Used")
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val response = client.newCall(GET(baseUrl)).execute()
|
||||
val soup = response.asJsoup()
|
||||
|
||||
val csrfToken = soup.selectFirst("meta[name=csrf-token]")?.attr("content")
|
||||
|
||||
val initialProps = soup.selectFirst("div[wire:initial-data*=frontend.global-search]")?.attr("wire:initial-data")?.let {
|
||||
json.parseToJsonElement(it)
|
||||
}
|
||||
val livewareData = soup.selectFirst("div[wire:initial-data*=frontend.global-search]")
|
||||
?.attr("wire:initial-data")
|
||||
?.parseJson<LiveWireDataDto>()
|
||||
|
||||
if (csrfToken != null && initialProps is JsonObject) {
|
||||
val serverMemo = initialProps["serverMemo"]!!.jsonObject
|
||||
val fingerprint = initialProps["fingerprint"]!!
|
||||
if (csrfToken == null) error("Couldn't find csrf-token")
|
||||
if (livewareData == null) error("Couldn't find LiveWireData")
|
||||
|
||||
val payload = buildJsonObject {
|
||||
put("fingerprint", fingerprint)
|
||||
put("serverMemo", serverMemo)
|
||||
// put("updates", json.parseToJsonElement("[{\"type\":\"syncInput\",\"payload\":{\"id\":\"03r6\",\"name\":\"query\",\"value\":\"$query\"}}]"))
|
||||
put("fingerprint", livewareData.fingerprint)
|
||||
put("serverMemo", livewareData.serverMemo)
|
||||
putJsonArray("updates") {
|
||||
addJsonObject {
|
||||
put("type", "syncInput")
|
||||
|
@ -233,51 +107,198 @@ class ReaperScans : ParsedHttpSource() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
}.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
return Request.Builder().url("$baseUrl/livewire/message/frontend.global-search").method("POST", payload).addHeader("x-csrf-token", csrfToken).addHeader("x-livewire", "true").build()
|
||||
val headers = Headers.Builder()
|
||||
.add("x-csrf-token", csrfToken)
|
||||
.add("x-livewire", "true")
|
||||
.build()
|
||||
|
||||
return POST("$baseUrl/livewire/message/frontend.global-search", headers, payload)
|
||||
}
|
||||
|
||||
throw Exception("search error")
|
||||
}
|
||||
override fun searchMangaSelector(): String = "a[href*=/comics/]"
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val responseText = response.body!!.string()
|
||||
val responseJson = json.parseToJsonElement(responseText).jsonObject
|
||||
val document = Jsoup.parse(responseJson["effects"]!!.jsonObject.get("html")?.jsonPrimitive?.content)
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
val html = response.parseJson<LiveWireResponseDto>().effects.html
|
||||
val mangas = Jsoup.parse(html, baseUrl).select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Page
|
||||
override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl${chapter.url}")
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
element.select("img").first()?.let {
|
||||
thumbnail_url = it.attr("abs:src")
|
||||
}
|
||||
title = element.select("p").first().text()
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
thumbnail_url = document.select("div > img").first().attr("abs:src")
|
||||
title = document.select("h1").first().text()
|
||||
|
||||
status = when (document.select("dt:contains(Release Status)").next().text()) {
|
||||
"On hold" -> SManga.ON_HIATUS
|
||||
"Complete" -> SManga.COMPLETED
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Dropped" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
genre = mutableListOf<String>().apply {
|
||||
when (document.select("dt:contains(Source Language)").next().text()) {
|
||||
"Korean" -> "Manhwa"
|
||||
"Chinese" -> "Manhua"
|
||||
"Japanese" -> "Manga"
|
||||
else -> null
|
||||
}?.let { add(it) }
|
||||
}.takeIf { it.isNotEmpty() }?.joinToString(",")
|
||||
|
||||
description = document.select("section > div:nth-child(1) > div > p").first().text()
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
private fun chapterListNextPageSelector(): String = "button[wire:click*=nextPage]"
|
||||
|
||||
override fun chapterListSelector() = "div[wire:id] > ul[role=list] > li"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
var document = response.asJsoup()
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
||||
val csrfToken = document.selectFirst("meta[name=csrf-token]")?.attr("content")
|
||||
|
||||
val livewareData = document.selectFirst("div[wire:initial-data*=frontend.comic-chapters-list]")
|
||||
?.attr("wire:initial-data")
|
||||
?.parseJson<LiveWireDataDto>()
|
||||
|
||||
if (csrfToken == null) error("Couldn't find csrf-token")
|
||||
if (livewareData == null) error("Couldn't find LiveWireData")
|
||||
|
||||
val fingerprint = livewareData.fingerprint
|
||||
var serverMemo = livewareData.serverMemo
|
||||
|
||||
var pageToQuery = 1
|
||||
var hasNextPage = true
|
||||
|
||||
while (hasNextPage) {
|
||||
if (pageToQuery != 1) {
|
||||
val payload = buildJsonObject {
|
||||
put("fingerprint", fingerprint)
|
||||
put("serverMemo", serverMemo)
|
||||
putJsonArray("updates") {
|
||||
addJsonObject {
|
||||
put("type", "callMethod")
|
||||
putJsonObject("payload") {
|
||||
put("id", "9jhcg")
|
||||
put("method", "gotoPage")
|
||||
putJsonArray("params") {
|
||||
add(pageToQuery)
|
||||
add("page")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
val headers = Headers.Builder()
|
||||
.add("x-csrf-token", csrfToken)
|
||||
.add("x-livewire", "true")
|
||||
.build()
|
||||
|
||||
val request = POST("$baseUrl/livewire/message/frontend.comic-chapters-list", headers, payload)
|
||||
|
||||
val responseData = client.newCall(request).execute().parseJson<LiveWireResponseDto>()
|
||||
|
||||
// response contains state that we need to preserve
|
||||
serverMemo = serverMemo.mergeLeft(responseData.serverMemo)
|
||||
document = Jsoup.parse(responseData.effects.html, baseUrl)
|
||||
}
|
||||
|
||||
document.select(chapterListSelector()).forEach { chapters.add(chapterFromElement(it)) }
|
||||
hasNextPage = document.selectFirst(chapterListNextPageSelector()) != null
|
||||
pageToQuery++
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.selectFirst("a")?.let { urlElement ->
|
||||
setUrlWithoutDomain(urlElement.attr("href"))
|
||||
urlElement.select("p").let {
|
||||
name = it.getOrNull(0)?.text() ?: ""
|
||||
date_upload = it.getOrNull(1)?.text()?.parseRelativeDate() ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("img.max-w-full").mapIndexed { index, element ->
|
||||
Page(index, "", element.attr("src"))
|
||||
Page(index, imageUrl = element.attr("src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
|
||||
// Helpers
|
||||
private inline fun <reified T> Response.parseJson(): T = use {
|
||||
it.body?.string().orEmpty().parseJson()
|
||||
}
|
||||
|
||||
// Parses dates in this form:
|
||||
// 21 horas ago
|
||||
// Taken from multisrc/madara/Madara.kt
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||
private inline fun <reified T> String.parseJson(): T = json.decodeFromString(this)
|
||||
|
||||
/**
|
||||
* Recursively merges j2 onto j1 in place
|
||||
* If j1 and j2 both contain keys whose values aren't both jsonObjects, j2's value overwrites j1's
|
||||
*
|
||||
*/
|
||||
private fun JsonObject.mergeLeft(j2: JsonObject): JsonObject = buildJsonObject {
|
||||
val j1 = this@mergeLeft
|
||||
j1.entries.forEach { (key, value) -> put(key, value) }
|
||||
j2.entries.forEach { (key, value) ->
|
||||
val j1Value = j1[key]
|
||||
when {
|
||||
j1Value !is JsonObject -> put(key, value)
|
||||
value is JsonObject -> put(key, j1Value.mergeLeft(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses dates in this form: 21 hours ago
|
||||
* Taken from multisrc/madara/Madara.kt
|
||||
*/
|
||||
private fun String.parseRelativeDate(): Long {
|
||||
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: return 0
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
return when {
|
||||
date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
date.contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
date.contains("week") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
||||
date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
contains("week") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
||||
contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
// Unused
|
||||
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not Used")
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not Used")
|
||||
|
||||
companion object {
|
||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package eu.kanade.tachiyomi.extension.en.reaperscans
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class LiveWireResponseDto(
|
||||
val effects: LiveWireEffectsDto,
|
||||
val serverMemo: JsonObject
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LiveWireEffectsDto(
|
||||
val html: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LiveWireDataDto(
|
||||
val fingerprint: JsonObject,
|
||||
val serverMemo: JsonObject
|
||||
)
|
Loading…
Reference in New Issue