diff --git a/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt index 432eec99..6cccf5b4 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt @@ -5,21 +5,24 @@ import android.os.Bundle import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.viewmodels.PlayerViewModel +import org.jellyfin.sdk.model.api.BaseItemDto import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel import java.lang.IllegalStateException class VideoVersionDialogFragment( - private val viewModel: MediaInfoViewModel + private val item: BaseItemDto, + private val viewModel: PlayerViewModel ) : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val items = viewModel.item.value?.mediaSources?.map { it.name } - return activity?.let { - val builder = MaterialAlertDialogBuilder(it) - builder.setTitle(getString(R.string.select_a_version)) - .setItems(items?.toTypedArray()) { _, which -> - viewModel.preparePlayerItems(which) - } - builder.create() + val items = item.mediaSources?.map { it.name }?.toTypedArray() + return activity?.let { activity -> + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.select_a_version) + .setItems(items) { _, which -> + viewModel.loadPlayerItems(item, which) + }.create() } ?: throw IllegalStateException("Activity cannot be null") } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index e4fadaf3..c87fbc6d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -6,7 +6,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -16,6 +18,8 @@ import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel +import dev.jdtech.jellyfin.viewmodels.PlayerViewModel +import timber.log.Timber @AndroidEntryPoint class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { @@ -23,6 +27,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { private lateinit var binding: EpisodeBottomSheetBinding private val viewModel: EpisodeBottomSheetViewModel by viewModels() + private val playerViewModel: PlayerViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -37,7 +42,16 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { binding.playButton.setOnClickListener { binding.playButton.setImageResource(android.R.color.transparent) binding.progressCircular.visibility = View.VISIBLE - viewModel.preparePlayerItems() + viewModel.item.value?.let { + playerViewModel.loadPlayerItems(it) + } + } + + playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems -> + when (playerItems) { + is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems) + is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems) + } } binding.checkButton.setOnClickListener { @@ -87,48 +101,38 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { binding.favoriteButton.setImageResource(drawable) }) - viewModel.navigateToPlayer.observe(viewLifecycleOwner, { - if (it) { - navigateToPlayerActivity( - viewModel.playerItems.toTypedArray(), - ) - viewModel.doneNavigateToPlayer() - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE - } - }) - - viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> - if (errorMessage != null) { - binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE - } else { - binding.playerItemsError.visibility = View.GONE - } - }) - - binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment( - viewModel.playerItemsError.value ?: getString(R.string.unknown_error) - ).show(parentFragmentManager, "errordialog") - } - viewModel.loadEpisode(args.episodeId) return binding.root } + private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) { + navigateToPlayerActivity(items.items.toTypedArray()) + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) + binding.progressCircular.visibility = View.INVISIBLE + } + + private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) { + Timber.e(error.message) + + binding.playerItemsError.isVisible = true + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) + binding.progressCircular.visibility = View.INVISIBLE + binding.playerItemsErrorDetails.setOnClickListener { + ErrorDialogFragment(error.message).show(parentFragmentManager, "errordialog") + } + } + private fun navigateToPlayerActivity( playerItems: Array, ) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt index 103fd145..74c00cb6 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -10,6 +10,7 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +23,9 @@ import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel +import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import org.jellyfin.sdk.model.api.BaseItemDto +import timber.log.Timber import org.jellyfin.sdk.model.serializer.toUUID import java.util.UUID @@ -31,6 +34,7 @@ class MediaInfoFragment : Fragment() { private lateinit var binding: FragmentMediaInfoBinding private val viewModel: MediaInfoViewModel by viewModels() + private val playerViewModel: PlayerViewModel by viewModels() private val args: MediaInfoFragmentArgs by navArgs() @@ -65,13 +69,6 @@ class MediaInfoFragment : Fragment() { viewModel.loadData(args.itemId, args.itemType) } - binding.errorLayout.errorDetailsButton.setOnClickListener { - ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show( - parentFragmentManager, - "errordialog" - ) - } - viewModel.item.observe(viewLifecycleOwner, { item -> if (item.originalTitle != item.name) { binding.originalTitle.visibility = View.VISIBLE @@ -94,21 +91,12 @@ class MediaInfoFragment : Fragment() { } }) - viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems -> - if (playerItems != null) { - navigateToPlayerActivity( - playerItems - ) - viewModel.doneNavigatingToPlayer() - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE + playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems -> + when (playerItems) { + is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems) + is PlayerViewModel.PlayerItems -> bindPlayerItems(playerItems) } - }) + } viewModel.played.observe(viewLifecycleOwner, { val drawable = when (it) { @@ -128,27 +116,6 @@ class MediaInfoFragment : Fragment() { binding.favoriteButton.setImageResource(drawable) }) - viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> - if (errorMessage != null) { - binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE - } else { - binding.playerItemsError.visibility = View.GONE - } - }) - - binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment( - viewModel.playerItemsError.value ?: getString(R.string.unknown_error) - ).show(parentFragmentManager, "errordialog") - } - binding.trailerButton.setOnClickListener { if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener val intent = Intent( @@ -178,19 +145,14 @@ class MediaInfoFragment : Fragment() { binding.playButton.setOnClickListener { binding.playButton.setImageResource(android.R.color.transparent) binding.progressCircular.visibility = View.VISIBLE - if (args.itemType == "Movie") { - if (viewModel.item.value?.mediaSources != null) { - if (viewModel.item.value?.mediaSources?.size!! > 1) { - VideoVersionDialogFragment(viewModel).show( - parentFragmentManager, - "videoversiondialog" - ) - } else { - viewModel.preparePlayerItems() - } + + viewModel.item.value?.let { item -> + playerViewModel.loadPlayerItems(item) { + VideoVersionDialogFragment(item, playerViewModel).show( + parentFragmentManager, + "videoversiondialog" + ) } - } else if (args.itemType == "Series") { - viewModel.preparePlayerItems() } } @@ -211,6 +173,33 @@ class MediaInfoFragment : Fragment() { viewModel.loadData(args.itemId, args.itemType) } + private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) { + navigateToPlayerActivity(items.items.toTypedArray()) + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) + binding.progressCircular.visibility = View.INVISIBLE + } + + private fun bindPlayerItemsError(error: PlayerViewModel.PlayerItemError) { + Timber.e(error.message) + + binding.playerItemsError.visibility = View.VISIBLE + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) + binding.progressCircular.visibility = View.INVISIBLE + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(error.message).show(parentFragmentManager, "errordialog") + } + } + private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { findNavController().navigate( MediaInfoFragmentDirections.actionMediaInfoFragmentToEpisodeBottomSheetFragment( diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt index 33ff30b5..cba60e78 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -6,16 +6,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.LocationType import timber.log.Timber import java.text.DateFormat import java.time.ZoneOffset -import java.util.* +import java.util.Date +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -40,14 +38,6 @@ constructor( private val _favorite = MutableLiveData() val favorite: LiveData = _favorite - private val _navigateToPlayer = MutableLiveData() - val navigateToPlayer: LiveData = _navigateToPlayer - - var playerItems: MutableList = mutableListOf() - - private val _playerItemsError = MutableLiveData() - val playerItemsError: LiveData = _playerItemsError - fun loadEpisode(episodeId: UUID) { viewModelScope.launch { try { @@ -63,56 +53,6 @@ constructor( } } - fun preparePlayerItems() { - _playerItemsError.value = null - viewModelScope.launch { - try { - createPlayerItems(_item.value!!) - _navigateToPlayer.value = true - } catch (e: Exception) { - _playerItemsError.value = e.toString() - } - } - } - - private suspend fun createPlayerItems(startEpisode: BaseItemDto) { - playerItems.clear() - - val playbackPosition = startEpisode.userData?.playbackPositionTicks?.div(10000) ?: 0 - // Intros - var introsCount = 0 - - if (playbackPosition <= 0) { - val intros = jellyfinRepository.getIntros(startEpisode.id) - for (intro in intros) { - if (intro.mediaSources.isNullOrEmpty()) continue - playerItems.add(PlayerItem(intro.name, intro.id, intro.mediaSources?.get(0)?.id!!, 0)) - introsCount += 1 - } - } - - val episodes = jellyfinRepository.getEpisodes( - startEpisode.seriesId!!, - startEpisode.seasonId!!, - startItemId = startEpisode.id, - fields = listOf(ItemFields.MEDIA_SOURCES) - ) - for (episode in episodes) { - if (episode.mediaSources.isNullOrEmpty()) continue - if (episode.locationType == LocationType.VIRTUAL) continue - playerItems.add( - PlayerItem( - episode.name, - episode.id, - episode.mediaSources?.get(0)?.id!!, - playbackPosition - ) - ) - } - - if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found") - } - fun markAsPlayed(itemId: UUID) { viewModelScope.launch { jellyfinRepository.markAsPlayed(itemId) @@ -151,8 +91,4 @@ constructor( item.premiereDate.toString() } } - - fun doneNavigateToPlayer() { - _navigateToPlayer.value = false - } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt index ff032f3b..54e3a94f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -6,17 +6,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemPerson -import org.jellyfin.sdk.model.api.ItemFields -import org.jellyfin.sdk.model.api.LocationType import timber.log.Timber -import java.util.* +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -53,9 +50,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _seasons = MutableLiveData>() val seasons: LiveData> = _seasons - private val _navigateToPlayer = MutableLiveData>() - val navigateToPlayer: LiveData> = _navigateToPlayer - private val _played = MutableLiveData() val played: LiveData = _played @@ -65,11 +59,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _error = MutableLiveData() val error: LiveData = _error - var playerItems: MutableList = mutableListOf() - - private val _playerItemsError = MutableLiveData() - val playerItemsError: LiveData = _playerItemsError - fun loadData(itemId: UUID, itemType: String) { _error.value = null viewModelScope.launch { @@ -177,97 +166,4 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { else -> dateString } } - - fun preparePlayerItems(mediaSourceIndex: Int? = null) { - _playerItemsError.value = null - viewModelScope.launch { - try { - createPlayerItems(_item.value!!, mediaSourceIndex) - _navigateToPlayer.value = playerItems.toTypedArray() - } catch (e: Exception) { - _playerItemsError.value = e.message - } - } - } - - private suspend fun createPlayerItems(series: BaseItemDto, mediaSourceIndex: Int? = null) { - playerItems.clear() - - val playbackPosition = item.value?.userData?.playbackPositionTicks?.div(10000) ?: 0 - - // Intros - var introsCount = 0 - - if (playbackPosition <= 0) { - val intros = jellyfinRepository.getIntros(series.id) - for (intro in intros) { - if (intro.mediaSources.isNullOrEmpty()) continue - playerItems.add(PlayerItem(intro.name, intro.id, intro.mediaSources?.get(0)?.id!!, 0)) - introsCount += 1 - } - } - - when (series.type) { - "Movie" -> { - playerItems.add( - PlayerItem( - series.name, - series.id, - series.mediaSources?.get(mediaSourceIndex ?: 0)?.id!!, - playbackPosition - ) - ) - } - "Series" -> { - if (nextUp.value != null) { - val startEpisode = nextUp.value!! - val episodes = jellyfinRepository.getEpisodes( - startEpisode.seriesId!!, - startEpisode.seasonId!!, - startItemId = startEpisode.id, - fields = listOf(ItemFields.MEDIA_SOURCES) - ) - for (episode in episodes) { - if (episode.mediaSources.isNullOrEmpty()) continue - if (episode.locationType == LocationType.VIRTUAL) continue - playerItems.add( - PlayerItem( - episode.name, - episode.id, - episode.mediaSources?.get(0)?.id!!, - 0 - ) - ) - } - } else { - for (season in seasons.value!!) { - if (season.indexNumber == 0) continue - val episodes = jellyfinRepository.getEpisodes( - series.id, - season.id, - fields = listOf(ItemFields.MEDIA_SOURCES) - ) - for (episode in episodes) { - if (episode.mediaSources.isNullOrEmpty()) continue - if (episode.locationType == LocationType.VIRTUAL) continue - playerItems.add( - PlayerItem( - episode.name, - episode.id, - episode.mediaSources?.get(0)?.id!!, - 0 - ) - ) - } - } - } - } - } - - if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found") - } - - fun doneNavigatingToPlayer() { - _navigateToPlayer.value = null - } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 4d177982..e24b42cc 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -8,7 +8,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.BasePlayer +import com.google.android.exoplayer2.DefaultRenderersFactory +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.models.PlayerItem @@ -18,7 +23,7 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber -import java.util.* +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -114,9 +119,8 @@ constructor( player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition) player.prepare() player.play() + pollPosition(player) } - - pollPosition(player) } private fun releasePlayer() { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt new file mode 100644 index 00000000..19d653ef --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -0,0 +1,150 @@ +package dev.jdtech.jellyfin.viewmodels + +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.jdtech.jellyfin.models.PlayerItem +import dev.jdtech.jellyfin.repository.JellyfinRepository +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.LocationType.VIRTUAL +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class PlayerViewModel @Inject internal constructor( + private val repository: JellyfinRepository +) : ViewModel() { + + private val playerItems = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + fun onPlaybackRequested(scope: LifecycleCoroutineScope, collector: (PlayerItemState) -> Unit) { + scope.launch { playerItems.collect { collector(it) } } + } + + fun loadPlayerItems( + item: BaseItemDto, + mediaSourceIndex: Int = 0, + onVersionSelectRequired: () -> Unit = { Unit } + ) { + Timber.d("Loading player items for item ${item.id}") + if (item.mediaSources.orEmpty().size > 1) { + onVersionSelectRequired() + } + + viewModelScope.launch { + val playbackPosition = item.userData?.playbackPositionTicks?.div(10000) ?: 0 + + val items = try { + createItems(item, playbackPosition, mediaSourceIndex).let(::PlayerItems) + } catch (e: Exception) { + PlayerItemError(e.message.orEmpty()) + } + + playerItems.tryEmit(items) + } + } + + private suspend fun createItems( + item: BaseItemDto, + playbackPosition: Long, + mediaSourceIndex: Int + ) = if (playbackPosition <= 0) { + prepareIntros(item) + prepareMediaPlayerItems( + item, + playbackPosition, + mediaSourceIndex + ) + } else { + prepareMediaPlayerItems(item, playbackPosition, mediaSourceIndex) + } + + private suspend fun prepareIntros(item: BaseItemDto): List { + return repository + .getIntros(item.id) + .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } + .map { intro -> + PlayerItem( + intro.name, + intro.id, + intro.mediaSources?.get(0)?.id!!, + 0 + ) + } + } + + private suspend fun prepareMediaPlayerItems( + item: BaseItemDto, + playbackPosition: Long, + mediaSourceIndex: Int + ): List = when (item.type) { + "Movie" -> itemToMoviePlayerItems(item, playbackPosition, mediaSourceIndex) + "Series" -> itemToPlayerItems(item, playbackPosition, mediaSourceIndex) + "Episode" -> itemToPlayerItems(item, playbackPosition, mediaSourceIndex) + else -> emptyList() + } + + private fun itemToMoviePlayerItems( + item: BaseItemDto, + playbackPosition: Long, + mediaSourceIndex: Int + ) = listOf( + PlayerItem( + item.name, + item.id, + item.mediaSources?.get(mediaSourceIndex)?.id!!, + playbackPosition + ) + ) + + private suspend fun itemToPlayerItems( + item: BaseItemDto, + playbackPosition: Long, + mediaSourceIndex: Int + ): List { + val nextUp = repository.getNextUp(item.seriesId) + + return if (nextUp.isEmpty()) { + repository + .getSeasons(item.seriesId!!) + .flatMap { episodesToPlayerItems(item, playbackPosition, mediaSourceIndex) } + } else { + episodesToPlayerItems(item, playbackPosition, mediaSourceIndex) + } + } + + private suspend fun episodesToPlayerItems( + item: BaseItemDto, + playbackPosition: Long, + mediaSourceIndex: Int + ): List { + val episodes = repository.getEpisodes( + seriesId = item.seriesId!!, + seasonId = item.seasonId!!, + fields = listOf(ItemFields.MEDIA_SOURCES), + startItemId = item.id + ) + + return episodes + .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } + .filter { it.locationType != VIRTUAL } + .map { episode -> + PlayerItem( + episode.name, + episode.id, + episode.mediaSources?.get(mediaSourceIndex)?.id!!, + playbackPosition + ) + } + } + + sealed class PlayerItemState + + data class PlayerItemError(val message: String): PlayerItemState() + data class PlayerItems(val items: List): PlayerItemState() +} \ No newline at end of file