Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
12241e8
feat: Add Firebase Profiles and History Tab auto-saving
zevidriz-cmd May 24, 2026
5212081
ci: Update prerelease action to use local keystore
zevidriz-cmd May 24, 2026
f189d79
ci: Add keystore for automated builds
zevidriz-cmd May 24, 2026
16ae650
ci: trigger github actions build
zevidriz-cmd May 24, 2026
f089cb2
ci: trigger github actions build 2
zevidriz-cmd May 24, 2026
cc58961
ci: add workflow_dispatch to prerelease
zevidriz-cmd May 24, 2026
17ad158
ci: add dummy google-services.json for github actions
zevidriz-cmd May 24, 2026
727f2e2
ci: capture gradle failure logs
zevidriz-cmd May 24, 2026
5813cf7
fix: suppress GestureBackNavigation lint error
zevidriz-cmd May 24, 2026
171efb7
Implement updates and fixes: auto-update, PIN entry haptics, UI tweak…
zevidriz-cmd May 24, 2026
f5b2f36
Fix NullPointerException in ProfileSelectorFragment on destroy
zevidriz-cmd May 24, 2026
8edebd1
Rename app to Cloudstream Plus, remove -PRE suffix, and default to fo…
zevidriz-cmd May 24, 2026
3ed81ad
Fix sync snackbar and watch history logic
zevidriz-cmd May 24, 2026
0944f50
Fix bugs in sync manager, plugins, and Android TV auth pairing
zevidriz-cmd May 25, 2026
68c5217
Trigger fresh new push
zevidriz-cmd May 25, 2026
eb97983
Fix Android Lint orientation error in rail_footer.xml
zevidriz-cmd May 25, 2026
8e8aafc
Update: Cloudstream Plus rebranding, update progress UI, history sync…
zevidriz-cmd May 25, 2026
3bd111a
Add subtitle framerate stretch controls and smart presets
zevidriz-cmd May 25, 2026
a4ae2e2
Fix subtitle drop-off bug, and overhaul app updater UI/install intent
zevidriz-cmd May 25, 2026
e6129de
Fix unresolved reference to C in CustomSubtitleDecoderFactory
zevidriz-cmd May 25, 2026
1e54512
Fix TV Authentication flow and QR scanner bugs
zevidriz-cmd May 25, 2026
e0b7e70
Add Prioritize Subtitles toggle to search filters
zevidriz-cmd May 26, 2026
ec52f29
Fix UI overlap for subtitle toggle switch
zevidriz-cmd May 26, 2026
0f088d5
Fix TV UI crash, add interactive Google Sign-In fallback for pairing
zevidriz-cmd May 26, 2026
305f117
Feature: Add MDBList IMDb and Rotten Tomatoes ratings to movie/series…
zevidriz-cmd May 26, 2026
9884044
Fix pairing crash on mobile app caused by blocking Tasks.await() on m…
zevidriz-cmd May 26, 2026
f2dbeba
Bump version to 4.7.1 for new release
zevidriz-cmd May 26, 2026
1be187f
Fix Android build and TV pairing errors. Bump version to 4.7.2.
zevidriz-cmd May 26, 2026
575a79a
Fix GitHub actions keystore path for release build. Bump version to 4…
zevidriz-cmd May 26, 2026
acaee8b
Inject MDBLIST_API_KEY directly into GitHub Actions workflow
zevidriz-cmd May 26, 2026
5830f74
Remove leaked google-services.json and add missing workflow secrets
zevidriz-cmd May 26, 2026
32a87a6
Hardcode MDBList API key
zevidriz-cmd May 26, 2026
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
5 changes: 5 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {},
"targets": {},
"etags": {}
}
7 changes: 7 additions & 0 deletions .github/workflows/build_to_archive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ jobs:
echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT

- name: Setup Google Services
run: |
if [ -n "${{ secrets.GOOGLE_SERVICES_JSON }}" ]; then
echo "${{ secrets.GOOGLE_SERVICES_JSON }}" > app/google-services.json
fi

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
Expand All @@ -72,6 +78,7 @@ jobs:
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
MDBLIST_API_KEY: ${{ secrets.MDBLIST_API_KEY }}

- uses: actions/checkout@v6
with:
Expand Down
47 changes: 24 additions & 23 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Pre-release

on:
workflow_dispatch:
push:
branches: [ master ]
paths-ignore:
Expand All @@ -19,14 +20,6 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"

- uses: actions/checkout@v6

- name: Set up JDK 17
Expand All @@ -38,31 +31,39 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Fetch keystore
id: fetch_keystore
- name: Setup Keystore
run: |
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
cp keystore.jks "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore"

- name: Setup Google Services
run: |
if [ -n "${{ secrets.GOOGLE_SERVICES_JSON }}" ]; then
echo "${{ secrets.GOOGLE_SERVICES_JSON }}" > app/google-services.json
fi

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}

- name: Run Gradle
run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar
run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar > gradle.log 2>&1 || (cat gradle.log ; exit 1)
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
SIGNING_KEY_PASSWORD: "cloudstream"
SIGNING_STORE_PASSWORD: "cloudstream"
MDBLIST_API_KEY: ${{ secrets.MDBLIST_API_KEY }}

- name: Push Logs
if: failure()
run: |
git config --global user.email "bot@example.com"
git config --global user.name "Bot"
git fetch
git checkout -b build-logs
git add gradle.log
git commit -m "Add build logs"
git push origin build-logs -f

- name: Create pre-release
uses: marvinpinto/action-automatic-releases@latest
Expand Down
38 changes: 33 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.dokka)
alias(libs.plugins.google.services)
}

val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
Expand Down Expand Up @@ -87,9 +88,13 @@ android {
if (System.getenv("SIGNING_KEY_ALIAS") != null) {
create("prerelease") {
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
val githubActionsStoreFile = File(tmpFilePath).listFiles()?.firstOrNull()
val envStoreFile = System.getenv("SIGNING_STORE_FILE")?.let { File(it) }
val rootStoreFile = File(rootDir, "keystore.jks")

val prereleaseStoreFile = githubActionsStoreFile ?: envStoreFile ?: rootStoreFile

storeFile = prereleaseStoreFile?.let { file(it) }
storeFile = prereleaseStoreFile
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
Expand All @@ -103,8 +108,8 @@ android {
applicationId = "com.lagradost.cloudstream3"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 68
versionName = "4.7.0"
versionCode = 71
versionName = "4.7.3"

manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()

Expand All @@ -126,6 +131,11 @@ android {
"SIMKL_CLIENT_SECRET",
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
)
buildConfigField(
"String",
"MDBLIST_API_KEY",
"\"aorzuy9py52y89nl78oaoskad\""
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down Expand Up @@ -162,7 +172,6 @@ android {
} else {
logger.warn("No prerelease signing config!")
}
versionNameSuffix = "-PRE"
versionCode = (System.currentTimeMillis() / 60000).toInt()
}
}
Expand Down Expand Up @@ -248,6 +257,7 @@ dependencies {
implementation(libs.palette.ktx) // Palette for Images -> Colors
implementation(libs.tvprovider)
implementation(libs.overlappingpanels) // Gestures
implementation("com.journeyapps:zxing-android-embedded:4.3.0") // QR Scanner
implementation(libs.biometric) // Fingerprint Authentication
implementation(libs.previewseekbar.media3) // SeekBar Preview
implementation(libs.qrcode.kotlin) // QR Code for PIN Auth on TV
Expand All @@ -269,9 +279,27 @@ dependencies {
implementation(libs.work.runtime.ktx)
implementation(libs.nicehttp) // HTTP Lib

// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.bundles.firebase)

// Google Sign-In (Credential Manager)
implementation(libs.credentials)
implementation(libs.credentials.play.services.auth)
implementation("com.google.android.gms:play-services-auth:21.0.0")
implementation(project(":library"))
}

configurations.all {
resolutionStrategy {
force("com.google.protobuf:protobuf-javalite:3.25.5")
}
}

// configurations.all {
// exclude(group = "com.google.firebase", module = "protolite-well-known-types")
// }

tasks.register<Jar>("androidSourcesJar") {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.directories) // Full Sources
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity
android:name=".ui.sync.CustomScannerActivity"
android:screenOrientation="portrait"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme" />
</application>

</manifest>
33 changes: 33 additions & 0 deletions app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,39 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory {
}

AppDebug.isDebug = BuildConfig.DEBUG

registerActivityLifecycleCallbacks(object : android.app.Application.ActivityLifecycleCallbacks {
private var startedActivities = 0
private var isChangingConfiguration = false

override fun onActivityCreated(activity: android.app.Activity, savedInstanceState: android.os.Bundle?) {}

override fun onActivityStarted(activity: android.app.Activity) {
if (startedActivities == 0) {
isChangingConfiguration = false
}
startedActivities++
}

override fun onActivityResumed(activity: android.app.Activity) {}
override fun onActivityPaused(activity: android.app.Activity) {}

override fun onActivityStopped(activity: android.app.Activity) {
startedActivities--
if (startedActivities == 0) {
if (!activity.isChangingConfigurations) {
// App went to background, lock it instantly
com.lagradost.cloudstream3.ui.account.AccountSelectActivity.hasLoggedIn = false
com.lagradost.cloudstream3.ui.sync.ProfileSelectorFragment.hasFirebaseLoggedIn = false
} else {
isChangingConfiguration = true
}
}
}

override fun onActivitySaveInstanceState(activity: android.app.Activity, outState: android.os.Bundle) {}
override fun onActivityDestroyed(activity: android.app.Activity) {}
})
}

override fun attachBaseContext(base: Context?) {
Expand Down
90 changes: 50 additions & 40 deletions app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ import kotlin.system.exitProcess
import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
companion object {
Expand Down Expand Up @@ -640,6 +642,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

override fun onResume() {
super.onResume()
if (!com.lagradost.cloudstream3.ui.account.AccountSelectActivity.hasLoggedIn) {
val intent = Intent(this, com.lagradost.cloudstream3.ui.account.AccountSelectActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
return
}

// --- Firebase Profile Background Check ---
val firebaseRepo = com.lagradost.cloudstream3.syncproviders.AccountManager.firebaseApi
if (firebaseRepo.auth.currentUser != null && !com.lagradost.cloudstream3.ui.sync.ProfileSelectorFragment.hasFirebaseLoggedIn) {
binding?.navHostFragment?.post {
try {
navigate(R.id.navigation_profile_selector)
} catch (e: Exception) {
logError(e)
}
}
}

afterPluginsLoadedEvent += ::onAllPluginsLoaded
setActivityInstance(this)
try {
Expand All @@ -649,6 +670,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} catch (e: Exception) {
logError(e)
}
val sp = PreferenceManager.getDefaultSharedPreferences(this)
if (sp.getBoolean("firebase_auto_sync_key", true)) {
ioSafe {
AccountManager.firebaseApi.syncLocalToFirestore(this@MainActivity)
}
}
}

override fun onPause() {
Expand Down Expand Up @@ -1325,6 +1352,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}

// --- Firebase Profile Check ---
val firebaseRepo = com.lagradost.cloudstream3.syncproviders.AccountManager.firebaseApi
if (firebaseRepo.auth.currentUser != null && getKey(HAS_DONE_SETUP_KEY, false) == true) {
ioSafe {
val profiles = firebaseRepo.getProfiles()
if (profiles.isNotEmpty()) {
if (profiles.size == 1 && !profiles.first().isLocked) {
val profile = profiles.first()
profile.lastUsed = System.currentTimeMillis()
firebaseRepo.saveProfile(profile)
firebaseRepo.selectProfile(profile)
firebaseRepo.syncLocalToFirestore(this@MainActivity)
com.lagradost.cloudstream3.ui.sync.ProfileSelectorFragment.hasFirebaseLoggedIn = true
}
}
}
}

// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
main {
Expand Down Expand Up @@ -1750,31 +1795,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
//noFocus(this)

val navProfileRoot = findViewById<LinearLayout>(R.id.nav_footer_root)

if (isLayout(TV or EMULATOR)) {
val navProfilePic = findViewById<ImageView>(R.id.nav_footer_profile_pic)
val navProfileCard = findViewById<CardView>(R.id.nav_footer_profile_card)

navProfileCard?.setOnClickListener {
showAccountSelectLinear()
}

val homeViewModel =
ViewModelProvider(this@MainActivity)[HomeViewModel::class.java]

observe(homeViewModel.currentAccount) { currentAccount ->
if (currentAccount != null) {
navProfilePic?.loadImage(
currentAccount.image
)
navProfileRoot.isVisible = true
} else {
navProfileRoot.isGone = true
}
}
} else {
navProfileRoot.isGone = true
}
navProfileRoot?.isGone = true
}

val rail = binding?.navRailView
Expand All @@ -1788,8 +1809,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

// The genius engineers at google did not actually
// write a nextFocus for the navrail
rail.findViewById<View?>(R.id.navigation_settings)?.nextFocusDownId =
R.id.nav_footer_profile_card
// rail.findViewById<View?>(R.id.navigation_settings)?.nextFocusDownId =
// R.id.nav_footer_profile_card
for (id in arrayOf(
R.id.navigation_home,
R.id.navigation_search,
Expand Down Expand Up @@ -1981,9 +2002,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

handleAppIntent(intent)

ioSafe {
runAutoUpdate()
}


FcastManager().init(this, false)

Expand Down Expand Up @@ -2021,16 +2040,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

try {
if (getKey(HAS_DONE_SETUP_KEY, false) != true) {
navController.navigate(R.id.navigation_setup_language)
// If no plugins bring up extensions screen
} else if (PluginManager.getPluginsOnline().isEmpty()
&& PluginManager.getPluginsLocal().isEmpty()
// && PREBUILT_REPOSITORIES.isNotEmpty()
) {
navController.navigate(
R.id.navigation_setup_extensions,
SetupFragmentExtensions.newInstance(false)
)
navController.navigate(R.id.navigation_welcome)
}
} catch (e: Exception) {
logError(e)
Expand Down
Loading