120 lines
4.8 KiB
Kotlin
120 lines
4.8 KiB
Kotlin
package eu.kanade.tachiyomi.extension.id.shinigami
|
|
|
|
import android.util.Base64
|
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
|
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
import eu.kanade.tachiyomi.source.model.Page
|
|
import eu.kanade.tachiyomi.source.model.SChapter
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.decodeFromString
|
|
import okhttp3.OkHttpClient
|
|
import org.jsoup.nodes.Document
|
|
import org.jsoup.nodes.Element
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
class Shinigami : Madara("Shinigami", "https://shinigamitoon.com", "id") {
|
|
// moved from Reaper Scans (id) to Shinigami (id)
|
|
override val id = 3411809758861089969
|
|
|
|
override val useNewChapterEndpoint = false
|
|
|
|
override fun searchPage(page: Int): String = if (page == 1) "" else "page/$page/"
|
|
|
|
override fun headersBuilder() = super.headersBuilder().apply {
|
|
add("Sec-Fetch-Dest", "document")
|
|
add("Sec-Fetch-Mode", "navigate")
|
|
add("Sec-Fetch-Site", "same-origin")
|
|
add("Upgrade-Insecure-Requests", "1")
|
|
add("X-Requested-With", randomString((1..20).random())) // added for webview, and removed in interceptor for normal use
|
|
}
|
|
|
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
.addInterceptor { chain ->
|
|
val request = chain.request()
|
|
val headers = request.headers.newBuilder().apply {
|
|
removeAll("X-Requested-With")
|
|
}.build()
|
|
|
|
chain.proceed(request.newBuilder().headers(headers).build())
|
|
}
|
|
.connectTimeout(10, TimeUnit.SECONDS)
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
.rateLimit(3)
|
|
.build()
|
|
|
|
override val mangaSubString = "semua-series"
|
|
|
|
// Tags are useless as they are just SEO keywords.
|
|
override val mangaDetailsSelectorTag = ""
|
|
|
|
override val chapterUrlSelector = "div.chapter-link:not([style~=display:\\snone]) a"
|
|
|
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
val urlElement = element.selectFirst(chapterUrlSelector)!!
|
|
|
|
name = urlElement.selectFirst("p.chapter-manhwa-title")?.text()
|
|
?: urlElement.ownText()
|
|
date_upload = urlElement.selectFirst("span.chapter-release-date > i")?.text()
|
|
.let { parseChapterDate(it) }
|
|
|
|
val fixedUrl = urlElement.attr("abs:href")
|
|
|
|
setUrlWithoutDomain(fixedUrl)
|
|
}
|
|
|
|
// Page list
|
|
@Serializable
|
|
data class CDT(val ct: String, val s: String)
|
|
|
|
override fun pageListParse(document: Document): List<Page> {
|
|
val script = document.selectFirst("script:containsData(chapter_data)")?.data()
|
|
?: throw Exception("chapter_data script not found")
|
|
|
|
val deobfuscated = Deobfuscator.deobfuscateScript(script)
|
|
?: throw Exception("Unable to deobfuscate chapter_data script")
|
|
|
|
val keyMatch = KEY_REGEX.find(deobfuscated)?.groupValues
|
|
?: throw Exception("Unable to find key")
|
|
|
|
val chapterData = json.decodeFromString<CDT>(
|
|
CHAPTER_DATA_REGEX.find(script)?.groupValues?.get(1) ?: throw Exception("Unable to get chapter data"),
|
|
)
|
|
val postId = POST_ID_REGEX.find(script)?.groupValues?.get(1) ?: throw Exception("Unable to get post_id")
|
|
val otherId = OTHER_ID_REGEX.findAll(script).firstOrNull { it.groupValues[1] != "post" }?.groupValues?.get(2) ?: throw Exception("Unable to get other id")
|
|
val key = otherId + keyMatch[1] + postId + keyMatch[2] + postId
|
|
val salt = chapterData.s.decodeHex()
|
|
|
|
val unsaltedCiphertext = Base64.decode(chapterData.ct, Base64.DEFAULT)
|
|
val ciphertext = SALTED + salt + unsaltedCiphertext
|
|
|
|
val decrypted = CryptoAES.decrypt(Base64.encodeToString(ciphertext, Base64.DEFAULT), key)
|
|
val data = json.decodeFromString<List<String>>(decrypted)
|
|
return data.mapIndexed { idx, it ->
|
|
Page(idx, document.location(), it)
|
|
}
|
|
}
|
|
|
|
// https://stackoverflow.com/a/66614516
|
|
private fun String.decodeHex(): ByteArray {
|
|
check(length % 2 == 0) { "Must have an even length" }
|
|
|
|
return chunked(2)
|
|
.map { it.toInt(16).toByte() }
|
|
.toByteArray()
|
|
}
|
|
|
|
private fun randomString(length: Int): String {
|
|
val charPool = ('a'..'z') + ('A'..'Z')
|
|
return List(length) { charPool.random() }.joinToString("")
|
|
}
|
|
|
|
companion object {
|
|
private val KEY_REGEX by lazy { Regex("""_id\s+\+\s+'(.*?)'\s+\+\s+post_id\s+\+\s+'(.*?)'\s+\+\s+post_id""") }
|
|
private val CHAPTER_DATA_REGEX by lazy { Regex("""var chapter_data\s*=\s*'(.*?)'""") }
|
|
private val POST_ID_REGEX by lazy { Regex("""var post_id\s*=\s*'(.*?)'""") }
|
|
private val OTHER_ID_REGEX by lazy { Regex("""var (\w+)_id\s*=\s*'(.*?)'""") }
|
|
}
|
|
}
|