1730 lines
66 KiB
JavaScript
Executable file
1730 lines
66 KiB
JavaScript
Executable file
const Spicetify = {
|
|
get CosmosAPI() {return window.cosmos},
|
|
get BridgeAPI() {return window.bridge},
|
|
get LiveAPI() {return window.live},
|
|
Player: {
|
|
addEventListener: (type, callback) => {
|
|
if (!(type in Spicetify.Player.eventListeners)) {
|
|
Spicetify.Player.eventListeners[type] = [];
|
|
}
|
|
Spicetify.Player.eventListeners[type].push(callback)
|
|
},
|
|
dispatchEvent: (event) => {
|
|
if (!(event.type in Spicetify.Player.eventListeners)) {
|
|
return true;
|
|
}
|
|
const stack = Spicetify.Player.eventListeners[event.type];
|
|
for (let i = 0; i < stack.length; i++) {
|
|
if (typeof stack[i] === "function") {
|
|
stack[i](event);
|
|
}
|
|
}
|
|
return !event.defaultPrevented;
|
|
},
|
|
eventListeners: {},
|
|
seek: (p) => {
|
|
if (p <= 1) {
|
|
p = Math.round(p * Spicetify.Player.origin.duration());
|
|
}
|
|
Spicetify.Player.origin.seek(p);
|
|
},
|
|
getProgress: () => Spicetify.Player.origin.progressbar.getRealValue(),
|
|
getProgressPercent: () => Spicetify.Player.origin.progressbar.getPercentage(),
|
|
getDuration: () => Spicetify.Player.origin.duration(),
|
|
setVolume: (v) => { Spicetify.Player.origin.changeVolume(v, false) },
|
|
increaseVolume: () => { Spicetify.Player.origin.increaseVolume() },
|
|
decreaseVolume: () => { Spicetify.Player.origin.decreaseVolume() },
|
|
getVolume: () => Spicetify.Player.origin.volume(),
|
|
next: () => { Spicetify.Player.origin._doSkipToNext() },
|
|
back: () => { Spicetify.Player.origin._doSkipToPrevious() },
|
|
togglePlay: () => { Spicetify.Player.origin._doTogglePlay() },
|
|
isPlaying: () => Spicetify.Player.origin.playing(),
|
|
toggleShuffle: () => { Spicetify.Player.origin.toggleShuffle() },
|
|
getShuffle: () => Spicetify.Player.origin.shuffle(),
|
|
setShuffle: (b) => { Spicetify.Player.origin.shuffle(b) },
|
|
toggleRepeat: () => { Spicetify.Player.origin.toggleRepeat() },
|
|
getRepeat: () => Spicetify.Player.origin.repeat(),
|
|
setRepeat: (r) => { Spicetify.Player.origin.repeat(r) },
|
|
getMute: () => Spicetify.Player.origin.mute(),
|
|
toggleMute: () => { Spicetify.Player.origin._doToggleMute() },
|
|
setMute: (b) => { Spicetify.Player.origin.changeVolume(Spicetify.Player.origin._unmutedVolume, b) },
|
|
formatTime: (ms) => Spicetify.Player.origin._formatTime(ms),
|
|
getHeart: () => Spicetify.LiveAPI(Spicetify.Player.data.track.uri).get("added"),
|
|
pause: () => {Spicetify.Player.isPlaying() && Spicetify.Player.togglePlay()},
|
|
play: () => {!Spicetify.Player.isPlaying() && Spicetify.Player.togglePlay()},
|
|
removeEventListener: (type, callback) => {
|
|
if (!(type in Spicetify.Player.eventListeners)) {
|
|
return;
|
|
}
|
|
const stack = Spicetify.Player.eventListeners[type];
|
|
for (let i = 0; i < stack.length; i++) {
|
|
if (stack[i] === callback) {
|
|
stack.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
skipBack: (amount = 15e3) => {Spicetify.Player.seek(Spicetify.Player.getProgress() - amount)},
|
|
skipForward: (amount = 15e3) => {Spicetify.Player.seek(Spicetify.Player.getProgress() + amount)},
|
|
toggleHeart: () => {document.querySelector('[data-interaction-target="save-remove-button"]').click()},
|
|
},
|
|
showNotification: (text) => {
|
|
Spicetify.EventDispatcher.dispatchEvent(
|
|
new Spicetify.Event(Spicetify.Event.TYPES.SHOW_NOTIFICATION_BUBBLE, {
|
|
i18n: text,
|
|
messageHtml: text
|
|
})
|
|
);
|
|
},
|
|
test: () => {
|
|
const SPICETIFY_METHOD = [
|
|
"Player",
|
|
"addToQueue",
|
|
"BridgeAPI",
|
|
"CosmosAPI",
|
|
"Event",
|
|
"EventDispatcher",
|
|
"getAudioData",
|
|
"Keyboard",
|
|
"URI",
|
|
"LiveAPI",
|
|
"LocalStorage",
|
|
"PlaybackControl",
|
|
"Queue",
|
|
"removeFromQueue",
|
|
"showNotification",
|
|
"getAblumArtColors",
|
|
"Menu",
|
|
"ContextMenu",
|
|
"Abba",
|
|
];
|
|
|
|
const PLAYER_METHOD = [
|
|
"addEventListener",
|
|
"back",
|
|
"data",
|
|
"decreaseVolume",
|
|
"dispatchEvent",
|
|
"eventListeners",
|
|
"formatTime",
|
|
"getDuration",
|
|
"getHeart",
|
|
"getMute",
|
|
"getProgress",
|
|
"getProgressPercent",
|
|
"getRepeat",
|
|
"getShuffle",
|
|
"getVolume",
|
|
"increaseVolume",
|
|
"isPlaying",
|
|
"next",
|
|
"pause",
|
|
"play",
|
|
"removeEventListener",
|
|
"seek",
|
|
"setMute",
|
|
"setRepeat",
|
|
"setShuffle",
|
|
"setVolume",
|
|
"skipBack",
|
|
"skipForward",
|
|
"toggleHeart",
|
|
"toggleMute",
|
|
"togglePlay",
|
|
"toggleRepeat",
|
|
"toggleShuffle",
|
|
]
|
|
|
|
let count = SPICETIFY_METHOD.length;
|
|
SPICETIFY_METHOD.forEach((method) => {
|
|
if (Spicetify[method] === undefined || Spicetify[method] === null) {
|
|
console.error(`Spicetify.${method} is not available. Please open an issue in Spicetify repository to inform me about it.`)
|
|
count--;
|
|
}
|
|
})
|
|
console.log(`${count}/${SPICETIFY_METHOD.length} Spicetify methods and objects are OK.`)
|
|
|
|
count = PLAYER_METHOD.length;
|
|
PLAYER_METHOD.forEach((method) => {
|
|
if (Spicetify.Player[method] === undefined || Spicetify.Player[method] === null) {
|
|
console.error(`Spicetify.Player.${method} is not available. Please open an issue in Spicetify repository to inform me about it.`)
|
|
count--;
|
|
}
|
|
})
|
|
console.log(`${count}/${PLAYER_METHOD.length} Spicetify.Player methods and objects are OK.`)
|
|
}
|
|
}
|
|
|
|
Spicetify.URI = (function () {
|
|
/**
|
|
* Copyright (c) 2017 Spotify AB
|
|
*
|
|
* Fast base62 encoder/decoder.
|
|
*
|
|
* Usage:
|
|
*
|
|
* Base62.toHex('1C0pasJ0dS2Z46GKh2puYo') // -> '34ff970885ca8fa02c0d6e459377d5d0'
|
|
* ^^^
|
|
* |
|
|
* Length-22 base62-encoded ID.
|
|
* Lengths other than 22 or invalid base62 IDs
|
|
* are not supported.
|
|
*
|
|
* Base62.fromHex('34ff970885ca8fa02c0d6e459377d5d0') // -> '1C0pasJ0dS2Z46GKh2puYo'
|
|
* ^^^
|
|
* |
|
|
* Length-32 hex-encoded ID.
|
|
* Lengths other than 32 are not supported.
|
|
*
|
|
* Written by @ludde, programatically tested and documented by @felipec.
|
|
*/
|
|
var Base62 = (function () {
|
|
// Alphabets
|
|
var HEX16 = '0123456789abcdef';
|
|
var BASE62 =
|
|
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
|
|
// Hexadecimal fragments
|
|
var HEX256 = [];
|
|
HEX256.length = 256;
|
|
for (var i = 0; i < 256; i++) {
|
|
HEX256[i] = HEX16[i >> 4] + HEX16[i & 0xf];
|
|
}
|
|
|
|
// Look-up tables
|
|
var ID62 = [];
|
|
ID62.length = 128;
|
|
for (var i = 0; i < BASE62.length; ++i) {
|
|
ID62[BASE62.charCodeAt(i)] = i;
|
|
}
|
|
var ID16 = [];
|
|
for (var i = 0; i < 16; i++) {
|
|
ID16[HEX16.charCodeAt(i)] = i;
|
|
}
|
|
for (var i = 0; i < 6; i++) {
|
|
ID16['ABCDEF'.charCodeAt(i)] = 10 + i;
|
|
}
|
|
|
|
return {
|
|
toHex: function (s) {
|
|
if (s.length !== 22) {
|
|
// Can only parse base62 ids with length == 22.
|
|
// Invalid base62 ids will lead to garbage in the output.
|
|
return null;
|
|
}
|
|
|
|
// 1 / (2^32)
|
|
var MAX_INT_INV = 2.3283064365386963e-10;
|
|
// 2^32
|
|
var MAX_INT = 0x100000000;
|
|
// 62^3
|
|
var P62_3 = 238328;
|
|
|
|
var p0, p1, p2, p3;
|
|
var v;
|
|
// First 7 characters fit in 2^53
|
|
// prettier-ignore
|
|
p0 =
|
|
ID62[s.charCodeAt(0)] * 56800235584 + // * 62^6
|
|
ID62[s.charCodeAt(1)] * 916132832 + // * 62^5
|
|
ID62[s.charCodeAt(2)] * 14776336 + // * 62^4
|
|
ID62[s.charCodeAt(3)] * 238328 + // * 62^3
|
|
ID62[s.charCodeAt(4)] * 3844 + // * 62^2
|
|
ID62[s.charCodeAt(5)] * 62 + // * 62^1
|
|
ID62[s.charCodeAt(6)]; // * 62^0
|
|
p1 = (p0 * MAX_INT_INV) | 0;
|
|
p0 -= p1 * MAX_INT;
|
|
// 62^10 < 2^64
|
|
v =
|
|
ID62[s.charCodeAt(7)] * 3844 +
|
|
ID62[s.charCodeAt(8)] * 62 +
|
|
ID62[s.charCodeAt(9)];
|
|
(p0 = p0 * P62_3 + v), (p0 = p0 - (v = (p0 * MAX_INT_INV) | 0) * MAX_INT);
|
|
p1 = p1 * P62_3 + v;
|
|
// 62^13 < 2^96
|
|
v =
|
|
ID62[s.charCodeAt(10)] * 3844 +
|
|
ID62[s.charCodeAt(11)] * 62 +
|
|
ID62[s.charCodeAt(12)];
|
|
(p0 = p0 * P62_3 + v), (p0 = p0 - (v = (p0 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p1 = p1 * P62_3 + v), (p1 = p1 - (v = (p1 * MAX_INT_INV) | 0) * MAX_INT);
|
|
p2 = v;
|
|
// 62^16 < 2^96
|
|
v =
|
|
ID62[s.charCodeAt(13)] * 3844 +
|
|
ID62[s.charCodeAt(14)] * 62 +
|
|
ID62[s.charCodeAt(15)];
|
|
(p0 = p0 * P62_3 + v), (p0 = p0 - (v = (p0 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p1 = p1 * P62_3 + v), (p1 = p1 - (v = (p1 * MAX_INT_INV) | 0) * MAX_INT);
|
|
p2 = p2 * P62_3 + v;
|
|
// 62^19 < 2^128
|
|
v =
|
|
ID62[s.charCodeAt(16)] * 3844 +
|
|
ID62[s.charCodeAt(17)] * 62 +
|
|
ID62[s.charCodeAt(18)];
|
|
(p0 = p0 * P62_3 + v), (p0 = p0 - (v = (p0 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p1 = p1 * P62_3 + v), (p1 = p1 - (v = (p1 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p2 = p2 * P62_3 + v), (p2 = p2 - (v = (p2 * MAX_INT_INV) | 0) * MAX_INT);
|
|
p3 = v;
|
|
v =
|
|
ID62[s.charCodeAt(19)] * 3844 +
|
|
ID62[s.charCodeAt(20)] * 62 +
|
|
ID62[s.charCodeAt(21)];
|
|
(p0 = p0 * P62_3 + v), (p0 = p0 - (v = (p0 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p1 = p1 * P62_3 + v), (p1 = p1 - (v = (p1 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p2 = p2 * P62_3 + v), (p2 = p2 - (v = (p2 * MAX_INT_INV) | 0) * MAX_INT);
|
|
(p3 = p3 * P62_3 + v), (p3 = p3 - (v = (p3 * MAX_INT_INV) | 0) * MAX_INT);
|
|
if (v) {
|
|
// carry not allowed
|
|
return null;
|
|
}
|
|
// prettier-ignore
|
|
return HEX256[p3 >>> 24] + HEX256[(p3 >>> 16) & 0xFF] + HEX256[(p3 >>> 8) & 0xFF] + HEX256[(p3) & 0xFF] +
|
|
HEX256[p2 >>> 24] + HEX256[(p2 >>> 16) & 0xFF] + HEX256[(p2 >>> 8) & 0xFF] + HEX256[(p2) & 0xFF] +
|
|
HEX256[p1 >>> 24] + HEX256[(p1 >>> 16) & 0xFF] + HEX256[(p1 >>> 8) & 0xFF] + HEX256[(p1) & 0xFF] +
|
|
HEX256[p0 >>> 24] + HEX256[(p0 >>> 16) & 0xFF] + HEX256[(p0 >>> 8) & 0xFF] + HEX256[(p0) & 0xFF];
|
|
},
|
|
|
|
fromHex: function (s) {
|
|
var i;
|
|
var p0 = 0, p1 = 0, p2 = 0;
|
|
for (i = 0; i < 10; i++) p2 = p2 * 16 + ID16[s.charCodeAt(i)];
|
|
for (i = 0; i < 11; i++) p1 = p1 * 16 + ID16[s.charCodeAt(i + 10)];
|
|
for (i = 0; i < 11; i++) p0 = p0 * 16 + ID16[s.charCodeAt(i + 21)];
|
|
if (isNaN(p0 + p1 + p2)) {
|
|
return null;
|
|
}
|
|
var P16_11 = 17592186044416; // 16^11
|
|
var INV_62 = 1.0 / 62;
|
|
|
|
var acc;
|
|
var ret = '';
|
|
i = 0;
|
|
for (; i < 7; ++i) {
|
|
acc = p2;
|
|
p2 = Math.floor(acc * INV_62);
|
|
acc = (acc - p2 * 62) * P16_11 + p1;
|
|
p1 = Math.floor(acc * INV_62);
|
|
acc = (acc - p1 * 62) * P16_11 + p0;
|
|
p0 = Math.floor(acc * INV_62);
|
|
ret = BASE62[acc - p0 * 62] + ret;
|
|
}
|
|
p1 += p2 * P16_11;
|
|
for (; i < 15; ++i) {
|
|
acc = p1;
|
|
p1 = Math.floor(acc * INV_62);
|
|
acc = (acc - p1 * 62) * P16_11 + p0;
|
|
p0 = Math.floor(acc * INV_62);
|
|
ret = BASE62[acc - p0 * 62] + ret;
|
|
}
|
|
p0 += p1 * P16_11;
|
|
for (; i < 21; ++i) {
|
|
acc = p0;
|
|
p0 = Math.floor(acc * INV_62);
|
|
ret = BASE62[acc - p0 * 62] + ret;
|
|
}
|
|
return BASE62[p0] + ret;
|
|
},
|
|
|
|
// Expose the lookup tables
|
|
HEX256: HEX256, // number -> 'hh'
|
|
ID16: ID16, // hexadecimal char code -> 0..15
|
|
ID62: ID62, // base62 char code -> 0..61
|
|
};
|
|
})();
|
|
|
|
/**
|
|
* The URI prefix for URIs.
|
|
*
|
|
* @const
|
|
* @private
|
|
*/
|
|
var URI_PREFIX = 'spotify:';
|
|
|
|
/**
|
|
* The URL prefix for Play.
|
|
*
|
|
* @const
|
|
* @private
|
|
*/
|
|
var PLAY_HTTP_PREFIX = 'http://play.spotify.com/';
|
|
|
|
/**
|
|
* The HTTPS URL prefix for Play.
|
|
*
|
|
* @const
|
|
* @private
|
|
*/
|
|
var PLAY_HTTPS_PREFIX = 'https://play.spotify.com/';
|
|
|
|
/**
|
|
* The URL prefix for Open.
|
|
*
|
|
* @const
|
|
* @private
|
|
*/
|
|
var OPEN_HTTP_PREFIX = 'http://open.spotify.com/';
|
|
|
|
/**
|
|
* The HTTPS URL prefix for Open.
|
|
*
|
|
* @const
|
|
* @private
|
|
*/
|
|
var OPEN_HTTPS_PREFIX = 'https://open.spotify.com/';
|
|
|
|
var ERROR_INVALID = new TypeError('Invalid Spotify URI!');
|
|
var ERROR_NOT_IMPLEMENTED = new TypeError('Not implemented!');
|
|
|
|
|
|
/**
|
|
* The format for the URI to parse.
|
|
*
|
|
* @enum {number}
|
|
* @private
|
|
*/
|
|
var Format = {
|
|
URI: 0,
|
|
URL: 1
|
|
};
|
|
|
|
/**
|
|
* Represents the result of a URI splitting operation.
|
|
*
|
|
* @typedef {{
|
|
* format: Format,
|
|
* components: Array.<string>
|
|
* }}
|
|
* @see _splitIntoComponents
|
|
* @private
|
|
*/
|
|
var SplittedURI;
|
|
|
|
/**
|
|
* Split an string URI or HTTP/HTTPS URL into components, skipping the prefix.
|
|
*
|
|
* @param {string} str A string URI to split.
|
|
* @return {SplittedURI} The parsed URI.
|
|
* @private
|
|
*/
|
|
var _splitIntoComponents = function (str) {
|
|
var components;
|
|
var format;
|
|
var query;
|
|
var anchor;
|
|
|
|
var querySplit = str.split('?');
|
|
if (querySplit.length > 1) {
|
|
str = querySplit.shift();
|
|
query = querySplit.pop();
|
|
|
|
var queryHashSplit = query.split('#');
|
|
if (queryHashSplit.length > 1) {
|
|
query = queryHashSplit.shift();
|
|
anchor = queryHashSplit.pop();
|
|
}
|
|
|
|
query = decodeQueryString(query);
|
|
}
|
|
|
|
var hashSplit = str.split('#');
|
|
if (hashSplit.length > 1) {
|
|
// first token
|
|
str = hashSplit.shift();
|
|
// last token
|
|
anchor = hashSplit.pop();
|
|
}
|
|
|
|
if (str.indexOf(URI_PREFIX) === 0) {
|
|
components = str.slice(URI_PREFIX.length).split(':');
|
|
format = Format.URI;
|
|
} else {
|
|
// For HTTP URLs, ignore any query string argument
|
|
str = str.split('?')[0];
|
|
|
|
if (str.indexOf(PLAY_HTTP_PREFIX) === 0) {
|
|
components = str.slice(PLAY_HTTP_PREFIX.length).split('/');
|
|
} else if (str.indexOf(PLAY_HTTPS_PREFIX) === 0) {
|
|
components = str.slice(PLAY_HTTPS_PREFIX.length).split('/');
|
|
} else if (str.indexOf(OPEN_HTTP_PREFIX) === 0) {
|
|
components = str.slice(OPEN_HTTP_PREFIX.length).split('/');
|
|
} else if (str.indexOf(OPEN_HTTPS_PREFIX) === 0) {
|
|
components = str.slice(OPEN_HTTPS_PREFIX.length).split('/');
|
|
} else {
|
|
throw ERROR_INVALID;
|
|
}
|
|
format = Format.URL;
|
|
}
|
|
|
|
if (anchor) {
|
|
components.push(anchor);
|
|
}
|
|
|
|
return {
|
|
format: format,
|
|
components: components,
|
|
query: query
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Encodes a component according to a format.
|
|
*
|
|
* @param {string} component A component string.
|
|
* @param {Format} format A format.
|
|
* @return {string} An encoded component string.
|
|
* @private
|
|
*/
|
|
var _encodeComponent = function (component, format) {
|
|
component = encodeURIComponent(component);
|
|
if (format === Format.URI) {
|
|
component = component.replace(/%20/g, '+');
|
|
}
|
|
|
|
// encode characters that are not encoded by default by encodeURIComponent
|
|
// but that the Spotify URI spec encodes: !'*()
|
|
component = component.replace(/[!'()]/g, escape);
|
|
component = component.replace(/\*/g, '%2A');
|
|
|
|
return component;
|
|
};
|
|
|
|
/**
|
|
* Decodes a component according to a format.
|
|
*
|
|
* @param {string} component An encoded component string.
|
|
* @param {Format} format A format.
|
|
* @return {string} An decoded component string.
|
|
* @private
|
|
*/
|
|
var _decodeComponent = function (component, format) {
|
|
var part = format == Format.URI ? component.replace(/\+/g, '%20') : component;
|
|
return decodeURIComponent(part);
|
|
};
|
|
|
|
/**
|
|
* Returns the components of a URI as an array.
|
|
*
|
|
* @param {URI} uri A uri.
|
|
* @param {Format} format The output format.
|
|
* @return {Array.<string>} An array of uri components.
|
|
* @private
|
|
*/
|
|
var _getComponents = function (uri, format) {
|
|
var base62;
|
|
if (uri.id) {
|
|
base62 = uri._base62Id;
|
|
}
|
|
|
|
var components;
|
|
var i;
|
|
var len;
|
|
switch (uri.type) {
|
|
case URI.Type.ALBUM:
|
|
components = [URI.Type.ALBUM, base62];
|
|
if (uri.disc) {
|
|
components.push(uri.disc);
|
|
}
|
|
return components;
|
|
case URI.Type.AD:
|
|
return [URI.Type.AD, uri._base62Id];
|
|
case URI.Type.ARTIST:
|
|
return [URI.Type.ARTIST, base62];
|
|
case URI.Type.ARTIST_TOPLIST:
|
|
return [URI.Type.ARTIST, base62, URI.Type.TOP, uri.toplist];
|
|
case URI.Type.DAILY_MIX:
|
|
return [URI.Type.DAILY_MIX, base62];
|
|
case URI.Type.SEARCH:
|
|
return [URI.Type.SEARCH, _encodeComponent(uri.query, format)];
|
|
case URI.Type.TRACK:
|
|
if (uri.context || uri.play) {
|
|
base62 += encodeQueryString({
|
|
context: uri.context,
|
|
play: uri.play
|
|
});
|
|
}
|
|
if (uri.anchor) {
|
|
base62 += '#' + uri.anchor;
|
|
}
|
|
return [URI.Type.TRACK, base62];
|
|
case URI.Type.TRACKSET:
|
|
var trackIds = [];
|
|
for (i = 0, len = uri.tracks.length; i < len; i++) {
|
|
trackIds.push(uri.tracks[i]._base62Id);
|
|
}
|
|
trackIds = [trackIds.join(',')];
|
|
// Index can be 0 sometimes (required for trackset)
|
|
if (uri.index !== null) {
|
|
trackIds.push('#', uri.index);
|
|
}
|
|
return [URI.Type.TRACKSET, _encodeComponent(uri.name)].concat(trackIds);
|
|
case URI.Type.FACEBOOK:
|
|
return [URI.Type.USER, URI.Type.FACEBOOK, uri.uid];
|
|
case URI.Type.AUDIO_FILE:
|
|
return [URI.Type.AUDIO_FILE, uri.extension, uri._base62Id];
|
|
case URI.Type.FOLDER:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.FOLDER, uri._base62Id];
|
|
case URI.Type.FOLLOWERS:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.FOLLOWERS];
|
|
case URI.Type.FOLLOWING:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.FOLLOWING];
|
|
case URI.Type.PLAYLIST:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.PLAYLIST, base62];
|
|
case URI.Type.PLAYLIST_V2:
|
|
return [URI.Type.PLAYLIST, base62];
|
|
case URI.Type.STARRED:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.STARRED];
|
|
case URI.Type.TEMP_PLAYLIST:
|
|
return [URI.Type.TEMP_PLAYLIST, uri.origin, uri.data];
|
|
case URI.Type.CONTEXT_GROUP:
|
|
return [URI.Type.CONTEXT_GROUP, uri.origin, uri.name];
|
|
case URI.Type.USER_TOPLIST:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.TOP, uri.toplist];
|
|
// Legacy Toplist
|
|
case URI.Type.USER_TOP_TRACKS:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.TOPLIST];
|
|
case URI.Type.TOPLIST:
|
|
return [URI.Type.TOP, uri.toplist].concat(uri.global ? [URI.Type.GLOBAL] : ['country', uri.country]);
|
|
case URI.Type.INBOX:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.INBOX];
|
|
case URI.Type.ROOTLIST:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.ROOTLIST];
|
|
case URI.Type.PUBLISHED_ROOTLIST:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.PUBLISHED_ROOTLIST];
|
|
case URI.Type.COLLECTION_TRACK_LIST:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.COLLECTION_TRACK_LIST, base62];
|
|
case URI.Type.PROFILE:
|
|
if (uri.args && uri.args.length > 0)
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format)].concat(uri.args);
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format)];
|
|
case URI.Type.LOCAL_ARTIST:
|
|
return [URI.Type.LOCAL, _encodeComponent(uri.artist, format)];
|
|
case URI.Type.LOCAL_ALBUM:
|
|
return [URI.Type.LOCAL, _encodeComponent(uri.artist, format), _encodeComponent(uri.album, format)];
|
|
case URI.Type.LOCAL:
|
|
return [URI.Type.LOCAL,
|
|
_encodeComponent(uri.artist, format),
|
|
_encodeComponent(uri.album, format),
|
|
_encodeComponent(uri.track, format),
|
|
uri.duration];
|
|
case URI.Type.LIBRARY:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.LIBRARY].concat(uri.category ? [uri.category] : []);
|
|
case URI.Type.IMAGE:
|
|
return [URI.Type.IMAGE, uri._base62Id];
|
|
case URI.Type.MOSAIC:
|
|
components = uri.ids.slice(0);
|
|
components.unshift(URI.Type.MOSAIC);
|
|
return components;
|
|
case URI.Type.RADIO:
|
|
return [URI.Type.RADIO, uri.args];
|
|
case URI.Type.SPECIAL:
|
|
components = [URI.Type.SPECIAL];
|
|
var args = uri.args || [];
|
|
for (i = 0, len = args.length; i < len; ++i)
|
|
components.push(_encodeComponent(args[i], format));
|
|
return components;
|
|
case URI.Type.STATION:
|
|
components = [URI.Type.STATION];
|
|
var args = uri.args || [];
|
|
for (i = 0, len = args.length; i < len; i++) {
|
|
components.push(_encodeComponent(args[i], format));
|
|
}
|
|
return components;
|
|
case URI.Type.APPLICATION:
|
|
components = [URI.Type.APP, uri._base62Id];
|
|
var args = uri.args || [];
|
|
for (i = 0, len = args.length; i < len; ++i)
|
|
components.push(_encodeComponent(args[i], format));
|
|
return components;
|
|
case URI.Type.COLLECTION_ALBUM:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.COLLECTION, URI.Type.ALBUM, base62];
|
|
case URI.Type.COLLECTION_MISSING_ALBUM:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.COLLECTION, URI.Type.ALBUM, base62, 'missing'];
|
|
case URI.Type.COLLECTION_ARTIST:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.COLLECTION, URI.Type.ARTIST, base62];
|
|
case URI.Type.COLLECTION:
|
|
return [URI.Type.USER, _encodeComponent(uri.username, format), URI.Type.COLLECTION].concat(uri.category ? [uri.category] : []);
|
|
case URI.Type.EPISODE:
|
|
if (uri.context || uri.play) {
|
|
base62 += encodeQueryString({
|
|
context: uri.context,
|
|
play: uri.play
|
|
});
|
|
}
|
|
return [URI.Type.EPISODE, base62];
|
|
case URI.Type.SHOW:
|
|
return [URI.Type.SHOW, base62];
|
|
case URI.Type.CONCERT:
|
|
return [URI.Type.CONCERT, base62];
|
|
default:
|
|
throw ERROR_INVALID;
|
|
}
|
|
};
|
|
|
|
var encodeQueryString = function (values) {
|
|
var str = '?';
|
|
for (var i in values) {
|
|
if (values.hasOwnProperty(i) && values[i] !== undefined) {
|
|
if (str.length > 1) {
|
|
str += '&';
|
|
}
|
|
str += i + '=' + encodeURIComponent(values[i]);
|
|
}
|
|
}
|
|
return str;
|
|
};
|
|
|
|
var decodeQueryString = function (str) {
|
|
return str.split('&').reduce(function (object, pair) {
|
|
pair = pair.split('=');
|
|
object[pair[0]] = decodeURIComponent(pair[1]);
|
|
return object;
|
|
}, {});
|
|
};
|
|
|
|
/**
|
|
* Parses the components of a URI into a real URI object.
|
|
*
|
|
* @param {Array.<string>} components The components of the URI as a string
|
|
* array.
|
|
* @param {Format} format The format of the source string.
|
|
* @return {URI} The URI object.
|
|
* @private
|
|
*/
|
|
var _parseFromComponents = function (components, format, query) {
|
|
var _current = 0;
|
|
query = query || {};
|
|
|
|
var _getNextComponent = function () {
|
|
return components[_current++];
|
|
};
|
|
|
|
var _getIdComponent = function () {
|
|
var component = _getNextComponent();
|
|
|
|
if (component.length > 22) {
|
|
throw new Error('Invalid ID');
|
|
}
|
|
return component;
|
|
};
|
|
|
|
var _getRemainingComponents = function () {
|
|
return components.slice(_current);
|
|
};
|
|
|
|
var _getRemainingString = function () {
|
|
var separator = (format == Format.URI) ? ':' : '/';
|
|
return components.slice(_current).join(separator);
|
|
};
|
|
|
|
var part = _getNextComponent();
|
|
var id;
|
|
var i;
|
|
var len;
|
|
|
|
switch (part) {
|
|
case URI.Type.ALBUM:
|
|
return URI.albumURI(_getIdComponent(), parseInt(_getNextComponent(), 10));
|
|
case URI.Type.AD:
|
|
return URI.adURI(_getNextComponent());
|
|
case URI.Type.ARTIST:
|
|
id = _getIdComponent();
|
|
if (_getNextComponent() == URI.Type.TOP) {
|
|
return URI.artistToplistURI(id, _getNextComponent());
|
|
} else {
|
|
return URI.artistURI(id);
|
|
}
|
|
case URI.Type.AUDIO_FILE:
|
|
return URI.audioFileURI(_getNextComponent(), _getNextComponent());
|
|
case URI.Type.DAILY_MIX:
|
|
return URI.dailyMixURI(_getIdComponent());
|
|
case URI.Type.TEMP_PLAYLIST:
|
|
return URI.temporaryPlaylistURI(_getNextComponent(), _getRemainingString());
|
|
case URI.Type.PLAYLIST:
|
|
return URI.playlistV2URI(_getIdComponent());
|
|
case URI.Type.SEARCH:
|
|
return URI.searchURI(_decodeComponent(_getRemainingString(), format));
|
|
case URI.Type.TRACK:
|
|
return URI.trackURI(_getIdComponent(), _getNextComponent(), query.context, query.play);
|
|
case URI.Type.TRACKSET:
|
|
var name = _decodeComponent(_getNextComponent());
|
|
var tracksArray = _getNextComponent();
|
|
var hashSign = _getNextComponent();
|
|
var index = parseInt(_getNextComponent(), 10);
|
|
// Sanity check: %23 is URL code for "#"
|
|
if (hashSign !== '%23' || isNaN(index)) {
|
|
index = null;
|
|
}
|
|
var tracksetTracks = [];
|
|
if (tracksArray) {
|
|
tracksArray = _decodeComponent(tracksArray).split(',');
|
|
for (i = 0, len = tracksArray.length; i < len; i++) {
|
|
var trackId = tracksArray[i];
|
|
tracksetTracks.push(URI.trackURI(trackId));
|
|
}
|
|
}
|
|
return URI.tracksetURI(tracksetTracks, name, index);
|
|
case URI.Type.CONTEXT_GROUP:
|
|
return URI.contextGroupURI(_getNextComponent(), _getNextComponent());
|
|
case URI.Type.TOP:
|
|
var type = _getNextComponent();
|
|
if (_getNextComponent() == URI.Type.GLOBAL) {
|
|
return URI.toplistURI(type, null, true);
|
|
} else {
|
|
return URI.toplistURI(type, _getNextComponent(), false);
|
|
}
|
|
case URI.Type.USER:
|
|
var username = _decodeComponent(_getNextComponent(), format);
|
|
var text = _getNextComponent();
|
|
if (username == URI.Type.FACEBOOK && text != null) {
|
|
return URI.facebookURI(parseInt(text, 10));
|
|
} else if (text != null) {
|
|
switch (text) {
|
|
case URI.Type.PLAYLIST:
|
|
return URI.playlistURI(username, _getIdComponent());
|
|
case URI.Type.FOLDER:
|
|
return URI.folderURI(username, _getIdComponent());
|
|
case URI.Type.COLLECTION_TRACK_LIST:
|
|
return URI.collectionTrackList(username, _getIdComponent());
|
|
case URI.Type.COLLECTION:
|
|
var collectionItemType = _getNextComponent();
|
|
switch (collectionItemType) {
|
|
case URI.Type.ALBUM:
|
|
id = _getIdComponent();
|
|
if (_getNextComponent() === 'missing') {
|
|
return URI.collectionMissingAlbumURI(username, id);
|
|
} else {
|
|
return URI.collectionAlbumURI(username, id);
|
|
}
|
|
case URI.Type.ARTIST:
|
|
return URI.collectionArtistURI(username, _getIdComponent());
|
|
default:
|
|
return URI.collectionURI(username, collectionItemType);
|
|
}
|
|
case URI.Type.STARRED:
|
|
return URI.starredURI(username);
|
|
case URI.Type.FOLLOWERS:
|
|
return URI.followersURI(username);
|
|
case URI.Type.FOLLOWING:
|
|
return URI.followingURI(username);
|
|
case URI.Type.TOP:
|
|
return URI.userToplistURI(username, _getNextComponent());
|
|
case URI.Type.INBOX:
|
|
return URI.inboxURI(username);
|
|
case URI.Type.ROOTLIST:
|
|
return URI.rootlistURI(username);
|
|
case URI.Type.PUBLISHED_ROOTLIST:
|
|
return URI.publishedRootlistURI(username);
|
|
case URI.Type.TOPLIST:
|
|
// legacy toplist
|
|
return URI.userTopTracksURI(username);
|
|
case URI.Type.LIBRARY:
|
|
return URI.libraryURI(username, _getNextComponent());
|
|
}
|
|
}
|
|
var rem = _getRemainingComponents();
|
|
if (text != null && rem.length > 0) {
|
|
return URI.profileURI(username, [text].concat(rem));
|
|
} else if (text != null) {
|
|
return URI.profileURI(username, [text]);
|
|
} else {
|
|
return URI.profileURI(username);
|
|
}
|
|
case URI.Type.LOCAL:
|
|
var artistNameComponent = _getNextComponent();
|
|
var artistName = artistNameComponent && _decodeComponent(artistNameComponent, format);
|
|
var albumNameComponent = _getNextComponent();
|
|
var albumName = albumNameComponent && _decodeComponent(albumNameComponent, format);
|
|
var trackNameComponent = _getNextComponent();
|
|
var trackName = trackNameComponent && _decodeComponent(trackNameComponent, format);
|
|
var durationComponent = _getNextComponent();
|
|
var duration = parseInt(durationComponent, 10);
|
|
if (trackNameComponent !== undefined) {
|
|
return URI.localURI(artistName, albumName, trackName, duration);
|
|
} else if (albumNameComponent !== undefined) {
|
|
return URI.localAlbumURI(artistName, albumName);
|
|
} else {
|
|
return URI.localArtistURI(artistName);
|
|
}
|
|
case URI.Type.IMAGE:
|
|
return URI.imageURI(_getIdComponent());
|
|
case URI.Type.MOSAIC:
|
|
return URI.mosaicURI(components.slice(_current));
|
|
case URI.Type.RADIO:
|
|
return URI.radioURI(_getRemainingString());
|
|
case URI.Type.SPECIAL:
|
|
var args = _getRemainingComponents();
|
|
for (i = 0, len = args.length; i < len; ++i)
|
|
args[i] = _decodeComponent(args[i], format);
|
|
return URI.specialURI(args);
|
|
case URI.Type.STATION:
|
|
return URI.stationURI(_getRemainingComponents());
|
|
case URI.Type.EPISODE:
|
|
return URI.episodeURI(_getIdComponent(), query.context, query.play);
|
|
case URI.Type.SHOW:
|
|
return URI.showURI(_getIdComponent());
|
|
case URI.Type.CONCERT:
|
|
return URI.concertURI(_getIdComponent());
|
|
case '':
|
|
break;
|
|
default:
|
|
if (part === URI.Type.APP) {
|
|
id = _getNextComponent();
|
|
} else {
|
|
id = part;
|
|
}
|
|
var decodedId = _decodeComponent(id, format);
|
|
if (_encodeComponent(decodedId, format) !== id) {
|
|
break;
|
|
}
|
|
var args = _getRemainingComponents();
|
|
for (i = 0, len = args.length; i < len; ++i)
|
|
args[i] = _decodeComponent(args[i], format);
|
|
return URI.applicationURI(decodedId, args);
|
|
}
|
|
|
|
throw ERROR_INVALID;
|
|
};
|
|
|
|
/**
|
|
* A class holding information about a uri.
|
|
*
|
|
* @constructor
|
|
* @param {URI.Type} type
|
|
* @param {Object} props
|
|
*/
|
|
function URI(type, props) {
|
|
this.type = type;
|
|
|
|
// Merge properties into URI object.
|
|
for (var prop in props) {
|
|
if (typeof props[prop] == 'function') {
|
|
continue;
|
|
}
|
|
this[prop] = props[prop];
|
|
}
|
|
}
|
|
|
|
// Lazy convert the id to hexadecimal only when requested
|
|
Object.defineProperty(URI.prototype, 'id', {
|
|
get: function () {
|
|
if (!this._hexId) {
|
|
this._hexId = this._base62Id ? URI.idToHex(this._base62Id) : undefined;
|
|
}
|
|
return this._hexId;
|
|
},
|
|
set: function (id) {
|
|
this._base62Id = id ? URI.hexToId(id) : undefined;
|
|
this._hexId = undefined;
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
URI.prototype.toAppType = function () {
|
|
if (this.type == URI.Type.APPLICATION) {
|
|
return URI.applicationURI(this.id, this.args);
|
|
} else {
|
|
var components = _getComponents(this, Format.URL);
|
|
var id = components.shift();
|
|
var len = components.length;
|
|
if (len) {
|
|
while (len--) {
|
|
components[len] = _decodeComponent(components[len], Format.URL);
|
|
}
|
|
}
|
|
if (this.type == URI.Type.RADIO) {
|
|
components = components.shift().split(':');
|
|
}
|
|
var result = URI.applicationURI(id, components);
|
|
return result;
|
|
}
|
|
};
|
|
URI.prototype.toRealType = function () {
|
|
if (this.type == URI.Type.APPLICATION) {
|
|
return _parseFromComponents([this.id].concat(this.args), Format.URI);
|
|
} else {
|
|
return new URI(null, this);
|
|
}
|
|
};
|
|
URI.prototype.toURI = function () {
|
|
return URI_PREFIX + _getComponents(this, Format.URI).join(':');
|
|
};
|
|
URI.prototype.toString = function () {
|
|
return this.toURI();
|
|
};
|
|
URI.prototype.toURLPath = function (opt_leadingSlash) {
|
|
var components = _getComponents(this, Format.URL);
|
|
if (components[0] === URI.Type.APP) {
|
|
components.shift();
|
|
}
|
|
|
|
// Some URIs are allowed to have empty components. It should be investigated
|
|
// whether we need to strip empty components at all from any URIs. For now,
|
|
// we check specifically for tracksets and local tracks and strip empty
|
|
// components for all other URIs.
|
|
//
|
|
// For tracksets, it's permissible to have a path that looks like
|
|
// 'trackset//trackURI' because the identifier parameter for a trackset can
|
|
// be blank. For local tracks, some metadata can be missing, like missing
|
|
// album name would be 'spotify:local:artist::track:duration'.
|
|
var isTrackset = components[0] === URI.Type.TRACKSET;
|
|
var isLocalTrack = components[0] === URI.Type.LOCAL;
|
|
var shouldStripEmptyComponents = !isTrackset && !isLocalTrack;
|
|
|
|
if (shouldStripEmptyComponents) {
|
|
var _temp = [];
|
|
for (var i = 0, l = components.length; i < l; i++) {
|
|
var component = components[i];
|
|
if (!!component) {
|
|
_temp.push(component);
|
|
}
|
|
}
|
|
components = _temp;
|
|
}
|
|
var path = components.join('/');
|
|
return opt_leadingSlash ? '/' + path : path;
|
|
};
|
|
URI.prototype.toPlayURL = function () {
|
|
return PLAY_HTTPS_PREFIX + this.toURLPath();
|
|
};
|
|
URI.prototype.toURL = function () {
|
|
return this.toPlayURL();
|
|
};
|
|
URI.prototype.toOpenURL = function () {
|
|
return OPEN_HTTPS_PREFIX + this.toURLPath();
|
|
};
|
|
URI.prototype.toSecurePlayURL = function () {
|
|
return this.toPlayURL();
|
|
};
|
|
URI.prototype.toSecureURL = function () {
|
|
return this.toPlayURL();
|
|
};
|
|
URI.prototype.toSecureOpenURL = function () {
|
|
return this.toOpenURL();
|
|
};
|
|
URI.prototype.idToByteString = function () {
|
|
var hexId = Base62.toHex(this._base62Id);
|
|
if (!hexId) {
|
|
var zero = '';
|
|
for (var i = 0; i < 16; i++) {
|
|
zero += String.fromCharCode(0);
|
|
}
|
|
return zero;
|
|
}
|
|
var data = '';
|
|
for (var i = 0; i < 32; i += 2) {
|
|
var upper = Base62.ID16[hexId.charCodeAt(i)];
|
|
var lower = Base62.ID16[hexId.charCodeAt(i + 1)];
|
|
var byte = (upper << 4) + lower;
|
|
data += String.fromCharCode(byte);
|
|
}
|
|
return data;
|
|
};
|
|
|
|
URI.prototype.getPath = function () {
|
|
var uri = this.toString().replace(/[#?].*/, '');
|
|
return uri;
|
|
}
|
|
|
|
URI.prototype.getBase62Id = function () {
|
|
return this._base62Id;
|
|
}
|
|
URI.prototype.isSameIdentity = function (uri) {
|
|
var uriObject = URI.from(uri);
|
|
if (!uriObject) return false;
|
|
if (this.toString() === uri.toString()) return true;
|
|
if (
|
|
(this.type === URI.Type.PLAYLIST || this.type === URI.Type.PLAYLIST_V2) &&
|
|
(uriObject.type === URI.Type.PLAYLIST || uriObject.type === URI.Type.PLAYLIST_V2)
|
|
) {
|
|
return this.id === uriObject.id;
|
|
} else if (this.type === URI.Type.STATION && uriObject.type === URI.Type.STATION) {
|
|
var thisStationContextUriObject = _parseFromComponents(this.args, Format.URI);
|
|
return !!thisStationContextUriObject &&
|
|
thisStationContextUriObject.isSameIdentity(
|
|
_parseFromComponents(uriObject.args, Format.URI)
|
|
);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
URI.Type = {
|
|
EMPTY: 'empty',
|
|
ALBUM: 'album',
|
|
AD: 'ad',
|
|
/** URI particle; not an actual URI. */
|
|
APP: 'app',
|
|
APPLICATION: 'application',
|
|
ARTIST: 'artist',
|
|
ARTIST_TOPLIST: 'artist-toplist',
|
|
AUDIO_FILE: 'audiofile',
|
|
COLLECTION: 'collection',
|
|
COLLECTION_ALBUM: 'collection-album',
|
|
COLLECTION_MISSING_ALBUM: 'collection-missing-album',
|
|
COLLECTION_ARTIST: 'collection-artist',
|
|
CONTEXT_GROUP: 'context-group',
|
|
DAILY_MIX: 'dailymix',
|
|
EPISODE: 'episode',
|
|
/** URI particle; not an actual URI. */
|
|
FACEBOOK: 'facebook',
|
|
FOLDER: 'folder',
|
|
FOLLOWERS: 'followers',
|
|
FOLLOWING: 'following',
|
|
/** URI particle; not an actual URI. */
|
|
GLOBAL: 'global',
|
|
IMAGE: 'image',
|
|
INBOX: 'inbox',
|
|
LOCAL_ARTIST: 'local-artist',
|
|
LOCAL_ALBUM: 'local-album',
|
|
LOCAL: 'local',
|
|
LIBRARY: 'library',
|
|
MOSAIC: 'mosaic',
|
|
PLAYLIST: 'playlist',
|
|
/** Only used for URI classification. Not a valid URI fragment. */
|
|
PLAYLIST_V2: 'playlist-v2',
|
|
PROFILE: 'profile',
|
|
PUBLISHED_ROOTLIST: 'published-rootlist',
|
|
RADIO: 'radio',
|
|
ROOTLIST: 'rootlist',
|
|
COLLECTION_TRACK_LIST: 'collectiontracklist',
|
|
SEARCH: 'search',
|
|
SHOW: 'show',
|
|
CONCERT: 'concert',
|
|
SPECIAL: 'special',
|
|
STARRED: 'starred',
|
|
STATION: 'station',
|
|
TEMP_PLAYLIST: 'temp-playlist',
|
|
/** URI particle; not an actual URI. */
|
|
TOP: 'top',
|
|
TOPLIST: 'toplist',
|
|
TRACK: 'track',
|
|
TRACKSET: 'trackset',
|
|
/** URI particle; not an actual URI. */
|
|
USER: 'user',
|
|
USER_TOPLIST: 'user-toplist',
|
|
USER_TOP_TRACKS: 'user-top-tracks',
|
|
/** Deprecated contant. Please use USER_TOP_TRACKS. */
|
|
USET_TOP_TRACKS: 'user-top-tracks'
|
|
};
|
|
URI.fromString = function (str) {
|
|
var splitted = _splitIntoComponents(str);
|
|
return _parseFromComponents(splitted.components, splitted.format, splitted.query);
|
|
};
|
|
URI.from = function (value) {
|
|
try {
|
|
if (value instanceof URI) {
|
|
return value;
|
|
}
|
|
if (typeof value == 'object' && value.type) {
|
|
return new URI(null, value);
|
|
}
|
|
return URI.fromString(value.toString());
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
};
|
|
URI.fromByteString = function (type, idByteString, opt_args) {
|
|
while (idByteString.length != 16) {
|
|
idByteString = String.fromCharCode(0) + idByteString;
|
|
}
|
|
var hexId = '';
|
|
for (var i = 0; i < idByteString.length; i++) {
|
|
var byte = idByteString.charCodeAt(i);
|
|
hexId += Base62.HEX256[byte];
|
|
}
|
|
var id = Base62.fromHex(hexId);
|
|
var args = opt_args || {};
|
|
args.id = id;
|
|
return new URI(type, args);
|
|
};
|
|
URI.clone = function (uri) {
|
|
if (!(uri instanceof URI)) {
|
|
return null;
|
|
}
|
|
return new URI(null, uri);
|
|
};
|
|
URI.getCanonicalUsername = function (username) {
|
|
return _encodeComponent(username, Format.URI);
|
|
};
|
|
URI.getDisplayUsername = function (username) {
|
|
return _decodeComponent(username, Format.URI);
|
|
};
|
|
URI.idToHex = function (id) {
|
|
if (id.length == 22) {
|
|
return Base62.toHex(id);
|
|
}
|
|
return id;
|
|
};
|
|
URI.hexToId = function (hex) {
|
|
if (hex.length == 32) {
|
|
return Base62.fromHex(hex);
|
|
}
|
|
return hex;
|
|
};
|
|
URI.emptyURI = function () {
|
|
return new URI(URI.Type.EMPTY, {});
|
|
};
|
|
URI.albumURI = function (id, disc) {
|
|
return new URI(URI.Type.ALBUM, { id: id, disc: disc });
|
|
};
|
|
URI.adURI = function (id) {
|
|
return new URI(URI.Type.AD, { id: id });
|
|
};
|
|
URI.audioFileURI = function (extension, id) {
|
|
return new URI(URI.Type.AUDIO_FILE, { id: id, extension: extension });
|
|
};
|
|
URI.artistURI = function (id) {
|
|
return new URI(URI.Type.ARTIST, { id: id });
|
|
};
|
|
URI.artistToplistURI = function (id, toplist) {
|
|
return new URI(URI.Type.ARTIST_TOPLIST, { id: id, toplist: toplist });
|
|
};
|
|
URI.dailyMixURI = function (id) {
|
|
return new URI(URI.Type.DAILY_MIX, { id: id });
|
|
};
|
|
URI.searchURI = function (query) {
|
|
return new URI(URI.Type.SEARCH, { query: query });
|
|
};
|
|
URI.trackURI = function (id, anchor, context, play) {
|
|
return new URI(URI.Type.TRACK, {
|
|
id: id,
|
|
anchor: anchor,
|
|
context: context ? URI.fromString(context) : context,
|
|
play: play
|
|
});
|
|
};
|
|
URI.tracksetURI = function (tracks, name, index) {
|
|
return new URI(URI.Type.TRACKSET, {
|
|
tracks: tracks,
|
|
name: name || '',
|
|
index: isNaN(index) ? null : index
|
|
});
|
|
};
|
|
URI.facebookURI = function (uid) {
|
|
return new URI(URI.Type.FACEBOOK, { uid: uid });
|
|
};
|
|
URI.followersURI = function (username) {
|
|
return new URI(URI.Type.FOLLOWERS, { username: username });
|
|
};
|
|
URI.followingURI = function (username) {
|
|
return new URI(URI.Type.FOLLOWING, { username: username });
|
|
};
|
|
URI.playlistURI = function (username, id) {
|
|
return new URI(URI.Type.PLAYLIST, { username: username, id: id });
|
|
};
|
|
URI.playlistV2URI = function (id) {
|
|
return new URI(URI.Type.PLAYLIST_V2, { id: id });
|
|
};
|
|
URI.folderURI = function (username, id) {
|
|
return new URI(URI.Type.FOLDER, { username: username, id: id });
|
|
};
|
|
URI.collectionTrackList = function (username, id) {
|
|
return new URI(URI.Type.COLLECTION_TRACK_LIST, { username: username, id: id });
|
|
};
|
|
URI.starredURI = function (username) {
|
|
return new URI(URI.Type.STARRED, { username: username });
|
|
};
|
|
URI.userToplistURI = function (username, toplist) {
|
|
return new URI(URI.Type.USER_TOPLIST, { username: username, toplist: toplist });
|
|
};
|
|
URI.userTopTracksURI = function (username) {
|
|
return new URI(URI.Type.USER_TOP_TRACKS, { username: username });
|
|
};
|
|
URI.toplistURI = function (toplist, country, global) {
|
|
return new URI(URI.Type.TOPLIST, { toplist: toplist, country: country, global: !!global });
|
|
};
|
|
URI.inboxURI = function (username) {
|
|
return new URI(URI.Type.INBOX, { username: username });
|
|
};
|
|
URI.rootlistURI = function (username) {
|
|
return new URI(URI.Type.ROOTLIST, { username: username });
|
|
};
|
|
URI.publishedRootlistURI = function (username) {
|
|
return new URI(URI.Type.PUBLISHED_ROOTLIST, { username: username });
|
|
};
|
|
URI.localArtistURI = function (artist) {
|
|
return new URI(URI.Type.LOCAL_ARTIST, { artist: artist });
|
|
};
|
|
URI.localAlbumURI = function (artist, album) {
|
|
return new URI(URI.Type.LOCAL_ALBUM, { artist: artist, album: album });
|
|
};
|
|
URI.localURI = function (artist, album, track, duration) {
|
|
return new URI(URI.Type.LOCAL, {
|
|
artist: artist,
|
|
album: album,
|
|
track: track,
|
|
duration: duration
|
|
});
|
|
};
|
|
URI.libraryURI = function (username, category) {
|
|
return new URI(URI.Type.LIBRARY, { username: username, category: category });
|
|
};
|
|
URI.collectionURI = function (username, category) {
|
|
return new URI(URI.Type.COLLECTION, { username: username, category: category });
|
|
};
|
|
URI.temporaryPlaylistURI = function (origin, data) {
|
|
return new URI(URI.Type.TEMP_PLAYLIST, { origin: origin, data: data });
|
|
};
|
|
URI.contextGroupURI = function (origin, name) {
|
|
return new URI(URI.Type.CONTEXT_GROUP, { origin: origin, name: name });
|
|
};
|
|
URI.profileURI = function (username, args) {
|
|
return new URI(URI.Type.PROFILE, { username: username, args: args });
|
|
};
|
|
URI.imageURI = function (id) {
|
|
return new URI(URI.Type.IMAGE, { id: id });
|
|
};
|
|
URI.mosaicURI = function (ids) {
|
|
return new URI(URI.Type.MOSAIC, { ids: ids });
|
|
};
|
|
URI.radioURI = function (args) {
|
|
args = typeof args === 'undefined' ? '' : args;
|
|
return new URI(URI.Type.RADIO, { args: args });
|
|
};
|
|
URI.specialURI = function (args) {
|
|
args = typeof args === 'undefined' ? [] : args;
|
|
return new URI(URI.Type.SPECIAL, { args: args });
|
|
};
|
|
URI.stationURI = function (args) {
|
|
args = typeof args === 'undefined' ? [] : args;
|
|
return new URI(URI.Type.STATION, { args: args });
|
|
};
|
|
URI.applicationURI = function (id, args) {
|
|
args = typeof args === 'undefined' ? [] : args;
|
|
return new URI(URI.Type.APPLICATION, { id: id, args: args });
|
|
};
|
|
URI.collectionAlbumURI = function (username, id) {
|
|
return new URI(URI.Type.COLLECTION_ALBUM, { username: username, id: id });
|
|
};
|
|
URI.collectionMissingAlbumURI = function (username, id) {
|
|
return new URI(URI.Type.COLLECTION_MISSING_ALBUM, { username: username, id: id });
|
|
};
|
|
URI.collectionArtistURI = function (username, id) {
|
|
return new URI(URI.Type.COLLECTION_ARTIST, { username: username, id: id });
|
|
};
|
|
URI.episodeURI = function (id, context, play) {
|
|
return new URI(URI.Type.EPISODE, {
|
|
id: id,
|
|
context: context ? URI.fromString(context) : context,
|
|
play: play
|
|
});
|
|
};
|
|
URI.showURI = function (id) {
|
|
return new URI(URI.Type.SHOW, { id: id });
|
|
};
|
|
URI.concertURI = function (id) {
|
|
return new URI(URI.Type.CONCERT, { id: id });
|
|
};
|
|
|
|
URI.isAlbum = function (uri) { return (URI.from(uri) || {}).type === URI.Type.ALBUM; };
|
|
URI.isAd = function (uri) { return (URI.from(uri) || {}).type === URI.Type.AD; };
|
|
URI.isApplication = function (uri) { return (URI.from(uri) || {}).type === URI.Type.APPLICATION; };
|
|
URI.isArtist = function (uri) { return (URI.from(uri) || {}).type === URI.Type.ARTIST; };
|
|
URI.isCollection = function (uri) { return (URI.from(uri) || {}).type === URI.Type.COLLECTION; };
|
|
URI.isCollectionAlbum = function (uri) { return (URI.from(uri) || {}).type === URI.Type.COLLECTION_ALBUM; };
|
|
URI.isCollectionArtist = function (uri) { return (URI.from(uri) || {}).type === URI.Type.COLLECTION_ARTIST; };
|
|
URI.isDailyMix = function (uri) { return (URI.from(uri) || {}).type === URI.Type.DAILY_MIX; };
|
|
URI.isEpisode = function (uri) { return (URI.from(uri) || {}).type === URI.Type.EPISODE; };
|
|
URI.isFacebook = function (uri) { return (URI.from(uri) || {}).type === URI.Type.FACEBOOK; };
|
|
URI.isFolder = function (uri) { return (URI.from(uri) || {}).type === URI.Type.FOLDER; };
|
|
URI.isLocalArtist = function (uri) { return (URI.from(uri) || {}).type === URI.Type.LOCAL_ARTIST; };
|
|
URI.isLocalAlbum = function (uri) { return (URI.from(uri) || {}).type === URI.Type.LOCAL_ALBUM; };
|
|
URI.isLocalTrack = function (uri) { return (URI.from(uri) || {}).type === URI.Type.LOCAL; };
|
|
URI.isMosaic = function (uri) { return (URI.from(uri) || {}).type === URI.Type.MOSAIC; };
|
|
URI.isPlaylistV1 = function (uri) { return (URI.from(uri) || {}).type === URI.Type.PLAYLIST; };
|
|
URI.isPlaylistV2 = function (uri) { return (URI.from(uri) || {}).type === URI.Type.PLAYLIST_V2; };
|
|
URI.isRadio = function (uri) { return (URI.from(uri) || {}).type === URI.Type.RADIO; };
|
|
URI.isRootlist = function (uri) { return (URI.from(uri) || {}).type === URI.Type.ROOTLIST; };
|
|
URI.isSearch = function (uri) { return (URI.from(uri) || {}).type === URI.Type.SEARCH; };
|
|
URI.isShow = function (uri) { return (URI.from(uri) || {}).type === URI.Type.SHOW; };
|
|
URI.isConcert = function (uri) { return (URI.from(uri) || {}).type === URI.Type.CONCERT; };
|
|
URI.isStation = function (uri) { return (URI.from(uri) || {}).type === URI.Type.STATION; };
|
|
URI.isTrack = function (uri) { return (URI.from(uri) || {}).type === URI.Type.TRACK; };
|
|
URI.isProfile = function (uri) { return (URI.from(uri) || {}).type === URI.Type.PROFILE; };
|
|
URI.isPlaylistV1OrV2 = function (uri) {
|
|
var uriObject = URI.from(uri);
|
|
return !!uriObject && (uriObject.type === URI.Type.PLAYLIST || uriObject.type === URI.Type.PLAYLIST_V2);
|
|
};
|
|
|
|
/**
|
|
* Export public interface
|
|
*/
|
|
return URI;
|
|
})();
|
|
|
|
Spicetify.getAudioData = (uri) => {
|
|
return new Promise((resolve, reject) => {
|
|
uri = uri || Spicetify.Player.data.track.uri;
|
|
const uriObj = Spicetify.URI.from(uri);
|
|
if (!uriObj && uriObj.Type !== Spicetify.URI.Type.TRACK) {
|
|
reject("URI is invalid.");
|
|
return;
|
|
}
|
|
|
|
Spicetify.CosmosAPI.resolver.get(
|
|
`hm://audio-attributes/v1/audio-analysis/${uriObj.getBase62Id()}`,
|
|
(error, payload) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
resolve(payload.getJSONBody());
|
|
})
|
|
});
|
|
}
|
|
|
|
Spicetify.colorExtractor = (uri) => {
|
|
return new Promise((resolve, reject) => {
|
|
Spicetify.CosmosAPI.resolver.get(
|
|
`hm://colorextractor/v1/extract-presets?uri=${uri}&format=json`,
|
|
(error, payload) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
const body = payload.getJSONBody();
|
|
if (body.entries && body.entries.length) {
|
|
const list = {};
|
|
for (const color of body.entries[0].color_swatches) {
|
|
list[color.preset] = `#${color.color.toString(16).padStart(6, "0")}`;
|
|
}
|
|
resolve(list);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
Spicetify.getAblumArtColors = async (uri) => {
|
|
uri = uri || Spicetify.Player.data.track.metadata.album_uri;
|
|
return await Spicetify.colorExtractor(uri);
|
|
}
|
|
|
|
Spicetify.Menu = (function() {
|
|
const collection = new Set();
|
|
|
|
const _hook = function(menuReact, itemReact, subMenuReact ) {
|
|
function createSingleItem(item) {
|
|
return menuReact.createElement(itemReact, {
|
|
label: item.name,
|
|
isChecked: item.isEnabled,
|
|
name: "spicetify-hook",
|
|
onClick: item.onClick,
|
|
});
|
|
}
|
|
|
|
const result = [];
|
|
|
|
for (const item of collection) {
|
|
let reactComp;
|
|
if (item.subItems) {
|
|
reactComp = menuReact.createElement(itemReact, { label: item.name },
|
|
menuReact.createElement(subMenuReact, { isSubmenu: true },
|
|
item.subItems.map(createSingleItem)
|
|
)
|
|
);
|
|
} else {
|
|
reactComp = createSingleItem(item);
|
|
}
|
|
result.push(reactComp);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
class Item {
|
|
constructor(name, isEnabled, onClick) {
|
|
this.name = name;
|
|
this.isEnabled = isEnabled;
|
|
this.onClick = () => {onClick(this)};
|
|
}
|
|
setState(isEnabled) {
|
|
this.isEnabled = isEnabled;
|
|
}
|
|
setName(name) {
|
|
this.name = name
|
|
}
|
|
register() {
|
|
collection.add(this);
|
|
}
|
|
deregister() {
|
|
collection.delete(this);
|
|
}
|
|
}
|
|
|
|
class SubMenu {
|
|
constructor(name, subItems) {
|
|
this.name = name;
|
|
this.subItems = subItems;
|
|
}
|
|
setName(name) {
|
|
this.name = name;
|
|
}
|
|
register() {
|
|
collection.add(this);
|
|
}
|
|
deregister() {
|
|
collection.delete(this);
|
|
}
|
|
}
|
|
|
|
return { Item, SubMenu, _hook }
|
|
})();
|
|
|
|
Spicetify.ContextMenu = (function () {
|
|
let itemList = new Set();
|
|
const iconList = ["add-to-playlist", "add-to-queue", "addfollow", "addfollowers", "addsuggestedsong", "airplay", "album", "album-contained", "arrow-down", "arrow-left", "arrow-right", "arrow-up", "artist", "artist-active", "attach", "available-offline", "ban", "ban-active", "block", "bluetooth", "browse", "browse-active", "camera", "carplay", "chart-down", "chart-new", "chart-up", "check", "check-alt", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "chromecast-connected", "chromecast-connecting-one", "chromecast-connecting-three", "chromecast-connecting-two", "chromecast-disconnected", "collaborative-playlist", "collection", "collection-active", "connect-to-devices", "copy", "destination-pin", "device-arm", "device-car", "device-computer", "device-mobile", "device-multispeaker", "device-other", "device-speaker", "device-tablet", "device-tv", "devices", "devices-alt", "discover", "download", "downloaded", "drag-and-drop", "edit", "email", "events", "facebook", "facebook-messenger", "filter", "flag", "follow", "fullscreen", "games-console", "gears", "googleplus", "grid-view", "headphones", "heart", "heart-active", "helpcircle", "highlight", "home", "home-active", "inbox", "info", "instagram", "library", "lightning", "line", "list-view", "localfile", "locked", "locked-active", "lyrics", "make—available-offline", "menu", "messages", "mic", "minimise", "mix", "more", "more-android", "new-spotify-connect", "new-volume", "newradio", "nikeplus", "notifications", "now-playing", "now-playing-active", "offline", "offline-sync", "pause", "payment", "paymenthistory", "play", "playback-speed-0point5x", "playback-speed-0point8x", "playback-speed-1point2x", "playback-speed-1point5x", "playback-speed-1x", "playback-speed-2x", "playback-speed-3x", "playlist", "playlist-folder", "plus", "plus-2px", "plus-alt", "podcasts", "podcasts-active", "public", "queue", "radio", "radio-active", "radioqueue", "redeem", "refresh", "released", "repeat", "repeatonce", "report-abuse", "running", "search", "search-active", "sendto", "share", "share-android", "sharetofollowers", "shows", "shuffle", "skip-back", "skip-forward", "skipback15", "skipforward15", "sleeptimer", "sms", "sort", "sortdown", "sortup", "spotify-connect", "spotify-connect-alt", "spotifylogo", "spotifypremium", "star", "star-alt", "subtitles", "tag", "thumbs-down", "thumbs-up", "time", "topcountry", "track", "trending", "trending-active", "tumblr", "twitter", "user", "user-active", "user-alt", "user-circle", "video", "volume", "volume-off", "volume-onewave", "volume-twowave", "warning", "watch", "whatsapp", "x", "settings"];
|
|
|
|
class Item {
|
|
constructor(name, onClick, shouldAdd = (uris) => true, icon = undefined) {
|
|
this.name = name;
|
|
this.onClick = onClick;
|
|
this.shouldAdd = shouldAdd;
|
|
if (icon) this.icon = icon;
|
|
}
|
|
set name(text) {
|
|
if (typeof text !== "string") {
|
|
throw "Spicetify.ContextMenu.Item: name is not a string";
|
|
}
|
|
this._name = text;
|
|
}
|
|
set shouldAdd(func) {
|
|
if (typeof func == "function") {
|
|
this._shouldAdd = func.bind(this);
|
|
} else {
|
|
throw "Spicetify.ContextMenu.Item: shouldAdd is not a function";
|
|
}
|
|
}
|
|
set onClick(func) {
|
|
if (typeof func == "function") {
|
|
this._onClick = func.bind(this);
|
|
} else {
|
|
throw "Spicetify.ContextMenu.Item: onClick is not a function";
|
|
}
|
|
}
|
|
set icon(name) {
|
|
if (!name) {
|
|
this._icon = null;
|
|
return;
|
|
}
|
|
if (!Item.iconList.includes(name)) {
|
|
throw `Spicetify.ContextMenu.Item: "${name}" is not a valid icon name.`;
|
|
}
|
|
this._icon = {
|
|
type: "spoticon",
|
|
value: name,
|
|
};
|
|
}
|
|
register() {
|
|
itemList.add(this);
|
|
}
|
|
deregister() {
|
|
itemList.remove(this);
|
|
}
|
|
}
|
|
|
|
Item.iconList = iconList;
|
|
|
|
class SubMenu {
|
|
constructor(name, items, shouldAdd = (uris) => true, icon = undefined) {
|
|
this.name = name;
|
|
this.items = items;
|
|
this.shouldAdd = shouldAdd;
|
|
if (icon) this.icon = icon;
|
|
}
|
|
set name(text) {
|
|
if (typeof text !== "string") {
|
|
throw "Spicetify.ContextMenu.SubMenu: name is not a string";
|
|
}
|
|
this._name = text;
|
|
}
|
|
set items(items) {
|
|
this._items = new Set(items);
|
|
}
|
|
addItem(item) {
|
|
this._items.add(item);
|
|
}
|
|
removeItem(item) {
|
|
this._items.remove(item);
|
|
}
|
|
set shouldAdd(func) {
|
|
if (typeof func == "function") {
|
|
this._shouldAdd = func.bind(this);
|
|
} else {
|
|
throw "Spicetify.ContextMenu.SubMenu: shouldAdd is not a function";
|
|
}
|
|
}
|
|
set icon(name) {
|
|
if (!name) {
|
|
this._icon = null;
|
|
return;
|
|
}
|
|
if (!SubMenu.iconList.includes()) {
|
|
throw `Spicetify.ContextMenu.SubMenu: "${name}" is not a valid icon name.`;
|
|
}
|
|
this._icon = {
|
|
type: "spoticon",
|
|
value: name,
|
|
};
|
|
}
|
|
register() {
|
|
itemList.add(this);
|
|
}
|
|
deregister() {
|
|
itemList.remove(this);
|
|
}
|
|
}
|
|
|
|
SubMenu.iconList = iconList;
|
|
|
|
function _addItems(contextMenuInstance, uris) {
|
|
for (const item of itemList) {
|
|
if (!item._shouldAdd(uris)) {
|
|
continue;
|
|
}
|
|
|
|
if (item._items) {
|
|
const subItemsList = []
|
|
for (const subItem of item._items) {
|
|
subItemsList.push({
|
|
fn: () => {
|
|
subItem._onClick(uris);
|
|
contextMenuInstance.hide();
|
|
},
|
|
icon: subItem._icon,
|
|
id: "",
|
|
text: subItem._name,
|
|
});
|
|
}
|
|
|
|
contextMenuInstance.addItem({
|
|
icon: item._icon,
|
|
id: "",
|
|
items: subItemsList,
|
|
text: item._name,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
contextMenuInstance.addItem({
|
|
fn: () => {
|
|
item._onClick(uris);
|
|
contextMenuInstance.hide();
|
|
},
|
|
icon: item._icon,
|
|
id: "",
|
|
text: item._name,
|
|
})
|
|
}
|
|
}
|
|
|
|
return { Item, SubMenu, _addItems };
|
|
})();
|
|
|
|
Spicetify.Abba = (function() {
|
|
const STORAGE_KEY = "Spicetify.OverrideAbbaFlags";
|
|
const STORAGE = window.top.localStorage;
|
|
|
|
const storedOverrideFlags = STORAGE.getItem(STORAGE_KEY);
|
|
window.__spotify.product_state.abbaOverrides = storedOverrideFlags;
|
|
|
|
let _overrideFlags;
|
|
if (storedOverrideFlags) {
|
|
try {
|
|
_overrideFlags = JSON.parse(storedOverrideFlags);
|
|
} catch {
|
|
_overrideFlags = {};
|
|
}
|
|
} else {
|
|
_overrideFlags = {};
|
|
}
|
|
|
|
function getFlag(name, callback) {
|
|
if (typeof callback !== "function") {
|
|
console.error("callback is not a function");
|
|
return;
|
|
}
|
|
if (typeof name === "string") {
|
|
name = [name];
|
|
}
|
|
Spicetify.CosmosAPI.resolver.post({
|
|
url: "sp://abba/v1/flags",
|
|
body: { flags: name }
|
|
}, (error, res) => {
|
|
if (error) {
|
|
console.error(error);
|
|
return;
|
|
}
|
|
callback(res.getJSONBody().flags);
|
|
});
|
|
}
|
|
|
|
function getInUseFlags(callback) {
|
|
if (typeof callback !== "function") {
|
|
console.error("callback is not a function");
|
|
return;
|
|
}
|
|
Spicetify.CosmosAPI.resolver.get("sp://abba/v1/requested_flag_names", (error, res) => {
|
|
if (error) {
|
|
console.error(error);
|
|
return;
|
|
}
|
|
callback(res.getJSONBody());
|
|
});
|
|
}
|
|
|
|
function getAllFlags(callback) {
|
|
if (typeof callback !== "function") {
|
|
console.error("callback is not a function");
|
|
return;
|
|
}
|
|
Spicetify.CosmosAPI.resolver.get("sp://abba/v1/all_flags", (error, res) => {
|
|
if (error) {
|
|
console.error(error);
|
|
return;
|
|
}
|
|
callback(res.getJSONBody());
|
|
});
|
|
}
|
|
|
|
function getOverrideFlags() {
|
|
return _overrideFlags;
|
|
}
|
|
|
|
function _syncStorage() {
|
|
const stringified = JSON.stringify(_overrideFlags);
|
|
STORAGE.setItem(STORAGE_KEY, stringified);
|
|
window.__spotify.product_state.abbaOverrides = stringified;
|
|
}
|
|
|
|
function addOverrideFlag(name, value) {
|
|
_overrideFlags[name] = value;
|
|
_syncStorage();
|
|
console.info("Please reload Spotify for overried flags to be effective")
|
|
}
|
|
|
|
function removeOverrideFlag(name) {
|
|
if (_overrideFlags.hasOwnProperty(name)) {
|
|
delete _overrideFlags[name];
|
|
_syncStorage();
|
|
console.info(`Flag ${name} succesfully removed from Override Flags. Please reload Spotify.`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
getFlag,
|
|
getInUseFlags,
|
|
getAllFlags,
|
|
getOverrideFlags,
|
|
addOverrideFlag,
|
|
removeOverrideFlag,
|
|
};
|
|
})();
|
|
|
|
// Put `Spicetify` object to `window` object so apps iframe could access to it via `window.top.Spicetify`
|
|
window.Spicetify = Spicetify;
|