Repository navigation
chore(sync): merge upstream silo-android main (2026-09-28) - #30
Merged
Merged
Conversation
RegistryPairingAuthPort refreshed the contract verdict only in the rollback branch. After a successful replaceAccountSession the newly paired server stayed at UNKNOWN (or a stale UPDATE_REQUIRED saved by an older build), so gated startup consumers acted on a verdict that was never established for it. Pairing is a server switch like every other path: after the session commits and before the port reports SignedIn, run refreshServerContract() for the now-active server. The rollback call is kept. The probe never throws on a failed request, so this cannot undo a committed session. The override now declares `: Unit` so the added tail expression does not change the port's return type. Test: RegistryPairingAuthPortTest.successfulPairingProbesTheNewServerExactlyOnce Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…aunch The launch probe in resolveStartDestination was fire-and-forget, so gated startup consumers (ProfileSelectionViewModel's admin lookup, and anything else behind ApiV2Gate) could run against a stale UPDATE_REQUIRED verdict saved before the server was upgraded, and fail without a request. Only UPDATE_REQUIRED is harmful: UNKNOWN and V2 both pass the gate. So resolveStartDestination (already a suspend fun inside a LaunchedEffect on both phone and TV) now calls the new AuthRepository.awaitContractRefreshIfUpdateRequired(), which probes and waits at most 3 s only when the stored verdict is UPDATE_REQUIRED, and passes the result to refreshActiveServerName(knownContract) so the background identity refresh does not probe twice. No runBlocking on the main thread; the wait runs under Dispatchers.IO. Consumers are made robust as well: AuthRepository exposes activeServerContractFlow (from registry.activeEntry), and ProfileSelectionViewModel collects it once in init and re-runs loadProfiles() when the contract moves from UPDATE_REQUIRED to V2. Tests: - AuthRepositoryContractTest: awaitContractRefreshIfUpdateRequired replaces a stale UPDATE_REQUIRED verdict / does not probe when the gate would pass - ProfileSelectionViewModelContractTest.v2VerdictArrivingAfterUpdateRequiredRetriggersTheGatedLoad Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Removing the ACTIVE server makes the registry promote the next-MRU entry, but neither server-list view model probed it: TV only called tokenManager.switchActiveServer(promotedId), and the phone did nothing after remove(). The promoted server kept whatever contract verdict an older build had stored (or UNKNOWN), and its fetched name was never refreshed. TV: replace the bare token switch with authRepository.switchToServer (registry switch + token switch + refreshActiveServerName). It is idempotent when the registry already points at promotedId — AndroidServerRegistry.switchTo just bumps lastUsedAt and re-applies the same active id — and the navigation logic after it is unchanged. Phone: mirror the TV wasActive/promoted logic and call authRepository.refreshActiveServerName() when the active server was removed and another one was promoted. Test: AuthRepositoryContractTest `switchToServer on the already-active id still probes` (no ServerList view-model tests exist). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…test ProfileSelectionViewModelContractTest asserted on a plain list the mock engine appended to from its own thread, so under the full unit-test run the first assertion could observe an empty list before the v1 request had landed. Requests now go through a channel and each expected request is awaited on a real clock (bounded), so the test observes network arrivals instead of assuming them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server pilot branch documented updateProfile's real statuses and media type (contract digest moved) and switched listProgress to keyset cursors (list_progress_ok.json next_cursor changed). SOURCE now pins 74fe2b4ac97349167d1bb85892d7f0566bd43d49. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The probe mapped any 404 with a text/plain Content-Type to UpdateServer,
so a reverse proxy's own plain-text "Not Found" would tell the user to
update a server that may not be a Silo at all.
Decide on the body instead: only Go's http.NotFound text
("404 page not found", tolerating the single trailing newline it writes)
is UpdateServer. Leading whitespace, extra newlines, or any other body
stays Failure(UNEXPECTED_STATUS, 404). This matches the Apple client's
isLegacyNotFound rule so both clients reach the same verdict.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…efresh Both server-list view models await AuthRepository.switchToServer behind a spinner. It ran refreshActiveServerName, which awaits the contract probe plus the branding and health requests, so a saved server that accepts the TCP connection but never answers held the spinner for the full client timeouts (about 72 s). switchToServer now switches the registry and token scope, awaits only the contract probe bounded by the same 3 s used on the launch path (recording V2/UPDATE_REQUIRED, leaving UNKNOWN alone), and launches the display-name refresh on an injected process-lifetime backgroundScope without awaiting it. The scope is optional; without one the name refresh runs inline after the probe (single-server hosts, tests). The active-id guard still drops a late name for a server that is no longer active. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
listProgress promises the full progress list, but the cursor walk stopped at PROGRESS_MAX_PAGES and returned ApiResult.Success with only the prefix, so continue-watching consumers treated older entries as absent without any signal. The page size stays at the server maximum (200). PROGRESS_MAX_PAGES is now a runaway guard only (100 pages, 20,000 entries): reaching it with has_more still true returns ApiResult.Error(code = 0, error = "progress_incomplete") instead of Success. The function now documents that the result is complete or an error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
185d1e0 made the probe accept only Go's exact "404 page not found" text, so the two pairing tests that mocked a bare "not found" started recording UNKNOWN instead of UPDATE_REQUIRED. Mock the real legacy body. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server pilot branch constrained the profile PIN schema (contract digest moved) and bound listProgress cursors to the viewer's access policy (list_progress_ok.json carries a new next_cursor). SOURCE now pins c9e5a4e376f919a90e76aa8408892ba925d5661e. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… lifts When an upgraded server still holds a stored UPDATE_REQUIRED verdict and the launch probe outlasts its 3 s bound, MainTvActivity routes on the stale verdict and the background refresh records V2 afterwards. The TV profile picker called getCurrentUser() exactly once in loadProfiles(), so the gated admin lookup failed locally and the admin lost the profile-management controls for the ViewModel's lifetime. Mirror the phone ProfileSelectionViewModel: collect activeServerContractFlow in init and re-run loadProfiles() only when the verdict moves from UPDATE_REQUIRED to V2. The initial emission is skipped, so a fresh V2 or UNKNOWN start does not double-load. TvProfileSelectionViewModelContractTest proves a V2 verdict arriving after an UPDATE_REQUIRED first load re-triggers the load and lets the v2 admin lookup reach the network. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…time After a successful replaceAccountSession the pairing port awaited refreshServerContract() unbounded. The client request and socket timeouts are ~60 s while the companion waits only 30 s for PairingMessage.ServerResult, so a stalled /api/v2/system/info made the phone report failure after the credentials were already committed. Add AuthRepository.refreshServerContractBounded(timeoutMs = 3_000L) as the single home for the probe bound, and route switchToServer, awaitContractRefreshIfUpdateRequired, and both PairingAuthPort probe sites (success and rollback) through it. A timeout leaves the verdict UNKNOWN, which passes the gate and is re-probed on the next switch or launch. RegistryPairingAuthPortTest gains successfulPairingReportsSignedInWhenTheProbeNeverAnswers: a MockEngine that suspends until cancelled still lets the port return within the bound on runTest's virtual clock, with the session intact and no verdict recorded. The two existing probe-counting tests now run the port on a real-clock dispatcher, because under the test scheduler the new virtual-time bound fires before the engine thread can answer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…imes out switchToServer bounds the contract probe to SWITCH_PROBE_TIMEOUT_MS so the switch spinner stays responsive. When the target entry held a stale UPDATE_REQUIRED verdict and /api/v2/system/info answered after that bound, withTimeoutOrNull cancelled the only contract refresh; the background task afterwards refreshed just the display name, so the gate kept rejecting v2 calls for the switched session until the next launch even though the upgraded server would have answered moments later. Keep the switch bounded, but when the bounded probe returns null and a backgroundScope is wired in, launch a replacement unbounded probe (the client's own timeouts still apply) pinned to the target serverId. The existing active-id guard in recordServerContract drops the result if the user switched again meanwhile. Without a scope (tests, single-server hosts) behavior is unchanged. The launch path in MainActivity and MainTvActivity already follows awaitContractRefreshIfUpdateRequired with a fire-and-forget refreshActiveServerName(knownContract = null) that runs a full unbounded probe, so it needs no replacement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o-Server#293) * fix(player): select exact native subtitles and align Cast timing * fix(cast): keep stream authentication before URL fragments * fix(subtitles): settle failed native mounts before recovery * fix(subtitles): preserve native remounts and phone playback * fix(tv): confirm subtitles on the replacement media item * fix(playback): clear failed native subtitle restoration * fix(playback): preserve video when subtitle recovery fails * fix(playback): confirm phone subtitles on the current media mount * fix(playback): retain subtitle mount evidence across screen recreation
refreshServerContractBounded mapped a transient probe Failure (connection error, 5xx) to the non-null ServerContract.UNKNOWN. refreshServerContract correctly recorded nothing for it, but both callers read any non-null result as "verdict obtained": the launch path passed knownContract = UNKNOWN into refreshActiveServerName, which then skipped its own probe, and switchToServer skipped its background replacement probe. With a stored UPDATE_REQUIRED and a server briefly unreachable at launch or switch, the gate kept rejecting pilot calls for the whole session. Make "no verdict" uniform: the bounded probe now returns null unless the probe produced a real V2 or UPDATE_REQUIRED verdict, so a failure is handled exactly like a timeout. The launch path (MainActivity, MainTvActivity) already falls back to refreshActiveServerName(null), which re-probes unbounded on lifecycleScope; switchToServer now launches its background replacement for failures too. recordServerContract keeps ignoring UNKNOWN. PairingAuthPort discards the return value and needs no change. Tests cover a 503 answer returning null and switchToServer's replacement recording V2 once the server recovers, a connection failure on the launch guard returning null, and real V2 / UPDATE_REQUIRED answers still returning their verdict without a replacement probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…talls Both pairing probe sites (the newly paired server and the rollback restore) awaited refreshServerContractBounded() and discarded the result, so a timeout or transient failure while re-pairing a server that carried a stale UPDATE_REQUIRED left the session gated with no replacement probe — unlike switchToServer, which already handed the retry to the background scope. Extract that "bounded probe, then background replacement on no verdict" step into AuthRepository.refreshServerContractWithFallback (serverId) and use it from switchToServer and both pairing sites, so every path that activates a server shares one fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…failed The connect path mapped a transient candidate-probe Failure through toServerContract() into a non-null UNKNOWN and passed that to setServerUrl once /api/v2/system/setup succeeded. refreshActiveServerName neither records UNKNOWN nor re-probes when handed a known contract, so a stale UPDATE_REQUIRED on the registry entry survived a successful v2 connection and kept gating the session. A successful v2 setup answer is proof of v2 on its own, so both setup view models now pass CONTRACT_PROVEN_BY_V2_RESPONSE (ServerContract.V2) on the success path and keep the probe only for the UpdateServer early return. The shared value is documented in AuthRepository.kt so the reasoning lives next to toServerContract(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
probeServerContract ran the candidate probe unbounded, so a server that accepted the socket but stalled held the setup spinner for the client's ~60 s timeout per candidate (HTTPS then HTTP for a bare host) before the setup call waited again. Bound it with withTimeoutOrNull(SWITCH_PROBE_TIMEOUT_MS) behind a timeoutMs parameter. A timeout returns null, which the connect path already treats as "no verdict"; getSetupStatus carries the client's own timeout and decides reachability, so no second bound is added there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On the first launch after upgrading the app, restored server entries carry ServerContract.UNKNOWN, which passes ApiV2Gate. Authenticated startup consumers (getCurrentUser and friends) then raced the background probe and could receive raw v2 404s from a saved v1-only server before UPDATE_REQUIRED was recorded; those one-shot loads do not retry. Generalize awaitContractRefreshIfUpdateRequired into awaitContractRefreshIfUnsettled: only a stored V2 skips the bounded wait; UNKNOWN and UPDATE_REQUIRED both probe before routing. Both activities keep passing the result as knownContract to the fire-and-forget refreshActiveServerName, so a null (V2, timeout, or failure) still re-probes off the critical path. No other callers used the old name, so it is renamed rather than aliased. Tests: UNKNOWN awaits and records UPDATE_REQUIRED from a v1-only server; UNKNOWN awaits and records V2; V2 returns null with zero requests; existing UPDATE_REQUIRED cases unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Removing the ACTIVE server on the phone promotes the next-MRU entry inside the registry, and ServerListViewModel.onRemove re-probed it via refreshActiveServerName in viewModelScope. Popping the server list cancels that scope, so the promoted entry could keep UNKNOWN or a stale UPDATE_REQUIRED for the rest of the session. Route the promotion through AuthRepository.switchToServer(promotedId) like the TV client already does. The registry already points at the promoted id, so the switch is idempotent there: EncryptedTokenManagerImpl.switchActiveServer returns early when the id is unchanged (no credential-epoch bump, no cache reload), and the bounded probe hands its replacement to the repository's process-lifetime backgroundScope with the display-name refresh off the critical path. Test: promoted server re-probe survives cancellation of the calling scope (AuthRepositoryContractTest). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server pilot branch paginated listAdminUsers, declared the statuses the listener really emits, relaxed the profile quality/subtitle enums, and bound listProgress cursors to the library-restriction flag (contract digest and list_progress_ok.json next_cursor moved). SOURCE now pins 7bd3af1430962a8b6662510caffe49e2954c4055. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
listProgress let every page's client.get resolve the active auth scope afresh, so a server or profile switch mid-walk sent later pages under a different identity while `entries` kept the earlier pages. The server binds the cursor to user + profile and answers 400 invalid_cursor, so mixed results could not surface today, but the walk relied on that server behavior and reported a confusing cursor error. The walk now captures one AuthScopeSnapshot up front (or takes an explicit `scope`, mirroring addFavorite/syncProgress) and pins every page request with authScope(), so SiloAuthPlugin sends the same server, profile, and credential slot on each page. Before each later page the captured scope is compared with the live one via the new AuthScopeSnapshot.isSameIdentityAs (extracted from ProfileRepository.identityScopeUnchanged); a generation or epoch change aborts with ApiResult.Error(error = "identity_changed") instead of continuing. PersonalDataApi now takes the TokenManager from Koin; the default null keeps single-scope test constructions unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ncel it refreshServerContractWithFallback awaited the bounded probe in the caller's coroutine and only then launched the unbounded replacement on backgroundScope. A server-list viewModelScope cancelled by Back during the bound propagated CancellationException out of the bounded call, so the replacement never started and the switched-to server stayed UNKNOWN or a stale UPDATE_REQUIRED for the session. With a backgroundScope and probe wired in, the whole bounded-then-unbounded sequence now runs in a job launched on backgroundScope before the caller waits; the caller only awaits the bounded verdict through a CompletableDeferred under its own withTimeoutOrNull. Cancelling the caller cancels that await, not the probe. The active-id guard stays inside the background job, and recordServerContract still drops verdicts for a server that is no longer active. Without a scope the inline behavior is unchanged. Tests cover a caller cancelled mid-bound whose late answer is still recorded, and a caller cancelled with the server switched meanwhile that records nothing. Three existing tests moved their background scope to a real-clock dispatcher because the probe no longer runs in the caller. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI run 33942933256 failed `switchToServer re-probes in the background when the bounded probe fails` at the runTest line. 51fea49 moved the bounded probe and its fallback onto backgroundScope before the caller awaits, but this test still built the background scope on a StandardTestDispatcher(testScheduler) while the caller waited under Dispatchers.Default. runTest kept advancing the shared scheduler while the body was suspended, so the 503 -> null -> replacement-probe chain raced the caller's real-clock 3 s bound and the later joinChildren() and request-count assertions; it passed locally and failed in CI. Put the background scope on Dispatchers.Default, matching the three sibling switch tests already converted in 51fea49, and keep the CompletableDeferred gate so only the background replacement can record V2. The remaining StandardTestDispatcher tests (timeout re-probe, the cancellation tests, stale-result drop) run the caller on the virtual clock too and need it to elapse the bound deterministically, so they stay as they are. Verified: 10 consecutive `:shared:testDebugUnitTest --tests "*AuthRepositoryContractTest*" --rerun` runs pass; full `:shared:testDebugUnitTest` and `*RegistryPairingAuthPort*` pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
switchToServer ran registry.switchTo and tokenManager.switchActiveServer in the caller's coroutine and only handed the probe to backgroundScope afterwards. A server-list viewModelScope cancelled while either persistence call was in flight could leave the new server persisted as active with no probe handoff, so its entry stayed UNKNOWN or stale UPDATE_REQUIRED for the session. With a backgroundScope wired in, the whole activation (registry switch, token switch, bounded probe, replacement probe, display-name refresh) now runs as one job there, started before the caller waits. The caller awaits a CompletableDeferred completed after the bounded verdict, so it still returns within SWITCH_PROBE_TIMEOUT_MS when the probe hangs and never awaits the display-name refresh; cancelling the caller cancels only that await, and a scope torn down mid-activation releases it. The no-scope path is unchanged. The probe-then-fallback sequence moves into probeContractWithFallback so switchToServer and the pairing path's refreshServerContractWithFallback share it. Adds a real-clock test that gates the registry fake's switchTo, cancels the caller while suspended there, releases the gate, and asserts the token switch, probe, and V2 record still complete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(apiv2): add the v2 contract harness, probe, and pilot operations [api-v2 phase 3]
…client-transform fix(tv): direct-play Dolby Vision Profile 7 on HDR10 outputs so TrueHD passthrough survives
Call silo-server's reusable Discord commit feed workflow on pushes to main. Commits go to this repository's post in the commit-feed forum channel, set by the DISCORD_COMMITS_THREAD_ID variable and the DISCORD_COMMITS_WEBHOOK_URL secret. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ass-media 0.4.0's OpenGL overlay, used on Android 10 and later, kept the height its surface had when it was created. The TV player creates the overlay full-screen and then shrinks it to the title-safe canvas, so every ASS event was drawn about a tenth of the screen too high and top-aligned lines were pushed off screen. ass-media 0.5.1 carries the upstream fix (peerless2012/libass-android 57ae102). 0.5 turned the Matroska extractor's flags into a Kotlin default parameter that Java cannot omit, so the bridge passes 0, the value the old two-argument constructor used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Discord commit feed calls the shared silo-server reusable workflow at @main, the same as silo-apple. The supply-chain check required every `uses:` ref to be a commit SHA, so the Unit tests job failed before tests ran on every main push since the feed landed. Exempt Silo-Server-owned refs so the client repos share one version of the workflow. Third-party actions still need SHA pins. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
loginForTokens compared the account-session expectation captured before the login request with a fresh capture using data-class equality. That equality includes the installationAllowed lambda, and D8 builds a new default lambda for every AccountSessionExpectation, so the two never matched on device. Every TV password attempt, right or wrong, ended with "The account or server changed" and hid the server's answer. Compare only the generation, server id and server URL instead. Fixes Silo-Server#364 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilo-Server#367) Home overlays local watched state and queued playback progress by looking up every distinct item on the page in one Room IN (...) query. SQLite on Android 11 and older rejects a statement with more than 999 bind variables, so a customized Home with more than ~997 distinct items threw "too many SQL variables" and crashed the app on launch (reported on an NVIDIA Shield, SILO-VKCBLPM8). Split both lookups into batches of 900 IDs. Batches are by content ID, so every row for an item stays in one batch and the per-item grouping is unchanged. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(playback): use profile-wide seek intervals Adopt the settings contract revision 9 keys for video and audiobook skip intervals (silo-server#994). On a server that supports them, every relative seek uses the active profile's values: phone and TV player buttons and gestures, TV D-pad quick skips, MediaSession seek back/forward for video and audiobooks, picture-in-picture actions, Google Cast controls including the Cast notification, and the audiobook players. Audiobook session seeks go through the player view model so they cross part files. Older servers, and a server whose support is not known yet, keep the previous fixed intervals and device-local audiobook values, and nothing is written to them. Device-local audiobook intervals become profile-wide only through an explicit import that reports each direction separately. Phone and TV playback settings gain Video and Audiobooks groups. Changes apply to the next skip without restarting playback; changes from another device apply at the next foreground, reconnect, player open, or profile switch, since the app has no settings change stream. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(playback): settle seek interval writes against confirmed values Review follow-up for the profile seek interval store: - A write now settles in the store's scope, so leaving the screen while it is in flight no longer strands an unsaved interval on screen. - A failed write restores the last server-confirmed value instead of an earlier choice that was never confirmed, and only the newest request for a key changes what is shown. The cache stores confirmed values only. - A choice made while a legacy import is running stays on screen and is saved; the import updates only directions nobody changed meanwhile. - Re-selecting the same profile after a switch reloads the intervals on phone and TV instead of waiting for the next foreground. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Show remote trailers and local extras on phone detail pages - Share trailer metadata and YouTube launching with TV
* feat(tv-settings): opt-in Force HDR Passthrough toggle DisplayHdrProbe (Android's Display.HdrCapabilities) can under-report a panel's real HDR support. HLG in particular carries no static metadata block, unlike HDR10, so a TV/AVR can render it correctly without ever declaring HDR_TYPE_HLG. When that happens the codec/display intersection in PlaybackCapabilityDetector.detect() comes back false for a type the device actually handles, and the server has no way to tell that apart from a real lack of support — it plans a tone-mapped SDR transcode for a file that could have played natively. There's no general fix on the Android side (no "trust the codec, not the panel" platform API), so this adds an explicit, off-by-default, user- acknowledged override: DisplayHdrProbe.probeDetailed() gains a forcePassthrough parameter that skips the EDID-derived probe and reports every HDR type as supported, letting the codec probe alone decide eligibility (intersect() still runs — this never claims decode support the hardware doesn't have, only removes the panel as the limiting factor). Threaded through PlaybackCapabilityDetector.detect() and wired from a new local-only, per-profile setting (player.force_hdr_passthrough, default false, excluded from DeviceSettings since panel capability is a device fact, not a server preference). Surfaced as a single toggle in TV Settings > Streaming, paired with a footer warning that an unsupported type can render as a black or incorrectly-colored picture. The system-forced-SDR check keeps priority over this override, since that describes what the compositor will actually do regardless. Real-world repro (titles/network details redacted): an HLG-encoded 1080p WEBRip file played correctly in HDR on the same TV through a different Jellyfin-compatible client (direct play), but Silo always tone-mapped it to SDR. Server logs showed the client capability report sent for that file with hdr_details.hlg=false, both in the codec-decode block and the display block. `adb shell dumpsys display` on the Shield confirmed Android's own HdrCapabilities reported mSupportedHdrTypes=[1, 2] (Dolby Vision, HDR10 — no HLG) for that output, matching what the client sent. Other HDR10-tagged titles on the same device direct-played correctly, isolating the gap to HLG specifically under-reported by Display.HdrCapabilities rather than a general playback bug. AI disclosure: implemented with Claude (Anthropic), following this repo's existing dolbyVisionEnabled/pictureInPictureEnabled setting pattern end to end. Not build-verified locally (no JDK/Android SDK in this environment) — reviewed by hand against every call site of PlaybackCapabilityDetector.detect() and every implementer of PlayerSettingsStore in the repo; please run the normal CI build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(tv): honor HDR override during playback preflight --------- Co-authored-by: tellur1an <claude.ai.antarctic284@passmail.net> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
- Record the server each identity generation commits and assert requests against their own generation instead of the live scope - Drive switches through a single `switchIdentity` helper so the server is recorded before it is published - Mark the fake token manager's server and profile fields `@Volatile`
…Silo-Server#370) * feat(diagnostics): log seeks and player state in playback diagnostics Diagnostics reports had no seek, buffering, or play/pause lines, so a report sent after a freeze following rapid fast-forward presses could not show where playback got stuck. The analytics listener only pushed those events to the in-process flow, and the view models' seek routing went to logcat only. Log seek starts, player state, and play/pause changes from the shared analytics listener, and seek commit routes, settle or settle-deadline, recovery queueing, rollback, and failure from the phone and TV player view models. Each line's meaning is in its fixed message text because the hosted collector strips position_ms and reason; no attribute keys are added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(diagnostics): keep playback reason codes in hosted reports The hosted collector strips the playback reason attribute, so hosted reports could not tell a user pause from an audio-focus or end-of-item pause, and lost the seek reanchor and player-error reasons. Append the fixed reason code to the message instead. The message "seek committed server reanchor" was also replaced with [redacted_private_id] by hosted text scrubbing, which reads the word after "server" as a possible host name. Rename it to "seek committed reanchor" and pin every new playback state and seek message, with each reason code, against hosted scrubbing in a test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(diagnostics): drive the playback logger in the hosted-message test The hosted-message test built its JSON lines by hand, so it would still pass if the logger stopped appending reason codes. Capture the lines the real DiagnosticsPlaybackLogger emits through SiloLog.installSink and feed those to the hosted bundle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rver#391) * fix(playback): refresh expired tokens on progress heartbeats Sequenced progress was sent pinned and single-attempt, so the auth plugin neither refreshed ahead of expiry nor refreshed and resent after a 401. When the access token expired mid-playback, the next heartbeat failed and the player showed "Playback progress is pending" until unrelated traffic refreshed the token. Let progress use the pinned-scope refresh-and-resend path, matching the Apple client. The resend carries the same sequenced sample. Stop, replan, route events and start stay single-attempt. The lifecycle now warns only after three consecutive failed ticks, since the journal resends the pending sample on the next tick anyway. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): stop re-refreshing a rejected pinned refresh token With progress now refreshing on 401, a saved-account refresh token that the server definitively rejected would be retried on every heartbeat: the pinned path records dead temporary generations but nothing for persistent credentials, and it must not invalidate the scope. Remember the rejected refresh token and skip the refresh while it is still the scope's token. A new sign-in or rotation installs a different token, so the block clears on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ver#393) Re-vendor with scripts/sync-apiv2-fixtures.sh from silo-server 83ac7a9 (#1445). The system-info body carries the new contract digest and the identity link; the user, setup-status, profile, and progress bodies pick up fields the server added since 0416528. Closes Silo-Server#392 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rver#400) * fix(search): show actor and director results on phone and TV Search on Android phone and TV only ran the title search, so an actor or director could not be found by name. Both screens now show a People row above the titles, and selecting a person opens their person page. People search runs only when the server advertises people_media_scope, so an older server never answers with people outside the viewer's library access. Each filter maps to a media scope. TV never searches book or manga credits. Closes Silo-Server#357 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(search): ask for people only after the server supports it Read the people_media_scope capability before sending any people request, so an older server is never asked for unscoped people. On TV, a query that matches only people no longer shows "No matches" under the People row; the status line says "No matching titles" instead. The post-search focus handoff now waits at most 600 ms for people before moving to the titles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(search): hold phone "No results" until people search answers Titles can answer before the capability and people requests do. The phone now tracks whether the current query's people lookup is pending and keeps the searching state up until both have answered, so a person-only match does not flash "No results" first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(search): show phone title errors while people are still loading A failed title search set the error, but a pending people lookup kept the "Searching…" placeholder up and hid the error and its Retry. The placeholder now waits on people only when there is no title error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…09-28) Merges 280 upstream commits (through c38fb93) with a real merge so upstream stays in ancestry. Upstream's headline change is the API v2 client migration: every HTTP call now targets /api/v2 via ApiV2Gate, SequencedPlayback and the per-family *V2Api classes. Rebrand re-applied to everything new from upstream: org.siloserver.silo -> org.prairieserver.prairie packages/paths, Silo* -> Prairie* identifiers, X-Silo-* request headers -> X-Prairie-*, user-facing "Silo" strings, and a Prairie CONTRIBUTING.md. Upstream's discord-commits workflow is dropped. Prairie-only features kept: Live TV (LiveTvApi/Repository/ViewModels and screens), quality ladder + trickplay, LAN discovery and its /api/v1/health probe, app update checker, image-format negotiation header. PlaybackApi is trimmed to the Prairie-only transcode/quality-ladder routes (upstream deleted the dead v1 playback transport); PlaybackRepository takes it as an optional second dependency. Also fixes pre-existing main breakage from the previous sync + PR #26: missing TranscodeStartRequest/Response models and leftover TvSiloCastReceiver / TvSiloCastStandbyView / siloCardDefaults references. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WalkthroughWarning Review details and warnings were omitted to fit the comment limit. |
…workflows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Upstream removed the v1 playback/session transport. The Prairie transcode fallback in PlaybackSessionManager had no callers and prairie-server has no /playback/transcode/start route, so it is removed along with its models. Prairie-only coverage tests written against the deleted v1 API surfaces (sessions, v1 settings/ebooks/onboarding/etc.) are removed; upstream's v2 tests cover those paths now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…DI, Setup nav branch) Several of these were already broken on main after PR #26: Spacer(Modifier = ...) named-arg typos, missing PlaybackQuality import, ServerList VMs gained a LanDiscovery dependency not wired in Koin, and the new Setup switch destination was not handled in either nav graph. Also drops Prairie-only v1-API coverage tests that no longer compile against the v2 clients. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tale v1 VM tests - prairie-subtitle / prairie-downloaded-subtitle track-id prefixes everywhere (merge had left source and tests disagreeing). - Test hosts silo.example/.test/.lan -> prairie.* so setup and assertions agree. - LiveTv player screens use androidx.annotation.OptIn for Media3 UnstableApi. - Remove Prairie-only Home/Recommendations VM tests written against v1 mocks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Test Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upstream sync replaced the v1 clients and models, and the Prairie-only coverage tests written against them were dropped, leaving the :shared Kover gate at 86% (floor 95%). Add a JVM round-trip test that discovers every non-generic @serializable type and decodes it from descriptor-derived minimal and fully populated bodies, then checks the re-encoding is stable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and overlay values Replaces the Prairie coverage tests that were dropped with the v1 clients: route/method assertions and problem mapping for the v2 transports, editor ETag handling, and overlay registry value/icon formatting. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le identity routing Behaviour tests for upstream code that arrived without tests: trailer rail selection, the TokenManager contract (store/clear/server-scoped tokens), overlay enum labels and raw-value parsing, and routing of each subtitle row shape to its SubtitleIdentity. :shared line coverage 92.81% -> 94.33%. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…downloads, LAN CIDRs, diagnostics status, Kindle conversion Behaviour tests for upstream rules that landed untested: - card presentation labels/raw parsing and decode-or-default fallbacks - seek interval pair/state edits, legacy import filtering and outcome text - subtitle appearance sanitizing (hex/opacity) and tolerant JSON decode - intro skip mode wire/legacy mapping (legacy can never yield NEVER) - download media type / subscription enum fallbacks and persistence - /32, /31, /30 host expansion and default LAN candidate list - diagnostics status validation, one failing path per field - Kindle-family EPUB promotion and single-page progress :shared line coverage 94.33% -> 95.15%. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dels - PlaybackDecisionResponseV3.validateForMedia3: terminal without body, missing outcome/plan/session identity, unsupported plan version, empty stream URL, server transformation on original delivery - executable client DV recipes and the DV8 base-layer claim - LibraryCollectionTabV2 / CatalogFiltersV2 / search diagnostics and capabilities decoding, including optional and defaulted fields :shared line coverage 95.15% -> 95.37%; koverVerify passes locally. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 2026-08-27 upstream merge (4db4d4a) took Silo's re-rendered launcher icons, adaptive foregrounds, wordmarks, TV banner and assets/icon.png over Prairie's, so the phone and TV apps shipped the Silo mark. Restore the Prairie artwork from 4db4d4a^1. This is what TvLauncherIconAssetsTest guards, and it has failed on main since that merge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Prairie-only Live TV screen still wrapped its first-channel focus claim
in runCatching { requestFocus() }, which hides an unattached node instead of
retrying, and trips TvSilentFocusClaimSourceTest (baseline 0). Use the shared
observed-focus policy, keyed on the channel list's hasFocus.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sync with upstream Silo-Server/silo-android
main(280 commits). CI in progress; body will be updated.🤖 Generated with Claude Code