New source: tr.turkanime
This commit is contained in:
parent
7804c2f258
commit
fe53f7a8c1
@ -20,7 +20,7 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
/**
|
/**
|
||||||
* Conforming with CryptoJS AES method
|
* Conforming with CryptoJS AES method
|
||||||
*/
|
*/
|
||||||
@Suppress("unused", "FunctionName")
|
@Suppress("unused")
|
||||||
object CryptoAES {
|
object CryptoAES {
|
||||||
|
|
||||||
private const val KEY_SIZE = 256
|
private const val KEY_SIZE = 256
|
||||||
@ -39,19 +39,41 @@ object CryptoAES {
|
|||||||
* @param password passphrase
|
* @param password passphrase
|
||||||
*/
|
*/
|
||||||
fun decrypt(cipherText: String, password: String): String {
|
fun decrypt(cipherText: String, password: String): String {
|
||||||
try {
|
return try {
|
||||||
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||||
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
||||||
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
||||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||||
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
||||||
return decryptAES(
|
decryptAES(
|
||||||
cipherTextBytes,
|
cipherTextBytes,
|
||||||
keyAndIV?.get(0) ?: ByteArray(32),
|
keyAndIV?.get(0) ?: ByteArray(32),
|
||||||
keyAndIV?.get(1) ?: ByteArray(16),
|
keyAndIV?.get(1) ?: ByteArray(16),
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return ""
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptWithSalt(cipherText: String, salt: String, password: String): String {
|
||||||
|
return try {
|
||||||
|
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||||
|
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
val keyAndIV = generateKeyAndIV(
|
||||||
|
32,
|
||||||
|
16,
|
||||||
|
1,
|
||||||
|
salt.decodeHex(),
|
||||||
|
password.toByteArray(Charsets.UTF_8),
|
||||||
|
md5
|
||||||
|
)
|
||||||
|
decryptAES(
|
||||||
|
ctBytes,
|
||||||
|
keyAndIV?.get(0) ?: ByteArray(32),
|
||||||
|
keyAndIV?.get(1) ?: ByteArray(16),
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
lib/synchrony/build.gradle.kts
Normal file
19
lib/synchrony/build.gradle.kts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
namespace = "eu.kanade.tachiyomi.lib.synchrony"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
targetSdk = AndroidConfig.targetSdk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.kotlin.stdlib)
|
||||||
|
compileOnly(libs.quickjs)
|
||||||
|
}
|
289
lib/synchrony/src/main/assets/synchrony-v2.4.2.1.js
Normal file
289
lib/synchrony/src/main/assets/synchrony-v2.4.2.1.js
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.lib.synchrony
|
||||||
|
|
||||||
|
import app.cash.quickjs.QuickJs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to deobfuscate JavaScript strings with synchrony.
|
||||||
|
*/
|
||||||
|
object Deobfuscator {
|
||||||
|
fun deobfuscateScript(source: String): String? {
|
||||||
|
val engine = QuickJs.create()
|
||||||
|
val originalScript = javaClass.getResource("/assets/$SCRIPT_NAME")
|
||||||
|
?.readText() ?: return null
|
||||||
|
|
||||||
|
// Sadly needed until QuickJS properly supports module imports:
|
||||||
|
// Regex for finding one and two in "export{one as Deobfuscator,two as Transformer};"
|
||||||
|
val regex = """export\{(.*) as Deobfuscator,(.*) as Transformer\};""".toRegex()
|
||||||
|
val synchronyScript = regex.find(originalScript)!!.let { match ->
|
||||||
|
val (deob, trans) = match.destructured
|
||||||
|
val replacement = "const Deobfuscator = $deob, Transformer = $trans;"
|
||||||
|
originalScript.replace(match.value, replacement)
|
||||||
|
}
|
||||||
|
engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
|
||||||
|
engine.evaluate(synchronyScript)
|
||||||
|
|
||||||
|
engine.set("source", TestInterface::class.java, object: TestInterface { override fun getValue() = source })
|
||||||
|
val result = engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
|
||||||
|
engine.close()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
private interface TestInterface {
|
||||||
|
fun getValue(): String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update this when the script is updated!
|
||||||
|
private const val SCRIPT_NAME = "synchrony-v2.4.2.1.js"
|
4
src/tr/turkanime/AndroidManifest.xml
Normal file
4
src/tr/turkanime/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.kanade.tachiyomi.animeextension">
|
||||||
|
</manifest>
|
22
src/tr/turkanime/build.gradle
Normal file
22
src/tr/turkanime/build.gradle
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib-cryptoaes"))
|
||||||
|
implementation(project(":lib-synchrony"))
|
||||||
|
implementation(project(":lib-voe-extractor"))
|
||||||
|
implementation(project(":lib-streamsb-extractor"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Türk Anime TV'
|
||||||
|
pkgNameSuffix = 'tr.turkanime'
|
||||||
|
extClass = '.TurkAnime'
|
||||||
|
extVersionCode = 1
|
||||||
|
libVersion = '13'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/tr/turkanime/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
BIN
src/tr/turkanime/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
src/tr/turkanime/res/web_hi_res_512.png
Normal file
BIN
src/tr/turkanime/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
@ -0,0 +1,65 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.tr.turkanime
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class AlucardExtractor(private val client: OkHttpClient, private val json: Json, private val baseUrl: String) {
|
||||||
|
private val refererHeader = Headers.headersOf("referer", baseUrl)
|
||||||
|
|
||||||
|
fun extractVideos(hosterLink: String, subber: String): List<Video> {
|
||||||
|
return try {
|
||||||
|
val sourcesId = hosterLink.substringBeforeLast("/true").substringAfterLast("/")
|
||||||
|
val playerJs = client.newCall(GET("$baseUrl/js/player.js"))
|
||||||
|
.execute().body.string()
|
||||||
|
val csrf = "(?<=')[a-zA-Z]{64}(?=')".toRegex().find(playerJs)!!.value
|
||||||
|
val sourcesResponse = client.newCall(
|
||||||
|
GET(
|
||||||
|
"$baseUrl/sources/$sourcesId/true",
|
||||||
|
Headers.headersOf(
|
||||||
|
"Referer",
|
||||||
|
hosterLink,
|
||||||
|
"X-Requested-With",
|
||||||
|
"XMLHttpRequest",
|
||||||
|
"Cookie",
|
||||||
|
"__",
|
||||||
|
"csrf-token",
|
||||||
|
csrf,
|
||||||
|
"User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||||
|
"(KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute().body.string()
|
||||||
|
|
||||||
|
val sources = json.decodeFromString<JsonObject>(sourcesResponse)["response"]!!
|
||||||
|
.jsonObject["sources"]!!
|
||||||
|
.jsonArray.first()
|
||||||
|
.jsonObject["file"]!!
|
||||||
|
.jsonPrimitive.content
|
||||||
|
|
||||||
|
val masterPlaylist = client.newCall(GET(sources, refererHeader))
|
||||||
|
.execute().body.string()
|
||||||
|
val separator = "#EXT-X-STREAM-INF"
|
||||||
|
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||||
|
val quality = it.substringAfter("RESOLUTION=")
|
||||||
|
.substringAfter("x")
|
||||||
|
.substringBefore("\n") + "p"
|
||||||
|
val videoUrl = it.substringAfter("\n")
|
||||||
|
.substringBefore("\n")
|
||||||
|
// TODO: This gives 403 in MPV
|
||||||
|
Video(videoUrl, "$subber: Alucard: $quality", videoUrl, refererHeader)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,321 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.tr.turkanime
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Base64
|
||||||
|
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.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
|
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||||
|
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class TurkAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "Türk Anime TV"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.turkanime.co"
|
||||||
|
|
||||||
|
override val lang = "tr"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val key: String
|
||||||
|
get() = preferences.getString(PREF_KEY_KEY, DEFAULT_KEY)!!
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/ajax/rankagore?sayfa=$page", xmlHeader)
|
||||||
|
|
||||||
|
override fun popularAnimeSelector() = "div.panel-visible"
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector() = "button.btn-default[data-loading-text*=Sonraki]"
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
val animeTitle = element.select("div.panel-title > a").first()!!
|
||||||
|
val name = animeTitle.attr("title")
|
||||||
|
.substringBefore(" izle")
|
||||||
|
val img = element.select("img.media-object")
|
||||||
|
val animeId = element.select("a.reactions").first()!!.attr("data-unique-id")
|
||||||
|
val animeUrl = ("https:" + animeTitle.attr("href")).toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
.addQueryParameter("animeId", animeId)
|
||||||
|
.build().toString()
|
||||||
|
return SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(animeUrl)
|
||||||
|
title = name
|
||||||
|
thumbnail_url = "https:" + img.attr("data-src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
val img = document.select("div.imaj > img.media-object").ifEmpty { null }
|
||||||
|
val studio = document.select("div#animedetay > table tr:contains(Stüdyo) > td:last-child a").ifEmpty { null }
|
||||||
|
val desc = document.select("div#animedetay p.ozet").ifEmpty { null }
|
||||||
|
val genres = document.select("div#animedetay > table tr:contains(Anime Türü) > td:last-child a")
|
||||||
|
.ifEmpty { null }
|
||||||
|
return SAnime.create().apply {
|
||||||
|
title = document.select("div#detayPaylas div.panel-title").text()
|
||||||
|
thumbnail_url = img?.let { "https:" + it.attr("data-src") }
|
||||||
|
author = studio?.text()
|
||||||
|
description = desc?.text()
|
||||||
|
genre = genres?.joinToString { it.text() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
|
val animeId = (baseUrl + anime.url).toHttpUrl().queryParameter("animeId")!!
|
||||||
|
return GET("https://www.turkanime.co/ajax/bolumler?animeId=$animeId", xmlHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListSelector() = "ul.menum li"
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
val a = element.select("a:has(span.bolumAdi)")
|
||||||
|
val title = a.attr("title")
|
||||||
|
val substring = title.substringBefore(". Bölüm")
|
||||||
|
val numIdx = substring.indexOfLast { !it.isDigit() } + 1
|
||||||
|
val numbers = substring.slice(numIdx..substring.lastIndex)
|
||||||
|
return SEpisode.create().apply {
|
||||||
|
setUrlWithoutDomain("https:" + a.attr("href"))
|
||||||
|
name = title
|
||||||
|
episode_number = numbers.toFloatOrNull() ?: 1F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
return super.episodeListParse(response).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val fansubbers = document.select("div#videodetay div.pull-right button")
|
||||||
|
return if (fansubbers.size == 1) {
|
||||||
|
getVideosFromHosters(document, fansubbers.first()!!.text().trim())
|
||||||
|
} else {
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
fansubbers.parallelMap {
|
||||||
|
val url = it.attr("onclick").trimOnClick()
|
||||||
|
val subDoc = client.newCall(GET(url, xmlHeader)).execute().asJsoup()
|
||||||
|
videoList.addAll(getVideosFromHosters(subDoc, it.text().trim()))
|
||||||
|
}
|
||||||
|
videoList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVideosFromHosters(document: Document, subber: String): List<Video> {
|
||||||
|
val selectedHoster = document.select("div#videodetay div.btn-group:not(.pull-right) > button.btn-danger")
|
||||||
|
val hosters = document.select("div#videodetay div.btn-group:not(.pull-right) > button.btn-default[onclick*=videosec]")
|
||||||
|
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val selectedHosterName = selectedHoster.text().trim()
|
||||||
|
if (selectedHosterName in SUPPORTED_HOSTERS) {
|
||||||
|
val src = document.select("iframe").attr("src")
|
||||||
|
videoList.addAll(getVideosFromSource(src, selectedHosterName, subber))
|
||||||
|
}
|
||||||
|
hosters.parallelMap {
|
||||||
|
val hosterName = it.text().trim()
|
||||||
|
if (hosterName !in SUPPORTED_HOSTERS) return@parallelMap
|
||||||
|
val url = it.attr("onclick").trimOnClick()
|
||||||
|
val videoDoc = client.newCall(GET(url, xmlHeader)).execute().asJsoup()
|
||||||
|
val src = videoDoc.select("iframe").attr("src")
|
||||||
|
videoList.addAll(getVideosFromSource(src, hosterName, subber))
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVideosFromSource(src: String, hosterName: String, subber: String): List<Video> {
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val cipherParamsEncoded = src
|
||||||
|
.substringAfter("/embed/#/url/")
|
||||||
|
.substringBefore("?status")
|
||||||
|
|
||||||
|
val cipherParams = json.decodeFromString<CipherParams>(
|
||||||
|
String(
|
||||||
|
Base64.decode(cipherParamsEncoded, Base64.DEFAULT),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val hosterLink = "https:" + json.decodeFromString<JsonPrimitive>(
|
||||||
|
CryptoAES.decryptWithSalt(
|
||||||
|
cipherParams.ct,
|
||||||
|
cipherParams.s,
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
).content
|
||||||
|
|
||||||
|
when (hosterName) {
|
||||||
|
"STREAMSB" -> {
|
||||||
|
videoList.addAll(StreamSBExtractor(client).videosFromUrl(hosterLink, refererHeader, "$subber:"))
|
||||||
|
}
|
||||||
|
"VOE" -> {
|
||||||
|
VoeExtractor(client).videoFromUrl(hosterLink, "$subber: VOE")?.let { video -> videoList.add(video) }
|
||||||
|
}
|
||||||
|
"ALUCARD(BETA)" -> {
|
||||||
|
videoList.addAll(AlucardExtractor(client, json, baseUrl).extractVideos(hosterLink, subber))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class CipherParams(
|
||||||
|
@Serializable
|
||||||
|
val ct: String,
|
||||||
|
@Serializable
|
||||||
|
val iv: String,
|
||||||
|
@Serializable
|
||||||
|
val s: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String.trimOnClick() = baseUrl + "/" + this.substringAfter("IndexIcerik('").substringBefore("'")
|
||||||
|
|
||||||
|
override fun videoFromElement(element: Element): Video = throw Exception("not used")
|
||||||
|
override fun videoListSelector(): String = throw Exception("not used")
|
||||||
|
override fun videoUrlParse(document: Document): String = throw Exception("not used")
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
|
||||||
|
POST(
|
||||||
|
"$baseUrl/arama?sayfa=$page",
|
||||||
|
Headers.headersOf("content-type", "application/x-www-form-urlencoded"),
|
||||||
|
FormBody.Builder().add("arama", query).build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||||
|
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||||
|
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ajax/yenieklenenseriler?sayfa=$page", xmlHeader)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||||
|
|
||||||
|
// =============================== Preferences ===============================
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val videoQualityPref = ListPreference(screen.context).apply {
|
||||||
|
key = "preferred_quality"
|
||||||
|
title = "Preferred quality"
|
||||||
|
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||||
|
entryValues = arrayOf("1080", "720", "480", "360")
|
||||||
|
setDefaultValue("1080")
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(videoQualityPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString("preferred_quality", "1080")
|
||||||
|
if (quality != null) {
|
||||||
|
val newList = mutableListOf<Video>()
|
||||||
|
var preferred = 0
|
||||||
|
for (video in this) {
|
||||||
|
if (video.quality.contains(quality)) {
|
||||||
|
newList.add(preferred, video)
|
||||||
|
preferred++
|
||||||
|
} else {
|
||||||
|
newList.add(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private val xmlHeader = Headers.headersOf("X-Requested-With", "XMLHttpRequest")
|
||||||
|
private val refererHeader = Headers.headersOf("Referer", baseUrl)
|
||||||
|
|
||||||
|
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||||
|
runBlocking(Dispatchers.Default) {
|
||||||
|
map { async { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKey() {
|
||||||
|
val script4 = client.newCall(GET("$baseUrl/embed/#/")).execute().asJsoup()
|
||||||
|
.select("script[defer]").getOrNull(1)
|
||||||
|
?.attr("src") ?: return
|
||||||
|
val embeds4 = client.newCall(GET(baseUrl + script4)).execute().body.string()
|
||||||
|
val name = "(?<=')[0-9a-f]{16}(?=')".toRegex().findAll(embeds4).toList().firstOrNull()?.value
|
||||||
|
|
||||||
|
val file5 = client.newCall(GET("$baseUrl/embed/js/embeds.$name.js")).execute().body.string()
|
||||||
|
val embeds5 = Deobfuscator.deobfuscateScript(file5) ?: return
|
||||||
|
val key = "(?<=')\\S{100}(?=')".toRegex().find(embeds5)?.value ?: return
|
||||||
|
preferences.edit().putString(PREF_KEY_KEY, key).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
GlobalScope.launch {
|
||||||
|
withContext(Dispatchers.IO) { getKey() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SUPPORTED_HOSTERS = listOf(
|
||||||
|
// TODO: Fix Alucard
|
||||||
|
// "ALUCARD(BETA)",
|
||||||
|
"STREAMSB",
|
||||||
|
"VOE")
|
||||||
|
|
||||||
|
private const val PREF_KEY_KEY = "key"
|
||||||
|
private const val DEFAULT_KEY = "710^8A@3@>T2}#zN5xK?kR7KNKb@-A!LzYL5~M1qU0UfdWsZoBm4UUat%}ueUv6E--*hDPPbH7K2bp9^3o41hw,khL:}Kx8080@M"
|
Loading…
x
Reference in New Issue
Block a user