Files

97 lines
4.4 KiB
JavaScript

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