Supraphonline foobar2000 tagging support

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

目前为 2024-09-15 提交的版本,查看 最新版本

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