// 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 = `` + `` + `${label}`; 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 }))); /* Export. The whole file is built in Rust and written by whichever door the platform has — the desktop's save dialog, the phone's create-document sheet — so all the command ever answers is where it went: a path there, the document's name here. The sanitiser is the Rust one, character for character (and written as a code-point test rather than a range, so nothing in this file is a control byte). A dismissed sheet (null) and a refusal are armed from the walk with `__ssMock.answer` / `.fail`, which `weather()` answers first. */ if (cmd === "playlist_export") { const p = plOf(args); if (!p) return Promise.reject(new Error("Playlist not found")); const fmt = String(args?.format ?? ""); if (!["m3u8", "xspf", "pls", "csv", "json"].includes(fmt)) return Promise.reject(new Error("Unknown export format " + fmt)); const safe = [...String(p.name)].map((c) => (/[<>:"\/\\|?*]/.test(c) || c.codePointAt(0) < 0x20 ? "_" : c)).join(""); return new Promise((r) => setTimeout(() => r(safe + "." + fmt), 120)); } 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_probe") { const path = String(args?.path || ""); if (!path) return Promise.resolve(null); /* the desktop's dialog, dismissed */ /* the NAME decides the format, the way playlist_formats::detect_format does — the extension first, the stem as the draft's title. The path is only where the bytes were. */ const name = String(args?.name || path.split(/[\\/]/).filter(Boolean).pop() || "Playlist"); const ext = (name.split(".").pop() || "").toLowerCase(); const format = ext === "m3u" || ext === "m3u8" ? "m3u" : ext === "pls" ? "pls" : ext === "xspf" ? "xspf" : ext === "csv" ? "csv" : ext === "json" ? "soulstream" : "text"; /* the paste door's own three lines, so the verdicts are already proven: one in the library, one stream, one unresolved */ const entries = impParse("Daft Punk - One More Time\nAphex Twin - Track 1\nID - ID"); return new Promise((r) => setTimeout(() => r(impDraft(entries, name, format, name.replace(/\.[^.]*$/, ""))), 500)); } 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; } /* The walk's other device: a phone that has just installed the app. The mock is the whole backend on that leg, so it can answer as one — and the flag has to survive `location.reload()`, which is why it rides in the URL fragment as well as in localStorage: a file:// document's storage is not something the harness can lean on, and a fragment always comes back. */ const fresh = (() => { try { if (location.hash.indexOf("fresh") >= 0) return true; } catch { /* no document */ } try { return localStorage.getItem("mockFresh") === "1"; } catch { return false; } })(); 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; /* the weather, BEFORE any dispatch, so a plugin: command and a catalog command arm exactly the same way (see the doors below) */ const forecast = weather(cmd); if (forecast) return forecast; /* a device that has just been installed: an empty database. Not an error and not a refusal — `pipeline::init` scans the sandbox root at startup and honestly finds nothing in it, so every one of these answers with an empty list. Everything else below answers exactly as it always did: this is the same mock with nothing in it yet. */ if (fresh && (cmd === "library_albums" || cmd === "library_album_stats" || cmd === "recent_albums" || cmd === "followed_artists" || cmd === "list_playlists" || cmd === "library_track_index" || cmd === "library_track_search" || cmd === "contacts_meta_list" || cmd === "buddy_list")) return Promise.resolve([]); 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 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); /* ── the weather ──────────────────────────────────────────────────────── Thirty rounds of walk drove the happy path and nothing else: not one step ever made a command reject, and not one ever saw the client signed out. This mock IS the whole backend behind the browser walk, so it is the only place the unhappy paths can be driven from. Four doors, all of them checked in mockInvoke BEFORE any dispatch, so a `plugin:` command and a catalog command arm exactly the same way: __ssMock.fail(cmd, message) the next and every call to `cmd` rejects with that exact string; "" disarms it. __ssMock.answer(cmd, value) `cmd` resolves with `value` — an empty list, a null page — instead of its fixture, so the walk names what "empty" means rather than the mock guessing. __ssMock.forget(cmd) takes one armed answer away. __ssMock.offline(on) the client goes down: everything the backend funnels through with_client() answers "not connected to Soulseek", connect_stored / connect_and_login answer the server string, and soulseek:state goes out. off restores and re-connects. __ssMock.clear() all of it away, for the walk's last step. They reject with a PLAIN STRING, because that is what Tauri rejects with: an Error would arrive as "Error: …" and hide half of what say() has to cope with. All of it sits behind `enabled`, so a real build never sees it. */ const armedFail = new Map(); const armedAnswer = new Map(); let offlineNow = false; /* what src-tauri/src/commands.rs runs through with_client(), plus the two searches and a download, which check the connection themselves */ const needsClient = (cmd) => /^(slsk_|buddy_)/.test(cmd) || cmd === "start_search" || cmd === "stop_search" || cmd === "download_file"; function weather(cmd) { const f = armedFail.get(cmd); if (f != null) return Promise.reject(f); if (offlineNow) { if (cmd === "connect_stored" || cmd === "connect_and_login") { return Promise.reject("could not reach server: io: failed to lookup address information: no address associated with hostname"); } if (needsClient(cmd)) return Promise.reject("not connected to Soulseek"); } if (armedAnswer.has(cmd)) return Promise.resolve(armedAnswer.get(cmd)); return undefined; } if (enabled && typeof window !== "undefined") { window.__ssMock = { emit: mockEmit, fail(cmd, message) { if (message) armedFail.set(cmd, String(message)); else armedFail.delete(cmd); return true; }, answer(cmd, value) { armedAnswer.set(cmd, value); return true; }, forget(cmd) { armedAnswer.delete(cmd); return true; }, offline(on) { offlineNow = !!on; mockEmit("soulseek:state", offlineNow ? { kind: "disconnected" } : { kind: "connected", username: MOCK_USER }); return offlineNow; }, clear() { armedFail.clear(); armedAnswer.clear(); if (offlineNow) { offlineNow = false; mockEmit("soulseek:state", { kind: "connected", username: MOCK_USER }); } return true; }, }; } 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 })); /* An artist whose exhaustive page the mock deliberately keeps for four seconds. Everything on screen before then came from the two fast lanes — which is the only way the walk can tell that they ran. The titles are deliberately plain, so the classifier's title heuristics leave them alone and the tabs are exactly what the counts below say. */ const CAT_SLOW = { id: 415, name: "Halcyon Drift", picture: null, album_count: 7, fans: 48000 }; const SLOW_ALBUMS = [ { id: 4150, title: "Halcyon Drift", release_date: "2014-04-08", record_type: "album", track_count: 11 }, { id: 4151, title: "Second Light", release_date: "2016-09-23", record_type: "album", track_count: 10 }, { id: 4152, title: "Slow Ascent", release_date: "2019-02-15", record_type: "album", track_count: 12 }, { id: 4153, title: "First Signal", release_date: "2013-06-01", record_type: "single", track_count: 2 }, { id: 4154, title: "Fourth Wall", release_date: "2021-05-07", record_type: "album", track_count: 9 }, { id: 4155, title: "Long Winter", release_date: "2023-01-20", record_type: "album", track_count: 10 }, { id: 4156, title: "Open Water", release_date: "2024-08-30", record_type: "album", track_count: 11 }, ].map((a) => ({ ...a, artist: CAT_SLOW.name, cover: svgArt((a.id * 17) % 360, a.title[0]), edition_group: null, is_soundtrack: false })); /* A prolific artist, so the discography page's cap is WALKED rather than argued about: 140 records, four a year from 1985, each plainly titled for the same reason SLOW_ALBUMS' are — the classifier must leave every one of them on the Albums tab, or the counts the walk asserts stop meaning anything. Deliberately NOT in RELATED: a fifth card in the rail would move the artist page's own card census (cat-artist-tab counts 2 images and 2 placeholders there). The walk reaches it with a seed, so no search answers for it either. */ const CAT_BIG = { id: 416, name: "Ember Parade", picture: null, album_count: 140, fans: 91000 }; const BIG_ALBUMS = Array.from({ length: 140 }, (_, i) => ({ id: 41600 + i, title: "Field Notes " + (i + 1), artist: CAT_BIG.name, cover: svgArt((i * 37) % 360, "F"), release_date: (1985 + Math.floor(i / 4)) + "-" + String((i % 4) * 3 + 2).padStart(2, "0") + "-15", record_type: "album", track_count: 8 + (i % 5), edition_group: null, is_soundtrack: false, })); /* 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 : id === CAT_SLOW.id ? CAT_SLOW : id === CAT_BIG.id ? CAT_BIG : RELATED.find((r) => r.id === id); if (!a) return Promise.reject(new Error("catalog_artist: unknown artist")); const own = a === CAT_ARTIST; /* the exhaustive page, kept four seconds on purpose: whatever is on screen before then came from the fast lanes and nowhere else */ const slow = a === CAT_SLOW; 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 : slow ? SLOW_ALBUMS : a === CAT_BIG ? BIG_ALBUMS : [{ 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: [], }), slow ? 4000 : 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 : id === CAT_SLOW.id ? CAT_SLOW : 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); /* the slow artist answers the identity search at once — it is only the exhaustive page that is kept waiting */ if (/halcyon/.test(q)) return new Promise((r) => setTimeout(() => r({ artists: [CAT_SLOW], albums: [], tracks: [] }), 120)); 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)); /* the banner ahead of the page. A hero picked on this phone is answered from the override with no network at all, exactly as the Rust does — and the field really is snake_case there (no rename_all on it) */ if (cmd === "catalog_artist_hero_seed") { const id = Number(args?.artistId); if (heroes[id]) return Promise.resolve({ banner: heroes[id], banner_custom: true }); const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id); if (!a) return Promise.resolve(null); return new Promise((r) => setTimeout(() => r({ banner: svgArt(210, a.name[0]), banner_custom: false }), 200)); } /* the one-round content stage: the raw first page of releases, before the exhaustive pass classifies them. It goes through checkedArtistPayload, so `artist.id` must equal the argument or the wrapper throws */ if (cmd === "catalog_artist_overview") { const id = Number(args?.artistId); const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : id === CAT_BIG.id ? CAT_BIG : RELATED.find((r) => r.id === id); if (!a) return Promise.reject(new Error("catalog_artist_overview: unknown artist")); const albums = id === CAT_SLOW.id ? SLOW_ALBUMS.slice(0, 4) : id === CAT_ARTIST.id ? CAT_DISCO.slice(0, 4) : id === CAT_BIG.id ? BIG_ALBUMS.slice(0, 4) : []; return new Promise((r) => setTimeout(() => r({ artist: a, popular_tracks: [], albums }), 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: !fresh }); 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" }); /* a document that HAD to be copied, with the two names apart: the cache file is safeName()'s — every character outside the plugin's allowed set replaced, so the "#" is an underscore there — and the document's own is what the draft must be read and titled by. Same on purpose: if the name stopped travelling, the path's basename would still say m3u8 and only the title would quietly go wrong, which is the bug. */ if (cmd === "plugin:storage|pick_file") return Promise.resolve({ path: "C:\\mock\\picked\\Warehouse set _3.m3u8", name: "Warehouse set #3.m3u8", size: 2048, copied: true }); /* the downloads notice: the mock walk cannot see the shade, so the calls are simply answered — what the walk reads is __ss.xfn, the app's own record of having posted and cleared */ if (cmd === "plugin:transfer-notice|update") return Promise.resolve(); if (cmd === "plugin:transfer-notice|clear") return Promise.resolve(); 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, setup_done: 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" }, }; /* the fresh device's own config: the library root Android actually hands a fresh install (the app's private sandbox — real, writable, and invisible to every other app), nothing shared, and the flag the tour stamps still unwritten. Mutated rather than replaced, so `set_settings` keeps working and a folder picked during the walk genuinely changes what the notes read. */ if (fresh) Object.assign(mockSettings, { download_dir: "/data/user/0/com.soulstream.app/files/Library", setup_done: false, share_files: 0, shared_folders: [], }); 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(fresh ? { username: "", has_password: false } : { 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 }); /* Two the phone can reach that the mock never knew, found while mapping every api.js wrapper against it for this round: unmocked they fall through to the real IPC, which on a file:// page rejects — a console warning AND a toast, in a step that has nothing to do with either. The transport is answered the way the real core answers it, with an event and nothing else; `resume` cannot restart the position ticks (they live in the closure the play_* command opened), so the state is right and the position simply stands still. */ if (cmd === "player_control") { const act = String(args?.action || ""); if (act === "pause" || act === "stop") { clearInterval(posTimer); posTimer = null; } if (act === "pause") later(60, () => mockEmit("soulseek:player", { kind: "paused" })); if (act === "stop") later(60, () => mockEmit("soulseek:player", { kind: "stopped" })); if (act === "resume") later(60, () => mockEmit("soulseek:player", { kind: "playing" })); return Promise.resolve(); } if (cmd === "open_external") return Promise.resolve(); 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)); /* 214 s at a frame per 100 ms: three sung stretches with quiet between them, which is exactly what activeSegments is built to find — 150 s of active time, far over its 4 s floor, so the rough pass lays all three lines and the first one lands at 10 s */ if (cmd === "lyrics_vocal_activity") { const sung = [[10, 60], [80, 140], [160, 200]]; const frames = Array.from({ length: 2140 }, (_, i) => (sung.some(([a, b]) => i >= a * 10 && i < b * 10) ? 0.9 : 0.04)); return new Promise((r) => setTimeout(() => r({ frameMs: 100, frames }), 250)); } 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; }