mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Handle mapping missing seasons or episodes
This commit is contained in:
parent
6d3e44c877
commit
f8efffa3c2
5 changed files with 84 additions and 45 deletions
|
|
@ -29,7 +29,7 @@ fun SeriesDetails(
|
||||||
viewModel.init(destination.itemId, destination.item, null, null)
|
viewModel.init(destination.itemId, destination.item, null, null)
|
||||||
}
|
}
|
||||||
val item by viewModel.item.observeAsState()
|
val item by viewModel.item.observeAsState()
|
||||||
val seasons by viewModel.seasons.observeAsState(listOf())
|
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
|
|
@ -44,7 +44,7 @@ fun SeriesDetails(
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = "Seasons",
|
title = "Seasons",
|
||||||
items = seasons,
|
items = seasons.items,
|
||||||
onClickItem = { navigationManager.navigateTo(it.destination()) },
|
onClickItem = { navigationManager.navigateTo(it.destination()) },
|
||||||
onLongClickItem = { },
|
onLongClickItem = { },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ class SeriesViewModel
|
||||||
private var player: Player? = null
|
private var player: Player? = null
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val seasons = MutableLiveData<List<BaseItem?>>(listOf())
|
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
||||||
val episodes = MutableLiveData<List<BaseItem?>>(listOf())
|
val episodes = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
|
|
@ -70,22 +70,22 @@ class SeriesViewModel
|
||||||
) {
|
) {
|
||||||
val item = fetchItem(seriesId, potential)
|
val item = fetchItem(seriesId, potential)
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
val seasonPager = getSeasons(item)
|
val seasonsInfo = getSeasons(item)
|
||||||
val episodePager =
|
val episodeInfo =
|
||||||
season?.let { seasonNum ->
|
(season ?: seasonsInfo.items.firstOrNull()?.indexNumber)
|
||||||
// TODO map season number to index in list
|
?.let { seasonNum ->
|
||||||
loadEpisodesInternal(seasonNum)
|
loadEpisodesInternal(seasonNum)
|
||||||
}
|
} ?: ItemListAndMapping.empty()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
seasons.value = seasonPager.orEmpty()
|
seasons.value = seasonsInfo
|
||||||
episodes.value = episodePager.orEmpty()
|
episodes.value = episodeInfo
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
maybePlayThemeSong()
|
maybePlayThemeSong()
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
seasons.value = listOf()
|
seasons.value = ItemListAndMapping.empty()
|
||||||
episodes.value = listOf()
|
episodes.value = ItemListAndMapping.empty()
|
||||||
loading.value = LoadingState.Error("Series $seriesId not found")
|
loading.value = LoadingState.Error("Series $seriesId not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ class SeriesViewModel
|
||||||
player = null
|
player = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSeasons(item: BaseItem): ApiRequestPager<GetItemsRequest>? {
|
private suspend fun getSeasons(item: BaseItem): ItemListAndMapping {
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
|
|
@ -164,12 +164,12 @@ class SeriesViewModel
|
||||||
val season = pager.getBlocking(index)
|
val season = pager.getBlocking(index)
|
||||||
Pair(season?.indexNumber!!, index)
|
Pair(season?.indexNumber!!, index)
|
||||||
}
|
}
|
||||||
mapOf(*pairs.toTypedArray())
|
val seasonNumToIndex = mapOf(*pairs.toTypedArray())
|
||||||
mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
||||||
return pager
|
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadEpisodesInternal(season: Int): ApiRequestPager<GetEpisodesRequest> {
|
private suspend fun loadEpisodesInternal(season: Int): ItemListAndMapping {
|
||||||
val request =
|
val request =
|
||||||
GetEpisodesRequest(
|
GetEpisodesRequest(
|
||||||
seriesId = item.value!!.id,
|
seriesId = item.value!!.id,
|
||||||
|
|
@ -188,21 +188,21 @@ class SeriesViewModel
|
||||||
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||||
pager.init()
|
pager.init()
|
||||||
Timber.Forest.v("Loaded ${pager.size} episodes for season $season")
|
Timber.Forest.v("Loaded ${pager.size} episodes for season $season")
|
||||||
return pager
|
return convertPager(pager)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisodes(season: Int) =
|
fun loadEpisodes(season: Int) =
|
||||||
viewModelScope.async(ExceptionHandler(true)) {
|
viewModelScope.async(ExceptionHandler(true)) {
|
||||||
val episodePager =
|
val episodes =
|
||||||
try {
|
try {
|
||||||
loadEpisodesInternal(season)
|
loadEpisodesInternal(season)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.Forest.e(e, "Error loading episodes for $seriesId for season $season")
|
Timber.Forest.e(e, "Error loading episodes for $seriesId for season $season")
|
||||||
// TODO show error in UI?
|
// TODO show error in UI?
|
||||||
listOf()
|
ItemListAndMapping.empty()
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
episodes.value = episodePager
|
this@SeriesViewModel.episodes.value = episodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,9 +225,34 @@ class SeriesViewModel
|
||||||
) = viewModelScope.launch(ExceptionHandler()) {
|
) = viewModelScope.launch(ExceptionHandler()) {
|
||||||
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!!
|
||||||
episodes.value =
|
episodes.value =
|
||||||
episodes.value!!.toMutableList().apply {
|
eps.copy(
|
||||||
|
items =
|
||||||
|
eps.items.toMutableList().apply {
|
||||||
this[listIndex] = item
|
this[listIndex] = item
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun convertPager(pager: ApiRequestPager<*>): ItemListAndMapping {
|
||||||
|
val pairs =
|
||||||
|
pager.mapIndexed { index, _ ->
|
||||||
|
val season = pager.getBlocking(index)
|
||||||
|
Pair(season?.indexNumber!!, index)
|
||||||
|
}
|
||||||
|
val seasonNumToIndex = mapOf(*pairs.toTypedArray())
|
||||||
|
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
||||||
|
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialogInfo
|
||||||
|
import com.github.damontecres.dolphin.ui.detail.ItemListAndMapping
|
||||||
import com.github.damontecres.dolphin.ui.detail.SeriesViewModel
|
import com.github.damontecres.dolphin.ui.detail.SeriesViewModel
|
||||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
|
@ -48,7 +49,7 @@ fun SeriesOverview(
|
||||||
destination: Destination.MediaItem,
|
destination: Destination.MediaItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SeriesViewModel = hiltViewModel(),
|
viewModel: SeriesViewModel = hiltViewModel(),
|
||||||
initialSeasonEpisode: SeasonEpisode = SeasonEpisode(0, 0),
|
initialSeasonEpisode: SeasonEpisode? = null,
|
||||||
) {
|
) {
|
||||||
val firstItemFocusRequester = remember { FocusRequester() }
|
val firstItemFocusRequester = remember { FocusRequester() }
|
||||||
val episodeRowFocusRequester = remember { FocusRequester() }
|
val episodeRowFocusRequester = remember { FocusRequester() }
|
||||||
|
|
@ -58,19 +59,20 @@ fun SeriesOverview(
|
||||||
viewModel.init(
|
viewModel.init(
|
||||||
destination.itemId,
|
destination.itemId,
|
||||||
destination.item,
|
destination.item,
|
||||||
initialSeasonEpisode.season,
|
initialSeasonEpisode?.season,
|
||||||
initialSeasonEpisode.episode,
|
initialSeasonEpisode?.episode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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(listOf())
|
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
||||||
val episodes by viewModel.episodes.observeAsState(listOf())
|
val episodes by viewModel.episodes.observeAsState(ItemListAndMapping.empty())
|
||||||
|
|
||||||
// TODO need to translate season/episode into tab/row index in case of missing seasons/episodes
|
// TODO need to translate season/episode into tab/row index in case of missing seasons/episodes
|
||||||
var position by rememberSaveable(
|
var position by rememberSaveable(
|
||||||
destination,
|
destination,
|
||||||
|
loading,
|
||||||
stateSaver =
|
stateSaver =
|
||||||
Saver(
|
Saver(
|
||||||
save = { listOf(it.seasonTabIndex, it.episodeRowIndex) },
|
save = { listOf(it.seasonTabIndex, it.episodeRowIndex) },
|
||||||
|
|
@ -79,19 +81,19 @@ fun SeriesOverview(
|
||||||
) {
|
) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
SeriesOverviewPosition(
|
SeriesOverviewPosition(
|
||||||
initialSeasonEpisode.season,
|
seasons.numberToIndex[initialSeasonEpisode?.season ?: 0] ?: 0,
|
||||||
initialSeasonEpisode.episode,
|
episodes.numberToIndex[initialSeasonEpisode?.episode ?: 0] ?: 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(episodes) {
|
LaunchedEffect(episodes.items) {
|
||||||
if (episodes.isNotEmpty()) {
|
if (episodes.items.isNotEmpty()) {
|
||||||
// TODO focus on first episode when changing seasons
|
// TODO focus on first episode when changing seasons
|
||||||
// firstItemFocusRequester.requestFocus()
|
// firstItemFocusRequester.requestFocus()
|
||||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||||
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,8 +109,8 @@ fun SeriesOverview(
|
||||||
LaunchedEffect(Unit) { episodeRowFocusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { episodeRowFocusRequester.tryRequestFocus() }
|
||||||
SeriesOverviewContent(
|
SeriesOverviewContent(
|
||||||
series = series,
|
series = series,
|
||||||
seasons = seasons,
|
seasons = seasons.items,
|
||||||
episodes = episodes,
|
episodes = episodes.items,
|
||||||
position = position,
|
position = position,
|
||||||
backdropImageUrl =
|
backdropImageUrl =
|
||||||
remember {
|
remember {
|
||||||
|
|
@ -121,7 +123,9 @@ fun SeriesOverview(
|
||||||
episodeRowFocusRequester = episodeRowFocusRequester,
|
episodeRowFocusRequester = episodeRowFocusRequester,
|
||||||
onFocus = {
|
onFocus = {
|
||||||
if (it.seasonTabIndex != position.seasonTabIndex) {
|
if (it.seasonTabIndex != position.seasonTabIndex) {
|
||||||
viewModel.loadEpisodes(seasons[it.seasonTabIndex]!!.indexNumber!!)
|
seasons.indexToNumber[it.seasonTabIndex]?.let { seasonNumber ->
|
||||||
|
viewModel.loadEpisodes(seasonNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
position = it
|
position = it
|
||||||
},
|
},
|
||||||
|
|
@ -143,7 +147,7 @@ fun SeriesOverview(
|
||||||
// TODO
|
// TODO
|
||||||
},
|
},
|
||||||
playOnClick = { resume ->
|
playOnClick = { resume ->
|
||||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||||
viewModel.release()
|
viewModel.release()
|
||||||
navigationManager.navigateTo(
|
navigationManager.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
|
|
@ -155,7 +159,7 @@ fun SeriesOverview(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watchOnClick = {
|
watchOnClick = {
|
||||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||||
val played = it.data.userData?.played ?: false
|
val played = it.data.userData?.played ?: false
|
||||||
viewModel.setWatched(it.id, !played, position.episodeRowIndex)
|
viewModel.setWatched(it.id, !played, position.episodeRowIndex)
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +168,7 @@ fun SeriesOverview(
|
||||||
// TODO show more actions
|
// TODO show more actions
|
||||||
},
|
},
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||||
overviewDialog =
|
overviewDialog =
|
||||||
ItemDetailsDialogInfo(
|
ItemDetailsDialogInfo(
|
||||||
title = it.name ?: "Unknown",
|
title = it.name ?: "Unknown",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
|
@ -40,9 +41,11 @@ import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.dolphin.ui.cards.BannerCard
|
import com.github.damontecres.dolphin.ui.cards.BannerCard
|
||||||
import com.github.damontecres.dolphin.ui.ifElse
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -71,6 +74,8 @@ fun SeriesOverviewContent(
|
||||||
val tabRowFocusRequester = remember { FocusRequester() }
|
val tabRowFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val focusedEpisode = episodes.getOrNull(position.episodeRowIndex)
|
val focusedEpisode = episodes.getOrNull(position.episodeRowIndex)
|
||||||
|
LaunchedEffect(position) {
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -174,6 +179,12 @@ fun SeriesOverviewContent(
|
||||||
item {
|
item {
|
||||||
key(position.seasonTabIndex) {
|
key(position.seasonTabIndex) {
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
|
OneTimeLaunchedEffect {
|
||||||
|
if (state.firstVisibleItemIndex != position.episodeRowIndex) {
|
||||||
|
state.scrollToItem(position.episodeRowIndex)
|
||||||
|
firstItemFocusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = state,
|
state = state,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
|
@ -205,7 +216,7 @@ fun SeriesOverviewContent(
|
||||||
onLongClick = { if (episode != null) onLongClick.invoke(episode) },
|
onLongClick = { if (episode != null) onLongClick.invoke(episode) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.ifElse(
|
Modifier.ifElse(
|
||||||
episodeIndex == 0,
|
episodeIndex == position.episodeRowIndex,
|
||||||
Modifier.focusRequester(firstItemFocusRequester),
|
Modifier.focusRequester(firstItemFocusRequester),
|
||||||
),
|
),
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import com.github.damontecres.dolphin.ui.detail.EpisodeDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
import com.github.damontecres.dolphin.ui.detail.SeasonDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.VideoDetails
|
import com.github.damontecres.dolphin.ui.detail.VideoDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails
|
import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails
|
||||||
import com.github.damontecres.dolphin.ui.detail.series.SeasonEpisode
|
|
||||||
import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview
|
||||||
import com.github.damontecres.dolphin.ui.main.MainPage
|
import com.github.damontecres.dolphin.ui.main.MainPage
|
||||||
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
import com.github.damontecres.dolphin.ui.playback.PlaybackContent
|
||||||
|
|
@ -64,7 +63,7 @@ fun DestinationContent(
|
||||||
navigationManager,
|
navigationManager,
|
||||||
destination,
|
destination,
|
||||||
modifier,
|
modifier,
|
||||||
initialSeasonEpisode = destination.seasonEpisode ?: SeasonEpisode(0, 0),
|
initialSeasonEpisode = destination.seasonEpisode,
|
||||||
)
|
)
|
||||||
|
|
||||||
BaseItemKind.SEASON ->
|
BaseItemKind.SEASON ->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue