forked from AlmightyHak/extensions-source
parent
ffb39ec544
commit
6816676e4a
11 changed files with 0 additions and 988 deletions
|
@ -1,14 +0,0 @@
|
|||
ext {
|
||||
extName = 'Aniwave'
|
||||
extClass = '.Aniwave'
|
||||
extVersionCode = 75
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:vidsrc-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
|
@ -1,538 +0,0 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidsrcextractor.VidsrcExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parallelMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Aniwave : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Aniwave"
|
||||
|
||||
override val id: Long = 98855593379717478
|
||||
|
||||
override val baseUrl by lazy {
|
||||
val customDomain = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
|
||||
if (customDomain.isNullOrBlank()) {
|
||||
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
} else {
|
||||
customDomain
|
||||
}
|
||||
}
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val utils by lazy { AniwaveUtils() }
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val refererHeaders = headers.newBuilder().apply {
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
private val markFiller by lazy { preferences.getBoolean(PREF_MARK_FILLERS_KEY, PREF_MARK_FILLERS_DEFAULT) }
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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 popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
element.select("a.name").let { a ->
|
||||
setUrlWithoutDomain(a.attr("href").substringBefore("?"))
|
||||
title = a.text()
|
||||
}
|
||||
thumbnail_url = element.select("div.poster img").attr("src")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String =
|
||||
"nav > ul.pagination > li.active ~ li"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/filter?sort=recently_updated&page=$page", refererHeaders)
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filters = AniwaveFilters.getSearchParameters(filters)
|
||||
|
||||
val vrf = if (query.isNotBlank()) utils.vrfEncrypt(query) else ""
|
||||
var url = "$baseUrl/filter?keyword=$query"
|
||||
|
||||
if (filters.genre.isNotBlank()) url += filters.genre
|
||||
if (filters.country.isNotBlank()) url += filters.country
|
||||
if (filters.season.isNotBlank()) url += filters.season
|
||||
if (filters.year.isNotBlank()) url += filters.year
|
||||
if (filters.type.isNotBlank()) url += filters.type
|
||||
if (filters.status.isNotBlank()) url += filters.status
|
||||
if (filters.language.isNotBlank()) url += filters.language
|
||||
if (filters.rating.isNotBlank()) url += filters.rating
|
||||
|
||||
return GET("$url&sort=${filters.sort}&page=$page&vrf=$vrf", refererHeaders)
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AniwaveFilters.FILTER_LIST
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
val newDocument = resolveSearchAnime(anime, document)
|
||||
anime.apply {
|
||||
title = newDocument.select("h1.title").text()
|
||||
genre = newDocument.select("div:contains(Genre) > span > a").joinToString { it.text() }
|
||||
description = newDocument.select("div.synopsis > div.shorting > div.content").text()
|
||||
author = newDocument.select("div:contains(Studio) > span > a").text()
|
||||
status = parseStatus(newDocument.select("div:contains(Status) > span").text())
|
||||
|
||||
val altName = "Other name(s): "
|
||||
newDocument.select("h1.title").attr("data-jp").let {
|
||||
if (it.isNotBlank()) {
|
||||
description = when {
|
||||
description.isNullOrBlank() -> altName + it
|
||||
else -> description + "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return anime
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val response = client.newCall(GET(baseUrl + anime.url)).execute()
|
||||
var document = response.asJsoup()
|
||||
document = resolveSearchAnime(anime, document)
|
||||
val id = document.selectFirst("div[data-id]")?.attr("data-id") ?: throw Exception("ID not found")
|
||||
|
||||
val vrf = utils.vrfEncrypt(id)
|
||||
|
||||
val listHeaders = headers.newBuilder().apply {
|
||||
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=$vrf#${anime.url}", listHeaders)
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "div.episodes ul > li > a"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val animeUrl = response.request.url.fragment!!
|
||||
val document = response.parseAs<ResultResponse>().toDocument()
|
||||
|
||||
val episodeElements = document.select(episodeListSelector())
|
||||
return episodeElements.parallelMapBlocking { episodeFromElements(it, animeUrl) }.reversed()
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
private fun episodeFromElements(element: Element, url: String): SEpisode {
|
||||
val title = element.parent()?.attr("title") ?: ""
|
||||
|
||||
val epNum = element.attr("data-num")
|
||||
val ids = element.attr("data-ids")
|
||||
val sub = if (element.attr("data-sub").toInt().toBoolean()) "Sub" else ""
|
||||
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") && markFiller) {
|
||||
" • Filler Episode"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val name = element.parent()?.select("span.d-title")?.text().orEmpty()
|
||||
val namePrefix = "Episode $epNum"
|
||||
|
||||
return SEpisode.create().apply {
|
||||
this.name = "Episode $epNum" +
|
||||
if (name.isNotEmpty() && name != namePrefix) ": $name" else ""
|
||||
this.url = "$ids&epurl=$url/ep-$epNum"
|
||||
episode_number = epNum.toFloat()
|
||||
date_upload = RELEASE_REGEX.find(title)?.let {
|
||||
parseDate(it.groupValues[1])
|
||||
} ?: 0L
|
||||
scanlator = arrayOf(sub, softSub, dub).filter(String::isNotBlank).joinToString(", ") + extraInfo
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val ids = episode.url.substringBefore("&")
|
||||
val vrf = utils.vrfEncrypt(ids)
|
||||
val url = "/ajax/server/list/$ids?vrf=$vrf"
|
||||
val epurl = episode.url.substringAfter("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> {
|
||||
val epurl = response.request.url.fragment!!
|
||||
val document = response.parseAs<ResultResponse>().toDocument()
|
||||
val hosterSelection = getHosters()
|
||||
val typeSelection = preferences.getStringSet(PREF_TYPE_TOGGLE_KEY, PREF_TYPES_TOGGLE_DEFAULT)!!
|
||||
|
||||
return document.select("div.servers > div").parallelFlatMapBlocking { elem ->
|
||||
val type = elem.attr("data-type").replaceFirstChar { it.uppercase() }
|
||||
elem.select("li").mapNotNull { serverElement ->
|
||||
val serverId = serverElement.attr("data-link-id")
|
||||
val serverName = serverElement.text().lowercase()
|
||||
if (hosterSelection.contains(serverName, true).not()) return@mapNotNull null
|
||||
if (typeSelection.contains(type, true).not()) return@mapNotNull null
|
||||
|
||||
VideoData(type, serverId, serverName)
|
||||
}
|
||||
}
|
||||
.parallelFlatMapBlocking { extractVideo(it, epurl) }
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private val vidsrcExtractor by lazy { VidsrcExtractor(client, headers) }
|
||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||
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.vrfEncrypt(server.serverId)
|
||||
|
||||
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(
|
||||
GET("$baseUrl/ajax/server/${server.serverId}?vrf=$vrf", listHeaders),
|
||||
).execute()
|
||||
if (response.code != 200) return emptyList()
|
||||
|
||||
return runCatching {
|
||||
val parsed = response.parseAs<ServerResponse>()
|
||||
val embedLink = utils.vrfDecrypt(parsed.result.url)
|
||||
when (server.serverName) {
|
||||
"vidstream" -> vidsrcExtractor.videosFromUrl(embedLink, "Vidstream", server.type)
|
||||
"megaf" -> vidsrcExtractor.videosFromUrl(embedLink, "MegaF", server.type)
|
||||
"moonf" -> filemoonExtractor.videosFromUrl(embedLink, "MoonF - ${server.type} - ")
|
||||
"streamtape" -> streamtapeExtractor.videoFromUrl(embedLink, "StreamTape - ${server.type}")?.let(::listOf) ?: emptyList()
|
||||
"mp4u" -> mp4uploadExtractor.videosFromUrl(embedLink, headers, suffix = " - ${server.type}")
|
||||
else -> emptyList()
|
||||
}
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
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> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareByDescending<Video> { it.quality.contains(lang) }
|
||||
.thenByDescending { it.quality.contains(quality) }
|
||||
.thenByDescending { it.quality.contains(server, true) },
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when (statusString) {
|
||||
"Releasing" -> SAnime.ONGOING
|
||||
"Completed" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveSearchAnime(anime: SAnime, document: Document): Document {
|
||||
if (document.location().startsWith("$baseUrl/filter?keyword=")) { // redirected to search
|
||||
val element = document.selectFirst(searchAnimeSelector())
|
||||
val foundAnimePath = element?.selectFirst("a[href]")?.attr("href")
|
||||
?: throw Exception("Search element not found (resolveSearch)")
|
||||
anime.url = foundAnimePath // probably doesn't work as intended
|
||||
return client.newCall(GET(baseUrl + foundAnimePath)).execute().asJsoup()
|
||||
}
|
||||
return document
|
||||
}
|
||||
|
||||
private fun getHosters(): Set<String> {
|
||||
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
||||
var invalidRecord = false
|
||||
hosterSelection.forEach { str ->
|
||||
val index = HOSTERS_NAMES.indexOf(str)
|
||||
if (index == -1) {
|
||||
invalidRecord = true
|
||||
}
|
||||
}
|
||||
|
||||
// found invalid record, reset to defaults
|
||||
if (invalidRecord) {
|
||||
preferences.edit().putStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT).apply()
|
||||
return PREF_HOSTER_DEFAULT.toSet()
|
||||
}
|
||||
|
||||
return hosterSelection.toSet()
|
||||
}
|
||||
|
||||
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_DEFAULT = "https://aniwave.to"
|
||||
|
||||
private const val PREF_CUSTOM_DOMAIN_KEY = "custom_domain"
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
|
||||
private const val PREF_LANG_KEY = "preferred_language"
|
||||
private const val PREF_LANG_DEFAULT = "Sub"
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Vidstream"
|
||||
|
||||
private const val PREF_MARK_FILLERS_KEY = "mark_fillers"
|
||||
private const val PREF_MARK_FILLERS_DEFAULT = true
|
||||
|
||||
private const val PREF_HOSTER_KEY = "hoster_selection"
|
||||
private val HOSTERS = arrayOf(
|
||||
"Vidstream",
|
||||
"Megaf",
|
||||
"MoonF",
|
||||
"StreamTape",
|
||||
"MP4u",
|
||||
)
|
||||
private val HOSTERS_NAMES = arrayOf(
|
||||
"vidstream",
|
||||
"megaf",
|
||||
"moonf",
|
||||
"streamtape",
|
||||
"mp4u",
|
||||
)
|
||||
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 ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
// validate hosters preferences and if invalid reset
|
||||
try {
|
||||
getHosters()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(screen.context, e.toString(), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = "Preferred domain"
|
||||
entries = arrayOf("aniwave.to", "aniwavetv.to (unofficial)")
|
||||
entryValues = arrayOf("https://aniwave.to", "https://aniwavetv.to")
|
||||
setDefaultValue(PREF_DOMAIN_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue(PREF_QUALITY_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)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LANG_KEY
|
||||
title = "Preferred Type"
|
||||
entries = TYPES
|
||||
entryValues = TYPES
|
||||
setDefaultValue(PREF_LANG_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)
|
||||
|
||||
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 {
|
||||
key = PREF_MARK_FILLERS_KEY
|
||||
title = "Mark filler episodes"
|
||||
setDefaultValue(PREF_MARK_FILLERS_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
preferences.edit().putBoolean(key, newValue as Boolean).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_HOSTER_KEY
|
||||
title = "Enable/Disable Hosts"
|
||||
entries = HOSTERS
|
||||
entryValues = HOSTERS_NAMES
|
||||
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.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)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PREF_CUSTOM_DOMAIN_KEY
|
||||
title = "Custom domain"
|
||||
setDefaultValue(null)
|
||||
val currentValue = preferences.getString(PREF_CUSTOM_DOMAIN_KEY, null)
|
||||
summary = if (currentValue.isNullOrBlank()) {
|
||||
"Custom domain of your choosing"
|
||||
} else {
|
||||
"Domain: \"$currentValue\". \nLeave blank to disable. Overrides any domain preferences!"
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val newDomain = newValue as String
|
||||
if (newDomain.isBlank() || URLUtil.isValidUrl(newDomain)) {
|
||||
summary = "Restart to apply changes"
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||
preferences.edit().putString(key, newDomain).apply()
|
||||
true
|
||||
} else {
|
||||
Toast.makeText(screen.context, "Invalid url. Url example: https://aniwave.to", Toast.LENGTH_LONG).show()
|
||||
false
|
||||
}
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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 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)
|
||||
}
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object AniwaveFilters {
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return this.filterIsInstance<R>().joinToString("") {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return this.filterIsInstance<R>().first()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||
options: Array<Pair<String, String>>,
|
||||
name: String,
|
||||
): String {
|
||||
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||
.mapNotNull { checkbox ->
|
||||
if (checkbox.state) {
|
||||
options.find { it.first == checkbox.name }!!.second
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.joinToString("&$name[]=").let {
|
||||
if (it.isBlank()) {
|
||||
""
|
||||
} else {
|
||||
"&$name[]=$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : QueryPartFilter("Sort order", NineAnimeFiltersData.SORT)
|
||||
|
||||
class GenreFilter : CheckBoxFilterList(
|
||||
"Genre",
|
||||
NineAnimeFiltersData.GENRE.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class CountryFilter : CheckBoxFilterList(
|
||||
"Country",
|
||||
NineAnimeFiltersData.COUNTRY.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class SeasonFilter : CheckBoxFilterList(
|
||||
"Season",
|
||||
NineAnimeFiltersData.SEASON.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class YearFilter : CheckBoxFilterList(
|
||||
"Year",
|
||||
NineAnimeFiltersData.YEAR.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class TypeFilter : CheckBoxFilterList(
|
||||
"Type",
|
||||
NineAnimeFiltersData.TYPE.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class StatusFilter : CheckBoxFilterList(
|
||||
"Status",
|
||||
NineAnimeFiltersData.STATUS.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class LanguageFilter : CheckBoxFilterList(
|
||||
"Language",
|
||||
NineAnimeFiltersData.LANGUAGE.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class RatingFilter : CheckBoxFilterList(
|
||||
"Rating",
|
||||
NineAnimeFiltersData.RATING.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
SortFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
GenreFilter(),
|
||||
CountryFilter(),
|
||||
SeasonFilter(),
|
||||
YearFilter(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
LanguageFilter(),
|
||||
RatingFilter(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val sort: String = "",
|
||||
val genre: String = "",
|
||||
val country: String = "",
|
||||
val season: String = "",
|
||||
val year: String = "",
|
||||
val type: String = "",
|
||||
val status: String = "",
|
||||
val language: String = "",
|
||||
val rating: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<SortFilter>(),
|
||||
filters.parseCheckbox<GenreFilter>(NineAnimeFiltersData.GENRE, "genre"),
|
||||
filters.parseCheckbox<CountryFilter>(NineAnimeFiltersData.COUNTRY, "country"),
|
||||
filters.parseCheckbox<SeasonFilter>(NineAnimeFiltersData.SEASON, "season"),
|
||||
filters.parseCheckbox<YearFilter>(NineAnimeFiltersData.YEAR, "year"),
|
||||
filters.parseCheckbox<TypeFilter>(NineAnimeFiltersData.TYPE, "type"),
|
||||
filters.parseCheckbox<StatusFilter>(NineAnimeFiltersData.STATUS, "status"),
|
||||
filters.parseCheckbox<LanguageFilter>(NineAnimeFiltersData.LANGUAGE, "language"),
|
||||
filters.parseCheckbox<RatingFilter>(NineAnimeFiltersData.RATING, "rating"),
|
||||
)
|
||||
}
|
||||
|
||||
private object NineAnimeFiltersData {
|
||||
val SORT = arrayOf(
|
||||
Pair("Most relevance", "most_relevance"),
|
||||
Pair("Recently updated", "recently_updated"),
|
||||
Pair("Recently added", "recently_added"),
|
||||
Pair("Release date", "release_date"),
|
||||
Pair("Trending", "trending"),
|
||||
Pair("Name A-Z", "title_az"),
|
||||
Pair("Scores", "scores"),
|
||||
Pair("MAL scores", "mal_scores"),
|
||||
Pair("Most watched", "most_watched"),
|
||||
Pair("Most favourited", "most_favourited"),
|
||||
Pair("Number of episodes", "number_of_episodes"),
|
||||
)
|
||||
|
||||
val GENRE = arrayOf(
|
||||
Pair("Action", "1"),
|
||||
Pair("Adventure", "2"),
|
||||
Pair("Avant Garde", "2262888"),
|
||||
Pair("Boys Love", "2262603"),
|
||||
Pair("Comedy", "4"),
|
||||
Pair("Demons", "4424081"),
|
||||
Pair("Drama", "7"),
|
||||
Pair("Ecchi", "8"),
|
||||
Pair("Fantasy", "9"),
|
||||
Pair("Girls Love", "2263743"),
|
||||
Pair("Gourmet", "2263289"),
|
||||
Pair("Harem", "11"),
|
||||
Pair("Horror", "14"),
|
||||
Pair("Isekai", "3457284"),
|
||||
Pair("Iyashikei", "4398552"),
|
||||
Pair("Josei", "15"),
|
||||
Pair("Kids", "16"),
|
||||
Pair("Magic", "4424082"),
|
||||
Pair("Mahou Shoujo", "3457321"),
|
||||
Pair("Martial Arts", "18"),
|
||||
Pair("Mecha", "19"),
|
||||
Pair("Military", "20"),
|
||||
Pair("Music", "21"),
|
||||
Pair("Mystery", "22"),
|
||||
Pair("Parody", "23"),
|
||||
Pair("Psychological", "25"),
|
||||
Pair("Reverse Harem", "4398403"),
|
||||
Pair("Romance", "26"),
|
||||
Pair("School", "28"),
|
||||
Pair("Sci-Fi", "29"),
|
||||
Pair("Seinen", "30"),
|
||||
Pair("Shoujo", "31"),
|
||||
Pair("Shounen", "33"),
|
||||
Pair("Slice of Life", "35"),
|
||||
Pair("Space", "36"),
|
||||
Pair("Sports", "37"),
|
||||
Pair("Super Power", "38"),
|
||||
Pair("Supernatural", "39"),
|
||||
Pair("Suspense", "2262590"),
|
||||
Pair("Thriller", "40"),
|
||||
Pair("Vampire", "41"),
|
||||
)
|
||||
|
||||
val COUNTRY = arrayOf(
|
||||
Pair("China", "120823"),
|
||||
Pair("Japan", "120822"),
|
||||
)
|
||||
|
||||
val SEASON = arrayOf(
|
||||
Pair("Fall", "fall"),
|
||||
Pair("Summer", "summer"),
|
||||
Pair("Spring", "spring"),
|
||||
Pair("Winter", "winter"),
|
||||
Pair("Unknown", "unknown"),
|
||||
)
|
||||
|
||||
val YEAR = arrayOf(
|
||||
Pair("2024", "2024"),
|
||||
Pair("2023", "2023"),
|
||||
Pair("2022", "2022"),
|
||||
Pair("2021", "2021"),
|
||||
Pair("2020", "2020"),
|
||||
Pair("2019", "2019"),
|
||||
Pair("2018", "2018"),
|
||||
Pair("2017", "2017"),
|
||||
Pair("2016", "2016"),
|
||||
Pair("2015", "2015"),
|
||||
Pair("2014", "2014"),
|
||||
Pair("2013", "2013"),
|
||||
Pair("2012", "2012"),
|
||||
Pair("2011", "2011"),
|
||||
Pair("2010", "2010"),
|
||||
Pair("2009", "2009"),
|
||||
Pair("2008", "2008"),
|
||||
Pair("2007", "2007"),
|
||||
Pair("2006", "2006"),
|
||||
Pair("2005", "2005"),
|
||||
Pair("2004", "2004"),
|
||||
Pair("2003", "2003"),
|
||||
Pair("2000s", "2000s"),
|
||||
Pair("1990s", "1990s"),
|
||||
Pair("1980s", "1980s"),
|
||||
Pair("1970s", "1970s"),
|
||||
Pair("1960s", "1960s"),
|
||||
Pair("1950s", "1950s"),
|
||||
Pair("1940s", "1940s"),
|
||||
Pair("1930s", "1930s"),
|
||||
Pair("1920s", "1920s"),
|
||||
Pair("1910s", "1910s"),
|
||||
)
|
||||
|
||||
val TYPE = arrayOf(
|
||||
Pair("Movie", "movie"),
|
||||
Pair("TV", "tv"),
|
||||
Pair("OVA", "ova"),
|
||||
Pair("ONA", "ona"),
|
||||
Pair("Special", "special"),
|
||||
Pair("Music", "music"),
|
||||
)
|
||||
|
||||
val STATUS = arrayOf(
|
||||
Pair("Not Yet Aired", "info"),
|
||||
Pair("Releasing", "releasing"),
|
||||
Pair("Completed", "completed"),
|
||||
)
|
||||
|
||||
val LANGUAGE = arrayOf(
|
||||
Pair("Sub and Dub", "subdub"),
|
||||
Pair("Sub", "sub"),
|
||||
Pair("Dub", "dub"),
|
||||
)
|
||||
|
||||
val RATING = arrayOf(
|
||||
Pair("G - All Ages", "g"),
|
||||
Pair("PG - Children", "pg"),
|
||||
Pair("PG 13 - Teens 13 and Older", "pg_13"),
|
||||
Pair("R - 17+, Violence & Profanity", "r"),
|
||||
Pair("R+ - Profanity & Mild Nudity", "r+"),
|
||||
Pair("Rx - Hentai", "rx"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.nineanime
|
||||
|
||||
import android.util.Base64
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class AniwaveUtils {
|
||||
|
||||
fun vrfEncrypt(input: String): String {
|
||||
var vrf = input
|
||||
ORDER.sortedBy {
|
||||
it.first
|
||||
}.forEach { item ->
|
||||
when (item.second) {
|
||||
"exchange" -> vrf = exchange(vrf, item.third)
|
||||
"rc4" -> vrf = rc4Encrypt(item.third.get(0), vrf)
|
||||
"reverse" -> vrf = vrf.reversed()
|
||||
"base64" -> vrf = Base64.encode(vrf.toByteArray(), Base64.URL_SAFE or Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return java.net.URLEncoder.encode(vrf, "utf-8")
|
||||
}
|
||||
|
||||
fun vrfDecrypt(input: String): String {
|
||||
var vrf = input
|
||||
ORDER.sortedByDescending {
|
||||
it.first
|
||||
}.forEach { item ->
|
||||
when (item.second) {
|
||||
"exchange" -> vrf = exchange(vrf, item.third.reversed())
|
||||
"rc4" -> vrf = rc4Decrypt(item.third.get(0), vrf)
|
||||
"reverse" -> vrf = vrf.reversed()
|
||||
"base64" -> vrf = Base64.decode(vrf, Base64.URL_SAFE).toString(Charsets.UTF_8)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return URLDecoder.decode(vrf, "utf-8")
|
||||
}
|
||||
|
||||
private fun rc4Encrypt(key: String, input: String): String {
|
||||
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
|
||||
var output = cipher.doFinal(input.toByteArray())
|
||||
output = Base64.encode(output, Base64.URL_SAFE or Base64.NO_WRAP)
|
||||
return output.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun rc4Decrypt(key: String, input: String): String {
|
||||
var vrf = input.toByteArray()
|
||||
vrf = Base64.decode(vrf, Base64.URL_SAFE)
|
||||
|
||||
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
vrf = cipher.doFinal(vrf)
|
||||
return vrf.toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun exchange(input: String, keys: List<String>): String {
|
||||
val key1 = keys.get(0)
|
||||
val key2 = keys.get(1)
|
||||
return input.map { i ->
|
||||
val index = key1.indexOf(i)
|
||||
if (index != -1) {
|
||||
key2[index]
|
||||
} else {
|
||||
i
|
||||
}
|
||||
}.joinToString("")
|
||||
}
|
||||
|
||||
private fun rot13(vrf: ByteArray): ByteArray {
|
||||
for (i in vrf.indices) {
|
||||
val byte = vrf[i]
|
||||
if (byte in 'A'.code..'Z'.code) {
|
||||
vrf[i] = ((byte - 'A'.code + 13) % 26 + 'A'.code).toByte()
|
||||
} else if (byte in 'a'.code..'z'.code) {
|
||||
vrf[i] = ((byte - 'a'.code + 13) % 26 + 'a'.code).toByte()
|
||||
}
|
||||
}
|
||||
return vrf
|
||||
}
|
||||
|
||||
private fun vrfShift(vrf: ByteArray): ByteArray {
|
||||
for (i in vrf.indices) {
|
||||
val shift = arrayOf(-2, -4, -5, 6, 2, -3, 3, 6)[i % 8]
|
||||
vrf[i] = vrf[i].plus(shift).toByte()
|
||||
}
|
||||
return vrf
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXCHANGE_KEY_1 = listOf("AP6GeR8H0lwUz1", "UAz8Gwl10P6ReH")
|
||||
private val KEY_1 = "ItFKjuWokn4ZpB"
|
||||
private val KEY_2 = "fOyt97QWFB3"
|
||||
private val EXCHANGE_KEY_2 = listOf("1majSlPQd2M5", "da1l2jSmP5QM")
|
||||
private val EXCHANGE_KEY_3 = listOf("CPYvHj09Au3", "0jHA9CPYu3v")
|
||||
private val KEY_3 = "736y1uTJpBLUX"
|
||||
|
||||
private val ORDER = listOf(
|
||||
Triple(1, "exchange", EXCHANGE_KEY_1),
|
||||
Triple(2, "rc4", listOf(KEY_1)),
|
||||
Triple(3, "rc4", listOf(KEY_2)),
|
||||
Triple(4, "exchange", EXCHANGE_KEY_2),
|
||||
Triple(5, "exchange", EXCHANGE_KEY_3),
|
||||
Triple(5, "reverse", emptyList()),
|
||||
Triple(6, "rc4", listOf(KEY_3)),
|
||||
Triple(7, "base64", emptyList()),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue