255 lines
6.3 KiB
JavaScript
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();
|
|
})();
|