parent
5e0d900321
commit
c6ad6636ec
4 changed files with 50 additions and 55 deletions
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeOwl'
|
extName = 'AnimeOwl'
|
||||||
extClass = '.AnimeOwl'
|
extClass = '.AnimeOwl'
|
||||||
extVersionCode = 18
|
extVersionCode = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -17,7 +17,6 @@ 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.parallelFlatMap
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
@ -35,12 +34,11 @@ import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
|
||||||
class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "AnimeOwl"
|
override val name = "AnimeOwl"
|
||||||
|
|
||||||
override val baseUrl = "https://animeowl.us"
|
override val baseUrl = "https://animeowl.live"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
|
@ -109,6 +107,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
genre = document.select("div.genre > a").joinToString { it.text() }
|
genre = document.select("div.genre > a").joinToString { it.text() }
|
||||||
author = document.select("div.type > a").text()
|
author = document.select("div.type > a").text()
|
||||||
status = parseStatus(document.select("div.status > span").text())
|
status = parseStatus(document.select("div.status > span").text())
|
||||||
|
thumbnail_url = document.selectFirst(".cover-img-container > img")?.attr("abs:src")
|
||||||
description = buildString {
|
description = buildString {
|
||||||
document.select("div.anime-desc.desc-content").text()
|
document.select("div.anime-desc.desc-content").text()
|
||||||
.takeIf { it.isNotBlank() }
|
.takeIf { it.isNotBlank() }
|
||||||
|
@ -127,32 +126,33 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
// ============================== 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 document = response.asJsoup()
|
||||||
val episodes = client.newCall(
|
val sub = document.select("#anime-cover-sub-content .episode-node").mapIndexed { idx, it ->
|
||||||
GET("$baseUrl/api/anime/$animeId/episodes"),
|
EpisodeResponse.Episode(
|
||||||
).execute()
|
id = it.text().toDouble(),
|
||||||
.parseAs<EpisodeResponse>()
|
episodeIndex = idx.toString(),
|
||||||
|
name = it.text(),
|
||||||
return listOf(
|
lang = "Sub",
|
||||||
episodes.sub.map { it.copy(lang = "Sub") },
|
href = it.attr("abs:href"),
|
||||||
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()
|
val dub = document.select("#anime-cover-dub-content .episode-node").mapIndexed { idx, it ->
|
||||||
|
EpisodeResponse.Episode(
|
||||||
|
id = it.text().toDouble(),
|
||||||
|
episodeIndex = idx.toString(),
|
||||||
|
name = it.text(),
|
||||||
|
lang = "Dub",
|
||||||
|
href = it.attr("abs:href"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listOf(sub, dub).flatten().groupBy { it.name }.map { (epNum, epList) ->
|
||||||
|
SEpisode.create().apply {
|
||||||
|
url = LinkData(epList.map { ep -> Link(ep.href!!, ep.lang!!) }).toJsonString()
|
||||||
episode_number = epNum.toFloatOrNull() ?: 0F
|
episode_number = epNum.toFloatOrNull() ?: 0F
|
||||||
name = "Episode $epNum"
|
name = "Episode $epNum"
|
||||||
}
|
}
|
||||||
}
|
}.sortedByDescending { it.episode_number }
|
||||||
.sortedByDescending { it.episode_number }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListSelector(): String = throw UnsupportedOperationException()
|
override fun episodeListSelector(): String = throw UnsupportedOperationException()
|
||||||
|
@ -160,9 +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> {
|
||||||
json.decodeFromString<LinkData>(episode.url)
|
return json.decodeFromString<LinkData>(episode.url).links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
|
||||||
.links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
|
}
|
||||||
|
|
||||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@ -228,15 +228,7 @@ class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LinkData.toJsonString(): String {
|
private fun LinkData.toJsonString(): String = json.encodeToString(this)
|
||||||
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 =
|
private fun parseStatus(statusString: String): Int =
|
||||||
when (statusString) {
|
when (statusString) {
|
||||||
|
|
|
@ -31,11 +31,12 @@ data class EpisodeResponse(
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Episode(
|
data class Episode(
|
||||||
val id: Int,
|
val id: Double? = null,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lang: String? = null,
|
val lang: String? = null,
|
||||||
@SerialName("episode_index")
|
@SerialName("episode_index")
|
||||||
val episodeIndex: String,
|
val episodeIndex: String,
|
||||||
|
val href: String? = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,18 +36,18 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
|
||||||
}
|
}
|
||||||
.let(Deobfuscator::deobfuscateScript)
|
.let(Deobfuscator::deobfuscateScript)
|
||||||
?: throw Exception("Unable to get clean JS")
|
?: throw Exception("Unable to get clean JS")
|
||||||
val jwt = JWT_REGEX.find(epJS)?.groupValues?.get(1) ?: throw Exception("Unable to get jwt")
|
|
||||||
|
val jwt = findFirstJwt(epJS) ?: throw Exception("Unable to get jwt")
|
||||||
|
|
||||||
val videoList = mutableListOf<Video>()
|
val videoList = mutableListOf<Video>()
|
||||||
val servers = client.newCall(GET("$baseUrl$dataSrc")).execute()
|
val servers = client.newCall(GET("$baseUrl$dataSrc")).execute()
|
||||||
.parseAs<OwlServers>()
|
.parseAs<OwlServers>()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
val lufDeferred = async {
|
val lufDeferred = async {
|
||||||
servers.luffy?.let { luffy ->
|
servers.luffy?.let { luffy ->
|
||||||
noRedirectClient.newCall(GET("${luffy}$jwt")).execute()
|
noRedirectClient.newCall(GET("${luffy}$jwt")).execute()
|
||||||
.use { it.headers["Location"] }
|
.use { it.headers["Location"] }
|
||||||
?.let { videoList.add(Video(it, "Luffy - ${link.lang} - 1080p", it)) }
|
?.let { videoList.add(Video(it, "${link.lang} Luffy:1080p", it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val kaiDeferred = async {
|
val kaiDeferred = async {
|
||||||
|
@ -70,18 +70,20 @@ class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String
|
||||||
return videoList
|
return videoList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHLS(url: String, server: String, lang: String): List<Video> =
|
private fun getHLS(url: String, server: String, lang: String): List<Video> {
|
||||||
client.newCall(GET(url)).execute()
|
return client.newCall(GET(url)).execute().let {
|
||||||
.parseAs<Stream>()
|
if (it.isSuccessful) {
|
||||||
.url
|
it.parseAs<Stream>().url.let {
|
||||||
.let {
|
playlistUtils.extractFromHls(it, videoNameGen = { qty -> "$lang $server:$qty" })
|
||||||
playlistUtils.extractFromHls(
|
}
|
||||||
it,
|
} else {
|
||||||
videoNameGen = { qty -> "$server - $lang - $qty" },
|
emptyList()
|
||||||
)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private fun findFirstJwt(text: String): String? {
|
||||||
private val JWT_REGEX by lazy { "const\\s+(?:[A-Za-z0-9_]*)\\s*=\\s*'([^']+)'".toRegex() }
|
val jwtPattern = Regex("['\"]([A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+)['\"]")
|
||||||
|
return jwtPattern.find(text)?.groupValues?.get(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue