// 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}`, })); } 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; }