The page's side of listeners' content (worker/src/content.ts): songs
signed-in listeners publish from the editor, their covers, comments on
any song, and pitches for Jev songs. Everything is screened by Jev before
it is public; the author sees their own held and pending items with why
(statusText). Reading needs no account; without the Worker (a static
host) every call fails and the page shows no listeners' content.
7export const LISTENERS = '/jev/listeners';
A listener song's id as the page knows it (nowPlaying, comments):
listener:<id>, which never looks like a site song's <theme>/<song>.
14async function call(path, { method = 'GET', body, form, fetchImpl = globalThis.fetch } = {}) { 15 const res = await fetchImpl(`${LISTENERS}/${path}`, { 16 method, 17 credentials: 'same-origin', 18 headers: body !== undefined ? { 'Content-Type': 'application/json' } : {}, 19 body: form ?? (body !== undefined ? JSON.stringify(body) : undefined), 20 }); 21 let parsed = null; 22 try { 23 parsed = await res.json(); 24 } catch { 25 // a static host's 404 page, or a proxy's error 26 } 27 if (!res.ok) throw new Error(parsed?.error ?? `the site answered ${res.status}`); 28 return parsed; 29} 30 31export const listSongs = (o) => call('songs', o).then((b) => b.songs); 32export const getSong = (id, o) => call(`songs/${id}`, o); 33export const publishSong = (fields, o) => call('songs', { ...o, method: 'POST', body: fields }); 34export const reviseSong = (id, fields, o) => call(`songs/${id}/revisions`, { ...o, method: 'POST', body: fields }); 35export function uploadCover(id, file, alt, o) { 36 const form = new FormData(); 37 form.set('image', file); 38 form.set('alt', alt); 39 return call(`songs/${id}/cover`, { ...o, method: 'PUT', form }); 40} 41export const listComments = (song, o) => call(`comments?song=${encodeURIComponent(song)}`, o).then((b) => b.comments); 42export const postComment = (song, body, o) => call('comments', { ...o, method: 'POST', body: { song, body } });
sort is 'new' (the default) or 'votes'; each pitch has its votes,
whether you voted (signed in), and the listener songs that answers it
your vote for a public pitch, cast or taken back → { votes, voted }
49export const votePitch = (id, on, o) => call(`pitches/${id}/vote`, { ...o, method: on ? 'PUT' : 'DELETE' });
the pitch your song answers (null for none); its author only
A listener's song in the shape the song browser uses for the site's own
(songs.mjs), from the Worker's list entry or its whole view (which also
has the code, spec and revisions). Its code is foreignCode, never
code: it plays only in the sandbox (sandbox.mjs), and every path that
plays a site's song reads code, so none of them can take it.
60export function asSong(s) { 61 return { 62 id: `${LISTENER_PREFIX}${s.id}`, 63 listenerId: s.id, 64 author: s.author, 65 authorId: s.authorId ?? null, 66 title: s.title, 67 theme: 'listeners', 68 description: s.description, 69 rev: s.rev, 70 timing: null, 71 art: s.art ? { p: s.art.art, model: s.art.model, method: s.art.method, criteria: s.art.criteria } : null, 72 cover: s.cover ? coverUrl(s.cover.id) : null, 73 coverAlt: s.cover?.alt ?? '', 74 foreignCode: s.code ?? null, 75 spec: s.spec ?? '', 76 revisions: s.revisions ?? [], 77 // the pitch it answers, its author's say (pitches.mjs) 78 pitch: s.pitch ?? null, 79 }; 80}
The listener songs this page has loaded whole, by page id, so the song view can find whatever is playing (nowPlaying holds its id).
Loads a listener song whole (its code and spec), keeps it, and returns it.
The ?listener=<id> a listener song's address carries, or null.
The title a song's code gives itself (// @title …), for the publish form.
104export const codeTitle = (code) => code?.match(/^\/\/ @title (.+)$/m)?.[1]?.trim() ?? '';
106const WHY = { 107 new: 'waiting for Jev', 108 budget: 'your Jev budget for today is spent', 109 'budget-unavailable': 'your Jev budget could not be checked', 110 unreachable: 'Jev could not be reached', 111 upstream: 'Jev answered with an error', 112 unanswered: 'Jev did not give a verdict', 113}; 114const clock = (ms) => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
An item's status (content-store.ts's Status), in words for its author.
117export function statusText(item, now = Date.now()) { 118 if (item.status === 'public') return 'public'; 119 if (item.status === 'held') { 120 const sure = typeof item.confidence === 'number' ? `, ${Math.round(item.confidence * 100)}% sure` : ''; 121 return `held: Jev says ${item.verdict}${sure}; only you can see it`; 122 } 123 const when = item.next > now ? `; Jev is asked again at ${clock(item.next)}` : '; Jev is asked again shortly'; 124 return `pending: ${WHY[item.why] ?? item.why}${when}`; 125}
A fine revision whose score is still owed, in words.
128export function scoreText(revision, now = Date.now()) { 129 if (revision.art !== null && revision.art !== undefined) return `${Math.round(revision.art * 100)}% art`; 130 if (!revision.score) return revision.status === 'public' ? 'art: being scored' : ''; 131 const s = revision.score; 132 const when = s.next > now ? ` (asked again at ${clock(s.next)})` : ''; 133 return `art pending: ${WHY[s.why] ?? s.why}${when}`; 134}