Remove Japscan due to cat and mouse game (#17892)

* Remove Japscan due to cat and mouse game

* [skip ci] Add to REMOVED_SOURCED.md
This commit is contained in:
KirinRaikage 2023-09-10 23:05:12 +02:00 committed by GitHub
parent 890e924074
commit f0c7a740ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2 additions and 381 deletions

View File

@ -43,7 +43,7 @@ jobs:
}, },
{ {
"type": "both", "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|day\\s*comics?|reaper\\s*scans|constellar\\s*scans|mode\\s*scanlator|bakai).*", "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|day\\s*comics?|reaper\\s*scans|constellar\\s*scans|mode\\s*scanlator|bakai|japscan).*",
"ignoreCase": true, "ignoreCase": true,
"labels": ["invalid"], "labels": ["invalid"],
"message": "{match} will not be added back as it is too difficult to maintain. Read [this](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/REMOVED_SOURCES.md) for more information." "message": "{match} will not be added back as it is too difficult to maintain. Read [this](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/REMOVED_SOURCES.md) for more information."

View File

@ -19,6 +19,7 @@ Here is a list of known sources that were removed.
- Hentai Kai https://github.com/tachiyomiorg/tachiyomi-extensions/issues/9999 - Hentai Kai https://github.com/tachiyomiorg/tachiyomi-extensions/issues/9999
- Hitomi.la https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11613 - Hitomi.la https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11613
- HQ Dragon https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065 - HQ Dragon https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065
- Japscan https://github.com/tachiyomiorg/tachiyomi-extensions/pull/17892
- Koushoku https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13329 - Koushoku https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13329
- LeerCapitulo https://github.com/tachiyomiorg/tachiyomi-extensions/pull/16255 - LeerCapitulo https://github.com/tachiyomiorg/tachiyomi-extensions/pull/16255
- Mangá Host https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065 - Mangá Host https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -1,16 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Japscan'
pkgNameSuffix = 'fr.japscan'
extClass = '.Japscan'
extVersionCode = 43
}
dependencies {
implementation(project(":lib-synchrony"))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,362 +0,0 @@
package eu.kanade.tachiyomi.extension.fr.japscan
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import android.util.Base64
import android.util.Log
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
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.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class Japscan : ConfigurableSource, ParsedHttpSource() {
override val id: Long = 11
override val name = "Japscan"
override val baseUrl = "https://www.japscan.lol"
override val lang = "fr"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 2)
.build()
companion object {
val dateFormat by lazy {
SimpleDateFormat("dd MMM yyyy", Locale.US)
}
private const val SHOW_SPOILER_CHAPTERS_Title = "Les chapitres en Anglais ou non traduit sont upload en tant que \" Spoilers \" sur Japscan"
private const val SHOW_SPOILER_CHAPTERS = "JAPSCAN_SPOILER_CHAPTERS"
private val prefsEntries = arrayOf("Montrer uniquement les chapitres traduit en Français", "Montrer les chapitres spoiler")
private val prefsEntryValues = arrayOf("hide", "show")
}
private fun chapterListPref() = preferences.getString(SHOW_SPOILER_CHAPTERS, "hide")
override fun headersBuilder() = super.headersBuilder()
.add("referer", "$baseUrl/")
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/mangas/", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
pageNumberDoc = document
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
val hasNextPage = false
return MangasPage(mangas, hasNextPage)
}
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaSelector() = "#top_mangas_week li"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a").first()!!.let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
manga.thumbnail_url = "$baseUrl/imgs/${it.attr("href").replace(Regex("/$"),".jpg").replace("manga","mangas")}".lowercase(Locale.ROOT)
}
return manga
}
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector())
.distinctBy { element -> element.select("a").attr("href") }
.map { element ->
latestUpdatesFromElement(element)
}
val hasNextPage = false
return MangasPage(mangas, hasNextPage)
}
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesSelector() = "#chapters h3.text-truncate, #chapters_list h3.text-truncate"
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isEmpty()) {
val uri = Uri.parse(baseUrl).buildUpon()
.appendPath("mangas")
filters.forEach { filter ->
when (filter) {
is TextField -> uri.appendPath(((page - 1) + filter.state.toInt()).toString())
is PageList -> uri.appendPath(((page - 1) + filter.values[filter.state]).toString())
else -> {}
}
}
return GET(uri.toString(), headers)
} else {
val formBody = FormBody.Builder()
.add("search", query)
.build()
val searchHeaders = headers.newBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
try {
val searchRequest = POST("$baseUrl/live-search/", searchHeaders, formBody)
val searchResponse = client.newCall(searchRequest).execute()
if (!searchResponse.isSuccessful) {
throw Exception("Code ${searchResponse.code} inattendu")
}
val jsonResult = json.parseToJsonElement(searchResponse.body.string()).jsonArray
if (jsonResult.isEmpty()) {
Log.d("japscan", "Search not returning anything, using duckduckgo")
throw Exception("Pas de données")
}
return searchRequest
} catch (e: Exception) {
// Fallback to duckduckgo if the search does not return any result
val uri = Uri.parse("https://duckduckgo.com/lite/").buildUpon()
.appendQueryParameter("q", "$query site:$baseUrl/manga/")
.appendQueryParameter("kd", "-1")
return GET(uri.toString(), headers)
}
}
}
override fun searchMangaNextPageSelector(): String = "li.page-item:last-child:not(li.active),.next_form .navbutton"
override fun searchMangaSelector(): String = "div.card div.p-2, a.result-link"
override fun searchMangaParse(response: Response): MangasPage {
if ("live-search" in response.request.url.toString()) {
val jsonResult = json.parseToJsonElement(response.body.string()).jsonArray
val mangaList = jsonResult.map { jsonEl -> searchMangaFromJson(jsonEl.jsonObject) }
return MangasPage(mangaList, hasNextPage = false)
}
return super.searchMangaParse(response)
}
override fun searchMangaFromElement(element: Element): SManga {
return if (element.attr("class") == "result-link") {
SManga.create().apply {
title = element.text().substringAfter(" ").substringBefore(" | JapScan")
setUrlWithoutDomain(element.attr("abs:href"))
}
} else {
SManga.create().apply {
thumbnail_url = element.select("img").attr("abs:src")
element.select("p a").let {
title = it.text()
url = it.attr("href")
}
}
}
}
private fun searchMangaFromJson(jsonObj: JsonObject): SManga = SManga.create().apply {
title = jsonObj["name"]!!.jsonPrimitive.content
url = jsonObj["url"]!!.jsonPrimitive.content
}
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst("#main .card-body")!!
val manga = SManga.create()
manga.thumbnail_url = infoElement.select("img").attr("abs:src")
val infoRows = infoElement.select(".row, .d-flex")
infoRows.select("p").forEach { el ->
when (el.select("span").text().trim()) {
"Auteur(s):" -> manga.author = el.text().replace("Auteur(s):", "").trim()
"Artiste(s):" -> manga.artist = el.text().replace("Artiste(s):", "").trim()
"Genre(s):" -> manga.genre = el.text().replace("Genre(s):", "").trim()
"Statut:" -> manga.status = el.text().replace("Statut:", "").trim().let {
parseStatus(it)
}
}
}
manga.description = infoElement.select("div:contains(Synopsis) + p").text().orEmpty()
return manga
}
private fun parseStatus(status: String) = when {
status.contains("En Cours") -> SManga.ONGOING
status.contains("Terminé") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "#chapters_list > div.collapse > div.chapters_list" +
if (chapterListPref() == "hide") { ":not(:has(.badge:contains(SPOILER),.badge:contains(RAW),.badge:contains(VUS)))" } else { "" }
// JapScan sometimes uploads some "spoiler preview" chapters, containing 2 or 3 untranslated pictures taken from a raw. Sometimes they also upload full RAWs/US versions and replace them with a translation as soon as available.
// Those have a span.badge "SPOILER" or "RAW". The additional pseudo selector makes sure to exclude these from the chapter list.
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.selectFirst("a")!!
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.ownText()
// Using ownText() doesn't include childs' text, like "VUS" or "RAW" badges, in the chapter name.
chapter.date_upload = element.selectFirst("span")!!.text().trim().let { parseChapterDate(it) }
return chapter
}
private fun parseChapterDate(date: String): Long {
return try {
dateFormat.parse(date)?.time ?: 0
} catch (e: ParseException) {
0L
}
}
private val decodingStringsRe: Regex = Regex("""'([\dA-Z]{62})'""", RegexOption.IGNORE_CASE)
private val sortedLookupString: List<Char> = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray().toList()
override fun pageListParse(document: Document): List<Page> {
val zjsurl = document.getElementsByTag("script").first {
it.attr("src").contains("zjs", ignoreCase = true)
}.attr("src")
Log.d("japscan", "ZJS at $zjsurl")
val obfuscatedZjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body.string()
val zjs = Deobfuscator.deobfuscateScript(obfuscatedZjs) ?: throw Exception("Impossible à désobfusquer ZJS")
val stringLookupTables = decodingStringsRe.findAll(zjs).mapNotNull {
it.groupValues[1].takeIf {
it.toCharArray().sorted() == sortedLookupString
}
}.toList()
if (stringLookupTables.size != 2) {
throw Exception("Attendait 2 chaînes de recherche dans ZJS, a trouvé ${stringLookupTables.size}")
}
val scrambledData = document.getElementById("data")!!.attr("data-data")
for (i in 0..1) {
Log.d("japscan", "descramble attempt $i")
val otherIndice = if (i == 0) 1 else 0
val lookupTable = stringLookupTables[i].zip(stringLookupTables[otherIndice]).toMap()
try {
val unscrambledData = scrambledData.map { lookupTable[it] ?: it }.joinToString("")
if (!unscrambledData.startsWith("ey")) {
// `ey` is the Base64 representation of a curly bracket. Since we're expecting a
// JSON object, we're counting this attempt as failed if it doesn't start with a
// curly bracket.
continue
}
val decoded = Base64.decode(unscrambledData, Base64.DEFAULT).toString(Charsets.UTF_8)
val data = json.parseToJsonElement(decoded).jsonObject
return data["imagesLink"]!!.jsonArray.mapIndexed { idx, it ->
Page(idx, imageUrl = it.jsonPrimitive.content)
}
} catch (_: Throwable) {}
}
throw Exception("Les deux tentatives de désembrouillage ont échoué")
}
override fun imageUrlParse(document: Document): String = ""
// Filters
private class TextField(name: String) : Filter.Text(name)
private class PageList(pages: Array<Int>) : Filter.Select<Int>("Page #", arrayOf(0, *pages))
override fun getFilterList(): FilterList {
val totalPages = pageNumberDoc?.select("li.page-item:last-child a")?.text()
val pagelist = mutableListOf<Int>()
return if (!totalPages.isNullOrEmpty()) {
for (i in 0 until totalPages.toInt()) {
pagelist.add(i + 1)
}
FilterList(
Filter.Header("Page alphabétique"),
PageList(pagelist.toTypedArray()),
)
} else {
FilterList(
Filter.Header("Page alphabétique"),
TextField("Page #"),
Filter.Header("Appuyez sur reset pour la liste"),
)
}
}
private var pageNumberDoc: Document? = null
// Prefs
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val chapterListPref = androidx.preference.ListPreference(screen.context).apply {
key = SHOW_SPOILER_CHAPTERS_Title
title = SHOW_SPOILER_CHAPTERS_Title
entries = prefsEntries
entryValues = prefsEntryValues
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(SHOW_SPOILER_CHAPTERS, entry).commit()
}
}
screen.addPreference(chapterListPref)
}
}