Merge remote-tracking branch 'origin/master' into batteryoptimization

This commit is contained in:
IndusAryan 2024-03-06 11:59:14 +05:30
commit 1fa4807716
11 changed files with 120 additions and 48 deletions

View file

@ -230,7 +230,7 @@ dependencies {
// Downloading & Networking // Downloading & Networking
implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime:2.9.0")
implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.github.Blatzar:NiceHttp:0.4.5") // HTTP Lib implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
} }
tasks.register("androidSourcesJar", Jar::class) { tasks.register("androidSourcesJar", Jar::class) {

View file

@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
@ -16,13 +17,52 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
// Code found in https://github.com/theonlymo/keys
// special credits to @theonlymo for providing key
class Megacloud : Rabbitstream() { class Megacloud : Rabbitstream() {
override val name = "Megacloud" override val name = "Megacloud"
override val mainUrl = "https://megacloud.tv" override val mainUrl = "https://megacloud.tv"
override val embed = "embed-2/ajax/e-1" 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<String, String> {
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<List<Int>> {
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() { class Dokicloud : Rabbitstream() {
@ -30,12 +70,14 @@ class Dokicloud : Rabbitstream() {
override val mainUrl = "https://dokicloud.one" 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() { open class Rabbitstream : ExtractorApi() {
override val name = "Rabbitstream" override val name = "Rabbitstream"
override val mainUrl = "https://rabbitstream.net" override val mainUrl = "https://rabbitstream.net"
override val requiresReferer = false override val requiresReferer = false
open val embed = "ajax/embed-4" 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( override suspend fun getUrl(
url: String, url: String,
@ -47,7 +89,7 @@ open class Rabbitstream : ExtractorApi() {
val response = app.get( val response = app.get(
"$mainUrl/$embed/getSources?id=$id", "$mainUrl/$embed/getSources?id=$id",
referer = url, referer = mainUrl,
headers = mapOf("X-Requested-With" to "XMLHttpRequest") headers = mapOf("X-Requested-With" to "XMLHttpRequest")
) )
@ -56,7 +98,7 @@ open class Rabbitstream : ExtractorApi() {
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) { val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
response.parsedSafe() response.parsedSafe()
} else { } else {
val (key, encData) = extractRealKey(sources, getRawKey()) val (key, encData) = extractRealKey(sources)
val decrypted = decryptMapped<List<Sources>>(encData, key) val decrypted = decryptMapped<List<Sources>>(encData, key)
SourcesResponses( SourcesResponses(
sources = decrypted, sources = decrypted,
@ -72,11 +114,11 @@ open class Rabbitstream : ExtractorApi() {
).forEach(callback) ).forEach(callback)
} }
decryptedSources?.tracks?.filter { it?.kind == "captions" }?.map { track -> decryptedSources?.tracks?.map { track ->
subtitleCallback.invoke( subtitleCallback.invoke(
SubtitleFile( SubtitleFile(
track?.label ?: "", track?.label ?: return@map,
track?.file ?: return@map track.file ?: return@map
) )
) )
} }
@ -84,25 +126,10 @@ open class Rabbitstream : ExtractorApi() {
} }
private suspend fun getRawKey(): String = app.get(key).text open suspend fun extractRealKey(sources: String): Pair<String, String> {
val rawKeys = parseJson<List<Int>>(app.get(key).text)
private fun extractRealKey(sources: String, stops: String): Pair<String, String> { val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
val decryptKey = parseJson<List<List<Int>>>(stops) return extractedKey to sources
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("")
} }
private inline fun <reified T> decryptMapped(input: String, key: String): T? { private inline fun <reified T> decryptMapped(input: String, key: String): T? {

View file

@ -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 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 script pass custom js to execute
* @param scriptCallback will be called with the result from custom js * @param scriptCallback will be called with the result from custom js
* @param timeout close webview after timeout
* */ * */
class WebViewResolver( class WebViewResolver(
val interceptUrl: Regex, val interceptUrl: Regex,
@ -38,18 +39,29 @@ class WebViewResolver(
val userAgent: String? = USER_AGENT, val userAgent: String? = USER_AGENT,
val useOkhttp: Boolean = true, val useOkhttp: Boolean = true,
val script: String? = null, val script: String? = null,
val scriptCallback: ((String) -> Unit)? = null val scriptCallback: ((String) -> Unit)? = null,
val timeout: Long = DEFAULT_TIMEOUT
) : ) :
Interceptor { Interceptor {
constructor(
interceptUrl: Regex,
additionalUrls: List<Regex> = 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( constructor(
interceptUrl: Regex, interceptUrl: Regex,
additionalUrls: List<Regex> = emptyList(), additionalUrls: List<Regex> = emptyList(),
userAgent: String? = USER_AGENT, userAgent: String? = USER_AGENT,
useOkhttp: Boolean = true useOkhttp: Boolean = true
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null) ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT)
companion object { companion object {
private const val DEFAULT_TIMEOUT = 60_000L
var webViewUserAgent: String? = null var webViewUserAgent: String? = null
@JvmName("getWebViewUserAgent1") @JvmName("getWebViewUserAgent1")
@ -262,7 +274,7 @@ class WebViewResolver(
var loop = 0 var loop = 0
// Timeouts after this amount, 60s // Timeouts after this amount, 60s
val totalTime = 60000L val totalTime = timeout
val delayTime = 100L val delayTime = 100L

View file

@ -440,9 +440,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
interceptor = interceptor interceptor = interceptor
).isSuccessful ).isSuccessful
} else { } else {
val statusResponse = status?.let { setStatus -> val statusResponse = this.status?.let { setStatus ->
val newStatus = val newStatus =
SimklListStatusType.values() SimklListStatusType.entries
.firstOrNull { it.value == setStatus }?.originalName .firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!! ?: SimklListStatusType.Watching.originalName!!
@ -479,9 +479,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).isSuccessful ).isSuccessful
} ?: true } ?: true
// You cannot rate if you are planning to watch it.
val shouldRate =
score != null && status != SimklListStatusType.Planning.value
val realScore = if (shouldRate) score else null
val historyResponse = val historyResponse =
// Only post if there are episodes or score to upload // Only post if there are episodes or score to upload
if (addEpisodes != null || score != null) { if (addEpisodes != null || shouldRate) {
app.post( app.post(
"${this.url}/sync/history", "${this.url}/sync/history",
json = StatusRequest( json = StatusRequest(
@ -492,8 +497,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
ids, ids,
addEpisodes?.first, addEpisodes?.first,
addEpisodes?.second, addEpisodes?.second,
score, realScore,
score?.let { time }, realScore?.let { time },
) )
), movies = emptyList() ), movies = emptyList()
), ),
@ -827,7 +832,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
if (foundItem != null) { if (foundItem != null) {
return SimklSyncStatus( return SimklSyncStatus(
status = foundItem.status?.let { SyncWatchType.fromInternalId(SimklListStatusType.fromString(it)?.value) } status = foundItem.status?.let {
SyncWatchType.fromInternalId(
SimklListStatusType.fromString(
it
)?.value
)
}
?: return null, ?: return null,
score = foundItem.user_rating, score = foundItem.user_rating,
watchedEpisodes = foundItem.watched_episodes_count, watchedEpisodes = foundItem.watched_episodes_count,
@ -839,7 +850,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
) )
} else { } else {
return SimklSyncStatus( return SimklSyncStatus(
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value) , status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
score = 0, score = 0,
watchedEpisodes = 0, watchedEpisodes = 0,
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
@ -859,8 +870,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val builder = SimklScoreBuilder.Builder() val builder = SimklScoreBuilder.Builder()
.apiUrl(this.mainUrl) .apiUrl(this.mainUrl)
.score(status.score, simklStatus?.oldScore) .score(status.score, simklStatus?.oldScore)
.status(status.status.internalId, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus -> .status(
SimklListStatusType.values().firstOrNull { status.status.internalId,
(status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
SimklListStatusType.entries.firstOrNull {
it.originalName == oldStatus it.originalName == oldStatus
}?.value }?.value
}) })
@ -996,7 +1009,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val list = getSyncListSmart() ?: return null val list = getSyncListSmart() ?: return null
val baseMap = val baseMap =
SimklListStatusType.values() SimklListStatusType.entries
.filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value }
.associate { .associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>() it.stringRes to emptyList<SyncAPI.LibraryItem>()

View file

@ -317,7 +317,7 @@ class HomeParentItemAdapterPreview(
homePreviewText.text = item.name homePreviewText.text = item.name
populateChips( populateChips(
homePreviewTags, homePreviewTags,
item.tags ?: emptyList(), item.tags?.take(6) ?: emptyList(),
R.style.ChipFilledSemiTransparent R.style.ChipFilledSemiTransparent
) )

View file

@ -684,6 +684,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultMetaYear.setText(d.yearText) resultMetaYear.setText(d.yearText)
resultMetaDuration.setText(d.durationText) resultMetaDuration.setText(d.durationText)
resultMetaRating.setText(d.ratingText) resultMetaRating.setText(d.ratingText)
resultMetaStatus.setText(d.onGoingText)
resultMetaContentRating.setText(d.contentRatingText) resultMetaContentRating.setText(d.contentRatingText)
resultCastText.setText(d.actorsText) resultCastText.setText(d.actorsText)
resultNextAiring.setText(d.nextAiringEpisode) resultNextAiring.setText(d.nextAiringEpisode)

View file

@ -1,10 +1,17 @@
package com.lagradost.cloudstream3.ui.settings.extensions 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding 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.R
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
@ -112,6 +119,17 @@ class RepoAdapter(
repositoryItemRoot.setOnClickListener { repositoryItemRoot.setOnClickListener {
clickCallback(repositoryData) 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 mainText.text = repositoryData.name
subText.text = repositoryData.url subText.text = repositoryData.url
} }

View file

@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import okhttp3.internal.toImmutableList
class TestViewModel : ViewModel() { class TestViewModel : ViewModel() {
data class TestProgress( data class TestProgress(

View file

@ -137,7 +137,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="5" android:maxLines="3"
android:paddingBottom="5dp" android:paddingBottom="5dp"
android:textSize="15sp" android:textSize="15sp"
tools:text="very nice tv series" /> tools:text="very nice tv series" />

View file

@ -387,6 +387,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<com.lagradost.cloudstream3.widget.FlowLayout <com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
app:itemSpacing="10dp"> app:itemSpacing="10dp">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View file

@ -174,6 +174,7 @@
<string name="sort_clear">Clear</string> <string name="sort_clear">Clear</string>
<string name="sort_save">Save</string> <string name="sort_save">Save</string>
<string name="copyTitle">Title copied!</string> <string name="copyTitle">Title copied!</string>
<string name="copyRepoUrl">Repo URL copied!</string>
<string name="subscribe_tooltip">New episode notification</string> <string name="subscribe_tooltip">New episode notification</string>
<string name="result_search_tooltip">Search in other extensions</string> <string name="result_search_tooltip">Search in other extensions</string>
<string name="recommendations_tooltip">Show recommendations</string> <string name="recommendations_tooltip">Show recommendations</string>