Commit Graph
95 Commits
Author SHA1 Message Date
edde746 05fd622968 feat(emby): add Emby as a MediaBrowser backend alongside Jellyfin
Emby is Jellyfin's upstream ancestor and speaks a near-identical MediaBrowser
API, so the existing Jellyfin stack is parameterised by a `MediaBrowserDialect`
rather than forked. `JellyfinClient`, its auth service, endpoint discovery, LAN
discovery, and the add/edit connection screens all take the dialect and keep one
implementation; `MediaBackend.emby` and `ConnectionKind.emby` carry it through
the neutral models, the Drift `kind` discriminator, downloads, and caches.

Every divergence below was measured against a live Emby 4.9.5 server, not
inferred from documentation, and each is documented at its capability getter.
Jellyfin's request strings stay byte-identical so nothing about its behaviour
changes.

Routes and auth
- Emby only accepts the pre-10.9 user-scoped item routes (`/Users/{id}/Items/…`,
  `/Users/{id}/PlayedItems/…`, `/Users/{id}/FavoriteItems/…`); the unprefixed
  forms Jellyfin 10.11 added return 404.
- The API is also served under a legacy `/emby` prefix, and both dialects accept
  the token as `X-Emby-Token` or `api_key=`.
- Emby answers only its own LAN discovery datagram ("who is EmbyServer?") and
  ignores Jellyfin's; its default HTTPS port is 8920.
- No `/QuickConnect` route exists, so Quick Connect stays Jellyfin-only.

Row fields Emby withholds
- `ProductionYear`, `OfficialRating`, `PremiereDate` and `DateCreated` are absent
  from list rows unless named in `Fields`, which would otherwise strip the year
  and age-rating badge from every card in the app.
- `UserData.LastPlayedDate` never appears on a list row under `Fields=UserData`,
  `EnableUserData=true` or the user-scoped `Ids=` form — only on the single-item
  detail route, or when the Emby-specific `UserDataLastPlayedDate` token is
  requested. Without it every recency-ordered surface silently degrades to
  library-add time, and `JellyfinApiCache.applyWatchState` stamps
  `DateTime.now()` on watched rows, so an offline watch-state pull would rewrite
  the cached play time of everything it walked.

Continue Watching and Next Up
- Emby computes Next Up per series only: the library-wide `/Shows/NextUp` query
  returns nothing under every parameter combination tried. The shelf is
  therefore reconstructed from a played-episode recency scan plus one
  `/Shows/NextUp?SeriesId=` per distinct series, bounded by a shared wall clock
  that covers the scan as well — per-request timeouts cannot bound the pass
  because `MediaServerHttpClient` times the connect and receive phases
  independently. Rows are stamped with their series' newest play from the same
  response that ordered them, so no per-series enrichment request is needed.
- `/Shows/NextUp` ignores `NextUpDateCutoff`, and no server-side played-date
  filter exists to delegate to (`MinDatePlayed` and `MinDateLastPlayed` are
  ignored; `MinDateLastSaved`, `MinDateCreated` and `MinPremiereDate` filter
  unrelated dates), so the 365-day window is applied to the scanned dates.
- The resume route returns items with no saved position, including plain next
  episodes, so the Emby resume leg reads from `/Items?Filters=IsResumable`.
- Emby is ahead of Jellyfin in one place: `/Users/{id}/Items/{id}/HideFromResume`
  makes Continue Watching removal a real capability.

Everything else
- `/Sessions/Playing` and `/Sessions/Playing/Progress` reject a body with no
  `PlaySessionId` (HTTP 400), so playback reporting always sends one.
- Passing any `MediaTypes` value to the playlist query returns an empty list.
- There is no aggregate `/Items/Filters` route; the four filter facets are
  reassembled from `/Genres`, `/OfficialRatings`, `/Studios` and `/Tags`.
- Metadata writes take name-pair lists (`Genres: [{'Name': 'Action'}]`); the
  plain string array is accepted and then silently discarded.
- Custom artwork uploads must be base64 text, not raw bytes — which was broken
  for Jellyfin too and is fixed for both.
- Trickplay, media segments and lyrics 404 on Emby, so scrub previews are absent
  and intro/credit markers fall back to chapter names.

Verified against a local Emby 4.9.5 and a Jellyfin 10.11.11 control server:
onboarding, browse, detail, playable stream URLs serving real bytes, subtitle
sidecars, watch-state write and restore, hubs, cross-server aggregation and
search across both backends simultaneously.
2026-08-05 06:09:26 +02:00
edde746 4872adcde3 feat(player): keep the session's explicit track choices across episodes
Episode advance carried live player state, so the viewer's choice only
survived while every episode could serve it: one episode without the
picked audio or subtitle fell back, and the fallback became the carry for
the rest of the session. The screen now keeps the last explicit audio,
subtitle, and secondary-subtitle choices for its lifetime; automatic
outcomes never overwrite them, so the choice retries on every following
episode and reattaches as soon as a catalog can serve it again.

Audio catches up with the subtitle carry from #1785. The old matcher
required raw language equality (a 'sv' pick never found a 'swe' row) and
otherwise took the first same-language track, flipping a commentary or
alternate-mix pick back to the main mix on every episode. Audio now uses
the same evidence bands as subtitles: bridged language parity is
authoritative, a unique title match vouches for untagged tracks, codec
and channel-count parity only break ties, and an ambiguous catalog
declines to the server's own choice instead of guessing. The synthesized
source descriptor also prefers the row's own title over the display title
that collapses to the bare language.

Episode advance previously sent no audio hint to negotiation at all, so a
transcode baked in the server's default audio no matter what was playing.
Both backends now resolve the carried semantics against the new episode's
streams: Jellyfin sends the resolved AudioStreamIndex, Plex feeds the
transcode decision, an explicit per-part stream id always wins, and a
failed match falls back to the server's pick.

close #1785
2026-08-04 16:10:02 +02:00
edde746 439ae1d733 perf(detail): paint a show before its on-deck episode is looked up
Jellyfin has no equivalent of Plex's bundled `?includeOnDeck=1`, so a show
detail open chained `/Shows/NextUp` behind the item fetch and the screen sat
on a spinner for both round trips. The second one is not needed to paint:
everything except the play button's episode label comes from the item.

`fetchItemWithOnDeck` now takes an `onItemReady` callback and invokes it as
soon as the item is known, when that is strictly before on-deck settles.
Plex returns both together and never invokes it.

Phone and desktop only. TV keeps its own reveal gate — `_isTvDetailReadyToReveal`
holds the foreground at opacity 0 until extras, related hubs, seasons and the
first episode page have all loaded, and those still run after the on-deck
lookup settles, so TV sees no change. Both halves are pinned by tests.

Measured on a remote Jellyfin server, 15 interleaved show-detail opens per
version: time to content 1264ms -> 1042ms (-18%), with the rest of the load
unchanged.

Seasons and extras deliberately still start after the whole lookup settles.
Starting them at the early paint measured worse (time to settled +21%)
because they contend with the on-deck request instead of overlapping it — the
same reason `/Shows/NextUp` is not fired in parallel with the item fetch.
That trade-off is also why TV was left alone rather than being unblocked by
moving those loads earlier.

Two ordering hazards the early paint introduces, both covered by
`media_detail_screen_test.dart`:

- The early call must not write on-deck. `_loadFullMetadata` runs again after
  playback, and clearing there would blank the play button for the length of
  the round trip. `onDeckSettled` marks the authoritative write, so a reload
  that finds the series finished still clears it.
- A settled empty on-deck must not drop the episode-derived fallback that
  `_ensureFallbackOnDeckEpisode` supplies.

close #1784
2026-08-04 08:38:19 +02:00
edde746 a759e8b3c6 perf(jellyfin): fetch a detail item once when several callers want it at once
Opening a detail screen issued two identical full-detail GETs for the same
id, concurrently: `_loadFullMetadata` calls `fetchItemWithOnDeck`, and
`_initWatchlistState` calls `fetchExternalIds`, which fetches the same item
purely to read `ProviderIds`. Playback start adds three more for its own id.

Each of those makes the server rebuild the entire dto — `People`, `Chapters`
and `MediaSources` cost a database query apiece and `Trickplay` costs several
plus a filesystem stat — so the duplicate is expensive on both ends.

`fetchItem` now shares an in-flight request per item id. Single-flight only:
once a request settles the next caller re-fetches, so nothing can serve a
stale item.

Measured on a remote Jellyfin server, 12 interleaved show-detail opens per
version: requests 4 -> 3, payload 28.4 KB -> 18.6 KB. Median wall time is
unchanged (1394ms -> 1386ms) because the duplicate ran alongside the first
rather than behind it; this removes duplicated work, not latency.

Two things were tried and rejected because measurement did not support them:
starting `/Shows/NextUp` in parallel with the detail fetch (the requests
contend rather than overlap — NextUp went from 380ms alone to 1395ms beside
it — and it costs a wasted request per movie), and dropping `Trickplay` /
`Chapters` from the detail field set (no measurable effect; both are real
data the playback path reads).

Refs #1784
2026-08-04 06:39:23 +02:00
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 35061f9f68 fix(jellyfin): scope global search to visible libraries
Jellyfin search rows do not expose their owning collection, so hidden libraries cannot be filtered after the response. Scope searches to visible libraries, stamp the returned rows, reuse the latest loaded views, propagate cancellation, and fail closed when views cannot be loaded.

Keep full candidate budgets and split music libraries into parallel album, audio, and artist requests. Album requests disable UserData and use the existing album field set to avoid recursive per-album work from #1552; audio requests retain cheap leaf play state.

close #1770
2026-08-02 09:29:59 +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 bc0d14a749 fix(explore): list every library copy of a title, not one per server
`MediaServerClient.findByExternalIds` returned `MediaItem?`, so the Explore
"In these libraries" chooser could never show more than one copy per server.
A movie held by both a 4K library and an HD library on one Plex server
therefore resolved to whichever copy came back first, with no way to reach
the other.

Return every id-verified match instead. `/library/all` is already
server-wide and each `Metadata` entry carries its own `librarySectionID`,
so both copies come back labelled with no extra request; Plex was simply
taking `Metadata[0]` and the title ladder was returning on its first hit.
An exact-guid hit no longer short-circuits the title search either — a
library still on a legacy agent has a different primary guid and is
invisible to the `guid=` filter.

Copies are deduped by global key and ordered best-first, and each row now
states its resolution, since library names need not mention it.

Resolution passes merge rather than replace: the cross-server fan-out logs
and skips per-server failures, so a later pass can come back short a server
that answered an earlier one, and a failed pass no longer claims the title
left the library. Duplicate keys fold field by field, because Jellyfin's
library stamp is a best-effort ancestors lookup that returns the item bare
when it fails and an unstamped row is indistinguishable from its sibling.
Focus nodes are keyed by copy and reclaimed after a merge re-sorts the
rows, so a dpad user is not thrown to a different copy.

close #1754
2026-08-02 06:40:04 +02:00
edde746 56ad48824b fix(jellyfin): pin MediaSourceId on every static stream URL
Jellyfin has no DirectStreamUrl field — MediaSourceInfo carries only
TranscodingUrl, and a DirectPlay decision returns no URL at all, leaving the
client to build /Videos/{id}/stream itself. The branch reading
DirectStreamUrl was therefore dead against every Jellyfin version, along with
the 'DirectStream' play method and the doc comment promising both.

The static URL also dropped MediaSourceId whenever the item had a single
source whose Id equalled the item id — an ordinary episode. The streaming
endpoint resolves a blank MediaSourceId to its own first sorted source
(VideoFile first, then widest video), so the omission silently streamed a
different file as soon as the item gained an alternate version. Forward the
id the negotiation settled on, as jellyfin-web, Findroid, and Streamyfin all
do unconditionally.

Every "pinned" fixture used a source id that differed from the item id, so no
test exercised the shape that dropped the param; add one that does.
2026-07-31 21:45:33 +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 0ef71e6489 feat(file-info): detail every version, file and stream the server reports
The sheet collapsed an item to `Media.first` / `MediaSources.first` and
rendered a fixed set of rows, so split files, extra versions, per-track
properties, HDR classification and Dolby Vision were all invisible.

Model the payload the way both servers shape it — versions own parts,
parts own streams — and project every property either backend populates
onto `MediaStreamDetails`. The field set comes from sweeping both test
servers in full through the clients' own request shapes (Plex 6842
Media / 6842 Part / 39864 Stream entries, Jellyfin 6349 sources / 37763
streams / 61224 attachments), so file presence, Dolby Vision layers,
dynamic range, sample rate, spatial audio, sidecar provenance, embedded
attachments, rotation and the lyric stream type all survive. A coverage
test fails when a server key is neither carried, folded into a sibling,
nor excluded with a reason.

File Info also stopped trusting the shared `/library/metadata/{id}`
cache row: `getPlaybackExtras` writes it without `includeStreams` /
`checkFiles`, so the sheet could render with no stream table at all.
Detect that shape and refetch once under the request context captured
before the cache read, so the outgoing token and the cache namespace
stay on one profile.

Rework the layout to match: a summary chip row, then flat tonal cards
with a two-column field grid that collapses to one column on narrow
viewports, per-stream cards with flag chips, and a copyable monospace
path row. The card fill is a tonal step off the text colour rather than
the `bg` token, which is one shade from the sheet surface on OLED.
2026-07-29 05:19:27 +02:00
edde746 0eee9f688d fix(explore): match sequel catalog entries to their parent library show
MAL/AniList season entries never matched the library show they belong to.
Both backends use the catalog title as a server-side filter before verifying
external ids, and a title like "Mushoku Tensei: Jobless Reincarnation Season 2"
cannot reach a show stored as "Mushoku Tensei: Jobless Reincarnation". Measured
against a 267-show Plex library, 3 of 113 mapped sequel entries matched.

The reverse lookup now takes two ordered title candidates instead of one: the
entry's own title and its season-stripped form, with typographic punctuation
normalised because both servers miss on a curly apostrophe. That matches 77 of
113. Widening it further to romaji/native/synonym variants reached only 81, so
the cap stays at two rather than spending up to five more requests per lookup
that finds nothing.

A sequel's year is its own season's, not the parent show's, so the +/-1 year
window is dropped for one - it would exclude the very show being looked for.
That also keeps a miss at the two requests the single-title lookup already
spent. A Plex Discover item additionally skips the title search entirely by
filtering on the plex:// guid its own rating key already is, which costs no
extra request and needs no cloud lookup.

A season 2+ entry only matches when the server really has that season, which
costs one children fetch on a match. Only a season both TVDB and TMDB agree on
is gated: which provider a library numbers its seasons by is a server setting
no dataset supplies and none of it is inferable from the ids an item exposes,
so a disagreeing reference is left ungated rather than gated on a guess.

The match cache is keyed per source and per entry rather than by canonical id,
which every season of a series shares: all five Mushoku Tensei entries collapse
to imdb:tt13293588, so one season-gated result would have poisoned the rest.

Entries whose Fribb row carries no provider id at all remain unmatched. That is
an upstream mapping gap, not something to guess around with extra lookups.

close #1704
2026-07-29 01:35:35 +02:00
edde746 8d0fe73ced fix(jellyfin): bound the series last-played pass with one shared deadline
The scoped lookups run in sequential batches, and MediaServerHttpClient
applies a per-call timeout to connect and receive separately. A silent
endpoint therefore cost up to two request timeouts per batch, and six
batches of that outlast the single request the scoped form replaced —
the enrichment could hold Continue Watching longer than the query it was
introduced to fix.

Give the pass one deadline instead of a per-batch check. It aborts the
in-flight batch and is also raced client-side, because aborting only asks
the transport to stop and not every client honours abortTrigger. Whatever
phase a lookup is stuck in — silent connect, delayed headers, stalled
body — the pass now ends at the deadline with whatever dates it has.
2026-07-28 18:05:10 +02:00
edde746 126f5e3aa6 fix(jellyfin): auto-select direct-played embedded subtitles
Plezy's device profile declares every subtitle format with
`Method: External`, so Jellyfin answers PlaybackInfo with
`DeliveryMethod: External` and a `DeliveryUrl` even for streams embedded
in a direct-played container. Direct play never fetches those URLs, but
the rows kept the delivery URL as `MediaSubtitleTrack.key`, and keyed
rows only match a native track loaded from the same URL. No embedded
track could satisfy that, so `selectSubtitleTrack` reported "still
pending" forever: playback started with subtitles off and logged the
five- and thirty-second waits, and the server's default subtitle had to
be picked by hand on every item.

Restrict sidecar identity to the rows an open actually fetched as
sidecars. A row that stays in the container loses `key` and
`usesExternalDelivery` and matches on metadata again; genuine
`IsExternal` files keep theirs, and remuxed or transcoded renditions
still resolve their sidecars by URL.

Also declare every subtitle format Embed-first so a direct-played
container reports embedded delivery in the first place, and make the
pending contract match its purpose on every backend. The
complete-catalog escape is no longer Plex-only, so a Jellyfin row the
native player has not produced keeps the pass pending instead of
committing an unrelated default and retiring the listener that was
waiting for the real track. A source id absent from the catalog no
longer defers a decision that can never change, and the thirty-second
deadline resolves from what has arrived instead of re-deriving the same
deferral and applying nothing.

close #1696
2026-07-28 15:22:23 +02:00
edde746 19542e57f4 fix(jellyfin): scope the continue watching last-played lookup per series
The Next Up shelf dated its rows from one server-wide
`/Items?SortBy=DatePlayed&Recursive=true` scan. Jellyfin 12.0-rc3 builds
that sort key by OR-ing an item's own progress with its alternate
versions' (`ItemId == e.Id || Item.PrimaryVersionId == e.Id`,
jellyfin/jellyfin#17044), which no index can serve, so the user's whole
UserData table is scanned per sorted row. Measured on identical
10,120-item libraries, that scan cost 25ms on 10.10.7 and 5.8-13.3s on
12.0-rc3 while pegging a core, so it blew the call's 10s budget and
starved every other client of the server for tens of seconds. Upstream
fixed the order mapper after rc3 in jellyfin/jellyfin#17422.

Ask each pending series for its own newest played episode instead:
`ParentId` bounds the sort input to that series, and the same 21 series
now resolve in 1.5s against the rc3 server with byte-identical dates.
The lookups run four at a time under a shared wall-clock budget and a
short per-request timeout, so a silent endpoint costs less than the one
default-budget request this replaced, and a `count: null` shelf can no
longer fan out one request per started series. Endpoint failover stays
off so a slow enrichment row cannot move the client off a working
endpoint.

close #1699
2026-07-28 11:39:58 +02:00
edde746 a56b9a3dfb Merge the deduplication and dead-code removal pass
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
2026-07-26 19:41:23 +02:00
edde746 1fea9ef6e3 fix(search): recover omitted Plex media categories
close #1598
2026-07-26 14:58:38 +02:00
edde746 4eaf4423a1 refactor: share focus chrome and simplify the TV picker and browse paths
Focus chrome was implemented twice, once in the focusable wrapper and once
in the focus builders; both now go through FocusChrome. TvColorPicker's
channel row was a copy of TvNumberSpinner and is now that widget in compact
density.

Also trims unused helpers and fields and simplifies the Jellyfin browse
paths.
2026-07-26 06:09:49 +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 4307c49cd2 refactor: remove unreachable code paths and unused members
Drops dead code across services, models, utils and widgets, including the
connection auth service, which had no implementer, and the Live TV DVR
provisioning models, which had no caller.

Tests that only covered deleted behaviour are removed or trimmed. No
behaviour change.
2026-07-26 06:09:47 +02:00
edde746 fcaa81bf3c fix(artwork): preserve clear-logo aspect ratios 2026-07-26 04:34:08 +02:00
edde746 0959cd3040 fix(libraries): exclude episodes from mixed roots
close #1675
2026-07-25 09:13:00 +02:00
edde746 2b3853a882 fix(player): preserve transcoded subtitles at high speed
close #1622
2026-07-25 04:21:37 +02:00
edde746 c7b9eec087 fix(player): preserve subtitles across episode changes
close #1635
2026-07-24 21:40:22 +02:00
edde746 102ef46d00 feat(jellyfin): lazily page recently added hubs
close #1611
2026-07-24 20:29:21 +02:00
edde746 e0bf66eea8 fix(runtime): harden application service boundaries 2026-07-24 03:46:46 +02:00
edde746 0707d4d9b1 fix(live-tv): stabilize HLS playback 2026-07-18 20:04:09 +02:00
edde746 a1b6a89714 fix(player): load subtitle sidecars with media
close #1583
2026-07-17 23:09:07 +02:00
edde746 d253450e9d fix(plex): show all artist releases
close #1577
2026-07-15 08:49:06 +02:00
edde746 d5f3e581df fix: eliminate cross-app consistency drift 2026-07-13 23:13:53 +02:00
edde746 ec249d2ed4 fix(jellyfin): repair music library browsing
Fixes #1557
2026-07-13 22:58:12 +02:00
edde746 65e63c206e fix(jellyfin): slim music hub row fields
close #1552

Latest Albums returns MusicAlbum folder dtos, where the browse count/user-data
fields each cost a recursive per-album COUNT query; request it with the slim
album fields + EnableUserData=false under a dedicated latestalbums identifier,
and drop the folder fields and Overview from the played-track rows.
2026-07-13 12:05:23 +02:00
edde746 f104b5ffae fix(jellyfin): prevent false offline loops 2026-07-13 11:30:17 +02:00
edde746 359a6bf02a refactor(live-tv): split optional DVR support 2026-07-12 08:42:25 +02:00
edde746 65d3d8e471 refactor(jellyfin): share auth and time handling 2026-07-12 08:42:22 +02:00
edde746 777780926c refactor(paging): share unknown-total sentinel 2026-07-12 08:42:21 +02:00
edde746 2679745941 fix: stop Jellyfin live TV sessions
close #1533
2026-07-11 22:00:05 +02:00
edde746 dcdbbc8483 feat(explore): match catalog items to library content via reverse external-id lookup 2026-07-10 07:07:07 +02:00
edde746 5867809560 fix: resolve Jellyfin, logout, playback, and Android regressions 2026-07-09 17:14:45 +02:00
edde746 422db75b5b feat(music): mpv audio playback engine with gapless queue service
Audio-only mpv core on every platform (dedicated
com.plezy/mpv_audio_player channels): parameterized android/windows/
linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping
video/window paths (vid=no, audio-display=no, gapless-audio=weak).
MusicPlaybackService drives an in-memory queue with shuffle/repeat,
file-loaded-event gapless arming (property edges coalesce and the
android bridge drops them), per-track progress reporting, OS media
controls, audio focus, sleep timer, and error auto-skip.
PlaybackCoordinator enforces one live native player: starting video
disposes the audio core first.
2026-07-05 19:26:28 +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 87aea49ab6 fix(servers): stop rebinds from flashing an empty home screen after sign-in
Signing in triggered two back-to-back profile rebinds; the second re-added
the same Jellyfin connection, which tore down the live client and aborted
the home screen's in-flight fetches. The aborted pass was committed as
loaded-empty, flashing 'no content available' until the follow-up load
landed. Fix at the root instead of patching the sign-in window:

- addJellyfinConnection now reuses the live client when the connection is
  unchanged (token, deviceId, URL set), matching the existing Plex
  refreshTokensForProfile behavior; material changes still recreate it.
- Cancelled requests are classified end-to-end: the client's
  treat-as-empty helpers rethrow cancellations, and the aggregation
  fan-outs report cancelledServerIds alongside succeededServerIds.
- A fetch pass in which zero servers succeeded is never authoritative:
  it keeps existing content instead of wiping it (also fixes the
  pre-existing blanking of home/sidebar on a totally failed refresh),
  stays in loading while disrupted (cancellation or binding in flight),
  and only commits loaded-empty on a settled failure.
2026-07-04 23:44:38 +02:00
edde746 7632d4bea7 feat(jellyfin): favorites library filter + heart favorite in the rate sheet
close #1485
2026-07-04 18:33:58 +02:00
edde746 79b75483bd style: fix CI formatting 2026-06-27 10:36:21 +02:00
edde746 dbe804f8db fix: order episodes by air date for offline, Jellyfin, and downloads
Extends the Plex play-queue fix (#1416) to the shared episode comparator
so Specials interleave by air date everywhere, not just Plex streaming:
offline next/prev, the Jellyfin online queue, offline OnDeck, and the
download/sync "next N" selection. Undated episodes fall back to
Specials-last, preserving the #1414 "never front-load the Specials
folder" guarantee. Jellyfin queue now requests PremiereDate.
2026-06-25 23:05:16 +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 7e1d5ad4c4 fix: add Jellyfin episode-added sort
close #1274
2026-06-18 16:22:01 +02:00
edde746 84b9eac09b fix(ci): restore sanity checks 2026-06-16 22:43:56 +02:00
edde746 01a5d11ef5 fix(jellyfin): use season episode endpoint 2026-06-14 18:34:12 +02:00