All checks were successful
review/code-claude Claude cross-family review: clean, no blocking findings
64 lines
6.8 KiB
Markdown
64 lines
6.8 KiB
Markdown
# Conductarr TV client
|
|
|
|
A UI-free JVM module for the TV operations in the [approved TV spec](https://pages.justinvisser.org/wolfskin/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:
|
|
|
|
```text
|
|
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.
|