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