fix(id/kuramanime): Fix video extractor + refactor (#2525)

This commit is contained in:
Claudemirovsky
2023-11-19 06:15:48 -03:00
committed by GitHub
parent cdc6c9c1a0
commit 5a7c722b17
2 changed files with 168 additions and 126 deletions

View File

@ -1,12 +1,18 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
ext { ext {
extName = 'Kuramanime' extName = 'Kuramanime'
pkgNameSuffix = 'id.kuramanime' pkgNameSuffix = 'id.kuramanime'
extClass = '.Kuramanime' extClass = '.Kuramanime'
extVersionCode = 8 extVersionCode = 9
libVersion = '13' libVersion = '13'
} }
dependencies {
implementation(project(":lib-streamtape-extractor"))
}
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.animeextension.id.kuramanime package eu.kanade.tachiyomi.animeextension.id.kuramanime
import android.app.Application import android.app.Application
import android.content.SharedPreferences 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
@ -10,17 +10,17 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
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.OkHttpClient import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
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 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"
@ -31,24 +31,71 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client = network.cloudflareClient
private val preferences: SharedPreferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun animeDetailsParse(document: Document): SAnime { // ============================== Popular ===============================
val anime = SAnime.create() override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime?page=$page")
val status = parseStatus(document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(3)").text().replace("Status: ", ""))
anime.title = document.select("div.anime__details__title > h3").text().replace("Judul: ", "") override fun popularAnimeSelector() = "div.product__item"
anime.genre = document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(1)").text().replace("Genre: ", "")
anime.status = status override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
anime.artist = document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(5)").text() setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.author = "UNKNOWN" thumbnail_url = element.selectFirst("a > div")?.attr("data-setbg")
return anime title = element.selectFirst("div.product__item__text > h5")!!.text()
} }
private fun parseStatus(statusString: String): Int { override fun popularAnimeNextPageSelector() = "div.product__pagination > a:last-child"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/anime?order_by=updated&page=$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/anime?search=$query&page=$page")
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
thumbnail_url = document.selectFirst("div.anime__details__pic")?.attr("data-setbg")
val details = document.selectFirst("div.anime__details__text")!!
title = details.selectFirst("div > h3")!!.text().replace("Judul: ", "")
val infos = details.selectFirst("div.anime__details__widget")!!
artist = infos.select("li:contains(Studio:) > a").eachText().joinToString().takeUnless(String::isEmpty)
status = parseStatus(infos.selectFirst("li:contains(Status:) > a")?.text())
genre = infos.select("li:contains(Genre:) > a, li:contains(Tema:) > a, li:contains(Demografis:) > a")
.eachText()
.joinToString { it.trimEnd(',', ' ') }
.takeUnless(String::isEmpty)
description = buildString {
details.selectFirst("p#synopsisField")?.text()?.also(::append)
details.selectFirst("div.anime__details__title > span")?.text()
?.also { append("\n\nAlternative names: $it\n") }
infos.select("ul > li").eachText().forEach { append("\n$it") }
}
}
private fun parseStatus(statusString: String?): Int {
return when (statusString) { return when (statusString) {
"Sedang Tayang" -> SAnime.ONGOING "Sedang Tayang" -> SAnime.ONGOING
"Selesai Tayang" -> SAnime.COMPLETED "Selesai Tayang" -> SAnime.COMPLETED
@ -56,135 +103,117 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun episodeFromElement(element: Element): SEpisode { // ============================== Episodes ==============================
val episode = SEpisode.create()
val epsNum = getNumberFromEpsString(element.text())
episode.setUrlWithoutDomain(element.attr("href"))
episode.episode_number = when {
epsNum.isNotEmpty() -> epsNum.toFloatOrNull() ?: 1F
else -> 1F
}
episode.name = element.text()
return episode
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
override fun episodeListSelector(): String = "#episodeLists"
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.use { it.asJsoup() }
val html = document.select(episodeListSelector()).attr("data-content") val html = document.selectFirst(episodeListSelector())?.attr("data-content")
val jsoupE = Jsoup.parse(html) ?: return emptyList()
return jsoupE.select("a").filter { ele -> !ele.attr("href").contains("batch") }.map { episodeFromElement(it) }.reversed() val newDoc = response.asJsoup(html)
return newDoc.select("a")
.filterNot { it.attr("href").contains("batch") }
.map(::episodeFromElement)
.reversed()
} }
private fun parseShortInfo(element: Element): SAnime { override fun episodeListSelector() = "a#episodeLists"
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href")) override fun episodeFromElement(element: Element) = SEpisode.create().apply {
anime.thumbnail_url = element.selectFirst("a > div")!!.attr("data-setbg") setUrlWithoutDomain(element.attr("href"))
anime.title = element.select("div.product__item__text > h5").text() name = element.text()
return anime episode_number = name.filter(Char::isDigit).toFloatOrNull() ?: 1F
} }
override fun latestUpdatesFromElement(element: Element): SAnime = parseShortInfo(element) // ============================ Video Links =============================
override fun latestUpdatesNextPageSelector(): String = "div.product__pagination > a:last-child"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime?order_by=updated&page=$page")
override fun latestUpdatesSelector(): String = "div.product__item"
override fun popularAnimeFromElement(element: Element): SAnime = parseShortInfo(element)
override fun popularAnimeNextPageSelector(): String = "div.product__pagination > a:last-child"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime")
override fun popularAnimeSelector(): String = "div.product__item"
override fun searchAnimeFromElement(element: Element): SAnime = parseShortInfo(element)
override fun searchAnimeNextPageSelector(): String = "div.product__pagination > a:last-child"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/anime?search=$query&page=$page")
override fun searchAnimeSelector(): String = "div.product__item"
override fun videoListSelector() = "video#player > source" override fun videoListSelector() = "video#player > source"
// Shall we add "archive", "archive-v2"? archive.org usually returns a beautiful 403 xD
private val supportedHosters = listOf("kuramadrive", "kuramadrive-v2", "streamtape")
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>() val doc = response.use { it.asJsoup() }
val document = response.asJsoup()
document.select("select#changeServer > option").forEach { val servers = doc.select("select#changeServer > option")
videoList.addAll( .map { it.attr("value") to it.text().substringBefore(" (") }
videosFromServer(response.request.url.toString(), it.attr("value"), it.text()), .filter { supportedHosters.contains(it.first) }
)
val episodeUrl = response.request.url
val headers = headersBuilder()
.set("Referer", episodeUrl.toString())
.set("X-Requested-With", "XMLHttpRequest")
.build()
return servers.flatMap { (server, serverName) ->
runCatching {
val newUrl = episodeUrl.newBuilder()
.addQueryParameter("dfgRr1OagZvvxbzHNpyCy0FqJQ18mCnb", getRequestHash(headers))
.addQueryParameter("twEvZlbZbYRWBdKKwxkOnwYF0VWoGGVg", server)
.build()
val playerDoc = client.newCall(GET(newUrl.toString(), headers)).execute()
.use { it.asJsoup() }
if (server == "streamtape") {
val url = playerDoc.selectFirst("div.video-content iframe")!!.attr("src")
streamtapeExtractor.videosFromUrl(url)
} else {
playerDoc.select("video#player > source").map {
val src = it.attr("src")
Video(src, "${it.attr("size")}p - $serverName", src)
}
}
}.getOrElse { emptyList<Video>() }
} }
return videoList.sort()
} }
private fun videosFromServer(episodeUrl: String, server: String, name: String): List<Video> { private fun getRequestHash(headers: Headers): String {
val document = client.newCall( val auth = "kuramanime:FDWUjAg6FXZpcbyTAkWrsgS8qAJNDDXKts:${System.currentTimeMillis()}"
GET("$episodeUrl?activate_stream=1&stream_server=$server", headers = headers), .let { Base64.encode(it.toByteArray(), Base64.NO_WRAP) }
).execute().asJsoup() .let { Base64.encodeToString(it, Base64.NO_WRAP) }
return document.select(videoListSelector()).map { videoFromElement(it, name, episodeUrl) } .let { URLEncoder.encode(it, "UTF-8") }
val newHeaders = headers.newBuilder()
.set("Authorization", "Bearer $auth")
.set("X-Request-ID", getRandomString())
.build()
return client.newCall(GET("$baseUrl/misc/post/EVhcpMNbO77acNZcHr2XVjaG8WAdNC1u", newHeaders)).execute()
.use { it.body.string() }
.trim('"')
}
private fun getRandomString(length: Int = 8): String {
val allowedChars = ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null) val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
if (quality != null) {
val newList = mutableListOf<Video>() return sortedWith(
var preferred = 0 compareBy { it.quality.contains(quality) },
for (video in this) { ).reversed()
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
} }
override fun videoFromElement(element: Element) = throw Exception("not used") override fun videoFromElement(element: Element) = throw Exception("not used")
private fun videoFromElement(element: Element, name: String, episodeUrl: String): Video {
var url = element.attr("src")
if (!url.startsWith("http")) {
url = episodeUrl + url
}
val quality = with(element.attr("size")) {
when {
contains("1080") -> "1080p"
contains("720") -> "720p"
contains("480") -> "480p"
contains("360") -> "360p"
else -> "Default"
}
} + " - $name"
return Video(url, quality, url)
}
override fun videoUrlParse(document: Document) = throw Exception("not used") override fun videoUrlParse(document: Document) = throw Exception("not used")
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = "preferred_quality" key = PREF_QUALITY_KEY
title = "Preferred quality" title = PREF_QUALITY_TITLE
entries = arrayOf("1080p", "720p", "480p", "360p") entries = PREF_QUALITY_ENTRIES
entryValues = arrayOf("1080", "720", "480", "360") entryValues = PREF_QUALITY_VALUES
setDefaultValue("1080") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -193,7 +222,14 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(videoQualityPref) }
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
} }
} }