[GMT] Extract artists from description & transfer artists between pages

Tries to extract artists from selected text or tracklist in group description, easy batch transfer of artist between different pages

目前為 2021-08-17 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         [GMT] Extract artists from description & transfer artists between pages
// @namespace    https://greasyfork.org/cs/users/321857-anakunda
// @run-at       document-end
// @version      1.44.1
// @description  Tries to extract artists from selected text or tracklist in group description, easy batch transfer of artist between different pages
// @author       Anakunda
// @copyright    2020-21, Anakunda (https://greasyfork.org/cs/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @match        https://*/torrents.php?id=*
// @match        https://*/torrents.php?*&id=*
// @match        https://*/upload.php*
// @match        https://*/requests.php?action=new*
// @match        https://*/requests.php?action=view&id=*
// @match        https://*/requests.php?action=edit&id=*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_getTab
// @grant        GM_saveTab
// @grant        GM_getTabs
// ==/UserScript==

'use strict';

const hasStyleSheet = styleSheet => document.styleSheetSets && document.styleSheetSets.contains(styleSheet);
const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);

const artistClasses = [
	'artist_main', 'artist_guest', 'artists_remix', 'artists_composers',
	'artists_conductors', 'artists_dj', 'artists_producer', 'artists_arranger',
];
const bcName = 'gazelle-artists', bcr = new BroadcastChannel(bcName), bcs = new BroadcastChannel(bcName);
let groupArtists, saveBtn, loadBtn, span;
function saveData(artists) {
	if (artists.length > 0) bcs.postMessage(artists.sort(function(a, b) {
		const c = (a[1] > 0 ? a[1] : Infinity) - (b[1] > 0 ? b[1] : Infinity);
		return c != 0 ? c : a[0].toLowerCase().localeCompare(b[0].toLowerCase());
	}));
	if (typeof GM_getTab == 'function' && typeof GM_saveTab == 'function') GM_getTab(function(tab) {
		if (artists.length > 0) {
			tab.artists = artists;
			tab.saveTimestamp = Date.now();
		} else delete tab.artists;
		GM_saveTab(tab);
	});
}
function setLoadData() {
	if (typeof GM_getTabs == 'function') GM_getTabs(function(tabs) {
		let artists = new Map;
		for (let tab in tabs) if (tabs[tab].saveTimestamp && tabs[tab].artists) artists.set(tabs[tab].saveTimestamp, tab);
		if (artists.size <= 0) return;
		loadBtn.artists = tabs[artists.get(Math.max(...artists.keys()))].artists;
		loadBtn.style.visibility = 'visible';
	});
	bcr.addEventListener('message', function(message) {
		console.assert(message instanceof MessageEvent && Array.isArray(message.data),
			'message instanceof MessageEvent && Array.isArray(message.data)');
		if (!(message instanceof MessageEvent) || !Array.isArray(message.data)) return false;
		loadBtn.artists = message.data;
		loadBtn.style.visibility = 'visible';
		//saveData(message.data);
	});
}
if ((groupArtists = document.body.querySelector('td#artistfields')) != null) {
	function addControls(root = document.body.querySelector('td#artistfields')) {
		if (!(root instanceof HTMLElement)
				|| (root = [':scope > a.brackets:last-of-type', ':scope > select[name="importance[]"]']
					.reduce((elem, selector) => elem || root.querySelector(selector), null)) == null) return;
		root.style.marginRight = '2em';

		if (!new URLSearchParams(document.location.search).has('groupid')) {
			loadBtn = document.createElement('A');
			loadBtn.id = 'load-gazelle-artists';
			loadBtn.textContent = 'Load';
			loadBtn.className = 'brackets';
			loadBtn.style.visibility = 'hidden';
			loadBtn.href = '#';
			loadBtn.onclick = function(evt) {
				if (!Array.isArray(evt.currentTarget.artists) || evt.currentTarget.artists.length <= 0) return;
				let artists = evt.currentTarget.artists.filter(artist => artist.length == 2), artistFields;
				while ((artistFields = document.body.querySelectorAll('input[name="artists[]"]')).length != artists.length)
					if (artistFields.length < artists.length) AddArtistField(); else RemoveArtistField();
				artists.forEach(function(artist, ndx) {
					artistFields[ndx].value = artist[0];
					artistFields[ndx].nextElementSibling.value = artist[1];
				});
				return false;
			};
			setLoadData();
			root.insertAdjacentElement('afterend', loadBtn);
		}

		saveBtn = document.createElement('A');
		saveBtn.id = 'save-gazelle-artists';
		saveBtn.textContent = 'Save';
		saveBtn.className = 'brackets';
		saveBtn.style = 'margin-right: 3px;';
		saveBtn.href = '#';
		saveBtn.onclick = function(evt) {
			evt.currentTarget.style.color = null;
			saveData(Array.from(document.body.querySelectorAll('input[name="artists[]"]'))
				.filter(artist => artist.value.trim().length > 0)
				.map(artist => [artist.value.trim(), artist.nextElementSibling.value]));
			evt.currentTarget.style.color = isDarkTheme ? 'lightgreen' : 'darkgreen';
			setTimeout(elem => { elem.style.color = null }, 1000, evt.currentTarget);
			return false;
		};
		root.insertAdjacentElement('afterend', saveBtn);
	}

	addControls(groupArtists);
	const categories = document.getElementById('categories'), dynamicForm = document.getElementById('dynamic_form');
	if (dynamicForm != null) new MutationObserver(function(mutationsList) {
		if (categories != null && !['0', 'Music'].includes(categories.value)) return;
		for (let mutation of mutationsList) for (let node of mutation.addedNodes)
			if (node.nodeName == 'TABLE' && node.classList.contains('layout')) return addControls();
	}).observe(dynamicForm, { childList: true });
	return;
}
if ((groupArtists = document.body.querySelector('div.box_artists > div.head')) != null) {
	loadBtn = groupArtists.querySelector('span.edit_artists');
	span = document.createElement('SPAN');
	span.style.float = 'right';
	if (loadBtn != null) span.style.marginRight = '1em';
	saveBtn = document.createElement('A');
	saveBtn.id = 'save-gazelle-artists';
	saveBtn.textContent = 'Save';
	saveBtn.className = 'brackets';
	saveBtn.href = '#';
	saveBtn.onclick = function(evt) {
		evt.currentTarget.style.color = null;
		const ac = artistClasses.concat(['artists_main', 'artists_guest']);
		saveData(Array.from(document.body.querySelectorAll('div.box_artists > ul > li > a:first-of-type'))
			.map(artist => [artist.textContent.trim(), ac.indexOf(artist.parentNode.className) % 8 + 1]));
		evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
		setTimeout(elem => { elem.style.color = null }, 1000, evt.currentTarget);
		return false;
	}
	span.append(saveBtn);
	groupArtists.append(span);
	if (loadBtn != null) {
		span = document.createElement('SPAN');
		span.style = 'float: right; margin-left: 3pt;';
		saveBtn = document.createElement('A');
		saveBtn.id = 'remove-all-artists';
		saveBtn.title = 'Kill \'Em All';
		saveBtn.textContent = 'X';
		saveBtn.className = 'brackets';
		saveBtn.style.color = 'red';
		saveBtn.href = '#';
		saveBtn.onclick = function(evt) {
			if (confirm('Kill \'Em All?'))
				for (let a of document.querySelectorAll('ul#artist_list > li > span.remove_artist > a')) a.click();
			return false;
		}
		span.append(saveBtn);
		groupArtists.insertBefore(span, loadBtn);
	}
}
if ((groupArtists = document.body.querySelector('div.box_addartists > div.head')) != null) {
	span = document.createElement('SPAN');
	span.style = 'float: right; margin-right: 1em;';
	loadBtn = document.createElement('A');
	loadBtn.id = 'load-gazelle-artists';
	loadBtn.textContent = 'Load';
	loadBtn.className = 'brackets';
	loadBtn.style.visibility = 'hidden';
	loadBtn.href = '#';
	loadBtn.onclick = function(evt) {
		let artists = evt.currentTarget.artists.filter(artist => artist.length == 2), artistFields;
		while ((artistFields = document.body.querySelectorAll('input[name="aliasname[]"]')).length < artists.length)
			AddArtistField();
		artistFields.forEach(function(elem, ndx) {
			elem.value = ndx < artists.length ? artists[ndx][0] : '';
			elem.nextElementSibling.value = ndx < artists.length ? artists[ndx][1] : 0;
		});
		return false;
	}
	setLoadData();
	span.append(loadBtn);
	groupArtists.append(span);

	span = document.createElement('SPAN');
	span.style = 'float: right; margin-right: 3px;';
	saveBtn = document.createElement('A');
	saveBtn.id = 'save-gazelle-artists';
	saveBtn.textContent = 'Save';
	saveBtn.className = 'brackets';
	saveBtn.href = '#';
	saveBtn.onclick = function(evt) {
		evt.currentTarget.style.color = null;
		saveData(Array.from(document.body.querySelectorAll('input[name="aliasname[]"]'))
			.filter(artist => artist.value.trim().length > 0)
			.map(artist => [artist.value.trim(), artist.nextElementSibling.value]));
		evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
		setTimeout(elem => { elem.style.color = null }, 1000, evt.currentTarget);
		return false;
	}
	span.append(saveBtn);
	groupArtists.append(span);
}

for (let selector of ['div.box_addartists', 'div.box_artists']) {
	if ((groupArtists = document.body.querySelector(selector)) == null) continue;
	groupArtists.ondragover = evt => false;
	groupArtists.ondragenter = evt => { evt.currentTarget.style.backgroundColor = 'lawngreen' };
	groupArtists[isFirefox ? 'ondragexit' : 'ondragleave'] = evt => { evt.currentTarget.style.backgroundColor = null };
	groupArtists.ondrop = function(evt) {
		evt.currentTarget.style.backgroundColor = null;
		if (evt.target.nodeName == 'INPUT' && evt.target.type == 'text') return true;
		showDialog(evt.dataTransfer.getData('text/plain').trim());
		return false;
	};
}

const addBox = document.body.querySelector('form.add_form[name="artists"]');
if (addBox == null) return;

Array.prototype.includesCaseless = function(str) {
	if (typeof str != 'string') return false;
	str = str.toLowerCase();
	return this.find(elem => typeof elem == 'string' && elem.toLowerCase() == str) != undefined;
};
Array.prototype.pushUnique = function(...items) {
	if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) });
	return this.length;
};
Array.prototype.pushUniqueCaseless = function(...items) {
	if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
	return this.length;
};

const siteApiTimeframeStorageKey = 'AJAX time frame', gazelleApiFrame = 10500;
let xhr = new XMLHttpRequest, modal = null, btnAdd = null, btnCustom = null,
		customCtrls = [ ], sel = null, ajaxRejects = 0;
let prefs = {
	set: function(prop, def) { this[prop] = GM_getValue(prop, def) }
};
let redacted_api_key = GM_getValue('redacted_api_key');
try { var siteArtistsCache = JSON.parse(sessionStorage.siteArtistsCache) } catch(e) { siteArtistsCache = [ ] }
try { var notSiteArtistsCache = JSON.parse(sessionStorage.notSiteArtistsCache) } catch(e) { notSiteArtistsCache = [ ] }

const styleSheet = `
.modal {
	position: fixed;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	background-color: rgba(0, 0, 0, 0.5);
	opacity: 0;
	visibility: hidden;
	transform: scale(1.1);
	transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
}

.modal-content {
	position: absolute;
	top: 50%;
	left: 50%;
	font-size: 17px;
	transform: translate(-50%, -50%);
	background-color: FloralWhite;
	color: black;
	width: 31rem;
	border-radius: 0.5rem;
	padding: 2rem 2rem 2rem 2rem;
	font-family: monospace;
}

.show-modal {
	opacity: 1;
	visibility: visible;
	transform: scale(1.0);
	transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
}

input[type="text"] { cursor: text; }
input[type="radio"] { cursor: pointer; }
.lbl { cursor: pointer; }

.tooltip {
	position: relative;
}

.tooltip .tooltiptext {
	visibility: hidden;
	width: 120px;
	background-color: #555;
	color: #fff;
	text-align: center;
	border-radius: 6px;
	padding: 5px 0;
	position: absolute;
	z-index: 1;
	bottom: 125%;
	left: 50%;
	margin-left: -60px;
	opacity: 0;
	transition: opacity 0.3s;
}

.tooltip .tooltiptext::after {
	position: absolute;
	top: 100%;
	left: 50%;
	margin-left: -5px;
	border-width: 5px;
	border-style: solid;
	border-color: #555 transparent transparent transparent;
}

.tooltip:hover .tooltiptext {
	visibility: visible;
	opacity: 1;
}

button.splitter {
	position: relative;
	width: 20pt;
	height: 20pt;
	text-align: center;
	font-weight: bold;
	font-size: 10pt;
	top: -1pt;
	background-color: darkolivegreen;
	color: white;
}
`;

btnAdd = document.createElement('input');
btnAdd.id = 'add-artists-from-selection';
btnAdd.value = 'Extract from selection';
btnAdd.onclick = add_from_selection;
btnAdd.type = 'button';
btnAdd.style.marginLeft = '5px';
btnAdd.style.visibility = 'hidden';
addBox.append(btnAdd);

let style = document.createElement('style');
document.head.appendChild(style);
style.id = 'artist-parser-form';
style.type = 'text/css';
style.innerHTML = styleSheet;
let el, elem = [ ];
elem.push(document.createElement('div'));
elem[elem.length - 1].className = 'modal';
elem[elem.length - 1].id = 'add-from-selection-form';
modal = elem[0];
elem.push(document.createElement('div'));
elem[elem.length - 1].className = 'modal-content';
elem.push(document.createElement('input'));
elem[elem.length - 1].id = 'btnFill';
elem[elem.length - 1].type = 'submit';
elem[elem.length - 1].value = 'Capture';
elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 30px;";
elem[elem.length - 1].onclick = doParse;
elem.push(document.createElement('input'));
elem[elem.length - 1].id = 'btnCancel';
elem[elem.length - 1].type = 'button';
elem[elem.length - 1].value = 'Cancel';
elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 65px;";
elem[elem.length - 1].onclick = closeModal;

let presetIndex = 0;
function addPreset(val, label = 'Custom', rx = null, order = [1, 2]) {
	elem.push(document.createElement('div'));
	el = document.createElement('input');
	elem[elem.length - 1].style.paddingBottom = '10px';
	el.id = 'parse-preset-' + val;
	el.name = 'parse-preset';
	el.value = val;
	if (val == 1) el.checked = true;
	el.type = 'radio';
	el.onchange = update_custom_ctrls;
	if (rx) {
		el.rx = rx;
		el.order = order;
	}
	if (val == 999) btnCustom = el;
	elem[elem.length - 1].appendChild(el);
	el = document.createElement('label');
	el.style.marginLeft = '10px';
	el.style.marginRight = '10px';
	el.htmlFor = 'parse-preset-' + val;
	el.className = 'lbl';
	el.innerHTML = label;
	elem[elem.length - 1].appendChild(el);
	if (val != 999) return;
	el = document.createElement('input');
	el.type = 'text';
	el.id = 'custom-pattern';
	el.style.width = '20rem';
	el.style.fontFamily = 'monospace';
	el.autoComplete = "on";
	addTooltip(el, 'RegExp to parse lines, first two captured groups are used');
	customCtrls.push(elem[elem.length - 1].appendChild(el));
	el = document.createElement('input');
	el.type = 'radio';
	el.name = 'parse-order';
	el.id = 'parse-order-1';
	el.value = 1;
	el.checked = true;
	el.style.marginLeft = '1rem';
	addTooltip(el, 'Captured regex groups assigned in order $1: artist(s), $2: assignment');
	customCtrls.push(elem[elem.length - 1].appendChild(el));
	el = document.createElement('label');
	el.htmlFor = 'parse-order-1';
	el.textContent = '→';
	el.style.marginLeft = '5px';
	elem[elem.length - 1].appendChild(el);
	el = document.createElement('input');
	el.type = 'radio';
	el.name = 'parse-order';
	el.id = 'parse-order-2';
	el.value = 2;
	el.style.marginLeft = '10px';
	addTooltip(el, 'Captured regex groups assigned in order $1: assignment, $2: artist(s)');
	customCtrls.push(elem[elem.length - 1].appendChild(el));
	el = document.createElement('label');
	el.htmlFor = 'parse-order-2';
	el.textContent = '←';
	el.style.marginLeft = '5px';
	elem[elem.length - 1].appendChild(el);
}
addPreset(++presetIndex, escapeHTML('<artist(s)>[ - <assignment>]'), /^\s*(.+?)(?:\:|\s+[\-\−\—\~\–]+\s+(.*?))?\s*$/);
addPreset(++presetIndex, escapeHTML('<artist>[, <assignment>]') +
	'<span style="font-family: initial;">&nbsp;&nbsp;<i>(HRA style)</i></span>', /^\s*(.+?)(?:\:|\s*,\s*(.*?))?\s*$/);
addPreset(++presetIndex, escapeHTML('<artist(s)>[: <assignment>]'), /^\s*(.+?)(?:\:|\s*:+\s*(.*?))?(?:\s*,)?\s*$/);
addPreset(++presetIndex, escapeHTML('<artist(s)>[ (<assignment>)]'), /^\s*(.+?)(?:\:|\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))?(?:\s*,)?\s*$/);
addPreset(++presetIndex, escapeHTML('<artist(s)>[ | <assignment>]'), /^\s*(.+?)(?:\s*\|\s*(.*?))?(?:\s*,)?\s*$/);
addPreset(++presetIndex, escapeHTML('[<assignment> - ]<artist(s)>'), /^\s*(?:(.*?)\s+[\-\−\—\~\–]+\s+)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
addPreset(++presetIndex, escapeHTML('[<assignment>: ]<artist(s)>'), /^\s*(?:(.*?)\s*:+\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
addPreset(++presetIndex, escapeHTML('[<assignment> | ]<artist(s)>'), /^\s*(?:(.*?)\s*\|\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
addPreset(++presetIndex, escapeHTML('<artist>[ / <assignment>]'), /^\s*(.+?)(?:\:|\s*\/+\s*(.*?))?(?:\s*,)?\s*$/);
addPreset(++presetIndex, escapeHTML('<artist>[; <assignment>]'), /^\s*(.+?)(?:\:|\s*;\s*(.*?))?(?:\s*,)?\s*$/);
addPreset(++presetIndex, escapeHTML('[<assignment> / ]<artist(s)>'), /^\s*(?:(.*?)\s*\/+\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
addPreset(++presetIndex, '<span style="font-family: initial;">From tracklist</span>',
	/^\s*((?:\d+|[A-Z](?:\d+)?)(?:[\-\.](?:\d+|[A-Za-z])|[A-Za-z])?)(?:\s*[\-\−\—\~\–\.\:]\s*|\s+)(.+?)(?:\s+(\((?:\d+:)?\d+:\d+\)|\[(?:\d+:)?\d+:\d+\]))?\s*$/, []);
addPreset(999);
elem.slice(2).forEach(k => { elem[1].appendChild(k) });
elem[0].appendChild(elem[1]);
document.body.appendChild(elem[0]);
window.addEventListener("click", windowOnClick);
document.addEventListener('selectionchange', function(evt) {
	let cs = window.getComputedStyle(modal);
	if (!btnAdd || window.getComputedStyle(modal).visibility != 'hidden') return;
	showHideAddbutton();
});

const vaParser = /^(?:Various(?:\s+Artists)?|Varios(?:\s+Artistas)?|V\/?A|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
const multiArtistParsers = [
	/\s*[\,\;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*(?:[Aa]nd|\&)\s+)?\s*/,
	/\s+(?:[\/\|\×]|meets)\s+/i,
];
const ampersandParsers = [
	/\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
	/\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
	/(?:\s*,)?\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
	/\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
];
const featArtistParsers = [
	///\s+(?:meets)\s+(.+?)\s*$/i,
	/* 0 */ /\s+(?:[Ww](?:ith|\.?\/)|[Aa]vec)\s+(?!his\b|her\b|Friends$|Strings$)(.+?)\s*$/,
	/* 1 */ /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff]eaturing\s+|(?:(?:[Ff]eat\.?|(?:[Ff]t|FT)\.))\s*|[Ff]\.?\/\s+)([^\(\)\[\]\{\}]+?)(?=\s*(?:[\(\[\{].*)?$)/,
	/* 2 */ /\s+\[\s*f(?:eat(?:\.?|uring)|t\.|\.?\/)\s+([^\[\]]+?)\s*\]/i,
	/* 3 */ /\s+\(\s*f(?:eat(?:\.?|uring)|t\.|\.?\/)\s+([^\(\)]+?)\s*\)/i,
	/* 4 */ /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i,
	/* 5 */ /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i,
	/* 6 */ /\s+\[\s*(?:with|[Ww]\.?\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/,
	/* 7 */ /\s+\(\s*(?:with|[Ww]\.?\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/,
];
const remixParsers = [
	/\s+\((?:The\s+)?Remix(?:e[sd])?\)/i,
	/\s+\[(?:The\s+)?Remix(?:e[sd])?\]/i,
	/\s+(?:The\s+)?Remix(?:e[sd])?\s*$/i,
	/^(?:The\s+)?(?:Remixes)\b|\b(?:The\s+)?(?:Remixes)$/,
	/\s+\(([^\(\)]+?)[\'\’\`]s[^\(\)]*\s(?:(?:Re)?Mix|Reworx)\)/i,
	/\s+\[([^\[\]]+?)[\'\’\`]s[^\[\]]*\s(?:(?:Re)?Mix|Reworx)\]/i,
	/\s+\(([^\(\)]+?)\s+(?:(?:Extended|Enhanced)\s+)?(?:Remix|Reworx)\)/i,
	/\s+\[([^\[\]]+?)\s+(?:(?:Extended|Enhanced)\s+)?(?:Remix|Reworx)\]/i,
	/\s+\(Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
	/\s+\[Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
	/(?:\s+[\-\−\—\–]|:)\s+(.+?)\s+(?:Remix)$/i,
];
const arrParsers = [
	/\s+\(arr(?:anged\s+by|\.)\s+([^\(\)]+?)\s*\)/i,
	/\s+\[arr(?:anged\s+by|\.)\s+([^\[\]]+?)\s*\]/i,
];
const prodParsers = [
	/\s+\(prod(?:uced(?:\s+by)?|\.\s+by)\s+([^\(\)]+?)\s*\)/i,
	/\s+\[prod(?:uced(?:\s+by)?|\.\s+by)\s+([^\[\]]+?)\s*\]/i,
];
const otherArtistsParsers = [
	[/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
	[/^()(.*?)\s+\(conductor\)$/i, 4],
	//[/^()(.*?)\s+\(.*\)$/i, 1],
];
const pseudoArtistParsers = [
	/* 0 */ vaParser,
	/* 1 */ /^(?:#??N[\/\-]?A|[JS]r\.?|Unknown(?:\s+Artist)?)$/i,
	/* 2 */ /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
	/* 3 */ /^(?:(Special\s+)??Guests?|Friends|(?:Studio\s+)?Orchestra)$/i,
	/* 4 */ /^(?:Various\s+Composers)$/i,
	/* 5 */ /^(?:[Aa]nonym)/,
	/* 6 */ /^(?:traditional|trad\.|lidová)$/i,
	/* 7 */ /\b(?:traditional|trad\.|lidová)$/,
	/* 8 */ /^(?:tradiční|lidová)\s+/,
	/* 9 */ /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
];
const trimRemixers = str => [
	/^(?:f(?:eat(?:\.?|uring)|t\.|\.?\/))\s+-\s+/i,
	/(?:(?:\s+|^)(?:Original|Extended|Enhanced|Radio|Dance|Club|Session|Raw|Vocal|Dub|Soulful|\d{4}))+$/i,
].reduce((r, rx) => r.replace(rx, ''), str.trim().consolidateWhitespace());

function showDialog(text) {
	if (!text) return;
	prefs.set('preset', 1);
	prefs.set('custom_pattern', '^\\s*(.+?)(?:\\s*:+\\s*(.*?)|\\:)?\\s*$');
	prefs.set('custom_pattern_order', 1);
	setRadiosValue('parse-preset', prefs.preset);
	customCtrls[0].value = prefs.custom_pattern;
	setRadiosValue('parse-order', prefs.custom_pattern_order);
	sel = text;
	update_custom_ctrls();
	modal.classList.add("show-modal");
}

function add_from_selection() {
	sel = document.getSelection();
	if (!sel.isCollapsed && modal != null) showDialog(sel.toString().trim());
}

function doParse(expr, flags = '') {
	closeModal();
	if (!sel) return;
	let preset = getSelectedRadio('parse-preset');
	if (preset == null) return;
	prefs.preset = preset.value;
	let order = preset.order,
			custom_parse_order = getSelectedRadio('parse-order'),
			rx = preset.rx;
	if (!rx && preset.value == 999 && custom_parse_order != null) {
		rx = new RegExp(customCtrls[0].value);
		order = custom_parse_order != null ?
			custom_parse_order.value == 1 ? [1, 2] : custom_parse_order.value == 2 ? [2, 1] : null : [1, 2];
	}
	groupArtists = artistClasses.map(category => Array.from(document.querySelectorAll(`ul#artist_list > li.${category} > a`)).map(a => a.textContent.trim()));
	cleanupArtistsForm();
	sel.split(/(?:\r?\n)+/).forEach(function(line) {
		line = line.trim().replace(/\s+\(tracks?\b[^\(\)]+\)/, '').replace(/\s+\[tracks?\b[^\[\]]+\]/, '');
		if (!line || /^\s*(?:Recorded|Mastered)\b/i.test(line)) return;
		let matches = /^\s*(?:Produced)[ \-\−\—\~\–]by (.+?)\s*$/.exec(line);
		if (matches != null) splitAmpersands(matches[1]).forEach(producer => { addArtist(producer, 7) });
		else if (rx instanceof RegExp && (matches = rx.exec(line)) != null) {
			if (!Array.isArray(order) || order.length < 2) {
				let title = matches[2];
				if (/^(.+?) [\-\−\—\–] /.test(title)) {
					let artist = RegExp.$1;
					if ((matches = featArtistParsers.slice(0, 2).reduce((m, rx) => m || rx.exec(artist), null)) != null) {
						splitAmpersands(artist.slice(0, matches.index)).forEach(artist => { addArtist(artist, 1) });
						splitAmpersands(matches[1]).forEach(artist => { addArtist(artist, 2) });
					} else splitAmpersands(artist).forEach(artist => { addArtist(artist, 1) });
				}
				if ((matches = featArtistParsers.slice(1).reduce((m, rx) => m || rx.exec(title), null)) != null)
					splitAmpersands(matches[1]).forEach(guest => { addArtist(guest, 2) });
				if ((matches = remixParsers.slice(4).reduce((m, rx) => m || rx.exec(title), null)) != null)
					splitAmpersands(trimRemixers(matches[1])).forEach(remixer => { addArtist(remixer, 3) });
				if ((matches = prodParsers.reduce((m, rx) => m || rx.exec(title), null)) != null)
					splitAmpersands(matches[1]).forEach(producer => { addArtist(producer, 7) });
// 				if ((matches = arrParsers.reduce((m, rx) => m || rx.exec(title), null)) != null)
// 					splitAmpersands(matches[1]).forEach(arranger => { addArtist(arranger, 8) });
			} else if (matches[order[0]]) {
				let role = deduceArtist(matches[order[1]]);
				splitAmpersands(matches[order[0]]).forEach(artist => { addArtist(artist, role) });
			} else splitAmpersands(matches[order[1]]).forEach(artist => { addArtist(artist, 2) });
		}
	});
	prefs.custom_pattern = customCtrls[0].value;
	prefs.custom_pattern_order = custom_parse_order != null ? custom_parse_order.value : 1;
	for (let i in prefs) { if (typeof prefs[i] != 'function') GM_setValue(i, prefs[i]) }
	if (siteArtistsCache.length > 0) sessionStorage.siteArtistsCache = JSON.stringify(siteArtistsCache);
	if (notSiteArtistsCache.length > 0) sessionStorage.notSiteArtistsCache = JSON.stringify(notSiteArtistsCache);

	function deduceArtist(str) {
		if (/\b(?:remix)/i.test(str)) return 3; // remixer
		if (/\b(?:composer|libretto|lyric\w*|written[ \-\−\—\~\–]by)\b/i.test(str)) return 4; // composer
		if (/\b(?:conduct|director\b|direction\b)/i.test(str)) return 5; // conductor
		if (/\b(?:compiler\b)/i.test(str)) return 6; // compiler
		if (/\b(?:producer\b|produced[ \-\−\—\~\–]by\b)/i.test(str)) return 7; // producer
		return 2;
	}

	function addArtist(name, type = 1) {
		if (!name || !(type > 0) || pseudoArtistParsers.some(rx => rx.test(name))) return false;
		// avoid dupes
		if (groupArtists[type - 1].includesCaseless(name)) return false;
		switch (type) {
			case 1: if ([4, 5].some(cat => groupArtists[cat].includesCaseless(name))) return false; break;
			case 2: if ([0, 4, 5].some(cat => groupArtists[cat].includesCaseless(name))) return false; break;
		}
		let input = assignFreeArtistField();
		if (input == null) throw 'could not allocate free artist slot';
		input.value = name;
		const importance = input.nextElementSibling;
		importance.value = type;
		groupArtists[type - 1].push(name);
		if (ampersandParsers.some(rx => rx.test(name))) {
			let button = document.createElement('button');
			button.className = 'splitter';
			button.textContent = '↔';
			button.onclick = function(evt) {
				let artists = [input.value];
				ampersandParsers.forEach(function(rx) {
					for (let index = artists.length - 1; index >= 0; --index) {
						artists.splice(index, 1, ...artists[index].split(rx));
					}
				});
				if (artists.length > 1) {
					const artistUsed = artist => artist && parseInt(importance.value) > 0
						&& groupArtists[parseInt(importance.value) - 1].includesCaseless(artist);
					input.value = !artistUsed(artists[0]) ? artists[0] : '';
					artists.slice(1).forEach(function(artist) {
						if (artistUsed(artist)) return;
						let input = assignFreeArtistField();
						if (input == null) throw 'could not allocate free artist slot';
						input.value = artist;
						input.nextElementSibling.value = importance.value;
					});
				}
				evt.target.remove();
			};
			input.insertAdjacentElement('afterend', button);
		}
		return true;
	}
}

function closeModal() {
	if (modal == null) return;
	showHideAddbutton();
	modal.classList.remove("show-modal");
}

function windowOnClick(event) {
	if (modal != null && event.target === modal) closeModal();
}

function update_custom_ctrls() {
	function en(elem) {
		if (elem == null || btnCustom == null) return;
		elem.disabled = !btnCustom.checked;
		elem.style.opacity = btnCustom.checked ? 1 : 0.5;
	}
	customCtrls.forEach(k => { en(k) });
}

function getSelectedRadio(name) {
	for (let i of document.getElementsByName(name)) { if (i.checked) return i }
	return null;
}

function setRadiosValue(name, val) {
	for (let i of document.getElementsByName(name)) { if (i.value == val) i.checked = true }
}

function showHideAddbutton() {
	//btnAdd.style.visibility = document.getSelection().type == 'Range' ? 'visible' : 'hidden';
	btnAdd.style.visibility = document.getSelection().isCollapsed ? 'hidden' : 'visible';
}

function escapeHTML(string) {
	let pre = document.createElement('pre'), text = document.createTextNode(string);
	pre.appendChild(text);
	return pre.innerHTML;
}

function cleanupArtistsForm() {
	document.querySelectorAll('div#AddArtists > input[name="aliasname[]"]').forEach(function(input) {
		input.value = '';
		input.nextElementSibling.value = 1;
	});
	document.querySelectorAll('div#AddArtists > button.splitter').forEach(button => { button.remove() });
}

function assignFreeArtistField() {
	function findFreeSlot() {
		for (let input of document.querySelectorAll('div#AddArtists > input[name="aliasname[]"]'))
			if (input.value.length <= 0) return input;
		return null;
	}
	return findFreeSlot() || (AddArtistField(), findFreeSlot());
}

function addTooltip(elem, text) {
	if (elem == null) return;
	elem.classList.add('tooltip');
	var tt = document.createElement('span');
	tt.className = 'tooltiptext';
	tt.textContent = text;
	elem.appendChild(tt);
}

function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
function looksLikeTrueName(artist, index = 0) {
	return twoOrMore(artist)
		&& (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
		&& artist.split(/\s+/).length >= 2
		&& !pseudoArtistParsers.some(rx => rx.test(artist)) || isSiteArtist(artist);
}

function strip(art, level = 0) {
	return typeof art == 'string' ? [
		/\s+(?:aka|AKA|Aka)\.?\s+(.*)$/g,
		/\s*\([^\(\)]+\)/g,
		/\s*\[[^\[\]]+\]/g,
		/\s*\{[^\{\}]+\}/g,
	].slice(level).reduce((acc, rx) => acc.replace(rx, ''), art) : undefined;
}

function isSiteArtist(artist) {
	if (!artist || notSiteArtistsCache.includesCaseless(artist)) return false;
	if (siteArtistsCache.includesCaseless(artist)) return true;
	let now = Date.now();
	try { var apiTimeFrame = JSON.parse(localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = { } }
	if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
		apiTimeFrame.timeStamp = now;
		apiTimeFrame.requestCounter = 1;
	} else ++apiTimeFrame.requestCounter;
	localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
	if (apiTimeFrame.requestCounter > 5) {
		if (groupArtists.some(art => art.includesCaseless(artist))) return true;
		console.debug('isSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
			artist + '" (' + apiTimeFrame.requestCounter + ')');
		++ajaxRejects;
		btnAdd.disabled = true;
		setTimeout(() => { btnAdd.disabled = false }, apiTimeFrame.timeStamp + gazelleApiFrame - now);
		return undefined;
	}
	try {
		let requestUrl = '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist);
		xhr.open('GET', requestUrl, false);
		if (document.location.hostname == 'redacted.ch' && redacted_api_key) xhr.setRequestHeader('Authorization', redacted_api_key);
		xhr.send();
		if (xhr.status == 404) {
			notSiteArtistsCache.push(artist);
			return false;
		}
		if (xhr.readyState != XMLHttpRequest.DONE || xhr.status < 200 || xhr.status >= 400) {
			console.warn('isSiteArtist("' + artist + '") error:', xhr, 'url:', document.location.origin + requestUrl);
			return undefined; // error
		}
		let response = JSON.parse(xhr.responseText);
		if (response.status != 'success') {
			notSiteArtistsCache.push(artist);
			return false;
		}
		if (!response.response) return false;
		siteArtistsCache.push(artist);
		return true;
	} catch(e) {
		console.error('isSiteArtist("' + artist + '"):', e, xhr);
		return undefined;
	}
}

function splitArtists(str, parsers = multiArtistParsers) {
	let result = [str];
	parsers.forEach(function(parser) {
		for (let i = result.length; i > 0; --i) {
			let j = result[i - 1].split(parser).map(strip);
			if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
					&& !isSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
		}
	});
	return result;
}

function splitAmpersands(artists) {
	if (!artists) return [ ];
	if (typeof artists == 'string') var result = splitArtists(strip(artists, 1));
		else if (Array.isArray(artists)) result = Array.from(artists); else return [];
	ampersandParsers.forEach(function(ampersandParser) {
		for (let i = result.length; i > 0; --i) {
			let j = result[i - 1].split(ampersandParser).map(strip);
			if (j.length <= 1 || isSiteArtist(result[i - 1]) || !j.every(looksLikeTrueName)) continue;
			result.splice(i - 1, 1, ...j.filter(function(artist) {
				return !result.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist));
			}));
		}
	});
	return result;
}