${h(JSON.stringify(data, excluder, 4))}

`); w.document.close(); } function storeclientid() { if (event.key == 'Enter') { client_id = document.getElementById('clientidbox').value.trim(); if (client_id) { if (!config) { config = {}; } config.client_id = client_id; saveConfig(); const c = document.getElementById('curio'); c.style.transition = '5s'; c.className = 'title'; document.getElementById('pageheader').style.display = 'block'; } startup(); ls(); } } function storetoindex() { event.target.setAttribute('v', true); if (event.key == 'Enter') { to_index = Array.from(document.getElementById('to_index').value.matchAll(/.+?(\*|$)/g)).map((m) => m[0].trim()); config.to_index = to_index; saveConfig(); } } function storeplaylistedfilters() { event.target.setAttribute('v', true); if (event.key == 'Enter') { playlisted_filters = document.getElementById('playlisted_filters').value.split(',').map((v) => v.trim()); config.playlisted_filters = playlisted_filters; saveConfig(); } } window.addEventListener("popstate", async function(event) { reloading = true; comppos = event.state; await startup(); reloading = false; }) window.addEventListener("paste", async function(event) { if (page && page.startsWith('spotify:playlist:')) { pid = page.split(':')[2]; pasted = await navigator.clipboard.readText(); pasteduris = Array.from(pasted.matchAll(/spotify:track:.{22}/g).map((m) => m[0])); pasteduris.push(...Array.from(pasted.matchAll(/https:\/\/open\.spotify\.com\/track\/(.{22})/g).map((m) => 'spotify:track:' + m[1]))); if (pasteduris.length > 0) { while (pasteduris.length > 0) { chunk = pasteduris.slice(0, 100); await callapi('playlists/' + pid + '/tracks', 'POST', {"uris": chunk}); pasteduris = pasteduris.slice(100); } getSongs(page); } } }) window.addEventListener('keydown', function(event) { if (event.metaKey && event.key === 'c' && window.getSelection().toString() === "") { event.preventDefault(); if (page.startsWith('spotify:playlist:')) { selected = Array.from(document.querySelectorAll('[selection]')); if (selected.length > 0) { uris = selected.map((t) => 'spotify:track:' + t.getAttribute('trackid')); } else { uris = sortedtracks.map((t) => t.uri); } } else if (page == 'getsongs') { uris = Array.from(document.querySelectorAll('.track')).slice(0, 100).map((t) => 'spotify:track:' + t.getAttribute('trackid')) } else { uris = [] } navigator.clipboard.writeText(uris.join('\n')); status("copied " + uris.length + " track" + (uris.length == 1 ? '' : 's') + ' to the clipboard', true); } }); var scrollThreshold = null; var afterscroll = null; window.addEventListener('scroll', () => { if (!scrollThreshold) { scrollThreshold = document.getElementById('output').offsetTop - document.getElementById('status').offsetHeight; } const currentScroll = window.pageYOffset || document.documentElement.scrollTop; // Add or remove the 'scrolled' class based on scroll position if (currentScroll >= scrollThreshold) { document.body.classList.add('scrolled'); } else { document.body.classList.remove('scrolled'); } document.body.classList.add('scrolling'); clearTimeout(afterscroll); afterscroll = window.setTimeout(() => {document.body.classList.remove('scrolling')}, 1000); }); function titleanimation() { document.getElementById('curio').className = 'title'; } function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; // Prevent CORS issues if loading from different domains img.onload = () => resolve(img); img.onerror = reject; img.src = url; }); } async function strips() { pid = page.split(':')[2]; const canvas = document.getElementById('stripcanvas'); canvas.style.display = 'block'; const ctx = canvas.getContext('2d'); imageurls = []; for (track of sortedtracks) { image = track.album.images[0].url if (!imageurls.includes(image)) { imageurls.push(image); if (imageurls.length >= 5) { break; } } } const images = await Promise.all(imageurls.map(loadImage)); images.forEach((image, index) => { const xOffset = index * 128; // Each strip starts at 120px intervals ctx.drawImage(image, xOffset, 0, 128, 640, xOffset, 0, 128, 640); }); quality = .95; const resultImageRaw = canvas.toDataURL('image/jpeg', quality); playlistimage = makeElement('a', null, '', ['playlistimage']); sidebar = document.getElementById('sidebar'); if (sidebar.firstChild.className == 'playlistimage') { sidebar.firstChild.remove(); } sidebar.insertBefore(playlistimage, sidebar.firstChild); const resultImage = resultImageRaw.split(',', 2)[1]; while (resultImage.length > 256 * 1024 && quality > .9) { quality -= .01; const resultImage = canvas.toDataURL('image/jpeg', quality).split(',', 2)[1]; } res = await callapi('https://api.spotify.com/v1/playlists/' + pid + '/images', 'PUT', resultImage); if (res && res.status != 202) { status('Error: Image too large. Saved instead.'); const link = document.createElement('a'); link.download = pid + '.png'; link.href = canvas.toDataURL(); link.click(); link.remove(); } canvas.style.display = 'none'; } function nudge(me) { me.style.top = Number(me.style.top.replace('px', '')) + (Math.random() >= .5 ? 1 : -1); me.style.left = Number(me.style.left.replace('px', '')) + (Math.random() >= .5 ? 1 : -1) } var scattered = false; var adrift_selector = '*'; //.album, .playlist, .artist, .track, .inline_track, .albumart, .playlistimage, a'; async function scatter(times=1000) { adrift = Array.from(document.querySelectorAll(adrift_selector)); // await new Promise(resolve => setTimeout(resolve, 10000)); if (!scattered) { adrift.forEach((a) => { a.style.position='relative'; a.style.transition='1.5s'; a.style.top = 0; a.style.left = 0; }); scattered = adrift[0].offsetLeft; } function scattermove(times) { for(n=0; n 0) { window.setTimeout(() => scattermove(times - 1), 0); } } window.setTimeout(() => scattermove(times), 0); } function gather() { adrift = Array.from(document.querySelectorAll(adrift_selector)); adrift.forEach((a) => { a.style.top = 0; a.style.left = 0; }); junk = adrift[0].offsetLeft; window.setTimeout(() => { adrift.forEach((a) => { a.style.position = ''; a.style.transition = '500ms'; }); scattered = false; }, 1500); } async function flagnew(album) { mainartist = album.artists[0].id; artistalbums = await getall('artists/' + mainartist + '/albums?include_groups=album,single,compilation&limit=50'); olderalbums = artistalbums.filter((a) => a.release_date < album.release_date).map((a) => a.id); oldertitles = new Set(); while (olderalbums.length > 0) { chunk = olderalbums.slice(0, 20); chunkdata = await callapi('albums?ids=' + chunk.join(',')); chunkdata.albums.forEach((ca) => { ca.tracks.items.forEach((cat) => { oldertitles.add(basetitle(cat.name)); }) }) olderalbums = olderalbums.slice(20); } newtracks = new Set(album.tracks.items.filter((t) => !oldertitles.has(basetitle(t.name))).map((t) => t.id)); return newtracks; } async function nextplaylist() { me = event.target; pindex = Number(me.getAttribute('pindex')); playlist_order = await store_get('playlist_order'); if (pindex < playlist_order.length - 1) { newpage(getSongs, 'spotify:playlist:' + playlist_order[pindex + 1]); } else { nextp = await callapi('me/playlists?limit=1&offset=' + (pindex + 1)); if (nextp) { newpage(getSongs, nextp.items[0].uri); me.setAttribute('pindex', pindex + 1); } } } function compform(s) { return(s.normalize().toLowerCase().replaceAll(/[^\p{L}\p{N}\s]/gu, '').replaceAll(/\s{2,}/g, ' ')); } function deparen(s) { hasparen = s.match(/^(.+?)\s*\(.+\)$/); if (hasparen) { s = hasparen[1]; } hasbraces = s.match(/^(.+?)\s*\[.+\]$/); if (hasbraces) { s = hasbraces[1]; } [ [' pt.', ' part'], [' Pt.', ' Part'] ].map((pattern) => s = s.replace(...pattern)) return(s); } function dedash(s) { return(s.split(/\s+-\s/)[0]) } function splitartist(s) { patterns = [/,\s*/, /\s+\/\s+/, /\s+(?:and|&)\s+/i, /\s+feat\.?/i, /\s+ft\.?/i]; for(p=0; p 1) { return(parts); } } return([]); } async function bulksearch(text) { let lines = text.trim().split('\n').map((line) => line.trim()).filter((line) => line != ''); if (document.getElementsByClassName('bulksearchtype')[0].value == 'track') { bias = 'track'; } else { bias = 'album'; } const numbered = /^[\d\.]+\s+/; if (lines.every((line) => line.match(numbered))) { lines = lines.map((line) => line.replace(numbered, '')); } let parts = lines.map((l) => l.split(/ +|\t/)); if (new Set(parts.map((p) => p.length)).size == 1) { const allnums = Array.from(Array(parts[0].length).keys().map((k) => parts.map((p) => p[k]).every((part) => part.match(/^[\d\.]+$/)))); parts = parts.map((pline) => pline.filter((val, i) => !allnums[i])) } lines = parts.map((p) => p.join(' ')).filter((val, i, list) => list.indexOf(val) === i); resultboxes = document.getElementsByClassName('bulkresulttable'); if (resultboxes.length > 0) { resultbox = resultboxes[0]; resultbox.innerHTML = ''; } else { resultbox = makeElement('table', document.getElementsByClassName('bulkresults')[0], '', ['bulkresulttable']); } for (linenum=0; linenum(.+)<\/i>$/, 'album'], [/^(.+?)\s+[·:–––\-]\s+(.+)$/, bias, 'reversible'], [/^(.+?):\s+(.+)$/, bias, 'reversible'], [/^(.+?)\s*,\s*(.+)$/, bias, 'reversible'], ]; for ([parser, restype, resreverse, prefunc] of parsers) { if (prefunc) { useline = prefunc(line); } else { useline = line; } parsed = useline.match(parser); if (parsed) { qtype = restype; target = restype + 's'; if (resreverse == 'reverse') { reverse = true; } else if (resreverse == 'reversible') { reversible = true; } break; } } if (parsed && target) { artist = parsed[reverse ? 2 : 1].normalize(); title = parsed[reverse ? 1 : 2].normalize(); } if (target) { trylist = [[artist, title]]; deparenartist = deparen(artist); deparentitle = deparen(title); dedashtitle = dedash(title); if (deparentitle && deparentitle != title) { trylist.push([artist, deparentitle]); } if (dedashtitle && dedashtitle != title) { trylist.push([artist, dedashtitle]); } if (deparenartist && deparenartist != artist) { trylist.push([deparenartist, title]) if (deparentitle && deparentitle != title) { trylist.push([deparenartist, deparentitle]); } if (dedashtitle && dedashtitle != title) { trylist.push([deparenartist, dedashtitle]); } } artists = splitartist(artist); for(ax=1; ax ([title, artist])); trylist.push(...fliplist); } matches = {'full': [], 'partial': []}; for(mode=0; mode<=1; mode++) { for(t=0; t a['name'] == artist || compform(a['name']) == artistcomp); titlematch = compform(rel['name']) == titlecomp; basematch = compform(dedash(deparen(rel['name']))) == titlecomp; if (target == 'tracks') { typematch = rel['album']['album_type'] != 'compilation'; } else { typematch = rel['album_type'] == 'album'; } // console.log({rel: rel, artistmatch: artistmatch, titlematch: titlematch, basematch: basematch, typematch: typematch}); // console.log({relartistcomp: compform(rel.artists[0]['name']), searchartistcomp: artistcomp}); // console.log({reltitlecomp: compform(rel['name']), searchtitlecomp: titlecomp}); if (artistmatch && titlematch && typematch) { matches['full'].push(rel); } else if ((typematch && (artistmatch || basematch)) || (artistmatch && basematch)) { matches['partial'].push(rel); } if (matches['full'].length > 0) { break; } } } if (matches['full'].length > 0) { break; } } if (matches['full'].length > 0) { matchmode = 'full'; } else if (matches['partial'].length > 0) { matchmode = 'partial'; } else { matchmode = null; } if (matchmode) { usematches = matches[matchmode]; } else { usematches = []; } for(u=0; u ' + h(rel['artists'][0]['name']) + '  ' + h(rel['name']) + ''; } else { if (rel['album']['album_type'] == 'single' && rel['album']['name'] == rel['name']) { albumtitle = '"' + h(rel['name']) + '"' + (rel['album']['total_tracks'] == 1 ? '' : (' +' + (rel['album']['total_tracks'] - 1))); } else { albumtitle = '' + h(rel['album']['name']) + ''; } title = ' ' + h(rel['artists'][0]['name']) + '  "' + h(rel['name']) + '" from ' + albumtitle + ''; } resdiv = makeElement('div', rescell, title, ['bulkres']); } } } bulkcheck(); } function bulkcheck() { checkeduris = Array.from(document.querySelectorAll('.rescheck:checked')).map((c) => c.value); document.getElementsByClassName('bulkadder')[0].disabled = checkeduris.length == 0; } function bulkshow() { bulkwidgets = document.getElementsByClassName('bulkwidgets')[0]; bulksearcher = document.getElementsByClassName('bulksearcher')[0]; bulksearchtype = document.getElementsByClassName('bulksearchtype')[0]; bulkaddbox = document.getElementsByClassName('bulkadd')[0]; if (bulkwidgets.style.display == 'block') { bulkwidgets.style.display = ''; bulksearcher.style.display = ''; bulksearchtype.style.display = ''; event.target.style.color = 'silver'; } else { bulkwidgets.style.display = 'block'; bulksearcher.style.display = 'inline'; bulksearchtype.style.display = 'inline'; event.target.style.color = 'gray'; if (bulkaddbox.value == '') { bulkaddbox.focus(); } } } async function bulkadd() { bulkshow(); window.setTimeout(async () => { const checkeduris = Array.from(document.querySelectorAll('.rescheck:checked')).map((c) => c.value); let albumids = checkeduris.filter((uri) => uri.startsWith('spotify:album:')).map((uri) => uri.split(':')[2]); const albumtrackindex = {} while (albumids.length > 0) { status('getting album tracks'); let chunk = albumids.slice(0, 20); let chunkres = await callapi('albums?ids=' + chunk.join(',')); let nextpage = null; let nextres = null; for (const album of chunkres.albums) { nextpage = album.tracks.next; while (nextpage) { nextres = await callapi(nextpage); album.tracks.items.push(...nextres.items); nextpage = nextres.next; } albumtrackindex[album.uri] = album.tracks.items.map((t) => t.uri); } albumids = albumids.slice(20); } let uris = checkeduris.flatMap((uri) => uri.startsWith('spotify:track:') ? [uri] : albumtrackindex[uri]); const pid = page.split(':')[2]; while (uris.length > 0) { status('adding tracks to playlist'); let chunk = uris.slice(0, 100); let chunkres = await callapi('playlists/' + pid + '/tracks', 'POST', {uris: chunk}); uris = uris.slice(100); } document.getElementsByClassName('bulkadd')[0].value = ''; document.getElementsByClassName('bulkresults')[0].innerHTML = ''; getSongs(page); }, 0); } // while (albumorder.length > 0) { // chunk = albumorder.slice(0, 20); // chunkres = await callapi('albums?ids=' + chunk.join(',')); // for(a=0; a 0) { // trackbox = document.getElementById('trackuriouter'); // textarea = document.getElementById('trackuritextarea'); // textarea.value = trackorder.join('\\n'); // textarea.rows = trackorder.length; // trackbox.style.display = 'block'; // } document.addEventListener('mousedown', function(event) { if (event.shiftKey && document.querySelector('[selection], [pselection], [inlineselection]')) { event.preventDefault(); } }); async function makeGenreMapOld(api) { pindex = {}; for (a in api.artists) { for (p in api.artists[a].playlists) { pid = api.artists[a].playlists[p].id; if (!(pid in pindex)) { pindex[pid] = []; } pindex[pid].push({ ti: api.artists[a].playlists[p].ti, artist_id: a, genres: api.artists[a].genres }); } } genremap = {}; artist_genre_counts = {}; for (p in pindex) { pindex[p].sort((a, b) => a.ti - b.ti); for (t of pindex[p]) { for (g of t.genres) { if (!(g in genremap)) { genremap[g] = {}; } if (t.ti < pindex[p].length) { aid1 = t.artist_id; aid2 = pindex[p][t.ti].artist_id; for (aid of [aid1, aid2]) { if (!(aid in artist_genre_counts)) { artist_genre_counts[aid] = {}; } if (!(g in artist_genre_counts[aid])) { artist_genre_counts[aid][g] = 0; } artist_genre_counts[aid][g] += 1; } for (og of pindex[p][t.ti].genres) { for (aid of [aid1, aid2]) { if (!(og in artist_genre_counts[aid])) { artist_genre_counts[aid][og] = 0; } artist_genre_counts[aid][og] += 1; } if (!(og in genremap)) { genremap[og] = {}; } if (!(og in genremap[g])) { genremap[g][og] = 0; } genremap[g][og] += 1; if (!(g in genremap[og])) { genremap[og][g] = 0; } genremap[og][g] += 1; } } } } } transitions = {} for (g in genremap) { selfref = genremap[g][g] || 0; if (selfref >= 10) { ogmin = Math.max(10, selfref / 10); for (og in genremap[g]) { if (g != og && genremap[g][og] >= ogmin) { if (!(g in transitions)) { transitions[g] = new Set(); } transitions[g].add(og); } } } } artist_genremap = {}; for (aid in artist_genre_counts) { for (g in artist_genre_counts[aid]) { if (artist_genre_counts[aid][g] >= 2) { if (!(aid in artist_genremap)) { artist_genremap[aid] = new Set(); } artist_genremap[aid].add(g); } } } return({pindex: pindex, genremap: genremap, transitions: transitions, artist_genremap: artist_genremap, artist_genre_counts: artist_genre_counts}); } async function makeGenreMap(api) { falgenres = await store_get('falgenres'); if (!falgenres) { fga = await fetch('csvs/falgenres_pps.csv'); fgt = await fga.text(); falgenres = {}; fgt.split('\r\n').slice(1).forEach((line) => { [aid, genres] = line.split(','); if (genres) { falgenres[aid] = genres.split('; '); } }) store_set('falgenres', falgenres); } pindex = {}; for (a in api.artists) { for (p in api.artists[a].playlists) { pid = api.artists[a].playlists[p].id; if (!(pid in pindex)) { pindex[pid] = []; } pindex[pid].push({ ti: api.artists[a].playlists[p].ti, artist_id: a, genres: api.artists[a].genres, falgenres: falgenres[a] }); } } genremap = {}; for (p in pindex) { pindex[p].sort((a, b) => a.ti - b.ti); for (t of pindex[p]) { for (g of (t.falgenres || [])) { if (!(g in genremap)) { genremap[g] = {}; genremap[g][g] = 1; } if (t.ti < pindex[p].length) { for (og of (pindex[p][t.ti].falgenres || [])) { if (!(og in genremap)) { genremap[og] = {}; genremap[og][og] = 1; } if (!(og in genremap[g])) { genremap[g][og] = 0; } genremap[g][og] += 1; if (!(g in genremap[og])) { genremap[og][g] = 0; } genremap[og][g] += 1; } } } } } transitions = {} for (g in genremap) { selfref = genremap[g][g] || 0; if (selfref >= 10) { ogmin = Math.max(10, selfref / 10); for (og in genremap[g]) { if (g != og && genremap[g][og] >= ogmin) { if (!(g in transitions)) { transitions[g] = new Set(); } transitions[g].add(og); } } } } return({transitions: transitions, falgenres: falgenres, genremap: genremap}); } async function guessSort(tracks) { regroupbutton = document.getElementById('regroupbutton'); regroupbutton.style.background = 'silver'; const api = await store_get('artist_playlist_index'); const mgm = await makeGenreMap(api); for(let track of tracks) { track.genres = new Set(); delete track.clusterseed; delete track.stray; delete track.assigned; for (artist of track.artists) { for (genre of (mgm.falgenres[artist.id] || [])) { track.genres.add(genre) } } } let queue = tracks.slice(0).sort((a, b) => Math.max(...Array.from(b.genres).map((g) => (g in mgm.genremap ? mgm.genremap[g][g] : 0))) - Math.max(...Array.from(a.genres).map((g) => (g in mgm.genremap ? mgm.genremap[g][g]: 0)))); let clusters = []; let strays = []; while (queue.length > 0) { let seed = queue.shift(); const cluster = [seed]; let seedgenres = seed.genres || new Set(); let corona = seedgenres.union(new Set(...Array.from(seedgenres).flatMap((g) => mgm.transitions[g] || []))); // console.log({seedgenres: seedgenres.size + ': ' + Array.from(seedgenres).sort().join(', '), corona: corona.size + ': ' + Array.from(corona).sort().join(', ')}) let othergenres = new Set(); while (queue.length > 0) { let candidates = queue.filter((t) => (t.genres || new Set()).intersection(seedgenres).size > 1); let candidatesfrom = 'core'; // console.log({candidates: candidates}) if (candidates.length == 0) { candidates = queue.filter((t) => (t.genres || new Set()).intersection(corona).size > 1); candidatesfrom = 'corona'; // console.log({corona_candidates: candidates}) } if (candidates.length == 0) { break; } else { const picked = candidates[0]; queue = queue.filter((t) => t != picked); cluster.push(picked); if (candidatesfrom == 'core') { for (const genre of picked.genres) { if (!seedgenres.has(genre)) { if (othergenres.has(genre)) { corona.add(genre); } else { othergenres.add(genre); } } } } } } if (cluster.length >= 3) { seed.clusterseed = true; clusters.push({seedgenres: seedgenres, corona: corona, tracks: cluster}); } else { strays.push(...cluster); } } clusters.sort((a, b) => Array.from(b.seedgenres).flatMap((g) => genremap[g][g]).reduce((sum, x) => sum + x, 0) - Array.from(a.seedgenres).flatMap((g) => genremap[g][g]).reduce((sum, x) => sum + x, 0) || b.tracks.length - a.tracks.length ) // clusters.forEach((c, ci) => c.ci = ci); for (stray of strays) { stray.stray = true; expandedgenres = new Set(...Array.from(stray.genres).flatMap((g) => mgm.transitions[g])); // console.log({expandedgenres: expandedgenres, intersections: clusters.map((c) => expandedgenres.intersection(c.corona).size)}) scored = clusters.map((c) => ({score: expandedgenres.intersection(c.corona).size, c: c})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score || b.c.tracks.length - a.c.tracks.length); // console.log({scored: scored}) if (scored.length > 0) { // console.log({addto: clusters[scored[0].c.ci]}) scored[0].c.tracks.push(stray); stray.assigned = true; } } clusters = clusters.map((c) => c.tracks); clusters.push(strays.filter((s) => !s.assigned)); // console.log({clusters: clusters, transitions: mgm.transitions}); updateSongOrder(clusters.flatMap((c) => c)); regroupbutton.style.background = 'buttonface'; } async function reorderPlaylist(pdata, neworder) { reorderbutton = document.getElementById('reorderbutton'); reorderbutton.style.background = 'silver'; newids = neworder.map((t) => t.id); currenttracks = await getall('playlists/' + pdata.id + '/tracks', 'items', 'track'); currentids = currenttracks.map((t) => t.id); // console.log({newids: newids, currentids: currentids, currenttracks: currenttracks}) snapshot = pdata.snapshot_id; for(x=0; x { apoints = a?.counts?.points || 0; pointsindex[a.id] = apoints; maxpoints = Math.max(maxpoints, apoints); genreindex[a.id] = a.genres; }); if (flow) { artistbox.classList.add('freeform'); } else { artistbox.classList.remove('freeform'); } await loadgenrecolor(); Array.from(document.querySelectorAll('.artist')) .sort((a, b) => (flow == 'cloud' ? 0 : ((pointsindex[b.getAttribute('aid')] || 0) - (pointsindex[a.getAttribute('aid')] || 0))) || sortform(a.querySelector('a').textContent).localeCompare(sortform(b.querySelector('a').textContent))) .forEach(async (a) => { aid = a.getAttribute('aid'); scale_factor = (pointsindex[aid] || 0) / maxpoints; a.style.fontSize = flow ? ((6 + 122 * scale_factor) + 'px') : '16px'; if (flow) { newcolor = getgenrecolor(genreindex[aid]); } else { newcolor = ''; } a.querySelector('a').style.color = newcolor; artistbox.appendChild(a); }); } var colorindex = null; async function loadgenrecolor() { genre_centroids_fetch = await fetch('/genre_centroids.csv'); genre_centroids_text = await genre_centroids_fetch.text(); genre_centroids = genre_centroids_text.split('\n').map((line) => line.trim().split(',')); colorindex = {}; genre_centroids.forEach(([genre,popularity,avg_acousticness,std_acousticness,avg_beat_strength,std_beat_strength,avg_bounciness,std_bounciness,avg_danceability,std_danceability,avg_dyn_range_mean,std_dyn_range_mean,avg_energy,std_energy,avg_flatness,std_flatness,avg_instrumentalness,std_instrumentalness,avg_loudness,std_loudness,avg_mechanism,std_mechanism,avg_organism,std_organism,avg_tempo,std_tempo,avg_valence,std_valence,avg_duration,std_duration,color,revcolor]) => colorindex[genre] = color); } function getgenrecolor(genrelist) { for (genre of genrelist || []) { if (genre in colorindex) { return '#' + colorindex[genre]; } } return 'gray'; } function getRandomAccessibleColor() { // Generate colors with sufficient contrast against white background // Using WCAG luminance calculation const r = Math.floor(Math.random() * 192); // Limit max to ensure darkness const g = Math.floor(Math.random() * 192); const b = Math.floor(Math.random() * 192); // Calculate relative luminance const rsRGB = r / 255; const gsRGB = g / 255; const bsRGB = b / 255; const rLum = rsRGB <= 0.03928 ? rsRGB/12.92 : Math.pow((rsRGB + 0.055)/1.055, 2.4); const gLum = gsRGB <= 0.03928 ? gsRGB/12.92 : Math.pow((gsRGB + 0.055)/1.055, 2.4); const bLum = bsRGB <= 0.03928 ? bsRGB/12.92 : Math.pow((bsRGB + 0.055)/1.055, 2.4); const luminance = 0.2126 * rLum + 0.7152 * gLum + 0.0722 * bLum; // Check if contrast ratio with white (1.0) is at least 4.5:1 const contrastRatio = (1.0 + 0.05) / (luminance + 0.05); if (contrastRatio < 4.5) { // Try again if contrast isn't sufficient return getRandomAccessibleColor(); } return `rgb(${r},${g},${b})`; } dactalintro = `

This isn't a search box, it's a query interface for exploring your retrieved data using a data-agnostic collation/transformation/analysis language called DACTAL. A typical DACTAL query starts with a datatype, like the above. That gets the list of all the things of that type. From there a query can chain of any number of any of these operations, in any order:
 
.   follow a property from all those things to other things, like tracks.artist
:   filter the list of things by some criteria, like tracks:artist=Nightwish
/   group the things, like tracks/artist
#   sort the things, like tracks#artists,album,-disc_number,-track_number
 
For more information, see furia.com/dactal.
`; switch_to_querypage = querypage; async function querypage(query=null) { if (!dactalloaded) await dactal_init(); page = 'querypage'; qp = await dactal_querypage(); box = document.getElementById('output'); box.textContent = ''; box.appendChild(qp); loadadapters(); document.getElementById('sidebar').textContent = ''; document.getElementById('albumsidebar').textContent = ''; if (query) { queryinput = document.getElementById("dactal"); queryinput.value = query; queryinput.focus(); } await ls(); } var indexkeys = 0; async function afterquery(query_results) { uris = []; if (query_results && query_results.length > 0) { first = query_results[0]; if (dactal.dtype(first, 'object')) { if ('uri' in first) { if (typeof first.uri == 'string' && first.uri.startsWith('spotify:track:')) { uris = query_results.map((item) => item.uri); } else if (Array.isArray(first.uri) && first.uri.length == 1 && first.uri[0].startsWith('spotify:track:')) { uris = query_results.map((item) => item.uri[0]); } } else if (Object.keys(first).length == 1 && first.value && dactal.dtype(first.value, 'string') && first.value.startsWith('spotify:track:')) { uris = query_results.map((item) => item.value); } } else if (dactal.dtype(query_results[0], 'string') && query_results[0].startsWith('spotify:track:')) { uris = query_results.slice(0); } uris = uris.filter((uri) => uri.startsWith('spotify:track:')); if (uris.length > 0) { if (uris.length == 1) { prompt = 'save this track as a playlist called '; } else { prompt = 'save these ' + uris.length + ' tracks as a playlist called' } cp = document.createElement('div'); cp.classList.add('create_playlist'); cp.innerHTML = prompt + ''; cp.getElementsByTagName('input')[0].addEventListener('keyup', (e) => { if (e.key == 'Enter') { create_playlist(e.target, uris).then((puri) => newpage(getSongs, puri)); } }) return cp; } if ((newindexkeys = Object.keys(dactal.index).length) != indexkeys) { await db_set('_index', dactal.index); indexkeys = newindexkeys; } } } async function loadaoty() { aotylists = [ "Albumism's 50 Best Albums of 2024.html", "AllMusic's 100 Favorite Albums of 2024.html", "Alternative Press's 50 Best Albums of 2024.html", "The Alternative's Best 50 Releases of 2024.html", "The Associated Press's Top Albums of 2024.html", "The Atlantic's 10 Best Albums of 2024.html", "Atwood Magazine's Albums of the Year 2024.html", "A.V. Club's 25 Best Albums of 2024.html", "Bandcamp Daily's Essential Releases of 2024.html", "BBC Radio 6 Music's Albums of the Year 2024.html", "Beats Per Minute's Top 50 Albums of 2024.html", "Billboard's 50 Best Albums of 2024.html", "Bleep's Top 10 Albums of the Year 2024.html", "BrooklynVegan's Top 50 Albums of 2024.html", "Business Insider's Best Albums of 2024.html", "Clash's Albums of the Year 2024.html", "Complex's 50 Best Albums of 2024.html", "Consequence's 50 Best Albums of 2024.html", "Coup De Main's Best Albums of 2024.html", "Crack Magazine's Top 50 Albums of 2024.html", "Dazed's 20 Best Albums of 2024.html", "Decibel's Top 40 Albums of 2024.html", "DIY's Best Albums of 2024.html", "DJ Mag's Top Albums of 2024.html", "Double J's 50 Best Albums of 2024.html", "EARMILK's Best 50 Albums of 2024.html", "The Economist's Best Albums of 2024.html", "Entertainment Weekly's 10 Best Albums of 2024.html", "Esquire's 10 Best Albums of 2024.html", "Exclaim!'s 50 Best Albums of 2024.html", "The FADER's 50 Best Albums of 2024.html", "FLOOD's Best Albums of 2024.html", "The Forty-Five's Best Albums of 2024.html", "Glide's 20 Best Albums of 2024.html", "God Is In The TV's Albums of the Year 2024.html", "Gorilla vs. Bear's Albums of 2024.html", "GQ [UK]'s 24 Best Albums of 2024.html", "The Guardian's 50 Best Albums of 2024.html", "Hot Press's 50 Best Albums of 2024.html", "HuffPost's Best Albums of 2024.html", "Hypebeast's 10 Best Albums of 2024.html", "The Independent's Best Albums of 2024.html", "KCRW's Best Albums of 2024.html", "Kerrang!'s 50 Best Albums of 2024.html", "Les Inrocks' 100 Best Albums of 2024.html", "The Line of Best Fit's Best Albums of the Year 2024.html", "Los Angeles Times' 20 Best Albums of 2024.html", "Loud and Quiet's Albums of the Year 2024.html", "Louder Than War's Top 100 Albums of 2024.html", "MAGNET's Top 25 Albums of 2024.html", "Metal Hammer's Top 50 Albums of 2024.html", "Metal Injection's Top 25 Records of 2024.html", "MOJO's 75 Best Albums of 2024.html", "MondoSonoro's Best Albums of 2024.html", "musicOMH's Top 50 Albums of 2024.html", "The Needle Drop's Top 50 Albums of 2024.html", "The New York Times_ Jon Caramanica's Best Albums of 2024.html", "The New York Times_ Jon Pareles' Best Albums of 2024.html", "The New York Times_ Lindsay Zoladz's Best Albums of 2024.html", "The New Yorker's Best Albums of 2024.html", "NME's 50 Best Albums of 2024.html", "No Ripcord's 50 Best Albums of 2024.html", "Northern Transmissions' Best Albums of 2024.html", "NPR Music's 50 Best Albums of 2024.html", "The Observer_ Kitty Empire's 10 Best Albums of 2024.html", "Okayplayer's Best Albums of 2024.html", "OOR's 20 Best Albums of 2024.html", "PAPER's Best Albums of 2024.html", "Paste's 100 Best Albums of 2024.html", "People's Top 10 Albums of 2024.html", "The Philadelphia Inquirer_ Dan DeLuca's Best Albums of 2024.html", "Pitchfork's 50 Best Albums of 2024.html", "PopMatters' 80 Best Albums of 2024.html", "Punktastic's Top 25 Albums of 2024.html", "The Quietus Albums of the Year 2024.html", "Radio X's 25 Best Albums of 2024.html", "Resident Advisor's Best Records of 2024.html", "RIFF's 35 Best Albums of 2024.html", "The Ringer's 30 Best Albums of 2024.html", "Rock Sound's Top 24 Albums of 2024.html", "Rolling Stone UK's 24 Best Albums of 2024.html", "Rolling Stone's 100 Best Albums of 2024.html", "Rolling Stone_ Rob Sheffield's Top 20 Albums of 2024.html", "Rough Trade UK's Albums of the Year 2024.html", "The Skinny's Albums of 2024.html", "Slant Magazine's 50 Best Albums of 2024.html", "Slate's 12 Best Albums of 2024.html", "Sound Opinions_ Greg Kot's Best Albums of 2024.html", "Sound Opinions_ Jim DeRogatis's Best Albums of 2024.html", "Spectrum Culture's Top 20 Albums of 2024.html", "SPIN's Albums of the Year 2024.html", "Sputnikmusic's Top 50 Albums of 2024.html", "Stereogum's 50 Best Albums of 2024.html", "Still Listening's Top 50 Albums Of 2024.html", "The Sunday Times' 25 Best Albums of 2024.html", "The Sydney Morning Herald's 10 Best Albums of 2024.html", "The Telegraph's 10 Best Albums of 2024.html", "Time Out's Best Albums of 2024.html", "TIME's 10 Best Albums of 2024.html", "Treble's 50 Best Albums of 2024.html", "TURN's 30 Best Albums of 2024.html", "Uncut's 80 Best Albums of 2024.html", "Uproxx_ Steven Hyden's Favorite Albums of 2024.html", "Uproxx's Best Albums of 2024.html", "Variety_ Chris Willman's Top 10 Albums of 2024.html", "Variety_ Jem Aswad's Top 10 Albums of 2024.html", "Variety_ Steven J. Horowitz's Top 10 Albums of 2024.html", "Vogue's 36 Best Albums of 2024.html", "Vulture's Best Albums of 2024.html", "The Washington Post's Best Albums of 2024.html", "The Wire's Records of the Year 2024.html", "WXPN's Best Albums of 2024.html" ]; const loaded = new Set(dactal.data.aoty.map((a) => a.source)); dactal.data.aoty ||= []; for (const aotylist of aotylists.filter((a) => !loaded.has(a.replaceAll('_', ':').replaceAll("'''", "'")))) { const url = '/aoty/' + encodeURIComponent(aotylist.replace()); console.log(url) const pageres = await fetch(url); const pagetext = await pageres.text(); const pagedom = document.createElement('iframe'); document.body.appendChild(pagedom); const list = {source: aotylist.replaceAll('_', ':').replaceAll("'''", "'"), entries: []} pagedom.addEventListener("load", (e) => { const doc = e.target.contentDocument; const listitems = doc.querySelectorAll('.albumListRow'); for (listitem of listitems) { [artist, album] = listitem.querySelector('.albumListTitle').textContent.replace(/^\d+. /, '').split(' - '); albumkey = artist + ': ' + album; spotify = listitem.querySelector('a[data-track-action="Spotify"]')?.href; entry = {albumkey: albumkey, aotylist: aotylist, aotyartist: artist, aotyalbum: album, aotyspotify: spotify, albumid: spotify ? spotify.split('/')[4] : null}; list.entries.unshift(entry); } dactal.data.aoty.push(list); pagedom.remove(); }) pagedom.srcdoc = pagetext; } } async function saveaoty() { await db_set('aoty', dactal.data.aoty); for (const aq of dactal.data.queries.filter((q) => q.tag == 'aoty').sort((a, b) => a.name.localeCompare(b.name)).reverse()) { console.log(aq.name) aq.results = await dactal.query(aq.query) } await db_set('queries', dactal.data.queries); }
Curio
data stored
Spotify API Client ID

To use this experimental DIY version of Curio:
- sign in to Spotify on developer.spotify.com
- go to the API Dashboard and "Create app"
- go to your new app's Settings page
- add https://everynoise.com/callback.html to the list of "Redirect URIs"
- check Web API and Web Playback SDK under "APIs used"
- Save your settings
- copy the app's "Client ID" into this form
- hit Enter
 
(this information will only be saved in your browser's local storage)