mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
ref: adjust addon subtitle behaviour and language mapping
This commit is contained in:
parent
32f937a4e0
commit
8b489c5f8e
3 changed files with 324 additions and 25 deletions
|
|
@ -186,9 +186,18 @@ val AvailableLanguageOptions: List<LanguagePreferenceOption> = listOf(
|
||||||
LanguagePreferenceOption("zu", Res.string.lang_zulu),
|
LanguagePreferenceOption("zu", Res.string.lang_zulu),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val Iso639Aliases = mapOf(
|
private val LanguageCodeAliases = mapOf(
|
||||||
|
"pt-pt" to "pt",
|
||||||
|
"pt_br" to "pt-BR",
|
||||||
|
"pt-br" to "pt-BR",
|
||||||
|
"br" to "pt-BR",
|
||||||
|
"pob" to "pt-BR",
|
||||||
"eng" to "en",
|
"eng" to "en",
|
||||||
"spa" to "es",
|
"spa" to "es",
|
||||||
|
"es-419" to "es-419",
|
||||||
|
"es_419" to "es-419",
|
||||||
|
"es-la" to "es-419",
|
||||||
|
"es-lat" to "es-419",
|
||||||
"fra" to "fr",
|
"fra" to "fr",
|
||||||
"fre" to "fr",
|
"fre" to "fr",
|
||||||
"deu" to "de",
|
"deu" to "de",
|
||||||
|
|
@ -200,14 +209,170 @@ private val Iso639Aliases = mapOf(
|
||||||
"kor" to "ko",
|
"kor" to "ko",
|
||||||
"zho" to "zh",
|
"zho" to "zh",
|
||||||
"chi" to "zh",
|
"chi" to "zh",
|
||||||
|
"zht" to "zh-TW",
|
||||||
|
"zhs" to "zh-CN",
|
||||||
|
"chi-tw" to "zh-TW",
|
||||||
|
"chi-cn" to "zh-CN",
|
||||||
|
"zh-tw" to "zh-TW",
|
||||||
|
"zh_tw" to "zh-TW",
|
||||||
|
"zh-cn" to "zh-CN",
|
||||||
|
"zh_cn" to "zh-CN",
|
||||||
"ara" to "ar",
|
"ara" to "ar",
|
||||||
"hin" to "hi",
|
"hin" to "hi",
|
||||||
"nld" to "nl",
|
"nld" to "nl",
|
||||||
"dut" to "nl",
|
"dut" to "nl",
|
||||||
"pol" to "pl",
|
"pol" to "pl",
|
||||||
"swe" to "sv",
|
"swe" to "sv",
|
||||||
|
"nor" to "no",
|
||||||
|
"dan" to "da",
|
||||||
|
"fin" to "fi",
|
||||||
"tur" to "tr",
|
"tur" to "tr",
|
||||||
|
"ell" to "el",
|
||||||
|
"gre" to "el",
|
||||||
"heb" to "he",
|
"heb" to "he",
|
||||||
|
"tha" to "th",
|
||||||
|
"vie" to "vi",
|
||||||
|
"ind" to "id",
|
||||||
|
"msa" to "ms",
|
||||||
|
"may" to "ms",
|
||||||
|
"ces" to "cs",
|
||||||
|
"cze" to "cs",
|
||||||
|
"hun" to "hu",
|
||||||
|
"ron" to "ro",
|
||||||
|
"rum" to "ro",
|
||||||
|
"ukr" to "uk",
|
||||||
|
"bul" to "bg",
|
||||||
|
"hrv" to "hr",
|
||||||
|
"srp" to "sr",
|
||||||
|
"slk" to "sk",
|
||||||
|
"slo" to "sk",
|
||||||
|
"slv" to "sl",
|
||||||
|
"cat" to "ca",
|
||||||
|
"alb" to "sq",
|
||||||
|
"sqi" to "sq",
|
||||||
|
"bos" to "bs",
|
||||||
|
"mac" to "mk",
|
||||||
|
"mkd" to "mk",
|
||||||
|
"lav" to "lv",
|
||||||
|
"lit" to "lt",
|
||||||
|
"est" to "et",
|
||||||
|
"isl" to "is",
|
||||||
|
"ice" to "is",
|
||||||
|
"glg" to "gl",
|
||||||
|
"baq" to "eu",
|
||||||
|
"eus" to "eu",
|
||||||
|
"wel" to "cy",
|
||||||
|
"cym" to "cy",
|
||||||
|
"gle" to "ga",
|
||||||
|
"ben" to "bn",
|
||||||
|
"tam" to "ta",
|
||||||
|
"tel" to "te",
|
||||||
|
"mal" to "ml",
|
||||||
|
"kan" to "kn",
|
||||||
|
"mar" to "mr",
|
||||||
|
"pan" to "pa",
|
||||||
|
"guj" to "gu",
|
||||||
|
"urd" to "ur",
|
||||||
|
"fas" to "fa",
|
||||||
|
"per" to "fa",
|
||||||
|
"amh" to "am",
|
||||||
|
"swa" to "sw",
|
||||||
|
"zul" to "zu",
|
||||||
|
"afr" to "af",
|
||||||
|
"mlt" to "mt",
|
||||||
|
"bel" to "be",
|
||||||
|
"geo" to "ka",
|
||||||
|
"kat" to "ka",
|
||||||
|
"arm" to "hy",
|
||||||
|
"hye" to "hy",
|
||||||
|
"aze" to "az",
|
||||||
|
"kaz" to "kk",
|
||||||
|
"uzb" to "uz",
|
||||||
|
"mon" to "mn",
|
||||||
|
"khm" to "km",
|
||||||
|
"lao" to "lo",
|
||||||
|
"mya" to "my",
|
||||||
|
"bur" to "my",
|
||||||
|
"sin" to "si",
|
||||||
|
"nep" to "ne",
|
||||||
|
"tgl" to "tl",
|
||||||
|
"fil" to "tl",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LanguageNameAliases = mapOf(
|
||||||
|
"afrikaans" to "af",
|
||||||
|
"albanian" to "sq",
|
||||||
|
"amharic" to "am",
|
||||||
|
"arabic" to "ar",
|
||||||
|
"armenian" to "hy",
|
||||||
|
"azerbaijani" to "az",
|
||||||
|
"basque" to "eu",
|
||||||
|
"belarusian" to "be",
|
||||||
|
"bengali" to "bn",
|
||||||
|
"bosnian" to "bs",
|
||||||
|
"bulgarian" to "bg",
|
||||||
|
"burmese" to "my",
|
||||||
|
"catalan" to "ca",
|
||||||
|
"chinese" to "zh",
|
||||||
|
"mandarin" to "zh",
|
||||||
|
"croatian" to "hr",
|
||||||
|
"czech" to "cs",
|
||||||
|
"danish" to "da",
|
||||||
|
"dutch" to "nl",
|
||||||
|
"english" to "en",
|
||||||
|
"estonian" to "et",
|
||||||
|
"filipino" to "tl",
|
||||||
|
"finnish" to "fi",
|
||||||
|
"french" to "fr",
|
||||||
|
"galician" to "gl",
|
||||||
|
"georgian" to "ka",
|
||||||
|
"german" to "de",
|
||||||
|
"greek" to "el",
|
||||||
|
"gujarati" to "gu",
|
||||||
|
"hebrew" to "he",
|
||||||
|
"hindi" to "hi",
|
||||||
|
"hungarian" to "hu",
|
||||||
|
"icelandic" to "is",
|
||||||
|
"indonesian" to "id",
|
||||||
|
"irish" to "ga",
|
||||||
|
"italian" to "it",
|
||||||
|
"japanese" to "ja",
|
||||||
|
"kannada" to "kn",
|
||||||
|
"kazakh" to "kk",
|
||||||
|
"khmer" to "km",
|
||||||
|
"korean" to "ko",
|
||||||
|
"lao" to "lo",
|
||||||
|
"latvian" to "lv",
|
||||||
|
"lithuanian" to "lt",
|
||||||
|
"macedonian" to "mk",
|
||||||
|
"malay" to "ms",
|
||||||
|
"malayalam" to "ml",
|
||||||
|
"maltese" to "mt",
|
||||||
|
"marathi" to "mr",
|
||||||
|
"mongolian" to "mn",
|
||||||
|
"nepali" to "ne",
|
||||||
|
"norwegian" to "no",
|
||||||
|
"persian" to "fa",
|
||||||
|
"polish" to "pl",
|
||||||
|
"punjabi" to "pa",
|
||||||
|
"romanian" to "ro",
|
||||||
|
"russian" to "ru",
|
||||||
|
"serbian" to "sr",
|
||||||
|
"sinhala" to "si",
|
||||||
|
"slovak" to "sk",
|
||||||
|
"slovenian" to "sl",
|
||||||
|
"swahili" to "sw",
|
||||||
|
"swedish" to "sv",
|
||||||
|
"tamil" to "ta",
|
||||||
|
"telugu" to "te",
|
||||||
|
"thai" to "th",
|
||||||
|
"turkish" to "tr",
|
||||||
|
"ukrainian" to "uk",
|
||||||
|
"urdu" to "ur",
|
||||||
|
"uzbek" to "uz",
|
||||||
|
"vietnamese" to "vi",
|
||||||
|
"welsh" to "cy",
|
||||||
|
"zulu" to "zu",
|
||||||
)
|
)
|
||||||
|
|
||||||
fun normalizeLanguageCode(language: String?): String? {
|
fun normalizeLanguageCode(language: String?): String? {
|
||||||
|
|
@ -218,13 +383,55 @@ fun normalizeLanguageCode(language: String?): String? {
|
||||||
?.takeIf { it.isNotBlank() }
|
?.takeIf { it.isNotBlank() }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
|
val tokenized = raw
|
||||||
|
.replace('-', ' ')
|
||||||
|
.replace('.', ' ')
|
||||||
|
.replace('/', ' ')
|
||||||
|
.replace(Regex("\\s+"), " ")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
fun containsAny(vararg values: String): Boolean =
|
||||||
|
values.any { value -> tokenized.contains(value) }
|
||||||
|
|
||||||
|
if (containsAny("portuguese", "portugues")) {
|
||||||
|
return when {
|
||||||
|
containsAny("brazil", "brasil", "brazilian", "brasileiro", "pt br", "ptbr", "pob", "(br)") ->
|
||||||
|
"pt-br"
|
||||||
|
containsAny("portugal", "european", "europeu", "iberian", "pt pt", "ptpt") ->
|
||||||
|
"pt"
|
||||||
|
else -> "pt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsAny("spanish", "espanol", "castellano")) {
|
||||||
|
return if (containsAny("latin", "latino", "latinoamerica", "latinoamericano", "lat am", "latam", "es 419", "es419", "(419)")) {
|
||||||
|
"es-419"
|
||||||
|
} else {
|
||||||
|
"es"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LanguageCodeAliases[raw]?.let { return it.replace('_', '-').lowercase() }
|
||||||
|
LanguageNameAliases[tokenized]?.let { return it }
|
||||||
|
LanguageNameAliases.entries
|
||||||
|
.sortedByDescending { it.key.length }
|
||||||
|
.firstOrNull { (name, _) ->
|
||||||
|
tokenized == name ||
|
||||||
|
tokenized.startsWith("$name ") ||
|
||||||
|
tokenized.endsWith(" $name") ||
|
||||||
|
tokenized.contains(" $name ")
|
||||||
|
}
|
||||||
|
?.let { return it.value }
|
||||||
|
|
||||||
val primary = raw.substringBefore('-')
|
val primary = raw.substringBefore('-')
|
||||||
val canonicalPrimary = Iso639Aliases[primary] ?: primary
|
val primaryAlias = LanguageCodeAliases[primary]?.replace('_', '-')?.lowercase()
|
||||||
val suffix = raw.substringAfter('-', "")
|
val suffix = raw.substringAfter('-', "")
|
||||||
return if (suffix.isBlank()) {
|
return if (suffix.isBlank()) {
|
||||||
canonicalPrimary
|
primaryAlias ?: primary
|
||||||
|
} else if (primaryAlias != null && !primaryAlias.contains('-')) {
|
||||||
|
"$primaryAlias-$suffix"
|
||||||
} else {
|
} else {
|
||||||
"$canonicalPrimary-$suffix"
|
primaryAlias ?: "$primary-$suffix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
|
import com.nuvio.app.features.addons.AddonResource
|
||||||
|
import com.nuvio.app.features.addons.ManagedAddon
|
||||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||||
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
||||||
import com.nuvio.app.features.details.MetaVideo
|
import com.nuvio.app.features.details.MetaVideo
|
||||||
|
|
@ -439,8 +441,24 @@ fun PlayerScreen(
|
||||||
var preferredSubtitleSelectionApplied by rememberSaveable(sourceUrl) { mutableStateOf(false) }
|
var preferredSubtitleSelectionApplied by rememberSaveable(sourceUrl) { mutableStateOf(false) }
|
||||||
var activeSubtitleTab by remember { mutableStateOf(SubtitleTab.BuiltIn) }
|
var activeSubtitleTab by remember { mutableStateOf(SubtitleTab.BuiltIn) }
|
||||||
val subtitleStyle = playerSettingsUiState.subtitleStyle
|
val subtitleStyle = playerSettingsUiState.subtitleStyle
|
||||||
|
val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val addonSubtitles by SubtitleRepository.addonSubtitles.collectAsStateWithLifecycle()
|
val addonSubtitles by SubtitleRepository.addonSubtitles.collectAsStateWithLifecycle()
|
||||||
val isLoadingAddonSubtitles by SubtitleRepository.isLoading.collectAsStateWithLifecycle()
|
val isLoadingAddonSubtitles by SubtitleRepository.isLoading.collectAsStateWithLifecycle()
|
||||||
|
val activeAddonSubtitleType = contentType ?: parentMetaType
|
||||||
|
val addonSubtitleFetchKey = remember(
|
||||||
|
addonsUiState.addons,
|
||||||
|
activeAddonSubtitleType,
|
||||||
|
activeVideoId,
|
||||||
|
) {
|
||||||
|
buildAddonSubtitleFetchKey(
|
||||||
|
addons = addonsUiState.addons,
|
||||||
|
type = activeAddonSubtitleType,
|
||||||
|
videoId = activeVideoId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var autoFetchedAddonSubtitlesForKey by rememberSaveable(activeSourceUrl, activeVideoId) {
|
||||||
|
mutableStateOf<String?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
fun refreshTracks() {
|
fun refreshTracks() {
|
||||||
val ctrl = playerController ?: return
|
val ctrl = playerController ?: return
|
||||||
|
|
@ -1092,8 +1110,8 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchAddonSubtitlesForActiveItem() {
|
fun fetchAddonSubtitlesForActiveItem() {
|
||||||
val type = contentType ?: return
|
val type = activeAddonSubtitleType.takeIf { it.isNotBlank() } ?: return
|
||||||
val videoId = activeVideoId ?: return
|
val videoId = activeVideoId?.takeIf { it.isNotBlank() } ?: return
|
||||||
SubtitleRepository.fetchAddonSubtitles(type, videoId)
|
SubtitleRepository.fetchAddonSubtitles(type, videoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1127,11 +1145,11 @@ fun PlayerScreen(
|
||||||
playerController?.applySubtitleStyle(subtitleStyle)
|
playerController?.applySubtitleStyle(subtitleStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(showSubtitleModal, activeSubtitleTab, contentType, activeVideoId) {
|
LaunchedEffect(activeSourceUrl, addonSubtitleFetchKey) {
|
||||||
if (!showSubtitleModal || activeSubtitleTab != SubtitleTab.Addons) return@LaunchedEffect
|
val fetchKey = addonSubtitleFetchKey ?: return@LaunchedEffect
|
||||||
if (!isLoadingAddonSubtitles && addonSubtitles.isEmpty()) {
|
if (autoFetchedAddonSubtitlesForKey == fetchKey) return@LaunchedEffect
|
||||||
fetchAddonSubtitlesForActiveItem()
|
autoFetchedAddonSubtitlesForKey = fetchKey
|
||||||
}
|
fetchAddonSubtitlesForActiveItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(playbackSnapshot.isLoading, playerController) {
|
LaunchedEffect(playbackSnapshot.isLoading, playerController) {
|
||||||
|
|
@ -1924,6 +1942,47 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildAddonSubtitleFetchKey(
|
||||||
|
addons: List<ManagedAddon>,
|
||||||
|
type: String?,
|
||||||
|
videoId: String?,
|
||||||
|
): String? {
|
||||||
|
val normalizedType = type?.takeIf { it.isNotBlank() } ?: return null
|
||||||
|
val normalizedVideoId = videoId?.takeIf { it.isNotBlank() } ?: return null
|
||||||
|
val compatibleSubtitleAddons = addons.mapNotNull { addon ->
|
||||||
|
val manifest = addon.manifest ?: return@mapNotNull null
|
||||||
|
val supportsSubtitles = manifest.resources.any { resource ->
|
||||||
|
resource.isCompatibleSubtitleResource(
|
||||||
|
type = normalizedType,
|
||||||
|
videoId = normalizedVideoId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!supportsSubtitles) return@mapNotNull null
|
||||||
|
"${manifest.id}:${manifest.transportUrl}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compatibleSubtitleAddons.isEmpty()) return null
|
||||||
|
return buildString {
|
||||||
|
append(normalizedType)
|
||||||
|
append('|')
|
||||||
|
append(normalizedVideoId)
|
||||||
|
append('|')
|
||||||
|
append(compatibleSubtitleAddons.sorted().joinToString("|"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AddonResource.isCompatibleSubtitleResource(type: String, videoId: String): Boolean {
|
||||||
|
val isSubtitleResource = name.equals("subtitles", ignoreCase = true) ||
|
||||||
|
name.equals("subtitle", ignoreCase = true)
|
||||||
|
if (!isSubtitleResource) return false
|
||||||
|
|
||||||
|
val requestType = if (type.equals("tv", ignoreCase = true)) "series" else type
|
||||||
|
val typeMatches = types.isEmpty() || types.any { it.equals(requestType, ignoreCase = true) }
|
||||||
|
if (!typeMatches) return false
|
||||||
|
|
||||||
|
return idPrefixes.isEmpty() || idPrefixes.any { prefix -> videoId.startsWith(prefix) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T> findPreferredTrackIndex(
|
private fun <T> findPreferredTrackIndex(
|
||||||
tracks: List<T>,
|
tracks: List<T>,
|
||||||
targets: List<String>,
|
targets: List<String>,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package com.nuvio.app.features.player
|
package com.nuvio.app.features.player
|
||||||
|
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
|
import com.nuvio.app.features.addons.AddonResource
|
||||||
import com.nuvio.app.features.addons.buildAddonResourceUrl
|
import com.nuvio.app.features.addons.buildAddonResourceUrl
|
||||||
import com.nuvio.app.features.addons.httpGetText
|
import com.nuvio.app.features.addons.httpGetText
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -15,6 +18,7 @@ import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
@ -35,8 +39,12 @@ object SubtitleRepository {
|
||||||
private val _error = MutableStateFlow<String?>(null)
|
private val _error = MutableStateFlow<String?>(null)
|
||||||
val error: StateFlow<String?> = _error.asStateFlow()
|
val error: StateFlow<String?> = _error.asStateFlow()
|
||||||
|
|
||||||
|
private var activeFetchJob: Job? = null
|
||||||
|
|
||||||
fun fetchAddonSubtitles(type: String, videoId: String) {
|
fun fetchAddonSubtitles(type: String, videoId: String) {
|
||||||
scope.launch {
|
activeFetchJob?.cancel()
|
||||||
|
activeFetchJob = scope.launch {
|
||||||
|
val requestType = canonicalSubtitleType(type)
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
_error.value = null
|
_error.value = null
|
||||||
_addonSubtitles.value = emptyList()
|
_addonSubtitles.value = emptyList()
|
||||||
|
|
@ -46,17 +54,13 @@ object SubtitleRepository {
|
||||||
|
|
||||||
for (addon in addons) {
|
for (addon in addons) {
|
||||||
val manifest = addon.manifest ?: continue
|
val manifest = addon.manifest ?: continue
|
||||||
val subtitleResource = manifest.resources.find { it.name == "subtitles" } ?: continue
|
val subtitleResource = manifest.resources.find { it.name.isSubtitleResourceName() } ?: continue
|
||||||
if (!subtitleResource.types.contains(type)) continue
|
if (!subtitleResource.supportsSubtitleType(requestType, videoId)) continue
|
||||||
|
|
||||||
val prefixMatch = subtitleResource.idPrefixes.isEmpty() ||
|
|
||||||
subtitleResource.idPrefixes.any { videoId.startsWith(it) }
|
|
||||||
if (!prefixMatch) continue
|
|
||||||
|
|
||||||
val subtitleUrl = buildAddonResourceUrl(
|
val subtitleUrl = buildAddonResourceUrl(
|
||||||
manifestUrl = manifest.transportUrl,
|
manifestUrl = manifest.transportUrl,
|
||||||
resource = "subtitles",
|
resource = "subtitles",
|
||||||
type = type,
|
type = requestType,
|
||||||
id = videoId,
|
id = videoId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,21 +73,23 @@ object SubtitleRepository {
|
||||||
|
|
||||||
for (element in subtitlesArray) {
|
for (element in subtitlesArray) {
|
||||||
val obj = element.jsonObject
|
val obj = element.jsonObject
|
||||||
val id = obj["id"]?.jsonPrimitive?.content
|
val id = obj.stringValue("id")
|
||||||
?: "${manifest.id}_${allSubs.size}"
|
?: "${manifest.id}_${allSubs.size}"
|
||||||
val url = obj["url"]?.jsonPrimitive?.content ?: continue
|
val url = obj.stringValue("url") ?: continue
|
||||||
val lang = obj["lang"]?.jsonPrimitive?.content ?: "unknown"
|
val rawLang = obj.subtitleLanguage() ?: "unknown"
|
||||||
|
val normalizedLang = normalizeLanguageCode(rawLang) ?: rawLang
|
||||||
|
|
||||||
allSubs.add(
|
allSubs.add(
|
||||||
AddonSubtitle(
|
AddonSubtitle(
|
||||||
id = id,
|
id = id,
|
||||||
url = url,
|
url = url,
|
||||||
language = lang,
|
language = normalizedLang,
|
||||||
display = "${getLanguageLabelForCode(lang)} (${addon.displayTitle})",
|
display = "${getLanguageLabelForCode(rawLang)} (${addon.displayTitle})",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (_: Throwable) {
|
} catch (error: Throwable) {
|
||||||
|
if (error is CancellationException) throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,8 +102,35 @@ object SubtitleRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
activeFetchJob?.cancel()
|
||||||
_addonSubtitles.value = emptyList()
|
_addonSubtitles.value = emptyList()
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
_error.value = null
|
_error.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canonicalSubtitleType(type: String): String =
|
||||||
|
if (type.equals("tv", ignoreCase = true)) "series" else type.lowercase()
|
||||||
|
|
||||||
|
private fun String.isSubtitleResourceName(): Boolean =
|
||||||
|
equals("subtitles", ignoreCase = true) || equals("subtitle", ignoreCase = true)
|
||||||
|
|
||||||
|
private fun AddonResource.supportsSubtitleType(type: String, videoId: String): Boolean {
|
||||||
|
val typeMatches = types.isEmpty() || types.any { it.equals(type, ignoreCase = true) }
|
||||||
|
if (!typeMatches) return false
|
||||||
|
return idPrefixes.isEmpty() || idPrefixes.any { prefix -> videoId.startsWith(prefix) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonObject.subtitleLanguage(): String? =
|
||||||
|
stringValue("lang")
|
||||||
|
?: stringValue("language")
|
||||||
|
?: stringValue("languageCode")
|
||||||
|
?: stringValue("locale")
|
||||||
|
?: stringValue("label")
|
||||||
|
|
||||||
|
private fun JsonObject.stringValue(name: String): String? =
|
||||||
|
this[name]
|
||||||
|
?.jsonPrimitive
|
||||||
|
?.contentOrNull
|
||||||
|
?.trim()
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue