diff --git a/.github/ISSUE_TEMPLATE/bug-report-playback.yaml b/.github/ISSUE_TEMPLATE/bug-report-playback.yaml index 3dae2fd3c6..0bd943fc36 100644 --- a/.github/ISSUE_TEMPLATE/bug-report-playback.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report-playback.yaml @@ -4,6 +4,22 @@ labels: - bug - playback body: + - type: checkboxes + id: before-posting + attributes: + label: "This issue respects the following points:" + description: All conditions are **required**. + options: + - label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-androidtv/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_. + required: true + - label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct). + required: true + - label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one. + required: true + - type: markdown + attributes: + value: | + ## Bug information - type: textarea id: description attributes: @@ -20,10 +36,24 @@ body: Instead, I expect … validations: required: true + - type: textarea + id: mediainfo + attributes: + label: Media info of the file + description: | + Please share the media information for the file causing issues. You can use a variety of tools to retrieve this information. + - Use ffprobe (`ffprobe ./file.mp4`) + - Copy the media info from the web interface + placeholder: Paste media info… + render: shell + - type: markdown + attributes: + value: | + ## Logs - type: textarea id: logs attributes: - label: Logs + label: Client logs description: | Please paste your crash logs here if applicable. You can find these in your servers dashboard under "logs". The file name should start with "upload_org.jellyfin.androidtv". @@ -37,31 +67,34 @@ body: Please paste your FFmpeg logs here. You can find these in your servers dashboard under "logs". placeholder: Paste logs… render: shell - - type: textarea - id: mediainfo + - type: markdown attributes: - label: Media info of the file - description: | - Please share the media information for the file causing issues. You can use a variety of tools to retrieve this information. - - Use ffprobe (`ffprobe ./file.mp4`) - - Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) - - Copy the media info from the web interface - placeholder: Paste media info… - render: shell + value: | + ## Environment - type: input id: app-version attributes: label: Application version description: The version of the installed Jellyfin Android TV app. - placeholder: 0.14.0 + placeholder: 0.18.2 validations: required: true + - type: dropdown + id: installation-source + attributes: + label: Where did you install the app from? + description: Choose the appropriate app store or installation method. + options: + - Google Play Store + - Amazon Appstore + - F-Droid + - Sideloaded APK - type: input id: device-info attributes: label: Device information description: Manufacturer and model - placeholder: Nvidia Shield Pro (2017), Amazon Fire TV Stick v1 (2014) + placeholder: Nvidia Shield TV Pro (2019), Fire TV Stick HD (2020) validations: required: true - type: input @@ -69,7 +102,7 @@ body: attributes: label: Android version description: Version of the OS and other information (e.g. custom ROM / OEM skin) - placeholder: Android 9, Fire OS 7 + placeholder: Android 13, Fire OS 7 validations: required: true - type: input @@ -77,6 +110,18 @@ body: attributes: label: Jellyfin server version description: If on unstable, please specify the commit hash. - placeholder: 10.8.1 + placeholder: 10.10.2 validations: required: true + - type: markdown + attributes: + value: | + ## Additional + - type: input + id: other-sources + attributes: + label: Other sources + description: If this topic has been discussed outside of GitHub, please link it. + placeholder: https://forum.jellyfin.org/… + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 0bd5a1b953..d174b711b4 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -3,6 +3,22 @@ description: Create a bug report labels: - bug body: + - type: checkboxes + id: before-posting + attributes: + label: "This issue respects the following points:" + description: All conditions are **required**. + options: + - label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-androidtv/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_. + required: true + - label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct). + required: true + - label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one. + required: true + - type: markdown + attributes: + value: | + ## Bug information - type: textarea id: description attributes: @@ -28,12 +44,16 @@ body: The file name should start with "upload_org.jellyfin.androidtv". Make sure that they don't contain any sensitive information like server URL, auth tokens or passwords. placeholder: Paste logs… + - type: markdown + attributes: + value: | + ## Environment - type: input id: app-version attributes: label: Application version description: The version of the installed Jellyfin Android TV app. - placeholder: 0.14.0 + placeholder: 0.18.2 validations: required: true - type: dropdown @@ -42,15 +62,16 @@ body: label: Where did you install the app from? description: Choose the appropriate app store or installation method. options: - - Google Play + - Google Play Store - Amazon Appstore + - F-Droid - Sideloaded APK - type: input id: device-info attributes: label: Device information description: Manufacturer and model - placeholder: Nvidia Shield Pro (2017), Amazon Fire TV Stick v1 (2014) + placeholder: Nvidia Shield TV Pro (2019), Fire TV Stick HD (2020) validations: required: true - type: input @@ -58,7 +79,7 @@ body: attributes: label: Android version description: Version of the OS and other information (e.g. custom ROM / OEM skin) - placeholder: Android 9, Fire OS 7 + placeholder: Android 13, Fire OS 7 validations: required: true - type: input @@ -66,6 +87,18 @@ body: attributes: label: Jellyfin server version description: If on unstable, please specify the commit hash. - placeholder: 10.8.1 + placeholder: 10.10.2 validations: required: true + - type: markdown + attributes: + value: | + ## Additional + - type: input + id: other-sources + attributes: + label: Other sources + description: If this topic has been discussed outside of GitHub, please link it. + placeholder: https://forum.jellyfin.org/… + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index cfaa3d740f..6cb5ff2877 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -3,12 +3,54 @@ description: Request a new feature labels: - enhancement body: + - type: checkboxes + id: before-posting + attributes: + label: "This request respects the following points:" + description: All conditions are **required**. + options: + - label: This request is **not** already on [GitHub](https://github.com/jellyfin/jellyfin-androidtv/labels/enhancement) _(I've searched it)_. + required: true + - label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct). + required: true + - type: textarea + attributes: + label: Problem description + description: Describe the issue you are experiencing or the gap you are trying to address. + placeholder: | + The Android TV app does not support displaying lyrics, while the Jellyfin web interface does. + This creates an inconsistent user experience across platforms. + validations: + required: true + - type: textarea + attributes: + label: Proposed solution + description: Provide a detailed description of what you would like to see implemented or changed. + placeholder: | + The app could display lyrics in the screensaver. Additionally, a lyrics section could be integrated into the page that opens when music is played. + validations: + required: true - type: textarea - id: description attributes: - label: Describe the feature you'd like - description: | - A clear and concise description of what you want to request. - You can also attach screenshots or screen recordings to help explain your request. + label: Alternatives considered + description: Outline any other approaches you have thought about or explored to solve the problem. + placeholder: | + An alternative approach could involve adding a dedicated "Lyrics" button in the app, similar to the Jellyfin web interface. + Users could press this button to open a full-screen lyrics view. validations: required: true + - type: textarea + attributes: + label: Additional information + description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request. + placeholder: Add any additional context here. + validations: + required: false + - type: input + id: other-sources + attributes: + label: Other sources + description: If this topic has been discussed outside of GitHub, please link it. + placeholder: https://forum.jellyfin.org/… + validations: + required: false diff --git a/.github/workflows/app-build.yaml b/.github/workflows/app-build.yaml index ddd0305637..8722a5f980 100644 --- a/.github/workflows/app-build.yaml +++ b/.github/workflows/app-build.yaml @@ -17,18 +17,18 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Java - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 + uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - name: Assemble debug APKs run: ./gradlew assembleDebug - name: Create publish bundle run: mkdir -p build/gh-app-publish/; find app/build/ -iname "*.apk" -exec mv "{}" build/gh-app-publish/ \; - name: Upload artifacts - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: build-artifacts retention-days: 14 diff --git a/.github/workflows/app-lint.yaml b/.github/workflows/app-lint.yaml index d06798aa67..2be0779952 100644 --- a/.github/workflows/app-lint.yaml +++ b/.github/workflows/app-lint.yaml @@ -19,16 +19,16 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Java - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 + uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - name: Run detekt and lint tasks run: ./gradlew detekt lint - name: Upload SARIF files - uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 + uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 if: ${{ always() }} with: sarif_file: . diff --git a/.github/workflows/app-publish.yaml b/.github/workflows/app-publish.yaml index 645e02263e..255fb3c667 100644 --- a/.github/workflows/app-publish.yaml +++ b/.github/workflows/app-publish.yaml @@ -14,12 +14,12 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Java - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 + uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - name: Set JELLYFIN_VERSION run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV - name: Assemble release files diff --git a/.github/workflows/app-test.yaml b/.github/workflows/app-test.yaml index 922f24e51b..db430bc76b 100644 --- a/.github/workflows/app-test.yaml +++ b/.github/workflows/app-test.yaml @@ -18,11 +18,11 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Java - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: distribution: temurin java-version: 21 - name: Setup Gradle - uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 + uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - name: Run test task run: ./gradlew test diff --git a/.github/workflows/gradlew-validate.yaml b/.github/workflows/gradlew-validate.yaml index 5d3310e543..f9580e4e90 100644 --- a/.github/workflows/gradlew-validate.yaml +++ b/.github/workflows/gradlew-validate.yaml @@ -19,4 +19,4 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 + uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 960491d5e6..02aaade313 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,6 +14,7 @@ - [mohd-akram](https://github.com/mohd-akram) - [3l0w](https://github.com/3l0w) - [MajMongoose](https://github.com/majmongoose) + - [Olaren15](https://github.com/Olaren15) # Emby Contributors diff --git a/app/src/main/java/org/jellyfin/androidtv/data/repository/ItemRepository.kt b/app/src/main/java/org/jellyfin/androidtv/data/repository/ItemRepository.kt new file mode 100644 index 0000000000..41a8787acc --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/data/repository/ItemRepository.kt @@ -0,0 +1,26 @@ +package org.jellyfin.androidtv.data.repository + +import org.jellyfin.sdk.model.api.ItemFields + +object ItemRepository { + val itemFields = setOf( + ItemFields.CAN_DELETE, + ItemFields.CHANNEL_INFO, + ItemFields.CHAPTERS, + ItemFields.CHILD_COUNT, + ItemFields.CUMULATIVE_RUN_TIME_TICKS, + ItemFields.DATE_CREATED, + ItemFields.DISPLAY_PREFERENCES_ID, + ItemFields.GENRES, + ItemFields.ITEM_COUNTS, + ItemFields.MEDIA_SOURCE_COUNT, + ItemFields.MEDIA_SOURCES, + ItemFields.MEDIA_STREAMS, + ItemFields.OVERVIEW, + ItemFields.PATH, + ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, + ItemFields.SERIES_PRIMARY_IMAGE, + ItemFields.TAGLINES, + ItemFields.TRICKPLAY, + ) +} diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt b/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt index ce51c0a3e6..89e4043249 100644 --- a/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt +++ b/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.androidtv.data.repository.UserViewsRepository import org.jellyfin.androidtv.integration.provider.ImageProvider import org.jellyfin.androidtv.preference.UserPreferences @@ -41,7 +42,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ImageFormat import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.ticks import org.koin.core.component.KoinComponent @@ -270,7 +270,7 @@ class LeanbackChannelWorker( withContext(Dispatchers.IO) { val resume = async { api.itemsApi.getResumeItems( - fields = listOf(ItemFields.DATE_CREATED, ItemFields.OVERVIEW), + fields = ItemRepository.itemFields, imageTypeLimit = 1, limit = 10, mediaTypes = listOf(MediaType.VIDEO), @@ -284,7 +284,7 @@ class LeanbackChannelWorker( imageTypeLimit = 1, limit = 10, enableResumable = false, - fields = listOf(ItemFields.DATE_CREATED, ItemFields.OVERVIEW), + fields = ItemRepository.itemFields, ).content.items } @@ -296,9 +296,7 @@ class LeanbackChannelWorker( withContext(Dispatchers.IO) { val latestEpisodes = async { api.userLibraryApi.getLatestMedia( - fields = listOf( - ItemFields.OVERVIEW, - ), + fields = ItemRepository.itemFields, limit = 50, includeItemTypes = listOf(BaseItemKind.EPISODE), isPlayed = false @@ -307,9 +305,7 @@ class LeanbackChannelWorker( val latestMovies = async { api.userLibraryApi.getLatestMedia( - fields = listOf( - ItemFields.OVERVIEW, - ), + fields = ItemRepository.itemFields, limit = 50, includeItemTypes = listOf(BaseItemKind.MOVIE), isPlayed = false @@ -318,9 +314,7 @@ class LeanbackChannelWorker( val latestMedia = async { api.userLibraryApi.getLatestMedia( - fields = listOf( - ItemFields.OVERVIEW, - ), + fields = ItemRepository.itemFields, limit = 50, includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES), isPlayed = false diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/MediaContentProvider.kt b/app/src/main/java/org/jellyfin/androidtv/integration/MediaContentProvider.kt index c9e10adeb9..8b02a047d4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/integration/MediaContentProvider.kt +++ b/app/src/main/java/org/jellyfin/androidtv/integration/MediaContentProvider.kt @@ -12,6 +12,7 @@ import android.provider.BaseColumns import kotlinx.coroutines.runBlocking import org.jellyfin.androidtv.BuildConfig import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.androidtv.integration.provider.ImageProvider import org.jellyfin.androidtv.util.ImageHelper import org.jellyfin.androidtv.util.sdk.isUsable @@ -21,7 +22,6 @@ import org.jellyfin.sdk.api.client.extensions.imageApi import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.api.ItemFields import org.koin.core.component.KoinComponent import org.koin.core.component.inject import timber.log.Timber @@ -75,7 +75,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent { searchTerm = query, recursive = true, limit = limit, - fields = setOf(ItemFields.TAGLINES) + fields = ItemRepository.itemFields ) items diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/ItemListViewHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/ItemListViewHelper.kt index 477b1098d8..57d0af0310 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/ItemListViewHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/ItemListViewHelper.kt @@ -3,9 +3,9 @@ package org.jellyfin.androidtv.ui import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi -import org.jellyfin.sdk.model.api.ItemFields import org.koin.java.KoinJavaComponent fun ItemListView.refresh() { @@ -14,14 +14,7 @@ fun ItemListView.refresh() { findViewTreeLifecycleOwner()?.lifecycleScope?.launch { val response by api.itemsApi.getItems( ids = mItemIds, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_SOURCES, - ) + fields = ItemRepository.itemFields ) response.items?.forEachIndexed { index, item -> diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/NowPlayingView.kt b/app/src/main/java/org/jellyfin/androidtv/ui/NowPlayingView.kt index 01321a1892..b176539ad7 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/NowPlayingView.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/NowPlayingView.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -50,7 +51,9 @@ import org.jellyfin.sdk.model.api.ImageType import org.koin.compose.koinInject @Composable -fun NowPlayingComposable() { +fun NowPlayingComposable( + onFocusableChange: (focusable: Boolean) -> Unit, +) { val playbackManager = koinInject() val navigationRepository = koinInject() val imageHelper = koinInject() @@ -59,6 +62,8 @@ fun NowPlayingComposable() { val item = entry?.run { baseItemFlow.collectAsState(baseItem) }?.value val progress = rememberPlayerProgress(playbackManager) + LaunchedEffect(item == null) { onFocusableChange(item != null) } + AnimatedVisibility( visible = item != null, enter = fadeIn(), @@ -153,5 +158,11 @@ class NowPlayingView @JvmOverloads constructor( defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { @Composable - override fun Content() = NowPlayingComposable() + override fun Content() = NowPlayingComposable( + // Workaround for older Android versions unable to find focus in our toolbar view when the NowPlayingView is added but inactive + onFocusableChange = { focusable -> + isFocusable = focusable + descendantFocusability = if (focusable) FOCUS_AFTER_DESCENDANTS else FOCUS_BLOCK_DESCENDANTS + } + ) } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowseViewFragmentHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowseViewFragmentHelper.kt index 39cb53469e..8498974a62 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowseViewFragmentHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowseViewFragmentHelper.kt @@ -2,12 +2,12 @@ package org.jellyfin.androidtv.ui.browsing import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.liveTvApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.LocationType import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.TimerInfoDto @@ -24,11 +24,7 @@ fun EnhancedBrowseFragment.getLiveTvRecordingsAndTimers( lifecycleScope.launch { runCatching { val recordings by api.liveTvApi.getRecordings( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, enableImages = true, limit = 40, ) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowsingUtils.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowsingUtils.kt index 096363fc1e..254998b1ff 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowsingUtils.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowsingUtils.kt @@ -3,13 +3,13 @@ package org.jellyfin.androidtv.ui.browsing import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.ApiClientException import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFilter import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder @@ -59,22 +59,13 @@ object BrowsingUtils { limit = 50, parentId = parentId, imageTypeLimit = 1, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ) + fields = ItemRepository.itemFields ) @JvmStatic fun createSeriesGetNextUpRequest(parentId: UUID) = GetNextUpRequest( seriesId = parentId, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ) + fields = ItemRepository.itemFields ) @JvmStatic @@ -84,13 +75,7 @@ object BrowsingUtils { itemType: BaseItemKind? = null, groupItems: Boolean? = null ) = GetLatestMediaRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ), + fields = ItemRepository.itemFields, parentId = parentId, limit = 50, imageTypeLimit = 1, @@ -101,42 +86,26 @@ object BrowsingUtils { @JvmStatic fun createSeasonsRequest(seriesId: UUID) = GetSeasonsRequest( seriesId = seriesId, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, ) @JvmStatic fun createUpcomingEpisodesRequest(parentId: UUID) = GetUpcomingEpisodesRequest( parentId = parentId, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, ) @JvmStatic fun createSimilarItemsRequest(itemId: UUID) = GetSimilarItemsRequest( itemId = itemId, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, limit = 20, ) @JvmStatic fun createLiveTVOnNowRequest() = GetRecommendedProgramsRequest( isAiring = true, - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHANNEL_INFO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, imageTypeLimit = 1, enableTotalRecordCount = false, limit = 150, @@ -146,12 +115,7 @@ object BrowsingUtils { fun createLiveTVUpcomingRequest() = GetRecommendedProgramsRequest( isAiring = false, hasAired = false, - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHANNEL_INFO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, imageTypeLimit = 1, enableTotalRecordCount = false, limit = 150, @@ -160,22 +124,14 @@ object BrowsingUtils { @JvmStatic @JvmOverloads fun createLiveTVRecordingsRequest(limit: Int? = null) = GetRecordingsRequest( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, enableImages = true, limit = limit, ) @JvmStatic fun createLiveTVMovieRecordingsRequest() = GetRecordingsRequest( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, enableImages = true, limit = 60, isMovie = true, @@ -183,11 +139,7 @@ object BrowsingUtils { @JvmStatic fun createLiveTVSeriesRecordingsRequest() = GetRecordingsRequest( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, enableImages = true, limit = 60, isSeries = true, @@ -195,11 +147,7 @@ object BrowsingUtils { @JvmStatic fun createLiveTVSportsRecordingsRequest() = GetRecordingsRequest( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, enableImages = true, limit = 60, isSports = true, @@ -207,11 +155,7 @@ object BrowsingUtils { @JvmStatic fun createLiveTVKidsRecordingsRequest() = GetRecordingsRequest( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, enableImages = true, limit = 60, isKids = true, @@ -224,31 +168,19 @@ object BrowsingUtils { @JvmStatic fun createAlbumArtistsRequest(parentId: UUID) = GetAlbumArtistsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.ITEM_COUNTS, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, parentId = parentId, ) @JvmStatic fun createArtistsRequest(parentId: UUID) = GetArtistsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.ITEM_COUNTS, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, parentId = parentId, ) @JvmStatic fun createPersonItemsRequest(personId: UUID, itemType: BaseItemKind) = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, personIds = setOf(personId), recursive = true, includeItemTypes = setOf(itemType), @@ -257,11 +189,7 @@ object BrowsingUtils { @JvmStatic fun createArtistItemsRequest(artistId: UUID, itemType: BaseItemKind) = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, artistIds = setOf(artistId), recursive = true, includeItemTypes = setOf(itemType), @@ -270,13 +198,7 @@ object BrowsingUtils { @JvmStatic fun createNextEpisodesRequest(seasonId: UUID, indexNumber: Int) = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, parentId = seasonId, includeItemTypes = setOf(BaseItemKind.EPISODE), startIndex = indexNumber, @@ -285,15 +207,7 @@ object BrowsingUtils { @JvmStatic fun createResumeItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_STREAMS, - ItemFields.MEDIA_SOURCES, - ), + fields = ItemRepository.itemFields, includeItemTypes = setOf(itemType), recursive = true, parentId = parentId, @@ -308,15 +222,7 @@ object BrowsingUtils { @JvmStatic fun createFavoriteItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_STREAMS, - ItemFields.MEDIA_SOURCES, - ), + fields = ItemRepository.itemFields, includeItemTypes = setOf(itemType), recursive = true, parentId = parentId, @@ -327,7 +233,7 @@ object BrowsingUtils { @JvmStatic fun createCollectionsRequest(parentId: UUID) = GetItemsRequest( - fields = setOf(ItemFields.CHILD_COUNT), + fields = ItemRepository.itemFields, includeItemTypes = setOf(BaseItemKind.BOX_SET), recursive = true, imageTypeLimit = 1, @@ -337,12 +243,7 @@ object BrowsingUtils { @JvmStatic fun createPremieresRequest(parentId: UUID) = GetItemsRequest( - fields = setOf( - ItemFields.DATE_CREATED, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, includeItemTypes = setOf(BaseItemKind.EPISODE), parentId = parentId, indexNumber = 1, @@ -358,13 +259,7 @@ object BrowsingUtils { @JvmStatic fun createLastPlayedRequest(parentId: UUID) = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, includeItemTypes = setOf(BaseItemKind.AUDIO), recursive = true, parentId = parentId, @@ -378,11 +273,7 @@ object BrowsingUtils { @JvmStatic fun createPlaylistsRequest() = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CUMULATIVE_RUN_TIME_TICKS, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, includeItemTypes = setOf(BaseItemKind.PLAYLIST), imageTypeLimit = 1, recursive = true, @@ -393,13 +284,7 @@ object BrowsingUtils { @JvmStatic fun createBrowseGridItemsRequest(parent: BaseItemDto): GetItemsRequest { val baseRequest = GetItemsRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ItemFields.DISPLAY_PREFERENCES_ID, - ), + fields = ItemRepository.itemFields, parentId = parent.id, ) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByGenreFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByGenreFragment.kt index e82c020016..eba768e801 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByGenreFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByGenreFragment.kt @@ -1,9 +1,9 @@ package org.jellyfin.androidtv.ui.browsing +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.genresApi import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.koin.android.ext.android.inject @@ -28,13 +28,7 @@ class ByGenreFragment : BrowseFolderFragment() { includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf), genres = setOf(genre.name.orEmpty()), recursive = true, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, ) rows.add(BrowseRowDef(genre.name, itemsRequest, 40)) } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByLetterFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByLetterFragment.kt index 259ed77427..b90681f0e4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByLetterFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/ByLetterFragment.kt @@ -1,8 +1,8 @@ package org.jellyfin.androidtv.ui.browsing import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.request.GetItemsRequest @@ -20,13 +20,7 @@ class ByLetterFragment : BrowseFolderFragment() { includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf), nameLessThan = letters.substring(0, 1), recursive = true, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, ) rows.add(BrowseRowDef("#", numbersItemsRequest, 40)) @@ -39,13 +33,7 @@ class ByLetterFragment : BrowseFolderFragment() { includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf), nameStartsWith = letter.toString(), recursive = true, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, ) rows.add(BrowseRowDef(letter.toString(), letterItemsRequest, 40)) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/CollectionFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/CollectionFragment.kt index dfa4d63f8c..e71ef45e42 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/CollectionFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/CollectionFragment.kt @@ -1,40 +1,28 @@ package org.jellyfin.androidtv.ui.browsing import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.request.GetItemsRequest class CollectionFragment : EnhancedBrowseFragment() { - companion object { - private val itemFields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_STREAMS, - ItemFields.MEDIA_SOURCES, - ) - } - override fun setupQueries(rowLoader: RowLoader) { val movies = GetItemsRequest( - fields = itemFields, + fields = ItemRepository.itemFields, parentId = mFolder.id, includeItemTypes = setOf(BaseItemKind.MOVIE), ) mRows.add(BrowseRowDef(getString(R.string.lbl_movies), movies, 100)) val series = GetItemsRequest( - fields = itemFields, + fields = ItemRepository.itemFields, parentId = mFolder.id, includeItemTypes = setOf(BaseItemKind.SERIES), ) mRows.add(BrowseRowDef(getString(R.string.lbl_tv_series), series, 100)) val others = GetItemsRequest( - fields = itemFields, + fields = ItemRepository.itemFields, parentId = mFolder.id, excludeItemTypes = setOf(BaseItemKind.MOVIE, BaseItemKind.SERIES), ) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/GenericFolderFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/GenericFolderFragment.kt index 77cbe09d53..16acd48dcb 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/GenericFolderFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/GenericFolderFragment.kt @@ -2,8 +2,8 @@ package org.jellyfin.androidtv.ui.browsing import org.jellyfin.androidtv.R import org.jellyfin.androidtv.data.querying.GetSpecialsRequest +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFilter import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder @@ -17,14 +17,6 @@ class GenericFolderFragment : EnhancedBrowseFragment() { BaseItemKind.USER_VIEW, BaseItemKind.CHANNEL_FOLDER_ITEM, ) - - private val itemFields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ) } override fun setupQueries(rowLoader: RowLoader) { @@ -39,7 +31,7 @@ class GenericFolderFragment : EnhancedBrowseFragment() { if (showSpecialViewTypes.contains(mFolder.type)) { if (mFolder.type != BaseItemKind.CHANNEL_FOLDER_ITEM) { val resume = GetItemsRequest( - fields = itemFields, + fields = ItemRepository.itemFields, parentId = mFolder.id, limit = 50, filters = setOf(ItemFilter.IS_RESUMABLE), @@ -50,7 +42,7 @@ class GenericFolderFragment : EnhancedBrowseFragment() { } val latest = GetItemsRequest( - fields = itemFields, + fields = ItemRepository.itemFields, parentId = mFolder.id, limit = 50, filters = setOf(ItemFilter.IS_UNPLAYED), @@ -61,7 +53,7 @@ class GenericFolderFragment : EnhancedBrowseFragment() { } val byName = GetItemsRequest( - fields = itemFields, + fields = ItemRepository.itemFields, parentId = mFolder.id, ) val header = when (mFolder.type) { diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt index c88439ae35..cb66204911 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/MainActivity.kt @@ -12,12 +12,15 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.jellyfin.androidtv.auth.repository.SessionRepository import org.jellyfin.androidtv.auth.repository.UserRepository import org.jellyfin.androidtv.databinding.ActivityMainBinding +import org.jellyfin.androidtv.integration.LeanbackChannelWorker import org.jellyfin.androidtv.ui.ScreensaverViewModel import org.jellyfin.androidtv.ui.background.AppBackground import org.jellyfin.androidtv.ui.navigation.NavigationAction @@ -35,6 +38,7 @@ class MainActivity : FragmentActivity() { private val sessionRepository by inject() private val userRepository by inject() private val screensaverViewModel by viewModel() + private val workManager by inject() private lateinit var binding: ActivityMainBinding @@ -104,6 +108,8 @@ class MainActivity : FragmentActivity() { override fun onStop() { super.onStop() + workManager.enqueue(OneTimeWorkRequestBuilder().build()) + lifecycleScope.launch { Timber.d("MainActivity stopped") sessionRepository.restoreSession(destroyOnly = true) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/SuggestedMoviesFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/SuggestedMoviesFragment.kt index bab4ea3ffe..b74033281c 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/browsing/SuggestedMoviesFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/browsing/SuggestedMoviesFragment.kt @@ -4,10 +4,10 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import org.jellyfin.androidtv.R import org.jellyfin.androidtv.constant.QueryType +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest @@ -34,13 +34,7 @@ class SuggestedMoviesFragment : EnhancedBrowseFragment() { for (item in response.items) { val similar = GetSimilarItemsRequest( itemId = item.id, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.CHILD_COUNT, - ItemFields.MEDIA_STREAMS, - ItemFields.MEDIA_SOURCES - ), + fields = ItemRepository.itemFields, limit = 7, ) mRows.add(BrowseRowDef(getString(R.string.because_you_watched, item.name), similar, QueryType.SimilarMovies)) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentHelper.kt index a7fc4762d9..070ab26e24 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentHelper.kt @@ -4,16 +4,15 @@ import android.content.Context import org.jellyfin.androidtv.R import org.jellyfin.androidtv.auth.repository.UserRepository import org.jellyfin.androidtv.constant.ChangeTriggerType +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.androidtv.ui.browsing.BrowseRowDef import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.request.GetNextUpRequest import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest import org.jellyfin.sdk.model.api.request.GetRecordingsRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest -import org.jellyfin.sdk.model.api.ItemFields as SdkItemFields class HomeFragmentHelper( private val context: Context, @@ -26,13 +25,7 @@ class HomeFragmentHelper( fun loadResume(title: String, includeMediaTypes: Collection): HomeFragmentRow { val query = GetResumeItemsRequest( limit = ITEM_LIMIT_RESUME, - fields = listOf( - SdkItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - SdkItemFields.OVERVIEW, - SdkItemFields.ITEM_COUNTS, - SdkItemFields.DISPLAY_PREFERENCES_ID, - SdkItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, imageTypeLimit = 1, enableTotalRecordCount = false, mediaTypes = includeMediaTypes, @@ -52,11 +45,7 @@ class HomeFragmentHelper( fun loadLatestLiveTvRecordings(): HomeFragmentRow { val query = GetRecordingsRequest( - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT - ), + fields = ItemRepository.itemFields, enableImages = true, limit = ITEM_LIMIT_RECORDINGS ) @@ -69,11 +58,7 @@ class HomeFragmentHelper( imageTypeLimit = 1, limit = ITEM_LIMIT_NEXT_UP, enableResumable = false, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.CHILD_COUNT, - ) + fields = ItemRepository.itemFields ) return HomeFragmentBrowseRowDefRow(BrowseRowDef(context.getString(R.string.lbl_next_up), query, arrayOf(ChangeTriggerType.TvPlayback))) @@ -82,12 +67,7 @@ class HomeFragmentHelper( fun loadOnNow(): HomeFragmentRow { val query = GetRecommendedProgramsRequest( isAiring = true, - fields = setOf( - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHANNEL_INFO, - ItemFields.CHILD_COUNT, - ), + fields = ItemRepository.itemFields, imageTypeLimit = 1, enableTotalRecordCount = false, limit = ITEM_LIMIT_ON_NOW diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentLatestRow.kt b/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentLatestRow.kt index 5d4c3eabe8..651714ac44 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentLatestRow.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentLatestRow.kt @@ -5,12 +5,12 @@ import androidx.leanback.widget.Row import org.jellyfin.androidtv.R import org.jellyfin.androidtv.auth.repository.UserRepository import org.jellyfin.androidtv.constant.ChangeTriggerType +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.androidtv.ui.browsing.BrowseRowDef import org.jellyfin.androidtv.ui.presentation.CardPresenter import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest class HomeFragmentLatestRow( @@ -28,12 +28,7 @@ class HomeFragmentLatestRow( .map { item -> // Create query and add it to a new row val request = GetLatestMediaRequest( - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.CHILD_COUNT, - ItemFields.SERIES_PRIMARY_IMAGE, - ), + fields = ItemRepository.itemFields, imageTypeLimit = 1, parentId = item.id, groupItems = true, diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java index 1571231fc0..da5b8ff8a2 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java @@ -678,7 +678,21 @@ public void setTitle(String title) { private String getRunTime() { Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getRunTimeTicks()); - return runtime != null && runtime > 0 ? String.format("%d%s", (int) Math.ceil((double) runtime / 600000000), getString(R.string.lbl_min)) : ""; + + if (runtime == null || runtime <= 0) { + return ""; + } + + int totalMinutes = (int) Math.ceil((double) runtime / 600000000); + + int hours = totalMinutes / 60; + int minutes = totalMinutes % 60; + + if (hours > 0) { + return getString(R.string.runtime_hours_minutes, hours, minutes); + } + + return getString(R.string.runtime_minutes, minutes); } private String getEndTime() { diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt index b06bb687f2..8494f502b2 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.androidtv.R import org.jellyfin.androidtv.data.model.DataRefreshService import org.jellyfin.androidtv.data.repository.ItemMutationRepository +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.androidtv.ui.navigation.Destinations import org.jellyfin.androidtv.ui.navigation.NavigationRepository import org.jellyfin.androidtv.util.apiclient.getSeriesOverview @@ -28,7 +29,6 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFilter import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType @@ -242,12 +242,7 @@ fun FullDetailsFragment.resumePlayback() { includeItemTypes = setOf(BaseItemKind.EPISODE), recursive = true, filters = setOf(ItemFilter.IS_UNPLAYED), - fields = setOf( - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ItemFields.CHAPTERS, - ItemFields.TRICKPLAY, - ), + fields = ItemRepository.itemFields, sortBy = setOf( ItemSortBy.PARENT_INDEX_NUMBER, ItemSortBy.INDEX_NUMBER, diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/ItemListFragmentHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/ItemListFragmentHelper.kt index e2102351ac..9baa933c4e 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/ItemListFragmentHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/ItemListFragmentHelper.kt @@ -4,27 +4,17 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.jellyfin.androidtv.data.repository.ItemMutationRepository +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.playlistsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.koin.android.ext.android.inject import java.util.UUID -private val itemFields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.OVERVIEW, - ItemFields.ITEM_COUNTS, - ItemFields.DISPLAY_PREFERENCES_ID, - ItemFields.CHILD_COUNT, - ItemFields.GENRES, - ItemFields.CHAPTERS, -) - fun ItemListFragment.loadItem(itemId: UUID) { val api by inject() @@ -48,7 +38,7 @@ fun MusicFavoritesListFragment.getFavoritePlaylist( filters = setOf(org.jellyfin.sdk.model.api.ItemFilter.IS_FAVORITE_OR_LIKES), sortBy = setOf(ItemSortBy.RANDOM), limit = 100, - fields = itemFields, + fields = ItemRepository.itemFields, ) callback(result.items) @@ -66,7 +56,7 @@ fun ItemListFragment.getPlaylist( item.type == BaseItemKind.PLAYLIST -> api.playlistsApi.getPlaylistItems( playlistId = item.id, limit = 150, - fields = itemFields, + fields = ItemRepository.itemFields, ) else -> api.itemsApi.getItems( @@ -75,7 +65,7 @@ fun ItemListFragment.getPlaylist( recursive = true, sortBy = setOf(ItemSortBy.SORT_NAME), limit = 200, - fields = itemFields, + fields = ItemRepository.itemFields, ) } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/livetv/LiveTvGuideFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/livetv/LiveTvGuideFragment.java index 082dba49b6..fadc4ce21d 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/livetv/LiveTvGuideFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/livetv/LiveTvGuideFragment.java @@ -235,7 +235,9 @@ private void load() { public void refreshFavorite(UUID channelId){ for (int i = 0; i < mChannels.getChildCount(); i++) { - GuideChannelHeader gch = (GuideChannelHeader)mChannels.getChildAt(i); + View child = mChannels.getChildAt(i); + if (!(child instanceof GuideChannelHeader)) continue; + GuideChannelHeader gch = (GuideChannelHeader) child; if (gch.getChannel().getId().equals(channelId.toString())) gch.refreshFavorite(); } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt index c875530e25..5ccd465e15 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt @@ -9,12 +9,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder import java.util.UUID @@ -35,7 +35,7 @@ class PictureViewerViewModel(private val api: ApiClient) : ViewModel() { val albumResponse by api.itemsApi.getItems( parentId = itemResponse.parentId, includeItemTypes = setOf(BaseItemKind.PHOTO), - fields = setOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO), + fields = ItemRepository.itemFields, sortBy = sortBy, sortOrder = listOf(sortOrder), ) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java index 0063bee98d..4306ff88c1 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/AudioNowPlayingFragment.java @@ -256,14 +256,9 @@ public void onProgress(long pos) { @Override public void onQueueStatusChanged(boolean hasQueue) { Timber.d("Queue status changed (hasQueue=%s)", hasQueue); - if (hasQueue) { - loadItem(); - if (mediaManager.getValue().isAudioPlayerInitialized()) { - updateButtons(); - } - } else { - if (navigationRepository.getValue().getCanGoBack()) navigationRepository.getValue().goBack(); - else navigationRepository.getValue().reset(Destinations.INSTANCE.getHome()); + loadItem(); + if (mediaManager.getValue().isAudioPlayerInitialized()) { + updateButtons(); } } @@ -308,6 +303,9 @@ private void loadItem() { if (mBaseItem != null) { updatePoster(); updateInfo(mBaseItem); + } else { + if (navigationRepository.getValue().getCanGoBack()) navigationRepository.getValue().goBack(); + else navigationRepository.getValue().navigate(Destinations.INSTANCE.getHome()); } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java index f02488cc8d..622d75da05 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java @@ -512,7 +512,7 @@ private VideoOptions buildExoPlayerOptions(@Nullable Integer forcedSubtitleIndex internalOptions.setMediaSourceId(currentMediaSource.getId()); } DeviceProfile internalProfile = new ExoPlayerProfile( - isLiveTv && !userPreferences.getValue().get(UserPreferences.Companion.getLiveTvDirectPlayEnabled()), + !internalOptions.getEnableDirectStream(), userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled()), userPreferences.getValue().get(UserPreferences.Companion.getAudioBehaviour()) == AudioBehavior.DOWNMIX_TO_STEREO ); @@ -901,6 +901,8 @@ public void seek(long pos, boolean skipToNext) { // set seekPosition so real position isn't used until playback starts again mSeekPosition = pos; + if (mCurrentStreamInfo == null) return; + // rebuild the stream // if an older device uses exoplayer to play a transcoded stream but falls back to the generic http stream instead of hls, rebuild the stream if (!mVideoManager.isSeekable()) { @@ -1023,7 +1025,8 @@ public void run() { private void startPauseReportLoop() { stopReportLoop(); - reportingHelper.getValue().reportProgress(mFragment, this, getCurrentlyPlayingItem(), getCurrentStreamInfo(), mCurrentPosition * 10000, true); + if (mCurrentStreamInfo == null) return; + reportingHelper.getValue().reportProgress(mFragment, this, getCurrentlyPlayingItem(), mCurrentStreamInfo, mCurrentPosition * 10000, true); mReportLoop = new Runnable() { @Override public void run() { diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java index e8f10ba00c..28e9015264 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManager.java @@ -44,7 +44,6 @@ import org.jellyfin.androidtv.data.compat.StreamInfo; import org.jellyfin.androidtv.preference.UserPreferences; import org.jellyfin.androidtv.preference.constant.ZoomMode; -import org.jellyfin.playback.media3.exoplayer.mapping.SubtitleKt; import org.jellyfin.sdk.api.client.ApiClient; import org.jellyfin.sdk.model.api.MediaStream; import org.jellyfin.sdk.model.api.MediaStreamType; @@ -346,7 +345,7 @@ public void setMediaStreamInfo(ApiClient api, StreamInfo streamInfo) { Uri subtitleUri = Uri.parse(api.createUrl(mediaStream.getDeliveryUrl(), Collections.emptyMap(), Collections.emptyMap(), true)); MediaItem.SubtitleConfiguration subtitleConfiguration = new MediaItem.SubtitleConfiguration.Builder(subtitleUri) .setId("JF_EXTERNAL:" + String.valueOf(mediaStream.getIndex())) - .setMimeType(SubtitleKt.getFfmpegSubtitleMimeType(mediaStream.getCodec())) + .setMimeType(VideoManagerHelperKt.getSubtitleMediaStreamCodec(mediaStream)) .setLanguage(mediaStream.getLanguage()) .setLabel(mediaStream.getDisplayTitle()) .setSelectionFlags(getSubtitleSelectionFlags(mediaStream)) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManagerHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManagerHelper.kt new file mode 100644 index 0000000000..b35ff89a5e --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/VideoManagerHelper.kt @@ -0,0 +1,19 @@ +package org.jellyfin.androidtv.ui.playback + +import androidx.core.net.toUri +import org.jellyfin.playback.media3.exoplayer.mapping.getFfmpegSubtitleMimeType +import org.jellyfin.sdk.model.api.MediaStream + +/** + * Return the media type for the codec found in this media stream. First tries to infer the media type from the streams delivery URL and + * falls back to the original stream codec. + */ +fun getSubtitleMediaStreamCodec(stream: MediaStream): String { + val codec = requireNotNull(stream.codec) + val codecMediaType = getFfmpegSubtitleMimeType(codec, "").ifBlank { null } + + val urlSubtitleExtension = stream.deliveryUrl?.toUri()?.lastPathSegment?.split('.')?.last() + val urlExtensionMediaType = urlSubtitleExtension?.let { getFfmpegSubtitleMimeType(it, "") }?.ifBlank { null } + + return urlExtensionMediaType ?: codecMediaType ?: urlSubtitleExtension ?: codec +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java index 4f8f777100..63ba9f9d24 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomPlaybackTransportControlGlue.java @@ -109,6 +109,13 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl protected void onDetachedFromHost() { mHandler.removeCallbacks(mRefreshEndTime); mHandler.removeCallbacks(mRefreshViewVisibility); + + closedCaptionsAction.removePopup(); + playbackSpeedAction.dismissPopup(); + selectAudioAction.dismissPopup(); + selectQualityAction.dismissPopup(); + zoomAction.dismissPopup(); + super.onDetachedFromHost(); } @@ -169,6 +176,7 @@ protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { super.onBindRowViewHolder(vh, item); vh.setOnKeyListener(CustomPlaybackTransportControlGlue.this); } + @Override protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { super.onUnbindRowViewHolder(vh); diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ClosedCaptionsAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ClosedCaptionsAction.kt index b706aed45e..f55c3e9e4d 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ClosedCaptionsAction.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ClosedCaptionsAction.kt @@ -17,6 +17,8 @@ class ClosedCaptionsAction( context: Context, customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue, ) : CustomAction(context, customPlaybackTransportControlGlue) { + private var popup: PopupMenu? = null + init { initializeWithIcon(R.drawable.ic_select_subtitle) } @@ -34,7 +36,8 @@ class ClosedCaptionsAction( } videoPlayerAdapter.leanbackOverlayFragment.setFading(false) - PopupMenu(context, view, Gravity.END).apply { + removePopup() + popup = PopupMenu(context, view, Gravity.END).apply { with(menu) { var order = 0 add(0, -1, order++, context.getString(R.string.lbl_none)).apply { @@ -51,11 +54,19 @@ class ClosedCaptionsAction( setGroupCheckable(0, true, false) } - setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) } + setOnDismissListener { + videoPlayerAdapter.leanbackOverlayFragment.setFading(true) + popup = null + } setOnMenuItemClickListener { item -> playbackController.setSubtitleIndex(item.itemId) true } - }.show() + } + popup?.show() + } + + fun removePopup() { + popup?.dismiss() } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlaybackSpeedAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlaybackSpeedAction.kt index b92adacd0e..825ab619d2 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlaybackSpeedAction.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/PlaybackSpeedAction.kt @@ -18,6 +18,7 @@ class PlaybackSpeedAction( ) : CustomAction(context, customPlaybackTransportControlGlue) { private val speedController = VideoSpeedController(playbackController) private val speeds = VideoSpeedController.SpeedSteps.entries.toTypedArray() + private var popup: PopupMenu? = null init { initializeWithIcon(R.drawable.ic_playback_speed) @@ -30,17 +31,20 @@ class PlaybackSpeedAction( view: View, ) { videoPlayerAdapter.leanbackOverlayFragment.setFading(false) - val speedMenu = populateMenu(context, view, speedController) + dismissPopup() + popup = populateMenu(context, view, speedController) - speedMenu.setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) } + popup?.setOnDismissListener { + videoPlayerAdapter.leanbackOverlayFragment.setFading(true) + popup = null + } - speedMenu.setOnMenuItemClickListener { menuItem -> + popup?.setOnMenuItemClickListener { menuItem -> speedController.currentSpeed = speeds[menuItem.itemId] - speedMenu.dismiss() true } - speedMenu.show() + popup?.show() } private fun populateMenu( @@ -57,4 +61,7 @@ class PlaybackSpeedAction( menu.getItem(speeds.indexOf(speedController.currentSpeed)).isChecked = true } + fun dismissPopup() { + popup?.dismiss() + } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectAudioAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectAudioAction.kt index 549a256bdb..9721dce1ab 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectAudioAction.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectAudioAction.kt @@ -8,7 +8,6 @@ import org.jellyfin.androidtv.R import org.jellyfin.androidtv.ui.playback.PlaybackController import org.jellyfin.androidtv.ui.playback.PlaybackManager import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue -import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment import org.jellyfin.androidtv.ui.playback.overlay.VideoPlayerAdapter class SelectAudioAction( @@ -16,6 +15,8 @@ class SelectAudioAction( customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue, private val playbackManager: PlaybackManager, ) : CustomAction(context, customPlaybackTransportControlGlue) { + private var popup: PopupMenu? = null + init { initializeWithIcon(R.drawable.ic_select_audio) } @@ -31,7 +32,8 @@ class SelectAudioAction( ?: return val currentAudioIndex = playbackController.audioStreamIndex - PopupMenu(context, view, Gravity.END).apply { + dismissPopup() + popup = PopupMenu(context, view, Gravity.END).apply { with(menu) { for (track in audioTracks) { add(0, track.index, track.index, track.displayTitle).apply { @@ -41,11 +43,19 @@ class SelectAudioAction( setGroupCheckable(0, true, false) } - setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) } + setOnDismissListener { + videoPlayerAdapter.leanbackOverlayFragment.setFading(true) + popup = null + } setOnMenuItemClickListener { item -> playbackController.switchAudioStream(item.itemId) true } - }.show() + } + popup?.show() + } + + fun dismissPopup() { + popup?.dismiss() } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt index 4303819ba3..bf61f401ea 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt @@ -20,6 +20,7 @@ class SelectQualityAction( private val previousQualitySelection = userPreferences[UserPreferences.maxBitrate] private val qualityController = VideoQualityController(previousQualitySelection, userPreferences) private val qualityProfiles = getQualityProfiles(context) + private var popup: PopupMenu? = null init { initializeWithIcon(R.drawable.ic_select_quality) @@ -32,7 +33,8 @@ class SelectQualityAction( view: View, ) { videoPlayerAdapter.leanbackOverlayFragment.setFading(false) - PopupMenu(context, view, Gravity.END).apply { + dismissPopup() + popup = PopupMenu(context, view, Gravity.END).apply { qualityProfiles.values.forEachIndexed { i, selected -> menu.add(0, i, i, selected) } @@ -45,13 +47,21 @@ class SelectQualityAction( } } - setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) } + setOnDismissListener { + videoPlayerAdapter.leanbackOverlayFragment.setFading(true) + popup = null + } setOnMenuItemClickListener { menuItem -> qualityController.currentQuality = qualityProfiles.keys.elementAt(menuItem.itemId) playbackController.refreshStream() dismiss() true } - }.show() + } + popup?.show() + } + + fun dismissPopup() { + popup?.dismiss() } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ZoomAction.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ZoomAction.kt index 291ef5e143..db304572f9 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ZoomAction.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/ZoomAction.kt @@ -3,6 +3,7 @@ package org.jellyfin.androidtv.ui.playback.overlay.action import android.content.Context import android.view.Gravity import android.view.View +import android.widget.PopupMenu import org.jellyfin.androidtv.R import org.jellyfin.androidtv.preference.constant.ZoomMode import org.jellyfin.androidtv.ui.playback.PlaybackController @@ -14,6 +15,8 @@ class ZoomAction( context: Context, customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue, ) : CustomAction(context, customPlaybackTransportControlGlue) { + private var popup: PopupMenu? = null + init { initializeWithIcon(R.drawable.ic_aspect_ratio) } @@ -25,7 +28,8 @@ class ZoomAction( view: View, ) { videoPlayerAdapter.leanbackOverlayFragment.setFading(false) - val popup = popupMenu(context, view, Gravity.END) { + dismissPopup() + popup = popupMenu(context, view, Gravity.END) { item(context.getString(R.string.lbl_fit)) { playbackController.setZoom(ZoomMode.FIT) }.apply { @@ -44,8 +48,15 @@ class ZoomAction( isChecked = playbackController.zoomMode == ZoomMode.STRETCH } } - popup.menu.setGroupCheckable(0, true, true) - popup.setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) } - popup.show() + popup?.menu?.setGroupCheckable(0, true, true) + popup?.setOnDismissListener { + videoPlayerAdapter.leanbackOverlayFragment.setFading(true) + popup = null + } + popup?.show() + } + + fun dismissPopup() { + popup?.dismiss() } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt index d477a202f9..25f4912119 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt @@ -31,7 +31,6 @@ import org.jellyfin.playback.jellyfin.queue.createBaseItemQueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaType -import kotlin.math.max @Suppress("TooManyFunctions") class RewriteMediaManager( @@ -136,25 +135,23 @@ class RewriteMediaManager( }.launchIn(this) playbackManager.queue.entry.onEach { updateAdapter() }.launchIn(this) + playbackManager.state.playbackOrder.onEach { updateAdapter() }.launchIn(this) } private fun updateAdapter() { - // Get all items as BaseRowItem - val items = queueSupplier - .items - // Map to audio queue items - .mapIndexed { index, item -> - AudioQueueBaseRowItem(item).apply { - playing = playbackManager.queue.entryIndex.value == index - } - } - // Remove items before currently playing item - .drop(max(0, playbackManager.queue.entryIndex.value)) + val currentItem = playbackManager.queue.entry.value?.baseItem?.let(::AudioQueueBaseRowItem)?.apply { + playing = true + } + // It's safe to run this blocking as all items are prefetched via the [BaseItemQueueSupplier] + val upcomingItems = runBlocking { playbackManager.queue.peekNext(100) } + .mapIndexedNotNull { index, item -> item.baseItem?.let(::AudioQueueBaseRowItem) } + + val items = listOfNotNull(currentItem) + upcomingItems // Update item row currentAudioQueue.replaceAll( items, - areItemsTheSame = { old, new -> (old as? AudioQueueBaseRowItem)?.baseItem == (new as? AudioQueueBaseRowItem)?.baseItem }, + areItemsTheSame = { old, new -> (old as? AudioQueueBaseRowItem)?.baseItem?.id == (new as? AudioQueueBaseRowItem)?.baseItem?.id }, // The equals functions for BaseRowItem only compare by id areContentsTheSame = { _, _ -> false }, ) @@ -269,7 +266,7 @@ class RewriteMediaManager( override fun togglePlayPause() { val playState = playbackManager.state.playState.value - if (playState == PlayState.PAUSED) playbackManager.state.unpause() + if (playState == PlayState.PAUSED || playState == PlayState.STOPPED) playbackManager.state.unpause() else if (playState == PlayState.PLAYING) playbackManager.state.pause() } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/search/SearchRepository.kt b/app/src/main/java/org/jellyfin/androidtv/ui/search/SearchRepository.kt index bede9080c0..051f5a9b18 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/search/SearchRepository.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/search/SearchRepository.kt @@ -1,11 +1,11 @@ package org.jellyfin.androidtv.ui.search +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.ApiClientException import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.request.GetItemsRequest import timber.log.Timber @@ -33,11 +33,7 @@ class SearchRepositoryImpl( limit = QUERY_LIMIT, imageTypeLimit = 1, includeItemTypes = itemTypes, - fields = listOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CAN_DELETE, - ItemFields.MEDIA_SOURCE_COUNT - ), + fields = ItemRepository.itemFields, recursive = true, enableTotalRecordCount = false, ) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/startup/fragment/ConnectHelpAlertFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/startup/fragment/ConnectHelpAlertFragment.kt index 26a89a830c..eadd2b9669 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/startup/fragment/ConnectHelpAlertFragment.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/startup/fragment/ConnectHelpAlertFragment.kt @@ -13,8 +13,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Done import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -87,7 +85,7 @@ private fun ConnectHelpAlert( ), ) { Icon( - imageVector = Icons.Default.Done, + painter = painterResource(R.drawable.ic_check), contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize), ) diff --git a/app/src/main/java/org/jellyfin/androidtv/util/sdk/SdkPlaybackHelper.kt b/app/src/main/java/org/jellyfin/androidtv/util/sdk/SdkPlaybackHelper.kt index 0ce99c506e..1f390d1bf5 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/sdk/SdkPlaybackHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/sdk/SdkPlaybackHelper.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.data.repository.ItemRepository import org.jellyfin.androidtv.preference.UserPreferences import org.jellyfin.androidtv.ui.navigation.NavigationRepository import org.jellyfin.androidtv.ui.playback.MediaManager @@ -24,7 +25,6 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.extensions.inWholeTicks @@ -83,16 +83,7 @@ class SdkPlaybackHelper( startItemId = mainItem.id, isMissing = false, limit = ITEM_QUERY_LIMIT, - fields = setOf( - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ItemFields.CHAPTERS, - ItemFields.PATH, - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ItemFields.TRICKPLAY, - ) + fields = ItemRepository.itemFields ) response.items @@ -113,16 +104,7 @@ class SdkPlaybackHelper( sortBy = if (shuffle) listOf(ItemSortBy.RANDOM) else listOf(ItemSortBy.SORT_NAME), recursive = true, limit = ITEM_QUERY_LIMIT, - fields = setOf( - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ItemFields.CHAPTERS, - ItemFields.PATH, - ItemFields.OVERVIEW, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT, - ItemFields.TRICKPLAY, - ) + fields = ItemRepository.itemFields ) response.items @@ -138,11 +120,7 @@ class SdkPlaybackHelper( ), recursive = true, limit = ITEM_QUERY_LIMIT, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.GENRES, - ItemFields.CHILD_COUNT - ), + fields = ItemRepository.itemFields, albumIds = listOf(mainItem.id) ) @@ -156,11 +134,7 @@ class SdkPlaybackHelper( sortBy = listOf(ItemSortBy.SORT_NAME), recursive = true, limit = ITEM_QUERY_LIMIT, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.GENRES, - ItemFields.CHILD_COUNT - ), + fields = ItemRepository.itemFields, artistIds = listOf(mainItem.id) ) @@ -174,14 +148,7 @@ class SdkPlaybackHelper( sortBy = if (shuffle) listOf(ItemSortBy.RANDOM) else null, recursive = true, limit = ITEM_QUERY_LIMIT, - fields = setOf( - ItemFields.MEDIA_SOURCES, - ItemFields.MEDIA_STREAMS, - ItemFields.CHAPTERS, - ItemFields.PATH, - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.CHILD_COUNT - ) + fields = ItemRepository.itemFields ) response.items @@ -278,11 +245,7 @@ class SdkPlaybackHelper( getScope(context).launch { val response by api.instantMixApi.getInstantMixFromItem( itemId = item.id, - fields = setOf( - ItemFields.PRIMARY_IMAGE_ASPECT_RATIO, - ItemFields.GENRES, - ItemFields.CHILD_COUNT - ) + fields = ItemRepository.itemFields ) val items = response.items diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000000..680932a65f --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_select_server.xml b/app/src/main/res/layout/fragment_select_server.xml index 5c70a1009e..b8994f4d43 100644 --- a/app/src/main/res/layout/fragment_select_server.xml +++ b/app/src/main/res/layout/fragment_select_server.xml @@ -70,7 +70,9 @@ + android:layout_height="wrap_content" + android:descendantFocusability="blocksDescendants" + android:focusable="false" /> "آخر عرض " إخراج ينتهي في - " دقيقة" مرات العرض SD شُوهد diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 12fab89921..790b92f90a 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -63,7 +63,6 @@ Visto SD Duración - " min" Termina Otras opciones Grabaciones diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 992593aed4..9097912913 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -51,7 +51,6 @@ Любімае Прагледжанае SD - " хв" Людзі Відэа Запісы diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 7ed1e1e3a0..2894314849 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -43,7 +43,6 @@ "Последно гледан " Режисиран от Приключва - " мин" Гледан Фаворит Сезони diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 6dce2eeb81..60dcd32cf0 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -48,7 +48,6 @@ বাতিল লোড হচ্ছে … ভিডিও লোড করতে ব্যর্থ - " সর্বনিম্ন" SD দেখা হয়েছে প্রিয় diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 4558cbbe63..fb6e8428a3 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -31,7 +31,6 @@ Preferències Capítols Reproduïr tots - " min" No Reprodueix tràiler(s) @@ -561,4 +560,20 @@ Color de traç dels subtítols Preferir FFmpeg per reproducció d\'àudio Utilitzar FFmpeg per a descodificar l\'àudio, encara que els còdecs de plataforma estiguin disponibles. + Configuració de zoom predeterminat + Salta + Introduccions + Previsualitzacions + Recapitulacions + Segments desconeguts + No facis res + Retard de l\'inici del vídeo + %1$dh %2$dm + %1$dm + Demanar per saltar + Publicitat + Crèdits + Habilitar previsualitzacions al reproductor + Longitud de salt cap endavant + Accions de segment de mitjans \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index aec18e5a22..14086aa778 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -49,7 +49,6 @@ Nadcházející SD Spuštěno - " min" Končí Režie "Naposledy přehráno " diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 4bd0836070..893504084c 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -48,7 +48,6 @@ "Geni " <Dim manylion> Gorffen - " mun" Yn rhedeg Penodau arbennig Golygfeydd diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 1942f44c0a..34470b7305 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -49,7 +49,6 @@ Kommende SD Spilletid - " min" Slutter Instrueret af "Sidst afspillet " diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2858958fe6..4d915a6f5d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -49,7 +49,6 @@ Als Nächstes SD Laufzeit - " min" Endet Regie "Zuletzt wiedergegeben " diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 17b97631ae..042776c838 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -75,7 +75,6 @@ Ανθρωποι "Προβλήθηκαν πρόσφατα " Τελειώνει - " λεπτά" Τρέχει SD Παρακολούθησα @@ -530,4 +529,6 @@ Τελευταία εβδομάδα Προγραμματισμένο για τις επόμενες 24 ώρες Ολες οι ηλικίες + %1$dω %2$dλ + %1$dλ \ No newline at end of file diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 3f461991b8..afe90eb79f 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -152,7 +152,6 @@ "Last played " Directed by Ends - " min" Runs SD Watched diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index d195389cab..35f031eea6 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -187,7 +187,6 @@ Ŝarĝas… Nekonata Fino - " min" Daŭro Aĵo Ĉapitroj diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index c79901f901..7f73485ac7 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -61,7 +61,6 @@ Personas Dirigido por Termina - " Min." Series Elemento Elementos diff --git a/app/src/main/res/values-es-rDO/strings.xml b/app/src/main/res/values-es-rDO/strings.xml index 614e540a20..165a080dbc 100644 --- a/app/src/main/res/values-es-rDO/strings.xml +++ b/app/src/main/res/values-es-rDO/strings.xml @@ -48,7 +48,6 @@ "Reproducido por última vez " Dirigido por Termina - " min" SD Visto Favorito diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 7780777012..0501de9d76 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -49,7 +49,6 @@ Por estrenar SD Duración - " min" Termina Dirigido por "Última reproducción " diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 72f82bc3b7..5f57e2bc11 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -43,7 +43,6 @@ Por Estrenar SD Duración - " min" Termina Dirigido Por "Ultimos vistos " diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 412ba40265..dad9890315 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -66,7 +66,6 @@ Horisontaalne Tulemas %1$,.0f Mbit/s - " -min" Lõppeb Videod Puudub diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 830e102688..b20517ad11 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -8,7 +8,6 @@ "آخرین پخش " به کارگردانیِ به پایان میرسد - " دقیقه" SD تماشا شده مورد علاقه diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 462d1258a6..b211d36d3a 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -69,7 +69,6 @@ Muut asetukset "Viimeksi toistettu " Päättyy - " min" SD Suosikki Tulossa @@ -565,4 +564,6 @@ %1$s Sarja %1$s Sarjat + %1$dt %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8b8b0e2da7..25c0c49735 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -49,7 +49,6 @@ À venir SD Durée - " min" Fin Réalisé par "Dernière lecture " @@ -575,4 +574,6 @@ Segments inconnus Activer trickplay dans le lecteur vidéo Longueur de l\'avance rapide + %1$dh %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 837946ccc2..6c063bb1d3 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -114,7 +114,6 @@ "נצפה לאחרונה " במאי נגמר - " דקות" הרצות SD נצפה @@ -580,4 +579,6 @@ בקש לדלג הפעלת טריקפליי בנגן הוידיאו גודל דילוג קדימה + %1$d שעות %2$d דקות + %1$d דקות \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8ac73463ae..5c51f3ef25 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -58,7 +58,6 @@ पसंदीदा देखा गया चलती है - " मिनट" समाप्त होगा निर्देशक SD diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index b2a5e11fbb..c320f6f141 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -16,7 +16,6 @@ "Posljednje izvedeno " Redatelj Završava - " min" SD Pogledano Omiljeno diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 531f594a37..31fc425fa4 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -22,7 +22,6 @@ "Utoljára lejátszott " Rendezte Végetér - " perc" SD Megtekintett Kedvenc @@ -565,4 +564,6 @@ Ismeretlen szegmensek Trickplay engedélyezése a videolejátszóban Előreugrás hossza + %1$dh %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index c848efd9a8..02fdbd164b 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -223,7 +223,6 @@ Orang Disutradarai oleh Berakhir - " mnt" Jalankan Vertikal Horisontal diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a862eea86c..1610afbbf3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -43,7 +43,6 @@ In Arrivo SD Dura - " min" Finisce Diretto da "Riprodotto l\'ultima volta " @@ -575,4 +574,6 @@ Salta Conclusioni Anteprime + %1$dh %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f00f0624eb..a026b43216 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -52,7 +52,6 @@ お気に入り 視聴済み SD - " 分" 監督 "最終再生 " その他オプション diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index a84f62c007..21eb70e25e 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -49,7 +49,6 @@ Kütılude SD Ūzaqtyğy - " min" Aiaqtaluy Rejisörı "Eñ keiıngı oinatylğan " diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ac95bdff6e..26ebff6895 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,7 +43,6 @@ 즐겨찾기 시청완료 SD - " 분" "마지막으로 재생 " 다른 옵션 사람들 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 8ea0d9aaee..aeab2bbe9d 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -86,7 +86,6 @@ Tuščia Kitos parinktys Režisierius - " min" Trukmė Elementas Elementai diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index e5fa538c22..7180060e34 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -129,7 +129,6 @@ "Pēdējoreiz Atskaņots " Garums Beidzas - " min" SD Skatītie Favorīti diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 7d3f07c703..dfe9d6d79d 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -76,7 +76,6 @@ Особи Други поставки Режирано од - " мин" СД Гледано Паузирај diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index fb892e3165..3dc5c48935 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,7 +280,6 @@ "അവസാനം കളിച്ചത് " സംവിധാനം ചെയ്തത് അവസാനിക്കുന്നു - " മിനിറ്റ്" റൺസ് എസ്ഡി കണ്ടു diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 5fdef2bcf7..f843cb3f81 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -49,7 +49,6 @@ Kommer SD Pågår - " minutter" Slutter Regissert av "Sist avspilt " @@ -561,4 +560,10 @@ Forhåndsvisninger Oppsummering Ukjente segmenter + Slå på trickplay i videospilleren + Standard zoom + Spør om å spole frem + Lengde det spoles frem med + %1$dt %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b70949b4a9..0bcdb16309 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -48,8 +48,7 @@ Seizoenen Binnenkort SD - Duur - " min" + Speelduur Afgelopen Regie "Laatst afgespeeld " @@ -565,4 +564,6 @@ Standaard zoommodus Trickplay in videospeler inschakelen Stapgrootte vooruitspringen + %1$du %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 66f4d451a6..14d10e8655 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -75,7 +75,6 @@ Administrator ograniczył odtwarzanie danego elementu Odtwarzanie nie dozwolone "Urodzony/a " - " min" SD " niezaimplementowane" Nadchodzące @@ -585,4 +584,6 @@ Domyślny tryb powiększenia Włącz trickplay w odtwarzaczu wideo Długość pomijania do przodu + %1$dh %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 01e31bd1ec..237254732a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -49,7 +49,6 @@ A Seguir SD Duração - " min" Finaliza às Dirigido por "Reproduzidos recentemente " diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 70bd28ffbd..6132bce0c2 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -50,7 +50,6 @@ Próximos SD Duração - " min" Termina às Realizado por "Reproduzido anteriormente " @@ -575,4 +574,6 @@ Créditos Ativar trickplay no leitor de vídeo Avançar reprodução (segundos) + %1$dh %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index c39ff6ae0b..a5ae484d16 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -34,7 +34,6 @@ "Ultimul redat " Regizat de Termină - " Minim" Rulează SD Vizionat diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1e2ea57a08..a09403f8d9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -50,7 +50,6 @@ Ожидаемое SD Длится - " мин" Конец Режиссёр "Последнее воспроизведение " diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 0df2383673..6660581e13 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -82,7 +82,6 @@ Ostatné možnosti Réžia Končí o - " min" Dĺžka SD Obľúbené @@ -575,4 +574,6 @@ Predvolený režim priblíženia Povoliť trickplay vo video prehrávači Dĺžka skoku dopredu + %1$dh %2$dm + %1$dm \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 561988799b..58ac4a35d0 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -29,7 +29,6 @@ "Zadnje predvajano " Režija Konec - " min" Teče SD Ogledano diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 6ce70651e1..deed820e04 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -4,7 +4,6 @@ "Последње гледано " Режија завршава се у - " мин" У току Гледано Омиљено diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index bf5f5b6afb..790554742b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -170,7 +170,6 @@ "Senast sedd " Regisserad av Slutar - " min" Speltid SD Sett diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 5b2b482878..092b52f52f 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -48,7 +48,6 @@ "கடைசியாக இயக்கியது " இயக்கம் முடிகிறது - " நிமிடம்" இயங்கும் SD பார்த்தது diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 6db9d07988..adccda4eb6 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -19,7 +19,6 @@ "เล่นล่าสุด " กำกับโดย จบ - " นาที" SD ดูแล้ว รายการโปรด @@ -218,4 +217,11 @@ ชมพู ฟ้า เหลือง + ตรง + รัน + %1$dh %2$dm + %1$dm + เริ่มการเติม + รันไทม์ + สลับสถานะการสุ่ม \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8783881d18..507983421c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -133,7 +133,6 @@ "En son oynatılan " Yönetmen Bitiş Zamanı - " dk" Süre SD İzlendi diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 9d585e6ec1..0694369315 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -64,7 +64,6 @@ كۆرگەنلىرىم نورمال ۋاقتى - " مىنۇت" ئاخىرلاشتۇرۇش رېژىسسور "ئالدىنقى قېتىم قويۇلغىنى " diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0caab3c477..2a2e027230 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -98,7 +98,6 @@ "Останнє відтворене " Режисер Завершується - " хв" Триває SD Обране diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 702c4a64b9..b0c3d3317f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -249,7 +249,6 @@ Nền được mã hóa màu Luồng phát TV trực tiếp Album - " phút" trong %1$d giờ tiếp theo %1$d trong số %2$d kênh Chọn bản phụ đề diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index de998bfdd9..e3088bcd52 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -43,7 +43,6 @@ 即将上映 标清 时长 - " 分钟" 结束 导演 "上次播放 " diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e786bca299..d6cccf9956 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -141,7 +141,6 @@ 搜尋文字(選擇鍵盤) 自動播放下一集 結束 - " 分鐘" 項目 項目 特典 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86e432b118..84ab1f7be7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,7 +64,8 @@ Watched SD "Runs" - " min" + %1$dh %2$dm + %1$dm "Ends" "Directed by" "Last played " diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8397c7a605..eb916ec872 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ aboutlibraries = "11.2.3" acra = "5.12.0" android-compileSdk = "35" -android-desugar = "2.1.3" +android-desugar = "2.1.4" android-gradle = "8.7.3" android-minSdk = "21" android-targetSdk = "35" @@ -16,7 +16,7 @@ androidx-core = "1.15.0" androidx-fragment = "1.8.5" androidx-leanback = "1.1.0-rc01" androidx-lifecycle = "2.8.7" -androidx-media3 = "1.5.0" +androidx-media3 = "1.5.1" androidx-preference = "1.2.1" androidx-recyclerview = "1.3.2" androidx-startup = "1.2.0" @@ -30,14 +30,14 @@ java-jdk = "21" jellyfin-androidx-media = "1.5.0+1" jellyfin-apiclient = "v0.7.10" jellyfin-sdk = "1.6.3" -koin = "4.0.0" -koin-compose = "4.0.0" +koin = "4.0.1" +koin-compose = "4.0.1" kotest = "5.9.1" kotlin = "2.0.21" kotlinx-coroutines = "1.9.0" kotlinx-serialization = "1.7.3" markwon = "4.6.2" -mockk = "1.13.13" +mockk = "1.13.14" slf4j-timber = "0.0.4" timber = "5.0.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eb1a55be0e..e1b837a19c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6b..f3b75f3b0d 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/playback/core/src/main/kotlin/PlayerState.kt b/playback/core/src/main/kotlin/PlayerState.kt index 17e160b771..cd340c1ab4 100644 --- a/playback/core/src/main/kotlin/PlayerState.kt +++ b/playback/core/src/main/kotlin/PlayerState.kt @@ -88,7 +88,13 @@ class MutablePlayerState( _videoSize.value = VideoSize(width, height) } - override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) = Unit + override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) { + // Make sure to start stream again if repeat mode is turned on + // Note: the QueueService is responsible for changing REPEAT_ENTRY_ONCE to NONE + if (_repeatMode.value != RepeatMode.NONE) { + backendService.backend?.play() + } + } }) volume = options.playerVolumeState diff --git a/playback/core/src/main/kotlin/queue/QueueService.kt b/playback/core/src/main/kotlin/queue/QueueService.kt index 7961cdc419..e6b9e067cd 100644 --- a/playback/core/src/main/kotlin/queue/QueueService.kt +++ b/playback/core/src/main/kotlin/queue/QueueService.kt @@ -113,14 +113,14 @@ class QueueService internal constructor() : PlayerService(), Queue { val repeatMode = if (useRepeatMode) state.repeatMode.value else RepeatMode.NONE return when (repeatMode) { - RepeatMode.NONE -> provider.provideIndices(amount, estimatedSize, currentQueueIndicesPlayed, entryIndex.value) + RepeatMode.NONE -> provider.provideIndices(amount, estimatedSize, currentQueueIndicesPlayed, _entryIndex.value) - RepeatMode.REPEAT_ENTRY_ONCE -> buildList { - add(entryIndex.value) - addAll(provider.provideIndices(amount, estimatedSize, currentQueueIndicesPlayed, entryIndex.value)) + RepeatMode.REPEAT_ENTRY_ONCE -> buildList(amount) { + add(_entryIndex.value) + addAll(provider.provideIndices(amount - 1, estimatedSize, currentQueueIndicesPlayed, _entryIndex.value)) }.take(amount) - RepeatMode.REPEAT_ENTRY_INFINITE -> List(amount) { entryIndex.value } + RepeatMode.REPEAT_ENTRY_INFINITE -> List(amount) { _entryIndex.value } } } @@ -132,13 +132,15 @@ class QueueService internal constructor() : PlayerService(), Queue { override suspend fun next(usePlaybackOrder: Boolean, useRepeatMode: Boolean): QueueEntry? { val index = getNextIndices(1, usePlaybackOrder, useRepeatMode).firstOrNull() ?: return null - if (usePlaybackOrder) { - // Automatically set repeat mode back to none when using the ONCE option - if (state.repeatMode.value == RepeatMode.REPEAT_ENTRY_ONCE && index == this._entryIndex.value) { - state.setRepeatMode(RepeatMode.NONE) - } else if (state.repeatMode.value == RepeatMode.NONE) { - orderIndexProvider.useNextIndex() - } + + val provider = if (usePlaybackOrder) orderIndexProvider else defaultOrderIndexProvider + val repeatMode = if (useRepeatMode) state.repeatMode.value else RepeatMode.NONE + + // Automatically set repeat mode back to none when using the ONCE option + if (repeatMode == RepeatMode.REPEAT_ENTRY_ONCE && index == this._entryIndex.value) { + state.setRepeatMode(RepeatMode.NONE) + } else if (repeatMode == RepeatMode.NONE) { + provider.useNextIndex() } return setIndex(index, true) diff --git a/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt b/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt index 2452d43be5..0fa261ec66 100644 --- a/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt +++ b/playback/core/src/main/kotlin/queue/order/RandomOrderIndexProvider.kt @@ -23,6 +23,6 @@ internal class RandomOrderIndexProvider : OrderIndexProvider { } override fun useNextIndex() { - nextIndices.remove(0) + nextIndices.removeAt(0) } } diff --git a/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt b/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt index f6e8b430c9..8a3d2ca52e 100644 --- a/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt +++ b/playback/core/src/main/kotlin/queue/order/ShuffleOrderIndexProvider.kt @@ -34,6 +34,6 @@ internal class ShuffleOrderIndexProvider : OrderIndexProvider { } override fun useNextIndex() { - nextIndices.remove(0) + nextIndices.removeAt(0) } } diff --git a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt index 4ecedaae28..929009fbdd 100644 --- a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt +++ b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt @@ -21,6 +21,7 @@ import org.jellyfin.sdk.model.api.PlaybackStartInfo import org.jellyfin.sdk.model.api.PlaybackStopInfo import org.jellyfin.sdk.model.api.QueueItem import org.jellyfin.sdk.model.extensions.inWholeTicks +import timber.log.Timber import kotlin.math.roundToInt import org.jellyfin.sdk.model.api.RepeatMode as SdkRepeatMode @@ -70,27 +71,29 @@ class PlaySessionService( val stream = entry.mediaStream ?: return val item = entry.baseItem ?: return - api.playStateApi.reportPlaybackStart( - PlaybackStartInfo( - itemId = item.id, - playSessionId = stream.identifier, - playlistItemId = item.playlistItemId, - canSeek = true, - isMuted = state.volume.muted, - volumeLevel = (state.volume.volume * 100).roundToInt(), - isPaused = state.playState.value != PlayState.PLAYING, - aspectRatio = state.videoSize.value.aspectRatio.toString(), - positionTicks = withContext(Dispatchers.Main) { state.positionInfo.active.inWholeTicks }, - playMethod = stream.conversionMethod.playMethod, - repeatMode = state.repeatMode.value.remoteRepeatMode, - nowPlayingQueue = getQueue(), - playbackOrder = when (state.playbackOrder.value) { - org.jellyfin.playback.core.model.PlaybackOrder.DEFAULT -> PlaybackOrder.DEFAULT - org.jellyfin.playback.core.model.PlaybackOrder.RANDOM -> PlaybackOrder.SHUFFLE - org.jellyfin.playback.core.model.PlaybackOrder.SHUFFLE -> PlaybackOrder.SHUFFLE - } + runCatching { + api.playStateApi.reportPlaybackStart( + PlaybackStartInfo( + itemId = item.id, + playSessionId = stream.identifier, + playlistItemId = item.playlistItemId, + canSeek = true, + isMuted = state.volume.muted, + volumeLevel = (state.volume.volume * 100).roundToInt(), + isPaused = state.playState.value != PlayState.PLAYING, + aspectRatio = state.videoSize.value.aspectRatio.toString(), + positionTicks = withContext(Dispatchers.Main) { state.positionInfo.active.inWholeTicks }, + playMethod = stream.conversionMethod.playMethod, + repeatMode = state.repeatMode.value.remoteRepeatMode, + nowPlayingQueue = getQueue(), + playbackOrder = when (state.playbackOrder.value) { + org.jellyfin.playback.core.model.PlaybackOrder.DEFAULT -> PlaybackOrder.DEFAULT + org.jellyfin.playback.core.model.PlaybackOrder.RANDOM -> PlaybackOrder.SHUFFLE + org.jellyfin.playback.core.model.PlaybackOrder.SHUFFLE -> PlaybackOrder.SHUFFLE + } + ) ) - ) + }.onFailure { error -> Timber.w("Failed to send playback start event", error) } } private suspend fun sendStreamUpdate() { @@ -98,27 +101,29 @@ class PlaySessionService( val stream = entry.mediaStream ?: return val item = entry.baseItem ?: return - api.playStateApi.reportPlaybackProgress( - PlaybackProgressInfo( - itemId = item.id, - playSessionId = stream.identifier, - playlistItemId = item.playlistItemId, - canSeek = true, - isMuted = state.volume.muted, - volumeLevel = (state.volume.volume * 100).roundToInt(), - isPaused = state.playState.value != PlayState.PLAYING, - aspectRatio = state.videoSize.value.aspectRatio.toString(), - positionTicks = withContext(Dispatchers.Main) { state.positionInfo.active.inWholeTicks }, - playMethod = stream.conversionMethod.playMethod, - repeatMode = state.repeatMode.value.remoteRepeatMode, - nowPlayingQueue = getQueue(), - playbackOrder = when (state.playbackOrder.value) { - org.jellyfin.playback.core.model.PlaybackOrder.DEFAULT -> PlaybackOrder.DEFAULT - org.jellyfin.playback.core.model.PlaybackOrder.RANDOM -> PlaybackOrder.SHUFFLE - org.jellyfin.playback.core.model.PlaybackOrder.SHUFFLE -> PlaybackOrder.SHUFFLE - } + runCatching { + api.playStateApi.reportPlaybackProgress( + PlaybackProgressInfo( + itemId = item.id, + playSessionId = stream.identifier, + playlistItemId = item.playlistItemId, + canSeek = true, + isMuted = state.volume.muted, + volumeLevel = (state.volume.volume * 100).roundToInt(), + isPaused = state.playState.value != PlayState.PLAYING, + aspectRatio = state.videoSize.value.aspectRatio.toString(), + positionTicks = withContext(Dispatchers.Main) { state.positionInfo.active.inWholeTicks }, + playMethod = stream.conversionMethod.playMethod, + repeatMode = state.repeatMode.value.remoteRepeatMode, + nowPlayingQueue = getQueue(), + playbackOrder = when (state.playbackOrder.value) { + org.jellyfin.playback.core.model.PlaybackOrder.DEFAULT -> PlaybackOrder.DEFAULT + org.jellyfin.playback.core.model.PlaybackOrder.RANDOM -> PlaybackOrder.SHUFFLE + org.jellyfin.playback.core.model.PlaybackOrder.SHUFFLE -> PlaybackOrder.SHUFFLE + } + ) ) - ) + }.onFailure { error -> Timber.w("Failed to send playback update event", error) } } private suspend fun sendStreamStop() { @@ -126,15 +131,17 @@ class PlaySessionService( val stream = entry.mediaStream ?: return val item = entry.baseItem ?: return - api.playStateApi.reportPlaybackStopped( - PlaybackStopInfo( - itemId = item.id, - playSessionId = stream.identifier, - playlistItemId = item.playlistItemId, - positionTicks = withContext(Dispatchers.Main) { state.positionInfo.active.inWholeTicks }, - failed = false, - nowPlayingQueue = getQueue(), + runCatching { + api.playStateApi.reportPlaybackStopped( + PlaybackStopInfo( + itemId = item.id, + playSessionId = stream.identifier, + playlistItemId = item.playlistItemId, + positionTicks = withContext(Dispatchers.Main) { state.positionInfo.active.inWholeTicks }, + failed = false, + nowPlayingQueue = getQueue(), + ) ) - ) + }.onFailure { error -> Timber.w("Failed to send playback stop event", error) } } } diff --git a/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt b/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt index 68b8f905e7..0f0343601a 100644 --- a/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt +++ b/playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt @@ -5,6 +5,7 @@ import android.content.Context import android.view.ViewGroup import androidx.annotation.OptIn import androidx.core.content.getSystemService +import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException @@ -101,6 +102,9 @@ class ExoPlayerBackend( setConstantBitrateSeekingAlwaysEnabled(true) } )) + .setAudioAttributes(AudioAttributes.Builder().apply { + setUsage(C.USAGE_MEDIA) + }.build(), true) .setPauseAtEndOfMediaItems(true) .build() .also { player -> @@ -209,6 +213,8 @@ class ExoPlayerBackend( } override fun play() { + // If the item has ended, revert first so the item will start over again + if (exoPlayer.playbackState == Player.STATE_ENDED) exoPlayer.seekTo(0) exoPlayer.play() } diff --git a/playback/media3/exoplayer/src/main/kotlin/mapping/audio.kt b/playback/media3/exoplayer/src/main/kotlin/mapping/audio.kt index 39a6a20213..3bd1b08860 100644 --- a/playback/media3/exoplayer/src/main/kotlin/mapping/audio.kt +++ b/playback/media3/exoplayer/src/main/kotlin/mapping/audio.kt @@ -5,10 +5,10 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @OptIn(UnstableApi::class) -fun getFfmpegAudioMimeType(codec: String) = codec.lowercase().let { codec -> +fun getFfmpegAudioMimeType(codec: String, fallback: String = codec) = codec.lowercase().let { codec -> ffmpegAudioMimeTypes[codec] ?: MimeTypes.getAudioMediaMimeType(codec) - ?: codec + ?: fallback } val ffmpegAudioMimeTypes = mapOf( diff --git a/playback/media3/exoplayer/src/main/kotlin/mapping/container.kt b/playback/media3/exoplayer/src/main/kotlin/mapping/container.kt index f3dd497b90..cc4898248e 100644 --- a/playback/media3/exoplayer/src/main/kotlin/mapping/container.kt +++ b/playback/media3/exoplayer/src/main/kotlin/mapping/container.kt @@ -5,12 +5,12 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @OptIn(UnstableApi::class) -fun getFfmpegContainerMimeType(codec: String) = codec.lowercase().let { codec -> +fun getFfmpegContainerMimeType(codec: String, fallback: String = codec) = codec.lowercase().let { codec -> ffmpegContainerMimeTypes[codec] ?: ffmpegVideoMimeTypes[codec] ?: ffmpegAudioMimeTypes[codec] ?: MimeTypes.getMediaMimeType(codec) - ?: codec + ?: fallback } @OptIn(UnstableApi::class) diff --git a/playback/media3/exoplayer/src/main/kotlin/mapping/subtitle.kt b/playback/media3/exoplayer/src/main/kotlin/mapping/subtitle.kt index 7bd18aa635..4479e8bc6a 100644 --- a/playback/media3/exoplayer/src/main/kotlin/mapping/subtitle.kt +++ b/playback/media3/exoplayer/src/main/kotlin/mapping/subtitle.kt @@ -5,10 +5,10 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @OptIn(UnstableApi::class) -fun getFfmpegSubtitleMimeType(codec: String): String = codec.lowercase().let { codec -> +fun getFfmpegSubtitleMimeType(codec: String, fallback: String = codec): String = codec.lowercase().let { codec -> ffmpegSubtitleMimeTypes[codec] ?: MimeTypes.getTextMediaMimeType(codec) - ?: codec + ?: fallback } @OptIn(UnstableApi::class) diff --git a/playback/media3/exoplayer/src/main/kotlin/mapping/video.kt b/playback/media3/exoplayer/src/main/kotlin/mapping/video.kt index d36e91b7cb..0c0c3d3e70 100644 --- a/playback/media3/exoplayer/src/main/kotlin/mapping/video.kt +++ b/playback/media3/exoplayer/src/main/kotlin/mapping/video.kt @@ -5,10 +5,10 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi @OptIn(UnstableApi::class) -fun getFfmpegVideoMimeType(codec: String) = codec.lowercase().let { codec -> +fun getFfmpegVideoMimeType(codec: String, fallback: String = codec) = codec.lowercase().let { codec -> ffmpegVideoMimeTypes[codec] ?: MimeTypes.getVideoMediaMimeType(codec) - ?: codec + ?: fallback } @OptIn(UnstableApi::class)