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
|
baseVersionCode = 9
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
implementation(project(":lib:zipinterceptor"))
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.peachscan
|
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityManager
|
import eu.kanade.tachiyomi.lib.zipinterceptor.ZipInterceptor
|
||||||
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.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
abstract class PeachScan(
|
abstract class PeachScan(
|
||||||
@ -53,7 +41,7 @@ abstract class PeachScan(
|
|||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addInterceptor(::zipImageInterceptor)
|
.addInterceptor(ZipInterceptor()::zipImageInterceptor)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@ -192,90 +180,6 @@ abstract class PeachScan(
|
|||||||
return GET(page.imageUrl!!, imgHeaders)
|
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 {
|
companion object {
|
||||||
const val URL_SEARCH_PREFIX = "slug:"
|
const val URL_SEARCH_PREFIX = "slug:"
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import android.util.Base64
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.Arrays
|
import java.util.Arrays
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.SecretKey
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
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.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
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 tachiyomi.decoder.ImageDecoder
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
/**
|
open class ZipInterceptor {
|
||||||
* 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 {
|
|
||||||
private var decodeMethod: Method
|
private var decodeMethod: Method
|
||||||
private var newInstanceMethod: Method
|
private var newInstanceMethod: Method
|
||||||
|
|
||||||
private var classSignature = ClassSignature.Newest
|
private var classSignature = ClassSignature.Newest
|
||||||
|
|
||||||
|
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||||
|
|
||||||
private enum class ClassSignature {
|
private enum class ClassSignature {
|
||||||
Old, New, Newest
|
Old, New, Newest
|
||||||
}
|
}
|
||||||
@ -121,4 +126,95 @@ object PeachScanUtils {
|
|||||||
|
|
||||||
return bitmap
|
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 {
|
ext {
|
||||||
extName = 'Lura Toon'
|
extName = 'Lura Toon'
|
||||||
extClass = '.LuraToon'
|
extClass = '.LuraToon'
|
||||||
themePkg = 'peachscan'
|
|
||||||
baseUrl = 'https://luratoons.com'
|
baseUrl = 'https://luratoons.com'
|
||||||
overrideVersionCode = 45
|
extVersionCode = 55
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':lib:randomua')
|
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.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
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.addRandomUAPreferenceToScreen
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
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.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
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 okhttp3.Response
|
||||||
import org.jsoup.nodes.Element
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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 :
|
class LuraToon : HttpSource(), ConfigurableSource {
|
||||||
PeachScan(
|
override val baseUrl = "https://luratoons.com"
|
||||||
"Lura Toon",
|
override val name = "Lura Toon"
|
||||||
"https://luratoons.com",
|
override val lang = "pt-BR"
|
||||||
"pt-BR",
|
override val supportsLatest = true
|
||||||
),
|
override val versionId = 2
|
||||||
ConfigurableSource {
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
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)
|
.rateLimit(3)
|
||||||
.setRandomUserAgent(
|
.setRandomUserAgent(
|
||||||
preferences.getPrefUAType(),
|
preferences.getPrefUAType(),
|
||||||
@ -37,27 +59,133 @@ class LuraToon :
|
|||||||
)
|
)
|
||||||
.build()
|
.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) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
addRandomUAPreferenceToScreen(screen)
|
addRandomUAPreferenceToScreen(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||||
val mangaUrl = element.ownerDocument()!!.location()
|
val data = response.parseAs<Manga>()
|
||||||
|
title = data.titulo
|
||||||
return super.chapterFromElement(element).apply {
|
author = data.autor
|
||||||
val num = url.removeSuffix("/")
|
artist = data.artista
|
||||||
.substringAfterLast("/")
|
genre = data.generos.joinToString(", ") { it.name }
|
||||||
val chapUrl = mangaUrl.removeSuffix("/") + "/$num/"
|
status = when (data.status) {
|
||||||
|
"Em Lançamento" -> SManga.ONGOING
|
||||||
setUrlWithoutDomain(chapUrl)
|
"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> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val capitulo = response.parseAs<CapituloPagina>()
|
||||||
val pathSegments = response.request.url.pathSegments
|
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")
|
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