Commit Graph
2383 Commits
Author SHA1 Message Date
edde746 938ddc5942 fix(startup): build the repair dialogs below the bootstrap MaterialApp
Tapping Repair on the startup-failure screen always threw: the repair flow received the gate State's own context, which has no Navigator, MaterialLocalizations, or ScaffoldMessenger, so both the confirm dialog and the fallback error snackbar raised instead of rendering. Thread the bootstrap home builder's context (below the MaterialApp) into the repair flow and its failure snackbar.
2026-08-21 19:23:39 +02:00
edde746 ec530c6713 fix(jellyfin): refuse ambiguous bare-scope cache resolution across users
With two profiles bound to two Jellyfin/Emby users on one server, a legacy bare machine-id lookup (offline downloads, cached playback metadata) could resolve another user's cache row, serving their resume position and token-stamped image URLs. Mirror the write-path guard: when the bare scope matches more than one user, log and return nothing.
2026-08-21 19:23:39 +02:00
edde746 a53041a352 fix(playback): match preferred track language by ISO variation, not string prefix
A 2-letter preferred language (es, ar, ru, ...) prefix-matched unrelated 3-letter track codes (est, arm, rum), so profile-based audio/subtitle selection could pick a wrong-language track whenever it preceded the wanted one in container order. Route the matcher through languageMatches, which compares exact codes, region variants, and ISO-639 variations.
2026-08-21 19:23:39 +02:00
294419109b feat(player): dismiss the skip prompt on Back instead of exiting (#2049)
While a Skip Intro / Skip Credits prompt is up and the chrome is down,
Back walked the screen's staged chain and exited playback. On a remote
nothing else means "no thanks": left/right seek, OK takes the skip,
up/down raises the OSD. Declining an intro cost you the episode.

Adds a stage for it alongside the sheet and content-strip stages that
already run locally in the player controls. Gated on the same condition
as Select's skip path, off on phones (#1938), off without canControl,
and off while a screen-level prompt owns the key.

The claim is latched for the whole press: handleBackKeyAction acts on
the key-up, and the button's own 7s auto-dismiss can fire in between.

Co-authored-by: cajunflavoredbob <cajunflavoredbob@users.noreply.github.com>
2026-08-21 08:48:44 +02:00
edde746 61268fab73 fix(livetv): derive recording state from grid subscription attributes
Recording indicators never appeared in the EPG and the program sheet kept
offering "Record" for already-subscribed programmes, so rules were created
repeatedly (nine duplicate subscriptions in the linked report). Two defects,
both confirmed against the official web client bundle:

- The sheet's scheduled-state check called
  /media/providers/<identifier>/media/subscriptions/mapping/<ratingKey>,
  which 404s: PMS mounts that route under the numeric MediaProvider id from
  /media/providers. The check failed instantly and unconditionally on every
  platform, so the button always read "Record".
- The guide derived its dot solely from cross-matching grab metadata, and
  the grab refresh issued right after scheduling raced the server
  materializing the grab, wiping the optimistic key before one frame showed.

The grid response already tags subscribed airings with subscriptionID /
grandparentSubscriptionID - the signal the official client renders from.
LiveTvProgram now carries those attributes; the guide checks them first
(grab matching stays as the secondary signal) and keeps local
schedule/cancel actions authoritative until the next grid load. The sheet
resolves Record vs Manage from the tagged rule key without a network call,
falling back to the fixed numeric-id mapping route for untagged airings.
Grab parsing also accepts the nested airing under "Video", which PMS uses
for every non-scheduled grab status in JSON.

close #2009
2026-08-21 07:44:27 +02:00
edde746 54af977f39 fix(library): refetch content tabs when the app resumes from a long backgrounding
Opening Plezy on a TV box resumes the resident process rather than
cold-starting it, so the Libraries grid kept showing the in-memory
content from the previous session -- hours stale -- until the user
switched libraries and back. Nothing on the resume path refreshed tab
content.

MainScreen now latches genuine backgrounding and, on a resume more than
five minutes later, refreshes LibrariesProvider and sweeps the content
tabs through each screen's in-place Refreshable.refresh() -- skipped
while offline, before startup priming, during playback, or with no
connected servers. LibrariesScreen.refresh() now refetches the selected
library's loaded tabs (the toolbar refresh action) instead of
re-selecting the saved library, which never reloaded them because tab
widgets only reload when the library's globalKey changes.

close #2043
2026-08-20 18:19:03 +02:00
edde746 21f48382bc fix(hubs): derive wide hub-row cards from the grid's widening scheme
A continue-watching episode card was 180dp on home but 232dp in the
grid behind "see all" at 480dp: hub rows widened the resolved poster
cell (x1.5) while grids widen the max extent (x1.8) before the integral
column packing, and the TV shelf multiplied the rail's tall card by the
same 1.5. Rows now adopt the grid's packed wide cell via
MediaGridDelegate.wideCellWidth, the TV shelf passes the hub's wide
flag to TvBrowseRailLayout.cardWidthFor, and the single widening
scheme is stated in MediaGridDelegate. Grids are untouched.
2026-08-20 14:49:33 +02:00
edde746 424c14a344 fix(tv): size HubSection shelf cards with the browse rail formula
Live TV "What's On" and catalog related rows are the two TV surfaces
without a TvBrowseRail path; HubSection's own clamp ([210, 340],
unscaled) rendered ~40% larger cards than every neighboring rail and
gave 720p TVs the same 210dp floor as 4K. Delegate the shelf card
width to TvBrowseRailLayout.cardWidthFor so the clamps scale and match
the rails.
2026-08-20 14:34:39 +02:00
edde746 fcb167e661 fix(library): size hub detail posters like home rows and library grids
Home hub rows showed ~3 large posters per row while the hub's "see all"
page packed 5 small ones on the same 360dp phone. Hub detail was the
only surface on the padding-aware target-count formula, which resolves
ceil(lerp(5, 2, f)) columns regardless of screen width (and 9+ columns
on TV). Drop the flag so hub detail shares the fixed-extent formula
with every other grid, and delete the now-unused
getMaxCrossAxisExtentWithPadding and its usePaddingAware plumbing.

close #2039
2026-08-20 14:34:27 +02:00
edde746 71cbb83c06 fix(player): keep a play/pause tap from crashing on a torn-down player
Transport from the controls layer ran unawaited with no error handling:
a tap racing player teardown (NOT_INITIALIZED on Android) or a wedged
mpv event queue (SET_PROPERTY_FAILED on Windows) crashed the app. The
controls path now tolerates failures the same way the remote transport
path one function above already does.
2026-08-20 13:31:03 +02:00
edde746 33d80b6d0e fix(companion): keep LAN discovery from emitting into a closed hosts stream
The most frequent Dart crash in production: CompanionRemoteProvider's
async crypto rebuild can call stopListening() on a discovery service
that was already disposed, and _emitHosts() then threw 'Bad state:
Cannot add new events after calling close'. Emitting after dispose is
now a no-op.
2026-08-20 13:31:03 +02:00
edde746 7c496413f8 fix(remote): survive app backgrounding by guarding the reconnect cycle and retrying on resume
Backgrounding the Android app or locking the screen near-always dumped the
companion remote back to the device-selection page, forcing a manual
re-connect. Three defects combined: the peer's trailing disconnected status
event knocked the session out of reconnecting and broke the retry chain, the
bounded backoff budget (~31s) burned out against restricted background
networking before the user returned, and nothing retried on resume.

The provider now ignores stale peer status/error events while a reconnect
cycle is active, defers retries while backgrounded instead of consuming the
budget, and on resume retries immediately with a fresh budget - or pings a
nominally-connected session so a dead socket fails into the reconnect path
right away. Authenticated socket errors in the peer now surface as
disconnects like clean closes, keeping both terminal signals on the
reconnect path.

close #2035
2026-08-20 07:10:51 +02:00
edde746 04673a8c8a fix(library): render Plex home videos with 16:9 thumbnails instead of cropped posters
Plex "Other Videos" libraries returned items as type=movie with
subtype=clip, which mapped to MediaKind.movie and rendered 2:3 poster
cards that cropped the generated 16:9 video-frame thumbs.

Items keep the movie kind so downloads, add-to, delete-from-server, and
detail navigation stay available, but subtype=clip now renders wide with
the thumb-first artwork clips already use. Sections marked subtype=clip
in /media/providers map to the clip library kind, giving Plex home-video
libraries the same folder-first grouping and wide grid cells as
MediaBrowser homevideos views. Wide-only hubs also keep 16:9 cards in
the poster episode modes, matching the TV rail's gate.

close #2036
2026-08-20 05:44:08 +02:00
edde746 958c27350d feat(navigation): open settings with Cmd+, / Ctrl+, on desktop
Pressing Cmd+, on macOS did nothing: the stock Flutter template left a
disabled Preferences menu item holding the key equivalent, and nothing
in the app handled the chord.

MainScreen now handles Cmd+, (macOS) / Ctrl+, (Windows/Linux) beside
the existing Cmd+F search shortcut, reusing its tab-aware open path. A
SettingsShortcut fallback above the profile navigator covers pushed
content routes, pushing a named settings route; a navigator observer
blocks the chord while settings is already in the stack, and the video
player route keeps ownership of the keyboard. The dead xib menu item
is removed.

close #1909
2026-08-20 05:28:37 +02:00
edde746 dcf2314d9d fix(player): interpolate the media-controls suspend flag in lifecycle log
The controller extraction left a bare $_mediaControls.suspendedForTvBackground
in the lifecycle diagnostic string, so the line logged the controller
instance instead of the boolean it exists to surface during TV
background-suspend debugging.
2026-08-20 03:30:07 +02:00
edde746 4c74d6a1d5 chore(player): latch TV background suspend state in one owned object
The Android TV background suspend (#1911) kept its grace timer, the
suspended latch, and four pre-stop position/track fields loose on the
State, with the latch/rollback/consume invariants enforced only by
convention across three lifecycle methods and the redelivery loop.
Move them into TvBackgroundSuspendState: latch() sets the snapshot and
the suspended flag together, clear() rolls both back when the native
stop fails, and consumeForRestore() drops the suspended latch before
handing the snapshot to the restore reload — the ordering the bounded
stop-report redelivery depends on. Pure eligibility predicates stay in
tv_background_suspend_policy.dart; orchestration stays in the
lifecycle part.
2026-08-20 03:08:43 +02:00
edde746 5842d30ae8 chore(player): route all live-session mutations through the live part
The live fallback ladder, retry latch, timeline suspend/resume, and
exit-on-resume flags were poked directly from the error handler, the
play-intent path, the playing-state listener, and lifecycle handling.
Give the live part named operations (_beginLiveLadderRetry,
_retryLiveStreamForPlayIntent, _resetLiveLadderOnPlaybackRestart,
_stopLiveSessionForTvBackground, _consumeLiveExitOnResume) and move
the timeline suspend/resume helpers over from lifecycle, so
LiveTvSessionState is mutated only by live_tv.dart and the initial
tune in the start composition root.
2026-08-20 03:04:14 +02:00
edde746 5e19d9dad4 chore(player): give the episode vertical an owned session-state object
Adjacency, loading flags, the Play Next prompt/countdown, the
transient-retry budget (#1867), the completion latch, and the per-
screen adjacency loader were thirteen loose State fields written by
the episode parts, the reload engine, and the stream listeners. Move
them into EpisodeSessionState — the episode analog of
LiveTvSessionState — so the vertical's mutable state has one home and
the reload engine's touch points (clear prompt on open, reset
adjacency and retry budget on swap, record failure reason) are visible
as writes to one object. Logic is unchanged; the screen disposes the
state object instead of the raw timer.
2026-08-20 03:00:27 +02:00
edde746 b0b4d5d6db chore(player): extract spurious-EOF recovery into an owned helper
The dead-stream recovery budget, its progress-based refill, and the
parked latch (#1520) were four State fields written from the commit
chokepoint, the position listener, the reload engine, seeking, and the
transport-intent handlers. Move the state machine into
SpuriousEofRecovery with explicit verbs (interceptEof, retry,
clearPark, resetBudget, onPositionAdvanced); the reload call rides one
injected callback that fills the common in-place-reload arguments. The
reload outcome enum becomes the public MediaReloadOutcome so owned
helpers outside the screen library can consume it.
2026-08-20 02:55:08 +02:00
edde746 a55ec642b3 chore(player): move the transition lock and generation into a gate object
The mutually-exclusive playback transition state, its lease identity,
the idle completer, and the playback generation counter were four State
fields manipulated by centralized screen methods. Move them verbatim
into PlaybackTransitionGate (tryAcquire/owns/advance/release/forceIdle/
waitForIdle/beginGeneration) so lease discipline is enforced by one
type and parts can no longer touch the raw fields. The transition enum
and lease type become public in the gate's library; leases are only
mintable through tryAcquire.
2026-08-20 02:50:18 +02:00
edde746 aedca9b416 chore(player): centralize first-frame readiness in FirstFrameGate
_hasRenderedFirstFrame had five writer files with hand-rolled
snapshot/restore pairs in the reload and channel-zap rollbacks, and the
paired _hasFirstFrame ValueNotifier was reset independently along the
open path. Move both flags into FirstFrameGate with explicit verbs —
markReady, resetUiForOpen, resetRenderedForAttempt, reset,
forceUiReadyOnFailure, snapshot/restore — so the UI-vs-reporting
asymmetry and the transactional rollbacks are enforced by one type.
The uiReady notifier keeps its identity across attempts; the video
surface, controls, and buffering overlay hold it by reference.
Unit-tests the gate's verb semantics.
2026-08-20 02:45:53 +02:00
edde746 d9a5fb034e chore(player): extract shader and zoom orchestration into a controller
Shader preset application, ambient lighting enable/restore/toggle, and
video zoom/boxfit lived as an extension reading per-attempt services
off the player State. Move them into VisualEffectsController, a plain
State-owned helper; the shader, ambient-lighting, and video-filter
services are injected as late-bound getters because all three are
re-created per playback attempt and nulled in teardown. Rebuilds keep
flowing through the single requestRebuild callback.
2026-08-20 02:40:30 +02:00
edde746 92cc49a265 chore(player): extract companion remote wiring into an owned binding
The CompanionRemoteReceiver callback install/uninstall, home-fallback
save/restore, provider sync handle, and receipt-time volume/seek
dispatch lived on the player State. Move them into
CompanionRemoteBinding, a plain State-owned helper: the binding
instance replaces the State as the receiver's playerOwner token with
identical identical()-guard semantics, and the provider is captured at
bind time because context.read can fail during dispose. Subtitle/audio
cycling stays on the State — the drain loop is bound to the playback
transition lease — and is passed in as callbacks.
2026-08-20 02:30:41 +02:00
edde746 1e83e4e6c2 chore(player): extract OS media-controls adapter into an owned controller
The Android TV background suspension latch, availability policy, and
resume/rewind restore logic lived as an extension on the 149-field
player State, with the suspension flag read and reset from three other
parts. Move them into MediaControlsScreenController, a plain
State-owned helper following the established player pattern: the
manager, player, and current item are injected as late-bound getters
because they are re-created per playback attempt. The controller now
solely owns the suspension latch; teardown clears it via
resetSuspension() instead of writing the State field directly.
2026-08-20 02:25:16 +02:00
edde746 e71580def9 chore(player): move the in-place reload engine into its own part
episode_navigation.dart fused two responsibilities: the episode
next/previous vertical and the screen's generic in-place media
transition engine (_switchPlaybackSource, _performPlaybackSourceSwitch,
_selectSourceSubtitleLocally, _reloadMediaInPlace,
_reapplyScopedPlayerPrefsForItemChange), which lifecycle restore,
spurious-EOF recovery, Watch Together, and the companion remote also
drive. Move the engine and deferTranscodeSubtitleSelection verbatim
into playback_reload.dart so the episode part contains only the
episode feature. No code changes beyond the file split.
2026-08-20 02:16:12 +02:00
edde746 2f47ed23cd chore: dedupe resolution display labels and fold single-use helpers
Two private formatters mapped the canonical server resolution label to
display text (media_quality_labels, media_version) with edge-case
drift, and several single-consumer helpers added indirection without
callers:

- add shared resolutionDisplayLabel in resolution_label.dart and
  migrate MediaVersion.displayLabel and the quality-label builder to it
- drop the resolutionLabelFromHeight compat re-export from
  jellyfin_mappers (no remaining consumers) and its stale doc note
- delete TraktCatalogSource.membershipKeysFor, byte-identical to the
  CatalogWatchlistMachinery default
- derive the rating sheet backend label from
  MediaBrowserDialect.productName instead of a hardcoded switch
- inline EndpointFailoverManager into failover_http_client.dart, its
  only importer, and remove endpoint_failover_interceptor.dart
2026-08-20 02:01:12 +02:00
edde746 b66eec827d fix(trackers): use the shared refresh timeout for MAL token refresh
MAL token refresh waited on the general 20s request timeout while the
Trakt and MDBList refresh paths use the dedicated 15s refresh timeout,
so a hung refresh held the 401 retry longer on MAL than on its
siblings. Align MAL with the shared refreshTimeout.
2026-08-20 01:59:00 +02:00
edde746 df0d71e6f8 fix(auth): let the Plex QR and browser waits cancel back to the sign-in actions
Google Play rejected 2.16.0 (versionCode 142) for an Android Automotive
navigation dead end: on a car, "Sign in with Plex" resolves to the
in-app QR wait, which offered only Retry — and the AAOS system bar has
no back button, so a touch user could not leave the screen without
killing the app. Add a Cancel action to the QR and browser-polling
states that aborts the attempt and returns to the initial sign-in
actions on every platform.

Verified on an API 34 Automotive emulator: Cancel returns to the
landing with the Jellyfin/Emby paths reachable.
2026-08-20 01:11:23 +02:00
edde746 7a4cdc8b5e fix(player): pin loudnorm output to 48 kHz float to stop startup audio stutter
Audio Normalization on the mpv path made every video start with stuttering
audio on Linux: dynamic-mode loudnorm always outputs float64 at 192 kHz, so
the AO opened at f64/192k and PipeWire had to convert/resample on the
deadline-critical path (~4x the per-cycle DSP work) while playback startup
load was still settling. Appending mpv's native format filter pins the
chain back to 48 kHz float, so the conversion happens once on the buffered
decode side and the AO opens a normal stream. Integrated loudness output is
unchanged (-14 LUFS, verified via ebur128). mpv's own format filter is used
instead of lavfi aformat so the fix does not depend on which lavfi filters
each platform's bundled ffmpeg compiles in.

Addresses the normalization half of #1720; the residual startup
micro-stutter with normalization disabled is a separate regression.
2026-08-19 23:25:59 +02:00
edde746 20896a3d48 fix(player): keep the wake-time profile picker responsive on Android TV
Sleeping the device mid-video with "Ask for profile on app open" enabled
left the app wedged on an unresponsive Choose Profile screen (Nvidia
Shield). The picker is pushed on the root navigator, but the player's
focus self-heal only consulted its own nested profile-session route, so
it yanked D-pad focus back behind the picker whenever it lost it.

Route currency now walks every enclosing navigator (isRouteChainCurrent)
before the player reclaims focus, primes loading-phase navigation focus,
or claims the surface on window focus. The resume-time prompt is also
skipped entirely while a video player is active: waking mid-stream
resumes the stream instead of stacking the picker over the session.

close #2034
2026-08-19 23:08:32 +02:00
edde746 4522f42bbf fix(focus): reveal focused items only during keyboard/D-pad sessions
Closing an episode context menu (or its Rate / File Info sheets) on a
show opened from a Home section scrolled the detail page back to the
top, and on shows with many seasons jumped the season selector back to
the entered-from season. The Home path parks invisible focus on the
initial episode/season target; dismissing the menu restored focus to
that parked node, whose focus-gain auto-scroll then yanked the viewport.

Focus chrome is already keyboard-mode-only, so make the focus-gain
reveal match: FocusableWrapper, FocusableChipStateMixin, and
FocusableTileStateMixin now scroll into view only during keyboard/D-pad
sessions. The gate reads the tracker's live state (new
InputModeTracker.currentMode) because the inherited provider is one
frame stale on the first navigation key of a session. The touch OSK
search submit keeps its jump-to-results via an explicit reveal, matching
the existing pointer-mode convention of pairing requestFocus with an
explicit scroll.

close #2031
2026-08-19 22:38:18 +02:00
edde746 a4b88df965 fix(explore): show Seerr request once the detail fetch resolves a tmdb id
Plex Discover's hub, search, and related endpoints ignore includeGuids,
so items opened from those rows carried no tmdb id and the Request
action never appeared — only watchlist items (whose endpoint does
return Guids) could be requested. The gate was frozen in initState from
the row item; it now reads the detail-enriched item, whose metadata
fetch does bring the tmdb id.

close #1959
2026-08-19 20:54:00 +02:00
edde746 810ef002ad fix(player): show source track audio in stats during passthrough
When mpv bitstreams (audio-spdif), audio-params describe the IEC 61937
carrier - 192 kHz "stereo" for E-AC-3 - so the performance overlay told
users their 5.1 track was playing as 2ch stereo. Every recent "EAC3
downgraded to PCM" report in #1300 reads back this string while the
receiver route stays healthy.

Show the source track's channel layout and sample rate instead, plus an
explicit Passthrough row naming the bitstreamed codec, on both the
mpv-channel and Android mpv-fallback stats paths.
2026-08-19 20:14:48 +02:00
edde746 cb900c255a fix(trackers): preserve AniList/MAL rewatch status when scrobbling
Finishing an episode of a show set to rewatching stomped the entry back
to watching. The scrobble write now reads the entry's status and rewatch
count in the same request as the episode count, keeps REPEATING /
is_rewatching entries rewatching, starts a rewatch when progress lands on
a completed entry, and bumps the rewatch count when a rewatch completes.

close #2026
2026-08-19 19:34:04 +02:00
edde746 43ab083cda fix(tvos): match Siri Remote swipe physics to measured native focus engine
Swipe navigation felt sluggish next to native tvOS apps: Plezy priced a
focus step at 0.55x the focused item's extent, throttled repeats to 140ms,
and stopped dead on finger lift - the inverse of the native engine's feel.

Retuned AppleTvRemoteTouchService to on-device measurements of the native
focus engine (issue #2006):
- one focus step per fixed 400pt of touch travel; drop item-extent scaling
  (measured step distance was identical across a 230pt and a 345pt axis)
- repeat cadence 140ms -> 60ms (native median)
- fast lifts glide one or two extra steps (>=2000 / >=8000 pt/s) within
  ~130ms of the lift; reversal pivots and new touches cancel the glide
2026-08-19 18:26:30 +02:00
edde746 3fb90a4948 fix(images): fetch full-resolution tile artwork on low-end hardware
Posters and episode thumbnails looked noticeably soft on Fire TV sticks and
similar boxes: the auto-detected reduced tier capped artwork DPR at 1.5 and
tightened tile decode caps, so every tile was fetched at 56% of its pixels
and upscaled twice on large 4K panels.

Drop the reduced-tier DPR cap and the poster/square/thumb decode caps so
tiles fetch and decode at full TV density on every tier; raise the reduced
image-cache budget to the 64MB TV baseline to absorb the larger tiles.
Backdrops keep their scrim-masked ~720p reduced caps, and the display budget
factor stays pinned to 1.0 there, so the low-RAM art ceiling is unchanged.

close #2020
2026-08-19 13:16:36 +02:00
edde746 4a1111f435 feat(navigation): restyle sidebar as M3 Expressive navigation rail
The custom sidebar read as an ad-hoc widget: collapsed rows were the
expanded layout clipped to the strip, the libraries tree used a
staircase of indents and mixed row heights, and hover expansion pushed
the whole content area sideways.

Rebuild the rail on M3 Expressive geometry: collapsed destinations are
icon-only 56x32 pills centered in an 80px strip (48px icon-only on TV),
expanded rows are fixed-height destinations with a full-width stadium
indicator, and the two layouts crossfade/lerp during the width morph
instead of clipping. The libraries section is flattened into full-width
destination rows under a chevron header at standard destination
metrics, hover/touch expansion overlays content as a rounded, shadowed
panel over a dimmed scrim instead of pushing it, and rail width, item
morphs, content translate, and bleed all share one duration/curve.
2026-08-19 09:11:18 +02:00
edde746 05ef93c55b fix(player): keep gpu-next off the Android hardware-decode path
HEVC files played through the mpv backend on the Nvidia Shield show a
solid blue screen with audio: under hwdec=mediacodec, gpu-next samples
the decoder output as a samplerExternalOES that libplacebo declares in
both shader stages, and the Tegra GLES linker rejects the pair ("struct
type mismatch between shaders for uniform"), failing every frame. The
in-chain gpu fallback never engages because gpu-next initializes fine,
and mpv/libplacebo master reproduce it unchanged.

DV reshaping — the reason gpu-next exists on Android — only happens
under software decode, so offer gpu-next exactly there and keep the
legacy gpu VO for hardware sessions on both the primary mpv backend and
the ExoPlayer fallback core.

close #2010
2026-08-18 23:21:19 +02:00
edde746 8d2eca42d4 feat(search): filter search results by media type
Searching a name shared by a movie, a show, and audio drowns the wanted
item in episode and track rows. The search screen now offers kind chips
(All, Movies, TV Shows, ...) above the results; chips derive from the
pre-rank candidate pool, and selecting one re-ranks that kind's
candidates with the full display budget, so a kind crowded out of the
trimmed All view still surfaces everything the servers returned for it.
Chip switches never refetch, and the filter falls back to All when a
refined query no longer returns its kind.

close #2001
2026-08-18 20:58:35 +02:00
edde746 0f3d06668f fix(jellyfin): read Emby playback shelves from the hide-aware resume route
Removing an item from Continue Watching on Emby posts HideFromResume,
which only the dedicated /Users/{uid}/Items/Resume route honors: the
IsResumable /Items query and the per-series /Shows/NextUp lookups kept
returning hidden rows, so removed items blipped back on every refresh.

Emby's dedicated route conflates both shelves (in-progress items plus
one zero-position next episode per started series), so every playback
surface now reads one hide-aware window from it and splits the rows by
PlaybackPositionTicks: positive rows feed Continue Watching, zero-
position episode rows feed Next Up. The 1+N per-series Next Up
reconstruction is deleted; one recency scan still supplies the play
dates and the NextUpDateCutoff window. Jellyfin request strings are
unchanged.

close #2003
2026-08-18 20:58:35 +02:00
edde746 7131be4cc0 fix(plex): request native-size artwork instead of server-side upscales
Browsing artwork showed PMS PhotoTranscoder log lines flagged
"upscaled: 1" on every request. The flag merely echoes the upscale=1
query param Plezy sent on every cover request (PMS prints it before
locating the source, including on cache hits and pure downscales), but
the param had a real cost: whenever the requested box exceeded the
source — hero art at the 4K display budget (3840x2160 requests against
the near-universal 1920x1080 agent art), portrait backdrops on >1080p
phones — PMS genuinely enlarged the image server-side. Verified against
PMS 1.43: ~2.5x the transcode time and transfer bytes for zero rendered
detail, since the client covers the slot on the GPU regardless and the
fit-policy decode bounds never enlarge.

Send upscale=0 on cover requests too: downscale output is byte-identical
and oversized requests now return the native image. Jellyfin needs no
change — MaxWidth/MaxHeight never enlarge.

close #1975
2026-08-18 19:47:52 +02:00
edde746 436fd7d2d5 fix(desktop): hide the window before exit teardown and keep shutdown out of live UI
Closing the window now runs a graceful exit (so trackers get their
terminal scrobble), but the teardown kept the window on screen for
seconds while screens visibly emptied as servers disconnected.

Hide the window as the first step of exit teardown, bounded so a
stalled platform channel cannot hold an already-accepted exit open, and
cover every graceful-exit entry (window X, Cmd+Q, OS-initiated), not
just the close button. Add a terminal MultiServerManager.shutdown() that
closes the status streams before draining clients: the still-mounted
widget tree must never observe an "all servers gone" snapshot mid-exit,
which would flip the app into offline UI and dismantle screens (and
with it the teardown-driven AppLifecycleListener used-after-dispose
assert). A health result landing after shutdown is a silent no-op.

Supersedes the flag-based approach in #1999.
2026-08-18 19:38:30 +02:00
edde746 c198bab04c fix(tv): keep focus on the played item when continue watching reorders
Exiting the player after enough progress moved the played item to the front of Continue Watching left D-pad focus at its old position, now occupied by a completely different item.

TvBrowseRail and HubSection now remap the focused index to follow the focused item identity when a hub reorders underneath it, falling back to the same series replacement entry when a finished episode is swapped for the next one, and HubSection focus memory is remapped so re-entering the row lands on the same item.

close #1987
2026-08-18 19:27:00 +02:00
edde746 ad1a12c998 fix(release): reset fresh state and preserve deferred phases 2026-08-18 14:45:32 +02:00
edde746 9fe6dbe9f2 fix(explore): add MDBList watchlist source
Connected MDBList accounts were omitted from Explore, so their watchlists could not be selected or displayed. Register the profile-scoped MDBList client as a catalog source with watchlist mutations and search.

close #1888
2026-08-18 13:13:06 +02:00
edde746 304d8a5e33 fix(plex): keep relay out of the phase-1 race, preferred persistence, and pinned sessions
A relay connection can win the phase-1 race (plex.tv edge answers fast),
gets saved as the preferred endpoint, and the cached URL then wins the
deterministic head-start probe at every bind — pinning future sessions to
relay's 2 Mbps cap even after direct connectivity returns, surfacing as
HTTP 500s on Original quality. A session that lands on relay legitimately
(direct down at connect time) stays there all evening: re-optimization
only fired on connectivity events.

Relay is now a fallback tier in the race: all probes still start at once,
but a relay success is held until every direct candidate has failed, and a
cached relay URL gets no head start. A single persist gate refuses relay
at all four endpoint-save sites, classified against both the live server
and the connect-time capture so rotated relay URIs cannot slip through.
While the active endpoint is relay, a bounded-backoff re-probe (30s/60s/
120s) re-races the candidates so a returning direct endpoint promotes
away without a restart.
2026-08-18 09:23:04 +02:00
edde746 986237491f fix(i18n): fill missing translations and remove unused keys 2026-08-18 08:41:13 +02:00
edde746 8461e6ba5f feat(detail): split play button with version picker segment when multiple versions exist
There was no way to tell from the detail screen that a movie or episode
has multiple versions (theatrical/extended cuts, 1080p/4K encodes); the
only entry point was the hidden Play Version item in the overflow menu.

The Play button now becomes a Material 3 Expressive split button when
the item carries more than one version and its server is reachable: the
main segment keeps plain Play (which already resumes the remembered
version), and a narrower chevron segment runs the existing Play Version
flow (version picker, quality picker when the backend can transcode,
preference save). The flow itself is extracted from the context menu
into a shared promptAndPlayVersion helper, and FocusableActionBar gains
per-action spacingBefore so the joined pair can sit tighter than the
rest of the row. Transcode-only quality picking stays in the overflow
menu so the chevron keeps signaling a real version choice.

close #1881
2026-08-18 07:41:54 +02:00
edde746 35dce90ea9 feat(player): persist player-sheet changes at a configurable scope
Changing playback speed, shader preset, aspect ratio, or a sync offset in
the player always rewrote the global default, so a show watched at 1.5x
forced 1.5x onto movies, an Anime4K pick followed the user into
live-action libraries, and a file-specific subtitle offset leaked onto the
next title.

Each of those settings now has a scope under Settings > Video Playback >
Remember Player Changes: don't save, everywhere (default, pre-existing
behavior), per library, or per show/movie. Player writes route through the
configured scope; playback start and in-place item changes resolve the
value for the current item, falling back to the global default. Items
without the needed identity (live TV placeholders, Jellyfin/Emby downloads
without a stamped library) keep the global behavior. Resolution consults
only the configured scope, so entries saved under a previous scope become
inert instead of shadowing the new one.

close #1984
close #1322
close #1616
2026-08-18 06:09:11 +02:00
edde746 483691d43a feat(player): surface skip forward/back on the iOS and tvOS lock screen and remote
On iOS and Apple TV the lock screen, Control Center, and iPhone remote card only offered previous/next track for video, so the transport buttons restarted the episode or jumped to the next one instead of skipping a few seconds.

MediaControlsManager suppressed the MPRemoteCommandCenter skip commands on all Darwin platforms because they displace the next/previous buttons — the right call for music, the wrong one for video. The suppression is now a per-surface policy: the video player opts in with preferSkipOverTrackButtons and advertises the in-player small-skip step (seekTimeSmall, default 10s) via setSkipIntervals, while music keeps next/previous as its lock-screen transport. Skip events already routed through MediaControlRouter to _seekRelative on every platform, so the OS-echoed interval drives the actual seek.

close #1994
2026-08-18 04:06:11 +02:00