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)