fix(id/kuramanime): Fix episode list in long series + fix video list (#2638)
This commit is contained in:
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user