// 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}`,
}));
}
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 === "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))));
}
if (cmd === "list_playlists") return Promise.resolve([]);
// 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)
: []);
}
return mockCatalog(cmd, args) ?? mockNet(cmd, args);
}
/* ── 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]];
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 === "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 === "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;
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({ download_dir: "C:\\mock\\Library" });
if (cmd === "set_settings") 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();
return undefined;
}
let token = 0;
let downloadSeq = 0;
const cacheReady = new Set(); // paths a preload has fully fetched
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 === "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 === "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 };
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);
}, 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(xfer ? Math.round(xfer.received) : 0);
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;
}