Merge branch 'main' into HentaiZM_Turc

This commit is contained in:
mobby45 2025-01-06 20:41:12 +01:00 committed by GitHub
commit ab76812a90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
225 changed files with 7651 additions and 1196 deletions

View file

@ -47,12 +47,13 @@ jobs:
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
# This step is going to commit, but this will not trigger another workflow.
- name: Bump extensions that uses a modified lib
if: steps.modified-libs.outputs.any_changed == 'true'
run: |
chmod +x ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
chmod +x ./.github/scripts/bump-versions.py
./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }}
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2
@ -174,4 +175,3 @@ jobs:
cwd: "./repo"
committer_name: Kohi-den-Bot
committer_email: 177773202+Kohi-den-Bot@users.noreply.github.com

View file

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 4
dependencies {
api(project(":lib:megacloud-extractor"))

View file

@ -48,7 +48,7 @@ abstract class ZoroTheme(
.clearOldHosts()
}
private val docHeaders = headers.newBuilder().apply {
protected 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/")
@ -217,7 +217,7 @@ abstract class ZoroTheme(
val serversDoc = response.parseAs<HtmlResponse>().getHtml()
val embedLinks = listOf("servers-sub", "servers-dub", "servers-mixed").map { type ->
val embedLinks = listOf("servers-sub", "servers-dub", "servers-mixed", "servers-raw").map { type ->
if (type !in typeSelection) return@map emptyList()
serversDoc.select("div.$type div.item").parallelMapNotNull {
@ -236,6 +236,7 @@ abstract class ZoroTheme(
}.flatten()
return embedLinks.parallelCatchingFlatMap(::extractVideo)
.sort()
}
abstract fun extractVideo(server: VideoData): List<Video>
@ -332,8 +333,8 @@ abstract class ZoroTheme(
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 TYPES_ENTRIES = arrayOf("Sub", "Dub", "Mixed", "Raw")
private val TYPES_ENTRY_VALUES = arrayOf("servers-sub", "servers-dub", "servers-mixed", "servers-raw")
private val PREF_TYPES_TOGGLE_DEFAULT = TYPES_ENTRY_VALUES.toSet()
}

View file

@ -0,0 +1,7 @@
plugins {
id("lib-android")
}
dependencies {
compileOnly(libs.aniyomi.lib)
}

View file

@ -0,0 +1,83 @@
@file:UseSerializers(BoxItemSerializer::class)
package eu.kanade.tachiyomi.lib.bangumiscraper
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializable
internal data class Images(
val large: String,
val common: String,
val medium: String,
val small: String,
)
@Serializable
internal data class BoxItem(
val key: String,
val value: String,
)
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = BoxItem::class)
internal object BoxItemSerializer : KSerializer<BoxItem> {
override fun deserialize(decoder: Decoder): BoxItem {
val item = (decoder as JsonDecoder).decodeJsonElement().jsonObject
val key = item["key"]!!.jsonPrimitive.content
val value = (item["value"] as? JsonPrimitive)?.contentOrNull ?: ""
return BoxItem(key, value)
}
}
@Serializable
internal data class Subject(
val name: String,
@SerialName("name_cn")
val nameCN: String,
val summary: String,
val images: Images,
@SerialName("meta_tags")
val metaTags: List<String>,
@SerialName("infobox")
val infoBox: List<BoxItem>,
) {
fun findAuthor(): String? {
return findInfo("导演", "原作")
}
fun findArtist(): String? {
return findInfo("美术监督", "总作画监督", "动画制作")
}
fun findInfo(vararg keys: String): String? {
keys.forEach { key ->
return infoBox.find { item ->
item.key == key
}?.value ?: return@forEach
}
return null
}
}
@Serializable
internal data class SearchItem(
val id: Int,
val name: String,
@SerialName("name_cn")
val nameCN: String,
val summary: String,
val images: Images,
)
@Serializable
internal data class SearchResponse(val results: Int, val list: List<SearchItem>)

View file

@ -0,0 +1,126 @@
package eu.kanade.tachiyomi.lib.bangumiscraper
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
enum class BangumiSubjectType(val value: Int) {
BOOK(1),
ANIME(2),
MUSIC(3),
GAME(4),
REAL(6),
}
enum class BangumiFetchType {
/**
* Give cover and summary info.
*/
SHORT,
/**
* Give all require info include genre and author info.
*/
ALL,
}
/**
* A helper class to fetch anime details from Bangumi
*/
object BangumiScraper {
private const val SEARCH_URL = "https://api.bgm.tv/search/subject"
private const val SUBJECTS_URL = "https://api.bgm.tv/v0/subjects"
/**
* Fetch anime details info from Bangumi
* @param fetchType check [BangumiFetchType] to get detail
* @param subjectType check [BangumiSubjectType] to get detail
* @param requestProducer used to custom request
*/
suspend fun fetchDetail(
client: OkHttpClient,
keyword: String,
fetchType: BangumiFetchType = BangumiFetchType.SHORT,
subjectType: BangumiSubjectType = BangumiSubjectType.ANIME,
requestProducer: (url: HttpUrl) -> Request = { url -> GET(url) },
): SAnime {
val httpUrl = SEARCH_URL.toHttpUrl().newBuilder()
.addPathSegment(keyword)
.addQueryParameter(
"responseGroup",
if (fetchType == BangumiFetchType.ALL) {
"small"
} else {
"medium"
},
)
.addQueryParameter("type", "${subjectType.value}")
.addQueryParameter("start", "0")
.addQueryParameter("max_results", "1")
.build()
val searchResponse = client.newCall(requestProducer(httpUrl)).awaitSuccess()
.checkErrorMessage().parseAs<SearchResponse>()
return if (searchResponse.list.isEmpty()) {
SAnime.create()
} else {
val item = searchResponse.list[0]
if (fetchType == BangumiFetchType.ALL) {
fetchSubject(client, "${item.id}", requestProducer)
} else {
SAnime.create().apply {
thumbnail_url = item.images.large
description = item.summary
}
}
}
}
private suspend fun fetchSubject(
client: OkHttpClient,
id: String,
requestProducer: (url: HttpUrl) -> Request,
): SAnime {
val httpUrl = SUBJECTS_URL.toHttpUrl().newBuilder().addPathSegment(id).build()
val subject = client.newCall(requestProducer(httpUrl)).awaitSuccess()
.checkErrorMessage().parseAs<Subject>()
return SAnime.create().apply {
thumbnail_url = subject.images.large
description = subject.summary
genre = buildList {
addAll(subject.metaTags)
subject.findInfo("动画制作")?.let { add(it) }
subject.findInfo("放送开始")?.let { add(it) }
}.joinToString()
author = subject.findAuthor()
artist = subject.findArtist()
if (subject.findInfo("播放结束") != null) {
status = SAnime.COMPLETED
} else if (subject.findInfo("放送开始") != null) {
status = SAnime.ONGOING
}
}
}
private fun Response.checkErrorMessage(): String {
val responseStr = body.string()
val errorMessage =
responseStr.parseAs<JsonElement>().jsonObject["error"]?.jsonPrimitive?.contentOrNull
if (errorMessage != null) {
throw BangumiScraperException(errorMessage)
}
return responseStr
}
}

View file

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.lib.bangumiscraper
class BangumiScraperException(message: String) : Exception(message)

View file

@ -23,13 +23,14 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
private val REGEX_SOURCES = Regex("""sources:\s*\[\{"file":"([^"]+)""")
private val REGEX_FILE = Regex("""file: ?"([^"]+)"""")
private val REGEX_SOURCE = Regex("""source = ?"([^"]+)"""")
private val REGEX_SUBS = Regex("""\[(.*?)\](https?://[^\s,]+)""")
private val REGEX_SUBS = Regex("""\{"file":"([^"]+)","label":"([^"]+)","kind":"captions","default":\w+\}""")
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
}
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
val newHeaders = headers.newBuilder()
.set("Referer", "$referer/")
.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.set("Accept-Language", "en-US,en;q=0.5")
.build()
@ -50,7 +51,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
val subtitleList = buildList {
val subtitles = REGEX_SUBS.findAll(decryptedScript)
subtitles.forEach {
add(Track(it.groupValues[2], decodeUnicodeEscape(it.groupValues[1])))
add(Track(it.groupValues[1], decodeUnicodeEscape(it.groupValues[2])))
}
}
@ -85,5 +86,4 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
@SerialName("chillx") val keys: List<String>
)
}
class ErrorLoadingException(message: String) : Exception(message)

View file

@ -0,0 +1,10 @@
plugins {
id("lib-android")
}
dependencies {
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1") {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
}
implementation(project(":lib:playlist-utils"))
}

View file

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.lib.fireplayerextractor
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
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 okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class FireplayerExtractor(
private val client: OkHttpClient,
private val defaultHost: String? = null,
) {
fun videosFromUrl(
url: String,
videoNameGen: (String) -> String = { quality -> quality },
videoHost: String? = null,
): List<Video> {
val host = videoHost ?: defaultHost ?: "https://${url.toHttpUrl().host}"
val headers = Headers.Builder()
.set("X-Requested-With", "XMLHttpRequest")
.set("Referer", host)
.set("Origin", "https://${host.toHttpUrl().host}")
.set("X-Requested-With", "XMLHttpRequest")
.build()
var id = url.substringAfterLast("/")
if (id.length < 32) {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val script =
doc.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")?.data()
?.replace(Regex("[\\u00E0-\\u00FC]"), "-") // Fix a bug in JsUnpacker with accents
?.let(JsUnpacker::unpackAndCombine)
?: doc.selectFirst("script:containsData(FirePlayer)")?.data()
if (script?.contains("FirePlayer(") == true) {
id = script.substringAfter("FirePlayer(\"").substringBefore('"')
}
}
val postUrl = "$host/player/index.php?data=$id&do=getVideo"
val body = FormBody.Builder()
.add("hash", id)
.add("r", "")
.build()
val masterUrl = client.newCall(POST(postUrl, headers, body = body)).execute()
.body.string()
.substringAfter("securedLink\":\"")
.substringBefore('"')
.replace("\\", "")
val playlistUtils = PlaylistUtils(client, headers)
return playlistUtils.extractFromHls(masterUrl, videoNameGen = videoNameGen)
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,606 @@
"use strict";
// https://megacloud.tv/images/image.png?v=0.1.0
window.decoded_png = new Uint8ClampedArray([
246, 246, 246, 255, 226, 234, 236, 255, 113, 170, 187, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 0, 255, 255, 1, 60, 139, 163, 192, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254,
130, 180, 196, 254, 242, 243, 244, 254, 246, 246, 246, 254, 243, 244, 245, 254, 105, 165, 184, 254, 60, 140,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164,
254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140,
164, 255, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164,
254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140,
164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164,
254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140,
164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 132, 181, 196, 254, 243, 245, 245,
254, 188, 212, 220, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 139, 164,
255, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164,
254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140,
164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 140,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 140, 164,
254, 60, 139, 164, 254, 63, 142, 165, 254, 217, 230, 233, 254, 132, 181, 196, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 255, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 255, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 177,
206, 216, 254, 119, 174, 190, 255, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 164, 198, 210, 255, 119, 174, 190, 254, 60, 140, 164, 254,
60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164,
254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140,
165, 254, 60, 140, 164, 254, 60, 140, 164, 255, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60,
140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164,
254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140,
165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60,
140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164,
254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140,
165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60,
140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164,
254, 60, 140, 164, 254, 60, 140, 165, 254, 60, 140, 164, 254, 60, 140, 164, 254, 60, 140, 164, 255, 163,
198, 210, 254, 119, 174, 190, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 255, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60,
139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139,
164, 254, 60, 139, 164, 255, 60, 139, 164, 254, 163, 198, 210, 254, 119, 174, 190, 254, 60, 139, 164, 254,
60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 255, 60, 140, 164, 254, 60, 139, 164,
254, 60, 139, 164, 254, 60, 140, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 139, 164, 254, 60, 140,
164, 255, 60, 139, 164, 254, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163,
198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 131, 180, 195,
255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255,
233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233,
239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239,
240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240,
255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255,
233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233,
239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 233, 239, 240, 255, 218, 230, 234, 255, 143, 187,
200, 255, 66, 143, 167, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164,
255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139,
164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255,
163, 198, 210, 255, 119, 173, 190, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 139, 185,
199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 223, 233, 236, 255, 136, 183, 197, 255, 69, 145, 168, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164,
255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139,
184, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 242, 243, 244, 255, 219, 231, 235, 255, 217,
229, 233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229,
233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229, 233,
255, 217, 229, 233, 255, 217, 229, 233, 255, 217, 229, 233, 255, 97, 160, 180, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164,
255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 173, 190, 255, 60,
140, 164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 102, 164, 183, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 163, 198, 210,
255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164,
255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102,
164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 173, 190,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163,
198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 184, 199,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60,
139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 173, 190, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255, 119,
174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 241,
243, 244, 255, 227, 235, 238, 255, 243, 245, 245, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 230, 237, 239, 255, 239, 242, 243, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 233, 239, 241, 255, 235, 239, 241, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 239, 242, 243, 255, 230, 236, 239,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 139,
185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 216, 230, 233, 255, 94, 160, 179, 255, 66, 143, 166, 255, 99, 161, 182, 255, 221, 232, 236, 255, 246,
246, 246, 255, 245, 245, 245, 255, 127, 178, 194, 255, 68, 144, 168, 255, 79, 150, 173, 255, 187, 213, 220,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 154, 193, 205, 255, 73, 147, 170, 255, 72, 146, 169,
255, 156, 194, 206, 255, 246, 246, 246, 255, 246, 246, 246, 255, 208, 223, 229, 255, 85, 154, 176, 255, 65,
143, 166, 255, 112, 169, 187, 255, 236, 240, 242, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
140, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60, 140, 164,
255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 137, 184, 197, 255, 60, 140, 165, 255, 60, 140, 164, 255, 60, 140, 164, 255, 88, 156, 177,
255, 210, 226, 230, 255, 133, 181, 196, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 85,
154, 175, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 239, 242, 243, 255, 64, 143, 166, 255, 60, 140, 165, 255, 60,
140, 164, 255, 60, 140, 164, 255, 146, 189, 202, 255, 203, 221, 227, 255, 70, 145, 168, 255, 60, 140, 164,
255, 60, 140, 165, 255, 60, 140, 164, 255, 165, 200, 210, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 140, 164, 255, 60, 140, 164, 255,
60, 140, 164, 255, 163, 198, 210, 255, 119, 173, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 137, 183, 198, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 67, 144, 167, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 86,
154, 175, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 237, 241, 242, 255, 64, 142, 166, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 64, 141, 166, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 164, 200, 211, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 163, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255,
60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 139, 184, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 218, 230, 234, 255, 83, 153, 174, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 67, 144, 167, 255,
174, 206, 215, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 153, 193, 205, 255, 60, 139, 164, 255,
60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 101, 163, 182, 255, 226, 235, 238, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255,
60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139,
164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 213, 227, 231, 255, 85, 154, 175, 255, 60, 140, 164, 255, 60, 140,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 64, 142, 165, 255, 178, 208, 216, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 242, 243, 244, 255, 148, 189, 203,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140,
164, 255, 97, 160, 180, 255, 231, 238, 240, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 173, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 244, 245, 245, 255, 112, 169, 187, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 67, 143, 167, 255, 229, 236, 239,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 200, 220,
226, 255, 61, 141, 165, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 140, 186, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 235, 240, 242, 255, 133, 181, 196, 255, 60, 139, 164, 255, 60, 139, 164, 255,
60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 86, 155, 176,
255, 221, 232, 236, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 203, 221, 227, 255, 70, 145, 168,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 150, 191, 204, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60,
139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 165, 199, 210, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 115, 171, 188, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 245, 245, 245, 255, 83, 153, 174, 255, 60, 140, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140,
164, 255, 60, 140, 164, 255, 61, 141, 165, 255, 192, 215, 222, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60,
140, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 131, 181, 196, 255, 60, 140, 165, 255, 60, 140, 164, 255, 60, 140, 164, 255, 67,
143, 168, 255, 140, 185, 200, 255, 83, 153, 174, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60, 140, 164,
255, 79, 151, 173, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 231, 238, 240, 255, 60, 140, 164, 255, 60, 140, 165, 255,
60, 140, 164, 255, 60, 140, 164, 255, 89, 156, 177, 255, 124, 176, 192, 255, 60, 140, 164, 255, 60, 140,
164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 158, 195, 207, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 140, 164, 255, 60, 140, 164,
255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 173, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 181, 208, 217, 255, 68, 144, 167, 255, 60, 139, 164, 255, 64, 141, 166, 255, 174,
205, 214, 255, 245, 246, 246, 255, 209, 224, 229, 255, 81, 151, 173, 255, 60, 139, 164, 255, 60, 139, 164,
255, 129, 179, 194, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 245, 246, 246, 255, 104, 165, 184, 255, 60, 139, 164,
255, 60, 139, 164, 255, 96, 160, 180, 255, 231, 237, 239, 255, 242, 244, 244, 255, 140, 185, 200, 255, 60,
139, 164, 255, 60, 139, 164, 255, 68, 144, 168, 255, 205, 222, 228, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 163, 183, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255,
60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 192, 215, 222, 255, 155, 194, 206, 255, 194, 216, 223, 255,
245, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 220, 231, 235, 255, 160, 197, 208, 255, 177,
207, 215, 255, 238, 242, 242, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 227, 236, 237, 255,
169, 202, 212, 255, 169, 202, 212, 255, 226, 234, 237, 255, 246, 246, 246, 255, 246, 246, 246, 255, 242,
244, 244, 255, 183, 210, 219, 255, 155, 194, 206, 255, 205, 222, 228, 255, 245, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164,
255, 60, 140, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119,
173, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139,
184, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164,
255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 215, 229, 233, 255,
222, 233, 236, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 223, 233, 236, 255, 213, 227, 232, 255, 245, 245, 245, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60,
140, 164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 242, 244, 244, 255, 111, 169,
186, 255, 60, 140, 165, 255, 64, 142, 166, 255, 132, 181, 196, 255, 216, 229, 233, 255, 246, 246, 246, 255,
246, 246, 246, 255, 222, 233, 236, 255, 164, 199, 210, 255, 97, 161, 180, 255, 73, 147, 170, 255, 106, 166,
184, 255, 178, 207, 216, 255, 235, 239, 242, 255, 246, 246, 246, 255, 241, 243, 244, 255, 201, 221, 226,
255, 117, 172, 189, 255, 61, 141, 165, 255, 61, 140, 164, 255, 138, 185, 198, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
102, 164, 183, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 173,
190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 196, 217, 224,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 74, 147, 171, 255, 132,
181, 195, 255, 150, 191, 204, 255, 77, 149, 172, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 95, 160, 180, 255, 158, 196, 207, 255, 120, 173, 191, 255, 61,
140, 165, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 63, 141, 165, 255, 230, 237, 239,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 102, 163, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255,
119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 202,
221, 226, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 67, 144, 167, 255, 236, 241, 242,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 163, 198, 210, 255,
119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 139, 185, 199, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 136, 183, 198, 255, 64, 142, 166, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 165,
255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 60, 140,
164, 255, 60, 140, 165, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60,
140, 164, 255, 60, 140, 164, 255, 60, 140, 165, 255, 71, 145, 168, 255, 153, 192, 205, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 102, 164, 183, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255,
119, 173, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 206, 224, 229, 255, 118, 172, 190, 255, 64, 141, 165, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 95, 160, 180, 255, 143,
187, 200, 255, 87, 155, 176, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 63, 141, 165, 255, 134, 181, 196, 255, 225, 235, 237, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 102, 163, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 163, 198,
210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 184, 199, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 212, 227, 231, 255,
129, 179, 194, 255, 89, 157, 177, 255, 85, 154, 175, 255, 111, 169, 187, 255, 183, 210, 219, 255, 246, 246,
246, 255, 246, 246, 246, 255, 243, 245, 245, 255, 172, 203, 213, 255, 106, 165, 184, 255, 83, 153, 174, 255,
93, 159, 179, 255, 142, 187, 201, 255, 221, 231, 235, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 139,
185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 244, 245, 246, 255, 244, 245, 245, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 243, 244, 245, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 140, 164,
255, 60, 140, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183,
255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 163, 198, 210, 255, 119, 174, 190, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 102, 163, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 163, 198, 210,
255, 119, 174, 190, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 139, 185, 199, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255,
246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246,
246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246,
246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246,
255, 246, 246, 246, 255, 246, 246, 246, 255, 102, 164, 183, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 163, 198, 210, 255, 129, 180, 195, 255, 60, 140, 164, 255, 60, 140, 165, 255, 60, 140, 164,
255, 93, 158, 178, 255, 158, 196, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159,
197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197,
207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207,
255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255,
159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159,
197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197,
207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207,
255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255,
159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159,
197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 159, 197,
207, 255, 159, 197, 207, 255, 159, 197, 207, 255, 156, 194, 207, 255, 69, 144, 168, 255, 60, 140, 164, 255,
60, 140, 164, 255, 60, 140, 164, 255, 169, 202, 212, 255, 176, 206, 215, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 203, 221, 227,
255, 234, 239, 240, 255, 84, 153, 175, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 107, 167, 185, 255, 241, 243, 244, 255, 246, 246, 246, 255, 203, 221, 227, 255, 85,
154, 176, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164,
255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140,
164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139,
164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60,
140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164,
255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140,
164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 139, 164, 255, 60,
140, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 140, 164, 255, 60, 139, 164,
255, 60, 139, 164, 255, 60, 140, 164, 255, 60, 139, 164, 255, 92, 157, 179, 255, 222, 232, 236, 255, 246,
246, 246, 255,
]);

File diff suppressed because one or more lines are too long

View file

@ -29,6 +29,7 @@ class MegaCloudExtractor(
private val json: Json by injectLazy()
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val webViewResolver by lazy { WebViewResolver(headers) }
private val cacheControl = CacheControl.Builder().noStore().build()
private val noCacheClient = client.newBuilder()
@ -141,10 +142,16 @@ class MegaCloudExtractor(
private fun getVideoDto(url: String): VideoDto {
val type = if (url.startsWith("https://megacloud.tv")) 0 else 1
val keyType = SOURCES_KEY[type]
val id = url.substringAfter(SOURCES_SPLITTER[type], "")
.substringBefore("?", "").ifEmpty { throw Exception("I HATE THE ANTICHRIST") }
if (type == 0) {
return webViewResolver.getSources(id)!!
}
val srcRes = client.newCall(GET(SERVER_URL[type] + SOURCES_URL[type] + id))
.execute()
.body.string()

View file

@ -0,0 +1,111 @@
package eu.kanade.tachiyomi.lib.megacloudextractor
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.lib.megacloudextractor.MegaCloudExtractor.VideoDto
import kotlinx.serialization.json.Json
import okhttp3.Headers
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class WebViewResolver(private val globalHeaders: Headers) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val json: Json by injectLazy()
private val tag by lazy { javaClass.simpleName }
class JsInterface(private val latch: CountDownLatch) {
var result: String? = null
@JavascriptInterface
fun setResponse(response: String) {
Log.d("WebViewResolver", "script result: $response")
result = response
latch.countDown()
}
}
fun getJsContent(file: String): String {
return javaClass.getResource(file)!!.readText()
}
@SuppressLint("SetJavaScriptEnabled")
fun getSources(xrax: String): VideoDto? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsi = JsInterface(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = globalHeaders["User-Agent"]
}
webview.addJavascriptInterface(jsi, "jsinterface")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
Log.d(tag, "onPageFinished $url")
super.onPageFinished(view, url)
Log.d(tag, "injecting scripts")
view?.evaluateJavascript(getJsContent("/assets/crypto-js.js")) {}
view?.evaluateJavascript(getJsContent("/assets/megacloud.decodedpng.js")) {}
view?.evaluateJavascript(getJsContent("/assets/megacloud.getsrcs.js")) {}
Log.d(tag, "running script")
view?.evaluateJavascript(
"getSources(\"${xrax}\")" +
".then( s => jsinterface.setResponse( JSON.stringify(s) ) )",
) {}
}
}
webview.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
Log.d(
tag,
"Chrome: [${consoleMessage?.messageLevel()}]" +
"${consoleMessage?.message()}" +
" at ${consoleMessage?.lineNumber()}" +
" in ${consoleMessage?.sourceId()}",
)
return super.onConsoleMessage(consoleMessage)
}
}
val headers = mapOf("X-Requested-With" to "org.lineageos.jelly")
webView?.loadUrl("https://megacloud.tv/about", headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsi.result?.let { json.decodeFromString<VideoDto>(it) }
}
companion object {
const val TIMEOUT_SEC: Long = 30
}
}

View file

@ -17,7 +17,12 @@ class MixDropExtractor(private val client: OkHttpClient) {
externalSubs: List<Track> = emptyList(),
referer: String = DEFAULT_REFERER,
): List<Video> {
val headers = Headers.headersOf("Referer", referer)
val headers = Headers.headersOf(
"Referer",
referer,
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
)
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
?.data()

View file

@ -1,13 +1,14 @@
ext {
extName = 'Hikari'
extClass = '.Hikari'
extVersionCode = 8
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:vidhide-extractor'))
implementation(project(':lib:chillx-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:vidhide-extractor'))
}

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.chillxextractor.ChillxExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
@ -220,6 +221,7 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val chillxExtractor by lazy { ChillxExtractor(client, headers) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
override fun videoListRequest(episode: SEpisode): Request {
@ -328,17 +330,19 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
}.filter { it.first.isNotEmpty() }
}
val embedUrls = sdEmbedUrls.ifEmpty {
subEmbedUrls + dubEmbedUrls
}
return embedUrls.parallelCatchingFlatMapBlocking {
return sdEmbedUrls.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}.ifEmpty {
(subEmbedUrls + dubEmbedUrls).parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}
}
}
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl, videoNameGen = { s -> "$name - $s" })
embedUrl.contains("filemoon", true) -> filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
name.contains("streamwish", true) -> streamwishExtractor.videosFromUrl(embedUrl, prefix = "$name - ")
else -> chillxExtractor.videoFromUrl(embedUrl, referer = baseUrl, prefix = "$name - ")
}

View file

@ -0,0 +1,8 @@
ext {
extName = 'Jable'
extClass = '.JableFactory'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,253 @@
package eu.kanade.tachiyomi.animeextension.all.jable
import android.app.Application
import android.content.SharedPreferences
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimeUpdateStrategy
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.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class Jable(override val lang: String) : AnimeHttpSource() {
override val baseUrl: String
get() = "https://jable.tv"
override val name: String
get() = "Jable"
override val supportsLatest: Boolean
get() = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json by injectLazy<Json>()
private var tagsUpdated = false
override fun animeDetailsRequest(anime: SAnime): Request {
return GET("$baseUrl${anime.url}?lang=${lang.toRequestLang()}", headers)
}
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup()
return SAnime.create().apply {
val info = doc.select(".info-header")
title = info.select(".header-left h4").text()
author = info.select(".header-left .model")
.joinToString { it.select("span[title]").attr("title") }
genre = doc.select(".tags a").joinToString { it.text() }
update_strategy = AnimeUpdateStrategy.ONLY_FETCH_ONCE
status = SAnime.COMPLETED
description = info.select(".header-right").text()
}
}
override fun episodeListParse(response: Response) = throw UnsupportedOperationException()
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
return listOf(
SEpisode.create().apply {
name = "Episode"
url = anime.url
},
)
}
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val videoUrl = doc.selectFirst("script:containsData(var hlsUrl)")!!.data()
.substringAfter("var hlsUrl = '").substringBefore("'")
return listOf(Video(videoUrl, "Default", videoUrl))
}
override fun latestUpdatesParse(response: Response): AnimesPage {
val doc = response.asJsoup()
if (!tagsUpdated) {
tagsUpdated = preferences.saveTags(
doc.select("a.tag").associate {
it.ownText() to it.attr("href").substringAfter(baseUrl).removePrefix("/")
.removeSuffix("/")
},
)
}
return AnimesPage(
doc.select(".container .video-img-box").map {
SAnime.create().apply {
setUrlWithoutDomain(it.select(".img-box a").attr("href"))
thumbnail_url = it.select(".img-box img").attr("data-src")
title = it.select(".detail .title").text()
}
},
true,
)
}
override fun latestUpdatesRequest(page: Int) =
searchRequest("latest-updates", page, latestFilter)
override fun popularAnimeParse(response: Response): AnimesPage = latestUpdatesParse(response)
override fun popularAnimeRequest(page: Int) =
searchRequest("hot", page, popularFilter)
override fun searchAnimeParse(response: Response) = latestUpdatesParse(response)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotEmpty()) {
searchRequest(
"search/$query",
page,
AnimeFilterList(filters.list + defaultSearchFunctionFilter),
query = query,
)
} else {
val path = filters.list.filterIsInstance<TagFilter>()
.firstOrNull()?.selected?.second?.takeUnless { it.isEmpty() } ?: "hot"
searchRequest(path, page, AnimeFilterList(filters.list + commonVideoListFuncFilter))
}
}
private fun searchRequest(
path: String,
page: Int,
filters: AnimeFilterList = AnimeFilterList(),
query: String = "",
): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
.addPathSegments("$path/")
.addQueryParameter("lang", lang.toRequestLang())
if (tagsUpdated) {
// load whole page for update filter tags info
urlBuilder.addQueryParameter("mode", "async")
}
filters.list.forEach {
when (it) {
is BlockFunctionFilter -> {
urlBuilder.addQueryParameter("function", it.selected.functionName)
.addQueryParameter("block_id", it.selected.blockId)
}
is SortFilter -> {
if (it.selected.second.isNotEmpty()) {
urlBuilder.addQueryParameter("sort_by", it.selected.second)
}
}
else -> {}
}
}
if (query.isNotEmpty()) {
urlBuilder.addQueryParameter("q", query)
}
urlBuilder.addQueryParameter("from", "%02d".format(page))
.addQueryParameter("_", System.currentTimeMillis().toString())
return GET(urlBuilder.build())
}
override fun getFilterList(): AnimeFilterList {
return AnimeFilterList(
SortFilter(
intl.filterPopularSortTitle,
arrayOf(
"" to "",
intl.hotMonth to "video_viewed_month",
intl.hotWeek to "video_viewed_week",
intl.hotDay to "video_viewed_today",
intl.hotAll to "video_viewed",
),
),
TagFilter(
intl.filterTagTitle,
buildList {
add("" to "")
preferences.getTags()?.forEach {
add(it.key to it.value)
}
}.toTypedArray(),
),
SortFilter(
intl.filterTagsSortTitle,
arrayOf(
"" to "",
intl.sortLatestUpdate to "post_date",
intl.sortMostView to "video_viewed",
intl.sortMostFavorite to "most_favourited",
intl.sortRecentBest to "post_date_and_popularity",
),
),
)
}
private fun SharedPreferences.getTags(): Map<String, String>? {
val savedStr = getString("${lang}_$PREF_KEY_TAGS", null)
if (savedStr.isNullOrEmpty()) {
return null
}
return json.decodeFromString<Map<String, String>>(savedStr)
}
private fun SharedPreferences.saveTags(tags: Map<String, String>): Boolean {
if (tags.isNotEmpty()) {
edit().putString("${lang}_$PREF_KEY_TAGS", json.encodeToString(tags)).apply()
return true
}
return false
}
private fun String.toRequestLang(): String {
if (this == "ja") return "jp"
return this
}
private val intl by lazy {
JableIntl(lang)
}
private val commonVideoListFuncFilter by lazy {
BlockFunctionFilter(
intl.popular,
arrayOf(BlockFunction(intl.popular, "list_videos_common_videos_list")),
)
}
private val defaultSearchFunctionFilter by lazy {
BlockFunctionFilter("", arrayOf(BlockFunction("", "list_videos_videos_list_search_result")))
}
private val popularFilter by lazy {
AnimeFilterList(
commonVideoListFuncFilter,
SortFilter(
intl.hotWeek,
arrayOf(intl.hotWeek to "video_viewed_week"),
),
)
}
private val latestFilter by lazy {
AnimeFilterList(
BlockFunctionFilter(
intl.latestUpdate,
arrayOf(BlockFunction(intl.latestUpdate, "list_videos_latest_videos_list")),
),
SortFilter(
intl.sortLatestUpdate,
arrayOf(intl.sortLatestUpdate to "post_date"),
),
)
}
companion object {
const val PREF_KEY_TAGS = "pref_key_tags"
}
}

View file

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.animeextension.all.jable
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class JableFactory : AnimeSourceFactory {
override fun createSources(): List<AnimeSource> {
return listOf(
Jable("zh"),
Jable("en"),
Jable("ja"),
)
}
}

View file

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.animeextension.all.jable
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
data class BlockFunction(
val name: String,
val blockId: String,
val functionName: String = "get_block",
)
class BlockFunctionFilter(name: String, private val functions: Array<BlockFunction>) :
AnimeFilter.Select<String>(name, functions.map { it.name }.toTypedArray()) {
val selected
get() = functions[state]
}
open class UriPartFilter(name: String, private val pairs: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(name, pairs.map { it.first }.toTypedArray()) {
val selected
get() = pairs[state]
}
class SortFilter(name: String, pairs: Array<Pair<String, String>>) : UriPartFilter(name, pairs)
class TagFilter(name: String, pairs: Array<Pair<String, String>>) : UriPartFilter(name, pairs)

View file

@ -0,0 +1,76 @@
package eu.kanade.tachiyomi.animeextension.all.jable
internal interface Intl {
val popular: String
val latestUpdate: String
val sortLatestUpdate: String
val sortMostView: String
val sortMostFavorite: String
val sortRecentBest: String
val hotDay: String
val hotWeek: String
val hotMonth: String
val hotAll: String
val filterPopularSortTitle: String
val filterTagsSortTitle: String
val filterTagTitle: String
}
internal class JableIntl private constructor(delegate: Intl) : Intl by delegate {
constructor(lang: String) : this(
when (lang) {
"zh" -> ZH()
"ja" -> JA()
"en" -> EN()
else -> ZH()
},
)
}
internal class ZH : Intl {
override val popular: String = "熱度優先"
override val latestUpdate: String = "新片優先"
override val sortLatestUpdate: String = "最近更新"
override val sortMostView: String = "最多觀看"
override val sortMostFavorite: String = "最高收藏"
override val sortRecentBest: String = "近期最佳"
override val hotDay: String = "今日熱門"
override val hotWeek: String = "本周熱門"
override val hotMonth: String = "本月熱門"
override val hotAll: String = "所有時間"
override val filterPopularSortTitle: String = "熱門排序"
override val filterTagsSortTitle: String = "通用排序"
override val filterTagTitle: String = "標籤"
}
internal class JA : Intl {
override val popular: String = "人気優先"
override val latestUpdate: String = "新作優先"
override val sortLatestUpdate: String = "最近更新"
override val sortMostView: String = "最も見ら"
override val sortMostFavorite: String = "最もお気に入"
override val sortRecentBest: String = "最近ベスト"
override val hotDay: String = "今日のヒット"
override val hotWeek: String = "今週のヒット"
override val hotMonth: String = "今月のヒット"
override val hotAll: String = "全ての時間"
override val filterPopularSortTitle: String = "人気ソート"
override val filterTagsSortTitle: String = "一般ソート"
override val filterTagTitle: String = "タグ"
}
internal class EN : Intl {
override val popular: String = "Hot"
override val latestUpdate: String = "Newest"
override val sortLatestUpdate: String = "Recent Update"
override val sortMostView: String = "Most Viewed"
override val sortMostFavorite: String = "Most Favorite"
override val sortRecentBest: String = "Best Recently"
override val hotDay: String = "Today"
override val hotWeek: String = "This Week"
override val hotMonth: String = "This Month"
override val hotAll: String = "All Time"
override val filterPopularSortTitle: String = "Popular Sorting"
override val filterTagsSortTitle: String = "General Sorting"
override val filterTagTitle: String = "Tag"
}

View file

@ -0,0 +1,16 @@
ext {
extName = 'JavGG'
extClass = '.Javgg'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:yourupload-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,240 @@
package eu.kanade.tachiyomi.animeextension.all.javgg
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Javgg : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "JavGG"
override val baseUrl = "https://javgg.net"
override val lang = "all"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "StreamWish"
private val SERVER_LIST = arrayOf(
"StreamWish",
"Voe",
"Okru",
"YourUpload",
"FileLions",
"StreamHideVid",
"TurboPlay",
)
}
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val animeDetails = SAnime.create().apply {
status = SAnime.COMPLETED
description = document.selectFirst("#cover")?.text()
}
document.select(".data .boxye2").forEach { element ->
val category = element.select("[id*=owye2]").text().trim()
val tags = element.select(".sgeneros3 a").joinToString { it.text() }
when {
category.contains("Genres:") -> animeDetails.genre = tags
category.contains("Cast:") -> animeDetails.artist = tags
category.contains("Maker:") -> animeDetails.author = tags
}
}
return animeDetails
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/trending/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select("article[id*=post-]")
val nextPage = document.select("#nextpagination").any()
val animeList = elements.map { element ->
SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
title = element.selectFirst(".data h3")!!.text()
thumbnail_url = element.selectFirst(".poster")?.getImageUrl()
}
}
return AnimesPage(animeList, nextPage)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/new-post/page/$page", headers)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return when {
query.isNotBlank() -> GET("$baseUrl/jav/page/$page?s=$query", headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(".result-item article")
val nextPage = document.select("#nextpagination").any()
val animeList = elements.map { element ->
SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
title = element.selectFirst(".details .title")!!.text()
thumbnail_url = element.selectFirst(".image")?.getImageUrl()
}
}
return AnimesPage(animeList, nextPage)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return if (document.select(".dooplay_player_option").any()) {
listOf(
SEpisode.create().apply {
name = "Episode 1"
episode_number = 1F
setUrlWithoutDomain(document.location())
},
)
} else {
emptyList()
}
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return document.select("[id*=source-player] iframe").parallelCatchingFlatMapBlocking {
val numOpt = it.closest(".source-box")?.attr("id")?.replace("source-player-", "")
val serverName = document.select("[data-nume=\"$numOpt\"] .server").text()
serverVideoResolver(serverName, it.attr("src"))
}
}
private fun serverVideoResolver(server: String, url: String): List<Video> {
val embedUrl = server.lowercase()
return when {
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(url)
embedUrl.contains("filelions") || embedUrl.contains("lion") -> StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "FileLions:$it" })
embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish") -> {
val docHeaders = headers.newBuilder()
.add("Origin", "https://streamwish.to")
.add("Referer", "https://streamwish.to/")
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" })
}
embedUrl.contains("vidhide") || embedUrl.contains("streamhide") ||
embedUrl.contains("guccihide") || embedUrl.contains("streamvid") -> StreamHideVidExtractor(client).videosFromUrl(url)
embedUrl.contains("voe") -> VoeExtractor(client).videosFromUrl(url)
embedUrl.contains("yourupload") || embedUrl.contains("upload") -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
embedUrl.contains("turboplay") -> {
val turboDocument = client.newCall(GET(url)).execute().asJsoup()
val masterUrl = turboDocument.select("#video_player").attr("data-hash")
val customHeaders = headers.newBuilder().apply {
add("Accept", "*/*")
add("Origin", "https://${turboDocument.location().toHttpUrl().host}")
add("Referer", "https://${turboDocument.location().toHttpUrl().host}/")
}.build()
listOf(Video(masterUrl, "TurboPlay", masterUrl, customHeaders))
}
else -> emptyList()
}
}
private fun org.jsoup.nodes.Element.getImageUrl(): String? {
val imageLinkRegex = """https?://[^\s]+\.(jpg|png)""".toRegex()
for (link in this.select("[href], [src]")) {
val href = link.attr("href")
val src = link.attr("src")
if (imageLinkRegex.matches(href)) {
return href
}
if (imageLinkRegex.matches(src)) {
return src
}
}
val textMatches = imageLinkRegex.find(this.text())
val htmlMatches = imageLinkRegex.find(this.outerHtml())
return textMatches?.value ?: htmlMatches?.value
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(server, true) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = QUALITY_LIST
entryValues = QUALITY_LIST
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)
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Jav Guru'
extClass = '.JavGuru'
extVersionCode = 16
extVersionCode = 17
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Sudatchi'
extClass = '.Sudatchi'
extVersionCode = 4
extVersionCode = 5
isNsfw = true
}

View file

@ -39,7 +39,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override val baseUrl = "https://sudatchi.com"
private val ipfsUrl = "https://gboesk298le91ct41kibaonc7o.ingress.akashprovid.com"
private val ipfsUrl = "https://ipfs.sudatchi.com"
override val lang = "all"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 1
extVersionCode = 2
containsNsfw = false
}

View file

@ -602,6 +602,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"AllDebrid",
"DebridLink",
"Offcloud",
"TorBox",
)
private val PREF_DEBRID_VALUES = arrayOf(
"none",
@ -610,6 +611,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"alldebrid",
"debridlink",
"offcloud",
"torbox",
)
// Sort

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio Anime (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 9
extVersionCode = 11
containsNsfw = false
}

View file

@ -375,6 +375,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
?.let { videos ->
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.released?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
}
?.filter { it.thumbnail != null }
?.map { video ->
SEpisode.create().apply {
episode_number = video.episode?.toFloat() ?: 0.0F
@ -663,6 +664,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"AllDebrid",
"DebridLink",
"Offcloud",
"TorBox",
)
private val PREF_DEBRID_VALUES = arrayOf(
"none",
@ -671,6 +673,7 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
"alldebrid",
"debridlink",
"offcloud",
"torbox",
)
// Sort

View file

@ -150,4 +150,5 @@ data class EpisodeVideo(
val episode: Int? = null,
val released: String? = null,
val title: String? = null,
val thumbnail: String? = null,
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime4up'
extClass = '.Anime4Up'
extVersionCode = 58
extVersionCode = 59
}
apply from: "$rootDir/common.gradle"

View file

@ -38,7 +38,7 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Anime4Up"
override val baseUrl = "https://anime4up.cam"
override val baseUrl = "https://anime4up.rest"
override val lang = "ar"
@ -136,7 +136,6 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
episode_number = name.substringAfterLast(" ").toFloatOrNull() ?: 0F
}
// ============================ Video Links =============================
@Serializable
data class Qualities(
val fhd: Map<String, String> = emptyMap(),
@ -144,14 +143,47 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val sd: Map<String, String> = emptyMap(),
)
@Serializable
data class WatchServerData(
val name: String,
val link: String,
val order: String,
val icon: Boolean,
)
override fun videoListParse(response: Response): List<Video> {
val base64 = response.asJsoup().selectFirst("input[name=wl]")
val document = response.asJsoup()
// Decode base64 for each quality level
val base64Fhd = document.selectFirst(".WatchServersEmbed form input[name='watch_fhd']")
?.attr("value")
?.let { String(Base64.decode(it, Base64.DEFAULT)) }
?: return emptyList()
?: "[]"
val parsedData = json.decodeFromString<Qualities>(base64)
val streamLinks = with(parsedData) { fhd + hd + sd }
val base64Hd = document.selectFirst(".WatchServersEmbed form input[name='watch_hd']")
?.attr("value")
?.let { String(Base64.decode(it, Base64.DEFAULT)) }
?: "[]"
val base64Sd = document.selectFirst(".WatchServersEmbed form input[name='watch_SD']")
?.attr("value")
?.let { String(Base64.decode(it, Base64.DEFAULT)) }
?: "[]"
// Parse the base64 decoded strings into lists of WatchServerData
val parsedFhd = json.decodeFromString<List<WatchServerData>>(base64Fhd)
val parsedHd = json.decodeFromString<List<WatchServerData>>(base64Hd)
val parsedSd = json.decodeFromString<List<WatchServerData>>(base64Sd)
// Convert to the old Qualities structure
val qualities = Qualities(
fhd = parsedFhd.associate { it.name to it.link },
hd = parsedHd.associate { it.name to it.link },
sd = parsedSd.associate { it.name to it.link },
)
// Use the same logic as the old implementation
val streamLinks = with(qualities) { fhd + hd + sd }
return streamLinks.values.distinct().flatMap(::extractVideos)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Egy Dead'
extClass = '.EgyDead'
extVersionCode = 13
extVersionCode = 14
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'CineClix'
extClass = '.CineClix'
extVersionCode = 15
extVersionCode = 16
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Einfach'
extClass = '.Einfach'
extVersionCode = 11
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Movie2k'
extClass = '.Movie2k'
extVersionCode = 6
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Serienstream'
extClass = '.Serienstream'
extVersionCode = 19
extVersionCode = 20
}
apply from: "$rootDir/common.gradle"
@ -10,4 +10,4 @@ dependencies {
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:dood-extractor'))
}
}

View file

@ -37,7 +37,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Serienstream"
override val baseUrl = "http://186.2.175.5"
override val baseUrl = "https://s.to"
override val lang = "de"
@ -91,7 +91,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val headers = Headers.Builder()
.add("Referer", "http://186.2.175.5/search")
.add("Referer", "https://s.to/search")
.add("origin", baseUrl)
.add("connection", "keep-alive")
.add("user-agent", "Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36")

View file

@ -1,7 +1,7 @@
ext {
extName = 'StreamCloud'
extClass = '.StreamCloud'
extVersionCode = 8
extVersionCode = 9
}
apply from: "$rootDir/common.gradle"

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay'
extClass = '.AniPlay'
themePkg = 'anilist'
overrideVersionCode = 3
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.multisrc.anilist.AniListAnimeHttpSource
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
@ -31,6 +31,7 @@ import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
@Suppress("unused")
class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "AniPlay"
override val lang = "en"
@ -67,6 +68,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}
}
private val baseHost: String get() = "${preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)}"
/* ====================================== Episode List ====================================== */
override fun episodeListRequest(anime: SAnime): Request {
@ -79,7 +82,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
val headersWithAction =
headers.newBuilder()
// next.js stuff I guess
.add("Next-Action", HEADER_NEXT_ACTION_EPISODE_LIST_VALUE)
.add("Next-Action", getHeaderValue(baseHost, NEXT_ACTION_EPISODE_LIST))
.build()
return POST(url = "$baseUrl/anime/info/$animeId", headersWithAction, requestBody)
@ -91,7 +94,11 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
val animeId = episodeListUrl.pathSegments[2]
val responseString = response.body.string()
val episodesArrayString = responseString.split("1:").last()
val episodesArrayString = extractEpisodeList(responseString)
if (episodesArrayString == null) {
Log.e("AniPlay", "Episode list not found - ${response.request}\nbody:${response.request.body}\n${responseString.substring(0,200)}")
throw Exception("Episode list not found")
}
val providers = episodesArrayString.parseAs<List<EpisodeListResponse>>()
val episodes = mutableMapOf<Int, EpisodeListResponse.Episode>()
@ -108,7 +115,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
source = provider.providerId,
episodeId = episode.id,
episodeNum = episode.number,
hasDub = episode.hasDub,
hasDub = episode.hasDub ?: false,
)
episodeExtras[episodeNumber] = existingEpisodeExtras + listOf(episodeExtra)
}
@ -136,7 +143,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
else -> ""
}
val filler = when {
episode.isFiller && isMarkFiller -> " • Filler Episode"
episode.isFiller == true && isMarkFiller -> " • Filler Episode"
else -> ""
}
val scanlator = "Sub$dub$filler"
@ -156,7 +163,6 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val episodeUrl = episode.url.toHttpUrl()
val animeId = episodeUrl.queryParameter("id") ?: return emptyList()
// val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList()
val extras = episodeUrl.queryParameter("extras")
?.let {
try {
@ -170,92 +176,158 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
try {
json.decodeFromString<List<EpisodeExtra>>(it)
} catch (e: SerializationException) {
Log.e("AniPlay", "Error parsing JSON", e)
Log.e("AniPlay", "Error parsing JSON extras", e)
emptyList()
}
}
?: emptyList()
val headersWithAction =
headers.newBuilder()
// next.js stuff I guess
.add("Next-Action", HEADER_NEXT_ACTION_SOURCES_LIST_VALUE)
.build()
val episodeDataList = extras.parallelFlatMapBlocking { extra ->
var timeouts = 0
var maxTimeout = 0
val videos = extras.parallelFlatMap { extra ->
val languages = mutableListOf("sub").apply {
if (extra.hasDub) add("dub")
}
languages.map { language ->
languages.parallelFlatMap { language ->
val epNum = if (extra.episodeNum == extra.episodeNum.toInt().toFloat()) {
extra.episodeNum.toInt().toString() // If it has no fractional part, convert it to an integer
extra.episodeNum.toInt().toString()
} else {
extra.episodeNum.toString() // If it has a fractional part, leave it as a float
extra.episodeNum.toString()
}
val requestBody = "[\"$animeId\",\"${extra.source}\",\"${extra.episodeId}\",\"$epNum\",\"$language\"]"
.toRequestBody("application/json".toMediaType())
val params = mapOf(
"id" to animeId,
"host" to extra.source,
"ep" to epNum,
"type" to language,
)
val builder = Uri.parse("$baseUrl/anime/watch").buildUpon()
val builder = Uri.parse("$baseUrl/anime/watch/$animeId").buildUpon()
params.map { (k, v) -> builder.appendQueryParameter(k, v); }
val url = builder.build().toString()
Log.i("AniPlay", "Url: $url")
val url = builder.build()
val headersWithAction =
headers.newBuilder()
.add("Next-Action", getHeaderValue(baseHost, NEXT_ACTION_SOURCES_LIST))
.build()
val requestBody = "[\"$animeId\",\"${extra.source}\",\"${extra.episodeId}\",\"$epNum\",\"$language\"]"
.toRequestBody("application/json".toMediaType())
val request = POST(url.toString(), headersWithAction, requestBody)
maxTimeout += 1
try {
val request = POST(url, headersWithAction, requestBody)
val response = client.newCall(request).execute()
getVideos(extra, language, request)
} catch (e: java.net.SocketTimeoutException) {
Log.e("AniPlay", "VideoList $url SocketTimeoutException", e)
timeouts++
emptyList()
} catch (e: IOException) {
Log.e("AniPlay", "VideoList $url IOException", e)
emptyList()
} catch (e: Exception) {
Log.e("AniPlay", "VideoList $url Exception", e)
emptyList()
}
}
}
val responseString = response.body.string()
val sourcesString = responseString.split("1:").last()
if (sourcesString.startsWith("null")) return@map null
val data = sourcesString.parseAs<VideoSourceResponse>()
if (videos.isEmpty() && timeouts != 0 && maxTimeout == timeouts) {
throw Exception("Timed out")
}
return videos.sort()
}
private fun getVideos(extra: EpisodeExtra, language: String, request: Request): List<Video> {
val response = client.newCall(request).execute()
val responseString = response.body.string()
val sourcesString = extractSourcesList(responseString) ?: return emptyList()
Log.i("AniPlay", "${extra.source} $language -> $sourcesString")
when (extra.source.lowercase()) {
"yuki" -> {
val data = sourcesString.parseAs<VideoSourceResponseYuki>()
return processEpisodeDataYuki(
EpisodeDataYuki(
source = extra.source,
language = language,
response = data,
),
)
}
else -> {
val data = sourcesString.parseAs<VideoSourceResponse>()
return processEpisodeData(
EpisodeData(
source = extra.source,
language = language,
response = data,
)
} catch (e: IOException) {
Log.w("AniPlay", "VideoList $url IOException", e)
null // Return null to be filtered out
} catch (e: Exception) {
Log.w("AniPlay", "VideoList $url Exception", e)
null // Return null to be filtered out
}
}.filterNotNull() // Filter out null values due to errors
),
)
}
}
}
private fun processEpisodeDataYuki(episodeData: EpisodeDataYuki): List<Video> {
val defaultSource = episodeData.response.sources?.firstOrNull()
if (defaultSource == null) {
Log.e("AniPlay", "defaultSource is null (${episodeData.response})")
return emptyList()
}
val videos = episodeDataList.flatMap { episodeData ->
val defaultSource = episodeData.response.sources?.firstOrNull {
it.quality in listOf("default", "auto")
} ?: return@flatMap emptyList()
val subtitles = episodeData.response.tracks
?.filter { it.kind?.lowercase() == "captions" }
?.map { Track(it.file, it.label ?: "Unknown") }
?: emptyList()
val subtitles = episodeData.response.subtitles
?.filter { it.lang != "Thumbnails" }
?.map { Track(it.url, it.lang) }
?: emptyList()
val serverName = getServerName(episodeData.source)
val typeName = getTypeName(episodeData.language).let {
if (it == "Sub" && subtitles.isNotEmpty()) "SoftSub" else it
}
playlistUtils.extractFromHls(
try {
return playlistUtils.extractFromHls(
playlistUrl = defaultSource.url,
videoNameGen = { quality ->
val serverName = getServerName(episodeData.source)
val typeName = when {
subtitles.isNotEmpty() -> "SoftSub"
else -> getTypeName(episodeData.language)
}
"$serverName - $quality - $typeName"
},
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
subtitleList = subtitles,
)
} catch (e: Exception) {
Log.e("AniPlay", "processEpisodeDataYuki extractFromHls Error (\"$serverName - $typeName\"): $e")
}
return videos.sort()
return emptyList()
}
private fun processEpisodeData(episodeData: EpisodeData): List<Video> {
val defaultSource = episodeData.response.sources?.firstOrNull {
it.quality in listOf("default", "auto")
} ?: return emptyList()
val subtitles = episodeData.response.subtitles
?.filter { it.lang?.lowercase() != "thumbnails" }
?.map { Track(it.url, it.lang ?: "Unk") }
?: emptyList()
val serverName = getServerName(episodeData.source)
val typeName = when {
subtitles.isNotEmpty() -> "SoftSub"
else -> getTypeName(episodeData.language)
}
try {
return playlistUtils.extractFromHls(
playlistUrl = defaultSource.url,
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
subtitleList = subtitles,
)
} catch (e: Exception) {
Log.e("AniPlay", "processEpisodeData extractFromHls Error (\"$serverName - $typeName\"): $e")
}
return emptyList()
}
override fun List<Video>.sort(): List<Video> {
@ -270,6 +342,34 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
)
}
private fun extractEpisodeList(input: String): String? {
return extractList(input, '[', ']')
}
private fun extractSourcesList(input: String): String? {
return extractList(input, '{', '}')
}
private fun extractList(input: String, bracket1: Char, bracket2: Char): String? {
val startMarker = "1:$bracket1"
val list1Index = input.indexOf(startMarker)
if (list1Index == -1) return null
val startIndex = list1Index + startMarker.length
var endIndex = startIndex
var bracketCount = 1
while (endIndex < input.length && bracketCount > 0) {
when (input[endIndex]) {
bracket1 -> bracketCount++
bracket2 -> bracketCount--
}
endIndex++
}
return if (bracketCount == 0) input.substring(startIndex - 1, endIndex) else null
}
/* ====================================== Preferences ====================================== */
override fun setupPreferenceScreen(screen: PreferenceScreen) {
@ -376,11 +476,17 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}
private fun getServerName(value: String): String {
val index = PREF_SERVER_ENTRY_VALUES.indexOf(value)
if (index == -1) {
return "Other"
}
return PREF_SERVER_ENTRIES[index]
}
private fun getTypeName(value: String): String {
val index = PREF_TYPE_ENTRY_VALUES.indexOf(value)
val index = PREF_TYPE_ENTRY_VALUES.indexOf(value.lowercase())
if (index == -1) {
return "Other"
}
return PREF_TYPE_ENTRIES[index]
}
@ -391,6 +497,10 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
} ?: 0L
}
private fun getHeaderValue(serverHost: String, key: String): String {
return HEADER_NEXT_ACTION[serverHost]?.get(key) ?: throw Exception("Bad host/key")
}
companion object {
private const val PREF_DOMAIN_KEY = "domain"
private val PREF_DOMAIN_ENTRIES = arrayOf("aniplaynow.live (default)", "aniplay.lol (backup)")
@ -398,8 +508,8 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_DOMAIN_DEFAULT = "aniplaynow.live"
private const val PREF_SERVER_KEY = "server"
private val PREF_SERVER_ENTRIES = arrayOf("Kuro (Gogoanime)", "Yuki (HiAnime)", "Yuno (Yugenanime)")
private val PREF_SERVER_ENTRY_VALUES = arrayOf("kuro", "yuki", "yuno")
private val PREF_SERVER_ENTRIES = arrayOf("Kuro", "Anya", "Yuki")
private val PREF_SERVER_ENTRY_VALUES = arrayOf("kuro", "anya", "yuki")
private const val PREF_SERVER_DEFAULT = "kuro"
private const val PREF_QUALITY_KEY = "quality"
@ -421,8 +531,19 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
private const val PREF_MARK_FILLER_EPISODE_DEFAULT = true
// These values has probably something to do with Next.js server and hydration
private const val HEADER_NEXT_ACTION_EPISODE_LIST_VALUE = "f3422af67c84852f5e63d50e1f51718f1c0225c4"
private const val HEADER_NEXT_ACTION_SOURCES_LIST_VALUE = "5dbcd21c7c276c4d15f8de29d9ef27aef5ea4a5e"
private const val NEXT_ACTION_EPISODE_LIST = "NEXT_ACTION_EPISODE_LIST"
private const val NEXT_ACTION_SOURCES_LIST = "NEXT_ACTION_SOURCES_LIST"
private val HEADER_NEXT_ACTION = mapOf(
PREF_DOMAIN_ENTRY_VALUES[0] to mapOf(
"NEXT_ACTION_EPISODE_LIST" to "f3422af67c84852f5e63d50e1f51718f1c0225c4",
"NEXT_ACTION_SOURCES_LIST" to "5dbcd21c7c276c4d15f8de29d9ef27aef5ea4a5e",
),
PREF_DOMAIN_ENTRY_VALUES[1] to mapOf(
"NEXT_ACTION_EPISODE_LIST" to "56e4151352ded056cbe226d2376c7436cffc9a37",
"NEXT_ACTION_SOURCES_LIST" to "8a76af451978c817dde2364326a5e4e45eb43db1",
),
)
private val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.animeextension.en.aniplay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@ -13,29 +12,15 @@ data class EpisodeListResponse(
data class Episode(
val id: String,
val number: Float,
val title: String,
val hasDub: Boolean,
val isFiller: Boolean,
val title: String?,
val hasDub: Boolean?,
val isFiller: Boolean?,
val img: String?,
val description: String?,
val createdAt: String?,
)
}
@Serializable
data class VideoSourceRequest(
val source: String,
@SerialName("episodeid")
val episodeId: String,
@SerialName("episodenum")
val episodeNum: String,
@SerialName("subtype")
val subType: String,
)
@Serializable
data class VideoSourceResponse(
val sources: List<Source>?,
@ -44,13 +29,35 @@ data class VideoSourceResponse(
@Serializable
data class Source(
val url: String,
val quality: String,
val quality: String?,
)
@Serializable
data class Subtitle(
val url: String,
val lang: String,
val lang: String?,
)
}
@Serializable
data class VideoSourceResponseYuki(
val sources: List<Source>?,
val tracks: List<Subtitle>?,
val anilistID: Int?,
val malID: Int?,
) {
@Serializable
data class Source(
val url: String,
val type: String?,
)
@Serializable
data class Subtitle(
val file: String,
val label: String?,
val kind: String?,
val default: Boolean?,
)
}
@ -68,3 +75,10 @@ data class EpisodeData(
val language: String,
val response: VideoSourceResponse,
)
@Serializable
data class EpisodeDataYuki(
val source: String,
val language: String,
val response: VideoSourceResponseYuki,
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'AsiaFlix'
extClass = '.AsiaFlix'
extVersionCode = 11
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AsianLoad'
extClass = '.AsianLoad'
extVersionCode = 40
extVersionCode = 41
}
apply from: "$rootDir/common.gradle"

View file

@ -2,8 +2,8 @@ ext {
extName = 'DonghuaStream'
extClass = '.DonghuaStream'
themePkg = 'animestream'
baseUrl = 'https://donghuastream.co.in'
overrideVersionCode = 6
baseUrl = 'https://donghuastream.org'
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
class DonghuaStream : AnimeStream(
"en",
"DonghuaStream",
"https://donghuastream.co.in",
"https://donghuastream.org",
) {
override val fetchFilters: Boolean
get() = false

View file

@ -1,7 +1,7 @@
ext {
extName = 'DramaCool'
extClass = '.DramaCool'
extVersionCode = 50
extVersionCode = 51
}
apply from: "$rootDir/common.gradle"

View file

@ -29,7 +29,7 @@ class DramaCool : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// TODO: Check frequency of url changes to potentially
// add back overridable baseurl preference
override val baseUrl = "https://asianc.sh/"
override val baseUrl = "https://asianc.co/"
override val lang = "en"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Kaido'
themePkg = 'zorotheme'
baseUrl = 'https://kaido.to'
overrideVersionCode = 6
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Multimovies'
themePkg = 'dooplay'
baseUrl = 'https://multimovies.art'
overrideVersionCode = 18
overrideVersionCode = 19
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'My Running Man'
extClass = '.MyRunningMan'
extVersionCode = 5
extVersionCode = 6
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.PinoyMoviePedia'
themePkg = 'dooplay'
baseUrl = 'https://pinoymoviepedia.ru'
overrideVersionCode = 0
overrideVersionCode = 1
isNsfw = true
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Rule34Video'
extClass = '.Rule34Video'
extVersionCode = 8
extVersionCode = 9
isNsfw = true
}

View file

@ -1,8 +1,10 @@
package eu.kanade.tachiyomi.animeextension.en.rule34video
import android.app.Application
import android.util.Log
import androidx.preference.EditTextPreference
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
@ -41,7 +43,21 @@ class Rule34Video : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/latest-updates/$page/")
override fun popularAnimeRequest(page: Int): Request {
return if (preferences.getBoolean(PREF_UPLOADER_FILTER_ENABLED_KEY, false)) {
val uploaderId = preferences.getString(PREF_UPLOADER_ID_KEY, "") ?: ""
if (uploaderId.isNotBlank()) {
val url = "$baseUrl/members/$uploaderId/videos/?mode=async&function=get_block&block_id=list_videos_uploaded_videos&sort_by=&from_videos=$page"
Log.e("Rule34Video", "Loading popular videos from uploader ID: $uploaderId, page: $page, URL: $url")
GET(url)
} else {
Log.e("Rule34Video", "Uploader filter enabled but ID is blank, loading latest updates.")
GET("$baseUrl/latest-updates/$page/")
}
} else {
GET("$baseUrl/latest-updates/$page/")
}
}
override fun popularAnimeSelector() = "div.item.thumb"
@ -102,21 +118,60 @@ class Rule34Video : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("h1.title_video")!!.text()
val info = document.selectFirst("#tab_video_info")!!
author = info.select("div.label:contains(Artist:) + a").eachText().joinToString()
title = document.selectFirst("h1.title_video")?.text().toString()
val infoRow = document.selectFirst("div.info.row")
val detailRows = document.select("div.row")
val artistElement = detailRows.select("div.col:has(div.label:contains(Artist)) a.item span.name").firstOrNull()
author = artistElement?.text().orEmpty()
description = buildString {
info.selectFirst("div.label:contains(Description:) > em")?.text()?.also { append("$it\n") }
info.selectFirst("i.icon-eye + span")?.text()?.also { append("\nViews : ${it.replace(" ", ",")}") }
info.selectFirst("i.icon-clock + span")?.text()?.also { append("\nDuration : $it") }
document.select("div.label:contains(Download) ~ a.tag_item")
detailRows.select("div.row:has(div.label > em) > div.label > em").html()
.replace("<br>", "\n") // Ensure single <br> tags are followed by a newline
.let { text ->
append(text)
}
append("\n\n") // Add extra spacing
infoRow?.selectFirst("div.item_info:nth-child(1) > span")?.text()?.let {
append("Uploaded: $it\n")
}
val artist = detailRows.select("div.col:has(div.label:contains(Artist)) a.item span.name")
.eachText()
.joinToString()
if (artist.isNotEmpty()) {
append("Artists: $artist\n")
}
val categories = detailRows.select("div.col:has(div.label:contains(Categories)) a.item span")
.eachText()
.joinToString()
if (categories.isNotEmpty()) {
append("Categories: $categories\n")
}
val uploader = detailRows.select("div.col:has(div.label:contains(Uploaded by)) a.item").text()
if (uploader.isNotEmpty()) {
append("Uploader: $uploader\n")
}
infoRow?.select("div.item_info:nth-child(2) > span")?.text()?.let {
val views = it.substringBefore(" ").replace(",", "")
append("Views: $views\n")
}
infoRow?.select("div.item_info:nth-child(3) > span")?.text()?.let { append("Duration: $it\n") }
document.select("div.row:has(div.label:contains(Download)) a.tag_item")
.eachText()
.joinToString { it.substringAfter(" ") }
.also { append("\nQuality : $it") }
.also { append("Quality: $it") }
}
genre = document.select("div.label:contains(Tags) ~ a.tag_item:not(:contains(Suggest))")
genre = document.select("div.row_spacer:has(div.label:contains(Tags)) a.tag_item:not(:contains(Suggest))")
.eachText()
.joinToString()
status = SAnime.COMPLETED
}
@ -186,6 +241,24 @@ class Rule34Video : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_UPLOADER_FILTER_ENABLED_KEY
title = "Filter by Uploader"
summary = "Load videos only from the specified uploader ID."
setDefaultValue(false)
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_UPLOADER_ID_KEY
title = "Uploader ID"
summary = "Enter the ID of the uploader (e.g., 98965). Requires \"Filter by Uploader\" to be enabled."
dialogTitle = "Enter Uploader ID"
var dependency = PREF_UPLOADER_FILTER_ENABLED_KEY
setOnPreferenceChangeListener { _, newValue ->
newValue?.toString().isNullOrBlank().not()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
@ -218,14 +291,20 @@ class Rule34Video : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return tagList.toTypedArray()
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
OrderFilter(),
CategoryBy(),
AnimeFilter.Separator(),
AnimeFilter.Header("Entered a \"tag\", click on \"filter\" then Click \"reset\" to load tags."),
TagFilter(),
TagSearch(tagsResults(tagDocument)),
)
override fun getFilterList(): AnimeFilterList = if (preferences.getBoolean(PREF_UPLOADER_FILTER_ENABLED_KEY, false) &&
preferences.getString(PREF_UPLOADER_ID_KEY, "")?.isNotBlank() == true
) {
AnimeFilterList() // If uploader filter is enabled and ID is set, show no other filters
} else {
AnimeFilterList(
OrderFilter(),
CategoryBy(),
AnimeFilter.Separator(),
AnimeFilter.Header("Entered a \"tag\", click on \"filter\" then Click \"reset\" to load tags."),
TagFilter(),
TagSearch(tagsResults(tagDocument)),
)
}
private class TagFilter : AnimeFilter.Text("Click \"reset\" without any text to load all A-Z tags.", "")
@ -267,5 +346,8 @@ class Rule34Video : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("2160p", "1080p", "720p", "480p", "360p")
private const val PREF_UPLOADER_FILTER_ENABLED_KEY = "uploader_filter_enabled"
private const val PREF_UPLOADER_ID_KEY = "uploader_id"
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Tokuzilla'
extClass = '.Tokuzilla'
extVersionCode = 17
extVersionCode = 20
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.HiAnime'
themePkg = 'zorotheme'
baseUrl = 'https://hianime.to'
overrideVersionCode = 40
overrideVersionCode = 44
}
apply from: "$rootDir/common.gradle"

View file

@ -4,6 +4,8 @@ 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
import eu.kanade.tachiyomi.network.GET
import okhttp3.Request
class HiAnime : ZoroTheme(
"en",
@ -22,6 +24,8 @@ class HiAnime : ZoroTheme(
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers, preferences) }
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/recently-updated?page=$page", docHeaders)
override fun extractVideo(server: VideoData): List<Video> {
return when (server.name) {
"StreamTape" -> {

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animefenix'
extClass = '.Animefenix'
extVersionCode = 40
extVersionCode = 42
}
apply from: "$rootDir/common.gradle"

View file

@ -78,7 +78,7 @@ object AnimeFenixFilters {
class SortFilter : QueryPartFilter("Orden", AnimeFenixFiltersData.SORT)
private object AnimeFenixFiltersData {
object AnimeFenixFiltersData {
val YEARS = (1990..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.reversed().toTypedArray()
val TYPES = arrayOf(

View file

@ -46,6 +46,23 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
private val preferences: SharedPreferences by lazy { Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) }
private object CSSQuery {
object AnimeList {
const val GRID = "main div.container div.grid"
const val ELEMENT = "a"
const val ELEMENT_URL = "a"
const val ELEMENT_TITLE = "div div h3"
const val ELEMENT_THUMBNAIL_URL = "img"
const val ELEMENT_STATUS = "div div span.bg-zinc-700"
const val NEXT = "a:has(span.sr-only:contains(Next))"
}
object EpisodeList {
const val EPISODE = "div.container div.bg-zinc-800 ul li"
const val NUMBER = "a span span"
const val URL = "a"
}
}
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
@ -65,14 +82,17 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select("article.serie-card")
val nextPage = document.select("ul.pagination-list li a.pagination-link:contains(Siguiente)").any()
val grid = document.select(CSSQuery.AnimeList.GRID)[1]
val elements = grid.select(CSSQuery.AnimeList.ELEMENT)
val nextPage = document.select(CSSQuery.AnimeList.NEXT).any()
val animeList = elements.map { element ->
SAnime.create().apply {
setUrlWithoutDomain(element.select("figure.image a").attr("abs:href"))
title = element.select("div.title h3 a").text()
thumbnail_url = element.select("figure.image a img").attr("abs:src")
description = element.select("div.serie-card__information p").text()
setUrlWithoutDomain(element.select(CSSQuery.AnimeList.ELEMENT_URL).attr("abs:href"))
title = element.select(CSSQuery.AnimeList.ELEMENT_TITLE).text()
thumbnail_url = element.select(CSSQuery.AnimeList.ELEMENT_THUMBNAIL_URL).attr("abs:src")
status = parseStatus(element.select(CSSQuery.AnimeList.ELEMENT_STATUS).text())
}
}
return AnimesPage(animeList, nextPage)
@ -96,12 +116,12 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select("ul.anime-page__episode-list.is-size-6 li").map { it ->
val epNum = it.select("a span").text().replace("Episodio", "")
return document.select(CSSQuery.EpisodeList.EPISODE).map { it ->
val epNum = it.select(CSSQuery.EpisodeList.NUMBER).text().replace("Episodio", "")
SEpisode.create().apply {
episode_number = epNum.toFloat()
name = "Episodio $epNum"
setUrlWithoutDomain(it.select("a").attr("abs:href"))
setUrlWithoutDomain(it.select(CSSQuery.EpisodeList.URL).attr("abs:href"))
}
}
}
@ -110,7 +130,9 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
val servers = document.selectFirst("script:containsData(var tabsArray)")!!.data()
.split("tabsArray").map { it.substringAfter("src='").substringBefore("'").replace("amp;", "") }
.split("\n")
.filter { it.contains("tabsArray[") }
.map { it.split(" = ").last().trim().trim(';').trim('"') }
.filter { it.contains("https") }
servers.forEach { server ->
@ -216,12 +238,13 @@ class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
).reversed()
}
override fun animeDetailsParse(response: Response): SAnime {
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val document = response.asJsoup()
return SAnime.create().apply {
title = document.select("h1.title.has-text-orange").text()
genre = document.select("a.button.is-small.is-orange.is-outlined.is-roundedX").joinToString { it.text() }
status = parseStatus(document.select("div.column.is-12-mobile.xis-3-tablet.xis-3-desktop.xhas-background-danger.is-narrow-tablet.is-narrow-desktop a").text())
with(document.selectFirst("main > div.relative > div.container > div.flex")!!) {
title = selectFirst("h1.font-bold")!!.ownText()
genre = select("div:has(h2:containsOwn(Géneros)) > div.flex > a").joinToString { it.text() }
status = parseStatus(selectFirst("li:has(> span:containsOwn(Estado))")!!.ownText())
description = select("div:has(h2:containsOwn(Sinopsis)) > p").text()
}
}

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeOnlineNinja'
themePkg = 'dooplay'
baseUrl = 'https://ww3.animeonline.ninja'
overrideVersionCode = 39
overrideVersionCode = 41
}
apply from: "$rootDir/common.gradle"
@ -14,4 +14,7 @@ dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:mixdrop-extractor'))
implementation(project(':lib:uqload-extractor'))
implementation(project(':lib:vidhide-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:mp4upload-extractor'))
}

View file

@ -4,20 +4,24 @@ import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.api.get
class AnimeOnlineNinja : DooPlay(
"es",
@ -91,7 +95,7 @@ class AnimeOnlineNinja : DooPlay(
} else if (path.startsWith("/letra") || path.startsWith("/tendencias")) {
val before = path.substringBeforeLast("/")
val after = path.substringAfterLast("/")
GET(baseUrl + before + "/page/$page/" + after)
GET("$baseUrl$before/page/$page/$after")
} else {
GET("$baseUrl$path/page/$page")
}
@ -100,11 +104,29 @@ class AnimeOnlineNinja : DooPlay(
// ============================== Episodes ==============================
override val episodeMovieText = "Película"
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealAnimeDoc(response.asJsoup())
val seasonList = doc.select(seasonListSelector)
return if (seasonList.isEmpty()) {
listOf(
SEpisode.create().apply {
setUrlWithoutDomain(doc.location())
episode_number = 1F
name = episodeMovieText
},
)
} else {
seasonList.reversed().flatMap { seasonElement ->
getSeasonEpisodes(seasonElement).reversed()
}
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
return players.flatMap { player ->
return players.parallelCatchingFlatMapBlocking { player ->
val name = player.selectFirst("span.title")!!.text()
val url = getPlayerUrl(player)
extractVideos(url, name)
@ -116,37 +138,41 @@ class AnimeOnlineNinja : DooPlay(
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private fun extractVideos(url: String, lang: String): List<Video> {
return when {
"saidochesto.top" in url || "MULTISERVER" in lang.uppercase() ->
extractFromMulti(url)
"filemoon" in url ->
filemoonExtractor.videosFromUrl(url, "$lang Filemoon - ", headers)
"dood" in url ->
doodExtractor.videoFromUrl(url, "$lang DoodStream", false)
?.let(::listOf)
"streamtape" in url ->
streamTapeExtractor.videoFromUrl(url, "$lang StreamTape")
?.let(::listOf)
"mixdrop" in url ->
mixDropExtractor.videoFromUrl(url, lang)
"uqload" in url ->
uqloadExtractor.videosFromUrl(url)
"wolfstream" in url -> {
client.newCall(GET(url, headers)).execute()
.asJsoup()
.selectFirst("script:containsData(sources)")
?.data()
?.let { jsData ->
val videoUrl = jsData.substringAfter("{file:\"").substringBefore("\"")
listOf(Video(videoUrl, "$lang WolfStream", videoUrl, headers = headers))
}
}
else -> null
} ?: emptyList<Video>()
try {
return when {
arrayOf("saidochesto.top").any(url) || "MULTISERVER" in lang.uppercase() -> extractFromMulti(url)
arrayOf("filemoon", "filemooon", "moon").any(url) -> filemoonExtractor.videosFromUrl(url, "$lang Filemoon:", headers)
arrayOf("doodstream", "dood.", "ds2play", "doods.").any(url) -> doodExtractor.videosFromUrl(url, "$lang DoodStream", false)
arrayOf("streamtape", "stp", "stape").any(url) -> streamTapeExtractor.videosFromUrl(url, "$lang StreamTape")
arrayOf("mixdrop").any(url) -> mixDropExtractor.videoFromUrl(url, prefix = "$lang ")
arrayOf("uqload").any(url) -> uqloadExtractor.videosFromUrl(url, prefix = lang)
"wolfstream" in url -> {
client.newCall(GET(url, headers)).execute()
.asJsoup()
.selectFirst("script:containsData(sources)")
?.data()
?.let { jsData ->
val videoUrl = jsData.substringAfter("{file:\"").substringBefore("\"")
listOf(Video(videoUrl, "$lang WolfStream", videoUrl, headers = headers))
}
}
arrayOf("mp4upload").any(url) -> mp4uploadExtractor.videosFromUrl(url, headers, prefix = "$lang ")
arrayOf("vidhide", "filelions.top", "vid.").any(url) -> vidHideExtractor.videosFromUrl(url) { "$lang VidHide:$it" }
arrayOf("wishembed", "streamwish", "strwish", "wish").any(url) -> streamWishExtractor.videosFromUrl(url, videoNameGen = { "$lang StreamWish:$it" })
else -> null
} ?: emptyList()
} catch (e: Exception) {
return emptyList()
}
}
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
private fun extractFromMulti(url: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val prefLang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
@ -267,6 +293,7 @@ class AnimeOnlineNinja : DooPlay(
override val prefQualityValues = arrayOf("480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
override val episodeNumberRegex by lazy { Regex("""(\d+(?:\.\d+)?)$""") }
companion object {
private const val PREF_LANG_KEY = "preferred_lang"
@ -276,7 +303,7 @@ class AnimeOnlineNinja : DooPlay(
private const val PREF_SERVER_DEFAULT = "Uqload"
private val PREF_LANG_ENTRIES = arrayOf("SUB", "All", "ES", "LAT")
private val PREF_LANG_VALUES = arrayOf("SUB", "", "ES", "LAT")
private val SERVER_LIST = arrayOf("Filemoon", "DoodStream", "StreamTape", "MixDrop", "Uqload", "WolfStream", "saidochesto.top")
private val SERVER_LIST = arrayOf("Filemoon", "DoodStream", "StreamTape", "MixDrop", "Uqload", "WolfStream", "saidochesto.top", "VidHide", "StreamWish", "Mp4Upload")
private const val PREF_VRF_INTERCEPT_KEY = "vrf_intercept"
private const val PREF_VRF_INTERCEPT_TITLE = "Intercept VRF links (Requiere Reiniciar)"

View file

@ -1,7 +1,7 @@
ext {
extName = 'CineCalidad'
extClass = '.CineCalidad'
extVersionCode = 2
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Cuevana'
extClass = '.CuevanaFactory'
extVersionCode = 33
extVersionCode = 34
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Doramasflix'
extClass = '.Doramasflix'
extVersionCode = 22
extVersionCode = 23
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Doramasyt'
extClass = '.Doramasyt'
extVersionCode = 14
extVersionCode = 15
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'HentaiLA'
extClass = '.Hentaila'
extVersionCode = 25
extVersionCode = 26
isNsfw = true
}

View file

@ -38,7 +38,7 @@ class Hentaila : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Hentaila"
override val baseUrl = "https://www4.hentaila.com"
override val baseUrl = "https://www5.hentaila.com"
override val lang = "es"

View file

@ -1,11 +1,18 @@
ext {
extName = 'HomeCine'
extClass = '.HomeCine'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:burstcloud-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:yourupload-extractor'))
implementation(project(':lib:fastream-extractor'))
implementation(project(':lib:upstream-extractor'))
implementation(project(':lib:filemoon-extractor'))
}

View file

@ -5,19 +5,27 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -25,7 +33,7 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "HomeCine"
override val baseUrl = "https://www3.homecine.tv"
override val baseUrl = "https://homecine.cc"
override val lang = "es"
@ -45,116 +53,175 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Fastream"
private val SERVER_LIST = arrayOf("Fastream")
private const val PREF_SERVER_DEFAULT = "YourUpload"
private val SERVER_LIST = arrayOf(
"YourUpload",
"BurstCloud",
"Voe",
"StreamWish",
"Mp4Upload",
"Fastream",
"Upstream",
"Filemoon",
)
}
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val animeDetails = SAnime.create().apply {
title = document.selectFirst(".mvic-desc h1")?.text()?.trim() ?: ""
status = if (document.location().contains("/series/")) SAnime.UNKNOWN else SAnime.COMPLETED
description = document.selectFirst(".mvic-desc .f-desc")?.ownText()
genre = document.select(".mvic-info [rel='category tag']").joinToString { it.text() }
thumbnail_url = document.selectFirst("[itemprop=image]")?.attr("abs:src")?.replace("/w185/", "/w500/")
document.select(".mvici-left p").map { it.text() }.map { textContent ->
when {
"Director" in textContent -> author = textContent.substringAfter("Director:").trim().split(", ").firstOrNull()
"Actors" in textContent -> artist = textContent.substringAfter("Actors:").trim().split(", ").firstOrNull()
}
}
}
return animeDetails
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/peliculas/page/$page", headers)
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/cartelera-series/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select("[data-movie-id] > a")
val nextPage = document.select(".pagination li a:contains(Last)").any()
val elements = document.select(".post")
val nextPage = document.select(".nav-links .current ~ a").any()
val animeList = elements.map { element ->
SAnime.create().apply {
title = element.selectFirst(".mli-info")?.text()?.trim() ?: ""
thumbnail_url = element.selectFirst("img")!!.attr("abs:data-original")
setUrlWithoutDomain(element.attr("abs:href"))
setUrlWithoutDomain(element.selectFirst(".lnk-blk")?.attr("abs:href") ?: "")
title = element.selectFirst(".entry-header .entry-title")?.text() ?: ""
description = element.select(".entry-content p").text() ?: ""
thumbnail_url = element.selectFirst(".post-thumbnail figure img")?.let { getImageUrl(it) }
}
}
return AnimesPage(animeList, nextPage)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = popularAnimeRequest(page)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$query", headers)
genreFilter.state != 0 -> GET("$baseUrl/${genreFilter.toUriPart()}/page/$page", headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/?s=$query", headers)
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
return SAnime.create().apply {
title = document.selectFirst("aside .entry-header .entry-title")?.text() ?: ""
description = document.select("aside .description p:not([class])").joinToString { it.text() }
thumbnail_url = document.selectFirst(".post-thumbnail img")?.let { getImageUrl(it)?.replace("/w185/", "/w500/") }
genre = document.select(".genres a").joinToString { it.text() }
status = if (document.location().contains("pelicula")) SAnime.COMPLETED else SAnime.UNKNOWN
}
}
private fun getImageUrl(element: Element): String? {
return when {
element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("src") -> element.attr("abs:src")
else -> null
}
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return if (document.location().contains("/series/")) {
var episodeCounter = 1F
document.select(".tvseason").flatMap { season ->
val noSeason = season.select(".les-title strong").text().substringAfter("Temporada").trim()
season.select(".les-content a").map { ep ->
SEpisode.create().apply {
episode_number = episodeCounter++
name = "T$noSeason - ${ep.text().trim()}"
setUrlWithoutDomain(ep.attr("abs:href"))
}
}
}.reversed()
} else {
val referer = response.request.url.toString()
return if (referer.contains("pelicula")) {
listOf(
SEpisode.create().apply {
episode_number = 1f
name = "PELÍCULA"
setUrlWithoutDomain(document.location())
name = "Película"
setUrlWithoutDomain(referer)
},
)
} else {
val chunkSize = Runtime.getRuntime().availableProcessors()
document.select(".sel-temp a")
.sortedByDescending { it.attr("data-season") }
.chunked(chunkSize).flatMap { chunk ->
chunk.parallelCatchingFlatMapBlocking { season ->
getDetailSeason(season, referer)
}
}.sortedByDescending {
it.name.substringBeforeLast("-")
}
}
}
private fun getDetailSeason(element: Element, referer: String): List<SEpisode> {
return try {
val post = element.attr("data-post")
val season = element.attr("data-season")
val formBody = FormBody.Builder()
.add("action", "action_select_season")
.add("season", season)
.add("post", post)
.build()
val request = Request.Builder()
.url("$baseUrl/wp-admin/admin-ajax.php")
.post(formBody)
.header("Origin", baseUrl)
.header("Referer", referer)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val detail = client.newCall(request).execute().asJsoup()
detail.select(".post").reversed().mapIndexed { idx, it ->
val epNumber = try {
it.select(".entry-header .num-epi").text().substringAfter("x").substringBefore("").trim()
} catch (_: Exception) { "${idx + 1}" }
SEpisode.create().apply {
setUrlWithoutDomain(it.select("a").attr("abs:href"))
name = "T$season - Episodio $epNumber"
episode_number = epNumber.toFloat()
}
}
} catch (_: Exception) {
emptyList()
}
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return document.select(".movieplay iframe").parallelCatchingFlatMapBlocking {
val link = it.attr("abs:src")
val videoList = mutableListOf<Video>()
document.select(".aa-tbs-video a").forEach {
val prefix = runCatching {
val tabElement = it.closest("[id*=tab]") ?: return@runCatching ""
val tabName = tabElement.attr("id")
val lang = document.selectFirst("[href=\"#$tabName\"]")?.ownText()?.trim() ?: ""
val lang = it.select(".server").text().lowercase()
when {
lang.lowercase().contains("latino") -> "[LAT]"
lang.lowercase().contains("castellano") -> "[CAST]"
lang.lowercase().contains("subtitulado") -> "[SUB]"
lang.contains("latino") -> "[LAT]"
lang.contains("castellano") -> "[CAST]"
lang.contains("sub") || lang.contains("vose") -> "[SUB]"
else -> ""
}
}.getOrDefault("")
serverVideoResolver(link, prefix)
}
}
val ide = it.attr("href")
var src = document.select("$ide iframe").attr("data-src").replace("#038;", "&").replace("&amp;", "")
try {
if (src.contains("home")) {
src = client.newCall(GET(src)).execute().asJsoup().selectFirst("iframe")?.attr("src") ?: ""
}
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
val embedUrl = url.lowercase()
return when {
embedUrl.contains("fastream") -> {
val link = if (url.contains("emb.html")) "https://fastream.to/embed-${url.split("/").last()}.html" else url
FastreamExtractor(client, headers).videosFromUrl(link, prefix = "$prefix Fastream:")
}
else -> emptyList()
if (src.contains("fastream")) {
if (src.contains("emb.html")) {
val key = src.split("/").last()
src = "https://fastream.to/embed-$key.html"
}
FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false, prefix = "$prefix Fastream:").also(videoList::addAll)
}
if (src.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(src, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("yourupload")) {
YourUploadExtractor(client).videoFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("voe")) {
VoeExtractor(client).videosFromUrl(src, prefix = "$prefix ").also(videoList::addAll)
}
if (src.contains("wishembed") || src.contains("streamwish") || src.contains("wish")) {
StreamWishExtractor(client, headers).videosFromUrl(src) { "$prefix StreamWish:$it" }.also(videoList::addAll)
}
if (src.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(src, headers = headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("filemoon") || src.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(src, headers = headers, prefix = "$prefix Filemoon:").let { videoList.addAll(it) }
}
} catch (_: Exception) {}
}
return videoList
}
override fun List<Video>.sort(): List<Video> {
@ -171,25 +238,6 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
).reversed()
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Género",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("Películas", "peliculas"),
Pair("Series", "series"),
),
)
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
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY
@ -207,22 +255,6 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
@ -238,5 +270,21 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Jkanime'
extClass = '.Jkanime'
extVersionCode = 24
extVersionCode = 25
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'MetroSeries'
extClass = '.MetroSeries'
extVersionCode = 11
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View file

@ -20,25 +20,20 @@ import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "MetroSeries"
override val baseUrl = "https://metroseries.net"
override val baseUrl = "https://www3.seriesmetro.net"
override val lang = "es"
@ -71,11 +66,11 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
)
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/series/page/$page", headers)
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/cartelera-series/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(".post-list, .results-post > .post")
val elements = document.select(".post")
val nextPage = document.select(".nav-links .current ~ a").any()
val animeList = elements.map { element ->
SAnime.create().apply {
@ -99,11 +94,11 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
return SAnime.create().apply {
title = document.selectFirst("main .entry-header .entry-title")?.text() ?: ""
description = document.select("main .entry-content p").joinToString { it.text() }
thumbnail_url = document.selectFirst("main .post-thumbnail img")?.let { getImageUrl(it) }
genre = document.select("main .entry-content .tagcloud a").joinToString { it.text() }
status = SAnime.UNKNOWN
title = document.selectFirst("aside .entry-header .entry-title")?.text() ?: ""
description = document.select("aside .description p:not([class])").joinToString { it.text() }
thumbnail_url = document.selectFirst(".post-thumbnail img")?.let { getImageUrl(it)?.replace("/w185/", "/w500/") }
genre = document.select(".genres a").joinToString { it.text() }
status = if (document.location().contains("pelicula")) SAnime.COMPLETED else SAnime.UNKNOWN
}
}
@ -118,34 +113,40 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val referer = response.request.url.toString()
val chunkSize = Runtime.getRuntime().availableProcessors()
val objectNumber = document.select("#aa-season").attr("data-object")
val episodes = document.select(".season-list li a")
.sortedByDescending { it.attr("data-season") }
.chunked(chunkSize).flatMap { chunk ->
chunk.parallelCatchingFlatMapBlocking { season ->
val pages = getDetailSeason(season, objectNumber, referer)
getPageEpisodeList(pages, referer, objectNumber, season.attr("data-season"))
return if (referer.contains("pelicula")) {
listOf(
SEpisode.create().apply {
episode_number = 1f
name = "Película"
setUrlWithoutDomain(referer)
},
)
} else {
val chunkSize = Runtime.getRuntime().availableProcessors()
document.select(".sel-temp a")
.sortedByDescending { it.attr("data-season") }
.chunked(chunkSize).flatMap { chunk ->
chunk.parallelCatchingFlatMapBlocking { season ->
getDetailSeason(season, referer)
}
}.sortedByDescending {
it.name.substringBeforeLast("-")
}
}.sortedByDescending {
it.name.substringBeforeLast("-")
}
return episodes
}
}
private fun getDetailSeason(element: Element, objectNumber: String, referer: String): IntRange {
try {
private fun getDetailSeason(element: Element, referer: String): List<SEpisode> {
return try {
val post = element.attr("data-post")
val season = element.attr("data-season")
val formBody = FormBody.Builder()
.add("action", "action_select_season")
.add("season", season)
.add("post", post)
.add("object", objectNumber)
.build()
val request = Request.Builder()
.url("https://metroseries.net/wp-admin/admin-ajax.php")
.url("$baseUrl/wp-admin/admin-ajax.php")
.post(formBody)
.header("Origin", baseUrl)
.header("Referer", referer)
@ -153,110 +154,72 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
.build()
val detail = client.newCall(request).execute().asJsoup()
val firstPage = try { detail.selectFirst("#aa-season > nav > span.page-numbers")?.text()?.toInt() ?: 1 } catch (_: Exception) { 1 }
val lastPage = try { detail.select(".pagination a.page-numbers:not(.next)").last()?.text()?.toInt() ?: firstPage } catch (_: Exception) { firstPage }
detail.select(".post").reversed().mapIndexed { idx, it ->
val epNumber = try {
it.select(".entry-header .num-epi").text().substringAfter("x").substringBefore("").trim()
} catch (_: Exception) { "${idx + 1}" }
return firstPage.rangeTo(lastPage)
} catch (_: Exception) {
return 1..1
}
}
private fun getPageEpisodeList(pages: IntRange, referer: String, objectNumber: String, season: String): List<SEpisode> {
val episodes = mutableListOf<SEpisode>()
try {
pages.parallelMapBlocking {
val formBody = FormBody.Builder()
.add("action", "action_pagination_ep")
.add("page", "$it")
.add("object", objectNumber)
.add("season", season)
.build()
val requestPage = Request.Builder()
.url("https://metroseries.net/wp-admin/admin-ajax.php")
.post(formBody)
.header("authority", baseUrl.toHttpUrl().host)
.header("Origin", "https://${baseUrl.toHttpUrl().host}")
.header("Referer", referer)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
client.newCall(requestPage).await().parseAsEpisodeList().also(episodes::addAll)
SEpisode.create().apply {
setUrlWithoutDomain(it.select("a").attr("abs:href"))
name = "T$season - Episodio $epNumber"
episode_number = epNumber.toFloat()
}
}
} catch (_: Exception) { }
return episodes
} catch (_: Exception) {
emptyList()
}
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
val termId = document.select("#option-players").attr("data-term")
document.select(".player-options-list li a").forEach {
document.select(".aa-tbs-video a").forEach {
val prefix = runCatching {
val lang = it.select(".option").text().lowercase()
val lang = it.select(".server").text().lowercase()
when {
lang.contains("latino") -> "[LAT]"
lang.contains("castellano") -> "[CAST]"
lang.contains("sub") -> "[SUB]"
lang.contains("sub") || lang.contains("vose") -> "[SUB]"
else -> ""
}
}.getOrDefault("")
val ide = it.attr("data-opt")
val formBody = FormBody.Builder()
.add("action", "action_player_series")
.add("ide", ide)
.add("term_id", termId)
.build()
val ide = it.attr("href")
var src = document.select("$ide iframe").attr("data-src").replace("#038;", "&").replace("&amp;", "")
try {
if (src.contains("metro")) {
src = client.newCall(GET(src)).execute().asJsoup().selectFirst("iframe")?.attr("src") ?: ""
}
val postRequest = Request.Builder()
.url("https://metroseries.net/wp-admin/admin-ajax.php")
.post(formBody)
.header("Origin", baseUrl)
.header("Referer", response.request.url.toString())
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val playerDocument = client.newCall(postRequest).execute().asJsoup()
playerDocument.select("iframe").forEach {
var src = it.attr("src").replace("#038;", "&").replace("&amp;", "")
try {
if (src.contains("metroseries")) {
src = client.newCall(GET(src)).execute().asJsoup().selectFirst("iframe")?.attr("src") ?: ""
if (src.contains("fastream")) {
if (src.contains("emb.html")) {
val key = src.split("/").last()
src = "https://fastream.to/embed-$key.html"
}
if (src.contains("fastream")) {
if (src.contains("emb.html")) {
val key = src.split("/").last()
src = "https://fastream.to/embed-$key.html"
}
FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false, prefix = "$prefix Fastream:").also(videoList::addAll)
}
if (src.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(src, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("yourupload")) {
YourUploadExtractor(client).videoFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("voe")) {
VoeExtractor(client).videosFromUrl(src, prefix = "$prefix ").also(videoList::addAll)
}
if (src.contains("wishembed") || src.contains("streamwish") || src.contains("wish")) {
StreamWishExtractor(client, headers).videosFromUrl(src) { "$prefix StreamWish:$it" }.also(videoList::addAll)
}
if (src.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(src, headers = headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("filemoon") || src.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(src, headers = headers, prefix = "$prefix Filemoon:").let { videoList.addAll(it) }
}
} catch (_: Exception) {}
}
FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false, prefix = "$prefix Fastream:").also(videoList::addAll)
}
if (src.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(src, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("yourupload")) {
YourUploadExtractor(client).videoFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("voe")) {
VoeExtractor(client).videosFromUrl(src, prefix = "$prefix ").also(videoList::addAll)
}
if (src.contains("wishembed") || src.contains("streamwish") || src.contains("wish")) {
StreamWishExtractor(client, headers).videosFromUrl(src) { "$prefix StreamWish:$it" }.also(videoList::addAll)
}
if (src.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(src, headers = headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("filemoon") || src.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(src, headers = headers, prefix = "$prefix Filemoon:").let { videoList.addAll(it) }
}
} catch (_: Exception) {}
}
return videoList
}
@ -324,21 +287,4 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
}
}.also(screen::addPreference)
}
private fun Response.parseAsEpisodeList(): List<SEpisode> {
return asJsoup().select(".episodes-list li a").reversed().mapIndexed { idx, it ->
val epNumber = try { it.ownText().substringAfter("x").substringBefore("").trim() } catch (_: Exception) { "${idx + 1}" }
val season = it.ownText().substringBefore("x").trim()
SEpisode.create().apply {
setUrlWithoutDomain(it.attr("abs:href"))
name = "T$season - E$epNumber - ${it.ownText().substringAfter("").trim()}"
episode_number = epNumber.toFloat()
date_upload = try {
SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH).parse(it.select("span").text()).time
} catch (_: Exception) {
System.currentTimeMillis()
}
}
}
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'MonosChinos'
extClass = '.MonosChinos'
extVersionCode = 28
extVersionCode = 29
}
apply from: "$rootDir/common.gradle"

View file

@ -0,0 +1,12 @@
ext {
extName = 'Pandrama'
extClass = '.Pandrama'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:vk-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,299 @@
package eu.kanade.tachiyomi.animeextension.es.pandrama
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
class Pandrama : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Pandrama"
override val baseUrl = "https://pandra.ma"
override val lang = "es"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Vk"
private val SERVER_LIST = arrayOf("Vk", "Okru")
}
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val details = SAnime.create()
for (element in document.select(".hl-full-box ul li")) {
val title = element.select("em").text()
val content = element.select("span")
when {
title.contains("Director:") -> details.author = element.ownText().trim()
title.contains("Estado:") -> details.status = parseStatus(content.text())
title.contains("Protagonistas:") -> details.artist = element.selectFirst("a")?.text()
title.contains("Género:") -> details.genre = element.select("a").joinToString { it.text() }
title.contains("Sinopsis:") -> details.description = element.ownText().trim()
}
}
return details
}
private fun parseStatus(status: String?) = when (status.orEmpty()) {
"En Emisión" -> SAnime.ONGOING
"Finalizado" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/vodshow/Dramas--------$page---/", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(".hl-vod-list li > a")
val nextPage = document.select(".hl-page-wrap a:not(.hl-disad) span:contains(siguiente)").any()
val animeList = elements.map { element ->
val langTag = element.select(".hl-pic-tag").text().trim()
val prefix = when {
langTag.contains("Español LAT") -> "\uD83C\uDDF2\uD83C\uDDFD "
langTag.contains("Español ES") -> "\uD83C\uDDEA\uD83C\uDDF8 "
else -> ""
}
SAnime.create().apply {
title = """$prefix ${element.attr("title")}""".trim()
thumbnail_url = element.attr("abs:data-original")
setUrlWithoutDomain(element.attr("abs:href"))
}
}
return AnimesPage(animeList, nextPage)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/vodshow/Dramas--hits------$page---/", headers)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = when {
query.isNotBlank() -> "$baseUrl/vodsearch/$query----------$page---/"
else -> "$baseUrl/vodshow/Dramas--------$page---/"
}
return GET(url, headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
if (!document.location().contains("vodsearch")) return popularAnimeParse(response)
val elements = document.select(".hl-one-list .hl-item-thumb")
val nextPage = document.select(".hl-page-wrap a:not(.hl-disad) span:contains(siguiente)").any()
val animeList = elements.map { element ->
SAnime.create().apply {
title = element.attr("title")
thumbnail_url = element.attr("abs:data-original")
setUrlWithoutDomain(element.attr("abs:href"))
}
}
return AnimesPage(animeList, nextPage)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select(".hl-plays-list li > a").groupBy { it.text().trim() }.map {
val urlList = json.encodeToString(it.value.map { it.attr("abs:href") })
SEpisode.create().apply {
name = it.key
episode_number = it.key.substringAfter("Ep.").trim().toFloatOrNull() ?: 0F
url = urlList
}
}.reversed()
}
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val serverData = json.decodeFromString<List<String>>(episode.url)
return serverData.parallelCatchingFlatMapBlocking {
val page = client.newCall(GET(it)).execute().asJsoup()
val jsonData = page.selectFirst("script:containsData(var player_aaaa)")
?.data()?.substringAfter("var player_aaaa=")?.trim()
?: return@parallelCatchingFlatMapBlocking emptyList()
val player = json.decodeFromString<PlayerDto>(jsonData)
val url = if (player.encrypt == 2) {
URLDecoder.decode(base64decode(player.url ?: ""), "UTF-8")
} else {
URLDecoder.decode(player.url ?: "", "UTF-8")
}
serverVideoResolver(url)
}
}
private val okruExtractor by lazy { OkruExtractor(client) }
private val vkExtractor by lazy { VkExtractor(client, headers) }
private fun serverVideoResolver(url: String): List<Video> {
val embedUrl = url.lowercase()
return when {
embedUrl.contains("ok.ru") || embedUrl.contains("okru") -> okruExtractor.videosFromUrl(url)
embedUrl.contains("vk.") -> vkExtractor.videosFromUrl(url)
else -> emptyList()
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(server, true) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%d"
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 = "Preferred quality"
entries = QUALITY_LIST
entryValues = QUALITY_LIST
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%d"
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)
}
private val base64DecodeChars = intArrayOf(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
)
private fun base64decode(str: String): String {
var c1: Int
var c2: Int
var c3: Int
var c4: Int
var i = 0
val len = str.length
val out = StringBuilder()
while (i < len) {
do {
c1 = base64DecodeChars[str[i].toInt() and 255]
i++
} while (i < len && c1 == -1)
if (c1 == -1) break
do {
c2 = base64DecodeChars[str[i].toInt() and 255]
i++
} while (i < len && c2 == -1)
if (c2 == -1) break
out.append(((c1 shl 2) or ((c2 and 48) shr 4)).toChar())
do {
c3 = str[i].toInt() and 255
if (c3 == 61) return out.toString()
c3 = base64DecodeChars[c3]
i++
} while (i < len && c3 == -1)
if (c3 == -1) break
out.append((((c2 and 15) shl 4) or ((c3 and 60) shr 2)).toChar())
do {
c4 = str[i].toInt() and 255
if (c4 == 61) return out.toString()
c4 = base64DecodeChars[c4]
i++
} while (i < len && c4 == -1)
if (c4 == -1) break
out.append((((c3 and 3) shl 6) or c4).toChar())
}
return out.toString()
}
@Serializable
data class PlayerDto(
@SerialName("flag") var flag: String? = null,
@SerialName("encrypt") var encrypt: Int? = null,
@SerialName("trysee") var trysee: Int? = null,
@SerialName("points") var points: Int? = null,
@SerialName("link") var link: String? = null,
@SerialName("poster") var poster: String? = null,
@SerialName("doblado") var doblado: String? = null,
@SerialName("vod_en_py") var vodEnPy: String? = null,
@SerialName("link_next") var linkNext: String? = null,
@SerialName("link_pre") var linkPre: String? = null,
@SerialName("vod_data") var vodData: VodData? = VodData(),
@SerialName("url") var url: String? = null,
@SerialName("url_next") var urlNext: String? = null,
@SerialName("from") var from: String? = null,
@SerialName("server") var server: String? = null,
@SerialName("note") var note: String? = null,
@SerialName("id") var id: String? = null,
@SerialName("sid") var sid: Int? = null,
@SerialName("nid") var nid: Int? = null,
)
@Serializable
data class VodData(
@SerialName("vod_name") var vodName: String? = null,
@SerialName("vod_actor") var vodActor: String? = null,
@SerialName("vod_director") var vodDirector: String? = null,
@SerialName("vod_class") var vodClass: String? = null,
)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'PelisForte'
extClass = '.PelisForte'
extVersionCode = 16
extVersionCode = 17
}
apply from: "$rootDir/common.gradle"
@ -21,4 +21,5 @@ dependencies {
implementation(project(':lib:playlist-utils'))
implementation(project(':lib:streamlare-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:vidguard-extractor'))
}

View file

@ -23,10 +23,12 @@ import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
@ -62,6 +64,7 @@ open class PelisForte : ConfigurableAnimeSource, AnimeHttpSource() {
"YourUpload", "Voe", "Mp4Upload", "Doodstream",
"Upload", "BurstCloud", "Upstream", "StreamTape",
"Fastream", "Filemoon", "StreamWish", "Okru",
"VidGuard",
)
}
@ -149,83 +152,70 @@ open class PelisForte : ConfigurableAnimeSource, AnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
document.select(".video-player iframe").forEach {
try {
val id = it.parent()?.attr("id")
val idTab = document.selectFirst("[href=\"#$id\"]")?.closest(".lrt")?.attr("id")
val lang = document.select("[tab=$idTab]").text()
val src = it.attr("src").ifEmpty { it.attr("data-src") }
val key = src.substringAfter("/?h=")
val player = "https://${src.toHttpUrl().host}/r.php?h=$key"
val prefix = when {
lang.contains("Latino", true) -> "[LAT]"
lang.contains("Subtitulado", true) -> "[SUB]"
lang.contains("Castellano", true) -> "[CAST]"
else -> ""
}
val locationsDdh = client.newCall(GET(player, headers = headers.newBuilder().add("referer", src).build()))
.execute().networkResponse.toString()
return document.select(".video-player iframe").parallelCatchingFlatMapBlocking {
val id = it.parent()?.attr("id")
val idTab = document.selectFirst("[href=\"#$id\"]")?.closest(".lrt")?.attr("id")
val lang = document.select("[tab=$idTab]").text()
val src = it.attr("src").ifEmpty { it.attr("data-src") }
val key = src.substringAfter("/?h=")
val player = "https://${src.toHttpUrl().host}/r.php?h=$key"
fetchUrls(locationsDdh).forEach {
serverVideoResolver(it, prefix, src).also(videoList::addAll)
}
} catch (_: Exception) {}
val prefix = when {
lang.contains("Latino", true) -> "[LAT]"
lang.contains("Subtitulado", true) -> "[SUB]"
lang.contains("Castellano", true) -> "[CAST]"
else -> ""
}
val locationsDdh = client.newCall(
GET(player, headers = headers.newBuilder().add("referer", src).build()),
).execute().networkResponse.toString()
fetchUrls(locationsDdh).flatMap { serverVideoResolver(it, prefix) }
}
return videoList
}
private fun serverVideoResolver(url: String, prefix: String = "", referer: String = ""): List<Video> {
val videoList = mutableListOf<Video>()
val embedUrl = url.lowercase()
try {
if (embedUrl.contains("voe")) {
VoeExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll)
/*--------------------------------Video extractors------------------------------------*/
private val voeExtractor by lazy { VoeExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val burstCloudExtractor by lazy { BurstCloudExtractor(client) }
private val fastreamExtractor by lazy { FastreamExtractor(client, headers) }
private val upstreamExtractor by lazy { UpstreamExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
return runCatching {
when {
arrayOf("voe").any(url) -> voeExtractor.videosFromUrl(url, "$prefix ")
arrayOf("ok.ru", "okru").any(url) -> okruExtractor.videosFromUrl(url, prefix)
arrayOf("filemoon", "moonplayer").any(url) -> filemoonExtractor.videosFromUrl(url, prefix = "$prefix Filemoon:")
arrayOf("uqload").any(url) -> uqloadExtractor.videosFromUrl(url, prefix)
arrayOf("mp4upload").any(url) -> mp4uploadExtractor.videosFromUrl(url, headers, prefix = "$prefix ")
arrayOf("wishembed", "streamwish", "strwish", "wish").any(url) -> {
streamWishExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })
}
arrayOf("doodstream", "dood.", "ds2play", "doods.").any(url) -> {
val url2 = url.replace("https://doodstream.com/e/", "https://d0000d.com/e/")
doodExtractor.videosFromUrl(url2, "$prefix DoodStream")
}
arrayOf("streamlare").any(url) -> streamlareExtractor.videosFromUrl(url, prefix)
arrayOf("yourupload", "upload").any(url) -> yourUploadExtractor.videoFromUrl(url, headers = headers, prefix = "$prefix ")
arrayOf("burstcloud", "burst").any(url) -> burstCloudExtractor.videoFromUrl(url, headers = headers, prefix = "$prefix ")
arrayOf("fastream").any(url) -> fastreamExtractor.videosFromUrl(url, prefix = "$prefix Fastream:")
arrayOf("upstream").any(url) -> upstreamExtractor.videosFromUrl(url, prefix = "$prefix ")
arrayOf("streamtape", "stp", "stape").any(url) -> streamTapeExtractor.videosFromUrl(url, quality = "$prefix StreamTape")
arrayOf("vembed", "guard", "listeamed", "bembed", "vgfplay").any(url) -> vidGuardExtractor.videosFromUrl(url, prefix = "$prefix ")
else -> emptyList()
}
if (embedUrl.contains("ok.ru") || embedUrl.contains("okru")) {
OkruExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll)
}
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(url, prefix = "${prefix}Filemoon").also(videoList::addAll)
}
if (embedUrl.contains("uqload")) {
UqloadExtractor(client).videosFromUrl(url, prefix = prefix).also(videoList::addAll)
}
if (embedUrl.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(url, headers, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("wishembed") || embedUrl.contains("streamwish") ||
embedUrl.contains("strwish") || embedUrl.contains("wish")
) {
val docHeaders = headers.newBuilder()
.add("Referer", referer)
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "${prefix}StreamWish:$it" }).also(videoList::addAll)
}
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
DoodExtractor(client).videoFromUrl(url, "${prefix}DoodStream", false)?.let { videoList.add(it) }
}
if (embedUrl.contains("streamlare")) {
StreamlareExtractor(client).videosFromUrl(url, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("yourupload")) {
YourUploadExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("fastream")) {
FastreamExtractor(client, headers).videosFromUrl(url, prefix = "${prefix}Fastream:").also(videoList::addAll)
}
if (embedUrl.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(url, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("streamtape")) {
StreamTapeExtractor(client).videoFromUrl(url, quality = "${prefix}StreamTape")?.let { videoList.add(it) }
}
} catch (_: Exception) {
}
return videoList
}.getOrNull() ?: emptyList()
}
override fun List<Video>.sort(): List<Video> {
@ -278,6 +268,8 @@ open class PelisForte : ConfigurableAnimeSource, AnimeHttpSource() {
fun toUriPart() = vals[state].second
}
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY

View file

@ -1,7 +1,7 @@
ext {
extName = 'Pelisplushd'
extClass = '.PelisplushdFactory'
extVersionCode = 58
extVersionCode = 59
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'TioanimeH'
extClass = '.TioanimeHFactory'
extVersionCode = 19
extVersionCode = 20
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.Tiodonghua'
themePkg = 'animestream'
baseUrl = 'https://anime.tiodonghua.com'
overrideVersionCode = 4
overrideVersionCode = 5
}
apply from: "$rootDir/common.gradle"

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeSAGA'
themePkg = 'dooplay'
baseUrl = 'https://www.animesaga.in'
overrideVersionCode = 11
overrideVersionCode = 14
}
apply from: "$rootDir/common.gradle"

Some files were not shown because too many files have changed in this diff Show more