// @ts-chec // NAME: Sort By Play Count // AUTHOR: Tetrax-10 // DESCRIPTION: Sorts Songs by Play Count, Popularity, Release date, Global Scrobbles, Personal Scrobbles Using Spotify and Last.FM /// let sortByPlayCount = 0; (async function sortByPlay() { if (!(Spicetify.Platform && Spicetify.React) && sortByPlayCount < 200) { setTimeout(sortByPlay, 300); sortByPlayCount++; return; } let { Type } = Spicetify.URI; let LFMApiKey = "44654ea047786d90338c17331a5f5d95"; const { React } = Spicetify; const { useState } = React; let sortedFolderName = "Sorted Playlist"; ////////////////////////////////////// CONFIG /////////////////////////////////////////// async function getLocalStorageDataFromKey(key) { return Spicetify.LocalStorage.get(key); } async function setLocalStorageDataWithKey(key, value) { Spicetify.LocalStorage.set(key, value); } function notification(text) { Spicetify.showNotification(text); } async function validateLocalStorage() { if (!CONFIG.lastFmUserName) { setLastFmUsername(); return false; } return true; } async function getConfig() { try { const parsed = JSON.parse(await getLocalStorageDataFromKey("sortByPlayCount:settings")); if (parsed && typeof parsed === "object") { return parsed; } throw "Config error Sort By Play Count"; } catch { await setLocalStorageDataWithKey("sortByPlayCount:settings", `{}`); // default settings return { myScrobbles: true, lastfmPlayCount: false, ascending: false, queue: false, isPrivate: true, artistMode: "all", removeDuplicate: true, artistNameMust: false, lastFmUserName: "", }; } } async function saveConfig() { await setLocalStorageDataWithKey("sortByPlayCount:settings", JSON.stringify(CONFIG)); } const CONFIG = await getConfig(); await saveConfig(); ////////////////////////////////////// UI /////////////////////////////////////////// let style = React.createElement( "style", null, `.popup-row::after { content: ""; display: table; clear: both; } .popup-row .col { display: flex; padding: 10px 0; align-items: center; } .popup-row .col.description { float: left; padding-right: 15px; } .popup-row .col.action { float: right; text-align: right; } .popup-row .div-title { color: var(--spice-text); } .popup-row .divider { height: 2px; border-width: 0; background-color: var(--spice-button-disabled); } .popup-row .space { margin-bottom: 20px; visibility: hidden; } .popup-row .info { /* font-size: 13px; */ } .popup-row .demo { font-size: 13px; color: #59CE8F; } .popup-row .little-space { margin-bottom: 10px; } .popup-row .inputbox { display: flex; flex-direction: column; padding: 15px; border-radius: 15px; border: 0; box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.06); } button.checkbox { align-items: center; border: 0px; border-radius: 50%; background-color: rgba(var(--spice-rgb-shadow), 0.7); color: var(--spice-text); cursor: pointer; display: flex; margin-inline-start: 12px; padding: 8px; } button.checkbox.disabled { color: rgba(var(--spice-rgb-text), 0.3); } select { color: var(--spice-text); background: rgba(var(--spice-rgb-shadow), 0.7); border: 0; height: 32px; } ::-webkit-scrollbar { width: 8px; } .login-button { background-color: var(--spice-button); border-radius: 8px; border-style: none; color: var(--spice-text); cursor: pointer; font-size: 14px; height: 40px; margin-right: 20px; padding: 5px 10px; text-align: center; } .green { background-color: #6BCB77; color: #25316D; } .red { background-color: #bf616a; }` ); function setLastFmUsername() { let addLoginContainer = React.createElement( "div", null, style, React.createElement( "div", { className: "popup-row" }, React.createElement("p", { className: "col description" }, "With Last.FM Integration you can now sort your top songs (your personal play count) with help of this extension. This extension can sort playlists, albums, even artist page with LastFM."), React.createElement( "a", { href: "https://github.com/Tetrax-10/Spicetify-Extensions/tree/master/Sort-by-Play-count#need-to-connect-spotify-and-lastfm-for-personal-scrobbles-to-work-optional-step", className: "demo", }, "Click for demo" ) ), React.createElement( "div", { className: "popup-row" }, React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "divider" }, null)), React.createElement("p", { className: "popup-row" }, React.createElement("p", null, "Enter your Last.FM Username :")), React.createElement("div", { className: "little-space" }, null), React.createElement("div", { className: "little-space" }, null), React.createElement("p", { className: "popup-row" }, React.createElement("input", { className: "inputbox", placeholder: CONFIG.lastFmUserName ? CONFIG.lastFmUserName : "Last.FM Username", required: true })), React.createElement("div", { className: "little-space" }, null), React.createElement("div", { className: "little-space" }, null), React.createElement(ButtonItem, { name: "Save", color: " green", onclickFun: async () => { let name = document.querySelector(".popup-row .inputbox").value.replace(/\n/g, ""); if (name === "" || !name) { alert("The UserName can't be blank"); return; } CONFIG.lastFmUserName = name; await saveConfig(); Spicetify.PopupModal.hide(); }, }) ) ); Spicetify.PopupModal.display({ title: "Connect Last.FM", content: addLoginContainer, }); } function DisplayIcon({ icon, size }) { return React.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "currentColor", dangerouslySetInnerHTML: { __html: icon, }, }); } function checkBoxItem({ name, field, onclickFun = () => {} }) { let [value, setValue] = useState(CONFIG[field]); return React.createElement( "div", { className: "popup-row" }, React.createElement("label", { className: "col description" }, name), React.createElement( "div", { className: "col action" }, React.createElement( "button", { className: "checkbox" + (value ? "" : " disabled"), onClick: async () => { let state = !value; CONFIG[field] = state; setValue(state); await saveConfig(); onclickFun(); }, }, React.createElement(DisplayIcon, { icon: Spicetify.SVGIcons.check, size: 16 }) ) ) ); } function dropDownItem({ name, field, options, onclickFun = () => {} }) { const [value, setValue] = useState(CONFIG[field]); return React.createElement( "div", { className: "popup-row" }, React.createElement("label", { className: "col description" }, name), React.createElement( "div", { className: "col action" }, React.createElement( "select", { value, onChange: async (e) => { setValue(e.target.value); CONFIG[field] = e.target.value; await saveConfig(); onclickFun(); }, }, Object.keys(options).map((item) => React.createElement( "option", { value: item, }, options[item] ) ) ) ) ); } function ButtonItem({ name, color = "", onclickFun }) { return React.createElement( "button", { className: `login-button${color}`, onClick: async () => { onclickFun(); }, }, name ); } function restartPopup() { let DOMcontent = React.createElement( "div", null, style, React.createElement(ButtonItem, { name: "Restart Now", color: " red", onclickFun: async () => { await Spicetify.CosmosAsync.post("sp://desktop/v1/restart"); }, }), React.createElement(ButtonItem, { name: "Restart Later", color: " green", onclickFun: async () => { Spicetify.PopupModal.hide(); }, }) ); Spicetify.PopupModal.display({ title: "Restart Spotify", content: DOMcontent, }); } let settingsDOMContent = React.createElement( "div", null, style, React.createElement("div", { className: "popup-row" }, React.createElement("h3", { className: "div-title" }, "Sort Options")), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "divider" }, null)), React.createElement(checkBoxItem, { name: "Last.FM My Scrobbles", field: "myScrobbles", }), React.createElement(checkBoxItem, { name: "Last.FM Play Count", field: "lastfmPlayCount", }), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "space" }, null)), React.createElement("div", { className: "popup-row" }, React.createElement("h3", { className: "div-title" }, "Sort Settings")), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "divider" }, null)), React.createElement(checkBoxItem, { name: "Ascending Order", field: "ascending", }), React.createElement(checkBoxItem, { name: "Add to Queue (Supports Multi-Device)", field: "queue", }), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "space" }, null)), React.createElement("div", { className: "popup-row" }, React.createElement("h3", { className: "div-title" }, "Create Playlist Settings")), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "divider" }, null)), React.createElement(checkBoxItem, { name: "Make Sorted Playlist Private", field: "isPrivate", }), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "space" }, null)), React.createElement("div", { className: "popup-row" }, React.createElement("h3", { className: "div-title" }, "Artist Sort Settings")), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "divider" }, null)), React.createElement(dropDownItem, { name: "What to Sort in Artist Page", field: "artistMode", options: { all: "All", album: "Albums", single: "Singles & EP", likedSongArtist: "Liked Songs", topTen: "Top 10 Songs", }, }), React.createElement(checkBoxItem, { name: "Remove Duplicate Songs", field: "removeDuplicate", }), React.createElement(checkBoxItem, { name: "Chosen Artist must be Included in Tracks", field: "artistNameMust", }), React.createElement("div", { className: "popup-row" }, React.createElement("hr", { className: "space" }, null)), React.createElement(ButtonItem, { name: "Login with Last.FM", color: " green", onclickFun: () => { Spicetify.PopupModal.hide(); setTimeout(setLastFmUsername, 300); }, }), React.createElement(ButtonItem, { name: "Like on GitHub 👍", onclickFun: () => { window.open("https://github.com/Tetrax-10/Spicetify-Extensions"); }, }) ); function settingsPage() { Spicetify.PopupModal.display({ title: "Sort By Play Count", content: settingsDOMContent, isLarge: true, }); } ////////////////////////////////////// Menu /////////////////////////////////////////// new Spicetify.Menu.Item("Sort By Play Count", false, settingsPage).register(); // Play Count function shouldAddSpotifyPlayCount(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ALBUM: case Type.ARTIST: return true; } return false; } // Popularity function shouldAddSpotifyPopularity(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); if (CONFIG.artistMode == "topTen" && uriObj.type == Type.ARTIST) { return false; } switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ALBUM: case Type.ARTIST: return true; } return false; } // Release date function shouldAddSpotifyReleaseDate(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); if (CONFIG.artistMode == "topTen" && uriObj.type == Type.ARTIST) { return false; } switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ARTIST: return true; } return false; } // Scrobbles function shouldAddLastFmScrobbles(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); if (CONFIG.artistMode == "likedSongArtist" && uriObj.type == Type.ARTIST) { return true; } switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ALBUM: return true; } return false; } // My Scrobbles function shouldAddLastFmMyScrobbles(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); if (!CONFIG.myScrobbles) { return false; } if (CONFIG.artistMode == "likedSongArtist" && uriObj.type == Type.ARTIST) { return true; } switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ALBUM: return true; } return false; } // Last.FM Play Count function shouldAddLastFmPlayCount(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); if (!CONFIG.lastfmPlayCount) { return false; } if (CONFIG.artistMode == "likedSongArtist" && uriObj.type == Type.ARTIST) { return true; } switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ALBUM: return true; } return false; } // Right Click ContextMenu function shouldAddContextMenu(uri) { let uriObj = Spicetify.URI.fromString(uri[0]); switch (uriObj.type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: case Type.ALBUM: case Type.ARTIST: return true; } return false; } let playCountItemSpotify = new Spicetify.ContextMenu.Item( "Play Count", async (rawUri) => { let uriInfo = rawUri[0].split(":"); let type = uriInfo[1]; let uri = uriInfo[2]; await fetchAndPlay(type, uri, "playcount", "spotify", "Play Count"); }, shouldAddSpotifyPlayCount, "play" ); let popularityItemSpotify = new Spicetify.ContextMenu.Item( "Popularity", async (rawUri) => { let uriInfo = rawUri[0].split(":"); let type = uriInfo[1]; let uri = uriInfo[2]; await fetchAndPlay(type, uri, "popularity", "spotify", "Popularity"); }, shouldAddSpotifyPopularity, "heart" ); let releaseDateItemSpotify = new Spicetify.ContextMenu.Item( "Release Date", async (rawUri) => { let uriInfo = rawUri[0].split(":"); let type = uriInfo[1]; let uri = uriInfo[2]; await fetchAndPlay(type, uri, "releaseDate", "spotify", "Release Date"); }, shouldAddSpotifyReleaseDate, "list-view" ); let scrobblesItemLastFM = new Spicetify.ContextMenu.Item( "Scrobbles", async (rawUri) => { let uriInfo = rawUri[0].split(":"); let type = uriInfo[1]; let uri = uriInfo[2]; await fetchAndPlay(type, uri, "scrobbles", "lastfm", "Scrobbles"); }, shouldAddLastFmScrobbles, "volume" ); let personalScrobblesItemLastFM = new Spicetify.ContextMenu.Item( "My Scrobbles", async (rawUri) => { if (!CONFIG.lastFmUserName) { await validateLocalStorage(); return; } let uriInfo = rawUri[0].split(":"); let type = uriInfo[1]; let uri = uriInfo[2]; await fetchAndPlay(type, uri, "personalScrobbles", "lastfm", "My Scrobbles"); }, shouldAddLastFmMyScrobbles, "artist" ); let playCountItemLastFM = new Spicetify.ContextMenu.Item( "Last.FM Play Count", async (rawUri) => { let uriInfo = rawUri[0].split(":"); let type = uriInfo[1]; let uri = uriInfo[2]; await fetchAndPlay(type, uri, "playcount", "lastfm", "Last.FM Play Count"); }, shouldAddLastFmPlayCount, "subtitles" ); new Spicetify.ContextMenu.SubMenu("Sort by", [playCountItemSpotify, popularityItemSpotify, releaseDateItemSpotify, scrobblesItemLastFM, personalScrobblesItemLastFM, playCountItemLastFM], shouldAddContextMenu, false).register(); new Spicetify.Topbar.Button("Add Sorted Songs to Playlist", "plus2px", createSortedPlaylist); ////////////////////////////////////// Fetchers /////////////////////////////////////////// async function createPlaylist(name, tracks, parentFolderURI = null) { return await Spicetify.CosmosAsync.post("sp://core-playlist/v1/rootlist", { operation: "create", playlist: true, uris: tracks, name: name, after: parentFolderURI, }); } async function makePlaylistPrivate(uri) { setTimeout(async () => { await Spicetify.CosmosAsync.post(`sp://core-playlist/v1/playlist/${uri}/set-base-permission`, { permission_level: "BLOCKED", }); }, 1000); } async function createFolder(name) { await Spicetify.Platform.RootlistAPI.createFolder(name, { before: "" }); } async function isFolderCreated(folderName) { let root = await Spicetify.Platform.RootlistAPI.getContents(); let folder = await root.items.find((item) => item.type === "folder" && item.name === folderName); return (await folder) ? folder.uri : false; } let name = "playlist"; function createItemName(itemName, platform, mode, type) { let time = CONFIG.ascending ? "Oldest" : "Latest"; let personal = mode == "personalScrobbles" ? "My " : ""; let order = CONFIG.ascending ? "Least" : "Most"; let like = CONFIG.artistMode == "likedSongArtist" ? "Liked " : ""; let naming = platform == "spotify" ? (mode == "playcount" ? "Played" : "Popular") : mode == "scrobbles" ? "Scrobbled" : "Played"; switch (type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: if (mode == "releaseDate") { name = `${time} Songs from ${itemName}`; break; } name = `${personal}${order} ${naming} Songs from ${itemName}`; break; case Type.ARTIST: if (mode == "releaseDate") { name = `${time} ${like}Songs from ${itemName}`; break; } if (CONFIG.artistMode == "topTen") { name = `Top 10 Songs of ${itemName}`; break; } if (CONFIG.artistMode == "single") { name = `${order} ${naming} Singles from ${itemName}`; break; } if (CONFIG.artistMode == "album") { name = `${order} ${naming} Album Songs from ${itemName}`; break; } if (CONFIG.artistMode == "likedSongArtist") { name = `${personal}${order} ${naming} Liked Songs from ${itemName}`; break; } name = `${order} ${naming} Songs from ${itemName}`; break; case Type.ALBUM: name = `${personal}${order} ${naming} Songs from ${itemName} Album`; break; } } async function createSortedPlaylist() { let folderURI = await isFolderCreated(sortedFolderName); if (folderURI) { try { let playlistSongs = sortedSongs.filter((item) => item.link != "spotify:delimiter").map((item) => item.link); let { uri } = await createPlaylist(name, playlistSongs, folderURI); if (CONFIG.isPrivate) { await makePlaylistPrivate(uri); } notification("Playlist Created"); } catch { notification("Sort some Songs to Create a playlist"); } } else { createFolder(sortedFolderName); createSortedPlaylist(); } } // Sort by "Play Count" or "Popularity" in Playlist Page - Platform Spotify async function fetchPlaylistTracksSpotify(uri, mode, name, type, platform) { let playlistRes = await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/spotify:playlist:${uri}/rows`); let trackHistory = []; let allTracks = []; let filteredTrack = []; let totalTrackCount = playlistRes.rows.length ? playlistRes.rows.length : 0; if (totalTrackCount) { let currentTrack = 0; createItemName((await Spicetify.Platform.PlaylistAPI.getMetadata(`spotify:playlist:${uri}`)).name, platform, mode, type); for (let playlistTrack of playlistRes.rows) { if (mode != "popularity") { currentTrack++; notification(`${currentTrack} / ${totalTrackCount} Songs`); if (playlistTrack.link.split(":")[1] == "track" && playlistTrack.playable && !playlistTrack.isLocal && !trackHistory.includes(playlistTrack.album.link)) { let albumRes = await Spicetify.CosmosAsync.get(`wg://album/v1/album-app/album/${playlistTrack.album.link.split(":")[2]}/desktop`); for (let tracks of albumRes.discs) { for (let track of tracks.tracks) { allTracks.push(track); trackHistory.push(playlistTrack.album.link); } } } for (let track of allTracks) { if (playlistTrack.link == track.uri && track[mode]) { filteredTrack.push({ playcount: track.playcount, popularity: track.popularity, link: track.uri, name: track.name, artist: track.artists[0].name }); } } } else { if (playlistTrack.link.split(":")[1] == "track" && !playlistTrack.isLocal && playlistTrack.playable && playlistTrack.popularity) { filteredTrack.push({ popularity: playlistTrack.popularity, link: playlistTrack.link, name: playlistTrack.name, artist: playlistTrack.artists[0].name }); } } } } else { notification(`${name} data not available! Try other options`); return []; } return filteredTrack; } // Sort by "Release Date" in Playlist Page and Artist Page (liked songs) - Platform Spotify API async function fetchPlaylistTracksReleaseDateSpotifyAPI(uri = null, source = null, mode = null, type = null, platform = null) { let playlistRes; let totalTrackCount; if (source) { playlistRes = source; totalTrackCount = source.length; } else { playlistRes = (await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/spotify:playlist:${uri}/rows`)).rows; totalTrackCount = playlistRes.length ? playlistRes.length : 0; } createItemName((await Spicetify.Platform.PlaylistAPI.getMetadata(`spotify:playlist:${uri}`)).name, platform, mode, type); let resRawArray = []; if (totalTrackCount) { let idString = ""; let count = 0; for (let track of playlistRes) { let info = track.link.split(":"); if (track.playable && !track.isLocal && info[1] == "track") { if (count) { idString += ","; } idString += info[2]; count++; if (count == 50) { idString += "|"; count = 0; } } } let requestArray = idString ? idString.split("|") : []; for (let apiString of requestArray) { if (apiString.length) { let rawRes = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/tracks?ids=${apiString}`); for (let track of rawRes.tracks) { if (track.album.release_date) { resRawArray.push({ releaseDate: track.album.release_date, link: track.uri, name: track.name, artist: track.artists[0].name }); } } } } } else { notification("Release Date data not available! Try other options"); return []; } return resRawArray; } let artistFetchTypeCount = { album: 0, single: 0 }; // scans every single songs from album or singles and creates valid object for sortByModeForQueue async function scanForTracksFromAlbums(res, allCount, artistName, mode, type) { let allTracks = []; for (let albums of res) { let albumsRes; try { if (albums.discs) { albumsRes = albums; } else { albumsRes = await Spicetify.CosmosAsync.get(`wg://album/v1/album-app/album/${albums.uri}/desktop`); } } catch (error) {} artistFetchTypeCount[type]++; notification(`${artistFetchTypeCount[type]} / ${allCount} ${type}s`); let releaseDate = `${albumsRes.year ? albumsRes.year : ""}${albumsRes.month ? "-" + albumsRes.month : ""}${albumsRes.day ? "-" + albumsRes.day : ""}`; for (let disc of albumsRes.discs) { for (let track of disc.tracks) { let condition = true; if (CONFIG.artistNameMust) { let artists = track.artists.map((artist) => artist.name); if (!artists.includes(artistName)) { condition = false; } } mode = mode == "releaseDate" ? "uri" : mode; if (track.playable && track[mode] && condition) { allTracks.push({ playcount: track.playcount, popularity: track.popularity, duration: track.duration, link: track.uri, name: track.name, artist: track.artists[0].name, releaseDate: releaseDate, }); } } } } return allTracks; } // Fetch Artist all albums songs or all singles songs or even both - Platform Spotify async function fetchArtistTracksSpotify(uri, mode, name, type, platform) { let artistRes = await Spicetify.CosmosAsync.get(`wg://artist/v1/${uri}/desktop?format=json`); let artistName = artistRes.info.name; let artistAlbums = artistRes.releases.albums; let artistSingles = artistRes.releases.singles; let allArtistAlbumsTracks = []; let allArtistSinglesTracks = []; let allAlbumsCount = artistAlbums.total_count; let allSinglesCount = artistSingles.total_count; if (allAlbumsCount != 0 && CONFIG.artistMode != "single") { allArtistAlbumsTracks = await scanForTracksFromAlbums(artistAlbums.releases, allAlbumsCount, artistName, mode, "album"); } if (allSinglesCount != 0 && CONFIG.artistMode != "album") { allArtistSinglesTracks = await scanForTracksFromAlbums(artistSingles.releases, allSinglesCount, artistName, mode, "single"); } let allArtistTracks = allArtistAlbumsTracks.concat(allArtistSinglesTracks); if (!allArtistTracks.length) { notification(`${name} data not available! Try other options`); return []; } if (CONFIG.removeDuplicate) { // sort the Songs by popularity so the original song will be on top of the array when compared to its duplicate songs allArtistTracks = await sortByMode(allArtistTracks, "popularity", false); // Duplicate songs will have same playcount and duration buy different popularity. // Song with higher popularity will be listed on top while searching a song in spotify while duplicate once (low popularity) will be below. // So this fill remove all songs that have same playcount and duration except the first occurrence. // as we have sorted songs by popularity the original will be on top allArtistTracks = allArtistTracks.filter((track_, index, array) => array.findIndex((track) => (track.playcount == track_.playcount && track.duration == track_.duration) || track.name == track_.name || track.playcount == track_.playcount) == index); } createItemName(artistRes.info.name, platform, mode, type); return allArtistTracks; } // Artist Page liked songs sort - Platform Spotify async function fetchArtistLikedTracksSpotify(uri, mode, name, type, platform) { let artistRes = await Spicetify.CosmosAsync.get(`sp://core-collection/unstable/@/list/tracks/artist/${uri}?responseFormat=protobufJson`); let trackHistory = []; let allTracks = []; let filteredTrack = []; let unFilteredTrack = []; let totalTrackCount = artistRes.item ? (artistRes.item.length ? artistRes.item.length : 0) : null; if (totalTrackCount) { createItemName((await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/artists/${uri}`)).name, platform, mode, type); for (let artistTrack of artistRes.item) { let currentTrack = 0; if (mode == "playcount") { currentTrack++; notification(`${currentTrack} / ${totalTrackCount} Songs`); let artistTrackMetaData = artistTrack.trackMetadata; if (artistTrackMetaData.playable && !artistTrackMetaData.isLocal && !trackHistory.includes(artistTrackMetaData.album.link)) { let albumRes = await Spicetify.CosmosAsync.get(`wg://album/v1/album-app/album/${artistTrackMetaData.album.link.split(":")[2]}/desktop`); for (let tracks of albumRes.discs) { for (let track of tracks.tracks) { allTracks.push(track); trackHistory.push(await artistTrackMetaData.album.link); } } } for (let track of allTracks) { if (artistTrackMetaData.link == track.uri && track.playcount) { filteredTrack.push({ playcount: track.playcount, popularity: track.popularity, link: track.uri, name: track.name, artist: track.artists[0].name }); } } } else if (mode == "popularity") { let playlistTrack = artistTrack.trackMetadata; if (playlistTrack.link.split(":")[1] == "track" && !playlistTrack.isLocal && playlistTrack.playable && playlistTrack.popularity) { filteredTrack.push({ popularity: playlistTrack.popularity, link: playlistTrack.link, name: playlistTrack.name, artist: playlistTrack.artist[0].name }); } } else { unFilteredTrack.push(artistTrack.trackMetadata); } } if (mode == "releaseDate") { filteredTrack = await fetchPlaylistTracksReleaseDateSpotifyAPI(null, unFilteredTrack); } } else { notification(`${name} data not available! Try other options`); return []; } return filteredTrack; } // Artist Page Top 10 Songs sort - Platform Spotify async function fetchArtistTopTenTracksSpotify(uri, mode, type, platform) { let artistRes = await Spicetify.CosmosAsync.get(`wg://artist/v1/${uri}/desktop?format=json`); let topTenTracks = []; createItemName(artistRes.info.name, platform, mode, type); artistRes.top_tracks.tracks.map((track) => { if (track.playcount) { topTenTracks.push({ playcount: track.playcount, link: track.uri, name: track.name, artist: artistRes.info.name }); } }); if (topTenTracks.length == 0) { notification("Play Count data not available!"); return []; } return topTenTracks; } // Sort Songs in Album Page - Platform Spotify async function fetchAlbumTracksSpotify(uri, mode, name, type, platform) { let res = await Spicetify.CosmosAsync.get(`wg://album/v1/album-app/album/${uri}/desktop`); let availables = []; createItemName(res.name, platform, mode, type); for (let disc of res.discs) { disc.tracks.forEach((track) => { if (track.playable) { availables.push(track); } }); } let unsortedArray = availables .filter((track) => track.playable && track[mode]) .map(async (track) => { return { playcount: track.playcount, popularity: track.popularity, link: track.uri, name: track.name, artist: track.artists[0].name }; }); if (unsortedArray.length == 0) { notification(`${name} data not available! Try other options`); return []; } return Promise.all(unsortedArray); } // get data from Last.FM async function fetchTrackInfoFromLastFM(artist, trackName, lastFmUsername) { let url = `https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=${LFMApiKey}&artist=${encodeURIComponent(artist)}&track=${encodeURIComponent(trackName)}&format=json&username=${encodeURIComponent(lastFmUsername)}`; try { let initialRequest = await fetch(url); let response = await initialRequest.json(); return response; } catch (error) {} } // Last.FM data to valid object for sortByModeForQueue async function createDataFromATrack(track, mode, type = "") { let artistName = ""; let link = ""; if (type) { link = track.link; artistName = track.artist[0].name; } else { link = track.uri; artistName = track.artists[0].name; } let trackInfo = await fetchTrackInfoFromLastFM(artistName, track.name, CONFIG.lastFmUserName); if (mode == "scrobbles") { mode = "listeners"; } if (mode == "personalScrobbles") { mode = "userplaycount"; } try { if (await trackInfo.message) { return null; } } catch (error) {} try { if (await trackInfo.track) { if (trackInfo.track[mode]) { return { playcount: trackInfo.track.listeners ? trackInfo.track.listeners : -1, scrobbles: trackInfo.track.playcount ? trackInfo.track.playcount : -1, personalScrobbles: trackInfo.track.userplaycount ? trackInfo.track.userplaycount : -1, link: link, name: track.name, artist: artistName, }; } } } catch (error) {} return null; } // Sort by "Scrobbles", "My Scrobbles" and "Last.FM Play Count" in Playlist Page - platform last.FM async function fetchPlaylistTracksLastfm(uri, mode, name, type, platform) { let res = await Spicetify.Platform.PlaylistAPI.getContents(`spotify:playlist:${uri}`); let totalTrackCount = res.items.length ? res.items.length : 0; let unsortedArray; if (totalTrackCount) { createItemName((await Spicetify.Platform.PlaylistAPI.getMetadata(`spotify:playlist:${uri}`)).name, platform, mode, type); unsortedArray = await res.items.filter((track) => track.type == "track" && track.isPlayable && !track.isLocal).map(async (track) => await createDataFromATrack(track, mode)); } if (!totalTrackCount || (await unsortedArray.length) == 0 || (await unsortedArray[0]) == null) { notification(`${name} data not available! Try other options`); return []; } return Promise.all(unsortedArray); } // Sort Liked Songs by "Scrobbles", "My Scrobbles" and "Last.FM Play Count" in Artist Page - platform last.FM async function fetchArtistLikedTracksLastfm(uri, mode, name, type, platform) { let res = await Spicetify.CosmosAsync.get(`sp://core-collection/unstable/@/list/tracks/artist/${uri}?responseFormat=protobufJson`); let totalTrackCount = res.item.length ? res.item.length : 0; let unsortedArray; if (totalTrackCount) { createItemName((await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/artists/${uri}`)).name, platform, mode, type); unsortedArray = await res.item.filter((artistTrack) => artistTrack.trackMetadata.playable && !artistTrack.trackMetadata.isLocal).map(async (artistTrack) => await createDataFromATrack(artistTrack.trackMetadata, mode, "artist")); } if (!totalTrackCount || (await unsortedArray.length) == 0 || (await unsortedArray[0]) == null) { notification(`${name} data not available! Try other options`); return []; } return Promise.all(unsortedArray); } // Sort Songs in Album Page - Platform Last.FM async function fetchAlbumTracksLastfm(uri, mode, name, type, platform) { let res = await Spicetify.CosmosAsync.get(`wg://album/v1/album-app/album/${uri}/desktop`); let availables; createItemName(res.name, platform, mode, type); for (let disc of res.discs) { availables = disc.tracks.filter((track) => track.playable); } let unsortedArray = await availables.filter((track) => track.playable).map(async (track) => await createDataFromATrack(track, mode)); if ((await unsortedArray.length) == 0 || (await unsortedArray[0]) == null) { notification(`${name} data not available! Try other options`); return []; } return Promise.all(unsortedArray); } /////////////////////////////////// SORT AND QUEUE /////////////////////////////////////// let sortedSongs; async function sortByMode(unsortedArray, mode, enableReverse = true) { let sortedArray; if (mode == "releaseDate") { sortedArray = await unsortedArray.sort((track1, track2) => (track1.releaseDate < track2.releaseDate ? 1 : -1)); } else { sortedArray = await unsortedArray.filter((track) => track).sort((track1, track2) => track2[mode] - track1[mode]); } if (CONFIG.ascending && enableReverse) { await sortedArray.reverse(); } sortedSongs = await sortedArray; // console.log(mode, await sortedArray); // uncomment to see what you gonna hear return await sortedArray; } async function formatForQueue(sortedArray) { sortedArray.push({ link: "spotify:delimiter" }); sortedArray = sortedArray.map((item) => ({ uri: item.link, provider: CONFIG.queue ? "queue" : "context", metadata: { is_queued: CONFIG.queue ? "true" : "false", }, })); return await sortedArray; } async function addToQueue(sortedArray, context, type, mode, platform) { let count = sortedArray.length - 1; if (count == 0) { return; } await Spicetify.Platform.PlayerAPI.clearQueue(); await Spicetify.CosmosAsync.put("sp://player/v2/main/queue", { queue_revision: Spicetify.Queue?.queueRevision, next_tracks: sortedArray, prev_tracks: Spicetify.Queue?.prevTracks, }); if (!CONFIG.queue) { await Spicetify.CosmosAsync.post("sp://player/v2/main/update", { context: { uri: context, url: `context://${context}`, }, }); } Spicetify.Player.next(); let time = CONFIG.ascending ? "Oldest" : "Latest"; let personal = mode == "personalScrobbles" ? "Your " : ""; let order = CONFIG.ascending ? "Least" : "Most"; let like = CONFIG.artistMode == "likedSongArtist" ? "Liked " : ""; let naming = platform == "spotify" ? (mode == "playcount" ? "Played" : "Popular") : mode == "scrobbles" ? "Scrobbled" : "Played"; switch (type) { case Type.PLAYLIST: case Type.PLAYLIST_V2: if (mode == "releaseDate") { notification(`${time} ${count} Songs`); break; } notification(`${personal}${order} ${naming} ${count} Songs`); break; case Type.ARTIST: if (mode == "releaseDate") { notification(`${time} ${count} ${like}Songs`); break; } if (CONFIG.artistMode == "topTen") { let order = CONFIG.ascending ? "Least" : "Most"; notification(`${order} Played Top 10 Songs`); break; } if (CONFIG.artistMode == "single") { notification(`Sorted ${artistFetchTypeCount.single} Singles, Totally ${count} Songs`); break; } if (CONFIG.artistMode == "album") { notification(`Sorted ${artistFetchTypeCount.album} Albums, Totally ${count} Songs`); break; } if (CONFIG.artistMode == "likedSongArtist") { notification(`${personal}${order} ${naming} ${count} Liked Songs`); break; } notification(`Sorted ${artistFetchTypeCount.album} Albums, ${artistFetchTypeCount.single} Singles, Totally ${count} Songs`); break; case Type.ALBUM: notification(`${personal}${order} ${naming} ${count} Songs`); break; } artistFetchTypeCount.album = 0; artistFetchTypeCount.single = 0; } ////////////////////////////////////// MAIN ///////////////////////////////////////// async function fetchAndPlay(type, uri, mode, platform, name) { try { let list; switch (type + platform) { case Type.PLAYLIST + "spotify": case Type.PLAYLIST_V2 + "spotify": if (mode == "releaseDate") { list = await fetchPlaylistTracksReleaseDateSpotifyAPI(uri, null, mode, type, platform); break; } list = await fetchPlaylistTracksSpotify(uri, mode, name, type, platform); break; case Type.PLAYLIST + "lastfm": case Type.PLAYLIST_V2 + "lastfm": list = await fetchPlaylistTracksLastfm(uri, mode, name, type, platform); break; case Type.ARTIST + "spotify": if (CONFIG.artistMode == "likedSongArtist") { list = await fetchArtistLikedTracksSpotify(uri, mode, name, type, platform); break; } if (CONFIG.artistMode == "topTen") { list = await fetchArtistTopTenTracksSpotify(uri, mode, type, platform); break; } list = await fetchArtistTracksSpotify(uri, mode, name, type, platform); break; case Type.ARTIST + "lastfm": list = await fetchArtistLikedTracksLastfm(uri, mode, name, type, platform); break; case Type.ALBUM + "spotify": list = await fetchAlbumTracksSpotify(uri, mode, name, type, platform); break; case Type.ALBUM + "lastfm": list = await fetchAlbumTracksLastfm(uri, mode, name, type, platform); break; } if (!list.length) { return; } let sortedSongs = await sortByMode(list, mode); let queueList = await formatForQueue(await sortedSongs); await addToQueue(await queueList, `spotify:${type}:${uri}`, type, mode, platform); } catch (error) { notification(`${error}`); console.log(error); } } })();