| .. | ||
| Dockerfile | ||
| gradle.sh | ||
| README.md | ||
| release-signing.properties | ||
| release-tag.sh | ||
| signing.init.gradle | ||
Wolfskin TV container builds
Run from a full Git checkout with the upstream v* and p* tags available. The
host needs Docker, GNU Make, Bash, Git, and flock (util-linux), not Java or the
Android SDK.
git fetch origin --tags
make apk # clean, signed default release: three ABI splits + universal
make test # app default/debug suite plus wolfskin-design JVM/capture suite
make apk-debug # default debug, no secrets; MPV stub and no extra codec decoders
APKs remain under app/build/outputs/apk/default/{release,debug}/. Debug builds
are not playback-complete and must not be distributed as releases. Upstream's
.github/workflows and dependency version catalog are unchanged; release builds
use the catalog's exact Wholphin extension version (currently 0.2.4). Do not
place local extension AARs in app/libs: upstream prefers them over the pinned
GitHub Packages dependencies.
Upload release assets renamed to Wolfskin.apk / Wolfskin-<abi>.apk for the
in-app updater. Gradle outputs
Wholphin-default-release-<versionName>-<versionCode>[-<abi>].apk; strip the
flavor, version, and code as well as replacing the prefix.
Upstream enables JVM unit tests only for debug variants; there is no
testDefaultReleaseUnitTest task. make test uses the closest default-flavor
task without changing upstream's test configuration.
Upstream v1.1.0 integration
The release merge brings Kotlin 2.4.21, Gradle 9.8.2, AGP 9.5.0, and
ExoplayerHdrUtils 0.4.1. The pinned SDK/JDK image and serialized Gradle wrapper
remain compatible without changes; native Wholphin extensions remain 0.2.4.
There are no committed dependency lockfiles or Gradle verification metadata.
Forgejo Actions stays disabled; upstream GitHub workflows remain dormant.
The app database upgrades from Room schema 35 through 40 to 41 on first launch after installation. The upstream migrations add series track choices and a home settings source preference. Downgrading with the upgraded database is not supported: a TV installation is a one-way step and requires explicit approval. The optional companion plugin can supply Discover credentials before locally saved Seerr profiles; do not configure an administrator API key in place of the household's per-profile Jellyfin login, which Conductarr uses for request routing.
Build only after committing a release merge: version names come from Git HEAD, not the working tree. An uncommitted merge still builds with the old version name. Neither an upstream merge nor its validation build authorizes a release or TV installation.
Wolfskin shell
Wolfskin UI lives in app/src/wolfskin/java, beside upstream source. The signed-in
entry in MainContent.kt calls WolfskinApp; Wolfskin-only routes use the single
Destination.Wolfskin subtype in the existing navigation stack. Other destinations
continue through upstream DestinationContent. Full-screen player/settings pages
cover the Wolfskin nav.
The independent :wolfskin-design module owns the shell components, fixed person
colours, focus policy, per-person root-view memory, palette and motion. Its
tokens.json is the copied approved TV-spec source, retaining the locked prototype
SHA and provenance. shell-tokens.json and discover-tokens.json add measurements
from that same prototype. Generate the committed Kotlin, CSS and checksum manifest together:
python3 wolfskin-design/generate_tokens.py
python3 wolfskin-design/generate_tokens.py --check
preBuild checks the manifest against the source JSON, generator and generated
outputs. Editing any of them without regenerating fails the build. The shell uses
the generated palette rather than the retired WOLFSKIN theme-enum value; the old
numeric value is reserved.
Compose output maps source weights 600/650 to the platform Bold (700) face, which resolves consistently in native captures; the approved JSON weights stay unchanged. Nav, picker names, labels and pill weights use that same mapping. Nav label colour fades for 200 ms ease-out; the blob retains its critically damped spring.
Settings pills have a separate 1 dp stroke (2 px on the stage), not a focus ring. The stroke is inset within the fixed 20 dp / 40 px height; horizontal border-box padding includes it. The 6 px dash / 4 px gap follows the browser's 3:2 stroke-width ratios. Resting borders are white at 22% opacity; focused borders use ink at 40%.
The small gear under the final person opens Settings with Requests focused. Settings groups hand off to existing upstream pages; Requests opens the household Coming-in sheet described below. Advanced settings → Wolfskin screens is the persistent rollout switch: turning it off renders upstream Home and library destinations with the upstream Backdrop and top scrim, while retaining Wolfskin's nav, picker and Settings list. Native Up traversal stays in content until the first focusable row, then enters the nav. Turning the switch on restores the labelled placeholders until the corresponding screen slices ship.
Debug people, tabs and initials come from the shell-only projection of the locked
prototype fixture in app/src/debug/assets/wolfskin-shell.json; release resources
do not package it. Saved Jellyfin identities use existing upstream services. The
shell makes no Conductarr calls and does not add Conductarr sign-in.
Discover adds the picks hero, type chips, catalogue shelves, service pages, continuous
See all grids, Hidden picks, request chooser and receipts. Its discover-title/...
destination is an explicit title-family integration stub exposing only Discover's
request and pick actions. Unheld titles have no trailer control until the pinned
Conductarr title contract supplies a trailer.
The debug-only fixture and local art live in
app/src/debug/assets/wolfskin-discover/. Release builds remain on the explicit
Conductarr sign-in state. The fixture host cannot construct a network client:
debug request outcomes and dismissals stay in memory. The real mutation adapter
accepts an injected ConductarrClient and uses only its typed routes; it is not
wired to a live server. Unknown request outcomes force a fresh title-requests read
instead of blindly submitting again, including after closing a sheet or choosing
the same uncertain recipient from another chooser. Whole-series requests require every season.
Discover's native capture suite covers its 28 named acceptance states and emits text-layout metrics beside the PNGs. Data-driven Android key replays cover opening Discover in 4 presses, requesting for yourself in 7, requesting for Mindel in 9 and Not interested in 8. Additional regressions exercise held-OK safety, uncertain receipts, per-recipient outcomes, collection recovery and Undo, unknown seasons, Dutch decimal formatting and short-row/tail clamping. Discover reuses the Libraries grid, poster, ratings plate, See all and recovery cards, bounded image loader, caption separator token and the shell's single hold guard. Both frames share the 180 ms scroll tween.
The TV keeps platform Roboto with the approved type sizes and weights. Browser system-font wrapping is not a per-title contract: no bundled font, forced title breaks or width compensation is applied. Discover's own stacked text preserves untrimmed CSS line boxes without changing merged shell/library typography. Hero, sheet and shared focused-content shadows use tokenized drop shadows, not Material elevation; their alpha animates in the draw phase. Native text-layout measurements accompany each acceptance capture; remaining font-driven wrapping and text-block positions must be listed explicitly in its comparison report.
The asynchronous debug fixture seam hands loading focus back to the first loaded action when Discover is still the selected destination. A JVM regression releases the actual host's suspended fixture load and presses Android OK: it must open the hero rather than activate the nav. Fixture completion does not steal a different destination or nav selection.
Native Roborazzi captures use Robolectric SDK 34 and
w960dp-h540dp-xhdpi (1920×1080 at density 2.0). make test writes capture PNGs and
the sampled Compose nav-spring curve to build/wolfskin-captures/. Key replays
exercise the Android event adapter, including the 6/8/10/11-press shell paths.
The profile hand-off capture targets the real HomeSettingsPage; Playback is a
separate press-count path.
Robolectric SDK 28/30/34 runtimes resolve through Gradle's existing persistent
dependency cache, then copy to wolfskin-design/build/robolectric-sdk/. Test workers
use that local directory in offline mode instead of downloading Android JARs into
the container's ephemeral home. A cache miss still needs Maven Central; rate-limited
hosts can seed the existing cache from SHA512-verified local Maven artifacts without
changing the wrapper or adding volumes.
Default-flavour launcher artwork and English, Hungarian and Polish string overrides
live in app/src/default/res/. Legacy Wolfskin strings in 25 translated
main resource folders have not yet been moved; non-default appstore/firetv
flavours therefore combine upstream icons with some legacy Wolfskin text. Those
flavours and a full locale migration are not certified by this slice. Debug
assembly runs checkWolfskinBranding against every packaged APK through AGP's public
artifact provider and AAPT2. It checks all string, plural and array configurations,
rejecting user-visible upstream branding while preserving the upstream legal
attribution and project URL. The check does not depend on incremental merge files.
Requests under Settings
The Requests row and Coming-in sheet share one immutable household snapshot. Ready requests do not count as open. Rows sort Failed, Downloading/Almost ready, Checking/Searching, Waiting, Ready. The bounded window shows five rows, or four with readiness expanded. Right expands Request/Download/Library/Playback; Left collapses; Up/Down collapse while moving. Opening a title preserves the request for Back, and closing the sheet restores its opener. Selecting a root or changing person clears the title-return link. Shared's authenticated actor always reads “by Shared”, never “by you”.
The debug-only provider loads app/src/debug/assets/wolfskin-requests.json and
its eight local posters, projected from the locked prototype fixture. Six stage
rows feed the running debug shell; the Shared and focused-rating records feed
native acceptance states. The identical projection is in design-test resources.
No “sample” or “example stages” pill is drawn. Release builds package none of
these assets and show Requests unavailable until Conductarr sign-in supplies
the generated-client data path. There is no Seerr fallback or handwritten HTTP.
Foreground refreshes once; an open sheet or unsettled requests refresh every
30 seconds while started. The immutable debug projection is loaded once, not
decoded again on every poll.
Thumbnails reuse LibraryImages with a 96×144-pixel request, RGB565, no crossfade,
the shared 64 MB memory/128 MB disk cache and four-read limit. Actor separator
dots use the surrounding text colour; the 40% rule applies only to poster captions.
Uncached focused ratings wait 350 ms; moving away cancels the read. Read ratings show immediately on return; sign-out clears visibility. This follows the frozen page and spec 6.12, resolving the contradictory no-dwell sentence in spec 7.8. Deterministic coroutine-clock tests cover cancellation, reentry, cached return, sign-out and refreshed locale scores; a thin Compose test verifies actual Dutch rendering and sign-out suppression. The approved nine-key Requests path is JSON data replayed through the real shell. Home's hero content is not implemented here: its launch-to-nav shell suffix is the acceptance seam.
Sheet dimensions, padding, glyphs, scales, colours and durations are generated tokens. Scoped sheet line boxes retain CSS heights and baselines independently of native leading rounding; shared shell/library typography is unchanged. Platform Roboto remains required: the host browser used DejaVu Sans, so its width, wrapping, ellipsis and text-block positions are not a wrapping-parity target. For example, the 44-pixel “Coming in” ink is 239 pixels wide in the reference and 194 pixels in native captures. No per-title compensation or font bundle is used. Native comparisons must report those structural differences explicitly. Readiness glyphs sit inline on the text baseline with no flex gap, matching the frozen block-span cascade. Items wrap intrinsically: Roboto fits the expanded readiness into two lines where the reference uses three; no grouping is forced.
The six rating marks retain the exact locked SVG source in
wolfskin-design/request-marks/; native-size PNGs and their hashes are generated
from the JSON dimensions with the locked contain fit (.rr .rb outranks .crow img).
Inside Coming-in rows they retain the prototype's dark surface3 chips with 12-pixel rounded corners.
Loaded poster thumbnails have no placeholder fill beneath their clipped edge.
uv run --with cairosvg==2.9.1 python wolfskin-design/generate_request_marks.py
RequestsCaptureTest writes st-081…st-086, st-088 and st-111 at 1920×1080,
density 2.0. Title selection currently uses an explicitly labelled
request-title/... destination until the independent title screen is integrated;
this slice does not claim to implement title pages, TV sign-in, release or device
deployment.
Libraries and See all
Movies, Shows and Anime use the shell's retained view and a continuous six-column lazy grid. Up from its first row reaches Sort/Filter, then the nav; Back preserves the card and scroll. Sort cycles A–Z, Recently added and Rating. The letter rail exists only at 200 titles with A–Z selected. Card OK activates on key-up through the same shell hold guard, without a long-press menu. Down clamps a short final title row before entering the separate Try again / Show more row; an empty slot cannot activate recovery. An unavailable library hides its unknown item count.
Caption separators use 40% of the existing muted colour's alpha. The rail follows the locked prototype CSS: bold 24 px letters, with the focused letter bold 28 px, overriding the spec table's generic caption style for this component. Library and Settings header initials use the muted colour over the person's fill; navigation and picker avatars retain their dark initials.
The debug-only wolfskin-library.json and library/assets/ contain the locked
prototype's titles and local poster art. Release libraries read through
LibraryReadFacade, with 200-item Jellyfin chunks and ten-minute per-server/person
snapshots using the existing atomic disk cache. Saved data is painted before a
quiet refresh; a failed refresh keeps it. Production ratings remain hidden until
Conductarr sign-in exists.
Home/Discover shelves open a full list through
WolfskinSeeAllLists.open(person, key, title, fullList, navigation, loadNextPage).
The optional suspend page provider returns LibraryPage(items, hasMore) using the
generated Conductarr client. The grid reads within ten cards of its end, deduplicates
title identities, and pauses on Try again or Show more rather than looping.
Title activation pushes the explicitly named library-title/{id} destination for
the title-screen implementation; this slice does not implement title pages.
LibraryCaptureTest renders the sixteen approved library/See all states offline.
LibraryKeysTest reads the approved JSON path and reaches Spirited Away in exactly
20 Android key presses. Paging and snapshot tests cover duplicate-only reads,
failure/retry, stale copies, and person/server isolation. Captures are written to
the same build/wolfskin-captures/ directory as the shell captures.
Toolchain and disk use
Dockerfile pins Temurin JDK 21.0.11+10 by image digest, command-line tools
23.0 and platform-tools 37.0.1 by archive and checksum, and build-tools
36.0.0 by SDK package version. The SDK manager registers platform 37.0 and
its licenses; the build rejects any platform revision other than 2. An SDK
revision upgrade therefore requires a deliberate Dockerfile update.
Gradle comes from the committed wrapper. The image is built for linux/amd64;
the default local image tag is wolfskin-build:sdk37-jdk21.
Upstream's build timestamp means APKs are not byte-for-byte reproducible.
Gradle downloads and caches live in the named volume wolfskin-gradle, not
/tmp. Module outputs stay in the checkout. Builds run with the host UID/GID,
--no-daemon, and at most four workers. The repository's Git metadata is mounted
read-only, including when the checkout is a linked worktree.
Override WOLFSKIN_BUILD_IMAGE to change the locally built image tag and
WOLFSKIN_GRADLE_VOLUME to isolate a cache. make build-image builds only the
image. Do not delete shared caches during another build.
Every Gradle container holds a host-side exclusive lock for its entire run.
Concurrent targets sharing a cache queue rather than timing out on Gradle's
cross-container cache journal lock; a waiting invocation prints one line.
The default lock is /var/tmp/${WOLFSKIN_GRADLE_VOLUME}.gradle.lock (using
wolfskin-gradle when unset). Override WOLFSKIN_GRADLE_LOCK_FILE with a path
whose parent directory already exists. All callers sharing a volume must use
the same lock path on the Docker host. Do not delete the file while builds are
running or waiting. The lock releases automatically when the invocation exits.
Release credentials
The default escrow is
/opt/stacks/ventoy/machine-parity/manifest-tank/secrets/wolfskin/. Check
mountpoint -q /mnt/ventoy before using that path. Set WOLFSKIN_SECRETS_DIR
to use another existing directory. Its required files are:
| File | Contents |
|---|---|
release.jks |
Separate Wolfskin PKCS12 keystore, RSA 4096, alias wolfskin |
release.properties |
Java properties storePassword and keyPassword |
github-packages-token |
GitHub token with read:packages, username JustinZeus |
The directory must be mode 0700, each file mode 0600, owned by the building
user. Never commit them or put their contents in logs. Back up the keystore and
passwords together: losing the release key forces a reinstall. Do not reuse
wolfskin-android's key or the gh CLI login token.
Release builds mount escrow read-only. The token is loaded inside the container
into upstream's ORG_GRADLE_PROJECT_WholphinExtensionsUsername and
ORG_GRADLE_PROJECT_WholphinExtensionsPassword environment hooks, never a Docker
command argument. The signing init script registers wolfskin; the nonsecret
release-signing.properties is mounted as local.properties so upstream's
release.signing.config hook selects it. Any host local.properties is hidden
only for the duration of this container. Keystore and passwords stay outside the
checkout and image. Missing credentials fail the release rather than silently
shipping the MPV stub.
Versioning and local release markers
Upstream computes versionName from
git describe --tags --long --match='v*', dropping the leading v, and computes
versionCode from the count of all local v* and p* tags. Preserve and fetch
those tags; a shallow or incomplete checkout can give an incorrect version.
Use the full git describe output as the human release name.
After committing the intended release, with a clean checkout and fetched tags:
make release-tag
make apk
The helper creates one lightweight pwolfskin-YYYY_MM_DD tag in UTC, appending
_1, _2, etc. on collision. Each marker adds one to versionCode without
changing upstream's v*-based version name. It never pushes tags, publishes a
release, or installs an APK. Do not delete published release markers. Tag
creation and publication must be serialized between release operators.
Inspecting release artifacts
Use apksigner verify --verbose --print-certs APK and
aapt2 dump badging APK inside the build image. Compare the signer SHA-256 with
keytool -list -v -alias wolfskin -keystore /path/to/release.jks (enter its
password privately). Inspect APK ZIP entries for real native MPV and ffmpeg
libraries for each shipped ABI; a successful Java compilation alone does not
prove that codec extensions are included.