Everynoise Enhancement Script

A script to slightly enhance everynoise's user experience

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Everynoise Enhancement Script
// @namespace    http://tampermonkey.net/
// @version      0.4.9
// @description  A script to slightly enhance everynoise's user experience
// @author       NeroYuki
// @match        https://everynoise.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=https://everynoise.com/
// @license      MIT
// ==/UserScript==

/**
function highlight(which) {
    if (gdivs.length == 0) {
        gdivs = document.getElementsByClassName('genre');
    }
    for (i=0; i<gdivs.length; i++) {
        thisdiv = gdivs[i];
        thistext = thisdiv.firstChild.textContent;
        if (thisdiv.className.indexOf('scanme') > -1) {
            basename = 'genre scanme';
        } else {
            basename = 'genre';
        }
        if (which.length > 0 && which.trim() == thistext.trim().replace('"', '')) {
            thisdiv.className = basename + ' current';
            if(typeof thisdiv.scrollIntoViewIfNeeded == 'function') {
                thisdiv.scrollIntoViewIfNeeded();
            } else {
                thisdiv.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
            }
        } else {
            thisdiv.className = basename;
        }
    }
}
**/

(function() {
    'use strict';

    // Your code here...
    var head = document.getElementsByTagName('head')[0];
    var thisScript = document.scripts[0]
    var cloneScript= document.createElement('script');
    if (thisScript) {
        // remove var scandur = 6000
        cloneScript.textContent = thisScript.textContent.replace('var scandur = 6000;', '');
        cloneScript.textContent = cloneScript.textContent.replace('function playx(key, genre, me) {', 'async function playx(key, genre, me) {');
        cloneScript.textContent = cloneScript.textContent.replace('highlight(genre);', 'highlight(genre, sample_title);');
        cloneScript.textContent = cloneScript.textContent.replace('function highlight(which)', 'function highlight(which, sample_title = "")');
        cloneScript.textContent = cloneScript.textContent.replace('me.setAttribute(\'played\', clicknumber++);', '');
        cloneScript.textContent = cloneScript.textContent.replace('async function playx(key, genre, me) {', 'async function playx(key, genre, me, loop = false) {');
        cloneScript.textContent = cloneScript.textContent.replace('function scan(e, state) {' , 'function scan(e, state, scan_duration = 29000) { if (!scan_duration || scan_duration < 0) {scan_duration = 29000}');
        cloneScript.textContent = cloneScript.textContent.replace('scan("continue")', 'scan("continue", document.getElementById("scan_length").value * 1000)');
        cloneScript.textContent = cloneScript.textContent.replace('scan("stop")', 'scan("stop", document.getElementById("scan_length").value * 1000)');
        cloneScript.textContent = cloneScript.textContent.replace('scandur = 6000', 'scandur = scan_duration');
        cloneScript.textContent = cloneScript.textContent.replace('if (nowplaying == genre) {', 'if (nowplaying == genre && !loop) {');
        cloneScript.textContent = cloneScript.textContent.replace('window.clearTimeout(nowpending);', 'if (!loop) { window.clearTimeout(nowpending); }');
        cloneScript.textContent = cloneScript.textContent.replace('picked.onclick();', 'if (!isLooping) { window.clearTimeout(nowpending); picked.onclick(); } else { window.clearTimeout(nowpending); }');
        cloneScript.textContent = cloneScript.textContent.replace('thisdiv.scrollIntoViewIfNeeded(false);', 'thisdiv.scrollIntoViewIfNeeded(true);');
        var cloneScriptLines = cloneScript.textContent.split('\n');
        var dataInsertLine = 0
        cloneScriptLines.splice(dataInsertLine, 0, `
var genreData = [];
fetch('https://raw.githubusercontent.com/NeroYuki/everynoise_enhancement_script/master/spotify_genres.json')
    .then(async (response) => {
        genreData = await response.json();
        
        // if current path starts with engenremap, dont append genres
        if (window.location.pathname.startsWith('/engenremap')) {
            return;
        }
        // iterate genreData, if item have is_append, attempt to add it to html 
        const appendGenres = genreData.filter(val => val.is_append).sort((a, b) => b.organic_index - a.organic_index);
        let id = 20000;

        // get the position of all genre divs 
        const elements = document.getElementsByClassName('genre');

        const positions = [];

        for (let i = 0; i < elements.length; i++) {
            const element = elements[i];
            const rect = element.getBoundingClientRect();
            const position = {
                top: parseInt(element.style.top.replace('px', '')),
                left: parseInt(element.style.left.replace('px', '')),
                width: rect.width,
                height: rect.height,
                id: element.id
            };
            positions.push(position);
        }

        // sort the positions by top ascending
        positions.sort((a, b) => a.top - b.top);

        console.log(positions);

        for (const genre of appendGenres) {
            // select div class canvas and append the genre follow this template
            // <div id=item8 preview_url="https://p.scdn.co/mp3-preview/3c1278cf0eb6aba0f72552a3aa469dfed37d8e75" class="genre scanme" scan=true style="color: #c18f02; top: 3064px; left: 1170px; font-size: 130%" role=button tabindex=0 onKeyDown="kb(event);" onclick="playx(&quot;0AAl3LtvIhEilWXZmYHeh5&quot;, &quot;reggaeton&quot;, this);" title="e.g. Zion &quot;More&quot;">reggaeton<a class=navlink href="engenremap-reggaeton.html" role=button tabindex=0 onKeyDown="kb(event);" onclick="event.stopPropagation();" >&raquo;</a> </div>
            const canvas = document.querySelector('.canvas');
            const genreDiv = document.createElement('div');
            genreDiv.className = 'genre scanme';
            genreDiv.id = "item" + id++;
            genreDiv.setAttribute('scan', true);
            // color is array of hsv, use css hsl to set color
            genreDiv.style.color = \`hsl(\${genre.color[0]}, \${genre.color[1] * 100}%, \${genre.color[2] * 100}%)\`;
            // top should use the organic_index * canvas height, must not overlap with other elements
            const initialTop = genre.organic_index * canvas.clientHeight;
            genreDiv.style.top = \`\${genre.organic_index * canvas.clientHeight}px\`;
            // left should use the atmospheric_index * canvas width
            const initialLeft = genre.atmospheric_index * canvas.clientWidth;
            genreDiv.style.left = \`\${genre.atmospheric_index * canvas.clientWidth}px\`;
            // font-size should use popularity level
            genreDiv.style.fontSize = \`100%\`;
            genreDiv.setAttribute('role', 'button');
            genreDiv.setAttribute('tabindex', 0);
            genreDiv.setAttribute('onKeyDown', 'kb(event);');
            //let spotify_playlist_id = genre.spotify_playlist.replace('https://embed.spotify.com/?uri=spotify:playlist:', '');
            genreDiv.setAttribute('onclick', \`playx("\${genre.track_id}", "\${genre.genre}", this);\`);
            genreDiv.setAttribute('title', \`e.g. \${genre.sample_song}\`);
            genreDiv.textContent = genre.genre;
            canvas.appendChild(genreDiv);

            // iterate from the the element with the same top position and check if it overlaps
            const rect = genreDiv.getBoundingClientRect();
            let overlapCheckStartIndex = positions.findIndex(val => val.top + rect.height >= initialTop);
            // start checking from overlapCheckStartIndex, if any of it overlaps with the current element
            if (overlapCheckStartIndex !== -1) {
                let overlap = false;
                let adjustment_y = 0;
                for (let i = overlapCheckStartIndex; i < positions.length; i++) {
                    const position = positions[i];
                    if ((position.top + position.height >= initialTop && position.top <= initialTop + rect.height) && (position.left + position.width >= initialLeft && position.left <= initialLeft + rect.width)) {
                        overlap = true;
                        adjustment_y = Math.max(adjustment_y, rect.height - (position.top - initialTop));
                        //console.log('overlap found, adjustment:', adjustment_y);
                    }
                    if (overlap) {
                        // shift every element below the current element by the adjustment_y
                        position.top += adjustment_y;
                    }
                    // in case position.top > top + 20, it is guaranteed that the rest of the elements will not overlap, break the loop
                    else if (position.top > initialTop + rect.height) {
                        break;
                    }
                }
            }

            // add the new element itself to the positions array
            positions.push({
                top: initialTop,
                left: initialLeft,
                width: rect.width,
                height: rect.height,
                id: genreDiv.id
            });

            // sort the positions array again
            positions.sort((a, b) => a.top - b.top);
        }

        console.log(positions);

        // re-position the elements with the positions array
        for (let i = 0; i < positions.length; i++) {
            const position = positions[i];
            const element = document.getElementById(position.id);
            element.style.top = position.top + 'px';
        }

        // adjust canvas size to fit all elements
        const lastPosition = positions[positions.length - 1];
        const canvasDiv = document.querySelector('.canvas');

        canvasDiv.style.height = lastPosition.top + lastPosition.height + 'px';
    });

var isLooping = false;

const dbName = "everynoise_ext";
let db = null;

const request = indexedDB.open(dbName, 3);

request.onerror = (event) => {
    alert('error, please disable the userscript');
};

request.onsuccess = (event) => {
    db = event.target.result;

    db.transaction("genres")
        .objectStore("genres").get("pop").onsuccess = (event) => {
            const res = event.target.result
            if (!res) {
                console.log('adding data');
                fetch('https://raw.githubusercontent.com/NeroYuki/everynoise_enhancement_script/master/spotify_genres_artists_map.json')
                    .then(async (response) => {
                        console.log('fetched artists genre mapping');
                        genreArtist = await response.json();
                        for (const genre of genreArtist) {
                            db
                                .transaction("genres", "readwrite")
                                .objectStore("genres")
                                .add(genre);
                        }
                    });
            }
        }
};

request.onupgradeneeded = (event) => {
    db = event.target.result;
    // delete the existing object store
    if (db.objectStoreNames.contains("genres")) {
        db.deleteObjectStore("genres");
    }

    const objectStore = db.createObjectStore("genres", { keyPath: "genre" });

    // Use transaction oncomplete to make sure the objectStore creation is
    // finished before adding data into it.
    objectStore.transaction.oncomplete = (event) => {
        console.log('creation done')
    };
};

function getData(dataStore, key) {
    return new Promise((resolve, reject) => {
        const dataFetch = db.transaction(dataStore)
            .objectStore(dataStore).get(key);
        dataFetch.onsuccess = (event) => {
            const res = event.target.result
            resolve(res);
        }
        dataFetch.onerror = (event) => {
            reject(event);
        }

    })
}
        `);
        var insertLoopLine = cloneScriptLines.findIndex(val => val.includes('var spotifyplayer = document.getElementById(\'spotifyplayer\');')) + 1;
        cloneScriptLines.splice(insertLoopLine, 0, `
        console.log(genre, key);
        spotifyplayer.onended = () => {
            console.log('ended')
            playx(key, genre, me, true);
        };
        `);
        var insertSelectionLine = cloneScriptLines.findIndex(val => val.includes('previewurl = me.getAttribute(\'preview_url\')')) + 1;
        cloneScriptLines.splice(insertSelectionLine, 0, `
        let sample_title = me.getAttribute('title').replace('e.g. ', '');
        const res = await getData('genres', genre)
        var selectionIndex = res?.artists ? Math.floor(Math.random() * res.artists.length + 1) : 0;
        if (selectionIndex > 1) {
            previewurl = res.artists[selectionIndex - 1].preview_url;
            sample_title = res.artists[selectionIndex - 1].sample_song;
        }
        `);

        var insertStartResetStateLine = cloneScriptLines.findIndex(val => val.includes('if (state == \'stop\') {')) - 1;
        cloneScriptLines.splice(insertStartResetStateLine, 0, `
        if (state == 'start') {
            isLooping = false;
        }
        `);

        var insertLine = cloneScriptLines.findIndex(val => val.includes('thisdiv.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});')) + 2;
        cloneScriptLines.splice(insertLine, 0, `
            var label = document.getElementById('genre_label');
            if (!label) {
                label = document.createElement('div');
                document.body.appendChild(label);
            }
            var title_label = document.getElementById('title_label');
            if (!title_label) {
                title_label = document.createElement('div');
                document.body.appendChild(title_label);
            }
            var loop_label = document.getElementById('loop_label');
            if (!loop_label) {
                loop_label = document.createElement('div');
                document.body.appendChild(loop_label);
            }
            let genreInfo = genreData.find(val => val.genre === which.trim())
            label.id = 'genre_label';
            title_label.id = 'title_label';
            loop_label.id = 'loop_label';
            label.style.position = title_label.style.position = loop_label.style.position = 'absolute';
            label.style.color = title_label.style.color = loop_label.style.color = 'black';
            label.style.backgroundColor = title_label.style.backgroundColor = 'white';
            loop_label.style.backgroundColor = isLooping ? 'green' : 'white';
            label.style.padding = title_label.style.padding = loop_label.style.padding = '5px';
            label.style.display = title_label.style.display = loop_label.style.display = 'none';
            label.textContent = genreInfo ? genreInfo.desc : "No description";
            title_label.textContent = '▶  ' + (sample_title ? sample_title : "Unknown title");
            loop_label.textContent = '↻'
            // set the label position to be above the highlighted div
            var rect = thisdiv.getBoundingClientRect();

            label.style.left = Math.max(10, (parseInt(thisdiv.style.left) - 100)) + 'px';
            label.style.width = '300px'
            label.style.top = (parseInt(thisdiv.style.top) + 136) + 'px';
            label.style.border = 'solid 1px black';
            // show the label
            label.style.display = 'block';

            title_label.style.left = Math.max(10, (parseInt(thisdiv.style.left) - 100)) + 'px';
            title_label.style.top = (parseInt(thisdiv.style.top) + 76) + 'px';
            title_label.style.border = 'solid 1px black';
            // show the label
            title_label.style.display = sample_title ? 'block' : 'none';

            loop_label.style.left = Math.max(10, (parseInt(thisdiv.style.left) - 124)) + 'px';
            loop_label.style.top = (parseInt(thisdiv.style.top) + 76) + 'px';
            loop_label.style.border = 'solid 1px black';
            // show the label
            loop_label.style.display = 'block';

            loop_label.onclick = () => {
                isLooping = !isLooping;
                loop_label.style.backgroundColor = isLooping ? 'green' : 'white';
            }

            if(typeof label.scrollIntoViewIfNeeded == 'function') {
                label.scrollIntoViewIfNeeded();
            }
        `);
        cloneScript.textContent = cloneScriptLines.join('\n');
        head.appendChild(cloneScript);
        thisScript.remove();
    }

    // find element with id "scan" and append a text input next to it
    var scanElement = document.getElementById('scan');
    if (scanElement) {
        var inputElement = document.createElement('input');
        inputElement.type = 'number';
        inputElement.id = 'scan_length';
        inputElement.placeholder = 'scan length';
        inputElement.style.marginLeft = '10px';
        inputElement.style.width = '80px';
        scanElement.parentNode.insertBefore(inputElement, scanElement.nextSibling);
    }
    // change scan element's onclick property string to replace scan(event, 'start') and scan(event, 'stop') to scan(event, 'start', <scan_length>) and scan(event, 'stop', <scan_length>)
    if (scanElement) {
        scanElement.removeAttribute('onclick');

        // add event listener to scan element
        // if (this.innerText == 'scan') {scan(event, 'start');this.innerHTML = 'stop';} else {scan(event, 'stop');this.innerHTML = 'scan';}
        scanElement.addEventListener('click', function() {
            if (this.innerText == 'scan') {
                scan(event, 'start', document.getElementById('scan_length').value * 1000);
                this.innerHTML = 'stop';
            } else {
                scan(event, 'stop', document.getElementById('scan_length').value * 1000);
                this.innerHTML = 'scan';
            }
        });
    }
})();