Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ android {
"SIMKL_CLIENT_SECRET",
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
)
buildConfigField(
"String",
"GOOGLE_CLIENT_ID",
"\"" + (localProperties["google.client_id"] ?: "") + "\""
)
buildConfigField(
"String",
"MAL_KEY",
Expand Down Expand Up @@ -289,6 +294,13 @@ dependencies {
implementation(libs.nicehttp) // HTTP Lib

implementation(project(":library"))

// Google Drive Sync
implementation(libs.play.services.auth) // AuthorizationClient for Drive token
implementation(libs.credentials)
implementation(libs.credentials.play.services.auth)
implementation(libs.googleid)
implementation(libs.kotlinx.coroutines.play.services) // .await() on GMS Tasks
}

tasks.register<Jar>("androidSourcesJar") {
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STR
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SHARE
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.google.SyncManager
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.WatchType
Expand Down Expand Up @@ -518,6 +519,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins,
R.id.navigation_test_providers,
R.id.navigation_settings_sync,
).contains(destination.id)


Expand Down Expand Up @@ -637,6 +639,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}

override fun onStart() {
super.onStart()
val prefs = getSharedPreferences("cs3_sync_prefs", Context.MODE_PRIVATE)
SyncManager.trySilentAuth(this)
if (prefs.getBoolean("sync_auto_enabled", false) && SyncManager.isEnabled(this)) {
SyncManager.pull(this)
}
}

override fun onResume() {
super.onResume()
afterPluginsLoadedEvent += ::onAllPluginsLoaded
Expand Down Expand Up @@ -667,6 +678,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}

override fun onStop() {
super.onStop()
val prefs = getSharedPreferences("cs3_sync_prefs", Context.MODE_PRIVATE)
if (prefs.getBoolean("sync_auto_enabled", false) && SyncManager.isEnabled(this)) {
SyncManager.push(this)
}
}

override fun dispatchKeyEvent(event: KeyEvent): Boolean =
CommonActivity.dispatchKeyEvent(this, event) ?: super.dispatchKeyEvent(event)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,56 @@ object PluginManager {
}
}

@Suppress("FunctionName", "DEPRECATION_ERROR")
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_restoreSyncPlugins(context: Context) {
assertNonRecursiveCallstack()
Log.d(TAG, "Restoring synced plugins...")

val onlinePlugins = getPluginsOnline().toList()
Log.d(TAG, "Found ${onlinePlugins.size} plugins in sync list")

var pluginsChanged = false

val updatedPlugins = onlinePlugins.amap { savedData ->
val oldFile = File(savedData.filePath)
val parentName = oldFile.parentFile?.name
val fileName = oldFile.name
var currentData = savedData

if (parentName != null) {
val newFile = File(context.filesDir, "$ONLINE_PLUGINS_FOLDER/$parentName/$fileName")
Log.d(TAG, "Mapping plugin: ${savedData.internalName} -> ${newFile.absolutePath}")

if (savedData.filePath != newFile.absolutePath) {
currentData = currentData.copy(filePath = newFile.absolutePath)
pluginsChanged = true
}

if (!newFile.exists() && currentData.url != null) {
Log.d(TAG, "Missing plugin file, downloading: ${currentData.internalName}")
val downloadedFile = downloadPluginToFile(
context,
currentData.url,
newFile,
null
)
if (downloadedFile == null) {
Log.e(TAG, "Failed to download plugin ${currentData.internalName}")
}
}
}
currentData
}

if (pluginsChanged) {
setKey(PLUGINS_KEY, updatedPlugins.toTypedArray())
}

___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context)
afterPluginsLoadedEvent.invoke(false)
}

/**
* Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb
*
Expand Down Expand Up @@ -520,42 +570,31 @@ object PluginManager {
}

val sortedPlugins = dir.listFiles()
// Always sort plugins alphabetically for reproducible results
Log.d(TAG, "Found ${sortedPlugins?.size ?: 0} local files in $LOCAL_PLUGINS_PATH")


Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: ${sortedPlugins?.size}")

// Use app-specific external files directory and copy the file there.
// We have to do this because on Android 14+, it otherwise gives SecurityException
// due to dex files and setReadOnly seems to have no effect unless it it here.
val pluginDirectory = File(context.getExternalFilesDir(null), "plugins")
if (!pluginDirectory.exists()) {
pluginDirectory.mkdirs() // Ensure the plugins directory exists
pluginDirectory.mkdirs()
}

// Make sure all local plugins are fully refreshed.
removeKey(PLUGINS_KEY_LOCAL)

sortedPlugins?.sortedBy { it.name }?.amap { file ->
Log.d(TAG, "Processing local file: ${file.name}")
try {
val destinationFile = File(pluginDirectory, file.name)

// Only copy the file if the destination file doesn't exist or if it
// has been modified (check file length and modification time).
if (!destinationFile.exists() ||
destinationFile.length() != file.length() ||
destinationFile.lastModified() != file.lastModified()
) {

// Copy the file to the app-specific plugin directory
file.copyTo(destinationFile, overwrite = true)

// After copying, set the destination file's modification time
// to match the source file. We do this for performance so that we
// can check the modification time and not make redundant writes.
destinationFile.setLastModified(file.lastModified())
}

// Load the plugin after it has been copied
maybeLoadPlugin(context, destinationFile)
} catch (t: Throwable) {
Log.e(TAG, "Failed to copy the file")
Expand All @@ -572,11 +611,8 @@ object PluginManager {
return checkSafeModeFile() || lastError != null
}

/**
* This can be used to override any extension loading to fix crashes!
* @return true if safe mode file is present
**/
fun checkSafeModeFile(): Boolean {

return safe {
val folder = File(CLOUD_STREAM_FOLDER)
if (!folder.exists()) return@safe false
Expand All @@ -587,9 +623,7 @@ object PluginManager {
} ?: false
}

/**
* @return True if successful, false if not
* */

private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension
val filePath = file.absolutePath
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.plugins

import android.content.Context
import android.util.Log
import androidx.annotation.WorkerThread
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
Expand Down Expand Up @@ -131,31 +132,40 @@ object RepositoryManager {

suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim()
return if (fixedUrl.contains("^https?://".toRegex())) {
fixedUrl
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
return@let if (!it.contains("^https?://".toRegex()))
"https://${it}"
else fixedUrl

// 1. Direct http/https URLs
if (fixedUrl.contains("^https?://".toRegex())) {
return fixedUrl
}

// 2. Custom protocols (cloudstreamrepo:// or cs.repo)
val customProtoRegex = "^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex()
if (fixedUrl.contains(customProtoRegex)) {
val stripped = fixedUrl.replace(customProtoRegex, "")
return if (!stripped.contains("^https?://".toRegex())) {
"https://$stripped"
} else {
fixedUrl
}
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
safeAsync {
if (fixedUrl.startsWith("!")) {
val response = app.get("https://py.md/${fixedUrl.removePrefix("!")}", allowRedirects = false)
val url = response.headers["Location"] ?: return@safeAsync null
if (url.startsWith("https://py.md/404")) return@safeAsync null
if (url.removeSuffix("/") == "https://py.md") return@safeAsync null
return@safeAsync url
} else {
val response = app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false)
val url = response.headers["Location"] ?: return@safeAsync null
if (url.startsWith("https://cutt.ly/404")) return@safeAsync null
if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null
return@safeAsync url
}
}

// 3. URL shorteners / IDs (py.md or cutt.ly)
if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
return safeAsync {
val isPyMd = fixedUrl.startsWith("!")
val baseUrl = if (isPyMd) "https://py.md" else "https://cutt.ly"
val path = if (isPyMd) fixedUrl.removePrefix("!") else fixedUrl

val response = app.get("$baseUrl/$path", allowRedirects = false)
val location = response.headers["Location"] ?: return@safeAsync null

if (location.startsWith("$baseUrl/404")) return@safeAsync null
if (location.removeSuffix("/") == baseUrl) return@safeAsync null
location
}
} else null
}

return null
}

suspend fun parseRepository(url: String): Repository? {
Expand Down Expand Up @@ -196,6 +206,8 @@ object RepositoryManager {
file: File,
expectedFileHash: String?
): File? {
Log.d("RepositoryManager", "Downloading $pluginUrl to ${file.absolutePath}")

return safeAsync {
val parentDir = file.parentFile ?: return@safeAsync null
parentDir.mkdirs()
Expand Down
Loading