97 lines
4.4 KiB
JavaScript
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); },
|
|
};
|