SoundCloud: Additional "Add to playlist" button

Adds an "Add to Playlist" button for easier and quicker access.

// ==UserScript==
// @name         SoundCloud: Additional "Add to playlist" button
// @description  Adds an "Add to Playlist" button for easier and quicker access.
// @version      1.1
// @author       iammordaty
// @namespace    https://github.com/iammordaty
// @match        https://soundcloud.com/*
// @license      MIT
// @grant        none
// @icon         https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
// ==/UserScript==

const ELEMENTS = '.soundList__item, .soundBadgeList__item, .listenEngagement__footer, .trackList__item, .historicalPlays__item';

const BUTTON_CLASS_NAMES = [
    'sc-button',
    'sc-button-icon',
    'sc-button-medium',
    'sc-button-responsive',
    'sc-button-secondary',
    'sc-button-small',
];

const getButtonClassList = refNodeClassList =>
    [...BUTTON_CLASS_NAMES.filter(value => refNodeClassList.includes(value)),
        'sc-button-add-to-playlist',
        'sc-button-addtoset'
    ];

const createButton = (container, refNode) => {
    const button = document.createElement('button');
    button.setAttribute('role', 'button');

    const classList = getButtonClassList([...refNode.classList]);
    button.classList.add(...classList);
    button.setAttribute('title', 'Add this track to Playlist');

    const innerDiv = document.createElement('div');
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 16 16');
    svg.setAttribute('aria-hidden', 'true');

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', 'M3.25 7V4.75H1v-1.5h2.25V1h1.5v2.25H7v1.5H4.75V7h-1.5zM9 4.75h6v-1.5H9v1.5zM15 9.875H1v-1.5h14v1.5zM1 15h14v-1.5H1V15z');
    path.setAttribute('fill', 'currentColor');

    svg.appendChild(path);
    innerDiv.appendChild(svg);
    button.appendChild(innerDiv);

    button.addEventListener('click', () => {
        container.querySelector('button[aria-label="more" i]').click();
        document.querySelector('button[title="add to playlist" i]').click();
    }, false);

    return button;
};

const insertAfter = (button, refButton) => refButton.parentNode.insertBefore(button, refButton);

const pending = new Set();
let scheduled = false;

const scheduleProcess = () => {
    if (scheduled) {
        return;
    }

    scheduled = true;

    requestAnimationFrame(() => {
        scheduled = false;

        pending.forEach(container => processContainer(container));
        pending.clear();
    });
};

const processContainer = container => {
    if (!(container instanceof HTMLElement)) {
        return;
    }

    const refButton = container.querySelector('.sc-button-more');

    if (!refButton) {
        return;
    }

    if (container.querySelector('.sc-button-add-to-playlist')) {
        return;
    }

    const button = createButton(container, refButton);

    insertAfter(button, refButton);
};

const scanPage = () => {
    document.querySelectorAll(ELEMENTS).forEach(el => pending.add(el));

    scheduleProcess();
};

const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
            if (!(node instanceof HTMLElement)) {
                continue;
            }

            if (node.matches(ELEMENTS)) {
                pending.add(node);
            }

            node.querySelectorAll?.(ELEMENTS).forEach(el => {
                pending.add(el);
            });
        }
    }

    scheduleProcess();
});

observer.observe(document.body, {
    childList: true,
    subtree: true
});

document.addEventListener('DOMContentLoaded', scanPage, false);
window.addEventListener('load', scanPage, false);