Add new source UHDMovies (#1179)

This commit is contained in:
Samfun75
2023-01-17 17:11:43 +03:00
committed by GitHub
parent 54e209fcd7
commit 1423151d1f
21 changed files with 490 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -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"

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -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<Application>()
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
}
}

View File

@ -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<Application>().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<List<SEpisode>> {
val response = client.newCall(GET(baseUrl + anime.url)).execute()
val resp = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
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<List<Video>> {
val urlJson = json.decodeFromString<EpLinks>(episode.url)
val failedMediaUrl = mutableListOf<Pair<String, String>>()
val videoList = mutableListOf<Video>()
videoList.addAll(
urlJson.urls.parallelMap { url ->
runCatching {
val (videos, mediaUrl) = extractVideo(url)
if (videos.isEmpty()) failedMediaUrl.add(Pair(mediaUrl, url.quality))
return@runCatching videos
}.getOrNull()
}
.filterNotNull()
.flatten()
)
videoList.addAll(
failedMediaUrl.mapNotNull { (url, quality) ->
runCatching {
extractGDriveLink(url, quality)
}.getOrNull()
}.flatten()
)
return Observable.just(videoList.sort())
}
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ==============================
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val postLink = "https://blog.officialboypalak.in/"
val formData = FormBody.Builder().add("_wp_http", epUrl.url).build()
val response = client.newCall(POST(postLink, body = formData)).execute().asJsoup()
val link = response.selectFirst("form#landing").attr("action")
val wpHttp = response.selectFirst("input[name=_wp_http2]").attr("value")
val token = response.selectFirst("input[name=token]").attr("value")
val blogFormData = FormBody.Builder()
.add("_wp_http2", wpHttp)
.add("token", token)
.build()
val blogResponse = client.newCall(POST(link, body = blogFormData)).execute().body!!.string()
val skToken = blogResponse.substringAfter("?go=").substringBefore("\"")
val tokenUrl = "$postLink?go=$skToken"
val cookieHeader = Headers.headersOf("Cookie", "$skToken=$wpHttp")
val tokenResponse = client.newBuilder().followRedirects(false).build()
.newCall(GET(tokenUrl, cookieHeader)).execute().asJsoup()
val redirectUrl = tokenResponse.select("meta[http-equiv=refresh]").attr("content")
.substringAfter("url=").substringBefore("\"")
val mediaResponse = client.newBuilder().followRedirects(false).build()
.newCall(GET(redirectUrl)).execute()
val path = mediaResponse.body!!.string().substringAfter("replace(\"").substringBefore("\"")
val mediaUrl = "https://" + mediaResponse.request.url.host + path
val videoList = mutableListOf<Video>()
for (type in 1..3) {
videoList.addAll(
extractWorkerLinks(mediaUrl, epUrl.quality, type)
)
}
return Pair(videoList, mediaUrl)
}
private val sizeRegex = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = sizeRegex.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href")
val decodedLink = if (link.contains("workers.dev")) {
link
} else {
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
}
Video(
url = decodedLink,
quality = "$quality - CF $type Worker ${index + 1}$size",
videoUrl = decodedLink
)
}
}
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val tokenClient = client.newBuilder().addInterceptor(TokenInterceptor()).build()
val response = tokenClient.newCall(GET(mediaUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")
val gdLink = gdBtn.attr("href")
val sizeMatch = sizeRegex.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) {
listOf()
} else {
val realLink = link.attr("action")
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
val newList = mutableListOf<Video>()
if (quality != null) {
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("uhdmovies.org.in")
entryValues = arrayOf("https://uhdmovies.org.in")
setDefaultValue("https://uhdmovies.org.in")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("2160p", "1080p", "720p", "480p")
entryValues = arrayOf("2160", "1080", "720", "480")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
}
@Serializable
data class EpLinks(
val urls: List<EpUrl>
)
@Serializable
data class EpUrl(
val quality: String,
val url: String
)
private fun EpLinks.toJson(): String {
return json.encodeToString(this)
}
// From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
}