From a79a38a22a6145c475f50eeadb79b451aee31c11 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 16 Sep 2026 10:21:58 +0200 Subject: docs: playlists — storage split, queue ops, and the UI design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 256 KB cap contradicted its own worked example; entries were also missing `size`/`name`, without which a stored track cannot be fetched at all. Storage becomes a manifest plus one blob per playlist, compressed, so starring a track no longer rewrites the collection. Adds the UI pass that was missing: the queue verbs, the menus, and the one real code change behind them — the player's queue can today only be replaced, not appended to. Co-Authored-By: Claude Opus 5 --- docs/MESHBAY_DESIGN.md | 57 ++- docs/playlists.md | 1096 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 847 insertions(+), 306 deletions(-) diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 2b6d1ac..aaa232c 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -32,7 +32,7 @@ | **this document** | the architecture, the trust model, and the reason each decision is what it is | | `MESHBAY_NODE_PROTOCOL.md` | the MNP wire format, message by message | | `transfers-v1.md` | the transfer system's failure-mode analysis, kept because a synthesis cannot carry "every way a slot can be lost" | -| `playlists.md` | the playlist design in full — decided, not built (§9.10) | +| `playlists.md` | the playlist design and its interface in full — decided, not built (§9.10) | | `cast-smart-tv.md` | the DLNA/UPnP device backend — designed, not built (§11.4) | | `WINDOWS-PORT.md` | the Windows port's audit and packaging detail (§11.2) | | `PACKAGING-GUIDE.md`, `HTTPS.md`, `MAIL-SERVER.md`, `windows-build.md` | installation and server operations | @@ -2327,10 +2327,23 @@ removed from the hub, and it is the same rule that keeps resume positions local: *nothing new learns what you watch.* An encrypted blob on the hub is technically trivial and is still refused. -It lives on the node instead, as **an opaque per-account blob in `bundles.db`** — -the same shape as the keypair bundle, which the node already stores and cannot -read. **No new trust boundary**: the node is not asked to hold a kind of thing it -does not already hold for that same account. +It lives on the node instead, as **opaque per-account blobs in `bundles.db`** — the +same shape as the keypair bundle, which the node already stores and cannot read. +**No new trust boundary**: the node is not asked to hold a kind of thing it does +not already hold for that same account. + +**One blob per playlist, plus a small manifest** — not one blob for the collection, +and the reason is write amplification rather than size. Under a single blob, +starring one track rewrites and re-uploads the whole collection to every node +reached; split, it rewrites that one playlist. The manifest — names, revisions, +tombstones, counts, a few KB — is also the only thing every menu needs, so "add to +playlist" draws instantly with every node offline, and a body is fetched only when +its playlist is opened or played. Blobs are **compressed before sealing and padded +after**: the payload is repetitive enough to be worth a factor of three, and the +padding is what stops a ciphertext length from counting somebody's tracks. The node +caps each blob and the account's total, and **refuses rather than truncates** — a +truncating cap silently loses tracks, which is the failure the whole design exists +to prevent. **The key is the one thing that must not be got wrong.** Identity keys are per node (§3.2), so a blob encrypted under one is unreadable from every other node — the @@ -2340,14 +2353,36 @@ is the bundle key, so `playlist_key = HKDF(bundle_key, info = Argon2 run, and a purpose-separated subkey rather than the bundle key reused with a different AAD (§4.4's rule). The nonce is 96 random bits and never a counter, for exactly the reason chat's is (§4.5): two devices of one account derive the *same* -key, which is the point. +key, which is the point. The AAD names the blob's *kind*, so one playlist's body +cannot be served in place of another's. **Merge is the hard third, and the granularity is what makes it tractable.** The -unit is **one playlist, not the collection**; revision counters order writes, never -the wall clock; and **a deletion is a tombstone, never an absence** — an absence is -indistinguishable from a device that has not seen the addition yet. A node that is -offline for a month therefore cannot corrupt anything: it holds an older revision of -some playlists and is overwritten per playlist, not wholesale. +unit is **one playlist, not the collection** — which the per-playlist blob now +makes true of the storage as well, so two devices editing two playlists do not even +write the same row. Revision counters order writes, never the wall clock; and **a +deletion is a tombstone, never an absence** — an absence is indistinguishable from a +device that has not seen the addition yet. A node that is offline for a month +therefore cannot corrupt anything: it holds an older revision of some playlists and +is overwritten per playlist, not wholesale. + +**The interface is a menu inside Music, not a page.** Music's player is already +persistent at shell level and already holds a queue that crosses groups; *loading* a +playlist replaces that queue, and below that call a playlist and an album are +indistinguishable — so auto-advance, shuffle and prefetch are unchanged by +construction. The one genuine code change is that the queue can today only be +*replaced*: "play next" and "add to queue" require it to become appendable, which +makes it a small reducer rather than three pieces of component state. Everything +else is a context menu on a cover or a track row, and one button in Music's sticky +toolbar. Because Music is mounted by both the group page and the Search page +(§9.11), a playlist built inside a group is managed from the consolidated view with +no second surface and no application-registry entry. + +**Unavailability is answered at play time, not at add time.** Whether a group is +reachable is only knowable by dialing, and refusing to add a track because its node +is off tonight loses the user's intent permanently to a condition that lasts an +evening. So adding never dials; playback skips, distinguishing a file that will not +decode (a property of that file) from a group that does not answer (a property of +that group, whose tracks are then skipped together). **It adds no dialing and no new streaming path.** Sync rides connections the client already makes, and playback is unchanged (§9.8). diff --git a/docs/playlists.md b/docs/playlists.md index 485011a..ecbbac4 100644 --- a/docs/playlists.md +++ b/docs/playlists.md @@ -1,23 +1,22 @@ # MeshBay — Playlists (design) > Status: **proposal**, not implemented. This was deferred out of the Music -> application's design, -> which deferred it for the right reason: *"a genuinely new category of -> per-account node state, not covered by anything E9 already enumerates — -> needs its own design pass (ownership, sync across devices, whether it's -> node-local or something else)"*. This document is that pass. +> application's design, which deferred it for the right reason: *"a genuinely +> new category of per-account node state, not covered by anything E9 already +> enumerates — needs its own design pass (ownership, sync across devices, +> whether it's node-local or something else)"*. This document is that pass. > > Read `MESHBAY_DESIGN.md` §9.8 first — Music is built, and this adds nothing -> to its playback path. Read §9.11 second: the -> cross-group consolidation this feature needs already exists there, and -> most of the work is recognising that. +> to its playback *path*. Read §9.11 second: the cross-group consolidation +> this feature needs already exists there, and most of the work is +> recognising that. > > **Scope, settled before writing this:** a playlist belongs to **one > account** and is never shared with other group members. That answer is what -> keeps §5 small; see §10 for what changes if it is ever reversed. +> keeps §6 small; see §13 for what changes if it is ever reversed. > -> Follows the project convention: every claim names the adversary it holds -> against (§9). +> Follows the project convention: every claim names the adversary or the +> failure it holds against (§12). --- @@ -28,36 +27,64 @@ turn up on that person's other devices — and so that one playlist may hold albums from **several different groups on several different nodes**, the way the Search page already searches a consolidated view. With the constraint, stated up front, that nodes go offline for an evening or for a month and that -this must not corrupt anything. +this must not corrupt anything. And — added after the first draft, and now +half of this document — **a user interface**: the queue verbs (play, play +next, enqueue) that a music player is expected to have, a way to build a +playlist from what is on screen, a way to manage playlists from one button, +and none of it breaking the album auto-advance and prefetch that already +work. + +## 0.1 What changed from the first draft, and why + +The first draft answered the storage question and stopped. Three things in it +were wrong or missing, and they are corrected here rather than patched: + +| | First draft | Now | § | +|---|---|---|---| +| Size | one blob for everything, capped at 256 KB | **manifest + one blob per playlist**, compressed, stored as a BLOB; caps per unit | §4, §5 | +| The stored track reference | "takes it unchanged" into `onPlayQueue` | **it does not** — `size` and `name` are missing and both are required to fetch a track at all | §5.3 | +| UI | one route `/playlists` and a sidebar entry | **dropped.** A menu in Music's sticky toolbar, which Search → Music inherits for free | §9, §13 O7 | + +The 256 KB figure contradicted its own worked example: the draft estimated +1 000 tracks at ~200 bytes as "~200 KB", against a cap applied to `blob_enc`, +which was a **base64 TEXT** column — so its own example was already ~267 KB on +the wire, over a cap it declared generous. §4 redoes the arithmetic with the +fields that actually have to be there. --- ## 1. What this design does not reopen -- **Views over the index, never a catalogue** (`MESHBAY_DESIGN.md` §6.5, §9.1, - draft-v6 §2.7). A playlist is a list of *references*; it creates no second - identity for a file and no server-side database of content. -- **Nothing about content reaches the hub** (H7, draft-v6 §2.5). §3.1. -- **No new streaming path.** `MESHBAY_DESIGN.md` §9.8 stands untouched: a track is - fetched through `pipelinedDownload` and handed to `