Add the pinned Conductarr TV client and disk-backed saved data #28

Merged
justinzeus merged 1 commit from fm/tv-conductarr-client into main 2026-11-05 13:18:47 +01:00
Owner

Outcome

Add the UI-free :conductarr-client foundation for the locked TV design. The module supplies typed calls for all 20 TV-used operations, 68 maintained kotlinx.serialization models, identity-scoped disk last-known-good (LKG) storage, and local contract/network verification.

Refs: approved TV spec, sections 3 and 8. Implements the pinned-client architecture decision and the Jellyfin token header contract (conductarr#914); does not activate production sign-in.

Pin and generator-versus-fallback decision

Vendor the unmodified OpenAPI 3.1.0 document from Conductarr commit 481492d3a8a0651a89901de548cd511218c0562e, read with git show <sha>:openapi.json from the Conductarr clone. The sibling openapi/commit.sha records the pin. No schema or endpoint was fetched from a running service.

Use the approved maintained-data-class/schema-check fallback. The standalone OpenAPI Generator 7.26.0 kotlin probe used kotlinx.serialization, coroutines and immutable models. Generation completed and retained nullable arrays, but emitted ErrorDetail.value as @Contextual Any? with no Any adapter. Compiling the actual emitted model and decoding an object-valued error reproduced Serializer for class 'Any' is not found. This is a real wire construct in the pinned schema, so generation does not handle the complete contract. The maintained fallback uses JsonElement?, with regression coverage for arbitrary JSON values.

Schema checks inspect the actual serialization descriptors against the pinned schemas (fields, referenced types, primitive widths, lists/maps, enums and requiredness). A separately reviewed schema snapshot also gates changes to formats, bounds, defaults and other constraints. Read-only $schema metadata is deliberately ignored. The route test independently enumerates every TV method/path pair and checks request/response schemas.

Client behavior

  • Caller-supplied Jellyfin credential sent as Authorization: MediaBrowser Token="<token>"; HTTPS except loopback, bounded connect/read/call timeouts, no redirect or mutation replay.
  • 401 maps to NeedsSignIn, invalidates that identity's saved cache and suppresses stale fallback. Other failures are typed and retain LKG.
  • Cold Flow paints the saved copy before networking; ETag/If-None-Match handles 304 without replacing the body or its saved timestamp.
  • App-private file-backed cache uses synced temporary files and atomic replacement. In-memory fake has matching capacity semantics. Server/token/query partitioning prevents cross-person reuse; diagnostics expose enum events, never credentials or response bodies.
  • Per-endpoint policies cover launch, foreground, daily refresh, feed expires_at, 30-minute discovery, six-hour/300-per-media ratings including cached 404 absence, per-season copies, caller-driven 30-second activity polling, and uncached request receipts.

Existing-file edits

Exactly two existing files changed:

  1. settings.gradle.kts: include :conductarr-client.
  2. app/build.gradle.kts: one implementation(project(":conductarr-client")) line to prove app integration.

No design-system, shell, MainContent.kt, navigation or UI source changed. New module documentation explains the fallback, public API, identity/storage constraints and refresh events.

Verification

Rebased onto current origin/main before the final gates. Both gates ran once, through docker/gradle.sh serialization:

  • make test: PASS, 6m40s; 550 app tests + 17 module tests, zero failures/errors/skips.
  • Runtime executable within that gate: PASS — live profile Justin -> disk reopen -> ETag 304 -> HTTP 503 retains Justin; requests=3 using a real loopback MockWebServer socket.
  • make apk-debug: PASS, 3m30s; full app debug assembly succeeds with the module dependency.

Additional local preflight: compile the generator failure reproduction and the fallback with the pinned Kotlin compiler; 17 standalone JUnit tests pass. Tests include 401, 304 reuse across disk reopen, network/HTTP/invalid-JSON LKG recovery, feed expiry, identity isolation, request receipt decoding/no replay, capacity boundaries, corrupt-cache recovery and credential-safe diagnostics. No live Conductarr/Jellyfin calls.

Existing build warnings include optional native decoders absent (MPV stub selected), configuration-time classpath resolution and Gradle deprecations. No check failed. The Kotlin language server exited during reference lookup; scoped text reference inspection was used instead.

Measured size

Artifact Bytes
Client JVM jar 847,838
Universal debug APK 56,679,709
arm64-v8a debug APK 43,782,738
armeabi-v7a debug APK 42,302,015
x86_64 debug APK 43,815,560

These are absolute artifact sizes, not a claimed delta. The vendored spec is 474,825 bytes and is a test/build contract, not a runtime resource. Local proof/logs are under firstmate's data/tv-conductarr-client/ task directory; no scratch scaffolding is committed.

Deliberately not done

No UI or shell integration, sign-in flow, production activation, live service calls, ratings-only API migration, backend API changes, ADB/device/install work, signed release, version bump, tag, release asset or merge. Screen code still needs to supply the selected person's token, private backup-excluded cache directory and lifecycle refresh events. Firstmate arranges cross-family review and owns the merge decision.

## Outcome Add the UI-free `:conductarr-client` foundation for the locked TV design. The module supplies typed calls for all 20 TV-used operations, 68 maintained kotlinx.serialization models, identity-scoped disk last-known-good (LKG) storage, and local contract/network verification. Refs: [approved TV spec, sections 3 and 8](https://pages.justinvisser.org/wolfskin/tv-spec). Implements the pinned-client architecture decision and the Jellyfin token header contract (conductarr#914); does not activate production sign-in. ## Pin and generator-versus-fallback decision Vendor the **unmodified** OpenAPI 3.1.0 document from Conductarr commit **`481492d3a8a0651a89901de548cd511218c0562e`**, read with `git show <sha>:openapi.json` from the Conductarr clone. The sibling `openapi/commit.sha` records the pin. No schema or endpoint was fetched from a running service. Use the approved maintained-data-class/schema-check fallback. The standalone OpenAPI Generator **7.26.0** `kotlin` probe used kotlinx.serialization, coroutines and immutable models. Generation completed and retained nullable arrays, but emitted `ErrorDetail.value` as `@Contextual Any?` with **no Any adapter**. Compiling the actual emitted model and decoding an object-valued error reproduced `Serializer for class 'Any' is not found.` This is a real wire construct in the pinned schema, so generation does not handle the complete contract. The maintained fallback uses `JsonElement?`, with regression coverage for arbitrary JSON values. Schema checks inspect the actual serialization descriptors against the pinned schemas (fields, referenced types, primitive widths, lists/maps, enums and requiredness). A separately reviewed schema snapshot also gates changes to formats, bounds, defaults and other constraints. Read-only `$schema` metadata is deliberately ignored. The route test independently enumerates every TV method/path pair and checks request/response schemas. ## Client behavior - Caller-supplied Jellyfin credential sent as `Authorization: MediaBrowser Token="<token>"`; HTTPS except loopback, bounded connect/read/call timeouts, no redirect or mutation replay. - 401 maps to `NeedsSignIn`, invalidates that identity's saved cache and suppresses stale fallback. Other failures are typed and retain LKG. - Cold Flow paints the saved copy before networking; ETag/If-None-Match handles 304 without replacing the body or its saved timestamp. - App-private file-backed cache uses synced temporary files and atomic replacement. In-memory fake has matching capacity semantics. Server/token/query partitioning prevents cross-person reuse; diagnostics expose enum events, never credentials or response bodies. - Per-endpoint policies cover launch, foreground, daily refresh, feed `expires_at`, 30-minute discovery, six-hour/300-per-media ratings including cached 404 absence, per-season copies, caller-driven 30-second activity polling, and uncached request receipts. ## Existing-file edits Exactly two existing files changed: 1. `settings.gradle.kts`: include `:conductarr-client`. 2. `app/build.gradle.kts`: one `implementation(project(":conductarr-client"))` line to prove app integration. No design-system, shell, `MainContent.kt`, navigation or UI source changed. New module documentation explains the fallback, public API, identity/storage constraints and refresh events. ## Verification Rebased onto current `origin/main` before the final gates. Both gates ran **once**, through `docker/gradle.sh` serialization: - `make test`: **PASS**, 6m40s; **550 app tests + 17 module tests**, zero failures/errors/skips. - Runtime executable within that gate: **PASS** — `live profile Justin -> disk reopen -> ETag 304 -> HTTP 503 retains Justin; requests=3` using a real loopback MockWebServer socket. - `make apk-debug`: **PASS**, 3m30s; full app debug assembly succeeds with the module dependency. Additional local preflight: compile the generator failure reproduction and the fallback with the pinned Kotlin compiler; 17 standalone JUnit tests pass. Tests include 401, 304 reuse across disk reopen, network/HTTP/invalid-JSON LKG recovery, feed expiry, identity isolation, request receipt decoding/no replay, capacity boundaries, corrupt-cache recovery and credential-safe diagnostics. No live Conductarr/Jellyfin calls. Existing build warnings include optional native decoders absent (MPV stub selected), configuration-time classpath resolution and Gradle deprecations. No check failed. The Kotlin language server exited during reference lookup; scoped text reference inspection was used instead. ### Measured size | Artifact | Bytes | |---|---:| | Client JVM jar | 847,838 | | Universal debug APK | 56,679,709 | | arm64-v8a debug APK | 43,782,738 | | armeabi-v7a debug APK | 42,302,015 | | x86_64 debug APK | 43,815,560 | These are absolute artifact sizes, not a claimed delta. The vendored spec is 474,825 bytes and is a test/build contract, not a runtime resource. Local proof/logs are under firstmate's `data/tv-conductarr-client/` task directory; no scratch scaffolding is committed. ## Deliberately not done No UI or shell integration, sign-in flow, production activation, live service calls, ratings-only API migration, backend API changes, ADB/device/install work, signed release, version bump, tag, release asset or merge. Screen code still needs to supply the selected person's token, private backup-excluded cache directory and lifecycle refresh events. Firstmate arranges cross-family review and owns the merge decision.
justinzeus added 1 commit 2026-11-05 13:08:32 +01:00
Add the pinned Conductarr TV client and disk cache
All checks were successful
review/code-claude Claude cross-family review: clean, no blocking findings
adbfe5d0e7
Author
Owner

Cross-family review (Claude Sonnet): CLEAN, no blocking findings. Reviewed head adbfe5d0e7184b56a352cba9669efbdf97ccfe35.

Checks:

  • Generator vs fallback: the ErrorDetail.value claim holds. The pinned schema has no type there, and the lane's recorded generator output is @Contextual Any? with a recorded Serializer for class 'Any' is not found failure. It is the only Any in 201 generated models, and sits in an error type the client never decodes. A type-mapping workaround was not tried, but spec 3.4 allows the fallback.
  • Drift checks fail: I ran ONE module-only Gradle run (docker/gradle.sh debug :conductarr-client:test) on a scratch copy with three mutations: a route path renamed in the pinned spec, a model field renamed, and a schema maxLength changed. Exactly those three tests failed (route check, field check, constraint snapshot) and the other 14 passed.
  • Pin: git show 481492d3a8a0651a89901de548cd511218c0562e:openapi.json is byte-identical to the vendored file (sha256 f2c00be3...bb33, 474,825 bytes). That commit is the tip of Conductarr main.
  • Routes: the 20 routes equal the Conductarr rows of spec 3.6. The typed surface is exactly the 66-schema closure of those routes plus ErrorModel/ErrorDetail. Missing: the spec section 8 "Display art" route GET /catalogue/titles/{tmdb}/{type}/display (medium follow-up). Extra: none.
  • Facade: the header is exactly Authorization: MediaBrowser Token="<token>" and matches the Conductarr server. The token appears only in the header and in the digest that names the cache scope: never in URLs, keys, file names or contents, logs or errors. 401 maps to NeedsSignIn with no stale fallback and clears that scope. Other failures are typed and keep the LKG. Cache paths are digest(base+token)-scoped, so no cross-person hit. Writes are fsynced temp plus atomic rename, corrupt records become misses, and error bodies never write.
  • Isolation: two existing files changed (settings.gradle.kts +1, app/build.gradle.kts +1). No UI, shell, navigation or design files, no version, signing or release change. SPDX GPL headers are on every new file. The only new resolved artifact is test-only mockwebserver3.
  • Tests: 17 module tests use real MockWebServer sockets and a reopened disk cache. Nothing existing was removed. I did not rerun the full suite.
  • Non-blocking:
    • M1: the display route is not wrapped.
    • L2: on pinned Conductarr main the Jellyfin sign-in allows only get-cockpit-operator, so the other 19 routes return 403 until the backend widens its allowlist.
    • L3: the client accepts uppercase hex tokens but the server accepts lowercase only.
    • L4: a re-login orphans the old saved data, and the groups without a capacity limit (title, cast, related, discover, filmography, readiness, dismissals) grow without bound.
    • Also strict enums, no timeout test and minor nits.

Full report: firstmate data/review-wolfskin-28/report.md. Recommendation: merge.

**Cross-family review (Claude Sonnet): CLEAN, no blocking findings.** Reviewed head `adbfe5d0e7184b56a352cba9669efbdf97ccfe35`. Checks: - **Generator vs fallback:** the `ErrorDetail.value` claim holds. The pinned schema has no `type` there, and the lane's recorded generator output is `@Contextual Any?` with a recorded `Serializer for class 'Any' is not found` failure. It is the only `Any` in 201 generated models, and sits in an error type the client never decodes. A type-mapping workaround was not tried, but spec 3.4 allows the fallback. - **Drift checks fail:** I ran ONE module-only Gradle run (`docker/gradle.sh debug :conductarr-client:test`) on a scratch copy with three mutations: a route path renamed in the pinned spec, a model field renamed, and a schema `maxLength` changed. Exactly those three tests failed (route check, field check, constraint snapshot) and the other 14 passed. - **Pin:** `git show 481492d3a8a0651a89901de548cd511218c0562e:openapi.json` is byte-identical to the vendored file (sha256 `f2c00be3...bb33`, 474,825 bytes). That commit is the tip of Conductarr `main`. - **Routes:** the 20 routes equal the Conductarr rows of spec 3.6. The typed surface is exactly the 66-schema closure of those routes plus `ErrorModel`/`ErrorDetail`. Missing: the spec section 8 "Display art" route `GET /catalogue/titles/{tmdb}/{type}/display` (medium follow-up). Extra: none. - **Facade:** the header is exactly `Authorization: MediaBrowser Token="<token>"` and matches the Conductarr server. The token appears only in the header and in the digest that names the cache scope: never in URLs, keys, file names or contents, logs or errors. 401 maps to NeedsSignIn with no stale fallback and clears that scope. Other failures are typed and keep the LKG. Cache paths are `digest(base+token)`-scoped, so no cross-person hit. Writes are fsynced temp plus atomic rename, corrupt records become misses, and error bodies never write. - **Isolation:** two existing files changed (`settings.gradle.kts` +1, `app/build.gradle.kts` +1). No UI, shell, navigation or design files, no version, signing or release change. SPDX GPL headers are on every new file. The only new resolved artifact is test-only `mockwebserver3`. - **Tests:** 17 module tests use real MockWebServer sockets and a reopened disk cache. Nothing existing was removed. I did not rerun the full suite. - **Non-blocking:** - M1: the display route is not wrapped. - L2: on pinned Conductarr main the Jellyfin sign-in allows only `get-cockpit-operator`, so the other 19 routes return 403 until the backend widens its allowlist. - L3: the client accepts uppercase hex tokens but the server accepts lowercase only. - L4: a re-login orphans the old saved data, and the groups without a capacity limit (title, cast, related, discover, filmography, readiness, dismissals) grow without bound. - Also strict enums, no timeout test and minor nits. Full report: firstmate `data/review-wolfskin-28/report.md`. Recommendation: merge.
justinzeus merged commit 2be4e30826 into main 2026-11-05 13:18:47 +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#28
No description provided.