cyp/app/js/mpd.js
2020-05-11 22:00:27 +02:00

188 lines
4.6 KiB
JavaScript

import * as parser from "./parser.js";
export default class MPD {
static async connect() {
let response = await fetch("/ticket", {method:"POST"});
let ticket = (await response.json()).ticket;
let ws = new WebSocket(createURL(ticket).href);
return new Promise((resolve, reject) => {
let mpd;
let initialCommand = {resolve: () => resolve(mpd), reject};
mpd = new this(ws, initialCommand);
});
}
constructor(/** @type WebSocket */ ws, initialCommand) {
this._ws = ws;
this._queue = [];
this._current = initialCommand;
this._canTerminateIdle = false;
ws.addEventListener("message", e => this._onMessage(e));
ws.addEventListener("close", e => this._onClose(e));
}
onClose(_e) {}
onChange(_changed) {}
command(cmd) {
if (cmd instanceof Array) { cmd = ["command_list_begin", ...cmd, "command_list_end"].join("\n"); }
return new Promise((resolve, reject) => {
this._queue.push({cmd, resolve, reject});
if (!this._current) {
this._advanceQueue();
} else if (this._canTerminateIdle) {
this._ws.send("noidle");
this._canTerminateIdle = false;
}
});
}
async status() {
let lines = await this.command("status");
return parser.linesToStruct(lines);
}
async currentSong() {
let lines = await this.command("currentsong");
return parser.linesToStruct(lines);
}
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] : []);
}
async listSongs(filter, window = null) {
let tokens = ["find", serializeFilter(filter)];
window && tokens.push("window", window.join(":"));
let lines = await this.command(tokens.join(" "));
return parser.songList(lines);
}
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 = "==") {
let tokens = ["("];
Object.entries(filter).forEach(([key, value], index) => {
index && tokens.push(" AND ");
tokens.push(`(${key} ${operator} "${escape(value)}")`);
});
tokens.push(")");
let filterStr = tokens.join("");
return `"${escape(filterStr)}"`;
}
function createURL(ticket) {
let url = new URL(location.href);
url.protocol = "ws";
url.hash = "";
url.searchParams.set("ticket", ticket);
return url;
}