Skip to content

feat: provider platform with multiple accounts and a provider store - #10

Open
sachk wants to merge 126 commits into
masterfrom
refactor/provider-seam
Open

sachk wants to merge 126 commits into
masterfrom
refactor/provider-seam

Conversation

@sachk

@sachk sachk commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

ethanndickson and others added 30 commits September 23, 2026 21:13
The structs every screen and the player consume are Spool's media model,
not a Jellyfin artefact. They now live in src/media/MediaTypes.h, in the
core (shell) source group. The two Jellyfin-only types move to the
provider side: AuthSession to src/api/JellyfinSession.h and
DiscoveredServer to src/discovery/DiscoveredServer.h. SeriesAudioSelection
is playback policy, so it joins the player group. No field changes.
src/provider/PlaybackSource.h is the whole surface the player used from
JellyfinApiFacade: media request headers, the TLS origin, the parallel
request budget, trickplay tile URLs, series episodes for queue
continuation, the three playback reports, and the two signals that
invalidate a prepared mpv. The facade implements it; PlayerController,
PlayQueueController and PlaybackReporter take the interface. The
X-Emby-Token header the player used to build itself now comes from the
Jellyfin side, so nothing under src/player includes src/api any more.
configurePlatformPlaybackCapabilities() took the Jellyfin facade so the
webOS probe could push its codec list straight into the device profile.
It now takes a std::function applier; main.cpp supplies the one that
reaches the facade. The platform layer no longer includes src/api.
Shared shell and page QML assumed the Jellyfin provider was there: the
top bar always drew a SyncPlay button, the shell always built a remote
now-playing bar, and Session, RemoteControl, SyncPlay and Management were
read unconditionally. Add a ProviderCapabilities singleton with one flag
per optional capability and read each provider-shaped singleton only
behind its flag, so a source without the capability has no such control
and none of its bindings ever run. Every flag is true for now; the Phase 3
C++ registry replaces the file under the same names.

The top bar's index space moves into TopBarNavigation.js so a gated-out
button drops out of the Left/Right sequence instead of leaving a dead
index, and a QML test pins both the capability names and that arithmetic.
…bridge

SettingsController no longer holds the Jellyfin facade. It emits the
playback preferences, the remote-control target flag and the updated user
configuration, asks for its remote half with remoteLoadRequested(), and
takes cultures and user configuration back through two apply methods.
JellyfinSettingsBridge in src/api does what the controller used to do
against the facade, including the once-per-session load guard and the
signed-in checks. AppController wires the pair together.
A Provider declares which optional capabilities its media source serves;
the registry holds the active one and publishes its flags as the eleven
booleans shared QML already gates on. Nothing uses them yet.
The login flow, the remote control page and its menu and now-playing
bar, the SyncPlay menu and the management dialog only exist for the
Jellyfin provider. Give them a directory of their own so the provider
can take them with it when it moves out, and point the shell, the
overlay chrome and the route stack at the new place. Same QML module,
so type names resolve as before.
tools/check-module-seam.sh fails when any core file (the platform, player
and shell groups, or anything under the core directories) includes
src/api or src/discovery, and runs as the module-seam ctest. Its --strict
mode also counts SPOOL_JELLYFIN_SOURCES files as provider; that still
reports the composition-root leaks through AppController, ArtworkService
and AccountProfile that the provider registry work removes, so CI runs
the default. LibraryQuery, SpoolLinkCodec, SpoolRemoteProtocol,
LibraryListModel and MovieGridModel include nothing from the provider and
move into the shell group.
JellyfinProvider owns the facade, LAN discovery and its cached server
list, the session, QuickConnect and remote control, and builds SyncPlay,
library management and the settings bridge once the app hands it the
core objects they sit on. It registers its own QML singletons and keeps
the session-driven lifecycle of its parts; main.cpp activates it in the
ProviderRegistry, whose ProviderCapabilities instance replaces the QML
placeholder singleton. The server and sign-out entry points move from
App to Session, where the state they act on lives.
The signed-in-as, switch-profile and sign-out rows are declared with
withAuth() and left out of the schema model when the active provider's
capabilities lack auth; SettingsController reads that from the
ProviderCapabilities instance main.cpp hands it.
…ehind interfaces

The content, home, search, prefetch and user-item-state controllers took
the Jellyfin facade and used a dozen of its methods. They now take
Catalog, SearchSource and UserItemStateSink, plain abstract classes that
carry exactly that surface, and ArtworkService asks an ArtworkSource for
image URLs instead of building Jellyfin's Items/{id}/Images path itself.
The facade implements all four beside PlaybackSource; one signedIn()
answers every interface's readiness question, and libraryScopeKey()
replaces the home controller's hand-rolled server/user cache key.

Tests render artwork requests through a recording source so they check
what the service asks for; the Jellyfin URL shape stays covered by the
facade URL test.
…oller

PlatformApplicationServices takes an ApplicationHooks object (player,
settings, a memory-pressure callback and the two forwarded signals) and
RenderBenchmark a RenderBenchmarkHooks struct, so nothing under
src/platform or src/diagnostics names the composition root. main.cpp
fills both in.
With the catalog, search, item-state and artwork surfaces behind
provider interfaces, the content, browse, home, search, prefetch and
user-item-state controllers, the artwork service and its image provider
and prefetcher, and the saved-account row no longer include anything
from the Jellyfin side. Move them from SPOOL_JELLYFIN_SOURCES to
SPOOL_SHELL_SOURCES and list the four new interface headers with them.

AccountProfile is a generic saved-account row (server, user, token,
avatar, timestamps) that core's profiles table already persists, so it
goes with core rather than staying provider-shaped by name alone.
…r of Jellyfin

Provider now hands out every interface the app consumes: catalog(),
artwork(), search(), itemState(), plus the new StreamQualityControl,
GroupPlayback and RemotePlayback, and carries the session-shaped hooks and
signals (ready, restoreFromStorage, setDeviceId, setLocale,
handleUnauthorized; sessionStarted/Ended, busyChanged, errorOccurred,
toastRequested, contentChanged) the app used to take from the Jellyfin
session and its controllers directly.

Catalog gains fetchLibraries, fetchLibraryFilterOptions and fetchItemsByIds;
PlaybackSource gains resolvePlayback (was negotiatePlayback) and
fetchMediaSegments. The transcoding ceiling moves behind
StreamQualityControl with its own streamQuality capability, and the player
overlay only offers the quality menu when it is set. SyncPlayController
implements GroupPlayback, RemoteControlController implements
RemotePlayback, and JellyfinProvider relays session and remote-command
signals, sets the artwork token, runs the deferred post-login work and
publishes playback activity itself.

AppController takes a Provider, includes nothing from the Jellyfin side,
guards every group and remote path against a provider that has neither,
and moves into the core source group.
…r at start-up

LocalProvider (src/providers/local) serves every media file under one
directory as a single library: browse pages, details, latest, search,
in-memory favourite/played/resume state, and a playback session whose URL
is the file itself. It has no sign-in, no artwork, no segments and no
reporting, and announces its session from restoreFromStorage() since there
is nothing to wait for. It is the second implementation of the provider
contract and stays as its permanent fixture (local-provider ctest against
tests/media/fixtures).

main.cpp registers both providers and activates one from --provider or
SPOOL_PROVIDER (default jellyfin); the local one takes --library-root or
SPOOL_LOCAL_LIBRARY. With nothing passed the Jellyfin path is unchanged.
The router now starts on home when the active provider has no auth, so
the provider's login page is never built without its singletons. The seam
check treats src/providers as provider code.
The plan the provider seam work follows, through the phases that have
landed and the ones that have not: the two-repo shape, the capability
contract, the picker and runtime download path, and the directory move
and extraction still to come.
Classify routes before synchronous Loader work and guard reentrant promotion. Replace stale per-step singleShots with an owned deadline, record failures and runtime context, and reject incomparable reports. Timing schema 2 requires new baselines.

Validated Python report regressions and five actual route-function scenarios with Node; the original route code fails the cold-classification regression. Qt test registered but not run: Qt 6.11 SDK is unavailable in this environment.
mpv reports a clean end of file whenever the demuxer runs out of data. A
network stream that drops and cannot be reopened (a failed HLS segment
run or a refused range reconnect) ends the same way, so Spool reported
the item as finished at its full runtime, marked it played and started
the next episode.

Only an end within ten seconds of the known duration now completes the
item. An earlier end is an interruption: the position is kept as a
resume point, and playback picks up there if the attempt made progress,
so a source that keeps ending early cannot restart forever.
… check

The settings schema test re-asserted spec fields, display labels, titles
and the absence of long-removed specs, none of which can regress without
a deliberate edit. The persisted key inventory, defaults and normalisers
stay.

The provider package test read a Jellyfin archive that was replaced on
26 September and skipped itself when the file was missing, leaving the
compressed-block decoder untested. It now finds the archive through
providers/lock.json and requires it.
A sign-in code now says what to do in full-size text above it instead of
a caption inside its box. The link screen takes a linkUrl: a desktop
opens it once a code is ready and offers it again as a button, a phone
shows it as a link, and a television spells it out. The code sits beside
a Copy button wherever there is a clipboard, a waiting line shows the
screen is listening, and small print such as trademark notices goes to a
footnote.

Add a provider gets page margins, a subtitle, rows with large icons,
names and summaries (official providers included), and buttons named for
what they do. This computer gets a computer icon instead of its initial.
Password sign-in drops its duplicate Spool/service row for a title that
says what to do.
filterOptions may name the BrowseFilters a library honours. The filter
box then offers only those and drops sections left empty, so a server
that ignores a filter no longer shows one that silently does nothing.
HDR (isHdr) is offered only where declared; no server answered it
before. isHdr counts as an active filter, which also keeps filtered pages
out of the unfiltered page cache. The panel is wider and taller, and its
title says how many filters are on.
Every official provider ships in the package again, pinned to its
published release asset, so a fresh install works offline from the
first launch. Builds with open or curated provider sources still install
newer store releases over the bundled ones.
…pv loader

webOS loads libmpv after the first frame and reaches it only through
WebOSMpvRuntime's forwards. Local thumbnails started calling
mpv_render_context_get_info in 8b7f60f, which had no forward, so the
webOS app no longer linked.
A provider could mark a subtitle external but had no way to say where it
was, so a sidecar .srt listed beside a directly played file could never
be shown. A Stream may now carry a url. The player adds each one to mpv
once the file loads, without selecting it, and orders the session's
streams as mpv lists them: the file's own tracks, then the added files.

A subtitle file on another origin would carry this stream's credentials
there, and one without a url cannot be fetched; both are left out rather
than shifting every track after them.

This branch was successfully deployed

2 active (outdated) deployments
github-pages — 4fbd3a19 Deployed Oct 1, 2026 by sachk via Publish platform update metadata #129
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants