feat(en/aniwave): Code refactor + small new features (#2237)
This commit is contained in:
@ -1,20 +1,22 @@
|
|||||||
apply plugin: 'com.android.application'
|
plugins {
|
||||||
apply plugin: 'kotlin-android'
|
alias(libs.plugins.android.application)
|
||||||
apply plugin: 'kotlinx-serialization'
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'Aniwave'
|
extName = 'Aniwave'
|
||||||
pkgNameSuffix = 'en.nineanime'
|
pkgNameSuffix = 'en.nineanime'
|
||||||
extClass = '.Aniwave'
|
extClass = '.Aniwave'
|
||||||
extVersionCode = 49
|
extVersionCode = 50
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib-filemoon-extractor'))
|
implementation(project(':lib-filemoon-extractor'))
|
||||||
implementation(project(':lib-mp4upload-extractor'))
|
implementation(project(':lib-mp4upload-extractor'))
|
||||||
implementation (project(':lib-streamtape-extractor'))
|
implementation(project(':lib-streamtape-extractor'))
|
||||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
implementation(project(':lib-playlist-utils'))
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -2,47 +2,38 @@ package eu.kanade.tachiyomi.animeextension.en.nineanime
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.MultiSelectListPreference
|
import androidx.preference.MultiSelectListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.nineanime.extractors.VidsrcExtractor
|
||||||
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.SAnime
|
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.Track
|
|
||||||
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.filemoonextractor.FilemoonExtractor
|
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
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 rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
@ -50,8 +41,9 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
override val id: Long = 98855593379717478
|
override val id: Long = 98855593379717478
|
||||||
|
|
||||||
override val baseUrl
|
override val baseUrl by lazy {
|
||||||
get() = preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||||
|
}
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -61,35 +53,36 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val utils by lazy { AniwaveUtils(client, headers) }
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
private val refererHeaders = headers.newBuilder().apply {
|
||||||
.add("Referer", "$baseUrl/")
|
add("Referer", "$baseUrl/")
|
||||||
|
}.build()
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/filter?sort=trending&page=$page")
|
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/filter?sort=trending&page=$page", refererHeaders)
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String = "div.ani.items > div.item"
|
override fun popularAnimeSelector(): String = "div.ani.items > div.item"
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||||
setUrlWithoutDomain(
|
element.select("a.name").let { a ->
|
||||||
element.select("a.name")
|
setUrlWithoutDomain(a.attr("href").substringBefore("?"))
|
||||||
.attr("href")
|
title = a.text()
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String =
|
override fun popularAnimeNextPageSelector(): String =
|
||||||
"nav > ul.pagination > li > a[rel=next]" // TODO The last 2 pages will be ignored, need to override fetchPopular to fix
|
"nav > ul.pagination > li.active ~ li"
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/filter?sort=recently_updated&page=$page")
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/filter?sort=recently_updated&page=$page", refererHeaders)
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
@ -99,20 +92,10 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val params = AniwaveFilters.getSearchParameters(filters)
|
val filters = AniwaveFilters.getSearchParameters(filters)
|
||||||
return client.newCall(searchAnimeRequest(page, query, params))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
searchAnimeParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
val vrf = if (query.isNotBlank()) utils.callEnimax(query, "vrf") else ""
|
||||||
throw Exception("Not used")
|
|
||||||
|
|
||||||
private fun searchAnimeRequest(page: Int, query: String, filters: AniwaveFilters.FilterSearchParams): Request {
|
|
||||||
val vrf = if (query.isNotBlank()) callEnimax(query, "vrf") else ""
|
|
||||||
var url = "$baseUrl/filter?keyword=$query"
|
var url = "$baseUrl/filter?keyword=$query"
|
||||||
|
|
||||||
if (filters.genre.isNotBlank()) url += filters.genre
|
if (filters.genre.isNotBlank()) url += filters.genre
|
||||||
@ -124,10 +107,7 @@ class Aniwave : 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(
|
return GET("$url&sort=${filters.sort}&page=$page&$vrf", refererHeaders)
|
||||||
"$url&sort=${filters.sort}&page=$page&$vrf",
|
|
||||||
headers = Headers.headersOf("Referer", "$baseUrl/"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
@ -165,19 +145,23 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
||||||
.selectFirst("div[data-id]")!!.attr("data-id")
|
.selectFirst("div[data-id]")!!.attr("data-id")
|
||||||
val vrf = callEnimax(id, "vrf")
|
val vrf = utils.callEnimax(id, "vrf")
|
||||||
return GET(
|
|
||||||
"$baseUrl/ajax/episode/list/$id?$vrf",
|
val listHeaders = headers.newBuilder().apply {
|
||||||
headers = Headers.headersOf("url", anime.url),
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
)
|
add("Referer", baseUrl + anime.url)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET("$baseUrl/ajax/episode/list/$id?$vrf#${anime.url}", listHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListSelector() = "div.episodes ul > li > a"
|
override fun episodeListSelector() = "div.episodes ul > li > a"
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val animeUrl = response.request.header("url").toString()
|
val animeUrl = response.request.url.fragment!!
|
||||||
val responseObject = json.decodeFromString<JsonObject>(response.body.string())
|
val document = response.parseAs<ResultResponse>().toDocument()
|
||||||
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
|
|
||||||
val episodeElements = document.select(episodeListSelector())
|
val episodeElements = document.select(episodeListSelector())
|
||||||
return episodeElements.parallelMap { episodeFromElements(it, animeUrl) }.reversed()
|
return episodeElements.parallelMap { episodeFromElements(it, animeUrl) }.reversed()
|
||||||
}
|
}
|
||||||
@ -185,10 +169,14 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
|
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
|
||||||
|
|
||||||
private fun episodeFromElements(element: Element, url: String): SEpisode {
|
private fun episodeFromElements(element: Element, url: String): SEpisode {
|
||||||
|
val title = element.parent()?.attr("title") ?: ""
|
||||||
|
|
||||||
val epNum = element.attr("data-num")
|
val epNum = element.attr("data-num")
|
||||||
val ids = element.attr("data-ids")
|
val ids = element.attr("data-ids")
|
||||||
val sub = element.attr("data-sub").toInt().toBoolean()
|
val sub = if (element.attr("data-sub").toInt().toBoolean()) "Sub" else ""
|
||||||
val dub = element.attr("data-dub").toInt().toBoolean()
|
val dub = if (element.attr("data-dub").toInt().toBoolean()) "Dub" else ""
|
||||||
|
val softSub = if (SOFTSUB_REGEX.find(title) != null) "SoftSub" else ""
|
||||||
|
|
||||||
val extraInfo = if (element.hasClass("filler") && preferences.getBoolean(PREF_MARK_FILLERS_KEY, PREF_MARK_FILLERS_DEFAULT)) {
|
val extraInfo = if (element.hasClass("filler") && preferences.getBoolean(PREF_MARK_FILLERS_KEY, PREF_MARK_FILLERS_DEFAULT)) {
|
||||||
" • Filler Episode"
|
" • Filler Episode"
|
||||||
} else {
|
} else {
|
||||||
@ -202,7 +190,10 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
if (name.isNotEmpty() && name != namePrefix) ": $name" else ""
|
if (name.isNotEmpty() && name != namePrefix) ": $name" else ""
|
||||||
this.url = "$ids&epurl=$url/ep-$epNum"
|
this.url = "$ids&epurl=$url/ep-$epNum"
|
||||||
episode_number = epNum.toFloat()
|
episode_number = epNum.toFloat()
|
||||||
scanlator = ((if (sub) "Sub" else "") + if (dub) ", Dub" else "") + extraInfo
|
date_upload = RELEASE_REGEX.find(title)?.let {
|
||||||
|
parseDate(it.groupValues[1])
|
||||||
|
} ?: 0L
|
||||||
|
scanlator = arrayOf(sub, softSub, dub).filter(String::isNotBlank).joinToString(", ") + extraInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,25 +201,40 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val ids = episode.url.substringBefore("&")
|
val ids = episode.url.substringBefore("&")
|
||||||
val vrf = callEnimax(ids, "vrf")
|
val vrf = utils.callEnimax(ids, "vrf")
|
||||||
val url = "/ajax/server/list/$ids?$vrf"
|
val url = "/ajax/server/list/$ids?$vrf"
|
||||||
val epurl = episode.url.substringAfter("epurl=")
|
val epurl = episode.url.substringAfter("epurl=")
|
||||||
return GET(baseUrl + url, headers = Headers.headersOf("url", epurl))
|
|
||||||
|
val listHeaders = headers.newBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
add("Referer", baseUrl + epurl)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET("$baseUrl$url#$epurl", listHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class VideoData(
|
||||||
|
val type: String,
|
||||||
|
val serverId: String,
|
||||||
|
val serverName: String,
|
||||||
|
)
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val epurl = response.request.header("url").toString()
|
val epurl = response.request.url.fragment!!
|
||||||
val responseObject = json.decodeFromString<JsonObject>(response.body.string())
|
val document = response.parseAs<ResultResponse>().toDocument()
|
||||||
val document = Jsoup.parse(JSONUtil.unescape(responseObject["result"]!!.jsonPrimitive.content))
|
|
||||||
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
||||||
|
val typeSelection = preferences.getStringSet(PREF_TYPE_TOGGLE_KEY, PREF_TYPES_TOGGLE_DEFAULT)!!
|
||||||
|
|
||||||
return document.select("div.servers > div").parallelMap { elem ->
|
return document.select("div.servers > div").parallelMap { elem ->
|
||||||
val type = elem.attr("data-type").replaceFirstChar { it.uppercase() }
|
val type = elem.attr("data-type").replaceFirstChar { it.uppercase() }
|
||||||
elem.select("li").mapNotNull { serverElement ->
|
elem.select("li").mapNotNull { serverElement ->
|
||||||
val serverId = serverElement.attr("data-link-id")
|
val serverId = serverElement.attr("data-link-id")
|
||||||
val serverName = serverElement.text().lowercase()
|
val serverName = serverElement.text().lowercase()
|
||||||
if (hosterSelection.contains(serverName).not()) return@mapNotNull null
|
if (hosterSelection.contains(serverName, true).not()) return@mapNotNull null
|
||||||
Triple(type, serverId, serverName)
|
if (typeSelection.contains(type, true).not()) return@mapNotNull null
|
||||||
|
|
||||||
|
VideoData(type, serverId, serverName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatten()
|
.flatten()
|
||||||
@ -245,134 +251,61 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
private fun extractVideo(server: Triple<String, String, String>, epUrl: String): List<Video> {
|
private val vidsrcExtractor by lazy { VidsrcExtractor(client, headers) }
|
||||||
val vrf = callEnimax(server.second, "rawVrf")
|
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||||
val referer = Headers.headersOf("referer", epUrl)
|
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||||
|
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||||
|
|
||||||
|
private fun extractVideo(server: VideoData, epUrl: String): List<Video> {
|
||||||
|
val vrf = utils.callEnimax(server.serverId, "rawVrf")
|
||||||
|
|
||||||
|
val listHeaders = headers.newBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
add("Referer", baseUrl + epUrl)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
val response = client.newCall(
|
val response = client.newCall(
|
||||||
GET("$baseUrl/ajax/server/${server.second}?$vrf", headers = referer),
|
GET("$baseUrl/ajax/server/${server.serverId}?$vrf", listHeaders),
|
||||||
).execute()
|
).execute()
|
||||||
if (response.code != 200) return emptyList()
|
if (response.code != 200) return emptyList()
|
||||||
val videoList = mutableListOf<Video>()
|
|
||||||
runCatching {
|
|
||||||
val parsed = json.decodeFromString<ServerResponse>(response.body.string())
|
|
||||||
val embedLink = callEnimax(parsed.result.url, "decrypt")
|
|
||||||
when (server.third) {
|
|
||||||
"vidplay", "mycloud" -> {
|
|
||||||
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
|
||||||
val (serverName, action) = when (server.third) {
|
|
||||||
"vidplay" -> Pair("VidPlay", "rawVizcloud")
|
|
||||||
"mycloud" -> Pair("MyCloud", "rawMcloud")
|
|
||||||
else -> return emptyList()
|
|
||||||
}
|
|
||||||
val rawURL = callEnimax(vidId, action) + "?${embedLink.substringAfter("?")}"
|
|
||||||
val rawReferer = Headers.headersOf(
|
|
||||||
"referer",
|
|
||||||
"$embedLink&autostart=true",
|
|
||||||
"x-requested-with",
|
|
||||||
"XMLHttpRequest",
|
|
||||||
)
|
|
||||||
val rawResponse = client.newCall(GET(rawURL, rawReferer)).execute().parseAs<MediaResponseBody>()
|
|
||||||
val playlistUrl = rawResponse.result.sources.first().file
|
|
||||||
.replace("#.mp4", "")
|
|
||||||
val embedReferer = Headers.headersOf(
|
|
||||||
"referer",
|
|
||||||
"https://${embedLink.toHttpUrl().host}/",
|
|
||||||
)
|
|
||||||
client.newCall(GET(playlistUrl, embedReferer)).execute().use {
|
|
||||||
parseVizPlaylist(
|
|
||||||
it.body.string(),
|
|
||||||
it.request.url,
|
|
||||||
"$serverName - ${server.first}",
|
|
||||||
embedReferer,
|
|
||||||
rawResponse.result.tracks,
|
|
||||||
)
|
|
||||||
}.also(videoList::addAll)
|
|
||||||
}
|
|
||||||
"filemoon" -> FilemoonExtractor(client)
|
|
||||||
.videosFromUrl(embedLink, "Filemoon - ${server.first}")
|
|
||||||
.also(videoList::addAll)
|
|
||||||
|
|
||||||
"streamtape" -> StreamTapeExtractor(client)
|
return runCatching {
|
||||||
.videoFromUrl(embedLink, "StreamTape - ${server.first}")
|
val parsed = response.parseAs<ServerResponse>()
|
||||||
?.let(videoList::add)
|
val embedLink = utils.callEnimax(parsed.result.url, "decrypt")
|
||||||
"mp4upload" -> Mp4uploadExtractor(client)
|
when (server.serverName) {
|
||||||
.videosFromUrl(embedLink, headers, suffix = " - ${server.first}")
|
"vidplay", "mycloud" -> vidsrcExtractor.videosFromUrl(embedLink, server.serverName, server.type)
|
||||||
.let(videoList::addAll)
|
"filemoon" -> filemoonExtractor.videosFromUrl(embedLink, "Filemoon - ${server.type}")
|
||||||
else -> null
|
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
|
||||||
|
"mp4upload" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")
|
||||||
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}
|
}.getOrElse { emptyList() }
|
||||||
return videoList
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseVizPlaylist(
|
|
||||||
masterPlaylist: String,
|
|
||||||
masterUrl: HttpUrl,
|
|
||||||
prefix: String,
|
|
||||||
embedReferer: Headers,
|
|
||||||
subTracks: List<MediaResponseBody.Result.SubTrack> = emptyList(),
|
|
||||||
): List<Video> {
|
|
||||||
val playlistHeaders = embedReferer.newBuilder()
|
|
||||||
.add("host", masterUrl.host)
|
|
||||||
.add("connection", "keep-alive")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
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.toString().substringBeforeLast("/") + "/" +
|
|
||||||
it.substringAfter("\n").substringBefore("\n")
|
|
||||||
Video(videoUrl, quality, videoUrl, playlistHeaders, subtitleTracks = subTracks.toTracks())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun callEnimax(query: String, action: String): String {
|
|
||||||
return if (action in listOf("rawVizcloud", "rawMcloud")) {
|
|
||||||
val referer = if (action == "rawVizcloud") "https://vidstream.pro/" else "https://mcloud.to/"
|
|
||||||
val futoken = client.newCall(
|
|
||||||
GET(referer + "futoken", headers),
|
|
||||||
).execute().use { it.body.string() }
|
|
||||||
val formBody = FormBody.Builder()
|
|
||||||
.add("query", query)
|
|
||||||
.add("futoken", futoken)
|
|
||||||
.build()
|
|
||||||
client.newCall(
|
|
||||||
POST(
|
|
||||||
url = "https://9anime.eltik.net/$action?apikey=aniyomi",
|
|
||||||
body = formBody,
|
|
||||||
),
|
|
||||||
).execute().parseAs<RawResponse>().rawURL
|
|
||||||
} else {
|
|
||||||
client.newCall(
|
|
||||||
GET("https://9anime.eltik.net/$action?query=$query&apikey=aniyomi"),
|
|
||||||
).execute().use {
|
|
||||||
val body = it.body.string()
|
|
||||||
when (action) {
|
|
||||||
"decrypt" -> {
|
|
||||||
json.decodeFromString<VrfResponse>(body).url
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
json.decodeFromString<VrfResponse>(body).let { vrf ->
|
|
||||||
"${vrf.vrfQuery}=${java.net.URLEncoder.encode(vrf.url, "utf-8")}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Int.toBoolean() = this == 1
|
private fun Int.toBoolean() = this == 1
|
||||||
|
|
||||||
|
private fun Set<String>.contains(s: String, ignoreCase: Boolean): Boolean {
|
||||||
|
return any { it.equals(s, ignoreCase) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||||
|
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||||
|
|
||||||
return this.sortedWith(
|
return this.sortedWith(
|
||||||
compareByDescending<Video> { it.quality.contains(quality) }
|
compareByDescending<Video> { it.quality.contains(quality) }
|
||||||
|
.thenByDescending { it.quality.contains(server, true) }
|
||||||
.thenByDescending { it.quality.contains(lang) },
|
.thenByDescending { it.quality.contains(lang) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseDate(dateStr: String): Long {
|
||||||
|
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||||
|
.getOrNull() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseStatus(statusString: String): Int {
|
private fun parseStatus(statusString: String): Int {
|
||||||
return when (statusString) {
|
return when (statusString) {
|
||||||
"Releasing" -> SAnime.ONGOING
|
"Releasing" -> SAnime.ONGOING
|
||||||
@ -390,66 +323,14 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
return json.decodeFromString(responseBody)
|
return json.decodeFromString(responseBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
|
|
||||||
return filter {
|
|
||||||
it.kind == "captions"
|
|
||||||
}.mapNotNull {
|
|
||||||
runCatching {
|
|
||||||
Track(
|
|
||||||
it.file,
|
|
||||||
it.label,
|
|
||||||
)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ServerResponse(
|
|
||||||
val result: Result,
|
|
||||||
) {
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Result(
|
|
||||||
val url: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class VrfResponse(
|
|
||||||
val url: String,
|
|
||||||
val vrfQuery: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class RawResponse(
|
|
||||||
val rawURL: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MediaResponseBody(
|
|
||||||
val status: Int,
|
|
||||||
val result: Result,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Result(
|
|
||||||
val sources: ArrayList<Source>,
|
|
||||||
val tracks: ArrayList<SubTrack> = ArrayList(),
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
data class Source(
|
|
||||||
val file: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SubTrack(
|
|
||||||
val file: String,
|
|
||||||
val label: String = "",
|
|
||||||
val kind: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val SOFTSUB_REGEX by lazy { Regex("""\bsoftsub\b""", RegexOption.IGNORE_CASE) }
|
||||||
|
private val RELEASE_REGEX by lazy { Regex("""Release: (\d+\/\d+\/\d+ \d+:\d+)""") }
|
||||||
|
|
||||||
|
private val DATE_FORMATTER by lazy {
|
||||||
|
SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
|
||||||
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
||||||
private const val PREF_DOMAIN_DEFAULT = "https://aniwave.to"
|
private const val PREF_DOMAIN_DEFAULT = "https://aniwave.to"
|
||||||
|
|
||||||
@ -459,6 +340,9 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
private const val PREF_LANG_KEY = "preferred_language"
|
private const val PREF_LANG_KEY = "preferred_language"
|
||||||
private const val PREF_LANG_DEFAULT = "Sub"
|
private const val PREF_LANG_DEFAULT = "Sub"
|
||||||
|
|
||||||
|
private const val PREF_SERVER_KEY = "preferred_server"
|
||||||
|
private const val PREF_SERVER_DEFAULT = "vidplay"
|
||||||
|
|
||||||
private const val PREF_MARK_FILLERS_KEY = "mark_fillers"
|
private const val PREF_MARK_FILLERS_KEY = "mark_fillers"
|
||||||
private const val PREF_MARK_FILLERS_DEFAULT = true
|
private const val PREF_MARK_FILLERS_DEFAULT = true
|
||||||
|
|
||||||
@ -478,6 +362,10 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
"mp4upload",
|
"mp4upload",
|
||||||
)
|
)
|
||||||
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
|
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
|
||||||
|
|
||||||
|
private const val PREF_TYPE_TOGGLE_KEY = "type_selection"
|
||||||
|
private val TYPES = arrayOf("Sub", "Softsub", "Dub")
|
||||||
|
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
@ -495,6 +383,7 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
val selected = newValue as String
|
val selected = newValue as String
|
||||||
val index = findIndexOfValue(selected)
|
val index = findIndexOfValue(selected)
|
||||||
val entry = entryValues[index] as String
|
val entry = entryValues[index] as String
|
||||||
|
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||||
preferences.edit().putString(key, entry).commit()
|
preferences.edit().putString(key, entry).commit()
|
||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
@ -517,9 +406,9 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_LANG_KEY
|
key = PREF_LANG_KEY
|
||||||
title = "Preferred language"
|
title = "Preferred Type"
|
||||||
entries = arrayOf("Sub", "Dub")
|
entries = TYPES
|
||||||
entryValues = arrayOf("Sub", "Dub")
|
entryValues = TYPES
|
||||||
setDefaultValue(PREF_LANG_DEFAULT)
|
setDefaultValue(PREF_LANG_DEFAULT)
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
|
|
||||||
@ -531,6 +420,22 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_SERVER_KEY
|
||||||
|
title = "Preferred Server"
|
||||||
|
entries = HOSTERS
|
||||||
|
entryValues = HOSTERS_NAMES
|
||||||
|
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)
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = PREF_MARK_FILLERS_KEY
|
key = PREF_MARK_FILLERS_KEY
|
||||||
title = "Mark filler episodes"
|
title = "Mark filler episodes"
|
||||||
@ -552,5 +457,18 @@ class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||||
}
|
}
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
MultiSelectListPreference(screen.context).apply {
|
||||||
|
key = PREF_TYPE_TOGGLE_KEY
|
||||||
|
title = "Enable/Disable Types"
|
||||||
|
entries = TYPES
|
||||||
|
entryValues = TYPES
|
||||||
|
setDefaultValue(PREF_TYPES_TOGGLE_DEFAULT)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ServerResponse(
|
||||||
|
val result: Result,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Result(
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VrfResponse(
|
||||||
|
val url: String,
|
||||||
|
val vrfQuery: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RawResponse(
|
||||||
|
val rawURL: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MediaResponseBody(
|
||||||
|
val status: Int,
|
||||||
|
val result: Result,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Result(
|
||||||
|
val sources: ArrayList<Source>,
|
||||||
|
val tracks: ArrayList<SubTrack> = ArrayList(),
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Source(
|
||||||
|
val file: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SubTrack(
|
||||||
|
val file: String,
|
||||||
|
val label: String = "",
|
||||||
|
val kind: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ResultResponse(
|
||||||
|
val result: String,
|
||||||
|
) {
|
||||||
|
fun toDocument(): Document {
|
||||||
|
return Jsoup.parseBodyFragment(result)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class AniwaveUtils(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
val json: Json by injectLazy()
|
||||||
|
|
||||||
|
fun callEnimax(query: String, action: String): String {
|
||||||
|
return if (action in listOf("rawVizcloud", "rawMcloud")) {
|
||||||
|
val referer = if (action == "rawVizcloud") "https://vidstream.pro/" else "https://mcloud.to/"
|
||||||
|
val futoken = client.newCall(
|
||||||
|
GET(referer + "futoken", headers),
|
||||||
|
).execute().use { it.body.string() }
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("query", query)
|
||||||
|
.add("futoken", futoken)
|
||||||
|
.build()
|
||||||
|
client.newCall(
|
||||||
|
POST(
|
||||||
|
url = "https://9anime.eltik.net/$action?apikey=aniyomi",
|
||||||
|
body = formBody,
|
||||||
|
),
|
||||||
|
).execute().parseAs<RawResponse>().rawURL
|
||||||
|
} else {
|
||||||
|
client.newCall(
|
||||||
|
GET("https://9anime.eltik.net/$action?query=$query&apikey=aniyomi"),
|
||||||
|
).execute().use {
|
||||||
|
val body = it.body.string()
|
||||||
|
when (action) {
|
||||||
|
"decrypt" -> {
|
||||||
|
json.decodeFromString<VrfResponse>(body).url
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
json.decodeFromString<VrfResponse>(body).let { vrf ->
|
||||||
|
"${vrf.vrfQuery}=${java.net.URLEncoder.encode(vrf.url, "utf-8")}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
val responseBody = use { it.body.string() }
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
}
|
@ -1,86 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
|
||||||
|
|
||||||
object JSONUtil {
|
|
||||||
fun escape(input: String): String {
|
|
||||||
val output = StringBuilder()
|
|
||||||
for (i in 0 until input.length) {
|
|
||||||
val ch = input[i]
|
|
||||||
val chx = ch.code
|
|
||||||
assert(chx != 0)
|
|
||||||
if (ch == '\n') {
|
|
||||||
output.append("\\n")
|
|
||||||
} else if (ch == '\t') {
|
|
||||||
output.append("\\t")
|
|
||||||
} else if (ch == '\r') {
|
|
||||||
output.append("\\r")
|
|
||||||
} else if (ch == '\\') {
|
|
||||||
output.append("\\\\")
|
|
||||||
} else if (ch == '"') {
|
|
||||||
output.append("\\\"")
|
|
||||||
} else if (ch == '\b') {
|
|
||||||
output.append("\\b")
|
|
||||||
} else if (ch == '\u000c') {
|
|
||||||
output.append("\\u000c")
|
|
||||||
} else if (chx >= 0x10000) {
|
|
||||||
assert(false) { "Java stores as u16, so it should never give us a character that's bigger than 2 bytes. It literally can't." }
|
|
||||||
} else if (chx > 127) {
|
|
||||||
output.append(String.format("\\u%04x", chx))
|
|
||||||
} else {
|
|
||||||
output.append(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unescape(input: String): String {
|
|
||||||
val builder = StringBuilder()
|
|
||||||
var i = 0
|
|
||||||
while (i < input.length) {
|
|
||||||
val delimiter = input[i]
|
|
||||||
i++ // consume letter or backslash
|
|
||||||
if (delimiter == '\\' && i < input.length) {
|
|
||||||
// consume first after backslash
|
|
||||||
val ch = input[i]
|
|
||||||
i++
|
|
||||||
if (ch == '\\' || ch == '/' || ch == '"' || ch == '\'') {
|
|
||||||
builder.append(ch)
|
|
||||||
} else if (ch == 'n') {
|
|
||||||
builder.append('\n')
|
|
||||||
} else if (ch == 'r') {
|
|
||||||
builder.append('\r')
|
|
||||||
} else if (ch == 't') {
|
|
||||||
builder.append(
|
|
||||||
'\t',
|
|
||||||
)
|
|
||||||
} else if (ch == 'b') {
|
|
||||||
builder.append('\b')
|
|
||||||
} else if (ch == 'f') {
|
|
||||||
builder.append(
|
|
||||||
'\u000c',
|
|
||||||
)
|
|
||||||
} else if (ch == 'u') {
|
|
||||||
val hex = StringBuilder()
|
|
||||||
|
|
||||||
// expect 4 digits
|
|
||||||
if (i + 4 > input.length) {
|
|
||||||
throw RuntimeException("Not enough unicode digits! ")
|
|
||||||
}
|
|
||||||
for (x in input.substring(i, i + 4).toCharArray()) {
|
|
||||||
if (!Character.isLetterOrDigit(x)) {
|
|
||||||
throw RuntimeException("Bad character in unicode escape.")
|
|
||||||
}
|
|
||||||
hex.append(x.lowercaseChar())
|
|
||||||
}
|
|
||||||
i += 4 // consume those four digits.
|
|
||||||
val code = hex.toString().toInt(16)
|
|
||||||
builder.append(code.toChar())
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("Illegal escape sequence: \\$ch")
|
|
||||||
}
|
|
||||||
} else { // it's not a backslash, or it's the last character.
|
|
||||||
builder.append(delimiter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.nineanime.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.nineanime.AniwaveUtils
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.nineanime.MediaResponseBody
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val utils by lazy { AniwaveUtils(client, headers) }
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
fun videosFromUrl(embedLink: String, name: String, type: String): List<Video> {
|
||||||
|
val vidId = embedLink.substringAfterLast("/").substringBefore("?")
|
||||||
|
val (serverName, action) = when (name) {
|
||||||
|
"vidplay" -> Pair("VidPlay", "rawVizcloud")
|
||||||
|
"mycloud" -> Pair("MyCloud", "rawMcloud")
|
||||||
|
else -> return emptyList()
|
||||||
|
}
|
||||||
|
val rawURL = utils.callEnimax(vidId, action) + "?${embedLink.substringAfter("?")}"
|
||||||
|
val rawReferer = Headers.headersOf(
|
||||||
|
"referer",
|
||||||
|
"$embedLink&autostart=true",
|
||||||
|
"x-requested-with",
|
||||||
|
"XMLHttpRequest",
|
||||||
|
)
|
||||||
|
val rawResponse = client.newCall(GET(rawURL, rawReferer)).execute().parseAs<MediaResponseBody>()
|
||||||
|
val playlistUrl = rawResponse.result.sources.first().file
|
||||||
|
.replace("#.mp4", "")
|
||||||
|
|
||||||
|
return playlistUtils.extractFromHls(
|
||||||
|
playlistUrl,
|
||||||
|
referer = "https://${embedLink.toHttpUrl().host}/",
|
||||||
|
videoNameGen = { q -> "$serverName - $type - $q" },
|
||||||
|
subtitleList = rawResponse.result.tracks.toTracks(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
val responseBody = use { it.body.string() }
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MediaResponseBody.Result.SubTrack>.toTracks(): List<Track> {
|
||||||
|
return filter {
|
||||||
|
it.kind == "captions"
|
||||||
|
}.mapNotNull {
|
||||||
|
runCatching {
|
||||||
|
Track(
|
||||||
|
it.file,
|
||||||
|
it.label,
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user