Skip to content

Split PlayerSession into PlaybackEngine + three stores (fixes #60, #62) - #79

Open
KyNorthstar wants to merge 4 commits into
nightlyfrom
feature/62-Great-Player-Session-Restructuring
Open

Split PlayerSession into PlaybackEngine + three stores (fixes #60, #62)#79
KyNorthstar wants to merge 4 commits into
nightlyfrom
feature/62-Great-Player-Session-Restructuring

Conversation

@KyNorthstar

@KyNorthstar KyNorthstar commented Sep 7, 2026

Copy link
Copy Markdown
Member

What

  • New PlaybackEngine — owns the AVPlayer, remote transport
    commands, Now Playing info, the periodic time observer, per-item
    end/time-jump sinks, restored-seek readiness KVO. isPlaying (and
    every other fact) is written in exactly one place.
  • New NowPlayingStore, HistoryStore, PlaylistLibraryStore — the
    three domains PlayerSession is a god-object — three unrelated domains in one class #62 already named, each owning its own persistence.
  • PlayerSession (779 → 336 lines) is now a thin facade composing
    all four.
  • MediaPlayerView (634 → 238 lines) owns no playback state — reads
    and displays session.engine. Adds init(playing: URL) /
    init(playing: [URL]) for genuinely drop-in single-file use.

Why

Fixes #60 by construction: nothing outside PlaybackEngine writes
isPlaying. Fixes #62 by not adding a fourth responsibility to the
god object while fixing #60 — splitting now instead.

Concrete payoff beyond tidiness: requestPlaybackOnNextLoad() /
takePlaybackIntent() / takePendingRestoredSeek() are deleted.
Their own doc comments explained they existed only because "the
session doesn't own the player" — once NowPlayingStore and
PlaybackEngine are composed together, that's no longer true, so
there's nothing left to park.

Naming: a Store is durable data independent of any player instance
and persists itself; the Engine is live machinery that exists only
while something is loaded and persists nothing. PlaybackEngine, not
Player — that name's taken by the existing AVPlayerViewController
wrapper.

Behavior changes (intentional)

  • Tapping the already-current queue entry now plays it if paused,
    rather than no-opping — the no-op only existed to avoid stranding a
    parked intent that no longer exists.
  • Remote-control-event registration now follows the engine's
    play/unload rather than the view's onAppear/onDisappear
    batteries no longer depend on a specific View staying alive.

Deliberately not done here

Verification

No Xcode in my environment. Verified by grepping for every removed
symbol across the tree and tracing every call site by hand — not by
building. A real build is still the first thing this needs.

Journal: LLM transparency/Claude Fable 5.1 • Issues #60 & #62 journal.md

Fixes #60
Fixes #62
Depends on #77

`displayName`, `systemImageName_menuItem`, and `systemImageName_preview`
lived in a private extension at the bottom of LibraryView.swift instead
of beside RepeatMode itself. Moved them to a new RepeatMode + display.swift,
following the same Type + purpose.swift pattern already used by
NativeImage + placeholder.swift and NativeImage + thumbnail.swift.

No logic changes. Both call sites in LibraryView.swift still resolve
against the new public extension.

Fixes #58

Co-authored-by: Claude 5 Opus <feedback+claude@kynorthstar.me>
KyNorthstar and others added 2 commits September 7, 2026 00:15
MediaPlayerView owned the AVPlayer as @State, with isPlaying fed by a
rate publisher and then consumed to command the player right back —
both an input and an output on the same property. Moving AVPlayer
ownership into the model layer fixes that: PlaybackEngine now owns the
player and everything around it (remote commands, Now Playing info,
PIP-adjacent state), and isPlaying is written in exactly one place.

That's a fourth domain PlayerSession would otherwise have absorbed on
top of the three it already had (now-playing/queue, history, saved
playlists), so this splits those into NowPlayingStore, HistoryStore,
and PlaylistLibraryStore at the same time. PlayerSession stays as a
thin facade composing all four — still the one object views hold.

requestPlaybackOnNextLoad() / takePlaybackIntent() /
takePendingRestoredSeek() are gone: they existed only because the
session didn't own the player, parking intent across a SwiftUI update
cycle. NowPlayingStore drives the engine directly instead.

MediaPlayerView now owns no playback state — it reads and displays
session.engine. Gains init(playing: URL) / init(playing: [URL]) for
drop-in single-file use: builds a memory-only PlayerSession, so
playback batteries (remote controls, Now Playing info, PIP) work
without writing history or a saved playlist anyone didn't ask for.

Behavior changes, both intentional:
- Tapping the already-current queue entry now plays it if paused
  (the old no-op only existed to avoid stranding parked intent)
- Remote-control-event registration now follows the engine's load/
  unload rather than MediaPlayerView's onAppear/onDisappear

currentPlaylist: dropped from MediaPlayerView's initializer — the view
has no use for it once the store drives the engine. One call site
(ContentView) updated.

No compiler available in this environment; verified by inspection and
tracing every call site by hand, not by building. Said so in the PR.

Fixes #60
Fixes #62

Co-authored-by: Claude 5 Opus <feedback+claude@kynorthstar.me>
@KyNorthstar
KyNorthstar force-pushed the feature/62-Great-Player-Session-Restructuring branch from 841c138 to 88ac41c Compare September 7, 2026 06:16
KyNorthstar added a commit that referenced this pull request Sep 7, 2026
Hoist guard let self out of the nested assumeIsolated closure into the
outer one that actually declares [weak self] — implicit-self-after-
guard only exempts a closure that unwraps in its own scope.

Co-authored-by: Claude 5 Opus <feedback+claude@kynorthstar.me>
Hoist guard let self out of the nested assumeIsolated closure into the
outer one that actually declares [weak self] — implicit-self-after-
guard only exempts a closure that unwraps in its own scope.

Co-authored-by: Claude 5 Opus <feedback+claude@kynorthstar.me>
@KyNorthstar
KyNorthstar force-pushed the feature/62-Great-Player-Session-Restructuring branch from 482c457 to 55221e3 Compare September 7, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Now Playing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PlayerSession is a god-object — three unrelated domains in one class isPlaying is both an input and an output

1 participant