diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 10fc1854..8917647f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,6 +233,7 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib } tasks.register("retrieveCommitHash") { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt index c3af8bee..3f910cd4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.* open class Acefile : ExtractorApi() { @@ -16,22 +15,19 @@ open class Acefile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val script = getAndUnpack(app.get(url).text) - val id = script.substringAfter("\"code\":\"").substringBefore("\",") - val doc = app.get("https://drive.google.com/uc?id=${base64Decode(id)}&export=download").document - val form = doc.select("form#download-form").attr("action") - val uc = doc.select("input#uc-download-link").attr("value") - val video = app.post( - form, data = mapOf( - "uc-download-link" to uc - ) - ).url + val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1) + val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text) + val service = """service\s*=\s*['"]([^'"]+)""".toRegex().find(script)?.groupValues?.get(1) + val serverUrl = """['"](\S+check&id\S+?)['"]""".toRegex().find(script)?.groupValues?.get(1) + ?.replace("\"+service+\"", service ?: return) + + val video = app.get(serverUrl ?: return, referer = "$mainUrl/").parsedSafe()?.data callback.invoke( ExtractorLink( this.name, this.name, - video, + video ?: return, "", Qualities.Unknown.value, INFER_TYPE @@ -40,4 +36,8 @@ open class Acefile : ExtractorApi() { } + data class Source( + val data: String? = null, + ) + } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index 68d543d3..f03a5525 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -49,8 +49,23 @@ open class Chillx : ExtractorApi() { val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) + val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) + val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val matches = subtitlePattern.findAll(subtitles ?: "") + val languageUrlPairs = matches.map { matchResult -> + val (language, url) = matchResult.destructured + decodeUnicodeEscape(language) to url + }.toList() + + languageUrlPairs.forEach{ (name, file) -> + subtitleCallback.invoke( + SubtitleFile( + name, + file + ) + ) + } // required val headers = mapOf( "Accept" to "*/*", @@ -67,18 +82,15 @@ open class Chillx : ExtractorApi() { "$mainUrl/", headers = headers ).forEach(callback) - - AppUtils.tryParseJson>("[$tracks]") - ?.filter { it.kind == "captions" }?.map { track -> - subtitleCallback.invoke( - SubtitleFile( - track.label ?: "", - track.file ?: return@map null - ) - ) - } } - + + private fun decodeUnicodeEscape(input: String): String { + val regex = Regex("u([0-9a-fA-F]{4})") + return regex.replace(input) { + it.groupValues[1].toInt(16).toChar().toString() + } + } + suspend fun getKey() = key ?: fetchKey().also { key = it } private suspend fun fetchKey(): String { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index eaf9c65f..8d78c1a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -22,9 +22,9 @@ open class Gofile : ExtractorApi() { val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { - Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) + Regex("fetchData.wt\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) } - app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken") + app.get("$mainApi/getContent?contentId=$id&token=$token&wt=$websiteToken") .parsedSafe()?.data?.contents?.forEach { callback.invoke( ExtractorLink( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index 7389db68..b557a53e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -10,4 +10,14 @@ class Hotlinger : ContentX() { class FourCX : ContentX() { override var name = "FourCX" override var mainUrl = "https://four.contentx.me" +} + +class PlayRu : ContentX() { + override var name = "PlayRu" + override var mainUrl = "https://playru.net" +} + +class FourPlayRu : ContentX() { + override var name = "FourPlayRu" + override var mainUrl = "https://four.playru.net" } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index d5b52dd7..2df81bc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.ExtractorApi @@ -16,13 +17,52 @@ import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec -// Code found in https://github.com/theonlymo/keys -// special credits to @theonlymo for providing key class Megacloud : Rabbitstream() { override val name = "Megacloud" override val mainUrl = "https://megacloud.tv" override val embed = "embed-2/ajax/e-1" - override val key = "https://raw.githubusercontent.com/theonlymo/keys/e1/key" + private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js" + + override suspend fun extractRealKey(sources: String): Pair { + val rawKeys = getKeys() + val sourcesArray = sources.toCharArray() + + var extractedKey = "" + var currentIndex = 0 + for (index in rawKeys) { + val start = index[0] + currentIndex + val end = start + index[1] + for (i in start until end) { + extractedKey += sourcesArray[i].toString() + sourcesArray[i] = ' ' + } + currentIndex += index[1] + } + + return extractedKey to sourcesArray.joinToString("").replace(" ", "") + } + + private suspend fun getKeys(): List> { + val script = app.get(scriptUrl).text + fun matchingKey(value: String): String { + return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1) + ?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key") + } + + val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);") + val indexPairs = regex.findAll(script).toList().map { match -> + val matchKey1 = matchingKey(match.groupValues[1]) + val matchKey2 = matchingKey(match.groupValues[2]) + try { + listOf(matchKey1.toInt(16), matchKey2.toInt(16)) + } catch (e: NumberFormatException) { + emptyList() + } + }.filter { it.isNotEmpty() } + + return indexPairs + } + } class Dokicloud : Rabbitstream() { @@ -30,12 +70,14 @@ class Dokicloud : Rabbitstream() { override val mainUrl = "https://dokicloud.one" } +// Code found in https://github.com/eatmynerds/key +// special credits to @eatmynerds for providing key open class Rabbitstream : ExtractorApi() { override val name = "Rabbitstream" override val mainUrl = "https://rabbitstream.net" override val requiresReferer = false open val embed = "ajax/embed-4" - open val key = "https://raw.githubusercontent.com/theonlymo/keys/e4/key" + open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt" override suspend fun getUrl( url: String, @@ -56,7 +98,7 @@ open class Rabbitstream : ExtractorApi() { val decryptedSources = if (sources == null || encryptedMap.encrypted == false) { response.parsedSafe() } else { - val (key, encData) = extractRealKey(sources, getRawKey()) + val (key, encData) = extractRealKey(sources) val decrypted = decryptMapped>(encData, key) SourcesResponses( sources = decrypted, @@ -75,8 +117,8 @@ open class Rabbitstream : ExtractorApi() { decryptedSources?.tracks?.map { track -> subtitleCallback.invoke( SubtitleFile( - track?.label ?: "", - track?.file ?: return@map + track?.label ?: return@map, + track.file ?: return@map ) ) } @@ -84,25 +126,10 @@ open class Rabbitstream : ExtractorApi() { } - private suspend fun getRawKey(): String = app.get(key).text - - private fun extractRealKey(sources: String, stops: String): Pair { - val decryptKey = parseJson>>(stops) - val sourcesArray = sources.toCharArray() - - var extractedKey = "" - var currentIndex = 0 - for (index in decryptKey) { - val start = index[0] + currentIndex - val end = start + index[1] - for (i in start until end) { - extractedKey += sourcesArray[i].toString() - sourcesArray[i] = ' ' - } - currentIndex += index[1] - } - - return extractedKey to sourcesArray.joinToString("") + open suspend fun extractRealKey(sources: String): Pair { + val rawKeys = parseJson>(app.get(key).text) + val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray()) + return extractedKey to sources } private inline fun decryptMapped(input: String, key: String): T? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt b/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt index 2c11bcdd..90872d94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt @@ -31,6 +31,7 @@ import java.net.URI * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. * @param script pass custom js to execute * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout * */ class WebViewResolver( val interceptUrl: Regex, @@ -38,18 +39,29 @@ class WebViewResolver( val userAgent: String? = USER_AGENT, val useOkhttp: Boolean = true, val script: String? = null, - val scriptCallback: ((String) -> Unit)? = null + val scriptCallback: ((String) -> Unit)? = null, + val timeout: Long = DEFAULT_TIMEOUT ) : Interceptor { + constructor( + interceptUrl: Regex, + additionalUrls: List = emptyList(), + userAgent: String? = USER_AGENT, + useOkhttp: Boolean = true, + script: String? = null, + scriptCallback: ((String) -> Unit)? = null, + ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT) + constructor( interceptUrl: Regex, additionalUrls: List = emptyList(), userAgent: String? = USER_AGENT, useOkhttp: Boolean = true - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null) + ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT) companion object { + private const val DEFAULT_TIMEOUT = 60_000L var webViewUserAgent: String? = null @JvmName("getWebViewUserAgent1") @@ -262,7 +274,7 @@ class WebViewResolver( var loop = 0 // Timeouts after this amount, 60s - val totalTime = 60000L + val totalTime = timeout val delayTime = 100L diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index b7f601ff..0e397f81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -317,7 +317,7 @@ class HomeParentItemAdapterPreview( homePreviewText.text = item.name populateChips( homePreviewTags, - item.tags ?: emptyList(), + item.tags?.take(6) ?: emptyList(), R.style.ChipFilledSemiTransparent ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index e7e50b9d..76066c2e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -681,6 +681,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { resultMetaYear.setText(d.yearText) resultMetaDuration.setText(d.durationText) resultMetaRating.setText(d.ratingText) + resultMetaStatus.setText(d.onGoingText) resultMetaContentRating.setText(d.contentRatingText) resultCastText.setText(d.actorsText) resultNextAiring.setText(d.nextAiringEpisode) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt index 602b45e4..7ac7cbb2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt @@ -1,10 +1,17 @@ package com.lagradost.cloudstream3.ui.settings.extensions +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.Toast import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.CommonActivity.activity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding @@ -112,6 +119,17 @@ class RepoAdapter( repositoryItemRoot.setOnClickListener { clickCallback(repositoryData) } + + repositoryItemRoot.setOnLongClickListener { + val clipboardManager = + activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager? + clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url)) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT) + } + return@setOnLongClickListener true + } + mainText.text = repositoryData.name subText.text = repositoryData.url } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 4f9c5e8c..637f65b9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -106,6 +106,8 @@ import com.lagradost.cloudstream3.extractors.ContentX import com.lagradost.cloudstream3.extractors.EmturbovidExtractor import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX +import com.lagradost.cloudstream3.extractors.PlayRu +import com.lagradost.cloudstream3.extractors.FourPlayRu import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -704,6 +706,8 @@ val extractorApis: MutableList = arrayListOf( ContentX(), Hotlinger(), FourCX(), + PlayRu(), + FourPlayRu(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index 6db7536f..ccc3a8e1 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -137,7 +137,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" - android:maxLines="5" + android:maxLines="3" android:paddingBottom="5dp" android:textSize="15sp" tools:text="very nice tv series" /> diff --git a/app/src/main/res/layout/fragment_result_swipe.xml b/app/src/main/res/layout/fragment_result_swipe.xml index eb2653d0..bb8a5c10 100644 --- a/app/src/main/res/layout/fragment_result_swipe.xml +++ b/app/src/main/res/layout/fragment_result_swipe.xml @@ -83,7 +83,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/subscribe_tooltip" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/baseline_notifications_none_24" android:layout_gravity="end|center_vertical" @@ -100,7 +100,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/action_add_to_favorites" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_favorite_border_24" android:layout_gravity="end|center_vertical" @@ -117,7 +117,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/result_share" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_outline_share_24" android:layout_gravity="end|center_vertical" @@ -135,7 +135,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/result_open_in_browser" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_public_24" android:layout_gravity="end|center_vertical" @@ -153,7 +153,7 @@ android:layout_height="30dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/result_search_tooltip" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/search_icon" android:layout_gravity="end|center_vertical" @@ -171,7 +171,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/recommendations_tooltip" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/baseline_list_alt_24" android:layout_gravity="end|center_vertical" diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 7b5fc291..a7ba4334 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -387,6 +387,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit Download Done %s - %s Update Started - Stream + Network stream Error Loading Links Links Reloaded Internal Storage @@ -174,6 +174,10 @@ Clear Save Title copied! + Repo URL copied! + New episode notification + Search in other extensions + Show recommendations Player Speed Subtitle Settings Text Color @@ -213,8 +217,8 @@ Player subtitles settings Chromecast Subtitles Chromecast subtitles settings - Eigengravy Mode - Adds a speed option in the player + Playback speed + Adds a speed option in the player Swipe to seek Swipe from side to side to control your position in a video Swipe to change settings @@ -391,9 +395,9 @@ Causes problems if set too high on devices with low storage space, such as Android TV. DNS over HTTPS Useful for bypassing ISP blocks - raw.githubusercontent.com Proxy + GitHub Proxy Could not reach GitHub. Turning on jsDelivr proxy… - Bypasses blocking of GitHub using jsDelivr. May cause updates to be delayed by few days. + Bypass blocking of raw github URLs using jsDelivr. May cause updates to be delayed by few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -439,13 +443,15 @@ General Random Button Show random button on Homepage and Library - Provider languages + Extension languages App Layout Preferred media - Enable NSFW on supported providers + Enable NSFW on supported Extensions Subtitle encoding Providers Provider test + Test all Extensions + This Test is meant for developers only and does not verifies or denies working of any extension. Layout Auto TV layout @@ -462,11 +468,11 @@ opensubtitles_key nginx_key password123 - MyCoolUsername + Username hello@world.com 127.0.0.1 - MyCoolSite - example.com + NewSiteName + https://example.com Language code (en)