diff --git a/multisrc/overrides/madara/reaperscans/src/ReaperScansFactory.kt b/multisrc/overrides/madara/reaperscans/src/ReaperScansFactory.kt index c015cf7b2..81bb5b826 100644 --- a/multisrc/overrides/madara/reaperscans/src/ReaperScansFactory.kt +++ b/multisrc/overrides/madara/reaperscans/src/ReaperScansFactory.kt @@ -1,10 +1,29 @@ package eu.kanade.tachiyomi.extension.all.reaperscans import eu.kanade.tachiyomi.multisrc.madara.Madara +import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.SourceFactory +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page 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.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale @@ -40,13 +59,205 @@ abstract class ReaperScans( } } -class ReaperScansEn : ReaperScans( - "https://reaperscans.com", - "en", - SimpleDateFormat("MMM dd,yyyy", Locale.US) -) { +class ReaperScansEn : ParsedHttpSource() { + override val name = "Reaper Scans" - override val versionId = 2 + override val baseUrl = "https://reaperscans.com" + + override val lang = "en" + + override val id = 5177220001642863679 + + override val supportsLatest = false + + private val json: Json by injectLazy() + + // Popular + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics?page=$page", headers) + + override fun popularMangaNextPageSelector(): String = "button[wire:click*=nextPage]" + + override fun popularMangaSelector(): String = "li" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("a.text-white").let { + title = it.text() + setUrlWithoutDomain(it.attr("href")) + } + thumbnail_url = element.select("img").attr("abs:src") + } + } + + // Latest + + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used") + + // 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 + else -> SManga.UNKNOWN + } + 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 { + var document = response.asJsoup() + val chapters = mutableListOf() + 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 csrf = csrfToken + var serverMemo = initialProps["serverMemo"]!!.jsonObject + var 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\"]}}]")) + }.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", csrf).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() + } + } + + 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) + } + + if (csrfToken != null && initialProps is JsonObject) { + var serverMemo = initialProps["serverMemo"]!!.jsonObject + var fingerprint = initialProps["fingerprint"]!! + + val payload = buildJsonObject { + put("fingerprint", fingerprint) + put("serverMemo", serverMemo) + put("updates", json.parseToJsonElement("[{\"type\":\"syncInput\",\"payload\":{\"id\":\"03r6\",\"name\":\"query\",\"value\":\"$query\"}}]")) + }.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) + + return Request.Builder().url("$baseUrl/livewire/message/frontend.global-search").method("POST", payload).addHeader("x-csrf-token", csrfToken).addHeader("x-livewire", "true").build() + } + + throw Exception("search error") + } + + 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 -> + searchMangaFromElement(element) + } + + return MangasPage(mangas, false) + } + + // Page + + override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl${chapter.url}") + + override fun pageListParse(document: Document): List { + return document.select("p.py-4 > img").mapIndexed { index, element -> + Page(index, "", element.attr("src")) + } + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used") } class ReaperScansTr : ReaperScans( diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index 79eccc0d2..1fc5cb5f7 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -17,7 +17,7 @@ class MadaraGenerator : ThemeSourceGenerator { MultiLang("MangaForFree.net", "https://mangaforfree.net", listOf("en", "ko", "all"), isNsfw = true, className = "MangaForFreeFactory", pkgName = "mangaforfree", overrideVersionCode = 1), MultiLang("Manhwa18.cc", "https://manhwa18.cc", listOf("en", "ko", "all"), isNsfw = true, className = "Manhwa18CcFactory", pkgName = "manhwa18cc", overrideVersionCode = 2), MultiLang("Olympus Scanlation", "https://olympusscanlation.com", listOf("es", "pt-BR")), - MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 7), + MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 8), MultiLang("Seven King Scanlation", "https://sksubs.net", listOf("es", "en"), isNsfw = true), SingleLang("1st Kiss Manga.love", "https://1stkissmanga.love", "en", className = "FirstKissMangaLove", overrideVersionCode = 1), SingleLang("1st Kiss Manhua", "https://1stkissmanhua.com", "en", className = "FirstKissManhua", overrideVersionCode = 3),