// 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 =
``;
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));
/* 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: false,
};
}
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_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([]);
if (cmd === "album_tracks")
return new Promise((r) => setTimeout(() => r(tracksFor(args.albumId)), 60));
if (cmd === "album_art") {
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 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"];
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 },
];
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 }));
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: null, banners: [], banner_custom: false, 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);
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"));
return Promise.resolve({ album: a, tracks: a.id === 302127 ? CAT_TRACKS.map((t, i) => ({ ...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 === "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 === "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,
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
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 === "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 === "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 === "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_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;
}