You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/core/media_server/contract.py

112 lines
4.5 KiB

"""Canonical contract for media server clients.
Narrow on purpose. Protocol body declares ONLY the methods every
registered client actually implements today — keeps the static
contract honest. Server-specific extras (Plex's
``set_music_library_by_name``, Jellyfin's user picker, Navidrome's
music folder filter, SoulSync's filesystem rescan) and methods that
most-but-not-all servers implement (``search_tracks`` on Plex /
Navidrome but not Jellyfin; ``get_recently_added_albums`` on
Jellyfin / Navidrome / SoulSync but not Plex) stay off the Protocol
and are reached through ``engine.client(name)`` directly.
The contract is a Protocol (structural typing) rather than an ABC —
existing PlexClient / JellyfinClient / NavidromeClient /
SoulSyncClient grew the same shape independently because every
caller needed the same four calls. This file just makes that
implicit contract explicit + the conformance test pins it.
"""
from __future__ import annotations
from typing import Any, List, Protocol, runtime_checkable
@runtime_checkable
class MediaServerClient(Protocol):
"""Structural contract every media server client must satisfy.
``runtime_checkable`` lets ``isinstance(client, MediaServerClient)``
work, but it ONLY checks method names — not signatures. The
conformance test in ``tests/media_server/test_conformance.py``
does the deeper class-level check via REQUIRED_METHODS.
"""
# ------------------------------------------------------------------
# Connection / lifecycle — required, every server implements
# ------------------------------------------------------------------
def is_connected(self) -> bool:
"""Cheap probe — does the client have a live connection /
token / session right now? Used by the dashboard status
indicators + endpoint guards."""
...
def ensure_connection(self) -> bool:
"""Re-auth or reconnect if needed. May make a network call.
Returns True if connection is usable after the call."""
...
# ------------------------------------------------------------------
# Library reads — required, every server implements
# ------------------------------------------------------------------
def get_all_artists(self) -> List[Any]:
"""Return every artist the server knows about. Each item is
a server-specific wrapper object (PlexArtist, JellyfinArtist,
NavidromeArtist, SoulSyncArtist) — caller treats them
opaquely."""
...
def get_all_album_ids(self) -> set:
"""Return the set of every album ID in the library. ID
format is server-native — caller doesn't introspect."""
...
# ---------------------------------------------------------------------------
# Required method set — pinned by the conformance test. Mirrors the
# Protocol body exactly so static + runtime contracts can't drift.
# ---------------------------------------------------------------------------
REQUIRED_METHODS = {
'is_connected',
'ensure_connection',
'get_all_artists',
'get_all_album_ids',
}
# Methods that exist on SOME servers but NOT all, listed here for
# discoverability. The conformance test does NOT enforce these. Callers
# that need one reach the per-server client directly via
# ``engine.client(name).<method>`` rather than going through the engine,
# since the engine has no uniform safe-default that fits every method.
#
# Coverage today (audited 2026-05):
# search_tracks: Plex ✓, Navidrome ✓, Jellyfin ✗, SoulSync ✗
# get_recently_added_albums: Jellyfin ✓, Navidrome ✓, SoulSync ✓, Plex ✗ (uses recentlyAdded() on music library)
# trigger_library_scan / is_library_scanning: Plex ✓, Jellyfin ✓, Navidrome ✓, SoulSync ✗ (filesystem walks in-process)
# get_library_stats: Plex ✓, Jellyfin ✓, Navidrome ✓, SoulSync ✗
# create_playlist / update_playlist / get_all_playlists / etc: Plex ✓, Jellyfin ✓, Navidrome ✓, SoulSync ✗
# update_artist_*, update_album_poster, update_track_metadata: Plex ✓, Jellyfin partial, Navidrome stubs, SoulSync ✗
KNOWN_PER_SERVER_METHODS = (
'search_tracks',
'get_recently_added_albums',
'trigger_library_scan',
'is_library_scanning',
'get_library_stats',
'create_playlist',
'update_playlist',
'append_to_playlist',
'copy_playlist',
'get_all_playlists',
'get_playlist_by_name',
'get_play_history',
'get_track_play_counts',
'update_artist_genres',
'update_artist_poster',
'update_album_poster',
'update_artist_biography',
'update_track_metadata',
)