|
|
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
|
|
|
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
|
|
|
|
import eu.kanade.tachiyomi.network.GET
|
|
|
|
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
|
|
|
import eu.kanade.tachiyomi.util.asJsoup
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
import kotlinx.coroutines.awaitAll
|
|
|
|
@ -24,13 +23,10 @@ import kotlinx.serialization.SerialName
|
|
|
|
|
import kotlinx.serialization.Serializable
|
|
|
|
|
import kotlinx.serialization.decodeFromString
|
|
|
|
|
import kotlinx.serialization.json.Json
|
|
|
|
|
import kotlinx.serialization.json.JsonObject
|
|
|
|
|
import kotlinx.serialization.json.jsonPrimitive
|
|
|
|
|
import okhttp3.Headers
|
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
|
import okhttp3.Request
|
|
|
|
|
import okhttp3.Response
|
|
|
|
|
import org.jsoup.Jsoup
|
|
|
|
|
import org.jsoup.nodes.Document
|
|
|
|
|
import org.jsoup.nodes.Element
|
|
|
|
|
import rx.Observable
|
|
|
|
@ -65,8 +61,6 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
// ============================== Popular ===============================
|
|
|
|
|
|
|
|
|
|
override fun popularAnimeRequest(page: Int): Request {
|
|
|
|
|
// make the vrf webview available beforehand
|
|
|
|
|
vrfInterceptor.wake()
|
|
|
|
|
return GET("$baseUrl/filter?sort=trending&page=$page")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -84,8 +78,6 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
// =============================== Latest ===============================
|
|
|
|
|
|
|
|
|
|
override fun latestUpdatesRequest(page: Int): Request {
|
|
|
|
|
// make the vrf webview available beforehand.
|
|
|
|
|
vrfInterceptor.wake()
|
|
|
|
|
return GET("$baseUrl/filter?sort=recently_updated&page=$page")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -131,11 +123,7 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
|
|
|
|
|
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 searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
|
|
|
|
|
|
|
|
|
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
|
|
|
|
|
|
|
|
@ -167,37 +155,30 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
// ============================== 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),
|
|
|
|
|
)
|
|
|
|
|
val link = "https://api.consumet.org/anime/9anime" + anime.url.replace("watch", "info", true)
|
|
|
|
|
return GET(link, headers = Headers.headersOf("url", anime.url))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun episodeListSelector() = "div.episodes ul > li > a"
|
|
|
|
|
override fun episodeListSelector() = throw Exception("not used")
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
val responseObject = json.decodeFromString<EpisodeResponse>(response.body.string())
|
|
|
|
|
return responseObject.episodes.parallelMap { episodeFromElements(it, animeUrl) }.reversed()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
|
|
|
|
|
|
|
|
|
|
private fun episodeFromElements(element: Element, url: String): SEpisode {
|
|
|
|
|
private fun episodeFromElements(item: EpisodeResponse.Episode, 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"
|
|
|
|
|
val epNum = item.epNum
|
|
|
|
|
val ids = item.subId + if (item.dubId.isNullOrBlank()) "" else ",${item.dubId}"
|
|
|
|
|
val sub = item.subId.isNotBlank()
|
|
|
|
|
val dub = !item.dubId.isNullOrBlank()
|
|
|
|
|
episode.url = "$ids&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 name = item.title
|
|
|
|
|
val namePrefix = "Episode $epNum"
|
|
|
|
|
episode.name = "Episode $epNum" +
|
|
|
|
|
if (name.isNotEmpty() && name != namePrefix) ": $name" else ""
|
|
|
|
@ -206,36 +187,28 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
|
|
|
|
|
// ============================ 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")}"
|
|
|
|
|
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
|
|
|
|
val ids = episode.url.substringBefore("&").split(",")
|
|
|
|
|
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()
|
|
|
|
|
val resp = client.newCall(GET("https://api.consumet.org/anime/9anime/servers/$subId")).execute()
|
|
|
|
|
val parsed = json.decodeFromString<List<Servers>>(resp.body.string())
|
|
|
|
|
parsed.map { serverElement ->
|
|
|
|
|
val server = serverElement.name.let {
|
|
|
|
|
if (it == "vidstream") "vizcloud" else it.lowercase()
|
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
val resp = client.newCall(GET("https://api.consumet.org/anime/9anime/servers/$dubId")).execute()
|
|
|
|
|
val parsed = json.decodeFromString<List<Servers>>(resp.body.string())
|
|
|
|
|
parsed.map { serverElement ->
|
|
|
|
|
val server = serverElement.name.let {
|
|
|
|
|
if (it == "vidstream") "vizcloud" else it.lowercase()
|
|
|
|
|
}
|
|
|
|
|
servers.add(Triple("Dub", dubId, server))
|
|
|
|
|
}
|
|
|
|
@ -244,18 +217,16 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
servers.filter {
|
|
|
|
|
listOf("vizcloud", "filemoon", "streamtape").contains(it.third)
|
|
|
|
|
}.parallelMap { videoList.addAll(extractVideoConsumet(it)) }
|
|
|
|
|
if (videoList.isNotEmpty()) return videoList
|
|
|
|
|
|
|
|
|
|
if (videoList.isNotEmpty()) return Observable.just(videoList.sort())
|
|
|
|
|
|
|
|
|
|
// If the above fail fallback to webview method
|
|
|
|
|
// Sub
|
|
|
|
|
document.selectFirst("div[data-type=sub] > ul > li[data-sv-id=41]")
|
|
|
|
|
?.attr("data-link-id")
|
|
|
|
|
?.let { videoList.addAll(extractVizVideo("Sub", epurl)) }
|
|
|
|
|
ids.getOrNull(0)?.let { videoList.addAll(extractVizVideo("Sub", epurl)) }
|
|
|
|
|
// Dub
|
|
|
|
|
document.selectFirst("div[data-type=dub] > ul > li[data-sv-id=41]")
|
|
|
|
|
?.attr("data-link-id")
|
|
|
|
|
?.let { videoList.addAll(extractVizVideo("Dub", epurl)) }
|
|
|
|
|
return videoList
|
|
|
|
|
ids.getOrNull(1)?.let { videoList.addAll(extractVizVideo("Dub", epurl)) }
|
|
|
|
|
|
|
|
|
|
return Observable.just(videoList.sort())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun videoListSelector() = throw Exception("not used")
|
|
|
|
@ -330,8 +301,6 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")!!
|
|
|
|
@ -350,6 +319,26 @@ class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Serializable
|
|
|
|
|
data class EpisodeResponse(
|
|
|
|
|
val episodes: List<Episode>,
|
|
|
|
|
) {
|
|
|
|
|
@Serializable
|
|
|
|
|
data class Episode(
|
|
|
|
|
@SerialName("id")
|
|
|
|
|
val subId: String,
|
|
|
|
|
val dubId: String? = null,
|
|
|
|
|
@SerialName("number")
|
|
|
|
|
val epNum: Int,
|
|
|
|
|
val title: String,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Serializable
|
|
|
|
|
data class Servers(
|
|
|
|
|
val name: String,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@Serializable
|
|
|
|
|
data class WatchResponse(
|
|
|
|
|
val headers: Header,
|
|
|
|
|