Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jellyfin/jellyfin-androidtv
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 9331be411eb5ac2e74c05773812a22808de86d6d
Choose a base ref
..
head repository: jellyfin/jellyfin-androidtv
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: c0ce8f0f1c5d8023d40f779ba7b02674de18ce73
Choose a head ref
2 changes: 1 addition & 1 deletion .github/workflows/app-lint.yaml
Original file line number Diff line number Diff line change
@@ -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@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
if: ${{ always() }}
with:
sarif_file: .
Original file line number Diff line number Diff line change
@@ -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());
}
}

Original file line number Diff line number Diff line change
@@ -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()
}

6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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.1.0"
kotlinx-coroutines = "1.10.1"
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"

4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions gradlew
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion playback/core/src/main/kotlin/PlayerState.kt
Original file line number Diff line number Diff line change
@@ -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
26 changes: 14 additions & 12 deletions playback/core/src/main/kotlin/queue/QueueService.kt
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -23,6 +23,6 @@ internal class RandomOrderIndexProvider : OrderIndexProvider {
}

override fun useNextIndex() {
nextIndices.remove(0)
nextIndices.removeAt(0)
}
}
Original file line number Diff line number Diff line change
@@ -34,6 +34,6 @@ internal class ShuffleOrderIndexProvider : OrderIndexProvider {
}

override fun useNextIndex() {
nextIndices.remove(0)
nextIndices.removeAt(0)
}
}
2 changes: 2 additions & 0 deletions playback/media3/exoplayer/src/main/kotlin/ExoPlayerBackend.kt
Original file line number Diff line number Diff line change
@@ -209,6 +209,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()
}