SakuraMangas: Fix loading content (#10685)
* Fix loading content * Remove @RequiresApi
This commit is contained in:
parent
d22070e11c
commit
d824aa0a17
@ -1,8 +1,12 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Sakura Mangás'
|
extName = 'Sakura Mangás'
|
||||||
extClass = '.SakuraMangas'
|
extClass = '.SakuraMangas'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:synchrony"))
|
||||||
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.extension.pt.sakuramangas
|
package eu.kanade.tachiyomi.extension.pt.sakuramangas
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@ -16,8 +19,12 @@ import okhttp3.FormBody
|
|||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
@ -30,6 +37,10 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
override val baseUrl = "https://sakuramangas.org"
|
override val baseUrl = "https://sakuramangas.org"
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(3, 2)
|
||||||
|
.build()
|
||||||
|
|
||||||
private var genresSet: Set<Genre> = emptySet()
|
private var genresSet: Set<Genre> = emptySet()
|
||||||
private var demographyOptions: List<Pair<String, String>> = listOf(
|
private var demographyOptions: List<Pair<String, String>> = listOf(
|
||||||
"Todos" to "",
|
"Todos" to "",
|
||||||
@ -55,7 +66,7 @@ class SakuraMangas : HttpSource() {
|
|||||||
// ================================ Latest =======================================
|
// ================================ Latest =======================================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
GET("$baseUrl/dist/sakura/models/home/home_ultimos.php", headers)
|
GET("$baseUrl/dist/sakura/models/home/__.home_ultimos.php", headers)
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
val result = response.parseAs<List<String>>()
|
val result = response.parseAs<List<String>>()
|
||||||
@ -76,7 +87,7 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder()
|
||||||
.add("seach", query)
|
.add("search", query)
|
||||||
.add("order", "3")
|
.add("order", "3")
|
||||||
.add("offset", ((page - 1) * 15).toString())
|
.add("offset", ((page - 1) * 15).toString())
|
||||||
.add("limit", "15")
|
.add("limit", "15")
|
||||||
@ -112,7 +123,7 @@ class SakuraMangas : HttpSource() {
|
|||||||
classification?.let { form.add("classification", it) }
|
classification?.let { form.add("classification", it) }
|
||||||
orderBy?.let { form.add("order", it) }
|
orderBy?.let { form.add("order", it) }
|
||||||
|
|
||||||
return POST("$baseUrl/dist/sakura/models/obras/obras_buscar.php", headers, form.build())
|
return POST("$baseUrl/dist/sakura/models/obras/__.obras_buscar.php", headers, form.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
@ -125,8 +136,8 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val result = response.parseAs<SakuraMangasResultDto>()
|
val result = response.parseAs<SakuraMangasResultDto>()
|
||||||
val seriesList =
|
val document = result.asJsoup("$baseUrl/obras/")
|
||||||
result.asJsoup("$baseUrl/obras/").select(".result-item").map(::searchMangaFromElement)
|
val seriesList = document.select(".result-item").map(::searchMangaFromElement)
|
||||||
return MangasPage(seriesList, result.hasMore)
|
return MangasPage(seriesList, result.hasMore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,42 +145,91 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}"
|
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}"
|
||||||
|
|
||||||
private fun mangaDetailsApiRequest(mangaId: String): Request {
|
private fun mangaDetailsApiRequest(mangaId: String, challenge: String, token: String): Request {
|
||||||
|
val proof = generateHeaderProof(challenge, keys.mangaInfo)!!
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder()
|
||||||
.add("manga_id", mangaId)
|
.add("manga_id", mangaId)
|
||||||
.add("dataType", "json")
|
.add("dataType", "json")
|
||||||
|
.add("challenge", challenge)
|
||||||
|
.add("proof", proof)
|
||||||
|
|
||||||
return POST("$baseUrl/dist/sakura/models/manga/manga_info.php", headers, form.build())
|
val detailsHeaders = headers.newBuilder()
|
||||||
|
.add("X-Verification-Key-1", keys.xVerificationKey1)
|
||||||
|
.add("X-Verification-Key-2", keys.xVerificationKey2)
|
||||||
|
.add("X-CSRF-Token", token)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/dist/sakura/models/manga/__obf__manga_info.php", detailsHeaders, form.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val mangaId = document.selectFirst("meta[manga-id]")!!.attr("manga-id")
|
val mangaId = document.selectFirst("meta[manga-id]")!!.attr("manga-id")
|
||||||
|
val challenge = document.selectFirst("meta[name=header-challenge]")!!.attr("content")
|
||||||
|
val token = document.selectFirst("meta[name=csrf-token]")!!.attr("content")
|
||||||
|
|
||||||
return client.newCall(mangaDetailsApiRequest(mangaId)).execute()
|
return client.newCall(mangaDetailsApiRequest(mangaId, challenge, token)).execute()
|
||||||
.parseAs<SakuraMangaInfoDto>().toSManga(document.baseUri())
|
.parseAs<SakuraMangaInfoDto>().toSManga(document.baseUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val keys: Keys by lazy {
|
||||||
|
val mangaInfoRegex = """(?:manga_info:\s+)(\d+)""".toRegex()
|
||||||
|
val chapterReadRegex = """(?:chapter_read:\s+)(\d+)""".toRegex()
|
||||||
|
val key1Regex = """(?:key1:\s+')([^']+)""".toRegex()
|
||||||
|
val key2Regex = """(?:key2:\s+')([^']+)""".toRegex()
|
||||||
|
|
||||||
|
val script = client.newCall(GET("$baseUrl/dist/sakura/global/security.obf.js", headers))
|
||||||
|
.execute().body.string()
|
||||||
|
|
||||||
|
val deobfuscated = Deobfuscator.deobfuscateScript(script)!!
|
||||||
|
|
||||||
|
Keys(
|
||||||
|
mangaInfo = mangaInfoRegex.find(deobfuscated)?.groupValues?.last()?.toLong() ?: 0L,
|
||||||
|
chapterRead = chapterReadRegex.find(deobfuscated)?.groupValues?.last()?.toLong() ?: 0L,
|
||||||
|
xVerificationKey1 = key1Regex.find(deobfuscated)?.groupValues?.last() ?: "",
|
||||||
|
xVerificationKey2 = key2Regex.find(deobfuscated)?.groupValues?.last() ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Keys(
|
||||||
|
val mangaInfo: Long,
|
||||||
|
val chapterRead: Long,
|
||||||
|
val xVerificationKey1: String,
|
||||||
|
val xVerificationKey2: String,
|
||||||
|
)
|
||||||
|
|
||||||
// ================================ Chapters =======================================
|
// ================================ Chapters =======================================
|
||||||
|
|
||||||
private fun chapterListApiRequest(mangaId: String, page: Int): Request {
|
private fun chapterListApiRequest(mangaId: String, challenge: String, token: String, page: Int): Request {
|
||||||
|
val proof = generateHeaderProof(challenge, keys.mangaInfo)!!
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder()
|
||||||
.add("manga_id", mangaId)
|
.add("manga_id", mangaId)
|
||||||
.add("offset", ((page - 1) * 90).toString())
|
.add("offset", ((page - 1) * 90).toString())
|
||||||
.add("order", "desc")
|
.add("order", "desc")
|
||||||
.add("limit", "90")
|
.add("limit", "90")
|
||||||
|
.add("challenge", challenge)
|
||||||
|
.add("proof", proof)
|
||||||
|
|
||||||
return POST("$baseUrl/dist/sakura/models/manga/manga_capitulos.php", headers, form.build())
|
val chapterHeaders = headers.newBuilder()
|
||||||
|
.add("X-Verification-Key-1", keys.xVerificationKey1)
|
||||||
|
.add("X-Verification-Key-2", keys.xVerificationKey2)
|
||||||
|
.add("X-CSRF-Token", token)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/dist/sakura/models/manga/__obf__manga_capitulos.php", chapterHeaders, form.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val mangaId = document.selectFirst("meta[manga-id]")!!.attr("manga-id")
|
val mangaId = document.selectFirst("meta[manga-id]")!!.attr("manga-id")
|
||||||
|
val challenge = document.selectFirst("meta[name=header-challenge]")!!.attr("content")
|
||||||
|
val token = document.selectFirst("meta[name=csrf-token]")!!.attr("content")
|
||||||
|
|
||||||
var page = 1
|
var page = 1
|
||||||
val chapters = mutableListOf<SChapter>()
|
val chapters = mutableListOf<SChapter>()
|
||||||
do {
|
do {
|
||||||
val doc = client.newCall(chapterListApiRequest(mangaId, page++)).execute().asJsoup()
|
val doc = client.newCall(chapterListApiRequest(mangaId, challenge, token, page++)).execute().asJsoup()
|
||||||
|
|
||||||
val chapterGroup = doc.select(".capitulo-item").map(::chapterFromElement).also {
|
val chapterGroup = doc.select(".capitulo-item").map(::chapterFromElement).also {
|
||||||
chapters += it
|
chapters += it
|
||||||
@ -202,14 +262,28 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
// ================================ Pages =======================================
|
// ================================ Pages =======================================
|
||||||
|
|
||||||
private fun pageListApiRequest(chapterId: String, token: String): Request {
|
private fun pageListApiRequest(
|
||||||
|
chapterId: String,
|
||||||
|
token: String,
|
||||||
|
challenge: String,
|
||||||
|
csrf: String,
|
||||||
|
): Request {
|
||||||
|
val proof = generateHeaderProof(challenge, keys.chapterRead)!!
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder()
|
||||||
.add("chapter_id", chapterId)
|
.add("chapter_id", chapterId)
|
||||||
.add("token", token)
|
.add("token", token)
|
||||||
|
.add("challenge", challenge)
|
||||||
|
.add("proof", proof)
|
||||||
|
|
||||||
|
val pageHeaders = headers.newBuilder()
|
||||||
|
.add("X-Verification-Key-1", keys.xVerificationKey1)
|
||||||
|
.add("X-Verification-Key-2", keys.xVerificationKey2)
|
||||||
|
.add("X-CSRF-Token", csrf)
|
||||||
|
.build()
|
||||||
|
|
||||||
return POST(
|
return POST(
|
||||||
"$baseUrl/dist/sakura/models/capitulo/capitulos_read.php",
|
"$baseUrl/dist/sakura/models/capitulo/__obf.__capltulos_read.php",
|
||||||
headers,
|
pageHeaders,
|
||||||
form.build(),
|
form.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -219,18 +293,20 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
val chapterId = document.selectFirst("meta[chapter-id]")!!.attr("chapter-id")
|
val chapterId = document.selectFirst("meta[chapter-id]")!!.attr("chapter-id")
|
||||||
val token = document.selectFirst("meta[token]")!!.attr("token")
|
val token = document.selectFirst("meta[token]")!!.attr("token")
|
||||||
|
val subtoken = document.selectFirst("meta[token]")!!.attr("subtoken")
|
||||||
|
val challenge = document.selectFirst("meta[name=header-challenge]")!!.attr("content")
|
||||||
|
val csrf = document.selectFirst("meta[name=csrf-token]")!!.attr("content")
|
||||||
|
|
||||||
val response = client.newCall(pageListApiRequest(chapterId, token)).execute()
|
val response = client.newCall(pageListApiRequest(chapterId, token, challenge, csrf)).execute()
|
||||||
.parseAs<SakuraMangaChapterReadDto>()
|
.parseAs<SakuraMangaChapterReadDto>()
|
||||||
|
|
||||||
val baseUrl = document.baseUri().trimEnd('/')
|
val baseUrl = document.baseUri().trimEnd('/')
|
||||||
|
|
||||||
return response.imageUrls.mapIndexed { index, url ->
|
return vortexDecipherV2(response.imageUrls, subtoken)
|
||||||
Page(
|
.parseAs<List<String>>()
|
||||||
index,
|
.mapIndexed { index, url ->
|
||||||
imageUrl = "$baseUrl/$url".toHttpUrl().toString(),
|
Page(index, imageUrl = "$baseUrl/$url".toHttpUrl().toString())
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
@ -319,4 +395,84 @@ class SakuraMangas : HttpSource() {
|
|||||||
|
|
||||||
return now.timeInMillis
|
return now.timeInMillis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function extracted from https://sakuramangas.org/dist/sakura/pages/capitulo/capitulo.v100w.obs.js
|
||||||
|
private fun generateHeaderProof(base64: String?, key: Long?): String? {
|
||||||
|
val userAgent = headers["User-Agent"]
|
||||||
|
if (base64 == null || key == null || userAgent == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val decoded = String(Base64.decode(base64, Base64.DEFAULT), Charsets.UTF_8)
|
||||||
|
|
||||||
|
val parts = decoded.split('/')
|
||||||
|
if (parts.size != 3) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val address = parts.first()
|
||||||
|
val pathSegment = parts.last()
|
||||||
|
|
||||||
|
var result = address + userAgent + key + pathSegment
|
||||||
|
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
repeat(29) {
|
||||||
|
val data = result.toByteArray(Charsets.UTF_8)
|
||||||
|
val hashBytes = digest.digest(data)
|
||||||
|
digest.reset()
|
||||||
|
result = hashBytes.joinToString("") { byte ->
|
||||||
|
String.format("%02x", byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} catch (_: Exception) {
|
||||||
|
throw IOException("Falha ao gerar token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function extracted from https://sakuramangas.org/dist/sakura/pages/capitulo/capitulo.v100w.obs.js
|
||||||
|
private fun vortexDecipherV2(dataBase64: String, key: String): String {
|
||||||
|
try {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256").digest(key.toByteArray(Charsets.UTF_8))
|
||||||
|
val buffer = ByteBuffer.wrap(digest).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
var v1 = buffer.int.toUInt()
|
||||||
|
var v2 = buffer.int.toUInt()
|
||||||
|
var v3 = buffer.int.toUInt()
|
||||||
|
var v4 = buffer.int.toUInt()
|
||||||
|
val c1 = buffer.int.toUInt()
|
||||||
|
val c2 = buffer.int.toUInt()
|
||||||
|
|
||||||
|
val decoded = Base64.decode(dataBase64, Base64.DEFAULT)
|
||||||
|
|
||||||
|
val output = ByteArray(decoded.size)
|
||||||
|
|
||||||
|
for (i in decoded.indices) {
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
v1 = (v1 + 2654435769u)
|
||||||
|
v2 = (v2 xor c1)
|
||||||
|
v3 = (v3 + v2)
|
||||||
|
val shift = (v1 and 31u).toInt()
|
||||||
|
v4 = (((v4 xor v3).rotateLeft(shift)))
|
||||||
|
} else {
|
||||||
|
v3 = (v3 + 1640531527u)
|
||||||
|
v4 = (v4 xor c2)
|
||||||
|
v1 = (v1 + v4)
|
||||||
|
val shift = (v3 and 31u).toInt()
|
||||||
|
v2 = (((v2 xor v1).rotateLeft(shift)))
|
||||||
|
}
|
||||||
|
val mask = (v1 xor v2 xor v3 xor v4).toInt() and 0xFF
|
||||||
|
output[i] = (decoded[i].toInt() xor mask).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.toString(Charsets.UTF_8)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IOException("Não foi possível descriptografar os dados do capítulo.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UInt.rotateLeft(bits: Int): UInt {
|
||||||
|
return (this shl bits) or (this shr (32 - bits))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,5 +56,5 @@ class SakuraMangaInfoDto(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class SakuraMangaChapterReadDto(
|
class SakuraMangaChapterReadDto(
|
||||||
val imageUrls: List<String>,
|
val imageUrls: String,
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user