Repository navigation
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>
…ere missed by git log)
…-only jobs; release title stays Silo Android
… ThumbhashImage; restore rebranded Fastfile UI strings
…act caused syntax error)
…rand-only on fork side)
…) rebranded; keep fork-local silo scheme/identifier conventions
JonahMMay
force-pushed
the
sync/upstream-2026-10-11
branch
from
October 11, 2026 18:21
e35e973 to
55a8b6b
Compare
…to sync branch; content adopted rebranded in 7fb9e06
JonahMMay
force-pushed
the
sync/upstream-2026-10-11
branch
from
October 11, 2026 18:21
55a8b6b to
0279c6c
Compare
…wnNames: apple treats it as unsupported)
…ks and class scope for accountNameOn (earlier sync mangled the tail)
…s-row modifiers into the header)
…pstream, transformed upstream) preserving fork siloCamel identifiers; resolve 46 conflicts keeping Live TV/Watch Together/PrairieCast/Profile Selection/stats
…the sibling-repo token)
…, keyboard refs, Watch Together members, settings divider, requests isEnabled/notice/actionInFlightId) and fix own-repo refs
…TV/requests surfaces) and fix TV shell/player compile breaks
…ingForCurrentViewer (fork playback-recovery surface)
…ck-recovery surface)
Author
|
Superseded by #41 (rebrand-aware merge). |
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.
Real merge of Silo-Server/silo-android upstream (80 commits) preserving upstream ancestry per docs/upstream-sync.md. Conflicts resolved with the established rebranding mapping; upstream additions placed at Prairie paths; upstream deletions honored; Silo Media L.L.C. attribution preserved. Merging only after CI (including Prairie invariants) is green.