fix(en/animeowl): Fix video extractor without webView and refactor (#2842)

This commit is contained in:
Samfun75 2024-01-29 15:53:09 +03:00 committed by GitHub
parent 388b2213fa
commit 01d32cf2a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 338 additions and 448 deletions

View File

@ -1,11 +1,12 @@
ext { ext {
extName = 'AnimeOwl' extName = 'AnimeOwl'
extClass = '.AnimeOwl' extClass = '.AnimeOwl'
extVersionCode = 14 extVersionCode = 15
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies { dependencies {
implementation(project(':lib:dood-extractor')) implementation(project(":lib:synchrony"))
implementation(project(":lib:playlist-utils"))
} }

View File

@ -4,7 +4,7 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.animeowl.extractors.GogoCdnExtractor import eu.kanade.tachiyomi.animeextension.en.animeowl.extractors.OwlExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -12,26 +12,22 @@ 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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.float import kotlinx.serialization.json.put
import kotlinx.serialization.json.int import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.putJsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
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
@ -44,7 +40,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeOwl" override val name = "AnimeOwl"
override val baseUrl = "https://anime-owl.net" override val baseUrl = "https://animeowl.us"
override val lang = "en" override val lang = "en"
@ -56,111 +52,107 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val owlServersExtractor by lazy { OwlExtractor(client, baseUrl) }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending?page=$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending?page=$page")
override fun popularAnimeSelector(): String = "div#anime-list > div.recent-anime" override fun popularAnimeSelector(): String = "div#anime-list > div"
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.page-item > a[rel=next]" override fun popularAnimeNextPageSelector(): String = "ul.pagination > li > a[rel=next]"
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply { return SAnime.create().apply {
setUrlWithoutDomain(element.select("div > a").attr("href")) setUrlWithoutDomain(element.select("a.title-link").attr("href"))
thumbnail_url = element.select("div.img-container > a > img").attr("src") thumbnail_url = element.select("img[data-src]").attr("data-src")
title = element.select("a.title-link").text() title = element.select("a.title-link h3").text()
} }
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/recent-episode/all") override suspend fun getLatestUpdates(page: Int): AnimesPage =
advancedSearchAnime(page, sort = Sort.Latest)
override fun latestUpdatesSelector(): String = popularAnimeSelector() override fun latestUpdatesRequest(page: Int): Request =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() override fun latestUpdatesSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun latestUpdatesNextPageSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
// =============================== Search =============================== // =============================== Search ===============================
override suspend fun getSearchAnime( override suspend fun getSearchAnime(
page: Int, page: Int,
query: String, query: String,
filters: AnimeFilterList, filters: AnimeFilterList,
): AnimesPage { ): AnimesPage = advancedSearchAnime(page, sort = Sort.Search, query = query)
val limit = 30
val mediaType = "application/json; charset=utf-8".toMediaType()
val body = """{"limit":$limit,"page":${page - 1},"pageCount":0,"value":"$query","sort":4,"selected":{"type":[],"genre":[],"year":[],"country":[],"season":[],"status":[],"sort":[],"language":[]}}""".toRequestBody(mediaType)
val response = client.newCall(POST("$baseUrl/api/advance-search", body = body, headers = headers)).execute()
val result = json.decodeFromString<JsonObject>(response.body.string())
val total = result["total"]!!.jsonPrimitive.int
val nextPage = ceil(total.toFloat() / limit).toInt() > page
val data = result["results"]!!.jsonArray
val animes = data.map { item ->
SAnime.create().apply {
setUrlWithoutDomain("/anime/${item.jsonObject["anime_slug"]!!.jsonPrimitive.content}/")
thumbnail_url = "$baseUrl${item.jsonObject["image"]!!.jsonPrimitive.content}"
title = item.jsonObject["anime_name"]!!.jsonPrimitive.content
}
}
return AnimesPage(animes, nextPage)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
throw UnsupportedOperationException() throw UnsupportedOperationException()
override fun searchAnimeSelector(): String = throw UnsupportedOperationException() override fun searchAnimeSelector(): String =
throw UnsupportedOperationException()
override fun searchAnimeNextPageSelector(): String = throw UnsupportedOperationException() override fun searchAnimeNextPageSelector(): String =
throw UnsupportedOperationException()
override fun searchAnimeFromElement(element: Element): SAnime = throw UnsupportedOperationException() override fun searchAnimeFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
val anime = SAnime.create() genre = document.select("div.genre > a").joinToString { it.text() }
anime.title = document.select("h3.anime-name").text() author = document.select("div.type > a").text()
anime.genre = document.select("div.genre > a").joinToString { it.text() } status = parseStatus(document.select("div.status > span").text())
anime.description = document.select("div.anime-desc.desc-content").text() description = buildString {
// No author info so use type of anime document.select("div.anime-desc.desc-content").text()
anime.author = document.select("div.type > a").text() .takeIf { it.isNotBlank() }
anime.status = parseStatus(document.select("div.status > span").text()) ?.let {
appendLine(it)
// add alternative name to anime description appendLine()
val altName = "Other name(s): " }
document.select("h4.anime-alternatives").text()?.let { document.select("h4.anime-alternatives").text()
if (it.isBlank().not()) { .takeIf { it.isNotBlank() }
anime.description = when { ?.let {
anime.description.isNullOrBlank() -> altName + it append("Other name(s): ")
else -> anime.description + "\n\n$altName" + it append(it)
} }
}
} }
return anime
} }
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val animeId = response.asJsoup().select("div#unq-anime-id").attr("animeId") val animeId = response.asJsoup().select("div#unq-anime-id").attr("animeId")
val episodesJson = client.newCall(GET("$baseUrl/api/anime/$animeId/episodes")).execute().body.string() val episodes = client.newCall(
val episodes = json.decodeFromString<JsonObject>(episodesJson) GET("$baseUrl/api/anime/$animeId/episodes"),
val subList = episodes["sub"]!!.jsonArray ).execute()
val dubList = episodes["dub"]!!.jsonArray .parseAs<EpisodeResponse>()
val subSlug = episodes["sub_slug"]!!.jsonPrimitive.content
val dubSlug = episodes["dub_slug"]!!.jsonPrimitive.content return listOf(
return subList.map { item -> episodes.sub.map { it.copy(lang = "Sub") },
val dub = dubList.find { episodes.dub.map { it.copy(lang = "Dub") },
it.jsonObject["name"]!!.jsonPrimitive.content == item.jsonObject["name"]!!.jsonPrimitive.content ).flatten()
.groupBy { it.name }
.map { (epNum, epList) ->
SEpisode.create().apply {
url = LinkData(
epList.map { ep ->
Link(
ep.buildUrl(episodes.subSlug, episodes.dubSlug),
ep.lang!!,
)
},
).toJsonString()
episode_number = epNum.toFloatOrNull() ?: 0F
name = "Episode $epNum"
}
} }
SEpisode.create().apply { .sortedByDescending { it.episode_number }
url = "{\"Sub\": \"https://portablegaming.co/watch/$subSlug/${item.jsonObject["episode_index"]!!.jsonPrimitive.content}\"," +
if (dub != null) {
"\"Dub\": \"https://portablegaming.co/watch/$dubSlug/${dub.jsonObject["episode_index"]!!.jsonPrimitive.content}\"}"
} else { "\"Dub\": \"\"}" }
episode_number = item.jsonObject["name"]!!.jsonPrimitive.float
name = "Episode " + item.jsonObject["name"]!!.jsonPrimitive.content
}
}.reversed()
} }
override fun episodeListSelector(): String = throw UnsupportedOperationException() override fun episodeListSelector(): String = throw UnsupportedOperationException()
@ -168,34 +160,9 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException() override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links ============================= // ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> { override suspend fun getVideoList(episode: SEpisode): List<Video> =
val urlJson = json.decodeFromString<JsonObject>(episode.url) json.decodeFromString<LinkData>(episode.url)
val videoList = mutableListOf<Video>() .links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
urlJson.mapNotNull { (key, value) ->
val link = value.jsonPrimitive.content
if (link.isNotEmpty()) {
// We won't need the interceptor if the jwt signing key is found
// Look at fileInterceptor.files for the signed jwt string
val fileInterceptor = FileRequestInterceptor()
val owlClient = client.newBuilder().addInterceptor(fileInterceptor).build()
val response = owlClient.newCall(GET(link)).execute().asJsoup()
val sources = response.select("ul.list-server > li > button")
sources.mapNotNull { source ->
if (source.text() == "No Ads") {
videoList.addAll(
extractOwlVideo(source.attr("data-source"), fileInterceptor.files, key),
)
} else {
videoList.addAll(
extractGogoVideo(source.attr("data-source"), key),
)
}
}
}
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException() override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
@ -204,138 +171,87 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException() override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun extractOwlVideo(link: String, files: List<Pair<String, Headers>>, lang: String): List<Video> { enum class Sort(val code: String) {
val videoList = mutableListOf<Video>() Latest("1"),
val response = client.newCall(GET(baseUrl + link)).execute().body.string() Search("4"),
val serverJson = json.decodeFromString<JsonObject>(response)
files.map { (url, headers) ->
if (url.contains("service=2")) {
videoList.add(Video(url, "Kaido - Deafult - $lang", url, headers = headers))
val luffyUrl = url.replace("service=2", "service=1")
videoList.add(Video(luffyUrl, "Luffy - Deafult - $lang", luffyUrl, headers = headers))
} else {
if (url.contains("service=1")) {
videoList.add(Video(url, "Luffy - Deafult - $lang", url, headers = headers))
val kaidoUrl = url.replace("service=1", "service=2")
videoList.add(Video(kaidoUrl, "Kaido - Deafult - $lang", kaidoUrl, headers = headers))
} else {
val luffyUrl = "$url&service=1"
videoList.add(Video(luffyUrl, "Luffy - Deafult - $lang", luffyUrl, headers = headers))
val kaidoUrl = "$url&service=2"
videoList.add(Video(kaidoUrl, "Kaido - Deafult - $lang", kaidoUrl, headers = headers))
}
}
}
if ("vidstream" in serverJson.keys) {
val zoroUrl = serverJson["vidstream"]!!.jsonPrimitive.content
val zoroHeaders = mapOf(
Pair("referer", "https://portablegaming.co/"),
Pair("origin", "https://portablegaming.co"),
Pair("host", zoroUrl.toHttpUrl().host),
Pair("Accept-Language", "en-US,en;q=0.9"),
Pair("User-Agent", " Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"),
)
val zoroResponse = Jsoup.connect(zoroUrl).headers(zoroHeaders).execute().body()
zoroResponse.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map {
val quality = "Zoro: " + it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl))
}
}
return videoList
} }
private fun extractGogoVideo(url: String, lang: String): List<Video> { private fun advancedSearchAnime(
val videoList = mutableListOf<Video>() page: Int,
val document = client.newCall(GET(url)).execute().asJsoup() sort: Sort,
query: String? = "",
// Vidstreaming: limit: Int? = 30,
GogoCdnExtractor(client, json).videosFromUrl(url).map { ): AnimesPage {
videoList.add( val body = buildJsonObject {
Video( put("lang22", 3)
it.url, put("value", query)
it.quality + " $lang", put("sortt", sort.code)
it.videoUrl, put("limit", limit)
headers = it.headers, put("page", page - 1)
), putJsonObject("selected") {
) putJsonArray("type") { emptyList<String>() }
} putJsonArray("sort") { emptyList<String>() }
putJsonArray("year") { emptyList<String>() }
// Doodstream mirror: putJsonArray("genre") { emptyList<String>() }
document.select("div#list-server-more > ul > li.linkserver:contains(Doodstream)") putJsonArray("season") { emptyList<String>() }
.firstOrNull()?.attr("data-video") putJsonArray("status") { emptyList<String>() }
?.let { link -> putJsonArray("country") { emptyList<String>() }
DoodExtractor(client).videosFromUrl(link).map { putJsonArray("language") { emptyList<String>() }
videoList.add(
Video(
it.url,
it.quality + " $lang",
it.videoUrl,
headers = it.headers,
),
)
}
} }
}.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
return videoList val result = client.newCall(
POST("$baseUrl/api/advance-search", body = body, headers = headers),
).execute()
.parseAs<SearchResponse>()
val nextPage = ceil(result.total.toFloat() / limit!!).toInt() > page
val animes = result.results.map { anime ->
SAnime.create().apply {
setUrlWithoutDomain("/anime/${anime.animeSlug}?mal=${anime.malId}")
thumbnail_url = "$baseUrl${anime.image}"
title = anime.animeName
}
}
return AnimesPage(animes, nextPage)
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "Luffy") val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val lang = preferences.getString("preferred_language", "Sub") val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
val newList = mutableListOf<Video>() return this.sortedWith(
if (quality != null || lang != null) { compareByDescending<Video> { it.quality.contains(lang) }
val qualityList = mutableListOf<Video>() .thenByDescending { it.quality.contains(quality) }
var preferred = 0 .thenByDescending { it.quality.contains(server, true) },
for (video in this) { )
if (video.quality.contains(quality!!)) {
qualityList.add(preferred, video)
preferred++
} else {
qualityList.add(video)
}
}
preferred = 0
for (video in qualityList) {
if (video.quality.contains(lang!!)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
} }
private fun parseStatus(statusString: String): Int { private fun LinkData.toJsonString(): String {
return when (statusString) { return json.encodeToString(this)
"Currently Airing" -> SAnime.ONGOING }
private fun EpisodeResponse.Episode.buildUrl(subSlug: String, dubSlug: String): String =
when (lang) {
"dub" -> dubSlug
else -> subSlug
}.let { "$baseUrl/watch/$it/$episodeIndex" }
private fun parseStatus(statusString: String): Int =
when (statusString) {
"Currently Airing", "Not yet aired" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED "Finished Airing" -> SAnime.COMPLETED
// IT says Not yet aired for some animes even tho there is available videos,
// so I choose ONGOING as it's a better fit than the other choices
"Not yet aired" -> SAnime.ONGOING
else -> SAnime.UNKNOWN else -> SAnime.UNKNOWN
} }
}
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("Luffy", "Kaido", "Zoro: 720p", "Zoro: 1080p") entries = PREF_QUALITY_LIST
entryValues = arrayOf("Luffy", "Kaido", "Zoro: 720p", "Zoro: 1080p") entryValues = PREF_QUALITY_LIST
setDefaultValue("Luffy") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -344,13 +260,14 @@ class AnimeOwl : 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)
val videoLangPref = ListPreference(screen.context).apply {
key = "preferred_language" ListPreference(screen.context).apply {
title = "Preferred Language" key = PREF_LANG_KEY
entries = arrayOf("Sub", "Dub") title = PREF_LANG_TITLE
entryValues = arrayOf("Sub", "Dub") entries = PREF_LANG_TYPES
setDefaultValue("Sub") entryValues = PREF_LANG_TYPES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -359,8 +276,39 @@ class AnimeOwl : 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)
screen.addPreference(videoLangPref) ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE
entries = PREF_SERVER_LIST
entryValues = PREF_SERVER_LIST
setDefaultValue(PREF_SERVER_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()
}
}.also(screen::addPreference)
}
companion object {
private const val PREF_LANG_KEY = "preferred_language"
private const val PREF_LANG_TITLE = "Preferred type"
private const val PREF_LANG_DEFAULT = "Sub"
private val PREF_LANG_TYPES = arrayOf("Sub", "Dub")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "Luffy"
private val PREF_SERVER_LIST = arrayOf("Luffy", "Kaido", "Boa")
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_LIST = arrayOf("1080p", "720p", "480p", "360p")
} }
} }

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SearchResponse(
val total: Int,
val results: List<Result>,
) {
@Serializable
data class Result(
@SerialName("mal_id")
val malId: Int,
@SerialName("anime_name")
val animeName: String,
@SerialName("anime_slug")
val animeSlug: String,
val image: String,
)
}
@Serializable
data class EpisodeResponse(
val sub: List<Episode>,
val dub: List<Episode>,
@SerialName("sub_slug")
val subSlug: String,
@SerialName("dub_slug")
val dubSlug: String,
) {
@Serializable
data class Episode(
val id: Int,
val name: String,
val lang: String? = null,
@SerialName("episode_index")
val episodeIndex: String,
)
}
@Serializable
data class LinkData(
val links: List<Link>,
)
@Serializable
data class Link(
val url: String,
val lang: String,
)
@Serializable
data class OwlServers(
val kaido: String? = null,
val luffy: String? = null,
val zoro: String? = null,
)
@Serializable
data class Stream(
val url: String,
)

View File

@ -1,86 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class FileRequestInterceptor : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
val files = mutableListOf<Pair<String, Headers>>()
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest)
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = request.header("User-Agent")
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/88.0.705.63\""
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("monkey-d-luffy.site")) {
files.add(Pair(request.url.toString(), request.requestHeaders.toHeaders()))
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(10, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return request
}
}

View File

@ -1,123 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.lang.Exception
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ExperimentalSerializationApi
class GogoCdnExtractor(private val client: OkHttpClient, private val json: Json) {
fun videosFromUrl(serverUrl: String): List<Video> {
try {
val document = client.newCall(GET(serverUrl)).execute().asJsoup()
val iv = document.select("div.wrapper")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val secretKey = document.select("body[class]")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val decryptionKey = document.select("div.videocontent")
.attr("class").substringAfter("videocontent-")
.filter { it.isDigit() }.toByteArray()
val encryptAjaxParams = cryptoHandler(
document.select("script[data-value]")
.attr("data-value"),
iv,
secretKey,
false,
).substringAfter("&")
val httpUrl = serverUrl.toHttpUrl()
val host = "https://" + httpUrl.host + "/"
val id = httpUrl.queryParameter("id") ?: throw Exception("error getting id")
val encryptedId = cryptoHandler(id, iv, secretKey)
val token = httpUrl.queryParameter("token")
val qualityPrefix = if (token != null) "Gogostream: " else "Vidstreaming: "
val jsonResponse = client.newCall(
GET(
"${host}encrypt-ajax.php?id=$encryptedId&$encryptAjaxParams&alias=$id",
Headers.headersOf(
"X-Requested-With",
"XMLHttpRequest",
),
),
).execute().body.string()
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
val decryptedData = cryptoHandler(data, iv, decryptionKey, false)
val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(decryptedData)["source"]!!.jsonArray
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
val fileURL = array[0].jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(fileURL)).execute().body.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
if (!videoUrl.startsWith("http")) {
videoUrl = fileURL.substringBeforeLast("/") + "/$videoUrl"
}
videoList.add(Video(videoUrl, qualityPrefix + quality, videoUrl))
}
} else {
array.forEach {
val label = it.jsonObject["label"].toString().lowercase(Locale.ROOT)
.trim('"').replace(" ", "")
val fileURL = it.jsonObject["file"].toString().trim('"')
val videoHeaders = Headers.headersOf("Referer", serverUrl)
if (label == "auto") {
autoList.add(
Video(
fileURL,
qualityPrefix + label,
fileURL,
headers = videoHeaders,
),
)
} else {
videoList.add(Video(fileURL, qualityPrefix + label, fileURL, headers = videoHeaders))
}
}
}
return videoList.sortedByDescending {
it.quality.substringAfter(qualityPrefix).substringBefore("p").toIntOrNull() ?: -1
} + autoList
} catch (e: Exception) {
return emptyList()
}
}
private fun cryptoHandler(
string: String,
iv: ByteArray,
secretKeyString: ByteArray,
encrypt: Boolean = true,
): String {
val ivParameterSpec = IvParameterSpec(iv)
val secretKey = SecretKeySpec(secretKeyString, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP)
}
}
}

View File

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl.extractors
import eu.kanade.tachiyomi.animeextension.en.animeowl.Link
import eu.kanade.tachiyomi.animeextension.en.animeowl.OwlServers
import eu.kanade.tachiyomi.animeextension.en.animeowl.Stream
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.OkHttpClient
class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String) {
private val playlistUtils by lazy { PlaylistUtils(client) }
private val noRedirectClient by lazy {
client.newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.build()
}
suspend fun extractOwlVideo(link: Link): List<Video> {
val dataSrc = client.newCall(GET(link.url)).execute()
.asJsoup()
.select("button#hot-anime-tab")
.attr("data-source")
val epJS = dataSrc.substringAfterLast("/")
.let {
client.newCall(GET("$baseUrl/players/$it.v2.js")).execute().body.string()
}
.let(Deobfuscator::deobfuscateScript)
?: throw Exception("Unable to get clean JS")
val jwt = JWT_REGEX.find(epJS)?.groupValues?.get(1) ?: throw Exception("Unable to get jwt")
val videoList = mutableListOf<Video>()
val servers = client.newCall(GET("$baseUrl$dataSrc")).execute()
.parseAs<OwlServers>()
coroutineScope {
val lufDeferred = async {
servers.luffy?.let { luffy ->
noRedirectClient.newCall(GET("${luffy}$jwt")).execute()
.use { it.headers["Location"] }
?.let { videoList.add(Video(it, "Luffy - ${link.lang} - 1080p", it)) }
}
}
val kaiDeferred = async {
servers.kaido?.let {
videoList.addAll(
getHLS("${it}$jwt", "Kaido", link.lang),
)
}
}
val zorDeferred = async {
servers.zoro?.let {
videoList.addAll(
getHLS("${it}$jwt", "Boa", link.lang),
)
}
}
awaitAll(lufDeferred, kaiDeferred, zorDeferred)
}
return videoList
}
private fun getHLS(url: String, server: String, lang: String): List<Video> =
client.newCall(GET(url)).execute()
.parseAs<Stream>()
.url
.let {
playlistUtils.extractFromHls(
it,
videoNameGen = { qty -> "$server - $lang - $qty" },
)
}
companion object {
private val JWT_REGEX by lazy { "const\\s+(?:[A-Za-z0-9_]*)\\s*=\\s*'([^']+)'".toRegex() }
}
}