Fixes & optimizations to series pages (#106)

Simplifies the process for finding the right season id & episode id
after clicking on an episode. This can also improve loading time for
seasons that have a large number of episodes (hundreds).

Also fixes handling for "unknown" seasons which don't have an
index/season number.
This commit is contained in:
damontecres 2025-10-29 11:02:21 -04:00 committed by GitHub
parent 808e379e02
commit 576c20edff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 130 additions and 118 deletions

View file

@ -1,6 +1,6 @@
package com.github.damontecres.wholphin.data.model package com.github.damontecres.wholphin.data.model
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.seasonEpisode import com.github.damontecres.wholphin.util.seasonEpisode
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -54,27 +54,23 @@ data class BaseItem(
// Redirect episodes & seasons to their series if possible // Redirect episodes & seasons to their series if possible
when (type) { when (type) {
BaseItemKind.EPISODE -> { BaseItemKind.EPISODE -> {
indexNumber?.let { episode -> data.seasonId?.let { seasonId ->
data.parentIndexNumber?.let { season ->
Destination.SeriesOverview(
data.seriesId!!,
BaseItemKind.SERIES,
this,
SeasonEpisode(season, episode),
)
}
} ?: Destination.MediaItem(id, type, this)
}
BaseItemKind.SEASON ->
data.indexNumber?.let { season ->
Destination.SeriesOverview( Destination.SeriesOverview(
data.seriesId!!, data.seriesId!!,
BaseItemKind.SERIES, BaseItemKind.SERIES,
this, this,
SeasonEpisode(season, 0), SeasonEpisodeIds(seasonId, data.parentIndexNumber, id, indexNumber),
) )
} ?: Destination.MediaItem(id, type, this) } ?: Destination.MediaItem(id, type, this)
}
BaseItemKind.SEASON ->
Destination.SeriesOverview(
data.seriesId!!,
BaseItemKind.SERIES,
this,
SeasonEpisodeIds(id, indexNumber, null, null),
)
else -> Destination.MediaItem(id, type, this) else -> Destination.MediaItem(id, type, this)
} }

View file

@ -375,3 +375,8 @@ suspend fun <T> MutableLiveData<T>.setValueOnMain(value: T) =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@setValueOnMain.value = value this@setValueOnMain.value = value
} }
fun equalsNotNull(
a: Any?,
b: Any?,
) = a != null && b != null && a == b

View file

@ -92,12 +92,12 @@ fun SeriesDetails(
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init(preferences, destination.itemId, destination.item, null, null) viewModel.init(preferences, destination.itemId, destination.item, null)
} }
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val item by viewModel.item.observeAsState() val item by viewModel.item.observeAsState()
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty()) val seasons by viewModel.seasons.observeAsState(listOf())
val people by viewModel.people.observeAsState(listOf()) val people by viewModel.people.observeAsState(listOf())
val similar by viewModel.similar.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf())
@ -190,7 +190,7 @@ private const val SIMILAR_ROW = PEOPLE_ROW + 1
fun SeriesDetailsContent( fun SeriesDetailsContent(
preferences: UserPreferences, preferences: UserPreferences,
series: BaseItem, series: BaseItem,
seasons: ItemListAndMapping, seasons: List<BaseItem>,
similar: List<BaseItem>, similar: List<BaseItem>,
people: List<Person>, people: List<Person>,
played: Boolean, played: Boolean,
@ -332,7 +332,7 @@ fun SeriesDetailsContent(
item { item {
ItemRow( ItemRow(
title = "Seasons", title = "Seasons",
items = seasons.items, items = seasons,
onClickItem = { onClickItem = {
position = SEASONS_ROW position = SEASONS_ROW
onClickItem.invoke(it) onClickItem.invoke(it)

View file

@ -1,3 +1,5 @@
@file:UseSerializers(UUIDSerializer::class)
package com.github.damontecres.wholphin.ui.detail.series package com.github.damontecres.wholphin.ui.detail.series
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -26,15 +28,20 @@ import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode import com.github.damontecres.wholphin.util.seasonEpisode
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
@Serializable @Serializable
@ -43,6 +50,14 @@ data class SeasonEpisode(
val episode: Int, val episode: Int,
) )
@Serializable
data class SeasonEpisodeIds(
val seasonId: UUID,
val seasonNumber: Int?,
val episodeId: UUID?,
val episodeNumber: Int?,
)
@Serializable @Serializable
data class SeriesOverviewPosition( data class SeriesOverviewPosition(
val seasonTabIndex: Int, val seasonTabIndex: Int,
@ -55,7 +70,7 @@ fun SeriesOverview(
destination: Destination.SeriesOverview, destination: Destination.SeriesOverview,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
initialSeasonEpisode: SeasonEpisode? = null, initialSeasonEpisode: SeasonEpisodeIds? = null,
) { ) {
LifecycleStartEffect(destination.itemId) { LifecycleStartEffect(destination.itemId) {
viewModel.maybePlayThemeSong( viewModel.maybePlayThemeSong(
@ -76,8 +91,7 @@ fun SeriesOverview(
preferences, preferences,
destination.itemId, destination.itemId,
destination.item, destination.item,
initialSeasonEpisode?.season, initialSeasonEpisode,
initialSeasonEpisode?.episode,
) )
initialLoadDone = true initialLoadDone = true
} }
@ -85,9 +99,9 @@ fun SeriesOverview(
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val series by viewModel.item.observeAsState(null) val series by viewModel.item.observeAsState(null)
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty()) val seasons by viewModel.seasons.observeAsState(listOf())
val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading) val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading)
val episodeList = (episodes as? EpisodeList.Success)?.episodes?.items val episodeList = (episodes as? EpisodeList.Success)?.episodes
var position by rememberSaveable( var position by rememberSaveable(
destination, destination,
@ -100,18 +114,18 @@ fun SeriesOverview(
) { ) {
mutableStateOf( mutableStateOf(
SeriesOverviewPosition( SeriesOverviewPosition(
seasons.numberToIndex[initialSeasonEpisode?.season ?: 0] ?: 0, seasons.indexOfFirstOrNull {
(episodes as? EpisodeList.Success)?.episodes?.numberToIndex[ equalsNotNull(it.id, initialSeasonEpisode?.seasonId) ||
initialSeasonEpisode?.episode equalsNotNull(it.indexNumber, initialSeasonEpisode?.seasonNumber)
?: 0, } ?: 0,
] ?: 0, (episodes as? EpisodeList.Success)?.initialIndex ?: 0,
), ),
) )
} }
if (initialLoadDone) { if (initialLoadDone) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
seasons.indexToNumber[position.seasonTabIndex]?.let { seasons.getOrNull(position.seasonTabIndex)?.let {
viewModel.loadEpisodes(it) viewModel.loadEpisodes(it.id)
} }
} }
} }
@ -123,10 +137,10 @@ fun SeriesOverview(
LaunchedEffect(episodes) { LaunchedEffect(episodes) {
episodes?.let { episodes -> episodes?.let { episodes ->
if (episodes is EpisodeList.Success) { if (episodes is EpisodeList.Success) {
if (episodes.episodes.items.isNotEmpty()) { if (episodes.episodes.isNotEmpty()) {
// TODO focus on first episode when changing seasons? // TODO focus on first episode when changing seasons?
// firstItemFocusRequester.requestFocus() // firstItemFocusRequester.requestFocus()
episodes.episodes.items.getOrNull(position.episodeRowIndex)?.let { episodes.episodes.getOrNull(position.episodeRowIndex)?.let {
viewModel.refreshEpisode(it.id, position.episodeRowIndex) viewModel.refreshEpisode(it.id, position.episodeRowIndex)
} }
} }
@ -137,7 +151,6 @@ fun SeriesOverview(
LaunchedEffect(position) { LaunchedEffect(position) {
(episodes as? EpisodeList.Success) (episodes as? EpisodeList.Success)
?.episodes ?.episodes
?.items
?.getOrNull(position.episodeRowIndex) ?.getOrNull(position.episodeRowIndex)
?.let { ?.let {
viewModel.lookUpChosenTracks(it.id, it) viewModel.lookUpChosenTracks(it.id, it)
@ -230,7 +243,7 @@ fun SeriesOverview(
SeriesOverviewContent( SeriesOverviewContent(
preferences = preferences, preferences = preferences,
series = series, series = series,
seasons = seasons.items, seasons = seasons,
episodes = episodes, episodes = episodes,
chosenStreams = chosenStreams, chosenStreams = chosenStreams,
position = position, position = position,
@ -245,8 +258,8 @@ fun SeriesOverview(
episodeRowFocusRequester = episodeRowFocusRequester, episodeRowFocusRequester = episodeRowFocusRequester,
onFocus = { onFocus = {
if (it.seasonTabIndex != position.seasonTabIndex) { if (it.seasonTabIndex != position.seasonTabIndex) {
seasons.indexToNumber[it.seasonTabIndex]?.let { seasonNumber -> seasons.getOrNull(it.seasonTabIndex)?.let { season ->
viewModel.loadEpisodes(seasonNumber) viewModel.loadEpisodes(season.id)
} }
} }
position = it position = it

View file

@ -85,7 +85,7 @@ fun SeriesOverviewContent(
val tabRowFocusRequester = remember { FocusRequester() } val tabRowFocusRequester = remember { FocusRequester() }
val focusedEpisode = val focusedEpisode =
(episodes as? EpisodeList.Success)?.episodes?.items?.getOrNull(position.episodeRowIndex) (episodes as? EpisodeList.Success)?.episodes?.getOrNull(position.episodeRowIndex)
Box( Box(
modifier = modifier =
@ -229,7 +229,7 @@ fun SeriesOverviewContent(
.focusRestorer(firstItemFocusRequester) .focusRestorer(firstItemFocusRequester)
.focusRequester(episodeRowFocusRequester), .focusRequester(episodeRowFocusRequester),
) { ) {
itemsIndexed(eps.episodes.items) { episodeIndex, episode -> itemsIndexed(eps.episodes) { episodeIndex, episode ->
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
if (interactionSource.collectIsFocusedAsState().value) { if (interactionSource.collectIsFocusedAsState().value) {
onFocus.invoke( onFocus.invoke(

View file

@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.detail.ItemViewModel import com.github.damontecres.wholphin.ui.detail.ItemViewModel
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
@ -68,7 +69,7 @@ class SeriesViewModel
private lateinit var seriesId: UUID private lateinit var seriesId: UUID
private lateinit var prefs: UserPreferences private lateinit var prefs: UserPreferences
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty()) val seasons = MutableLiveData<List<BaseItem>>(listOf())
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading) val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
val people = MutableLiveData<List<Person>>(listOf()) val people = MutableLiveData<List<Person>>(listOf())
val similar = MutableLiveData<List<BaseItem>>() val similar = MutableLiveData<List<BaseItem>>()
@ -77,8 +78,7 @@ class SeriesViewModel
prefs: UserPreferences, prefs: UserPreferences,
itemId: UUID, itemId: UUID,
potential: BaseItem?, potential: BaseItem?,
season: Int?, seasonEpisodeIds: SeasonEpisodeIds?,
episode: Int?,
) { ) {
this.seriesId = itemId this.seriesId = itemId
this.prefs = prefs this.prefs = prefs
@ -89,16 +89,28 @@ class SeriesViewModel
) + Dispatchers.IO, ) + Dispatchers.IO,
) { ) {
val item = fetchItem(seriesId, potential) val item = fetchItem(seriesId, potential)
val seasonsInfo = getSeasons(item) val seasons = getSeasons(item)
// If a particular season was requested, fetch those episodes, otherwise get the first season // If a particular season was requested, fetch those episodes, otherwise get the first season
val initialSeason =
if (seasonEpisodeIds != null) {
seasons.firstOrNull {
equalsNotNull(it.id, seasonEpisodeIds.seasonId) ||
equalsNotNull(it.indexNumber, seasonEpisodeIds.seasonNumber)
}
} else {
seasons.firstOrNull()
}
val episodeInfo = val episodeInfo =
(season ?: seasonsInfo.items.firstOrNull()?.indexNumber) initialSeason?.let {
?.let { seasonNum -> loadEpisodesInternal(
loadEpisodesInternal(seasonNum) it.id,
} ?: EpisodeList.Error("Could not determine season") seasonEpisodeIds?.episodeId,
seasonEpisodeIds?.episodeNumber,
)
} ?: EpisodeList.Error("Could not determine season for selected episode")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
seasons.value = seasonsInfo this@SeriesViewModel.seasons.value = seasons
episodes.value = episodeInfo episodes.value = episodeInfo
loading.value = LoadingState.Success loading.value = LoadingState.Success
people.value = people.value =
@ -108,18 +120,20 @@ class SeriesViewModel
}.orEmpty() }.orEmpty()
} }
if (!similar.isInitialized) { if (!similar.isInitialized) {
val similar = viewModelScope.launchIO {
api.libraryApi val similar =
.getSimilarItems( api.libraryApi
GetSimilarItemsRequest( .getSimilarItems(
userId = serverRepository.currentUser?.id, GetSimilarItemsRequest(
itemId = itemId, userId = serverRepository.currentUser?.id,
fields = SlimItemFields, itemId = itemId,
limit = 25, fields = SlimItemFields,
), limit = 25,
).content.items ),
.map { BaseItem.from(it, api, true) } ).content.items
this@SeriesViewModel.similar.setValueOnMain(similar) .map { BaseItem.from(it, api, true) }
this@SeriesViewModel.similar.setValueOnMain(similar)
}
} }
} }
} }
@ -163,7 +177,7 @@ class SeriesViewModel
themeSongPlayer.stop() themeSongPlayer.stop()
} }
private suspend fun getSeasons(series: BaseItem): ItemListAndMapping { private suspend fun getSeasons(series: BaseItem): List<BaseItem> {
val request = val request =
GetItemsRequest( GetItemsRequest(
parentId = series.id, parentId = series.id,
@ -178,30 +192,26 @@ class SeriesViewModel
ItemFields.SEASON_USER_DATA, ItemFields.SEASON_USER_DATA,
), ),
) )
val pager = val seasons =
ApiRequestPager( GetItemsRequestHandler.execute(api, request).content.items.map {
api, BaseItem.from(
request, it,
GetItemsRequestHandler, api,
viewModelScope, )
)
pager.init()
Timber.Forest.v("Loaded ${pager.size} seasons for series ${series.id}")
val pairs =
pager.mapIndexed { index, _ ->
val season = pager.getBlocking(index)
Pair(season?.indexNumber!!, index)
} }
val seasonNumToIndex = mapOf(*pairs.toTypedArray()) Timber.v("Loaded ${seasons.size} seasons for series ${series.id}")
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray()) return seasons
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
} }
private suspend fun loadEpisodesInternal(season: Int): EpisodeList { private suspend fun loadEpisodesInternal(
seasonId: UUID,
episodeId: UUID?,
episodeNumber: Int?,
): EpisodeList {
val request = val request =
GetEpisodesRequest( GetEpisodesRequest(
seriesId = item.value!!.id, seriesId = seriesId,
season = season, seasonId = seasonId,
sortBy = ItemSortBy.INDEX_NUMBER, sortBy = ItemSortBy.INDEX_NUMBER,
fields = fields =
listOf( listOf(
@ -215,18 +225,30 @@ class SeriesViewModel
) )
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope) val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
pager.init() pager.init()
Timber.v("Loaded ${pager.size} episodes for season $season") val initialIndex =
return EpisodeList.Success(convertPager(pager)) if (episodeId != null || episodeNumber != null) {
pager
.indexOfBlocking {
equalsNotNull(it?.id, episodeId) ||
equalsNotNull(it?.indexNumber, episodeNumber)
}.coerceAtLeast(0)
} else {
// Force the first page to to be fetched
pager.getBlocking(0)
0
}
Timber.v("Loaded ${pager.size} episodes for season $seasonId, initialIndex=$initialIndex")
return EpisodeList.Success(pager, initialIndex)
} }
fun loadEpisodes(season: Int) { fun loadEpisodes(seasonId: UUID) {
this@SeriesViewModel.episodes.value = EpisodeList.Loading this@SeriesViewModel.episodes.value = EpisodeList.Loading
viewModelScope.launch(ExceptionHandler(true)) { viewModelScope.launchIO(ExceptionHandler(true)) {
val episodes = val episodes =
try { try {
loadEpisodesInternal(season) loadEpisodesInternal(seasonId, null, null)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error loading episodes for $seriesId for season $season") Timber.e(e, "Error loading episodes for $seriesId for season $seasonId")
EpisodeList.Error(e) EpisodeList.Error(e)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -295,13 +317,13 @@ class SeriesViewModel
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val base = api.userLibraryApi.getItem(itemId).content val base = api.userLibraryApi.getItem(itemId).content
val item = BaseItem.Companion.from(base, api) val item = BaseItem.Companion.from(base, api)
val eps = episodes.value!! val eps = episodes.value
if (eps is EpisodeList.Success) { if (eps is EpisodeList.Success) {
val newItems = val newItems =
eps.episodes.items.toMutableList().apply { eps.episodes.toMutableList().apply {
this[listIndex] = item this[listIndex] = item
} }
val newValue = EpisodeList.Success(eps.episodes.copy(items = newItems)) val newValue = eps.copy(episodes = newItems)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
episodes.value = newValue episodes.value = newValue
} }
@ -398,32 +420,6 @@ class SeriesViewModel
} }
} }
data class ItemListAndMapping(
val items: List<BaseItem?>,
val numberToIndex: Map<Int, Int>,
val indexToNumber: Map<Int, Int>,
) {
companion object {
fun empty() = ItemListAndMapping(listOf(), mapOf(), mapOf())
}
}
/**
* Calculate the index<->season/ep number pairings
*
* This allows for handling of missing seasons
*/
private suspend fun convertPager(pager: ApiRequestPager<*>): ItemListAndMapping {
val pairs =
pager.mapIndexed { index, _ ->
val item = pager.getBlocking(index)
Pair(item?.indexNumber ?: index, index)
}
val seasonNumToIndex = mapOf(*pairs.toTypedArray())
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
}
sealed interface EpisodeList { sealed interface EpisodeList {
data object Loading : EpisodeList data object Loading : EpisodeList
@ -435,6 +431,7 @@ sealed interface EpisodeList {
} }
data class Success( data class Success(
val episodes: ItemListAndMapping, val episodes: List<BaseItem?>,
val initialIndex: Int,
) : EpisodeList ) : EpisodeList
} }

View file

@ -7,6 +7,7 @@ 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.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
@ -48,7 +49,7 @@ sealed class Destination(
val itemId: UUID, val itemId: UUID,
val type: BaseItemKind, val type: BaseItemKind,
@Transient val item: BaseItem? = null, @Transient val item: BaseItem? = null,
val seasonEpisode: SeasonEpisode? = null, val seasonEpisode: SeasonEpisodeIds? = null,
) : Destination() { ) : Destination() {
override fun toString(): String = "SeriesOverview(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)" override fun toString(): String = "SeriesOverview(itemId=$itemId, type=$type, seasonEpisode=$seasonEpisode)"
} }