Support playing local movie trailers (#36)

Adds support for playing local movie trailers. External trailers, such
as ones on YouTube, are not yet supported.

Also fixes a couple issues with request handlers
This commit is contained in:
damontecres 2025-10-19 00:15:45 -04:00 committed by GitHub
parent bb19ba316b
commit 63baf7f904
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 61 additions and 2 deletions

View file

@ -35,7 +35,7 @@ fun ChapterRow(
LazyRow(
state = rememberLazyListState(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(16.dp),
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
modifier =
Modifier
.padding(start = 16.dp)

View file

@ -28,12 +28,15 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.model.BaseItem
@ -42,8 +45,11 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.chooseSource
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.ChapterRow
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -70,6 +76,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID
@ -89,6 +96,7 @@ class MovieViewModel
val people = MutableLiveData<List<Person>>(listOf())
val chapters = MutableLiveData<List<Chapter>>(listOf())
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
val trailers = MutableLiveData<List<BaseItem>>(listOf())
override fun init(
itemId: UUID,
@ -103,6 +111,16 @@ class MovieViewModel
withContext(Dispatchers.Main) {
chosenStreams.value = result
}
val localTrailerCount = item.data.localTrailerCount ?: 0
if (localTrailerCount > 0) {
val trailers =
api.userLibraryApi.getLocalTrailers(itemId).content.map {
BaseItem.from(it, api)
}
withContext(Dispatchers.Main) {
this@MovieViewModel.trailers.value = trailers
}
}
}
withContext(Dispatchers.Main) {
people.value =
@ -180,6 +198,7 @@ fun MovieDetails(
val item by viewModel.item.observeAsState()
val people by viewModel.people.observeAsState(listOf())
val chapters by viewModel.chapters.observeAsState(listOf())
val trailers by viewModel.trailers.observeAsState(listOf())
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
@ -200,6 +219,7 @@ fun MovieDetails(
chosenStreams = chosenStreams,
people = people,
chapters = chapters,
trailers = trailers,
playOnClick = {
viewModel.navigationManager.navigateTo(
Destination.Playback(
@ -270,6 +290,15 @@ fun MovieDetails(
watchOnClick = {
viewModel.setWatched((movie.data.userData?.played ?: false).not())
},
trailerOnClick = { trailer ->
viewModel.navigationManager.navigateTo(
Destination.Playback(
itemId = trailer.id,
item = trailer,
positionMs = 0L,
),
)
},
modifier = modifier,
)
}
@ -310,7 +339,9 @@ fun MovieDetailsContent(
chosenStreams: ChosenStreams?,
people: List<Person>,
chapters: List<Chapter>,
trailers: List<BaseItem>,
playOnClick: (Duration) -> Unit,
trailerOnClick: (BaseItem) -> Unit,
overviewOnClick: () -> Unit,
watchOnClick: () -> Unit,
moreOnClick: () -> Unit,
@ -359,7 +390,7 @@ fun MovieDetailsContent(
}
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(32.dp),
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
modifier = Modifier.fillMaxSize(),
) {
item {
@ -407,6 +438,27 @@ fun MovieDetailsContent(
)
}
}
if (trailers.isNotEmpty()) {
item {
ItemRow(
title = stringResource(R.string.trailers),
items = trailers,
onClickItem = trailerOnClick,
onLongClickItem = {},
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
SeasonCard(
item = item,
onClick = onClick,
onLongClick = onLongClick,
imageHeight = Cards.height2x3,
imageWidth = Dp.Unspecified,
showImageOverlay = false,
modifier = mod,
)
},
)
}
}
if (chapters.isNotEmpty()) {
item {
ChapterRow(

View file

@ -476,6 +476,7 @@ class PlaybackViewModel
it.release()
player.removeListener(it)
}
val activityListener =
TrackActivityPlaybackListener(
api = api,

View file

@ -230,6 +230,7 @@ val GetResumeItemsRequestHandler =
request.copy(
startIndex = startIndex,
limit = limit,
enableTotalRecordCount = enableTotalRecordCount,
)
override suspend fun execute(
@ -249,6 +250,7 @@ val GetNextUpRequestHandler =
request.copy(
startIndex = startIndex,
limit = limit,
enableTotalRecordCount = enableTotalRecordCount,
)
override suspend fun execute(
@ -268,6 +270,7 @@ val GetSuggestionsRequestHandler =
request.copy(
startIndex = startIndex,
limit = limit,
enableTotalRecordCount = enableTotalRecordCount,
)
override suspend fun execute(
@ -306,6 +309,7 @@ val GetGenresRequestHandler =
request.copy(
startIndex = startIndex,
limit = limit,
enableTotalRecordCount = enableTotalRecordCount,
)
override suspend fun execute(

View file

@ -20,6 +20,7 @@ val supportItemKinds =
BaseItemKind.SEASON,
BaseItemKind.COLLECTION_FOLDER,
BaseItemKind.USER_VIEW,
BaseItemKind.TRAILER,
)
val supportedCollectionTypes =

View file

@ -82,5 +82,6 @@
<string name="playback_overrides">Playback overrides</string>
<string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string>
<string name="trailers">Trailers</string>
</resources>