From 6df3ef14f66dd3cfc038ee922c563684eb84ce4e Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:07:28 +0000 Subject: [PATCH 01/22] First steps for multiplatform API (#1003) * First steps for multiplatform api * Buildconfig testing * Fix publishing and classes.jar * Update build.gradle.kts --- .idea/gradle.xml | 7 +- app/build.gradle.kts | 34 ++++++++-- .../com/lagradost/cloudstream3/MainAPI.kt | 2 - .../lagradost/cloudstream3/mvvm/Lifecycle.kt | 16 +++++ build.gradle.kts | 8 ++- library/build.gradle.kts | 68 +++++++++++++++++++ library/src/androidMain/AndroidManifest.xml | 2 + .../kotlin/com/lagradost/api/Log.kt | 21 ++++++ .../kotlin/com/lagradost/api/Log.kt | 8 +++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../cloudstream3/mvvm/ArchComponentExt.kt | 35 +++------- .../jvmMain/kotlin/com/lagradost/api/Log.kt | 19 ++++++ settings.gradle.kts | 3 +- 13 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt create mode 100644 library/build.gradle.kts create mode 100644 library/src/androidMain/AndroidManifest.xml create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt (86%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/Log.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c5c0ff3b..d7c08c9c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02946e85..e07162d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.net.URL @@ -13,6 +14,7 @@ plugins { val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() +var isLibraryDebug = false fun String.execute() = ByteArrayOutputStream().use { baot -> if (project.exec { @@ -103,6 +105,7 @@ android { ) } debug { + isLibraryDebug = true isDebuggable = true applicationIdSuffix = ".debug" proguardFiles( @@ -232,18 +235,37 @@ 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(project(":library") { + this.extra.set("isDebug", isLibraryDebug) + }) } -tasks.register("androidSourcesJar", Jar::class) { +tasks.register("androidSourcesJar") { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -// For GradLew Plugin -tasks.register("makeJar", Copy::class) { - from("build/intermediates/compile_app_classes_jar/prereleaseDebug") - into("build") - include("classes.jar") +tasks.register("copyJar") { + from( + "build/intermediates/compile_app_classes_jar/prereleaseDebug", + "../library/build/libs" + ) + into("build/app-classes") + include("classes.jar", "library-jvm*.jar") + // Remove the version + rename("library-jvm.*.jar", "library-jvm.jar") +} + +// Merge the app classes and the library classes into classes.jar +tasks.register("makeJar") { + dependsOn(tasks.getByName("copyJar")) + from( + zipTree("build/app-classes/classes.jar"), + zipTree("build/app-classes/library-jvm.jar") + ) + destinationDirectory.set(layout.buildDirectory) + archivesName = "classes" } tasks.withType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index ecbdcbbc..7b1b5775 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String { } } -class ErrorLoadingException(message: String? = null) : Exception(message) - fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt new file mode 100644 index 00000000..3df5197c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.mvvm + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} diff --git a/build.gradle.kts b/build.gradle.kts index 801a3c0f..ab1918fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,8 @@ buildscript { classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") + // Universal build config + classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") } } @@ -22,6 +24,6 @@ plugins { id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} +//tasks.register("clean") { +// delete(rootProject.layout.buildDirectory) +//} diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 00000000..42a8c943 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,68 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("com.android.library") + id("com.codingfeline.buildkonfig") +} + +kotlin { + version = "1.0.0" + androidTarget() + jvm() + + sourceSets { + commonMain.dependencies { + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser + ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API + Level 25 or Less. */ + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } +} + +repositories { + mavenLocal() + maven("https://jitpack.io") +} + +tasks.withType { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +buildkonfig { + packageName = "com.lagradost.api" + exposeObjectWithName = "BuildConfig" + + defaultConfigs { + val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true + buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) + } +} + +android { + compileSdk = 34 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + // If this is the same com.lagradost.cloudstream3.R stops working + namespace = "com.lagradost.api" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +publishing { + publications { + withType { + groupId = "com.lagradost.api" + } + } +} \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..12524411 --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +import android.util.Log + +actual object Log { + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } + + actual fun w(tag: String, message: String) { + Log.w(tag, message) + } + + actual fun e(tag: String, message: String) { + Log.e(tag, message) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..4b8e6329 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,8 @@ +package com.lagradost.api + +expect object Log { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String) + fun e(tag: String, message: String) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt new file mode 100644 index 00000000..87ee4815 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -0,0 +1,3 @@ +package com.lagradost.cloudstream3 + +class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 817d7db3..d3b4999a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.mvvm -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import com.bumptech.glide.load.HttpException -import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.api.BuildConfig +import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -158,14 +143,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } - is HttpException -> { - Resource.Failure( - false, - throwable.statusCode, - null, - throwable.message ?: "HttpException" - ) - } +// is HttpException -> { +// Resource.Failure( +// false, +// throwable.statusCode, +// null, +// throwable.message ?: "HttpException" +// ) +// } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..e9a0e6b4 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,19 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + println("DEBUG $tag: $message") + } + + actual fun i(tag: String, message: String) { + println("INFO $tag: $message") + } + + actual fun w(tag: String, message: String) { + println("WARNING $tag: $message") + } + + actual fun e(tag: String, message: String) { + println("ERROR $tag: $message") + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 17070047..eabd9f0e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "CloudStream" -include(":app") \ No newline at end of file +include(":app") +include(":library") \ No newline at end of file From 9a18ef641136cf9335c830145cf5b1bc4a62f8e3 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 17 Apr 2024 23:48:33 +0200 Subject: [PATCH 02/22] bugfix: fixing regex special chars break it (#1047) --- .../main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 153dbd3e..d9f0b382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("\\b\\w+\\b") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 From 6cef9f7ea257f4af8ed3f739f79c1d01b1b3b36e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 20 Apr 2024 22:18:49 +0200 Subject: [PATCH 03/22] Filtering first unwatched episode respects watched state (#1049) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 6a83f396..13621cda 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> + ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched + } + val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From e01ff4d843810467660add2a8464973a673daa08 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 22 Apr 2024 01:13:55 +0200 Subject: [PATCH 04/22] Fix NewPipeExtractor lib path (#1050) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e07162d7..f854865d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers + implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding From 4399a612dfa0672acefc7de17c37884ee64331c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 22 Apr 2024 02:14:36 +0300 Subject: [PATCH 05/22] Update Vidmoly.kt (#1051) --- .../java/com/lagradost/cloudstream3/extractors/Vidmoly.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 615cfd74..979fd8c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - + val headers = mapOf( + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + "Sec-Fetch-Dest" to "iframe" + ) val script = app.get( url, + headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} \ No newline at end of file +} From 0744189020fb3132ebf0debed899e522ab4df246 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:18:54 +0530 Subject: [PATCH 06/22] feat(ui): show account name and image on main settings page (#1001) --- .../ui/settings/SettingsFragment.kt | 52 ++++++++++++++----- app/src/main/res/drawable/rounded_outline.xml | 13 +++++ app/src/main/res/layout/main_settings.xml | 9 ++-- 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_outline.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index dfa84998..443eeda7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import androidx.core.view.children -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -18,12 +18,14 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -133,7 +135,6 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root - //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -141,21 +142,44 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") + /** used to debug leaks + showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : + ${VideoDownloadManager.downloadProgressEvent.size}") **/ - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - binding?.settingsProfile?.isVisible = true - break + fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue + + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + return true // sync profile exists + } } + return false // not syncing } + + // display local account information if not syncing + if (!hasProfilePictureFromAccountManagers(accountManagers)) { + val activity = activity ?: return + val currentAccount = try { + DataStoreHelper.accounts.firstOrNull { + it.keyIndex == DataStoreHelper.selectedKeyIndex + } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } + + } catch (t: IllegalStateException) { + Log.e("AccountManager", "Activity not found", t) + null + } + + binding?.settingsProfilePic?.setImage(currentAccount?.image) + binding?.settingsProfileText?.text = currentAccount?.name + } + binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml new file mode 100644 index 00000000..b85ace8e --- /dev/null +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index 2c90d958..0b931843 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -24,7 +24,6 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:padding="20dp" - android:visibility="gone" tools:visibility="visible"> + android:scaleType="centerCrop" + android:foreground="@drawable/rounded_outline" + tools:src="@drawable/profile_bg_orange" + android:contentDescription="@string/account"/> + + tools:text="Quick Brown Fox" /> Date: Mon, 22 Apr 2024 16:59:14 +0200 Subject: [PATCH 07/22] Trakt meta provider for extensions (#1026) --- .../metaproviders/TraktProvider.kt | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt new file mode 100644 index 00000000..98e12bcd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -0,0 +1,430 @@ +package com.lagradost.cloudstream3.metaproviders + +import android.net.Uri +import com.lagradost.cloudstream3.* +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.util.Locale +import java.text.SimpleDateFormat +import kotlin.math.roundToInt + +open class TraktProvider : MainAPI() { + override var name = "Trakt" + override val hasMainPage = true + override val providerType = ProviderType.MetaProvider + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") + + override val mainPage = mainPageOf( + "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now + "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time + "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now + "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + return newHomePageResponse(request.name, results) + } + + private fun MediaDetails.toSearchResponse(): SearchResponse { + + val media = this.media ?: this + val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries + val poster = media.images?.poster?.firstOrNull() + + if (mediaType == TvType.Movie) { + return newMovieSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.Movie, + ) { + posterUrl = fixPath(poster) + } + } else { + return newTvSeriesSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.TvSeries, + ) { + this.posterUrl = fixPath(poster) + } + } + } + + override suspend fun search(query: String): List? { + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + + return results + } + override suspend fun load(url: String): LoadResponse { + + val data = parseJson(url) + val mediaDetails = data.mediaDetails + val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" + + val posterUrl = mediaDetails?.images?.poster?.firstOrNull() + val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() + + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + + val actors = parseJson(resActor).cast?.map { + ActorData( + Actor( + name = it.person?.name!!, + image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") + ), + roleString = it.character + ) + } + + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + + val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } + + val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") + val isBollywood = mediaDetails?.country == "in" + + if (data.type == TvType.Movie) { + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + //jpTitle = later if needed as it requires another network request, + airedDate = mediaDetails?.released + ?: mediaDetails?.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + ).toJson() + + return newMovieLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + dataUrl = linkData.toJson(), + type = if (isAnime) TvType.AnimeMovie else TvType.Movie, + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } else { + + val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val episodes = mutableListOf() + val seasons = parseJson>(resSeasons) + val seasonsNames = mutableListOf() + + seasons.forEach { season -> + + seasonsNames.add( + SeasonData( + season.number!!, + season.title + ) + ) + + season.episodes?.map { episode -> + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + season = episode.season, + episode = episode.number, + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + airedYear = mediaDetails?.year, + lastSeason = seasons.size, + epsTitle = episode.title, + //jpTitle = later if needed as it requires another network request, + date = episode.firstAired, + airedDate = episode.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + isCartoon = isCartoon + ).toJson() + + episodes.add( + Episode( + data = linkData.toJson(), + name = episode.title, + season = episode.season, + episode = episode.number, + posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), + rating = episode.rating?.times(10)?.roundToInt(), + description = episode.overview, + ).apply { + this.addDate(episode.firstAired) + } + ) + } + } + + return newTvSeriesLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + type = if (isAnime) TvType.Anime else TvType.TvSeries, + episodes = episodes + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.Anime else TvType.TvSeries + this.episodes = episodes + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.showStatus = getStatus(mediaDetails.status) + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.seasonNames = seasonsNames + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } + } + + private suspend fun getApi(url: String) : String { + return app.get( + url = url, + headers = mapOf( + "Content-Type" to "application/json", + "trakt-api-version" to "2", + "trakt-api-key" to traktClientId, + ) + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + APIHolder.unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + + private fun getStatus(t: String?): ShowStatus { + return when (t) { + "returning series" -> ShowStatus.Ongoing + "continuing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private fun fixPath(url: String?): String? { + url ?: return null + return "https://$url" + } + + private fun getWidthImageUrl(path: String?, width: String) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + val fileName = Uri.parse(path).lastPathSegment ?: return null + return "https://image.tmdb.org/t/p/${width}/${fileName}" + } + + private fun getOriginalWidthImageUrl(path: String?) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + return getWidthImageUrl(path, "original") + } + + data class Data( + val type: TvType? = null, + val mediaDetails: MediaDetails? = null, + ) + + data class MediaDetails( + @JsonProperty("title") val title: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("tagline") val tagline: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("country") val country: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("homepage") val homepage: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("votes") val votes: Long? = null, + @JsonProperty("comment_count") val commentCount: Long? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("languages") val languages: List? = null, + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") val genres: List? = null, + @JsonProperty("certification") val certification: String? = null, + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("airs") val airs: Airs? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + ) + + data class Airs( + @JsonProperty("day") val day: String? = null, + @JsonProperty("time") val time: String? = null, + @JsonProperty("timezone") val timezone: String? = null, + ) + + data class Ids( + @JsonProperty("trakt") val trakt: Int? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") val tvrage: String? = null, + ) + + data class Images( + @JsonProperty("fanart") val fanart: List? = null, + @JsonProperty("poster") val poster: List? = null, + @JsonProperty("logo") val logo: List? = null, + @JsonProperty("clearart") val clearart: List? = null, + @JsonProperty("banner") val banner: List? = null, + @JsonProperty("thumb") val thumb: List? = null, + @JsonProperty("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") val headshot: List? = null, + ) + + data class People( + @JsonProperty("cast") val cast: List? = null, + ) + + data class Cast( + @JsonProperty("character") val character: String? = null, + @JsonProperty("characters") val characters: List? = null, + @JsonProperty("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") val person: Person? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Person( + @JsonProperty("name") val name: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Seasons( + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") val episodes: List? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class TraktEpisode( + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class LinkData( + val id: Int? = null, + val imdbId: String? = null, + val tvdbId: Int? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + val aniId: String? = null, + val animeId: String? = null, + val title: String? = null, + val year: Int? = null, + val orgTitle: String? = null, + val isAnime: Boolean = false, + val airedYear: Int? = null, + val lastSeason: Int? = null, + val epsTitle: String? = null, + val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, + val isAsian: Boolean = false, + val isBollywood: Boolean = false, + val isCartoon: Boolean = false, + ) +} \ No newline at end of file From e6b9d621f96beba6e427aa092d09bb448caf8d93 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:00:27 +0200 Subject: [PATCH 08/22] feat(ui): added option to reset sub delay (#1041) --- .../ui/player/AbstractPlayerFragment.kt | 14 ++++++++------ .../lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index cfa6682d..0865b220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.DefaultTimeBar -import androidx.media3.ui.PlayerView -import androidx.media3.ui.SubtitleView -import androidx.media3.ui.TimeBar +import androidx.media3.ui.* import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar @@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> + // Resets subtitle delay on ended video + player.setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 210bfdca..31adbc87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { + // Resets subtitle delay on ended video + setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( From e2946cad6b0eb2ef602174f8da38ab1a289ac8e2 Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Sun, 28 Apr 2024 00:00:40 +0800 Subject: [PATCH 09/22] Added Vidguard Extractor (#1053) --- .../cloudstream3/extractors/Vidguard.kt | 101 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} 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 5a845326..592dc6f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -186,6 +186,7 @@ import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -888,7 +889,8 @@ val extractorApis: MutableList = arrayListOf( StreamWishExtractor(), EmturbovidExtractor(), Vtbe(), - EPlayExtractor() + EPlayExtractor(), + Vidguardto() ) From 004c481a5eb8ac8bb0c5a486f2e1f5b35e414f52 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 27 Apr 2024 19:11:22 +0300 Subject: [PATCH 10/22] feat(ui): Episode Air date & Upcoming countdown (#1058) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 34 ++++++++++++++++++- .../cloudstream3/ui/result/ResultFragment.kt | 5 ++- .../ui/result/ResultViewModel2.kt | 6 ++-- app/src/main/res/drawable/hourglass_24.xml | 9 +++++ .../main/res/layout/result_episode_large.xml | 23 +++++++++++-- app/src/main/res/values/strings.xml | 1 + 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/hourglass_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index fad349c8..2019aa50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,9 +9,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent @@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -104,7 +108,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -260,6 +264,33 @@ class EpisodeAdapter( } } + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -271,6 +302,7 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index a1574eec..1d3f5a08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } 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 37a905a7..499fced2 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 @@ -2277,7 +2277,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2326,7 +2327,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + Episodes %1$d-%2$d %1$d %2$s + Upcoming in %s S E No Episodes found From 138e1a1f0ea4515c33274ac4fa3805e9595dd85e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Apr 2024 04:40:15 +0800 Subject: [PATCH 11/22] Don't check year when checking duplicates if year is empty (#1060) Some sources don't use year which makes this not match when it really should match --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 499fced2..de339aee 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 @@ -1099,13 +1099,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && year == it.year } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } From ff1ffbeb836a1bc94d002044ba1863e93fd654dc Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Mon, 29 Apr 2024 03:42:38 +0800 Subject: [PATCH 12/22] Update Voe.kt (#1062) --- .../lagradost/cloudstream3/extractors/Voe.kt | 66 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 10 ++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} 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 592dc6f9..75dceb54 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -83,6 +83,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -139,6 +140,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +177,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -208,6 +211,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -890,7 +894,11 @@ val extractorApis: MutableList = arrayListOf( EmturbovidExtractor(), Vtbe(), EPlayExtractor(), - Vidguardto() + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers() ) From 949b5830b644d3ac23216dd533d40943ab5f6347 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 1 May 2024 20:29:49 +0300 Subject: [PATCH 13/22] feat(ui): Fix downloads focus on TV (#1066) --- .../cloudstream3/ui/download/DownloadChildFragment.kt | 3 ++- .../cloudstream3/ui/download/DownloadFragment.kt | 3 +++ .../java/com/lagradost/cloudstream3/utils/UIHelper.kt | 10 ++++++++++ app/src/main/res/layout/download_child_episode.xml | 5 ++++- app/src/main/res/layout/download_header_episode.xml | 6 +++++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..d138a1e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -89,9 +90,9 @@ class DownloadChildFragment : Fragment() { setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e08eb772..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index eedb626a..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -45,6 +45,7 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -208,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file From c07e6d3222123ce9b711cafa8827f682f9ad9516 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:58:32 +0200 Subject: [PATCH 14/22] hotfix: Remove resume information (#1063) --- .../cloudstream3/ui/download/button/PieFetchButton.kt | 4 ++++ .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a729f33a..f1031c24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -13,6 +13,8 @@ import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE @@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES open class PieFetchButton(context: Context, attributeSet: AttributeSet) : @@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { + removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 50a8df02..7d4d5d98 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -187,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - private const val KEY_RESUME_PACKAGES = "download_resume" + const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" From d3828eeafed0fd4fbeb32c4d37dee2126296b564 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:59:05 +0200 Subject: [PATCH 15/22] refact: rename logcat file (#1061) Rename logcat file to prevent override --- .../cloudstream3/ui/settings/SettingsUpdates.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index fb24c185..4aaa5e12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = - VideoDownloadManager.setupStream( + fileStream = VideoDownloadManager.setupStream( it.context, - "logcat", + "logcat_${date}", null, "txt", false From c28a3cb9873d64634b1e7bb131ef648ab40fd22e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 4 May 2024 04:15:34 -0700 Subject: [PATCH 16/22] Extractor: new VidSrcTo extractor (#1044) --- .../cloudstream3/extractors/VidSrcTo.kt | 65 +++++++++++++++++++ .../cloudstream3/extractors/Vidplay.kt | 4 ++ .../metaproviders/TmdbProvider.kt | 2 + .../cloudstream3/utils/ExtractorApi.kt | 2 + 4 files changed, 73 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt new file mode 100644 index 00000000..b9065688 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -0,0 +1,65 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } + } + + private fun DecryptUrl(encUrl: String): String { + var data = encUrl.toByteArray() + data = Base64.decode(data, Base64.URL_SAFE) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + data = cipher.doFinal(data) + return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index d5d0fb32..c5e01552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key +class AnyVidplay(hostUrl: String) : Vidplay() { + override val mainUrl = hostUrl +} + class MyCloud : Vidplay() { override val name = "MyCloud" override val mainUrl = "https://mcloud.bz" diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 50301e22..c5b4d453 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, + this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, + this.name ?: this.original_name, ).toJson(), season = season.season_number ) 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 75dceb54..6106845e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -185,6 +185,7 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime @@ -876,6 +877,7 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), + VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), From 83c473d9f801cc43c0716453bea79afc539a1fea Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:16:09 +0300 Subject: [PATCH 17/22] More external Ids in Trakt meta provider (#1075) --- .../cloudstream3/metaproviders/TraktProvider.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 98e12bcd..37c6be1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -118,8 +118,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), title = mediaDetails?.title, year = mediaDetails?.year, @@ -139,7 +143,6 @@ open class TraktProvider : MainAPI() { type = if (isAnime) TvType.AnimeMovie else TvType.Movie, ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie this.posterUrl = getOriginalWidthImageUrl(posterUrl) this.year = mediaDetails.year @@ -177,8 +180,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), season = episode.season, episode = episode.number, @@ -220,7 +227,6 @@ open class TraktProvider : MainAPI() { episodes = episodes ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.Anime else TvType.TvSeries this.episodes = episodes this.posterUrl = getOriginalWidthImageUrl(posterUrl) @@ -406,8 +412,12 @@ open class TraktProvider : MainAPI() { data class LinkData( val id: Int? = null, + val traktId: Int? = null, + val traktSlug: String? = null, + val tmdbId: Int? = null, val imdbId: String? = null, val tvdbId: Int? = null, + val tvrageId: String? = null, val type: String? = null, val season: Int? = null, val episode: Int? = null, From 71bd48f4930d255beabe6f86b7e4057b732dc70e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:17:52 +0300 Subject: [PATCH 18/22] feat(ui): Hide Downloads & Settings Back button on TV (#1074) --- .../ui/download/DownloadChildFragment.kt | 11 ++++++++--- .../ui/quicksearch/QuickSearchFragment.kt | 12 ++++++++++-- .../ui/settings/SettingsFragment.kt | 19 ++++++++++++------- app/src/main/res/layout/quick_search.xml | 7 +++---- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index d138a1e6..f54c8698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -86,9 +89,11 @@ class DownloadChildFragment : Fragment() { binding?.downloadChildToolbar?.apply { title = name - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } setAppBarNoScrollFlagsOnTV() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index e9e00736..85e20d1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow @@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - binding?.quickSearchBack?.setOnClickListener { - activity?.popCurrentPage() + if (isLayout(PHONE or EMULATOR)) { + binding?.quickSearchBack?.apply { + isVisible = true + setOnClickListener { + activity?.popCurrentPage() + } + } } if (isLayout(TV)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 443eeda7..8ac17928 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.account import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper @@ -84,9 +85,11 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -98,10 +101,12 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) diff --git a/app/src/main/res/layout/quick_search.xml b/app/src/main/res/layout/quick_search.xml index 12d94aaa..84f2c548 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -23,11 +23,10 @@ android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_arrow_back_24" app:tint="@android:color/white" - android:focusable="true" + android:visibility="gone" android:layout_width="25dp" - android:layout_height="wrap_content"> - - + android:layout_height="wrap_content" + tools:visibility="visible"> Date: Sun, 5 May 2024 04:30:42 +0530 Subject: [PATCH 19/22] Updates and Chillx Extractor Updated (#1065) --- .../cloudstream3/extractors/Chillx.kt | 48 ++++++++++--------- .../cloudstream3/extractors/EPlay.kt | 1 - .../lagradost/cloudstream3/extractors/Vtbe.kt | 1 - 3 files changed, 25 insertions(+), 25 deletions(-) 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 f03a5525..26567c7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler -import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true - private var key: String? = null + companion object { + private var key: String? = null + + suspend fun fetchKey(): String { + return if (key != null) { + key!! + } else { + val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") + key = fetch + key!! + } + } + } + + @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("\\s*=\\s*'([^']+)").find( + val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( app.get( url, - referer = referer ?: "", - headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language" to "en-US,en;q=0.5", - ) + referer = url, ).text )?.groupValues?.get(1) - val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - + val key = fetchKey() + val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + 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 { - return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() - } - data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + + data class Keys( + @JsonProperty("chillx") val key: List ) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt index 565a2680..2cb12e16 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt index 65af01ec..919a9cbd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* From 3874cb9f9d3a2b0894c59346b92fb6eec4fa2b2e Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Thu, 9 May 2024 23:06:33 +0800 Subject: [PATCH 20/22] Update Dailymotion Extractor (#1081) --- .../cloudstream3/extractors/Dailymotion.kt | 23 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 5 +++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 0df93dc5..2343a92e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL +class Geodailymotion : Dailymotion() { + override val name = "GeoDailymotion" + override val mainUrl = "https://geo.dailymotion.com" +} + open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false + private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/")) { - return url - } - val vid = getVideoId(url) ?: return null - return "$mainUrl/embed/video/$vid" + if (url.contains("/embed/") || url.contains("/video/")) { + return url } + if (url.contains("geo.dailymotion.com")) { + val videoId = url.substringAfter("video=") + return "$baseUrl/embed/video/$videoId" + } + return null + } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("video/") + val id = path.substringAfter("/video/") if (id.matches(videoIdRegex)) { return id } 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 6106845e..0e4dc870 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -900,7 +901,9 @@ val extractorApis: MutableList = arrayListOf( Simpulumlamerop(), Urochsunloath(), Yipsu(), - MetaGnathTuggers() + MetaGnathTuggers(), + Geodailymotion(), + ) From f1cc4db89cc6c1a2cd6316e81340206b56a72ad6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 9 May 2024 18:08:18 +0300 Subject: [PATCH 21/22] Show Season number for next airing episode (#1071) --- .../com/lagradost/cloudstream3/MainAPI.kt | 17 +++++++-- .../ui/result/ResultViewModel2.kt | 6 +++- .../main/res/layout/fragment_result_tv.xml | 36 +++++++++---------- app/src/main/res/values/strings.xml | 1 + 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7b1b5775..699159b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1448,11 +1448,24 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } - data class NextAiring( val episode: Int, val unixTime: Long, -) + val season: Int? = null, +) { + /** + * Secondary constructor for backwards compatibility without season. + * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. + */ + constructor( + episode: Int, + unixTime: Long, + ) : this ( + episode, + unixTime, + null + ) +} /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined 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 de339aee..61b65bc2 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 @@ -197,7 +197,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = when (airing.season) { + + null -> txt(R.string.next_episode_format, airing.episode) + else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) + } } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 2ec2ae0a..893c19ff 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:textStyle="bold" tools:text="The Perfect Run The Perfect Run" /> + + - - + android:orientation="horizontal"> + android:layout_marginEnd="5dp" + tools:text="Season 2 Episode 1022 will be released in" /> %1$s Ep %2$d Cast: %s Episode %d will be released in + Season %1$d Episode %2$d will be released in %1$dd %2$dh %3$dm %1$dh %2$dm %dm From ee4d1dedc5adb1be656a05d1ee0f41b11f9d0a84 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 9 May 2024 19:46:54 +0000 Subject: [PATCH 22/22] Add basic fcast support (#1084) --- .../lagradost/cloudstream3/MainActivity.kt | 3 + .../cloudstream3/ui/player/IGenerator.kt | 10 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 + .../ui/result/ResultViewModel2.kt | 44 ++++++ .../cloudstream3/utils/ExtractorApi.kt | 13 +- .../cloudstream3/utils/fcast/FcastManager.kt | 135 ++++++++++++++++++ .../cloudstream3/utils/fcast/FcastSession.kt | 60 ++++++++ .../cloudstream3/utils/fcast/Packets.kt | 62 ++++++++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 7baac71c..56322b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -161,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.fcast.FcastManager import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile @@ -1756,6 +1757,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, runAutoUpdate() } + FcastManager().init(this, false) + APIRepository.dubStatusActive = getApiDubstatusSettings() try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57..c5de1a1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -10,7 +10,8 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast + Chromecast, + Fcast } fun LoadType.toSet() : Set { @@ -29,12 +30,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) + LoadType.Fcast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 2019aa50..e4fd0559 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -55,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 +const val ACTION_FCAST = 19 + const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) 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 61b65bc2..a32942f6 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 @@ -83,6 +83,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.cloudstream3.utils.fcast.FcastSession +import com.lagradost.cloudstream3.utils.fcast.Opcode +import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -1519,6 +1523,13 @@ class ResultViewModel2 : ViewModel() { ) ) } + + if (FcastManager.currentDevices.isNotEmpty()) { + options.add( + txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1694,6 +1705,39 @@ class ResultViewModel2 : ViewModel() { } } + ACTION_FCAST -> { + val devices = FcastManager.currentDevices.toList() + postPopup( + txt(R.string.player_settings_select_cast_device), + devices.map { txt(it.name) }) { index -> + if (index == null) return@postPopup + val device = devices.getOrNull(index) + + acquireSingleLink( + click.data, + LoadType.Fcast, + txt(R.string.episode_action_cast_mirror) + ) { (result, index) -> + val host = device?.host ?: return@acquireSingleLink + val link = result.links.firstOrNull() ?: return@acquireSingleLink + + FcastSession(host).use { session -> + session.sendMessage( + Opcode.Play, + PlayMessage( + link.type.getMimeType(), + link.url, + headers = mapOf( + "referer" to link.referer, + "user-agent" to USER_AGENT + ) + link.headers + ) + ) + } + } + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, 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 0e4dc870..61cdd26a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -308,7 +308,18 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET, + MAGNET; + + // See https://www.iana.org/assignments/media-types/media-types.xhtml + fun getMimeType(): String { + return when (this) { + VIDEO -> "video/mp4" + M3U8 -> "application/x-mpegURL" + DASH -> "application/dash+xml" + TORRENT -> "application/x-bittorrent" + MAGNET -> "application/x-bittorrent" + } + } } private fun inferTypeFromUrl(url: String): ExtractorLinkType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt new file mode 100644 index 00000000..9ff5cc08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.ResolveListener +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe + +class FcastManager { + private var nsdManager: NsdManager? = null + + // Used for receiver + private val registrationListenerTcp = DefaultRegistrationListener() + private fun getDeviceName(): String { + return "${Build.MANUFACTURER}-${Build.MODEL}" + } + + /** + * Start the fcast service + * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app + */ + fun init(context: Context, registerReceiver: Boolean) = ioSafe { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + val serviceType = "_fcast._tcp" + + if (registerReceiver) { + val serviceName = "$APP_PREFIX-${getDeviceName()}" + + val serviceInfo = NsdServiceInfo().apply { + this.serviceName = serviceName + this.serviceType = serviceType + this.port = TCP_PORT + } + + nsdManager?.registerService( + serviceInfo, + NsdManager.PROTOCOL_DNS_SD, + registrationListenerTcp + ) + } + + nsdManager?.discoverServices( + serviceType, + NsdManager.PROTOCOL_DNS_SD, + DefaultDiscoveryListener() + ) + } + + fun stop() { + nsdManager?.unregisterService(registrationListenerTcp) + } + + inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { + val tag = "DiscoveryListener" + override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") + } + + override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") + } + + override fun onDiscoveryStarted(serviceType: String?) { + Log.d(tag, "Discovery started: $serviceType") + } + + override fun onDiscoveryStopped(serviceType: String?) { + Log.d(tag, "Discovery stopped: $serviceType") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + nsdManager?.resolveService(serviceInfo, object : ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + currentDevices.add(PublicDeviceInfo(serviceInfo)) + + Log.d( + tag, + "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" + ) + } + }) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + // May remove duplicates, but net and port is null here, preventing device specific identification + currentDevices.removeAll { + it.rawName == serviceInfo.serviceName + } + + Log.d(tag, "Service lost: ${serviceInfo.serviceName}") + } + } + + companion object { + const val APP_PREFIX = "CloudStream" + val currentDevices: MutableList = mutableListOf() + + class DefaultRegistrationListener : NsdManager.RegistrationListener { + val tag = "DiscoveryService" + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service registered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service registration failed: errorCode=$errorCode") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service unregistration failed: errorCode=$errorCode") + } + } + + const val TCP_PORT = 46899 + } +} + +class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { + val rawName: String = serviceInfo.serviceName + val host: String? = serviceInfo.host.hostAddress + val name = rawName.replace("-", " ") + host?.let { " $it" } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt new file mode 100644 index 00000000..1f33bca4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.safefile.closeQuietly +import java.io.DataOutputStream +import java.net.Socket +import kotlin.jvm.Throws + +class FcastSession(private val hostAddress: String): AutoCloseable { + val tag = "FcastSession" + + private var socket: Socket? = null + @Throws + @WorkerThread + fun open(): Socket { + val socket = Socket(hostAddress, FcastManager.TCP_PORT) + this.socket = socket + return socket + } + + override fun close() { + socket?.closeQuietly() + socket = null + } + + @Throws + private fun acquireSocket(): Socket { + return socket ?: open() + } + + fun ping() { + sendMessage(Opcode.Ping, null) + } + + fun sendMessage(opcode: Opcode, message: T) { + ioSafe { + val socket = acquireSocket() + val outputStream = DataOutputStream(socket.getOutputStream()) + + val json = message?.toJson() + val content = json?.toByteArray() ?: ByteArray(0) + + // Little endian starting from 1 + // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 + val size = content.size + 1 + + val sizeArray = ByteArray(4) { num -> + (size shr 8 * num and 0xff).toByte() + } + + Log.d(tag, "Sending message with size: $size, opcode: $opcode") + outputStream.write(sizeArray) + outputStream.write(ByteArray(1) { opcode.value }) + outputStream.write(content) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt new file mode 100644 index 00000000..61c00d6e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils.fcast + +// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 +enum class Opcode(val value: Byte) { + None(0), + Play(1), + Pause(2), + Resume(3), + Stop(4), + Seek(5), + PlaybackUpdate(6), + VolumeUpdate(7), + SetVolume(8), + PlaybackError(9), + SetSpeed(10), + Version(11), + Ping(12), + Pong(13); +} + + +data class PlayMessage( + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null +) + +data class SeekMessage( + val time: Double +) + +data class PlaybackUpdateMessage( + val generationTime: Long, + val time: Double, + val duration: Double, + val state: Int, + val speed: Double +) + +data class VolumeUpdateMessage( + val generationTime: Long, + val volume: Double +) + +data class PlaybackErrorMessage( + val message: String +) + +data class SetSpeedMessage( + val speed: Double +) + +data class SetVolumeMessage( + val volume: Double +) + +data class VersionMessage( + val version: Long +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd2426c..a8108623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,6 +357,7 @@ Download error, check storage permissions Chromecast episode Chromecast mirror + Cast mirror Play in app Play in %s Play in browser @@ -634,7 +635,9 @@ VLC MPV Web Video Cast + Fcast Web browser + Select cast device App not found All Languages Skip %s