Files
SoulStream/src/lib/mockLibrary.js
T
KeelrLeek aff29aba5c android: a streamed file copied into a folder, the listening port, the dev build's diagnostics
The now-playing sheet's streamed track can be copied into a folder picked
on the phone, as the file it is — the cache written out, nothing downloaded
again, no library row (copy_stream_to_folder, which the desktop has had a
command for and no UI). You → Sharing carries the listening port, what
peers dial when they download from you: unset is the client's own 50300,
the app asks the router to forward it over UPnP and falls back to the
server relaying, and a change applies on the next connect. The dev flavour
gets the desktop's debug overlay as a screen under This phone — CPU,
memory, the network, the stream and its loudness, what the pre-buffer is
fetching, the size of the play history — behind devBuild in util.js, which
is Vite's own DEV flag, so the embedded build the phone keeps has no row
to tap.
2026-09-13 23:59:47 -05:00

889 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Dev-only mock library data, for exercising the Library UI on a machine
// without a populated music library. Inert unless explicitly enabled by
// setting localStorage.mockLibrary = "1" in the webview (then reload);
// remove the key to go back to real backend data.
const ARTISTS = ["Aphex Twin","Boards of Canada","Caribou","Daft Punk","Eno Brian","Four Tet","Grimes","Holly Herndon","Jon Hopkins","Kraftwerk","LCD Soundsystem","Moderat","Nils Frahm","Oneohtrix","Portishead","Radiohead","Squarepusher","Tycho","Underworld","Vangelis","Wilco","Yello","Zero 7","Air","Bonobo","Burial","Clark","Deadmau5","Emancipator","Floating Points","Gold Panda","Helios","Kiasmos","Lorn","Moby","Nujabes","Ott","Plaid","Rival Consoles","Shpongle"];
// Titles vary by slot so the Library's classifier has something real to file:
// a live set, demos, a remix record and a collection appear per artist cycle,
// and a few rows have NO art_path so the generated artwork can be judged.
const TITLE_SHAPES = [
(a, j) => `Album ${j + 1} of ${a}`,
(a) => `Live at the Roundhouse`,
(a) => `Four-Track Demos`,
(a) => `Remixes 2001`,
(a) => `Singles Collection`,
];
const albums = [];
let aid = 1;
for (let i = 0; i < ARTISTS.length; i++) {
const n = 2 + (i % 4); // 2-5 albums per artist
for (let j = 0; j < n; j++) {
const shape = j < 2 ? TITLE_SHAPES[0] : TITLE_SHAPES[(i + j) % TITLE_SHAPES.length];
albums.push({
id: aid,
artist: ARTISTS[i],
title: shape(ARTISTS[i], j),
year: 1995 + ((i + j) % 30),
track_count: j === 2 && i % 5 === 0 ? 3 : 8 + ((i + j) % 7),
art_path: (i + j) % 4 === 3 ? null : `mock://art/${aid}`,
artist_art_path: `mock://artist/${i}`,
added_at: 1700000000 + i * 86400 * 9 + j * 86400 * 40,
path: `C:\\mock\\${i}\\${j}`,
});
aid++;
}
}
// Per-album file aggregates, the same shape library_album_stats returns.
// Deterministic variety: a third FLAC 16/44.1, a few 24/96, the rest MP3.
function statsFor(a) {
const kind = a.id % 7;
const flac = kind === 0 || kind === 3 || kind === 5;
const hires = kind === 5;
const perTrack = flac ? (hires ? 120e6 : 38e6) : 9e6;
return {
album_id: a.id,
secs: a.track_count * (170 + (a.id % 90)),
bytes: Math.round(a.track_count * perTrack),
avg_kbps: flac ? (hires ? 2400 : 900) + (a.id % 120) : 192 + (a.id % 4) * 32,
bit_depth: flac ? (hires ? 24 : 16) : null,
sample_rate: flac ? (hires ? 96000 : 44100) : null,
formats: [flac ? "flac" : "mp3"],
last_played_at: a.id % 3 === 0 ? null : 1723000000 - (a.id % 40) * 86400,
};
}
function svgArt(hue, label) {
const s =
`<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">` +
`<rect width="256" height="256" fill="hsl(${hue},45%,35%)"/>` +
`<text x="128" y="140" font-size="90" text-anchor="middle" fill="rgba(255,255,255,.75)" font-family="sans-serif">${label}</text></svg>`;
return "data:image/svg+xml;base64," + btoa(s);
}
function tracksFor(albumId) {
const album = albums.find((a) => a.id === albumId);
const artist = album?.artist ?? "?";
const s = album ? statsFor(album) : null;
const ext = s?.formats[0] ?? "mp3";
const n = album?.track_count ?? 10;
return Array.from({ length: n }, (_, k) => ({
id: albumId * 100 + k,
album_id: albumId,
title: `Track ${k + 1}`,
artist,
track_no: k + 1,
duration_secs: 120 + ((albumId + k) % 9) * 23,
bitrate_kbps: s?.avg_kbps ?? 320,
bit_depth: s?.bit_depth ?? null,
sample_rate: s?.sample_rate ?? null,
size: Math.round((s?.bytes ?? 40e6) / n),
path: `C:\\mock\\t${albumId}-${k}.${ext}`,
}));
}
/* ── playlists: the desktop's shapes (library::Playlist rows with the
command layer's duration_secs / unresolved_count, PlaylistItemPayload
rows with `missing`). One pinned playlist holding every row kind, a
setlist with coherent cue times, and an empty one. Mutations bump the
revision and emit soulseek:playlists-changed, as the real commands do. */
let plSeq = 3;
let plItemSeq = 100;
const PL_T0 = 1_725_000_000;
const PL = [
{ id: 1, name: "Late night drive", track_count: 0, description: "Slow, wide, mostly synths.", kind: "manual", pinned: true, cover_path: "mock://cover1", created_at: PL_T0, updated_at: PL_T0, revision: 1, duration_secs: 0, unresolved_count: 0 },
{ id: 2, name: "Boiler Room 2019", track_count: 0, description: null, kind: "setlist", pinned: false, cover_path: null, created_at: PL_T0 + 10, updated_at: PL_T0 + 10, revision: 1, duration_secs: 0, unresolved_count: 0 },
{ id: 3, name: "Empty one", track_count: 0, description: null, kind: "manual", pinned: false, cover_path: null, created_at: PL_T0 + 20, updated_at: PL_T0 + 20, revision: 1, duration_secs: 0, unresolved_count: 0 },
];
function plLocalRow(albumId, k, extra = {}) {
const t = tracksFor(albumId)[k];
const a = albums.find((x) => x.id === albumId);
return { title: t.title, artist: a.artist, album: a.title, duration_secs: t.duration_secs, path: t.path, art_path: a.art_path, kind: "local", source_line: null, start_ms: null, note: null, track_id: null, art_url: null, missing: false, ...extra };
}
const PL_ITEMS = {
1: [
plLocalRow(1, 0), plLocalRow(2, 1), plLocalRow(5, 2, { missing: true }), plLocalRow(9, 3),
{ title: "One More Time", artist: "Daft Punk", album: "Discovery", duration_secs: 200, path: null, art_path: null, kind: "stream", source_line: "Daft Punk - One More Time", start_ms: null, note: null, track_id: "3135550", art_url: svgArt(200, "D"), missing: false },
{ title: "ID", artist: "ID", album: null, duration_secs: null, path: null, art_path: null, kind: "unresolved", source_line: "07 ID - ID", start_ms: null, note: null, track_id: null, art_url: null, missing: false },
],
2: [plLocalRow(3, 0, { start_ms: 0 }), plLocalRow(3, 1, { start_ms: 245_000 }), plLocalRow(3, 2, { start_ms: 1_130_000 })],
3: [],
};
for (const id of Object.keys(PL_ITEMS)) PL_ITEMS[id].forEach((r, i) => { r.id = ++plItemSeq; r.position = i; });
function plSync(p) {
const rows = PL_ITEMS[p.id] || [];
p.track_count = rows.length;
p.duration_secs = rows.reduce((n, r) => n + (r.duration_secs || 0), 0);
p.unresolved_count = rows.filter((r) => r.kind === "unresolved").length;
}
PL.forEach(plSync);
function plTouch(p) {
if (p) { p.revision++; p.updated_at = Math.floor(Date.now() / 1000); plSync(p); }
setTimeout(() => mockEmit("soulseek:playlists-changed", {}), 30);
}
const plOf = (args) => PL.find((p) => p.id === Number(args?.id ?? args?.playlistId));
/* the per-track auto-sync switch the lyrics sheet flips */
let autoSyncOff = false;
/* every track has three timed lines; the sheet's tools have something to act on */
function mockLyrics(title) {
return {
status: "synced", source: "lrclib",
synced: [{ time_ms: 0, text: "Here is " + (title || "a song") }, { time_ms: 4000, text: "Every word in time" }, { time_ms: 9000, text: "And the last one, late" }],
plain: null, offset_ms: 0, auto_offset_ms: 0, auto_sync_off: autoSyncOff,
};
}
function mockPlaylists(cmd, args) {
if (cmd === "list_playlists") return Promise.resolve(PL.map((p) => ({ ...p })));
if (cmd === "playlist_items") return new Promise((r) => setTimeout(() => r((PL_ITEMS[Number(args?.id)] || []).map((x) => ({ ...x }))), 80));
if (cmd === "playlist_cover") { const p = plOf(args); return Promise.resolve(p && p.cover_path ? svgArt(p.id === 1 ? 330 : 280, p.id === 1 ? "L" : "M") : null); }
if (cmd === "playlist_cover_set") { const p = plOf(args); if (!p) return Promise.reject(new Error("no such playlist")); p.cover_path = String(args?.path || "mock://photo"); plTouch(p); return Promise.resolve(svgArt(300, "P")); }
if (cmd === "playlist_cover_clear") { const p = plOf(args); if (p) p.cover_path = null; plTouch(p); return Promise.resolve(); }
if (cmd === "create_playlist") {
const p = { id: ++plSeq, name: args?.name || "New Playlist", track_count: 0, description: null, kind: args?.kind || "manual", pinned: false, cover_path: null, created_at: Math.floor(Date.now() / 1000), updated_at: 0, revision: 0, duration_secs: 0, unresolved_count: 0 };
PL.push(p);
PL_ITEMS[p.id] = [];
plTouch(p);
return Promise.resolve(p.id);
}
if (cmd === "rename_playlist") { const p = plOf(args); if (p) p.name = args.name; plTouch(p); return Promise.resolve(); }
if (cmd === "set_playlist_description") { const p = plOf(args); if (p) p.description = args.description || null; plTouch(p); return Promise.resolve(); }
if (cmd === "set_playlist_pinned") { const p = plOf(args); if (p) p.pinned = !!args.pinned; plTouch(p); return Promise.resolve(); }
if (cmd === "duplicate_playlist") {
const p = plOf(args);
if (!p) return Promise.reject(new Error("duplicate_playlist: no such playlist"));
const c = { ...p, id: ++plSeq, name: args?.name || p.name + " copy", pinned: false, revision: 0 };
PL.push(c);
PL_ITEMS[c.id] = (PL_ITEMS[p.id] || []).map((x, i) => ({ ...x, id: ++plItemSeq, position: i }));
plTouch(c);
return Promise.resolve(c.id);
}
if (cmd === "delete_playlist") {
const i = PL.findIndex((p) => p.id === Number(args?.id));
if (i >= 0) { delete PL_ITEMS[PL[i].id]; PL.splice(i, 1); }
plTouch(null);
return Promise.resolve();
}
if (cmd === "playlist_remove_items") {
const p = plOf(args);
const ids = new Set((args?.itemIds || []).map(Number));
if (p) { PL_ITEMS[p.id] = (PL_ITEMS[p.id] || []).filter((x) => !ids.has(x.id)); PL_ITEMS[p.id].forEach((x, i) => { x.position = i; }); }
plTouch(p);
return Promise.resolve();
}
if (cmd === "playlist_add_items") {
const p = plOf(args);
if (!p) return Promise.reject(new Error("playlist_add_items: no such playlist"));
const rows = (args?.items || []).map((it) => ({
title: it.title, artist: it.artist || "", album: it.album ?? null, duration_secs: it.duration_secs ?? null,
path: it.path ?? null, art_path: it.art_path ?? null,
kind: it.path ? "local" : (it.title && it.artist ? "stream" : "unresolved"),
source_line: it.source_line ?? null, start_ms: it.start_ms ?? null, note: it.note ?? null,
track_id: it.track_id ?? null, art_url: it.art_url ?? null, missing: false, id: ++plItemSeq,
}));
const list = PL_ITEMS[p.id];
const at = args?.at == null ? list.length : Math.min(Number(args.at), list.length);
list.splice(at, 0, ...rows);
list.forEach((x, i) => { x.position = i; });
plTouch(p);
return Promise.resolve(rows.map((r) => r.id));
}
if (cmd === "playlist_set_order") {
const p = plOf(args);
if (!p) return Promise.reject(new Error("playlist_set_order: no such playlist"));
const by = new Map((PL_ITEMS[p.id] || []).map((x) => [x.id, x]));
const next = (args?.orderedIds || []).map(Number).map((id) => by.get(id)).filter(Boolean);
for (const x of PL_ITEMS[p.id] || []) if (!next.includes(x)) next.push(x);
PL_ITEMS[p.id] = next;
next.forEach((x, i) => { x.position = i; });
plTouch(p);
return Promise.resolve();
}
if (cmd === "playlist_relink_item") {
const p = plOf(args);
const it = p ? (PL_ITEMS[p.id] || []).find((x) => x.id === Number(args?.itemId)) : null;
if (!it) return Promise.reject(new Error("playlist_relink_item: no such row"));
if (args.path != null) { it.path = args.path; it.kind = "local"; it.missing = false; }
if (args.artPath != null) it.art_path = args.artPath;
if (args.durationSecs != null) it.duration_secs = args.durationSecs;
if (args.title != null) it.title = args.title;
if (args.artist != null) it.artist = args.artist;
if (args.album != null) it.album = args.album;
plTouch(p);
return Promise.resolve();
}
return undefined;
}
/* ── the import doors: a pasted tracklist, a link, a YouTube set — a draft
in the backend's shape (ImportDraft / DraftEntry, resolved against the
mock library), then commit makes the playlist ── */
function impEntry(ordinal, artist, title, raw, start_ms = null) {
const own = albums.find((a) => a.artist.toLowerCase() === artist.toLowerCase());
const tr = own ? tracksFor(own.id).find((t) => t.title.toLowerCase() === title.toLowerCase()) : null;
const weak = !artist || !title || title.toLowerCase() === "id";
return {
ordinal, title, artist, album: tr ? own.title : null,
duration_secs: tr ? tr.duration_secs : (weak ? null : 200 + ((ordinal * 37) % 180)),
start_ms, raw,
resolution: tr ? "local" : weak ? "unresolved" : "stream",
matched_by: tr ? "tags" : null,
path: tr ? tr.path : null, art_path: tr ? own.art_path : null,
track_id: null, art_url: null, isrc: null,
};
}
function impParse(text) {
const out = [];
for (const raw of String(text || "").split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
const m = /^(?:(\d+):(\d\d)(?::(\d\d))?\s+)?(.*?)\s+[-–]\s+(.*)$/.exec(line);
const start_ms = m && m[1] != null ? ((m[3] != null ? (+m[1] * 3600 + +m[2] * 60 + +m[3]) : (+m[1] * 60 + +m[2])) * 1000) : null;
out.push(impEntry(out.length, m ? m[4].trim() : "", m ? m[5].trim() : line, raw, start_ms));
}
return out;
}
function impDraft(entries, source_name, format, name = null, extra = {}) {
return {
source_name, format, name, description: null,
kind: entries.some((e) => e.start_ms != null) ? "setlist" : "manual",
entries, warnings: [],
resolved_count: entries.filter((e) => e.resolution === "local").length,
stream_count: entries.filter((e) => e.resolution === "stream").length,
unresolved_count: entries.filter((e) => e.resolution === "unresolved").length,
cover_url: null,
...extra,
};
}
function mockImport(cmd, args) {
if (cmd === "playlist_import_text") {
const entries = impParse(args?.text);
if (!entries.length) return Promise.reject(new Error("Nothing in that text reads as a track."));
return new Promise((r) => setTimeout(() => r(impDraft(entries, "Pasted tracklist", "text")), 400));
}
if (cmd === "playlist_import_link" || cmd === "playlist_import_youtube") {
const url = String(args?.url || "");
if (!/^https?:\/\//.test(url)) return Promise.reject(new Error("That is not a link."));
const set = cmd === "playlist_import_youtube";
const format = set ? "youtube" : /spotify/.test(url) ? "spotify" : /apple/.test(url) ? "apple-music" : /tidal/.test(url) ? "tidal" : /deezer/.test(url) ? "deezer" : /youtu/.test(url) ? "youtube-playlist" : null;
if (!format) return Promise.reject(new Error("That link is not a playlist SoulStream can read."));
const lines = set
? ["00:00 Daft Punk - One More Time", "05:20 Daft Punk - Aerodynamic", "09:00 ID - ID", "12:10 Aphex Twin - Track 1"]
: ["Daft Punk - One More Time", "Daft Punk - Aerodynamic", "Daft Punk - Digital Love", "Aphex Twin - Track 1", "ID - ID"];
const entries = impParse(lines.join("\n"));
return new Promise((r) => {
let done = 0;
const total = entries.length;
const tick = setInterval(() => {
done++;
mockEmit("playlist:link-progress", { done, total, waiting: false });
if (done >= total) {
clearInterval(tick);
setTimeout(() => r(impDraft(entries,
set ? "Mock Channel — Boiler Room 2019" : "Mock Curator — Late Night Mix", format,
set ? "Boiler Room 2019 (live)" : "Late Night Mix",
{ cover_url: svgArt(280, "M"), warnings: set ? ["Tracklist from the video's chapters."] : [] })), 200);
}
}, 180);
});
}
if (cmd === "playlist_import_track_links") {
const links = String(args?.text || "").split(/\r?\n/).map((l) => l.trim())
.filter((l) => /^https?:\/\//.test(l) || /^spotify:track:/.test(l));
if (!links.length) return Promise.reject(new Error("Nothing to import in that paste."));
/* one row per link, in the paste's order — the lines a real lookup
would have read off each track's own public page. A page each is
slow, and the pace here is slow on purpose: the walk's still takes
long enough to miss a wait card that closes inside a second. */
const named = ["Aphex Twin - Track 1", "Daft Punk - One More Time", "Bicep - Glue", "Four Tet - Track 2"];
const entries = impParse(links.map((_, i) => named[i % named.length]).join("\n"));
return new Promise((r) => {
let done = 0;
const tick = setInterval(() => {
done++;
mockEmit("playlist:link-progress", { done, total: links.length, waiting: false });
if (done >= links.length) {
clearInterval(tick);
setTimeout(() => r(impDraft(entries, args?.name || "Pasted track links", "links", args?.name ?? null, {
warnings: [entries.length + " track" + (entries.length === 1 ? "" : "s") + " read from " + links.length + " pasted link" + (links.length === 1 ? "" : "s") + "."],
})), 250);
}
}, 600);
});
}
if (cmd === "playlist_import_folder") {
const root = String(args?.path || "");
if (!root) return Promise.resolve(null); /* the desktop's dialog, dismissed */
const name = root.split(/[\\/]/).filter(Boolean).pop() || "Folder";
/* four files off a record the library already holds, as the backend's
walk hands them over: every row local, matched by path */
const own = albums[0];
const entries = tracksFor(own.id).slice(0, 4).map((t, i) => ({
ordinal: i, title: t.title, artist: own.artist, album: own.title,
duration_secs: t.duration_secs, start_ms: null, raw: t.path,
resolution: "local", matched_by: "path", path: t.path, art_path: own.art_path,
track_id: null, art_url: null, isrc: null,
}));
return new Promise((r) => setTimeout(() => r(impDraft(entries, name, "folder", name)), 500));
}
if (cmd === "playlist_import_commit") {
const name = String(args?.name || "").trim();
if (!name) return Promise.reject(new Error("The playlist needs a name"));
const p = { id: ++plSeq, name, track_count: 0, description: args?.description ?? null, kind: args?.kind === "setlist" ? "setlist" : "manual", pinned: false, cover_path: args?.coverUrl ? "mock://cover" : null, created_at: Math.floor(Date.now() / 1000), updated_at: 0, revision: 0, duration_secs: 0, unresolved_count: 0 };
PL.push(p);
PL_ITEMS[p.id] = (args?.entries || []).map((e, i) => ({
id: ++plItemSeq, position: i, title: e.title, artist: e.artist, album: e.album ?? null, duration_secs: e.duration_secs ?? null,
path: e.path ?? null, art_path: e.art_path ?? null,
kind: e.path ? "local" : e.resolution === "unresolved" ? "unresolved" : "stream",
source_line: e.raw ?? null, start_ms: e.start_ms ?? null, note: null,
track_id: e.track_id != null ? String(e.track_id) : null, art_url: e.art_url ?? null, missing: false,
}));
plTouch(p);
return Promise.resolve(p.id);
}
if (cmd === "catalog_search_tracks") {
const words = String(args?.query || "").toLowerCase().replace(/\b(artist|track|album):/g, " ").replace(/["']/g, " ").split(/\s+/).filter(Boolean);
const hits = CAT_TRACKS.filter((t) => { const hay = (t.artist + " " + t.title + " " + t.album).toLowerCase(); return words.every((w) => hay.includes(w)); });
return new Promise((r) => setTimeout(() => r(hits.slice(0, args?.limit || 10)), 90));
}
if (cmd === "catalog_track_by_isrc") return Promise.resolve(null);
if (cmd === "catalog_artist_track_candidates") return Promise.resolve([]);
return undefined;
}
const enabled = (() => {
try {
return localStorage.getItem("mockLibrary") === "1";
} catch {
return false;
}
})();
/** Returns a Promise for mocked commands, or undefined to pass through. */
export function mockInvoke(cmd, args) {
if (!enabled) return undefined;
if (cmd === "app_status") return Promise.resolve({ version: "mock", connected_as: null });
if (cmd === "library_albums") return Promise.resolve(albums);
if (cmd === "library_album_stats") return Promise.resolve(albums.map(statsFor));
if (cmd === "recent_albums") return Promise.resolve(albums.slice(0, 3).map((a, i) => ({ kind: "library", album_id: a.id, title: a.title, artist: a.artist, art_path: a.art_path, last_played_at: Math.floor(Date.now() / 1000) - 3600 * (i + 1) })));
if (cmd === "album_tracks")
return new Promise((r) => setTimeout(() => r(tracksFor(args.albumId)), 60));
if (cmd === "album_art") {
/* not a path: the real one fails to open it, which is why the art
gate has to answer a data: URL itself */
if (String(args.path || "").startsWith("data:")) return Promise.reject(new Error("album_art: not a path"));
const m = /(\d+)$/.exec(args.path || "");
const num = m ? +m[1] : 0;
return Promise.resolve(svgArt((num * 47) % 360, String.fromCharCode(65 + (num % 26))));
}
{ const pl = mockPlaylists(cmd, args) ?? mockImport(cmd, args); if (pl) return pl; }
// The track index, as the library crate shapes it (TrackIndexRow) — the
// phone's Tracks tab and its library search read these.
if (cmd === "library_track_index" || cmd === "library_track_search") {
const rows = albums.flatMap((a) =>
tracksFor(a.id).map((t) => ({
title: t.title, artist: a.artist, album: a.title,
duration_secs: t.duration_secs, path: t.path, art_path: a.art_path,
})));
if (cmd === "library_track_index") return Promise.resolve(rows);
const words = String(args?.query || "").toLowerCase().split(/\s+/).filter(Boolean);
return Promise.resolve(words.length
? rows.filter((r) => { const hay = (r.artist + r.title + r.album).toLowerCase(); return words.every((w) => hay.includes(w)); }).slice(0, 40)
: []);
}
const r = mockCatalog(cmd, args) ?? mockNet(cmd, args);
if (r === undefined) console.warn("[mock] no mock for " + cmd + " — the real IPC is not here");
return r;
}
/* ── the fake network ───────────────────────────────────────────────────
A Soulseek behind the phone's screens: a stored account that signs in at
boot, a room list, rooms with a few lines in them, a search answered by
five peers, and a stream that arrives over twelve seconds with its
waveform growing — enough to walk Search, Community and the rising lane
in a plain browser. Events go out over the bus below; the harness can
fire its own through window.__ssMock.emit. */
const bus = new Map();
/** Returns a Promise<unlisten> for a mocked page, or undefined to pass through. */
export function mockListen(event, cb) {
if (!enabled) return undefined;
let set = bus.get(event);
if (!set) { set = new Set(); bus.set(event, set); }
set.add(cb);
return Promise.resolve(() => { set.delete(cb); });
}
export function mockEmit(event, payload) {
for (const cb of bus.get(event) || []) {
try { cb({ event, payload }); } catch (e) { console.error(e); }
}
}
const later = (ms, fn) => setTimeout(fn, ms);
if (enabled && typeof window !== "undefined") window.__ssMock = { emit: mockEmit };
const MOCK_USER = "mockuser";
const ROOMS = [["Lounge", 412], ["Electronic", 98], ["Vinyl Rips", 37], ["Jazz", 61], ["Lossless Only", 23], ...Array.from({ length: 90 }, (_, i) => ["Room " + (i + 1), 90 - i])];
const PEOPLE = ["ada", "bo", "cy", "dee"];
const LINES = ["anyone got the 2024 remaster?", "the vinyl rip is warmer imo", "that folder is complete btw", "brb"];
/* one person already named, so a cold read of the list has something in it */
const CONTACTS = [{ username: "dusty_crates", petname: "Dusty the digger", group: "Traders", note: "always has the vinyl rips", trusted: true, favorite: false }];
const PEERS = [
{ username: "hifi_hank", slot: true, speed: 2400, queue: 0, ext: "flac", depth: 24, rate: 96000 },
{ username: "dusty_crates", slot: true, speed: 900, queue: 0, ext: "flac", depth: 16, rate: 44100 },
{ username: "bo", slot: false, speed: 1500, queue: 3, ext: "flac", depth: 16, rate: 44100 },
{ username: "mp3_marge", slot: true, speed: 500, queue: 0, ext: "mp3", bitrate: 320 },
{ username: "slowpoke", slot: false, speed: 60, queue: 12, ext: "mp3", bitrate: 192 },
];
/* a peer's own card, and the comparison overlap.rs keeps per username */
const MOCK_PIC = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mOQz+7HihiGlgQAV11GQceAU4sAAAAASUVORK5CYII=";
const MOCK_ABOUT = "Ripping my own vinyl since 2004. FLAC only, folders named Artist - Year - Album. Ask before you queue a whole discography.";
const MOCK_LIKES = ["french house", "vinyl rips", "detroit techno", "boards of canada"];
const MOCK_HATES = ["transcodes", "128 kbps"];
const OVERLAP_COMMON = [["Daft Punk", "Discovery"], ["Daft Punk", "Homework"], ["Caribou", "Swim"]];
const OVERLAP_GAPS = [
["Boards of Canada", "Geogaddi"],
["Aphex Twin", "Drukqs"],
["Daft Punk", "Alive 2007"],
["Burial", "Untrue"],
["Four Tet", "Rounds"],
["Autechre", "Amber"],
];
const overlaps = new Map();
const pairsOf = (list) => list.map(([artist, album]) => ({ artist, album }));
const TITLES = ["One More Time", "Aerodynamic", "Digital Love", "Harder, Better, Faster, Stronger", "Crescendolls", "Nightvision", "Superheroes", "High Life", "Something About Us", "Voyager", "Veridis Quo", "Short Circuit", "Face to Face", "Too Long"];
function peerResponse(token, peer, query) {
const artist = /daft/i.test(query) ? "Daft Punk" : (query.split(/\s+/)[0] || "Various");
const album = /discovery/i.test(query) ? "Discovery" : "Homework";
const folder = "@@shared\\Music\\" + artist + "\\" + artist + " - 2001 - " + album + (peer.ext === "flac" ? " [FLAC]" : " [MP3 " + peer.bitrate + "]");
const files = TITLES.map((t, i) => {
const name = String(i + 1).padStart(2, "0") + " - " + t + "." + peer.ext;
const duration = 200 + ((i * 37) % 180);
const size = peer.ext === "flac" ? Math.round(duration * (peer.depth === 24 ? 320000 : 110000)) : Math.round(duration * peer.bitrate * 125);
return { path: folder + "\\" + name, name, folder, size, extension: peer.ext, bitrate: peer.bitrate ?? null, duration, sample_rate: peer.rate ?? 44100, bit_depth: peer.depth ?? null, vbr: false };
});
return { token, username: peer.username, has_free_upload_slot: peer.slot, upload_speed: peer.speed, queue_length: peer.queue, files };
}
/* the catalog: one artist, two records, the songs of one of them */
const CAT_ARTIST = { id: 27, name: "Daft Punk", picture: svgArt(210, "DP"), album_count: 4, fans: 4200000 };
const CAT_NAMESAKE = { id: 28, name: "Daft Punk", picture: "https://cdn-images.dzcdn.net/images/artist/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg", album_count: 1, fans: 120 };
const CAT_ALBUMS = [
{ id: 302127, title: "Discovery", artist: "Daft Punk", cover: null, release_date: "2001-03-12", record_type: "album", track_count: 14, edition_group: 302127, is_soundtrack: false },
{ id: 302128, title: "Homework", artist: "Daft Punk", cover: null, release_date: "1997-01-20", record_type: "album", track_count: 16, edition_group: null, is_soundtrack: false },
];
const CAT_TRACKS = TITLES.map((t, i) => ({
id: 3135550 + i, title: t, artist: "Daft Punk", artist_id: 27, rank: 900000 - i * 1000,
album: "Discovery", album_id: 302127, cover: null, duration_secs: 200 + ((i * 37) % 180), preview: null, track_no: i + 1,
}));
const CAT_DISCO = [
...CAT_ALBUMS,
{ id: 302129, title: "Discovery (Deluxe Edition)", artist: "Daft Punk", cover: null, release_date: "2001-03-12", record_type: "album", track_count: 16, edition_group: 302127, is_soundtrack: false },
{ id: 302130, title: "Alive 1997", artist: "Daft Punk", cover: null, release_date: "2001-10-01", record_type: "album", track_count: 1, edition_group: null, is_soundtrack: false },
{ id: 302131, title: "One More Time", artist: "Daft Punk", cover: null, release_date: "2000-11-13", record_type: "single", track_count: 2, edition_group: null, is_soundtrack: false },
{ id: 302132, title: "Musique Vol. 1 1993–2005", artist: "Daft Punk", cover: null, release_date: "2006-04-03", record_type: "compile", track_count: 15, edition_group: null, is_soundtrack: false },
{ id: 302133, title: "TRON: Legacy (Original Motion Picture Soundtrack)", artist: "Daft Punk", cover: null, release_date: "2010-12-03", record_type: "album", track_count: 22, edition_group: null, is_soundtrack: true },
{ id: 302134, title: "Human After All", artist: "Daft Punk", cover: null, release_date: "2005-03-14", record_type: "album", track_count: 10, edition_group: null, is_soundtrack: false },
{ id: 302135, title: "Random Access Memories", artist: "Daft Punk", cover: null, release_date: "2013-05-17", record_type: "album", track_count: 13, edition_group: null, is_soundtrack: false },
];
/* the follows feed needs covers and dates near today */
const dayIso = (offset) => new Date(Date.now() + offset * 86400000).toISOString().slice(0, 10);
for (const a of CAT_DISCO) a.cover = svgArt((a.id * 31) % 360, a.title[0]);
CAT_DISCO.push(
{ id: 302136, title: "Infinity Repeating", artist: "Daft Punk", cover: svgArt(300, "I"), release_date: dayIso(-9), record_type: "single", track_count: 2, edition_group: null, is_soundtrack: false },
{ id: 302137, title: "Alive 2027", artist: "Daft Punk", cover: svgArt(120, "A"), release_date: dayIso(21), record_type: "album", track_count: 12, edition_group: null, is_soundtrack: false },
);
const NEWS = [
{ title: "Daft Punk announce Alive 2027 — a live record from the reunion shows", url: "https://example.com/news/1", source: "Pitchfork", date: Math.floor(Date.now() / 1000) - 3600 * 5 },
{ title: "Infinity Repeating gets a video", url: "https://example.com/news/2", source: "NME", date: Math.floor(Date.now() / 1000) - 86400 * 2 },
];
const RELATED = [["Justice", 411], ["Air", 412], ["Kavinsky", 413], ["Chemical Brothers", 414]].map(([name, id], i) => ({ id, name, picture: i % 2 ? null : svgArt(40 + id, name[0]), album_count: 3, fans: 900000 - id * 100 }));
/* the heroes chosen this session — pick_image → set_artist_hero */
const heroes = {};
let follows = [];
function mockCatalog(cmd, args) {
if (cmd === "catalog_artist") {
const id = Number(args?.artistId);
const a = id === CAT_ARTIST.id ? CAT_ARTIST : RELATED.find((r) => r.id === id);
if (!a) return Promise.reject(new Error("catalog_artist: unknown artist"));
const own = a === CAT_ARTIST;
return new Promise((r) => setTimeout(() => r({
artist: a, banner: heroes[a.id] ?? null, banners: heroes[a.id] ? [heroes[a.id]] : [], banner_custom: !!heroes[a.id], logos: [],
popular_tracks: own ? CAT_TRACKS.slice(0, 7).map((t, i) => ({ ...t, listeners: 2_400_000 - i * 310_000 })) : [],
albums: own ? CAT_DISCO : [{ id: 900 + id, title: a.name + " Live", artist: a.name, cover: null, release_date: "2015-01-01", record_type: "album", track_count: 9, edition_group: null, is_soundtrack: false }],
top_genre: "Electronic", socials: [],
}), 220));
}
if (cmd === "catalog_artist_socials") {
const id = Number(args?.artistId);
return new Promise((r) => setTimeout(() => r(id === CAT_ARTIST.id ? [{ kind: "website", url: "https://example.com" }, { kind: "wikipedia", url: "https://en.wikipedia.org/wiki/Daft_Punk" }] : []), 300));
}
if (cmd === "home_warm") return Promise.resolve();
if (cmd === "home_artist_news") return new Promise((r) => setTimeout(() => r(Object.fromEntries((args?.artists || []).map((n) => [n, /daft/i.test(n) ? NEWS : []]))), 150));
if (cmd === "catalog_artist_identity") return Promise.resolve({ artist_id: Number(args?.artistId), albums: [], socials: [], type_hints: [], owned_album_ids: [] });
if (cmd === "catalog_artist_profile") return Promise.resolve({ listeners: 3_100_000, plays: 210_000_000, tags: ["electronic", "house", "french house", "dance"] });
if (cmd === "catalog_artist_bio") return Promise.resolve("Daft Punk were a French electronic music duo formed in Paris in 1993 by Guy-Manuel de Homem-Christo and Thomas Bangalter. They achieved popularity in the late 1990s as part of the French house movement.");
if (cmd === "catalog_related") return Promise.resolve(RELATED);
/* Popular's own lane: ranked, with counts — and for the artists whose
page gives none (the related ones), the only Popular there is */
if (cmd === "catalog_artist_popular") {
const id = Number(args?.artistId);
const a = id === CAT_ARTIST.id ? CAT_ARTIST : RELATED.find((r) => r.id === id);
if (!a) return Promise.resolve([]);
const rows = id === CAT_ARTIST.id
? CAT_TRACKS.slice(0, 7)
: TITLES.slice(0, 4).map((t, i) => ({
id: 4200000 + id * 10 + i, title: t, artist: a.name, artist_id: id, rank: 700000 - i * 1000,
album: a.name + " Live", album_id: 900 + id, cover: null, duration_secs: 210 + i * 13, preview: null, track_no: i + 1,
}));
return new Promise((r) => setTimeout(() => r(rows.map((t, i) => ({ ...t, listeners: 1_900_000 - i * 240_000 }))), 260));
}
/* the deferred pass: a count per row, by that row's own artist id */
if (cmd === "catalog_track_listeners") {
const rows = args?.tracks || [];
return new Promise((r) => setTimeout(() => r(rows.map((t, i) => (t.artistId ? 640_000 - i * 20_000 : null))), 260));
}
if (cmd === "set_artist_hero") { const id = Number(args?.artistId); heroes[id] = svgArt(180, "H"); return Promise.resolve({ data_url: heroes[id], width: 3840, height: 2160 }); }
if (cmd === "clear_artist_hero") { const id = Number(args?.artistId); const had = id in heroes; delete heroes[id]; return Promise.resolve(had); }
if (cmd === "artist_play_stats") return Promise.resolve({ seconds: 7380, last_played_at: Math.floor(Date.now() / 1000) - 86400, top_title: "Track 1", top_count: 12 });
if (cmd === "followed_artists") return Promise.resolve(follows.slice());
if (cmd === "follow_artist") { follows.push({ artist_id: args?.artistId, name: args?.name, portrait: args?.portrait ?? null, followed_at: Date.now() / 1000 }); later(30, () => mockEmit("soulseek:follows-changed", {})); return Promise.resolve(); }
if (cmd === "unfollow_artist") { follows = follows.filter((f) => f.artist_id !== args?.artistId); later(30, () => mockEmit("soulseek:follows-changed", {})); return Promise.resolve(); }
if (cmd === "catalog_search") {
const q = String(args?.query || "").toLowerCase();
const hit = /daft|discovery|one more|punk/.test(q);
return new Promise((r) => setTimeout(() => r(hit
? { artists: [CAT_ARTIST, CAT_NAMESAKE], albums: CAT_ALBUMS, tracks: CAT_TRACKS.slice(0, 8) }
: { artists: [], albums: [], tracks: [] }), 120));
}
if (cmd === "catalog_album") {
const a = CAT_ALBUMS.find((x) => x.id === Number(args?.albumId));
if (!a) return Promise.reject(new Error("catalog_album: unknown album"));
/* the last two rows are a guest's: the album's own artist anchor
has no count for them, which is what the deferred pass is for */
return Promise.resolve({ album: a, tracks: a.id === 302127 ? CAT_TRACKS.map((t, i) => (i >= CAT_TRACKS.length - 2 ? { ...t, artist: "Daft Punk feat. Todd Edwards", artist_id: 411 } : { ...t, listeners: 2_400_000 - i * 150_000 })) : [], album_plays: a.id === 302127 ? 310_000_000 : null, album_listeners: a.id === 302127 ? 4_100_000 : null, totals_summed: false });
}
if (cmd === "catalog_album_info") {
const t = String(args?.title || "");
return new Promise((r) => setTimeout(() => r(/discovery/i.test(t)
? { description: "Discovery is the second studio album by the French electronic music duo Daft Punk, released on 12 March 2001. It marks a shift from the Chicago house of their debut to a house style more heavily inspired by disco, post-disco, garage house and synth-pop. The album's singles included the worldwide hit One More Time.", genre: "Electronic", label: "Virgin", source: "wikipedia" }
: null), 150));
}
if (cmd === "download_file") {
const id = ++downloadSeq;
const total = args?.size || 20e6;
const name = String(args?.path || "").split("\\").pop();
const base = { id, username: args?.username, path: args?.path, name, queue_place: null, local_path: null, error: null };
later(200, () => mockEmit("soulseek:download", { ...base, state: "connecting", received: 0, total }));
let got = 0;
const step = setInterval(() => {
got = Math.min(total, got + total / 8);
mockEmit("soulseek:download", { ...base, state: got >= total ? "complete" : "transferring", received: Math.round(got), total, local_path: got >= total ? "C:\\mock\\library\\" + name : null });
if (got >= total) { clearInterval(step); mockEmit("soulseek:library-changed", {}); }
}, 400 + (id % 3) * 120);
return Promise.resolve(id);
}
if (cmd === "cancel_download") return Promise.resolve();
if (cmd === "rescan_library") return Promise.resolve();
if (cmd === "library_art_prewarm") return Promise.resolve();
if (cmd === "refresh_all_album_art") return new Promise((r) => setTimeout(() => r(140), 300));
if (cmd === "refresh_artist_art") return new Promise((r) => setTimeout(() => r(26), 300));
if (cmd === "catalog_art") return Promise.resolve(svgArt(200, "D"));
if (cmd === "stream_cache_ready") return Promise.resolve(cacheReady.has(args?.path) || (xfer && xfer.path === args?.path && xfer.received >= xfer.total));
if (cmd === "preload_stream") {
const path = args?.path;
cacheSizes.set(path, args?.size || 0);
if (!cacheReady.has(path) && !preloading.has(path)) { preloading.add(path); later(4000, () => { preloading.delete(path); cacheReady.add(path); }); }
return Promise.resolve();
}
if (cmd === "preload_cached_stream") return Promise.resolve(cacheReady.has(args?.path));
if (cmd === "excluded_search_phrases") return Promise.resolve([]);
if (cmd === "copy_stream_to_folder") {
/* the real command copies only what the stream cache already holds */
let saved = 0, missing = 0;
for (const f of args?.files || []) { if (cacheReady.has(f.path)) saved++; else missing++; }
return Promise.resolve([saved, missing]);
}
if (cmd === "play_row_count") return Promise.resolve(4210);
if (cmd === "debug_stats") {
/* the overlay's shape, camelCase as serde renames it; the numbers
wander a little so the screen is seen to be live */
const t = Date.now() / 1000;
return Promise.resolve({
cpuProcess: 7.5 + Math.sin(t), cpuSystem: 22 + Math.cos(t) * 3,
gpuPercent: null, gpuAppPercent: null,
memWorkingSet: 431_000_000, memPrivate: 0,
memSystemUsed: 5_400_000_000, memSystemTotal: 7_900_000_000,
netRxBps: 1_250_000, netTxBps: 96_000,
processCount: 3, uptimeSecs: 742,
dlReceived: xfer ? Math.round(xfer.received) : null,
dlTotal: xfer ? xfer.total : null,
dlDone: xfer ? xfer.received >= xfer.total : null,
normalizationEnabled: mockSettings.volume_normalization !== false, targetLufs: -14,
loudness: { lufs: -9.8, peak: 0.98, gainDb: -4.2, cached: true, analyzing: false, source: "cache", headStartDb: 0, prefixGainDb: null },
mainFadeGain: 1, normNowDb: -4.2, normTargetDb: -4.2,
previewVolume: null, previewNormDb: null,
preloadDl: [{ username: "dusty_crates", path: "Music\\Daft Punk\\Discovery\\02 Aerodynamic.flac", state: "transferring", place: null, received: 5_000_000, total: 12_000_000, speedBps: 850_000 }],
});
}
if (cmd === "get_settings") return Promise.resolve({ ...mockSettings, stream_source_prefs: { ...mockSettings.stream_source_prefs }, download_source_prefs: { ...mockSettings.download_source_prefs } });
if (cmd === "set_settings") {
/* the command's arguments are camelCase; the config's fields are snake_case */
for (const [k, v] of Object.entries(args || {})) mockSettings[k.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase())] = v;
return Promise.resolve();
}
if (cmd === "plugin:storage|pick_folder") return Promise.resolve({ uri: "content://mock/tree/primary%3AMusic", path: "C:\\mock\\Music" });
if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: true });
if (cmd === "plugin:storage|request_all_files_access") return Promise.resolve();
if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\mock\\photo.jpg" });
if (cmd === "plugin:share|pending_share") {
/* the harness shares by setting window.__ssMock.share and waking the page */
const t = (typeof window !== "undefined" && window.__ssMock?.share) || null;
if (t && window.__ssMock) window.__ssMock.share = null;
return Promise.resolve({ text: t });
}
return undefined;
}
let token = 0;
let downloadSeq = 0;
const mockSettings = {
download_dir: "C:\\mock\\Library",
volume_normalization: true,
library_layout: "{artist}\\{album}",
shared_folders: [],
upload_slots: 2,
listen_port: null,
share_files: 1234,
word_timing: false,
replace_embedded_art: true,
embed_art_max_edge: 1500,
write_folder_cover: true,
stream_keep_forever: false,
stream_retention_days: 30,
stream_cache_max_mb: 2048,
stream_source_prefs: { max: "any", min: "any", priority: "quality" },
download_source_prefs: { max: "any", min: "any", priority: "quality" },
};
const cacheReady = new Set(); // paths a preload or a finished stream has fully fetched
const cacheSizes = new Map(); // path → the file's size, for stream_cache_len
const preloading = new Set();
let xfer = null; // the one fake transfer: { received, total, timer }
let posTimer = null; // the fake engine's position ticks
/* the interests this fake account has declared, and the server's match on
them — a declaration moves both, the way the real server does */
const MY_INTERESTS = { likes: ["Daft Punk", "french house", "Warp Records"], hates: ["loudness war"] };
function mockNet(cmd, args) {
if (cmd === "get_credentials") return Promise.resolve({ username: MOCK_USER, has_password: true });
if (cmd === "connect_stored" || cmd === "connect_and_login") {
later(400, () => mockEmit("soulseek:state", { kind: "connected", username: MOCK_USER }));
return Promise.resolve();
}
if (cmd === "disconnect") { later(50, () => mockEmit("soulseek:state", { kind: "disconnected" })); return Promise.resolve(); }
if (cmd === "forget_credentials") return Promise.resolve();
if (cmd === "buddy_list") return Promise.resolve(["ada", "cy"]);
if (cmd === "buddy_add") return Promise.resolve(["ada", "cy", args?.username].filter(Boolean));
if (cmd === "buddy_remove") return Promise.resolve(["ada", "cy"].filter((u) => u !== args?.username));
if (cmd === "contacts_meta_list") return Promise.resolve(CONTACTS.slice());
if (cmd === "contact_meta_set") {
/* the backend's own rule: an entry with nothing set is dropped */
const meta = args?.meta || {};
const txt = (v) => (typeof v === "string" && v.trim() ? v.trim() : null);
const i = CONTACTS.findIndex((c) => c.username === meta.username);
if (i >= 0) CONTACTS.splice(i, 1);
const kept = { username: meta.username, petname: txt(meta.petname), group: txt(meta.group), note: txt(meta.note), trusted: !!meta.trusted, favorite: !!meta.favorite };
if (kept.petname || kept.group || kept.note || kept.trusted || kept.favorite) CONTACTS.push(kept);
return Promise.resolve(CONTACTS.slice());
}
if (cmd === "slsk_room_list") {
later(200, () => mockEmit("soulseek:room-list", ROOMS.map(([name, users]) => ({ name, users }))));
return Promise.resolve();
}
if (cmd === "slsk_join_room") {
const room = args?.room;
later(250, () => mockEmit("soulseek:room-joined", { room, users: [...PEOPLE, MOCK_USER].map((username) => ({ username })) }));
LINES.forEach((message, i) => later(400 + i * 350, () => mockEmit("soulseek:room-message", { room, username: PEOPLE[i % PEOPLE.length], message, ts: Math.floor(Date.now() / 1000) })));
return Promise.resolve();
}
if (cmd === "slsk_leave_room") { later(50, () => mockEmit("soulseek:room-left", { room: args?.room })); return Promise.resolve(); }
if (cmd === "slsk_say_room") {
later(120, () => mockEmit("soulseek:room-message", { room: args?.room, username: MOCK_USER, message: args?.message, ts: Math.floor(Date.now() / 1000) }));
return Promise.resolve();
}
if (cmd === "slsk_message_user") {
later(900, () => mockEmit("soulseek:private-message", { username: args?.username, message: "sure, sending", timestamp: Math.floor(Date.now() / 1000) }));
return Promise.resolve();
}
if (cmd === "slsk_browse_user") {
/* a share: two records and a folder of scans, in the browse DTO's shape */
const dir = (album, ext, bitrate) => {
const folder = "@@shared\\Music\\Daft Punk\\Daft Punk - 2001 - " + album + (ext === "flac" ? " [FLAC]" : " [MP3 " + bitrate + "]");
return {
name: folder,
files: TITLES.slice(0, album === "Discovery" ? 14 : 8).map((t, i) => {
const name = String(i + 1).padStart(2, "0") + " - " + t + "." + ext;
const duration = 200 + ((i * 37) % 180);
return { name, path: folder + "\\" + name, size: ext === "flac" ? duration * 110000 : duration * bitrate * 125, ext, bitrate: ext === "flac" ? null : bitrate, duration };
}),
};
};
return new Promise((r) => setTimeout(() => r([
dir("Discovery", "flac"),
dir("Homework", "mp3", 320),
{ name: "@@shared\\Music\\Scans", files: [{ name: "front.jpg", path: "@@shared\\Music\\Scans\\front.jpg", size: 1200000, ext: "jpg", bitrate: null, duration: null }] },
]), 600));
}
if (cmd === "slsk_user_info") {
const u = String(args?.username || "");
const peer = PEERS.find((x) => x.username === u);
return new Promise((r) => setTimeout(() => r({
username: u,
description: MOCK_ABOUT,
picture_png_base64: MOCK_PIC,
picture_note: null,
total_uploads: 18422,
queue_size: peer ? peer.queue : 0,
has_free_slot: peer ? peer.slot : true,
}), 450));
}
if (cmd === "slsk_user_interests") {
return new Promise((r) => setTimeout(() => r({ likes: MOCK_LIKES.slice(), hates: MOCK_HATES.slice() }), 400));
}
if (cmd === "overlap_list") return Promise.resolve([...overlaps.values()]);
if (cmd === "overlap_compute") {
/* the real one browses their whole share before it compares: the wait is the point */
const u = String(args?.username || "");
const o = {
username: u,
computed_at: Math.floor(Date.now() / 1000),
folders: 812,
audio_files: 9134,
their_albums: 74,
albums_in_common: pairsOf(OVERLAP_COMMON),
albums_in_common_count: OVERLAP_COMMON.length,
artists_in_common: ["Daft Punk", "Caribou"],
artists_in_common_count: 2,
gaps: pairsOf(OVERLAP_GAPS),
gaps_count: OVERLAP_GAPS.length,
score: OVERLAP_COMMON.length / 74,
};
return new Promise((r) => setTimeout(() => { overlaps.set(u.toLowerCase(), o); r(o); }, 1100));
}
if (cmd === "overlap_forget") { overlaps.delete(String(args?.username || "").toLowerCase()); return Promise.resolve(); }
if (cmd === "start_search") {
const t = ++token;
const q = String(args?.query || "");
later(300, () => mockEmit("soulseek:search-results", [peerResponse(t, PEERS[1], q)]));
later(700, () => mockEmit("soulseek:search-results", [peerResponse(t, PEERS[0], q), peerResponse(t, PEERS[3], q)]));
later(1300, () => mockEmit("soulseek:search-results", [peerResponse(t, PEERS[2], q), peerResponse(t, PEERS[4], q)]));
return Promise.resolve(t);
}
if (cmd === "stop_search") return Promise.resolve();
if (cmd === "slsk_interests_mine") return Promise.resolve({ likes: MY_INTERESTS.likes.slice(), hates: MY_INTERESTS.hates.slice() });
if (cmd === "slsk_interest_set") {
const thing = String(args?.thing || "").trim();
if (!thing) return Promise.reject(new Error("nothing to declare"));
const list = args?.like ? MY_INTERESTS.likes : MY_INTERESTS.hates;
const at = list.findIndex((t) => t.toLowerCase() === thing.toLowerCase());
if (args?.on && at < 0) list.push(thing);
if (!args?.on && at >= 0) list.splice(at, 1);
return new Promise((r) => setTimeout(() => r({ likes: MY_INTERESTS.likes.slice(), hates: MY_INTERESTS.hates.slice() }), 200));
}
if (cmd === "slsk_similar_users") {
/* the server ranks on the likes we declared: one more name per like */
const pool = ["hifi_hank", "dusty_crates", "mp3_marge", "ada", "cy", "bo"];
const n = MY_INTERESTS.likes.length;
return new Promise((r) => setTimeout(() => r(pool.slice(0, Math.min(pool.length, n + 1)).map((username, i) => ({ username, rating: Math.max(1, n - i) }))), 300));
}
if (cmd === "player_state") return Promise.resolve({ status: "idle", position: 0 });
if (cmd === "preload_local") return Promise.resolve();
if (cmd === "waveform") {
const n = args?.buckets || 900;
return new Promise((r) => setTimeout(() => r(Array.from({ length: n }, (_, i) => 0.2 + 0.75 * Math.abs(Math.sin(i * 0.23) * Math.cos(i * 0.017)))), 120));
}
if (cmd === "lyrics_get") return new Promise((r) => setTimeout(() => r(mockLyrics(args?.title)), 150));
if (cmd === "lyrics_search") return new Promise((r) => setTimeout(() => r([
{ id: 901, title: args?.title || "Track", artist: args?.artist || "?", album: "The Mock Sessions", synced: true, instrumental: false, durationSecs: 214 },
{ id: 902, title: (args?.title || "Track") + " (Live)", artist: args?.artist || "?", album: null, synced: false, instrumental: false, durationSecs: 233 },
]), 200));
if (cmd === "lyrics_select") return Promise.resolve({ ...mockLyrics(args?.title), source: "lrclib #" + args?.candidateId });
if (cmd === "lyrics_set_offset") return Promise.resolve();
if (cmd === "lyrics_save_text") {
const lines = String(args?.text || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
const timed = lines.map((l) => { const m = /^\[(\d+):(\d\d(?:\.\d+)?)\]\s*(.*)$/.exec(l); return m ? { time_ms: Math.round((+m[1] * 60 + +m[2]) * 1000), text: m[3] } : null; });
const synced = timed.every(Boolean);
return Promise.resolve({ status: synced ? "synced" : "plain", source: "pasted", synced: synced ? timed : [], plain: synced ? null : lines.join("\n"), offset_ms: 0, auto_offset_ms: 0, auto_sync_off: false });
}
if (cmd === "lyrics_set_auto_sync") { autoSyncOff = !args?.enabled; return Promise.resolve(); }
if (cmd === "lyrics_save_lines") {
/* the Rust drops the empty lines and sorts by time — a half-tapped
set is stored as the lines that HAVE one */
const lines = (args?.lines || []).filter((l) => String(l?.text || "").trim())
.map((l) => ({ time_ms: l.time_ms, text: l.text })).sort((a, b) => a.time_ms - b.time_ms);
if (!lines.length) return Promise.reject(new Error("Nothing to save"));
/* the Rust's payload_from does NOT carry the entry's auto-sync flag
back from a write — false here, faithfully, so the walk notices if
the store ever applies a save over the switch */
return Promise.resolve({ status: "synced", source: "user", synced: lines, plain: null, offset_ms: 0, auto_offset_ms: 0, auto_sync_off: false });
}
/* the vocal-reduced copy: a render takes a beat, and it is a new file */
if (cmd === "lyrics_prepare_instrumental") return new Promise((r) => setTimeout(() => r({ path: String(args?.path || "") + ".instrumental.wav", cached: false, durationSecs: 214 }), 400));
if (cmd === "lyrics_clear") return Promise.resolve();
if (cmd === "clear_play_history") return Promise.resolve();
if (cmd === "refresh_album_art") return new Promise((r) => setTimeout(() => r("C:\\mock\\cover.jpg"), 200));
if (cmd === "play_local") {
/* a file: the engine reports playing after a beat, then the position
from where it was asked to start */
if (xfer) { clearInterval(xfer.timer); xfer = null; }
clearInterval(posTimer);
const duration = args?.duration || 240;
let secs = Number(args?.startSecs) || 0;
later(300, () => {
mockEmit("soulseek:player", { kind: "playing" });
posTimer = setInterval(() => { secs += 0.5; mockEmit("soulseek:player", { kind: "position", secs, duration }); }, 500);
});
return Promise.resolve();
}
if (cmd === "play_stream") {
if (xfer) clearInterval(xfer.timer);
clearInterval(posTimer);
const total = args?.size || 30e6;
const duration = args?.duration || 240;
xfer = { received: 0, total, timer: null, path: args?.path };
cacheSizes.set(args?.path, total);
xfer.timer = setInterval(() => {
xfer.received = Math.min(total, xfer.received + total / 30);
const done = xfer.received >= total;
mockEmit("soulseek:stream", { received: Math.round(xfer.received), total, done, failed: null });
if (done) { clearInterval(xfer.timer); cacheReady.add(xfer.path); }
}, 400);
let secs = 0;
later(1200, () => {
mockEmit("soulseek:player", { kind: "playing" });
posTimer = setInterval(() => { secs += 0.5; mockEmit("soulseek:player", { kind: "position", secs, duration }); }, 500);
});
return Promise.resolve();
}
if (cmd === "stream_waveform") {
const coverage = xfer ? xfer.received / xfer.total : 0;
const n = Math.round((args?.buckets || 900) * coverage);
const peaks = Array.from({ length: n }, (_, i) => 0.25 + 0.7 * Math.abs(Math.sin(i * 0.37) * Math.cos(i * 0.011)));
return Promise.resolve({ coverage, peaks });
}
if (cmd === "stream_cache_len") return Promise.resolve(cacheReady.has(args?.path) ? (cacheSizes.get(args.path) || 0) : (xfer && xfer.path === args?.path ? Math.round(xfer.received) : 0));
if (cmd === "save_to_library") {
/* a byte-complete cache files at once; anything else goes to the download pipeline */
let organized = 0, queued = 0;
for (const f of args?.files || []) {
if (cacheReady.has(f.path)) { organized++; continue; }
queued++;
mockInvoke("download_file", { username: f.username, path: f.path, size: f.size });
}
if (organized) later(30, () => mockEmit("soulseek:library-changed", {}));
return Promise.resolve([organized, queued]);
}
if (cmd === "stream_embedded_art") return Promise.resolve(xfer && xfer.received > 200000 ? svgArt(200, "D") : null);
if (cmd === "record_play") return Promise.resolve(1);
if (cmd === "add_play_seconds") return Promise.resolve();
return undefined;
}