Supraphonline foobar2000 tagging support

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

当前为 2020-09-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Supraphonline foobar2000 tagging support
  3. // @name:cs Supraphonline podpora tagování ve foobar2000
  4. // @name:en Supraphonline foobar2000 tagging support
  5. // @namespace https://greasyfork.org/cs/users/321857-anakunda
  6. // @version 1.3
  7. // @description Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000
  8. // @description:cs Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000
  9. // @description:en Copies album metadata to clipboard in machine parseable format
  10. // @author Já, Osobně
  11. // @copyright 2020, Anakunda (https://greasyfork.org/cs/users/321857-anakunda)
  12. // @license GPL-3.0-or-later
  13. // @iconURL https://www.supraphonline.cz/favicon.ico
  14. // @match http*://*.supraphonline.cz/album/*
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_setClipboard
  17. // @grant GM_getValue
  18. // @grant GM_getValue
  19. // @grant GM_deleteValue
  20. // @require https://greasyfork.org/scripts/408084-xhrlib/code/xhrLib.js
  21.  
  22. // ==/UserScript==
  23.  
  24. // Výraz pro 'Automatically Fill Values' funkci ve foobaru2000:
  25. // %album artist%%album%%date%%releasedate%%genre%%label%%catalog%%discnumber%%totaldiscs%%discsubtitle%%tracknumber%%totaltracks%%artist%%title%%performer%%composer%%media%%comment%%url%
  26.  
  27. 'use strict';
  28.  
  29. Array.prototype.includesCaseless = function(str) {
  30. if (typeof str != 'string') return false;
  31. str = str.toLowerCase();
  32. return this.some(elem => typeof elem == 'string' && elem.toLowerCase() == str);
  33. };
  34. Array.prototype.pushUnique = function(...items) {
  35. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) });
  36. return this.length;
  37. };
  38. Array.prototype.pushUniqueCaseless = function(...items) {
  39. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  40. return this.length;
  41. };
  42. Array.prototype.equalTo = function(arr) {
  43. return Array.isArray(arr) && arr.length == this.length
  44. && Array.from(arr).sort().toString() == Array.from(this).sort().toString();
  45. };
  46. Array.prototype.equalCaselessTo = function(arr) {
  47. function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
  48. return Array.isArray(arr) && arr.length == this.length
  49. && arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
  50. };
  51.  
  52. var hTimer = setInterval(function() {
  53. var ref = document.querySelector('form.table-action');
  54. if (ref == null) return;
  55. clearInterval(hTimer);
  56. var child = document.createElement('button');
  57. child.id = 'copy-info-to-clipboard';
  58. child.textContent = 'Kopírovat do schránky';
  59. child.type = 'button';
  60. child.name = 'copy-info-to-clipboard';
  61. child.className = 'btn btn-danger topframe_login';
  62. child.style.marginRight = '10px';
  63. child.onclick = fetchAlbum;
  64. ref.prepend(child);
  65. }, 1000);
  66.  
  67. function fetchAlbum(evt) {
  68. var original_text = evt.target.textContent;
  69. evt.target.disabled = true;
  70. evt.target.textContent = 'Pracuji...';
  71.  
  72. let tracks = [], discNumber, discSubtitle, domParser = new DOMParser(), ref, media, encoding, format, bitdepth,
  73. trackIdentifiers, releaseDate, totalDiscs, samplerate, catalogue, imgUrl, album, totalTracks, albumYear,
  74. label, identifiers = {};
  75. const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  76. const pseudoArtistParsers = [
  77. /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
  78. /^(?:traditional|trad\.|lidová)$/i,
  79. /\b(?:traditional|trad\.|lidová)$/,
  80. /^(?:tradiční|lidová)\s+/,
  81. /^(?:[Aa]nonym)/,
  82. /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  83. /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  84. /^(?:Various\s+Composers)$/i,
  85. /^(?:Guests|Friends)$/i,
  86. ];
  87. const VA = 'Various Artists';
  88.  
  89. if (/\/album\/(\d+)\b/i.test(document.URL)) identifiers.SUPRAPHONLINE_ID = parseInt(RegExp.$1);
  90. let artist = Array.from(document.querySelectorAll('div.visible-lg-block > h2.album-artist > a'))
  91. .map(a => a.title || a.textContent.trim());
  92. let isVA = (ref = document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]')) != null ?
  93. vaParser.test(ref.content) : artist.length <= 0;
  94. if ((ref = document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
  95. if ((ref = document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  96. let genres = (ref = document.querySelector('meta[itemprop="genre"]')) != null ? ref.content : undefined;
  97. if ((ref = document.querySelector('li.album-version > div.selected > div')) != null) {
  98. if (/\b(?:MP3)\b/.test(ref.textContent)) {
  99. media = 'WEB'; encoding = 'lossy'; format = 'MP3';
  100. }
  101. if (/\b(?:FLAC)\b/.test(ref.textContent)) {
  102. media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 16;
  103. }
  104. if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) {
  105. media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 24;
  106. }
  107. if (/\b(?:CD)\b/.test(ref.textContent)) media = 'CD';
  108. if (/\b(?:LP)\b/.test(ref.textContent)) media = 'Vinyl';
  109. }
  110. const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
  111. document.querySelectorAll('ul.summary > li').forEach(function(li) {
  112. if (li.childElementCount <= 0) return;
  113. let key = li.firstElementChild.textContent, value = li.lastChild.textContent.trim();
  114. if (key.includes('Nosič')) media = value;
  115. if (key.includes('Datum vydání')) releaseDate = normalizeDate(value, 'cs');
  116. if (key.includes('První vydání')) albumYear = extractYear(value);
  117. if (key.includes('Žánr')) genres = translateGenre(value);
  118. if (key.includes('Vydavatel')) label = value;
  119. if (key.includes('Katalogové číslo')) catalogue = value;
  120. if (key.includes('Formát')) {
  121. if (/\b(?:FLAC|WAV|AIFF?)\b/.test(value)) { encoding = 'lossless'; format = 'FLAC' }
  122. if (/\b(\d+)[\-\s]?bits?\b/i.test(value)) bitdepth = parseInt(RegExp.$1);
  123. if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(value)) samplerate = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
  124. }
  125. //if (key.includes('Celková stopáž')) totalTime = timeStringToTime(value);
  126. if (copyrightParser.test(key) && !albumYear) albumYear = extractYear(value);
  127. });
  128. const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
  129. let artists = [], ndx;
  130. for (let i = 0; i < creators.length; ++i) artists[i] = {};
  131. document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
  132. if ((ref = it.querySelector('h3')) != null) {
  133. ndx = undefined;
  134. creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
  135. } else {
  136. if (typeof ndx != 'number') return;
  137. if (ndx == 2) var role = 'ensemble';
  138. else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
  139. if ((ref = it.querySelector('a')) != null) {
  140. if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
  141. artists[ndx][role].pushUnique([ref.textContent.trim(), document.location.origin + ref.pathname]);
  142. }
  143. }
  144. });
  145. let description = Array.from(document.querySelectorAll('div[itemprop="description"] p'))
  146. .map(p => p.textContent.trim()).join('\n\n').replace(/\s+/g, ' ');
  147. let performers = [], composer = [], conductor = [], DJs = [], volMedia;
  148. for (let i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
  149. var a = artists[i][role].map(a => a[0]);
  150. artist.pushUnique(...a);
  151. ([
  152. 'conductor',
  153. 'choirmaster',
  154. 'director',
  155. ].includes(role) ? conductor : role == 'DJ' ? DJs : performers).pushUnique(...a);
  156. });
  157. Object.keys(artists[0]).forEach(function(role) { // composers
  158. composer.pushUnique(...artists[0][role].map(it => it[0])
  159. .filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
  160. });
  161. if ((ref = document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content.replace(/\?.*$/, '');
  162. document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(tr, index) {
  163. if (tr.classList.contains('cd-header') && (ref = tr.querySelector('td > h3')) != null
  164. && /\b(?:(\S*?)\s*)?(\d+)\b/.test(ref.textContent)) {
  165. volMedia = RegExp.$1 ? RegExp.lastMatch : undefined;
  166. discNumber = parseInt(RegExp.$2) || undefined;
  167. }
  168. if (tr.classList.contains('song-header') && (ref = tr.querySelector('td')) != null)
  169. discSubtitle = ref.title || ref.textContent.trim();
  170. if (tr.classList.contains('track') && tr.id) {
  171. trackIdentifiers = {
  172. TRACK_ID: /^(?:track)-(\d+)$/i.test(tr.id) ? parseInt(RegExp.$1) : undefined,
  173. };
  174. if (volMedia) trackIdentifiers.VOL_MEDIA = volMedia;
  175. let track = {
  176. artist: isVA ? VA : undefined,
  177. artists: /*isVA ? undefined : */artist,
  178. album: album,
  179. album_year: /*trackYear || */albumYear || undefined,
  180. release_date: releaseDate,
  181. label: label,
  182. catalog: catalogue,
  183. encoding: encoding,
  184. codec: format,
  185. bitdepth: bitdepth,
  186. samplerate: samplerate || undefined,
  187. media: media,
  188. genre: genres,
  189. disc_number: discNumber,
  190. total_discs: totalDiscs,
  191. disc_subtitle: discSubtitle,
  192. track_number: /^\s*(\d+)\.?\s*$/.test(tr.children[0].firstChild.textContent) ?
  193. parseInt(RegExp.$1) || RegExp.$1 : undefined,
  194. total_tracks: totalTracks,
  195. title: (ref = tr.querySelector('meta[itemprop="name"][content]')) != null ? ref.content
  196. : (ref = tr.querySelector('td > a.trackdetail')) != null ? ref.textContent.trim() : undefined,
  197. performers: performers.length > 0 ? performers : undefined,
  198. composers: composer.length > 0 ? composer : undefined,
  199. conductors: conductor.length > 0 ? conductor : undefined,
  200. compilers: DJs.length > 0 ? DJs : undefined,
  201. duration: durationFromMeta(tr),
  202. url: document.location.origin + document.location.pathname,
  203. description: description,
  204. identifiers: mergeIds(),
  205. cover_url: imgUrl,
  206. };
  207. tracks.push((function() {
  208. if ((ref = tr.querySelector('td > a.trackdetail')) == null) return Promise.reject('link not found');
  209. return globalFetch(ref.pathname + ref.search).then(function(response) {
  210. var detail = response.document.querySelector('div[data-swap="trackdetail-' +
  211. track.identifiers.TRACK_ID + '"] > div > div.row');
  212. if (detail == null) return Promise.reject('element not found');
  213. detail.querySelectorAll('div[class]:nth-of-type(1) > ul > li').forEach(function(li) {
  214. var key = li.querySelector('span'), value = li.lastChild.textContent.trim();
  215. if (key != null && value) key = key.textContent.trim(); else return;
  216. if (key.startsWith('Žánr')) track.genre = value;
  217. if (key.startsWith('Nahrávka dokončena')) track.rec_year = extractYear(value);
  218. if (key.startsWith('Místo nahrání')) track.venue = value;
  219. if (key.startsWith('Rok prvního vydání')) track.pub_year = extractYear(value);
  220. if (copyrightParser.test(key)) track.copyright = value;
  221. });
  222. let trackArtists = [];
  223. for (let i = 0; i < 8; ++i) trackArtists[i] = [];
  224. detail.querySelectorAll('div[class]:nth-of-type(2) > ul > li').forEach(function(li) {
  225. var role = li.querySelector('span');
  226. var artists = Array.from(li.getElementsByTagName('a')).map(a => a.textContent.trim())
  227. .filter(artist => !pseudoArtistParsers.some(rx => rx.test(artist)));
  228. if (role != null && artists.length > 0) role = translateRole(role); else return;
  229. if (role.startsWith('remix'))
  230. trackArtists[2].pushUnique(...artists);
  231. else if (['music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto', 'music improvisation', 'author'].includes(role))
  232. trackArtists[3].pushUnique(...artists);
  233. else if (role == 'DJ')
  234. trackArtists[5].pushUnique(...artists);
  235. else if (role == 'produced by')
  236. trackArtists[6].pushUnique(...artists);
  237. else if (role == 'recorded by') {
  238. } else {
  239. if (['MainArtist'].includes(role))
  240. trackArtists[0].pushUnique(...artists);
  241. else if (['ensemble', 'Artist'].includes(role) || /\b(?:vocals)\b/.test(role))
  242. artists.forEach(_artist => {
  243. trackArtists[artist.includesCaseless(_artist) ? 0 : 1].pushUnique(...artists);
  244. });
  245. else if (['FeaturedArtist'].includes(role))
  246. trackArtists[1].pushUnique(...artists);
  247. else if (['conductor', 'choirmaster', 'director'].includes(role))
  248. trackArtists[4].pushUnique(...artists);
  249. trackArtists[7].pushUnique(...artists.map(artist => artist + ' (' + role + ')'));
  250. }
  251. });
  252. if (trackArtists[1].length > 0 && trackArtists[0].length <= 0) {
  253. trackArtists[0] = trackArtists[1]; trackArtists[1] = [];
  254. }
  255. if (trackArtists[0].length > 0
  256. && (isVA || !trackArtists[0].equalCaselessTo(artist) || trackArtists[1].length > 0)) {
  257. track.track_artists = trackArtists[0];
  258. if (trackArtists[1].length > 0) track.track_guests = trackArtists[1];
  259. }
  260. [
  261. [3, 'composer'],
  262. [4, 'conductor'],
  263. [2, 'remixer'],
  264. [5, 'compiler'],
  265. [6, 'producer'],
  266. [7, 'performer'],
  267. ].forEach(def => { if (trackArtists[def[0]].length > 0) track[def[1] + 's'] = trackArtists[def[0]] })
  268. return track;
  269. });
  270. })().catch(function(reason) {
  271. console.error('Supraphonline parser failed to get track', index + 1, 'detail:', reason);
  272. return Promise.resolve(track);
  273. }));
  274. } // track
  275. });
  276. Promise.all(tracks).then(tracks => tracks.map(track => {
  277. if (Array.isArray(track.track_artists) && track.track_artists.length > 0) {
  278. var trackArtist = joinArtists(track.track_artists);
  279. if (Array.isArray(track.track_guests) && track.track_guests.length > 0)
  280. trackArtist += ' feat. ' + joinArtists(track.track_guests);
  281. }
  282. return [
  283. isVA ? VA : joinArtists(track.artists) || '',
  284. track.album || '',
  285. track.album_year || track.pub_year || '',
  286. track.release_date || '',
  287. track.genre || '',
  288. track.label || '',
  289. track.catalog || '',
  290. track.disc_number || '',
  291. track.total_discs > 1 ? track.total_discs : '',
  292. track.disc_subtitle || '',
  293. track.track_number || '',
  294. track.total_tracks || '',
  295. trackArtist || (Array.isArray(track.artists) && track.artists.length > 0 ?
  296. joinArtists(track.artists) : track.artist) || '',
  297. track.title || '',
  298. (track.performers || []).join(', '),
  299. (track.composers || []).join(', '),
  300. track.media,
  301. track.description,
  302. track.url,
  303. ].join('\x1E');
  304. }).join('\n')).then(clipBoard => { GM_setClipboard(clipBoard, 'text') }).catch(e => { alert(e) }).then(function() {
  305. evt.target.disabled = false;
  306. evt.target.textContent = original_text;
  307. });
  308.  
  309. function translateGenre(genre) {
  310. if (!genre || typeof genre != 'string') return undefined;
  311. [
  312. ['Orchestrální hudba', 'Orchestral Music'],
  313. ['Komorní hudba', 'Chamber Music'],
  314. ['Vokální', 'Classical, Vocal'],
  315. ['Klasická hudba', 'Classical'],
  316. ['Melodram', 'Classical, Melodram'],
  317. ['Symfonie', 'Symphony'],
  318. ['Vánoční hudba', 'Christmas Music'],
  319. [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
  320. ['Dechová hudba', 'Brass Music'],
  321. ['Elektronika', 'Electronic'],
  322. ['Folklor', 'Folclore, World Music'],
  323. ['Instrumentální hudba', 'Instrumental'],
  324. ['Latinské rytmy', 'Latin'],
  325. ['Meditační hudba', 'Meditative'],
  326. ['Vojenská hudba', 'Military Music'],
  327. ['Pro děti', 'Children'],
  328. ['Pro dospělé', 'Adult'],
  329. ['Mluvené slovo', 'Spoken Word'],
  330. ['Audiokniha', 'audiobook'],
  331. ['Humor', 'humour'],
  332. ['Pohádka', 'Fairy-Tale'],
  333. ].forEach(function(subst) {
  334. if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
  335. || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
  336. });
  337. return genre;
  338. }
  339.  
  340. function translateRole(elem) {
  341. return elem instanceof HTMLElement ? [
  342. [/\b(?:klavír)\b/, 'piano'],
  343. [/\b(?:housle)\b/, 'violin'],
  344. [/\b(?:varhany)\b/, 'organ'],
  345. [/\b(?:cembalo)\b/, 'harpsichord'],
  346. [/\b(?:trubka)\b/, 'trumpet'],
  347. [/\b(?:soprán)\b/, 'soprano'],
  348. [/\b(?:alt)\b/, 'alto'],
  349. [/\b(?:baryton)\b/, 'baritone'],
  350. [/\b(?:bas)\b/, 'basso'],
  351. [/\b(?:syntezátor)\b/, 'synthesizer'],
  352. [/\b(?:zpěv)\b/, 'vocals'],
  353. [/\b(?:čte|četba)\b/, 'narration'],
  354. [/\b(?:akustická kytara)\b/, 'acoustic guitar'],
  355. [/\b(?:kytara)\b/, 'guitar'],
  356. ['hudební těleso', 'ensemble'],
  357. ['vypravuje', 'narration'],
  358. ['komentář', 'commentary'],
  359. ['hovoří a zpívá', 'speaks and sings'],
  360. ['hovoří', 'spoken by'],
  361. ['improvizace', 'improvisation'],
  362. ['původní text', 'original lyrics'],
  363. ['hudba+text', 'music+lyrics'],
  364. ['český text', 'czech lyrics'],
  365. ['text', 'lyrics'],
  366. ['hudba', 'music'],
  367. ['hudební improvizace', 'music improvisation'],
  368. ['autor', 'author'],
  369. ['účinkuje', 'participating'],
  370. ['dirigent', 'conductor'],
  371. ['sbormistr', 'choirmaster'],
  372. ['řídí', 'director'],
  373. ['produkce', 'produced by'],
  374. ['nahrál', 'recorded by'],
  375. ['digitální přepis', 'A/D transfer'],
  376. ].reduce((r, def) => r.replace(...def), elem.textContent.trim().replace(/\s*:.*$/, '')) : undefined;
  377. }
  378.  
  379. function mergeIds() {
  380. var r = Object.assign({}, identifiers, trackIdentifiers);
  381. trackIdentifiers = {};
  382. return r;
  383. }
  384. }
  385.  
  386. function joinArtists(arr, decorator = artist => artist) {
  387. if (!Array.isArray(arr)) return null;
  388. if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
  389. if (arr.length < 3) return arr.map(decorator).join(' & ');
  390. return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
  391. }
  392.  
  393. function timeStringToTime(str) {
  394. if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
  395. var t = 0, a = RegExp.$2.split(':');
  396. while (a.length > 0) t = t * 60 + parseFloat(a.shift());
  397. return RegExp.$1 ? -t : t;
  398. }
  399.  
  400. function normalizeDate(str, countryCode = undefined) {
  401. if (typeof str != 'string') return null;
  402. var match;
  403. function formatOutput(yearIndex, montHindex, dayIndex) {
  404. var year = parseInt(match[yearIndex]), month = parseInt(match[montHindex]), day = parseInt(match[dayIndex]);
  405. if (year < 30) year += 2000; else if (year < 100) year += 1900;
  406. if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 0 || day > 31) return null;
  407. return year.toString() + '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
  408. }
  409. if ((match = /\b(\d{4})-(\d{1,2})-(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // US
  410. if ((match = /\b(\d{4})\/(\d{1,2})\/(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3);
  411. if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null
  412. && (parseInt(match[1]) > 12 || /\b(?:be|it)/.test(countryCode))) return formatOutput(3, 2, 1); // BE, IT
  413. if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null) return formatOutput(3, 1, 2); // US
  414. if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // UK, IE, FR, ES
  415. if ((match = /\b(\d{1,2})-(\d{1,2})-((?:\d{2}|\d{4}))\b/.exec(str)) != null) return formatOutput(3, 2, 1); // NL
  416. if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // CZ, DE
  417. if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{2})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // AT, CH, DE, LU
  418. if ((match = /\b(\d{4})\. *(\d{1,2})\. *(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // JP
  419. return extractYear(str);
  420. }
  421.  
  422. function extractYear(expr) {
  423. if (typeof expr == 'number') return Math.round(expr);
  424. if (typeof expr != 'string') return null;
  425. if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
  426. var d = new Date(expr);
  427. return parseInt(isNaN(d) ? expr : d.getFullYear());
  428. }
  429.  
  430. function durationFromMeta(elem) {
  431. if (!(elem instanceof HTMLElement)) return undefined;
  432. let meta = elem.querySelector('meta[itemprop="duration"][content]');
  433. if (meta == null) return undefined;
  434. let m = /^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.exec(meta.content);
  435. if (m != null)
  436. return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
  437. m = timeStringToTime(meta.content);
  438. return m != null ? m : undefined;
  439. }