fix(id/kuramanime): Fix episode list in long series + fix video list (#2638)

This commit is contained in:
Claudemirovsky
2023-12-14 06:58:50 -03:00
committed by GitHub
parent 03c6a5e8ae
commit d9cbdcb9b5
2 changed files with 84 additions and 26 deletions

View File

@ -1,13 +1,14 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
} }
ext { ext {
extName = 'Kuramanime' extName = 'Kuramanime'
pkgNameSuffix = 'id.kuramanime' pkgNameSuffix = 'id.kuramanime'
extClass = '.Kuramanime' extClass = '.Kuramanime'
extVersionCode = 10 extVersionCode = 11
libVersion = '13' libVersion = '13'
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.animeextension.id.kuramanime package eu.kanade.tachiyomi.animeextension.id.kuramanime
import android.app.Application import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
@ -13,14 +12,16 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
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.lang.Exception import java.lang.Exception
import java.net.URLEncoder
class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Kuramanime" override val name = "Kuramanime"
@ -37,6 +38,8 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val json: Json by injectLazy()
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime?page=$page") override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime?page=$page")
@ -112,10 +115,31 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val newDoc = response.asJsoup(html) val newDoc = response.asJsoup(html)
return newDoc.select("a") val limits = newDoc.select("a.btn-secondary")
.filterNot { it.attr("href").contains("batch") }
.map(::episodeFromElement) return when {
.reversed() limits.isEmpty() -> { // 12 episodes or less
newDoc.select("a")
.filterNot { it.attr("href").contains("batch") }
.map(::episodeFromElement)
.reversed()
}
else -> { // More than 12 episodes
val (start, end) = limits.eachText().take(2).map {
it.filter(Char::isDigit).toInt()
}
val location = document.location()
(end downTo start).map { episodeNumber ->
SEpisode.create().apply {
name = "Ep $episodeNumber"
episode_number = episodeNumber.toFloat()
setUrlWithoutDomain("$location/episode/$episodeNumber")
}
}
}
}
} }
override fun episodeListSelector() = "a#episodeLists" override fun episodeListSelector() = "a#episodeLists"
@ -137,6 +161,14 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val doc = response.use { it.asJsoup() } val doc = response.use { it.asJsoup() }
val scriptData = doc.selectFirst("[data-js]")?.attr("data-js")
?.let(::getScriptData)
?: return emptyList()
val csrfToken = doc.selectFirst("meta[name=csrf-token]")
?.attr("csrf-token")
?: return emptyList()
val servers = doc.select("select#changeServer > option") val servers = doc.select("select#changeServer > option")
.map { it.attr("value") to it.text().substringBefore(" (") } .map { it.attr("value") to it.text().substringBefore(" (") }
.filter { supportedHosters.contains(it.first) } .filter { supportedHosters.contains(it.first) }
@ -150,9 +182,20 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return servers.flatMap { (server, serverName) -> return servers.flatMap { (server, serverName) ->
runCatching { runCatching {
val newHeaders = headers.newBuilder()
.set("X-CSRF-TOKEN", csrfToken)
.set("X-Fuck-ID", scriptData.tokenId)
.set("X-Request-ID", getRandomString())
.set("X-Request-Index", "0")
.build()
val hash = client.newCall(GET("$baseUrl/" + scriptData.authPath, newHeaders)).execute()
.use { it.body.string() }
.trim('"')
val newUrl = episodeUrl.newBuilder() val newUrl = episodeUrl.newBuilder()
.addQueryParameter("dfgRr1OagZvvxbzHNpyCy0FqJQ18mCnb", getRequestHash(headers)) .addQueryParameter(scriptData.tokenParam, hash)
.addQueryParameter("twEvZlbZbYRWBdKKwxkOnwYF0VWoGGVg", server) .addQueryParameter(scriptData.serverParam, server)
.build() .build()
val playerDoc = client.newCall(GET(newUrl.toString(), headers)).execute() val playerDoc = client.newCall(GET(newUrl.toString(), headers)).execute()
@ -171,27 +214,41 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
private val scriptToken by lazy { private fun getScriptData(scriptName: String): ScriptDataDto? {
client.newCall(GET("$baseUrl/assets/js/arc-signal.min.js")).execute() val scriptUrl = "$baseUrl/assets/js/$scriptName.js"
val scriptCode = client.newCall(GET(scriptUrl, headers)).execute()
.use { it.body.string() } .use { it.body.string() }
.substringAfter("kuramanime:\"+\"")
.substringBefore('"') // Trust me, I hate this too.
val scriptJson = scriptCode.lines()
.filter { it.contains(": '") || it.contains(": \"") }
.map {
val (key, value) = it.split(":", limit = 2).map(String::trim)
val fixedValue = value.replace("'", "\"").substringBeforeLast(',')
"\"$key\": $fixedValue"
}.joinToString(prefix = "{", postfix = "}")
return runCatching {
json.decodeFromString<ScriptDataDto>(scriptJson)
}.onFailure { it.printStackTrace() }.getOrNull()
} }
private fun getRequestHash(headers: Headers): String { @Serializable
val auth = "kuramanime:${scriptToken}ts:${System.currentTimeMillis()}" internal data class ScriptDataDto(
.let { Base64.encode(it.toByteArray(), Base64.NO_WRAP) } @SerialName("MIX_PREFIX_AUTH_ROUTE_PARAM")
.let { Base64.encodeToString(it, Base64.NO_WRAP) } private val authPathPrefix: String,
.let { URLEncoder.encode(it, "UTF-8") }
val newHeaders = headers.newBuilder() @SerialName("MIX_AUTH_ROUTE_PARAM")
.set("Authorization", "Bearer $auth") private val authPathSuffix: String,
.set("X-Request-ID", getRandomString())
.build()
return client.newCall(GET("$baseUrl/misc/post/EVhcpMNbO77acNZcHr2XVjaG8WAdNC1u", newHeaders)).execute() @SerialName("MIX_AUTH_KEY") private val authKey: String,
.use { it.body.string() } @SerialName("MIX_AUTH_TOKEN") private val authToken: String,
.trim('"')
@SerialName("MIX_PAGE_TOKEN_KEY") val tokenParam: String,
@SerialName("MIX_STREAM_SERVER_KEY") val serverParam: String,
) {
val authPath = authPathPrefix + authPathSuffix
val tokenId = "$authKey:$authToken"
} }
private fun getRandomString(length: Int = 8): String { private fun getRandomString(length: Int = 8): String {