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