Nine: reorganize the code (#1341)
* nine: reorganize the code use consumet to get vizcloud playlist link and other servers embed links fix filter not working when search query is empty * Change Vizcloud to Vidstream to match site
This commit is contained in:
parent
f4e38d617a
commit
a5c2427e70
@ -1,16 +1,19 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = '9anime'
|
extName = '9anime'
|
||||||
pkgNameSuffix = 'en.nineanime'
|
pkgNameSuffix = 'en.nineanime'
|
||||||
extClass = '.NineAnime'
|
extClass = '.NineAnime'
|
||||||
extVersionCode = 31
|
extVersionCode = 32
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly libs.bundles.coroutines
|
compileOnly libs.bundles.coroutines
|
||||||
|
implementation (project(':lib-streamtape-extractor'))
|
||||||
|
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -21,6 +21,7 @@ class JsVrfInterceptor(private val baseUrl: String) {
|
|||||||
fun wake() = ""
|
fun wake() = ""
|
||||||
|
|
||||||
fun getVrf(query: String): String {
|
fun getVrf(query: String): String {
|
||||||
|
if (query.isBlank()) return ""
|
||||||
val jscript = getJs(query)
|
val jscript = getJs(query)
|
||||||
val cdl = CountDownLatch(1)
|
val cdl = CountDownLatch(1)
|
||||||
var vrf = ""
|
var vrf = ""
|
||||||
|
@ -11,6 +11,8 @@ 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.streamtapeextractor.StreamTapeExtractor
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.nineanime.extractors.Mp4uploadExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@ -18,6 +20,8 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -58,161 +62,40 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
return Headers.Builder().add("Referer", baseUrl)
|
return Headers.Builder().add("Referer", baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String = "div.ani.items > div"
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
// make the vrf webview available beforehand. please find another solution for this :)
|
// make the vrf webview available beforehand
|
||||||
vrfInterceptor.wake()
|
vrfInterceptor.wake()
|
||||||
return GET("$baseUrl/filter?sort=trending&page=$page")
|
return GET("$baseUrl/filter?sort=trending&page=$page")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = "div.ani.items > div"
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||||
setUrlWithoutDomain(element.select("a.name").attr("href").substringBefore("?"))
|
setUrlWithoutDomain(element.select("a.name").attr("href").substringBefore("?"))
|
||||||
thumbnail_url = element.select("div.poster img").attr("src")
|
thumbnail_url = element.select("div.poster img").attr("src")
|
||||||
title = element.select("a.name").text()
|
title = element.select("a.name").text()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String = "nav > ul.pagination > li > a[aria-label=pagination.next]"
|
override fun popularAnimeNextPageSelector(): String =
|
||||||
|
"nav > ul.pagination > li > a[aria-label=pagination.next]"
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
// =============================== Latest ===============================
|
||||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup().selectFirst("div[data-id]").attr("data-id")
|
|
||||||
val vrf = vrfInterceptor.getVrf(id)
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/ajax/episode/list/$id?vrf=${java.net.URLEncoder.encode(vrf, "utf-8")}", headers = Headers.headersOf("url", anime.url))
|
// make the vrf webview available beforehand.
|
||||||
|
vrfInterceptor.wake()
|
||||||
|
return GET("$baseUrl/filter?sort=recently_updated&page=$page")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||||
runBlocking {
|
|
||||||
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||||
val animeUrl = response.request.header("url").toString()
|
|
||||||
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
|
||||||
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
|
|
||||||
val episodeElements = document.select(episodeListSelector())
|
|
||||||
return episodeElements.parallelMap { episodeFromElements(it, animeUrl) }.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListSelector() = "div.episodes ul > li > a"
|
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||||
|
|
||||||
private fun episodeFromElements(element: Element, url: String): SEpisode {
|
// =============================== Search ===============================
|
||||||
val episode = SEpisode.create()
|
|
||||||
val epNum = element.attr("data-num")
|
|
||||||
val ids = element.attr("data-ids")
|
|
||||||
val sub = element.attr("data-sub").toInt().toBoolean()
|
|
||||||
val dub = element.attr("data-dub").toInt().toBoolean()
|
|
||||||
episode.url = "/ajax/server/list/$ids?vrf=&epurl=$url/ep-$epNum"
|
|
||||||
episode.episode_number = epNum.toFloat()
|
|
||||||
val langPrefix = "[" + if (sub) {
|
|
||||||
"Sub"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
} + if (dub) {
|
|
||||||
",Dub"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
} + "]"
|
|
||||||
val name = element.parent()?.select("span.d-title")?.text().orEmpty()
|
|
||||||
val namePrefix = "Episode $epNum"
|
|
||||||
episode.name = "Episode $epNum" + if (sub || dub) {
|
|
||||||
": $langPrefix"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
} + if (name.isNotEmpty() && name != namePrefix) {
|
|
||||||
" $name"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
return episode
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
|
|
||||||
|
|
||||||
private fun Int.toBoolean() = this == 1
|
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
|
||||||
val ids = episode.url.substringAfter("list/").substringBefore("?vrf")
|
|
||||||
val vrf = vrfInterceptor.getVrf(ids)
|
|
||||||
val url = "/ajax/server/list/$ids?vrf=${java.net.URLEncoder.encode(vrf, "utf-8")}"
|
|
||||||
val epurl = episode.url.substringAfter("epurl=")
|
|
||||||
return GET(baseUrl + url, headers = Headers.headersOf("url", epurl))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val epurl = response.request.header("url").toString()
|
|
||||||
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
|
||||||
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
|
|
||||||
val videoList = mutableListOf<Video>()
|
|
||||||
|
|
||||||
// Sub
|
|
||||||
document.select("div[data-type=sub] > ul > li[data-sv-id=41]")
|
|
||||||
.firstOrNull()?.attr("data-link-id")
|
|
||||||
?.let { videoList.addAll(extractVideo("Sub", epurl)) }
|
|
||||||
// Dub
|
|
||||||
document.select("div[data-type=dub] > ul > li[data-sv-id=41]")
|
|
||||||
.firstOrNull()?.attr("data-link-id")
|
|
||||||
?.let { videoList.addAll(extractVideo("Dub", epurl)) }
|
|
||||||
return videoList
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractVideo(lang: String, epurl: String): List<Video> {
|
|
||||||
val jsInterceptor = client.newBuilder().addInterceptor(JsInterceptor(lang.lowercase())).build()
|
|
||||||
val result = jsInterceptor.newCall(GET("$baseUrl$epurl")).execute()
|
|
||||||
val masterUrl = result.request.url.toString()
|
|
||||||
val masterPlaylist = result.body!!.string()
|
|
||||||
return masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
|
||||||
.split("#EXT-X-STREAM-INF:").map {
|
|
||||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p $lang"
|
|
||||||
val videoUrl = masterUrl.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n")
|
|
||||||
Video(videoUrl, quality, videoUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector() = throw Exception("not used")
|
|
||||||
|
|
||||||
override fun videoFromElement(element: Element) = throw Exception("not used")
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document) = throw Exception("not used")
|
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
|
||||||
val quality = preferences.getString("preferred_quality", "1080")
|
|
||||||
val lang = preferences.getString("preferred_language", "Sub")
|
|
||||||
if (quality != null && lang != null) {
|
|
||||||
val newList = mutableListOf<Video>()
|
|
||||||
var preferred = 0
|
|
||||||
for (video in this) {
|
|
||||||
if (video.quality.contains(quality) && video.quality.contains(lang)) {
|
|
||||||
newList.add(preferred, video)
|
|
||||||
preferred++
|
|
||||||
} else {
|
|
||||||
newList.add(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If dub is preferred language and anime do not have dub version, respect preferred quality
|
|
||||||
if (lang == "Dub" && newList.first().quality.contains("Dub").not()) {
|
|
||||||
newList.clear()
|
|
||||||
for (video in this) {
|
|
||||||
if (video.quality.contains(quality)) {
|
|
||||||
newList.add(preferred, video)
|
|
||||||
preferred++
|
|
||||||
} else {
|
|
||||||
newList.add(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newList
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(element.select("div.poster a").attr("href"))
|
|
||||||
thumbnail_url = element.select("div.poster img").attr("src")
|
|
||||||
title = element.select("a.name").text()
|
|
||||||
}
|
|
||||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
|
||||||
|
|
||||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||||
val params = NineAnimeFilters.getSearchParameters(filters)
|
val params = NineAnimeFilters.getSearchParameters(filters)
|
||||||
@ -223,10 +106,11 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
||||||
|
throw Exception("Not used")
|
||||||
|
|
||||||
private fun searchAnimeRequest(page: Int, query: String, filters: NineAnimeFilters.FilterSearchParams): Request {
|
private fun searchAnimeRequest(page: Int, query: String, filters: NineAnimeFilters.FilterSearchParams): Request {
|
||||||
val vrf = vrfInterceptor.getVrf(query)
|
val vrf = if (query.isNotBlank()) vrfInterceptor.getVrf(query) else ""
|
||||||
|
|
||||||
var url = "$baseUrl/filter?keyword=$query"
|
var url = "$baseUrl/filter?keyword=$query"
|
||||||
|
|
||||||
@ -239,11 +123,26 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
if (filters.language.isNotBlank()) url += filters.language
|
if (filters.language.isNotBlank()) url += filters.language
|
||||||
if (filters.rating.isNotBlank()) url += filters.rating
|
if (filters.rating.isNotBlank()) url += filters.rating
|
||||||
|
|
||||||
return GET("$url&sort=${filters.sort}&vrf=${java.net.URLEncoder.encode(vrf, "utf-8")}&page=$page", headers = Headers.headersOf("Referer", "$baseUrl/"))
|
return GET(
|
||||||
|
"$url&sort=${filters.sort}&vrf=${java.net.URLEncoder.encode(vrf, "utf-8")}&page=$page",
|
||||||
|
headers = Headers.headersOf("Referer", "$baseUrl/")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(element.select("div.poster a").attr("href"))
|
||||||
|
thumbnail_url = element.select("div.poster img").attr("src")
|
||||||
|
title = element.select("a.name").text()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||||
|
|
||||||
override fun getFilterList(): AnimeFilterList = NineAnimeFilters.filterList
|
override fun getFilterList(): AnimeFilterList = NineAnimeFilters.filterList
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime {
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
anime.title = document.select("h1.title").text()
|
anime.title = document.select("h1.title").text()
|
||||||
@ -265,6 +164,178 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
return anime
|
return anime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
|
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
||||||
|
.selectFirst("div[data-id]").attr("data-id")
|
||||||
|
val vrf = vrfInterceptor.getVrf(id)
|
||||||
|
return GET(
|
||||||
|
"$baseUrl/ajax/episode/list/$id?vrf=${java.net.URLEncoder.encode(vrf, "utf-8")}",
|
||||||
|
headers = Headers.headersOf("url", anime.url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListSelector() = "div.episodes ul > li > a"
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val animeUrl = response.request.header("url").toString()
|
||||||
|
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
||||||
|
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
|
||||||
|
val episodeElements = document.select(episodeListSelector())
|
||||||
|
return episodeElements.parallelMap { episodeFromElements(it, animeUrl) }.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
|
||||||
|
|
||||||
|
private fun episodeFromElements(element: Element, url: String): SEpisode {
|
||||||
|
val episode = SEpisode.create()
|
||||||
|
val epNum = element.attr("data-num")
|
||||||
|
val ids = element.attr("data-ids")
|
||||||
|
val sub = element.attr("data-sub").toInt().toBoolean()
|
||||||
|
val dub = element.attr("data-dub").toInt().toBoolean()
|
||||||
|
episode.url = "/ajax/server/list/$ids?vrf=&epurl=$url/ep-$epNum"
|
||||||
|
episode.episode_number = epNum.toFloat()
|
||||||
|
episode.scanlator = (if (sub) "Sub" else "") + if (dub) ", Dub" else ""
|
||||||
|
val name = element.parent()?.select("span.d-title")?.text().orEmpty()
|
||||||
|
val namePrefix = "Episode $epNum"
|
||||||
|
episode.name = "Episode $epNum" +
|
||||||
|
if (name.isNotEmpty() && name != namePrefix) ": $name" else ""
|
||||||
|
return episode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
|
val ids = episode.url.substringAfter("list/").substringBefore("?vrf")
|
||||||
|
val vrf = vrfInterceptor.getVrf(ids)
|
||||||
|
val url = "/ajax/server/list/$ids?vrf=${java.net.URLEncoder.encode(vrf, "utf-8")}"
|
||||||
|
val epurl = episode.url.substringAfter("epurl=")
|
||||||
|
return GET(baseUrl + url, headers = Headers.headersOf("url", epurl))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val epurl = response.request.header("url").toString()
|
||||||
|
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
||||||
|
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
|
||||||
|
val servers = mutableListOf<Triple<String, String, String>>()
|
||||||
|
val ids = response.request.url.encodedPath.substringAfter("list/")
|
||||||
|
.substringBefore("?")
|
||||||
|
.split(",")
|
||||||
|
ids.getOrNull(0)?.let { subId ->
|
||||||
|
document.select("li[data-ep-id=$subId]").map { serverElement ->
|
||||||
|
val server = serverElement.text().let {
|
||||||
|
if (it == "Vidstream") "vizcloud" else it?.lowercase() ?: "vizcloud"
|
||||||
|
}
|
||||||
|
servers.add(Triple("Sub", subId, server))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids.getOrNull(1)?.let { dubId ->
|
||||||
|
document.select("li[data-ep-id=$dubId]").map { serverElement ->
|
||||||
|
val server = serverElement.text().let {
|
||||||
|
if (it == "Vidstream") "vizcloud" else it?.lowercase() ?: "vizcloud"
|
||||||
|
}
|
||||||
|
servers.add(Triple("Dub", dubId, server))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servers.filter {
|
||||||
|
listOf("vizcloud", "filemoon", "streamtape").contains(it.third)
|
||||||
|
}.parallelMap { videoList.addAll(extractVideoConsumet(it)) }
|
||||||
|
if (videoList.isNotEmpty()) return videoList
|
||||||
|
|
||||||
|
// If the above fail fallback to webview method
|
||||||
|
// Sub
|
||||||
|
document.select("div[data-type=sub] > ul > li[data-sv-id=41]")
|
||||||
|
.firstOrNull()?.attr("data-link-id")
|
||||||
|
?.let { videoList.addAll(extractVizVideo("Sub", epurl)) }
|
||||||
|
// Dub
|
||||||
|
document.select("div[data-type=dub] > ul > li[data-sv-id=41]")
|
||||||
|
.firstOrNull()?.attr("data-link-id")
|
||||||
|
?.let { videoList.addAll(extractVizVideo("Dub", epurl)) }
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoListSelector() = throw Exception("not used")
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element) = throw Exception("not used")
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document) = throw Exception("not used")
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private fun extractVizVideo(lang: String, epurl: String): List<Video> {
|
||||||
|
val jsInterceptor =
|
||||||
|
client.newBuilder().addInterceptor(JsInterceptor(lang.lowercase())).build()
|
||||||
|
val result = jsInterceptor.newCall(GET("$baseUrl$epurl")).execute()
|
||||||
|
val masterUrl = result.request.url.toString()
|
||||||
|
val masterPlaylist = result.body!!.string()
|
||||||
|
return parseVizPlaylist(masterPlaylist, masterUrl, "Vidstream - $lang")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractVideoConsumet(server: Triple<String, String, String>): List<Video> {
|
||||||
|
val response = client.newCall(
|
||||||
|
GET("https://api.consumet.org/anime/9anime/watch/${server.second}?server=${server.third}")
|
||||||
|
).execute()
|
||||||
|
if (response.code != 200) return emptyList()
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val parsed = json.decodeFromString<WatchResponse>(response.body!!.string())
|
||||||
|
val embedLink = parsed.embedURL ?: parsed.headers.referer
|
||||||
|
when (server.third) {
|
||||||
|
"vizcloud" -> {
|
||||||
|
parsed.sources?.map { source ->
|
||||||
|
val playlist = client.newCall(GET(source.url)).execute()
|
||||||
|
videoList.addAll(
|
||||||
|
parseVizPlaylist(
|
||||||
|
playlist.body!!.string(),
|
||||||
|
playlist.request.url.toString(),
|
||||||
|
"Vidstream - ${server.first}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"filemoon" -> FilemoonExtractor(client)
|
||||||
|
.videoFromUrl(embedLink, "Filemoon - ${server.first}").let {
|
||||||
|
videoList.addAll(it)
|
||||||
|
}
|
||||||
|
"streamtape" -> StreamTapeExtractor(client)
|
||||||
|
.videoFromUrl(embedLink, "StreamTape - ${server.first}")?.let {
|
||||||
|
videoList.add(it)
|
||||||
|
}
|
||||||
|
// For later use if we can get the embed link
|
||||||
|
"mp4upload" -> Mp4uploadExtractor(client)
|
||||||
|
.videoFromUrl(embedLink, "Mp4Upload - ${server.first}").let {
|
||||||
|
videoList.addAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseVizPlaylist(masterPlaylist: String, masterUrl: String, prefix: String): List<Video> {
|
||||||
|
return masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
||||||
|
.split("#EXT-X-STREAM-INF:").map {
|
||||||
|
val quality = "$prefix " + it.substringAfter("RESOLUTION=")
|
||||||
|
.substringAfter("x").substringBefore("\n") + "p"
|
||||||
|
val videoUrl = masterUrl.substringBeforeLast("/") + "/" +
|
||||||
|
it.substringAfter("\n").substringBefore("\n")
|
||||||
|
Video(videoUrl, quality, videoUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toBoolean() = this == 1
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||||
|
val lang = preferences.getString("preferred_language", "Sub")!!
|
||||||
|
|
||||||
|
return this.sortedWith(
|
||||||
|
compareByDescending<Video> { it.quality.contains(quality) }
|
||||||
|
.thenBy { it.quality.contains(lang) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseStatus(statusString: String): Int {
|
private fun parseStatus(statusString: String): Int {
|
||||||
return when (statusString) {
|
return when (statusString) {
|
||||||
"Releasing" -> SAnime.ONGOING
|
"Releasing" -> SAnime.ONGOING
|
||||||
@ -273,18 +344,28 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
@Serializable
|
||||||
|
data class WatchResponse(
|
||||||
|
val headers: Header,
|
||||||
|
val sources: List<Source>? = null,
|
||||||
|
val embedURL: String? = null
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Header(
|
||||||
|
@SerialName("Referer")
|
||||||
|
val referer: String,
|
||||||
|
@SerialName("User-Agent")
|
||||||
|
val agent: String
|
||||||
|
)
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
@Serializable
|
||||||
|
data class Source(
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
val url: String,
|
||||||
// make the vrf webview available beforehand. please find another solution for this :)
|
@SerialName("isM3U8")
|
||||||
vrfInterceptor.wake()
|
val hls: Boolean
|
||||||
return GET("$baseUrl/filter?sort=recently_updated&page=$page")
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
val domainPref = ListPreference(screen.context).apply {
|
val domainPref = ListPreference(screen.context).apply {
|
||||||
key = "preferred_domain"
|
key = "preferred_domain"
|
||||||
@ -335,4 +416,8 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
screen.addPreference(videoQualityPref)
|
screen.addPreference(videoQualityPref)
|
||||||
screen.addPreference(videoLanguagePref)
|
screen.addPreference(videoLanguagePref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = runBlocking {
|
||||||
|
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||||
|
|
||||||
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CaptionElement(
|
||||||
|
val file: String,
|
||||||
|
val label: String,
|
||||||
|
val kind: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class FilemoonExtractor(private val client: OkHttpClient) {
|
||||||
|
fun videoFromUrl(url: String, prefix: String = "Filemoon"): List<Video> {
|
||||||
|
try {
|
||||||
|
val unpacked = client.newCall(GET(url)).execute().asJsoup().select("script:containsData(eval)").mapNotNull { element ->
|
||||||
|
element?.data()
|
||||||
|
?.let { JsUnpacker.unpackAndCombine(it) }
|
||||||
|
}.first { it.contains("{file:") }
|
||||||
|
|
||||||
|
val subtitleTracks = mutableListOf<Track>()
|
||||||
|
if (unpacked.contains("fetch('")) {
|
||||||
|
val subtitleString = unpacked.substringAfter("fetch('").substringBefore("').")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (subtitleString.isNotEmpty()) {
|
||||||
|
val subResponse = client.newCall(
|
||||||
|
GET(subtitleString)
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
val subtitles = Json.decodeFromString<List<CaptionElement>>(subResponse.body!!.string())
|
||||||
|
for (sub in subtitles) {
|
||||||
|
subtitleTracks.add(Track(sub.file, sub.label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val masterUrl = unpacked.substringAfter("{file:\"").substringBefore("\"}")
|
||||||
|
|
||||||
|
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body!!.string()
|
||||||
|
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
|
||||||
|
val subtitleRegex = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
|
||||||
|
try {
|
||||||
|
subtitleTracks.addAll(
|
||||||
|
subtitleRegex.findAll(masterPlaylist).map {
|
||||||
|
Track(
|
||||||
|
it.groupValues[2],
|
||||||
|
it.groupValues[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (_: Error) {}
|
||||||
|
|
||||||
|
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
|
||||||
|
.forEach {
|
||||||
|
val quality = "$prefix " + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
|
||||||
|
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
try {
|
||||||
|
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitleTracks))
|
||||||
|
} catch (e: Error) {
|
||||||
|
videoList.add(Video(videoUrl, quality, videoUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.nineanime.extractors
|
||||||
|
|
||||||
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class Mp4uploadExtractor(private val client: OkHttpClient) {
|
||||||
|
fun videoFromUrl(url: String, prefix: String = "Original (Mp4upload)"): List<Video> {
|
||||||
|
val headers = Headers.headersOf("referer", "https://mp4upload.com/")
|
||||||
|
val body = client.newCall(GET(url, headers = headers)).execute().body!!.string()
|
||||||
|
val packed = body.substringAfter("eval(function(p,a,c,k,e,d)")
|
||||||
|
.substringBefore("</script>")
|
||||||
|
val unpacked = JsUnpacker.unpackAndCombine("eval(function(p,a,c,k,e,d)$packed")
|
||||||
|
?: return emptyList()
|
||||||
|
val videoUrl = unpacked.substringAfter("player.src(\"").substringBefore("\");")
|
||||||
|
return listOf(
|
||||||
|
Video(videoUrl, prefix, videoUrl, headers)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user