diff --git a/src/en/uhdmovies/AndroidManifest.xml b/src/en/uhdmovies/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/en/uhdmovies/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/uhdmovies/build.gradle b/src/en/uhdmovies/build.gradle
new file mode 100644
index 000000000..8bfa4ee1a
--- /dev/null
+++ b/src/en/uhdmovies/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'UHD Movies (Experimental)'
+ pkgNameSuffix = 'en.uhdmovies'
+ extClass = '.UHDMovies'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+dependencies {
+ compileOnly libs.bundles.coroutines
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/uhdmovies/res/mipmap-anydpi-v26/ic_launcher.xml b/src/en/uhdmovies/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..90f958096
--- /dev/null
+++ b/src/en/uhdmovies/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher.png b/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..d0f680344
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher_adaptive_back.png b/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..19669488f
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher_adaptive_fore.png b/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..9a991a4c1
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-hdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher.png b/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..169e9211d
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher_adaptive_back.png b/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..75025cfd5
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher_adaptive_fore.png b/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..d56dbc96b
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-mdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher.png b/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..d7587ff17
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher_adaptive_back.png b/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..9784f16c8
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..30e281f5d
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher.png b/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..afc976ea2
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..04ef206c8
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..61c6bb99d
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..798bdeb7f
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
new file mode 100644
index 000000000..66a5487a2
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ
diff --git a/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
new file mode 100644
index 000000000..5690a1f5f
Binary files /dev/null and b/src/en/uhdmovies/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ
diff --git a/src/en/uhdmovies/res/play_store_512.png b/src/en/uhdmovies/res/play_store_512.png
new file mode 100644
index 000000000..42b1f7c6e
Binary files /dev/null and b/src/en/uhdmovies/res/play_store_512.png differ
diff --git a/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/TokenInterceptor.kt b/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/TokenInterceptor.kt
new file mode 100644
index 000000000..7919800ae
--- /dev/null
+++ b/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/TokenInterceptor.kt
@@ -0,0 +1,88 @@
+package eu.kanade.tachiyomi.animeextension.en.uhdmovies
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.os.Handler
+import android.os.Looper
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import eu.kanade.tachiyomi.network.GET
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.concurrent.CountDownLatch
+
+class TokenInterceptor : Interceptor {
+
+ private val context = Injekt.get()
+ private val handler by lazy { Handler(Looper.getMainLooper()) }
+
+ class JsObject(private val latch: CountDownLatch, var payload: String = "") {
+ @JavascriptInterface
+ fun passPayload(passedPayload: String) {
+ payload = passedPayload
+ latch.countDown()
+ }
+ }
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+
+ val newRequest = resolveWithWebView(originalRequest) ?: originalRequest
+
+ return chain.proceed(newRequest)
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun resolveWithWebView(request: Request): Request? {
+
+ val latch = CountDownLatch(1)
+
+ var webView: WebView? = null
+
+ val origRequestUrl = request.url.toString()
+
+ val jsinterface = JsObject(latch)
+
+ // Get url with token with promise
+ val jsScript = """
+ (async () => {
+ var data = await generate("direct");
+ window.android.passPayload(data.url);
+ })();""".trim()
+
+ val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
+
+ handler.post {
+ val webview = WebView(context)
+ webView = webview
+ with(webview.settings) {
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ databaseEnabled = true
+ useWideViewPort = false
+ loadWithOverviewMode = false
+ userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0"
+ webview.addJavascriptInterface(jsinterface, "android")
+ webview.webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ view?.evaluateJavascript(jsScript) {}
+ }
+ }
+ webView?.loadUrl(origRequestUrl, headers)
+ }
+ }
+
+ latch.await()
+
+ handler.post {
+ webView?.stopLoading()
+ webView?.destroy()
+ webView = null
+ }
+ return if (jsinterface.payload.isNotBlank()) GET(jsinterface.payload) else null
+ }
+}
diff --git a/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt b/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
new file mode 100644
index 000000000..6c617e299
--- /dev/null
+++ b/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
@@ -0,0 +1,378 @@
+package eu.kanade.tachiyomi.animeextension.en.uhdmovies
+
+import android.app.Application
+import android.content.SharedPreferences
+import android.util.Base64
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+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.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+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.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import okhttp3.FormBody
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+
+@ExperimentalSerializationApi
+class UHDMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "UHD Movies (Experimental)"
+
+ override val baseUrl by lazy { preferences.getString("preferred_domain", "https://uhdmovies.org.in")!! }
+
+ override val lang = "en"
+
+ override val supportsLatest = false
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val json: Json by injectLazy()
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+
+ override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/page/$page/")
+
+ override fun popularAnimeSelector(): String = "div#content div.gridlove-posts > div"
+
+ override fun popularAnimeNextPageSelector(): String =
+ "div#content > nav.gridlove-pagination > a.next"
+
+ override fun popularAnimeFromElement(element: Element): SAnime {
+ return SAnime.create().apply {
+ setUrlWithoutDomain(element.select("div.entry-image > a").attr("abs:href"))
+ thumbnail_url = element.select("div.entry-image > a > img").attr("abs:src")
+ title = element.select("div.entry-image > a").attr("title")
+ .replace("Download", "").trim()
+ }
+ }
+
+ // =============================== Latest ===============================
+
+ override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used")
+
+ override fun latestUpdatesSelector(): String = throw Exception("Not Used")
+
+ override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
+
+ override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used")
+
+ // =============================== Search ===============================
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ val cleanQuery = query.replace(" ", "+").lowercase()
+ return GET("$baseUrl/page/$page/?s=$cleanQuery")
+ }
+
+ override fun searchAnimeSelector(): String = popularAnimeSelector()
+
+ override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
+
+ override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
+
+ // =========================== Anime Details ============================
+
+ override fun animeDetailsParse(document: Document): SAnime {
+ return SAnime.create()
+ }
+
+ // ============================== Episodes ==============================
+
+ override fun fetchEpisodeList(anime: SAnime): Observable> {
+ val response = client.newCall(GET(baseUrl + anime.url)).execute()
+ val resp = response.asJsoup()
+ val episodeList = mutableListOf()
+ val episodeElements = resp.select("p:has(a[href^=https://href.li])[style*=center]")
+ val qualityRegex = "[0-9]{3,4}p".toRegex(RegexOption.IGNORE_CASE)
+ if (episodeElements.first().text().contains("Episode", true) ||
+ episodeElements.first().text().contains("Zip", true)
+ ) {
+ episodeElements.map { row ->
+ val prevP = row.previousElementSibling()
+
+ val seasonRegex = "[ .]S(?:eason)?[ .]?([0-9]{1,2})[ .]".toRegex(RegexOption.IGNORE_CASE)
+ val result = seasonRegex.find(prevP.text())
+ val season = (
+ result?.groups?.get(1)?.value ?: let {
+ val prevPre = row.previousElementSiblings().prev("pre")
+ val preResult = seasonRegex.find(prevPre.first().text())
+ preResult?.groups?.get(1)?.value ?: let {
+ val title = resp.select("h1.entry-title")
+ val titleResult = "[ .\\[(]S(?:eason)?[ .]?([0-9]{1,2})[ .\\])]".toRegex(RegexOption.IGNORE_CASE).find(title.text())
+ titleResult?.groups?.get(1)?.value ?: "-1"
+ }
+ }
+ ).replaceFirst("^0+(?!$)".toRegex(), "")
+
+ val qualityMatch = qualityRegex.find(prevP.text())
+ val quality = qualityMatch?.value ?: "HD"
+
+ row.select("a").filter {
+ !it.text().contains("Zip", true) &&
+ !it.text().contains("Pack", true)
+ }.map { linkElement ->
+ val episode = linkElement.text().replace("Episode", "", true).trim()
+ Triple(
+ season + "_$episode",
+ linkElement.attr("href")!!.substringAfter("?id="),
+ quality
+ )
+ }
+ }.flatten().groupBy { it.first }.map { group ->
+ val (season, episode) = group.key.split("_")
+ episodeList.add(
+ SEpisode.create().apply {
+ url = EpLinks(
+ urls = group.value.map {
+ EpUrl(url = it.second, quality = it.third)
+ }
+ ).toJson()
+ name = "Season $season Ep $episode"
+ episode_number = episode.toFloat()
+ }
+ )
+ }
+ } else {
+ episodeElements.filter {
+ !it.text().contains("Zip", true) &&
+ !it.text().contains("Pack", true)
+ }.map { row ->
+ val prevP = row.previousElementSibling()
+ val qualityMatch = qualityRegex.find(prevP.text())
+ val quality = qualityMatch?.value ?: "HD"
+
+ row.select("a").map { linkElement ->
+ Pair(linkElement.attr("href")!!.substringAfter("?id="), quality)
+ }
+ }.flatten().let { link ->
+ episodeList.add(
+ SEpisode.create().apply {
+ url = EpLinks(
+ urls = link.map {
+ EpUrl(url = it.first, quality = it.second)
+ }
+ ).toJson()
+ name = "Movie"
+ episode_number = 0F
+ }
+ )
+ }
+ if (episodeList.isEmpty()) throw Exception("Only Zip Pack Available")
+ }
+ return Observable.just(episodeList.reversed())
+ }
+
+ override fun episodeListSelector(): String = throw Exception("Not Used")
+
+ override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not Used")
+
+ // ============================ Video Links =============================
+
+ override fun fetchVideoList(episode: SEpisode): Observable> {
+ val urlJson = json.decodeFromString(episode.url)
+ val failedMediaUrl = mutableListOf>()
+ val videoList = mutableListOf