diff --git a/app/build.gradle b/app/build.gradle index ed641a0a..b80c820f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,7 +170,7 @@ dependencies { // Networking // implementation "com.squareup.okhttp3:okhttp:4.9.2" // implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1" - implementation 'com.github.Blatzar:NiceHttp:0.3.2' + implementation 'com.github.Blatzar:NiceHttp:0.3.3' // Util to skip the URI file fuckery 🙏 implementation "com.github.tachiyomiorg:unifile:17bec43" diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index dc6cc454..d282f6dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1118,6 +1118,11 @@ data class NextAiring( val unixTime: Long, ) +/** + * @param season To be mapped with episode season, not shown in UI if displaySeason is defined + * @param name To be shown next to the season like "Season $displaySeason $name" but if displaySeason is null then "$name" + * @param displaySeason What to be displayed next to the season name, if null then the name is the only thing shown. + * */ data class SeasonData( val season: Int, val name: String? = null, @@ -1198,9 +1203,12 @@ data class AnimeLoadResponse( override var backgroundPosterUrl: String? = null, ) : LoadResponse, EpisodeResponse +/** + * If episodes already exist appends the list. + * */ fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List?) { if (episodes.isNullOrEmpty()) return - this.episodes[status] = episodes + this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes } suspend fun MainAPI.newAnimeLoadResponse( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index c5eaf40e..7ec1fb22 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay +class DoodWfExtractor : DoodLaExtractor() { + override var mainUrl = "https://dood.wf" +} + class DoodCxExtractor : DoodLaExtractor() { override var mainUrl = "https://dood.cx" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt new file mode 100644 index 00000000..d2e56bf1 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -0,0 +1,178 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import org.jsoup.nodes.Element +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class Gdriveplayerapi: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayerapi.com" +} + +class Gdriveplayerapp: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.app" +} + +class Gdriveplayerfun: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.fun" +} + +class Gdriveplayerio: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.io" +} + +class Gdriveplayerme: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.me" +} + +class Gdriveplayerbiz: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.biz" +} + +class Gdriveplayerorg: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.org" +} + +class Gdriveplayerus: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.us" +} + +class Gdriveplayerco: Gdriveplayer() { + override val mainUrl: String = "https://gdriveplayer.co" +} + +open class Gdriveplayer : ExtractorApi() { + override val name = "Gdrive" + override val mainUrl = "https://gdriveplayer.to" + override val requiresReferer = false + + private fun unpackJs(script: Element): String? { + return script.select("script").find { it.data().contains("eval(function(p,a,c,k,e,d)") } + ?.data()?.let { getAndUnpack(it) } + } + + private fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + // https://stackoverflow.com/a/41434590/8166854 + private fun GenerateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = "MD5", + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): List? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return listOf( + generatedData.copyOfRange(0, keyLength), + generatedData.copyOfRange(keyLength, targetKeySize) + ) + } catch (e: DigestException) { + return null + } + } + + private fun cryptoAESHandler( + data: AesData, + pass: ByteArray, + encrypt: Boolean = true + ): String? { + val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null + val cipher = Cipher.getInstance("AES/CBC/NoPadding") + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(data.ct))) + } else { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(data.ct.toByteArray())) + + } + } + + private fun Regex.first(str: String): String? { + return find(str)?.groupValues?.getOrNull(1) + } + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val document = app.get(url).document + + val eval = unpackJs(document)?.replace("\\", "") ?: return + val data = AppUtils.tryParseJson(Regex("data='(\\S+?)'").first(eval)) ?: return + val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) + ?.split(Regex("\\D+")) + ?.joinToString("") { + Char(it.toInt()).toString() + }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } + ?: throw ErrorLoadingException("can't find password") + val decryptedData = + cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "") + ?.substringAfter("sources:[")?.substringBefore("],") + + Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(decryptedData ?: return).map { + it.groupValues[1] to it.groupValues[2] + }.toList().distinctBy { it.second }.map { (link, quality) -> + callback.invoke( + ExtractorLink( + source = this.name, + name = this.name, + url = "${httpsify(link)}&res=$quality", + referer = mainUrl, + quality = quality.toIntOrNull() ?: Qualities.Unknown.value, + headers = mapOf("Range" to "bytes=0-") + ) + ) + } + + } + + data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index a933c484..461f56d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -7,6 +7,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +class Sbflix : StreamSB() { + override var mainUrl = "https://sbflix.xyz" + override var name = "Sbflix" +} + class Vidgomunime : StreamSB() { override var mainUrl = "https://vidgomunime.xyz" } @@ -111,7 +116,7 @@ open class StreamSB : ExtractorApi() { }.first() val bytes = id.toByteArray() val bytesToHex = bytesToHex(bytes) - val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" + val master = "$mainUrl/sources44/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" val headers = mapOf( "watchsb" to "streamsb", ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index a64f0d8d..5c3276fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -7,6 +7,7 @@ import com.bumptech.glide.load.HttpException import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* +import java.io.InterruptedIOException import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.net.ssl.SSLHandshakeException @@ -157,7 +158,7 @@ suspend fun safeApiCall( } safeFail(throwable) } - is SocketTimeoutException -> { + is SocketTimeoutException, is InterruptedIOException -> { Resource.Failure( true, null, @@ -192,7 +193,7 @@ suspend fun safeApiCall( true, null, null, - (throwable.message ?: "SSLHandshakeException") + "\nTry again later." + (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS." ) } else -> safeFail(throwable) diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index 03ec6ae8..13299002 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -5,15 +5,12 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.nicehttp.Requests -import com.lagradost.nicehttp.getCookies import com.lagradost.nicehttp.ignoreAllSSLErrors import okhttp3.Cache import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders import okhttp3.OkHttpClient -import okhttp3.Request import java.io.File -import java.util.concurrent.TimeUnit fun Requests.initClient(context: Context): OkHttpClient { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 02fe60ca..48919308 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1483,15 +1483,20 @@ class ResultViewModel2 : ViewModel() { 0 -> txt(R.string.no_season) else -> { val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames - val seasonData = - seasonNames.getSeason(indexer.season) - val suffix = seasonData?.name?.let { " $it" } ?: "" - txt( - R.string.season_format, - txt(R.string.season), - seasonData?.displaySeason ?: indexer.season, - suffix - ) + val seasonData = seasonNames.getSeason(indexer.season) + + // If displaySeason is null then only show the name! + if (seasonData?.name != null && seasonData.displaySeason == null) { + txt(seasonData.name) + } else { + val suffix = seasonData?.name?.let { " $it" } ?: "" + txt( + R.string.season_format, + txt(R.string.season), + seasonData?.displaySeason ?: indexer.season, + suffix + ) + } } } ) @@ -1587,8 +1592,8 @@ class ResultViewModel2 : ViewModel() { val idIndex = ep.key.id for ((index, i) in ep.value.withIndex()) { val episode = i.episode ?: (index + 1) - val id = mainId + episode + idIndex * 1000000 - if (!existingEpisodes.contains(episode)) { + val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0) + if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) val seasonData = loadResponse.seasonNames.getSeason(i.season) val eps = @@ -1597,8 +1602,8 @@ class ResultViewModel2 : ViewModel() { filterName(i.name), i.posterUrl, episode, - null, - seasonData?.displaySeason ?: i.season, + seasonData?.season ?: i.season, + if (seasonData != null) seasonData.displaySeason else i.season, i.data, loadResponse.apiName, id, @@ -1610,7 +1615,7 @@ class ResultViewModel2 : ViewModel() { mainId ) - val season = eps.season ?: 0 + val season = eps.seasonIndex ?: 0 val indexer = EpisodeIndexer(ep.key, season) episodes[indexer]?.add(eps) ?: run { episodes[indexer] = mutableListOf(eps) @@ -1625,15 +1630,14 @@ class ResultViewModel2 : ViewModel() { mutableMapOf() val existingEpisodes = HashSet() for ((index, episode) in loadResponse.episodes.sortedBy { - (it.season?.times(10000) ?: 0) + (it.episode ?: 0) + (it.season?.times(10_000) ?: 0) + (it.episode ?: 0) }.withIndex()) { val episodeIndex = episode.episode ?: (index + 1) val id = - mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1 + mainId + (episode.season?.times(100_000) ?: 0) + episodeIndex + 1 if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) - val seasonIndex = episode.season?.minus(1) - val currentSeason = + val seasonData = loadResponse.seasonNames.getSeason(episode.season) val ep = @@ -1642,8 +1646,8 @@ class ResultViewModel2 : ViewModel() { filterName(episode.name), episode.posterUrl, episodeIndex, - seasonIndex, - currentSeason?.displaySeason ?: episode.season, + seasonData?.season ?: episode.season, + if (seasonData != null) seasonData.displaySeason else episode.season, episode.data, loadResponse.apiName, id, @@ -1655,7 +1659,7 @@ class ResultViewModel2 : ViewModel() { mainId ) - val season = episode.season ?: 0 + val season = ep.seasonIndex ?: 0 val indexer = EpisodeIndexer(DubStatus.None, season) episodes[indexer]?.add(ep) ?: kotlin.run { @@ -1747,16 +1751,17 @@ class ResultViewModel2 : ViewModel() { val seasonData = loadResponse.seasonNames.getSeason(seasonNumber) val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber val suffix = seasonData?.name?.let { " $it" } ?: "" - - val name = - /*loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData -> - txt(seasonData) - } ?:*/txt( - R.string.season_format, - txt(R.string.season), - fixedSeasonNumber, - suffix - ) + // If displaySeason is null then only show the name! + val name = if (seasonData?.name != null && seasonData.displaySeason == null) { + txt(seasonData.name) + } else { + txt( + R.string.season_format, + txt(R.string.season), + fixedSeasonNumber, + suffix + ) + } name to seasonNumber }) } @@ -1812,7 +1817,12 @@ class ResultViewModel2 : ViewModel() { } private fun loadTrailers(loadResponse: LoadResponse) = ioSafe { - _trailers.postValue(getTrailers(loadResponse, 3)) // we dont want to fetch too many trailers + _trailers.postValue( + getTrailers( + loadResponse, + 3 + ) + ) // we dont want to fetch too many trailers } private suspend fun getTrailers( 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 7a65df30..ae3d8c06 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -236,6 +236,7 @@ val extractorApis: MutableList = arrayListOf( Ssbstream(), Sbthe(), Vidgomunime(), + Sbflix(), Fastream(), @@ -269,6 +270,7 @@ val extractorApis: MutableList = arrayListOf( DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), + DoodWfExtractor(), AsianLoad(), @@ -321,6 +323,17 @@ val extractorApis: MutableList = arrayListOf( Mvidoo(), Streamplay(), + Gdriveplayerapi(), + Gdriveplayerapp(), + Gdriveplayerfun(), + Gdriveplayerio(), + Gdriveplayerme(), + Gdriveplayerbiz(), + Gdriveplayerorg(), + Gdriveplayerus(), + Gdriveplayerco(), + Gdriveplayer(), + YoutubeExtractor(), YoutubeShortLinkExtractor(), YoutubeMobileExtractor(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 42d200d0..54492e6d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -200,16 +200,23 @@ class InAppUpdater { private suspend fun Activity.downloadUpdate(url: String): Boolean { try { Log.d(LOG_TAG, "Downloading update: $url") + val appUpdateName = "CloudStream" + val appUpdateSuffix = "apk" - val localContext = this + // Delete all old updates + this.cacheDir.listFiles()?.filter { + it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix + }?.forEach { + it.deleteOnExit() + } - val downloadedFile = File.createTempFile("CloudStream", ".apk") + val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix") val sink: BufferedSink = downloadedFile.sink().buffer() updateLock.withLock { sink.writeAll(app.get(url).body.source()) sink.close() - openApk(localContext, Uri.fromFile(downloadedFile)) + openApk(this, Uri.fromFile(downloadedFile)) } return true } catch (e: Exception) { diff --git a/app/src/main/res/layout/add_account_input.xml b/app/src/main/res/layout/add_account_input.xml index 1471af9c..ea48a80f 100644 --- a/app/src/main/res/layout/add_account_input.xml +++ b/app/src/main/res/layout/add_account_input.xml @@ -1,131 +1,134 @@ - - - - - - - + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:layout_marginBottom="10dp" + android:orientation="horizontal"> - + + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:textColor="?attr/textColor" + android:textSize="20sp" + android:textStyle="bold" + tools:text="Test" /> - - - + + + + android:layout_height="wrap_content" + android:autofillHints="username" + android:hint="@string/example_username" + android:inputType="text" + android:nextFocusLeft="@id/apply_btt" + android:nextFocusRight="@id/cancel_btt" + android:nextFocusDown="@id/login_email_input" + android:requiresFadingEdge="vertical" + android:textColorHint="?attr/grayTextColor" + tools:ignore="LabelFor" /> + + + + + + + + + + + android:id="@+id/apply_btt" + style="@style/WhiteButton" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical|end" + android:text="@string/login" /> + android:id="@+id/cancel_btt" + style="@style/BlackButton" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_cancel" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plugins.xml b/app/src/main/res/layout/fragment_plugins.xml index 54eae80f..15e0d2f9 100644 --- a/app/src/main/res/layout/fragment_plugins.xml +++ b/app/src/main/res/layout/fragment_plugins.xml @@ -102,18 +102,21 @@ style="@style/RoundedSelectableButton" android:nextFocusLeft="@id/home_select_cartoons" + android:nextFocusRight="@id/home_select_livestreams" android:text="@string/documentaries" /> - +