2026-10-20 22:29:58 +00:00
|
|
|
# gandora storage contract
|
|
|
|
|
|
|
|
|
|
The page keeps everything through one object, `db`, in `index.html`. Today it is IndexedDB in the browser; a backend
|
2026-10-21 06:01:48 +00:00
|
|
|
replaces it by implementing the same calls and pointing `db` at the new implementation. Nothing else in the page
|
|
|
|
|
touches storage (`brain.js` reads through `db.all()` and `db.get()` too).
|
2026-10-20 22:29:58 +00:00
|
|
|
|
|
|
|
|
```
|
|
|
|
|
db.put(row, store = "tracks") // create or replace, keyed by row.id
|
2026-10-21 06:01:48 +00:00
|
|
|
db.get(id, store = "tracks") // one row, or null
|
2026-10-20 22:29:58 +00:00
|
|
|
db.all(store = "tracks") // every row, newest first (tracks by `made`, stations by `last`)
|
|
|
|
|
db.del(id, store = "tracks")
|
|
|
|
|
db.open() // internal; a backend adapter can drop it
|
|
|
|
|
```
|
|
|
|
|
|
2026-10-21 06:01:48 +00:00
|
|
|
Three stores: `tracks`, `stations`, `tapes`. `db.all("tapes")` puts a virtual tape first, `{ id: "kept", name: "KEPT",
|
|
|
|
|
ids: [every kept track, oldest first], virtual: true }`, built from the tracks; `db.put` ignores it and `db.get("kept",
|
|
|
|
|
"tapes")` is null (the page's `tapeById` knows).
|
2026-10-20 22:29:58 +00:00
|
|
|
|
|
|
|
|
## tracks
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
{
|
|
|
|
|
id: "1760998800000-211979153", // spec.made + "-" + hash32(title); stable, made by the page
|
|
|
|
|
made: 1760998800000, // ms epoch, when the DJ wrote it
|
|
|
|
|
kept: false, // pinned by KEEP (the only pin)
|
|
|
|
|
plays: 3, // play starts + loop passes
|
|
|
|
|
blob: Blob, // the audio (FLAC from Venice, mp3 from Runware); a backend may return { url } instead
|
|
|
|
|
spec: { // the song on paper: enough to re-render it anywhere
|
|
|
|
|
title, tags, lyrics, bpm, key, energy, line, // what the DJ wrote (tags = the music prompt, <= 500 chars)
|
|
|
|
|
seed, station, stationId, // where it came from
|
|
|
|
|
reaction: "🔥 ❤️ \"less snare\" [skipped]", // the listener's reactions, in order; words quoted, skips bracketed
|
|
|
|
|
guess: 0.71, // the taste net's prediction when it was cooked (optional)
|
|
|
|
|
plays, id, made // mirrored for convenience
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## stations
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
{
|
|
|
|
|
id: "s-1760998800000-211979153", // "s-" + ms + hash32(seed)
|
|
|
|
|
seed: "rainy trains",
|
|
|
|
|
name: "Platform Nine", // chosen by the DJ on the first track, then stable
|
|
|
|
|
taste: { like: [...], avoid: [...], notes: [...] }, // last 8 of each, in words, fed to the DJ
|
|
|
|
|
history: [ { id, title, tags, bpm, key, energy, reaction, made } ], // last 8 tracks, no blobs
|
|
|
|
|
made: 1760998800000,
|
|
|
|
|
last: 1761002400000, // touched on every play and reaction
|
|
|
|
|
played: 23
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2026-10-21 06:01:48 +00:00
|
|
|
## tapes
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
{
|
|
|
|
|
id: "tape-1761002400000",
|
|
|
|
|
name: "late trains",
|
|
|
|
|
ids: [ "1760998800000-211979153", ... ], // track ids, in order
|
|
|
|
|
made: 1761002400000,
|
|
|
|
|
last: 1761002400000
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2026-10-20 22:29:58 +00:00
|
|
|
## what a backend adds
|
|
|
|
|
|
|
|
|
|
- identity: rows are per listener; `/user/station/track` links need the listener in the key
|
|
|
|
|
- `blob` as a URL the `<audio>` element and the Web Audio analyser can load (same-origin or CORS-enabled)
|
2026-10-21 06:01:48 +00:00
|
|
|
- sync: the page calls `db.all()` freely (library, taste, a station's tracks, play counts), so cache on the client and
|
|
|
|
|
keep `all()` cheap; `db.get()` is on the playback path (stepping back and forward, tapes), so keep it fast too
|
2026-10-20 22:29:58 +00:00
|
|
|
|
|
|
|
|
## what stays on the page
|
|
|
|
|
|
|
|
|
|
The taste net (`brain.js`) trains in the browser from `db.all()` rows; it needs only `spec` + `kept` + `plays`.
|
|
|
|
|
Share links (`?t=`) carry a spec, not an id, so they work with no backend at all; with one, a link can carry the id
|
|
|
|
|
and the backend's audio instead of re-rendering.
|