// Everything the player keeps in the browser: settings, saved song mixes, stem playlists, // and offline downloads (IndexedDB). import { load, store, uuid } from "./util.js"; const KEY = "stemify-web"; export const S = Object.assign({ servers: [], // [{id, url, token, name, env?, sections: [{key, title, on}]}] stemUrl: "", // empty = same origin as this page stemToken: "", fmt: "opus160", volume: 0.85, speed: 1, shuffle: false, repeat: "off", mix: {}, // stem name -> {level, muted, solo}; the live mix keepMix: true, // carry the mix to the next track when it has no saved mix karaoke: false, karaokeMode: "off", softDb: -12, sorts: {}, // screen -> sort value stemFilter: "", // "" | "stems" (only songs with stems) | "nostems" precache: 5, // Cache Ahead: songs fetched ahead of the one playing (0-50) presetOrder: [], // preset ids in mixer order: "b:Name" built-in, "c:Name" custom hiddenPresets: [], // deleted built-ins (deleted custom presets are just gone) customPresets: [], // [{name, mix}] lockedPreset: "", // every song starts with this preset acapella: false, // spell "A Cappella" as "Acapella" bar: { speed: true, mixer: true, lyrics: true, queue: true }, // player bar buttons stations: [], // web radio: [{id, name, url}] theme: "amber", accent: "", css: "", }, load(KEY) || {}); if (!S.clientId) S.clientId = uuid(); if (S.fmt.startsWith("opus") && !new Audio().canPlayType('audio/ogg; codecs="opus"')) S.fmt = "flac"; // Before multiple servers there was one S.plex and one S.section. if (S.plex) { const p = S.plex; if (!p.env) S.servers.push({ id: "m" + Date.now().toString(36), url: p.url, token: p.token, name: p.name || p.url, sections: S.section ? [{ key: String(S.section), title: "Music", on: true }] : [] }); delete S.plex; delete S.section; } if ("stemsOnly" in S) { if (S.stemsOnly) S.stemFilter = "stems"; delete S.stemsOnly; } if (S.albumSort) { S.sorts.albums = S.albumSort; delete S.albumSort; } let timer; export function save() { clearTimeout(timer); timer = setTimeout(() => store(KEY, S), 300); } // ---- saved song mixes, keyed by the stem entry's key ----------------------------------- const MIX_KEY = KEY + "-mixes"; export const songMixes = load(MIX_KEY) || {}; export function saveSongMix(key, mix) { songMixes[key] = JSON.parse(JSON.stringify(mix)); store(MIX_KEY, songMixes); } export function forgetSongMix(key) { delete songMixes[key]; store(MIX_KEY, songMixes); } // ---- stem playlists: tracks plus an optional mix per item ------------------------------ const PL_KEY = KEY + "-playlists"; export const playlists = load(PL_KEY) || []; export const savePlaylists = () => store(PL_KEY, playlists); export function newPlaylist(title) { const p = { id: uuid(), title, items: [], created: Date.now(), updated: Date.now() }; playlists.unshift(p); savePlaylists(); return p; } export function addToPlaylist(p, tracks, mix) { for (const t of tracks) p.items.push({ uid: uuid(), track: t, mix: mix ? JSON.parse(JSON.stringify(mix)) : null }); p.updated = Date.now(); savePlaylists(); } // ---- offline downloads ----------------------------------------------------------------- // One record per track: {id, track, entry, fmt, size, peaks, lyrics, blobs: {name: Blob}, group}. // group is the album or playlist it was downloaded with: {title, href}. let dbp; function db() { return dbp ||= new Promise((res, rej) => { const r = indexedDB.open("stemify-web", 1); r.onupgradeneeded = () => r.result.createObjectStore("tracks", { keyPath: "id" }); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); } const req = r => new Promise((res, rej) => { r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); async function os(mode) { return (await db()).transaction("tracks", mode).objectStore("tracks"); } export const Offline = { ids: new Set(), async init() { try { this.ids = new Set(await req((await os("readonly")).getAllKeys())); } catch {} }, has: id => Offline.ids.has(id), async get(id) { return this.ids.has(id) ? req((await os("readonly")).get(id)) : null; }, async all() { return req((await os("readonly")).getAll()); }, async put(rec) { await req((await os("readwrite")).put(rec)); this.ids.add(rec.id); }, async remove(id) { await req((await os("readwrite")).delete(id)); this.ids.delete(id); }, };