mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-21 18:02:42 +01:00
LuraToon: Refactor to use API JSON without scraper (#6029)
* LuraToon: Refactor to use API JSON without scraper * Bump version code to 46 * LuraToon: fix problems details, search, latest and decrypt zip files images using AES * LuraToon: fix pagination latest list * Refactor create lib to zip interceptor and AES decrypt file for LuraToon and PeachScan * LuraToon: Remove unused code * LuraToon: fix problem with lint on lura zip interceptor * Refactor for each list files Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Refactor use another method to sort caps Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Refactor move code decrypt file from lib CryptoAES to local extension * Refactor add alert exception if not found list chapters * Refactor functions to remove redundancy as suggested * Update version id --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
parent
c29330892a
commit
8973569273
@ -5,5 +5,5 @@ plugins {
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
implementation(project(":lib:zipinterceptor"))
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||
|
||||
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.lib.zipinterceptor.ZipInterceptor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@ -21,23 +16,16 @@ 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 rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
abstract class PeachScan(
|
||||
@ -53,7 +41,7 @@ abstract class PeachScan(
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
.newBuilder()
|
||||
.addInterceptor(::zipImageInterceptor)
|
||||
.addInterceptor(ZipInterceptor()::zipImageInterceptor)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
@ -192,90 +180,6 @@ abstract class PeachScan(
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
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 }
|
||||
.mapNotNull {
|
||||
val entryName = it.name
|
||||
val splitEntryName = entryName.split('.')
|
||||
val entryIndex = splitEntryName.first().toInt()
|
||||
val entryType = splitEntryName.last()
|
||||
|
||||
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
|
||||
zis.readBytes()
|
||||
} else {
|
||||
val svgBytes = zis.readBytes()
|
||||
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||
?: return@mapNotNull null
|
||||
|
||||
Base64.decode(b64, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
entryIndex to PeachScanUtils.decodeImage(imageData, isLowRamDevice, filename, entryName)
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import android.util.Base64
|
||||
import java.security.MessageDigest
|
||||
import java.util.Arrays
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
7
lib/zipinterceptor/build.gradle.kts
Normal file
7
lib/zipinterceptor/build.gradle.kts
Normal file
@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
}
|
@ -1,28 +1,33 @@
|
||||
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||
package eu.kanade.tachiyomi.lib.zipinterceptor
|
||||
|
||||
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 okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Method
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* TachiyomiJ2K is on a 2-year-old version of ImageDecoder at the time of writing,
|
||||
* with a different signature than the one being used as a compile-only dependency.
|
||||
*
|
||||
* Because of this, if [ImageDecoder.decode] is called as-is on TachiyomiJ2K, we
|
||||
* end up with a [NoSuchMethodException].
|
||||
*
|
||||
* This is a hack for determining which signature to call when decoding images.
|
||||
*/
|
||||
object PeachScanUtils {
|
||||
open class ZipInterceptor {
|
||||
private var decodeMethod: Method
|
||||
private var newInstanceMethod: Method
|
||||
|
||||
private var classSignature = ClassSignature.Newest
|
||||
|
||||
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||
|
||||
private enum class ClassSignature {
|
||||
Old, New, Newest
|
||||
}
|
||||
@ -121,4 +126,95 @@ object PeachScanUtils {
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
|
||||
open fun zipGetByteStream(request: Request, response: Response): InputStream {
|
||||
return response.body.byteStream()
|
||||
}
|
||||
|
||||
open fun requestIsZipImage(request: Request): Boolean {
|
||||
return request.url.fragment == "page" && request.url.pathSegments.last().contains(".zip")
|
||||
}
|
||||
|
||||
fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val filename = request.url.pathSegments.last()
|
||||
|
||||
if (requestIsZipImage(request).not()) {
|
||||
return response
|
||||
}
|
||||
|
||||
val zis = ZipInputStream(zipGetByteStream(request, response))
|
||||
|
||||
val images = generateSequence { zis.nextEntry }
|
||||
.mapNotNull {
|
||||
val entryName = it.name
|
||||
val splitEntryName = entryName.split('.')
|
||||
val entryIndex = splitEntryName.first().toInt()
|
||||
val entryType = splitEntryName.last()
|
||||
|
||||
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
|
||||
zis.readBytes()
|
||||
} else {
|
||||
val svgBytes = zis.readBytes()
|
||||
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||
?: return@mapNotNull null
|
||||
|
||||
Base64.decode(b64, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
entryIndex to decodeImage(imageData, isLowRamDevice, filename, entryName)
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
ext {
|
||||
extName = 'Lura Toon'
|
||||
extClass = '.LuraToon'
|
||||
themePkg = 'peachscan'
|
||||
baseUrl = 'https://luratoons.com'
|
||||
overrideVersionCode = 45
|
||||
extVersionCode = 55
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib:randomua')
|
||||
implementation project(':lib:zipinterceptor')
|
||||
}
|
||||
|
@ -3,33 +3,55 @@ package eu.kanade.tachiyomi.extension.pt.randomscan
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.extension.pt.randomscan.dto.Capitulo
|
||||
import eu.kanade.tachiyomi.extension.pt.randomscan.dto.CapituloPagina
|
||||
import eu.kanade.tachiyomi.extension.pt.randomscan.dto.MainPage
|
||||
import eu.kanade.tachiyomi.extension.pt.randomscan.dto.Manga
|
||||
import eu.kanade.tachiyomi.extension.pt.randomscan.dto.SearchResponse
|
||||
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||
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.multisrc.peachscan.PeachScan
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.getValue
|
||||
|
||||
class LuraToon :
|
||||
PeachScan(
|
||||
"Lura Toon",
|
||||
"https://luratoons.com",
|
||||
"pt-BR",
|
||||
),
|
||||
ConfigurableSource {
|
||||
class LuraToon : HttpSource(), ConfigurableSource {
|
||||
override val baseUrl = "https://luratoons.com"
|
||||
override val name = "Lura Toon"
|
||||
override val lang = "pt-BR"
|
||||
override val supportsLatest = true
|
||||
override val versionId = 2
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
override val client = network.cloudflareClient
|
||||
.newBuilder()
|
||||
.addInterceptor(::loggedVerifyInterceptor)
|
||||
.addInterceptor(LuraZipInterceptor()::zipImageInterceptor)
|
||||
.rateLimit(3)
|
||||
.setRandomUserAgent(
|
||||
preferences.getPrefUAType(),
|
||||
@ -37,27 +59,133 @@ class LuraToon :
|
||||
)
|
||||
.build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/main/?part=${page - 1}", headers)
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/main/?part=${page - 1}", headers)
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = GET("$baseUrl/api/autocomplete/$query", headers)
|
||||
override fun chapterListRequest(manga: SManga) = GET("$baseUrl/api/obra/${manga.url.trimStart('/')}", headers)
|
||||
override fun mangaDetailsRequest(manga: SManga) = chapterListRequest(manga)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
addRandomUAPreferenceToScreen(screen)
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val mangaUrl = element.ownerDocument()!!.location()
|
||||
|
||||
return super.chapterFromElement(element).apply {
|
||||
val num = url.removeSuffix("/")
|
||||
.substringAfterLast("/")
|
||||
val chapUrl = mangaUrl.removeSuffix("/") + "/$num/"
|
||||
|
||||
setUrlWithoutDomain(chapUrl)
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val data = response.parseAs<Manga>()
|
||||
title = data.titulo
|
||||
author = data.autor
|
||||
artist = data.artista
|
||||
genre = data.generos.joinToString(", ") { it.name }
|
||||
status = when (data.status) {
|
||||
"Em Lançamento" -> SManga.ONGOING
|
||||
"Finalizado" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
thumbnail_url = "$baseUrl${data.capa}"
|
||||
|
||||
val category = data.tipo
|
||||
val synopsis = data.sinopse
|
||||
description = "Tipo: $category\n\n$synopsis"
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString<T>(body.string())
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.parseAs<MainPage>()
|
||||
|
||||
val mangas = document.lancamentos.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
thumbnail_url = "$baseUrl${it.capa}"
|
||||
setUrlWithoutDomain("/${it.slug}/")
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, document.lancamentos.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
chapterListParse(manga, response)
|
||||
}
|
||||
}
|
||||
|
||||
fun chapterListParse(manga: SManga, response: Response): List<SChapter> {
|
||||
if (response.code == 404) {
|
||||
throw Exception("Capitulos não encontrados, tente migrar o manga, alguns nomes da LuraToon mudaram")
|
||||
}
|
||||
|
||||
val comics = response.parseAs<Manga>()
|
||||
|
||||
return comics.caps.sortedByDescending {
|
||||
it.num
|
||||
}.map { chapterFromElement(manga, it) }
|
||||
}
|
||||
|
||||
private fun chapterFromElement(manga: SManga, capitulo: Capitulo) = SChapter.create().apply {
|
||||
val capSlug = capitulo.slug.trimStart('/')
|
||||
val mangaUrl = manga.url.trimEnd('/').trimStart('/')
|
||||
setUrlWithoutDomain("/api/obra/$mangaUrl/$capSlug")
|
||||
name = capitulo.num.toString().removeSuffix(".0")
|
||||
date_upload = runCatching {
|
||||
dateFormat.parse(capitulo.data)!!.time
|
||||
}.getOrDefault(0L)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val capitulo = response.parseAs<CapituloPagina>()
|
||||
val pathSegments = response.request.url.pathSegments
|
||||
if (pathSegments.contains("login") || pathSegments.isEmpty()) {
|
||||
return (0 until capitulo.files).map { i ->
|
||||
Page(i, baseUrl, "$baseUrl/api/cap-download/${capitulo.obra.id}/${capitulo.id}/$i?obra_id=${capitulo.obra.id}&cap_id=${capitulo.id}&slug=${pathSegments[2]}&cap_slug=${pathSegments[3]}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangas = response.parseAs<SearchResponse>().obras.map {
|
||||
SManga.create().apply {
|
||||
title = it.titulo
|
||||
thumbnail_url = "$baseUrl${it.capa}"
|
||||
setUrlWithoutDomain("/${it.slug}/")
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.parseAs<MainPage>()
|
||||
|
||||
val mangas = document.top_10.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
thumbnail_url = "$baseUrl${it.capa}"
|
||||
setUrlWithoutDomain("/${it.slug}/")
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private fun loggedVerifyInterceptor(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
val pathSegments = response.request.url.pathSegments
|
||||
if (response.request.url.pathSegments.contains("login") || pathSegments.isEmpty()) {
|
||||
throw Exception("Faça o login na WebView para acessar o contéudo")
|
||||
}
|
||||
return super.pageListParse(response)
|
||||
if (response.code == 429) {
|
||||
throw Exception("A LuraToon lhe bloqueou por acessar rápido demais, aguarde por volta de 1 minuto e tente novamente")
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply {
|
||||
timeZone = TimeZone.getTimeZone("America/Sao_Paulo")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.randomscan
|
||||
|
||||
import eu.kanade.tachiyomi.lib.zipinterceptor.ZipInterceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class LuraZipInterceptor : ZipInterceptor() {
|
||||
fun decryptFile(encryptedData: ByteArray, keyBytes: ByteArray): ByteArray {
|
||||
val keyHash = MessageDigest.getInstance("SHA-256").digest(keyBytes)
|
||||
|
||||
val key: SecretKey = SecretKeySpec(keyHash, "AES")
|
||||
|
||||
val counter = encryptedData.copyOfRange(0, 8)
|
||||
val iv = IvParameterSpec(counter)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, iv)
|
||||
|
||||
val decryptedData = cipher.doFinal(encryptedData.copyOfRange(8, encryptedData.size))
|
||||
|
||||
return decryptedData
|
||||
}
|
||||
|
||||
override fun requestIsZipImage(request: Request): Boolean {
|
||||
return request.url.pathSegments.contains("cap-download")
|
||||
}
|
||||
|
||||
override fun zipGetByteStream(request: Request, response: Response): InputStream {
|
||||
val keyData = listOf("obra_id", "slug", "cap_id", "cap_slug").joinToString("") {
|
||||
request.url.queryParameterValues(it).first().toString()
|
||||
}.toByteArray(StandardCharsets.UTF_8)
|
||||
val encryptedData = response.body.bytes()
|
||||
|
||||
val decryptedData = decryptFile(encryptedData, keyData)
|
||||
return ByteArrayInputStream(decryptedData)
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.randomscan.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Genero(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Capitulo(
|
||||
val num: Double,
|
||||
val data: String,
|
||||
val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Manga(
|
||||
val capa: String,
|
||||
val titulo: String,
|
||||
val autor: String?,
|
||||
val artista: String?,
|
||||
val status: String,
|
||||
val sinopse: String,
|
||||
val tipo: String,
|
||||
val generos: List<Genero>,
|
||||
val caps: List<Capitulo>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Obra(
|
||||
val id: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CapituloPagina(
|
||||
val id: Int,
|
||||
val obra: Obra,
|
||||
val files: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MainPageManga(
|
||||
val title: String,
|
||||
val capa: String,
|
||||
val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MainPage(
|
||||
val lancamentos: List<MainPageManga>,
|
||||
val top_10: List<MainPageManga>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResponseManga(
|
||||
val titulo: String,
|
||||
val capa: String,
|
||||
val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResponse(
|
||||
val obras: List<SearchResponseManga>,
|
||||
)
|
Loading…
Reference in New Issue
Block a user