Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/id/kuramanime/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}

ext {
extName = 'Kuramanime'
pkgNameSuffix = 'id.kuramanime'
extClass = '.Kuramanime'
extVersionCode = 8
extVersionCode = 9
libVersion = '13'
}

dependencies {
implementation(project(":lib-streamtape-extractor"))
}

apply from: "$rootDir/common.gradle"
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.animeextension.id.kuramanime

import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
Expand All @@ -10,17 +10,17 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Headers
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
import java.net.URLEncoder

class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Kuramanime"
Expand All @@ -31,160 +31,189 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {

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)
}

override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
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: ", "")
anime.genre = document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(1)").text().replace("Genre: ", "")
anime.status = status
anime.artist = document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(5)").text()
anime.author = "UNKNOWN"
return anime
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime?page=$page")

private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Sedang Tayang" -> SAnime.ONGOING
"Selesai Tayang" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
override fun popularAnimeSelector() = "div.product__item"

override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.selectFirst("a > div")?.attr("data-setbg")
title = element.selectFirst("div.product__item__text > h5")!!.text()
}

override fun episodeFromElement(element: Element): SEpisode {
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()
override fun popularAnimeNextPageSelector() = "div.product__pagination > a:last-child"

return episode
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/anime?order_by=updated&page=$page")

private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
override fun latestUpdatesSelector() = popularAnimeSelector()

override fun episodeListSelector(): String = "#episodeLists"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)

override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()

val html = document.select(episodeListSelector()).attr("data-content")
val jsoupE = Jsoup.parse(html)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/anime?search=$query&page=$page")

return jsoupE.select("a").filter { ele -> !ele.attr("href").contains("batch") }.map { episodeFromElement(it) }.reversed()
}
override fun searchAnimeSelector() = popularAnimeSelector()

private fun parseShortInfo(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.thumbnail_url = element.selectFirst("a > div")!!.attr("data-setbg")
anime.title = element.select("div.product__item__text > h5").text()
return anime
}
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)

override fun latestUpdatesFromElement(element: Element): SAnime = parseShortInfo(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()

override fun latestUpdatesNextPageSelector(): String = "div.product__pagination > a:last-child"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
thumbnail_url = document.selectFirst("div.anime__details__pic")?.attr("data-setbg")

override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime?order_by=updated&page=$page")
val details = document.selectFirst("div.anime__details__text")!!

override fun latestUpdatesSelector(): String = "div.product__item"
title = details.selectFirst("div > h3")!!.text().replace("Judul: ", "")

override fun popularAnimeFromElement(element: Element): SAnime = parseShortInfo(element)
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())

override fun popularAnimeNextPageSelector(): String = "div.product__pagination > a:last-child"
genre = infos.select("li:contains(Genre:) > a, li:contains(Tema:) > a, li:contains(Demografis:) > a")
.eachText()
.joinToString { it.trimEnd(',', ' ') }
.takeUnless(String::isEmpty)

override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime")
description = buildString {
details.selectFirst("p#synopsisField")?.text()?.also(::append)

override fun popularAnimeSelector(): String = "div.product__item"
details.selectFirst("div.anime__details__title > span")?.text()
?.also { append("\n\nAlternative names: $it\n") }

override fun searchAnimeFromElement(element: Element): SAnime = parseShortInfo(element)
infos.select("ul > li").eachText().forEach { append("\n$it") }
}
}

override fun searchAnimeNextPageSelector(): String = "div.product__pagination > a:last-child"
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Sedang Tayang" -> SAnime.ONGOING
"Selesai Tayang" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}

// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.use { it.asJsoup() }

override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/anime?search=$query&page=$page")
val html = document.selectFirst(episodeListSelector())?.attr("data-content")
?: return emptyList()

val newDoc = response.asJsoup(html)

return newDoc.select("a")
.filterNot { it.attr("href").contains("batch") }
.map(::episodeFromElement)
.reversed()
}

override fun searchAnimeSelector(): String = "div.product__item"
override fun episodeListSelector() = "a#episodeLists"

override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.text()
episode_number = name.filter(Char::isDigit).toFloatOrNull() ?: 1F
}

// ============================ Video Links =============================
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> {
val videoList = mutableListOf<Video>()
val document = response.asJsoup()
val doc = response.use { it.asJsoup() }

document.select("select#changeServer > option").forEach {
videoList.addAll(
videosFromServer(response.request.url.toString(), it.attr("value"), it.text()),
)
}
val servers = doc.select("select#changeServer > option")
.map { it.attr("value") to it.text().substringBefore(" (") }
.filter { supportedHosters.contains(it.first) }

return videoList.sort()
}
val episodeUrl = response.request.url

private fun videosFromServer(episodeUrl: String, server: String, name: String): List<Video> {
val document = client.newCall(
GET("$episodeUrl?activate_stream=1&stream_server=$server", headers = headers),
).execute().asJsoup()
return document.select(videoListSelector()).map { videoFromElement(it, name, episodeUrl) }
}
val headers = headersBuilder()
.set("Referer", episodeUrl.toString())
.set("X-Requested-With", "XMLHttpRequest")
.build()

override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
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 {
newList.add(video)
playerDoc.select("video#player > source").map {
val src = it.attr("src")
Video(src, "${it.attr("size")}p - $serverName", src)
}
}
}
return newList
}.getOrElse { emptyList<Video>() }
}
return this
}

override fun videoFromElement(element: Element) = throw Exception("not used")
private fun getRequestHash(headers: Headers): String {
val auth = "kuramanime:FDWUjAg6FXZpcbyTAkWrsgS8qAJNDDXKts:${System.currentTimeMillis()}"
.let { Base64.encode(it.toByteArray(), Base64.NO_WRAP) }
.let { Base64.encodeToString(it, Base64.NO_WRAP) }
.let { URLEncoder.encode(it, "UTF-8") }

private fun videoFromElement(element: Element, name: String, episodeUrl: String): Video {
var url = element.attr("src")
if (!url.startsWith("http")) {
url = episodeUrl + url
}
val newHeaders = headers.newBuilder()
.set("Authorization", "Bearer $auth")
.set("X-Request-ID", getRandomString())
.build()

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)
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> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!

return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}

override fun videoFromElement(element: Element) = throw Exception("not used")

override fun videoUrlParse(document: Document) = throw Exception("not used")

// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"

setOnPreferenceChangeListener { _, newValue ->
Expand All @@ -193,7 +222,14 @@ class Kuramanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}.also(screen::addPreference)
}

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
}
}