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