What.CD: YAVAH

Yet Another Various Artists Helper

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           What.CD: YAVAH
// @namespace      hateradio)))
// @author         hateradio
// @version        7.2
// @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\.sh/(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        26 Nov 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()

})()