diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/specs/2026-07-06-radio-browser-lookup-spec.org | 4 | ||||
| -rw-r--r-- | docs/specs/2026-07-06-radio-browser-lookup.prototype.html | 190 |
2 files changed, 194 insertions, 0 deletions
diff --git a/docs/specs/2026-07-06-radio-browser-lookup-spec.org b/docs/specs/2026-07-06-radio-browser-lookup-spec.org index 9db7b92f..161e95a6 100644 --- a/docs/specs/2026-07-06-radio-browser-lookup-spec.org +++ b/docs/specs/2026-07-06-radio-browser-lookup-spec.org @@ -151,6 +151,10 @@ Add cj/music-radio-search (query -> search -> completing-read multi-select -> wr - url.el error handling is the likeliest rabbit hole. Keep the client tiny: one GET, parse, or a single user-error. Do not build a general HTTP layer. - Stream URL choice: a station can list several codec/bitrate variants under one name. v1 takes url_resolved as-is; picking among a station's variants is a vNext refinement, not a v1 problem. +* References / Appendix +- UI prototype (open in a browser): [[file:2026-07-06-radio-browser-lookup.prototype.html][2026-07-06-radio-browser-lookup.prototype.html]] — faithful vertico + marginalia mockup of the four minibuffer screens (query, station list, multi-select, created+playing) with real jazz-search data. Shows the two candidate-annotation variants the design chooses between (A: codec/bitrate/country/votes/plays; B: codec/bitrate/country/votes/tags). +- API shape verified live 2026-07-06 against de1.api.radio-browser.info (recorded in Problem / Context). + * Review and iteration history ** 2026-07-06 Mon @ 10:12:00 -0500 — Claude (for Craig) — author - What: resolved all four open decisions from Craig's answers and folded them into the design, config, rollout, and acceptance sections. diff --git a/docs/specs/2026-07-06-radio-browser-lookup.prototype.html b/docs/specs/2026-07-06-radio-browser-lookup.prototype.html new file mode 100644 index 00000000..69d3c435 --- /dev/null +++ b/docs/specs/2026-07-06-radio-browser-lookup.prototype.html @@ -0,0 +1,190 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Radio-browser lookup — UI prototype</title> +<style> + :root { + --page-bg: #f3f0ea; --page-fg: #2b2822; --page-dim: #6b6459; + --card-line: #d9d3c7; --accent: #9a6b2f; + /* Emacs frame (always dark — it depicts a dark editor) */ + --e-bg: #15140f; --e-fg: #cfc8b8; --e-dim: #857c6c; + --e-sel-bg: #2a2620; --e-sel-fg: #e6c98a; --e-prompt: #8fb0c4; + --e-mark: #d98f7a; --e-play: #8faf7f; --e-modeline: #201d17; + --e-rule: #322d25; + } + @media (prefers-color-scheme: dark) { + :root { --page-bg: #14130f; --page-fg: #d4cdbf; --page-dim: #8a8272; + --card-line: #2c281f; --accent: #d8a24f; } + } + :root[data-theme="light"] { --page-bg: #f3f0ea; --page-fg: #2b2822; --page-dim: #6b6459; --card-line: #d9d3c7; --accent: #9a6b2f; } + :root[data-theme="dark"] { --page-bg: #14130f; --page-fg: #d4cdbf; --page-dim: #8a8272; --card-line: #2c281f; --accent: #d8a24f; } + + * { box-sizing: border-box; } + body { + margin: 0; background: var(--page-bg); color: var(--page-fg); + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + line-height: 1.55; padding: 3rem 1.25rem 5rem; + } + .wrap { max-width: 60rem; margin: 0 auto; } + header { margin-bottom: 2.5rem; } + .eyebrow { text-transform: uppercase; letter-spacing: .14em; font-size: .72rem; + color: var(--accent); font-weight: 600; margin: 0 0 .5rem; } + h1 { font-size: 1.9rem; margin: 0 0 .6rem; text-wrap: balance; font-weight: 650; } + header p { margin: .3rem 0; max-width: 62ch; color: var(--page-dim); } + header p strong { color: var(--page-fg); font-weight: 600; } + + .step { margin: 2.4rem 0 .8rem; } + .step .n { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; } + .step h2 { display: inline; font-size: 1.15rem; font-weight: 620; } + .step + .note { margin: 0 0 1rem; color: var(--page-dim); font-size: .92rem; max-width: 64ch; } + + /* Emacs frame */ + .frame { + background: var(--e-bg); color: var(--e-fg); + border: 1px solid var(--e-rule); border-radius: 8px; overflow: hidden; + box-shadow: 0 12px 34px -20px rgba(0,0,0,.6); + font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + font-size: 13.5px; + } + .buffer { padding: .55rem 0 0; } + .buf-line { padding: .05rem 1rem; white-space: pre; color: var(--e-dim); } + .buf-line .txt { color: var(--e-fg); } + .modeline { + background: var(--e-modeline); color: var(--e-dim); + padding: .2rem 1rem; margin-top: .55rem; + border-top: 1px solid var(--e-rule); border-bottom: 1px solid var(--e-rule); + display: flex; gap: 1.2rem; font-size: 12.5px; + } + .modeline .lead { color: var(--e-fg); } + .mini { padding: .45rem 1rem .6rem; display: flex; align-items: baseline; } + .prompt { color: var(--e-prompt); } + .input { color: var(--e-fg); } + .caret { display: inline-block; width: .55ch; height: 1.15em; background: var(--e-fg); + translate: 0 .18em; margin-left: 1px; } + + /* vertico completion list */ + .vhead { padding: .45rem 1rem .3rem; color: var(--e-prompt); border-top: 1px solid var(--e-rule); } + .vhead .count { color: var(--e-dim); float: right; font-variant-numeric: tabular-nums; } + .cand { display: grid; grid-template-columns: 1.4ch 1fr auto; gap: .5ch; + padding: .12rem 1rem; align-items: baseline; } + .cand .mark { color: var(--e-mark); } + .cand .name { color: var(--e-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .cand .ann { color: var(--e-dim); white-space: pre; font-variant-numeric: tabular-nums; text-align: right; } + .cand.sel { background: var(--e-sel-bg); } + .cand.sel .name { color: var(--e-sel-fg); } + .cand.marked .name { color: var(--e-fg); } + .cand.marked .mark { color: var(--e-mark); } + .tag { color: #7f94a6; } + + .playrow .np { color: var(--e-play); } + .legend { margin-top: 1.4rem; padding-top: 1.2rem; border-top: 1px solid var(--card-line); + color: var(--page-dim); font-size: .9rem; } + .legend code { background: color-mix(in srgb, var(--page-fg) 8%, transparent); + padding: .05rem .35rem; border-radius: 4px; font-family: ui-monospace, monospace; font-size: .85em; } + .pick { display: flex; gap: .6rem; flex-wrap: wrap; margin: .6rem 0 0; } + .pill { border: 1px solid var(--card-line); border-radius: 999px; padding: .15rem .7rem; + font-size: .82rem; color: var(--page-dim); } + .pill b { color: var(--accent); } +</style> +</head> +<body> +<div class="wrap"> + <header> + <p class="eyebrow">Emacs · vertico + marginalia · prototype</p> + <h1>Radio-browser station lookup — how the screens read</h1> + <p>One command, <strong>cj/music-radio-search</strong>, taking you from a query to audio. These are faithful mockups of the four minibuffer screens, drawn with a real live search for <strong>jazz</strong> (results sorted by popularity). Everything is monospace because it all lives in the minibuffer.</p> + <p>The one real design choice is the station line: what metadata rides alongside the name, and in what order. Step 2 shows two variants — pick one.</p> + </header> + + <!-- STEP 1 --> + <div class="step"><span class="n">1</span> <h2>The query</h2></div> + <p class="note">Bound next to <code style="font-family:ui-monospace,monospace">R</code> in the playlist keymap. You type a name or keyword; Enter fires the search.</p> + <div class="frame"> + <div class="buffer"> + <div class="buf-line">;; <span class="txt">*EMMS-Playlist*</span> — 3 tracks, playing “Kind of Blue / So What”</div> + </div> + <div class="modeline"><span class="lead">*EMMS-Playlist*</span><span>Radio</span><span>▶ playing</span></div> + <div class="mini"><span class="prompt">Radio search: </span><span class="input">jazz</span><span class="caret"></span></div> + </div> + + <!-- STEP 2A --> + <div class="step"><span class="n">2</span> <h2>The station list</h2> <span style="color:var(--page-dim);font-size:.9rem">— Variant A: codec · bitrate · country · ♥votes ▶plays</span></div> + <p class="note">vertico lists the matches; marginalia annotates each on the right, dimmed. The current row carries the gold highlight. Codec and bitrate tell you the quality; ♥ is radio-browser votes, ▶ is play count — both proxies for “is this station any good.”</p> + <div class="frame"> + <div class="vhead">Stations for “jazz” (TAB to mark, RET to create+play): <span class="count">7/842</span></div> + <div class="cand sel"><span class="mark"></span><span class="name">Adroit Jazz Underground</span><span class="ann">MP3 320k US ♥174208 ▶148</span></div> + <div class="cand"><span class="mark"></span><span class="name">101 SMOOTH JAZZ</span><span class="ann">MP3 128k US ♥86929 ▶389</span></div> + <div class="cand"><span class="mark"></span><span class="name">Adroit Jazz Underground HD Opus</span><span class="ann">OGG 192k US ♥67573 ▶51</span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Blues</span><span class="ann">MP3 128k FR ♥62960 ▶113</span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio</span><span class="ann">MP3 192k FR ♥49272 ▶108</span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Classic Jazz</span><span class="ann">MP3 128k FR ♥27269 ▶52</span></div> + <div class="cand"><span class="mark"></span><span class="name">Radio Swiss Jazz</span><span class="ann">MP3 128k CH ♥26626 ▶58</span></div> + <div class="mini"><span class="prompt">Stations for “jazz”…: </span><span class="input"></span><span class="caret"></span></div> + </div> + + <!-- STEP 2B --> + <div class="step"><span class="n">2</span> <h2>The station list</h2> <span style="color:var(--page-dim);font-size:.9rem">— Variant B: adds a tag snippet, drops play count</span></div> + <p class="note">Same list, but the annotation trades ▶plays for the station’s top tags — more help telling two same-named stations apart by what they actually play, at the cost of a wider line.</p> + <div class="frame"> + <div class="vhead">Stations for “jazz” (TAB to mark, RET to create+play): <span class="count">7/842</span></div> + <div class="cand sel"><span class="mark"></span><span class="name">Adroit Jazz Underground</span><span class="ann">MP3 320k US ♥174208 · <span class="tag">bebop, hard bop, cool</span></span></div> + <div class="cand"><span class="mark"></span><span class="name">101 SMOOTH JAZZ</span><span class="ann">MP3 128k US ♥86929 · <span class="tag">smooth jazz, easy</span></span></div> + <div class="cand"><span class="mark"></span><span class="name">Adroit Jazz Underground HD Opus</span><span class="ann">OGG 192k US ♥67573 · <span class="tag">avant-garde, opus</span></span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Blues</span><span class="ann">MP3 128k FR ♥62960 · <span class="tag">blues, jazz</span></span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio</span><span class="ann">MP3 192k FR ♥49272 · <span class="tag">jazz, soul</span></span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Classic Jazz</span><span class="ann">MP3 128k FR ♥27269 · <span class="tag">classical, jazz</span></span></div> + <div class="cand"><span class="mark"></span><span class="name">Radio Swiss Jazz</span><span class="ann">MP3 128k CH ♥26626 · <span class="tag">public radio</span></span></div> + <div class="mini"><span class="prompt">Stations for “jazz”…: </span><span class="input"></span><span class="caret"></span></div> + </div> + + <!-- STEP 3 --> + <div class="step"><span class="n">3</span> <h2>Marking several</h2></div> + <p class="note">It’s <code style="font-family:ui-monospace,monospace">completing-read-multiple</code>: TAB marks a row (red bullet, name stays lit), and one search can create several stations at once. Here three are marked; Enter creates all three.</p> + <div class="frame"> + <div class="vhead">Stations for “jazz” (TAB to mark, RET to create+play): <span class="count">7/842</span></div> + <div class="cand marked"><span class="mark">●</span><span class="name">Adroit Jazz Underground</span><span class="ann">MP3 320k US ♥174208 ▶148</span></div> + <div class="cand"><span class="mark"></span><span class="name">101 SMOOTH JAZZ</span><span class="ann">MP3 128k US ♥86929 ▶389</span></div> + <div class="cand"><span class="mark"></span><span class="name">Adroit Jazz Underground HD Opus</span><span class="ann">OGG 192k US ♥67573 ▶51</span></div> + <div class="cand marked"><span class="mark">●</span><span class="name">Jazz Radio Blues</span><span class="ann">MP3 128k FR ♥62960 ▶113</span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio</span><span class="ann">MP3 192k FR ♥49272 ▶108</span></div> + <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Classic Jazz</span><span class="ann">MP3 128k FR ♥27269 ▶52</span></div> + <div class="cand sel marked"><span class="mark">●</span><span class="name">Radio Swiss Jazz</span><span class="ann">MP3 128k CH ♥26626 ▶58</span></div> + <div class="mini"><span class="prompt">Stations for “jazz”…: </span><span class="input">Adroit Jazz Underground,Jazz Radio Blues,Radio Swiss Jazz</span><span class="caret"></span></div> + </div> + + <!-- STEP 4 --> + <div class="step"><span class="n">4</span> <h2>Created and playing</h2></div> + <p class="note">Each pick is written as an .m3u into the MPD playlist directory (Decision 3), then enqueued and started through mpv (Decision 4) — the search ends in sound. The echo area confirms; the three land in the playlist, the first now playing.</p> + <div class="frame"> + <div class="buffer"> + <div class="buf-line playrow"><span class="np">▶ </span><span class="txt">Adroit Jazz Underground</span></div> + <div class="buf-line"> <span class="txt">Jazz Radio Blues</span></div> + <div class="buf-line"> <span class="txt">Radio Swiss Jazz</span></div> + </div> + <div class="modeline"><span class="lead">*EMMS-Playlist*</span><span>Radio</span><span class="playrow"><span class="np">▶ playing</span></span></div> + <div class="mini"><span class="prompt" style="color:var(--e-play)">Created + playing 3 stations </span><span style="color:var(--e-dim)">→ ~/.local/share/mpd/playlists/</span></div> + </div> + + <div class="legend"> + <div>Two things to react to:</div> + <div class="pick"> + <span class="pill"><b>A</b> — quality + popularity (codec · bitrate · country · ♥ ▶)</span> + <span class="pill"><b>B</b> — quality + tags (codec · bitrate · country · ♥ · tags)</span> + </div> + <p style="margin:.9rem 0 0">Everything else (the flow, multi-select, create-and-play into the MPD dir) matches the resolved spec. Data is a live jazz search; ♥ = radio-browser votes, ▶ = play count. This is a static mockup — no live search runs in the page.</p> + </div> +</div> + +<script> + (function () { + var root = document.documentElement; + try { + var t = localStorage.getItem('theme'); + if (t === 'dark' || t === 'light') root.setAttribute('data-theme', t); + } catch (e) {} + })(); +</script> +</body> +</html> |
