add source and create decode url image
This commit is contained in:
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.parsers.site.madara
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
@@ -10,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import org.koitharu.kotatsu.parsers.util.cryptoaes.CryptoAES
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -291,6 +293,7 @@ internal abstract class MadaraParser(
|
||||
|
||||
val stateDiv = (body.selectFirst("div.post-content_item:contains(Status)")
|
||||
?: body.selectFirst("div.post-content_item:contains(Statut)")
|
||||
?: body.selectFirst("div.post-content_item:contains(État)")
|
||||
?: body.selectFirst("div.post-content_item:contains(حالة العمل)")
|
||||
?: body.selectFirst("div.post-content_item:contains(Estado)")
|
||||
?: body.selectFirst("div.post-content_item:contains(สถานะ)")
|
||||
@@ -392,18 +395,67 @@ internal abstract class MadaraParser(
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val root = doc.body().selectFirst("div.main-col-inner")?.selectFirst("div.reading-content")
|
||||
?: throw ParseException("Root not found", fullUrl)
|
||||
return root.select("div.page-break").map { div ->
|
||||
val img = div.selectFirst("img") ?: div.parseFailed("Page image not found")
|
||||
val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
|
||||
val chapterProtector = doc.selectFirst("#chapter-protector-data")
|
||||
|
||||
if (chapterProtector == null) {
|
||||
val root = doc.body().selectFirst("div.main-col-inner")?.selectFirst("div.reading-content")
|
||||
?: throw ParseException("Root not found", fullUrl)
|
||||
return root.select("div.page-break").map { div ->
|
||||
val img = div.selectFirst("img") ?: div.parseFailed("Page image not found")
|
||||
val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
|
||||
val chapterProtectorHtml = chapterProtector.html()
|
||||
val password = chapterProtectorHtml.substringAfter("wpmangaprotectornonce='").substringBefore("';")
|
||||
val chapterData = JSONObject(
|
||||
chapterProtectorHtml.substringAfter("chapter_data='").substringBefore("';").replace("\\/", "/"),
|
||||
)
|
||||
val unsaltedCiphertext = context.decodeBase64(chapterData.getString("ct"))
|
||||
val salt = chapterData.getString("s").toString().decodeHex()
|
||||
val ciphertext = SALTED + salt + unsaltedCiphertext
|
||||
|
||||
val rawImgArray = CryptoAES.decrypt(Base64.getEncoder().encodeToString(ciphertext), password)
|
||||
val imgArrayString = rawImgArray
|
||||
.replace("[", "")
|
||||
.replace("]", "")
|
||||
.replace("\\", "")
|
||||
.replace("\"", "")
|
||||
|
||||
|
||||
return imgArrayString.split(",").map { url ->
|
||||
MangaPage(
|
||||
id = generateUid(url.toString()),
|
||||
url = url.toString(),
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun String.decodeHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
val SALTED = "Salted__".toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
@@ -605,4 +657,5 @@ internal abstract class MadaraParser(
|
||||
it.substring(0, pos) to it.substring(pos + 1)
|
||||
}.toMutableMap()
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
|
||||
@MangaSourceParser("MANGACULTIVATOR", "MangaCultivator", "en")
|
||||
internal class MangaCultivator(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.MANGACULTIVATOR, "mangacultivator.com", 10)
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.en
|
||||
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
|
||||
@MangaSourceParser("MANHUASY", "Manhuasy", "en")
|
||||
internal class Manhuasy(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.MANHUASY, "www.manhuasy.com") {
|
||||
|
||||
override val tagPrefix = "manhua-genre/"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("APENASMAISUMYAOI", "ApenasmaisumYaoi", "pt")
|
||||
internal class ApenasmaisumYaoi(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.APENASMAISUMYAOI, "apenasmaisumyaoi.com") {
|
||||
|
||||
override val isNsfwSource = true
|
||||
override val datePattern: String = "dd 'de' MMMMM 'de' yyyy"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("LEITORIZAKAYA", "Leitorizakaya", "pt")
|
||||
internal class Leitorizakaya(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.LEITORIZAKAYA, "leitorizakaya.net") {
|
||||
|
||||
override val isNsfwSource = true
|
||||
override val datePattern: String = "dd/MM/yyyy"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||
import org.koitharu.kotatsu.parsers.util.parseFailed
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("LIMASCANS", "Lima Scans", "pt")
|
||||
internal class LimaScans(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.LIMASCANS, "limascans.xyz/v2", 10) {
|
||||
|
||||
override val isNsfwSource = true
|
||||
override val postreq = true
|
||||
override val datePattern: String = "dd 'de' MMMMM 'de' yyyy"
|
||||
|
||||
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
|
||||
|
||||
|
||||
val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
|
||||
val url = "https://$domain/wp-admin/admin-ajax.php"
|
||||
val postdata = "action=manga_get_chapters&manga=$mangaId"
|
||||
val doc = webClient.httpPost(url, postdata).parseHtml()
|
||||
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
|
||||
return doc.select(selectchapter).mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirst("a")
|
||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||
val link = href + stylepage
|
||||
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectdate)?.text()
|
||||
val name = a.selectFirst("p")?.text() ?: a.ownText()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
url = link.replace("/v2", ""),
|
||||
name = name,
|
||||
number = i + 1,
|
||||
branch = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.madara.pt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
|
||||
|
||||
@MangaSourceParser("SINENSISSCANS", "Sinensis Scans", "pt")
|
||||
internal class SinensisScans(context: MangaLoaderContext) :
|
||||
MadaraParser(context, MangaSource.SINENSISSCANS, "sinensisscans.com") {
|
||||
|
||||
override val datePattern: String = "dd/MM/yyyy"
|
||||
}
|
||||
132
src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt
Normal file
132
src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package org.koitharu.kotatsu.parsers.util.cryptoaes
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.Arrays
|
||||
import java.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Conforming with CryptoJS AES method
|
||||
*/
|
||||
@Suppress("unused", "FunctionName")
|
||||
object CryptoAES {
|
||||
|
||||
private const val KEY_SIZE = 256
|
||||
private const val IV_SIZE = 128
|
||||
private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING"
|
||||
private const val AES = "AES"
|
||||
private const val KDF_DIGEST = "MD5"
|
||||
|
||||
/**
|
||||
* Decrypt using CryptoJS defaults compatible method.
|
||||
* Uses KDF equivalent to OpenSSL's EVP_BytesToKey function
|
||||
*
|
||||
* http://stackoverflow.com/a/29152379/4405051
|
||||
* @param cipherText base64 encoded ciphertext
|
||||
* @param password passphrase
|
||||
*/
|
||||
fun decrypt(cipherText: String, password: String): String {
|
||||
try {
|
||||
val ctBytes = Base64.getDecoder().decode(cipherText)
|
||||
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
||||
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
||||
return decryptAES(cipherTextBytes,
|
||||
keyAndIV?.get(0) ?: ByteArray(32),
|
||||
keyAndIV?.get(1) ?: ByteArray(16))
|
||||
} catch (e: Exception) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt using CryptoJS defaults compatible method.
|
||||
*
|
||||
* @param cipherText base64 encoded ciphertext
|
||||
* @param keyBytes key as a bytearray
|
||||
* @param ivBytes iv as a bytearray
|
||||
*/
|
||||
fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String {
|
||||
return try {
|
||||
val cipherTextBytes = Base64.getDecoder().decode(cipherText)
|
||||
decryptAES(cipherTextBytes, keyBytes, ivBytes)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt using CryptoJS defaults compatible method.
|
||||
*
|
||||
* @param cipherTextBytes encrypted text as a bytearray
|
||||
* @param keyBytes key as a bytearray
|
||||
* @param ivBytes iv as a bytearray
|
||||
*/
|
||||
private fun decryptAES(cipherTextBytes: ByteArray, keyBytes: ByteArray, ivBytes: ByteArray): String {
|
||||
return try {
|
||||
val cipher = Cipher.getInstance(HASH_CIPHER)
|
||||
val keyS = SecretKeySpec(keyBytes, AES)
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(ivBytes))
|
||||
cipher.doFinal(cipherTextBytes).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a key and an initialization vector (IV) with the given salt and password.
|
||||
*
|
||||
* https://stackoverflow.com/a/41434590
|
||||
* This method is equivalent to OpenSSL's EVP_BytesToKey function
|
||||
* (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c).
|
||||
* By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data.
|
||||
*
|
||||
* @param keyLength the length of the generated key (in bytes)
|
||||
* @param ivLength the length of the generated IV (in bytes)
|
||||
* @param iterations the number of digestion rounds
|
||||
* @param salt the salt data (8 bytes of data or `null`)
|
||||
* @param password the password data (optional)
|
||||
* @param md the message digest algorithm to use
|
||||
* @return an two-element array with the generated key and IV
|
||||
*/
|
||||
private fun generateKeyAndIV(keyLength: Int, ivLength: Int, iterations: Int, salt: ByteArray, password: ByteArray, md: MessageDigest): Array<ByteArray?>? {
|
||||
val digestLength = md.digestLength
|
||||
val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
|
||||
val generatedData = ByteArray(requiredLength)
|
||||
var generatedLength = 0
|
||||
return try {
|
||||
md.reset()
|
||||
|
||||
// Repeat process until sufficient data has been generated
|
||||
while (generatedLength < keyLength + ivLength) {
|
||||
|
||||
// Digest data (last digest if available, password data, salt if available)
|
||||
if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength)
|
||||
md.update(password)
|
||||
if (salt != null) md.update(salt, 0, 8)
|
||||
md.digest(generatedData, generatedLength, digestLength)
|
||||
|
||||
// additional rounds
|
||||
for (i in 1 until iterations) {
|
||||
md.update(generatedData, generatedLength, digestLength)
|
||||
md.digest(generatedData, generatedLength, digestLength)
|
||||
}
|
||||
generatedLength += digestLength
|
||||
}
|
||||
|
||||
// Copy key and IV into separate byte arrays
|
||||
val result = arrayOfNulls<ByteArray>(2)
|
||||
result[0] = generatedData.copyOfRange(0, keyLength)
|
||||
if (ivLength > 0) result[1] = generatedData.copyOfRange(keyLength, keyLength + ivLength)
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
} finally {
|
||||
// Clean out temporary data
|
||||
Arrays.fill(generatedData, 0.toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user