From 5d09f08d37edb2fb361d72691b45bfb513c4955f Mon Sep 17 00:00:00 2001
From: Aria Moradi <aria.moradi007@gmail.com>
Date: Sun, 9 Oct 2022 13:22:43 +0330
Subject: [PATCH] Reaper Scans new site (#13751)

* reaperscans

* use livewire to load chapterlist

* bring back other langs

* fix imports

* add search to reaperscans

Co-authored-by: henrik9999 <22085664+henrik9999@users.noreply.github.com>
---
 .../reaperscans/src/ReaperScansFactory.kt     | 223 +++++++++++++++++-
 .../multisrc/madara/MadaraGenerator.kt        |   2 +-
 2 files changed, 218 insertions(+), 7 deletions(-)

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<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 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<Page> {
+        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),