// ==UserScript== // @name MusicBrainz Magic Tagger Button // @description Automatically enable the green tagger button on MusicBrainz.org depending on whether Picard is running. // @version 0.7.9 // @author Philipp Wolfer // @namespace https://uploadedlobster.com // @license MIT // @icon https://raw.githubusercontent.com/phw/musicbrainz-magic-tagger-button/main/resources/mblookup-tagger-default.png // @match https://*.musicbrainz.org/cdtoc/* // @match https://*.musicbrainz.org/collection/* // @match https://*.musicbrainz.org/recording/* // @match https://*.musicbrainz.org/release-group/* // @match https://*.musicbrainz.org/release/* // @match https://*.musicbrainz.org/series/* // @match https://*.musicbrainz.org/taglookup // @match https://*.musicbrainz.org/search // @match https://*.musicbrainz.org/artist/*/releases // @exclude /^https://(.*\.)?musicbrainz.org/release/add/ // @exclude /^https://(.*\.)?musicbrainz.org/.*/create/ // @exclude /^https://(.*\.)?musicbrainz.org/.*/.*/edit/ // @exclude /^https://(.*\.)?musicbrainz.org/cdtoc/.*/.*/ // @exclude /^https://(.*\.)?musicbrainz.org/collection/.*/.*/ // @exclude /^https://(.*\.)?musicbrainz.org/release-group/.*/.*/ // @exclude /^https://(.*\.)?musicbrainz.org/series/.*/.*/ // @grant GM.xmlHttpRequest // @connect 127.0.0.1 // @inject-into content // @noframes // @homepageURL https://github.com/phw/musicbrainz-magic-tagger-button // @downloadURL https://raw.githubusercontent.com/phw/musicbrainz-magic-tagger-button/main/mb-magic-tagger-button.user.js // @updateURL https://raw.githubusercontent.com/phw/musicbrainz-magic-tagger-button/main/mb-magic-tagger-button.user.js // ==/UserScript== // You can change the default port below if you have running Picard on a // different port then the default 8000. const TAGGER_DEFAULT_PORT = 8000 // As Picard can end up running on a higher port if the configured port is in // use, this script probes a range of ports up to TAGGER_MAX_PORT. You can // lower this value or even set it to TAGGER_DEFAULT_PORT to reduce the amount // of probing requests. const TAGGER_MAX_PORT = 8010 // Usually Picard will listen on your local device only. You might want to // set this to a different IP address in your local network if Picard is // running on a different computer then your browser. const TAGGER_HOST = '127.0.0.1' // MusicBrainz Magic Tagger Button - Copyright (c) 2021-2022 Philipp Wolfer // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. const TAGGER_ERROR_MESSAGE = 'Loading this release or recording into MusicBrainz Picard failed.\nPlease make sure Picard is running and the browser integration is activated.' const TAGGER_ICON_SUCCESS = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAANCAMAAAADg7fkAAAAUVBMVEUAAAAzMzM/OzxDQ0NMSUpVVlhaV1hmZmZ2c3R7fHWEhX2Nj4WXmIyZmZmfoaShopOqq5isq6uvmv+zs566ubnDw6fMy6zMzMzW1dXj4+P///9p+Ql3AAAAhElEQVQoz42RUQ+CMAyEe0NBx+aGYNX9/x9qRyFkL9h7uDTdl+aWo1I1Ofov4T6XLrB/nquSEZ7ZQt4ws4lcMId+XEnSzWa0G20rlxMe13En62trOggJ9rHEoSXXUzodpEsZEcFwU3JmHzSnJjsOtznLhGz7uxSE+8tGlm8PW0eit6H3H3qWGo9r6lL+AAAAAElFTkSuQmCC' const TAGGER_ICON_ERROR = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAANCAMAAAADg7fkAAAAUVBMVEUAAAAzMzM/OzxDQ0NMSUpVVlhaV1hmZmZ2c3R7fHWEhX2Nj4WXmIyZmZmfoaShopOqq5isq6uzs566ubnDw6fMy6zMzMzW1dXj4+P/Zmb///8r7DMeAAAAh0lEQVQoz42RyQ6DMAxEM0BpCUkJi2mT///QJpgIq4fWI3k5PI3GsklFc2P+K3OvrnVk428V0sMSacgHFlKRKxbXjyeZ0xzFLV5bmU2Y8LyNJ8llvttBgqxP/l49o/Bk8vKcAjyc9BRehreaM1gncwqu4nz7jKC7PT8Iw6Yj07uH7kdZu+LvH+KRIrjjFXuTAAAAAElFTkSuQmCC' function logger (level, ...args) { if (!console) { return } let func = console.log if (level && typeof console[level] === 'function') { func = console[level] } func('[Magic Tagger Button]', ...args) } const debug = (...args) => logger('debug', ...args) const log = (...args) => logger('log', ...args) const warn = (...args) => logger('warn', ...args) const error = (...args) => logger('error', ...args) let xmlHttpRequest if (typeof (GM) !== 'undefined' && GM.xmlHttpRequest) { debug('Using GM.xmlHttpRequest') xmlHttpRequest = GM.xmlHttpRequest } else { debug('Using XMLHttpRequest') xmlHttpRequest = function xmlHttpRequest (details) { const xhr = new XMLHttpRequest() xhr.timeout = details.timeout || 0 xhr.open(details.method, details.url) xhr.onload = details.onload.bind(null, xhr) xhr.onerror = details.onerror.bind(null, xhr) xhr.send() } } function makeRequest (method, url) { return new Promise((resolve, reject) => { const successHandler = (response) => { resolve({ method: method, url: url, status: response.status, statusText: response.statusText, responseText: response.responseText, }) } const errorHandler = (response) => { const msg = `Request failed ${method} ${url}: ${response.status} ${response.statusText}` reject(new Error(msg)) } xmlHttpRequest({ method: method, url: url, timeout: 200, onload: successHandler, onerror: errorHandler, ontimeout: errorHandler, }) }) } async function probeTagger (port) { try { const response = await makeRequest('GET', `http://${TAGGER_HOST}:${port}`) debug(response) const text = response.responseText || '' return text.match(/MusicBrainz-Picard/) || text.match(/Nothing to see here/) } catch (reason) { debug(reason) return false } } async function detectTaggerPort () { for (let port = TAGGER_DEFAULT_PORT; port <= TAGGER_MAX_PORT; port++) { debug(`Probing port ${port}`) if (await probeTagger(port)) { return port } } } function allTaggerButtons () { return document.querySelectorAll('a.tagger-icon') } function findTaggerButton () { const button = document.querySelector('a.tagger-icon') if (button && button.href) { const url = new URL(button.href) return { protocol: url.protocol, host: url.host, port: parseInt(url.port, 10) } } } function setTaggerButtonStatus (button, icon, title) { button.setAttribute('title', title) const img = button.getElementsByTagName('img')[0] img.setAttribute('src', icon) } function improveTaggerButtons () { const taggerButtons = allTaggerButtons() for (const button of taggerButtons) { const newButton = button.cloneNode(true) button.parentNode.replaceChild(newButton, button) newButton.addEventListener('click', async (event) => { event.preventDefault() try { const url = new URL(newButton.href) url.host = TAGGER_HOST debug('Tagger button clicked', url) const response = await makeRequest('GET', url) if (response.status >= 200 && response.status < 400) { debug('Tagger request successful', response.responseText) setTaggerButtonStatus(newButton, TAGGER_ICON_SUCCESS, response.responseText) } else { error('Tagger request was answered with an error', response) setTaggerButtonStatus(newButton, TAGGER_ICON_ERROR, TAGGER_ERROR_MESSAGE) } } catch (reason) { error('Tagger request error', reason) setTaggerButtonStatus(newButton, TAGGER_ICON_ERROR, TAGGER_ERROR_MESSAGE) } }) } } function hideAllTaggerButtons () { const taggerButtons = allTaggerButtons() for (const button of taggerButtons) { button.style.display = 'none' } } function findCurrentlyUsedTaggerPort () { const url = new URL(document.location.href) const tport = parseInt(url.searchParams.get('tport'), 10) if (tport) { return tport } const taggerInfo = findTaggerButton() if (taggerInfo) { return taggerInfo.port } } function getCurrentUrlWithPort (port) { const url = new URL(document.location.href) url.searchParams.set('tport', port) return url } function reloadWithTaggerPort (port) { document.location.href = getCurrentUrlWithPort(port) } async function disableTaggerButtons () { hideAllTaggerButtons() // Perform a request in the background to clear the tport from session const url = getCurrentUrlWithPort(0) url.pathname = '/' try { await makeRequest('GET', url) } catch (reason) { warn(reason) } } function checkCurrentPageExcluded () { const url = new URL(document.location.href) // Special handling for search pages if (url.pathname === '/search' && !['release', 'recording'].includes(url.searchParams.get('type'))) { debug(`No tagger buttons on ${url.searchParams.get('type')} search page.`) return true } return false } async function run () { if (checkCurrentPageExcluded()) { return } log('Initializing MusicBrainz Magic Tagger Button!') const currentPort = findCurrentlyUsedTaggerPort() if (currentPort) { improveTaggerButtons() if (await probeTagger(currentPort)) { log(`Tagger button configured for port ${currentPort}.`) return } } const taggerPort = await detectTaggerPort() if (taggerPort) { log(`Found Picard listening on port ${taggerPort}.`) if (currentPort !== taggerPort) { log(`Reloading to activate tagger button on port ${taggerPort}...`) reloadWithTaggerPort(taggerPort) } else { debug('Tagger button already active') } } else { log('Could not find Picard listening for tagger button') if (currentPort) { debug('Disable tagger buttons') await disableTaggerButtons() } } } window.addEventListener('load', run, false)