feat(multisrc): Add ZoroTheme generator (#2347)

This commit is contained in:
Secozzi 2023-10-10 22:42:11 +00:00 committed by GitHub
parent 8b08d19606
commit a33aadae87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 680 additions and 540 deletions

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ZoroThemeGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="aniyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.zorotheme.ZoroThemeGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=zorotheme" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=zorotheme" />
</method>
</configuration>
</component>

View File

@ -0,0 +1,21 @@
plugins {
id("com.android.library")
kotlin("android")
id("kotlinx-serialization")
}
android {
compileSdk = AndroidConfig.compileSdk
namespace = "eu.kanade.tachiyomi.lib.megacloudextractor"
defaultConfig {
minSdk = AndroidConfig.minSdk
}
}
dependencies {
compileOnly(libs.bundles.common)
implementation(project(":lib-cryptoaes"))
implementation(project(":lib-playlist-utils"))
}
// BUMPS: 0

View File

@ -1,19 +1,30 @@
package eu.kanade.tachiyomi.animeextension.en.zoro.extractors package eu.kanade.tachiyomi.lib.megacloudextractor
import eu.kanade.tachiyomi.animeextension.en.zoro.dto.SourceResponseDto import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animeextension.en.zoro.dto.VideoDto import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animeextension.en.zoro.dto.VideoLink
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import okhttp3.Headers
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import okhttp3.CacheControl
import okhttp3.HttpUrl.Companion.toHttpUrl
class AniWatchExtractor(private val client: OkHttpClient) { class MegaCloudExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val cacheControl = CacheControl.Builder().noStore().build()
private val noCacheClient = client.newBuilder()
.cache(null)
.build()
companion object { companion object {
private val SERVER_URL = arrayOf("https://megacloud.tv", "https://rapid-cloud.co") private val SERVER_URL = arrayOf("https://megacloud.tv", "https://rapid-cloud.co")
private val SOURCES_URL = arrayOf("/embed-2/ajax/e-1/getSources?id=", "/ajax/embed-6-v2/getSources?id=") private val SOURCES_URL = arrayOf("/embed-2/ajax/e-1/getSources?id=", "/ajax/embed-6-v2/getSources?id=")
@ -24,7 +35,7 @@ class AniWatchExtractor(private val client: OkHttpClient) {
private fun cipherTextCleaner(data: String, type: String): Pair<String, String> { private fun cipherTextCleaner(data: String, type: String): Pair<String, String> {
// TODO: fetch the key only when needed, using a thread-safe map // TODO: fetch the key only when needed, using a thread-safe map
// (Like ConcurrentMap?) or MUTEX hacks. // (Like ConcurrentMap?) or MUTEX hacks.
val indexPairs = client.newCall(GET("https://raw.githubusercontent.com/Claudemirovsky/keys/e$type/key")) val indexPairs = noCacheClient.newCall(GET("https://raw.githubusercontent.com/Claudemirovsky/keys/e$type/key", cache = cacheControl))
.execute() .execute()
.use { it.body.string() } .use { it.body.string() }
.let { json.decodeFromString<List<List<Int>>>(it) } .let { json.decodeFromString<List<List<Int>>>(it) }
@ -49,7 +60,23 @@ class AniWatchExtractor(private val client: OkHttpClient) {
} }
} }
fun getVideoDto(url: String): VideoDto { fun getVideosFromUrl(url: String, type: String, name: String): List<Video> {
val video = getVideoDto(url)
val masterUrl = video.sources.first().file
val subs2 = video.tracks
?.filter { it.kind == "captions" }
?.map { Track(it.file, it.label) }
?: emptyList()
return playlistUtils.extractFromHls(
masterUrl,
videoNameGen = { "$name - $it - $type" },
subtitleList = subs2,
referer = "https://${url.toHttpUrl().host}/"
)
}
private fun getVideoDto(url: String): VideoDto {
val type = if (url.startsWith("https://megacloud.tv")) 0 else 1 val type = if (url.startsWith("https://megacloud.tv")) 0 else 1
val keyType = SOURCES_KEY[type] val keyType = SOURCES_KEY[type]
@ -60,10 +87,32 @@ class AniWatchExtractor(private val client: OkHttpClient) {
.use { it.body.string() } .use { it.body.string() }
val data = json.decodeFromString<SourceResponseDto>(srcRes) val data = json.decodeFromString<SourceResponseDto>(srcRes)
if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes) if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes)
val ciphered = data.sources.jsonPrimitive.content.toString() val ciphered = data.sources.jsonPrimitive.content.toString()
val decrypted = json.decodeFromString<List<VideoLink>>(tryDecrypting(ciphered, keyType)) val decrypted = json.decodeFromString<List<VideoLink>>(tryDecrypting(ciphered, keyType))
return VideoDto(decrypted, data.tracks) return VideoDto(decrypted, data.tracks)
} }
@Serializable
data class VideoDto(
val sources: List<VideoLink>,
val tracks: List<TrackDto>? = null,
)
@Serializable
data class SourceResponseDto(
val sources: JsonElement,
val encrypted: Boolean = true,
val tracks: List<TrackDto>? = null,
)
@Serializable
data class VideoLink(val file: String = "")
@Serializable
data class TrackDto(val file: String, val kind: String, val label: String = "")
} }

View File

@ -97,8 +97,9 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
val playlistHttpUrl = playlistUrl.toHttpUrl() val playlistHttpUrl = playlistUrl.toHttpUrl()
val masterBase = "https://${playlistHttpUrl.host}${playlistHttpUrl.encodedPath}" val masterBase = playlistHttpUrl.newBuilder().apply {
.substringBeforeLast("/") + "/" removePathSegment(playlistHttpUrl.pathSize - 1)
}.build().toString() + "/"
// Get subtitles // Get subtitles
val subtitleTracks = subtitleList + SUBTITLE_REGEX.findAll(masterPlaylist).mapNotNull { val subtitleTracks = subtitleList + SUBTITLE_REGEX.findAll(masterPlaylist).mapNotNull {
@ -126,6 +127,8 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
getAbsoluteUrl(url, playlistUrl, masterBase) getAbsoluteUrl(url, playlistUrl, masterBase)
} ?: return@mapNotNull null } ?: return@mapNotNull null
Video( Video(
videoUrl, videoNameGen(resolution), videoUrl, videoUrl, videoNameGen(resolution), videoUrl,
headers = videoHeadersGen(headers, referer, videoUrl), headers = videoHeadersGen(headers, referer, videoUrl),
@ -139,7 +142,8 @@ class PlaylistUtils(private val client: OkHttpClient, private val headers: Heade
url.isEmpty() -> null url.isEmpty() -> null
url.startsWith("http") -> url url.startsWith("http") -> url
url.startsWith("//") -> "https:$url" url.startsWith("//") -> "https:$url"
url.startsWith("/") -> "https://" + playlistUrl.toHttpUrl().host + url url.startsWith("/") -> playlistUrl.toHttpUrl().newBuilder().encodedPath("/").build().toString()
.substringBeforeLast("/") + url
else -> masterBase + url else -> masterBase + url
} }
} }

View File

@ -0,0 +1,4 @@
dependencies {
implementation(project(":lib-megacloud-extractor"))
implementation(project(':lib-streamtape-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.animeextension.en.kaido
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.megacloudextractor.MegaCloudExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.multisrc.zorotheme.ZoroTheme
class Kaido : ZoroTheme(
"en",
"Kaido",
"https://kaido.to",
) {
override val hosterNames: List<String> = listOf(
"Vidstreaming",
"Vidcloud",
"StreamTape",
)
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers) }
override fun extractVideo(server: VideoData): List<Video> {
return when (server.name) {
"StreamTape" -> {
streamtapeExtractor.videoFromUrl(server.link, "Streamtape - ${server.type}")
?.let(::listOf)
?: emptyList()
}
"Vidstreaming", "Vidcloud" -> megaCloudExtractor.getVideosFromUrl(server.link, server.type, server.name)
else -> emptyList()
}
}
}

View File

@ -0,0 +1,4 @@
dependencies {
implementation(project(":lib-megacloud-extractor"))
implementation(project(':lib-streamtape-extractor'))
}

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.megacloudextractor.MegaCloudExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.multisrc.zorotheme.ZoroTheme
class AniWatch : ZoroTheme(
"en",
"AniWatch",
"https://aniwatch.to",
) {
override val id = 6706411382606718900L
override val ajaxRoute = "/v2"
override val hosterNames: List<String> = listOf(
"Vidstreaming",
"MegaCloud",
"StreamTape",
)
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers) }
override fun extractVideo(server: VideoData): List<Video> {
return when (server.name) {
"StreamTape" -> {
streamtapeExtractor.videoFromUrl(server.link, "Streamtape - ${server.type}")
?.let(::listOf)
?: emptyList()
}
"Vidstreaming", "MegaCloud" -> megaCloudExtractor.getVideosFromUrl(server.link, server.type, server.name)
else -> emptyList()
}
}
}

View File

@ -0,0 +1,449 @@
package eu.kanade.tachiyomi.multisrc.zorotheme
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
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.multisrc.zorotheme.dto.HtmlResponse
import eu.kanade.tachiyomi.multisrc.zorotheme.dto.SourcesResponse
import eu.kanade.tachiyomi.network.GET
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
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 uy.kohesive.injekt.injectLazy
abstract class ZoroTheme(
override val lang: String,
override val name: String,
override val baseUrl: String,
) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val docHeaders = headers.newBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
add("Host", baseUrl.toHttpUrl().host)
add("Referer", "$baseUrl/")
}.build()
protected open val ajaxRoute = ""
abstract val hosterNames: List<String>
private val useEnglish by lazy { preferences.getTitleLang == "English" }
private val markFiller by lazy { preferences.markFiller }
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/most-popular?page=$page", docHeaders)
override fun popularAnimeSelector(): String = "div.flw-item"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
element.selectFirst("div.film-detail a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = if (useEnglish && it.hasAttr("title")) {
it.attr("title")
} else {
it.attr("data-jname")
}
}
thumbnail_url = element.selectFirst("div.film-poster > img")!!.attr("data-src")
}
override fun popularAnimeNextPageSelector() = "li.page-item a[title=Next]"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/top-airing?page=$page", docHeaders)
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 params = ZoroThemeFilters.getSearchParameters(filters)
val endpoint = if (query.isEmpty()) "filter" else "search"
val url = "$baseUrl/$endpoint".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addIfNotBlank("keyword", query)
addIfNotBlank("type", params.type)
addIfNotBlank("status", params.status)
addIfNotBlank("rated", params.rated)
addIfNotBlank("score", params.score)
addIfNotBlank("season", params.season)
addIfNotBlank("language", params.language)
addIfNotBlank("sort", params.sort)
addIfNotBlank("sy", params.start_year)
addIfNotBlank("sm", params.start_month)
addIfNotBlank("sd", params.start_day)
addIfNotBlank("ey", params.end_year)
addIfNotBlank("em", params.end_month)
addIfNotBlank("ed", params.end_day)
addIfNotBlank("genres", params.genres)
}.build().toString()
return GET(url, docHeaders)
}
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList() = ZoroThemeFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
thumbnail_url = document.selectFirst("div.anisc-poster img")!!.attr("src")
document.selectFirst("div.anisc-info")!!.let { info ->
author = info.getInfo("Studios:")
status = parseStatus(info.getInfo("Status:"))
genre = info.getInfo("Genres:", isList = true)
description = buildString {
info.getInfo("Overview:")?.also { append(it + "\n") }
info.getInfo("Aired:", full = true)?.also(::append)
info.getInfo("Premiered:", full = true)?.also(::append)
info.getInfo("Synonyms:", full = true)?.also(::append)
info.getInfo("Japanese:", full = true)?.also(::append)
}
}
}
private fun Element.getInfo(
tag: String,
isList: Boolean = false,
full: Boolean = false,
): String? {
if (isList) {
return select("div.item-list:contains($tag) > a").eachText().joinToString()
}
val value = selectFirst("div.item-title:contains($tag)")
?.selectFirst("*.name, *.text")
?.text()
return if (full && value != null) "\n$tag $value" else value
}
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Currently Airing" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val id = anime.url.substringAfterLast("-")
return GET("$baseUrl/ajax$ajaxRoute/episode/list/$id", apiHeaders(baseUrl + anime.url))
}
override fun episodeListSelector() = "a.ep-item"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.parseAs<HtmlResponse>().getHtml()
return document.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
}
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
episode_number = element.attr("data-number").toFloatOrNull() ?: 1F
name = "Ep. ${element.attr("data-number")}: ${element.attr("title")}"
setUrlWithoutDomain(element.attr("href"))
if (element.hasClass("ssl-item-filler") && markFiller) {
scanlator = "Filler Episode"
}
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val id = episode.url.substringAfterLast("?ep=")
return GET("$baseUrl/ajax$ajaxRoute/episode/servers?episodeId=$id", apiHeaders(baseUrl + episode.url))
}
data class VideoData(
val type: String,
val link: String,
val name: String,
)
override fun videoListParse(response: Response): List<Video> {
val episodeReferer = response.request.header("referer")!!
val typeSelection = preferences.typeToggle
val hosterSelection = preferences.hostToggle
val serversDoc = response.parseAs<HtmlResponse>().getHtml()
val embedLinks = listOf("servers-sub", "servers-dub", "servers-mixed").map { type ->
if (type !in typeSelection) return@map emptyList()
serversDoc.select("div.$type div.item").parallelMapNotNull {
val id = it.attr("data-id")
val type = it.attr("data-type")
val name = it.text()
if (hosterSelection.contains(name, true).not()) return@parallelMapNotNull null
val link = client.newCall(
GET("$baseUrl/ajax$ajaxRoute/episode/sources?id=$id", apiHeaders(episodeReferer)),
).execute().parseAs<SourcesResponse>().link ?: ""
VideoData(type, link, name)
}
}.flatten()
return embedLinks.parallelCatchingFlatMap(::extractVideo).ifEmpty {
throw Exception("Failed to extract videos.")
}
}
protected open fun extractVideo(server: VideoData): List<Video> {
return emptyList()
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// ============================= Utilities ==============================
private inline fun <A, B> Iterable<A>.parallelMapNotNull(crossinline f: suspend (A) -> B?): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll().filterNotNull()
}
private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
runBlocking {
map { async(Dispatchers.Default) { runCatching { f(it) }.getOrElse { emptyList() } } }.awaitAll().flatten()
}
private inline fun <reified T> Response.parseAs(): T {
return use { it.body.string() }.let(json::decodeFromString)
}
private fun Set<String>.contains(s: String, ignoreCase: Boolean): Boolean {
return any { it.equals(s, ignoreCase) }
}
private fun apiHeaders(referer: String): Headers = headers.newBuilder().apply {
add("Accept", "*/*")
add("Host", baseUrl.toHttpUrl().host)
add("Referer", referer)
add("X-Requested-With", "XMLHttpRequest")
}.build()
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
return this
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.prefQuality
val lang = preferences.prefLang
val server = preferences.prefServer
return this.sortedWith(
compareByDescending<Video> { it.quality.contains(quality) }
.thenByDescending { it.quality.contains(server, true) }
.thenByDescending { it.quality.contains(lang, true) },
)
}
private val SharedPreferences.getTitleLang
get() = getString(PREF_TITLE_LANG_KEY, PREF_TITLE_LANG_DEFAULT)!!
private val SharedPreferences.markFiller
get() = getBoolean(MARK_FILLERS_KEY, MARK_FILLERS_DEFAULT)
private val SharedPreferences.prefQuality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.prefServer
get() = getString(PREF_SERVER_KEY, hosterNames.first())!!
private val SharedPreferences.prefLang
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
private val SharedPreferences.hostToggle
get() = getStringSet(PREF_HOSTER_KEY, hosterNames.toSet())!!
private val SharedPreferences.typeToggle
get() = getStringSet(PREF_TYPE_TOGGLE_KEY, PREF_TYPES_TOGGLE_DEFAULT)!!
companion object {
private const val PREF_TITLE_LANG_KEY = "preferred_title_lang"
private const val PREF_TITLE_LANG_DEFAULT = "Romaji"
private val PREF_TITLE_LANG_LIST = arrayOf("Romaji", "English")
private const val MARK_FILLERS_KEY = "mark_fillers"
private const val MARK_FILLERS_DEFAULT = true
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_HOSTER_KEY = "hoster_selection"
private const val PREF_TYPE_TOGGLE_KEY = "type_selection"
private val TYPES_ENTRIES = arrayOf("Sub", "Dub", "Mixed")
private val TYPES_ENTRY_VALUES = arrayOf("servers-sub", "servers-dub", "servers-mixed")
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES_ENTRY_VALUES.toSet()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_TITLE_LANG_KEY
title = "Preferred title language"
entries = PREF_TITLE_LANG_LIST
entryValues = PREF_TITLE_LANG_LIST
setDefaultValue(PREF_TITLE_LANG_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 new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = MARK_FILLERS_KEY
title = "Mark filler episodes"
setDefaultValue(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)
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_SERVER_KEY
title = "Preferred Server"
entries = hosterNames.toTypedArray()
entryValues = hosterNames.toTypedArray()
setDefaultValue(hosterNames.first())
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_ENTRIES
entryValues = TYPES_ENTRIES
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)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Enable/Disable Hosts"
entries = hosterNames.toTypedArray()
entryValues = hosterNames.toTypedArray()
setDefaultValue(hosterNames.toSet())
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_ENTRIES
entryValues = TYPES_ENTRY_VALUES
setDefaultValue(PREF_TYPES_TOGGLE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
}

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.animeextension.en.zoro package eu.kanade.tachiyomi.multisrc.zorotheme
import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniWatchFilters { object ZoroThemeFilters {
open class QueryPartFilter( open class QueryPartFilter(
displayName: String, displayName: String,
val vals: Array<Pair<String, String>>, val vals: Array<Pair<String, String>>,
@ -24,25 +23,25 @@ object AniWatchFilters {
} }
} }
class TypeFilter : QueryPartFilter("Type", AniWatchFiltersData.TYPES) class TypeFilter : QueryPartFilter("Type", ZoroThemeFiltersData.TYPES)
class StatusFilter : QueryPartFilter("Status", AniWatchFiltersData.STATUS) class StatusFilter : QueryPartFilter("Status", ZoroThemeFiltersData.STATUS)
class RatedFilter : QueryPartFilter("Rated", AniWatchFiltersData.RATED) class RatedFilter : QueryPartFilter("Rated", ZoroThemeFiltersData.RATED)
class ScoreFilter : QueryPartFilter("Score", AniWatchFiltersData.SCORES) class ScoreFilter : QueryPartFilter("Score", ZoroThemeFiltersData.SCORES)
class SeasonFilter : QueryPartFilter("Season", AniWatchFiltersData.SEASONS) class SeasonFilter : QueryPartFilter("Season", ZoroThemeFiltersData.SEASONS)
class LanguageFilter : QueryPartFilter("Language", AniWatchFiltersData.LANGUAGES) class LanguageFilter : QueryPartFilter("Language", ZoroThemeFiltersData.LANGUAGES)
class SortFilter : QueryPartFilter("Sort by", AniWatchFiltersData.SORTS) class SortFilter : QueryPartFilter("Sort by", ZoroThemeFiltersData.SORTS)
class StartYearFilter : QueryPartFilter("Start year", AniWatchFiltersData.YEARS) class StartYearFilter : QueryPartFilter("Start year", ZoroThemeFiltersData.YEARS)
class StartMonthFilter : QueryPartFilter("Start month", AniWatchFiltersData.MONTHS) class StartMonthFilter : QueryPartFilter("Start month", ZoroThemeFiltersData.MONTHS)
class StartDayFilter : QueryPartFilter("Start day", AniWatchFiltersData.DAYS) class StartDayFilter : QueryPartFilter("Start day", ZoroThemeFiltersData.DAYS)
class EndYearFilter : QueryPartFilter("End year", AniWatchFiltersData.YEARS) class EndYearFilter : QueryPartFilter("End year", ZoroThemeFiltersData.YEARS)
class EndMonthFilter : QueryPartFilter("End month", AniWatchFiltersData.MONTHS) class EndMonthFilter : QueryPartFilter("End month", ZoroThemeFiltersData.MONTHS)
class EndDayFilter : QueryPartFilter("End day", AniWatchFiltersData.DAYS) class EndDayFilter : QueryPartFilter("End day", ZoroThemeFiltersData.DAYS)
class GenresFilter : CheckBoxFilterList( class GenresFilter : CheckBoxFilterList(
"Genres", "Genres",
AniWatchFiltersData.GENRES.map { CheckBoxVal(it.first, false) }, ZoroThemeFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
) )
val FILTER_LIST get() = AnimeFilterList( val FILTER_LIST get() = AnimeFilterList(
@ -90,7 +89,7 @@ object AniWatchFilters {
.first() .first()
.state.mapNotNull { format -> .state.mapNotNull { format ->
if (format.state) { if (format.state) {
AniWatchFiltersData.GENRES.find { it.first == format.name }!!.second ZoroThemeFiltersData.GENRES.find { it.first == format.name }!!.second
} else { null } } else { null }
}.joinToString(",") }.joinToString(",")
@ -114,7 +113,7 @@ object AniWatchFilters {
) )
} }
private object AniWatchFiltersData { private object ZoroThemeFiltersData {
val ALL = Pair("All", "") val ALL = Pair("All", "")

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.multisrc.zorotheme
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class ZoroThemeGenerator : ThemeSourceGenerator {
override val themePkg = "zorotheme"
override val themeClass = "ZoroTheme"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("AniWatch", "https://aniwatch.to", "en", isNsfw = false, pkgName = "zoro", overrideVersionCode = 35),
SingleLang("Kaido", "https://kaido.to", "en", isNsfw = false),
)
companion object {
@JvmStatic
fun main(args: Array<String>) = ZoroThemeGenerator().createAll()
}
}

View File

@ -1,7 +1,23 @@
package eu.kanade.tachiyomi.animeextension.en.zoro.dto package eu.kanade.tachiyomi.multisrc.zorotheme.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
@Serializable
data class HtmlResponse(
val html: String,
) {
fun getHtml(): Document {
return Jsoup.parseBodyFragment(html)
}
}
@Serializable
data class SourcesResponse(
val link: String? = null,
)
@Serializable @Serializable
data class VideoDto( data class VideoDto(

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".en.zoro.AniWatchUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="aniwatch.to"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,21 +0,0 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'AniWatch.to'
pkgNameSuffix = 'en.zoro'
extClass = '.AniWatch'
extVersionCode = 35
libVersion = '13'
}
dependencies {
implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-cryptoaes'))
implementation(project(':lib-playlist-utils'))
}
apply from: "$rootDir/common.gradle"

View File

@ -1,424 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animeextension.en.zoro.dto.VideoDto
import eu.kanade.tachiyomi.animeextension.en.zoro.extractors.AniWatchExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
class AniWatch : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AniWatch.to"
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
override val id = 6706411382606718900L
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
private val ajaxRoute by lazy { if (baseUrl == "https://kaido.to") "" else "/v2" }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.flw-item"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/most-popular?page=$page")
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
thumbnail_url = element.selectFirst("div.film-poster > img")!!.attr("data-src")
val filmDetail = element.selectFirst("div.film-detail a")!!
setUrlWithoutDomain(filmDetail.attr("href"))
title = filmDetail.attr("data-jname")
}
override fun popularAnimeNextPageSelector(): String = "li.page-item a[title=Next]"
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/top-airing")
override fun latestUpdatesSelector() = popularAnimeSelector()
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
override fun searchAnimeSelector() = popularAnimeSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$slug"))
.asObservableSuccess()
.map(::searchAnimeBySlugParse)
} else {
super.fetchSearchAnime(page, query, filters)
}
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AniWatchFilters.getSearchParameters(filters)
val endpoint = if (query.isEmpty()) "filter" else "search"
val url = "$baseUrl/$endpoint".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addIfNotBlank("keyword", query)
.addIfNotBlank("type", params.type)
.addIfNotBlank("status", params.status)
.addIfNotBlank("rated", params.rated)
.addIfNotBlank("score", params.score)
.addIfNotBlank("season", params.season)
.addIfNotBlank("language", params.language)
.addIfNotBlank("sort", params.sort)
.addIfNotBlank("sy", params.start_year)
.addIfNotBlank("sm", params.start_month)
.addIfNotBlank("sd", params.start_day)
.addIfNotBlank("ey", params.end_year)
.addIfNotBlank("em", params.end_month)
.addIfNotBlank("ed", params.end_day)
.addIfNotBlank("genres", params.genres)
.build()
return GET(url.toString())
}
override fun getFilterList() = AniWatchFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val info = document.selectFirst("div.anisc-info")!!
val detail = document.selectFirst("div.anisc-detail")!!
thumbnail_url = document.selectFirst("div.anisc-poster img")!!.attr("src")
title = detail.selectFirst("h2")!!.attr("data-jname")
author = info.getInfo("Studios:")
status = parseStatus(info.getInfo("Status:"))
genre = info.getInfo("Genres:", isList = true)
description = buildString {
info.getInfo("Overview:")?.also { append(it + "\n") }
detail.select("div.film-stats div.tick-dub").eachText().also {
append("\nLanguage: " + it.joinToString())
}
info.getInfo("Aired:", full = true)?.also(::append)
info.getInfo("Premiered:", full = true)?.also(::append)
info.getInfo("Synonyms:", full = true)?.also(::append)
info.getInfo("Japanese:", full = true)?.also(::append)
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeListRequest(anime: SAnime): Request {
val id = anime.url.substringAfterLast("-")
val referer = Headers.headersOf("Referer", baseUrl + anime.url)
return GET("$baseUrl/ajax$ajaxRoute/episode/list/$id", referer)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = Jsoup.parse(response.parseAs<HtmlResponse>().html)
return document.select("a.ep-item")
.map(::episodeFromElement)
.reversed()
}
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
episode_number = element.attr("data-number").toFloatOrNull() ?: 1F
name = "Episode ${element.attr("data-number")}: ${element.attr("title")}"
setUrlWithoutDomain(element.attr("href"))
if (element.hasClass("ssl-item-filler") && preferences.getBoolean(MARK_FILLERS_KEY, MARK_FILLERS_DEFAULT)) {
scanlator = "Filler Episode"
}
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val id = episode.url.substringAfterLast("?ep=")
val referer = Headers.headersOf("Referer", baseUrl + episode.url)
return GET("$baseUrl/ajax$ajaxRoute/episode/servers?episodeId=$id", referer)
}
private val aniwatchExtractor by lazy { AniWatchExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val serversDoc = Jsoup.parse(response.parseAs<HtmlResponse>().html)
return serversDoc.select("div.server-item")
.parallelMap { server ->
val name = server.text()
val id = server.attr("data-id")
val subDub = server.attr("data-type")
val url = "$baseUrl/ajax$ajaxRoute/episode/sources?id=$id"
val reqBody = client.newCall(GET(url, episodeReferer)).execute()
.use { it.body.string() }
val sourceUrl = reqBody.substringAfter("\"link\":\"")
.substringBefore("\"")
runCatching {
when {
"Vidstreaming" in name || "Vidcloud" in name -> {
aniwatchExtractor.getVideoDto(sourceUrl).let {
getVideosFromServer(it, subDub, name)
}
}
"Streamtape" in name ->
StreamTapeExtractor(client)
.videoFromUrl(sourceUrl, "StreamTape - $subDub")
?.let(::listOf)
else -> null
}
}.onFailure { it.printStackTrace() }.getOrNull() ?: emptyList()
}.flatten()
}
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private fun getVideosFromServer(video: VideoDto, subDub: String, name: String): List<Video> {
val masterUrl = video.sources.first().file
val subs2 = video.tracks
?.filter { it.kind == "captions" }
?.mapNotNull { Track(it.file, it.label) }
?: emptyList<Track>()
val subs = subLangOrder(subs2)
return playlistUtils.extractFromHls(
masterUrl,
videoNameGen = { "$name - $it - $subDub" },
subtitleList = subs,
)
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(type) },
),
).reversed()
}
private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
return tracks.sortedWith(
compareBy { it.lang.contains(language) },
).reversed()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_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_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
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_TYPE_KEY
title = PREF_TYPE_TITLE
entries = PREF_TYPE_ENTRIES
entryValues = PREF_TYPE_ENTRIES
setDefaultValue(PREF_TYPE_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_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_ENTRIES
entryValues = PREF_SUB_ENTRIES
setDefaultValue(PREF_SUB_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 = MARK_FILLERS_KEY
title = MARK_FILLERS_TITLE
setDefaultValue(MARK_FILLERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T {
return use { it.body.string() }.let(json::decodeFromString)
}
@Serializable
private data class HtmlResponse(val html: String)
private fun parseStatus(statusString: String?): Int {
return when (statusString) {
"Currently Airing" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun Element.getInfo(
tag: String,
isList: Boolean = false,
full: Boolean = false,
): String? {
if (isList) {
return select("div.item-list:contains($tag) > a").eachText().joinToString()
}
val value = selectFirst("div.item-title:contains($tag)")
?.selectFirst("*.name, *.text")
?.text()
return if (full && value != null) "\n$tag $value" else value
}
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
return this
}
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
const val PREFIX_SEARCH = "slug:"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred video quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "720p", "1080p")
private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private const val PREF_DOMAIN_DEFAULT = "https://kaido.to"
private val PREF_DOMAIN_ENTRIES = arrayOf("kaido.to", "aniwatch.to")
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf("https://kaido.to", "https://aniwatch.to")
private const val PREF_TYPE_KEY = "preferred_type"
private const val PREF_TYPE_TITLE = "Preferred episode type/mode"
private const val PREF_TYPE_DEFAULT = "dub"
private val PREF_TYPE_ENTRIES = arrayOf("sub", "dub")
private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language"
private const val PREF_SUB_DEFAULT = "English"
private val PREF_SUB_ENTRIES = arrayOf(
"English",
"Spanish",
"Portuguese",
"French",
"German",
"Italian",
"Japanese",
"Russian",
"Arabic",
)
private const val MARK_FILLERS_KEY = "mark_fillers"
private const val MARK_FILLERS_TITLE = "Mark filler episodes"
private const val MARK_FILLERS_DEFAULT = true
}
}

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://aniwatch.to/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class AniWatchUrlActivity : Activity() {
private val tag = "AniWatchUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val slug = pathSegments[0]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AniWatch.PREFIX_SEARCH}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}