Last active
February 7, 2021 14:19
-
-
Save orenyomtov/2c2b8f430efb8841064ee67f35055a00 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(async function () { | |
async function fetchAccessToken() { | |
let r = await fetch('https://open.spotify.com/get_access_token?reason=transport&productType=web_player'); | |
let j = await r.json(); | |
return j.accessToken; | |
} | |
async function fetchUserId() { | |
let r = await fetch('https://api.spotify.com/v1/me', { headers: { authorization: `Bearer ${accessToken}` } }); | |
let j = await r.json(); | |
return j.id; | |
} | |
async function fetchAllTracksInPlaylist(playlistId, next = false) { | |
if (next === null) | |
return [] | |
let url = next ? next : `https://api.spotify.com/v1/playlists/${playlistId}/tracks?limit=100&market=from_token&type=track,episode`; | |
let r = await fetch(url, { headers: { authorization: `Bearer ${accessToken}` } }); | |
let j = await r.json(); | |
return extractTrackIds(j).concat(await fetchAllTracksInPlaylist(playlistId, j.next)) | |
} | |
function extractTrackIds(j) { | |
return j.items.map(item => item.track.id); | |
} | |
async function fetchAllUserPlaylists(userId, next = false) { | |
if (next === null) | |
return [] | |
let url = next ? next : `https://api.spotify.com/v1/users/${userId}/playlists?offset=0&limit=50`; | |
let r = await fetch(url, { headers: { authorization: `Bearer ${accessToken}` } }); | |
let j = await r.json(); | |
return (await extractPlaylists(j.items.filter(x => x.owner.id == userId))).concat(await fetchAllUserPlaylists(userId, j.next)) | |
} | |
async function extractPlaylists(playlists) { | |
return await Promise.all(playlists.map(async (item) => { | |
return { | |
"name": item.name, | |
"href": item.external_urls.spotify, | |
"id": item.id, | |
"tracks": await fetchAllTracksInPlaylist(item.id) | |
} | |
})); | |
} | |
function injectCSS() { | |
var styles = ` | |
/* Tooltip container */ | |
.tooltip { | |
position: relative; | |
display: inline-block; | |
} | |
/* Tooltip text */ | |
.tooltip .tooltiptext { | |
visibility: hidden; | |
width: 250px; | |
background-color: #555; | |
color: #fff; | |
text-align: center; | |
padding: 5px 0; | |
border-radius: 6px; | |
/* Position the tooltip text */ | |
position: absolute; | |
z-index: 1; | |
bottom: auto; | |
left: 128%; | |
/* Fade in tooltip */ | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
/* Show the tooltip text when you mouse over the tooltip container */ | |
.tooltip:hover .tooltiptext { | |
visibility: visible; | |
opacity: 1; | |
} | |
` | |
var styleSheet = document.createElement("style") | |
styleSheet.type = "text/css" | |
styleSheet.innerText = styles | |
document.head.appendChild(styleSheet) | |
} | |
function addPlaylistCounters() { | |
let trackRows = Array.from(document.querySelectorAll('div[data-testid="tracklist-row"]:not(.playlistCounterAdded)')); | |
for (trackRow of trackRows) { | |
try { | |
console.log(trackRow) | |
let playlistCounter = document.createElement("div"); | |
trackRow.insertBefore(playlistCounter, trackRow.children[trackRow.childElementCount - 1]); | |
let trackId = getTrackIdFromTrackRow(trackRow); | |
let playlists = getPlaylistsContainingTrack(trackId); | |
playlistCounter.outerHTML = ` | |
<div class="tracklist-col playlistsCounter"> | |
<div class="tracklist-top-align tooltip"> | |
(${playlists.length})<span class="tooltiptext">This track exists in the following playlists: | |
${playlists.map(playlist => `<br><a href="${playlist.href}">${playlist.name}</a>`).join("")} | |
</span> | |
</div> | |
</div> | |
` | |
} catch (e) { | |
console.error(e); | |
} | |
trackRow.classList.add('playlistCounterAdded') | |
} | |
setTimeout(addPlaylistCounters, 100); | |
} | |
function getTrackIdFromTrackRow(trackRow) { | |
let reactContextmenuWrapper = trackRow.parentElement.parentElement; | |
let reactInternalInstancePropertyName = Object.keys(reactContextmenuWrapper).find(key => key.startsWith('__reactInternalInstance')) | |
let reactInternalInstance = reactContextmenuWrapper[reactInternalInstancePropertyName]; | |
return reactInternalInstance.child.key.split(':').pop().split('/')[0]; | |
} | |
function getPlaylistsContainingTrack(trackId) { | |
return playlists.filter(playlist => playlist.tracks.includes(trackId)); | |
} | |
injectCSS(); | |
alert('Fetching all your playlists and their tracks.. Please wait.. This can take up to a minute..') | |
let accessToken = await fetchAccessToken(); | |
let userId = await fetchUserId(accessToken); | |
let playlists = await fetchAllUserPlaylists(userId); | |
alert('Playlists fetching completed :)') | |
addPlaylistCounters(); | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment