mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Add ability to search & download subtitles (#78)
Closes #74 Adds an option to download subtitles from a remote source. This uses the Jellyfin server's API, so you need to install a plugin on your server that provides subtitle searching such as the [official OpenSubtitles plugin](https://github.com/jellyfin/jellyfin-plugin-opensubtitles). Initially it will search for subtitles in the device's current language, but you can search for a different language if needed.
This commit is contained in:
parent
4f5b0c7eee
commit
07a0db8e98
7 changed files with 454 additions and 66 deletions
|
|
@ -27,6 +27,8 @@ sealed class AppColors private constructor() {
|
|||
val DarkRed = Color(0xFF400000)
|
||||
val DarkCyan = Color(0xFF21556E)
|
||||
val DarkPurple = Color(0xFF261370)
|
||||
|
||||
val GoldenYellow = Color(0xFFDAB440)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,19 +200,15 @@ fun DialogPopup(
|
|||
onDismissRequest = onDismissRequest,
|
||||
properties = properties,
|
||||
) {
|
||||
val elevatedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
LazyColumn(
|
||||
DialogPopupContent(
|
||||
title = title,
|
||||
dialogItems = dialogItems,
|
||||
waiting = waiting,
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissOnClick = dismissOnClick,
|
||||
elevation = elevation,
|
||||
modifier =
|
||||
Modifier
|
||||
// .widthIn(min = 520.dp, max = 300.dp)
|
||||
// .dialogFocusable()
|
||||
.graphicsLayer {
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(28.0.dp)
|
||||
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||
.padding(PaddingValues(24.dp))
|
||||
.onKeyEvent { event ->
|
||||
Modifier.onKeyEvent { event ->
|
||||
val code = event.nativeKeyEvent.keyCode
|
||||
if (event.nativeKeyEvent.action == KeyEvent.ACTION_UP &&
|
||||
code in
|
||||
|
|
@ -226,6 +222,33 @@ fun DialogPopup(
|
|||
}
|
||||
false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogPopupContent(
|
||||
title: String,
|
||||
dialogItems: List<DialogItemEntry>,
|
||||
waiting: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
dismissOnClick: Boolean = true,
|
||||
elevation: Dp = 3.dp,
|
||||
) {
|
||||
val elevatedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
|
||||
LazyColumn(
|
||||
modifier =
|
||||
modifier
|
||||
// .widthIn(min = 520.dp, max = 300.dp)
|
||||
// .dialogFocusable()
|
||||
.graphicsLayer {
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(28.0.dp)
|
||||
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||
.padding(PaddingValues(24.dp)),
|
||||
) {
|
||||
stickyHeader {
|
||||
Text(
|
||||
|
|
@ -259,8 +282,6 @@ fun DialogPopup(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogPopup(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
package com.github.damontecres.wholphin.ui.playback
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItemDivider
|
||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import org.jellyfin.sdk.model.api.RemoteSubtitleInfo
|
||||
|
||||
@Composable
|
||||
fun DownloadSubtitlesContent(
|
||||
state: SubtitleSearch,
|
||||
language: String,
|
||||
onSearch: (String) -> Unit,
|
||||
onClickDownload: (RemoteSubtitleInfo) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (val s = state) {
|
||||
SubtitleSearch.Searching -> {
|
||||
Wrapper {
|
||||
Text(
|
||||
text = "Searching...",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SubtitleSearch.Downloading -> {
|
||||
Wrapper {
|
||||
Text(
|
||||
text = "Downloading...",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is SubtitleSearch.Error -> Wrapper { ErrorMessage(null, s.ex, modifier) }
|
||||
|
||||
is SubtitleSearch.Success -> {
|
||||
val dialogItems = convertRemoteSubtitles(s.options, onClickDownload)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
val elevatedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
modifier
|
||||
.graphicsLayer {
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(28.0.dp)
|
||||
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||
.padding(PaddingValues(24.dp)),
|
||||
) {
|
||||
Text(
|
||||
text = "Search & download subtitles",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
var lang by remember { mutableStateOf(language) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "Language",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
EditTextBox(
|
||||
value = lang,
|
||||
onValueChange = { lang = it },
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onSearch = {
|
||||
onSearch(lang)
|
||||
},
|
||||
),
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
imeAction = ImeAction.Search,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (dialogItems.isEmpty()) {
|
||||
Text(
|
||||
text = "No remote subtitles were found",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
itemsIndexed(dialogItems) { index, item ->
|
||||
when (item) {
|
||||
is DialogItemDivider -> HorizontalDivider(Modifier.height(16.dp))
|
||||
|
||||
is DialogItem ->
|
||||
ListItem(
|
||||
selected = false,
|
||||
enabled = item.enabled,
|
||||
onClick = {
|
||||
item.onClick.invoke()
|
||||
},
|
||||
headlineContent = item.headlineContent,
|
||||
overlineContent = item.overlineContent,
|
||||
supportingContent = item.supportingContent,
|
||||
leadingContent = item.leadingContent,
|
||||
trailingContent = item.trailingContent,
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
index == 0,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Wrapper(content: @Composable BoxScope.() -> Unit) {
|
||||
val elevatedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(28.0.dp)
|
||||
}.drawBehind { drawRect(color = elevatedContainerColor) }
|
||||
.padding(PaddingValues(24.dp)),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
fun convertRemoteSubtitles(
|
||||
options: List<RemoteSubtitleInfo>,
|
||||
onClick: (RemoteSubtitleInfo) -> Unit,
|
||||
) = options.map { op ->
|
||||
DialogItem(
|
||||
onClick = { onClick.invoke(op) },
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = op.name ?: "",
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
val strings =
|
||||
buildList {
|
||||
op.providerName?.let(::add)
|
||||
op.threeLetterIsoLanguageName?.let(::add)
|
||||
if (op.forced == true) {
|
||||
add("Forced")
|
||||
}
|
||||
add("${op.downloadCount ?: 0} downloads")
|
||||
}
|
||||
Text(
|
||||
text = strings.joinToString(" - "),
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
op.communityRating?.let { rating ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = rating.toString(),
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
tint = AppColors.GoldenYellow,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -9,10 +9,11 @@ data class SubtitleStream(
|
|||
val external: Boolean,
|
||||
val forced: Boolean,
|
||||
val default: Boolean,
|
||||
val displayTitle: String?,
|
||||
) {
|
||||
val displayName: String
|
||||
get() =
|
||||
listOfNotNull(
|
||||
displayTitle ?: listOfNotNull(
|
||||
language,
|
||||
title,
|
||||
codec,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ sealed interface PlaybackAction {
|
|||
|
||||
data object ShowVideoFilterDialog : PlaybackAction
|
||||
|
||||
data object SearchCaptions : PlaybackAction
|
||||
|
||||
data class ToggleCaptions(
|
||||
val index: Int,
|
||||
) : PlaybackAction
|
||||
|
|
@ -401,9 +403,9 @@ fun RightPlaybackButtons(
|
|||
val options = subtitleStreams.map { it.displayName }
|
||||
Timber.v("subtitleIndex=$subtitleIndex, options=$options")
|
||||
val currentChoice =
|
||||
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex }?.plus(1) ?: 0
|
||||
subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size
|
||||
BottomDialog(
|
||||
choices = listOf("None") + options,
|
||||
choices = options + listOf("None", "Search & Download"),
|
||||
currentChoice = currentChoice,
|
||||
onDismissRequest = {
|
||||
onControllerInteraction.invoke()
|
||||
|
|
@ -415,13 +417,16 @@ fun RightPlaybackButtons(
|
|||
}
|
||||
},
|
||||
onSelectChoice = { index, _ ->
|
||||
val send =
|
||||
if (index == 0) {
|
||||
TrackIndex.DISABLED
|
||||
if (index in subtitleStreams.indices) {
|
||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(subtitleStreams[index].index))
|
||||
} else {
|
||||
subtitleStreams[index - 1].index
|
||||
val idx = index - subtitleStreams.size
|
||||
if (idx == 0) {
|
||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(TrackIndex.DISABLED))
|
||||
} else {
|
||||
onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions)
|
||||
}
|
||||
}
|
||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(send))
|
||||
},
|
||||
gravity = Gravity.END,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
|
|
@ -37,8 +39,11 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.media3.common.Player
|
||||
|
|
@ -129,6 +134,9 @@ fun PlaybackPage(
|
|||
val nextUp by viewModel.nextUp.observeAsState(null)
|
||||
val playlist by viewModel.playlist.observeAsState(Playlist(listOf()))
|
||||
|
||||
val subtitleSearch by viewModel.subtitleSearch.observeAsState(null)
|
||||
val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language)
|
||||
|
||||
// TODO move to viewmodel?
|
||||
val cueListener =
|
||||
remember {
|
||||
|
|
@ -339,6 +347,10 @@ fun PlaybackPage(
|
|||
viewModel.changeSubtitleStream(it.index)
|
||||
}
|
||||
|
||||
PlaybackAction.SearchCaptions -> {
|
||||
viewModel.searchForSubtitles()
|
||||
}
|
||||
|
||||
PlaybackAction.Next -> {
|
||||
// TODO focus is lost
|
||||
viewModel.playUpNextUp()
|
||||
|
|
@ -468,6 +480,42 @@ fun PlaybackPage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
subtitleSearch?.let { state ->
|
||||
val wasPlaying = remember { player.isPlaying }
|
||||
LaunchedEffect(Unit) {
|
||||
player.pause()
|
||||
}
|
||||
val onDismissRequest = {
|
||||
if (wasPlaying) {
|
||||
player.play()
|
||||
}
|
||||
viewModel.cancelSubtitleSearch()
|
||||
}
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
),
|
||||
) {
|
||||
DownloadSubtitlesContent(
|
||||
state = state,
|
||||
language = subtitleSearchLanguage,
|
||||
onSearch = { lang ->
|
||||
viewModel.searchForSubtitles(lang)
|
||||
},
|
||||
onClickDownload = {
|
||||
viewModel.downloadAndSwitchSubtitles(it.id, wasPlaying)
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier =
|
||||
Modifier
|
||||
.widthIn(max = 640.dp)
|
||||
.heightIn(max = 400.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.playback
|
|||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -30,6 +31,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream
|
|||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.seekBack
|
||||
|
|
@ -61,6 +63,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaSegmentsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.subtitleApi
|
||||
import org.jellyfin.sdk.api.client.extensions.trickplayApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||
|
|
@ -76,7 +79,7 @@ import org.jellyfin.sdk.model.api.PlayMethod
|
|||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.PlaystateCommand
|
||||
import org.jellyfin.sdk.model.api.PlaystateMessage
|
||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||
import org.jellyfin.sdk.model.api.RemoteSubtitleInfo
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
|
|
@ -290,6 +293,7 @@ class PlaybackViewModel
|
|||
it.isExternal,
|
||||
it.isForced,
|
||||
it.isDefault,
|
||||
it.displayTitle,
|
||||
)
|
||||
}.orEmpty()
|
||||
val audioStreams =
|
||||
|
|
@ -556,7 +560,7 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun changeSubtitleStream(index: Int?) {
|
||||
fun changeSubtitleStream(index: Int?): Job =
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
changeStreams(
|
||||
dto,
|
||||
|
|
@ -567,7 +571,6 @@ class PlaybackViewModel
|
|||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrickplayUrl(index: Int): String? {
|
||||
val itemId = dto.id
|
||||
|
|
@ -616,7 +619,8 @@ class PlaybackViewModel
|
|||
if (segments.items.isNotEmpty()) {
|
||||
while (isActive) {
|
||||
delay(500L)
|
||||
val currentTicks = withContext(Dispatchers.Main) { player.currentPosition.milliseconds.inWholeTicks }
|
||||
val currentTicks =
|
||||
withContext(Dispatchers.Main) { player.currentPosition.milliseconds.inWholeTicks }
|
||||
val currentSegment =
|
||||
segments.items
|
||||
.firstOrNull {
|
||||
|
|
@ -667,7 +671,7 @@ class PlaybackViewModel
|
|||
private var lastInteractionDate: Date = Date()
|
||||
|
||||
fun reportInteraction() {
|
||||
Timber.v("reportInteraction")
|
||||
// Timber.v("reportInteraction")
|
||||
lastInteractionDate = Date()
|
||||
}
|
||||
|
||||
|
|
@ -781,6 +785,71 @@ class PlaybackViewModel
|
|||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
val subtitleSearch = MutableLiveData<SubtitleSearch?>(null)
|
||||
val subtitleSearchLanguage = MutableLiveData<String>(Locale.current.language)
|
||||
|
||||
fun searchForSubtitles(language: String = Locale.current.language) {
|
||||
subtitleSearch.value = SubtitleSearch.Searching
|
||||
subtitleSearchLanguage.value = language
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
currentItemPlayback.value?.itemId?.let {
|
||||
Timber.v("Searching for remote subtitles for %s", it)
|
||||
val results by api.subtitleApi.searchRemoteSubtitles(
|
||||
itemId = it,
|
||||
language = language,
|
||||
)
|
||||
subtitleSearch.setValueOnMain(SubtitleSearch.Success(results))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception while searching for subtitles")
|
||||
subtitleSearch.setValueOnMain(SubtitleSearch.Error(null, ex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAndSwitchSubtitles(
|
||||
subtitleId: String?,
|
||||
wasPlaying: Boolean,
|
||||
) {
|
||||
if (subtitleId == null) {
|
||||
subtitleSearch.value = SubtitleSearch.Error("Subtitle has no ID", null)
|
||||
} else {
|
||||
subtitleSearch.value = SubtitleSearch.Downloading
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
currentItemPlayback.value?.let {
|
||||
Timber.v(
|
||||
"Downloading remote subtitles for itemId=%s, sourceId=%s: %s",
|
||||
it.itemId,
|
||||
it.sourceId,
|
||||
subtitleId,
|
||||
)
|
||||
api.subtitleApi.downloadRemoteSubtitles(
|
||||
itemId = it.sourceId ?: it.itemId,
|
||||
subtitleId = subtitleId,
|
||||
)
|
||||
|
||||
subtitleSearch.setValueOnMain(null)
|
||||
changeSubtitleStream(0).join()
|
||||
withContext(Dispatchers.Main) {
|
||||
if (wasPlaying) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception while downloading subtitles: $subtitleId")
|
||||
subtitleSearch.setValueOnMain(SubtitleSearch.Error(null, ex))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSubtitleSearch() {
|
||||
subtitleSearch.value = null
|
||||
}
|
||||
}
|
||||
|
||||
data class CurrentPlayback(
|
||||
|
|
@ -790,6 +859,21 @@ data class CurrentPlayback(
|
|||
val liveStreamId: String?,
|
||||
)
|
||||
|
||||
sealed interface SubtitleSearch {
|
||||
data object Searching : SubtitleSearch
|
||||
|
||||
data object Downloading : SubtitleSearch
|
||||
|
||||
data class Success(
|
||||
val options: List<RemoteSubtitleInfo>,
|
||||
) : SubtitleSearch
|
||||
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val ex: Exception?,
|
||||
) : SubtitleSearch
|
||||
}
|
||||
|
||||
val Format.idAsInt: Int?
|
||||
@OptIn(UnstableApi::class)
|
||||
get() =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue