Supraphonline podpora tagování ve foobar2000

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

Verze ze dne 07. 09. 2020. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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.3
// @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_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @require      https://greatest.deepsurf.us/scripts/408084-xhrlib/code/xhrLib.js

// ==/UserScript==

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

'use strict';

Array.prototype.includesCaseless = function(str) {
  if (typeof str != 'string') return false;
  str = str.toLowerCase();
  return this.some(elem => typeof elem == 'string' && elem.toLowerCase() == str);
};
Array.prototype.pushUnique = function(...items) {
  if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) });
  return this.length;
};
Array.prototype.pushUniqueCaseless = function(...items) {
  if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  return this.length;
};
Array.prototype.equalTo = function(arr) {
  return Array.isArray(arr) && arr.length == this.length
  	&& Array.from(arr).sort().toString() == Array.from(this).sort().toString();
};
Array.prototype.equalCaselessTo = function(arr) {
  function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
  return Array.isArray(arr) && arr.length == this.length
  	&& arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
};

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...';

  let tracks = [], discNumber, discSubtitle, domParser = new DOMParser(), ref, media, encoding, format, bitdepth,
	  trackIdentifiers, releaseDate, totalDiscs, samplerate, catalogue, imgUrl, album, totalTracks, albumYear,
	  label, identifiers = {};
  const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  const pseudoArtistParsers = [
	/^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
	/^(?:traditional|trad\.|lidová)$/i,
	/\b(?:traditional|trad\.|lidová)$/,
	/^(?:tradiční|lidová)\s+/,
	/^(?:[Aa]nonym)/,
	/^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
	/^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
	/^(?:Various\s+Composers)$/i,
	/^(?:Guests|Friends)$/i,
  ];
  const VA = 'Various Artists';

  if (/\/album\/(\d+)\b/i.test(document.URL)) identifiers.SUPRAPHONLINE_ID = parseInt(RegExp.$1);
  let artist = Array.from(document.querySelectorAll('div.visible-lg-block > h2.album-artist > a'))
	.map(a => a.title || a.textContent.trim());
  let isVA = (ref = document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]')) != null ?
	vaParser.test(ref.content) : artist.length <= 0;
  if ((ref = document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
  if ((ref = document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  let genres = (ref = document.querySelector('meta[itemprop="genre"]')) != null ? ref.content : undefined;
  if ((ref = document.querySelector('li.album-version > div.selected > div')) != null) {
	if (/\b(?:MP3)\b/.test(ref.textContent)) {
	  media = 'WEB'; encoding = 'lossy'; format = 'MP3';
	}
	if (/\b(?:FLAC)\b/.test(ref.textContent)) {
	  media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 16;
	}
	if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) {
	  media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 24;
	}
	if (/\b(?:CD)\b/.test(ref.textContent)) media = 'CD';
	if (/\b(?:LP)\b/.test(ref.textContent)) media = 'Vinyl';
  }
  const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
  document.querySelectorAll('ul.summary > li').forEach(function(li) {
	if (li.childElementCount <= 0) return;
	let key = li.firstElementChild.textContent, value = li.lastChild.textContent.trim();
	if (key.includes('Nosič')) media = value;
	if (key.includes('Datum vydání')) releaseDate = normalizeDate(value, 'cs');
	if (key.includes('První vydání')) albumYear = extractYear(value);
	if (key.includes('Žánr')) genres = translateGenre(value);
	if (key.includes('Vydavatel')) label = value;
	if (key.includes('Katalogové číslo')) catalogue = value;
	if (key.includes('Formát')) {
	  if (/\b(?:FLAC|WAV|AIFF?)\b/.test(value)) { encoding = 'lossless'; format = 'FLAC' }
	  if (/\b(\d+)[\-\s]?bits?\b/i.test(value)) bitdepth = parseInt(RegExp.$1);
	  if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(value)) samplerate = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
	}
	//if (key.includes('Celková stopáž')) totalTime = timeStringToTime(value);
	if (copyrightParser.test(key) && !albumYear) albumYear = extractYear(value);
  });
  const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
  let artists = [], ndx;
  for (let i = 0; i < creators.length; ++i) artists[i] = {};
  document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
	if ((ref = it.querySelector('h3')) != null) {
	  ndx = undefined;
	  creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
	} else {
	  if (typeof ndx != 'number') return;
	  if (ndx == 2) var role = 'ensemble';
		else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
	  if ((ref = it.querySelector('a')) != null) {
		if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
		artists[ndx][role].pushUnique([ref.textContent.trim(), document.location.origin + ref.pathname]);
	  }
	}
  });
  let description = Array.from(document.querySelectorAll('div[itemprop="description"] p'))
  	.map(p => p.textContent.trim()).join('\n\n').replace(/\s+/g, ' ');
  let performers = [], composer = [], conductor = [], DJs = [], albumGuests = [], volMedia;
  for (let i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
	var a = artists[i][role].map(a => a[0]);
	([
	  'conductor', 'choirmaster', 'director',
	].includes(role) ? conductor : role == 'DJ' ? DJs : [
	  'FeaturedArtist',
	].includes(role) ? albumGuests : artist).pushUnique(...a);
  });
  Object.keys(artists[0]).forEach(function(role) { // composers
	composer.pushUnique(...artists[0][role].map(it => it[0])
		.filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
  });
  if ((ref = document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content.replace(/\?.*$/, '');
  document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(tr, index) {
	if (tr.classList.contains('cd-header') && (ref = tr.querySelector('td > h3')) != null
		&& /\b(?:(\S*?)\s*)?(\d+)\b/.test(ref.textContent)) {
	  volMedia = RegExp.$1 ? RegExp.lastMatch : undefined;
	  discNumber = parseInt(RegExp.$2) || undefined;
	}
	if (tr.classList.contains('song-header') && (ref = tr.querySelector('td')) != null)
	  discSubtitle = ref.title || ref.textContent.trim();
	if (tr.classList.contains('track') && tr.id) {
	  trackIdentifiers = {
		TRACK_ID: /^(?:track)-(\d+)$/i.test(tr.id) ? parseInt(RegExp.$1) : undefined,
	  };
	  if (volMedia) trackIdentifiers.VOL_MEDIA = volMedia;
	  let track = {
		artist: isVA ? VA : undefined,
		artists: !isVA && artist.length > 0 ? artist : undefined,
		//featured_artists: albumGuests.length > 0 ? albumGuests : undefined,
		album: album,
		album_year: /*trackYear || */albumYear || undefined,
		release_date: releaseDate,
		label: label,
		catalog: catalogue,
		encoding: encoding,
		codec: format,
		bitdepth: bitdepth,
		samplerate: samplerate || undefined,
		media: media,
		genre: genres,
		disc_number: discNumber,
		total_discs: totalDiscs,
		disc_subtitle: discSubtitle,
		track_number: /^\s*(\d+)\.?\s*$/.test(tr.children[0].firstChild.textContent) ?
			parseInt(RegExp.$1) || RegExp.$1 : undefined,
		total_tracks: totalTracks,
		title: (ref = tr.querySelector('meta[itemprop="name"][content]')) != null ? ref.content
			: (ref = tr.querySelector('td > a.trackdetail')) != null ? ref.textContent.trim() : undefined,
		performers: performers.length > 0 ? performers : undefined,
		composers: composer.length > 0 ? composer : undefined,
		conductors: conductor.length > 0 ? conductor : undefined,
		compilers: DJs.length > 0 ? DJs : undefined,
		duration: durationFromMeta(tr),
		url: document.location.origin + document.location.pathname,
		description: description,
		identifiers: mergeIds(),
		cover_url: imgUrl,
	  };
	  tracks.push((function() {
		if ((ref = tr.querySelector('td > a.trackdetail')) == null) return Promise.reject('link not found');
		return globalFetch(ref.pathname + ref.search).then(function(response) {
		  var detail = response.document.querySelector('div[data-swap="trackdetail-' +
			track.identifiers.TRACK_ID + '"] > div > div.row');
		  if (detail == null) return Promise.reject('element not found');
		  detail.querySelectorAll('div[class]:nth-of-type(1) > ul > li').forEach(function(li) {
			var key = li.querySelector('span'), value = li.lastChild.textContent.trim();
			if (key != null && value) key = key.textContent.trim(); else return;
			if (key.startsWith('Žánr')) track.genre = value;
			if (key.startsWith('Nahrávka dokončena')) track.rec_year = extractYear(value);
			if (key.startsWith('Místo nahrání')) track.venue = value;
			if (key.startsWith('Rok prvního vydání')) track.pub_year = extractYear(value);
			if (copyrightParser.test(key)) track.copyright = value;
		  });
		  let trackArtists = [];
		  for (let i = 0; i < 8; ++i) trackArtists[i] = [];
		  detail.querySelectorAll('div[class]:nth-of-type(2) > ul > li').forEach(function(li) {
			var role = li.querySelector('span');
			var artists = Array.from(li.getElementsByTagName('a')).map(a => a.textContent.trim())
				.filter(artist => !pseudoArtistParsers.some(rx => rx.test(artist)));
			if (role != null && artists.length > 0) role = translateRole(role); else return;
			if (role.startsWith('remix'))
			  trackArtists[2].pushUnique(...artists);
			else if ([
			  'music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto',
			  'music improvisation', 'author', 'ComposerLyricist', 'Composer', 'Lyricist', 'Author',
			].includes(role))
			  trackArtists[3].pushUnique(...artists);
			else if (role == 'DJ')
			  trackArtists[5].pushUnique(...artists);
			else if (['produced by', 'Producer'].includes(role))
			  trackArtists[6].pushUnique(...artists);
			else if (![
			  'recorded by', 'Engineer', 'Additional Producer', 'Recording Engineer', 'Mix Engineer',
			  'Mastering Engineer', 'Asst. Recording Engineer', 'StudioPersonnel', 'Mixer', 'programming',
			  'Synthesizer Programming', 'Programmer', 'Arranger', 'String Arranger', 'Assistant Mixer',
			].includes(role)) {
			  if (['MainArtist'].includes(role))
				trackArtists[0].pushUnique(...artists);
			  else if (['ensemble', 'Artist'].includes(role) || /\b(?:vocals)\b/.test(role))
				artists.forEach(_artist => {
				  trackArtists[artist.includesCaseless(_artist) ? 0 : 1].pushUnique(...artists);
				});
			  else if (['FeaturedArtist'].includes(role))
				trackArtists[1].pushUnique(...artists);
			  else if (['conductor', 'choirmaster', 'director'].includes(role))
				trackArtists[4].pushUnique(...artists);
			  trackArtists[7].pushUnique(...artists.map(artist => artist + ' (' + role + ')'));
			}
		  });
		  if (trackArtists[1].length > 0 && trackArtists[0].length <= 0) {
			trackArtists[0] = trackArtists[1]; trackArtists[1] = [];
		  }
		  if (trackArtists[0].length > 0 && (isVA || !trackArtists[0].equalCaselessTo(artist)
				|| trackArtists[1].length > 0/*!trackArtists[1].equalCaselessTo(albumGuests)*/)) {
			track.track_artists = trackArtists[0];
			if (trackArtists[1].length > 0) track.track_guests = trackArtists[1];
		  }
		  [
			[3, 'composer'],
			[4, 'conductor'],
			[2, 'remixer'],
			[5, 'compiler'],
			//[6, 'producer'],
			[7, 'performer'],
		  ].forEach(def => { if (trackArtists[def[0]].length > 0) track[def[1] + 's'] = trackArtists[def[0]] })
		  return track;
		});
	  })().catch(function(reason) {
		console.error('Supraphonline parser failed to get track', index + 1, 'detail:', reason);
		return Promise.resolve(track);
	  }));
	} // track
  });
  Promise.all(tracks).then(tracks => tracks.map(track => {
	if (Array.isArray(track.track_artists) && track.track_artists.length > 0) {
	  var trackArtist = joinArtists(track.track_artists);
	  if (Array.isArray(track.track_guests) && track.track_guests.length > 0)
		trackArtist += ' feat. ' + joinArtists(track.track_guests);
	}
	return [
	  isVA ? VA : joinArtists(track.artists) || '',
	  track.album || '',
	  track.album_year || track.pub_year || '',
	  track.release_date || '',
	  track.genre || '',
	  track.label || '',
	  track.catalog || '',
	  track.disc_number || '',
	  track.total_discs > 1 ? track.total_discs : '',
	  track.disc_subtitle || '',
	  track.track_number || '',
	  track.total_tracks || '',
	  trackArtist || (Array.isArray(track.artists) && track.artists.length > 0 ?
		joinArtists(track.artists) : track.artist) || '',
	  track.title || '',
	  (track.performers || []).join(', '),
	  (track.composers || []).join(', '),
	  track.media,
	  track.description,
	  track.url,
	].join('\x1E');
  }).join('\n')).then(clipBoard => { GM_setClipboard(clipBoard, 'text') }).catch(e => { alert(e) }).then(function() {
	evt.target.disabled = false;
	evt.target.textContent = original_text;
  });

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

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

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

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

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

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

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

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