Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> Unit)? = null
): List<Subtitle> = withContext(Dispatchers.IO) {
val allAddons = installedAddons.first()
// Include:
Expand Down Expand Up @@ -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<Subtitle> = runCatching {
withTimeout(SUBTITLE_TIMEOUT_MS) {
val addonUrl = addon.url ?: return@withTimeout emptyList<Subtitle>()
Expand Down Expand Up @@ -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<String>? = 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(
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/kotlin/com/arflix/tv/server/AiKeyWebPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ $sharedCss
<a id="geminiLink" href="/gemini" class="provider-btn">
<span class="badge badge-gemini">BETTER QUALITY</span>
<span class="name">Google Gemini</span>
<span class="model">Gemini 2.5 Flash · Free tier available</span>
<span class="model">Gemini 3.5 Flash · Free tier available</span>
</a>
</div>
<script>
Expand Down
Loading
Loading