const DEFAULT_WS_URL = "ws://localhost:8000/ws/spotify"; const DEFAULT_RECONNECT_MS = 1000; class SpotiQueue { constructor(wsUrl = DEFAULT_WS_URL, reconnectMs = DEFAULT_RECONNECT_MS) { this.wsUrl = wsUrl; this.reconnectMs = reconnectMs; this.socket = null; this.reconnectInterval = null; this.closing = false; this.startedPlaying = false; this.button = null; this._onOpen = this._onOpen.bind(this); this._onMessage = this._onMessage.bind(this); this._onError = this._onError.bind(this); this._onClose = this._onClose.bind(this); } parseCommand(commandStr) { try { const parsed = JSON.parse(commandStr); if ( typeof parsed === "object" && parsed !== null && typeof parsed.c === "string" && (!("d" in parsed) || typeof parsed.d === "object") ) { return parsed; } return null; } catch { return null; } } connect() { console.log("[SpotiQueue] Trying to connect to server..."); if ( this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) ) { return; } this.socket = new WebSocket(this.wsUrl); this.socket.addEventListener("open", this._onOpen); this.socket.addEventListener("message", this._onMessage); this.socket.addEventListener("error", this._onError); this.socket.addEventListener("close", this._onClose); } stop() { if (this.reconnectInterval) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } if (this.socket) { this.closing = true; try { this.socket.close(); } catch (_e) { // empty } this.socket = null; } console.log("[SpotiQueue] Disconnecting from server, Bye!"); } send(objOrString) { if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return; const payload = (typeof objOrString === "string") ? objOrString : JSON.stringify(objOrString); try { this.socket.send(payload); } catch (err) { console.error("[SpotiQueue] Failed to send:", err); } } _onOpen() { if (this.reconnectInterval) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } if (this.button) this.button.style.color = "#1DB954"; console.log("[SpotiQueue] Connected to server!"); } async _onMessage(event) { let data; try { data = JSON.parse(event.data); } catch { console.warn("[SpotiQueue] Received non-JSON message"); return; } if (data.c === "ping") { this.send({ c: "pong" }); return; } if (data.c === "next_song" && data.d && data.d.song) { try { Spicetify.Player.playUri(data.d.song); Spicetify.Player.setRepeat(false); this.startedPlaying = true; console.log("[SpotiQueue] New song received!"); } catch (err) { console.error("[SpotiQueue] Error playing received song:", err); } } if (data.c === "search" && data.d && data.d.query && data.d.id) { const { query, id } = data.d; let songs = []; console.log("[SpotiQueue] Searching for", query); try { const { searchDesktop } = Spicetify.GraphQL.Definitions; const { data } = await Spicetify.GraphQL.Request(searchDesktop, { searchTerm: query, limit: 10, offset: 0, numberOfTopResults: 10, includeAudiobooks: false, includePreReleases: false, IncludeArtistHasConcertsField: false, includeAuthors: true, }); console.log(data) songs = data.searchV2.tracksV2.items.reduce((o, c) => { const item = c.item.data; if (!item.playability.playable) return o; const album = item.albumOfTrack; o.push({ name: item.name, uri: item.uri, artists: item.artists.items.map(v => v.profile?.name), album: { name: album.name, coverUrl: album.coverArt.sources.sort((a, b) => b.height - a.height ).map((v) => v.url )[0], }, }); return o; }, []); } catch (e) { console.log("[SpotiQueue] Search error", e); // _ } console.log("[SpotiQueue] Found", songs); this.send({ "c": "search", "d": { "id": id, "results": songs, }, }); } } _onError() { try { if (this.socket) this.socket.close(); } catch { // ignore } } _onClose() { console.log( "[SpotiQueue] closed connection", this.reconnectInterval, this.closing, ); if (!this.reconnectInterval && !this.closing) { this.reconnectInterval = setInterval( () => this.connect(), this.reconnectMs, ); } if (this.button) this.button.style.color = ""; this.closing = false; } initUi() { const btn = new Spicetify.Topbar.Button( "SpotiQueue", `
hi
`, // SVG icon or markup () => { if (!this.socket) { if (Spicetify.GraphQL.Definitions.searchDesktop === undefined) { Spicetify.showNotification("Please search something (e.g a song) before trying to use SpotiQueue. This will load required dependencies.") return } this.connect(); this.button.innerText = "bye"; } else { this.stop(); this.button.innerText = "hi"; } }, ); this.button = btn.button; Spicetify.Player.addEventListener("songchange", (info) => { console.log(info); if ( this.socket && this.socket.readyState === WebSocket.OPEN && !this.startedPlaying ) { Spicetify.Player.pause(); console.log("[SpotiQueue] Requesting new song..."); this.send({ c: "next_song" }); } this.startedPlaying = false; }); } } (function init() { if ( !Spicetify.Player || !Spicetify.Platform || !Spicetify.GraphQL || !Spicetify.GraphQL.Request || !Spicetify.GraphQL.Definitions ) { setTimeout(init, 100); console.log("[SpotiQueue] loading extension... "); return; } const client = new SpotiQueue(); client.initUi(); })();