feat(src/en): New source: AnimeTake (#2411)

This commit is contained in:
Salman Ali
2023-10-25 16:27:57 +05:45
committed by GitHub
parent 7ae7ecdc49
commit ea1928731f
11 changed files with 613 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,18 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'AnimeTake'
pkgNameSuffix = 'en.animetake'
extClass = '.AnimeTake'
extVersionCode = 1
containsNsfw = false
}
dependencies {
implementation(project(":lib-dood-extractor"))
implementation(project(":lib-mp4upload-extractor"))
implementation(project(":lib-filemoon-extractor"))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,289 @@
package eu.kanade.tachiyomi.animeextension.en.animetake
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.animetake.extractors.VidstreamingExtractor
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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
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
import java.text.SimpleDateFormat
import java.util.Locale
class AnimeTake : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeTake"
override val baseUrl = "https://animetake.tv"
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animelist/popular")
override fun popularAnimeSelector() = "div.col-sm-6"
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.select("div > a").attr("href"))
thumbnail_url = baseUrl + element.select("div.latestep_image > img").attr("data-src")
title = element.select("span.latestep_title > h4").first()!!.ownText()
}
}
override fun popularAnimeNextPageSelector() = "ul.pagination > li.page-item:last-child"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/animelist/?page=$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimeTakeFilters.getSearchParameters(filters)
val cleanQuery = query.replace(" ", "+").lowercase()
return if (query.isNotEmpty()) {
GET("$baseUrl/search/?search=$cleanQuery&page=$page")
} else {
val multiString = buildString {
if (params.letters.isNotEmpty()) append(params.letters + "&")
if (params.genres.isNotEmpty()) append(params.genres + "&")
if (params.score.isNotEmpty()) append(params.score + "&")
if (params.years.isNotEmpty()) append(params.years + "&")
if (params.ratings.isNotEmpty()) append(params.ratings + "&")
}
GET("$baseUrl/animelist/?page=$page&$multiString")
}
}
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
// ============================== Filters ===============================
override fun getFilterList() = AnimeTakeFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("h3 > b").text()
anime.genre = document.select("a.animeinfo_label").joinToString {
it.select("span").text()
}
anime.description = document.select("div.visible-md").first()!!.ownText()
anime.status =
parseStatus(document.select("div.well > center:contains(Next Episode)").isNotEmpty())
document.select("div.well > p").first()!!.text().let {
if (it.isBlank().not()) {
anime.description = when {
anime.description.isNullOrBlank() -> it
else -> anime.description + "\n\n" + it
}
}
}
return anime
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div#eps > div > a[href]"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodesLink = document.select(episodeListSelector())
return episodesLink.map(::episodeFromElement).reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val upDate = element.select("div.col-xs-12 > span.label").text()
date_upload = parseDate(upDate)
val epName = element.select("div.col-xs-12 > div.anime-title > b").text()
name = epName
episode_number = 0F
}
}
// ============================ Video Links =============================
private val doodExtractor by lazy { DoodExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val vidstreamingExtractor by lazy { VidstreamingExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val scripts = document.select("div#divscript > script").filter { it ->
it.data().contains("function")
}
return scripts.parallelMap { elem ->
runCatching {
val data = elem.data().trimIndent()
val url = baseUrl + extractIframeSrc(data)
if (data.contains("vidstream()")) {
val iframeSrc = client.newCall(GET(url)).execute().asJsoup()
.select("iframe").attr("src")
extractVideo(iframeSrc)
} else if (data.contains("fm()")) {
val iframeSrc = client.newCall(GET(url)).execute().asJsoup()
.select("iframe").attr("src")
filemoonExtractor.videosFromUrl(url = iframeSrc, headers = headers)
} else {
emptyList()
}
}.getOrElse { emptyList() }
}.flatten().ifEmpty { throw Exception("Failed to fetch videos") }
}
private fun extractVideo(url: String): List<Video> {
val videos = vidstreamingExtractor.videosFromUrl(url, "Vidstreaming - ")
val request = GET(url)
val response = client.newCall(request).execute()
val document = response.asJsoup()
val servers = document.select("div#list-server-more > ul > li.linkserver")
return servers.parallelMap {
val link = it.attr("data-video")
when (it.text().lowercase()) {
"doodstream" -> doodExtractor.videosFromUrl(link)
"mp4upload" -> mp4uploadExtractor.videosFromUrl(link, headers)
else -> emptyList()
}
}.flatten().toMutableList().apply { addAll(videos) }.toList()
.ifEmpty { throw Exception("Failed to extract videos") }
}
override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
override fun videoFromElement(element: Element) = throw Exception("Not Used")
override fun videoListSelector() = throw Exception("Not Used")
// ============================= Utilities ==============================
private fun extractIframeSrc(scriptData: String): String {
val iframeRegex = "<iframe[^>]*>.*?</iframe>".toRegex()
val iframe = iframeRegex.find(scriptData)!!.value
val srcRegex = "(?<=src=\").*?(?=[\\*\"])".toRegex()
return srcRegex.find(iframe)!!.value
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server) },
),
).reversed()
}
private fun parseDate(date: String): Long {
val formatter = SimpleDateFormat("dd LLLL yyyy", Locale.ENGLISH)
val newDate = formatter.parse(date)
val dateStr = newDate?.let { DATE_FORMATTER.format(it) }
return runCatching { DATE_FORMATTER.parse(dateStr!!)?.time }
.getOrNull() ?: 0L
}
private fun parseStatus(statusBool: Boolean): Int {
return if (statusBool) {
SAnime.ONGOING
} else {
SAnime.COMPLETED
}
}
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Vidstreaming"
private val PREF_SERVER_ENTRIES = arrayOf(
"Vidstreaming",
"Filemoon",
"Doodstream",
"Mp4upload",
)
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRIES
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
}

View File

@ -0,0 +1,169 @@
package eu.kanade.tachiyomi.animeextension.en.animetake
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimeTakeFilters {
open class CheckBoxFilterList(name: String, pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) :
AnimeFilter.CheckBox(name, state)
inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.joinToString("&") { "$name[]=$it" }
}
internal class LetterFilter : CheckBoxFilterList("Letter", AnimeTakeFiltersData.LETTER)
internal class GenresFilter : CheckBoxFilterList("Genre", AnimeTakeFiltersData.GENRE)
internal class ScoreFilter : CheckBoxFilterList("Score", AnimeTakeFiltersData.SCORE)
internal class YearFilter : CheckBoxFilterList("Year", AnimeTakeFiltersData.YEAR)
internal class RatingFilter : CheckBoxFilterList("Rating", AnimeTakeFiltersData.RATING)
val FILTER_LIST
get() = AnimeFilterList(
AnimeFilter.Header("Note: Ignores search"),
LetterFilter(),
GenresFilter(),
ScoreFilter(),
YearFilter(),
RatingFilter(),
)
internal data class FilterSearchParams(
val letters: String = "",
val genres: String = "",
val score: String = "",
val years: String = "",
val ratings: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<LetterFilter>(AnimeTakeFiltersData.LETTER, "letters"),
filters.parseCheckbox<GenresFilter>(AnimeTakeFiltersData.GENRE, "genres"),
filters.parseCheckbox<ScoreFilter>(AnimeTakeFiltersData.SCORE, "score"),
filters.parseCheckbox<YearFilter>(AnimeTakeFiltersData.YEAR, "years"),
filters.parseCheckbox<RatingFilter>(AnimeTakeFiltersData.RATING, "ratings"),
)
}
private object AnimeTakeFiltersData {
val LETTER = ('A'..'Z').map {
Pair(it.toString(), it.toString())
}.toTypedArray()
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("Magic", "Magic"),
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 SCORE = arrayOf(
Pair("Masterpiece (9+)", "masterpiece"),
Pair("Fantastic (8+)", "fantastic"),
Pair("Very Good+(7+)", "verygood"),
Pair("Fine (6+)", "fine"),
Pair("Average (5+)", "average"),
Pair("Bad (4+)", "bad"),
Pair("Very Bad (3+)", "verybad"),
Pair("Unwatchable (2+)", "unwatchable"),
)
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"),
)
val RATING = arrayOf(
Pair("G - All Ages", "allages"),
Pair("PG 13 - Teens 13 and Older", "pg13"),
Pair("R - 17+, Violence & Profanity", "r17"),
Pair("R+ - Profanity & Mild Nudity", "rplus"),
)
}
}

View File

@ -0,0 +1,135 @@
package eu.kanade.tachiyomi.animeextension.en.animetake.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.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 uy.kohesive.injekt.injectLazy
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 by injectLazy()
fun videosFromUrl(serverUrl: String, prefix: String): List<Video> {
try {
val document = client.newCall(GET(serverUrl)).execute().asJsoup()
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 = serverUrl.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", serverUrl)
if (label == "auto") {
autoList.add(
Video(
fileURL,
label + qualitySuffix,
fileURL,
headers = videoHeaders,
),
)
} else {
videoList.add(
Video(
fileURL,
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)
}
}
}