Commit graph

5 commits

Author SHA1 Message Date
Broque Thomas
2ebaf2e6e3 MS Gap 1: Lift shared TrackInfo + PlaylistInfo to neutral types module
Plex / Jellyfin / Navidrome each defined a near-identical
XTrackInfo (id / title / artist / album / duration / track_number /
year / rating) and XPlaylistInfo (id / title / description /
duration / leaf_count / tracks). Three classes that grew up by
copy-paste — not a real contract difference.

Lifted both to core/media_server/types.py as canonical TrackInfo +
PlaylistInfo. Per-server constructors live as classmethods on the
unified class (TrackInfo.from_plex_track, PlaylistInfo.from_plex_playlist)
matching the metadata Album.from_X_dict pattern Cin's POC uses.
Heavy plexapi imports stay lazy under TYPE_CHECKING.

- core/plex_client.py / jellyfin_client.py / navidrome_client.py:
  per-server XTrackInfo / XPlaylistInfo dataclass definitions
  removed; each module now imports TrackInfo + PlaylistInfo from
  the neutral package and uses the shared name internally.
- core/matching_engine.py: was annotating callers with PlexTrackInfo
  even though sync_service hands it Jellyfin / Navidrome instances
  at runtime when those servers are active. Annotation is now the
  unified TrackInfo, so signatures match the actual contract.
- services/sync_service.py: same import + annotation update.
2026-05-05 18:25:28 -07:00
Broque Thomas
49f7679eef MS Cin-1 + Cin-2: Explicit contract inheritance + generic accessors
Apply the Cin-1 / Cin-2 pattern from the download refactor PR to the
media server engine PR before review.

Cin-1 — explicit inheritance:
- PlexClient, JellyfinClient, NavidromeClient, SoulSyncClient now
  explicitly inherit MediaServerClient instead of relying on
  structural typing alone. Pre-change a reader of plex_client.py
  had no way to know the class was supposed to satisfy the contract.
- Removed the engine + registry re-exports from
  core/media_server/__init__.py to break the circular import that
  the inheritance change introduced (importing the package now
  triggered a chain that loaded clients before their base class
  resolved). Submodules import directly: from
  core.media_server.engine import MediaServerEngine, etc.
- Conformance test now also asserts isinstance() / issubclass()
  against MediaServerClient — drift in any class fails at the test
  boundary instead of at runtime.

Cin-2 — generic accessors + singleton:
- engine.configured_clients() — replaces the legacy per-server
  `if X and X.is_connected(): clients[name] = X` chains in
  web_server.py.
- engine.reload_config(name=None) — generic dispatch, so callers
  pass the server name instead of reaching for plex_client.reload_config()
  directly.
- get_media_server_engine() / set_media_server_engine() singleton
  factory matching the get_metadata_engine() / get_download_orchestrator()
  shape. web_server.py boots via set_media_server_engine(...) so
  factory + global handle share state.
- 7 new tests pin the accessors + singleton behaviour.
2026-05-05 16:59:05 -07:00
Broque Thomas
971d683ebd C1: Migrate status-check dispatch sites to engine
Two sites in web_server.py replaced:
- /status route's media-server connectivity check (4-way if/elif
  for plex/jellyfin/navidrome/soulsync) → engine.is_connected()
- /api/playlists endpoint's server_connected check (3-way if/elif)
  → engine.is_connected()

Engine reads active_server config + dispatches to the right client
with internal connection caching preserved (the underlying clients
all cache is_connected() calls).

Engine constructor now accepts a pre-built clients={...} dict so
web_server.py wires the same instances as its existing per-client
globals — no double-init.

Suite still green. Per-server clients still accessible via
engine.client(name) for source-specific reaches.
2026-05-04 21:25:20 -07:00
Broque Thomas
6b54ca6598 Phase B: Add MediaServerEngine skeleton
`MediaServerEngine` reads the active server from config + dispatches
to the corresponding registered client. Per-server reaches still
work through `engine.client(name)`.

Required-method dispatch (is_connected, ensure_connection,
get_all_artists, get_all_album_ids) returns safe defaults when
the active client failed to initialize OR when the method raises.

Optional-method dispatch (search_tracks, trigger_library_scan,
is_library_scanning, get_library_stats, get_recently_added_albums)
checks hasattr first — SoulSync standalone has no
trigger_library_scan or get_library_stats, engine no-ops with
appropriate defaults instead of forcing every client to declare
stub methods.

10 new engine tests pin: active-server resolution, required
dispatch routing, exception safety, missing-optional-method
fallback shape. Suite still green (1951 passed).

Engine isn't on any production code path yet — Phase C migrates
the 33 web_server.py dispatch sites to call engine.method()
instead of hand-branching by active_server name.
2026-05-04 21:14:17 -07:00
Broque Thomas
f702196dca Phase 0: Add MediaServerClient contract + registry
`core/media_server/` package with the Protocol contract that
every media server client (Plex, Jellyfin, Navidrome, SoulSync
standalone) satisfies, plus the registry that holds them.

Required methods conservatively limited to the four every server
truly implements today: is_connected, ensure_connection,
get_all_artists, get_all_album_ids. Other generic methods
(search_tracks, trigger_library_scan, get_recently_added_albums,
etc.) are listed as OPTIONAL — present on most servers but not
all (SoulSync has no library-scan API since it walks the filesystem
directly; Jellyfin uses a different search shape). Phase B's
engine adapters route around the gaps with per-server fallback
instead of forcing every client to declare a no-op stub.

Same registry shape as the download plugin registry — single
source of truth for which servers exist + name resolution. Adding
a 5th server (Subsonic, Emby, etc.) becomes one register call
plus the new client class.

5 conformance tests pin every server class implements every
required method. Plan doc at docs/media-server-engine-refactor-plan.md.

Pure additive — no consumer routes through the contract or
registry yet. Suite still green (1921 passed).
2026-05-04 20:49:25 -07:00