Add support for cinema mode (#1305)

## Description
Adds support for cinema mode, which is enabled by default.

When enabled and if starting playback from the beginning, the server is
queried for any "intros", ie prerolls or trailers, to play first. These
are shown in the queue and can be skipped if desired.

The server has to provide the intros, so a plugin like
https://github.com/jellyfin/jellyfin-plugin-intros or
https://github.com/CherryFloors/jellyfin-plugin-cinemamode is required
server-side.

### Related issues
Closes #418
Closes #1180

### Testing
Emulator and server w/
https://github.com/CherryFloors/jellyfin-plugin-cinemamode

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-04-26 10:16:26 -04:00 committed by GitHub
parent ea6a0227b0
commit edb37f5d69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 170 additions and 64 deletions

View file

@ -12,7 +12,7 @@ import java.util.UUID
* This is not the same thing as a Jellyfin server playlist * This is not the same thing as a Jellyfin server playlist
*/ */
class Playlist( class Playlist(
items: List<BaseItem>, items: List<PlaylistItem>,
startIndex: Int = 0, startIndex: Int = 0,
) { ) {
val items = items.subList(startIndex, items.size) val items = items.subList(startIndex, items.size)
@ -23,15 +23,15 @@ class Playlist(
fun hasNext(): Boolean = (index + 1) < items.size fun hasNext(): Boolean = (index + 1) < items.size
fun getPreviousAndReverse(): BaseItem = items[--index] fun getPreviousAndReverse(): PlaylistItem = items[--index]
fun getAndAdvance(): BaseItem = items[++index] fun getAndAdvance(): PlaylistItem = items[++index]
fun peek(): BaseItem? = items.getOrNull(index + 1) fun peek(): PlaylistItem? = items.getOrNull(index + 1)
fun upcomingItems(): List<BaseItem> = items.subList(index + 1, items.size) fun upcomingItems(): List<PlaylistItem> = items.subList(index + 1, items.size)
fun advanceTo(id: UUID): BaseItem? { fun advanceTo(id: UUID): PlaylistItem? {
while (hasNext()) { while (hasNext()) {
val potential = getAndAdvance() val potential = getAndAdvance()
if (potential.id == id) { if (potential.id == id) {
@ -52,3 +52,22 @@ data class PlaylistInfo(
val count: Int, val count: Int,
val mediaType: MediaType, val mediaType: MediaType,
) )
sealed interface PlaylistItem {
val id: UUID
val item: BaseItem
data class Media(
override val item: BaseItem,
) : PlaylistItem {
override val id: UUID
get() = item.id
}
data class Intro(
override val item: BaseItem,
) : PlaylistItem {
override val id: UUID
get() = item.id
}
}

View file

@ -455,6 +455,18 @@ sealed interface AppPreference<Pref, T> {
summaryOff = R.string.disabled, summaryOff = R.string.disabled,
) )
val CinemaMode =
AppSwitchPreference<AppPreferences>(
title = R.string.cinema_mode,
defaultValue = true,
getter = { it.playbackPreferences.cinemaMode },
setter = { prefs, value ->
prefs.updatePlaybackPreferences { cinemaMode = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val RememberSelectedTab = val RememberSelectedTab =
AppSwitchPreference<AppPreferences>( AppSwitchPreference<AppPreferences>(
title = R.string.remember_selected_tab, title = R.string.remember_selected_tab,
@ -1166,6 +1178,7 @@ val advancedPreferences =
preferences = preferences =
listOf( listOf(
AppPreference.OneClickPause, AppPreference.OneClickPause,
AppPreference.CinemaMode,
AppPreference.GlobalContentScale, AppPreference.GlobalContentScale,
AppPreference.SkipSegments, AppPreference.SkipSegments,
AppPreference.MaxBitrate, AppPreference.MaxBitrate,

View file

@ -54,6 +54,7 @@ class AppPreferencesSerializer
refreshRateSwitching = refreshRateSwitching =
AppPreference.RefreshRateSwitching.defaultValue AppPreference.RefreshRateSwitching.defaultValue
resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue
cinemaMode = AppPreference.CinemaMode.defaultValue
overrides = overrides =
PlaybackOverrides PlaybackOverrides

View file

@ -19,6 +19,7 @@ import com.github.damontecres.wholphin.preferences.updateMpvOptions
import com.github.damontecres.wholphin.preferences.updateMusicPreferences import com.github.damontecres.wholphin.preferences.updateMusicPreferences
import com.github.damontecres.wholphin.preferences.updatePhotoPreferences import com.github.damontecres.wholphin.preferences.updatePhotoPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
@ -327,5 +328,11 @@ class AppUpgradeHandler
} }
} }
} }
if (previous.isEqualOrBefore(Version.fromString("0.6.2-18-g0"))) {
appPreferences.updateData {
it.updatePlaybackPreferences { cinemaMode = AppPreference.CinemaMode.defaultValue }
}
}
} }
} }

View file

@ -7,12 +7,14 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.data.model.PlaylistInfo import com.github.damontecres.wholphin.data.model.PlaylistInfo
import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.components.baseItemKinds import com.github.damontecres.wholphin.ui.components.baseItemKinds
import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.gt import com.github.damontecres.wholphin.ui.gt
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.playback.playable import com.github.damontecres.wholphin.ui.playback.playable
import com.github.damontecres.wholphin.ui.toBaseItems
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
@ -22,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.playlistsApi import org.jellyfin.sdk.api.client.extensions.playlistsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.api.client.extensions.videosApi
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@ -247,14 +250,14 @@ class PlaylistCreator
-> { -> {
val list = val list =
buildList { buildList {
add(BaseItem(item, false)) add(PlaylistItem.Media(BaseItem(item, false)))
if (item.partCount.gt(1)) { if (item.partCount.gt(1)) {
api.videosApi api.videosApi
.getAdditionalPart(item.id) .getAdditionalPart(item.id)
.content.items .content.items
.map { .map {
BaseItem(it, false) PlaylistItem.Media(BaseItem(it, false))
}.let(::addAll) }.let(::addAll)
} }
} }
@ -274,14 +277,14 @@ class PlaylistCreator
} }
} }
private suspend fun List<BaseItemDto>.convertAndAddParts(useSeriesForPrimary: Boolean = false): List<BaseItem> = private suspend fun List<BaseItemDto>.convertAndAddParts(useSeriesForPrimary: Boolean = false): List<PlaylistItem> =
buildList { buildList {
this@convertAndAddParts.forEach { ep -> this@convertAndAddParts.forEach { ep ->
add(BaseItem(ep, useSeriesForPrimary)) add(PlaylistItem.Media(BaseItem(ep, useSeriesForPrimary)))
if (ep.partCount.gt(1)) { if (ep.partCount.gt(1)) {
val parts = val parts =
api.videosApi.getAdditionalPart(ep.id).content.items.map { part -> api.videosApi.getAdditionalPart(ep.id).content.items.map { part ->
BaseItem(part, useSeriesForPrimary) PlaylistItem.Media(BaseItem(part, useSeriesForPrimary))
} }
addAll(parts) addAll(parts)
} }

View file

@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.PlaylistCreationResult import com.github.damontecres.wholphin.services.PlaylistCreationResult
@ -105,9 +106,9 @@ class PlayExternalViewModel
} }
try { try {
val queriedItem = api.userLibraryApi.getItem(itemId).content val queriedItem = api.userLibraryApi.getItem(itemId).content
val base = val playlistItem =
if (queriedItem.type.playable) { if (queriedItem.type.playable) {
queriedItem PlaylistItem.Media(BaseItem(queriedItem))
} else if (destination is Destination.PlaybackList) { } else if (destination is Destination.PlaybackList) {
val playlistResult = val playlistResult =
playlistCreator.createFrom( playlistCreator.createFrom(
@ -134,18 +135,15 @@ class PlayExternalViewModel
navigationManager.goBack() navigationManager.goBack()
return@launchDefault return@launchDefault
} }
r.playlist.items r.playlist.items.first()
.first()
.data
} }
} }
} else { } else {
throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}") throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}")
} }
val item = BaseItem(base, false)
val playbackConfig = val playbackConfig =
serverRepository.currentUser.value?.let { user -> serverRepository.currentUser.value?.let { user ->
itemPlaybackDao.getItem(user, base.id)?.let { itemPlaybackDao.getItem(user, playlistItem.id)?.let {
Timber.v("Fetched itemPlayback from DB: %s", it) Timber.v("Fetched itemPlayback from DB: %s", it)
if (it.sourceId != null) { if (it.sourceId != null) {
it it
@ -154,20 +152,25 @@ class PlayExternalViewModel
} }
} }
} }
val mediaSource = streamChoiceService.chooseSource(base, playbackConfig) val item =
val plc = streamChoiceService.getPlaybackLanguageChoice(base) when (playlistItem) {
is PlaylistItem.Intro -> playlistItem.item
is PlaylistItem.Media -> playlistItem.item
}
val mediaSource = streamChoiceService.chooseSource(item.data, playbackConfig)
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
if (mediaSource == null) { if (mediaSource == null) {
Timber.w("Media source is null") Timber.w("Media source is null")
return@launchDefault return@launchDefault
} }
savedStateHandle[KEY_ID] = base.id savedStateHandle[KEY_ID] = playlistItem.id
savedStateHandle[KEY_MEDIA_ID] = mediaSource.id savedStateHandle[KEY_MEDIA_ID] = mediaSource.id
val subtitleIndex = val subtitleIndex =
streamChoiceService streamChoiceService
.chooseSubtitleStream( .chooseSubtitleStream(
source = mediaSource, source = mediaSource,
audioStream = null, audioStream = null,
seriesId = base.seriesId, seriesId = item.data.seriesId,
itemPlayback = playbackConfig, itemPlayback = playbackConfig,
plc = plc, plc = plc,
prefs = prefs, prefs = prefs,

View file

@ -35,6 +35,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.Chapter
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.data.model.TrackIndex
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.PlayerBackend
@ -185,7 +186,7 @@ class PlaybackViewModel
private lateinit var preferences: UserPreferences private lateinit var preferences: UserPreferences
internal lateinit var itemId: UUID internal lateinit var itemId: UUID
internal lateinit var item: BaseItem internal lateinit var currentItem: PlaylistItem
internal var forceTranscoding: Boolean = false internal var forceTranscoding: Boolean = false
private var activityListener: TrackActivityPlaybackListener? = null private var activityListener: TrackActivityPlaybackListener? = null
private val jobs = mutableListOf<Job>() private val jobs = mutableListOf<Job>()
@ -319,9 +320,9 @@ class PlaybackViewModel
} }
this.itemId = itemId this.itemId = itemId
val queriedItem = api.userLibraryApi.getItem(itemId).content val queriedItem = api.userLibraryApi.getItem(itemId).content
val base = val playlistItem =
if (queriedItem.type.playable) { if (queriedItem.type.playable) {
queriedItem PlaylistItem.Media(BaseItem(queriedItem, false))
} else if (destination is Destination.PlaybackList) { } else if (destination is Destination.PlaybackList) {
val playlistResult = val playlistResult =
playlistCreator.createFrom( playlistCreator.createFrom(
@ -349,9 +350,7 @@ class PlaybackViewModel
this@PlaybackViewModel.playlist.value = r.playlist this@PlaybackViewModel.playlist.value = r.playlist
} }
} }
r.playlist.items r.playlist.items.first()
.first()
.data
} }
} }
} else { } else {
@ -359,10 +358,39 @@ class PlaybackViewModel
} }
viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() } viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() }
val item = BaseItem.from(base, api)
val intros =
// If not resuming playback & cinema mode is enabled, get potential intros
if (positionMs == 0L && preferences.appPreferences.playbackPreferences.cinemaMode) {
api.userLibraryApi
.getIntros(
itemId = playlistItem.id,
userId = serverRepository.currentUser.value?.id,
).content.items
.map {
PlaylistItem.Intro(BaseItem(it))
}
} else {
emptyList()
}
val firstItem =
if (intros.isNotEmpty()) {
Timber.v("Got %s intros", intros.size)
val currentPlaylist =
this@PlaybackViewModel
.playlist.value
?.items
.orEmpty()
val newPlaylist = Playlist(intros + currentPlaylist)
this@PlaybackViewModel.playlist.setValueOnMain(newPlaylist)
intros.first()
} else {
playlistItem
}
val played = val played =
play( play(
item, firstItem,
positionMs, positionMs,
itemPlayback, itemPlayback,
forceTranscoding, forceTranscoding,
@ -374,9 +402,13 @@ class PlaybackViewModel
if (!isPlaylist && preferences.appPreferences.playbackPreferences.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER) { if (!isPlaylist && preferences.appPreferences.playbackPreferences.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER) {
val result = playlistCreator.createFrom(queriedItem) val result = playlistCreator.createFrom(queriedItem)
if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) { if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) {
withContext(Dispatchers.Main) { val currentPlaylist =
this@PlaybackViewModel.playlist.value = result.playlist this@PlaybackViewModel
} .playlist.value
?.items
.orEmpty()
val newPlaylist = Playlist(currentPlaylist + result.playlist.items)
this@PlaybackViewModel.playlist.setValueOnMain(newPlaylist)
} }
} }
} }
@ -384,18 +416,24 @@ class PlaybackViewModel
/** /**
* Play an item * Play an item
* *
* @param item the item to play * @param currentItem the item to play
* @param positionMs the starting playback position in milliseconds * @param positionMs the starting playback position in milliseconds
* @param itemPlayback the parameters for playback such chosen subtitle or audio streams * @param itemPlayback the parameters for playback such chosen subtitle or audio streams
* @param forceTranscoding whether the user has requested to force playback via transcoding * @param forceTranscoding whether the user has requested to force playback via transcoding
*/ */
private suspend fun play( private suspend fun play(
item: BaseItem, playlistItem: PlaylistItem,
positionMs: Long, positionMs: Long,
itemPlayback: ItemPlayback? = null, itemPlayback: ItemPlayback? = null,
forceTranscoding: Boolean = this.forceTranscoding, forceTranscoding: Boolean = this.forceTranscoding,
): Boolean = ): Boolean =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val item =
when (playlistItem) {
is PlaylistItem.Intro -> playlistItem.item
is PlaylistItem.Media -> playlistItem.item
}
Timber.i("Playing ${item.id}") Timber.i("Playing ${item.id}")
// New item, so we can clear the media segment tracker & subtitle cues // New item, so we can clear the media segment tracker & subtitle cues
@ -415,7 +453,7 @@ class PlaybackViewModel
) )
return@withContext false return@withContext false
} }
this@PlaybackViewModel.item = item this@PlaybackViewModel.currentItem = playlistItem
this@PlaybackViewModel.itemId = item.id this@PlaybackViewModel.itemId = item.id
val isLiveTv = item.type == BaseItemKind.TV_CHANNEL val isLiveTv = item.type == BaseItemKind.TV_CHANNEL
@ -875,7 +913,7 @@ class PlaybackViewModel
Timber.d("Changing audio track to %s", index) Timber.d("Changing audio track to %s", index)
val itemPlayback = val itemPlayback =
itemPlaybackRepository.saveTrackSelection( itemPlaybackRepository.saveTrackSelection(
item = item, item = currentItem.item,
itemPlayback = currentItemPlayback.value!!, itemPlayback = currentItemPlayback.value!!,
trackIndex = index, trackIndex = index,
type = MediaStreamType.AUDIO, type = MediaStreamType.AUDIO,
@ -889,7 +927,7 @@ class PlaybackViewModel
streamChoiceService.resolveSubtitleIndex( streamChoiceService.resolveSubtitleIndex(
source = source, source = source,
audioStreamIndex = index, audioStreamIndex = index,
seriesId = item.data.seriesId, seriesId = currentItem.item.data.seriesId,
subtitleIndex = itemPlayback.subtitleIndex, subtitleIndex = itemPlayback.subtitleIndex,
prefs = preferences, prefs = preferences,
) )
@ -898,7 +936,7 @@ class PlaybackViewModel
} }
changeStreams( changeStreams(
item, currentItem.item,
itemPlayback, itemPlayback,
index, index,
resolvedSubtitleIndex, resolvedSubtitleIndex,
@ -913,7 +951,7 @@ class PlaybackViewModel
Timber.d("Changing subtitle track to %s", index) Timber.d("Changing subtitle track to %s", index)
val itemPlayback = val itemPlayback =
itemPlaybackRepository.saveTrackSelection( itemPlaybackRepository.saveTrackSelection(
item = item, item = currentItem.item,
itemPlayback = currentItemPlayback.value!!, itemPlayback = currentItemPlayback.value!!,
trackIndex = index, trackIndex = index,
type = MediaStreamType.SUBTITLE, type = MediaStreamType.SUBTITLE,
@ -927,7 +965,7 @@ class PlaybackViewModel
streamChoiceService.resolveSubtitleIndex( streamChoiceService.resolveSubtitleIndex(
source = source, source = source,
audioStreamIndex = itemPlayback.audioIndex, audioStreamIndex = itemPlayback.audioIndex,
seriesId = item.data.seriesId, seriesId = currentItem.item.data.seriesId,
subtitleIndex = index, subtitleIndex = index,
prefs = preferences, prefs = preferences,
) )
@ -936,7 +974,7 @@ class PlaybackViewModel
} }
changeStreams( changeStreams(
item, currentItem.item,
itemPlayback, itemPlayback,
itemPlayback.audioIndex, itemPlayback.audioIndex,
resolvedIndex, resolvedIndex,
@ -971,7 +1009,7 @@ class PlaybackViewModel
mediaSourceId: UUID? = currentItemPlayback.value?.sourceId, mediaSourceId: UUID? = currentItemPlayback.value?.sourceId,
): String? = ): String? =
trickPlayInfo?.let { trickPlayInfo?.let {
val itemId = item.id val itemId = currentItem.id
return api.trickplayApi.getTrickplayTileImageUrl( return api.trickplayApi.getTrickplayTileImageUrl(
itemId, itemId,
trickPlayInfo.width, trickPlayInfo.width,
@ -982,12 +1020,28 @@ class PlaybackViewModel
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_ENDED) { if (playbackState == Player.STATE_ENDED) {
viewModelScope.launchIO { Timber.v("Playback state is STATE_ENDED")
val nextItem = playlist.value?.peek() viewModelScope.launchDefault {
Timber.v("Setting next up to ${nextItem?.id}") when (val nextItem = playlist.value?.peek()) {
is PlaylistItem.Intro -> {
Timber.v("Next item is intro, so playing immediately")
playNextUp()
}
is PlaylistItem.Media -> {
if (currentItem is PlaylistItem.Intro) {
Timber.v("Current item is intro, so playing next up immediately")
playNextUp()
} else {
Timber.v("Setting next up to ${nextItem.id}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
nextUp.value = nextItem nextUp.value = nextItem.item
if (nextItem == null) { }
}
}
null -> {
Timber.v("No next up")
navigationManager.goBack() navigationManager.goBack()
} }
} }
@ -1053,9 +1107,11 @@ class PlaybackViewModel
outroShownSegments.add(currentSegment.id) outroShownSegments.add(currentSegment.id)
) { ) {
val nextItem = playlist.peek() val nextItem = playlist.peek()
if (nextItem is PlaylistItem.Media) {
Timber.v("Setting next up during outro to ${nextItem?.id}") Timber.v("Setting next up during outro to ${nextItem?.id}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
nextUp.value = nextItem nextUp.value = nextItem.item
}
} }
} else { } else {
val behavior = val behavior =
@ -1180,7 +1236,7 @@ class PlaybackViewModel
fun playNextUp() { fun playNextUp() {
playlist.value?.let { playlist.value?.let {
if (it.hasNext()) { if (it.hasNext()) {
viewModelScope.launchIO { viewModelScope.launchDefault {
cancelUpNextEpisode() cancelUpNextEpisode()
val item = it.getAndAdvance() val item = it.getAndAdvance()
val played = play(item, 0) val played = play(item, 0)
@ -1195,7 +1251,7 @@ class PlaybackViewModel
fun playPrevious() { fun playPrevious() {
playlist.value?.let { playlist.value?.let {
if (it.hasPrevious()) { if (it.hasPrevious()) {
viewModelScope.launchIO { viewModelScope.launchDefault {
cancelUpNextEpisode() cancelUpNextEpisode()
val item = it.getPreviousAndReverse() val item = it.getPreviousAndReverse()
val played = play(item, 0) val played = play(item, 0)
@ -1256,7 +1312,7 @@ class PlaybackViewModel
PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> { PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> {
Timber.w("Playback error during ${it.playMethod}, falling back to transcoding") Timber.w("Playback error during ${it.playMethod}, falling back to transcoding")
changeStreams( changeStreams(
item, currentItem.item,
currentItemPlayback.value!!, currentItemPlayback.value!!,
currentItemPlayback.value?.audioIndex, currentItemPlayback.value?.audioIndex,
currentItemPlayback.value?.subtitleIndex, currentItemPlayback.value?.subtitleIndex,

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.text.intl.Locale
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.data.model.TrackIndex
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.onMain import com.github.damontecres.wholphin.ui.onMain
@ -106,12 +107,13 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
while (maxAttempts > 0 && subtitleCount == newCount) { while (maxAttempts > 0 && subtitleCount == newCount) {
maxAttempts-- maxAttempts--
delay(1500) delay(1500)
item = val base = BaseItem(api.userLibraryApi.getItem(itemId = it.itemId).content)
BaseItem.from( currentItem =
api.userLibraryApi.getItem(itemId = it.itemId).content, when (currentItem) {
api, is PlaylistItem.Intro -> PlaylistItem.Intro(base)
) is PlaylistItem.Media -> PlaylistItem.Media(base)
mediaSource = streamChoiceService.chooseSource(item.data, it) }
mediaSource = streamChoiceService.chooseSource(currentItem.item.data, it)
if (mediaSource == null) { if (mediaSource == null) {
// This shouldn't happen, but just in case // This shouldn't happen, but just in case
showToast( showToast(
@ -158,7 +160,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
) )
} }
this@downloadAndSwitchSubtitles.changeStreams( this@downloadAndSwitchSubtitles.changeStreams(
item, currentItem.item,
currentItemPlayback.value!!, currentItemPlayback.value!!,
audioIndex, audioIndex,
newStream.index, newStream.index,

View file

@ -79,9 +79,9 @@ fun QueueRowOverlay(
if (isFocused) controllerViewState.pulseControls() if (isFocused) controllerViewState.pulseControls()
} }
SeasonCard( SeasonCard(
item = item, item = item.item,
onClick = { onClick = {
onClickPlaylist.invoke(item) onClickPlaylist.invoke(item.item)
controllerViewState.hideControls() controllerViewState.hideControls()
}, },
onLongClick = {}, onLongClick = {},

View file

@ -86,6 +86,7 @@ message PlaybackPreferences {
bool refresh_rate_switching = 22; bool refresh_rate_switching = 22;
bool resolution_switching = 23; bool resolution_switching = 23;
string external_player = 24; string external_player = 24;
bool cinema_mode = 25;
} }
message HomePagePreferences{ message HomePagePreferences{

View file

@ -767,6 +767,7 @@
<string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string> <string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string>
<string name="ass_subtitle_mode_transcode">Burn in/transcode on server</string> <string name="ass_subtitle_mode_transcode">Burn in/transcode on server</string>
<string name="ass_subtitle_playback">SSA/ASS subtitle playback</string> <string name="ass_subtitle_playback">SSA/ASS subtitle playback</string>
<string name="cinema_mode">Cinema mode</string>
<string name="show_more">Show %1$s more</string> <string name="show_more">Show %1$s more</string>
<string name="skip_behavior">Skip behavior</string> <string name="skip_behavior">Skip behavior</string>
<string name="skip_behavior_summary">For intros, credits, etc</string> <string name="skip_behavior_summary">For intros, credits, etc</string>