Commit Graph
1045 Commits
Author SHA1 Message Date
edde746 c84b12eacd fix(livetv): allow cold MediaBrowser tuners time to open
Emby and Jellyfin Live TV could fail before playback when opening a cold tuner took longer than ten seconds. Use the existing thirty-second tune timeout only for live PlaybackInfo requests that open a source, preserving other request budgets and transport aborts.

Cover delayed initial tuning and recovery, bounded cancellation without replay, and unchanged VOD and metadata timeouts.

close #2274
2026-09-07 20:32:16 +02:00
edde746 e727f5bef4 fix(emby): negotiate MPEG-TS HLS for live TV
Emby Live TV can fail to parse fMP4 HLS fragments, and retrying the same negotiated URL cannot change the segment container.

Restrict Emby Live TV negotiation to the existing MPEG-TS profile on initial tune and direct-play recovery. Preserve direct play, stream copy, Jellyfin negotiation, and Emby VOD. Cover transport scoping and recovery with protocol regressions.

close #2273
2026-09-07 20:32:16 +02:00
edde746 048bf688b2 fix(player): await terminal playback reporting before desktop exit
Closing the app during video playback left the backend session alive until it timed out. Await shared shutdown completion and the terminal video report before closing server clients, while keeping exit bounded when teardown stalls.

close #2275
2026-09-07 20:09:12 +02:00
56b990c04c feat(jellyfin): report the platform in the MediaBrowser client name (#2261)
Jellyfin and Emby sessions expose Client and DeviceName but no platform,
so dashboards and session trackers (Tracearr's normalizeClient, for one)
keyword-match the Client string the way they do for "Jellyfin Android TV"
and "Swiftfin tvOS". Plezy sent Client="Plezy" on every platform, so every
install showed up as platform "Plezy", and an unresolvable device name
became Device="Plezy" as well.

Add jellyfinClientName, which appends the platform ("Plezy Android TV",
"Plezy tvOS", "Plezy iOS", ...), and jellyfinDeviceName, which falls back
through the hardware model and the platform before the app name, and use
both at the two Jellyfin/Emby header call sites. Every emitted client
string was checked against Tracearr's matcher. The Plex headers and the
DisplayPreferences client key are unchanged.


Claude-Session: https://claude.ai/code/session_0134eXvhukgvG6Szg5NTwAim

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 01:39:07 +02:00
edde746 8c5d0b4213 fix(seerr): refresh authority without crossing session ownership
Permission changes could be confused with expired authentication, and delayed session persistence could cross a profile boundary.

Refresh raw authority after route denials and foreground resumes without login or request replay. Fence refreshes to the current binding, serialize persisted session ownership, and reconcile open request surfaces after grants and revocations.
2026-09-06 20:21:11 +02:00
edde746 89b326e3f3 fix(catalog): retain native titles within the bounded matching budget
Native-title library copies could be missed when only display and romaji titles reached the bounded search budget.

Prefer typed Japanese titles, preserve row and detail aliases, and keep matching within the existing four-query budget and external-ID verification rules.
2026-09-06 20:21:11 +02:00
edde746 cf0dd0607e fix(profiles): keep playback preferences bound to their authenticated account
Picker ordering could select another account for playback preferences, and delayed operations could publish data after the active account changed.

Resolve playback authority independently of the account picker and fence repository, controller, and inline settings operations to their original authenticated account.
2026-09-06 20:21:11 +02:00
edde746 9a44f062a6 fix(network): own connection races and rearm exhausted push channels
One stalled DNS family or a cancelled WebSocket upgrade could keep a connection attempt alive. Push channels also stayed exhausted after the app returned to the foreground.

Race address families independently, own connection tasks through upgrade handoff, and rearm exhausted push channels on a genuine foreground resume.
2026-09-06 20:21:11 +02:00
edde746 647971a5d6 fix(seerr): follow permission grants and revocations on open request surfaces
A Seerr permission change reached the app only as a sign-in-time snapshot: a catalog detail decided once, when opened, whether it could show Request, and the request sheet only re-read the mask when something else rebuilt it. A grant left the action hidden until the screen was reopened, a revocation left it visible, and submitting then surfaced Seerr's raw English 403 body while the sheet stayed open with gates it no longer had. A disconnect while a detail was open left it holding a source wrapping a disposed client.

Permission changes now propagate through the existing provider graph without replacing clients: the client adopts the fresh `/auth/me` body it already fetches while telling a permission miss from a dead session, and a live-session denial is a typed `SeerrPermissionException`. The detail screen derives its Request eligibility from the account's current mask and source on every build; the sheet reconciles grants (loads destinations, shows 4K) and revocations (trims them, or closes with a localized reason the host surfaces) and re-checks authority locally before submitting.
2026-09-06 15:25:49 +02:00
edde746 c20a6cf162 fix(catalog): keep a library copy's label through a retry that lost its stamp
A catalog detail's library labels disappeared for the rest of the session after a re-lookup: the matcher replaced each answering server's copies with the raw items of the new wave, so a Jellyfin retry whose best-effort ancestors stamp failed overwrote a labeled copy with an unlabeled one, and the unlabeled copy was then memoized as the authoritative hit.

Membership stays the answering server's to define, but every copy it returns again is folded onto its predecessor through the shared merge policy, which now treats library id and title as a pair: a copy naming a different library takes the new id with its own (possibly null) title instead of inheriting the old library's name, one naming the same library fills a missing title, and one naming no library keeps the known pair.
2026-09-06 15:00:27 +02:00
edde746 01750ad92b fix(network): own a WebSocket connection attempt through cancellation and TLS
Cancelling a WebSocket connect (deadline, stop, dispose, a newer attempt)
only dropped or timed out a future: the shared never-closed HttpClient kept
the address race and TLS handshake alive, and once SecureSocket.secure had
detached the raw socket even a real task cancel was a no-op, so after
ClientHello the peer stayed open and the connect future never settled.

The happy-eyeballs task now owns the transport through the TLS handoff:
cancel is idempotent, settles the task at once, destroys an undelivered raw
winner, and destroys whatever a handshake in flight eventually yields. Each
WebSocket upgrade runs on its own HttpClient inside a connection attempt
with completion plus cancellation; the library-event, companion-remote and
Watch Together relay sockets cancel it on stop, disposal, deadline and
supersession. dart:io offers no handle to abort an in-flight handshake, so
a black-holed ClientHello closes when the handshake settles, not at cancel.
2026-09-06 13:28:51 +02:00
edde746 5de2d28166 fix(seerr): only a rejection Seerr itself emits may invalidate a session
A JSON-bodied 401 or 403 from a gateway or auth proxy in front of Seerr was
read as a Seerr session rejection, so a valid Quick Connect session was
unlinked by a Cloudflare or forward-auth wall, and the /auth/me confirmation
probe was never checked for the same.

One endpoint-aware classifier now recognizes only the two rejection shapes
Seerr's middleware and error handler actually produce (403 with their exact
bodies; the forwarded Jellyfin 401 on /auth/jellyfin) and is applied to the
primary request, the identity probe, the post-reauth retry, login and the
post-login identity read. Anything else keeps the stored credentials and
surfaces as an intermediary failure; genuine expiry and credential rejection
keep their re-auth and unlink behavior.
2026-09-06 13:24:52 +02:00
edde746 deeec6810d fix(library): never let merged library events narrow their refresh coverage
Coalescing library-change frames could lose libraries: a frame that could
not name its libraries (whole server) merged with a frame naming {A} flushed
as {A}, and an event naming physical Jellyfin folders where only some
resolved to a loaded library bumped the resolved one and silently skipped the
grouped view the rest belonged to.

Whole-server scope is now absorbing in the socket's pending window, and the
provider widens to every library on the server whenever any named id fails
to resolve; a fully resolved id set stays precise.
2026-09-06 13:16:51 +02:00
edde746 e660a06499 fix(watch-together): seed a fresh host's room rate from its saved speed
A host that created a room started the room at 1.0x while its own player later
moved to the saved playback speed: the coordinator seeded the room rate from
the player at attach, but attach runs before the track-selection pass that
applies the saved speed, and rate intent is (deliberately) no longer inferred
from the player's rate stream. Guests then kept correcting against a rate the
host was not running.

The screen now resolves the saved speed up front and declares it through
attachment; the coordinator seeds a fresh epoch with it before the loading
broadcast and applies it to the host player, keeps the room's agreed rate on a
same-item reload, and a promoted host without a broadcast rate falls back to
the declared one. While a room owns playback, track-selection passes no longer
reapply the local saved speed underneath it.
2026-09-06 13:11:28 +02:00
edde746 c228a12d1e fix(media): preserve profile, source, and focus identity across refreshes
Library refreshes could move focus, catalog lookups could cross profile or query boundaries, and detail labels could describe a different source from playback.

Keep hub and grid focus with committed item identities. Pace pushed deletions without delaying local eviction. Bind push channels to committed authentication sessions and catalog completeness to the active profile and effective query. Resolve preview source and container defaults with playback selection rules.

The shared client contracts and all consumers migrate together. Include regression fixtures and document safe host-transfer compatibility.

Verified: 6826 Flutter tests passed, 5 skipped; aggregate quality checks and final analyzer/formatting checks passed. All 86 owned paths match the isolated validated snapshot.
2026-09-06 04:34:58 +02:00
edde746 dca8029a99 fix(settings): normalize legacy skip modes across persistence boundaries 2026-09-06 03:54:43 +02:00
edde746 3fb8794619 fix(player): dispatch newer live seeks after a failed reopen 2026-09-06 03:50:26 +02:00
edde746 98c6a09b41 chore(player): remove temporary native playback diagnostics
Production playback still carried recurring Windows HDR probes, Android subtitle profiling, and verbose mpv logs with debug logging disabled.

Remove the Windows probe, pin the published mpv-build profiler cleanup, and honor Android debug logging preferences for video and music while preserving warnings and errors.
2026-09-06 02:30:08 +02:00
AlbertandGitHub d2d73f35f8 fix(jellyfin): send the token as ApiKey so Jellyfin 12 accepts trickplay, subtitle and socket URLs (#2252)
Jellyfin 12 ships with EnableLegacyAuthorization=false, which drops the
legacy `api_key=` query spelling (jellyfin/jellyfin#15559). Every
authenticated URL Plezy self-authenticates via the query string then
fails with 401: trickplay sprite sheets (visible as missing scrub
thumbnails), transcoding/subtitle/Live TV URLs built through
_withApiKey, and the library-event websocket (403 on upgrade).

`ApiKey=` is read unconditionally by Jellyfin 10.8 through 12, while
Emby only accepts `api_key=`, so the parameter name now comes from
MediaBrowserDialect.tokenQueryParam. Emby output is byte-identical.
Image URLs keep `api_key`: Jellyfin serves item images without
authentication, and the artwork cache keys strip that exact name.

Fixes #2247
2026-09-06 01:21:51 +02:00
edde746 d6f0aa8936 fix(catalog): keep library copies held by a server that was never asked
A title's copy disappeared from a catalog item's library matches when the
server holding it went offline, and the screen then claimed the title was not
in the library at all.

The reverse-lookup fan-out only reaches online clients, so a registered
server that is offline lands in neither the succeeded, failed nor cancelled
set. The fold read that absence as "left the account" and dropped the
server's verified copies. Worse, the surviving wave looked complete, so it
was memoized for the rest of the profile session and never asked again. With
no server online at all, every server's copies were erased and the detail
screen asserted "Not in your library" over servers nobody had queried.

LibraryLookupResult now names the registered servers a wave could not even
reach, which needs a registered-server set on MultiServerManager because an
auth-rejected Plex server holds no client at all. The fold's rule is stated
positively -- only a server that answered may replace its own entry -- a wave
that skipped a server is never memoized past the TTL, and the detail screen
counts those servers as unchecked alongside the ones that failed.
2026-09-06 00:53:55 +02:00
edde746 65af57dd93 fix(playback): preview tracks from the same media source the player will use
The detail page's audio/subtitle preview could contradict playback on Jellyfin and Emby: a server default of -1 (subtitles off) previewed as a subtitle track, and a missing default with a container-default subtitle previewed as on where playback plays none (#1779 again).

The preview rebuilt a MediaSourceInfo from the item's version by hand, a third mapping beside the two the backends use for playback, and had already diverged twice. It now runs the ladder over the MediaSourceInfo the backend maps for playback, fetched from the metadata cache in the existing probe, and the hand-built mapping is deleted.
2026-09-05 23:14:12 +02:00
edde746 bc483a8206 fix(catalog): keep a server's verified library copies when it sits out a later lookup wave
A catalog item that had a copy on server A lost it from the detail page when a later refresh could not reach A: partial results expire after the negative TTL and the fresh wave replaced the cache as a unit, so A failing while B answered empty left nothing, with no evidence A had removed anything.

The cache now holds per-server answers with one invariant: a server's answer is replaced only by that server. Servers that failed or were cancelled keep their last-known copies, servers named in no set have left the account, and the failed set still passes through so the outage stays visible and the wave is retried.
2026-09-05 23:14:11 +02:00
edde746 c0d090079b fix(library-events): close a websocket whose upgrade lands after the connect deadline
A library notification connection that timed out could still complete later and stay open after the owner was disposed. IOWebSocketChannel.connect applies connectTimeout with Future.timeout and drops the pending connect, so nothing could ever close a socket that finished late.

The channel factory now resolves only to established channels: it owns WebSocket.connect, applies its own deadline, closes a late upgrade, and returns a channel built from the resolved socket. The socket class no longer holds half-open channels, which also removes the connected-flag workaround around the library's close semantics.
2026-09-05 23:14:11 +02:00
edde746 c5968029e0 fix(plex): send metadata type on Discover guid match so library items resolve for the watchlist
"Add to Watchlist" disappeared from library card menus after the first open, and
the detail-screen bookmark never appeared, for every Plex movie and show (#1873).
Plex Discover's `/library/metadata/matches` answers a `guid` lookup only when
paired with the numeric metadata `type`; a bare guid returns an empty container.
The empty result was cached as "no source can hold this item", hiding the entry
on the next open.

`PlexDiscoverClient.match` now takes the item kind and sends `type=1` (movie) or
`type=2` (show) alongside the guid; other kinds return null without a request.

Fixes #1873
2026-09-05 15:15:19 +02:00
edde746 cd22475cb6 fix(plex): send the media type to Discover's matches endpoint so external ids resolve
Adding a Trakt, MAL or library title to the Plex watchlist failed with "no
rating key": /library/metadata/matches answers an empty container unless
`type` is sent, so every external-id resolution against Discover came back
empty. The client now passes the movie/show type with the guid.
2026-09-05 14:44:02 +02:00
edde746 bc13ff007a perf(explore): resolve library copies with one guid filter and a native-title search per server
The Explore library lookup spent up to eight search-index queries per
server per tap, plus one children fetch per candidate for sequels, and the
Trakt path added two alias/translation requests per detail open to reach
romaji and localized library titles.

Both backends index originalTitle on every copy of a foreign title whatever
language it is filed under, so the lookup now leads with the native title
and drops alternate titles as candidates: one query reaches the English,
romaji, localized and native copies alike. Plex's /library/all?guid= filter
is a literal prefix match with comma as OR, so every legacy-agent guid form
the external ids imply rides one indexed request alongside the Discover
guid, and the sequel gate asks for the season of every candidate in one
/library/all?type=3&show.id=…&season.index=N request. Worst case is five
concurrent requests per server, typically two or three, with no cloud call.

The Trakt alias/translation fetch is removed: the copies it reached are the
ones the native title reaches, and Trakt already sends original_title.

close #2098
2026-09-05 14:44:02 +02:00
edde746 29a6286fe6 fix(linux): suppress Steam Input duplicate actions
With Steam Input enabled on Linux, every controller press acted twice:
Steam's desktop layout injects arrow/Enter/Escape keys through a uinput
keyboard while the physical controller stays readable over evdev, so
GamepadService synthesized the same key a second time. The duplicate
guard that fixed this on Windows was gated to Platform.isWindows and its
native key handler was never registered on Linux. Enable both on Linux.

close #1694
2026-09-05 14:44:02 +02:00
edde746 6815850b61 feat(explore): match Trakt items to romaji and localized library titles via Trakt aliases
A Trakt item only carried its English title, so a Plex or Jellyfin library
filed under the romaji title (`Sousou no Frieren`), the user's language
(`Frieren: Tras finalizar el viaje`) or the native title stayed invisible to
the Explore library lookup.

The Trakt detail load now fetches /aliases and /translations/{app locale}
alongside people and related, picks one ASCII alias from the item's own
country plus the locale translation as alternate titles, and the matcher
reads original_title for every source. The lookup budget grows to four
title families, the matcher keys its cache on the candidate titles, and the
detail screen re-resolves when a detail load adds titles, not only ids.

close #2098
2026-09-05 14:44:02 +02:00
edde746 a2c9bea041 fix(explore): find library copies through the Plex search index and report servers that could not answer
Explore showed "Not in your library" for titles the user owns. Two causes:
Plex's /library/all?title= filter is an ordered word-prefix substring match,
so `Oshi no Ko` never found a library titled `[Oshi no Ko]` and a romaji or
localized title was unreachable once the first candidate hit; and a server
that answered slower than the 10s header budget was treated as a dead
endpoint, dropped from the wave, cached as a negative for ten minutes and
cascaded through its stale LAN candidates.

Plex title candidates now go through /hubs/search with includeGuids, the
full-text index behind Plex's own search, which also covers originalTitle.
Both backends search every candidate concurrently and union the id-verified
copies instead of stopping at the first title that hit; the candidate cap
becomes two title families so a sequel can reach its alternate title. Lookup
requests run under a dedicated deadline with endpoint failover off. The
aggregation layer reports per-server failures, the matcher only memoizes
complete positive waves, and the detail screen shows "Couldn't check N
servers" instead of claiming absence.

close #2098
2026-09-05 14:44:01 +02:00
edde746 d2fe21b169 feat(player): per-marker skip modes with an Off option for intros and credits
Turning auto-skip off still surfaced a skip button on every intro and
credits marker, so a viewer who wants to watch episodes in full had no
way to silence it. The two auto-skip switches become per-marker
selectors — Off, Show button, Automatic — stored as skip_intro_mode and
skip_credits_mode. Off filters the marker before it becomes current, so
the player behaves as if the server had sent none: no prompt, no
countdown, no TV autofocus, and Back reaches the screen. Flipping a kind
to Off while its prompt is up drops the prompt immediately.

The legacy auto_skip_intro/auto_skip_credits booleans migrate on first
read: true becomes Automatic, false becomes Show button, matching what
each meant before.

close #2138
2026-09-05 04:18:36 +02:00
edde746 f45eaf1367 feat(library): add a toggle to hide watched indicators on cards
Watched posters always carried the corner checkmark, and viewers who
find it noisy had no way to turn it off. A new Appearance switch,
Show Watched Indicators (default on), gates the checkmark on every
surface that stamps watch state onto artwork: grid cards, hub rows,
folder tree rows, episode and playlist thumbnails. Progress bars and the
unwatched-count pill keep their own behavior.

close #1998
2026-09-05 04:18:35 +02:00
edde746 4f9e527ef2 feat(tvos): default audio passthrough on for Apple TV
Apple TV shipped with passthrough opt-in while the E-AC-3/Atmos
sample-buffer renderer was unverified, so Atmos titles played as PCM
until the viewer found the switch. The renderer is now hardware-verified
on real receivers, so the pref defaults on for Apple TV the same way it
does for Android TV. An explicit stored value still wins.

close #1300
2026-09-05 04:18:35 +02:00
edde746 5efc4f384c fix(plex): honor the quality preset on Live TV instead of leaving the server to pick the tier
Playing a Plex live channel from outside the LAN always came back as a full
re-encode at the server's highest transcode tier (20 Mbps 1080p on the
reporter's server), no matter which quality the app was set to. The Plex live
path never sent a ceiling: a live source has no bitrate the server can check
against its remote-stream limit, so a remote session without a client cap
falls onto the server's own top tier. On the LAN the server remuxes anyway,
which hid the gap.

The live stream path now reads the same saved preset the library path and
the Jellyfin live path (#2198) already honor. Original is unchanged: a remux
(directStream=1) with no ceiling. A capped preset pins directStream=0 and
sends the bitrate limitation clause plus the videoResolution/videoQuality caps
so the encode lands at the chosen tier rather than the server's. The preset is
session state, so a recovery re-tune keeps the cap.

directPlay stays 0: a tuned Plex session is only reachable through the
transcoder's HLS output, so "no re-encode" on live means a remux, not direct
play. Capped sessions use the h264-only TS target because every codec in the
target becomes an encode output once directStream is off, and HEVC into TS is
the #1859 corruption; the broadcast-codec live target remains the Original
remux menu.

close #2072
2026-09-05 04:15:17 +02:00
edde746 12a380807d fix(live-tv): anchor the live clock on the playback transcode's server origin
A skip back of 10-20 s on a freshly tuned Plex Live TV channel landed on
or after the frame being shown, while the same skip worked once the viewer
had already time-shifted. Offset-less opens (initial tune, retry, channel
zap, subtitle switch at the edge) pinned stream position zero to wall clock
at open time, but the transcode starts behind real time by tuner ingest and
encoder start-up latency; offset opens were exact because the server
defines their origin.

Every /:/timeline response already carries the playback transcode as the
top-level TranscodeSession next to the CaptureBuffer wrapper; its timeStamp
is the epoch of stream position zero, which Plex's own client uses as the
playhead origin. The parser used to return only the capture window. It now
returns both, and each heartbeat re-anchors the clock on the playback
origin unless the response predates the current open or an open is still
calibrating. Offset-less opens seed a provisional anchor from the capture
edge instead of wall clock, and the live-edge threshold is widened to 15 s
so a live stream trailing the edge by normal latency still reads as live.
2026-09-05 01:51:03 +02:00
edde746 af9fb3f17c fix(live-tv): calibrate replacement stream clocks 2026-09-04 16:49:55 +02:00
edde746 fa8da33ec8 fix(tv): drop a stale Discover rail-focus claim, log swallowed tvOS Menu presses, and unblock shelf test resets
Three findings from the #2239 investigation.

Discover armed "focus the browse rail when it has hubs" on its first
load and kept it armed indefinitely while the rail had nothing to show.
Hubs landing minutes later — reconnect, push refresh — then yanked the
remote off a sidebar item the user had since moved to. The claim now
lapses once focus sits on a control outside the screen; a bare content
scope (the startup state) still lets the rail take its initial focus.

On Apple TV a Menu press that reaches MainScreen at the home root is
consumed silently by design (the engine normally hands root Menu to
UIKit). That branch is what turned a focus slip into a "dead remote"
report, so it now logs passthrough state, picker state, and the primary
focus label.

SystemShelfService.debugReset awaited the mutation tail queued by the
previous widget test, whose FakeAsync zone ends without flushing the
microtasks that settle the chain, so any second test in a file using it
hung in setUp. It now drops the queue synchronously; the #2239 session
regression test moves back beside the profile-switch test it belongs
with.
2026-09-03 23:54:09 +02:00
edde746 e46b3dab7c fix(seerr): diagnose an auth proxy in front of Seerr instead of misreporting it
A Seerr instance behind forward-auth (Authelia, Authentik, Cloudflare
Access) or HTTP Basic failed setup with "No Seerr instance at … (HTTP
200)": the probe followed the proxy's redirect to its login page and
decoded the HTML as "not JSON". A live session that later hit the same
wall took the proxy's 401 as Seerr's, re-authed through the wall, failed,
and unlinked a perfectly good stored session.

Seerr's API never redirects and always answers with JSON, so a 3xx or a
non-JSON 401/403 is the proxy talking. Seerr requests no longer follow
redirects, that shape maps to a SeerrProxyException with a message that
says what to do, the probe reports it in place of "no instance", and the
client neither re-auths nor unlinks on it.

close #1877
2026-09-03 21:42:47 +02:00
edde746 e67bf996c5 fix(player): keep subtitles on the picture when ambient lighting is on
With ambient lighting enabled, ASS subtitles anchored to a corner (and PGS
bitmaps) rendered against the whole screen instead of the video: the effect
stretches mpv's frame to the window with video-aspect-override and lets the
shader composite the real picture inside it, and mpv places subtitles against
that stretched frame.

The pinned libmpv now carries --sub-video-rect-aspect (edde746/mpv-build
ec08018), which makes mpv derive its subtitle margins from the picture's real
aspect inside the displayed rect. AmbientLightingService hands it the video
aspect before overriding the frame, so a libmpv without the option refuses
cleanly with the frame untouched, and resets it on disable.

Verified on a Pixel 7 (2400x1080) with a 16:9 file whose OP credits are
\pos-anchored: subtitle extents are pixel-identical with ambient lighting on
and off, and mpv reports OSD borders l=240 r=240 for the overridden frame.

close #2120
2026-09-03 21:28:10 +02:00
edde746 a230d51598 fix(plex): keep a LAN session on its local endpoint after device sleep
Apple TV (and any suspended process) wakes with dead keep-alive sockets in
the HTTP pool. The first request after resume — the health probe — failed
with a connection error, and the failover cascade treated that as a dead
endpoint: it validated the remote candidate, switched to it, and persisted
it as preferred. Nothing walked the session back to the local endpoint,
because the only re-optimization trigger is a connectivity event and a
same-interface sleep/wake never produces one. Only killing the app fixed it.

FailoverHttpClient now runs the existing candidate trust gate against the
current endpoint on a connection error and retries in place when it answers;
timeouts and 5xx still cascade directly. MultiServerManager gains
reoptimizeDemotedServers, called from the resume probe, which re-races any
online Plex server sitting on a remote or relay endpoint while a local one
is published.

close #2056
2026-09-03 18:13:28 +02:00
edde746 e4f3888694 fix(seerr): seed advanced requests from the Sonarr anime defaults and expose tags
Requesting an anime series with REQUEST_ADVANCED routed it to the standard
Sonarr profile and root folder. The sheet seeded its pickers from
activeProfileId/activeDirectory only, then posted them as explicit overrides,
which beat the anime defaults Seerr would otherwise apply at approval. A user
without the permission sent no overrides and was routed correctly.

The service DTOs now carry activeAnimeProfileId, activeAnimeDirectory,
activeAnimeLanguageProfileId, activeTags, activeAnimeTags and the instance's
tag list. The sheet detects the TMDB anime keyword on the TV details it
already fetches and seeds each picker the way Seerr's web requester does:
the anime value when the series is an anime and the instance configures one,
else the standard value. Options carry the "(Default)" marker and an anime
series shows the same note as the web UI.

Tags are editable: an inline checklist under the advanced pickers, seeded from
the instance's (anime) default tags and posted as `tags`. It is inline rather
than a nested sheet page because the host builds only the top page, so a push
would dispose the sheet and drop the season and picker selections on the way
back.

close #2215
2026-09-03 16:46:27 +02:00
edde746 42b6710f04 fix(player): stop letting item metadata language override the audio track
The audio ladder still carried a per-item language tier between the
server's selected stream and the account preference. For Jellyfin and
Emby the value mapped to it was `PreferredMetadataLanguage`, which is the
library's metadata-scraping language - it inherits from the parent, the
library options, or the server config and says nothing about playback -
so on any source whose selected/default stream did not map to a native
track it silently picked audio in the scraping language instead of the
user's preferred one. Plex's item-level `audioLanguage` reached the same
tier, after 2dfebf32b and 7ae293631 had already settled that the server
folds every preference level into the stream `selected` flags.

Drop the tier and the plumbing that only fed it:
TrackSelectionPriority.perMedia, MediaItem.audioLanguage and
subtitleLanguage, PlexMetadataDto.audioLanguage/subtitleLanguage/
subtitleMode, and the Jellyfin PreferredMetadataLanguage mapping. The
order is now carried selection, server-selected or default source
stream, account preference, native default. Persisted cache entries
holding the old keys decode unchanged.
2026-09-03 13:08:46 +02:00
edde746 c4be378219 fix(detail): make the track status read-only and right-align it
The action row's track status was a button that opened a pre-play
chooser, but on TV the hero describes whichever episode the rail has
focused, and focusing the row hands the hero back to the show, so there
was never a per-episode target for the control to act on. It also sat
inline with the buttons in muted grey, reading as a sixth, low-contrast
action.

The status is now plain text at the row's far end, right-aligned to the
hero's text column, in the hero's own ink, off the focus path. The
chooser sheet, the per-screen choice, and the Play pass-through go with
it, along with the overlay-host and track-tile changes that only served
the chooser.
2026-09-03 13:03:24 +02:00
edde746 f967b9b2f2 feat(detail): preview and choose the audio and subtitle tracks before Play
The detail screen gave no hint of what pressing Play would do with the
file: which audio track and which subtitles the player would start with,
or the picture format. Changing tracks meant starting playback first.

The action row now ends with a status line — picture labels, then the
audio and subtitle rows the player will select — computed by the player's
own selection ladder (TrackSelectionService) over the server's stream
rows, so it agrees with what the player then does. Select opens a
pre-play chooser with the track sheet's two columns; Play and rail
activation pass the pick to the player as its navigation-tier preference,
and a pick carries across episodes by semantics, forced-ness included.
Plex listings carry no stream rows, so a focused episode is probed once,
debounced, and cached for the screen.

Along the way: MediaStream keeps the server's `selected` apart from the
container `default` flag (they were OR'd, so the account's pick was
indistinguishable from the default track); the overlay sheet host no
longer overrides a row that claimed focus itself.
2026-09-03 13:03:13 +02:00
edde746 51beb5ac4b fix(player): leave Plex subtitle auto-selection to the server
Plex Web reads only the `selected` flag PMS stamps on each stream; the
account's autoSelectSubtitle is applied server-side when that flag is
computed. Plezy's Priority 2 already trusts the same flag, but once
playback read AccountPreferences the Plex mode became non-null and could
reach the client-side profile pass whenever there was no media info or no
embedded subtitle streams - forcing subtitles off for Plex's 0 ("manually
selected") or on for 2, over a decision the server had already made.

TrackSelectionService now skips the profile subtitle mode for Plex items;
MediaBrowser keeps it because those servers do not pre-select.
2026-09-03 11:37:12 +02:00
edde746 b43cd81102 chore(profiles): let AccountPreferencesController own the active user's playback preferences
UserProfileProvider kept a second cache of the active user's audio/subtitle
defaults beside AccountPreferencesRepository, re-implemented the active
profile/connection listener wiring and Plex Home token resolution, and for
Plex Home profiles bypassed the shared cache entirely (fetching /user with
its own client), so a write in the Account preferences screen never reached
Home-profile playback.

The controller now resolves the active account (the head of its sorted
account list), loads it through the one repository on attach, on profile
switch, and when the account's token changes, and exposes it as
activePreferences; ensureActiveLoaded replaces initialize() for the startup
gate. Playback, offline waiting, and logout storage clearing read the
controller directly; UserProfileProvider, the per-backend user-profile DTOs,
PlexAuthService.getUserProfile, and JellyfinClient.fetchUserProfile are
deleted.

The ranked defaultAudioLanguages/defaultSubtitleLanguages lists go with
them: Plex Web never reads them, PMS applies them when it stamps `selected`
on streams, and Plezy already trusts that flag ahead of the profile. They
only refined a best-effort audio fallback for Plex items with no media
info, which the primary language covers; on that path a second preferred
language is no longer tried before the file's default track.

Plex Home profiles now read /user/profile with the switched token through
the same cache as local profiles.
2026-09-03 11:36:56 +02:00
edde746 59f44d9ae1 chore(trackers,downloads): share rating request matching and download queue feedback
MDBList and Trakt repeated the rating type, entry matching, and request
body construction; tracker_rating_match.dart now owns them with the
service label passed in. The collection detail screen re-implemented
fetchAndQueueListDownload minus its cellular-blocked branch and now uses
it, and the three showDownloadOptionsAndQueue call sites share
queueDownloadWithFeedback, so the detail action buttons surface a queue
failure as an error snackbar instead of an unhandled async error.
2026-09-03 11:36:29 +02:00
edde746 7b1b0d766b chore: reuse drainPages, SerialFutureQueue, and FutureCoalescer for hand-rolled equivalents
Three MediaBrowser pagination loops re-implemented drainPages; it gains an
onPage hook (fires after intermediate pages only) so the two
progressive-render loops fit as well as the episode queue. Five
handwritten FIFO future tails (companion lifecycle, Seerr persistence,
tracker write lock, companion host-auth commit, Watch Together message
routing) become SerialFutureQueue, and the companion peer service's
disconnect/dispose in-flight joins become FutureCoalescer.
2026-09-03 11:36:29 +02:00
edde746 960adf42f2 perf(downloads): resolve cache scopes once per server and build one deletion plan
Loading the downloads list re-resolved the backend and profile scope for
every row, and the provider's bulk lookup only tried the public key, so
every MediaBrowser leaf fell back to a per-item lookup that resolved the
scope again (twice more for episodes and tracks). Startup was therefore
O(downloads x profile bindings) in database selects.

getAllPinnedMetadata now resolves each distinct server once, covers
queued and in-progress rows too, and returns the scope snapshot beside
the items; hydration tries the exact compound key, then the public key,
then the existing per-item fallback, and reuses the snapshot for parent
rows. findProfileScopeId prefetches bound connections in one select while
keeping binding precedence. A provider test pins the select count as
constant across download count.

Deleting a container also read the row, resolved metadata, and queried
its children twice; deleteDownload now loads them once and threads the
snapshot through the file deletion, keeping the cancel/progress/file/row
order intact.
2026-09-03 11:36:29 +02:00
edde746 e8e67df379 chore(plex): map metadata type codes and fetch detail metadata in one place
The movie/show/season/episode/artist/album/track to 1/2/3/4/8/9/10 table
was repeated in the query translator (forward and inverse), collection
creation, and the metadata-edit adapter; PlexMetadataType now owns
forKind/kindFor and each caller keeps its own unsupported sentinel.

getMetadataWithImagesAndOnDeck and _getMetadataWithImages shared the
endpoint, cache key, query flags, first-row parsing, and library-section
stamping and differed only by includeOnDeck; they are one private typed
fetcher returning (metadata, onDeckEpisode), so fetchItemWithOnDeck no
longer reads a string-keyed dynamic map.
2026-09-03 11:36:29 +02:00
edde746 54efd4fe73 fix(emby): stop leading the transcode codec list with AV1
Capping quality against an Emby server on macOS (or any device with an
AV1 decoder) failed with HTTP 500: Emby takes the first entry of the
TranscodingProfile codec list verbatim and has no AV1 encoder. Jellyfin
rotates admin-disabled codecs to the back, which is what the AV1-first
list relied on. Gate AV1 on that dialect behavior.

close #2230
2026-09-03 08:02:09 +02:00