MusicBrainz: TOWER RECORDS ONLINE Importer

Hello!

I made a simple userscript for importing release data from TOWER RECORDS ONLINE to MusicBrainz.
Feel free to use/modify it.

// ==UserScript==
// @name         MusicBrainz: TOWER RECORDS ONLINE Importer
// @version      1.0
// @match        https://tower.jp/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    function text(el) { return el ? el.textContent.trim() : ''; }

    function getJSONLD() {
        const scripts = document.querySelectorAll('script[type="application/ld+json"]');
        for (const s of scripts) {
            try {
                const json = JSON.parse(s.textContent);
                if (Array.isArray(json)) {
                    for (const item of json) if (item['@type'] === 'Product') return item;
                }
                if (json['@type'] === 'Product') return json;
            } catch (e) {}
        }
        return null;
    }

    function parseDate(str) {
        if (!str) return {};
        const m = str.match(/(\d{4})-(\d{2})-(\d{2})/);
        if (!m) return {};
        return { year: m[1], month: m[2], day: m[3] };
    }

    function getArtist(json) {
        return json?.byArtist?.name || text(document.querySelector('a[href*="/artist/"]')) || '';
    }

    function getCatalogNumber() {
        const tdList = document.querySelectorAll('td.TOL-item-info-PC-tab-basic-info-table-column');
        for (const td of tdList) {
            if (td.textContent.includes('規格品番')) {
                return td.nextElementSibling?.textContent.trim() || '';
            }
        }
        return '';
    }

    function parseLengthToMs(lengthStr) {
        const parts = lengthStr.split(':').map(p => parseInt(p, 10));
        if (parts.length === 2) return (parts[0]*60 + parts[1]) * 1000;
        if (parts.length === 3) return (parts[0]*3600 + parts[1]*60 + parts[2]) * 1000;
        return undefined;
    }

    // メディアごとに分けてトラック取得
    function getMediaList() {
        const mediaSpans = document.querySelectorAll('.text-size-16.is-bold');
        const mediaList = [];

        mediaSpans.forEach((span) => {
            const match = span.textContent.match(/^\d+\.\[(.+?)\]/);
            if (!match) return;
            let format = match[1].trim();

            // 変換処理
            if (format === 'CDシングル') format = 'CD';
            if (format === 'CDアルバム') format = 'CD';
            if (format === 'DVD') format = 'DVD-Video';
            if (format === 'Blu-ray Disc') format = 'Blu-ray';
            if (format === '書籍') format = null;
            if (format === 'グッズ') format = null;


            // 直下の ol > li からトラック取得
            const mediumLi = span.closest('li')?.querySelectorAll('ol > li.TOL-item-info-PC-tab-recorded-contents-list-track-item');
            if (!mediumLi) return;

            const tracks = [];
            mediumLi.forEach((li) => {
                const titleEl = li.querySelector('.TOL-item-info-PC-tab-recorded-contents-list-track-title');
                const lengthEl = li.querySelector('.TOL-item-info-PC-tab-recorded-contents-list-track-length');
                if (!titleEl) return;
                const title = titleEl.textContent.trim();
                const lengthMs = lengthEl ? parseLengthToMs(lengthEl.textContent.trim()) : undefined;
                tracks.push({ title, lengthMs });
            });

            mediaList.push({ format, tracks });
        });

        return mediaList;
    }

    function buildForm(data) {
        const form = document.createElement('form');
        form.method = 'POST';
        form.action = 'https://musicbrainz.org/release/add';

        function add(name, value) {
            if (!value) return;
            const input = document.createElement('input');
            input.type = 'hidden';
            input.name = name;
            input.value = value;
            form.appendChild(input);
        }

        add('name', data.title);
        add('artist_credit.names.0.name', data.artist);
        add('artist_credit.names.0.artist.name', data.artist);

        add('events.0.date.year', data.date.year);
        add('events.0.date.month', data.date.month);
        add('events.0.date.day', data.date.day);
        add('events.0.country', 'JP');

        add('status', 'official');
        add('labels.0.name', data.label);
        add('labels.0.catalog_number', data.catalogNumber);
        if (data.barcode) add('barcode', data.barcode);

        // メディアごとにトラック追加
        data.media.forEach((medium, mi) => {
            add(`mediums.${mi}.format`, medium.format);
            medium.tracks.forEach((t, i) => {
                add(`mediums.${mi}.track.${i}.name`, t.title);
                add(`mediums.${mi}.track.${i}.artist_credit.names.0.name`, data.artist);
                if (t.lengthMs) add(`mediums.${mi}.track.${i}.length`, t.lengthMs);
            });
        });

        add('urls.0.url', location.href);
        add('urls.0.link_type', 79);

        add('edit_note', `Imported from TOWER RECORDS ONLINE (${location.href}).`);

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

    function createButton(data) {
        const btn = document.createElement('button');
        btn.textContent = 'Import to MB';
        Object.assign(btn.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            zIndex: '99999',
            padding: '10px 14px',
            background: '#ff6600',
            color: '#fff',
            border: 'none',
            borderRadius: '6px',
            cursor: 'pointer',
            fontWeight: 'bold'
        });

        btn.onclick = () => buildForm(data);
        document.body.appendChild(btn);
    }

    function init() {
        const json = getJSONLD();
        const data = {
            title: json?.name || text(document.querySelector('h1')),
            artist: getArtist(json),
            date: parseDate(json?.releaseDate),
            label: json?.brand?.name || '',
            catalogNumber: getCatalogNumber(),
            barcode: json?.sku || '',
            media: getMediaList()
        };

        createButton(data);
    }

    // Load + 2.5秒遅延で確実にDOM取得
    window.addEventListener('load', () => setTimeout(init, 2500));
})();

Currently supports:

Multiple media parsing
Track lengths
CD/DVD/Blu-ray/書籍/グッズ format normalization

5 Likes

Great, I will try this! :slight_smile:
Thank you @yakumo0209.

However, the nature of userscripts is that it must be often updated to follow website changes.

If you would host your userscript somewhere, it would automatically update for all users, each time you update it and increase its version number.

Here are a few possible places to host it (most simples are GitLab Snippets and GitHub Gists, I guess):

Alphabetical order.
Depending where you already have an account.


At the moment, the Import to MB button is partially hidden by the Language button:

But clicking it works great! :sparkling_heart:

1 Like

I uploaded on GitHub.

2 Likes