Add extension: AnimeXin (#1255)

* Add extension

* sort
This commit is contained in:
Secozzi
2023-02-09 09:20:43 +01:00
committed by GitHub
parent d909e96ec7
commit 11a3bd2115
16 changed files with 1292 additions and 0 deletions

View File

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

View File

@ -0,0 +1,19 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'AnimeXin'
pkgNameSuffix = 'all.animexin'
extClass = '.AnimeXin'
extVersionCode = 1
libVersion = '13'
}
dependencies {
compileOnly libs.bundles.coroutines
implementation(project(':lib-okru-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@ -0,0 +1,333 @@
package eu.kanade.tachiyomi.animeextension.all.animexin
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DailymotionExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.FembedExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.StreamSBExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.YouTubeExtractor
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.okruextractor.OkruExtractor
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
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 java.text.SimpleDateFormat
import java.util.Locale
class AnimeXin : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeXin"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animexin.vip")!! }
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)
}
companion object {
private val DateFormatter by lazy {
SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
}
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/")
override fun popularAnimeSelector(): String = "div.wpop-weekly > ul > li"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.series").attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img").attr("src").substringBefore("?resize")
title = element.selectFirst("a.series:not(:has(img))").text()
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime/?page=$page&status=&type=&order=update")
override fun latestUpdatesSelector(): String = searchAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = searchAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = searchAnimeFromElement(element)
// =============================== 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 = AnimeXinFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
private fun searchAnimeRequest(page: Int, query: String, filters: AnimeXinFilters.FilterSearchParams): Request {
return if (query.isNotEmpty()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val multiChoose = mutableListOf<String>()
if (filters.genres.isNotEmpty()) multiChoose.add(filters.genres)
if (filters.seasons.isNotEmpty()) multiChoose.add(filters.seasons)
if (filters.studios.isNotEmpty()) multiChoose.add(filters.studios)
val multiString = if (multiChoose.isEmpty()) "" else multiChoose.joinToString("&") + "&"
GET("$baseUrl/anime/?page=$page&${multiString}status=${filters.status}&type=${filters.type}&sub=${filters.sub}&order=${filters.order}")
}
}
override fun searchAnimeSelector(): String = "div.listupd > article"
override fun searchAnimeNextPageSelector(): String = "div.hpage > a:contains(Next)"
override fun searchAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a").attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img").attr("src").substringBefore("?resize")
title = element.selectFirst("div.tt").text()
}
}
override fun getFilterList(): AnimeFilterList = AnimeXinFilters.filterList
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
title = document.selectFirst("h1.entry-title").text()
thumbnail_url = document.selectFirst("div.thumb > img").attr("src").substringBefore("?resize")
status = SAnime.COMPLETED
description = document.select("div[itemprop=description] p")?.let {
it.joinToString("\n\n") { t -> t.text() } +
"\n\n" +
document.select("div.info-content > div > span").joinToString("\n") { info ->
info.text().replace(":", ": ")
}
} ?: ""
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return document.select("div.eplister > ul > li").map { episodeElement ->
val numberText = episodeElement.selectFirst("div.epl-num").text()
val numberString = numberText.substringBefore(" ")
val episodeNumber = if (numberText.contains("part 2", true)) {
numberString.toFloatOrNull()?.plus(0.5F) ?: 0F
} else {
numberString.toFloatOrNull() ?: 0F
}
SEpisode.create().apply {
episode_number = episodeNumber
name = numberText
date_upload = parseDate(episodeElement.selectFirst("div.epl-date")?.text() ?: "")
setUrlWithoutDomain(episodeElement.selectFirst("a").attr("href").toHttpUrl().encodedPath)
}
}
}
override fun episodeListSelector(): String = throw Exception("Not Used")
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used")
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
videoList.addAll(
document.select("select.mirror > option[value~=.]").parallelMap { source ->
runCatching {
var decoded = Jsoup.parse(
String(Base64.decode(source.attr("value"), Base64.DEFAULT))
).select("iframe[src~=.]").attr("src")
if (!decoded.startsWith("http")) decoded = "https:$decoded"
val prefix = "${source.text()} - "
when {
decoded.contains("ok.ru") -> {
OkruExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
decoded.contains("sbhight") || decoded.contains("sbrity") || decoded.contains("sbembed.com") || decoded.contains("sbembed1.com") || decoded.contains("sbplay.org") ||
decoded.contains("sbvideo.net") || decoded.contains("streamsb.net") || decoded.contains("sbplay.one") ||
decoded.contains("cloudemb.com") || decoded.contains("playersb.com") || decoded.contains("tubesb.com") ||
decoded.contains("sbplay1.com") || decoded.contains("embedsb.com") || decoded.contains("watchsb.com") ||
decoded.contains("sbplay2.com") || decoded.contains("japopav.tv") || decoded.contains("viewsb.com") ||
decoded.contains("sbfast") || decoded.contains("sbfull.com") || decoded.contains("javplaya.com") ||
decoded.contains("ssbstream.net") || decoded.contains("p1ayerjavseen.com") || decoded.contains("sbthe.com") ||
decoded.contains("vidmovie.xyz") || decoded.contains("sbspeed.com") || decoded.contains("streamsss.net") ||
decoded.contains("sblanh.com") || decoded.contains("tvmshow.com") || decoded.contains("sbanh.com") ||
decoded.contains("streamovies.xyz") -> {
StreamSBExtractor(client).videosFromUrl(decoded, headers, prefix = prefix)
}
decoded.contains("dailymotion") -> {
DailymotionExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
decoded.contains("https://dood") -> {
DoodExtractor(client).videosFromUrl(decoded, quality = source.text())
}
decoded.contains("fembed") ||
decoded.contains("anime789.com") || decoded.contains("24hd.club") || decoded.contains("fembad.org") ||
decoded.contains("vcdn.io") || decoded.contains("sharinglink.club") || decoded.contains("moviemaniac.org") ||
decoded.contains("votrefiles.club") || decoded.contains("femoload.xyz") || decoded.contains("albavido.xyz") ||
decoded.contains("feurl.com") || decoded.contains("dailyplanet.pw") || decoded.contains("ncdnstm.com") ||
decoded.contains("jplayer.net") || decoded.contains("xstreamcdn.com") || decoded.contains("fembed-hd.com") ||
decoded.contains("gcloud.live") || decoded.contains("vcdnplay.com") || decoded.contains("superplayxyz.club") ||
decoded.contains("vidohd.com") || decoded.contains("vidsource.me") || decoded.contains("cinegrabber.com") ||
decoded.contains("votrefile.xyz") || decoded.contains("zidiplay.com") || decoded.contains("ndrama.xyz") ||
decoded.contains("fcdn.stream") || decoded.contains("mediashore.org") || decoded.contains("suzihaza.com") ||
decoded.contains("there.to") || decoded.contains("femax20.com") || decoded.contains("javstream.top") ||
decoded.contains("viplayer.cc") || decoded.contains("sexhd.co") || decoded.contains("fembed.net") ||
decoded.contains("mrdhan.com") || decoded.contains("votrefilms.xyz") || // decoded.contains("") ||
decoded.contains("embedsito.com") || decoded.contains("dutrag.com") || // decoded.contains("") ||
decoded.contains("youvideos.ru") || decoded.contains("streamm4u.club") || // decoded.contains("") ||
decoded.contains("moviepl.xyz") || decoded.contains("asianclub.tv") || // decoded.contains("") ||
decoded.contains("vidcloud.fun") || decoded.contains("fplayer.info") || // decoded.contains("") ||
decoded.contains("diasfem.com") || decoded.contains("javpoll.com") || decoded.contains("reeoov.tube") ||
decoded.contains("suzihaza.com") || decoded.contains("ezsubz.com") || decoded.contains("vidsrc.xyz") ||
decoded.contains("diampokusy.com") || decoded.contains("diampokusy.com") || decoded.contains("i18n.pw") ||
decoded.contains("vanfem.com") || decoded.contains("fembed9hd.com") || decoded.contains("votrefilms.xyz") || decoded.contains("watchjavnow.xyz")
-> {
val newUrl = decoded.replace("https://www.fembed.com", "https://vanfem.com")
FembedExtractor(client).videosFromUrl(newUrl, prefix = prefix)
}
decoded.contains("gdriveplayer") -> {
GdrivePlayerExtractor(client).videosFromUrl(decoded, name = source.text())
}
decoded.contains("youtube.com") -> {
YouTubeExtractor(client).videosFromUrl(decoded, prefix = prefix)
}
else -> null
}
}.getOrNull()
}.filterNotNull().flatten()
)
return videoList.sort()
}
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")!!
val language = preferences.getString("preferred_language", "All Sub")!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(language, true) },
)
).reversed()
}
private fun parseDate(dateStr: String): Long {
return runCatching { DateFormatter.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("animexin.vip")
entryValues = arrayOf("https://animexin.vip")
setDefaultValue("https://animexin.vip")
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 videoLangPref = ListPreference(screen.context).apply {
key = "preferred_language"
title = "Preferred Video Language"
entries = arrayOf("All Sub", "English", "Spanish", "Arabic", "German", "Indonesia", "Italian", "Polish", "Portuguese", "Thai", "Turkish")
entryValues = arrayOf("All Sub", "English", "Spanish", "Arabic", "German", "Indonesia", "Italian", "Polish", "Portuguese", "Thai", "Turkish")
setDefaultValue("All Sub")
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(domainPref)
screen.addPreference(videoQualityPref)
screen.addPreference(videoLangPref)
}
// From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
}

View File

@ -0,0 +1,221 @@
package eu.kanade.tachiyomi.animeextension.all.animexin
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimeXinFilters {
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.getFirst<R>() 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 GenresFilter : CheckBoxFilterList(
"Genres",
AnimeXinFiltersData.genres.map { CheckBoxVal(it.first, false) }
)
class SeasonsFilter : CheckBoxFilterList(
"Seasons",
AnimeXinFiltersData.seasons.map { CheckBoxVal(it.first, false) }
)
class StudiosFilter : CheckBoxFilterList(
"Studios",
AnimeXinFiltersData.studios.map { CheckBoxVal(it.first, false) }
)
class StatusFilter : QueryPartFilter("Status", AnimeXinFiltersData.status)
class TypeFilter : QueryPartFilter("Type", AnimeXinFiltersData.type)
class SubFilter : QueryPartFilter("Sub", AnimeXinFiltersData.sub)
class OrderFilter : QueryPartFilter("Order", AnimeXinFiltersData.order)
val filterList = AnimeFilterList(
GenresFilter(),
SeasonsFilter(),
StudiosFilter(),
AnimeFilter.Separator(),
StatusFilter(),
TypeFilter(),
SubFilter(),
OrderFilter(),
)
data class FilterSearchParams(
val genres: String = "",
val seasons: String = "",
val studios: String = "",
val status: String = "",
val type: String = "",
val sub: String = "",
val order: String = ""
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenresFilter>(AnimeXinFiltersData.genres, "genre"),
filters.parseCheckbox<SeasonsFilter>(AnimeXinFiltersData.seasons, "season"),
filters.parseCheckbox<StudiosFilter>(AnimeXinFiltersData.studios, "studio"),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<SubFilter>(),
filters.asQueryPart<OrderFilter>(),
)
}
private object AnimeXinFiltersData {
val all = Pair("All", "")
val genres = arrayOf(
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Comedy", "comedy"),
Pair("Demon", "demon"),
Pair("Drama", "drama"),
Pair("Fantasy", "fantasy"),
Pair("Game", "game"),
Pair("Historical", "historical"),
Pair("Isekai", "isekai"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mystery", "mystery"),
Pair("Over Power", "over-power"),
Pair("Reincarnation", "reincarnation"),
Pair("Romance", "romance"),
Pair("School", "school"),
Pair("Sci-fi", "sci-fi"),
Pair("Supernatural", "supernatural"),
Pair("War", "war")
)
val seasons = arrayOf(
Pair("4", "4"),
Pair("OVA", "ova"),
Pair("Season 1", "season-1"),
Pair("Season 2", "season-2"),
Pair("Season 3", "season-3"),
Pair("Season 4", "season-4"),
Pair("Season 5", "season-5"),
Pair("Season 7", "season-7"),
Pair("Season 8", "season-8"),
Pair("season1", "season1"),
Pair("Winter 2023", "winter-2023")
)
val studios = arrayOf(
Pair("2:10 Animatión", "210-animation"),
Pair("ASK Animation Studio", "ask-animation-studio"),
Pair("Axis Studios", "axis-studios"),
Pair("Azure Sea Studios", "azure-sea-studios"),
Pair("B.C May Pictures", "b-c-may-pictures"),
Pair("B.CMAY PICTURES", "b-cmay-pictures"),
Pair("BigFireBird Animation", "bigfirebird-animation"),
Pair("Bili Bili", "bili-bili"),
Pair("Bilibili", "bilibili"),
Pair("Build Dream", "build-dream"),
Pair("BYMENT", "byment"),
Pair("CG Year", "cg-year"),
Pair("CHOSEN", "chosen"),
Pair("Cloud Art", "cloud-art"),
Pair("Colored Pencil Animation", "colored-pencil-animation"),
Pair("D.ROCK-ART", "d-rock-art"),
Pair("Djinn Power", "djinn-power"),
Pair("Green Monster Team", "green-monster-team"),
Pair("Haoliners Animation", "haoliners-animation"),
Pair("He Zhou Culture", "he-zhou-culture"),
Pair("L²Studio", "l%c2%b2studio"),
Pair("Lingsanwu Animation", "lingsanwu-animation"),
Pair("Mili Pictures", "mili-pictures"),
Pair("Nice Boat Animation", "nice-boat-animation"),
Pair("Original Force", "original-force"),
Pair("Pb Animation Co. Ltd.", "pb-animation-co-ltd"),
Pair("Qing Xiang", "qing-xiang"),
Pair("Ruo Hong Culture", "ruo-hong-culture"),
Pair("Samsara Animation Studio", "samsara-animation-studio"),
Pair("Shanghai Foch Film Culture Investment", "shanghai-foch-film-culture-investment"),
Pair("Shanghai Motion Magic", "shanghai-motion-magic"),
Pair("Shenman Entertainment", "shenman-entertainment"),
Pair("Soyep", "soyep"),
Pair("soyep.cn", "soyep-cn"),
Pair("Sparkly Key Animation Studio", "sparkly-key-animation-studio"),
Pair("Tencent Penguin Pictures.", "tencent-penguin-pictures"),
Pair("Wan Wei Mao Donghua", "wan-wei-mao-donghua"),
Pair("Wawayu Animation", "wawayu-animation"),
Pair("Wonder Cat Animation", "wonder-cat-animation"),
Pair("Xing Yi Kai Chen", "xing-yi-kai-chen"),
Pair("Xuan Yuan", "xuan-yuan"),
Pair("Year Young Culture", "year-young-culture")
)
val status = arrayOf(
all,
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
Pair("Upcoming", "upcoming"),
Pair("Hiatus", "hiatus"),
)
val type = arrayOf(
all,
Pair("TV Series", "tv"),
Pair("OVA", "ova"),
Pair("Movie", "movie"),
Pair("Live Action", "live action"),
Pair("Special", "special"),
Pair("BD", "bd"),
Pair("ONA", "ona"),
Pair("Music", "music")
)
val sub = arrayOf(
all,
Pair("Sub", "sub"),
Pair("Dub", "dub"),
Pair("RAW", "raw"),
)
val order = arrayOf(
Pair("Default", ""),
Pair("A-Z", "title"),
Pair("Z-A", "titlereverse"),
Pair("Latest Update", "update"),
Pair("Latest Added", "latest"),
Pair("Popular", "popular"),
Pair("Rating", "rating"),
)
}
}

View File

@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
@Serializable
data class DailyQuality(
val qualities: Auto,
val subtitles: Subtitle? = null
) {
@Serializable
data class Auto(
val auto: List<Item>
) {
@Serializable
data class Item(
val type: String,
val url: String
)
}
@Serializable
data class Subtitle(
val data: Map<String, SubtitleObject>
) {
@Serializable
data class SubtitleObject(
val label: String,
val urls: List<String>
)
}
}
class DailymotionExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String): List<Video> {
val videoList = mutableListOf<Video>()
val htmlString = client.newCall(GET(url)).execute().body!!.string()
val internalData = htmlString.substringAfter("\"dmInternalData\":").substringBefore("</script>")
val ts = internalData.substringAfter("\"ts\":").substringBefore(",")
val v1st = internalData.substringAfter("\"v1st\":\"").substringBefore("\",")
val jsonUrl = "https://www.dailymotion.com/player/metadata/video/${url.toHttpUrl().encodedPath}?locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
val parsed = json.decodeFromString<DailyQuality>(
client.newCall(GET(jsonUrl))
.execute().body!!.string()
)
val subtitleList = mutableListOf<Track>()
if (parsed.subtitles != null) {
try {
subtitleList.addAll(
parsed.subtitles.data.map { k ->
Track(
k.value.urls.first(),
k.value.label
)
}
)
} catch (a: Exception) { }
}
val masterUrl = parsed.qualities.auto.first().url
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body!!.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",NAME") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
try {
videoList.add(Video(videoUrl, prefix + quality, videoUrl, subtitleTracks = subtitleList))
} catch (a: Exception) {
videoList.add(Video(videoUrl, prefix + quality, videoUrl))
}
}
return videoList
}
}

View File

@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class DoodExtractor(private val client: OkHttpClient) {
fun videoFromUrl(
url: String,
quality: String? = null,
redirect: Boolean = true
): Video? {
val newQuality = quality ?: "Doodstream" + if (redirect) " mirror" else ""
return try {
val response = client.newCall(GET(url)).execute()
val newUrl = if (redirect) response.request.url.toString() else url
val doodTld = newUrl.substringAfter("https://dood.").substringBefore("/")
val content = response.body!!.string()
val subtitleList = mutableListOf<Track>()
val subtitleRegex = """src:'//(srt[^']*?)',\s*label:'([^']*?)'""".toRegex()
try {
subtitleList.addAll(
subtitleRegex.findAll(content).map {
Track(
"https://" + it.groupValues[1],
it.groupValues[2]
)
}
)
} catch (a: Exception) { }
if (!content.contains("'/pass_md5/")) return null
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val expiry = System.currentTimeMillis()
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", newUrl)
)
).execute().body!!.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
try {
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld), subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld))
}
} catch (e: Exception) {
null
}
}
fun videosFromUrl(
url: String,
quality: String? = null,
redirect: Boolean = true
): List<Video> {
val video = videoFromUrl(url, quality, redirect)
return video?.let { listOf(it) } ?: emptyList<Video>()
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
private fun doodHeaders(tld: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://dood.$tld/")
}.build()
}

View File

@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
@Serializable
data class FembedResponse(
val success: Boolean,
val data: List<FembedVideo> = emptyList(),
val captions: List<Caption> = emptyList()
) {
@Serializable
data class FembedVideo(
val file: String,
val label: String
)
@Serializable
data class Caption(
val id: String,
val hash: String,
val language: String,
val extension: String,
)
}
class FembedExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, prefix: String = "", redirect: Boolean = false): List<Video> {
val videoApi = if (redirect) {
(
runCatching {
client.newCall(GET(url)).execute().request.url.toString()
.replace("/v/", "/api/source/")
}.getOrNull() ?: return emptyList<Video>()
)
} else {
url.replace("/v/", "/api/source/")
}
val body = runCatching {
client.newCall(POST(videoApi)).execute().body?.string().orEmpty()
}.getOrNull() ?: return emptyList()
val userId = client.newCall(GET(url)).execute().asJsoup()
.selectFirst("script:containsData(USER_ID)")
.data()
.substringAfter("USER_ID")
.substringAfter("'")
.substringBefore("'")
val jsonResponse = try { Json { ignoreUnknownKeys = true }.decodeFromString<FembedResponse>(body) } catch (e: Exception) { FembedResponse(false, emptyList(), emptyList()) }
return if (jsonResponse.success) {
val subtitleList = mutableListOf<Track>()
try {
subtitleList.addAll(
jsonResponse.captions.map {
Track(
"https://${url.toHttpUrl().host}/asset/userdata/$userId/caption/${it.hash}/${it.id}.${it.extension}",
it.language
)
}
)
} catch (a: Exception) { }
jsonResponse.data.map {
val quality = ("Fembed:${it.label}").let {
if (prefix.isNotBlank()) "$prefix $it"
else it
}
try {
Video(it.file, quality, it.file, subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(it.file, quality, it.file)
}
}
} else { emptyList<Video>() }
}
}

View File

@ -0,0 +1,154 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import android.util.Base64
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Track
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.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import org.jsoup.Jsoup
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class GdrivePlayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, name: String): List<Video> {
val headers = Headers.headersOf(
"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Host", "gdriveplayer.to",
"Referer", "https://animexin.vip/",
"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0"
)
val body = client.newCall(GET(url.replace(".me", ".to"), headers = headers)).execute()
.body!!.string()
val subtitleUrl = Jsoup.parse(body).selectFirst("div:contains(\\.srt)")
val subtitleList = mutableListOf<Track>()
if (subtitleUrl != null) {
try {
subtitleList.add(
Track(
"https://gdriveplayer.to/?subtitle=" + subtitleUrl.text(),
"Subtitles"
)
)
} catch (a: Exception) { }
}
val eval = JsUnpacker.unpackAndCombine(body)!!.replace("\\", "")
val json = Json.decodeFromString<JsonObject>(REGEX_DATAJSON.getFirst(eval))
val sojson = REGEX_SOJSON.getFirst(eval)
.split(Regex("\\D+"))
.joinToString("") {
Char(it.toInt()).toString()
}
val password = REGEX_PASSWORD.getFirst(sojson).toByteArray()
val decrypted = decryptAES(password, json)!!
val secondEval = JsUnpacker.unpackAndCombine(decrypted)!!.replace("\\", "")
return REGEX_VIDEOURL.findAll(secondEval)
.distinctBy { it.groupValues[2] } // remove duplicates by quality
.map {
val qualityStr = it.groupValues[2]
val quality = "$PLAYER_NAME ${qualityStr}p - $name"
val videoUrl = "https:" + it.groupValues[1] + "&res=$qualityStr"
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(videoUrl, quality, videoUrl)
}
}.toList()
}
private fun decryptAES(password: ByteArray, json: JsonObject): String? {
val salt = json["s"]!!.jsonPrimitive.content
val encodedCiphetext = json["ct"]!!.jsonPrimitive.content
val ciphertext = Base64.decode(encodedCiphetext, Base64.DEFAULT)
val (key, iv) = GenerateKeyAndIv(password, salt.decodeHex())
?: return null
val keySpec = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decryptedData = String(cipher.doFinal(ciphertext))
return decryptedData
}
// https://stackoverflow.com/a/41434590/8166854
private fun GenerateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.getDigestLength()
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
var generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
val result = listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
)
return result
} catch (e: DigestException) {
return null
}
}
private fun Regex.getFirst(item: String): String {
return find(item)?.groups?.elementAt(1)?.value!!
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
private const val PLAYER_NAME = "GDRIVE"
private val REGEX_DATAJSON = Regex("data='(\\S+?)'")
private val REGEX_PASSWORD = Regex("var pass = \"(\\S+?)\"")
private val REGEX_SOJSON = Regex("null,['|\"](\\w+)['|\"]")
private val REGEX_VIDEOURL = Regex("file\":\"(\\S+?)\".*?res=(\\d+)")
}
}

View File

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import eu.kanade.tachiyomi.animesource.model.Track
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 StreamSBExtractor(private val client: OkHttpClient) {
protected fun bytesToHex(bytes: ByteArray): String {
val hexArray = "0123456789ABCDEF".toCharArray()
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v = bytes[j].toInt() and 0xFF
hexChars[j * 2] = hexArray[v ushr 4]
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
}
return String(hexChars)
}
// animension, asianload and dramacool uses "common = false"
private fun fixUrl(url: String, common: Boolean): String {
val sbUrl = url.substringBefore("/e/")
val id = url.substringAfter("/e/")
.substringBefore("?")
.substringBefore(".html")
return if (common) {
val hexBytes = bytesToHex(id.toByteArray())
"$sbUrl/sources50/625a364258615242766475327c7c${hexBytes}7c7c4761574550654f7461566d347c7c73747265616d7362"
} else {
"$sbUrl/sources50/${bytesToHex("||$id||||streamsb".toByteArray())}/"
}
}
fun videosFromUrl(url: String, headers: Headers, prefix: String = "", suffix: String = "", common: Boolean = true): List<Video> {
val newHeaders = headers.newBuilder()
.set("referer", url)
.set("watchsb", "sbstream")
.set("authority", "embedsb.com")
.build()
return try {
val master = fixUrl(url, common)
val json = Json.decodeFromString<JsonObject>(
client.newCall(GET(master, newHeaders))
.execute().body!!.string()
)
val subtitleList = mutableListOf<Track>()
val subsList = json["stream_data"]!!.jsonObject["subs"]
if (subsList != null) {
try {
subtitleList.addAll(
subsList.jsonArray.map {
Track(
it.jsonObject["file"]!!.jsonPrimitive.content,
it.jsonObject["label"]!!.jsonPrimitive.content,
)
}
)
} catch (a: Exception) { }
}
val masterUrl = json["stream_data"]!!.jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(masterUrl, newHeaders))
.execute()
.body!!.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val quality = ("StreamSB:" + resolution).let {
if (prefix.isNotBlank()) "$prefix $it"
else it
}.let {
if (suffix.isNotBlank()) "$it $suffix"
else it
}
val videoUrl = it.substringAfter("\n").substringBefore("\n")
try {
Video(videoUrl, quality, videoUrl, headers = newHeaders, subtitleTracks = subtitleList)
} catch (a: Exception) {
Video(videoUrl, quality, videoUrl, headers = newHeaders)
}
}
} catch (e: Exception) {
emptyList<Video>()
}
}
fun videosFromDecryptedUrl(realUrl: String, headers: Headers, prefix: String = "", suffix: String = ""): List<Video> {
return try {
val json = Json.decodeFromString<JsonObject>(client.newCall(GET(realUrl, headers)).execute().body!!.string())
val masterUrl = json["stream_data"]!!.jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(masterUrl, headers)).execute().body!!.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val quality = ("StreamSB:$resolution").let {
if (prefix.isNotBlank()) "$prefix $it"
else it
}.let {
if (suffix.isNotBlank()) "$it $suffix"
else it
}
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl, headers = headers)
}
} catch (e: Exception) {
emptyList()
}
}
}

View File

@ -0,0 +1,182 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
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 kotlinx.serialization.json.long
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy
import java.text.CharacterIterator
import java.text.StringCharacterIterator
class YouTubeExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String): List<Video> {
// Ported from https://github.com/dermasmid/scrapetube/blob/master/scrapetube/scrapetube.py
// TODO: Make code prettier
// GET KEY
var ytcfgString = ""
val videoId = url.substringAfter("/embed/")
val document = client.newCall(
GET(url.replace("/embed/", "/watch?v="))
).execute().asJsoup()
for (element in document.select("script")) {
val scriptData = element.data()
if (scriptData.startsWith("(function() {window.ytplayer={};")) {
ytcfgString = scriptData
}
}
val apiKey = getKey(ytcfgString, "innertubeApiKey")
val playerUrl = "https://www.youtube.com/youtubei/v1/player?key=$apiKey&prettyPrint=false"
val body = """
{
"context":{
"client":{
"clientName":"ANDROID",
"clientVersion":"17.31.35",
"androidSdkVersion":30,
"userAgent":"com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip",
"hl":"en",
"timeZone":"UTC",
"utcOffsetMinutes":0
}
},
"videoId":"$videoId",
"params":"8AEB",
"playbackContext":{
"contentPlaybackContext":{
"html5Preference":"HTML5_PREF_WANTS"
}
},
"contentCheckOk":true,
"racyCheckOk":true
}
""".trimIndent().toRequestBody("application/json".toMediaType())
val headers = Headers.headersOf(
"X-YouTube-Client-Name", "3",
"X-YouTube-Client-Version", "17.31.35",
"Origin", "https://www.youtube.com",
"User-Agent", "com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip",
"content-type", "application/json"
)
val postResponse = client.newCall(
POST(playerUrl, headers = headers, body = body)
).execute()
val responseObject = json.decodeFromString<JsonObject>(postResponse.body!!.string())
val videoList = mutableListOf<Video>()
val formats = responseObject["streamingData"]!!
.jsonObject["adaptiveFormats"]!!
.jsonArray
val audioTracks = mutableListOf<Track>()
val subtitleTracks = mutableListOf<Track>()
// Get Audio
for (format in formats) {
if (format.jsonObject["mimeType"]!!.jsonPrimitive.content.startsWith("audio/webm")) {
try {
audioTracks.add(
Track(
format.jsonObject["url"]!!.jsonPrimitive.content,
format.jsonObject["audioQuality"]!!.jsonPrimitive.content +
" (${formatBits(format.jsonObject["averageBitrate"]!!.jsonPrimitive.long)}ps)"
)
)
} catch (a: Exception) { }
}
}
// Get Subtitles
if (responseObject.containsKey("captions")) {
val captionTracks = responseObject["captions"]!!
.jsonObject["playerCaptionsTracklistRenderer"]!!
.jsonObject["captionTracks"]!!
.jsonArray
for (caption in captionTracks) {
val captionJson = caption.jsonObject
try {
subtitleTracks.add(
Track(
// TODO: Would replacing srv3 with vtt work for every video?
captionJson["baseUrl"]!!.jsonPrimitive.content.replace("srv3", "vtt"),
captionJson["name"]!!.jsonObject["runs"]!!.jsonArray[0].jsonObject["text"]!!.jsonPrimitive.content
)
)
} catch (a: Exception) { }
}
}
// List formats
for (format in formats) {
val mimeType = format.jsonObject["mimeType"]!!.jsonPrimitive.content
if (mimeType.startsWith("video/mp4")) {
videoList.add(
try {
Video(
format.jsonObject["url"]!!.jsonPrimitive.content,
prefix + format.jsonObject["qualityLabel"]!!.jsonPrimitive.content +
" (${mimeType.substringAfter("codecs=\"").substringBefore("\"")})",
format.jsonObject["url"]!!.jsonPrimitive.content,
audioTracks = audioTracks,
subtitleTracks = subtitleTracks
)
} catch (a: Exception) {
Video(
format.jsonObject["url"]!!.jsonPrimitive.content,
prefix + format.jsonObject["qualityLabel"]!!.jsonPrimitive.content +
" (${mimeType.substringAfter("codecs=\"").substringBefore("\"")})",
format.jsonObject["url"]!!.jsonPrimitive.content
)
}
)
}
}
return videoList
}
fun getKey(string: String, key: String): String {
var pattern = Regex("\"$key\":\"(.*?)\"")
return pattern.find(string)?.groupValues?.get(1) ?: ""
}
@SuppressLint("DefaultLocale")
fun formatBits(bits: Long): String? {
var bits = bits
if (-1000 < bits && bits < 1000) {
return "${bits}b"
}
val ci: CharacterIterator = StringCharacterIterator("kMGTPE")
while (bits <= -999950 || bits >= 999950) {
bits /= 1000
ci.next()
}
return java.lang.String.format("%.0f%cb", bits / 1000.0, ci.current())
}
}