diff --git a/.github/workflows/app-build.yaml b/.github/workflows/app-build.yaml index 8aec4624cb..49311865af 100644 --- a/.github/workflows/app-build.yaml +++ b/.github/workflows/app-build.yaml @@ -12,12 +12,12 @@ permissions: jobs: build: name: Build - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Java - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/app-lint.yaml b/.github/workflows/app-lint.yaml index e5f81a9943..441df45907 100644 --- a/.github/workflows/app-lint.yaml +++ b/.github/workflows/app-lint.yaml @@ -14,12 +14,12 @@ permissions: jobs: lint: name: Lint - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Java - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: distribution: temurin java-version: 17 @@ -28,7 +28,7 @@ jobs: - name: Run detekt and lint tasks run: ./gradlew detekt lint - name: Upload SARIF files - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 if: ${{ always() }} with: sarif_file: . diff --git a/.github/workflows/app-publish.yaml b/.github/workflows/app-publish.yaml index 761f3d0b92..c5db2e7557 100644 --- a/.github/workflows/app-publish.yaml +++ b/.github/workflows/app-publish.yaml @@ -8,13 +8,13 @@ on: jobs: publish: name: Publish - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ contains(github.repository_owner, 'jellyfin') }} steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Java - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/app-test.yaml b/.github/workflows/app-test.yaml index b3219619fe..f736450240 100644 --- a/.github/workflows/app-test.yaml +++ b/.github/workflows/app-test.yaml @@ -13,12 +13,12 @@ permissions: jobs: test: name: Test - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Setup Java - uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/gradlew-validate.yaml b/.github/workflows/gradlew-validate.yaml index 6824232e32..2386ea40df 100644 --- a/.github/workflows/gradlew-validate.yaml +++ b/.github/workflows/gradlew-validate.yaml @@ -14,9 +14,9 @@ permissions: jobs: validate: name: Validate - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 diff --git a/.github/workflows/repo-merge-conflict.yaml b/.github/workflows/repo-merge-conflict.yaml index 4621c79bc5..3e9143f32c 100644 --- a/.github/workflows/repo-merge-conflict.yaml +++ b/.github/workflows/repo-merge-conflict.yaml @@ -9,7 +9,7 @@ on: jobs: triage: name: Triage - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ contains(github.repository_owner, 'jellyfin') }} steps: - uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2 diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index 45e34c325e..0024694531 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -12,7 +12,7 @@ permissions: jobs: triage: name: Triage - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ contains(github.repository_owner, 'jellyfin') }} steps: - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 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 df90ad9e7f..b278cc3931 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 @@ -61,7 +61,7 @@ class MainActivity : FragmentActivity() { if (savedInstanceState == null && navigationRepository.canGoBack) navigationRepository.reset(clearHistory = true) navigationRepository.currentAction - .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .onEach { action -> handleNavigationAction(action) backPressedCallback.isEnabled = navigationRepository.canGoBack diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java index b647883ece..5db4b5a63b 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java @@ -125,7 +125,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (data.hasExtra(API_MX_RESULT_POSITION)) { pos = data.getIntExtra(API_MX_RESULT_POSITION, 0); } else if (data.hasExtra(API_VLC_RESULT_POSITION)) { - pos = data.getIntExtra(API_VLC_RESULT_POSITION, 0); + pos = (int) data.getLongExtra(API_VLC_RESULT_POSITION, 0); } else if (data.hasExtra(API_VIMU_RESULT_POSITION)) { pos = data.getIntExtra(API_VIMU_RESULT_POSITION, 0); } diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 50249341ed..98a65cc57c 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -77,7 +77,7 @@ Dette elementet kunne ikke bli avspilt Ingen elementer Tom - Legg til alle påfølgende episoder i køen + Spill av neste episode automatisk Søk tekst (velg for tastatur) Spill av første som ikke er sett Merk som usett @@ -407,7 +407,7 @@ Denne appen er optimalisert for TV-er. Vi anbefaler å bruke mobilappen vår på andre enheter. Denne serveren bruker Jellyfin versjon %1$s, som ikke støttes. Oppdater til Jellyfin %2$s eller nyere for å fortsette å bruke appen. App oppdatert til %1$s -\nTakk for at du deltar i Jellyfin beta-programmet +\nTakk for at du deltar i Jellyfin beta-programmet. Bruk skjermsparer i appen Aktiver reaktiv hjemmeside Vis Jellyfin-skjermspareren mens appen er åpen @@ -539,4 +539,21 @@ Kun vis elementer med en aldersgrense Viser alle elementer Alle kanaler + Bakgrunnsfarge for undertekst + Oppdater tjeneren fra %1$s til minst %2$s for å fortsette å bruke appen etter neste oppdatering. + Foretrekk FFmpeg for lydavspilling + Bruk FFmpeg for å dekode lyd, selv om plattformkodeker er tilgjengelige. + Strekfarge for undertekst + Farge for undertekst + Veldig liten + Liten + Normal + Stor + Veldig stor + Aktuell kø + Forrige uke + Bruk ekstern avspiller + Fortsett å lytte + Siste 24 timene + Planlagt i løpet av de neste 24 timene \ No newline at end of file diff --git a/playback/core/src/main/kotlin/element/ElementsContainer.kt b/playback/core/src/main/kotlin/element/ElementsContainer.kt index b5740de950..9731da6808 100644 --- a/playback/core/src/main/kotlin/element/ElementsContainer.kt +++ b/playback/core/src/main/kotlin/element/ElementsContainer.kt @@ -1,5 +1,10 @@ package org.jellyfin.playback.core.element +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import java.util.concurrent.ConcurrentHashMap /** @@ -7,6 +12,11 @@ import java.util.concurrent.ConcurrentHashMap */ open class ElementsContainer { private val elements = ConcurrentHashMap, Any?>() + private val updateFlow = MutableSharedFlow>( + replay = 1, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) fun get(key: ElementKey): T = getOrNull(key) ?: error("No element found for key $key.") @@ -18,9 +28,17 @@ open class ElementsContainer { fun put(key: ElementKey, value: T) { elements[key] = value + updateFlow.tryEmit(key) } fun remove(key: ElementKey) { elements.remove(key) + updateFlow.tryEmit(key) + } + + fun getFlow(key: ElementKey): Flow { + return updateFlow + .map { getOrNull(key) } + .distinctUntilChanged() } } diff --git a/playback/core/src/main/kotlin/element/delegates.kt b/playback/core/src/main/kotlin/element/delegates.kt index 967d9a797c..0e5733be0c 100644 --- a/playback/core/src/main/kotlin/element/delegates.kt +++ b/playback/core/src/main/kotlin/element/delegates.kt @@ -1,5 +1,7 @@ package org.jellyfin.playback.core.element +import kotlinx.coroutines.flow.Flow +import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -38,3 +40,10 @@ fun requiredElement( thisRef.put(key, value) } } + +/** + * Delegate for the flow of an element. + */ +fun elementFlow( + key: ElementKey, +) = ReadOnlyProperty> { thisRef, _ -> thisRef.getFlow(key) } diff --git a/playback/core/src/main/kotlin/mediastream/QueueEntryMediaStream.kt b/playback/core/src/main/kotlin/mediastream/QueueEntryMediaStream.kt index d15974e068..5acaefcfe6 100644 --- a/playback/core/src/main/kotlin/mediastream/QueueEntryMediaStream.kt +++ b/playback/core/src/main/kotlin/mediastream/QueueEntryMediaStream.kt @@ -2,6 +2,7 @@ package org.jellyfin.playback.core.mediastream import org.jellyfin.playback.core.element.ElementKey import org.jellyfin.playback.core.element.element +import org.jellyfin.playback.core.element.elementFlow import org.jellyfin.playback.core.queue.QueueEntry private val mediaStreamKey = ElementKey("MediaStream") @@ -10,3 +11,8 @@ private val mediaStreamKey = ElementKey("MediaStream") * Get or set the [MediaStream] for this [QueueEntry]. */ var QueueEntry.mediaStream by element(mediaStreamKey) + +/** + * Get the [MediaStream] flow for this [QueueEntry]. + */ +val QueueEntry.mediaStreamFlow by elementFlow(mediaStreamKey) diff --git a/playback/core/src/main/kotlin/mediastream/normalizationGainElement.kt b/playback/core/src/main/kotlin/mediastream/normalizationGainElement.kt index aa460ee8fc..e9098e557f 100644 --- a/playback/core/src/main/kotlin/mediastream/normalizationGainElement.kt +++ b/playback/core/src/main/kotlin/mediastream/normalizationGainElement.kt @@ -2,6 +2,7 @@ package org.jellyfin.playback.core.mediastream import org.jellyfin.playback.core.element.ElementKey import org.jellyfin.playback.core.element.element +import org.jellyfin.playback.core.element.elementFlow import org.jellyfin.playback.core.queue.QueueEntry private val normalizationGainKey = ElementKey("NormalizationGain") @@ -11,3 +12,9 @@ private val normalizationGainKey = ElementKey("NormalizationGain") * apply a gain to the audio output. The normalization gain must target a loudness of -23LUFS. */ var QueueEntry.normalizationGain by element(normalizationGainKey) + +/** + * Get the flow of [normalizationGain]. + * @see normalizationGain + */ +val QueueEntry.normalizationGainFlow by elementFlow(normalizationGainKey) diff --git a/playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt b/playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt index 6380c47f12..5a4753776f 100644 --- a/playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt +++ b/playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt @@ -1,6 +1,7 @@ package org.jellyfin.playback.core.queue import org.jellyfin.playback.core.element.ElementKey +import org.jellyfin.playback.core.element.elementFlow import org.jellyfin.playback.core.element.requiredElement import java.time.LocalDate import kotlin.time.Duration @@ -42,3 +43,9 @@ private val metadataKey = ElementKey("QueueEntryMetadata") * Get or set the [QueueEntryMetadata] for this [QueueEntry]. Defaults to [QueueEntryMetadata.Empty]. */ var QueueEntry.metadata by requiredElement(metadataKey) { QueueEntryMetadata.Empty } + +/** + * Get the flow of [metadata]. + * @see metadata + */ +val QueueEntry.metadataFlow by elementFlow(metadataKey) diff --git a/playback/core/src/main/kotlin/queue/QueueService.kt b/playback/core/src/main/kotlin/queue/QueueService.kt index aae04da267..1c4a3e7a77 100644 --- a/playback/core/src/main/kotlin/queue/QueueService.kt +++ b/playback/core/src/main/kotlin/queue/QueueService.kt @@ -7,6 +7,9 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.jellyfin.playback.core.PlaybackManager +import org.jellyfin.playback.core.backend.PlayerBackendEventListener +import org.jellyfin.playback.core.mediastream.PlayableMediaStream +import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.plugin.PlayerService @@ -44,6 +47,17 @@ class QueueService internal constructor() : PlayerService(), Queue { PlaybackOrder.SHUFFLE -> ShuffleOrderIndexProvider() } }.launchIn(coroutineScope) + + // Automatically advance when current stream ends + manager.backendService.addListener(object : PlayerBackendEventListener { + override fun onPlayStateChange(state: PlayState) = Unit + override fun onVideoSizeChange(width: Int, height: Int) = Unit + override fun onMediaStreamEnd(mediaStream: PlayableMediaStream) { + coroutineScope.launch { + next(usePlaybackOrder = true, useRepeatMode = true) + } + } + }) } // Entry management diff --git a/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt b/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt index 4d43226b86..e4a8fab9b1 100644 --- a/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt +++ b/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt @@ -2,6 +2,7 @@ package org.jellyfin.playback.jellyfin.queue import org.jellyfin.playback.core.element.ElementKey import org.jellyfin.playback.core.element.element +import org.jellyfin.playback.core.element.elementFlow import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.sdk.model.api.BaseItemDto @@ -11,3 +12,8 @@ private val baseItemKey = ElementKey("BaseItemDto") * Get or set the [BaseItemDto] for this [QueueEntry]. */ var QueueEntry.baseItem by element(baseItemKey) + +/** + * Get the [BaseItemDto] flow for this [QueueEntry]. + */ +val QueueEntry.baseItemFlow by elementFlow(baseItemKey) diff --git a/playback/jellyfin/src/main/kotlin/queue/mediaSourceIdElement.kt b/playback/jellyfin/src/main/kotlin/queue/mediaSourceIdElement.kt index a3c1b68e8b..a5bfcab0bf 100644 --- a/playback/jellyfin/src/main/kotlin/queue/mediaSourceIdElement.kt +++ b/playback/jellyfin/src/main/kotlin/queue/mediaSourceIdElement.kt @@ -2,6 +2,7 @@ package org.jellyfin.playback.jellyfin.queue import org.jellyfin.playback.core.element.ElementKey import org.jellyfin.playback.core.element.element +import org.jellyfin.playback.core.element.elementFlow import org.jellyfin.playback.core.queue.QueueEntry private val mediaSourceIdKey = ElementKey("MediaSource") @@ -11,3 +12,9 @@ private val mediaSourceIdKey = ElementKey("MediaSource") * behavior. */ var QueueEntry.mediaSourceId by element(mediaSourceIdKey) + +/** + * Get the flow of [mediaSourceId]. + * @see mediaSourceId + */ +val QueueEntry.mediaSourceIdFlow by elementFlow(mediaSourceIdKey)