274 lines
6.6 KiB
JavaScript
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();
|
|
})();
|