Commit Graph
3002 Commits
Author SHA1 Message Date
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 3703ac47c3 refactor(explore): share the catalog enum-to-label mappings
The catalog detail screen and the card badges each kept their own enum-to-i18n switches; the genuinely shared mappings (season name, rank scope, request state, status/format/relation labels) now live in catalog_labels.dart, while badge precedence and the deliberately different availability/4k wording stay local with comments naming the divergence.
2026-08-17 19:02:20 +02:00
edde746 c390e0fb0c refactor(detail): share the season-download filter and the download-retry pipeline
The downloaded-episodes-for-a-season filter+sort was written out three times in media_detail_screen (with drifting show-id expressions, preserved per call site), and the failed/cancelled download branches in the action buttons duplicated the same six-step retry pipeline verbatim. One helper each.
2026-08-17 19:02:20 +02:00
edde746 3665d416c4 refactor(providers): deduped peer disposal, hoisted discover outcome tail, live server-connection getter
CompanionRemoteProvider memoized peer disposals in an Expando even though CompanionRemotePeerService.dispose() already dedups concurrent and repeat calls — the wrapper now only keeps cleanup failures from escaping teardown, and the test fake mirrors the service's idempotent-dispose contract. DiscoverProvider._loadOnce carried the same outcome/generation/boundary tail copy-pasted in three branches, now one local settlePassOutcome. OfflineModeProvider cached _hasServerConnection in five write sites when it always equalled a two-term expression — now a live getter.
2026-08-17 19:02:20 +02:00
edde746 2e1a7130fc refactor(downloads): one auto-remove-watched core, trimmed offline-watch provider, shared focus-restore mixin
The auto-remove-watched-downloads rule existed twice (DownloadProvider's sweep and OfflineWatchProvider's single-item copy) — the kind/completed/delete/title core now lives once on DownloadProvider, with the watched judgment staying per caller because the offline path fires before metadata can reflect a local mark. OfflineWatchProvider drops five members with no production callers (isSyncing, getPendingSyncCount, isWatched, getViewOffset, getEpisodesWithWatchStatus) plus the sync-service listener that only served them. The downloads screen's three verbatim 'suppressAutoFocus flipped — focus the first item' didUpdateWidget blocks are now one mixin.
2026-08-17 19:02:20 +02:00
edde746 55382deaf9 refactor(android): drop the dead mpv-command emulation on the ExoPlayer backend
PlayerAndroid.command() emulated four mpv commands ('loadfile', 'seek', 'stop', 'sub-add') that no caller ever sends through the interface, while the commands actually dispatched ('change-list', 'drop-buffers', 'sub-seek', 'screenshot') fell into the default branch and vanished. The override is now a documented no-op, which is what every dispatched command already observed.
2026-08-17 19:02:19 +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 908b4a8dac refactor(database): serialize durable mutations through one queue, keep only guarded watch-action mutators
Every identity/pending mutation nested a per-instance SerialFutureQueue inside the static tvOS-recovery queue, so the per-instance layer added no serialization domain (reentrancy is already prevented by the durability Zone check). The unguarded updateSyncAttempt/deleteWatchAction pair had zero production callers — the sync service deliberately uses the revision-guarded variants.

Tests migrated to the IfUnchanged pair by passing the row's current revision.
2026-08-17 19:02:18 +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 0f016e4e13 fix(player): start the playback fetch before player construction
The playback data resolve — the one network request that must land
before open() — was kicked off only after the SharedPreferences load,
the Windows display-mode sync, the music-session teardown in
claimVideo(), Player construction, and (Android/Exo) a native
setLogLevel, so none of that setup overlapped the round trip.

Move the kickoff to immediately after the settings reads; the resolve
depends only on settings and provider lookups, so display sync, player
construction, and the whole mpv property chain now hide behind the
network latency instead of preceding it.

Also remove redundant work elsewhere on the start path: memoize
PlayerAndroid.getHeapSize (asked again on every open for an immutable
device value), skip the ten subtitle-style settings reads on mpv
backends where setSubtitleStyle is a no-op, and stop navigateToVideoPlayer
awaiting SettingsService.getInstance twice per launch — the external-player
read now sits behind the supportsExternalPlayers guard.
2026-08-17 13:46:51 +02:00
edde746 953acedcab fix(player): pipeline mpv http-header commands at open
Every mpv open rebuilt the HTTP header list with one awaited channel
round trip per change-list command — with Plex's 9-13 identity headers
that is ~12 serialized round trips sitting between the playback resolve
landing and loadfile, on every mpv backend.

Dispatch the clr and append commands without awaiting between sends and
await them together: the method channel delivers messages in send order
and the native side executes them in arrival order, so the
clr-before-append contract holds while the latency collapses to one
round trip. Failures still propagate out of open() unswallowed.
2026-08-17 13:46:41 +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 a2b3377d91 fix(libraries): stop d-pad up snapping collection and playlist grids to top
On TV, pressing UP in the library Collections or Playlists grid snapped
the list back to the top and dropped focus on the tab chips, making long
lists impossible to navigate. Default directional focus traversal scrolls
the found card into view via Scrollable.ensureVisible, whose
outer-scrollable pass routes through the NestedScrollView coordinator and
resets the inner grid position to zero on every UP press.

Give the shared paginated card grid explicit per-card d-pad navigation,
matching the browse tab: managed per-index focus nodes, row/column moves
that request focus directly, first-row UP to the tab bar, and
first-column LEFT to the sidebar. Focus changes now scroll only through
FocusableWrapper's delta-based auto-scroll.

close #1977
2026-08-17 08:58:00 +02:00
edde746 e1b488f860 fix(lifecycle): force-close IOClients stuck in unabortable TCP connects
tvOS logs flood with "HTTP client drain timed out" / "close deferred"
warning pairs after every Plex endpoint race, and each sweep leaks the
losing probes' sockets for the OS connect timeout (~75 s of SYN
retries on Darwin).

A request stuck in TCP connect cannot be aborted: package:http's
IOClient only registers the abort handler once openUrl completes, so
the graceful drain never finishes and ManagedHttpClient defers the
inner close that would have reclaimed the socket.

Fix at both layers: every IOClient now gets an explicit
HttpClient.connectionTimeout matching MediaServerTimeouts.connect, and
ManagedHttpClient gains an opt-in forceCloseOnDrainTimeout escalation
used by all dart:io-backed clients — close(force: true) promptly fails
in-flight requests, unlike the native-callback clients (CupertinoClient)
the deferral exists for.

close #1972
2026-08-17 08:08:45 +02:00
edde746 a79084b0a5 chore(player): link upstream issue androidx/media#3377 in seek workaround 2026-08-17 04:47:06 +02:00
edde746 75704c7a9b test(player): cover tracks-after-clusters MKV seeking on device
Canary asserts stock media3 1.11.0 still reports these files unseekable
and snaps seeks to the start — when a media3 upgrade makes it fail, the
TrackAwareSeekMap repair in CuelessSeekExtractorWrapper can be retired.
The second test drives the production wrapper stack and requires the
seek to hold.
2026-08-17 04:47:06 +02:00
edde746 f2ce587a85 fix(player): seek via per-track cues in MKV files with Tracks after Clusters
media3 1.11.0 builds the Matroska seek map while parsing Cues, which for
files whose Tracks element follows the Clusters is before any track is
known — the map permanently reports unseekable and ExoPlayer coerces
every seek to t=0, snapping playback to the start. Route seeks through
TrackAwareSeekMap's per-track cue lookups using the track IDs observed
on the extractor output.
2026-08-17 04:39:59 +02:00
edde746 947fddf540 fix(ci): resolve the repository root after the symbols script moved
The scripts reorganization moved upload_symbols.dart from scripts/ into
scripts/release/ but kept the single-.parent root resolution, so the
script searched scripts/build and scripts/debug-info and every CI symbol
upload failed with "no symbols found". The sibling checks scripts were
updated to .parent.parent in the same move; match them.
2026-08-17 02:46:36 +02:00
edde746 50a9365be8 fix(linux): keep the Wayland display away from mpv on software GL
On a Wayland session whose compositor hands clients no GPU device
(Cinnamon/Muffin 6.6.3 on Mint 22.3), Mesa falls back to llvmpipe and
libva-wayland's vaInitialize segfaults inside mpv_render_context_create
the moment mpv is given the wl_display - the app silently closes when
playback starts. 2.14 was the first release to hit this because its
bundled libmpv is the first with VAAPI compiled in, and the device init
runs eagerly at render-context creation, so the hardware-decoding
toggle cannot avoid it.

Skip MPV_RENDER_PARAM_WL_DISPLAY when GL_RENDERER names a software
rasterizer: zero-copy Wayland interop does not exist on llvmpipe anyway,
and mpv's hwdec=auto still reaches the GPU through a DRM render node,
which works even on these sessions. Accelerated sessions keep the
zero-copy handoff unchanged. Verified in a container against Muffin and
weston on llvmpipe: the handle is withheld and playback works.
2026-08-17 02:20:43 +02:00
edde746 06ecba5fb5 fix(player): render Dolby Vision via gpu-next on the Android mpv backend
Dolby Vision Profile 5 files play with pink and purple colors on Android
devices without DV support (#1902): vo=gpu cannot apply DV RPU reshaping,
so the raw IPTPQc2 base layer reaches the screen. The shipped libmpv AAR
builds mpv 0.41 with libplacebo, where gpu-next is the upstream default VO
and reshapes DV correctly.

Prefer vo=gpu-next with an explicit vo=gpu fallback on both the primary
mpv backend and the ExoPlayer failure-fallback path, drop the
vd-lavc-film-grain=cpu override so film grain applies on the GPU under
gpu-next, and report current-vo in player stats.

Verified on a Pixel 7 (no DV display or decoder): gpu-next initializes on
Mali-G710/GLES 3.2, SDR hardware decode is unchanged, and a 4K DV stream
decodes to dolbyvision/bt.2020/pq with correct colors under software
decode. Reshaping still needs software decode: FFmpeg 8.0's mediacodec
wrapper exports no DOVI side data, so hardware-decoded DV keeps playing
the base layer untouched.
2026-08-17 01:41:46 +02:00
edde746 096e285ab0 fix(ui): center sheet back button hover circle on the arrow
Hovering the back button in bottom sheet headers drew the circular
highlight 12px right of the arrow: the 48px hit target was positioned
from the stack's padded origin while the glyph sits flush at the
leading content edge. Move the horizontal padding onto the header row
and position the target so the InkResponse box centers on the glyph.
2026-08-17 01:41:46 +02:00
edde746 aeb6108654 feat(scripts): add the deploy.py multi-channel release pipeline
One command releases to Play, Amazon, App Store Connect (iOS + tvOS), the GitHub build farm, Microsoft Store, and the GitHub release + cask, with checkpointed resume. Replaces the fastlane release flow; first used for 2.14.0.
2026-08-17 01:40:54 +02:00
edde746 f622ba8efe chore(scripts): group scripts into checks, codegen, maestro and release subdirectories
scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
2026-08-17 01:40:54 +02:00
edde746 e47b4e0a73 fix(macos): fall back to AVFoundation audio when CoreAudio fails to open
On macOS 27 beta, CoreAudio rejects ao_coreaudio's channel-layout setup
with paramErr (-50), and since 2.14.0 pinned ao=coreaudio as the only
output, the failed init left every video playing with no audio and no
selectable audio track. Append avfoundation as the fallback, mirroring
upstream mpv's macOS probe order: every format still opens through the
HAL-backed CoreAudio path when it works, and the fallback only engages
when CoreAudio's init fails outright.

close #1964
2026-08-16 22:40:53 +02:00
edde746 9dd8aa4679 fix(macos): remove the audio passthrough option
Enabling audio passthrough on macOS 2.14.0 silenced every AC3/EAC3/DTS
item: the macOS player now pins ao=coreaudio, where audio-spdif redirects
to coreaudio_exclusive. That needs an IEC61937-capable device Mac setups
essentially never have, and with a restricted ao list mpv has no PCM
fallback, so the failed AO init stalls playback with no audio. The toggle
never delivered real bitstreaming on macOS anyway (2.13 decoded through
AVPlayer), so stop offering and applying it there.
2026-08-16 22:40:53 +02:00
edde746 3f6e4c0716 fix(windows): render black letterbox bars in HDR via newer bundled libmpv
With HDR playback enabled on Windows, letterbox bars rendered dark gray
instead of black on HDR displays, for both SDR and HDR content. The
bundled libmpv (20251228-git-a58dd8a) carries a libplacebo that maps the
gpu-next background clear color through the display's reported black
point in HDR mode; libplacebo ff2799a67 (2026-01-07) fixes it by using
infinite contrast for the background. The pin had already been past the
fix (20260303) but was downgraded to 20251228 when downloads moved to
SourceForge.

Bump to 20260809-git-dd5d17d328 (mpv v0.41.0-920, libplacebo v7.371.0),
verified to contain the fix commit.

close #1965
2026-08-16 22:40:52 +02:00
edde746 49c117db83 fix(music): open the gapless next track ahead of the boundary
Gapless playback still gapped between tracks on network streams: mpv
only opened the armed next track's stream at the moment the current one
ended, so any server whose connect+probe outlasts the audio output's
buffered tail (~0.5s) - remote Plex over TLS, a transcode session
starting up - produced an audible dropout at every transition.

Enable mpv's prefetch-playlist when arming a network track so the open
happens while the current track still plays. Measured on a Pixel 7 the
boundary goes from 60-233ms of inline network work to 9-16ms with no
network activity at all; a failed or superseded prefetch falls back to
the old boundary open. Local fdclose:// arms keep prefetch off: an
early open would consume the fd while playlist-pos still reads 0,
breaking _clearArmedNext's "provably never opened" close proof.

close #1869
2026-08-16 22:40:52 +02:00
github-actions[bot] d77836ef22 chore: update cask to 2.14.0 2026-08-16 16:38:32 +00:00
edde746 92b0524edb chore: bump version to 2.14.0 2.14.0 2026-08-16 17:00:37 +02:00
edde746 bb95d54bac style(android): fix ktlint violations in PgsCompositionParser 2026-08-16 16:57:27 +02:00
Aldo BarrerasandGitHub 36b2549506 Add loudnorm to enabled ffmpeg filters. (#1958) 2026-08-16 16:48:29 +02:00
edde746 9c6fcd2625 refactor(player): share one playback-open orchestration between start and reload
_startPlayback and _reloadMediaInPlace each inlined the same ~180-line open sequence around the playback_open.dart helpers — frame-rate prep, display priming, startup gate, external-subtitle plan, open, track manager build, track apply, gate release — with comments instructing that the two copies be kept in sync, and they had already drifted (live _isTranscoding vs result.isTranscoding, Watch Together attach vs detach/reattach).

The sequence now lives once in _openResolvedMedia; the genuine divergences are explicit parameters and caller hooks (transcoding source, WT handling, session-commit boundary, automotive start deferral, resume timing), so a future change to open sequencing lands in both flows by construction. Every await boundary and staleness guard keeps its original position in both flows.
2026-08-16 16:48:02 +02:00
edde746 24f63a63c5 refactor(libraries): make loadItems the single library-tab load hook
BaseLibraryTabState documented an abstract loadData() that every tab "must implement", but three of the four tabs override loadItems() entirely and carried never-invoked empty loadData stubs; only the recommended tab exercised the contract, so the class docs described an extension surface that did not exist.

loadItems() is now the one overridable hook, and the shared load transaction (generation tracking, localized error mapping, post-frame onDataLoaded) lives once in the protected runLoadTransaction helper that the recommended tab and the focus tests route through. No tab's load behavior changes.
2026-08-16 16:48:02 +02:00
edde746 364cdea59b refactor(libraries): show server labels with one policy in the picker and the dropdown
The library quick picker and the libraries dropdown each hand-rolled their own BackendBadge + server-name label rendering with divergent show-when policies: the picker only labeled libraries whose titles collided, so a uniquely-titled library on a multi-server list showed its server in the dropdown but not in the picker.

Both surfaces now share library_server_label.dart — one label widget, one grouping loop over groupLibrariesByFirstAppearance, and one policy: group headers whenever the visible list spans more than one server, per-row labels only in ungrouped lists. The picker's duplicate-title heuristic is gone; its test now pins the unified policy.
2026-08-16 16:48:02 +02:00
edde746 ab199b04cd refactor(profiles): resolve the active profile's Plex token through one shared helper
The job "which Plex token represents the active profile right now" was implemented three times against the same registries — the Discover session supplier, the Seerr token supplier, and UserProfileProvider's settings refresh — and the copies had already diverged on whether a Plex Home profile may fall back to the account token.

resolveActivePlexToken in lib/profiles/active_plex_token.dart now owns the policy: the per-user ProfileConnection token wins when present, else the account-owner token, with an explicit allowAccountTokenForHomeUser flag (true for Discover/Seerr, false for the settings refresh, which must not impersonate the owner). The three call sites keep only their genuine differences.
2026-08-16 16:48:02 +02:00
edde746 ef2ab13abd refactor(profiles): render the profile picker from ActiveProfileProvider
profiles_view.dart rebuilt the exact merged-profile view ActiveProfileProvider already computes — the same four source streams, the same merge/avatar derivation, plus a hand-rolled combineLatest4 — and the profile switch screen was its only consumer while already reading the provider for activeId.

The screen now renders from the provider (new connectionsByProfile/connectionsById/plexHomeByConnectionId getters) and gates loading on provider initialization; visibleProfileConnections moved to profile_merge.dart for profile_detail_screen; profiles_view.dart is deleted. The switch-screen tests initialize the provider up front like boot does, using a timer-less PlexHomeService subclass so start()'s periodic refresh timer cannot trip the widget-test pending-timer invariant; the deleted pipeline's merge assertions were ported to profile_merge_test and active_profile_provider_test.
2026-08-16 16:48:02 +02:00
edde746 fc061dc38d refactor(media): merge the two MediaStreamKind enums into one shared enum
lib/media declared two different enums named MediaStreamKind — a 4-value one in media_stream.dart and a 7-value one in media_file_info.dart — so any file importing both libraries silently picked one by import order.

media_stream.dart's enum now carries the full member set (image/data/lyric added before unknown; the original four ordinals are preserved) and media_file_info.dart re-exports it instead of declaring its own.
2026-08-16 16:48:01 +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 ec605fd8b7 fix(downloads): keep the parallel download limit intact on slow networks
On a slow connection a queued batch of episodes would end up
downloading all at once instead of one at a time. Every download that
hit Android's 9-minute background task limit re-enqueued its
continuation outside the native holding queue's accounting while the
interrupted run freed a slot, permanently raising the effective
concurrency; the notification Resume action leaked the same way on
both Android and iOS.

Pins background_downloader to a fork revision that routes timeout and
notification resumes through the holding queue, only adjusts its
counters for tasks the queue actually promoted, and periodically
recalculates Android queue state the way iOS already did.

close #1955
2026-08-16 16:22:03 +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 a7354cd3b6 fix(android): render every PGS composition object in ExoPlayer
PGS subtitles vanished or displaced each other when a display set put two
images on screen at once (dialogue plus a sign or song caption), and
palette-only fade updates blanked the subtitle entirely. media3's PgsParser
keeps one bitmap buffer and only the first composition object's coordinates,
and it discards all state between display sets.

Replace it with PgsCompositionParser, a port of FFmpeg's pgssubdec model:
epoch-scoped object/palette caches keyed by id, in-place palette updates,
one cue per composition object reference, and limited-range BT.709/BT.601
color conversion selected by plane height.

close #1953
2026-08-16 14:03:51 +02:00
edde746 eaccef33dc fix(subtitles): clear a displayed ASS cue immediately when subtitles are disabled
Turning subtitles off (or hiding them) while an ASS/SSA cue was on screen left
that cue painted until its natural end time on the ExoPlayer backend: AssHandler
nulls the libass track, after which every render returns null, no payload ever
reaches the GL thread again, and the overlay keeps its last swapped atlas. The
existing invalidateSubtitles() call from the #1387 fix could never repaint it.

The atlas pipeline now detects a trackless render request and hands the GL
thread one zero-quad payload, which clears and swaps a transparent frame. The
clear is keyed on the renderer state generation so one dropped as stale is
retried, and it self-heals from per-video-frame requests while playing; the
existing invalidate on the disable transition covers the paused case.

close #1884
2026-08-16 12:42:04 +02:00
edde746 2ff3b350ae feat(player): give sync delay sliders 50ms steps over a ±10s range
The sync delay slider spanned ±60s with 100ms steps, making fine
adjustment between e.g. 100ms and 200ms impractical on touch and mouse.
The slider now covers ±10s at 50ms per step (taps and D-pad included),
while the +/- step buttons still reach the ±60s absolute limit via
long-press, so existing large saved offsets keep working and display
their true value.

close #1907
2026-08-16 09:18:49 +02:00
edde746 bb642fce03 fix(plex): send Live TV favorite updates as JSON so they persist again
Adding or removing a Plex Live TV favorite channel never persisted: the PUT
to epg.provider.plex.tv/settings/favoriteChannels answered 400. The dio ->
package:http migration (15b22f75) lost dio's implicit JSON content type, so
the JSON-encoded favorites list went out as text/plain and the Plex cloud
refused to parse the body. Verified live: the identical payload succeeds
with application/json and the mutation persists.

MediaServerHttpClient now defaults content-type to application/json for
structured bodies. Callers and client defaults still win (the headers map
is case-insensitive and already populated), so Jellyfin's pinned default
and the octet-stream artwork upload are unaffected; favorites is the only
Plex request with a JSON body.

close #1878
2026-08-16 09:14:28 +02:00
edde746 8069683bd3 fix(ios): give music playback lock-screen and headphone controls
Music on iOS played through a mixable audio session: mpv's audiounit
output requests mixWithOthers unless audio-exclusive is set, and a
mixable session disqualifies the app from Now Playing. iOS ignored the
published metadata and remote-command targets, so the lock screen showed
no controls, headphone buttons drove the previous media app, and other
apps kept playing alongside plezy.

Set audio-exclusive on the audio-only core at init on iOS — the same
contract the video player already applies at playback start. Its only
effect there is dropping mixWithOthers.

close #1921
2026-08-16 09:00:49 +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