feat(en/aniwave): Code refactor + small new features (#2237)

This commit is contained in:
Secozzi
2023-09-22 10:15:46 +00:00
committed by GitHub
parent 11c9369ba0
commit 9585aec265
6 changed files with 340 additions and 324 deletions

View File

@ -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"

View File

@ -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()
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")}"
}
}
}
}
} }
}.getOrElse { emptyList() }
} }
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)
} }
} }

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}