Commit Graph
932 Commits
Author SHA1 Message Date
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 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 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 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
edde746 137f2a0e95 merge: music-library polish and Live TV guide day navigation
Brings in the feedback-driven fixes: user-assigned Plex playlist
posters, square music-grid gutters, grouped artist discography
(albums/singles & EPs/live/compilations for Plex), and the guide
day-jump rework (mounted reloads, immediate day apply, focus
retention).
2026-08-17 23:03:24 +02:00
edde746 cff6666219 feat(music): group artist discography into albums, singles & EPs, live, and compilations
Artist pages showed one flat album grid, unlike Plex's own clients.
Plex listing rows never carry Format/Subformat tags (even with
resolveTags=1), so fetchArtistDiscography follows the unchanged album
listing with one batched /library/metadata/{ids} request - whose rows
do include the tags - and classifies each album individually
(EP/Single, then live, then compilation, case-insensitively). A failed
tag fetch degrades to the flat list, and single-album artists skip the
lookup entirely. Jellyfin/Emby carry no album taxonomy on the wire and
return a single albums group.

The artist screen renders one titled section per non-empty group
(single-group artists keep the flat grid), with the grid focus index
space offset per section so D-pad traversal crosses section
boundaries.
2026-08-17 22:44:12 +02:00
edde746 b0798cde9f fix(plex): backfill show logos on continue-watching rows missing them
PMS versions before the ~1.43 hub refresh omit a show's inherited clearLogo
image from episode/season hub rows, so the Discover hero fell back to the
title for TV series while movies (which carry their own logo) and the show's
detail page (which reads the show metadata directly) kept theirs.

Resolve the missing owners' logos in one bulk /library/metadata request
(comma-joined rating keys) and stamp them onto the rows, mirroring the
grandparent lookup Plex Web performs. Best-effort: a failed lookup never
fails the shelf and is retried on the next refresh.
2026-08-17 21:46:12 +02:00
edde746 efd33b3a4e fix(database): restore api_cache.cached_at read by the playback freshness gate
Main stopped compiling: the v21 column drop treated cached_at as
never-read, but the fresh-cache-first playback metadata gate
(ApiCacheSingleton.getIfFresh, from the playback start-latency fix)
reads it on every Plex and Jellyfin playback start, and a later
cleanup removed the put() stamp the gate depends on.

Reinstate the column, keep the v21 migration to the connections
half only (it never shipped in a release), restore the explicit
put() timestamp - on conflict the upsert only updates the
companion's columns, so without it a refreshed row would keep its
original write time and read as permanently stale - and copy
cached_at through the pinned-metadata rescope statement again. The
v21 migration test now pins that api_cache is left untouched.
2026-08-17 20:47:40 +02:00
edde746 86498547ba fix(plex): burn Live TV DVB subtitles on explicit selection
Plex Live TV lost DVB subtitles when 2.10 moved the live path to HLS
(#1983): the transcode starts with subtitles=none, which drops a
tuner's bitmap subtitle streams from the output entirely - unlike
in-band CEA captions, they are separate elementary streams.

The tune response now surfaces the part's embedded bitmap subtitle
streams as selectable tracks in the player's subtitle menu. Picking one
points the part's server-side selection at that stream and rebuilds the
live transcode with subtitles=burn, preserving the time-shift position.
Off stays the default so tuner captions are never auto-burned (#1590),
and channels without bitmap streams keep the native track list. The
selection survives stream recovery by re-mapping onto the re-tuned
session's streams and resets on channel zap. Jellyfin live sessions
record subtitle selection as intentionally unsupported: their one
negotiated URL has no rebuild to deliver it through.
2026-08-17 20:20:20 +02:00
edde746 a7527718df refactor(services): delete dead branches, write-only fields, and test-only cache/service surface
plex_client.selectStreams' allParts=false branch, download_manager's both-branches-true conditional and unscoped getAllPinnedMetadata tail, playback_source_resolver.preferOffline, and update_service's unreachable catches had no reachable path; PlaybackSession.result/streamHeaders, PlaybackContext.clientScopeId, FileInfoStreams.audioStream, PrefsRepairOutcome.settingsReset/sessionsSalvaged, and JellyfinLiveSessionTracker.playSessionId were write-only; PlexConnection.directUrl, PlexServer.isOnline plus its parse-and-persist presence field, SafStorageService.createDirectory, FullscreenStateManager.stopMonitoring/dispose, the play_queue_launcher re-export shim, track-selection's constant params, and PlexApiCache.unpinForOffline/isPinnedRatingKey (matching the earlier Jellyfin removal) were dead API.
2026-08-17 19:02:25 +02:00
edde746 a33c423985 refactor(models): stop parsing wire fields and enum members nothing consumes
Parse-only DTO surface deleted across trackers and catalog models: quality-preset storageKey/fromStorage round-trips (persistence uses EnumPref on .name), PlexHome's placeholder identity fields (id stays — deriveHomeSecret reads it), SeerrUser.email/avatar, SimklSearchResult.endpointType, SimklDetail.type, SimklAllItemsEntry.status/isShow, TraktCatalogEntry.rank, CatalogItem.relevance, and the never-produced CatalogRankScope.favorited/.seasonal members with their label arms. Tracker root: OAuthProxyStart.expiresIn, TrackerSession.scope/isExpired, copyWith slimmed to its one real parameter, TrackerHttpClient.service. TrackerAccountStore.service and the trackerAccountStore() alias are live (persisted-blob validation, callers) and stay.
2026-08-17 19:02:24 +02:00
edde746 1a26b0dd08 refactor(media): delete write-only item fields, dead copyWith/getters, and the unread live-TV activity wrapper
PlexMediaItem.subtitleMode/extraType were mapped from DTOs and never read (subtype stays — the detail screen's trailer picker destructures it); MediaPlaylist.copyWith, PagedMediaListState.mapItems, isMusicContent/isCollection, and MediaHub.copyWith's eight never-passed params had zero call sites; LiveTvActivityResult wrapped reloadGuide results nobody unwrapped (now Future<void>); PlayerLogLevel.none was unreachable — nothing parses or produces it; MediaSourceInfo.getPartId() was an accessor over its own public field.
2026-08-17 19:02:23 +02:00
edde746 00a6e056b0 refactor(plex): drop the test-only raw-JSON mapper flavours and title obfuscation branches
PlexMappers advertised a parse-and-map FromJson flavour of every mapper 'for callers that haven't already parsed a DTO' — no such production caller exists; every call site uses the DTO-typed flavour. Tests now parse the DTO and map it, preserving every assertion. The kBlurArtwork JSON-walking branches in the DTO factories (title/summary vowel rotation on the decode hot path) are deleted with the flag's title half.
2026-08-17 19:02:22 +02:00
edde746 78fa2c29e3 refactor(input): compile the disabled diagnostics and screenshot-blur subsystems out of normal builds
TextInputDiagnostics.enabled and kBlurArtwork were mutable/const-false flags nothing ever set, yet the diagnostics side eagerly built log strings on the gamepad and key-simulator hot paths on every event. Both flags are now const bool.fromEnvironment (PLEZY_TEXT_INPUT_DIAGNOSTICS / PLEZY_BLUR_ARTWORK) so the branches const-fold away, every eager interpolation sits behind a guard, and screenshot blurring stays one dart-define away. The vowel-rotation title obfuscation had no consumer need beyond artwork blur and is gone.
2026-08-17 19:02:22 +02:00
edde746 99b0528623 refactor(settings): drop the orphaned mute-toggle helper
SettingsService.resolveMuteToggle had zero production callers — the live mute policy moved to VideoVolumeController.toggleMute long ago, leaving two diverged implementations of the same rule. The two TV-aware pref defaults in the same file now read through the PlatformDetector facade like the rest of the app.
2026-08-17 19:02:22 +02:00
edde746 f5c7b95b02 refactor(tv): read TV detection through the PlatformDetector facade everywhere
TV state had two interchangeable entry points — PlatformDetector.isTV/isAppleTV and the raw TvDetectionService.*Sync accessors — with call sites split arbitrarily between them. App code now goes through the facade; the raw accessors document that they exist for the facade and tests.
2026-08-17 19:02:21 +02:00
edde746 d78a7d925e refactor(trackers): single-client Trakt refresh, distinct Simkl coalescer names
TraktClient's static per-refresh-token coalescer, initiating-vs-joiner branches, session-adoption identity checks, and both updateSession push methods defended a multi-client world production never creates — the one long-lived client is shared via TrackersProvider, and the two throwaway clients never refresh. Refresh now matches MalClient/MdblistClient's instance-coalescer shape. Simkl's _watchlistLoad field shadowed the identically-named coalescer on the mixin it applies (legal today, a trap on any future library merge) — renamed with comments distinguishing the two caches.
2026-08-17 19:02:21 +02:00
edde746 a910aa95cb refactor(tvos): publish menu passthrough directly, trim untuned remote-touch knobs
TvosMenuPolicyPublisher batched an idempotent fire-and-forget publish behind a transaction-depth counter that could never exceed one across its three non-nesting call sites — the call sites now publish directly and the pure predicate keeps its test coverage. AppleTvRemoteTouchService exposed 14 injectable knobs of which only six are exercised anywhere (verified per knob); the other eight are private constants or internal construction now.
2026-08-17 19:02:20 +02:00
edde746 4436efea72 refactor(models): drop parse-only wire fields nothing reads
Four model surfaces parsed, coerced, and re-serialized data with zero readers: PlayQueueResponse's selectedItemIndex/offset/sourceURI/version (version stays as a parse-time validity gate), PlexUserProfile's seven non-track-selection account fields, MediaSubscriptionCreateRequest's hints/params/providers, and the whole CatalogPlayState pipeline (parsed by the Plex catalog source, merged and round-tripped on every CatalogItem, read by nobody).
2026-08-17 19:02:19 +02:00
edde746 8e9c81fe47 refactor(media): plain LocalPlayQueue, trimmed MediaItem factory, one library-content entry point
PlayQueue was a freezed union whose Plex variant was never constructed (server queues flow through PlayQueueResponse), whose pattern getters had zero call sites, and whose backendId was written but never read — it is now a plain four-field LocalPlayQueue. The MediaItem compatibility factory re-listed the entire ~65-field union surface twice while every caller passes at most 22 fields; it now declares exactly that union. fetchLibraryContent had zero production callers (the UI pages through fetchLibraryPagedContent), so the interface method and both implementations are gone, with Jellyfin's drain folded into its paged entry point.
2026-08-17 19:02:19 +02:00
edde746 093c0596f1 refactor(jellyfin): declare shared client internals once, make live-TV negotiation private
Two part mixins re-declared members owned by siblings (five playback methods with full 17-parameter signatures, _safeFetchItemsArray with four lint suppressions), violating jellyfin_client.dart's own 'declared exactly once' contract — they now live on _JellyfinClientInternals, with _safeFetchItemsArray declared as its positional core so no suppressions survive. LiveTvSupport.resolveStreamUrl's only caller anywhere was Jellyfin's own startPlayback, so it is now a private negotiation step and Plex drops its permanent null stub; the negotiation tests observe the same contract through startPlayback and the heartbeat wire. JellyfinApiCache's unpinForOffline/isPinnedItemId (zero callers, kept only for Plex symmetry) are gone, as are MediaSubscriptionCreateRequest's never-populated hints/params/providers expansions in the Plex live-TV serializer.
2026-08-17 19:02:19 +02:00
edde746 8033a308ce refactor(connection): drop dead status/kind enums, share the Plex account build pipeline
Connection.status was write-only health state (only the Jellyfin refresh wrote it, nothing read it — MultiServerManager owns real server status), ConnectionKind duplicated MediaBackend line for line, the token-to-PlexAccountConnection pipeline existed three times (sign-in, dev-token seed, legacy migration) with drifting label/dedup policy, and JellyfinConnectionAuthService.validate/refresh/signOut had no production callers.

Connection.kind is now MediaBackend (persisted 'plex'/'jellyfin'/'emby' strings are identical, no migration); buildPlexAccountConnection in plex_account_setup.dart owns identity resolution with the three callers keeping only their genuine deltas; the test-only auth trio and its jellyfinSignOut timeout constant are gone.
2026-08-17 19:02:18 +02:00
edde746 68c328b469 feat(search): label results with their source server and library
One server with overlapping libraries (movies in both an HD and a 4K
library, shows duplicated per language) returns search results that
cannot be told apart, forcing users to open each copy to find the
right one (#1970).

Each search row now carries a source line under the summary: backend
icon, server name, and library name, shown whenever the owning server
has more than one library. Plex rows name their section inline, with
sectionKey-only rows back-filled from the loaded libraries; Jellyfin
and Emby send no library field on search hits, so search always runs
the per-library scoped fan-out that previously only served
hidden-library exclusion and stamps every hit with its library. The
Jellyfin detail lookup adds a best-effort /Items/{id}/Ancestors stamp
so full metadata stays attributed, row refreshes merge instead of
dropping the stamp, and the mapper no longer misreads SeriesStudio or
ParentId as library identity.

Episode rows trade one summary line for the source line so the
wide-thumb row keeps its height, and the badge paints slightly below
the line-box center to sit on the text's optical middle.

close #1970
2026-08-17 17:13:26 +02:00
edde746 01f04eda42 fix(ui): show loading feedback while play actions fetch
Several Play entry points gave no feedback while their network round
trips ran: a show or season Play button silently awaited a seasons
fetch (with a 10 s completer window) and a first-episode fetch, and
Plex collection/playlist launches showed no spinner at all. Where a
spinner did exist, the first request was gated on the dialog's first
frame instead of running concurrently with it.

Extract executeWithLoading's dialog plumbing into
ScopedLoadingDialogController (mount-aware dismissal, idempotent,
never pops a foreign route) and reuse it for the show/season Play
path. Start the launcher operation before awaiting the dialog frame so
the first round trip overlaps the render, and show the loading
indicator for Plex list launches too.
2026-08-17 13:47:00 +02:00
edde746 be9197eda6 fix(playback): serve playback metadata from fresh cache instead of refetching
Videos take noticeably long to start. On Plex, tapping Play refetched
/library/metadata/{id} network-first even though the detail screen wrote
a strict superset of that exact payload under the same cache key seconds
earlier. On Jellyfin/Emby, playback start issued a full-detail item GET
before the PlaybackInfo negotiation, and the video controls issued the
same heavy GET again while the stream was opening (#1784) — the most
expensive queries a small home server serves, paid two to three times
per start.

Add ApiCache.getIfFresh over the existing cachedAt column and serve
rows younger than playbackMetadataCacheFreshness (5 min) without a
round trip. Plex guards with the strict stream-detail check
(_plexMetadataHasStreamDetail) so a thin row written by
getPlaybackExtras' lean fetch still goes to the network; Jellyfin's
row has a single full-shape writer, so fetchPlaybackBundle and
fetchPlaybackExtras share the new fetchItemFreshCacheFirst. Any miss,
stale, thin, or malformed row falls through to the unchanged
network-first path, and offline behavior is untouched.
2026-08-17 13:46:33 +02:00
edde746 58b510a6c4 fix(music): handle hardware media keys while the app is foreground on Android
Media buttons on HID remotes (USB/Bluetooth keyboards, common on Android
TV) are delivered as key events to the focused window instead of the
MediaSession, so they only worked while the app was backgrounded. A
global handler now routes play/pause, next/previous, stop, and
fast-forward/rewind to the live music session anywhere in the app and
consumes the key burst, so a press can neither leak to Android's
fallback MediaSession dispatch nor start a focused library item. Same
lifecycle as the OS media session; video playback never coexists with it
because claiming video disposes the music session first.

close #1948
2026-08-16 16:47:06 +02:00
edde746 25b2939f0a fix(player): stop same-day Specials from hijacking up next
close #1952

Shows whose Season 0 holds aftershow featurettes (e.g. House of the
Dragon "Inside the Episode") share air dates with the episodes they
accompany, so the air-date interleave from #1416 queued them between
regular episodes: playing S03E04 offered S00E76/S00E84 as up next.

Air date alone cannot separate canon Specials from featurettes, so
Specials placement is now a three-way preference, defaulting to
"follow server order":

- respectServer: Plex keeps its server-built /allLeaves queue (aired
  order, Specials interleaved, as before); the Jellyfin queue now
  preserves the /Shows/{id}/Episodes response order, which is
  Jellyfin's native watch order — Specials placed only via explicit
  AirsBefore* metadata per the server-wide DisplaySpecialsWithinSeasons
  setting (the endpoint ignores SortBy except Random). Client-side
  selections with no server order to respect (offline next/prev,
  download "next N", offline OnDeck) fall back to Specials-last.
- airDate: the #1416 aired interleave on every surface.
- specialsLast: Specials strictly after the regular seasons (#1414);
  Plex builds its show queue from /children.
2026-08-16 15:31:47 +02:00
edde746 d1d393eec0 fix(plex): stop timeline heartbeats once the server terminates the session
Pausing past the server's paused-session limit (or an admin stop) removed
the session on PMS, but Plezy kept sending paused timeline heartbeats,
re-registering it as a zombie session the server could no longer clear.

Plex signals the termination with terminationCode/terminationText on the
MediaContainer of the next timeline reply. Detect it, close the reporting
session with one final stopped report at the current playhead (which
removes the session row), and suppress paused heartbeats plus the
transcode keepalive until playback actually resumes, which opens a fresh
server session.

close #1916
2026-08-16 08:58:57 +02:00
edde746 be863a0c1c fix(linux): remove the Flutter-texture fallback permanently
The display-agnostic texture renderer restored by 9cdfe759 is deleted
again, this time for good: it is a second, SDR-only rendering stack
(isolated EGL context on Flutter's display plus EGL-image handoff) kept
alive solely to host sessions that cannot bring up the Wayland plane -
X11/XWayland or a failed plane bootstrap - and it was the source of the
native lifecycle and EGL state-churn fixes of the 9f2e0507 era. Linux
video now requires a Wayland compositor; a session that cannot host the
plane fails initialization with VIDEO_PLANE_UNSUPPORTED instead of
silently rendering through the second stack.

Reverts the restore commit's machinery: mpv_texture.cc/.h and
mpv_gpu_bootstrap.cc/.h deleted, the plugin's texture-registrar,
bootstrap, and waitForVideoReady paths removed, MpvPlayer's texture-mode
render-context API dropped, and the Dart-side renderMode setting, its
settings tile, translation keys, and the Player textureId member
withdrawn. The #1874 HDR diagnostic work is unaffected.
2026-08-15 01:20:44 +02:00
edde746 7937a7e30a style(dart): apply dart format to the Linux rendering changes 2026-08-14 19:30:32 +02:00
edde746 9cdfe7591e feat(linux): restore the Flutter-texture path as the SDR fallback
2.13.0 deleted the display-agnostic EGL/Flutter-texture renderer and made
the native Wayland plane the only path, hard-rejecting every session that
cannot host one - X11, XWayland (SteamOS Gaming Mode runs native apps
through Gamescope's XWayland), or a plane whose EGL bootstrap failed. This
restores the 2.11.0 texture path from git history as the fallback:

- Re-add mpv_texture.cc/.h and mpv_gpu_bootstrap.cc/.h (2.11.0 verbatim):
  an FlTextureGL whose populate renders mpv into an offscreen FBO sampled
  by Flutter via an EGL image.
- MpvPlayer gains the texture-mode render-context API (InitRenderContext,
  HasRenderContext, GetEglDisplay/Context, Render(w,h,fbo)) beside the
  plane's InitRenderContextForSurface/RenderToSurface. The isolated ES 2.0
  context on Flutter's display and the X11 display param are exactly the
  2.11.0 configuration hardware decode demonstrably worked in.
- initialize now tries the plane first and falls back to the texture path
  when it cannot be brought up, returning the texture id (Dart's 2.11.0
  'result is int' contract) with waitForVideoReady gating playback until
  the GPU bootstrap settles. Both paths share one mpv core, so the
  plane/texture decision precedes render-context creation.
- hdr-enabled/hdr-tone-mapping are intercepted in texture mode (no plane,
  no HDR); the HDR toggle hides itself via isHDRSupported.
- New Linux setting 'Video rendering mode' (Automatic / Texture) forces
  the fallback - the user-visible workaround for plane-only trouble and
  for the hwdec interop regression, plus translations in all locales.

SDR only on the fallback, matching 2.11.0; the plane path is unchanged.
2026-08-14 18:16:32 +02:00
edde746 8f9ffc76eb fix(emby): show scrub previews from the /Videos/{id}/index.bif transport
Scrubbing an Emby video showed no preview thumbnails on the timeline
while Plex and Jellyfin both worked: Emby's scrubThumbnails capability
was off, so the player never attempted a load. It was off because the
4.9.5 test server answered the preview endpoints with empty payloads —
its extraction task had not run.

Emby's preview transport is a Roku-format BIF at
/Videos/{id}/index.bif?Width=320, the same wire format Plex serves, so
the existing BIF parser handles it unchanged. Enable the capability and
route Emby previews through the shared BifThumbnailService, whose load
now takes a bytes callback instead of a Plex-typed client. MediaBrowser
sources also carry videoAspectRatio from the video stream so the
tooltip sizes itself to the stream. A server without extracted frames
still answers a header-only BIF, which parses to zero frames and keeps
the tooltip suppressed.

close #1930
2026-08-14 13:36:29 +02:00
edde746 cd4432f27c fix(auth): keep polling Plex PIN after transient network errors 2026-08-14 12:20:20 +02:00
edde746 5f6d9dc610 fix(navigation): keep the sidebar Libraries section collapsed across restarts
Collapsing Libraries in the sidebar only lasted until the app closed. The
next launch always came back expanded, which buries the rest of the nav
menu for anyone running a lot of libraries.

The expansion flag was plain widget state on SideNavigationRailState. It
survived tab navigation only because MainScreen pins the rail with a
GlobalKey, and died on relaunch, profile switch or a layout change. It now
lives in the librariesSectionExpanded preference, so the rail reads and
writes the persisted value and follows a settings reset or import too.

close #1896
2026-08-13 21:44:25 +02:00
edde746 922789c014 fix(tv): stop the server session when a backgrounded TV releases the player
On Fire OS, standby never freezes a backgrounded app, so the paused
heartbeats that kept a suspended session "alive and resumable" pinned it
in the server dashboard indefinitely. The live session bought nothing:
the restore path already performs a fresh playback decision.

When the TV background grace expires, stop the heartbeat timer (its
paused tick also keeps a Plex transcode session alive) and report the
session stopped before releasing the native pipeline, since stop()
resets the player state the report reads. Standby entry can drop Wi-Fi
into power-save and mutations never fail over, so a failed terminal
report is redelivered on a bounded schedule, gated on actual backend
delivery and on the suspend still standing so a restored session is
never reported stopped.

close #1911
2026-08-13 20:34:12 +02:00
edde746 6663353895 fix(player): retry episode advances that fail on a transient server blip
An EOF-driven advance does one cold metadata fetch with a single endpoint
failover and no transient retry. When connectivity to the server drops for
the ~20s that fetch needs (issue log: both plex.direct endpoints connect
timed out, then the running stream's own TLS socket died), the reload
rolled back to the finished episode's last frame: black screen, progress
bar parked at the end, no way forward but the transport controls - while
pressing Next by hand seconds later succeeded. The per-item metadata cache
row could not absorb the blip either, because adjacency comes from queue
containers, so the next episode's row is cold at the exact moment the
transition needs it.

Three changes:

- A failed in-place reload now records its classified failure reason, and
  an advance that ran with the completion latch set re-presents the Play
  Next prompt when that reason is serverUnavailable. With auto-play
  enabled the countdown re-fires the advance up to two times before the
  prompt goes manual-only; Watch Together sessions and mid-episode Next
  presses (whose rolled-back stream is still valid) keep the existing
  handling. playNextRetryPresentation owns the decision and is unit-tested.

- Committing adjacency now best-effort prefetches the next episode's full
  metadata row through fetchItem, which writes the exact row playback
  initialization falls back to on both backends (Plex: same cache key and
  full playback query shape; Jellyfin: the /Users/{uid}/Items/{id} row the
  playback bundle reads). A warm row turns a blip at the transition into a
  normal start.

- JellyfinClient.fetchItem's documented "pure transport error -> cached
  row" fallback was dead code: the HTTP layer wraps transport errors into
  MediaServerHttpException, which the first catch rethrew unconditionally.
  Status-less, non-cancelled failures now take the fallback; answered
  requests (401/403/5xx) and cancellations surface unchanged.

Verified with new contract tests (Plex: cold row fails transiently ->
fetchItem primes -> the same failing fetch serves playback from cache;
Jellyfin: primed row survives a transport failure into fetchPlaybackBundle)
plus the full test/screens/video_player and test/services suites and
analyzer parity.

close #1867
2026-08-11 09:08:07 +02:00
edde746 3a704a2b9b fix(player): seek Plex transcodes in-band instead of pre-warming at the resume offset
A quality switch or resumed open at a nonzero position sent offset=T on the
HLS start URL, waited for the readiness probe to touch the segment at T, and
then had mpv seek to T anyway. mpv's stream probing always reads segment zero
first, and a Plex segment request is a seek, so the transcoder was dragged
through seek(T) -> seek(0) -> seek(T) within seconds of the open. Measured
against PMS 1.43, a segment response that races such a restart can be left
open with headers sent and no data or error, and ffmpeg's HLS segment reads
have no default timeout, so playback buffered forever after the first frame
(issue #1859). Starting the session plain and letting the player's start=T
request the resume segment performs the one unavoidable transcoder seek.

The offset request parameter, the readiness probe, and the probe-only
getStatus HTTP helper are removed; live TV time-shift keeps its own offset
path. Transcode opens now also set an explicit network-timeout with
demuxer-level reconnect options: mpv's stream-layer reconnect settings never
reach ffmpeg's HLS segment fetches, so a silently hung segment response now
times out after 20s and is re-requested on a fresh connection instead of
buffering indefinitely. Verified against a live PMS (resume plays from the
requested position) and a stall harness (hung segment re-requested at 20s
with no content skip).
2026-08-10 23:04:35 +02:00
edde746 d19ec625cd fix(tv): remove the background Watch Next refresh
2.13.0's ShelfRefreshWorker boots a second headless FlutterEngine in
the app process to refresh the launcher row every six hours. Its
foreground guard is checked only once at worker start, so launching the
app during a run leaves two engines sharing a low-RAM TV for up to 90
seconds, and a failed run retries with backoff. Suspected of
destabilizing the compositor on the 32-bit TCL panel in #1862. The tvOS
Top Shelf live fetch is unaffected and stays.

The foreground sync pipeline keeps the row fresh while the app runs, as
before 2.13.0. Updated devices still carry the persisted periodic job,
which would wake the process once more only to fail instantiating the
deleted class; the package-replaced receiver now cancels it.
2026-08-10 22:30:51 +02:00
edde746 c2bd1d28fd fix(subtitles): load external subtitle files with the media whether or not selected
Since a1b6a8971 only the selected sidecar attached at open, so mpv's
track-list carried one external subtitle and the track sheet could only
offer the rest as primary source switches - tap-and-hold on a
non-selected external track selected it as primary instead of secondary.

Real external files are cheap static fetches, so Jellyfin, Plex direct
play, and offline discovery now mark them preload and they ride along in
sub-files at open, keeping every external track selectable as a
secondary subtitle without a reopen. Embedded rows extracted on a
transcode stay lazy: extraction can stall behind the transcoder, which
is exactly what used to trip the sidecar open guard.

close #1860
2026-08-10 21:23:55 +02:00
edde746 ea356a6112 fix(plex): use fMP4 HLS for video transcodes so HEVC presets stop corrupting
Non-Original presets advertised hevc inside the mpegts HLS target; a Plex
Pass server with HEVC encoding enabled obliges, and its HEVC encode -> TS
segmenter path emits parameter sets mpv rejects ("PPS changed between
slices"). The VOD target now requests fragmented MP4 (verified against
PMS 1.22-1.43), retrying once with an H.264-only TS profile when a
server's decision does not echo the mp4 container back, and falling back
to direct play when neither is honoured. Live TV keeps its own TS target:
live sessions copy broadcast hevc/mpeg2video streams, a path the encoder
bug does not touch.

Presets also now send the videoResolution/videoQuality caps their labels
promise; previously only the bitrate limitation went out, so a "1080p
8 Mbps" preset delivered 2160p at a starved 8 Mbps.

close #1859
2026-08-10 20:38:45 +02:00
edde746 69fadc220d chore: clean up code comments 2026-08-10 20:28:41 +02:00
edde746 369c6279d6 fix(i18n): translate the player, downloads and server-setup text left in English
A Portuguese user reported "Skip Intro" rendering in English on Android TV.
The locale files were not the problem - all 22 were structurally complete.
skip_marker_button.dart simply never imported strings.g.dart and assigned
'Skip Intro' / 'Skip Credits' / 'Next Episode' as plain literals. An audit of
lib/ found ~120 more sites in the same state, in four shapes that need
different fixes:

A literal in a file that never imported the i18n layer is the easy one -
skip_marker_button, performance_stats, track_label_builder and codec_utils all
render text with no `t` in the file at all. TrackLabelBuilder._compose now takes
a fallbackLabel builder instead of an English fallbackPrefix, so the caller
supplies t.audioTracks.track / t.videoControls.subtitleTrack and every unnamed
audio and subtitle row in the track menus is localized.

English reaching the user through an exception message is the widest one, and
it needs care: MediaServerException.message feeds both toString() - logs and
Sentry grouping - and verbatim UI display. Localizing it in place would make
bug-report logs follow the user's locale and split one Sentry issue into 22.
The MediaServer and Seerr families instead gain a nullable `display` alongside
the English `message`, and the six screens that print these errors read
`display ?? message`. PlaybackException keeps the opposite rule, because it
already carries a PlaybackFailureReason for logic and classifyPlaybackFailure
already builds it from t.messages: its stragglers are localized at the throw
site. That also removes the literal "Exception: " prefix Live TV users saw on
a tune failure, since PlaybackException.toString() returns the bare message.

Localized parts hand-concatenated with bare English are the shape no search for
Text('...') can find: '${t.common.pause} auto-scroll' on the home carousel,
'${day} at ${time}' on the Live TV schedule row, and an actor-screen count that
hand-rolled its plural as `n == 1 ? 'title' : 'titles'` - wrong for ru and pl
regardless of translation, now a real Slang plural.

Finally a literal assigned to provider state that a widget renders later:
DownloadProgress.errorMessage, and the four background_downloader notification
bodies, which sit inside a plugin config call where no widget-shaped search
reaches them.

Two things surfaced while converting. track_chapter_controls compared a track
label against 'Audio Track N' to swap in a localized version; once the builder
localized its own fallback that branch became unreachable, so it and the
orphaned _joinTrackLabel are gone. And discovery_view's PeerError fallback arm
looks like a leak but is not - its producers already localize, and a test says
so - so it stays as it is.

All 21 non-base locales are translated, including the 21 keys left empty by
earlier commits that were falling back to English. No locale has an empty value.

scripts/check_hardcoded_strings.py guards the three shapes a structural check
can see, and runs in ci_checks.sh after translation hygiene. Its first draft
passed its own tests while missing this very bug, because 'Skip Intro' is bound
to a local rather than handed to Text(); the name-bound rule that closes that
gap is restricted to phrase-shaped literals, or it cannot tell copy from the
identifiers this codebase binds constantly ('cast_row', 'auto', 'liveTv'). It
cannot see English inside a throw or assigned to a provider field - neither is
distinguishable from a log message without dataflow analysis - and the docstring
says so. label: and actionLabel: are deliberately unscanned: here they name a
diagnostic operation, and a check that is chronically red is a check that gets
switched off.

One commit rather than one per area: the keys, the 22 locale files and the
generated output are a single unit, and any partial split fails the repo's own
unused-key scan on the way through.

close #1856
2026-08-10 15:32:43 +02:00