Merge pull request #30 from jarnedemeulemeester/develop

Version 0.1.2
This commit is contained in:
Jarne Demeulemeester 2021-08-26 23:59:20 +02:00 committed by GitHub
commit 905332e097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 419 additions and 215 deletions

View file

@ -16,8 +16,8 @@ android {
applicationId "dev.jdtech.jellyfin" applicationId "dev.jdtech.jellyfin"
minSdkVersion 24 minSdkVersion 24
targetSdkVersion 31 targetSdkVersion 31
versionCode 2 versionCode 3
versionName "0.1.1" versionName "0.1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View file

@ -20,4 +20,4 @@
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep public class dev.jdtech.jellyfin.models.PlayerItem -keepnames class dev.jdtech.jellyfin.models.PlayerItem

View file

@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.models.FavoriteSection import dev.jdtech.jellyfin.models.FavoriteSection
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.ImageType
import java.util.* import java.util.*
@BindingAdapter("servers") @BindingAdapter("servers")
@ -35,11 +36,12 @@ fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
fun bindItemImage(imageView: ImageView, item: BaseItemDto) { fun bindItemImage(imageView: ImageView, item: BaseItemDto) {
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") 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 Glide
.with(imageView.context) .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()) .transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800) .placeholder(R.color.neutral_800)
.into(imageView) .into(imageView)
@ -49,18 +51,17 @@ fun bindItemImage(imageView: ImageView, item: BaseItemDto) {
@BindingAdapter("itemBackdropImage") @BindingAdapter("itemBackdropImage")
fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) { fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) {
if (item != null) { if (item == null) return
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
Glide Glide
.with(imageView.context) .with(imageView.context)
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Backdrop")) .load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/${ImageType.BACKDROP}"))
.transition(DrawableTransitionOptions.withCrossFade()) .transition(DrawableTransitionOptions.withCrossFade())
.into(imageView) .into(imageView)
imageView.contentDescription = "${item.name} backdrop" imageView.contentDescription = "${item.name} backdrop"
} }
}
@BindingAdapter("itemBackdropById") @BindingAdapter("itemBackdropById")
fun bindItemBackdropById(imageView: ImageView, itemId: UUID) { fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
@ -68,7 +69,7 @@ fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
Glide Glide
.with(imageView.context) .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()) .transition(DrawableTransitionOptions.withCrossFade())
.into(imageView) .into(imageView)
} }
@ -79,20 +80,6 @@ fun bindCollections(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
adapter.submitList(data) 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") @BindingAdapter("people")
fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) { fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
val adapter = recyclerView.adapter as PersonListAdapter val adapter = recyclerView.adapter as PersonListAdapter
@ -105,7 +92,7 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
Glide Glide
.with(imageView.context) .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()) .transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800) .placeholder(R.color.neutral_800)
.into(imageView) .into(imageView)
@ -125,15 +112,38 @@ fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
adapter.submitList(data) adapter.submitList(data)
} }
@BindingAdapter("episodeImage") @BindingAdapter("baseItemImage")
fun bindEpisodeImage(imageView: ImageView, episode: BaseItemDto) { fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
if (episode == null) return
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") 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 Glide
.with(imageView.context) .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()) .transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800) .placeholder(R.color.neutral_800)
.into(imageView) .into(imageView)
@ -147,28 +157,12 @@ fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
Glide Glide
.with(imageView.context) .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()) .transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800) .placeholder(R.color.neutral_800)
.into(imageView) .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") @BindingAdapter("favoriteSections")
fun bindFavoriteSections(recyclerView: RecyclerView, data: List<FavoriteSection>?) { fun bindFavoriteSections(recyclerView: RecyclerView, data: List<FavoriteSection>?) {
val adapter = recyclerView.adapter as FavoritesListAdapter val adapter = recyclerView.adapter as FavoritesListAdapter

View file

@ -38,7 +38,7 @@ class PlayerActivity : AppCompatActivity() {
}) })
if (viewModel.player.value == null) { if (viewModel.player.value == null) {
viewModel.initializePlayer(args.items, args.playbackPosition) viewModel.initializePlayer(args.items)
} }
hideSystemUI() hideSystemUI()
} }

View file

@ -1,14 +1,11 @@
package dev.jdtech.jellyfin.database package dev.jdtech.jellyfin.database
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.*
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
@Dao @Dao
interface ServerDatabaseDao { interface ServerDatabaseDao {
@Insert @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(server: Server) fun insert(server: Server)
@Update @Update

View file

@ -11,12 +11,12 @@ class VideoVersionDialogFragment(
private val viewModel: MediaInfoViewModel private val viewModel: MediaInfoViewModel
) : DialogFragment() { ) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 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 { return activity?.let {
val builder = AlertDialog.Builder(it) val builder = AlertDialog.Builder(it)
builder.setTitle("Select a version") builder.setTitle("Select a version")
.setItems(items.toTypedArray()) { _, which -> .setItems(items?.toTypedArray()) { _, which ->
viewModel.navigateToPlayer(viewModel.mediaSources.value!![which]) viewModel.preparePlayerItems(which)
} }
builder.create() builder.create()
} ?: throw IllegalStateException("Activity cannot be null") } ?: throw IllegalStateException("Activity cannot be null")

View file

@ -37,7 +37,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.playButton.setOnClickListener { binding.playButton.setOnClickListener {
binding.playButton.setImageResource(android.R.color.transparent) binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.visibility = View.VISIBLE binding.progressCircular.visibility = View.VISIBLE
viewModel.preparePlayer() viewModel.preparePlayerItems()
} }
binding.checkButton.setOnClickListener { binding.checkButton.setOnClickListener {
@ -91,10 +91,14 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
if (it) { if (it) {
navigateToPlayerActivity( navigateToPlayerActivity(
viewModel.playerItems.toTypedArray(), viewModel.playerItems.toTypedArray(),
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
) )
viewModel.doneNavigateToPlayer() 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 binding.progressCircular.visibility = View.INVISIBLE
} }
}) })
@ -102,7 +106,12 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage ->
if (errorMessage != null) { if (errorMessage != null) {
binding.playerItemsError.visibility = View.VISIBLE 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 binding.progressCircular.visibility = View.INVISIBLE
} else { } else {
binding.playerItemsError.visibility = View.GONE binding.playerItemsError.visibility = View.GONE
@ -110,7 +119,9 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}) })
binding.playerItemsErrorDetails.setOnClickListener { 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) viewModel.loadEpisode(args.episodeId)
@ -120,12 +131,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
private fun navigateToPlayerActivity( private fun navigateToPlayerActivity(
playerItems: Array<PlayerItem>, playerItems: Array<PlayerItem>,
playbackPosition: Long
) { ) {
findNavController().navigate( findNavController().navigate(
EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity( EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity(
playerItems, playerItems,
playbackPosition
) )
) )
} }

View file

@ -14,6 +14,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -44,6 +45,7 @@ class FavoriteFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.favoritesRecyclerView.visibility = View.GONE binding.favoritesRecyclerView.visibility = View.GONE
} else { } else {

View file

@ -12,6 +12,7 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.adapters.ViewListAdapter import dev.jdtech.jellyfin.adapters.ViewListAdapter
import dev.jdtech.jellyfin.databinding.FragmentHomeBinding import dev.jdtech.jellyfin.databinding.FragmentHomeBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.HomeViewModel import dev.jdtech.jellyfin.viewmodels.HomeViewModel
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -73,6 +74,7 @@ class HomeFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.viewsRecyclerView.visibility = View.GONE binding.viewsRecyclerView.visibility = View.GONE
} else { } else {
@ -86,7 +88,10 @@ class HomeFragment : Fragment() {
} }
binding.errorLayout.errorDetailsButton.setOnClickListener { 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 return binding.root
@ -96,7 +101,8 @@ class HomeFragment : Fragment() {
findNavController().navigate( findNavController().navigate(
HomeFragmentDirections.actionNavigationHomeToLibraryFragment( HomeFragmentDirections.actionNavigationHomeToLibraryFragment(
view.id, view.id,
view.name view.name,
view.type
) )
) )
} }

View file

@ -14,6 +14,7 @@ import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@AndroidEntryPoint @AndroidEntryPoint
@ -41,6 +42,7 @@ class LibraryFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.itemsRecyclerView.visibility = View.GONE binding.itemsRecyclerView.visibility = View.GONE
} else { } else {
@ -50,7 +52,7 @@ class LibraryFragment : Fragment() {
}) })
binding.errorLayout.errorRetryButton.setOnClickListener { binding.errorLayout.errorRetryButton.setOnClickListener {
viewModel.loadItems(args.libraryId) viewModel.loadItems(args.libraryId, args.libraryType)
} }
binding.errorLayout.errorDetailsButton.setOnClickListener { binding.errorLayout.errorDetailsButton.setOnClickListener {
@ -65,7 +67,7 @@ class LibraryFragment : Fragment() {
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item -> ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item) navigateToMediaInfoFragment(item)
}) })
viewModel.loadItems(args.libraryId) viewModel.loadItems(args.libraryId, args.libraryType)
} }
private fun navigateToMediaInfoFragment(item: BaseItemDto) { private fun navigateToMediaInfoFragment(item: BaseItemDto) {

View file

@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.CollectionListAdapter import dev.jdtech.jellyfin.adapters.CollectionListAdapter
import dev.jdtech.jellyfin.databinding.FragmentMediaBinding import dev.jdtech.jellyfin.databinding.FragmentMediaBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.MediaViewModel import dev.jdtech.jellyfin.viewmodels.MediaViewModel
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -66,6 +67,7 @@ class MediaFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.viewsRecyclerView.visibility = View.GONE binding.viewsRecyclerView.visibility = View.GONE
} else { } else {
@ -89,7 +91,8 @@ class MediaFragment : Fragment() {
findNavController().navigate( findNavController().navigate(
MediaFragmentDirections.actionNavigationMediaToLibraryFragment( MediaFragmentDirections.actionNavigationMediaToLibraryFragment(
library.id, library.id,
library.name library.name,
library.collectionType,
) )
) )
} }

View file

@ -19,6 +19,7 @@ import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -48,6 +49,7 @@ class MediaInfoFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.mediaInfoScrollview.visibility = View.GONE binding.mediaInfoScrollview.visibility = View.GONE
} else { } else {
@ -61,7 +63,10 @@ class MediaInfoFragment : Fragment() {
} }
binding.errorLayout.errorDetailsButton.setOnClickListener { 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 -> viewModel.item.observe(viewLifecycleOwner, { item ->
@ -89,8 +94,7 @@ class MediaInfoFragment : Fragment() {
viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems -> viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems ->
if (playerItems != null) { if (playerItems != null) {
navigateToPlayerActivity( navigateToPlayerActivity(
playerItems, playerItems
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
) )
viewModel.doneNavigatingToPlayer() viewModel.doneNavigatingToPlayer()
binding.playButton.setImageDrawable( binding.playButton.setImageDrawable(
@ -137,7 +141,9 @@ class MediaInfoFragment : Fragment() {
}) })
binding.playerItemsErrorDetails.setOnClickListener { 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 { binding.trailerButton.setOnClickListener {
@ -162,33 +168,18 @@ class MediaInfoFragment : Fragment() {
binding.playButton.setImageResource(android.R.color.transparent) binding.playButton.setImageResource(android.R.color.transparent)
binding.progressCircular.visibility = View.VISIBLE binding.progressCircular.visibility = View.VISIBLE
if (args.itemType == "Movie") { if (args.itemType == "Movie") {
if (!viewModel.mediaSources.value.isNullOrEmpty()) { if (viewModel.item.value?.mediaSources != null) {
if (viewModel.mediaSources.value!!.size > 1) { if (viewModel.item.value?.mediaSources?.size!! > 1) {
VideoVersionDialogFragment(viewModel).show( VideoVersionDialogFragment(viewModel).show(
parentFragmentManager, parentFragmentManager,
"videoversiondialog" "videoversiondialog"
) )
} else { } else {
navigateToPlayerActivity( viewModel.preparePlayerItems()
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
} }
} }
} else if (args.itemType == "Series") { } else if (args.itemType == "Series") {
viewModel.preparePlayer() viewModel.preparePlayerItems()
} }
} }
@ -230,12 +221,10 @@ class MediaInfoFragment : Fragment() {
private fun navigateToPlayerActivity( private fun navigateToPlayerActivity(
playerItems: Array<PlayerItem>, playerItems: Array<PlayerItem>,
playbackPosition: Long,
) { ) {
findNavController().navigate( findNavController().navigate(
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity( MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
playerItems, playerItems,
playbackPosition
) )
) )
} }

View file

@ -15,6 +15,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -47,6 +48,7 @@ class SearchResultFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.searchResultsRecyclerView.visibility = View.GONE binding.searchResultsRecyclerView.visibility = View.GONE
} else { } else {

View file

@ -13,6 +13,7 @@ import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -39,6 +40,7 @@ class SeasonFragment : Fragment() {
viewModel.error.observe(viewLifecycleOwner, { error -> viewModel.error.observe(viewLifecycleOwner, { error ->
if (error != null) { if (error != null) {
checkIfLoginRequired(error)
binding.errorLayout.errorPanel.visibility = View.VISIBLE binding.errorLayout.errorPanel.visibility = View.VISIBLE
binding.episodesRecyclerView.visibility = View.GONE binding.episodesRecyclerView.visibility = View.GONE
} else { } else {

View file

@ -7,5 +7,6 @@ import java.util.*
@Parcelize @Parcelize
data class PlayerItem( data class PlayerItem(
val itemId: UUID, val itemId: UUID,
val mediaSourceId: String val mediaSourceId: String,
val playbackPosition: Long
) : Parcelable ) : Parcelable

View file

@ -6,5 +6,6 @@ import java.util.*
data class View( data class View(
val id: UUID, val id: UUID,
val name: String?, val name: String?,
var items: List<BaseItemDto>? = null var items: List<BaseItemDto>? = null,
val type: String?
) )

View file

@ -10,7 +10,11 @@ interface JellyfinRepository {
suspend fun getItem(itemId: UUID): BaseItemDto suspend fun getItem(itemId: UUID): BaseItemDto
suspend fun getItems(parentId: UUID? = null): List<BaseItemDto> suspend fun getItems(
parentId: UUID? = null,
includeTypes: List<String>? = null,
recursive: Boolean = false
): List<BaseItemDto>
suspend fun getFavoriteItems(): List<BaseItemDto> suspend fun getFavoriteItems(): List<BaseItemDto>
@ -28,7 +32,7 @@ interface JellyfinRepository {
seriesId: UUID, seriesId: UUID,
seasonId: UUID, seasonId: UUID,
fields: List<ItemFields>? = null, fields: List<ItemFields>? = null,
startIndex: Int? = null startItemId: UUID? = null
): List<BaseItemDto> ): List<BaseItemDto>
suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo> suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo>
@ -50,4 +54,6 @@ interface JellyfinRepository {
suspend fun markAsPlayed(itemId: UUID) suspend fun markAsPlayed(itemId: UUID)
suspend fun markAsUnplayed(itemId: UUID) suspend fun markAsUnplayed(itemId: UUID)
suspend fun getIntros(itemId: UUID): List<BaseItemDto>
} }

View file

@ -25,12 +25,18 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
return item return item
} }
override suspend fun getItems(parentId: UUID?): List<BaseItemDto> { override suspend fun getItems(
parentId: UUID?,
includeTypes: List<String>?,
recursive: Boolean
): List<BaseItemDto> {
val items: List<BaseItemDto> val items: List<BaseItemDto>
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
items = jellyfinApi.itemsApi.getItems( items = jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
parentId = parentId parentId = parentId,
includeItemTypes = includeTypes,
recursive = recursive
).content.items ?: listOf() ).content.items ?: listOf()
} }
return items return items
@ -109,7 +115,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
seriesId: UUID, seriesId: UUID,
seasonId: UUID, seasonId: UUID,
fields: List<ItemFields>?, fields: List<ItemFields>?,
startIndex: Int? startItemId: UUID?
): List<BaseItemDto> { ): List<BaseItemDto> {
val episodes: List<BaseItemDto> val episodes: List<BaseItemDto>
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -118,7 +124,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.userId!!, jellyfinApi.userId!!,
seasonId = seasonId, seasonId = seasonId,
fields = fields, fields = fields,
startIndex = startIndex startItemId = startItemId
).content.items ?: listOf() ).content.items ?: listOf()
} }
return episodes return episodes
@ -260,4 +266,14 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId) jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId)
} }
} }
override suspend fun getIntros(itemId: UUID): List<BaseItemDto> {
val intros: List<BaseItemDto>
withContext(Dispatchers.IO) {
intros =
jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items
?: listOf()
}
return intros
}
} }

View file

@ -1,11 +1,23 @@
package dev.jdtech.jellyfin.utils 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 dev.jdtech.jellyfin.models.View
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber
fun BaseItemDto.toView(): View { fun BaseItemDto.toView(): View {
return View( return View(
id = id, 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())
}
}

View file

@ -10,6 +10,8 @@ import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto 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 timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.time.ZoneOffset import java.time.ZoneOffset
@ -61,28 +63,53 @@ constructor(
} }
} }
fun preparePlayer() { fun preparePlayerItems() {
_playerItemsError.value = null _playerItemsError.value = null
viewModelScope.launch { viewModelScope.launch {
try { try {
createPlayerItems(_item.value!!) createPlayerItems(_item.value!!)
_navigateToPlayer.value = true _navigateToPlayer.value = true
} catch (e: Exception) { } catch (e: Exception) {
_playerItemsError.value = e.message _playerItemsError.value = e.toString()
} }
} }
} }
private suspend fun createPlayerItems(startEpisode: BaseItemDto) { 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( val episodes = jellyfinRepository.getEpisodes(
startEpisode.seriesId!!, startEpisode.seriesId!!,
startEpisode.seasonId!!, startEpisode.seasonId!!,
startIndex = startEpisode.indexNumber?.minus(1) startItemId = startEpisode.id,
fields = listOf(ItemFields.MEDIA_SOURCES)
) )
for (episode in episodes) { for (episode in episodes) {
val mediaSources = jellyfinRepository.getMediaSources(episode.id) if (episode.mediaSources.isNullOrEmpty()) continue
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) 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) { fun markAsPlayed(itemId: UUID) {

View file

@ -78,7 +78,7 @@ constructor(
_favoriteSections.value = tempFavoriteSections _favoriteSections.value = tempFavoriteSections
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
_finishedLoading.value = true _finishedLoading.value = true
} }

View file

@ -12,7 +12,9 @@ import dev.jdtech.jellyfin.models.HomeSection
import dev.jdtech.jellyfin.models.View import dev.jdtech.jellyfin.models.View
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.toView import dev.jdtech.jellyfin.utils.toView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -50,25 +52,14 @@ constructor(
_finishedLoading.value = false _finishedLoading.value = false
viewModelScope.launch { viewModelScope.launch {
try { try {
jellyfinRepository.postCapabilities() jellyfinRepository.postCapabilities()
val views: MutableList<View> = 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<HomeItem>() val items = mutableListOf<HomeItem>()
withContext(Dispatchers.Default) {
val resumeItems = jellyfinRepository.getResumeItems() val resumeItems = jellyfinRepository.getResumeItems()
val resumeSection = val resumeSection =
HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems) HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems)
@ -83,12 +74,37 @@ constructor(
if (!nextUpItems.isNullOrEmpty()) { if (!nextUpItems.isNullOrEmpty()) {
items.add(HomeItem.Section(nextUpSection)) items.add(HomeItem.Section(nextUpSection))
} }
}
_views.value = items
val views: MutableList<View> = 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) } _views.value = items + views.map { HomeItem.ViewItem(it) }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
_finishedLoading.value = true _finishedLoading.value = true
} }

View file

@ -23,15 +23,25 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _error = MutableLiveData<String>() private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error val error: LiveData<String> = _error
fun loadItems(parentId: UUID) { fun loadItems(parentId: UUID, libraryType: String?) {
_error.value = null _error.value = null
_finishedLoading.value = false _finishedLoading.value = false
Timber.d("$libraryType")
val itemType = when (libraryType) {
"movies" -> "Movie"
"tvshows" -> "Series"
else -> null
}
viewModelScope.launch { viewModelScope.launch {
try { try {
_items.value = jellyfinRepository.getItems(parentId) _items.value = jellyfinRepository.getItems(
parentId,
includeTypes = if (itemType != null) listOf(itemType) else null,
recursive = true
)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
_finishedLoading.value = true _finishedLoading.value = true
} }

View file

@ -66,7 +66,7 @@ constructor(
_navigateToMain.value = true _navigateToMain.value = true
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
} }
} }

View file

@ -13,7 +13,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemPerson 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 timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -52,9 +53,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _seasons = MutableLiveData<List<BaseItemDto>>() private val _seasons = MutableLiveData<List<BaseItemDto>>()
val seasons: LiveData<List<BaseItemDto>> = _seasons val seasons: LiveData<List<BaseItemDto>> = _seasons
private val _mediaSources = MutableLiveData<List<MediaSourceInfo>>()
val mediaSources: LiveData<List<MediaSourceInfo>> = _mediaSources
private val _navigateToPlayer = MutableLiveData<Array<PlayerItem>>() private val _navigateToPlayer = MutableLiveData<Array<PlayerItem>>()
val navigateToPlayer: LiveData<Array<PlayerItem>> = _navigateToPlayer val navigateToPlayer: LiveData<Array<PlayerItem>> = _navigateToPlayer
@ -91,12 +89,9 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
_nextUp.value = getNextUp(itemId) _nextUp.value = getNextUp(itemId)
_seasons.value = jellyfinRepository.getSeasons(itemId) _seasons.value = jellyfinRepository.getSeasons(itemId)
} }
if (itemType == "Movie") {
_mediaSources.value = jellyfinRepository.getMediaSources(itemId)
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) 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 _playerItemsError.value = null
viewModelScope.launch { viewModelScope.launch {
try { try {
createPlayerItems(_item.value!!) createPlayerItems(_item.value!!, mediaSourceIndex)
_navigateToPlayer.value = playerItems.toTypedArray() _navigateToPlayer.value = playerItems.toTypedArray()
} catch (e: Exception) { } catch (e: Exception) {
_playerItemsError.value = e.message _playerItemsError.value = e.message
@ -195,28 +190,78 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
} }
} }
private suspend fun createPlayerItems(series: BaseItemDto) { 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
}
}
when (series.type) {
"Movie" -> {
playerItems.add(
PlayerItem(
series.id,
series.mediaSources?.get(mediaSourceIndex ?: 0)?.id!!,
playbackPosition
)
)
}
"Series" -> {
if (nextUp.value != null) { if (nextUp.value != null) {
val startEpisode = nextUp.value!! val startEpisode = nextUp.value!!
val episodes = jellyfinRepository.getEpisodes(startEpisode.seriesId!!, startEpisode.seasonId!!, startIndex = startEpisode.indexNumber?.minus(1)) val episodes = jellyfinRepository.getEpisodes(
startEpisode.seriesId!!,
startEpisode.seasonId!!,
startItemId = startEpisode.id,
fields = listOf(ItemFields.MEDIA_SOURCES)
)
for (episode in episodes) { for (episode in episodes) {
val mediaSources = jellyfinRepository.getMediaSources(episode.id) if (episode.mediaSources.isNullOrEmpty()) continue
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add(
PlayerItem(
episode.id,
episode.mediaSources?.get(0)?.id!!,
0
)
)
} }
} else { } else {
for (season in seasons.value!!) { for (season in seasons.value!!) {
if (season.indexNumber == 0) continue if (season.indexNumber == 0) continue
val episodes = jellyfinRepository.getEpisodes(series.id, season.id) val episodes = jellyfinRepository.getEpisodes(
series.id,
season.id,
fields = listOf(ItemFields.MEDIA_SOURCES)
)
for (episode in episodes) { for (episode in episodes) {
val mediaSources = jellyfinRepository.getMediaSources(episode.id) if (episode.mediaSources.isNullOrEmpty()) continue
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add(
PlayerItem(
episode.id,
episode.mediaSources?.get(0)?.id!!,
0
)
)
}
} }
} }
} }
} }
fun navigateToPlayer(mediaSource: MediaSourceInfo) { if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found")
_navigateToPlayer.value = arrayOf(PlayerItem(item.value!!.id, mediaSource.id!!))
} }
fun doneNavigatingToPlayer() { fun doneNavigatingToPlayer() {

View file

@ -44,7 +44,7 @@ constructor(
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
_finishedLoading.value = true _finishedLoading.value = true
} }

View file

@ -39,8 +39,7 @@ constructor(
private val sp = PreferenceManager.getDefaultSharedPreferences(application) private val sp = PreferenceManager.getDefaultSharedPreferences(application)
fun initializePlayer( fun initializePlayer(
items: Array<PlayerItem>, items: Array<PlayerItem>
playbackPosition: Long
) { ) {
val renderersFactory = val renderersFactory =
@ -61,6 +60,7 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
val mediaItems: MutableList<MediaItem> = mutableListOf() val mediaItems: MutableList<MediaItem> = mutableListOf()
try {
for (item in items) { for (item in items) {
val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId)
Timber.d("Stream url: $streamUrl") Timber.d("Stream url: $streamUrl")
@ -71,8 +71,11 @@ constructor(
.build() .build()
mediaItems.add(mediaItem) 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.playWhenReady = playWhenReady
player.prepare() player.prepare()
_player.value = player _player.value = player
@ -84,10 +87,14 @@ constructor(
private fun releasePlayer() { private fun releasePlayer() {
_player.value?.let { player -> _player.value?.let { player ->
runBlocking { runBlocking {
try {
jellyfinRepository.postPlaybackStop( jellyfinRepository.postPlaybackStop(
UUID.fromString(player.currentMediaItem?.mediaId), UUID.fromString(player.currentMediaItem?.mediaId),
player.currentPosition.times(10000) player.currentPosition.times(10000)
) )
} catch (e: Exception) {
Timber.e(e)
}
} }
} }
@ -107,11 +114,15 @@ constructor(
override fun run() { override fun run() {
viewModelScope.launch { viewModelScope.launch {
if (player.currentMediaItem != null) { if (player.currentMediaItem != null) {
try {
jellyfinRepository.postPlaybackProgress( jellyfinRepository.postPlaybackProgress(
UUID.fromString(player.currentMediaItem!!.mediaId), UUID.fromString(player.currentMediaItem!!.mediaId),
player.currentPosition.times(10000), player.currentPosition.times(10000),
!player.isPlaying !player.isPlaying
) )
} catch (e: Exception) {
Timber.e(e)
}
} }
} }
handler.postDelayed(this, 2000) handler.postDelayed(this, 2000)
@ -123,7 +134,11 @@ constructor(
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}") Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
viewModelScope.launch { viewModelScope.launch {
try {
jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId)) jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId))
} catch (e: Exception) {
Timber.e(e)
}
} }
} }

View file

@ -74,7 +74,7 @@ constructor(
_sections.value = tempSections _sections.value = tempSections
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
_finishedLoading.value = true _finishedLoading.value = true
} }

View file

@ -35,7 +35,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
_episodes.value = getEpisodes(seriesId, seasonId) _episodes.value = getEpisodes(seriesId, seasonId)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
_error.value = e.message _error.value = e.toString()
} }
_finishedLoading.value = true _finishedLoading.value = true
} }

View file

@ -21,7 +21,7 @@
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main" android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:defaultNavHost="true" app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view" app:layout_constraintBottom_toTopOf="@id/nav_view"
@ -32,7 +32,7 @@
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_layout" android:id="@+id/main_toolbar_layout"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main" app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main"
@ -47,6 +47,5 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View file

@ -24,7 +24,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:collectionImage="@{collection}" app:baseItemImage="@{collection}"
app:layout_constraintDimensionRatio="H,16:9" app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -5,6 +5,10 @@
<data> <data>
<import type="android.view.View" />
<import type="org.jellyfin.sdk.model.api.LocationType" />
<variable <variable
name="viewModel" name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel" /> type="dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel" />
@ -33,11 +37,32 @@
android:layout_height="85dp" android:layout_height="85dp"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:itemPrimaryImage="@{viewModel.item}" app:baseItemImage="@{viewModel.item}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/holder" app:layout_constraintTop_toBottomOf="@id/holder"
app:shapeAppearance="@style/roundedImageView" /> app:shapeAppearance="@style/roundedImageView" />
<FrameLayout
android:id="@+id/missing_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circle_background"
android:backgroundTint="@color/red"
android:visibility="@{viewModel.item.locationType == LocationType.VIRTUAL ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="@id/episode_image"
app:layout_constraintTop_toTopOf="@id/episode_image">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="M"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/progress_bar" android:id="@+id/progress_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -7,6 +7,8 @@
<import type="android.view.View" /> <import type="android.view.View" />
<import type="org.jellyfin.sdk.model.api.LocationType" />
<variable <variable
name="episode" name="episode"
type="org.jellyfin.sdk.model.api.BaseItemDto" /> type="org.jellyfin.sdk.model.api.BaseItemDto" />
@ -24,7 +26,7 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:episodeImage="@{episode}" app:baseItemImage="@{episode}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -44,6 +46,27 @@
app:layout_constraintEnd_toEndOf="@id/episode_image" app:layout_constraintEnd_toEndOf="@id/episode_image"
app:layout_constraintTop_toTopOf="@id/episode_image" /> app:layout_constraintTop_toTopOf="@id/episode_image" />
<FrameLayout
android:id="@+id/missing_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circle_background"
android:backgroundTint="@color/red"
android:visibility="@{episode.locationType == LocationType.VIRTUAL ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toStartOf="@id/played_icon"
app:layout_constraintTop_toTopOf="@id/episode_image">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="M"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/progress_bar" android:id="@+id/progress_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -74,8 +97,8 @@
android:id="@+id/episode_desc" android:id="@+id/episode_desc"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:text="@{episode.overview}"
android:scrollbars="none" android:scrollbars="none"
android:text="@{episode.overview}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -16,18 +16,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".fragments.HomeFragment"> tools:context=".fragments.HomeFragment">
<com.google.android.material.progressindicator.CircularProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator" android:id="@+id/loading_indicator"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
app:trackCornerRadius="10dp" />
<include android:id="@+id/error_layout" layout="@layout/error_panel" /> <include
android:id="@+id/error_layout"
layout="@layout/error_panel" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/views_recycler_view" android:id="@+id/views_recycler_view"

View file

@ -358,7 +358,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:itemPrimaryImage="@{viewModel.nextUp}" app:baseItemImage="@{viewModel.nextUp}"
app:layout_constraintDimensionRatio="H,16:9" app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -22,7 +22,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:episodeImage="@{episode}" app:baseItemImage="@{episode}"
app:layout_constraintDimensionRatio="H,16:9" app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -32,7 +32,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:layoutAnimation="@anim/overview_media_animation"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="12dp" android:paddingHorizontal="12dp"
app:homeEpisodes="@{section.items}" app:homeEpisodes="@{section.items}"

View file

@ -42,7 +42,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clipToPadding="false" android:clipToPadding="false"
android:layoutAnimation="@anim/overview_media_animation"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="12dp" android:paddingHorizontal="12dp"
app:items="@{view.items}" app:items="@{view.items}"

View file

@ -84,6 +84,10 @@
app:exitAnim="@anim/nav_default_exit_anim" app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" /> app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<argument
android:name="libraryType"
app:argType="string"
app:nullable="true" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/mediaInfoFragment" android:id="@+id/mediaInfoFragment"
@ -156,9 +160,6 @@
<argument <argument
android:name="items" android:name="items"
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" /> app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
<argument
android:name="playbackPosition"
app:argType="long" />
</activity> </activity>
<fragment <fragment
android:id="@+id/favoriteFragment" android:id="@+id/favoriteFragment"
@ -239,5 +240,8 @@
</fragment> </fragment>
<include app:graph="@navigation/aboutlibs_navigation" /> <include app:graph="@navigation/aboutlibs_navigation" />
<action
android:id="@+id/action_global_loginFragment"
app:destination="@id/loginFragment" />
</navigation> </navigation>

View file

@ -51,7 +51,7 @@
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="error_preparing_player_items">Error preparing player items.</string> <string name="error_preparing_player_items">Error preparing player items.</string>
<string name="view_details">View details</string> <string name="view_details">View details</string>
<string name="view_details_underlined"><u>@string/view_details</u></string> <string name="view_details_underlined"><u>View details</u></string>
<string name="about">About</string> <string name="about">About</string>
<string name="privacy_policy">Privacy policy</string> <string name="privacy_policy">Privacy policy</string>
<string name="app_info">App info</string> <string name="app_info">App info</string>

View file

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.5.21" ext.kotlin_version = "1.5.30"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()