diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt index 18488300b..d63acb19d 100644 --- a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt +++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt @@ -353,6 +353,7 @@ class CloudSyncRepository @Inject constructor( private val subtitleAiEnabledKey = androidx.datastore.preferences.core.booleanPreferencesKey("subtitle_ai_enabled") private val subtitleAiAutoSelectKey = androidx.datastore.preferences.core.booleanPreferencesKey("subtitle_ai_auto_select") private val subtitleAiFindBestMatchKey = androidx.datastore.preferences.core.booleanPreferencesKey("subtitle_ai_find_best_match") + private val subtitlePreloadEnabledKey = androidx.datastore.preferences.core.booleanPreferencesKey("subtitle_preload_enabled") private val subtitleAiApiKeyKey = androidx.datastore.preferences.core.stringPreferencesKey("subtitle_ai_api_key") private val subtitleAiModelKey = androidx.datastore.preferences.core.stringPreferencesKey("subtitle_ai_model") private val subtitleRemoveHearingImpairedKey = androidx.datastore.preferences.core.booleanPreferencesKey("subtitle_remove_hearing_impaired") @@ -491,6 +492,7 @@ class CloudSyncRepository @Inject constructor( root.put("subtitleAiEnabled", prefs[subtitleAiEnabledKey] ?: false) root.put("subtitleAiAutoSelect", prefs[subtitleAiAutoSelectKey] ?: false) root.put("subtitleAiFindBestMatch", prefs[subtitleAiFindBestMatchKey] ?: false) + root.put("subtitlePreloadEnabled", prefs[subtitlePreloadEnabledKey] ?: true) root.put("subtitleAiApiKey", prefs[subtitleAiApiKeyKey] ?: "") root.put("subtitleAiModel", prefs[subtitleAiModelKey] ?: "GROQ_LLAMA_70B") root.put("subtitleRemoveHearingImpaired", prefs[subtitleRemoveHearingImpairedKey] ?: true) @@ -1230,6 +1232,12 @@ class CloudSyncRepository @Inject constructor( prefs[subtitleAiEnabledKey] = root.optBoolean("subtitleAiEnabled", false) prefs[subtitleAiAutoSelectKey] = root.optBoolean("subtitleAiAutoSelect", false) prefs[subtitleAiFindBestMatchKey] = root.optBoolean("subtitleAiFindBestMatch", false) + // Only apply when the backup actually carries the field — backups pushed by app + // versions that predate the setting must not reset it (realtime sync pulls after + // EVERY push from any device, so one old-version device would keep wiping it). + if (root.has("subtitlePreloadEnabled")) { + prefs[subtitlePreloadEnabledKey] = root.optBoolean("subtitlePreloadEnabled", false) + } val apiKey = root.optString("subtitleAiApiKey", "") if (apiKey.isNotBlank()) prefs[subtitleAiApiKeyKey] = apiKey val model = root.optString("subtitleAiModel", "GROQ_LLAMA_70B") diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt index f97888b2e..3fbde2f41 100644 --- a/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt +++ b/app/src/main/kotlin/com/arflix/tv/data/repository/StreamRepository.kt @@ -2711,12 +2711,22 @@ class StreamRepository @Inject constructor( * Fetch subtitles for the currently selected stream (important for OpenSubtitles matching). * Many subtitle providers (esp. OpenSubtitles) work best when `videoHash` and `videoSize` are provided. */ + /** + * @param softDeadlineMs When set, don't let slow addons hold the merged list hostage: after + * this long, harvest the addons that finished and CANCEL the rest (their subs are dropped + * for this fetch). Used by the subtitle-preload prepare gate, where every extra second is + * startup delay. Null = classic behavior (wait for all, each capped by [SUBTITLE_TIMEOUT_MS]). + * @param onPendingAddons Reports the names of addons still in flight whenever the set changes + * (and finally an empty list) — surfaces WHICH addon is slow on the loading screen. + */ suspend fun fetchSubtitlesForSelectedStream( mediaType: MediaType, imdbId: String, season: Int?, episode: Int?, - stream: StreamSource? + stream: StreamSource?, + softDeadlineMs: Long? = null, + onPendingAddons: ((List) -> Unit)? = null ): List = withContext(Dispatchers.IO) { val allAddons = installedAddons.first() // Include: @@ -2760,8 +2770,8 @@ class StreamRepository @Inject constructor( // Query addons in parallel — with a generous timeout, sequential fetches would stack // each slow addon's latency onto the total. - subtitleAddons.map { addon -> - async { + val jobs = subtitleAddons.map { addon -> + addon.name to async { suspend fun attempt(): List = runCatching { withTimeout(SUBTITLE_TIMEOUT_MS) { val addonUrl = addon.url ?: return@withTimeout emptyList() @@ -2800,7 +2810,35 @@ class StreamRepository @Inject constructor( } subs } - }.awaitAll().flatten() + } + + if (softDeadlineMs == null && onPendingAddons == null) { + return@withContext jobs.map { it.second }.awaitAll().flatten() + } + + val deadlineAt = softDeadlineMs?.let { System.currentTimeMillis() + it } + var lastPending: List? = null + while (true) { + val pending = jobs.filter { !it.second.isCompleted }.map { it.first } + if (pending != lastPending) { + lastPending = pending + onPendingAddons?.invoke(pending) + } + if (pending.isEmpty()) break + if (deadlineAt != null && System.currentTimeMillis() >= deadlineAt) { + Log.w( + "StreamRepository", + "subtitle fetch soft deadline (${softDeadlineMs}ms) — dropping slow addons: $pending" + ) + jobs.forEach { (_, job) -> if (!job.isCompleted) job.cancel() } + break + } + delay(150) + } + onPendingAddons?.invoke(emptyList()) + jobs.mapNotNull { (_, job) -> + if (job.isCompleted && !job.isCancelled) job.await() else null + }.flatten() } private fun buildSubtitlesUrl( diff --git a/app/src/main/kotlin/com/arflix/tv/server/AiKeyWebPage.kt b/app/src/main/kotlin/com/arflix/tv/server/AiKeyWebPage.kt index 28ebd6b6f..bddd2476a 100644 --- a/app/src/main/kotlin/com/arflix/tv/server/AiKeyWebPage.kt +++ b/app/src/main/kotlin/com/arflix/tv/server/AiKeyWebPage.kt @@ -75,7 +75,7 @@ $sharedCss BETTER QUALITY Google Gemini - Gemini 2.5 Flash · Free tier available + Gemini 3.5 Flash · Free tier available