feat(multisrc): Add ZoroTheme generator (#2347)
11
.run/ZoroThemeGenerator.run.xml
Normal 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>
|
21
lib/megacloud-extractor/build.gradle.kts
Normal 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
|
@ -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 = "")
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
multisrc/overrides/zorotheme/kaido/additional.gradle
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
implementation(project(":lib-megacloud-extractor"))
|
||||||
|
implementation(project(':lib-streamtape-extractor'))
|
||||||
|
}
|
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
BIN
multisrc/overrides/zorotheme/kaido/res/play_store_512.png
Normal file
After Width: | Height: | Size: 96 KiB |
33
multisrc/overrides/zorotheme/kaido/src/Kaido.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
multisrc/overrides/zorotheme/zoro/additional.gradle
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dependencies {
|
||||||
|
implementation(project(":lib-megacloud-extractor"))
|
||||||
|
implementation(project(':lib-streamtape-extractor'))
|
||||||
|
}
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
37
multisrc/overrides/zorotheme/zoro/src/AniWatch.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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", "")
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
@ -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>
|
|
@ -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"
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|