Fix inconsistent playback end behavior issues (#1616)

## Description
The exact behavior of what happens when playback ends based on the
user's settings would change between versions. This PR defines the
behaviors better and includes unit tests to verify future changes won't
break it again.

### Related issues
Fixes #1548
Related to https://github.com/damontecres/Wholphin/pull/1257

### Testing
Emulator & unit tests

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-06-23 17:23:47 -04:00 • committed by GitHub
parent 899e3decc2
commit 552c3b5500
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 9 deletions

View file

@ -1034,14 +1034,26 @@ class PlaybackViewModel
} }
is PlaylistItem.Media -> { is PlaylistItem.Media -> {
if (currentItem is PlaylistItem.Intro) { val prefs = preferences.appPreferences.playbackPreferences
Timber.v("Current item is intro, so playing next up immediately") when {
playNextUp() currentItem is PlaylistItem.Intro -> {
} else if (preferences.appPreferences.playbackPreferences.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER) { Timber.v("Current item is intro, so playing next up immediately")
Timber.v("Setting next up to ${nextItem.id}") playNextUp()
_state.update { it.copy(nextUp = nextItem.item) } }
} else {
controllerViewState.showControls() prefs.showNextUpWhen == ShowNextUpWhen.NEXT_UP_NEVER && !prefs.autoPlayNext -> {
Timber.v("Never show or auto play next up, returning")
navigationManager.goBack()
}
prefs.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER -> {
Timber.v("Setting next up to ${nextItem.id}")
_state.update { it.copy(nextUp = nextItem.item) }
}
else -> {
controllerViewState.showControls()
}
} }
} }

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlaybackPreferences
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
@ -456,11 +457,12 @@ class PlaybackViewModelTests {
} }
@Test @Test
fun `Show next up never`() = fun `Show next up never with automatic play`() =
runTest(testDispatcher) { runTest(testDispatcher) {
setupForPlaylist() setupForPlaylist()
setupPreferences { setupPreferences {
showNextUpWhen = ShowNextUpWhen.NEXT_UP_NEVER showNextUpWhen = ShowNextUpWhen.NEXT_UP_NEVER
autoPlayNext = true
} }
val viewModel = createViewModel(Destination.PlaybackList(playlist.id)) val viewModel = createViewModel(Destination.PlaybackList(playlist.id))
@ -476,7 +478,108 @@ class PlaybackViewModelTests {
viewModel.state.value.also { state -> viewModel.state.value.also { state ->
Assert.assertNull(state.nextUp) Assert.assertNull(state.nextUp)
Assert.assertTrue(state.hasNext)
} }
verify(exactly = 0) { mockNavigationManager.goBack() }
}
@Test
fun `Show next up never without automatic play`() =
runTest(testDispatcher) {
setupForPlaylist()
setupPreferences {
showNextUpWhen = ShowNextUpWhen.NEXT_UP_NEVER
autoPlayNext = false
}
val viewModel = createViewModel(Destination.PlaybackList(playlist.id))
viewModel.state.value.also { state ->
Assert.assertEquals(playlistItems.items, state.playlist.items)
Assert.assertEquals(0, state.playlistIndex)
Assert.assertNull(state.nextUp)
}
viewModel.onPlaybackStateChanged(Player.STATE_ENDED)
testScheduler.advanceUntilIdle()
viewModel.state.value.also { state ->
Assert.assertNull(state.nextUp)
Assert.assertTrue(state.hasNext)
}
verify(exactly = 1) { mockNavigationManager.goBack() }
}
@Test
fun `Playback ends with no next up`() =
runTest(testDispatcher) {
setupForPlaylist()
setupPreferences {
showNextUpWhen = ShowNextUpWhen.END_OF_PLAYBACK
autoPlayNext = false
}
val viewModel = createViewModel(Destination.PlaybackList(playlist.id))
viewModel.state.value.also { state ->
Assert.assertEquals(playlistItems.items, state.playlist.items)
Assert.assertEquals(0, state.playlistIndex)
Assert.assertNull(state.nextUp)
}
viewModel.playNextUp()
viewModel.playNextUp()
testScheduler.advanceUntilIdle()
viewModel.onPlaybackStateChanged(Player.STATE_ENDED)
testScheduler.advanceUntilIdle()
viewModel.state.value.also { state ->
Assert.assertNull(state.nextUp)
Assert.assertFalse(state.hasNext)
}
verify(exactly = 1) { mockNavigationManager.goBack() }
}
@Test
fun `Intro playback ends always play next`() =
runTest(testDispatcher) {
setupPreferences {
cinemaMode = true
showNextUpWhen = ShowNextUpWhen.NEXT_UP_NEVER
autoPlayNext = false
}
setupForPlaylist()
coEvery { mockUserLibraryApi.getItem(movie.id) } returns successResponse(movie)
coEvery { mockUserLibraryApi.getIntros(movie.id, any()) } returns
successQueryResult(listOf(intro))
val viewModel = createViewModel(Destination.PlaybackList(playlist.id))
viewModel.state.value.also { state ->
Assert.assertEquals(
listOf(PlaylistItem.Intro(BaseItem(intro))) + playlistItems.items,
state.playlist.items,
)
Assert.assertEquals(0, state.playlistIndex)
Assert.assertNull(state.nextUp)
}
viewModel.onPlaybackStateChanged(Player.STATE_ENDED)
testScheduler.advanceUntilIdle()
viewModel.state.value.also { state ->
Assert.assertEquals(1, state.playlistIndex)
Assert.assertNull(state.nextUp)
}
verify(exactly = 0) { mockNavigationManager.goBack() }
val mediaItems = mutableListOf<MediaItem>()
verify(exactly = 2) {
mockPlayer.setMediaItem(withArg { mediaItems.add(it) }, any<Long>())
}
Assert.assertEquals(2, mediaItems.size)
Assert.assertEquals(intro.id.toString(), mediaItems[0].mediaId)
Assert.assertEquals(movie.id.toString(), mediaItems[1].mediaId)
} }
@Test @Test