-
Notifications
You must be signed in to change notification settings - Fork 48
Toastify Broadcaster
Since version 1.10.13, Toastify can optionally broadcast Spotify's events on your system using a local WebSocket server.
If the feature is enabled in the settings (Advanced tab), Toastify will create a local server on the default port 41348
and listen for WebSocket connections on /ws/toastify
(so, the full URL would be ws://locahost:41348/ws/toastify
).
Whenever a Spotify event happens, Toastify will redirect it to every connected client on the WebSocket. Here is the full list of event messages sent by the local server:
-
HELLO {JSON-serialized GreetingsObject}
(see JsonGreetingsObject) -
CURRENT-TRACK {JSON-serialized track}
(see JsonTrack) PLAY-STATE {"playing": [true|false]}
The following code has been successfully used in my audio visualizer for Wallpaper Engine.
Link to the wallpaper here.
The Toastify object handles the WebSocket connection:
const Toastify = (function() {
const WS_SCHEME = "ws";
const WS_HOST = "localhost";
const WS_PATH = "ws/toastify";
let map = new WeakMap();
let _ = function(obj) {
if (!map.has(obj))
map.set(obj, {});
return map.get(obj);
};
function Toastify(port) {
_(this).port = port > 1023 ? port : 41348;
_(this).dispatchers = {};
_(this).dispatchers[Toastify.Events.hello] = [];
_(this).dispatchers[Toastify.Events.currenttrackreceived] = [];
_(this).dispatchers[Toastify.Events.playstatechanged] = [];
// Define properties
Object.defineProperty(this, "wsUrl", {
get: () => {
let port = _(this).port;
return `${WS_SCHEME}://${WS_HOST}:${port}/${WS_PATH}`;
}
});
}
// PUBLIC FUNCTIONS
Toastify.prototype.openConnection = function() {
let self = this;
let ws = _(this).webSocket;
if (ws && ws.readyState !== WebSocket.CLOSED)
console.warn("Socket already connected!");
else {
ws = new WebSocket(this.wsUrl);
ws.onopen = function(e) { log("Connection opened!"); };
ws.onclose = function(e) {
let reason = WebSocket.getCloseEventDescription(e.code);
log(`Connection closed with code: ${e.code} (${reason})`);
};
ws.onerror = function(e) {};
ws.onmessage = function(e) {
if (typeof e.data === typeof String())
interpretMessage(self, e.data);
else {
let json = JSON.stringify(e.data);
console.log(`Non-string message received:\n ${json}`);
}
};
_(this).webSocket = ws;
}
};
Toastify.prototype.closeConnection = function() {
let ws = _(this).webSocket;
if (!ws || ws.readyState !== WebSocket.OPEN)
console.warn("Socket not connected!");
else {
ws.close(1000, "Closing from client");
}
};
Toastify.prototype.changePort = function(port) {
if (!Number.isInteger(port))
return;
if (this.isOpen())
this.closeConnection();
_(this).port = port > 1023 ? port : 41348;
this.openConnection();
};
Toastify.prototype.isOpen = function() {
let ws = _(this).webSocket;
return (ws || false) && ws.readyState === WebSocket.OPEN;
};
Toastify.prototype.isClosed = function() {
let ws = _(this).webSocket;
return !ws || ws.readyState === WebSocket.CLOSED;
};
Toastify.prototype.on = function(eventName, handler) {
if (!(handler instanceof Function))
return;
let dispatchers = _(this).dispatchers[eventName];
if (dispatchers)
dispatchers.push(handler);
};
// PRIVATE FUNCTIONS
const dispatch = function(toastify, eventName, ...args) {
let dispatchers = _(toastify).dispatchers[eventName];
if (dispatchers && dispatchers.length > 0) {
for (let i = 0; i < dispatchers.length; ++i) {
setTimeout(() => dispatchers[i](...args));
}
}
};
const interpretMessage = function(toastify, message) {
if (message) {
const regex = /^([^\s]+)(?: (.+))?$/;
const match = message.match(regex);
if (match) {
let eventName;
switch (match[1]) {
case "HELLO":
eventname = Toastify.Events.hello;
dispatch(toastify, eventName, JSON.parse(match[2]));
break;
case "CURRENT-TRACK":
eventName = Toastify.Events.currenttrackreceived;
dispatch(toastify, eventName, JSON.parse(match[2]));
break;
case "PLAY-STATE":
eventName = Toastify.Events.playstatechanged;
dispatch(toastify, eventName, JSON.parse(match[2]));
break;
default:
console.warn(`Unknown message received: "${match[1]}"`);
break;
}
}
}
};
if (!window.log)
window.log = console.log;
const log = function(message, ...optionalParams) {
if (typeof message === typeof String())
window.log(`[Toastify] ${message}`, ...optionalParams);
else
window.log("[Toastify]", message, ...optionalParams);
};
return Toastify;
}());
let Events = {
get hello() { return "hello"; },
get currenttrackreceived() { return "currenttrackreceived"; },
get playstatechanged() { return "playstatechanged"; }
};
Object.defineProperty(Toastify, "Events", { get: () => Events });
It depends on the following extension function to translate WebSocket's close event codes to readable names.
WebSocket.getCloseEventDescription = function(code) { return WebSocketCloseEvent[code]; };
const WebSocketCloseEvent = {
1000: "Normal Closure",
1001: "Going Away",
1002: "Protocol Error",
1003: "Unsupported",
1005: "No Status Received",
1006: "Abnormal Closure",
1007: "Invalid Frame Payload Data",
1008: "Policy Violation",
1009: "Message Too Big",
1010: "Missing Extension",
1011: "Internal Error",
1012: "Service Restart",
1013: "Try Again Later",
1014: "Bad Gateway",
1015: "TLS Handshake"
};
let toastify = new Toastify();
toastify.on(Toastify.Events.hello, (obj) => console.log("Server said HELLO! Current Spotify state:", obj));
toastify.on(Toastify.Events.currenttrackreceived, (track) => console.log("Spotify track:", track));
toastify.on(Toastify.Events.playstatechanged, (state) => console.log("Spotify state:", state));
toastify.openConnection();