Fix music artist listing & show music in collections (#1739)

## Description
Switches to use the Artists API instead of Items API when fetching the
list of artists in a library.

Enables fetching artists, albums, and music videos in a collection.

Also adds titles for the remaining `BaseItemKind` types for future
proofing even if they aren't used anywhere in the app.

### Related issues
Fixes #1733
Fixes #1734
Fixes #1735


### Testing
Emulator, created a new collection with the above. Also verified the
artist query against the web UI

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-23 14:23:48 -04:00 • committed by GitHub
parent 00490b14ec
commit 5bc874a9ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 134 additions and 35 deletions

View file

@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.VideoType import org.jellyfin.sdk.model.api.VideoType
import org.jellyfin.sdk.model.api.request.GetArtistsRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.api.request.GetPersonsRequest
import org.jellyfin.sdk.model.serializer.UUIDSerializer import org.jellyfin.sdk.model.serializer.UUIDSerializer
@ -138,6 +139,22 @@ data class GetItemsFilter(
isFavorite = favorite, isFavorite = favorite,
) )
fun applyTo(req: GetArtistsRequest) =
req.copy(
minCommunityRating = minCommunityRating,
isFavorite = favorite,
genreIds = genres,
personIds = persons,
studioIds = studios,
tags = tags,
officialRatings = officialRatings,
years =
buildSet {
years?.letNotEmpty(::addAll)
decades?.forEach { addAll(it..<(it + 10)) }
},
)
/** /**
* Merge another [GetItemsFilter] onto this one, replacing only unset values * Merge another [GetItemsFilter] onto this one, replacing only unset values
*/ */
@ -162,4 +179,5 @@ data class GetItemsFilter(
enum class GetItemsFilterOverride { enum class GetItemsFilterOverride {
NONE, NONE,
PERSON, PERSON,
ARTIST,
} }

View file

@ -230,36 +230,36 @@ fun formatTypeName(type: BaseItemKind): Int =
BaseItemKind.PLAYLIST -> R.string.playlists BaseItemKind.PLAYLIST -> R.string.playlists
BaseItemKind.PERSON -> R.string.people_title BaseItemKind.PERSON -> R.string.people_title
BaseItemKind.BOX_SET -> R.string.collections BaseItemKind.BOX_SET -> R.string.collections
BaseItemKind.AUDIO -> TODO() BaseItemKind.AUDIO -> R.string.songs
BaseItemKind.CHANNEL -> R.string.channels BaseItemKind.CHANNEL -> R.string.channels
BaseItemKind.GENRE -> R.string.genres BaseItemKind.GENRE -> R.string.genres
BaseItemKind.LIVE_TV_CHANNEL -> R.string.channels BaseItemKind.LIVE_TV_CHANNEL -> R.string.channels
BaseItemKind.MUSIC_ALBUM -> TODO() BaseItemKind.MUSIC_ALBUM -> R.string.albums
BaseItemKind.MUSIC_ARTIST -> TODO() BaseItemKind.MUSIC_ARTIST -> R.string.artists
BaseItemKind.MUSIC_GENRE -> TODO() BaseItemKind.MUSIC_GENRE -> R.string.genres
BaseItemKind.MUSIC_VIDEO -> TODO() BaseItemKind.MUSIC_VIDEO -> R.string.music_videos
BaseItemKind.PHOTO -> R.string.photos BaseItemKind.PHOTO -> R.string.photos
BaseItemKind.PHOTO_ALBUM -> TODO() BaseItemKind.PHOTO_ALBUM -> R.string.photos
BaseItemKind.PROGRAM -> TODO() BaseItemKind.PROGRAM -> R.string.programs
BaseItemKind.RECORDING -> TODO() BaseItemKind.RECORDING -> R.string.recordings
BaseItemKind.SEASON -> R.string.tv_seasons BaseItemKind.SEASON -> R.string.tv_seasons
BaseItemKind.STUDIO -> R.string.studios BaseItemKind.STUDIO -> R.string.studios
BaseItemKind.TRAILER -> R.string.trailers_title BaseItemKind.TRAILER -> R.string.trailers_title
BaseItemKind.TV_CHANNEL -> R.string.channels BaseItemKind.TV_CHANNEL -> R.string.channels
BaseItemKind.TV_PROGRAM -> TODO() BaseItemKind.TV_PROGRAM -> R.string.programs
BaseItemKind.USER_ROOT_FOLDER -> TODO() BaseItemKind.USER_ROOT_FOLDER -> R.string.folders_title
BaseItemKind.USER_VIEW -> TODO() BaseItemKind.USER_VIEW -> R.string.user_views
BaseItemKind.YEAR -> TODO() BaseItemKind.YEAR -> R.string.year
BaseItemKind.AGGREGATE_FOLDER -> TODO() BaseItemKind.AGGREGATE_FOLDER -> R.string.folders_title
BaseItemKind.AUDIO_BOOK -> TODO() BaseItemKind.AUDIO_BOOK -> R.string.audio_books
BaseItemKind.BASE_PLUGIN_FOLDER -> TODO() BaseItemKind.BASE_PLUGIN_FOLDER -> R.string.folders_title
BaseItemKind.BOOK -> TODO() BaseItemKind.BOOK -> R.string.books
BaseItemKind.CHANNEL_FOLDER_ITEM -> TODO() BaseItemKind.CHANNEL_FOLDER_ITEM -> R.string.folders_title
BaseItemKind.COLLECTION_FOLDER -> TODO() BaseItemKind.COLLECTION_FOLDER -> R.string.library
BaseItemKind.FOLDER -> TODO() BaseItemKind.FOLDER -> R.string.folders_title
BaseItemKind.MANUAL_PLAYLISTS_FOLDER -> TODO() BaseItemKind.MANUAL_PLAYLISTS_FOLDER -> R.string.playlists
BaseItemKind.LIVE_TV_PROGRAM -> TODO() BaseItemKind.LIVE_TV_PROGRAM -> R.string.programs
BaseItemKind.PLAYLISTS_FOLDER -> TODO() BaseItemKind.PLAYLISTS_FOLDER -> R.string.playlists
} }
/** /**

View file

@ -67,6 +67,7 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetArtistsHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.GetPersonsHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
@ -92,6 +93,7 @@ import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.request.GetArtistsRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.api.request.GetPersonsRequest
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
@ -373,6 +375,26 @@ class CollectionFolderViewModel
) )
newPager newPager
} }
GetItemsFilterOverride.ARTIST -> {
val item = state.value.item.successValue
val request =
filter.applyTo(
GetArtistsRequest(
parentId = item?.id,
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
),
)
val newPager =
ApiRequestPager(
api,
request,
GetArtistsHandler,
viewModelScope,
useSeriesForPrimary = useSeriesForPrimary,
)
newPager
}
} }
private fun createGetItemsRequest( private fun createGetItemsRequest(

View file

@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.MusicService
@ -203,7 +204,7 @@ fun CollectionFolderMusic(
CollectionFolderFilter( CollectionFolderFilter(
filter = filter =
GetItemsFilter( GetItemsFilter(
includeItemTypes = listOf(BaseItemKind.MUSIC_ARTIST), override = GetItemsFilterOverride.ARTIST,
), ),
), ),
showTitle = false, showTitle = false,

View file

@ -425,7 +425,7 @@ fun CollectionDetailsContent(
onClickPlay = onClickPlay, onClickPlay = onClickPlay,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onFocusPosition = { position -> onFocusPosition = { position ->
Timber.v("onFocusPosition=%s", position) // Timber.v("onFocusPosition=%s", position)
focusedItem = focusedItem =
position.let { position.let {
val key = val key =

View file

@ -51,18 +51,45 @@ fun CollectionRows(
if (row is HomeRowLoadingState.Success) { if (row is HomeRowLoadingState.Success) {
// TODO not great to do this in the UI // TODO not great to do this in the UI
val viewOptions = val viewOptions =
if (type == BaseItemKind.EPISODE) { when (type) {
HomeRowViewOptions( BaseItemKind.EPISODE -> {
heightDp = Cards.HEIGHT_EPISODE, HomeRowViewOptions(
episodeAspectRatio = AspectRatio.WIDE, heightDp = Cards.HEIGHT_EPISODE,
showTitles = cardViewOptions.showTitles, episodeAspectRatio = AspectRatio.WIDE,
useSeries = false, showTitles = cardViewOptions.showTitles,
) useSeries = false,
} else { )
HomeRowViewOptions( }
showTitles = cardViewOptions.showTitles,
) BaseItemKind.MUSIC_ARTIST,
BaseItemKind.MUSIC_ALBUM,
BaseItemKind.PLAYLIST,
BaseItemKind.AUDIO,
-> {
HomeRowViewOptions(
heightDp = Cards.HEIGHT_EPISODE,
aspectRatio = AspectRatio.SQUARE,
showTitles = cardViewOptions.showTitles,
)
}
BaseItemKind.VIDEO,
BaseItemKind.MUSIC_VIDEO,
-> {
HomeRowViewOptions(
heightDp = Cards.HEIGHT_EPISODE,
aspectRatio = AspectRatio.WIDE,
showTitles = cardViewOptions.showTitles,
)
}
else -> {
HomeRowViewOptions(
showTitles = cardViewOptions.showTitles,
)
}
} }
row.copy(viewOptions = viewOptions) row.copy(viewOptions = viewOptions)
} else { } else {
row row

View file

@ -489,6 +489,9 @@ class CollectionViewModel
BaseItemKind.EPISODE, BaseItemKind.EPISODE,
BaseItemKind.VIDEO, BaseItemKind.VIDEO,
BaseItemKind.BOX_SET, BaseItemKind.BOX_SET,
BaseItemKind.MUSIC_ARTIST,
BaseItemKind.MUSIC_ALBUM,
BaseItemKind.MUSIC_VIDEO,
) )
const val VIEW_OPTIONS_KEY = "CollectionViewOptions" const val VIEW_OPTIONS_KEY = "CollectionViewOptions"

View file

@ -10,6 +10,7 @@ import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass import kotlinx.serialization.modules.subclass
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.Response import org.jellyfin.sdk.api.client.Response
import org.jellyfin.sdk.api.client.extensions.artistsApi
import org.jellyfin.sdk.api.client.extensions.genresApi import org.jellyfin.sdk.api.client.extensions.genresApi
import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.liveTvApi import org.jellyfin.sdk.api.client.extensions.liveTvApi
@ -21,6 +22,7 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
import org.jellyfin.sdk.model.api.GetProgramsDto import org.jellyfin.sdk.model.api.GetProgramsDto
import org.jellyfin.sdk.model.api.request.GetArtistsRequest
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
import org.jellyfin.sdk.model.api.request.GetGenresRequest import org.jellyfin.sdk.model.api.request.GetGenresRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest
@ -367,6 +369,25 @@ object GetLiveTvChannelsRequestHandler : RequestHandler<GetLiveTvChannelsRequest
): Response<BaseItemDtoQueryResult> = api.liveTvApi.getLiveTvChannels(request) ): Response<BaseItemDtoQueryResult> = api.liveTvApi.getLiveTvChannels(request)
} }
@Serializable
object GetArtistsHandler : RequestHandler<GetArtistsRequest> {
override fun prepare(
request: GetArtistsRequest,
startIndex: Int,
limit: Int,
enableTotalRecordCount: Boolean,
): GetArtistsRequest =
request.copy(
startIndex = startIndex,
limit = limit,
)
override suspend fun execute(
api: ApiClient,
request: GetArtistsRequest,
): Response<BaseItemDtoQueryResult> = api.artistsApi.getArtists((request))
}
val requestSerializersModule = val requestSerializersModule =
SerializersModule { SerializersModule {
polymorphic(Any::class) { polymorphic(Any::class) {
@ -382,5 +403,6 @@ val requestSerializersModule =
subclass(GetStudiosRequest::class) subclass(GetStudiosRequest::class)
subclass(GetRecordingsRequest::class) subclass(GetRecordingsRequest::class)
subclass(GetLiveTvChannelsRequest::class) subclass(GetLiveTvChannelsRequest::class)
subclass(GetArtistsHandler::class)
} }
} }

View file

@ -874,4 +874,10 @@
<string name="any_language">Any language</string> <string name="any_language">Any language</string>
<string name="hash_matches">Hash matches</string> <string name="hash_matches">Hash matches</string>
<string name="subtitles_hearing_impaired">SDH</string> <string name="subtitles_hearing_impaired">SDH</string>
<string name="programs">TV programs</string>
<string name="recordings">Recordings</string>
<string name="folders_title">Folders</string>
<string name="user_views">User views</string>
<string name="books">Books</string>
<string name="audio_books">Audio books</string>
</resources> </resources>