(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i { if (this.connected) { try { await fetch(this.server); } catch (_a) { } } }, 5 * 60000); } connect(server) { if (!server) server = this.ltPlayer.settingsManager.settings.server; if ((0, spotifyUtils_1.getCurrentTrackUri)() != "") { (0, spotifyUtils_1.forcePlayTrack)(""); setTimeout(() => this.connect(server), 100); return; } this.server = server; this.connecting = true; this.ltPlayer.ui.renderBottomInfo(react_1.default.createElement(bottomInfo_1.default, { server: server, loading: true })); // this.ltPlayer.ui.menuItems.joinServer?.setName("Leave the server") this.socket = (0, socket_io_client_1.io)(server, { 'secure': true, }); this.socket.on("connect", () => { this.ltPlayer.ui.renderBottomInfo(react_1.default.createElement(bottomInfo_1.default, { server: server })); this.connecting = false; this.connected = true; this.ltPlayer.isHost = false; this.socket.emit("login", this.ltPlayer.settingsManager.settings.name, this.ltPlayer.version, (versionRequirements) => { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.disconnect(); setTimeout(() => this.ltPlayer.ui.windowMessage(`Your Spotify Listen Together's version isn't compatible with the server's version. Consider switching to a version that meets these requirements: "${versionRequirements}".`), 1); }); this.ltPlayer.onLogin(); }); this.socket.onAny((ev, ...args) => { console.log(`Receiving ${ev}: ${args}`); }); this.socket.on("changeSong", (trackUri) => { if ((0, spotifyUtils_1.isListenableTrackType)((0, spotifyUtils_1.getTrackType)(trackUri))) this.ltPlayer.onChangeSong(trackUri); }); this.socket.on("updateSong", (pause, milliseconds) => { if ((0, spotifyUtils_1.isListenableTrackType)()) this.ltPlayer.onUpdateSong(pause, milliseconds); }); this.socket.on("bottomMessage", (message) => { this.ltPlayer.ui.bottomMessage(message); }); this.socket.on("windowMessage", (message) => { this.ltPlayer.ui.windowMessage(message); }); this.socket.on("listeners", (clients) => { this.ltPlayer.ui.renderBottomInfo(react_1.default.createElement(bottomInfo_1.default, { server: server, listeners: clients })); }); this.socket.on("isHost", (isHost) => { if (isHost != this.ltPlayer.isHost) { this.ltPlayer.isHost = isHost; if (isHost) { // this.ltPlayer.ui.menuItems.requestHost?.setName("Cancel hosting"); this.ltPlayer.ui.bottomMessage("You are now a host."); } else { // this.ltPlayer.ui.menuItems.requestHost?.setName("Request host"); this.ltPlayer.ui.bottomMessage("You are no longer a host."); } } }); this.socket.on("songRequested", (trackUri, trackName, fromListener) => { this.ltPlayer.ui.songRequestPopup(trackName, fromListener, () => { (0, spotifyUtils_1.forcePlayTrack)(trackUri); }); }); this.socket.on("disconnect", () => this.disconnect()); this.socket.on("error", () => { this.disconnect(); this.ltPlayer.ui.windowMessage(`Couldn't connect to "${server}".`); }); } disconnect() { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.disconnect(); this.socket = null; this.connected = false; this.ltPlayer.isHost = false; this.connecting = false; // this.ltPlayer.ui.menuItems.joinServer?.setName("Join a server") // this.ltPlayer.ui.menuItems.requestHost?.setName("Request host"); this.ltPlayer.ui.renderBottomInfo(react_1.default.createElement(bottomInfo_1.default, { server: "" })); this.ltPlayer.ui.disconnectedPopup(); } } exports.default = Client; },{"./spotifyUtils":9,"./ui/bottomInfo":11,"react":49,"socket.io-client":50}],3:[function(require,module,exports){ "use strict"; /// var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ltPlayer_1 = __importDefault(require("./ltPlayer")); (function listenTogetherMain() { if (!Spicetify.CosmosAsync || !Spicetify.Platform || !Spicetify.LocalStorage) { setTimeout(listenTogetherMain, 1000); return; } const ltPlayer = new ltPlayer_1.default(); })(); },{"./ltPlayer":5}],4:[function(require,module,exports){ "use strict"; // https://gist.github.com/JasonKleban/50cee44960c225ac1993c922563aa540 Object.defineProperty(exports, "__esModule", { value: true }); exports.LiteEvent = void 0; class LiteEvent { constructor() { this.handlers = []; } on(handler) { this.handlers.push(handler); } off(handler) { this.handlers = this.handlers.filter(h => h !== handler); } trigger(data) { this.handlers.slice(0).forEach(h => h(data)); } expose() { return this; } } exports.LiteEvent = LiteEvent; },{}],5:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const client_1 = __importDefault(require("./client")); const patcher_1 = __importStar(require("./patcher")); const settings_1 = __importDefault(require("./settings")); const ui_1 = __importDefault(require("./ui/ui")); const package_json_1 = __importDefault(require("../package.json")); require("./spotifyUtils"); const spotifyUtils_1 = require("./spotifyUtils"); class LTPlayer { constructor() { this.client = new client_1.default(this); this.patcher = new patcher_1.default(this); this.spotifyUtils = new spotifyUtils_1.SpotifyUtils(this); this.settingsManager = new settings_1.default(); this.ui = new ui_1.default(this); this.isHost = false; this.version = package_json_1.default.version; this.watchingAd = false; this.trackLoaded = true; this.currentLoadingTrack = ""; this.volumeChangeEnabled = false; this.canChangeVolume = true; this.lastVolume = null; this.patcher.patchAll(); this.patcher.trackChanged.on((trackUri) => { this.onSongChanged(trackUri); }); setInterval(() => { if (this.client.connected && (0, spotifyUtils_1.getTrackType)() === spotifyUtils_1.TrackType.Ad) { (0, spotifyUtils_1.resumeTrack)(); } }, 2000); this.volumeChangeEnabled = !!patcher_1.OGFunctions.setVolume; // For testing Spicetify.OGFunctions = patcher_1.OGFunctions; } requestChangeSong(trackUri) { var _a; (_a = this.client.socket) === null || _a === void 0 ? void 0 : _a.emit("requestChangeSong", trackUri); } requestUpdateSong(paused, milliseconds) { var _a; let trackType = (0, spotifyUtils_1.getTrackType)(); if ((0, spotifyUtils_1.isListenableTrackType)(trackType)) (_a = this.client.socket) === null || _a === void 0 ? void 0 : _a.emit("requestUpdateSong", paused, milliseconds); else this.onUpdateSong(paused, trackType === spotifyUtils_1.TrackType.Ad ? undefined : milliseconds); } async requestSong(trackUri) { var _a; let data = await (0, spotifyUtils_1.getTrackData)(trackUri); if (data && data.error === undefined) { (_a = this.client.socket) === null || _a === void 0 ? void 0 : _a.emit("requestSong", trackUri, data.name || "UNKNOWN NAME"); } } // Received onChangeSong(trackUri) { var _a; if (this.currentLoadingTrack === trackUri) { if (this.trackLoaded) (_a = this.client.socket) === null || _a === void 0 ? void 0 : _a.emit("changedSong", this.currentLoadingTrack); } else { (0, spotifyUtils_1.forcePlayTrack)(trackUri); } } onUpdateSong(pause, milliseconds) { if (milliseconds != undefined) patcher_1.OGFunctions.seekTo(milliseconds); if (pause) { (0, spotifyUtils_1.pauseTrack)(); } else { (0, spotifyUtils_1.resumeTrack)(); } } // Events onSongChanged(trackUri) { var _a, _b; if (trackUri === undefined) trackUri = (0, spotifyUtils_1.getCurrentTrackUri)(); console.log(`Changed track to ${trackUri}`); this.currentLoadingTrack = trackUri; console.trace(); if (this.client.connected) { if ((0, spotifyUtils_1.isListenableTrackType)((0, spotifyUtils_1.getTrackType)(trackUri))) { this.trackLoaded = false; (_a = this.client.socket) === null || _a === void 0 ? void 0 : _a.emit("loadingSong", trackUri); this.spotifyUtils.onTrackLoaded(trackUri, () => { var _a, _b, _c, _d, _e; this.trackLoaded = true; (0, spotifyUtils_1.pauseTrack)(); patcher_1.OGFunctions.seekTo(0); (_a = this.client.socket) === null || _a === void 0 ? void 0 : _a.emit("changedSong", trackUri, (_c = (_b = Spicetify.Platform.PlayerAPI._state) === null || _b === void 0 ? void 0 : _b.item) === null || _c === void 0 ? void 0 : _c.name, (_e = (_d = Spicetify.Platform.PlayerAPI._state) === null || _d === void 0 ? void 0 : _d.item) === null || _e === void 0 ? void 0 : _e.images[0]['url']); // Change volume back to normal if (this.volumeChangeEnabled) { patcher_1.OGFunctions.setVolume(this.lastVolume); this.lastVolume = null; this.canChangeVolume = true; } }); } else { (_b = this.client.socket) === null || _b === void 0 ? void 0 : _b.emit("changedSong", trackUri); } } } onLogin() { (0, spotifyUtils_1.pauseTrack)(); if (this.volumeChangeEnabled) { this.canChangeVolume = true; this.lastVolume = Spicetify.Player.getVolume(); this.ui.bottomMessage("Connected to the server."); } } muteBeforePlay() { // Lower volume to 0s if (this.volumeChangeEnabled) { this.canChangeVolume = false; if (this.lastVolume === null) this.lastVolume = Spicetify.Player.getVolume(); patcher_1.OGFunctions.setVolume(0); } } } exports.default = LTPlayer; },{"../package.json":1,"./client":2,"./patcher":6,"./settings":8,"./spotifyUtils":9,"./ui/ui":13}],6:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OGFunctions = void 0; const liteEvent_1 = require("./liteEvent"); const spotifyUtils_1 = require("./spotifyUtils"); class Patcher { constructor(ltPlayer) { this.ltPlayer = ltPlayer; this.lastData = null; this.onTrackChanged = new liteEvent_1.LiteEvent(); } get trackChanged() { return this.onTrackChanged.expose(); } patchAll() { var _a; exports.OGFunctions = { play: Spicetify.Platform.PlayerAPI.play.bind(Spicetify.Platform.PlayerAPI), pause: Spicetify.Platform.PlayerAPI.pause.bind(Spicetify.Platform.PlayerAPI), resume: Spicetify.Platform.PlayerAPI.resume.bind(Spicetify.Platform.PlayerAPI), seekTo: Spicetify.Platform.PlayerAPI.seekTo.bind(Spicetify.Platform.PlayerAPI), skipToNext: Spicetify.Platform.PlayerAPI.skipToNext.bind(Spicetify.Platform.PlayerAPI), skipToPrevious: Spicetify.Platform.PlayerAPI.skipToPrevious.bind(Spicetify.Platform.PlayerAPI), emitSync: Spicetify.Platform.PlayerAPI._events._emitter.emitSync.bind(Spicetify.Platform.PlayerAPI._events._emitter), setVolume: (_a = Spicetify.Platform.PlayerAPI._volume) === null || _a === void 0 ? void 0 : _a.setVolume.bind(Spicetify.Platform.PlayerAPI._volume), }; Spicetify.Platform.PlayerAPI._cosmos.sub("sp://player/v2/main", (data) => { var _a, _b, _c, _d; if (!data) return; if (((_b = (_a = this.lastData) === null || _a === void 0 ? void 0 : _a.track) === null || _b === void 0 ? void 0 : _b.uri) !== ((_c = data === null || data === void 0 ? void 0 : data.track) === null || _c === void 0 ? void 0 : _c.uri)) { console.log(data); this.onTrackChanged.trigger(((_d = data === null || data === void 0 ? void 0 : data.track) === null || _d === void 0 ? void 0 : _d.uri) || ""); } this.lastData = data; }); Spicetify.Platform.PlayerAPI._events._emitter.emitSync = (e, t) => { var _a; if (this.ltPlayer.client.connected && !this.ltPlayer.isHost) { if ((t === null || t === void 0 ? void 0 : t.action) === "play" && ((_a = t === null || t === void 0 ? void 0 : t.options) === null || _a === void 0 ? void 0 : _a.ltForced) !== true) return; if ((t === null || t === void 0 ? void 0 : t.action) === "pause" && (0, spotifyUtils_1.isTrackPaused)()) return; if ((t === null || t === void 0 ? void 0 : t.action) === "resume" && !(0, spotifyUtils_1.isTrackPaused)()) return; } return exports.OGFunctions.emitSync(e, t); }; Spicetify.Platform.PlayerAPI.play = (uri, origins, options) => { console.log(`Play: uri=${JSON.stringify(uri)}\norigins=${JSON.stringify(origins)}\noptions=${JSON.stringify(options)}`); this.restrictAccess(() => exports.OGFunctions.play(uri, origins, options), () => { var _a; if ((options === null || options === void 0 ? void 0 : options.repeat) === undefined) { // Don't do anything if the function was executed by what plays the next song. Spicetify.showNotification("Only the hosts can change songs!"); if (typeof (uri === null || uri === void 0 ? void 0 : uri.uri) === "string") { if ((0, spotifyUtils_1.isListenableTrackType)((0, spotifyUtils_1.getTrackType)(uri.uri))) this.ltPlayer.requestSong(uri.uri); else if (typeof ((_a = options === null || options === void 0 ? void 0 : options.skipTo) === null || _a === void 0 ? void 0 : _a.uri) === "string" && (0, spotifyUtils_1.isListenableTrackType)((0, spotifyUtils_1.getTrackType)(options.skipTo.uri))) this.ltPlayer.requestSong(options.skipTo.uri); } } }, () => { this.ltPlayer.muteBeforePlay(); exports.OGFunctions.play(uri, origins, options); }, this.ltPlayer.isHost || (options === null || options === void 0 ? void 0 : options.ltForced) === true); }; Spicetify.Platform.PlayerAPI.pause = () => { this.restrictAccess(() => exports.OGFunctions.pause(), () => Spicetify.showNotification("Only the hosts can pause songs!"), () => { this.ltPlayer.requestUpdateSong(true, Spicetify.Player.getProgress()); }); }; Spicetify.Platform.PlayerAPI.resume = () => { this.restrictAccess(() => exports.OGFunctions.resume(), () => Spicetify.showNotification("Only the hosts can resume songs!"), () => { this.ltPlayer.requestUpdateSong(false, Spicetify.Player.getProgress()); }); }; Spicetify.Platform.PlayerAPI.skipToNext = (e) => { this.restrictAccess(() => exports.OGFunctions.skipToNext(e), () => Spicetify.showNotification("Only the hosts can change songs!")); }; Spicetify.Platform.PlayerAPI.seekTo = (milliseconds) => { this.restrictAccess(() => exports.OGFunctions.seekTo(milliseconds), () => Spicetify.showNotification("Only the hosts can seek songs!"), () => { this.ltPlayer.requestUpdateSong(!Spicetify.Player.isPlaying(), milliseconds); }); }; Spicetify.Platform.PlayerAPI.skipToPrevious = (e) => { this.restrictAccess(() => exports.OGFunctions.skipToPrevious(e), () => Spicetify.showNotification("Only the hosts can change songs!"), () => { if (Spicetify.Player.getProgress() <= 3000) exports.OGFunctions.skipToPrevious(e); else Spicetify.Player.seek(0); }); }; if (exports.OGFunctions.setVolume) Spicetify.Platform.PlayerAPI._volume.setVolume = (e) => { if (!this.ltPlayer.client.connected || this.ltPlayer.canChangeVolume) exports.OGFunctions.setVolume(e); }; Spicetify.Platform.History.listen(({ pathname }) => { let pathParts = pathname === null || pathname === void 0 ? void 0 : pathname.split("/", 3).filter(i => i); if (((pathParts === null || pathParts === void 0 ? void 0 : pathParts.length) || 0) >= 2 && pathParts[0].toLowerCase() == "listentogether") { this.ltPlayer.ui.joinAServerQuick(decodeURIComponent(pathParts[1])); Spicetify.Platform.History.goBack(); } }); } restrictAccess(ogFunc, restrictCallback, hostFunc, access) { if (!this.ltPlayer.client.connected && !this.ltPlayer.client.connecting) { ogFunc(); } else if ((access !== undefined && access) || (access === undefined && this.ltPlayer.isHost)) { if (hostFunc) hostFunc(); else ogFunc(); } else { restrictCallback(); } } } exports.default = Patcher; },{"./liteEvent":4,"./spotifyUtils":9}],7:[function(require,module,exports){ var css = ".lt-popup-button:enabled:active,\n.lt-popup-button:enabled:hover {\n background-color: rgba(255,255,255,.1);\n color: #fff;\n text-decoration: none;\n}\n.lt-popup-button:disabled {\n color: rgba(255,255,255,.5);\n}\n.lt-popup-button {\n background: transparent;\n border: 0;\n border-radius: 2px;\n cursor: default;\n text-decoration: none;\n -webkit-padding-end: 8px;\n -webkit-box-pack: justify;\n -ms-flex-pack: justify;\n -webkit-box-align: center;\n -ms-flex-align: center;\n align-items: center;\n color: rgba(255,255,255,.9);\n display: -webkit-box;\n display: -ms-flexbox;\n display: flex;\n gap: 8px;\n height: 40px;\n justify-content: space-between;\n padding: 12px;\n padding-inline-end: 8px;\n position: relative;\n text-align: start;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n width: 100%;\n}\n.lt-popup-text {\n font-size: 16px;\n overflow-y: visible;\n}\n"; (require("browserify-css").createStyle(css, { "href": "dist\\src\\public\\ui.css" }, { "insertAt": "bottom" })); module.exports = css; },{"browserify-css":18}],8:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); class Settings { constructor() { this.settingsVersion = "1"; this.server = ""; this.name = "Unnamed"; } } class SettingsManager { constructor() { let settingsString = Spicetify.LocalStorage.get("listenTogether"); let newSettings = new Settings(); if (settingsString !== null) { this.settings = JSON.parse(settingsString); if (this.settings.settingsVersion !== newSettings.settingsVersion) { this.settings = newSettings; this.saveSettings(); } } else { this.settings = newSettings; this.saveSettings(); } } saveSettings() { Spicetify.LocalStorage.set("listenTogether", JSON.stringify(this.settings)); } } exports.default = SettingsManager; },{}],9:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpotifyUtils = exports.forcePlay = exports.forcePlayTrack = exports.getTrackProgress = exports.isTrackPaused = exports.resumeTrack = exports.pauseTrack = exports.isTrack = exports.getTrackType = exports.isListenableTrackType = exports.getCurrentTrackUri = exports.getTrackData = exports.TrackType = void 0; const patcher_1 = require("./patcher"); var TrackType; (function (TrackType) { TrackType[TrackType["Ad"] = 0] = "Ad"; TrackType[TrackType["Episode"] = 1] = "Episode"; TrackType[TrackType["Track"] = 2] = "Track"; TrackType[TrackType["NoTrack"] = 3] = "NoTrack"; TrackType[TrackType["Unknown"] = 4] = "Unknown"; })(TrackType = exports.TrackType || (exports.TrackType = {})); async function getTrackData(trackUri) { if (isTrack(trackUri)) trackUri = trackUri.split(":")[2]; let token = await Spicetify.CosmosAsync.get("sp://auth/v2/token"); return await fetch(`https://api.spotify.com/v1/tracks/${trackUri}`, { "headers": { "authorization": "Bearer " + token.accessToken } }).then(a => a.json()); } exports.getTrackData = getTrackData; function getCurrentTrackUri() { var _a; return ((_a = Spicetify.Platform.PlayerAPI._state.item) === null || _a === void 0 ? void 0 : _a.uri) || ""; } exports.getCurrentTrackUri = getCurrentTrackUri; function isListenableTrackType(trackType) { if (trackType === undefined) trackType = getTrackType(); return trackType === TrackType.Episode || trackType === TrackType.Track; } exports.isListenableTrackType = isListenableTrackType; function getTrackType(trackUri) { if (trackUri === undefined) trackUri = getCurrentTrackUri(); switch ((trackUri.split(":")[1] || "").toLowerCase()) { case "": { return TrackType.NoTrack; } case "ad": { return TrackType.Ad; } case "track": { return TrackType.Track; } case "episode": { return TrackType.Episode; } default: { return TrackType.Unknown; } } } exports.getTrackType = getTrackType; function isTrack(trackUri) { return (trackUri.match(/:/g) || []).length == 2; } exports.isTrack = isTrack; function pauseTrack() { if (Spicetify.Player.isPlaying()) patcher_1.OGFunctions.pause(); } exports.pauseTrack = pauseTrack; function resumeTrack() { if (!Spicetify.Player.isPlaying()) patcher_1.OGFunctions.resume(); } exports.resumeTrack = resumeTrack; function isTrackPaused() { return !Spicetify.Player.isPlaying(); } exports.isTrackPaused = isTrackPaused; function getTrackProgress() { return Spicetify.Player.getProgress(); } exports.getTrackProgress = getTrackProgress; function forcePlayTrack(trackUri) { forcePlay({ uri: trackUri }, {}, {}); } exports.forcePlayTrack = forcePlayTrack; function forcePlay(uri, origins, options) { Spicetify.Platform.PlayerAPI.play(uri, origins, Object.assign(Object.assign({}, options), { ltForced: true })); } exports.forcePlay = forcePlay; class SpotifyUtils { constructor(ltPlayer) { this.ltPlayer = ltPlayer; this.loadedInterval = null; this.timeoutLoadedCallback = null; } onTrackLoaded(trackUri, callback) { if (this.loadedInterval) clearInterval(this.loadedInterval); if (this.timeoutLoadedCallback) clearTimeout(this.timeoutLoadedCallback); this.loadedInterval = setInterval(() => { var _a, _b, _c, _d; console.log(`check loaded: ${getCurrentTrackUri()}===${trackUri} ${(_b = (_a = Spicetify.Platform.PlayerAPI._state) === null || _a === void 0 ? void 0 : _a.item) === null || _b === void 0 ? void 0 : _b.name} ${!Spicetify.Platform.PlayerAPI._state.isBuffering}`); if (getCurrentTrackUri() === trackUri && ((_d = (_c = Spicetify.Platform.PlayerAPI._state) === null || _c === void 0 ? void 0 : _c.item) === null || _d === void 0 ? void 0 : _d.name) && !Spicetify.Platform.PlayerAPI._state.isBuffering) { if (this.loadedInterval) clearInterval(this.loadedInterval); if (this.timeoutLoadedCallback) clearTimeout(this.timeoutLoadedCallback); callback(); } }, 100); this.timeoutLoadedCallback = setTimeout(() => { if (this.timeoutLoadedCallback) clearTimeout(this.timeoutLoadedCallback); if (this.loadedInterval) clearInterval(this.loadedInterval); callback(); }, 5000); return this.loadedInterval; } } exports.SpotifyUtils = SpotifyUtils; },{"./patcher":6}],10:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = ''; },{}],11:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = __importDefault(require("react")); const styles = { textContainer: { fontSize: '14px', position: 'absolute', display: 'flex', justifyContent: 'space-between', width: '100%', left: '0', bottom: '0', maxHeight: '22px' } }; function BottomInfo(props) { return react_1.default.createElement("div", { style: styles.textContainer }, !!props.server ? react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement("span", { style: { maxHeight: '22px', overflow: 'hidden', maxWidth: '50%' } }, `Listen Together ${props.loading ? "trying to connect" : "connected"} to ${props.server}`), props.loading ? react_1.default.createElement(react_1.default.Fragment, null) : react_1.default.createElement("span", { style: { maxHeight: '22px', overflow: 'hidden', maxWidth: '50%' } }, `Listeners: `, " ", props.listeners ? props.listeners.map((listener, i) => { let color = ""; let title = "Listener"; if (listener.isHost && listener.watchingAD) { color = "LimeGreen"; title = "Host and watching an AD"; } else if (listener.watchingAD) { color = "LimeGreen"; title = "Watching an AD"; } else if (listener.isHost) { color = "Orange"; title = "Host"; } return react_1.default.createElement("span", { key: i, title: title, style: { color: color } }, listener.name + (i !== props.listeners.length - 1 ? ", " : "")); }) : "")) : react_1.default.createElement(react_1.default.Fragment, null)); } exports.default = BottomInfo; },{"react":49}],12:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Popup = void 0; const react_1 = __importDefault(require("react")); const styles = { center: { display: 'flex', justifyContent: 'center', alignItems: 'center' }, button: { height: '32px' } }; var Popup; (function (Popup) { class Textbox extends react_1.default.Component { constructor(props) { super(props); if (props.defaultValue != undefined && this.props.onInput != undefined) { this.props.onInput(props.defaultValue); } } render() { return react_1.default.createElement("tr", null, react_1.default.createElement("td", { style: { paddingBottom: this.props.bottomSpace || 20 } }, react_1.default.createElement("div", { style: styles.center }, this.props.name)), react_1.default.createElement("td", { style: { paddingBottom: this.props.bottomSpace || 20 } }, react_1.default.createElement("div", { style: styles.center }, react_1.default.createElement("input", { onInput: (e) => { if (this.props.onInput) this.props.onInput(e.currentTarget.value); }, className: 'main-playlistEditDetailsModal-titleInput', type: 'text', defaultValue: this.props.defaultValue || "", placeholder: this.props.example || "" })))); } } Popup.Textbox = Textbox; class Text extends react_1.default.Component { constructor(props) { super(props); } render() { return react_1.default.createElement("tr", null, react_1.default.createElement("td", { colSpan: 2, style: { paddingBottom: this.props.bottomSpace || 20 } }, react_1.default.createElement("div", { style: this.props.centered || true ? styles.center : {} }, this.props.text))); } } Popup.Text = Text; class Button extends react_1.default.Component { constructor(props) { super(props); } render() { return react_1.default.createElement("tr", null, react_1.default.createElement("td", { colSpan: 2, style: { paddingBottom: this.props.bottomSpace || 10 } }, react_1.default.createElement("button", { className: "lt-popup-button", onClick: () => { if (this.props.onClick) this.props.onClick(); }, disabled: this.props.disabled }, react_1.default.createElement("span", { className: "lt-popup-text", dir: "auto" }, this.props.text)))); } } Popup.Button = Button; function close() { Spicetify.PopupModal.hide(); } Popup.close = close; function create(title, closed, buttonNames, content) { let buttons = []; buttonNames.forEach((btnName) => { buttons.push(react_1.default.createElement("button", { className: 'main-buttons-button main-button-secondary main-playlistEditDetailsModal-save', style: styles.button, type: 'button', onClick: () => { closed(btnName); } }, btnName)); }); Spicetify.PopupModal.display({ title: title, content: (react_1.default.createElement("div", { style: styles.center }, react_1.default.createElement("table", { style: { width: '100%' } }, content.map((elem) => { return react_1.default.createElement(react_1.default.Fragment, null, elem); }), react_1.default.createElement("tr", null, react_1.default.createElement("td", { colSpan: 2 }, react_1.default.createElement("div", { style: styles.center }, react_1.default.createElement("table", { style: { width: '100%' } }, react_1.default.createElement("tr", null, buttons.map((btn) => { return react_1.default.createElement("td", null, react_1.default.createElement("div", { style: styles.center }, btn)); }))))))))) }); } Popup.create = create; })(Popup = exports.Popup || (exports.Popup = {})); },{"react":49}],13:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = __importDefault(require("react")); const server_1 = require("react-dom/server"); const bottomInfo_1 = __importDefault(require("./bottomInfo")); const popup_1 = require("./popup"); const ListenTogetherIcon_1 = __importDefault(require("./ListenTogetherIcon")); const package_json_1 = __importDefault(require("../../package.json")); const css = require('../public/ui.css'); class UI { constructor(ltPlayer) { this.ltPlayer = ltPlayer; this.bottomInfoContainer = null; new Spicetify.Topbar.Button("Listen Together", ListenTogetherIcon_1.default, () => this.openMenu()); let loop = setInterval(() => { let playingBar = document.getElementsByClassName("main-nowPlayingBar-nowPlayingBar").item(0); if (playingBar) { clearInterval(loop); this.bottomInfoContainer = document.createElement("div"); this.bottomInfoContainer.id = "listenTogether-bottomInfo"; playingBar.appendChild(this.bottomInfoContainer); this.renderBottomInfo(react_1.default.createElement(bottomInfo_1.default, { server: "" })); } }, 100); } songRequestPopup(trackName, fromListener, permitted) { popup_1.Popup.create("Listen Together", (btn) => { if (btn === "Play") permitted(); popup_1.Popup.close(); }, ["Play"], [ react_1.default.createElement(popup_1.Popup.Text, { text: `${fromListener} wants to play "${trackName}".` }) ]); } openMenu() { popup_1.Popup.create("Listen Together", () => popup_1.Popup.close(), [], [ react_1.default.createElement(popup_1.Popup.Button, { text: (this.ltPlayer.client.connected || this.ltPlayer.client.connecting) ? "Leave the server" : "Join a server", onClick: () => this.onClickJoinAServer() }), react_1.default.createElement(popup_1.Popup.Button, { text: (this.ltPlayer.isHost ? "Stop hosting" : "Request host"), onClick: () => this.onClickRequestHost(), disabled: !this.ltPlayer.client.connected }), react_1.default.createElement(popup_1.Popup.Button, { text: "About", onClick: () => this.onClickAbout() }), ]); } windowMessage(message) { popup_1.Popup.create("Listen Together", () => popup_1.Popup.close(), ["OK"], [ react_1.default.createElement(popup_1.Popup.Text, { text: message }) ]); } bottomMessage(message) { Spicetify.showNotification(message); } disconnectedPopup() { popup_1.Popup.create("Listen Together", (btn) => { if (btn === "Reconnect") { this.ltPlayer.client.connect(); } popup_1.Popup.close(); }, ["Reconnect"], [ react_1.default.createElement(popup_1.Popup.Text, { text: "Disconnected from the server." }) ]); } joinAServerQuick(address) { if (!this.ltPlayer.client.connected && !this.ltPlayer.client.connecting && !!address) { this.ltPlayer.settingsManager.settings.server = address; this.ltPlayer.settingsManager.saveSettings(); if (!this.ltPlayer.settingsManager.settings.name) { this.onClickJoinAServer(); } else { this.ltPlayer.client.connect(); } } } onClickJoinAServer() { if (this.ltPlayer.client.connected || this.ltPlayer.client.connecting) { this.ltPlayer.client.disconnect(); } else { this.joinServerPopup((btn, address, name) => { if (btn === "Host a server") { window.location.href = "https://heroku.com/deploy?template=https://github.com/FlafyDev/spotify-listen-together-server"; } else { popup_1.Popup.close(); if (!!address && !!name) { this.ltPlayer.settingsManager.settings.server = address; this.ltPlayer.settingsManager.settings.name = name; this.ltPlayer.settingsManager.saveSettings(); this.ltPlayer.client.connect(); } } }); } } onClickRequestHost() { var _a; if (this.ltPlayer.client.connected) { if (this.ltPlayer.isHost) { (_a = this.ltPlayer.client.socket) === null || _a === void 0 ? void 0 : _a.emit("cancelHost"); popup_1.Popup.close(); } else { this.requestHostPopup((password) => { var _a; if (!!password) { (_a = this.ltPlayer.client.socket) === null || _a === void 0 ? void 0 : _a.emit("requestHost", password); } popup_1.Popup.close(); }); } } else { this.windowMessage("Please connect to a server before requesting host."); } } onClickAbout() { popup_1.Popup.create("Listen Together", () => { popup_1.Popup.close(); }, [], [ react_1.default.createElement(popup_1.Popup.Text, { text: `Listen Together v${package_json_1.default.version} created by FlafyDev`, centered: false }), react_1.default.createElement(popup_1.Popup.Button, { text: "Github", onClick: () => window.location.href = "https://github.com/FlafyDev/spotify-listen-together" }), ]); } joinServerPopup(callback) { let address = ""; let name = ""; popup_1.Popup.create("Listen Together", (btn) => callback(btn, address, name), ["Join", "Host a server"], [ react_1.default.createElement(popup_1.Popup.Textbox, { name: "Server address", example: "https://www.server.com/", defaultValue: this.ltPlayer.settingsManager.settings.server, onInput: (text) => { address = text; } }), react_1.default.createElement(popup_1.Popup.Textbox, { name: "Your name", example: "Joe", defaultValue: this.ltPlayer.settingsManager.settings.name, onInput: (text) => { name = text; } }), ]); } requestHostPopup(callback) { let password = ""; popup_1.Popup.create("Listen Together", () => callback(password), ["Request"], [ react_1.default.createElement(popup_1.Popup.Text, { text: "Request host" }), react_1.default.createElement(popup_1.Popup.Textbox, { name: "Password", onInput: (text) => password = text }) ]); } renderBottomInfo(bottomInfo) { if (this.bottomInfoContainer) { this.bottomInfoContainer.innerHTML = (0, server_1.renderToStaticMarkup)(bottomInfo); } } } exports.default = UI; },{"../../package.json":1,"../public/ui.css":7,"./ListenTogetherIcon":10,"./bottomInfo":11,"./popup":12,"react":49,"react-dom/server":46}],14:[function(require,module,exports){ /** * Expose `Emitter`. */ exports.Emitter = Emitter; /** * Initialize a new `Emitter`. * * @api public */ function Emitter(obj) { if (obj) return mixin(obj); } /** * Mixin the emitter properties. * * @param {Object} obj * @return {Object} * @api private */ function mixin(obj) { for (var key in Emitter.prototype) { obj[key] = Emitter.prototype[key]; } return obj; } /** * Listen on the given `event` with `fn`. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.on = Emitter.prototype.addEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; (this._callbacks['$' + event] = this._callbacks['$' + event] || []) .push(fn); return this; }; /** * Adds an `event` listener that will be invoked a single * time then automatically removed. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.once = function(event, fn){ function on() { this.off(event, on); fn.apply(this, arguments); } on.fn = fn; this.on(event, on); return this; }; /** * Remove the given callback for `event` or all * registered callbacks. * * @param {String} event * @param {Function} fn * @return {Emitter} * @api public */ Emitter.prototype.off = Emitter.prototype.removeListener = Emitter.prototype.removeAllListeners = Emitter.prototype.removeEventListener = function(event, fn){ this._callbacks = this._callbacks || {}; // all if (0 == arguments.length) { this._callbacks = {}; return this; } // specific event var callbacks = this._callbacks['$' + event]; if (!callbacks) return this; // remove all handlers if (1 == arguments.length) { delete this._callbacks['$' + event]; return this; } // remove specific handler var cb; for (var i = 0; i < callbacks.length; i++) { cb = callbacks[i]; if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1); break; } } // Remove event specific arrays for event types that no // one is subscribed for to avoid memory leak. if (callbacks.length === 0) { delete this._callbacks['$' + event]; } return this; }; /** * Emit `event` with the given args. * * @param {String} event * @param {Mixed} ... * @return {Emitter} */ Emitter.prototype.emit = function(event){ this._callbacks = this._callbacks || {}; var args = new Array(arguments.length - 1) , callbacks = this._callbacks['$' + event]; for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } if (callbacks) { callbacks = callbacks.slice(0); for (var i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(this, args); } } return this; }; // alias used for reserved events (protected method) Emitter.prototype.emitReserved = Emitter.prototype.emit; /** * Return array of callbacks for `event`. * * @param {String} event * @return {Array} * @api public */ Emitter.prototype.listeners = function(event){ this._callbacks = this._callbacks || {}; return this._callbacks['$' + event] || []; }; /** * Check if this emitter has `event` handlers. * * @param {String} event * @return {Boolean} * @api public */ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; },{}],15:[function(require,module,exports){ /** * Expose `Backoff`. */ module.exports = Backoff; /** * Initialize backoff timer with `opts`. * * - `min` initial timeout in milliseconds [100] * - `max` max timeout [10000] * - `jitter` [0] * - `factor` [2] * * @param {Object} opts * @api public */ function Backoff(opts) { opts = opts || {}; this.ms = opts.min || 100; this.max = opts.max || 10000; this.factor = opts.factor || 2; this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0; this.attempts = 0; } /** * Return the backoff duration. * * @return {Number} * @api public */ Backoff.prototype.duration = function(){ var ms = this.ms * Math.pow(this.factor, this.attempts++); if (this.jitter) { var rand = Math.random(); var deviation = Math.floor(rand * this.jitter * ms); ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation; } return Math.min(ms, this.max) | 0; }; /** * Reset the number of attempts. * * @api public */ Backoff.prototype.reset = function(){ this.attempts = 0; }; /** * Set the minimum duration * * @api public */ Backoff.prototype.setMin = function(min){ this.ms = min; }; /** * Set the maximum duration * * @api public */ Backoff.prototype.setMax = function(max){ this.max = max; }; /** * Set the jitter * * @api public */ Backoff.prototype.setJitter = function(jitter){ this.jitter = jitter; }; },{}],16:[function(require,module,exports){ /* * base64-arraybuffer 1.0.1 * Copyright (c) 2021 Niklas von Hertzen * Released under MIT License */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['base64-arraybuffer'] = {})); }(this, (function (exports) { 'use strict'; var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Use a lookup table to find the index. var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); for (var i = 0; i < chars.length; i++) { lookup[chars.charCodeAt(i)] = i; } var encode = function (arraybuffer) { var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; for (i = 0; i < len; i += 3) { base64 += chars[bytes[i] >> 2]; base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; base64 += chars[bytes[i + 2] & 63]; } if (len % 3 === 2) { base64 = base64.substring(0, base64.length - 1) + '='; } else if (len % 3 === 1) { base64 = base64.substring(0, base64.length - 2) + '=='; } return base64; }; var decode = function (base64) { var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4; if (base64[base64.length - 1] === '=') { bufferLength--; if (base64[base64.length - 2] === '=') { bufferLength--; } } var arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer); for (i = 0; i < len; i += 4) { encoded1 = lookup[base64.charCodeAt(i)]; encoded2 = lookup[base64.charCodeAt(i + 1)]; encoded3 = lookup[base64.charCodeAt(i + 2)]; encoded4 = lookup[base64.charCodeAt(i + 3)]; bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); } return arraybuffer; }; exports.decode = decode; exports.encode = encode; Object.defineProperty(exports, '__esModule', { value: true }); }))); },{}],17:[function(require,module,exports){ 'use strict' exports.byteLength = byteLength exports.toByteArray = toByteArray exports.fromByteArray = fromByteArray var lookup = [] var revLookup = [] var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' for (var i = 0, len = code.length; i < len; ++i) { lookup[i] = code[i] revLookup[code.charCodeAt(i)] = i } // Support decoding URL-safe base64 strings, as Node.js does. // See: https://en.wikipedia.org/wiki/Base64#URL_applications revLookup['-'.charCodeAt(0)] = 62 revLookup['_'.charCodeAt(0)] = 63 function getLens (b64) { var len = b64.length if (len % 4 > 0) { throw new Error('Invalid string. Length must be a multiple of 4') } // Trim off extra bytes after placeholder bytes are found // See: https://github.com/beatgammit/base64-js/issues/42 var validLen = b64.indexOf('=') if (validLen === -1) validLen = len var placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4) return [validLen, placeHoldersLen] } // base64 is 4/3 + up to two characters of the original data function byteLength (b64) { var lens = getLens(b64) var validLen = lens[0] var placeHoldersLen = lens[1] return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen } function _byteLength (b64, validLen, placeHoldersLen) { return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen } function toByteArray (b64) { var tmp var lens = getLens(b64) var validLen = lens[0] var placeHoldersLen = lens[1] var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) var curByte = 0 // if there are placeholders, only get up to the last complete 4 chars var len = placeHoldersLen > 0 ? validLen - 4 : validLen var i for (i = 0; i < len; i += 4) { tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)] arr[curByte++] = (tmp >> 16) & 0xFF arr[curByte++] = (tmp >> 8) & 0xFF arr[curByte++] = tmp & 0xFF } if (placeHoldersLen === 2) { tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4) arr[curByte++] = tmp & 0xFF } if (placeHoldersLen === 1) { tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2) arr[curByte++] = (tmp >> 8) & 0xFF arr[curByte++] = tmp & 0xFF } return arr } function tripletToBase64 (num) { return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F] } function encodeChunk (uint8, start, end) { var tmp var output = [] for (var i = start; i < end; i += 3) { tmp = ((uint8[i] << 16) & 0xFF0000) + ((uint8[i + 1] << 8) & 0xFF00) + (uint8[i + 2] & 0xFF) output.push(tripletToBase64(tmp)) } return output.join('') } function fromByteArray (uint8) { var tmp var len = uint8.length var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes var parts = [] var maxChunkLength = 16383 // must be multiple of 3 // go through the array every three bytes, we'll deal with trailing stuff later for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) } // pad the end with zeros, but make sure to not forget the extra bytes if (extraBytes === 1) { tmp = uint8[len - 1] parts.push( lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3F] + '==' ) } else if (extraBytes === 2) { tmp = (uint8[len - 2] << 8) + uint8[len - 1] parts.push( lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3F] + lookup[(tmp << 2) & 0x3F] + '=' ) } return parts.join('') } },{}],18:[function(require,module,exports){ 'use strict'; // For more information about browser field, check out the browser field at https://github.com/substack/browserify-handbook#browser-field. var styleElementsInsertedAtTop = []; var insertStyleElement = function(styleElement, options) { var head = document.head || document.getElementsByTagName('head')[0]; var lastStyleElementInsertedAtTop = styleElementsInsertedAtTop[styleElementsInsertedAtTop.length - 1]; options = options || {}; options.insertAt = options.insertAt || 'bottom'; if (options.insertAt === 'top') { if (!lastStyleElementInsertedAtTop) { head.insertBefore(styleElement, head.firstChild); } else if (lastStyleElementInsertedAtTop.nextSibling) { head.insertBefore(styleElement, lastStyleElementInsertedAtTop.nextSibling); } else { head.appendChild(styleElement); } styleElementsInsertedAtTop.push(styleElement); } else if (options.insertAt === 'bottom') { head.appendChild(styleElement); } else { throw new Error('Invalid value for parameter \'insertAt\'. Must be \'top\' or \'bottom\'.'); } }; module.exports = { // Create a tag with optional data attributes createLink: function(href, attributes) { var head = document.head || document.getElementsByTagName('head')[0]; var link = document.createElement('link'); link.href = href; link.rel = 'stylesheet'; for (var key in attributes) { if ( ! attributes.hasOwnProperty(key)) { continue; } var value = attributes[key]; link.setAttribute('data-' + key, value); } head.appendChild(link); }, // Create a