Chaban's userscripts and bookmarklet support thread

A while ago I started creating proper userscripts with the help of AI tools (for the curios, I use Gemini).
If you found bugs, have suggestions or don’t know how to use them you can now also ask in this thread.

A few bookmarklet’s and userscript’s use cases where already explained in the general and announcements thread

For reference, you can find my userscripts at greasyfork and bookmarklets in the MusicBrainz wiki

The most recently updated script is the Reports Statistics one where you can now toggle between all or only reports with subscriptions.

5 Likes

@karpuzikov

Hello, just found out that you making scripts.
I want to ask, is it possible to create a script that can perform a search during release adding/editing in the recording selection, allowing it to search recordings by ISRC?
btw what is your native language?

Even if it is possible (and I manage to pull it off) I’m not sure how feasible it would be given that the release editor is currently being converted to React by @Bitmap if I’m not mistaken and whether it would still work afterwards.

However I think being able to search for recordings by ISRCs in the release editor could be quite useful (until it is possible to seed ISRCs and suggest matches) and I’d suggest to file a ticket for it.

1 Like

For anyone using @kellnerd’s Harmony, you may not have noticed that one of chaban’s new scripts makes life much, much easier. I recommend:


Using @yomo12’s “Harmony Open All Links” script:


In conjunction with @chaban’s “Click buttons across tabs” script, to submit all the tabs:


Bonus: To update existing releases you can add @RustyNova’s “MusicBrainz: Go to Harmony” script:

1 Like

FYI, there is now a semi-official Harmony release actions page linker which supports some more MB pages in @kellnerd’s repo

Also, if you use my button clicker script it will probably conflict with @yomo12’s because they use the same broadcast channel right now. I guess I could change that or you could instead use the more versatile Linkclump Plus

1 Like

FYI @rinsuki made a new script MusicBrainz: Seed URLs to Release Recordings, once we have this, I made a new script Harmony Recordings Relationship Seeder.

Works Beautiful, no need to open/close multiple tabs.

3 Likes

Well… I’ll add that alistral can find those missing recordings links for you in the first place!

Just use alistral musicbrainz clippy [start recording mbid] -w missing_recording_link or just alistral musicbrainz clippy -w missing_recording_link to use your Listenbrainz’s listen history.

… I cannot guarantee it to not be addictive

Ooh, thanks @yomo12 and @rinsuki, your script combo works wonderfully as well!!

2 Likes

I just updated my “Click buttons across tabs” script to automatically click the URL seeding button provided by @rinsuki’s script. Also thanks to @yomo12 for implementing it so quickly for Harmony.

Here is another recommendation from my personal workflow. Get the Link Extractor extension to open multiple links from all selected tabs. That way you can open all relevant Harmony release actions links more efficiently and comfortably when importing lots of releases.

@yomo12, if you were to change your HarmonyRelationshipSeeder script to add actual links rather than buttons with click listeners it would also work with the above approach. (already adjusted it for myself)

My Link Extractor config and patterns

[
  "All Services",
  "x_seed.image.0.url=https%3A%2F%2Fmusic.apple.com",
  "magicisrc",
  "edit-artist.url"
]

It prioritizes Apple Music cover art for quality, if not present it needs to be manually handled.
The delay is important for the MB API rate limit, the website is more lenient. In my experience up to 50-100 tabs at once are possible.

2 Likes

Can you give me the changed code? I will update this for everyone.

Thanks

diff --git a/mnt/chromeos/MyFiles/Downloads/Harmony Recordings Relationship Seeder_1.4.txt b/mnt/chromeos/MyFiles/Downloads/Harmony Recordings Relationship Seeder_1.5.txt
index a65409c..addcdab 100644
--- a/mnt/chromeos/MyFiles/Downloads/Harmony Recordings Relationship Seeder_1.4.txt	
+++ b/mnt/chromeos/MyFiles/Downloads/Harmony Recordings Relationship Seeder_1.5.txt	
@@ -1,43 +1,59 @@
 // ==UserScript==
-// @name         Harmony Recordings Relationship Seeder
-// @namespace    http://tampermonkey.net/
-// @downloadURL  https://github.com/YoGo9/Scripts/raw/main/HarmonyRelationshipSeeder.user.js
-// @updateURL    https://github.com/YoGo9/Scripts/raw/main/HarmonyRelationshipSeeder.user.js
-// @version      1.4
-// @description  Generate MusicBrainz relationship seeder URLs from Harmony streaming links. Creates separate seeders for each streaming service.
-// @author       YoGo9
-// @match        https://harmony.pulsewidth.org.uk/release/actions*
-// @grant        none
-// @run-at       document-end
+// @name        Harmony Recordings Relationship Seeder
+// @namespace   https://musicbrainz.org/user/chaban
+// @downloadURL https://github.com/YoGo9/Scripts/raw/main/HarmonyRelationshipSeeder.user.js
+// @updateURL   https://github.com/YoGo9/Scripts/raw/main/HarmonyRelationshipSeeder.user.js
+// @version     1.5
+// @tag         ai-created
+// @description Generate MusicBrainz relationship seeder URLs from Harmony streaming links. Creates separate seeders for each streaming service.
+// @author      YoGo9
+// @license     MIT
+// @match       https://harmony.pulsewidth.org.uk/release/actions*
+// @grant       none
+// @run-at      document-end
 // ==/UserScript==
 
 (function() {
     'use strict';
 
-    // Wait for page to load
     if (document.readyState === 'loading') {
         document.addEventListener('DOMContentLoaded', init);
     } else {
         init();
     }
 
+    /**
+     * Initializes the script by checking for the presence of the first recording section
+     * and then creating the seeder buttons.
+     */
     function init() {
         const firstRecordingSection = document.querySelector('.message a[href*="edit-recording.url"]');
-        if (!firstRecordingSection) return;
+        if (!firstRecordingSection) {
+            return;
+        }
 
         createSeederButtons();
     }
 
+    /**
+     * Creates and appends the seeder buttons (now <a> elements) to the page.
+     * Each button allows generating a seeder URL for a specific streaming service
+     * or for all available services. The seeder URL is directly set as the href.
+     */
     function createSeederButtons() {
         const firstRecordingMessage = document.querySelector('.message:has(a[href*="edit-recording.url"])');
-        if (!firstRecordingMessage) return;
+        if (!firstRecordingMessage) {
+            return;
+        }
 
         const availableServices = getAvailableServices();
-        if (availableServices.length === 0) return;
+        if (availableServices.length === 0) {
+            return;
+        }
 
         const buttonContainer = document.createElement('div');
         buttonContainer.className = 'message';
-        
+
         let buttonsHtml = `
             <svg class="icon" width="24" height="24" stroke-width="2">
                 <use xlink:href="/icon-sprite.svg#link"></use>
@@ -46,49 +62,43 @@
                 <p><strong>Generate Relationship Seeders:</strong></p>
         `;
 
-        // Individual service buttons
         for (let service of availableServices) {
             const serviceInfo = getServiceInfo(service);
+            const seederUrl = generateSeederUrl(service);
             buttonsHtml += `
-                <button class="seeder-btn" data-service="${service}" style="background: ${serviceInfo.color}; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin: 2px 5px 2px 0;">
+                <a href="${seederUrl}" class="seeder-btn" data-service="${service}"
+                   style="background: ${serviceInfo.color}; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin: 2px 5px 2px 0; text-decoration: none; display: inline-block;">
                     ${serviceInfo.name}
-                </button>
+                </a>
             `;
         }
 
-        // All Services button
         if (availableServices.length > 1) {
+            const allServicesSeederUrl = generateAllServicesSeeder();
             buttonsHtml += `
                 <span style="margin: 0 10px; color: #666;">|</span>
-                <button class="seeder-btn-all" style="background: #28a745; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin: 2px 5px 2px 0; font-weight: bold;">
+                <a href="${allServicesSeederUrl}" class="seeder-btn-all"
+                   style="background: #28a745; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin: 2px 5px 2px 0; font-weight: bold; text-decoration: none; display: inline-block;">
                     All Services
-                </button>
+                </a>
             `;
         }
 
         buttonsHtml += `<p style="font-size: 12px; color: #666; margin-top: 5px;">Create seeder URLs for individual services or all at once</p></div>`;
-        
+
         buttonContainer.innerHTML = buttonsHtml;
         firstRecordingMessage.parentNode.insertBefore(buttonContainer, firstRecordingMessage);
-
-        // Add click handlers
-        buttonContainer.querySelectorAll('.seeder-btn').forEach(btn => {
-            btn.addEventListener('click', (e) => {
-                generateSeederUrl(e.target.getAttribute('data-service'));
-            });
-        });
-
-        const allServicesBtn = buttonContainer.querySelector('.seeder-btn-all');
-        if (allServicesBtn) {
-            allServicesBtn.addEventListener('click', generateAllServicesSeeder);
-        }
     }
 
+    /**
+     * Scans the page to identify available streaming services based on linked URLs.
+     * @returns {Array<string>} An array of unique service identifiers (e.g., 'spotify', 'deezer').
+     */
     function getAvailableServices() {
         const services = new Set();
         document.querySelectorAll('.message').forEach(message => {
             if (!message.textContent.includes('Link external IDs')) return;
-            
+
             const entityLinks = message.querySelector('.entity-links');
             if (!entityLinks) return;
 
@@ -100,6 +110,11 @@
         return Array.from(services);
     }
 
+    /**
+     * Provides display information (name and color) for known streaming services.
+     * @param {string} service - The internal identifier of the service (e.g., 'spotify').
+     * @returns {Object} An object containing the service's display name and a color code.
+     */
     function getServiceInfo(service) {
         const serviceMap = {
             'spotify': { name: 'Spotify', color: '#1DB954' },
@@ -112,6 +127,11 @@
         return serviceMap[service] || { name: service, color: '#007bff' };
     }
 
+    /**
+     * Determines the streaming service from a given URL.
+     * @param {string} url - The URL to check.
+     * @returns {string} The service identifier (e.g., 'spotify') or 'unknown' if not matched.
+     */
     function getServiceFromUrl(url) {
         if (url.includes('open.spotify.com/track/')) return 'spotify';
         if (url.includes('www.deezer.com/track/')) return 'deezer';
@@ -122,56 +142,71 @@
         return 'unknown';
     }
 
+    /**
+     * Generates a MusicBrainz relationship seeder URL for a specific target service.
+     * This function now *returns* the URL instead of opening a new window.
+     * @param {string} targetService - The service for which to generate the seeder (e.g., 'spotify').
+     * @returns {string|null} The generated seeder URL or null if an error occurs.
+     */
     function generateSeederUrl(targetService) {
         try {
             const recordings = extractRecordingDataForService(targetService);
             if (recordings.length === 0) {
-                alert(`No ${targetService} URLs found for recordings on this page`);
-                return;
+                console.warn(`No ${targetService} URLs found for recordings on this page`);
+                return null;
             }
 
             const releaseMbid = extractReleaseMbid();
             if (!releaseMbid) {
-                alert('Could not find MusicBrainz release ID');
-                return;
+                console.error('Could not find MusicBrainz release ID');
+                return null;
             }
 
             const seederData = buildSeederData(releaseMbid, recordings, targetService);
             const seederUrl = buildSeederUrl(releaseMbid, seederData);
 
-            copyToClipboard(seederUrl);
-            window.open(seederUrl, '_blank');
+            return seederUrl;
         } catch (error) {
             console.error('Error generating seeder:', error);
-            alert('Error generating seeder URL: ' + error.message);
+            console.error('Error generating seeder URL: ' + error.message);
+            return null;
         }
     }
 
+    /**
+     * Generates a MusicBrainz relationship seeder URL for all available services.
+     * This function now *returns* the URL instead of opening a new window.
+     * @returns {string|null} The generated seeder URL or null if an error occurs.
+     */
     function generateAllServicesSeeder() {
         try {
             const recordings = extractAllRecordingData();
             if (recordings.length === 0) {
-                alert('No streaming URLs found for recordings on this page');
-                return;
+                console.warn('No streaming URLs found for recordings on this page');
+                return null;
             }
 
             const releaseMbid = extractReleaseMbid();
             if (!releaseMbid) {
-                alert('Could not find MusicBrainz release ID');
-                return;
+                console.error('Could not find MusicBrainz release ID');
+                return null;
             }
 
             const seederData = buildAllServicesSeederData(releaseMbid, recordings);
             const seederUrl = buildSeederUrl(releaseMbid, seederData);
 
-            copyToClipboard(seederUrl);
-            window.open(seederUrl, '_blank');
+            return seederUrl;
         } catch (error) {
             console.error('Error generating all services seeder:', error);
-            alert('Error generating seeder URL: ' + error.message);
+            console.error('Error generating seeder URL: ' + error.message);
+            return null;
         }
     }
 
+    /**
+     * Extracts the MusicBrainz Release MBID from a link on the page.
+     * @returns {string|null} The MBID string or null if not found.
+     */
     function extractReleaseMbid() {
         const mbLink = document.querySelector('a[href*="musicbrainz.org/release/"]');
         if (mbLink) {
@@ -181,9 +216,14 @@
         return null;
     }
 
+    /**
+     * Extracts recording data (MBID, URL, relationship types) for a specific service.
+     * @param {string} targetService - The service to filter URLs by.
+     * @returns {Array<Object>} An array of recording objects.
+     */
     function extractRecordingDataForService(targetService) {
         const recordings = [];
-        
+
         document.querySelectorAll('.message').forEach(message => {
             if (!message.textContent.includes('Link external IDs')) return;
 
@@ -209,9 +249,13 @@
         return recordings;
     }
 
+    /**
+     * Extracts all streaming URLs and their relationship types for each recording.
+     * @returns {Array<Object>} An array of recording objects, each containing an array of URLs.
+     */
     function extractAllRecordingData() {
         const recordings = [];
-        
+
         document.querySelectorAll('.message').forEach(message => {
             if (!message.textContent.includes('Link external IDs')) return;
 
@@ -236,13 +280,20 @@
         return recordings;
     }
 
+    /**
+     * Extracts a specific streaming URL and its relationship types for a given service
+     * from a set of entity links.
+     * @param {HTMLElement} entityLinks - The container element with external links.
+     * @param {string} targetService - The service to find the URL for.
+     * @returns {Object|null} An object containing the URL and its types, or null if not found.
+     */
     function extractUrlForService(entityLinks, targetService) {
         const links = entityLinks.querySelectorAll('a[href]');
 
         for (let link of links) {
             const url = link.href;
             const service = getServiceFromUrl(url);
-            
+
             if (service === targetService) {
                 const relationshipTypes = extractRelationshipTypesFromHarmony(entityLinks, url);
                 if (relationshipTypes.length > 0) {
@@ -256,6 +307,11 @@
         return null;
     }
 
+    /**
+     * Extracts all streaming URLs and their associated relationship types from a set of entity links.
+     * @param {HTMLElement} entityLinks - The container element with external links.
+     * @returns {Array<Object>} An array of URL objects, each with url, types, and service.
+     */
     function extractAllStreamingUrls(entityLinks) {
         const urls = [];
         const links = entityLinks.querySelectorAll('a[href]');
@@ -263,7 +319,7 @@
         for (let link of links) {
             const url = link.href;
             const service = getServiceFromUrl(url);
-            
+
             if (service !== 'unknown') {
                 const relationshipTypes = extractRelationshipTypesFromHarmony(entityLinks, url);
                 if (relationshipTypes.length > 0) {
@@ -278,22 +334,28 @@
         return urls;
     }
 
+    /**
+     * Extracts relationship types for a given URL by parsing the Harmony edit URL.
+     * @param {HTMLElement} entityLinks - The container element holding the entity links.
+     * @param {string} targetUrl - The specific URL to find relationship types for.
+     * @returns {Array<string>} An array of relationship type names.
+     */
     function extractRelationshipTypesFromHarmony(entityLinks, targetUrl) {
         const messageDiv = entityLinks.closest('.message');
         const editLink = messageDiv ? messageDiv.querySelector('a[href*="edit-recording.url"]') : null;
-        
+
         if (!editLink) return [];
 
         const editUrl = decodeURIComponent(editLink.href);
         const urlPattern = /edit-recording\.url\.(\d+)\.text=([^&]+)&edit-recording\.url\.\1\.link_type_id=(\d+)/g;
         const matches = [...editUrl.matchAll(urlPattern)];
-        
+
         const relationshipTypes = [];
-        
+
         for (let match of matches) {
             const [, index, encodedUrl, linkTypeId] = match;
             const decodedUrl = decodeURIComponent(encodedUrl);
-            
+
             if (decodedUrl === targetUrl) {
                 const relationshipType = getLinkTypeName(linkTypeId);
                 if (relationshipType) {
@@ -301,14 +363,19 @@
                 }
             }
         }
-        
+
         return relationshipTypes;
     }
 
+    /**
+     * Maps MusicBrainz link type IDs to human-readable names.
+     * @param {string} linkTypeId - The numeric ID of the link type.
+     * @returns {string|null} The name of the link type or null if not found.
+     */
     function getLinkTypeName(linkTypeId) {
         const linkTypeMap = {
             '254': 'purchase for download',
-            '255': 'download for free', 
+            '255': 'download for free',
             '268': 'free streaming',
             '979': 'streaming',
             '256': 'production',
@@ -323,17 +390,24 @@
         return linkTypeMap[linkTypeId] || null;
     }
 
+    /**
+     * Builds the JSON data payload for the MusicBrainz relationship seeder for a single service.
+     * @param {string} releaseMbid - The MusicBrainz ID of the release.
+     * @param {Array<Object>} recordings - An array of recording data objects.
+     * @param {string} service - The target streaming service.
+     * @returns {Object} The seeder data object.
+     */
     function buildSeederData(releaseMbid, recordings, service) {
         const serviceInfo = getServiceInfo(service);
         const harmonyUrl = window.location.href;
         const albumUrl = getAlbumUrlForService(service);
-        
+
         let note = `Release: https://musicbrainz.org/release/${releaseMbid}\n${serviceInfo.name} links from Harmony: ${harmonyUrl}`;
-        
+
         if (albumUrl) {
             note += `\n${serviceInfo.name} Album: ${albumUrl}`;
         }
-        
+
         const seederData = {
             note: note,
             version: 1,
@@ -350,12 +424,18 @@
         return seederData;
     }
 
+    /**
+     * Builds the JSON data payload for the MusicBrainz relationship seeder for all services.
+     * @param {string} releaseMbid - The MusicBrainz ID of the release.
+     * @param {Array<Object>} recordings - An array of recording data objects, each with multiple URLs.
+     * @returns {Object} The seeder data object.
+     */
     function buildAllServicesSeederData(releaseMbid, recordings) {
         const harmonyUrl = window.location.href;
         const availableServices = getAvailableServices();
-        
+
         let note = `Release: https://musicbrainz.org/release/${releaseMbid}\nAll services from Harmony: ${harmonyUrl}`;
-        
+
         for (let service of availableServices) {
             const albumUrl = getAlbumUrlForService(service);
             if (albumUrl) {
@@ -363,7 +443,7 @@
                 note += `\n${serviceInfo.name} Album: ${albumUrl}`;
             }
         }
-        
+
         const seederData = {
             note: note,
             version: 2,
@@ -380,56 +460,51 @@
         return seederData;
     }
 
+    /**
+     * Retrieves the album URL for a specific streaming service from the Harmony page.
+     * @param {string} service - The service identifier.
+     * @returns {string|null} The album URL or null if not found.
+     */
     function getAlbumUrlForService(service) {
         const providerMap = {
             'spotify': 'Spotify',
-            'deezer': 'Deezer', 
+            'deezer': 'Deezer',
             'itunes': 'iTunes',
             'tidal': 'Tidal',
             'bandcamp': 'Bandcamp',
             'beatport': 'Beatport'
         };
-        
+
         const providerName = providerMap[service];
         if (!providerName) return null;
-        
+
         const providerItem = document.querySelector(`li[data-provider="${providerName}"]`);
         if (!providerItem) return null;
-        
+
         const providerLink = providerItem.querySelector('a.provider-id');
         return providerLink ? providerLink.href : null;
     }
 
+    /**
+     * Extracts the MusicBrainz ID (MBID) from a given URL for a specific entity type.
+     * @param {string} url - The URL to parse.
+     * @param {string} entityType - The type of MusicBrainz entity (e.g., 'recording', 'release').
+     * @returns {string|null} The extracted MBID or null if not found.
+     */
     function extractMbidFromUrl(url, entityType) {
         const match = url.match(new RegExp(`musicbrainz\\.org\\/${entityType}\\/([a-f0-9-]+)`));
         return match ? match[1] : null;
     }
 
+    /**
+     * Constructs the full MusicBrainz relationship seeder URL.
+     * @param {string} releaseMbid - The MusicBrainz ID of the release.
+     * @param {Object} seederData - The JSON data payload for the seeder.
+     * @returns {string} The complete seeder URL.
+     */
     function buildSeederUrl(releaseMbid, seederData) {
         const encodedData = encodeURIComponent(JSON.stringify(seederData));
         return `https://musicbrainz.org/release/${releaseMbid}/edit-relationships#seed-urls-v1=${encodedData}`;
     }
 
-    function copyToClipboard(text) {
-        if (navigator.clipboard) {
-            navigator.clipboard.writeText(text).then(() => {
-                console.log('Seeder URL copied to clipboard');
-            }).catch(err => {
-                console.error('Failed to copy to clipboard:', err);
-            });
-        } else {
-            const textArea = document.createElement('textarea');
-            textArea.value = text;
-            document.body.appendChild(textArea);
-            textArea.select();
-            try {
-                document.execCommand('copy');
-                console.log('Seeder URL copied to clipboard (fallback)');
-            } catch (err) {
-                console.error('Failed to copy to clipboard:', err);
-            }
-            document.body.removeChild(textArea);
-        }
-    }
-
 })();

Thank you!

Updated the script.

Some small updates:

YouTube: MusicBrainz Importer

  • Prefetches data on navigation start for faster button updates so it shouldn’t blink anymore
  • Synchronizes the video checkboxes on the recording creation form. No more forgetting to mark the links as video (MBS-12287).

Click buttons across tabs
Due the lack of progress on the beta since April 2025 I’ve decided to implement error handling similar to my multi-tool bookmarklet. In other terms, it will reload the page when an error occurs and attempts to re-submit until it succeeds. (Very hacky, don’t look at the code :face_with_peeking_eye:)

2 Likes