diff --git a/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt b/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt index 5859d3efe..d0275ed8b 100644 --- a/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt +++ b/lib/cryptoaes/src/main/java/eu/kanade/tachiyomi/lib/cryptoaes/CryptoAES.kt @@ -94,6 +94,22 @@ object CryptoAES { } } + /** + * Encrypt using CryptoJS defaults compatible method. + * + * @param plainText plaintext + * @param keyBytes key as a bytearray + * @param ivBytes iv as a bytearray + */ + fun encrypt(plainText: String, keyBytes: ByteArray, ivBytes: ByteArray): String { + return try { + val cipherTextBytes = plainText.toByteArray() + encryptAES(cipherTextBytes, keyBytes, ivBytes) + } catch (e: Exception) { + "" + } + } + /** * Decrypt using CryptoJS defaults compatible method. * @@ -114,6 +130,26 @@ object CryptoAES { } } + /** + * Encrypt using CryptoJS defaults compatible method. + * + * @param plainTextBytes encrypted text as a bytearray + * @param keyBytes key as a bytearray + * @param ivBytes iv as a bytearray + */ + private fun encryptAES(plainTextBytes: ByteArray, keyBytes: ByteArray, ivBytes: ByteArray): String { + return try { + val cipher = try { + Cipher.getInstance(HASH_CIPHER) + } catch (e: Throwable) { Cipher.getInstance(HASH_CIPHER_FALLBACK) } + val keyS = SecretKeySpec(keyBytes, AES) + cipher.init(Cipher.ENCRYPT_MODE, keyS, IvParameterSpec(ivBytes)) + Base64.encodeToString(cipher.doFinal(plainTextBytes), Base64.DEFAULT) + } catch (e: Exception) { + "" + } + } + /** * Generates a key and an initialization vector (IV) with the given salt and password. * diff --git a/src/en/asiaflix/AndroidManifest.xml b/src/en/asiaflix/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/src/en/asiaflix/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/en/asiaflix/build.gradle b/src/en/asiaflix/build.gradle new file mode 100644 index 000000000..f767bba69 --- /dev/null +++ b/src/en/asiaflix/build.gradle @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} + +ext { + extName = 'AsiaFlix' + pkgNameSuffix = 'en.asiaflix' + extClass = '.AsiaFlix' + extVersionCode = 1 +} + +dependencies { + implementation(project(':lib-cryptoaes')) + implementation(project(':lib-playlist-utils')) + implementation(project(':lib-streamwish-extractor')) + implementation(project(':lib-dood-extractor')) + implementation(project(':lib-streamtape-extractor')) + implementation(project(':lib-mixdrop-extractor')) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/asiaflix/res/mipmap-hdpi/ic_launcher.png b/src/en/asiaflix/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..be5530f9e Binary files /dev/null and b/src/en/asiaflix/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/asiaflix/res/mipmap-mdpi/ic_launcher.png b/src/en/asiaflix/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a6c085f67 Binary files /dev/null and b/src/en/asiaflix/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/asiaflix/res/mipmap-xhdpi/ic_launcher.png b/src/en/asiaflix/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..85a527f84 Binary files /dev/null and b/src/en/asiaflix/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/asiaflix/res/mipmap-xxhdpi/ic_launcher.png b/src/en/asiaflix/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..29527629a Binary files /dev/null and b/src/en/asiaflix/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/asiaflix/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/asiaflix/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..74ebdbd47 Binary files /dev/null and b/src/en/asiaflix/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/asiaflix/res/web_hi_res_512.png b/src/en/asiaflix/res/web_hi_res_512.png new file mode 100644 index 000000000..a7cebae2b Binary files /dev/null and b/src/en/asiaflix/res/web_hi_res_512.png differ diff --git a/src/en/asiaflix/src/eu/kanade/tachiyomi/animeextension/en/asiaflix/AsiaFlix.kt b/src/en/asiaflix/src/eu/kanade/tachiyomi/animeextension/en/asiaflix/AsiaFlix.kt new file mode 100644 index 000000000..98731c9dd --- /dev/null +++ b/src/en/asiaflix/src/eu/kanade/tachiyomi/animeextension/en/asiaflix/AsiaFlix.kt @@ -0,0 +1,315 @@ +package eu.kanade.tachiyomi.animeextension.en.asiaflix + +import android.app.Application +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.DetailsResponseDto +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.EncryptedResponseDto +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.EpisodeResponseDto +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.SearchDto +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.SearchEntry +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.SourceDto +import eu.kanade.tachiyomi.animeextension.en.asiaflix.dto.StreamHeadDto +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.cryptoaes.CryptoAES +import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor +import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils +import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.floatOrNull +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.util.Locale +import kotlin.math.min + +class AsiaFlix : AnimeHttpSource(), ConfigurableAnimeSource { + + override val name = "AsiaFlix" + + override val baseUrl = "https://asiaflix.app" + + private val apiUrl = "https://api.asiaflix.app/api/v2" + + override val lang = "en" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + override val client = network.cloudflareClient + + private val apiHeaders by lazy { + headersBuilder() + .set("Accept", "application/json, text/plain, */*") + .set("Referer", "$baseUrl/") + .set("Origin", baseUrl) + .set("X-Requested-By", "asiaflix-web") + .build() + } + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int): Request { + val url = "$apiUrl/drama/explore/full?schedule=0&sort=1&fields=name,+image,+altNames,+synopsis,+genre,+tvStatus&limit=$LIMIT&page=$page" + + return GET(url, apiHeaders) + } + + override fun popularAnimeParse(response: Response): AnimesPage { + val result = response.parseAs>() + val series = result[1].parseAs>() + + val entries = series.map(DetailsResponseDto::toSAnime) + val hasNextPage = entries.size == LIMIT + + return AnimesPage(entries, hasNextPage) + } + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int): Request { + val url = "$apiUrl/drama/explore/full?schedule=0&sort=3&fields=name,+image,+altNames,+synopsis,+genre,+tvStatus&limit=$LIMIT&page=$page" + + return GET(url, apiHeaders) + } + + override fun latestUpdatesParse(response: Response) = popularAnimeParse(response) + + // =============================== Search =============================== + private lateinit var searchEntries: SearchDto + + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (page == 1) { + super.fetchSearchAnime(page, query, filters) + } else { + Observable.just(paginatedSearchParse(page)) + } + } + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val url = "$apiUrl/drama/search".toHttpUrl().newBuilder() + .addQueryParameter("q", query.trim()) + .build() + .toString() + + return GET(url, apiHeaders) + } + + override fun searchAnimeParse(response: Response): AnimesPage { + searchEntries = response.parseAs() + + return paginatedSearchParse(1) + } + + private fun paginatedSearchParse(page: Int): AnimesPage { + val end = min(page * 20, searchEntries.size) + val entries = searchEntries.subList((page - 1) * 20, end).map(SearchEntry::toSAnime) + + return AnimesPage(entries, end < searchEntries.size) + } + + // =========================== Anime Details ============================ + override fun fetchAnimeDetails(anime: SAnime): Observable { + return client.newCall(internalAnimeDetailsRequest(anime)) + .asObservableSuccess() + .map(::animeDetailsParse) + } + + // workaround to get correct WebView url + override fun animeDetailsRequest(anime: SAnime): Request { + val slug = anime.title.titleToSlug() + return GET("$baseUrl/show-details/$slug/${anime.url}") + } + + private fun internalAnimeDetailsRequest(anime: SAnime): Request { + return GET("$apiUrl/drama?id=${anime.url}", apiHeaders) + } + + override fun animeDetailsParse(response: Response): SAnime { + return response.parseAs().toSAnime() + } + + // ============================== Episodes ============================== + override fun episodeListRequest(anime: SAnime) = internalAnimeDetailsRequest(anime) + + override fun episodeListParse(response: Response): List { + val result = response.parseAs() + + return result.episodes.map { + SEpisode.create().apply { + name = "Episode ${it.number}" + episode_number = it.number.floatOrNull ?: -1f + scanlator = it.sub + url = it.url + } + }.sortedByDescending { it.episode_number } + } + + // ============================ Video Links ============================= + private val streamHead by lazy { + client.newCall(GET("$apiUrl/utility/get-stream-headers", apiHeaders)) + .execute() + .parseAs() + .source + } + + override fun videoListRequest(episode: SEpisode): Request { + return GET(streamHead + episode.url, headers) + } + + private val playlistUtils by lazy { PlaylistUtils(client, headers) } + private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) } + private val doodStreamExtractor by lazy { DoodExtractor(client) } + private val streamTapeExtractor by lazy { StreamTapeExtractor(client) } + private val mixDropExtractor by lazy { MixDropExtractor(client) } + + override fun videoListParse(response: Response): List