2019-03-20 03:45:23 +08:00
|
|
|
import * as parser from "./parser.js";
|
|
|
|
|
2020-05-08 02:06:43 +08:00
|
|
|
let ws, app;
|
2019-03-20 03:45:23 +08:00
|
|
|
let commandQueue = [];
|
2019-03-20 23:20:17 +08:00
|
|
|
let current;
|
2020-05-06 05:03:36 +08:00
|
|
|
let canTerminateIdle = false;
|
2019-03-20 03:45:23 +08:00
|
|
|
|
2020-05-08 00:45:33 +08:00
|
|
|
function onError(e) {
|
|
|
|
console.error(e);
|
|
|
|
current && current.reject(e);
|
|
|
|
ws = null; // fixme
|
|
|
|
}
|
|
|
|
|
|
|
|
function onClose(e) {
|
|
|
|
console.warn(e);
|
|
|
|
current && current.reject(e);
|
|
|
|
ws = null; // fixme
|
|
|
|
}
|
|
|
|
|
2019-03-20 03:45:23 +08:00
|
|
|
function onMessage(e) {
|
2020-05-06 05:03:36 +08:00
|
|
|
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);
|
2019-03-20 03:45:23 +08:00
|
|
|
}
|
2020-05-06 05:03:36 +08:00
|
|
|
current = null;
|
2019-03-20 03:45:23 +08:00
|
|
|
|
2020-05-08 00:45:33 +08:00
|
|
|
if (commandQueue.length > 0) {
|
|
|
|
advanceQueue();
|
|
|
|
} else {
|
|
|
|
setTimeout(idle, 0); // only after resolution callbacks
|
|
|
|
}
|
2019-03-20 03:45:23 +08:00
|
|
|
}
|
|
|
|
|
2020-05-08 00:45:33 +08:00
|
|
|
function advanceQueue(){
|
|
|
|
current = commandQueue.shift();
|
|
|
|
ws.send(current.cmd);
|
2019-03-25 22:49:23 +08:00
|
|
|
}
|
|
|
|
|
2020-05-06 05:03:36 +08:00
|
|
|
async function idle() {
|
2020-05-08 00:45:33 +08:00
|
|
|
if (current) { return; }
|
|
|
|
|
2020-05-06 05:03:36 +08:00
|
|
|
canTerminateIdle = true;
|
2020-05-09 16:55:41 +08:00
|
|
|
let lines = await command("idle stored_playlist playlist player options mixer");
|
2020-05-06 05:03:36 +08:00
|
|
|
canTerminateIdle = false;
|
|
|
|
let changed = parser.linesToStruct(lines).changed || [];
|
|
|
|
changed = [].concat(changed);
|
2020-05-08 02:06:43 +08:00
|
|
|
(changed.length > 0) && app.dispatchEvent(new CustomEvent("idle-change", {detail:changed}));
|
2019-03-20 05:56:39 +08:00
|
|
|
}
|
2019-03-20 03:45:23 +08:00
|
|
|
|
2019-03-20 05:56:39 +08:00
|
|
|
export async function command(cmd) {
|
2019-03-20 23:20:17 +08:00
|
|
|
if (cmd instanceof Array) { cmd = ["command_list_begin", ...cmd, "command_list_end"].join("\n"); }
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
commandQueue.push({cmd, resolve, reject});
|
2020-05-08 00:45:33 +08:00
|
|
|
|
|
|
|
if (!current) {
|
|
|
|
advanceQueue();
|
|
|
|
} else if (canTerminateIdle) {
|
|
|
|
ws.send("noidle");
|
|
|
|
canTerminateIdle = false;
|
|
|
|
}
|
2019-03-20 05:56:39 +08:00
|
|
|
});
|
2019-03-20 03:45:23 +08:00
|
|
|
}
|
|
|
|
|
2020-05-06 05:03:36 +08:00
|
|
|
export async function status() {
|
|
|
|
let lines = await command("status");
|
|
|
|
return parser.linesToStruct(lines);
|
2019-03-20 23:20:17 +08:00
|
|
|
}
|
|
|
|
|
2020-05-06 05:03:36 +08:00
|
|
|
export async function currentSong() {
|
|
|
|
let lines = await command("currentsong");
|
|
|
|
return parser.linesToStruct(lines);
|
2019-03-20 03:45:23 +08:00
|
|
|
}
|
|
|
|
|
2019-03-22 22:35:04 +08:00
|
|
|
export async function listQueue() {
|
|
|
|
let lines = await command("playlistinfo");
|
|
|
|
return parser.songList(lines);
|
|
|
|
}
|
|
|
|
|
2019-03-28 22:23:28 +08:00
|
|
|
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]);
|
|
|
|
}
|
|
|
|
|
2019-03-25 22:49:23 +08:00
|
|
|
export async function listPath(path) {
|
|
|
|
let lines = await command(`lsinfo "${escape(path)}"`);
|
|
|
|
return parser.pathContents(lines);
|
|
|
|
}
|
|
|
|
|
2020-03-13 17:36:13 +08:00
|
|
|
export async function listTags(tag, filter = {}) {
|
2019-03-25 22:49:23 +08:00
|
|
|
let tokens = ["list", tag];
|
2020-03-13 17:36:13 +08:00
|
|
|
if (Object.keys(filter).length) {
|
2019-03-25 22:49:23 +08:00
|
|
|
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);
|
2019-03-30 19:57:01 +08:00
|
|
|
return [].concat(tag in parsed ? parsed[tag] : []);
|
2019-03-25 22:49:23 +08:00
|
|
|
}
|
|
|
|
|
2019-03-30 19:57:01 +08:00
|
|
|
export async function listSongs(filter, window = null) {
|
2020-03-14 23:15:14 +08:00
|
|
|
let tokens = ["find", serializeFilter(filter)];
|
2020-03-14 06:01:16 +08:00
|
|
|
window && tokens.push("window", window.join(":"));
|
2019-03-25 22:49:23 +08:00
|
|
|
let lines = await command(tokens.join(" "));
|
|
|
|
return parser.songList(lines);
|
|
|
|
}
|
|
|
|
|
2020-03-14 06:01:16 +08:00
|
|
|
export async function searchSongs(filter) {
|
2020-03-14 23:15:14 +08:00
|
|
|
let tokens = ["search", serializeFilter(filter, "contains")];
|
2020-03-14 06:01:16 +08:00
|
|
|
let lines = await command(tokens.join(" "));
|
|
|
|
return parser.songList(lines);
|
|
|
|
}
|
|
|
|
|
2019-03-25 22:49:23 +08:00
|
|
|
export async function albumArt(songUrl) {
|
|
|
|
let data = [];
|
|
|
|
let offset = 0;
|
2020-03-14 06:01:16 +08:00
|
|
|
let params = ["albumart", `"${escape(songUrl)}"`, offset];
|
|
|
|
|
2019-03-25 22:49:23 +08:00
|
|
|
while (1) {
|
2020-03-14 06:01:16 +08:00
|
|
|
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; }
|
2019-03-25 22:49:23 +08:00
|
|
|
}
|
2019-03-29 03:28:55 +08:00
|
|
|
return null;
|
2019-03-25 22:49:23 +08:00
|
|
|
}
|
|
|
|
|
2020-05-06 05:03:36 +08:00
|
|
|
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)}"`;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function escape(str) {
|
|
|
|
return str.replace(/(['"\\])/g, "\\$1");
|
|
|
|
}
|
|
|
|
|
2020-05-08 02:06:43 +08:00
|
|
|
export async function init(a) {
|
|
|
|
app = a;
|
2019-04-15 21:13:38 +08:00
|
|
|
let response = await fetch("/ticket", {method:"POST"});
|
|
|
|
let ticket = (await response.json()).ticket;
|
|
|
|
|
2020-05-08 02:06:43 +08:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
current = {resolve, reject};
|
2020-05-06 05:03:36 +08:00
|
|
|
|
2019-03-20 03:45:23 +08:00
|
|
|
try {
|
2019-10-01 21:36:39 +08:00
|
|
|
let url = new URL(location.href);
|
|
|
|
url.protocol = "ws";
|
|
|
|
url.hash = "";
|
|
|
|
url.searchParams.set("ticket", ticket);
|
|
|
|
ws = new WebSocket(url.href);
|
2019-03-20 03:45:23 +08:00
|
|
|
} catch (e) { reject(e); }
|
|
|
|
|
|
|
|
ws.addEventListener("error", onError);
|
|
|
|
ws.addEventListener("message", onMessage);
|
|
|
|
ws.addEventListener("close", onClose);
|
|
|
|
});
|
|
|
|
}
|