var stats = (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // external-global-plugin:react var require_react = __commonJS({ "external-global-plugin:react"(exports, module) { module.exports = Spicetify.React; } }); // external-global-plugin:react-dom var require_react_dom = __commonJS({ "external-global-plugin:react-dom"(exports, module) { module.exports = Spicetify.ReactDOM; } }); // ../../../AppData/Local/Temp/spicetify-creator/index.jsx var spicetify_creator_exports = {}; __export(spicetify_creator_exports, { default: () => render }); // src/app.tsx var import_react29 = __toESM(require_react()); // ../node_modules/spcr-navigation-bar/useNavigationBar.tsx var import_react3 = __toESM(require_react()); // ../node_modules/spcr-navigation-bar/navBar.tsx var import_react2 = __toESM(require_react()); var import_react_dom = __toESM(require_react_dom()); // ../node_modules/spcr-navigation-bar/optionsMenu.tsx var import_react = __toESM(require_react()); var OptionsMenuItemIcon = /* @__PURE__ */ import_react.default.createElement("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "currentColor" }, /* @__PURE__ */ import_react.default.createElement("path", { d: "M13.985 2.383L5.127 12.754 1.388 8.375l-.658.77 4.397 5.149 9.618-11.262z" })); var OptionsMenuItem = import_react.default.memo((props) => { return /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.MenuItem, { onClick: props.onSelect, icon: props.isSelected ? OptionsMenuItemIcon : null }, props.value); }); var OptionsMenu = import_react.default.memo((props) => { const menuRef = import_react.default.useRef(null); const menu = /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.Menu, null, props.options.map( (option) => /* @__PURE__ */ import_react.default.createElement(OptionsMenuItem, { value: option.link, isSelected: option.isActive, onSelect: () => { props.onSelect(option.link); menuRef.current?.click(); } }) )); return /* @__PURE__ */ import_react.default.createElement(Spicetify.ReactComponent.ContextMenu, { menu, trigger: "click", action: "toggle", renderInLine: true }, /* @__PURE__ */ import_react.default.createElement("button", { className: navBar_module_default.optionsMenuDropBox, ref: menuRef }, /* @__PURE__ */ import_react.default.createElement("span", { className: props.bold ? "main-type-mestoBold" : "main-type-mesto" }, props.options.find((o) => o.isActive)?.link || props.defaultValue), /* @__PURE__ */ import_react.default.createElement("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "currentColor" }, /* @__PURE__ */ import_react.default.createElement("path", { d: "M3 6l5 5.794L13 6z" })))); }); var optionsMenu_default = OptionsMenu; // postcss-module:C:\Users\user\AppData\Local\Temp\tmp-17120-n3iWRzissBjg\18f3cbbb4bc3\navBar.module.css var navBar_module_default = { "topBarHeaderItem": "navBar-module__topBarHeaderItem___piw4C_stats", "topBarHeaderItemLink": "navBar-module__topBarHeaderItemLink___xA4uv_stats", "topBarActive": "navBar-module__topBarActive___XhWpm_stats", "topBarNav": "navBar-module__topBarNav___qWGeZ_stats", "optionsMenuDropBox": "navBar-module__optionsMenuDropBox___pzfNI_stats" }; // ../node_modules/spcr-navigation-bar/navBar.tsx var NavbarItem2 = class { constructor(link, isActive) { this.link = link; this.isActive = isActive; } }; var NavbarItemComponent = (props) => { return /* @__PURE__ */ import_react2.default.createElement("li", { className: navBar_module_default.topBarHeaderItem, onClick: (e) => { e.preventDefault(); props.switchTo(props.item.link); } }, /* @__PURE__ */ import_react2.default.createElement("a", { className: `${navBar_module_default.topBarHeaderItemLink} ${props.item.isActive ? navBar_module_default.topBarActive : ""}`, "aria-current": "page", draggable: false, href: "" }, /* @__PURE__ */ import_react2.default.createElement("span", { className: "main-type-mestoBold" }, props.item.link))); }; var NavbarMore = import_react2.default.memo(({ items, switchTo }) => { return /* @__PURE__ */ import_react2.default.createElement("li", { className: `${navBar_module_default.topBarHeaderItem} ${items.find((item) => item.isActive) ? navBar_module_default.topBarActive : ""}` }, /* @__PURE__ */ import_react2.default.createElement(optionsMenu_default, { options: items, onSelect: switchTo, defaultValue: "More", bold: true })); }); var NavbarContent = (props) => { const resizeHost = document.querySelector(".Root__main-view .os-resize-observer-host") ?? document.querySelector(".Root__main-view .os-size-observer"); const [windowSize, setWindowSize] = (0, import_react2.useState)(resizeHost.clientWidth); const resizeHandler = () => setWindowSize(resizeHost.clientWidth); (0, import_react2.useEffect)(() => { const observer = new ResizeObserver(resizeHandler); observer.observe(resizeHost); return () => { observer.disconnect(); }; }, [resizeHandler]); return /* @__PURE__ */ import_react2.default.createElement(NavbarContext, null, /* @__PURE__ */ import_react2.default.createElement(Navbar, { ...props, windowSize })); }; var NavbarContext = (props) => { return import_react_dom.default.createPortal( /* @__PURE__ */ import_react2.default.createElement("div", { className: "main-topbar-topbarContent" }, props.children), document.querySelector(".main-topBar-topbarContentWrapper") ); }; var Navbar = (props) => { const navBarListRef = import_react2.default.useRef(null); const [childrenSizes, setChildrenSizes] = (0, import_react2.useState)([]); const [availableSpace, setAvailableSpace] = (0, import_react2.useState)(0); const [outOfRangeItemIndexes, setOutOfRangeItemIndexes] = (0, import_react2.useState)([]); let items = props.links.map((link) => new NavbarItem2(link, link === props.activeLink)); (0, import_react2.useEffect)(() => { if (!navBarListRef.current) return; const children = Array.from(navBarListRef.current.children); const navBarItemSizes = children.map((child) => child.clientWidth); setChildrenSizes(navBarItemSizes); }, []); (0, import_react2.useEffect)(() => { if (!navBarListRef.current) return; setAvailableSpace(navBarListRef.current.clientWidth); }, [props.windowSize]); (0, import_react2.useEffect)(() => { if (!navBarListRef.current) return; let totalSize = childrenSizes.reduce((a, b) => a + b, 0); if (totalSize <= availableSpace) { setOutOfRangeItemIndexes([]); return; } const viewMoreButtonSize = Math.max(...childrenSizes); const itemsToHide = []; let stopWidth = viewMoreButtonSize; childrenSizes.forEach((childWidth, i) => { if (availableSpace >= stopWidth + childWidth) { stopWidth += childWidth; } else if (i !== items.length) { itemsToHide.push(i); } }); setOutOfRangeItemIndexes(itemsToHide); }, [availableSpace, childrenSizes]); return /* @__PURE__ */ import_react2.default.createElement("nav", { className: navBar_module_default.topBarNav }, /* @__PURE__ */ import_react2.default.createElement("ul", { className: navBar_module_default.topBarHeader, ref: navBarListRef }, items.filter((_, id) => !outOfRangeItemIndexes.includes(id)).map( (item) => /* @__PURE__ */ import_react2.default.createElement(NavbarItemComponent, { item, switchTo: props.switchCallback }) ), outOfRangeItemIndexes.length ? /* @__PURE__ */ import_react2.default.createElement(NavbarMore, { items: outOfRangeItemIndexes.map((i) => items[i]), switchTo: props.switchCallback }) : null)); }; var navBar_default = NavbarContent; // ../node_modules/spcr-navigation-bar/useNavigationBar.tsx var useNavigationBar = (links) => { const [activeLink, setActiveLink] = (0, import_react3.useState)(links[0]); const navbar = /* @__PURE__ */ import_react3.default.createElement(navBar_default, { links, activeLink, switchCallback: (link) => setActiveLink(link) }); return [navbar, activeLink, setActiveLink]; }; var useNavigationBar_default = useNavigationBar; // src/pages/top_artists.tsx var import_react17 = __toESM(require_react()); // ../shared/dropdown/useDropdownMenu.tsx var import_react5 = __toESM(require_react()); // ../shared/dropdown/dropdown.tsx var import_react4 = __toESM(require_react()); function CheckIcon() { return /* @__PURE__ */ import_react4.default.createElement(Spicetify.ReactComponent.IconComponent, { iconSize: 16, semanticColor: "textBase", dangerouslySetInnerHTML: { __html: '' } }); } var MenuItem = (props) => { const { ReactComponent } = Spicetify; const { option, isActive, switchCallback } = props; const activeStyle = { backgroundColor: "rgba(var(--spice-rgb-selected-row),.1)" }; return /* @__PURE__ */ import_react4.default.createElement(ReactComponent.MenuItem, { trigger: "click", onClick: () => switchCallback(option), "data-checked": isActive, trailingIcon: isActive ? /* @__PURE__ */ import_react4.default.createElement(CheckIcon, null) : void 0, style: isActive ? activeStyle : void 0 }, option.name); }; var DropdownMenu = (props) => { const { ContextMenu, Menu, TextComponent } = Spicetify.ReactComponent; const { options, activeOption, switchCallback } = props; const optionItems = options.map((option) => { return /* @__PURE__ */ import_react4.default.createElement(MenuItem, { option, isActive: option === activeOption, switchCallback }); }); const MenuWrapper2 = (props2) => { return /* @__PURE__ */ import_react4.default.createElement(Menu, { ...props2 }, optionItems); }; return /* @__PURE__ */ import_react4.default.createElement(ContextMenu, { menu: /* @__PURE__ */ import_react4.default.createElement(MenuWrapper2, null), trigger: "click" }, /* @__PURE__ */ import_react4.default.createElement("button", { className: "x-sortBox-sortDropdown", type: "button", role: "combobox", "aria-expanded": "false" }, /* @__PURE__ */ import_react4.default.createElement(TextComponent, { variant: "mesto", semanticColor: "textSubdued" }, activeOption.name), /* @__PURE__ */ import_react4.default.createElement("svg", { role: "img", height: "16", width: "16", "aria-hidden": "true", className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small", viewBox: "0 0 16 16", "data-encore-id": "icon" }, /* @__PURE__ */ import_react4.default.createElement("path", { d: "m14 6-6 6-6-6h12z" })))); }; var dropdown_default = DropdownMenu; // ../shared/dropdown/useDropdownMenu.tsx var useDropdownMenu = (options, storageVariable) => { const initialOptionID = storageVariable && Spicetify.LocalStorage.get(`${storageVariable}:active-option`); const initialOption = initialOptionID && options.find((e) => e.id === initialOptionID); const [activeOption, setActiveOption] = (0, import_react5.useState)(initialOption || options[0]); const [availableOptions, setAvailableOptions] = (0, import_react5.useState)(options); const dropdown = /* @__PURE__ */ import_react5.default.createElement(dropdown_default, { options: availableOptions, activeOption, switchCallback: (option) => { setActiveOption(option); if (storageVariable) Spicetify.LocalStorage.set(`${storageVariable}:active-option`, option.id); } }); return [dropdown, activeOption, setActiveOption, setAvailableOptions]; }; var useDropdownMenu_default = useDropdownMenu; // ../library/src/components/collection_menu.tsx var import_react8 = __toESM(require_react()); // ../library/src/components/text_input_dialog.tsx var import_react6 = __toESM(require_react()); var TextInputDialog = (props) => { const { ButtonPrimary } = Spicetify.ReactComponent; const { def, placeholder, onSave } = props; const [value, setValue] = import_react6.default.useState(def); const onSubmit = (e) => { e.preventDefault(); Spicetify.PopupModal.hide(); onSave(value); }; return /* @__PURE__ */ import_react6.default.createElement(import_react6.default.Fragment, null, /* @__PURE__ */ import_react6.default.createElement("form", { className: "text-input-form", onSubmit }, /* @__PURE__ */ import_react6.default.createElement("label", { className: "text-input-wrapper" }, /* @__PURE__ */ import_react6.default.createElement("input", { className: "text-input", type: "text", value, placeholder, onChange: (e) => setValue(e.target.value) })), /* @__PURE__ */ import_react6.default.createElement("button", { type: "submit", "data-encore-id": "buttonPrimary", className: "Button-sc-qlcn5g-0 Button-small-buttonPrimary" }, /* @__PURE__ */ import_react6.default.createElement("span", { className: "ButtonInner-sc-14ud5tc-0 ButtonInner-small encore-bright-accent-set" }, "Save")))); }; var text_input_dialog_default = TextInputDialog; // ../library/src/components/leading_icon.tsx var import_react7 = __toESM(require_react()); var LeadingIcon = ({ path }) => { return /* @__PURE__ */ import_react7.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", dangerouslySetInnerHTML: { __html: `${path}` }, iconSize: 16 }); }; var leading_icon_default = LeadingIcon; // ../library/src/components/collection_menu.tsx var editIconPath = ''; var deleteIconPath = ''; var CollectionMenu = ({ id }) => { const { Menu, MenuItem: MenuItem2 } = Spicetify.ReactComponent; const deleteCollection = () => { SpicetifyLibrary.CollectionWrapper.deleteCollection(id); }; const renameCollection = () => { const name = SpicetifyLibrary.CollectionWrapper.getCollection(id).name; const rename = (newName) => { SpicetifyLibrary.CollectionWrapper.renameCollection(id, newName); }; Spicetify.PopupModal.display({ title: "Rename Collection", content: /* @__PURE__ */ import_react8.default.createElement(text_input_dialog_default, { def: name, onSave: rename }) }); }; const image = SpicetifyLibrary.CollectionWrapper.getCollection(id).imgUrl; const setCollectionImage = () => { const setImg = (imgUrl) => { SpicetifyLibrary.CollectionWrapper.setCollectionImage(id, imgUrl); }; Spicetify.PopupModal.display({ title: "Set Collection Image", content: /* @__PURE__ */ import_react8.default.createElement(text_input_dialog_default, { def: image, placeholder: "Image URL", onSave: setImg }) }); }; const removeImage = () => { SpicetifyLibrary.CollectionWrapper.removeCollectionImage(id); }; return /* @__PURE__ */ import_react8.default.createElement(Menu, null, /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { path: editIconPath }), onClick: renameCollection }, "Rename"), /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { path: deleteIconPath }), onClick: deleteCollection }, "Delete"), /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { path: editIconPath }), onClick: setCollectionImage }, "Set Collection Image"), image && /* @__PURE__ */ import_react8.default.createElement(MenuItem2, { leadingIcon: /* @__PURE__ */ import_react8.default.createElement(leading_icon_default, { path: deleteIconPath }), onClick: removeImage }, "Remove Collection Image")); }; var collection_menu_default = CollectionMenu; // ../library/src/components/folder_menu.tsx var import_react9 = __toESM(require_react()); var editIconPath2 = ''; var deleteIconPath2 = ''; var FolderMenu = ({ uri }) => { const { MenuItem: MenuItem2, Menu } = Spicetify.ReactComponent; const image = SpicetifyLibrary.FolderImageWrapper.getFolderImage(uri); const setImage = () => { const setNewImage = (newUrl) => { SpicetifyLibrary.FolderImageWrapper.setFolderImage({ uri, url: newUrl }); }; Spicetify.PopupModal.display({ title: "Set Folder Image", content: /* @__PURE__ */ import_react9.default.createElement(text_input_dialog_default, { def: image, onSave: setNewImage }) }); }; const removeImage = () => { SpicetifyLibrary.FolderImageWrapper.removeFolderImage(uri); }; return /* @__PURE__ */ import_react9.default.createElement(Menu, null, /* @__PURE__ */ import_react9.default.createElement(MenuItem2, { leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { path: editIconPath2 }), onClick: setImage }, "Set Folder Image"), image && /* @__PURE__ */ import_react9.default.createElement(MenuItem2, { leadingIcon: /* @__PURE__ */ import_react9.default.createElement(leading_icon_default, { path: deleteIconPath2 }), onClick: removeImage }, "Remove Folder Image")); }; var folder_menu_default = FolderMenu; // ../shared/components/spotify_card.tsx var import_react11 = __toESM(require_react()); // ../shared/components/folder_fallback.tsx var import_react10 = __toESM(require_react()); var FolderSVG = (e) => { return /* @__PURE__ */ import_react10.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", viewBox: "0 0 24 24", size: "xxlarge", dangerouslySetInnerHTML: { __html: '' }, ...e }); }; var folder_fallback_default = FolderSVG; // ../shared/components/spotify_card.tsx function SpotifyCard(props) { const { Cards, TextComponent, ArtistMenu, AlbumMenu, PodcastShowMenu, PlaylistMenu, ContextMenu } = Spicetify.ReactComponent; const { FeatureCard: Card, CardImage } = Cards; const { createHref, push } = Spicetify.Platform.History; const { type, header, uri, imageUrl, subheader, artistUri } = props; const backupImageUrl = type === "folder" || type === "collection" ? "https://raw.githubusercontent.com/harbassan/spicetify-apps/main/shared/placeholders/folder_placeholder.png" : "https://raw.githubusercontent.com/harbassan/spicetify-apps/main/shared/placeholders/def_placeholder.png"; const Menu = () => { switch (type) { case "artist": return /* @__PURE__ */ import_react11.default.createElement(ArtistMenu, { uri }); case "album": return /* @__PURE__ */ import_react11.default.createElement(AlbumMenu, { uri, artistUri, canRemove: true }); case "playlist": return /* @__PURE__ */ import_react11.default.createElement(PlaylistMenu, { uri }); case "show": return /* @__PURE__ */ import_react11.default.createElement(PodcastShowMenu, { uri }); case "collection": return /* @__PURE__ */ import_react11.default.createElement(collection_menu_default, { id: uri }); case "folder": return /* @__PURE__ */ import_react11.default.createElement(folder_menu_default, { uri }); default: return /* @__PURE__ */ import_react11.default.createElement(import_react11.default.Fragment, null); } }; const lastfmProps = type === "lastfm" ? { onClick: () => window.open(uri, "_blank"), isPlayable: false, delegateNavigation: true } : {}; const folderProps = type === "folder" ? { delegateNavigation: true, onClick: () => { createHref({ pathname: `/library/folder/${uri}` }); push({ pathname: `/library/folder/${uri}` }); } } : {}; const collectionProps = type === "collection" ? { delegateNavigation: true, onClick: () => { createHref({ pathname: `/library/collection/${uri}` }); push({ pathname: `/library/collection/${uri}` }); } } : {}; return /* @__PURE__ */ import_react11.default.createElement(ContextMenu, { menu: Menu(), trigger: "right-click" }, /* @__PURE__ */ import_react11.default.createElement(Card, { featureIdentifier: type, headerText: header, renderCardImage: () => /* @__PURE__ */ import_react11.default.createElement(CardImage, { images: [ { height: 640, url: imageUrl, width: 640 } ], isCircular: type === "artist", FallbackComponent: folder_fallback_default }), renderSubHeaderContent: () => /* @__PURE__ */ import_react11.default.createElement(TextComponent, { as: "div", variant: "mesto", semanticColor: "textSubdued", children: subheader }), uri, ...lastfmProps, ...folderProps, ...collectionProps })); } var spotify_card_default = SpotifyCard; // src/endpoints.ts var lfmperiods = { short_term: "1month", medium_term: "6month", long_term: "overall" }; var LASTFM = { toptracks: (user, key, range) => `http://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${key}&format=json&period=${lfmperiods[range]}`, topalbums: (user, key, range) => `https://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=${user}&api_key=${key}&format=json&period=${lfmperiods[range]}`, topartists: (user, key, range) => `https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${key}&format=json&period=${lfmperiods[range]}`, charts: (key, type) => `http://ws.audioscrobbler.com/2.0/?method=chart.gettop${type}&api_key=${key}&format=json` }; var SPOTIFY = { toptracks: (range) => `https://api.spotify.com/v1/me/top/tracks?limit=50&offset=0&time_range=${range}`, topartists: (range) => `https://api.spotify.com/v1/me/top/artists?limit=50&offset=0&time_range=${range}`, artists: (artists) => `https://api.spotify.com/v1/artists?ids=${filter(artists)}`, rootlist: "sp://core-playlist/v1/rootlist", playlist: (uri) => `sp://core-playlist/v1/playlist/${uri}`, search: (track, artist) => `https://api.spotify.com/v1/search?q=track:${filter(track)}+artist:${filter(artist)}&type=track`, searchartist: (artist) => `https://api.spotify.com/v1/search?q=artist:${filter(artist)}&type=artist`, searchalbum: (album, artist) => `https://api.spotify.com/v1/search?q=${filter(album)}+artist:${filter(artist)}&type=album`, audiofeatures: (ids) => `https://api.spotify.com/v1/audio-features?ids=${ids}`, queryliked: (ids) => `https://api.spotify.com/v1/me/tracks/contains?ids=${ids}` }; var PLACEHOLDER = "https://raw.githubusercontent.com/harbassan/spicetify-apps/main/stats/src/styles/placeholder.png"; // src/funcs.ts function filter(str) { const normalizedStr = str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); return normalizedStr.replace(/[^a-zA-Z0-9\-._~:/?#[\]@!$&()*+,;= ]/g, "").replace(/ /g, "+"); } var updatePageCache = (i, callback, activeOption, lib = false) => { let cacheInfo = Spicetify.LocalStorage.get("stats:cache-info"); if (!cacheInfo) return; let cacheInfoArray = JSON.parse(cacheInfo); if (!cacheInfoArray[i]) { if (!lib) { ["short_term", "medium_term", "long_term"].filter((option) => option !== activeOption).forEach((option) => callback(option, true, false)); } if (lib === "charts") { ["artists", "tracks"].filter((option) => option !== activeOption).forEach((option) => callback(option, true, false)); } callback(activeOption, true); cacheInfoArray[i] = true; Spicetify.LocalStorage.set("stats:cache-info", JSON.stringify(cacheInfoArray)); } }; var apiRequest = async (name, url, timeout = 5, log = true) => { try { const timeStart = window.performance.now(); const response = await Spicetify.CosmosAsync.get(url); if (log) console.log("stats -", name, "fetch time:", window.performance.now() - timeStart); return response; } catch (e) { if (timeout === 0) { console.log("stats -", name, "all requests failed:", e); console.log("stats -", name, "giving up"); return null; } else { if (timeout === 5) { console.log("stats -", name, "request failed:", e); console.log("stats -", name, "retrying..."); } await new Promise((resolve) => setTimeout(resolve, 5e3)); return apiRequest(name, url, timeout - 1); } } }; var fetchAudioFeatures = async (ids) => { const batchSize = 100; const batches = []; ids = ids.filter((id) => id.match(/^[a-zA-Z0-9]{22}$/)); for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize); batches.push(batch); } const promises = batches.map((batch, index) => { return apiRequest(`audioFeaturesBatch${index}`, SPOTIFY.audiofeatures(batch.join(",")), 5, false); }); const responses = await Promise.all(promises); const data = responses.reduce((acc, response) => { if (!response?.audio_features) return acc; return acc.concat(response.audio_features); }, []); return data; }; var fetchTopAlbums = async (albums, cachedAlbums) => { let album_keys = Object.keys(albums).filter((id) => id.match(/^[a-zA-Z0-9]{22}$/)).sort((a, b) => albums[b] - albums[a]).slice(0, 100); let release_years = {}; let total_album_tracks = 0; const cachedAlbumsSet = new Set(cachedAlbums?.map((album) => album.uri)); let top_albums = await Promise.all( album_keys.map(async (albumID) => { let albumMeta; if (cachedAlbums && cachedAlbumsSet.has(`spotify:album:${albumID}`)) { albumMeta = cachedAlbums.find((album) => album.uri === `spotify:album:${albumID}`); } if (!albumMeta) { try { albumMeta = await Spicetify.GraphQL.Request(Spicetify.GraphQL.Definitions.getAlbum, { uri: `spotify:album:${albumID}`, locale: "en", offset: 0, limit: 50 }); if (!albumMeta?.data?.albumUnion?.name) throw new Error("Invalid URI"); } catch (e) { console.error("stats - album metadata request failed:", e); return; } } const releaseYear = albumMeta?.release_year || albumMeta.data.albumUnion.date.isoString.slice(0, 4); release_years[releaseYear] = (release_years[releaseYear] || 0) + albums[albumID]; total_album_tracks += albums[albumID]; return { name: albumMeta.name || albumMeta.data.albumUnion.name, uri: albumMeta.uri || albumMeta.data.albumUnion.uri, image: albumMeta.image || albumMeta.data.albumUnion.coverArt.sources[0]?.url || PLACEHOLDER, release_year: releaseYear, freq: albums[albumID] }; }) ); top_albums = top_albums.filter((el) => el != null).slice(0, 10); return [top_albums, Object.entries(release_years), total_album_tracks]; }; var fetchTopArtists = async (artists) => { if (Object.keys(artists).length === 0) return [[], [], 0]; let artist_keys = Object.keys(artists).filter((id) => id.match(/^[a-zA-Z0-9]{22}$/)).sort((a, b) => artists[b] - artists[a]).slice(0, 50); let genres = {}; let total_genre_tracks = 0; const artistsMeta = await apiRequest("artistsMetadata", SPOTIFY.artists(artist_keys.join(","))); let top_artists = artistsMeta?.artists?.map((artist) => { if (!artist) return null; artist.genres.forEach((genre) => { genres[genre] = (genres[genre] || 0) + artists[artist.id]; }); total_genre_tracks += artists[artist.id]; return { name: artist.name, uri: artist.uri, image: artist.images[2]?.url || PLACEHOLDER, freq: artists[artist.id] }; }); top_artists = top_artists.filter((el) => el != null).slice(0, 10); const top_genres = Object.entries(genres).sort((a, b) => b[1] - a[1]).slice(0, 10); return [top_artists, top_genres, total_genre_tracks]; }; var convertTrackData = async (data) => { return await Promise.all( data.map(async (item) => { const spotifyItem = await Spicetify.CosmosAsync.get(SPOTIFY.search(item.name, item.artist.name)).then( (res) => res.tracks?.items[0] ); if (!spotifyItem) { console.log(`couldn't find track: ${item.name} by ${item.artist.name}`); return { name: item.name, image: PLACEHOLDER, uri: item.url, artists: [{ name: item.artist.name, uri: item.artist.url }], duration: 0, album: "N/A", popularity: 0, explicit: false, album_uri: item.url }; } return { name: item.name, image: spotifyItem.album.images[0].url, uri: spotifyItem.uri, id: spotifyItem.id, artists: spotifyItem.artists.map((artist) => ({ name: artist.name, uri: artist.uri })), duration: spotifyItem.duration_ms, album: spotifyItem.album.name, popularity: spotifyItem.popularity, explicit: spotifyItem.explicit, album_uri: spotifyItem.album.uri, release_year: spotifyItem.album.release_date.slice(0, 4) }; }) ); }; var convertAlbumData = async (data) => { return await Promise.all( data.map(async (item) => { const spotifyItem = await Spicetify.CosmosAsync.get(SPOTIFY.searchalbum(item.name, item.artist.name)).then( (res) => res.albums?.items[0] ); if (!spotifyItem) { console.log(`couldn't find album: ${item.name} by ${item.artist.name}`); return { name: item.name, image: PLACEHOLDER, uri: item.url, id: item.mbid }; } return { name: item.name, image: spotifyItem.images[0].url, uri: spotifyItem.uri, id: spotifyItem.id }; }) ); }; var convertArtistData = async (data) => { return await Promise.all( data.map(async (item) => { const spotifyItem = await Spicetify.CosmosAsync.get(SPOTIFY.searchartist(item.name)).then( (res) => res.artists?.items[0] ); if (!spotifyItem) { console.log(`couldn't find artist: ${item.name}`); return { name: item.name, image: PLACEHOLDER, uri: item.url, id: item.mbid }; } return { name: item.name, image: spotifyItem.images[0].url, uri: spotifyItem.uri, id: spotifyItem.id, genres: spotifyItem.genres }; }) ); }; var checkLiked = async (tracks) => { const nullIndexes = []; tracks.forEach((track, index) => { if (track === null) { nullIndexes.push(index); } }); const apiResponse = await apiRequest("checkLiked", SPOTIFY.queryliked(tracks.filter((e) => e).join(","))); if (!apiResponse) return; const response = []; let nullIndexesIndex = 0; for (let i = 0; i < tracks.length; i++) { if (nullIndexes.includes(i)) { response.push(false); } else { response.push(apiResponse[nullIndexesIndex]); nullIndexesIndex++; } } return response; }; async function queue(list, context = null) { list.push("spotify:delimiter"); const { _queue, _client } = Spicetify.Platform.PlayerAPI._queue; const { prevTracks, queueRevision } = _queue; const nextTracks = list.map((uri) => ({ contextTrack: { uri, uid: "", metadata: { is_queued: "false" } }, removed: [], blocked: [], provider: "context" })); _client.setQueue({ nextTracks, prevTracks, queueRevision }); if (context) { const { sessionId } = Spicetify.Platform.PlayerAPI.getState(); Spicetify.Platform.PlayerAPI.updateContext(sessionId, { uri: `spotify:user:${Spicetify.Platform.LibraryAPI._currentUsername}:top:tracks`, url: "" }); } Spicetify.Player.next(); } // ../shared/components/status.tsx var import_react12 = __toESM(require_react()); var ErrorIcon = () => { return /* @__PURE__ */ import_react12.default.createElement("svg", { "data-encore-id": "icon", role: "img", "aria-hidden": "true", viewBox: "0 0 24 24", className: "status-icon" }, /* @__PURE__ */ import_react12.default.createElement("path", { d: "M11 18v-2h2v2h-2zm0-4V6h2v8h-2z" }), /* @__PURE__ */ import_react12.default.createElement("path", { d: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM1 12C1 5.925 5.925 1 12 1s11 4.925 11 11-4.925 11-11 11S1 18.075 1 12z" })); }; var LibraryIcon = () => { return /* @__PURE__ */ import_react12.default.createElement("svg", { role: "img", height: "46", width: "46", "aria-hidden": "true", viewBox: "0 0 24 24", "data-encore-id": "icon", className: "status-icon" }, /* @__PURE__ */ import_react12.default.createElement("path", { d: "M14.5 2.134a1 1 0 0 1 1 0l6 3.464a1 1 0 0 1 .5.866V21a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1V3a1 1 0 0 1 .5-.866zM16 4.732V20h4V7.041l-4-2.309zM3 22a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1zm6 0a1 1 0 0 1-1-1V3a1 1 0 0 1 2 0v18a1 1 0 0 1-1 1z" })); }; var Status = (props) => { const [isVisible, setIsVisible] = import_react12.default.useState(false); import_react12.default.useEffect(() => { const to = setTimeout(() => { setIsVisible(true); }, 500); return () => clearTimeout(to); }, []); return isVisible ? /* @__PURE__ */ import_react12.default.createElement(import_react12.default.Fragment, null, /* @__PURE__ */ import_react12.default.createElement("div", { className: "loadingWrapper" }, props.icon === "error" ? /* @__PURE__ */ import_react12.default.createElement(ErrorIcon, null) : /* @__PURE__ */ import_react12.default.createElement(LibraryIcon, null), /* @__PURE__ */ import_react12.default.createElement("h1", null, props.heading), /* @__PURE__ */ import_react12.default.createElement("h3", null, props.subheading))) : /* @__PURE__ */ import_react12.default.createElement(import_react12.default.Fragment, null); }; var status_default = Status; // ../shared/components/page_container.tsx var import_react14 = __toESM(require_react()); // src/components/buttons/create_playlist_button.tsx var import_react13 = __toESM(require_react()); async function createPlaylistAsync(infoToCreatePlaylist) { const { Platform, showNotification } = Spicetify; const { RootlistAPI, PlaylistAPI } = Platform; try { const { playlistName, itemsUris } = infoToCreatePlaylist; const playlistUri = await RootlistAPI.createPlaylist(playlistName, { before: "start" }); await PlaylistAPI.add(playlistUri, itemsUris, { before: "start" }); } catch (error) { console.error(error); showNotification("Failed to create playlist", true, 1e3); } } function CreatePlaylistButton(props) { const { TooltipWrapper, ButtonSecondary } = Spicetify.ReactComponent; const { infoToCreatePlaylist } = props; return /* @__PURE__ */ import_react13.default.createElement(TooltipWrapper, { label: "Turn Into Playlist", renderInline: true, placement: "top" }, /* @__PURE__ */ import_react13.default.createElement(ButtonSecondary, { "aria-label": "Turn Into Playlist", children: "Turn Into Playlist", semanticColor: "textBase", buttonSize: "sm", onClick: () => createPlaylistAsync(infoToCreatePlaylist), className: "stats-make-playlist-button" })); } var create_playlist_button_default = CreatePlaylistButton; // ../shared/components/page_container.tsx var PageContainer = (props) => { const { title, infoToCreatePlaylist, headerEls, children } = props; const { TextComponent } = Spicetify.ReactComponent; return /* @__PURE__ */ import_react14.default.createElement("section", { className: "contentSpacing" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "page-header" }, /* @__PURE__ */ import_react14.default.createElement("div", { className: "header-left" }, /* @__PURE__ */ import_react14.default.createElement(TextComponent, { children: title, as: "h1", variant: "canon", semanticColor: "textBase" }), infoToCreatePlaylist ? /* @__PURE__ */ import_react14.default.createElement(create_playlist_button_default, { infoToCreatePlaylist }) : null), /* @__PURE__ */ import_react14.default.createElement("div", { className: "header-right" }, headerEls)), /* @__PURE__ */ import_react14.default.createElement("div", { className: "page-content" }, children)); }; var page_container_default = PageContainer; // ../shared/components/settings_button.tsx var import_react15 = __toESM(require_react()); function SettingsIcon() { return /* @__PURE__ */ import_react15.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", dangerouslySetInnerHTML: { __html: '' }, iconSize: 16 }); } function SettingsButton(props) { const { TooltipWrapper, ButtonTertiary } = Spicetify.ReactComponent; const { configWrapper } = props; return /* @__PURE__ */ import_react15.default.createElement(TooltipWrapper, { label: "Settings", renderInline: true, placement: "top" }, /* @__PURE__ */ import_react15.default.createElement(ButtonTertiary, { buttonSize: "sm", onClick: configWrapper.launchModal, "aria-label": "Settings", iconOnly: SettingsIcon })); } var settings_button_default = SettingsButton; // src/components/buttons/refresh_button.tsx var import_react16 = __toESM(require_react()); function RefreshIcon() { return /* @__PURE__ */ import_react16.default.createElement(Spicetify.ReactComponent.IconComponent, { semanticColor: "textSubdued", iconSize: 16, dangerouslySetInnerHTML: { __html: '' } }); } function RefreshButton(props) { const { ButtonTertiary, TooltipWrapper } = Spicetify.ReactComponent; const { callback } = props; return /* @__PURE__ */ import_react16.default.createElement(TooltipWrapper, { label: "Refresh", renderInline: true, placement: "top" }, /* @__PURE__ */ import_react16.default.createElement(ButtonTertiary, { buttonSize: "sm", onClick: callback, "aria-label": "Refresh", iconOnly: RefreshIcon })); } var refresh_button_default = RefreshButton; // src/pages/top_artists.tsx var topArtistsReq = async (time_range, configWrapper) => { const { config } = configWrapper; if (config["use-lastfm"] === true) { if (!config["api-key"] || !config["lastfm-user"]) return 300; const { ["lastfm-user"]: user, ["api-key"]: key } = config; const response = await apiRequest("lastfm", LASTFM.topartists(user, key, time_range)); if (!response) return 200; return await convertArtistData(response.topartists.artist); } else { const response = await apiRequest("topArtists", SPOTIFY.topartists(time_range)); if (!response) return 200; return response.items.map((artist) => { const image = artist.images[2]?.url || artist.images[1]?.url || PLACEHOLDER; return { id: artist.id, name: artist.name, image, uri: artist.uri, genres: artist.genres }; }); } }; var DropdownOptions = [ { id: "short_term", name: "Past Month" }, { id: "medium_term", name: "Past 6 Months" }, { id: "long_term", name: "All Time" } ]; var ArtistsPage = ({ configWrapper }) => { const [topArtists, setTopArtists] = import_react17.default.useState(100); const [dropdown, activeOption, setActiveOption] = useDropdownMenu_default(DropdownOptions, "stats:top-artists"); const fetchTopArtists2 = async (time_range, force, set = true) => { if (!force) { let storedData = Spicetify.LocalStorage.get(`stats:top-artists:${time_range}`); if (storedData) return setTopArtists(JSON.parse(storedData)); } const start = window.performance.now(); const topArtists2 = await topArtistsReq(time_range, configWrapper); if (set) setTopArtists(topArtists2); Spicetify.LocalStorage.set(`stats:top-artists:${time_range}`, JSON.stringify(topArtists2)); console.log("total artists fetch time:", window.performance.now() - start); }; import_react17.default.useEffect(() => { updatePageCache(0, fetchTopArtists2, activeOption.id); }, []); import_react17.default.useEffect(() => { fetchTopArtists2(activeOption.id); }, [activeOption]); const refresh = () => { fetchTopArtists2(activeOption.id, true); }; const props = { title: "Top Artists", headerEls: [dropdown, /* @__PURE__ */ import_react17.default.createElement(refresh_button_default, { callback: refresh }), /* @__PURE__ */ import_react17.default.createElement(settings_button_default, { configWrapper })] }; switch (topArtists) { case 300: return /* @__PURE__ */ import_react17.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react17.default.createElement(status_default, { icon: "error", heading: "No API Key or Username", subheading: "Please enter these in the settings menu" })); case 200: return /* @__PURE__ */ import_react17.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react17.default.createElement(status_default, { icon: "error", heading: "Failed to Fetch Top Artists", subheading: "An error occurred while fetching the data" })); case 100: return /* @__PURE__ */ import_react17.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react17.default.createElement(status_default, { icon: "library", heading: "Loading", subheading: "Fetching data..." })); } const artistCards = topArtists.map((artist, index) => /* @__PURE__ */ import_react17.default.createElement(spotify_card_default, { type: artist.uri.includes("last") ? "lastfm" : "artist", uri: artist.uri, header: artist.name, subheader: `#${index + 1} Artist`, imageUrl: artist.image })); return /* @__PURE__ */ import_react17.default.createElement(import_react17.default.Fragment, null, /* @__PURE__ */ import_react17.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react17.default.createElement("div", { className: `main-gridContainer-gridContainer grid` }, artistCards))); }; var top_artists_default = import_react17.default.memo(ArtistsPage); // src/pages/top_tracks.tsx var import_react20 = __toESM(require_react()); // src/components/track_row.tsx var import_react18 = __toESM(require_react()); var ArtistLink = ({ name, uri, index, length }) => { return /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement("a", { draggable: "true", dir: "auto", href: uri, tabIndex: -1 }, name), index === length ? null : ", "); }; var ExplicitBadge = import_react18.default.memo(() => { return /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement("span", { className: "TypeElement-ballad-textSubdued-type main-trackList-rowBadges", "data-encore-id": "type" }, /* @__PURE__ */ import_react18.default.createElement("span", { "aria-label": "Explicit", className: "main-tag-container", title: "Explicit" }, "E"))); }); var LikedIcon = ({ active, uri }) => { const [liked, setLiked] = import_react18.default.useState(active); const toggleLike = () => { if (liked) { Spicetify.Platform.LibraryAPI.remove(uri); } else { Spicetify.Platform.LibraryAPI.add(uri); } setLiked(!liked); }; import_react18.default.useEffect(() => { setLiked(active); }, [active]); return /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { label: liked ? `Remove from Your Library` : "Save to Your Library", placement: "top" }, /* @__PURE__ */ import_react18.default.createElement("button", { type: "button", role: "switch", "aria-checked": liked, "aria-label": "Remove from Your Library", onClick: toggleLike, className: liked ? "main-addButton-button main-trackList-rowHeartButton main-addButton-active" : "main-addButton-button main-trackList-rowHeartButton", tabIndex: -1 }, /* @__PURE__ */ import_react18.default.createElement("svg", { role: "img", height: "16", width: "16", "aria-hidden": "true", viewBox: "0 0 16 16", "data-encore-id": "icon", className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small" }, /* @__PURE__ */ import_react18.default.createElement("path", { d: liked ? "M15.724 4.22A4.313 4.313 0 0 0 12.192.814a4.269 4.269 0 0 0-3.622 1.13.837.837 0 0 1-1.14 0 4.272 4.272 0 0 0-6.21 5.855l5.916 7.05a1.128 1.128 0 0 0 1.727 0l5.916-7.05a4.228 4.228 0 0 0 .945-3.577z" : "M1.69 2A4.582 4.582 0 0 1 8 2.023 4.583 4.583 0 0 1 11.88.817h.002a4.618 4.618 0 0 1 3.782 3.65v.003a4.543 4.543 0 0 1-1.011 3.84L9.35 14.629a1.765 1.765 0 0 1-2.093.464 1.762 1.762 0 0 1-.605-.463L1.348 8.309A4.582 4.582 0 0 1 1.689 2zm3.158.252A3.082 3.082 0 0 0 2.49 7.337l.005.005L7.8 13.664a.264.264 0 0 0 .311.069.262.262 0 0 0 .09-.069l5.312-6.33a3.043 3.043 0 0 0 .68-2.573 3.118 3.118 0 0 0-2.551-2.463 3.079 3.079 0 0 0-2.612.816l-.007.007a1.501 1.501 0 0 1-2.045 0l-.009-.008a3.082 3.082 0 0 0-2.121-.861z" })))); }; var DraggableComponent = ({ uri, title, ...props }) => { const dragHandler = Spicetify.ReactHook.DragHandler?.([uri], title); return /* @__PURE__ */ import_react18.default.createElement("div", { onDragStart: dragHandler, draggable: "true", ...props }, props.children); }; function playAndQueue(uri, uris) { uris = uris.filter((u) => !u.includes("last")); uris = uris.concat(uris.splice(0, uris.indexOf(uri))); queue(uris); } var MenuWrapper = import_react18.default.memo((props) => /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.AlbumMenu, { ...props })); var TrackRow = (props) => { const ArtistLinks = props.artists.map((artist, index) => { return /* @__PURE__ */ import_react18.default.createElement(ArtistLink, { index, length: props.artists.length - 1, name: artist.name, uri: artist.uri }); }); return /* @__PURE__ */ import_react18.default.createElement(import_react18.default.Fragment, null, /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.ContextMenu, { menu: /* @__PURE__ */ import_react18.default.createElement(MenuWrapper, { uri: props.uri }), trigger: "right-click" }, /* @__PURE__ */ import_react18.default.createElement("div", { role: "row", "aria-rowindex": 2, "aria-selected": "false" }, /* @__PURE__ */ import_react18.default.createElement(DraggableComponent, { uri: props.uri, title: `${props.name} \u2022 ${props.artists.map((artist) => artist.name).join(", ")}`, className: "main-trackList-trackListRow main-trackList-trackListRowGrid", role: "presentation", onClick: (event) => event.detail === 2 && playAndQueue(props.uri, props.uris), style: { height: 56 } }, /* @__PURE__ */ import_react18.default.createElement("div", { className: "main-trackList-rowSectionIndex", role: "gridcell", "aria-colindex": 1, tabIndex: -1 }, /* @__PURE__ */ import_react18.default.createElement("div", { uri: props.uri, className: "main-trackList-rowMarker" }, /* @__PURE__ */ import_react18.default.createElement("span", { className: "TypeElement-ballad-type main-trackList-number", "data-encore-id": "type" }, props.index), /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { label: `Play ${props.name} by ${props.artists.map((artist) => artist.name).join(", ")}`, placement: "top" }, /* @__PURE__ */ import_react18.default.createElement("button", { className: "main-trackList-rowImagePlayButton", "aria-label": `Play ${props.name}`, tabIndex: -1, onClick: () => playAndQueue(props.uri, props.uris) }, /* @__PURE__ */ import_react18.default.createElement("svg", { role: "img", height: "24", width: "24", "aria-hidden": "true", className: "Svg-img-24 Svg-img-24-icon main-trackList-rowPlayPauseIcon", viewBox: "0 0 24 24", "data-encore-id": "icon" }, /* @__PURE__ */ import_react18.default.createElement("path", { d: "m7.05 3.606 13.49 7.788a.7.7 0 0 1 0 1.212L7.05 20.394A.7.7 0 0 1 6 19.788V4.212a.7.7 0 0 1 1.05-.606z" })))))), /* @__PURE__ */ import_react18.default.createElement("div", { className: "main-trackList-rowSectionStart", role: "gridcell", "aria-colindex": 2, tabIndex: -1 }, /* @__PURE__ */ import_react18.default.createElement("img", { "aria-hidden": "false", draggable: "false", loading: "eager", src: props.image, alt: "", className: "main-image-image main-trackList-rowImage main-image-loaded", width: "40", height: "40" }), /* @__PURE__ */ import_react18.default.createElement("div", { className: "main-trackList-rowMainContent" }, /* @__PURE__ */ import_react18.default.createElement("div", { dir: "auto", className: "TypeElement-ballad-textBase TypeElement-ballad-textBase-type main-trackList-rowTitle standalone-ellipsis-one-line", "data-encore-id": "type" }, props.name), props.explicit && /* @__PURE__ */ import_react18.default.createElement(ExplicitBadge, null), /* @__PURE__ */ import_react18.default.createElement("span", { className: "TypeElement-mesto-textSubdued TypeElement-mesto-textSubdued-type main-trackList-rowSubTitle standalone-ellipsis-one-line", "data-encore-id": "type" }, ArtistLinks))), /* @__PURE__ */ import_react18.default.createElement("div", { className: "main-trackList-rowSectionVariable", role: "gridcell", "aria-colindex": 3, tabIndex: -1 }, /* @__PURE__ */ import_react18.default.createElement("span", { "data-encore-id": "type", className: "TypeElement-mesto TypeElement-mesto-type" }, /* @__PURE__ */ import_react18.default.createElement("a", { draggable: "true", className: "standalone-ellipsis-one-line", dir: "auto", href: props.album_uri, tabIndex: -1 }, props.album))), /* @__PURE__ */ import_react18.default.createElement("div", { className: "main-trackList-rowSectionEnd", role: "gridcell", "aria-colindex": 5, tabIndex: -1 }, /* @__PURE__ */ import_react18.default.createElement(LikedIcon, { active: props.liked || false, uri: props.uri }), /* @__PURE__ */ import_react18.default.createElement("div", { className: "TypeElement-mesto-textSubdued TypeElement-mesto-textSubdued-type main-trackList-rowDuration", "data-encore-id": "type" }, Spicetify.Player.formatTime(props.duration)), /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.ContextMenu, { menu: /* @__PURE__ */ import_react18.default.createElement(MenuWrapper, { uri: props.uri }), trigger: "click" }, /* @__PURE__ */ import_react18.default.createElement("button", { type: "button", "aria-haspopup": "menu", "aria-label": `More options for ${props.name}`, className: "main-moreButton-button Button-sm-16-buttonTertiary-iconOnly-condensed-useBrowserDefaultFocusStyle Button-small-small-buttonTertiary-iconOnly-condensed-useBrowserDefaultFocusStyle main-trackList-rowMoreButton", tabIndex: -1 }, /* @__PURE__ */ import_react18.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { label: `More options for ${props.name} by ${props.artists.map((artist) => artist.name).join(", ")}`, placement: "top" }, /* @__PURE__ */ import_react18.default.createElement("span", null, /* @__PURE__ */ import_react18.default.createElement("svg", { role: "img", height: "16", width: "16", "aria-hidden": "true", viewBox: "0 0 16 16", "data-encore-id": "icon", className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small" }, /* @__PURE__ */ import_react18.default.createElement("path", { d: "M3 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm6.5 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zM16 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z" }))))))))))); }; var track_row_default = import_react18.default.memo(TrackRow); // src/components/tracklist.tsx var import_react19 = __toESM(require_react()); var Tracklist = ({ minified = false, children }) => { return /* @__PURE__ */ import_react19.default.createElement("div", { role: "grid", "aria-rowcount": minified ? 5 : 50, "aria-colcount": 4, className: "main-trackList-trackList main-trackList-indexable", tabIndex: 0 }, !minified && /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-trackList-trackListHeader", role: "presentation" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-trackList-trackListHeaderRow main-trackList-trackListRowGrid", role: "row", "aria-rowindex": 1 }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-trackList-rowSectionIndex", role: "columnheader", "aria-colindex": 1, "aria-sort": "none", tabIndex: -1 }, "#"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-trackList-rowSectionStart", role: "columnheader", "aria-colindex": 2, "aria-sort": "none", tabIndex: -1 }, /* @__PURE__ */ import_react19.default.createElement("button", { className: "main-trackList-column main-trackList-sortable", tabIndex: -1 }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "TypeElement-mesto-type standalone-ellipsis-one-line", "data-encore-id": "type" }, "Title"))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-trackList-rowSectionVariable", role: "columnheader", "aria-colindex": 3, "aria-sort": "none", tabIndex: -1 }, /* @__PURE__ */ import_react19.default.createElement("button", { className: "main-trackList-column main-trackList-sortable", tabIndex: -1 }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "TypeElement-mesto-type standalone-ellipsis-one-line", "data-encore-id": "type" }, "Album"))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-trackList-rowSectionEnd", role: "columnheader", "aria-colindex": 5, "aria-sort": "none", tabIndex: -1 }, /* @__PURE__ */ import_react19.default.createElement(Spicetify.ReactComponent.TooltipWrapper, { label: "Duration", placement: "top" }, /* @__PURE__ */ import_react19.default.createElement("button", { "aria-label": "Duration", className: "main-trackList-column main-trackList-durationHeader main-trackList-sortable", tabIndex: -1 }, /* @__PURE__ */ import_react19.default.createElement("svg", { role: "img", height: "16", width: "16", "aria-hidden": "true", viewBox: "0 0 16 16", "data-encore-id": "icon", className: "Svg-img-16 Svg-img-16-icon Svg-img-icon Svg-img-icon-small" }, /* @__PURE__ */ import_react19.default.createElement("path", { d: "M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z" }), /* @__PURE__ */ import_react19.default.createElement("path", { d: "M8 3.25a.75.75 0 0 1 .75.75v3.25H11a.75.75 0 0 1 0 1.5H7.25V4A.75.75 0 0 1 8 3.25z" }))))))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "main-rootlist-wrapper", role: "presentation", style: { height: (minified ? 5 : 50) * 56 } }, /* @__PURE__ */ import_react19.default.createElement("div", { role: "presentation" }, children))); }; var tracklist_default = Tracklist; // src/pages/top_tracks.tsx var topTracksReq = async (time_range, configWrapper) => { const { config } = configWrapper; if (config["use-lastfm"] === true) { if (!config["api-key"] || !config["lastfm-user"]) return 300; const { ["lastfm-user"]: user, ["api-key"]: key } = config; const lastfmData = await apiRequest("lastfm", LASTFM.toptracks(user, key, time_range)); if (!lastfmData) return 200; const spotifyData = await convertTrackData(lastfmData.toptracks.track); const likedArray2 = await checkLiked(spotifyData.map((track) => track.id)); if (!likedArray2) return 200; spotifyData.forEach((track, index) => { track.liked = likedArray2[index]; }); return spotifyData; } const response = await apiRequest("topTracks", SPOTIFY.toptracks(time_range)); if (!response) return 200; const likedArray = await checkLiked(response.items.map((track) => track.id)); if (!likedArray) return 200; return response.items.map((track, index) => { const images = track.album.images; const image = images[2]?.url || images[1]?.url || PLACEHOLDER; return { liked: likedArray[index], name: track.name, image, uri: track.uri, id: track.id, artists: track.artists.map((artist) => ({ name: artist.name, uri: artist.uri })), duration: track.duration_ms, album: track.album.name, album_uri: track.album.uri, popularity: track.popularity, explicit: track.explicit, release_year: track.album.release_date.slice(0, 4) }; }); }; var DropdownOptions2 = [ { id: "short_term", name: "Past Month" }, { id: "medium_term", name: "Past 6 Months" }, { id: "long_term", name: "All Time" } ]; var TracksPage = ({ configWrapper }) => { const { LocalStorage } = Spicetify; const [topTracks, setTopTracks] = import_react20.default.useState(100); const [dropdown, activeOption] = useDropdownMenu_default(DropdownOptions2, "stats:top-tracks"); const fetchTopTracks = async (time_range, force, set = true) => { if (!force) { const storedData = LocalStorage.get(`stats:top-tracks:${time_range}`); if (storedData) return setTopTracks(JSON.parse(storedData)); } const start = window.performance.now(); const topTracks2 = await topTracksReq(time_range, configWrapper); if (set) setTopTracks(topTracks2); LocalStorage.set(`stats:top-tracks:${time_range}`, JSON.stringify(topTracks2)); console.log("total tracks fetch time:", window.performance.now() - start); }; import_react20.default.useEffect(() => { updatePageCache(1, fetchTopTracks, activeOption.id); }, []); import_react20.default.useEffect(() => { fetchTopTracks(activeOption.id); }, [activeOption]); const refresh = () => { fetchTopTracks(activeOption.id, true); }; const props = { title: "Top Tracks", headerEls: [dropdown, /* @__PURE__ */ import_react20.default.createElement(refresh_button_default, { callback: refresh }), /* @__PURE__ */ import_react20.default.createElement(settings_button_default, { configWrapper })] }; switch (topTracks) { case 300: return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react20.default.createElement(status_default, { icon: "error", heading: "No API Key or Username", subheading: "Please enter these in the settings menu" })); case 200: return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react20.default.createElement(status_default, { icon: "error", heading: "Failed to Fetch Top Tracks", subheading: "An error occurred while fetching the data" })); case 100: return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react20.default.createElement(status_default, { icon: "library", heading: "Loading", subheading: "Fetching data..." })); } const infoToCreatePlaylist = { playlistName: `Top Songs - ${activeOption}`, itemsUris: topTracks.map((track) => track.uri) }; const trackRows = topTracks.map((track, index) => /* @__PURE__ */ import_react20.default.createElement(track_row_default, { index: index + 1, ...track, uris: topTracks.map((track2) => track2.uri) })); return /* @__PURE__ */ import_react20.default.createElement(page_container_default, { ...props, infoToCreatePlaylist }, /* @__PURE__ */ import_react20.default.createElement(tracklist_default, null, trackRows)); }; var top_tracks_default = import_react20.default.memo(TracksPage); // src/pages/top_genres.tsx var import_react25 = __toESM(require_react()); // src/components/cards/stat_card.tsx var import_react21 = __toESM(require_react()); function formatValue(name, value) { switch (name) { case "tempo": return `${Math.round(value)} bpm`; case "popularity": return `${Math.round(value)} %`; default: return `${Math.round(value * 100)} %`; } } function normalizeString(inputString) { return inputString.charAt(0).toUpperCase() + inputString.slice(1).toLowerCase(); } function StatCard(props) { const { TextComponent } = Spicetify.ReactComponent; const { label, value } = props; return /* @__PURE__ */ import_react21.default.createElement("div", { className: "main-card-card" }, /* @__PURE__ */ import_react21.default.createElement(TextComponent, { as: "div", semanticColor: "textBase", variant: "alto", children: typeof value === "number" ? formatValue(label, value) : value }), /* @__PURE__ */ import_react21.default.createElement(TextComponent, { as: "div", semanticColor: "textBase", variant: "balladBold", children: normalizeString(label) })); } var stat_card_default = StatCard; // src/components/cards/genres_card.tsx var import_react22 = __toESM(require_react()); var genreLine = (name, value, limit, total) => { return /* @__PURE__ */ import_react22.default.createElement("div", { className: "stats-genreRow" }, /* @__PURE__ */ import_react22.default.createElement("div", { className: "stats-genreRowFill", style: { width: `calc(${value / limit * 100}% + ${(limit - value) / (limit - 1) * 100}px)` } }, /* @__PURE__ */ import_react22.default.createElement("span", { className: "stats-genreText" }, name)), /* @__PURE__ */ import_react22.default.createElement("span", { className: "stats-genreValue" }, Math.round(value / total * 100) + "%")); }; var genreLines = (genres, total) => { return genres.map(([genre, value]) => { return genreLine(genre, value, genres[0][1], total); }); }; var genresCard = ({ genres, total }) => { const genresArray = genres.sort(([, a], [, b]) => b - a).slice(0, 10); return /* @__PURE__ */ import_react22.default.createElement("div", { className: `main-card-card stats-genreCard` }, genreLines(genresArray, total)); }; var genres_card_default = genresCard; // src/components/inline_grid.tsx var import_react23 = __toESM(require_react()); function scrollGrid(event) { const { target } = event; if (!(target instanceof HTMLElement)) return; const grid = target.parentNode?.querySelector("div"); if (!grid) return; grid.scrollLeft += grid.clientWidth; if (grid.scrollWidth - grid.clientWidth - grid.scrollLeft <= grid.clientWidth) { grid.setAttribute("data-scroll", "end"); } else { grid.setAttribute("data-scroll", "both"); } } function scrollGridLeft(event) { const { target } = event; if (!(target instanceof HTMLElement)) return; const grid = target.parentNode?.querySelector("div"); if (!grid) return; grid.scrollLeft -= grid.clientWidth; if (grid.scrollLeft <= grid.clientWidth) { grid.setAttribute("data-scroll", "start"); } else { grid.setAttribute("data-scroll", "both"); } } function InlineGrid(props) { const { children, special } = props; return /* @__PURE__ */ import_react23.default.createElement("section", { className: "stats-gridInlineSection" }, /* @__PURE__ */ import_react23.default.createElement("button", { className: "stats-scrollButton", onClick: scrollGridLeft }, "<"), /* @__PURE__ */ import_react23.default.createElement("button", { className: "stats-scrollButton", onClick: scrollGrid }, ">"), /* @__PURE__ */ import_react23.default.createElement("div", { className: `main-gridContainer-gridContainer stats-gridInline${special ? " stats-specialGrid" : ""}`, "data-scroll": "start" }, children)); } var inline_grid_default = import_react23.default.memo(InlineGrid); // src/components/shelf.tsx var import_react24 = __toESM(require_react()); function Shelf(props) { const { TextComponent } = Spicetify.ReactComponent; const { title, children } = props; return /* @__PURE__ */ import_react24.default.createElement("section", { className: "main-shelf-shelf Shelf" }, /* @__PURE__ */ import_react24.default.createElement("div", { className: "main-shelf-header" }, /* @__PURE__ */ import_react24.default.createElement("div", { className: "main-shelf-topRow" }, /* @__PURE__ */ import_react24.default.createElement("div", { className: "main-shelf-titleWrapper" }, /* @__PURE__ */ import_react24.default.createElement(TextComponent, { children: title, as: "h2", variant: "canon", semanticColor: "textBase" })))), /* @__PURE__ */ import_react24.default.createElement("section", null, children)); } var shelf_default = import_react24.default.memo(Shelf); // src/pages/top_genres.tsx var DropdownOptions3 = [ { id: "short_term", name: "Past Month" }, { id: "medium_term", name: "Past 6 Months" }, { id: "long_term", name: "All Time" } ]; var GenresPage = ({ configWrapper }) => { const { LocalStorage } = Spicetify; const [topGenres, setTopGenres] = import_react25.default.useState(100); const [dropdown, activeOption] = useDropdownMenu_default(DropdownOptions3, "stats:top-genres"); const fetchTopGenres = async (time_range, force, set = true, force_refetch) => { if (!force) { let storedData = LocalStorage.get(`stats:top-genres:${time_range}`); if (storedData) return setTopGenres(JSON.parse(storedData)); } const start = window.performance.now(); const cacheInfo = JSON.parse(LocalStorage.get("stats:cache-info")); const fetchedItems = await Promise.all( ["artists", "tracks"].map(async (type, index) => { if (cacheInfo[index] === true && !force_refetch) { return await JSON.parse(LocalStorage.get(`stats:top-${type}:${time_range}`)); } const fetchedItems2 = await (type === "artists" ? topArtistsReq(time_range, configWrapper) : topTracksReq(time_range, configWrapper)); cacheInfo[index] = true; cacheInfo[2] = true; LocalStorage.set(`stats:top-${type}:${time_range}`, JSON.stringify(fetchedItems2)); LocalStorage.set("stats:cache-info", JSON.stringify(cacheInfo)); return fetchedItems2; }) ); for (let i = 0; i < 2; i++) { if (fetchedItems[i] === 200 || fetchedItems[i] === 300) return setTopGenres(fetchedItems[i]); } const fetchedArtists = fetchedItems[0].filter((artist) => artist?.genres); const fetchedTracks = fetchedItems[1].filter((track) => track?.id); const genres = fetchedArtists.reduce((acc, artist) => { artist.genres.forEach((genre) => { const index = acc.findIndex(([g]) => g === genre); if (index !== -1) { acc[index][1] += 1 * Math.abs(fetchedArtists.indexOf(artist) - 50); } else { acc.push([genre, 1 * Math.abs(fetchedArtists.indexOf(artist) - 50)]); } }); return acc; }, []); let trackPopularity = 0; let explicitness = 0; let releaseData = []; const topTracks = fetchedTracks.map((track) => { trackPopularity += track.popularity; if (track.explicit) explicitness++; if (track.release_year) { const year = track.release_year; const index = releaseData.findIndex(([y]) => y === year); if (index !== -1) { releaseData[index][1] += 1; } else { releaseData.push([year, 1]); } } return track.id; }); async function testDupe(track) { const spotifyItem = await apiRequest( "track", SPOTIFY.search(track.name, track.artists[0].name), 1, false ).then((res) => res.tracks?.items); if (!spotifyItem) return false; return spotifyItem.some((item) => { return item.name === track.name && item.popularity > track.popularity; }); } let obscureTracks2 = []; for (let i = 0; i < fetchedTracks.length; i++) { let track = fetchedTracks[i]; if (!track?.popularity) continue; if (obscureTracks2.length < 5) { const dupe = await testDupe(track); if (dupe) continue; obscureTracks2.push(track); obscureTracks2.sort((a, b) => b.popularity - a.popularity); continue; } for (let j = 0; j < 5; j++) { if (track.popularity < obscureTracks2[j].popularity) { const dupe = await testDupe(track); if (dupe) break; obscureTracks2.splice(j, 0, track); obscureTracks2 = obscureTracks2.slice(0, 5); break; } } } const fetchedFeatures = await fetchAudioFeatures(topTracks); let audioFeatures = { danceability: 0, energy: 0, valence: 0, speechiness: 0, acousticness: 0, instrumentalness: 0, liveness: 0, tempo: 0 }; for (let i = 0; i < fetchedFeatures.length; i++) { if (!fetchedFeatures[i]) continue; const track = fetchedFeatures[i]; Object.keys(audioFeatures).forEach((feature) => { audioFeatures[feature] += track[feature]; }); } audioFeatures = { popularity: trackPopularity, explicitness, ...audioFeatures }; for (let key in audioFeatures) { audioFeatures[key] = audioFeatures[key] / 50; } console.log("total genres fetch time:", window.performance.now() - start); if (set) setTopGenres({ genres, features: audioFeatures, years: releaseData, obscureTracks: obscureTracks2 }); LocalStorage.set( `stats:top-genres:${time_range}`, JSON.stringify({ genres, features: audioFeatures, years: releaseData, obscureTracks: obscureTracks2 }) ); }; import_react25.default.useEffect(() => { updatePageCache(2, fetchTopGenres, activeOption.id); }, []); import_react25.default.useEffect(() => { fetchTopGenres(activeOption.id); }, [activeOption]); const refresh = () => { fetchTopGenres(activeOption.id, true); }; const props = { title: "Top Genres", headerEls: [dropdown, /* @__PURE__ */ import_react25.default.createElement(refresh_button_default, { callback: refresh }), /* @__PURE__ */ import_react25.default.createElement(settings_button_default, { configWrapper })] }; switch (topGenres) { case 300: return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react25.default.createElement(status_default, { icon: "error", heading: "No API Key or Username", subheading: "Please enter these in the settings menu" })); case 200: return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react25.default.createElement(status_default, { icon: "error", heading: "Failed to Fetch Top Genres", subheading: "An error occurred while fetching the data" })); case 100: return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react25.default.createElement(status_default, { icon: "library", heading: "Loading", subheading: "Fetching data..." })); } const statCards = Object.entries(topGenres.features).map(([key, value]) => { return /* @__PURE__ */ import_react25.default.createElement(stat_card_default, { label: key, value }); }); const obscureTracks = topGenres.obscureTracks.map((track, index) => /* @__PURE__ */ import_react25.default.createElement(track_row_default, { index: index + 1, ...track, uris: topGenres.obscureTracks.map((track2) => track2.uri) })); return /* @__PURE__ */ import_react25.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react25.default.createElement("section", { className: "main-shelf-shelf Shelf" }, /* @__PURE__ */ import_react25.default.createElement(genres_card_default, { genres: topGenres.genres, total: 1275 }), /* @__PURE__ */ import_react25.default.createElement(inline_grid_default, { special: true }, statCards)), /* @__PURE__ */ import_react25.default.createElement(shelf_default, { title: "Release Year Distribution" }, /* @__PURE__ */ import_react25.default.createElement(genres_card_default, { genres: topGenres.years, total: 50 })), /* @__PURE__ */ import_react25.default.createElement(shelf_default, { title: "Most Obscure Tracks" }, /* @__PURE__ */ import_react25.default.createElement(tracklist_default, { minified: true }, obscureTracks))); }; var top_genres_default = import_react25.default.memo(GenresPage); // src/pages/library.tsx var import_react26 = __toESM(require_react()); var DropdownOptions4 = [ { id: "owned", name: "My Playlists" }, { id: "all", name: "All Playlists" } ]; var LibraryPage = ({ configWrapper }) => { const [library, setLibrary] = import_react26.default.useState(100); const [dropdown, activeOption, setActiveOption] = useDropdownMenu_default(DropdownOptions4, "stats:library"); const fetchData = async (option, force, set = true) => { try { if (!force) { let storedData = Spicetify.LocalStorage.get(`stats:library:${option}`); if (storedData) return setLibrary(JSON.parse(storedData)); } const start = window.performance.now(); const rootlistItems = await apiRequest("rootlist", SPOTIFY.rootlist); const flattenPlaylists = (items) => { const playlists2 = []; items.forEach((row) => { if (row.type === "playlist") { playlists2.push(row); } else if (row.type === "folder") { if (!row.rows) return; const folderPlaylists = flattenPlaylists(row.rows); playlists2.push(...folderPlaylists); } }); return playlists2; }; let playlists = flattenPlaylists(rootlistItems?.rows); playlists = playlists.sort((a, b) => a.ownedBySelf === b.ownedBySelf ? 0 : a.ownedBySelf ? -1 : 1); let indexOfFirstNotOwned = -1; let playlistUris = []; let trackCount = 0; let ownedTrackCount = 0; playlists.forEach((playlist) => { if (playlist.totalLength === 0) return; if (!playlist.ownedBySelf && indexOfFirstNotOwned === -1) indexOfFirstNotOwned = playlistUris.length; playlistUris.push(playlist.link); trackCount += playlist.totalLength; if (playlist.ownedBySelf) ownedTrackCount += playlist.totalLength; }, 0); const playlistsMeta = await Promise.all( playlistUris.map((uri) => { return apiRequest("playlistsMetadata", SPOTIFY.playlist(uri), 5, false); }) ); let duration = 0; let trackIDs = []; let popularity = 0; let albums = {}; let artists = {}; let explicitCount = 0; let ownedDuration = 0; let ownedArtists = {}; let ownedPopularity = 0; let ownedAlbums = {}; let ownedExplicitCount = 0; for (let i = 0; i < playlistsMeta.length; i++) { const playlist = playlistsMeta[i]; if (!playlist) continue; if (i === indexOfFirstNotOwned) { ownedDuration = duration; ownedArtists = Object.assign({}, artists); ownedPopularity = popularity; ownedExplicitCount = explicitCount; ownedAlbums = Object.assign({}, albums); } duration += playlist.playlist.duration; playlist.items.forEach((track) => { if (!track?.album) return; if (track.link.includes("local")) return; trackIDs.push(track.link.split(":")[2]); if (track.isExplicit) explicitCount++; popularity += track.popularity; const albumID = track.album.link.split(":")[2]; albums[albumID] = albums[albumID] ? albums[albumID] + 1 : 1; track.artists.forEach((artist) => { const artistID = artist.link.split(":")[2]; artists[artistID] = artists[artistID] ? artists[artistID] + 1 : 1; }); }); } const [topArtists, topGenres, topGenresTotal] = await fetchTopArtists(artists); const [ownedTopArtists, ownedTopGenres, ownedTopGenresTotal] = await fetchTopArtists(ownedArtists); const [topAlbums, releaseYears, releaseYearsTotal] = await fetchTopAlbums(albums); const [ownedTopAlbums, ownedReleaseYears, ownedReleaseYearsTotal] = await fetchTopAlbums( ownedAlbums, topAlbums ); const fetchedFeatures = await fetchAudioFeatures(trackIDs); const audioFeatures = { danceability: 0, energy: 0, valence: 0, speechiness: 0, acousticness: 0, instrumentalness: 0, liveness: 0, tempo: 0 }; let ownedAudioFeatures = {}; for (let i = 0; i < fetchedFeatures.length; i++) { if (i === ownedTrackCount) { ownedAudioFeatures = { popularity: ownedPopularity, explicitness: ownedExplicitCount, ...audioFeatures }; } if (!fetchedFeatures[i]) continue; const track = fetchedFeatures[i]; Object.keys(audioFeatures).forEach((feature) => { audioFeatures[feature] += track[feature]; }); } const allAudioFeatures = { popularity, explicitness: explicitCount, ...audioFeatures }; for (let key in allAudioFeatures) { allAudioFeatures[key] /= fetchedFeatures.length; } for (let key in ownedAudioFeatures) { ownedAudioFeatures[key] /= ownedTrackCount; } const ownedStats = { audioFeatures: ownedAudioFeatures, trackCount: ownedTrackCount, totalDuration: ownedDuration, artists: ownedTopArtists, artistCount: Object.keys(ownedArtists).length, genres: ownedTopGenres, genresDenominator: ownedTopGenresTotal, playlistCount: indexOfFirstNotOwned > 0 ? indexOfFirstNotOwned : 0, albums: ownedTopAlbums, years: ownedReleaseYears, yearsDenominator: ownedReleaseYearsTotal }; const allStats = { playlistCount: playlists.length, audioFeatures: allAudioFeatures, trackCount, totalDuration: duration, artistCount: Object.keys(artists).length, artists: topArtists, genres: topGenres, genresDenominator: topGenresTotal, albums: topAlbums, years: releaseYears, yearsDenominator: releaseYearsTotal }; if (set) { if (option === "all" && allStats.playlistCount) setLibrary(allStats); else if (option === "owned" && ownedStats.playlistCount) setLibrary(ownedStats); else return setLibrary(300); } Spicetify.LocalStorage.set(`stats:library:all`, JSON.stringify(allStats)); Spicetify.LocalStorage.set(`stats:library:owned`, JSON.stringify(ownedStats)); console.log("total library fetch time:", window.performance.now() - start); } catch (e) { console.error(e); setLibrary(200); } }; import_react26.default.useEffect(() => { updatePageCache(3, fetchData, activeOption.id, true); }, []); import_react26.default.useEffect(() => { fetchData(activeOption.id); }, [activeOption]); const refresh = () => { fetchData(activeOption.id, true); }; const props = { title: "Library Analysis", headerEls: [dropdown, /* @__PURE__ */ import_react26.default.createElement(refresh_button_default, { callback: refresh }), /* @__PURE__ */ import_react26.default.createElement(settings_button_default, { configWrapper })] }; switch (library) { case 300: return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react26.default.createElement(status_default, { icon: "error", heading: "No Playlists In Your Library", subheading: "Try adding some playlists first" })); case 200: return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react26.default.createElement(status_default, { icon: "error", heading: "Failed to Fetch Stats", subheading: "Make an issue on Github" })); case 100: return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react26.default.createElement(status_default, { icon: "library", heading: "Analysing your Library", subheading: "This may take a while" })); } const statCards = Object.entries(library.audioFeatures).map(([key, value]) => { return /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { label: key, value }); }); const artistCards = library.artists.slice(0, 10).map((artist) => { return /* @__PURE__ */ import_react26.default.createElement(spotify_card_default, { type: "artist", uri: artist.uri, header: artist.name, subheader: `Appears in ${artist.freq} tracks`, imageUrl: artist.image }); }); const albumCards = library.albums.map((album) => { return /* @__PURE__ */ import_react26.default.createElement(spotify_card_default, { type: "album", uri: album.uri, header: album.name, subheader: `Appears in ${album.freq} tracks`, imageUrl: album.image }); }); return /* @__PURE__ */ import_react26.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react26.default.createElement("section", { className: "stats-libraryOverview" }, /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { label: "Total Playlists", value: library.playlistCount.toString() }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { label: "Total Tracks", value: library.trackCount.toString() }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { label: "Total Artists", value: library.artistCount.toString() }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { label: "Total Minutes", value: Math.floor(library.totalDuration / 60).toString() }), /* @__PURE__ */ import_react26.default.createElement(stat_card_default, { label: "Total Hours", value: (library.totalDuration / (60 * 60)).toFixed(1) })), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { title: "Most Frequent Genres" }, /* @__PURE__ */ import_react26.default.createElement(genres_card_default, { genres: library.genres, total: library.genresDenominator }), /* @__PURE__ */ import_react26.default.createElement(inline_grid_default, { special: true }, statCards)), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { title: "Most Frequent Artists" }, /* @__PURE__ */ import_react26.default.createElement(inline_grid_default, null, artistCards)), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { title: "Most Frequent Albums" }, /* @__PURE__ */ import_react26.default.createElement(inline_grid_default, null, albumCards)), /* @__PURE__ */ import_react26.default.createElement(shelf_default, { title: "Release Year Distribution" }, /* @__PURE__ */ import_react26.default.createElement(genres_card_default, { genres: library.years, total: library.yearsDenominator }))); }; var library_default = import_react26.default.memo(LibraryPage); // src/pages/charts.tsx var import_react27 = __toESM(require_react()); var DropdownOptions5 = [ { id: "artists", name: "Top Artists" }, { id: "tracks", name: "Top Tracks" } ]; var ChartsPage = ({ configWrapper }) => { const [chartData, setChartData] = import_react27.default.useState(100); const [dropdown, activeOption, setActiveOption] = useDropdownMenu_default(DropdownOptions5, "stats:charts"); async function fetchChartData(type, force, set = true) { if (!force) { let storedData = Spicetify.LocalStorage.get(`stats:charts:${type}`); if (storedData) return setChartData(JSON.parse(storedData)); } const api_key = configWrapper.config["api-key"]; if (!api_key) return setChartData(200); const response = await apiRequest("charts", LASTFM.charts(api_key, type)); if (!response) return setChartData(500); const data = response[type].track || response[type].artist; const cardData = await (type == "artists" ? convertArtistData(data) : convertTrackData(data)); if (type === "tracks") { const likedArray = await checkLiked(cardData.map((track) => track.id)); if (!likedArray) return setChartData(200); cardData.forEach((track, index) => { track.liked = likedArray[index]; }); } if (set) setChartData(cardData); Spicetify.LocalStorage.set(`stats:charts:${type}`, JSON.stringify(cardData)); } import_react27.default.useEffect(() => { updatePageCache(4, fetchChartData, activeOption.id, "charts"); }, []); import_react27.default.useEffect(() => { fetchChartData(activeOption.id); }, [activeOption]); const refresh = () => { fetchChartData(activeOption.id, true); }; const props = { title: "Top Albums", headerEls: [dropdown, /* @__PURE__ */ import_react27.default.createElement(refresh_button_default, { callback: refresh }), /* @__PURE__ */ import_react27.default.createElement(settings_button_default, { configWrapper })] }; switch (chartData) { case 200: return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react27.default.createElement(status_default, { icon: "error", heading: "No API Key", subheading: "Please enter your Last.fm API key in the settings menu." })); case 500: return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react27.default.createElement(status_default, { icon: "error", heading: "Error", subheading: "An error occurred while fetching the data." })); case 100: return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react27.default.createElement(status_default, { icon: "library", heading: "Loading", subheading: "Fetching data from Last.fm..." })); } if (!chartData[0]?.album) { const artistCards = chartData.map((artist, index) => { const type = artist.uri.startsWith("https") ? "lastfm" : "artist"; return /* @__PURE__ */ import_react27.default.createElement(spotify_card_default, { type, uri: artist.uri, header: artist.name, subheader: `#${index + 1} Artist`, imageUrl: artist.image }); }); props.title = `Charts - Top Artists`; return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react27.default.createElement("div", { className: `main-gridContainer-gridContainer grid` }, artistCards)); } else { const date = new Date().toLocaleDateString("en-US", { year: "numeric", month: "2-digit", day: "2-digit" }); const infoToCreatePlaylist = { playlistName: `Charts - Top Tracks - ${date}`, itemsUris: chartData.map((track) => track.uri) }; const trackRows = chartData.map((track, index) => /* @__PURE__ */ import_react27.default.createElement(track_row_default, { index: index + 1, ...track, uris: chartData.map((track2) => track2.uri) })); props.title = `Charts - Top Tracks`; return /* @__PURE__ */ import_react27.default.createElement(page_container_default, { ...props, infoToCreatePlaylist }, /* @__PURE__ */ import_react27.default.createElement(tracklist_default, null, trackRows)); } }; var charts_default = import_react27.default.memo(ChartsPage); // src/pages/top_albums.tsx var import_react28 = __toESM(require_react()); var topAlbumsReq = async (time_range, configWrapper) => { const { config } = configWrapper; if (!config["api-key"] || !config["lastfm-user"]) return 300; const { ["lastfm-user"]: user, ["api-key"]: key } = config; const response = await apiRequest("lastfm", LASTFM.topalbums(user, key, time_range)); if (!response) return 200; return await convertAlbumData(response.topalbums.album); }; var DropdownOptions6 = [ { id: "short_term", name: "Past Month" }, { id: "medium_term", name: "Past 6 Months" }, { id: "long_term", name: "All Time" } ]; var AlbumsPage = ({ configWrapper }) => { const { LocalStorage } = Spicetify; const [topAlbums, setTopAlbums] = import_react28.default.useState(100); const [dropdown, activeOption] = useDropdownMenu_default(DropdownOptions6, "stats:top-albums"); const fetchTopAlbums2 = async (time_range, force, set = true) => { if (!force) { let storedData = LocalStorage.get(`stats:top-albums:${time_range}`); if (storedData) return setTopAlbums(JSON.parse(storedData)); } const start = window.performance.now(); const topAlbums2 = await topAlbumsReq(time_range, configWrapper); if (set) setTopAlbums(topAlbums2); LocalStorage.set(`stats:top-albums:${time_range}`, JSON.stringify(topAlbums2)); console.log("total albums fetch time:", window.performance.now() - start); }; import_react28.default.useEffect(() => { updatePageCache(5, fetchTopAlbums2, activeOption.id); }, []); import_react28.default.useEffect(() => { fetchTopAlbums2(activeOption.id); }, [activeOption]); const refresh = () => { fetchTopAlbums2(activeOption.id, true); }; const props = { title: "Top Albums", headerEls: [dropdown, /* @__PURE__ */ import_react28.default.createElement(refresh_button_default, { callback: refresh }), /* @__PURE__ */ import_react28.default.createElement(settings_button_default, { configWrapper })] }; switch (topAlbums) { case 300: return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react28.default.createElement(status_default, { icon: "error", heading: "No API Key or Username", subheading: "Please enter these in the settings menu" })); case 200: return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react28.default.createElement(status_default, { icon: "error", heading: "Failed to Fetch Top Artists", subheading: "An error occurred while fetching the data" })); case 100: return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react28.default.createElement(status_default, { icon: "library", heading: "Loading", subheading: "Fetching data..." })); } const albumCards = topAlbums.map((album, index) => { const type = album.uri.startsWith("https") ? "lastfm" : "album"; return /* @__PURE__ */ import_react28.default.createElement(spotify_card_default, { type, uri: album.uri, header: album.name, subheader: `#${index + 1} Album`, imageUrl: album.image }); }); return /* @__PURE__ */ import_react28.default.createElement(page_container_default, { ...props }, /* @__PURE__ */ import_react28.default.createElement("div", { className: `main-gridContainer-gridContainer grid` }, albumCards)); }; var top_albums_default = import_react28.default.memo(AlbumsPage); // package.json var version = "0.3.3"; // src/app.tsx var checkForUpdates = (setNewUpdate) => { fetch("https://api.github.com/repos/harbassan/spicetify-apps/releases").then((res) => res.json()).then( (result) => { const releases = result.filter((release) => release.name.startsWith("stats")); setNewUpdate(releases[0].name.slice(7) !== version); }, (error) => { console.log("Failed to check for updates", error); } ); }; var App = () => { const [config, setConfig] = import_react29.default.useState({ ...SpicetifyStats.ConfigWrapper.Config }); const launchModal = () => { SpicetifyStats.ConfigWrapper.launchModal(setConfig); }; const configWrapper = { config, launchModal }; const pages = { ["Artists"]: /* @__PURE__ */ import_react29.default.createElement(top_artists_default, { configWrapper }), ["Tracks"]: /* @__PURE__ */ import_react29.default.createElement(top_tracks_default, { configWrapper }), ["Albums"]: /* @__PURE__ */ import_react29.default.createElement(top_albums_default, { configWrapper }), ["Genres"]: /* @__PURE__ */ import_react29.default.createElement(top_genres_default, { configWrapper }), ["Library"]: /* @__PURE__ */ import_react29.default.createElement(library_default, { configWrapper }), ["Charts"]: /* @__PURE__ */ import_react29.default.createElement(charts_default, { configWrapper }) }; const tabPages = ["Artists", "Tracks", "Albums", "Genres", "Library", "Charts"].filter( (page) => configWrapper.config[`show-${page.toLowerCase()}`] ); const [navBar, activeLink, setActiveLink] = useNavigationBar_default(tabPages); const [hasPageSwitched, setHasPageSwitched] = import_react29.default.useState(false); const [newUpdate, setNewUpdate] = import_react29.default.useState(false); import_react29.default.useEffect(() => { setActiveLink(Spicetify.LocalStorage.get("stats:active-link") || "Artists"); checkForUpdates(setNewUpdate); setHasPageSwitched(true); }, []); import_react29.default.useEffect(() => { Spicetify.LocalStorage.set("stats:active-link", activeLink); }, [activeLink]); if (!hasPageSwitched) { return /* @__PURE__ */ import_react29.default.createElement(import_react29.default.Fragment, null); } return /* @__PURE__ */ import_react29.default.createElement("div", { id: "stats-app" }, navBar, newUpdate && /* @__PURE__ */ import_react29.default.createElement("div", { className: "new-update" }, "New app update available! Visit", " ", /* @__PURE__ */ import_react29.default.createElement("a", { href: "https://github.com/harbassan/spicetify-apps/releases" }, "harbassan/spicetify-apps"), " to install."), pages[activeLink]); }; var app_default = App; // ../../../AppData/Local/Temp/spicetify-creator/index.jsx var import_react30 = __toESM(require_react()); function render() { return /* @__PURE__ */ import_react30.default.createElement(app_default, null); } return __toCommonJS(spicetify_creator_exports); })(); const render=()=>stats.default();