Supraphonline foobar2000 tagging support

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

As of 2020-09-01. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Supraphonline foobar2000 tagging support
// @name:cs      Supraphonline podpora tagování ve foobar2000
// @name:en      Supraphonline foobar2000 tagging support
// @namespace    https://greatest.deepsurf.us/cs/users/321857-anakunda
// @version      1.2
// @description  Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000
// @description:cs  Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000
// @description:en  Copies album metadata to clipboard in machine parseable format
// @author       Já, Osobně
// @copyright    2020, Anakunda (https://greatest.deepsurf.us/cs/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @iconURL      https://www.supraphonline.cz/favicon.ico
// @match        http*://*.supraphonline.cz/album/*
// @grant        GM_getValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest

// ==/UserScript==

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

'use strict';

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;
};

var hTimer = setInterval(function() {
  var ref = document.querySelector('form.table-action');
  if (ref == null) return;
  clearInterval(hTimer);
  var child = document.createElement('button');
  child.id = 'copy-info-to-clipboard';
  child.textContent = 'Kopírovat do schránky';
  child.type = 'button';
  child.name = 'copy-info-to-clipboard';
  child.className = 'btn btn-danger topframe_login';
  child.style.marginRight = '10px';
  child.onclick = fetchAlbum;
  ref.prepend(child);
}, 1000);

function fetchAlbum(evt) {
  var original_text = evt.target.textContent;
  evt.target.disabled = true;
  evt.target.textContent = 'Pracuji...';
  var isVA = false, ndx, artists = [], tracks = [], totalTime, discNumber, discSubtitle;
  var domParser = new DOMParser();
  var release = {
	artists: [],
	composers: [],
	performers: [],
	totalDiscs: 1,
  };
  document.querySelectorAll('h2.album-artist > a').forEach(it => { release.artists.pushUnique(it.title) });
  if (release.artists.length == 0 && (ref = document.querySelector('h2.album-artist[title]')) != null) {
	if (/^(?:Různí(?:\s+interpreti)?|Various(?:\s+Artists)?|VA)$/i.test(ref.title)) isVA = true;
	if (isVA) release.artists = ['Various Artists']; //else albumArtist = [ref.title];
  }
  var ref = document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]');
  if (ref != null && /^(?:Různí\s+interpreti)$/i.test(ref.content)) isVA = true;
  if ((ref = document.querySelector('h1[itemprop="name"]')) != null) release.album = ref.firstChild.data.trim();
  if ((ref = document.querySelector('meta[itemprop="numTracks"]')) != null) release.totalTracks = parseInt(ref.content);
  if ((ref = document.querySelector('meta[itemprop="genre"]')) != null) release.genre = ref.content;
  if ((ref = document.querySelector('li.album-version > div.selected > div')) != null) {
	if (/\b(?:CD)\b/.test(ref.textContent)) release.media = 'CD';
	if (/\b(?:LP)\b/.test(ref.textContent)) release.media = 'Vinyl';
	if (/\b(?:MP3)\b/.test(ref.textContent)) release.media = 'WEB-DL';
	if (/\b(?:FLAC)\b/.test(ref.textContent)) release.media = 'WEB-DL';
	if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) release.media = 'WEB-DL';
  }
  document.querySelectorAll('ul.summary > li').forEach(function(it) {
	if (it.children.length < 1) return;
	if (it.children[0].textContent.startsWith('Nosič')) release.media = it.lastChild.textContent.trim();
	if (it.children[0].textContent.startsWith('Datum vydání')) release.releaseDate = normalizeDate(it.lastChild.textContent);
	//if (it.children[0].textContent.startsWith('Žánr')) release.genre = it.lastChild.textContent.trim();
	if (it.children[0].textContent.startsWith('Vydavatel')) release.label = it.lastChild.textContent.trim();
	if (it.children[0].textContent.startsWith('Katalogové číslo')) release.catalogue = it.lastChild.textContent.trim();
	if (it.children[0].textContent.startsWith('Celková stopáž')) totalTime = timeStringToTime(it.lastChild.textContent.trim());
	if (/^(?:\([PC]\)|℗|©)$/i.test(it.children[0].textContent)) release.albumYear = extract_year(it.lastChild.data);
  });
  [
	[/^(?:Orchestrální\s+hudba)$/i, 'Orchestral Music'],
	[/^(?:Komorní\s+hudba)$/i, 'Chamber Music'],
	[/^(?:Vokální)$/i, 'Classical, Vocal'],
	[/^(?:Klasická\s+hudba)$/i, 'Classical'],
	[/^(?:Melodram)$/i, 'Classical, Melodram'],
	[/^(?:Symfonie)$/i, 'Symphony'],
	[/^(?:Vánoční\s+hudba)$/i, 'Christmas Music'],
	[/^(?:Alternativní)$/i, 'Alternative'],
	[/^(?:Dechová\s+hudba)$/i, 'Brass Music'],
	[/^(?:Elektronika)$/i, 'Electronic'],
	[/^(?:Folklor)$/i, 'Folclore, World Music'],
	[/^(?:Instrumentální\s+hudba)$/i, 'Instrumental'],
	[/^(?:Latinské\s+rytmy)$/i, 'Latin'],
	[/^(?:Meditační\s+hudba)$/i, 'Meditative'],
	[/^(?:Pro\s+děti)$/i, 'Children'],
  ].forEach(it => { if (it[0].test(release.genre)) release.genre = it[1] });
  const creators = [
	'autoři',
	'interpreti',
	'tělesa',
	'digitalizace',
  ];
  for (var i = 0; i < 4; ++i) artists[i] = {};
  document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
	if ((ref = it.querySelector('h3')) != null) {
	  ndx = undefined;
	  creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
	} else {
	  if (typeof ndx != 'number') return;
	  ref = it.querySelector('span');
	  let key = ref != null ? ref.textContent.replace(/\s*:.*$/, '').toLowerCase() : undefined;
// 	  [
// 		[/^(?:zpěv)$/, 'vocal'],
// 		[/^(?:hudba)$/, 'music'],
// 		[/^(?:původní\s+text)$/, 'original lyrics'],
// 		[/^(?:text)$/, 'lyrics'],
// 		[/^(?:autor)$/, 'author'],
// 		[/^(?:účinkuje)$/, 'participating'],
// 		[/^(?:nahrál)$/, 'recorded'],
// 		[/^(?:sbormistr)$/, 'choirmaster'],
// 		[/^(?:řídí|dirigent)$/, 'conductor'],
// 	  ].forEach(it => { if (it[0].test(key)) key = it[1] });
	  if (!(artists[ndx][key] instanceof Array)) artists[ndx][key] = [];
	  artists[ndx][key].pushUnique(it.querySelector('a').textContent.trim());
	}
  });
  release.description = Array.from(document.querySelectorAll('div[itemprop="description"] p'))
	.map(it => it.textContent.trim()).join(' ').replace(/[\r\n]+/g, ' ');
  for (i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(it => { release.performers.pushUnique(...artists[i][it]) });
  Object.keys(artists[0]).forEach(it => { release.composers.pushUnique(...artists[0][it]) });
  if (release.artists.length == 0 && !isVA) release.artists = release.performers;
  var promises = [];
  document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(it) {
	if (it.classList.contains('cd-header')) {
	  discNumber = /\b\d+\b/.test(it.querySelector(':scope h3').firstChild.data.trim()) ?
	  	parseInt(RegExp.lastMatch) : undefined;
	  if (discNumber > release.totalDiscs) release.totalDiscs = discNumber;
	}
	if (it.classList.contains('song-header')) discSubtitle = it.children[0].title.trim() || undefined;
	if (it.classList.contains('track') && it.id) {
	  let track = {
		id: parseInt(it.id.replace(/^track-/i, '')),
		artists: [],
		performers: [],
		composers: [],
		conductors: [],
		producers: [],
		discNumber: discNumber,
		discSubtitle: discSubtitle,
	  };
	  if (/^\s*(\d+)\.?\s*$/.test(it.children[0].firstChild.textContent)) track.trackNumber = parseInt(RegExp.$1);
	  ref = it.querySelectorAll('meta[itemprop="name"]');
	  if (ref.length > 0) track.title = ref[0].content;
	  if (/^PT(\d+)H(\d+)M(\d+)S$/i.test(it.querySelector('meta[itemprop="duration"]').content)) {
		track.duration = parseInt(RegExp.$1 || 0) * 60**2 + parseInt(RegExp.$2 || 0) * 60 + parseInt(RegExp.$3 || 0);
	  }
	  tracks.push(track);
	  ref = it.querySelector('td > a.trackdetail');
	  if (ref != null) promises.push(new Promise(function(resolve, reject) {
		GM_xmlhttpRequest({
		  method: 'GET',
		  url: ref.href,
		  responseType: 'text/html',
		  context: track,
		  onload: function(response) {
			if (response.readyState != 4 || response.status != 200) {
			  reject('Response error ' + response.status + ' (' + response.statusText + ')');
			  return;
			}
			let dom = domParser.parseFromString(response.responseText, "text/html");
			if (dom == null) reject('Parse error');
			var track = response.context;
			var tr_det = dom.querySelector('div[data-swap="trackdetail-' + track.id + '"] > div > div.row');
			if (tr_det == null) reject('No trackdetail for track-' + track.id);
			tr_det.querySelectorAll('div:nth-of-type(1) > ul > li > span').forEach(function(it) {
			  if (it.textContent.startsWith('Nahrávka dokončena')) track.recordDate = it.nextSibling.data.trim();
			  if (it.textContent.startsWith('Místo nahrání')) track.recordPlace = it.nextSibling.data.trim();
			  if (it.textContent.startsWith('Rok prvního vydání')) track.albumYear = parseInt(it.nextSibling.data);
			  if (it.textContent.startsWith('(P)')) track.copyright = it.nextSibling.data.trim();
			  if (it.textContent.startsWith('Žánr')) track.genre = it.nextSibling.data.trim();
			});
			tr_det.querySelectorAll('div:nth-of-type(2) > ul > li > span').forEach(function(it) {
			  function oneOf(arr) { return arr.some(k => elem[0].startsWith(k)) }
			  var elem = [it.textContent.trim().replace(/:.*/, ''), it.nextElementSibling.textContent.trim()];
			  if (oneOf(['hudba', 'text', 'hudba+text', 'původní text', 'český text', 'libreto'])) {
				track.composers.push(elem);
			  } else if (oneOf(['nahrál'])) {
			  } else if (oneOf(['dirigent', 'řídí'])) {
				track.conductors.push(elem);
			  } else if (oneOf(['produkce'])) {
				track.producers.push(elem[1]);
			  } else {
				track.performers.push(elem);
			  }
			});
			// TODO: vyfiltrovat track.artists z track.performers
			resolve(true);
		  },
		  onerror: response => { reject('Response error ' + response.status + ' (' + response.statusText + ')') },
		  ontimeout: function() { reject('Timeout') },
		});
	  }));
	}
  });
  Promise.all(promises).then(function() {
	var clipBoard = '';
	tracks.sort(function(a, b) {
	  var k = a.discNumber - b.discNumber;
	  return isNaN(k) || k == 0 ? a.trackNumber - b.trackNumber : k;
	}).forEach(function(track) {
	  clipBoard += [
		joinArtists(release.artists),
		release.album,
		release.albumYear || track.albumYear,
		release.releaseDate,
		track.genre || release.genre,
		release.label,
		release.catalogue,
		track.discNumber,
		release.totalDiscs > 1 ? release.totalDiscs : undefined,
		track.discSubtitle,
		track.trackNumber,
		release.totalTracks,
		joinArtists(track.artists.length > 0 ? track.artists : release.artists),
		track.title,
		track.performers.concat(track.conductors).map(desc).join(', ') || release.performers.join(', '),
		track.composers.map(desc).join(', ') || release.composers.join(', '),
		release.media,
		release.description,
		document.location.origin + document.location.pathname,
	  ].join('\x1E') + '\n';
	});
	GM_setClipboard(clipBoard, 'text');
  }).catch(e => { alert(e) }).then(function() {
	evt.target.disabled = false;
	evt.target.textContent = original_text;
  });
}

function desc(el) {
  var res = el[1];
  if (el[0] && el[0] != 'hudební tělesa') res += ' (' + el[0] + ')';
  return res;
}

function joinArtists(list) {
  return list.length < 3 ? list.join(' a ') : list.slice(0, -1).join(', ') + ' a ' + list[list.length - 1];
}

function normalizeDate(str) {
  if (typeof str != 'string') return null;
  if (/\b(d{4})-(\d+)-(\d+)\b/.test(str)) {
	var D = parseInt(RegExp.$3);
	var M = parseInt(RegExp.$2);
	var Y = parseInt(RegExp.$1);
  } else if (/\b(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})\b/.test(str)) {
	D = parseInt(RegExp.$2);
	M = parseInt(RegExp.$1);
	Y = parseInt(RegExp.$3);
  } else if (/\b(\d{1,2})\.\s?(\d{1,2})\.\s?(\d{2}|\d{4})\b/.test(str)) {
	D = parseInt(RegExp.$1);
	M = parseInt(RegExp.$2);
	Y = parseInt(RegExp.$3);
  }
  if (Y < 100) Y += 2000;
  return D > 0 && M > 0 && Y > 0 ?
	Y.toString().padStart(4, '0') + '-' + M.toString().padStart(2, '0') + '-' + D.toString().padStart(2, '0')
  		: extract_year(str);
}

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());
}