login using spotify below and receive your stats in FUN FORM
/
document.addEventListener('DOMContentLoaded', function () {
(function () {
const CLIENT_ID = '0ed77f382f824e7895a26ba94de99fca';
const REDIRECT_URI = window.location.origin + window.location.pathname;
const SCOPES = 'user-top-read user-read-email user-read-private';
const loginBtn = document.getElementById('ss-login-btn');
const refreshBtn = document.getElementById('ss-refresh-btn');
const logoutBtn = document.getElementById('ss-logout-btn');
const userWrap = document.getElementById('ss-user');
const toolbar = document.getElementById('ss-toolbar');
const topCards = document.getElementById('ss-top-cards');
const sections = document.getElementById('ss-sections');
const actions = document.getElementById('ss-actions');
const avatarEl = document.getElementById('ss-avatar');
const userNameEl = document.getElementById('ss-user-name');
const userSubEl = document.getElementById('ss-user-sub');
const topTrackImg = document.getElementById('ss-top-track-img');
const topTrackTitle = document.getElementById('ss-top-track-title');
const topTrackSub = document.getElementById('ss-top-track-sub');
const topArtistImg = document.getElementById('ss-top-artist-img');
const topArtistTitle = document.getElementById('ss-top-artist-title');
const topArtistSub = document.getElementById('ss-top-artist-sub');
const tracksList = document.getElementById('ss-tracks-list');
const artistsList = document.getElementById('ss-artists-list');
const statusEl = document.getElementById('ss-status');
const errorEl = document.getElementById('ss-error');
if (
!loginBtn ||
!refreshBtn ||
!logoutBtn ||
!userWrap ||
!toolbar ||
!topCards ||
!sections ||
!actions ||
!avatarEl ||
!userNameEl ||
!userSubEl ||
!topTrackImg ||
!topTrackTitle ||
!topTrackSub ||
!topArtistImg ||
!topArtistTitle ||
!topArtistSub ||
!tracksList ||
!artistsList ||
!statusEl ||
!errorEl
) {
return;
}
const STORAGE = {
verifier: 'spotify_pkce_code_verifier',
accessToken: 'spotify_access_token',
expiresAt: 'spotify_access_token_expires_at',
currentRange: 'spotify_current_time_range'
};
let currentRange = localStorage.getItem(STORAGE.currentRange) || 'short_term';
function setStatus(msg) {
statusEl.textContent = msg || '';
}
function setError(msg) {
errorEl.textContent = msg || '';
}
function showApp() {
userWrap.style.display = 'flex';
toolbar.style.display = 'flex';
topCards.style.display = 'grid';
sections.style.display = 'grid';
actions.style.display = 'flex';
loginBtn.style.display = 'none';
}
function hideApp() {
userWrap.style.display = 'none';
toolbar.style.display = 'none';
topCards.style.display = 'none';
sections.style.display = 'none';
actions.style.display = 'none';
loginBtn.style.display = 'inline-block';
}
function setActiveRange(range) {
currentRange = range;
localStorage.setItem(STORAGE.currentRange, range);
document.querySelectorAll('#ss-toolbar .ss-pill').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-range') === range);
});
}
function randomString(length) {
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let text = '';
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
async function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
function base64encode(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function generateCodeChallenge(verifier) {
const hashed = await sha256(verifier);
return base64encode(hashed);
}
async function loginWithSpotify() {
setError('');
setStatus('Redirecting to Spotify...');
loginBtn.disabled = true;
try {
const verifier = randomString(64);
const challenge = await generateCodeChallenge(verifier);
localStorage.setItem(STORAGE.verifier, verifier);
const params = new URLSearchParams({
client_id: CLIENT_ID,
response_type: 'code',
redirect_uri: REDIRECT_URI,
scope: SCOPES,
code_challenge_method: 'S256',
code_challenge: challenge
});
window.location.href = 'https://accounts.spotify.com/authorize?' + params.toString();
} catch (err) {
console.error(err);
setError('Could not start Spotify login.');
setStatus('');
loginBtn.disabled = false;
}
}
async function exchangeCodeForToken(code) {
const verifier = localStorage.getItem(STORAGE.verifier);
if (!verifier) {
throw new Error('Missing PKCE code verifier.');
}
const body = new URLSearchParams({
client_id: CLIENT_ID,
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
code_verifier: verifier
});
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
});
const data = await response.json();
if (!response.ok) {
console.error(data);
throw new Error(data.error_description || data.error || 'Token exchange failed.');
}
const expiresAt = Date.now() + (data.expires_in * 1000);
localStorage.setItem(STORAGE.accessToken, data.access_token);
localStorage.setItem(STORAGE.expiresAt, String(expiresAt));
localStorage.removeItem(STORAGE.verifier);
return data.access_token;
}
function getStoredToken() {
const token = localStorage.getItem(STORAGE.accessToken);
const expiresAt = parseInt(localStorage.getItem(STORAGE.expiresAt) || '0', 10);
if (!token || !expiresAt) return null;
if (Date.now() >= expiresAt) {
localStorage.removeItem(STORAGE.accessToken);
localStorage.removeItem(STORAGE.expiresAt);
return null;
}
return token;
}
function clearSession() {
localStorage.removeItem(STORAGE.accessToken);
localStorage.removeItem(STORAGE.expiresAt);
localStorage.removeItem(STORAGE.verifier);
}
async function spotifyFetch(endpoint, token) {
const response = await fetch('https://api.spotify.com/v1' + endpoint, {
headers: {
Authorization: 'Bearer ' + token
}
});
const data = await response.json();
if (!response.ok) {
console.error(data);
throw new Error(data.error?.message || 'Spotify API request failed.');
}
return data;
}
function safeImage(item, type) {
if (type === 'track') {
return item?.album?.images?.[2]?.url || item?.album?.images?.[1]?.url || item?.album?.images?.[0]?.url || '';
}
if (type === 'artist') {
return item?.images?.[2]?.url || item?.images?.[1]?.url || item?.images?.[0]?.url || '';
}
return '';
}
function renderUser(profile) {
const avatar = profile?.images?.[0]?.url || '';
avatarEl.src = avatar || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56"><rect width="100%" height="100%" fill="#2a2a2a"/></svg>'
);
userNameEl.textContent = profile.display_name || 'Spotify User';
userSubEl.textContent = profile.email || (profile.product ? 'Plan: ' + profile.product : 'Connected');
}
function renderTopCards(tracks, artists) {
const topTrack = tracks[0];
const topArtist = artists[0];
if (topTrack) {
topTrackImg.src = safeImage(topTrack, 'track') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="100%" height="100%" rx="12" fill="#2a2a2a"/></svg>'
);
topTrackTitle.textContent = topTrack.name || '—';
topTrackSub.textContent = (topTrack.artists || []).map(a => a.name).join(', ') || '—';
} else {
topTrackImg.src = '';
topTrackTitle.textContent = 'No top track found';
topTrackSub.textContent = 'Try another time range';
}
if (topArtist) {
topArtistImg.src = safeImage(topArtist, 'artist') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="100%" height="100%" rx="12" fill="#2a2a2a"/></svg>'
);
topArtistTitle.textContent = topArtist.name || '—';
topArtistSub.textContent = (topArtist.genres && topArtist.genres.length)
? topArtist.genres.slice(0, 2).join(', ')
: 'Top artist';
} else {
topArtistImg.src = '';
topArtistTitle.textContent = 'No top artist found';
topArtistSub.textContent = 'Try another time range';
}
}
function renderTracks(tracks) {
tracksList.innerHTML = '';
if (!tracks.length) {
tracksList.innerHTML = '<div class="ss-sub">No top tracks found for this time range.</div>';
return;
}
tracks.slice(0, 10).forEach((track, index) => {
const row = document.createElement('div');
row.className = 'ss-row';
const artists = (track.artists || []).map(a => a.name).join(', ');
const imgSrc = safeImage(track, 'track') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect width="100%" height="100%" rx="10" fill="#2a2a2a"/></svg>'
);
row.innerHTML = `
<div class="ss-rank">${index + 1}</div>
<img class="ss-thumb" src="${imgSrc}" alt="">
<div class="ss-meta">
<div class="ss-name">${escapeHtml(track.name || 'Untitled')}</div>
<div class="ss-sub">${escapeHtml(artists || 'Unknown artist')}</div>
</div>
`;
tracksList.appendChild(row);
});
}
function renderArtists(artists) {
artistsList.innerHTML = '';
if (!artists.length) {
artistsList.innerHTML = '<div class="ss-sub">No top artists found for this time range.</div>';
return;
}
artists.slice(0, 10).forEach((artist, index) => {
const row = document.createElement('div');
row.className = 'ss-row';
const genreText = (artist.genres && artist.genres.length)
? artist.genres.slice(0, 2).join(', ')
: 'Artist';
const imgSrc = safeImage(artist, 'artist') || 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect width="100%" height="100%" rx="10" fill="#2a2a2a"/></svg>'
);
row.innerHTML = `
<div class="ss-rank">${index + 1}</div>
<img class="ss-thumb" src="${imgSrc}" alt="">
<div class="ss-meta">
<div class="ss-name">${escapeHtml(artist.name || 'Unnamed artist')}</div>
<div class="ss-sub">${escapeHtml(genreText)}</div>
</div>
`;
artistsList.appendChild(row);
});
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
async function loadStats(token) {
setError('');
setStatus('Loading your Spotify stats...');
try {
const [profile, topTracks, topArtists] = await Promise.all([
spotifyFetch('/me', token),
spotifyFetch('/me/top/tracks?limit=10&time_range=' + currentRange, token),
spotifyFetch('/me/top/artists?limit=10&time_range=' + currentRange, token)
]);
renderUser(profile);
renderTopCards(topTracks.items || [], topArtists.items || []);
renderTracks(topTracks.items || []);
renderArtists(topArtists.items || []);
setActiveRange(currentRange);
showApp();
setStatus('Showing ' + labelForRange(currentRange) + ' stats.');
} catch (err) {
console.error(err);
clearSession();
hideApp();
setError(
'Could not load Spotify stats.\n\n' +
(err.message || 'Unknown error') +
'\n\nMake sure your Spotify app redirect URI exactly matches this page URL:\n' +
REDIRECT_URI
);
setStatus('');
}
}
function labelForRange(range) {
if (range === 'short_term') return 'Last 4 Weeks';
if (range === 'medium_term') return 'Last 6 Months';
return 'All Time';
}
async function handleCallbackIfPresent() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const error = params.get('error');
if (error) {
setError('Spotify login was cancelled or failed: ' + error);
return null;
}
if (!code) {
return null;
}
setStatus('Finishing Spotify login...');
const token = await exchangeCodeForToken(code);
const cleanUrl = window.location.origin + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
return token;
}
async function init() {
if (!CLIENT_ID) {
setError('Add your Spotify Client ID in the code first.');
hideApp();
return;
}
setActiveRange(currentRange);
document.querySelectorAll('#ss-toolbar .ss-pill').forEach(btn => {
btn.addEventListener('click', async function () {
const token = getStoredToken();
if (!token) {
setError('Your session expired. Please connect again.');
hideApp();
return;
}
const range = this.getAttribute('data-range');
setActiveRange(range);
await loadStats(token);
});
});
loginBtn.addEventListener('click', loginWithSpotify);
refreshBtn.addEventListener('click', async function () {
const token = getStoredToken();
if (!token) {
setError('Your session expired. Please connect again.');
hideApp();
return;
}
await loadStats(token);
});
logoutBtn.addEventListener('click', function () {
clearSession();
hideApp();
setStatus('Disconnected.');
setError('');
tracksList.innerHTML = '';
artistsList.innerHTML = '';
});
try {
const callbackToken = await handleCallbackIfPresent();
const token = callbackToken || getStoredToken();
if (token) {
await loadStats(token);
} else {
hideApp();
setStatus('Click "Connect with Spotify" to begin.');
}
} catch (err) {
console.error(err);
clearSession();
hideApp();
setError(
'Spotify login failed.\n\n' +
(err.message || 'Unknown error') +
'\n\nCheck that your Spotify redirect URI exactly matches:\n' +
REDIRECT_URI
);
setStatus('');
} finally {
loginBtn.disabled = false;
}
}
init();
})();
});