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 org.jellyfin.sdk.model.api.BaseItemKind
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.GetPersonsRequest
import org.jellyfin.sdk.model.serializer.UUIDSerializer
@ -138,6 +139,22 @@ data class GetItemsFilter(
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
*/
@ -162,4 +179,5 @@ data class GetItemsFilter(
enum class GetItemsFilterOverride {
NONE,
PERSON,
ARTIST,
}

View file

@ -230,36 +230,36 @@ fun formatTypeName(type: BaseItemKind): Int =
BaseItemKind.PLAYLIST -> R.string.playlists
BaseItemKind.PERSON -> R.string.people_title
BaseItemKind.BOX_SET -> R.string.collections
BaseItemKind.AUDIO -> TODO()
BaseItemKind.AUDIO -> R.string.songs
BaseItemKind.CHANNEL -> R.string.channels
BaseItemKind.GENRE -> R.string.genres
BaseItemKind.LIVE_TV_CHANNEL -> R.string.channels
BaseItemKind.MUSIC_ALBUM -> TODO()
BaseItemKind.MUSIC_ARTIST -> TODO()
BaseItemKind.MUSIC_GENRE -> TODO()
BaseItemKind.MUSIC_VIDEO -> TODO()
BaseItemKind.MUSIC_ALBUM -> R.string.albums
BaseItemKind.MUSIC_ARTIST -> R.string.artists
BaseItemKind.MUSIC_GENRE -> R.string.genres
BaseItemKind.MUSIC_VIDEO -> R.string.music_videos
BaseItemKind.PHOTO -> R.string.photos
BaseItemKind.PHOTO_ALBUM -> TODO()
BaseItemKind.PROGRAM -> TODO()
BaseItemKind.RECORDING -> TODO()
BaseItemKind.PHOTO_ALBUM -> R.string.photos
BaseItemKind.PROGRAM -> R.string.programs
BaseItemKind.RECORDING -> R.string.recordings
BaseItemKind.SEASON -> R.string.tv_seasons
BaseItemKind.STUDIO -> R.string.studios
BaseItemKind.TRAILER -> R.string.trailers_title
BaseItemKind.TV_CHANNEL -> R.string.channels
BaseItemKind.TV_PROGRAM -> TODO()
BaseItemKind.USER_ROOT_FOLDER -> TODO()
BaseItemKind.USER_VIEW -> TODO()
BaseItemKind.YEAR -> TODO()
BaseItemKind.AGGREGATE_FOLDER -> TODO()
BaseItemKind.AUDIO_BOOK -> TODO()
BaseItemKind.BASE_PLUGIN_FOLDER -> TODO()
BaseItemKind.BOOK -> TODO()
BaseItemKind.CHANNEL_FOLDER_ITEM -> TODO()
BaseItemKind.COLLECTION_FOLDER -> TODO()
BaseItemKind.FOLDER -> TODO()
BaseItemKind.MANUAL_PLAYLISTS_FOLDER -> TODO()
BaseItemKind.LIVE_TV_PROGRAM -> TODO()
BaseItemKind.PLAYLISTS_FOLDER -> TODO()
BaseItemKind.TV_PROGRAM -> R.string.programs
BaseItemKind.USER_ROOT_FOLDER -> R.string.folders_title
BaseItemKind.USER_VIEW -> R.string.user_views
BaseItemKind.YEAR -> R.string.year
BaseItemKind.AGGREGATE_FOLDER -> R.string.folders_title
BaseItemKind.AUDIO_BOOK -> R.string.audio_books
BaseItemKind.BASE_PLUGIN_FOLDER -> R.string.folders_title
BaseItemKind.BOOK -> R.string.books
BaseItemKind.CHANNEL_FOLDER_ITEM -> R.string.folders_title
BaseItemKind.COLLECTION_FOLDER -> R.string.library
BaseItemKind.FOLDER -> R.string.folders_title
BaseItemKind.MANUAL_PLAYLISTS_FOLDER -> R.string.playlists
BaseItemKind.LIVE_TV_PROGRAM -> R.string.programs
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.DataLoadingState
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.GetPersonsHandler
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.ItemSortBy
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.GetPersonsRequest
import org.jellyfin.sdk.model.serializer.toUUID
@ -373,6 +375,26 @@ class CollectionFolderViewModel
)
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(

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.CollectionFolderFilter
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.services.BackdropService
import com.github.damontecres.wholphin.services.MusicService
@ -203,7 +204,7 @@ fun CollectionFolderMusic(
CollectionFolderFilter(
filter =
GetItemsFilter(
includeItemTypes = listOf(BaseItemKind.MUSIC_ARTIST),
override = GetItemsFilterOverride.ARTIST,
),
),
showTitle = false,

View file

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

View file

@ -51,18 +51,45 @@ fun CollectionRows(
if (row is HomeRowLoadingState.Success) {
// TODO not great to do this in the UI
val viewOptions =
if (type == BaseItemKind.EPISODE) {
HomeRowViewOptions(
heightDp = Cards.HEIGHT_EPISODE,
episodeAspectRatio = AspectRatio.WIDE,
showTitles = cardViewOptions.showTitles,
useSeries = false,
)
} else {
HomeRowViewOptions(
showTitles = cardViewOptions.showTitles,
)
when (type) {
BaseItemKind.EPISODE -> {
HomeRowViewOptions(
heightDp = Cards.HEIGHT_EPISODE,
episodeAspectRatio = AspectRatio.WIDE,
showTitles = cardViewOptions.showTitles,
useSeries = false,
)
}
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)
} else {
row

View file

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

View file

@ -10,6 +10,7 @@ import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import org.jellyfin.sdk.api.client.ApiClient
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.itemsApi
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.model.api.BaseItemDtoQueryResult
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.GetGenresRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
@ -367,6 +369,25 @@ object GetLiveTvChannelsRequestHandler : RequestHandler<GetLiveTvChannelsRequest
): 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 =
SerializersModule {
polymorphic(Any::class) {
@ -382,5 +403,6 @@ val requestSerializersModule =
subclass(GetStudiosRequest::class)
subclass(GetRecordingsRequest::class)
subclass(GetLiveTvChannelsRequest::class)
subclass(GetArtistsHandler::class)
}
}

View file

@ -874,4 +874,10 @@
<string name="any_language">Any language</string>
<string name="hash_matches">Hash matches</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>