spotiqueue/extension/spotiqueue.js

255 lines
6.3 KiB
JavaScript

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",
`<p>hi</p>`, // 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();
})();