Wicked Witch Scan: Add new website (#440)
* Wicked Witch Scan: Add new website * use pt-BR locale for parsing dates * fix: Use the original source name in ID calculation * move new site to individual extension * translate some more stuff * remove import * use parseBodyFragment Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * chop down the forest --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
parent
40fa020e0e
commit
93df09d758
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest />
|
|
@ -0,0 +1,11 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Wicked Witch Scan (Novo)'
|
||||||
|
extClass = '.WickedWitchScan'
|
||||||
|
extVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290")
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,280 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.pt.wickedwitchscannovo
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.Base64
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
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.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
class WickedWitchScan : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val name = "Wicked Witch Scan"
|
||||||
|
|
||||||
|
override val lang = "pt-BR"
|
||||||
|
|
||||||
|
override val baseUrl = "https://wicked-witch-scan.com"
|
||||||
|
|
||||||
|
// Source changed from Madara to homegrown website
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
.newBuilder()
|
||||||
|
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||||
|
.addInterceptor(::zipImageInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val simpleDateFormat by lazy {
|
||||||
|
SimpleDateFormat("d 'de' MMMM 'de' yyyy 'às' HH:mm", Locale("pt", "BR")).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("America/Sao_Paulo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/todas-as-obras/", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = ".comics__all__box"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
val anchor = element.selectFirst(".titulo__comic__allcomics")!!
|
||||||
|
|
||||||
|
setUrlWithoutDomain(anchor.attr("href"))
|
||||||
|
title = anchor.text()
|
||||||
|
thumbnail_url = element.selectFirst(".box-image img")?.attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.comic"
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||||
|
title = element.selectFirst(".titulo__comic")!!.text()
|
||||||
|
thumbnail_url = element.selectFirst(".comic__img")?.attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegments("auto-complete/")
|
||||||
|
addQueryParameter("term", query)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val manga = json.parseToJsonElement(response.body.string()).jsonArray.map {
|
||||||
|
val element = Jsoup.parseBodyFragment(it.jsonObject["html"]!!.jsonPrimitive.content)
|
||||||
|
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(manga, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||||
|
|
||||||
|
title = element.selectFirst("span")!!.text()
|
||||||
|
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String? = null
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.selectFirst(".desc__titulo__comic")!!.text()
|
||||||
|
author = document.selectFirst(".sumario__specs__box:contains(Autor) + .sumario__specs__tipo")?.text()
|
||||||
|
genre = document.select("a[href*=pesquisar?category]").joinToString { it.text() }
|
||||||
|
status = when (document.selectFirst(".sumario__specs__box:contains(Status) + .sumario__specs__tipo")?.text()) {
|
||||||
|
"Em Lançamento" -> SManga.ONGOING
|
||||||
|
"Finalizado" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
thumbnail_url = document.selectFirst(".sumario__img")?.attr("abs:src")
|
||||||
|
|
||||||
|
val category = document.selectFirst(".categoria__comic")?.text()
|
||||||
|
val synopsis = document.selectFirst(".sumario__sinopse__texto")?.text()
|
||||||
|
description = "Tipo: $category\n\n$synopsis"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = ".link__capitulos"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
|
||||||
|
name = element.selectFirst(".numero__capitulo")!!.text()
|
||||||
|
date_upload = runCatching {
|
||||||
|
val date = element.selectFirst(".data__lançamento")!!.text()
|
||||||
|
|
||||||
|
simpleDateFormat.parse(date)?.time
|
||||||
|
}.getOrNull() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val mediaType = document.selectFirst(".categoria__comic")?.text()
|
||||||
|
|
||||||
|
if (mediaType == "Novel") {
|
||||||
|
// Google translated, sorry
|
||||||
|
throw Exception("Novelas não podem ser lidos em Tachiyomi, acesse o site")
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val scriptElement = document.selectFirst("script:containsData(const urls =[)")
|
||||||
|
?: throw Exception("Não foi possível encontrar o script com dados de imagem.")
|
||||||
|
|
||||||
|
val urls = scriptElement.html().substringAfter("const urls =[").substringBefore("];")
|
||||||
|
|
||||||
|
return urls.split(",").mapIndexed { i, it ->
|
||||||
|
Page(i, imageUrl = baseUrl + it.trim().removeSurrounding("'") + "#page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||||
|
|
||||||
|
private fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
val filename = request.url.pathSegments.last()
|
||||||
|
|
||||||
|
if (request.url.fragment != "page" || !filename.contains(".zip")) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
val zis = ZipInputStream(response.body.byteStream())
|
||||||
|
|
||||||
|
val images = generateSequence { zis.nextEntry }
|
||||||
|
.map {
|
||||||
|
val entryName = it.name
|
||||||
|
val splitEntryName = entryName.split('.')
|
||||||
|
val entryIndex = splitEntryName.first().toInt()
|
||||||
|
val entryType = splitEntryName.last()
|
||||||
|
|
||||||
|
val imageData = if (entryType == "avif") {
|
||||||
|
zis.readBytes()
|
||||||
|
} else {
|
||||||
|
val svgBytes = zis.readBytes()
|
||||||
|
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||||
|
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||||
|
?: throw IOException("Não foi possível corresponder a imagem no conteúdo SVG")
|
||||||
|
|
||||||
|
Base64.decode(b64, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
val decoder = ImageDecoder.newInstance(ByteArrayInputStream(imageData))
|
||||||
|
|
||||||
|
if (decoder == null || decoder.width <= 0 || decoder.height <= 0) {
|
||||||
|
throw IOException("Falha ao inicializar o decodificador de imagem")
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmap = decoder.decode(rgb565 = isLowRamDevice)
|
||||||
|
|
||||||
|
decoder.recycle()
|
||||||
|
|
||||||
|
if (bitmap == null) {
|
||||||
|
throw IOException("Não foi possível decodificar a imagem $filename#$entryName")
|
||||||
|
}
|
||||||
|
|
||||||
|
entryIndex to bitmap
|
||||||
|
}
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
zis.closeEntry()
|
||||||
|
zis.close()
|
||||||
|
|
||||||
|
val totalWidth = images.maxOf { it.second.width }
|
||||||
|
val totalHeight = images.sumOf { it.second.height }
|
||||||
|
|
||||||
|
val result = Bitmap.createBitmap(totalWidth, totalHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
|
var dy = 0
|
||||||
|
|
||||||
|
images.forEach {
|
||||||
|
val srcRect = Rect(0, 0, it.second.width, it.second.height)
|
||||||
|
val dstRect = Rect(0, dy, it.second.width, dy + it.second.height)
|
||||||
|
|
||||||
|
canvas.drawBitmap(it.second, srcRect, dstRect, null)
|
||||||
|
|
||||||
|
dy += it.second.height
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||||
|
|
||||||
|
val image = output.toByteArray()
|
||||||
|
val body = image.toResponseBody("image/jpeg".toMediaType())
|
||||||
|
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(body)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityManager#isLowRamDevice is based on a system property, which isn't
|
||||||
|
* necessarily trustworthy. 1GB is supposedly the regular threshold.
|
||||||
|
*
|
||||||
|
* Instead, we consider anything with less than 3GB of RAM as low memory
|
||||||
|
* considering how heavy image processing can be.
|
||||||
|
*/
|
||||||
|
private val isLowRamDevice by lazy {
|
||||||
|
val ctx = Injekt.get<Application>()
|
||||||
|
val activityManager = ctx.getSystemService("activity") as ActivityManager
|
||||||
|
val memInfo = ActivityManager.MemoryInfo()
|
||||||
|
|
||||||
|
activityManager.getMemoryInfo(memInfo)
|
||||||
|
|
||||||
|
memInfo.totalMem < 3L * 1024 * 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue