diff --git a/app/build.gradle b/app/build.gradle index f3f12b4a..9c56d103 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId "dev.jdtech.jellyfin" minSdkVersion 24 targetSdkVersion 31 - versionCode 2 - versionName "0.1.1" + versionCode 3 + versionName "0.1.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 39fc3b6c..6ea8b71f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,4 +20,4 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep public class dev.jdtech.jellyfin.models.PlayerItem \ No newline at end of file +-keepnames class dev.jdtech.jellyfin.models.PlayerItem \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index 40315453..c336aeb1 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.models.FavoriteSection import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemPerson +import org.jellyfin.sdk.model.api.ImageType import java.util.* @BindingAdapter("servers") @@ -35,11 +36,12 @@ fun bindItems(recyclerView: RecyclerView, data: List?) { fun bindItemImage(imageView: ImageView, item: BaseItemDto) { val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") - val itemId = if (item.type == "Episode") item.seriesId else item.id + val itemId = + if (item.type == "Episode" || item.type == "Season" && item.imageTags.isNullOrEmpty()) item.seriesId else item.id Glide .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/Primary")) + .load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.PRIMARY}")) .transition(DrawableTransitionOptions.withCrossFade()) .placeholder(R.color.neutral_800) .into(imageView) @@ -49,17 +51,16 @@ fun bindItemImage(imageView: ImageView, item: BaseItemDto) { @BindingAdapter("itemBackdropImage") fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) { - if (item != null) { - val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") + if (item == null) return + val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") - Glide - .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Backdrop")) - .transition(DrawableTransitionOptions.withCrossFade()) - .into(imageView) + Glide + .with(imageView.context) + .load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/${ImageType.BACKDROP}")) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(imageView) - imageView.contentDescription = "${item.name} backdrop" - } + imageView.contentDescription = "${item.name} backdrop" } @BindingAdapter("itemBackdropById") @@ -68,7 +69,7 @@ fun bindItemBackdropById(imageView: ImageView, itemId: UUID) { Glide .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/Backdrop")) + .load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.BACKDROP}")) .transition(DrawableTransitionOptions.withCrossFade()) .into(imageView) } @@ -79,20 +80,6 @@ fun bindCollections(recyclerView: RecyclerView, data: List?) { adapter.submitList(data) } -@BindingAdapter("collectionImage") -fun bindCollectionImage(imageView: ImageView, item: BaseItemDto) { - val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") - - Glide - .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Primary")) - .transition(DrawableTransitionOptions.withCrossFade()) - .placeholder(R.color.neutral_800) - .into(imageView) - - imageView.contentDescription = "${item.name} image" -} - @BindingAdapter("people") fun bindPeople(recyclerView: RecyclerView, data: List?) { val adapter = recyclerView.adapter as PersonListAdapter @@ -105,7 +92,7 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) { Glide .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${person.id}/Images/Primary")) + .load(jellyfinApi.api.baseUrl.plus("/items/${person.id}/Images/${ImageType.PRIMARY}")) .transition(DrawableTransitionOptions.withCrossFade()) .placeholder(R.color.neutral_800) .into(imageView) @@ -125,15 +112,38 @@ fun bindHomeEpisodes(recyclerView: RecyclerView, data: List?) { adapter.submitList(data) } -@BindingAdapter("episodeImage") -fun bindEpisodeImage(imageView: ImageView, episode: BaseItemDto) { +@BindingAdapter("baseItemImage") +fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) { + if (episode == null) return + val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") - val imageType = if (episode.type == "Movie") "Backdrop" else "Primary" + var imageItemId = episode.id + var imageType = ImageType.PRIMARY + + if (!episode.imageTags.isNullOrEmpty()) { + when (episode.type) { + "Movie" -> { + if (!episode.backdropImageTags.isNullOrEmpty()) { + imageType = ImageType.BACKDROP + } + } + else -> { + if (!episode.imageTags!!.keys.contains(ImageType.PRIMARY)) { + imageType = ImageType.BACKDROP + } + } + } + } else { + if (episode.type == "Episode") { + imageItemId = episode.seriesId!! + imageType = ImageType.BACKDROP + } + } Glide .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${episode.id}/Images/$imageType")) + .load(jellyfinApi.api.baseUrl.plus("/items/${imageItemId}/Images/$imageType")) .transition(DrawableTransitionOptions.withCrossFade()) .placeholder(R.color.neutral_800) .into(imageView) @@ -147,28 +157,12 @@ fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) { Glide .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${seasonId}/Images/Primary")) + .load(jellyfinApi.api.baseUrl.plus("/items/${seasonId}/Images/${ImageType.PRIMARY}")) .transition(DrawableTransitionOptions.withCrossFade()) .placeholder(R.color.neutral_800) .into(imageView) } -@BindingAdapter("itemPrimaryImage") -fun bindItemPrimaryImage(imageView: ImageView, item: BaseItemDto?) { - if (item != null) { - val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") - - Glide - .with(imageView.context) - .load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Primary")) - .transition(DrawableTransitionOptions.withCrossFade()) - .placeholder(R.color.neutral_800) - .into(imageView) - - imageView.contentDescription = "${item.name} poster" - } -} - @BindingAdapter("favoriteSections") fun bindFavoriteSections(recyclerView: RecyclerView, data: List?) { val adapter = recyclerView.adapter as FavoritesListAdapter diff --git a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index f4ad3546..ea3ad3ad 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -38,7 +38,7 @@ class PlayerActivity : AppCompatActivity() { }) if (viewModel.player.value == null) { - viewModel.initializePlayer(args.items, args.playbackPosition) + viewModel.initializePlayer(args.items) } hideSystemUI() } diff --git a/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt b/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt index 7363f121..f7f3efef 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt @@ -1,14 +1,11 @@ package dev.jdtech.jellyfin.database import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.Query -import androidx.room.Update +import androidx.room.* @Dao interface ServerDatabaseDao { - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(server: Server) @Update 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 912d4074..84caf7bf 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt @@ -11,12 +11,12 @@ class VideoVersionDialogFragment( private val viewModel: MediaInfoViewModel ) : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val items = viewModel.mediaSources.value!!.map { it.name } + val items = viewModel.item.value?.mediaSources?.map { it.name } return activity?.let { val builder = AlertDialog.Builder(it) builder.setTitle("Select a version") - .setItems(items.toTypedArray()) { _, which -> - viewModel.navigateToPlayer(viewModel.mediaSources.value!![which]) + .setItems(items?.toTypedArray()) { _, which -> + viewModel.preparePlayerItems(which) } builder.create() } ?: throw IllegalStateException("Activity cannot be null") 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 123391e0..e4fadaf3 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -37,7 +37,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { binding.playButton.setOnClickListener { binding.playButton.setImageResource(android.R.color.transparent) binding.progressCircular.visibility = View.VISIBLE - viewModel.preparePlayer() + viewModel.preparePlayerItems() } binding.checkButton.setOnClickListener { @@ -91,10 +91,14 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { if (it) { navigateToPlayerActivity( viewModel.playerItems.toTypedArray(), - viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000) ) viewModel.doneNavigateToPlayer() - binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) binding.progressCircular.visibility = View.INVISIBLE } }) @@ -102,7 +106,12 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> if (errorMessage != null) { binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) binding.progressCircular.visibility = View.INVISIBLE } else { binding.playerItemsError.visibility = View.GONE @@ -110,7 +119,9 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { }) binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment( + viewModel.playerItemsError.value ?: getString(R.string.unknown_error) + ).show(parentFragmentManager, "errordialog") } viewModel.loadEpisode(args.episodeId) @@ -120,12 +131,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { private fun navigateToPlayerActivity( playerItems: Array, - playbackPosition: Long ) { findNavController().navigate( EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity( playerItems, - playbackPosition ) ) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt index d92e5a04..768fd6a2 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt @@ -14,6 +14,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -44,6 +45,7 @@ class FavoriteFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.favoritesRecyclerView.visibility = View.GONE } else { diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt index 78891d63..2752ef5b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt @@ -12,6 +12,7 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewListAdapter import dev.jdtech.jellyfin.databinding.FragmentHomeBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.HomeViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -73,6 +74,7 @@ class HomeFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.viewsRecyclerView.visibility = View.GONE } else { @@ -86,7 +88,10 @@ class HomeFragment : Fragment() { } binding.errorLayout.errorDetailsButton.setOnClickListener { - ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show( + parentFragmentManager, + "errordialog" + ) } return binding.root @@ -96,7 +101,8 @@ class HomeFragment : Fragment() { findNavController().navigate( HomeFragmentDirections.actionNavigationHomeToLibraryFragment( view.id, - view.name + view.name, + view.type ) ) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt index 3eba789f..739f00f9 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt @@ -14,6 +14,7 @@ import dev.jdtech.jellyfin.viewmodels.LibraryViewModel import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.utils.checkIfLoginRequired import org.jellyfin.sdk.model.api.BaseItemDto @AndroidEntryPoint @@ -41,6 +42,7 @@ class LibraryFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.itemsRecyclerView.visibility = View.GONE } else { @@ -50,7 +52,7 @@ class LibraryFragment : Fragment() { }) binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadItems(args.libraryId) + viewModel.loadItems(args.libraryId, args.libraryType) } binding.errorLayout.errorDetailsButton.setOnClickListener { @@ -65,7 +67,7 @@ class LibraryFragment : Fragment() { ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item -> navigateToMediaInfoFragment(item) }) - viewModel.loadItems(args.libraryId) + viewModel.loadItems(args.libraryId, args.libraryType) } private fun navigateToMediaInfoFragment(item: BaseItemDto) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt index cb6b35d0..c0ed3b2b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt @@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.adapters.CollectionListAdapter import dev.jdtech.jellyfin.databinding.FragmentMediaBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.MediaViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -66,6 +67,7 @@ class MediaFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.viewsRecyclerView.visibility = View.GONE } else { @@ -89,7 +91,8 @@ class MediaFragment : Fragment() { findNavController().navigate( MediaFragmentDirections.actionNavigationMediaToLibraryFragment( library.id, - library.name + library.name, + library.collectionType, ) ) } 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 a94aa985..4bb4ffd2 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -19,6 +19,7 @@ import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment 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 org.jellyfin.sdk.model.api.BaseItemDto @@ -48,6 +49,7 @@ class MediaInfoFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.mediaInfoScrollview.visibility = View.GONE } else { @@ -61,7 +63,10 @@ class MediaInfoFragment : Fragment() { } binding.errorLayout.errorDetailsButton.setOnClickListener { - ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show( + parentFragmentManager, + "errordialog" + ) } viewModel.item.observe(viewLifecycleOwner, { item -> @@ -89,8 +94,7 @@ class MediaInfoFragment : Fragment() { viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems -> if (playerItems != null) { navigateToPlayerActivity( - playerItems, - viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000) + playerItems ) viewModel.doneNavigatingToPlayer() binding.playButton.setImageDrawable( @@ -137,7 +141,9 @@ class MediaInfoFragment : Fragment() { }) binding.playerItemsErrorDetails.setOnClickListener { - ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + ErrorDialogFragment( + viewModel.playerItemsError.value ?: getString(R.string.unknown_error) + ).show(parentFragmentManager, "errordialog") } binding.trailerButton.setOnClickListener { @@ -162,33 +168,18 @@ class MediaInfoFragment : Fragment() { binding.playButton.setImageResource(android.R.color.transparent) binding.progressCircular.visibility = View.VISIBLE if (args.itemType == "Movie") { - if (!viewModel.mediaSources.value.isNullOrEmpty()) { - if (viewModel.mediaSources.value!!.size > 1) { + if (viewModel.item.value?.mediaSources != null) { + if (viewModel.item.value?.mediaSources?.size!! > 1) { VideoVersionDialogFragment(viewModel).show( parentFragmentManager, "videoversiondialog" ) } else { - navigateToPlayerActivity( - arrayOf( - PlayerItem( - args.itemId, - viewModel.mediaSources.value!![0].id!! - ) - ), - viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000), - ) - binding.playButton.setImageDrawable( - ContextCompat.getDrawable( - requireActivity(), - R.drawable.ic_play - ) - ) - binding.progressCircular.visibility = View.INVISIBLE + viewModel.preparePlayerItems() } } } else if (args.itemType == "Series") { - viewModel.preparePlayer() + viewModel.preparePlayerItems() } } @@ -230,12 +221,10 @@ class MediaInfoFragment : Fragment() { private fun navigateToPlayerActivity( playerItems: Array, - playbackPosition: Long, ) { findNavController().navigate( MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity( playerItems, - playbackPosition ) ) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt index f13de478..35e2fc65 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt @@ -15,6 +15,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -47,6 +48,7 @@ class SearchResultFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.searchResultsRecyclerView.visibility = View.GONE } else { diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt index 7ae52879..d680ff90 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt @@ -13,6 +13,7 @@ import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.adapters.EpisodeListAdapter import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment +import dev.jdtech.jellyfin.utils.checkIfLoginRequired import dev.jdtech.jellyfin.viewmodels.SeasonViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -39,6 +40,7 @@ class SeasonFragment : Fragment() { viewModel.error.observe(viewLifecycleOwner, { error -> if (error != null) { + checkIfLoginRequired(error) binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.episodesRecyclerView.visibility = View.GONE } else { diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt index 0f6852d4..726d62a3 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -7,5 +7,6 @@ import java.util.* @Parcelize data class PlayerItem( val itemId: UUID, - val mediaSourceId: String + val mediaSourceId: String, + val playbackPosition: Long ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/View.kt b/app/src/main/java/dev/jdtech/jellyfin/models/View.kt index 7c88dd6b..4f624a7f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/View.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/View.kt @@ -6,5 +6,6 @@ import java.util.* data class View( val id: UUID, val name: String?, - var items: List? = null + var items: List? = null, + val type: String? ) \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index 84ed4781..d99de89b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -10,7 +10,11 @@ interface JellyfinRepository { suspend fun getItem(itemId: UUID): BaseItemDto - suspend fun getItems(parentId: UUID? = null): List + suspend fun getItems( + parentId: UUID? = null, + includeTypes: List? = null, + recursive: Boolean = false + ): List suspend fun getFavoriteItems(): List @@ -28,7 +32,7 @@ interface JellyfinRepository { seriesId: UUID, seasonId: UUID, fields: List? = null, - startIndex: Int? = null + startItemId: UUID? = null ): List suspend fun getMediaSources(itemId: UUID): List @@ -50,4 +54,6 @@ interface JellyfinRepository { suspend fun markAsPlayed(itemId: UUID) suspend fun markAsUnplayed(itemId: UUID) + + suspend fun getIntros(itemId: UUID): List } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 1f393e98..c206fb53 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -25,12 +25,18 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep return item } - override suspend fun getItems(parentId: UUID?): List { + override suspend fun getItems( + parentId: UUID?, + includeTypes: List?, + recursive: Boolean + ): List { val items: List withContext(Dispatchers.IO) { items = jellyfinApi.itemsApi.getItems( jellyfinApi.userId!!, - parentId = parentId + parentId = parentId, + includeItemTypes = includeTypes, + recursive = recursive ).content.items ?: listOf() } return items @@ -109,7 +115,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep seriesId: UUID, seasonId: UUID, fields: List?, - startIndex: Int? + startItemId: UUID? ): List { val episodes: List withContext(Dispatchers.IO) { @@ -118,7 +124,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep jellyfinApi.userId!!, seasonId = seasonId, fields = fields, - startIndex = startIndex + startItemId = startItemId ).content.items ?: listOf() } return episodes @@ -260,4 +266,14 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId) } } + + override suspend fun getIntros(itemId: UUID): List { + val intros: List + withContext(Dispatchers.IO) { + intros = + jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items + ?: listOf() + } + return intros + } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt index fd2e2892..55ec8723 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt @@ -1,11 +1,23 @@ package dev.jdtech.jellyfin.utils +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import dev.jdtech.jellyfin.MainNavigationDirections import dev.jdtech.jellyfin.models.View import org.jellyfin.sdk.model.api.BaseItemDto +import timber.log.Timber fun BaseItemDto.toView(): View { return View( id = id, - name = name + name = name, + type = collectionType ) +} + +fun Fragment.checkIfLoginRequired(error: String) { + if (error.contains("401")) { + Timber.d("Login required!") + findNavController().navigate(MainNavigationDirections.actionGlobalLoginFragment()) + } } \ No newline at end of file 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 baa784df..e09b18a8 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -10,6 +10,8 @@ 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 @@ -61,28 +63,53 @@ constructor( } } - fun preparePlayer() { + fun preparePlayerItems() { _playerItemsError.value = null viewModelScope.launch { try { createPlayerItems(_item.value!!) _navigateToPlayer.value = true } catch (e: Exception) { - _playerItemsError.value = e.message + _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.id, intro.mediaSources?.get(0)?.id!!, 0)) + introsCount += 1 + } + } + val episodes = jellyfinRepository.getEpisodes( startEpisode.seriesId!!, startEpisode.seasonId!!, - startIndex = startEpisode.indexNumber?.minus(1) + startItemId = startEpisode.id, + fields = listOf(ItemFields.MEDIA_SOURCES) ) for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + if (episode.mediaSources.isNullOrEmpty()) continue + if (episode.locationType == LocationType.VIRTUAL) continue + playerItems.add( + PlayerItem( + episode.id, + episode.mediaSources?.get(0)?.id!!, + playbackPosition + ) + ) } + + if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found") } fun markAsPlayed(itemId: UUID) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt index eb339dc7..ee42e62a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt @@ -78,7 +78,7 @@ constructor( _favoriteSections.value = tempFavoriteSections } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } _finishedLoading.value = true } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt index 70807509..0628f27a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -12,7 +12,9 @@ import dev.jdtech.jellyfin.models.HomeSection import dev.jdtech.jellyfin.models.View import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.utils.toView +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.BaseItemDto import timber.log.Timber import java.util.* @@ -50,45 +52,59 @@ constructor( _finishedLoading.value = false viewModelScope.launch { try { + + jellyfinRepository.postCapabilities() - val views: MutableList = mutableListOf() - val userViews = jellyfinRepository.getUserViews() - for (view in userViews) { - Timber.d("Collection type: ${view.collectionType}") - if (view.collectionType == "homevideos" || - view.collectionType == "music" || - view.collectionType == "playlists" || - view.collectionType == "books" - ) continue - val latestItems = jellyfinRepository.getLatestMedia(view.id) - if (latestItems.isEmpty()) continue - val v = view.toView() - v.items = latestItems - views.add(v) - } val items = mutableListOf() - val resumeItems = jellyfinRepository.getResumeItems() - val resumeSection = - HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems) + withContext(Dispatchers.Default) { - if (!resumeItems.isNullOrEmpty()) { - items.add(HomeItem.Section(resumeSection)) + val resumeItems = jellyfinRepository.getResumeItems() + val resumeSection = + HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems) + + if (!resumeItems.isNullOrEmpty()) { + items.add(HomeItem.Section(resumeSection)) + } + + val nextUpItems = jellyfinRepository.getNextUp() + val nextUpSection = HomeSection(UUID.randomUUID(), nextUpString, nextUpItems) + + if (!nextUpItems.isNullOrEmpty()) { + items.add(HomeItem.Section(nextUpSection)) + } } - val nextUpItems = jellyfinRepository.getNextUp() - val nextUpSection = HomeSection(UUID.randomUUID(), nextUpString, nextUpItems) + _views.value = items - if (!nextUpItems.isNullOrEmpty()) { - items.add(HomeItem.Section(nextUpSection)) + val views: MutableList = mutableListOf() + + withContext(Dispatchers.Default) { + val userViews = jellyfinRepository.getUserViews() + + for (view in userViews) { + Timber.d("Collection type: ${view.collectionType}") + if (view.collectionType == "homevideos" || + view.collectionType == "music" || + view.collectionType == "playlists" || + view.collectionType == "books" || + view.collectionType == "livetv" + ) continue + val latestItems = jellyfinRepository.getLatestMedia(view.id) + if (latestItems.isEmpty()) continue + val v = view.toView() + v.items = latestItems + views.add(v) + } } _views.value = items + views.map { HomeItem.ViewItem(it) } + } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } _finishedLoading.value = true } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt index ac0d75e9..3f8ee64f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt @@ -23,15 +23,25 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _error = MutableLiveData() val error: LiveData = _error - fun loadItems(parentId: UUID) { + fun loadItems(parentId: UUID, libraryType: String?) { _error.value = null _finishedLoading.value = false + Timber.d("$libraryType") + val itemType = when (libraryType) { + "movies" -> "Movie" + "tvshows" -> "Series" + else -> null + } viewModelScope.launch { try { - _items.value = jellyfinRepository.getItems(parentId) + _items.value = jellyfinRepository.getItems( + parentId, + includeTypes = if (itemType != null) listOf(itemType) else null, + recursive = true + ) } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } _finishedLoading.value = true } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt index 7722a3e7..0d9a4185 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt @@ -66,7 +66,7 @@ constructor( _navigateToMain.value = true } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } } } 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 ea564c2d..4b0a5485 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -13,7 +13,8 @@ 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.MediaSourceInfo +import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.LocationType import timber.log.Timber import java.util.* import javax.inject.Inject @@ -52,9 +53,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _seasons = MutableLiveData>() val seasons: LiveData> = _seasons - private val _mediaSources = MutableLiveData>() - val mediaSources: LiveData> = _mediaSources - private val _navigateToPlayer = MutableLiveData>() val navigateToPlayer: LiveData> = _navigateToPlayer @@ -91,12 +89,9 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { _nextUp.value = getNextUp(itemId) _seasons.value = jellyfinRepository.getSeasons(itemId) } - if (itemType == "Movie") { - _mediaSources.value = jellyfinRepository.getMediaSources(itemId) - } } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } } } @@ -183,11 +178,11 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } - fun preparePlayer() { + fun preparePlayerItems(mediaSourceIndex: Int? = null) { _playerItemsError.value = null viewModelScope.launch { try { - createPlayerItems(_item.value!!) + createPlayerItems(_item.value!!, mediaSourceIndex) _navigateToPlayer.value = playerItems.toTypedArray() } catch (e: Exception) { _playerItemsError.value = e.message @@ -195,28 +190,78 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } - private suspend fun createPlayerItems(series: BaseItemDto) { - if (nextUp.value != null) { - val startEpisode = nextUp.value!! - val episodes = jellyfinRepository.getEpisodes(startEpisode.seriesId!!, startEpisode.seasonId!!, startIndex = startEpisode.indexNumber?.minus(1)) - for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + 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.id, intro.mediaSources?.get(0)?.id!!, 0)) + introsCount += 1 } - } else { - for (season in seasons.value!!) { - if (season.indexNumber == 0) continue - val episodes = jellyfinRepository.getEpisodes(series.id, season.id) - for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + } + + when (series.type) { + "Movie" -> { + playerItems.add( + PlayerItem( + 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.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.id, + episode.mediaSources?.get(0)?.id!!, + 0 + ) + ) + } + } } } } - } - fun navigateToPlayer(mediaSource: MediaSourceInfo) { - _navigateToPlayer.value = arrayOf(PlayerItem(item.value!!.id, mediaSource.id!!)) + if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found") } fun doneNavigatingToPlayer() { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt index 60e1e1c6..359ee45d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt @@ -44,7 +44,7 @@ constructor( } } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } _finishedLoading.value = true } 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 80e30af7..53d9cfd7 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -39,8 +39,7 @@ constructor( private val sp = PreferenceManager.getDefaultSharedPreferences(application) fun initializePlayer( - items: Array, - playbackPosition: Long + items: Array ) { val renderersFactory = @@ -61,18 +60,22 @@ constructor( viewModelScope.launch { val mediaItems: MutableList = mutableListOf() - for (item in items) { - val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) - Timber.d("Stream url: $streamUrl") - val mediaItem = - MediaItem.Builder() - .setMediaId(item.itemId.toString()) - .setUri(streamUrl) - .build() - mediaItems.add(mediaItem) + try { + for (item in items) { + val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) + Timber.d("Stream url: $streamUrl") + val mediaItem = + MediaItem.Builder() + .setMediaId(item.itemId.toString()) + .setUri(streamUrl) + .build() + mediaItems.add(mediaItem) + } + } catch (e: Exception) { + Timber.e(e) } - player.setMediaItems(mediaItems, currentWindow, playbackPosition) + player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition) player.playWhenReady = playWhenReady player.prepare() _player.value = player @@ -84,10 +87,14 @@ constructor( private fun releasePlayer() { _player.value?.let { player -> runBlocking { - jellyfinRepository.postPlaybackStop( - UUID.fromString(player.currentMediaItem?.mediaId), - player.currentPosition.times(10000) - ) + try { + jellyfinRepository.postPlaybackStop( + UUID.fromString(player.currentMediaItem?.mediaId), + player.currentPosition.times(10000) + ) + } catch (e: Exception) { + Timber.e(e) + } } } @@ -107,11 +114,15 @@ constructor( override fun run() { viewModelScope.launch { if (player.currentMediaItem != null) { - jellyfinRepository.postPlaybackProgress( - UUID.fromString(player.currentMediaItem!!.mediaId), - player.currentPosition.times(10000), - !player.isPlaying - ) + try { + jellyfinRepository.postPlaybackProgress( + UUID.fromString(player.currentMediaItem!!.mediaId), + player.currentPosition.times(10000), + !player.isPlaying + ) + } catch (e: Exception) { + Timber.e(e) + } } } handler.postDelayed(this, 2000) @@ -123,7 +134,11 @@ constructor( override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { Timber.d("Playing MediaItem: ${mediaItem?.mediaId}") viewModelScope.launch { - jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId)) + try { + jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId)) + } catch (e: Exception) { + Timber.e(e) + } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt index b9fc1e47..3a656efd 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt @@ -74,7 +74,7 @@ constructor( _sections.value = tempSections } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } _finishedLoading.value = true } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt index 39f7525c..2be9065c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt @@ -35,7 +35,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { _episodes.value = getEpisodes(seriesId, seasonId) } catch (e: Exception) { Timber.e(e) - _error.value = e.message + _error.value = e.toString() } _finishedLoading.value = true } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 47d6f80c..e41857e6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -21,7 +21,7 @@ - diff --git a/app/src/main/res/layout/collection_item.xml b/app/src/main/res/layout/collection_item.xml index 2261215b..ad5ba921 100644 --- a/app/src/main/res/layout/collection_item.xml +++ b/app/src/main/res/layout/collection_item.xml @@ -24,7 +24,7 @@ android:layout_height="0dp" android:adjustViewBounds="true" android:scaleType="centerCrop" - app:collectionImage="@{collection}" + app:baseItemImage="@{collection}" app:layout_constraintDimensionRatio="H,16:9" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/episode_bottom_sheet.xml b/app/src/main/res/layout/episode_bottom_sheet.xml index 5bb2b42b..ea0cba8c 100644 --- a/app/src/main/res/layout/episode_bottom_sheet.xml +++ b/app/src/main/res/layout/episode_bottom_sheet.xml @@ -5,6 +5,10 @@ + + + + @@ -33,11 +37,32 @@ android:layout_height="85dp" android:layout_marginStart="24dp" android:scaleType="centerCrop" - app:itemPrimaryImage="@{viewModel.item}" + app:baseItemImage="@{viewModel.item}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/holder" app:shapeAppearance="@style/roundedImageView" /> + + + + + + + @@ -24,7 +26,7 @@ android:layout_width="100dp" android:layout_height="100dp" android:scaleType="centerCrop" - app:episodeImage="@{episode}" + app:baseItemImage="@{episode}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -44,6 +46,27 @@ app:layout_constraintEnd_toEndOf="@id/episode_image" app:layout_constraintTop_toTopOf="@id/episode_image" /> + + + + + - + app:layout_constraintTop_toTopOf="parent" /> - + + - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d268baba..6d9f9612 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,7 +51,7 @@ Theme Error preparing player items. View details - @string/view_details + View details About Privacy policy App info diff --git a/build.gradle b/build.gradle index 5f6ed4c6..415aa2e9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.5.21" + ext.kotlin_version = "1.5.30" repositories { google() mavenCentral()