MusicBrainz: Apple Music 导入器

从 Apple Music 导入发行信息到 MusicBrainz,并提供封面下载及智能页面跳转功能。

// ==UserScript==
// @name         MusicBrainz: Apple Music Importer
// @name:zh-CN   MusicBrainz: Apple Music 导入器
// @namespace    https://github.com/MoeclubM
// @version      1.0.0
// @description  Imports release information from Apple Music to MusicBrainz, provides cover download and smart page navigation.
// @description:zh-CN 从 Apple Music 导入发行信息到 MusicBrainz,并提供封面下载及智能页面跳转功能。
// @author       MoeCaa
// @license      MIT
// @homepageURL  https://github.com/MoeclubM/webscripts/tree/main/musicbrainz
// @supportURL   https://github.com/MoeclubM/webscripts/issues
// @match        *://music.apple.com/*/album/*
// @match        *://music.apple.com/*/song/*
// @connect      musicbrainz.org
// @connect      *.mzstatic.com
// @grant        GM_addStyle
// @grant        GM_xmlHttpRequest
// @grant        GM_info
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const MB_DOMAIN = 'musicbrainz.org';
    let lastProcessedUrl = '';

    // --- Styles ---
    GM_addStyle(`
        .mb-button-wrapper {
            position: fixed; top: 80px; right: 15px; z-index: 9999;
            display: flex; flex-direction: column; gap: 5px;
        }
        .mb-button {
            color: white; padding: 10px 15px;
            border-radius: 5px; border: none; font-size: 14px; font-weight: bold;
            cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            transition: background-color 0.2s;
            text-decoration: none; text-align: center;
            display: block;
        }
        #mb-importer-btn { background-color: #BA54A2; }
        #mb-importer-btn:hover { background-color: #8A3373; }
        #mb-artwork-downloader-btn { background-color: #555555; }
        #mb-artwork-downloader-btn:hover { background-color: #333333; }
        #mb-redirect-to-album-btn { background-color: #28a745; }
        #mb-redirect-to-album-btn:hover { background-color: #218838; }
    `);

    // --- Helper Functions ---

    function parseISODuration(duration) {
        if (!duration) return 0;
        const matches = duration.match(/PT(?:(\d+)M)?(?:(\d+)S)?/);
        const minutes = matches[1] ? parseInt(matches[1], 10) : 0;
        const seconds = matches[2] ? parseInt(matches[2], 10) : 0;
        return (minutes * 60 + seconds) * 1000;
    }

    function addField(form, name, value) {
        if (value === undefined || value === null || value === '') return;
        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = name;
        input.value = value;
        form.appendChild(input);
    }

    // --- Main Seeding Function ---

    function importToMusicBrainz() {
        console.log('Importing to MusicBrainz...');

        const schemaScript = document.querySelector('script[id="schema:music-album"]');
        if (!schemaScript) {
            alert('Error: Could not find album data on the page! Please make sure you are on an album page.');
            return;
        }

        const data = JSON.parse(schemaScript.textContent);
        const release = {};

        const originalTitle = data.name;
        const titleLower = originalTitle.toLowerCase();

        // 1. Determine Release Group Type (Primary and Secondary)
        release.primaryType = 'album';
        release.secondaryTypes = [];

        if (/ - ep$/i.test(originalTitle)) release.primaryType = 'ep';
        else if (/ - single$/i.test(originalTitle)) release.primaryType = 'single';

        if (titleLower.includes('original soundtrack') || titleLower.includes('soundtrack')) release.secondaryTypes.push('soundtrack');
        if (/\blive\b/i.test(titleLower)) release.secondaryTypes.push('live');
        if (/\bremix\b/i.test(titleLower)) release.secondaryTypes.push('remix');
        if (/\bdemo\b/i.test(titleLower)) release.secondaryTypes.push('demo');
        if (/\bmixtape\b/i.test(titleLower)) release.secondaryTypes.push('mixtape/street');

        // 2. Prepare Release Data
        release.title = originalTitle
            .replace(/ - (EP|Single)$/i, '')
            .replace(/\s*\((Original Soundtrack|Soundtrack|Live|Remix|Demo|Mixtape)\)/ig, '')
            .trim();

        release.artist = data.byArtist[0].name;

        const releaseDate = new Date(data.datePublished);
        release.year = releaseDate.getUTCFullYear();
        release.month = String(releaseDate.getUTCMonth() + 1).padStart(2, '0');
        release.day = String(releaseDate.getUTCDate()).padStart(2, '0');

        const countryMatch = window.location.pathname.match(/^\/([a-z]{2})\//);
        release.country = countryMatch ? countryMatch[1].toUpperCase() : 'XW';

        const footerDescElement = document.querySelector('p[data-testid="tracklist-footer-description"]');
        if (footerDescElement) {
            const copyrightLineMatch = footerDescElement.textContent.match(/(℗\s*\d{4}\s*.*)/);
            if (copyrightLineMatch) {
                release.annotation = copyrightLineMatch[0].trim();
                const labelTextMatch = copyrightLineMatch[0].match(/℗\s*\d{4}\s*(.*)/);
                if (labelTextMatch) release.label = labelTextMatch[1].trim();
            }
        }

        const artworkUrlForBarcode = document.querySelector('meta[property="og:image"]')?.content;
        if (artworkUrlForBarcode) {
            const barcodeMatch = artworkUrlForBarcode.match(/(\d{12,13})/);
            if (barcodeMatch) release.barcode = barcodeMatch[1];
        }

        // 3. Build and Submit Form
        const form = document.createElement('form');
        form.method = 'POST';
        form.action = `https://${MB_DOMAIN}/release/add`;
        form.target = '_blank';
        form.acceptCharset = 'UTF-8';

        addField(form, 'name', release.title);
        addField(form, 'artist_credit.names.0.name', release.artist);
        addField(form, 'status', 'official');
        addField(form, 'packaging', 'None');
        if (release.barcode) addField(form, 'barcode', release.barcode);
        if (release.annotation) addField(form, 'annotation', release.annotation);
        addField(form, 'comment', 'Digital Media');

        addField(form, 'events.0.date.year', release.year);
        addField(form, 'events.0.date.month', release.month);
        addField(form, 'events.0.date.day', release.day);
        addField(form, 'events.0.country', release.country);

        addField(form, 'type', release.primaryType);
        release.secondaryTypes.forEach(secType => {
            addField(form, 'type', secType);
        });

        if (release.label) addField(form, 'labels.0.name', release.label);

        const trackRows = document.querySelectorAll('div[data-testid="track-list-item"]');
        data.tracks.forEach((trackData, index) => {
            const prefix = `mediums.0.track.${index}`;
            addField(form, `${prefix}.name`, trackData.name);
            addField(form, `${prefix}.number`, index + 1);
            addField(form, `${prefix}.length`, parseISODuration(trackData.duration));

            const trackRow = trackRows[index];
            if (trackRow) {
                const byLine = trackRow.querySelector('.songs-list-row__by-line');
                const artists = byLine ? Array.from(byLine.querySelectorAll('a')).map(a => a.textContent.trim()) : [release.artist];
                artists.forEach((artist, artIndex) => {
                     addField(form, `${prefix}.artist_credit.names.${artIndex}.name`, artist);
                     if (artIndex < artists.length - 1) {
                         addField(form, `${prefix}.artist_credit.names.${artIndex}.join_phrase`, ', ');
                     }
                });
            }
        });

        addField(form, 'mediums.0.format', 'Digital Media');

        addField(form, 'urls.0.url', window.location.href);

        const allTypes = [release.primaryType.toUpperCase(), ...release.secondaryTypes.map(t => t.charAt(0).toUpperCase() + t.slice(1))];
        const editNote = `Types automatically set by script: ${allTypes.join(', ')}\n\nImported from Apple Music: ${window.location.href}`;
        addField(form, 'edit_note', editNote);

        document.body.appendChild(form);
        form.submit();
        document.body.removeChild(form);
    }

    // --- UI Setup Functions ---

    function cleanupInterface() {
        const wrapper = document.getElementById('mb-button-wrapper');
        if (wrapper) wrapper.remove();
    }

    function setupAlbumInterface(albumSchema) {
        cleanupInterface();

        const wrapper = document.createElement('div');
        wrapper.id = 'mb-button-wrapper';
        wrapper.className = 'mb-button-wrapper';

        const artworkSrcset = document.querySelector('div[data-testid="artwork-component"] picture source[type="image/jpeg"]')?.srcset;
        if (artworkSrcset) {
            const bestArtUrl = artworkSrcset.split(',').pop().trim().split(' ')[0];
            const highResUrl = bestArtUrl.replace(/\/\d+x\d+bb(?:-60)?\.jpg$/, '/9999x9999bb.jpg');

            const openArtLink = document.createElement('a');
            openArtLink.id = 'mb-artwork-downloader-btn';
            openArtLink.className = 'mb-button';
            openArtLink.href = highResUrl;
            openArtLink.target = '_blank';
            openArtLink.innerHTML = 'ART';
            openArtLink.title = 'Open highest resolution artwork in new tab';
            wrapper.appendChild(openArtLink);
        }

        const importBtn = document.createElement('button');
        importBtn.id = 'mb-importer-btn';
        importBtn.className = 'mb-button';
        importBtn.innerHTML = 'MB';
        importBtn.title = 'Import this release to MusicBrainz';
        importBtn.addEventListener('click', importToMusicBrainz, false);
        wrapper.appendChild(importBtn);

        document.body.appendChild(wrapper);
    }

    function setupSongInterface(songSchema) {
        cleanupInterface();

        const data = JSON.parse(songSchema.textContent);
        const albumUrl = data.inAlbum?.url;

        if (albumUrl) {
            const wrapper = document.createElement('div');
            wrapper.id = 'mb-button-wrapper';
            wrapper.className = 'mb-button-wrapper';

            const redirectBtn = document.createElement('a');
            redirectBtn.id = 'mb-redirect-to-album-btn';
            redirectBtn.className = 'mb-button';
            redirectBtn.innerHTML = 'Go to Album Page';
            redirectBtn.title = 'This is a song page. Click to navigate to the full album page for import.';
            redirectBtn.href = albumUrl;

            wrapper.appendChild(redirectBtn);
            document.body.appendChild(wrapper);
        }
    }

    // --- Main Execution Logic ---

    function mainLoop() {
        const currentUrl = window.location.href;
        if (currentUrl === lastProcessedUrl) {
            return;
        }

        cleanupInterface();
        lastProcessedUrl = currentUrl;

        setTimeout(() => {
            const albumSchema = document.querySelector('script[id="schema:music-album"]');
            const songSchema = document.querySelector('script[id="schema:song"]');

            if (albumSchema) {
                setupAlbumInterface(albumSchema);
            } else if (songSchema) {
                setupSongInterface(songSchema);
            }
        }, 500);
    }

    setInterval(mainLoop, 500);

})();