Fix person credits: movie release dates, full filmography, sorted order (#1950)

<!-- By submitting this pull request, you acknowledge that you have read
the [contributing
guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md,
including the AI/LLM policy, and [developer's
guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md)
-->

## Description

Person pages that get a filmography from Seerr had three problems, all
in the same place.

The release date was taken from `firstAirDate` alone — the field TMDB
fills for series — so
movie cards showed no year at all. The library person page capped the
row at 25 cast + 25 crew while
`DiscoverPersonPage` had no cap, because each screen carried its own
copy of the same logic.
And `cast + crew` left the row in no order a viewer can read, with a
title appearing twice when
a person is both cast and crew on it.

This PR:

* maps credit dates as `releaseDate ?: firstAirDate` — the expression
the search result mapping
  already uses;
* moves the credits logic into a single `SeerrService.personCredits()`
used by both screens, so
  they cannot drift apart again;
* drops the 25 + 25 cap, de-duplicates by TMDB id and sorts by release
date descending, with
  undated entries last.

Rows on movie and series pages are untouched: only the
`BaseItemKind.PERSON` branch of
`similar()` changed.

Two things worth your opinion:

* Without the cap, the row for a person with a long filmography is long
— 135 cards for Cameron
Diaz, for example. If you would rather keep a cap, the row could keep
the newest 25 and gain a
`ViewMoreCard` opening the full grid — `Destination.DiscoveredItem` with
a `PERSON` item
already routes to `DiscoverPersonPage`. I am happy to add that in a
second commit.
* The sort is fixed to release date descending. It now lives in one
function, so a different key
  or a user-facing choice would be cheap to add later.

### Related issues

Fixes #1873

### Testing

CI in my fork ran the repository's own PR workflow on this branch:
`pre-commit` (ktlint) and
`assembleDefaultDebug` with `testDefaultDebugUnitTest` are both green.

I then installed a debug build of the same code on an NVIDIA Shield TV
Pro 2019 (Android 11),
against Jellyfin 10.11.11 with Jellyseerr, and opened Cameron Diaz from
the cast row of
*Charlie's Angels* before and after the change:

* movie cards show a year now; before, the only two cards in the row
with a year were the two
  TV entries;
* the row is ordered by release date descending, movies and series
interleaved, instead of the
  API order of all cast followed by all crew;
* the row is no longer cut at 25 + 25 — it now runs back to her 1952
credit, and the entries
  TMDB has no date for sit at the very end of the row.

Not tested on any other device; nothing in the change is device
specific.

## Screenshots

Person page of Cameron Diaz, reached from the cast row of *Charlie's
Angels*. The library rows
above the Discover row are the same in every shot and are useful as a
reference: they always had
their years.

Before, start of the Discover row — no year under any card:
<img width="1920" height="1080" alt="pr1873-1-before-start"
src="https://github.com/user-attachments/assets/585d0670-ae39-4214-9004-0c92262991da"
/>

Before, end of the Discover row — the row stops here, and the only two
years in it belong to the
two TV entries:
<img width="1920" height="1080" alt="pr1873-2-before-end"
src="https://github.com/user-attachments/assets/85c58575-2ad0-4369-a103-60f04c8d2350"
/>

After, start of the row — years on the movie cards, ordered by date
descending:
<img width="1920" height="1080" alt="pr1873-3-after-start"
src="https://github.com/user-attachments/assets/804745c5-e1a6-4fe4-93bd-dc6c97a7a876"
/>

After, end of the row — the list now reaches 1952, with the undated
entries last:
<img width="1920" height="1080" alt="pr1873-4-after-end"
src="https://github.com/user-attachments/assets/2c32389c-ec0b-4df5-b9d1-f564ef10d8b3"
/>

## AI or LLM usage

The code was written with Claude Opus 5 (Claude Code): the two mapping
lines, the extracted
`personCredits()` function and the two call sites. I reviewed every line
and can explain it.
This description was written by me in Russian and translated into
English with the same
assistant.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
SergeiAK 2026-09-11 22:22:02 +03:00 • committed by GitHub
parent 9c56965c82
commit 6486bd19ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 32 deletions

View file

@ -124,21 +124,7 @@ class SeerrService
}
BaseItemKind.PERSON -> {
api.personApi
.personPersonIdCombinedCreditsGet(personId = it)
.let { credits ->
val cast =
credits.cast
?.take(25)
?.map { createDiscoverItem(it) }
.orEmpty()
val crew =
credits.crew
?.take(25)
?.map { createDiscoverItem(it) }
.orEmpty()
cast + crew
}
personCredits(it)
}
else -> {
@ -150,6 +136,50 @@ class SeerrService
null
}
/**
* Get the combined movie & TV credits of a person as [DiscoverItem]s
*
* The two lists returned by the API are merged, de-duplicated by id since a person can be
* both cast and crew on the same title, and sorted by release date descending with undated
* entries last
*/
suspend fun personCredits(personId: Int): List<DiscoverItem> =
api.personApi
.personPersonIdCombinedCreditsGet(personId = personId)
.let { credits ->
val cast = credits.cast?.map { createDiscoverItem(it) }.orEmpty()
val crew = credits.crew?.map { createDiscoverItem(it) }.orEmpty()
(cast + crew)
.distinctBy { it.id }
.sortedByDescending { it.releaseDate }
}
/**
* Create a [DiscoverItem] for a person in the library so that the full grid of their
* credits can be opened from their page
*
* @return the item or null if the person has no TMDB id
*/
fun discoverPerson(item: BaseItem): DiscoverItem? =
item.data.providerIds
?.get("Tmdb")
?.toIntOrNull()
?.let {
DiscoverItem(
id = it,
type = SeerrItemType.PERSON,
title = item.name,
subtitle = null,
overview = item.data.overview,
availability = SeerrAvailability.UNKNOWN,
releaseDate = null,
posterUrl = null,
backDropUrl = null,
logoUrl = null,
jellyfinItemId = null,
)
}
suspend fun getTvSeries(item: BaseItem): TvDetails? =
if (active.first()) {
item.data.providerIds
@ -424,7 +454,7 @@ class SeerrService
availability =
SeerrAvailability.from(credit.mediaInfo?.status)
?: SeerrAvailability.UNKNOWN,
releaseDate = toLocalDate(credit.firstAirDate),
releaseDate = toLocalDate(credit.releaseDate ?: credit.firstAirDate),
posterUrl =
createImageUrl(
ImageType.PRIMARY,
@ -451,7 +481,7 @@ class SeerrService
availability =
SeerrAvailability.from(credit.mediaInfo?.status)
?: SeerrAvailability.UNKNOWN,
releaseDate = toLocalDate(credit.firstAirDate),
releaseDate = toLocalDate(credit.releaseDate ?: credit.firstAirDate),
posterUrl =
createImageUrl(
ImageType.PRIMARY,