Show PWA source ratings (RT, IMDb, TMDB from Seerr) on the TV #24

Merged
justinzeus merged 5 commits from fm/wolfskin-23-ratings into main 2026-10-29 20:29:43 +01:00
Owner

Closes justinzeus/wolfskin#23

Wolfskin TV now shows ratings from the same source as the phone app (PWA): Seerr. Rotten Tomatoes critics and audience, IMDb (movies only) and TMDB, each as a badge plus number, using the PWA's rules. No Conductarr client, no new credentials, no new screens or settings.

What changed

Data and rules (SourceRatings, SeerrRatingsService)

  • Movies: Seerr ratingscombined (RT critics, RT audience, IMDb). Series: tv/{id}/ratings (RT critics and audience). TMDB: voteAverage/voteCount, fetched only when no other source has a score.
  • PWA rules applied exactly: only observed scores are shown; TMDB only when nothing else is shown; a zero-vote (or negative-vote) IMDb/TMDB score is unrated, never 0; out-of-range scores are dropped; the RT icon comes from Seerr's status label (Certified Fresh/Fresh = fresh, Rotten = rotten, Upright, Spilled), never a percentage threshold. A critics score with no recognised label gets the PWA's muted tomato, an audience score with none gets the upright popcorn.
  • Cache is keyed by media type plus TMDB id (the old Discover cache used the bare id, so a movie and a series with the same id could show each other's rating). Failures are no longer cached as "no rating": a failed call returns null and is retried next time. A 404 from ratingscombined is treated as an answer ("no RT/IMDb data") and TMDB is still tried.
  • Seerr spec fix (seerr-api.yml): IMDb criticsScore is now a number (it was integer, which would throw on 8.8), added criticsScoreCount, added RT audienceScore/audienceRating to /tv/{id}/ratings, and Certified Fresh to the TV critics enum so an unexpected label cannot break parsing. voteAverage/voteCount were already present on movie and TV details.

Discover screens (focused header on SeerrDiscoverPage, DiscoverMovieDetailsHeader, DiscoverSeriesDetails): use the new model. The old "audience percent / 10 shown as a star" is gone. The Discover page now cancels the previous in-flight rating request when focus moves.

Jellyfin-owned movie and series DETAILS headers: the TMDB id from providerIds["Tmdb"] is resolved to the same Seerr ratings through an async side channel (MovieState.sourceRatings, SeriesState.sourceRatings). Until Seerr answers, the header shows the Jellyfin star and tomato, so there is no blank flash. Jellyfin's values stay as the fallback when Seerr is not configured, the title has no TMDB id, the call fails, or Seerr has no score at all for the title. Seerr's connection is established asynchronously after login, so the lookup follows the "Seerr active" flow rather than checking once.

Left as they were: episodes, music, slideshow, TV guide, and the home focused header (below).

Open choices and how they were resolved

  • Owned titles: Seerr set shown, Jellyfin values only as the fallback. Once Seerr answers with any score, the Jellyfin star/tomato are replaced, so a title with only an RT score shows no Jellyfin star beside it.
  • User toggles kept: COMMUNITY_RATING now controls IMDb/TMDB, CRITIC_RATING controls Rotten Tomatoes (critics and audience). Existing preferences keep working. The Discover headers never honoured the toggles and still do not.
  • Badges: converted the PWA SVGs to Android vector drawables (ic_rt_upright, ic_rt_spilled, ic_rating_imdb, ic_rating_tmdb); fresh and rotten tomatoes already existed and are identical to the PWA's. The conversion was mechanical (paths, circles, one gradient) with no loss. IMDb/TMDB are drawn 1.8x as wide as tall, like the PWA. I did not check the licence of the PWA's IMDb/TMDB artwork.
  • Score text: IMDb and TMDB print with one decimal (8.8, 7.6). The PWA prints the raw number, which would show an unrounded TMDB average like 7.633 [inferred, not seen live]. The PWA also links each badge to its source page; that is dropped on the TV.
  • Home focused header: left on Jellyfin values. Home rows request items without ProviderIds, so a focused home item has no TMDB id. Enriching it would need a changed field list for every home row plus a per-focus request, which is not the simple, safe change the brief asked for.
  • Collections: left on Jellyfin values. Jellyfin's Tmdb id on a box set is a TMDB collection id, not a movie id, so resolving it as a movie would show the wrong title's ratings.

Tests

  • make test: 423 tests, 0 failures. New: SourceRatingsTest (12: PWA rules, zero-vote, TMDB-only-when-nothing-else, range checks, status-to-icon mapping and defaults, toggle mapping, Jellyfin fallback in withSourceRatings) and SeerrRatingsServiceTest (11: endpoint choice per type, TMDB only as fallback, cache keying movie vs series with the same id, success cached once, failures not cached and retried, 404 as an answer, fallback to null for no TMDB id / episode / collection / call failure / Seerr inactive).
  • make apk-debug: build successful.
  • The first three commits were built together; I did not build each commit separately. The last commit puts Rotten Tomatoes before IMDb, as in the PWA (the Jellyfin fallback keeps its star-then-tomato order).
  • First review head (c54acc9c): make test 0 failures (423 tests), make apk-debug successful.

Review follow-ups (head 04d659b1)

  • Movie screen: MovieViewModel.init() runs again on every resume and the ratings flow never completes, so collectors stacked up. It now keeps a ratingsJob and cancels it before relaunching. The series screen launches once from the VM init, so it was already fine.
  • Seerr parsing is tolerant: the RT criticsRating/audienceRating fields are plain strings in the spec (they were enums), so an unexpected or new label can no longer fail the whole response; the score still shows, with the muted tomato for an unknown critics label. New test parses real JSON with unknown labels through the generated client.
  • The successful-result cache expires 6 hours after write (the 300-entry bound stays).
  • Movie and series state is seeded from the cache (cachedFor), so a revisit shows the Seerr ratings immediately instead of flashing the Jellyfin values. A later null from Seerr keeps what is already shown.
  • make test: 424 tests, 0 failures. make apk-debug: successful.

Before/after capture

Real composables (MovieDetailsHeader, DiscoverMovieDetailsHeader) rendered off-device with Robolectric native graphics at 1920x1080 (960x540dp, xhdpi), same title and same data in both states; the BEFORE images come from main (d78a23a8), the AFTER images from this branch. Files: /root/.treehouse/firstmate-7bab20/2/firstmate/data/wolfskin-23-ratings/capture/ (before-jellyfin-movie.png, after-jellyfin-movie.png, before-discover-movie.png, after-discover-movie.png). Data: Jellyfin holds 8.1 community and 92 critic; Seerr has RT 92 Certified Fresh, RT audience 95 Upright, IMDb 8.5. Before on Discover: 9.5 star / 92% tomato; after: 92% tomato, 95% popcorn, IMDb 8.5. Not captured: backdrop, the real font on a TV, and the Jellyfin "Ends at" time differs between runs because it is clock-based.

Not verified

  • No live Seerr data on this host (no Seerr container), so no real ratingscombined/tv/{id}/ratings response has been parsed. The spec changes follow Conductarr's Go client, its tests, and the PWA's fixtures.
  • Nothing was run on a device or emulator (no emulator or KVM here, TV's ADB is off). Layout of the new badges at TV size is covered by the before/after capture below, not by on-device use.
  • Whether the household Jellyfin libraries carry CriticRating, and so which owned titles fall back to it.
Closes justinzeus/wolfskin#23 Wolfskin TV now shows ratings from the same source as the phone app (PWA): Seerr. Rotten Tomatoes critics and audience, IMDb (movies only) and TMDB, each as a badge plus number, using the PWA's rules. No Conductarr client, no new credentials, no new screens or settings. ## What changed **Data and rules (`SourceRatings`, `SeerrRatingsService`)** - Movies: Seerr `ratingscombined` (RT critics, RT audience, IMDb). Series: `tv/{id}/ratings` (RT critics and audience). TMDB: `voteAverage`/`voteCount`, fetched only when no other source has a score. - PWA rules applied exactly: only observed scores are shown; TMDB only when nothing else is shown; a zero-vote (or negative-vote) IMDb/TMDB score is unrated, never 0; out-of-range scores are dropped; the RT icon comes from Seerr's status label (Certified Fresh/Fresh = fresh, Rotten = rotten, Upright, Spilled), never a percentage threshold. A critics score with no recognised label gets the PWA's muted tomato, an audience score with none gets the upright popcorn. - Cache is keyed by media type plus TMDB id (the old Discover cache used the bare id, so a movie and a series with the same id could show each other's rating). Failures are no longer cached as "no rating": a failed call returns null and is retried next time. A 404 from `ratingscombined` is treated as an answer ("no RT/IMDb data") and TMDB is still tried. - Seerr spec fix (`seerr-api.yml`): IMDb `criticsScore` is now a number (it was `integer`, which would throw on 8.8), added `criticsScoreCount`, added RT `audienceScore`/`audienceRating` to `/tv/{id}/ratings`, and `Certified Fresh` to the TV critics enum so an unexpected label cannot break parsing. `voteAverage`/`voteCount` were already present on movie and TV details. **Discover screens** (focused header on `SeerrDiscoverPage`, `DiscoverMovieDetailsHeader`, `DiscoverSeriesDetails`): use the new model. The old "audience percent / 10 shown as a star" is gone. The Discover page now cancels the previous in-flight rating request when focus moves. **Jellyfin-owned movie and series DETAILS headers**: the TMDB id from `providerIds["Tmdb"]` is resolved to the same Seerr ratings through an async side channel (`MovieState.sourceRatings`, `SeriesState.sourceRatings`). Until Seerr answers, the header shows the Jellyfin star and tomato, so there is no blank flash. Jellyfin's values stay as the fallback when Seerr is not configured, the title has no TMDB id, the call fails, or Seerr has no score at all for the title. Seerr's connection is established asynchronously after login, so the lookup follows the "Seerr active" flow rather than checking once. **Left as they were**: episodes, music, slideshow, TV guide, and the home focused header (below). ## Open choices and how they were resolved - Owned titles: Seerr set shown, Jellyfin values only as the fallback. Once Seerr answers with any score, the Jellyfin star/tomato are replaced, so a title with only an RT score shows no Jellyfin star beside it. - User toggles kept: `COMMUNITY_RATING` now controls IMDb/TMDB, `CRITIC_RATING` controls Rotten Tomatoes (critics and audience). Existing preferences keep working. The Discover headers never honoured the toggles and still do not. - Badges: converted the PWA SVGs to Android vector drawables (`ic_rt_upright`, `ic_rt_spilled`, `ic_rating_imdb`, `ic_rating_tmdb`); fresh and rotten tomatoes already existed and are identical to the PWA's. The conversion was mechanical (paths, circles, one gradient) with no loss. IMDb/TMDB are drawn 1.8x as wide as tall, like the PWA. I did not check the licence of the PWA's IMDb/TMDB artwork. - Score text: IMDb and TMDB print with one decimal (`8.8`, `7.6`). The PWA prints the raw number, which would show an unrounded TMDB average like 7.633 [inferred, not seen live]. The PWA also links each badge to its source page; that is dropped on the TV. - Home focused header: **left on Jellyfin values.** Home rows request items without `ProviderIds`, so a focused home item has no TMDB id. Enriching it would need a changed field list for every home row plus a per-focus request, which is not the simple, safe change the brief asked for. - Collections: left on Jellyfin values. Jellyfin's `Tmdb` id on a box set is a TMDB collection id, not a movie id, so resolving it as a movie would show the wrong title's ratings. ## Tests - `make test`: 423 tests, 0 failures. New: `SourceRatingsTest` (12: PWA rules, zero-vote, TMDB-only-when-nothing-else, range checks, status-to-icon mapping and defaults, toggle mapping, Jellyfin fallback in `withSourceRatings`) and `SeerrRatingsServiceTest` (11: endpoint choice per type, TMDB only as fallback, cache keying movie vs series with the same id, success cached once, failures not cached and retried, 404 as an answer, fallback to null for no TMDB id / episode / collection / call failure / Seerr inactive). - `make apk-debug`: build successful. - The first three commits were built together; I did not build each commit separately. The last commit puts Rotten Tomatoes before IMDb, as in the PWA (the Jellyfin fallback keeps its star-then-tomato order). - First review head (c54acc9c): `make test` 0 failures (423 tests), `make apk-debug` successful. ## Review follow-ups (head 04d659b1) - Movie screen: `MovieViewModel.init()` runs again on every resume and the ratings flow never completes, so collectors stacked up. It now keeps a `ratingsJob` and cancels it before relaunching. The series screen launches once from the VM `init`, so it was already fine. - Seerr parsing is tolerant: the RT `criticsRating`/`audienceRating` fields are plain strings in the spec (they were enums), so an unexpected or new label can no longer fail the whole response; the score still shows, with the muted tomato for an unknown critics label. New test parses real JSON with unknown labels through the generated client. - The successful-result cache expires 6 hours after write (the 300-entry bound stays). - Movie and series state is seeded from the cache (`cachedFor`), so a revisit shows the Seerr ratings immediately instead of flashing the Jellyfin values. A later null from Seerr keeps what is already shown. - `make test`: 424 tests, 0 failures. `make apk-debug`: successful. ## Before/after capture Real composables (`MovieDetailsHeader`, `DiscoverMovieDetailsHeader`) rendered off-device with Robolectric native graphics at 1920x1080 (960x540dp, xhdpi), same title and same data in both states; the BEFORE images come from main (`d78a23a8`), the AFTER images from this branch. Files: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/wolfskin-23-ratings/capture/` (`before-jellyfin-movie.png`, `after-jellyfin-movie.png`, `before-discover-movie.png`, `after-discover-movie.png`). Data: Jellyfin holds 8.1 community and 92 critic; Seerr has RT 92 Certified Fresh, RT audience 95 Upright, IMDb 8.5. Before on Discover: `9.5 star / 92% tomato`; after: `92% tomato, 95% popcorn, IMDb 8.5`. Not captured: backdrop, the real font on a TV, and the Jellyfin "Ends at" time differs between runs because it is clock-based. ## Not verified - No live Seerr data on this host (no Seerr container), so no real `ratingscombined`/`tv/{id}/ratings` response has been parsed. The spec changes follow Conductarr's Go client, its tests, and the PWA's fixtures. - Nothing was run on a device or emulator (no emulator or KVM here, TV's ADB is off). Layout of the new badges at TV size is covered by the before/after capture below, not by on-device use. - Whether the household Jellyfin libraries carry `CriticRating`, and so which owned titles fall back to it.
justinzeus added 3 commits 2026-10-29 20:00:52 +01:00
justinzeus added 1 commit 2026-10-29 20:16:04 +01:00
Show Rotten Tomatoes before IMDb like the PWA
All checks were successful
review/code-claude Claude code review: pass, no blocking findings
c54acc9c1d
Author
Owner

Code review (Claude): PASS, no blocking findings. Reviewed head c54acc9c1d5f37533760fd9da43a7326b43a9f7e.

Checks: make test on that head, 423 tests, 0 failures; PWA rules compared line by line with conductarr ratings.go / TitleRatings.vue (observed-only, TMDB only as fallback, zero-vote unrated, range checks, RT icon from label with PWA defaults); Seerr spec edit exercised with a throwaway parse test on the generated client (decimal IMDb, int64 vote count, RT audience on TV, Certified Fresh, missing/null/unknown fields all parse); old DiscoverRating fully removed; owned-header fallback unchanged when Seerr is inactive / no TMDB id; toggle mapping; four capture PNGs; scope (no home/episode/collection/music/TV guide change, no versionCode/credential/Conductarr); tests are additions only and assert behaviour.

Non-blocking notes (full detail in the review report):

  1. MovieViewModel.init() runs from the VM init and from every resume, so the new never-ending forItem(...).collect is launched repeatedly (2 duplicate ratingscombined calls at open, one extra collector per resume). Keep and cancel a Job, or launch it once.
  2. Ratings cache has no expiry, so a long-running TV app never refreshes a score until restart.
  3. Owned header briefly shows Jellyfin values before the Seerr ones even on a cache hit; cached() is only used by tests and could seed the initial state.
  4. Score text is %.1f (PWA prints the raw number); disclosed in the PR body.
  5. Seerr TMDB-only replaces a Jellyfin critic score (disclosed).
  6. A truly unknown RT label makes the whole response fail to parse, which falls back to Jellyfin / no rating; only Certified Fresh was added.
  7. Import order in five Discover files (pre-commit ktlint autofix).

Not verified: a real Seerr answer (no Seerr on this host); nothing run on a TV.

**Code review (Claude): PASS, no blocking findings.** Reviewed head `c54acc9c1d5f37533760fd9da43a7326b43a9f7e`. Checks: `make test` on that head, 423 tests, 0 failures; PWA rules compared line by line with conductarr `ratings.go` / `TitleRatings.vue` (observed-only, TMDB only as fallback, zero-vote unrated, range checks, RT icon from label with PWA defaults); Seerr spec edit exercised with a throwaway parse test on the generated client (decimal IMDb, int64 vote count, RT audience on TV, `Certified Fresh`, missing/null/unknown fields all parse); old `DiscoverRating` fully removed; owned-header fallback unchanged when Seerr is inactive / no TMDB id; toggle mapping; four capture PNGs; scope (no home/episode/collection/music/TV guide change, no versionCode/credential/Conductarr); tests are additions only and assert behaviour. Non-blocking notes (full detail in the review report): 1. `MovieViewModel.init()` runs from the VM `init` and from every resume, so the new never-ending `forItem(...).collect` is launched repeatedly (2 duplicate `ratingscombined` calls at open, one extra collector per resume). Keep and cancel a Job, or launch it once. 2. Ratings cache has no expiry, so a long-running TV app never refreshes a score until restart. 3. Owned header briefly shows Jellyfin values before the Seerr ones even on a cache hit; `cached()` is only used by tests and could seed the initial state. 4. Score text is `%.1f` (PWA prints the raw number); disclosed in the PR body. 5. Seerr TMDB-only replaces a Jellyfin critic score (disclosed). 6. A truly unknown RT label makes the whole response fail to parse, which falls back to Jellyfin / no rating; only `Certified Fresh` was added. 7. Import order in five Discover files (pre-commit ktlint autofix). Not verified: a real Seerr answer (no Seerr on this host); nothing run on a TV.
justinzeus added 1 commit 2026-10-29 20:28:53 +01:00
Tolerate unknown RT labels, expire cached ratings, avoid duplicate collectors and Jellyfin flash
All checks were successful
review/code-claude Reviewed c54acc9c (PASS); review-fix delta re-checked by firstmate
04d659b115
Author
Owner

Scoped re-check of the review-fix delta (c54acc9c..04d659b1, 5 files, +89/-38), read in full by firstmate: (1) movie screen keeps a ratingsJob and cancels it before relaunching, so resumes no longer stack collectors; the series screen already launches once. (2) A null answer no longer overwrites a value already shown (ratings ?: it.sourceRatings), and the initial state is seeded from cachedFor(item), so a revisit does not flash Jellyfin values. (3) Successful cache entries expire after 6 hours (bound 300 kept). (4) The Seerr spec now types the RT criticsRating/audienceRating labels as free strings (examples only, no enum), so an unexpected label can no longer drop the whole response; the model maps the string label. Tests: 424 pass per the author (one more than before, an unknown-label case). Nothing else changed from the reviewed head c54acc9c, which passed the full review. Status posted by firstmate for the delta.

Scoped re-check of the review-fix delta (`c54acc9c..04d659b1`, 5 files, +89/-38), read in full by firstmate: (1) movie screen keeps a `ratingsJob` and cancels it before relaunching, so resumes no longer stack collectors; the series screen already launches once. (2) A null answer no longer overwrites a value already shown (`ratings ?: it.sourceRatings`), and the initial state is seeded from `cachedFor(item)`, so a revisit does not flash Jellyfin values. (3) Successful cache entries expire after 6 hours (bound 300 kept). (4) The Seerr spec now types the RT `criticsRating`/`audienceRating` labels as free strings (examples only, no enum), so an unexpected label can no longer drop the whole response; the model maps the string label. Tests: 424 pass per the author (one more than before, an unknown-label case). Nothing else changed from the reviewed head `c54acc9c`, which passed the full review. Status posted by firstmate for the delta.
justinzeus merged commit 853a3989b2 into main 2026-10-29 20:29:43 +01:00
Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#24
No description provided.