Selecting a song on the search page navigates to a song's album with the song focused (#1661)

## Description
Selecting a song from search now resolves to its album's detail page,
scrolled to and focusing that song, instead of sending you to an
"unsupported item type" page. Implemented in the shared
BaseItem.destination() rather than in search specifically, so any caller
that navigates to a song gets the same behavior.

A song with no albumId plays directly while remaining in the search
results page: NavigationManager.navigateTo() intercepts a bare AUDIO
destination and hands it to a new MusicService.playSong(). Handling this
in the single navigation funnel means every caller gets it for free with
no loading-screen flash.

Adds BaseItemDestinationTest covering the new destination() logic.

### Related issues
https://github.com/damontecres/Wholphin/issues/1656

### Testing
Device: Sony Bravia 4K VH21

#### Tests on a search results page, song with albumId:
1. Select a song sends you to album page with cursor on song. Another
press plays it
2. Back button brings you back to search results.

Tested with both songs at the start of an album and songs deep into a
100+ songs album.

#### Tests on a search results page, song with no albumId:
1. Select a songs plays the song while staying in the search results
page. No loading screen.

## AI or LLM usage
I used Cursor and Claude Code for planning, implementation and code
review.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
Stefan Bullones 2026-07-29 00:45:02 +02:00 • committed by GitHub
parent 79cb93db08
commit 438c9dd704
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 394 additions and 20 deletions

View file

@ -0,0 +1,49 @@
package com.github.damontecres.wholphin.test
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.nav.Destination
import org.jellyfin.sdk.model.api.BaseItemKind
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.util.UUID
class BaseItemDestinationTest {
@Test
fun audioWithAlbumId_navigatesToAlbumWithInitialSongId() {
val songId = UUID.randomUUID()
val albumId = UUID.randomUUID()
val item = BaseItem(song(id = songId, albumId = albumId), false)
val dest = item.destination()
assertEquals(Destination.MediaItem::class, dest::class)
val media = dest as Destination.MediaItem
assertEquals(albumId, media.itemId)
assertEquals(BaseItemKind.MUSIC_ALBUM, media.type)
assertEquals(songId, media.initialSongId)
}
@Test
fun audioWithoutAlbumId_returnsSelfAsMediaItem() {
val songId = UUID.randomUUID()
val item = BaseItem(song(id = songId, albumId = null), false)
val dest = item.destination()
assertEquals(songId, (dest as Destination.MediaItem).itemId)
assertEquals(BaseItemKind.AUDIO, dest.type)
assertNull(dest.initialSongId)
}
@Test
fun movie_usesStandardDestination() {
val movieId = UUID.randomUUID()
val item = BaseItem(movie(id = movieId), false)
val dest = item.destination()
assertEquals(movieId, (dest as Destination.MediaItem).itemId)
assertEquals(BaseItemKind.MOVIE, dest.type)
}
}

View file

@ -42,6 +42,24 @@ fun episode(
genreItems = genres,
)
/**
* Create a simple [BaseItemDto] song
*/
fun song(
id: UUID = UUID.randomUUID(),
albumId: UUID? = null,
name: String = "Test Song",
genres: List<NameGuidPair>? = null,
): BaseItemDto =
BaseItemDto(
id = id,
type = BaseItemKind.AUDIO,
name = name,
albumId = albumId,
seriesId = null,
genreItems = genres,
)
/**
* Create a simple [BaseItemDto] playlist
*/