fix(id/OtakuDesu): Fix video extractor + refactor (#1669)

This commit is contained in:
Claudemirovsky
2023-06-02 08:09:59 +00:00
committed by GitHub
parent 5a01ebe7df
commit 22820a45ab
2 changed files with 234 additions and 149 deletions

View File

@ -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 = 'OtakuDesu'
pkgNameSuffix = 'id.otakudesu'
extClass = '.OtakuDesu'
extVersionCode = 18
extVersionCode = 19
libVersion = '13'
}
dependencies {
implementation(project(":lib-yourupload-extractor"))
}
apply from: "$rootDir/common.gradle"

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animeextension.id.otakudesu
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
@ -12,11 +13,19 @@ 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.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
@ -41,30 +50,27 @@ class OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val zing = document.select("div.infozingle")
val status = parseStatus(zing.select("p:nth-child(6) > span").text().replace("Status: ", ""))
anime.title = zing.select("p:nth-child(1) > span").text().replace("Judul: ", "")
anime.genre = zing.select("p:nth-child(11) > span").text().replace("Genre: ", "")
anime.status = status
anime.artist = zing.select("p:nth-child(10) > span").text()
anime.author = zing.select("p:nth-child(4) > span").text()
return SAnime.create().apply {
val info = document.selectFirst("div.infozingle")!!
title = info.getInfo("Judul") ?: ""
genre = info.getInfo("Genre")
status = parseStatus(info.getInfo("Status"))
artist = info.getInfo("Studio")
author = info.getInfo("Produser")
// Others
// Jap title
anime.description = document.select("p:nth-child(2) > span").text()
// Score
anime.description = anime.description + "\n" + document.select("p:nth-child(3) > span").text()
// Total Episode
anime.description = anime.description + "\n" + document.select("p:nth-child(7) > span").text()
// Synopsis
anime.description = anime.description + "\n\n\nSynopsis: \n" + document.select("div.sinopc > p").joinToString("\n\n") { it.text() }
return anime
description = buildString {
info.getInfo("Japanese", false)?.let { append("$it\n") }
info.getInfo("Skor", false)?.let { append("$it\n") }
info.getInfo("Total Episode", false)?.let { append("$it\n") }
append("\n\nSynopsis:\n")
document.select("div.sinopc > p").eachText().forEach { append("$it\n\n") }
}
}
}
private fun parseStatus(statusString: String): Int {
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
@ -72,87 +78,67 @@ class OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
// ============================== Episodes ==============================
private val nameRegex by lazy { ".+?(?=Episode)|\\sSubtitle.+".toRegex() }
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val epsNum = getNumberFromEpsString(element.select("span > a").text())
episode.setUrlWithoutDomain(element.select("span > a").attr("href"))
episode.episode_number = when {
(epsNum.isNotEmpty()) -> epsNum.toFloat()
else -> 1F
return SEpisode.create().apply {
val link = element.selectFirst("span > a")!!
val text = link.text()
episode_number = text.substringAfter("Episode ")
.substringBefore(" ")
.toFloatOrNull() ?: 1F
setUrlWithoutDomain(link.attr("href"))
name = text.replace(nameRegex, "")
date_upload = element.selectFirst("span.zeebr")?.text().toDate()
}
episode.name = element.select("span > a").text().replace(".+?(?=Episode)|\\sSubtitle.+".toRegex(), "")
episode.date_upload = reconstructDate(element.select("span.zeebr").text())
return episode
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
private fun reconstructDate(Str: String): Long {
val newStr = Str.replace("Januari", "Jan").replace("Februari", "Feb").replace("Maret", "Mar").replace("April", "Apr").replace("Mei", "May").replace("Juni", "Jun").replace("Juli", "Jul").replace("Agustus", "Aug").replace("September", "Sep").replace("Oktober", "Oct").replace("November", "Nov").replace("Desember", "Dec")
val pattern = SimpleDateFormat("d MMM yyyy", Locale.US)
return pattern.parse(newStr.replace(",", " "))!!.time
}
override fun episodeListSelector(): String = "#venkonten > div.venser > div:nth-child(8) > ul > li"
override fun episodeListSelector() = "#venkonten > div.venser > div:nth-child(8) > ul > li"
// =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("div.thumb > a")!!.attr("href"))
anime.thumbnail_url = element.selectFirst("div.thumb > a > div.thumbz > img")!!.attr("src")
anime.title = element.select("div.thumb > a > div.thumbz > h2").text()
return anime
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.selectFirst("h2")!!.text()
}
}
override fun latestUpdatesNextPageSelector(): String = "a.next.page-numbers"
override fun latestUpdatesNextPageSelector() = "a.next.page-numbers"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/ongoing-anime/page/$page")
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ongoing-anime/page/$page")
override fun latestUpdatesSelector(): String = "div.detpost"
override fun latestUpdatesSelector() = "div.detpost div.thumb > a"
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("div.thumb > a")!!.attr("href"))
anime.thumbnail_url = element.selectFirst("div.thumb > a > div.thumbz > img")!!.attr("src")
anime.title = element.select("div.thumb > a > div.thumbz > h2").text()
return anime
}
override fun popularAnimeNextPageSelector(): String = "a.next.page-numbers"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/complete-anime/page/$page")
override fun popularAnimeSelector(): String = "div.detpost"
// ============================== Popular ===============================
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/complete-anime/page/$page")
override fun popularAnimeSelector() = latestUpdatesSelector()
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("not used")
private fun searchAnimeFromElement(element: Element, ui: String): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(
return SAnime.create().apply {
when (ui) {
"search" -> element.selectFirst("h2 > a")!!.attr("href")
"genres" -> element.select(".col-anime-title > a").attr("href")
else -> element.selectFirst("div.thumb > a")!!.attr("href")
},
)
anime.thumbnail_url = when (ui) {
"search" -> element.selectFirst("img")!!.attr("src")
"genres" -> element.select(".col-anime-cover > img").attr("src")
else -> element.selectFirst("div.thumb > a > div.thumbz > img")!!.attr("src")
"search" -> {
val link = element.selectFirst("h2 > a")!!
setUrlWithoutDomain(link.attr("href"))
title = link.text().replace(" Subtitle Indonesia", "")
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
else -> {
val link = element.selectFirst(".col-anime-title > a")!!
setUrlWithoutDomain(link.attr("href"))
title = link.text()
thumbnail_url = element.selectFirst(".col-anime-cover > img")!!.attr("src")
}
}
}
anime.title = when (ui) {
"search" -> element.select("h2 > a").text().replace(" Subtitle Indonesia", "")
"genres" -> element.select(".col-anime-title > a").text()
else -> element.select("div.thumb > a > div.thumbz > h2").text()
}
return anime
}
override fun searchAnimeNextPageSelector(): String = "a.next.page-numbers"
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
@ -165,94 +151,125 @@ class OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
}
override fun searchAnimeSelector(): String = "#venkonten > div > div.venser > div > div > ul > li"
override fun searchAnimeSelector() = "#venkonten > div > div.venser > div > div > ul > li"
private val genreSelector = ".col-anime"
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val ui = when {
document.select(".col-anime").isNullOrEmpty() -> "search"
document.select("#venkonten > div > div.venser > div > div > ul > li").isNullOrEmpty() -> "genres"
document.selectFirst(genreSelector) == null -> "search"
document.selectFirst(searchAnimeSelector()) == null -> "genres"
else -> "unknown"
}
val animes = when (ui) {
"genres" -> document.select(".col-anime").map { element -> searchAnimeFromElement(element, ui) }
"search" -> document.select("#venkonten > div > div.venser > div > div > ul > li").map { element -> searchAnimeFromElement(element, ui) }
else -> document.select("div.detpost").map { element -> popularAnimeFromElement(element) }
"genres" -> document.select(genreSelector).map { searchAnimeFromElement(it, ui) }
"search" -> document.select(searchAnimeSelector()).map { searchAnimeFromElement(it, ui) }
else -> document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement)
}
val hasNextPage = searchAnimeNextPageSelector().let { selector ->
document.select(selector).first()
} != null
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun videoListSelector() = "div.download > ul > li > a:nth-child(2)"
// ============================ Video Links =============================
override fun videoListSelector() = "div.mirrorstream ul li > a"
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++
} else {
newList.add(video)
override fun videoListParse(response: Response): List<Video> {
val doc = response.use { it.asJsoup() }
val script = doc.selectFirst("script:containsData({action:)")!!
.data()
val nonceAction = script.substringAfter("{action:\"").substringBefore('"')
val action = script.substringAfter("action:\"").substringBefore('"')
val nonce = getNonce(nonceAction)
return doc.select(videoListSelector())
.parallelMapNotNull {
runCatching { getEmbedLinks(it, action, nonce) }.getOrNull()
}
.parallelMapNotNull {
runCatching {
getVideosFromEmbed(it.first, it.second)
}.getOrNull()
}.flatten()
}
private fun getEmbedLinks(element: Element, action: String, nonce: String): Pair<String, String> {
val decodedData = element.attr("data-content").b64Decode()
.drop(1)
.dropLast(1)
val (id, mirror, quality) = decodedData.split(",").map {
it.substringAfter(":").replace("\"", "")
}
val form = FormBody.Builder().apply {
add("id", id)
add("i", mirror)
add("q", quality)
add("nonce", nonce)
add("action", action)
}.build()
val doc = client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", body = form))
.execute()
.body.string()
.substringAfter(":\"")
.substringBefore('"')
.b64Decode()
.let(Jsoup::parse)
val url = doc.selectFirst("iframe")!!.attr("src")
return Pair(quality, url)
}
private fun getVideosFromEmbed(quality: String, link: String): List<Video> {
return when {
"yourupload" in link -> {
val id = link.substringAfter("id=").substringBefore("&")
val url = "https://yourupload.com/embed/$id"
YourUploadExtractor(client).videoFromUrl(url, headers, "YourUpload - $quality")
}
"desustream" in link -> {
client.newCall(GET(link, headers)).execute().use {
val doc = it.asJsoup()
val script = doc.selectFirst("script:containsData(sources)")!!.data()
val videoUrl = script.substringAfter("sources:[{")
.substringAfter("file':'")
.substringBefore("'")
listOf(Video(videoUrl, "DesuStream - $quality", videoUrl, headers))
}
}
return newList
}
return this
}
override fun videoFromElement(element: Element): Video {
val res = client.newCall(GET(element.attr("href"))).execute().asJsoup()
val scr = res.select("script:containsData(dlbutton)").html()
var url = element.attr("href").substringBefore("/v/")
val numbs = scr.substringAfter("\" + (").substringBefore(") + \"")
val firstString = scr.substringAfter(" = \"").substringBefore("\" + (")
val num = numbs.substringBefore(" % ").toInt()
val lastString = scr.substringAfter("913) + \"").substringBefore("\";")
val nums = num % 51245 + num % 913
url += firstString + nums.toString() + lastString
val quality = with(lastString) {
when {
contains("1080p") -> "1080p"
contains("720p") -> "720p"
contains("480p") -> "480p"
contains("360p") -> "360p"
else -> "Default"
"mp4upload" in link -> {
client.newCall(GET(link, headers)).execute().use {
val doc = it.asJsoup()
val script = doc.selectFirst("script:containsData(player.src)")!!.data()
val videoUrl = script.substringAfter("src: \"").substringBefore('"')
listOf(Video(videoUrl, "Mp4upload - $quality", videoUrl, headers))
}
}
else -> emptyList()
}
return Video(url, quality, url)
}
private fun getNonce(action: String): String {
val form = FormBody.Builder().add("action", action).build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", body = form))
.execute()
.use {
it.body.string().substringAfter(":\"").substringBefore('"')
}
}
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
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")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
// filter
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
@ -305,4 +322,66 @@ class OtakuDesu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
// ============================= Utilities ==============================
private fun Element.getInfo(info: String, cut: Boolean = true): String? {
return selectFirst("p > span:has(b:contains($info))")?.text()
?.let {
when {
cut -> it.substringAfter(":")
else -> it
}.trim()
}
}
private fun String?.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this?.trim() ?: "")?.time }
.getOrNull() ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareByDescending { it.quality.equals(quality) },
)
}
private inline fun <A, B> Iterable<A>.parallelMapNotNull(crossinline f: suspend (A) -> B?): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll().filterNotNull()
}
private fun String.b64Decode(): String {
return String(Base64.decode(this, Base64.DEFAULT))
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("d MMM,yyyy", Locale("id", "ID"))
}
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")
}
}