objectified the mpd module, app now reconnects

This commit is contained in:
Ondřej Žára 2020-05-11 22:00:27 +02:00
parent 0b96e4da31
commit 9c66c7c1c1
4 changed files with 405 additions and 460 deletions

View file

@ -237,145 +237,170 @@ function pathContents(lines) {
return result; return result;
} }
let ws, app; class MPD {
let commandQueue = []; static async connect() {
let current; let response = await fetch("/ticket", {method:"POST"});
let canTerminateIdle = false; let ticket = (await response.json()).ticket;
function onError(e) { let ws = new WebSocket(createURL(ticket).href);
console.error(e);
current && current.reject(e);
ws = null; // fixme
}
function onClose(e) { return new Promise((resolve, reject) => {
console.warn(e); let mpd;
current && current.reject(e); let initialCommand = {resolve: () => resolve(mpd), reject};
ws = null; // fixme mpd = new this(ws, initialCommand);
} });
function onMessage(e) {
if (!current) { return; }
let lines = JSON.parse(e.data);
let last = lines.pop();
if (last.startsWith("OK")) {
current.resolve(lines);
} else {
console.warn(last);
current.reject(last);
} }
current = null;
if (commandQueue.length > 0) { constructor(/** @type WebSocket */ ws, initialCommand) {
advanceQueue(); this._ws = ws;
} else { this._queue = [];
setTimeout(idle, 0); // only after resolution callbacks this._current = initialCommand;
this._canTerminateIdle = false;
ws.addEventListener("message", e => this._onMessage(e));
ws.addEventListener("close", e => this._onClose(e));
} }
}
function advanceQueue(){ onClose(_e) {}
current = commandQueue.shift(); onChange(_changed) {}
ws.send(current.cmd);
}
async function idle() { command(cmd) {
if (current) { return; } if (cmd instanceof Array) { cmd = ["command_list_begin", ...cmd, "command_list_end"].join("\n"); }
canTerminateIdle = true; return new Promise((resolve, reject) => {
let lines = await command("idle stored_playlist playlist player options mixer"); this._queue.push({cmd, resolve, reject});
canTerminateIdle = false;
let changed = linesToStruct(lines).changed || [];
changed = [].concat(changed);
(changed.length > 0) && app.dispatchEvent(new CustomEvent("idle-change", {detail:changed}));
}
async function command(cmd) { if (!this._current) {
if (cmd instanceof Array) { cmd = ["command_list_begin", ...cmd, "command_list_end"].join("\n"); } this._advanceQueue();
} else if (this._canTerminateIdle) {
this._ws.send("noidle");
this._canTerminateIdle = false;
}
});
}
return new Promise((resolve, reject) => { async status() {
commandQueue.push({cmd, resolve, reject}); let lines = await this.command("status");
return linesToStruct(lines);
}
if (!current) { async currentSong() {
advanceQueue(); let lines = await this.command("currentsong");
} else if (canTerminateIdle) { return linesToStruct(lines);
ws.send("noidle"); }
canTerminateIdle = false;
async listQueue() {
let lines = await this.command("playlistinfo");
return songList(lines);
}
async listPlaylists() {
let lines = await this.command("listplaylists");
let parsed = linesToStruct(lines);
let list = parsed["playlist"];
if (!list) { return []; }
return (list instanceof Array ? list : [list]);
}
async listPath(path) {
let lines = await this.command(`lsinfo "${escape(path)}"`);
return pathContents(lines);
}
async listTags(tag, filter = {}) {
let tokens = ["list", tag];
if (Object.keys(filter).length) {
tokens.push(serializeFilter(filter));
let fakeGroup = Object.keys(filter)[0]; // FIXME hack for MPD < 0.21.6
tokens.push("group", fakeGroup);
} }
}); let lines = await this.command(tokens.join(" "));
} let parsed = linesToStruct(lines);
return [].concat(tag in parsed ? parsed[tag] : []);
async function status() {
let lines = await command("status");
return linesToStruct(lines);
}
async function currentSong() {
let lines = await command("currentsong");
return linesToStruct(lines);
}
async function listQueue() {
let lines = await command("playlistinfo");
return songList(lines);
}
async function listPlaylists() {
let lines = await command("listplaylists");
let parsed = linesToStruct(lines);
let list = parsed["playlist"];
if (!list) { return []; }
return (list instanceof Array ? list : [list]);
}
async function listPath(path) {
let lines = await command(`lsinfo "${escape(path)}"`);
return pathContents(lines);
}
async function listTags(tag, filter = {}) {
let tokens = ["list", tag];
if (Object.keys(filter).length) {
tokens.push(serializeFilter(filter));
let fakeGroup = Object.keys(filter)[0]; // FIXME hack for MPD < 0.21.6
tokens.push("group", fakeGroup);
} }
let lines = await command(tokens.join(" "));
let parsed = linesToStruct(lines);
return [].concat(tag in parsed ? parsed[tag] : []);
}
async function listSongs(filter, window = null) { async listSongs(filter, window = null) {
let tokens = ["find", serializeFilter(filter)]; let tokens = ["find", serializeFilter(filter)];
window && tokens.push("window", window.join(":")); window && tokens.push("window", window.join(":"));
let lines = await command(tokens.join(" ")); let lines = await this.command(tokens.join(" "));
return songList(lines); return songList(lines);
}
async function searchSongs(filter) {
let tokens = ["search", serializeFilter(filter, "contains")];
let lines = await command(tokens.join(" "));
return songList(lines);
}
async function albumArt(songUrl) {
let data = [];
let offset = 0;
let params = ["albumart", `"${escape(songUrl)}"`, offset];
while (1) {
params[2] = offset;
try {
let lines = await command(params.join(" "));
data = data.concat(lines[2]);
let metadata = linesToStruct(lines.slice(0, 2));
if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]);
} catch (e) { return null; }
} }
return null;
async searchSongs(filter) {
let tokens = ["search", serializeFilter(filter, "contains")];
let lines = await this.command(tokens.join(" "));
return songList(lines);
}
async albumArt(songUrl) {
let data = [];
let offset = 0;
let params = ["albumart", `"${escape(songUrl)}"`, offset];
while (1) {
params[2] = offset;
try {
let lines = await this.command(params.join(" "));
data = data.concat(lines[2]);
let metadata = linesToStruct(lines.slice(0, 2));
if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]);
} catch (e) { return null; }
}
return null;
}
escape(...args) { return escape(...args); }
_onMessage(e) {
if (!this._current) { return; }
let lines = JSON.parse(e.data);
let last = lines.pop();
if (last.startsWith("OK")) {
this._current.resolve(lines);
} else {
console.warn(last);
this._current.reject(last);
}
this._current = null;
if (this._queue.length > 0) {
this._advanceQueue();
} else {
setTimeout(() => this._idle(), 0); // only after resolution callbacks
}
}
_onClose(e) {
console.warn(e);
this._current && this._current.reject(e);
this._ws = null;
this.onClose(e);
}
_advanceQueue() {
this._current = this._queue.shift();
this._ws.send(this._current.cmd);
}
async _idle() {
if (this._current) { return; }
this._canTerminateIdle = true;
let lines = await this.command("idle stored_playlist playlist player options mixer");
this._canTerminateIdle = false;
let changed = linesToStruct(lines).changed || [];
changed = [].concat(changed);
(changed.length > 0) && this.onChange(changed);
}
}
function escape(str) {
return str.replace(/(['"\\])/g, "\\$1");
} }
function serializeFilter(filter, operator = "==") { function serializeFilter(filter, operator = "==") {
@ -390,141 +415,14 @@ function serializeFilter(filter, operator = "==") {
return `"${escape(filterStr)}"`; return `"${escape(filterStr)}"`;
} }
function escape(str) { function createURL(ticket) {
return str.replace(/(['"\\])/g, "\\$1"); let url = new URL(location.href);
url.protocol = "ws";
url.hash = "";
url.searchParams.set("ticket", ticket);
return url;
} }
async function init(a) {
app = a;
let response = await fetch("/ticket", {method:"POST"});
let ticket = (await response.json()).ticket;
return new Promise((resolve, reject) => {
current = {resolve, reject};
try {
let url = new URL(location.href);
url.protocol = "ws";
url.hash = "";
url.searchParams.set("ticket", ticket);
ws = new WebSocket(url.href);
} catch (e) { reject(e); }
ws.addEventListener("error", onError);
ws.addEventListener("message", onMessage);
ws.addEventListener("close", onClose);
});
}
var mpd = /*#__PURE__*/Object.freeze({
__proto__: null,
command: command,
status: status,
currentSong: currentSong,
listQueue: listQueue,
listPlaylists: listPlaylists,
listPath: listPath,
listTags: listTags,
listSongs: listSongs,
searchSongs: searchSongs,
albumArt: albumArt,
serializeFilter: serializeFilter,
escape: escape,
init: init
});
function command$1(cmd) {
console.warn(`mpd-mock does not know "${cmd}"`);
}
function status$1() {
return {
volume: 50,
elapsed: 10,
duration: 70,
state: "play"
}
}
function currentSong$1() {
return {
duration: 70,
file: "name.mp3",
Title: "Title of song",
Artist: "Artist of song",
Album: "Album of song",
Track: "6",
Id: 2
}
}
function listQueue$1() {
return [
{Id:1, Track:"5", Title:"Title 1", Artist:"AAA", Album:"BBB", duration:30, file:"a.mp3"},
currentSong$1(),
{Id:3, Track:"7", Title:"Title 3", Artist:"CCC", Album:"DDD", duration:230, file:"c.mp3"},
];
}
function listPlaylists$1() {
return [
"Playlist 1",
"Playlist 2",
"Playlist 3"
];
}
function listPath$1(path) {
return {
"directory": [
{"directory": "Dir 1"},
{"directory": "Dir 2"},
{"directory": "Dir 3"}
],
"file": [
{"file": "File 1"},
{"file": "File 2"},
{"file": "File 3"}
]
}
}
function listTags$1(tag, filter = null) {
switch (tag) {
case "AlbumArtist": return ["Artist 1", "Artist 2", "Artist 3"];
case "Album": return ["Album 1", "Album 2", "Album 3"];
}
}
function listSongs$1(filter, window = null) {
return listQueue$1();
}
function searchSongs$1(filter) {
return listQueue$1();
}
function albumArt$1(songUrl) {
return new Promise(resolve => setTimeout(resolve, 1000));
}
function init$1() {}
var mpdMock = /*#__PURE__*/Object.freeze({
__proto__: null,
command: command$1,
status: status$1,
currentSong: currentSong$1,
listQueue: listQueue$1,
listPlaylists: listPlaylists$1,
listPath: listPath$1,
listTags: listTags$1,
listSongs: listSongs$1,
searchSongs: searchSongs$1,
albumArt: albumArt$1,
init: init$1
});
let ICONS={}; let ICONS={};
ICONS["library-music"] = `<svg viewBox="0 0 24 24"> ICONS["library-music"] = `<svg viewBox="0 0 24 24">
<path d="M4,6H2V20A2,2 0 0,0 4,22H18V20H4M18,7H15V12.5A2.5,2.5 0 0,1 12.5,15A2.5,2.5 0 0,1 10,12.5A2.5,2.5 0 0,1 12.5,10C13.07,10 13.58,10.19 14,10.5V5H18M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2Z"/> <path d="M4,6H2V20A2,2 0 0,0 4,22H18V20H4M18,7H15V12.5A2.5,2.5 0 0,1 12.5,15A2.5,2.5 0 0,1 10,12.5A2.5,2.5 0 0,1 12.5,10C13.07,10 13.58,10.19 14,10.5V5H18M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2Z"/>
@ -673,43 +571,22 @@ function initIcons() {
}); });
} }
async function initMpd(app) {
try {
await init(app);
return mpd;
} catch (e) {
return mpdMock;
}
}
class App extends HTMLElement { class App extends HTMLElement {
static get observedAttributes() { return ["component"]; } static get observedAttributes() { return ["component"]; }
constructor() { constructor() {
super(); super();
initIcons(); initIcons();
} }
async connectedCallback() { async connectedCallback() {
this.mpd = await initMpd(this); await waitForChildren(this);
const children = Array.from(this.querySelectorAll("*")); window.addEventListener("hashchange", e => this._onHashChange());
const names = children.map(node => node.nodeName.toLowerCase()) this._onHashChange();
.filter(name => name.startsWith("cyp-"));
const unique = new Set(names);
const promises = [...unique].map(name => customElements.whenDefined(name));
await Promise.all(promises);
await this._connect();
this.dispatchEvent(new CustomEvent("load")); this.dispatchEvent(new CustomEvent("load"));
const onHashChange = () => {
const component = location.hash.substring(1) || "queue";
if (component != this.component) { this.component = component; }
};
window.addEventListener("hashchange", onHashChange);
onHashChange();
} }
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
@ -724,10 +601,49 @@ class App extends HTMLElement {
get component() { return this.getAttribute("component"); } get component() { return this.getAttribute("component"); }
set component(component) { this.setAttribute("component", component); } set component(component) { this.setAttribute("component", component); }
_onHashChange() {
const component = location.hash.substring(1) || "queue";
if (component != this.component) { this.component = component; }
}
_onChange(changed) { this.dispatchEvent(new CustomEvent("idle-change", {detail:changed})); }
_onClose(e) {
setTimeout(() => this._connect(), 3000);
}
async _connect() {
const attempts = 3;
for (let i=0;i<attempts;i++) {
try {
let mpd = await MPD.connect();
mpd.onChange = changed => this._onChange(changed);
mpd.onClose = e => this._onClose(e);
this.mpd = mpd;
return;
} catch (e) {
await sleep(500);
}
}
alert(`Failed to connect to MPD after ${attempts} attempts. Please reload the page to try again.`);
}
} }
customElements.define("cyp-app", App); customElements.define("cyp-app", App);
function sleep(ms) { return new Promise(resolve =>setTimeout(resolve, ms)); }
function waitForChildren(app) {
const children = Array.from(app.querySelectorAll("*"));
const names = children.map(node => node.nodeName.toLowerCase())
.filter(name => name.startsWith("cyp-"));
const unique = new Set(names);
const promises = [...unique].map(name => customElements.whenDefined(name));
return Promise.all(promises);
}
const TAGS = ["cyp-song", "cyp-tag", "cyp-path"]; const TAGS = ["cyp-song", "cyp-tag", "cyp-path"];
class Selection { class Selection {
@ -832,19 +748,21 @@ class Component extends HTMLElement {
} }
class Menu extends Component { class Menu extends Component {
_onAppLoad() { connectedCallback() {
/** @type HTMLElement[] */ /** @type HTMLElement[] */
this._tabs = Array.from(this.querySelectorAll("[data-for]")); this._tabs = Array.from(this.querySelectorAll("[data-for]"));
this._tabs.forEach(tab => { this._tabs.forEach(tab => {
tab.addEventListener("click", _ => this._app.setAttribute("component", tab.dataset.for)); tab.addEventListener("click", _ => this._app.setAttribute("component", tab.dataset.for));
}); });
}
_onAppLoad() {
this._app.addEventListener("queue-length-change", e => { this._app.addEventListener("queue-length-change", e => {
this.querySelector(".queue-length").textContent = `(${e.detail})`; this.querySelector(".queue-length").textContent = `(${e.detail})`;
}); });
} }
_onComponentChange(component) { _onComponentChange(component) {
this._tabs.forEach(tab => { this._tabs.forEach(tab => {
tab.classList.toggle("active", tab.dataset.for == component); tab.classList.toggle("active", tab.dataset.for == component);

View file

@ -1,5 +1,4 @@
import * as mpd from "../mpd.js"; import MPD from "../mpd.js";
import * as mpdMock from "../mpd-mock.js";
import * as html from "../html.js"; import * as html from "../html.js";
function initIcons() { function initIcons() {
@ -11,43 +10,22 @@ function initIcons() {
}); });
} }
async function initMpd(app) {
try {
await mpd.init(app);
return mpd;
} catch (e) {
return mpdMock;
}
}
class App extends HTMLElement { class App extends HTMLElement {
static get observedAttributes() { return ["component"]; } static get observedAttributes() { return ["component"]; }
constructor() { constructor() {
super(); super();
initIcons(); initIcons();
} }
async connectedCallback() { async connectedCallback() {
this.mpd = await initMpd(this); await waitForChildren(this);
const children = Array.from(this.querySelectorAll("*")); window.addEventListener("hashchange", e => this._onHashChange());
const names = children.map(node => node.nodeName.toLowerCase()) this._onHashChange();
.filter(name => name.startsWith("cyp-"));
const unique = new Set(names);
const promises = [...unique].map(name => customElements.whenDefined(name));
await Promise.all(promises);
await this._connect();
this.dispatchEvent(new CustomEvent("load")); this.dispatchEvent(new CustomEvent("load"));
const onHashChange = () => {
const component = location.hash.substring(1) || "queue";
if (component != this.component) { this.component = component; }
}
window.addEventListener("hashchange", onHashChange);
onHashChange();
} }
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
@ -62,6 +40,45 @@ class App extends HTMLElement {
get component() { return this.getAttribute("component"); } get component() { return this.getAttribute("component"); }
set component(component) { this.setAttribute("component", component); } set component(component) { this.setAttribute("component", component); }
_onHashChange() {
const component = location.hash.substring(1) || "queue";
if (component != this.component) { this.component = component; }
}
_onChange(changed) { this.dispatchEvent(new CustomEvent("idle-change", {detail:changed})); }
_onClose(e) {
setTimeout(() => this._connect(), 3000);
}
async _connect() {
const attempts = 3;
for (let i=0;i<attempts;i++) {
try {
let mpd = await MPD.connect();
mpd.onChange = changed => this._onChange(changed);
mpd.onClose = e => this._onClose(e);
this.mpd = mpd;
return;
} catch (e) {
await sleep(500);
}
}
alert(`Failed to connect to MPD after ${attempts} attempts. Please reload the page to try again.`);
}
} }
customElements.define("cyp-app", App); customElements.define("cyp-app", App);
function sleep(ms) { return new Promise(resolve =>setTimeout(resolve, ms)); }
function waitForChildren(app) {
const children = Array.from(app.querySelectorAll("*"));
const names = children.map(node => node.nodeName.toLowerCase())
.filter(name => name.startsWith("cyp-"));
const unique = new Set(names);
const promises = [...unique].map(name => customElements.whenDefined(name));
return Promise.all(promises);
}

View file

@ -1,19 +1,21 @@
import Component from "../component.js"; import Component from "../component.js";
class Menu extends Component { class Menu extends Component {
_onAppLoad() { connectedCallback() {
/** @type HTMLElement[] */ /** @type HTMLElement[] */
this._tabs = Array.from(this.querySelectorAll("[data-for]")); this._tabs = Array.from(this.querySelectorAll("[data-for]"));
this._tabs.forEach(tab => { this._tabs.forEach(tab => {
tab.addEventListener("click", _ => this._app.setAttribute("component", tab.dataset.for)); tab.addEventListener("click", _ => this._app.setAttribute("component", tab.dataset.for));
}); });
}
_onAppLoad() {
this._app.addEventListener("queue-length-change", e => { this._app.addEventListener("queue-length-change", e => {
this.querySelector(".queue-length").textContent = `(${e.detail})`; this.querySelector(".queue-length").textContent = `(${e.detail})`;
}); });
} }
_onComponentChange(component) { _onComponentChange(component) {
this._tabs.forEach(tab => { this._tabs.forEach(tab => {
tab.classList.toggle("active", tab.dataset.for == component); tab.classList.toggle("active", tab.dataset.for == component);

View file

@ -1,144 +1,170 @@
import * as parser from "./parser.js"; import * as parser from "./parser.js";
let ws, app;
let commandQueue = [];
let current;
let canTerminateIdle = false;
function onError(e) { export default class MPD {
console.error(e); static async connect() {
current && current.reject(e); let response = await fetch("/ticket", {method:"POST"});
ws = null; // fixme let ticket = (await response.json()).ticket;
}
function onClose(e) { let ws = new WebSocket(createURL(ticket).href);
console.warn(e);
current && current.reject(e);
ws = null; // fixme
}
function onMessage(e) { return new Promise((resolve, reject) => {
if (!current) { return; } let mpd;
let initialCommand = {resolve: () => resolve(mpd), reject};
let lines = JSON.parse(e.data); mpd = new this(ws, initialCommand);
let last = lines.pop(); });
if (last.startsWith("OK")) {
current.resolve(lines);
} else {
console.warn(last);
current.reject(last);
} }
current = null;
if (commandQueue.length > 0) { constructor(/** @type WebSocket */ ws, initialCommand) {
advanceQueue(); this._ws = ws;
} else { this._queue = [];
setTimeout(idle, 0); // only after resolution callbacks this._current = initialCommand;
this._canTerminateIdle = false;
ws.addEventListener("message", e => this._onMessage(e));
ws.addEventListener("close", e => this._onClose(e));
} }
}
function advanceQueue(){ onClose(_e) {}
current = commandQueue.shift(); onChange(_changed) {}
ws.send(current.cmd);
}
async function idle() { command(cmd) {
if (current) { return; } if (cmd instanceof Array) { cmd = ["command_list_begin", ...cmd, "command_list_end"].join("\n"); }
canTerminateIdle = true; return new Promise((resolve, reject) => {
let lines = await command("idle stored_playlist playlist player options mixer"); this._queue.push({cmd, resolve, reject});
canTerminateIdle = false;
let changed = parser.linesToStruct(lines).changed || [];
changed = [].concat(changed);
(changed.length > 0) && app.dispatchEvent(new CustomEvent("idle-change", {detail:changed}));
}
export async function command(cmd) { if (!this._current) {
if (cmd instanceof Array) { cmd = ["command_list_begin", ...cmd, "command_list_end"].join("\n"); } this._advanceQueue();
} else if (this._canTerminateIdle) {
this._ws.send("noidle");
this._canTerminateIdle = false;
}
});
}
return new Promise((resolve, reject) => { async status() {
commandQueue.push({cmd, resolve, reject}); let lines = await this.command("status");
return parser.linesToStruct(lines);
}
if (!current) { async currentSong() {
advanceQueue(); let lines = await this.command("currentsong");
} else if (canTerminateIdle) { return parser.linesToStruct(lines);
ws.send("noidle"); }
canTerminateIdle = false;
async listQueue() {
let lines = await this.command("playlistinfo");
return parser.songList(lines);
}
async listPlaylists() {
let lines = await this.command("listplaylists");
let parsed = parser.linesToStruct(lines);
let list = parsed["playlist"];
if (!list) { return []; }
return (list instanceof Array ? list : [list]);
}
async listPath(path) {
let lines = await this.command(`lsinfo "${escape(path)}"`);
return parser.pathContents(lines);
}
async listTags(tag, filter = {}) {
let tokens = ["list", tag];
if (Object.keys(filter).length) {
tokens.push(serializeFilter(filter));
let fakeGroup = Object.keys(filter)[0]; // FIXME hack for MPD < 0.21.6
tokens.push("group", fakeGroup);
} }
}); let lines = await this.command(tokens.join(" "));
} let parsed = parser.linesToStruct(lines);
return [].concat(tag in parsed ? parsed[tag] : []);
export async function status() {
let lines = await command("status");
return parser.linesToStruct(lines);
}
export async function currentSong() {
let lines = await command("currentsong");
return parser.linesToStruct(lines);
}
export async function listQueue() {
let lines = await command("playlistinfo");
return parser.songList(lines);
}
export async function listPlaylists() {
let lines = await command("listplaylists");
let parsed = parser.linesToStruct(lines);
let list = parsed["playlist"];
if (!list) { return []; }
return (list instanceof Array ? list : [list]);
}
export async function listPath(path) {
let lines = await command(`lsinfo "${escape(path)}"`);
return parser.pathContents(lines);
}
export async function listTags(tag, filter = {}) {
let tokens = ["list", tag];
if (Object.keys(filter).length) {
tokens.push(serializeFilter(filter));
let fakeGroup = Object.keys(filter)[0]; // FIXME hack for MPD < 0.21.6
tokens.push("group", fakeGroup);
} }
let lines = await command(tokens.join(" "));
let parsed = parser.linesToStruct(lines);
return [].concat(tag in parsed ? parsed[tag] : []);
}
export async function listSongs(filter, window = null) { async listSongs(filter, window = null) {
let tokens = ["find", serializeFilter(filter)]; let tokens = ["find", serializeFilter(filter)];
window && tokens.push("window", window.join(":")); window && tokens.push("window", window.join(":"));
let lines = await command(tokens.join(" ")); let lines = await this.command(tokens.join(" "));
return parser.songList(lines); return parser.songList(lines);
}
export async function searchSongs(filter) {
let tokens = ["search", serializeFilter(filter, "contains")];
let lines = await command(tokens.join(" "));
return parser.songList(lines);
}
export async function albumArt(songUrl) {
let data = [];
let offset = 0;
let params = ["albumart", `"${escape(songUrl)}"`, offset];
while (1) {
params[2] = offset;
try {
let lines = await command(params.join(" "));
data = data.concat(lines[2]);
let metadata = parser.linesToStruct(lines.slice(0, 2));
if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]);
} catch (e) { return null; }
} }
return null;
async searchSongs(filter) {
let tokens = ["search", serializeFilter(filter, "contains")];
let lines = await this.command(tokens.join(" "));
return parser.songList(lines);
}
async albumArt(songUrl) {
let data = [];
let offset = 0;
let params = ["albumart", `"${escape(songUrl)}"`, offset];
while (1) {
params[2] = offset;
try {
let lines = await this.command(params.join(" "));
data = data.concat(lines[2]);
let metadata = parser.linesToStruct(lines.slice(0, 2));
if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]);
} catch (e) { return null; }
}
return null;
}
escape(...args) { return escape(...args); }
_onMessage(e) {
if (!this._current) { return; }
let lines = JSON.parse(e.data);
let last = lines.pop();
if (last.startsWith("OK")) {
this._current.resolve(lines);
} else {
console.warn(last);
this._current.reject(last);
}
this._current = null;
if (this._queue.length > 0) {
this._advanceQueue();
} else {
setTimeout(() => this._idle(), 0); // only after resolution callbacks
}
}
_onClose(e) {
console.warn(e);
this._current && this._current.reject(e);
this._ws = null;
this.onClose(e);
}
_advanceQueue() {
this._current = this._queue.shift();
this._ws.send(this._current.cmd);
}
async _idle() {
if (this._current) { return; }
this._canTerminateIdle = true;
let lines = await this.command("idle stored_playlist playlist player options mixer");
this._canTerminateIdle = false;
let changed = parser.linesToStruct(lines).changed || [];
changed = [].concat(changed);
(changed.length > 0) && this.onChange(changed);
}
}
export function escape(str) {
return str.replace(/(['"\\])/g, "\\$1");
} }
export function serializeFilter(filter, operator = "==") { export function serializeFilter(filter, operator = "==") {
@ -153,28 +179,10 @@ export function serializeFilter(filter, operator = "==") {
return `"${escape(filterStr)}"`; return `"${escape(filterStr)}"`;
} }
export function escape(str) { function createURL(ticket) {
return str.replace(/(['"\\])/g, "\\$1"); let url = new URL(location.href);
} url.protocol = "ws";
url.hash = "";
export async function init(a) { url.searchParams.set("ticket", ticket);
app = a; return url;
let response = await fetch("/ticket", {method:"POST"});
let ticket = (await response.json()).ticket;
return new Promise((resolve, reject) => {
current = {resolve, reject};
try {
let url = new URL(location.href);
url.protocol = "ws";
url.hash = "";
url.searchParams.set("ticket", ticket);
ws = new WebSocket(url.href);
} catch (e) { reject(e); }
ws.addEventListener("error", onError);
ws.addEventListener("message", onMessage);
ws.addEventListener("close", onClose);
});
} }