diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-15 02:09:23 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-15 02:09:23 -0500 |
| commit | d9fa8f4db2b9e6d7f610094950b460cdee146e47 (patch) | |
| tree | 99ce7c91e18865f193117592cd9f3804992dbd83 /docs | |
| parent | f6ed94cf36cdb81ac8b33f5118e9ec978d3dcd77 (diff) | |
| download | dotemacs-d9fa8f4db2b9e6d7f610094950b460cdee146e47.tar.gz dotemacs-d9fa8f4db2b9e6d7f610094950b460cdee146e47.zip | |
docs(design): commit music-config-without-emms spec + readiness review
The spec lays out the EMMS-removal design: package-owned track and
playlist structs, a narrow backend protocol with mpv as the v1 backend,
state-change hooks replacing EMMS player hooks, an overlay-based
selected-track marker, a fake-backend test architecture, a quantified
performance budget, a 22-step parity walk, and the migration plan.
The review tracks implementation readiness: which migration-plan step
is safe to start, which decisions still block the rest, and the exact
spec edits required.
Two decisions landed this session and are now baked into the spec:
- Platform support: Linux and macOS get full features; Windows runs in
degraded mode (play/stop/next/previous only) because Emacs cannot
natively connect to mpv's Windows named-pipe IPC. Anyone who wants
full Windows parity can wire mpvc.exe shellout or a w32-* named-pipe
client as a follow-up.
- File-extension scope: cj/music-file-extensions stays as-is. webm and
ape files in ~/music are intentionally skipped.
Socket path now references temporary-file-directory instead of a
hardcoded /tmp/ prefix so the spec stays consistent with the Windows
section.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/music-config-without-emms-review.org | 299 | ||||
| -rw-r--r-- | docs/design/music-config-without-emms.org | 543 |
2 files changed, 842 insertions, 0 deletions
diff --git a/docs/design/music-config-without-emms-review.org b/docs/design/music-config-without-emms-review.org new file mode 100644 index 00000000..67ef0d1b --- /dev/null +++ b/docs/design/music-config-without-emms-review.org @@ -0,0 +1,299 @@ +#+TITLE: Review: music-config Without EMMS -- Implementation Readiness +#+AUTHOR: Craig Jennings +#+DATE: 2026-05-15 +#+OPTIONS: toc:nil num:nil + +Spec reviewed: =docs/design/music-config-without-emms.org= +Third pass. This review answers the single question: is the spec in +shape to start =/start-work= against? + +Prior reviews (architectural critique, test/perf/parity/packaging +analysis) are in git history and remain accurate. This pass +distills them into a go / no-go and a punch list of exactly what +must change before implementation starts. + +* Verdict + +**No, not quite.** Step 1 of the Migration Plan can start today. +Steps 2-7 need five interlocked decisions resolved in the spec first. +Without them, the implementer would be making those decisions +mid-flight -- exactly when the cost of being wrong is highest. + +Estimated spec work to reach implementable: **1-2 hours of focused +editing.** No new analysis needed; the answers are already in the +prior reviews. + +* What is implementable today + +The pure-helper extraction (Migration Plan step 1) does not depend on +any of the blocking decisions. Specifically: + +- =cj/music--valid-file-p= (with =webm= and =ape= added per the prior format audit) +- =cj/music--valid-directory-p= +- =cj/music--collect-entries-recursive= +- =cj/music--completion-table= +- =cj/music--m3u-file-tracks= +- =cj/music--get-m3u-files= / =--get-m3u-basenames= +- =cj/music--safe-filename= +- =cj/music--format-duration= +- =cj/music--append-track-to-m3u-file= +- =cj/music--assert-valid-playlist-file= +- =cj/music-create-radio-station= + +These are already EMMS-free in spirit; their tests already pass +without EMMS mocking. Extracting them is roughly a 2-3 day commit +with characterization tests as the safety net. + +That work can start before the spec revisions land. But it should +not be open-ended -- it is one phase, after which work stops until +the spec is revised. + +* What is blocking the rest + +Five interlocked decisions. Each is small to resolve; the cost of +not resolving them is large. + +** B1: mpv IPC transport + +Spec lines 125-131 punt on this with "Pause/seek/volume can use +mpv's JSON IPC socket if available; otherwise v1 may support only +play/stop/next/previous and leave pause/seek/volume as explicit +follow-up work." + +The fallback path produces a worse player than today's EMMS+mpv +(which has working pause). Per-track-restart pause is an 80-200 ms +spawn cost plus the loss of the playing position. That is a +user-visible regression. + +**Decide**: long-running mpv + JSON-IPC socket from day one. + +This blocks Migration Plan step 4 (mpv backend). + +** B2: state-change hook contract + +Today's module subscribes to six EMMS hooks (=started=, =stopped=, +=paused=, =finished=, =cleared=, plus advice on the three toggle +commands). The header overlay, consume-after-finish, random-history, +auto-advance, and modeline updates all depend on them. + +Spec lists these hooks under "Existing EMMS Coupling" but the +Proposed Architecture has no replacement -- the backend protocol on +spec lines 100-111 is read-side only. + +**Decide**: abnormal hook =cj/music-state-change-functions= fired by +the backend with a plist =(:event STARTED|STOPPED|PAUSED|RESUMED|FINISHED +:track TRACK :error MAYBE)=. Five events. Dispatched from the mpv +IPC process filter on the corresponding mpv events +(=playback-restart=, =pause=, =end-file=). + +This blocks Migration Plan step 5 (rewire public commands). Without +it, "rewire" has no API to rewire to. + +** B3: selected-track representation + +Spec proposes =selected-index= in the playlist struct -- fine for +state. But the buffer view still needs a marker / overlay / face on +the playing line. The current code uses +=emms-playlist-selected-marker= (a buffer marker) plus +=emms-playlist-selected-face=. + +**Decide**: overlay at the line whose index matches +=selected-index=, face =cj/music-playlist-selected-face=, repositioned +on each =STARTED= event. Single overlay reused across track +changes. + +This blocks playlist-view rendering and reorder behavior. + +** B4: test architecture + +~110 of the 252 existing tests mock EMMS at the boundary (=emms-next=, +=emms-random=, =emms-playlist-track-at=, =emms-playlist-mode-kill-track=, +etc.). Without a shared fake-backend testutil, the rewrite happens +110 times in 110 different shapes. + +**Decide**: =tests/testutil-music-backend.el= provides +=cj/test-music-fake-backend= (matching the real backend's plist +shape) plus an =:events= ledger that records each call as +=(verb . args)=. Tests bind =cj/music-current-backend= to the fake +and assert sequences against the ledger. Build this first; rewrite +tests against it second. + +Without this, effort balloons by roughly a factor of three. + +** B5: cross-platform stance + +mpv's IPC works over unix domain sockets on Linux+macOS but uses +named pipes on Windows. Emacs's =make-network-process= does not +natively support Windows named pipes. + +**Decided this session (Craig, 2026-05-15)**: **best-effort on +Windows.** v1 ships full-feature on Linux + macOS; Windows gets a +degraded mode with play / stop / next / previous only (no pause, no +seek, no volume) via =start-process= + stdin or one-shot +=call-process= per command. The README documents the limitation. +Anyone who wants to fund Option B (=mpvc.exe= shellout) or Option C +(=w32-*= named-pipe client) can do it as a follow-up. + +This unblocks the implementer to write Linux+macOS code without +spending the v1 budget on Windows IPC plumbing. Quote Craig in +the spec body so the rationale survives. + +* Smaller items the implementer should not have to invent + +** S1: feature-parity scope + +13 EMMS features are missing from the spec entirely (catalogued in +the prior parity review): seek forward (=f=), seek backward (=b=), +volume up (=+= / ===), volume down (=-=), one-shot shuffle (=Z=), +info popup (=i=), center-on-current (=o=), kill-track (=d= or +similar), bury buffer (=q=), append-to-M3U (=A=), active-window +background tint, dired/dirvish add (=+= in dired). + +**Decide**: preserve all 13 in the playlist-mode keymap. Volume and +seek depend on B1 (IPC); if B1 is decided in favor of IPC, they all +come along for free. Dired/Dirvish add is independent and worth +explicitly listing in the Goals section. + +** S2: URL-vs-file routing + +Today's mpv player detects URLs vs files via +=emms-player-mpv-regexp=. Spec does not carry this forward. + +**Decide**: small helper + +#+begin_src elisp +(defun cj/music--track-type-from-name (name) + (cond ((string-match-p "\\`\\(?:https?\\|mms\\)://" name) 'url) + ((cj/music--valid-file-p name) 'file) + (t nil))) +#+end_src + +Used when constructing tracks; populates the existing =type= slot. + +** S3: metadata strategy + +Track struct includes =title=, =artist=, =duration= but the spec +says "tag extraction can be added later." + +**Decide one of two**: + +- Drop the metadata fields from v1's struct. Track description + falls back to filename-without-extension (matches current + =cj/music--track-description= behavior for untagged files). +- Keep the fields; populate via mpv IPC =get_property metadata= on + the =STARTED= event. Lazy, free, fits the IPC story from B1. + +Recommend the second. It's two lines of code given B1's IPC +infrastructure is already there. + +** S4: scope declaration + +Spec is ambiguous on whether this stays a personal module or ships +as a public package. + +**Decide**: personal Phase 1 (=modules/music-config.el=, =cj/= +namespace). Re-evaluate Phase 3 (=cadenza=, GitHub, MELPA) after +~4 weeks of daily use. Quote Craig's preference for this phasing in +the spec body. + +* The exact spec edits + +Concrete inserts and deletions, in order. Roughly 1-2 hours of +focused work, no new analysis. + +1. **Rewrite "Initial Backend"** (spec lines 118-133) -- drop the + "if available" / "otherwise" fork. Single decision: long-running + mpv with =--input-ipc-server=<PID-stamped socket under + =temporary-file-directory=>=, JSON commands over + =make-network-process :family 'local=, async event filter on the + socket's process buffer. + +2. **Insert "Process Lifecycle"** subsection after "Initial + Backend" -- one mpv per Emacs session, killed on =kill-emacs-hook=, + socket file removed in the same hook. Stale-socket cleanup on + startup (loop =/tmp/cj-music-mpv-*.sock= or the platform + equivalent and unlink). + +3. **Insert "State-Change Hooks"** subsection after "Backend + Protocol" -- =cj/music-state-change-functions= abnormal hook + with the 5-event plist contract. List the mpv events that + trigger each. + +4. **Insert "Selected-Track Representation"** subsection in the + Playlist Buffer section -- single overlay at the line whose + track-index matches =selected-index=, face + =cj/music-playlist-selected-face=, repositioned on the =STARTED= + event. + +5. **Insert "Test Architecture"** subsection at the top of the + Migration Plan -- =testutil-music-backend.el=, fake-backend plist + shape, events ledger, characterization tests for the six + user-facing commands (=next=, =previous=, =toggle-consume=, + =playlist-toggle=, =playlist-load=, =playlist-clear=) before any + non-pure code moves. + +6. **Insert "Platform Support"** subsection -- quote Craig's + 2026-05-15 decision: full features on Linux + macOS; Windows is + best-effort with play/stop/next/previous only. Document in the + README and surface in =M-x cj/music-doctor= when run on Windows. + +7. **Rewrite the track struct definition** (spec line 72-79) per + S3 -- either drop metadata fields or keep with a one-line note + about lazy population via mpv IPC. + +8. **Expand "Goals"** to list the 13 missing features per S1 with + "preserve" disposition. Mark volume / seek as + "preserve if B1=IPC, drop otherwise" -- but B1 is IPC, so they + all stay. + +9. **Insert URL-detection helper** (S2) in the M3U Handling section + or alongside the track struct. Cite where it's used. + +10. **Insert "Scope"** subsection before Goals -- this is a personal + module first; potential public package later. Quote Craig's + preference. Name the future public name candidate (=cadenza=). + +11. **Update "Existing EMMS Coupling"** inventory with the surfaces + missed in the prior review (kill-track, shift-track, bury, + selected-marker, =emms-source-file-default-directory=, + =emms-player-mpv-parameters=, =emms-player-mpv-regexp=, + =emms-track-description-function=, the custom-set-faces block, + and the =emms-all= / =emms-mode-line-mode= setup calls). + +12. **Strengthen "Acceptance Criteria"** with a parity-walk entry + (the 22-step user workflow from the prior review). + +13. **Add "Effort estimate"** and "Risks" sections to match the + format of the other =docs/design/= specs. + +14. **Add ~webm~ and ~ape~** to =cj/music-file-extensions= (or note + that the migration step does so). Two extensions, two-line + edit; the audit confirms files of both types exist in =~/music=. + +* What stays as-is + +The spec's data model (track struct, playlist struct), backend +plist shape, M3U-as-persistence stance, pure-helpers-first migration +ordering, and the overall layering (data / backend / view) are all +sound. Don't rework them. The fixes above are inserts and +clarifications, not architectural changes. + +* Recommended next step + +Two paths, pick one: + +1. **Apply the 14 edits directly.** I draft them as concrete inserts + into the spec, you review the final spec end-to-end, then + =/start-work= against the revised spec. Roughly the same 1-2 + hours but spent on review rather than writing. + +2. **You apply the edits.** This review is the punch list. Work + through items 1-14, ping me when ready, I do one more pass to + verify, then =/start-work=. + +Either way, the next milestone is "the spec passes Phase 3 (Approach) +of =/start-work= without me having to invent the missing decisions +on the fly." + +Until then, only the pure-helper extraction (Migration Plan step 1) +is safe to start. diff --git a/docs/design/music-config-without-emms.org b/docs/design/music-config-without-emms.org new file mode 100644 index 00000000..929423df --- /dev/null +++ b/docs/design/music-config-without-emms.org @@ -0,0 +1,543 @@ +#+TITLE: Design: music-config Without EMMS +#+AUTHOR: Craig Jennings +#+DATE: 2026-05-15 + +* Status + +Specification only. No implementation has been started. + +Effort: Large. This is a multi-week module rewrite involving process +management, player state, playlist state, a playlist major mode, and updates +across the existing music test suite. + +* Problem + +=modules/music-config.el= is currently an EMMS configuration module, not an +independent music module. The useful workflows are local to this config, but +the core state lives in EMMS: the playlist buffer is the source of truth, player +state is reported through EMMS hooks, and track metadata is read through EMMS +track objects. + +That shape makes the module harder to test and evolve than it needs to be. +Simple playlist operations have to load or stub EMMS, and UI refresh, +consume-on-finish, random history, and auto-advance are all coupled to EMMS +player hooks. + +The target design is a standalone music module that owns playlist state, +controls =mpv= directly through a small backend protocol, and renders a +playlist buffer as a view over package-owned data. + +This remains a personal config module in =modules/music-config.el=. It should +be internally coherent enough that it could later become a package, but v1 does +not target MELPA or a public package boundary. + +* Goals + +- Keep the current user workflows: fuzzy add, recursive directory add, + Dired/Dirvish add, M3U load/save/edit/reload, append-track-to-M3U, radio + station creation, playlist window toggle, random/repeat/single/consume + controls, track reordering, and mpv playback. +- Make EMMS unnecessary for =music-config.el= load, tests, and normal use. +- Separate domain logic from playback and UI so helpers stay easy to test. +- Replace EMMS-bound keymap entries with =cj/music-*= commands. +- Keep playlist files portable M3U files. + +* Non-Goals + +- Reimplementing the full EMMS feature set. +- Building a music library database, tag editor, or metadata indexer. +- Supporting multiple player daemons in v1. +- Supporting album art, lyrics, queue persistence across Emacs restarts, or + remote control protocols beyond mpv. +- Publishing a standalone =cj-music= package, adding MELPA metadata, or + converting all personal configuration variables to public =defcustom= forms. + +* Existing EMMS Coupling + +The current module depends on EMMS in these areas: + +- Playback commands: =emms-pause=, =emms-stop=, =emms-next=, + =emms-previous=, =emms-start=, =emms-random=, + =emms-seek-forward=, =emms-seek-backward=, =emms-volume-raise=, + =emms-volume-lower=, and shuffle/repeat/random toggles. +- Add/load/save commands: =emms-add-file=, =emms-add-directory-tree=, + =emms-play-playlist=, =emms-playlist-save=, + =emms-source-playlist-ask-before-overwrite=, and + =emms-playlist-clear=. +- Playlist buffer operations: =emms-playlist-buffer=, + =emms-playlist-mode=, =emms-playlist-track-at=, + =emms-playlist-current-selected-track=, =emms-playlist-select=, + =emms-playlist-mode-go=, =emms-playlist-mode-bury-buffer=, + =emms-playlist-mode-center-current=, + =emms-playlist-mode-shift-track-up=, + =emms-playlist-mode-shift-track-down=, + =emms-playlist-mode-kill-track=, and + =emms-playlist-selected-marker=. +- Track representation: =emms-track-name=, =emms-track-type=, + =emms-track-get=, =emms-track-simple-description=, and + =emms-track-description-function=. +- Lifecycle hooks and state: =emms-player-started-hook=, + =emms-player-stopped-hook=, =emms-player-paused-hook=, + =emms-player-finished-hook=, =emms-playlist-cleared-hook=, + =emms-player-playing-p=, =emms-player-paused-p=, + =emms-random-playlist=, =emms-repeat-playlist=, and + =emms-repeat-track=. +- EMMS setup: =emms-all=, =emms-mode-line-mode=, + =emms-playing-time-disable-display=, =emms-source-file-default-directory=, + =emms-playlist-default-major-mode=, =emms-player-list=, + =emms-player-mpv-parameters=, =emms-player-mpv-regexp=, and EMMS playlist + faces. + +Anything outside those areas, especially file discovery, safe filename +generation, M3U parsing, and radio station file creation, can remain mostly +unchanged. + +* Proposed Architecture + +** Data Model + +Introduce a package-owned track model: + +#+begin_src emacs-lisp +(cl-defstruct cj/music-track + type ; 'file or 'url + name ; absolute file path or stream URL + title + artist + duration) +#+end_src + +The =title=, =artist=, and =duration= slots are populated opportunistically from +mpv IPC metadata after a track starts. V1 must still behave correctly when +metadata is absent by displaying a filename or decoded URL. + +Introduce playlist state that belongs to this package: + +#+begin_src emacs-lisp +(cl-defstruct cj/music-playlist + tracks + selected-index + file + repeat-playlist + repeat-track + random + consume + random-history) +#+end_src + +The playlist buffer should render this state. It should not be the source of +truth. Buffer text becomes a view over =cj/music-current-playlist=. + +Track construction should use a small type helper instead of EMMS's mpv regex: + +#+begin_src emacs-lisp +(defun cj/music--track-type-from-name (name) + (cond ((string-match-p "\\`\\(?:https?\\|mms\\)://" name) 'url) + ((cj/music--valid-file-p name) 'file) + (t nil))) +#+end_src + +** Read-Side State API + +UI code should read player and playlist state through package-owned helpers, +not through backend internals: + +- =cj/music-playing-p= +- =cj/music-paused-p= +- =cj/music-current-track= +- =cj/music-playlist-state= +- =cj/music-track-description= + +The playlist header, modeline indicators, and tests should use these helpers. + +** Backend Protocol + +Playback should go through a narrow backend plist: + +#+begin_src emacs-lisp +(:name 'mpv + :available-p cj/music-mpv-available-p + :play cj/music-mpv-play + :pause cj/music-mpv-pause + :resume cj/music-mpv-resume + :stop cj/music-mpv-stop + :seek cj/music-mpv-seek + :volume cj/music-mpv-volume + :status cj/music-mpv-status + :metadata cj/music-mpv-metadata) +#+end_src + +The module should provide commands such as =cj/music-play=, +=cj/music-pause=, =cj/music-stop=, =cj/music-next=, =cj/music-previous=, +=cj/music-seek-forward=, =cj/music-seek-backward=, +=cj/music-volume-raise=, and =cj/music-volume-lower=. Those commands operate +on package playlist state and then call the selected backend. + +** State-Change Hooks + +Replace EMMS player hooks with one package-owned abnormal hook: + +#+begin_src emacs-lisp +(defvar cj/music-state-change-functions nil + "Abnormal hook run when music player state changes. +Each function receives a plist: +(:event EVENT :track TRACK :error ERROR).") +#+end_src + +Events for v1: + +- =started= +- =paused= +- =resumed= +- =stopped= +- =finished= +- =error= +- =playlist-changed= +- =mode-changed= + +The mpv backend is responsible for dispatching player events from process +sentinels and IPC event messages. Package features should subscribe here: + +- Header refresh runs on every event. +- Random history records on =started= when random mode is active. +- Consume mode removes the finished track on =finished=. +- Auto-advance runs on =finished= unless playback was deliberately stopped. +- Playlist-file reset runs on =playlist-changed= when the playlist is cleared. + +** mpv Backend + +V1 should use mpv JSON IPC from the start. Pause, seek, and volume are core +workflow parity, and implementing them later would leave a worse player than +the current EMMS+mpv setup. + +Spawn mpv with: + +#+begin_src sh +mpv --no-video --quiet --audio-display=no \ + --input-ipc-server=<SOCKET-PATH> TRACK +#+end_src + +The socket path lives under =temporary-file-directory= (=/tmp/= on +Linux/macOS, =%TEMP%= on Windows) and includes the effective UID and Emacs +process id, e.g. =cj-music-mpv-1000-12345.sock=. On startup, remove stale +=cj-music-mpv-*= sockets for the current UID when no matching process owns +them. On Emacs exit, stop playback and remove the active socket. + +Minimum mpv version: 0.17 (when JSON IPC stabilized). All current +Linux/macOS/Windows distributions ship something newer. + +Backend responsibilities: + +- Start mpv for the selected track and connect to the IPC socket with + =make-network-process=. +- Send JSON commands for pause/resume, seek, and volume. +- Subscribe to mpv events such as =playback-restart=, =pause=, and =end-file=. +- Query metadata on track start with =get_property metadata= and update the + selected track's metadata slots. +- Distinguish deliberate stops from natural track completion so the sentinel + does not auto-advance after an explicit stop. +- Report process and IPC errors through =cj/music-state-change-functions= with + =:event error=. + +** Platform Support + +Linux and macOS are the primary v1 targets. Both expose mpv's JSON IPC over +a Unix domain socket, which Emacs reads with =make-network-process :family +'local=. All features (play/stop/next/previous, pause/resume, seek, volume, +metadata) work identically on those platforms. + +Windows is best-effort. mpv on Windows uses named pipes +(=\\.\pipe\<name>=) for IPC instead of Unix sockets, and Emacs's +=make-network-process= does not natively connect to Windows named pipes. +Rather than block v1 on a Windows IPC layer, v1 ships a degraded mode on +Windows: + +- Spawn mpv with =start-process= and feed commands over stdin or via + per-command =call-process= invocations. +- Available commands: play, stop, next, previous. +- Not available on Windows in v1: pause/resume, seek, volume. +- =M-x cj/music-doctor= reports the degraded state on Windows so the user + is not surprised by missing functionality. + +Craig's call, 2026-05-15: best-effort on Windows is acceptable for v1. +Anyone who needs full Windows parity can fund a follow-up that wires named +pipes via =mpvc.exe= shellout or a =w32-*= named-pipe client. This call +unblocks the implementer to focus on the Linux/macOS path without spending +v1 budget on Windows IPC plumbing. + +** Selected-Track Representation + +Use =selected-index= as the durable state value. The playlist buffer should +display the selected/current track with an overlay plus a selected-track face. + +Reordering should: + +1. Swap entries in the playlist state's =tracks= vector/list. +2. Update =selected-index= so it continues to point at the same logical track. +3. Re-render the playlist buffer. +4. Reposition the selected overlay. + +Consume mode should remove the finished track from playlist state, then +re-render. It should not edit raw buffer text as the source of truth. + +** Playlist Buffer + +Define a package-owned major mode, for example =cj/music-playlist-mode=. + +The buffer should preserve the current key surface: + +- =RET= or =p= to play the selected track. +- =SPC= to pause/resume. +- =s= to stop. +- =>= / =n= and =<= / =P= for next/previous. +- =f= / =b= for seek forward/backward. +- =+= / === / =-= for volume. +- =a= to add music. +- =A= to append the track at point to an M3U. +- =c= / =C= to clear. +- =L=, =S=, =E=, and =g= for playlist load/save/edit/reload. +- =r=, =t=, =z=, and =x= for repeat playlist, repeat track, random, and + consume. +- =Z= to shuffle. +- =i= for track info. +- =o= to jump to the playing track. +- =q= to bury the playlist buffer. +- =S-<up>= / =S-<down>= and =C-<up>= / =C-<down>= for reordering. + +The current overlay/header design can stay, but it should read from the +package state APIs instead of EMMS variables. + +The active-window background highlight should be preserved, because it is part +of the current playlist window affordance. + +** Playlist State Rules + +- Shuffle changes playlist order and clears random history. +- Reload replaces playlist tracks from disk and clears random history. +- Save writes only track names/URLs to M3U; random history and mode state are + not persisted. +- Repeat playlist, repeat track, random, and consume are package-owned runtime + flags. +- Clearing a playlist clears the associated M3U file path. + +** M3U Handling + +Keep M3U as the persistence format. Existing helpers should be reused or moved +behind pure APIs: + +- =cj/music--m3u-file-tracks= should parse paths and URLs. +- Saving should write one track per line, using relative paths where possible. +- Radio station creation should keep writing =#EXTM3U= and =#EXTINF= entries. + +** Test Architecture + +The rewrite should not update every command-flow test with one-off mocks. +Introduce a shared fake backend first: + +#+begin_src emacs-lisp +;; tests/testutil-music-backend.el +(defvar cj/test-music-fake-backend + '(:name fake + :available-p cj/test-music--fake-available-p + :play cj/test-music--fake-play + :pause cj/test-music--fake-pause + :resume cj/test-music--fake-resume + :stop cj/test-music--fake-stop + :seek cj/test-music--fake-seek + :volume cj/test-music--fake-volume + :status cj/test-music--fake-status + :metadata cj/test-music--fake-metadata)) +#+end_src + +The fake backend should keep a simple event ledger, for example +=(:playing-p BOOL :paused-p BOOL :track TRACK :events LIST)=. Command tests +bind =cj/music-current-backend= to this fake and assert ordered backend events +instead of stubbing individual EMMS functions. + +Before rewriting non-pure command implementations, add or preserve +characterization tests for: + +- =cj/music-next= +- =cj/music-previous= +- =cj/music-toggle-consume= +- =cj/music-playlist-toggle= +- =cj/music-playlist-load= +- =cj/music-playlist-clear= + +The existing pure-helper tests should mostly survive unchanged. The command +tests, random-navigation tests, consume tests, playlist-buffer tests, and header +tests should migrate to package state plus the fake backend. + +The real mpv IPC client should have integration tests tagged =:slow= and +skipped when =mpv= is not on =PATH=. Default =make test= should not depend on +mpv being installed. + +** Performance Budget + +V1 should keep the UI responsive for realistic playlist sizes: + +- =cj/music-playlist-load= on a 1000-track M3U should complete in under 500 ms, + excluding disk cold-cache effects. +- =S-<up>= and =S-<down>= should return control within 50 ms for playlists up + to 5000 tracks. +- Pause/resume command dispatch over mpv IPC should complete in under 100 ms, + excluding audio-device resume latency. +- Header refresh after metadata arrival should stay below a frame budget + target of 16 ms. + +Full playlist re-rendering is acceptable for load, clear, shuffle, reload, and +consume-after-finish. Reordering should avoid an obvious O(n) full erase and +insert on every keypress if it misses the 50 ms budget. Start simple, measure, +then add incremental line swaps or rendered-line caching only if needed. + +Metadata extraction must be lazy. Query mpv metadata when a track starts and +refresh the header when it arrives. Do not eagerly scan all tracks on playlist +load. + +** Parity Walk + +Before removing the EMMS implementation, run a manual parity walk against the +new implementation: + +1. =F10= opens the playlist in a bottom side window; =F10= again closes it. +2. =C-; m a= completes files and dirs under =cj/music-root=; choosing a file + adds that track. +3. Choosing a directory adds music files from that tree. +4. In Dirvish, marking files and pressing =+= adds them. +5. In the playlist, =RET= plays, =SPC= pauses, and =SPC= resumes. +6. =>= advances and =<= goes back. +7. =z= toggles random; next chooses randomly; previous uses random history. +8. =r= toggles repeat-playlist. +9. =t= toggles repeat-track. +10. =x= toggles consume; finished tracks disappear. +11. =S= saves an M3U in =cj/music-m3u-root=. +12. =L= loads a saved playlist in order. +13. =g= reloads the current M3U after manual edits. +14. =E= opens the M3U for editing. +15. =R= creates a radio-station M3U that can be loaded and played. +16. =S-<up>= and =S-<down>= reorder tracks while state and view stay in sync. +17. =c= and =C= clear the playlist. +18. =q= buries the playlist buffer. +19. =i= shows current track info. +20. =o= centers the playlist on the current track. +21. =+= and =-= adjust volume and the change persists across track changes. +22. =f= and =b= seek forward/backward. + +* Migration Plan + +1. Extract pure helpers and tests into EMMS-free units: file validation, + recursive collection, M3U parsing/writing, safe filenames, radio station + content, URL/file track typing, and playlist state operations. +2. Introduce package-owned track and playlist state structs. +3. Add =cj/music-playlist-mode= and make it render package playlist state with + selected-track overlay support. +4. Add =tests/testutil-music-backend.el= and migrate command-flow tests to the + fake backend. +5. Implement the mpv backend in focused steps: + - Process spawn, socket path management, IPC connection, and state-change + hook plumbing. + - Play, stop, next, and previous, including finished-track auto-advance. + - Pause/resume, seek, and volume through IPC. + - Metadata read on track start through IPC. +6. Rewire public commands and Dired/Dirvish integration to use the new + state/backend APIs. +7. Replace EMMS functions in =cj/music-map= and the playlist-mode keymap with + =cj/music-*= commands. +8. Remove =cj/emms--setup= and the on-demand EMMS loading pattern. +9. Delete the =use-package emms= block once parity is covered. + +No EMMS compatibility adapter is planned. This is a personal config, and the +cleaner migration is to keep existing public =cj/music-*= command names while +swapping their implementation behind the scenes. + +* Acceptance Criteria + +- Loading =music-config.el= does not require EMMS or reference EMMS symbols. +- =init.el= still loads after the =use-package emms= block is removed. +- A new smoke test confirms =music-config.el= can be required in batch with no + EMMS package installed. +- Existing focused music tests pass without EMMS preload or EMMS stubs. +- =tests/testutil-music-backend.el= exists and command-flow tests use it + instead of direct EMMS stubs. +- New tests cover playlist state, backend command dispatch, IPC command + formatting, M3U persistence, Dired/Dirvish add routing, and the EMMS-free load + smoke path. +- Slow mpv IPC integration tests are tagged =:slow= and skipped when =mpv= is + unavailable. +- The F10 and =C-; m= workflows still open/show the playlist and expose the + same high-level commands. +- All keys from the current playlist-mode keymap work in + =cj/music-playlist-mode=. +- M3U load/save/reload/edit and radio station creation work without EMMS. +- Local-file and stream URL playback work through mpv. +- Pause/resume, seek, and volume work through mpv IPC. +- Random, repeat playlist, repeat track, consume, shuffle, and track reordering + are represented in package-owned state and covered by focused tests. +- The parity walk passes. +- The performance budget is met or deviations are documented with measurements. + +* Risks + +| Risk | Mitigation | +|-----------------------------------------------+-------------------------------------------------------------------------------------------------------------| +| mpv IPC socket races or stale sockets | Use UID/PID-stamped socket paths, clean stale sockets on startup, and remove the active socket on exit. | +| Auto-advance fires after explicit stop | Track deliberate stop state and test sentinel/event ordering. | +| Metadata availability differs by stream/file | Treat metadata as optional; filename/URL descriptions remain the fallback. | +| Playlist buffer gets out of sync with state | Make state the only source of truth and render buffer text from state after every playlist mutation. | +| Dirvish =+= workflow regresses | Include Dired/Dirvish add routing in migration and tests. | +| Test rewrite spreads bespoke fakes everywhere | Add one shared fake backend and migrate command tests through it. | +| Playlist rendering is too slow on large M3Us | Start simple, measure against the budget, and add incremental rendering only if needed. | +| Rewrite is too broad for one commit | Split implementation by the migration plan; keep pure helpers and state changes separate from backend work. | + +* Considered But Not Chosen + +** Publish as a standalone MELPA package in v1 + +Rejected for v1. A public package would require namespace cleanup, +=Package-Requires=, autoload cookies, defgroups/defcustoms, license headers, +README/CHANGELOG work, package-lint/checkdoc cleanup, CI matrix support, and +removal of personal config dependencies like =user-constants= and +=cj/custom-keymap=. That work is real and useful only after the personal +module migration proves the design. The v1 scope stays in +=modules/music-config.el=. + +** Rewrite every test individually + +Rejected. Roughly half of the current music tests exercise EMMS-backed command +flows. Replacing each EMMS mock one at a time would duplicate setup and make +backend semantics inconsistent across tests. A shared fake backend gives the +rewrite one seam to test command dispatch, state changes, and event ordering. + +** Eager metadata extraction for every playlist track + +Rejected. Scanning metadata for every track on load would require either many +short mpv invocations or another tag-reading dependency, and it scales poorly +for large M3Us. V1 reads metadata lazily from mpv IPC only when a track starts. + +** Full incremental playlist renderer from the start + +Deferred. Incremental rendering is more complex and may not be needed for the +actual playlist sizes in this config. The spec sets a budget instead: full +rendering is fine where it meets the budget, and reorder operations should only +gain incremental swaps or cached rendered lines if measurement shows a problem. + +** Depend on an external mpv IPC package immediately + +Undecided, not chosen as a requirement. A local JSON IPC helper may be small +enough and easier to test in this config. Depending on an existing package is +still open if the local helper starts to recreate too much protocol machinery. + +** Add =webm= and =ape= to =cj/music-file-extensions= + +Rejected. =~/music= contains a small number of =webm= and =ape= files (~22 +each), and mpv can play both, but Craig's call (2026-05-15) is to leave the +extension list as-is. Those files stay silently filtered out of fuzzy +completion and recursive directory adds. Easy to revisit later by adding the +two strings to the list; no architectural impact. + +* Open Decisions + +- Exact mpv IPC client implementation: small local JSON helper or dependency on + an existing mpv IPC package. +- Whether metadata should be cached only in memory or written into extended M3U + comments later. |
