# Spec: Enforce the unified player boundary **Status:** Proposed **Requirements:** DR-095 (new); relates to UR-005 and the unified-player-boundary principle in CLAUDE.md and [02-svelte-frontend.md](../architecture/02-svelte-frontend.md) **UX spec:** n/a — refactor, no user-visible change. **Supersedes / revises:** n/a ## Summary The stated principle is that UI controls playback **only** through `playerController` ([src/lib/player/index.ts](../../src/lib/player/index.ts)), never by calling `commands.player*` directly. There are **52 direct call sites outside** that facade. This spec routes the genuine playback-control calls through the facade, narrows the principle's wording so it stops forbidding things it never meant to forbid, and adds the lint rule that keeps it true — because this rule is the one design principle in the audit with **no automated check at all**, and it is also the one that drifted furthest. ## Motivation Direct `commands.player*` usage outside `src/lib/player/`, by file: | File | Sites | |---|---| | [queue.ts](../../src/lib/stores/queue.ts) | 10 | | [player/[id]/+page.svelte](../../src/routes/player/[id]/+page.svelte) | 9 | | [VideoPlayer.svelte](../../src/lib/components/player/VideoPlayer.svelte) | 8 | | [settings/+page.svelte](../../src/routes/settings/+page.svelte) | 5 | | [sleepTimer.ts](../../src/lib/stores/sleepTimer.ts) / [auth.ts](../../src/lib/stores/auth.ts) / [autoplay.ts](../../src/lib/api/autoplay.ts) | 4 each | | [preload.ts](../../src/lib/services/preload.ts) | 3 | | [library/[id]](../../src/routes/library/[id]/+page.svelte), [playerEvents.ts](../../src/lib/services/playerEvents.ts), [playbackMode.ts](../../src/lib/stores/playbackMode.ts) | 1–2 each | These are **not** equivalent violations, and treating them as one number is why the rule has been easy to ignore. Three distinct groups: **(a) Genuine violations — playback control with a facade method that already exists.** `playerStop` ×6, `playerPlayTracks` ×4, `playerSeek` ×2, `playerPlayAlbumTrack` ×2, `playerNext`, `playerPrevious`, `playerSkipTo`, `playerToggleShuffle`, `playerCycleRepeat`, `playerRemoveFromQueue`, `playerMoveInQueue`, `playerAddTrackById`, `playerAddTracksByIds`, `playerSetSubtitleTrack`, `playerPlayItem`. The facade exposes `stop()`, `seek()`, `next()`, `previous()`, `skipTo()`, `toggleShuffle()`, `cycleRepeat()`, `removeFromQueue()`, `moveInQueue()`, `addTrackById()`, `addTracksByIds()`, `setSubtitleTrack()`, `playTracks()`, `playAlbumTrack()`, `playItem()` — every one of these has a facade equivalent that is simply not being called. `queue.ts` is the starkest case: it imports `commands` directly and re-implements ten methods the facade already provides. **(b) Playback control with no facade method.** `playerPlayQueue`, `playerGetQueue`, `playerGetStatus`, `playerEnterBackgroundAudio`, `playerExitBackgroundAudio`, `playerSetSleepTimer`, `playerCancelSleepTimer`, `playerPlayNextEpisode`, `playerCancelAutoplayCountdown`. In scope for the principle, but currently *impossible* to comply with — the facade has no surface for them. A rule that cannot be followed is not being broken so much as it is unfinished. **(c) Not playback control.** `playerConfigureJellyfin` ×3, `playerDisableJellyfin`, `playerGet/SetAudioSettings`, `playerGet/SetVideoSettings`, `playerGetEqPresets`, `playerGet/SetAutoplaySettings`, `playerGet/SetCacheConfig`, `playerPreloadUpcoming`. These are configuration and lifecycle calls that happen to live under the `player_` command prefix. The principle is about *who is authoritative for playback state* — settings CRUD isn't that. The audit's read: the rule as written is violated 52 times, which makes real drift indistinguishable from acceptable usage, and that ambiguity is what lets group (a) persist. Note also that the principle **is** well-honoured where it matters most — the read side is clean, with UI reading state exclusively from the facade's re-exported stores. The write side is what drifted. ## Layer assignment Frontend-internal refactor. No domain logic moves and nothing new crosses IPC — the same Rust commands are called, through one module instead of many. | Logic / responsibility | Layer | Why it belongs there | |------------------------|-------|----------------------| | Playback command dispatch (adapter routing: native vs HTML5) | Frontend — `src/lib/player/` **only** | Presentation-layer plumbing, but must be centralised: the facade picks between the native backend and the HTML5 `