Commit Graph
44 Commits
Author SHA1 Message Date
edde746 74d3af3ae1 perf(home): load the home screen once instead of twice per cold start
The Discover tab fanned out its whole request set twice on every cold
start and replayed slow rows on a shrinking timeout ladder, so a healthy
remote server produced anywhere from 4s to 15s of loading.

Measured against a remote Jellyfin server with four libraries, 24
interleaved cold-start samples per side:

  requests  19 -> 9      payload  219 KB -> 94 KB
  settled   5231ms -> 2502ms median, 13222ms -> 5927ms p95

Four independent causes:

- Retry policy. `Client.send` resolves on response headers, so the
  connect budget covers the server's think time and a slow-but-alive
  query raises `connectionTimeout`. Replaying it made the server re-run
  the query with a shorter budget than the one it just missed; the
  `[10s, 8s, 5s]` ladder turned an 11s answer into an empty row after
  23s. Hub surfaces now get one whole-request deadline, retry only
  immediate connection errors, and the deadline bounds the whole call
  including the request still in flight.

- Request shape. `/Items/Latest` groups a TV library by series, so its
  rows are Series folder dtos and `RecursiveItemCount`/`ChildCount` cost
  a DB count each, per row. Hub rows now ask for `Overview` only; watch
  state survives because Jellyfin derives `UserData.Played` from
  `UnplayedItemCount` when the count fields are absent. `/Shows/NextUp`
  sends `NextUpDateCutoff` to bound the server's series-key scan, and
  `Thumb` leaves `EnableImageTypes` since nothing reads it. `UserData`
  and `PremiereDate` leave the browse set: neither is an `ItemFields`
  member, so the server dropped them anyway.

- Fan-out. Per-library hubs ran in batches of three separated by a
  barrier, so one slow library stalled every library behind it. A
  sliding window keeps the same peak concurrency without head-of-line
  blocking. Concurrent `fetchLibraries` calls now share one `/Views`
  instead of racing two identical round trips, Plex's global and music
  hub legs start together, and Jellyfin gets Plex's pool tuning.

- Duplicate pass. `DiscoverScreen.initState` starts a load and the
  online-entry hook asked for a full refresh on top of it, which
  `CoalescedLoadCoordinator` correctly queued as a trailing pass. The
  hook now calls `primeRefresh`, which rides along with a load already
  in flight; profile switches still go through `fullRefresh`.

Refs #1784
2026-08-04 04:35:06 +02:00
edde746 860ce1e11a feat(profiles): show the first linked connection's user picture
A local profile had no picture of its own and always fell back to
initials. It now borrows the user picture of the connection it was
linked to first — oldest Connection.createdAt, ties broken by
connection id, since the join table carries no creation time.

Jellyfin links resolve to /Users/{id}/Images/Primary, keyed by the
PrimaryImageTag now captured at authentication and refreshed from the
/Users/Me body checkHealth already fetches. That endpoint is anonymous
on every Jellyfin release, so the URL carries no api_key and the access
token stays out of the image cache key. Plex links resolve the Home
user the link points at against PlexHomeService's live cache, so no
account-level lookup is needed and the picture tracks Plex's own
refresh.

The picture is derived per snapshot and never written back onto a
Profile: ProfileDetailScreen upserts the model it holds, so a
persisted URL would go stale and outlive the connection it came from.
Plex Home profiles are untouched, including one whose Plex avatar is
unset — it keeps its initials rather than borrowing a lent connection's
picture.

close #1667
2026-08-04 02:22:44 +02:00
edde746 2a7e5f4f9c fix(jellyfin): ask the server who may delete before offering it
Jellyfin never consults IsAdministrator when authorizing a library
delete: BaseItem.IsAuthorizedToDelete looks at EnableContentDeletion and
the per-library grant, and only the first user a server creates gets the
former for free. Gating the "Delete from server" entry on the admin bit
therefore offered a destructive action that answers 401 to later
administrators, and hid it from plain users who do hold the grant.

Ask the server per item instead, through the new
MediaDeletionPermissionClient capability: BaseItemDto.CanDelete already
folds the global grant, the per-library grant, and item state such as
missing files or an in-progress recording. The probe runs when a menu
opens on a deletable kind, costs ~0.5 KB, carries a whole-request
deadline because the client's own budget covers connect and receive
separately, and fails closed on anything unknown. Plex keeps its
account-level owner/admin gate; it has no per-item permission on the
wire.

close #1749
2026-08-02 07:08:14 +02:00
edde746 daab4f1e24 fix(player): preserve the forced-subtitle class across episode boundaries
Plex treats a subtitle stream as forced when its title says "Forced" even
with the API flag unset. Every forced comparison now uses that effective
forced-ness on both sides: the match scorer, the low-metadata hard gate,
the Jellyfin OnlyForced/Smart profile modes, and stream-index negotiation.

Carrying a track choice into the next episode no longer reuses the
same-item identity matchers. A sealed SubtitlePreference (off / track
reference / semantic intent) replaces the id-'navigation' pseudo-track
through the whole preference channel, and cross-item intents hard-require
language and forced-class parity. When the next episode has no track of
the same class, the intent declines and selection falls through to the
server's own per-episode choice instead of latching onto a full track by
position and persisting that mistake back to the server.

Intents wait for pending native tracks under the same catalog-completeness
rule as source ids, so an early decline cannot retire the selection
listener before the real track arrives.

Ref #1716
2026-07-30 02:46:10 +02:00
edde746 53535e1678 fix(jellyfin): percent-encode the MediaBrowser auth header
Since real device names started reaching the header, an accented one
made login impossible: dart:io refuses header values above 0x7F, and
CFNetwork puts the raw code unit on the wire as a Latin-1 byte, which
Kestrel rejects as a malformed request with 400 before Jellyfin routes
POST /Users/AuthenticateByName.

Encode every field the way the official Jellyfin SDK does; the server
already reverses it with WebUtility.UrlDecode, so the wire value stays
pure ASCII while the device list shows the real name. Quotes, commas
and `=` no longer need stripping either. sanitizeHeaderValue, which
still guards the Plex headers, now folds Latin letters to their base
form instead of emitting bytes no transport accepts.

close #1685
2026-07-28 05:07:00 +02:00
edde746 352b88109b refactor: extract shared mixins and helpers, drop dead abstractions
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.

Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
2026-07-26 06:09:48 +02:00
edde746 c7b9eec087 fix(player): preserve subtitles across episode changes
close #1635
2026-07-24 21:40:22 +02:00
edde746 e0bf66eea8 fix(runtime): harden application service boundaries 2026-07-24 03:46:46 +02:00
edde746 d5f3e581df fix: eliminate cross-app consistency drift 2026-07-13 23:13:53 +02:00
edde746 359a6bf02a refactor(live-tv): split optional DVR support 2026-07-12 08:42:25 +02:00
edde746 fff559da06 feat(music): add music data layer for plex and jellyfin
Backend-neutral fetchArtistAlbums/fetchAlbumTracks/fetchInstantMix/
fetchLyrics, lyrics model + LRC parser, audio quality presets, music
transcode (plex /music universal, jellyfin audio PlaybackInfo profile),
station/InstantMix radio, music search, playlist type derivation, and
audio download resolution.
2026-07-05 15:55:52 +02:00
edde746 ee3c75ba9e fix(playback): remember last played media version for continue watching
close #1492
2026-07-05 14:29:57 +02:00
edde746 d80a1ed15a feat: report real device identity to media servers
Resolve platform, hardware model, and friendly device name once via a
shared DeviceIdentityService and send them to both backends: Plex gets
a real X-Plex-Platform plus X-Plex-Device/X-Plex-Device-Name (shown as
Player in dashboards/Tautulli), Jellyfin gets the device name in the
MediaBrowser auth header. Transcode and live-TV decision requests keep
their pinned platform names, which Plex validates server-side.

close #1270
2026-07-05 01:22:20 +02:00
edde746 791433bceb fix: download and queue episodes in watch order, Specials last
close #1414
2026-06-25 10:47:13 +02:00
edde746 01a5d11ef5 fix(jellyfin): use season episode endpoint 2026-06-14 18:34:12 +02:00
edde746 23a423fe85 refactor(livetv): playback session lifecycle behind LiveTvSupport 2026-06-12 16:06:12 +02:00
edde746 665603b314 fix(jellyfin): bounded transient retry on hub surfaces 2026-06-12 15:33:58 +02:00
edde746 0d7dd093c4 fix(jellyfin): flip server status when all endpoints are exhausted 2026-06-12 15:26:39 +02:00
edde746 c849a213f0 refactor(net): shared FailoverHttpClient for both backends 2026-06-12 15:22:52 +02:00
edde746 c2fe142293 refactor(watch): WatchActions owns mark-watched orchestration and events 2026-06-12 14:09:21 +02:00
edde746 92063bd58f fix(playback): skip redundant mark-watched
close #1287
2026-06-09 12:19:27 +02:00
edde746 8efd8f1dc8 fix(jellyfin): upload artwork as binary 2026-06-07 13:32:07 +02:00
edde746 74b8dc4561 refactor: type server identifiers 2026-06-01 11:06:03 +02:00
edde746 09f5121732 feat(metadata): add Jellyfin edit support 2026-06-01 08:34:38 +02:00
edde746 d5c8778074 fix: harden reconnect and playback regressions 2026-05-31 20:33:38 +02:00
edde746 d93ea9813f fix(playback): harden offline source reporting 2026-05-29 19:55:01 +02:00
edde746 6642348a96 feat(jellyfin): support multiple server urls 2026-05-28 03:43:46 +02:00
edde746 209101796b refactor: use private named constructor params 2026-05-20 23:48:29 +02:00
edde746 c855dba3be fix(media): paginate large server lists 2026-05-17 20:25:39 +02:00
edde746 4577f26aa8 fix(lifecycle): abort HTTP before close 2026-05-12 15:11:16 +02:00
edde746 00917a0393 refactor(media): split media and jellyfin client 2026-05-11 05:24:07 +02:00
edde746 c38c0cf55d fix(jellyfin): hide unsupported continue watching removal
close #995
2026-05-09 04:41:13 +02:00
edde746 5fe5534bae fix(jellyfin): bound home artwork load 2026-05-08 06:06:09 +02:00
edde746 955bc9548c refactor: adopt shared mixins, helpers, and sliver widgets 2026-05-05 03:06:56 +02:00
edde746 ed4be7b96d refactor: strip obvious comments 2026-05-04 22:40:18 +02:00
edde746 9e4230597f fix(jellyfin): label per-library hubs with library name
close #975
2026-05-04 10:24:01 +02:00
edde746 720d0d2604 feat(livetv): add DVR and recording support 2026-05-04 09:31:53 +02:00
edde746 aa3c83bad1 fix(jellyfin): handle external transcode subtitles 2026-05-03 10:12:11 +02:00
edde746 70606c8ac5 fix(jellyfin): align playback and browse requests 2026-05-03 09:33:42 +02:00
edde746 81e036dc4f feat: support Jellyfin media segments
close #960
2026-05-02 18:30:12 +02:00
edde746 1781fc06ae fix: merge Jellyfin next up into continue watching 2026-05-02 11:19:04 +02:00
edde746 8fabf56337 refactor: split playback and media hotspots 2026-05-02 06:33:05 +02:00
edde746 c602936d2a fix: support forced skip marker fallback
close #956
2026-05-01 04:35:30 +02:00
edde746 31d2d9dc98 feat: jellyfin 2026-05-01 01:20:36 +02:00