Repository navigation
Sync upstream silo-android (83 commits) — rebrand-aware merge - #41
Merged
Merged
Conversation
…r option (Silo-Server#413) * feat(playback): add independent subtitle text opacity and a gray color option White subtitle text against a dark background is uncomfortably bright on an OLED or HDR display. The subtitle appearance settings had no way to dim the text itself: backgroundOpacity only affects the box behind it, and the font color palette had no gray option. Adds a textOpacity field (1-100, default 100) alongside the existing backgroundOpacity, applied to the text color independently of the background, plus a gray swatch in the font color palette, on both phone and TV. Both opacity controls moved from a slider to a typed percent field (phone) or a D-pad picker (TV, matching the existing paradigm) after a slider proved too imprecise to hit low values. Also re-vendors the settings contract at manifest revision 13 and fixes a found bug where the percent field committed on every keystroke instead of on blur, which could leave the displayed value out of sync with what was saved. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(playback): keep TV opacity pickers in sync with off-cadence values The TV background/text opacity pickers only listed their fixed step values (0/25/50/75/100 or every 5), so a value set via the phone's free-typed percent field could have no matching option. Opening the picker then selected the first step, and pressing Select silently overwrote the real value. Fixed with a shared helper that unions the step list with the current value, matching the existing delayPicker pattern. Also commits a pending opacity edit when the subtitle sheet is dismissed while the field is still focused, since that tears the composable down without firing onFocusChanged(false). Found by CodeRabbit and Kody review on PR Silo-Server#413. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(playback): apply subtitle-appearance edits as a transform, not a snapshot Every control in the phone subtitle-style sheet built its update as appearance.copy(field = value) against a composable-captured appearance snapshot, then wrote the whole object back. Two edits that both go through that snapshot in the same composition pass — most reachably, the two opacity fields committing independently as the sheet is dismissed, since the sheet can tear down both PercentInputRows before either write round-trips back into the read appearance would race: whichever write lands first can be clobbered by the second, which was still building its new value from the appearance the sheet had before the first write applied. SubtitleStyleSheet's onUpdate now takes a transform, not a precomputed value, and PlayerViewModel/SettingsViewModel apply it against the freshest value read from the store immediately before writing — the same pattern TvSettingsViewModel.editAppearance already used for the identical reason. Found by Kody review on PR Silo-Server#413, after the previous commit's fix for lost edits on sheet dismissal made this reachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(playback): make subtitle-appearance updates atomic at the store Reading subtitleAppearanceFlow.first() and then writing the transformed result back separately narrowed the race the previous commit targeted, but didn't close it: two concurrent callers (e.g. both opacity fields committing as the sheet is dismissed, each in its own launched coroutine) could still both read the same value before either write landed, and the second write would discard the first edit. Adds PlayerSettingsStore.updateSubtitleAppearance(transform), which reads the current appearance from DataStore's own preferences snapshot inside the same edit {} transaction that writes the result. DataStore serializes edit calls against each other, so this is an actual fix rather than a smaller window: no other write can land between the read and the write. The interface keeps a default (read-then-write) implementation for test fakes that only need to capture a value, not real atomicity. PlayerViewModel, SettingsViewModel, and TvSettingsViewModel.editAppearance (which had the identical read-then-write pattern, predating this feature) all move to the atomic method. Found by Kody (critical) and CodeRabbit (major) review on PR Silo-Server#413, after the previous commit's snapshot-based fix made the race reachable but not closed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(settings): re-vendor settings contract at manifest revision 14 The server moved subtitle textOpacity to revision 14 because revision 13 went to the advisory_age overlay id. Re-vendor manifest.json and conformance.json from the server and regenerate SettingKeys. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(playback): gate subtitle text opacity on settings revision 14 The server schema for playback.subtitle_appearance only accepts textOpacity from manifest revision 14 and rejects unknown properties. A server below 14 answers a write that carries it with a contract error, the flusher drops the op, and the next refresh reverts every subtitle edit in that write. The flusher now decides at send time: it strips textOpacity for a server known to be below 14, sends it whole from 14, and holds the write (kept queued and retried) while the revision is unknown, so a value queued before the probe answers neither fails on an old server nor resets the stored value on a new one. The revision comes from a shared SettingsContractRevision cache that refreshFromServer re-reads before it pushes. The phone style sheet, TV HUD, and TV settings hide the Text Opacity control when the server is known to be below 14. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(playback): enqueue subtitle appearance writes in commit order updateSubtitleAppearance applied its transform inside the DataStore edit but enqueued the result after the edit returned. The flusher keeps the last value enqueued per key, so two concurrent edits that committed A then B could enqueue B then A, and the server kept the older composite. The enqueue now runs inside the transaction, in updateSubtitleAppearance, flushProjectedSubtitleAppearance, and the override-enable path, so the queued value is always the one the latest commit wrote. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(playback): close the keyboard when a subtitle percentage is done Pressing Done in the Text Opacity or Background Opacity field committed the value but left the field focused and the keyboard open over the sheet. Done now commits, clears focus, and hides the keyboard. Commit on blur stays, and the row skips the repeat commit that clearing focus triggers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): drop a revision probe answered by a different server The capabilities request goes to whichever server is active when it is sent. A server switch during the fetch cached the new server's revision under the old URL, so the flusher could strip textOpacity for a server that stores it, or send it to one that rejects it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): start text opacity pickers at a readable value 1% reads as no subtitles. The HUD now offers 25-100% and Settings 5-100%, matching the tvOS app; a stored value between steps is still offered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(settings): point the vendored contract at its merged server commit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kamil Gielas <kamilgielas@MacBook-Pro-Kamil.local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
…o-Server#423) * feat(detail): show the same external ratings on every title page Title pages now show the server's ratings row: each source's mark and score in the order the server sends them. On the phone and TV detail pages the row replaces the IMDb-only token. TMDB's mark is its approved logo, and every other source's mark is its name as dimmed text. Screen readers hear "IMDb 8.5". A server without the row falls back to IMDb, then TMDB, never Rotten Tomatoes. On a TV series page, the ratings stay in the hero when an episode is focused. The TV focus marquee shows one rating with its mark, IMDb or else TMDB, where it used to show a bare number. Request pages show the TMDB vote average as a TMDB entry. Poster rating badges now carry their source in the label ("IMDb 8.5", "RT 93%", "RT Audience 95%") and draw no icon, so the tomato mark is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(detail): show at most three ratings on one line on phones With an administrator's extra rating sources turned on, the phone title page wrapped the external ratings onto a second line: Back to the Future with Rotten Tomatoes and Metacritic on showed three entries with two more underneath. The phone layout now shows the first three ratings in the server's order, on one line. When even those do not fit (a narrow screen, large text, or a long source name from a plugin), whole entries drop from the end instead of wrapping or being cut off. The tablet and fold layout and Android TV still show every rating. Web and iOS apply the same limit. The limit is ExternalRatings.PHONE_LIMIT with a forPhone helper, covered by a unit test. The TV's single-line row that drops whole tokens moves to android-shared so both apps use it; it relies only on stable Compose layout APIs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(detail): keep formats visible and one bad rating from breaking detail - TV hero: ratings drop before playback formats and sources when the facts row runs out of width. - The hero, Home marquee and TV request line share one facts row, so all three drop whole tokens instead of clipping; a long episode title still ends in an ellipsis. - Decode the ratings list entry by entry and make score optional, so a malformed entry no longer fails the whole item detail. - Expanded phone hero: ratings sit on one line, and the text block takes only the height the actions leave. - Phone request: no availability-label fallback when a TMDB rating shows. - Move the poster-badge label changes out of this PR. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(detail): let any TV rating drop first and wrap tablet ratings - WholeTokenRow now places "·" separators itself, only between the tokens it shows, so the TV facts row can drop a rating that comes first without leaving an orphaned divider. - The tablet and foldable hero wraps its ratings again; the text block is already clipped to the height the actions leave. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(detail): keep the ratings row visible on foldable and small tablet layouts Beside a portrait poster, the expanded detail hero gave its whole text block a fixed share of the height and clipped it, so at fold and small tablet widths (about 664dp) a tall title logo pushed the ratings row out of view. Only the title is weighted now: the facts and ratings rows are measured first, and the logo scales down to the height that is left. Play still lines up with the bottom of the poster. The server now sends at most three ratings, so the tablet row no longer wraps. Phone and tablet share one single-line row that drops whole entries from the end when space runs out, matching TV. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): let genres give way before ratings in the detail facts row When the TV detail facts row ran out of width, every rating dropped before the type and genre words, so Back to the Future lost RT and the Moon Knight series page kept only IMDb. The source tokens (type, genres, episode numbers) now give way first, then ratings from the end of the list, so IMDb stays longest. Other facts and any playback formats stay visible, as before. This matches tvOS. WholeTokenRow's dropFirst takes a rank so a row can drop one group of tokens before another. The foldable and small-tablet hero also gets a 12dp gap between the ratings row and Play. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(ratings): say every layout shows ratings on one line The PHONE_LIMIT doc still said tablet, fold and TV layouts show the whole ratings list. They now share the one-line row that drops whole entries from the end, and the server sends at most three ratings. PHONE_LIMIT stays as a guard against an older server that sends more. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo-Server#439) * feat(requests): make requests look and behave like the rest of the app Port the iOS and tvOS Requests redesign (silo-apple Silo-Server#558, Silo-Server#561, Silo-Server#540) to Android phone and Android TV. - Shared: the server's `state` decides display status; a four-step progress track; My Requests buckets; a session detail cache with prefetch; admin approve/decline/retry with a 60-second hold after a lost response; create, cancel and moderation are never resent. - Phone: large-title hub with a status summary, My Requests in grouped rows with a filter menu and swipe to cancel, a new Approvals page, and request detail on the library title hero. - TV: Requests becomes a top-bar tab in the Skyline layout, with a preview panel that shows status and progress, admin rows, and request detail on the library detail hero. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): address review findings on request state and routing - Clear request session state on every feature reset (sign-in, sign-out, server and profile switches) and drop reads or mutation broadcasts that answer after a reset. - Approval reads: a newer read wins, and a read sent before a decision can't bring the decided row back. - Unconfirmed create and cancel holds now end only when a read shows the result, or after 60 seconds, like admin decisions. - A proxy's 502/503/504 without the server's problem body counts as an uncertain outcome. - Request detail reuses a /requests/mine read from the last 30 seconds, re-reading only to settle an uncertain action; the hub counts pending approvals from one page; a page's own decision no longer triggers a second read. - TV long-press actions are computed by the caller, so the dialog no longer writes parent state during composition. - RequestRouter moves to android-shared and handles every request tap on phone and TV, including both search screens. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): address PR review findings - TV: each request detail opened from another one gets its own back-stack entry and view model, instead of reusing the current one. - TV: a failed action's message no longer replays when the page returns, and an approval queue that fails to load says so. - Request detail applies a create, cancel or decision result as soon as the server returns it, so a failed re-read can't bring back Request, Cancel or the decision buttons. - A failed or declined title the server calls requestable offers Request again instead of a blocking status. - A /requests/mine read that a create or cancel overtook keeps the patched list and doesn't count as fresh. - Phone: My Requests pull-to-refresh also refreshes the approvals card; grouped rows are separate lazy items; the row error text comes from the caller. - FEATURES.md no longer says the admin stats dashboard is exposed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…fline display (Silo-Server#432) * feat(downloads): save poster and series artwork with downloads for offline display Download rows loaded the catalog's signed poster URL, so offline they showed only the ThumbHash once Coil's cache no longer held that URL. Fetch the poster and series poster listed in the offline manifest with each new download, store them with its assets, and show them in the Downloads tab tiles. Episode rows use the series poster, never the 16:9 episode still. Closes Silo-Server#431 * fix(downloads): refuse redirects on offline artwork fetches The server streams artwork itself, so a redirect is never legitimate. Following one would fetch a URL the same-server check never vetted. * fix(downloads): refresh tiles when only the poster ThumbHash is saved The saved-artwork query skipped rows whose only artwork was posterThumbhash, so a movie whose poster fetch failed never triggered a Downloads reload and kept a blank tile.
The library sort entry labelled Year was named ReleaseDate and sent year, under a stale note that the server lacked release_date. Rename it to Year (still sends year, so saved sorts restore unchanged) and add a Release Date entry that sends release_date, newest first. Library browse and collection grids list both; favorites and watchlist keep Year only.
…lo-Server#437) * feat(detail): mark a whole season watched on Android phone and TV Phone: long-press a season chip, or use the series overflow menu, to mark the selected season watched or unwatched. TV: the More dialog adds the same entry while a season tab is selected. Both send the season id to the existing watched endpoint, update optimistically, roll back on failure, and re-read the season list, series hero, and visible episodes afterwards. Season and series writes now clear this device's older resume positions for the episodes they reached, and marking a whole series refreshes the episode list and season state on both apps. Closes Silo-Server#435 Closes Silo-Server#436 * fix(detail): read the server fresh after a season or series watched change After a watched write, the season list and episode refreshes now skip the cached fallback and coalesced requests that still hold the pre-write state (CatalogRepository fresh reads). Resume positions are also cleared for a season whose episodes were not loaded yet, and the TV rollback keeps episodes marked on their own while the season write was pending. * fix(detail): undo an episode's failed write in a failed season rollback When both a season write and an overlapping single-episode write fail, the episode's own rollback restores the season's optimistic state. The season rollback skipped that episode as changed since it began, leaving it marked. It now restores episodes whose latest individual write failed. * fix(detail): keep an episode's last successful write in a failed season rollback A failed latest episode write does not mean every episode write since the season write began failed. The season rollback now skips any episode with a successful individual write since then, which the episode's own rollback already left showing. * fix(detail): restore episodes with no newer write in a failed season rollback An episode write admitted before the season write but finishing after it changed the phone's success record, so the season rollback skipped an episode it should restore. Restore whenever no episode write was admitted after the season write began. * fix(detail): clear resume state and respect series writes in season watched changes Marking a season or series on phone now clears the loaded episodes' resume points along with setting played, as TV already did, so Resume does not offer the old position if the follow-up read fails. A season write that fails after a newer series write began re-reads the server instead of restoring its stale snapshot, on both apps. * fix(detail): tighten resume cleanup and overlapping series writes Resume cleanup after a season or series write now checks the write's identity generation, so a server, account, or profile switch in between cannot clear another profile's rows, and a series write also clears seasons that were not loaded. A season rollback treats a newer series write as superseding only if it has not failed, and a successful series write leaves episodes changed on their own since it began. * fix(detail): apply a series write to episodes whose newer write failed A successful series write skipped every episode with an individual write admitted since it began, including writes that failed and rolled back. It now skips only episodes whose newer write did not fail. * test(detail): cover a series write after an episode's failed write * fix(detail): keep an episode's successful write when a series write lands A successful series write now leaves an episode that had a successful individual write since the series write began, even if a later write to that episode failed, matching the season rollback rule. * fix(detail): compare episode successes by admission order on phone A late success from an episode write admitted before a series or season write counted as a newer change on phone. Compare the successful write's admission generation with the one captured when the parent write began, as TV already does with its watermark.
…ilo-Server#429) "Download series" ignored the batch endpoint's result, so a request the server rejected (such as the concurrent download limit) looked like nothing happened. Single downloads only reported success or failure. Both now report the start, and a rejected request shows the server's message in a toast, with a fallback when it has none. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilo-Server#438) Turning off "Card overlay badges" in the web settings stores the profile setting ui.card_overlays_enabled, which the Android apps never read, so badges kept showing on posters. The overlay store now reads it alongside ui.card_overlays and resolves "show badges" the way the web does: the profile's choice when it has one, otherwise the server-wide default from overlay-config. That default is no longer treated as a hard off, so a profile that turns badges on sees them too.
…TV (Silo-Server#430) * feat(settings): add the title art preference on Android phone and TV Read and write ui.title_art (settings manifest revision 16) through one app-wide store. When title art is off, detail heroes, the TV marquee and logo prefetch use the text title instead of logo art. Settings → Interface on the phone and General on TV get "Show title art" and "Apply to all devices", with a note when the profile-wide value is in force. Phone heroes put the title type and studio or network on one eyebrow line. Vendors the revision 16 manifest and conformance fixture. * fix(settings): pin one profile across a title art change Every request in a title art change (the PUT, the profile DELETE and the re-read) and every refresh read now carries one captured auth scope, the way the settings flusher pins its writes. A profile switch part-way through makes the remaining requests fail as identity_changed instead of changing the new profile's value. * fix(settings): drop title art changes queued behind a failed write When a title art write fails, changes queued behind it are dropped instead of sent with the failed change's unsaved scope, the store returns to the confirmed state and re-reads the server, and only server-confirmed values are cached. A refresh now captures its generation before reading the profile and auth scope, so a profile switch during those reads can no longer paint or cache the previous profile's value. * fix(settings): stop a stalled title art write blocking the next server Each server or profile now serializes its own title art writes. A write still on the wire for the previous identity keeps its own lock, and its completion is discarded by the generation check, so the new identity's saves go out at once. * fix(settings): show failed title art saves on phone and TV The store now exposes saveError, set when a title art write fails and cleared by the next successful save, a fresh read when Settings opens, or an identity change. Both settings surfaces show it under the switches, the same way the skip-interval controls show their save errors. * refactor(detail): keep the phone hero eyebrow and marquee layout unchanged Move the MOVIE · STUDIO eyebrow line and the marquee synopsis rule to their own change so this pull request covers only the title art setting. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): keep title art toggles working through cast sessions and cold starts - A remote-playback overlay moves the identity generation without changing the profile, so the write guard now compares server, credential epoch and profile instead of rejecting the toggle as a profile change. - The switches stay disabled until this session's read lands, so a toggle never picks its scope from a stale cached 'Apply to all devices'. - Concurrent hydrate and refresh calls share one load instead of each sending capabilities and effective-values requests. - Settings refreshes once when it opens, not each time the LazyColumn item scrolls back into view (which also cleared a shown save error). - Drop isSaving and lastError, which no screen read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(settings): gate title logos through one helper Logo surfaces call titleLogoUrl instead of each reading LocalShowTitleArt, and the local is no longer static, so the cold-start flip from the default recomposes only its readers instead of the whole app tree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(startup): stop TV first-row warming from waiting on title art The warm read the store's current answer (cached, else logos on) instead of awaiting two settings requests before any backdrop was fetched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(settings): share the hashed cache key between settings stores Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): keep title art saves working across cast overlays The write guard compared credential epochs between any two snapshots, but a remote-playback overlay's snapshot always carries epoch 0 while the saved account's carries its sign-in epoch. A read on one side of an overlay start or end therefore failed every toggle on the other side with "The acting account or profile changed", on the same profile. The epoch is now compared only between two saved-account snapshots, and server ids are matched the way the overlay handoff matches them. A load also took its cache key from separate profile and server reads and its request authority from a third read, so an overlay starting or ending between them could cache one profile's answer under another's key. The key now comes from the same snapshot the load is pinned to. --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e apps (Silo-Server#445) * feat(settings): lay out Android and Android TV Settings like the Apple apps Settings on the phone was one long scrolling form of web-style cards, and the TV settings had drifted from tvOS: mono section headers, an eyebrow above each pane, a narrow rail without descriptions, and different labels and grouping. Phone, following the iOS Settings (silo-apple Silo-Server#546): - The overview is an inset-grouped list on black: large title, account card (avatar, "username · host", Admin badge), search, then one-line rows with graphite icon tiles and the current value. Sections: Preferences, Playback, Library, Support, Connection, Experimental, About, Sign Out. - Interface, Playback, Subtitles, Downloads, and Notifications are pushed sub-pages (new settings/* routes sharing the overview's view model), with Apple's labels, group order, and footers. Subtitle appearance is edited inline in Text, Background, and Layout groups under a live preview. - Diagnostics follows the Apple page order with menu pickers. - Rows use Apple's grouped palette: #1C1C1E cells, #3A3A3C tiles, green switches, title-case headers, and popup pickers with a checkmark. TV, following tvOS Settings: - Black ground, a wider rail with tvOS rail descriptions and outlined glyphs, and a pane header with the category glyph on a graphite tile. - Sans section headers, tvOS-proportioned rows, a card-style option picker, and the tvOS confirmation card. Text keeps the 14sp TV floor. - Playback moves Audio Language into Streaming; Subtitles orders Language, Behavior, Forced, splits out Metadata, and says Use Device Settings; Server shows Address, App Version, and Privacy Policy. Android-only settings stay, placed in the matching group. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): keep group dividers right when rows change, and restore Diagnostics privacy link Row dividers were assigned by first-composition order, so a group whose first row changed while it stayed on screen (search filtering, a row swapping in place in Diagnostics) drew a hairline on its top edge. Every row now draws its hairline and the group paints over its own top edge. The phone Diagnostics page lost its Privacy Policy link in the restyle; it is back as the last Capture row. The subtitle preview parses colours with toColorInt, as lint asks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): preview the device caption style when Use Device Settings is on The phone subtitle preview drew the saved custom style even with Use Device Settings on, while playback and the TV preview use the device caption style. The preview now reads effectiveSubtitleAppearanceFlow; the Text, Background, and Layout groups still edit the saved style. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): keep Settings rail text on one line at large font sizes Rail rows are a fixed 50dp. At font scale 1.3, "Language and appearance" wrapped and its second line was cut off mid-glyph. Title and description now ellipsize on one line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…V sign-in (Silo-Server#448) * feat(auth): sign in with the server's identity provider and the new TV sign-in The phone signs in through the server's OIDC provider in a Custom Tab, offers "Not you? Switch account", and connects or disconnects providers in Settings. The TV shows an 8-digit code with a QR, follows opened and canceled states, and hides the password form on provider-only servers; phones approve its code. Every sign-out now runs one shared teardown, which also clears title art. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): bind provider flows to the saved login and fix two TV approval gaps A provider sign-in or link left open in the browser now records the saved server's durable login id when it starts. The redirect is redeemed only while that login is unchanged, so a password sign-in or sign-out in between supersedes the flow, and a link still finishes after Android recreates the process. Password sign-in and sign-out also discard the pending flow. A silo://device?token= link can retry its lookup with Continue, and the nearby TV approval card names the account it signs the TV in as. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(pairing): fail fast on TV refusals and stop stale sign-in work The phone now reports a TV's failed result that arrives before any code (a different server at that address, an expired code) instead of waiting out the 30-90 second timeout. A pairing connection that ended after a newer advertiser start no longer touches that start's listener or busy flag. The nearby TV card goes away and its pairing is cancelled when the route hides the offer. In Sign-in settings, Cancel stops a link ticket request still under way, and identities are shown only while the same login is signed in. A TV approval whose sign-in context changed no longer cancels a password attempt made since. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): keep TV approvals on the server and account they started with The TV polls a resumed code only while the sign-in context it was started under is unchanged, and withdraws an expired code that failed to poll before asking for a new one. On the phone, a code lookup goes only to the server chosen when it started, a token link always uses the active server, an unknown lookup status offers no decision, and nearby approval stops when the card can't name the account. Provider discovery for a URL no longer depends on the active server's update verdict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): drop results from superseded sign-in flows and stale pairing runs A provider redemption now publishes its outcome only while its flow is still the latest: starting a new flow, or a sign-in or sign-out discarding the pending one, silences it, and discarding also clears an unshown result. On the TV, a pairing run that ends after a newer connection took over the receiver leaves that connection's push, session and status alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): don't spend a pinned bearer after its scope is gone When a fresh-auth request's refresh fails and its scope was signed out or replaced meanwhile, the request now fails instead of going out with the bearer captured before. On the TV, an advertiser startup superseded by a later start or stop closes its socket instead of publishing it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): let a nearby phone hear the sign-in result before leaving When a nearby phone approved the code, the sign-in screen now waits up to five seconds for that session to finish its result and done exchange before routing on, so the phone doesn't report a failed pairing for a TV that signed in. A completed session routes on after its own dwell as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): keep the nearby wait from cancelling its own fallback route Consuming the sign-in success flag changes the effect's key, so doing it before the wait cancelled the wait and its fallback route. It is consumed after the wait now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): show a link's result only on the server it was made for Sign-in settings now ignores a provider link result for another saved server, so switching servers before opening Settings doesn't show one server's "Connected" or failure on the other. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): recheck the server before saying a link connected The "Connected" message after a provider link is shown only if the link's server is still the active one once the section has reloaded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(pairing): keep stale attempts out of the connection that replaced them A TV pairing attempt that outlived its connection (its session save can't be cancelled) no longer reports its result into the newer connection. The "Connected" message after a provider link also requires the same login, not only the same server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(pairing): hide the nearby TV offer over audiobooks and the reader The nearby-TV card and its discovery are now off on the audiobook player and book reader routes too, not only the video player and cast remote. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): name the provider in "Use a different account" The link under the provider buttons picks another account at the identity provider, not another way into Silo, which read wrong on a server with password sign-in off. With one provider it now says "Use a different <provider> account"; with several it stays generic and opens the chooser. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Member PRs and default-branch builds ran on the private Linux runners. Route them to GitHub-hosted runners while the private runner setup is rebuilt; the reusable workflow already supports the hosted path, which non-member PRs use today. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r#454) * feat(auth): sign in with network identity on phone and TV When the server lists a network provider (such as the Tailscale plugin) for this device, phone and TV offer "Continue as <owner>" above the other ways in. One press posts {} to the listed network_sign_in_path on the saved server base and saves the returned token pair exactly as a password sign-in does. The TV button takes initial focus; the device code, QR and password form stay available. Refusals (network_identity_required, not_permitted, email_in_use and the shared ones) have readable text that names the provider. Phone Settings > Sign-in offers "Connect <provider>" for a listed network provider when getExternalSignInCapabilities reports network_sign_in, asking only for the account password (linkAccountIdentityWithNetwork). Vendors only the network sign-in contract fixtures and the updated capabilities fixture, and adds them to the sync script's selection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): free the network button after a device approval and offer it in the TV password form A phone approval that cancelled a pending "Continue as" sign-in left the network button busy when saving the approval failed, which also blocked the password form. The device handoff now frees both buttons when it ends. On servers without device sign-in, the TV opens on the password form, which had no "Continue as" button. The form now shows it above the fields and focuses it, including when the providers answer before the device check. The two-line button uses less vertical padding so the form still fits a 960x540dp screen. The TV's email_in_use message for network sign-in no longer points to Settings, which the TV does not have. It points to the phone or web app. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): stop a cancelled link and keep busy network sign-ins blocking Cancel in the Settings directory and network prompts now stops the link call still under way, like the browser prompt already did, and reloads the section so a link the server made first still shows. Before, the link kept running and could report "Connected" after the prompt was gone. On the phone, "Continue as" keeps its own busy flag. Reloading the sign-in options no longer frees the password form and provider buttons while the request is still out. On the TV, "Sign in with a password" waits while "Continue as" is signing in, so the form can't hide the "Signing in…" label. Once "Continue as" is refused, the device code's recovery action ("Try again", "Show a new code") takes focus again. Cleanups: the phone's network refusals in Settings map through NetworkSignInFailure and share their copy with the sign-in screen; the TV error text takes one provider name for the attempt; the network sign-in call trusts the path discovery already validated; and the unused OAuthHandshakeCapabilities.networkProvider is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): keep a late Sign-in reload from undoing a newer one Cancelling a link prompt reloads the Sign-in section, and so does every link that succeeds. If the cancelled prompt's reload answered after the next link's reload, it showed the identities from before that link. Each reload now takes a generation, and only the newest publishes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(contracts): re-vendor the API v2 fixtures from the network sign-in server commit SOURCE named the earlier OIDC server commit, which doesn't contain the network sign-in fixtures. A full re-sync from the server branch that adds them reproduces every vendored body; the only other change is the system info fixture's contract digest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): disable the TV password form while Continue as signs in On a server without device sign-in, the password form shows "Continue as" above the fields. The view model already ignored a password submission while the network sign-in waited, but the fields, the IME Done action and Sign in stayed enabled, so Sign in looked live and did nothing. They now follow one signingIn state (password or network busy) that the view model's guards also use. Focus stays on the waiting "Continue as", and the D-pad steps between it and the secondary actions past the disabled controls. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(auth): disable the phone password form during any sign-in The phone view model ignores a password submission while any sign-in is under way (signInBusy: password, browser provider start, or Continue as). The Sign in button only followed the network wait, though, and the fields, the show-password toggle and the IME Go action never did, so they looked live and did nothing. All of them now follow signInBusy, as the TV form does. AuroraTextField gains an enabled flag that dims its text when off. 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#380) * fix(tv): play phone-launched titles with sequenced playback Sequenced playback required a saved login, so a title a phone launched on the TV under its temporary remote-playback profile failed to start with identity_unavailable. Attempts admitted under a temporary identity now run in memory only: their credentials die with the process, so they stay out of the durable journal and recovery, and only the identity that started an attempt may act for it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): settle a phone's uncertain start before its next title The start fence compared pending attempts with the saved login's marker, so a start sent under a temporary remote-playback identity whose reply was lost was never settled: the next title started beside an orphaned server session. The fence now uses the current identity's marker, and a temporary identity settles its own uncertain attempts (replay the start, then stop) with its own scope before admitting another. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): drop an ended remote-playback identity's attempts A temporary attempt whose identity ended before its stop was confirmed never settled, so compaction never removed it: each interrupted handoff kept its session and history for the life of the TV process, and owns() kept answering for it. Saving now drops temporary attempts that don't belong to the current identity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): forget an ended remote-playback identity's captured scope Dropping the attempt from the journal left its captured scope, adoption and subtitle headers (which hold the bearer header) in memory. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): don't restore the scope of an attempt pruned while saving Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): report an identity change when a phone's progress retry is pruned If the phone's remote-playback identity ended while a retried progress sample was in flight, saving the receipt dropped the attempt and the follow-up lookup threw, so the player saw playback_storage instead of identity_changed. Look the attempt up nullably and answer identity_changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Silo-Server#459) * feat(onboarding): redesign the first-run flow on Android phone and TV Port of the Apple first-run redesign (Silo-Server/silo-apple#586): one brand-light backdrop behind server setup, sign-in, profiles and PIN on both apps, tinted to the server's accent and to the selected person. - Phone: address field with Advanced options and Recent servers, a "Sign in" screen with the server chip and branded subtitle, round profile avatars sized to the household, full-screen PIN keypad. - TV: phone-first setup with three picture steps, a separate address screen, sign-in with the code card on the right, restyled pairing, picker and PIN. - A saved server that still has a session goes straight to profiles. - After a sign-in, a one-profile household with no PIN skips the picker. - Branding (subtitle, accent, mark) is read from the public branding endpoint; asset URLs are accepted only on the server's own origin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): keep the server address fixed while it is probed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er#444) * fix(downloads): refresh saved subtitles the server retimed A video download saves its subtitle sidecars once, when it completes. When the server later syncs or resets a stored subtitle's timing, the download's manifest lists that subtitle at a new revision, but the saved copy kept the old timing, so offline playback stayed out of sync. Saved sidecars now record the manifest fetch_url and revision. A periodic worker (every 12 hours with a network) re-reads the manifest of each completed download of the active login and profile, re-fetches only the sidecars whose revision changed, replaces each file in place, and records the new revision. Captures saved before this change carry no fetch_url and are left as they are. Refs Silo-Server#442 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): publish refreshed subtitles only while the download is unchanged The refresh worker wrote each re-fetched sidecar over the saved file before it checked that the download was still the one it read. A download replaced or deleted meanwhile could have a fresh capture's subtitle overwritten with the old download's bytes, leaving its recorded revision wrong. Stage the re-fetched sidecars beside the saved files instead. The worker publishes them inside its generation and ownership check, and only over a saved copy that is unchanged since staging; whatever is not published is deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): hold one lock per download while subtitles are captured, refreshed, or deleted A subtitle refresh checked the download's row and its saved file, then replaced the file and rewrote the row in later steps. A track capture for the same download could write new sidecars in between, and the refresh would move the old subtitle's bytes over them. A delete landing between the refresh's read and write let the refresh write the deleted row back. The capture, the refresh, and both delete paths now hold a process-wide lock per (server, profile, file) across their check and writes. Each takes it before the identity barrier or a database transaction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): record refreshed subtitle revisions only while the row is unchanged The subtitle refresh checked the download's row and wrote its new revisions in separate statements. Re-downloading the same file writes the replacement row without the identity barrier or the slot lock, so a replacement landing between them was overwritten with the old download. The check, the file publish, and the row write now run in one Room transaction (DownloadMetadataStore.updateSidecarIf), like completePendingDeletion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o-Server#464) * feat(playback): play embedded MKV text subtitles from the stream Advertise native embedded subtitles for Matroska SubRip and ASS tracks on original-HTTP delivery, identified by container_track_id. Media3's MatroskaExtractor sets Format.id to the Matroska TrackNumber, which the server records as container_track_id, so the existing exact-ID selection works unchanged. The MKV entry reports libass styling and font-attachment support so the planner keeps ASS on the sidecar path when fidelity would be lost. Closes Silo-Server#457 * docs(playback): note MKV stream-path costs and tidy capability entry Import NativeEmbeddedSubtitleCapability, drop the test that duplicated existing exact-ID coverage, document the legacy S_ASS/S_SSA and seek costs, and stop describing the open server PR as shipped. * docs(playback): note that legacy S_ASS/S_SSA tracks stay on sidecars * fix(playback): accept native MKV subtitle plans in the Media3 validator validateForMedia3 still required mov_text in an MP4/MOV/M4V source, so a native embedded plan for an MKV SubRip or ASS track was rejected with subtitle_embedded_failed and replanned to a sidecar. Accept the pairs the client advertises and keep rejecting the rest. * refactor(playback): define native embedded subtitle pairs once The capability detector and validateForMedia3 each hardcoded the container/codec pairs Android selects natively, and their drift is what rejected MKV plans. Both now read NATIVE_EMBEDDED_SUBTITLE_CODECS. The validator also accepts the srt and tx3g aliases the server normalizes and drops the mov/m4v containers the server never reports. * docs(playback): state the container track ID assumption for native pairs
…late (Silo-Server#465) * fix(tv): offer the full subtitle-language list in search and AI translate Android TV kept its own 28-language list, so a Croatian (hr) preference fell back to English in the AI translation picker. Move LanguageNames to android-shared and derive the TV pickers from it, matching the phone app. Fixes Silo-Server#271 * fix(tv): match audio and target languages across 2- and 3-letter codes
* fix(playback): stop claiming m2ts/mts direct play Media3's TsExtractor reads only 188-byte transport-stream packets. Blu-ray and AVCHD .m2ts/.mts files use 192-byte BDAV packets, so no Media3 extractor recognizes them and direct play fails with ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED before recovering through a failure replan to an HLS remux. Drop m2ts and mts from the advertised direct-play containers. The same list feeds the top-level client capabilities, the original_http delivery capability and the download capabilities, so the server now remuxes these files from the start and a download no longer fetches original bytes the player cannot open. ts, mpegts and mpeg-ts stay. The video/mp2t MIME mapping for m2ts/mts in SiloPlayerFactory stays: it is only a hint used when the server still serves original bytes (for example an older server that ignores the capability list), it is the correct MIME type for the format, and it does not change the outcome. Related to Silo-Server#449 * docs(playback): scope the m2ts note to what the server remuxes The container policy comment said dropping m2ts/mts keeps every download from fetching the original bytes. Whole-series downloads at Original quality still do, because the server does not check device capabilities for that batch. Say so, and split the M2TS row in the playback validation gates so M2TS/MTS is expected to use server adaptation rather than direct play.
…lo-Server#443) When the server syncs or resets a stored subtitle's timing, it sends subtitle_timing_changed to the file's playback sessions and serves the new timing from the same sidecar URL. Media3 keeps the cues it already parsed, so a subtitle selected before its sync finished stayed off by the old offset until playback restarted. Handle the event on phone and TV: when the retimed subtitle is the mounted sidecar, remount the same item at the same position so the cues are fetched again. Stored subtitles are matched by download ID or by the downloaded_subtitle_id the server pins on a v3 sidecar URL. Refs Silo-Server#442 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ubtitles (Silo-Server#446) * fix(player): reload a subtitle the server retimed during playback When the server syncs or resets a stored subtitle's timing, it sends subtitle_timing_changed to the file's playback sessions and serves the new timing from the same sidecar URL. Media3 keeps the cues it already parsed, so a subtitle selected before its sync finished stayed off by the old offset until playback restarted. Handle the event on phone and TV: when the retimed subtitle is the mounted sidecar, remount the same item at the same position so the cues are fetched again. Stored subtitles are matched by download ID or by the downloaded_subtitle_id the server pins on a v3 sidecar URL. Refs Silo-Server#442 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(subtitles): show sync status and sync/reset actions for stored subtitles silo-server#1783 aligns stored subtitles to the file's audio and lets the account that added a subtitle, or an admin, start a sync or reset its timing. The Android players showed none of it. Phone and TV now read each stored subtitle's timing and latest sync job and show it in the web player's words ("Syncing…", "Synced −3.2 s", "Synced −23.7 s · ×1.0008 speed", "Already in sync", "Doesn't match this video", "Sync failed"). For the selected stored subtitle they offer "Sync subtitle" and "Reset timing": in the phone's Audio & Subtitles sheet, and as a Timing row in the TV HUD's Subtitles pane. A running job is polled until it ends. A refusal (403) replaces the actions with a short explanation; unsupported formats (422) and stale versions (412) say so. A shared StoredSubtitleSyncController holds the state for both apps; SubtitleSyncV2Api covers the capability probe, sync, sync status, and the If-Match timing reset. The TV picker no longer marks an option selected when none is current, so an action menu shows no checkmark. Refs Silo-Server#442 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(subtitles): sync subtitle files next to the media, with live progress, on phone and TV (Silo-Server#463) * fix(downloads): refresh saved subtitles the server retimed A video download saves its subtitle sidecars once, when it completes. When the server later syncs or resets a stored subtitle's timing, the download's manifest lists that subtitle at a new revision, but the saved copy kept the old timing, so offline playback stayed out of sync. Saved sidecars now record the manifest fetch_url and revision. A periodic worker (every 12 hours with a network) re-reads the manifest of each completed download of the active login and profile, re-fetches only the sidecars whose revision changed, replaces each file in place, and records the new revision. Captures saved before this change carry no fetch_url and are left as they are. Refs Silo-Server#442 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(subtitles): sync subtitle files next to the media, with live progress, on phone and TV The server now syncs any playable subtitle to the file's audio, stored or a sidecar, keyed by the inventory's sync_key, and reports each step over the playback socket. Follow that on phone and TV: - Shared: sync API by sync key (list, read, start, set timing with If-Match), sync_key on inventory rows, subtitle_sync_updated and keyed subtitle_timing_changed decoding, SubtitleSyncController (realtime with a 3 s polling fallback, jobs this viewer started, one cue reload per timing change), and a port of the web player's feedback state machine. - Phone: per-track status lines, a Timing section with progress, phase, result, reset and the sidecar note, and a non-blocking status card. - TV: status lines in the track picker, a Timing row with progress and result, and the same card while the HUD is closed. The HUD sync state is built in the view model to keep TvPlayerScreen within ART's JIT limit. - Offline: saved sidecars refresh on a new manifest revision too, but not when their position now names another file. - Player: a sidecar remount right after a track switch no longer crashes in NonGatingSidecarPeriod (continueLoading without a pending reset). Refs Silo-Server#442 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(subtitles): name merged stored subtitles for sync Rows merged from the stored-subtitle list (sessions without an inventory, the phone's fresh-subtitle restore, and TV refreshes) carried no sync_key, so they lost the stored-subtitle status and timing actions. Name each one stored-{id}, which the server treats as equivalent, for the formats it can retime. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv): show sync status for merged stored subtitles in the track picker A row merged from the stored-subtitle list maps to a Downloaded identity, which the HUD's status lookup ignored, so the picker showed no status for it. Look those rows up by stored subtitle ID. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(subtitles): follow the selected track and wait for its cues in sync feedback - The track on screen is the selected subtitle when the player has it mounted. A legacy session mounts every row, and taking the first mounted key could point the feedback at the wrong track. - A mount's cue revisions count as loaded once Media3 reports a subtitle track selected on the live player, not as soon as the mount returns, so "Subtitles synced" waits for the reloaded track. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): refresh a positional sidecar only while its row still describes it An external:{index} ref names a sidecar by its position, which can come to name another file when sidecars are added or removed. Refresh such a row only while it lists the language, title, format, and forced and SDH flags the saved file was captured with. Stored downloaded:{id} refs are stable and skip the check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): publish refreshed subtitles only while the download is unchanged The refresh worker wrote each re-fetched sidecar over the saved file before it checked that the download was still the one it read. A download replaced or deleted meanwhile could have a fresh capture's subtitle overwritten with the old download's bytes, leaving its recorded revision wrong. Stage the re-fetched sidecars beside the saved files instead. The worker publishes them inside its generation and ownership check, and only over a saved copy that is unchanged since staging; whatever is not published is deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(subtitles): keep the newer sync state when responses and updates cross The server doesn't sequence its HTTP responses and realtime updates. A start response that left before a fast job finished could arrive after the outcome and rewind it to "pending": the status went back to Syncing, the track reloaded twice, and the card could stay on its progress. Ignore a snapshot of an older job, or of the same job further back, while still following the job if this viewer started it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): hold one lock per download while subtitles are captured, refreshed, or deleted A subtitle refresh checked the download's row and its saved file, then replaced the file and rewrote the row in later steps. A track capture for the same download could write new sidecars in between, and the refresh would move the old subtitle's bytes over them. A delete landing between the refresh's read and write let the refresh write the deleted row back. The capture, the refresh, and both delete paths now hold a process-wide lock per (server, profile, file) across their check and writes. Each takes it before the identity barrier or a database transaction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(downloads): record refreshed subtitle revisions only while the row is unchanged The subtitle refresh checked the download's row and wrote its new revisions in separate statements. Re-downloading the same file writes the replacement row without the identity barrier or the slot lock, so a replacement landing between them was overwritten with the old download. The check, the file publish, and the row write now run in one Room transaction (DownloadMetadataStore.updateSidecarIf), like completePendingDeletion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(subtitles): ignore a read sent before the subtitle state already shown Only job progress decided whether a response was stale, so a response with the same job, or none, always replaced the entry. Two quick timing changes, such as an adjustment and then a reset, start two reads; if the first one answered last, its old timing came back and the track reloaded again. The controller now numbers each read as it is sent and each realtime update as it arrives. A response from a read sent before the view on screen is ignored, unless it reports a job that view has not reached: the server can only have produced that later, and it may be a job this viewer just started. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o-Server#379) * fix(cast): keep the TV reachable after a TLS 1.2 client connects SiloCastTls handed BouncyCastle one shared PSK array. BouncyCastle zeroes the PSK buffer after a TLS 1.2 handshake, so the first TLS 1.2 client wiped the key for the rest of the process, and every later handshake (TLS 1.3 included) failed with bad_record_mac until the TV app restarted. The key and identity are now fresh arrays on every use. A regression test connects two TLS 1.2-only clients and then an ordinary client; it fails without the fix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cast): wait for the TLS test's server thread before cleanup The regression test iterated the server sessions while the worker thread could still be adding the last one. It now joins the worker (bounded) and reads a synchronized list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-Server#381) Android and Apple peers disagreed on parts of the Remote Control wire format: - A frame with a message type this build doesn't know failed to decode, and the decode error tore the session down. Unknown types are now skipped; the Apple apps get the same change. A known type with a broken payload still fails. - The TV answered a command name it doesn't implement with player_not_ready. It now ignores it without an error, as tvOS does. - The TV sent its aspect ratio as fit/zoom/stretch and the phone sent the subtitle position "standard"; tvOS uses fit/fill/stretch and "bottom" and rejects anything else. Episode subtitles now read "Series · S1 · E2". - A discovered TV without a `v` TXT key was treated as protocol v2; like iOS, it is now treated as v1. - Apple removed the HDR toggle. The phone remote drops it too, and the TV advertises supportsHDRToggle = false (the key stays on the wire because older remotes require it) and ignores set_hdr_enabled from older phones. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stop (Silo-Server#481) * fix(tv): end the remote-playback identity after the player's session stop Backgrounding the TV app during phone-handed-off playback ended the temporary identity while the player's queued final progress report and stopSession were still in flight, so stopSession failed with 401. The player's ON_STOP observer queues its teardown before MainTvActivity.onStop() calls TvSiloCastReceiver.stop(). stop() now waits (up to 15 s) for that queued teardown before ending the identity, so an unconsumed identity is still always ended. The generation check moves inside RemotePlaybackIdentityManager's lock so a delayed cleanup cannot end a replacement identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep the deferred stop cleanup off a reused handoff identity stop() now waits up to 15 s for the player's teardown before ending the temporary identity. If the TV returns and the same phone hands off again in that window, prepare() reuses the identity with the same generation, so the generation guard alone let the old cleanup revoke it. A reuse now bumps a claim count on the active identity, and the stop cleanup ends it only if it is still exactly the identity it captured. An offer that fails before claiming the identity leaves it to the cleanup, so the identity is still always ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(tv): cover the remote-playback identity claim guard Covers the three outcomes of stop()'s deferred identity cleanup: a same-phone handoff that reuses the identity blocks it, an offer that fails before claiming the identity still lets it end, and a replacement identity is left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): tie the stop cleanup's claim check to the receiver run The claim-count snapshot could still leak the identity: a handoff from the session stop() was tearing down could finish its reuse after stop() took the snapshot. The cleanup then skipped, and with the receiver scope gone nothing else ended it. Each start() now begins a new receiver run, and a handoff tags the identity with the run that installed or reused it. stop()'s cleanup ends the active identity unless a later run has claimed it, so claims from the stopped run never protect it. The cleanup is now always scheduled, since a stopped-run handoff can install an identity after stop(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mental toggle (Silo-Server#377) * docs(watch-party): plan the Android Watch Party against the current contract The September 21 draft predated the server's buffering and catch-up policy and Apple's merged client. This plan follows server main 2812ac39, the web reference client, and Apple main 5d9dc764, and lists every Android input and session-replacement path the room must own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): adopt the current room contract in the shared room owner The shared Watch Party layer followed an older server contract. Room proof never renewed, the same-room rejoin reused an obsolete socket, connection replacement was treated as a transport drop and reclaimed the membership, suggestion hydration ran on the socket receive path, and events could be dropped without notice. The API now covers every v2 room operation with status and body validation, caller-selected ids kept by the logical action, and a single attempt for the non-retryable start, select, and promote. Host operations authorize by identity; member operations send the room proof, which is adopted from every successful room response. WatchTogetherRepository owns one engagement per membership: it renews the proof before expiry, orders HTTP snapshots against socket receipts so a delayed response cannot erase a newer attachment, treats ended snapshots, room_closed, and connection_replaced as terminal, serializes room and vote actions and reconciles uncertain outcomes by reading the room, drains suggestion pages off the socket path, and samples the server clock on the room socket from the moment it opens. A socket that stayed up reconnects at once after the server's five-minute rotation; quick failures keep retrying through the host's two-minute grace. RoomSession keys connections by membership generation, so an explicit rejoin replaces the old socket. The player controllers and entry ViewModels are patched to the new API only; the player binding and shared lobby are rebuilt in later commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): cache availability and remember the recent party Entry points need to know whether this server and profile can use Watch Party, and a user who is displaced or loses the connection needs a way back into the party. WatchPartyAvailabilityRepository reads the room and playback capability documents, caches the answer for the captured authority, and never lets a failed probe downgrade a known answer. RecentWatchParties keeps one room code per device in the encrypted preferences for 24 hours, shown only to the server, login, and profile that saved it; no room or join token is stored. The recorder keeps it after a replacement or lost connection, forgets it when the room ended, and clears it before sign-out, account replacement, or server removal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): add the shared room playback binding The phone and TV room controllers each carried their own copy of the sync mechanics, reported the intended rather than the actual player state, never re-armed readiness for a second seek on the same session, and had no support for the server's buffering and catch-up policy. RoomPlaybackBinding runs one event loop per playback epoch against a small player port. It attaches the committed session on every socket epoch and waits for the server's echo, reports observed state every 1.5 s and stays quiet while stalled, seeking, or suspended, schedules commands on the server clock and advances a late Play, acknowledges the exact waiting command until a fresh snapshot confirms it, reports buffering after a 2 s stall, acknowledges recovery while catching up, and converges small drift by a bounded temporary rate. Media reloads for Play corrections are paced by RoomReloadBudget; explicit seeks are never delayed. The catch-up rules are ported from the web client. The repository implements the binding's room interface. The platform adapters follow in later commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): start room playback on the room's exact file and position A room start could fall back to another version when the room's file was missing, resume from the viewer's own progress when no position was passed, and let a replan move the viewer onto a different file. Room starts now carry a WatchPartyPlaybackContext. Both starters play exactly the room's file or fail, start at the room position (an explicit zero included), never apply personal resume or the resume rewind, send allow_alternate_versions: false so every replan keeps the file, and refuse a plan for a different file. StartParams keeps the flag so a 404 renewal pins the same file. Solo starts omit the field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): add the player-side Watch Party coordinator Both players need the same wiring around the room binding: the room's playback context, status, transport requests, reconciliation of seeks made outside the app, leave and end, and coordinated source fallback for the four approved refusal reasons. WatchPartyPlayback provides it so each platform keeps only a thin adapter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): share hub, lobby, and picker state between phone and TV The phone and TV each had their own entry, lobby, and suggestion ViewModels. They opened the player whenever a title was selected, held a lone host in the lobby, created vote rooms from the menu, minted new ids on every retry, and normalized codes differently. Shared ViewModels now back both platforms. The hub probes availability, hosts Host Picks rooms (staging a title without starting it), keeps a create's room id for an explicit retry after an uncertain outcome, joins by normalized code or invitation link (never sending a token to another server), and offers Return to Party and Rejoin. The lobby exposes eligibility computed from phase, role, capabilities, and pending work, advisory lobby Ready, suggestions with Queue (stage, never promote), and opens the player only when the room plays. The picker loads the server's shared rows and debounces search. Invitation parsing accepts codes, web links with a base path, and the silo://watch-party app link proposed in silo-apple#412. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): add the device-local Experimental toggle Watch Party ships off in release builds and on in debug builds, like the Apple apps. WatchPartyExperiment keeps the choice on the device, never syncs it, and leaves any active party when it is turned off. Both apps inject it as WatchPartyExposure; the entry points switch to it with the new party screens. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(watch-party): expose party state to the playback harness Device runs need to correlate each client's room, socket, commands, and catch-up state and to measure the spread between two players. The debug receiver now reports sampling timestamps and a party object (room phase, revision, connection epoch, attached and applied commands, clock offset, correction rate) with no tokens or tickets, and the script gains party and spread commands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): never let the HTTP engine resend start, select, or promote A new test shows OkHttp silently resending a Watch Party start when a reused connection drops after the request was sent. These operations are non-retryable on the server: a replay after another selection could start the wrong item. createPlatformHttpClient and createSiloClient take a retryOnConnectionFailure switch, and the Watch Party API sends start, select, and promote on a client with it off. The repository already reconciles such uncertain outcomes by reading the room. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): drive the phone player from the room binding RoomSyncController reported intended state, never re-armed readiness, played downloaded copies, and let local controls and system media controls fork the viewer from the room. The phone player now uses WatchPartyPlayback. Room loads play exactly the room's file at the room position, start paused, skip downloads, and reuse the room context on every restart; version switching and On Deck are refused. The ViewModel reports the true engine position mapped to the source timeline and applies room seeks, play state, and a temporary correction rate through paths that are never user intent. Every user and system transport input goes through the room; seeks the screen did not issue are sent to the room or undone. Audio focus loss, a noisy route, the sleep timer, and backgrounding are local suspensions. Cast and automatic PiP are off in a room, intro skip is pinned to Ask with no pill for members who cannot seek, and approved refusals ask the room for a coordinated fallback. Start refusal reasons now reach the ViewModel. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): hold room commands during a mount and while suspended A room Play reached a locally suspended player, and a command that landed while media was still mounting acted on media being replaced. The binding now leaves a suspended player paused (resuming re-attaches, and the server resends the room position) and holds a command for up to five seconds while a mount or seek is in flight; a late Play still advances by the full elapsed server time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): drive the TV player from the room binding TvRoomSyncController duplicated the phone controller's defects, left the room when the TV app paused or stopped, and suppressed all SiloCast transport in rooms, hosts included. The TV player now uses WatchPartyPlayback. It loads exactly the room's file at the room position, starts paused, and reuses the room context on every restart; version switching, the Dolby Vision restart, speed writes, and next-up are refused. The ViewModel reports what the player is really doing and applies room seeks, play state, and a temporary correction rate through paths that are never user intent. Remote keys, the scrubber, skips, chapters, the intro pill, MediaSession, admin commands, and SiloCast transport go through the room's permission decision; SiloCast launches are refused while a party plays. ON_PAUSE/ON_STOP, audio focus, and the sleep timer are local holds instead of leaving the room. Approved refusals ask the room for a coordinated fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): refuse SiloCast handoffs while the TV is in a party A handoff swaps in the phone's profile, and any identity change leaves the Watch Party the TV is in. The receiver now cancels the offer with watch_party_active and asks the viewer to leave the party first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): phone hub, lobby, picker, party panel, and invitations Replace the phone's Watch Together screens with the Watch Party experience on the shared ViewModels: - Hub (profile menu, detail action, invitation): availability, Return to Party, Rejoin, host, and join by code or link. - Lobby: staged title, members and lobby Ready, Start, mode and guest policy, invitation, suggestions and votes; only the room phase opens the player. - Picker sheet for staging and suggesting. - Party panel over the retained player (Back): members, invite, Leave, Return everyone to lobby, End for everyone (D2). - In-playback status: waiting for, catching up, continuing without, host away, and a reconnect notice after two seconds. - Detail pages: one party action (Watch Party, Add to party, Play for everyone, Suggest to Party) and the D5 leave-first guard in front of solo playback, home quick play, and external player links. - silo://watch-party invitations, accepted only while the Experimental toggle is on; the join token stays in memory and routes carry an opaque id. - Settings → Experimental → Watch Party. - A restored party route without a live membership opens the hub. Entry points follow WatchPartyExposure. The old watchtogether package and its tests are removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): TV hub, lobby, picker, party panel, and settings toggle Replace the TV's Watch Together dialogs and lobby with the Watch Party experience on the shared ViewModels: - Hub from the profile menu: availability states with a focusable control, Return to Party, Rejoin, Host (Host Picks), Join with code, and why the last party ended. - Lobby: staged title and edition, members and lobby Ready, Start, options, Leave with the host note, the code for everyone and a QR for the host, and suggestions. Controls are disabled rather than removed so live updates keep focus. - Picker with server rows and search. - Detail page: Watch Party, Add to party, Play for everyone (confirmed), or Suggest to Party, and a leave-first prompt in front of solo Play and extras (D5). A launcher play link during a party opens the title page instead. - Back in party playback opens a party panel over the running player (members, code, Back to playback, Return everyone to lobby, End, Leave) and a status overlay that never takes focus. - Settings → General → Experimental → Watch Party. - Party screens already on the back stack are reused, and a restored party route without a live membership opens the hub. The old watchtogether package and its tests are removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(watch-party): report why the last room socket ended in the debug status Live qualification showed room sockets ending after 17-40 s on one emulator with nothing in the server log to explain it. The repository now records each socket attempt's end (terminal frame, replacement, or the transport exception class and message) with how long it was open, and the debug receiver's party status includes it as lastSocketEnd. It carries no tickets or tokens. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): issues found in live phone and TV qualification - Ending a party for everyone left a Rejoin for a room that no longer exists. The repository now announces rooms this device closed, and the recent-party recorder forgets them. Leave still keeps Rejoin, which the host Leave dialog promises. - Joining an ended room showed the generic conflict text. A join 409 now says the party has ended. - TV: the confirm dialog meant to focus Cancel, but the popup handed focus to its first row (End party). Focus now moves to the intended row through the observed-focus helper. - TV: a guest's on-screen Pause did nothing and said nothing. It now goes to the room like the remote's play/pause key, so a refusal shows its notice. - TV: the join-code keypad was translucent over the hub's text. It now has a scrim and a near-opaque panel, like the party's other dialogs. - Phone: member statuses no longer shift with name length. - Phone: finding an existing party player uses the public back-stack API instead of the restricted currentBackStack. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(watch-party): report the server's raw refusal reason on phone The phone told the room why playback was refused from diagnosticsCode, a telemetry allow-list, while the TV used the terminal reason exactly as the server sent it. The mobile starter now records terminalReason too, and the phone reports it, so both platforms send the same value. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(watch-party): remove the Watch Together entry leftovers Both platforms now use the shared Watch Party ViewModels and the Experimental toggle, so these have no callers: - CLIENT_WATCH_TOGETHER_SURFACE_ENABLED and its test (WatchPartyExposure replaces it; WatchPartyExperimentTest covers the per-build default). - WatchTogetherEntryPolicy and its test (content-present entry rules). - The WatchTogetherEntryGateway interface and its DI binding. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: describe Watch Party as experimental behind the Settings toggle D1 exposes Watch Party on phone and TV behind Settings → Experimental → Watch Party (on in debug builds, off in release builds) and the server's capabilities. Update the product-exposure rules, README, FEATURES (with an experimental legend), and docs/README to match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(harness): measure party spread from the fresh player position `spread` used screenPositionSec, which the TV updates only every 500 ms, so readings swung by up to half a second. It now uses the player position when that is on the same timeline as the screen position, and falls back to the screen position when a remux or transcode starts mid-title. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): stop remembering a party as it closes The TV hub offered Rejoin for a party it had just ended for everyone. The repository publishes an end while the last snapshot is still live, then clears both. The recorder checked the live membership first, so it remembered the room again, and the cleared pair never reached the forget branch. A room-gone end now wins, and a membership whose own room just ended is never remembered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): a dead room socket can no longer stop the playback binding On a throttled TV, the socket died while the binding was sending to it. Its outgoing channel was cancelled, the pending send threw CancellationException, and the realtime client rethrew it as if the binding had been cancelled. The binding's event loop ended silently. The TV kept playing 30 minutes behind the room, marked as left behind, and never caught up, even after it reconnected and new commands arrived. A send now propagates only the caller's own cancellation; any other failure, including a cancelled channel, is a failed send the binding retries on the next socket. Each send is also bounded to 5 s, so the loop never waits on a socket that has stopped draining. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): stop a phone guest from fighting the room's seeks With a web host seeking, the phone guest seeked back and forth between the old and new positions every ~35 ms, thousands of times, and never became ready. A MediaController reports its own seek at once and the session reports it again; the phone's issued-seek tracker consumed the first match, so the second report looked like an outside seek, which a guest undoes, which the room redoes. The TV's tracker never consumed matches and did not loop. Issued seeks now stay recognized until they expire, as on TV, and the coordinator undoes at most one outside seek per two seconds, so a future mismatch cannot turn into a loop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): keep party invitations through sign-in A silo://watch-party link that arrived before sign-in or profile selection was pinned to the identity generation at arrival. Signing in moves that generation, so the pending hub route failed its scope check and was consumed without opening. Invitations now travel unscoped, like account invitations: delivery still waits until the user is past setup, login, and profile selection, and the hub joins only when the link's server is the active one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): start each membership with a fresh server clock The room clock estimator kept its samples across memberships. After leaving one party and joining another, possibly on another server, the new room's commands were scheduled with the old offset, and the estimator could keep preferring the old low-RTT samples for up to ten minutes. Installing or clearing a membership now resets and republishes the estimate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): offer TV "Add to party" only when the server stages The TV detail page offered "Add to party" to a Host Picks host whenever the lobby was open, even on a server that does not advertise staged_selection, and then called the unsupported endpoint. It now requires the capability, as the phone's eligibility rule already did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): repair command timing and party transitions * fix(watch-party): undo every denied outside seek on a guest The two-second cooldown on undoing outside seeks meant a guest's second genuine seek within that window (a double press on a headset or notification control) stayed at a position the room had refused until a later command corrected it. Both players now recognize their own restore seeks, so every outside seek is undone. For a guest, only a runaway burst of more than four within two seconds stops restoring, as a guard against a loop; a host always restores so its report cannot re-anchor the room early. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): show only movies, series, and episodes in picker rows The picker's Continue together and watchlist rows come from profile catalog access, which can include ebooks and other non-video items. Search results were already limited to playable video types, but the rows were not, so TV could show reading items (excluded on TV) and both platforms could offer items that cannot play in a party. The shared picker ViewModel now applies the same filter to both rows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): restore every denied external seek * feat(watch-party): match the iOS and tvOS Watch Party design The Android party screens were plain utility lists while Apple's are artwork-led rooms. Phone and TV now follow the Apple layouts: - Lobby: the staged title's backdrop fills the room, with a code pill, connection dot, options, a hero with facts and quality chips, member seats whose rings show readiness, a voting ballot or suggestion list, and the one action that matters pinned at the bottom. - Back during party playback reopens that room over the player with "Return to playback" as the main action. - New invitation page with a QR code of the invite link. - Hub, options, join keypad, and the title picker (Home-style shelves plus a confirmation page with "Who's seen it") follow Apple too. The lobby's wording and foregrounded action come from one shared policy ported from Apple's WatchPartyLobbyPolicy, so phone and TV say the same thing. Guests can mark ready in any lobby, as on Apple, and a voting room only offers Start when the server can promote its leader. Avatar colours use Apple's hash, so a member has one colour on every client. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): close three review gaps in party commands and entry - A room command whose execute_at can't be read is now a malformed frame, so the room reconciles instead of running the command at once. - A SiloCast launch is refused whenever the TV is in a party, including the lobby, where no party player is registered yet. - The TV suggestion menu offers Queue only when the server supports staged selection, as the phone already does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): suggest in playing vote rooms and await TV departure - A host on a detail page in a playing voting room now gets "Suggest to Party" on phone and TV. "Play for everyone" sends a direct selection, which the server refuses in vote rooms. - The TV's "Leave and play" waits for the party to finish leaving before starting solo playback, as the phone already does, so a queued room command can't reach the new playback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): re-check server support whenever the hub is shown A successful capability probe was cached until the process or identity changed, so a server that turned Watch Party off went unnoticed. The phone and TV hubs now force a probe each time they're shown outside a party, as Apple does. A failed probe still keeps the last known answer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): reconcile instead of running unknown room actions A transport command with an action this client doesn't know decoded as Unknown, and the playback binding treated it as advancing playback. The decoder now reports it as malformed, so the room reconciles instead. The server sends only play, pause, and seek. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): tighten room commands, host entry, and outside play/pause - A transport command without a known playback_state is malformed and reconciles the room; the server always sends one. - Phone and TV hubs hide Start on servers without staged selection and say the party can still be joined, instead of offering a button the view model then refuses. TV focus falls to Join there. - After an outside play/pause (notification, headset, Assistant), both players sample the room state only once the room's play/pause is restored, so a report can't carry the outside change to the room. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): hide detail-page hosting on servers that can't host The detail page still offered "Watch Party" (host with this title) on a server without staged selection, and the hub then refused it. Phone and TV now hide that entry once the server is known not to host, and keep it while support is still unknown so the hub can explain the result. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): wait for the hub re-check and reuse in-flight suggestions - The phone hub now waits for its forced capability re-check before it consumes an invitation or detail-page host handoff, since the previous answer stays published while the probe runs. - "Suggest to Party" records its request before sending, so a second tap while the first is in flight resends the same suggestion id instead of adding a duplicate, and a later result can't clear another's retry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): fresh start per title, TV host re-check, lobby suggest reuse - A new selection revision resets the reload budget and stall history, so the previous title's backoff and lower-quality offer don't carry over to the new stream. - TV detail-page hosting forces a capability re-check and waits for it before creating a party, like the hub. - The lobby's "Suggest" reuses the id of an uncertain or in-flight suggestion of the same title, and only a request's own outcome can clear the retry draft. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(watch-party): harden room sync after review - Return the correction rate to 1x while the room socket is detached or reconnecting, instead of leaving up to 1.25x running. - Hold a Play correction that the reload budget defers and run it once the budget allows, rather than playing on at the wrong position. - Count expiring ws-tickets against the reconnect budget and renew the proof when its expiry is unknown, so the loop cannot retry forever. - Renew the room proof on the server clock estimate, and never re-read the room back to back after a successful renewal. - Pair each pong's receipt time with the monotonic time of the same moment, so a queued pong no longer reads as a wall-clock jump and wipes the clock estimate. - Retry a ws-ticket 401 as a transport failure instead of ending the party. - A coalesced suggestion read now shares the read's real outcome. - Phone retries resume where the player was within the same epoch. - Share the issued-seek tracker, invite server match, and notice copy between phone and TV in android-shared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rver#482) * fix(tv): start and stop the cast receiver as sign-in changes The TV checked sign-in once each time the app came to the foreground. Signing in or picking a profile while the app stayed open never started the SiloCast receiver, so remotes couldn't find the TV until it was backgrounded and reopened. Signing out left the receiver running. MainTvActivity now re-checks sign-in after every completed identity transition, inside repeatOnLifecycle(STARTED), and calls start() or stop() on the main thread when the answer changes. A remote-playback temporary identity counts as signed in, so a handoff is never stopped. The receiver's server-switch handler now ignores profile and last-used updates to the registry entry. Re-registering on a profile clear raced the stop() that followed, so the TV stayed listed in Bonjour after sign-out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): drop stale cast sign-in reads and late receiver starts A sign-in result read on IO could reach the main thread after a newer identity change and briefly start or stop the receiver. Each result now carries the identity generation it was read under, and the activity ignores it if the generation has moved on; the transition's DID_CHANGE triggers a fresh read. TvSiloCastReceiver.start() published its socket and advertised from an IO job that stop() could not interrupt, so a quick sign-in then sign-out could leave the TV listening and listed in Bonjour. The job now publishes and advertises under the receiver lock only while its scope is current, and closes the socket otherwise. The server-switch collector re-advertises through refreshAdvertisement() for the same reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…Silo-Server#477) * feat(tv-settings): opt-in True Black Bars for grey HDR letterbox bars On some Android TV devices, such as the Onn 4K Pro, the letterbox bars of a Dolby Vision title narrower than 16:9 glow grey. The bar rows hold only the player's opaque black, and the device composites that black above reference black while Dolby Vision is on. Other devices do the opposite, and no platform signal tells them apart. Add an off-by-default, device-local True Black Bars toggle under Settings > Playback > Streaming. When it is on and the first frame has rendered, the player clears its area to transparent instead of painting it black, so the bars come from the output's own black. Reset Playback Overrides clears it, and it never syncs to the server. Refs Silo-Server#475 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(tv-player): keep the black plate while Navigation fades the player The navigation fade draws the player into a layer, so clearing it there let the other screen show through the bars. Clear only after the first frame and once the transition has settled. Found in cross-provider review. Refs Silo-Server#475 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilo-Server#483) * fix(playback): never block playback on an earlier unfinished session Starting playback refused with "A previous playback request needs recovery" while an earlier session for the profile was unsettled. The settle step skipped sessions recorded under another server address, account or installation, but the check that blocked playback did not, so one such session blocked playback until app data was cleared. The server allows a new session while an older one is open and expires sessions nobody stops. Start now resends pending stops and then starts regardless; unreachable attempts and lost starts are dropped rather than replayed; a failed stop no longer fails the player session. The manual "Retry Pending Playback Stops" Settings rows are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): bound stop retries on the playback start path Start retried every pending stop with three attempts each, and stop requests can take up to 60 seconds, so a server that hung on stops could hold each start for minutes per pending stop. Make one attempt per stop and end the pass at the first failure; the rest wait for the next start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) A PGS subtitle sent as a .sup sidecar (the remux and transcode route) was read from start to finish as soon as it was mounted, and every caption image it decoded stayed in the sample queue until playback reached it. For a feature film that is tens of MB of Java heap; a phone with a 192 MB heap ran out of memory, or stalled at startup, as soon as the track was selected. The sidecar wrapper keeps re-continuing its loader so a sidecar never gates the video. It now leaves the loader parked once its queued cues are more than two minutes past the playhead, and continues it as playback catches up (from the playback ticks) or after a seek. Only the PGS sidecar sets a limit; text sidecars still load in one go. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lo-Server#468) * fix(tv): keep library browse sort and filters across app restarts Android TV held a library's Browse sort and filters only in the screen's ViewModel, so closing the app reset them to Title A-Z with no filters. Phone, web, iOS, tvOS, and macOS all bring them back. TV now saves them with the phone's BrowsePrefsStore, moved to android-shared and keyed under its own `androidtv` prefix (phone keys are unchanged). They are saved per server, profile, and library, and restored when Browse opens. The A-Z jump and the preset sections are not saved, matching tvOS. The TV filter panel gains the "Preserve sort & filters" toggle the other clients have. * fix(tv): offer Clear filters when a filtered Browse request fails The Browse error screen replaces the Sort and Filter row, and Retry sends the same query. A saved filter the server rejects would keep Browse failing after every restart. When filters are active, the error screen now offers Clear filters, which reloads Browse and overwrites the saved state while keeping the sort. * refactor(tv): simplify the Browse preserve toggle and save check Pass the preserve value and its handler to the filter panel as one nullable parameter, so a caller can't show the row with a value that disagrees with the store, and drive the row from that value instead of a local copy. Decide whether to save Browse state by comparing sort, order and facets directly rather than converting both filters to the saved form. Return early from onTabSelected before reading saved state for a section that is already selected, and say which saved values the restore actually drops.
…rtwork (Silo-Server#530) * feat(artwork): show a default brand glow for missing artwork A title with no artwork showed a flat dark tile on phone and tablet, and a movie icon on TV whatever its type. It read as a loading bug. Posters, covers and stills now show one default artwork when the image is missing or fails to load: the Silo blue, pink and orange as a soft glow, the same for every title, matching the Apple and web clients. A failed load with a ThumbHash keeps the ThumbHash. ThumbhashImage gets an opt-in defaultArtwork flag, so backdrops, logos, avatars and person photos are unchanged. The TV episode rail and TvPoster drop their own empty-URL branches and go through ThumbhashImage. The default artwork carries the image's content description, so a card with its caption hidden keeps an accessible name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(artwork): quieter default artwork with a type mark Missing posters, covers and stills now show the brand glow at about a third of its strength on a lighter base, with a small film, TV, headphones or book mark for the item's type, matching the Apple and web clients. Cards that centre their own play or book glyph over the artwork show the glow without a mark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(artwork): fall back after a missing series poster or a bad ThumbHash On an episode page where neither the season nor the series has a poster, the tablet poster box now shows the default artwork once the series lookup finishes instead of staying a flat tile. A ThumbHash that fails to decode no longer blocks the default artwork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(artwork): resolve the tablet poster when an episode has no parent An episode without a series ID or season number never runs the series poster lookup, so mark the poster resolved and let the default artwork show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilo-Server#479) On Amlogic boxes connected to TVs without DTS, the platform DTS decoder played silent. When the active HDMI sink lists compressed codecs but no DTS, DTS now goes to the bundled FFmpeg decoder, and DTS passthrough is no longer advertised to the server for that sink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silo-Server#532) Android TV saved Browse sort and filters per library, so a mixed library's Movies and Series tabs shared one saved state and one "Preserve sort & filters" toggle. A genre or sort chosen under Movies reopened under Series. Save them per media scope: a scoped view of a mixed library uses its own `.type-<scope>` key in BrowsePrefsStore. Unscoped libraries keep their existing keys, so saved state from before this change still restores. Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…er#447) * fix(mobile): separate movies and series in mixed libraries * fix(android): cancel superseded library recommendation loads * fix(mobile): keep refilling full scoped shelves and preserve profile overrides A full inline slice is not proof of exhaustion: Recently Added and Recently Released report total_count = len(items) after LIMIT item_limit, so a movie-only slice of 20 hid older series without a refill or notice. Only an under-filled slice (items < item_limit, total_count <= items) now skips the refill. The catalog section source pages the stored page_sections definition, not the profile-resolved one, so refilling a customized or profile-added shelf replaced its override (e.g. a movie-only Random row filled with series). Those shelves now keep their scoped inline slice and report it as incomplete instead of paging the admin definition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mobile): offer no shuffle in Movies- or Series-scoped collection views The phone counterpart of the TV change: a shuffle request carries only the collection kind and id, so a scoped view of a mixed library's collection would shuffle both types. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(mobile): offer no library shuffle in a Movies- or Series-scoped view A shuffle request carries no media type, so shuffling a mixed library with the Series chip selected would also play movies. Match the scoped collection views and hide it until the request can carry the scope. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(catalog): keep matching inline items when a scoped refill completes A completed refill returned only what the catalog pages yielded, so a changing shelf such as Random could drop a series it already showed inline, or vanish. Merge matching inline items into the result with the same content-id deduplication the incomplete paths use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(catalog): put matching inline items first when merging a scoped refill Appending them after the refilled items let the trim to the shelf limit evict them whenever a fresh Random sample alone filled the shelf. They are the start of an ordered shelf anyway, so leading with them keeps order and guarantees they survive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * perf(catalog): stop refilling a scoped shelf once inline items complete it The refill loop compared only the newly paged items with the shelf target, although the result also keeps the matching inline items. A Random shelf whose inline items nearly filled it kept paging, up to eight requests, after one new item had already completed it. Count the merged, deduplicated items instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…can (Silo-Server#488) * feat(cast): start phone-to-TV playback without a handoff when the TV can Pressing Play on the phone while Remote Control drives an Android TV ran the full profile handoff every time, even when the TV was already signed in with the same profile, and the TV ended the borrowed profile whenever a player closed. Each title took several seconds to start, and the TV showed nothing while it waited. Android TV: - Answer handoff_ready at once (reused: true) and play on the TV's own identity when the controller was authorized at hello against the TV's own server, no temporary identity is installed, and the TV is signed in with the offered profile (RemotePlaybackOwnIdentityPolicy). - Keep a borrowed profile while its phone holds the slot; hand it back when that phone leaves, another phone takes over, or after 10 minutes idle. The idle limit replaces the 60 s ready-without-launch timeout. - Unpublish an identity before its server logout, so nothing launches on or reuses an identity being ended; a started hand-back is no longer cancellable, and its logout is capped at 5 s. - Show "Starting {title}" / "From {phone}" on the standby view while a full handoff runs. Phone: - Send the item's title in handoff_offer (optional, display-only). - Show the title's poster and "Starting on {TV}..." from the tap until the TV reports that title, with tick, confirm, and reject haptics. - Read "Connected / to {TV}" in the mini player while the TV is idle. Closes Silo-Server#486 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cast): close two launch races found in review - Phone: read the outgoing title before sending the launch frame, so the TV's state for the new title is never recorded as the title being replaced, which left the remote on "Starting..." until the 30 s timeout. - TV: a scheduled hand-back and launch admission now decide under the receiver lock. A hand-back commits only while nothing is playing on, or launching to, the identity; admission refuses an identity whose end has committed or that is gone, with handoff_required. The bookkeeping after an end now runs in its non-cancellable step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): clear only the ended generation's pending launch After a hand-back's logout, clear the pending launch only when it still belongs to the ended generation, under the receiver lock that launch admission uses. A launch admitted for a newer identity in the meantime keeps its protection from the idle limit until its player registers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): admit one launch at a time, from the phone that holds the TV A launch still being handled for a phone that just lost the slot could be admitted after the new phone's handoff began, and the new phone's handoff could then replace the identity before that launch's player registered. - Launch admission now runs only for the active session and only when no other launch is pending. Each admitted launch owns the pending marker, and cleanup clears the marker only for its owner. - A handoff offer is refused with handoff_cancel reason launch_pending while a launch is on its way to the player. - A pending launch expires after 30 s, so a player that never opens cannot hold off every later handoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): drop an expired or abandoned launch before navigation opens it When a pending launch expired, or the receiver stopped, the marker was cleared but the request could still sit in the launch channel. Navigation would open it later, possibly under a newer phone's identity. Expiry now drops its own request if navigation has not taken it, and stop() drops any queued request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): check a phone's launch is still current before its player opens Dropping a queued request on expiry could not reach a request navigation had already taken. That launch could still open its player after it expired, or after its identity changed, and play under the wrong identity. - Each admitted launch now carries an id through navigation and the player route. Expiry or stop() revokes the id. - Navigation confirms the id right before it opens the player and skips a stale launch, releasing its pending marker so the next launch is not refused. - The player registers with the id. A launch that went stale in the moment before registration registers nothing, and the screen closes the way a phone's Stop does. Only the launched player takes the launch's identity generation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): tie a phone's launch to the identity it was admitted on The launch check from b35aeac left gaps. A launch on the TV's own profile carried no identity, so a profile switch before its player opened went unnoticed. A recreated player skipped the check for its launch even after the borrowed identity ended. The player screen started loading before the check ran, and a stale player left through the generic Back, which could close a newer launch's player. - Each admitted launch records the IdentityTransitionBarrier generation. Any sign-in, profile, server or borrowed-identity change since makes it stale, on the TV's own profile as well as a borrowed one. Admission refuses such a launch with handoff_required. - A recreated player re-registers only while its launch's identity is unchanged. - Navigation checks the launch before it creates the player screen, so a stale launch never starts loading. - A launch that goes stale just before registration stops what its player started and leaves only its own back stack entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): ignore unrelated server removals and recheck a stale player before stopping it Every IdentityTransitionBarrier transition bumps its generation, including removing a saved server that is not the active one. A launch admitted just before such a removal was refused although its identity had not changed. A stale player's exit also stopped the shared service player before it checked whether a newer launch's player had already replaced it. - The receiver counts only WILL_CHANGE transitions that affect the current identity, the filter NotificationsRepository uses, and compares launches against that count. - The stale-launch callback now checks that this back stack entry is still on top right before it stops playback and pops it. Otherwise the newer navigation has already replaced and torn down this player. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): read a launch's identity only while no identity change is in flight The identity revision advances at WILL_CHANGE, before the change lands. A launch admitted in between could take the new revision, check the old profile, and then stay valid after the switch. Admission now reads the identity and its revision inside IdentityTransitionBarrier.withCurrentGeneration, which holds identity changes off while it runs, so a change already under way is waited out instead of half observed. The read retries if changes keep landing, then refuses the launch with handoff_required. This is the same fence captureAccountSessionExpectation uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The "Set up with your phone" screen pictured step 1, "Open Silo on your phone", with three slanted bars on black: a hand-drawn stand-in that has no play triangle or bar cutouts and doesn't match the app icon. Draw the flat Silo mark from silo-branding (logo/svg/silo-mark-flat.svg, added as the silo_mark vector) on the app icon's #010D9F field, at the 84% height the icon uses. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Silo-Server#500) * fix(settings): let a device go back to the profile's playback settings Android writes every playback setting as a device override, but the only way back was "Reset Playback Overrides", which cleared everything at once, without asking and without feedback. Each setting the profile can also hold now offers "Use profile setting": the first option of the Quality, Audio Language, Show Next Up and Skip Intros pickers (checked while the device has no value of its own), and a "Use Profile Setting" row under the Auto-Play Next Episode and Skip Credits switches while the device holds its own value. Choosing it deletes the device value through the same scoped queue writes use, and clears the keys a control writes together (both quality axes; the intro skip enum and its deprecated boolean). The store tracks which keys this device holds, from its own writes and from the scope each refresh reports. Choosing no audio language now clears the device value instead of storing an explicit null over the profile's language, so the separate "No preference" entry is gone from the device picker. The bulk action is now "Use Profile Settings". It asks first ("Use your profile's settings on this device?") and shows a notice afterwards, which says when the change is still waiting for the server. The copy says that device-only settings go back to their defaults, which is what the action does to them. On TV, the option pickers now focus the current choice when they open; before, the dialog's first row kept focus, which would now be "Use profile setting". * fix(settings): confirm the profile reset against the server's answer Use Profile Settings reported success whenever the write queue came back empty. The queue also drops deletes it cannot retry, such as a 401 or 403, so the notice could say the profile's settings apply while the device values were still on the server. The reset now reports success only when the following refresh reads the server's values and none of them still resolves from this device. * fix(settings): confirm a device reset only for the profile it started on resetAllDeviceSettings confirmed success from a refresh that re-read the active scope, so a profile switch or sign-out during the reset could confirm it from another profile's answer and write that answer into the wrong store. The confirming refresh now runs against the scope captured at the start, with its authority, and the reset reports false once that scope is no longer active. * fix(settings): stop promising a reset will apply after reconnecting "Use Profile Settings" said the profile's settings would apply when the device reconnects whenever the server didn't confirm the reset. That only holds while the app stays alive and offline. The write queue is in memory and drops deletes it can't retry (a refused delete, a profile switch), so in those cases nothing applies later. The notice now says the server didn't confirm the change and asks the user to try again; running the reset again is safe. The legacy device-settings import also marks each value it writes as this device's own, as every other device write does, so imported values offer "Use profile setting" before the next refresh confirms them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… scope (Silo-Server#478) * fix(collections): open library-tab collection items without a library scope Items in a collection opened from a library's Collections tab were read with that library's id. The server answers 404 for an item scoped to a library that does not hold it, so any item filed in another library showed as unavailable. User collections often mix libraries, and admin collections can be shown in libraries other than their primary one. Phone and TV now open these items without a library scope, like the personal Collections screens, the web client, and the Apple clients. The route's library id still loads the collection itself. * fix(collections): shuffle library-tab collections without a library scope A collection shuffle draws from the whole collection, but both apps started it with the tab's library, so the player asked for each pick with library_id set and the server returned 404 for any pick filed in another library. Start collection shuffles unscoped, like item clicks and the Apple apps, and drop the phone view model's unused browseLibraryId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#527) * fix(player): consume and reconcile recap and credits markers * fix(player): keep markers bound to the file being played The marker reconcile read watch detail without naming the playing file, so the server filled on-demand markers for the default version and the reconcile cleared the playing version's ranges. It now sends file_id. A markers_updated event is applied only when its file_id matches the playing file; the server also delivers a file's update to sessions that requested that file but play another. Recovery and subtitle replans that land on another file in the same session now take that file's markers and reconcile them, since no new realtime connection opens to do it. Download playback reads the downloaded version's markers instead of the root ranges. Also moves the applyUpdatedMarkers KDoc back onto its function and uses imports in place of fully qualified names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(player): name the file and room library in marker reads Download playback now sends the downloaded file's id with its watch detail read, so the server fills that version's on-demand markers; local playback has no control session to reconcile them later. The TV marker reconcile reads watch detail in the same library playback started from. A Watch Party can move to a title in another library while the player stays open, and the launch library then pointed elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): hide the marker skip pill when playback fails The TV overlays still draw over the error screen, so a failure inside a recap or credits range left the Skip pill visible and focusable beside Retry. The marker target is now off while playback is loading or failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…othing (Silo-Server#472) * fix(libraries): tell an empty library apart from filters that match nothing An empty library and a filter that matched nothing showed the same kind of message, so a viewer could not tell whether to clear filters or wait for media. On phone the copy depended on whether sort or filters had changed; on TV every empty browse said "No titles match the current filters." When a library browse comes back empty, the screen now checks whether the library holds anything at all. With no filter or letter applied, the empty page already answers that. Otherwise it reads one unfiltered item without a total (CatalogRepository.libraryHasItems), straight from the API so a one-item page never replaces the cached offline page. An empty library says "This library is empty", as on the web; filters that hide everything keep the filter message. If the check fails, the existing copy stays. This covers the phone library and browse screens and the TV library Browse tab. Refs Silo-Server#451 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(libraries): scope the empty-library check to the mixed-library type In a mixed library's Movies or Series view, the one-item check asked the whole library, so a Series view with no series reported "No matches" once a filter was set, while the same view without a filter said "This library is empty". Ask within the view's type on phone and TV, as Apple does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
…-Server#440) * feat(profiles): send stale PIN profiles back to profile selection When an administrator changes an account's access, the server stops accepting PIN profile tokens minted before the change and answers 403 profile_verification_required. Android had no general handling, so every screen showed a failed load, and the phone picker failed too because it still sent the stale token. The v2 error decoder now reports that refusal with the profile id and token the request presented. A recovery clears the profile selection only when that is still the active profile on the active server, never the sign-in, and phone and TV open the profile picker. Reports for an identity that has since changed, a remote-playback overlay, or household-management calls (which use the same problem type for a missing permission) are ignored, so refusals cannot loop. The prompt waits behind playback and sign-in screens, and the picker reloads in place when it is already open. * fix(notifications): resume realtime after the profile is re-verified A 401 or 403 on the events ticket stopped the notifications realtime loop for the rest of the session. After an access change that is the normal path for a PIN profile: the ticket is refused until the user enters the PIN again, and the unread badge then never updated live. The loop now waits for the next identity change (PIN re-entry, profile switch, account or server change) and reconnects with a new ticket. A socket the server closes with 4001 access_changed already ends cleanly and reconnects like any other close. * feat(realtime): refresh access-dependent screens when access changes When an administrator changes an account's access, the events socket sends {"type":"access_changed"} and closes with code 4001. Android reconnected, but only Home could refetch, so libraries, search, item details, and collections kept showing what the old policy allowed. The events socket now reports the frame and the 4001 close once per connection. Home, the library lists and TV tabs, search, item details, and collections refetch through their existing quiet refresh paths on phone and TV, coalesced so both sockets seeing the same change cause one refresh. The sockets reconnect with a new ticket as before. A server that never sends the signal leaves all of this idle. * test(auth): cover the quiet refresh on token_refresh_required After a role change the server rejects older access tokens with 401 token_refresh_required while the refresh token keeps working. The auth plugin already refreshes and retries once on any 401; these tests pin that for the active and the pinned background path and check that the session and profile selection survive. * fix(realtime): refresh once when both events sockets report one access change The Home and notifications sockets each detect an access change on their own server-side check, which runs every 15 seconds from each connection's start. Their reports are usually seconds apart, so the 750 ms debounce let both through and screens such as search refetched twice. The first report now refreshes at once, and reports within 20 seconds of it count as the same change. * fix(profiles): log only the exception class when recovery init fails * fix(detail): show the unavailable state when an access change hides the title The access-change refresh reused the quiet return refresh, which ignored every error, so a title the viewer lost access to stayed on screen. A 403 (other than a stale profile proof), 404, or 410 now replaces the detail with the error the initial load shows; network failures and 5xx still keep the current detail. * fix(profiles): re-check a deferred profile prompt when remote playback ends A prompt deferred behind a remote-playback overlay was only decided again on the next navigation. The recovery now re-emits the pending prompt after each completed identity transition, and both navigation hosts collect that, so ending the overlay sends the user to profile selection without another navigation. * fix(tv): refresh library pages when the viewer's access changes TvLibraryDetailViewModel kept its one-time loaded flags across an access change, so recommended sections, browse results, audiobook groups, facets, and collections stayed stale. The screen now reloads the visible tab and marks the rest for reload when next opened. * fix(tv): re-probe library collections after an access change The collections probe was keyed only on the library list, which can stay equal while collection visibility changes, so the Collections pill kept its old answer. * fix(detail): reload a refused title when access changes again After an access change refused a title, the detail was cleared, and the next access-change refresh returned early because there was no detail to refresh. A title the viewer regained access to stayed on the error until they tapped Retry. With no detail on screen, an access change now runs the full load, as Retry does. * fix(detail): keep a refused title hidden when the full load runs again The full detail load paints the durable cached copy before the live request returns, and a refusal does not evict that cache. When the live request was refused, the error was set but the cached detail stayed, and the screen shows detail ahead of the error. After an access change refused a title, the next access change, or Retry, brought the refused title back with all its actions. On phone and TV, a refusal (404, 410, or a 403 other than profile_verification_required) during the full load now clears the cached detail, so the screen shows the error. Network errors and other failures keep it as before. Adds phone and TV tests for both access-change paths: a title the viewer regains comes back, and a title still refused stays on the error. * fix(realtime): apply access changes a screen missed while away Screens refreshed for an access change only while composed, so a retained ViewModel whose screen was behind another destination (TV library behind a detail page, detail behind the player, phone Libraries while another tab showed) kept the old policy's data when the user came back. Each of those ViewModels now keeps an AccessChangeCursor with the last revision it applied, and its screen refreshes on return for a change reported while it was away, under the same coalescing window. Related gaps from review: - A detail page whose first load was still in flight ignored an access change, so the old policy's answer stayed. The change now replaces the unfinished load on phone and TV. - Phone Libraries reloaded only the visible subtab; it now forgets which subtabs and facets were loaded, as TV does. - Home dropped a realtime signal that arrived during a running fetch; it now runs one follow-up refresh after it. - After profile recovery cleared the selection, Back on the phone picker returned to Main with no profile. Back now leaves the app instead. - A refusal no longer clears a phone detail page whose title has a completed download, so its local play and delete actions stay. - The TV collections probe rethrows cancellation instead of publishing a partial set when an access change restarts it. * fix(detail): reload past a pre-change warm-up after an access change Replacing an in-flight detail load did not help when the load had joined a Home warm-up for the same title: the new load joined the same request, answered under the old policy. Detail reloads and quiet refreshes after an access change now send their own request on phone and TV. The phone Libraries hub and library collections screen also let an older library-list or collections load land after the refresh an access change triggered. A newer load now cancels the unfinished one. * fix(tv): skip a pre-change warm-up when an access change reruns the full detail load * fix(realtime): check the library list for an access change after the app returns to the foreground The events sockets close while the app is in the background, and the server compares each new connection with the policy its own ticket was minted under, so a change made in that time was never reported. Returning to the foreground now fetches the library list and reports an access change when its set of libraries differs from the cached list the screens were built from. * fix(realtime): refresh for a change reported by a socket opened after the last refresh Access-change reports within 20 seconds of a refresh were all treated as the second socket's report of the same change. A socket that reconnects after reporting compares against its new ticket, so its report inside that window is a second edit, and it was dropped for good. Reports now carry the time their connection opened, and a report from a connection opened after the last refresh refreshes again. * fix(detail): keep an older quiet refresh from overwriting a newer one A return or watched-change refresh still in flight when an access change refreshes the page could land last and restore a title the newer read found refused. Each quiet detail read now replaces the unfinished one, and keeps its access-change refusal handling and, on TV, the player's saved position. * fix(tv): let only the newest library-list, filters, and collections load publish An access change reloads search media types and library facets and collections while an earlier load may still be running. The newer load now cancels the older one, so the pre-change answer cannot land after it. * fix(tv): keep a watched change from restoring an older resume position A watched-change refresh that replaces an unfinished playback-return refresh no longer re-applies that return's saved position, which the watched write has cleared. Search also declares its library-load job before init, which starts the first load, and a test covers a reload after an access change superseding that first load. * fix(profiles): return to the server list from a picker opened while adding a server The picker's Back handler closed the app whenever no profile was selected and another screen sat below it. Adding a server from the server list leaves that list below the picker with no profile yet, so Back closed the app instead of returning to it. Close the app only when the screen below belongs to the signed-in shell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(search): keep an older search from overwriting the access-change re-run An access change re-runs the current query while the earlier request for the same query can still be in flight. Both published, so a pre-change answer that finished last restored titles the viewer had lost. Each new search now takes a generation, and answers from older ones are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(collections): let only the newest collections load publish Collections refreshes on an access change, but the initial load or an earlier refresh could finish later and restore collections the viewer can no longer open, on phone and TV. Loads now share one job; a new load cancels the previous one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(libraries): keep a pending facet load from refilling facets after an access change refreshAfterAccessChange() cleared the Browse facets but left the query generation alone, so a facet load still in flight stored the old policy's facets, and Browse then skipped fetching them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): declare the library page's load jobs before init init starts the first Recommended load, but recommendedGeneration and recommendedJob were declared below it, so their initializers ran afterward: the job was dropped and the generation reset to 0. An access-change reload then could not cancel the first load and reused its generation, so both answers published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lo-Server#427) * feat(settings): hide spoilers for unwatched episodes, per profile Adopt the settings contract at revision 15, which adds the profile settings catalog.hide_unwatched_episode_images and catalog.hide_unwatched_episode_overviews. When a profile turns them on, the phone and TV apps blur the still and hide the description of episodes the profile has not started: episode rails, the series hero, episode detail, Continue Watching / Next Up cards, the TV home marquee, and the players' next-episode screens. A Spoilers group in Settings holds the two switches, shown only when the server serves revision 15. Below API 31, where Modifier.blur does nothing, a hidden still shows only its ThumbHash. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): reload spoiler settings when the server or profile changes The spoiler store remembered only that it had loaded, not for which server and profile. After Add Server and sign-in, it kept the previous server's answer until the app restarted. It now follows the active server and profile and ties each load to them. The series hero also no longer reserves an empty block where a hidden episode description would have been. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(settings): preserve episode spoiler protection across updates Cancel obsolete hydration, preserve pending toggles, and cache writes for their original profile. Rebuild TV marquee projections when spoiler preferences change and protect episode detail fallback artwork. Co-Authored-By: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(tv): refresh marquee spoilers after watch updates Refresh displayed and pending marquee sources from their own current rows while preserving focus and enrichment. Cover watch changes and duplicate episode IDs across rows. Co-Authored-By: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(spoilers): preserve series artwork with v2 provenance Carry selected image provenance through phone and TV cards, episode rails, marquee projections, detail fallbacks, and player next-up. Keep series artwork visible and protect older responses conservatively. Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(settings): require revision 16 for spoiler keys Preserve revision 15 for the watchlist settings already published on server main. Refresh generated bindings and fixture records from the integrated server contract. Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(downloads): protect unwatched episode artwork Persist artwork provenance and episode user data, protect inherited headers, and apply current local watch and resume state. Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(tv): publish safe artwork for protected Watch Next episodes Resolve profile preferences in background sync, use safe series artwork, and fence obsolete publication. Clear and reseed tiles when image protection changes. Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(downloads): refresh episode watch state before artwork projection Use authenticated server snapshots while preserving pending and newer local writes. Fence concurrent section builds so older snapshots cannot overwrite a refresh or deletion. Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com> * fix(tv): preserve Watch Next tiles on ordinary startup Wait for known preferences and persist the last image-protection value. Clear only when protection becomes enabled; verify that unchanged settings preserve the publication generation across seeder recreation. Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com> * fix: track watch and playback mutations separately * fix: protect episode artwork in catalog grids * fix: clear catalog progress on unwatched resets * fix(settings): treat spoiler keys the server refuses as unsupported A server can reach the gating settings revision without these keys: server main took revision 16 for another setting. Its batched effective read then answers 404 unknown_setting, which the store kept as a transient error, so support stayed Unknown, the Spoilers settings stayed hidden, and the TV Watch Next worker retried forever. A 404 now means unsupported, as it already does for the capabilities call. Also point stale "revision 15" comments at the gating constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(downloads): read episode watch state only for image protection - Skip the per-episode watch reads and the Room overlay unless image protection is on, fetch when it turns on, and let pull-to-refresh reconcile download records without waiting for those reads. - Wait for the first load instead of publishing an empty list, which flashed the empty state when the tab opened. - Build sections one at a time, so a build started from a sidecar map that a reload is replacing can't win publication. - Season and series rows name the download their poster came from, so a season showing another season's still is still protected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep Watch Next safe through failed lookups and interrupted wipes A failed series lookup was treated as "no safe artwork", so the run omitted the protected tile and deleted it from the launcher. It now fails the run so the worker retries. Enabling image protection recorded the setting before the tile wipe finished; a process death in between skipped the wipe on the next start. The setting is now recorded after the wipe completes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep resume guards for rows written before positionUpdatedAtMs The migration adds positionUpdatedAtMs without a backfill. A delayed watched acknowledgement treated those rows as older and could clear a newer resume sample, and a rejected watched write queued before the upgrade could not restore the resume point it cleared. Both guards now fall back to clientUpdatedAtMs for such rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mobile): reset episode progress when marking it watched or unwatched The detail page changed only the played flag, so an in-progress episode marked watched and then unwatched kept its old progress, counted as started, and showed its still despite image protection. Both changes now clear progress, as the server does and the TV app already did, and a failed write restores the exact previous state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): rebuild the hero backdrop when the item's watch state changes Keyed on the content id, the hero kept the spoiler decision it made when the item first appeared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(settings): gate spoiler settings on contract revision 17 Server #1709 now builds on server main, where revision 16 belongs to other settings, and adds the two spoiler keys in revision 17. Raise the gate to 17, regenerate SettingKeys, and re-vendor the settings and API v2 fixtures from silo-server 51953b6c1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(player): keep shuffled movie overviews visible with spoiler protection Shuffle post-roll built its next-item state without the unwatched flag, so the default (unwatched) hid every shuffled movie's overview when episode descriptions were hidden. A shuffle pick carries no watch state, so episodes stay protected and movies never are, on phone and TV. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(spoilers): honor artwork provenance and close review races - Detail heroes (phone compact and expanded, TV) and the TV marquee's enrichment backdrop now follow the displayed artwork's backdrop_is_episode_still flag instead of assuming every detail backdrop is series art. - A Watch Next wipe that finishes after a newer protection change no longer records its older hide_images value. - A server or profile switch shows the new identity's cached answer right away, and a superseded refresh releases its lock before its second request. - A failed watched toggle undoes only the fields it changed, and only while they still show, so a newer reload survives. - Shuffle post-roll uses the pick's own watch state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(detail): undo a failed watched toggle even after new progress A return refresh can overlay a newer playback position on the optimistic watched state. The rollback then skipped the whole snapshot and left the episode marked watched after the write failed. Restore played while it still shows the attempted value, and restore progress only while it still shows the reset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): pin spoiler writes to the profile that made them A toggle queued behind another write was sent with whatever auth scope was current when the request ran, so a server or profile switch in between wrote the old choice to the new profile. Capture the auth scope with the store's identity and pass it to every write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): pin the spoiler value read to the captured profile The refresh resolved both keys against whatever profile was active when the request ran, while its answer is cached and written back under the identity captured at the start. Pass the same captured auth scope to the effective-values read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(watch-party): apply spoiler protection to party previews The picker's confirmation page, the staged title in the lobby and the player panel, the invite page, and suggestion posters showed an unwatched episode's still and description even with protection on. They now follow the same rule as every other surface, from this profile's watch state and each artwork slot's provenance, on phone and TV. A title known only by type counts as an unstarted episode, as a suggestion does. The staged detail is read again when the room changes phase, so a title the party has played stops being hidden. Matches the Watch Party coverage in Silo-Server/silo-apple#559. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(watch-party): keep protected stills hidden while artwork changes The phone room backdrop crossfaded on the URL alone, so an outgoing protected still was drawn with the incoming artwork's treatment for the length of the fade. Each backdrop now carries its own thumbhash, poster blur, and hidden state through the crossfade. The TV invite page opened from the player left the staged backdrop unprotected. It now receives the spoiler decision, which every caller must pass, and that overlay re-reads the staged detail on phase changes like the lobby. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(watch-party): hide the current backdrop at once when protection turns on With each crossfade layer keeping its own hidden state, turning protection on for an unchanged artwork faded the sharp still out over 400 ms. A layer showing the current artwork now takes the current protection immediately; a different outgoing artwork keeps its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(spoilers): close three remaining protection gaps - Home and the TV detail page now clear resume progress when an episode is marked unwatched, as the server does. Before, the episode still counted as started and its still stayed visible until a refresh. - The phone Watch Party picker's shelf and search tiles hide an unwatched episode's still, not only the confirmation page. - A forced profile-verification recovery clears the spoiler store on phone and TV, like Switch Profile, so the next profile never starts from the previous profile's answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(spoilers): keep cast identities off the launcher and report failed saves - While a phone's cast identity is the TV's temporary scope, the launcher effect no longer seeds Watch Next and the sync worker skips, so the phone profile's titles never replace the TV owner's row. The TV profile seeds again when the cast ends. - The TV marquee and root backdrop crossfades show the current item's protected copy at once when protection turns on for the focused episode, instead of fading the sharp still and synopsis out. - A failed spoiler-switch save now explains itself in Settings on phone and TV, as title art and skip intervals do, instead of silently snapping back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): apply a protection change to departing marquee layers A protection change during a crossfade between two items left the outgoing item's sharp still and synopsis fading out. Each departing layer is now re-derived from its source item when the current preferences change what it shows, and left as is otherwise, so enrichment crossfades are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep safe enriched artwork when protecting departing layers underPrefs rebuilt an episode with no description on every call, since it compared the overview switch rather than whether a description was hidden. On the root backdrop that dropped the enriched series art. Protection turning on also rebuilt from the section item, losing an explicit series backdrop. Compare what is actually hidden, drop only what the preferences now hide (using the shown backdrop's provenance), rebuild only to reveal, and apply it only to departing layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): fence Watch Next writes and picker spoilers to this profile - The Watch Next sync now treats a phone's temporary cast identity as an invalid owner at every authority check, including the final write, so a handoff that starts mid-run cannot publish the phone profile's titles to the TV launcher. - The TV Watch Party picker decides spoiler protection from this profile's own watch state. Its shelves carry the room's progress for the progress bar, which let another member's progress reveal a still and left this profile's own started search results blurred. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): use this profile's watch state for poster cards too The party picker's own-progress override reached the backdrop and reference-shelf cards but not the default poster card, which still read the room's progress. Each row item now computes one unwatched decision and derives both the still and the poster protection from it. The Watch Next authority fence also runs the suspending identity lookup before validating the captured owner, so the owner is checked last. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Codex (gpt-6.1-sol) <noreply@openai.com>
…-Server#541) * feat(tv): add artwork and an On Deck row to the Up Next overlay The TV Up Next overlay was a plain black screen with a text panel, while tvOS shows the next episode's artwork behind it and an On Deck row of other items in progress underneath. Android TV now does both: - The next episode's still (else the first On Deck item's art) fills the screen, blurred and dimmed. Modifier.blur does nothing before Android 12, so the blur comes from a tiny decode under its own cache key. The mini-player pane is cleared from the layer so the video stays visible. - On Deck reuses the phone's projection of home continue-watching sections. Picking an item replaces the player with one for that item, which resumes from its saved position; the current session finishes as any exit does. Hidden in a Watch Party. - The overlay is one fixed screen. The video is a separate surface placed at the pane's bounds and cannot follow a scroll, so the panel is tightened (runtime on the episode line, two-line synopsis, Keep Watching beside Back) and On Deck sits along the bottom, five cards wide. - The row uses TvSmoothBringIntoViewSpec so focus arriving from the panel does not slide it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): resume On Deck picks from the saved position and redraw the cut-out The On Deck snapshot is taken when the current item starts, so passing its position as a resume override could rewind an item watched elsewhere since. The pick now opens without an override and the player reads the saved position. The artwork cut-out read pane coordinates from state, but layout hands back the same coordinates object each time, so a moved pane never redrew it. The pane's bounds are now held in state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): route On Deck picks by item type and let the panel scroll On Deck dropped each section item's type and always opened the video player, so an audiobook in progress would open in the wrong player. The pick now carries the type through tvPlayDestinationFor, as the home rows do. The fixed Up Next layout squeezed the panel's last controls when its content outgrew the space above On Deck (large font sizes, a shuffle's extra rows). The panel now scrolls on its own; the video pane beside it stays put. TvSmoothBringIntoViewSpec keeps it from scrolling on every focus move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep items TV hides out of On Deck The On Deck projection read raw home sections, so a book or other item TV hides could appear on the Up Next overlay. It now applies visibleOnTv first, as TV Home does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep the On Deck row through a startup replan A replan swaps the session ID for the same item without reloading On Deck, so keying the load on the session discarded the row. The generation and content checks already reject stale loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep safe next-episode art under spoiler protection Carry still_is_episode_still into the next-episode state so the Up Next backdrop shows artwork the server marks as not an episode still. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
…TV Home on view (Silo-Server#536) * feat(metadata): mark AI-translated descriptions and translate episodes and TV marquee on view Decode machine_translated_fields on item detail, season and episode rows and section/catalog cards, plus pending_translation_language on episode rows and cards. Show "Translated by AI" under machine-translated overviews on phone and TV detail pages, a compact marker on episode rows, and an icon in the TV focus marquee. The series page starts the shown season's translation job in auto mode when an episode row reports a missing description, and season pages refresh their episode list while the job runs. The TV focus marquee translates the item the viewer rests on in auto mode. Changing the profile's metadata language re-reads Home, For You and library lists. * fix(tv): translate on view only for Featured cards in the focus marquee The web client translates only the visible Featured slide. Cards resting in ordinary rows now keep their text and AI marker but no longer start translation jobs. * fix(tv): keep a marquee translation after leaving Home The marquee kept the translated description only in its own memory, so returning from a detail page showed the original text again. Re-read the Home rows when a translation lands. * fix(metadata): retry a season's translation after a switch, and mark only AI overviews Switching seasons while another season's translation was polling latched the new season as already translated, so its episodes stayed untranslated for the session on phone and TV. The new season's job now starts once the running one ends. The "Translated by AI" mark sits with the overview, but it showed for any machine-translated field, so an AI tagline alone marked a provider or manual overview as AI. It now requires the overview to be machine-translated. * fix: preserve translation refresh and episode state * fix: preserve translation retry and episode ownership * fix: invalidate translated detail caches on language changes * fix: bind language cache invalidation to its profile --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
Silo-Server#542) * feat(tv): ask who's watching on a new Android TV and offer to remember A new Android TV now starts with Profile Selection set to Every Time. After the first profile is picked, Silo asks once whether to open that profile automatically: Remember switches to Automatic, Ask every time keeps asking. A TV that already remembers a profile keeps Automatic. * fix(tv): keep remembered profiles on other servers and drop stale remember offers The first-launch settle looked only at the active server's profile slot, so a TV whose active server had no profile selected but another saved server did was treated as new: it saved Every Time and cleared that server's profile on the next switch. Count a profile saved on any server. The remember offer could outlive its profile. If profile-verification recovery cleared the profile, or the grid's identity changed, while the dialog was open, either answer still saved and opened Home with no active profile. Drop the offer on those reloads, and save or navigate only while the offered profile is still the active one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tv): keep a fresh remember offer and settle only from a readable registry Clearing the offer whenever a grid reload found the identity changed also erased the offer from a pick committed while that reload was in flight, since the commit itself moves the identity. The viewer was left on an empty picker with the profile committed. Drop the offer only when its profile is no longer the active one. An unreadable server registry reports no servers, so the first-launch settle took an existing TV for a new one and saved Every Time for good. Skip the settle until the registry loads. Mark the launch resolved only after settling, so an Activity recreated mid-settle still settles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ilo-Server#551) Read and decode Home responses and the Room Home cache off the main thread, show rows before writing the cache, let Home's first load join the startup warmup's read when no membership change has been acknowledged, and skip the TV resume refresh while the first load runs. Closes Silo-Server#549 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 upstream commits since eb1e2ed. Rebrand-aware merge by Prairie-Server/.github upstream-sync/prairie_sync.py: the 3-way base is rebrand(merge-base), theirs is rebrand(upstream), and the second parent is the real upstream commit. The tool auto-resolved the brand/format-only hunks. The remaining 35 files were resolved by hand, keeping both sides: - Live TV stays reachable: phone ProfileMenu/MainScreen, TV TvRoute and TvMainShell, alongside upstream's Watch Party. - Upstream removed QualitySelector (replaced by PlaybackQualitySheet). Prairie's quality ladder is now the fallback when the server plan has no quality menu, on both phone and TV. - Upstream's Settings re-layout removed ServerInfoSection and AccountSection. Prairie's Updates row and the Quick Connect row now live in SettingsScreen, and the invariant anchors are re-pointed. - Stats-for-nerds content is rebuilt inside upstream's PlayerMenu layout. The ThumbhashImage format cascade is kept with upstream's failed flag. LAN-discovery first-run is kept on phone and TV. - Fixed the DI arity for ServerListViewModel, and the silo_wordmark references to prairie_wordmark. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Upstream's new marquee referenced R.color.silo_icon_background, which only exists as prairie_icon_background in Prairie. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WalkthroughWarning Review details and warnings were omitted to fit the comment limit. |
Upstream ca53cb0 (Silo-Server#381) retired the command; the test kept asserting it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The merge left upstream's new call sites rebranded (toPrairieCastVideoFillMode, toPrairieCastSubtitlePosition) while the private helpers kept their Silo names, so the TV app would not compile. Name them all Prairie. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cope in CollectionsViewModelLoadOrderTest Upstream's player restyle dropped the HudBody* constants and the tv MaterialTheme import that Prairie's playback stats block still uses. The collections load-order test leaked an in-flight viewModelScope load past resetMain, surfacing as UncaughtExceptionsBeforeTest in the next test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Upstream dropped Kover, so the 83-commit merge brought untested code that took :shared to 94.4%. Add tests for the pure Watch Party presentation, playback-context and request-target-summary logic, and exclude live I/O glue (OkHttp, websocket frame helpers, server identity probe) like the other realtime clients already are. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tMain cancel() alone let the in-flight Ktor call resume on a reset Main, which surfaced as UncaughtExceptionsBeforeTest in LiveTvViewModelTest. 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.
Replaces #37. That branch had 25 "restore what the re-merge dropped" commits and red CI. This one is a fresh merge made with
prairie_sync.pyfrom Prairie-Server/.github#1: the 3-way base is rebrand(merge-base), theirs is rebrand(upstream), and the second parent is the real upstream commit726684d. Land with "Create a merge commit".The 35 files the tool couldn't auto-resolve were resolved by hand, keeping both sides.
check-prairie-invariants.sh: all 78 hold.audit: no conflict markers.Prairie features carried across upstream refactors
TvRouteandTvMainShell, next to upstream's Watch Party.QualitySelector(feat(player): show the server quality menu on phone and align the TV picker Silo-Server/silo-android#495, replaced by PlaybackQualitySheet). Prairie's quality ladder is now the fallback when the server plan has no quality menu, on phone and TV. Please review both paths.SettingsScreen.kt, and the invariant anchors are re-pointed.failedflag. LAN-discovery first-run is kept.ServerListViewModelDI arity, andsilo_wordmark/silo_icon_background→ theprairie_*resources.Judgement calls
PrairieDialog/PrairieDialogAction, which have no icon slot: cast, downloads, requests, server list.notice/errorbehaviour upstream removed. Others were trimmed to the new API.SignOutTeardown(Settings → Sign Out) doesn't resetLiveTvFeatureStore; only the profile-menu sign-out does.silo_*prefix (silo_title_art,silo_profile_launch, …), matching Prairie's existing persisted keys.Not built locally (memory limits on the sync host). CI is the build gate.
🤖 Generated with Claude Code