Add extension: AnimeDao (#1284)
This commit is contained in:
@ -26,8 +26,9 @@ class StreamSBExtractor(private val client: OkHttpClient) {
|
|||||||
|
|
||||||
// animension, asianload and dramacool uses "common = false"
|
// animension, asianload and dramacool uses "common = false"
|
||||||
private fun fixUrl(url: String, common: Boolean): String {
|
private fun fixUrl(url: String, common: Boolean): String {
|
||||||
val sbUrl = url.substringBefore("/e/")
|
val sbUrl = url.substringBefore("/e/").substringBefore("/embed-")
|
||||||
val id = url.substringAfter("/e/")
|
val id = url.substringAfter("/e/")
|
||||||
|
.substringAfter("/embed-")
|
||||||
.substringBefore("?")
|
.substringBefore("?")
|
||||||
.substringBefore(".html")
|
.substringBefore(".html")
|
||||||
return if (common) {
|
return if (common) {
|
||||||
|
2
src/en/animedao/AndroidManifest.xml
Normal file
2
src/en/animedao/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.animeextension" />
|
23
src/en/animedao/build.gradle
Normal file
23
src/en/animedao/build.gradle
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'AnimeDao'
|
||||||
|
pkgNameSuffix = 'en.animedao'
|
||||||
|
extClass = '.AnimeDao'
|
||||||
|
extVersionCode = 1
|
||||||
|
libVersion = '13'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly libs.bundles.coroutines
|
||||||
|
implementation(project(':lib-streamtape-extractor'))
|
||||||
|
implementation(project(':lib-streamsb-extractor'))
|
||||||
|
implementation(project(':lib-fembed-extractor'))
|
||||||
|
implementation(project(':lib-dood-extractor'))
|
||||||
|
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/animedao/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/animedao/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
BIN
src/en/animedao/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/animedao/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
src/en/animedao/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/animedao/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
src/en/animedao/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/animedao/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
BIN
src/en/animedao/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/animedao/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
src/en/animedao/res/web_hi_res_512.png
Normal file
BIN
src/en/animedao/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
@ -0,0 +1,391 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.animedao
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.animedao.extractors.MixDropExtractor
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.animedao.extractors.Mp4uploadExtractor
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.animedao.extractors.VidstreamingExtractor
|
||||||
|
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.ParsedAnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
|
override val name = "AnimeDao"
|
||||||
|
|
||||||
|
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animedao.to")!! }
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DateFormatter by lazy {
|
||||||
|
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animelist/popular")
|
||||||
|
|
||||||
|
override fun popularAnimeSelector(): String = "div.container > div.row > div.col-md-6"
|
||||||
|
|
||||||
|
override fun popularAnimeNextPageSelector(): String? = null
|
||||||
|
|
||||||
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
|
val thumbnailUrl = element.selectFirst("img").attr("data-src")
|
||||||
|
|
||||||
|
return SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a").attr("href"))
|
||||||
|
thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) {
|
||||||
|
thumbnailUrl
|
||||||
|
} else {
|
||||||
|
baseUrl + thumbnailUrl
|
||||||
|
}
|
||||||
|
title = element.selectFirst("span.animename").text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String = "div#latest-tab-pane > div.row > div.col-md-6"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String? = popularAnimeNextPageSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||||
|
val thumbnailUrl = element.selectFirst("img").attr("data-src")
|
||||||
|
|
||||||
|
return SAnime.create().apply {
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a.animeparent").attr("href"))
|
||||||
|
thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) {
|
||||||
|
thumbnailUrl
|
||||||
|
} else {
|
||||||
|
baseUrl + thumbnailUrl
|
||||||
|
}
|
||||||
|
title = element.selectFirst("span.animename").text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
|
||||||
|
|
||||||
|
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||||
|
val params = AnimeDaoFilters.getSearchParameters(filters)
|
||||||
|
return client.newCall(searchAnimeRequest(page, query, params))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
searchAnimeParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val animes = if (response.request.url.encodedPath.startsWith("/animelist/")) {
|
||||||
|
document.select(searchAnimeSelectorFilter()).map { element ->
|
||||||
|
searchAnimeFromElement(element)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.select(searchAnimeSelector()).map { element ->
|
||||||
|
searchAnimeFromElement(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return AnimesPage(animes, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchAnimeRequest(page: Int, query: String, filters: AnimeDaoFilters.FilterSearchParams): Request {
|
||||||
|
return if (query.isNotBlank()) {
|
||||||
|
val cleanQuery = query.replace(" ", "+")
|
||||||
|
GET("$baseUrl/search/?search=$cleanQuery", headers = headers)
|
||||||
|
} else {
|
||||||
|
var url = "$baseUrl/animelist/".toHttpUrlOrNull()!!.newBuilder()
|
||||||
|
.addQueryParameter("status[]=", filters.status)
|
||||||
|
.addQueryParameter("order[]=", filters.order)
|
||||||
|
.build().toString()
|
||||||
|
|
||||||
|
if (filters.genre.isNotBlank()) url += "&${filters.genre}"
|
||||||
|
if (filters.rating.isNotBlank()) url += "&${filters.rating}"
|
||||||
|
if (filters.letter.isNotBlank()) url += "&${filters.letter}"
|
||||||
|
if (filters.year.isNotBlank()) url += "&${filters.year}"
|
||||||
|
if (filters.score.isNotBlank()) url += "&${filters.score}"
|
||||||
|
url += "&page=$page"
|
||||||
|
|
||||||
|
GET(url, headers = headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
|
private fun searchAnimeSelectorFilter(): String = "div.container div.col-12 > div.row > div.col-md-6"
|
||||||
|
|
||||||
|
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.page-item:has(i.fa-arrow-right):not(.disabled)"
|
||||||
|
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||||
|
|
||||||
|
// ============================== FILTERS ===============================
|
||||||
|
|
||||||
|
override fun getFilterList(): AnimeFilterList = AnimeDaoFilters.filterList
|
||||||
|
|
||||||
|
// =========================== Anime Details ============================
|
||||||
|
|
||||||
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
|
val thumbnailUrl = document.selectFirst("div.card-body img").attr("data-src")
|
||||||
|
val moreInfo = document.select("div.card-body table > tbody > tr").joinToString("\n") { it.text() }
|
||||||
|
|
||||||
|
return SAnime.create().apply {
|
||||||
|
title = document.selectFirst("div.card-body h2").text()
|
||||||
|
thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) {
|
||||||
|
thumbnailUrl
|
||||||
|
} else {
|
||||||
|
baseUrl + thumbnailUrl
|
||||||
|
}
|
||||||
|
status = document.selectFirst("div.card-body table > tbody > tr:has(>td:contains(Status)) td:not(:contains(Status))")?.let {
|
||||||
|
parseStatus(it.text())
|
||||||
|
} ?: SAnime.UNKNOWN
|
||||||
|
description = (document.selectFirst("div.card-body div:has(>b:contains(Description))")?.ownText() ?: "") + "\n\n$moreInfo"
|
||||||
|
genre = document.select("div.card-body table > tbody > tr:has(>td:contains(Genres)) td > a").joinToString(", ") { it.text() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
return if (preferences.getBoolean("preferred_episode_sorting", false)) {
|
||||||
|
super.episodeListParse(response).sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.episode_number },
|
||||||
|
{ it.name }
|
||||||
|
)
|
||||||
|
).reversed()
|
||||||
|
} else {
|
||||||
|
super.episodeListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun episodeListSelector(): String = "div#episodes-tab-pane > div.row > div > div.card"
|
||||||
|
|
||||||
|
override fun episodeFromElement(element: Element): SEpisode {
|
||||||
|
val episodeName = element.selectFirst("span.animename").text()
|
||||||
|
val episodeTitle = element.selectFirst("div.animetitle")?.text() ?: ""
|
||||||
|
|
||||||
|
return SEpisode.create().apply {
|
||||||
|
name = "$episodeName $episodeTitle"
|
||||||
|
episode_number = if (episodeName.contains("Episode ", true)) {
|
||||||
|
episodeName.substringAfter("Episode ").substringBefore(" ").toFloatOrNull() ?: 0F
|
||||||
|
} else { 0F }
|
||||||
|
date_upload = element.selectFirst("span.date")?.let { parseDate(it.text()) } ?: 0L
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a[href]").attr("href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Video Links =============================
|
||||||
|
|
||||||
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val serverList = mutableListOf<Server>()
|
||||||
|
val script = document.selectFirst("script:containsData(videowrapper)").data()
|
||||||
|
val frameRegex = """function (\w+).*?iframe src=\"(.*?)\"""".toRegex()
|
||||||
|
|
||||||
|
frameRegex.findAll(script).forEach {
|
||||||
|
val redirected = client.newCall(GET(baseUrl + it.groupValues[2])).execute().request.url.toString()
|
||||||
|
serverList.add(
|
||||||
|
Server(
|
||||||
|
redirected,
|
||||||
|
it.groupValues[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get videos
|
||||||
|
videoList.addAll(
|
||||||
|
serverList.parallelMap { server ->
|
||||||
|
runCatching {
|
||||||
|
val prefix = "${server.name} - "
|
||||||
|
val url = server.url
|
||||||
|
|
||||||
|
when {
|
||||||
|
url.contains("streamtape") -> {
|
||||||
|
val video = StreamTapeExtractor(client).videoFromUrl(url, server.name)
|
||||||
|
if (video == null) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
listOf(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url.contains("streamsb") -> {
|
||||||
|
StreamSBExtractor(client).videosFromUrl(url, headers = headers, prefix = prefix)
|
||||||
|
}
|
||||||
|
url.contains("vidstreaming") -> {
|
||||||
|
VidstreamingExtractor(client, json).videosFromUrl(url, prefix = prefix)
|
||||||
|
}
|
||||||
|
url.contains("sbhight") || url.contains("sbrity") || url.contains("sbembed.com") || url.contains("sbembed1.com") || url.contains("sbplay.org") ||
|
||||||
|
url.contains("sbvideo.net") || url.contains("streamsb.net") || url.contains("sbplay.one") ||
|
||||||
|
url.contains("cloudemb.com") || url.contains("playersb.com") || url.contains("tubesb.com") ||
|
||||||
|
url.contains("sbplay1.com") || url.contains("embedsb.com") || url.contains("watchsb.com") ||
|
||||||
|
url.contains("sbplay2.com") || url.contains("japopav.tv") || url.contains("viewsb.com") ||
|
||||||
|
url.contains("sbfast") || url.contains("sbfull.com") || url.contains("javplaya.com") ||
|
||||||
|
url.contains("ssbstream.net") || url.contains("p1ayerjavseen.com") || url.contains("sbthe.com") ||
|
||||||
|
url.contains("vidmovie.xyz") || url.contains("sbspeed.com") || url.contains("streamsss.net") ||
|
||||||
|
url.contains("sblanh.com") || url.contains("tvmshow.com") || url.contains("sbanh.com") ||
|
||||||
|
url.contains("streamovies.xyz") || url.contains("vcdn.space") -> {
|
||||||
|
val newUrl = url.replace("https://www.fembed.com", "https://vanfem.com")
|
||||||
|
FembedExtractor(client).videosFromUrl(newUrl, prefix = prefix, redirect = true)
|
||||||
|
}
|
||||||
|
url.contains("mixdrop") -> {
|
||||||
|
MixDropExtractor(client).videoFromUrl(url, prefix = prefix)
|
||||||
|
}
|
||||||
|
url.contains("https://dood") -> {
|
||||||
|
DoodExtractor(client).videosFromUrl(url, quality = server.name)
|
||||||
|
}
|
||||||
|
url.contains("mp4upload") -> {
|
||||||
|
val headers = headers.newBuilder().set("referer", "https://mp4upload.com/").build()
|
||||||
|
Mp4uploadExtractor(client).getVideoFromUrl(url, headers, prefix)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}.filterNotNull().flatten()
|
||||||
|
)
|
||||||
|
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
override fun List<Video>.sort(): List<Video> {
|
||||||
|
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||||
|
|
||||||
|
return this.sortedWith(
|
||||||
|
compareBy { it.quality.contains(quality) }
|
||||||
|
).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Server(
|
||||||
|
val url: String,
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseDate(dateStr: String): Long {
|
||||||
|
return runCatching { DateFormatter.parse(dateStr)?.time }
|
||||||
|
.getOrNull() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(statusString: String): Int {
|
||||||
|
return when (statusString) {
|
||||||
|
"Ongoing" -> SAnime.ONGOING
|
||||||
|
"Completed" -> SAnime.COMPLETED
|
||||||
|
else -> SAnime.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From Dopebox
|
||||||
|
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||||
|
runBlocking {
|
||||||
|
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val domainPref = ListPreference(screen.context).apply {
|
||||||
|
key = "preferred_domain"
|
||||||
|
title = "Preferred domain (requires app restart)"
|
||||||
|
entries = arrayOf("animedao.to")
|
||||||
|
entryValues = arrayOf("https://animedao.to")
|
||||||
|
setDefaultValue("https://animedao.to")
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val episodeSortPref = SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = "preferred_episode_sorting"
|
||||||
|
title = "Attempt episode sorting"
|
||||||
|
summary = """AnimeDao displays the episodes in either ascending or descending order,
|
||||||
|
| enable to attempt order or disable to set same as website.""".trimMargin()
|
||||||
|
setDefaultValue(true)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val new = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(key, new).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.addPreference(domainPref)
|
||||||
|
screen.addPreference(videoQualityPref)
|
||||||
|
screen.addPreference(episodeSortPref)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,256 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.animedao
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
|
||||||
|
object AnimeDaoFilters {
|
||||||
|
open class QueryPartFilter(
|
||||||
|
displayName: String,
|
||||||
|
val vals: Array<Pair<String, String>>
|
||||||
|
) : AnimeFilter.Select<String>(
|
||||||
|
displayName,
|
||||||
|
vals.map { it.first }.toTypedArray()
|
||||||
|
) {
|
||||||
|
fun toQueryPart() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||||
|
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||||
|
return this.filterIsInstance<R>().joinToString("") {
|
||||||
|
(it as QueryPartFilter).toQueryPart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||||
|
return this.filterIsInstance<R>().first()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||||
|
options: Array<Pair<String, String>>,
|
||||||
|
name: String,
|
||||||
|
): String {
|
||||||
|
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||||
|
.mapNotNull { checkbox ->
|
||||||
|
if (checkbox.state)
|
||||||
|
options.find { it.first == checkbox.name }!!.second
|
||||||
|
else null
|
||||||
|
}.joinToString("&$name[]=").let {
|
||||||
|
if (it.isBlank()) ""
|
||||||
|
else "$name[]=$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenreFilter : CheckBoxFilterList(
|
||||||
|
"Genre",
|
||||||
|
AnimeDaoFiltersData.genre.map { CheckBoxVal(it.first, false) }
|
||||||
|
)
|
||||||
|
|
||||||
|
class RatingFilter : CheckBoxFilterList(
|
||||||
|
"Rating",
|
||||||
|
AnimeDaoFiltersData.rating.map { CheckBoxVal(it.first, false) }
|
||||||
|
)
|
||||||
|
|
||||||
|
class LetterFilter : CheckBoxFilterList(
|
||||||
|
"Letter",
|
||||||
|
AnimeDaoFiltersData.letter.map { CheckBoxVal(it.first, false) }
|
||||||
|
)
|
||||||
|
|
||||||
|
class YearFilter : CheckBoxFilterList(
|
||||||
|
"Year",
|
||||||
|
AnimeDaoFiltersData.year.map { CheckBoxVal(it.first, false) }
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter : QueryPartFilter("Status", AnimeDaoFiltersData.status)
|
||||||
|
|
||||||
|
class ScoreFilter : CheckBoxFilterList(
|
||||||
|
"Score",
|
||||||
|
AnimeDaoFiltersData.score.map { CheckBoxVal(it.first, false) }
|
||||||
|
)
|
||||||
|
|
||||||
|
class OrderFilter : QueryPartFilter("Order", AnimeDaoFiltersData.order)
|
||||||
|
|
||||||
|
val filterList = AnimeFilterList(
|
||||||
|
GenreFilter(),
|
||||||
|
RatingFilter(),
|
||||||
|
LetterFilter(),
|
||||||
|
YearFilter(),
|
||||||
|
ScoreFilter(),
|
||||||
|
AnimeFilter.Separator(),
|
||||||
|
StatusFilter(),
|
||||||
|
OrderFilter()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FilterSearchParams(
|
||||||
|
val genre: String = "",
|
||||||
|
val rating: String = "",
|
||||||
|
val letter: String = "",
|
||||||
|
val year: String = "",
|
||||||
|
val status: String = "",
|
||||||
|
val score: String = "",
|
||||||
|
val order: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||||
|
if (filters.isEmpty()) return FilterSearchParams()
|
||||||
|
|
||||||
|
return FilterSearchParams(
|
||||||
|
filters.parseCheckbox<GenreFilter>(AnimeDaoFiltersData.genre, "genres"),
|
||||||
|
filters.parseCheckbox<RatingFilter>(AnimeDaoFiltersData.rating, "ratings"),
|
||||||
|
filters.parseCheckbox<LetterFilter>(AnimeDaoFiltersData.letter, "letters"),
|
||||||
|
filters.parseCheckbox<YearFilter>(AnimeDaoFiltersData.year, "years"),
|
||||||
|
filters.asQueryPart<StatusFilter>(),
|
||||||
|
filters.parseCheckbox<ScoreFilter>(AnimeDaoFiltersData.score, "score"),
|
||||||
|
filters.asQueryPart<OrderFilter>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object AnimeDaoFiltersData {
|
||||||
|
val genre = arrayOf(
|
||||||
|
Pair("Action", "Action"),
|
||||||
|
Pair("Adventure", "Adventure"),
|
||||||
|
Pair("Chinese", "Chinese"),
|
||||||
|
Pair("Comedy", "Comedy"),
|
||||||
|
Pair("Detective", "Detective"),
|
||||||
|
Pair("Drama", "Drama"),
|
||||||
|
Pair("Ecchi", "Ecchi"),
|
||||||
|
Pair("Fantasy", "Fantasy"),
|
||||||
|
Pair("Gourmet", "Gourmet"),
|
||||||
|
Pair("Harem", "Harem"),
|
||||||
|
Pair("High Stakes Game", "High Stakes Game"),
|
||||||
|
Pair("Historical", "Historical"),
|
||||||
|
Pair("Horror", "Horror"),
|
||||||
|
Pair("Isekai", "Isekai"),
|
||||||
|
Pair("Iyashikei", "Iyashikei"),
|
||||||
|
Pair("Josei", "Josei"),
|
||||||
|
Pair("Kids", "Kids"),
|
||||||
|
Pair("Martial Arts", "Martial Arts"),
|
||||||
|
Pair("Mecha", "Mecha"),
|
||||||
|
Pair("Military", "Military"),
|
||||||
|
Pair("Music", "Music"),
|
||||||
|
Pair("Mystery", "Mystery"),
|
||||||
|
Pair("Mythology", "Mythology"),
|
||||||
|
Pair("Parody", "Parody"),
|
||||||
|
Pair("Psychological", "Psychological"),
|
||||||
|
Pair("Racing", "Racing"),
|
||||||
|
Pair("Reincarnation", "Reincarnation"),
|
||||||
|
Pair("Romance", "Romance"),
|
||||||
|
Pair("Samurai", "Samurai"),
|
||||||
|
Pair("School", "School"),
|
||||||
|
Pair("Sci-Fi", "Sci-Fi"),
|
||||||
|
Pair("Seinen", "Seinen"),
|
||||||
|
Pair("Shoujo", "Shoujo"),
|
||||||
|
Pair("Shoujo Ai", "Shoujo Ai"),
|
||||||
|
Pair("Shounen", "Shounen"),
|
||||||
|
Pair("Shounen Ai", "Shounen Ai"),
|
||||||
|
Pair("Slice of Life", "Slice of Life"),
|
||||||
|
Pair("Space", "Space"),
|
||||||
|
Pair("Sports", "Sports"),
|
||||||
|
Pair("Strategy Game", "Strategy Game"),
|
||||||
|
Pair("Super Power", "Super Power"),
|
||||||
|
Pair("Supernatural", "Supernatural"),
|
||||||
|
Pair("Survival", "Survival"),
|
||||||
|
Pair("Suspense", "Suspense"),
|
||||||
|
Pair("Team Sports", "Team Sports"),
|
||||||
|
Pair("Time Travel", "Time Travel"),
|
||||||
|
Pair("Vampire", "Vampire"),
|
||||||
|
Pair("Video Game", "Video Game")
|
||||||
|
)
|
||||||
|
|
||||||
|
val rating = arrayOf(
|
||||||
|
Pair("G - All Ages", "all-ages"),
|
||||||
|
Pair("PG - Children", "children"),
|
||||||
|
Pair("PG-13 - Teens 13 or older", "pg13"),
|
||||||
|
Pair("R - 17+ (violence & profanity)", "r17"),
|
||||||
|
Pair("R+ - Mild Nudity", "rplus")
|
||||||
|
)
|
||||||
|
|
||||||
|
val letter = arrayOf(
|
||||||
|
Pair("A", "A"),
|
||||||
|
Pair("B", "B"),
|
||||||
|
Pair("C", "C"),
|
||||||
|
Pair("D", "D"),
|
||||||
|
Pair("E", "E"),
|
||||||
|
Pair("F", "F"),
|
||||||
|
Pair("G", "G"),
|
||||||
|
Pair("H", "H"),
|
||||||
|
Pair("I", "I"),
|
||||||
|
Pair("J", "J"),
|
||||||
|
Pair("K", "K"),
|
||||||
|
Pair("L", "L"),
|
||||||
|
Pair("M", "M"),
|
||||||
|
Pair("N", "N"),
|
||||||
|
Pair("O", "O"),
|
||||||
|
Pair("P", "P"),
|
||||||
|
Pair("Q", "Q"),
|
||||||
|
Pair("R", "R"),
|
||||||
|
Pair("S", "S"),
|
||||||
|
Pair("T", "T"),
|
||||||
|
Pair("U", "U"),
|
||||||
|
Pair("V", "V"),
|
||||||
|
Pair("W", "W"),
|
||||||
|
Pair("X", "X"),
|
||||||
|
Pair("Y", "Y"),
|
||||||
|
Pair("Z", "Z")
|
||||||
|
)
|
||||||
|
|
||||||
|
val year = arrayOf(
|
||||||
|
Pair("2023", "2023"),
|
||||||
|
Pair("2022", "2022"),
|
||||||
|
Pair("2021", "2021"),
|
||||||
|
Pair("2020", "2020"),
|
||||||
|
Pair("2019", "2019"),
|
||||||
|
Pair("2018", "2018"),
|
||||||
|
Pair("2017", "2017"),
|
||||||
|
Pair("2016", "2016"),
|
||||||
|
Pair("2015", "2015"),
|
||||||
|
Pair("2014", "2014"),
|
||||||
|
Pair("2013", "2013"),
|
||||||
|
Pair("2012", "2012"),
|
||||||
|
Pair("2011", "2011"),
|
||||||
|
Pair("2010", "2010"),
|
||||||
|
Pair("2009", "2009"),
|
||||||
|
Pair("2008", "2008"),
|
||||||
|
Pair("2007", "2007"),
|
||||||
|
Pair("2006", "2006"),
|
||||||
|
Pair("2005", "2005"),
|
||||||
|
Pair("2004", "2004"),
|
||||||
|
Pair("2003", "2003"),
|
||||||
|
Pair("2002", "2002"),
|
||||||
|
Pair("2001", "2001"),
|
||||||
|
Pair("2000", "2000"),
|
||||||
|
Pair("1990 - 1999", "1990"),
|
||||||
|
Pair("1980 - 1989", "1980"),
|
||||||
|
Pair("1970 - 1979", "1970"),
|
||||||
|
Pair("1960 - 1969", "1960")
|
||||||
|
)
|
||||||
|
|
||||||
|
val status = arrayOf(
|
||||||
|
Pair("Status", ""),
|
||||||
|
Pair("Ongoing", "Ongoing"),
|
||||||
|
Pair("Completed", "Completed")
|
||||||
|
)
|
||||||
|
|
||||||
|
val score = arrayOf(
|
||||||
|
Pair("Outstanding (9+)", "outstanding"),
|
||||||
|
Pair("Excellent (8+)", "excellent"),
|
||||||
|
Pair("Very Good (7+)", "verygood"),
|
||||||
|
Pair("Good (6+)", "good"),
|
||||||
|
Pair("Average (5+)", "average"),
|
||||||
|
Pair("Poor (4+)", "poor"),
|
||||||
|
Pair("Bad (3+)", "bad"),
|
||||||
|
Pair("Dont Watch (2+)", "dontwatch")
|
||||||
|
)
|
||||||
|
|
||||||
|
val order = arrayOf(
|
||||||
|
Pair("Order", ""),
|
||||||
|
Pair("Name A -> Z", "nameaz"),
|
||||||
|
Pair("Name Z -> A", "nameza"),
|
||||||
|
Pair("Date New -> Old", "datenewold"),
|
||||||
|
Pair("Date Old -> New", "dateoldnew"),
|
||||||
|
Pair("Score", "score"),
|
||||||
|
Pair("Most Watched", "mostwatched")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.animedao.extractors
|
||||||
|
|
||||||
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class MixDropExtractor(private val client: OkHttpClient) {
|
||||||
|
fun videoFromUrl(url: String, lang: String = "", prefix: String = ""): List<Video> {
|
||||||
|
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
|
||||||
|
?.data()
|
||||||
|
?.let { JsUnpacker.unpackAndCombine(it) }
|
||||||
|
?: return emptyList<Video>()
|
||||||
|
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
|
||||||
|
.substringBefore("\"")
|
||||||
|
val quality = ("MixDrop").let {
|
||||||
|
if (lang.isNotBlank()) {
|
||||||
|
"$it($lang)"
|
||||||
|
} else it
|
||||||
|
}
|
||||||
|
val referer = Headers.headersOf("Referer", "https://mixdrop.co/")
|
||||||
|
return listOf(Video(url, prefix + quality, videoUrl, headers = referer))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.animedao.extractors
|
||||||
|
|
||||||
|
import dev.datlag.jsunpacker.JsUnpacker
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class Mp4uploadExtractor(private val client: OkHttpClient) {
|
||||||
|
fun getVideoFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
||||||
|
val body = client.newCall(GET(url, headers = headers)).execute().body!!.string()
|
||||||
|
|
||||||
|
val packed = body.substringAfter("<script type='text/javascript'>eval(function(p,a,c,k,e,d)")
|
||||||
|
.substringBefore("</script>")
|
||||||
|
val unpacked = JsUnpacker.unpackAndCombine("eval(function(p,a,c,k,e,d)" + packed) ?: return emptyList()
|
||||||
|
val videoUrl = unpacked.substringAfter("player.src(\"").substringBefore("\");")
|
||||||
|
return listOf(
|
||||||
|
Video(videoUrl, "$prefix Mp4upload", videoUrl, headers = Headers.headersOf("Referer", "https://www.mp4upload.com/"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.animedao.extractors
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
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.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
class VidstreamingExtractor(private val client: OkHttpClient, private val json: Json) {
|
||||||
|
fun videosFromUrl(serverUrl: String, prefix: String = ""): List<Video> {
|
||||||
|
try {
|
||||||
|
var document = client.newCall(GET(serverUrl)).execute().asJsoup()
|
||||||
|
var newUrl = serverUrl
|
||||||
|
if (serverUrl.contains("/embedded/") && document.selectFirst("body").childrenSize() == 1) {
|
||||||
|
newUrl = document.selectFirst("iframe").attr("src")
|
||||||
|
document = client.newCall(
|
||||||
|
GET(newUrl)
|
||||||
|
).execute().asJsoup()
|
||||||
|
}
|
||||||
|
|
||||||
|
val script = document.selectFirst("script:containsData(playerInstance)")
|
||||||
|
if (script != null) {
|
||||||
|
val url = script.data().substringAfter("\"file\": '").substringBefore("'")
|
||||||
|
val headers = Headers.headersOf(
|
||||||
|
"Accept", "*/*",
|
||||||
|
"Host", url.toHttpUrl().host,
|
||||||
|
"Origin", "https://${newUrl.toHttpUrl().host}"
|
||||||
|
)
|
||||||
|
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val masterPlaylist = client.newCall(GET(url, headers = headers)).execute().body!!.string()
|
||||||
|
|
||||||
|
if (masterPlaylist.contains("#EXT-X-STREAM-INF:")) {
|
||||||
|
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
||||||
|
.split("#EXT-X-STREAM-INF:").forEach {
|
||||||
|
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
|
||||||
|
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
if (!videoUrl.startsWith("http")) {
|
||||||
|
videoUrl = url.substringBeforeLast("/") + "/$videoUrl"
|
||||||
|
}
|
||||||
|
videoList.add(Video(videoUrl, "$prefix$quality (Vid-mp4 - Vidstreaming)", videoUrl))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
videoList.add(Video(url, "$prefix (Vid-mp4 - Vidstreaming)", url))
|
||||||
|
}
|
||||||
|
return videoList
|
||||||
|
}
|
||||||
|
val iv = document.select("div.wrapper")
|
||||||
|
.attr("class").substringAfter("container-")
|
||||||
|
.filter { it.isDigit() }.toByteArray()
|
||||||
|
val secretKey = document.select("body[class]")
|
||||||
|
.attr("class").substringAfter("container-")
|
||||||
|
.filter { it.isDigit() }.toByteArray()
|
||||||
|
val decryptionKey = document.select("div.videocontent")
|
||||||
|
.attr("class").substringAfter("videocontent-")
|
||||||
|
.filter { it.isDigit() }.toByteArray()
|
||||||
|
val encryptAjaxParams = cryptoHandler(
|
||||||
|
document.select("script[data-value]")
|
||||||
|
.attr("data-value"),
|
||||||
|
iv, secretKey, false
|
||||||
|
).substringAfter("&")
|
||||||
|
|
||||||
|
val httpUrl = newUrl.toHttpUrl()
|
||||||
|
val host = "https://" + httpUrl.host + "/"
|
||||||
|
val id = httpUrl.queryParameter("id") ?: throw Exception("error getting id")
|
||||||
|
val encryptedId = cryptoHandler(id, iv, secretKey)
|
||||||
|
val token = httpUrl.queryParameter("token")
|
||||||
|
val qualitySuffix = if (token != null) " (Vid-mp4 - Gogostream)" else " (Vid-mp4 - Vidstreaming)"
|
||||||
|
|
||||||
|
val jsonResponse = client.newCall(
|
||||||
|
GET(
|
||||||
|
"${host}encrypt-ajax.php?id=$encryptedId&$encryptAjaxParams&alias=$id",
|
||||||
|
Headers.headersOf(
|
||||||
|
"X-Requested-With", "XMLHttpRequest"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).execute().body!!.string()
|
||||||
|
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
|
||||||
|
val decryptedData = cryptoHandler(data, iv, decryptionKey, false)
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
|
val autoList = mutableListOf<Video>()
|
||||||
|
val array = json.decodeFromString<JsonObject>(decryptedData)["source"]!!.jsonArray
|
||||||
|
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
|
||||||
|
val fileURL = array[0].jsonObject["file"].toString().trim('"')
|
||||||
|
val masterPlaylist = client.newCall(GET(fileURL)).execute().body!!.string()
|
||||||
|
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
||||||
|
.split("#EXT-X-STREAM-INF:").forEach {
|
||||||
|
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
|
||||||
|
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
if (!videoUrl.startsWith("http")) {
|
||||||
|
videoUrl = fileURL.substringBeforeLast("/") + "/$videoUrl"
|
||||||
|
}
|
||||||
|
videoList.add(Video(videoUrl, prefix + quality + qualitySuffix, videoUrl))
|
||||||
|
}
|
||||||
|
} else array.forEach {
|
||||||
|
val label = it.jsonObject["label"].toString().lowercase(Locale.ROOT)
|
||||||
|
.trim('"').replace(" ", "")
|
||||||
|
val fileURL = it.jsonObject["file"].toString().trim('"')
|
||||||
|
val videoHeaders = Headers.headersOf("Referer", newUrl)
|
||||||
|
if (label == "auto") autoList.add(
|
||||||
|
Video(
|
||||||
|
fileURL,
|
||||||
|
prefix + label + qualitySuffix,
|
||||||
|
fileURL,
|
||||||
|
headers = videoHeaders
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else videoList.add(Video(fileURL, prefix + label + qualitySuffix, fileURL, headers = videoHeaders))
|
||||||
|
}
|
||||||
|
return videoList.sortedByDescending {
|
||||||
|
it.quality.substringBefore(qualitySuffix).substringBefore("p").toIntOrNull() ?: -1
|
||||||
|
} + autoList
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cryptoHandler(
|
||||||
|
string: String,
|
||||||
|
iv: ByteArray,
|
||||||
|
secretKeyString: ByteArray,
|
||||||
|
encrypt: Boolean = true
|
||||||
|
): String {
|
||||||
|
val ivParameterSpec = IvParameterSpec(iv)
|
||||||
|
val secretKey = SecretKeySpec(secretKeyString, "AES")
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
return if (!encrypt) {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
|
||||||
|
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
|
||||||
|
} else {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
|
||||||
|
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user