From 9c2a7c1f48ad5a834cb5ba1dc9f2d8a35af8f1af Mon Sep 17 00:00:00 2001 From: sonyx Date: Wed, 28 Oct 2026 21:48:08 -0300 Subject: [PATCH] feat: prefer complete local downloads before streaming extraction --- .../ui/player/LocalFirstGenerator.kt | 54 +++++++++++++++++++ .../ui/result/ResultViewModel2.kt | 2 +- .../ui/settings/SettingsGeneralScreen.kt | 10 ++++ .../utils/downloader/DownloadManager.kt | 10 +++- .../utils/downloader/LocalDownloadPolicy.kt | 6 +++ .../res/values-es/local_playback_options.xml | 5 ++ .../res/values/local_playback_options.xml | 5 ++ app/src/main/res/xml/settings_general.xml | 3 ++ .../downloader/LocalDownloadPolicyTest.kt | 23 ++++++++ .../cloudstream4/DevicePreferenceStore.kt | 1 + 10 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/LocalFirstGenerator.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/downloader/LocalDownloadPolicy.kt create mode 100644 app/src/main/res/values-es/local_playback_options.xml create mode 100644 app/src/main/res/values/local_playback_options.xml create mode 100644 app/src/test/java/com/lagradost/cloudstream3/utils/downloader/LocalDownloadPolicyTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LocalFirstGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LocalFirstGenerator.kt new file mode 100644 index 00000000000..50d454bb10a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LocalFirstGenerator.kt @@ -0,0 +1,54 @@ +package com.lagradost.cloudstream3.ui.player + +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.CloudStreamApp.Companion.context +import com.lagradost.cloudstream3.services.DownloadQueueService +import com.lagradost.cloudstream3.ui.result.ResultEpisode +import com.lagradost.cloudstream3.utils.DataStore.getKey +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.downloader.LocalDownloadPolicy +import com.lagradost.cloudstream3.utils.downloader.DownloadObjects +import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException + +class LocalFirstGenerator(private val remote: RepoLinkGenerator) : VideoGenerator(remote.videos) { + override val hasCache = remote.hasCache + override val canSkipLoading = remote.canSkipLoading + override fun getId(index: Int) = remote.getId(index) + + override suspend fun generateLinks( + clearCache: Boolean, + sourceTypes: Set, + callback: (Pair) -> Unit, + subtitleCallback: (SubtitleData) -> Unit, + offset: Int, + isCasting: Boolean, + ): Boolean { + val ctx = context + val episode = videos.getOrNull(offset) + if (!isCasting && !clearCache && ctx != null && episode != null && + PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean("prefer_local_downloads", true)) { + val local = withContext(Dispatchers.IO) { + try { + val info = VideoDownloadManager.getDownloadFileInfo(ctx, episode.id) ?: return@withContext null + val completed = ctx.getKey(VideoDownloadManager.KEY_COMPLETED_DOWNLOAD_BYTES, episode.id.toString()) + val busy = DownloadQueueService.downloadInstances.value.any { it.downloadQueueWrapper.id == episode.id } + if (!LocalDownloadPolicy.isComplete(info.fileLength, info.totalBytes, completed, busy)) return@withContext null + ctx.contentResolver.openAssetFileDescriptor(info.path, "r")?.use { } ?: return@withContext null + val meta = ctx.getKey(VideoDownloadManager.KEY_DOWNLOAD_INFO, episode.id.toString()) + ?: return@withContext null + ExtractorUri(uri = info.path, name = episode.name ?: remote.page?.name.orEmpty(), + id = episode.id, parentId = episode.parentId, season = episode.season, episode = episode.episode, + headerName = remote.page?.name, tvType = remote.page?.type, + basePath = meta.basePath, relativePath = meta.relativePath, displayName = meta.displayName) + } catch (_: IOException) { null } catch (_: SecurityException) { null } + } + if (local != null) return DownloadFileGenerator(listOf(local)).generateLinks( + false, sourceTypes, callback, subtitleCallback, 0, false) + } + return remote.generateLinks(clearCache, sourceTypes, callback, subtitleCallback, offset, isCasting) + } +} 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 17a698de2df..339bbae3283 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 @@ -1565,7 +1565,7 @@ class ResultViewModel2 : ViewModel() { activity?.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( - generator, index,list + com.lagradost.cloudstream3.ui.player.LocalFirstGenerator(generator), index,list ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt index 6aa94fa883c..f69f6d26c81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneralScreen.kt @@ -114,6 +114,16 @@ object SettingsGeneralScreen : SearchableSettings { }.sortedBy { fromTagToLanguageName(it) ?: it } return persistentListOf( + Preference.PreferenceGroup( + title = stringResource(R.string.local_playback_options_title), + preferenceItems = persistentListOf( + Preference.PreferenceItem.SwitchPreference( + preference = settings.general.preferLocalDownloads, + title = stringResource(R.string.prefer_local_downloads_title), + subtitle = stringResource(R.string.prefer_local_downloads_summary), + ), + ), + ), Preference.PreferenceGroup(title = stringResource(R.string.extension_language), preferenceItems = persistentListOf( Preference.PreferenceItem.ListPreference( preference = settings.general.locale, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt index adefedd2053..eb946f92821 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt @@ -196,6 +196,7 @@ object VideoDownloadManager { const val KEY_RESUME_PACKAGES = "download_resume_2" const val KEY_DOWNLOAD_INFO = "download_info" + const val KEY_COMPLETED_DOWNLOAD_BYTES = "completed_download_bytes" /** A key to save all the downloads which have not yet started and those currently running, using [DownloadQueueWrapper] * [KEY_RESUME_PACKAGES] can store keys which should not be automatically queued, unlike this key. @@ -639,6 +640,7 @@ object VideoDownloadManager { } fun setDownloadFileInfoTemplate(template: DownloadedFileInfo) { + if (id != null) removeKey(KEY_COMPLETED_DOWNLOAD_BYTES, id.toString()) downloadFileInfoTemplate = template updateFileInfo() } @@ -665,6 +667,9 @@ object VideoDownloadManager { get() = internalType set(value) { internalType = value + if (id != null && value == DownloadType.IsDone) { + setKey(KEY_COMPLETED_DOWNLOAD_BYTES, id.toString(), bytesWritten) + } notify() } @@ -1617,7 +1622,10 @@ object VideoDownloadManager { private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) - if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + if (success) { + context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + context.removeKey(KEY_COMPLETED_DOWNLOAD_BYTES, id.toString()) + } return success } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/LocalDownloadPolicy.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/LocalDownloadPolicy.kt new file mode 100644 index 00000000000..fe09b7f412e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/LocalDownloadPolicy.kt @@ -0,0 +1,6 @@ +package com.lagradost.cloudstream3.utils.downloader + +internal object LocalDownloadPolicy { + fun isComplete(length: Long, expected: Long, completedLength: Long?, busy: Boolean): Boolean = + !busy && length > 0 && (completedLength == length || (expected > 0 && length == expected)) +} diff --git a/app/src/main/res/values-es/local_playback_options.xml b/app/src/main/res/values-es/local_playback_options.xml new file mode 100644 index 00000000000..8ea578ee2eb --- /dev/null +++ b/app/src/main/res/values-es/local_playback_options.xml @@ -0,0 +1,5 @@ + + Reproducción local + Reproducir primero las descargas + Usa una descarga completa y accesible antes de solicitar enlaces. Desactívalo para priorizar el streaming. + diff --git a/app/src/main/res/values/local_playback_options.xml b/app/src/main/res/values/local_playback_options.xml new file mode 100644 index 00000000000..d3a8465c1bd --- /dev/null +++ b/app/src/main/res/values/local_playback_options.xml @@ -0,0 +1,5 @@ + + Local playback + Play downloaded files first + Use a complete, readable download before requesting streaming links. Turn off to keep streaming first. + diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index f8b9b4f452f..b8ff997c6f1 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -1,6 +1,9 @@ + + +