diff --git a/src/en/allanimechi/AndroidManifest.xml b/src/en/allanimechi/AndroidManifest.xml new file mode 100644 index 0000000000..568741e54f --- /dev/null +++ b/src/en/allanimechi/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/en/allanimechi/build.gradle b/src/en/allanimechi/build.gradle new file mode 100644 index 0000000000..298526f506 --- /dev/null +++ b/src/en/allanimechi/build.gradle @@ -0,0 +1,26 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} + +ext { + extName = 'AllAnimeChi' + pkgNameSuffix = 'en.allanimechi' + extClass = '.AllAnimeChi' + extVersionCode = 1 + libVersion = '13' +} + +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')) +} + + +apply from: "$rootDir/common.gradle" diff --git a/src/en/allanimechi/res/mipmap-hdpi/ic_launcher.png b/src/en/allanimechi/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..4a4cf9bf7a Binary files /dev/null and b/src/en/allanimechi/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/allanimechi/res/mipmap-mdpi/ic_launcher.png b/src/en/allanimechi/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..be7a1c19e4 Binary files /dev/null and b/src/en/allanimechi/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/allanimechi/res/mipmap-xhdpi/ic_launcher.png b/src/en/allanimechi/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..3603cefed6 Binary files /dev/null and b/src/en/allanimechi/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/allanimechi/res/mipmap-xxhdpi/ic_launcher.png b/src/en/allanimechi/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..531455438f Binary files /dev/null and b/src/en/allanimechi/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/allanimechi/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/allanimechi/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..c9dfc3051b Binary files /dev/null and b/src/en/allanimechi/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/allanimechi/res/web_hi_res_512.png b/src/en/allanimechi/res/web_hi_res_512.png new file mode 100644 index 0000000000..337bc317b4 Binary files /dev/null and b/src/en/allanimechi/res/web_hi_res_512.png differ diff --git a/src/en/allanimechi/src/eu/kanade/tachiyomi/animeextension/en/allanimechi/AllAnimeChi.kt b/src/en/allanimechi/src/eu/kanade/tachiyomi/animeextension/en/allanimechi/AllAnimeChi.kt new file mode 100644 index 0000000000..c4ee7130e0 --- /dev/null +++ b/src/en/allanimechi/src/eu/kanade/tachiyomi/animeextension/en/allanimechi/AllAnimeChi.kt @@ -0,0 +1,671 @@ +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.network.asObservableSuccess +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +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.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy + +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 + + override val client: OkHttpClient = network.cloudflareClient + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().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() + 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() + 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 fetchAnimeDetails(anime: SAnime): Observable { + return client.newCall(animeDetailsRequestInternal(anime)) + .asObservableSuccess() + .map { response -> + animeDetailsParse(response).apply { initialized = true } + } + } + + private fun animeDetailsRequestInternal(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 animeDetailsRequest(anime: SAnime): Request { + return GET("data:text/plain,This%20extension%20does%20not%20exist%20as%20a%20website%21") + } + + override fun animeDetailsParse(response: Response): SAnime { + val show = response.parseAs().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("
", "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 = animeDetailsRequestInternal(anime) + + override fun episodeListParse(response: Response): List { + val media = response.parseAs() + 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) } + + 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