diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml index 2e9f8b049..44dea99a7 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|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, "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." diff --git a/REMOVED_SOURCES.md b/REMOVED_SOURCES.md index cbd0199c9..731995e63 100644 --- a/REMOVED_SOURCES.md +++ b/REMOVED_SOURCES.md @@ -19,6 +19,7 @@ Here is a list of known sources that were removed. - Hentai Kai https://github.com/tachiyomiorg/tachiyomi-extensions/issues/9999 - Hitomi.la https://github.com/tachiyomiorg/tachiyomi-extensions/pull/11613 - 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 - LeerCapitulo https://github.com/tachiyomiorg/tachiyomi-extensions/pull/16255 - Mangá Host https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065 diff --git a/src/fr/japscan/AndroidManifest.xml b/src/fr/japscan/AndroidManifest.xml deleted file mode 100644 index 8072ee00d..000000000 --- a/src/fr/japscan/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/fr/japscan/build.gradle b/src/fr/japscan/build.gradle deleted file mode 100644 index 57fdc6215..000000000 --- a/src/fr/japscan/build.gradle +++ /dev/null @@ -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" diff --git a/src/fr/japscan/res/mipmap-hdpi/ic_launcher.png b/src/fr/japscan/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 4659fbb8c..000000000 Binary files a/src/fr/japscan/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/fr/japscan/res/mipmap-mdpi/ic_launcher.png b/src/fr/japscan/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 19a5531b0..000000000 Binary files a/src/fr/japscan/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/fr/japscan/res/mipmap-xhdpi/ic_launcher.png b/src/fr/japscan/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 7320f09eb..000000000 Binary files a/src/fr/japscan/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/fr/japscan/res/mipmap-xxhdpi/ic_launcher.png b/src/fr/japscan/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 52e248fa6..000000000 Binary files a/src/fr/japscan/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/fr/japscan/res/mipmap-xxxhdpi/ic_launcher.png b/src/fr/japscan/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 166cd3cfe..000000000 Binary files a/src/fr/japscan/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/fr/japscan/res/web_hi_res_512.png b/src/fr/japscan/res/web_hi_res_512.png deleted file mode 100644 index 3af8c2771..000000000 Binary files a/src/fr/japscan/res/web_hi_res_512.png and /dev/null differ diff --git a/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt b/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt deleted file mode 100644 index 50ab54f2d..000000000 --- a/src/fr/japscan/src/eu/kanade/tachiyomi/extension/fr/japscan/Japscan.kt +++ /dev/null @@ -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().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 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray().toList() - - override fun pageListParse(document: Document): List { - 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) : Filter.Select("Page #", arrayOf(0, *pages)) - - override fun getFilterList(): FilterList { - val totalPages = pageNumberDoc?.select("li.page-item:last-child a")?.text() - val pagelist = mutableListOf() - 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) - } -}