Skip to content
This repository has been archived by the owner on Feb 2, 2023. It is now read-only.

Toastify Broadcaster

Alessandro Attard Barbini edited this page Sep 11, 2019 · 13 revisions

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]}

Example of a client Javascript app

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"
};

Usage

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();
Clone this wiki locally