Add extension: AnimeDao (#1284)

This commit is contained in:
Secozzi
2023-02-17 20:52:59 +01:00
committed by GitHub
parent cc7d9f0c3c
commit f4cbcef2dc
14 changed files with 872 additions and 1 deletions

View File

@ -26,8 +26,9 @@ class StreamSBExtractor(private val client: OkHttpClient) {
// animension, asianload and dramacool uses "common = false"
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/")
.substringAfter("/embed-")
.substringBefore("?")
.substringBefore(".html")
return if (common) {

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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)
}
}

View File

@ -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")
)
}
}

View File

@ -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))
}
}

View File

@ -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/"))
)
}
}

View File

@ -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)
}
}
}