spotiqueue/extension/spotiqueue.js

274 lines
6.6 KiB
JavaScript

const DEFAULT_WS_URL =
localStorage.getItem("spotiqueueUrl") ?? "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;
}
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);
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") {
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;
}
}
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,
});
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(e) {
try {
console.log(e);
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.closing) {
this.button.style.color = "";
}
this.closing = false;
}
initUi() {
const btn = new Spicetify.Topbar.Button("SpotiQueue", "enhance", () => {});
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();
});
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;
}
globalThis.spotiQueue = new SpotiQueue();
globalThis.spotiQueue.initUi();
})();