[RED/OPS/NWCD] Upload Assistant

Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection (via pasted output of copy command), release integrity check, two tracklist layouts, colours customization, featured artists extraction, classical works formatting, coverart fetching from store, checking for previous upload and more. As alternative to pasted playlist, e.g. for requests creation, valid URL to product page on supported web can be used -- see below.

当前为 2020-02-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name [RED/OPS/NWCD] Upload Assistant
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.209
  5. // @description Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection (via pasted output of copy command), release integrity check, two tracklist layouts, colours customization, featured artists extraction, classical works formatting, coverart fetching from store, checking for previous upload and more. As alternative to pasted playlist, e.g. for requests creation, valid URL to product page on supported web can be used -- see below.
  6. // @author Anakunda
  7. // @iconURL https://redacted.ch/favicon.ico
  8. // @match https://redacted.ch/upload.php*
  9. // @match https://redacted.ch/torrents.php?action=editgroup&*
  10. // @match https://redacted.ch/torrents.php?action=edit&*
  11. // @match https://redacted.ch/requests.php?action=new*
  12. // @match https://redacted.ch/requests.php?action=edit*
  13. // @match https://notwhat.cd/upload.php*
  14. // @match https://notwhat.cd/torrents.php?action=editgroup&*
  15. // @match https://notwhat.cd/torrents.php?action=edit&*
  16. // @match https://notwhat.cd/requests.php?action=new*
  17. // @match https://notwhat.cd/requests.php?action=edit*
  18. // @match https://orpheus.network/upload.php*
  19. // @match https://orpheus.network/torrents.php?action=editgroup&*
  20. // @match https://orpheus.network/torrents.php?action=edit&*
  21. // @match https://orpheus.network/requests.php?action=new*
  22. // @match https://orpheus.network/requests.php?action=edit*
  23. // @connect file://*
  24. // @connect *
  25. // @grant GM_xmlhttpRequest
  26. // @grant GM_getValue
  27. // @grant GM_setValue
  28. // @grant GM_deleteValue
  29. // //@require https://connect.soundcloud.com/sdk/sdk-3.3.2.js
  30. // @require https://greasyfork.org/scripts/393837-qobuzlib/code/QobuzLib.js
  31. // @require https://greasyfork.org/scripts/394414-ua-resource/code/UA-resource.js
  32. // ==/UserScript==
  33.  
  34. // Additional setup: to work, set the pattern below as built-in foobar2000 copy command or custom Text Tools plugin quick copy command
  35. // $replace($replace([%album artist%]$char(30)[%album%]$char(30)[$if3(%date%,%ORIGINAL RELEASE DATE%,%year%)]$char(30)[$if3(%releasedate%,%retail date%,%date%,%year%)]$char(30)[$if2(%label%,%publisher%)]$char(30)[$if3(%catalog%,%CATALOGNUMBER%,%CATALOG NUMBER%,%labelno%,%catalog #%,%SKU%)]$char(30)[%country%]$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%,%format%,%source%,%MEDIATYPE%,%SOURCEMEDIA%,%discogs_format%)]$char(30)[%genre%[|%style%]]$char(30)[%discnumber%]$char(30)[$if2(%totaldiscs%,%disctotal%)]$char(30)[%discsubtitle%]$char(30)[%track number%]$char(30)[$if2(%totaltracks%,%TRACKTOTAL%)]$char(30)[%title%]$char(30)[%track artist%]$char(30)[$if($strcmp(%performer%,%artist%),,%performer%)]$char(30)[$if3(%composer%,%writer%,%SONGWRITER%,%author%,%LYRICIST%)]$char(30)[%conductor%]$char(30)[%remixer%]$char(30)[$if2(%compiler%,%mixer%)]$char(30)[$if2(%producer%,%producedby%)]$char(30)[%length_seconds_fp%]$char(30)[%length_samples%]$char(30)[%filesize%]$char(30)[%replaygain_album_gain%]$char(30)[%album dynamic range%]$char(30)[%__tool%][ | $if2(%MQAENCODER%,%ENCODER%)][ | %ENCODER_OPTIONS%]$char(30)[$if2(%url%,%www%)]$char(30)[$directory_path(%path%)]$char(30)[$if2(%comment%,%description%)]$char(30)$trim([BARCODE=$trim($replace($if3(%barcode%,%UPC%,%EAN%,%MCN%), ,)) ][DISCID=$trim(%DISCID%) ][ASIN=$trim(%ASIN%) ][ISRC=$trim(%ISRC%) ][ISWC=$trim(%ISWC%) ][DISCOGS_ID=$trim(%discogs_release_id%) ][MBID=$trim(%MUSICBRAINZ_ALBUMID%) ][ACCURATERIPCRC=$trim(%ACCURATERIPCRC%) ][ACCURATERIPDISCID=$trim(%ACCURATERIPDISCID%) ][ACCURATERIPID=$trim(%ACCURATERIPID%) ][SOURCEID=$trim($replace(%SOURCEID%, ,_)) ][CT_TOC=$trim(%CDTOC%) ][ITUNES_TOC=$trim(%ITUNES_CDDB_1%) ][RELEASETYPE=$replace($if2(%RELEASETYPE%,%RELEASE TYPE%), ,_) ][COMPILATION=$trim(%compilation%) ][EXPLICIT=$trim(%EXPLICIT%) ]SCENE=$if($and(%ENCODER%,%LANGUAGE%,%MEDIA%,%PUBLISHER%,%RELEASE TYPE%,%RETAIL DATE%,%RIP DATE%,%RIPPING TOOL%),1,0) [ORIGINALFORMAT=$trim($replace(%ORIGINALFORMAT%, ,_)) ][BPM=$trim(%BPM%) ]),$char(13),$char(29)),$char(10),$char(28))
  36. //
  37. // List of supported domains for online capturing of release details:
  38. //
  39. // Music releases:
  40. // - qobuz.com
  41. // - highresaudio.com
  42. // - bandcamp.com
  43. // - prestomusic.com
  44. // - discogs.com
  45. // - supraphonline.cz
  46. // - bontonland.cz (closing soon)
  47. // - nativedsd.com
  48. // - junodownload.com
  49. // - hdtracks.com
  50. // - deezer.com
  51. // - spotify.com
  52. // - prostudiomasters.com
  53. // - play.google.com
  54. // - 7digital.com
  55. // - e-onkyo.com
  56. // - acousticsounds.com
  57. // - indies.eu
  58. // - beatport.com
  59. // - traxsource.com
  60. // - musicbrainz.org
  61. // - music.apple.com
  62. //
  63. // Ebooks releases:
  64. // - martinus.cz, martinus.sk
  65. // - goodreads.com
  66. // - databazeknih.cz
  67. //
  68. // Application releases:
  69. // - sanet.st
  70.  
  71. 'use strict';
  72.  
  73. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || window.InstallTrigger;
  74.  
  75. function testDomain(domain) {
  76. return document.location.hostname.toLowerCase() == domain.toLowerCase();
  77. }
  78. function testPath(path, query) {
  79. return document.location.pathname.toLowerCase() == '/'.concat(path.toLowerCase(), '.php')
  80. && (!query || document.location.search.toLowerCase().startsWith('?'.concat(query.toLowerCase())));
  81. }
  82.  
  83. const isRED = testDomain('redacted.ch');
  84. const isNWCD = testDomain('notwhat.cd');
  85. const isOPS = testDomain('orpheus.network');
  86.  
  87. const isUpload = testPath('upload');
  88. const isEdit = testPath('torrents', 'action=editgroup&');
  89. const isRequestNew = testPath('requests', 'action=new');
  90. const isRequestEdit = testPath('requests', 'action=edit&');
  91. const isAddFormat = isUpload && /\bgroupid=(\d+)\b/i.test(document.location.search);
  92.  
  93. const urlParser = /^\s*(https?:\/\/\S+)\s*$/i;
  94. const imghostOrigin = 'https://ptpimg.me';
  95. const mbrRlsPrefix = 'https://musicbrainz.org/release/';
  96. const discogsOrigin = 'https://www.discogs.com';
  97. const deezerAlbumPrefix = 'https://www.deezer.com/album/';
  98. const descriptionFields = ['album_desc', 'body', 'description', 'release_desc', 'release_lineage'];
  99. //const promiseAll = Promise.allSettled || Promise.all;
  100.  
  101. const spotify_clientid = '6d358a207c634b1ebac640149a6090da';
  102. const spotify_clientsecret = '4c59880a4ec241ed9c89a24e66468c64';
  103. const discogs_token = 'CISOUfiQctZCkUedWJzPhzTXxRYihifZgflZAfEm';
  104. const lastfm_api_key = 'b9f26370d7266fbb3151b2ad4f7a74c9';
  105.  
  106. const defaultPrefs = {
  107. autfill_delay: 1000, // delay in ms to autofill form after pasting text into box, 0 to disable
  108. clean_on_apply: 0, // clean the input box on successfull fill
  109. cleanup_descriptions: 1, // pre-submit cleanup to all description fields (remove empty placeholders, redundant info and garbage like empty tag pairs etc.)
  110. keep_meaningles_composers: 0, // keep composers from file tags also for non-composer emphasing genres
  111. default_medium: '', // preset this media type if it can't be deduced from metadata (Gazelle-compatible names as they appear in dropdown, empty string to not use)
  112. single_threshold: 10 * 60, // For autodetection of release type: max length of single in s
  113. EP_threshold: 30 * 60, // For autodetection of release type: max time of EP in s
  114. auto_rehost_cover: 1, // PTPIMG / using 3rd party script
  115. auto_preview_cover: 1,
  116. huge_image_warning: 5, // threshold in MB for making bandwith stressing cover size warning // 0 to disable
  117. cover_lookup_provider: 'all', // itunes | lastfm | deezer | qobuz | musicbrainz | google | all | empty for no lookup
  118. fetch_tags_from_artist: 0, // add N most used tags from release artist (if one) - experimental/may inject nonsense tags for coinciding artists; 0 for disable
  119. check_integrity_online: 1, // If provided URL tag, compare local release with release online and lookup for discrepancies
  120. check_whitespace: 1, // check tags for leading/trailing spaces and unreadable characters
  121. estimate_decade_tag: 1, // deduce decade tag (1980s, etc.) from album year for regular albums
  122. ops_always_edition: 1, // (only new uploads) don't use original release but always specific edition (standard on other trackers)
  123. dragdrop_patch_to_ptpimgit: 1,
  124. sacd_decoder: 'foobar2000\'s SACD decoder (DSD2PCM direct / 64fp / 30kHz lowpass)',
  125. ptpimg_api_key: '',
  126. selfrelease_label: 'self-released',
  127. discogs_key: '', // Applicxation/Consumer Key
  128. discogs_secret: '', // Application/Consumer Secret
  129. soundcloud_clientid: '',
  130. catbox_userhash: '',
  131. upcoming_tags: '', // add this tag(s) to upcoming releases (requests); empty to disable
  132. remap_texttools_newlines: 0, // convert underscores to linebreaks (ambiguous)
  133. messages_verbosity: 0,
  134. // request specific
  135. request_default_bounty: 0, // set this bounty in MB after successfull fill of request form / 0 for disable
  136. always_request_perfect_flac: 0,
  137. include_tracklist_in_request: 0, // 0: include one line summary only; 1: include full tracklisting
  138. // tracklist specific
  139. tracklist_style: 1, // 1: classic with components colouring, 2: propertional font right-justified, 3: classic center aligned
  140. sort_tracklist: 1,
  141. max_tracklist_width: 80, // right margin of the right aligned tracklist. should not exceed the group description width on any device
  142. tracklist_size: 2, // PHPBB fonst size
  143. title_separator: '. ', // divisor of track# and title
  144. pad_leader: ' ',
  145. bpm_summary: 1,
  146. tracklist_head_color: '#62a6ad', // #4682B4 / #a7bdd0
  147. // classical tracklist only components colouring
  148. tracklist_disctitle_color: '#2bb7b7', // #bb831c
  149. tracklist_work_color: '#98984d', // #b16890
  150. tracklist_tracknumber_color: '#8899AA',
  151. tracklist_artist_color: '#b79665',
  152. tracklist_composer_color: '#8ca014',
  153. tracklist_duration_color: '#33a6cc', // #2196f3
  154. };
  155. var prefs = {
  156. save: function() {
  157. for (var iter in this) {
  158. if (typeof this[iter] != 'function' && this[iter] != undefined) GM_setValue(iter, this[iter]);
  159. }
  160. },
  161. };
  162. Object.keys(defaultPrefs).forEach(key => { prefs[key] = GM_getValue(key, defaultPrefs[key]) });
  163.  
  164. document.head.appendChild(document.createElement('style')).innerHTML = `
  165. .ua-messages {
  166. text-indent: -2em;
  167. margin-left: 2em;
  168. font: 11px "Segoe UI", Calibri, sans-serif;
  169. }
  170. .ua-messages-bg { padding: 15px; text-align: left; background-color: darkslategray; }
  171.  
  172. .ua-critical { color: red; font-weight: bold; font-size: 13px; }
  173. .ua-warning { color: #ff8d00; font-weight: 500; font-size: 12px; }
  174. .ua-notice { color: #e3d67b; }
  175. .ua-info { color: white; }
  176.  
  177. .ua-button { vertical-align: middle; background-color: transparent; }
  178. .ua-button2 { /*color: beige; */width: 13em; font: 300 x-small "Segoe UI", Calibri, sans-serif; }
  179. .ua-input {
  180. font: 600 x-small "Segoe UI", Calibri, sans-serif;
  181. color: slategray; background-color: antiquewhite;
  182. width: 620px; height: 40px;
  183. margin-top: 8px; margin-bottom: 8px;
  184. }
  185. .ua-input:focus { color: black; }
  186.  
  187. #cover-preview {
  188. width: 100%;
  189. /*box-shadow: 3px 3px 3px;*/
  190. }
  191. #cover-size {
  192. width: 100%;
  193. color: white; background-color: #0a4a75;
  194. font: 12px "Segoe UI", Calibri, sans-serif;
  195. text-align: center;
  196. /*padding-top: 5px;*/
  197. }
  198.  
  199. ::placeholder {
  200. font: 10pt "Segoe UI", Calibri, sans-serif;
  201. color: gray;
  202. opacity: 0.5;
  203. /*text-shadow: 0px 0px 3px #b4b4b4;*/
  204. font-weight: bold;
  205. }
  206. `;
  207.  
  208. var ref, tbl, elem, child, rehostItBtn, gazelleApiTimeFrame = {}, tfMessages = [];
  209. var spotifyCredentials = {}, discogsCredentials = {}, siteArtistsCache = {}, notSiteArtistsCache = [];
  210. var messages = null, autofill = false, dom, domParser = new DOMParser();
  211. const ctxt = document.createElement('canvas').getContext('2d');
  212.  
  213. if (isUpload) {
  214. ref = document.querySelector('form#upload_table > div#dynamic_form');
  215. if (ref == null) return;
  216. common1();
  217. let x = [];
  218. x.push(document.createElement('tr'));
  219. x[0].classList.add('ua-button');
  220. child = document.createElement('input');
  221. child.id = 'fill-from-text';
  222. child.value = 'Fill from text (overwrite)';
  223. child.type = 'button';
  224. child.className = 'ua-button2';
  225. child.onclick = fillFromText;
  226. x[0].append(child);
  227. elem.append(x[0]);
  228. x.push(document.createElement('tr'));
  229. x[1].classList.add('ua-button');
  230. child = document.createElement('input');
  231. child.id = 'fill-from-text-weak';
  232. child.value = 'Fill from text (keep values)';
  233. child.type = 'button';
  234. child.className = 'ua-button2';
  235. child.onclick = fillFromText;
  236. x[1].append(child);
  237. elem.append(x[1]);
  238. common2();
  239. ref.parentNode.insertBefore(tbl, ref);
  240. } else if (isEdit) {
  241. ref = document.querySelector('form.edit_form > div > div > input[type="submit"]');
  242. if (ref == null) return;
  243. ref = ref.parentNode;
  244. ref.parentNode.insertBefore(document.createElement('br'), ref);
  245. common1();
  246. child = document.createElement('input');
  247. child.id = 'append-from-text';
  248. child.value = 'Fill from text (append)';
  249. child.type = 'button';
  250. child.className = 'ua-button2';
  251. child.style.height = '52px';
  252. child.onclick = fillFromText;
  253. elem.append(child);
  254. common2();
  255. tbl.style.marginBottom = '10px';
  256. ref.parentNode.insertBefore(tbl, ref);
  257. } else if (isRequestNew) {
  258. ref = document.getElementById('categories');
  259. if (ref == null) return;
  260. ref = ref.parentNode.parentNode.nextElementSibling;
  261. ref.parentNode.insertBefore(document.createElement('br'), ref);
  262. common1();
  263. child = document.createElement('input');
  264. child.id = 'fill-from-text-weak';
  265. child.value = 'Fill from URL';
  266. child.type = 'button';
  267. child.className = 'ua-button2';
  268. child.style.height = '52px';
  269. child.onclick = fillFromText;
  270. elem.append(child);
  271. common2();
  272. child = document.createElement('td');
  273. child.colSpan = 2;
  274. child.append(tbl);
  275. elem = document.createElement('tr');
  276. elem.append(child);
  277. ref.parentNode.insertBefore(elem, ref);
  278. } else if (isRequestEdit) {
  279. ref = document.querySelector('input#button[type="submit"]');
  280. if (ref == null) return;
  281. ref = ref.parentNode.parentNode;
  282. ref.parentNode.insertBefore(document.createElement('br'), ref);
  283. common1();
  284. child = document.createElement('input');
  285. child.id = 'append-from-text';
  286. child.value = 'Fill from text (append)';
  287. child.type = 'button';
  288. child.className = 'ua-button2';
  289. child.style.height = '52px';
  290. child.onclick = fillFromText;
  291. elem.append(child);
  292. common2();
  293. tbl.style.marginBottom = '10px';
  294. elem = document.createElement('tr');
  295. child = document.createElement('td');
  296. child.colSpan = 2;
  297. child.append(tbl);
  298. elem.append(child);
  299. ref.parentNode.insertBefore(elem, ref);
  300. }
  301.  
  302. function common1() {
  303. tbl = document.createElement('tr');
  304. tbl.style.backgroundColor = 'darkgoldenrod';
  305. tbl.style.verticalAlign = 'middle';
  306. elem = document.createElement('td');
  307. elem.style.textAlign = 'center';
  308. child = document.createElement('textarea');
  309. child.id = 'UA-data';
  310. child.name = 'UA-data';
  311. child.className = 'ua-input';
  312. child.spellcheck = false;
  313. child.placeholder = 'Paste / drag & drop selected album from foobar2000 or URL from supported site here';
  314. child.onpaste = uaInsert;
  315. if (!isNWCD) {
  316. child.ondrop = uaInsert;
  317. child.ondragover = clear0;
  318. if (isFirefox) child.oninput = fixFirefoxDropBug;
  319. } else child.ondrop = child.ondragstart = child.ondragover = function(evt) {
  320. evt.preventDefault();
  321. evt.stopPropagation();
  322. return false;
  323. };
  324. var desc = document.getElementById('body');
  325. if (desc != null && urlParser.test(desc.value)) {
  326. child.value = RegExp.$1;
  327. desc.value = '';
  328. if (prefs.autfill_delay > 0) {
  329. autofill = true;
  330. setTimeout(fillFromText, prefs.autfill_delay);
  331. };
  332. }
  333. elem.append(child);
  334. tbl.append(elem);
  335. elem = document.createElement('td');
  336. elem.style.textAlign = 'center';
  337. }
  338. function common2() {
  339. tbl.append(elem);
  340. var tb = document.createElement('tbody');
  341. tb.append(tbl);
  342. tbl = document.createElement('table');
  343. tbl.id = 'upload assistant';
  344. tbl.append(tb);
  345. }
  346.  
  347. if ((ref = document.getElementById('categories')) != null) {
  348. ref.addEventListener('change', function(e) {
  349. elem = document.getElementById('upload assistant');
  350. if (elem != null) elem.style.visibility = this.value < 4
  351. || ['Music', 'Applications', 'E-Books', 'Audiobooks'].includes(this.value) ? 'visible' : 'collapse';
  352. setTimeout(setHandlers, 2000);
  353. });
  354. }
  355.  
  356. if ((ref = document.getElementById('upload-table') || document.querySelector('form.edit_form')
  357. || document.getElementById('upload_table') || document.getElementById('request_form')) != null) {
  358. ref.ondragover = voidDragHandler1;
  359. ref.ondrop = voidDragHandler1;
  360. }
  361. setHandlers();
  362. if ((ref = isUpload ? document.getElementById('file') : null) != null) {
  363. ref.oninput = function(evt) { if (evt.target.files.length > 0) validataTorrentFile(evt.target.files[0]) };
  364. if (ref.files.length > 0) validataTorrentFile(ref.files[0]);
  365. }
  366. if (!isRED && (ref = document.querySelector('table#dnulist')) != null) {
  367. function toggleVisibility() {
  368. var show = ref.style.display.toLowerCase() == 'none';
  369. ref.style.display = show ? 'block' : 'none';
  370. ref.previousElementSibling.style.display = show ? 'block' : 'none';
  371. }
  372. toggleVisibility();
  373. if ((ref = document.querySelector('h3#dnu_header')) != null) {
  374. elem = ref.parentNode;
  375. child = document.createElement('a');
  376. child.href = '#';
  377. child.onclick = function(evt) {
  378. if ((ref = document.querySelector('table#dnulist')) != null) toggleVisibility();
  379. };
  380. child.append(ref);
  381. elem.prepend(child);
  382. }
  383. }
  384.  
  385. if (isRequestNew) {
  386. let title = document.querySelector('input[name="title"]');
  387. if (title != null) setTimeout(function(e) { title.readOnly = false }, 1000);
  388. }
  389.  
  390. Array.prototype.includesCaseless = function(str) {
  391. return typeof str == 'string' && this.find(it => typeof it == 'string' && it.toLowerCase() == str.toLowerCase()) != undefined;
  392. };
  393. Array.prototype.pushUnique = function(...items) {
  394. items.forEach(it => { if (!this.includes(it)) this.push(it) });
  395. return this.length;
  396. };
  397. Array.prototype.pushUniqueCaseless = function(...items) {
  398. items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  399. return this.length;
  400. };
  401. // Array.prototype.getUnique = function(prop) {
  402. // return this.every((it) => it[prop] && it[prop] == this[0][prop]) ? this[0][prop] : null;
  403. // };
  404. Array.prototype.equalTo = function(arr) {
  405. return Array.isArray(arr) && arr.length == this.length
  406. && Array.from(arr).sort().toString() == Array.from(this).sort().toString();
  407. };
  408. Array.prototype.homogeneous = function() {
  409. return this.every(elem => elem === this[0]);
  410. }
  411.  
  412. String.prototype.toASCII = function() {
  413. return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
  414. };
  415. String.prototype.trueLength = function() {
  416. return Array.from(this).length;
  417. //return this.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').length;
  418. // var index = 0, width = 0, len = 0;
  419. // while (index < this.length) {
  420. // var point = this.codePointAt(index);
  421. // width = 0;
  422. // while (point) {
  423. // ++width;
  424. // point = point >> 8;
  425. // }
  426. // index += Math.round(width / 2);
  427. // ++len;
  428. // }
  429. // return len;
  430. };
  431. String.prototype.flatten = function() {
  432. return this.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
  433. };
  434. String.prototype.expand = function() {
  435. return this.replace(/\x1D/g, '\r').replace(/\x1C/g, '\n');
  436. };
  437. String.prototype.titleCase = function() {
  438. return this.toLowerCase().split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(' ');
  439. };
  440. String.prototype.collapseGaps = function() {
  441. return this.replace(/(?:[ \t]*\r?\n){3,}/g, '\n\n').replace(/\[(\w+)\]\[\/\1\]/ig,'').trim();
  442. };
  443. Date.prototype.getDateValue = function() {
  444. return Math.floor((this.getTime() / 1000 / 60 - this.getTimezoneOffset()) / 60 / 24);
  445. };
  446. File.prototype.getText = function(encoding) {
  447. return new Promise(function(resolve, reject) {
  448. var reader = new FileReader();
  449. reader.onload = function() { resolve(reader.result) };
  450. reader.onerror = reader.onabort = reader.ontimeout = error => { reject('FileReader error (' + this.name + ')') };
  451. reader.readAsText(this, encoding);
  452. }.bind(this));
  453. };
  454. class HTML extends String { };
  455.  
  456. const excludedCountries = [
  457. /\b(?:United\s+States|USA?)\b/,
  458. /\b(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|UK)\b/,
  459. /\b(?:Europe|European\s+Union|EU)\b/,
  460. /\b(?:Unknown)\b/,
  461. ];
  462.  
  463. class TagManager extends Array {
  464. constructor(...tags) {
  465. super();
  466. this.presubstitutions = [
  467. [/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
  468. [/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
  469. [/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
  470. ];
  471. this.substitutions = [
  472. [/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
  473. [/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
  474. [/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
  475. ['AOR', 'album.oriented.rock'],
  476. [/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
  477. [/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
  478. [/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
  479. ['World', 'world.music'],
  480. [/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
  481. [/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhytm.and.blues'],
  482. [/\b(?:Soundtracks?)$/i, 'score'],
  483. ['Electro', 'electronic'],
  484. ['Metal', 'heavy.metal'],
  485. ['NonFiction', 'non.fiction'],
  486. ['Rap', 'hip.hop'],
  487. ['NeoSoul', 'neo.soul'],
  488. ['NuJazz', 'nu.jazz'],
  489. [/^J[\s\-]Pop$/i, 'jpop'],
  490. [/^K[\s\-]Pop$/i, 'jpop'],
  491. [/^J[\s\-]Rock$/i, 'jrock'],
  492. ['Hardcore', 'hardcore.punk'],
  493. ['Garage', 'garage.rock'],
  494. [/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
  495. [/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
  496. [/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
  497. [/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
  498. [/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
  499. ['GoaTrance', 'goa.trance'],
  500. [/^Female\s+Vocal\w*$/i, 'female.vocalist'],
  501. ['Contemporary R&B', 'contemporary.rhytm.and.blues'],
  502. // Country aliases
  503. ['Canada', 'canadian'],
  504. ['Australia', 'australian'],
  505. ['New Zealand', 'new.zealander'],
  506. ['Japan', 'japanese'],
  507. ['Taiwan', 'thai'],
  508. ['China', 'chinese'],
  509. ['Singapore', 'singaporean'],
  510. [/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
  511. ['Turkey', 'turkish'],
  512. ['Israel', 'israeli'],
  513. ['France', 'french'],
  514. ['Germany', 'german'],
  515. ['Spain', 'spanish'],
  516. ['Italy', 'italian'],
  517. ['Sweden', 'swedish'],
  518. ['Norway', 'norwegian'],
  519. ['Finland', 'finnish'],
  520. ['Greece', 'greek'],
  521. [/^(?:Netherlands|Holland)$/i, 'dutch'],
  522. ['Belgium', 'belgian'],
  523. ['Luxembourg', 'luxembourgish'],
  524. ['Denmark', 'danish'],
  525. ['Switzerland', 'swiss'],
  526. ['Austria', 'austrian'],
  527. ['Portugal', 'portugese'],
  528. ['Ireland', 'irish'],
  529. ['Scotland', 'scotish'],
  530. ['Iceland', 'icelandic'],
  531. [/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
  532. [/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
  533. ['Hungary', 'hungarian'],
  534. ['Poland', 'polish'],
  535. ['Estonia', 'estonian'],
  536. ['Latvia', 'latvian'],
  537. ['Lithuania', 'lithuanian'],
  538. ['Moldova', 'moldovan'],
  539. ['Armenia', 'armenian'],
  540. ['Ukraine', 'ukrainian'],
  541. ['Yugoslavia', 'yugoslav'],
  542. ['Serbia', 'serbian'],
  543. ['Slovenia', 'slovenian'],
  544. ['Croatia', 'croatian'],
  545. ['Macedonia', 'macedonian'],
  546. ['Montenegro', 'montenegrin'],
  547. ['Romania', 'romanian'],
  548. ['Malta', 'maltese'],
  549. ['Brazil', 'brazilian'],
  550. ['Mexico', 'mexican'],
  551. ['Argentina', 'argentinean'],
  552. ['Jamaica', 'jamaican'],
  553. // Books
  554. ['Beletrie', 'fiction'],
  555. ['Satira', 'satire'],
  556. ['Komiks', 'comics'],
  557. ['Komix', 'comics'],
  558. // Removals
  559. ['Unknown'],
  560. ['Other'],
  561. ['New'],
  562. ['Ostatni'],
  563. ['Knihy'],
  564. ['Audioknihy'],
  565. ['dsbm'],
  566. [/^(?:Audio\s*kniha|Audio\s*Book)$/i],
  567. ].concat(excludedCountries.map(it => [it]));
  568. this.splits = [
  569. ['Alternative', 'Indie'],
  570. ['Rock', 'Pop'],
  571. ['Soul', 'Funk'],
  572. ['Ska', 'Rocksteady'],
  573. ['Jazz Fusion', 'Jazz Rock'],
  574. ['Rock', 'Pop'],
  575. ['Jazz', 'Funk'],
  576. ];
  577. this.additions = [
  578. [/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
  579. [/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Vocal|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Latin|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
  580. [/^(?:Opera)$/i, 'classical'],
  581. [/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
  582. [/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
  583. [/^(?:Symphony)$/i, 'classical'],
  584. [/^(?:Sacred\s+Vocal)\b/i, 'classical'],
  585. [/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
  586. ];
  587. if (tags.length > 0) this.add(...tags);
  588. }
  589.  
  590. add(...tags) {
  591. var added = 0;
  592. for (var tag of tags) {
  593. if (typeof tag != 'string') continue;
  594. qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
  595. this.presubstitutions.forEach(k => { if (k[0].test(tag)) tag = tag.replace(k[0], k[1]) });
  596. tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(function(tag) {
  597. //qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
  598. tag = tag.toASCII().replace(/\(.*?\)|\[.*?\]|\{.*?\}/g, '').trim();
  599. if (tag.length <= 0 || tag == '?') return null;
  600. function test(obj) {
  601. return typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase()
  602. || obj instanceof RegExp && obj.test(tag);
  603. }
  604. for (var k of this.substitutions) {
  605. if (test(k[0])) {
  606. if (k.length >= 1) added += this.add(...k.slice(1));
  607. else addMessage('invalid tag \'' + tag + '\' found', 'warning');
  608. return;
  609. }
  610. }
  611. for (k of this.additions) {
  612. if (test(k[0])) added += this.add(...k.slice(1));
  613. }
  614. for (k of this.splits) {
  615. if (new RegExp('^' + k[0] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[1] + '$', 'i').test(tag)) {
  616. added += this.add(k[0], k[1]); return;
  617. }
  618. if (new RegExp('^' + k[1] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[0] + '$', 'i').test(tag)) {
  619. added += this.add(k[0], k[1]); return;
  620. }
  621. }
  622. tag = tag.
  623. replace(/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1').
  624. replace(/\b(?:Alt\.)(?=\s+)/i, 'Alternative').
  625. replace(/^[3-9]0s$/i, '19$0').
  626. replace(/^[0-2]0s$/i, '20$0').
  627. replace(/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2').
  628. replace(/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and ').
  629. replace(/[\s\-\−\—\–\_\.\,\'\`\~]+/g, '.').
  630. replace(/[^\w\.]+/g, '').
  631. toLowerCase();
  632. if (tag.length >= 2 && !this.includes(tag)) {
  633. this.push(tag);
  634. ++added;
  635. }
  636. }.bind(this));
  637. }
  638. return added;
  639. }
  640. toString() { return Array.from(this).sort().join(', ') }
  641. };
  642.  
  643. return;
  644.  
  645. function fillFromText(evt) {
  646. if (evt == undefined && !autofill) return;
  647. autofill = false;
  648. var overwrite = this.id == 'fill-from-text';
  649. var clipBoard = document.getElementById('UA-data');
  650. if (clipBoard == null) return false;
  651. const VA = 'Various Artists';
  652. messages = document.getElementById('UA-messages');
  653. //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
  654. //if (typeof clipBoard != 'string') return false;
  655. var i, matches, sourceUrl, category = document.getElementById('categories'), xhr = new XMLHttpRequest();
  656. if (category == null && document.getElementById('releasetype') != null
  657. || category != null && (category.value == 0 || category.value == 'Music')) return fillFromText_Music();
  658. if (category != null && (category.value == 1 || category.value == 'Applications')) return fillFromText_Apps();
  659. if (category != null && (category.value == 2 || category.value == 3
  660. || category.value == 'E-Books' || category.value == 'Audiobooks')) return fillFromText_Ebooks();
  661. return category == null ? fillFromText_Apps(true).catch(reason => fillFromText_Ebooks()) : Promise.reject('no category');
  662.  
  663. function fillFromText_Music() {
  664. if (messages != null) messages.parentNode.removeChild(messages);
  665. const dcRlsParser = /^https?:\/\/(?:\w+\.)*discogs\.com\/releases?\/(\d+)(?=$|\/|\?)/i;
  666. const mbrRlsParser = /^https?:\/\/musicbrainz\.org\/(?:\w+\/)*release\/([\w\-]+)/i;
  667. const divs = ['—', '⸺', '⸻'];
  668. const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  669. const multiArtistParsers = [
  670. /(?:\s+[\/\|\×]|\s*(?:;|,(?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?))\s+/,
  671. ];
  672. const pseudoArtistParsers = [
  673. /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
  674. /^(?:traditional|lidová)$/i,
  675. /\b(?:traditional|lidová)$/,
  676. /^(?:tradiční|lidová)\s+/,
  677. /^(?:[Aa]nonym)/,
  678. /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  679. /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  680. /^(?:Various\s+Composers)$/i,
  681. ];
  682. var onlineSource = urlParser.test(clipBoard.value) && RegExp.$1, isVA;
  683. return (onlineSource ? fetchOnline_Music(onlineSource) :
  684. Promise.resolve(clipBoard.value.split(/(?:\r?\n)+/).filter(line => line.trim().length > 0).map(function(line, ndx) {
  685. var metaData = line.split('\x1E'), track = {
  686. artist: metaData.shift() || undefined,
  687. album: metaData.shift() || undefined,
  688. album_year: metaData.shift() || undefined,
  689. release_date: metaData.shift() || undefined,
  690. label: metaData.shift() || undefined,
  691. catalog: metaData.shift() || undefined,
  692. country: metaData.shift() || undefined,
  693. encoding: metaData.shift() || undefined,
  694. codec: metaData.shift() || undefined,
  695. codec_profile: metaData.shift() || undefined,
  696. bitrate: metaData.shift() || undefined,
  697. bd: metaData.shift() || undefined,
  698. sr: metaData.shift() || undefined,
  699. channels: metaData.shift() || undefined,
  700. media: metaData.shift() || undefined,
  701. genre: metaData.shift() || undefined,
  702. discnumber: metaData.shift() || undefined,
  703. totaldiscs: metaData.shift() || undefined,
  704. discsubtitle: metaData.shift() || undefined,
  705. tracknumber: metaData.shift() || undefined,
  706. totaltracks: metaData.shift() || undefined,
  707. title: metaData.shift() || undefined,
  708. track_artist: metaData.shift() || undefined,
  709. performer: metaData.shift() || undefined,
  710. composer: metaData.shift() || undefined,
  711. conductor: metaData.shift() || undefined,
  712. remixer: metaData.shift() || undefined,
  713. compiler: metaData.shift() || undefined,
  714. producer: metaData.shift() || undefined,
  715. duration: metaData.shift() || undefined,
  716. samples: metaData.shift() || undefined,
  717. filesize: metaData.shift() || undefined,
  718. rg: metaData.shift() || undefined,
  719. dr: metaData.shift() || undefined,
  720. vendor: metaData.shift() || undefined,
  721. url: metaData.shift() || undefined,
  722. dirpath: metaData.shift() || undefined,
  723. description: metaData.shift() || undefined,
  724. identifiers: {},
  725. };
  726. metaData.shift().trim().split(/\s+/).forEach(function(it) {
  727. if (/([\w\-]+)[=:](.*)/.test(it)) track.identifiers[RegExp.$1.toUpperCase()] = RegExp.$2.replace(/\x1B/g, ' ');
  728. });
  729. if (prefs.check_whitespace) Object.keys(track).forEach(function(propName) {
  730. if (typeof track[propName] != 'string') return;
  731. if (propName != 'description' && (track[propName].includes('\x1C') || track[propName].includes('\x1D'))) {
  732. track[propName] = track[propName].replace(/[\x1C\x1D]+/g, '');
  733. addMessage('track #' + (ndx + 1) + ' contains linebreaks in tag <' + propName + '>', 'warning');
  734. }
  735. if ((propName == 'description' ? /[\x00-\x08\x0A-\x19]/g : /[\x00-\x19]/g).test(track[propName])) {
  736. track[propName] = track[propName].replace(/[\x00-\x08\x0A-\x19]/g, '');
  737. if (propName != 'description') track[propName].replace(/\x09+/g, ' ');
  738. addMessage('track #' + (ndx + 1) + ' contains control codes in tag <' + propName + '>', 'warning');
  739. }
  740. if (/^\s+$/.test(track[propName])) {
  741. track[propName] = undefined;
  742. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains only spaces', 'warning');
  743. } else if (/^\s+|\s+$/.test(track[propName])) {
  744. track[propName] = track[propName].trim();
  745. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains leading/trailing spaces', 'warning');
  746. }
  747. if (/[ \xA0]{2,}/.test(track[propName])) {
  748. track[propName] = track[propName].replace(/[ \xA0]{2,}/g, ' ')
  749. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains multiple spaces', 'warning');
  750. }
  751. });
  752. if (track.description == '.') track.description = undefined; else if (track.description) {
  753. track.description = track.description.expand();
  754. if (prefs.remap_texttools_newlines) track.description = track.description.replace(/__/g, '\r\n').replace(/_/g, '\n') // ambiguous
  755. track.description = track.description.collapseGaps();
  756. }
  757. ['bitrate', 'bd', 'sr', 'channels', 'totaldiscs', 'totaltracks', 'samples', 'filesize', 'dr'].forEach(function(propName) {
  758. if (track[propName] != undefined) track[propName] = parseInt(track[propName]);
  759. });
  760. ['duration'].forEach(function(propName) {
  761. if (track[propName] != undefined) track[propName] = parseFloat(track[propName]);
  762. });
  763. if (track.album_year) track.album_year = extractYear(track.album_year) || NaN;
  764. return track;
  765. }))
  766. ).then(parseTracks).catch(e => { if (e) addMessage(e, 'critical') });
  767.  
  768. function parseTracks(tracks) {
  769. if (tracks.length <= 0) {
  770. clipBoard.value = '';
  771. throw 'no tracks found';
  772. }
  773. var albumBitrate = 0, totalTime = 0, albumSize = 0, media, release = { totaldiscs: 1, srs: [] };
  774. tracks.forEach(function(track) {
  775. if (!track.artist) {
  776. clipBoard.value = '';
  777. throw new HTML('main artist must be defined in every track' + ruleLink('2.3.16.4'));
  778. }
  779. if (!track.album) {
  780. clipBoard.value = '';
  781. throw new HTML('album title must be defined in every track' + ruleLink('2.3.16.4'));
  782. }
  783. if (!track.tracknumber) {
  784. clipBoard.value = '';
  785. throw new HTML('all track numbers must be defined' + ruleLink('2.3.16.4'));
  786. }
  787. if (!track.title) {
  788. clipBoard.value = '';
  789. throw new HTML('all track titles must be defined' + ruleLink('2.3.16.4'));
  790. }
  791. if (track.duration != undefined && isUpload && (isNaN(track.duration) || track.duration <= 0)) {
  792. clipBoard.value = '';
  793. throw 'invalid track #' + track.tracknumber + ' length: ' + track.duration;
  794. }
  795. if (track.codec && !['FLAC', 'MP3', 'AAC', 'DTS', 'AC3'].includes(track.codec)) {
  796. clipBoard.value = '';
  797. throw 'disallowed codec present (' + track.codec + ')';
  798. }
  799. if (/\b(?:MQAEncode) v(\d+(?:\.\d+)*)\b/.test(track.vendor)) {
  800. clipBoard.value = '';
  801. throw 'MQA encoded release (' + RegExp.lastMatch + ')';
  802. }
  803. if (/^(\d+)\s*[\/]\s*(\d+)$/.test(track.tracknumber)) { // track/totaltracks
  804. addMessage('nonstandard track number formatting for track ' + RegExp.$1 + ': ' + track.tracknumber, 'warning');
  805. track.tracknumber = RegExp.$1;
  806. if (!track.totaltracks) track.totaltracks = parseInt(RegExp.$2);
  807. } else if (/^(\d+)[\.\-](\d+)$/.test(track.tracknumber)) { // discnumber.tracknumber
  808. addMessage('nonstandard track number formatting for track ' + RegExp.$2 + ': ' + track.tracknumber, 'warning');
  809. if (!track.discnumber) track.discnumber = parseInt(RegExp.$1);
  810. track.tracknumber = RegExp.$2;
  811. }
  812. if (track.discnumber) {
  813. if (/^(\d+)\s*\/\s*(\d+)/.test(track.discnumber)) {
  814. addMessage('nonstandard disc number formatting for track ' + track.tracknumber + ': ' + track.discnumber, 'warning');
  815. track.discnumber = RegExp.$1;
  816. if (!track.totaldiscs) track.totaldiscs = RegExp.$2;
  817. } else track.discnumber = parseInt(track.discnumber);
  818. if (isNaN(track.discnumber)) {
  819. addMessage('invalid disc numbering for track ' + track.tracknumber, 'warning');
  820. track.discnumber = undefined;
  821. }
  822. if (track.discnumber > release.totaldiscs) release.totaldiscs = track.discnumber;
  823. }
  824. totalTime += track.duration;
  825. albumBitrate += track.bitrate * track.duration;
  826. albumSize += track.filesize;
  827. });
  828. if (!tracks.every(it => it.discnumber > 0) && !tracks.every(it => !it.discnumber)) {
  829. clipBoard.value = '';
  830. throw 'inconsistent release (mix of tracks with and without disc number)';
  831. }
  832. if (release.totaldiscs > 1 && tracks.some(it => it.totaldiscs != release.totaldiscs))
  833. addMessage('at least one track not having properly set TOTALDISCS (' + release.totaldiscs + ')', 'info');
  834.  
  835. function setUniqueProperty(propName, propNameLiteral) {
  836. let homogeneous = new Set(tracks.map(it => it[propName]).filter(it => it != undefined && it != null));
  837. if (homogeneous.size > 1) {
  838. var diverses = '', it = homogeneous.values(), val;
  839. while (!(val = it.next()).done) diverses += '<br>\t' + val.value;
  840. clipBoard.value = '';
  841. throw new HTML('mixed releases not accepted (' + propNameLiteral + ') - supposedly user compilation' + diverses);
  842. }
  843. release[propName] = homogeneous.values().next().value;
  844. }
  845. setUniqueProperty('artist', 'album artist');
  846. setUniqueProperty('album', 'album title');
  847. setUniqueProperty('album_year', 'album year');
  848. setUniqueProperty('release_date', 'release date');
  849. setUniqueProperty('encoding', 'encoding');
  850. setUniqueProperty('codec', 'codec');
  851. setUniqueProperty('codec_profile', 'codec profile');
  852. setUniqueProperty('vendor', 'vendor');
  853. setUniqueProperty('media', 'media');
  854. setUniqueProperty('channels', 'channels');
  855. setUniqueProperty('label', 'label');
  856. setUniqueProperty('country', 'country');
  857.  
  858. tracks.forEach(function(iter) {
  859. setProperty('trackArtists', 'track_artist');
  860. setProperty('totalTracks', 'totaltracks');
  861. setProperty('discSubtitles', 'discsubtitle');
  862. setProperty('composers', 'composer');
  863. setProperty('catalogs', 'catalog');
  864. setProperty('bitrates', 'bitrate');
  865. setProperty('bds', 'bd');
  866. setProperty('rgs', 'rg');
  867. setProperty('drs', 'dr');
  868. if (iter.sr) if (typeof release.srs[iter.sr] != 'number') release.srs[iter.sr] = iter.duration;
  869. else release.srs[iter.sr] += iter.duration;
  870. setProperty('dirpaths', 'dirpath');
  871. setProperty('descriptions', 'description');
  872. setProperty('genres', 'genre');
  873. setProperty('urls', 'url');
  874. setProperty('coverUrls', 'cover_url');
  875.  
  876. function setProperty(propName, trackProp) {
  877. if (!Array.isArray(release[propName])) release[propName] = [];
  878. if (iter[trackProp] !== undefined && iter[trackProp] !== null && (typeof iter[trackProp] != 'string'
  879. || iter[trackProp].length > 0) && !release[propName].includes(iter[trackProp])) {
  880. release[propName].push(iter[trackProp]);
  881. }
  882. }
  883. });
  884. if (!release.totalTracks) addMessage('total tracks not set', 'warning');
  885. if (release.totalTracks.length > 0) {
  886. if (release.totalTracks.length > 1) {
  887. addMessage('total tracks not consistent across release: ' + release.totalTracks, 'warning');
  888. } else if (release.totalTracks[0] != tracks.length) {
  889. addMessage('total tracks not matching tracklist length: ' +
  890. release.totalTracks[0] + ' != ' + tracks.length, 'warning');
  891. }
  892. }
  893. tracks.forEach(function(track1, ndx1) {
  894. if (tracks.some((track2, ndx2) => ndx2 < ndx1 && track1.tracknumber == track2.tracknumber
  895. && track1.discnumber == track2.discnumber && track1.discsubtitle == track2.discsubtitle)) {
  896. addMessage('duplicate track ' + (track1.discnumber ? track1.discnumber + '-' : '') +
  897. (track1.discsubtitle ? track1.discsubtitle + '-' : '') + track1.tracknumber, 'warning');
  898. }
  899. });
  900. function validatorFunc(arr, validator, str) {
  901. if (arr.length <= 0 || !arr.some(validator)) return true;
  902. clipBoard.value = '';
  903. throw 'disallowed ' + str + ' present (' + arr.filter(validator) + ')';
  904. }
  905. validatorFunc(release.bds, bd => ![16, 24].includes(bd), 'bit depths');
  906. validatorFunc(Object.keys(release.srs),
  907. sr => sr < 44100 || sr > 192000 || sr % 44100 != 0 && sr % 48000 != 0, 'sample rates');
  908. var albumBPM = Math.round(tracks.reduce(function(acc, track) {
  909. return acc + parseInt(track.identifiers.BPM) * track.duration;
  910. }, 0) / totalTime);
  911. var composerEmphasis = false, isFromDSD = false, isClassical = false;
  912. var canSort = tracks.every((tr1, ndx1) => tracks.every((tr2, ndx2) => ndx1 == ndx2
  913. || tr1.tracknumber != tr2.tracknumber || tr1.discnumber != tr2.discnumber));
  914. var yadg_prefil = '', releaseType, editionTitle, iter, rx;
  915. var tags = new TagManager();
  916. albumBitrate /= totalTime;
  917. if (tracks.every(it => /^(?:Single)$/i.test(it.identifiers.RELEASETYPE))
  918. || tracks.length == 1 && totalTime > 0 && totalTime < prefs.single_threshold) {
  919. releaseType = getReleaseIndex('Single');
  920. } else if (tracks.every(it => it.identifiers.RELEASETYPE == 'EP')) {
  921. releaseType = getReleaseIndex('EP');
  922. } else if (tracks.every(it => /^soundtrack$/i.test(it.identifiers.RELEASETYPE))) {
  923. releaseType = getReleaseIndex('Soundtrack');
  924. tags.add('score');
  925. composerEmphasis = true;
  926. }
  927. if (release.genres.length > 0) {
  928. const classicalGenreParsers = [
  929. /\b(?:Classical|Classique|Klassik|Symphony|Symphonic(?:al)?|Operas?|Operettas?|Ballets?|(?:Violin|Cello|Piano)\s+Solos?|Chamber|Choral|Choirs?|Orchestral|Etudes?|Duets|Concertos?|Cantatas?|Requiems?|Passions?|Mass(?:es)?|Oratorios?|Poems?|Sacred|Secular|Vocal\s+Music)\b/i,
  930. ];
  931. release.genres.forEach(function(genre) {
  932. classicalGenreParsers.forEach(function(classicalGenreParser) {
  933. if (classicalGenreParser.test(genre) && !/\b(?:metal|rock|pop)\b/i.test(genre)) {
  934. composerEmphasis = true;
  935. isClassical = true
  936. }
  937. });
  938. if (/\b(?:Jazz|Vocal)\b/i.test(genre) && !/\b(?:Nu|Future|Acid)[\s\-\−\—\–]*Jazz\b/i.test(genre)
  939. && !/\bElectr(?:o|ic)[\s\-\−\—\–]?Swing\b/i.test(genre)) {
  940. composerEmphasis = true;
  941. }
  942. if (/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i.test(genre)) {
  943. if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
  944. composerEmphasis = true;
  945. }
  946. if (/\b(?:Christmas\s+Music)\b/i.test(genre)) {
  947. composerEmphasis = true;
  948. }
  949. tags.add(...genre.split(/\s*\|\s*/));
  950. });
  951. if (release.genres.length > 1) addMessage('inconsistent genre accross album: ' + release.genres, 'warning');
  952. }
  953. if (!onlineSource && isClassical && !tracks.every(track => track.composer)) {
  954. addMessage(new HTML('all tracks composers must be set for clasical music' + ruleLink('2.3.17')), 'warning');
  955. //return false;
  956. }
  957. // Processing artists: recognition, splitting and dividing to categores
  958. var ajaxRejects = 0;
  959. const ampersandParsers = [
  960. /\s+(?:meets|vs\.?|X)\s+/i,
  961. /\s*[;\/\|\×]\s*/,
  962. /\s+(?:[\&\+]|and)\s+(?!:his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
  963. /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
  964. ];
  965. const featParsers = [
  966. /\s+(?:meets)\s+(.*?)\s*$/i,
  967. /\s+(?:[Ww]ith)\s+(?!:his\b|her\b|Friends$|Strings$)(.*?)\s*$/,
  968. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:eaturing|t\.))\s+(.*?)\s*$/,
  969. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff]eat\.)\s+(.*?)\s*$/, // [0]
  970. /\s+\[\s*f(?:eat(?:\.|uring)|t\.)\s+([^\[\]]+?)\s*\]/i, // [1]
  971. /\s+\(\s*f(?:eat(?:\.|uring)|t\.)\s+([^\(\)]+?)\s*\)/i, // [2]
  972. /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i, // [3]
  973. /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i, // [4]
  974. /\s+\[\s*with\s+(?!:his\b|her\b|Friends$|Strings$)([^\[\]]+?)\s*\]/i, // [5]
  975. /\s+\(\s*with\s+(?!:his\b|her\b|Friends$|Strings$)([^\(\)]+?)\s*\)/i, // [6]
  976. ];
  977. const remixParsers = [
  978. /\s+\((?:The\s+)Remix(?:e[sd])?\)/i,
  979. /\s+\[(?:The\s+)Remix(?:e[sd])?\]/i,
  980. /\s+(?:The\s+)Remix(?:e[sd])?\s*$/i,
  981. /\s+\(([^\(\)]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\)/i,
  982. /\s+\[([^\[\]]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\]/i,
  983. /\s+\(\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
  984. /\s+\[\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
  985. ];
  986. const otherArtistsParsers = [
  987. [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
  988. [/^()(.*?)\s+\(conductor\)$/i, 4],
  989. //[/^()(.*?)\s+\(.*\)$/i, 1],
  990. ];
  991. const artistStrips = [
  992. /\s+(?:aka|AKA)\.?\s+(.*)$/,
  993. /\s+\(([^\(\)]+)\)$/,
  994. /\s+\[([^\[\]]+)\]$/,
  995. /\s+\{([^\{\}]+)\}$/,
  996. ];
  997. const roleCollisions = [
  998. [4, 5], // main
  999. [0, 4], // guest
  1000. [], // remixer
  1001. [], // composer
  1002. [], // conductor
  1003. [], // DJ/compiler
  1004. [], // producer
  1005. ];
  1006. isVA = vaParser.test(release.artist);
  1007. var artists = [];
  1008. for (i = 0; i < 7; ++i) artists[i] = [];
  1009.  
  1010. if (!isVA) {
  1011. addArtists(0, yadg_prefil = spliceGuests(release.artist));
  1012. if (ampersandParsers.some(rx => rx.test(yadg_prefil))) getSiteArtist(yadg_prefil); // priority cache record
  1013. }
  1014. var albumGuests = Array.from(artists[1]);
  1015.  
  1016. featParsers.slice(3).forEach(function(rx, ndx) {
  1017. matches = rx.exec(release.album);
  1018. if (matches != null && (ndx < 5 || splitArtists(matches[1]).every((artist, ndx) => looksLikeTrueName(artist, 1)))) {
  1019. addArtists(1, matches[1]);
  1020. addMessage('featured artist(s) in album title (' + release.album + ')', 'warning');
  1021. release.album = release.album.replace(rx, '');
  1022. }
  1023. });
  1024. remixParsers.slice(3).forEach(function(rx) {
  1025. if (rx.test(release.album)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1026. })
  1027. if (((matches = /^(.*?)\s+Presents\s+(.*)$/.exec(release.album)) != null
  1028. || isVA && (matches = (/\s+\(compiled\s+by\s+(.*?)\)\s*$/i.exec(release.album)
  1029. || /\s+compiled\s+by\s+(.*?)\s*$/i.exec(release.album))) != null) && looksLikeTrueName(matches[1])) {
  1030. addArtists(5, matches[1]);
  1031. if (!releaseType) releaseType = getReleaseIndex('Compilation');
  1032. }
  1033.  
  1034. for (iter of tracks) {
  1035. addTrackPerformers(iter.track_artist);
  1036. addTrackPerformers(iter.performer);
  1037. addArtists(2, iter.remixer);
  1038. addArtists(3, iter.composer);
  1039. addArtists(4, iter.conductor);
  1040. addArtists(5, iter.compiler);
  1041. addArtists(6, iter.producer);
  1042.  
  1043. if (iter.title) {
  1044. featParsers.slice(3).forEach(function(rx, ndx) {
  1045. matches = rx.exec(iter.title);
  1046. if (matches != null && (ndx < 5 || splitArtists(matches[1]).every((artist, ndx) => looksLikeTrueName(artist, 1)))) {
  1047. iter.track_artist = (!isVA && (!iter.track_artist || iter.track_artist.includes(matches[1])) ?
  1048. iter.artist : iter.track_artist) + ' feat. ' + matches[1];
  1049. addArtists(1, matches[1]);
  1050. addMessage('featured artist(s) in track title (#' + iter.tracknumber + ': ' + iter.title + ')', 'warning');
  1051. iter.title = iter.title.replace(rx, '');
  1052. }
  1053. });
  1054. if (!iter.remixer) remixParsers.slice(3).forEach(function(rx) {
  1055. if (rx.test(iter.title)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1056. });
  1057. }
  1058. if (isClassical && !iter.composer && /^([^\(\)\[\]\{\},:]+?)(?:\s*\(\d{4}\s*-\s*\d{4}\))/.test(iter.discsubtitle)) {
  1059. //track.composer = RegExp.$1;
  1060. addArtists(3, RegExp.$1);
  1061. }
  1062. }
  1063. for (i = 0; i < Math.round(tracks.length / 2); ++i) splitAmpersands();
  1064.  
  1065. function addArtists(ndx, str) {
  1066. if (str) splitArtists(str).forEach(function(artist) {
  1067. artist = ndx != 0 ? strip(artist) : guessOtherArtists(artist);
  1068. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1069. && !artists[ndx].includesCaseless(artist)
  1070. && !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist))) artists[ndx].push(artist);
  1071. });
  1072. }
  1073. function addTrackPerformers(str) {
  1074. if (str) splitArtists(spliceGuests(str, 1)).forEach(function(artist) {
  1075. artist = guessOtherArtists(artist);
  1076. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1077. && !artists[0].includesCaseless(artist)
  1078. && (isVA || !artists[1].includesCaseless(artist))) artists[isVA ? 0 : 1].push(artist);
  1079. });
  1080. }
  1081. function splitArtists(str) {
  1082. var result = [str];
  1083. multiArtistParsers.forEach(function(multiArtistParser) {
  1084. for (i = result.length; i > 0; --i) {
  1085. var j = result[i - 1].split(multiArtistParser);
  1086. if (j.length >= 2 && j.every(twoOrMore)
  1087. && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
  1088. && !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
  1089. }
  1090. });
  1091. return result;
  1092. }
  1093. function splitAmpersands(array) {
  1094. var i, j, ndx;
  1095. if (array) {
  1096. var result = [];
  1097. ampersandParsers.forEach(function(ampersandParser) {
  1098. for (i = array.length; i > 0; --i) {
  1099. if ((j = array[i - 1].split(ampersandParser)).length > 1
  1100. && (j.some(it1 => artists.some(it2 => it2.includesCaseless(it1))) || j.every(looksLikeTrueName))
  1101. && !getSiteArtist(array[i - 1])) add(...j); else add(array[i - 1]);
  1102. }
  1103. });
  1104. return result;
  1105.  
  1106. function add(...artists) {
  1107. artists.forEach(function(artist) {
  1108. if (!result.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist))) result.push(artist);
  1109. });
  1110. }
  1111. }
  1112. for (ndx = 0; ndx < artists.length; ++ndx) {
  1113. ampersandParsers.forEach(function(ampersandParser) {
  1114. for (i = artists[ndx].length; i > 0; --i) {
  1115. if ((j = artists[ndx][i - 1].split(ampersandParser)).length < 2
  1116. || !j.some(it1 => artists.some(it2 => it2.includesCaseless(it1))) && !j.every(looksLikeTrueName)
  1117. || getSiteArtist(artists[ndx][i - 1])) continue;
  1118. artists[ndx].splice(i - 1, 1, ...j.filter(function(artist) {
  1119. return !artists[ndx].includesCaseless(artist)
  1120. && !pseudoArtistParsers.some(rx => rx.test(artist))
  1121. && !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist));
  1122. }));
  1123. }
  1124. });
  1125. }
  1126. }
  1127. function spliceGuests(str, level = 1) {
  1128. (level > 0 ? featParsers.slice(level) : featParsers).forEach(function(it) {
  1129. if (it.test(str)) {
  1130. addArtists(1, RegExp.$1);
  1131. str = str.replace(it, '');
  1132. }
  1133. });
  1134. return str;
  1135. }
  1136. function guessOtherArtists(name) {
  1137. otherArtistsParsers.forEach(function(it) {
  1138. if (!it[0].test(name)) return;
  1139. addArtists(it[1], RegExp.$2);
  1140. name = RegExp.$1;
  1141. });
  1142. return strip(name);
  1143. }
  1144. function getSiteArtist(artist) {
  1145. //if (isOPS) return undefined;
  1146. if (!artist || notSiteArtistsCache.includesCaseless(artist)) return null;
  1147. var key = Object.keys(siteArtistsCache).find(it => it.toLowerCase() == artist.toLowerCase());
  1148. if (key) return siteArtistsCache[key];
  1149. var now = new Date().getTime();
  1150. if (!gazelleApiTimeFrame.timeStamp || now > gazelleApiTimeFrame.timeStamp + 10100) {
  1151. gazelleApiTimeFrame.timeStamp = now;
  1152. gazelleApiTimeFrame.requestCounter = 0;
  1153. };
  1154. if (++gazelleApiTimeFrame.requestCounter > 5) {
  1155. console.debug('getSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
  1156. artist + '" (' + gazelleApiTimeFrame.requestCounter + ')');
  1157. if (prefs.messages_verbosity >= 2) addMessage('AJAX API request exceeding time frame: artistname="' +
  1158. artist + '" (' + gazelleApiTimeFrame.requestCounter + ')', 'notice');
  1159. ++ajaxRejects;
  1160. return undefined;
  1161. }
  1162. xhr.open('GET', document.location.origin + '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist), false);
  1163. xhr.send();
  1164. if (xhr.readyState != XMLHttpRequest.DONE || xhr.status != 200) {
  1165. console.log('getSiteArtist("' + artist + '"): XMLHttpRequest readyState:' + xhr.readyState + ' status:' + xhr.status);
  1166. return undefined; // error
  1167. }
  1168. try {
  1169. let response = JSON.parse(xhr.responseText);
  1170. if (response.status != 'success') {
  1171. notSiteArtistsCache.pushUniqueCaseless(artist);
  1172. return null;
  1173. }
  1174. return (siteArtistsCache[artist] = response.response);
  1175. } catch(e) {
  1176. console.warn('UA::getSiteArtist(): ' + e);
  1177. return undefined;
  1178. }
  1179. }
  1180. function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
  1181. function looksLikeTrueName(artist, index = 0) {
  1182. return twoOrMore(artist)
  1183. && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
  1184. && artist.split(/\s+/).length >= 2
  1185. && !pseudoArtistParsers.some(rx => rx.test(artist)) || getSiteArtist(artist);
  1186. }
  1187. function strip(art) {
  1188. return artistStrips.reduce(function(acc, rx, ndx) {
  1189. return ndx != 1 || rx.test(acc) && !notMonospaced(RegExp.$1) ? acc.replace(rx, '') : acc;
  1190. }, art);
  1191. }
  1192. function getRealTrackArtist(track) {
  1193. if (typeof track != 'object') return null;
  1194. if (track.track_artist == release.artist) return undefined;
  1195. var trackArtist = track.track_artist;
  1196. if (trackArtist && !isVA) {
  1197. let trackArtists = [], trackGuests = [], ta = trackArtist;
  1198. featParsers.slice(1).forEach(function(rx, ndx) {
  1199. if (!rx.test(ta)) return;
  1200. trackGuests.pushUniqueCaseless(RegExp.$1);
  1201. ta = ta.replace(rx, '');
  1202. });
  1203. splitArtists(ta).forEach(function(artist) {
  1204. otherArtistsParsers.forEach(it => { if (it[0].test(artist)) artist = RegExp.$1 });
  1205. artist = strip(artist);
  1206. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))) trackArtists.pushUniqueCaseless(artist);
  1207. });
  1208. if (splitAmpersands(trackArtists).equalTo(artists[0])
  1209. && splitAmpersands(trackGuests).equalTo(albumGuests)) trackArtist = undefined;
  1210. }
  1211. return trackArtist;
  1212. }
  1213.  
  1214. if (elementWritable(document.getElementById('artist'))) {
  1215. let artistIndex = 0;
  1216. catLoop: for (i = 0; i < 7; ++i) for (iter of artists[i]
  1217. .filter(artist => !roleCollisions[i].some(n => artists[n].includesCaseless(artist)))
  1218. .sort((a, b) => a.localeCompare(b))) {
  1219. if (isUpload) {
  1220. var id = 'artist';
  1221. if (artistIndex > 0) id += '_' + artistIndex;
  1222. while ((ref = document.getElementById(id)) == null) AddArtistField();
  1223. } else {
  1224. while ((ref = document.querySelectorAll('input[name="artists[]"]')).length <= artistIndex) AddArtistField();
  1225. ref = ref[artistIndex];
  1226. }
  1227. if (ref == null) throw new Error('Failed to allocate artist fields');
  1228. ref.value = iter;
  1229. ref.nextElementSibling.value = i + 1;
  1230. if (++artistIndex >= 200) break catLoop;
  1231. }
  1232. if (overwrite && artistIndex > 0) while (document.getElementById('artist_' + artistIndex) != null) {
  1233. RemoveArtistField();
  1234. }
  1235. }
  1236.  
  1237. // Processing album title
  1238. const editionParsers = [
  1239. /\s+\(((?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissu(?:ed)?|Deluxe|Enhanced|Expanded|Limited|Version|\d+th\s+Anniversary)\b[^\(\)]*|[^\(\)]*\b(?:Edition|Version|Promo|Release|Édition|Reissue))\)$/i,
  1240. /\s+\[((?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissu(?:ed)?|Deluxe|Enhanced|Expanded|Limited|Version|\d+th\s+Anniversary)\b[^\[\]]*|[^\[\]]*\b(?:Edition|Version|Promo|Release|Édition|Reissue))\]$/i,
  1241. /\s+-\s+([^\[\]\(\)\-\−\—\–]*\b(?:(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Bonus\s+Track)\b[^\[\]\(\)\-\−\—\–]*|Reissue|Edition|Version|Promo|Enhanced|Release|Édition))$/i,
  1242. ];
  1243. const mediaParsers = [
  1244. [/\s+(?:\[(?:LP|Vinyl|12"|7")\]|\((?:LP|Vinyl|12"|7")\))$/, 'Vinyl'],
  1245. [/\s+(?:\[SA-?CD\]|\(SA-?CD\))$/, 'SACD'],
  1246. [/\s+(?:\[(?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\]|\((?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\))$/, 'Blu-Ray'],
  1247. [/\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/, 'DVD'],
  1248. ];
  1249. const releaseTypeParsers = [
  1250. [/\s+(?:-\s+Single|\[Single\]|\(Single\))$/i, 'Single', true, true],
  1251. [/\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/, 'EP', true, true],
  1252. [/\s+\((?:Live|En\s+directo?|Ao\s+Vivo)\b[^\(\)]*\)$/i, 'Live album', false, false],
  1253. [/\s+\[(?:Live|En\s+directo?|Ao\s+Vivo)\b[^\[\]]*\]$/i, 'Live album', false, false],
  1254. [/(?:^Live\s+(?:[aA]t|[Ii]n)\b|^Directo?\s+[Ee]n\b|\bUnplugged\b|\bAcoustic\s+Stage\b|\s+Live$)/, 'Live album', false, false],
  1255. [/\b(?:(?:Best\s+of|Greatest\s+Hits|Complete\s+(.+?\s+)(?:Albums|Recordings))\b|Collection$)|^The(\s+\w+)+Years$/i, 'Anthology', false, false],
  1256. ];
  1257. var album = release.album;
  1258. releaseTypeParsers.forEach(function(it) {
  1259. if (it[0].test(album)) {
  1260. if (it[2] || !releaseType) releaseType = getReleaseIndex(it[1]);
  1261. if (it[3]) album = album.replace(it[0], '');
  1262. }
  1263. });
  1264. rx = '\\b(?:Soundtrack|Score|Motion\\s+Picture|Series|Television|Original(?:\\s+\\w+)?\\s+Cast|Music\\s+from|(?:Musique|Bande)\\s+originale)\\b';
  1265. if (reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
  1266. if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
  1267. tags.add('score');
  1268. composerEmphasis = true;
  1269. }
  1270. remixParsers.forEach(function(rx) {
  1271. if (rx.test(album) && !releaseType) releaseType = getReleaseIndex('Remix');
  1272. });
  1273. editionParsers.forEach(function(rx) {
  1274. if (rx.test(album) && (!RegExp.$1.toLowerCase().startsWith('remaster') || !release.album_year
  1275. || release.album_year != extractYear(release.release_date))) {
  1276. album = album.replace(rx, '');
  1277. editionTitle = RegExp.$1;
  1278. }
  1279. });
  1280. mediaParsers.forEach(function(it) {
  1281. if (it[0].test(album)) {
  1282. album = album.replace(it[0], '');
  1283. media = it[1];
  1284. }
  1285. });
  1286. if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  1287. ref.value = album;
  1288. }
  1289.  
  1290. if (yadg_prefil) yadg_prefil += ' ';
  1291. yadg_prefil += album;
  1292. if (elementWritable(ref = document.getElementById('yadg_input'))) {
  1293. ref.value = yadg_prefil || '';
  1294. if (yadg_prefil && (ref = document.getElementById('yadg_submit')) != null && !ref.disabled) ref.click();
  1295. }
  1296.  
  1297. if (!release.album_year) release.album_year = parseInt(getHomoIdentifier('PUBYEAR')) || undefined;
  1298. if (elementWritable(ref = document.getElementById('year'))) {
  1299. ref.value = release.album_year || '';
  1300. }
  1301. i = release.release_date && extractYear(release.release_date);
  1302. if (elementWritable(ref = document.getElementById('remaster_year'))
  1303. || !isUpload && i > 0 && (ref = document.querySelector('input[name="year"]')) != null && !ref.disabled) {
  1304. ref.value = i || '';
  1305. }
  1306. //if (tracks.every(it => it.identifiers.EXPLICIT == '0')) editionTitle = 'Clean' + (editionTitle ? ' / ' + editionTitle : '');
  1307. [/\s+\(([^\(\)]+)\)\s*$/, /\s+\[([^\[\]]+)\]\s*$/, /\s+\{([^\{\}]+)\}\s*$/].forEach(function(rx) {
  1308. var version = tracks.map(track => rx.test(track.title) ? RegExp.$1 : null);
  1309. version = version.homogeneous() && version[0] || undefined;
  1310. if (!editionTitle && /\b(?:Remastered|Remasterisée|Remasterizado|Acoustic|Instrumental)\b/i.test(version)) {
  1311. editionTitle = version;
  1312. }
  1313. if (!releaseType && /\b(?:Live)\b/i.test(version)) releaseType = getReleaseIndex('Live album');
  1314. });
  1315. if (elementWritable(ref = document.getElementById('remaster_title'))) ref.value = editionTitle || '';
  1316. if (elementWritable(ref = document.getElementById('remaster_record_label')
  1317. || document.querySelector('input[name="recordlabel"]'))) {
  1318. ref.value = release.label ? prefs.selfrelease_label && !isVA && release.label == release.artist
  1319. || /^(?:independent|vlastní\s+náklad|Self[\s\-]Released)$/i.test(release.label)
  1320. || /^iMD-/.test(release.label) ? prefs.selfrelease_label : release.label.split(/\s*;\s*/g).join(' / ') : '';
  1321. }
  1322. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  1323. || document.querySelector('input[name="cataloguenumber"]'))) {
  1324. ref.value = release.catalogs.length >= 1
  1325. && release.catalogs.map(it => it.replace(/\s*;\s*/g, ' / ')).join(' / ') || barcode() || '';
  1326. }
  1327. var scene = getHomoIdentifier('SCENE');
  1328. if (isUpload && scene != undefined && (ref = document.getElementById('scene')) != null && !ref.disabled) try {
  1329. ref.checked = eval(scene.toLowerCase());
  1330. } catch(e) { console.warn('Invalid SCENE value (' + scene + ')') }
  1331. var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
  1332. if (elementWritable(ref = document.getElementById('format'))) {
  1333. ref.value = release.codec || (isRED ? '' : '---');
  1334. ref.onchange(); //exec(function() { Format() });
  1335. }
  1336. if (isRequestNew) {
  1337. if (prefs.always_request_perfect_flac) reqSelectFormats('FLAC');
  1338. else if (release.codec) reqSelectFormats(release.codec);
  1339. }
  1340. var sel;
  1341. if (release.encoding == 'lossless') {
  1342. sel = tracks.some(track => track.bd == 24) ? '24bit Lossless' : 'Lossless';
  1343. } else if (release.bitrates.length >= 1) {
  1344. let lame_version = release.codec == 'MP3' && /^LAME(\d+)\.(\d+)/i.test(release.vendor) ?
  1345. parseInt(RegExp.$1) * 1000 + parseInt(RegExp.$2) : undefined;
  1346. if (release.codec == 'MP3' && release.codec_profile == 'VBR V0') {
  1347. sel = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
  1348. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V1') {
  1349. sel = 'V1 (VBR)'
  1350. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V2') {
  1351. sel = lame_version >= 3094 ? sel = 'V2 (VBR)' : 'APS (VBR)'
  1352. } else if (release.bitrates.length == 1 && [192, 256, 320].includes(Math.round(release.bitrates[0]))) {
  1353. sel = Math.round(release.bitrates[0]);
  1354. } else {
  1355. sel = 'Other';
  1356. }
  1357. }
  1358. if ((ref = document.getElementById('bitrate')) != null && !ref.disabled && (overwrite || !br_isSet)) {
  1359. ref.value = sel || '';
  1360. ref.onchange(); //exec(function() { Bitrate() });
  1361. if (sel == 'Other' && (ref = document.getElementById('other_bitrate')) != null) {
  1362. ref.value = Math.round(release.bitrates.length == 1 ? release.bitrates[0] : albumBitrate);
  1363. if ((ref = document.getElementById('vbr')) != null) ref.checked = release.bitrates.length > 1;
  1364. }
  1365. }
  1366. if (isRequestNew) {
  1367. if (prefs.always_request_perfect_flac) {
  1368. reqSelectBitrates('Lossless', '24bit Lossless');
  1369. } else if (sel) reqSelectBitrates(sel);
  1370. }
  1371. if (release.media) {
  1372. sel = undefined;
  1373. [
  1374. [/\b(?:WEB|File|Download|digital\s+media)\b/i, 'WEB'],
  1375. [/\bCD\b/, 'CD'],
  1376. [/\b(?:SA-?CD|[Hh]ybrid)\b/, 'SACD'],
  1377. [/\b(?:[Bb]lu[\-\−\—\–\s]?[Rr]ay|BRD?|BD)\b/, 'Blu-Ray'],
  1378. [/\bDVD(?:-?A)?\b/, 'DVD'],
  1379. [/\b(?:[Vv]inyl\b|LP\b|12"|7")/, 'Vinyl'],
  1380. ].forEach(k => { if (k[0].test(release.media)) sel = k[1] });
  1381. media = sel || media;
  1382. }
  1383. if (!media) {
  1384. if (tracks.every(isRedBook)) {
  1385. addMessage('media not determined - CD estimated', 'info');
  1386. media = 'CD';
  1387. } else if (tracks.some(t => t.bd > 16 || (t.sr > 0 && t.sr != 44100) || t.samples > 0 && t.samples % 588 != 0)) {
  1388. addMessage('media not determined - NOT CD', 'info');
  1389. }
  1390. } else if (media != 'CD' && tracks.every(isRedBook)) {
  1391. addMessage('CD as source media is estimated (' + media + ')', 'info');
  1392. }
  1393. if (elementWritable(ref = document.getElementById('media'))) {
  1394. ref.value = media || !tracks.some(notRedBook) && prefs.default_medium || (isRED ? '' : '---');
  1395. }
  1396. if (isRequestNew) {
  1397. if (prefs.always_request_perfect_flac) reqSelectMedias('WEB', 'CD', 'Blu-Ray', 'DVD', 'SACD')
  1398. else if (media) reqSelectMedias(media);
  1399. }
  1400. function isRedBook(track) {
  1401. return track.bd == 16 && track.sr == 44100 && track.channels == 2 && track.samples > 0 && track.samples % 588 == 0;
  1402. }
  1403. function notRedBook(track) {
  1404. return track.bd && track.bd != 16 || track.sr && track.sr != 44100
  1405. || track.channels && track.channels != 2 || track.samples && track.samples % 588 != 0;
  1406. }
  1407. if (tracks.every(it => it.identifiers.ORIGINALFORMAT && it.identifiers.ORIGINALFORMAT.includes('DSD'))) {
  1408. isFromDSD = true;
  1409. }
  1410. // Release type
  1411. if (!releaseType) {
  1412. if (/\b(?:Mixtape)\b/i.test(release.album)) releaseType = getReleaseIndex('Mixtape');
  1413. else if (isVA) releaseType = getReleaseIndex('Compilation');
  1414. else if (tracks.every(it => it.identifiers.COMPILATION == 1)) releaseType = getReleaseIndex('Anthology');
  1415. }
  1416. if ((!releaseType || releaseType == 5) && totalTime <= prefs.EP_threshold && tracks.every(function(track) {
  1417. const rxs = [/\s+\([^\(\)]+\)\s*$/, /\s+\[[^\[\]]+\]\s*$/];
  1418. return rxs.reduce((acc, rx) => acc.replace(rx, ''), track.title)
  1419. == rxs.reduce((acc, rx) => acc.replace(rx, ''), tracks[0].title);
  1420. })) {
  1421. releaseType = getReleaseIndex('Single');
  1422. }
  1423. if (!releaseType) if (totalTime > 0 && totalTime < prefs.single_threshold) {
  1424. releaseType = getReleaseIndex('Single');
  1425. } else if (totalTime > 0 && totalTime < prefs.EP_threshold) {
  1426. releaseType = getReleaseIndex('EP');
  1427. }
  1428. if ((ref = document.getElementById('releasetype')) != null && !ref.disabled
  1429. && (overwrite || ref.value == 0 || ref.value == '---')) ref.value = releaseType || getReleaseIndex('Album');
  1430. // Tags
  1431. if (prefs.estimate_decade_tag && (isNaN(totalTime) || totalTime < 2 * 60 * 60)
  1432. && release.album_year > 1900 && [1, 3, 5, 9, 13, undefined].includes(releaseType)
  1433. /*&& !/\b(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissue|Anniversary|Collector(?:'?s)?)\b/i.test(editionTitle)*/)
  1434. tags.add(Math.floor(release.album_year/10) * 10 + 's'); // experimental
  1435. if (release.country) {
  1436. if (!excludedCountries.some(it => it.test(release.country))) tags.add(release.country);
  1437. }
  1438. if (elementWritable(ref = document.getElementById('tags'))) {
  1439. ref.value = tags.toString();
  1440. if (artists[0].length == 1 && prefs.fetch_tags_from_artist > 0) setTimeout(function() {
  1441. var artist = getSiteArtist(artists[0][0]);
  1442. if (!artist) return;
  1443. tags.add(...artist.tags.sort((a, b) => b.count - a.count).map(it => it.name)
  1444. .slice(0, prefs.fetch_tags_from_artist));
  1445. var ref = document.getElementById('tags');
  1446. ref.value = tags.toString();
  1447. }, 3000);
  1448. }
  1449. if (!composerEmphasis && !prefs.keep_meaningles_composers) {
  1450. document.querySelectorAll('input[name="artists[]"]').forEach(function(i) {
  1451. if (['4', '5'].includes(i.nextElementSibling.value)) i.value = '';
  1452. });
  1453. }
  1454.  
  1455. const doubleParsParsers = [
  1456. /\(+(\([^\(\)]*\))\)+/,
  1457. /\[+(\[[^\[\]]*\])\]+/,
  1458. /\{+(\{[^\{\}]*\})\}+/,
  1459. ];
  1460. tracks.forEach(function(track) {
  1461. doubleParsParsers.forEach(function(rx) {
  1462. if (!rx.test(track.title)) return;
  1463. addMessage('doubled parentheses in track #' + track.tracknumber + ' title ("' + track.title + '")', 'warning');
  1464. //track.title.replace(rx, RegExp.$1);
  1465. });
  1466. });
  1467. if (tracks.length > 1 && tracks.map(track => track.title).homogeneous()) {
  1468. addMessage('all tracks having same title: ' + tracks[0].title, 'warning');
  1469. }
  1470. if (isUpload && !isOPS) findPreviousUploads();
  1471. // Album description
  1472. sourceUrl = getStoreUrl();
  1473. const vinylTest = /^((?:Vinyl|LP) rip by\s+)(.*)$/im;
  1474. const vinyltrackParser = /^([A-Z])[\-\.\s]?((\d+)(?:\.\d+)?)$/;
  1475. const classicalWorkParsers = [
  1476. /^(.*\S):\s+(.*)$/,
  1477. /^(.+?):\s+([IVXC]+\.\s+.*)$/,
  1478. ];
  1479. var description;
  1480. if (isRequestNew || isRequestEdit) { // request
  1481. description = [];
  1482. if (release.release_date) {
  1483. i = new Date(release.release_date);
  1484. let today = new Date(new Date().toDateString());
  1485. description.push((isNaN(i) || i < today ? 'Released' : 'Releasing') + ' ' +
  1486. (isNaN(i) ? release.release_date : i.toDateString()));
  1487. if ((ref = document.getElementById('tags')) != null && !ref.disabled) {
  1488. let tags = new TagManager(ref.value);
  1489. if (prefs.upcoming_tags && i >= today) tags.add(prefs.upcoming_tags);
  1490. ref.value = tags.toString();
  1491. }
  1492. }
  1493. if (!prefs.include_tracklist_in_request) {
  1494. let summary = '';
  1495. if (release.totaldiscs > 1) summary += release.totaldiscs + ' discs, ';
  1496. summary += tracks.length + ' track'; if (tracks.length > 1) summary += 's';
  1497. if (totalTime > 0) summary += ', ' + makeTimeString(totalTime);
  1498. description.push(summary);
  1499. }
  1500. if (sourceUrl || release.urls.length > 0) description.push(getUrls());
  1501. if (release.catalogs.length == 1 && /^\d{10,}$/.test(release.catalogs[0]) || /^\d{10,}$/.test(barcode())) {
  1502. description.push('[url=https://www.google.com/search?q=' + RegExp.lastMatch + ']Find more stores...[/url]');
  1503. }
  1504. if (prefs.include_tracklist_in_request) description.push(genPlaylist());
  1505. if (release.descriptions.length > 0) Array.prototype.push.apply(description, release.descriptions);
  1506. description = genAlbumHeader().concat(description.join('\n\n'));
  1507. if (description.length > 0) {
  1508. ref = document.getElementById('description');
  1509. if (elementWritable(ref)) {
  1510. ref.value = description;
  1511. } else if (isRequestEdit && ref != null && !ref.disabled) {
  1512. ref.value = ref.value.length > 0 ? ref.value.concat('\n\n', description) : ref.value = description;
  1513. preview(0);
  1514. }
  1515. }
  1516. } else { // upload
  1517. description = '';
  1518. if (prefs.bpm_summary && albumBPM > 0) {
  1519. if (description.length <= 0) description = '\n';
  1520. description += '\nAverage album BPM: [code]' + albumBPM + '[/code]';
  1521. }
  1522. /*if (release.release_date) {
  1523. let rd = new Date(release.release_date);
  1524. if (!isNaN(rd)) description = '\n\nRelease date: ' + rd.toDateString();
  1525. }*/
  1526. let vinylRipInfo;
  1527. if (release.descriptions.length > 0) {
  1528. description += '\n\n';
  1529. if (release.descriptions.length == 1 && release.descriptions[0]
  1530. && (matches = vinylTest.exec(release.descriptions[0])) != null) {
  1531. vinylRipInfo = release.descriptions[0].slice(matches.index).trim().split(/(?:[ \t]*\r?\n)+/);
  1532. description += release.descriptions[0].slice(0, matches.index).trim();
  1533. } else description += release.descriptions.join('\n\n');
  1534. }
  1535. if (elementWritable(ref = document.getElementById('album_desc'))) {
  1536. ref.value = genPlaylist().concat(description);
  1537. preview(0);
  1538. }
  1539. if ((ref = document.getElementById('body') || document.querySelector('textarea[name="body"]')) != null
  1540. && !ref.disabled) {
  1541. if (ref.value.length == 0) ref.value = genPlaylist().concat(description); else {
  1542. let editioninfo = '';
  1543. if (editionTitle) {
  1544. editioninfo = '[size=5][b]' + editionTitle;
  1545. if (release.release_date && (i = extractYear(release.release_date)) > 0) editioninfo += ' (' + i + ')';
  1546. editioninfo += '[/b][/size]\n\n';
  1547. }
  1548. ref.value = ref.value.concat('\n\n', editioninfo, genPlaylist(false, false), description);
  1549. }
  1550. preview(0);
  1551. }
  1552. // Release description
  1553. if (elementWritable(ref = document.getElementById('release_samplerate'))) {
  1554. ref.value = Object.keys(release.srs).length == 1 ? Math.floor(Object.keys(release.srs)[0] / 1000) :
  1555. Object.keys(release.srs).length > 1 ? '999' : '';
  1556. }
  1557. let lineage = '', rlsDesc = '';
  1558. let drInfo = '[hide=DR' + (release.drs.length == 1 ? release.drs[0] : '') + '][pre][/pre]';
  1559. let hasSR = Object.keys(release.srs).length > 0;
  1560. let srInfo = hasSR ? Object.keys(release.srs).sort((a, b) => release.srs[b] - release.srs[a])
  1561. .map(f => f / 1000).join('/').concat('kHz') : null;
  1562. if (tracks.some(track => track.bd > 16)) {
  1563. if (['Blu-Ray', 'DVD', 'SACD'].includes(media)) {
  1564. if (!isNWCD) rlsDesc = srInfo;
  1565. addChannelInfo();
  1566. if (media == 'SACD' || isFromDSD) addDSDInfo();
  1567. if (prefs.cleanup_descriptions) addDRInfo();
  1568. //addRGInfo();
  1569. addHybridInfo();
  1570. drInfo += '[/hide]';
  1571. } else if (media == 'Vinyl') {
  1572. let hassr = hasSR && (!isNWCD || Object.keys(release.srs).length > 1);
  1573. if (hassr) lineage = srInfo + ' ';
  1574. if (vinylRipInfo) {
  1575. vinylRipInfo[0] = vinylRipInfo[0].replace(vinylTest, '$1[color=blue]$2[/color]');
  1576. if (hassr) vinylRipInfo[0] = vinylRipInfo[0].replace(/^Vinyl\b/, 'vinyl');
  1577. lineage += vinylRipInfo[0] + '\n\n[u]Lineage:[/u]' + vinylRipInfo.slice(1).map(l => '\n'.concat([
  1578. // RuTracker translation
  1579. ['Код класса состояния винила', 'Vinyl condition class'],
  1580. ['Устройство воспроизведения', 'Turntable'],
  1581. ['Головка звукоснимателя', 'Cartridge'],
  1582. ['Картридж', 'Cartridge'],
  1583. ['Предварительный усилитель', 'Preamplifier'],
  1584. ['АЦП', 'ADC'],
  1585. ['Программа-оцифровщик', 'Software'],
  1586. ['Обработка', 'Post-processing'],
  1587. ].reduce((acc, it) => acc.replace(it[0], it[1]), l))).join('');
  1588. } else lineage += (hassr ? 'Vinyl' : ' vinyl') + ' rip by [color=blue][/color]\n\n[u]Lineage:[/u]\n';
  1589. let imgs = '\n[img][/img]'.repeat(6);
  1590. if (!isNWCD) drInfo += '\n'.concat(imgs); else lineage += '\n\n[hide]'.concat(imgs.slice(1), '[/hide]');
  1591. drInfo += '[/hide]';
  1592. } else { // WEB Hi-Res
  1593. if (!isNWCD || Object.keys(release.srs).length > 1) rlsDesc = srInfo;
  1594. if (release.channels && release.channels != 2) addChannelInfo();
  1595. if (isFromDSD) addDSDInfo();
  1596. if (!isFromDSD || prefs.cleanup_descriptions) addDRInfo();
  1597. //addRGInfo();
  1598. addHybridInfo();
  1599. if (isFromDSD || prefs.cleanup_descriptions || Object.keys(release.srs).length == 1
  1600. && Object.keys(release.srs)[0] == 88200) drInfo += '[/hide]'; else drInfo = null;
  1601. }
  1602. } else { // 16bit or lossy
  1603. if (Object.keys(release.srs).some(f => f != 44100)) rlsDesc = srInfo;
  1604. if (release.channels && release.channels != 2) addChannelInfo();
  1605. addDRInfo();
  1606. //addRGInfo();
  1607. if (prefs.cleanup_descriptions) drInfo += '[/hide]'; else drInfo = null;
  1608. if (release.codec == 'MP3' && release.vendor) {
  1609. // TODO: parse mp3 vendor string
  1610. } else if (['AAC', 'Opus', 'Vorbis'].includes(release.codec) && release.vendor) {
  1611. let _encoder_settings = release.vendor;
  1612. if (release.codec == 'AAC' && /^qaac\s+[\d\.]+/i.test(release.vendor)) {
  1613. let enc = [];
  1614. if (matches = release.vendor.match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
  1615. if (matches = release.vendor.match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
  1616. if (matches = release.vendor.match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
  1617. if (matches = release.vendor.match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
  1618. if (matches = release.vendor.match(/\bQuality\s+(\d+)\b/i)) enc[5] = matches[1];
  1619. _encoder_settings = 'Converted by Apple\'s ' + enc[2] + ' encoder (' + enc[3] + '-' + enc[4] + ')';
  1620. }
  1621. lineage = _encoder_settings;
  1622. }
  1623. }
  1624. function addDSDInfo() {
  1625. var nfo = ' DSD64';
  1626. if (prefs.sacd_decoder) nfo += ' using ' + prefs.sacd_decoder;
  1627. nfo += '\nOutput gain: [code]+0dB[/code]';
  1628. if (isNWCD) lineage = 'From' .concat(nfo); else {
  1629. if (rlsDesc.length > 0) rlsDesc += ' from'; else rlsDesc = 'From';
  1630. rlsDesc += nfo;
  1631. }
  1632. }
  1633. function addDRInfo() {
  1634. if (release.drs.length < 1 || document.getElementById('release_dynamicrange') != null) return;
  1635. var nfo = 'DR' + release.drs[0];
  1636. if (release.drs[0] < 4) nfo = '[color=red]'.concat(nfo, '[/color]');
  1637. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1638. rlsDesc += nfo;
  1639. }
  1640. function addRGInfo() {
  1641. if (release.rgs.length != 1) return;
  1642. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1643. rlsDesc += 'RG'; //rlsDesc += 'RG ' + rgs[0];
  1644. }
  1645. function addChannelInfo() {
  1646. if (!release.channels) return;
  1647. var chi = getChanString(release.channels);
  1648. if (chi.length <= 0) return;
  1649. if (rlsDesc.length > 0) rlsDesc += ', '; else rlsDesc = 'Channels configuration: ';
  1650. rlsDesc += chi;
  1651. }
  1652. function addHybridInfo() {
  1653. if (release.bds.length > 1) release.bds.filter(bd => bd != 24).forEach(function(bd) {
  1654. var hybrid_tracks = tracks.filter(it => it.bd == bd).sort(trackComparer).map(function(it) {
  1655. return (release.totaldiscs > 1 && it.discnumber ? it.discnumber + '-' : '').concat(it.tracknumber);
  1656. });
  1657. if (hybrid_tracks.length < 1) return;
  1658. if (rlsDesc.length > 0) rlsDesc += '\n';
  1659. rlsDesc += 'Note: track';
  1660. if (hybrid_tracks.length > 1) rlsDesc += 's';
  1661. rlsDesc += ' #' + hybrid_tracks.join(', ') +
  1662. (hybrid_tracks.length > 1 ? ' are' : ' is') + ' ' + bd + 'bit lossless';
  1663. });
  1664. }
  1665. rlsDesc = rlsDesc.length > 0 ? [rlsDesc] : [];
  1666. if ((ref = document.getElementById('release_lineage')) != null) {
  1667. lineage = lineage ? [lineage] : [];
  1668. if (drInfo) rlsDesc.push(drInfo);
  1669. if (sourceUrl || release.urls.length > 0) lineage.push(getUrls());
  1670. if (elementWritable(ref)) {
  1671. ref.value = lineage.join('\n\n');
  1672. preview(1);
  1673. }
  1674. } else {
  1675. if (lineage.length > 0) rlsDesc.push(lineage);
  1676. if (drInfo) rlsDesc.push(drInfo);
  1677. if (sourceUrl || release.urls.length > 0) rlsDesc.push(getUrls());
  1678. }
  1679. if (elementWritable(ref = document.getElementById('release_desc'))) {
  1680. ref.value = rlsDesc.join('\n\n');
  1681. if (rlsDesc.length > 0) preview(isNWCD ? 2 : 1);
  1682. }
  1683. if (release.encoding == 'lossless' && release.codec == 'FLAC'
  1684. && tracks.some(track => track.bd == 24) && release.dirpaths.length == 1) {
  1685. if ((ref = document.getElementById('release_desc')) != null) GM_xmlhttpRequest({
  1686. method: 'GET',
  1687. url: new URL('file:'.concat(release.dirpaths[0], '\\foo_dr.txt')).href,
  1688. responseType: 'blob',
  1689. onload: function(response) {
  1690. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) return defaultErrorHandler(response);
  1691. if (!/(\[hide=DR\d*\]\[pre\])\[\/pre\]/im.test(ref.value)) return;
  1692. var ndx = RegExp.lastIndex + RegExp.$1.length;
  1693. ref.value = ref.value.slice(0, ndx).concat(response.responseText, ref.value.slice(ndx));
  1694. },
  1695. onerror: error => { console.error('foo_dr.txt not exists or is forbidden to read') },
  1696. onabort: defaultAbortHandler,
  1697. ontimeout: defaultTimeoutHandler,
  1698. });
  1699. }
  1700. }
  1701. if (ajaxRejects > 0) {
  1702. i = 'AJAX request(s) eliminated due to Gazelle policy. ' +
  1703. 'Multiple artists not split correctly? Relaunch parsing in overwrite mode without page reload'
  1704. let delay = gazelleApiTimeFrame.timeStamp + 10100 - new Date().getTime();
  1705. if (delay >= 0) {
  1706. i += ' after ' + Math.ceil(delay / 1000) + 's';
  1707. setTimeout(() => { addMessage('new AJAX timeframe for requery available', 'info') }, delay);
  1708. }
  1709. addMessage(i + '.', 'info');
  1710. }
  1711. if (elementWritable(ref = document.getElementById('release_dynamicrange'))) {
  1712. ref.value = release.drs.length == 1 ? release.drs[0] : '';
  1713. }
  1714. if (isRequestNew && prefs.request_default_bounty > 0) {
  1715. let amount = prefs.request_default_bounty < 1024 ? prefs.request_default_bounty : prefs.request_default_bounty / 1024;
  1716. if ((ref = document.getElementById('amount_box')) != null && !ref.disabled) ref.value = amount;
  1717. if ((ref = document.getElementById('unit')) != null && !ref.disabled) {
  1718. ref.value = prefs.request_default_bounty < 1024 ? 'mb' : 'gb';
  1719. }
  1720. Calculate();
  1721. }
  1722. if (!media && (ref = document.getElementById('media')) != null && ref.value && ref.value != '---') media = ref.value;
  1723. if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))) {
  1724. (release.coverUrls.length > 0 ? setCover(release.coverUrls[0]) : Promise.reject('No cover URL'))
  1725. .catch(getCoverOnline).catch(searchCoverOnline);
  1726. }
  1727. if (!onlineSource) {
  1728. onlineSource = (sourceUrl || release.urls.length > 0 ?
  1729. fetchOnline_Music(sourceUrl || release.urls[0], true).then(completeFromOnlineSource) : Promise.reject('No URL'));
  1730. if (prefs.check_integrity_online) onlineSource.catch(reason => lookupOnlineSource().then(function(result) {
  1731. if (typeof result == 'object') return parseLastFm(result);
  1732. if (urlParser.test(result)) return fetchOnline_Music(result, true);
  1733. return Promise.reject('Unhandled format');
  1734. })).then(onlineCheck).catch(function(reason) {
  1735. if (!media || media == 'WEB') tracks.forEach(function(track) {
  1736. if (!track.duration || track.duration < 29.6 || iter.duration > 30.4) return;
  1737. addMessage('track ' + track.tracknumber + ' possible track preview', 'warning');
  1738. });
  1739. });
  1740. }
  1741. if (prefs.clean_on_apply) clipBoard.value = '';
  1742. prefs.save();
  1743. return true;
  1744.  
  1745. // ---------------------------------------------------------------------------------------------------------------
  1746.  
  1747. function genPlaylist(pad = true, header = true) {
  1748. var style = prefs.tracklist_style;
  1749. if (style == 2 && (tracks.map(track => track.title).some(notMonospaced))
  1750. || tracks.map(track => track.track_artist).some(notMonospaced)
  1751. || composerEmphasis && tracks.map(track => track.composer).some(notMonospaced)) style = 1;
  1752. if (!style || style <= 0) return null;
  1753. var playlist = '';
  1754. if (tracks.length > 1 || isRequestNew || isRequestEdit) {
  1755. if (style == 3) playlist = '[align=center]';
  1756. if (pad && isRED) playlist += '[pad=5|0|0|0]';
  1757. if (header) playlist += genAlbumHeader();
  1758. playlist += '[size=4][b][color=' + prefs.tracklist_head_color + ']Tracklisting[/color][/b][/size]';
  1759. if (pad && isRED) playlist += '[/pad]';
  1760. playlist += '\n'; //'[hr]';
  1761. let lastDisc, lastSubtitle, lastWork, lastSide, vinylTrackWidth;
  1762. let block = 0, classicalWorks = new Map();
  1763. if (composerEmphasis /*isClassical*/ && !tracks.some(it => it.discsubtitle)) {
  1764. tracks.forEach(function(track) {
  1765. if (!track.composer) return;
  1766. (/*isClassical ? classicalWorkParsers : */classicalWorkParsers.slice(1)).forEach(function(classicalWorkParser) {
  1767. if (track.classical_work || !classicalWorkParser.test(track.title)) return;
  1768. classicalWorks.set(track.classical_work = RegExp.$1, {});
  1769. track.classical_title = RegExp.$2;
  1770. });
  1771. });
  1772. for (iter of classicalWorks.keys()) {
  1773. let work = tracks.filter(track => track.classical_work == iter);
  1774. if (work.length > 1 || tracks.every(track => track.classical_work)) {
  1775. if (work.map(it => it.track_artist).homogeneous()) classicalWorks.get(iter).performer = work[0].track_artist;
  1776. if (work.map(it => it.composer).homogeneous()) classicalWorks.get(iter).composer = work[0].composer;
  1777. } else {
  1778. work.forEach(function(track) {
  1779. delete track.classical_work;
  1780. delete track.classical_title;
  1781. });
  1782. classicalWorks.delete(iter);
  1783. }
  1784. }
  1785. }
  1786. let track, duration, volumes = new Map(tracks.map(it => [it.discnumber, undefined])), tnOffset = 0;
  1787. volumes.forEach(function(val, key) {
  1788. volumes.set(key, new Set(tracks.filter(it => it.discnumber == key).map(it => it.discsubtitle)).size)
  1789. });
  1790. if (!tracks.every(it => !isNaN(parseInt(it.tracknumber.toString())))
  1791. && !tracks.every(it => vinyltrackParser.test(it.tracknumber.toString().toUpperCase()))) {
  1792. addMessage('inconsistent tracks numbering (' + tracks.map(it => it.tracknumber) + ')', 'warning');
  1793. }
  1794. vinylTrackWidth = tracks.reduce(function(acc, it) {
  1795. return Math.max(vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) && parseInt(RegExp.$3), acc);
  1796. }, 0);
  1797. if (vinylTrackWidth) {
  1798. vinylTrackWidth = vinylTrackWidth.toString().length;
  1799. tracks.forEach(function(it) {
  1800. if (vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) != null)
  1801. it.tracknumber = RegExp.$1 + RegExp.$3.padStart(vinylTrackWidth, '0');
  1802. });
  1803. ++vinylTrackWidth;
  1804. }
  1805. if (release.totaldiscs < 2 && tracks.reduce(computeLowestTrack, undefined) - 1)
  1806. addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
  1807. const padStart = '[pad=0|0|5|0]';
  1808. if (canSort && prefs.sort_tracklist) tracks.sort(trackComparer);
  1809. for (iter of tracks) {
  1810. var trackArtist = getRealTrackArtist(iter), title = '';
  1811. var ttwidth = vinylTrackWidth || (release.totaldiscs > 1 && iter.discnumber ?
  1812. tracks.filter(it => it.discnumber == iter.discnumber) : tracks).reduce(function (accumulator, it) {
  1813. return Math.max(accumulator, (parseInt(it.tracknumber) || it.tracknumber).toString().length);
  1814. }, 2);
  1815. function realTrackNumber() {
  1816. var tn = parseInt(iter.tracknumber);
  1817. return isNaN(tn) ? iter.tracknumber : (tn - tnOffset).toString().padStart(ttwidth, '0');
  1818. }
  1819. switch (style) {
  1820. case 1:
  1821. case 3: {
  1822. prologue('[size=' + prefs.tracklist_size + ']', '[/size]\n');
  1823. track = '[b][color=' + prefs.tracklist_tracknumber_color + ']';
  1824. track += realTrackNumber();
  1825. track += '[/color][/b]' + prefs.title_separator;
  1826. if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
  1827. title = '[color=' + prefs.tracklist_artist_color + ']' + trackArtist + '[/color] - ';
  1828. }
  1829. title += iter.classical_title || iter.title;
  1830. if (iter.composer && composerEmphasis && release.composers.length != 1
  1831. && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
  1832. title = title.concat(' [color=', prefs.tracklist_composer_color, '](', iter.composer, ')[/color]');
  1833. }
  1834. playlist += track + title;
  1835. if (iter.duration) playlist += ' [i][color=' + prefs.tracklist_duration_color +'][' +
  1836. makeTimeString(iter.duration) + '][/color][/i]';
  1837. break;
  1838. }
  1839. case 2: {
  1840. prologue('[size=' + prefs.tracklist_size + '][pre]', '[/pre][/size]');
  1841. track = realTrackNumber();
  1842. track += prefs.title_separator;
  1843. if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
  1844. title = trackArtist + ' - ';
  1845. }
  1846. title += iter.classical_title || iter.title;
  1847. if (composerEmphasis && iter.composer && release.composers.length != 1
  1848. && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
  1849. title = title.concat(' (', iter.composer, ')');
  1850. }
  1851. let l = 0, j, left, padding, spc;
  1852. duration = iter.duration ? '[' + makeTimeString(iter.duration) + ']' : null;
  1853. let width = prefs.max_tracklist_width - track.length;
  1854. if (duration) width -= duration.length + 1;
  1855. while (title.trueLength() > 0) {
  1856. j = width;
  1857. if (title.trueLength() > width) {
  1858. while (j > 0 && title[j] != ' ') { --j }
  1859. if (j <= 0) j = width;
  1860. }
  1861. left = title.slice(0, j).trim();
  1862. if (++l <= 1) {
  1863. playlist += track + left;
  1864. if (duration) {
  1865. spc = width - left.trueLength();
  1866. padding = (spc < 2 ? ' '.repeat(spc) : ' ' + prefs.pad_leader.repeat(spc - 1)) + ' ';
  1867. playlist += padding + duration;
  1868. }
  1869. width = prefs.max_tracklist_width - track.length - 2;
  1870. } else playlist += '\n' + ' '.repeat(track.length) + left;
  1871. title = title.slice(j).trim();
  1872. }
  1873. break;
  1874. }
  1875. }
  1876. }
  1877. switch (style) {
  1878. case 1:
  1879. case 3:
  1880. if (totalTime > 0) playlist += '\n\n' + divs[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
  1881. ']Total time: [i]' + makeTimeString(totalTime) + '[/i][/color][/size]';
  1882. break;
  1883. case 2:
  1884. if (totalTime > 0) {
  1885. duration = '[' + makeTimeString(totalTime) + ']';
  1886. playlist += '\n\n' + divs[0].repeat(32).padStart(prefs.max_tracklist_width);
  1887. playlist += '\n' + 'Total time:'.padEnd(prefs.max_tracklist_width - duration.length) + duration;
  1888. }
  1889. playlist += '[/pre][/size]';
  1890. break;
  1891. }
  1892. if (style == 3) playlist += '[/align]';
  1893.  
  1894. function computeLowestTrack(acc, track) {
  1895. if (Number.isNaN(acc)) return NaN;
  1896. var tn = parseInt(track.tracknumber);
  1897. if (isNaN(tn)) return NaN;
  1898. return isNaN(acc) || tn < acc ? tn : acc;
  1899. }
  1900.  
  1901. function prologue(prefix, postfix) {
  1902. function block1() {
  1903. if (block == 3) playlist += postfix;
  1904. playlist += '\n';
  1905. if (isRED && ![1, 2].includes(block)) playlist += padStart;
  1906. block = 1;
  1907. }
  1908. function block2() {
  1909. if (block == 3) playlist += postfix;
  1910. playlist += '\n';
  1911. if (isRED && ![1, 2].includes(block)) playlist += padStart;
  1912. block = 2;
  1913. }
  1914. function block3() {
  1915. //if (block == 2 && isRED) playlist += '[hr]';
  1916. if (isRED && [1, 2].includes(block)) playlist += '[/pad]';
  1917. playlist += '\n';
  1918. if (block != 3) playlist += prefix;
  1919. block = 3;
  1920. }
  1921. if (release.totaldiscs > 1 && iter.discnumber != lastDisc) {
  1922. block1();
  1923. lastDisc = iter.discnumber;
  1924. lastSubtitle = lastWork = undefined;
  1925. playlist += '[color=' + prefs.tracklist_disctitle_color + '][size=3][b]';
  1926. if (iter.identifiers.VOL_MEDIA && tracks.filter(it => it.discnumber == iter.discnumber)
  1927. .every(it => it.identifiers.VOL_MEDIA == iter.identifiers.VOL_MEDIA)) {
  1928. playlist += iter.identifiers.VOL_MEDIA.toUpperCase() + ' ';
  1929. }
  1930. playlist += 'Disc ' + iter.discnumber;
  1931. if (iter.discsubtitle && (volumes.get(iter.discnumber) || 0) == 1) {
  1932. playlist += ' – ' + iter.discsubtitle;
  1933. lastSubtitle = iter.discsubtitle;
  1934. }
  1935. playlist += '[/b][/size]';
  1936. duration = tracks.filter(it => it.discnumber == iter.discnumber).reduce((acc, it) => acc + it.duration, 0);
  1937. if (duration > 0) playlist += ' [size=2][i][' + makeTimeString(duration) + '][/i][/size]';
  1938. playlist += '[/color]';
  1939. tnOffset = tracks.filter(track => track.discnumber == iter.discnumber).reduce(computeLowestTrack, undefined) - 1 || 0;
  1940. if (tnOffset) addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
  1941. }
  1942. if (iter.discsubtitle != lastSubtitle) {
  1943. if (block != 1 || iter.discsubtitle) block1();
  1944. if (iter.discsubtitle) {
  1945. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]' + iter.discsubtitle + '[/b][/size]';
  1946. duration = tracks.filter(it => it.discsubtitle == iter.discsubtitle)
  1947. .reduce((acc, it) => acc + it.duration, 0);
  1948. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  1949. playlist += '[/color]';
  1950. }
  1951. lastSubtitle = iter.discsubtitle;
  1952. }
  1953. if (iter.classical_work != lastWork) {
  1954. if (iter.classical_work) {
  1955. block2();
  1956. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
  1957. if (release.composers.length != 1 && classicalWorks.get(iter.classical_work).composer) {
  1958. playlist += classicalWorks.get(iter.classical_work).composer + ': ';
  1959. }
  1960. playlist += iter.classical_work;
  1961. playlist += '[/b]';
  1962. if (classicalWorks.get(iter.classical_work).performer
  1963. && classicalWorks.get(iter.classical_work).performer != release.artist) {
  1964. playlist += ' (' + classicalWorks.get(iter.classical_work).performer + ')';
  1965. }
  1966. playlist += '[/size]';
  1967. duration = tracks.filter(it => it.classical_work == iter.classical_work)
  1968. .reduce((acc, it) => acc + it.duration, 0);
  1969. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  1970. playlist += '[/color]';
  1971. } else {
  1972. if (block > 2) block1();
  1973. }
  1974. lastWork = iter.classical_work;
  1975. }
  1976. if (vinyltrackParser.test(iter.tracknumber)) {
  1977. if (block == 3 && lastSide && RegExp.$1 != lastSide) playlist += '\n';
  1978. lastSide = RegExp.$1;
  1979. }
  1980. block3();
  1981. } // prologue
  1982. } else { // single
  1983. playlist += '[align=center]';
  1984. playlist += isRED ? '[pad=20|20|20|20]' : '';
  1985. playlist += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + release.artist + '[/color]';
  1986. playlist += isRED ? '[hr]' : '\n'.concat(divs[0].repeat(24), '\n');
  1987. playlist += tracks[0].title + '[/b]';
  1988. if (tracks[0].composer) {
  1989. playlist += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
  1990. }
  1991. playlist += '\n\n[color=' + prefs.tracklist_duration_color +'][' + makeTimeString(tracks[0].duration) + '][/color][/size]';
  1992. if (isRED) playlist += '[/pad]';
  1993. playlist += '[/align]';
  1994. }
  1995. return playlist;
  1996. }
  1997.  
  1998. function getUrls() {
  1999. var result = [];
  2000. if (sourceUrl) result.push(sourceUrl);
  2001. Array.prototype.push.apply(result, release.urls.filter(function(url) {
  2002. return !sourceUrl || url.toLowerCase() != sourceUrl.toLowerCase();
  2003. }));
  2004. return result.map(url => urlParser.test(url) ? '[url]' + url + '[/url]' : url).join('\n');
  2005. }
  2006.  
  2007. function genAlbumHeader() {
  2008. return !isVA && artists[0].length >= 3 ? '[size=4]' +
  2009. joinArtists(artists[0], artist => '[artist]' + artist + '[/artist]') + ' – ' + release.album + '[/size]\n\n' : '';
  2010. }
  2011.  
  2012. function findPreviousUploads() {
  2013. let search = new URLSearchParams(document.location.search);
  2014. if (search.get('groupid')) localFetch('/torrents.php?action=grouplog&groupid=' + search.get('groupid')).then(function(dom) {
  2015. dom.querySelectorAll('table > tbody > tr.rowa').forEach(function(tr) {
  2016. if (/^\s*deleted\b/i.test(tr.children[3].textContent))
  2017. scanLog('Torrent ' + tr.children[1].firstChild.textContent);
  2018. });
  2019. }); else {
  2020. let query = '';
  2021. if (!isVA && artists[0].length >= 1 && artists[0].length <= 3) query = artists[0].join(', ') + ' - ';
  2022. query += release.album;
  2023. scanLog(query);
  2024. }
  2025.  
  2026. function scanLog(query) {
  2027. localFetch('/log.php?search=' + encodeURIComponent(query)).then(function(dom) {
  2028. dom.querySelectorAll('table > tbody > tr.rowb').forEach(function(tr) {
  2029. var size, msg = tr.children[1].textContent.trim();
  2030. if (/\b[\d\s]+(?:\.\d+)?\s*(?:([KMGT])I?)?B\b/.test(msg)) size = get_size_from_string(RegExp.lastMatch);
  2031. if (!msg.includes('deleted') || (/\[(.*)\/(.*)\/(.*)\]/.test(msg) ?
  2032. !release.codec || release.codec != RegExp.$1
  2033. //|| !release.encoding || release.encoding != RegExp.$2
  2034. || !media || media != RegExp.$3 :
  2035. !size || !albumSize || Math.abs(albumSize / size - 1) >= 0.1)) return;
  2036. addMessage('possibly same release previously uploaded and deleted: ' + msg, 'warning');
  2037. });
  2038. });
  2039. }
  2040.  
  2041. function get_size_from_string(str) {
  2042. var matches = /\b([\d\s]+(?:\.\d+)?)\s*(?:([KMGT])I?)?B\b/.exec(str.replace(',', '.').toUpperCase());
  2043. if (!matches) return null;
  2044. var size = parseFloat(matches[1].replace(/\s+/g, ''));
  2045. if (matches[2] == 'K') { size *= Math.pow(1024, 1) }
  2046. else if (matches[2] == 'M') { size *= Math.pow(1024, 2) }
  2047. else if (matches[2] == 'G') { size *= Math.pow(1024, 3) }
  2048. else if (matches[2] == 'T') { size *= Math.pow(1024, 4) }
  2049. return Math.round(size);
  2050. }
  2051. }
  2052.  
  2053. function getHomoIdentifier(id) {
  2054. id = id.toUpperCase();
  2055. return tracks.every((elem, ndx, arr) => elem.identifiers[id] != undefined
  2056. && elem.identifiers[id] === arr[0].identifiers[id]) ? tracks[0].identifiers[id] : undefined;
  2057. }
  2058.  
  2059. function barcode() {
  2060. var r = getHomoIdentifier('BARCODE');
  2061. if (r) r = parseInt(r.replace(/\s+/g, ''));
  2062. if (r) return r;
  2063. if (release.catalogs.length == 1) r = parseInt(release.catalogs[0].replace(/[\s\-]/g, ''));
  2064. return r > 10**10 ? r : undefined;
  2065. }
  2066.  
  2067. function getStoreUrl() {
  2068. for (var it of [
  2069. ['ACOUSTICSOUNDS_ID', 'https://store.acousticsounds.com/d/{ID}/'],
  2070. ['AMAZON_ID', 'https://www.amazon.com/gp/product/{ID}'],
  2071. ['APPLE_ID', 'https://music.apple.com/album/{ID}'],
  2072. ['ASIN', 'https://www.amazon.com/gp/product/{ID}'],
  2073. ['BEATPORT_ID', 'https://www.beatport.com/release/2/{ID}'],
  2074. ['DEEZER_ID', deezerAlbumPrefix + '{ID}'],
  2075. ['DISCOGS_ID', discogsOrigin + '/release/{ID}'],
  2076. ['EONKYO_ID', 'https://www.e-onkyo.com/music/album/{ID}/'],
  2077. ['GOOGLE_ID', 'https://play.google.com/store/music/album/?id={ID}'],
  2078. ['INDIESSCOPE_ID', 'https://www.indies.eu/alba/{ID}/'],
  2079. ['ITUNES_ID', 'https://music.apple.com/album/{ID}'],
  2080. ['JUNODOWNLOAD_ID', 'https://www.junodownload.com/products/{ID}'],
  2081. ['MBID', mbrRlsPrefix + '{ID}'],
  2082. ['PROSTUDIOMASTERS_ID', 'https://www.prostudiomasters.com/album/page/{ID}'],
  2083. ['SPOTIFY_ID', 'https://open.spotify.com/album/{ID}'],
  2084. ['TRAXSOURCE_ID', 'https://www.traxsource.com/title/{ID}/'],
  2085. ]) {
  2086. let ID = getHomoIdentifier(it[0]);
  2087. if (ID) return it[1].replace('{ID}', ID);
  2088. }
  2089. return undefined;
  2090. }
  2091.  
  2092. function getCoverOnline() {
  2093. var url = sourceUrl || release.urls[0], apiFirst;
  2094. if (i = getHomoIdentifier('APPLE_ID') || getHomoIdentifier('ITUNES_ID')
  2095. || /^https?:\/\/(?:\w+\.)*apple\.com\/.*\/(\d+)(?=$|\?)/i.test(url) && RegExp.$1) {
  2096. apiFirst = queryItunesAPI('lookup', { id: i })
  2097. .then(result => result.resultCount > 0 ? setItunesImage(result.results[0]) : Promise.reject('no cover'));
  2098. } else if (i = getHomoIdentifier('DEEZER_ID')
  2099. || /^https:\/\/(?:\w+\.)*deezer\.com\/(\w+\/)*album\/(\d+)$/i.test(url) && RegExp.$1) {
  2100. apiFirst = queryDeezerAPI('album/' + i)
  2101. .then(result => result.total > 0 ? setDeezerImage(result.data[0]) : Promise.reject('No cover'));
  2102. } else if ((prefs.discogs_key && prefs.discogs_secret || discogs_token)
  2103. && (i = getHomoIdentifier('DISCOGS_ID') || dcRlsParser.test(url) && RegExp.$1)) {
  2104. apiFirst = queryDiscogsAPI('releases/' + i).then(function(release) {
  2105. return release.images.length > 0 ? setCover(release.images[0].uri) : Promise.reject('No cover');
  2106. });
  2107. } else if ((i = getHomoIdentifier('MBID') || mbrRlsParser.test(url) && RegExp.$1)) {
  2108. apiFirst = getMusicBrainzCovers(i).then(function(covers) {
  2109. return covers && covers[1].length > 0 ? setCover(covers[1][0]) : Promise.reject('No cover');
  2110. });
  2111. } else apiFirst = Promise.reject('No known API binding');
  2112. return apiFirst.catch(reason => urlParser.test(url) ? new Promise((resolve, reject) => GM_xmlhttpRequest({
  2113. method: 'GET',
  2114. url: dcRlsParser.test(url) ? discogsOrigin + '/release/' + RegExp.$1 + '/images' : url,
  2115. onload: function(response) {
  2116. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) return reject(defaultErrorHandler(response));
  2117. var ref, imgUrl, hostname = new URL(response.finalUrl).hostname.toLowerCase();
  2118. var dom = domParser.parseFromString(response.responseText, 'text/html');
  2119. function testDomain(url, selector) {
  2120. return hostname.includes(url.toLowerCase()) ? dom.querySelector(selector) : null;
  2121. }
  2122. if ((ref = testDomain('qobuz.com', 'div.album-cover > img')) != null) {
  2123. return setCover(ref.src.replace(/_\d{3}(?=\.\w+$)/, '_max')).catch(reason => setCover(ref.src))
  2124. .then(resolve).catch(reject);
  2125. } else if ((ref = testDomain('highresaudio.com', 'div.albumbody > img.cover[data-pin-media]')) != null) {
  2126. imgUrl = ref.dataset.pinMedia;
  2127. } else if ((ref = testDomain('bandcamp.com', 'div#tralbumArt > a.popupImage')) != null) {
  2128. imgUrl = ref.href;
  2129. } else if ((ref = testDomain('7digital.com', 'span.release-packshot-image > img[itemprop="image"]')) != null) {
  2130. imgUrl = ref.src;
  2131. } else if ((ref = testDomain('hdtracks.', 'p.product-image > img')) != null) {
  2132. imgUrl = ref.src;
  2133. } else if ((ref = testDomain('discogs.com', 'div#view_images > p:first-of-type > span > img')) != null) {
  2134. imgUrl = ref.src;
  2135. } else if ((ref = testDomain('prestomusic.com', 'div.c-product-block__aside > a')) != null) {
  2136. imgUrl = ref.href.replace(/\?\d+$/, '');
  2137. } else if ((ref = testDomain('bontonland.cz', 'a.detailzoom')) != null) {
  2138. imgUrl = ref.href;
  2139. } else if ((ref = testDomain('nativedsd.com', 'a#album-cover')) != null) {
  2140. imgUrl = ref.href;
  2141. } else if ((ref = testDomain('prostudiomasters.com', 'img.album-art')) != null) {
  2142. imgUrl = ref.currentSrc || ref.src;
  2143. } else if ((ref = testDomain('e-onkyo.com', 'figure > a.colorbox')) != null) {
  2144. imgUrl = new URL(response.finalUrl).origin + ref.pathname;
  2145. } else if ((ref = testDomain('store.acousticsounds.com', 'div#detail > link[rel="image_src"]')) != null) {
  2146. imgUrl = ref.href.replace(/\/medium\//i, '/large/');
  2147. } else if ((ref = testDomain('indies.eu', 'div.obrazekDetail > img')) != null) {
  2148. imgUrl = ref.src;
  2149. } else if ((ref = testDomain('beatport.com', 'div > img.interior-release-chart-artwork')) != null) {
  2150. imgUrl = ref.src;
  2151. } else if ((ref = testDomain('supraphonline.cz', 'meta[itemprop="image"]')) != null) {
  2152. imgUrl = ref.content.replace(/\?.*$/, '');
  2153. } else if ((ref = dom.querySelector('meta[property="og:image"]')
  2154. || dom.querySelector('meta[itemprop="image"]')) != null && ref.content) {
  2155. imgUrl = ref.content;
  2156. }
  2157. if (imgUrl) setCover(imgUrl).then(resolve).catch(reject); else reject('No URL to parse');
  2158. },
  2159. onerror: error => reject(defaultErrorHandler(error)),
  2160. onabort: abort => reject(defaultAbortHandler(abort)),
  2161. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  2162. })) : Promise.reject('No valid URL to parse'));
  2163. }
  2164.  
  2165. function searchCoverOnline() {
  2166. switch (typeof prefs.cover_lookup_provider == 'string' && prefs.cover_lookup_provider.toLowerCase()) {
  2167. case 'itunes': return searchCoverOnline_iTunes();
  2168. case 'deezer': return searchCoverOnline_Deezer();
  2169. case 'google': return searchCoverOnline_GooglePlay();
  2170. case 'musicbrainz': return searchCoverOnline_MBR();
  2171. case 'lastfm': return searchCoverOnline_LastFM();
  2172. case 'qobuz': return searchCoverOnline_Qobuz();
  2173. case 'all': return searchCoverOnline_iTunes()
  2174. .catch(searchCoverOnline_LastFM)
  2175. .catch(searchCoverOnline_Deezer)
  2176. .catch(searchCoverOnline_MBR)
  2177. .catch(searchCoverOnline_Qobuz)
  2178. .catch(searchCoverOnline_GooglePlay);
  2179. }
  2180. return Promise.reject('no valid service selected');
  2181.  
  2182. function searchCoverOnline_iTunes() {
  2183. return queryItunesAPI('search', {
  2184. term: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2185. media: 'music',
  2186. entity: 'album',
  2187. //country: 'US',
  2188. }).then(function(result) {
  2189. if (result.resultCount <= 0) return Promise.reject('Apple Music: no results');
  2190. return setItunesImage(result.results[0])
  2191. .then(release => { info('Apple Music', release.collectionViewUrl, release.collectionId) });
  2192. });
  2193. }
  2194. function searchCoverOnline_Deezer() {
  2195. return queryDeezerAPI('search', {
  2196. q: 'artist:"' + (isVA ? VA : release.artist) + '" album:"' + release.album + '"',
  2197. strict: 'on',
  2198. order: 'RANKING',
  2199. }).then(function(result) {
  2200. if (result.total <= 0) return Promise.reject('Deezer: no results');
  2201. return setDeezerImage(result.data[0])
  2202. .then(release => { info('Deezer', deezerAlbumPrefix + release.id, release.id) });
  2203. });
  2204. }
  2205. function searchCoverOnline_GooglePlay() {
  2206. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  2207. method: 'GET',
  2208. url: 'https://play.google.com/store/search?' + new URLSearchParams({
  2209. q: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2210. c: 'music',
  2211. }).toString(),
  2212. onload: function(response) {
  2213. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) return reject(defaulErrorHandler(response));
  2214. var dom = domParser.parseFromString(response.responseText, 'text/html');
  2215. resolve(dom.querySelectorAll('div:first-of-type + div[jscontroller]:last-of-type'));
  2216. },
  2217. onerror: error => reject(defaultErrorHandler(error)),
  2218. onabort: abort => reject(defaultAbortHandler(abort)),
  2219. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  2220. })).then(function(results) {
  2221. if (results.length > 0) for (var ndx = 0; ndx < results.length; ++ndx) {
  2222. let items = [];
  2223. results[ndx].querySelectorAll(':scope > div').forEach(function(result) {
  2224. var img = result.querySelector('span > span > img');
  2225. img = img != null ? (img.src || img.dataset.src).replace(/=[a-z]\d+$/, '=w0') : null;
  2226. var title = result.querySelector('a > div[title]');
  2227. if (title != null) {
  2228. var artist = title.parentNode.parentNode.parentNode.querySelector('a > div:not([title])')
  2229. artist = artist != null ? artist.textContent.trim() : null;
  2230. var url = title.parentNode.href;
  2231. var id = /\?id=(\w+)\b/i.test(title.parentNode.href) && RegExp.$1 || null;
  2232. title = title.textContent.trim();
  2233. } else title = null;
  2234. items.push({ id: id, url: url, artist: artist, album: title, imgUrl: img });
  2235. });
  2236. if (items.length > 0 && items[0].imgUrl) return setCover(items[0].imgUrl)
  2237. .then(release => { info('Google Play Music', items[0].url, items[0].id) });
  2238. }
  2239. return Promise.reject('Google Play Music: no matches');
  2240. });
  2241. }
  2242. function searchCoverOnline_MBR() {
  2243. var barCode = barcode();
  2244. (barCode ? queryMusicBrainzAPI('release', { query: 'barcode:' + barCode }) : Promise.reject('No barcode'))
  2245. .catch(function(reason) {
  2246. var asin = getHomoIdentifier('ASIN');
  2247. if (!asin) return Promise.rečject('No ASIN');
  2248. return queryMusicBrainzAPI('release', { query: 'asin:' + asin.replace(/\s+/g, '') });
  2249. }).catch(reason => queryMusicBrainzAPI('release', {
  2250. query: 'release:"' + release.album + '" AND artist:"' + (isVA ? VA : release.artist) + '"',
  2251. })).then(function(result) {
  2252. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2253. return Promise.all(result.releases.map(release => getMusicBrainzCovers(release.id)));
  2254. }).then(function(releases) {
  2255. for (var rls of releases) if (rls && rls[1].length > 0) return setCover(rls[1][0]).then(() => {
  2256. if (/\/release\/(\S+)$/i.test(rls[0])) info('Musicbrains', rls[0], RegExp.$1);
  2257. });
  2258. return Promise.reject('MusicBrainz: no covers found');
  2259. });
  2260. }
  2261. function searchCoverOnline_LastFM() {
  2262. return queryLastFmAPI('album.getinfo', {
  2263. artist: (isVA ? VA : release.artist),
  2264. album: release.album,
  2265. }).then(function(result) {
  2266. if (result.error) return Promise.reject(result.message);
  2267. var r = result.album.image.filter(image => image.size == /*'extralarge'*/'mega');
  2268. if (r.length <= 0) return Promise.reject('Last.fm: no cover for matched album');
  2269. r = r[0]['#text'];
  2270. if (!r) return Promise.reject('Last.fm: no cover for matched album');
  2271. return setCover(r.replace(/\/\d+x\d+\//, '/')).catch(reason => setCover(r))
  2272. .then(() => { info('Last.fm', result.album.url, result.album.id || result.album.mbid || '#N/A') });
  2273. });
  2274. }
  2275. function searchCoverOnline_Qobuz() {
  2276. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  2277. method: 'GET',
  2278. url: 'https://www.qobuz.com/search?' + new URLSearchParams({
  2279. q: (isVA ? VA : release.artist) + ' ' + release.album,
  2280. i: 'boutique',
  2281. }).toString(),
  2282. onload: function(response) {
  2283. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) return reject(defaultErrorHandler(response));
  2284. var dom = domParser.parseFromString(response.responseText, 'text/html');
  2285. var results = dom.querySelectorAll('div.search-results > div.product');
  2286. if (results.length <= 0) return reject('Qobuz: no matches');
  2287. var f = filter(true);
  2288. if (f.length > 0 || (f = filter(false)).length > 0) resolve(f[0]); else reject('Qobuz: no matches');
  2289.  
  2290. function filter(strict = true) {
  2291. var _results = [];
  2292. results.forEach(function(result) {
  2293. var _result = {};
  2294. _result.artist = result.querySelector('div.artist-name > a');
  2295. if (_result.artist != null) _result.artist = _result.artist.textContent.trim();
  2296. _result.title = result.querySelector('div.album-title > a');
  2297. if (_result.title != null) {
  2298. _result.id = _result.title.pathname.replace(/^.*\//, '');
  2299. _result.href = 'https://www.qobuz.com' + _result.title.pathname;
  2300. _result.title = _result.title.textContent.trim();
  2301. }
  2302. _result.imgUrl = result.querySelector('div.album-cover > a > img');
  2303. if (_result.imgUrl != null) _result.imgUrl = _result.imgUrl.dataset.src || _result.imgUrl.src;
  2304. if (_result.artist != null && _result.title != null && _result.imgUrl != null
  2305. && releasesMatch(_result.artist, _result.title, strict)) _results.push(_result);
  2306. });
  2307. return _results;
  2308. }
  2309. },
  2310. onerror: error => reject(defaultErrorHandler(error)),
  2311. onabort: abort => reject(defaultAbortHandler(abort)),
  2312. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  2313. })).then(function(album) {
  2314. setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_max'))
  2315. .catch(reason => setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_600')))
  2316. .catch(reason => setCover(album.imgUrl))
  2317. .then(function(imgUrl) {
  2318. info('Qobuz', album.href, album.id);
  2319. return imgUrl;
  2320. });
  2321. });
  2322. }
  2323. function info(service, url, id) {
  2324. addMessage(new HTML('used cover image from ' + service + ' release ID ' +
  2325. '<a style="color: #00f3ff;" target="_blank" href="'+ url + '">' + id + '</a>'), 'info');
  2326. }
  2327. }
  2328.  
  2329. function setItunesImage(result) {
  2330. return urlParser.test(result.artworkUrl100) ?
  2331. setCover(result.artworkUrl100.replace('100x100bb', '100000x100000-999')).then(() => result)
  2332. : Promise.reject('Apple Music image not valid URL');
  2333. }
  2334. function setDeezerImage(result) {
  2335. return urlParser.test(result.album.cover_xl) ?
  2336. setCover(result.album.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0'))
  2337. .then(() => result.album) : Promise.reject('Deezer image not valid URL');
  2338. }
  2339.  
  2340. function completeFromOnlineSource(onlineTracks) {
  2341. fillMissingValue(document.getElementById('media'), 'media');
  2342. fillMissingValue(document.getElementById('year'), 'album_year');
  2343. ref = document.getElementById('remaster_year') || !isUpload && document.querySelector('input[name="year"]');
  2344. if (ref != null && !ref.disabled && (ref.value == '' || !isRED && ref.value == '---')) {
  2345. var value = getHomoValue('release_date');
  2346. if (value != null) ref.value = extractYear(value);
  2347. }
  2348. fillMissingValue(document.getElementById('remaster_record_label')
  2349. || document.querySelector('input[name="recordlabel"]'), 'label');
  2350. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  2351. || document.querySelector('input[name="cataloguenumber"]'))) {
  2352. let catNo = getHomoValue('catalog');
  2353. if (!catNo && onlineTracks.every(track => track.identifiers.BARCODE
  2354. && track.identifiers.BARCODE == onlineTracks[0].identifiers.BARCODE)) {
  2355. catNo = parseInt(onlineTracks[0].identifiers.BARCODE.replace(/\s+/g, ''));
  2356. }
  2357. if (catNo) ref.value = catNo;
  2358. }
  2359. return onlineTracks;
  2360.  
  2361. function getHomoValue(propName) {
  2362. return onlineTracks[0][propName] && onlineTracks.map(track => track[propName]).homogeneous() ?
  2363. onlineTracks[0][propName] : null;
  2364. }
  2365. function fillMissingValue(node, propName) {
  2366. if (!node || node.disabled || node.value != '' && (isRED || node.value != '---')) return;
  2367. var value = getHomoValue(propName);
  2368. if (value != null) node.value = value;
  2369. }
  2370. }
  2371.  
  2372. function onlineCheck(onlineTracks) {
  2373. if (!Array.isArray(onlineTracks) || onlineTracks.length <= 0) {
  2374. addMessage('online check not performed (empty tracklist)', 'notice');
  2375. return Promise.reject('No tracklist');
  2376. }
  2377. var issueCounter = 0;
  2378. if (onlineTracks.map(track => track.artist).homogeneous()
  2379. && (release.artist || '').toLowerCase() != (onlineTracks[0].artist || '').toLowerCase()
  2380. && !artists[0].equalTo(splitArtists(onlineTracks[0].artist || ''))) {
  2381. ++issueCounter;
  2382. addMessage(new HTML('online album main artist mismatch ("' +
  2383. safeText(release.artist).bold() + '" ≠ "' + safeText(onlineTracks[0].artist).bold() + '")'), 'notice');
  2384. }
  2385. if ((release.album || '').toLowerCase() != (onlineTracks[0].album || '').toLowerCase()
  2386. && onlineTracks.map(track => track.album).homogeneous()) {
  2387. ++issueCounter;
  2388. addMessage(new HTML('online album title mismatch ("' +
  2389. safeText(release.album).bold() + '" ≠ "' + safeText(onlineTracks[0].album).bold() + '")'), 'notice');
  2390. }
  2391. if (release.label && onlineTracks[0].label && onlineTracks.map(track => track.label).homogeneous()
  2392. && (release.label || '').toASCII().toLowerCase().replace(/-/g, '/')
  2393. != onlineTracks[0].label.toASCII().toLowerCase().replace(/-/g, '/')) {
  2394. ++issueCounter;
  2395. addMessage(new HTML('online album label mismatch ("' +
  2396. safeText(release.label).bold() + '" ≠ "' + safeText(onlineTracks[0].label).bold() + '")'), 'notice');
  2397. }
  2398. if (release.catalogs.length == 1 && onlineTracks[0].catalog
  2399. && onlineTracks.map(track => track.catalog).homogeneous()
  2400. && release.catalogs[0].toASCII().toLowerCase().replace(/[\s\-]/g, '')
  2401. != onlineTracks[0].catalog.toASCII().toLowerCase().replace(/[\s\-]/g, '')) {
  2402. ++issueCounter;
  2403. addMessage(new HTML('online album catalogue# mismatch ("' +
  2404. safeText(release.catalogs[0]).bold() + '" ≠ "' + safeText(onlineTracks[0].catalog).bold() + '")'), 'notice');
  2405. }
  2406. if (onlineTracks[0].album_year && onlineTracks.map(track => track.album_year).homogeneous()
  2407. && release.album_year != onlineTracks[0].album_year) {
  2408. ++issueCounter;
  2409. addMessage(new HTML('online album year mismatch (' +
  2410. (release.album_year || '').toString().bold() + ' ≠ ' + onlineTracks[0].album_year.toString().bold() + ')'), 'warning');
  2411. }
  2412. if (onlineTracks[0].release_date && onlineTracks.map(track => track.release_date).homogeneous()
  2413. && new Date(release.release_date.toString()).getDateValue()
  2414. != new Date(onlineTracks[0].release_date.toString()).getDateValue()) {
  2415. ++issueCounter;
  2416. addMessage(new HTML('online album release date mismatch (' +
  2417. (release.release_date || '').toString().bold() + ' ≠ ' + onlineTracks[0].release_date.toString().bold() + ')'), 'notice');
  2418. }
  2419. if (tracks.length != onlineTracks.length) {
  2420. ++issueCounter;
  2421. addMessage(new HTML('online album different tracklist length (' + tracks.length.toString().bold() +
  2422. ' ≠ ' + onlineTracks.length.toString().bold() + ')'), 'warning');
  2423. }
  2424. if (totalTime > 0) {
  2425. let ttOnline = onlineTracks.reduce((acc, track) => acc + (track.duration || NaN), 0);
  2426. if (ttOnline > 0 && Math.abs(totalTime - ttOnline) * 100 / ttOnline > (media != 'Vinyl' ? 0.75 : 2.5)) {
  2427. ++issueCounter;
  2428. addMessage(new HTML('online album duration mismatch (' + makeTimeString(totalTime).bold() +
  2429. ' ≠ ' + makeTimeString(ttOnline).bold() + ')'), 'warning');
  2430. }
  2431. }
  2432. if (tracks.length == onlineTracks.length) for (let ndx = 0; ndx < tracks.length; ++ndx) {
  2433. if ((tracks[ndx].title || '').toLowerCase() != (onlineTracks[ndx].title || '').toLowerCase()
  2434. && (tracks[ndx].title || '').toLowerCase() != featParsers.slice(3).reduce(function(acc, rx, ndx) {
  2435. return rx.test(acc) && (ndx < 5 || splitArtists(RegExp.$1).every((artist, ndx) => looksLikeTrueName(artist, 1))) ?
  2436. acc.replace(rx, '') : acc;
  2437. }, onlineTracks[ndx].title || '').toLowerCase()) {
  2438. ++issueCounter;
  2439. addMessage('online track #' + (ndx + 1) + ' title mismatch ("' +
  2440. (tracks[ndx].title || '') + '" ≠ "' + (onlineTracks[ndx].title || '') + '")', 'notice');
  2441. }
  2442. if (/*isVA && */onlineTracks[ndx].track_artist
  2443. && (tracks[ndx].track_artist || '').toLowerCase() != onlineTracks[ndx].track_artist.toLowerCase()
  2444. && (tracks[ndx].track_artist || '').toLowerCase() != onlineTracks[ndx].track_artist.replace(/ & (?!.* & )/, ', ').toLowerCase()) {
  2445. ++issueCounter;
  2446. addMessage('online track #' + (ndx + 1) + ' track artist mismatch ("' +
  2447. (tracks[ndx].track_artist || '') + '" ≠ "' + (onlineTracks[ndx].track_artist || '') + '")', 'notice');
  2448. }
  2449. if (tracks[ndx].tracknumber != onlineTracks[ndx].tracknumber) {
  2450. ++issueCounter;
  2451. addMessage('online track #' + (ndx + 1) + ' track number mismatch (' +
  2452. tracks[ndx].tracknumber + ' ≠ ' + onlineTracks[ndx].tracknumber + ')',
  2453. release.totaldiscs > 1 ? 'notice' : 'warning');
  2454. }
  2455. if (onlineTracks[ndx].discnumber && (onlineTracks[ndx].discnumber > 1 || tracks[ndx].discnumber)
  2456. && tracks[ndx].discnumber != onlineTracks[ndx].discnumber) {
  2457. ++issueCounter;
  2458. addMessage('online track #' + (ndx + 1) + ' disc number mismatch (' +
  2459. tracks[ndx].discnumber + ' ≠ ' + onlineTracks[ndx].discnumber + ')', 'warning');
  2460. }
  2461. if (onlineTracks[ndx].discsubtitle
  2462. && (tracks[ndx].discsubtitle || '').toLowerCase() != onlineTracks[ndx].discsubtitle.toLowerCase()) {
  2463. ++issueCounter;
  2464. addMessage('online track #' + (ndx + 1) + ' disc subtitle mismatch ("' +
  2465. (tracks[ndx].discsubtitle || '') + '" ≠ "' + onlineTracks[ndx].discsubtitle + '")', 'notice');
  2466. }
  2467. let timeDif = tracks[ndx].duration && onlineTracks[ndx].duration
  2468. && Math.abs(tracks[ndx].duration - onlineTracks[ndx].duration);
  2469. if (timeDif >= (media != 'Vinyl' ? 1.5 : 5)) {
  2470. ++issueCounter;
  2471. addMessage('online track #' + (ndx + 1) + ' duration mismatch (' +
  2472. makeTimeString(tracks[ndx].duration) + ' ≠ ' + makeTimeString(onlineTracks[ndx].duration) + ')',
  2473. (timeDif >= (media != 'Vinyl' ? 3 : 8) ? 'warning' : 'notice'));
  2474. }
  2475. }
  2476. if (prefs.messages_verbosity >= 1 && issueCounter <= 0) addMessage('online check completed without remarks', 'info');
  2477. }
  2478.  
  2479. function lookupOnlineSource() {
  2480. var albumTitle = release.artist && release.album;
  2481. var barCode = barcode();
  2482. return (barCode ? querySpotifyAPI('search', { q: 'barcode:' + barCode, type: 'album' }).then(function(result) {
  2483. if (result.albums.total <= 0) return Promise.reject('Spotify: no matches');
  2484. if (result.albums.total > 1) return Promise.reject('Spotify: ambiguity');
  2485. info('Spotify', result.albums.items[0].external_urls.spotify, result.albums.items[0].id);
  2486. return result.albums.items[0].href;
  2487. }) : Promise.reject('No barcode')).catch(reason => reason == 'Spotify: ambiguity' ? Promise.reject(reason) :
  2488. albumTitle && (!media || ['CD', 'WEB'].includes(media)) ? querySpotifyAPI('search', {
  2489. q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
  2490. type: 'album',
  2491. limit: 50,
  2492. }).then(function(result) {
  2493. if (result.albums.total <= 0) return Promise.reject('No matches');
  2494. var f = filter(true);
  2495. if (f.length > 1) return Promise.reject('Spotify: ambiguity');
  2496. if (f.length < 1) {
  2497. f = filter(false);
  2498. if (f.length > 1) return Promise.reject('Spotify: ambiguity');
  2499. if (f.length < 1) return Promise.reject('Spotify: no matches');
  2500. }
  2501. info('Spotify', f[0].external_urls.spotify, f[0].id);
  2502. return f[0].href;
  2503.  
  2504. function filter(strict) {
  2505. return result.albums.items.filter(function(album) {
  2506. return releasesMatch(album.artists.map(artist => artist.name), album.name, strict)
  2507. && (release.release_type == getReleaseIndex('Single')) == (album.album_type == 'single');
  2508. })
  2509. }
  2510. }) : Promise.reject('Insufficient information')).catch(reason => barCode ?
  2511. queryMusicBrainzAPI('release', { query: 'barcode:' + barCode }).then(MB_simpleCheck)
  2512. : Promise.reject('No barcode')).catch(function(reason) {
  2513. if (reason == 'MusicBrainz: ambiguity') return Promise.reject(reason);
  2514. var asin = getHomoIdentifier('ASIN');
  2515. if (!asin) return Promise.rečject('MusicBrainz: insufficient information');
  2516. return queryMusicBrainzAPI('release', { query: 'asin:' + asin.replace(/\s+/g, '') }).then(MB_simpleCheck);
  2517. }).catch(function(reason) {
  2518. if (reason == 'MusicBrainz: ambiguity') return Promise.reject(reason);
  2519. var discId = getHomoIdentifier('DISCID');
  2520. var TOC = getHomoIdentifier('ITUNES_TOC');
  2521. if (TOC) {
  2522. TOC = TOC.split('+');
  2523. if (!discId && TOC.length > 0) discId = TOC[0];
  2524. TOC = [1, TOC[2], TOC[1]].concat(TOC.slice(3));
  2525. } else if (TOC = getHomoIdentifier('CT_TOC')) {
  2526. TOC = TOC.split('+');
  2527. TOC = [1, parseInt(TOC.shift(), 16), parseInt(TOC.pop(), 16)].concat(TOC.map(frame => parseInt(frame, 16)));
  2528. }
  2529. if (!discId && !TOC) return Promise.rečject('MusicBrainz: insufficient information');
  2530. if (TOC) TOC = { toc: TOC.join('+'), 'media-format': 'all' };
  2531. return queryMusicBrainzAPI('discid/' + (discId || '-'), TOC).then(MB_simpleCheck);
  2532. }).catch(function(reason) {
  2533. if (reason == 'MusicBrainz: ambiguity') return Promise.reject(reason);
  2534. if (!albumTitle/* || !media*/) return Promise.reject('MusicBrainz: insufficient information');
  2535. return queryMusicBrainzAPI('release', {
  2536. query: 'release:"' + release.album + '" AND artist:"' + (isVA ? VA : release.artist) + '"',
  2537. }).then(function(result) {
  2538. if (result.count < 1) return Promise.reject('MusicBrainz: no matches');
  2539. var f = filter(true);
  2540. if (f.length > 1) return Promise.reject('MusicBrainz: ambiguity');
  2541. if (f.length < 1) {
  2542. f = filter(false);
  2543. if (f.length > 1) return Promise.reject('MusicBrainz: ambiguity');
  2544. if (f.length < 1) return Promise.reject('MusicBrainz: no matches');
  2545. }
  2546. info('MusicBrainz', mbrRlsPrefix + f[0].id, f[0].id);
  2547. return mbrRlsPrefix + f[0].id;
  2548.  
  2549. function filter(strict) {
  2550. return result.releases.filter(function(album) {
  2551. return album.quality != 'low'
  2552. && (media ? [media] : tracks.some(notRedBook) ? ['WEB'] : ['CD', 'WEB'])
  2553. .some(_media => album.media.map(media => remapMedia(media.format)).includes(_media)
  2554. && releasesMatch(album['artist-credit'].map(artist => artist.name), album.title, strict));
  2555. });
  2556.  
  2557. function remapMedia(MBmedia) {
  2558. return [
  2559. ['Digital Media', 'WEB'],
  2560. ].reduce((acc, subst) => acc.toLowerCase() == subst[0].toLowerCase() ? subst[1] : acc, MBmedia);
  2561. }
  2562. }
  2563. });
  2564. }).catch(reason => albumTitle && (!media || media == 'WEB') ? queryItunesAPI('search', {
  2565. term: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2566. media: 'music',
  2567. entity: 'album',
  2568. //country: 'US',
  2569. }).then(function(result) {
  2570. if (result.resultCount <= 0) return Promise.reject('Apple Music: no matches');
  2571. var f = filter(true);
  2572. if (f.length > 1) return Promise.reject('Apple Music: ambiguity');
  2573. if (f.length < 1) {
  2574. f = filter(false);
  2575. if (f.length > 1) return Promise.reject('Apple Music: ambiguity');
  2576. if (f.length < 1) return Promise.reject('Apple Music: no matches');
  2577. console.debug(`Apple Music fuzzy match: "${release.artist}" == "${f[0].artistName}", "${release.album}" == "{f[0].collectionName}"`);
  2578. }
  2579. info('Apple Music', f[0].collectionViewUrl, f[0].collectionId);
  2580. return Promise.resolve(f[0].collectionViewUrl);
  2581.  
  2582. function filter(strict) {
  2583. return result.results.filter(function(result) {
  2584. var isSingle = result.collectionName.endsWith(' - Single');
  2585. if (isSingle) result.collectionName = result.collectionName.slice(0, -9);
  2586. isSingle = isSingle || result.collectionType == 'Single';
  2587. return (releasesMatch(result.artistName, result.collectionName, strict))
  2588. && (release.release_type == getReleaseIndex('Single')) == isSingle;
  2589. });
  2590. }
  2591. }) : Promise.reject('Insufficient information')).catch(reason => albumTitle && (!media || ['CD', 'WEB'].includes(media))
  2592. && (!release.totaldiscs || release.totaldiscs < 2) ? queryDeezerAPI('search', {
  2593. q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
  2594. strict: 'on',
  2595. order: 'RANKING',
  2596. }).then(function(result) {
  2597. if (result.total <= 0) return Promise.reject('Deezer: no matches');
  2598. var f = filter(true);
  2599. if (f.length > 1) return Promise.reject('Deezer: ambiguity');
  2600. if (f.length < 1) {
  2601. f = filter(false);
  2602. if (f.length > 1) return Promise.reject('Deezer: ambiguity');
  2603. if (f.length < 1) return Promise.reject('Deezer: no matches');
  2604. }
  2605. info('Deezer', deezerAlbumPrefix + f[0].id, f[0].id);
  2606. return Promise.resolve('https://api.deezer.com/album/' + f[0].id);
  2607.  
  2608. function filter(strict) {
  2609. var albums = new Map();
  2610. result.data.forEach(function(match) {
  2611. if (!releasesMatch(match.artist.name, match.album.title, strict)) return;
  2612. if (!albums.has(match.album.id)) albums.set(match.album.id, match.album);
  2613. });
  2614. return Array.from(albums.values());
  2615. }
  2616. }) : Promise.reject('Insufficient information')).catch(function(reason) {
  2617. if (!albumTitle && !barCode || !media) return Promise.reject('Insufficient information');
  2618. var query = { type: 'release' };
  2619. if (barCode) query.barcode = barCode; else {
  2620. query.artist = '"' + release.artist + '"';
  2621. query.release_title = '"' + release.album + '"';
  2622. //if (release.catalogs.length > 0) query.catno = release.catalogs.join('; ');
  2623. }
  2624. return queryDiscogsAPI('database/search', query).then(function(result) {
  2625. if (result.results.length <= 0) return Promise.reject('Discogs: no matches');
  2626. if (barCode) {
  2627. if (result.results.length > 1) return Promise.reject('Discogs: ambiguity');
  2628. var f = result.results;
  2629. } else {
  2630. f = filter(true);
  2631. if (f.length <= 0) return Promise.reject('Discogs: no matches');
  2632. if (f.length > 1) return Promise.reject('Discogs: ambiguity');
  2633. }
  2634. info('Discogs', discogsOrigin + f[0].uri. f[0].id);
  2635. return f[0].resource_url;
  2636.  
  2637. function filter(caseless) {
  2638. var title2 = caseless ? release.artist.toLowerCase() + ' - ' + release.album.toLowerCase()
  2639. : release.artist + ' - ' + release.album;
  2640. return result.results.filter(function(album) {
  2641. return (caseless ? album.title.toLowerCase() : album.title).replace(/\s+\(\d+\)(?= - )/, '') == title2
  2642. && (!Array.isArray(album.format) || album.format.some(format => dcFmtToGazelle(format) === media));
  2643. })
  2644. }
  2645. });
  2646. }).catch(reason => albumTitle && (!media || ['CD', 'WEB'].includes(media))
  2647. && (!release.totaldiscs || release.totaldiscs < 2) ? queryLastFmAPI('album.getinfo', {
  2648. artist: (isVA ? VA : release.artist),
  2649. album: release.album,
  2650. }).then(function(result) {
  2651. if (result.error) return Promise.reject(result.message)
  2652. info('Last.fm', result.album.url, result.album.id || result.album.mbid || '#N/A');
  2653. return result.album;
  2654. }) : Promise.reject('Insufficient information')).catch(function(reason) {
  2655. reason = 'online check not performed (no matches for this release)';
  2656. if (prefs.check_integrity_online) addMessage(reason, 'notice');
  2657. return Promise.reject(reason);
  2658. });
  2659.  
  2660. function info(service, url, id) {
  2661. if (prefs.check_integrity_online) addMessage(new HTML('checking online against ' + service +
  2662. ' release ID <a style="color: #00f3ff;" target="_blank" href="' + url + '">' + id + '</a>'), 'info');
  2663. }
  2664. function MB_simpleCheck(result) {
  2665. if (result.count < 1) return Promise.reject('MusicBrainz: no matches');
  2666. //if (result.count > 1) return Promise.reject('MusicBrainz: ambiguity');
  2667. info('MusicBrainz', mbrRlsPrefix + result.releases[0].id, result.releases[0].id);
  2668. return Promise.resolve(mbrRlsPrefix + result.releases[0].id);
  2669. }
  2670. }
  2671.  
  2672. function ruleLink(rule) {
  2673. return ' (<a href="https://redacted.ch/rules.php?p=upload#r' + rule + '" target="_blank">' + rule + '</a>)';
  2674. }
  2675.  
  2676. function releasesMatch(remoteArtist, remoteTitle, strict = true) {
  2677. var localArtist = (isVA ? VA : release.artist.toLowerCase()).toLowerCase();
  2678. if ((typeof remoteArtist == 'string' && localArtist != remoteArtist.toLowerCase()
  2679. || Array.isArray(remoteArtist) && localArtist != remoteArtist.join(', ').toLowerCase()
  2680. && localArtist != remoteArtist.join(' & ').toLowerCase()
  2681. && localArtist != remoteArtist.join(' and ').toLowerCase()
  2682. && localArtist != joinArtists(remoteArtist).toLowerCase())
  2683. && !artists[0].equalTo(Array.isArray(remoteArtist) ? remoteArtist : (remoteArtist = splitArtists(remoteArtist)))
  2684. && !artists[0].equalTo(splitAmpersands(remoteArtist))) return false;
  2685. var localTitle = release.album.toLowerCase();
  2686. if (localTitle == (remoteTitle = remoteTitle.toLowerCase())) return true;
  2687. if (strict) return false;
  2688. return localTitle.includes(remoteTitle) || remoteTitle.includes(localTitle);
  2689. }
  2690.  
  2691. function trackComparer(a, b) {
  2692. var cmp;
  2693. if (release.totaldiscs > 1) {
  2694. cmp = a.discnumber - b.discnumber;
  2695. if (!isNaN(cmp) && cmp != 0) return cmp;
  2696. } else {
  2697. cmp = (a.discsubtitle || '').localeCompare(b.discsubtitle || '');
  2698. //if (cmp != 0) return cmp;
  2699. }
  2700. cmp = parseInt(a.tracknumber) - parseInt(b.tracknumber);
  2701. if (!isNaN(cmp)) return cmp;
  2702. var m1 = vinyltrackParser.exec(a.tracknumber.toUpperCase());
  2703. var m2 = vinyltrackParser.exec(b.tracknumber.toUpperCase());
  2704. return m1 != null && m2 != null ?
  2705. m1[1].localeCompare(m2[1]) || parseFloat(m1[2]) - parseFloat(m2[2]) :
  2706. a.tracknumber.toUpperCase().localeCompare(b.tracknumber.toUpperCase());
  2707. }
  2708.  
  2709. function reqSelectFormats(...vals) {
  2710. vals.forEach(function(val) {
  2711. ['MP3', 'FLAC', 'AAC', 'AC3', 'DTS'].forEach(function(fmt, ndx) {
  2712. if (val.toLowerCase() == fmt.toLowerCase() && (ref = document.getElementById('format_' + ndx)) != null) {
  2713. ref.checked = true;
  2714. ref.onchange();
  2715. }
  2716. });
  2717. });
  2718. }
  2719.  
  2720. function reqSelectBitrates(...vals) {
  2721. vals.forEach(function(val) {
  2722. var ndx = 10;
  2723. [
  2724. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  2725. 'V0 (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  2726. ].forEach((it, _ndx) => { if (val.toString().toLowerCase() == it.toString().toLowerCase()) ndx = _ndx });
  2727. if ((ref = document.getElementById('bitrate_' + ndx)) != null) {
  2728. ref.checked = true;
  2729. ref.onchange();
  2730. }
  2731. });
  2732. }
  2733.  
  2734. function reqSelectMedias(...vals) {
  2735. vals.forEach(function(val) {
  2736. ['CD', 'DVD', 'Vinyl', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB', 'Blu-Ray'].forEach(function(med, ndx) {
  2737. if (val == med && (ref = document.getElementById('media_' + ndx)) != null) {
  2738. ref.checked = true;
  2739. ref.onchange();
  2740. }
  2741. });
  2742. if (val == 'CD') {
  2743. if ((ref = document.getElementById('needlog')) != null) {
  2744. ref.checked = true;
  2745. ref.onchange();
  2746. if ((ref = document.getElementById('minlogscore')) != null) ref.value = 100;
  2747. }
  2748. if ((ref = document.getElementById('needcue')) != null) ref.checked = true;
  2749. //if ((ref = document.getElementById('needchecksum')) != null) ref.checked = true;
  2750. }
  2751. });
  2752. }
  2753.  
  2754. function getReleaseIndex(str) {
  2755. var ndx;
  2756. [
  2757. ['Album', 1],
  2758. ['Soundtrack', 3],
  2759. ['EP', 5],
  2760. ['Anthology', 6],
  2761. ['Compilation', 7],
  2762. ['Single', 9],
  2763. ['Live album', 11],
  2764. ['Remix', 13],
  2765. ['Bootleg', 14],
  2766. ['Interview', 15],
  2767. ['Mixtape', 16],
  2768. ['Demo', 17],
  2769. ['Concert Recording', 18],
  2770. ['DJ Mix', 19],
  2771. ['Unknown', 21],
  2772. ].forEach(k => { if (str.toLowerCase() == k[0].toLowerCase()) ndx = k[1] });
  2773. return ndx || 21;
  2774. }
  2775.  
  2776. function getChanString(n) {
  2777. if (!n) return null;
  2778. const chanmap = [
  2779. 'mono',
  2780. 'stereo',
  2781. '2.1',
  2782. '4.0 surround sound',
  2783. '5.0 surround sound',
  2784. '5.1 surround sound',
  2785. '7.0 surround sound',
  2786. '7.1 surround sound',
  2787. ];
  2788. return n >= 1 && n <= 8 ? chanmap[n - 1] : n + 'chn surround sound';
  2789. }
  2790. } // parseTracks
  2791.  
  2792. function fetchOnline_Music(url, weak = false) {
  2793. if (!urlParser.test(url)) return Promise.reject('Invalid URL');
  2794. const discParser = /^(?:CD|DIS[CK]\s+|VOLUME\s+|DISCO\s+|DISQUE\s+)(\d+)(?:\s+of\s+(\d+))?$/i;
  2795. var ref, dom, artist, album, albumYear, releaseDate, channels, label, composer, bd, sr = 44.1,
  2796. description, compiler, producer, totalTracks, discSubtitle, discNumber, trackNumber, totalDiscs,
  2797. title, trackArtist, catalogue, encoding, format, bitrate, duration, country, media = 'WEB', imgUrl,
  2798. genres = [], trs, tracks = [], identifiers = {}, trackIdentifiers = {};
  2799. if (url.toLowerCase().includes('qobuz.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  2800. method: 'GET', url: url, onload: function(response) {
  2801. prologue(response);
  2802. const error = new Error('Failed to parse Qobus release page');
  2803. var mainArtist;
  2804. if ((ref = dom.querySelector('div.album-meta > h2.album-meta__artist')) == null) throw error;
  2805. artist = ref.title || ref.textContent.trim();
  2806. if ((ref = dom.querySelector('div.album-meta > h1.album-meta__title')) == null) throw error;
  2807. album = ref.title || ref.textContent.trim();
  2808. ref = dom.querySelector('div.album-meta > ul > li:first-of-type');
  2809. if (ref != null) releaseDate = normalizeDate(ref.textContent);
  2810. ref = dom.querySelector('div.album-meta > ul > li:nth-of-type(2) > a');
  2811. if (ref != null) mainArtist = ref.title || ref.textContent.trim();
  2812. //ref = dom.querySelector('p.album-about__copyright');
  2813. //if (ref != null) albumYear = extractYear(ref.textContent);
  2814. dom.querySelectorAll('section#about > ul > li').forEach(function(it) {
  2815. function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) }
  2816. if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1);
  2817. if (/\b(\d+)\s*(?:track|pist[ae]|tracce|traccia)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1);
  2818. if (['Label', 'Etichetta', 'Sello'].some(l => it.textContent.trimLeft().startsWith(l))) {
  2819. label = it.firstElementChild.textContent.replace(/\s+/g, ' ').trim();
  2820. }
  2821. else if (['Composer', 'Compositeur', 'Komponist', 'Compositore', 'Compositor'].some(matchLabel)) {
  2822. composer = it.firstElementChild.textContent.trim();
  2823. if (pseudoArtistParsers.some(rx => rx.test(composer))) composer = undefined;
  2824. } else if (['Genre', 'Genere', 'Género'].some(g => it.textContent.startsWith(g)) && it.childElementCount > 0) {
  2825. genres = Array.from(it.querySelectorAll('a')).map(elem => elem.textContent.trim());
  2826. /*
  2827. if (genres.length >= 1 && ['Pop/Rock'].includes(genres[0])) genres.shift();
  2828. if (genres.length >= 2 && ['Alternative & Indie'].includes(genres[genres.length - 1])) genres.shift();
  2829. if (genres.length >= 1 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) {
  2830. while (genres.length > 1) genres.shift();
  2831. }
  2832. */
  2833. while (genres.length > 1) genres.shift();
  2834. }
  2835. });
  2836. bd = 16; channels = 2; // defaults to CD quality
  2837. dom.querySelectorAll('span.album-quality__info').forEach(function(k) {
  2838. if (/\b([\d\.\,]+)\s*kHz\b/i.test(k.textContent) != null) sr = parseFloat(RegExp.$1.replace(',', '.'));
  2839. if (/\b(\d+)[\-\s]*Bits?\b/i.test(k.textContent) != null) bd = parseInt(RegExp.$1);
  2840. if (/\b(?:Stereo)\b/i.test(k.textContent)) channels = 2;
  2841. if (/\b(\d)\.(\d)\b/.test(k.textContent)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  2842. });
  2843. getDescFromNode('section#description > p', response.finalUrl, true);
  2844. if ((ref = dom.querySelector('a[title="Qobuzissime"]')) != null) {
  2845. if (description) description += '\n';
  2846. description += '[align=center][url=https://www.qobuz.com' + ref.pathname +
  2847. '][img]https://ptpimg.me/4z35uj.png[/img][/url][/align]';
  2848. }
  2849. if ((ref = dom.querySelector('div.album-cover > img')) != null) {
  2850. imgUrl = ref.src.replace(/_\d{3}(?=\.\w+$)/, '_max');
  2851. }
  2852. trs = dom.querySelectorAll('div.player__item > div.player__tracks > div.track > div.track__items');
  2853. if (!totalTracks) totalTracks = trs.length;
  2854. resolve(Array.from(trs).map(function(tr) {
  2855. discSubtitle = discNumber = undefined;
  2856. trackIdentifiers = { TRACK_ID: tr.parentNode.dataset.track };
  2857. if (tr.parentNode.dataset.gtm) try {
  2858. let gtm = JSON.parse(tr.parentNode.dataset.gtm);
  2859. if (gtm.product.id) trackIdentifiers.QOBUZ_ID = gtm.product.id;
  2860. //if (gtm.product.type) trackIdentifiers.RELEASETYPE = gtm.product.type;
  2861. if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory];
  2862. } catch(e) { console.warn(e) }
  2863. if ((ref = tr.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) {
  2864. discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim();
  2865. guessDiscNumber();
  2866. }
  2867. return {
  2868. artist: artist,
  2869. album: album,
  2870. album_year: albumYear,
  2871. release_date: releaseDate,
  2872. label: label,
  2873. encoding: 'lossless',
  2874. codec: 'FLAC',
  2875. bd: bd || undefined,
  2876. sr: sr * 1000 || undefined,
  2877. channels: channels || undefined,
  2878. media: media,
  2879. genre: genres.join('; '),
  2880. discnumber: discNumber,
  2881. totaldiscs: totalDiscs,
  2882. discsubtitle: discSubtitle,
  2883. tracknumber: parseInt(tr.querySelector('span[itemprop="position"]').textContent),
  2884. totaltracks: totalTracks,
  2885. title: (tr.querySelector('div.track__item--name > span') || tr.querySelector('span.track__item--name'))
  2886. .textContent.trim().replace(/\s+/g, ' '),
  2887. composer: composer,
  2888. duration: timeStringToTime(tr.querySelector('span.track__item--duration').textContent),
  2889. url: response.finalUrl,
  2890. description: description,
  2891. identifiers: mergeIds(),
  2892. cover_url: imgUrl,
  2893. };
  2894. }));
  2895. },
  2896. onerror: error => reject(defaultErrorHandler(error)),
  2897. onabort: abort => reject(defaultAbortHandler(abort)),
  2898. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  2899. }));
  2900. else if (url.toLowerCase().includes('highresaudio.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  2901. method: 'GET', url: url, onload: function(response) {
  2902. prologue(response);
  2903. if ((ref = dom.querySelector('h1 > span.artist')) != null) artist = ref.textContent.trim();
  2904. if ((ref = dom.getElementById('h1-album-title')) != null) album = ref.firstChild.textContent.trim();
  2905. dom.querySelectorAll('div.album-col-info-data > div > p').forEach(function(k) {
  2906. if (/\b(?:Genre|Subgenre)\b/i.test(k.firstChild.textContent)) genres.push(k.lastChild.textContent.trim());
  2907. if (/\b(?:Label)\b/i.test(k.firstChild.textContent)) label = k.lastChild.textContent.trim();
  2908. if (/\b(?:Album[\s\-]Release)\b/i.test(k.firstChild.textContent)) {
  2909. albumYear = extractYear(k.lastChild.textContent);
  2910. }
  2911. if (/\b(?:HRA[\s\-]Release)\b/i.test(k.firstChild.textContent)) {
  2912. releaseDate = normalizeDate(k.lastChild.textContent);
  2913. }
  2914. });
  2915. i = 0;
  2916. dom.querySelectorAll('tbody > tr > td.col-format').forEach(function(format) {
  2917. if (!/^(FLAC)\s*(\d+(?:[\.\,]\d+)?)\b/.test(format.textContent)) return;
  2918. format = RegExp.$1;
  2919. sr = parseFloat(RegExp.$2.replace(',', '.'));
  2920. ++i;
  2921. });
  2922. if (i > 1) sr = undefined; // ambiguous
  2923. getDescFromNode('div#albumtab-info > p', response.finalUrl);
  2924. if ((ref = dom.querySelector('div.albumbody > img.cover[data-pin-media]')) != null) imgUrl = ref.dataset.pinMedia;
  2925. trs = dom.querySelectorAll('ul.playlist > li.pltrack');
  2926. resolve(Array.from(trs).map(function(tr) {
  2927. discNumber = undefined; discSubtitle = tr;
  2928. while ((discSubtitle = discSubtitle.previousElementSibling) != null) {
  2929. if (discSubtitle.nodeName == 'LI' && discSubtitle.className == 'plinfo') {
  2930. discSubtitle = discSubtitle.textContent.replace(/\s*:$/, '').trim();
  2931. guessDiscNumber();
  2932. break;
  2933. }
  2934. }
  2935. return {
  2936. artist: artist,
  2937. album: album,
  2938. album_year: albumYear,
  2939. release_date: releaseDate,
  2940. label: label,
  2941. encoding: 'lossless',
  2942. codec: 'FLAC',
  2943. bd: 24,
  2944. sr: sr * 1000,
  2945. media: media,
  2946. genre: genres.join('; '),
  2947. discnumber: discNumber,
  2948. totaldiscs: totalDiscs,
  2949. discsubtitle: discSubtitle || undefined,
  2950. tracknumber: parseInt(tr.querySelector('span.track').textContent),
  2951. totaltracks: trs.length,
  2952. title: tr.querySelector('span.title').textContent.trim().replace(/\s+/g, ' '),
  2953. duration: timeStringToTime(tr.querySelector('span.time').textContent),
  2954. url: response.finalUrl,
  2955. description: description,
  2956. identifiers: mergeIds(),
  2957. cover_url: imgUrl,
  2958. };
  2959. }));
  2960. },
  2961. onerror: error => reject(defaultErrorHandler(error)),
  2962. onabort: abort => reject(defaultAbortHandler(abort)),
  2963. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  2964. }));
  2965. else if (url.toLowerCase().includes('bandcamp.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  2966. method: 'GET', url: url, onload: function(response) {
  2967. prologue(response);
  2968. if ((ref = dom.querySelector('span[itemprop="byArtist"] > a')) != null) artist = ref.textContent.trim();
  2969. if ((ref = dom.querySelector('h2[itemprop="name"]')) != null) album = ref.textContent.trim();
  2970. ref = dom.querySelector('div.tralbum-credits');
  2971. if (ref != null && /\brelease[ds]\s+(.*?\b\d{4})\b/i.test(ref.textContent)) releaseDate = RegExp.$1;
  2972. ref = dom.querySelector('span.back-link-text > br');
  2973. if (ref != null && ref.nextSibling != null) label = ref.nextSibling.textContent.trim(); else {
  2974. ref = dom.querySelector('p#band-name-location > span.title');
  2975. if (ref != null) label = ref.textContent.trim();
  2976. }
  2977. let tags = new TagManager;
  2978. dom.querySelectorAll('div.tralbum-tags > a.tag').forEach(function(tag) {
  2979. if ([
  2980. artist,
  2981. ].every(t => tag.textContent.trim().toLowerCase() != t.toLowerCase())) tags.add(tag.textContent.trim());
  2982. });
  2983. description = [];
  2984. dom.querySelectorAll('div.tralbumData').forEach(function(div) {
  2985. if (!div.classList.contains('tralbum-tags')) description.push(html2php(div, response.finalUrl).trim());
  2986. });
  2987. description = description.filter(p => p).join('\n\n');
  2988. if ((ref = dom.querySelector('div#tralbumArt > a.popupImage')) != null) imgUrl = ref.href;
  2989. trs = dom.querySelectorAll('table.track_list > tbody > tr[itemprop="tracks"]');
  2990. resolve(Array.from(trs).map(tr => ({
  2991. artist: artist,
  2992. album: album,
  2993. //album_year: extractYear(releaseDate),
  2994. release_date: releaseDate,
  2995. label: label,
  2996. media: media,
  2997. genre: tags.toString(),
  2998. discnumber: discNumber,
  2999. totaldiscs: totalDiscs,
  3000. tracknumber: parseInt(tr.querySelector('div.track_number').textContent),
  3001. totaltracks: trs.length,
  3002. title: (tr.querySelector('div.title span.track-title')
  3003. || tr.querySelector('div.title span[itemprop="name"]')).textContent.trim().replace(/\s+/g, ' '),
  3004. duration: (ref = tr.querySelector('span.time')) != null && timeStringToTime(ref.textContent) || undefined,
  3005. url: response.finalUrl,
  3006. description: description,
  3007. identifiers: mergeIds(),
  3008. cover_url: imgUrl,
  3009. })));
  3010. },
  3011. onerror: error => reject(defaultErrorHandler(error)),
  3012. onabort: abort => reject(defaultAbortHandler(abort)),
  3013. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3014. }));
  3015. else if (url.toLowerCase().includes('prestomusic.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3016. method: 'GET', url: url, onload: function(response) {
  3017. prologue(response);
  3018. artist = getArtists(dom.querySelectorAll('div.c-product-block__contributors > p'));
  3019. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3020. if (isVA) artist = [];
  3021. ref = dom.querySelector('h1.c-product-block__title');
  3022. if (ref != null) album = ref.lastChild.textContent.trim();
  3023. dom.querySelectorAll('div.c-product-block__metadata > ul > li').forEach(function(li) {
  3024. if (li.firstChild.textContent.includes('Release Date')) {
  3025. releaseDate = extractYear(li.lastChild.textContent);
  3026. } else if (li.firstChild.textContent.includes('Label')) {
  3027. label = li.lastChild.textContent.trim();
  3028. } else if (li.firstChild.textContent.includes('Catalogue No')) {
  3029. catalogue = li.lastChild.textContent.trim();
  3030. }
  3031. });
  3032. composer = [];
  3033. dom.querySelectorAll('div#related > div > ul > li').forEach(function(li) {
  3034. if (li.parentNode.previousElementSibling.textContent.includes('Composers')) {
  3035. composer.push(li.firstChild.textContent.trim().replace(/^(.*?)\s*,\s+(.*)$/, '$2 $1'));
  3036. }
  3037. });
  3038. composer = composer.join(', ') || undefined;
  3039. genres = undefined;
  3040. if (/\/jazz\//i.test(response.finalUrl)) genres = 'Jazz';
  3041. if (/\/classical\//i.test(response.finalUrl)) genres = 'Classical';
  3042. getDescFromNode('div#about > div > p', response.finalUrl, true);
  3043. if ((ref = dom.querySelector('div.c-product-block__aside > a')) != null) imgUrl = ref.href.replace(/\?\d+$/, '');
  3044. trs = dom.querySelectorAll('div.has--sample');
  3045. trackNumber = 0;
  3046. resolve(Array.from(trs).map(function(tr) {
  3047. discNumber = discSubtitle = undefined;
  3048. var parent = tr;
  3049. if (tr.classList.contains('c-track')) {
  3050. parent = tr.parentNode.parentNode;
  3051. if (parent.classList.contains('c-expander')) parent = parent.parentNode;
  3052. if ((ref = parent.querySelector(':scope > div > div > div > p.c-track__title')) != null) {
  3053. discSubtitle = ref.textContent.trim().replace(/\s+/g, ' ');
  3054. guessDiscNumber();
  3055. }
  3056. }
  3057. trackArtist = getArtists(parent.querySelectorAll(':scope > div.c-track__details > ul > li'));
  3058. if (trackArtist.equalTo(artist)) trackArtist = [];
  3059. return {
  3060. artist: isVA ? VA : artist.join('; '),
  3061. album: album,
  3062. //album_year: extractYear(releaseDate),
  3063. release_date: releaseDate,
  3064. label: label,
  3065. catalog: catalogue,
  3066. media: 'WEB',
  3067. genre: genres,
  3068. discnumber: discNumber,
  3069. totaldiscs: totalDiscs,
  3070. discsubtitle: discSubtitle,
  3071. tracknumber: ++trackNumber,
  3072. totaltracks: trs.length,
  3073. title: (ref = tr.querySelector('p.c-track__title')) ? ref.textContent.trim().replace(/\s+/g, ' ') : undefined,
  3074. track_artist: joinArtists(trackArtist),
  3075. composer: composer,
  3076. duration: timeStringToTime(tr.querySelector('div.c-track__duration').textContent),
  3077. url: response.finalUrl,
  3078. description: description,
  3079. identifiers: mergeIds(),
  3080. };
  3081. }));
  3082.  
  3083. function getArtists(nodeList) {
  3084. var artists = [];
  3085. nodeList.forEach(function(_artists) {
  3086. _artists = _artists.textContent.trim();
  3087. if (_artists.startsWith('Record')) return;
  3088. splitArtists(_artists).forEach(artist => { artists.push(artist.replace(/\s*\([^\(\)]*\)$/, '')) });
  3089. });
  3090. return artists.filter(artist => artist.length > 0);
  3091. }
  3092. },
  3093. onerror: error => reject(defaultErrorHandler(error)),
  3094. onabort: abort => reject(defaultAbortHandler(abort)),
  3095. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3096. }));
  3097. else if (url.toLowerCase().includes('discogs.com/') && /\/releases?\/(\d+)\b/i.test(url)) {
  3098. return queryDiscogsAPI('releases/' + RegExp.$1).then(function(release) {
  3099. const removeArtistNdx = /\s*\(\d+\)$/;
  3100. const editionTest = /^(?:.+?\s+Edition|Remaster(?:ed)|Remasterizado|Remasterisée|Reissue|.+?\s+Release|Enhanced|Promo)$/;
  3101. media = undefined;
  3102. identifiers.DISCOGS_ID = release.id;
  3103. var master = release.master_url ? new Promise((resolve, reject) => GM_xmlhttpRequest({
  3104. method: 'GET',
  3105. url: release.master_url,
  3106. responseType: 'json',
  3107. onload: function(response) {
  3108. if (response.readyState == XMLHttpRequest.DONE && response.status == 200) resolve(response.response);
  3109. else reject(defaultErrorHandler(response));
  3110. },
  3111. })) : Promise.reject('master release not available');
  3112. var albumArtists = getArtists(release);
  3113. if (albumArtists[0].length > 0) {
  3114. artist = albumArtists[0].join('; ');
  3115. if (albumArtists[1].length > 0) artist += ' feat. ' + albumArtists[1].join('; ');
  3116. }
  3117. album = release.title;
  3118. var editions = [];
  3119. label = []; catalogue = [];
  3120. release.labels.forEach(function(it) {
  3121. //if (it.entity_type_name != 'Label') return;
  3122. if (!/^Not On Label\b/i.test(it.name)) label.pushUniqueCaseless(it.name.replace(removeArtistNdx, ''));
  3123. catalogue.pushUniqueCaseless(it.catno);
  3124. });
  3125. description = '';
  3126. if (release.companies && release.companies.length > 0) {
  3127. description = '[b]Companies, etc.[/b]\n';
  3128. let type_names = new Set(release.companies.map(it => it.entity_type_name));
  3129. type_names.forEach(function(type_name) {
  3130. description += '\n' + type_name + ' – ' + release.companies
  3131. .filter(it => it.entity_type_name == type_name)
  3132. .map(function(it) {
  3133. var result = '[url=' + discogsOrigin + '/label/' + it.id + ']' +
  3134. it.name.replace(removeArtistNdx, '') + '[/url]';
  3135. if (it.catno) result += ' – ' + it.catno;
  3136. return result;
  3137. })
  3138. .join(', ');
  3139. });
  3140. }
  3141. if (release.extraartists && release.extraartists.length > 0) {
  3142. if (description) description += '\n\n';
  3143. description += '[b]Credits[/b]\n';
  3144. let roles = new Set(release.extraartists.map(it => it.role));
  3145. roles.forEach(function(role) {
  3146. description += '\n' + role + ' – ' + release.extraartists
  3147. .filter(artist => artist.role == role)
  3148. .map(function(artist) {
  3149. var result = '[url=' + discogsOrigin + '/artist/' + artist.id + ']' +
  3150. (artist.anv || artist.name).replace(removeArtistNdx, '') + '[/url]';
  3151. if (artist.tracks) result += ' (tracks: ' + artist.tracks + ')';
  3152. return result;
  3153. })
  3154. .join(', ');
  3155. });
  3156. }
  3157. if (release.notes) {
  3158. if (description) description += '\n\n';
  3159. description += '[b]Notes[/b]\n\n' + release.notes.trim();
  3160. }
  3161. if (Array.isArray(release.identifiers) && release.identifiers.length > 0) {
  3162. if (description) description += '\n\n';
  3163. description += '[b]Barcode and Other Identifiers[/b]\n';
  3164. release.identifiers.forEach(function(it) {
  3165. description += '\n' + it.type;
  3166. if (it.description) description += ' (' + it.description + ')';
  3167. description += ': ' + it.value;
  3168. });
  3169. }
  3170. [
  3171. ['Single', 'Single'],
  3172. ['EP', 'EP'],
  3173. ['Compilation', 'Compilation'],
  3174. ['Soundtrack', 'Soundtrack'],
  3175. ].forEach(function(k) {
  3176. if (release.formats.every(it => Array.isArray(it.descriptions) && it.descriptions.includesCaseless(k[0]))) {
  3177. identifiers.RELEASETYPE = k[1];
  3178. }
  3179. });
  3180. release.identifiers.forEach(function(id) {
  3181. identifiers[id.type.toUpperCase().replace(/\s*\/\s*/g, '-').replace(/\W/g, '_')] = id.value;
  3182. });
  3183. if (identifiers.BARCODE) identifiers.BARCODE = identifiers.BARCODE;
  3184. release.formats.forEach(function(fmt) {
  3185. if (editionTest.test(fmt.text)) editions.push(fmt.text);
  3186. if (Array.isArray(fmt.descriptions)) fmt.descriptions.forEach(function(desc) {
  3187. if (editionTest.test(desc)) editions.push(desc);
  3188. });
  3189. if (media) return;
  3190. if (/\bFile\b/.test(fmt.name)) {
  3191. media = 'WEB';
  3192. if (['FLAC', 'WAV', 'AIF', 'AIFF', 'AIFC', 'PCM', 'ALAC', 'APE', 'WavPack']
  3193. .some(k => fmt.descriptions.includes(k))) {
  3194. encoding = 'lossless'; format = 'FLAC';
  3195. } else if (fmt.descriptions.includes('AAC')) {
  3196. encoding = 'lossy'; format = 'AAC'; bd = undefined;
  3197. if (/(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
  3198. } else if (fmt.descriptions.includes('MP3')) {
  3199. encoding = 'lossy'; format = 'MP3'; bd = undefined;
  3200. if (/\b(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
  3201. } else if (['DFF', 'DSD'].some(k => fmt.descriptions.includes(k))) {
  3202. encoding = 'lossless';
  3203. } else if (['AMR', 'MP2', 'ogg-vorbis', 'Opus', 'SHN', 'WMA'].some(k => fmt.descriptions.includes(k))) {
  3204. encoding = 'lossy';
  3205. }
  3206. } else media = dcFmtToGazelle(fmt.name) || undefined;
  3207. });
  3208. if (editions.length > 0) album += ' (' + editions.join(' / ') + ')';
  3209. if (Array.isArray(release.images) && release.images[0] && release.images[0].resource_url/*uri*/) {
  3210. imgUrl = release.images[0].resource_url/*uri*/;
  3211. }
  3212. totalTracks = release.tracklist.filter(track => track.type_.toLowerCase() == 'track').length;
  3213. return master.then(enumTracks, function(e) {
  3214. addMessage(e, 'notice');
  3215. return enumTracks({});
  3216. });
  3217.  
  3218. function getArtists(root) {
  3219. function filterArtists(rx, anv = true) {
  3220. return Array.isArray(root.extraartists) && rx instanceof RegExp ?
  3221. root.extraartists.filter(it => rx.test(it.role))
  3222. .map(it => (anv && it.anv || it.name || '').replace(removeArtistNdx, '')) : [];
  3223. }
  3224. var artists = [];
  3225. for (var ndx = 0; ndx < 7; ++ndx) artists[ndx] = [];
  3226. ndx = 0;
  3227. if (root.artists) root.artists.forEach(function(it) {
  3228. artists[ndx].push((it.anv || it.name).replace(removeArtistNdx, ''));
  3229. if (/^feat/i.test(it.join)) ndx = 1;
  3230. });
  3231. return [
  3232. artists[0],
  3233. artists[1].concat(filterArtists(/^(?:featuring)$/i)),
  3234. artists[2].concat(filterArtists(/\b(?:Remixed[\s\-]By|Remixer)\b/i)),
  3235. artists[3].concat(filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, false)),
  3236. artists[4].concat(filterArtists(/\b(?:Conducted[\s\-]By|Conductor)\b/i)),
  3237. artists[5].concat(filterArtists(/\b(?:Compiled[\s\-]By|Compiler)\b/i)),
  3238. artists[6].concat(filterArtists(/\b(?:Produced[\s\-]By|Producer)\b/i)),
  3239. // filter off from performers
  3240. filterArtists(/\b(?:(?:Mixed)[\s\-]By|Mixer)\b/i),
  3241. filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, true),
  3242. ];
  3243. }
  3244.  
  3245. function enumTracks(master) {
  3246. var tags = new TagManager();
  3247. if (release.genres) tags.add(...release.genres);
  3248. if (release.styles) tags.add(...release.styles);
  3249. if (master.genres) tags.add(...master.genres);
  3250. if (master.styles) tags.add(...master.styles);
  3251. release.tracklist.forEach(function(track) {
  3252. switch (track.type_.toLowerCase()) {
  3253. case 'heading':
  3254. discSubtitle = track.title;
  3255. break;
  3256. case 'track': {
  3257. trackIdentifiers = {};
  3258. if (/^([a-zA-Z]+)?(\d+)-(\w+)$/.test(track.position)) {
  3259. if (RegExp.$1) trackIdentifiers.VOL_MEDIA = RegExp.$1;
  3260. discNumber = RegExp.$2;
  3261. trackNumber = RegExp.$3;
  3262. } else {
  3263. discNumber = undefined;
  3264. trackNumber = track.position;
  3265. }
  3266. let trackArtists = getArtists(track);
  3267. if (trackArtists[0].length > 0 && !trackArtists[0].equalTo(albumArtists[0])
  3268. || trackArtists[1].length > 0 && !trackArtists[1].equalTo(albumArtists[1])) {
  3269. trackArtist = (trackArtists[0].length > 0 ? trackArtists : albumArtists)[0].join('; ');
  3270. if (trackArtists[1].length > 0) trackArtist += ' feat. ' + trackArtists[1].join('; ');
  3271. } else trackArtist = null;
  3272. let performer = Array.isArray(track.extraartists) && track.extraartists
  3273. .map(artist => (artist.anv || artist.name).replace(removeArtistNdx, ''))
  3274. .filter(function(artist) {
  3275. return !albumArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
  3276. && !trackArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
  3277. });
  3278. tracks.push({
  3279. artist: artist,
  3280. album: album,
  3281. album_year: master.year,
  3282. release_date: release.released,
  3283. label: label.join(' / '),
  3284. catalog: catalogue.join(' / '),
  3285. country: release.country,
  3286. encoding: encoding,
  3287. codec: format,
  3288. bitrate: bitrate,
  3289. bd: bd,
  3290. media: media,
  3291. genre: tags.toString(),
  3292. discnumber: discNumber,
  3293. totaldiscs: release.format_quantity,
  3294. discsubtitle: discSubtitle,
  3295. tracknumber: trackNumber,
  3296. totaltracks: totalTracks,
  3297. title: track.title,
  3298. track_artist: trackArtist,
  3299. performer: Array.isArray(performer) && performer.join('; ') || undefined,
  3300. composer: stringyfyRole(3),
  3301. conductor: stringyfyRole(4),
  3302. remixer: stringyfyRole(2),
  3303. compiler: stringyfyRole(5),
  3304. producer: stringyfyRole(6),
  3305. duration: timeStringToTime(track.duration),
  3306. description: description,
  3307. identifiers: mergeIds(),
  3308. cover_url: imgUrl,
  3309. });
  3310.  
  3311. function stringyfyRole(ndx) {
  3312. return (Array.isArray(trackArtists[ndx]) && trackArtists[ndx].length > 0 ?
  3313. trackArtists : albumArtists)[ndx].join('; ');
  3314. }
  3315. }
  3316. }
  3317. });
  3318. return tracks;
  3319. }
  3320. });
  3321. } else if (url.toLowerCase().includes('supraphonline.cz/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3322. method: 'GET', url: url.replace(/\?.*$/, ''), onload: function(response) {
  3323. prologue(response);
  3324. const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
  3325. var ndx, conductor = [], origin = new URL(response.finalUrl).origin;
  3326. genres = undefined; artist = [];
  3327. dom.querySelectorAll('h2.album-artist > a').forEach(function(it) {
  3328. artist.pushUnique(it.title);
  3329. });
  3330. if (artist.length == 0 && (ref = dom.querySelector('h2.album-artist[title]')) != null) {
  3331. isVA = vaParser.test(ref.title);
  3332. }
  3333. ref = dom.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]');
  3334. if (ref != null && vaParser.test(ref.content)) isVA = true;
  3335. if (isVA) artist = [];
  3336. if ((ref = dom.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
  3337. if ((ref = dom.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  3338. if ((ref = dom.querySelector('meta[itemprop="genre"]')) != null) genres = ref.content;
  3339. if ((ref = dom.querySelector('li.album-version > div.selected > div')) != null) {
  3340. if (/\b(?:MP3)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossy'; format = 'MP3'; }
  3341. if (/\b(?:FLAC)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 16; }
  3342. if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 24; }
  3343. if (/\b(?:CD)\b/.test(ref.textContent)) { media = 'CD'; }
  3344. if (/\b(?:LP)\b/.test(ref.textContent)) { media = 'Vinyl'; }
  3345. }
  3346. dom.querySelectorAll('ul.summary > li').forEach(function(it) {
  3347. if (it.childElementCount < 1) return;
  3348. if (it.firstElementChild.textContent.includes('Nosič')) media = it.lastChild.textContent.trim();
  3349. if (it.firstElementChild.textContent.includes('Datum vydání')) releaseDate = normalizeDate(it.lastChild.textContent);
  3350. if (it.firstElementChild.textContent.includes('První vydání')) albumYear = extractYear(it.lastChild.data);
  3351. //if (it.firstElementChild.textContent.includes('Žánr')) genre = it.lastChild.textContent.trim();
  3352. if (it.firstElementChild.textContent.includes('Vydavatel')) label = it.lastChild.textContent.trim();
  3353. if (it.firstElementChild.textContent.includes('Katalogové číslo')) catalogue = it.lastChild.textContent.trim();
  3354. if (it.firstElementChild.textContent.includes('Formát')) {
  3355. if (/\b(?:FLAC|WAV|AIFF?)\b/.test(it.lastChild.textContent)) { encoding = 'lossless'; format = 'FLAC'; }
  3356. if (/\b(\d+)[\-\s]?bits?\b/i.test(it.lastChild.textContent)) bd = parseInt(RegExp.$1);
  3357. if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(it.lastChild.textContent)) sr = parseFloat(RegExp.$1.replace(',', '.'));
  3358. }
  3359. if (it.firstElementChild.textContent.includes('Celková stopáž')) totalTime = timeStringToTime(it.lastChild.textContent.trim());
  3360. if (copyrightParser.test(it.firstElementChild.textContent) && !albumYear) albumYear = extractYear(it.lastChild.data);
  3361. });
  3362. const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
  3363. artists = [];
  3364. for (i = 0; i < 4; ++i) artists[i] = {};
  3365. dom.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
  3366. if ((ref = it.querySelector('h3')) != null) {
  3367. ndx = undefined;
  3368. creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
  3369. } else {
  3370. if (typeof ndx != 'number') return;
  3371. let role;
  3372. if (ndx == 2) role = 'ensemble';
  3373. else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
  3374. if ((ref = it.querySelector('a')) != null) {
  3375. if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
  3376. var href = new URL(ref.href);
  3377. artists[ndx][role].pushUnique([ref.textContent.trim(), origin + href.pathname]);
  3378. }
  3379. }
  3380. });
  3381. getDescFromNode('div[itemprop="description"] p', response.finalUrl, true);
  3382. composer = [];
  3383. var performers = [], DJs = [];
  3384. function dumpArtist(ndx, role) {
  3385. if (!role || role == 'undefined') return;
  3386. if (description.length > 0) description += '\n' ;
  3387. description += '[color=#9576b1]' + role + '[/color] – ';
  3388. //description += artists[ndx][role].map(artist => '[artist]' + artist[0] + '[/artist]').join(', ');
  3389. description += artists[ndx][role].map(artist => '[url=' + artist[1] + ']' + artist[0] + '[/url]').join(', ');
  3390. }
  3391. for (i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
  3392. var a = artists[i][role].map(a => a[0]);
  3393. artist.pushUnique(...a);
  3394. (['conductor', 'choirmaster'].includes(role) ? conductor : role == 'DJ' ? DJs : performers).pushUnique(...a);
  3395. if (i != 2) dumpArtist(i, role);
  3396. });
  3397. Object.keys(artists[0]).forEach(function(role) { // composers
  3398. composer.pushUnique(...artists[0][role].map(it => it[0])
  3399. .filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
  3400. dumpArtist(0, role);
  3401. });
  3402. Object.keys(artists[3]).forEach(role => { dumpArtist(3, role) }); // ADC & mastering
  3403. if ((ref = dom.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content.replace(/\?.*$/, '');
  3404. var promises = [];
  3405. dom.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(row) {
  3406. promises.push(row.id && (ref = row.querySelector('td > a.trackdetail')) != null ? new Promise(function(resolve, reject) {
  3407. var id = parseInt(row.id.replace(/^track-/i, ''));
  3408. GM_xmlhttpRequest({
  3409. method: 'GET',
  3410. url: origin + ref.pathname + ref.search,
  3411. context: id,
  3412. onload: function(response) {
  3413. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) return reject(defaultErrorHandler(response));
  3414. var domDetail = domParser.parseFromString(response.responseText, 'text/html');
  3415. var track = domDetail.getElementById('track-' + response.context);
  3416. if (track != null) {
  3417. resolve([track, domDetail.querySelector('div[data-swap="trackdetail-' + response.context + '"] > div > div.row')]);
  3418. } else reject('Track detail not located');
  3419. },
  3420. onerror: error => reject(defaultErrorHandler(error)),
  3421. onabort: abort => reject(defaultAbortHandler(abort)),
  3422. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  3423. });
  3424. }) : Promise.resolve([row, null]));
  3425. });
  3426. return Promise.all(promises).then(function(rows) {
  3427. rows.forEach(function(tr) {
  3428. if (!(tr[0] instanceof HTMLElement)) throw new Error('Assertion failed: tr[0] != HTMLElement');
  3429. if (tr[0].id && tr[0].classList.contains('track')) {
  3430. tr[2] = [];
  3431. for (i = 0; i < 8; ++i) tr[2][i] = [];
  3432. if (!(tr[1] instanceof HTMLElement)) return;
  3433. tr[1].querySelectorAll('div[class]:nth-of-type(2) > ul > li > span').forEach(function(li) {
  3434. function oneOf(...arr) { return arr.some(role => key == role) }
  3435. var key = translateRole(li);
  3436. var val = li.nextElementSibling.textContent.trim();
  3437. if (pseudoArtistParsers.some(rx => rx.test(val))) return;
  3438. if (key.startsWith('remix')) {
  3439. tr[2][2].pushUnique(val);
  3440. } else if (oneOf('music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto', 'music improvisation', 'author')) {
  3441. tr[2][3].pushUnique(val);
  3442. } else if (oneOf('conductor', 'choirmaster')) {
  3443. tr[2][4].pushUnique(val);
  3444. } else if (key == 'DJ') {
  3445. tr[2][5].pushUnique(val);
  3446. } else if (key == 'produced by') {
  3447. tr[2][6].pushUnique(val);
  3448. } else if (key == 'recorded by') {
  3449. } else {
  3450. tr[2][7].pushUnique(val);
  3451. }
  3452. });
  3453. }
  3454. });
  3455. var guests = rows.filter(tr => tr.length >= 3).map(it => it[2][7])
  3456. .reduce((acc, trpf) => trpf.filter(trpf => acc.includes(trpf)))
  3457. .filter(it => !artist.includes(it));
  3458. rows.forEach(function(tr) {
  3459. if (tr[0].classList.contains('cd-header')) {
  3460. discNumber = /\b\d+\b/.test(tr[0].querySelector('h3').firstChild.data.trim())
  3461. && parseInt(RegExp.lastMatch) || undefined;
  3462. }
  3463. if (tr[0].classList.contains('song-header')) discSubtitle = tr[0].firstElementChild.title.trim() || undefined;
  3464. if (tr[0].id && tr[0].classList.contains('track')) {
  3465. var copyright, trackGenre, trackYear, recordPlace, recordDate, trackIdentifiers = {};
  3466. if (/^track-(\d+)$/i.test(tr[0].id)) trackIdentifiers.TRACK_ID = RegExp.$1;
  3467. if (tr[1] instanceof HTMLElement) {
  3468. tr[1].querySelectorAll('div[class]:nth-of-type(1) > ul > li > span').forEach(function(li) {
  3469. if (li.textContent.startsWith('Nahrávka dokončena')) {
  3470. trackIdentifiers.RECYEAR = extractYear(recordDate = li.nextSibling.data.trim());
  3471. }
  3472. if (li.textContent.startsWith('Místo nahrání')) {
  3473. recordPlace = li.nextSibling.data.trim();
  3474. }
  3475. if (li.textContent.startsWith('Rok prvního vydání')) {
  3476. trackIdentifiers.PUBYEAR = (trackYear = parseInt(li.nextSibling.data));
  3477. }
  3478. //if (copyrightParser.test(li.textContent)) copyright = li.nextSibling.data.trim();
  3479. if (li.textContent.startsWith('Žánr')) trackGenre = li.nextSibling.data.trim();
  3480. });
  3481. }
  3482. if (!isVA && tr[2][0].equalTo(artist)) tr[2][0] = [];
  3483. tracks.push({
  3484. artist: isVA ? VA : artist.join('; '),
  3485. album: album,
  3486. album_year: /*trackYear || */albumYear || undefined,
  3487. release_date: releaseDate,
  3488. label: label,
  3489. catalog: catalogue,
  3490. encoding: encoding,
  3491. codec: format,
  3492. bd: bd,
  3493. sr: sr * 1000,
  3494. media: media,
  3495. genre: translateGenre(genres) + ' | ' + translateGenre(trackGenre),
  3496. discnumber: discNumber,
  3497. totaldiscs: totalDiscs,
  3498. discsubtitle: discSubtitle,
  3499. tracknumber: /^\s*(\d+)\.?\s*$/.test(tr[0].firstElementChild.firstChild.textContent) ?
  3500. parseInt(RegExp.$1) : undefined,
  3501. totaltracks: totalTracks,
  3502. title: tr[0].querySelector('meta[itemprop="name"]').content,
  3503. track_artist: joinArtists(tr[2][0]),
  3504. performer: tr[2][7].join('; ') || performers.join('; '),
  3505. composer: tr[2][3].join(', ') || composer.join(', '),
  3506. conductor: tr[2][4].join('; ') || conductor.join('; '),
  3507. remixer: tr[2][2].join('; '),
  3508. compiler: tr[2][5].join('; ') || DJs.join('; '),
  3509. producer: tr[2][6].join('; '),
  3510. duration: durationFromMeta(tr[0]),
  3511. url: response.finalUrl,
  3512. description: description,
  3513. identifiers: mergeIds(),
  3514. cover_url: imgUrl,
  3515. });
  3516. }
  3517. });
  3518. resolve(tracks);
  3519. });
  3520.  
  3521. function translateGenre(genre) {
  3522. if (!genre || typeof genre != 'string') return undefined;
  3523. [
  3524. ['Orchestrální hudba', 'Orchestral Music'],
  3525. ['Komorní hudba', 'Chamber Music'],
  3526. ['Vokální', 'Classical, Vocal'],
  3527. ['Klasická hudba', 'Classical'],
  3528. ['Melodram', 'Classical, Melodram'],
  3529. ['Symfonie', 'Symphony'],
  3530. ['Vánoční hudba', 'Christmas Music'],
  3531. [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
  3532. ['Dechová hudba', 'Brass Music'],
  3533. ['Elektronika', 'Electronic'],
  3534. ['Folklor', 'Folclore, World Music'],
  3535. ['Instrumentální hudba', 'Instrumental'],
  3536. ['Latinské rytmy', 'Latin'],
  3537. ['Meditační hudba', 'Meditative'],
  3538. ['Vojenská hudba', 'Military Music'],
  3539. ['Pro děti', 'Children'],
  3540. ['Pro dospělé', 'Adult'],
  3541. ['Mluvené slovo', 'Spoken Word'],
  3542. ['Audiokniha', 'audiobook'],
  3543. ['Humor', 'humour'],
  3544. ['Pohádka', 'Fairy-Tale'],
  3545. ].forEach(function(subst) {
  3546. if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
  3547. || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
  3548. });
  3549. return genre;
  3550. }
  3551. function translateRole(elem) {
  3552. if (!(elem instanceof HTMLElement)) return undefined;
  3553. var role = elem.textContent.trim().toLowerCase().replace(/\s*:.*$/, '');
  3554. [
  3555. [/\b(?:klavír)\b/, 'piano'],
  3556. [/\b(?:housle)\b/, 'violin'],
  3557. [/\b(?:varhany)\b/, 'organ'],
  3558. [/\b(?:cembalo)\b/, 'harpsichord'],
  3559. [/\b(?:trubka)\b/, 'trumpet'],
  3560. [/\b(?:soprán)\b/, 'soprano'],
  3561. [/\b(?:alt)\b/, 'alto'],
  3562. [/\b(?:baryton)\b/, 'baritone'],
  3563. [/\b(?:bas)\b/, 'basso'],
  3564. [/\b(?:syntezátor)\b/, 'synthesizer'],
  3565. [/\b(?:zpěv)\b/, 'vocals'],
  3566. [/^(?:čte|četba)$/, 'narration'],
  3567. ['vypravuje', 'narration'],
  3568. ['komentář', 'commentary'],
  3569. ['hovoří', 'spoken by'],
  3570. ['hovoří a zpívá', 'speaks and sings'],
  3571. ['improvizace', 'improvisation'],
  3572. ['hudební těleso', 'ensemble'],
  3573. ['hudba', 'music'],
  3574. ['text', 'lyrics'],
  3575. ['hudba+text', 'music+lyrics'],
  3576. ['původní text', 'original lyrics'],
  3577. ['český text', 'czech lyrics'],
  3578. ['hudební improvizace', 'music improvisation'],
  3579. ['autor', 'author'],
  3580. ['účinkuje', 'participating'],
  3581. ['řídí', 'conductor'],
  3582. ['dirigent', 'conductor'],
  3583. ['sbormistr', 'choirmaster'],
  3584. ['produkce', 'produced by'],
  3585. ['nahrál', 'recorded by'],
  3586. ['digitální přepis', 'A/D transfer'],
  3587. ].forEach(function(subst) {
  3588. if (typeof subst[0] == 'string' && role.toLowerCase() == subst[0].toLowerCase()
  3589. || subst[0] instanceof RegExp && subst[0].test(role)) role = role.replace(subst[0], subst[1]);
  3590. });
  3591. return role;
  3592. }
  3593. },
  3594. onerror: error => reject(defaultErrorHandler(error)),
  3595. onabort: abort => reject(defaultAbortHandler(abort)),
  3596. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3597. }));
  3598. else if (url.toLowerCase().includes('bontonland.cz/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3599. method: 'GET', url: url, onload: function(response) {
  3600. prologue(response);
  3601. ref = dom.querySelector('div#detailheader > h1');
  3602. if (ref != null && /^(.*?)\s*:\s*(.*)$/.test(ref.textContent.trim())) {
  3603. artist = RegExp.$1;
  3604. isVA = vaParser.test(artist);
  3605. album = RegExp.$2;
  3606. }
  3607. media = 'CD';
  3608. dom.querySelectorAll('table > tbody > tr > td.nazevparametru').forEach(function(it) {
  3609. if (it.textContent.includes('Datum vydání')) {
  3610. releaseDate = normalizeDate(it.nextElementSibling.textContent);
  3611. albumYear = extractYear(it.nextElementSibling.textContent);
  3612. } else if (it.textContent.includes('Nosič / počet')) {
  3613. if (/^(.*?)\s*\/\s*(.*)$/.test(it.nextElementSibling.textContent)) {
  3614. media = RegExp.$1;
  3615. totalDiscs = RegExp.$2;
  3616. }
  3617. } else if (it.textContent.includes('Interpret')) {
  3618. artist = it.nextElementSibling.textContent.trim();
  3619. } else if (it.textContent.includes('EAN')) {
  3620. identifiers.BARCODE = it.nextElementSibling.textContent.trim();
  3621. }
  3622. });
  3623. getDescFromNode('div#detailtabpopis > div[class^="pravy"] > div > p:not(:last-of-type)', response.finalUrl, true);
  3624. if ((ref = dom.querySelector('a.detailzoom')) != null) imgUrl = ref.href;
  3625. const plParser = /^(\d+)(?:\s*[\/\.\-\:\)])?\s+(.*?)(?:\s+((?:(?:\d+:)?\d+:)?\d+))?$/;
  3626. ref = dom.querySelector('div#detailtabpopis > div[class^="pravy"] > div > p:last-of-type');
  3627. if (ref == null) throw new Error('Playlist not located');
  3628. var trackList = html2php(ref, response.finalUrl).trim().split(/[\r\n]+/);
  3629. trackList = trackList.filter(it => plParser.test(it.trim())).map(it => plParser.exec(it.trim()));
  3630. resolve(Array.from(trackList).map(track => ({
  3631. artist: isVA ? VA : artist,
  3632. album: album,
  3633. //album_year: extractYear(releaseDate),
  3634. release_date: releaseDate,
  3635. label: label,
  3636. media: media,
  3637. tracknumber: track[1],
  3638. totaltracks: trackList.length,
  3639. title: track[2],
  3640. duration: timeStringToTime(track[3]),
  3641. url: response.finalUrl,
  3642. description: description,
  3643. identifiers: mergeIds(),
  3644. cover_url: imgUrl,
  3645. })));
  3646. },
  3647. onerror: error => reject(defaultErrorHandler(error)),
  3648. onabort: abort => reject(defaultAbortHandler(abort)),
  3649. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3650. }));
  3651. else if (url.toLowerCase().includes('nativedsd.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3652. method: 'GET', url: url, onload: function(response) {
  3653. prologue(response);
  3654. if ((ref = dom.querySelector('div.the-content > header > h2')) != null) artist = ref.firstChild.data.trim();
  3655. isVA = vaParser.test(artist);
  3656. if ((ref = dom.querySelector('div.the-content > header > h1')) != null) album = ref.firstChild.data.trim();
  3657. if ((ref = dom.querySelector('div.the-content > header > h3')) != null) composer = ref.firstChild.data.trim();
  3658. if ((ref = dom.querySelector('div.the-content > header > h1 > small')) != null) albumYear = extractYear(ref.firstChild.data);
  3659. releaseDate = albumYear; // weak
  3660. ref = dom.querySelector('div#breadcrumbs > div[class] > a:nth-of-type(2)');
  3661. if (ref != null) label = ref.firstChild.data.trim();
  3662. if (label == 'Albums') label = undefined;
  3663. if ((ref = dom.querySelector('h2#sku')) != null) {
  3664. if (/^Catalog Number: (.*)$/m.test(ref.firstChild.textContent)) catalogue = RegExp.$1;
  3665. if (/^ID: (.*)$/m.test(ref.lastChild.textContent)) identifiers.NATIVEDSD_ID = RegExp.$1;
  3666. }
  3667. identifiers.ORIGINALFORMAT = 'DSD';
  3668. getDescFromNode('div.the-content > div.entry > p', response.finalUrl, false);
  3669. if ((ref = dom.querySelector('div#repertoire > div > p')) != null) {
  3670. let repertoire = html2php(ref, url);
  3671. if (description) description += '\n\n';
  3672. let ndx = repertoire.indexOf('\n[b]Track');
  3673. description += (ndx >= 0 ? repertoire.slice(0, ndx) : repertoire).trim().flatten();
  3674. }
  3675. ref = dom.querySelectorAll('div#techspecs > table > tbody > tr');
  3676. if (ref.length > 0) {
  3677. if (description) description += '\n\n';
  3678. description += '[b][u]Tech specs[/u][/b]';
  3679. ref.forEach(function(it) {
  3680. description += '\n[b]'.concat(it.children[0].textContent.trim(), '[/b] ', it.children[1].textContent.trim());
  3681. });
  3682. }
  3683. if ((ref = dom.querySelector('a#album-cover')) != null) imgUrl = ref.href;
  3684. trs = dom.querySelectorAll('div#track-list > table > tbody > tr[id^="track"]');
  3685. resolve(Array.from(trs).map(function(tr) {
  3686. title = undefined;
  3687. trackIdentifiers = { TRACK_ID: tr.id.replace(/^track-/i, '') };
  3688. var trackComposer;
  3689. if ((ref = tr.children[1]) != null) {
  3690. title = ref.firstChild.textContent.trim();
  3691. trackComposer = ref.childNodes[2] && ref.childNodes[2].textContent.trim() || undefined;
  3692. }
  3693. return {
  3694. artist: isVA ? VA : artist,
  3695. album: album,
  3696. album_year: albumYear,
  3697. release_date: releaseDate,
  3698. label: label,
  3699. catalog: catalogue,
  3700. encoding: 'lossless', // encoding
  3701. codec: 'FLAC', // format
  3702. bd: 24,
  3703. sr: 88200,
  3704. media: media,
  3705. genre: genres.join('; '), // 'Jazz'
  3706. discnumber: discNumber,
  3707. totaldiscs: totalDiscs,
  3708. discsubtitle: discSubtitle,
  3709. tracknumber: (ref = tr.firstElementChild.firstElementChild) != null ?
  3710. parseInt(ref.firstChild.data.trim().replace(/\..*$/, '')) : undefined,
  3711. totaltracks: trs.length,
  3712. title: title,
  3713. composer: trackComposer || composer,
  3714. duration: (ref = tr.children[2]) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  3715. url: response.finalUrl,
  3716. description: description,
  3717. identifiers: mergeIds(),
  3718. cover_url: imgUrl,
  3719. };
  3720. }));
  3721.  
  3722. function getArtists(elem) {
  3723. if (elem == null) return undefined;
  3724. var artists = [];
  3725. splitArtists(elem.textContent.trim()).forEach(function(it) {
  3726. artists.push(it.replace(/\s*\([^\(\)]*\)$/, ''));
  3727. });
  3728. return artists.join(', ');
  3729. }
  3730. },
  3731. onerror: error => reject(defaultErrorHandler(error)),
  3732. onabort: abort => reject(defaultAbortHandler(abort)),
  3733. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3734. }));
  3735. else if (url.toLowerCase().includes('junodownload.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3736. method: 'GET', url: url, onload: function(response) {
  3737. prologue(response);
  3738. if (/\/([\d\-]+)\/?$/.test(response.finalUrl)) identifiers.JUNODOWNLOAD_ID = RegExp.$1;
  3739. var productArtist;
  3740. if ((ref = dom.querySelectorAll('div.breadcrumb_text > span:not([class])')).length == 4) {
  3741. artist = Array.from(ref[ref.length - 1].querySelectorAll('a')).map(elem => elem.textContent.trim());
  3742. productArtist = ref[ref.length - 1].textContent.trim();
  3743. } else if ((ref = dom.querySelector('h2.product-artist')) != null) {
  3744. artist = Array.from(ref.querySelectorAll('a')).map(elem => elem.textContent.trim().titleCase());
  3745. productArtist = ref.textContent.trim().titleCase();
  3746. }
  3747. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3748. if (isVA) artist = [];
  3749. if ((ref = dom.querySelector('meta[itemprop="name"]')) != null) album = ref.content.trim();
  3750. if ((ref = dom.querySelector('meta[itemprop="author"]')) != null) label = ref.content.trim();
  3751. if ((ref = dom.querySelector('span[itemprop="datePublished"]')) != null) releaseDate = ref.firstChild.data.trim();
  3752. dom.querySelectorAll('div.mb-3 > strong').forEach(function(it) {
  3753. if (it.textContent.startsWith('Genre')) {
  3754. ref = it;
  3755. while ((ref = ref.nextElementSibling) != null && ref.nodeName == 'A') genres.push(ref.textContent.trim());
  3756. } else if (it.textContent.startsWith('Cat')) {
  3757. if ((ref = it.nextSibling) != null && ref.nodeType == 3) catalogue = ref.data;
  3758. }
  3759. });
  3760. getDescFromNode('div[itemprop="review"]', response.finalUrl);
  3761. if ((ref = dom.querySelector('meta[property="og:image"]')) != null) imgUrl = ref.content;
  3762. trs = dom.querySelectorAll('div.product-tracklist > div[itemprop="track"]');
  3763. resolve(Array.from(trs).map(function(tr) {
  3764. trackIdentifiers = { BPM: tr.children[2].textContent.trim() };
  3765. trackNumber = undefined;
  3766. tr.querySelector('div.track-title').childNodes.forEach(function(n) {
  3767. if (trackNumber || n.nodeType != 3) return;
  3768. trackNumber = n.data.trim().replace(/\s*\..*$/, '');
  3769. });
  3770. trackArtist = (ref = tr.querySelector('meta[itemprop="byArtist"]')) != null ? ref.content : undefined;
  3771. title = (ref = tr.querySelector('span[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  3772. if (title && trackArtist && title.startsWith(trackArtist + ' - ')) title = title.slice(trackArtist.length + 3);
  3773. if (trackArtist && trackArtist == productArtist) trackArtist = undefined;
  3774. return {
  3775. artist: isVA ? VA : artist.join('; '),
  3776. album: album,
  3777. album_year: extractYear(releaseDate),
  3778. release_date: releaseDate,
  3779. label: label,
  3780. catalog: catalogue,
  3781. media: media,
  3782. genre: genres.join('; '),
  3783. discnumber: discNumber,
  3784. totaldiscs: totalDiscs,
  3785. discsubtitle: discSubtitle,
  3786. tracknumber: trackNumber,
  3787. totaltracks: trs.length,
  3788. title: title,
  3789. track_artist: trackArtist,
  3790. duration: durationFromMeta(tr),
  3791. url: !identifiers.JUNODOWNLOAD_ID ? response.finalUrl : undefined,
  3792. description: description,
  3793. identifiers: mergeIds(),
  3794. cover_url: imgUrl,
  3795. };
  3796. }));
  3797. },
  3798. onerror: error => reject(defaultErrorHandler(error)),
  3799. onabort: abort => reject(defaultAbortHandler(abort)),
  3800. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3801. }));
  3802. else if (/\bhdtracks(?:\.\w+)+\//i.test(url)) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3803. method: 'GET', url: url, onload: function(response) {
  3804. prologue(response);
  3805. dom.querySelectorAll('div.album-main-details > ul > li > span').forEach(function(it) {
  3806. if (it.textContent.startsWith('Title')) album = it.nextSibling.data.trim();
  3807. if (it.textContent.startsWith('Artist')) artist = it.nextElementSibling.textContent.trim();
  3808. if (it.textContent.startsWith('Genre')) {
  3809. ref = it;
  3810. while ((ref = ref.nextElementSibling) != null) genres.push(ref.textContent.trim());
  3811. }
  3812. if (it.textContent.startsWith('Label')) label = it.nextElementSibling.textContent.trim();
  3813. if (it.textContent.startsWith('Release Date')) releaseDate = normalizeDate(it.nextSibling.data.trim());
  3814. });
  3815. isVA = vaParser.test(artist);
  3816. if ((ref = dom.querySelector('p.product-image > img')) != null) imgUrl = ref.src;
  3817. trs = dom.querySelectorAll('table#track-table > tbody > tr[id^="track"]');
  3818. resolve(Array.from(trs).map(function(tr) {
  3819. format = tr.querySelector('td:nth-of-type(4) > span').textContent.trim();
  3820. sr = tr.querySelector('td:nth-of-type(5)').textContent.trim().replace(/\/.*/, '');
  3821. if (/^([\d\.\,]+)\s*\/\s*(\d+)$/.test(sr)) {
  3822. sr = Math.round(parseFloat(RegExp.$1.replace(',', '.')) * 1000);
  3823. bd = parseInt(RegExp.$2);
  3824. } else sr = Math.round(parseFloat(sr) * 1000);
  3825. return {
  3826. artist: isVA ? VA : artist,
  3827. album: album,
  3828. //album_year: extractYear(releaseDate),
  3829. release_date: releaseDate,
  3830. label: label,
  3831. catalog: catalogue,
  3832. encoding: 'lossless',
  3833. be: bd || 24,
  3834. sr: sr || undefined,
  3835. media: media,
  3836. genre: genres.join('; '),
  3837. //discnumber: discNumber,
  3838. //totaldiscs: totaldiscs,
  3839. //discsubtitle: discSubtitle,
  3840. tracknumber: (ref = tr.querySelector('td:first-of-type')) != null ? parseInt(ref.textContent.trim()) : undefined,
  3841. totaltracks: trs.length,
  3842. title: (ref = tr.querySelector('td.track-name')) != null ? ref.textContent.trim() : undefined,
  3843. duration: (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
  3844. url: response.finalUrl,
  3845. identifiers: mergeIds(),
  3846. cover_url: imgUrl,
  3847. }
  3848. }));
  3849. },
  3850. onerror: error => reject(defaultErrorHandler(error)),
  3851. onabort: abort => reject(defaultAbortHandler(abort)),
  3852. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3853. }));
  3854. else if (/^https?:\/\/(?:\w+\.)?deezer\.com\/(?:\w+\/)*album\/(\d+)/i.test(url)) {
  3855. return queryDeezerAPI('album/' + RegExp.$1).then(function(release) {
  3856. isVA = vaParser.test(release.artist.name);
  3857. identifiers.DEEZER_ID = release.id;
  3858. identifiers.RELEASETYPE = release.record_type;
  3859. if (release.upc) identifiers.BARCODE = release.upc;
  3860. if (release.cover_xl) imgUrl = release.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0');
  3861. return release.tracks.data.map(function(track, ndx) {
  3862. trackIdentifiers = { TRACK_ID: track.id };
  3863. trackArtist = track.artist.name;
  3864. if (!isVA && trackArtist && trackArtist == release.artist.name) trackArtist = undefined;
  3865. return {
  3866. artist: isVA ? VA : release.artist.name,
  3867. album: release.title,
  3868. release_date: release.release_date,
  3869. label: release.label,
  3870. media: media,
  3871. genre: release.genres.data.map(it => it.name).join('; '),
  3872. tracknumber: ndx + 1,
  3873. totaltracks: release.nb_tracks,
  3874. title: track.title,
  3875. track_artist: trackArtist,
  3876. duration: track.duration,
  3877. //url: deezerAlbumPrefix + release.id,
  3878. identifiers: mergeIds(),
  3879. cover_url: imgUrl,
  3880. };
  3881. });
  3882. });
  3883. } else if (url.toLowerCase().includes('spotify.com/') && /\/albums?\/(\w+)$/i.test(url)) {
  3884. return querySpotifyAPI('albums/' + RegExp.$1).then(function(release) {
  3885. artist = release.artists.map(artist => artist.name);
  3886. isVA = release.artists.length == 0 || release.artists.length == 1 && vaParser.test(release.artists[0].name);
  3887. totalDiscs = release.tracks.items.reduce((acc, track) => Math.max(acc, track.disc_number), 0);
  3888. identifiers.SPOTIFY_ID = release.id;
  3889. identifiers.RELEASETYPE = release.album_type;
  3890. identifiers.BARCODE = release.external_ids.upc;
  3891. var image = release.images.reduce((acc, image) => image.width * image.height > acc.width * acc.height ? image : acc);
  3892. return release.tracks.items.map(function(track, ndx) {
  3893. trackIdentifiers = {
  3894. TRACK_ID: track.id,
  3895. EXPLICIT: Number(track.explicit),
  3896. };
  3897. trackArtist = track.artists.map(artist => artist.name);
  3898. if (!isVA && trackArtist.equalTo(artist)) trackArtist = [];
  3899. return {
  3900. artist: isVA ? VA : joinArtists(artist),
  3901. album: release.name,
  3902. release_date: release.release_date,
  3903. label: release.label,
  3904. media: media,
  3905. genre: release.genres.join('; '),
  3906. discnumber: track.disc_number,
  3907. totaldiscs: totalDiscs,
  3908. discsubtitle: discSubtitle,
  3909. tracknumber: track.track_number,
  3910. totaltracks: release.total_tracks,
  3911. title: track.name,
  3912. track_artist: joinArtists(trackArtist),
  3913. duration: track.duration_ms / 1000,
  3914. //url: 'https://open.spotify.com/album/' + release.id,
  3915. identifiers: mergeIds(),
  3916. cover_url: image ? image.url : undefined,
  3917. };
  3918. });
  3919. });
  3920. } else if (url.toLowerCase().includes('prostudiomasters.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  3921. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  3922. prologue(response);
  3923. if (/\/page\/(\d+)$/i.test(response.finalUrl)) identifiers.PROSTUDIOMASTERS_ID = RegExp.$1;
  3924. artist = Array.from(dom.querySelectorAll('h2.ArtistName > a')).map(node => node.textContent.trim());
  3925. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3926. if (isVA) artist = [];
  3927. if ((ref = dom.querySelector('h3.AlbumName')) != null) album = ref.textContent.trim();
  3928. if ((ref = dom.querySelector('div.pline')) != null
  3929. && /^(?:[℗©]\s*)+(\d{4})\s+(.+)/.test(ref.textContent.trim())) {
  3930. releaseDate = RegExp.$1;
  3931. label = RegExp.$2;
  3932. }
  3933. getDescFromNode('div.album-info', response.finalUrl, false);
  3934. if ((ref = dom.querySelector('img.album-art')) != null) imgUrl = ref.currentSrc || ref.src;
  3935. trs = dom.querySelectorAll('div.album-tracks > div.tracks > table > tbody > tr');
  3936. totalTracks = Array.from(trs).filter(tr => tr.classList.contains('track-playable')).length;
  3937. discNumber = 0;
  3938. trs.forEach(function(tr) {
  3939. if (tr.classList.contains('track-playable')) {
  3940. trackArtist = []; sr = bd = format = title = undefined; trackIdentifiers = {};
  3941. if (ref = tr.getAttribute('data-track-id')) trackIdentifiers.TRACK_ID = ref;
  3942. trackNumber = (ref = tr.querySelector('div.num')) != null ? parseInt(ref.firstChild.textContent.trim()) : undefined;
  3943. if (trackNumber == 1) ++discNumber;
  3944. if ((ref = tr.querySelector('td.track-name > div.name')) != null) {
  3945. title = ref.firstChild.textContent.trim();
  3946. if ((ref = ref.querySelector(':scope small')) != null) {
  3947. trackArtist = splitArtists(ref.firstChild.textContent);
  3948. if (!isVA && trackArtist.equalTo(artist)) trackArtist = [];
  3949. }
  3950. }
  3951. if ((ref = tr.querySelector('span.track-format')) != null && /^(\d+(?:[,\.]\d+)?)\s*([kMG]Hz)(?:\s+(\d+)-bit)?\s*\|\s*(\S+)$/i.test(ref.textContent.trim())) {
  3952. sr = parseFloat(RegExp.$1);
  3953. ['khz', 'mhz', 'ghz'].forEach((unit, ndx) => { if (RegExp.$2.toLowerCase() == unit) sr *= 1000 ** (ndx + 1) });
  3954. sr = Math.round(sr) || undefined;
  3955. bd = parseInt(RegExp.$3) || undefined;
  3956. format = RegExp.$4;
  3957. }
  3958. tracks.push({
  3959. artist: isVA ? VA : artist.join('; '),
  3960. album: album,
  3961. //album_year: extractYear(releaseDate),
  3962. release_date: releaseDate,
  3963. label: label,
  3964. catalog: catalogue,
  3965. codec: format,
  3966. bd: bd,
  3967. sr: sr,
  3968. media: media,
  3969. discnumber: discNumber,
  3970. totaldiscs: totalDiscs,
  3971. discsubtitle: discSubtitle,
  3972. tracknumber: trackNumber,
  3973. totaltracks: totalTracks,
  3974. title: title,
  3975. track_artist: joinArtists(trackArtist),
  3976. duration: (ref = tr.querySelector('td:last-of-type')) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  3977. url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
  3978. description: description,
  3979. identifiers: mergeIds(),
  3980. cover_url: imgUrl,
  3981. });
  3982. } else if ((ref = tr.querySelector('div.grouping-title')) != null) {
  3983. discSubtitle = ref.textContent.trim();
  3984. guessDiscNumber();
  3985. }
  3986. });
  3987. resolve(tracks);
  3988. },
  3989. onerror: error => reject(defaultErrorHandler(error)),
  3990. onabort: abort => reject(defaultAbortHandler(abort)),
  3991. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  3992. }));
  3993. /*
  3994. else if (url.toLowerCase().includes('soundcloud.com/') && prefs.soundcloud_clientid) {
  3995. SC.initialize({
  3996. client_id: prefs.soundcloud_clientid,
  3997. redirect_uri: 'https://dont.spam.me/',
  3998. });
  3999. SC.connect().then(function() { return SC.resolve(url) }).then(function(release) {
  4000. isVA = vaParser.test(release.artist.name);
  4001. identifiers.SOUNDCLOUD_ID = release.id;
  4002. identifiers.RELEASETYPE = release.record_type;
  4003. release.tracks.data.forEach(function(track, ndx) {
  4004. trackIdentifiers = { TRACK_ID: track.id };
  4005. trackArtist = track.artist.name;
  4006. if (!isVA && trackArtist && trackArtist == release.artist.name) trackArtist = undefined;
  4007. tracks.push({});
  4008. });
  4009. return tracks;
  4010. });
  4011. return true;
  4012. */
  4013. else if (url.toLowerCase().includes('play.google.com/store/music/album/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4014. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4015. prologue(response);
  4016. var search = new URLSearchParams(new URL(response.finalUrl).search);
  4017. var ID = search.get('id'), trackID, aggregateRating;
  4018. if (ID) identifiers.GOOGLE_ID = ID;
  4019. var root = dom.querySelector('div[itemtype="https://schema.org/MusicAlbum"]');
  4020. if (root == null) throw new Error('Unexpected Google Play metadata structure');
  4021. if ((ref = root.querySelector('div[itemprop="byArtist"]')) != null) {
  4022. artist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
  4023. isVA = artist.length == 1 && vaParser.test(artist[0]);
  4024. }
  4025. if ((ref = root.querySelector('meta[itemprop="name"]')) != null) album = ref.content;
  4026. genres = Array.from(root.querySelectorAll('meta[itemprop="genre"]')).map(elem => elem.content);
  4027. if ((ref = root.querySelector('meta[itemprop="datePublished"]')) != null) releaseDate = ref.content;
  4028. if ((ref = root.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  4029. if ((ref = root.querySelector('meta[itemprop="ratingValue"]')) != null) aggregateRating = parseFloat(ref.content);
  4030. //getDescFromNode('???', response.finalUrl, false);
  4031. if ((ref = dom.querySelector('h1[class][itemprop="name"] > span')) != null
  4032. && (ref = ref.parentNode.parentNode.querySelector('div[class] > span[class]')) != null
  4033. && /\bExplicit/i.test(ref.textContent)) identifiers.EXPLICIT = 1;
  4034. if ((ref = dom.querySelector('span > a[itemprop="genre"]')) != null) try {
  4035. label = ref.parentNode.nextElementSibling.textContent.trim().replace(/^(?:[©℗]|\([cCpP]\))\s*\d{4}\s+/, '');
  4036. } catch(e) {
  4037. console.warn('Unexpected HTML structure (' + e + ')');
  4038. }
  4039. if ((ref = dom.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content;
  4040. var volumes = dom.querySelectorAll('c-wiz > div > h2');
  4041. if (volumes.length <= 0) {
  4042. //dom.querySelectorAll('c-wiz > div > table > tbody > tr[class]').forEach(scanPlaylist);
  4043. trackNumber = 0;
  4044. root.querySelectorAll('div[itemprop="track"]').forEach(function(tr) {
  4045. trackArtist = undefined; trackIdentifiers = {};
  4046. if ((ref = tr.querySelector('meta[itemprop="url"]')) != null) {
  4047. search = new URLSearchParams(new URL(ref.content).search);
  4048. let trackID = search.get('tid');
  4049. if (trackID) trackIdentifiers.TRACK_ID = trackID;
  4050. }
  4051. ++trackNumber;
  4052. title = (ref = tr.querySelector('meta[itemprop="name"]')) != null ? ref.content : undefined;
  4053. if ((ref = tr.querySelector('div[itemprop="byArtist"]')) != null) {
  4054. trackArtist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
  4055. trackArtist = (isVA || !Array.isArray(artist) || !trackArtist.equalTo(artist)) && joinArtists(trackArtist) || undefined;
  4056. }
  4057. duration = durationFromMeta(tr);
  4058. addTrack();
  4059. });
  4060. } else volumes.forEach(function(volume) {
  4061. discNumber = undefined; discSubtitle = volume.textContent.trim();
  4062. guessDiscNumber();
  4063. volume.nextElementSibling.querySelectorAll('tbody > tr[class]').forEach(scanPlaylist);
  4064. });
  4065. resolve(tracks);
  4066.  
  4067. function scanPlaylist(tr) {
  4068. trackNumber = (ref = tr.querySelector('td:nth-of-type(1) > div')) != null ? parseInt(ref.textContent) : undefined;
  4069. title = (ref = tr.querySelector('td[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  4070. duration = (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined;
  4071. trackArtist = Array.from(tr.querySelectorAll('td:nth-of-type(4) > a')).map(it => it.textContent.trim());
  4072. trackArtist = (isVA || !Array.isArray(artist) || !trackArtist.equalTo(artist)) && joinArtists(trackArtist) || undefined;
  4073. addTrack();
  4074. }
  4075. function addTrack() {
  4076. tracks.push({
  4077. artist: isVA ? VA : artist.join('; '),
  4078. album: album,
  4079. //album_year: extractYear(releaseDate),
  4080. release_date: releaseDate,
  4081. label: label,
  4082. catalog: catalogue,
  4083. media: media,
  4084. genre: genres.join('; '),
  4085. discnumber: discNumber,
  4086. totaldiscs: totalDiscs,
  4087. discsubtitle: discSubtitle,
  4088. tracknumber: trackNumber,
  4089. totaltracks: totalTracks,
  4090. title: title,
  4091. track_artist: trackArtist,
  4092. duration: duration,
  4093. url: identifiers.GOOGLE_ID ? undefined : response.finalUrl,
  4094. //description: description,
  4095. identifiers: mergeIds(),
  4096. cover_url: imgUrl,
  4097. });
  4098. }
  4099. },
  4100. onerror: error => reject(defaultErrorHandler(error)),
  4101. onabort: abort => reject(defaultAbortHandler(abort)),
  4102. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4103. }));
  4104. else if (url.toLowerCase().includes('7digital.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4105. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4106. prologue(response);
  4107. if ((ref = dom.querySelector('table.release-track-list')) != null) identifiers['7DIGITAL_ID'] = ref.dataset.releaseid;
  4108. artist = Array.from(dom.querySelectorAll('h2.release-info-artist > span[itemprop="byArtist"] > meta[itemprop="name"]')).map(node => node.content);
  4109. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4110. if (isVA) artist = [];
  4111. if ((ref = dom.querySelector('h1.release-info-title')) != null) album = ref.textContent.trim();
  4112. if ((ref = dom.querySelector('div.release-date-info > p')) != null) releaseDate = normalizeDate(ref.textContent);
  4113. if ((ref = dom.querySelector('div.release-label-info > p')) != null) label = ref.textContent.trim();
  4114. dom.querySelectorAll('dl.release-data > dt.release-data-label').forEach(function(dt) {
  4115. if (/\bGenres?:/.test(dt.textContent)) genres = Array.from(dt.nextElementSibling.querySelectorAll('a')).map(a => a.textContent.trim());
  4116. });
  4117. //getDescFromNode('div.album-info', response.finalUrl, false);
  4118. if ((ref = dom.querySelector('span.release-packshot-image > img[itemprop="image"]')) != null) imgUrl = ref.src;
  4119. totalTracks = dom.querySelectorAll('table.release-track-list > tbody > tr.release-track').length;
  4120. dom.querySelectorAll('table.release-track-list').forEach(function(table) {
  4121. discSubtitle = discNumber = undefined;
  4122. if ((ref = table.querySelector('caption > h4.release-disc-info')) != null) {
  4123. discSubtitle = ref.textContent.trim();
  4124. guessDiscNumber();
  4125. }
  4126. table.querySelectorAll('tbody > tr.release-track').forEach(function(tr) {
  4127. trackIdentifiers = {};
  4128. if (tr.dataset.trackid) trackIdentifiers.TRACK_ID = tr.dataset.trackid;
  4129. tracks.push({
  4130. artist: isVA ? VA : artist.join('; '),
  4131. album: album,
  4132. //album_year: extractYear(releaseDate),
  4133. release_date: releaseDate,
  4134. label: label,
  4135. catalog: catalogue,
  4136. media: media,
  4137. genre: genres.join('; '),
  4138. discnumber: discNumber,
  4139. totaldiscs: totalDiscs,
  4140. discsubtitle: discSubtitle,
  4141. tracknumber: (ref = tr.querySelector('td.release-track-preview > em.release-track-preview-text')) != null ?
  4142. ref.textContent.trim() : undefined,
  4143. totaltracks: totalTracks,
  4144. title: (ref = tr.querySelector('td.release-track-name > meta[itemprop="name"]')) != null ?
  4145. ref.content : undefined,
  4146. duration: durationFromMeta(tr),
  4147. url: !identifiers['7DIGITAL_ID'] ? response.finalUrl : undefined,
  4148. description: description,
  4149. identifiers: mergeIds(),
  4150. cover_url: imgUrl,
  4151. });
  4152. });
  4153. });
  4154. resolve(tracks);
  4155. },
  4156. onerror: error => reject(defaultErrorHandler(error)),
  4157. onabort: abort => reject(defaultAbortHandler(abort)),
  4158. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4159. }));
  4160. else if (url.toLowerCase().includes('e-onkyo.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4161. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4162. prologue(response);
  4163. if (/\/album\/(\w+)\/?$/.test(response.finalUrl)) identifiers.EONKYO_ID = RegExp.$1;
  4164. artist = Array.from(dom.querySelectorAll('div.jacketDetailArea p.artistsName > a'))
  4165. .map(node => node.textContent.trim());
  4166. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4167. if (isVA) artist = [];
  4168. if ((ref = dom.querySelector('div.jacketDetailArea p.packageTtl')) != null) album = ref.textContent.trim();
  4169. if ((ref = dom.querySelector('div.jacketDetailArea p.recordlabelName > a')) != null) label = ref.textContent.trim();
  4170. if ((ref = dom.querySelector('div.jacketDetailArea p.releaseDay > a')) != null) releaseDate = normalizeDate(ref.textContent);
  4171. if ((ref = dom.querySelector('div.jacketDetailArea p.packageNoteDetail')) != null
  4172. && /^\s*(?:\(C\)|©)\s+(\d{4})\b/i.test(ref.lastChild.textContent)) albumYear = parseInt(RegExp.$1);
  4173. //getDescFromNode('div#credit', response.finalUrl, true);
  4174. if (/\s+\(\s*(?:(\d+)[\-\s]*bit)?\s*\/?\s*(?:(\d+(?:\.\d+)?)\s*kHz)?\s*\)\s*$/i.test(album)) {
  4175. album = RegExp.leftContext;
  4176. bd = parseInt(RegExp.$1) || undefined;
  4177. sr = parseFloat(RegExp.$2);
  4178. }
  4179. if ((ref = dom.querySelector('figure > a.colorbox')) != null) {
  4180. imgUrl = new URL(response.finalUrl).origin + ref.pathname;
  4181. }
  4182. trs = dom.querySelectorAll('dl.musicList > dd.musicBox');
  4183. resolve(Array.from(trs).map(tr => ({
  4184. //var trackId = tr.dataset.trackid;
  4185. //if (trackId) trackId = 'TRACK_ID=' + trackId;
  4186. //trackArtist = tr.children[5].textContent.trim();
  4187. //if (trackArtist == artist.join(', ')) trackArtist = undefined;
  4188. artist: isVA ? VA : artist.join('; '),
  4189. album: album,
  4190. album_year: albumYear,
  4191. release_date: releaseDate,
  4192. label: label,
  4193. catalog: catalogue,
  4194. encoding: 'lossless',
  4195. codec: 'FLAC',
  4196. bd: bd,
  4197. sr: sr * 1000 || undefined,
  4198. media: media,
  4199. //discnumber: discNumber,
  4200. //totaldiscs: totalDiscs,
  4201. //discsubtitle: discSubtitle,
  4202. tracknumber: (ref = tr.querySelector('div.musicListNo')) != null ? ref.textContent.trim() : undefined,
  4203. totaltracks: trs.length,
  4204. title: (ref = tr.querySelector('div.musicTtl > span')) != null ? ref.title : undefined,
  4205. duration: (ref = tr.querySelector('div.musicTime')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
  4206. url: !identifiers.EONKYO_ID ? response.finalUrl : undefined,
  4207. description: description,
  4208. identifiers: mergeIds(),
  4209. cover_url: imgUrl,
  4210. })));
  4211. },
  4212. onerror: error => reject(defaultErrorHandler(error)),
  4213. onabort: abort => reject(defaultAbortHandler(abort)),
  4214. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4215. }));
  4216. else if (url.toLowerCase().includes('store.acousticsounds.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4217. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4218. prologue(response);
  4219. if (/\/(\d+)\/$/.test(response.finalUrl)) identifiers.ACOUSTICSOUNDS_ID = RegExp.$1;
  4220. artist = Array.from(dom.querySelectorAll('div > h1 > a')).map(node => node.textContent.trim());
  4221. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4222. if (isVA) artist = [];
  4223. if ((ref = dom.querySelector('div > h1')) != null) album = ref.lastChild.wholeText.trim().replace(/\s*-\s*/, '');
  4224. dom.querySelectorAll('div > p > table > tbody > tr > td:first-of-type').forEach(function(td) {
  4225. if (/^(?:Label):/i.test(td.textContent)) label = td.nextElementSibling.textContent.trim();
  4226. if (/^(?:Genre):/i.test(td.textContent)) genres[0] = td.nextElementSibling.textContent.trim();
  4227. if (/^(?:Product\s+No):/i.test(td.textContent)) catalogue = td.nextElementSibling.textContent.trim();
  4228. if (/^(?:Category):/i.test(td.textContent)
  4229. && /^(.+)\s+(\d+(?:\.\d+)?)\s*kHz(?:\s*\/\s*(\d+)[\s\-]?bit)?\s+Download\b/.test(td.nextElementSibling.textContent.trim())) {
  4230. format = RegExp.$1;
  4231. sr = parseFloat(RegExp.$2) * 1000;
  4232. bd = parseInt(RegExp.$3);
  4233. }
  4234. });
  4235. getDescFromNode('div#description > p', response.finalUrl, true);
  4236. if ((ref = dom.querySelector('div#detail > link[rel="image_src"]')) != null) {
  4237. imgUrl = ref.href.replace(/\/medium\//i, '/large/');
  4238. }
  4239. trs = dom.querySelectorAll('div#tracks > table > tbody > tr');
  4240. trackNumber = 0;
  4241. resolve(Array.from(trs).map(tr => ({
  4242. artist: isVA ? VA : artist.join('; '),
  4243. album: album,
  4244. //album_year: extractYear(releaseDate),
  4245. release_date: releaseDate,
  4246. label: label,
  4247. catalog: catalogue,
  4248. encoding: ['FLAC', 'DSD'].includes(format) ? 'lossless' : undefined,
  4249. codec: format,
  4250. bd: bd,
  4251. sr: sr,
  4252. media: media,
  4253. genre: genres.join('; '),
  4254. //discnumber: discNumber,
  4255. //totaldiscs: totalDiscs,
  4256. //discsubtitle: discSubtitle,
  4257. tracknumber: ++trackNumber,
  4258. totaltracks: trs.length,
  4259. title: (ref = tr.querySelector('td[nowrap]')) != null ? ref.textContent.trim() : undefined,
  4260. url: !identifiers.ACOUSTICSOUNDS_ID ? response.finalUrl : undefined,
  4261. description: description,
  4262. identifiers: mergeIds(),
  4263. cover_url: imgUrl,
  4264. })));
  4265. },
  4266. onerror: error => reject(defaultErrorHandler(error)),
  4267. onabort: abort => reject(defaultAbortHandler(abort)),
  4268. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4269. }));
  4270. else if (url.toLowerCase().includes('indies.eu/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4271. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4272. prologue(response);
  4273. if (/\/alba\/(\d+)\//.test(response.finalUrl)) identifiers.INDIESSCOPE_ID = RegExp.$1;
  4274. ref = dom.querySelector(':root > body > div > div > div > h2');
  4275. if (ref != null) artist = Array.from(ref.childNodes).map(node => node.textContent.trim());
  4276. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4277. if (isVA) artist = [];
  4278. if ((ref = dom.querySelector(':root > body > div > div > div > h1')) != null) album = ref.textContent.trim();
  4279. if ((ref = dom.querySelector('div.infoBox')) != null) {
  4280. let ndx = 0;
  4281. ref.childNodes.forEach(function(child) {
  4282. if (child.nodeName == 'BR') { ++ndx; return; }
  4283. switch (ndx) {
  4284. case 0:
  4285. if (child.nodeType == Node.TEXT_NODE) {
  4286. label = child.wholeText.trim();
  4287. if (/^(.*)\s+\/\s+(\d{4})$/.test(label)) {
  4288. label = RegExp.$1;
  4289. releaseDate = RegExp.$2;
  4290. }
  4291. }
  4292. break;
  4293. case 1:
  4294. if (child.nodeType == Node.ELEMENT_NODE) genres.push(child.textContent.trim());
  4295. break;
  4296. case 2:
  4297. if (child.nodeType == Node.ELEMENT_NODE) catalogue = child.textContent.trim();
  4298. break;
  4299. }
  4300. });
  4301. }
  4302. getDescFromNode('div.popis > section', response.finalUrl, true);
  4303. if ((ref = dom.querySelector('div.obrazekDetail > img')) != null) imgUrl = ref.src;
  4304. trs = dom.querySelectorAll('table.skladby > tbody > tr');
  4305. resolve(Array.from(trs).map(function(tr) {
  4306. title = undefined;
  4307. if ((ref = tr.querySelector('td.nazev')) != null) {
  4308. trackNumber = parseInt(ref.firstChild.wholeText);
  4309. title = ref.querySelector('strong').textContent.trim();
  4310. }
  4311. return {
  4312. artist: isVA ? VA : artist.join('; '),
  4313. album: album,
  4314. //album_year: extractYear(releaseDate),
  4315. release_date: releaseDate,
  4316. label: label,
  4317. catalog: catalogue,
  4318. codec: format,
  4319. media: media,
  4320. genre: genres.join('; '),
  4321. //discnumber: discNumber,
  4322. //totaldiscs: totalDiscs,
  4323. //discsubtitle: discSubtitle,
  4324. tracknumber: trackNumber,
  4325. totaltracks: trs.length,
  4326. title: title,
  4327. duration: (ref = tr.querySelector('td:nth-of-type(4)')) != null ? timeStringToTime(ref.textContent) : undefined,
  4328. identifiers: !identifiers.INDIESSCOPE_ID ? response.finalUrl : undefined,
  4329. description: description,
  4330. identifiers: mergeIds(),
  4331. cover_url: imgUrl,
  4332. };
  4333. }));
  4334. },
  4335. onerror: error => reject(defaultErrorHandler(error)),
  4336. onabort: abort => reject(defaultAbortHandler(abort)),
  4337. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4338. }));
  4339. else if (url.toLowerCase().includes('beatport.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4340. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4341. prologue(response);
  4342. if (/\/release\/(?:\d\/)?(?:\S+-)?(\d+)\b/i.test(response.finalUrl)) identifiers.BEATPORT_ID = RegExp.$1;
  4343. artist = Array.from(dom.querySelectorAll('span > a[data-artist]')).map(node => node.textContent.trim());
  4344. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4345. if (isVA) artist = [];
  4346. if ((ref = dom.querySelector('div > h1')) != null) album = ref.textContent.trim();
  4347. dom.querySelectorAll('ul > li > span.category').forEach(function(span) {
  4348. if (/^(?:Release\s+Date)/i.test(span.textContent)) releaseDate = span.nextElementSibling.textContent.trim();
  4349. if (/^(?:Label)/i.test(span.textContent)) label = span.nextElementSibling.textContent.trim();
  4350. if (/^(?:Catalog)/i.test(span.textContent)) catalogue = span.nextElementSibling.textContent.trim();
  4351. });
  4352. getDescFromNode('div.interior-expandable', response.finalUrl, true);
  4353. if ((ref = dom.querySelector('div > img.interior-release-chart-artwork')) != null) imgUrl = ref.src;
  4354. trs = dom.querySelectorAll('div.tracks > ul > li.track');
  4355. resolve(Array.from(trs).map(function(tr) {
  4356. title = undefined; trackIdentifiers = {};
  4357. if ((ref = tr.querySelector('span.buk-track-primary-title')) != null) {
  4358. title = ref.title || ref.textContent.trim();
  4359. if ((ref = tr.querySelector('span.buk-track-remixed')) != null) title += ' (' + ref.textContent.trim() + ')';
  4360. }
  4361. trackArtist = Array.from(tr.querySelectorAll('p.buk-track-artists > a')).map(a => a.textContent.trim());
  4362. if (!isVA && trackArtist.equalTo(artist)) trackArtist = [];
  4363. if ((ref = tr.querySelector('p.buk-track-bpm')) != null) trackIdentifiers.BPM = ref.textContent;
  4364. return {
  4365. artist: isVA ? VA : artist.join('; '),
  4366. album: album,
  4367. //album_year: extractYear(releaseDate),
  4368. release_date: releaseDate,
  4369. label: (ref = tr.querySelector('p.buk-track-labels')) != null ? ref.textContent.trim() : label,
  4370. catalog: catalogue,
  4371. codec: format,
  4372. media: media,
  4373. genre: Array.from(tr.querySelectorAll('p.buk-track-genre > a')).map(a => a.textContent).join('; '),
  4374. //discnumber: discNumber,
  4375. //totaldiscs: totalDiscs,
  4376. //discsubtitle: discSubtitle,
  4377. tracknumber: (ref = tr.querySelector('div.buk-track-num')) != null ? ref.textContent.trim() : undefined,
  4378. totaltracks: trs.length,
  4379. title: title,
  4380. track_artist: joinArtists(trackArtist),
  4381. remixer: Array.from(tr.querySelectorAll('p.buk-track-remixers > a')).map(a => a.textContent.trim()).join('; '),
  4382. duration: (ref = tr.querySelector('p.buk-track-length')) != null ? timeStringToTime(ref.textContent) : undefined,
  4383. url: !identifiers.BEATPORT_ID ? response.finalUrl : undefined,
  4384. description: description,
  4385. identifiers: mergeIds(),
  4386. cover_url: imgUrl,
  4387. };
  4388. }));
  4389. },
  4390. onerror: error => reject(defaultErrorHandler(error)),
  4391. onabort: abort => reject(defaultAbortHandler(abort)),
  4392. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4393. }));
  4394. else if (url.toLowerCase().includes('traxsource.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4395. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4396. prologue(response);
  4397. if (/\/title\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.TRAXSOURCE_ID = RegExp.$1;
  4398. artist = Array.from(dom.querySelectorAll('h1.artists > a.com-artists')).map(node => node.textContent.trim());
  4399. if (artist.length <= 0 && (ref = dom.querySelector('h1.artists')) != null) artist = [ref.textContent.trim()];
  4400. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4401. if (isVA) artist = [];
  4402. if ((ref = dom.querySelector('h1.title')) != null) album = ref.textContent.trim();
  4403. if ((ref = dom.querySelector('a.com-label')) != null) label = ref.textContent.trim();
  4404. if ((ref = dom.querySelector('div.cat-rdate')) != null) {
  4405. catalogue = ref.textContent.trim();
  4406. if (/\s*\|\s*(\S+)$/.test(catalogue)) {
  4407. catalogue = RegExp.leftContent;
  4408. releaseDate = normalizeDate(RegExp.$1);
  4409. }
  4410. }
  4411. getDescFromNode('div.desc', response.finalUrl, true);
  4412. if ((ref = dom.querySelector('meta[property="og:image"]')) != null) imgUrl = ref.content;
  4413. trs = dom.querySelectorAll('div.trklist > div.trk-row');
  4414. resolve(Array.from(trs).map(function(tr) {
  4415. trackIdentifiers = {};
  4416. title = (ref = tr.querySelector('div.title > a')) != null ? ref.textContent.trim() : undefined;
  4417. if (title && (ref = tr.querySelector('span.version')) != null ) {
  4418. if (ref.firstChild.nodeType == Node.TEXT_NODE
  4419. && (i = ref.firstChild.wholeText.trim()).length > 0) title += ` (${i})`;
  4420. }
  4421. trackArtist = Array.from(tr.querySelectorAll('div.artists a.com-artists')).map(a => a.textContent.trim());
  4422. if (!isVA && trackArtist.equalTo(artist)) trackArtist = [];
  4423. return {
  4424. artist: isVA ? VA : artist.join('; '),
  4425. album: album,
  4426. //album_year: extractYear(releaseDate),
  4427. release_date: releaseDate,
  4428. label: label,
  4429. catalog: catalogue,
  4430. media: media,
  4431. genre: Array.from(tr.querySelectorAll('div.genre > a')).map(a => a.textContent.trim()).join('; '),
  4432. //discnumber: discNumber,
  4433. //totaldiscs: totalDiscs,
  4434. //discsubtitle: discSubtitle,
  4435. tracknumber: (ref = tr.querySelector('div.tnum')) != null ? ref.textContent.trim() : undefined,
  4436. totaltracks: trs.length,
  4437. title: title,
  4438. track_artist: joinArtists(trackArtist),
  4439. remixer: Array.from(tr.querySelectorAll('div.artists a.com-remixers')).map(a => a.textContent.trim()).join('; '),
  4440. duration: (ref = tr.querySelector('span.duration')) != null ? timeStringToTime(ref.textContent) : undefined,
  4441. url: !identifiers.TRAXSOURCE_ID ? response.finalUrl : undefined,
  4442. description: description,
  4443. identifiers: mergeIds(),
  4444. cover_url: imgUrl,
  4445. };
  4446. }));
  4447. },
  4448. onerror: error => reject(defaultErrorHandler(error)),
  4449. onabort: abort => reject(defaultAbortHandler(abort)),
  4450. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4451. }));
  4452. else if (url.toLowerCase().includes('music.apple.com/')) return new Promise((resolve, reject) => GM_xmlhttpRequest({
  4453. method: 'GET', url: url, responseType: 'document', onload: function(response) {
  4454. prologue(response);
  4455. if (/\/(\d+)(?=$|\?)/.test(response.finalUrl)) identifiers.APPLE_ID = RegExp.$1;
  4456. artist = Array.from(dom.querySelectorAll('span.product-header__identity > a.link')).map(a => a.textContent.trim());
  4457. if (artist.length <= 0 && (ref = dom.querySelector('span.product-header__identity')) != null) {
  4458. artist = [ref.textContent.trim()];
  4459. }
  4460. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4461. if ((ref = dom.querySelector('h1 > span.product-header__title')) != null) {
  4462. album = ref.textContent.trim();
  4463. if (album.endsWith(' - Single')) {
  4464. identifiers.RELEASETYPE = 'Single';
  4465. album = album.slice(0, -9);
  4466. }
  4467. }
  4468. genres = Array.from(dom.querySelectorAll('ul.inline-list > li:first-of-type > a')).map(a => a.textContent.trim());
  4469. if ((ref = dom.querySelector('meta[property="music:release_date"]')) != null) releaseDate = ref.content;
  4470. if ((ref = dom.querySelector('li.link-list__item--copyright')) != null) {
  4471. label = ref.textContent.replace(/^.*[©℗]\s+\d{4}\s+/, '');
  4472. }
  4473. description = html2php(dom.querySelector('section.product-hero-desc__section'), response.finalUrl);
  4474. if (description && !description.includes('[quote]')) {
  4475. description = '[quote]' + description.collapseGaps() + '[/quote]';
  4476. }
  4477. discNumber = 0;
  4478. trs = dom.querySelectorAll('table > tbody > tr[id]');
  4479. resolve(Array.from(trs).map(function(tr) {
  4480. trackIdentifiers = {};
  4481. trackNumber = (ref = tr.querySelector('span.table__row__number')) != null ? parseInt(ref.textContent) : undefined;
  4482. if (trackNumber == 1) ++discNumber;
  4483. trackArtist = /*(ref = tr.querySelector('div.table__row__titles > div:last-of-type')) != null ?
  4484. ref.textContent.trim() : */undefined;
  4485. if (!isVA && trackArtist == joinArtists(artist)) trackArtist = undefined;
  4486. if ((ref = tr.querySelector('time.table__row__duration-counter')) != null
  4487. && /^PT(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/.test(ref.dateTime)) {
  4488. duration = (parseInt(RegExp.$1) * 60**2 || 0) + (parseInt(RegExp.$2) * 60 || 0) + (parseInt(RegExp.$3) || 0);
  4489. } else duration = undefined;
  4490. return {
  4491. artist: isVA ? VA : artist.join('; '),
  4492. album: album,
  4493. //album_year: extractYear(releaseDate),
  4494. release_date: releaseDate,
  4495. label: label,
  4496. media: media,
  4497. genre: genres.join('; '),
  4498. discnumber: discNumber,
  4499. tracknumber: trackNumber,
  4500. totaltracks: trs.length,
  4501. title: (ref = tr.querySelector('div.table__row__headline')) != null ? ref.textContent.trim() : undefined,
  4502. track_artist: trackArtist,
  4503. duration: duration,
  4504. description: description,
  4505. url: !identifiers.APPLE_ID ? response.finalUrl : undefined,
  4506. identifiers: mergeIds(),
  4507. };
  4508. }));
  4509. },
  4510. onerror: error => reject(defaultErrorHandler(error)),
  4511. onabort: abort => reject(defaultAbortHandler(abort)),
  4512. ontimeout: timeOut => reject(defaultTimeoutHandler(timeOut)),
  4513. }));
  4514. else if (mbrRlsParser.test(url)) { // MusicBrainz
  4515. var entities = [
  4516. 'aliases', 'annotation', 'artist-credits', 'artists', 'collections', 'discids', 'genres',
  4517. 'isrcs', 'labels', 'media', 'ratings', 'recordings', 'release-groups', 'tags', 'url-rels',
  4518. ];
  4519. return queryMusicBrainzAPI('release/' + RegExp.$1, { inc: entities.join('+') }).then(function(release) {
  4520. if (release.error) return Promise.reject(release.error);
  4521. identifiers.MBID = release.id;
  4522. if (release.barcode) identifiers.BARCODE = release.barcode;
  4523. if (release.asin) identifiers.ASIN = release.asin;
  4524. if (release['release-group']['primary-type']) identifiers.RELEASETYPE = release['release-group']['primary-type'];
  4525. artist = Array.isArray(release['artist-credit']) ? release['artist-credit'].map(artist => artist.name) : [];
  4526. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4527. if (Array.isArray(release.genres)) genres = release.genres.map(genre => genre.name);
  4528. if (Array.isArray(release.tags)) Array.prototype.push.apply(genres, release.tags.map(tag => tag.name));
  4529. if (genres.length <= 0) {
  4530. if (Array.isArray(release['release-group'].genres)) {
  4531. Array.prototype.push.apply(genres, release['release-group'].genres.map(tag => tag.name));
  4532. }
  4533. if (Array.isArray(release['release-group'].tags)) {
  4534. Array.prototype.push.apply(genres, release['release-group'].tags.map(tag => tag.name));
  4535. }
  4536. }
  4537. label = release['label-info'].map(label => label.label.name);
  4538. catalogue = release['label-info'].map(label => label['catalog-number']);
  4539. release.media.forEach(function(medium, ndx) {
  4540. medium.tracks.forEach(function(track, ndx) {
  4541. trackIdentifiers = { TRACK_ID: track.id };
  4542. if (Array.isArray(track['artist-credit'])) {
  4543. trackArtist = track['artist-credit'].map(artist => artist.name);
  4544. trackArtist = trackArtist.length > 0 && (isVA || !trackArtist.equalTo(artist));
  4545. } else trackArtist = false;
  4546. tracks.push({
  4547. artist: isVA ? VA : artist.join(', '),
  4548. album: /*release['release-group'].title || */release.title,
  4549. album_year: extractYear(release['release-group']['first-release-date']),
  4550. release_date: release.date,
  4551. genre: genres.join('; '),
  4552. label: label.filter(label => label).join(' / '),
  4553. catalog: catalogue.filter(catno => catno).join(' / '),
  4554. media: medium.format,
  4555. discnumber: medium.position,
  4556. discsubtitle: medium.title,
  4557. totaldiscs: release.media.length,
  4558. tracknumber: track.number,
  4559. title: track.title,
  4560. track_artist: trackArtist ? track['artist-credit']
  4561. .map(artist => artist.name.concat(artist.joinphrase)).join('') : undefined,
  4562. duration: track.length / 1000,
  4563. //country: release.country,
  4564. description: release.annotation,
  4565. identifiers: mergeIds(),
  4566. });
  4567. });
  4568. });
  4569. return tracks;
  4570. });
  4571. }
  4572. if (!weak) clipBoard.value = '';
  4573. return Promise.reject(new URL(url).hostname + ' not supported');
  4574.  
  4575. function mergeIds() {
  4576. var r = Object.assign(identifiers, trackIdentifiers);
  4577. trackIdentifiers = {};
  4578. return r;
  4579. }
  4580.  
  4581. function getDescFromNode(selector, url, quote = false) {
  4582. description = [];
  4583. dom.querySelectorAll(selector).forEach(function(node) {
  4584. var p = html2php(node, url).trim();
  4585. if (p) description.push(p);
  4586. });
  4587. description = description.join('\n\n');
  4588. if (quote && description.length > 0 && !description.includes('[quote]')) {
  4589. description = '[quote]' + description + '[/quote]';
  4590. }
  4591. }
  4592.  
  4593. function durationFromMeta(elem) {
  4594. var m = elem.querySelector('meta[itemprop="duration"]');
  4595. if (m == null) return undefined;
  4596. if (/^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.test(m.content))
  4597. return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
  4598. m = timeStringToTime(m.content);
  4599. return m != null ? m : undefined;
  4600. }
  4601.  
  4602. function guessDiscNumber() {
  4603. if (discParser.test(discSubtitle)) {
  4604. discSubtitle = undefined;
  4605. discNumber = parseInt(RegExp.$1);
  4606. }
  4607. }
  4608.  
  4609. function prologue(response) {
  4610. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) throw defaultErrorHandler(response);
  4611. dom = domParser.parseFromString(response.responseText, 'text/html');
  4612. }
  4613. } // fetchOnline_Music
  4614.  
  4615. function parseLastFm(album) {
  4616. if (typeof album != 'object') return Promise.reject('invalid object')
  4617. var identifiers = {}, description = [];
  4618. if (album.id) identifiers.LASTFM_ID = album.id;
  4619. if (album.mbid) identifiers.MBID = album.mbid;
  4620. if (album.wiki && album.wiki.summary) description.push(album.wiki.summary);
  4621. if (album.wiki && album.wiki.content) description.push(album.wiki.content);
  4622. var genres = album.tags.tag.map(tag => tag.name);
  4623. description = description.join('\n\n');
  4624. var imgUrl = album.image.filter(image => image.size == /*'extralarge'*/'mega');
  4625. if (imgUrl.length > 0) {
  4626. imgUrl = imgUrl[0]['#text'];
  4627. if (imgUrl) imgUrl = imgUrl.replace(/\/\d+x\d+\//, '/');
  4628. } else imgUrl = undefined;
  4629. return Promise.resolve(album.tracks.track.map((track, ndx) => ({
  4630. artist: album.artist,
  4631. album: album.name,
  4632. genre: genres.join('; ') || undefined,
  4633. title: track.name,
  4634. tracknumber: ndx + 1,
  4635. track_artist: track.artist.name != album.artist ? track.artist.name : undefined,
  4636. duration: parseFloat(track.duration) || undefined,
  4637. url: album.url,
  4638. description: description || undefined,
  4639. identifiers: identifiers,
  4640. cover_url: imgUrl,
  4641. })));
  4642. }
  4643.  
  4644. function joinArtists(arr, decorator = artist => artist) {
  4645. if (!Array.isArray(arr)) return null;
  4646. if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
  4647. if (arr.length < 3) return arr.map(decorator).join(' & ');
  4648. return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
  4649. }
  4650. } // fillFromText_Music
  4651.  
  4652. function fillFromText_Apps(weak = false) {
  4653. if (messages != null) messages.parentNode.removeChild(messages);
  4654. if (!urlParser.test(clipBoard.value)) {
  4655. addMessage('valid URL accepted for this category', 'critical');
  4656. return false;
  4657. }
  4658. sourceUrl = RegExp.$1;
  4659. var description, tags = new TagManager();
  4660. if (sourceUrl.toLowerCase().includes('://sanet')) {
  4661. return globalFetch(sourceUrl).then(function(dom) {
  4662. i = dom.querySelector('h1.item_title > span');
  4663. var title = i == null ? undefined : i.textContent
  4664. .replace(/\s+\((?:x|ia|em)(?:64)\)/ig, ' (64-bit)')
  4665. .replace(/\s+\(x(?:86|32)\)/ig, ' (32-bit)')
  4666. .replace(/\s+(?:Build)\s+(\d+)\b/g, ' build $1')
  4667. .replace(/\s+(?:Multilingual|Multi(?:-|\s)*lang(?:uage)?)\b/g, ' multilingual');
  4668. description = html2php(dom.querySelector('section.descr'), response.finalUrl).trim();
  4669. if (/\s*^[ \t]*(?:\[i\]\[\/i\])?Homepage\s*$.*/im.test(description)) description = RegExp.leftContext;
  4670. description = description.split(/[ \t]*\r?\n/).slice(6).map(line => line.trim()).join('\n')
  4671. .replace(/^[ \t]*(?:\[i\]\[\/i\])?Screenshots:?\s*/igm, '')
  4672. .replace(/^[ \t]*(?:\[i\]\[\/i\])?(\[b\]Release\s+Notes:?\[\/b\])(?:[ \t]*\r?\n)+/igm, '$1\n')
  4673. .replace(/\[hr\]/ig, '\n');
  4674. ref = dom.querySelector('section.descr > div.release-info');
  4675. var releaseInfo = ref != null && ref.textContent.trim();
  4676. if (/\b(?:Languages?)\s*:\s*(.*?)\s*(?:$|\|)/i.exec(releaseInfo) != null) {
  4677. description += '\n\n[b]Languages:[/b]\n' + RegExp.$1;
  4678. }
  4679. if ((ref = dom.querySelector('div.txtleft > a')) != null) {
  4680. let url;
  4681. if (ref.pathname.toLowerCase().startsWith('/confirm/url/') && urlParser.test(ref.text)) {
  4682. url = ref.text.trim();
  4683. } else url = ref.href;
  4684. description += '\n\n[b]Product page:[/b]\n[url]' + removeRedirect(url) + '[/url]';
  4685. }
  4686. writeDescription(description.collapseGaps());
  4687. if ((ref = dom.querySelector('section.descr > div.center > a.mfp-image')) != null) {
  4688. setCover(ref.href);
  4689. } else {
  4690. ref = dom.querySelector('section.descr > div.center > img[data-src]');
  4691. if (ref != null) setCover(ref.dataset.src);
  4692. }
  4693. var internalTags = Array.from(dom.querySelectorAll('ul.item_tags_list > li > a[rel="tag"]'))
  4694. .map(elem => elem.textContent.toLowerCase().trim());
  4695. if ((ref = dom.querySelector('a.cat:last-of-type > span')) != null) {
  4696. if (ref.textContent.toLowerCase() == 'windows') {
  4697. tags.add('apps.windows');
  4698. if (/\b(?:(?:x|ia|em)64)\b/i.test(releaseInfo) || /\(64[-\s]*bit\)/i.test(title)) tags.add('win64');
  4699. if (/\b(?:x86|x32)\b/i.test(releaseInfo) || /\(32[-\s]*bit\)/i.test(title)) tags.add('win32');
  4700. }
  4701. if (ref.textContent.toLowerCase() == 'macos') tags.add('apps.mac');
  4702. if (ref.textContent.toLowerCase() == 'linux' || ref.textContent.toLowerCase() == 'unix') tags.add('apps.linux');
  4703. if (ref.textContent.toLowerCase() == 'android') tags.add('apps.android');
  4704. if (ref.textContent.toLowerCase() == 'ios') tags.add('apps.ios');
  4705. }
  4706. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  4707. if (title && !/\(\d+-?bit\)/i.test(title)) {
  4708. if (tags.includes('win64') && !tags.includes('win32')) title += ' (64-bit)';
  4709. if (tags.includes('win32') && !tags.includes('win64')) title += ' (32-bit)';
  4710. }
  4711. if (elementWritable(ref = document.getElementById('title'))) ref.value = title || '';
  4712. });
  4713. }
  4714. if (!weak) {
  4715. addMessage('this domain not supported', 'critical');
  4716. clipBoard.value = '';
  4717. }
  4718. return Promise.reject('this domain not supported');
  4719. } // fillFromText_Apps
  4720.  
  4721. function fillFromText_Ebooks(weak = false) {
  4722. if (messages != null) messages.parentNode.removeChild(messages);
  4723. if (!urlParser.test(clipBoard.value)) {
  4724. addMessage('only URL accepted for this category', 'critical');
  4725. return Promise.reject('only URL accepted for this category');
  4726. }
  4727. sourceUrl = RegExp.$1;
  4728. var description, tags = new TagManager();
  4729. if (sourceUrl.toLowerCase().includes('martinus.cz') || sourceUrl.toLowerCase().includes('martinus.sk')) {
  4730. return globalFetch(sourceUrl).then(function(dom) {
  4731. function get_detail(x, y) {
  4732. var ref = dom.querySelector('section#details > div > div > div:first-of-type > div:nth-child(' +
  4733. x + ') > dl:nth-child(' + y + ') > dd');
  4734. return ref != null ? ref.textContent.trim() : null;
  4735. }
  4736.  
  4737. i = dom.querySelectorAll('article > ul > li > a');
  4738. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  4739. description = joinAuthors(i);
  4740. if ((i = dom.querySelector('article > h1')) != null) description += ' – ' + i.textContent.trim();
  4741. i = dom.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
  4742. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  4743. ref.value = description;
  4744. }
  4745.  
  4746. ref = dom.querySelector('section#description > div');
  4747. if (ref != null) description = html2php(ref).replace(/^\s*\[img\].*?\[\/img\]\s*/i, '').trim();
  4748. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  4749. const translation_map = [
  4750. [/\b(?:originál)/i, 'Original title'],
  4751. [/\b(?:datum|dátum|rok)\b/i, 'Release date'],
  4752. [/\b(?:katalog|katalóg)/i, 'Catalogue #'],
  4753. [/\b(?:stran|strán)\b/i, 'Page count'],
  4754. [/\bjazyk/i, 'Language'],
  4755. [/\b(?:nakladatel|vydavatel)/i, 'Publisher'],
  4756. [/\b(?:doporuč|ODPORÚČ)/i, 'Age rating'],
  4757. ];
  4758. dom.querySelectorAll('section#details > div > div > div:first-of-type > div > dl').forEach(function(detail) {
  4759. var lbl = detail.children[0].textContent.trim();
  4760. var val = detail.children[1].textContent.trim();
  4761. if (/\b(?:rozm)/i.test(lbl) || /\b(?:vazba|vázba)\b/i.test(lbl)) return;
  4762. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  4763. if (/\b(?:ISBN)\b/i.test(lbl)) {
  4764. let wcUrl = new URL('https://www.worldcat.org/isbn/' + detail.children[1].textContent.trim());
  4765. val = '[url=' + wcUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
  4766. findOCLC(wcUrl);
  4767. // } else if (/\b(?:ISBN)\b/i.test(lbl)) {
  4768. // val = '[url=https://www.goodreads.com/search/search?q=' + detail.children[1].textContent.trim() +
  4769. // '&search_type=books]' + detail.children[1].textContent.trim() + '[/url]';
  4770. }
  4771. description += '\n[b]' + lbl + ':[/b] ' + val;
  4772. });
  4773. sourceUrl = new URL(sourceUrl);
  4774. description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.href + '[/url]';
  4775. writeDescription(description.collapseGaps());
  4776.  
  4777. if ((i = dom.querySelector('a.mj-product-preview > img')) != null) {
  4778. setCover(i.src.replace(/\?.*/, ''));
  4779. } else if ((i = dom.querySelector('head > meta[property="og:image"]')) != null) {
  4780. setCover(i.content.replace(/\?.*/, ''));
  4781. }
  4782.  
  4783. dom.querySelectorAll('dd > ul > li > a').forEach(x => { tags.add(x.textContent) });
  4784. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) {
  4785. ref.value = tags.toString();
  4786. }
  4787. });
  4788. } else if (sourceUrl.toLowerCase().includes('goodreads.com')) {
  4789. return globalFetch(sourceUrl).then(function(dom) {
  4790. i = dom.querySelectorAll('a.authorName > span');
  4791. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  4792. description = joinAuthors(i);
  4793. if ((i = dom.querySelector('h1#bookTitle')) != null) description += ' – ' + i.textContent.trim();
  4794. if ((i = dom.querySelector('div#details > div.row:nth-of-type(2)')) != null
  4795. && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  4796. ref.value = description;
  4797. }
  4798.  
  4799. var description = [];
  4800. dom.querySelectorAll('div#description span:last-of-type').forEach(function(node) {
  4801. description = html2php(node, sourceUrl).trim();
  4802. });
  4803. if (description.length > 0 && !description.includes('[quote]')) {
  4804. description = '[quote]' + description.trim() + '[/quote]';
  4805. }
  4806.  
  4807. function strip(str) {
  4808. return typeof str == 'string' ?
  4809. str.replace(/\s{2,}/g, ' ').replace(/[\n\r]+/, '').replace(/\s*\.{3}(?:less|more)\b/g, '').trim() : null;
  4810. }
  4811.  
  4812. dom.querySelectorAll('div#details > div.row').forEach(k => { description += '\n' + strip(k.innerText) });
  4813. description += '\n';
  4814.  
  4815. dom.querySelectorAll('div#bookDataBox > div.clearFloats').forEach(function(detail) {
  4816. var lbl = detail.children[0].textContent.trim();
  4817. var val = strip(detail.children[1].textContent);
  4818. if (/\b(?:ISBN)\b/i.test(lbl) && (/\b(\d{13})\b/.test(val) || /\b(\d{10})\b/.test(val))) {
  4819. let wcUrl = new URL('https://www.worldcat.org/isbn/' + RegExp.$1);
  4820. val = '[url=' + wcUrl.href + ']' + strip(detail.children[1].textContent) + '[/url]';
  4821. findOCLC(wcUrl);
  4822. }
  4823. description += '\n[b]' + lbl + ':[/b] ' + val;
  4824. });
  4825. if ((ref = dom.querySelector('span[itemprop="ratingValue"]')) != null) {
  4826. description += '\n[b]Rating:[/b] ' + Math.round(parseFloat(ref.firstChild.textContent) * 20) + '%';
  4827. }
  4828. sourceUrl = new URL(sourceUrl);
  4829. // if ((ref = dom.querySelector('div#buyButtonContainer > ul > li > a.buttonBar')) != null) {
  4830. // let u = new URL(ref.href);
  4831. // description += '\n[url=' + sourceUrl.origin + u.pathname + '?' + u.search + ']Libraries[/url]';
  4832. // }
  4833. description += '\n\n[b]More info and reviews:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
  4834. dom.querySelectorAll('div.clearFloats.bigBox').forEach(function(bigBox) {
  4835. if (bigBox.id == 'aboutAuthor' && (ref = bigBox.querySelector('h2 > a')) != null) {
  4836. description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  4837. if ((ref = bigBox.querySelector('div.bigBoxBody a > div[style*="background-image"]')) != null) {
  4838. }
  4839. if ((ref = bigBox.querySelector('div.bookAuthorProfile__about > span[id]:last-of-type')) != null) {
  4840. description += '\n' + html2php(ref, sourceUrl).trim().replace(/^\[i\]Librarian\s+Note:.*?\[\/i\]\s+/i, '');
  4841. }
  4842. } else if ((ref = bigBox.querySelector('h2 > a[href^="/trivia/"]')) != null) {
  4843. description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  4844. if ((ref = bigBox.querySelector('div.bigBoxContent > div.mediumText')) != null) {
  4845. description += '\n' + ref.firstChild.textContent.trim();
  4846. }
  4847. // } else if ((ref = bigBox.querySelector('h2 > a[href^="/work/quotes/"]')) != null) {
  4848. // description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  4849. // bigBox.querySelectorAll('div.bigBoxContent > div.stacked > span.readable').forEach(function(quote) {
  4850. // description += '\n' + ref.firstChild.textContent.trim();
  4851. // });
  4852. }
  4853. });
  4854. writeDescription(description.collapseGaps());
  4855. if ((ref = dom.querySelector('div.editionCover > img')) != null) setCover(ref.src.replace(/\?.*/, ''));
  4856. dom.querySelectorAll('div.elementList > div.left').forEach(tag => { tags.add(tag.textContent.trim()) });
  4857. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  4858. });
  4859. } else if (sourceUrl.toLowerCase().includes('databazeknih.cz')) {
  4860. if (!sourceUrl.toLowerCase().includes('show=alldesc')) {
  4861. if (!sourceUrl.includes('?')) { sourceUrl += '?show=alldesc' } else { sourceUrl += '&show=alldesc' }
  4862. }
  4863. return globalFetch(sourceUrl).then(function(dom) {
  4864. i = dom.querySelectorAll('span[itemprop="author"] > a');
  4865. if (i != null && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  4866. description = joinAuthors(i);
  4867. if ((i = dom.querySelector('h1[itemprop="name"]')) != null) description += ' – ' + i.textContent.trim();
  4868. i = dom.querySelector('span[itemprop="datePublished"]');
  4869. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  4870. ref.value = description;
  4871. }
  4872.  
  4873. ref = dom.querySelector('p[itemprop="description"]');
  4874. if (ref != null) description = html2php(ref, sourceUrl).trim();
  4875. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  4876. const translation_map = [
  4877. [/\b(?:orig)/i, 'Original title'],
  4878. [/\b(?:série)\b/i, 'Series'],
  4879. [/\b(?:vydáno)\b/i, 'Released'],
  4880. [/\b(?:stran)\b/i, 'Page count'],
  4881. [/\b(?:jazyk)\b/i, 'Language'],
  4882. [/\b(?:překlad)/i, 'Translation'],
  4883. [/\b(?:autor obálky)\b/i, 'Cover author'],
  4884. ];
  4885. dom.querySelectorAll('table.bdetail tr').forEach(function(detail) {
  4886. var lbl = detail.children[0].textContent.trim();
  4887. var val = detail.children[1].textContent.trim();
  4888. if (/(?:žánr|\bvazba)\b/i.test(lbl)) return;
  4889. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  4890. if (/\b(?:ISBN)\b/i.test(lbl) && /\b(\d+(?:-\d+)*)\b/.exec(val) != null) {
  4891. let wcUrl = new URL('https://www.worldcat.org/isbn/' + RegExp.$1.replace(/-/g, ''));
  4892. val = '[url=' + wcUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
  4893. findOCLC(wcUrl);
  4894. }
  4895. description += '\n[b]' + lbl + '[/b] ' + val;
  4896. });
  4897.  
  4898. sourceUrl = new URL(sourceUrl);
  4899. description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
  4900. writeDescription(description.collapseGaps());
  4901.  
  4902. if ((ref = dom.querySelector('div#icover_mid > a')) != null) setCover(ref.href.replace(/\?.*/, ''));
  4903. if ((ref = dom.querySelector('div#lbImage')) != null && /\burl\("(.*)"\)/i.test(i.style.backgroundImage)) {
  4904. setCover(RegExp.$1.replace(/\?.*/, ''));
  4905. }
  4906.  
  4907. dom.querySelectorAll('h5[itemprop="genre"] > a').forEach(tag => { tags.add(tag.textContent.trim()) });
  4908. dom.querySelectorAll('a.tag').forEach(tag => { tags.add(tag.textContent.trim()) });
  4909. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  4910. });
  4911. }
  4912. if (!weak) {
  4913. addMessage('domain not supported', 'critical');
  4914. clipBoard.value = '';
  4915. }
  4916. return Promise.reject('domain not supported');
  4917.  
  4918. function joinAuthors(nodeList) {
  4919. if (typeof nodeList != 'object') return null;
  4920. return Array.from(nodeList).map(it => it.textContent.trim()).join(' & ');
  4921. }
  4922.  
  4923. function findOCLC(url) {
  4924. if (!url) return false;
  4925. var oclc = document.querySelector('input[name="oclc"]');
  4926. if (!elementWritable(oclc)) return false;
  4927. globalFetch(url).then(function(dom) {
  4928. var ref = dom.querySelector('tr#details-oclcno > td:last-of-type');
  4929. if (ref != null) oclc.value = ref.textContent.trim();
  4930. });
  4931. return true;
  4932. }
  4933. } // fillFromText_Ebooks
  4934.  
  4935. function preview(n) {
  4936. if (!prefs.auto_preview) return;
  4937. var btn = document.querySelector('input.button_preview_' + n + '[type="button"][value="Preview"]');
  4938. if (btn != null) btn.click();
  4939. }
  4940.  
  4941. function writeDescription(desc) {
  4942. if (typeof desc != 'string') return;
  4943. if (elementWritable(ref = document.querySelector('textarea#desc')
  4944. || document.querySelector('textarea#description'))) ref.value = desc;
  4945. if ((ref = document.getElementById('body')) != null && !ref.disabled) {
  4946. if (ref.value.length > 0) ref.value += '\n\n';
  4947. ref.value += desc;
  4948. }
  4949. }
  4950.  
  4951. function queryItunesAPI(key, params) {
  4952. return queryGenericAPI('itunes.apple.com', key, params);
  4953. }
  4954. function queryDeezerAPI(key, params) {
  4955. return queryGenericAPI('api.deezer.com', key, params);
  4956. }
  4957. function queryDiscogsAPI(key, params) {
  4958. if (prefs.discogs_key && prefs.discogs_secret) {
  4959. var hdr = { Authorization: 'Discogs key=' + prefs.discogs_key + ', secret=' + prefs.discogs_secret };
  4960. } else if (discogs_token) hdr = { Authorization: 'Discogs token=' + discogs_token };
  4961. return queryGenericAPI('api.discogs.com', key, params, hdr);
  4962. }
  4963. function queryMusicBrainzAPI(key, params) {
  4964. return queryGenericAPI('musicbrainz.org', 'ws/2/' + key + '/', Object.assign({ fmt: 'json' }, params));
  4965. }
  4966. function querySpotifyAPI(key, params) {
  4967. return key ? setToken().then(credentials => queryGenericAPI('api.spotify.com', 'v1/' + key, params, {
  4968. 'Authorization': credentials.token_type + ' ' + credentials.access_token,
  4969. })) : Promise.reject('No API expression');
  4970.  
  4971. function setToken() {
  4972. if (isTokenValid()) return Promise.resolve(spotifyCredentials);
  4973. if (!spotify_clientid || !spotify_clientsecret) return Promise.reject('Spotify credentials not configured');
  4974. return new Promise(function(resolve, reject) {
  4975. const data = new URLSearchParams({
  4976. 'grant_type': 'client_credentials',
  4977. });
  4978. GM_xmlhttpRequest({
  4979. method: 'POST',
  4980. url: 'https://accounts.spotify.com/api/token',
  4981. headers: {
  4982. 'Content-Type': 'application/x-www-form-urlencoded',
  4983. 'Content-Length': data.toString().length,
  4984. 'Authorization': 'Basic ' + btoa(spotify_clientid + ':' + spotify_clientsecret),
  4985. },
  4986. responseType: 'json',
  4987. data: data.toString(),
  4988. onload: function(response) {
  4989. if (response.readyState == XMLHttpRequest.DONE && response.status == 200) {
  4990. spotifyCredentials = response.response;
  4991. spotifyCredentials.expires = new Date().getTime() + spotifyCredentials.expires_in;
  4992. if (isTokenValid()) resolve(spotifyCredentials); else reject('Invalid token');
  4993. } else reject('Response error ' + response.status + ' (' + JSON.parse(response.response).error + ')');
  4994. },
  4995. onerror: error => reject(defaultErrorHandler(error)),
  4996. onabort: abort => reject(defaultAbortHandler(abort)),
  4997. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  4998. });
  4999. });
  5000. }
  5001.  
  5002. function isTokenValid() {
  5003. return spotifyCredentials.token_type && spotifyCredentials.token_type
  5004. && spotifyCredentials.access_token && spotifyCredentials.expires >= new Date().getTime() + 30;
  5005. }
  5006. }
  5007. function queryLastFmAPI(method, params) {
  5008. return lastfm_api_key ? queryGenericAPI('ws.audioscrobbler.com', '2.0/', Object.assign({
  5009. method: method,
  5010. api_key: lastfm_api_key,
  5011. format: 'json',
  5012. }, params || {})) : Promise.reject('Last.fm API key not configured');
  5013. }
  5014.  
  5015. function queryGenericAPI(domain, key, params, headers) {
  5016. if (!key) return Promise.reject(new Error('Keyword missing'));
  5017. var retryCount = 0;
  5018. return new Promise(function(resolve, reject) {
  5019. var url = 'https://' + domain + '/' + key;
  5020. var query = new URLSearchParams(params || undefined).toString();
  5021. if (query.length > 0) url += '?' + query;
  5022. if (typeof headers != 'object') headers = {};
  5023. headers.Accept = 'application/json';
  5024. queryInternal();
  5025.  
  5026. function queryInternal() {
  5027. GM_xmlhttpRequest({
  5028. method: 'GET',
  5029. url: url,
  5030. responseType: 'json',
  5031. headers: headers,
  5032. onload: function(response) {
  5033. if (response.status == 503) return http503Handler(1000, response, 'onload');
  5034. if (response.readyState == XMLHttpRequest.DONE || response.status == 200) resolve(response.response);
  5035. else reject(defaultErrorHandler(response));
  5036. },
  5037. onerror: error => error.status == 503 ? http503Handler(1000, error, 'onerror')
  5038. : reject(defaultErrorHandler(error)),
  5039. onabort: abort => reject(defaultAbortHandler(abort)),
  5040. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  5041. });
  5042. }
  5043. function http503Handler(delay, response, event) {
  5044. if (retryCount++ > 10) reject(defaultErrorHandler(response));
  5045. setTimeout(function() { queryInternal() }, delay);
  5046. console.debug('[UA] queryGenericAPI encountered HTTP/503 error for url ' + url + '; event: ' + event);
  5047. }
  5048. });
  5049. }
  5050.  
  5051. function getMusicBrainzCovers(mbrid) {
  5052. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  5053. method: 'GET',
  5054. url: 'https://coverartarchive.org/release/' + mbrid,
  5055. responseType: 'json',
  5056. onload: function(response) {
  5057. if (response.status == 404) return resolve(null);
  5058. if (response.readyState != XMLHttpRequest.DONE || response.status != 200) return reject(defaultErrorHandler(response));
  5059. resolve([response.response.release, response.response.images.filter(function(image) {
  5060. return image.front || Array.isArray(image.types) && image.types.includes('Front');
  5061. }).map(image => image.image)]);
  5062. },
  5063. onerror: error => reject(defaultErrorHandler(error)),
  5064. onabort: abort => reject(defaultAbortHandler(abort)),
  5065. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  5066. }));
  5067. }
  5068.  
  5069. function setCover(url) {
  5070. var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  5071. if (!elementWritable(image)) return Promise.reject('Image input not available');
  5072. return testImageUrl(url).then(function(url) {
  5073. if (!isNWCD) {
  5074. image.value = url;
  5075. coverPreview(image, url);
  5076. if (prefs.auto_rehost_cover && !url.toLowerCase().startsWith(imghostOrigin)) {
  5077. //if (rehostItBtn != null) rehostItBtn.click(); else {
  5078. image.disabled = true;
  5079. rehost2PTPIMG([url])
  5080. .then(urls => urls.length > 0 ? (image.value = urls[0]) : url)
  5081. .catch(reason => { alert(reason) })
  5082. .then(url => { image.disabled = false; return url });
  5083. //}
  5084. }
  5085. return url;
  5086. } else return uploadToImagehost(url).then(function(result) {
  5087. coverPreview(image, image.value = result.url);
  5088. return result.url;
  5089. });
  5090. });
  5091. }
  5092.  
  5093. function elementWritable(elem) {
  5094. return elem != null && !elem.disabled && (overwrite || elem.value == '' || !isRED && elem.value == '---');
  5095. }
  5096. } // fillFromText
  5097.  
  5098. function addMessage(text, cls) {
  5099. switch (cls) {
  5100. case 'info': var prefix = 'Info'; break;
  5101. case 'notice': prefix = 'Notice'; break;
  5102. case 'warning': prefix = 'Warning'; break;
  5103. case 'critical': prefix = 'FATAL'; break;
  5104. default: return null;
  5105. }
  5106. messages = document.getElementById('UA-messages');
  5107. if (messages == null) {
  5108. var ua = document.getElementById('upload assistant');
  5109. if (ua == null) return null;
  5110. messages = document.createElement('TR');
  5111. if (messages == null) return null;
  5112. messages.id = 'UA-messages';
  5113. ua.firstElementChild.append(messages);
  5114.  
  5115. elem = document.createElement('TD');
  5116. if (elem == null) return null;
  5117. elem.colSpan = 2;
  5118. elem.className = 'ua-messages-bg';
  5119. messages.append(elem);
  5120. } else {
  5121. elem = messages.firstElementChild; // tbody
  5122. if (elem == null) return null;
  5123. }
  5124. var div = document.createElement('DIV');
  5125. div.classList.add('ua-messages', 'ua-'.concat(cls));
  5126. div[text instanceof HTML ? 'innerHTML' : 'textContent'] = prefix.concat(': ', text);
  5127. return elem.appendChild(div);
  5128. }
  5129.  
  5130. function defaultErrorHandler(response) {
  5131. var str = 'XHR readyState=' + response.readyState + ', status=' + response.status;
  5132. if (response.statusText) str += ' (' + response.statusText + ')';
  5133. if (response.error) str += ' (' + response.error + ')';
  5134. const e = new Error(str);
  5135. console.error(e);
  5136. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  5137. return e;
  5138. }
  5139. function defaultAbortHandler(response, showMessage) {
  5140. const e = 'XHR: abort';
  5141. console.error(e);
  5142. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  5143. return e;
  5144. }
  5145. function defaultTimeoutHandler(response, showMessage) {
  5146. const e = 'XHR: timeout';
  5147. console.error(e);
  5148. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  5149. return e;
  5150. }
  5151.  
  5152. function setHandlers() {
  5153. if (prefs.cleanup_descriptions) ['form.create_form', 'form.edit_form', 'form#request_form'].forEach(function(sel) {
  5154. if ((ref = document.querySelector(sel)) != null) ref.addEventListener('submit', cleanupDescriptions);
  5155. });
  5156.  
  5157. if ((ref = document.getElementById('yadg_input')) != null) ref.ondrop = clear0;
  5158.  
  5159. if (!isNWCD) {
  5160. if ((ref = document.getElementById('image') || document.querySelector('input[name="image"]')) != null) {
  5161. ref.ondragover = voidDragHandler0;
  5162. ref.ondblclick = imageClear;
  5163. ref.ondrop = imageDropHandler;
  5164. ref.onpaste = imagePasteHandler;
  5165. }
  5166.  
  5167. rehostItBtn = document.querySelector('input.rehost_it_cover[type="button"]');
  5168. if (prefs.dragdrop_patch_to_ptpimgit && rehostItBtn != null) {
  5169. rehostItBtn.dataset.caption = rehostItBtn.value;
  5170. rehostItBtn.ondragover = voidDragHandler0;
  5171. rehostItBtn.ondrop = rehostDropHandler;
  5172. }
  5173. }
  5174. // Now rape OPS upload form, but only gently
  5175. if (isOPS && isUpload && (ref = document.getElementById('remaster')) != null) {
  5176. ref.checked = true;
  5177. if (!isAddFormat && prefs.ops_always_edition) {
  5178. elem = ref.parentNode.parentNode;
  5179. elem.style.display = 'none';
  5180. if ((ref = document.querySelector('span#year_label_not_remaster')) != null) ref.textContent = 'Initial year:';
  5181. if ((ref = document.querySelector('tr#edition_year > td.label')) != null) ref.textContent = 'Edition year:';
  5182. if ((ref = document.querySelector('tr#edition_title > td.label')) != null) ref.textContent = 'Edition title:';
  5183. if ((ref = document.getElementById('label_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  5184. if ((ref = document.getElementById('catalogue_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  5185. document.querySelectorAll('table#edition_information > tbody > tr')
  5186. .forEach(tr => { elem.parentNode.insertBefore(tr, elem) });
  5187. } else Remaster();
  5188. }
  5189.  
  5190. Array.from(document.getElementsByTagName('textarea')).forEach(function(textArea) {
  5191. if (textArea.className == 'ua-input') return;
  5192. textArea.ondragover = voidDragHandler0;
  5193. textArea.ondrop = descDropHandler;
  5194. textArea.onpaste = descPasteHandler;
  5195. });
  5196. }
  5197.  
  5198. function html2php(node, url, tagChain = []) {
  5199. if (!node || typeof node != 'object') return null;
  5200. switch (node.nodeType) {
  5201. case Node.ELEMENT_NODE: {
  5202. let tags = [], _tags = [], text = [];
  5203. for (let i = 0; i < 5; ++i) text[i] = '';
  5204. switch (node.nodeName) {
  5205. case 'P':
  5206. text[0] = '\n'; text[4] = '\n';
  5207. break;
  5208. case 'DIV':
  5209. text[0] = '\n\n'; text[4] = '\n\n';
  5210. break;
  5211. case 'DT':
  5212. text[4] = '\n';
  5213. break;
  5214. case 'DD':
  5215. text[4] = '\n';
  5216. if (isRED) addTag('pad=0|0|0|30'); else text[0] = ' ';
  5217. break;
  5218. case 'LABEL':
  5219. addTag('b');
  5220. text[0] = '\n\n';
  5221. break;
  5222. case 'BR':
  5223. return '\n';
  5224. case 'HR':
  5225. return isRED ? '[hr]' : '\n';
  5226. case 'B': case 'STRONG':
  5227. addTag('b');
  5228. break;
  5229. case 'I': case 'EM': case 'DFN': case 'CITE': case 'VAR':
  5230. addTag('i');
  5231. break;
  5232. case 'U': case 'INS':
  5233. addTag('u');
  5234. break;
  5235. case 'DEL':
  5236. addTag('s');
  5237. break;
  5238. case 'CODE': case 'SAMP': case 'KBD':
  5239. addTag('code');
  5240. text[2] = node.textContent;
  5241. break;
  5242. case 'PRE':
  5243. addTag('pre');
  5244. text[2] = node.textContent;
  5245. break;
  5246. case 'BLOCKQUOTE': case 'QUOTE':
  5247. addTag('quote');
  5248. break;
  5249. case 'Q':
  5250. text[1] = '"'; text[3] = '"';
  5251. break;
  5252. case 'H1':
  5253. addTag('size=5'); addTag('b');
  5254. text[0] = '\n\n'; text[4] = '\n\n';
  5255. break;
  5256. case 'H2':
  5257. addTag('size=4'); addTag('b');
  5258. text[0] = '\n\n'; text[4] = '\n\n';
  5259. break;
  5260. case 'H3':
  5261. addTag('size=3'); addTag('b');
  5262. text[0] = '\n\n'; text[4] = '\n\n';
  5263. break;
  5264. case 'H4': case 'H5': case 'H6':
  5265. addTag('b');
  5266. text[0] = '\n\n'; text[4] = '\n\n';
  5267. break;
  5268. case 'SMALL':
  5269. addTag('size=1');
  5270. break;
  5271. case 'OL': case 'UL':
  5272. _tags.push(node.nodeName.toLowerCase());
  5273. break;
  5274. case 'DL':
  5275. _tags.push(node.nodeName.toLowerCase());
  5276. break;
  5277. case 'LI':
  5278. switch (tagChain.reverse().find(tag => /^[ou]l$/.test(tag))) {
  5279. case 'ol': text[0] = '[#] '; text[4] = '\n'; break;
  5280. case 'ul': text[0] = '[*] '; text[4] = '\n'; break;
  5281. default: return '';
  5282. }
  5283. break;
  5284. case 'A': {
  5285. addTag('url=' + removeRedirect(node.href));
  5286. break;
  5287. }
  5288. case 'IMG':
  5289. addTag('img');
  5290. text[2] = node.dataset.src || node.src;
  5291. break;
  5292. case 'DETAILS': {
  5293. let summary = node.querySelector('summary');
  5294. summary = summary != null ? '='.concat(summary.textContent.trim()) : '';
  5295. addTag('hide' + summary);
  5296. break;
  5297. }
  5298. case 'AUDIO': case 'BASE': case 'BUTTON': case 'CANVAS': case 'COL': case 'COLGROUP': case 'DATALIST':
  5299. case 'DIALOG': case 'EMBED': case 'FIELDSET': case 'FORM': case 'HEAD': case 'INPUT': case 'LEGEND':
  5300. case 'LINK': case 'MAP': case 'META': case 'METER': case 'NOSCRIPT': case 'OBJECT': case 'OPTGROUP':
  5301. case 'OPTION': case 'PARAM': case 'PROGRESS': case 'SELECT': case 'SOURCE': case 'STYLE': case 'SUMMARY':
  5302. case 'SVG': case 'TEMPLATE': case 'TEXTAREA': case 'TITLE': case 'TRACK': case 'VIDEO':
  5303. return '';
  5304. }
  5305. if (['left', 'center', 'right'].some(al => node.style.textAlign.toLowerCase() == al)) {
  5306. addTag('align=' + node.style.textAlign.toLowerCase());
  5307. }
  5308. if (node.style.fontWeight >= 700) addTag('b');
  5309. switch (node.style.fontStyle.toLowerCase()) {
  5310. case 'italic': addTag('i'); break;
  5311. }
  5312. switch (node.style.textDecorationLine.toLowerCase()) {
  5313. case 'underline': addTag('u'); break;
  5314. case 'line-through': addTag('s'); break;
  5315. }
  5316. if (node.style.color) {
  5317. ctxt.fillStyle = elem.style.color;
  5318. if (ctxt.fillStyle != '#000000' && /^#(?:[a-f0-8]{2}){3,4}$/i.test(ctxt.fillStyle)) {
  5319. addTag('color=' + ctxt.fillStyle);
  5320. }
  5321. }
  5322. if (!text[2]) node.childNodes.forEach(function(node) {
  5323. text[2] += html2php(node, url, tagChain.concat(tags.concat(_tags).map(tag => tag.replace(/=.*$/, ''))));
  5324. });
  5325. if (node.nodeName = 'A' && text[2].trim().length <= 0) {
  5326. text[2] = removeRedirect(node.href);
  5327. tags.splice(-1, 1, 'url');
  5328. }
  5329. return text[0].concat((text[1] || text[2] || text[3] ? tags.map(tag => '[' + tag + ']').join('').concat(text[1],
  5330. text[2], text[3], tags.reverse().map(tag => '[/' + tag.replace(/=.*$/, '') + ']').join('')) : ''), text[4]);
  5331.  
  5332. function addTag(tag) {
  5333. if (tagChain.concat(tags.map(tag => tag.replace(/=.*$/, ''))).includesCaseless(tag.replace(/=.*$/, ''))) return;
  5334. tags.push(tag);
  5335. }
  5336. }
  5337. case Node.TEXT_NODE:
  5338. return node.wholeText.replace(/\s+/g, ' ');
  5339. case Node.DOCUMENT_NODE:
  5340. return html2php(node.body, url);
  5341. }
  5342. return '';
  5343. }
  5344.  
  5345. function coverPreview(anchor, src, size) {
  5346. if (!prefs.auto_preview_cover || anchor.parentNode.previousElementSibling == null) return;
  5347. if ((child = document.getElementById('cover-preview')) == null) {
  5348. if (!(anchor instanceof HTMLElement)) return;
  5349. elem = document.createElement('div');
  5350. elem.style = 'padding-top: 10px; float: right; width: 90%;';
  5351. child = document.createElement('img');
  5352. child.id = 'cover-preview';
  5353. elem.append(child);
  5354. var div = document.createElement('div');
  5355. div.id = 'cover-size';
  5356. elem.append(div);
  5357. anchor.parentNode.previousElementSibling.append(document.createElement('br'));
  5358. anchor.parentNode.previousElementSibling.append(elem);
  5359. }
  5360. div = div || document.getElementById('cover-size');
  5361. if (urlParser.test(src)) {
  5362. child.onload = function(evt) {
  5363. this.onload = null;
  5364. if (!this.naturalWidth || !this.naturalHeight) return; // invalid image
  5365. (size > 0 ? Promise.resolve(size) : getRemoteFileSize(src)).then(function(size) {
  5366. var warn = prefs.huge_image_warning && size > prefs.huge_image_warning * 2**20;
  5367. var html = warn ? '<strong style="color: #ff4c4c;">' + formattedSize(size) + '</strong>' : formattedSize(size);
  5368. div.innerHTML = this.naturalWidth + '×' + this.naturalHeight + ' (' + html + ')';
  5369. if (!warn) return;
  5370. addMessage('high cover size (' + formattedSize(size) + ')', 'notice');
  5371. }.bind(this)).catch(reason => { div.textContent = this.naturalWidth + '×' + this.naturalHeight });
  5372. };
  5373. child.src = src;
  5374. } else div.textContent = child.src = '';
  5375. }
  5376.  
  5377. function getRemoteFileSize(url) {
  5378. return new Promise(function(resolve, reject) {
  5379. var imageSize, abort = GM_xmlhttpRequest({
  5380. method: 'GET', url: url, responseType: 'blob',
  5381. onreadystatechange: function(response) {
  5382. if (imageSize || response.readyState < XMLHttpRequest.HEADERS_RECEIVED
  5383. || !/^Content-Length:\s*(\d+)\b/im.test(response.responseHeaders)) return;
  5384. var imageSize = parseInt(RegExp.$1);
  5385. if (isNaN(imageSize)) return; //reject('Wrong size received');
  5386. resolve(imageSize);
  5387. abort.abort();
  5388. },
  5389. onload: function(response) { // fail-safe
  5390. if (imageSize) return;
  5391. if (response.status == 200) resolve(response.responseText.length);
  5392. else reject(new Error('Image not accessible'));
  5393. },
  5394. onerror: response => { reject(new Error('Image not accessible')) },
  5395. ontimeout: response => { reject(new Error('Image not accessible')) },
  5396. });
  5397. });
  5398. }
  5399.  
  5400. function removeRedirect(uri) {
  5401. return typeof uri != 'string' ? null : [
  5402. 'anonymz.com/?',
  5403. 'anonym.to/?',
  5404. 'nullrefer.com/?',
  5405. 'dereferer.me/?',
  5406. 'reho.st/?',
  5407. ].reduce(function(acc, it) {
  5408. if (acc.toLowerCase().startsWith('https://' + it)) return acc.slice(it.length + 8);
  5409. if (acc.toLowerCase().startsWith('http://' + it)) return acc.slice(it.length + 7);
  5410. return acc;
  5411. }, uri);
  5412. }
  5413.  
  5414. function cleanupDescriptions(evt) {
  5415. descriptionFields.forEach(function(ID) {
  5416. if ((ref = evt.target.querySelector('textarea#' + ID)) == null || ref.value.length <= 0) return;
  5417. var clean = ref.value
  5418. .replace(/[ \t]*Vinyl rip by \[color=\S+\]\[\/color\]\s*/im, '')
  5419. .replace(/\[u\]Lineage:\[\/u\]\n\n/i, '')
  5420. for (var i = 0; i < 3; ++i) clean = clean.replace(/\s*\[(\w+)(?:=([^\[\]]*))?\]\[\/\1\]/gm, '');
  5421. const drMatch = [
  5422. /(^| \| )DR(\d+)$\s+/m,
  5423. /(?:^| \| )DR(\d+)(?=$| \| )/gm,
  5424. ];
  5425. var m = /\[hide=DR(\d+)?\]\[pre\]/i.exec(clean);
  5426. //if (m != null && drMatch[0].test(clean) && RegExp.$2 == m[1]) clean = clean.replace(drMatch[0], '$1');
  5427. if (m != null && drMatch[1].test(clean) && RegExp.$1 == m[1]) clean = clean.replace(drMatch[1], '');
  5428. ref.value = clean.replace(/(?:[ \t]*\r?\n){3,}/g, '\n\n').replace(/[ \t]+$/gm, '').trim();
  5429. });
  5430. return true;
  5431. }
  5432.  
  5433. function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*'.concat(expr, '[^\\(\\)]*\\)$'), 'i') }
  5434. function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*'.concat(expr, '[^\\[\\]]*\\]$'), 'i') }
  5435.  
  5436. function notMonospaced(str) {
  5437. return /[\u0080-\u009F]/.test(str)
  5438. // || /[\u0000-\u001F]/.test(str) // Control character
  5439. // || /[\u0020-\u007F]/.test(str) // Basic Latin
  5440. // || /[\u0080-\u00FF]/.test(str) // Latin-1 Supplement
  5441. // || /[\u0100-\u017F]/.test(str) // Latin Extended-A
  5442. // || /[\u0180-\u024F]/.test(str) // Latin Extended-B
  5443. // || /[\u0250-\u02AF]/.test(str) // IPA Extensions
  5444. || /[\u02B0-\u02FF]/.test(str) // Spacing Modifier Letters
  5445. || /[\u0300-\u036F]/.test(str) // Combining Diacritical Marks
  5446. || /[\u0370-\u03FF]/.test(str) // Greek and Coptic
  5447. || /[\u0400-\u04FF]/.test(str) // Cyrillic
  5448. || /[\u0500-\u052F]/.test(str) // Cyrillic Supplement
  5449. || /[\u0530-\u058F]/.test(str) // Armenian
  5450. || /[\u0590-\u05FF]/.test(str) // Hebrew
  5451. || /[\u0600-\u06FF]/.test(str) // Arabic
  5452. || /[\u0700-\u074F]/.test(str) // Syriac
  5453. || /[\u0750-\u077F]/.test(str) // Arabic Supplement
  5454. || /[\u0780-\u07BF]/.test(str) // Thaana
  5455. || /[\u07C0-\u07FF]/.test(str) // NKo
  5456. || /[\u0800-\u083F]/.test(str) // Samaritan
  5457. || /[\u0840-\u085F]/.test(str) // Mandaic
  5458. || /[\u0860-\u086F]/.test(str) // Syriac Supplement
  5459. || /[\u08A0-\u08FF]/.test(str) // Arabic Extended-A
  5460. || /[\u0900-\u097F]/.test(str) // Devanagari
  5461. || /[\u0980-\u09FF]/.test(str) // Bengali
  5462. || /[\u0A00-\u0A7F]/.test(str) // Gurmukhi
  5463. || /[\u0A80-\u0AFF]/.test(str) // Gujarati
  5464. || /[\u0B00-\u0B7F]/.test(str) // Oriya
  5465. || /[\u0B80-\u0BFF]/.test(str) // Tamil
  5466. || /[\u0C00-\u0C7F]/.test(str) // Telugu
  5467. || /[\u0C80-\u0CFF]/.test(str) // Kannada
  5468. || /[\u0D00-\u0D7F]/.test(str) // Malayalam
  5469. || /[\u0D80-\u0DFF]/.test(str) // Sinhala
  5470. || /[\u0E00-\u0E7F]/.test(str) // Thai
  5471. || /[\u0E80-\u0EFF]/.test(str) // Lao
  5472. || /[\u0F00-\u0FFF]/.test(str) // Tibetan
  5473. || /[\u1000-\u109F]/.test(str) // Myanmar
  5474. || /[\u10A0-\u10FF]/.test(str) // Georgian
  5475. || /[\u1100-\u11FF]/.test(str) // Hangul Jamo
  5476. || /[\u1200-\u137F]/.test(str) // Ethiopic
  5477. || /[\u1380-\u139F]/.test(str) // Ethiopic Supplement
  5478. || /[\u13A0-\u13FF]/.test(str) // Cherokee
  5479. || /[\u1400-\u167F]/.test(str) // Unified Canadian Aboriginal Syllabics
  5480. || /[\u1680-\u169F]/.test(str) // Ogham
  5481. || /[\u16A0-\u16FF]/.test(str) // Runic
  5482. || /[\u1700-\u171F]/.test(str) // Tagalog
  5483. || /[\u1720-\u173F]/.test(str) // Hanunoo
  5484. || /[\u1740-\u175F]/.test(str) // Buhid
  5485. || /[\u1760-\u177F]/.test(str) // Tagbanwa
  5486. || /[\u1780-\u17FF]/.test(str) // Khmer
  5487. || /[\u1800-\u18AF]/.test(str) // Mongolian
  5488. || /[\u18B0-\u18FF]/.test(str) // Unified Canadian Aboriginal Syllabics Extended
  5489. || /[\u1900-\u194F]/.test(str) // Limbu
  5490. || /[\u1950-\u197F]/.test(str) // Tai Le
  5491. || /[\u1980-\u19DF]/.test(str) // New Tai Lue
  5492. || /[\u19E0-\u19FF]/.test(str) // Khmer Symbols
  5493. || /[\u1A00-\u1A1F]/.test(str) // Buginese
  5494. || /[\u1A20-\u1AAF]/.test(str) // Tai Tham
  5495. || /[\u1AB0-\u1AFF]/.test(str) // Combining Diacritical Marks Extended
  5496. || /[\u1B00-\u1B7F]/.test(str) // Balinese
  5497. || /[\u1B80-\u1BBF]/.test(str) // Sundanese
  5498. || /[\u1BC0-\u1BFF]/.test(str) // Batak
  5499. || /[\u1C00-\u1C4F]/.test(str) // Lepcha
  5500. || /[\u1C50-\u1C7F]/.test(str) // Ol Chiki
  5501. || /[\u1C80-\u1C8F]/.test(str) // Cyrillic Extended C
  5502. || /[\u1CC0-\u1CCF]/.test(str) // Sundanese Supplement
  5503. || /[\u1CD0-\u1CFF]/.test(str) // Vedic Extensions
  5504. || /[\u1D00-\u1D7F]/.test(str) // Phonetic Extensions
  5505. || /[\u1D80-\u1DBF]/.test(str) // Phonetic Extensions Supplement
  5506. || /[\u1DC0-\u1DFF]/.test(str) // Combining Diacritical Marks Supplement
  5507. // || /[\u1E00-\u1EFF]/.test(str) // Latin Extended Additional
  5508. || /[\u1F00-\u1FFF]/.test(str) // Greek Extended
  5509. || /[\u200B-\u200F\u2028\u2029\u203B\u202A-\u202E\u2060-\u206F]/.test(str) //|| /[\u2000-\u206F]/.test(str) // General Punctuation
  5510. || /[\u2070-\u209F]/.test(str) // Superscripts and Subscripts
  5511. // || /[\u20A0-\u20CF]/.test(str) // Currency Symbols
  5512. || /[\u20D0-\u20FF]/.test(str) // Combining Diacritical Marks for Symbols
  5513. // || /[\u2100-\u214F]/.test(str) // Letterlike Symbols
  5514. || /[\u2150-\u218F]/.test(str) // Number Forms
  5515. // || /[\u2190-\u21FF]/.test(str) // Arrows
  5516. || /[\u2200-\u22FF]/.test(str) // Mathematical Operators
  5517. || /[\u2300-\u23FF]/.test(str) // Miscellaneous Technical
  5518. || /[\u2400-\u243F]/.test(str) // Control Pictures
  5519. // || /[\u2440-\u245F]/.test(str) // Optical Character Recognition
  5520. || /[\u2460-\u24FF]/.test(str) // Enclosed Alphanumerics
  5521. || /[\u2500-\u257F]/.test(str) // Box Drawing
  5522. // || /[\u2580-\u259F]/.test(str) // Block Elements
  5523. || /[\u25A0-\u25FF]/.test(str) // Geometric Shapes
  5524. || /[\u2600-\u26FF]/.test(str) // Miscellaneous Symbols
  5525. || /[\u2700-\u27BF]/.test(str) // Dingbats
  5526. || /[\u27C0-\u27EF]/.test(str) // Miscellaneous Mathematical Symbols-A
  5527. || /[\u27F0-\u27FF]/.test(str) // Supplemental Arrows-A
  5528. || /[\u2800-\u28FF]/.test(str) // Braille Patterns
  5529. || /[\u2900-\u297F]/.test(str) // Supplemental Arrows-B
  5530. // || /[\u2980-\u29FF]/.test(str) // Miscellaneous Mathematical Symbols-B
  5531. // || /[\u2A00-\u2AFF]/.test(str) // Supplemental Mathematical Operators
  5532. || /[\u2B00-\u2BFF]/.test(str) // Miscellaneous Symbols and Arrows
  5533. || /[\u2C00-\u2C5F]/.test(str) // Glagolitic
  5534. // || /[\u2C60-\u2C7F]/.test(str) // Latin Extended-C
  5535. || /[\u2C80-\u2CFF]/.test(str) // Coptic
  5536. || /[\u2D00-\u2D2F]/.test(str) // Georgian Supplement
  5537. || /[\u2D30-\u2D7F]/.test(str) // Tifinagh
  5538. || /[\u2D80-\u2DDF]/.test(str) // Ethiopic Extended
  5539. || /[\u2DE0-\u2DFF]/.test(str) // Cyrillic Extended-A
  5540. || /[\u2E00-\u2E7F]/.test(str) // Supplemental Punctuation
  5541. || /[\u2E80-\u2EFF]/.test(str) // CJK Radicals Supplement
  5542. || /[\u2F00-\u2FDF]/.test(str) // Kangxi Radicals
  5543. || /[\u2FF0-\u2FFF]/.test(str) // Ideographic Description Characters
  5544. || /[\u3000-\u303F]/.test(str) // CJK Symbols and Punctuation
  5545. || /[\u3040-\u309F]/.test(str) // Hiragana
  5546. || /[\u30A0-\u30FF]/.test(str) // Katakana
  5547. || /[\u3100-\u312F]/.test(str) // Bopomofo
  5548. || /[\u3130-\u318F]/.test(str) // Hangul Compatibility Jamo
  5549. || /[\u3190-\u319F]/.test(str) // Kanbun
  5550. || /[\u31A0-\u31BF]/.test(str) // Bopomofo Extended
  5551. || /[\u31C0-\u31EF]/.test(str) // CJK Strokes
  5552. || /[\u31F0-\u31FF]/.test(str) // Katakana Phonetic Extensions
  5553. || /[\u3200-\u32FF]/.test(str) // Enclosed CJK Letters and Months
  5554. || /[\u3300-\u33FF]/.test(str) // CJK Compatibility
  5555. || /[\u3400-\u4DBF]/.test(str) // CJK Unified Ideographs Extension A
  5556. || /[\u4DC0-\u4DFF]/.test(str) // Yijing Hexagram Symbols
  5557. || /[\u4E00-\u9FFF]/.test(str) // CJK Unified Ideographs
  5558. // || /[\uA000-\uA48F]/.test(str) // Yi Syllables
  5559. // || /[\uA490-\uA4CF]/.test(str) // Yi Radicals
  5560. || /[\uA4D0-\uA4FF]/.test(str) // Lisu
  5561. || /[\uA500-\uA63F]/.test(str) // Vai
  5562. || /[\uA640-\uA69F]/.test(str) // Cyrillic Extended-B
  5563. || /[\uA6A0-\uA6FF]/.test(str) // Bamum
  5564. || /[\uA700-\uA71F]/.test(str) // Modifier Tone Letters
  5565. || /[\uA720-\uA7FF]/.test(str) // Latin Extended-D
  5566. || /[\uA800-\uA82F]/.test(str) // Syloti Nagri
  5567. || /[\uA830-\uA83F]/.test(str) // Common Indic Number Forms
  5568. || /[\uA840-\uA87F]/.test(str) // Phags-pa
  5569. || /[\uA880-\uA8DF]/.test(str) // Saurashtra
  5570. || /[\uA8E0-\uA8FF]/.test(str) // Devanagari Extended
  5571. || /[\uA900-\uA92F]/.test(str) // Kayah Li
  5572. || /[\uA930-\uA95F]/.test(str) // Rejang
  5573. || /[\uA960-\uA97F]/.test(str) // Hangul Jamo Extended-A
  5574. || /[\uA980-\uA9DF]/.test(str) // Javanese
  5575. || /[\uA9E0-\uA9FF]/.test(str) // Myanmar Extended-B
  5576. || /[\uAA00-\uAA5F]/.test(str) // Cham
  5577. || /[\uAA60-\uAA7F]/.test(str) // Myanmar Extended-A
  5578. || /[\uAA80-\uAADF]/.test(str) // Tai Viet
  5579. || /[\uAAE0-\uAAFF]/.test(str) // Meetei Mayek Extensions
  5580. || /[\uAB00-\uAB2F]/.test(str) // Ethiopic Extended-A
  5581. // || /[\uAB30-\uAB6F]/.test(str) // Latin Extended-E
  5582. || /[\uAB70-\uABBF]/.test(str) // Cherokee Supplement
  5583. || /[\uABC0-\uABFF]/.test(str) // Meetei Mayek
  5584. || /[\uAC00-\uD7AF]/.test(str) // Hangul Syllables
  5585. || /[\uD7B0-\uD7FF]/.test(str) // Hangul Jamo Extended-B
  5586. || /[\uD800-\uDB7F]/.test(str) // High Surrogates
  5587. // || /[\uDB80-\uDBFF]/.test(str) // High Private Use Surrogates
  5588. || /[\uDC00-\uDFFF]/.test(str) // Low Surrogates
  5589. || /[\uE000-\uF8FF]/.test(str) // Private Use Area
  5590. || /[\uF900-\uFAFF]/.test(str) // CJK Compatibility Ideographs
  5591. || /[\uFB00-\uFB4F]/.test(str) // Alphabetic Presentation Forms
  5592. || /[\uFB50-\uFDFF]/.test(str) // Arabic Presentation Forms-A
  5593. || /[\uFE00-\uFE0F]/.test(str) // Variation Selectors
  5594. || /[\uFE10-\uFE1F]/.test(str) // Vertical Forms
  5595. || /[\uFE20-\uFE2F]/.test(str) // Combining Half Marks
  5596. || /[\uFE30-\uFE4F]/.test(str) // CJK Compatibility Forms
  5597. || /[\uFE50-\uFE6F]/.test(str) // Small Form Variants
  5598. || /[\uFE70-\uFEFF]/.test(str) // Arabic Presentation Forms-B
  5599. || /[\uFF00-\uFFEF]/.test(str) // Halfwidth and Fullwidth Forms
  5600. || /[\uFFF0-\uFFFF]/.test(str) // Specials
  5601. // || /[\u10000-\uFFFFF]/.test(str) // Others
  5602. }
  5603.  
  5604. function makeTimeString(duration) {
  5605. let t = Math.abs(Math.round(duration));
  5606. let H = Math.floor(t / 60 ** 2);
  5607. let M = Math.floor(t / 60 % 60);
  5608. let S = t % 60;
  5609. return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
  5610. ':' + S.toString().padStart(2, '0');
  5611. }
  5612.  
  5613. function timeStringToTime(str) {
  5614. if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
  5615. var t = 0, a = RegExp.$2.split(':');
  5616. while (a.length > 0) t = t * 60 + parseFloat(a.shift());
  5617. return RegExp.$1 ? -t : t;
  5618. }
  5619.  
  5620. function normalizeDate(str) {
  5621. if (typeof str != 'string') return null;
  5622. if (/\b(\d{4}-\d+-\d+|\d{1,2}\/\d{1,2}\/\d{2})\b/.test(str)) return RegExp.$1; // US (clash with BE, IT)
  5623. if (/\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // UK, IRL, FR
  5624. if (/\b(\d{1,2})-(\d{1,2})-(\d{2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // NL
  5625. if (/\b(\d{1,2})\.\s?(\d{1,2})\.\s?(\d{2}|\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // AT, CH, DE, LU, CE
  5626. if (/\b(\d{4})\.\s?(\d{1,2})\.\s?(\d{1,2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1; // JP
  5627. return extractYear(str);
  5628. }
  5629.  
  5630. function extractYear(expr) {
  5631. if (typeof expr == 'number') return Math.round(expr);
  5632. if (typeof expr != 'string') return null;
  5633. if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
  5634. var d = new Date(expr);
  5635. return parseInt(isNaN(d) ? expr : d.getFullYear());
  5636. }
  5637.  
  5638. function formattedSize(size) {
  5639. return size < 1024**1 ? Math.round(size) + ' B'
  5640. : size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB'
  5641. : size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB'
  5642. : size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB'
  5643. : size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB'
  5644. : (Math.round(size * 100 / 2**50) / 100) + ' PiB';
  5645. }
  5646.  
  5647. function safeText(unsafeText) {
  5648. let div = document.createElement('div');
  5649. div.innerText = unsafeText || '';
  5650. return div.innerHTML;
  5651. }
  5652.  
  5653. function testImageUrl(url) {
  5654. if (!urlParser.test(url)) return Promise.reject('not an image');
  5655. if (['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(function(ext) {
  5656. return url.toLowerCase().endsWith('.'.concat(ext));
  5657. })) return Promise.resolve(url); // weak quick test
  5658. return new Promise(function(resolve, reject) {
  5659. var img = new Image();
  5660. img.onload = function() { resolve(this.src) };
  5661. img.onerror = img.onabort = img.ontimeout = error => { reject(url.concat(' not valid image')) };
  5662. img.src = url;
  5663. });
  5664. }
  5665. function testImageUrls(urls) {
  5666. return Array.isArray(urls) ? Promise.all(urls.map(testImageUrl)) : Promise.reject('URLs not an array');
  5667. }
  5668.  
  5669. function imageClear(evt) {
  5670. evt.target.value = '';
  5671. coverPreview(evt.target, null);
  5672. }
  5673.  
  5674. function imageDropHandler(evt) { return imageDataHandler(evt, evt.dataTransfer) }
  5675. function imagePasteHandler(evt) { return imageDataHandler(evt, evt.clipboardData) }
  5676. function imageDataHandler(evt, data) {
  5677. if (!data) return true;
  5678. if (data.files.length > 0 && data.files[0].type.toLowerCase().startsWith('image/')) {
  5679. evt.target.disabled = true;
  5680. if (evt.target.hTimer) {
  5681. clearTimeout(evt.target.hTimer);
  5682. delete evt.target.hTimer;
  5683. }
  5684. evt.target.style.backgroundColor = '#800000';
  5685. let size = data.files[0].size;
  5686. upload2PTPIMG([data.files[0]]).then(function(urls) {
  5687. evt.target.value = urls[0];
  5688. evt.target.style.backgroundColor = '#004000';
  5689. evt.target.hTimer = setTimeout(function() {
  5690. evt.target.style.backgroundColor = null;
  5691. delete evt.target.hTimer;
  5692. }, 10000);
  5693. coverPreview(evt.target, urls[0], size);
  5694. }).catch(function(error) {
  5695. evt.target.style.backgroundColor = null;
  5696. imageClear(evt);
  5697. alert(error);
  5698. }).then(function() { evt.target.disabled = false });
  5699. return false;
  5700. } else if (data.items.length > 0) {
  5701. testImageUrl((data.getData('text/uri-list') || data.getData('text/plain')).split(/\r?\n/)[0]).then(function(url) {
  5702. evt.target.value = url;
  5703. coverPreview(evt.target, url);
  5704. if (!prefs.auto_rehost_cover || url.toLowerCase().startsWith(imghostOrigin)) return;
  5705. //if (rehostItBtn != null) return rehostItBtn.click();
  5706. evt.target.disabled = true;
  5707. rehost2PTPIMG([url])
  5708. .then(function(urls) { if (urls.length > 0) evt.target.value = urls[0] })
  5709. .catch(e => { alert(e) })
  5710. .then(function() { evt.target.disabled = false });
  5711. }).catch(e => { console.warn(e) });
  5712. return false;
  5713. }
  5714. return true;
  5715. }
  5716.  
  5717. function descDropHandler(evt) {
  5718. if (evt.dataTransfer == null || evt.shiftKey) return true;
  5719. if (evt.dataTransfer.files.length > 0) {
  5720. let images = [];
  5721. Array.from(evt.dataTransfer.files).forEach(function(file) {
  5722. switch (file.type) {
  5723. case '':
  5724. if (!['log'/*, 'nfo'*/].some(ext => file.name.toLowerCase().endsWith('.' + ext))) break;
  5725. case 'text/plain':
  5726. //case 'text/nfo': // malformed encoding
  5727. case 'text/log':
  5728. evt.target.disabled = true;
  5729. file.getText(file.name.toLowerCase().endsWith('.nfo') ? 'ibm850' : 'utf-8').then(function(text) {
  5730. var isDR = file.name.toLowerCase().endsWith('foo_dr.txt') && /^Official DR value:\s*DR(\d+)\b/im.test(text);
  5731. if (isDR) var DR = parseInt(RegExp.$1);
  5732. var tag = isDR || file.name.toLowerCase().endsWith('.nfo') ? 'pre' : 'code';
  5733. var php = isDR ? '[hide=DR' + RegExp.$1 + '][' + tag + ']' + text + '[/' + tag + '][/hide]'
  5734. : '[hide=' + file.name + '][' + tag + ']' + text + '[/' + tag + '][/hide]';
  5735. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  5736. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) +
  5737. php + evt.target.value.slice(evt.rangeOffset);
  5738. } else if (isDR && /\[hide=DR\d*\]\[pre\]\[\/pre\]/i.test(evt.target.value)) {
  5739. evt.target.value = RegExp.leftContext + php.slice(0, -7) + RegExp.rightContext;
  5740. } else if (isDR && /\[hide=DR(\d*)\]((?:\[pre\](foobar2000[\s\S]+?)^\[\/pre\]\s*)+)(?:\[pre\]\[\/pre\])?/im.test(evt.target.value)) {
  5741. php = '[hide=DR';
  5742. if (parseInt(RegExp.$1) == DR) php += RegExp.$1;
  5743. evt.target.value = RegExp.leftContext.concat(php, ']', RegExp.$2.trim(), '\n[pre]', text, '[/pre]', RegExp.rightContext);
  5744. } else if (!isDR && /\[hide\](?:\[code\]\[\/code\])?\[\/hide\]/i.test(evt.target.value)) {
  5745. evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
  5746. } else if (!isDR && /(\[hide=[^\]]+\])(?:\[code\]\[\/code\])?(\[\/hide\])/i.test(evt.target.value)) {
  5747. evt.target.value = RegExp.leftContext.concat(RegExp.$1, '[code]', text, '[/code]', RegExp.$2, RegExp.rightContext);
  5748. } else evt.target.value += '\n\n'.concat(php);
  5749. }).catch(function(e) { alert(e) }).then(function() {
  5750. if (!evt.target.style.background) evt.target.disabled = false;
  5751. });
  5752. break;
  5753. case 'image/png':
  5754. case 'image/jpeg':
  5755. case 'image/gif':
  5756. case 'image/bmp':
  5757. //case 'image/webp':
  5758. //case 'image/svg+xml':
  5759. images.push(file);
  5760. break;
  5761. }
  5762. });
  5763. if (images.length > 0) {
  5764. evt.target.disabled = true;
  5765. evt.target.style.background = '#FF000040 no-repeat center center url(' + ulImgData +')';
  5766. //evt.target.style.background = '#FF000040 no-repeat center center url(https://svgshare.com/i/H16.svg)';
  5767. upload2PTPIMG(images).then(urlHandler.bind({ tag: 'img' })).catch(error => { alert(error) }).then(function() {
  5768. evt.target.style.background = null;
  5769. evt.target.disabled = false;
  5770. });
  5771. }
  5772. return false;
  5773. } else if (evt.dataTransfer.items.length > 0) {
  5774. let content = evt.dataTransfer.getData('text/uri-list');
  5775. if (content) {
  5776. content = content.split(/\r?\n/);
  5777. testImageUrls(content).then(function(urls) {
  5778. if (prefs.auto_rehost_cover) {
  5779. evt.target.disabled = true;
  5780. rehost2PTPIMG(urls).then(urlHandler.bind({ tag: 'img' })).catch(e => { alert(e) }).then(function() {
  5781. evt.target.disabled = false;
  5782. });
  5783. } else urlHandler.bind({ tag: 'img' })(content);
  5784. }).catch(function(e) {
  5785. let as = domParser.parseFromString(evt.dataTransfer.getData('text/html'), 'text/html').body.querySelectorAll('a');
  5786. urlHandler.bind({ tag: 'url', titles: Array.from(as).map(a => a.textContent.trim()) })(content);
  5787. });
  5788. } else if (content = evt.dataTransfer.getData('text/html')) {
  5789. textHandler(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
  5790. } else if (content = evt.dataTransfer.getData('text/plain')) {
  5791. textHandler(content);
  5792. }
  5793. return false;
  5794. }
  5795. return true;
  5796.  
  5797. function urlHandler(urls) {
  5798. const rx = new RegExp('\\[' + this.tag + '\\]\\[\\/' + this.tag + '\\]', 'i');
  5799. urls.forEach(function(url, ndx) {
  5800. if (url.length <= 0 || !urlParser.test(urls)) return;
  5801. var php = '[' + this.tag;
  5802. php += Array.isArray(this.titles) && this.titles[ndx] ? '=' + url + ']' + this.titles[ndx] : ']' + url;
  5803. php += '[/' + this.tag +']';
  5804. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  5805. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) +
  5806. php + evt.target.value.slice(evt.rangeOffset);
  5807. } else if (rx.test(evt.target.value)) {
  5808. evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
  5809. } else evt.target.value += '\n\n'.concat(php);
  5810. }.bind(this));
  5811. }
  5812. function textHandler(php) {
  5813. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  5814. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + php + evt.target.value.slice(evt.rangeOffset);
  5815. } else evt.target.value += '\n\n'.concat(php);
  5816. }
  5817. }
  5818.  
  5819. function descPasteHandler(evt) {
  5820. if (evt.clipboardData == null || evt.clipboardData.items.length <= 0) return true;
  5821. var content = evt.clipboardData.getData('text/html');
  5822. if (!content) return true;
  5823. content = html2php(domParser.parseFromString(content, 'text/html')).collapseGaps();
  5824. var selStart = evt.target.selectionStart;
  5825. evt.target.value = evt.target.value.slice(0, evt.target.selectionStart)
  5826. .concat(content, evt.target.value.slice(evt.target.selectionEnd));
  5827. evt.target.setSelectionRange(selStart + content.length, selStart + content.length);
  5828. return false;
  5829. }
  5830.  
  5831. function rehostDropHandler(evt) {
  5832. if (evt.dataTransfer == null) return false;
  5833. var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  5834. if (image == null) return false;
  5835. if (evt.dataTransfer.files.length > 0) {
  5836. evt.preventDefault();
  5837. evt.stopPropagation();
  5838. evt.currentTarget.disabled = true;
  5839. if (evt.currentTarget.hTimer) {
  5840. clearTimeout(evt.currentTarget.hTimer);
  5841. delete evt.currentTarget.hTimer;
  5842. }
  5843. evt.currentTarget.value = 'Uploading...';
  5844. evt.currentTarget.style.backgroundColor = '#A00000';
  5845. var evtSrc = evt.currentTarget;
  5846. upload2PTPIMG(evt.dataTransfer.files).then(function(results) {
  5847. if (urlParser.test(results[0])) {
  5848. image.value = results[0];
  5849. evtSrc.style.backgroundColor = '#008000';
  5850. evtSrc.hTimer = setTimeout(function() {
  5851. evtSrc.style.backgroundColor = null;
  5852. delete evtSrc.hTimer;
  5853. }, 10000);
  5854. coverPreview(image, results[0], evt.dataTransfer.files[0].size);
  5855. } else evtSrc.style.backgroundColor = null;
  5856. }).catch(function(error) {
  5857. evtSrc.style.backgroundColor = null;
  5858. alert(error);
  5859. }).then(function() {
  5860. evtSrc.value = evtSrc.dataset.caption;
  5861. evtSrc.disabled = false;
  5862. });
  5863. } else if (evt.dataTransfer.items.length > 0) {
  5864. testImageUrl((evt.dataTransfer.getData('text/uri-list')
  5865. || evt.dataTransfer.getData('text/plain')).split(/\r?\n/)[0]).then(function(url) {
  5866. evt.preventDefault();
  5867. evt.stopPropagation();
  5868. image.value = url;
  5869. coverPreview(image, url);
  5870. if (url.toLowerCase().startsWith(imghostOrigin)) return;
  5871. image.disabled = true;
  5872. rehost2PTPIMG([url])
  5873. .then(function(urls) { if (urls.length > 0) image.value = urls[0] })
  5874. .catch(e => { alert(e) })
  5875. .then(function() { image.disabled = false });
  5876. return false;
  5877. }).catch(e => { console.warn(e) });
  5878. }
  5879. return false;
  5880. }
  5881.  
  5882. function uaInsert(evt) {
  5883. if (evt.clipboardData) evt.target.value = '';
  5884. if (!(prefs.autfill_delay > 0)) return true;
  5885. autofill = true;
  5886. setTimeout(fillFromText, prefs.autfill_delay);
  5887. }
  5888.  
  5889. // Firefox accepts dropped playlist in malformed form, try to detect and correct it
  5890. function fixFirefoxDropBug(evt) {
  5891. if (evt.target == null || evt.target.value.length <= 0) return true;
  5892. var tl = (Math.sqrt(4 * evt.target.value.split('\n').length - 3) + 1) / 2;
  5893. if (tl < 2 || tl != Math.floor(tl) || evt.target.value.length % tl != 0) return true;
  5894. var l = evt.target.value.length / tl;
  5895. var s = evt.target.value.slice(0, l);
  5896. for (var i = 1; i < tl; ++i) if (evt.target.value.slice(i * l, (i + 1) * l) != s) return true;
  5897. evt.target.value = s;
  5898. return true;
  5899. }
  5900.  
  5901. function clear0(evt) { if (evt.target.value.length > 0) evt.target.value = '' }
  5902. function clear1(evt) { if (evt.buttons == 4) clear0(evt) }
  5903. function voidDragHandler0(evt) { return false }
  5904. function voidDragHandler1(evt) {
  5905. return !evt.dataTransfer.types.includes('Files') || evt.target.nodeName == 'TEXTAREA'
  5906. || evt.target.nodeName == 'INPUT' && evt.target.type == 'file'
  5907. }
  5908.  
  5909. function upload2PTPIMG(files, elem) {
  5910. var frs = Array.from(files).filter(function(file) {
  5911. return file instanceof File && ['jpeg', 'png', 'gif', 'bmp'].some(ext => file.type == 'image/' + ext);
  5912. }).map(file => new Promise(function(resolve, reject) {
  5913. var reader = new FileReader();
  5914. reader.onload = function() { resolve({ file: file, data: reader.result }) };
  5915. reader.onerror = reader.onabort = reader.ontimeout = error => { reject('FileReader error (' + file.name + ')') };
  5916. reader.readAsBinaryString(file);
  5917. }));
  5918. return frs.length > 0 ? getPTPIMGapiKey().then(apiKey => Promise.all(frs).then(images => new Promise(function(resolve, reject) {
  5919. const boundary = '------NN-GGn-PTPIMG';
  5920. var data = '--' + boundary + '\r\n';
  5921. images.forEach(function(image, ndx) {
  5922. data += 'Content-Disposition: form-data; name="file-upload[' + ndx +
  5923. ']"; filename="' + image.file.name.toASCII() + '"\r\n';
  5924. data += 'Content-Type: ' + image.file.type + '\r\n\r\n';
  5925. data += image.data + '\r\n';
  5926. data += '--' + boundary + '\r\n';
  5927. });
  5928. data += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
  5929. data += apiKey + '\r\n';
  5930. data += '--' + boundary + '--\r\n';
  5931. GM_xmlhttpRequest({
  5932. method: 'POST',
  5933. url: imghostOrigin + '/upload.php',
  5934. responseType: 'json',
  5935. headers: {
  5936. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  5937. 'Content-Length': data.length,
  5938. },
  5939. data: data,
  5940. binary: true,
  5941. onload: function(response) {
  5942. if (response.readyState == XMLHttpRequest.DONE && response.status == 200) {
  5943. resolve(response.response.map(item => imghostOrigin + '/' + item.code + '.' + item.ext));
  5944. } else {
  5945. reject(`Response error ${response.readyState}/${response.status} (${response.statusText})`);
  5946. }
  5947. },
  5948. onprogress: elem instanceof HTMLInputElement ?
  5949. progress => { elem.value = 'Uploading... (' + progress.position + '%)' } : undefined,
  5950. onerror: error => reject(defaultErrorHandler(error)),
  5951. onabort: abort => reject(defaultAbortHandler(abort)),
  5952. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  5953. });
  5954. }))) : Promise.reject('Nothing to upload');
  5955. }
  5956.  
  5957. function rehost2PTPIMG(urls) {
  5958. return testImageUrls(urls).then(urls => getPTPIMGapiKey().then(apiKey => new Promise(function(resolve, reject) {
  5959. const boundary = '------NN-GGn-PTPIMG';
  5960. const dcTest = /^https?:\/\/(?:\w+\.)?discogs\.com\//i;
  5961. var data = '--' + boundary + '\r\n';
  5962. data += 'Content-Disposition: form-data; name="link-upload"\r\n\r\n';
  5963. data += urls.map(url => dcTest.test(url.trim()) ? 'https://reho.st/' + url.trim() : url.trim()).join('\r\n') + '\r\n';
  5964. data += '--' + boundary + '\r\n';
  5965. data += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
  5966. data += apiKey + '\r\n';
  5967. data += '--' + boundary + '--\r\n';
  5968. GM_xmlhttpRequest({
  5969. method: 'POST',
  5970. url: imghostOrigin + '/upload.php',
  5971. responseType: 'json',
  5972. headers: {
  5973. 'Content-type': 'multipart/form-data; boundary=' + boundary,
  5974. 'Content-Length': data.length,
  5975. },
  5976. data: data,
  5977. onload: function(response) {
  5978. if (response.readyState == XMLHttpRequest.DONE && response.status == 200) {
  5979. resolve(response.response.map(item => imghostOrigin + '/' + item.code + '.' + item.ext));
  5980. } else {
  5981. reject(`Response error ${response.readyState}/${response.status} (${response.statusText})`);
  5982. }
  5983. },
  5984. onerror: error => reject(defaultErrorHandler(error)),
  5985. onabort: abort => reject(defaultAbortHandler(abort)),
  5986. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  5987. });
  5988. })));
  5989. }
  5990.  
  5991. function getPTPIMGapiKey() {
  5992. try {
  5993. var apiKey = prefs.ptpimg_api_key || JSON.parse(window.localStorage.ptpimg_it).api_key;
  5994. if (apiKey) return Promise.resolve(apiKey);
  5995. } catch(e) { console.warn(e) }
  5996. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  5997. method: 'GET',
  5998. url: imghostOrigin,
  5999. responseType: 'document',
  6000. onload: function(response) {
  6001. if (response.readyState == XMLHttpRequest.DONE && response.status == 200) {
  6002. apiKey = domParser.parseFromString(response.responseText, 'text/html').getElementById('api_key');
  6003. if (apiKey != null && apiKey.value) {
  6004. GM_setValue('ptpimg_api_key', prefs.ptpimg_api_key = apiKey.value);
  6005. resolve(apiKey.value);
  6006. alert(`Your PTPIMG API key ${apiKey.value} was successfully configured`);
  6007. } else reject(`PTPIMG API key isn\'t configured.
  6008. Please login to ${imghostOrigin}/ and repeat the action
  6009.  
  6010. If you don\'t have PTPIMG account, to avoid this warning in
  6011. future consider to set auto_rehost_cover to 0 in preferences
  6012. (Tampermonkey menu -> right click to Upload Assistant -> Storage tab)`);
  6013. } else reject(new Error('XHR readyState=' + response.readyState +
  6014. ', status=' + response.status + ' (' + response.error + ')'));
  6015. },
  6016. onerror: error => reject(defaultErrorHandler(error)),
  6017. onabort: abort => reject(defaultAbortHandler(abort)),
  6018. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6019. }));
  6020. }
  6021.  
  6022. function dcFmtToGazelle(format) {
  6023. if (/^(?:CD|CDi|CDr|HDCD)\b/.test(format)) return 'CD';
  6024. if (/\b(?:File|AAC|AIFC|AIFF|ALAC|AMR|APE|DFF|DSD|FLAC|MP2|MP3|ogg-vorbis|Opus|SHN|WAV|WavPack|WMA|WMV)\b/.test(format)) return 'WEB';
  6025. if (/^(?:Vinyl|LP|\d+(?:\.\d+)?\s*")$/.test(format)) return 'Vinyl';
  6026. if (/\b(?:SACD|Hybrid)\b/.test(format)) return 'SACD';
  6027. if (/^(?:Blu[ \-]?ray)\b/i.test(format)) return 'Blu-Ray';
  6028. if (/^(?:DVD|HD\s+DVD)/.test(format)) return 'DVD';
  6029. if (/^(?:Cassette|Microcassette)$/i.test(format)) return 'Cassette';
  6030. if (/^(?:DAT)$/.test(format)) return 'DAT';
  6031. if (/^(?:Soundboard)$/i.test(format)) return 'Soundboard';
  6032. //if (/^(?:Memory\s+Stick)$/i.test(format)) return ??
  6033. return null;
  6034. }
  6035.  
  6036. function queryAjaxAPI(action, params) {
  6037. if (!action) return Promise.reject(new Error('action missing'));
  6038. var retryCount = 0;
  6039. return new Promise(function(resolve, reject) {
  6040. params = new URLSearchParams(params || undefined);
  6041. params.set('action', action);
  6042. var url = '/ajax.php?'.concat(params);
  6043. var xhr = new XMLHttpRequest();
  6044. queryInternal();
  6045.  
  6046. function queryInternal() {
  6047. var now = new Date().getTime();
  6048. if (!gazelleApiTimeFrame.timeStamp || now > gazelleApiTimeFrame.timeStamp + 10100) {
  6049. gazelleApiTimeFrame.timeStamp = now;
  6050. gazelleApiTimeFrame.requestCounter = 0;
  6051. };
  6052. if (++gazelleApiTimeFrame.requestCounter <= 5) {
  6053. xhr.open('GET', url, true);
  6054. xhr.setRequestHeader('Accept', 'application/json');
  6055. xhr.responseType = 'json';
  6056. xhr.onload = function() {
  6057. if (xhr.status == 503) return http503Handler(3333, 'onload');
  6058. if (xhr.status != 200) return reject(defaultErrorHandler(xhr));
  6059. if (xhr.response.status == 'success') resolve(xhr.response.response);
  6060. else reject(xhr.response.status);
  6061. };
  6062. xhr.onerror = function() {
  6063. if (xhr.status == 503) http503Handler(3333, xhr, 'onerror'); else reject(defaultErrorHandler(xhr));
  6064. };
  6065. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  6066. xhr.timeout = 10000;
  6067. xhr.send();
  6068. /*
  6069. GM_xmlhttpRequest({
  6070. method: 'GET',
  6071. url: url,
  6072. responseType: 'json',
  6073. headers: { 'Accept': 'application/json' },
  6074. onload: function(response) {
  6075. if (response.status == 503) return http503Handler(3333, response, 'onload');
  6076. if (response.readyState == XMLHttpRequest.DONE || response.status == 200) resolve(response.response);
  6077. else reject(defaultErrorHandler(response));
  6078. },
  6079. onerror: error => error.status == 503 ? http503Handler(3333, error, 'onerror')
  6080. : reject(defaultErrorHandler(error)),
  6081. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6082. });
  6083. */
  6084. } else {
  6085. setTimeout(queryInternal, gazelleApiTimeFrame.timeStamp + 10100 - now);
  6086. console.debug('AJAX API request quota exceeded: /ajax.php?action=' + action + ' (' +
  6087. gazelleApiTimeFrame.requestCounter + ')');
  6088. if (prefs.messages_verbosity >= 1) {
  6089. addMessage('AJAX API request exceeding time frame: action=' +
  6090. action + ' (' + gazelleApiTimeFrame.requestCounter + ')', 'notice');
  6091. } else addMessage('please wait for next AJAX timeframe', 'notice');
  6092. }
  6093.  
  6094. function http503Handler(delay, /*response, */event) {
  6095. if (retryCount++ <= 10) setTimeout(queryInternal, delay); else reject(defaultErrorHandler(xhr));
  6096. console.debug('[UA] queryAjaxAPI encountered HTTP/503 error for url ' + url + '; event: ' + event);
  6097. }
  6098. }
  6099. });
  6100. }
  6101.  
  6102. function validataTorrentFile(torrent) {
  6103. tfMessages.forEach(node => { node.remove() });
  6104. tfMessages = [];
  6105. var pos = 0, infoBegin = 0, infoEnd = 0, fr = new FileReader();
  6106. fr.onload = function(evt) {
  6107. torrent = bdecode(new Uint8Array(fr.result));
  6108. torrent.info.files.forEach(function(file) {
  6109. var folderName = decodeURIComponent(escape(torrent.info.name));
  6110. var fileName = decodeURIComponent(escape(file.path[0]));
  6111. var totalLen = folderName.trueLength() + 1 + fileName.trueLength();
  6112. if (totalLen > 180) tfMessages.push(addMessage(new HTML('file "' + safeText(fileName).bold() +
  6113. '" exceeding allowed length (' + totalLen + ' > 180)'), 'warning'));
  6114. if (/\.(?:torrent|\!ut|\!qb|url|lnk)$/i.test(fileName)) {
  6115. tfMessages.push(addMessage(new HTML('forbidden file "' + safeText(fileName).bold() + '"'), 'warning'));
  6116. }
  6117. });
  6118. pos = document.querySelector('td.ua-messages-bg');
  6119. if (pos != null && pos.childElementCount <= 0) pos.parentNode.remove();
  6120. };
  6121. fr.onerror = fr.onabort = fr.ontimeout = error => { console.error('FileReader error (' + torrent.name + ')') };
  6122. fr.readAsArrayBuffer(torrent);
  6123.  
  6124. function bdecode(str) {
  6125. if (pos > str.length) return null;
  6126. switch (str[pos]) {
  6127. case 100: // char code for 'd'
  6128. ++pos;
  6129. var retval = [];
  6130. while (str[pos] != 101){ // char code for 'e'
  6131. var key = bdecode(str);
  6132. var val = bdecode(str);
  6133. if (key === null || val === null) break;
  6134. retval[key] = val;
  6135. }
  6136. if(infoEnd == -1) infoEnd = pos + 1;
  6137. retval.isDct = true;
  6138. ++pos;
  6139. return retval;
  6140. case 108: // char code for 'l'
  6141. ++pos;
  6142. retval = [];
  6143. while (str[pos] != 101){ // char code for 'e'
  6144. let val = bdecode(str);
  6145. if (val === null) break;
  6146. retval.push(val);
  6147. }
  6148. ++pos;
  6149. return retval;
  6150. case 105: // char code for 'i'
  6151. ++pos;
  6152. var digits = Array.prototype.indexOf.call(str, 101, pos) - pos; // 101 = char code for 'e'
  6153. val = '';
  6154. for (var i = pos; i < digits + pos; ++i) val += String.fromCharCode(str[i]);
  6155. val = Math.round(parseFloat(val));
  6156. pos += digits + 1;
  6157. return val;
  6158. default:
  6159. digits = Array.prototype.indexOf.call(str, 58, pos) - pos; //58 = char code for ':'
  6160. if (digits < 0 || digits > 20) return null;
  6161. var len = '';
  6162. for (i = pos; i < digits + pos; ++i) len += String.fromCharCode(str[i]);
  6163. len = parseInt(len);
  6164. pos += digits + 1;
  6165. var fstring = '';
  6166. for (i = pos; i < len + pos; ++i) fstring += String.fromCharCode(str[i]);
  6167. pos += len;
  6168. if(fstring == 'info') {
  6169. infoBegin = pos;
  6170. infoEnd = -1;
  6171. }
  6172. return fstring;
  6173. }
  6174. }
  6175. }
  6176.  
  6177. function localFetch(url, params, data) {
  6178. return url ? new Promise(function(resolve, reject) {
  6179. var xhr = new XMLHttpRequest();
  6180. xhr.open(getParam('method') || 'GET', url, true);
  6181. if ((xhr.responseType = getParam('responseType') || 'document') == 'json') {
  6182. xhr.setRequestHeader('Accept', 'application/json');
  6183. }
  6184. var headers = getParam('headers');
  6185. if (typeof headers == 'object') Object.keys(headers).forEach(key => { xhr.setRequestHeader(key, headers[key]) });
  6186. xhr.onload = function() { if (xhr.status == 200) resolve(xhr.response); else reject(defaultErrorHandler(xhr)); };
  6187. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  6188. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  6189. xhr.timeout = 10000;
  6190. xhr.send(data || getParam('body'));
  6191. }) : Promise.reject(new Error('URL missing'));
  6192.  
  6193. function getParam(key) {
  6194. if (!key || typeof key != 'string' || typeof params != 'object') return undefined;
  6195. key = Object.keys(params).find(_key => _key.toLowerCase() == key.toLowerCase());
  6196. return key && params[key] || undefined;
  6197. }
  6198. }
  6199.  
  6200. function globalFetch(url, params, data) {
  6201. return url ? new Promise(function(resolve, reject) {
  6202. params = Object.assign({}, params || {}, { url: url });
  6203. if (!getParam('method') ) params.method = 'GET';
  6204. if (!getParam('responseType')) params.responseType = 'document';
  6205. if (getParam('responseType').toLowerCase() == 'json') {
  6206. if (typeof params.headers != 'object') params.headers = {};
  6207. params.headers.Accept = 'application/json';
  6208. }
  6209. if (data) params.data = data;
  6210. params.onload = function(response) {
  6211. if (response.status != 200) return reject(defaultErrorHandler(response));
  6212. resolve(
  6213. getParam('responseType').toLowerCase() == 'document' ?
  6214. domParser.parseFromString(response.responseText, 'text/html') :
  6215. getParam('responseType').toLowerCase() == 'xml' ? response.responseXML :
  6216. getParam('responseType').toLowerCase() == 'text' ? response.responseText : response.response
  6217. );
  6218. };
  6219. params.onerror = error => reject(defaultErrorHandler(error));
  6220. params.ontimeout = timeout => reject(defaultTimeoutHandler(timeout));
  6221. GM_xmlhttpRequest(params);
  6222. }) : Promise.reject(new Error('URL missing'));
  6223.  
  6224. function getParam(key) {
  6225. if (!key || typeof key != 'string' || typeof params != 'object') return undefined;
  6226. key = Object.keys(params).find(_key => _key.toLowerCase() == key.toLowerCase());
  6227. return key && params[key] || undefined;
  6228. }
  6229. }