wolfskin/conductarr-client
justinzeus adbfe5d0e7
All checks were successful
review/code-claude Claude cross-family review: clean, no blocking findings
Add the pinned Conductarr TV client and disk cache
2026-11-05 12:56:06 +01:00
..
openapi Add the pinned Conductarr TV client and disk cache 2026-11-05 12:56:06 +01:00
src Add the pinned Conductarr TV client and disk cache 2026-11-05 12:56:06 +01:00
.gitignore Add the pinned Conductarr TV client and disk cache 2026-11-05 12:56:06 +01:00
build.gradle.kts Add the pinned Conductarr TV client and disk cache 2026-11-05 12:56:06 +01:00
README.md Add the pinned Conductarr TV client and disk cache 2026-11-05 12:56:06 +01:00

Conductarr TV client

A UI-free JVM module for the TV operations in the approved TV spec, sections 3 and 8. The Android app depends on this module; no screen, navigation, login flow or production service is changed.

Pinned contract and generator decision

openapi/openapi.json is the unmodified OpenAPI 3.1.0 document from Conductarr commit 481492d3a8a0651a89901de548cd511218c0562e. openapi/commit.sha records the source. It was read with git show <commit>:openapi.json from the Conductarr clone, not downloaded from a running service.

The approved fallback is used: checked-in, maintained kotlinx.serialization data classes and a thin typed TV API, rather than build-time generation.

The OpenAPI Generator 7.26.0 probe used the kotlin generator with serializationLibrary=kotlinx_serialization, useCoroutines=true, modelMutable=false and enumPropertyNaming=UPPERCASE. Generation completed with its OpenAPI 3.1 beta warning. Nullable arrays were retained. However, the unconstrained JSON schema at ErrorDetail.value became @Contextual Any?. The generated serializer module has no Any adapter. Compiling the generated ErrorDetail and decoding {"value":{"nested":[1,null,true]}} with kotlinx.serialization produced:

Serializer for class 'Any' is not found.

Thus the generator does not handle every construct in this pinned document. The fallback uses JsonElement? for arbitrary error values. Its regression test covers objects, arrays, numbers, booleans and null.

ContractTest checks:

  • All 20 TV method/path pairs against the pinned paths, including response and mutation body schemas.
  • All 68 maintained schemas through their actual serialization descriptors: fields, references, primitive widths, collections, maps, enum wire values and requiredness. Read-only $schema metadata is intentionally ignored. Optional scalar properties use nullable Kotlin fields to represent absence; encoding omits them.
  • A separately reviewed schema snapshot, src/test/resources/model-contracts.json, so changes to formats, limits, defaults or other schema constraints also require explicit review. Models preserve those wire shapes; Conductarr remains responsible for business validation and authority.

When updating the pin, update the models and reviewed schema snapshot together. Do not regenerate the Seerr client into this module or substitute a running server's schema.

API and identity

Create a ConductarrClient(baseUrl, jellyfinToken, cache) for the selected person's Jellyfin session. The caller supplies the token; the client sends exactly Authorization: MediaBrowser Token="<token>". The token must match the pinned contract's 32-hex Jellyfin access-token shape. HTTPS is required except for loopback fake servers. Base URL credentials, query strings and fragments are rejected. A path prefix is preserved.

TvRoutes provides typed calls for operator, profiles, feed, dismissals, catalogue options/discovery/title/cast/related/enrichment/seasons, performers/filmography, activity/readiness, requests and library audiences. Request actors cannot be supplied: only the authenticated operator establishes the actor. The request body's requester selects the beneficiary.

client.read(call, trigger) is a cold Flow<ReadResult<T>>: collect it once per screen-level read. It emits saved data before starting network work, then a revalidation result when due. ReadResult.Failure.saved retains LKG on transport, HTTP and decoding failure. A 401 returns NeedsSignIn without fallback, invalidates that client's identity-scoped saved data, and prevents further calls on that instance. Create a new instance after sign-in/person switch. No Seerr fallback exists.

client.mutate(call) returns a typed success or error. It never uses cached receipts, retries or follows redirects. Unknown request outcomes must be checked using TvRoutes.requests(tmdb, media); do not blindly resend.

The transport owns its OkHttp client rather than accepting arbitrary credential-logging interceptors. Diagnostics expose only ClientEvent enum values. Returned errors exclude bodies and exception text. Default timeouts: connection 10 s, read 20 s, whole call 30 s. At most two catalogue reads run concurrently. Close the client when its identity/session is discarded.

Saved data and refresh events

Pass an app-private, backup-excluded directory to FileCacheStore; one store instance owns a directory in a process. Writes use a synced temporary file and atomic same-filesystem replacement. Corrupt records become misses. MemoryCacheStore is the deterministic fake. Data is not encrypted: cached household content belongs in private app storage, never shared/external storage.

Cache keys partition by normalized server URL, credential digest, endpoint, path parameters and sorted query. Raw credentials are neither stored nor logged. A stale lifetime means refresh eligibility, not deletion of LKG. Saved.savedAtMillis supplies the Saved HH:MM timestamp; 304 preserves that timestamp while advancing validation time.

Operation family Refresh rule
Operator and recommendation profiles Retain indefinitely; LAUNCH refreshes
Library audiences Daily
Feed Server expires_at; conditional ETag revalidation
Catalogue options FOREGROUND refreshes
Discovery pages 30 minutes per parameterized lane/page
Enrichment ratings 6 hours; 300 entries per movie/TV group; 404 means saved absence, failures never overwrite LKG
Title, cast, related, performer, filmography, dismissals, readiness OPEN refreshes
Seasons OPEN on season focus; retained per season, 300 entries
Activity 30 seconds for caller-driven POLL, or FOREGROUND
Title requests and request mutations No cache

IF_STALE observes lifetimes; FORCE explicitly revalidates; CACHE_ONLY paints without network. The caller schedules launch, foreground, screen-open and activity polling events; the module does not run a poller or own a UI spinner. Title-related calls should be hoisted, not issued by individual cards. ETags are sent when present on any endpoint, regardless of Cache-Control: no-store; this is the TV-owned application cache, not an HTTP response cache.

Verification

The root make test selects testDefaultDebugUnitTest in this JVM module as well as the app. That module alias runs the JUnit contract/MockWebServer suite and the ClientSmokeKt executable. The smoke exercises a real loopback socket, disk reopen, ETag 304, and 503 recovery. All tests are local; none contact Conductarr or Jellyfin.

The build gate is make test followed by make apk-debug, using the repository's serialized Docker Gradle wrapper. No signed release, installation, ADB, version change or production activation is part of this module.