diff --git a/extension/spotiqueue.js b/extension/spotiqueue.js index da999b1..bad7d01 100644 --- a/extension/spotiqueue.js +++ b/extension/spotiqueue.js @@ -1,4 +1,5 @@ -const DEFAULT_WS_URL = "ws://localhost:8000/ws/spotify"; +const DEFAULT_WS_URL = + localStorage.getItem("spotiqueueUrl") ?? "ws://localhost:8000/ws/spotify"; const DEFAULT_RECONNECT_MS = 1000; class SpotiQueue { @@ -72,14 +73,16 @@ class SpotiQueue { this.socket = null; } + if (this.button) this.button.style.color = "#e22134"; 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); + const payload = + typeof objOrString === "string" + ? objOrString + : JSON.stringify(objOrString); try { this.socket.send(payload); } catch (err) { @@ -111,14 +114,19 @@ class SpotiQueue { return; } - if (data.c === "next_song" && data.d && data.d.song) { - try { - Spicetify.Player.playUri(data.d.song); - Spicetify.Player.setRepeat(false); + if (data.c === "next_song") { + if (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); + } + } else { + Spicetify.Player.play(); this.startedPlaying = true; - console.log("[SpotiQueue] New song received!"); - } catch (err) { - console.error("[SpotiQueue] Error playing received song:", err); } } @@ -141,7 +149,6 @@ class SpotiQueue { includeAuthors: true, }); - console.log(data) songs = data.searchV2.tracksV2.items.reduce((o, c) => { const item = c.item.data; @@ -151,14 +158,12 @@ class SpotiQueue { o.push({ name: item.name, uri: item.uri, - artists: item.artists.items.map(v => v.profile?.name), + 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], + coverUrl: album.coverArt.sources + .sort((a, b) => b.height - a.height) + .map((v) => v.url)[0], }, }); @@ -171,16 +176,17 @@ class SpotiQueue { console.log("[SpotiQueue] Found", songs); this.send({ - "c": "search", - "d": { - "id": id, - "results": songs, + c: "search", + d: { + id: id, + results: songs, }, }); } } - _onError() { + _onError(e) { try { + console.log(e); if (this.socket) this.socket.close(); } catch { // ignore @@ -193,6 +199,7 @@ class SpotiQueue { this.reconnectInterval, this.closing, ); + if (!this.reconnectInterval && !this.closing) { this.reconnectInterval = setInterval( () => this.connect(), @@ -200,35 +207,68 @@ class SpotiQueue { ); } - if (this.button) this.button.style.color = ""; + if (this.button && !this.closing) { + 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 - } + const btn = new Spicetify.Topbar.Button("SpotiQueue", "enhance", () => {}); - this.connect(); - this.button.innerText = "bye"; - } else { - this.stop(); - this.button.innerText = "hi"; - } - }, - ); this.button = btn.button; + this.button.style.color = "#e22134"; + this.button.addEventListener("click", (e) => { + console.log(e.pointerId); + if (this.socket) { + this.stop(); + return; + } + + if (Spicetify.GraphQL.Definitions.searchDesktop === undefined) { + Spicetify.Platform.History.push("/search/"); + setTimeout(() => { + this.button.click(); + Spicetify.Platform.History.goBack(); + }, 200); + return; + } + + this.closing = false; + this.button.style.color = ""; + this.connect(); + }); + this.button.addEventListener("contextmenu", (e) => { + e.preventDefault(); + + const div = document.createElement("div"); + div.innerHTML = ` +
+ + +
+ `; + Spicetify.PopupModal.display({ title: "WebSocket URL", content: div }); + + div.querySelector("input").value = this.wsUrl + div.querySelector("button").onclick = () => { + const newUrl = div.querySelector("input").value.trim(); + if (!newUrl) return; + + localStorage.setItem("spotiqueueUrl", newUrl); + this.wsUrl = newUrl; + this.stop(); + Spicetify.PopupModal.hide(); + Spicetify.showNotification("Saved!"); + }; + }); Spicetify.Player.addEventListener("songchange", (info) => { console.log(info); if ( - this.socket && this.socket.readyState === WebSocket.OPEN && + this.socket && + this.socket.readyState === WebSocket.OPEN && !this.startedPlaying ) { Spicetify.Player.pause(); @@ -242,14 +282,17 @@ class SpotiQueue { (function init() { if ( - !Spicetify.Player || !Spicetify.Platform || !Spicetify.GraphQL || - !Spicetify.GraphQL.Request || !Spicetify.GraphQL.Definitions + !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(); + globalThis.spotiQueue = new SpotiQueue(); + globalThis.spotiQueue.initUi(); })(); diff --git a/src/main.ts b/src/main.ts index e82b581..55eb43c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,11 @@ import { SpotifyWS } from "./spotify.ts" import { HeartbeatWS } from "./heartbeat.ts"; import { PlayerManager } from "./playerManager.ts"; -import { serveFile } from "@std/http/file-server"; +import { serveDir } from "@std/http/file-server"; import { UserWS } from "./user.ts"; const USER_PATTERN = new URLPattern({pathname: "/ws/user"}) const SPOTIFY_PATTERN = new URLPattern({pathname: "/ws/spotify"}) -const song = "spotify:track:2VxJVGiTsK6UyrxPJJ2lR9"; function commonWs(req: Request) { const { socket, response } = Deno.upgradeWebSocket(req); @@ -33,11 +32,11 @@ Deno.serve((req) => { const spState = spotify?.ws.readyState; if (spState !== undefined && spState !== WebSocket.CLOSED) spotify?.ws.close(); - spotify = new SpotifyWS(socket, () => song); + new SpotifyWS(socket); new HeartbeatWS(socket); return response } - return serveFile(req, "./static/index.html") + return serveDir(req, {fsRoot: "./static", urlRoot: ""}) }) diff --git a/src/playerManager.ts b/src/playerManager.ts index 0b404bd..acfee68 100644 --- a/src/playerManager.ts +++ b/src/playerManager.ts @@ -58,6 +58,11 @@ export class PlayerManager { return song.song; } + public broadcastStatus() { + for (const user of Object.values(this.userQueue)) { + user.userWS.updateStatus(); + } + } public updateMergedQueue() { this.generateMergedQueue(); this.broadcastMergedQueue(); @@ -69,7 +74,7 @@ export class PlayerManager { } private broadcastMergedQueue() { for (const user of Object.values(this.userQueue)) { - user.userWS.broadcastQueue(this.mergedQueue); + user.userWS.updateQueue(); } } diff --git a/src/spotify.ts b/src/spotify.ts index d224219..9611c16 100644 --- a/src/spotify.ts +++ b/src/spotify.ts @@ -22,7 +22,10 @@ export class SpotifyWS { this.onMessage = this.onMessage.bind(this); this.onClose = this.onClose.bind(this); + this.onOpen = this.onOpen.bind(this); + this.ws.addEventListener("message", this.onMessage); + this.ws.addEventListener("open", this.onOpen); this.ws.addEventListener("close", this.onClose); } @@ -43,7 +46,10 @@ export class SpotifyWS { public sendSong(uri?: string) { const song = uri ?? playerManager.getNext()?.uri; - if (!song) return; + if (!song) { + this.send(buildCommand("next_song")); + return + }; this.send(buildCommand("next_song", { song })); } @@ -53,8 +59,13 @@ export class SpotifyWS { this.ws.send(data); } + private onOpen() { + globalThis.spotify = this; + playerManager.broadcastStatus(); + } private onClose() { globalThis.spotify = undefined; + playerManager.broadcastStatus(); } private onMessage(msg: MessageEvent) { const text = msg.data; diff --git a/src/user.ts b/src/user.ts index 47448fc..4d52667 100644 --- a/src/user.ts +++ b/src/user.ts @@ -23,24 +23,23 @@ export class UserWS { this.ws.send(buildCommand("logout")) this.ws.close(); } - public broadcastQueue(queue: MergedQueue) { - this.ws.send(buildCommand("updatemergedqueue", {queue})) - } public updateQueue() { const queue = this.playerQueue?.queue; if (!queue) return; - this.ws.send(buildCommand("getqueue", {queue})) + this.ws.send(buildCommand("getqueue", {personal: queue, merged: playerManager.mergedQueue })) + } + public updateStatus() { + this.ws.send(buildCommand("status", {status: spotify !== undefined})) } - private spotifyConnected(): boolean { - return spotify === undefined; - } private login(name: string) { this.name = name; this.loggedIn = true; this.playerQueue = playerManager.login(name, this); + this.updateQueue(); + this.updateStatus(); } private async onMessage(msg: MessageEvent) { const command = parseCommand(msg.data); @@ -73,12 +72,11 @@ export class UserWS { const cmd = command as Command<{query: string}>; - const songs = await spotify?.search(cmd.d.query) + let songs = await spotify?.search(cmd.d.query).catch(_ => console.log("Failed to search...")) if (songs === undefined) { - this.ws.send(buildCommand("search", {songs: [], connected: false})) - } else { - this.ws.send(buildCommand("search", {songs, connected: true})) - } + songs = [] + } + this.ws.send(buildCommand("search", {songs})) break } @@ -104,6 +102,11 @@ export class UserWS { this.playerQueue.queue.push(cmd.d.song); playerManager.updateMergedQueue(); + break + } + case "status": { + this.updateStatus(); + break } } } diff --git a/static/index.html b/static/index.html index bca5d1a..6f37483 100644 --- a/static/index.html +++ b/static/index.html @@ -1,352 +1,543 @@ - + - Spotify Queue - + + SpotiQueue + + -