VGMdb Metadata Format

Copy album metadata as a formatted string to clipboard

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         VGMdb Metadata Format
// @namespace    https://vgmdb.net/
// @version      2.1
// @description  Copy album metadata as a formatted string to clipboard
// @author       kahpaibe
// @match        https://vgmdb.net/album/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const COPY_BUTTON_TEXT = 'Copy All Metadata';
    const COPIED_TEXT = '✔ COPIED!';

// ------------------------------------------------------------------
// --- Button Styling and Helper Functions (Unchanged) ---
// ------------------------------------------------------------------

    const styleButton = (button) => {
        button.style.cursor = 'pointer';
        button.style.marginLeft = '8px';
        button.style.padding = '1px 6px';
        button.style.fontSize = '0.75em';
        button.style.color = '#FFD700';
        button.style.background = 'transparent';
        button.style.border = '1px solid #FFD700';
        button.style.transition = 'background 0.3s, color 0.3s';
        button.style.verticalAlign = 'middle';
    };

    const copyToClipboard = (text, button) => {
        navigator.clipboard.writeText(text).then(() => {
            const originalText = button.innerText;
            button.innerText = COPIED_TEXT;
            setTimeout(() => button.innerText = originalText, 1500);
        }).catch(err => {
            console.error('Could not copy text: ', err);
            alert('Failed to copy metadata.');
        });
    };

// ------------------------------------------------------------------
// --- Metadata Retrieval Functions (UPDATED: getFieldText) ---
// ------------------------------------------------------------------

    /**
     * Extracts a single text field (e.g., Catalog Number, Publisher) from the info table.
     */
    const getFieldText = (labelText) => {
        const rows = document.querySelectorAll('#album_infobit_large tr');
        const row = Array.from(rows).find(r => {
            const labelCell = r.querySelector('td span.label b');
            return labelCell && labelCell.textContent.trim() === labelText;
        });

        if (row && row.cells.length > 1) {
            const valueCell = row.cells[1];

            // For Publisher, we often want the linked text, if it exists
            if (labelText === 'Publisher') {
                const link = valueCell.querySelector('a.productname');
                if (link) {
                    return link.textContent.trim();
                }
            }
            // For Catalog Number and fallback, use the entire cell text
            return valueCell.textContent.trim();
        }
        return 'N/A';
    };

    const getAlbumTitle = () => {
        let mainTitleSpan = null;
        const innermain = document.querySelector('#innermain');
        if (!innermain) return 'N/A';

        // 1. Try to find a visible Japanese title span first
        const jaTitles = innermain.querySelectorAll('.albumtitle[lang="ja"]');
        mainTitleSpan = Array.from(jaTitles).find(span => {
            const computedStyle = window.getComputedStyle(span);
            return computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden';
        });

        // 2. If no visible Japanese title, fall back to the first general .albumtitle
        if (!mainTitleSpan) {
             mainTitleSpan = innermain.querySelector('.albumtitle');
        }

        if (mainTitleSpan) {
            // To get text content while ignoring buttons, clone the element and remove them.
            const temp = mainTitleSpan.cloneNode(true);
            temp.querySelectorAll('button').forEach(btn => btn.remove());
            const titleText = temp.textContent;
            return titleText.trim().replace(/(\s\/\s){2,}/g, ' / ').replace(/ {2,}/g, ' ');
        }
        return 'N/A';
    };

    const getFormattedTracklist = () => {
        const tracklistContainer = document.querySelector('#tracklist');
        if (!tracklistContainer) return 'Tracklist data not found.';

        const spans = tracklistContainer.querySelectorAll('span');
        let result = '';

        spans.forEach(span => {
            if (!/Disc \d+/.test(span.textContent)) return;

            let sibling = span.nextElementSibling;
            while (sibling && sibling.tagName !== 'TABLE') {
                sibling = sibling.nextElementSibling;
            }
            if (!sibling) return;

            const trackTable = sibling;
            const trackRows = trackTable.querySelectorAll('tr.rolebit');
            if (trackRows.length === 0) return;

            const discTitleMatch = span.textContent.match(/Disc \d+/);
            const discTitle = discTitleMatch ? discTitleMatch[0] : 'Unknown Disc';

            result += `\n**${discTitle}**:\n`;

            trackRows.forEach(row => {
                const number = row.querySelector('td .label')?.textContent.trim();
                const title = row.querySelectorAll('td')[1]?.textContent.trim();
                const duration = row.querySelectorAll('td')[2]?.textContent.trim();

                if (number && title && duration) {
                    result += `${number}. ${title} [${duration}]\n`;
                } else if (number && title) {
                    result += `${number}. ${title}\n`;
                }
            });
        });

        // Fallback for single-disc albums without a "Disc 1" header
        if (!result) {
            const directTable = tracklistContainer.querySelector('table');
            if (directTable) {
                result += `\n**Disc 1**:\n`;
                directTable.querySelectorAll('tr.rolebit').forEach(row => {
                    const number = row.querySelector('td .label')?.textContent.trim();
                    const title = row.querySelectorAll('td')[1]?.textContent.trim();
                    const duration = row.querySelectorAll('td')[2]?.textContent.trim();

                    if (number && title && duration) {
                        result += `${number}. ${title} [${duration}]\n`;
                    } else if (number && title) {
                        result += `${number}. ${title}\n`;
                    }
                });
            }
        }

        return result.trim() || 'Tracklist data not found.';
    };

// ------------------------------------------------------------------
// --- Final Metadata Generation and Execution (UPDATED) ---
// ------------------------------------------------------------------

    const generateMetadataText = () => {
        const url = window.location.href;
        const title = getAlbumTitle();
        const catalogNumber = getFieldText('Catalog Number');
        const artist = getFieldText('Publisher'); // Get Publisher to use as Artist
        const tracklist = getFormattedTracklist();

        let output = `Title: ${title}\n`;
        output += `Artist: ${artist}\n`; // NEW: Artist line
        output += `Catalog: ${catalogNumber}\n`;
        output += `Info: ${url}\n`;
        output += '\nTracklist:\n';
        output += tracklist;

        return output;
    };

    const initializeUnifiedButton = () => {
        const albumToolsSpan = document.getElementById('albumtools');
        const outerContainer = albumToolsSpan ? albumToolsSpan.parentElement : null;
        if (!outerContainer) return;

        const newButton = document.createElement('span');
        newButton.style.cursor = 'pointer';
        newButton.innerText = COPY_BUTTON_TEXT;
        styleButton(newButton);

        newButton.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const metadataText = generateMetadataText();
            copyToClipboard(metadataText, newButton);
        });

        // 1. Create a pipe separator for the button's right side
        const rightPipe = document.createElement('span');
        rightPipe.style.color = 'rgb(206, 255, 255)';
        rightPipe.innerText = ' | ';
        rightPipe.style.marginLeft = '8px';

        // 2. Insert the button and its pipe immediately before the existing albumtools span
        outerContainer.insertBefore(rightPipe, albumToolsSpan);
        outerContainer.insertBefore(newButton, rightPipe);
    };

    // Defer initialization slightly to ensure all page content has rendered.
    setTimeout(initializeUnifiedButton, 500);

})();