Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,16 @@
ext {
extName = 'AllAnime'
extClass = '.AllAnime'
extVersionCode = 31
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamlare-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:gogostream-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -0,0 +1,667 @@
package eu.kanade.tachiyomi.animeextension.en.allanime
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.allanime.extractors.AllAnimeExtractor
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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AllAnime"
override val baseUrl by lazy { preferences.baseUrl }
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val data = buildJsonObject {
putJsonObject("variables") {
put("type", "anime")
put("size", PAGE_SIZE)
put("dateRange", 7)
put("page", page)
}
put("query", POPULAR_QUERY)
}
return buildPost(data)
}
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<PopularResult>()
val animeList = parsed.data.queryPopular.recommendations.filter { it.anyCard != null }.map {
SAnime.create().apply {
title = when (preferences.titleStyle) {
"romaji" -> it.anyCard!!.name
"eng" -> it.anyCard!!.englishName ?: it.anyCard.name
else -> it.anyCard!!.nativeName ?: it.anyCard.name
}
thumbnail_url = it.anyCard.thumbnail
url = "${it.anyCard._id}<&sep>${it.anyCard.slugTime ?: ""}<&sep>${it.anyCard.name.slugify()}"
}
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val data = buildJsonObject {
putJsonObject("variables") {
putJsonObject("search") {
put("allowAdult", false)
put("allowUnknown", false)
}
put("limit", PAGE_SIZE)
put("page", page)
put("translationType", preferences.subPref)
put("countryOrigin", "ALL")
}
put("query", SEARCH_QUERY)
}
return buildPost(data)
}
override fun latestUpdatesParse(response: Response): AnimesPage = parseAnime(response)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filters = AllAnimeFilters.getSearchParameters(filters)
return if (query.isNotEmpty()) {
val data = buildJsonObject {
putJsonObject("variables") {
putJsonObject("search") {
put("query", query)
put("allowAdult", false)
put("allowUnknown", false)
}
put("limit", PAGE_SIZE)
put("page", page)
put("translationType", preferences.subPref)
put("countryOrigin", "ALL")
}
put("query", SEARCH_QUERY)
}
buildPost(data)
} else {
val data = buildJsonObject {
putJsonObject("variables") {
putJsonObject("search") {
put("allowAdult", false)
put("allowUnknown", false)
if (filters.season != "all") put("season", filters.season)
if (filters.releaseYear != "all") put("year", filters.releaseYear.toInt())
if (filters.genres != "all") {
put("genres", json.decodeFromString(filters.genres))
put("excludeGenres", buildJsonArray { })
}
if (filters.types != "all") put("types", json.decodeFromString(filters.types))
if (filters.sortBy != "update") put("sortBy", filters.sortBy)
}
put("limit", PAGE_SIZE)
put("page", page)
put("translationType", preferences.subPref)
put("countryOrigin", filters.origin)
}
put("query", SEARCH_QUERY)
}
buildPost(data)
}
}
override fun searchAnimeParse(response: Response): AnimesPage = parseAnime(response)
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AllAnimeFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
val data = buildJsonObject {
putJsonObject("variables") {
put("_id", anime.url.split("<&sep>").first())
}
put("query", DETAILS_QUERY)
}
return buildPost(data)
}
override fun getAnimeUrl(anime: SAnime): String {
val (id, time, slug) = anime.url.split("<&sep>")
val slugTime = if (time.isNotEmpty()) "-st-$time" else time
val siteUrl = preferences.siteUrl
return "$siteUrl/anime/$id/$slug$slugTime"
}
override fun animeDetailsParse(response: Response): SAnime {
val show = response.parseAs<DetailsResult>().data.show
return SAnime.create().apply {
genre = show.genres?.joinToString(separator = ", ") ?: ""
status = parseStatus(show.status)
author = show.studios?.firstOrNull()
description = buildString {
append(
Jsoup.parseBodyFragment(
show.description?.replace("<br>", "br2n") ?: "",
).text().replace("br2n", "\n"),
)
append("\n\n")
append("Type: ${show.type ?: "Unknown"}")
append("\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}")
append("\nScore: ${show.score ?: "-"}")
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val data = buildJsonObject {
putJsonObject("variables") {
put("_id", anime.url.split("<&sep>").first())
}
put("query", EPISODES_QUERY)
}
return buildPost(data)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val subPref = preferences.subPref
val medias = response.parseAs<SeriesResult>()
val episodesDetail = if (subPref == "sub") {
medias.data.show.availableEpisodesDetail.sub!!
} else {
medias.data.show.availableEpisodesDetail.dub!!
}
return episodesDetail.map { ep ->
val numName = ep.toIntOrNull() ?: (ep.toFloatOrNull() ?: "1")
SEpisode.create().apply {
episode_number = ep.toFloatOrNull() ?: 0F
name = "Episode $numName ($subPref)"
url = json.encodeToString(
buildJsonObject {
putJsonObject("variables") {
put("showId", medias.data.show._id)
put("translationType", subPref)
put("episodeString", ep)
}
put("query", STREAMS_QUERY)
},
)
}
}
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val payload = episode.url
.toRequestBody("application/json; charset=utf-8".toMediaType())
val siteUrl = preferences.siteUrl
val postHeaders = headers.newBuilder().apply {
add("Accept", "*/*")
add("Content-Length", payload.contentLength().toString())
add("Content-Type", payload.contentType().toString())
add("Host", baseUrl.toHttpUrl().host)
add("Origin", siteUrl)
add("Referer", "$baseUrl/")
}.build()
return POST("$baseUrl/api", headers = postHeaders, body = payload)
}
private val allAnimeExtractor by lazy { AllAnimeExtractor(client, headers, preferences.siteUrl) }
private val gogoStreamExtractor by lazy { GogoStreamExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val response = client.newCall(videoListRequest(episode)).await()
val videoJson = response.parseAs<EpisodeResult>()
val videoList = mutableListOf<Pair<Video, Float>>()
val serverList = mutableListOf<Server>()
val hosterSelection = preferences.getHosters
val altHosterSelection = preferences.getAltHosters
// list of alternative hosters
val mappings = listOf(
"vidstreaming" to listOf("vidstreaming", "https://gogo", "playgo1.cc", "playtaku"),
"doodstream" to listOf("dood"),
"okru" to listOf("ok.ru"),
"mp4upload" to listOf("mp4upload.com"),
"streamlare" to listOf("streamlare.com"),
)
videoJson.data.episode.sourceUrls.forEach { video ->
val videoUrl = video.sourceUrl.decryptSource()
val matchingMapping = mappings.firstOrNull { (altHoster, urlMatches) ->
altHosterSelection.contains(altHoster) && videoUrl.containsAny(urlMatches)
}
when {
videoUrl.startsWith("/apivtwo/") && INTERAL_HOSTER_NAMES.any {
Regex("""\b${it.lowercase()}\b""").find(video.sourceName.lowercase()) != null &&
hosterSelection.contains(it.lowercase())
} -> {
serverList.add(Server(videoUrl, "internal ${video.sourceName}", video.priority))
}
altHosterSelection.contains("player") && video.type == "player" -> {
serverList.add(Server(videoUrl, "player@${video.sourceName}", video.priority))
}
matchingMapping != null -> {
serverList.add(Server(videoUrl, matchingMapping.first, video.priority))
}
}
}
videoList.addAll(
serverList.parallelCatchingFlatMap { server ->
val sName = server.sourceName
when {
sName.startsWith("internal ") -> {
allAnimeExtractor.videoFromUrl(server.sourceUrl, server.sourceName)
}
sName.startsWith("player@") -> {
val endPoint = client.newCall(GET("${preferences.siteUrl}/getVersion")).await()
.parseAs<AllAnimeExtractor.VersionResponse>()
.episodeIframeHead
val videoHeaders = headers.newBuilder().apply {
add("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
add("Host", server.sourceUrl.toHttpUrl().host)
add("Referer", "$endPoint/")
}.build()
listOf(
Video(
server.sourceUrl,
"Original (player ${server.sourceName.substringAfter("player@")})",
server.sourceUrl,
headers = videoHeaders,
),
)
}
sName == "vidstreaming" -> {
gogoStreamExtractor.videosFromUrl(server.sourceUrl.replace(Regex("^//"), "https://"))
}
sName == "dood" -> {
doodExtractor.videosFromUrl(server.sourceUrl)
}
sName == "okru" -> {
okruExtractor.videosFromUrl(server.sourceUrl)
}
sName == "mp4upload" -> {
mp4uploadExtractor.videosFromUrl(server.sourceUrl, headers)
}
sName == "streamlare" -> {
streamlareExtractor.videosFromUrl(server.sourceUrl)
}
else -> emptyList()
}.let { it.map { v -> Pair(v, server.priority) } }
},
)
return prioritySort(videoList)
}
// ============================= Utilities ==============================
private fun String.decryptSource(): String {
return if (this.startsWith("-")) {
this.substringAfterLast('-').chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray().map {
(it.toInt() xor 56).toChar()
}.joinToString("")
} else {
this
}
}
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
val prefServer = preferences.prefServer
val quality = preferences.quality
val subPref = preferences.subPref
return pList.sortedWith(
compareBy(
{ if (prefServer == "site_default") it.second else it.first.quality.contains(prefServer, true) },
{ it.first.quality.contains(quality, true) },
{ it.first.quality.contains(subPref, true) },
),
).reversed().map { t -> t.first }
}
private fun buildPost(dataObject: JsonObject): Request {
val payload = json.encodeToString(dataObject)
.toRequestBody("application/json; charset=utf-8".toMediaType())
val siteUrl = preferences.siteUrl
val postHeaders = headers.newBuilder().apply {
add("Accept", "*/*")
add("Content-Length", payload.contentLength().toString())
add("Content-Type", payload.contentType().toString())
add("Host", baseUrl.toHttpUrl().host)
add("Origin", siteUrl)
add("Referer", "$baseUrl/")
}.build()
return POST("$baseUrl/api", headers = postHeaders, body = payload)
}
data class Server(
val sourceUrl: String,
val sourceName: String,
val priority: Float,
)
private fun parseStatus(string: String?): Int {
return when (string) {
"Releasing" -> SAnime.ONGOING
"Finished" -> SAnime.COMPLETED
"Not Yet Released" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun String.slugify(): String {
return this.replace("""[^a-zA-Z0-9]""".toRegex(), "-")
.replace("""-{2,}""".toRegex(), "-")
.lowercase()
}
private fun parseAnime(response: Response): AnimesPage {
val parsed = response.parseAs<SearchResult>()
val animeList = parsed.data.shows.edges.map { ani ->
SAnime.create().apply {
title = when (preferences.titleStyle) {
"romaji" -> ani.name
"eng" -> ani.englishName ?: ani.name
else -> ani.nativeName ?: ani.name
}
thumbnail_url = ani.thumbnail
url = "${ani._id}<&sep>${ani.slugTime ?: ""}<&sep>${ani.name.slugify()}"
}
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
private fun String.containsAny(keywords: List<String>): Boolean {
return keywords.any { this.contains(it) }
}
companion object {
private const val PAGE_SIZE = 26 // number of items to retrieve when calling API
private val INTERAL_HOSTER_NAMES = arrayOf(
"Default", "Ac", "Ak", "Kir", "Rab", "Luf-mp4",
"Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls",
)
private val ALT_HOSTER_NAMES = arrayOf(
"player",
"vidstreaming",
"okru",
"mp4upload",
"streamlare",
"doodstream",
)
private const val PREF_SITE_DOMAIN_KEY = "preferred_site_domain"
private const val PREF_SITE_DOMAIN_DEFAULT = "https://allanime.to"
private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_DEFAULT = "https://api.allanime.day"
private const val PREF_SERVER_KEY = "preferred_server"
private val PREF_SERVER_ENTRIES = arrayOf("Site Default") +
INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size) +
ALT_HOSTER_NAMES
private val PREF_SERVER_ENTRY_VALUES = arrayOf("site_default") +
INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size).map {
it.lowercase()
}.toTypedArray() +
ALT_HOSTER_NAMES
private const val PREF_SERVER_DEFAULT = "site_default"
private const val PREF_HOSTER_KEY = "hoster_selection"
private val PREF_HOSTER_ENTRY_VALUES = INTERAL_HOSTER_NAMES.map {
it.lowercase()
}.toTypedArray()
private val PREF_HOSTER_DEFAULT = setOf("default", "ac", "ak", "kir", "luf-mp4", "si-hls", "s-mp4", "ac-hls")
private const val PREF_ALT_HOSTER_KEY = "alt_hoster_selection"
private const val PREF_QUALITY_KEY = "preferred_quality"
private val PREF_QUALITY_ENTRIES = arrayOf(
"2160p",
"1440p",
"1080p",
"720p",
"480p",
"360p",
"240p",
"80p",
)
private val PREF_QUALITY_ENTRY_VALUES = PREF_QUALITY_ENTRIES.map {
it.substringBefore("p")
}.toTypedArray()
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
private const val PREF_TITLE_STYLE_DEFAULT = "romaji"
private const val PREF_SUB_KEY = "preferred_sub"
private const val PREF_SUB_DEFAULT = "sub"
}
// ============================== Settings ==============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SITE_DOMAIN_KEY
title = "Preferred domain for site (requires app restart)"
entries = arrayOf("allmanga.to")
entryValues = arrayOf("https://allmanga.to")
setDefaultValue(PREF_SITE_DOMAIN_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_DOMAIN_KEY
title = "Preferred domain (requires app restart)"
entries = arrayOf("api.allanime.day")
entryValues = arrayOf("https://api.allanime.day")
setDefaultValue(PREF_DOMAIN_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 Video Server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRY_VALUES
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)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = "Enable/Disable Hosts"
entries = INTERAL_HOSTER_NAMES
entryValues = PREF_HOSTER_ENTRY_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_ALT_HOSTER_KEY
title = "Enable/Disable Alternative Hosts"
entries = ALT_HOSTER_NAMES
entryValues = ALT_HOSTER_NAMES
setDefaultValue(ALT_HOSTER_NAMES.toSet())
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_VALUES
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_TITLE_STYLE_KEY
title = "Preferred Title Style"
entries = arrayOf("Romaji", "English", "Native")
entryValues = arrayOf("romaji", "eng", "native")
setDefaultValue(PREF_TITLE_STYLE_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_SUB_KEY
title = "Prefer subs or dubs?"
entries = arrayOf("Subs", "Dubs")
entryValues = arrayOf("sub", "dub")
setDefaultValue(PREF_SUB_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)
}
private val SharedPreferences.subPref
get() = getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
private val SharedPreferences.baseUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
private val SharedPreferences.siteUrl
get() = getString(PREF_SITE_DOMAIN_KEY, PREF_SITE_DOMAIN_DEFAULT)!!
private val SharedPreferences.titleStyle
get() = getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
private val SharedPreferences.quality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.prefServer
get() = getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
private val SharedPreferences.getHosters
get() = getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
private val SharedPreferences.getAltHosters
get() = getStringSet(PREF_ALT_HOSTER_KEY, ALT_HOSTER_NAMES.toSet())!!
}

View file

@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.animeextension.en.allanime
import kotlinx.serialization.Serializable
@Serializable
data class PopularResult(
val data: PopularResultData,
) {
@Serializable
data class PopularResultData(
val queryPopular: QueryPopularData,
) {
@Serializable
data class QueryPopularData(
val recommendations: List<Recommendation>,
) {
@Serializable
data class Recommendation(
val anyCard: Card? = null,
) {
@Serializable
data class Card(
val _id: String,
val name: String,
val thumbnail: String,
val englishName: String? = null,
val nativeName: String? = null,
val slugTime: String? = null,
)
}
}
}
}
@Serializable
data class SearchResult(
val data: SearchResultData,
) {
@Serializable
data class SearchResultData(
val shows: SearchResultShows,
) {
@Serializable
data class SearchResultShows(
val edges: List<SearchResultEdge>,
) {
@Serializable
data class SearchResultEdge(
val _id: String,
val name: String,
val thumbnail: String,
val englishName: String? = null,
val nativeName: String? = null,
val slugTime: String? = null,
)
}
}
}
@Serializable
data class DetailsResult(
val data: DataShow,
) {
@Serializable
data class DataShow(
val show: SeriesShows,
) {
@Serializable
data class SeriesShows(
val thumbnail: String,
val genres: List<String>? = null,
val studios: List<String>? = null,
val season: AirSeason? = null,
val status: String? = null,
val score: Float? = null,
val type: String? = null,
val description: String? = null,
) {
@Serializable
data class AirSeason(
val quarter: String,
val year: Int,
)
}
}
}
@Serializable
data class SeriesResult(
val data: DataShow,
) {
@Serializable
data class DataShow(
val show: SeriesShows,
) {
@Serializable
data class SeriesShows(
val _id: String,
val availableEpisodesDetail: AvailableEps,
) {
@Serializable
data class AvailableEps(
val sub: List<String>? = null,
val dub: List<String>? = null,
)
}
}
}
@Serializable
data class EpisodeResult(
val data: DataEpisode,
) {
@Serializable
data class DataEpisode(
val episode: Episode,
) {
@Serializable
data class Episode(
val sourceUrls: List<SourceUrl>,
) {
@Serializable
data class SourceUrl(
val sourceUrl: String,
val type: String,
val sourceName: String,
val priority: Float = 0F,
)
}
}
}

View file

@ -0,0 +1,231 @@
package eu.kanade.tachiyomi.animeextension.en.allanime
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AllAnimeFilters {
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>>,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("\",\"").let {
if (it.isBlank()) {
"all"
} else {
"[\"$it\"]"
}
}
}
class OriginFilter : QueryPartFilter("Origin", AllAnimeFiltersData.ORIGIN)
class SeasonFilter : QueryPartFilter("Season", AllAnimeFiltersData.SEASONS)
class ReleaseYearFilter : QueryPartFilter("Released at", AllAnimeFiltersData.YEARS)
class SortByFilter : QueryPartFilter("Sort By", AllAnimeFiltersData.SORT_BY)
class TypesFilter : CheckBoxFilterList(
"Types",
AllAnimeFiltersData.TYPES.map { CheckBoxVal(it.first, false) },
)
class GenresFilter : CheckBoxFilterList(
"Genres",
AllAnimeFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
)
val FILTER_LIST get() = AnimeFilterList(
OriginFilter(),
SeasonFilter(),
ReleaseYearFilter(),
SortByFilter(),
AnimeFilter.Separator(),
TypesFilter(),
GenresFilter(),
)
data class FilterSearchParams(
val origin: String = "",
val season: String = "",
val releaseYear: String = "",
val sortBy: String = "",
val types: String = "",
val genres: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<OriginFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<ReleaseYearFilter>(),
filters.asQueryPart<SortByFilter>(),
filters.parseCheckbox<TypesFilter>(AllAnimeFiltersData.TYPES),
filters.parseCheckbox<GenresFilter>(AllAnimeFiltersData.GENRES),
)
}
private object AllAnimeFiltersData {
val ALL = Pair("All", "all")
val ORIGIN = arrayOf(
Pair("All", "ALL"),
Pair("Japan", "JP"),
Pair("China", "CN"),
Pair("Korea", "KR"),
)
val SEASONS = arrayOf(
ALL,
Pair("Winter", "Winter"),
Pair("Spring", "Spring"),
Pair("Summer", "Summer"),
Pair("Fall", "Fall"),
)
val YEARS = arrayOf(
ALL,
Pair("2024", "2024"),
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("1999", "1999"),
Pair("1998", "1998"),
Pair("1997", "1997"),
Pair("1996", "1996"),
Pair("1995", "1995"),
Pair("1994", "1994"),
Pair("1993", "1993"),
Pair("1992", "1992"),
Pair("1991", "1991"),
Pair("1990", "1990"),
Pair("1989", "1989"),
Pair("1988", "1988"),
Pair("1987", "1987"),
Pair("1986", "1986"),
Pair("1985", "1985"),
Pair("1984", "1984"),
Pair("1983", "1983"),
Pair("1982", "1982"),
Pair("1981", "1981"),
Pair("1980", "1980"),
Pair("1979", "1979"),
Pair("1978", "1978"),
Pair("1977", "1977"),
Pair("1976", "1976"),
Pair("1975", "1975"),
)
val SORT_BY = arrayOf(
Pair("Update", "update"),
Pair("Name Asc", "Name_ASC"),
Pair("Name Desc", "Name_DESC"),
Pair("Ratings", "Top"),
)
val TYPES = arrayOf(
Pair("Movie", "Movie"),
Pair("ONA", "ONA"),
Pair("OVA", "OVA"),
Pair("Special", "Special"),
Pair("TV", "TV"),
Pair("Unknown", "Unknown"),
)
val GENRES = arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Cars", "Cars"),
Pair("Comedy", "Comedy"),
Pair("Dementia", "Dementia"),
Pair("Demons", "Demons"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Game", "Game"),
Pair("Harem", "Harem"),
Pair("Historical", "Historical"),
Pair("Horror", "Horror"),
Pair("Isekai", "Isekai"),
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("Parody", "Parody"),
Pair("Police", "Police"),
Pair("Psychological", "Psychological"),
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("Super Power", "Super Power"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
Pair("Unknown", "Unknown"),
Pair("Vampire", "Vampire"),
Pair("Yaoi", "Yaoi"),
Pair("Yuri", "Yuri"),
)
}
}

View file

@ -0,0 +1,119 @@
package eu.kanade.tachiyomi.animeextension.en.allanime
fun buildQuery(queryAction: () -> String): String {
return queryAction()
.trimIndent()
.replace("%", "$")
}
val POPULAR_QUERY: String = buildQuery {
"""
query(
%type: VaildPopularTypeEnumType!
%size: Int!
%page: Int
%dateRange: Int
) {
queryPopular(
type: %type
size: %size
dateRange: %dateRange
page: %page
) {
total
recommendations {
anyCard {
_id
name
thumbnail
englishName
nativeName
slugTime
}
}
}
}
"""
}
val SEARCH_QUERY: String = buildQuery {
"""
query(
%search: SearchInput
%limit: Int
%page: Int
%translationType: VaildTranslationTypeEnumType
%countryOrigin: VaildCountryOriginEnumType
) {
shows(
search: %search
limit: %limit
page: %page
translationType: %translationType
countryOrigin: %countryOrigin
) {
pageInfo {
total
}
edges {
_id
name
thumbnail
englishName
nativeName
slugTime
}
}
}
"""
}
val DETAILS_QUERY = buildQuery {
"""
query (%_id: String!) {
show(
_id: %_id
) {
thumbnail
description
type
season
score
genres
status
studios
}
}
"""
}
val EPISODES_QUERY = buildQuery {
"""
query (%_id: String!) {
show(
_id: %_id
) {
_id
availableEpisodesDetail
}
}
"""
}
val STREAMS_QUERY = buildQuery {
"""
query(
%showId: String!,
%translationType: VaildTranslationTypeEnumType!,
%episodeString: String!
) {
episode(
showId: %showId
translationType: %translationType
episodeString: %episodeString
) {
sourceUrls
}
}
"""
}

View file

@ -0,0 +1,252 @@
package eu.kanade.tachiyomi.animeextension.en.allanime.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.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.Locale
class AllAnimeExtractor(private val client: OkHttpClient, private val headers: Headers, private val siteUrl: String) {
private val json: Json by injectLazy()
private fun bytesIntoHumanReadable(bytes: Long): String {
val kilobyte: Long = 1000
val megabyte = kilobyte * 1000
val gigabyte = megabyte * 1000
val terabyte = gigabyte * 1000
return if (bytes in 0 until kilobyte) {
"$bytes b/s"
} else if (bytes in kilobyte until megabyte) {
(bytes / kilobyte).toString() + " kb/s"
} else if (bytes in megabyte until gigabyte) {
(bytes / megabyte).toString() + " mb/s"
} else if (bytes in gigabyte until terabyte) {
(bytes / gigabyte).toString() + " gb/s"
} else if (bytes >= terabyte) {
(bytes / terabyte).toString() + " tb/s"
} else {
"$bytes bits/s"
}
}
fun videoFromUrl(url: String, name: String): List<Video> {
val videoList = mutableListOf<Video>()
val endPoint = json.decodeFromString<VersionResponse>(
client.newCall(GET("$siteUrl/getVersion")).execute().body.string(),
).episodeIframeHead
val resp = client.newCall(
GET(endPoint + url.replace("/clock?", "/clock.json?")),
).execute()
if (resp.code != 200) {
return emptyList()
}
val body = resp.body.string()
val linkJson = json.decodeFromString<VideoLink>(body)
for (link in linkJson.links) {
val subtitles = mutableListOf<Track>()
if (!link.subtitles.isNullOrEmpty()) {
subtitles.addAll(
link.subtitles.map { sub ->
val label = if (sub.label != null) {
" - ${sub.label}"
} else {
""
}
Track(sub.src, Locale(sub.lang).displayLanguage + label)
},
)
}
if (link.mp4 == true) {
videoList.add(
Video(
link.link,
"Original ($name - ${link.resolutionStr})",
link.link,
subtitleTracks = subtitles,
),
)
} else if (link.hls == true) {
val newClient = OkHttpClient()
val masterHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", link.link.toHttpUrl().host)
.add("Origin", endPoint)
.add("Referer", "$endPoint/")
.build()
val resp = runCatching {
newClient.newCall(
GET(link.link, headers = masterHeaders),
).execute()
}.getOrNull()
if (resp != null && resp.code == 200) {
val masterPlaylist = resp.body.string()
val audioList = mutableListOf<Track>()
if (masterPlaylist.contains("#EXT-X-MEDIA:TYPE=AUDIO")) {
val audioInfo = masterPlaylist.substringAfter("#EXT-X-MEDIA:TYPE=AUDIO")
.substringBefore("\n")
val language = audioInfo.substringAfter("NAME=\"").substringBefore("\"")
val url = audioInfo.substringAfter("URI=\"").substringBefore("\"")
audioList.add(
Track(url, language),
)
}
if (!masterPlaylist.contains("#EXT-X-STREAM-INF:")) {
return if (audioList.isEmpty()) {
listOf(Video(link.link, "$name - ${link.resolutionStr}", link.link, subtitleTracks = subtitles, headers = masterHeaders))
} else {
listOf(Video(link.link, "$name - ${link.resolutionStr}", link.link, subtitleTracks = subtitles, audioTracks = audioList, headers = masterHeaders))
}
}
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
.forEach {
val bandwidth = if (it.contains("AVERAGE-BANDWIDTH")) {
" " + bytesIntoHumanReadable(it.substringAfter("AVERAGE-BANDWIDTH=").substringBefore(",").toLong())
} else {
""
}
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p$bandwidth ($name - ${link.resolutionStr})"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
if (!videoUrl.startsWith("http")) {
videoUrl = resp.request.url.toString().substringBeforeLast("/") + "/$videoUrl"
}
val plHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", videoUrl.toHttpUrl().host)
.add("Origin", endPoint)
.add("Referer", "$endPoint/")
.build()
if (audioList.isEmpty()) {
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitles, headers = plHeaders))
} else {
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitles, audioTracks = audioList, headers = plHeaders))
}
}
}
} else if (link.crIframe == true) {
link.portData!!.streams.forEach {
if (it.format == "adaptive_dash") {
videoList.add(
Video(
it.url,
"Original (AC - Dash${if (it.hardsub_lang.isEmpty()) "" else " - Hardsub: ${it.hardsub_lang}"})",
it.url,
subtitleTracks = subtitles,
),
)
} else if (it.format == "adaptive_hls") {
val resp = runCatching {
client.newCall(
GET(it.url, headers = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")),
).execute()
}.getOrNull()
if (resp != null && resp.code == 200) {
val masterPlaylist = resp.body.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
.forEach { t ->
val quality = t.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p (AC - HLS${if (it.hardsub_lang.isEmpty()) "" else " - Hardsub: ${it.hardsub_lang}"})"
var videoUrl = t.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitles))
}
}
}
}
} else if (link.dash == true) {
val audioList = link.rawUrls?.audios?.map {
Track(it.url, bytesIntoHumanReadable(it.bandwidth))
}
val videos = link.rawUrls?.vids?.map {
if (audioList == null) {
Video(it.url, "$name - ${it.height} ${bytesIntoHumanReadable(it.bandwidth)}", it.url, subtitleTracks = subtitles)
} else {
Video(it.url, "$name - ${it.height} ${bytesIntoHumanReadable(it.bandwidth)}", it.url, audioTracks = audioList, subtitleTracks = subtitles)
}
}
if (videos != null) {
videoList.addAll(videos)
}
}
}
return videoList
}
@Serializable
data class VersionResponse(
val episodeIframeHead: String,
)
@Serializable
data class VideoLink(
val links: List<Link>,
) {
@Serializable
data class Link(
val link: String,
val hls: Boolean? = null,
val mp4: Boolean? = null,
val dash: Boolean? = null,
val crIframe: Boolean? = null,
val resolutionStr: String,
val subtitles: List<Subtitles>? = null,
val rawUrls: RawUrl? = null,
val portData: Stream? = null,
) {
@Serializable
data class Subtitles(
val lang: String,
val src: String,
val label: String? = null,
)
@Serializable
data class Stream(
val streams: List<StreamObject>,
) {
@Serializable
data class StreamObject(
val format: String,
val url: String,
val audio_lang: String,
val hardsub_lang: String,
)
}
@Serializable
data class RawUrl(
val vids: List<DashStreamObject>? = null,
val audios: List<DashStreamObject>? = null,
) {
@Serializable
data class DashStreamObject(
val bandwidth: Long,
val height: Int,
val url: String,
)
}
}
}
}

View file

@ -0,0 +1,17 @@
ext {
extName = 'AllAnimeChi'
extClass = '.AllAnimeChi'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:gogostream-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamlare-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:playlist-utils'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View file

@ -0,0 +1,680 @@
package eu.kanade.tachiyomi.animeextension.en.allanimechi
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors.AllAnimeExtractor
import eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors.InternalExtractor
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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Locale
class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AllAnimeChi"
override val baseUrl = "aHR0cHM6Ly9hY2FwaS5hbGxhbmltZS5kYXk=".decodeBase64()
private val siteUrl = "aHR0cHM6Ly9hbGxhbmltZS50bw==".decodeBase64()
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val apiHeaders = Headers.Builder().apply {
add("app-version", "android_c-253")
add("content-type", "application/json; charset=UTF-8")
add("from-app", "YW5pbWVjaGlja2Vu".decodeBase64())
add("host", baseUrl.toHttpUrl().host)
add("platformstr", "android_c")
add("Referer", "$siteUrl/")
add("user-agent", "Dart/2.19 (dart:io)")
}.build()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val variables = buildJsonObject {
put("type", "anime")
put("size", PAGE_SIZE)
put("dateRange", 7)
put("page", page)
put("allowAdult", false)
put("allowUnknown", false)
put("denyEcchi", false)
}.encode()
val extensions = buildJsonObject {
putJsonObject("persistedQuery") {
put("version", 1)
put("sha256Hash", POPULAR_HASH)
}
}.encode()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addQueryParameter("variables", variables)
addQueryParameter("extensions", extensions)
}.build().toString().replace("%3A", ":")
return GET(url, apiHeaders)
}
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<PopularResult>()
val animeList = parsed.data.queryPopular.recommendations.mapNotNull {
it.anyCard?.toSAnime(preferences.titleStyle)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val variables = buildJsonObject {
putJsonObject("search") {
put("allowAdult", false)
put("allowUnknown", false)
put("denyEcchi", false)
}
put("translationType", preferences.subPref)
put("limit", PAGE_SIZE)
put("page", page)
put("countryOrigin", "ALL")
}.encode()
val extensions = buildJsonObject {
putJsonObject("persistedQuery") {
put("version", 1)
put("sha256Hash", LATEST_HASH)
}
}.encode()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addQueryParameter("variables", variables)
addQueryParameter("extensions", extensions)
}.build().toString().replace("%3A", ":")
return GET(url, apiHeaders)
}
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.parseAs<SearchResult>()
val animeList = parsed.data.shows.edges.map {
it.toSAnime(preferences.titleStyle)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AllAnimeChiFilters.getSearchParameters(filters)
val variables = buildJsonObject {
putJsonObject("search") {
if (query.isBlank()) {
if (params.genres.isNotEmpty()) {
putJsonArray("genres") {
params.genres.forEach {
add(it)
}
}
}
if (params.type != "all") {
putJsonArray("types") {
add(params.type)
}
}
if (params.season != "all") {
put("season", params.season)
}
if (params.releaseYear != "all") {
put("year", params.releaseYear.toInt())
}
if (params.episodeCount != "all") {
val (start, end) = params.episodeCount.split("-")
if (start.isNotBlank()) put("epRangeStart", start.toInt())
if (end.isNotBlank()) put("epRangeEnd", end.toInt())
}
} else {
put("query", query)
}
put("sortBy", "Latest_Update")
put("allowAdult", false)
put("allowUnknown", false)
put("denyEcchi", false)
}
put("translationType", preferences.subPref)
put("limit", PAGE_SIZE)
put("page", page)
put("countryOrigin", params.origin)
}.encode()
val extensions = buildJsonObject {
putJsonObject("persistedQuery") {
put("version", 1)
put("sha256Hash", LATEST_HASH)
}
}.encode()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addQueryParameter("variables", variables)
addQueryParameter("extensions", extensions)
}.build().toString().replace("%3A", ":")
return GET(url, apiHeaders)
}
override fun searchAnimeParse(response: Response): AnimesPage = latestUpdatesParse(response)
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AllAnimeChiFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
val variables = buildJsonObject {
put("_id", anime.url)
}.encode()
val extensions = buildJsonObject {
putJsonObject("persistedQuery") {
put("version", 1)
put("sha256Hash", DETAILS_HASH)
}
}.encode()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addQueryParameter("variables", variables)
addQueryParameter("extensions", extensions)
}.build().toString()
return GET(url, apiHeaders)
}
override fun getAnimeUrl(anime: SAnime): String {
return "data:text/plain,This%20extension%20does%20not%20have%20a%20website."
}
override fun animeDetailsParse(response: Response): SAnime {
val show = response.parseAs<DetailsResult>().data.show
return SAnime.create().apply {
genre = show.genres?.joinToString(separator = ", ") ?: ""
status = parseStatus(show.status)
author = show.studios?.firstOrNull()
description = buildString {
append(
Jsoup.parseBodyFragment(
show.description?.replace("<br>", "br2n") ?: "",
).text().replace("br2n", "\n"),
)
append("\n\n")
append("Type: ${show.type ?: "Unknown"}")
append("\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}")
append("\nScore: ${show.score ?: "-"}")
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
override fun episodeListParse(response: Response): List<SEpisode> {
val media = response.parseAs<SeriesResult>()
val subPref = preferences.subPref
val episodesDetail = if (subPref == "sub") {
media.data.show.availableEpisodesDetail.sub!!
} else {
media.data.show.availableEpisodesDetail.dub!!
}
return episodesDetail.map { ep ->
val numName = ep.toIntOrNull() ?: (ep.toFloatOrNull() ?: "1")
SEpisode.create().apply {
episode_number = ep.toFloatOrNull() ?: 0F
name = "Episode $numName ($subPref)"
url = buildJsonObject {
put("showId", media.data.show._id)
put("translationType", subPref)
put("episodeString", ep)
}.encode()
}
}
}
// ============================ Video Links =============================
private val internalExtractor by lazy { InternalExtractor(client, apiHeaders, headers) }
override fun videoListRequest(episode: SEpisode): Request {
val variables = episode.url
val extensions = buildJsonObject {
putJsonObject("persistedQuery") {
put("version", 1)
put("sha256Hash", STREAMS_HASH)
}
}.encode()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addQueryParameter("variables", variables)
addQueryParameter("extensions", extensions)
}.build().toString()
return GET(url, apiHeaders)
}
override fun videoListParse(response: Response): List<Video> {
val videoJson = response.parseAs<EpisodeResult>()
val hosterBlackList = preferences.getHosterBlacklist
val altHosterBlackList = preferences.getAltHosterBlacklist
val useHosterNames = preferences.useHosterName
val serverList = videoJson.data.episode.sourceUrls.mapNotNull { video ->
when {
// "Internal" sources
video.sourceUrl.startsWith("/apivtwo/") && !hosterBlackList.any {
Regex("""\b${it.lowercase()}\b""").find(video.sourceName.lowercase()) != null
} -> {
Server(video.sourceUrl, video.sourceName, video.priority, "internal")
}
// Player, direct video links.
video.type == "player" && !altHosterBlackList.contains(video.sourceName, true) -> {
Server(video.sourceUrl, video.sourceName, video.priority, "player")
}
// External video players
!altHosterBlackList.contains(video.sourceName, true) && !video.sourceUrl.startsWith("/apivtwo/") -> {
Server(video.sourceUrl, video.sourceName, video.priority, "external")
}
else -> null
}
}
return prioritySort(
serverList.parallelCatchingFlatMapBlocking { getVideoFromServer(it, useHosterNames) },
)
}
private fun getVideoFromServer(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
return when (server.type) {
"player" -> getFromPlayer(server, useHosterName)
"internal" -> internalExtractor.videosFromServer(server, useHosterName, removeRaw = preferences.removeRaw)
"external" -> getFromExternal(server, useHosterName)
else -> emptyList()
}
}
private fun getFromPlayer(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
val name = if (useHosterName) {
getHostName(server.sourceUrl, server.sourceName)
} else {
server.sourceName
}
val videoHeaders = headers.newBuilder().apply {
add("origin", siteUrl)
add("referer", "$siteUrl/")
}.build()
val video = Video(server.sourceUrl, name, server.sourceUrl, headers = videoHeaders)
return listOf(
Pair(video, server.priority),
)
}
private val okruExtractor by lazy { OkruExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val gogoExtractor by lazy { GogoStreamExtractor(client) }
private val allanimeExtractor by lazy { AllAnimeExtractor(client, headers) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private fun getFromExternal(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
val url = server.sourceUrl.replace(Regex("""^//"""), "https://")
val prefix = if (useHosterName) {
"${getHostName(url, server.sourceName)} - "
} else {
"${server.sourceName} - "
}
val videoList = when {
url.startsWith("https://ok") -> okruExtractor.videosFromUrl(url, prefix = prefix)
url.startsWith("https://filemoon") -> filemoonExtractor.videosFromUrl(url, prefix = prefix)
url.startsWith("https://streamlare") -> streamlareExtractor.videosFromUrl(url, prefix = prefix)
url.startsWith("https://mp4upload") -> mp4uploadExtractor.videosFromUrl(url, headers, prefix = prefix)
server.sourceName.equals("Vid-mp4", true) -> gogoExtractor.videosFromUrl(url)
url.startsWith("https://allanime") -> allanimeExtractor.videosFromUrl(url, prefix = prefix)
url.startsWith("https://streamwish") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { q -> prefix + q })
else -> emptyList()
}
return videoList.map { Pair(it, server.priority) }
}
// ============================= Utilities ==============================
private fun getHostName(host: String, fallback: String): String {
return host.toHttpUrlOrNull()?.host?.split(".")?.let {
it.getOrNull(it.size - 2)?.replaceFirstChar { c ->
if (c.isLowerCase()) c.titlecase(Locale.ROOT) else c.toString()
}
} ?: fallback
}
private fun String.decodeBase64(): String {
return String(Base64.decode(this, Base64.DEFAULT))
}
private fun JsonObject.encode(): String {
return json.encodeToString(this)
}
data class Server(
val sourceUrl: String,
val sourceName: String,
val priority: Float,
val type: String,
)
fun Set<String>.contains(element: String, ignoreCase: Boolean): Boolean {
return this.any { it.equals(element, ignoreCase = ignoreCase) }
}
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
val prefServer = preferences.prefServer
val quality = preferences.quality
val subPref = preferences.subPref
return pList.sortedWith(
compareBy(
{ if (prefServer == "site_default") it.second else it.first.quality.contains(prefServer, true) },
{ it.first.quality.contains(quality, true) },
{ it.first.quality.contains(subPref, true) },
),
).reversed().map { t -> t.first }
}
private fun parseStatus(string: String?): Int {
return when (string) {
"Releasing" -> SAnime.ONGOING
"Finished" -> SAnime.COMPLETED
"Not Yet Released" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
companion object {
private const val PAGE_SIZE = 30 // number of items to retrieve when calling API
private const val POPULAR_HASH = "31a117653812a2547fd981632e8c99fa8bf8a75c4ef1a77a1567ef1741a7ab9c"
private const val LATEST_HASH = "e42a4466d984b2c0a2cecae5dd13aa68867f634b16ee0f17b380047d14482406"
private const val DETAILS_HASH = "bb263f91e5bdd048c1c978f324613aeccdfe2cbc694a419466a31edb58c0cc0b"
private const val STREAMS_HASH = "5e7e17cdd0166af5a2d8f43133d9ce3ce9253d1fdb5160a0cfd515564f98d061"
private val INTERNAL_HOSTER_NAMES = arrayOf(
"Default 0 (Cr/vrv)",
"Default",
"Luf-mp4 (Gogo)",
"S-mp4",
"Sak",
"Uv-mp4",
)
private val EXTERNAL_HOSTER_NAMES = arrayOf(
"AK",
"Fm-Hls",
"Mp4",
"Ok",
"Sw",
"Vid-mp4 (Vidstreaming)",
"Yt-mp4",
)
private const val PREF_HOSTER_BLACKLIST_KEY = "pref_hoster_blacklist"
private val PREF_HOSTER_BLACKLIST_ENTRY_VALUES = INTERNAL_HOSTER_NAMES.map {
it.lowercase().substringBefore(" (")
}.toTypedArray()
private const val PREF_ALT_HOSTER_BLACKLIST_KEY = "pref_alt_hoster_blacklist"
private val PREF_ALT_HOSTER_BLACKLIST_ENTRY_VALUES = EXTERNAL_HOSTER_NAMES.map {
it.lowercase().substringBefore(" (")
}.toTypedArray()
private const val PREF_REMOVE_RAW_KEY = "pref_remove_raw"
private const val PREF_REMOVE_RAW_DEFAULT = true
// Names as they appear in video list
private val HOSTER_NAMES = arrayOf(
"AK", "Crunchyroll", "Default", "Fm-Hls", "Luf-mp4",
"Mp4", "Ok", "S-mp4", "Sak", "Sw", "Uv-mp4",
"Vidstreaming", "Vrv", "Yt-mp4",
)
private const val PREF_SERVER_KEY = "preferred_server"
private val PREF_SERVER_ENTRIES = arrayOf("Site Default") +
HOSTER_NAMES
private val PREF_SERVER_ENTRY_VALUES = arrayOf("site_default") +
HOSTER_NAMES.map { it.lowercase() }
private const val PREF_SERVER_DEFAULT = "site_default"
private const val PREF_QUALITY_KEY = "preferred_quality"
private val PREF_QUALITY_ENTRIES = arrayOf(
"2160p",
"1440p",
"1080p",
"720p",
"480p",
"360p",
"240p",
"80p",
)
private val PREF_QUALITY_ENTRY_VALUES = PREF_QUALITY_ENTRIES.map {
it.substringBefore("p")
}.toTypedArray()
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
private const val PREF_TITLE_STYLE_DEFAULT = "romaji"
private const val PREF_SUB_KEY = "preferred_sub"
private const val PREF_SUB_DEFAULT = "sub"
private const val PREF_USE_HOSTER_NAMES_KEY = "use_host_prefix"
private const val PREF_USE_HOSTER_NAMES_DEFAULT = false
}
// ============================== Settings ==============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred Video Server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRY_VALUES
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)
MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_BLACKLIST_KEY
title = "Internal hoster blacklist"
entries = INTERNAL_HOSTER_NAMES
entryValues = PREF_HOSTER_BLACKLIST_ENTRY_VALUES
setDefaultValue(emptySet<String>())
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_ALT_HOSTER_BLACKLIST_KEY
title = "External hoster blacklist"
entries = EXTERNAL_HOSTER_NAMES
entryValues = PREF_ALT_HOSTER_BLACKLIST_ENTRY_VALUES
setDefaultValue(emptySet<String>())
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_VALUES
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_TITLE_STYLE_KEY
title = "Preferred Title Style"
entries = arrayOf("Romaji", "English", "Native")
entryValues = arrayOf("romaji", "eng", "native")
setDefaultValue(PREF_TITLE_STYLE_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_SUB_KEY
title = "Prefer subs or dubs?"
entries = arrayOf("Subs", "Dubs")
entryValues = arrayOf("sub", "dub")
setDefaultValue(PREF_SUB_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)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_REMOVE_RAW_KEY
title = "Attempt to filter out raw"
setDefaultValue(PREF_REMOVE_RAW_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_USE_HOSTER_NAMES_KEY
title = "Use names of video hoster"
setDefaultValue(PREF_USE_HOSTER_NAMES_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.subPref
get() = getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
private val SharedPreferences.titleStyle
get() = getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
private val SharedPreferences.quality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.prefServer
get() = getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
private val SharedPreferences.getHosterBlacklist
get() = getStringSet(PREF_HOSTER_BLACKLIST_KEY, emptySet())!!
private val SharedPreferences.getAltHosterBlacklist
get() = getStringSet(PREF_ALT_HOSTER_BLACKLIST_KEY, emptySet())!!
private val SharedPreferences.removeRaw
get() = getBoolean(PREF_REMOVE_RAW_KEY, PREF_REMOVE_RAW_DEFAULT)
private val SharedPreferences.useHosterName
get() = getBoolean(PREF_USE_HOSTER_NAMES_KEY, PREF_USE_HOSTER_NAMES_DEFAULT)
}

View file

@ -0,0 +1,150 @@
package eu.kanade.tachiyomi.animeextension.en.allanimechi
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.serialization.Serializable
@Serializable
data class PopularResult(
val data: PopularResultData,
) {
@Serializable
data class PopularResultData(
val queryPopular: QueryPopularData,
) {
@Serializable
data class QueryPopularData(
val recommendations: List<Recommendation>,
) {
@Serializable
data class Recommendation(
val anyCard: Card? = null,
) {
@Serializable
data class Card(
val _id: String,
val name: String,
val thumbnail: String,
val englishName: String? = null,
val nativeName: String? = null,
) {
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"romaji" -> name
"eng" -> englishName ?: name
else -> nativeName ?: name
}
thumbnail_url = thumbnail
url = _id
}
}
}
}
}
}
@Serializable
data class SearchResult(
val data: SearchResultData,
) {
@Serializable
data class SearchResultData(
val shows: SearchResultShows,
) {
@Serializable
data class SearchResultShows(
val edges: List<SearchResultEdge>,
) {
@Serializable
data class SearchResultEdge(
val _id: String,
val name: String,
val thumbnail: String,
val englishName: String? = null,
val nativeName: String? = null,
) {
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"romaji" -> name
"eng" -> englishName ?: name
else -> nativeName ?: name
}
thumbnail_url = thumbnail
url = _id
}
}
}
}
}
@Serializable
data class DetailsResult(
val data: DataShow,
) {
@Serializable
data class DataShow(
val show: SeriesShows,
) {
@Serializable
data class SeriesShows(
val thumbnail: String,
val genres: List<String>? = null,
val studios: List<String>? = null,
val season: AirSeason? = null,
val status: String? = null,
val score: Float? = null,
val type: String? = null,
val description: String? = null,
) {
@Serializable
data class AirSeason(
val quarter: String,
val year: Int,
)
}
}
}
@Serializable
data class SeriesResult(
val data: DataShow,
) {
@Serializable
data class DataShow(
val show: SeriesShows,
) {
@Serializable
data class SeriesShows(
val _id: String,
val availableEpisodesDetail: AvailableEps,
) {
@Serializable
data class AvailableEps(
val sub: List<String>? = null,
val dub: List<String>? = null,
)
}
}
}
@Serializable
data class EpisodeResult(
val data: DataEpisode,
) {
@Serializable
data class DataEpisode(
val episode: Episode,
) {
@Serializable
data class Episode(
val sourceUrls: List<SourceUrl>,
) {
@Serializable
data class SourceUrl(
val sourceUrl: String,
val type: String,
val sourceName: String,
val priority: Float = 0F,
)
}
}
}

View file

@ -0,0 +1,214 @@
package eu.kanade.tachiyomi.animeextension.en.allanimechi
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AllAnimeChiFilters {
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>>,
): List<String> {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}
}
class OriginFilter : QueryPartFilter("Origin", AllAnimeChiFiltersData.ORIGIN)
class SeasonFilter : QueryPartFilter("Season", AllAnimeChiFiltersData.SEASONS)
class ReleaseYearFilter : QueryPartFilter("Released at", AllAnimeChiFiltersData.YEARS)
class EpisodeCountFilter : QueryPartFilter("Episode Count", AllAnimeChiFiltersData.EPISODE_COUNT)
class TypeFilter : QueryPartFilter("Type", AllAnimeChiFiltersData.TYPES)
class GenresFilter : CheckBoxFilterList(
"Genres",
AllAnimeChiFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("NOTE: Ignored if using text search!"),
OriginFilter(),
SeasonFilter(),
ReleaseYearFilter(),
AnimeFilter.Separator(),
TypeFilter(),
EpisodeCountFilter(),
GenresFilter(),
)
data class FilterSearchParams(
val origin: String = "",
val season: String = "",
val releaseYear: String = "",
val type: String = "",
val episodeCount: String = "",
val genres: List<String> = emptyList(),
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<OriginFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<ReleaseYearFilter>(),
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<EpisodeCountFilter>(),
filters.parseCheckbox<GenresFilter>(AllAnimeChiFiltersData.GENRES),
)
}
private object AllAnimeChiFiltersData {
val ALL = Pair("All", "all")
val ORIGIN = arrayOf(
Pair("All", "ALL"),
Pair("Japan", "JP"),
Pair("China", "CN"),
Pair("Korea", "KR"),
)
val SEASONS = arrayOf(
ALL,
Pair("Winter", "Winter"),
Pair("Spring", "Spring"),
Pair("Summer", "Summer"),
Pair("Fall", "Fall"),
)
val YEARS = arrayOf(
ALL,
Pair("2024", "2024"),
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("1999", "1999"),
Pair("1998", "1998"),
Pair("1997", "1997"),
Pair("1996", "1996"),
Pair("1995", "1995"),
Pair("1994", "1994"),
Pair("1993", "1993"),
Pair("1992", "1992"),
Pair("1991", "1991"),
Pair("1990", "1990"),
Pair("1989", "1989"),
Pair("1988", "1988"),
Pair("1987", "1987"),
Pair("1986", "1986"),
Pair("1985", "1985"),
Pair("1984", "1984"),
Pair("1983", "1983"),
Pair("1982", "1982"),
Pair("1981", "1981"),
Pair("1980", "1980"),
Pair("1979", "1979"),
Pair("1978", "1978"),
Pair("1977", "1977"),
Pair("1976", "1976"),
Pair("1975", "1975"),
)
val TYPES = arrayOf(
ALL,
Pair("TV", "TV"),
Pair("OVA", "OVA"),
Pair("Movie", "Movie"),
Pair("Special", "Special"),
Pair("ONA", "ONA"),
Pair("Unknown", "Unknown"),
)
val GENRES = arrayOf(
Pair("Samurai", "Samurai"),
Pair("School", "School"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Isekai", "Isekai"),
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Cars", "Cars"),
Pair("Comedy", "Comedy"),
Pair("Dementia", "Dementia"),
Pair("Demons", "Demons"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Game", "Game"),
Pair("Harem", "Harem"),
Pair("Historical", "Historical"),
Pair("Horror", "Horror"),
Pair("Josei", "Josei"),
Pair("Kids", "Kids"),
Pair("Magic", "Magic"),
Pair("Martial Arts", "Martial Arts"),
Pair("Mecha", "Mecha"),
Pair("Parody", "Parody"),
Pair("Police", "Police"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Thriller", "Thriller"),
Pair("Unknown", "Unknown"),
Pair("Vampire", "Vampire"),
Pair("Yaoi", "Yaoi"),
Pair("Yuri", "Yuri"),
)
val EPISODE_COUNT = arrayOf(
ALL,
Pair("1-5", "1-5"),
Pair("6-10", "6-10"),
Pair("11-15", "11-15"),
Pair("16-25", "16-25"),
Pair("26-70", "26-70"),
Pair("71-255", "71-255"),
Pair("256+", "256-"),
)
}
}

View file

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class AllAnimeExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String): List<Video> {
val jsonHeaders = headers.newBuilder().apply {
add("Accept", "*/*")
add("Host", url.toHttpUrl().host)
add("Referer", url)
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "same-origin")
add("X-Requested-With", "Y29tLmFsbGFuaW1lLmFuaW1lY2hpY2tlbg==".decodeBase64())
}.build()
val decoded = url.toHttpUrl().queryParameter("source")?.decodeBase64() ?: return emptyList()
val slug = json.decodeFromString<SourceUrl>(decoded).idUrl
val data = client.newCall(
GET("https://${url.toHttpUrl().host}$slug", headers = jsonHeaders),
).execute().parseAs<VideoData>()
return data.links.flatMap { link ->
val subtitleList = link.subtitles.map {
Track(it.src, it.label)
}
val audioList = link.rawUrls.audios.map {
Track(it.url, formatBytes(it.bandwidth) + "/s")
}
link.rawUrls.vids.map { vid ->
val bandwidth = formatBytes(vid.bandwidth) + "/s"
val name = "$prefix${vid.height}p ($bandwidth)"
Video(vid.url, name, vid.url, subtitleTracks = subtitleList, audioTracks = audioList)
}
}
}
// ============================= Utilities ==============================
private fun String.decodeBase64(): String {
return String(Base64.decode(this, Base64.DEFAULT))
}
private fun formatBytes(bytes: Long): String {
return when {
bytes >= 1_000_000_000 -> "%.2f GB".format(bytes / 1_000_000_000.0)
bytes >= 1_000_000 -> "%.2f MB".format(bytes / 1_000_000.0)
bytes >= 1_000 -> "%.2f KB".format(bytes / 1_000.0)
bytes > 1 -> "$bytes bytes"
bytes == 1L -> "$bytes byte"
else -> ""
}
}
@Serializable
data class SourceUrl(
val idUrl: String,
)
@Serializable
data class VideoData(
val links: List<LinkObject>,
) {
@Serializable
data class LinkObject(
val resolutionStr: String,
val rawUrls: UrlObject,
val subtitles: List<SubtitleObject>,
) {
@Serializable
data class UrlObject(
val vids: List<VideoObject>,
val audios: List<AudioObject>,
) {
@Serializable
data class VideoObject(
val bandwidth: Long,
val height: Int,
val url: String,
)
@Serializable
data class AudioObject(
val bandwidth: Long,
val url: String,
)
}
@Serializable
data class SubtitleObject(
val label: String,
val src: String,
)
}
}
}

View file

@ -0,0 +1,154 @@
package eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animeextension.en.allanimechi.AllAnimeChi
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.Locale
class InternalExtractor(private val client: OkHttpClient, private val apiHeaders: Headers, private val headers: Headers) {
private val blogUrl = "aHR0cHM6Ly9ibG9nLmFsbGFuaW1lLnBybw==".decodeBase64()
private val playlistHeaders = Headers.headersOf(
"User-Agent",
"Dalvik/2.1.0 (Linux; U; Android 13; Pixel 5 Build/TQ3A.230705.001.B4)",
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val json: Json by injectLazy()
fun videosFromServer(server: AllAnimeChi.Server, useHosterName: Boolean, removeRaw: Boolean): List<Pair<Video, Float>> {
val blogHeaders = apiHeaders.newBuilder().apply {
set("host", blogUrl.toHttpUrl().host)
}.build()
val videoData = client.newCall(
GET(blogUrl + server.sourceUrl.replace("clock?id=", "clock.json?id="), blogHeaders),
).execute().parseAs<VideoData>()
val videoList = videoData.links.flatMap {
when {
it.hls == true -> getFromHls(server, it, useHosterName, removeRaw)
it.mp4 == true -> getFromMp4(server, it, useHosterName)
else -> emptyList()
}
}
return videoList
}
private fun getFromMp4(server: AllAnimeChi.Server, data: VideoData.LinkObject, useHosterName: Boolean): List<Pair<Video, Float>> {
val host = if (useHosterName) getHostName(data.link, server.sourceName) else server.sourceName
val baseName = "$host - ${data.resolutionStr}"
val video = Video(data.link, baseName, data.link, headers = playlistHeaders)
return listOf(
Pair(video, server.priority),
)
}
private fun getFromHls(server: AllAnimeChi.Server, data: VideoData.LinkObject, useHosterName: Boolean, removeRaw: Boolean): List<Pair<Video, Float>> {
if (removeRaw && data.resolutionStr.contains("raw", true)) return emptyList()
val host = if (useHosterName) getHostName(data.link, server.sourceName) else server.sourceName
val linkHost = data.link.toHttpUrl().host
// Doesn't seem to work
if (server.sourceName.equals("Luf-mp4", true)) {
return getFromGogo(server, data, host)
}
// Hardcode some names
val baseName = if (linkHost.contains("crunchyroll")) {
"Crunchyroll - ${data.resolutionStr}"
.replace("m_SUB", "SoftSub")
.replace("vo_SUB", "Sub")
.replace("SUB", "Sub")
} else if (linkHost.contains("vrv")) {
"Vrv - ${data.resolutionStr}"
.replace("m_SUB", "SoftSub")
.replace("vo_SUB", "Sub")
.replace("SUB", "Sub")
} else {
"$host - ${data.resolutionStr}"
}
// Get stuff
val hlsHeaders = playlistHeaders.newBuilder().apply {
set("host", linkHost)
}.build()
val masterHeaders = hlsHeaders.newBuilder().apply {
data.headers?.entries?.forEach {
set(it.key, it.value)
}
}.build()
val videoList = playlistUtils.extractFromHls(
data.link,
videoNameGen = { q -> "$baseName - ${data.resolutionStr} - $q" },
masterHeadersGen = { _, _ -> masterHeaders },
)
return videoList.map {
Pair(it, server.priority)
}
}
private fun getFromGogo(server: AllAnimeChi.Server, data: VideoData.LinkObject, hostName: String): List<Pair<Video, Float>> {
val host = data.link.toHttpUrl().host
// Seems to be dead
if (host.contains("maverickki", true)) return emptyList()
val videoList = playlistUtils.extractFromHls(
data.link,
videoNameGen = { q -> "$hostName - ${data.resolutionStr} - $q" },
referer = "https://playtaku.net/",
)
return videoList.map {
Pair(it, server.priority)
}
}
// ============================= Utilities ==============================
private fun getHostName(host: String, fallback: String): String {
return host.toHttpUrlOrNull()?.host?.split(".")?.let {
it.getOrNull(it.size - 2)?.replaceFirstChar { c ->
if (c.isLowerCase()) c.titlecase(Locale.ROOT) else c.toString()
}
} ?: fallback
}
@Serializable
data class VideoData(
val links: List<LinkObject>,
) {
@Serializable
data class LinkObject(
val link: String,
val resolutionStr: String,
val mp4: Boolean? = null,
val hls: Boolean? = null,
val headers: Map<String, String>? = null,
)
}
private fun String.decodeBase64(): String {
return String(Base64.decode(this, Base64.DEFAULT))
}
}

View file

@ -0,0 +1,7 @@
ext {
extName = 'AllMovies'
extClass = '.AllMovies'
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View file

@ -0,0 +1,337 @@
package eu.kanade.tachiyomi.animeextension.en.allmovies
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AllMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AllMoviesForYou"
override val baseUrl = "https://allmoviesforyou.net"
override val lang = "en"
override val supportsLatest = false
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Popular Anime
override fun popularAnimeSelector(): String = "article.TPost > a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/series/page/$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.title = element.select("h2.Title").text()
anime.thumbnail_url = "https:" + element.select("div.Image figure img").attr("data-src")
return anime
}
override fun popularAnimeNextPageSelector(): String = "div.nav-links a:last-child"
// Episodes
override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val seriesLink = document.select("link[rel=canonical]").attr("abs:href")
if (seriesLink.contains("/series/")) {
val seasonsHtml = client.newCall(
GET(
seriesLink,
headers = Headers.headersOf("Referer", document.location()),
),
).execute().asJsoup()
val seasonsElements = seasonsHtml.select("section.SeasonBx.AACrdn a")
seasonsElements.forEach {
val seasonEpList = parseEpisodesFromSeries(it)
episodeList.addAll(seasonEpList)
}
} else {
val episode = SEpisode.create()
episode.name = document.select("div.TPMvCn h1.Title").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(seriesLink)
episodeList.add(episode)
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.episode_number = element.select("td > span.Num").text().toFloat()
val seasonNum = element.ownerDocument()!!.select("div.Title span").text()
episode.name = "Season $seasonNum" + "x" + element.select("td span.Num").text() + " : " + element.select("td.MvTbTtl > a").text()
episode.setUrlWithoutDomain(element.select("td.MvTbPly > a.ClA").attr("abs:href"))
return episode
}
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
val seasonId = element.attr("abs:href")
val episodesHtml = client.newCall(GET(seasonId)).execute().asJsoup()
val episodeElements = episodesHtml.select("tr.Viewed")
return episodeElements.map { episodeFromElement(it) }
}
// Video urls
override fun videoListRequest(episode: SEpisode): Request {
val document = client.newCall(GET(baseUrl + episode.url)).execute().asJsoup()
val iframe = document.select("iframe[src*=/?trembed]").attr("abs:src")
return GET(iframe)
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return videosFromElement(document)
}
override fun videoListSelector() = "iframe"
private fun videosFromElement(document: Document): List<Video> {
val videoList = mutableListOf<Video>()
val elements = document.select(videoListSelector())
for (element in elements) {
val url = element.attr("abs:src")
val location = element.ownerDocument()!!.location()
val videoHeaders = Headers.headersOf("Referer", location)
when {
url.contains("https://dood") -> {
val newQuality = "Doodstream mirror"
val video = Video(url, newQuality, doodUrlParse(url), headers = videoHeaders)
videoList.add(video)
}
url.contains("streamhub") -> {
val response = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
val script = response.selectFirst("script:containsData(m3u8)")!!
val data = script.data()
val masterUrl = masterExtractor(data)
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl))
}
}
}
}
return videoList
}
private fun masterExtractor(code: String): String {
val stringsRegex = """(?<!\\)'.+?(?<!\\)'""".toRegex()
val strings = stringsRegex.findAll(code).map {
it.value
}.toList()
var p = strings[3]
val k = strings[4].split('|')
val numbersRegex = """(?<=,)\d+(?=,)""".toRegex()
val numbers = numbersRegex.findAll(code).map {
it.value.toInt()
}.toList()
val a = numbers[0]
var c = numbers[1] - 1
while (c >= 0) {
val replaceRegex = ("""\b""" + c.toString(a) + """\b""").toRegex()
p = p.replace(replaceRegex, k[c])
c--
}
val sourcesRegex = """(?<=sources':\[\{src:").+?(?=")""".toRegex()
return sourcesRegex.find(p)!!.value
}
private fun doodUrlParse(url: String): String? {
val response = client.newCall(GET(url.replace("/d/", "/e/"))).execute()
val content = response.body.string()
if (!content.contains("'/pass_md5/")) return null
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
val token = md5.substringAfterLast("/")
val doodTld = url.substringAfter("https://dood.").substringBefore("/")
val randomString = getRandomString()
val expiry = System.currentTimeMillis()
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", url),
),
).execute().body.string()
return "$videoUrlStart$randomString?token=$token&expiry=$expiry"
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// search
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("article a").attr("href"))
anime.title = element.select("h2.Title").text()
anime.thumbnail_url = "https:" + element.select("div.Image figure img").attr("data-src")
return anime
}
override fun searchAnimeNextPageSelector(): String = "div.nav-links a:last-child"
override fun searchAnimeSelector(): String = "ul.MovieList li"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/page/$page?s=$query", headers)
} else {
val url = "$baseUrl/category/".toHttpUrlOrNull()!!.newBuilder()
filters.forEach { filter ->
when (filter) {
is GenreFilter -> url.addPathSegment(filter.toUriPart())
else -> {}
}
}
url.addPathSegment("page")
url.addPathSegment("$page")
GET(url.toString(), headers)
}
}
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("div.TPMvCn h1.Title").text()
anime.genre = document.select("p.Genre a").joinToString(", ") { it.text() }
anime.status = parseStatus(document.select("div.Info").text()) // span.Qlty
anime.author = document.select("p.Director span a").joinToString(", ") { it.text() }
anime.description = document.select("div.TPMvCn div.Description p:first-of-type").text()
return anime
}
private fun parseStatus(status: String?) = when {
status == null -> SAnime.UNKNOWN
status.contains("AIR", ignoreCase = true) -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
// Latest
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
// Filters
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("NOTE: Ignored if using text search!"),
AnimeFilter.Separator(),
GenreFilter(getGenreList()),
)
private class GenreFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Genres", vals)
private fun getGenreList() = arrayOf(
Pair("Action & Adventure", "action-adventure"),
Pair("Adventure", "aventure"),
Pair("Animation", "animation"),
Pair("Comedy", "comedy"),
Pair("Crime", "crime"),
Pair("Disney", "disney"),
Pair("Drama", "drama"),
Pair("Family", "family"),
Pair("Fantasy", "fantasy"),
Pair("History", "fistory"),
Pair("Horror", "horror"),
Pair("Kids", "kids"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Reality", "reality"),
Pair("Romance", "romance"),
Pair("Sci-Fi & Fantasy", "sci-fi-fantasy"),
Pair("Science Fiction", "science-fiction"),
Pair("Thriller", "thriller"),
Pair("War", "war"),
Pair("War & Politics", "war-politics"),
Pair("Western", "western"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// settings
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
entryValues = arrayOf("1080", "720", "480", "360", "240")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}

View file

@ -0,0 +1,7 @@
ext {
extName = 'AnimeFlix'
extClass = '.AnimeFlix'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -0,0 +1,398 @@
package eu.kanade.tachiyomi.animeextension.en.animeflix
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MultipartBody
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeFlix"
override val baseUrl = "https://animeflix.mobi"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
// prevent base64 images
thumbnail_url = element.selectFirst("img")!!.run {
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
}
title = element.selectFirst("header")!!.text()
}
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cleanQuery = query.replace(" ", "+").lowercase()
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
SubPageFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Isekai", "isekai"),
Pair("Drama", "drama"),
Pair("Psychological", "psychological"),
Pair("Ecchi", "ecchi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Magic", "magic"),
Pair("Slice Of Life", "slice-of-life"),
Pair("Sports", "sports"),
Pair("Comedy", "comedy"),
Pair("Fantasy", "fantasy"),
Pair("Horror", "horror"),
Pair("Yaoi", "yaoi"),
),
)
private class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Ongoing", "ongoing"),
Pair("Latest Release", "latest-release"),
Pair("Movies", "movies"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text()
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
status = when (infosDiv.getInfo("Status").toString()) {
"Completed" -> SAnime.COMPLETED
"Currently Airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
artist = infosDiv.getInfo("Studios")
author = infosDiv.getInfo("Producers")
genre = infosDiv.getInfo("Genres")
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
}
private fun Element.getInfo(info: String) =
selectFirst("li:contains($info)")?.ownText()?.trim()
// ============================== Episodes ==============================
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
val qualityRegex by lazy { """(\d+)p""".toRegex() }
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val document = client.newCall(GET(baseUrl + anime.url)).execute()
.asJsoup()
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
}
seasonsLinks.flatMap { (seasonNumber, season) ->
val serverListSeason = season.map {
val previousText = it.previousElementSibling()!!.text()
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
val url = it.selectFirst("a")!!.attr("href")
val episodesDocument = client.newCall(GET(url)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
}
}
transposeEpisodes(serverListSeason)
}
} else {
val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
Pair(it.selectFirst("a")!!.attr("href"), quality)
}
// Load episodes
val serversList = driveList.map { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(drive.second, it.attr("href"), it.text())
}
}
transposeEpisodes(serversList)
}
return episodeList.reversed()
}
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
transpose(serversList).mapIndexed { index, serverList ->
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(json.encodeToString(serverList))
}
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val urls = json.decodeFromString<List<EpUrl>>(episode.url)
val leechUrls = urls.map {
val firstLeech = client.newCall(GET(it.url)).execute()
.asJsoup()
.selectFirst("script:containsData(downlaod_button)")!!
.data()
.substringAfter("<a href=\"")
.substringBefore("\">")
val path = client.newCall(GET(firstLeech)).execute()
.body.string()
.substringAfter("replace(\"")
.substringBefore("\"")
val link = "https://" + firstLeech.toHttpUrl().host + path
EpUrl(it.quality, link, it.name)
}
val videoList = leechUrls.parallelCatchingFlatMap { url ->
if (url.url.toHttpUrl().encodedPath == "/404") return@parallelCatchingFlatMap emptyList()
val (videos, mediaUrl) = extractVideo(url)
when {
videos.isEmpty() -> {
extractGDriveLink(mediaUrl, url.quality).ifEmpty {
getDirectLink(mediaUrl, "instant", "/mfile/")?.let {
listOf(Video(it, "${url.quality}p - GDrive Instant link", it))
} ?: emptyList()
}
}
else -> videos
}
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val matchResult = qualityRegex.find(epUrl.name)
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
return (1..3).toList().flatMap { type ->
extractWorkerLinks(epUrl.url, quality, type)
}.let { Pair(it, epUrl.url) }
}
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href")
val decodedLink = if (link.contains("workers.dev")) {
link
} else {
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
}
Video(
url = decodedLink,
quality = "${quality}p - CF $type Worker ${index + 1}$size",
videoUrl = decodedLink,
)
}
}
private fun getDirectLink(url: String, action: String = "direct", newPath: String = "/file/"): String? {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(async function taskaction)")
?.data()
?: return url
val key = script.substringAfter("key\", \"").substringBefore('"')
val form = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("action", action)
.addFormDataPart("key", key)
.addFormDataPart("action_token", "")
.build()
val headers = headersBuilder().set("x-token", url.toHttpUrl().host).build()
val req = client.newCall(POST(url.replace("/file/", newPath), headers, form)).execute()
return runCatching {
json.decodeFromString<DriveLeechDirect>(req.body.string()).url
}.getOrNull()
}
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val neoUrl = getDirectLink(mediaUrl) ?: mediaUrl
val response = client.newCall(GET(neoUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href")
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) {
emptyList()
} else {
val realLink = link.attr("action")
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun <E> transpose(xs: List<List<E>>): List<List<E>> {
// Helpers
fun <E> List<E>.head(): E = this.first()
fun <E> List<E>.tail(): List<E> = this.takeLast(this.size - 1)
fun <E> E.append(xs: List<E>): List<E> = listOf(this).plus(xs)
xs.filter { it.isNotEmpty() }.let { ys ->
return when (ys.isNotEmpty()) {
true -> ys.map { it.head() }.append(transpose(ys.map { it.tail() }))
else -> emptyList()
}
}
}
@Serializable
data class EpUrl(
val quality: String,
val url: String,
val name: String,
)
@Serializable
data class DriveLeechDirect(val url: String? = null)
companion object {
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
}
// ============================== 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_VALUES
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)
}
}

View file

@ -0,0 +1,12 @@
ext {
extName = 'Animeflix.live'
extClass = '.AnimeflixLive'
extVersionCode = 4
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:gogostream-extractor'))
implementation(project(':lib:playlist-utils'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,503 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import GenreFilter
import SortFilter
import SubPageFilter
import TypeFilter
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlin.math.min
class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Animeflix.live"
override val baseUrl by lazy { preferences.baseUrl }
private val apiUrl by lazy { preferences.apiUrl }
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val apiHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Referer", "$baseUrl/")
}.build()
private val docHeaders = headersBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
add("Host", apiUrl.toHttpUrl().host)
add("Referer", "$baseUrl/")
}.build()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$apiUrl/popular?page=${page - 1}", apiHeaders)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$apiUrl/trending?page=${page - 1}", apiHeaders)
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.parseAs<TrendingDto>()
val titlePref = preferences.titleType
val animeList = parsed.trending.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val sort = filters.filterIsInstance<SortFilter>().first().getValue()
val type = filters.filterIsInstance<TypeFilter>().first().getValues()
val genre = filters.filterIsInstance<GenreFilter>().first().getValues()
val subPage = filters.filterIsInstance<SubPageFilter>().first().getValue()
if (subPage.isNotBlank()) {
return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders)
}
if (query.isEmpty()) {
throw Exception("Search must not be empty")
}
val filtersObj = buildJsonObject {
put("sort", sort)
if (type.isNotEmpty()) {
put("type", json.encodeToString(type))
}
if (genre.isNotEmpty()) {
put("genre", json.encodeToString(genre))
}
}.toJsonString()
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("info")
addPathSegment("")
addQueryParameter("query", query)
addQueryParameter("limit", "15")
addQueryParameter("filters", filtersObj)
addQueryParameter("k", query.substr(0, 3).sk())
}.build()
return GET(url, apiHeaders)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
val hasNextPage = if (response.request.url.queryParameter("limit") == null) {
animeList.size == 44
} else {
animeList.size == 15
}
return AnimesPage(animeList, hasNextPage)
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
SortFilter(),
TypeFilter(),
GenreFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"),
SubPageFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
return GET("$apiUrl/getslug/${anime.url}", apiHeaders)
}
override fun getAnimeUrl(anime: SAnime): String {
return "$baseUrl/search/${anime.title}?anime=${anime.url}"
}
override fun animeDetailsParse(response: Response): SAnime {
val titlePref = preferences.titleType
return response.parseAs<DetailsDto>().toSAnime(titlePref)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val lang = preferences.lang
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("episodes")
addQueryParameter("id", anime.url)
addQueryParameter("dub", (lang == "Dub").toString())
addQueryParameter("c", anime.url.sk())
}.build()
return GET(url, apiHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val slug = response.request.url.queryParameter("id")!!
return response.parseAs<EpisodeResponseDto>().episodes.map {
it.toSEpisode(slug)
}.sortedByDescending { it.episode_number }
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("server", "")
addQueryParameter("c", episode.url.substringAfter("/watch/").sk())
}.build()
return GET(url, apiHeaders)
}
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
val initialPlayerUrl = apiUrl + response.parseAs<ServerDto>().source
val initialServer = initialPlayerUrl.toHttpUrl().queryParameter("server")!!
val initialPlayerDocument = client.newCall(
GET(initialPlayerUrl, docHeaders),
).execute().asJsoup().unescape()
videoList.addAll(
videosFromPlayer(
initialPlayerDocument,
initialServer.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
),
)
// Go through rest of servers
val servers = initialPlayerDocument.selectFirst("script:containsData(server-settings)")!!.data()
val serversHtml = SERVER_REGEX.findAll(servers).map {
Jsoup.parseBodyFragment(it.groupValues[1])
}.toList()
videoList.addAll(
serversHtml.parallelCatchingFlatMapBlocking {
val server = serverMapping[
it.selectFirst("button")!!
.attr("onclick")
.substringAfter("postMessage('")
.substringBefore("'"),
]
if (server == initialServer) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val serverUrl = response.request.url.newBuilder()
.setQueryParameter("server", server)
.build()
val playerUrl = apiUrl + client.newCall(
GET(serverUrl, apiHeaders),
).execute().parseAs<ServerDto>().source
if (server != playerUrl.toHttpUrl().queryParameter("server")!!) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val playerDocument = client.newCall(
GET(playerUrl, docHeaders),
).execute().asJsoup().unescape()
videosFromPlayer(
playerDocument,
server.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
)
},
)
return videoList
}
private val serverMapping = mapOf(
"settings-0" to "moon",
"settings-1" to "sun",
"settings-2" to "zoro",
"settings-3" to "gogo",
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
return baseHeaders.newBuilder().apply {
add("Accept", "*/*")
add("Accept-Language", "en-US,en;q=0.5")
add("Host", videoUrl.toHttpUrl().host)
add("Origin", "https://${apiUrl.toHttpUrl().host}")
add("Referer", "$apiUrl/")
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "cross-site")
}.build()
}
private fun Document.unescape(): Document {
val unescapeScript = this.selectFirst("script:containsData(unescape)")
return if (unescapeScript == null) {
this
} else {
val data = URLDecoder.decode(unescapeScript.data(), "UTF-8")
Jsoup.parse(data, this.location())
}
}
private fun videosFromPlayer(document: Document, name: String): List<Video> {
val dataScript = document.selectFirst("script:containsData(m3u8)")
?.data() ?: return emptyList()
val subtitleList = document.select("video > track[kind=captions]").map {
Track(it.attr("id"), it.attr("label"))
}
var masterPlaylist = M3U8_REGEX.find(dataScript)?.groupValues?.get(1)
?: return emptyList()
if (name.equals("moon", true)) {
masterPlaylist += dataScript.substringAfter("`${'$'}{url}")
.substringBefore("`")
}
return playlistUtils.extractFromHls(
masterPlaylist,
videoHeadersGen = ::getVideoHeaders,
videoNameGen = { q -> "$name - $q" },
subtitleList = subtitleList,
)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.quality
val server = preferences.server
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
private fun JsonObject.toJsonString(): String {
return json.encodeToString(this)
}
private fun String.sk(): String {
val t = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val n = 17 + (t.get(Calendar.DAY_OF_MONTH) - t.get(Calendar.MONTH)) / 2
return this.toCharArray().fold("") { acc, c ->
acc + c.code.toString(n).padStart(2, '0')
}
}
private fun String.substr(start: Int, end: Int): String {
val stop = min(end, this.length)
return this.substring(start, stop)
}
companion object {
private val SERVER_REGEX = Regex("""'1' === '1'.*?(<button.*?</button>)""", RegexOption.DOT_MATCHES_ALL)
private val M3U8_REGEX = Regex("""const ?\w*? ?= ?`(.*?)`""")
private const val PAGE_SIZE = 24
private const val PREF_DOMAIN_KEY = "pref_domain_key"
private const val PREF_DOMAIN_DEFAULT = "https://animeflix.live,https://api.animeflix.dev"
private val PREF_DOMAIN_ENTRIES = arrayOf("animeflix.live", "animeflix.ro")
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf(
"https://animeflix.live,https://api.animeflix.dev",
"https://animeflix.ro,https://api.animeflixtv.to",
)
private const val PREF_TITLE_KEY = "pref_title_type_key"
private const val PREF_TITLE_DEFAULT = "English"
private val PREF_TITLE_ENTRIES = arrayOf("English", "Native", "Romaji")
private const val PREF_LANG_KEY = "pref_lang_key"
private const val PREF_LANG_DEFAULT = "Sub"
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
private const val PREF_SERVER_KEY = "pref_server_key"
private const val PREF_SERVER_DEFAULT = "Moon"
private val PREF_SERVER_ENTRIES = arrayOf("Moon", "Sun", "Zoro", "Gogo")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain (requires app restart)"
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_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_TITLE_KEY
title = "Preferred Title Type"
entries = PREF_TITLE_ENTRIES
entryValues = PREF_TITLE_ENTRIES
setDefaultValue(PREF_TITLE_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_LANG_KEY
title = "Preferred Language"
entries = PREF_LANG_ENTRIES
entryValues = PREF_LANG_ENTRIES
setDefaultValue(PREF_LANG_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_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_VALUES
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)
}
private val SharedPreferences.baseUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").first()
private val SharedPreferences.apiUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").last()
private val SharedPreferences.titleType
get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
private val SharedPreferences.lang
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
private val SharedPreferences.quality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.server
get() = getString(PREF_SERVER_KEY, PREF_QUALITY_DEFAULT)!!
}

View file

@ -0,0 +1,123 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import kotlin.math.ceil
import kotlin.math.floor
@Serializable
class TrendingDto(
val trending: List<AnimeDto>,
)
@Serializable
class AnimeDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
}
}
@Serializable
class DetailsDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val description: String,
val genres: List<String>,
val status: String? = null,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
genre = genres.joinToString()
status = this@DetailsDto.status.parseStatus()
description = Jsoup.parseBodyFragment(
this@DetailsDto.description.replace("<br>", "br2n"),
).text().replace("br2n", "\n")
}
private fun String?.parseStatus(): Int = when (this?.lowercase()) {
"releasing" -> SAnime.ONGOING
"finished" -> SAnime.COMPLETED
"cancelled" -> SAnime.CANCELLED
else -> SAnime.UNKNOWN
}
}
@Serializable
class EpisodeResponseDto(
val episodes: List<EpisodeDto>,
) {
@Serializable
class EpisodeDto(
val number: Float,
val title: String? = null,
) {
fun toSEpisode(slug: String): SEpisode = SEpisode.create().apply {
val epNum = if (floor(number) == ceil(number)) {
number.toInt().toString()
} else {
number.toString()
}
url = "/watch/$slug-episode-$epNum"
episode_number = number
name = if (title == null) {
"Episode $epNum"
} else {
"Ep. $epNum - $title"
}
}
}
}
@Serializable
class ServerDto(
val source: String,
)

View file

@ -0,0 +1,81 @@
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
open class UriPartFilter(
name: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun getValue(): String {
return vals[state].second
}
}
open class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }) {
fun getValues(): List<String> {
return state.filter { it.state }.map { it.value }
}
}
class SortFilter : UriPartFilter(
"Sort",
arrayOf(
Pair("Recently Updated", "recently_updated"),
Pair("Recently Added", "recently_added"),
Pair("Release Date ↓", "release_date_down"),
Pair("Release Date ↑", "release_date_up"),
Pair("Name A-Z", "title_az"),
Pair("Best Rating", "scores"),
Pair("Most Watched", "most_watched"),
Pair("Anime Length", "number_of_episodes"),
),
)
class TypeFilter : UriMultiSelectFilter(
"Type",
arrayOf(
Pair("TV", "TV"),
Pair("Movie", "MOVIE"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "SPECIAL"),
),
)
class GenreFilter : UriMultiSelectFilter(
"Genre",
arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mecha", "Mecha"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
),
)
class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Movies", "movies"),
Pair("Series", "series"),
),
)

View file

@ -0,0 +1,15 @@
ext {
extName = 'AnimeKhor'
extClass = '.AnimeKhor'
themePkg = 'animestream'
baseUrl = 'https://animekhor.xyz'
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.animeextension.en.animekhor
import eu.kanade.tachiyomi.animeextension.en.animekhor.extractors.StreamHideExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
class AnimeKhor : AnimeStream(
"en",
"AnimeKhor",
"https://animekhor.xyz",
) {
// ============================ Video Links =============================
override fun getVideoList(url: String, name: String): List<Video> {
val prefix = "$name - "
return when {
url.contains("ahvsh.com") || name.equals("streamhide", true) -> {
StreamHideExtractor(client, headers).videosFromUrl(url, prefix = prefix)
}
url.contains("ok.ru") -> {
OkruExtractor(client).videosFromUrl(url, prefix = prefix)
}
url.contains("streamwish") -> {
val docHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, prefix)
}
// TODO: Videos won't play
// url.contains("animeabc.xyz") -> {
// AnimeABCExtractor(client, headers).videosFromUrl(url, prefix = prefix)
// }
else -> emptyList()
}
}
}

View file

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.animeextension.en.animekhor.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class AnimeABCExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val videoList = mutableListOf<Video>()
val document = client.newCall(GET(url, headers = headers)).execute().asJsoup()
val data = document.selectFirst("script:containsData(m3u8)")?.data() ?: return emptyList()
val sources = json.decodeFromString<List<Source>>(
"[${data.substringAfter("sources:")
.substringAfter("[")
.substringBefore("]")}]",
)
sources.forEach { src ->
val masterplHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Connection", "keep-alive")
.add("Host", url.toHttpUrl().host)
.add("Referer", url)
.build()
val masterPlaylist = client.newCall(
GET(src.file.replace("^//".toRegex(), "https://"), headers = masterplHeaders),
).execute().body.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = prefix + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p ${src.label ?: ""}"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl, headers = masterplHeaders))
}
}
return videoList
}
@Serializable
data class Source(
val file: String,
val label: String? = null,
)
}

View file

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.animeextension.en.animekhor.extractors
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 okhttp3.Headers
import okhttp3.OkHttpClient
class StreamHideExtractor(private val client: OkHttpClient, private val headers: Headers) {
// from nineanime / ask4movie FilemoonExtractor
private val subtitleRegex = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val page = client.newCall(GET(url, headers = headers)).execute().body.string()
val unpacked = JsUnpacker.unpackAndCombine(page) ?: page
val playlistUrl = unpacked.substringAfter("sources:")
.substringAfter("file:\"")
.substringBefore('"')
val playlistData = client.newCall(GET(playlistUrl, headers = headers)).execute().body.string()
val subs = subtitleRegex.findAll(playlistData).map {
val subUrl = fixUrl(it.groupValues[2], playlistUrl)
Track(subUrl, it.groupValues[1])
}.toList()
val separator = "#EXT-X-STREAM-INF"
return playlistData.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n")
.substringAfter("x")
.substringBefore(",") + "p"
val urlPart = it.substringAfter("\n").substringBefore("\n")
val videoUrl = fixUrl(urlPart, playlistUrl)
Video(videoUrl, "${prefix}StreamHide:$resolution", videoUrl, subtitleTracks = subs)
}
}
private fun fixUrl(urlPart: String, playlistUrl: String) =
when {
!urlPart.startsWith("https:") -> playlistUrl.substringBeforeLast("/") + "/$urlPart"
else -> urlPart
}
}

View file

@ -0,0 +1,16 @@
ext {
extName = 'Animenosub'
extClass = '.Animenosub'
themePkg = 'animestream'
baseUrl = 'https://animenosub.com'
overrideVersionCode = 5
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -0,0 +1,122 @@
package eu.kanade.tachiyomi.animeextension.en.animenosub
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.animenosub.extractors.VidMolyExtractor
import eu.kanade.tachiyomi.animeextension.en.animenosub.extractors.VtubeExtractor
import eu.kanade.tachiyomi.animeextension.en.animenosub.extractors.WolfstreamExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import org.jsoup.nodes.Element
class Animenosub : AnimeStream(
"en",
"Animenosub",
"https://animenosub.com",
) {
// ============================== Episodes ==============================
override fun getEpisodeName(element: Element, epNum: String): String {
val episodeTitle = element.selectFirst("div.epl-title")?.text() ?: ""
val complement = if (episodeTitle.contains("Episode $epNum", true)) "" else episodeTitle
return "Ep. $epNum $complement"
}
// ============================ Video Links =============================
override fun getVideoList(url: String, name: String): List<Video> {
val prefix = "$name - "
return when {
url.contains("streamwish") -> {
StreamWishExtractor(client, headers).videosFromUrl(url, prefix)
}
url.contains("vidmoly") -> {
VidMolyExtractor(client).getVideoList(url, name)
}
url.contains("https://vtbe") -> {
VtubeExtractor(client, headers).videosFromUrl(url, baseUrl, prefix)
}
url.contains("wolfstream") -> {
WolfstreamExtractor(client).videosFromUrl(url, prefix)
}
url.contains("filemoon") -> {
FilemoonExtractor(client).videosFromUrl(url, prefix, headers)
}
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
val videoTypePref = ListPreference(screen.context).apply {
key = PREF_TYPE_KEY
title = PREF_TYPE_TITLE
entries = PREF_TYPE_VALUES
entryValues = PREF_TYPE_VALUES
setDefaultValue(PREF_TYPE_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()
}
}
val videoServer = ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE
entries = PREF_SERVER_VALUES
entryValues = PREF_SERVER_VALUES
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()
}
}
screen.addPreference(videoTypePref)
screen.addPreference(videoServer)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.startsWith(type, true) },
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
companion object {
private const val PREF_TYPE_KEY = "preferred_type"
private const val PREF_TYPE_TITLE = "Preferred Video Type"
private const val PREF_TYPE_DEFAULT = "SUB"
private val PREF_TYPE_VALUES = arrayOf("SUB", "RAW")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred Video Server"
private const val PREF_SERVER_DEFAULT = "StreamWish"
private val PREF_SERVER_VALUES = arrayOf(
"StreamWish",
"VidMoly",
"Vtube",
"WolfStream",
"Filemoon",
)
}
}

View file

@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.animeextension.en.animenosub.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class VidMolyExtractor(private val client: OkHttpClient) {
private val regexPlaylist = Regex("file:\"(\\S+?)\"")
fun getVideoList(url: String, lang: String): List<Video> {
val body = client.newCall(GET(url)).execute()
.body.string()
val playlistUrl = regexPlaylist.find(body)!!.groupValues.get(1)
val headers = Headers.headersOf("Referer", "https://vidmoly.to")
val playlistData = client.newCall(GET(playlistUrl, headers)).execute()
.body.string()
val separator = "#EXT-X-STREAM-INF:"
return playlistData.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, "$lang - $quality", videoUrl, headers)
}
}
}

View file

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.animeextension.en.animenosub.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.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class VtubeExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(url: String, baseUrl: String, prefix: String): List<Video> {
val videoList = mutableListOf<Video>()
val docHeaders = headers.newBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", url.toHttpUrl().host)
.add("Referer", "$baseUrl/")
.build()
val doc = client.newCall(GET(url, headers = docHeaders)).execute().asJsoup()
val jsEval = doc.selectFirst("script:containsData(m3u8)")!!.data()
val masterUrl = JsUnpacker.unpackAndCombine(jsEval)
?.substringAfter("source")
?.substringAfter("file:\"")
?.substringBefore("\"")
?: return emptyList()
val playlistHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Host", masterUrl.toHttpUrl().host)
.add("Origin", "https://${url.toHttpUrl().host}")
.add("Referer", "https://${url.toHttpUrl().host}/")
.build()
val masterPlaylist = client.newCall(
GET(masterUrl, headers = playlistHeaders),
).execute().body.string()
val separator = "#EXT-X-STREAM-INF:"
masterPlaylist.substringAfter(separator).split(separator).forEach {
val quality = prefix + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
val videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl, headers = playlistHeaders))
}
return videoList
}
}

View file

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.animeextension.en.animenosub.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class WolfstreamExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
val url = client.newCall(
GET(url),
).execute().asJsoup().selectFirst("script:containsData(sources)")?.let {
it.data().substringAfter("{file:\"").substringBefore("\"")
} ?: return emptyList()
return listOf(
Video(url, "${prefix}WolfStream", url),
)
}
}

View file

@ -0,0 +1,11 @@
ext {
extName = 'Animension'
extClass = '.Animension'
extVersionCode = 20
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:dood-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -0,0 +1,213 @@
package eu.kanade.tachiyomi.animeextension.en.animension
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.float
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Animension() : ConfigurableAnimeSource, AnimeHttpSource() {
override val lang = "en"
override val name = "Animension"
override val baseUrl = "https://animension.to/"
override val supportsLatest = true
private val apiUrl = "https://animension.to/public-api"
private val json = Json {
ignoreUnknownKeys = true
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Popular
override fun popularAnimeRequest(page: Int): Request =
GET("$apiUrl/search.php?dub=0&sort=popular-week&page=$page")
override fun popularAnimeParse(response: Response): AnimesPage {
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
val animes = responseJson.map { anime ->
val data = anime.jsonArray
SAnime.create().apply {
title = data[0].jsonPrimitive.content
url = data[1].jsonPrimitive.content
thumbnail_url = data[2].jsonPrimitive.content
}
}
val hasNextPage = responseJson.size >= 25
return AnimesPage(animes, hasNextPage)
}
// Episode
override fun episodeListRequest(anime: SAnime): Request {
return GET("$apiUrl/episodes.php?id=${anime.url}", headers)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
val episodes = responseJson.map { episode ->
val data = episode.jsonArray
SEpisode.create().apply {
name = "Episode ${data[2]}"
url = data[1].jsonPrimitive.content
episode_number = data[2].jsonPrimitive.float
date_upload = data[3].jsonPrimitive.long.toMilli()
}
}
return episodes
}
// Video urls
override fun videoListRequest(episode: SEpisode): Request =
GET("$apiUrl/episode.php?id=${episode.url}", headers)
override fun videoListParse(response: Response): List<Video> {
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
val videos = json.decodeFromString<JsonObject>(responseJson[3].jsonPrimitive.content)
val videoList = mutableListOf<Video>()
for (key in videos.keys.toList()) {
val url = videos[key]!!.jsonPrimitive.content
when {
url.contains("dood") -> {
val video = DoodExtractor(client).videoFromUrl(url)
if (video != null) {
videoList.add(video)
}
}
}
}
return videoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoUrlParse(response: Response) = throw UnsupportedOperationException()
// Anime details
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val anime = SAnime.create()
anime.thumbnail_url = document.select("div.thumb img").attr("src")
anime.title = document.select("h1.entry-title").text()
anime.description = document.select("div.desc").text()
anime.genre = document.select("div.genxed span a").joinToString { it.text() }
anime.status = parseStatus(document.select("div.spe span:contains(Status)").text().substringAfter("Status: "))
return anime
}
// Latest
override fun latestUpdatesRequest(page: Int): Request =
GET("$apiUrl/index.php?page=$page&mode=sub")
override fun latestUpdatesParse(response: Response): AnimesPage {
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
val animes = responseJson.map { anime ->
val data = anime.jsonArray
SAnime.create().apply {
title = data[0].jsonPrimitive.content
url = data[1].jsonPrimitive.content
thumbnail_url = data[4].jsonPrimitive.content
}
}
val hasNextPage = responseJson.size >= 25
return AnimesPage(animes, hasNextPage)
}
// Search
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
GET("$apiUrl/search.php?search_text=$query&page=$page", headers)
override fun searchAnimeParse(response: Response): AnimesPage {
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
val animes = responseJson.map { anime ->
val data = anime.jsonArray
SAnime.create().apply {
title = data[0].jsonPrimitive.content
url = data[1].jsonPrimitive.content
thumbnail_url = data[2].jsonPrimitive.content
}
}
val hasNextPage = responseJson.size >= 25
return AnimesPage(animes, hasNextPage)
}
// Utilities
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Finished" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun Long.toMilli(): Long = this * 1000
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "Doodstream")
entryValues = arrayOf("1080", "720", "480", "360", "Doodstream")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}

View file

@ -0,0 +1,12 @@
ext {
extName = 'AnimeOwl'
extClass = '.AnimeOwl'
extVersionCode = 18
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:synchrony"))
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -0,0 +1,314 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.animeowl.extractors.OwlExtractor
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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
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 uy.kohesive.injekt.injectLazy
import kotlin.math.ceil
@ExperimentalSerializationApi
class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeOwl"
override val baseUrl = "https://animeowl.us"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val owlServersExtractor by lazy { OwlExtractor(client, baseUrl) }
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending?page=$page")
override fun popularAnimeSelector(): String = "div#anime-list > div"
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li > a[rel=next]"
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.select("a.title-link").attr("href"))
thumbnail_url = element.select("img[data-src]").attr("data-src")
title = element.select("a.title-link h3").text()
}
}
// =============================== Latest ===============================
override suspend fun getLatestUpdates(page: Int): AnimesPage =
advancedSearchAnime(page, sort = Sort.Latest)
override fun latestUpdatesRequest(page: Int): Request =
throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
// =============================== Search ===============================
override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage = advancedSearchAnime(page, sort = Sort.Search, query = query)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
throw UnsupportedOperationException()
override fun searchAnimeSelector(): String =
throw UnsupportedOperationException()
override fun searchAnimeNextPageSelector(): String =
throw UnsupportedOperationException()
override fun searchAnimeFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
genre = document.select("div.genre > a").joinToString { it.text() }
author = document.select("div.type > a").text()
status = parseStatus(document.select("div.status > span").text())
description = buildString {
document.select("div.anime-desc.desc-content").text()
.takeIf { it.isNotBlank() }
?.let {
appendLine(it)
appendLine()
}
document.select("h4.anime-alternatives").text()
.takeIf { it.isNotBlank() }
?.let {
append("Other name(s): ")
append(it)
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val animeId = response.asJsoup().select("div#unq-anime-id").attr("animeId")
val episodes = client.newCall(
GET("$baseUrl/api/anime/$animeId/episodes"),
).execute()
.parseAs<EpisodeResponse>()
return listOf(
episodes.sub.map { it.copy(lang = "Sub") },
episodes.dub.map { it.copy(lang = "Dub") },
).flatten()
.groupBy { it.name }
.map { (epNum, epList) ->
SEpisode.create().apply {
url = LinkData(
epList.map { ep ->
Link(
ep.buildUrl(episodes.subSlug, episodes.dubSlug),
ep.lang!!,
)
},
).toJsonString()
episode_number = epNum.toFloatOrNull() ?: 0F
name = "Episode $epNum"
}
}
.sortedByDescending { it.episode_number }
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> =
json.decodeFromString<LinkData>(episode.url)
.links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
enum class Sort(val code: String) {
Latest("1"),
Search("4"),
}
private fun advancedSearchAnime(
page: Int,
sort: Sort,
query: String? = "",
limit: Int? = 30,
): AnimesPage {
val body = buildJsonObject {
put("lang22", 3)
put("value", query)
put("sortt", sort.code)
put("limit", limit)
put("page", page - 1)
putJsonObject("selected") {
putJsonArray("type") { emptyList<String>() }
putJsonArray("sort") { emptyList<String>() }
putJsonArray("year") { emptyList<String>() }
putJsonArray("genre") { emptyList<String>() }
putJsonArray("season") { emptyList<String>() }
putJsonArray("status") { emptyList<String>() }
putJsonArray("country") { emptyList<String>() }
putJsonArray("language") { emptyList<String>() }
}
}.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val result = client.newCall(
POST("$baseUrl/api/advance-search", body = body, headers = headers),
).execute()
.parseAs<SearchResponse>()
val nextPage = ceil(result.total.toFloat() / limit!!).toInt() > page
val animes = result.results.map { anime ->
SAnime.create().apply {
setUrlWithoutDomain("/anime/${anime.animeSlug}?mal=${anime.malId}")
thumbnail_url = "$baseUrl${anime.image}"
title = anime.animeName
}
}
return AnimesPage(animes, nextPage)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareByDescending<Video> { it.quality.contains(lang) }
.thenByDescending { it.quality.contains(quality) }
.thenByDescending { it.quality.contains(server, true) },
)
}
private fun LinkData.toJsonString(): String {
return json.encodeToString(this)
}
private fun EpisodeResponse.Episode.buildUrl(subSlug: String, dubSlug: String): String =
when (lang) {
"dub" -> dubSlug
else -> subSlug
}.let { "$baseUrl/watch/$it/$episodeIndex" }
private fun parseStatus(statusString: String): Int =
when (statusString) {
"Currently Airing", "Not yet aired" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
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_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_TYPES
entryValues = PREF_LANG_TYPES
setDefaultValue(PREF_LANG_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 = PREF_SERVER_TITLE
entries = PREF_SERVER_LIST
entryValues = PREF_SERVER_LIST
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)
}
companion object {
private const val PREF_LANG_KEY = "preferred_language"
private const val PREF_LANG_TITLE = "Preferred type"
private const val PREF_LANG_DEFAULT = "Sub"
private val PREF_LANG_TYPES = arrayOf("Sub", "Dub")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "Luffy"
private val PREF_SERVER_LIST = arrayOf("Luffy", "Kaido", "Boa")
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_LIST = arrayOf("1080p", "720p", "480p", "360p")
}
}

View file

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SearchResponse(
val total: Int,
val results: List<Result>,
) {
@Serializable
data class Result(
@SerialName("mal_id")
val malId: Int,
@SerialName("anime_name")
val animeName: String,
@SerialName("anime_slug")
val animeSlug: String,
val image: String,
)
}
@Serializable
data class EpisodeResponse(
val sub: List<Episode>,
val dub: List<Episode>,
@SerialName("sub_slug")
val subSlug: String,
@SerialName("dub_slug")
val dubSlug: String,
) {
@Serializable
data class Episode(
val id: Int,
val name: String,
val lang: String? = null,
@SerialName("episode_index")
val episodeIndex: String,
)
}
@Serializable
data class LinkData(
val links: List<Link>,
)
@Serializable
data class Link(
val url: String,
val lang: String,
)
@Serializable
data class OwlServers(
val kaido: String? = null,
val luffy: String? = null,
val zoro: String? = null,
)
@Serializable
data class Stream(
val url: String,
)

View file

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.animeextension.en.animeowl.extractors
import eu.kanade.tachiyomi.animeextension.en.animeowl.Link
import eu.kanade.tachiyomi.animeextension.en.animeowl.OwlServers
import eu.kanade.tachiyomi.animeextension.en.animeowl.Stream
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import okhttp3.OkHttpClient
class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String) {
private val playlistUtils by lazy { PlaylistUtils(client) }
private val noRedirectClient by lazy {
client.newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.build()
}
suspend fun extractOwlVideo(link: Link): List<Video> {
val dataSrc = client.newCall(GET(link.url)).execute()
.asJsoup()
.select("button#hot-anime-tab")
.attr("data-source")
val epJS = dataSrc.substringAfterLast("/")
.let {
client.newCall(GET("$baseUrl/players/$it.v2.js")).execute().body.string()
}
.let(Deobfuscator::deobfuscateScript)
?: throw Exception("Unable to get clean JS")
val jwt = JWT_REGEX.find(epJS)?.groupValues?.get(1) ?: throw Exception("Unable to get jwt")
val videoList = mutableListOf<Video>()
val servers = client.newCall(GET("$baseUrl$dataSrc")).execute()
.parseAs<OwlServers>()
coroutineScope {
val lufDeferred = async {
servers.luffy?.let { luffy ->
noRedirectClient.newCall(GET("${luffy}$jwt")).execute()
.use { it.headers["Location"] }
?.let { videoList.add(Video(it, "Luffy - ${link.lang} - 1080p", it)) }
}
}
val kaiDeferred = async {
servers.kaido?.let {
videoList.addAll(
getHLS("${it}$jwt", "Kaido", link.lang),
)
}
}
val zorDeferred = async {
servers.zoro?.let {
videoList.addAll(
getHLS("${it}$jwt", "Boa", link.lang),
)
}
}
awaitAll(lufDeferred, kaiDeferred, zorDeferred)
}
return videoList
}
private fun getHLS(url: String, server: String, lang: String): List<Video> =
client.newCall(GET(url)).execute()
.parseAs<Stream>()
.url
.let {
playlistUtils.extractFromHls(
it,
videoNameGen = { qty -> "$server - $lang - $qty" },
)
}
companion object {
private val JWT_REGEX by lazy { "const\\s+(?:[A-Za-z0-9_]*)\\s*=\\s*'([^']+)'".toRegex() }
}
}

View file

@ -0,0 +1,11 @@
ext {
extName = 'AnimePahe'
extClass = '.AnimePahe'
extVersionCode = 27
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,381 @@
package eu.kanade.tachiyomi.animeextension.en.animepahe
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.animepahe.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.LatestAnimeDto
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.ResponseDto
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.SearchResultDto
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.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.ceil
import kotlin.math.floor
class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val interceptor = DdosGuardInterceptor(network.client)
override val client = network.client.newBuilder()
.addInterceptor(interceptor)
.build()
override val name = "AnimePahe"
override val baseUrl by lazy {
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
}
override val lang = "en"
override val supportsLatest = true
private val json = Json {
ignoreUnknownKeys = true
}
// =========================== Anime Details ============================
/**
* This override is necessary because AnimePahe does not provide permanent
* URLs to its animes, so we need to fetch the anime session every time.
*
* @see episodeListRequest
*/
override fun animeDetailsRequest(anime: SAnime): Request {
val animeId = anime.getId()
// We're using coroutines here to run it inside another thread and
// prevent android.os.NetworkOnMainThreadException when trying to open
// webview or share it.
val session = runBlocking {
withContext(Dispatchers.IO) {
fetchSession(anime.title, animeId)
}
}
return GET("$baseUrl/anime/$session?anime_id=$animeId")
}
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
return SAnime.create().apply {
title = document.selectFirst("div.title-wrapper > h1 > span")!!.text()
author = document.selectFirst("div.col-sm-4.anime-info p:contains(Studio:)")
?.text()
?.replace("Studio: ", "")
status = parseStatus(document.selectFirst("div.col-sm-4.anime-info p:contains(Status:) a")!!.text())
thumbnail_url = document.selectFirst("div.anime-poster a")!!.attr("href")
genre = document.select("div.anime-genre ul li").joinToString { it.text() }
val synonyms = document.selectFirst("div.col-sm-4.anime-info p:contains(Synonyms:)")
?.text()
description = document.select("div.anime-summary").text() +
if (synonyms.isNullOrEmpty()) "" else "\n\n$synonyms"
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/api?m=airing&page=$page")
override fun latestUpdatesParse(response: Response): AnimesPage {
val latestData = response.parseAs<ResponseDto<LatestAnimeDto>>()
val hasNextPage = latestData.currentPage < latestData.lastPage
val animeList = latestData.items.map { anime ->
SAnime.create().apply {
title = anime.title
thumbnail_url = anime.snapshot
val animeId = anime.id
setUrlWithoutDomain("/anime/?anime_id=$animeId")
artist = anime.fansub
}
}
return AnimesPage(animeList, hasNextPage)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
GET("$baseUrl/api?m=search&l=8&q=$query")
override fun searchAnimeParse(response: Response): AnimesPage {
val searchData = response.parseAs<ResponseDto<SearchResultDto>>()
val animeList = searchData.items.map { anime ->
SAnime.create().apply {
title = anime.title
thumbnail_url = anime.poster
val animeId = anime.id
setUrlWithoutDomain("/anime/?anime_id=$animeId")
}
}
return AnimesPage(animeList, false)
}
// ============================== Popular ===============================
// This source doesnt have a popular animes page,
// so we use latest animes page instead.
override suspend fun getPopularAnime(page: Int) = getLatestUpdates(page)
override fun popularAnimeParse(response: Response): AnimesPage = TODO()
override fun popularAnimeRequest(page: Int): Request = TODO()
// ============================== Episodes ==============================
/**
* This override is necessary because AnimePahe does not provide permanent
* URLs to its animes, so we need to fetch the anime session every time.
*
* @see animeDetailsRequest
*/
override fun episodeListRequest(anime: SAnime): Request {
val session = fetchSession(anime.title, anime.getId())
return GET("$baseUrl/api?m=release&id=$session&sort=episode_desc&page=1")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val url = response.request.url.toString()
val session = url.substringAfter("&id=").substringBefore("&")
return recursivePages(response, session)
}
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
return episodes.map { episode ->
SEpisode.create().apply {
date_upload = episode.createdAt.toDate()
val session = episode.session
setUrlWithoutDomain("/play/$animeSession/$session")
val epNum = episode.episodeNumber
episode_number = epNum
val epName = if (floor(epNum) == ceil(epNum)) {
epNum.toInt().toString()
} else {
epNum.toString()
}
name = "Episode $epName"
}
}.toMutableList()
}
private fun recursivePages(response: Response, animeSession: String): List<SEpisode> {
val episodesData = response.parseAs<ResponseDto<EpisodeDto>>()
val page = episodesData.currentPage
val hasNextPage = page < episodesData.lastPage
val returnList = parseEpisodePage(episodesData.items, animeSession)
if (hasNextPage) {
val nextPage = nextPageRequest(response.request.url.toString(), page + 1)
returnList += recursivePages(nextPage, animeSession)
}
return returnList
}
private fun nextPageRequest(url: String, page: Int): Response {
val request = GET(url.substringBeforeLast("&page=") + "&page=$page")
return client.newCall(request).execute()
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val downloadLinks = document.select("div#pickDownload > a")
return document.select("div#resolutionMenu > button").mapIndexed { index, btn ->
val kwikLink = btn.attr("data-src")
val quality = btn.text()
val paheWinLink = downloadLinks[index].attr("href")
getVideo(paheWinLink, kwikLink, quality)
}
}
private fun getVideo(paheUrl: String, kwikUrl: String, quality: String): Video {
return if (preferences.getBoolean(PREF_LINK_TYPE_KEY, PREF_LINK_TYPE_DEFAULT)) {
val videoUrl = KwikExtractor(client).getHlsStreamUrl(kwikUrl, referer = baseUrl)
Video(
videoUrl,
quality,
videoUrl,
headers = Headers.headersOf("referer", "https://kwik.cx"),
)
} else {
val videoUrl = KwikExtractor(client).getStreamUrlFromKwik(paheUrl)
Video(videoUrl, quality, videoUrl)
}
}
override fun List<Video>.sort(): List<Video> {
val subPreference = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val shouldBeAv1 = preferences.getBoolean(PREF_AV1_KEY, PREF_AV1_DEFAULT)
val shouldEndWithEng = subPreference == "eng"
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ Regex("""\beng\b""").containsMatchIn(it.quality.lowercase()) == shouldEndWithEng },
{ it.quality.lowercase().contains("av1") == shouldBeAv1 },
),
).reversed()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = 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()
}
}
val domainPref = ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_VALUES
setDefaultValue(PREF_DOMAIN_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()
}
}
val subPref = ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_ENTRIES
entryValues = PREF_SUB_VALUES
setDefaultValue(PREF_SUB_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()
}
}
val linkPref = SwitchPreferenceCompat(screen.context).apply {
key = PREF_LINK_TYPE_KEY
title = PREF_LINK_TYPE_TITLE
summary = PREF_LINK_TYPE_SUMMARY
setDefaultValue(PREF_LINK_TYPE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}
val av1Pref = SwitchPreferenceCompat(screen.context).apply {
key = PREF_AV1_KEY
title = PREF_AV1_TITLE
summary = PREF_AV1_SUMMARY
setDefaultValue(PREF_AV1_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(domainPref)
screen.addPreference(subPref)
screen.addPreference(linkPref)
screen.addPreference(av1Pref)
}
// ============================= Utilities ==============================
private fun fetchSession(title: String, animeId: String): String {
return client.newCall(GET("$baseUrl/api?m=search&q=$title"))
.execute()
.body.string()
.substringAfter("\"id\":$animeId")
.substringAfter("\"session\":\"")
.substringBefore("\"")
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Currently Airing" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun SAnime.getId() = url.substringAfterLast("?anime_id=").substringBefore("\"")
private fun String.toDate(): Long {
return runCatching {
DATE_FORMATTER.parse(this)?.time ?: 0L
}.getOrNull() ?: 0L
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "preffered_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "360p")
private const val PREF_DOMAIN_KEY = "preffered_domain"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private const val PREF_DOMAIN_DEFAULT = "https://animepahe.com"
private val PREF_DOMAIN_ENTRIES = arrayOf("animepahe.com", "animepahe.ru", "animepahe.org")
private val PREF_DOMAIN_VALUES by lazy {
PREF_DOMAIN_ENTRIES.map { "https://" + it }.toTypedArray()
}
private const val PREF_SUB_KEY = "preffered_sub"
private const val PREF_SUB_TITLE = "Prefer subs or dubs?"
private const val PREF_SUB_DEFAULT = "jpn"
private val PREF_SUB_ENTRIES = arrayOf("sub", "dub")
private val PREF_SUB_VALUES = arrayOf("jpn", "eng")
private const val PREF_LINK_TYPE_KEY = "preffered_link_type"
private const val PREF_LINK_TYPE_TITLE = "Use HLS links"
private const val PREF_LINK_TYPE_DEFAULT = false
private val PREF_LINK_TYPE_SUMMARY by lazy {
"""Enable this if you are having Cloudflare issues.
|Note that this will break the ability to seek inside of the video unless the episode is downloaded in advance.
""".trimMargin()
}
// Big slap to whoever misspelled `preferred`
private const val PREF_AV1_KEY = "preffered_av1"
private const val PREF_AV1_TITLE = "Use AV1 codec"
private const val PREF_AV1_DEFAULT = false
private val PREF_AV1_SUMMARY by lazy {
"""Enable to use AV1 if available
|Turn off to never select av1 as preferred codec
""".trimMargin()
}
}
}

View file

@ -0,0 +1,69 @@
package eu.kanade.tachiyomi.animeextension.en.animepahe
import android.webkit.CookieManager
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
class DdosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieManager by lazy { CookieManager.getInstance() }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
// Check if DDos-GUARD is on
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
return response
}
response.close()
val cookies = cookieManager.getCookie(originalRequest.url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
} else {
emptyList()
}
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return chain.proceed(originalRequest)
}
val newCookie = getNewCookie(originalRequest.url) ?: return chain.proceed(originalRequest)
val newCookieHeader = (oldCookie + newCookie).joinToString("; ") {
"${it.name}=${it.value}"
}
return chain.proceed(originalRequest.newBuilder().addHeader("cookie", newCookieHeader).build())
}
fun getNewCookie(url: HttpUrl): Cookie? {
val cookies = cookieManager.getCookie(url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
} else {
emptyList()
}
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return ddg2Cookie
}
val wellKnown = client.newCall(GET("https://check.ddos-guard.net/check.js"))
.execute().body.string()
.substringAfter("'", "")
.substringBefore("'", "")
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
return client.newCall(GET(checkUrl)).execute().header("set-cookie")?.let {
Cookie.parse(url, it)
}
}
companion object {
private val ERROR_CODES = listOf(403)
private val SERVER_CHECK = listOf("ddos-guard")
}
}

View file

@ -0,0 +1,159 @@
/** The following file is slightly modified and taken from: https://github.com/LagradOst/CloudStream-3/blob/4d6050219083d675ba9c7088b59a9492fcaa32c7/app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimePaheProvider.kt
* It is published under the following license:
*
MIT License
Copyright (c) 2021 Osten
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*
*/
package eu.kanade.tachiyomi.animeextension.en.animepahe
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Response
import kotlin.math.pow
class KwikExtractor(private val client: OkHttpClient) {
private var cookies: String = ""
private val kwikParamsRegex = Regex("""\("(\w+)",\d+,"(\w+)",(\d+),(\d+),\d+\)""")
private val kwikDUrl = Regex("action=\"([^\"]+)\"")
private val kwikDToken = Regex("value=\"([^\"]+)\"")
private fun isNumber(s: String?): Boolean {
return s?.toIntOrNull() != null
}
fun getHlsStreamUrl(kwikUrl: String, referer: String): String {
val eContent = client.newCall(GET(kwikUrl, Headers.headersOf("referer", referer)))
.execute().asJsoup()
val script = eContent.selectFirst("script:containsData(eval\\(function)")!!.data().substringAfterLast("eval(function(")
val unpacked = JsUnpacker.unpackAndCombine("eval(function($script")!!
return unpacked.substringAfter("const source=\\'").substringBefore("\\';")
}
fun getStreamUrlFromKwik(paheUrl: String): String {
val noRedirects = client.newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.build()
val kwikUrl = "https://" + noRedirects.newCall(GET("$paheUrl/i")).execute()
.header("location")!!.substringAfterLast("https://")
val fContent =
client.newCall(GET(kwikUrl, Headers.headersOf("referer", "https://kwik.cx/"))).execute()
cookies += fContent.header("set-cookie")!!
val fContentString = fContent.body.string()
val (fullString, key, v1, v2) = kwikParamsRegex.find(fContentString)!!.destructured
val decrypted = decrypt(fullString, key, v1.toInt(), v2.toInt())
val uri = kwikDUrl.find(decrypted)!!.destructured.component1()
val tok = kwikDToken.find(decrypted)!!.destructured.component1()
var content: Response? = null
var code = 419
var tries = 0
val noRedirectClient = OkHttpClient().newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.cookieJar(client.cookieJar)
.build()
while (code != 302 && tries < 20) {
content = noRedirectClient.newCall(
POST(
uri,
Headers.headersOf(
"referer",
fContent.request.url.toString(),
"cookie",
fContent.header("set-cookie")!!.replace("path=/;", ""),
),
FormBody.Builder().add("_token", tok).build(),
),
).execute()
code = content.code
++tries
}
if (tries > 19) {
throw Exception("Failed to extract the stream uri from kwik.")
}
val location = content?.header("location").toString()
content?.close()
return location
}
private fun decrypt(fullString: String, key: String, v1: Int, v2: Int): String {
var r = ""
var i = 0
while (i < fullString.length) {
var s = ""
while (fullString[i] != key[v2]) {
s += fullString[i]
++i
}
var j = 0
while (j < key.length) {
s = s.replace(key[j].toString(), j.toString())
++j
}
r += (getString(s, v2).toInt() - v1).toChar()
++i
}
return r
}
private fun getString(content: String, s1: Int): String {
val s2 = 10
val characterMap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
val slice2 = characterMap.slice(0 until s2)
var acc: Long = 0
for ((n, i) in content.reversed().withIndex()) {
acc += when (isNumber("$i")) {
true -> "$i".toLong()
false -> 0L
} * s1.toDouble().pow(n.toDouble()).toInt()
}
var k = ""
while (acc > 0) {
k = slice2[(acc % s2).toInt()] + k
acc = (acc - (acc % s2)) / s2
}
return when (k != "") {
true -> k
false -> "0"
}
}
}

View file

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.en.animepahe.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ResponseDto<T>(
@SerialName("current_page")
val currentPage: Int,
@SerialName("last_page")
val lastPage: Int,
@EncodeDefault
@SerialName("data")
val items: List<T> = emptyList(),
)
@Serializable
data class LatestAnimeDto(
@SerialName("anime_title")
val title: String,
val snapshot: String,
@SerialName("anime_id")
val id: Int,
val fansub: String,
)
@Serializable
data class SearchResultDto(
val title: String,
val poster: String,
val id: Int,
)
@Serializable
data class EpisodeDto(
@SerialName("created_at")
val createdAt: String,
val session: String,
@SerialName("episode")
val episodeNumber: Float,
)

View file

@ -0,0 +1,7 @@
ext {
extName = 'AnimeParadise'
extClass = '.AnimeParadise'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Some files were not shown because too many files have changed in this diff Show more