Remove two dead Brazilian sources (#18754)
Remove two dead Brazilian sources.
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -1,17 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Golden Mangás'
|
||||
pkgNameSuffix = 'pt.goldenmangas'
|
||||
extClass = '.GoldenMangas'
|
||||
extVersionCode = 22
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib-randomua"))
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 72 KiB |
|
@ -1,333 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.goldenmangas
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.lib.randomua.PREF_KEY_CUSTOM_UA
|
||||
import eu.kanade.tachiyomi.lib.randomua.PREF_KEY_RANDOM_UA
|
||||
import eu.kanade.tachiyomi.lib.randomua.RANDOM_UA_ENTRIES
|
||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
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 java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class GoldenMangas : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
// Hardcode the id because the language wasn't specific.
|
||||
override val id: Long = 6858719406079923084
|
||||
|
||||
override val name = "Golden Mangás"
|
||||
|
||||
override val baseUrl = "https://www.goldenmanga.top"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val baseHttpUrl: HttpUrl
|
||||
get() = preferences.baseUrl.toHttpUrl()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.addInterceptor(ObsoleteExtensionInterceptor())
|
||||
.setRandomUserAgent(
|
||||
userAgentType = preferences.getPrefUAType(),
|
||||
customUA = preferences.getPrefCustomUA(),
|
||||
)
|
||||
.rateLimitPath("/mangas", 1, 8.seconds)
|
||||
.rateLimitPath("/mm-admin/uploads", 1, 8.seconds)
|
||||
.rateLimitPath("/timthumb.php", 1, 3.seconds)
|
||||
.rateLimitPath("/index.php", 1, 3.seconds)
|
||||
.addInterceptor(::guessNewUrlIntercept)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Accept", ACCEPT)
|
||||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||
.add("Referer", REFERER)
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "div#maisLidos div.itemmanga:not(:contains(Avisos e Recados))"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.selectFirst("h3")!!.text().withoutLanguage()
|
||||
thumbnail_url = element.selectFirst("img")!!.absUrl("src")
|
||||
url = element.attr("href")
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val path = if (page > 1) "/index.php?pagina=$page" else ""
|
||||
return GET("$baseUrl$path", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div.col-sm-12.atualizacao > div.row:not(:contains(Avisos e Recados))"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
|
||||
val infoElement = element.selectFirst("div.col-sm-10.col-xs-8 h3")!!
|
||||
val thumbElement = element.selectFirst("a:first-child div img")!!
|
||||
|
||||
title = infoElement.text().withoutLanguage()
|
||||
thumbnail_url = thumbElement.absUrl("src")
|
||||
.replace("w=100&h=140", "w=380&h=600")
|
||||
url = element.select("a:first-child").attr("href")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "ul.pagination li:last-child a"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.set("Referer", "$baseUrl/mangas")
|
||||
.build()
|
||||
|
||||
val url = "$baseUrl/mangas".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("busca", query)
|
||||
.toString()
|
||||
|
||||
return GET(url, newHeaders)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.mangas.col-lg-2 a"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.selectFirst("h3")!!.text().withoutLanguage()
|
||||
thumbnail_url = element.selectFirst("img")!!.absUrl("src")
|
||||
url = element.attr("href")
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val mangaResult = runCatching { super.mangaDetailsParse(response) }
|
||||
val manga = mangaResult.getOrNull()
|
||||
|
||||
if (manga?.title.isNullOrEmpty() && !response.hasChangedDomain) {
|
||||
throw Exception(MIGRATE_WARNING)
|
||||
}
|
||||
|
||||
return manga!!
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
val infoElement = document.selectFirst("div.row > div.col-sm-8 > div.row")!!
|
||||
val firstColumn = infoElement.selectFirst("div.col-sm-4.text-right > img")!!
|
||||
val secondColumn = infoElement.selectFirst("div.col-sm-8")!!
|
||||
|
||||
title = secondColumn.select("h2:eq(0)").text().withoutLanguage()
|
||||
author = secondColumn.select("h5:contains(Autor)").text().withoutLabel()
|
||||
artist = secondColumn.select("h5:contains(Artista)").text().withoutLabel()
|
||||
genre = secondColumn.select("h5:contains(Genero) a").toList()
|
||||
.filter { it.text().isNotEmpty() }
|
||||
.joinToString { it.text() }
|
||||
status = secondColumn.select("h5:contains(Status) a").text().toStatus()
|
||||
description = document.select("#manga_capitulo_descricao").text()
|
||||
thumbnail_url = firstColumn.attr("abs:src")
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chaptersResult = runCatching { super.chapterListParse(response) }
|
||||
val chapterList = chaptersResult.getOrNull()
|
||||
|
||||
if (chapterList.isNullOrEmpty() && !response.hasChangedDomain) {
|
||||
throw Exception(MIGRATE_WARNING)
|
||||
}
|
||||
|
||||
return chapterList.orEmpty()
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul#capitulos li.row"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
val firstColumn = element.selectFirst("a > div.col-sm-5")!!
|
||||
val secondColumn = element.select("div.col-sm-5.text-right a:not([href^='/'])")
|
||||
|
||||
name = firstColumn.text().substringBefore("(").trim()
|
||||
scanlator = secondColumn.joinToString { it.text() }
|
||||
date_upload = firstColumn.select("div.col-sm-5 span[style]").text().toDate()
|
||||
url = element.select("a").attr("href")
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", baseUrl + chapter.url.substringBeforeLast("/"))
|
||||
.build()
|
||||
|
||||
return GET(baseUrl + chapter.url, newHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pagesResult = runCatching { super.pageListParse(response) }
|
||||
val pageList = pagesResult.getOrNull()
|
||||
|
||||
if (pageList.isNullOrEmpty() && !response.hasChangedDomain) {
|
||||
throw Exception(MIGRATE_WARNING)
|
||||
}
|
||||
|
||||
return pageList.orEmpty()
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val chapterImages = document.selectFirst("div.col-sm-12[id^='capitulos_images']:has(img[pag])")
|
||||
|
||||
val isNovel = document.selectFirst(".block_text_border") !== null
|
||||
|
||||
if (chapterImages == null && isNovel) {
|
||||
throw Exception(CHAPTER_IS_NOVEL_ERROR)
|
||||
}
|
||||
|
||||
return chapterImages?.select("img[pag]")
|
||||
.orEmpty()
|
||||
.mapIndexed { i, element ->
|
||||
Page(i, document.location(), element.attr("abs:src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Accept", ACCEPT_IMAGE)
|
||||
.set("Referer", page.url)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val uaPreference = ListPreference(screen.context).apply {
|
||||
key = PREF_KEY_RANDOM_UA
|
||||
title = "User Agent aleatório"
|
||||
summary = "%s"
|
||||
entries = arrayOf("Desativado", "Desktop", "Celular")
|
||||
entryValues = RANDOM_UA_ENTRIES
|
||||
setDefaultValue("off")
|
||||
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val customUaPreference = EditTextPreference(screen.context).apply {
|
||||
key = PREF_KEY_CUSTOM_UA
|
||||
title = "User Agent personalizado"
|
||||
summary = "Deixe em branco para usar o User Agent padrão do aplicativo. " +
|
||||
"Ignorado se User Agent aleatório está ativado."
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
Headers.Builder().add("User-Agent", newValue as String).build()
|
||||
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Toast.makeText(screen.context, "User Agent inválido: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(uaPreference)
|
||||
screen.addPreference(customUaPreference)
|
||||
}
|
||||
|
||||
private fun guessNewUrlIntercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
if (chain.request().url.host == "raw.githubusercontent.com") {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response.hasChangedDomain && preferences.baseUrl == baseUrl) {
|
||||
return response
|
||||
}
|
||||
|
||||
preferences.baseUrl = "https://${response.request.url.host}"
|
||||
|
||||
val newUrl = chain.request().url.toString()
|
||||
.replaceFirst(baseUrl, preferences.baseUrl)
|
||||
.toHttpUrl()
|
||||
val newRequest = chain.request().newBuilder()
|
||||
.url(newUrl)
|
||||
.build()
|
||||
|
||||
response.close()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
private var SharedPreferences.baseUrl: String
|
||||
get() = getString(BASE_URL_PREF, this@GoldenMangas.baseUrl)!!
|
||||
set(newValue) = edit().putString(BASE_URL_PREF, newValue).apply()
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(trim())?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
private val Response.hasChangedDomain: Boolean
|
||||
get() = request.url.host != baseHttpUrl.host &&
|
||||
request.url.host.contains("goldenmang")
|
||||
|
||||
private fun String.toStatus() = when (this) {
|
||||
"Ativo" -> SManga.ONGOING
|
||||
"Completo" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun String.withoutLabel(): String = substringAfter(":").trim()
|
||||
|
||||
private fun String.withoutLanguage(): String = replace(FLAG_REGEX, "").trim()
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," +
|
||||
"image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
private const val ACCEPT_IMAGE = "image/webp,image/apng,image/*,*/*;q=0.8"
|
||||
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5"
|
||||
private const val REFERER = "https://google.com/"
|
||||
|
||||
private val FLAG_REGEX = "\\((Pt[-/]br|Scan)\\)".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private const val CHAPTER_IS_NOVEL_ERROR =
|
||||
"O capítulo é uma novel em formato de texto e não possui imagens."
|
||||
|
||||
private const val MIGRATE_WARNING = "Migre o item da Golden Mangás para Golden Mangás para atualizar a URL."
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("(dd/MM/yyyy)", Locale("pt", "BR"))
|
||||
}
|
||||
|
||||
private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações."
|
||||
private const val BASE_URL_PREF = "base_url"
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.goldenmangas
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
class ObsoleteExtensionInterceptor : Interceptor {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private var isObsolete: Boolean? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (isObsolete == null) {
|
||||
val extRepoResponse = chain.proceed(GET(REPO_URL))
|
||||
val extRepo = json.decodeFromString<List<ExtensionJsonObject>>(extRepoResponse.body.string())
|
||||
|
||||
isObsolete = !extRepo.any { ext ->
|
||||
ext.pkg == this.javaClass.`package`?.name && ext.lang == "pt-BR"
|
||||
}
|
||||
}
|
||||
|
||||
if (isObsolete == true) {
|
||||
throw IOException("Extensão obsoleta. Desinstale e migre para outras fontes.")
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
val pkg: String,
|
||||
val lang: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val REPO_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/index.min.json"
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.goldenmangas
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.Semaphore
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
class SpecificPathRateLimitInterceptor(
|
||||
private val path: String,
|
||||
private val permits: Int,
|
||||
period: Duration,
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayDeque<Long>(permits)
|
||||
private val rateLimitMillis = period.inWholeMilliseconds
|
||||
private val fairLock = Semaphore(1, true)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val call = chain.call()
|
||||
if (call.isCanceled()) throw IOException("Canceled")
|
||||
|
||||
val request = chain.request()
|
||||
|
||||
if (!request.url.encodedPath.startsWith(path)) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
try {
|
||||
fairLock.acquire()
|
||||
} catch (e: InterruptedException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
|
||||
val requestQueue = this.requestQueue
|
||||
val timestamp: Long
|
||||
|
||||
try {
|
||||
synchronized(requestQueue) {
|
||||
while (requestQueue.size >= permits) { // queue is full, remove expired entries
|
||||
val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis
|
||||
var hasRemovedExpired = false
|
||||
while (requestQueue.isEmpty().not() && requestQueue.first <= periodStart) {
|
||||
requestQueue.removeFirst()
|
||||
hasRemovedExpired = true
|
||||
}
|
||||
if (call.isCanceled()) {
|
||||
throw IOException("Canceled")
|
||||
} else if (hasRemovedExpired) {
|
||||
break
|
||||
} else {
|
||||
try { // wait for the first entry to expire, or notified by cached response
|
||||
(requestQueue as Object).wait(requestQueue.first - periodStart)
|
||||
} catch (_: InterruptedException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add request to queue
|
||||
timestamp = SystemClock.elapsedRealtime()
|
||||
requestQueue.addLast(timestamp)
|
||||
}
|
||||
} finally {
|
||||
fairLock.release()
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
if (response.networkResponse == null) { // response is cached, remove it from queue
|
||||
synchronized(requestQueue) {
|
||||
if (requestQueue.isEmpty() || timestamp < requestQueue.first) return@synchronized
|
||||
requestQueue.removeFirstOccurrence(timestamp)
|
||||
(requestQueue as Object).notifyAll()
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.rateLimitPath(
|
||||
path: String,
|
||||
permits: Int,
|
||||
period: Duration = 1.seconds,
|
||||
) = addInterceptor(SpecificPathRateLimitInterceptor(path, permits, period))
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -1,13 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Mundo Webtoon'
|
||||
pkgNameSuffix = 'pt.mundowebtoon'
|
||||
extClass = '.MundoWebtoon'
|
||||
extVersionCode = 8
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 55 KiB |
|
@ -1,218 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.mundowebtoon
|
||||
|
||||
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.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MundoWebtoon : ParsedHttpSource() {
|
||||
|
||||
override val name = "Mundo Webtoon"
|
||||
|
||||
override val baseUrl = "https://mundowebtoon.com"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(ObsoleteExtensionInterceptor())
|
||||
.addInterceptor(::sanitizeHtmlIntercept)
|
||||
.rateLimit(1, 3, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Accept", ACCEPT)
|
||||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaSelector(): String =
|
||||
"div.section:contains(mais lídos) + div.section div.andro_product"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("h6.andro_product-title small").text().withoutLanguage()
|
||||
thumbnail_url = element.select("div.andro_product-thumb img").srcAttr()
|
||||
setUrlWithoutDomain(element.select("div.andro_product-thumb > a").attr("abs:href"))
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val path = if (page > 1) "/index.php?pagina=$page" else ""
|
||||
return GET("$baseUrl$path", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div.row.atualizacoes div.andro_product"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("h5.andro_product-title").text().withoutLanguage()
|
||||
thumbnail_url = element.select("div.andro_product-thumb img").srcAttr()
|
||||
setUrlWithoutDomain(element.select("div.andro_product-thumb > a").attr("abs:href"))
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "ul.paginacao li:last-child:not(.active) a"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.set("Referer", "$baseUrl/mangas")
|
||||
.build()
|
||||
|
||||
val url = "$baseUrl/mangas".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("busca", query)
|
||||
.toString()
|
||||
|
||||
return GET(url, newHeaders)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.container div.andro_product"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
title = element.select("span.andro_product-title").text().withoutLanguage()
|
||||
thumbnail_url = element.select("div.andro_product-thumb img").srcAttr()
|
||||
setUrlWithoutDomain(element.select("div.andro_product-thumb > a").attr("abs:href"))
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
val infoElement = document.selectFirst("div.andro_product-single-content")!!
|
||||
|
||||
title = infoElement.select("div.mangaTitulo h3").text().withoutLanguage()
|
||||
author = infoElement.select("div.BlDataItem a[href*=autor]")
|
||||
.joinToString(", ") { it.text() }
|
||||
artist = infoElement.select("div.BlDataItem a[href*=artista]")
|
||||
.joinToString(", ") { it.text() }
|
||||
genre = infoElement.select("div.col-md-12 a.label-warning[href*=genero]").toList()
|
||||
.filter { it.text().isNotEmpty() }
|
||||
.joinToString { it.text().trim() }
|
||||
status = infoElement.selectFirst("div.BlDataItem a[href*=status]")
|
||||
?.text()?.toStatus() ?: SManga.UNKNOWN
|
||||
description = infoElement.select("div.andro_product-excerpt").text()
|
||||
thumbnail_url = document.select("div.andro_product-single-thumb img").srcAttr()
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.CapitulosListaTodos div.CapitulosListaItem"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
name = element.selectFirst("h5")!!.ownText()
|
||||
scanlator = element.select("a.color_gray[target='_blank']")
|
||||
.joinToString(", ") { it.text() }
|
||||
date_upload = element.select("h5 span[style]").text().toDate()
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterUrl = (baseUrl + chapter.url).toHttpUrl()
|
||||
|
||||
val payload = FormBody.Builder()
|
||||
.add("data", chapterUrl.pathSegments[1])
|
||||
.add("num", chapterUrl.pathSegments[2])
|
||||
.add("modo", "1")
|
||||
.add("busca", "img")
|
||||
.build()
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.add("Content-Length", payload.contentLength().toString())
|
||||
.add("Content-Type", payload.contentType().toString())
|
||||
.set("Referer", baseUrl + chapter.url)
|
||||
.build()
|
||||
|
||||
return POST("$baseUrl/leitor_image.php", newHeaders, payload)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("img[pag]")
|
||||
.mapIndexed { i, element ->
|
||||
Page(i, document.location(), element.attr("abs:src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Accept", ACCEPT_IMAGE)
|
||||
.set("Referer", page.url)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
private fun sanitizeHtmlIntercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
if (!response.headers["Content-Type"].orEmpty().contains("text/html")) {
|
||||
return response
|
||||
}
|
||||
|
||||
val newBody = response.body.string()
|
||||
.replace("\t", "")
|
||||
.replace(SCRIPT_REGEX, "")
|
||||
.replace(HEAD_REGEX, "<head></head>")
|
||||
.replace(COMMENT_REGEX, "")
|
||||
.toResponseBody(HTML_MEDIA_TYPE)
|
||||
|
||||
response.close()
|
||||
|
||||
return response.newBuilder()
|
||||
.body(newBody)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun Elements.srcAttr(): String =
|
||||
attr(if (hasAttr("data-src")) "data-src" else "src")
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(trim())?. time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
private fun String.toStatus() = when (this) {
|
||||
"Ativo" -> SManga.ONGOING
|
||||
"Completo" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun String.withoutLanguage(): String = replace(FLAG_REGEX, "").trim()
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," +
|
||||
"image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
private const val ACCEPT_IMAGE = "image/webp,image/apng,image/*,*/*;q=0.8"
|
||||
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5"
|
||||
|
||||
private val FLAG_REGEX = "\\((Pt[-/]br|Scan)\\)".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val SCRIPT_REGEX = "<script>.*</script>"
|
||||
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
||||
private val HEAD_REGEX = "<head>.*</head>"
|
||||
.toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
||||
private val COMMENT_REGEX = "<!--.*-->".toRegex(RegexOption.MULTILINE)
|
||||
|
||||
private val HTML_MEDIA_TYPE = "text/html".toMediaType()
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("(dd/MM/yyyy)", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.mundowebtoon
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
class ObsoleteExtensionInterceptor : Interceptor {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private var isObsolete: Boolean? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (isObsolete == null) {
|
||||
val extRepoResponse = chain.proceed(GET(REPO_URL))
|
||||
val extRepo = json.decodeFromString<List<ExtensionJsonObject>>(extRepoResponse.body.string())
|
||||
|
||||
isObsolete = !extRepo.any { ext ->
|
||||
ext.pkg == this.javaClass.`package`?.name && ext.lang == "pt-BR"
|
||||
}
|
||||
}
|
||||
|
||||
if (isObsolete == true) {
|
||||
throw IOException("Extensão obsoleta. Desinstale e migre para outras fontes.")
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
val pkg: String,
|
||||
val lang: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val REPO_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/index.min.json"
|
||||
}
|
||||
}
|