What.CD: YAVAH

Yet Another Various Artists Helper

// ==UserScript==
// @name           What.CD: YAVAH
// @namespace      hateradio)))
// @author         hateradio
// @version        7.1
// @description    Yet Another Various Artists Helper
// @icon           data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjY0NDMyN0UzOUEwQzExRTA4REU5OUY2M0Q5RDZGNTQ1IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjY0NDMyN0U0OUEwQzExRTA4REU5OUY2M0Q5RDZGNTQ1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NjQ0MzI3RTE5QTBDMTFFMDhERTk5RjYzRDlENkY1NDUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjQ0MzI3RTI5QTBDMTFFMDhERTk5RjYzRDlENkY1NDUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6q1rvzAAAEMElEQVR42uyaP0gcQRTG12CpYBkVUkWto22CYqtiq7FWrNVervdPK5r2oq2orSiptQkE1LRqkSooBNIk+xvyLe/m9la9XDzIvoFh9mZnZme+973vvRE7enp6kjKXF3/aD2n9VbJa4eAdKQPepu2nkhJgAAa8KrEHvHyRlLw4AA6AA+AAOAAOgAPgADgADoAD4AA4AA6AA+AAOAAOgAPgADgADoAD4AA4AA6AA+AAOAAOgAPQmjI7O5scHx8nq6urWd/o6Gjoo/JeZWFhIfTRtqIsLS3VfaNtDOju7s6ep6amcvsnJiZCe3Bw0JJvjoyMhPbi4iL3fedzHPzm5ia0XV1doR0cHEyGh4eTu7u7cHj1Awq/T09Pk9vb21wAsSjsoVxeXiaVSqVmLCzT+729vWztvPXapgGiIwe1DNDGT05Ocg+/tbWVjRGQy8vL2e+1tbWa9zMzM2EeQLcVAH2czVCxPn2Hh4ehv7e3N2MF/QImBo1x5+fnyfj4eLK4uBj6BwYGMvZo/tzcXBjDWMrV1VXDvXU+p+WhIwcBBOhpgRkbGwvPAiUusuz6+npGfw4Zv2e+1mUMoNC2FQBLP0SO37u7u+E39AQYDsBzHgAAhPUp9/f3ud/Qe+mN7WtE/7ZEAYlc3gHpB4SiIsorvMXhUqoPoGLF9fV1exnAoaT4tLK+rCMRtOK3v78f2unp6TAHGqMTCJ2dq7V4D4iTk5Oh2lKkAc8eBWIri9IcQKKF3wKKpS7hTu8pPK+srGRrbWxsZO+tK7FGEav4X+H3aVstaSb87tEMIAbjc/heXppbrVZr4rnSXJv+QmH1S6Di1Jh1rDbgCvTLJeJCf/yOeUVzmnIBCRcb1UE50Pz8fBaeRDUrTIxRgeYaY/vtHMKjzeP1LRsJVOQqsY9LKIt8/8kAIDYSLImM2LC5uZn5n01YFIpsri+/7uvrq0tyAEh3AAACbMZrTgxaf39/Xegr6v9rEZSwAACHZ0P0adMWnO3t7WzjsohYYGO01tOcmBGsf3Z2VgdaUZz/JwwQC6Cwwg1WRn1jS7JpDqqNDQ0N1TFA8VpzcDGxBssrgxPz7JzYvXBDaQtVgBbF/6bD4NHRUc1NzAoPWR4A7ezshD5Zzlpb11KyP/q5sDDHWl+XJcV4zYk1wDIrrzyGAU9OhCRiWMvGVytYsfpay2lTjOUmF98LOLwsayOIAKAyVtqiy48dQyR5KP43zQBtzvqdBKvoEmQB1Fzd3mxmGGdxjXL+Rv6ft7+WMkAfsP5lQ5ilsuK4rsDycaWtEjlZykYDXXdtHsK30RPWka7ENz0JpdyvpQxQLGbDsWDFOX5sbSuE2rRETmujB9b3bdEcGaARAx66NXoq3Gwq7H8WdwD+XwB+lPj8PwHgOK3fSnj4r2n9TB7wPa1v0ko0eF2Sw39J60fY/1uAAQDAo7S7+Gt3nAAAAABJRU5ErkJggg==
// @include        /https://redacted\.ch/(torrents\.php(\?|\?page=\d+&)id=\d+(&torrentid=\d+)?(#comments)?|upload\.php(\?requestid=\d+)?|requests\.php*)/
// @include        /https://orpheus\.network/(torrents\.php(\?|\?page=\d+&)id=\d+(&torrentid=\d+)?(#comments)?|upload\.php(\?requestid=\d+)?|requests\.php*)/
// @include        /https://notwhat\.cd/(torrents\.php(\?|\?page=\d+&)id=\d+(&torrentid=\d+)?(#comments)?|upload\.php(\?requestid=\d+)?|requests\.php*)/
// #updated        22 Sep 2024
// #since          18 Jun 2010
// ==/UserScript==

(() => {

    if (!Element.prototype.matches)
        Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector

    if (!Element.prototype.closest) {
        Element.prototype.closest = function (s) {
            let el = this
            if (!document.documentElement.contains(el)) return null
            do {
                if (el.matches(s)) return el
                el = el.parentElement || el.parentNode
            } while (el !== null && el.nodeType === 1)
            return null
        }
    }

    const _ = {
        css: text => {
            if (!this.style) {
                this.style = document.createElement('style')
                this.style.type = 'text/css'
                document.body.appendChild(this.style)
            }
            this.style.appendChild(document.createTextNode(`${text}\n`))
        },
        js: func => {
            const script = document.createElement('script')
            script.type = 'application/javascript'
            script.textContent = `;(${func})();`
            document.body.appendChild(script)
            document.body.removeChild(script)
        },
        debounce: (func, wait) => {
            let timeout
            return function (...args) {
                const run = () => {
                    timeout = null
                    func.apply(this, args)
                }

                clearTimeout(timeout)
                timeout = setTimeout(run, wait)
            }
        },
        on: (element, type, selector, listener) => {
            element.addEventListener(type, event => {
                const found = event.target.closest(selector)
                if (found) listener.call(found, event)
            }, false)
        }
    }

    class YavaMenu {

        constructor() {
            this.sibling = document.querySelector('.box_addartists, #artist_tr')
            this.setup()
            YavaMenu.check = document.getElementById('yavah_semi')
        }

        get types() {
            return ['Main', 'Guest', 'Remixer', 'Composer', 'Conductor', 'DJ / Compiler', 'Producer']
        }

        setup() {
            if (!this.sibling) return

            const box = document.createElement('div')
            box.id = 'YAVAH'
            _.on(box, 'click', 'a', this.toggle)

            this.boxSetup(box)

            box.querySelector('a').click()
            this.box = box
        }

        boxSetup(box) {
            box.className = 'box'
            box.innerHTML = `
                <div class="head">
                    <strong><abbr title="Yet Another Various Artists Helper">YAVAH</abbr></strong>
                </div>
                <div style="padding: 3px 6px 6px">
                    ${this.generateInputs()}
                </div>`

            this.sibling.parentElement.insertBefore(box, this.sibling)
        }

        toggle(e) {
            e.preventDefault()
            const tog = this.parentElement.nextElementSibling.classList.toggle('hidden')
            this.innerHTML = `<code>${tog ? '+' : '-'}</code> ${this.dataset.type}`
        }

        generateInputs() {
            // let yavahtog = this.nextElementSibling.classList.toggle('hidden');
            // this.firstElementChild.innerHTML = '<code>' + (yavahtog ? '+' : '-') + '</code> ' + this.firstElementChild.dataset.type
            const boxes = this.types.map(type => {
                return `<p><a href="#" data-type="${type}"><code>+</code> ${type}</a></p><textarea class="noWhutBB yavahtext hidden"></textarea>`
            }).join('')

            return `
            <p>Enter artists, one per line.</p>
            <p>
                <input type="checkbox" id="yavah_semi"> <label for="yavah_semi">Split semi-colons</label>
            </p>
            ${boxes}
            <p>Review the changes below.</p>`
        }

        addEvent(cb) {
            return this.box && !this.box.addEventListener('input', _.debounce(cb, 250), false)
        }

    }

    class YavaMenuTr extends YavaMenu {
        boxSetup(box) {
            box.innerHTML = this.generateInputs()

            const tr = document.createElement('tr')
            tr.innerHTML = '<td class="label">YAVAH</td><td></td>'
            tr.lastElementChild.appendChild(box)

            this.sibling.parentElement.insertBefore(tr, this.sibling)
        }
    }

    class Yavah {

        constructor() {
            this.selector = 'input[name="aliasname[]"]:last-of-type, input[name="artists[]"]:last-of-type'
            this.add = document.querySelector('.box_addartists a, #artistfields a.brackets')
            this.inputs = document.querySelector('#AddArtists, #artistfields')

            if (!this.inputs)
                return

            _.css('#YAVAH p a { display:block } .yavahtext{width: 90%; height: 6em}')

            this.stored = this.inputs.innerHTML
            this.event = this.event.bind(this)
        }

        regex() {
            if (YavaMenu.check.checked)
                return /[^\r\n;]+/g
            return /[^\r\n]+/g
        }

        /**
         *
         * @param {HTMLTextAreaElement} textarea
         * @param {number} index Index of HTMLOptionElement within HTMLSelectElement to set (Main, Guest, Composer, etc.)
         */
        fill(textarea, index) {
            const lines = textarea.value.match(this.REGEX) || []
            const unique = new Set(lines.map(_ => _.trim()))
            unique.delete('')

            unique.forEach(name => {
                this.inputs.querySelector(this.selector).value = name
                this.inputs.querySelector('select:last-of-type').value = index + 1
                this.add.click()
            })
        }

        event() {
            // Reset the artist box
            this.inputs.innerHTML = this.stored
            _.js(() => window.ArtistFieldCount = -1000)

            const textareas = document.querySelectorAll('#YAVAH textarea')
            this.REGEX = this.regex()
            Array.from(textareas).forEach(this.fill, this)
        }

        static main() {
            const yava = new Yavah()
            const menu = /(?:requests\.php|upload\.php)/.test(document.location.pathname) ? new YavaMenuTr : new YavaMenu
            menu.addEvent(yava.event)

            if (document.location.hash === '#yavah-test') {
                new YavaTest(yava)
            }
        }

    }

    class YavaTest {
        constructor(yavah) {
            this.y = yavah

            this.indices = '1-1-1-2-2-2-3-3-3-4-4-4-5-5-5-6-6-6-7-7-7-1'
            this.values = 'A1;A1.1-A2-A3-B1;B1.1-B2-B3-C1;C1.1-C2-C3-D1;D1.1-D2-D3-E1;E1.1-E2-E3-F1;F1.1-F2-F3-G1;G1.1-G2-G3-'

            this.indicesSemi =  '1-1-1-1-2-2-2-2-3-3-3-3-4-4-4-4-5-5-5-5-6-6-6-6-7-7-7-7-1'
            this.valuesSemi = this.values.replaceAll(';', '-')

            this.testInit()
            this.testRegular()
            this.testSemi()
        }

        testInit() {
            console.log('YAVAH TEST!')

            // asume if no inputs, then there is nothing to do
            if (!this.y.inputs)
                return

            // fill data
            const textareas = Array.from(document.querySelectorAll('.yavahtext'))
            textareas.forEach((t, i) => {
                const ch = String.fromCharCode(65 + i);
                t.value = `${ch}1;${ch}1.1\n${ch}2\n${ch}3`
            })

            // go for it!
            this.y.event()
        }

        selects(indices) {
            // assert dropdowns
            console.group('YAVAH DROPDOWN TEST')

            const selects = document.querySelectorAll('#AddArtists select, #artistfields select')

            if (selects.length === indices.split('-').length)
                console.debug('[Passed] Valid number of dropdowns')
            else
                console.warn('Invalid number of dropdowns')

            if (Array.from(selects).map(_ => _.value).join('-') === indices)
                console.debug('[Passed] Valid values for dropdowns')
            else
                console.warn('Invalid values for dropdowns')

            console.groupEnd()
        }

        inputs(values) {
            // assert inputs
            console.group('YAVAH INPUT TEST')

            const inputs = document.querySelectorAll('input[name="aliasname[]"], input[name="artists[]"]')

            if (inputs.length === values.split('-').length)
                console.debug('[Passed] Valid number of inputs')
            else
                console.warn('Invalid number of inputs')

            if (Array.from(inputs).map(_ => _.value).join('-') === values)
                console.debug('[Passed] Valid values for inputs')
            else
                console.warn('Invalid values for inputs')
            
            console.groupEnd()
        }

        testRegular() {
            console.info('Starting Regular Test')
            this.selects(this.indices)
            this.inputs(this.values)
        }

        testSemi() {
            console.info('Starting Semicolon Test')
            YavaMenu.check.checked = true
            this.y.event()
            this.selects(this.indicesSemi)
            this.inputs(this.valuesSemi)
        }

    }

    Yavah.main()

})()