Greasy Fork is available in English.

RED/NWCD Upload Assistant

Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection (via pasted output of copy command), release integrity check, two tracklist layouts, colours customization, featured artists extraction, classical works formatting, coverart fetching from store and more. As alternative to pasted playlist, e.g. for requests creation, valid URL to product page on supported web can be used -- see below for list of supported sites.

اعتبارا من 05-11-2019. شاهد أحدث إصدار.

// ==UserScript==
// @name         RED/NWCD Upload Assistant
// @namespace    https://greasyfork.org/users/321857-anakunda
// @version      1.178
// @description  Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection (via pasted output of copy command), release integrity check, two tracklist layouts, colours customization, featured artists extraction, classical works formatting, coverart fetching from store and more. As alternative to pasted playlist, e.g. for requests creation, valid URL to product page on supported web can be used -- see below for list of supported sites.
// @author       Anakunda
// @iconURL      https://redacted.ch/favicon.ico
// @match        https://redacted.ch/upload.php*
// @match        https://redacted.ch/torrents.php?action=editgroup*
// @match        https://redacted.ch/requests.php?action=new*
// @match        https://redacted.ch/requests.php?action=edit*
// @match        https://notwhat.cd/upload.php*
// @match        https://notwhat.cd/torrents.php?action=editgroup*
// @match        https://notwhat.cd/requests.php?action=new*
// @match        https://notwhat.cd/requests.php?action=edit*
// @match        https://orpheus.network/upload.php*
// @match        https://orpheus.network/torrents.php?action=editgroup*
// @match        https://orpheus.network/requests.php?action=new*
// @match        https://orpheus.network/requests.php?action=edit*
// @connect      file://*
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_log
// //@require      https://connect.soundcloud.com/sdk/sdk-3.3.2.js
// ==/UserScript==

// Additional setup: to work, set the pattern below as built-in foobar2000 copy command or custom Text Tools plugin quick copy command
//   [$fix_eol(%album artist%,)]$char(30)[$fix_eol(%album%,)]$char(30)[$if3(%date%,%ORIGINAL RELEASE DATE%,%year%)]$char(30)[$if3(%releasedate%,%retail date%,%date%,%year%)]$char(30)[$fix_eol($if3(%label%,%publisher%,%COPYRIGHT%),)]$char(30)[$fix_eol($if3(%catalog%,%CATALOGNUMBER%,%CATALOG NUMBER%,%labelno%,%catalog #%,%barcode%,%UPC%,%EAN%,%MCN%),)]$char(30)[%country%]$char(30)%__encoding%$char(30)%__codec%$char(30)[%__codec_profile%]$char(30)[%__bitrate%]$char(30)[%__bitspersample%]$char(30)[%__samplerate%]$char(30)[%__channels%]$char(30)[$if3(%media%,%format%,%source%,%MEDIATYPE%,%SOURCEMEDIA%,%discogs_format%)]$char(30)[$fix_eol(%genre%,)]|[$fix_eol(%style%,)]$char(30)[$num(%discnumber%,0)]$char(30)[$num($if2(%totaldiscs%,%disctotal%),0)]$char(30)[$fix_eol(%discsubtitle%,)]$char(30)[%track number%]$char(30)[$num($if2(%totaltracks%,%TRACKTOTAL%),0)]$char(30)[$fix_eol(%title%,)]$char(30)[$fix_eol(%track artist%,)]$char(30)[$if($strcmp($fix_eol(%performer%,),$fix_eol(%artist%,)),,$fix_eol(%performer%,))]$char(30)[$fix_eol($if3(%composer%,%writer%,%SONGWRITER%,%author%,%LYRICIST%),)]$char(30)[$fix_eol(%conductor%,)]$char(30)[$fix_eol(%remixer%,)]$char(30)[$fix_eol($if2(%compiler%,%mixer%),)]$char(30)[$fix_eol($if2(%producer%,%producedby%),)]$char(30)%length_seconds_fp%$char(30)%length_samples%$char(30)[%replaygain_album_gain%]$char(30)[%album dynamic range%]$char(30)[%__tool%][ | %ENCODER%][ | %ENCODER_OPTIONS%]$char(30)[$fix_eol($if2(%url%,%www%),)]$char(30)$directory_path(%path%)$char(30)[$replace($replace($if2(%comment%,%description%),$char(13),$char(29)),$char(10),$char(28))]$char(30)$trim([RELEASETYPE=$replace($if2(%RELEASETYPE%,%RELEASE TYPE%), ,_) ][COMPILATION=%compilation% ][ISRC=%isrc% ][EXPLICIT=%EXPLICIT% ][ORIGINALFORMAT=%ORIGINALFORMAT% ][ASIN=%ASIN% ][DISCOGS_ID=%discogs_release_id% ][SOURCEID=%SOURCEID% ][BPM=%BPM% ])
//
// List of supported domains for online capturing of release details:
//
// Music releases:
// - qobuz.com
// - highresaudio.com
// - bandcamp.com
// - prestomusic.com
// - discogs.com
// - supraphonline.cz
// - bontonland.cz (closing soon)
// - nativedsd.com
// - junodownload.com
// - hdtracks.com
// - deezer.com
// - spotify.com
//
// Ebooks releases:
// - martinus.cz, martinus.sk
// - goodreads.com
// - databazeknih.cz
//
// Application releases:
// - sanet.st

'use strict';

const isRED = document.domain.toLowerCase().endsWith('redacted.ch');
const isNWCD = document.domain.toLowerCase().endsWith('notwhat.cd');
const isOrpheus = document.domain.toLowerCase().endsWith('orpheus.network');

const isUpload = document.URL.toLowerCase().includes('/upload\.php');
const isEdit = document.URL.toLowerCase().includes('/torrents.php?action=editgroup');
const isRequestNew = document.URL.toLowerCase().includes('/requests.php?action=new');
const isRequestEdit = document.URL.toLowerCase().includes('/requests.php?action=edit');

var prefs = {
  set: function(prop, def) { this[prop] = GM_getValue(prop, def) },
  save: function() {
	for (var iter in this) {
	  if (typeof this[iter] != 'function' && this[iter] != undefined) GM_setValue(iter, this[iter]);
	}
  },
};
prefs.set('remap_texttools_newlines', 0); // convert underscores to linebreaks (ambiguous)
prefs.set('clean_on_apply', 0); // clean the input box on successfull fill
prefs.set('keep_meaningles_composers', 0); // keep composers from file tags also for non-composer emphasis works
prefs.set('always_hide_dnu_list', 0); // risky!
prefs.set('single_threshold', 8 * 60); // Max length of single in s
prefs.set('EP_threshold', 28 * 60); // Max time of EP in s
prefs.set('autfill_delay', 1000); // delay in ms to autofill for after pasting text into box, 0 to disable
prefs.set('auto_preview_cover', 1);
prefs.set('auto_rehost_cover', 1);
prefs.set('fetch_tags_from_artist', 0); // add n most used tags from release artist (if one) - experimental
prefs.set('always_request_perfect_flac', 0);
prefs.set('request_default_bounty', 0);
prefs.set('ptpimg_api_key');
prefs.set('spotify_clientid');
prefs.set('spotify_clientsecret');
prefs.set('soundcloud_clientid');
prefs.set('catbox_userhash');
prefs.set('dragdrop_patch_to_ptpimgit', 1);
// tracklist specific
prefs.set('tracklist_style', 1); // 1: classical, 2: propertional right aligned
prefs.set('max_tracklist_width', 80); // right margin of the right aligned tracklist. should not exceed the group description width on any device
prefs.set('title_separator', '. '); // divisor of track# and title
prefs.set('pad_leader', ' ');
prefs.set('tracklist_head_color', '#4682B4'); // #a7bdd0
prefs.set('tracklist_single_color', '#708080');
// classical tracklist only components colouring
prefs.set('tracklist_disctitle_color', '#008B8B');
prefs.set('tracklist_work_color', 'Olive'); //#b16890
prefs.set('tracklist_tracknumber_color', '#8899AA');
prefs.set('tracklist_artist_color', '#889B2F');
prefs.set('tracklist_composer_color', '#556B2F');
prefs.set('tracklist_duration_color', '#4682B4');

document.head.appendChild(document.createElement('style')).innerHTML = `
.ua-messages { text-indent: -2em; margin-left: 2em; }
.ua-messages-bg { padding: 15px; text-align: left; background-color: darkslategray; }
.ua-critical { color: red; font-weight: bold; }
.ua-warning { color: #ff8d00; font-weight: 500; }
.ua-info { color: white; }

.ua-button { vertical-align: middle; background-color: transparent; }
.ua-input {
  width: 610px; height: 3em;
  margin-top: 8px; margin-bottom: 8px;
  background-color: antiquewhite;
  font-size: small;
}
`;

var ref, tbl, elem, child, tb, spotifyCredentials = {};
var messages = null, autofill = false, domParser = new DOMParser(), dom;

if (isUpload) {
  ref = document.querySelector('form#upload_table > div#dynamic_form');
  if (ref == null) return;
  common1();
  let x = [];
  x.push(document.createElement('tr'));
  x[0].classList.add('ua-button');
  child = document.createElement('input');
  child.id = 'fill-from-text';
  child.value = 'Fill from text (overwrite)';
  child.type = 'button';
  child.style.width = '13em';
  child.onclick = fill_from_text;
  x[0].append(child);
  elem.append(x[0]);
  x.push(document.createElement('tr'));
  x[1].classList.add('ua-button');
  child = document.createElement('input');
  child.id = 'fill-from-text-weak';
  child.value = 'Fill from text (keep values)';
  child.type = 'button';
  child.style.width = '13em';
  child.onclick = fill_from_text;
  x[1].append(child);
  elem.append(x[1]);
  common2();
  ref.parentNode.insertBefore(tbl, ref);
} else if (isEdit) {
  ref = document.querySelector('form.edit_form > div > div > input[type="submit"]');
  if (ref == null) return;
  ref = ref.parentNode;
  ref.parentNode.insertBefore(document.createElement('br'), ref);
  common1();
  child = document.createElement('input');
  child.id = 'append-from-text';
  child.value = 'Fill from text (append)';
  child.type = 'button';
  child.onclick = fill_from_text;
  elem.append(child);
  common2();
  tbl.style.marginBottom = '10px';
  ref.parentNode.insertBefore(tbl, ref);
} else if (isRequestNew) {
  ref = document.getElementById('categories');
  if (ref == null) return;
  ref = ref.parentNode.parentNode.nextElementSibling;
  ref.parentNode.insertBefore(document.createElement('br'), ref);
  common1();
  child = document.createElement('input');
  child.id = 'fill-from-text-weak';
  child.value = 'Fill from URL';
  child.type = 'button';
  child.onclick = fill_from_text;
  elem.append(child);
  common2();
  child = document.createElement('td');
  child.colSpan = 2;
  child.append(tbl);
  elem = document.createElement('tr');
  elem.append(child);
  ref.parentNode.insertBefore(elem, ref);
} else if (isRequestEdit) {
  ref = document.querySelector('input#button[type="submit"]');
  if (ref == null) return;
  ref = ref.parentNode.parentNode;
  ref.parentNode.insertBefore(document.createElement('br'), ref);
  common1();
  child = document.createElement('input');
  child.id = 'append-from-text';
  child.value = 'Fill from text (append)';
  child.type = 'button';
  child.onclick = fill_from_text;
  elem.append(child);
  common2();
  tbl.style.marginBottom = '10px';
  elem = document.createElement('tr');
  child = document.createElement('td');
  child.colSpan = 2;
  child.append(tbl);
  elem.append(child);
  ref.parentNode.insertBefore(elem, ref);
}

if ((ref = document.getElementById('image') || document.querySelector('input[name="image"]')) != null) {
  ref.ondblclick = clear0;
  ref.onmousedown = clear1;
  ref.ondrop = clear0;
}

function clear0() { this.value = '' }
function clear1(e) { if (e.button == 1) this.value = '' }
function autoFill(e) {
  autofill = true;
  setTimeout(fill_from_text, prefs.autfill_delay);
}

function common1() {
  tbl = document.createElement('tr');
  tbl.style.backgroundColor = 'darkgoldenrod';
  tbl.style.verticalAlign = 'middle';
  elem = document.createElement('td');
  elem.style.textAlign = 'center';
  child = document.createElement('textarea');
  child.id = 'UA data';
  child.name = 'UA data';
  child.classList.add('ua-input');
  child.spellcheck = false;
  //child.ondblclick = clear0;
  child.onmousedown = clear1;
  child.ondrop = clear0;
  if (prefs.autfill_delay > 0) {
	child.onpaste = autoFill;
	child.ondrop = autoFill;
  }
  elem.append(child);
  tbl.append(elem);
  elem = document.createElement('td');
  elem.style.textAlign = 'center';
}
function common2() {
  tbl.append(elem);
  tb = document.createElement('tbody');
  tb.append(tbl);
  tbl = document.createElement('table');
  tbl.id = 'upload assistant';
  tbl.append(tb);
}

// if (prefs.always_hide_dnu_list && (ref = document.querySelector('div#content > div:first-of-type')) != null) {
//   ref.style.display = 'none'; // Hide DNU list (warning - risky!)
// }

if ((ref = document.getElementById('yadg_input')) != null) ref.ondrop = clear0;

Array.prototype.includesCaseless = function(str) {
  return typeof str == 'string' && this.find(it => it.toLowerCase() == str.toLowerCase()) != undefined;
};
Array.prototype.pushUnique = function(...items) {
  items.forEach(it => { if (!this.includes(it)) this.push(it) });
  return this.length;
};
Array.prototype.pushUniqueCaseless = function(...items) {
  items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  return this.length;
};
// Array.prototype.getUnique = function(prop) {
//   return this.every((it) => it[prop] && it[prop] == this[0][prop]) ? this[0][prop] : null;
// };
Array.prototype.equalTo = function(arr) {
  return arr instanceof Array && arr.length == this.length && arr.sort().toString() == this.sort().toString();
}
String.prototype.toASCII = function() {
  return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
}

const excludedCountries = [
  /\b(?:United States|USA?)\b/,
  /\b(?:United Kingdom|Great Britain|England|GB|UK)\b/,
  /\b(?:Europe|European Union|EU)\b/,
  /\b(?:Unknown)\b/,
];

class TagManager extends Array {
  constructor() {
	super();
	this.presubstitutions = [
	  [/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
	  [/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
	  [/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
	];
	this.substitutions = [
	  [/^(?:Alternativ)(?:\s+und\s+|\s*[&+]\s*)(?:indie)$/i, 'alternative', 'indie'],
	  [/^(?:Alternatif)(?:\s+et\s+|\s*[&+]\s*)(?:Inde)$/i, 'alternative', 'indie'],
	  [/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
	  [/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
	  [/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
	  [/^AOR$/, 'album.oriented.rock'],
	  [/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
	  [/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
	  [/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
	  [/^World$/i, 'world.music'],
	  [/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
	  [/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhytm.and.blues'],
	  [/\b(?:Soundtracks?)$/i, 'score'],
	  [/^(?:Electro)$/i, 'electronic'],
	  [/^(?:Metal)$/i, 'heavy.metal'],
	  [/^(?:NonFiction)$/i, 'non.fiction'],
	  [/^(?:Rap)$/i, 'hip.hop'],
	  [/^(?:NeoSoul)$/i, 'neo.soul'],
	  [/^(?:NuJazz)$/i, 'nu.jazz'],
	  [/^(?:Hardcore)$/i, 'hardcore.punk'],
	  [/^(?:garage)$/i, 'garage.rock'],
	  [/^(?:Ambiente)$/i, 'ambient'],
	  [/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
	  [/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
	  [/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
	  [/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
	  [/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
	  [/^GoaTrance$/i, 'goa.trance'],
	  [/^Female\s+Vocal\w*$/i, 'female.vocalist'],
	  // Country aliases
	  [/^(?:Canada)$/i, 'canadian'],
	  [/^(?:Australia)$/i, 'australian'],
	  [/^(?:Japan)$/i, 'japanese'],
	  [/^(?:Taiwan)$/i, 'thai'],
	  [/^(?:China)$/i, 'chinese'],
	  [/^(?:Russia|USSR)$/i, 'russian'],
	  [/^(?:France)$/i, 'french'],
	  [/^(?:Germany)$/i, 'german'],
	  [/^(?:Spain)$/i, 'spanish'],
	  [/^(?:Italy)$/i, 'italian'],
	  [/^(?:Sweden)$/i, 'swedish'],
	  [/^(?:Norway)$/i, 'norwegian'],
	  [/^(?:Finland)$/i, 'finnish'],
	  [/^(?:Greece)$/i, 'greek'],
	  [/^(?:Netherlands|Holland)$/i, 'dutch'],
	  [/^(?:Belgium)$/i, 'belgian'],
	  [/^(?:Denmark)$/i, 'danish'],
	  [/^(?:Austria)$/i, 'austrian'],
	  [/^(?:Portugal)$/i, 'portugese'],
	  [/^(?:Switzerland)$/i, 'swiss'],
	  [/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
	  [/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
	  [/^(?:Poland)$/i, 'polish'],
	  [/^(?:Hungary)$/i, 'hungarian'],
	  [/^(?:Yugoslavia)$/i, 'yugoslav'],
	  [/^(?:Brazil)$/i, 'brazilian'],
	  [/^(?:Mexico)$/i, 'mexican'],
	  [/^(?:Argentina)$/i, 'argentinean'],
	  [/^(?:Jamaica)$/i, 'jamaican'],
	];
	this.splits = [
	  ['Alternative', 'Indie'],
	  ['Rock', 'Pop'],
	  ['Soul', 'Funk'],
	  ['Ska', 'Rocksteady'],
	  ['Jazz Fusion', 'Jazz Rock'],
	  ['Rock', 'Pop'],
	];
	this.additions = [
	  [/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
	  [/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Vocal|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Latin|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
	  [/^(?:Opera)$/i, 'classical'],
	  [/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
	  [/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
	  [/^(?:Symphony)$/i, 'classical'],
	  [/^(?:Sacred\s+Vocal)\b/i, 'classical'],
	  [/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
	];
	this.removals = [
	  /^(?:Unknown)$/i,
	  /^(?:Other)$/i,
	  /^(?:New)$/i,
	  /^(?:Ostatni)$/i,
	].concat(excludedCountries);
  }

  add(...tags) {
	var added = 0;
	for (var tag of tags) {
	  if (typeof tag != 'string') continue;
	  this.presubstitutions.forEach(k => { if (k[0].test(tag)) tag = tag.replace(k[0], k[1]) });
	  tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(function(tag) {
		tag = tag.toASCII().replace(/\(.*?\)|\[.*?\]|\{.*?\}/g, '').trim();
		if (tag.length <= 0 || tag == '?') return null;
		function test(obj) {
		  return obj instanceof RegExp && obj.test(tag)
			|| typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase();
		}
		if (this.removals.some(k => test(k))) {
		  addMessage('Warning: bad tag \'' + tag + '\' found', 'ua-warning');
		  return;
		}
		for (var k of this.additions) {
		  if (test(k[0])) added += this.add(...k.slice(1));
		}
		for (k of this.splits) {
		  if (new RegExp('^' + k[0] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[1] + '$', 'i').test(tag)) {
			added += this.add(k[0], k[1]); return;
		  }
		  if (new RegExp('^' + k[1] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[0] + '$', 'i').test(tag)) {
			added += this.add(k[0], k[1]); return;
		  }
		}
		for (k of this.substitutions) {
		  if (test(k[0])) { added += this.add(...k.slice(1)); return; }
		}
		tag = tag.
		  replace(/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1').
		  replace(/\b(?:Alt\.)(?=\s+)/i, 'Alternative').
		  replace(/^[3-9]0s$/i, '19$0').
		  replace(/^[0-2]0s$/i, '20$0').
		  replace(/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2').
		  replace(/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and ').
		  replace(/[\s\-\−\—\–\_\.\,\'\`\~]+/g, '.').
		  replace(/[^\w\.]+/g, '').
		  toLowerCase();
		if (tag.length >= 2 && !this.includes(tag)) {
		  this.push(tag);
		  ++added;
		}
	  }.bind(this));
	}
	return added;
  }
  toString() {
	return this.length > 0 ? this.sort().join(', ') : null;
  }
};

if ((ref = document.getElementById('categories')) != null) {
  ref.addEventListener('change', function(e) {
	elem = document.getElementById('upload assistant');
	if (elem != null) elem.style.visibility = this.value < 4
		|| ['Music', 'Applications', 'E-Books', 'Audiobooks'].includes(this.value) ? 'visible' : 'collapse';
  });
}

var rehostItBtn = document.querySelector('input.rehost_it_cover[type="button"]');
if (prefs.dragdrop_patch_to_ptpimgit && rehostItBtn != null && !isNWCD) {
  rehostItBtn.ondragover = voidDragHandler;
  rehostItBtn.ondrop = imageDropHandler;
}

return;

function fill_from_text(e) {
  if (e == undefined && !autofill) return;
  autofill = false;
  var overwrite = this.id == 'fill-from-text';
  var clipBoard = document.getElementById('UA data');
  if (clipBoard == null) return false;
  const urlParser = /^\s*(https?:\/\/[\S]+)\s*$/i;
  messages = document.getElementById('UA messages');
  //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
  //if (typeof clipBoard != 'string') return false;
  var i, matches, url, category = document.getElementById('categories');
  if (category == null && document.getElementById('releasetype') != null
	  || category != null && (category.value == 0 || category.value == 'Music')) return fill_from_text_music();
  if (category != null && (category.value == 1 || category.value == 'Applications')) return fill_from_text_apps();
  if (category != null && (category.value == 2 || category.value == 3
	|| category.value == 'E-Books' || category.value == 'Audiobooks')) return fill_from_text_books();
  return category == null ? fill_from_text_apps(true) || fill_from_text_books() : false;

  function fill_from_text_music() {
	if (messages != null) messages.parentNode.removeChild(messages);
	const divs = ['—', '⸺', '⸻'];
	const vaParser = /^(?:Various(?:\s+Artists)?|VA|Různí(?:\s+interpreti)?)$/i;
	const multiArtistParsers = [
	  /\s*(?:[;\/\|\×]|,(?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?)\s*/,
	];
	const ampersandParsers = [
	  /\s+(?:meets|vs\.?|X)\s+/i,
	  /\s+(?:[\&\+]|and)\s+(?!(?:his|her|Friends)\b)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
	  /\s*\+\s*(?!(?:The|his|her|Friends)\b)/i,
	];
	const featParsers = [
	  /\s+(?:meets)\s+(.*?)\s*$/i,
	  /\s+(?:[Ff]eaturing|[Ww]ith)\s+(.*?)\s*$/,
	  /\s+(?:[Ff]eat|[Ff]t)\.\s+(.*?)\s*$/,
	  /\s+\[(?:[Ff]eat(?:\.|uring)|[Ff]t\.|[Ww]ith)\s+([^\[\]]+?)\s*\]/,
	  /\s+\((?:[Ff]eat(?:\.|uring)|[Ff]t\.|[Ww]ith)\s+([^\(\)]+?)\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,
	  /\s+\(([^\(\)]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\)/i,
	  /\s+\[([^\[\]]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\]/i,
	  /\s+\((?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
	  /\s+\[(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
	];
	const otherArtistsParsers = [
	  [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
	  [/^()(.*?)\s+\(conductor\)$/i, 4],
	  //[/^()(.*?)\s+\(.*\)$/i, 1],
	];
	const pseudoArtistParsers = [
	  /^(?:#?N\/?A|[JS]r\.?)$/i,
	  /^(?:traditional|lidová)$/i,
	  /\b(?:traditional|lidová)$/,
	  /^(?:[Aa]nonym)/,
	  /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
	  /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
	];
	const noAKAs = /\s+(?:aka|AKA)\s+(.*)/;
	var track, tracks = [], totalDiscs = 1, media, xhr = new XMLHttpRequest();
	if (urlParser.test(clipBoard.value)) return init_from_url_music(RegExp.$1);
	function ruleLink(rule) { return ' (<a href="https://redacted.ch/rules.php?p=upload#r' + rule + '" target="_blank">' + rule + '</a>)' }
	var albumBitrate = 0, totalTime = 0;
	for (iter of clipBoard.value.split(/[\r\n]+/)) {
	  if (!iter.trim()) continue; // skip empty lines
	  let metaData = iter.split('\x1E');
	  track = {
		artist: metaData.shift().trim() || undefined,
		album: metaData.shift().trim() || undefined,
		album_year: safeParseYear(metaData.shift().trim()),
		release_date: metaData.shift().trim() || undefined,
		label: metaData.shift().trim() || undefined,
		catalog: metaData.shift().trim() || undefined,
		country: metaData.shift().trim() || undefined,
		encoding: metaData.shift().trim() || undefined,
		codec: metaData.shift().trim() || undefined,
		codec_profile: metaData.shift().trim() || undefined,
		bitrate: safeParseInt(metaData.shift()),
		bd: safeParseInt(metaData.shift()),
		sr: safeParseInt(metaData.shift()),
		channels: safeParseInt(metaData.shift()),
		media: metaData.shift().trim() || undefined,
		genre: metaData.shift().trim() || undefined,
		discnumber: safeParseInt(metaData.shift()),
		totaldiscs: safeParseInt(metaData.shift()),
		discsubtitle: metaData.shift().trim() || undefined,
		tracknumber: metaData.shift().trim() || undefined,
		totaltracks: safeParseInt(metaData.shift()),
		title: metaData.shift().trim() || undefined,
		track_artist: metaData.shift().trim() || undefined,
		performer: metaData.shift().trim() || undefined,
		composer: metaData.shift().trim() || undefined,
		conductor: metaData.shift().trim() || undefined,
		remixer: metaData.shift().trim() || undefined,
		compiler: metaData.shift().trim() || undefined,
		producer: metaData.shift().trim() || undefined,
		duration: safeParseFloat(metaData.shift()),
		samples: safeParseInt(metaData.shift()),
		rg: metaData.shift().trim() || undefined,
		dr: metaData.shift().trim() || undefined,
		vendor: metaData.shift().trim() || undefined,
		url: metaData.shift().trim() || undefined,
		dirpath: metaData.shift() || undefined,
		comment: metaData.shift().trim() || undefined,
		identifiers: {},
	  };
	  if (!track.artist) {
		addMessage('FATAL: main artist must be defined in every track' + ruleLink('2.3.16.4'), 'ua-critical', true);
		clipBoard.value = '';
		throw new Error('artist missing');
	  }
	  if (!track.album) {
		addMessage('FATAL: album title must be defined in every track' + ruleLink('2.3.16.4'), 'ua-critical', true);
		clipBoard.value = '';
		throw new Error('album mising');
	  }
	  if (!track.tracknumber) {
		addMessage('FATAL: all track numbers must be defined' + ruleLink('2.3.16.4'), 'ua-critical', true);
		clipBoard.value = '';
		throw new Error('tracknumber missing');
	  }
	  if (!track.title) {
		addMessage('FATAL: all track titles must be defined' + ruleLink('2.3.16.4'), 'ua-critical', true);
		clipBoard.value = '';
		throw new Error('track title missing');
	  }
	  if (track.duration != undefined && isUpload && (isNaN(track.duration) || track.duration <= 0)) {
		addMessage('FATAL: invalid track #' + track.tracknumber + ' length: ' + track.duration, 'ua-critical');
		clipBoard.value = '';
		throw new Error('invalid duration');
	  }
	  if (track.codec && !['FLAC', 'MP3', 'AAC', 'DTS', 'AC3'].includes(track.codec)) {
		addMessage('FATAL: disallowed codec present (' + track.codec + ')', 'ua-critical');
		clipBoard.value = '';
		throw new Error('invalid format');
	  }
	  if (track.discnumber > totalDiscs) totalDiscs = track.discnumber;
	  if (track.comment == '.') track.comment = undefined;
	  if (track.comment) {
		track.comment = track.comment.replace(/\x1D/g, '\r').replace(/\x1C/g, '\n');
		if (prefs.remap_texttools_newlines) track.comment = track.comment.replace(/__/g, '\r\n').replace(/_/g, '\n') // ambiguous
	  }
	  if (track.dr != null) track.dr = parseInt(track.dr); // DR0
	  metaData.shift().trim().split(/\s+/).forEach(function(it) {
		if (/([\w\-]+)[=:](.*)/.test(it)) track.identifiers[RegExp.$1.toUpperCase()] = RegExp.$2.replace(/\x1B/g, ' ');
	  });
	  totalTime += track.duration || NaN;
	  albumBitrate += (track.duration || NaN) * (track.bitrate || NaN);

	  tracks.push(track);

	  function safeParseInt(x) { return typeof x != 'string' ? null : x.length <= 0 ? undefined : parseInt(x) }
	  function safeParseFloat(x) { return typeof x != 'string' ? null : x.length <= 0 ? undefined : parseFloat(x) }
	  function safeParseYear(x) { return typeof x != 'string' ? null : x.length <= 0 ? undefined : extract_year(x) || NaN }
	}
	if (tracks.length <= 0) {
	  addMessage('FATAL: no tracks found', 'ua-critical', true);
	  clipBoard.value = '';
	  throw new Error('no tracks');
	}
	if (!tracks.every(it => it.discnumber > 0) && !tracks.every(it => !it.discnumber)) {
	  addMessage('FATAL: inconsistent release (mix of tracks with and without disc number)', 'ua-critical', true);
	  clipBoard.value = '';
	  throw new Error('inconsistent disc numbering');
	}

	var release = {};
	['catalogs', 'bds', 'genres', 'srs', 'urls', 'comments', 'trackArtists', 'bitrates',
	 'drs', 'rgs', 'dirpaths', 'composers'].forEach(it => { release[it] = [] });
	function setUniqueProperty(propName, propNameLiteral) {
	  let homogenous = new Set(tracks.map(it => it[propName]).filter(it => it != undefined && it != null));
	  if (homogenous.size > 1) {
		var diverses = '', it = homogenous.values(), val;
		while (!(val = it.next()).done) diverses += '<br>\t' + val.value;
		addMessage('FATAL: mixed releases not accepted (' + propNameLiteral + ') - supposedly user compilation' + diverses, 'ua-critical', true);
		clipBoard.value = '';
		throw new Error('mixed release (' + propNameLiteral + ')');
	  }
	  release[propName] = homogenous.values().next().value;
	}
	setUniqueProperty('artist', 'album artist', true);
	setUniqueProperty('album', 'album title', true);
	setUniqueProperty('album_year', 'album year');
	setUniqueProperty('release_date', 'release date');
	setUniqueProperty('encoding', 'encoding');
	setUniqueProperty('codec', 'codec');
	setUniqueProperty('codec_profile', 'codec profile');
	setUniqueProperty('vendor', 'vendor');
	setUniqueProperty('media', 'media');
	setUniqueProperty('channels', 'channels');
	setUniqueProperty('label', 'label');
	setUniqueProperty('country', 'country');

	tracks.forEach(function(iter) {
	  push_unique('trackArtists', 'track_artist');
	  push_unique('composers', 'composer');
	  push_unique('catalogs', 'catalog');
	  push_unique('bitrates', 'bitrate');
	  push_unique('bds', 'bd');
	  push_unique('rgs', 'rg');
	  push_unique('drs', 'dr');
	  if (iter.sr) {
		if (typeof release.srs[iter.sr] != 'number') {
		  release.srs[iter.sr] = iter.duration;
		} else {
		  release.srs[iter.sr] += iter.duration;
		}
	  }
	  push_unique('dirpaths', 'dirpath');
	  push_unique('comments', 'comment');
	  push_unique('genres', 'genre');
	  push_unique('urls', 'url');

	  function push_unique(relProp, prop) {
		if (iter[prop] !== undefined && iter[prop] !== null && (typeof iter[prop] != 'string'
			|| iter[prop].length > 0) && !release[relProp].includes(iter[prop])) release[relProp].push(iter[prop]);
	  }
	});
	function validatorFunc(arr, validator, str) {
	  if (arr.length <= 0 || !arr.some(validator)) return true;
	  addMessage('FATAL: disallowed ' + str + ' present (' + arr.filter(validator) + ')', 'ua-critical');
	  clipBoard.value = '';
	  throw new Error('disallowed ' + str);
	}
	validatorFunc(release.bds, bd => ![16, 24].includes(bd), 'bit depths');
	validatorFunc(Object.keys(release.srs),
		sr => sr < 44100 || sr > 192000 || sr % 44100 != 0 && sr % 48000 != 0, 'sample rates');

	var composerEmphasis = false, isFromDSD = false, isClassical = false;
	var yadg_prefil = '', releaseType, editionTitle, isVA, iter, rx;
	var tags = new TagManager();
	albumBitrate /= totalTime;
	if (tracks.every(it => /^(?:single)$/i.test(it.identifiers.RELEASETYPE))
		|| totalTime > 0 && totalTime <= prefs.single_threshold) {
	  releaseType = getReleaseIndex('Single');
	} else if (tracks.every(it => it.identifiers.RELEASETYPE == 'EP')
		|| totalTime > 0 && totalTime <= prefs.EP_threshold) {
	  releaseType = getReleaseIndex('EP');
	} else if (tracks.every(it => /^soundtrack$/i.test(it.identifiers.RELEASETYPE))) {
	  releaseType = getReleaseIndex('Soundtrack');
	  tags.add('score');
	  composerEmphasis = true;
	}

	// Processing artists: recognition, splitting and dividing to categores
	var roleCollisions = [
	  [4, 5], // main
	  [0, 4], // guest
	  [], // remixer
	  [], // composer
	  [], // conductor
	  [], // DJ/compiler
	  [], // producer
	];
	isVA = vaParser.test(release.artist);
	var artists = [];
	for (i = 0; i < 7; ++i) artists[i] = [];

	if (!isVA) addArtists(0, yadg_prefil = spliceGuests(release.artist));

	featParsers.slice(3).forEach(function(rx) {
	  if (rx.test(release.album)) {
		addArtists(1, RegExp.$1);
		addMessage('Warning: featured artist(s) in album title (' + release.album + ')', 'ua-warning');
		release.album = release.album.replace(rx, '');
	  }
	});
	remixParsers.slice(3).forEach(function(rx) {
	  if (rx.test(release.album)) addArtists(2, RegExp.$1);
	})
	if (isVA && (/\s+\(compiled\s+by\s+(.*?)\)\s*$/i.test(release.album)
		|| /\s+compiled\s+by\s+(.*?)\s*$/i.test(release.album))) {
	  addArtists(5, RegExp.$1);
	  if (!releaseType) releaseType = getReleaseIndex('Compilation');
	}

	for (iter of tracks) {
	  addTrackPerformers(iter.track_artist);
	  addTrackPerformers(iter.performer);
	  addArtists(2, iter.remixer);
	  addArtists(3, iter.composer);
	  addArtists(4, iter.conductor);
	  addArtists(5, iter.compiler);
	  addArtists(6, iter.producer);

	  if (iter.title) {
		featParsers.slice(3).forEach(function(rx) {
		  if (rx.test(iter.title)) {
			iter.track_artist = (!isVA && (!iter.track_artist || iter.track_artist.includes(RegExp.$1)) ?
								 iter.artist : iter.track_artist) + ' feat. ' + RegExp.$1;
			addArtists(1, RegExp.$1);
			addMessage('Warning: featured artist(s) in track title (#' + iter.tracknumber + ': ' + iter.title + ')', 'ua-warning');
			iter.title = iter.title.replace(rx, '');
		  }
		});
		remixParsers.slice(3).forEach(function(rx) {
		  if (rx.test(iter.title)) addArtists(2, RegExp.$1);
		});
	  }
	}
	for (i = 0; i < Math.round(tracks.length / 2); ++i) splitAmpersands();

	function addArtists(ndx, str) {
	  if (str) splitArtists(str).forEach(function(artist) {
		artist = ndx != 0 ? artist.replace(noAKAs, '') : guessOtherArtists(artist);
		if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
			&& !artists[ndx].includesCaseless(artist)
			&& !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist))) artists[ndx].push(artist);
	  });
	}
	function addTrackPerformers(str) {
	  if (str) splitArtists(spliceGuests(str, 1)).forEach(function(artist) {
		artist = guessOtherArtists(artist);
		if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
			&& !artists[0].includesCaseless(artist)
			&& (isVA || !artists[1].includesCaseless(artist))) artists[isVA ? 0 : 1].push(artist);
	  });
	}
	function splitArtists(str) {
	  var result = [str];
	  multiArtistParsers.forEach(function(multiArtistParser) {
		for (i = result.length; i > 0; --i) {
		  var j = result[i - 1].split(multiArtistParser);
		  if (j.length >= 2 && j.every(twoOrMore)
			  && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
			  	&& !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
		}
	  });
	  return result;
	}
	function splitAmpersands() {
	  for (var ndx = 0; ndx < artists.length; ++ndx) {
		ampersandParsers.forEach(function(ampersandParser) {
		  for (var i = artists[ndx].length; i > 0; --i) {
			var j = artists[ndx][i - 1].split(ampersandParser);
			if (j.length >= 2 && j.every(twoOrMore) && !getSiteArtist(artists[ndx][i - 1])
				&& (j.some(it1 => artists.some(it2 => it2.includesCaseless(it1))) || j.every(looksLikeTrueName))) {
			  artists[ndx].splice(i - 1, 1, ...j.filter(function(artist) {
				return !artists[ndx].includesCaseless(artist)
					&& !pseudoArtistParsers.some(rx => rx.test(artist))
					&& !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist));
			  }));
			}
		  }
		});
	  }
	}
	function spliceGuests(str, level = 1) {
	  (level ? featParsers.slice(level) : featParsers).forEach(function(it) {
		if (it.test(str)) {
		  addArtists(1, RegExp.$1);
		  str = str.replace(it, '');
		}
	  });
	  return str;
	}
	function guessOtherArtists(name) {
	  otherArtistsParsers.forEach(function(it) {
		if (!it[0].test(name)) return;
		addArtists(it[1], RegExp.$2);
		name = RegExp.$1;
	  });
	  return name.replace(noAKAs, '');
	}
	function getSiteArtist(artist) {
	  if (!artist) return null;
	  xhr.open('GET', 'https://' + document.domain + '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist), false);
	  xhr.send();
	  if (xhr.readyState != 4 || xhr.status != 200) {
		console.log('getSiteArtist("' + artist + '"): XMLHttpRequest readyState:' + xhr.readyState + ' status:' + xhr.status);
		return undefined; // error
	  }
	  var response = JSON.parse(xhr.responseText);
	  return response.status == 'success' ? response.response : null;
	}
	function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
	function looksLikeTrueName(artist, index) {
	  return (index == 0 || !/^(?:The|his|her|Friends)\s+/i.test(artist)) && artist.split(/\s+/).length >= 2
	  	&& !pseudoArtistParsers.some(rx => rx.test(artist)) || getSiteArtist(artist);
	}

	if (elementWritable(document.getElementById('artist'))) {
	  let artistIndex = 0;
	  catLoop: for (i = 0; i < 7; ++i) for (iter of artists[i]
		  .filter(artist => !roleCollisions[i].some(n => artists[n].includesCaseless(artist)))
		  .sort((a, b) => a.localeCompare(b))) {
		if (isUpload) {
		  var id = 'artist';
		  if (artistIndex > 0) id += '_' + artistIndex;
		  while ((ref = document.getElementById(id)) == null) addArtistField();
		} else {
		  while ((ref = document.querySelectorAll('input[name="artists[]"]')).length <= artistIndex) addArtistField();
		  ref = ref[artistIndex];
		}
		if (ref == null) throw new Error('Failed to allocate artist fields');
		ref.value = iter;
		ref.nextElementSibling.value = i + 1;
		if (++artistIndex >= 200) break catLoop;
	  }
	  if (overwrite && artistIndex > 0) while (document.getElementById('artist_' + artistIndex) != null) {
		removeArtistField();
	  }
	}

	// Processing album title
	const remasterParsers = [
	  /\s+\(((?:Remaster(?:ed)?|Reissu(?:ed)?|Deluxe|Enhanced|Expanded|Limited)\b[^\(\)]*|[^\(\)]*\b(?:Edition|Version|Promo|Release))\)$/i,
	  /\s+\[((?:Remaster(?:ed)?|Reissu(?:ed)?|Deluxe|Enhanced|Expanded|Limited)\b[^\[\]]*|[^\[\]]*\b(?:Edition|Version|Promo|Release))\]$/i,
	  /\s+-\s+([^\[\]\(\)\-\−\—\–]*\b(?:(?:Remaster(?:ed)?|Bonus\s+Track)\b[^\[\]\(\)\-\−\—\–]*|Reissue|Edition|Version|Promo|Enhanced|Release))$/i
	];
	const mediaParsers = [
	  [/\s+(?:\[(?:LP|Vinyl|12"|7")\]|\((?:LP|Vinyl|12"|7")\))$/, 'Vinyl'],
	  [/\s+(?:\[SA-?CD\]|\(SA-?CD\))$/, 'SACD'],
	  [/\s+(?:\[(?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\]|\((?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\))$/, 'Blu-Ray'],
	  [/\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/, 'DVD'],
	];
	const releaseTypeParsers = [
	  [/\s+(?:-\s+Single|\[Single\]|\(Single\))$/i, 'Single', true, true],
	  [/\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/, 'EP', true, true],
	  [/\s+\((?:Live|En\s+directo?|Ao\s+Vivo)\b[^\(\)]*\)$/i, 'Live album', false, false],
	  [/\s+\[(?:Live|En\s+directo?|Ao\s+Vivo)\b[^\[\]]*\]$/i, 'Live album', false, false],
	  [/(?:^Live\s+(?:[aA]t|[Ii]n)\b|^Directo?\s+[Ee]n\b|\bUnplugged\b|\bAcoustic\s+Stage\b|\s+Live$)/, 'Live album', false, false],
	  [/\b(?:Best [Oo]f|Greatest Hits|Complete\s+(.+?\s+)(?:Albums|Recordings))\b/, 'Anthology', false, false],
	];
	var album = release.album;
	releaseTypeParsers.forEach(function(it) {
	  if (it[0].test(album)) {
		if (it[2] || !releaseType) releaseType = getReleaseIndex(it[1]);
		if (it[3]) album = album.replace(it[0], '');
	  }
	});
	rx = '\\b(?:Soundtrack|Score|Motion\\s+Picture|Series|Television|Original(?:\\s+\\w+)?\\s+Cast|Music\\s+from|(?:Musique|Bande)\\s+originale)\\b';
	if (reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
	  if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
	  tags.add('score');
	  composerEmphasis = true;
	}
	remixParsers.forEach(function(rx) {
	  if (rx.test(album) && !releaseType) releaseType = getReleaseIndex('Remix');
	});
	remasterParsers.forEach(function(rx) {
	  if (rx.test(album)) {
		album = album.replace(rx, '');
		editionTitle = RegExp.$1;
	  }
	});
	mediaParsers.forEach(function(it) {
	  if (it[0].test(album)) {
		album = album.replace(it[0], '');
		media = it[1];
	  }
	});
	if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
	  ref.value = album;
	}

	if (yadg_prefil) yadg_prefil += ' ';
	yadg_prefil += album;
	if (elementWritable(ref = document.getElementById('yadg_input'))) {
	  ref.value = yadg_prefil || '';
	  if (yadg_prefil && (ref = document.getElementById('yadg_submit')) != null && !ref.disabled) ref.click();
	}

	if (!release.album_year && tracks.every(function(tr) {
		  return tr.identifiers.PUBYEAR && tr.identifiers.PUBYEAR == tracks[0].identifiers.PUBYEAR;
		})) release.album_year = parseInt(tracks[0].identifiers.PUBYEAR);
	if (elementWritable(ref = document.getElementById('year'))) {
	  ref.value = release.album_year || '';
	}
	i = release.release_date && extract_year(release.release_date);
	if (elementWritable(ref = document.getElementById('remaster_year'))
		|| !isUpload && i > 0 && (ref = document.querySelector('input[name="year"]')) != null && !ref.disabled) {
	  ref.value = i || '';
	}
	//if (tracks.every(it => it.identifiers.EXPLICIT == '0')) editionTitle = 'Clean' + (editionTitle ? ' / ' + editionTitle : '');
	if (!editionTitle && (tracks.every(it => it.title.endsWith(' (Remastered)') || it.title.endsWith(' [Remastered]')))) {
	  editionTitle = 'Remastered';
	}
	if (elementWritable(ref = document.getElementById('remaster_title'))) {
	  ref.value = editionTitle || '';
	}
	rx = /\s*[\,\;]\s*/g;
	if (elementWritable(ref = document.getElementById('remaster_record_label') || document.querySelector('input[name="recordlabel"]'))) {
	  ref.value = release.label && release.label.replace(rx, ' / ') || '';
	}
	if (elementWritable(ref = document.getElementById('remaster_catalogue_number') || document.querySelector('input[name="cataloguenumber"]'))) {
	  let barcode = tracks.every(function(it, ndx, arr) {
		return it.identifiers.BARCODE && it.identifiers.BARCODE == arr[0].identifiers.BARCODE;
	  }) && tracks[0].identifiers.BARCODE;
	  ref.value = release.catalogs.length >= 1 && release.catalogs.map(k => k.replace(rx, ' / ')).join(' / ')
	  	|| barcode || '';
	}
	var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
	if (elementWritable(ref = document.getElementById('format'))) {
	  ref.value = release.codec || '';
	  ref.onchange(); //exec(function() { Format() });
	}
	if (isRequestNew) {
	  if (prefs.always_request_perfect_flac) reqSelectFormats('FLAC');
	  else if (release.codec) reqSelectFormats(release.codec);
	}
	var sel;
	if (release.encoding == 'lossless') {
	  sel = release.bds.includes(24) ? '24bit Lossless' : 'Lossless';
	} else if (release.bitrates.length >= 1) {
	  let lame_version = release.codec == 'MP3' && /^LAME(\d+)\.(\d+)/i.test(release.vendor) ?
		  parseInt(RegExp.$1) * 1000 + parseInt(RegExp.$2) : undefined;
	  if (release.codec == 'MP3' && release.codec_profile == 'VBR V0') {
		sel = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
	  } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V1') {
		sel = 'V1 (VBR)'
	  } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V2') {
		sel = lame_version >= 3094 ? sel = 'V2 (VBR)' : 'APS (VBR)'
	  } else if (release.bitrates.length == 1 && [192, 256, 320].includes(Math.round(release.bitrates[0]))) {
		sel = Math.round(release.bitrates[0]);
	  } else {
		sel = 'Other';
	  }
	}
	if ((ref = document.getElementById('bitrate')) != null && !ref.disabled && (overwrite || !br_isSet)) {
	  ref.value = sel || '';
	  ref.onchange(); //exec(function() { Bitrate() });
	  if (sel == 'Other' && (ref = document.getElementById('other_bitrate')) != null) {
		ref.value = Math.round(release.bitrates.length == 1 ? release.bitrates[0] : albumBitrate);
		if ((ref = document.getElementById('vbr')) != null) ref.checked = release.bitrates.length > 1;
	  }
	}
	if (isRequestNew) {
	  if (prefs.always_request_perfect_flac) {
		reqSelectBitrates('Lossless', '24bit Lossless');
	  } else if (sel) reqSelectBitrates(sel);
	}
	if (release.media) {
	  sel = undefined;
	  [
		[/\b(?:WEB|File|Download|digital\s+media)\b/i, 'WEB'],
		[/\bCD\b/, 'CD'],
		[/\b(?:SA-?CD|[Hh]ybrid)\b/, 'SACD'],
		[/\b(?:[Bb]lu[\-\−\—\–\s]?[Rr]ay|BRD?|BD)\b/, 'Blu-Ray'],
		[/\bDVD(?:-?A)?\b/, 'DVD'],
		[/\b(?:[Vv]inyl\b|LP\b|12"|7")/, 'Vinyl'],
	  ].forEach(k => { if (k[0].test(release.media)) sel = k[1] });
	  media = sel || media;
	}
	if (!media) {
	  if (tracks.every(isRedBook)) {
		addMessage('Info: media not determined - CD estimated', 'ua-info');
		media = 'CD';
	  } else if (tracks.some(t => t.bd > 16 || (t.sr > 0 && t.sr != 44100) || t.samples > 0 && t.samples % 588 != 0)) {
		addMessage('Info: media not determined - NOT CD', 'ua-info');
	  }
	} else if (media != 'CD' && tracks.every(isRedBook)) {
	  addMessage('Info: CD as source media is estimated (' + media + ')', 'ua-info');
	}
	if (elementWritable(ref = document.getElementById('media'))) ref.value = media || '';
	if (isRequestNew) {
	  if (prefs.always_request_perfect_flac) reqSelectMedias('WEB', 'CD', 'Blu-Ray', 'DVD', 'SACD')
	  else if (media) reqSelectMedias(media);
	}
	function isRedBook(t) {
	  return t.bd == 16 && t.sr == 44100 && t.channels == 2 && t.samples > 0 && t.samples % 588 == 0;
	}
	if (media == 'WEB') for (iter of tracks) {
	  if (iter.duration > 29.5 && iter.duration < 30.5) {
		addMessage('Warning: track ' + iter.tracknumber + ' possible preview', 'ua-warning');
	  }
	}
	if (tracks.every(it => it.identifiers.ORIGINALFORMAT && it.identifiers.ORIGINALFORMAT.includes('DSD'))) {
	  isFromDSD = true;
	}
	if (release.genres.length > 0) {
	  const classicalGenreParsers = [
		/\b(?:Classical|Classique|Klassik|Symphony|Symphonic(?:al)?|Operas?|Operettas?|Ballets?|(?:Violin|Cello|Piano)\s+Solos?|Chamber|Choral|Choirs?|Orchestral|Etudes?|Duets|Concertos?|Cantatas?|Requiems?|Passions?|Mass(?:es)?|Oratorios?|Poems?|Sacred|Secular|Vocal\s+Music)\b/i,
	  ];
	  release.genres.forEach(function(genre) {
		classicalGenreParsers.forEach(function(classicalGenreParser) {
		  if (classicalGenreParser.test(genre) && !/\b(?:metal|rock|pop)\b/i.test(genre)) {
			composerEmphasis = true;
			isClassical = true
		  }
		});
		if (/\b(?:Jazz|Vocal)\b/i.test(genre) && !/\b(?:Nu|Future|Acid)[\s\-\−\—\–]*Jazz\b/i.test(genre)
		   && !/\bElectr(?:o|ic)[\s\-\−\—\–]?Swing\b/i.test(genre)) {
		  composerEmphasis = true;
		}
		if (/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i.test(genre)) {
		  if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
		  composerEmphasis = true;
		}
		if (/\b(?:Christmas\s+Music)\b/i.test(genre)) {
		  composerEmphasis = true;
		}
	  	tags.add(genre);
	  });
	  if (release.genres.length > 1) {
		addMessage('Warning: inconsistent genre accross album: ' + release.genres, 'ua-warning');
	  }
	}
	if (release.country) {
	  if (!excludedCountries.some(it => it.test(release.country))) tags.add(release.country);
	}
	if (elementWritable(ref = document.getElementById('tags'))) {
	  ref.value = tags.length >= 1 ? tags.toString() : '';
	  if (artists[0].length == 1 && prefs.fetch_tags_from_artist > 0) setTimeout(function() {
		var artist = getSiteArtist(artists[0][0]);
		if (!artist) return;
		tags.add(...artist.tags.sort((a, b) => b.count - a.count).map(it => it.name).slice(0, prefs.fetch_tags_from_artist));
		var ref = document.getElementById('tags');
		ref.value = tags.toString();
	  }, 3000);
	}
	if (isClassical && !tracks.every(it => it.composer)) {
	  addMessage('Warning: all tracks composers must be defined for clasical music' + ruleLink('2.3.17'), 'ua-warning', true);
	  //return false;
	}
	if (!releaseType) {
	  if (tracks.every(it => it.title.endsWith(' (Live)') || it.title.endsWith(' [Live]'))) {
		releaseType = getReleaseIndex('Live album');
	  } else if (tracks.every(function(it) {
		const p = /^([^\(\)\[\]]+?)(?:\s+(?:\([^\(\)]*\)|\[[^\[\]]*\]))?$/;
		try { return p.exec(it.title)[1] == p.exec(tracks[0].title)[1] } catch(e) { return false }
	  })) {
		releaseType = getReleaseIndex('Single');
	  } else if (/\b(?:Mixtape)\b/i.test(release.album)) {
		releaseType = getReleaseIndex('Mixtape');
	  } else if (isVA) {
		releaseType = getReleaseIndex('Compilation');
	  } else if (tracks.every(it => it.identifiers.COMPILATION == 1)) {
		releaseType = getReleaseIndex('Anthology');
	  }
	}
	if ((ref = document.getElementById('releasetype')) != null && !ref.disabled && (overwrite || ref.value == 0)) {
	  ref.value = releaseType || getReleaseIndex('Album');
	}
	if (!composerEmphasis && !prefs.keep_meaningles_composers) {
	  document.querySelectorAll('input[name="artists[]"]').forEach(function(i) {
		if (['4', '5'].includes(i.nextElementSibling.value)) i.value = '';
	  });
	}
	const doubleParsParsers = [
	  /\(+(\([^\(\)]*\))\)+/,
	  /\[+(\[[^\[\]]*\])\]+/,
	  /\{+(\{[^\{\}]*\})\}+/,
	];
	for (iter of tracks) {
	  doubleParsParsers.forEach(function(rx) {
	  	if (rx.test(iter.title)) {
		  addMessage('Warning: doubled parentheses in track #' + iter.tracknumber +
					 ' title ("' + iter.title + '")', 'ua-warning');
		  //iter.title.replace(rx, RegExp.$1);
		}
	  });
	}
	if (tracks.length > 1 && array_homogenous(tracks.map(k => k.title))) {
	  addMessage('Warning: all tracks having same title: ' + tracks[0].title, 'ua-warning');
	}
	// Album description
	var description;
	url = release.urls.length == 1 && release.urls[0];
	function makeUrlFromId(identifier, urlBase) {
	  if (!url && tracks.every(track => track.identifiers[identifier]
			&& track.identifiers[identifier] == tracks[0].identifiers[identifier])) {
		url = urlBase + tracks[0].identifiers[identifier];
	  }
	}
	makeUrlFromId('DISCOGS_ID', 'https://www.discogs.com/release/');
	makeUrlFromId('ITUNES_ID', 'https://music.apple.com/album/');
	makeUrlFromId('APPLE_ID', 'https://music.apple.com/album/');
	makeUrlFromId('SPOTIFY_ID', 'https://open.spotify.com/album/');
	makeUrlFromId('DEEZER_ID', 'https://www.deezer.com/album/');
	const vinylTest = /^((?:Vinyl|LP) rip by\s+)(.*)$/im;
	const vinyltrackParser = /^([A-Z])[\-\.\s]?((\d+)(?:\.\d+)?)$/;
	const classicalWorkParsers = [
	  /^(.+?):\s+([IVXC]+\.\s+.*)$/,
	  /^(.*\S):\s+(.*)$/,
	];
	if (isRequestNew || isRequestEdit) { // request
	  description = []
	  if (release.release_date) {
		i = new Date(release.release_date);
		let today = new Date();
		today.setHours(0); today.setMinutes(0); today.setSeconds(0); today.setMilliseconds(0);
		description.push((i < today ? 'Released' : 'Releasing') + ' ' +
			(isNaN(i) ? release.release_date : i.toDateString()));
	  }
	  let summary = '';
	  if (totalDiscs > 1) summary += totalDiscs + 'discs, ';
	  summary += tracks.length + ' track(s)';
	  if (totalTime > 0) summary += ', ' + makeTimeString(totalTime);
	  description.push(summary);
	  if (url) description.push('[url]' + url + '[/url]');
	  if (release.catalogs.length == 1 && /^\d{10,}$/.test(release.catalogs[0])
		  || tracks.every(it => it.identifiers.BARCODE && it.identifiers.BARCODE == tracks[0].identifiers.BARCODE)
			&& /^\d{10,}$/.test(tracks[0].identifiers.BARCODE)) {
		description.push('[url=https://www.google.com/search?q=' + RegExp.lastMatch + ']Find more stores...[/url]');
	  }
	  if (release.comments.length == 1) description.push(release.comments[0]);
	  description = description.join('\n\n');
	  if (description.length > 0) {
		ref = document.getElementById('description');
		if (elementWritable(ref)) {
		  ref.value = description;
		} else if (isRequestEdit && ref != null && !ref.disabled) {
		  ref.value = ref.textLength > 0 ? ref.value.concat('\n\n', description) : ref.value = description;
		  preview(0);
		}
	  }
	} else { // upload
	  description = genPlaylist();
	  let vinylRipInfo;
	  if (release.comments.length == 1 && release.comments[0]) {
		if (matches = release.comments[0].match(vinylTest)) {
		  vinylRipInfo = release.comments[0].slice(matches.index).trim().split(/[\r\n]+/);
		  description = description.concat('\n\n', release.comments[0].slice(0, matches.index).trim());
		} else {
		  description += '\n\n' + release.comments[0];
		}
	  }
	  if (elementWritable(ref = document.getElementById('album_desc'))) {
		ref.value = description;
		preview(0);
	  }
	  if ((ref = document.getElementById('body')) != null && !ref.disabled) {
		let editioninfo;
		if (editionTitle) {
		  editioninfo = '[size=5][b]' + editionTitle;
		  if (release.release_date && (i = extract_year(release.release_date)) > 0) editioninfo += ' (' + i + ')';
		  editioninfo = editioninfo.concat('[/b][/size]\n\n');
		} else editioninfo = '';
		ref.value = ref.textLength > 0 ?
		  ref.value.concat('\n\n', editioninfo, description) : editioninfo + description;
		preview(0);
	  }
	  // Release description
	  var lineage = '', comment = '', drinfo, srcinfo;
	  if (elementWritable(ref = document.getElementById('release_samplerate'))) {
		ref.value = Object.keys(release.srs).length == 1 ? Math.floor(Object.keys(release.srs)[0] / 1000) :
		Object.keys(release.srs).length > 1 ? '999' : null;
	  }
	  if (Object.keys(release.srs).length > 0) {
		let kHz = Object.keys(release.srs).sort((a, b) => release.srs[b] - release.srs[a])
			.map(f => f / 1000).join('/').concat('kHz');
		if (release.bds.some(bd => bd > 16)) {
		  drinfo = '[hide=DR' + (release.drs.length == 1 ? release.drs[0] : '') + '][pre][/pre]';
		  if (media == 'Vinyl') {
			let hassr = ref == null || Object.keys(release.srs).length > 1;
			lineage = hassr ? kHz + ' ' : '';
			if (vinylRipInfo) {
			  vinylRipInfo[0] = vinylRipInfo[0].replace(vinylTest, '$1[color=blue]$2[/color]');
			  if (hassr) { vinylRipInfo[0] = vinylRipInfo[0].replace(/^Vinyl\b/, 'vinyl') }
			  lineage += vinylRipInfo[0] + '\n\n[u]Lineage:[/u]' + vinylRipInfo.slice(1).map(k => '\n' + k).join('');
			} else {
			  lineage += (hassr ? 'Vinyl' : ' vinyl') + ' rip by [color=blue][/color]\n\n[u]Lineage:[/u]';
			}
			drinfo += '\n\n[img][/img]\n[img][/img]\n[img][/img][/hide]';
		  } else if (['Blu-Ray', 'DVD', 'SACD'].includes(media)) {
			lineage = ref ? '' : kHz;
			if (release.channels) add_channel_info();
			if (media == 'SACD' || isFromDSD) {
			  lineage += ' from DSD64 using foobar2000\'s SACD decoder (direct-fp64)';
			  lineage += '\nOutput gain +0dB';
			}
			drinfo += '[/hide]';
			//add_rg_info();
		  } else { // WEB Hi-Res
			if (ref == null || Object.keys(release.srs).length > 1) lineage = kHz;
			if (release.channels && release.channels != 2) add_channel_info();
			if (isFromDSD) {
			  lineage += ' from DSD64 using foobar2000\'s SACD decoder (direct-fp64)';
			  lineage += '\nOutput gain +0dB';
			} else {
			  add_dr_info();
			}
			//if (lineage.length > 0) add_rg_info();
			if (release.bds.length > 1) release.bds.filter(bd => bd != 24).forEach(function(bd) {
			  let hybrid_tracks = tracks.filter(it => it.bd == bd).sort(trackComparer).map(function(it) {
				return (totalDiscs > 1 && it.discnumber ? it.discnumber + '-' : '').concat(it.tracknumber);
			  });
			  if (hybrid_tracks.length < 1) return;
			  if (lineage) lineage += '\n';
			  lineage += 'Note: track';
			  if (hybrid_tracks.length > 1) lineage += 's';
			  lineage += ' #' + hybrid_tracks.join(', ') +
				(hybrid_tracks.length > 1 ? ' are' : ' is') + ' ' + bd + 'bit lossless';
			});
			if (Object.keys(release.srs).length == 1 && Object.keys(release.srs)[0] == 88200 || isFromDSD) {
			  drinfo += '[/hide]';
			} else {
			  drinfo = null;
			}
		  }
		} else { // 16bit or lossy
		  if (Object.keys(release.srs).some(f => f != 44100)) lineage = kHz;
		  if (release.channels && release.channels != 2) add_channel_info();
		  //add_dr_info();
		  //if (lineage.length > 0) add_rg_info();
		  if (['AAC', 'Opus', 'Vorbis'].includes(release.codec) && release.vendor) {
			let _encoder_settings = release.vendor;
			if (release.codec == 'AAC' && /^qaac\s+[\d\.]+/i.test(release.vendor)) {
			  let enc = [];
			  if (matches = release.vendor.match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
			  if (matches = release.vendor.match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
			  if (matches = release.vendor.match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
			  if (matches = release.vendor.match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
			  if (matches = release.vendor.match(/\bQuality\s+(\d+)\b/i)) enc[5] = matches[1];
			  _encoder_settings = 'Converted by Apple\'s ' + enc[2] + ' encoder (' + enc[3] + '-' + enc[4] + ')';
			}
			if (lineage) lineage += '\n\n';
			lineage += _encoder_settings;
		  }
		}
	  }
	  function add_dr_info() {
		if (release.drs.length != 1 || document.getElementById('release_dynamicrange') != null) return false;
		if (lineage.length > 0) lineage += ' | ';
		if (release.drs[0] < 4) lineage += '[color=red]';
		lineage += 'DR' + release.drs[0];
		if (release.drs[0] < 4) lineage += '[/color]';
		return true;
	  }
	  function add_rg_info() {
		if (release.rgs.length != 1) return false;
		if (lineage.length > 0) lineage += ' | ';
		lineage += 'RG'; //lineage += 'RG ' + rgs[0];
		return true;
	  }
	  function add_channel_info() {
		if (!release.channels) return false;
		let chi = getChanString(release.channels);
		if (lineage.length > 0 && chi.length > 0) lineage += ', ';
		lineage += chi;
		return chi.length > 0;
	  }
	  if (url) srcinfo = '[url]' + url + '[/url]';
	  if ((ref = document.getElementById('release_lineage')) != null) {
		if (elementWritable(ref)) {
		  if (drinfo) comment = drinfo;
		  if (lineage && srcinfo) lineage += '\n\n';
		  if (srcinfo) lineage += srcinfo;
		  ref.value = lineage;
		  preview(1);
		}
	  } else {
		comment = lineage;
		if (comment && drinfo) comment += '\n\n';
		if (drinfo) comment += drinfo;
		if (comment && srcinfo) comment += '\n\n';
		if (srcinfo) comment += srcinfo;
	  }
	  if (elementWritable(ref = document.getElementById('release_desc'))) {
		ref.value = comment;
		if (comment.length > 0) preview(isNWCD ? 2 : 1);
	  }
	  if (release.encoding == 'lossless' && release.codec == 'FLAC'
		  && release.bds.includes(24) && release.dirpaths.length == 1) {
		var uri = new URL(release.dirpaths[0] + '\\foo_dr.txt');
		GM_xmlhttpRequest({
		  method: 'GET',
		  url: uri.href,
		  responseType: 'blob',
		  onload: function(response) {
			if (response.readyState != 4 || !response.responseText) return;
			var rlsDesc = document.getElementById('release_lineage') || document.getElementById('release_desc');
			if (rlsDesc == null) return;
			var value = rlsDesc.value;
			matches = value.match(/(^\[hide=DR\d*\]\[pre\])\[\/pre\]/im);
			if (matches == null) return;
			var index = matches.index + matches[1].length;
			rlsDesc.value = value.slice(0, index).concat(response.responseText, value.slice(index));
		  }
		});
	  }
	}
	if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))) {
	  if (tracks.every(track => track.identifiers.IMGURL && track.identifiers.IMGURL == tracks[0].identifiers.IMGURL)) {
		setImage(tracks[0].identifiers.IMGURL);
	  } else {
		if (/^https?:\/\/(\w+\.)?discogs\.com\/release\/[\w\-]+\/?$/i.test(url)) url += '/images';
		getCoverOnline(url);
	  }
	}
	// 	} else if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))
	// 	  && ((ref = document.getElementById('album_desc')) != null || (ref = document.getElementById('body')) != null)
	// 		&& ref.textLength > 0 && (matches = ref.value.matchAll(/\b(https?\/\/[\w\-\&\_\?\=]+)/i)) != null) {

	if (elementWritable(ref = document.getElementById('release_dynamicrange'))) {
	  ref.value = release.drs.length == 1 ? release.drs[0] : '';
	}
	if (isRequestNew && prefs.request_default_bounty > 0) {
	  let amount = prefs.request_default_bounty < 1024 ? prefs.request_default_bounty : prefs.request_default_bounty / 1024;
	  if ((ref = document.getElementById('amount_box')) != null && !ref.disabled) ref.value = amount;
	  if ((ref = document.getElementById('unit')) != null && !ref.disabled) {
		ref.value = prefs.request_default_bounty < 1024 ? 'mb' : 'gb';
	  }
	  exec(function() { Calculate() });
	}
	if (prefs.clean_on_apply) clipBoard.value = '';
	prefs.save();
	return true;

	function getChanString(n) {
	  if (!n) return null;
	  const chanmap = [
		'mono',
		'stereo',
		'2.1',
		'4.0 surround sound',
		'5.0 surround sound',
		'5.1 surround sound',
		'7.0 surround sound',
		'7.1 surround sound',
	  ];
	  return n >= 1 && n <= 8 ? chanmap[n - 1] : n + 'chn surround sound';
	}
	function genPlaylist() { // ===================================== The Playlist =====================================
	  var dur, playlist = isVA || artists[0].length < 3 ? '' : '[size=4]' +
		  joinArtists(artists[0], artist => '[artist]' + artist + '[/artist]') + ' – ' + release.album + '[/size]\n\n';
	  if (tracks.length > 1) {
		playlist += isRED ? '[pad=5|0|0|0]' : '';
		playlist += '[size=4][color=' + prefs.tracklist_head_color + '][b]Tracklisting[/b][/color][/size]';
		if (isRED) playlist += '[/pad]';
		playlist += '\n'; //'[hr]';
		let lastDisc, lastSubtitle, lastWork, lastSide, vinylTrackWidth;
		let block = 0, classicalWorks = new Map();
		if (isClassical && !tracks.some(it => it.discsubtitle)) {
		  tracks.forEach(function(track) {
			classicalWorkParsers.forEach(function(classicalWorkParser) {
			  if (track.classical_work || !classicalWorkParser.test(track.title)) return;
			  classicalWorks.set(track.classical_work = RegExp.$1, {});
			  track.classical_title = RegExp.$2;
			});
		  });
		  for (iter of classicalWorks.keys()) {
			let work = tracks.filter(track => track.classical_work == iter);
			if (array_homogenous(work.map(it => it.track_artist))) {
			  classicalWorks.get(iter).performer = work[0].track_artist;
			}
			if (array_homogenous(work.map(it => it.composer))) {
			  classicalWorks.get(iter).composer = work[0].composer;
			}
		  }
		}
		let volumes = new Map(tracks.map(it => [it.discnumber, undefined]));
		volumes.forEach(function(val, key) {
		  volumes.set(key, new Set(tracks.filter(it => it.discnumber == key).map(it => it.discsubtitle)).size)
		});
		if (!tracks.every(it => !isNaN(parseInt(it.tracknumber)))
			&& !tracks.every(it => vinyltrackParser.test(it.tracknumber.toUpperCase()))) {
		  addMessage('Warning: inconsistent tracks numbering (' + tracks.map(it => it.tracknumber) + ')', 'ua-warning');
		}
		vinylTrackWidth = tracks.reduce(function(acc, it) {
		  return Math.max(vinyltrackParser.test(it.tracknumber.toUpperCase()) && parseInt(RegExp.$3), acc);
		}, 0);
		if (vinylTrackWidth) {
		  vinylTrackWidth = vinylTrackWidth.toString().length;
		  tracks.forEach(function(it) {
			if (vinyltrackParser.test(it.tracknumber.toUpperCase()) != null)
			  it.tracknumber = RegExp.$1 + RegExp.$3.padStart(vinylTrackWidth, '0');
		  });
		  ++vinylTrackWidth;
		}

		const padStart = '[pad=0|0|5|0]';
		function prologue(prefix, postfix) {
		  function block1() {
			if (block == 3) playlist += postfix;
			playlist += '\n';
			if (isRED && ![1, 2].includes(block)) playlist += padStart;
			block = 1;
		  }
		  function block2() {
			if (block == 3) playlist += postfix;
			playlist += '\n';
			if (isRED && ![1, 2].includes(block)) playlist += padStart;
			block = 2;
		  }
		  function block3() {
			if (block == 2) playlist += '[hr]';
			if (isRED && [1, 2].includes(block)) playlist += '[/pad]';
			playlist += '\n';
			if (block != 3) playlist += prefix;
			block = 3;
		  }
		  var blockDuration;
		  if (totalDiscs > 1 && iter.discnumber != lastDisc) {
			block1();
			lastDisc = iter.discnumber;
			lastSubtitle = lastWork = undefined;
			playlist += '[color=' + prefs.tracklist_disctitle_color + '][size=3][b]';
			if (iter.identifiers.VOL_MEDIA && tracks.filter(it => it.discnumber == iter.discnumber)
				.every(it => it.identifiers.VOL_MEDIA == iter.identifiers.VOL_MEDIA)) {
			  playlist += iter.identifiers.VOL_MEDIA.toUpperCase() + ' ';
			}
			playlist += 'Disc ' + iter.discnumber;
			if (iter.discsubtitle && (volumes.get(iter.discnumber) || 0) == 1) {
			  playlist += ' – ' + iter.discsubtitle;
			  lastSubtitle = iter.discsubtitle;
			}
			playlist += '[/b][/size]';
			blockDuration = tracks.filter(it => it.discnumber == iter.discnumber)
			  .reduce((acc, it) => acc + it.duration, 0);
			if (blockDuration > 0) playlist += ' [size=2][i][' + makeTimeString(blockDuration) + '][/i][/size]';
			playlist += '[/color]';
		  }
		  if (iter.discsubtitle != lastSubtitle) {
			if (block != 1 || iter.discsubtitle) block1();
			if (iter.discsubtitle) {
			  playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]' + iter.discsubtitle + '[/b][/size]';
			  blockDuration = tracks.filter(it => it.discsubtitle == iter.discsubtitle)
				.reduce((acc, it) => acc + it.duration, 0);
			  if (blockDuration > 0) playlist += ' [size=1][i][' + makeTimeString(blockDuration) + '][/i][/size]';
			  playlist += '[/color]';
			}
			lastSubtitle = iter.discsubtitle;
		  }
		  if (iter.classical_work != lastWork) {
			if (iter.classical_work) {
			  block2();
			  playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
			  if (release.composers.length != 1 && classicalWorks.get(iter.classical_work).composer) {
				playlist += classicalWorks.get(iter.classical_work).composer + ': ';
			  }
			  playlist += iter.classical_work;
			  playlist += '[/b]';
			  if (classicalWorks.get(iter.classical_work).performer
				  && classicalWorks.get(iter.classical_work).performer != release.artist) {
				playlist += ' (' + classicalWorks.get(iter.classical_work).performer + ')';
			  }
			  playlist += '[/size]';
			  blockDuration = tracks.filter(it => it.classical_work == iter.classical_work)
				.reduce((acc, it) => acc + it.duration, 0);
			  if (blockDuration > 0) playlist += ' [size=1][i][' + makeTimeString(blockDuration) + '][/i][/size]';
			  playlist += '[/color]';
			} else {
			  if (block > 2) block1();
			}
			lastWork = iter.classical_work;
		  }
		  if (vinyltrackParser.test(iter.tracknumber)) {
			if (block == 3 && lastSide && RegExp.$1 != lastSide) playlist += '\n';
			lastSide = RegExp.$1;
		  }
		  block3();
		} // prologue

		for (iter of tracks.sort(trackComparer)) {
		  let title = '';
		  let ttwidth = vinylTrackWidth || (totalDiscs > 1 && iter.discnumber ?
											tracks.filter(it => it.discnumber == iter.discnumber) : tracks)
		  .reduce((accumulator, it) => Math.max(accumulator, iter.tracknumber.toString().length), 2);
		  if (prefs.tracklist_style == 1) { // STYLE 1 ----------------------------------------
			prologue('[size=2]', '[/size]\n');
			track = '[b][color=' + prefs.tracklist_tracknumber_color + ']';
			track += isNaN(parseInt(iter.tracknumber)) ? iter.tracknumber : iter.tracknumber.padStart(ttwidth, '0');
			track += '[/color][/b]' + prefs.title_separator;
			if (iter.track_artist && iter.track_artist != release.artist
				&& (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
			  title = '[color=' + prefs.tracklist_artist_color + ']' + iter.track_artist + '[/color] - ';
			}
			title += iter.classical_title || iter.title;
			if (iter.composer && composerEmphasis && release.composers.length != 1
				&& (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
			  title = title.concat(' [color=', prefs.tracklist_composer_color, '](', iter.composer, ')[/color]');
			}
			playlist += track + title;
			if (iter.duration) playlist += ' [i][color=' + prefs.tracklist_duration_color +'][' +
			  makeTimeString(iter.duration) + '][/color][/i]';
		  } else if (prefs.tracklist_style == 2) { // STYLE 2 ----------------------------------------
			prologue('[size=2][pre]', '[/pre][/size]');
			track = isNaN(parseInt(iter.tracknumber)) ? iter.tracknumber : iter.tracknumber.padStart(ttwidth, '0');
			track += prefs.title_separator;
			if (iter.track_artist && iter.track_artist != release.artist
				&& (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
			  title = iter.track_artist + ' - ';
			}
			title += iter.classical_title || iter.title;
			if (composerEmphasis && iter.composer && release.composers.length != 1
				&& (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
			  title = title.concat(' (', iter.composer, ')');
			}
			dur = iter.duration ? '[' + makeTimeString(iter.duration) + ']' : null;
			let l = 0, j, left, padding, spc;
			let width = prefs.max_tracklist_width - track.length;
			if (dur) width -= dur.length + 1;
			while (title.length > 0) {
			  j = width;
			  if (title.length > width) {
				while (j > 0 && title[j] != ' ') { --j }
				if (j <= 0) j = width;
			  }
			  left = title.slice(0, j).trim();
			  if (++l <= 1) {
				playlist += track + left;
				if (dur) {
				  spc = width - left.length;
				  padding = (spc < 2 ? ' '.repeat(spc) : ' ' + prefs.pad_leader.repeat(spc - 1)) + ' ';
				  playlist += padding + dur;
				}
				width = prefs.max_tracklist_width - track.length - 2;
			  } else {
				playlist += '\n' + ' '.repeat(track.length) + left;
			  }
			  title = title.slice(j).trim();
			}
		  }
		}
		if (prefs.tracklist_style == 1 && totalTime > 0) {
		  playlist += '\n\n' + divs[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
			']Total time: [i]' + makeTimeString(totalTime) + '[/i][/color][/size]';
		} else if (prefs.tracklist_style == 2) {
		  if (totalTime > 0) {
			dur = '[' + makeTimeString(totalTime) + ']';
			playlist += '\n\n' + divs[0].repeat(32).padStart(prefs.max_tracklist_width);
			playlist += '\n' + 'Total time:'.padEnd(prefs.max_tracklist_width - dur.length) + dur;
		  }
		  playlist += '[/pre][/size]';
		}
	  } else { // single
		playlist += '[align=center]';
		playlist += isRED ? '[pad=20|20|20|20]' : '';
		playlist += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + release.artist + '[/color][hr]';
		//playlist += '[color=' + prefs.tracklist_single_color + ']';
		playlist += tracks[0].title;
		//playlist += '[/color]'
		playlist += '[/b]';
		if (tracks[0].composer) {
		  playlist += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
		}
		playlist += '\n\n[color=' + prefs.tracklist_duration_color +'][' +
		  makeTimeString(tracks[0].duration) + '][/color][/size]';
		if (isRED) playlist += '[/pad]';
		playlist += '[/align]';
	  }
	  return playlist;
	}
	function init_from_url_music(url, weak = false) {
	  if (!/^https?:\/\//i.test(url)) return false;
	  var artist, album, albumYear, releaseDate, channels, label, composer, bd, sr = 44.1,
		  description, compiler, producer, totalTracks, discSubtitle, discNumber, trackNumber,
		  title, trackArtist, catalogue, encoding, format, bitrate, duration, country;
	  if (url.toLowerCase().includes('qobuz.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  var error = new Error('Error parsing Qobus release page'), mainArtist;
		  if ((ref = dom.querySelector('div.album-meta > h2.album-meta__artist')) == null) throw error;
		  artist = ref.title || ref.textContent.trim();
		  if ((ref = dom.querySelector('div.album-meta > h1.album-meta__title')) == null) throw error;
		  album = ref.title || ref.textContent.trim();
		  ref = dom.querySelector('div.album-meta > ul > li:first-of-type');
		  if (ref != null) releaseDate = normalizeDate(ref.textContent);
		  ref = dom.querySelector('div.album-meta > ul > li:nth-of-type(2) > a');
		  if (ref != null) mainArtist = ref.title || ref.textContent.trim();
		  ref = dom.querySelector('p.album-about__copyright');
		  albumYear = ref != null && extract_year(ref.textContent) || extract_year(releaseDate);
		  let genres = [];
		  dom.querySelectorAll('section#about > ul > li').forEach(function(it) {
			function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) }
			if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1);
			if (/\b(\d+)\s*(?:track|pist[ae]|tracce)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1);
			if (it.textContent.trimLeft().startsWith('Label')) label = it.children[0].textContent.trim()
			else if (['Composer', 'Compositeur', 'Komponist'].some(matchLabel)) {
			  composer = it.children[0].textContent.trim();
			  if (/\bVarious\b/i.test(composer)) composer = null;
			} else if (it.textContent.startsWith('Genre') && it.children.length > 0) {
			  it.querySelectorAll('a').forEach(it => { genres.push(it.textContent.trim()) });
			  if (genres.length > 0 && ['Pop/Rock'].includes(genres[0])) genres.shift();
			  if (genres.length > 0 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) {
				while (genres.length > 1) genres.shift();
			  }
			}
		  });
  		  bd = 16; channels = 2;
		  dom.querySelectorAll('span.album-quality__info').forEach(function(k) {
			if (/\b([\d\.\,]+)\s*kHz\b/i.test(k.textContent) != null) sr = parseFloat(RegExp.$1.replace(',', '.'));
			if (/\b(\d+)[\-\s]*Bits?\b/i.test(k.textContent) != null) bd = parseInt(RegExp.$1);
			if (/\b(?:Stereo)\b/i.test(k.textContent)) channels = 2;
			if (/\b(\d)\.(\d)\b/.test(k.textContent)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
		  });
		  get_desc_from_node('section#description > p', response.finalUrl, true);
		  if ((ref = dom.querySelector('a[title="Qobuzissime"]')) != null) {
			description += '\x1C[align=center][url=https://www.qobuz.com' + ref.pathname +
			  '][img]https://ptpimg.me/4z35uj.png[/img][/url][/align]';
		  }
		  ref = dom.querySelectorAll('div.player__tracks > div.track > div.track__items');
		  let works = dom.querySelectorAll('div.player__tracks > p.player__work');
		  if (!totalTracks) totalTracks = ref.length;
		  ref.forEach(function(k) {
			discSubtitle = null;
			works.forEach(function(j) {
			  if (j.compareDocumentPosition(k) == Node.DOCUMENT_POSITION_FOLLOWING) discSubtitle = j
			});
			discSubtitle = discSubtitle != null ? discSubtitle.textContent.trim() : undefined;
			if (/^\s*(?:dis[ck]|disco|disque)\s+(\d+)\s*$/i.test(discSubtitle)) {
			  discNumber = parseInt(RegExp.$1);
			  discSubtitle = undefined;
			} else discNumber = undefined;
			if (discNumber > totalDiscs) totalDiscs = discNumber;
			trackNumber = parseInt(k.querySelector('span[itemprop="position"]').textContent.trim());
			title = k.querySelector('span.track__item--name').textContent.trim().replace(/\s+/g, ' ');
			duration = timeStringToTime(k.querySelector('span.track__item--duration').textContent);
			trackArtist = undefined;
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  undefined, // catalogue
			  undefined, // country
			  'lossless',
			  'FLAC',
			  undefined,
			  undefined,
			  bd,
			  sr * 1000,
			  channels,
			  'WEB',
			  genres.join('; '),
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  description,
			  undefined,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (url.toLowerCase().includes('highresaudio.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  ref = dom.querySelector('h1 > span.artist');
		  if (ref != null) artist = ref.textContent.trim();
		  ref = dom.getElementById('h1-album-title');
		  if (ref != null) album = ref.firstChild.textContent.trim();
		  let genres = [], format;
		  dom.querySelectorAll('div.album-col-info-data > div > p').forEach(function(k) {
			if (/\b(?:Genre|Subgenre)\b/i.test(k.firstChild.textContent)) genres.push(k.lastChild.textContent.trim());
			if (/\b(?:Label)\b/i.test(k.firstChild.textContent)) label = k.lastChild.textContent.trim();
			if (/\b(?:Album[\s\-]Release)\b/i.test(k.firstChild.textContent)) {
			  albumYear = normalizeDate(k.lastChild.textContent);
			}
			if (/\b(?:HRA[\s\-]Release)\b/i.test(k.firstChild.textContent)) {
			  releaseDate = normalizeDate(k.lastChild.textContent);
			}
		  });
		  i = 0;
		  dom.querySelectorAll('tbody > tr > td.col-format').forEach(function(k) {
			if (/^(FLAC)\s*([\d\.\,]+)\b/.exec(k.textContent) != null) {
			  format = RegExp.$1;
			  sr = parseFloat(RegExp.$2.replace(/,/g, '.'));
			  ++i;
			}
		  });
		  if (i > 1) sr = undefined; // ambiguous
		  get_desc_from_node('div#albumtab-info > p', response.finalUrl);
		  ref = dom.querySelectorAll('ul.playlist > li.pltrack');
		  totalTracks = ref.length;
		  ref.forEach(function(k) {
			discSubtitle = k;
			while ((discSubtitle = discSubtitle.previousElementSibling) != null) {
			  if (discSubtitle.nodeName == 'LI' && discSubtitle.className == 'plinfo') {
				discSubtitle = discSubtitle.textContent.replace(/\s*:$/, '').trim();
				if (/\b(?:DIS[CK]|Volume|CD)\s*(\d+)\b/i.exec(discSubtitle)) discNumber = parseInt(RegExp.$1);
				break;
			  }
			}
			//if (discnumber > totalDiscs) totalDiscs = discnumber;
			trackNumber = parseInt(k.querySelector('span.track').textContent.trim());
			title = k.querySelector('span.title').textContent.trim().replace(/\s+/g, ' ');
			duration = timeStringToTime(k.querySelector('span.time').textContent);
			trackArtist = undefined;
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  undefined, // catalogue
			  undefined, // country
			  'lossless',
			  'FLAC', //format,
			  undefined,
			  undefined,
			  24,
			  sr * 1000,
			  2,
			  'WEB',
			  genres.join('; '),
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  description,
			  undefined,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (url.toLowerCase().includes('bandcamp.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  ref = dom.querySelector('span[itemprop="byArtist"] > a');
		  if (ref != null) artist = ref.textContent.trim();
		  ref = dom.querySelector('h2[itemprop="name"]');
		  if (ref != null) album = ref.textContent.trim();
		  ref = dom.querySelector('div.tralbum-credits');
		  if (ref != null && /\breleased\s+(.*?\b\d{4})\b/i.test(ref.textContent)) {
			releaseDate = RegExp.$1;
			albumYear = releaseDate;
		  }
		  ref = dom.querySelector('p#band-name-location > span.title');
		  if (ref != null) label = ref.textContent.trim();
		  let tags = new TagManager;
		  dom.querySelectorAll('div.tralbum-tags > a.tag').forEach(k => { tags.add(k.textContent.trim()) });
		  description = [];
		  dom.querySelectorAll('div.tralbumData').forEach(function(k) {
			if (!k.classList.contains('tralbum-tags')) description.push(html2php(k, response.finalUrl))
		  });
		  description = description.join('\n\n').replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
		  ref = dom.querySelectorAll('table.track_list > tbody > tr[itemprop="tracks"]');
		  totalTracks = ref.length;
		  ref.forEach(function(k) {
			trackNumber = parseInt(k.querySelector('div.track_number').textContent);
			title = k.querySelector('span.track-title').textContent.trim().replace(/\s+/g, ' ');
			duration = timeStringToTime(k.querySelector('span.time').textContent);
			trackArtist = undefined;
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  undefined, // catalogue
			  undefined, // country
			  undefined, //'lossless',
			  undefined, //'FLAC',
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  2,
			  'WEB',
			  tags.toString(),
			  discNumber,
			  totalDiscs,
			  undefined,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  description,
			  undefined,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (url.toLowerCase().includes('prestomusic.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  artist = getArtists(dom.querySelectorAll('div.c-product-block__contributors > p'));
		  ref = dom.querySelector('h1.c-product-block__title');
		  if (ref != null) album = ref.lastChild.textContent.trim();
		  dom.querySelectorAll('div.c-product-block__metadata > ul > li').forEach(function(k) {
			if (k.firstChild.textContent.includes('Release Date')) {
			  releaseDate = extract_year(k.lastChild.textContent);
			} else if (k.firstChild.textContent.includes('Label')) {
			  label = k.lastChild.textContent.trim();
			} else if (k.firstChild.textContent.includes('Catalogue No')) {
			  catalogue = k.lastChild.textContent.trim();
			}
		  });
		  albumYear = releaseDate;
		  var genre;
		  if (/\/jazz\//i.test(response.finalUrl)) genre = 'Jazz';
		  if (/\/classical\//i.test(response.finalUrl)) genre = 'Classical';
		  get_desc_from_node('div#about > div > p', response.finalUrl, true);
		  ref = dom.querySelectorAll('div#related > div > ul > li');
		  composer = [];
		  ref.forEach(function(k) {
			if (k.parentNode.previousElementSibling.textContent.includes('Composers')) {
			  composer.push(k.firstChild.textContent.trim().replace(/^(.*?)\s*,\s+(.*)$/, '$2 $1'));
			}
		  });
		  composer = composer.join(', ') || undefined;
		  ref = dom.querySelectorAll('div.has--sample');
		  totalTracks = ref.length;
		  trackNumber = 0;
		  ref.forEach(function(it) {
			trackNumber = ++trackNumber;
			title = it.querySelector('p.c-track__title').textContent.trim().replace(/\s+/g, ' ');
			duration = timeStringToTime(it.querySelector('div.c-track__duration').textContent);
			let parent = it;
			if (it.classList.contains('c-track')) {
			  parent = it.parentNode.parentNode;
			  if (parent.classList.contains('c-expander')) parent = parent.parentNode;
			  discSubtitle = parent.querySelector(':scope > div > div > div > p.c-track__title');
			  discSubtitle = discSubtitle != null ? discSubtitle.textContent.trim().replace(/\s+/g, ' ') : undefined;
			} else {
			  discSubtitle = null;
			}
			trackArtist = getArtists(parent.querySelectorAll(':scope > div.c-track__details > ul > li'));
			if (trackArtist.equalTo(artist)) trackArtist = [];
			track = [
			  artist.join('; '),
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  catalogue,
			  undefined, // country
			  undefined, // encoding
			  undefined, // format
			  undefined,
			  undefined, // bitrate
			  undefined, // BD
			  undefined, // SR
			  2,
			  'WEB',
			  genre,
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist.join(', '),
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  description,
			  undefined,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();

		  function getArtists(nodeList) {
			var artists = [];
			nodeList.forEach(function(it) {
			  if (it.textContent.startsWith('Record')) return;
			  splitArtists(it.textContent.trim()).forEach(function(it) {
				artists.push(it.replace(/\s*\([^\(\)]*\)$/, ''));
			  });
			});
			return artists;
		  }
		} });
		return true;
	  } else if (url.toLowerCase().includes('discogs.com/') && /\/releases?\/(\d+)\b/i.test(url)) {
		GM_xmlhttpRequest({ method: 'GET', url: 'https://api.discogs.com/releases/' + RegExp.$1, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  var json = JSON.parse(response.responseText);
		  const removeArtistNdx = /\s*\(\d+\)$/;
		  function getArtists(root) {
			function filterArtists(rx, anv = true) {
			  return root.extraartists instanceof Array && rx instanceof RegExp ?
				root.extraartists
				.filter(it => rx.test(it.role))
				.map(it => (anv && it.anv || it.name || '').replace(removeArtistNdx, '')) : [];
			}
			var artists = [];
			for (var ndx = 0; ndx < 7; ++ndx) artists[ndx] = [];
			ndx = 0;
			if (root.artists) root.artists.forEach(function(it) {
			  artists[ndx].push((it.anv || it.name).replace(removeArtistNdx, ''));
			  if (/^feat/i.test(it.join)) ndx = 1;
			});
			return [
			  artists[0],
			  artists[1].concat(filterArtists(/^(?:featuring)$/i)),
			  artists[2].concat(filterArtists(/\b(?:Remixed[\s\-]By|Remixer)\b/i)),
			  artists[3].concat(filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, false)),
			  artists[4].concat(filterArtists(/\b(?:Conducted[\s\-]By|Conductor)\b/i)),
			  artists[5].concat(filterArtists(/\b(?:Compiled[\s\-]By|Compiler)\b/i)),
			  artists[6].concat(filterArtists(/\b(?:Produced[\s\-]By|Producer)\b/i)),
			  // filter off from performers
			  filterArtists(/\b(?:(?:Mixed)[\s\-]By|Mixer)\b/i),
			  filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, true),
			];
		  }
		  var albumArtists = getArtists(json);
		  if (albumArtists[0].length > 0) {
			artist = albumArtists[0].join('; ');
			if (albumArtists[1].length > 0) artist += ' feat. ' + albumArtists[1].join('; ');
		  }
		  album = json.title;
		  var editions = [];
		  if (editions.length > 0) album += ' (' + editions.join(' / ') + ')';
		  label = [];
		  catalogue = [];
		  json.labels.forEach(function(it) {
			//if (it.entity_type_name != 'Label') return;
			if (!/^Not On Label\b/i.test(it.name)) label.pushUniqueCaseless(it.name.replace(removeArtistNdx, ''));
			catalogue.pushUniqueCaseless(it.catno);
		  });
		  description = '';
		  if (json.companies && json.companies.length > 0) {
			description = '[b]Companies, etc.[/b]\n';
			let type_names = new Set(json.companies.map(it => it.entity_type_name));
			type_names.forEach(function(type_name) {
			  description += '\n' + type_name + ' – ' + json.companies
				.filter(it => it.entity_type_name == type_name)
			  	.map(function(it) {
				  var result = '[url=https://www.discogs.com/label/' + it.id + ']' +
					  it.name.replace(removeArtistNdx, '') + '[/url]';
				  if (it.catno) result += ' – ' + it.catno;
				  return result;
			  	})
			  	.join(', ');
			});
		  }
		  if (json.extraartists && json.extraartists.length > 0) {
			if (description) description += '\n\n';
			description += '[b]Credits[/b]\n';
			let roles = new Set(json.extraartists.map(it => it.role));
			roles.forEach(function(role) {
			  description += '\n' + role + ' – ' + json.extraartists
				.filter(it => it.role == role)
			  	.map(function(it) {
				  var result = '[url=https://www.discogs.com/artist/' + it.id + ']' +
					  (it.anv || it.name).replace(removeArtistNdx, '') + '[/url]';
				  if (it.tracks) result += ' (tracks: ' + it.tracks + ')';
				  return result;
			  	})
			  	.join(', ');
			});
		  }
		  if (json.notes) {
			if (description) description += '\n\n';
			description += '[b]Notes[/b]\n\n' + json.notes.trim();
		  }
		  if (json.identifiers && json.identifiers.length > 0) {
			if (description) description += '\n\n';
			description += '[b]Barcode and Other Identifiers[/b]\n';
			json.identifiers.forEach(function(it) {
			  description += '\n' + it.type;
			  if (it.description) description += ' (' + it.description + ')';
			  description += ': ' + it.value;
			});
		  }
		  var identifiers = ['DISCOGS_ID=' + json.id];
		  [
			['Single', 'Single'],
			['EP', 'EP'],
			['Compilation', 'Compilation'],
			['Soundtrack', 'Soundtrack'],
		  ].forEach(function(k) {
			if (json.formats.every(it => it.descriptions && it.descriptions.includesCaseless(k[0]))) {
			  identifiers.push('RELEASETYPE=' + k[1]);
			}
		  });
		  json.identifiers.forEach(function(it) {
			identifiers.push(it.type.replace(/\W+/g, '_').toUpperCase() + '=' + it.value.replace(/\s/g, '\x1B'));
		  });
		  json.formats.forEach(function(it) {
			if (it.descriptions) it.descriptions.forEach(function(it) {
			  if (/^(?:.+?\s+Edition|Remaster(?:ed)|Reissue|.+?\s+Release|Enhanced|Promo)$/.test(it)) {
				editions.push(it);
			  }
			});
			if (media) return;
			if (it.name.includes('File')) {
			  if (['FLAC', 'WAV', 'AIF', 'AIFF', 'PCM'].some(k => it.descriptions.includes(k))) {
				media = 'WEB'; encoding = 'lossless'; format = 'FLAC';
			  } else if (it.descriptions.includes('AAC')) {
				media = 'WEB'; encoding = 'lossy'; format = 'AAC'; bd = undefined;
				if (/(\d+)\s*kbps\b/i.test(it.text)) bitrate = parseInt(RegExp.$1);
			  } else if (it.descriptions.includes('MP3')) {
				media = 'WEB'; encoding = 'lossy'; format = 'MP3'; bd = undefined;
				if (/(\d+)\s*kbps\b/i.test(it.text)) bitrate = parseInt(RegExp.$1);
			  }
			} else if (['CD', 'DVD', 'Vinyl', 'LP', '7"', '12"', '10"', '5"', 'SACD', 'Hybrid', 'Blu',
						'Cassette','Cartridge', 'Laserdisc', 'VCD'].some(k => it.name.includes(k))) media = it.name;
		  });
		  json.tracklist.forEach(function(track) {
			if (track.type_.toLowerCase() == 'heading') {
			  discSubtitle = track.title;
			} else if (track.type_.toLowerCase() == 'track') {
			  if (/^([a-zA-Z]+)?(\d+)-(\w+)$/.test(track.position)) {
				if (RegExp.$1) identifiers.push('VOL_MEDIA=' + RegExp.$1.replace(/\s/g, '\x1B'));
				discNumber = RegExp.$2;
				trackNumber = RegExp.$3;
			  } else {
				discNumber = undefined;
				trackNumber = track.position;
			  }
			  let trackArtists = getArtists(track);
			  if (trackArtists[0].length > 0 && !trackArtists[0].equalTo(albumArtists[0])
				  || trackArtists[1].length > 0 && !trackArtists[1].equalTo(albumArtists[1])) {
				trackArtist = (trackArtists[0].length > 0 ? trackArtists : albumArtists)[0].join('; ');
				if (trackArtists[1].length > 0) trackArtist += ' feat. ' + trackArtists[1].join('; ');
			  } else {
				trackArtist = null;
			  }
			  let performer = track.extraartists instanceof Array && track.extraartists
			  	.map(it => (track.anv || track.name).replace(removeArtistNdx, ''))
			  	.filter(function(artist) {
				  return !albumArtists.slice(2).some(it => it instanceof Array && it.includes(artist))
				  	&& !trackArtists.slice(2).some(it => it instanceof Array && it.includes(artist))
				});
			  track = [
				artist,
				album,
				json.year,
				json.released,
				label.join(' / '),
				catalogue.join(' / '),
				json.country,
				encoding,
				format,
				undefined,
				bitrate,
				bd,
				undefined, // samplerate
				undefined, // channels
				media,
				(json.genres ? json.genres.join('; ') : '') + (json.styles ? ' | ' + json.styles.join('; ') : ''),
				discNumber,
				json.format_quantity,
				discSubtitle,
				trackNumber,
				json.tracklist.length,
				track.title,
				trackArtist,
				performer instanceof Array && performer.join('; ') || undefined,
				stringyfyRole(3), // composers
				stringyfyRole(4), // conductors
				stringyfyRole(2), // remixers
				stringyfyRole(5), // DJs/compilers
				stringyfyRole(6), // producers
				timeStringToTime(track.duration),
				undefined,
				undefined,
				undefined,
				undefined,
				undefined, //'https://www.discogs.com/release/' + json.id,
				undefined,
				description.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D'),
				identifiers.join(' '),
			  ];
			  tracks.push(track.join('\x1E'));

			  function stringyfyRole(ndx) {
				return (trackArtists[ndx] instanceof Array && trackArtists[ndx].length > 0 ?
						trackArtists : albumArtists)[ndx].join('; ');
			  }
			}
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (url.toLowerCase().includes('supraphonline.cz')) {
		GM_xmlhttpRequest({ method: 'GET', url: url.replace(/\?.*$/, ''), onload: function(response) {
		  if (response.readyState != 4 || response.status != 200)
			throw new Error('Response error ' + response.status + ' (' + response.statusText + ')');
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
		  var genre, ndx, conductor = [], origin = new URL(response.finalUrl).origin;
		  artist = [];
		  dom.querySelectorAll('h2.album-artist > a').forEach(function(it) {
			artist.pushUnique(it.title);
		  });
		  isVA = false;
		  if (artist.length == 0 && (ref = dom.querySelector('h2.album-artist[title]')) != null) {
			if (vaParser.test(ref.title)) isVA = true;
		  }
		  ref = dom.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]');
		  if (ref != null && vaParser.test(ref.content)) isVA = true;
		  if (isVA) artist = [];
		  if ((ref = dom.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
		  if ((ref = dom.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
		  if ((ref = dom.querySelector('meta[itemprop="genre"]')) != null) genre = ref.content;
		  if ((ref = dom.querySelector('li.album-version > div.selected > div')) != null) {
			if (/\b(?:CD)\b/.test(ref.textContent)) { media = 'CD'; }
			if (/\b(?:LP)\b/.test(ref.textContent)) { media = 'Vinyl'; }
			if (/\b(?:MP3)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossy'; format = 'MP3'; }
			if (/\b(?:FLAC)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 16; }
			if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 24; }
		  }
		  dom.querySelectorAll('ul.summary > li').forEach(function(it) {
			if (it.children.length < 1) return;
			if (it.children[0].textContent.includes('Nosič')) media = it.lastChild.textContent.trim();
			if (it.children[0].textContent.includes('Datum vydání')) releaseDate = normalizeDate(it.lastChild.textContent);
			if (it.children[0].textContent.includes('První vydání')) albumYear = extract_year(it.lastChild.data);
			//if (it.children[0].textContent.includes('Žánr')) genre = it.lastChild.textContent.trim();
			if (it.children[0].textContent.includes('Vydavatel')) label = it.lastChild.textContent.trim();
			if (it.children[0].textContent.includes('Katalogové číslo')) catalogue = it.lastChild.textContent.trim();
			if (it.children[0].textContent.includes('Formát')) {
			  if (/\b(?:FLAC|WAV|AIFF?)\b/.test(it.lastChild.textContent)) { encoding = 'lossless'; format = 'FLAC'; }
			  if (/\b(\d+)[\-\s]?bits?\b/i.test(it.lastChild.textContent)) bd = parseInt(RegExp.$1);
			  if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(it.lastChild.textContent)) sr = parseFloat(RegExp.$1.replace(',', '.'));
			}
			if (it.children[0].textContent.includes('Celková stopáž')) totalTime = timeStringToTime(it.lastChild.textContent.trim());
			if (copyrightParser.test(it.children[0].textContent) && !albumYear) albumYear = extract_year(it.lastChild.data);
		  });
		  const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
		  artists = [];
		  for (i = 0; i < 4; ++i) artists[i] = {};
		  dom.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;
			  let role;
			  if (ndx == 2) role = 'ensemble';
			  	else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
			  if ((ref = it.querySelector('a')) != null) {
				if (!(artists[ndx][role] instanceof Array)) artists[ndx][role] = [];
				var href = new URL(ref.href);
				artists[ndx][role].pushUnique([ref.textContent.trim(), origin + href.pathname]);
			  }
			}
		  });
		  get_desc_from_node('div[itemprop="description"] p', response.finalUrl, true);
		  composer = [];
		  var performers = [], DJs = [];
		  function dumpArtist(ndx, role) {
			if (!role || role == 'undefined') return;
			if (description.length > 0) description += '\x1C' ;
			description += '[color=#9576b1]' + role + '[/color] – ';
			//description += artists[ndx][role].map(artist => '[artist]' + artist[0] + '[/artist]').join(', ');
			description += artists[ndx][role].map(artist => '[url=' + artist[1] + ']' + artist[0] + '[/url]').join(', ');
		  }
		  for (i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
			var a = artists[i][role].map(a => a[0]);
			artist.pushUnique(...a);
			(['conductor', 'choirmaster'].includes(role) ? conductor : role == 'DJ' ? DJs : performers).pushUnique(...a);
			if (i != 2) dumpArtist(i, role);
		  });
		  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))));
			dumpArtist(0, role);
		  });
		  Object.keys(artists[3]).forEach(role => { dumpArtist(3, role) }); // ADC & mastering
		  var promises = [];
		  dom.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(row) {
			promises.push(row.id && (ref = row.querySelector('td > a.trackdetail')) != null ? new Promise(function(resolve, reject) {
			  var id = parseInt(row.id.replace(/^track-/i, ''));
			  GM_xmlhttpRequest({
				method: 'GET',
				url: origin + ref.pathname + ref.search,
				context: id,
				onload: function(response) {
				  if (response.readyState == 4 || response.status == 200) {
					var domDetail = domParser.parseFromString(response.responseText, 'text/html');
					var track = domDetail.getElementById('track-' + response.context);
					if (track != null) {
					  resolve([track, domDetail.querySelector('div[data-swap="trackdetail-' + response.context + '"] > div > div.row')]);
					} else reject('Track detail not located');
				  } else {
					reject('Response error ' + response.status + ' (' + response.statusText + ')');
				  }
				},
				onerror: response => { reject('Response error ' + response.status + ' (' + response.statusText + ')') },
				ontimeout: function() { reject('Timeout') },
			  });
			}) : Promise.resolve([row, null]));
		  });
		  Promise.all(promises).then(function(rows) {
			rows.forEach(function(tr) {
			  if (!(tr[0] instanceof HTMLElement)) throw new Error('Assertion failed: tr[0] != HTMLElement');
			  if (tr[0].id && tr[0].classList.contains('track')) {
				tr[2] = [];
				for (i = 0; i < 8; ++i) tr[2][i] = [];
				if (!(tr[1] instanceof HTMLElement)) return;
				tr[1].querySelectorAll('div[class]:nth-of-type(2) > ul > li > span').forEach(function(li) {
				  function oneOf(...arr) { return arr.some(role => key == role) }
				  var key = translateRole(li);
				  var val = li.nextElementSibling.textContent.trim();
				  if (pseudoArtistParsers.some(rx => rx.test(val))) return;
				  if (key.startsWith('remix')) {
					tr[2][2].pushUnique(val);
				  } else if (oneOf('music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto', 'music improvisation', 'author')) {
					tr[2][3].pushUnique(val);
				  } else if (oneOf('conductor', 'choirmaster')) {
					tr[2][4].pushUnique(val);
				  } else if (key == 'DJ') {
					tr[2][5].pushUnique(val);
				  } else if (key == 'produced by') {
					tr[2][6].pushUnique(val);
				  } else if (key == 'recorded by') {
				  } else {
					tr[2][7].pushUnique(val);
				  }
				});
			  }
			});
			var guests = rows.filter(tr => tr.length >= 3).map(it => it[2][7])
				.reduce((acc, trpf) => trpf.filter(trpf => acc.includes(trpf)))
				.filter(it => !artist.includes(it));
			rows.forEach(function(tr) {
			  if (tr[0].classList.contains('cd-header')) {
				discNumber = /\b\d+\b/.test(tr[0].querySelector('h3').firstChild.data.trim())
				&& parseInt(RegExp.lastMatch) || undefined;
			  }
			  if (tr[0].classList.contains('song-header')) {
				discSubtitle = tr[0].children[0].title.trim() || undefined;
			  }
			  if (tr[0].id && tr[0].classList.contains('track')) {
				var copyright, trackGenre, trackYear, recordPlace, recordDate, identifiers = '';
				if (/^track-(\d+)$/i.test(tr[0].id)) identifiers = 'TRACK_ID=' + RegExp.$1;
				trackNumber = /^\s*(\d+)\.?\s*$/.test(tr[0].children[0].firstChild.textContent) ?
				  parseInt(RegExp.$1) : undefined;
				title = tr[0].querySelector('meta[itemprop="name"]').content;
				duration = (ref = tr[0].querySelector('meta[itemprop="duration"]')) != null
					&& /^PT(\d+)H(\d+)M(\d+)S$/i.test(ref.content) ?
				  parseInt(RegExp.$1 || 0) * 60**2 +
				  parseInt(RegExp.$2 || 0) * 60 +
				  parseInt(RegExp.$3 || 0) : undefined;
				if (tr[1] instanceof HTMLElement) {
				  tr[1].querySelectorAll('div[class]:nth-of-type(1) > ul > li > span').forEach(function(li) {
					if (li.textContent.startsWith('Nahrávka dokončena')) {
					  identifiers += ' RECYEAR=' + extract_year(recordDate = li.nextSibling.data.trim());
					}
					if (li.textContent.startsWith('Místo nahrání')) {
					  recordPlace = li.nextSibling.data.trim();
					}
					if (li.textContent.startsWith('Rok prvního vydání')) {
					  identifiers += ' PUBYEAR=' + (trackYear = parseInt(li.nextSibling.data));
					}
					//if (copyrightParser.test(li.textContent)) copyright = li.nextSibling.data.trim();
					if (li.textContent.startsWith('Žánr')) trackGenre = li.nextSibling.data.trim();
				  });
				}
				if (!isVA && tr[2][0].equalTo(artist)) tr[2][0] = [];
				track = [
				  isVA ? 'Various Artists' : artist.join('; '),
				  album,
				  /*trackYear || */albumYear || undefined,
				  releaseDate,
				  label,
				  catalogue,
				  undefined, // country
				  encoding,
				  format,
				  undefined,
				  undefined,
				  bd,
				  sr * 1000,
				  2,
				  media,
				  translateGenre(genre) + ' | ' + translateGenre(trackGenre),
				  discNumber,
				  totalDiscs,
				  discSubtitle,
				  trackNumber,
				  totalTracks,
				  title,
				  joinArtists(tr[2][0]),
				  tr[2][7].join('; ') || performers.join('; '),
				  tr[2][3].join(', ') || composer.join(', '),
				  tr[2][4].join('; ') || conductor.join('; '),
				  tr[2][2].join('; '),
				  tr[2][5].join('; ') || DJs.join('; '),
				  tr[2][6].join('; '),
				  duration,
				  undefined,
				  undefined,
				  undefined,
				  undefined,
				  response.finalUrl,
				  undefined,
				  description,
				  identifiers,
				];
				tracks.push(track.join('\x1E'));
			  }
			});
			clipBoard.value = tracks.join('\n');
			fill_from_text_music();
		  }).catch(e => { alert(e) });

		  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'],
			  ['Pro děti', 'Children'],
			  ['Pro dospělé', 'Adult'],
			  ['Mluvené slovo', 'Spoken Word'],
			  ['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) {
			if (!(elem instanceof HTMLElement)) return undefined;
			var role = elem.textContent.trim().toLowerCase().replace(/\s*:.*$/, '');
			[
			  ['zpěv', 'vocals'],
			  ['klavír', 'piano'],
			  ['housle', 'violin'],
			  ['varhany', 'organ'],
			  ['cembalo', 'harpsichord'],
			  ['soprán', 'soprano'],
			  ['alt', 'alto'],
			  ['baryton', 'baritone'],
			  ['bas', 'basso'],
			  ['syntezátor', 'synthesizer'],
			  ['čte', 'narrator'],
			  ['hovoří', 'spoken by'],
			  ['hudební těleso', 'ensemble'],
			  ['hudba', 'music'],
			  ['text', 'lyrics'],
			  ['hudba+text', 'music+lyrics'],
			  ['původní text', 'original lyrics'],
			  ['český text', 'czech lyrics'],
			  ['hudební improvizace', 'music improvisation'],
			  ['autor', 'author'],
			  ['účinkuje', 'participating'],
			  ['řídí', 'conductor'],
			  ['dirigent', 'conductor'],
			  ['sbormistr', 'choirmaster'],
			  ['produkce', 'produced by'],
			  ['nahrál', 'recorded by'],
			  ['digitální přepis', 'A/D transfer'],
			].forEach(function(subst) {
			  if (typeof subst[0] == 'string' && role.toLowerCase() == subst[0].toLowerCase()
				 || subst[0] instanceof RegExp && subst[0].test(role)) role = subst[1]
			});
			return role;
		  }
		} });
		return true;
	  } else if (url.toLowerCase().includes('bontonland.cz')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  ref = dom.querySelector('div#detailheader > h1');
		  if (ref != null && /^(.*?)\s*:\s*(.*)$/.test(ref.textContent.trim())) {
			artist = RegExp.$1;
			album = RegExp.$2;
		  }
		  var EAN;
		  dom.querySelectorAll('table > tbody > tr > td.nazevparametru').forEach(function(it) {
			if (it.textContent.includes('Datum vydání')) {
			  releaseDate = normalizeDate(it.nextElementSibling.textContent);
			  albumYear = extract_year(it.nextElementSibling.textContent);
			} else if (it.textContent.includes('Nosič / počet')) {
			  if (/^(.*?)\s*\/\s*(.*)$/.test(it.nextElementSibling.textContent)) {
				media = RegExp.$1;
				totalDiscs = RegExp.$2;
			  }
			} else if (it.textContent.includes('Interpret')) {
			  artist = it.nextElementSibling.textContent.trim();
			} else if (it.textContent.includes('EAN')) {
			  EAN = 'BARCODE=' + it.nextElementSibling.textContent.trim();
			}
		  });
		  get_desc_from_node('div#detailtabpopis > div[class^="pravy"] > div > p:not(:last-of-type)', response.finalUrl, true);
		  const plParser = /^(\d+)(?:\s*[\/\.\-\:\)])?\s+(.*?)(?:\s+((?:(?:\d+:)?\d+:)?\d+))?$/;
		  ref = dom.querySelector('div#detailtabpopis > div[class^="pravy"] > div > p:last-of-type');
		  if (ref == null) throw new Error('Playlist not located');
		  var trackList = html2php(ref).split(/[\r\n]+/);
		  trackList = trackList.filter(it => plParser.test(it.trim())).map(it => plParser.exec(it.trim()));
		  totalTracks = trackList.length;
		  if (!totalTracks) throw new Error('Playlist empty');
		  trackList.forEach(function(it) {
			trackNumber = it[1];
			title = it[2];
			duration = timeStringToTime(it[3]);
			trackArtist = undefined;
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  undefined, // catalogue
			  undefined, // country
			  undefined, // encoding
			  undefined, // format
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  'CD', // media
			  undefined, // genre
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  undefined, // composer
			  undefined,
			  undefined,
			  undefined, // compiler
			  undefined, // producer
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  description,
			  EAN,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (url.toLowerCase().includes('nativedsd.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  var NDSD_ID = 'ORIGINALFORMAT=DSD', genre;
		  ref = dom.querySelector('div.the-content > header > h2');
		  if (ref != null) artist = ref.firstChild.data.trim();
		  ref = dom.querySelector('div.the-content > header > h1');
		  if (ref != null) album = ref.firstChild.data.trim();
		  ref = dom.querySelector('div.the-content > header > h3');
		  if (ref != null) composer = ref.firstChild.data.trim();
		  ref = dom.querySelector('div.the-content > header > h1 > small');
		  if (ref != null) albumYear = extract_year(ref.firstChild.data);
		  releaseDate = albumYear; // weak
		  ref = dom.querySelector('div#breadcrumbs > div[class] > a:nth-of-type(2)');
		  if (ref != null) label = ref.firstChild.data.trim();
		  ref = dom.querySelector('h2#sku');
		  if (ref != null) {
			if (/^Catalog Number: (.*)$/m.test(ref.firstChild.textContent)) catalogue = RegExp.$1;
			if (/^ID: (.*)$/m.test(ref.lastChild.textContent)) NDSD_ID += ' NATIVEDSD_ID=' + RegExp.$1;
		  }
		  get_desc_from_node('div.the-content > div.entry > p', response.finalUrl, false);
		  ref = dom.querySelector('div#repertoire > div > p');
		  if (ref != null) {
			let repertoire = html2php(ref, url).trim();
			let ndx = repertoire.indexOf('\n[b]Track');
			if (description) description += '\x1C\x1C';
			description += (ndx >= 0 ? repertoire.slice(0, ndx).trim() : repertoire)
			  .replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
		  }
		  ref = dom.querySelectorAll('div#techspecs > table > tbody > tr');
		  if (ref.length > 0) {
			if (description) description += '\x1C\x1C';
			description += '[b][u]Tech specs[/u][/b]';
			ref.forEach(function(it) {
			  description += '\n[b]'.concat(it.children[0].textContent.trim(), '[/b] ',
				it.children[1].textContent.trim()).replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
			});
		  }
		  ref = dom.querySelectorAll('div#track-list > table > tbody > tr[id^="track"]');
		  totalTracks = ref.length;
		  ref.forEach(function(it) {
			ref = it.children[0].children[0];
			if (ref != null) trackNumber = parseInt(ref.firstChild.data.trim().replace(/\..*$/, ''));
			let trackComposer;
			ref = it.children[1];
			if (ref != null) {
			  title = ref.firstChild.textContent.trim();
			  trackComposer = ref.childNodes[2] && ref.childNodes[2].textContent.trim() || undefined;
			}
			ref = it.children[2];
			if (ref != null) duration = timeStringToTime(ref.firstChild.data);
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  catalogue,
			  undefined, // country
			  'lossless', // encoding
			  'FLAC', // format
			  undefined,
			  undefined, // bitrate
			  24, //bd,
			  88200,
			  2,
			  'WEB',
			  genre, // 'Jazz'
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  trackComposer || composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  description,
			  NDSD_ID + ' TRACK_ID=' + it.id.replace(/^track-/i, ''),
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();

		  function getArtists(elem) {
			if (elem == null) return undefined;
			var artists = [];
			splitArtists(elem.textContent.trim()).forEach(function(it) {
			  artists.push(it.replace(/\s*\([^\(\)]*\)$/, ''));
			});
			return artists.join(', ');
		  }
		} });
		return true;
	  } else if (url.toLowerCase().includes('junodownload.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  ref = dom.querySelector('h2.product-artist > a');
		  if (ref != null) artist = titleCase(ref.firstChild.data.trim());
		  ref = dom.querySelector('meta[itemprop="name"]');
		  if (ref != null) album = ref.content;
		  ref = dom.querySelector('meta[itemprop="author"]');
		  if (ref != null) label = ref.content;
		  ref = dom.querySelector('span[itemprop="datePublished"]');
		  if (ref != null) releaseDate = ref.firstChild.data.trim();
		  var genres = [];
		  dom.querySelectorAll('div.mb-3 > strong').forEach(function(it) {
			if (it.textContent.startsWith('Genre')) {
			  ref = it;
			  while ((ref = ref.nextElementSibling) != null && ref.nodeName == 'A') {
				genres.push(ref.textContent.trim());
			  }
			} else if (it.textContent.startsWith('Cat')) {
			  if ((ref = it.nextSibling) != null && ref.nodeType == 3) catalogue = ref.data;
			}
		  });

		  ref = dom.querySelectorAll('div.product-tracklist > div[itemprop="track"]');
		  totalTracks = ref.length;
		  ref.forEach(function(it) {
			trackNumber = it.querySelector('div.track-title').firstChild.data.trim();
			if (/^(\d+)\./.test(trackNumber)) trackNumber = parseInt(RegExp.$1);
			title = it.querySelector('span[itemprop="name"]').textContent.trim();
			i = it.querySelector('meta[itemprop="duration"]');
			duration = i != null && /^P(\d+)H(\d+)M(\d+)S$/i.test(i.content) ?
			  (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0) : undefined;
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  catalogue,
			  undefined, // country
			  undefined, // encoding
			  undefined, // format
			  undefined,
			  undefined, // bitrate
			  undefined, //bd,
			  undefined, // SR
			  2,
			  'WEB',
			  genres.join('; '),
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  undefined, // description
			  'BPM=' + it.children[2].textContent.trim(),
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();

		  function getArtists(elem) {
			if (elem == null) return undefined;
			var artists = [];
			splitArtists(elem.textContent.trim()).forEach(function(it) {
			  artists.push(it.replace(/\s*\([^\(\)]*\)$/, ''));
			});
			return artists.join(', ');
		  }
		} });
		return true;
	  } else if (url.toLowerCase().includes('hdtracks.com')) {
		GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  dom = domParser.parseFromString(response.responseText, 'text/html');
		  var genres = [];
		  dom.querySelectorAll('div.album-main-details > ul > li > span').forEach(function(it) {
			if (it.textContent.startsWith('Title')) album = it.nextSibling.data.trim();
			if (it.textContent.startsWith('Artist')) artist = it.nextElementSibling.textContent.trim();
			if (it.textContent.startsWith('Genre')) {
			  ref = it;
			  while ((ref = ref.nextElementSibling) != null) genres.push(ref.textContent.trim());
			}
			if (it.textContent.startsWith('Label')) label = it.nextElementSibling.textContent.trim();
			if (it.textContent.startsWith('Release Date')) releaseDate = normalizeDate(it.nextSibling.data.trim());
		  });
		  if (!albumYear) albumYear = extract_year(releaseDate);
		  ref = dom.querySelectorAll('table#track-table > tbody > tr[id^="track"]');
		  totalTracks = ref.length;
		  ref.forEach(function(it) {
			trackNumber = parseInt(it.querySelector('td:first-of-type').textContent.trim());
			title = it.querySelector('td.track-name').textContent.trim();
			duration = timeStringToTime(it.querySelector('td:nth-of-type(3)').textContent.trim());
			format = it.querySelector('td:nth-of-type(4) > span').textContent.trim();
			sr = it.querySelector('td:nth-of-type(5)').textContent.trim().replace(/\/.*/, '');
			if (/^([\d\.\,]+)\s*\/\s*(\d+)$/.test(sr)) {
			  sr = Math.round(parseFloat(RegExp.$1.replace(',', '.')) * 1000);
			  bd = parseInt(RegExp.$2);
			} else sr = Math.round(parseFloat(sr) * 1000);
			track = [
			  artist,
			  album,
			  albumYear,
			  releaseDate,
			  label,
			  catalogue,
			  undefined, // country
			  'lossless',
			  undefined, // format
			  undefined,
			  undefined, // bitrate
			  bd || 24,
			  sr || undefined,
			  2,
			  'WEB',
			  genres.join('; '),
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  trackNumber,
			  totalTracks,
			  title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  response.finalUrl,
			  undefined,
			  undefined, // description
			  undefined,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (/^https?:\/\/(?:\w+\.)?deezer\.com\/(?:\w+\/)*album\/(\d+)/i.test(url)) {
		GM_xmlhttpRequest({ method: 'GET', url: 'https://api.deezer.com/album/' + RegExp.$1, onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) throw new Error('Ready state ' + response.readyState + ', Response error ' + response.status + ' (' + response.statusText + ')');
		  var json = JSON.parse(response.responseText);
		  if (json == null) return;

		  isVA = vaParser.test(json.artist.name);
		  var identifiers = 'DEEZER_ID=' + json.id + ' RELEASETYPE=' + json.record_type;
		  json.tracks.data.forEach(function(track, ndx) {
			trackArtist = track.artist.name;
			if (!isVA && trackArtist && trackArtist == json.artist.name) trackArtist = undefined;
			track = [
			  isVA ? 'Various Artists' : json.artist.name,
			  json.title,
			  undefined, //extract_year(json.release_date),
			  json.release_date,
			  json.label,
			  json.upc,
			  undefined, // country
			  undefined, // encoding
			  undefined, // format
			  undefined,
			  undefined, // bitrate
			  undefined, //bd,
			  undefined, // SR
			  2,
			  'WEB',
			  json.genres.data.map(it => it.name).join('; '),
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  ndx + 1,
			  json.nb_tracks,
			  track.title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  track.duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  undefined, //'https://www.deezer.com/album/' + json.id,
			  undefined,
			  undefined, // description
			  identifiers + ' TRACK_ID=' + track.id,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		} });
		return true;
	  } else if (/^https?:\/\/(?:\w+\.)?spotify\.com\/(?:\w+\/)*albums?\/(\w+)/i.test(url)) {
		querySpotifyAPI('https://api.spotify.com/v1/albums/' + RegExp.$1).then(function(json) {
		  isVA = json.artists.length == 1 && vaParser.test(json.artists[0].name);
		  artist = json.artists.map(artist => artist.name);
		  totalDiscs = json.tracks.items.reduce((acc, track) => Math.max(acc, track.disc_number), 0);
		  var identifiers = 'RELEASETYPE=' + json.album_type + ' SPOTIFY_ID=' + json.id;
		  var image = json.images.reduce((acc, image) => image.width * image.height > acc.width * acc.height ? image : acc);
		  //if (image) identifiers += ' IMGURL=' + image.url;
		  json.tracks.items.forEach(function(track, ndx) {
			trackArtist = track.artists.map(artist => artist.name);
			if (!isVA && json.artists.length > 0 && trackArtist.equalTo(artist)) trackArtist = [];
			track = [
			  isVA ? 'Various Artists' : joinArtists(artist),
			  json.name,
			  undefined, //extract_year(json.release_date),
			  json.release_date,
			  json.label,
			  json.external_ids.upc,
			  undefined, // country
			  undefined, // encoding
			  undefined, // format
			  undefined,
			  undefined, // bitrate
			  undefined, // BD
			  undefined, // SR
			  2,
			  'WEB',
			  json.genres.join('; '),
			  totalDiscs > 1 ? track.disc_number : undefined,
			  totalDiscs > 1 ? totalDiscs : undefined,
			  undefined, // discSubtitle
			  track.track_number,
			  json.total_tracks,
			  track.name,
			  joinArtists(trackArtist),
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  track.duration_ms / 1000,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  undefined, //'https://open.spotify.com/album/' + json.id,
			  undefined,
			  undefined, // description
			  identifiers +
			  	' EXPLICIT=' + Number(track.explicit) +
			  	' TRACK_ID=' + track.id,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		}).catch(e => { alert(e) });
		return true;

		function querySpotifyAPI(api_url) {
		  if (!api_url) return Promise.reject('No API URL');
		  return setToken().then(function(credentials) {
			return new Promise(function(resolve, reject) {
			  GM_xmlhttpRequest({
				method: 'GET',
				url: api_url,
				headers: {
				  'Accept': 'application/json',
				  'Authorization': credentials.token_type + ' ' + credentials.access_token,
				},
				responseType: 'json',
				onload: function(response) {
				  if (response.readyState == 4 && response.status == 200) resolve(response.response);
				  	else reject('Response error ' + response.status + ' (' + response.statusText + ')');
				},
				onerror: response => { reject('Response error ' + response.status + ' (' + response.statusText + ')') },
				ontimeout: function() { reject('Timeout') },
			  });
			});
		  });
		}
		function setToken() {
		  if (isTokenValid()) return Promise.resolve(spotifyCredentials);
		  if (!prefs.spotify_clientid || !prefs.spotify_clientsecret) return Promise.reject('Spotify credentials not set');
		  return new Promise(function(resolve, reject) {
			const data = new URLSearchParams({
			  grant_type: 'client_credentials',
			});
			GM_xmlhttpRequest({
			  method: 'POST',
			  url: 'https://accounts.spotify.com/api/token',
			  headers: {
				'Content-Type': 'application/x-www-form-urlencoded',
				'Content-Length': data.toString().length,
				'Authorization': 'Basic ' + btoa(prefs.spotify_clientid + ':' + prefs.spotify_clientsecret),
			  },
			  responseType: 'json',
			  data: data.toString(),
			  onload: function(response) {
				if (response.readyState == 4 && response.status == 200) {
				  spotifyCredentials = response.response;
				  spotifyCredentials.expires = new Date().getTime() + spotifyCredentials.expires_in;
				  if (isTokenValid()) resolve(spotifyCredentials); else reject('Invalid token');
				} else reject('Response error ' + response.status + ' (' + JSON.parse(response.response).error + ')');
			  },
			  onerror: response => { reject('Response error ' + response.status + ' (' + response.statusText + ')') },
			  ontimeout: function() { reject('Timeout') },
			});
		  });
		}
		function isTokenValid() {
		  return spotifyCredentials.token_type && spotifyCredentials.token_type.toLowerCase() == 'bearer'
		  	&& spotifyCredentials.access_token && spotifyCredentials.expires >= new Date().getTime() + 30;
		}
	  } else if (url.toLowerCase().includes('soundcloud.com') && prefs.soundcloud_clientid) {
// 		SC.initialize({
// 		  client_id: prefs.soundcloud_clientid,
// 		  redirect_uri: 'https://dont.spam.me/',
// 		});
		SC.connect().then(function() { return SC.resolve(url) }).then(function(json) {
		  isVA = vaParser.test(json.artist.name);
		  var identifiers = 'SOUNDCLOUD_ID=' + json.id + ' RELEASETYPE=' + json.record_type;
		  json.tracks.data.forEach(function(track, ndx) {
			trackArtist = track.artist.name;
			if (!isVA && trackArtist && trackArtist == json.artist.name) trackArtist = undefined;
			track = [
			  isVA ? 'Various Artists' : json.artist.name,
			  json.title,
			  undefined, //extract_year(json.release_date),
			  json.release_date,
			  json.label,
			  json.upc,
			  undefined, // country
			  undefined, // encoding
			  undefined, // format
			  undefined,
			  undefined, // bitrate
			  undefined, //bd,
			  undefined, // SR
			  2,
			  'WEB',
			  json.genres.data.map(it => it.name).join('; '),
			  discNumber,
			  totalDiscs,
			  discSubtitle,
			  ndx + 1,
			  json.nb_tracks,
			  track.title,
			  trackArtist,
			  undefined,
			  composer,
			  undefined,
			  undefined,
			  compiler,
			  producer,
			  track.duration,
			  undefined,
			  undefined,
			  undefined,
			  undefined,
			  undefined, //'https://www.deezer.com/album/' + json.id,
			  undefined,
			  undefined, // description
			  identifiers + ' TRACK_ID=' + track.id,
			];
			tracks.push(track.join('\x1E'));
		  });
		  clipBoard.value = tracks.join('\n');
		  fill_from_text_music();
		});
		return true;
	  }
	  if (!weak) {
		addMessage('This domain not supported', 'ua-critical');
		clipBoard.value = '';
	  }
	  return false;

	  function get_desc_from_node(selector, url, quote = false) {
		description = [];
		dom.querySelectorAll(selector).forEach(k => { description.push(html2php(k, url).trim()) });
		description = description.join('\n\n').trim().replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
		if (quote && description.length > 0) description = '[quote]' + description + '[/quote]';
	  }
	} // init_from_url_music

	function trackComparer(a, b) {
	  var cmp = a.discnumber - b.discnumber;
	  if (!isNaN(cmp) && cmp != 0) return cmp;
	  cmp = parseInt(a.tracknumber) - parseInt(b.tracknumber);
	  if (!isNaN(cmp)) return cmp;
	  var m1 = vinyltrackParser.exec(a.tracknumber.toUpperCase());
	  var m2 = vinyltrackParser.exec(b.tracknumber.toUpperCase());
	  return m1 != null && m2 != null ?
		m1[1].localeCompare(m2[1]) || parseFloat(m1[2]) - parseFloat(m2[2]) :
		a.tracknumber.toUpperCase().localeCompare(b.tracknumber.toUpperCase());
	}

	function normalizeDate(str) {
	  if (typeof str != 'string') return null;
	  if (/\b(d{4}-\d+-\d+|\d{1,2}\/\d{1,2}\/\d{2})\b/.test(str)) return RegExp.$1;
	  if (/\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3;
	  if (/\b(\d{1,2})\.\s?(\d{1,2})\.\s?(\d{2}|\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3;
	  return extract_year(str);
	}

	function getCoverOnline(url) {
	  GM_xmlhttpRequest({
		method: 'GET',
		url: url,
		onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  var ref, dom = domParser.parseFromString(response.responseText, 'text/html');
		  function testDomain(url, selector) {
			return typeof url == 'string' && response.finalUrl.toLowerCase().includes(url.toLowerCase()) ?
			  dom.querySelector(selector) : null;
		  }
		  if ((ref = testDomain('qobuz.com', 'div.album-cover > img')) != null) {
			setImage(ref.src);
		  } else if ((ref = testDomain('highresaudio.com', 'div.albumbody > img.cover[data-pin-media]')) != null) {
			setImage(ref.dataset.pinMedia);
		  } else if ((ref = testDomain('bandcamp.com', 'div#tralbumArt > a.popupImage')) != null) {
			setImage(ref.href);
		  } else if ((ref = testDomain('7digital.com', 'span.release-packshot-image > img[itemprop="image"]')) != null) {
			setImage(ref.src);
		  } else if ((ref = testDomain('hdtracks.com', 'p.product-image > img')) != null) {
			setImage(ref.src);
		  } else if ((ref = testDomain('discogs.com', 'div#view_images > p:first-of-type > span > img')) != null) {
			setImage(ref.src);
		  } else if ((ref = testDomain('junodownload.com', 'a.productimage')) != null) {
			setImage(ref.href);
		  } else if ((ref = testDomain('supraphonline.cz', 'meta[itemprop="image"]')) != null) {
			setImage(ref.content.replace(/\?.*$/, ''));
		  } else if ((ref = testDomain('prestomusic.com', 'div.c-product-block__aside > a')) != null) {
			setImage(ref.href.replace(/\?\d+$/, ''));
		  } else if ((ref = testDomain('bontonland.cz', 'a.detailzoom')) != null) {
			setImage(ref.href);
		  } else if ((ref = testDomain('nativedsd.com', 'a#album-cover')) != null) {
			setImage(ref.href);
		  } else if ((ref = testDomain('deezer.com', 'meta[property="og:image"]')) != null) {
			setImage(ref.content);
		  } else if ((ref = testDomain('spotify.com', 'meta[property="og:image"]')) != null) {
			setImage(ref.content);
		  }
		},
		//onerror: response => { throw new Error('Response error ' + response.status + ' (' + response.statusText + ')') },
		//ontimeout: function() { throw new Error('Timeout') },
	  });
	}

	function reqSelectFormats(...vals) {
	  vals.forEach(function(val) {
		[
		  ['MP3', 0],
		  ['FLAC', 1],
		  ['AAC', 2],
		  ['AC3', 3],
		  ['DTS', 4],
		].forEach(function(fmt) {
		  if (val.toLowerCase() == fmt[0].toLowerCase()
			  && (ref = document.getElementById('format_' + fmt[1])) != null) {
			ref.checked = true;
			ref.onchange();
		  }
		});
	  });
	}

	function reqSelectBitrates(...vals) {
	  vals.forEach(function(val) {
		var ndx = 10;
		[
		  [192, 0],
		  ['APS (VBR)', 1],
		  ['V2 (VBR)', 2],
		  ['V1 (VBR)', 3],
		  [256, 4],
		  ['APX (VBR)', 5],
		  ['V0 (VBR)', 6],
		  [320, 7],
		  ['Lossless', 8],
		  ['24bit Lossless', 9],
		  ['Other', 10],
		].forEach(function(it) {
		  if ((typeof val == 'string' ? val.toLowerCase() : val)
			  == (typeof it[0] == 'string' ? it[0].toLowerCase() : it[0])) ndx = it[1]
		});
		if ((ref = document.getElementById('bitrate_' + ndx)) != null) {
		  ref.checked = true;
		  ref.onchange();
		}
	  });
	}

	function reqSelectMedias(...vals) {
	  vals.forEach(function(val) {
		[
		  ['CD', 0],
		  ['DVD', 1],
		  ['Vinyl', 2],
		  ['Soundboard', 3],
		  ['SACD', 4],
		  ['DAT', 5],
		  ['Cassette', 6],
		  ['WEB', 7],
		  ['Blu-Ray', 8],
		].forEach(function(med) {
		  if (val == med[0] && (ref = document.getElementById('media_' + med[1])) != null) {
			ref.checked = true;
			ref.onchange();
		  }
		});
		if (val == 'CD') {
		  if ((ref = document.getElementById('needlog')) != null) {
			ref.checked = true;
			ref.onchange();
			if ((ref = document.getElementById('minlogscore')) != null) ref.value = 100;
		  }
		  if ((ref = document.getElementById('needcue')) != null) ref.checked = true;
		  //if ((ref = document.getElementById('needchecksum')) != null) ref.checked = true;
		}
	  });
	}

  	function getReleaseIndex(str) {
	  var ndx;
	  [
		['Album', 1],
		['Soundtrack', 3],
		['EP', 5],
		['Anthology', 6],
		['Compilation', 7],
		['Single', 9],
		['Live album', 11],
		['Remix', 13],
		['Bootleg', 14],
		['Interview', 15],
		['Mixtape', 16],
		['Demo', 17],
		['Concert Recording', 18],
		['DJ Mix', 19],
		['Unknown', 21],
	  ].forEach(k => { if (str.toLowerCase() == k[0].toLowerCase()) ndx = k[1] });
	  return ndx || 21;
	}

	function joinArtists(arr, decorator = artist => artist) {
	  if (!(arr instanceof Array)) 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());
	}
  } // fill_from_text_music

  function fill_from_text_apps(weak = false) {
	if (messages != null) messages.parentNode.removeChild(messages);
	if (!urlParser.test(clipBoard.value)) {
	  addMessage('Only URL accepted for this category', 'ua-critical');
	  return false;
	}
	url = RegExp.$1;
	var description, tags = new TagManager();
	if (url.toLowerCase().includes('//sanet')) {
	  GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) throw new Error('GM_xmlhttpRequest readyState=' + response.status + ', status=' + response.status);
		dom = domParser.parseFromString(response.responseText, 'text/html');
		i = dom.querySelector('h1.item_title > span');
		if (elementWritable(ref = document.getElementById('title'))) {
		  ref.value = i != null ? i.textContent.
			replace(/\(x64\)$/i, '(64-bit)').
			replace(/\b(?:Build)\s+(\d+)/, 'build $1').
			replace(/\b(?:Multilingual|Multilanguage)\b/, 'multilingual') : null;
		}
		description = html2php(dom.querySelector('section.descr'), response.finalUrl);
		if (/\s*^(?:\[i\]\[\/i\])?Homepage$.*/m.test(description)) description = RegExp.leftContext;
		description = description.trim().split(/\n/).slice(5).map(k => k.trimRight()).join('\n').trim();
		ref = dom.querySelector('section.descr > div.release-info');
		var releaseInfo = ref != null && ref.textContent.trim();
		if (/\b(?:Languages?)\s*:\s*(.*?)\s*(?:$|\|)/i.exec(releaseInfo) != null) {
		  description += '\n\n[b]Languages:[/b]\n' + RegExp.$1;
		}
		ref = dom.querySelector('div.txtleft > a');
		if (ref != null) description += '\n\n[b]Product page:[/b]\n[url]' + deAnonymize(ref.href) + '[/url]';
		writeDescription(description);
		if ((ref = dom.querySelector('section.descr > div.center > a.mfp-image')) != null) {
		  setImage(ref.href);
		} else {
		  ref = dom.querySelector('section.descr > div.center > img[data-src]');
		  if (ref != null) setImage(ref.dataset.src);
		}
		var cat = dom.querySelector('a.cat:last-of-type > span');
		if (cat != null) {
		  if (cat.textContent.toLowerCase() == 'windows') {
			tags.add('apps.windows');
			if (/\b(?:x64)\b/i.test(releaseInfo)) tags.add('win64');
			if (/\b(?:x86)\b/i.test(releaseInfo)) tags.add('win32');
		  }
		  if (cat.textContent.toLowerCase() == 'macos') tags.add('apps.mac');
		  if (cat.textContent.toLowerCase() == 'linux' || cat.textContent.toLowerCase() == 'unix') tags.add('apps.linux');
		  if (cat.textContent.toLowerCase() == 'android') tags.add('apps.android');
		  if (cat.textContent.toLowerCase() == 'ios') tags.add('apps.ios');
		}
		if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	}
	if (!weak) {
	  addMessage('This domain not supported', 'ua-critical');
	  clipBoard.value = '';
	}
	return false;
  }

  function fill_from_text_books(weak = false) {
	if (messages != null) messages.parentNode.removeChild(messages);
	if (!urlParser.test(clipBoard.value)) {
	  addMessage('Only URL accepted for this category', 'ua-critical');
	  return false;
	}
	url = RegExp.$1;
	var description, tags = new TagManager();
	if (url.toLowerCase().includes('martinus.cz') || url.toLowerCase().includes('martinus.sk')) {
	  GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) throw new Error('GM_xmlhttpRequest readyState=' + response.status + ', status=' + response.status);
		dom = domParser.parseFromString(response.responseText, 'text/html');
		function get_detail(x, y) {
		  var ref = dom.querySelector('section#details > div > div > div:first-of-type > div:nth-child(' +
			x + ') > dl:nth-child(' + y + ') > dd');
		  return ref != null ? ref.textContent.trim() : null;
		}

		i = dom.querySelectorAll('article > ul > li > a');
		if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
		  description = joinAuthors(i);
		  if ((i = dom.querySelector('article > h1')) != null) description += ' – ' + i.textContent.trim();
		  i = dom.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
		  if (i != null && (i = extract_year(i.textContent))) description += ' (' + i + ')';
		  ref.value = description;
		}

		description = '[quote]' + html2php(dom.querySelector('section#description > div')).
			replace(/^\s*\[img\].*?\[\/img\]\s*/i, '') + '[/quote]';
		const translation_map = [
		  [/\b(?:originál)/i, 'Original title'],
		  [/\b(?:datum|dátum|rok)\b/i, 'Release date'],
		  [/\b(?:katalog|katalóg)/i, 'Catalogue #'],
		  [/\b(?:stran|strán)\b/i, 'Page count'],
		  [/\bjazyk/i, 'Language'],
		  [/\b(?:nakladatel|vydavatel)/i, 'Publisher'],
		  [/\b(?:doporuč|ODPORÚČ)/i, 'Age rating'],
		];
		dom.querySelectorAll('section#details > div > div > div:first-of-type > div > dl').forEach(function(detail) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = detail.children[1].textContent.trim();
		  if (/\b(?:rozm)/i.test(lbl) || /\b(?:vazba|vázba)\b/i.test(lbl)) return;
		  translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
		  if (/\b(?:ISBN)\b/i.test(lbl)) {
			url = new URL('https://www.worldcat.org/isbn/' + detail.children[1].textContent.trim());
			val = '[url=' + url.href + ']' + detail.children[1].textContent.trim() + '[/url]';
			findOCLC(url);
// 		  } else if (/\b(?:ISBN)\b/i.test(lbl)) {
// 			val = '[url=https://www.goodreads.com/search/search?q=' + detail.children[1].textContent.trim() +
// 			  '&search_type=books]' + detail.children[1].textContent.trim() + '[/url]';
		  }
		  description += '\n[b]' + lbl + ':[/b] ' + val;
		});
		url = new URL(response.finalUrl);
		description += '\n\n[b]More info:[/b]\n[url]' + url.href + '[/url]';
		writeDescription(description);

		if ((i = dom.querySelector('a.mj-product-preview > img')) != null) {
		  setImage(i.src.replace(/\?.*/, ''));
		} else if ((i = dom.querySelector('head > meta[property="og:image"]')) != null) {
		  setImage(i.content.replace(/\?.*/, ''));
		}

		dom.querySelectorAll('dd > ul > li > a').forEach(x => { tags.add(x.textContent) });
		if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	} else if (url.toLowerCase().includes('goodreads.com')) {
	  GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) throw new Error('GM_xmlhttpRequest readyState=' + response.status + ', status=' + response.status);
		dom = domParser.parseFromString(response.responseText, 'text/html');
		i = dom.querySelectorAll('a.authorName > span');
		if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
		  description = joinAuthors(i);
		  if ((i = dom.querySelector('h1#bookTitle')) != null) description += ' – ' + i.textContent.trim();
		  if ((i = dom.querySelector('div#details > div.row:nth-of-type(2)')) != null
			  && (i = extract_year(i.textContent))) description += ' (' + i + ')';
		  ref.value = description;
		}

		var description = [];
		dom.querySelectorAll('div#description span:last-of-type').forEach(function(it) {
		  description = html2php(it, response.finalUrl);
		});
		description = '[quote]' + description.trim() + '[/quote]';

		function strip(str) {
		  return typeof str == 'string' ?
			str.replace(/\s{2,}/g, ' ').replace(/[\n\r]+/, '').replace(/\s*\.{3}(?:less|more)\b/g, '').trim() : null;
		}

		dom.querySelectorAll('div#details > div.row').forEach(k => { description += '\n' + strip(k.innerText) });
		description += '\n';

		dom.querySelectorAll('div#bookDataBox > div.clearFloats').forEach(function(detail) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = strip(detail.children[1].textContent);
		  if (/\b(?:ISBN)\b/i.test(lbl) && (/\b(\d{13})\b/.test(val) || /\b(\d{10})\b/.test(val))) {
			url = new URL('https://www.worldcat.org/isbn/' + RegExp.$1);
			val = '[url=' + url.href + ']' + strip(detail.children[1].textContent) + '[/url]';
			findOCLC(url);
		  }
		  description += '\n[b]' + lbl + ':[/b] ' + val;
		});
		if ((ref = dom.querySelector('span[itemprop="ratingValue"]')) != null) {
		  description += '\n[b]Rating:[/b] ' + Math.round(parseFloat(ref.firstChild.textContent) * 20) + '%';
		}
		url = new URL(response.finalUrl);
// 		if ((ref = dom.querySelector('div#buyButtonContainer > ul > li > a.buttonBar')) != null) {
// 		  let u = new URL(ref.href);
// 		  description += '\n[url=' + url.origin + u.pathname + '?' + u.search + ']Libraries[/url]';
// 		}
		description += '\n\n[b]More info and reviews:[/b]\n[url]' + url.origin + url.pathname + '[/url]';
		dom.querySelectorAll('div.clearFloats.bigBox').forEach(function(bigBox) {
		  if (bigBox.id == 'aboutAuthor' && (ref = bigBox.querySelector('h2 > a')) != null) {
			description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
			if ((ref = bigBox.querySelector('div.bigBoxBody a > div[style*="background-image"]')) != null) {
			}
			if ((ref = bigBox.querySelector('div.bookAuthorProfile__about > span[id]:last-of-type')) != null) {
			  description += '\n' + html2php(ref, response.finalUrl).replace(/^\[i\]Librarian\s+Note:.*?\[\/i\]\s+/i, '');
			}
		  } else if ((ref = bigBox.querySelector('h2 > a[href^="/trivia/"]')) != null) {
			description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
			if ((ref = bigBox.querySelector('div.bigBoxContent > div.mediumText')) != null) {
			  description += '\n' + ref.firstChild.textContent.trim();
			}
// 		  } else if ((ref = bigBox.querySelector('h2 > a[href^="/work/quotes/"]')) != null) {
// 			description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
// 			bigBox.querySelectorAll('div.bigBoxContent > div.stacked > span.readable').forEach(function(quote) {
// 			  description += '\n' + ref.firstChild.textContent.trim();
// 			});
		  }
		});
		writeDescription(description);
		if ((ref = dom.querySelector('div.editionCover > img')) != null) setImage(ref.src.replace(/\?.*/, ''));
		dom.querySelectorAll('div.elementList > div.left').forEach(tag => { tags.add(tag.textContent.trim()) });
		if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
	  }, });
	  return true;
	} else if (url.toLowerCase().includes('databazeknih.cz')) {
	  if (!url.toLowerCase().includes('show=alldesc')) {
		if (!url.includes('?')) { url += '?show=alldesc' } else { url += '&show=alldesc' }
	  }
	  GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) throw new Error('GM_xmlhttpRequest readyState=' + response.status + ', status=' + response.status);
		dom = domParser.parseFromString(response.responseText, 'text/html');
		i = dom.querySelectorAll('span[itemprop="author"] > a');
		if (i != null && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
		  description = joinAuthors(i);
		  if ((i = dom.querySelector('h1[itemprop="name"]')) != null) description += ' – ' + i.textContent.trim();
		  i = dom.querySelector('span[itemprop="datePublished"]');
		  if (i != null && (i = extract_year(i.textContent))) description += ' (' + i + ')';
		  ref.value = description;
		}

		description = '[quote]' + html2php(dom.querySelector('p[itemprop="description"]'), response.finalUrl) + '[/quote]';
		const translation_map = [
		  [/\b(?:orig)/i, 'Original title'],
		  [/\b(?:série)\b/i, 'Series'],
		  [/\b(?:vydáno)\b/i, 'Released'],
		  [/\b(?:stran)\b/i, 'Page count'],
		  [/\b(?:jazyk)\b/i, 'Language'],
		  [/\b(?:překlad)/i, 'Translation'],
		  [/\b(?:autor obálky)\b/i, 'Cover author'],
		];
		dom.querySelectorAll('table.bdetail tr').forEach(function(detail) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = detail.children[1].textContent.trim();
		  if (/(?:žánr|\bvazba)\b/i.test(lbl)) return;
		  translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
		  if (/\b(?:ISBN)\b/i.test(lbl) && /\b(\d+(?:-\d+)*)\b/.exec(val) != null) {
			url = new URL('https://www.worldcat.org/isbn/' + RegExp.$1.replace(/-/g, ''));
			val = '[url=' + url.href + ']' + detail.children[1].textContent.trim() + '[/url]';
			findOCLC(url);
		  }
		  description += '\n[b]' + lbl + '[/b] ' + val;
		});

		url = new URL(response.finalUrl);
		description += '\n\n[b]More info:[/b]\n[url]' + url.origin + url.pathname + '[/url]';
		writeDescription(description);

		if ((ref = dom.querySelector('div#icover_mid > a')) != null) setImage(ref.href.replace(/\?.*/, ''));
		if ((ref = dom.querySelector('div#lbImage')) != null && /\burl\("(.*)"\)/i.test(i.style.backgroundImage)) {
		  setImage(RegExp.$1.replace(/\?.*/, ''));
		}

		dom.querySelectorAll('h5[itemprop="genre"] > a').forEach(tag => { tags.add(tag.textContent.trim()) });
		dom.querySelectorAll('a.tag').forEach(tag => { tags.add(tag.textContent.trim()) });
		if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
	  }, });
	  return true;
	}
	if (!weak) {
	  addMessage('This domain not supported', 'ua-critical');
	  clipBoard.value = '';
	}
	return false;

	function joinAuthors(nodeList) {
	  if (typeof nodeList != 'object') return null;
	  return Array.from(nodeList).map(it => it.textContent.trim()).join(' & ');
	}

	function findOCLC(url) {
	  if (!url) return false;
	  var oclc = document.querySelector('input[name="oclc"]');
	  if (!elementWritable(oclc)) return false;
	  GM_xmlhttpRequest({
		method: 'GET',
		url: url,
		onload: function(response) {
		  if (response.readyState != 4 || response.status != 200) return;
		  var dom = domParser.parseFromString(response.responseText, 'text/html');
		  var ref = dom.querySelector('tr#details-oclcno > td:last-of-type');
		  if (ref != null) oclc.value = ref.textContent.trim();
		},
	  });
	  return true;
	}
  }

  function preview(n) {
	if (!prefs.auto_preview) return;
	var btn = document.querySelector('input.button_preview_' + n + '[type="button"][value="Preview"]');
	if (btn != null) btn.click();
  }

  function html2php(node, url) {
	var php = '';
	if (node instanceof HTMLElement) node.childNodes.forEach(function(ch) {
	  if (ch.nodeType == 3) {
		php += ch.data.replace(/\s+/g, ' ');
	  } else if (ch.nodeName == 'P') {
		php += '\n' + html2php(ch, url);
	  } else if (ch.nodeName == 'DIV') {
		php += '\n\n' + html2php(ch, url) + '\n\n';
	  } else if (ch.nodeName == 'LABEL') {
		php += '\n\n[b]' + html2php(ch, url) + '[/b]';
	  } else if (ch.nodeName == 'SPAN') {
		php += html2php(ch, url);
	  } else if (ch.nodeName == 'BR' || ch.nodeName == 'HR') {
		php += '\n';
	  } else if (ch.nodeName == 'B' || ch.nodeName == 'STRONG') {
		php += '[b]' + html2php(ch, url) + '[/b]';
	  } else if (ch.nodeName == 'I' || ch.nodeName == 'EM') {
		php += '[i]' + html2php(ch, url) + '[/i]';
	  } else if (ch.nodeName == 'U') {
		php += '[u]' + html2php(ch, url) + '[/u]';
	  } else if (ch.nodeName == 'CODE') {
		php += '[pre]' + ch.textContent + '[/pre]';
	  } else if (ch.nodeName == 'A') {
		php += ch.childNodes.length > 0 ?
		  '[url=' + deAnonymize(ch.href) + ']' + html2php(ch, url) + '[/url]' :
			'[url]' + deAnonymize(ch.href) + '[/url]';
	  } else if (ch.nodeName == 'IMG') {
		php += '[img]' + (ch.dataset.src || ch.src) + '[/img]';
	  }
	});
	return php;
  }

  function deAnonymize(uri) {
	return typeof uri == 'string' ? uri.replace(/^https?:\/\/(?:www\.)?anonymz\.com\/\?/i, '') : null;
  }

  function writeDescription(desc) {
	if (typeof desc != 'string') return;
	if (elementWritable(ref = document.querySelector('textarea#desc')
		|| document.querySelector('textarea#description'))) ref.value = desc;
	if ((ref = document.getElementById('body')) != null && !ref.disabled) {
	  if (ref.textLength > 0) ref.value += '\n\n';
	  ref.value += desc;
	}
  }

  function setImage(url) {
	var image = document.getElementById('image') || document.querySelector('input[name="image"]');
	if (!elementWritable(image)) return false;
	image.value = url;

	if (prefs.auto_preview_cover && image.id) {
	  if ((child = document.getElementById('cover preview')) == null) {
		elem = document.createElement('div');
		elem.style.paddingTop = '10px';
		child = document.createElement('img');
		child.id = 'cover preview';
		child.style.width = '90%';
		elem.append(child);
		image.parentNode.previousElementSibling.append(elem);
	  }
	  child.src = url;
	}
	if (prefs.auto_rehost_cover) {
	  if (rehostItBtn != null) {
		rehostItBtn.click();
	  } else {
		rehost2PTPIMG([url]).then(urls => { if (urls.length > 0) image.value = urls[0] }).catch(e => { alert(e) });
	  }
	}
  }

  function elementWritable(elem) { return elem != null && !elem.disabled && (overwrite || !elem.value) }
}

function addArtistField() { exec(function() { AddArtistField() }) }
function removeArtistField() { exec(function() { RemoveArtistField() }) }

function array_homogenous(arr) { return arr.every(k => k === arr[0]) }

function titleCase(str) {
  return str.toLowerCase().split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(' ');
}

function exec(fn) {
  let script = document.createElement('script');
  script.type = 'application/javascript';
  script.textContent = '(' + fn + ')();';
  document.body.appendChild(script); // run the script
  document.body.removeChild(script); // clean up
}

function makeTimeString(duration) {
  let t = Math.abs(Math.round(duration));
  let H = Math.floor(t / 60 ** 2);
  let M = Math.floor(t / 60 % 60);
  let S = t % 60;
  return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
	':' + S.toString().padStart(2, '0');
}

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 extract_year(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);
  var d = new Date(expr);
  return parseInt(isNaN(d) ? expr : d.getFullYear());
}

function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*'.concat(expr, '[^\\(\\)]*\\)$'), 'i') }
function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*'.concat(expr, '[^\\[\\]]*\\]$'), 'i') }

function addMessage(text, cls, html = false) {
  messages = document.getElementById('UA messages');
  if (messages == null) {
	var ua = document.getElementById('upload assistant');
	if (ua == null) return null;
	messages = document.createElement('TR');
	if (messages == null) return null;
	messages.id = 'UA messages';
	ua.children[0].append(messages);

	elem = document.createElement('TD');
	if (elem == null) return null;
	elem.colSpan = 2;
	elem.className = 'ua-messages-bg';
	messages.append(elem);
  } else {
	elem = messages.children[0]; // tbody
	if (elem == null) return null;
  }
  var div = document.createElement('DIV');
  div.classList.add('ua-messages', cls);
  div[html ? 'innerHTML' : 'textContent'] = text;
  return elem.appendChild(div);
}

function imageDropHandler(evt) {
  evt.preventDefault();
  if (evt.dataTransfer.files.length <= 0) return;
  var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  if (image == null) return;
  if (evt.currentTarget.busy) throw new Error('Wait till current upload finishes');
  evt.currentTarget.busy = true;
  if (evt.currentTarget.hTimer) {
	clearTimeout(evt.currentTarget.hTimer);
	delete evt.currentTarget.hTimer;
  }
  var origlabel = evt.currentTarget.value;
  evt.currentTarget.value = 'Uploading...';
  evt.currentTarget.style.backgroundColor = '#A00000';
  var evtSrc = evt.currentTarget;
  upload2PTPIMG(evt.dataTransfer.files[0]).then(function(result) {
	if (result.length > 0) {
	  image.value = result[0];
	  evtSrc.style.backgroundColor = '#00A000';
	  evtSrc.hTimer = setTimeout(function() {
		evtSrc.style.backgroundColor = null;
		delete evtSrc.hTimer;
	  }, 10000);
	} else evtSrc.style.backgroundColor = null;
  }).catch(function(e) {
	alert(e);
	evtSrc.style.backgroundColor = null;
  }).then(function() {
	evtSrc.busy = false;
	evtSrc.value = origlabel;
  });
}

function voidDragHandler(evt) { evt.preventDefault() }

function upload2PTPIMG(file, elem) {
  if (!(file instanceof File)) return Promise.reject('Bad parameter (file)');
  var config = JSON.parse(window.localStorage.ptpimg_it);
  if (!prefs.ptpimg_api_key && !config.api_key) return Promise.reject('API key not set');
  return new Promise(function(resolve, reject) {
	var reader = new FileReader();
	var fr = new Promise(function(resolve, reject) {
	  reader.onload = function() { resolve(reader.result) }
	  reader.readAsBinaryString(file); //readAsArrayBuffer(file);
	});
	fr.then(function(result) {
	  const boundary = '----NN-GGn-PTPIMG';
	  var data = '--' + boundary + '\r\n';
	  data += 'Content-Disposition: form-data; name="file-upload[0]"; filename="' + file.name.toASCII() + '"\r\n';
	  data += 'Content-Type: ' + file.type + '\r\n\r\n';
	  data += result + '\r\n';
	  data += '--' + boundary + '\r\n';
	  data += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
	  data += (prefs.ptpimg_api_key || config.api_key) + '\r\n';
	  data += '--' + boundary + '--\r\n';
	  GM_xmlhttpRequest({
		method: 'POST',
		url: 'https://ptpimg.me/upload.php',
		responseType: 'json',
		headers: {
		  'Content-Type': 'multipart/form-data; boundary=' + boundary,
		  'Content-Length': data.length,
		},
		data: data,
		binary: true,
		onload: function(response) {
		  if (response.readyState == 4 && response.status == 200) {
			resolve(response.response.map(item => 'https://ptpimg.me/' + item.code + '.' + item.ext));
		  } else reject('Response error ' + response.status + ' (' + response.statusText + ')');
		},
		onprogress: elem instanceof HTMLInputElement ?
			arg => { elem.value = 'Uploading... (' + arg.position + '%)' } : undefined,
		onerror: response => { reject('Response error ' + response.status + ' (' + response.statusText + ')') },
		ontimeout: function() { reject('Timeout') },
	  });
	});
  });
}

function rehost2PTPIMG(urls) {
  if (!Array.isArray(urls)) return Promise.reject('Bad parameter (urls)');
  var config = JSON.parse(window.localStorage.ptpimg_it);
  if (!prefs.ptpimg_api_key && !config.api_key) return Promise.reject('API key not set');
  return new Promise(function(resolve, reject) {
	const boundary = 'NN-GGn-PTPIMG';
	const dcTest = /^https?:\/\/(?:\w+\.)?discogs\.com\//i;
	var data = '--' + boundary + "\n";
	data += 'Content-Disposition: form-data; name="link-upload"\n\n';
	data += urls.map(url => dcTest.test(url) ? 'https://reho.st/' + url : url).join('\n') + '\n';
	data += '--' + boundary + '\n';
	data += 'Content-Disposition: form-data; name="api_key"\n\n';
	data += (prefs.ptpimg_api_key || config.api_key) + '\n';
	data += '--' + boundary + '--';
	GM_xmlhttpRequest({
	  method: 'POST',
	  url: 'https://ptpimg.me/upload.php',
	  responseType: 'json',
	  headers: {
		'Content-type': 'multipart/form-data; boundary=' + boundary,
		'Content-Length': data.length,
	  },
	  data: data,
	  onload: function(response) {
		if (response.readyState == 4 && response.status == 200) {
		  resolve(response.response.map(item => 'https://ptpimg.me/' + item.code + '.' + item.ext));
		} else reject('Response error ' + response.status + ' (' + response.statusText + ')');
	  },
	  onerror: response => { reject('Response error ' + response.status + ' (' + response.statusText + ')') },
	  ontimeout: function() { reject('Timeout') },
	});
  });
}