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