// ==UserScript==
// @name Harmony: Enhancements
// @namespace https://musicbrainz.org/user/chaban
// @version 1.0.0
// @tag ai-created
// @description Adds some convenience features, various UI and behavior settings, as well as an improved language detection to Harmony.
// @author chaban
// @license MIT
// @match https://harmony.pulsewidth.org.uk/*
// @connect none
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_info
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_NAME = GM_info.script.name;
const TOOLTIP_DISPLAY_DURATION = 2000;
// --- CONFIGURATION ---
const SETTINGS_CONFIG = {
// Seeder Behavior
skipConfirmation: { key: 'enhancements.seeder.skipConfirmation', label: 'Skip MusicBrainz confirmation page when adding a new release', defaultValue: false, section: 'Seeder Behavior', type: 'checkbox' },
updateProperties: { key: 'enhancements.seeder.updateProperties', label: 'Include GTIN and packaging when updating an existing release', defaultValue: false, section: 'Seeder Behavior', type: 'checkbox' },
// UI Settings
hideDebugMessages: { key: 'enhancements.ui.hideDebugMessages', label: 'Hide debug messages on release pages', defaultValue: true, section: 'UI Settings', type: 'checkbox' },
hideReleaseInfo: { key: 'enhancements.ui.hideReleaseInfo', label: 'Hide Availability, Sources, and External Links sections', defaultValue: false, section: 'UI Settings', type: 'checkbox' },
// Convenience Features
clipboardRelookup: { key: 'enhancements.ui.clipboardRelookup', label: `Add 'Re-Lookup with MBID from Clipboard' button`, defaultValue: true, section: 'Convenience Features', type: 'checkbox' },
actionsRelookup: { key: 'enhancements.ui.actionsRelookup', label: 'Add "Re-Lookup" link on Release Actions page', defaultValue: true, section: 'Convenience Features', type: 'checkbox' },
copyTracklist: { key: 'enhancements.ui.copyTracklist', label: `Enable copying tracklist by clicking "Track" header`, defaultValue: true, section: 'Convenience Features', type: 'checkbox' },
// Language Detection
languageDetection: { key: 'enhancements.lang.enabled', label: 'Enable title language detection', defaultValue: true, section: 'Language Detection', type: 'checkbox' },
confidenceThreshold: { key: 'enhancements.lang.confidenceThreshold', label: 'Confidence Threshold', defaultValue: 50, section: 'Language Detection', type: 'range' },
conflictThreshold: { key: 'enhancements.lang.conflictThreshold', label: 'Harmony Conflict Threshold', defaultValue: 90, section: 'Language Detection', type: 'range' },
detectSingles: { key: 'enhancements.lang.detectSingles', label: 'Analyze single-track releases', defaultValue: false, section: 'Language Detection', type: 'checkbox' },
ignoreHarmony: { key: 'enhancements.lang.ignoreHarmony', label: `Force overwrite Harmony's guess`, defaultValue: false, section: 'Language Detection', type: 'checkbox' },
stopWords: { key: 'enhancements.lang.stopWords', label: 'English Stop Words (one per line)', defaultValue: ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'bye', 'for', 'from', 'is', 'it', 'of', 'off', 'on', 'the', 'to', 'was', 'with'], section: 'Language Detection', type: 'textarea' },
techTerms: { key: 'enhancements.lang.techTerms', label: 'Technical Terms (one per line, regex supported)', defaultValue: ['live', 'remix(es)?', 'edit(ion)?', 'medley', 'mix', 'version(s)?', 'instrumental', 'album', 'radio', 'single', 'vocal', 'dub', 'club', 'extended', 'original', 'acoustic', 'unplugged', 'mono', 'stereo', 'demo', 'remaster(ed)?', 'f(ea)?t\\.?', 'sped up', 'slowed', 'chopped', 'screwed', '8d'], section: 'Language Detection', type: 'textarea' },
};
const ISO_639_1_TO_3_MAP = {'aa':'aar','ab':'abk','ae':'ave','af':'afr','ak':'aka','am':'amh','an':'arg','ar':'ara','as':'asm','av':'ava','ay':'aym','az':'aze','ba':'bak','be':'bel','bg':'bul','bi':'bis','bm':'bam','bn':'ben','bo':'bod','br':'bre','bs':'bos','ca':'cat','ce':'che','ch':'cha','co':'cos','cr':'cre','cs':'ces','cu':'chu','cv':'chv','cy':'cym','da':'dan','de':'deu','dv':'div','dz':'dzo','ee':'ewe','el':'ell','en':'eng','eo':'epo','es':'spa','et':'est','eu':'eus','fa':'fas','ff':'ful','fi':'fin','fj':'fij','fo':'fao','fr':'fra','fy':'fry','ga':'gle','gd':'gla','gl':'glg','gn':'grn','gu':'guj','gv':'glv','ha':'hau','he':'heb','hi':'hin','ho':'hmo','hr':'hrv','ht':'hat','hu':'hun','hy':'hye','hz':'her','ia':'ina','id':'ind','ie':'ile','ig':'ibo','ii':'iii','ik':'ipk','io':'ido','is':'isl','it':'ita','iu':'iku','ja':'jpn','jv':'jav','ka':'kat','kg':'kon','ki':'kik','kj':'kua','kk':'kaz','kl':'kal','km':'khm','kn':'kan','ko':'kor','kr':'kau','ks':'kas','ku':'kur','kv':'kom','kw':'cor','ky':'kir','la':'lat','lb':'ltz','lg':'lug','li':'lim','ln':'lin','lo':'lao','lt':'lit','lu':'lub','lv':'lav','mg':'mlg','mh':'mah','mi':'mri','mk':'mkd','ml':'mal','mn':'mon','mr':'mar','ms':'msa','mt':'mlt','my':'mya','na':'nau','nb':'nob','nd':'nde','ne':'nep','ng':'ndo','nl':'nld','nn':'nno','no':'nor','nr':'nbl','nv':'nav','ny':'nya','oc':'oci','oj':'oji','om':'orm','or':'ori','os':'oss','pa':'pan','pi':'pli','pl':'pol','ps':'pus','pt':'por','qu':'que','rm':'roh','rn':'run','ro':'ron','ru':'rus','rw':'kin','sa':'san','sc':'srd','sd':'snd','se':'sme','sg':'sag','si':'sin','sk':'slv','sl':'slv','sm':'smo','sn':'sna','so':'som','sq':'sqi','sr':'srp','ss':'ssw','st':'sot','su':'sun','sv':'swe','sw':'swa','ta':'tam','te':'tel','tg':'tgk','th':'tha','ti':'tir','tk':'tuk','tl':'tgl','tn':'tsn','to':'ton','tr':'tur','ts':'tso','tt':'tat','tw':'twi','ty':'tah','ug':'uig','uk':'ukr','ur':'urd','uz':'uzb','ve':'ven','vi':'vie','vo':'vol','wa':'wln','wo':'wol','xh':'xho','yi':'yid','yo':'yor','za':'zha','zh':'zho','zu':'zul'};
const getISO639_3_Code = (code) => ISO_639_1_TO_3_MAP[code] || null;
const langDetectState = {
detector: null,
apiFailed: false,
result: null,
};
// --- UTILITY FUNCTIONS ---
async function getSettings() {
const settings = {};
for (const config of Object.values(SETTINGS_CONFIG)) {
settings[config.key] = await GM_getValue(config.key, config.defaultValue);
}
return settings;
}
function showTooltip(message, type, event) {
const tooltip = document.createElement('div');
tooltip.textContent = message;
tooltip.style.cssText = `
position: fixed; background-color: ${type === 'success' ? '#4CAF50' : '#f44336'};
color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px;
z-index: 10001; opacity: 0; transition: opacity 0.3s;
pointer-events: none; white-space: nowrap;
`;
document.body.appendChild(tooltip);
tooltip.style.left = `${event.clientX - (tooltip.offsetWidth / 2)}px`;
tooltip.style.top = `${event.clientY - tooltip.offsetHeight - 10}px`;
setTimeout(() => { tooltip.style.opacity = '1'; }, 10);
setTimeout(() => {
tooltip.style.opacity = '0';
tooltip.addEventListener('transitionend', () => tooltip.remove());
}, TOOLTIP_DISPLAY_DURATION);
}
// --- SETTINGS PAGE ---
function initSettingsPage(settings) {
const main = document.querySelector('main');
if (!main || main.querySelector('.harmony-enhancements-settings-container')) return;
const sections = Object.values(SETTINGS_CONFIG).reduce((acc, config) => {
(acc[config.section] = acc[config.section] || []).push(config);
return acc;
}, {});
const container = document.createElement('div');
container.className = 'harmony-enhancements-settings-container';
container.style.marginTop = '2em';
for (const [name, configs] of Object.entries(sections)) {
const h3 = document.createElement('h3');
h3.textContent = name;
container.appendChild(h3);
configs.forEach(config => {
const wrap = document.createElement('div');
wrap.className = 'row';
const lbl = document.createElement('label');
lbl.htmlFor = config.key;
lbl.textContent = config.label;
lbl.style.cursor = 'pointer';
let input;
switch (config.type) {
case 'checkbox':
input = document.createElement('input');
input.type = 'checkbox';
input.checked = settings[config.key];
wrap.append(input, lbl);
break;
case 'range':
input = document.createElement('input');
input.type = 'range';
input.min = 0;
input.max = 100;
input.value = settings[config.key];
const val = document.createElement('span');
val.textContent = ` ${settings[config.key]}%`;
input.addEventListener('input', () => val.textContent = ` ${input.value}%`);
wrap.append(lbl, input, val);
break;
case 'textarea':
input = document.createElement('textarea');
input.rows = 5;
input.value = Array.isArray(settings[config.key]) ? settings[config.key].join('\n') : '';
input.style.width = '100%';
wrap.style.flexDirection = 'column';
wrap.style.alignItems = 'flex-start';
wrap.append(lbl, input);
break;
}
if (input) {
input.id = config.key;
const save = () => {
let value;
if (config.type === 'checkbox') value = input.checked;
else if (config.type === 'range') value = parseInt(input.value, 10);
else if (config.type === 'textarea') value = input.value.split('\n').map(s => s.trim()).filter(Boolean);
GM_setValue(config.key, value);
};
input.addEventListener('change', save);
if (config.type === 'range') input.addEventListener('input', save);
}
container.appendChild(wrap);
});
}
main.appendChild(container);
}
// --- ENHANCEMENT MODULES ---
const enhancements = {
skipConfirmation: (form, enabled) => {
if (!enabled || !form || form.action.includes('skip_confirmation=1')) return;
try {
const url = new URL(form.action);
url.searchParams.set('skip_confirmation', '1');
form.action = url.toString();
} catch (e) {
console.error(`${SCRIPT_NAME}: Could not parse form action URL.`, e);
}
},
updateProperties: (form, enabled) => {
if (!enabled || !form) return;
const append = (name, value) => {
if (!value) return;
let input = form.querySelector(`input[type="hidden"][name="${name}"]`);
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = name;
form.appendChild(input);
}
if (input.value !== value) input.value = value;
};
const gtin = Array.from(document.querySelectorAll('th')).find(th => th.textContent?.trim() === 'GTIN')?.nextElementSibling?.textContent?.trim();
append('barcode', gtin);
append('packaging', 'None');
},
toggleReleaseInfo: (enabled) => {
if (!enabled) return;
const terms = ['Availability', 'Sources', 'External links'];
document.querySelectorAll('.release-info > tbody > tr').forEach(row => {
const header = row.querySelector('th');
if (header && terms.includes(header.innerText.trim())) {
row.style.display = 'none';
}
});
},
addClipboardButton: (enabled) => {
if (!enabled || document.getElementById('redo-lookup-mbid-button')) return;
const lookupBtn = document.querySelector('input[type="submit"][value="Lookup"]');
const container = lookupBtn?.closest('.input-with-overlay');
if (!container) return;
const newBtn = document.createElement('input');
newBtn.type = 'submit';
newBtn.value = 'Re-Lookup with MBID from Clipboard';
newBtn.id = 'redo-lookup-mbid-button';
newBtn.className = lookupBtn.className;
container.parentElement.insertBefore(newBtn, container.nextSibling);
newBtn.addEventListener('click', async (e) => {
e.preventDefault();
try {
const text = await navigator.clipboard.readText();
const mbid = (text.match(/musicbrainz\.org\/release\/([a-f0-9\-]{36})/i) || [])[1];
if (mbid) {
const url = new URL(window.location.href);
url.searchParams.set('musicbrainz', mbid);
window.location.href = url.toString();
} else {
showTooltip('No MBID Found in Clipboard!', 'error', e);
}
} catch (err) {
const message = err.name === 'NotAllowedError' ? 'Clipboard permission denied!' : 'Could not read clipboard!';
showTooltip(message, 'error', e);
}
});
},
addActionsRelookupLink: (enabled) => {
if (!enabled || document.getElementById('relookup-link-container')) return;
const h2 = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.includes('Release Actions'));
if (!h2) return;
const mbid = new URLSearchParams(window.location.search).get('release_mbid');
if (!mbid) return;
const params = new URLSearchParams({ musicbrainz: mbid });
document.querySelectorAll('.provider-list li').forEach(item => {
const pName = item.getAttribute('data-provider')?.toLowerCase();
const pId = item.querySelector('.provider-id')?.textContent.trim();
if (pName && pId) params.set(pName, pId);
});
const url = `/release?${params.toString()}`;
const container = document.createElement('div');
container.id = 'relookup-link-container';
container.className = 'message';
container.innerHTML = `<svg class="icon" width="24" height="24" stroke-width="2"><use xlink:href="/icon-sprite.svg#brand-metabrainz"></use></svg><p><a href="${url}">Re-Lookup with Harmony</a></p>`;
h2.parentNode.insertBefore(container, h2.nextSibling);
},
makeTracklistCopyable: (header, enabled) => {
if (!enabled || !header || header.dataset.copyApplied) return;
header.dataset.copyApplied = 'true';
header.title = 'Click to copy this tracklist';
header.addEventListener('click', async (e) => {
const table = header.closest('table.tracklist');
if (!table) return;
const getCleanText = (element, selectorsToRemove) => {
if (!element) return '';
const clone = element.cloneNode(true);
clone.querySelectorAll(selectorsToRemove.join(',')).forEach(el => el.remove());
return clone.textContent.trim();
};
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
const requiredCols = ['Track', 'Title', 'Artists', 'Length'];
const colIndices = Object.fromEntries(requiredCols.map(name => [name, headers.indexOf(name)]));
if (Object.values(colIndices).some(i => i === -1)) {
showTooltip(`Required columns missing: ${requiredCols.join(', ')}`, 'error', e);
return;
}
const lines = Array.from(table.querySelectorAll('tbody tr')).map(row => {
const cells = row.children;
const num = cells[colIndices.Track]?.textContent.trim() || '';
const title = getCleanText(cells[colIndices.Title], ['ul.alt-values']);
const artist = getCleanText(cells[colIndices.Artists]?.querySelector('.artist-credit'), ['ul.alt-values']);
const len = (getCleanText(cells[colIndices.Length], ['ul.alt-values'])).split('.')[0];
return `${num}. ${title}${artist ? ` - ${artist}` : ''} (${len})`;
});
if (lines.length > 0) {
try {
await navigator.clipboard.writeText(lines.join('\n'));
showTooltip('Tracklist copied!', 'success', e);
header.style.color = 'green';
setTimeout(() => header.style.color = '', TOOLTIP_DISPLAY_DURATION);
} catch (err) {
showTooltip('Failed to copy tracklist!', 'error', e);
header.style.color = 'red';
setTimeout(() => header.style.color = '', TOOLTIP_DISPLAY_DURATION);
}
}
});
},
runLanguageDetection: async (settings) => {
if (langDetectState.result !== null) {
enhancements.applyLanguageDetectionResult(settings);
return;
}
if (!langDetectState.detector && !langDetectState.apiFailed) {
if ('LanguageDetector' in window) {
try {
const nativeDetector = await window.LanguageDetector.create();
langDetectState.detector = (text) => nativeDetector.detect(text);
} catch (error) {
console.error(`${SCRIPT_NAME} LanguageDetector API failed to initialize.`, error);
langDetectState.apiFailed = true;
}
} else {
langDetectState.apiFailed = true;
}
}
if (langDetectState.apiFailed) {
langDetectState.result = { skipped: true, debugInfo: { analyzedText: 'LanguageDetector API not available or failed to load.' } };
enhancements.applyLanguageDetectionResult(settings);
return;
}
const scriptElement = document.querySelector('script[id^="__FRSH_STATE_"]');
if (!scriptElement?.textContent) return;
const data = JSON.parse(scriptElement.textContent);
const releaseData = data.v?.flat().find(prop => prop?.release)?.release;
if (!releaseData) return;
const { title: releaseTitle, media } = releaseData;
const trackTitles = media?.flatMap(m => m.tracklist.map(t => t.title)) || [];
const trackCount = media?.reduce((sum, m) => sum + (m.tracklist?.length || 0), 0) || 0;
if (trackCount === 1 && !settings[SETTINGS_CONFIG.detectSingles.key]) {
langDetectState.result = { skipped: true, debugInfo: { analyzedText: 'Skipped: Single track release detection is disabled.' } };
enhancements.applyLanguageDetectionResult(settings);
return;
}
const allTitles = [releaseTitle, ...trackTitles].filter(Boolean);
if (allTitles.length === 0) return;
const techTerms = settings[SETTINGS_CONFIG.techTerms.key];
const stopWords = new Set(settings[SETTINGS_CONFIG.stopWords.key]);
const enclosedRegex = new RegExp(`\\s*(?:\\([^)]*\\b(${techTerms.join('|')})\\b[^)]*\\)|\\[[^\\]]*\\b(${techTerms.join('|')})\\b[^\\]]*\\])`, 'ig');
const trailingRegex = new RegExp(`\\s+[-–]\\s+.*(?:${techTerms.map(t => `\\b${t}\\b`).join('|')}).*`, 'ig');
const cleanedTitles = allTitles.map(title => title.replace(enclosedRegex, '').replace(trailingRegex, '').trim()).filter(Boolean);
const uniqueTitles = [...new Set(cleanedTitles)];
const textToAnalyze = uniqueTitles.join(' . ');
if (!textToAnalyze.replaceAll(/\P{Letter}/gu, '')) {
langDetectState.result = { languageName: '[No linguistic content]', confidence: 100, languageCode3: 'zxx', isZxx: true, debugInfo: { allResults: [], analyzedText: textToAnalyze } };
} else {
const results = await langDetectState.detector(textToAnalyze);
if (results.length === 0) return;
const final = results[0];
langDetectState.result = {
languageName: new Intl.DisplayNames(['en'], { type: 'language' }).of(final.detectedLanguage),
confidence: Math.round(final.confidence * 100),
languageCode3: getISO639_3_Code(final.detectedLanguage),
isZxx: false,
skipped: false,
debugInfo: { allResults: results, analyzedText: textToAnalyze }
};
}
enhancements.applyLanguageDetectionResult(settings);
},
applyLanguageDetectionResult: (settings) => {
if (!langDetectState.result) return;
const container = document.querySelector('div.release');
if (!container) return;
document.getElementById('harmony-enhancements-language-analysis')?.remove();
const { languageName, confidence, languageCode3, isZxx, skipped, debugInfo } = langDetectState.result;
const confidenceThreshold = settings[SETTINGS_CONFIG.confidenceThreshold.key];
const messageDiv = document.createElement('div');
messageDiv.id = 'harmony-enhancements-language-analysis';
messageDiv.className = 'message debug';
messageDiv.innerHTML = `<svg class="icon" width="24" height="24" stroke-width="2"><use xlink:href="/icon-sprite.svg#bug"></use></svg><div><p></p></div>`;
const p = messageDiv.querySelector('p');
if (skipped) {
p.textContent = debugInfo.analyzedText;
} else {
const b = document.createElement('b');
b.textContent = languageName;
p.append('Guessed language (LanguageDetector API): ', b, ` (${confidence}% confidence)`);
if (confidence < confidenceThreshold) {
const i = document.createElement('i');
i.textContent = ` - below ${confidenceThreshold}% threshold, no changes applied.`;
p.append(i);
}
p.append(document.createElement('br'), `Analyzed block: "${debugInfo.analyzedText}"`);
}
const findInsertionAnchor = () => {
const messages = container.querySelectorAll('.message.debug');
let langGuessMsg = null;
let scriptGuessMsg = null;
for (const msg of messages) {
const text = msg.textContent;
if (text.includes('Guessed language of the titles:')) langGuessMsg = msg;
else if (text.includes('Detected scripts of the titles:')) scriptGuessMsg = msg;
}
if (langGuessMsg) return langGuessMsg.nextSibling;
if (scriptGuessMsg) return scriptGuessMsg.nextSibling;
return container.querySelector('.message');
};
const insertionAnchor = findInsertionAnchor();
const parent = (insertionAnchor?.parentElement || container.querySelector('.message')?.parentElement) || container;
parent.insertBefore(messageDiv, insertionAnchor);
const updateSeeder = (code) => {
const form = document.querySelector('form[name="release-seeder"]');
if (!form) return;
let input = form.querySelector('input[name="language"]');
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = 'language';
form.appendChild(input);
}
if (input.value !== code) input.value = code;
};
const langRow = Array.from(document.querySelectorAll('.release-info th')).find(th => th.textContent.trim() === 'Language')?.parentElement;
if (isZxx) {
if (langRow) langRow.querySelector('td').textContent = '[No linguistic content]';
updateSeeder('zxx');
return;
}
if (skipped || confidence < confidenceThreshold) return;
const newContent = `${languageName} (${confidence}% confidence)`;
if (langRow) {
const cell = langRow.querySelector('td');
const originalText = cell.textContent.trim();
const originalLang = originalText.replace(/\s*\(.*\)/, '').trim();
const harmonyConfidence = parseInt((originalText.match(/\((\d+)%\sconfidence\)/) || [])[1] || '0', 10);
const shouldOverwrite = settings[SETTINGS_CONFIG.ignoreHarmony.key] || originalLang.toLowerCase() === languageName.toLowerCase() || harmonyConfidence < settings[SETTINGS_CONFIG.conflictThreshold.key];
if (shouldOverwrite && cell.textContent !== newContent) {
cell.textContent = newContent;
updateSeeder(languageCode3);
}
} else if (!document.getElementById('harmony-enhancements-language-row')) {
const table = document.querySelector('.release-info tbody');
const scriptRow = Array.from(table.querySelectorAll('th')).find(th => th.textContent.trim() === 'Script')?.parentElement;
const newRow = table.insertRow(scriptRow ? scriptRow.rowIndex + 1 : -1);
newRow.id = 'harmony-enhancements-language-row';
newRow.innerHTML = `<th>Language</th><td>${newContent}</td>`;
updateSeeder(languageCode3);
}
}
};
// --- INITIALIZATION AND ROUTING ---
function applyEnhancements(node, settings) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const applyToNodeAndDescendants = (selector, action) => {
if (node.matches(selector)) action(node);
node.querySelectorAll(selector).forEach(action);
};
applyToNodeAndDescendants('table.tracklist th', header => {
if (header.textContent.trim() === 'Track') {
enhancements.makeTracklistCopyable(header, settings[SETTINGS_CONFIG.copyTracklist.key]);
}
});
applyToNodeAndDescendants('input[type="submit"][value="Lookup"]', () => {
enhancements.addClipboardButton(settings[SETTINGS_CONFIG.clipboardRelookup.key]);
});
applyToNodeAndDescendants('h2', h2 => {
if (h2.textContent.includes('Release Actions')) {
enhancements.addActionsRelookupLink(settings[SETTINGS_CONFIG.actionsRelookup.key]);
}
});
applyToNodeAndDescendants('.release-info', () => {
enhancements.toggleReleaseInfo(settings[SETTINGS_CONFIG.hideReleaseInfo.key]);
});
applyToNodeAndDescendants('form[action^="https://musicbrainz.org/release/add"]', form => {
enhancements.skipConfirmation(form, settings[SETTINGS_CONFIG.skipConfirmation.key]);
});
applyToNodeAndDescendants('form[name="release-update-seeder"]', form => {
enhancements.updateProperties(form, settings[SETTINGS_CONFIG.updateProperties.key]);
});
applyToNodeAndDescendants('script[id^="__FRSH_STATE_"]', () => {
if (settings[SETTINGS_CONFIG.languageDetection.key]) {
enhancements.runLanguageDetection(settings);
}
});
}
function applyGlobalStyles(settings) {
const css = `
.release-artist::before { content: "by "; }
.release-artist > :first-child { margin-left: 0.25em; }
${settings[SETTINGS_CONFIG.hideDebugMessages.key] ? '.message.debug { display: none !important; }' : ''}
th[data-copy-applied] {
cursor: pointer; text-decoration: underline; color: #3B82F6;
-webkit-user-select: none; user-select: none; transition: color 0.2s;
}
th[data-copy-applied]:hover { color: #1D4ED8; }
`;
GM_addStyle(css);
}
async function main() {
const settings = await getSettings();
const path = window.location.pathname;
applyGlobalStyles(settings);
if (path.startsWith('/settings')) {
initSettingsPage(settings);
} else if (path.startsWith('/release')) {
const observer = new MutationObserver(() => {
observer.disconnect();
applyEnhancements(document.body, settings);
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
});
applyEnhancements(document.body, settings);
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
}
}
main().catch(e => console.error(`[${SCRIPT_NAME}]`, e));
})();