Commit Graph
3071 Commits
Author SHA1 Message Date
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 fc96ddc1ab style(android): fold initialVideoOutput body onto its signature per ktlint 2026-08-19 23:06:08 +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 65713858ea fix(tvos): open the sign-in keyboard after fast server probes instead of freezing remote input
Adding an Emby server on Apple TV locked the app up after entering the
URL: the server probe answered inside the URL keyboard's dismissal
animation, so the username field's system keyboard silently never
presented while its editing session kept ownership of every remote
press — leaving only Menu, which suspends the app. Jellyfin was
unaffected in practice because Quick Connect skips the username focus.

Engine 3.44.0+6 defers first-responder takes until the in-flight
keyboard presentation clears and routes remote presses to the engine
whenever no keyboard is actually on screen, so the session can no
longer strand the user.

close #2011
2026-08-19 12:57:15 +02:00
edde746 744e72d76b fix(player): stop continuous Dolby Direct Play judder via MPVKit 1.0.23
Direct Play of any (E-)AC-3 track on Apple TV developed persistent
micro-stutter: mpv's audio clock was derived from es_pts on the
compressed path, which is credited a whole 32 ms burst the moment a
payload lands, sawtoothing the clock on a ~0.8 s cycle that video
retires as visible skip/hold pairs. AAC content was unaffected, which
is why the symptom tracked Dolby audio and survived every VO-side fix.

MPVKit 1.0.23 measures the clock from IEC samples actually handed to
ao_read_data instead. Verified with an HDMI capture rig against the
CI-built binary: EAC3 display cadence is now statistically identical
to AAC (was 47.6 skips/min plus 453 multi-frame holds over 38 min).

close #1776
2026-08-19 12:57:15 +02:00
edde746 761303de11 fix(subtitles): render zlib-compressed embedded MKV subtitles on the ExoPlayer path
Embedded subtitles on mkvmerge-muxed files with compressed subtitle tracks
(the anime-release convention, reported against AV1 encodes) never rendered:
the track selected fine but nothing ever appeared on screen.

Two defects compounded. ContentCompAlgo 0 (zlib) is the Matroska default
value, so mkvmerge omits the element and the explicit-value detection never
engaged - media3 ignores an empty ContentCompression and silently emits
compressed samples. And for text subtitle tracks TrackOutput-level inflation
can never work: MatroskaExtractor prepends the plaintext timecode prefix to
the still-compressed payload and truncates the sample at the first NUL byte
before any TrackOutput runs, while AssTrackOutput feeds that same internal
buffer to libass, which then parses zero events and reports every frame as a
changed empty render.

Treat the presence of ContentCompression as zlib until an explicit
ContentCompAlgo says otherwise, and for SRT/ASS/SSA/VTT tracks inflate the
block frame payload before the parent's subtitle sample assembly, leaving the
TrackOutput wrapper to the formats it can actually handle. Blocks that are
laced, corrupt, or over-bound pass through byte-identical. The detection fix
also makes the existing whole-sample TrackOutput inflation engage for
PGS/VobSub zlib tracks, which media3 does not truncate.

Verified with mkvmerge zlib/plain fixture extraction tests and end-to-end on
an API 36 emulator against a Jellyfin direct-play mux mirroring the reported
file.

close #2023
close #2022
2026-08-19 12:34:40 +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 4b6533d25b fix(linux): stop the HDR watchdog from firing after synchronous no-op transactions
On a Wayland HDR session, colors flickered between HDR and washed-out SDR every ~5 seconds: the mpv-leg timeout in apply_hdr_state was armed after SetHdrOutput returned, so a synchronous reply (the no-op short-circuit that runs on every playback restart and re-describe) orphaned the timer. Five seconds later it fired against a healthy plane and withdrew the live HDR image description; the next re-apply re-attached it and armed the next orphan.

Arm the timeout only while the reply is genuinely outstanding. An asynchronous reply still removes it, and a genuinely wedged core is still bounded.

Verified in a headless-sway Docker repro against the same PQ/BT.2020 file: 2.15.0 logs the false 'mpv never answered the output colour-space switch' warning 5 s after plane bring-up and again after source recognition; the fixed build logs none across bring-up, HDR playback, and seeks.

close #2016
2026-08-19 06:07:25 +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 2a6f752860 fix(release): make multi-channel deployment resumable 2026-08-18 18:00:52 +02:00
github-actions[bot] 16c0e00302 chore: update cask to 2.15.0 2026-08-18 15:05:13 +00:00
edde746 6d75e58f52 chore: bump version to 2.15.0 2.15.0 2026-08-18 14:50:24 +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 35eef09f72 fix(tvos): present video on the host clock instead of the media timebase
Direct Play on Apple TV develops recurring micro-stutter that worsens
the longer playback runs and clears temporarily on pause/play
(issue #1776). Since 2.4 the avfoundation VO has presented frames
against a media-time CMTimebase anchored and rate-servoed to follow
mpv's audio-slaved schedule; clock skew between the two domains can
only be retired by rate slewing or a re-anchor snap that retimes every
queued frame at once. The media timebase exists for PiP, which tvOS
does not have.

Bump MPVKit to 1.0.22 and opt tvOS into its new
avfoundation-presentation=host mode: samples carry mpv's scheduled
display time against a free-running host-clock timebase, so drift is
absorbed per frame inside mpv and no timing state accumulates in the
VO. The queue lead returns to 128ms; 500ms only dampened media-mode
snaps and quadrupled their blast radius. 1.0.22 also paces the
compressed-passthrough feed off the most-advanced playhead reading,
fixing the periodic audio dropouts passthrough users reported on 2.14.
2026-08-18 08:38:46 +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 93d3ec65e2 fix(exoplayer): decode DTS with FFmpeg when the stream cannot bitstream
DTS-HD files play silently on the Onn 4K Plus: DTS decode is license-gated
in its firmware, so c2.amlogic.audio.decoder.dtshd initialises, drains and
advances the playback position while rendering silence. Route DTS-family
decode to the bundled FFmpeg decoder whenever the track will actually be
decoded - passthrough off, downmix, normalization, a failure block, or a
route that cannot bitstream DTS in any shape. Bitstream-capable routes are
untouched: media3 selects direct output before it ever consults the decoder
list, so raw passthrough, the IEC 61937 carrier and the tunneling gate keep
their exact behavior. Kodi ships the same policy as its only configuration:
its MediaCodec audio whitelist is empty and DTS always decodes in FFmpeg.

close #1995
2026-08-18 06:28:28 +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 b4a84c6a28 fix(profiles): don't re-prompt for profile when a system overlay closes
On Fire TV, invoking Alexa and dismissing its overlay popped the profile
picker over a still-playing session: MainScreen treated every resumed
lifecycle event as an app open and re-applied "ask for profile on open".
The Alexa overlay only produces inactive -> resumed, which the handler
could not tell apart from a real return to the foreground.

Track the deepest lifecycle state since the last resume in a small gate
(ProfileSelectionResumeGate): only a genuine backgrounding (hidden,
paused, or detached) arms the prompt for the next resume. This also
keeps iOS working, where returning from the background arrives as
hidden -> inactive -> resumed.

close #1990
2026-08-18 02:09:28 +02:00
edde746 c5e7ce648b fix(player): gate audio passthrough on the IEC 61937 shapes each backend opens
On Shield Experience 8.x (API 28) ExoPlayer force-decoded TrueHD to
multi-channel PCM, and enabling passthrough on the mpv backend made every
non-AAC file wedge on an infinite buffering spinner with no audio pipeline
at all.

Two defects behind one symptom pair:

- The MAT/DTS-HD carrier oracle stopped at API 29 because
  AudioTrack.isDirectPlaybackSupported does not exist below it, so API 24-28
  never offered the carrier even on routes that genuinely bitstream it. Below
  API 29 the HDMI AudioDeviceInfo is the only vouching signal, so the carrier
  is now offered when an HDMI output explicitly advertises IEC 61937 at
  192kHz/8ch; unspecified (empty) capability arrays deliberately do not
  count, because an unvouched IEC track that initialises without being
  bitstreamed renders as full-scale noise. A route that advertises and still
  refuses the track fails AudioTrack init into the existing force-decode
  recovery.

- The mpv audio-spdif list was gated on the raw encodings the route
  advertises, but mpv's audiotrack AO opens every spdif format as a stereo
  ENCODING_IEC61937 track clamped to the 48kHz mixer rate. E-AC3, TrueHD and
  DTS-HD MA structurally cannot survive that shape, and a route can advertise
  raw encodings while its HAL takes no IEC track at all - naming any codec
  there leaves mpv's audio chain stuck before AO init (reproduced on a Shield:
  spdif_ac3 selected, no AudioTrack ever opened, playback never starts). The
  list is now capped to ac3,dts and additionally gated on the route accepting
  the stereo IEC shape, using the same tiered oracle as the carrier. The
  primary mpv backend previously wrote a hardcoded five-codec list from Dart;
  it now asks the plugin to derive the value from the audio route, like the
  ExoPlayer fallback already did.

Verified on a Shield (API 30) with a full-capability EDID: TrueHD rides the
carrier and survives speed transitions (instrumentation), and an E-AC3 title
that previously wedged under mpv+passthrough now decodes and plays with
audio-spdif=ac3,dts.

close #1991
2026-08-17 23:34:05 +02:00
edde746 7cfd68c491 test(player): match the renamed carrier diagnostic in the mismatch regression
The rate-mismatch regression greps the audio diagnostics for the literal
'MAT/IEC 61937 carrier', but 658469f9 reworded the sink's engage message to
'TrueHD (MAT) via IEC 61937 carrier' when DTS-HD joined the carrier. The test
self-skips on carrier-less CI emulators, so the rot only surfaces on real
hardware, where the fallback behaves correctly and the assertion still fails.
Match the shared 'via IEC 61937 carrier' fragment both codecs emit.
2026-08-17 23:33:35 +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 1bdeca3b5f fix(libraries): give square music grids an inter-card gutter
Square-card (music) grids had zero delegate spacing - the only
separation was each card's internal 3px padding, and square shapes are
excluded from the full-card spacing path, so playlist and album tiles
sat nearly edge to edge. Square grids now get an 8px cross/main gutter
(automotive keeps its larger spacing); poster, list, and full-card
layouts are unchanged.
2026-08-17 22:44:12 +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 417f3a5e36 fix(livetv): keep the guide mounted during reloads and apply day picks immediately
The guide replaced itself with a bare spinner on every window change,
unmounting the day-picker anchor and the guide focus node - after one
day or time change the pickers silently no-oped and the remote went
dead. Picking a day also applied nothing by itself: the window only
moved after the second (time-slot) menu, so backing out of it read as
'the day picker does not work'.

The grid now stays mounted during in-session reloads behind a light
loading overlay, a day pick re-anchors the window immediately (the slot
menu is an optional refinement), D-pad SELECT arms the key-up
suppressor before opening the menu, and the day chip uses the existing
'Tomorrow' translation.
2026-08-17 21:36:35 +02:00
edde746 d4d44e6c72 fix(music): show user-assigned Plex playlist poster instead of the auto composite
Plex emits an auto-generated composite mosaic for every playlist, so the
displayImagePath fallback order (composite first) meant a poster the user
assigned in Plex could never appear. Prefer thumbPath and fall back to the
composite; no-op for Jellyfin/Emby, which never set a composite.
2026-08-17 20:59:13 +02:00
edde746 658469f954 feat(player): bitstream DTS-HD MA through the IEC 61937 carrier on Android
Fire TV Stick 4K Max (and other Fire OS devices) advertise ENCODING_DTS_HD on
the HDMI route but only implement the DTS-HD basic profile, so media3's raw
path builds an AudioTrack that drains normally while the receiver hears
silence. The same routes do bitstream the 192kHz/7.1 ENCODING_IEC61937 carrier
TrueHD already rides, so DTS-HD MA access units are now packed into DTS type IV
bursts (a port of FFmpeg's spdif_header_dts4 at the 768kHz HD rate, the same
bytes Kodi's IEC packer produces) and played through that carrier.

The TrueHD MAT carrier sink is generalized into IecCarrierSink with one
IecCarrierPacker per codec. While the carrier route exists DTS-HD is binary -
the carrier or decoded PCM, never the lying raw path; routes without the
carrier keep the pre-carrier raw behavior. Master Audio peaks the carrier
cannot hold strip to the always-fitting core substream for ~60s, exactly as
FFmpeg does, and the packer output is pinned byte-for-byte against FFmpeg
spdif golden fixtures.

close #1988
2026-08-17 20:58:04 +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 5fe1e6dc65 refactor(ui): delete unwired log output, dead notifier filters, and never-passed widget params
MemoryLogOutput extended LogOutput but was never wired as a Logger output (storage happens in the printer); DeletionNotifier/WatchStateNotifier's forServer/forItem filtered streams and WatchStateEvent.mediaType had no production consumers (tests now filter .stream directly); context.hiddenLibraries/profileSettings and toPlexUrl had zero call sites; MonoTokens.splashFactory was never read; OptimizedMediaImage's enableTranscoding/cacheKey chains, FocusableMediaCard.width/height/forceGridMode, TvBrowseRail's constant-zero gap functions, and media_image_helper's scaleFactor were never varied by any caller. The sha1 LRU stays (rebuild-hot artwork URLs); TvRailTrailing.none and the mono-theme copyWith stay live (lerp delegates to copyWith). Also carries the MusicPlayContext.id argument drops in the music screens and remaining sweep test updates.
2026-08-17 19:02:25 +02:00
edde746 1df47ed8e1 refactor(watch-together): remove the unconstructable disconnected state and write-only session surface
Participant.lastKnownPosition had zero reads or writes; SessionState.disconnected was never constructed, making isInSession's comparison tautological (now _session != null); the session indicator's onLeaveSession callback had no production passer; enterRoom's bool was awaited and discarded. Music engine: MusicPlayContext.id and sleepTimerEndsAt were write-only, and the coordinator constructor seam was never injected.
2026-08-17 19:02:25 +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 7cd5bb64bb refactor(player): remove dead completion-latch channels, unreachable settings states, and unread DV stats
classifyPosition's return enum was discarded by its only production caller (rearm is the side effect that matters); the latch's tolerance/retry knobs were never tuned; _loadAdjacentEpisodes' result was unread by all three callers; countsAsApplied carried a subsumed disjunct and _syncMediaControlsAvailability a stale-generation guard with no suspension point before it; _SettingsView.audioSync/subtitleSync could never be current (both routes early-return into the sync bar); PerformanceStats.dvRpuOutputTooSmall/dvPlaybackReason and ContentStrip.chaptersLoaded were written but never read.
2026-08-17 19:02:24 +02:00
edde746 b38df008ec refactor(screens): delete never-passed knobs, dead enum values, and the unconsumed context-menu onTap
plex_pin_auth_flow's QR knobs, ProfileNameField's navigation params, MusicDetailHeader.wideArtworkSize, and _buildReorderableList's bool were never passed; DownloadType.manage was never constructed; MediaContextMenu.onTap was declared but never invoked — removed with its four card passes (each card's own gesture handlers stay); the pin dialog's obscure chain was always true and _PinKey.label's icon branches unreachable; libraries_screen._isInitialLoad was write-only; LiveTvActionsMixin loses dead findChannel (the generic parameter stays — required by the on-clause, the report was wrong); add_jellyfin's focus-map removal branch ran only against an empty map; the recommended tab's hub-key null fallback was dead behind _ensureHubKeys. media_card also loses its always-false seasonal-rank comparison from the CatalogRankScope removal.
2026-08-17 19:02:24 +02:00
edde746 ceab5a5b59 refactor(providers): remove the unreachable init fallback, unread load results, and dead refresh surface
hidden_libraries' constructor always seeds _initFuture, so ensureInitialized's ?? _initialize() fallback could never run (field now late final); _loadLibrariesInternal returned a bool no caller read; OfflineModeProvider.refresh() had zero call sites; ExploreScreen's Refreshable application was dispatched by nobody (only the discover tab and library tabs are dispatched); the discover null-serverId branch stays — the TV spotlight resolver passes null items during initial load.
2026-08-17 19:02:24 +02:00