Supraphonline foobar2000 tagging support

Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Supraphonline foobar2000 tagging support
// @name:cs      Supraphonline podpora tagování ve foobar2000
// @name:en      Supraphonline foobar2000 tagging support
// @namespace    https://greasyfork.org/cs/users/321857-anakunda
// @version      1.49
// @description  Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000
// @description:cs  Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000
// @description:en  Copies album metadata to clipboard in machine parseable format
// @author       Anakunda
// @copyright    2024, Anakunda (https://openuserjs.org/users/Anakunda)
// @license      GPL-3.0-or-later
// @iconURL      https://www.supraphonline.cz/favicon.ico
// @match        https://*.supraphonline.cz/*
// @match        https://supraphonline.cz/*
// @match        http://*.supraphonline.cz/*
// @match        http://supraphonline.cz/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_info
// @grant        window.onurlchange
// @require      https://openuserjs.org/src/libs/Anakunda/Requests.min.js
// ==/UserScript==

// Výraz pro 'Automatically Fill Values' funkci ve foobaru2000:
//   %album artist%%album%%date%%releasedate%%genre%%label%%catalog%%discnumber%%totaldiscs%%discsubtitle%%tracknumber%%totaltracks%%artist%%title%%performer%%composer%%lyricist%%writer%%arranger%%media%%comment%%url%

'use strict';

Array.prototype.includesCaseless = function(str) {
	if (typeof str != 'string') return false;
	str = str.toLowerCase();
	return this.some(elem => typeof elem == 'string' && elem.toLowerCase() == str);
};
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;
};
Array.prototype.equalTo = function(arr) {
	return Array.isArray(arr) && arr.length == this.length
	&& Array.from(arr).sort().toString() == Array.from(this).sort().toString();
};
Array.prototype.equalCaselessTo = function(arr) {
	function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
	return Array.isArray(arr) && arr.length == this.length
	&& arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
};

function joinArtists(arr, decorator = artist => artist) {
	if (!Array.isArray(arr)) return null;
	if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
	if (arr.length < 3) return arr.map(decorator).join(' & ');
	return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
}

function timeStringToTime(str) {
	if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
	var t = 0, a = RegExp.$2.split(':');
	while (a.length > 0) t = t * 60 + parseFloat(a.shift());
	return RegExp.$1 ? -t : t;
}

function normalizeDate(str, countryCode = undefined) {
	if (typeof str != 'string') return null;
	var match;
	function formatOutput(yearIndex, montHindex, dayIndex) {
		var year = parseInt(match[yearIndex]), month = parseInt(match[montHindex]), day = parseInt(match[dayIndex]);
		if (year < 30) year += 2000; else if (year < 100) year += 1900;
		if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 0 || day > 31) return null;
		return year.toString() + '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
	}
	if ((match = /\b(\d{4})-(\d{1,2})-(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // US
	if ((match = /\b(\d{4})\/(\d{1,2})\/(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3);
	if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null
			&& (parseInt(match[1]) > 12 || /\b(?:be|it)/.test(countryCode))) return formatOutput(3, 2, 1); // BE, IT
	if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null) return formatOutput(3, 1, 2); // US
	if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // UK, IE, FR, ES
	if ((match = /\b(\d{1,2})-(\d{1,2})-((?:\d{2}|\d{4}))\b/.exec(str)) != null) return formatOutput(3, 2, 1); // NL
	if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // CZ, DE
	if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{2})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // AT, CH, DE, LU
	if ((match = /\b(\d{4})\. *(\d{1,2})\. *(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // JP
	return extractYear(str);
}

function extractYear(expr) {
	if (typeof expr == 'number') return Math.round(expr);
	if (typeof expr != 'string') return null;
	if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
	let d = new Date(expr);
	return parseInt(isNaN(d) ? expr : d.getUTCFullYear());
}

function durationFromMeta(elem) {
	if (!(elem instanceof HTMLElement)) return undefined;
	let meta = elem.querySelector('meta[itemprop="duration"][content]');
	if (meta == null) return undefined;
	let m = /^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.exec(meta.content);
	if (m != null) return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
	return (m = timeStringToTime(meta.content)) != null ? m : undefined;
}

let requestTimestamp, requestCounter = 0;
const fetchTrackDetails = url => new Promise((resolve, reject) => (function request() {
	if (!requestTimestamp || Date.now() >= requestTimestamp)
		[requestTimestamp, requestCounter] = [Date.now() + 10e3, 0];
	if (++requestCounter > 30) setTimeout(request, requestTimestamp - Date.now());
	else LocalXHR.get(url, { fatalErrors: [503] }).then(({document}) => { resolve(document) },
		reason => { /^HTTP error 503\b/.test(reason) ? setTimeout(request, 10e3) : reject(reason) });
})());

function fetchAlbum(evt) {
	const currentTarget = evt.currentTarget;
	if (currentTarget.disabled) return; else currentTarget.disabled = true;
	currentTarget.textContent = 'Pracuji...';
	let tracks = [ ], discNumber = 0, discSubtitle, ref, media, encoding, format, bitdepth,
			trackIdentifiers, releaseDate, samplerate, catalogue, imgUrl, album, totalTracks, albumYear,
			label, identifiers = { };
	const vaParser = /^(?:Various(?:\s+Artists)?|Varios(?:\s+Artistas)?|V\/?A|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
	const pseudoArtistParsers = [
		/^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
		/^(?:traditional|trad\.|lidová)$/i,
		/\b(?:traditional|trad\.|lidová)$/,
		/^(?:tradiční|lidová)\s+/,
		/^(?:[Aa]nonym)/,
		/^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
		/^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
		/^(?:Various\s+Composers)$/i,
		/^(?:Guests|Friends)$/i,
	];
	const VA = 'Various Artists';
	const artistClassParsers = [
		/*  0 */ [/^(?:Main\s?Artist)$/i],
		/*  1 */ [/^(?:Featured\s?Artist)$/i],
		/*  2 */ [/^(?:Remix)/i],
		/*  3 */ [/(?:^(?:Composer|music|music\simprovisation))$/i],
		/*  4 */ [/^(?:Conductor|(?:Chorus|Choir)\s?Master|Director|conducts|(?:conducted|directed)[\s\-]by)$/i],
		/*  5 */ [/^(?:DJ|Compiler|Compiled[\s\-]By|compiled[\s\-]by)$/],
		/*  6 */ [/^(?:Producer|produced[\s\-]by)$/i],
		/*  7 */ [/^(?:Artist|Soloist|Vocals|Ensemble|Orchestra|Choir)$/i],
		/*  8 */ [
			/\b(?:Recorded|Engineer|Producer|Mixer|Programming|Programmer|Arranger|Assistant|Translation)\b/i,
			/(?:PersonnelMastering)\b/i,
		],
		/*  9 */ [/^(?:Arranger|Arranged[\-\s]By)$/i],
		/* 10 */ [/(?:^(?:(?:Composer)?Lyricist|libreto)|\b(?:lyrics))$/i],
		/* 11 */ [/(?:^(?:Author|Writer|written[\s\-]by))$/i],
	];
	if (/\/album\/(\d+)\b/i.test(document.URL)) identifiers.SUPRAPHONLINE_ID = parseInt(RegExp.$1);
	let artist = Array.from(document.querySelectorAll('div.visible-lg-block > h2.album-artist > a'))
		.map(a => a.title || a.textContent.trim());
	let isVA = (ref = document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]')) != null ?
		vaParser.test(ref.content) : artist.length <= 0;
	if ((ref = document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
	if ((ref = document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
	let genres = (ref = document.querySelector('meta[itemprop="genre"]')) != null ? ref.content : undefined;
	if ((ref = document.querySelector('li.album-version > div.selected > div')) != null) {
		if (/\b(?:MP3)\b/.test(ref.textContent))
			[media, encoding, format] = ['Digital Media', 'lossy', 'MP3'];
		if (/\b(?:FLAC)\b/.test(ref.textContent))
			[media, encoding, format, bitdepth] = ['Digital Media', 'lossless', 'FLAC', 16];
		if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent))
			[media, encoding, format, bitdepth] = ['Digital Media', 'lossless', 'FLAC', 24];
		if (/\b(?:CD)\b/.test(ref.textContent)) media = 'CD';
		if (/\b(?:LP)\b/.test(ref.textContent)) media = 'Vinyl';
	}
	const copyrightParser = /^(?:\([CP]\)|©|℗)$/i;
	document.querySelectorAll('ul.summary > li').forEach(function(li) {
		if (li.childElementCount <= 0) return;
		let key = li.firstElementChild.textContent, value = li.lastChild.textContent.trim();
		if (key.includes('Nosič')) media = value;
		if (key.includes('Datum vydání')) releaseDate = normalizeDate(value, 'cs');
		if (key.includes('První vydání')) albumYear = extractYear(value);
		if (key.includes('Žánr')) genres = translateGenre(value);
		if (key.includes('Vydavatel')) label = value;
		if (key.includes('Katalogové číslo')) catalogue = value;
		if (key.includes('Formát')) {
			if (/\b(?:FLAC|WAV|AIFF?)\b/.test(value)) { encoding = 'lossless'; format = 'FLAC' }
			if (/\b(\d+)[\-\s]?bits?\b/i.test(value)) bitdepth = parseInt(RegExp.$1);
			if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(value)) samplerate = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
		}
		//if (key.includes('Celková stopáž')) totalTime = timeStringToTime(value);
		if (copyrightParser.test(key) && !albumYear) albumYear = extractYear(value);
	});
	const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
	let artists = [ ], ndx;
	for (let i = 0; i < creators.length; ++i) artists[i] = {};
	document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
		if ((ref = it.querySelector('h3')) != null) {
			ndx = undefined;
			creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
		} else {
			if (typeof ndx != 'number') return;
			if (ndx == 2) var role = 'ensemble';
			else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
			if ((ref = it.querySelector('a')) != null) {
				if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
				artists[ndx][role].pushUnique([ref.textContent.trim(), document.location.origin + ref.pathname]);
			}
		}
	});
	let description = Array.from(document.querySelectorAll('div[itemprop="description"] p'))
		.map(p => p.textContent.trim()).join('\n\n').replace(/\s+/g, ' ');
	let performers = [ ], composer = [ ], conductor = [ ], DJs = [ ], albumGuests = [ ], lyricist = [ ],
			writer = [ ], arranger = [ ], volMedia;
	for (let i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
		var a = artists[i][role].map(a => a[0]);
		([
			'conductor', 'choirmaster', 'director',
		].includes(role) ? conductor : role == 'DJ' ? DJs : [
			'FeaturedArtist',
		].includes(role) ? albumGuests : artist).pushUnique(...a);
	});
	Object.keys(artists[0]).forEach(function(role) { // composers
		composer.pushUnique(...artists[0][role].map(it => it[0])
			.filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
	});
	if ((ref = document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content.replace(/\?.*$/, '');
	const totalDiscs = document.querySelectorAll('table.table-tracklist > tbody > tr.cd-header').length;
	document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(tr, index) {
		if (tr.classList.contains('cd-header') && (ref = tr.querySelector('td > h3')) != null
				&& /\b(?:(\S*?)\s*)?(\d+)\b/.test(ref.textContent)) {
			volMedia = RegExp.$1 ? RegExp.lastMatch : undefined;
			++discNumber; // = parseInt(RegExp.$2) || undefined;
		}
		if (tr.classList.contains('song-header') && (ref = tr.querySelector('td')) != null)
			discSubtitle = ref.title || ref.textContent.trim();
		if (tr.classList.contains('track') && tr.id) {
			trackIdentifiers = { TRACK_ID: /^(?:track)-(\d+)$/i.exec(tr.id) };
			if (trackIdentifiers.TRACK_ID != null) trackIdentifiers.TRACK_ID = parseInt(trackIdentifiers.TRACK_ID[1]);
			else delete trackIdentifiers.TRACK_ID;
			if (volMedia) trackIdentifiers.VOL_MEDIA = volMedia;
			let track = {
				artist: isVA ? VA : undefined,
				artists: !isVA && artist.length > 0 ? artist : undefined,
				//featured_artists: albumGuests.length > 0 ? albumGuests : undefined,
				album: album,
				album_year: /*trackYear || */albumYear || undefined,
				release_date: releaseDate,
				label: label,
				catalog: catalogue,
				encoding: encoding,
				codec: format,
				bitdepth: bitdepth,
				samplerate: samplerate || undefined,
				media: media,
				genre: genres,
				disc_number: discNumber || 1,
				total_discs: totalDiscs > 0 ? totalDiscs : 1,
				disc_subtitle: discSubtitle,
				track_number: /^\s*(\d+)\.?\s*$/.test(tr.children[0].firstChild.textContent) ?
				parseInt(RegExp.$1) || RegExp.$1 : undefined,
				total_tracks: totalTracks,
				title: (ref = tr.querySelector('meta[itemprop="name"][content]')) != null ? ref.content
					: (ref = tr.querySelector('td > a.trackdetail')) != null ? ref.textContent.trim() : undefined,
				performers: performers.length > 0 ? performers : undefined,
				composers: composer.length > 0 ? composer : undefined,
				lyricists: lyricist.length > 0 ? lyricist : undefined,
				writers: writer.length > 0 ? writer : undefined,
				arrangers: arranger.length > 0 ? arranger : undefined,
				conductors: conductor.length > 0 ? conductor : undefined,
				compilers: DJs.length > 0 ? DJs : undefined,
				duration: durationFromMeta(tr),
				url: document.location.origin + document.location.pathname,
				description: description,
				identifiers: mergeIds(),
				cover_url: imgUrl,
			};
			tracks.push((function(track) {
				if ((ref = tr.querySelector('td > a.trackdetail')) == null) return Promise.reject('link not found');
				return fetchTrackDetails(ref.pathname + ref.search).then(function(document) {
					let detail = document.body.querySelector('div[data-swap="trackdetail-' + track.identifiers.TRACK_ID + '"] > div > div.row');
					if (detail == null) return Promise.reject('element not found');
					detail.querySelectorAll('div[class]:nth-of-type(1) > ul > li').forEach(function(li) {
						var key = li.querySelector('span'), value = li.lastChild;
						if (key == null || value.nodeType != Node.TEXT_NODE) return;
						key = key.textContent.trim(); value = value.wholeText.trim();
						if (!key || !value) return;
						if (key.startsWith('Žánr')) track.genre = value;
						if (key.startsWith('Nahrávka dokončena')) track.rec_year = extractYear(value);
						if (key.startsWith('Místo nahrání')) track.venue = value;
						if (key.startsWith('Rok prvního vydání')) track.pub_year = extractYear(value);
						if (copyrightParser.test(key)) track.copyright = value;
					});
					let trackArtists = [ ];
					for (let i = 0; i < 12; ++i) trackArtists[i] = [ ];
					detail.querySelectorAll('div[class]:nth-of-type(2) > ul > li').forEach(function(li) {
						var role = li.querySelector('span');
						var artists = Array.from(li.getElementsByTagName('a')).map(a => a.textContent.trim())
							.filter(artist => !pseudoArtistParsers.some(rx => rx.test(artist)));
						if (role != null && artists.length > 0) role = translateRole(role); else return;
						if (artistClassParsers[2].some(rx => rx.test(role)))
							trackArtists[2].pushUnique(...artists);
						else if (artistClassParsers[3].some(rx => rx.test(role)))
							trackArtists[3].pushUnique(...artists);
						else if (artistClassParsers[9].some(rx => rx.test(role)))
							trackArtists[9].pushUnique(...artists);
						else if (artistClassParsers[10].some(rx => rx.test(role)))
							trackArtists[10].pushUnique(...artists);
						else if (artistClassParsers[11].some(rx => rx.test(role)))
							trackArtists[11].pushUnique(...artists);
						else if (artistClassParsers[5].some(rx => rx.test(role)))
							trackArtists[5].pushUnique(...artists);
						else if (artistClassParsers[6].some(rx => rx.test(role)))
							trackArtists[6].pushUnique(...artists);
						else if (role.toLowerCase() == 'performer' || !artistClassParsers[8].some(rx => rx.test(role))) {
							if (artistClassParsers[0].some(rx => rx.test(role)))
								trackArtists[0].pushUnique(...artists);
							else if (artistClassParsers[1].some(rx => rx.test(role)))
								trackArtists[1].pushUnique(...artists);
							else if (artistClassParsers[4].some(rx => rx.test(role)))
								trackArtists[4].pushUnique(...artists);
							else artists.forEach(_artist => {
								if (artist.includesCaseless(_artist)) trackArtists[0].pushUnique(_artist);
								else if (artistClassParsers[7].some(rx => rx.test(role))) trackArtists[1].pushUnique(_artist);
							});
							trackArtists[7].pushUnique(...artists.map(artist => artist + ' (' + role + ')'));
						}
					});
					if (trackArtists[1].length > 0 && trackArtists[0].length <= 0) {
						trackArtists[0] = trackArtists[1]; trackArtists[1] = [];
					}
					if (trackArtists[0].length > 0 && (isVA || !trackArtists[0].equalCaselessTo(artist)
							|| trackArtists[1].length > 0/*!trackArtists[1].equalCaselessTo(albumGuests)*/)) {
						track.track_artists = trackArtists[0];
						if (trackArtists[1].length > 0) track.track_guests = trackArtists[1];
					}
					[
						[2, 'remixer'],
						[3, 'composer'],
						[4, 'conductor'],
						[5, 'compiler'],
						//[6, 'producer'],
						[7, 'performer'],
						[9, 'arranger'],
						[10, 'lyricist'],
						[11, 'writer'],
					].forEach(def => { if (trackArtists[def[0]].length > 0) track[def[1] + 's'] = trackArtists[def[0]] })
					return track;
				});
			})(track).catch(function(reason) {
				console.error('Supraphonline parser failed to get track', index + 1, 'detail:', reason);
				return Promise.resolve(track);
			}));
		} // track
	});
	Promise.all(tracks).then(tracks => tracks.map(track => {
		if (Array.isArray(track.track_artists) && track.track_artists.length > 0) {
			var trackArtist = joinArtists(track.track_artists);
			if (Array.isArray(track.track_guests) && track.track_guests.length > 0)
				trackArtist += ' feat. ' + joinArtists(track.track_guests);
		}
		return [
			isVA ? VA : joinArtists(track.artists) || '',
			track.album || '',
			track.album_year || track.pub_year || '',
			track.release_date || '',
			track.genre || '',
			track.label || '',
			track.catalog || '',
			track.disc_number || '',
			track.total_discs > 1 ? track.total_discs : '',
			track.disc_subtitle || '',
			track.track_number || '',
			tracks.filter(_track => _track.disc_number == track.disc_number).length || track.total_tracks || '',
			trackArtist || (Array.isArray(track.artists) && track.artists.length > 0 ?
				joinArtists(track.artists) : track.artist) || '',
			track.title || '',
			(track.performers || [ ]).join(', '),
			(track.composers || [ ]).join(', '),
			(track.lyricists || [ ]).join(', '),
			(track.writers || [ ]).join(', '),
			(track.arrangers || [ ]).join(', '),
			track.media,
			track.description,
			track.url,
		].join('\x1E');
	}).join('\n')).then(clipBoard => { GM_setClipboard(clipBoard, 'text') }).catch(e => { alert(e) }).then(function() {
		[currentTarget.disabled, currentTarget.textContent] = [false, '✔'];
		currentTarget.style.backgroundColor = 'green';
		setTimeout(function(button) {
			button.style.backgroundColor = null;
			button.textContent = button.dataset.originalText;
		}, 5000, currentTarget);
	});

	function translateGenre(genre) {
		if (!genre || typeof genre != 'string') return undefined;
		[
			['Orchestrální hudba', 'Orchestral Music'],
			['Komorní hudba', 'Chamber Music'],
			['Vokální', 'Classical, Vocal'],
			['Klasická hudba', 'Classical'],
			['Melodram', 'Classical, Melodram'],
			['Symfonie', 'Symphony'],
			['Vánoční hudba', 'Christmas Music'],
			[/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
			['Dechová hudba', 'Brass Music'],
			['Elektronika', 'Electronic'],
			['Folklor', 'Folclore, World Music'],
			['Instrumentální hudba', 'Instrumental'],
			['Latinské rytmy', 'Latin'],
			['Meditační hudba', 'Meditative'],
			['Vojenská hudba', 'Military Music'],
			['Pro děti', 'Children'],
			['Pro dospělé', 'Adult'],
			['Mluvené slovo', 'Spoken Word'],
			['Audiokniha', 'audiobook'],
			['Humor', 'humour'],
			['Pohádka', 'Fairy-Tale'],
		].forEach(function(subst) {
			if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
					|| subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
		});
		return genre;
	}

	function translateRole(elem) {
		return elem instanceof HTMLElement ? [
			[/\b(?:klavír)\b/ig, 'piano'],
			[/\b(?:housle)\b/ig, 'violin'],
			[/\b(?:violoncello)\b/ig, 'cello'],
			[/\b(?:viola)\b/ig, 'alto'],
			[/\b(?:varhany)\b/ig, 'organ'],
			[/\b(?:cembalo)\b/ig, 'harpsichord'],
			[/\b(?:trubka)\b/ig, 'trumpet'],
			[/\b(?:soprán)\b/ig, 'soprano'],
			[/\b(?:alt)\b/ig, 'alto'],
			[/\b(?:baryton)\b/ig, 'baritone'],
			[/\b(?:bas)\b/ig, 'basso'],
			[/\b(?:akordeon)\b/ig, 'accordion'],
			[/\b(?:syntezátor)\b/ig, 'synthesizer'],
			[/\b(?:klávesové nástroje)\b/ig, 'keyboards'],
			[/\b(?:bicí)\b/ig, 'drums'],
			[/\b(?:kontrabas)\b/ig, 'double-bass'],
			[/\b(?:zpěv|vokál)\b/ig, 'vocals'],
			[/\b(?:baskytara)\b/ig, 'bass guitar'],
			[/\b(?:havajská kytara)\b/ig, 'steel guitar'],
			[/\b(?:akustická kytara)\b/ig, 'acoustic guitar'],
			[/\b(?:kytara)\b/ig, 'guitar'],
			[/\b(?:kytary)\b/ig, 'guitars'],
			[/(?:čte|četba)\b/ig, 'narration'],
			[/\b(?:vypravuje)\b/ig, 'narration'],
			[/\b(?:hudební těleso)\b/ig, 'ensemble'],
			[/\b(?:Umělec)\b/ig, 'Artist'],
			[/\b(?:improvizace)\b/ig, 'improvisation'],
			['český', 'czech'],
			['původní', 'original'],
			[/\b(?:text)\b/ig, 'lyrics'],
			[/\b(?:hudba)\b/ig, 'music'],
			['hudební', 'music'],
			[/\b(?:autor)\b/ig, 'author'],
			[/\b(?:překlad)\b/ig, 'translation'],
			['účinkuje', 'participating'],
			['hovoří a zpívá', 'speaks and sings'],
			['hovoří', 'spoken by'],
			['komentář', 'commentary'],
			[/\b(?:dirigent)\b/ig, 'conductor'],
			['řídí', 'director'],
			[/\b(?:sbormistr)\b/ig, 'choirmaster'],
			['programování', 'programming'],
			[/\b(?:produkce)\b/ig, 'produced by'],
			['nahrál', 'recorded by'],
			[/\b(?:digitální přepis)\b/ig, 'A/D transfer'],
		].reduce((r, def) => r.replace(...def), elem.textContent.trim().replace(/\s*:.*$/, '')) : undefined;
	}

	function mergeIds() {
		const r = Object.assign({}, identifiers, trackIdentifiers);
		trackIdentifiers = {};
		return r;
	}
}

function addButton() {
	if (!document.location.pathname.startsWith('/album/')) return;
	const container = document.body.querySelector('form.table-action'), id = 'copy-info-to-clipboard';
	if (container == null || container.querySelector('button#' + id) != null) return;
	const button = Object.assign(document.createElement('button'), {
		id: id,
		type: 'button',
		className: 'btn btn-danger topframe_login',
		textContent: 'Kopírovat do schránky',
		style: 'margin-right: 10px;',
		onclick: fetchAlbum,
	});
	button.dataset.originalText = button.textContent;
	container.prepend(button);
};
addButton();
window.onurlchange = addButton;