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 {
extName = 'AnimeOwl'
extClass = '.AnimeOwl'
extVersionCode = 14
extVersionCode = 15
}
apply from: "$rootDir/common.gradle"
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 androidx.preference.ListPreference
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.model.AnimeFilterList
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.Video
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.POST
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.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.float
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
@ -44,7 +40,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeOwl"
override val baseUrl = "https://anime-owl.net"
override val baseUrl = "https://animeowl.us"
override val lang = "en"
@ -56,111 +52,107 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val owlServersExtractor by lazy { OwlExtractor(client, baseUrl) }
// ============================== Popular ===============================
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 {
return SAnime.create().apply {
setUrlWithoutDomain(element.select("div > a").attr("href"))
thumbnail_url = element.select("div.img-container > a > img").attr("src")
title = element.select("a.title-link").text()
setUrlWithoutDomain(element.select("a.title-link").attr("href"))
thumbnail_url = element.select("img[data-src]").attr("data-src")
title = element.select("a.title-link h3").text()
}
}
// =============================== 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 ===============================
override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage {
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)
}
): AnimesPage = advancedSearchAnime(page, sort = Sort.Search, query = query)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
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 ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("h3.anime-name").text()
anime.genre = document.select("div.genre > a").joinToString { it.text() }
anime.description = document.select("div.anime-desc.desc-content").text()
// No author info so use type of anime
anime.author = document.select("div.type > a").text()
anime.status = parseStatus(document.select("div.status > span").text())
// add alternative name to anime description
val altName = "Other name(s): "
document.select("h4.anime-alternatives").text()?.let {
if (it.isBlank().not()) {
anime.description = when {
anime.description.isNullOrBlank() -> altName + it
else -> anime.description + "\n\n$altName" + it
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
genre = document.select("div.genre > a").joinToString { it.text() }
author = document.select("div.type > a").text()
status = parseStatus(document.select("div.status > span").text())
description = buildString {
document.select("div.anime-desc.desc-content").text()
.takeIf { it.isNotBlank() }
?.let {
appendLine(it)
appendLine()
}
document.select("h4.anime-alternatives").text()
.takeIf { it.isNotBlank() }
?.let {
append("Other name(s): ")
append(it)
}
}
}
return anime
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
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 = json.decodeFromString<JsonObject>(episodesJson)
val subList = episodes["sub"]!!.jsonArray
val dubList = episodes["dub"]!!.jsonArray
val subSlug = episodes["sub_slug"]!!.jsonPrimitive.content
val dubSlug = episodes["dub_slug"]!!.jsonPrimitive.content
return subList.map { item ->
val dub = dubList.find {
it.jsonObject["name"]!!.jsonPrimitive.content == item.jsonObject["name"]!!.jsonPrimitive.content
val episodes = client.newCall(
GET("$baseUrl/api/anime/$animeId/episodes"),
).execute()
.parseAs<EpisodeResponse>()
return listOf(
episodes.sub.map { it.copy(lang = "Sub") },
episodes.dub.map { it.copy(lang = "Dub") },
).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 {
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()
.sortedByDescending { it.episode_number }
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
@ -168,34 +160,9 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val urlJson = json.decodeFromString<JsonObject>(episode.url)
val videoList = mutableListOf<Video>()
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 suspend fun getVideoList(episode: SEpisode): List<Video> =
json.decodeFromString<LinkData>(episode.url)
.links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
@ -204,138 +171,87 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private fun extractOwlVideo(link: String, files: List<Pair<String, Headers>>, lang: String): List<Video> {
val videoList = mutableListOf<Video>()
val response = client.newCall(GET(baseUrl + link)).execute().body.string()
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
enum class Sort(val code: String) {
Latest("1"),
Search("4"),
}
private fun extractGogoVideo(url: String, lang: String): List<Video> {
val videoList = mutableListOf<Video>()
val document = client.newCall(GET(url)).execute().asJsoup()
// Vidstreaming:
GogoCdnExtractor(client, json).videosFromUrl(url).map {
videoList.add(
Video(
it.url,
it.quality + " $lang",
it.videoUrl,
headers = it.headers,
),
)
}
// Doodstream mirror:
document.select("div#list-server-more > ul > li.linkserver:contains(Doodstream)")
.firstOrNull()?.attr("data-video")
?.let { link ->
DoodExtractor(client).videosFromUrl(link).map {
videoList.add(
Video(
it.url,
it.quality + " $lang",
it.videoUrl,
headers = it.headers,
),
)
}
private fun advancedSearchAnime(
page: Int,
sort: Sort,
query: String? = "",
limit: Int? = 30,
): AnimesPage {
val body = buildJsonObject {
put("lang22", 3)
put("value", query)
put("sortt", sort.code)
put("limit", limit)
put("page", page - 1)
putJsonObject("selected") {
putJsonArray("type") { emptyList<String>() }
putJsonArray("sort") { emptyList<String>() }
putJsonArray("year") { emptyList<String>() }
putJsonArray("genre") { emptyList<String>() }
putJsonArray("season") { emptyList<String>() }
putJsonArray("status") { emptyList<String>() }
putJsonArray("country") { emptyList<String>() }
putJsonArray("language") { emptyList<String>() }
}
}.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> {
val quality = preferences.getString("preferred_quality", "Luffy")
val lang = preferences.getString("preferred_language", "Sub")
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
val newList = mutableListOf<Video>()
if (quality != null || lang != null) {
val qualityList = mutableListOf<Video>()
var preferred = 0
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
return this.sortedWith(
compareByDescending<Video> { it.quality.contains(lang) }
.thenByDescending { it.quality.contains(quality) }
.thenByDescending { it.quality.contains(server, true) },
)
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Currently Airing" -> SAnime.ONGOING
private fun LinkData.toJsonString(): String {
return json.encodeToString(this)
}
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
// 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
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("Luffy", "Kaido", "Zoro: 720p", "Zoro: 1080p")
entryValues = arrayOf("Luffy", "Kaido", "Zoro: 720p", "Zoro: 1080p")
setDefaultValue("Luffy")
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -344,13 +260,14 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoLangPref = ListPreference(screen.context).apply {
key = "preferred_language"
title = "Preferred Language"
entries = arrayOf("Sub", "Dub")
entryValues = arrayOf("Sub", "Dub")
setDefaultValue("Sub")
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_TYPES
entryValues = PREF_LANG_TYPES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -359,8 +276,39 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(videoLangPref)
}.also(screen::addPreference)
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() }
}
}