RED (+ NWCD, Orpheus) Upload Assistant

Script fills in as much accurately the upload and group edit forms based on foobar2000's playlist selection via pasted output of copy command, release consistency check, two tracklist layouts, basic colours customization, featured artists extraction, image URl fetching from store and more...

当前为 2019-08-30 提交的版本,查看 最新版本

// ==UserScript==
// @name         RED (+ NWCD, Orpheus) Upload Assistant
// @namespace    https://greatest.deepsurf.us/cs/users/321857-anakunda
// @version      1.69
// @description  Script fills in as much accurately the upload and group edit forms based on foobar2000's playlist selection via pasted output of copy command, release consistency check, two tracklist layouts, basic colours customization, featured artists extraction, image URl fetching from store and more...
// @author       Anakunda
// @iconURL      https://redacted.ch/favicon.ico
// @match        https://redacted.ch/upload.php*
// @match        https://redacted.ch/torrents.php?action=editgroup*
// @match        https://notwhat.cd/upload.php*
// @match        https://notwhat.cd/torrents.php?action=editgroup*
// @match        https://orpheus.network/upload.php*
// @match        https://orpheus.network/torrents.php?action=editgroup*
// @connect      file://*
// @connect      sanet.st
// @connect      martinus.cz
// @connect      martinus.sk
// @connect      alza.cz
// @connect      goodreads.com
// @connect      databazeknih.cz
// @connect      ptpimg.me
// @connect      bandcamp.com
// @connect      qobuz.com
// @connect      hdtracks.com
// @connect      highresaudio.com
// @connect      7digital.com
// @connect      junodownload.com
// @connect      discogs.com
// @connect      supraphonline.cz
// @grant        RegExp
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_log
// ==/UserScript==

// The pattern for built-in copy command or custom Text Tools quick copy command, which is handled by this helper is:
//   $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($if2(%label%,%publisher%),)]$char(30)[$fix_eol($if3(%catalog%,%CATALOGNUMBER%,%catalog #%,%barcode%,%UPC%,%EAN%),)]$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%,%discogs_format%,%source%)]$char(30)[$fix_eol(%genre%,)]','[$fix_eol(%style%,)]$char(30)[$num(%discnumber%,0)]$char(30)[$num(%totaldiscs%,0)]$char(30)[$fix_eol(%discsubtitle%,)]$char(30)[%track number%]$char(30)[$num(%totaltracks%,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(%composer%,)]$char(30)[$fix_eol(%conductor%,)]$char(30)[$fix_eol(%remixer%,)]$char(30)[$fix_eol(%compiler%,)]$char(30)[$fix_eol($if2(%producer%,%producedby%),)]$char(30)%length_seconds_fp%$char(30)[%replaygain_album_gain%]$char(30)[%album dynamic range%]$char(30)[%__tool%][ | %ENCODER%][ | %ENCODER_OPTIONS%]$char(30)[$fix_eol($if2(%url%,'https://www.discogs.com/release/'%discogs_release_id%),)]$char(30)$directory_path(%path%)$char(30)[$replace($replace(%comment%,$char(13),$char(29)),$char(10),$char(28))]

'use strict';

const single_threshold = 8 * 60; // Max length of single in s
const EP_threshold = 28 * 60; // Max time of EP in s

var iter, rows = [], ref, tbl, elem, child, tb, warning = null;
if (document.URL.search(/\/upload\.php\b/) >= 0) {
  ref = document.querySelector('form#upload_table > div#dynamic_form');
  if (ref == null) return;
  tbl = document.createElement('tr');

  elem = document.createElement('td');
  child = document.createElement('textarea');
  child.id = 'import_data';
  child.name = 'import_data';
  child.cols = 50;
  child.rows = 3;
  child.style.width = '35rem';
  child.style.height = '3rem';
  child.className = ' wbbarea';
  child.setAttribute('data-wbb', '');
  elem.appendChild(child);
  tbl.appendChild(elem);

  elem = document.createElement('td');
  elem.align = 'right';
  let x = [];
  x.push(document.createElement('tr'));
  x[0].style.verticalAlign = 'middle';
  child = document.createElement('input');
  child.id = 'fill-from-text';
  child.value = 'Fill from text (overwrite)';
  child.type = 'button';
  child.style.width = '11.5rem';
  child.addEventListener("click", fill_from_text, false);
  x[0].appendChild(child);
  elem.appendChild(x[0]);
  x.push(document.createElement('tr'));
  x[1].style.verticalAlign = 'middle';
  child = document.createElement('input');
  child.id = 'fill-from-text-weak';
  child.value = 'Fill from text (keep values)';
  child.type = 'button';
  child.style.width = '11.5rem';
  child.addEventListener("click", fill_from_text, false);
  x[1].appendChild(child);
  elem.appendChild(x[1]);
  tbl.appendChild(elem);

  tb = document.createElement('tbody');
  tb.appendChild(tbl);
  tbl = document.createElement('table');
  tbl.cellPadding = 3;
  tbl.cellSpacing = '';
  tbl.border = 0;
  tbl.className = 'layout border';
  tbl.width = '100%';
  tbl.appendChild(tb);
  ref.parentNode.insertBefore(tbl, ref);
} else if (document.URL.indexOf('/torrents.php?action=editgroup') >= 0) {
  ref = document.querySelector('form.edit_form > div > div > input[type="submit"]');
  if (ref == null) return;
  elem = document.createElement('br');
  ref.parentNode.insertBefore(elem, ref);

  tbl = document.createElement('tr');
  elem = document.createElement('td');
  child = document.createElement('textarea');
  child.id = 'import_data';
  child.name = 'import_data';
  child.cols = 50;
  child.rows = 3;
  child.style.width = '35rem';
  child.style.height = '3rem';
  child.className = ' wbbarea';
  child.setAttribute('data-wbb', '');
  elem.appendChild(child);
  tbl.appendChild(elem);
  elem = document.createElement('td');
  elem.align = 'right';
  child = document.createElement('input');
  child.id = 'append-from-text';
  child.value = 'Fill from text (append)';
  child.type = 'button';
  child.addEventListener("click", fill_from_text, false);
  elem.appendChild(child);
  tbl.appendChild(elem);
  tb = document.createElement('tbody');
  tb.appendChild(tbl);
  tbl = document.createElement('table');
  tbl.cellPadding = 3;
  tbl.cellSpacing = '';
  tbl.border = 0;
  tbl.className = 'layout border';
  tbl.width = '100%';
  tbl.appendChild(tb);
  ref.parentNode.insertBefore(tbl, ref);
}
// Hide DNU list (warning - risky!)
//if ((ref = document.querySelector('div#content > div:first-of-type')) != null) ref.style.display = 'none';

class TagManager extends Array {
  constructor() {
	super();
	this.substitutions = [
	  [/^Alternative\s*&\s*Indie$/i, 'alternative', 'indie'],
	  [/^Pop and Rock$/i, 'pop', 'rock'],
	  [/^Rock and Pop$/i, 'pop', 'rock'],
	  [/^World\s*&\s*Country$/i, 'world.music', 'country'],
	  [/^Jazz Fusion\s*&\s*Jazz Rock$/i, 'Jazz Fusion', 'Jazz Rock'],
	  [/^(?:Singer\s*&\s*)?Songwriter$/i, 'singer.songwriter'],
	  [/^Singer and Songwriter$/i, 'singer.songwriter'],
	  [/^Rock\s*'?N\s+Roll$/i, 'roock.and.roll'],
	  [/^R\s*&\s*B$/i, 'rhytm.and.blues'],
	  [/^Drum\s*&\s*Bass$/i, 'drum.and.bass'],
	  [/^Rock\s*(?:[\-\/]\s*)?Pop$/i, 'pop.rock'],
	  [/^(?:film )?soundtracks?$/i, 'score'],
	  [/^electro$/i, 'electronic'],
	  [/^metal$/i, 'heavy.metal'],
	  [/^nonfiction$/i, 'non.fiction'],
	];
  }

  add(...tags) {
	var added = 0;
	for (var tag of tags) {
	  if (typeof tag != 'string') continue;
	  tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(function(tag) {
		tag = tag.normalize("NFD").replace(/[\u0300-\u036f]/g, '').trim();
		if (tag.length <= 0 || tag == '?') return;
		for (var k of this.substitutions) {
		  if (k[0].test(tag)) { added += this.add(...k.slice(1)); return; }
		}
		tag = tag.replace(/\s*[&\+]\s*/g, ' and ').replace(/[\!\@\#\$\%\^\*\?\<\"\[\{\]\}\=]+/g, '').
			replace(/[\s\-\_\.\'\`\~]+/g, '.').toLowerCase();
		if (!this.includes(tag)) {
		  this.push(tag);
		  ++added;
		}
	  }.bind(this));
	}
	return added;
  }
  toString() {
	return this.length > 0 ? this.sort().join(', ') : '';
  }
};

function fill_from_text() {
  var overwrite = this.id != 'fill-from-text-weak';
  var clipBoard = document.getElementById('import_data');
  if (clipBoard == null) return false;
  //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
  //if (typeof clipBoard != 'string') return false;
  var category = document.getElementById('categories');
  var ref, iter, i, matches, rx;
  if (category == null && document.getElementById('releasetype') != null
	  || category != null && category.value == 0) return fill_from_text_music();
  if (category != null && category.value == 1) return fill_from_text_apps();
  if (category != null && (category.value == 2 || category.value == 3)) return fill_from_text_books();
  return category == null ? fill_from_text_apps() || fill_from_text_books() : false;

  function fill_from_text_music() {
	var prefs = {
	  set: function(prop, def) { this[prop] = GM_getValue(prop, def) }
	};
	const div = ['—', '⸺', '⸻'];
	var lines = clipBoard.value.split(/[\r\n]+/);
	let track, tracks = [];
	prefs.set('remap_texttools_newlines', 0); // convert underscores to linebreaks (ambiguous)
	prefs.set('clean_on_apply', 0); // clean the input box on successfull fill
	// 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('tracklist_head_color', '#4682B4');
	prefs.set('tracklist_single_color', '#708080');
	// classical tracklist only components colouring
	prefs.set('tracklist_discsubtitle_color', '#008B8B');
	prefs.set('tracklist_classicalblock_color', 'Olive');
	prefs.set('tracklist_tracknumber_color', '#8899AA');
	prefs.set('tracklist_artist_color', '#889B2F');
	prefs.set('tracklist_composer_color', '#556B2F');
	prefs.set('tracklist_duration_color', '#4682B4');

	for (iter of lines) {
	  let metaData = iter.split('\x1E');
	  track = {
		artist: metaData.shift().trim() || null,
		album: metaData.shift().trim() || null,
		album_year: extract_year(metaData.shift().trim()),
		release_year: extract_year(metaData.shift().trim()),
		label: metaData.shift().trim() || null,
		catalog: metaData.shift().trim() || null,
		encoding: metaData.shift().trim() || null,
		codec: metaData.shift().trim() || null,
		codec_profile: metaData.shift().trim() || null,
		bitrate: parseFloat(metaData.shift().trim()) || null,
		bd: parseInt(metaData.shift().trim()) || null,
		sr: parseInt(metaData.shift().trim()) || null,
		channels: parseInt(metaData.shift().trim()) || null,
		media: metaData.shift().trim() || null,
		genre: metaData.shift().trim() || null,
		discnumber: parseInt(metaData.shift().trim()) || null,
		totaldiscs: parseInt(metaData.shift().trim()) || null,
		discsubtitle: metaData.shift().trim() || null,
		tracknumber: metaData.shift().trim() || null,
		totaltracks: parseInt(metaData.shift().trim()) || null,
		title: metaData.shift().trim() || null,
		track_artist: metaData.shift().trim() || null,
		performer: metaData.shift().trim() || null,
		composer: metaData.shift().trim() || null,
		conductor: metaData.shift().trim() || null,
		remixer: metaData.shift().trim() || null,
		compiler: metaData.shift().trim() || null,
		producer: metaData.shift().trim() || null,
		duration: parseFloat(metaData.shift().trim()) || null,
		rg: metaData.shift().trim() || null,
		dr: metaData.shift().trim() || null,
		vendor: metaData.shift().trim() || null,
		url: metaData.shift().trim() || null,
		dirpath: metaData.shift() || null,
		comment: metaData.shift().trim() || null,
	  };
	  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
	  tracks.push(track);
	}
	var album_artists = [], albums = [], album_years = [], release_years = [], labels = [], catalogs = [];
	var codecs = [], bds = [], medias = [], genres = [], srs = {}, urls = [], comments = [], track_artists = [];
	var encodings = [], bitrates = [], codec_profiles = [], drs = [], channels = [], rgs = [], dirpaths = [];
	var vendors = [];
	let is_va = false, composer_significant = false, is_from_dsd = false, is_classical = false;
	var total_time = 0, release_type = 1, album_bitrate = 0, totaldiscs = 1, artist_counter = 0;
	var edition_title, media, yadg_prefil = '';
	const featParser1 = /\(feat(?:\.|uring)\s+([^\(\)]+?)\s*\)$/i;
	const featParser2 = /\[feat(?:\.|uring)\s+([^\[\]]+?)\s*\]$/i;
	for (iter of tracks) {
	  push_unique(album_artists, 'artist');
	  push_unique(track_artists, 'track_artist');
	  push_unique(albums, 'album');
	  push_unique(album_years, 'album_year');
	  push_unique(release_years, 'release_year');
	  push_unique(labels, 'label');
	  push_unique(catalogs, 'catalog');
	  push_unique(encodings, 'encoding');
	  push_unique(codecs, 'codec');
	  push_unique(codec_profiles, 'codec_profile');
	  push_unique(bitrates, 'bitrate');
	  push_unique(bds, 'bd');
	  push_unique(channels, 'channels');
	  push_unique(medias, 'media');
	  if (iter.sr) {
		if (typeof srs[iter.sr] != 'number') {
		  srs[iter.sr] = iter.duration;
		} else {
		  srs[iter.sr] += iter.duration;
		}
	  }
	  push_unique(genres, 'genre');
	  push_unique(urls, 'url');
	  push_unique(comments, 'comment');
	  push_unique(rgs, 'rg');
	  push_unique(drs, 'dr');
	  push_unique(vendors, 'vendor');
	  push_unique(dirpaths, 'dirpath');

	  if (iter.discnumber > totaldiscs) totaldiscs = iter.discnumber;
	  total_time += iter.duration;
	  album_bitrate += iter.duration * iter.bitrate;
	}
	function push_unique(array, prop) {
	  if (iter[prop] !== undefined && iter[prop] !== null && (typeof iter[prop] != 'string' || iter[prop].length > 0)
		  && !array.includes(iter[prop])) array.push(iter[prop]);
	}
	// inconsistent releases not allowed - die
	if (encodings.length > 1) { push_warning('Fuzzy releases aren\'t allowed (encoding): ' + encodings); return false; }
	if (codecs.length > 1) { push_warning('Fuzzy releases aren\'t allowed (codec): ' + codecs); return false; }
	if (codec_profiles.length > 1) { push_warning('Fuzzy releases aren\'t allowed (codec profile): ' + codec_profiles); return false; }
	if (vendors.length > 1) { push_warning('Fuzzy releases aren\'t allowed (vendor): ' + vendors); return false; }
	if (medias.length > 1) { push_warning('Fuzzy releases aren\'t allowed (media): ' + medias); return false; }
	if (channels.length > 1) { push_warning('Fuzzy releases aren\'t allowed (channel): ' + channels); return false; }
	if (album_artists.length > 1) { push_warning('Fuzzy releases aren\'t allowed (album artists): ' + album_artists); return false; }
	if (albums.length > 1) { push_warning('Fuzzy releases aren\'t allowed (album): ' + albums); return false; }
	album_bitrate /= total_time;
	if (total_time <= single_threshold) {
	  release_type = 9; // single
	} else if (total_time <= EP_threshold) {
	  release_type = 5; // EP
	}
	if (album_artists.length == 1 && (ref = document.getElementById('artist')) != null) {
	  let artist_parser = /\s*(?:[\,\;\/\|]|(?:&)\s+(?!(?:The|His|Friends)\b))+\s*/i;
	  let weak_artist_parser = /\s*[\,\;\/\|]+\s*/;
	  let guest_parser = /^(.*?)(?:\s+(?:feat(?:\.|uring)|with)\s+(.*))?$/;
	  let main_artists = [], guests = [], composers = [], conductors = [];
	  var remixers = [], performers = [], compilers = [], producers = [];
	  if (matches = album_artists[0].match(guest_parser)) {
		let j;
		function twoOrMore(k) { return k.length >= 2 };
		if (matches[1].search(/^(?:Various(?: Artists?)?|VA)$/) >= 0) {
		  is_va = true;
		} else {
		  j = matches[1].split(artist_parser);
		  main_artists = j.every(twoOrMore) ? j : [ matches[0] ];
		  yadg_prefil = matches[1];
		}
		if (!is_va && matches[2]) {
		  guests = matches[2].split(weak_artist_parser);
		  if (!guests.every(twoOrMore)) guests = matches[2];
		}
		for (iter of tracks) {
		  add_track_artists('track_artist');
		  add_track_artists('performer');
		  if (iter.title
			  && ((matches = iter.title.match(/\(remix(?:ed)? by ([^\(\)]+)\)/i))
				  || (matches = iter.title.match(/\(([^\(\)]+?)(?:'s)? remix\)/i))
				  || (matches = iter.title.match(/\[remix(?:ed)? by ([^\[\]]+)\]/i))
				  || (matches = iter.title.match(/\[([^\[\]]+?)(?:'s)? remix\]/i)))) {
			j = matches[1].split(weak_artist_parser);
			for (i of j.every(twoOrMore) ? j : [ matches[0] ]) { if (!remixers.includes(i)) remixers.push(i); }
		  }
		  if (iter.title && ((matches = iter.title.match(featParser1)) || (matches = iter.title.match(featParser2)))) {
			j = matches[1].split(weak_artist_parser);
			for (i of j.every(twoOrMore) ? j : [ matches[1] ]) { if (!guests.includes(i)) guests.push(i) }
		  }
		  add_artists(composers, 'composer');
		  add_artists(conductors, 'conductor');
		  add_artists(compilers, 'compiler');
		  add_artists(remixers, 'remixer');
		  add_artists(producers, 'producer');
		} // loop tracks
		// split ampersands
		for (i = main_artists.length; i > 0; --i) {
		  j = main_artists[i - 1].split(' & ');
		  if (j.length >= 2 && j.every(twoOrMore) && j.some(k => main_artists.includes(k) || guests.includes(k))) {
			main_artists.splice(i - 1, 1, ...j.filter(k => !main_artists.includes(k)));
		  }
		}
		for (i = guests.length; i > 0; --i) {
		  j = guests[i - 1].split(' & ');
		  if (j.length >= 2 && j.every(twoOrMore) && j.some(k => main_artists.includes(k) || guests.includes(k))) {
			guests.splice(i - 1, 1, ...j.filter(k => !main_artists.includes(k) && !guests.includes(k)));
		  }
		}
		estimate_artists_from_comments(conductors, 'conductor');
		//estimate_artists_from_comments(producers, 'producer');
		function add_track_artists(prop) {
		  if (iter[prop] && (matches = iter[prop].match(guest_parser))) {
			j = matches[1].split(weak_artist_parser);
			for (i of j.every(twoOrMore) ? j : [ matches[1] ]) {
			  if (!main_artists.includes(i) && (is_va || !guests.includes(i))) {
				if (is_va) { main_artists.push(i) } else { guests.push(i) }
			  }
			}
			if (matches[2]) {
			  j = matches[2].split(weak_artist_parser);
			  for (i of j.every(twoOrMore) ? j : [ matches[2] ]) {
				if (!main_artists.includes(i) && !guests.includes(i)) guests.push(i);
			  }
			}
		  }
		}
		function add_artists(list, prop) {
		  if (!iter[prop]) return;
		  j = iter[prop].split(weak_artist_parser);
		  for (i of j.every(twoOrMore) ? j : [ iter[prop] ]) { if (!list.includes(i)) list.push(i) }
		}
		function estimate_artists_from_comments(list, expr) {
		  var rx = new RegExp('^(.*)\s*:\s*(?:' + expr + ')$', 'im');
		  for (var k of comments) {
			if ((matches = rx.exec(k)) && !list.includes(matches[1])) list.push(matches[1]);
		  };
		}
		if (!ref.disabled) {
		  let artist_index = 0;
		  feed_artist_category(main_artists, 1);
		  feed_artist_category(guests.filter(k => !main_artists.includes(k) && !conductors.includes(k)), 2);
		  feed_artist_category(remixers, 3);
		  feed_artist_category(composers, 4);
		  feed_artist_category(conductors, 5);
		  feed_artist_category(compilers, 6);
		  feed_artist_category(producers, 7);
		  function feed_artist_category(list, type) {
			for (iter of list.sort()) {
			  let id = 'artist';
			  if (artist_index > 0) {
				id += '_' + artist_index;
				if (document.getElementById(id) == null) add_artist();
			  }
			  ref = document.getElementById(id);
			  if (ref != null && (overwrite || !ref.value)) {
				ref.value = iter;
				ref.nextElementSibling.value = type;
			  }
			  ++artist_index;
			}
		  }
		}
	  }
	}
	if (is_va && release_type == 1) release_type = 7; // compilation
	if (albums.length == 1) {
	  let album = albums[0];
	  rx = /\s+(?:-\s+Single|\[Single\]|\(Single\))$/i;
	  if (rx.test(album)) {
		album = album.replace(rx, '');
		release_type = 9; // single
	  }
	  rx = /\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/;
	  if (rx.test(album)) {
		album = album.replace(rx, '')
		release_type = 5; // EP
	  }
	  rx = /\s+\((?:Live|En directo?|Ao Vivo)\b[^\(\)]*\)$/i;
	  if (rx.test(album)) {
		//album = album.replace(rx, '')
		if (release_type == 1) release_type = 11; // live album
	  }
	  rx = /\s+\[(?:Live|En directo?|Ao Vivo)\b[^\[\]]*\]$/i;
	  if (rx.test(album)) {
		//album = album.replace(rx, '')
		if (release_type == 1) release_type = 11; // live album
	  }
	  if (album.search(/(?:^Live|^Directo? [Ee]n|\bUnplugged|\bAcoustic Stage)\b/) >= 0 && release_type == 1) {
		release_type = 11; // live album
	  }
	  rx = /\b(?:Best [Oo]f|Greatest Hits)\b/;
	  if (rx.test(album) && release_type == 1) release_type = 6; // Anthology
	  rx = '\\b(?:Soundtrack|Score|Motion Picture|Series|Television|Original(?: \w+)? Cast|Music from|Musique originale|Bande originale)\\b';
	  if (reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
		//album = album.replace(rx, '')
		release_type = 3; // soundtrack
		composer_significant = true;
	  }
	  rx = /\s+(?:\([^\(\)]*\bRemix(?:e[ds])?\b[^\(\)]*\)|Remix(?:e[ds])?)$/i;
	  if (rx.test(album)) {
		//album = album.replace(rx, '')
		if (release_type == 1) release_type = 13; // remix
	  }
	  rx = /\s+\[[^\[\]]*\bRemix(?:e[ds])?\b[^\[\]]*\]$/i;
	  if (rx.test(album)) {
		//album = album.replace(rx, '')
		if (release_type == 1) release_type = 13; // remix
	  }
	  rx = /\s+\(([^\(\)]*\b(?:Remaster(?:ed)?\b[^\(\)]*|Reissue|Edition|Version))\)$/i;
	  if (matches = rx.exec(album)) {
		album = album.replace(rx, '');
		edition_title = matches[1];
	  }
	  rx = /\s+\[([^\[\]]*\b(?:Remaster(?:ed)?\b[^\[\]]*|Reissue|Edition|Version))\]$/i;
	  if (matches = rx.exec(album)) {
		album = album.replace(rx, '');
		edition_title = matches[1];
	  }
	  rx = /\s+-\s+([^\[\]\(\)\-]*\b(?:(?:Remaster(?:ed)?|Bonus Track)\b[^\[\]\(\)\-]*|Reissue|Edition|Version))$/i;
	  if (matches = rx.exec(album)) {
		album = album.replace(rx, '');
		edition_title = matches[1];
	  }
	  if (featParser1.test(album)) album = album.replace(featParser1, '');
	  if (featParser2.test(album)) album = album.replace(featParser1, '');
	  rx = /\s+(?:\[LP\]|\(LP\))$/;
	  if (matches = rx.exec(album)) { album = album.replace(rx, ''); media = 'Vinyl'; }
	  rx = /\s+(?:\[SACD\]|\(SACD\))$/;
	  if (matches = rx.exec(album)) { album = album.replace(rx, ''); media = 'SACD'; }
	  rx = /\s+(?:\[(?:Blu[\s\-]?Ray|B[DR])\]|\((?:Blu[\s\-]?Ray|B[DR])\))$/;
	  if (matches = rx.exec(album)) { album = album.replace(rx, ''); media = 'Blu-Ray'; }
	  rx = /\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/;
	  if (matches = rx.exec(album)) { album = album.replace(rx, ''); media = 'DVD'; }
	  if (element_writable(ref = document.getElementById('title'))) ref.value = album;
	  if (yadg_prefil) { yadg_prefil += ' - ' }
	  yadg_prefil += album;
	}
	if (yadg_prefil && (ref = document.getElementById('yadg_input')) != null) {
	  ref.value = yadg_prefil;
	  ref = document.getElementById('yadg_submit');
	  if (ref != null && !ref.disabled) ref.click();
	}
	if (album_years.length == 1) {
	  if (element_writable(ref = document.getElementById('year'))) ref.value = album_years[0];
	} else if (album_years.length > 1) {
	  push_warning('Warning: inconsistent album year accross album: ' + album_years);
	}
	if (release_years.length == 1) {
	  if (element_writable(ref = document.getElementById('remaster_year'))) ref.value = release_years[0];
	} else if (release_years.length > 1) {
	  push_warning('Warning: inconsistent release year accross album: ' + release_years);
	}
	if (edition_title) {
	  if (element_writable(ref = document.getElementById('remaster_title'))) ref.value = edition_title;
	}
	rx = /\s*[\,\;]\s*/g;
	if (labels.length == 1 && element_writable(ref = document.getElementById('remaster_record_label'))) {
	  ref.value = labels[0].replace(rx, ' / ');
	} else if (labels.length > 1) {
	  push_warning('Warning: inconsistent label accross album: ' + labels);
	}
	if (catalogs.length > 0 && element_writable(ref = document.getElementById('remaster_catalogue_number'))) {
	  ref.value = catalogs.map(k => k.replace(rx, ' / ')).join(' / ');
	}
	var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
	if (codecs.length > 0 && element_writable(ref = document.getElementById('format'))) {
	  ref.value = codecs[0];
	  exec(function() { Format() });
	}
	let sel;
	if (encodings[0] == 'lossless') {
	  if (!bds.every(k => [16, 24].includes(k))) {
		sel = null; // album containing disallowed bit depth
	  } else {
		sel = bds.includes(24) ? '24bit Lossless' : 'Lossless';
	  }
	} else if (bitrates.length > 0) {
	  let lame_version = vendors.length > 0 && (matches = vendors[0].match(/^LAME(\d+)\.(\d+)/i)) ?
		  parseInt(matches[1]) * 1000 + parseInt(matches[2]) : undefined;
	  if (codec_profiles.length == 1 && codec_profiles[0] == 'VBR V0') {
		sel = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
	  } else if (codec_profiles.length == 1 && codec_profiles[0] == 'VBR V1') {
		sel = 'V1 (VBR)'
	  } else if (codec_profiles.length == 1 && codec_profiles[0] == 'VBR V2') {
		sel = lame_version >= 3094 ? sel = 'V2 (VBR)' : 'APS (VBR)'
	  } else if (bitrates.length == 1 && [192, 256, 320].includes(Math.round(bitrates[0]))) {
		sel = Math.round(bitrates[0]);
	  } else if (bitrates.length >= 1) {
		if (element_writable(ref = document.getElementById('bitrate')) && ref.value != 'Other') {
		  ref.value = 'Other';
		  exec(function() { Bitrate() });
		}
		if (element_writable(ref = document.getElementById('other_bitrate'))) {
		  ref.value = Math.round(bitrates.length == 1 ? bitrates[0] : album_bitrate);
		  if ((ref = document.getElementById('vbr')) != null) ref.checked = bitrates.length > 1;
		}
	  }
	}
	if (sel && (ref = document.getElementById('bitrate')) != null && !elem.disabled && (overwrite || !br_isSet)) {
	  ref.value = sel;
	}
	if (medias.length > 0) {
	  sel = undefined;
	  if (medias[0].search(/\b(?:WEB|File|Digital Download)\b/i) >= 0) sel = 'WEB';
	  if (medias[0].search(/\bCD\b/) >= 0) sel = 'CD';
	  if (medias[0].search(/\b(?:SACD|Hybrid)\b/) >= 0) sel = 'SACD';
	  if (medias[0].search(/\bBlu[-\s]?Ray\b/i) >= 0) sel = 'Blu-Ray';
	  if (medias[0].search(/\bDVD(?:-?A)?\b/) >= 0) sel = 'DVD';
	  if (medias[0].search(/\b(?:Vinyl\b|LP\b|12"|7")/) >= 0) sel = 'Vinyl';
	  if (sel) media = sel;
	  if (media && element_writable(ref = document.getElementById('media'))) ref.value = sel;
	}
	if (genres.length >= 1) {
	  let _genres = new TagManager();
	  for (let genre of genres) {
		if (genre.search(/\b(?:Classical|Symphony|Symphonic(?:al)?$|Chamber|Choral|Opera|Klassik|Duets)\b/i) >= 0)
		{ composer_significant = true; is_classical = true }
		if (genre.search(/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i) >= 0)
		{ composer_significant = true; if (release_type == 1) release_type = 3; }
	  	_genres.add(genre);
	  }
	  if (_genres.length > 0 && element_writable(ref = document.getElementById('tags'))) ref.value = _genres.toString();
	}
	if (element_writable(ref = document.getElementById('releasetype'))) ref.value = release_type;

	var description, ripinfo, dur, vinyl_test = /^(Vinyl rip by\s+)(.*)$/im;
	if (tracks.length > 1) {
	  gen_full_tracklist();
	} else { // single
	  description = '[align=center]';
	  description += isRED() ? '[pad=20|20|20|20]' : '';
	  description += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + album_artists[0] + '[/color][hr]';
	  //description += '[color=' + prefs.tracklist_single_color + ']';
	  description += tracks[0].title;
	  //description += '[/color]'
	  description += '[/b]';
	  if (tracks[0].composer) {
		description += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
	  }
	  description += '\n\n[color=' + prefs.tracklist_duration_color +'][' +
		make_time_string(tracks[0].duration) + '][/color][/size]';
	  if (isRED()) description += '[/pad]';
	  description += '[/align]';
	}

	if (comments.length == 1 && comments[0]) {
	  let cmt = comments[0];
	  if (matches = cmt.match(vinyl_test)) {
		ripinfo = cmt.slice(matches.index).trim().split(/[\r\n]+/);
		description = description.concat('\n\n', cmt.slice(0, matches.index).trim());
	  } else {
		description = description.concat('\n\n', cmt);
	  }
	}
	if (element_writable(ref = document.getElementById('album_desc'))) ref.value = description;
	if ((ref = document.getElementById('body')) != null && !ref.disabled) {
	  let editioninfo;
	  if (edition_title) {
		editioninfo = '[size=5][b]' + edition_title;
		if (release_years.length >= 1) { editioninfo = editioninfo.concat(' (', release_years[0] + ')') }
		editioninfo = editioninfo.concat('[/b][/size]\n\n');
	  } else { editioninfo = '' }
	  if (ref.textLength > 0) {
		ref.value = ref.value.concat('\n\n', editioninfo, description);
	  } else {
		ref.value = editioninfo + description;
	  }
	}
	let lineage = '', comment = '', drinfo, srcinfo;
	if (Object.keys(srs).length > 0) {
	  let kHz = Object.keys(srs).sort((a, b) => srs[b] - srs[a]).map(f => f / 1000).join('/').concat('kHz');
	  if (element_writable(ref = document.getElementById('release_samplerate'))) {
		ref.value = Object.keys(srs).length > 1 ? '999' : Math.floor(Object.keys(srs)[0] / 1000);
	  }
	  if (bds.includes(24)) {
		if (drs.length >= 1) drinfo = '[hide=DR' + (drs.length == 1 ? drs[0] : '') + '][pre][/pre]';
		if (media == 'Vinyl') {
		  let hassr = ref == null || Object.keys(srs).length > 1;
		  lineage = hassr ? kHz + ' ' : '';
		  if (ripinfo) {
			ripinfo[0] = ripinfo[0].replace(vinyl_test, '$1[color=blue]$2[/color]');
			if (hassr) { ripinfo[0] = ripinfo[0].replace(/^Vinyl\b/, 'vinyl') }
			lineage += ripinfo[0] + '\n\n[u]Lineage:[/u]' + ripinfo.slice(1).map(k => '\n' + k).join('');
		  } else {
			lineage += (hassr ? 'Vinyl' : ' vinyl') + ' rip by [color=blue][/color]\n\n[u]Lineage:[/u]';
		  }
		  if (drs.length >= 1) drinfo += '\n\n[img][/img]\n[img][/img]\n[img][/img][/hide]';
		} else if (['Blu-Ray', 'DVD', 'SACD'].includes(media)) {
		  lineage = ref ? '' : kHz;
		  if (channels.length == 1) add_channel_info();
		  if (media == 'SACD' || is_from_dsd) {
			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(srs).length > 1) lineage = kHz;
		  if (channels.length == 1 && channels[0] != 2) add_channel_info();
		  add_dr_info();
		  //if (lineage.length > 0) add_rg_info();
		  if (bds.length >= 2) {
			let hybrid_tracks = tracks.filter(k => k.bd < 24).map(k => k.tracknumber);
			if (hybrid_tracks) {
			  if (lineage) lineage += '\n';
			  lineage += 'Note: track';
			  if (hybrid_tracks.length > 1) lineage += 's';
			  lineage += ' #' + hybrid_tracks.sort().join(', ') +
				(hybrid_tracks.length > 1 ? ' are' : ' is') + ' 16bit lossless';
			}
		  }
		  drinfo = Object.keys(srs).includes(88200) ? drinfo.concat('[/hide]') : null;
		}
	  } else { // 16bit or lossy
		if (Object.keys(srs).some(f => f != 44100)) lineage = kHz;
		if (channels.length == 1 && channels[0] != 2) add_channel_info();
		//add_dr_info();
		//if (lineage.length > 0) add_rg_info();
		if (['AAC', 'Opus', 'Vorbis'].includes(codecs[0]) && vendors[0]) {
		  let _encoder_settings = vendors[0];
		  if (codecs[0] == 'AAC' && vendors[0].search(/^qaac\s+[\d\.]+/i) >= 0) {
			let enc = [];
			if (matches = vendors[0].match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
			if (matches = vendors[0].match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
			if (matches = vendors[0].match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
			if (matches = vendors[0].match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
			if (matches = vendors[0].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 (drs.length != 1 || document.getElementById('release_dynamicrange') != null) return false;
	  if (lineage.length > 0) lineage += ' | ';
	  if (drs[0] < 4) lineage += '[color=red]';
	  lineage += 'DR' + drs[0];
	  if (drs[0] < 4) lineage += '[/color]';
	  return true;
	}
	function add_rg_info() {
	  if (rgs.length != 1) return false;
	  if (lineage.length > 0) lineage += ' | ';
	  lineage += 'RG'; //lineage += 'RG ' + rgs[0];
	  return true;
	}
	function add_channel_info() {
	  if (channels.length != 1) return false;
	  let chi = getChanString(channels[0]);
	  if (lineage.length > 0 && chi.length > 0) lineage += ', ';
	  lineage += chi;
	  return chi.length > 0;
	}
	if (urls.length == 1 && urls[0]) {
	  srcinfo = '[url]' + urls[0] + '[/url]';
	  if (element_writable(document.getElementById('image'))) {
		let u = urls[0];
		if (u.search(/^https?:\/\/(\w+\.)?discogs\.com\/release\/[\w\-]+\/?$/i) >= 0) u += '/images/';
	  	GM_xmlhttpRequest({ method: 'GET', url: u, onload: fetch_image_from_store });
	  }
// 	} else if (element_writable(document.getElementById('image'))
// 	  && ((ref = document.getElementById('album_desc')) != null || (ref = document.getElementById('body')) != null)
// 		&& ref.textLength > 0 && (matches = ref.value.matchAll(/\b(https?\/\/[\w\-\&\_\?\=]+)/i)) != null) {
	}
	ref = document.getElementById('release_lineage');
	if (ref != null) {
	  if (element_writable(ref)) {
		if (drinfo) comment = drinfo;
		if (lineage && srcinfo) lineage += '\n\n';
		if (srcinfo) lineage += srcinfo;
		ref.value = lineage;
	  }
	} else {
	  comment = lineage;
	  if (comment && drinfo) comment += '\n\n';
	  if (drinfo) comment += drinfo;
	  if (comment && srcinfo) comment += '\n\n';
	  if (srcinfo) comment += srcinfo;
	}
	if (comment.length > 0) {
	  if (element_writable(ref = document.getElementById('release_desc'))) ref.value = comment;
	}
	if (encodings[0] == 'lossless' && codecs[0] == 'FLAC' && bds.includes(24) && dirpaths.length == 1) {
	  var uri = new URL(dirpaths[0] + '\\foo_dr.txt');
	  GM_xmlhttpRequest({
		method: 'GET',
		url: uri.href,
		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 (drs.length == 1) {
	  if (element_writable(ref = document.getElementById('release_dynamicrange'))) ref.value = drs[0];
	}
	if (prefs.clean_on_apply) document.getElementById('import_data').value = null;
	for (iter in prefs) {
	  if (typeof prefs[iter] != 'function') GM_setValue(iter, prefs[iter]);
	}
	if (tb && warning) tb.removeChild(warning);
	return true;

	function gen_full_tracklist() { // ========================= TACKLIST =========================
	  description = isRED() ? '[pad=5|0|0|0]' : '';
	  description += '[size=4][color=' + prefs.tracklist_head_color + '][b]Tracklisting[/b][/color][/size]';
	  if (isRED()) '[/pad]';
	  let classical_units = new Set();
	  if (is_classical) {
		for (track of tracks) {
		  if (matches = track.title.match(/^(.+?)\s*:\s+(.*)$/)) {
			classical_units.add(track.classical_unit_title = matches[1]);
			track.classical_title = matches[2];
		  } else {
			track.classical_unit_title = null;
		  }
		}
		for (let unit of classical_units.keys()) {
		  let group_performer = array_homogenous(tracks.filter(k => k.classical_unit_title === unit).map(k => k.track_artist));
		  let group_composer = array_homogenous(tracks.filter(k => k.classical_unit_title === unit).map(k => k.composer));
		  for (track of tracks) {
			if (track.classical_unit_title !== unit) continue;
			if (group_composer) track.classical_unit_composer = track.composer;
			if (group_performer) track.classical_unit_performer = track.track_artist;
		  }
		}
	  }
	  let block = 1, lastdisc, lastsubtitle, lastside, vinyl_trackwidth;
	  let lastwork = classical_units.size > 0 ? null : undefined;
	  description += '\n';
	  let volumes = new Map(tracks.map(k => [k.discnumber, undefined]));
	  volumes.forEach(function(val, key) {
		volumes.set(key, array_homogenous(tracks.filter(k => k.discnumber == key).map(k => k.discsubtitle)));
	  });
	  if (media == 'Vinyl') {
		let max_side_track = undefined;
		rx = /^([A-Z])(\d+)?(\.(\d+))?/i;
		for (iter of tracks) {
		  if (matches = iter.tracknumber.match(rx)) {
			max_side_track = Math.max(parseInt(matches[2]) || 1, max_side_track || 0);
		  }
		}
		if (typeof max_side_track == 'number') {
		  max_side_track = max_side_track.toString().length;
		  vinyl_trackwidth = 1 + max_side_track;
		  for (iter of tracks) {
			if (matches = iter.tracknumber.match(rx)) {
			  iter.tracknumber = matches[1].toUpperCase();
			  if (matches[2]) iter.tracknumber += matches[2].padStart(max_side_track, '0');
			}
		  }
		}
	  }
	  function prologue(prefix, postfix) {
		function block1() {
		  if (block == 3) description += postfix;
		  description += '\n';
		  block = 1;
		}
		function block2() {
		  if (block == 3) description += postfix;
		  description += '\n';
		  block = 2;
		}
		function block3() {
		  if (block == 2) { description += '[hr]' } else { description += '\n' }
		  if (block != 3) description += prefix;
		  block = 3;
		}
		if (totaldiscs > 1 && iter.discnumber != lastdisc) {
		  block1();
		  description += '[size=3][color=' + prefs.tracklist_discsubtitle_color + '][b]Disc ' + iter.discnumber;
		  if (iter.discsubtitle && (!volumes.has(iter.discnumber) || volumes.get(iter.discnumber))) {
			description += ' - ' + iter.discsubtitle;
			lastsubtitle = iter.discsubtitle;
		  }
		  description += '[/b][/color][/size]';
		  lastdisc = iter.discnumber;
		}
		if (iter.discsubtitle != lastsubtitle) {
		  block1();
		  if (iter.discsubtitle) {
			description += '[size=2][color=' + prefs.tracklist_discsubtitle_color + '][b]' +
			  iter.discsubtitle + '[/b][/color][/size]';
		  }
		  lastsubtitle = iter.discsubtitle;
		}
		if (iter.classical_unit_title !== lastwork) {
		  if (iter.classical_unit_composer || iter.classical_unit_title || iter.classical_unit_performer) {
			block2();
			description += '[size=2][color=' + prefs.tracklist_classicalblock_color + '][b]';
			if (iter.classical_unit_composer) description += iter.classical_unit_composer + ': ';
			if (iter.classical_unit_title) description += iter.classical_unit_title;
			description += '[/b]';
			if (iter.classical_unit_performer) description += ' (' + iter.classical_unit_performer + ')';
			description += '[/color][/size]';
		  } else {
			if (block != 2) block1();
		  }
		  lastwork = iter.classical_unit_title;
		}
		block3();
		if (media == 'Vinyl') {
		  let c = iter.tracknumber[0].toUpperCase();
		  if (lastside != undefined && c != lastside) description += '\n';
		  lastside = c;
		}
	  }
	  for (iter of tracks.sort(function(a, b) {
		var d = a.discnumber - b.discnumber;
		var t = a.tracknumber - b.tracknumber;
		return isNaN(d) || d == 0 ? isNaN(t) ? a.tracknumber.localeCompare(b.tracknumber) : t : d;
	  })) {
		let title = '';
		let ttwidth = vinyl_trackwidth || Math.max((iter.totaltracks || tracks.length).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.classical_unit_performer) {
			title = '[color=' + prefs.tracklist_artist_color + ']' + iter.track_artist + '[/color] - ';
		  }
		  title += iter.classical_title || iter.title;
		  if (iter.composer && composer_significant && !iter.classical_unit_composer) {
			title = title.concat(' [color=', prefs.tracklist_composer_color, '](', iter.composer, ')[/color]');
		  }
		  description += track + title + ' [i][color=' + prefs.tracklist_duration_color +'][' +
			make_time_string(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.classical_unit_performer) title = iter.track_artist + ' - ';
		  title += iter.classical_title || iter.title;
		  if (iter.composer && composer_significant && !iter.classical_unit_composer) {
			title = title.concat(' (', iter.composer, ')');
		  }
		  dur = '[' + make_time_string(iter.duration) + ']';
		  let l = 0, width = prefs.max_tracklist_width - track.length - dur.length - 1;
		  while (title.length > 0) {
			let j = width;
			if (title.length > width) {
			  while (j > 0 && title[j] != ' ') { --j }
			  if (j <= 0) { j = width }
			}
			let left = title.slice(0, j).trim();
			if (++l <= 1) {
			  description = description.concat(track, left.padEnd(width, ' '), ' ', dur);
			  width = prefs.max_tracklist_width - track.length - 2;
			} else {
			  description = description.concat('\n', ' '.repeat(track.length), left);
			}
			title = title.slice(j).trim();
		  }
		}
		if (iter.title.search(/\((?:feat\.|featuring)\s/i) >= 0) {
		  push_warning('Featured artist(s) in track names', false, '#ff6600');
		}
	  }
	  if (prefs.tracklist_style == 1) {
		description += '\n\n' + div[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
		  ']Total time: [i]' + make_time_string(total_time) + '[/i][/color][/size]';
	  } else if (prefs.tracklist_style == 2) {
		dur = '[' + make_time_string(total_time) + ']';
		description = description.concat('\n\n', div[0].repeat(32).padStart(prefs.max_tracklist_width));
		description = description.concat('\n', 'Total time:'.padEnd(prefs.max_tracklist_width - dur.length), dur);
		description = description.concat('[/pre][/size]');
	  }
	}

	function getChanString(n) {
	  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 fetch_image_from_store(response) {
	  if (response.readyState != 4 || !response.responseText) return;
	  ref = document.getElementById('image');
	  var parser = new DOMParser();
	  var html = parser.parseFromString(response.responseText, "text/html");
	  if (response.finalUrl.toLowerCase().indexOf('qobuz.com') >= 0
		  && (ref = html.querySelector('div.album-cover > img')) != null) set_image(ref.src);
	  if (response.finalUrl.toLowerCase().indexOf('7digital.com') >= 0
		  && (ref = html.querySelector('span.release-packshot-image > img[itemprop="image"]')) != null) {
		set_image(ref.src);
	  }
	  if (response.finalUrl.toLowerCase().indexOf('highresaudio.com') >= 0
		  && (ref = html.querySelector('div.albumbody > img.cover[data-pin-media]')) != null) {
		set_image(ref.dataset.pinMedia);
	  }
	  if (response.finalUrl.toLowerCase().indexOf('hdtracks.com') >= 0
		  && (ref = html.querySelector('p.product-image > img')) != null) set_image(ref.src);
	  if (response.finalUrl.toLowerCase().indexOf('bandcamp.com') >= 0
		  && (ref = html.querySelector('div#tralbumArt > a.popupImage')) != null) set_image(ref.href);
	  if (response.finalUrl.toLowerCase().indexOf('discogs.com') >= 0
		  && (ref = html.querySelector('div#view_images > p:first-of-type > span > img')) != null) set_image(ref.src);
	  if (response.finalUrl.toLowerCase().indexOf('junodownload.com') >= 0
		  && (ref = html.querySelector('a.productimage')) != null) set_image(ref.href);
	  if (response.finalUrl.toLowerCase().indexOf('supraphonline.cz') >= 0
		  && (ref = html.querySelector('div.sexycover > img')) != null) set_image(ref.src.replace(/\?\d+$/, ''));
	}
  }

  function fill_from_text_apps() {
	if (clipBoard.value.search(/^https?:\/\//i) < 0) return false;
	var description, tags = new TagManager();
	if (clipBoard.value.toLowerCase().indexOf('//sanet') >= 0) {
	  GM_xmlhttpRequest({ method: 'GET', url: clipBoard.value, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) return;
		var parser = new DOMParser();
		var html = parser.parseFromString(response.responseText, "text/html");

		i = html.querySelector('h1.item_title > span');
		if (i != null && element_writable(ref = document.getElementById('title'))) {
		  ref.value = i.textContent.replace(/\(x64\)$/i, '(64-bit)').replace(/\bBuild\s+(\d+)/, 'build $1').
		  replace(/\bMultilingual\b/, 'multilingual').replace(/\bMultilanguage\b/, 'multilanguage');
		}

		i = html.querySelector('section.descr');
		if (i != null) {
		  description = '';
		  ref = html.querySelector('section.descr > div.release-info');
		  if (ref != null) var releaseInfo = ref.textContent.trim();
		  desc_extract(i);
		  ref = html.querySelector('div.txtleft > a');
		  if (ref != null) description += '\n\n[b]Product page:[/b]\n[url]' + de_anonymize(ref.href) + '[/url]';
		  write_description(description);

		  function desc_extract(node) {
			for (var i of node.childNodes) {
			  if (i.nodeType == 3) {
				if (i.length < 5) continue;
				description += i.textContent.trim();
			  } else if (i.nodeName == 'BR' || i.nodeName == 'HR') {
				description += '\n';
			  } else if (i.nodeName == 'LABEL') {
				description += '\n\n[b]' + i.textContent.trim() + '[/b]\n';
			  } else if (i.nodeName == 'A') {
				if (i.classList.contains('mfp-image')) {
				  //rehost_imgs([de_anonymize(i.href)]).then(new_url => {
				  //  description += '\n\n[img]' + new_url + '[/img]'
				  //}).catch(function() {
				  //  description += '\n\n[img]' + de_anonymize(i.href) + '[/img]'
				  //});
				  description += '\n\n[img]' + de_anonymize(i.href) + '[/img]'
				} else {
				  description += '[url=' + de_anonymize(i.href) + ']' + i.textContent.trim() + '[/url]';
				}
			  } else if (i.nodeName == 'B' || i.nodeName == 'STRONG') {
				description += '[b]' + i.textContent + '[/b]';
			  } else if (i.nodeName == 'I') {
				description += '[i]' + i.textContent + '[/i]';
			  } else if (i.nodeName == 'DIV') {
				if (i.classList.contains('scrpad') || i.classList.contains('aleft')) {
				  desc_extract(i);
				  description += '\n';
				}
			  }
			}
		  }
		}

		i = html.querySelector('section.descr > div.center > a.mfp-image');
		if (i != null) {
		  set_image(i.href);
		} else {
		  i = html.querySelector('section.descr > div.center > img[data-src]');
		  if (i != null) set_image(i.dataset.src);
		}

		var cat = html.querySelector('a.cat:last-of-type > span');
		if (cat != null) {
		  if (cat.textContent.toLowerCase() == 'windows') {
			tags.add('apps.windows');
			if (releaseInfo && releaseInfo.search(/\bx64\b/i) >= 0) tags.add('win64');
			if (releaseInfo && releaseInfo.search(/\bx86\b/i) >= 0) 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 && element_writable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	}
	return false;

	function de_anonymize(uri) {
	  return uri ? uri.replace('http://anonymz.com/?', '').replace('https://anonymz.com/?', '') : null;
	}

	function html2php(str) {
	  return str ? str.replace(/\<b\>/ig, '[b]').replace(/\<\/b\>/ig, '[/b]').
	  	replace(/\<i\>/ig, '[i]').replace(/\<\/i\>/ig, '[/i]') : null;
	}
  }

  function fill_from_text_books() {
	if (clipBoard.value.search(/^https?:\/\//i) < 0) return false;
	var description, tags = new TagManager();
	if (clipBoard.value.toLowerCase().indexOf('martinus.cz') >= 0 || clipBoard.value.toLowerCase().indexOf('martinus.sk') >= 0) {
	  GM_xmlhttpRequest({ method: 'GET', url: clipBoard.value, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) return;
		var parser = new DOMParser();
		var html = parser.parseFromString(response.responseText, "text/html");

		function get_detail(x, y) {
		  var ref = html.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 = html.querySelectorAll('article > ul > li > a');
		if (i != null && element_writable(ref = document.getElementById('title'))) {
		  description = join_authors(i);
		  i = html.querySelector('article > h1');
		  if (i != null) description += ' - ' + i.textContent.trim();
		  i = html.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
		  if (i != null && (matches = i.textContent.match(/\b(\d{4})\b/)) != null) description += ' (' + matches[1] + ')';
		  ref.value = description;
		}

		description = '[quote]';
		i = html.querySelector('section#description > div');
		if (i != null) {
		  desc_extract(i);

		  function desc_extract(node) {
			for (var i of node.childNodes) {
			  if (i.nodeType == 3 || i.nodeName == 'P') {
				//if (i.length < 10) continue;
				description += i.textContent;
			  } else if (i.nodeName == 'BR' || i.nodeName == 'HR') {
				description += '\n';
			  } else if (i.nodeName == 'B' || i.nodeName == 'STRONG') {
				description += '[b]' + i.textContent + '[/b]';
			  } else if (i.nodeName == 'I') {
				description += '[i]' + i.textContent + '[/i]';
			  } else if (i.nodeName == 'DIV') {
				//desc_extract(i);
				//description += '\n';
			  }
			}
		  }
		}

		description += '[/quote]';
		let details = html.querySelectorAll('section#details > div > div > div:first-of-type > div > dl');
		for (var detail of details) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = detail.children[1].textContent.trim();
		  if (lbl.search(/\b(?:originál)/i) >= 0) lbl = 'Original title'
		  else if (lbl.search(/\b(?:rozm)/i) >= 0) continue //lbl = 'Size'
		  else if (lbl.search(/\b(?:datum|dátum|rok)\b/i) >= 0) lbl = 'Release date'
		  else if (lbl.search(/\b(?:katalog|katalóg)/i) >= 0) lbl = 'Catalogue #'
		  else if (lbl.search(/\b(?:stran|strán)\b/i) >= 0) lbl = 'Page count'
		  else if (lbl.search(/\bjazyk/i) >= 0) lbl = 'Language'
		  else if (lbl.search(/\b(?:nakladatel|vydavatel)/i) >= 0) lbl = 'Publisher'
		  else if (lbl.search(/\b(?:vazba|vázba)\b/i) >= 0) continue //lbl = 'Binding'
		  else if (lbl.search(/\b(?:doporuč|ODPORÚČ)/i) >= 0) lbl = 'Age rating'
		  else if (lbl.search(/\b(?:ISBN)\b/i) >= 0) {
			val = '[url=https://www.worldcat.org/isbn/' + detail.children[1].textContent.trim() +
			  ']' + detail.children[1].textContent.trim() + '[/url]';
// 		  } else if (lbl.search(/\b(?:ISBN)\b/i) >= 0) {
// 			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;
		}
		description += '\n[b]More info:[/b] ' + response.finalUrl;
		write_description(description);

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

		var cat = html.querySelectorAll('dd > ul > li > a');
		if (cat != null) cat.forEach(x => { tags.add(x.textContent) });
		if (tags.length > 0 && element_writable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	} else if (clipBoard.value.toLowerCase().indexOf('goodreads.com') >= 0) {
	  GM_xmlhttpRequest({ method: 'GET', url: clipBoard.value, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) return;
		var parser = new DOMParser();
		var html = parser.parseFromString(response.responseText, "text/html");

		i = html.querySelectorAll('a.authorName > span');
		if (i != null && element_writable(ref = document.getElementById('title'))) {
		  description = join_authors(i);
		  i = html.querySelector('h1#bookTitle');
		  if (i != null) description += ' - ' + i.textContent.trim();
		  i = html.querySelector('div#details > div#row:nth-child(2)');
		  if (i != null && (matches = i.textContent.match(/\b(\d{4})\b/)) != null) description += ' (' + matches[1] + ')';
		  ref.value = description;
		}

		description = '[quote]';
		i = html.querySelector('div#description > span:last-of-type');
		if (i != null) {
		  desc_extract(i);

		  function desc_extract(node) {
			for (var i of node.childNodes) {
			  if (i.nodeType == 3 || i.nodeName == 'P') {
				//if (i.length < 10) continue;
				description += i.textContent;
			  } else if (i.nodeName == 'BR' || i.nodeName == 'HR') {
				description += '\n';
			  } else if (i.nodeName == 'B' || i.nodeName == 'STRONG') {
				description += '[b]' + i.textContent + '[/b]';
			  } else if (i.nodeName == 'I') {
				description += '[i]' + i.textContent + '[/i]';
			  } else if (i.nodeName == 'DIV') {
				//desc_extract(i);
				//description += '\n';
			  }
			}
		  }
		}
		description += '[/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;
		}

		i = html.querySelectorAll('div#details > div.row');
		if (i != null) i.forEach(k => { description += '\n' + strip(k.innerText) });
		description += '\n';

		let details = html.querySelectorAll('div#bookDataBox > div.clearFloats');
		for (var detail of details) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = strip(detail.children[1].textContent);
		  if (lbl.search(/\b(?:ISBN)\b/i) >= 0 && ((matches = val.match(/\b(\d{13})\b/)) != null
				|| (matches = val.match(/\b(\d{10})\b/)) != null)) {
			val = '[url=https://www.worldcat.org/isbn/' + matches[1] + ']' + strip(detail.children[1].textContent) + '[/url]';
		  }
		  description += '\n[b]' + lbl + ':[/b] ' + val;
		}
		description += '\n[b]More info:[/b] ' + response.finalUrl;
		write_description(description);

		if ((i = html.querySelector('div.editionCover > img')) != null) {
		  set_image(i.src.replace(/\?.*/, ''));
		}

		var cat = html.querySelectorAll('div.elementList > div.left');
		if (cat != null) cat.forEach(x => { tags.add(x.textContent.trim()) });
		if (tags.length > 0 && element_writable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	} else if (clipBoard.value.toLowerCase().indexOf('databazeknih.cz') >= 0) {
	  let url = clipBoard.value;
	  if (url.toLowerCase().indexOf('show=alldesc') < 0) {
		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) return;
		var parser = new DOMParser();
		var html = parser.parseFromString(response.responseText, "text/html");

		i = html.querySelectorAll('span[itemprop="author"] > a');
		if (i != null && element_writable(ref = document.getElementById('title'))) {
		  description = join_authors(i);
		  i = html.querySelector('h1[itemprop="name"]');
		  if (i != null) description += ' - ' + i.textContent.trim();
		  i = html.querySelector('span[itemprop="datePublished"]');
		  if (i != null && (matches = i.textContent.match(/\b(\d{4})\b/)) != null) description += ' (' + matches[1] + ')';
		  ref.value = description;
		}

		description = '[quote]';
		i = html.querySelector('p[itemprop="description"]');
		if (i != null) desc_extract(i);

		function desc_extract(node) {
		  for (var i of node.childNodes) {
			if (i.nodeType == 3 || i.nodeName == 'P') {
			  //if (i.length < 10) continue;
			  description += i.textContent.trim();
			} else if (i.nodeName == 'BR' || i.nodeName == 'HR') {
			  description += '\n';
			} else if (i.nodeName == 'B' || i.nodeName == 'STRONG') {
			  description += '[b]' + i.textContent + '[/b]';
			} else if (i.nodeName == 'I') {
			  description += '[i]' + i.textContent + '[/i]';
			} else if (i.nodeName == 'DIV') {
			  //desc_extract(i);
			  //description += '\n';
			}
		  }
		}
		description += '[/quote]';

		let details = html.querySelectorAll('table.bdetail tr');
		if (details != null) details.forEach(function(detail) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = detail.children[1].textContent.trim();
		  if (lbl.search(/\bžánr\b/i) >= 0) return;
		  if (lbl.search(/\b(?:ISBN)\b/i) >= 0) {
			val = '[url=https://www.worldcat.org/isbn/' + val.replace(/-/g, '') +
			  ']' + detail.children[1].textContent.trim() + '[/url]';
		  }
		  description += '\n[b]' + lbl + '[/b] ' + val;
		});
		description += '\n[b]More info:[/b] ' + response.finalUrl.replace(/\?.*/, '');
		write_description(description);

		if ((i = html.querySelector('div#icover_mid > a')) != null) set_image(i.href.replace(/\?.*/, ''));
		if ((i = html.querySelector('div#lbImage')) != null
			&& (matches = i.style.backgroundImage.match(/\burl\("(.*)"\)/i)) != null) {
		  set_image(matches[1].replace(/\?.*/, ''));
		}

		var cat = html.querySelectorAll('h5[itemprop="genre"] > a');
		if (cat != null) cat.forEach(x => { tags.add(x.textContent.trim()) });
		cat = html.querySelectorAll('a.tag');
		if (cat != null) cat.forEach(x => { tags.add(x.textContent.trim()) });
		if (tags.length > 0 && element_writable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	} else if (clipBoard.value.toLowerCase().indexOf('alza.cz') >= 0) {
	  GM_xmlhttpRequest({ method: 'GET', url: clipBoard.value, onload: function(response) {
		if (response.readyState != 4 || response.status != 200) return;
		var parser = new DOMParser();
		var html = parser.parseFromString(response.responseText, "text/html");

		i = html.querySelectorAll('div.media-details > div.row50 > span.value > a');
		if (i != null && element_writable(ref = document.getElementById('title'))) {
		  description = join_authors(i);
		  i = html.querySelector('h1[itemprop="name"]');
		  if (i != null) description += ' - ' + i.textContent.trim();
		  i = html.querySelector('div.media-details > div:nth-child(3) > span');
		  if (i != null && (matches = i.textContent.match(/\b(\d{4})\b/)) != null) description += ' (' + matches[1] + ')';
		  ref.value = description;
		}

		description = '[quote]';
		i = html.querySelector('div#celek > div > div > p');
		if (i != null) desc_extract(i);

		function desc_extract(node) {
		  for (var i of node.childNodes) {
			if (i.nodeType == 3 || i.nodeName == 'P') {
			  //if (i.length < 10) continue;
			  description += i.textContent.trim();
			} else if (i.nodeName == 'BR' || i.nodeName == 'HR') {
			  description += '\n';
			} else if (i.nodeName == 'B' || i.nodeName == 'STRONG') {
			  description += '[b]' + i.textContent + '[/b]';
			} else if (i.nodeName == 'I') {
			  description += '[i]' + i.textContent + '[/i]';
			} else if (i.nodeName == 'DIV') {
			  //desc_extract(i);
			  //description += '\n';
			}
		  }
		}
		description += '[/quote]';

		//i = html.querySelector('div.params .act');
		//if (i != null) i.click();
		let details = html.querySelectorAll('div.groupValues > div.row');
		if (details != null) details.forEach(function(detail) {
		  var lbl = detail.children[0].textContent.trim();
		  var val = detail.children[1].textContent.trim();
		  if (lbl.search(/\bžánr\b/i) >= 0) return;
		  if (lbl.search(/\bformát\b/i) >= 0) return;
		  if (lbl.search(/\b(?:ISBN)\b/i) >= 0) {
			val = '[url=https://www.worldcat.org/isbn/' + val.replace(/-/g, '') +
			  ']' + detail.children[1].textContent.trim() + '[/url]';
		  }
		  description += '\n[b]' + lbl + ':[/b] ' + val;
		});
		description += '\n[b]More info:[/b] ' + response.finalUrl.replace(/\?.*/, '');
		write_description(description);

		if ((i = html.querySelector('div#icover_mid > a')) != null) set_image(i.href.replace(/\?.*/, ''));
		if ((i = html.querySelector('div#lbImage')) != null
			&& (matches = i.style.backgroundImage.match(/\burl\("(.*)"\)/i)) != null) {
		  set_image(matches[1].replace(/\?.*/, ''));
		}

		var cat = html.querySelectorAll('h5[itemprop="genre"] > a');
		if (cat != null) cat.forEach(x => { tags.add(x.textContent.trim()) });
		cat = html.querySelectorAll('a.tag');
		if (cat != null) cat.forEach(x => { tags.add(x.textContent.trim()) });
		if (tags.length > 0 && element_writable(ref = document.getElementById('tags'))) {
		  ref.value = tags.toString();
		}
	  }, });
	  return true;
	}
	return false;

	function join_authors(nodeList) {
	  if (typeof nodeList != 'object') return null;
	  var authors = [];
	  nodeList.forEach(k => { authors.push(k.textContent.trim()) });
	  return authors.join(' & ');
	}
  }

  function write_description(desc) {
	if (typeof desc != 'string') return;
	if (element_writable(ref = document.getElementById('desc'))) ref.value = desc;
	if ((ref = document.getElementById('body')) != null && !ref.disabled) {
	  if (ref.textLength > 0) ref.value += '\n\n';
	  ref.value += desc;
	}
  }

  function set_image(url) {
	var image = document.getElementById('image');
	if (!element_writable(image)) return false;
	image.value = url;
	var rehost_btn = document.querySelector('input.rehost_it_cover[type="button"]');
	if (rehost_btn != null) {
	  rehost_btn.click();
	} else {
	  var pr = rehost_imgs([url]);
	  if (pr != null) pr.then(new_urls => { image.value = new_urls[0] });
	}
  }

  // PTPIMG rehoster taken from `PTH PTPImg It`
  function rehost_imgs(urls) {
	if (!Array.isArray(urls)) return null;;
	var config = JSON.parse(window.localStorage.ptpimg_it);
	return config.api_key ? new Promise(ptpimg_upload_urls).catch(m => { alert(m) }) : null;

	function ptpimg_upload_urls(resolve, reject) {
	  const boundary = 'NN-GGn-PTPIMG';
	  var data = '--' + boundary + "\n";
	  data += 'Content-Disposition: form-data; name="link-upload"\n\n';
	  data += urls.map(function(url) {
		return url.toLowerCase().indexOf('://reho.st/') < 0 && url.toLowerCase().indexOf('discogs.com') >= 0 ?
		  'https://reho.st/' + url : url;
	  }).join('\n') + '\n';
	  data += '--' + boundary + '\n';
	  data += 'Content-Disposition: form-data; name="api_key"\n\n';
	  data += 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,
		},
		data: data,
		onload: response => {
		  if (response.status != 200) reject('Response error ' + response.status);
		  resolve(response.response.map(item => 'https://ptpimg.me/' + item.code + '.' + item.ext));
		},
	  });
	}
  }

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

function add_artist() { exec(function() { AddArtistField() }) }

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

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 make_time_string(duration) {
  let t = Math.round(duration);
  t = Math.abs(t);
  let x = Math.floor(t / 60 ** 2);
  let res;
  if (x > 0) {
	res = x + ':' + Math.floor(t / 60 % 60).toString().padStart(2, '0');
  } else {
	res = Math.floor(t / 60 % 60).toString();
  }
  return res + ':' + (t % 60).toString().padStart(2, '0');
}

function extract_year(expr) {
  if (typeof expr != 'string') return null;
  var year = parseInt(expr);
  if (year > 0) return year;
  var m = expr.match(/\b(\d{4})\b/);
  return m && (year = parseInt(m[1])) > 0 ? year : null;
}

function isRED() { return document.domain.toLowerCase().endsWith('redacted.ch') }
function isNWCD() { return document.domain.toLowerCase().endsWith('notwhat.cd') }
function isOrpheus() { return document.domain.toLowerCase().endsWith('orpheus.network') }

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

function push_warning(text, bold = true, color = 'red') {
  if (!warning) {
	if (!tb) return false;
	warning = document.createElement('tr');
	if (warning == null) return false;
	elem = document.createElement('td');
	elem.style.textAlign = 'center';
	elem.colSpan = 2;
	if (elem == null) return false;
	elem.style.color = color;
	warning.appendChild(elem);
	tb.appendChild(warning);
  }
  warning.children[0].innerHTML = (bold ? '<b>' : '') + text + (bold ? '</b>' : '');
}