Merge Wholphin v1.1.0 while preserving Wolfskin features #27

Merged
justinzeus merged 72 commits from fm/wolfskin-upstream-1-1-0 into main 2026-11-05 01:49:35 +01:00
Owner

Outcome

Merge upstream Wholphin v1.1.0 into Wolfskin while preserving every fork delta. This is a real two-parent merge, not a rebase or squash.

  • Wolfskin base: 6bb382a12655216cdf5a590f5ddb24ad0293dfae; upstream tag: 2bb662fd603143753493d13e2b91ac6d18bdb8f5.
  • Head: 670e95f2fa6f7d94c70d36cfbfb2e9d713697c92. git describe --tags --long --match='v*' is v1.1.0-29-g670e95f2; version naming remains upstream-tag-plus-distance, with Wolfskin p markers reserved for an authorized release. No version override, new tag, release, asset upload, or TV installation.
  • Ready for the cross-family review arranged by firstmate; no review status or merge performed by this worker. AI-assisted merge preparation and inspection.
  • GPL notices/license and original Kotlin namespace/packages remain intact. applicationId stays org.justinvisser.wolfskin.tv.

Conflict log

File / conflict Resolution and reason
ui/detail/discover/DiscoverSeriesDetails.kt: rating parameter Combine upstream availability: SeerrAvailability with Wolfskin rating: SourceRatings?. Wolfskin wins the rating type so RT/IMDb/TMDB badges remain; upstream wins the added availability input so corrected request status/permissions work.
ui/setup/seerr/AddSeerrServer.kt: API-key URL Combine as seerrUrlPrefill(initialUrl): upstream/plugin URL wins when nonblank, otherwise Wolfskin's editable https://seerr.justinvisser.org default wins. Initial focus uses the effective URL, so a prefilled URL moves to the key field.
Same file: username/password URL Same combination: retain plugin URL and Wolfskin fallback, without changing authentication method. Wolfskin's Jellyfin-first login chooser order survives the automatic merge.

All other overlapping edits merged automatically. No side-wide ours/theirs checkout. In particular, SourceRatings service/model/row, updater and its Forgejo fixtures, theme/artwork, profile focus, Docker wrapper/lock, Makefile and existing Wolfskin tests are retained. The Conductarr integration currently uses Seerr with per-profile Jellyfin identities and matching PWA ratings; no new Conductarr API or Discover implementation is added.

Wolfskin-path preservation check

The baseline is 116 paths changed by Wolfskin relative to v1.0.9. All 116 still differ from upstream v1.1.0 after this merge; none vanished into an upstream replacement. 83 paths are byte-identical to Wolfskin main, including all fork-added source/resources/tests, updater, artwork, HomePage.kt, UiConstants.kt, QuickDetails.kt, Rating.kt, SourceRatingsRow.kt, and Docker/Makefile code. The 33 changed baseline paths are 32 upstream overlaps plus docker/README.md integration documentation.

Restricted fork delta Files Insertions Deletions
Before: v1.0.9 → Wolfskin main 116 3271 720
After: v1.1.0 → merged HEAD 116 3293 721
PR: main...HEAD, baseline paths only 33 1076 281

Reproduce with the original base rather than a later moving main:

mapfile -t wolfskin_paths < <(git diff --name-only v1.0.9 6bb382a12655216cdf5a590f5ddb24ad0293dfae)
git diff --stat v1.0.9 6bb382a12655216cdf5a590f5ddb24ad0293dfae -- "${wolfskin_paths[@]}"
git diff --stat v1.1.0 670e95f2fa6f7d94c70d36cfbfb2e9d713697c92 -- "${wolfskin_paths[@]}"
git diff main...HEAD --stat -- "${wolfskin_paths[@]}"
Wolfskin-only added files retained (28; 27 unchanged, build README extended)
Makefile
app/src/main/java/com/github/damontecres/wholphin/WolfskinDefaults.kt
app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt
app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt
app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt
app/src/main/java/com/github/damontecres/wholphin/ui/preferences/WolfskinAbout.kt
app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt
app/src/main/res/drawable/ic_rating_imdb.xml
app/src/main/res/drawable/ic_rating_tmdb.xml
app/src/main/res/drawable/ic_rt_spilled.xml
app/src/main/res/drawable/ic_rt_upright.xml
app/src/main/res/values/wolfskin.xml
app/src/test/java/com/github/damontecres/wholphin/WolfskinDefaultsTest.kt
app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt
app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt
app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt
app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt
app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt
app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt
app/src/test/resources/forgejo_release_latest.json
app/src/test/resources/forgejo_release_suffixed.json
app/src/test/resources/forgejo_releases.json
docker/Dockerfile
docker/README.md
docker/gradle.sh
docker/release-signing.properties
docker/release-tag.sh
docker/signing.init.gradle
Full 116-path Wolfskin baseline
Intents.md
Makefile
README.md
app/build.gradle.kts
app/src/debug/res/values/strings.xml
app/src/main/ic_launcher-playstore.png
app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt
app/src/main/java/com/github/damontecres/wholphin/WolfskinDefaults.kt
app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt
app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt
app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt
app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt
app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt
app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt
app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt
app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt
app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt
app/src/main/java/com/github/damontecres/wholphin/ui/cards/WatchedIcon.kt
app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt
app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt
app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt
app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt
app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverUtils.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt
app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt
app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt
app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt
app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt
app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt
app/src/main/java/com/github/damontecres/wholphin/ui/preferences/SwitchPreference.kt
app/src/main/java/com/github/damontecres/wholphin/ui/preferences/WolfskinAbout.kt
app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt
app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt
app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt
app/src/main/java/com/github/damontecres/wholphin/ui/theme/Theme.kt
app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt
app/src/main/proto/WholphinDataStore.proto
app/src/main/res/drawable/ic_rating_imdb.xml
app/src/main/res/drawable/ic_rating_tmdb.xml
app/src/main/res/drawable/ic_rt_spilled.xml
app/src/main/res/drawable/ic_rt_upright.xml
app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
app/src/main/res/mipmap-hdpi/ic_launcher.webp
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
app/src/main/res/mipmap-mdpi/ic_launcher.webp
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
app/src/main/res/mipmap-xhdpi/ic_banner.png
app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
app/src/main/res/values-cs/strings.xml
app/src/main/res/values-da/strings.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-et/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-hr/strings.xml
app/src/main/res/values-in/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-iw/strings.xml
app/src/main/res/values-nb-rNO/strings.xml
app/src/main/res/values-nl/strings.xml
app/src/main/res/values-nn/strings.xml
app/src/main/res/values-pl/strings.xml
app/src/main/res/values-pt-rBR/strings.xml
app/src/main/res/values-pt/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-sk/strings.xml
app/src/main/res/values-sl/strings.xml
app/src/main/res/values-sv/strings.xml
app/src/main/res/values-th/strings.xml
app/src/main/res/values-tr/strings.xml
app/src/main/res/values-uk/strings.xml
app/src/main/res/values-zh-rCN/strings.xml
app/src/main/res/values-zh-rTW/strings.xml
app/src/main/res/values/ic_banner_background.xml
app/src/main/res/values/ic_launcher_background.xml
app/src/main/res/values/strings.xml
app/src/main/res/values/wolfskin.xml
app/src/main/seerr/seerr-api.yml
app/src/test/java/com/github/damontecres/wholphin/WolfskinDefaultsTest.kt
app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt
app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt
app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt
app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt
app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt
app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt
app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt
app/src/test/resources/forgejo_release_latest.json
app/src/test/resources/forgejo_release_suffixed.json
app/src/test/resources/forgejo_releases.json
docker/Dockerfile
docker/README.md
docker/gradle.sh
docker/release-signing.properties
docker/release-tag.sh
docker/signing.init.gradle
Requested main...HEAD --stat restricted to Wolfskin baseline paths
 app/build.gradle.kts                               |   1 +
 .../github/damontecres/wholphin/MainActivity.kt    | 221 +++++++++++----------
 .../damontecres/wholphin/data/model/BaseItem.kt    |   1 +
 .../wholphin/preferences/AppPreference.kt          | 102 ++++++++--
 .../github/damontecres/wholphin/ui/Formatting.kt   |  62 ++++++
 .../wholphin/ui/components/CollectionFolderList.kt |  61 +++---
 .../ui/detail/discover/DiscoverMovieDetails.kt     |  10 +-
 .../ui/detail/discover/DiscoverMovieViewModel.kt   |   9 +-
 .../ui/detail/discover/DiscoverSeriesDetails.kt    |  13 +-
 .../ui/detail/discover/DiscoverSeriesViewModel.kt  |  46 ++++-
 .../wholphin/ui/detail/movie/MovieViewModel.kt     |   9 +-
 .../wholphin/ui/detail/series/SeriesViewModel.kt   |   9 +-
 .../damontecres/wholphin/ui/nav/NavDrawer.kt       |   8 +-
 .../wholphin/ui/preferences/PreferencesContent.kt  | 121 ++++-------
 .../wholphin/ui/setup/seerr/AddSeerrServer.kt      |  25 ++-
 .../ui/setup/seerr/AddSeerrServerDialog.kt         | 136 +++++++++++++
 app/src/main/proto/WholphinDataStore.proto         |  19 +-
 app/src/main/res/values-de/strings.xml             |  40 ++++
 app/src/main/res/values-es/strings.xml             |  15 ++
 app/src/main/res/values-et/strings.xml             |  40 ++++
 app/src/main/res/values-fr/strings.xml             |  58 ++++++
 app/src/main/res/values-in/strings.xml             |  16 ++
 app/src/main/res/values-it/strings.xml             |  38 ++++
 app/src/main/res/values-iw/strings.xml             |  22 +-
 app/src/main/res/values-pl/strings.xml             |  42 ++++
 app/src/main/res/values-pt/strings.xml             |  40 ++++
 app/src/main/res/values-ru/strings.xml             |  39 ++++
 app/src/main/res/values-sk/strings.xml             |  16 ++
 app/src/main/res/values-sv/strings.xml             |   5 +
 app/src/main/res/values-tr/strings.xml             |  43 +++-
 app/src/main/res/values-zh-rTW/strings.xml         |  32 +++
 app/src/main/res/values/strings.xml                |  37 +++-
 docker/README.md                                   |  21 ++
 33 files changed, 1076 insertions(+), 281 deletions(-)

Full before/after patches, stats, immutable-path inventory and logs are preserved outside the checkout under /root/.treehouse/firstmate-7bab20/2/firstmate/data/wolfskin-upstream-1-1-0/.

Build, tests and artifact evidence

GREEN: one make apk and one make test, as requested; no repeated Gradle validation runs.

Command / scenario Observed result
make apk → bash docker/gradle.sh release clean assembleDefaultRelease BUILD SUCCESSFUL in 6m 42s; 70 tasks executed; release-vital lint tasks included; signed universal + armeabi-v7a + arm64-v8a + x86_64 APKs.
make test → bash docker/gradle.sh debug testDefaultDebugUnitTest BUILD SUCCESSFUL in 3m 58s; 66 suites, 550 tests, 0 failures, 0 errors, 0 skipped. Existing assertions retained. Includes Wolfskin ratings/episode scores/row UI, Seerr defaults, active-profile focus, Forgejo updater; upstream folder inheritance, session restoration and Dolby conversion tests.
Temporary SQLite smoke, using the actual Room-generated migration SQL 35→40→41 succeeded; rows in all 11 original tables preserved, schema-41 columns matched, source default UNSET, new track-choice insert, foreign-key check and integrity check passed. No live database touched.
apksigner verify --verbose --print-certs and aapt2 dump badging for all 4 APKs in the pinned Docker image All verify; package org.justinvisser.wolfskin.tv, label Wolfskin, same signing SHA-256 5cd708ffa94cc22fa41648377a305af3afe0f6da394c57e67024df43c1fcb5a0.
ARM32 APK ELF inspection 17 armeabi-v7a native libraries are real 32-bit ARM ELF, including libmpv.so, libffmpegJNI.so, libdav1dJNI.so and new libdovi_android.so. APK SHA-256 173920183c27f3ffb8c8bd46271753f0d9042192903205536fbd9b6e3b7b6ccd.
git diff --check main...HEAD Nonzero: inherited CRLF lines in gradlew.bat are reported as trailing whitespace. Wrapper is byte-identical to v1.1.0 (blob 03754c4c3d9206f5c9ef69e41990dfe26a05251e); do not normalize the upstream Windows wrapper in this merge.

Artifact-version limitation: validation started while the merge was uncommitted. Source code is the merged tree, but Git-derived APK metadata is still 1.0.9-28-g6bb382a1, code 64. These are local proof artifacts, not release-ready APKs. The committed head describes as v1.1.0-29-g670e95f2; any future authorized release must build its committed/tagged head with the existing versioning process. After the successful smoke/build, only docker/README.md documentation was added before committing; no compiled source changed. Working tree is clean.

Exact extra smoke commands:

python3 /root/.treehouse/firstmate-7bab20/2/firstmate/data/wolfskin-upstream-1-1-0/migration_smoke.py /root/.treehouse/wolfskin-8aa825/1/wolfskin
docker run --rm --platform linux/amd64 --mount type=bind,src=/root/.treehouse/wolfskin-8aa825/1/wolfskin/app/build/outputs/apk/default/release,dst=/apks,readonly wolfskin-build:sdk37-jdk21 bash -euc 'for apk in /apks/*.apk; do printf "\nAPK: %s\n" "$apk"; apksigner verify --verbose --print-certs "$apk"; aapt2 dump badging "$apk"; done'

No failing application test was reproduced: this is a release integration, not a standalone bug fix. The first temporary smoke harness attempt failed on an absent optional JSON indices key; correcting the harness to accept a schema without indexes produced the passing run, with no project change or weakened assertion.

Toolchain / CI

Upstream Kotlin 2.4.21, Gradle 9.8.2, AGP 9.5.0, ExoplayerHdrUtils 0.4.1, serialization/dependency bumps imported unchanged. The existing digest-pinned SDK37/JDK21 Docker image and docker/gradle.sh work without code changes; Wholphin native extensions remain 0.2.4. Host flock serialization, --no-daemon and four-worker limit unchanged. There are no tracked dependency lockfiles or verification-metadata files to update, and no existing wrapper distribution checksum field; this does not add a new verification policy. Gradle 10 deprecation/configuration-time resolution, OpenAPI long mapping and Kotlin warnings remain visible, not suppressed. Generated OpenAPI and Room schema files were not edited beyond unchanged upstream files imported by the merge.

Forgejo repository API confirms has_actions=false; upstream GitHub workflow dependency updates are imported but stay dormant, and no Forgejo workflow is enabled. This Android repository has no make check target or Go packages: the copied Conductarr race-test/issue-closing gates are not applicable, and no fictitious Conductarr issue is closed. No Chromium/browser suite, live Arr/provider/queue operation, release asset change, or TV action.

TV layout impact — hand-tuned layout work (wolfskin#10)

  • No upstream changes to HomeRowPresets.kt, ItemGrid.kt or HomePage.kt. Wolfskin's hand-tuned home spacing, watched marks, row/detail ratings and active-profile focus remain; no layout conflict.
  • CollectionFolderGrid.kt / CollectionFolderList.kt now render through CollectionFolderViewScope. PlaylistDetails.kt uses the same CollectionFolderViewContent as libraries, aligning header/view options/grid/list/dense layout and focus/position behavior. This is a visible playlist/library change, not a claim that physical alignment was verified.
  • CollectionFolderViewModel.getAncestorDisplayInfo: a folder without its own view options looks up the first ancestor with saved settings; saved child view/sort wins, inherited sort is used only when saved-display-info is enabled, filters are not inherited, and a display-info-ID override bypasses ancestor lookup. This can change untouched subfolders' view/sort without changing saved preferences.
  • Home row data fetching moves to HomeDataService; main home/row focus code is unchanged. Home settings add a Source row (shifting settings-list focus indices), source selection and explicit save/discard on exit. Unset source order is local → server profile → companion plugin → defaults; selecting/resetting to plugin settings can visibly change rows. Existing local rows remain first in unset-source lookup.

Seerr / Discover and future Conductarr-backed Discover

  • Upstream now distinguishes movie vs TV request permissions and ordinary vs 4K request eligibility; dialogs/button availability honor these. Series availability considers the current user's pending/approved requests, and pending/declined season edits/cancellation handling changes.
  • The optional companion plugin exposes /wholphin/public, /wholphin/homesettings, /wholphin/seerrsettings; it can supply home rows/custom endpoints and Discover URL/credentials. UserSwitchListener tries plugin Seerr login before saved per-profile login. Successful plugin login can use an API key or local account, and plugin-managed connections cannot be removed from the TV UI. If no plugin settings are returned or plugin login returns false, saved per-profile login is tried; an exception while fetching plugin settings can abort that coroutine before fallback.
  • Potential semantic collision: a plugin-provided admin API key/shared account can replace the intended household Jellyfin identity and therefore bypass Conductarr's per-user request routing. Do not configure that as an administrator shortcut. This merge installs/configures no server plugin and preserves the current Jellyfin-first chooser/default Seerr URL; plugin URLs are retained when supplied. Conductarr-backed Discover remains future work, not implemented or designed here.
  • Wolfskin source-rating behavior stays: shared RT/IMDb/TMDB badge row on every existing surface, per-episode TMDB distinction, cache expiry, unknown RT-label handling, profile-aware activation and cancelled/debounced lookups. All existing fork rating tests pass.

Jellyfin 12, session restoration and app database

  • Upstream release notes: this is the final 10.10/10.11-compatible release; v1.2.0 planned for December requires Jellyfin 12+.
  • Code, not inference: WholphinApplication.newMinimumServerVersion = ServerVersion(12, 0, 0). SwitchServerViewModel, SwitchUserViewModel and PreferencesViewModel classify 12+ as SUPPORTED; the future-unsupported warning is for older supported servers. Jellyfin SDK stays 1.7.1. No new rejection or future-unsupported warning for Justin's v12 server; no live-server verification performed.
  • Restoration now uses MostRecentServerSharedPreferences (server URL/ID and user ID/token), revalidates the stored user through getCurrentUser, respects protected profiles, and routes startup errors back to the server list. Hot auto-login can reuse an unprotected current user.
  • Upstream first-launch risk: AppUpgradeHandler migrates old currentServerId/currentUserId only when the current app version is ≤1.0.8-0-g0, not the previous version. A 1.1.0 upgrade therefore does not seed the new recent-server provider from legacy preferences. Cold launch may require selecting the existing server/profile again; the Room records remain. Imported unchanged rather than expanding this merge into an upstream bug fix.
  • One-way TV install: the app database upgrades 35 → 40 → 41 on first launch after install, adding SeriesTrackChoice and users.c_homeSettingsSource (default UNSET). Downgrading while retaining that database is not supported; no downgrade/destructive fallback is registered. Firstmate explicitly authorized unchanged upstream migrations after checking the fork has no Wolfskin Room entities, DAOs or schema changes (only non-Room BaseItem, DiscoverItem, SourceRatings differ). Upstream AppUpgradeHandler converts legacy language choices when the new table is empty. No TV installation or live migration here.

Playback / Xiaomi MiTV-AFMU0 risk notes

Target box: Android 14, armeabi-v7a, S905X5M. JVM/build/ELF evidence does not prove its decoder behavior.

  • HDR10/HDR10+/Dolby Vision 5/7/8 gain AUTO/ENABLE/DISABLE overrides. The old force-profile-7 flag migrates to ENABLE. Incorrect device capability reports or forced overrides can change direct-play/transcode choice or picture/color output.
  • Experimental DV7→8.1 requires opt-in, a profile-8 decoder and no profile-7 decoder. Extractor codec advertisement and native RPU conversion run together; enhancement layer is dropped, no video re-encoding. ARM32 libdovi_android.so is present, but physical Dolby Vision activation, frame cost and passthrough remain unverified. JNI loading failure leaves original frames while the extractor may already advertise profile 8: exercise this only in the approved device window, not by enabling it here.
  • ExoPlayer now requests movie/media audio focus, so background music should pause; this does not change the MPV construction path. Verify focus loss/regain, passthrough and external-player return.
  • MediaSession uses TrustAllControllersCallback: accepts default player commands from all controllers, broader than play/pause alone. Useful for remote/Cast controls, with a larger local-controller command surface. Fire OS voice permission is Fire-flavor only, not the default Xiaomi build.
  • In-play max bitrate changes reopen streams at the current position and submit a new maxStreamingBitrate. Verify resume point/audio/subtitle continuity across direct-play/transcode transitions.
  • Upstream track persistence, AC3 override fix, VLC restart position, image tags, HDHomeRun liveStreamId and pager fixes are imported. Track-choice/schema upgrade plus per-profile Seerr/plugin precedence are the main cross-cutting Wolfskin regression risks; no fork playback override was dropped.

Device verification limit

not smoke-tested on the TV. A smoke test needs Justin's ADB window and explicit installation approval. Walk:

  1. First-launch database upgrade, cold/hot restart, automatic sign-in and protected-profile fallback; both household profiles and active-profile focus.
  2. Home/row focus and settings source save/discard; library and playlist grid/list/dense alignment; subfolder inheritance/child override and sort persistence.
  3. Home quick details, movie/series/episode/focused episode, Discover ratings, episode TMDB, rating toggles and unavailable-Seerr fallback; verify profile identity and ordinary/4K permissions without submitting requests.
  4. Forgejo updater discovery/asset selection (no publish/install); ExoPlayer and MPV movie/episode playback, next-episode audio/subtitle persistence, audio focus/remote controls, bitrate switching and resume.
  5. SDR/HDR10/HDR10+/DV5/7/8 and passthrough/refresh switching; experimental DV7 conversion off/on only with approved suitable content.

Publishing a Wolfskin release or installing/updating on the TV remains outside this PR's authorization.

## Outcome Merge upstream [Wholphin v1.1.0](https://github.com/damontecres/Wholphin/releases/tag/v1.1.0) into Wolfskin while preserving every fork delta. This is a real two-parent merge, not a rebase or squash. - Wolfskin base: `6bb382a12655216cdf5a590f5ddb24ad0293dfae`; upstream tag: `2bb662fd603143753493d13e2b91ac6d18bdb8f5`. - Head: `670e95f2fa6f7d94c70d36cfbfb2e9d713697c92`. `git describe --tags --long --match='v*'` is `v1.1.0-29-g670e95f2`; version naming remains upstream-tag-plus-distance, with Wolfskin `p` markers reserved for an authorized release. No version override, new tag, release, asset upload, or TV installation. - Ready for the cross-family review arranged by firstmate; no review status or merge performed by this worker. AI-assisted merge preparation and inspection. - GPL notices/license and original Kotlin namespace/packages remain intact. `applicationId` stays `org.justinvisser.wolfskin.tv`. ## Conflict log | File / conflict | Resolution and reason | | --- | --- | | `ui/detail/discover/DiscoverSeriesDetails.kt`: rating parameter | Combine upstream `availability: SeerrAvailability` with Wolfskin `rating: SourceRatings?`. Wolfskin wins the rating type so RT/IMDb/TMDB badges remain; upstream wins the added availability input so corrected request status/permissions work. | | `ui/setup/seerr/AddSeerrServer.kt`: API-key URL | Combine as `seerrUrlPrefill(initialUrl)`: upstream/plugin URL wins when nonblank, otherwise Wolfskin's editable `https://seerr.justinvisser.org` default wins. Initial focus uses the effective URL, so a prefilled URL moves to the key field. | | Same file: username/password URL | Same combination: retain plugin URL and Wolfskin fallback, without changing authentication method. Wolfskin's Jellyfin-first login chooser order survives the automatic merge. | All other overlapping edits merged automatically. No side-wide `ours`/`theirs` checkout. In particular, SourceRatings service/model/row, updater and its Forgejo fixtures, theme/artwork, profile focus, Docker wrapper/lock, Makefile and existing Wolfskin tests are retained. The Conductarr integration currently uses Seerr with per-profile Jellyfin identities and matching PWA ratings; no new Conductarr API or Discover implementation is added. ## Wolfskin-path preservation check The baseline is **116 paths** changed by Wolfskin relative to v1.0.9. All 116 still differ from upstream v1.1.0 after this merge; none vanished into an upstream replacement. **83 paths are byte-identical to Wolfskin main**, including all fork-added source/resources/tests, updater, artwork, `HomePage.kt`, `UiConstants.kt`, `QuickDetails.kt`, `Rating.kt`, `SourceRatingsRow.kt`, and Docker/Makefile code. The 33 changed baseline paths are 32 upstream overlaps plus `docker/README.md` integration documentation. | Restricted fork delta | Files | Insertions | Deletions | | --- | ---: | ---: | ---: | | Before: v1.0.9 → Wolfskin main | 116 | 3271 | 720 | | After: v1.1.0 → merged HEAD | 116 | 3293 | 721 | | PR: main...HEAD, baseline paths only | 33 | 1076 | 281 | Reproduce with the original base rather than a later moving main: ```bash mapfile -t wolfskin_paths < <(git diff --name-only v1.0.9 6bb382a12655216cdf5a590f5ddb24ad0293dfae) git diff --stat v1.0.9 6bb382a12655216cdf5a590f5ddb24ad0293dfae -- "${wolfskin_paths[@]}" git diff --stat v1.1.0 670e95f2fa6f7d94c70d36cfbfb2e9d713697c92 -- "${wolfskin_paths[@]}" git diff main...HEAD --stat -- "${wolfskin_paths[@]}" ``` <details><summary>Wolfskin-only added files retained (28; 27 unchanged, build README extended)</summary> ```text Makefile app/src/main/java/com/github/damontecres/wholphin/WolfskinDefaults.kt app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt app/src/main/java/com/github/damontecres/wholphin/ui/preferences/WolfskinAbout.kt app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt app/src/main/res/drawable/ic_rating_imdb.xml app/src/main/res/drawable/ic_rating_tmdb.xml app/src/main/res/drawable/ic_rt_spilled.xml app/src/main/res/drawable/ic_rt_upright.xml app/src/main/res/values/wolfskin.xml app/src/test/java/com/github/damontecres/wholphin/WolfskinDefaultsTest.kt app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt app/src/test/resources/forgejo_release_latest.json app/src/test/resources/forgejo_release_suffixed.json app/src/test/resources/forgejo_releases.json docker/Dockerfile docker/README.md docker/gradle.sh docker/release-signing.properties docker/release-tag.sh docker/signing.init.gradle ``` </details> <details><summary>Full 116-path Wolfskin baseline</summary> ```text Intents.md Makefile README.md app/build.gradle.kts app/src/debug/res/values/strings.xml app/src/main/ic_launcher-playstore.png app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt app/src/main/java/com/github/damontecres/wholphin/WolfskinDefaults.kt app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt app/src/main/java/com/github/damontecres/wholphin/data/model/SourceRatings.kt app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt app/src/main/java/com/github/damontecres/wholphin/services/SeerrRatingsService.kt app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt app/src/main/java/com/github/damontecres/wholphin/ui/cards/WatchedIcon.kt app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderList.kt app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt app/src/main/java/com/github/damontecres/wholphin/ui/components/Rating.kt app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt app/src/main/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRow.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetailsHeader.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverUtils.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetailsHeader.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt app/src/main/java/com/github/damontecres/wholphin/ui/preferences/SwitchPreference.kt app/src/main/java/com/github/damontecres/wholphin/ui/preferences/WolfskinAbout.kt app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServer.kt app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/AddSeerrServerDialog.kt app/src/main/java/com/github/damontecres/wholphin/ui/theme/Theme.kt app/src/main/java/com/github/damontecres/wholphin/ui/theme/colors/WolfskinThemeColors.kt app/src/main/proto/WholphinDataStore.proto app/src/main/res/drawable/ic_rating_imdb.xml app/src/main/res/drawable/ic_rating_tmdb.xml app/src/main/res/drawable/ic_rt_spilled.xml app/src/main/res/drawable/ic_rt_upright.xml app/src/main/res/mipmap-anydpi-v26/ic_banner.xml app/src/main/res/mipmap-hdpi/ic_launcher.webp app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp app/src/main/res/mipmap-hdpi/ic_launcher_round.webp app/src/main/res/mipmap-mdpi/ic_launcher.webp app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp app/src/main/res/mipmap-mdpi/ic_launcher_round.webp app/src/main/res/mipmap-xhdpi/ic_banner.png app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png app/src/main/res/mipmap-xhdpi/ic_launcher.webp app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp app/src/main/res/mipmap-xxhdpi/ic_launcher.webp app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp app/src/main/res/values-cs/strings.xml app/src/main/res/values-da/strings.xml app/src/main/res/values-de/strings.xml app/src/main/res/values-es/strings.xml app/src/main/res/values-et/strings.xml app/src/main/res/values-fr/strings.xml app/src/main/res/values-hr/strings.xml app/src/main/res/values-in/strings.xml app/src/main/res/values-it/strings.xml app/src/main/res/values-iw/strings.xml app/src/main/res/values-nb-rNO/strings.xml app/src/main/res/values-nl/strings.xml app/src/main/res/values-nn/strings.xml app/src/main/res/values-pl/strings.xml app/src/main/res/values-pt-rBR/strings.xml app/src/main/res/values-pt/strings.xml app/src/main/res/values-ru/strings.xml app/src/main/res/values-sk/strings.xml app/src/main/res/values-sl/strings.xml app/src/main/res/values-sv/strings.xml app/src/main/res/values-th/strings.xml app/src/main/res/values-tr/strings.xml app/src/main/res/values-uk/strings.xml app/src/main/res/values-zh-rCN/strings.xml app/src/main/res/values-zh-rTW/strings.xml app/src/main/res/values/ic_banner_background.xml app/src/main/res/values/ic_launcher_background.xml app/src/main/res/values/strings.xml app/src/main/res/values/wolfskin.xml app/src/main/seerr/seerr-api.yml app/src/test/java/com/github/damontecres/wholphin/WolfskinDefaultsTest.kt app/src/test/java/com/github/damontecres/wholphin/data/model/SourceRatingsTest.kt app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsItemsTest.kt app/src/test/java/com/github/damontecres/wholphin/services/SeerrRatingsServiceTest.kt app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt app/src/test/java/com/github/damontecres/wholphin/ui/WolfskinThemeColorsTest.kt app/src/test/java/com/github/damontecres/wholphin/ui/components/SourceRatingsRowTest.kt app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt app/src/test/resources/forgejo_release_latest.json app/src/test/resources/forgejo_release_suffixed.json app/src/test/resources/forgejo_releases.json docker/Dockerfile docker/README.md docker/gradle.sh docker/release-signing.properties docker/release-tag.sh docker/signing.init.gradle ``` </details> <details><summary>Requested main...HEAD --stat restricted to Wolfskin baseline paths</summary> ```text app/build.gradle.kts | 1 + .../github/damontecres/wholphin/MainActivity.kt | 221 +++++++++++---------- .../damontecres/wholphin/data/model/BaseItem.kt | 1 + .../wholphin/preferences/AppPreference.kt | 102 ++++++++-- .../github/damontecres/wholphin/ui/Formatting.kt | 62 ++++++ .../wholphin/ui/components/CollectionFolderList.kt | 61 +++--- .../ui/detail/discover/DiscoverMovieDetails.kt | 10 +- .../ui/detail/discover/DiscoverMovieViewModel.kt | 9 +- .../ui/detail/discover/DiscoverSeriesDetails.kt | 13 +- .../ui/detail/discover/DiscoverSeriesViewModel.kt | 46 ++++- .../wholphin/ui/detail/movie/MovieViewModel.kt | 9 +- .../wholphin/ui/detail/series/SeriesViewModel.kt | 9 +- .../damontecres/wholphin/ui/nav/NavDrawer.kt | 8 +- .../wholphin/ui/preferences/PreferencesContent.kt | 121 ++++------- .../wholphin/ui/setup/seerr/AddSeerrServer.kt | 25 ++- .../ui/setup/seerr/AddSeerrServerDialog.kt | 136 +++++++++++++ app/src/main/proto/WholphinDataStore.proto | 19 +- app/src/main/res/values-de/strings.xml | 40 ++++ app/src/main/res/values-es/strings.xml | 15 ++ app/src/main/res/values-et/strings.xml | 40 ++++ app/src/main/res/values-fr/strings.xml | 58 ++++++ app/src/main/res/values-in/strings.xml | 16 ++ app/src/main/res/values-it/strings.xml | 38 ++++ app/src/main/res/values-iw/strings.xml | 22 +- app/src/main/res/values-pl/strings.xml | 42 ++++ app/src/main/res/values-pt/strings.xml | 40 ++++ app/src/main/res/values-ru/strings.xml | 39 ++++ app/src/main/res/values-sk/strings.xml | 16 ++ app/src/main/res/values-sv/strings.xml | 5 + app/src/main/res/values-tr/strings.xml | 43 +++- app/src/main/res/values-zh-rTW/strings.xml | 32 +++ app/src/main/res/values/strings.xml | 37 +++- docker/README.md | 21 ++ 33 files changed, 1076 insertions(+), 281 deletions(-) ``` </details> Full before/after patches, stats, immutable-path inventory and logs are preserved outside the checkout under `/root/.treehouse/firstmate-7bab20/2/firstmate/data/wolfskin-upstream-1-1-0/`. ## Build, tests and artifact evidence **GREEN:** one `make apk` and one `make test`, as requested; no repeated Gradle validation runs. | Command / scenario | Observed result | | --- | --- | | `make apk` → `bash docker/gradle.sh release clean assembleDefaultRelease` | `BUILD SUCCESSFUL in 6m 42s`; 70 tasks executed; release-vital lint tasks included; signed universal + armeabi-v7a + arm64-v8a + x86_64 APKs. | | `make test` → `bash docker/gradle.sh debug testDefaultDebugUnitTest` | `BUILD SUCCESSFUL in 3m 58s`; 66 suites, **550 tests, 0 failures, 0 errors, 0 skipped**. Existing assertions retained. Includes Wolfskin ratings/episode scores/row UI, Seerr defaults, active-profile focus, Forgejo updater; upstream folder inheritance, session restoration and Dolby conversion tests. | | Temporary SQLite smoke, using the actual Room-generated migration SQL | 35→40→41 succeeded; rows in all 11 original tables preserved, schema-41 columns matched, source default `UNSET`, new track-choice insert, foreign-key check and integrity check passed. No live database touched. | | `apksigner verify --verbose --print-certs` and `aapt2 dump badging` for all 4 APKs in the pinned Docker image | All verify; package `org.justinvisser.wolfskin.tv`, label `Wolfskin`, same signing SHA-256 `5cd708ffa94cc22fa41648377a305af3afe0f6da394c57e67024df43c1fcb5a0`. | | ARM32 APK ELF inspection | 17 armeabi-v7a native libraries are real 32-bit ARM ELF, including `libmpv.so`, `libffmpegJNI.so`, `libdav1dJNI.so` and new `libdovi_android.so`. APK SHA-256 `173920183c27f3ffb8c8bd46271753f0d9042192903205536fbd9b6e3b7b6ccd`. | | `git diff --check main...HEAD` | **Nonzero:** inherited CRLF lines in `gradlew.bat` are reported as trailing whitespace. Wrapper is byte-identical to v1.1.0 (blob `03754c4c3d9206f5c9ef69e41990dfe26a05251e`); do not normalize the upstream Windows wrapper in this merge. | **Artifact-version limitation:** validation started while the merge was uncommitted. Source code is the merged tree, but Git-derived APK metadata is still `1.0.9-28-g6bb382a1`, code `64`. These are local proof artifacts, **not release-ready APKs**. The committed head describes as `v1.1.0-29-g670e95f2`; any future authorized release must build its committed/tagged head with the existing versioning process. After the successful smoke/build, only `docker/README.md` documentation was added before committing; no compiled source changed. Working tree is clean. Exact extra smoke commands: ```bash python3 /root/.treehouse/firstmate-7bab20/2/firstmate/data/wolfskin-upstream-1-1-0/migration_smoke.py /root/.treehouse/wolfskin-8aa825/1/wolfskin docker run --rm --platform linux/amd64 --mount type=bind,src=/root/.treehouse/wolfskin-8aa825/1/wolfskin/app/build/outputs/apk/default/release,dst=/apks,readonly wolfskin-build:sdk37-jdk21 bash -euc 'for apk in /apks/*.apk; do printf "\nAPK: %s\n" "$apk"; apksigner verify --verbose --print-certs "$apk"; aapt2 dump badging "$apk"; done' ``` No failing application test was reproduced: this is a release integration, not a standalone bug fix. The first temporary smoke harness attempt failed on an absent optional JSON `indices` key; correcting the harness to accept a schema without indexes produced the passing run, with no project change or weakened assertion. ### Toolchain / CI Upstream Kotlin **2.4.21**, Gradle **9.8.2**, AGP **9.5.0**, ExoplayerHdrUtils **0.4.1**, serialization/dependency bumps imported unchanged. The existing digest-pinned SDK37/JDK21 Docker image and `docker/gradle.sh` work without code changes; Wholphin native extensions remain **0.2.4**. Host `flock` serialization, `--no-daemon` and four-worker limit unchanged. There are **no tracked dependency lockfiles or verification-metadata files** to update, and no existing wrapper distribution checksum field; this does not add a new verification policy. Gradle 10 deprecation/configuration-time resolution, OpenAPI `long` mapping and Kotlin warnings remain visible, not suppressed. Generated OpenAPI and Room schema files were not edited beyond unchanged upstream files imported by the merge. Forgejo repository API confirms `has_actions=false`; upstream GitHub workflow dependency updates are imported but stay dormant, and no Forgejo workflow is enabled. This Android repository has no `make check` target or Go packages: the copied Conductarr race-test/issue-closing gates are not applicable, and no fictitious Conductarr issue is closed. No Chromium/browser suite, live Arr/provider/queue operation, release asset change, or TV action. ## TV layout impact — hand-tuned layout work (wolfskin#10) - **No upstream changes to `HomeRowPresets.kt`, `ItemGrid.kt` or `HomePage.kt`**. Wolfskin's hand-tuned home spacing, watched marks, row/detail ratings and active-profile focus remain; no layout conflict. - `CollectionFolderGrid.kt` / `CollectionFolderList.kt` now render through `CollectionFolderViewScope`. `PlaylistDetails.kt` uses the same `CollectionFolderViewContent` as libraries, aligning header/view options/grid/list/dense layout and focus/position behavior. This is a visible playlist/library change, not a claim that physical alignment was verified. - `CollectionFolderViewModel.getAncestorDisplayInfo`: a folder without its own view options looks up the first ancestor with saved settings; saved child view/sort wins, inherited sort is used only when saved-display-info is enabled, **filters are not inherited**, and a display-info-ID override bypasses ancestor lookup. This can change untouched subfolders' view/sort without changing saved preferences. - Home row data fetching moves to `HomeDataService`; main home/row focus code is unchanged. Home settings add a Source row (shifting settings-list focus indices), source selection and explicit save/discard on exit. Unset source order is local → server profile → companion plugin → defaults; selecting/resetting to plugin settings can visibly change rows. Existing local rows remain first in unset-source lookup. ## Seerr / Discover and future Conductarr-backed Discover - Upstream now distinguishes movie vs TV request permissions and ordinary vs 4K request eligibility; dialogs/button availability honor these. Series availability considers the current user's pending/approved requests, and pending/declined season edits/cancellation handling changes. - The optional companion plugin exposes `/wholphin/public`, `/wholphin/homesettings`, `/wholphin/seerrsettings`; it can supply home rows/custom endpoints and Discover URL/credentials. `UserSwitchListener` tries plugin Seerr login **before** saved per-profile login. Successful plugin login can use **an API key or local account**, and plugin-managed connections cannot be removed from the TV UI. If no plugin settings are returned or plugin login returns false, saved per-profile login is tried; an exception while fetching plugin settings can abort that coroutine before fallback. - **Potential semantic collision:** a plugin-provided admin API key/shared account can replace the intended household Jellyfin identity and therefore bypass Conductarr's per-user request routing. Do not configure that as an administrator shortcut. This merge installs/configures no server plugin and preserves the current Jellyfin-first chooser/default Seerr URL; plugin URLs are retained when supplied. Conductarr-backed Discover remains future work, not implemented or designed here. - Wolfskin source-rating behavior stays: shared RT/IMDb/TMDB badge row on every existing surface, per-episode TMDB distinction, cache expiry, unknown RT-label handling, profile-aware activation and cancelled/debounced lookups. All existing fork rating tests pass. ## Jellyfin 12, session restoration and app database - [Upstream release notes](https://github.com/damontecres/Wholphin/releases/tag/v1.1.0): this is the final 10.10/10.11-compatible release; v1.2.0 planned for December requires Jellyfin 12+. - Code, not inference: `WholphinApplication.newMinimumServerVersion = ServerVersion(12, 0, 0)`. `SwitchServerViewModel`, `SwitchUserViewModel` and `PreferencesViewModel` classify **12+ as SUPPORTED**; the future-unsupported warning is for older supported servers. Jellyfin SDK stays **1.7.1**. No new rejection or future-unsupported warning for Justin's v12 server; no live-server verification performed. - Restoration now uses `MostRecentServerSharedPreferences` (server URL/ID and user ID/token), revalidates the stored user through `getCurrentUser`, respects protected profiles, and routes startup errors back to the server list. Hot auto-login can reuse an unprotected current user. - **Upstream first-launch risk:** `AppUpgradeHandler` migrates old `currentServerId/currentUserId` only when the **current app version** is ≤`1.0.8-0-g0`, not the previous version. A 1.1.0 upgrade therefore does not seed the new recent-server provider from legacy preferences. Cold launch may require selecting the existing server/profile again; the Room records remain. Imported unchanged rather than expanding this merge into an upstream bug fix. - **One-way TV install:** the app database upgrades **35 → 40 → 41 on first launch after install**, adding `SeriesTrackChoice` and `users.c_homeSettingsSource` (default `UNSET`). Downgrading while retaining that database is **not supported**; no downgrade/destructive fallback is registered. Firstmate explicitly authorized unchanged upstream migrations after checking the fork has **no Wolfskin Room entities, DAOs or schema changes** (only non-Room `BaseItem`, `DiscoverItem`, `SourceRatings` differ). Upstream `AppUpgradeHandler` converts legacy language choices when the new table is empty. No TV installation or live migration here. ## Playback / Xiaomi MiTV-AFMU0 risk notes Target box: Android 14, armeabi-v7a, S905X5M. JVM/build/ELF evidence does not prove its decoder behavior. - HDR10/HDR10+/Dolby Vision 5/7/8 gain AUTO/ENABLE/DISABLE overrides. The old force-profile-7 flag migrates to ENABLE. Incorrect device capability reports or forced overrides can change direct-play/transcode choice or picture/color output. - Experimental DV7→8.1 requires opt-in, a profile-8 decoder and no profile-7 decoder. Extractor codec advertisement and native RPU conversion run together; enhancement layer is dropped, no video re-encoding. ARM32 `libdovi_android.so` is present, but physical Dolby Vision activation, frame cost and passthrough remain unverified. JNI loading failure leaves original frames while the extractor may already advertise profile 8: exercise this only in the approved device window, not by enabling it here. - ExoPlayer now requests movie/media **audio focus**, so background music should pause; this does not change the MPV construction path. Verify focus loss/regain, passthrough and external-player return. - MediaSession uses `TrustAllControllersCallback`: accepts default player commands from all controllers, broader than play/pause alone. Useful for remote/Cast controls, with a larger local-controller command surface. Fire OS voice permission is Fire-flavor only, not the default Xiaomi build. - In-play **max bitrate** changes reopen streams at the current position and submit a new maxStreamingBitrate. Verify resume point/audio/subtitle continuity across direct-play/transcode transitions. - Upstream track persistence, AC3 override fix, VLC restart position, image tags, HDHomeRun liveStreamId and pager fixes are imported. Track-choice/schema upgrade plus per-profile Seerr/plugin precedence are the main cross-cutting Wolfskin regression risks; no fork playback override was dropped. ## Device verification limit **not smoke-tested on the TV**. A smoke test needs Justin's ADB window and explicit installation approval. Walk: 1. First-launch database upgrade, cold/hot restart, automatic sign-in and protected-profile fallback; both household profiles and active-profile focus. 2. Home/row focus and settings source save/discard; library and playlist grid/list/dense alignment; subfolder inheritance/child override and sort persistence. 3. Home quick details, movie/series/episode/focused episode, Discover ratings, episode TMDB, rating toggles and unavailable-Seerr fallback; verify profile identity and ordinary/4K permissions **without submitting requests**. 4. Forgejo updater discovery/asset selection (no publish/install); ExoPlayer and MPV movie/episode playback, next-episode audio/subtitle persistence, audio focus/remote controls, bitrate switching and resume. 5. SDR/HDR10/HDR10+/DV5/7/8 and passthrough/refresh switching; experimental DV7 conversion off/on only with approved suitable content. Publishing a Wolfskin release or installing/updating on the TV remains outside this PR's authorization.
justinzeus added 72 commits 2026-11-05 01:43:51 +01:00
## Description
This PR reworks some of the internal handling of changing users and auto
sign in.

1. Most recently used server/user is migrated from `AppPreferences` to
`MostRecentServerProvider`
2. Checking if a user profile is protected occurs at a lower level in
`ServerRepository` so fewer services need to check for this
3. Now always defaults to the most recent server's user list in cases
where the most recent user can't be switched to such as if the profile
is protected, auto signin is disabled, or and error occurred

The default implementation for `MostRecentServerProvider` uses Android's
`SharedPreferences`. `SharedPreferences` was already was being used to
track the most recent server for crash reporting (the crash reporting
process cannot use the Hilt DI). This PR enhances this and adds a better
API for saving/retrieving.

This also removes the last bit of "user specific" data from
`AppPreferences`.

### Related issues
None specific, but there have been a few related to auto sign-in &
intents that were recently fixed. This PR is a more detailed approach to
those same problems.

### Testing
Manually via the emulator. There are also unit tests for most (all?) of
the different settings and state combinations.

## Screenshots
N/A

## AI or LLM usage
None
Description
Adding per-format HDR override preferences (HDR10, HDR10+, Dolby Vision
Profile 5/7/8) to control device compatibility information sent to the
Jellyfin server, so users can force specific formats on or off
independently instead of disabling Dolby Vision as a whole.

Related issues
This is a common problem across the Jellyfin ecosystem, but there isn't
a specific issue for Wholphin. My experience is the issue is FireTV
(model and generation) + TV dependent + media dependent. I tested videos
from
https://kodi.wiki/view/Samples with the same DOVIWithHDR10 flag that
worked while other movies I have with It is a very frustrating user
experience and the current workaround of setting the bitrate really low
to force a transcode is sub-optimal IMO as it lowers the video quality.
This allows the user to maintain full quality while disabling only the
specific format(s) causing problems.

https://github.com/jellyfin/jellyfin-androidtv/issues/4021
https://github.com/jellyfin/jellyfin-androidtv/issues/4892
https://github.com/nova-video-player/aos-AVP/issues/1503

https://www.reddit.com/r/jellyfin/comments/12jqoxh/dolby_vision_video_not_working_on_fire_stick_4k/

Testing

<!-- Describe how this change was tested and on what device(s) -->
FireTV 4K (gen 1) + Vizio
FireTV 4K Max (gen 1) + LG
FireTV 4K Max (gen 2) + Vizio
FireTV (gen 1) + Olevia


AI or LLM usage
Claude code assisted and wrote much of the code in this PR. I've
reviewed it all and run the pre-commit linting and formatting.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
## Description

This change applies only to subfolders that do not have their own saved
display settings.

When a subfolder has no saved settings, it now inherits the view type
and sort order from the closest ancestor with saved settings. Filter
settings are not inherited.

If the user changes the settings within a subfolder, those settings are
saved for that subfolder and take precedence over the inherited
settings.

### Related issues

Fixes #1768

### Testing

Tested an App Store flavor debug build on the following setup:

- Device: Xiaomi TV A Pro 43
- Jellyfin server version: 12.1
- Library: A hierarchical recording library stored on a NAS

I changed the view type and sort order at the top level of the library
and confirmed that both settings were inherited by untouched subfolders.

## Screenshots

N/A - no UI changes

## AI or LLM usage

Claude was used to write the changes in `CollectionFolderView.kt`.

I performed the testing on a physical Google TV device and verified the
behavior described above.

The commit also includes a `Co-Authored-By: Claude` trailer.

This description was written in Japanese and translated with an LLM.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Description
This PR overhauls how audio and subtitle track persistence works for
series! Previously, when you chose an audio or subtitle track for an
episode, only the language was saved and used for subsequent episodes.
This meant aspects about why that choice were lost such as a forced or
hearing impaired track.

This PR changes the persistence to include more details about the chosen
track. Additionally, the persistence is saved at both the season and
series levels.

## How it works

Selecting the track uses the first success option:
1. Episode specific - User has explicitly chosen an audio/subtitle track
for this episode
2. Season specific - User has chosen the track for another episode in
this season
3. Series specific - User has chosen the track for another episode in
this series
4. User preferences - User has _not_ chosen a track for the episode,
season, nor series, so determine track based on the preferred audio
language, preferred subtitle language, and subtitle mode

Additionally, the season or series specific choices use a scoring
algorithm to find the best suited track. For example, if the chosen
tracks for an episode in the season had the forced flag, this will be
preferred for subsequent episodes. Same for SDH/hearing impaired or
title, etc. This means that the common case, where a season has
substantially identical tracks, will use the same ones across all
episodes (after choosing for one, otherwise falling back to the series
or user preferences).

Finally, this also means that users that prefer SDH/hearing impaired
subtitles can be prioritized now, though there is no global preference
for this yet.

The previous track language choices are migrated to the new algorithm.
They lack the additional context, but this makes for a seamless
transition.

## UI changes

The context menu for choosing audio or subtitle tracks now displays why
that track was chosen.

### Related issues
Fixes #1795
Maybe fixes #1783
Related to #1136 & #1798
Closes #1470

### Testing
Previous unit tests for track selection all pass, plus there are a few
additional unit tests. I've done some testing on the emulator and it
seems to work well. But I think real world testing will be needed.

## AI or LLM usage
None
## Description

Wholphin builds item image URLs without Jellyfin's `tag` query
parameter. `ImageUrlService` already receives the item's `imageTags` and
`backdropTags`, and the low-level `getItemImageUrl` supports `tag`, but
no branch passed it. Today only chapter and now-playing images set it.

Before: `/Items/{id}/Images/Primary?quality=96&fillHeight=229`
After:
`/Items/{id}/Images/Primary?quality=96&fillHeight=229&tag={ImageTags.Primary}`

This is how the official Jellyfin clients request images. jellyfin-web
and the official Jellyfin Android TV app both copy `ImageTags[...]` (and
the parent/series tags) into the image URL.

I also added `ImageUrlServiceTest`, which covers each branch.


### Related issues

I tested each one on the emulator against a stock Jellyfin 12.1 server
(no plugins), comparing `main` with this branch:

- **#366 (Image cache expiration): fixed by this PR.**
- **#1249 (blank tile for a newly added movie) and #1338 (episode cover
never refreshes): already fixed on `main` since v1.0.3, not by this
PR.** 
- **#360 (Option to hide spoilers): partly.**

## AI or LLM usage

This change was made with AI assistance (Claude Code). It was used for:
- the code changes in `ImageUrlService` and the call sites listed above
- the new `ImageUrlServiceTest`
- the emulator testing and benchmarks
- drafting this description

The testing is what's described above: unit tests, an Android TV
emulator against Jellyfin 12.1 servers (with Jellyfin Enhanced and
stock), request logs from a proxy, and a check of every tag against the
server's metadata. I've reviewed the change and can explain it.

I'm also a maintainer on Jellyfin Enhanced, which is how I ran into
this. I'm happy to share examples of my own coding work if that helps
with reviewing this.
## Description
The refactor in https://github.com/damontecres/Wholphin/pull/1955
(included in `v1.0.9`) swapped the branches for the experimental "Use
AC3 for surround sound audio" breaking it.

Unfortunately, there isn't a real workaround to get the AC3 behavior
back. With the experimental settings enabled & "Use AC3 for surround
sound audio" _disabled_, surround sound audio will transcode to stereo
AAC. Otherwise if experimental settings are _disabled_ and/or "Use AC3
for surround sound audio" is _enabled_, there's no effect.

### Related issues
Credit to @StefanBS who opened #2090, but closed it before I verified
the bug
Closes #2093

### Testing
Manually via emulator

## Screenshots
N/A

## AI or LLM usage
None
## Description
No longer assume that if a user has "Request 4K" then they must also
have regular "Request". Now the requesting dialogs will handle:
- User has only "Request" (including just "Request Movie" vs "Request
TV")
- User has only "Request 4K"
- User has both

Also fixes an issue where the request button would be disabled if a
previous request was declined.

And also because it helped me with testing this and is useful, the
dialog in settings to remove a Seerr server now also shows the username
in addition to URL.

### Related issues

https://github.com/damontecres/Wholphin/issues/702#issuecomment-6132914691

### Testing
Manually on the emulator with the test cases above. I also tested with
and without "Advanced Requests" permission

## Screenshots
N/A, the dialog change is minor

## AI or LLM usage
None
## Description
PR to add support for a Wholphin server plugin:
https://github.com/damontecres/jellyfin-plugin-wholphin

Note: The plugin is not required to use Wholphin. It adds features the
server admin can use to set default settings like the home page or
eventually Seerr connection info.

### Changes
Wholphin will use the home page settings in order of:
1. Saved on the device locally
2. Saved on the server in the user's profile settings (display
preferences)
3. Provided by `jellyfin-plugin-wholphin`
4. Default fallback: combined continue watching & recently added in
libraries

Under customize home page, you can now see the "source" of the home
settings. This also allows to pick a different source

Finally, instead of the home customization being saved in the background
only locally, if there are any changes, a dialog is shown with options
to save locally, server-side user profile, or discard.

So overall, it's more clear where the home page settings are from and
where the user wants to save them.

### Install

[See instructions
here](https://github.com/damontecres/Wholphin/releases/tag/develop-server-plugin)

### Related issues
Related to #598

### Testing
WIP

## Screenshots
N/A

## AI or LLM usage
None

---------

Co-authored-by: Mario Reisinger <marioreisinger95@gmail.com>
## Description
Rename music "Top Rated Unwatched" row to "Top Rated"

### Related issues
Typo.

### Testing
Android TV

## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
<img width="1920" height="1080" alt="pr2-music-recommended-top-rated"
src="https://github.com/user-attachments/assets/3978bc9b-a020-4ee7-ae35-feb19e19ebae"
/>

## AI or LLM usage
Claude wrote the code, I reviewed, deploed a -debug.apk to my device and
tested live.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## Description
Now that Jellyfin 12 has been out for over six weeks, it's time to start
considering dropping support for Jellyfin 10 & 11 as part of #1640.

This PR adds a warning message on the switch server, switch user, &
preferences pages if the current server version is <12.

### Related issues
See https://github.com/damontecres/Wholphin/discussions/1640 for further
discussion!

### Testing
Emulator w/ faked data

## Screenshots
N/A, reuses the same error message space

## AI or LLM usage
None
## Description
This PR updates the playlist details page to more closely match the look
of a library grid tab. This allows for a few things:
- Customize layout (grid, list, backdrop, etc)
- Show logos
- Random button
- The play all/filter/etc buttons no longer awkwardly show all the time

Basically, a playlist details will default to the list layout with "Show
details" enabled

The library list layout was actually based on the playlist details
layout originally, so this unifies the code. This means a bit less
maintenance and improvements to one page benefits the other.

### Dev note

This PR adds a `CollectionFolderViewScope` to pass down information
about the current collection and layout. This allows for easier
customizing of components. The PR also lifts the card & list item
composable to be parameters of `CollectionFolderViewContent` which,
again, allows for lots of customization.

### Related issues
Related to
https://github.com/damontecres/Wholphin/discussions/2078#discussioncomment-19046988

### Testing
Emulator

## Screenshots

### Before
<img width="1280" height="720" alt="playlist_ui_before Large"
src="https://github.com/user-attachments/assets/1cf8db82-4f53-4f20-a05e-ab937ffa3e3f"
/>

### After
<img width="1280" height="720" alt="playlist_ui_after Large"
src="https://github.com/user-attachments/assets/96ee6c1a-1a20-44dc-b979-0d314539a04a"
/>

## AI or LLM usage
None
## Description
Just a small change to AndroidManifest.xml so that Alexa voice commands
work in Wholphin.

"To enable voice command and control to work with your application, add
the permission com.amazon.permission.media.session.voicecommandcontrol
to your application manifest"
See
https://developer.amazon.com/docs/fire-tv/mediasession-api-integration.html

This is required only for Fire OS <14.

### Related issues
I couldn't find any related issues.

Steps to reproduce: Start some media -> Tell Alexa to pause/play
("Alexa, pause", "Alexa, play")
Expected behavior: Media pauses/resumes almost immediately.
Actual behavior: Pausing takes ~10 seconds, resuming doesn't work at all
Environment: 
- FireTV Cube Gen 3 running Fire OS 7.7.1.7 (PS7717/5741)
- Wholphin 1.0.9-0-g2d87d526
- Jellyfin server 12.2

### Testing
See environment above.
After the change pause/play works immediately.

## Screenshots
N/A

## AI or LLM usage
I used Claude to analyze the issue and Claude Code to build a debug app
for testing.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description

When resetting the watch state or choosing to start from the beginning,
the "from_start" flag isn't passed to VLC. Because of that, VLC doesn't
restart playback from the beginning as expected.

This change adds the flag when needed so VLC starts from the correct
position.

## AI or LLM usage

я использовал Codex

Co-authored-by: osipovgleb <19697793+osipovgleb@users.noreply.github.com>
## Description
Show album artist under album name on album cards

### Related issues
Fixes #2055

### Testing
Android TV 

## Screenshots
<img width="1920" height="1080" alt="pr1-albums-grid-artist-year"
src="https://github.com/user-attachments/assets/d00a8f78-8bf9-41da-8220-5cc7f6862cf5"
/>

### AI or LLM usage
Claude did the code, I reviewed it, then deployed a -debug.apk to my
device to confirm feature live.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## Description
https://github.com/damontecres/jellyfin-plugin-wholphin/pull/3 added a
new endpoint for Seerr configuration in Wholphin server plugin.

This PR adds support for reading that configuration and either
pre-filling the URL or automatically logging in.

Some code was copied from #1418 (thanks @matschi95!), but that PR worked
by inserting a DB record for the plugin provided server. Since the local
DB was preferred, this meant plugin config changes wouldn't be reflected
in the client. This PR instead attempts the connection and only updates
the in-memory connection state, so user switching and app restarts will
re-fetch from the plugin.

If the server plugin is not installed or no Seerr server is configured,
nothing in Wholphin changes.

### Related issues
#598 

This PR replaces #1418


### Testing
Emulator with different scenarios

## Screenshots
N/A

## AI or LLM usage
None
## Description
Adds information to the debug page about detected HDR format for both
the device and the display.

I also wanted to try out the new `Grid` layout composable! It's much
better than the `SimpleTable`/`TableRow` composables I wrote previously.

### Related issues
There's a few issues/PR for improvements to HDR/DV support, so this will
help with debugging

### Testing
Emulator & nvidia shield on HDR TV

## Screenshots
N/A, just tables of info

## AI or LLM usage
None
## Description

**On Amlogic boxes (S905X4 and similar) Dolby Vision Profile 7 does not
play as Dolby Vision.**
The server transcodes it, or, with "Direct play Dolby Vision Profile 7"
switched on by hand, the
decoder answers `NoSupport` and fallback ends in HDR10. Devices with a
native Profile 7 decoder,
such as the Shield, are not affected and nothing changes for them.

This PR converts Profile 7 to 8.1 on the fly. It has two halves and one
decision.

The preference `convertDolbyVisionProfile7` is proto field 6 in the
experimental settings, off by
default. In `WholphinRenderersFactory` the filter comes first in the
list, followed by the HDR10+
masking from #1876.

**What this PR does not do.** The enhancement layer is dropped; that is
the price of the
conversion, which is why devices with a native Profile 7 decoder are
left alone.
`BlockAdditional` (an enhancement layer outside the stream) is not read
— like the rest of
Wholphin today — so such a file stays HDR10, as it is now.

### Related issues

Closes #244

Builds on #1876 (the `BitstreamFilter` list). Related to #1540. @fanni42
asked for exactly this on
an S905X4 in #1876.

### Testing

**Hardware** — a debug build of this branch, Homatics box (Amlogic
S905X4, Android 14) + LG BX,
Jellyfin 12.1.0, a Profile 7 FEL remux with the enhancement layer in
band, played from the start
for 39 seconds. Only the new preference was switched on; manual "Direct
play Dolby Vision
Profile 7" was **off**:

* `DolbyVisionProfile7FilterTest` (14) — a fake rewriter on a direct
buffer: offset ≠ 0,
position/limit untouched, every "bytes stay as they were" branch,
three-byte start codes,
  `UnsatisfiedLinkError` tried once;
* `DolbyVisionProfile7ExtractorsFactoryTest` (5, mockk) — only the video
track is rewritten;
* `DolbyVisionProfile7ConversionTest` (6) — codec strings, DRM, `dvh1`.

CI in my fork ran on this branch plus a debug signing commit:
`pre-commit`,
`testDefaultDebugUnitTest` and `assembleDefaultDebug` are all green. The
hardware measurement
above is that build.

## Screenshots

The only UI change is one toggle in the experimental settings.

## AI or LLM usage

The code was written with Claude (Claude Code): the filter, the
extractor wrapper, the condition
and the tests. I reviewed every line and can explain it. It was tested
with the unit tests and
the hardware measurement above. This description was written by me in
Russian and translated
into English with the same assistant.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Adds an option to the change the max bitrate during playback. It
defaults to the global setting and can be changed in the settings (gear
icon) on the playback controls.

It is applied for the entire playback session. So if you start an
episode and change it, will be used for each next up episode. If you
back out of playback and start again, it will reset back to the global
default.

### Related issues
Closes #1382

### Testing
Testing on the emulator

## Screenshots
N/A

## AI or LLM usage
None
## Description
As usual, a PR to fix various smaller bugs in the latest release.

- Fixes not showing the Videos tab on favorites page
- Fixes the Random button not working for photos
- Long nav drawer item names use a marquee scroll if needed
- Fixes a crash when clicking "View More" on a studios rows on the home
page

### Related issues
Fixes #2070
Fixes #2069
Fixes #2135

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
## Description
The video player passes false to setAudioAttributes (since #1258 I
think), as ExoPlayer never requests audio focus. This will pass `true`.

### Related issues
#447, #1258.

### Testing
Not tested on a device.

### Screenshots
Not applicable

### AI or LLM usage
None.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
The Music / Artists view currently appears to use the artist metadata
from individual tracks. While at a track level this makes perfect sense,
it can result in a very noisy and fragmented artists view.

For example, an album by Eminem might contain tracks with artists such
as:
- Eminem
- Eminem feat. 50 Cent
- Eminem ft. Rihanna
- Eminem & Dr. Dre

Each of these seem to appear as a separate artist entry (even though
they are all tracks from the same album by Eminem)

This becomes particularly noticeable in large music libraries, where
featured artists, collaborations, remixes, and slightly different artist
metadata ends up resulting in a large number of sparsely populated or
duplicate / strange looking artist entries.

This PR adds an option / toggle to determine which metadata field is
used to populate the artists view. This option sits neatly within the
current artist view settings e.g. Music > Artists > View options >
Artist source
- Artist (current behavior)
- Album Artist (use the album artist metadata instead)

That Artist setting stays as the default choice (so nobody sees a change
unless they pick it) but now gives the option to filter down only to
Album Artists if desired - this choice is saved per library and applies
immediately.

### Related issues
Closes #1868

### Testing
I've tested it extensively on an Amazon Fire TV Cube Gen 2 (using the
firetv debug variant) the following manual tests were performed:
- The list changes when you pick Album artist and importantly the
selection survives a full app restart
- It was tested on a large library and can confirm only the Album
Artists remained (e.g. only one Eminem entry remained)

Automated checks were run (testFiretvDebugUnitTest & pre-commit) As well
as two new tests being added to the CollectionFolderViewModelTest.kt:
- For letter counting its checking via the album artist endpoint (and
not the standard one) when the source is set as ALBUM_ARTIST
- When the source is changed via the saveViewOptions its triggering a
load from the album artist endpoint

## Screenshots
Please see the screenshots below showing the UI impact / modification:
<img width="4624" height="3472" alt="1"
src="https://github.com/user-attachments/assets/6b53575e-ad15-4f66-b9c0-074657128a1d"
/>
<img width="4624" height="3472" alt="2"
src="https://github.com/user-attachments/assets/528c5766-44d8-43d3-8519-778f5aab7441"
/>
<img width="4624" height="3472" alt="3"
src="https://github.com/user-attachments/assets/5de2fb6d-173d-4ac3-ad1b-e08f27f4dafd"
/>

## AI or LLM usage
The code was written / supported with Claude - the approach (e.g. design
/ storing the choice in ViewOptions) endpoint research etc was done by
myself - I reviewed the approaches / refactoring and than deployed a
debug.apk to my local device to confirm everything was working as
expected.

I've reviewed each line and can explain it / the thinking to approaches
/ logic etc and am comfortable submitting the PR.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
- From media3 1.11, controllers that aren't trusted get read-only player
commands by default.
- Google Home sends its commands through
com.google.android.apps.mediashell (Google's Cast service on the TV),
which isn't trusted, so play/pause from it was dropped.
- Logcat showed: MediaSessionLegacyStub: Calling play() omitted due to
COMMAND_PLAY_PAUSE not being available
- The change: a MediaSession.Callback (TrustAllControllersCallback in
PlayerFactory.kt) that gives every controller all player commands again.
It uses onConnectAsync and AcceptedResultBuilder(session, controller),
so untrusted controllers keep the restricted session commands.
- It's applied to the video session (createMediaSession) and the music
session (MusicService).
- It probably started with the media3 1.11 bump in #1814.

### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->
- Fixes #2087 

### Testing
- JMGO N1S 4K, Google TV, Android 14, ExoPlayer.
- Before the fix: play/pause from Google Home did nothing, and logcat
showed the "omitted" warning.
- After the fix: pause and play from Google Home work. Logcat shows each
command reaching handleSetPlayWhenReady, and the warning is gone.
- The TV remote still works.
- I didn't test music playback; leave that out or test it first.

## Screenshots
- None; no UI changes.

## AI or LLM usage
- Claude Code read the logcat output, found the media3 1.11
untrusted-controller change, and wrote TrustAllControllersCallback plus
the two setCallback(...) calls.
Updates dependencies to max versions still supporting API level 23

#2152 will allow for further updates
Currently translated at 99.8% (669 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/et/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/zh_Hant/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/it/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/tr/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/de/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/et/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/sk/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/fr/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/pl/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/id/
Currently translated at 20.4% (137 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/sr/
Currently translated at 72.9% (489 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/hu/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/es/
Currently translated at 100.0% (670 of 670 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/he/
Currently translated at 91.0% (629 of 691 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/sv/
Currently translated at 100.0% (691 of 691 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/et/
Currently translated at 100.0% (691 of 691 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/fr/
Currently translated at 99.4% (687 of 691 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/zh_Hant/
Currently translated at 100.0% (691 of 691 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/pl/
Currently translated at 82.9% (573 of 691 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/hu/
Currently translated at 100.0% (693 of 693 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/et/
Currently translated at 100.0% (693 of 693 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/ru/
Currently translated at 99.5% (690 of 693 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/de/
Currently translated at 100.0% (693 of 693 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/it/
Currently translated at 100.0% (693 of 693 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/tr/
Currently translated at 100.0% (695 of 695 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/et/
Currently translated at 99.1% (689 of 695 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/zh_Hant/
Currently translated at 100.0% (696 of 696 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/de/
Currently translated at 99.8% (695 of 696 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/pt/
Currently translated at 100.0% (696 of 696 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/tr/
Currently translated at 100.0% (696 of 696 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/et/
Currently translated at 100.0% (696 of 696 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/fr/
Currently translated at 100.0% (696 of 696 strings)

Translation: Wholphin/Wholphin
Translate-URL: https://translate.codeberg.org/projects/wholphin/wholphin/pl/
## Description

I was having trouble playing HDHomeRun channels through Jellyfin in
Wholphin. They started quickly in the official Jellyfin app, but
Wholphin would sometimes spin or fall back when switching channels.

This change includes the live stream ID returned by Jellyfin when
building the Direct Play URL. It also adds a regression test.

### Related issues

Related to #2014 and possibly #1903. My testing was with an HDHomeRun; I
haven’t verified the TVHeadend setups in those reports.

### Testing

Tested on a Google TV Streamer with multiple local channels, FAST
channels, and movie playback. The tested content used Direct Play, and
the HDHomeRun tuners were released after stopping.

All 18 playback tests and the formatting check passed after the
requested test cleanup.

## Screenshots

No UI changes.

## AI or LLM usage

I used OpenAI Codex to help investigate the issue, implement the change
in PlaybackViewModel.kt, add the regression test in
PlaybackViewModelTests.kt, and run validation. Codex also helped draft
this description.
## Description
- Fixes typo that could prevent restoring a page with artists or album
artists
- Clarified some javadocs

### Related issues
N/A

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
Update translations as of 2026-11-03

A big thank you to all of the contributors!
v1.1.0
All checks were successful
review/code-claude Claude cross-family review: approve, no blocking findings
670e95f2fa
-----BEGIN SSH SIGNATURE-----
 U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgrqffh93Kw9xITbVISXK/jkih9K
 Q1NSQeC6TBqiLCg2oAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
 AAAAQOwlHRfQqaoMZESsY18BqmXMIMwtJzU8cAEB2FWHNv5x2j/ZsGi8ES7VNW+rYtgV/B
 KF9ctdqeDoocL8r8da+AY=
 -----END SSH SIGNATURE-----

Merge upstream v1.1.0 while preserving Wolfskin customizations
Author
Owner

Cross-family code review (Claude Sonnet): APPROVE, no blocking findings. Reviewed head 670e95f2fa6f7d94c70d36cfbfb2e9d713697c92 (parents Wolfskin main 6bb382a1 + upstream v1.1.0 2bb662fd). This is a comment and commit status only; I did not approve-merge or edit the PR.

Checks:

  1. Conflicts/auto-merges. I redid the merge with git merge-tree --write-tree 6bb382a1 v1.1.0: conflicts are exactly the two named files, and HEAD differs from Git's own merge only in those two resolutions plus the new docker/README.md. So the other 30 overlapping files are the plain 3-way result, with the Wolfskin delta kept and the upstream delta taken. DiscoverSeriesDetails.kt (upstream availability: SeerrAvailability + Wolfskin rating: SourceRatings?) is consistent with its ViewModel and callers. AddSeerrServer.kt uses seerrUrlPrefill(initialUrl) in both flows (plugin URL wins when nonblank, else the Wolfskin default). Sampled rating, theme, NavDrawer, preferences, MainActivity, proto and Seerr discover: Wolfskin deltas intact.
  2. Preservation. I reproduced every number: 116 baseline paths, all 116 still differ from v1.1.0, 83 byte-identical to main, 33 changed (32 overlaps + docker/README.md), stats 3271/720 -> 3293/721 and 1076/281. Fork-added source, updater, theme, drawables, tests, docker scripts and Makefile are blob-equal to main.
  3. Room. Schemas, AppDatabase, DatabaseModule, all DAOs/entities, and TestDbMigrations are identical to v1.1.0. Wolfskin has no Room entity, DAO or schema delta (v1.0.9..main empty). Migration is 35->40->41, one-way.
  4. No release artifacts. The only app/build.gradle.kts change is implementation(libs.exoplayerhdrutils). applicationId is still org.justinvisser.wolfskin.tv. No version override, tag, signing change or .forgejo; .github equals upstream and stays dormant. LICENSE intact.
  5. Evidence. The recorded test-summary.json (66 suites / 550 tests / 0 failures), make apk log, and the 35->40->41 migration smoke match the PR body. The stale 1.0.9-28-g6bb382a1 APK version is disclosed. Gradle not rerun. Minor: make-test.log is a truncated capture; the test result rests on the JSON summary.
  6. Flagged risks. Verified in code: plugin Seerr login runs before the saved per-profile login (UserSwitchListener.kt); the Jellyfin 12 minimum is ServerVersion(12,0,0), and v1.1.0 release notes say it is the last 10.10/10.11 release; the AppUpgradeHandler gap (current, not previous, compared to 1.0.8-0-g0) is real. None needs a code change now.

Non-blocking: the upstream gradlew.bat CRLF makes git diff --check nonzero (correctly untouched). The TV device walk stays outstanding and gates any future install, not this merge. Consider a Wolfskin issue for the session-restoration gap before the TV install.

Full report: /root/.treehouse/firstmate-7bab20/2/firstmate/data/review-wolfskin-27/report.md

**Cross-family code review (Claude Sonnet): APPROVE, no blocking findings.** Reviewed head `670e95f2fa6f7d94c70d36cfbfb2e9d713697c92` (parents Wolfskin main `6bb382a1` + upstream v1.1.0 `2bb662fd`). This is a comment and commit status only; I did not approve-merge or edit the PR. Checks: 1. **Conflicts/auto-merges.** I redid the merge with `git merge-tree --write-tree 6bb382a1 v1.1.0`: conflicts are exactly the two named files, and HEAD differs from Git's own merge only in those two resolutions plus the new `docker/README.md`. So the other 30 overlapping files are the plain 3-way result, with the Wolfskin delta kept and the upstream delta taken. `DiscoverSeriesDetails.kt` (upstream `availability: SeerrAvailability` + Wolfskin `rating: SourceRatings?`) is consistent with its ViewModel and callers. `AddSeerrServer.kt` uses `seerrUrlPrefill(initialUrl)` in both flows (plugin URL wins when nonblank, else the Wolfskin default). Sampled rating, theme, NavDrawer, preferences, MainActivity, proto and Seerr discover: Wolfskin deltas intact. 2. **Preservation.** I reproduced every number: 116 baseline paths, all 116 still differ from v1.1.0, 83 byte-identical to main, 33 changed (32 overlaps + docker/README.md), stats 3271/720 -> 3293/721 and 1076/281. Fork-added source, updater, theme, drawables, tests, docker scripts and Makefile are blob-equal to main. 3. **Room.** Schemas, `AppDatabase`, `DatabaseModule`, all DAOs/entities, and `TestDbMigrations` are identical to v1.1.0. Wolfskin has no Room entity, DAO or schema delta (v1.0.9..main empty). Migration is 35->40->41, one-way. 4. **No release artifacts.** The only `app/build.gradle.kts` change is `implementation(libs.exoplayerhdrutils)`. `applicationId` is still `org.justinvisser.wolfskin.tv`. No version override, tag, signing change or `.forgejo`; `.github` equals upstream and stays dormant. LICENSE intact. 5. **Evidence.** The recorded `test-summary.json` (66 suites / 550 tests / 0 failures), `make apk` log, and the 35->40->41 migration smoke match the PR body. The stale `1.0.9-28-g6bb382a1` APK version is disclosed. Gradle not rerun. Minor: `make-test.log` is a truncated capture; the test result rests on the JSON summary. 6. **Flagged risks.** Verified in code: plugin Seerr login runs before the saved per-profile login (`UserSwitchListener.kt`); the Jellyfin 12 minimum is `ServerVersion(12,0,0)`, and v1.1.0 release notes say it is the last 10.10/10.11 release; the `AppUpgradeHandler` gap (`current`, not `previous`, compared to `1.0.8-0-g0`) is real. None needs a code change now. Non-blocking: the upstream `gradlew.bat` CRLF makes `git diff --check` nonzero (correctly untouched). The TV device walk stays outstanding and gates any future install, not this merge. Consider a Wolfskin issue for the session-restoration gap before the TV install. Full report: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/review-wolfskin-27/report.md`
justinzeus merged commit de452448ba into main 2026-11-05 01:49:35 +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#27
No description provided.