feat: add Anizone (#557)
* feat: add AniZone * fix episode number parsing
This commit is contained in:
parent
b4e9c0d3a5
commit
5b33d95e03
8 changed files with 543 additions and 0 deletions
11
src/all/anizone/build.gradle
Normal file
11
src/all/anizone/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
ext {
|
||||||
|
extName = 'AniZone'
|
||||||
|
extClass = '.AniZone'
|
||||||
|
extVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:playlist-utils"))
|
||||||
|
}
|
BIN
src/all/anizone/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/anizone/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
src/all/anizone/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/anizone/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
src/all/anizone/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/anizone/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
src/all/anizone/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/anizone/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
src/all/anizone/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/anizone/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
|
@ -0,0 +1,513 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.all.anizone
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
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.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.addJsonObject
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup.parseBodyFragment
|
||||||
|
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
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class AniZone : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
|
override val name = "AniZone"
|
||||||
|
|
||||||
|
override val baseUrl = "https://anizone.to"
|
||||||
|
|
||||||
|
override val lang = "all"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var token: String = ""
|
||||||
|
|
||||||
|
private val snapShots: MutableMap<String, String> = mutableMapOf(
|
||||||
|
ANIME_SNAPSHOT_KEY to "",
|
||||||
|
EPISODE_SNAPSHOT_KEY to "",
|
||||||
|
VIDEO_SNAPSHOT_KEY to "",
|
||||||
|
)
|
||||||
|
|
||||||
|
private var loadCount: Int = 0
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int): Request {
|
||||||
|
return if (page == 1) {
|
||||||
|
loadCount = 0
|
||||||
|
snapShots[ANIME_SNAPSHOT_KEY] = ""
|
||||||
|
|
||||||
|
val updates = buildJsonObject {
|
||||||
|
put("sort", "title-asc")
|
||||||
|
}
|
||||||
|
val calls = buildJsonArray { }
|
||||||
|
|
||||||
|
createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls)
|
||||||
|
} else {
|
||||||
|
val updates = buildJsonObject { }
|
||||||
|
val calls = buildJsonArray {
|
||||||
|
addJsonObject {
|
||||||
|
put("path", "")
|
||||||
|
put("method", "loadMore")
|
||||||
|
putJsonArray("params") { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
|
val html = response.parseAs<LivewireDto>().getHtml(ANIME_SNAPSHOT_KEY)
|
||||||
|
|
||||||
|
val animeList = html.select("div.grid > div").drop(loadCount)
|
||||||
|
.map(::animeFromElement)
|
||||||
|
val hasNextPage = html.selectFirst("div[x-intersect~=loadMore]") != null
|
||||||
|
|
||||||
|
loadCount += animeList.size
|
||||||
|
|
||||||
|
return AnimesPage(animeList, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animeFromElement(element: Element): SAnime {
|
||||||
|
return SAnime.create().apply {
|
||||||
|
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||||
|
with(element.selectFirst("a.inline")!!) {
|
||||||
|
setUrlWithoutDomain(attr("href"))
|
||||||
|
title = text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return if (page == 1) {
|
||||||
|
loadCount = 0
|
||||||
|
snapShots[ANIME_SNAPSHOT_KEY] = ""
|
||||||
|
|
||||||
|
val updates = buildJsonObject {
|
||||||
|
put("sort", "release-desc")
|
||||||
|
}
|
||||||
|
val calls = buildJsonArray { }
|
||||||
|
|
||||||
|
createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls)
|
||||||
|
} else {
|
||||||
|
popularAnimeRequest(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||||
|
return popularAnimeParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
val sortFilter = filters.filterIsInstance<SortFilter>().first()
|
||||||
|
|
||||||
|
return if (page == 1) {
|
||||||
|
loadCount = 0
|
||||||
|
snapShots[ANIME_SNAPSHOT_KEY] = ""
|
||||||
|
|
||||||
|
val updates = buildJsonObject {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
put("search", query)
|
||||||
|
}
|
||||||
|
put("sort", sortFilter.toUriPart())
|
||||||
|
}
|
||||||
|
val calls = buildJsonArray { }
|
||||||
|
|
||||||
|
createLivewireReq(ANIME_SNAPSHOT_KEY, updates, calls)
|
||||||
|
} else {
|
||||||
|
popularAnimeRequest(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
|
return popularAnimeParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Filters ===============================
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList {
|
||||||
|
return AnimeFilterList(SortFilter())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SortFilter : UriPartFilter(
|
||||||
|
"Sort",
|
||||||
|
arrayOf(
|
||||||
|
Pair("A-Z", "title-asc"),
|
||||||
|
Pair("Z-A", "title-desc"),
|
||||||
|
Pair("Earliest Release", "release-asc"),
|
||||||
|
Pair("Latest Release", "release-desc"),
|
||||||
|
Pair("First Added", "added-asc"),
|
||||||
|
Pair("Last Added", "added-desc"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||||
|
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val infoDiv = document.select("div.flex.items-start > div")[1]
|
||||||
|
|
||||||
|
return SAnime.create().apply {
|
||||||
|
thumbnail_url = document.selectFirst("div.flex.items-start img")!!.attr("abs:img")
|
||||||
|
|
||||||
|
with(infoDiv) {
|
||||||
|
title = selectFirst("h1")!!.text()
|
||||||
|
status = select("span.flex")[1].parseStatus()
|
||||||
|
description = selectFirst("div:has(>h3:contains(Synopsis)) > div")?.html()
|
||||||
|
?.replace("<br>", "\n")
|
||||||
|
?.replace(MULTILINE_REGEX, "\n\n")
|
||||||
|
genre = select("div > a").joinToString { it.text() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.parseStatus(): Int = when (this.text().lowercase()) {
|
||||||
|
"completed" -> SAnime.COMPLETED
|
||||||
|
"ongoing" -> SAnime.ONGOING
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
|
private fun getPredefinedSnapshots(slug: String): String {
|
||||||
|
return when (slug) {
|
||||||
|
"/anime/uyyyn4kf" -> """{"data":{"anime":[null,{"class":"anime","key":68,"s":"mdl"}],"title":null,"search":"","listSize":1104,"sort":"release-asc","sortOptions":[{"release-asc":"First Aired","release-desc":"Last Aired"},{"s":"arr"}],"view":"list","paginators":[{"page":1},{"s":"arr"}]},"memo":{"id":"GD1OiEMOJq6UQDQt1OBt","name":"pages.anime-detail","path":"anime\/uyyyn4kf","method":"GET","children":[],"scripts":[],"assets":[],"errors":[],"locale":"en"},"checksum":"5800932dd82e4862f34f6fd72d8098243b32643e8accb8da6a6a39cd0ee86acd"}"""
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
|
snapShots[EPISODE_SNAPSHOT_KEY] = getPredefinedSnapshots(anime.url)
|
||||||
|
|
||||||
|
val updates = buildJsonObject {
|
||||||
|
put("sort", "release-desc")
|
||||||
|
}
|
||||||
|
val calls = buildJsonArray { }
|
||||||
|
|
||||||
|
return createLivewireReq(EPISODE_SNAPSHOT_KEY, updates, calls, anime.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val document = response.parseAs<LivewireDto>().getHtml(EPISODE_SNAPSHOT_KEY)
|
||||||
|
val episodeList = document.select(episodeSelector)
|
||||||
|
.map(::episodeFromElement)
|
||||||
|
.toMutableList()
|
||||||
|
loadCount = episodeList.size
|
||||||
|
|
||||||
|
var hasMore = document.selectFirst("div[x-intersect~=loadMore]") != null
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
val updates = buildJsonObject { }
|
||||||
|
val calls = buildJsonArray {
|
||||||
|
addJsonObject {
|
||||||
|
put("path", "")
|
||||||
|
put("method", "loadMore")
|
||||||
|
putJsonArray("params") { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val resp = client.newCall(
|
||||||
|
createLivewireReq(EPISODE_SNAPSHOT_KEY, updates, calls),
|
||||||
|
).execute().parseAs<LivewireDto>().getHtml(EPISODE_SNAPSHOT_KEY)
|
||||||
|
|
||||||
|
val episodes = resp.select(episodeSelector)
|
||||||
|
.drop(loadCount)
|
||||||
|
.map(::episodeFromElement)
|
||||||
|
|
||||||
|
episodeList.addAll(episodes)
|
||||||
|
loadCount += episodes.size
|
||||||
|
|
||||||
|
hasMore = resp.selectFirst("div[x-intersect~=loadMore]") != null
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodeList
|
||||||
|
}
|
||||||
|
|
||||||
|
private val episodeSelector = "ul > li"
|
||||||
|
|
||||||
|
private fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
val url = element.selectFirst("a[href]")!!.attr("abs:href")
|
||||||
|
|
||||||
|
return SEpisode.create().apply {
|
||||||
|
setUrlWithoutDomain(url)
|
||||||
|
name = element.selectFirst("h3")!!.text()
|
||||||
|
date_upload = element.select("div.flex-row > span").getOrNull(1)
|
||||||
|
?.text()
|
||||||
|
?.let { parseDate(it) }
|
||||||
|
?: 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
|
return GET(baseUrl + episode.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val playlistUtils: PlaylistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val serverSelects = document.select("button[wire:click]")
|
||||||
|
.filter { video ->
|
||||||
|
video.attr("wire:click").contains("setVideo")
|
||||||
|
}
|
||||||
|
|
||||||
|
val subtitles = document.select("track[kind=subtitles]").map {
|
||||||
|
Track(it.attr("src"), it.attr("label"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val m3u8List = mutableListOf(
|
||||||
|
VideoData(
|
||||||
|
url = document.selectFirst("media-player")!!.attr("src"),
|
||||||
|
name = serverSelects.first().text(),
|
||||||
|
subtitles = subtitles,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
snapShots[VIDEO_SNAPSHOT_KEY] = document.getSnapshot()
|
||||||
|
|
||||||
|
serverSelects.drop(1).forEach { video ->
|
||||||
|
val regex = "setVideo\\('(\\d+)'\\)".toRegex()
|
||||||
|
val matchResult = regex.find(video.attr("wire:click"))
|
||||||
|
val videoId = if (matchResult != null && matchResult.groupValues.size == 1) {
|
||||||
|
matchResult.groupValues[1]
|
||||||
|
} else {
|
||||||
|
"0"
|
||||||
|
}
|
||||||
|
val updates = buildJsonObject { }
|
||||||
|
val calls = buildJsonArray {
|
||||||
|
add(
|
||||||
|
buildJsonObject {
|
||||||
|
put("path", "")
|
||||||
|
put("method", "setVideo")
|
||||||
|
putJsonArray("params") {
|
||||||
|
add(videoId.toInt())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val doc = client.newCall(
|
||||||
|
createLivewireReq(VIDEO_SNAPSHOT_KEY, updates, calls, response.request.url.encodedPath),
|
||||||
|
).execute().parseAs<LivewireDto>().getHtml(VIDEO_SNAPSHOT_KEY)
|
||||||
|
|
||||||
|
val subs = doc.select("track[kind=subtitles]").map {
|
||||||
|
Track(it.attr("src"), it.attr("label"))
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.selectFirst("media-player")?.attr("src")?.also {
|
||||||
|
m3u8List.add(
|
||||||
|
VideoData(
|
||||||
|
url = it,
|
||||||
|
name = video.text(),
|
||||||
|
subtitles = subs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverList = if (preferences.dub) {
|
||||||
|
m3u8List
|
||||||
|
} else {
|
||||||
|
m3u8List.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverList.flatMap {
|
||||||
|
playlistUtils.extractFromHls(
|
||||||
|
playlistUrl = it.url,
|
||||||
|
referer = "$baseUrl/",
|
||||||
|
videoNameGen = { q -> "${it.name} - $q" },
|
||||||
|
subtitleList = it.subtitles,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VideoData(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val subtitles: List<Track>,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.quality
|
||||||
|
return sortedWith(
|
||||||
|
compareBy { it.quality.contains(quality) },
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private fun LivewireDto.getHtml(mapKey: String): Document {
|
||||||
|
val data = this.components.first()
|
||||||
|
|
||||||
|
snapShots[mapKey] = data.snapshot.replace("\\\"", "\"")
|
||||||
|
|
||||||
|
return parseBodyFragment(
|
||||||
|
data.effects.html.replace("\\\"", "\"")
|
||||||
|
.replace("\\n", ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.getSnapshot(): String {
|
||||||
|
return this.selectFirst("main > div[wire:snapshot]")!!
|
||||||
|
.attr("wire:snapshot")
|
||||||
|
.replace(""", "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLivewireReq(
|
||||||
|
mapKey: String,
|
||||||
|
updates: JsonObject,
|
||||||
|
calls: JsonArray,
|
||||||
|
initialSlug: String = "/anime",
|
||||||
|
): Request {
|
||||||
|
val firstSnapshot = snapShots[mapKey] ?: ""
|
||||||
|
|
||||||
|
if (firstSnapshot.isEmpty() || token.isEmpty()) {
|
||||||
|
val doc = client.newCall(GET(baseUrl + initialSlug, headers)).execute()
|
||||||
|
.asJsoup()
|
||||||
|
|
||||||
|
snapShots[mapKey] = doc.getSnapshot()
|
||||||
|
|
||||||
|
token = doc.selectFirst("script[data-csrf]")
|
||||||
|
?.attr("data-csrf")
|
||||||
|
?.takeIf(String::isNotEmpty)
|
||||||
|
?: throw Exception("Failed to get csrf token")
|
||||||
|
}
|
||||||
|
|
||||||
|
val headers = headersBuilder().apply {
|
||||||
|
add("X-Livewire", "")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val body = buildJsonObject {
|
||||||
|
put("_token", token)
|
||||||
|
putJsonArray("components") {
|
||||||
|
addJsonObject {
|
||||||
|
put("calls", calls)
|
||||||
|
put("snapshot", snapShots[mapKey])
|
||||||
|
put("updates", updates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toRequestBody()
|
||||||
|
|
||||||
|
return POST("$baseUrl/livewire/update", headers, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonObject.toRequestBody(): RequestBody {
|
||||||
|
return json.encodeToString(this).toRequestBody(
|
||||||
|
"application/json".toMediaType(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDate(dateStr: String): Long {
|
||||||
|
return try {
|
||||||
|
DATE_FORMAT.parse(dateStr)!!.time
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SharedPreferences.quality
|
||||||
|
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
|
|
||||||
|
private val SharedPreferences.dub
|
||||||
|
get() = getBoolean(PREF_DUB_KEY, PREF_DUB_DEFAULT)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val MULTILINE_REGEX = Regex("""\n{2,}""")
|
||||||
|
private val DATE_FORMAT by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||||
|
|
||||||
|
private const val ANIME_SNAPSHOT_KEY = "anime_snapshot_key"
|
||||||
|
private const val EPISODE_SNAPSHOT_KEY = "episode_snapshot_key"
|
||||||
|
private const val VIDEO_SNAPSHOT_KEY = "video_snapshot_key"
|
||||||
|
|
||||||
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
|
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||||
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
|
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
|
||||||
|
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||||
|
|
||||||
|
private const val PREF_DUB_KEY = "attempt_dub"
|
||||||
|
private const val PREF_DUB_TITLE = "Attempt to prefer dub"
|
||||||
|
private const val PREF_DUB_DEFAULT = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Preferences =============================
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_QUALITY_KEY
|
||||||
|
title = PREF_QUALITY_TITLE
|
||||||
|
entries = PREF_QUALITY_ENTRIES
|
||||||
|
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||||
|
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, new ->
|
||||||
|
val index = findIndexOfValue(new as String)
|
||||||
|
preferences.edit().putString(key, entryValues[index] as String).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = PREF_DUB_KEY
|
||||||
|
title = PREF_DUB_TITLE
|
||||||
|
setDefaultValue(PREF_DUB_DEFAULT)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val new = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(key, new).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package eu.kanade.tachiyomi.animeextension.all.anizone
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class LivewireDto(
|
||||||
|
val components: List<ComponentDto>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class ComponentDto(
|
||||||
|
val snapshot: String,
|
||||||
|
val effects: EffectsDto,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class EffectsDto(
|
||||||
|
val html: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue