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