diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml index 47eecb5de..cf89f2e14 100644 --- a/.github/workflows/issue_moderator.yml +++ b/.github/workflows/issue_moderator.yml @@ -43,7 +43,7 @@ jobs: }, { "type": "both", - "regex": ".*(hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|colamanhua|mangadig|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangatoday|manga\\.town|onemanga\\.info|koushoku|ksk\\.moe|comikey|leercapitulo|c[uứ]u\\s*truy[eệ]n).*", + "regex": ".*(hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|colamanhua|mangadig|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangatoday|manga\\.town|onemanga\\.info|koushoku|ksk\\.moe|comikey|leercapitulo|c[uứ]u\\s*truy[eệ]n|reaper\\s*scans).*", "ignoreCase": true, "labels": ["invalid"], "message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information." diff --git a/REMOVED_SOURCES.md b/REMOVED_SOURCES.md index 365f776f7..3cced66be 100644 --- a/REMOVED_SOURCES.md +++ b/REMOVED_SOURCES.md @@ -19,6 +19,7 @@ - ManhuaScan https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7129 - ManhwaHot https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7129 - Neox Scanlator https://github.com/tachiyomiorg/tachiyomi-extensions/pull/12695 +- Reaper Scans (EN) https://github.com/tachiyomiorg/tachiyomi-extensions/pull/16819 - SuperMangás and SuperHentais https://github.com/tachiyomiorg/tachiyomi-extensions/pull/6348 - TopToon+ https://github.com/tachiyomiorg/tachiyomi-extensions/pull/10851 - Tsuki Mangás https://github.com/tachiyomiorg/tachiyomi-extensions/pull/8609 diff --git a/src/en/reaperscans/AndroidManifest.xml b/src/en/reaperscans/AndroidManifest.xml deleted file mode 100644 index 3905aa593..000000000 --- a/src/en/reaperscans/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/en/reaperscans/build.gradle b/src/en/reaperscans/build.gradle deleted file mode 100644 index 566bcdfd2..000000000 --- a/src/en/reaperscans/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlinx-serialization' - -ext { - extName = 'Reaper Scans' - pkgNameSuffix = 'en.reaperscans' - extClass = '.ReaperScans' - extVersionCode = 46 -} - -apply from: "$rootDir/common.gradle" diff --git a/src/en/reaperscans/res/mipmap-hdpi/ic_launcher.png b/src/en/reaperscans/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index f6c5fc5a7..000000000 Binary files a/src/en/reaperscans/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/reaperscans/res/mipmap-mdpi/ic_launcher.png b/src/en/reaperscans/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index e7ffd65f3..000000000 Binary files a/src/en/reaperscans/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/reaperscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/reaperscans/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d79fd54e9..000000000 Binary files a/src/en/reaperscans/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/reaperscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/reaperscans/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 779d9aea3..000000000 Binary files a/src/en/reaperscans/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 08a0864d9..000000000 Binary files a/src/en/reaperscans/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/reaperscans/res/web_hi_res_512.png b/src/en/reaperscans/res/web_hi_res_512.png deleted file mode 100644 index 1c6f8c99c..000000000 Binary files a/src/en/reaperscans/res/web_hi_res_512.png and /dev/null differ diff --git a/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScans.kt b/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScans.kt deleted file mode 100644 index fce0d3b88..000000000 --- a/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScans.kt +++ /dev/null @@ -1,411 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.reaperscans - -import android.util.Base64 -import android.util.Log -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.interceptor.rateLimit -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.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.contentOrNull -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray -import kotlinx.serialization.json.putJsonObject -import okhttp3.Headers -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -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 rx.Observable -import uy.kohesive.injekt.injectLazy -import java.util.Calendar -import java.util.concurrent.TimeUnit -import kotlin.random.Random - -class ReaperScans : ParsedHttpSource() { - - override val name = "Reaper Scans" - - override val baseUrl = "https://reaperscans.com" - - override val lang = "en" - - override val id = 5177220001642863679 - - override val supportsLatest = true - - private val json: Json by injectLazy() - - override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .rateLimit(1, 2, TimeUnit.SECONDS) - .build() - - // 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("div > a[href*=/comics/]:nth-child(2)").let { - title = it.text() - setUrlWithoutDomain(it.attr("href")) - } - thumbnail_url = element.select("img").first()?.imgAttr() - } - } - - // Latest - override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/latest/comics?page=$page", headers) - - override fun latestUpdatesNextPageSelector(): String = "button[wire:click*=nextPage]" - - override fun latestUpdatesSelector(): String = "div > p > a[href*=/comics/]" - - override fun latestUpdatesFromElement(element: Element): SManga { - return SManga.create().apply { - element.let { - title = it.text().trim() - setUrlWithoutDomain(it.attr("href")) - } - thumbnail_url = element.parent()?.parent()?.parent()?.parent()?.select("img")?.first()?.imgAttr() - } - } - - // Search - 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 livewareData = soup.selectFirst("div[wire:initial-data*=comics]") - ?.attr("wire:initial-data") - ?.parseJson() - - if (csrfToken == null) error("Couldn't find csrf-token") - if (livewareData == null) error("Couldn't find LiveWireData") - - val routeName = livewareData.fingerprint["name"]?.jsonPrimitive?.contentOrNull - ?: error("Couldn't find routeName") - - // Javascript: (Math.random() + 1).toString(36).substring(8) - val generateId = { "1.${Random.nextLong().toString(36)}".substring(10) } // Not exactly the same, but results in a 3-5 character string - val payload = buildJsonObject { - put("fingerprint", livewareData.fingerprint) - put("serverMemo", livewareData.serverMemo) - putJsonArray("updates") { - addJsonObject { - put("type", "syncInput") - putJsonObject("payload") { - put("id", generateId()) - put("name", "query") - put("value", query) - } - } - } - }.toString().toRequestBody(JSON_MEDIA_TYPE) - - val headers = Headers.Builder() - .add("x-csrf-token", csrfToken) - .add("x-livewire", "true") - .build() - - return POST("$baseUrl/livewire/message/$routeName", headers, payload) - } - - override fun searchMangaSelector(): String = "a[href*=/comics/]" - - override fun searchMangaParse(response: Response): MangasPage { - val html = response.parseJson().effects.html - val mangas = Jsoup.parse(html, baseUrl).select(searchMangaSelector()).map { element -> - searchMangaFromElement(element) - } - return MangasPage(mangas, false) - } - - override fun searchMangaFromElement(element: Element): SManga { - return SManga.create().apply { - setUrlWithoutDomain(element.attr("href")) - element.select("img").first()?.let { - thumbnail_url = it.imgAttr() - } - title = element.select("p").first()!!.text() - } - } - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - if (query.startsWith(PREFIX_ID_SEARCH)) { - val realUrl = "/comics/" + query.removePrefix(PREFIX_ID_SEARCH) - val manga = SManga.create().apply { - url = realUrl - } - return fetchMangaDetails(manga).map { - MangasPage(listOf(it.apply { url = realUrl }), false) - } - } - return super.fetchSearchManga(page, query, filters) - } - - // Details - override fun mangaDetailsParse(document: Document): SManga { - return SManga.create().apply { - thumbnail_url = document.select("div > img").first()!!.imgAttr() - title = document.select("h1").first()!!.text() - - status = when (document.select("dt:contains(Release Status)").next().first()!!.text()) { - "On hold" -> SManga.ON_HIATUS - "Complete" -> SManga.COMPLETED - "Ongoing" -> SManga.ONGOING - "Dropped" -> SManga.CANCELLED - else -> SManga.UNKNOWN - } - - genre = mutableListOf().apply { - when (document.select("dt:contains(Source Language)").next().first()!!.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] > div > ul[role=list] > li" - - override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - val chapters = mutableListOf() - document.select(chapterListSelector()).forEach { chapters.add(chapterFromElement(it)) } - var hasNextPage = document.selectFirst(chapterListNextPageSelector()) != null - - if (!hasNextPage) { - return chapters - } - - val csrfToken = document.selectFirst("meta[name=csrf-token]")?.attr("content") - ?: error("Couldn't find csrf-token") - - val livewareData = document.selectFirst("div[wire:initial-data*=Models\\\\Comic]") - ?.attr("wire:initial-data") - ?.parseJson() - ?: error("Couldn't find LiveWireData") - - val routeName = livewareData.fingerprint["name"]?.jsonPrimitive?.contentOrNull - ?: error("Couldn't find routeName") - - val fingerprint = livewareData.fingerprint - var serverMemo = livewareData.serverMemo - - var pageToQuery = 2 - - // Javascript: (Math.random() + 1).toString(36).substring(8) - val generateId = { "1.${Random.nextLong().toString(36)}".substring(10) } // Not exactly the same, but results in a 3-5 character string - while (hasNextPage) { - val payload = buildJsonObject { - put("fingerprint", fingerprint) - put("serverMemo", serverMemo) - putJsonArray("updates") { - addJsonObject { - put("type", "callMethod") - putJsonObject("payload") { - put("id", generateId()) - 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/$routeName", headers, payload) - - val responseData = client.newCall(request).execute().parseJson() - - // response contains state that we need to preserve - serverMemo = serverMemo.mergeLeft(responseData.serverMemo) - val chaptersHtml = Jsoup.parse(responseData.effects.html, baseUrl) - chaptersHtml.select(chapterListSelector()).forEach { chapters.add(chapterFromElement(it)) } - hasNextPage = chaptersHtml.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 { - val csrfToken = document.selectFirst("meta[name=csrf-token]")?.attr("content") - - val livewareData = document.selectFirst("div[wire:initial-data*=display-chapter]") - ?.attr("wire:initial-data") - ?.parseJson() - - if (csrfToken == null) error("Couldn't find csrf-token") - if (livewareData == null) error("Couldn't find LiveWireData") - - val routeName = livewareData.fingerprint["name"]?.jsonPrimitive?.contentOrNull - ?: error("Couldn't find routeName") - - val tunstileName = document.selectFirst("script:containsData(captchacallback)")?.html() - ?.let { tunstile.find(it)?.groupValues?.get(1) } - ?: error("Couldn't fine Tunstile Name") - - // Javascript: (Math.random() + 1).toString(36).substring(8) - val generateId = { "1.${Random.nextLong().toString(36)}".substring(10) } // Not exactly the same, but results in a 3-5 character string - val payload = buildJsonObject { - put("fingerprint", livewareData.fingerprint) - put("serverMemo", livewareData.serverMemo) - putJsonArray("updates") { - addJsonObject { - put("type", "callMethod") - putJsonObject("payload") { - put("id", generateId()) - put("method", "${"$"}set") - putJsonArray("params") { - add(tunstileName) - add(randomString()) - } - } - } - } - }.toString().toRequestBody(JSON_MEDIA_TYPE) - - val headers = Headers.Builder() - .add("x-csrf-token", csrfToken) - .add("x-livewire", "true") - .build() - - val liveWireRequest = POST("$baseUrl/livewire/message/$routeName", headers, payload) - - val liveWireResponse = client.newCall(liveWireRequest).execute() - - val html = runCatching { liveWireResponse.parseJson().effects.html } - .getOrElse { - Log.e(name, it.stackTraceToString()) - error("Fuck you Reaper Scans") - } - - return Jsoup.parse(html, baseUrl).select("img").mapIndexed { idx, element -> - Page(idx, imageUrl = element.imgAttr()) - } - } - - private fun randomString(): String { - val bytes_288 = Random.nextBytes(ByteArray(288)) - val base64_288 = Base64.encodeToString(bytes_288, Base64.DEFAULT) - - val bytes_16 = Random.nextBytes(ByteArray(16)) - val base64_16 = Base64.encodeToString(bytes_16, Base64.DEFAULT) - - val bytes_32 = Random.nextBytes(ByteArray(32)) - val hex32 = bytes_32.joinToString("") { "%02x".format(it) } - - return "0.$base64_288.$base64_16.$hex32" - } - - // Helpers - private inline fun Response.parseJson(): T = use { - it.body.string().parseJson() - } - - private inline fun 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 { - 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 - } - } - - private fun Element.imgAttr(): String = when { - hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") - hasAttr("data-src") -> attr("abs:data-src") - hasAttr("data-cfsrc") -> attr("abs:data-cfsrc") - else -> attr("abs:src") - } - - // 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() - const val PREFIX_ID_SEARCH = "id:" - private val tunstile by lazy { Regex("""set\s*\(\s*\"([^"]*)""") } - } -} diff --git a/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansDto.kt b/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansDto.kt deleted file mode 100644 index 62246fb66..000000000 --- a/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansDto.kt +++ /dev/null @@ -1,21 +0,0 @@ -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, -) diff --git a/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansUrlActivity.kt b/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansUrlActivity.kt deleted file mode 100644 index 6442e6411..000000000 --- a/src/en/reaperscans/src/eu/kanade/tachiyomi/extension/en/reaperscans/ReaperScansUrlActivity.kt +++ /dev/null @@ -1,34 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.reaperscans - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -class ReaperScansUrlActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size >= 2) { - val id = pathSegments[1] - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", ReaperScans.PREFIX_ID_SEARCH + id) - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e("ReaperScansUrlActivity", e.toString()) - } - } else { - Log.e("ReaperScansUrlActivity", "could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -}