From 59d687f5a07a3748df963c9aaba39d098ed435c6 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Mon, 23 Aug 2021 11:12:56 +0200 Subject: [PATCH 01/16] Remove livetv section from home screen --- .../main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..6cd070bc 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -58,7 +58,8 @@ constructor( if (view.collectionType == "homevideos" || view.collectionType == "music" || view.collectionType == "playlists" || - view.collectionType == "books" + view.collectionType == "books" || + view.collectionType == "livetv" ) continue val latestItems = jellyfinRepository.getLatestMedia(view.id) if (latestItems.isEmpty()) continue From 45fc40b51e9ae67a8a03906e632dc0b179f7eebb Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Mon, 23 Aug 2021 11:14:34 +0200 Subject: [PATCH 02/16] Fix if media items are in folders --- .../dev/jdtech/jellyfin/fragments/HomeFragment.kt | 3 ++- .../jdtech/jellyfin/fragments/LibraryFragment.kt | 4 ++-- .../dev/jdtech/jellyfin/fragments/MediaFragment.kt | 3 ++- .../main/java/dev/jdtech/jellyfin/models/View.kt | 3 ++- .../jellyfin/repository/JellyfinRepository.kt | 6 +++++- .../jellyfin/repository/JellyfinRepositoryImpl.kt | 10 ++++++++-- .../java/dev/jdtech/jellyfin/utils/extensions.kt | 3 ++- .../jdtech/jellyfin/viewmodels/LibraryViewModel.kt | 14 ++++++++++++-- app/src/main/res/navigation/main_navigation.xml | 4 ++++ 9 files changed, 39 insertions(+), 11 deletions(-) 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..545a6d04 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt @@ -96,7 +96,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..24399079 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt @@ -50,7 +50,7 @@ class LibraryFragment : Fragment() { }) binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.loadItems(args.libraryId) + viewModel.loadItems(args.libraryId, args.libraryType) } binding.errorLayout.errorDetailsButton.setOnClickListener { @@ -65,7 +65,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..fea8b358 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt @@ -89,7 +89,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/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..69bf92ea 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 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..393b87ef 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 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..c68d73b0 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt @@ -6,6 +6,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto fun BaseItemDto.toView(): View { return View( id = id, - name = name + name = name, + type = collectionType ) } \ No newline at end of file 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..f6b58c44 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt @@ -23,12 +23,22 @@ 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 diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 2807ccd7..b7928333 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -84,6 +84,10 @@ app:exitAnim="@anim/nav_default_exit_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popExitAnim="@anim/nav_default_pop_exit_anim" /> + Date: Mon, 23 Aug 2021 12:28:57 +0200 Subject: [PATCH 03/16] Fix underlined "View details" string --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e621032ab22847449b7417745cd3e8d96d2e4932 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Mon, 23 Aug 2021 16:42:00 +0200 Subject: [PATCH 04/16] Show complete error message --- .../jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt | 2 +- .../java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt | 2 +- .../main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt | 2 +- .../java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt | 2 +- .../main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt | 2 +- .../java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt | 2 +- .../main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt | 2 +- .../dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt | 2 +- .../main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) 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..c86cb355 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -68,7 +68,7 @@ constructor( createPlayerItems(_item.value!!) _navigateToPlayer.value = true } catch (e: Exception) { - _playerItemsError.value = e.message + _playerItemsError.value = e.toString() } } } 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 6cd070bc..3ba945ad 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -89,7 +89,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/LibraryViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt index f6b58c44..3f8ee64f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt @@ -41,7 +41,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { ) } 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..5a8695ec 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -96,7 +96,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } 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/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/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 } From 1c6c8640b05ba9bc0e173545c8c075a321d36de1 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Tue, 24 Aug 2021 18:01:52 +0200 Subject: [PATCH 05/16] Ask for login if server responds with 401 --- .../dev/jdtech/jellyfin/database/ServerDatabaseDao.kt | 7 ++----- .../dev/jdtech/jellyfin/fragments/FavoriteFragment.kt | 2 ++ .../dev/jdtech/jellyfin/fragments/HomeFragment.kt | 7 ++++++- .../dev/jdtech/jellyfin/fragments/LibraryFragment.kt | 2 ++ .../dev/jdtech/jellyfin/fragments/MediaFragment.kt | 2 ++ .../jdtech/jellyfin/fragments/MediaInfoFragment.kt | 2 ++ .../jdtech/jellyfin/fragments/SearchResultFragment.kt | 2 ++ .../dev/jdtech/jellyfin/fragments/SeasonFragment.kt | 2 ++ .../main/java/dev/jdtech/jellyfin/utils/extensions.kt | 11 +++++++++++ app/src/main/res/navigation/main_navigation.xml | 3 +++ 10 files changed, 34 insertions(+), 6 deletions(-) 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/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 545a6d04..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 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 24399079..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 { 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 fea8b358..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 { 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..87df83de 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 { 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/utils/extensions.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/extensions.kt index c68d73b0..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,7 +1,11 @@ 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( @@ -9,4 +13,11 @@ fun BaseItemDto.toView(): View { 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/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index b7928333..559933c0 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -243,5 +243,8 @@ + \ No newline at end of file From 78117aee3e6c1291ab092fa9b55125e5d6390043 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Tue, 24 Aug 2021 20:10:30 +0200 Subject: [PATCH 06/16] Upgrade kotlin to 1.5.30 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From 2ff9239e8cb364b88406f1ef188deecbd6642192 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Tue, 24 Aug 2021 20:26:57 +0200 Subject: [PATCH 07/16] Improve home loading --- .../jellyfin/viewmodels/HomeViewModel.kt | 65 ++++++++++++------- app/src/main/res/layout/activity_main.xml | 5 +- app/src/main/res/layout/fragment_home.xml | 12 ++-- app/src/main/res/layout/next_up_section.xml | 1 - app/src/main/res/layout/view_item.xml | 1 - 5 files changed, 48 insertions(+), 36 deletions(-) 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 3ba945ad..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,43 +52,56 @@ 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" || - view.collectionType == "livetv" - ) 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.toString() 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/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 63f600f6..f0e22609 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -16,18 +16,18 @@ android:layout_height="match_parent" tools:context=".fragments.HomeFragment"> - + app:layout_constraintTop_toTopOf="parent" /> - + Date: Wed, 25 Aug 2021 11:00:58 +0200 Subject: [PATCH 08/16] Handle empty mediaSources --- .../jellyfin/fragments/EpisodeBottomSheetFragment.kt | 2 +- .../jdtech/jellyfin/fragments/MediaInfoFragment.kt | 2 +- .../jdtech/jellyfin/repository/JellyfinRepository.kt | 2 +- .../jellyfin/repository/JellyfinRepositoryImpl.kt | 4 ++-- .../viewmodels/EpisodeBottomSheetViewModel.kt | 6 ++++-- .../jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt | 11 +++++++++-- 6 files changed, 18 insertions(+), 9 deletions(-) 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..a6535000 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 { 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 87df83de..b02f3991 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -190,7 +190,7 @@ class MediaInfoFragment : Fragment() { } } } else if (args.itemType == "Series") { - viewModel.preparePlayer() + viewModel.preparePlayerItems() } } 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 69bf92ea..526d0745 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -32,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 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 393b87ef..4dc70436 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -115,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) { @@ -124,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 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 c86cb355..383deb1d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -61,7 +61,7 @@ constructor( } } - fun preparePlayer() { + fun preparePlayerItems() { _playerItemsError.value = null viewModelScope.launch { try { @@ -77,12 +77,14 @@ constructor( val episodes = jellyfinRepository.getEpisodes( startEpisode.seriesId!!, startEpisode.seasonId!!, - startIndex = startEpisode.indexNumber?.minus(1) + startItemId = startEpisode.id ) for (episode in episodes) { val mediaSources = jellyfinRepository.getMediaSources(episode.id) + if (mediaSources.isEmpty()) continue playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) } + if (playerItems.isEmpty()) throw Exception("No playable items found") } fun markAsPlayed(itemId: UUID) { 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 5a8695ec..ac3a068d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -183,7 +183,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } - fun preparePlayer() { + fun preparePlayerItems() { _playerItemsError.value = null viewModelScope.launch { try { @@ -198,9 +198,14 @@ 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)) + val episodes = jellyfinRepository.getEpisodes( + startEpisode.seriesId!!, + startEpisode.seasonId!!, + startItemId = startEpisode.id + ) for (episode in episodes) { val mediaSources = jellyfinRepository.getMediaSources(episode.id) + if (mediaSources.isEmpty()) continue playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) } } else { @@ -209,10 +214,12 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { val episodes = jellyfinRepository.getEpisodes(series.id, season.id) for (episode in episodes) { val mediaSources = jellyfinRepository.getMediaSources(episode.id) + if (mediaSources.isEmpty()) continue playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) } } } + if (playerItems.isEmpty()) throw Exception("No playable items found") } fun navigateToPlayer(mediaSource: MediaSourceInfo) { From be2c4dcb9a115aa58008d431df87717087b2b1f0 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Wed, 25 Aug 2021 18:01:18 +0200 Subject: [PATCH 09/16] Improve image loading --- .../dev/jdtech/jellyfin/BindingAdapters.kt | 90 +++++++++---------- app/src/main/res/layout/collection_item.xml | 2 +- .../main/res/layout/episode_bottom_sheet.xml | 2 +- app/src/main/res/layout/episode_item.xml | 2 +- .../main/res/layout/fragment_media_info.xml | 2 +- app/src/main/res/layout/home_episode_item.xml | 2 +- 6 files changed, 47 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index 40315453..cf6c6580 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.imageTags!!.keys.contains(ImageType.BACKDROP)) { + 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/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..7c1bbdd1 100644 --- a/app/src/main/res/layout/episode_bottom_sheet.xml +++ b/app/src/main/res/layout/episode_bottom_sheet.xml @@ -33,7 +33,7 @@ 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" /> diff --git a/app/src/main/res/layout/episode_item.xml b/app/src/main/res/layout/episode_item.xml index 428b9fac..2e6c5b3a 100644 --- a/app/src/main/res/layout/episode_item.xml +++ b/app/src/main/res/layout/episode_item.xml @@ -24,7 +24,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" diff --git a/app/src/main/res/layout/fragment_media_info.xml b/app/src/main/res/layout/fragment_media_info.xml index 3ebd9fec..f01d5ab0 100644 --- a/app/src/main/res/layout/fragment_media_info.xml +++ b/app/src/main/res/layout/fragment_media_info.xml @@ -358,7 +358,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:adjustViewBounds="true" - app:itemPrimaryImage="@{viewModel.nextUp}" + app:baseItemImage="@{viewModel.nextUp}" app:layout_constraintDimensionRatio="H,16:9" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/home_episode_item.xml b/app/src/main/res/layout/home_episode_item.xml index 06b52350..84ba99ce 100644 --- a/app/src/main/res/layout/home_episode_item.xml +++ b/app/src/main/res/layout/home_episode_item.xml @@ -22,7 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:scaleType="centerCrop" - app:episodeImage="@{episode}" + app:baseItemImage="@{episode}" app:layout_constraintDimensionRatio="H,16:9" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" From 1417d9722341b93e72241b02cccfc8e2601a983a Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Wed, 25 Aug 2021 18:10:00 +0200 Subject: [PATCH 10/16] Fix crash in player when no connection to server --- .../viewmodels/PlayerActivityViewModel.kt | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) 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..2b40efe4 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -84,10 +84,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 +111,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) From 653d41c68aa872f1d27822e9dc4d547ab6a50f91 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Wed, 25 Aug 2021 18:26:48 +0200 Subject: [PATCH 11/16] Fix continue watching movie image --- app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index cf6c6580..c336aeb1 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -124,7 +124,7 @@ fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) { if (!episode.imageTags.isNullOrEmpty()) { when (episode.type) { "Movie" -> { - if (episode.imageTags!!.keys.contains(ImageType.BACKDROP)) { + if (!episode.backdropImageTags.isNullOrEmpty()) { imageType = ImageType.BACKDROP } } From bb208d68fde57d4dc195fab922f39c5779d5d77b Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Wed, 25 Aug 2021 18:45:04 +0200 Subject: [PATCH 12/16] Add missing episode icon --- .../main/res/layout/episode_bottom_sheet.xml | 25 +++++++++++++++++++ app/src/main/res/layout/episode_item.xml | 25 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/episode_bottom_sheet.xml b/app/src/main/res/layout/episode_bottom_sheet.xml index 7c1bbdd1..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 @@ + + + + @@ -38,6 +42,27 @@ app:layout_constraintTop_toBottomOf="@id/holder" app:shapeAppearance="@style/roundedImageView" /> + + + + + + + @@ -44,6 +46,27 @@ app:layout_constraintEnd_toEndOf="@id/episode_image" app:layout_constraintTop_toTopOf="@id/episode_image" /> + + + + + Date: Thu, 26 Aug 2021 10:21:10 +0200 Subject: [PATCH 13/16] Update proguard rules --- app/proguard-rules.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 25ac5524d733e31a37b2fecdb48c331329898395 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Thu, 26 Aug 2021 15:36:56 +0200 Subject: [PATCH 14/16] Rework how player items are created Add support for intros and improve loading speed --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 2 +- .../dialogs/VideoVersionDialogFragment.kt | 6 +- .../fragments/EpisodeBottomSheetFragment.kt | 21 ++-- .../jellyfin/fragments/MediaInfoFragment.kt | 35 ++---- .../dev/jdtech/jellyfin/models/PlayerItem.kt | 3 +- .../jellyfin/repository/JellyfinRepository.kt | 2 + .../repository/JellyfinRepositoryImpl.kt | 10 ++ .../viewmodels/EpisodeBottomSheetViewModel.kt | 33 +++++- .../jellyfin/viewmodels/MediaInfoViewModel.kt | 101 ++++++++++++------ .../viewmodels/PlayerActivityViewModel.kt | 33 +++--- .../main/res/navigation/main_navigation.xml | 3 - 11 files changed, 160 insertions(+), 89 deletions(-) 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/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 a6535000..e4fadaf3 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -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/MediaInfoFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt index b02f3991..4bb4ffd2 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -63,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 -> @@ -91,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( @@ -139,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 { @@ -164,29 +168,14 @@ 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") { @@ -232,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/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/repository/JellyfinRepository.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index 526d0745..d99de89b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -54,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 4dc70436..c206fb53 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -266,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/viewmodels/EpisodeBottomSheetViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt index 383deb1d..1ee2c478 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,7 @@ 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 timber.log.Timber import java.text.DateFormat import java.time.ZoneOffset @@ -74,17 +75,39 @@ constructor( } 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!!, - startItemId = startEpisode.id + startItemId = startEpisode.id, + fields = listOf(ItemFields.MEDIA_SOURCES) ) for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - if (mediaSources.isEmpty()) continue - playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!)) + if (episode.mediaSources.isNullOrEmpty()) continue + playerItems.add( + PlayerItem( + episode.id, + episode.mediaSources?.get(0)?.id!!, + playbackPosition + ) + ) } - if (playerItems.isEmpty()) throw Exception("No playable items found") + + 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/MediaInfoViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt index ac3a068d..975672f2 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,7 @@ 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 timber.log.Timber import java.util.* import javax.inject.Inject @@ -52,9 +52,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,9 +88,6 @@ 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.toString() @@ -183,11 +177,11 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } - fun preparePlayerItems() { + 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,35 +189,76 @@ 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!!, - startItemId = startEpisode.id - ) - for (episode in episodes) { - val mediaSources = jellyfinRepository.getMediaSources(episode.id) - if (mediaSources.isEmpty()) continue - 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) - if (mediaSources.isEmpty()) continue - 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 + 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 + playerItems.add( + PlayerItem( + episode.id, + episode.mediaSources?.get(0)?.id!!, + 0 + ) + ) + } + } } } } - if (playerItems.isEmpty()) throw Exception("No playable items found") - } - 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/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 2b40efe4..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 @@ -131,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/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 559933c0..aa8393ad 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -160,9 +160,6 @@ - Date: Thu, 26 Aug 2021 16:08:38 +0200 Subject: [PATCH 15/16] Fix missing episodes being sent to the player --- .../jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt | 2 ++ .../java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt | 3 +++ 2 files changed, 5 insertions(+) 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 1ee2c478..e09b18a8 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -11,6 +11,7 @@ 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 @@ -98,6 +99,7 @@ constructor( ) for (episode in episodes) { if (episode.mediaSources.isNullOrEmpty()) continue + if (episode.locationType == LocationType.VIRTUAL) continue playerItems.add( PlayerItem( episode.id, 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 975672f2..4b0a5485 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.LocationType import timber.log.Timber import java.util.* import javax.inject.Inject @@ -227,6 +228,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { ) for (episode in episodes) { if (episode.mediaSources.isNullOrEmpty()) continue + if (episode.locationType == LocationType.VIRTUAL) continue playerItems.add( PlayerItem( episode.id, @@ -245,6 +247,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { ) for (episode in episodes) { if (episode.mediaSources.isNullOrEmpty()) continue + if (episode.locationType == LocationType.VIRTUAL) continue playerItems.add( PlayerItem( episode.id, From e474544a3d314813e8f12ef51be373aac328ce70 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Thu, 26 Aug 2021 23:58:07 +0200 Subject: [PATCH 16/16] Update versionCode to 3 and versionName to 0.1.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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" }