[RED/OPS/NWCD] Upload Assistant

Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection or web link, offline and online release integrity check, tracklist format customization, featured artists extraction, classical works formatting, cover art fetching from store, checking for previous upload, form enhancements and more

当前为 2020-03-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name [RED/OPS/NWCD] Upload Assistant
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.216
  5. // @description Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection or web link, offline and online release integrity check, tracklist format customization, featured artists extraction, classical works formatting, cover art fetching from store, checking for previous upload, form enhancements and more
  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. // @require https://greasyfork.org/scripts/396340-js-sha1/code/js-sha1.js
  33. // @require https://greasyfork.org/scripts/396360-js-stringdistance/code/js-stringdistance.js
  34. // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
  35. // ==/UserScript==
  36.  
  37. // Additional setup: to work, set the pattern below as built-in foobar2000 copy command or custom Text Tools plugin quick copy command
  38. // $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))
  39. //
  40. // As alternative to pasted playlist, e.g. requests creation, valid URL to page on supported web can be used.
  41. // List of supported domains:
  42. //
  43. // For music releases:
  44. // - qobuz.com
  45. // - highresaudio.com
  46. // - bandcamp.com
  47. // - prestomusic.com
  48. // - discogs.com
  49. // - supraphonline.cz
  50. // - bontonland.cz (closing soon)
  51. // - nativedsd.com
  52. // - junodownload.com
  53. // - hdtracks.com
  54. // - deezer.com
  55. // - spotify.com
  56. // - prostudiomasters.com
  57. // - play.google.com
  58. // - 7digital.com
  59. // - e-onkyo.com
  60. // - acousticsounds.com
  61. // - indies.eu
  62. // - beatport.com
  63. // - traxsource.com
  64. // - musicbrainz.org
  65. // - music.apple.com
  66. // - vgmdb.net
  67. // - tidal.com (requires account)
  68. //
  69. // For e-bbook releases:
  70. // - martinus.cz, martinus.sk
  71. // - goodreads.com
  72. // - databazeknih.cz
  73. //
  74. // For application releases:
  75. // - sanet.st
  76.  
  77. 'use strict';
  78.  
  79. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || window.InstallTrigger;
  80.  
  81. function testDomain(domain) {
  82. return document.location.hostname.toLowerCase() == domain.toLowerCase();
  83. }
  84. function testPath(path, query) {
  85. return document.location.pathname.toLowerCase() == '/'.concat(path.toLowerCase(), '.php')
  86. && (!query || document.location.search.toLowerCase().startsWith('?'.concat(query.toLowerCase())));
  87. }
  88.  
  89. const isRED = testDomain('redacted.ch');
  90. const isNWCD = testDomain('notwhat.cd');
  91. const isOPS = testDomain('orpheus.network');
  92.  
  93. const isUpload = testPath('upload');
  94. const isEdit = testPath('torrents', 'action=editgroup&');
  95. const isRequestNew = testPath('requests', 'action=new');
  96. const isRequestEdit = testPath('requests', 'action=edit&');
  97. const isAddFormat = isUpload && /\bgroupid=(\d+)\b/i.test(document.location.search);
  98.  
  99. const urlParser = /^\s*(https?:\/\/\S+)\s*$/i;
  100. const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp'];
  101. const imghostOrigin = 'https://ptpimg.me';
  102. const mbrRlsPrefix = 'https://musicbrainz.org/release/';
  103. const discogsOrigin = 'https://www.discogs.com';
  104. const deezerAlbumPrefix = 'https://www.deezer.com/album/';
  105. const descriptionFields = ['album_desc', 'body', 'description', 'release_desc', 'release_lineage'];
  106. //const promiseAll = Promise.allSettled || Promise.all;
  107.  
  108. const spotify_clientid = '6d358a207c634b1ebac640149a6090da';
  109. const spotify_clientsecret = '4c59880a4ec241ed9c89a24e66468c64';
  110. const discogs_token = 'CISOUfiQctZCkUedWJzPhzTXxRYihifZgflZAfEm';
  111. const lastfm_api_key = 'b9f26370d7266fbb3151b2ad4f7a74c9';
  112.  
  113. const defaultPrefs = {
  114. autfill_delay: 1000, // delay in ms to autofill form after pasting text into box, 0 to disable
  115. clean_on_apply: 0, // clean the input box on successfull fill
  116. cleanup_descriptions: 1, // pre-submit cleanup to all description fields (remove empty placeholders, redundant info and garbage like empty tag pairs etc.)
  117. keep_meaningles_composers: 0, // keep composers from file tags also for non-composer emphasing genres
  118. 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)
  119. single_threshold: 10 * 60, // For autodetection of release type: max length of single in s
  120. EP_threshold: 30 * 60, // For autodetection of release type: max time of EP in s
  121. auto_rehost_cover: 1, // PTPIMG / using 3rd party script
  122. auto_preview_cover: 1,
  123. huge_image_warning: 5, // threshold in MB for making bandwith stressing cover size warning // 0 to disable
  124. cover_lookup_provider: 'all', // itunes | lastfm | deezer | musicbrainz | qobuz | google | all | empty for no lookup
  125. 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
  126. check_integrity_online: 1, // If provided URL tag, compare local release with release online and lookup for discrepancies
  127. check_whitespace: 1, // check tags for leading/trailing spaces and unreadable characters
  128. estimate_decade_tag: 1, // deduce decade tag (1980s, etc.) from album year for regular albums
  129. ops_always_edition: 1, // (only new uploads) don't use original release but always specific edition (unify with other trackers)
  130. dragdrop_patch_to_ptpimgit: 1,
  131. sacd_decoder: 'foobar2000\'s SACD decoder (DSD2PCM direct / 64fp / 30kHz lowpass)',
  132. ptpimg_api_key: '',
  133. catbox_userhash: '',
  134. selfrelease_label: 'self-released',
  135. discogs_key: '', // Applicxation/Consumer Key
  136. discogs_secret: '', // Application/Consumer Secret
  137. //soundcloud_clientid: '',
  138. tidal_userid: '',
  139. tidal_userpassword: '',
  140. tidal_token: '',
  141. tidal_clientid: '',
  142. tidal_clientkey: '',
  143. malzo_uid: '',
  144. malzo_password: '',
  145. upcoming_tags: '', // add this tag(s) to upcoming releases (requests); empty to disable
  146. remap_texttools_newlines: 0, // convert underscores to linebreaks (ambiguous)
  147. honour_rg: 0, // do a reminder on missing RG info; off by default
  148. honour_dr: 0, // do a reminder on missing DR info (only for Hi-Res tracks); off by default
  149. messages_verbosity: 0,
  150. // request specific
  151. request_default_bounty: 0, // set this bounty in MB after successfull fill of request form / 0 for disable
  152. always_request_perfect_flac: 0,
  153. include_tracklist_in_request: 0, // 0: include one line summary only; 1: include full tracklisting
  154. // tracklist specific
  155. tracklist_style: 1, // 1: classic with components colouring, 2: propertional font right-justified, 3: classic center aligned
  156. sort_tracklist: 1,
  157. reformat_trackartist: 1, // (if track artist differs from main artist) rebuild track artist from partial track artists, turn off if generating wrong track artists
  158. max_tracklist_width: 80, // right margin of the right aligned tracklist. should not exceed the group description width on any device
  159. tracklist_size: 2, // PHPBB fonst size
  160. title_separator: '. ', // divisor of track# and title
  161. pad_leader: ' ',
  162. bpm_summary: 1,
  163. tracklist_head_color: '#62a6ad', // #4682B4 / #a7bdd0
  164. // classical tracklist only components colouring
  165. tracklist_disctitle_color: '#2bb7b7', // #bb831c
  166. tracklist_work_color: '#98984d', // #b16890
  167. tracklist_tracknumber_color: '#8899AA',
  168. tracklist_artist_color: '#b79665',
  169. tracklist_composer_color: '#8ca014',
  170. tracklist_duration_color: '#33a6cc', // #2196f3
  171. // online check paramaters
  172. strict_online_check: 0, // set to 1 for strict online check (metadata comparison is case sensitive)
  173. duration_divergency: 0.75, // maximum tolerated playlists difference in %
  174. vinyl_duration_divergency: 2.5, // maximum tolerated playlists difference in % for vinyl releases
  175. };
  176. var prefs = {
  177. save: function() {
  178. for (var iter in this) {
  179. if (typeof this[iter] != 'function' && this[iter] != undefined) GM_setValue(iter, this[iter]);
  180. }
  181. },
  182. };
  183. Object.keys(defaultPrefs).forEach(key => { prefs[key] = GM_getValue(key, defaultPrefs[key]) });
  184.  
  185. document.head.appendChild(document.createElement('style')).innerHTML = `
  186. .ua-messages {
  187. text-indent: -2em;
  188. margin-left: 2em;
  189. font: 10pt Calibri, "Segoe UI", sans-serif;
  190. }
  191. .ua-messages-bg { padding: 15px; text-align: left; background-color: darkslategray; }
  192.  
  193. .ua-critical { color: red; font-weight: bold; font-size: 12pt; }
  194. .ua-warning { color: #ff8d00; font-weight: 500; font-size: 11pt; }
  195. .ua-notice { color: #e3d67b; }
  196. .ua-info { color: white; }
  197.  
  198. .ua-button { vertical-align: middle; background-color: transparent; }
  199. .ua-button2 { /*color: beige; */width: 13em; font: 300 x-small "Segoe UI", Calibri, sans-serif; }
  200. .ua-input {
  201. font: 600 x-small "Segoe UI", Calibri, sans-serif;
  202. color: slategray; background-color: antiquewhite;
  203. width: 620px; height: 40px;
  204. margin-top: 8px; margin-bottom: 8px;
  205. }
  206. .ua-input:focus { color: black; }
  207.  
  208. #cover-preview {
  209. width: 100%;
  210. /*box-shadow: 3px 3px 3px;*/
  211. }
  212. #cover-size {
  213. width: 100%;
  214. color: white; background-color: #0a4a75;
  215. font: 12px "Segoe UI", Calibri, sans-serif;
  216. text-align: center;
  217. /*padding-top: 5px;*/
  218. }
  219.  
  220. ::placeholder {
  221. font: 12pt Calibri, "Segoe UI", sans-serif;
  222. color: gray;
  223. opacity: 0.5;
  224. /*text-shadow: 0px 0px 3px #b4b4b4;*/
  225. font-weight: bold;
  226. }
  227. `;
  228.  
  229. var ref, tbl, elem, child, rehostItBtn, gazelleApiTimeFrame = {}, tfMessages = [];
  230. var spotifyCredentials = {}, tidalToken = {}, tidalSession = {}, discogsCredentials = {};
  231. var siteArtistsCache = {}, notSiteArtistsCache = [];
  232. var messages = null, autofill = false, dom, domParser = new DOMParser();
  233. const ctxt = document.createElement('canvas').getContext('2d');
  234.  
  235. if (isUpload) {
  236. if ((ref = document.querySelector('form#upload_table > div#dynamic_form')) == null) return;
  237. common1();
  238. let x = [];
  239. x.push(document.createElement('tr'));
  240. x[0].classList.add('ua-button');
  241. child = document.createElement('input');
  242. child.id = 'fill-from-text';
  243. child.value = 'Fill from text (overwrite)';
  244. child.type = 'button';
  245. child.className = 'ua-button2';
  246. child.onclick = fillFromText;
  247. x[0].append(child);
  248. elem.append(x[0]);
  249. x.push(document.createElement('tr'));
  250. x[1].classList.add('ua-button');
  251. child = document.createElement('input');
  252. child.id = 'fill-from-text-weak';
  253. child.value = 'Fill from text (keep values)';
  254. child.type = 'button';
  255. child.className = 'ua-button2';
  256. child.onclick = fillFromText;
  257. x[1].append(child);
  258. elem.append(x[1]);
  259. common2();
  260. ref.parentNode.insertBefore(tbl, ref);
  261. } else if (isEdit) {
  262. if ((ref = document.querySelector('form.edit_form > div > div > input[type="submit"]')) == null) return;
  263. ref = ref.parentNode;
  264. ref.parentNode.insertBefore(document.createElement('br'), ref);
  265. common1();
  266. child = document.createElement('input');
  267. child.id = 'append-from-text';
  268. child.value = 'Fill from text (append)';
  269. child.type = 'button';
  270. child.className = 'ua-button2';
  271. child.style.height = '52px';
  272. child.onclick = fillFromText;
  273. elem.append(child);
  274. common2();
  275. tbl.style.marginBottom = '10px';
  276. ref.parentNode.insertBefore(tbl, ref);
  277. } else if (isRequestNew) {
  278. if ((ref = document.getElementById('categories')) == null) return;
  279. ref = ref.parentNode.parentNode.nextElementSibling;
  280. ref.parentNode.insertBefore(document.createElement('br'), ref);
  281. common1();
  282. child = document.createElement('input');
  283. child.id = 'fill-from-text-weak';
  284. child.value = 'Fill from URL';
  285. child.type = 'button';
  286. child.className = 'ua-button2';
  287. child.style.height = '52px';
  288. child.onclick = fillFromText;
  289. elem.append(child);
  290. common2();
  291. child = document.createElement('td');
  292. child.colSpan = 2;
  293. child.append(tbl);
  294. elem = document.createElement('tr');
  295. elem.append(child);
  296. ref.parentNode.insertBefore(elem, ref);
  297. } else if (isRequestEdit) {
  298. if ((ref = document.querySelector('input#button[type="submit"]')) == null) return;
  299. ref = ref.parentNode.parentNode;
  300. ref.parentNode.insertBefore(document.createElement('br'), ref);
  301. common1();
  302. child = document.createElement('input');
  303. child.id = 'append-from-text';
  304. child.value = 'Fill from text (append)';
  305. child.type = 'button';
  306. child.className = 'ua-button2';
  307. child.style.height = '52px';
  308. child.onclick = fillFromText;
  309. elem.append(child);
  310. common2();
  311. tbl.style.marginBottom = '10px';
  312. elem = document.createElement('tr');
  313. child = document.createElement('td');
  314. child.colSpan = 2;
  315. child.append(tbl);
  316. elem.append(child);
  317. ref.parentNode.insertBefore(elem, ref);
  318. }
  319.  
  320. function common1() {
  321. tbl = document.createElement('tr');
  322. tbl.style.backgroundColor = 'darkgoldenrod';
  323. tbl.style.verticalAlign = 'middle';
  324. elem = document.createElement('td');
  325. elem.style.textAlign = 'center';
  326. child = document.createElement('textarea');
  327. child.id = 'UA-data';
  328. child.name = 'UA-data';
  329. child.className = 'ua-input';
  330. child.spellcheck = false;
  331. child.placeholder = 'Paste/drop album from foobar2000 or release URL here';
  332. child.onpaste = uaInsert;
  333. if (!isNWCD) {
  334. child.ondrop = uaInsert;
  335. child.ondragover = clear0;
  336. if (isFirefox) child.oninput = fixFirefoxDropBug;
  337. } else child.ondrop = child.ondragstart = child.ondragover = function(evt) {
  338. evt.preventDefault();
  339. evt.stopPropagation();
  340. return false;
  341. };
  342. var desc = document.getElementById('body');
  343. if (desc != null && urlParser.test(desc.value)) {
  344. child.value = RegExp.$1;
  345. desc.value = '';
  346. if (prefs.autfill_delay > 0) {
  347. autofill = true;
  348. setTimeout(fillFromText, prefs.autfill_delay);
  349. };
  350. }
  351. elem.append(child);
  352. tbl.append(elem);
  353. elem = document.createElement('td');
  354. elem.style.textAlign = 'center';
  355. }
  356. function common2() {
  357. tbl.append(elem);
  358. var tb = document.createElement('tbody');
  359. tb.append(tbl);
  360. tbl = document.createElement('table');
  361. tbl.id = 'upload assistant';
  362. tbl.append(tb);
  363. }
  364.  
  365. if ((ref = document.getElementById('categories')) != null) {
  366. ref.addEventListener('change', function(e) {
  367. elem = document.getElementById('upload assistant');
  368. if (elem != null) elem.style.visibility = this.value < 4
  369. || ['Music', 'Applications', 'E-Books', 'Audiobooks'].includes(this.value) ? 'visible' : 'collapse';
  370. setTimeout(setHandlers, 2000);
  371. });
  372. }
  373.  
  374. if ((ref = document.getElementById('upload-table') || document.querySelector('form.edit_form')
  375. || document.getElementById('upload_table') || document.getElementById('request_form')) != null) {
  376. ref.ondragover = voidDragHandler1;
  377. ref.ondrop = voidDragHandler1;
  378. }
  379. setHandlers();
  380. if ((ref = isUpload ? document.getElementById('file') : null) != null) {
  381. ref.oninput = function(evt) { if (evt.target.files.length > 0) validataTorrentFile(evt.target.files[0]) };
  382. if (ref.files.length > 0) validataTorrentFile(ref.files[0]);
  383. }
  384. if (!isRED && (ref = document.querySelector('table#dnulist')) != null) {
  385. function toggleVisibility() {
  386. var show = ref.style.display.toLowerCase() == 'none';
  387. ref.style.display = show ? 'block' : 'none';
  388. ref.previousElementSibling.style.display = show ? 'block' : 'none';
  389. }
  390. toggleVisibility();
  391. if ((ref = document.querySelector('h3#dnu_header')) != null) {
  392. elem = ref.parentNode;
  393. child = document.createElement('a');
  394. child.href = '#';
  395. child.onclick = function(evt) {
  396. if ((ref = document.querySelector('table#dnulist')) != null) toggleVisibility();
  397. };
  398. child.append(ref);
  399. elem.prepend(child);
  400. }
  401. }
  402.  
  403. if (isRequestNew) {
  404. let title = document.querySelector('input[name="title"]');
  405. if (title != null) for (i = 1; i < 6; ++i) setTimeout(function(e) { title.readOnly = false }, i * 1000);
  406. }
  407.  
  408. Array.prototype.includesCaseless = function(str) {
  409. if (typeof str != 'string') return false;
  410. str = str.toLowerCase();
  411. return this.find(elem => typeof elem == 'string' && elem.toLowerCase() == str) != undefined;
  412. };
  413. Array.prototype.pushUnique = function(...items) {
  414. items.forEach(it => { if (!this.includes(it)) this.push(it) });
  415. return this.length;
  416. };
  417. Array.prototype.pushUniqueCaseless = function(...items) {
  418. items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  419. return this.length;
  420. };
  421. // Array.prototype.getUnique = function(prop) {
  422. // return this.every((it) => it[prop] && it[prop] == this[0][prop]) ? this[0][prop] : null;
  423. // };
  424. Array.prototype.equalTo = function(arr) {
  425. return Array.isArray(arr) && arr.length == this.length
  426. && Array.from(arr).sort().toString() == Array.from(this).sort().toString();
  427. };
  428. Array.prototype.equalCaselessTo = function(arr) {
  429. function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
  430. return Array.isArray(arr) && arr.length == this.length
  431. && arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
  432. };
  433. Array.prototype.homogeneous = function() {
  434. return this.every(elem => elem === this[0]);
  435. }
  436.  
  437. String.prototype.toASCII = function() {
  438. return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
  439. };
  440. String.prototype.trueLength = function() {
  441. return Array.from(this).length;
  442. //return this.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').length;
  443. // var index = 0, width = 0, len = 0;
  444. // while (index < this.length) {
  445. // var point = this.codePointAt(index);
  446. // width = 0;
  447. // while (point) {
  448. // ++width;
  449. // point = point >> 8;
  450. // }
  451. // index += Math.round(width / 2);
  452. // ++len;
  453. // }
  454. // return len;
  455. };
  456. String.prototype.flatten = function() {
  457. return this.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
  458. };
  459. String.prototype.expand = function() {
  460. return this.replace(/\x1D/g, '\r').replace(/\x1C/g, '\n');
  461. };
  462. String.prototype.titleCase = function() {
  463. return this.toLowerCase().split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(' ');
  464. };
  465. String.prototype.collapseGaps = function() {
  466. return this.replace(/(?:[ \t]*\r?\n){3,}/g, '\n\n').replace(/\[(\w+)\]\[\/\1\]/ig,'').trim();
  467. };
  468. Date.prototype.getDateValue = function() {
  469. return Math.floor((this.getTime() / 1000 / 60 - this.getTimezoneOffset()) / 60 / 24);
  470. };
  471. File.prototype.getText = function(encoding) {
  472. return new Promise(function(resolve, reject) {
  473. var reader = new FileReader();
  474. reader.onload = function() { resolve(reader.result) };
  475. reader.onerror = reader.ontimeout = error => { reject('FileReader error (' + this.name + ')') };
  476. reader.readAsText(this, encoding);
  477. }.bind(this));
  478. };
  479. class HTML extends String { };
  480.  
  481. const excludedCountries = [
  482. /\b(?:United\s+States|USA?)\b/,
  483. /\b(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|UK)\b/,
  484. /\b(?:Europe|European\s+Union|EU)\b/,
  485. /\b(?:Unknown)\b/,
  486. ];
  487.  
  488. class TagManager extends Array {
  489. constructor(...tags) {
  490. super();
  491. this.presubstitutions = [
  492. [/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
  493. [/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
  494. [/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
  495. [/\s*,\s*(?:&\s*|and\s+)/i, ' & '],
  496. ];
  497. this.substitutions = [
  498. [/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
  499. [/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
  500. [/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
  501. ['AOR', 'album.oriented.rock'],
  502. [/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
  503. [/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
  504. [/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
  505. ['World', 'world.music'],
  506. [/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
  507. [/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhytm.and.blues'],
  508. [/\b(?:Soundtracks?)$/i, 'score'],
  509. ['Electro', 'electronic'],
  510. ['Metal', 'heavy.metal'],
  511. ['NonFiction', 'non.fiction'],
  512. ['Rap', 'hip.hop'],
  513. ['NeoSoul', 'neo.soul'],
  514. ['NuJazz', 'nu.jazz'],
  515. [/^J[\s\-]Pop$/i, 'jpop'],
  516. [/^K[\s\-]Pop$/i, 'jpop'],
  517. [/^J[\s\-]Rock$/i, 'jrock'],
  518. ['Hardcore', 'hardcore.punk'],
  519. ['Garage', 'garage.rock'],
  520. [/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
  521. [/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
  522. [/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
  523. [/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
  524. [/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
  525. ['GoaTrance', 'goa.trance'],
  526. [/^Female\s+Vocal\w*$/i, 'female.vocalist'],
  527. ['Contemporary R&B', 'contemporary.rhytm.and.blues'],
  528. // Country aliases
  529. ['Canada', 'canadian'],
  530. ['Australia', 'australian'],
  531. ['New Zealand', 'new.zealander'],
  532. ['Japan', 'japanese'],
  533. ['Taiwan', 'thai'],
  534. ['China', 'chinese'],
  535. ['Singapore', 'singaporean'],
  536. [/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
  537. ['Turkey', 'turkish'],
  538. ['Israel', 'israeli'],
  539. ['France', 'french'],
  540. ['Germany', 'german'],
  541. ['Spain', 'spanish'],
  542. ['Italy', 'italian'],
  543. ['Sweden', 'swedish'],
  544. ['Norway', 'norwegian'],
  545. ['Finland', 'finnish'],
  546. ['Greece', 'greek'],
  547. [/^(?:Netherlands|Holland)$/i, 'dutch'],
  548. ['Belgium', 'belgian'],
  549. ['Luxembourg', 'luxembourgish'],
  550. ['Denmark', 'danish'],
  551. ['Switzerland', 'swiss'],
  552. ['Austria', 'austrian'],
  553. ['Portugal', 'portugese'],
  554. ['Ireland', 'irish'],
  555. ['Scotland', 'scotish'],
  556. ['Iceland', 'icelandic'],
  557. [/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
  558. [/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
  559. ['Hungary', 'hungarian'],
  560. ['Poland', 'polish'],
  561. ['Estonia', 'estonian'],
  562. ['Latvia', 'latvian'],
  563. ['Lithuania', 'lithuanian'],
  564. ['Moldova', 'moldovan'],
  565. ['Armenia', 'armenian'],
  566. ['Ukraine', 'ukrainian'],
  567. ['Yugoslavia', 'yugoslav'],
  568. ['Serbia', 'serbian'],
  569. ['Slovenia', 'slovenian'],
  570. ['Croatia', 'croatian'],
  571. ['Macedonia', 'macedonian'],
  572. ['Montenegro', 'montenegrin'],
  573. ['Romania', 'romanian'],
  574. ['Malta', 'maltese'],
  575. ['Brazil', 'brazilian'],
  576. ['Mexico', 'mexican'],
  577. ['Argentina', 'argentinean'],
  578. ['Jamaica', 'jamaican'],
  579. // Books
  580. ['Beletrie', 'fiction'],
  581. ['Satira', 'satire'],
  582. ['Komiks', 'comics'],
  583. ['Komix', 'comics'],
  584. // Removals
  585. ['Unknown'],
  586. ['Other'],
  587. ['New'],
  588. ['Ostatni'],
  589. ['Knihy'],
  590. ['Audioknihy'],
  591. ['dsbm'],
  592. [/^(?:Audio\s*kniha|Audio\s*Book)$/i],
  593. ].concat(excludedCountries.map(it => [it]));
  594. this.splits = [
  595. ['Alternative', 'Indie'],
  596. ['Rock', 'Pop'],
  597. ['Soul', 'Funk'],
  598. ['Ska', 'Rocksteady'],
  599. ['Jazz Fusion', 'Jazz Rock'],
  600. ['Rock', 'Pop'],
  601. ['Jazz', 'Funk'],
  602. ];
  603. this.additions = [
  604. [/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
  605. [/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Vocal|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Latin|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
  606. [/^(?:Opera)$/i, 'classical'],
  607. [/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
  608. [/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
  609. [/^(?:Symphony)$/i, 'classical'],
  610. [/^(?:Sacred\s+Vocal)\b/i, 'classical'],
  611. [/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
  612. ];
  613. if (tags.length > 0) this.add(...tags);
  614. }
  615.  
  616. add(...tags) {
  617. var added = 0;
  618. for (var tag of tags) {
  619. if (typeof tag != 'string') continue;
  620. qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
  621. this.presubstitutions.forEach(k => { if (k[0].test(tag)) tag = tag.replace(k[0], k[1]) });
  622. tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(function(tag) {
  623. //qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
  624. tag = tag.toASCII().replace(/\(.*?\)|\[.*?\]|\{.*?\}/g, '').trim();
  625. if (tag.length <= 0 || tag == '?') return null;
  626. function test(obj) {
  627. return typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase()
  628. || obj instanceof RegExp && obj.test(tag);
  629. }
  630. for (var k of this.substitutions) {
  631. if (test(k[0])) {
  632. if (k.length >= 1) added += this.add(...k.slice(1));
  633. else addMessage('invalid tag \'' + tag + '\' found', 'warning');
  634. return;
  635. }
  636. }
  637. for (k of this.additions) {
  638. if (test(k[0])) added += this.add(...k.slice(1));
  639. }
  640. for (k of this.splits) {
  641. if (new RegExp('^' + k[0] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[1] + '$', 'i').test(tag)) {
  642. added += this.add(k[0], k[1]); return;
  643. }
  644. if (new RegExp('^' + k[1] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[0] + '$', 'i').test(tag)) {
  645. added += this.add(k[0], k[1]); return;
  646. }
  647. }
  648. tag = tag.
  649. replace(/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1').
  650. replace(/\b(?:Alt\.)(?=\s+)/i, 'Alternative').
  651. replace(/^[3-9]0s$/i, '19$0').
  652. replace(/^[0-2]0s$/i, '20$0').
  653. replace(/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2').
  654. replace(/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and ').
  655. replace(/[\s\-\−\—\–\_\.\,\'\`\~]+/g, '.').
  656. replace(/[^\w\.]+/g, '').
  657. toLowerCase();
  658. if (tag.length >= 2 && !this.includes(tag)) {
  659. this.push(tag);
  660. ++added;
  661. }
  662. }.bind(this));
  663. }
  664. return added;
  665. }
  666. toString() { return Array.from(this).sort().join(', ') }
  667. };
  668.  
  669. return;
  670.  
  671. function fillFromText(evt) {
  672. if (evt == undefined && !autofill) return;
  673. autofill = false;
  674. var overwrite = this.id == 'fill-from-text';
  675. var clipBoard = document.getElementById('UA-data');
  676. if (clipBoard == null) return false;
  677. const VA = 'Various Artists';
  678. messages = document.getElementById('UA-messages');
  679. //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
  680. //if (typeof clipBoard != 'string') return false;
  681. var i, matches, sourceUrl, category = document.getElementById('categories'), xhr = new XMLHttpRequest();
  682. if (category == null && document.getElementById('releasetype') != null
  683. || category != null && (category.value == 0 || category.value == 'Music')) return fillFromText_Music();
  684. if (category != null && (category.value == 1 || category.value == 'Applications')) return fillFromText_Apps();
  685. if (category != null && (category.value == 2 || category.value == 3
  686. || category.value == 'E-Books' || category.value == 'Audiobooks')) return fillFromText_Ebooks();
  687. return category == null ? fillFromText_Apps(true).catch(reason => fillFromText_Ebooks()) : Promise.reject('no category');
  688.  
  689. function fillFromText_Music() {
  690. if (messages != null) messages.parentNode.removeChild(messages);
  691. const dcRlsParser = /^https?:\/\/(?:\w+\.)*discogs\.com\/releases?\/(\d+)(?=$|\/|\?)/i;
  692. const mbrRlsParser = /^https?:\/\/musicbrainz\.org\/(?:\w+\/)*release\/([\w\-]+)/i;
  693. const divs = ['—', '⸺', '⸻'];
  694. const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  695. const multiArtistParsers = [
  696. /\s*[,;](?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?\s*/,
  697. /\s+[\/\|\×]\s+/,
  698. ];
  699. const pseudoArtistParsers = [
  700. /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
  701. /^(?:traditional|lidová)$/i,
  702. /\b(?:traditional|lidová)$/,
  703. /^(?:tradiční|lidová)\s+/,
  704. /^(?:[Aa]nonym)/,
  705. /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  706. /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  707. /^(?:Various\s+Composers)$/i,
  708. ];
  709. var isVA, onlineSource = urlParser.test(clipBoard.value) && RegExp.$1;
  710. return (onlineSource ? fetchOnline_Music(onlineSource) :
  711. Promise.resolve(clipBoard.value.split(/(?:\r?\n)+/).filter(line => line.trim().length > 0).map(function(line, ndx) {
  712. const fields = [
  713. /* 00 */ 'artist', 'album', 'album_year', 'release_date', 'label', 'catalog', 'country', 'encoding',
  714. /* 08 */ 'codec', 'codec_profile', 'bitrate', 'bd', 'sr', 'channels', 'media', 'genre', 'discnumber',
  715. /* 17 */ 'totaldiscs', 'discsubtitle', 'tracknumber', 'totaltracks', 'title', 'track_artist', 'performer',
  716. /* 24 */ 'composer', 'conductor', 'remixer', 'compiler', 'producer', 'duration', 'samples', 'filesize',
  717. /* 32 */ 'ag', 'dr', 'vendor', 'url', 'dirpath', 'description', 'identifiers',
  718. ];
  719. var metaData = line.expand().split('\x1E'), track = { identifiers: {} }, identifiers = [];
  720. if (metaData.length < fields.length) {
  721. console.error('invalid data format for track #' + (ndx + 1) + ': length:', metaData.length,
  722. '(' + fields.length + '); metaData:', metaData, '; line:', line);
  723. throw 'invalid data format for track #' + (ndx + 1) + ' (see console log for details)';
  724. } else if (metaData.length > fields.length) {
  725. console.warn('unexpected data format for track #' + (ndx + 1) + ': length:', metaData.length,
  726. '(' + fields.length + '); metaData:', metaData, '; line:', line);
  727. }
  728. fields.forEach(function(propName) {
  729. if (propName == 'identifiers') {
  730. identifiers = metaData.shift().trim().split(/\s+/);
  731. return;
  732. }
  733. track[propName] = metaData.shift();
  734. if (track[propName] === '') track[propName] = undefined;
  735. });
  736. identifiers.forEach(function(id) {
  737. if (/^([\w\-]+)[=:](\S*)$/.test(id)) track.identifiers[RegExp.$1.toUpperCase()] = RegExp.$2.replace(/\x1B/g, ' ');
  738. });
  739. if (prefs.check_whitespace) Object.keys(track).forEach(function(propName) {
  740. if (typeof track[propName] != 'string') return;
  741. if (propName != 'description' && (track[propName].includes('\r') || track[propName].includes('\n'))) {
  742. track[propName] = track[propName].replace(/[\r\n]+/g, '');
  743. addMessage('track #' + (ndx + 1) + ' contains linebreaks in tag <' + propName + '>', 'warning');
  744. }
  745. if ((i = propName == 'description' ? /[\x00-\x08\x0B\x0C\x0E-\x19]+/g : /[\x00-\x19]+/g).test(track[propName])) {
  746. track[propName] = track[propName].replace(i, '');
  747. addMessage('track #' + (ndx + 1) + ' contains control codes in tag <' + propName + '>', 'warning');
  748. }
  749. if (/^[\s\xA0]+$/.test(track[propName])) {
  750. track[propName] = undefined;
  751. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains only whitespace', 'warning');
  752. } else if (/^[\s\xA0]+|[\s\xA0]+$/.test(track[propName])) {
  753. track[propName] = track[propName].trim();
  754. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains leading/trailing whitespace', 'warning');
  755. }
  756. if (/[ \xA0]{2,}/.test(track[propName])) {
  757. track[propName] = track[propName].replace(/[ \xA0]{2,}/g, ' ')
  758. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains multiple spaces', 'warning');
  759. }
  760. });
  761. if (track.description == '.') track.description = undefined; else if (track.description) {
  762. if (prefs.remap_texttools_newlines)
  763. track.description = track.description.replace(/__/g, '\r\n').replace(/_/g, '\n') // ambiguous
  764. track.description = track.description.collapseGaps();
  765. }
  766. ['bitrate', 'bd', 'sr', 'channels', 'totaldiscs', 'totaltracks', 'samples', 'filesize', 'dr'].forEach(function(propName) {
  767. if (track[propName] !== undefined) track[propName] = parseInt(track[propName]);
  768. });
  769. ['duration'].forEach(function(propName) {
  770. if (track[propName] !== undefined) track[propName] = parseFloat(track[propName]);
  771. });
  772. if (track.album_year) track.album_year = extractYear(track.album_year) || NaN;
  773. ['ag', 'tg', 'ap', 'tp'].forEach(function(propName) {
  774. track[propName] = /^([\+\-]?\d+(?:\.\d+)?)\s*dB\b/i.test(track[propName]) ? parseFloat(RegExp.$1) : undefined;
  775. });
  776. return track;
  777. }))
  778. ).then(parseTracks).catch(e => { if (e) addMessage(e, 'critical') });
  779.  
  780. function parseTracks(tracks) {
  781. if (tracks.length <= 0) {
  782. clipBoard.value = '';
  783. throw 'no tracks found';
  784. }
  785. var albumBitrate = 0, totalTime = 0, albumSize = 0, media, release = { totaldiscs: 1, srs: [] };
  786. tracks.forEach(function(track) {
  787. if (!track.artist) {
  788. clipBoard.value = '';
  789. throw new HTML('main artist must be defined in every track' + ruleLink('2.3.16.4'));
  790. }
  791. if (!track.album) {
  792. clipBoard.value = '';
  793. throw new HTML('album title must be defined in every track' + ruleLink('2.3.16.4'));
  794. }
  795. if (!track.tracknumber) {
  796. clipBoard.value = '';
  797. throw new HTML('all track numbers must be defined' + ruleLink('2.3.16.4'));
  798. }
  799. if (!track.title) {
  800. clipBoard.value = '';
  801. throw new HTML('all track titles must be defined' + ruleLink('2.3.16.4'));
  802. }
  803. if (track.duration != undefined && isUpload && (isNaN(track.duration) || track.duration <= 0)) {
  804. clipBoard.value = '';
  805. throw 'invalid track #' + track.tracknumber + ' length: ' + track.duration;
  806. }
  807. if (track.codec && !['FLAC', 'MP3', 'AAC', 'DTS', 'AC3'].includes(track.codec)) {
  808. clipBoard.value = '';
  809. throw 'disallowed codec present (' + track.codec + ')';
  810. }
  811. if (/\b(?:MQAEncode) v(\d+(?:\.\d+)*)\b/.test(track.vendor)) {
  812. clipBoard.value = '';
  813. throw 'MQA encoded release (' + RegExp.lastMatch + ')';
  814. }
  815. if (/^(?:#?N\/A|No\s+Label|Not\s+On\s+Label)$/i.test(track.label)) track.label = undefined;
  816. if (/^(?:#?N\/A)$/i.test(track.catalog)) track.catalog = undefined;
  817. if (/^(\d+)\s*[\/]\s*(\d+)$/.test(track.tracknumber)) { // track/totaltracks
  818. addMessage('nonstandard track number formatting for track ' + RegExp.$1 + ': ' + track.tracknumber, 'warning');
  819. track.tracknumber = RegExp.$1;
  820. if (!track.totaltracks) track.totaltracks = parseInt(RegExp.$2);
  821. } else if (/^(\d+)[\.\-](\d+)$/.test(track.tracknumber)) { // discnumber.tracknumber
  822. addMessage('nonstandard track number formatting for track ' + RegExp.$2 + ': ' + track.tracknumber, 'warning');
  823. if (!track.discnumber) track.discnumber = parseInt(RegExp.$1);
  824. track.tracknumber = RegExp.$2;
  825. }
  826. if (track.discnumber) {
  827. if (/^(\d+)\s*\/\s*(\d+)/.test(track.discnumber)) {
  828. addMessage('nonstandard disc number formatting for track ' + track.tracknumber + ': ' + track.discnumber, 'warning');
  829. track.discnumber = RegExp.$1;
  830. if (!track.totaldiscs) track.totaldiscs = RegExp.$2;
  831. } else track.discnumber = parseInt(track.discnumber);
  832. if (isNaN(track.discnumber)) {
  833. addMessage('invalid disc numbering for track ' + track.tracknumber, 'warning');
  834. track.discnumber = undefined;
  835. }
  836. if (track.discnumber > release.totaldiscs) release.totaldiscs = track.discnumber;
  837. }
  838. totalTime += track.duration;
  839. albumBitrate += track.bitrate * track.duration;
  840. albumSize += track.filesize;
  841. });
  842. if (!tracks.every(track => track.discnumber > 0) && !tracks.every(track => !track.discnumber)) {
  843. addMessage('inconsistent release (mix of tracks with and without disc number)', 'warning');
  844. }
  845. if (!onlineSource && release.totaldiscs > 1 && tracks.some(it => it.totaldiscs != release.totaldiscs))
  846. addMessage('at least one track not having properly set TOTALDISCS (' + release.totaldiscs + ')', 'info');
  847.  
  848. function setUniqueProperty(propName, propNameLiteral) {
  849. let homogeneous = new Set(tracks.map(it => it[propName]).filter(it => it != undefined && it != null));
  850. if (homogeneous.size > 1) {
  851. var diverses = '', it = homogeneous.values(), val;
  852. while (!(val = it.next()).done) diverses += '<br>\t' + val.value;
  853. clipBoard.value = '';
  854. throw new HTML('mixed releases not accepted (' + propNameLiteral + ') - supposedly user compilation' + diverses);
  855. }
  856. release[propName] = homogeneous.values().next().value;
  857. }
  858. setUniqueProperty('artist', 'album artist');
  859. setUniqueProperty('album', 'album title');
  860. setUniqueProperty('album_year', 'album year');
  861. setUniqueProperty('release_date', 'release date');
  862. setUniqueProperty('encoding', 'encoding');
  863. setUniqueProperty('codec', 'codec');
  864. setUniqueProperty('codec_profile', 'codec profile');
  865. setUniqueProperty('vendor', 'vendor');
  866. setUniqueProperty('media', 'media');
  867. setUniqueProperty('channels', 'channels');
  868. setUniqueProperty('label', 'label');
  869. setUniqueProperty('country', 'country');
  870.  
  871. tracks.forEach(function(iter) {
  872. setProperty('trackArtists', 'track_artist');
  873. setProperty('totalTracks', 'totaltracks');
  874. setProperty('discSubtitles', 'discsubtitle');
  875. setProperty('composers', 'composer');
  876. setProperty('catalogs', 'catalog');
  877. setProperty('bitrates', 'bitrate');
  878. setProperty('bds', 'bd');
  879. setProperty('ags', 'ag');
  880. setProperty('drs', 'dr');
  881. if (iter.sr) if (typeof release.srs[iter.sr] != 'number') release.srs[iter.sr] = iter.duration;
  882. else release.srs[iter.sr] += iter.duration;
  883. setProperty('dirpaths', 'dirpath');
  884. setProperty('descriptions', 'description');
  885. setProperty('genres', 'genre');
  886. setProperty('urls', 'url');
  887. setProperty('coverUrls', 'cover_url');
  888.  
  889. function setProperty(propName, trackProp) {
  890. if (!Array.isArray(release[propName])) release[propName] = [];
  891. if (iter[trackProp] !== undefined && iter[trackProp] !== null && (typeof iter[trackProp] != 'string'
  892. || iter[trackProp].length > 0) && !release[propName].includes(iter[trackProp])) {
  893. release[propName].push(iter[trackProp]);
  894. }
  895. }
  896. });
  897. if (!release.totalTracks) addMessage('total tracks not set', 'warning');
  898. if (release.totalTracks.length > 0) {
  899. if (release.totalTracks.length > 1) {
  900. addMessage('total tracks not consistent across release: ' + release.totalTracks, 'warning');
  901. } else if (release.totalTracks[0] != tracks.length) {
  902. addMessage('total tracks not matching tracklist length: ' +
  903. release.totalTracks[0] + ' != ' + tracks.length, 'warning');
  904. }
  905. }
  906. tracks.forEach(function(track1, ndx1) {
  907. if (tracks.some((track2, ndx2) => ndx2 < ndx1 && track1.tracknumber == track2.tracknumber
  908. && track1.discnumber == track2.discnumber && track1.discsubtitle == track2.discsubtitle)) {
  909. addMessage('duplicate track ' + (track1.discnumber ? track1.discnumber + '-' : '') +
  910. (track1.discsubtitle ? track1.discsubtitle + '-' : '') + track1.tracknumber, 'warning');
  911. }
  912. });
  913. function validatorFunc(arr, validator, str) {
  914. if (arr.length <= 0 || !arr.some(validator)) return true;
  915. clipBoard.value = '';
  916. throw 'disallowed ' + str + ' present (' + arr.filter(validator) + ')';
  917. }
  918. validatorFunc(release.bds, bd => ![16, 24].includes(bd), 'bit depths');
  919. validatorFunc(Object.keys(release.srs),
  920. sr => sr < 44100 || sr > 192000 || sr % 44100 != 0 && sr % 48000 != 0, 'sample rates');
  921. if (!onlineSource && prefs.honour_rg && tracks.some(track => track.ag === undefined))
  922. addMessage('at least one track is missing RG info', 'notice');
  923. if (!onlineSource && release.ags.length > 1) addMessage('album RG differs across the release', 'notice');
  924. if (!onlineSource && prefs.honour_dr && tracks.some(track => track.bd > 16 && track.dr === undefined))
  925. addMessage('at least one high resolution track is missing DR info', 'notice');
  926. if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))) {
  927. (release.coverUrls.length > 0 ? setCover(release.coverUrls[0]) : Promise.reject('No cover URL'))
  928. .catch(getCoverOnline).catch(searchCoverOnline);
  929. }
  930. var albumBPM = Math.round(tracks.reduce(function(acc, track) {
  931. return acc + parseInt(track.identifiers.BPM) * track.duration;
  932. }, 0) / totalTime);
  933. var composerEmphasis = false, isFromDSD = false, isClassical = false;
  934. var canSort = tracks.every((tr1, ndx1) => tracks.every((tr2, ndx2) => ndx1 == ndx2
  935. || tr1.tracknumber != tr2.tracknumber || tr1.discnumber != tr2.discnumber));
  936. var yadg_prefil = '', releaseType, editionTitle, iter, rx;
  937. var barCode = getHomoIdentifier('BARCODE');
  938. if (barCode) barCode = parseInt(barCode.replace(/\s+/g, ''));
  939. if (!Number.isInteger(barCode)) {
  940. if (release.catalogs.length == 1) barCode = parseInt(release.catalogs[0].replace(/[\s\-]/g, ''));
  941. if (!Number.isInteger(barCode) || barCode < 10**10) barCode = undefined;
  942. }
  943. var tags = new TagManager();
  944. albumBitrate /= totalTime;
  945. if (tracks.every(it => /^(?:Single)$/i.test(it.identifiers.RELEASETYPE))
  946. || tracks.length == 1 && totalTime > 0 && totalTime < prefs.single_threshold) {
  947. releaseType = getReleaseIndex('Single');
  948. } else if (tracks.every(it => it.identifiers.RELEASETYPE == 'EP')) {
  949. releaseType = getReleaseIndex('EP');
  950. } else if (tracks.every(it => /^soundtrack$/i.test(it.identifiers.RELEASETYPE))) {
  951. releaseType = getReleaseIndex('Soundtrack');
  952. tags.add('score');
  953. composerEmphasis = true;
  954. }
  955. if (release.genres.length > 0) {
  956. const classicalGenreParsers = [
  957. /\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,
  958. ];
  959. release.genres.forEach(function(genre) {
  960. classicalGenreParsers.forEach(function(classicalGenreParser) {
  961. if (classicalGenreParser.test(genre) && !/\b(?:metal|rock|pop)\b/i.test(genre)) {
  962. composerEmphasis = true;
  963. isClassical = true
  964. }
  965. });
  966. if (/\b(?:Jazz|Vocal)\b/i.test(genre) && !/\b(?:Nu|Future|Acid)[\s\-\−\—\–]*Jazz\b/i.test(genre)
  967. && !/\bElectr(?:o|ic)[\s\-\−\—\–]?Swing\b/i.test(genre)) {
  968. composerEmphasis = true;
  969. }
  970. if (/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i.test(genre)) {
  971. if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
  972. composerEmphasis = true;
  973. }
  974. if (/\b(?:Christmas\s+Music)\b/i.test(genre)) {
  975. composerEmphasis = true;
  976. }
  977. tags.add(...genre.split(/\s*\|\s*/));
  978. });
  979. if (release.genres.length > 1) addMessage('inconsistent genre accross album: ' + release.genres, 'warning');
  980. }
  981. if (!onlineSource && isClassical && !tracks.every(track => track.composer)) {
  982. addMessage(new HTML('all tracks composers must be set for clasical music' + ruleLink('2.3.17')), 'warning');
  983. //return false;
  984. }
  985. // Processing artists: recognition, splitting and dividing to categores
  986. var ajaxRejects = 0;
  987. const ampersandParsers = [
  988. /\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
  989. /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
  990. /\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
  991. /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
  992. ];
  993. const featParsers = [
  994. /\s+(?:meets)\s+(.*?)\s*$/i,
  995. /\s+(?:[Ww]ith)\s+(?!his\b|her\b|Friends$|Strings$)(.*?)\s*$/,
  996. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:eaturing|t\.))\s+(.*?)\s*$/,
  997. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:ea)?t\.)\s+(.*?)\s*$/, // [0]
  998. /\s+\[\s*f(?:eat(?:\.|uring)|t\.)\s+([^\[\]]+?)\s*\]/i, // [1]
  999. /\s+\(\s*f(?:eat(?:\.|uring)|t\.)\s+([^\(\)]+?)\s*\)/i, // [2]
  1000. /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i, // [3]
  1001. /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i, // [4]
  1002. /\s+\[\s*with\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/, // [5]
  1003. /\s+\(\s*with\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/, // [6]
  1004. ];
  1005. const remixParsers = [
  1006. /\s+\((?:The\s+)Remix(?:e[sd])?\)/i,
  1007. /\s+\[(?:The\s+)Remix(?:e[sd])?\]/i,
  1008. /\s+(?:The\s+)Remix(?:e[sd])?\s*$/i,
  1009. /^(Remixes)\b/,
  1010. /\s+\(([^\(\)]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\)/i,
  1011. /\s+\[([^\[\]]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\]/i,
  1012. /\s+\(\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
  1013. /\s+\[\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
  1014. ];
  1015. const otherArtistsParsers = [
  1016. [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
  1017. [/^()(.*?)\s+\(conductor\)$/i, 4],
  1018. //[/^()(.*?)\s+\(.*\)$/i, 1],
  1019. ];
  1020. const artistStrips = [
  1021. /\s+(?:aka|AKA)\.?\s+(.*)$/,
  1022. /\s+\(([^\(\)]+)\)$/,
  1023. /\s+\[([^\[\]]+)\]$/,
  1024. /\s+\{([^\{\}]+)\}$/,
  1025. ];
  1026. const roleCollisions = [
  1027. [4, 5], // main
  1028. [0, 4], // guest
  1029. [], // remixer
  1030. [], // composer
  1031. [], // conductor
  1032. [], // DJ/compiler
  1033. [], // producer
  1034. ];
  1035. isVA = vaParser.test(release.artist);
  1036. var artists = [];
  1037. for (i = 0; i < 7; ++i) artists[i] = [];
  1038.  
  1039. if (!isVA) {
  1040. addArtists(0, yadg_prefil = spliceGuests(release.artist));
  1041. if (ampersandParsers.some(rx => rx.test(yadg_prefil))) getSiteArtist(yadg_prefil); // priority cache record
  1042. }
  1043. var albumGuests = Array.from(artists[1]);
  1044.  
  1045. featParsers.slice(3).forEach(function(rx, ndx) {
  1046. if ((matches = rx.exec(release.album)) == null) return;
  1047. if (ndx >= 5 && !splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)))
  1048. .every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
  1049. addArtists(1, matches[1]);
  1050. addMessage('featured artist(s) in album title (' + release.album + ')', 'warning');
  1051. release.album = release.album.replace(rx, '');
  1052. });
  1053. remixParsers.slice(4).forEach(function(rx) {
  1054. if (rx.test(release.album)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1055. })
  1056. if (((matches = /^(.*?)\s+Presents\s+(.*)$/.exec(release.album)) != null
  1057. || isVA && (matches = (/\s+\(compiled\s+by\s+(.*?)\)\s*$/i.exec(release.album)
  1058. || /\s+compiled\s+by\s+(.*?)\s*$/i.exec(release.album))) != null) && looksLikeTrueName(matches[1])) {
  1059. addArtists(5, matches[1]);
  1060. if (!releaseType) releaseType = getReleaseIndex('Compilation');
  1061. }
  1062.  
  1063. for (iter of tracks) {
  1064. addTrackPerformers(iter.track_artist);
  1065. addTrackPerformers(iter.performer);
  1066. addArtists(2, iter.remixer);
  1067. addArtists(3, iter.composer);
  1068. addArtists(4, iter.conductor);
  1069. addArtists(5, iter.compiler);
  1070. addArtists(6, iter.producer);
  1071.  
  1072. if (iter.title) {
  1073. featParsers.slice(3).forEach(function(rx, ndx) {
  1074. if ((matches = rx.exec(iter.title)) == null) return;
  1075. if (ndx >= 5 && !splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)))
  1076. .every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
  1077. iter.track_artist = (!isVA && (!iter.track_artist || iter.track_artist.includes(matches[1])) ?
  1078. iter.artist : iter.track_artist) + ' feat. ' + matches[1];
  1079. addArtists(1, matches[1]);
  1080. addMessage('featured artist(s) in track title (#' + iter.tracknumber + ': ' + iter.title + ')', 'warning');
  1081. iter.title = iter.title.replace(rx, '');
  1082. });
  1083. if (!iter.remixer) remixParsers.slice(4).forEach(function(rx) {
  1084. if (rx.test(iter.title)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1085. });
  1086. }
  1087. if (isClassical && !iter.composer && /^([^\(\)\[\]\{\},:]+?)(?:\s*\(\d{4}\s*-\s*\d{4}\))/.test(iter.discsubtitle)) {
  1088. //track.composer = RegExp.$1;
  1089. addArtists(3, RegExp.$1);
  1090. }
  1091. }
  1092. for (i = 0; i < Math.round(tracks.length / 2); ++i) splitAmpersands();
  1093. albumGuests = splitAmpersands(albumGuests);
  1094.  
  1095. function addArtists(ndx, str) {
  1096. if (str) splitArtists(str).forEach(function(artist) {
  1097. artist = ndx != 0 ? strip(artist) : guessOtherArtists(artist);
  1098. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1099. && !artists[ndx].includesCaseless(artist)
  1100. && !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist))) artists[ndx].push(artist);
  1101. });
  1102. }
  1103. function addTrackPerformers(str) {
  1104. if (str) splitArtists(spliceGuests(str, 1)).forEach(function(artist) {
  1105. artist = guessOtherArtists(artist);
  1106. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1107. && !artists[0].includesCaseless(artist)
  1108. && (isVA || !artists[1].includesCaseless(artist))) artists[isVA ? 0 : 1].push(artist);
  1109. });
  1110. }
  1111. function splitArtists(str, parsers = multiArtistParsers) {
  1112. var result = [str];
  1113. parsers.forEach(function(parser) {
  1114. for (var i = result.length; i > 0; --i) {
  1115. var j = result[i - 1].split(parser).map(strip);
  1116. if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
  1117. && !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
  1118. }
  1119. });
  1120. return result;
  1121. }
  1122. function splitAmpersands(_artists) {
  1123. if (_artists) {
  1124. let result;
  1125. if (typeof _artists == 'string') result = splitArtists(_artists);
  1126. else if (Array.isArray(_artists)) result = Array.from(_artists);
  1127. else return [];
  1128. splitInternal(result);
  1129. return result;
  1130. }
  1131. for (let ndx = 0; ndx < artists.length; ++ndx) splitInternal(artists[ndx], roleCollisions[ndx]);
  1132.  
  1133. function splitInternal(refArr, roleCollisions) {
  1134. ampersandParsers.forEach(function(ampersandParser) {
  1135. for (var i = refArr.length; i > 0; --i) {
  1136. var j = refArr[i - 1].split(ampersandParser).map(strip);
  1137. if (j.length <= 1 || !j.some(it1 => artists.some(it2 => it2.includesCaseless(it1)))
  1138. && !j.every(looksLikeTrueName) || getSiteArtist(refArr[i - 1])) continue;
  1139. refArr.splice(i - 1, 1, ...j.filter(function(artist) {
  1140. return !refArr.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist))
  1141. && (!Array.isArray(roleCollisions) || !roleCollisions.some(n => artists[n].includesCaseless(artist)));
  1142. }));
  1143. }
  1144. });
  1145. }
  1146. }
  1147. function spliceGuests(str, level = 1) {
  1148. (level > 0 ? featParsers.slice(level) : featParsers).forEach(function(rx, ndx) {
  1149. var matches = rx.exec(str);
  1150. if (matches != null && (level + ndx < 8
  1151. || splitArtists(matches[1]).every((artist, ndx) => looksLikeTrueName(artist, 1)))) {
  1152. addArtists(1, matches[1]);
  1153. str = str.replace(rx, '');
  1154. }
  1155. });
  1156. return str;
  1157. }
  1158. function guessOtherArtists(name) {
  1159. otherArtistsParsers.forEach(function(it) {
  1160. if (!it[0].test(name)) return;
  1161. addArtists(it[1], RegExp.$2);
  1162. name = RegExp.$1;
  1163. });
  1164. return strip(name);
  1165. }
  1166. function getSiteArtist(artist) {
  1167. //if (isOPS) return undefined;
  1168. if (!artist || notSiteArtistsCache.includesCaseless(artist)) return null;
  1169. var key = Object.keys(siteArtistsCache).find(it => it.toLowerCase() == artist.toLowerCase());
  1170. if (key) return siteArtistsCache[key];
  1171. var now = new Date().getTime();
  1172. if (!gazelleApiTimeFrame.timeStamp || now > gazelleApiTimeFrame.timeStamp + 10100) {
  1173. gazelleApiTimeFrame.timeStamp = now;
  1174. gazelleApiTimeFrame.requestCounter = 0;
  1175. };
  1176. if (++gazelleApiTimeFrame.requestCounter > 5) {
  1177. console.debug('getSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
  1178. artist + '" (' + gazelleApiTimeFrame.requestCounter + ')');
  1179. if (prefs.messages_verbosity >= 2) addMessage('AJAX API request exceeding time frame: artistname="' +
  1180. artist + '" (' + gazelleApiTimeFrame.requestCounter + ')', 'notice');
  1181. ++ajaxRejects;
  1182. return undefined;
  1183. }
  1184. xhr.open('GET', document.location.origin + '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist), false);
  1185. xhr.send();
  1186. if (xhr.readyState != XMLHttpRequest.DONE || xhr.status != 200) {
  1187. console.log('getSiteArtist("' + artist + '"): XMLHttpRequest readyState:' + xhr.readyState + ' status:' + xhr.status);
  1188. return undefined; // error
  1189. }
  1190. try {
  1191. let response = JSON.parse(xhr.responseText);
  1192. if (response.status != 'success') {
  1193. notSiteArtistsCache.pushUniqueCaseless(artist);
  1194. return null;
  1195. }
  1196. return (siteArtistsCache[artist] = response.response);
  1197. } catch(e) {
  1198. console.warn('UA::getSiteArtist(): ' + e);
  1199. return undefined;
  1200. }
  1201. }
  1202. function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
  1203. function looksLikeTrueName(artist, index = 0) {
  1204. return twoOrMore(artist)
  1205. && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
  1206. && artist.split(/\s+/).length >= 2
  1207. && !pseudoArtistParsers.some(rx => rx.test(artist)) || typeof getSiteArtist(artist) == 'object';
  1208. }
  1209. function strip(art) {
  1210. return artistStrips.reduce(function(acc, rx, ndx) {
  1211. return ndx != 1 || rx.test(acc) && !notMonospaced(RegExp.$1) ? acc.replace(rx, '') : acc;
  1212. }, art);
  1213. }
  1214. function getArtists(trackArtist) {
  1215. if (!trackArtist || typeof trackArtist != 'string') trackArtist = '';
  1216. otherArtistsParsers.forEach(it => { if (it[0].test(trackArtist)) trackArtist = RegExp.$1 });
  1217. var result = [ [], [] ];
  1218. featParsers.slice(1).forEach(function(rx, ndx) {
  1219. if ((matches = rx.exec(trackArtist)) == null || ndx >= 7 && !looksLikeTrueName(matches[1], 1)) return;
  1220. splitAmpersands(matches[1]).forEach(artist => { result[1].pushUniqueCaseless(artist) });
  1221. trackArtist = trackArtist.replace(rx, '');
  1222. });
  1223. splitAmpersands(trackArtist).forEach(artist => { result[0].pushUniqueCaseless(artist) });
  1224. return result;
  1225. }
  1226.  
  1227. if (elementWritable(document.getElementById('artist') || document.getElementById('artist_0'))) {
  1228. let artistIndex = 0;
  1229. const enSorter = /^(?:The)\s+/;
  1230. catLoop: for (i = 0; i < artists.length; ++i) for (iter of artists[i]
  1231. .filter(artist => !roleCollisions[i].some(n => artists[n].includesCaseless(artist)))
  1232. .sort((a, b) => a.replace(enSorter, '').localeCompare(b.replace(enSorter, '')))) {
  1233. if (isUpload) {
  1234. var id = 'artist';
  1235. if (artistIndex > 0) id += '_' + artistIndex;
  1236. while ((ref = document.getElementById(id)) == null) AddArtistField();
  1237. } else {
  1238. while ((ref = document.querySelectorAll('input[name="artists[]"]')).length <= artistIndex) AddArtistField();
  1239. ref = ref[artistIndex];
  1240. }
  1241. if (ref == null) throw new Error('Failed to allocate artist fields');
  1242. ref.value = iter;
  1243. ref.nextElementSibling.value = i + 1;
  1244. if (++artistIndex >= 200) break catLoop;
  1245. }
  1246. if (overwrite && artistIndex > 0) while (document.getElementById('artist_' + artistIndex) != null) {
  1247. RemoveArtistField();
  1248. }
  1249. }
  1250.  
  1251. // Processing album title
  1252. const editionParsers = [
  1253. /\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,
  1254. /\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,
  1255. /\s+-\s+([^\[\]\(\)\-\−\—\–]*\b(?:(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Bonus\s+Track)\b[^\[\]\(\)\-\−\—\–]*|Reissue|Edition|Version|Promo|Enhanced|Release|Édition))$/i,
  1256. ];
  1257. const mediaParsers = [
  1258. [/\s+(?:\[(?:LP|Vinyl|12"|7")\]|\((?:LP|Vinyl|12"|7")\))$/, 'Vinyl'],
  1259. [/\s+(?:\[SA-?CD\]|\(SA-?CD\))$/, 'SACD'],
  1260. [/\s+(?:\[(?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\]|\((?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\))$/, isOPS ? 'BD' : 'Blu-Ray'],
  1261. [/\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/, 'DVD'],
  1262. ];
  1263. const releaseTypeParsers = [
  1264. [/\s+(?:-\s+Single|\[Single\]|\(Single\))$/i, 'Single', true, true],
  1265. [/\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/, 'EP', true, true],
  1266. [/\s+\((?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\(\)]*\)$/i, 'Live album', false, false],
  1267. [/\s+\[(?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\[\]]*\]$/i, 'Live album', false, false],
  1268. [/(?:^Live\s+(?:[aA]t|[Ii]n)\b|^Directo?\s+[Ee]n\b|\bUnplugged\b|\bAcoustic\s+Stage\b|\s+Live$)/, 'Live album', false, false],
  1269. [/\b(?:(?:Best\s+of|Greatest\s+Hits|Complete\s+(.+?\s+)(?:Albums|Recordings))\b|Collection$)|^The(\s+\w+)+Years$/i, 'Anthology', false, false],
  1270. ];
  1271. var album = release.album;
  1272. releaseTypeParsers.forEach(function(it) {
  1273. if (it[0].test(album)) {
  1274. if (it[2] || !releaseType) releaseType = getReleaseIndex(it[1]);
  1275. if (it[3]) album = album.replace(it[0], '');
  1276. }
  1277. });
  1278. rx = '\\b(?:Soundtrack|Score|Motion\\s+Picture|Series|Television|Original(?:\\s+\\w+)?\\s+Cast|Music\\s+from|(?:Musique|Bande)\\s+originale)\\b';
  1279. if (reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
  1280. if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
  1281. tags.add('score');
  1282. composerEmphasis = true;
  1283. }
  1284. remixParsers.forEach(function(rx) {
  1285. if (rx.test(album) && !releaseType) releaseType = getReleaseIndex('Remix');
  1286. });
  1287. editionParsers.forEach(function(rx) {
  1288. if (rx.test(album) && (!RegExp.$1.toLowerCase().startsWith('remaster') || !release.album_year
  1289. || release.album_year != extractYear(release.release_date)) && !isRequestNew && !isRequestEdit) {
  1290. album = album.replace(rx, '');
  1291. editionTitle = RegExp.$1;
  1292. }
  1293. });
  1294. mediaParsers.forEach(function(it) {
  1295. if (it[0].test(album)) {
  1296. album = album.replace(it[0], '');
  1297. media = it[1];
  1298. }
  1299. });
  1300. if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  1301. ref.value = album;
  1302. }
  1303.  
  1304. if (yadg_prefil) yadg_prefil += ' ';
  1305. yadg_prefil += album;
  1306. if (elementWritable(ref = document.getElementById('yadg_input'))) {
  1307. ref.value = yadg_prefil || '';
  1308. if (yadg_prefil && (ref = document.getElementById('yadg_submit')) != null && !ref.disabled) ref.click();
  1309. }
  1310.  
  1311. if (!release.album_year) release.album_year = parseInt(getHomoIdentifier('PUBYEAR')) || undefined;
  1312. if (elementWritable(ref = document.getElementById('year'))) {
  1313. ref.value = release.album_year || '';
  1314. }
  1315. i = release.release_date && extractYear(release.release_date);
  1316. if (elementWritable(ref = document.getElementById('remaster_year'))
  1317. || !isUpload && i > 0 && (ref = document.querySelector('input[name="year"]')) != null && !ref.disabled) {
  1318. ref.value = i || '';
  1319. }
  1320. //if (tracks.every(it => it.identifiers.EXPLICIT == 0)) editionTitle = 'Clean' + (editionTitle ? ' / ' + editionTitle : '');
  1321. [/\s+\(([^\(\)]+)\)\s*$/, /\s+\[([^\[\]]+)\]\s*$/, /\s+\{([^\{\}]+)\}\s*$/].forEach(function(rx) {
  1322. var version = tracks.map(track => rx.test(track.title) ? RegExp.$1 : null);
  1323. version = version.homogeneous() && version[0] || undefined;
  1324. if (!editionTitle && /\b(?:Remastered|Remasterisée|Remasterizado|Acoustic|Instrumental)\b/i.test(version)) {
  1325. editionTitle = version;
  1326. }
  1327. if (!releaseType && /\b(?:Live)\b/i.test(version)) releaseType = getReleaseIndex('Live album');
  1328. });
  1329. if (elementWritable(ref = document.getElementById('remaster_title'))) ref.value = editionTitle || '';
  1330. if (elementWritable(ref = document.getElementById('remaster_record_label')
  1331. || document.querySelector('input[name="recordlabel"]'))) {
  1332. ref.value = release.label ? prefs.selfrelease_label && !isVA && release.label == release.artist
  1333. || /^(?:independent|vlastní\s+náklad|Self[\s\-]Released)$/i.test(release.label)
  1334. || /^iMD-/.test(release.label) ? prefs.selfrelease_label : release.label.split(/\s*;\s*/g).join(' / ') : '';
  1335. }
  1336. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  1337. || document.querySelector('input[name="cataloguenumber"]'))) {
  1338. ref.value = release.catalogs.length >= 1
  1339. && release.catalogs.map(it => it.replace(/\s*;\s*/g, ' / ')).join(' / ') || barCode || '';
  1340. }
  1341. var scene = getHomoIdentifier('SCENE');
  1342. if (isUpload && scene != undefined && (ref = document.getElementById('scene')) != null && !ref.disabled) try {
  1343. ref.checked = eval(scene.toLowerCase());
  1344. } catch(e) { console.warn('Invalid SCENE value (' + scene + ')') }
  1345. var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
  1346. if (elementWritable(ref = document.getElementById('format'))) {
  1347. ref.value = release.codec || (isRED ? '' : '---');
  1348. ref.onchange(); //exec(function() { Format() });
  1349. }
  1350. if (isRequestNew) {
  1351. if (prefs.always_request_perfect_flac) reqSelectFormats('FLAC');
  1352. else if (release.codec) reqSelectFormats(release.codec);
  1353. }
  1354. var sel;
  1355. if (release.encoding == 'lossless') {
  1356. sel = tracks.some(track => track.bd == 24) ? '24bit Lossless' : 'Lossless';
  1357. } else if (release.bitrates.length >= 1) {
  1358. let lame_version = release.codec == 'MP3' && /^LAME(\d+)\.(\d+)/i.test(release.vendor) ?
  1359. parseInt(RegExp.$1) * 1000 + parseInt(RegExp.$2) : undefined;
  1360. if (release.codec == 'MP3' && release.codec_profile == 'VBR V0') {
  1361. sel = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
  1362. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V1') {
  1363. sel = 'V1 (VBR)'
  1364. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V2') {
  1365. sel = lame_version >= 3094 ? sel = 'V2 (VBR)' : 'APS (VBR)'
  1366. } else if (release.bitrates.length == 1 && [192, 256, 320].includes(Math.round(release.bitrates[0]))) {
  1367. sel = Math.round(release.bitrates[0]);
  1368. } else {
  1369. sel = 'Other';
  1370. }
  1371. }
  1372. if ((ref = document.getElementById('bitrate')) != null && !ref.disabled && (overwrite || !br_isSet)) {
  1373. ref.value = sel || '';
  1374. ref.onchange(); //exec(function() { Bitrate() });
  1375. if (sel == 'Other' && (ref = document.getElementById('other_bitrate')) != null) {
  1376. ref.value = Math.round(release.bitrates.length == 1 ? release.bitrates[0] : albumBitrate);
  1377. if ((ref = document.getElementById('vbr')) != null) ref.checked = release.bitrates.length > 1;
  1378. }
  1379. }
  1380. if (isRequestNew) {
  1381. if (prefs.always_request_perfect_flac) {
  1382. reqSelectBitrates('Lossless', '24bit Lossless');
  1383. } else if (sel) reqSelectBitrates(sel);
  1384. }
  1385. if (release.media) {
  1386. sel = undefined;
  1387. [
  1388. [/\b(?:WEB|File|Download|digital\s+media)\b|^Digital$/i, 'WEB'],
  1389. [/\bCD\b/, 'CD'],
  1390. [/\b(?:SA-?CD|[Hh]ybrid)\b/, 'SACD'],
  1391. [/\b(?:[Bb]lu[\-\−\—\–\s]?[Rr]ay|BRD?|BD)\b/, isOPS ? 'BD' : 'Blu-Ray'],
  1392. [/\bDVD(?:-?A)?\b/, 'DVD'],
  1393. [/\b(?:[Vv]inyl\b|LP\b|12"|7")/, 'Vinyl'],
  1394. ].forEach(k => { if (k[0].test(release.media)) sel = k[1] });
  1395. media = sel || media;
  1396. }
  1397. if (!media) {
  1398. if (tracks.every(isRedBook)) {
  1399. addMessage('media not determined - CD estimated', 'info');
  1400. media = 'CD';
  1401. } else if (tracks.some(t => t.bd > 16 || (t.sr > 0 && t.sr != 44100) || t.samples > 0 && t.samples % 588 != 0)) {
  1402. addMessage('media not determined - NOT CD', 'info');
  1403. }
  1404. } else if (media != 'CD' && tracks.every(isRedBook)) {
  1405. addMessage('CD as source media is estimated (' + media + ')', 'info');
  1406. }
  1407. if (elementWritable(ref = document.getElementById('media'))) {
  1408. ref.value = media || !tracks.some(notRedBook) && prefs.default_medium || (isRED ? '' : '---');
  1409. }
  1410. if (isRequestNew) {
  1411. if (prefs.always_request_perfect_flac) reqSelectMedias('WEB', 'CD', isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD')
  1412. else if (media) reqSelectMedias(media);
  1413. }
  1414. function isRedBook(track) {
  1415. return track.bd == 16 && track.sr == 44100 && track.channels == 2 && track.samples > 0 && track.samples % 588 == 0;
  1416. }
  1417. function notRedBook(track) {
  1418. return track.bd && track.bd != 16 || track.sr && track.sr != 44100
  1419. || track.channels && track.channels != 2 || track.samples && track.samples % 588 != 0;
  1420. }
  1421. if (tracks.every(it => it.identifiers.ORIGINALFORMAT && it.identifiers.ORIGINALFORMAT.includes('DSD'))) {
  1422. isFromDSD = true;
  1423. }
  1424. // Release type
  1425. if (!releaseType) {
  1426. if (/\b(?:Mixtape)\b/i.test(release.album)) releaseType = getReleaseIndex('Mixtape');
  1427. else if (isVA) releaseType = getReleaseIndex('Compilation');
  1428. else if (tracks.every(it => it.identifiers.COMPILATION == 1)) releaseType = getReleaseIndex('Anthology');
  1429. }
  1430. if ((!releaseType || releaseType == 5) && totalTime <= prefs.EP_threshold && tracks.every(function(track) {
  1431. const rxs = [/\s+\([^\(\)]+\)\s*$/, /\s+\[[^\[\]]+\]\s*$/];
  1432. return rxs.reduce((acc, rx) => acc.replace(rx, ''), track.title)
  1433. == rxs.reduce((acc, rx) => acc.replace(rx, ''), tracks[0].title);
  1434. })) {
  1435. releaseType = getReleaseIndex('Single');
  1436. }
  1437. if (!releaseType) if (totalTime > 0 && totalTime < prefs.single_threshold) {
  1438. releaseType = getReleaseIndex('Single');
  1439. } else if (totalTime > 0 && totalTime < prefs.EP_threshold) {
  1440. releaseType = getReleaseIndex('EP');
  1441. }
  1442. if ((ref = document.getElementById('releasetype')) != null && !ref.disabled
  1443. && (overwrite || ref.value == 0 || ref.value == '---')) ref.value = releaseType || getReleaseIndex('Album');
  1444. // Tags
  1445. if (prefs.estimate_decade_tag && (isNaN(totalTime) || totalTime < 2 * 60 * 60)
  1446. && release.album_year > 1900 && [1, 3, 5, 9, 13, undefined].includes(releaseType)
  1447. /*&& !/\b(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissue|Anniversary|Collector(?:'?s)?)\b/i.test(editionTitle)*/)
  1448. tags.add(Math.floor(release.album_year/10) * 10 + 's'); // experimental
  1449. if (release.country) {
  1450. if (!excludedCountries.some(it => it.test(release.country))) tags.add(release.country);
  1451. }
  1452. if (elementWritable(ref = document.getElementById('tags'))) {
  1453. ref.value = tags.toString();
  1454. if (artists[0].length == 1 && prefs.fetch_tags_from_artist > 0) setTimeout(function() {
  1455. var artist = getSiteArtist(artists[0][0]);
  1456. if (!artist) return;
  1457. tags.add(...artist.tags.sort((a, b) => b.count - a.count).map(it => it.name)
  1458. .slice(0, prefs.fetch_tags_from_artist));
  1459. var ref = document.getElementById('tags');
  1460. ref.value = tags.toString();
  1461. }, 3000);
  1462. }
  1463. if (!composerEmphasis && !prefs.keep_meaningles_composers) {
  1464. document.querySelectorAll('input[name="artists[]"]').forEach(function(i) {
  1465. if (['4', '5'].includes(i.nextElementSibling.value)) i.value = '';
  1466. });
  1467. }
  1468.  
  1469. const doubleParsParsers = [
  1470. /\(+(\([^\(\)]*\))\)+/,
  1471. /\[+(\[[^\[\]]*\])\]+/,
  1472. /\{+(\{[^\{\}]*\})\}+/,
  1473. ];
  1474. tracks.forEach(function(track) {
  1475. doubleParsParsers.forEach(function(rx) {
  1476. if (!rx.test(track.title)) return;
  1477. addMessage('doubled parentheses in track #' + track.tracknumber + ' title ("' + track.title + '")', 'warning');
  1478. //track.title.replace(rx, RegExp.$1);
  1479. });
  1480. });
  1481. if (tracks.length > 1 && tracks.map(track => track.title).homogeneous()) {
  1482. addMessage('all tracks having same title: ' + tracks[0].title, 'warning');
  1483. }
  1484. if (isUpload && !isOPS) findPreviousUploads();
  1485. // Album description
  1486. sourceUrl = getStoreUrl();
  1487. if ((ref = document.querySelector('tr#autofill_tr > td > select')) != null) {
  1488. if (i = getHomoIdentifier('DISCOGS_ID')) {
  1489. ref.value = 'discogs';
  1490. ref.onchange();
  1491. if (elementWritable(ref = document.getElementById('discogs'))) ref.value = i;
  1492. } else if (i = getHomoIdentifier('MBID')) {
  1493. ref.value = 'musicbrainz';
  1494. ref.onchange();
  1495. if (elementWritable(ref = document.getElementById('musicbrainz'))) ref.value = i;
  1496. }
  1497. }
  1498. const vinylTest = /^((?:Vinyl|LP) rip by\s+)(.*)$/im;
  1499. const vinyltrackParser = /^([A-Z])[\-\.\s]?((\d+)(?:\.\d+)?)$/;
  1500. const classicalWorkParsers = [
  1501. /^(.*\S):\s+(.*)$/,
  1502. /^(.+?):\s+([IVXC]+\.\s+.*)$/,
  1503. ];
  1504. var description;
  1505. if (isRequestNew || isRequestEdit) { // request
  1506. description = [];
  1507. if (release.release_date) {
  1508. i = new Date(release.release_date);
  1509. let today = new Date(new Date().toDateString());
  1510. description.push((isNaN(i) || i < today ? 'Released' : 'Releasing') + ' ' +
  1511. (isNaN(i) ? release.release_date : i.toDateString()));
  1512. if ((ref = document.getElementById('tags')) != null && !ref.disabled) {
  1513. let tags = new TagManager(ref.value);
  1514. if (prefs.upcoming_tags && i >= today) tags.add(prefs.upcoming_tags);
  1515. ref.value = tags.toString();
  1516. }
  1517. }
  1518. if (!prefs.include_tracklist_in_request) {
  1519. let summary = '';
  1520. if (release.totaldiscs > 1) summary += release.totaldiscs + ' discs, ';
  1521. summary += tracks.length + ' track'; if (tracks.length > 1) summary += 's';
  1522. if (totalTime > 0) summary += ', ' + makeTimeString(totalTime);
  1523. description.push(summary);
  1524. }
  1525. if (sourceUrl || release.urls.length > 0) description.push(getUrls());
  1526. if (release.catalogs.length == 1 && /^\d{10,}$/.test(release.catalogs[0]) || /^\d{10,}$/.test(barCode)) {
  1527. description.push('[url=https://www.google.com/search?q=' + RegExp.lastMatch + ']Find more stores...[/url]');
  1528. }
  1529. if (prefs.include_tracklist_in_request) description.push(genPlaylist());
  1530. if (release.descriptions.length > 0) Array.prototype.push.apply(description, release.descriptions);
  1531. description = genAlbumHeader().concat(description.join('\n\n'));
  1532. if (description.length > 0) {
  1533. ref = document.getElementById('description') || document.querySelector('textarea[name="description"]');
  1534. if (elementWritable(ref)) {
  1535. ref.value = description;
  1536. } else if (isRequestEdit && ref != null && !ref.disabled) {
  1537. ref.value = ref.value.length > 0 ? ref.value.concat('\n\n', description) : ref.value = description;
  1538. preview(0);
  1539. }
  1540. }
  1541. } else { // upload
  1542. description = '';
  1543. if (prefs.bpm_summary && albumBPM > 0) {
  1544. if (description.length <= 0) description = '\n';
  1545. description += '\nAverage album BPM: [code]' + albumBPM + '[/code]';
  1546. }
  1547. /*if (release.release_date) {
  1548. let rd = new Date(release.release_date);
  1549. if (!isNaN(rd)) description = '\n\nRelease date: ' + rd.toDateString();
  1550. }*/
  1551. let vinylRipInfo;
  1552. if (release.descriptions.length > 0) {
  1553. description += '\n\n';
  1554. if (release.descriptions.length == 1 && release.descriptions[0]
  1555. && (matches = vinylTest.exec(release.descriptions[0])) != null) {
  1556. vinylRipInfo = release.descriptions[0].slice(matches.index).trim().split(/(?:[ \t]*\r?\n)+/);
  1557. description += release.descriptions[0].slice(0, matches.index).trim();
  1558. } else description += release.descriptions.join('\n\n');
  1559. }
  1560. if (elementWritable(ref = document.getElementById('album_desc'))) {
  1561. ref.value = genPlaylist().concat(description);
  1562. preview(0);
  1563. }
  1564. if ((ref = document.getElementById('body') || document.querySelector('textarea[name="body"]')) != null
  1565. && !ref.disabled) {
  1566. if (ref.value.length == 0) ref.value = genPlaylist().concat(description); else {
  1567. let editioninfo = '';
  1568. if (editionTitle) {
  1569. editioninfo = '[size=5][b]' + editionTitle;
  1570. if (release.release_date && (i = extractYear(release.release_date)) > 0) editioninfo += ' (' + i + ')';
  1571. editioninfo += '[/b][/size]\n\n';
  1572. }
  1573. ref.value = ref.value.concat('\n\n', editioninfo, genPlaylist(false, false), description);
  1574. }
  1575. preview(0);
  1576. }
  1577. // Release description
  1578. if (elementWritable(ref = document.getElementById('release_samplerate'))) {
  1579. ref.value = Object.keys(release.srs).length == 1 ? Math.floor(Object.keys(release.srs)[0] / 1000) :
  1580. Object.keys(release.srs).length > 1 ? '999' : '';
  1581. }
  1582. let lineage = '', rlsDesc = '';
  1583. let drInfo = '[hide=DR' + (release.drs.length == 1 ? release.drs[0] : '') + '][pre][/pre]';
  1584. let hasSR = Object.keys(release.srs).length > 0;
  1585. let srInfo = hasSR ? Object.keys(release.srs).sort((a, b) => release.srs[b] - release.srs[a])
  1586. .map(f => f / 1000).join('/').concat('kHz') : null;
  1587. if (tracks.some(track => track.bd > 16)) {
  1588. if ([isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD'].includes(media)) {
  1589. if (!isNWCD) rlsDesc = srInfo;
  1590. addChannelInfo();
  1591. if (media == 'SACD' || isFromDSD) addDSDInfo();
  1592. if (prefs.cleanup_descriptions) addDRInfo();
  1593. //addRGInfo();
  1594. addHybridInfo();
  1595. drInfo += '[/hide]';
  1596. } else if (media == 'Vinyl') {
  1597. let hassr = hasSR && (!isNWCD || Object.keys(release.srs).length > 1);
  1598. if (hassr) lineage = srInfo + ' ';
  1599. if (vinylRipInfo) {
  1600. if (vinylTest.test(vinylRipInfo[0]) && RegExp.$2.toLowerCase() != 'unknown')
  1601. vinylRipInfo[0] = vinylRipInfo[0].replace(vinylTest, '$1[color=blue]$2[/color]');
  1602. if (hassr) vinylRipInfo[0] = vinylRipInfo[0].replace(/^Vinyl\b/, 'vinyl');
  1603. lineage += vinylRipInfo[0] + '\n\n[u]Lineage:[/u]' + vinylRipInfo.slice(1).map(l => '\n'.concat([
  1604. // RuTracker translation
  1605. ['Код класса состояния винила', 'Vinyl condition class'],
  1606. ['Устройство воспроизведения', 'Turntable'],
  1607. ['Головка звукоснимателя', 'Cartridge'],
  1608. ['Картридж', 'Cartridge'],
  1609. ['Предварительный усилитель', 'Preamplifier'],
  1610. ['АЦП', 'ADC'],
  1611. ['Программа-оцифровщик', 'Software'],
  1612. ['Обработка звука', 'Audio post-processing'],
  1613. ['Обработка', 'Post-processing'],
  1614. ].reduce((acc, it) => acc.replace(it[0], it[1]), l))).join('');
  1615. } else lineage += (hassr ? 'Vinyl' : ' vinyl') + ' rip by [color=blue][/color]\n\n[u]Lineage:[/u]\n';
  1616. let imgs = '\n[img][/img]'.repeat(6);
  1617. if (!isNWCD) drInfo += '\n'.concat(imgs); else lineage += '\n\n[hide]'.concat(imgs.slice(1), '[/hide]');
  1618. drInfo += '[/hide]';
  1619. } else { // WEB Hi-Res
  1620. if (!isNWCD || Object.keys(release.srs).length > 1) rlsDesc = srInfo;
  1621. if (release.channels && release.channels != 2) addChannelInfo();
  1622. if (isFromDSD) addDSDInfo();
  1623. if (!isFromDSD || prefs.cleanup_descriptions) addDRInfo();
  1624. //addRGInfo();
  1625. addHybridInfo();
  1626. if (isFromDSD || prefs.cleanup_descriptions || Object.keys(release.srs).length == 1
  1627. && Object.keys(release.srs)[0] == 88200) drInfo += '[/hide]'; else drInfo = null;
  1628. }
  1629. } else { // 16bit or lossy
  1630. if (Object.keys(release.srs).some(f => f != 44100)) rlsDesc = srInfo;
  1631. if (release.channels && release.channels != 2) addChannelInfo();
  1632. addDRInfo();
  1633. //addRGInfo();
  1634. if (prefs.cleanup_descriptions) drInfo += '[/hide]'; else drInfo = null;
  1635. if (release.codec == 'MP3' && release.vendor) {
  1636. // TODO: parse mp3 vendor string
  1637. } else if (['AAC', 'Opus', 'Vorbis'].includes(release.codec) && release.vendor) {
  1638. let _encoder_settings = release.vendor;
  1639. if (release.codec == 'AAC' && /^qaac\s+[\d\.]+/i.test(release.vendor)) {
  1640. let enc = [];
  1641. if (matches = release.vendor.match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
  1642. if (matches = release.vendor.match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
  1643. if (matches = release.vendor.match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
  1644. if (matches = release.vendor.match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
  1645. if (matches = release.vendor.match(/\bQuality\s+(\d+)\b/i)) enc[5] = matches[1];
  1646. _encoder_settings = 'Converted by Apple\'s ' + enc[2] + ' encoder (' + enc[3] + '-' + enc[4] + ')';
  1647. }
  1648. lineage = _encoder_settings;
  1649. }
  1650. }
  1651. function addDSDInfo() {
  1652. var nfo = ' DSD64';
  1653. if (prefs.sacd_decoder) nfo += ' using ' + prefs.sacd_decoder;
  1654. nfo += '\nOutput gain: [code]+0dB[/code]';
  1655. if (isNWCD) lineage = 'From' .concat(nfo); else {
  1656. if (rlsDesc.length > 0) rlsDesc += ' from'; else rlsDesc = 'From';
  1657. rlsDesc += nfo;
  1658. }
  1659. }
  1660. function addDRInfo() {
  1661. if (release.drs.length < 1 || document.getElementById('release_dynamicrange') != null) return;
  1662. var nfo = 'DR' + release.drs[0];
  1663. if (release.drs[0] < 4) nfo = '[color=red]'.concat(nfo, '[/color]');
  1664. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1665. rlsDesc += nfo;
  1666. }
  1667. function addRGInfo() {
  1668. if (release.ags.length <= 0) return;
  1669. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1670. rlsDesc += 'RG'; //rlsDesc += 'RG ' + ags[0];
  1671. }
  1672. function addChannelInfo() {
  1673. if (!release.channels) return;
  1674. var chi = getChanString(release.channels);
  1675. if (chi.length <= 0) return;
  1676. if (rlsDesc.length > 0) rlsDesc += ', '; else rlsDesc = 'Channels configuration: ';
  1677. rlsDesc += chi;
  1678. }
  1679. function addHybridInfo() {
  1680. if (release.bds.length > 1) release.bds.filter(bd => bd != 24).forEach(function(bd) {
  1681. var hybrid_tracks = tracks.filter(it => it.bd == bd).sort(trackComparer).map(function(it) {
  1682. return (release.totaldiscs > 1 && it.discnumber ? it.discnumber + '-' : '').concat(it.tracknumber);
  1683. });
  1684. if (hybrid_tracks.length < 1) return;
  1685. if (rlsDesc.length > 0) rlsDesc += '\n';
  1686. rlsDesc += 'Note: track';
  1687. if (hybrid_tracks.length > 1) rlsDesc += 's';
  1688. rlsDesc += ' #' + hybrid_tracks.join(', ') +
  1689. (hybrid_tracks.length > 1 ? ' are' : ' is') + ' ' + bd + 'bit lossless';
  1690. });
  1691. }
  1692. rlsDesc = rlsDesc.length > 0 ? [rlsDesc] : [];
  1693. if ((ref = document.getElementById('release_lineage')) != null) {
  1694. lineage = lineage ? [lineage] : [];
  1695. if (drInfo) rlsDesc.push(drInfo);
  1696. if (sourceUrl || release.urls.length > 0) lineage.push(getUrls());
  1697. if (elementWritable(ref)) {
  1698. ref.value = lineage.join('\n\n');
  1699. preview(1);
  1700. }
  1701. } else {
  1702. if (lineage.length > 0) rlsDesc.push(lineage);
  1703. if (drInfo) rlsDesc.push(drInfo);
  1704. if (sourceUrl || release.urls.length > 0) rlsDesc.push(getUrls());
  1705. }
  1706. if (elementWritable(ref = document.getElementById('release_desc'))) {
  1707. ref.value = rlsDesc.join('\n\n');
  1708. if (rlsDesc.length > 0) preview(isNWCD ? 2 : 1);
  1709. }
  1710. if (release.encoding == 'lossless' && release.codec == 'FLAC'
  1711. && tracks.some(track => track.bd == 24) && release.dirpaths.length == 1) {
  1712. if ((ref = document.getElementById('release_desc')) != null) GM_xmlhttpRequest({
  1713. method: 'GET',
  1714. url: new URL('file:'.concat(release.dirpaths[0], '\\foo_dr.txt')).href,
  1715. responseType: 'blob',
  1716. onload: function(response) {
  1717. if (response.status < 200 || response.status >= 400) return defaultErrorHandler(response);
  1718. if (!/(\[hide=DR\d*\]\[pre\])\[\/pre\]/im.test(ref.value)) return;
  1719. var ndx = RegExp.lastIndex + RegExp.$1.length;
  1720. ref.value = ref.value.slice(0, ndx).concat(response.responseText, ref.value.slice(ndx));
  1721. },
  1722. onerror: error => { console.error('foo_dr.txt not exists or is forbidden to read') },
  1723. ontimeout: defaultTimeoutHandler,
  1724. });
  1725. }
  1726. }
  1727. if (ajaxRejects > 0) {
  1728. i = 'AJAX request(s) eliminated due to Gazelle policy. ' +
  1729. 'Multiple artists not split correctly? Relaunch parsing in overwrite mode without page reload'
  1730. let delay = gazelleApiTimeFrame.timeStamp + 10100 - new Date().getTime();
  1731. if (delay >= 0) {
  1732. i += ' after ' + Math.ceil(delay / 1000) + 's';
  1733. setTimeout(() => { addMessage('new AJAX timeframe for requery available', 'info') }, delay);
  1734. }
  1735. addMessage(i + '.', 'notice');
  1736. }
  1737. if (elementWritable(ref = document.getElementById('release_dynamicrange'))) {
  1738. ref.value = release.drs.length == 1 ? release.drs[0] : '';
  1739. }
  1740. if (isRequestNew && prefs.request_default_bounty > 0) {
  1741. let amount = prefs.request_default_bounty < 1024 ? prefs.request_default_bounty : prefs.request_default_bounty / 1024;
  1742. if ((ref = document.getElementById('amount_box')) != null && !ref.disabled) ref.value = amount;
  1743. if ((ref = document.getElementById('unit')) != null && !ref.disabled) {
  1744. ref.value = prefs.request_default_bounty < 1024 ? 'mb' : 'gb';
  1745. }
  1746. try { Calculate() } catch(e) { /* Orpheus bug void handler */ }
  1747. }
  1748. if (!media && (ref = document.getElementById('media')) != null && ref.value && ref.value != '---') media = ref.value;
  1749. if (!onlineSource) {
  1750. onlineSource = (sourceUrl || release.urls.length > 0 ?
  1751. fetchOnline_Music(sourceUrl || release.urls[0], true).then(completeFromOnlineSource) : Promise.reject('No URL'));
  1752. if (prefs.check_integrity_online) onlineSource.catch(reason => lookupOnlineSource().then(function(result) {
  1753. if (typeof result == 'object') return parseLastFm(result);
  1754. if (urlParser.test(result)) return fetchOnline_Music(result, true);
  1755. return Promise.reject('Unhandled format');
  1756. })).then(onlineCheck).catch(function(reason) {
  1757. if (!media || media == 'WEB') tracks.forEach(function(track) {
  1758. if (!track.duration || track.duration < 29.6 || track.duration > 30.4) return;
  1759. addMessage('track ' + track.tracknumber + ' possible track preview', 'warning');
  1760. });
  1761. });
  1762. }
  1763. if (prefs.clean_on_apply) clipBoard.value = '';
  1764. prefs.save();
  1765. return true;
  1766.  
  1767. // ---------------------------------------------------------------------------------------------------------------
  1768.  
  1769. function genPlaylist(pad = true, header = true) {
  1770. var style = prefs.tracklist_style;
  1771. if (style == 2 && (tracks.map(track => track.title).some(notMonospaced))
  1772. || tracks.map(track => track.track_artist).some(notMonospaced)
  1773. || composerEmphasis && tracks.map(track => track.composer).some(notMonospaced)) style = 3;
  1774. if (!style || style <= 0) return null;
  1775. var playlist = '';
  1776. if (tracks.length > 1 || isRequestNew || isRequestEdit) {
  1777. if (style == 3) playlist = '[align=center]';
  1778. if (pad && isRED) playlist += '[pad=5|0|0|0]';
  1779. if (header) playlist += genAlbumHeader();
  1780. playlist += '[size=4][b][color=' + prefs.tracklist_head_color + ']Tracklisting[/color][/b][/size]';
  1781. if (pad && isRED) playlist += '[/pad]';
  1782. playlist += '\n'; //'[hr]';
  1783. let lastDisc, lastSubtitle, lastWork, lastSide, vinylTrackWidth;
  1784. let block = 0, classicalWorks = new Map();
  1785. if (composerEmphasis /*isClassical*/ && !tracks.some(it => it.discsubtitle)) {
  1786. tracks.forEach(function(track) {
  1787. if (!track.composer) return;
  1788. (/*isClassical ? classicalWorkParsers : */classicalWorkParsers.slice(1)).forEach(function(classicalWorkParser) {
  1789. if (track.classical_work || !classicalWorkParser.test(track.title)) return;
  1790. classicalWorks.set(track.classical_work = RegExp.$1, {});
  1791. track.classical_title = RegExp.$2;
  1792. });
  1793. });
  1794. for (iter of classicalWorks.keys()) {
  1795. let work = tracks.filter(track => track.classical_work == iter);
  1796. if (work.length > 1 || tracks.every(track => track.classical_work)) {
  1797. if (work.map(it => it.track_artist).homogeneous()) classicalWorks.get(iter).performer = work[0].track_artist;
  1798. if (work.map(it => it.composer).homogeneous()) classicalWorks.get(iter).composer = work[0].composer;
  1799. } else {
  1800. work.forEach(function(track) {
  1801. delete track.classical_work;
  1802. delete track.classical_title;
  1803. });
  1804. classicalWorks.delete(iter);
  1805. }
  1806. }
  1807. }
  1808. let track, duration, volumes = new Map(tracks.map(it => [it.discnumber, undefined])), tnOffset = 0;
  1809. volumes.forEach(function(val, key) {
  1810. volumes.set(key, new Set(tracks.filter(it => it.discnumber == key).map(it => it.discsubtitle)).size)
  1811. });
  1812. if (!tracks.every(it => !isNaN(parseInt(it.tracknumber.toString())))
  1813. && !tracks.every(it => vinyltrackParser.test(it.tracknumber.toString().toUpperCase()))) {
  1814. addMessage('inconsistent tracks numbering (' + tracks.map(it => it.tracknumber) + ')', 'warning');
  1815. }
  1816. vinylTrackWidth = tracks.reduce(function(acc, it) {
  1817. return Math.max(vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) && parseInt(RegExp.$3), acc);
  1818. }, 0);
  1819. if (vinylTrackWidth) {
  1820. vinylTrackWidth = vinylTrackWidth.toString().length;
  1821. tracks.forEach(function(it) {
  1822. if (vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) != null)
  1823. it.tracknumber = RegExp.$1 + RegExp.$3.padStart(vinylTrackWidth, '0');
  1824. });
  1825. ++vinylTrackWidth;
  1826. }
  1827. if (release.totaldiscs < 2 && tracks.reduce(computeLowestTrack, undefined) - 1)
  1828. addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
  1829. const padStart = '[pad=0|0|5|0]';
  1830. if (canSort && prefs.sort_tracklist) tracks.sort(trackComparer);
  1831. for (iter of tracks) {
  1832. let title = '', trackArtist;
  1833. if (iter.track_artist && iter.track_artist != release.artist) {
  1834. let trackArtists = getArtists(iter.track_artist);
  1835. if (!trackArtists[0].equalCaselessTo(artists[0]) || !trackArtists[1].equalCaselessTo(albumGuests))
  1836. if (prefs.reformat_trackartist) {
  1837. trackArtist = joinArtists(trackArtists[0]);
  1838. if (trackArtists[1].length > 0) trackArtist += ' feat. '.concat(joinArtists(trackArtists[1]));
  1839. } else trackArtist = iter.track_artist;
  1840. }
  1841. var ttwidth = vinylTrackWidth || (release.totaldiscs > 1 && iter.discnumber ?
  1842. tracks.filter(it => it.discnumber == iter.discnumber) : tracks).reduce(function (accumulator, it) {
  1843. return Math.max(accumulator, (parseInt(it.tracknumber) || it.tracknumber).toString().length);
  1844. }, 2);
  1845. function realTrackNumber() {
  1846. var tn = parseInt(iter.tracknumber);
  1847. return isNaN(tn) ? iter.tracknumber : (tn - tnOffset).toString().padStart(ttwidth, '0');
  1848. }
  1849. switch (style) {
  1850. case 1:
  1851. case 3: {
  1852. prologue('[size=' + prefs.tracklist_size + ']', '[/size]\n');
  1853. track = '[b][color=' + prefs.tracklist_tracknumber_color + ']';
  1854. track += realTrackNumber();
  1855. track += '[/color][/b]' + prefs.title_separator;
  1856. if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
  1857. title = '[color=' + prefs.tracklist_artist_color + ']' + trackArtist + '[/color] - ';
  1858. }
  1859. title += iter.classical_title || iter.title;
  1860. if (iter.composer && composerEmphasis && release.composers.length != 1
  1861. && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
  1862. title = title.concat(' [color=', prefs.tracklist_composer_color, '](', iter.composer, ')[/color]');
  1863. }
  1864. playlist += track + title;
  1865. if (iter.duration) playlist += ' [i][color=' + prefs.tracklist_duration_color +'][' +
  1866. makeTimeString(iter.duration) + '][/color][/i]';
  1867. break;
  1868. }
  1869. case 2: {
  1870. prologue('[size=' + prefs.tracklist_size + '][pre]', '[/pre][/size]');
  1871. track = realTrackNumber();
  1872. track += prefs.title_separator;
  1873. if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
  1874. title = trackArtist + ' - ';
  1875. }
  1876. title += iter.classical_title || iter.title;
  1877. if (composerEmphasis && iter.composer && release.composers.length != 1
  1878. && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
  1879. title = title.concat(' (', iter.composer, ')');
  1880. }
  1881. let l = 0, j, left, padding, spc;
  1882. duration = iter.duration ? '[' + makeTimeString(iter.duration) + ']' : null;
  1883. let width = prefs.max_tracklist_width - track.length;
  1884. if (duration) width -= duration.length + 1;
  1885. while (title.trueLength() > 0) {
  1886. j = width;
  1887. if (title.trueLength() > width) {
  1888. while (j > 0 && title[j] != ' ') { --j }
  1889. if (j <= 0) j = width;
  1890. }
  1891. left = title.slice(0, j).trim();
  1892. if (++l <= 1) {
  1893. playlist += track + left;
  1894. if (duration) {
  1895. spc = width - left.trueLength();
  1896. padding = (spc < 2 ? ' '.repeat(spc) : ' ' + prefs.pad_leader.repeat(spc - 1)) + ' ';
  1897. playlist += padding + duration;
  1898. }
  1899. width = prefs.max_tracklist_width - track.length - 2;
  1900. } else playlist += '\n' + ' '.repeat(track.length) + left;
  1901. title = title.slice(j).trim();
  1902. }
  1903. break;
  1904. }
  1905. }
  1906. }
  1907. switch (style) {
  1908. case 1:
  1909. case 3:
  1910. if (totalTime > 0) playlist += '\n\n' + divs[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
  1911. ']Total time: [i]' + makeTimeString(totalTime) + '[/i][/color][/size]';
  1912. break;
  1913. case 2:
  1914. if (totalTime > 0) {
  1915. duration = '[' + makeTimeString(totalTime) + ']';
  1916. playlist += '\n\n' + divs[0].repeat(32).padStart(prefs.max_tracklist_width);
  1917. playlist += '\n' + 'Total time:'.padEnd(prefs.max_tracklist_width - duration.length) + duration;
  1918. }
  1919. playlist += '[/pre][/size]';
  1920. break;
  1921. }
  1922. if (style == 3) playlist += '[/align]';
  1923.  
  1924. function computeLowestTrack(acc, track) {
  1925. if (Number.isNaN(acc)) return NaN;
  1926. var tn = parseInt(track.tracknumber);
  1927. if (isNaN(tn)) return NaN;
  1928. return isNaN(acc) || tn < acc ? tn : acc;
  1929. }
  1930.  
  1931. function prologue(prefix, postfix) {
  1932. function block1() {
  1933. if (block == 3) playlist += postfix;
  1934. playlist += '\n';
  1935. if (isRED && ![1, 2].includes(block)) playlist += padStart;
  1936. block = 1;
  1937. }
  1938. function block2() {
  1939. if (block == 3) playlist += postfix;
  1940. playlist += '\n';
  1941. if (isRED && ![1, 2].includes(block)) playlist += padStart;
  1942. block = 2;
  1943. }
  1944. function block3() {
  1945. //if (block == 2 && isRED) playlist += '[hr]';
  1946. if (isRED && [1, 2].includes(block)) playlist += '[/pad]';
  1947. playlist += '\n';
  1948. if (block != 3) playlist += prefix;
  1949. block = 3;
  1950. }
  1951. if (release.totaldiscs > 1 && iter.discnumber != lastDisc) {
  1952. block1();
  1953. lastDisc = iter.discnumber;
  1954. lastSubtitle = lastWork = undefined;
  1955. playlist += '[color=' + prefs.tracklist_disctitle_color + '][size=3][b]';
  1956. if (iter.identifiers.VOL_MEDIA && tracks.filter(it => it.discnumber == iter.discnumber)
  1957. .every(it => it.identifiers.VOL_MEDIA == iter.identifiers.VOL_MEDIA)) {
  1958. playlist += iter.identifiers.VOL_MEDIA.toUpperCase() + ' ';
  1959. }
  1960. playlist += 'Disc ' + iter.discnumber;
  1961. if (iter.discsubtitle && (volumes.get(iter.discnumber) || 0) == 1) {
  1962. playlist += ' – ' + iter.discsubtitle;
  1963. lastSubtitle = iter.discsubtitle;
  1964. }
  1965. playlist += '[/b][/size]';
  1966. duration = tracks.filter(it => it.discnumber == iter.discnumber).reduce((acc, it) => acc + it.duration, 0);
  1967. if (duration > 0) playlist += ' [size=2][i][' + makeTimeString(duration) + '][/i][/size]';
  1968. playlist += '[/color]';
  1969. tnOffset = tracks.filter(track => track.discnumber == iter.discnumber).reduce(computeLowestTrack, undefined) - 1 || 0;
  1970. if (tnOffset) addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
  1971. }
  1972. if (iter.discsubtitle != lastSubtitle) {
  1973. if (block != 1 || iter.discsubtitle) block1();
  1974. if (iter.discsubtitle) {
  1975. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]' + iter.discsubtitle + '[/b][/size]';
  1976. duration = tracks.filter(it => it.discsubtitle == iter.discsubtitle)
  1977. .reduce((acc, it) => acc + it.duration, 0);
  1978. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  1979. playlist += '[/color]';
  1980. }
  1981. lastSubtitle = iter.discsubtitle;
  1982. }
  1983. if (iter.classical_work != lastWork) {
  1984. if (iter.classical_work) {
  1985. block2();
  1986. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
  1987. if (release.composers.length != 1 && classicalWorks.get(iter.classical_work).composer) {
  1988. playlist += classicalWorks.get(iter.classical_work).composer + ': ';
  1989. }
  1990. playlist += iter.classical_work;
  1991. playlist += '[/b]';
  1992. if (classicalWorks.get(iter.classical_work).performer
  1993. && classicalWorks.get(iter.classical_work).performer != release.artist) {
  1994. playlist += ' (' + classicalWorks.get(iter.classical_work).performer + ')';
  1995. }
  1996. playlist += '[/size]';
  1997. duration = tracks.filter(it => it.classical_work == iter.classical_work)
  1998. .reduce((acc, it) => acc + it.duration, 0);
  1999. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  2000. playlist += '[/color]';
  2001. } else {
  2002. if (block > 2) block1();
  2003. }
  2004. lastWork = iter.classical_work;
  2005. }
  2006. if (vinyltrackParser.test(iter.tracknumber)) {
  2007. if (block == 3 && lastSide && RegExp.$1 != lastSide) playlist += '\n';
  2008. lastSide = RegExp.$1;
  2009. }
  2010. block3();
  2011. } // prologue
  2012. } else { // single
  2013. playlist += '[align=center]';
  2014. playlist += isRED ? '[pad=20|20|20|20]' : '';
  2015. playlist += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + release.artist + '[/color]';
  2016. playlist += isRED ? '[hr]' : '\n'.concat(divs[0].repeat(24), '\n');
  2017. playlist += tracks[0].title + '[/b]';
  2018. if (tracks[0].composer) {
  2019. playlist += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
  2020. }
  2021. playlist += '\n\n[color=' + prefs.tracklist_duration_color +'][' + makeTimeString(tracks[0].duration) + '][/color][/size]';
  2022. if (isRED) playlist += '[/pad]';
  2023. playlist += '[/align]';
  2024. }
  2025. return playlist;
  2026. }
  2027.  
  2028. function getUrls() {
  2029. var result = [];
  2030. if (sourceUrl) result.push(sourceUrl);
  2031. Array.prototype.push.apply(result, release.urls.filter(function(url) {
  2032. return !sourceUrl || url.toLowerCase() != sourceUrl.toLowerCase();
  2033. }));
  2034. return result.map(url => urlParser.test(url) ? '[url]' + url + '[/url]' : url).join('\n');
  2035. }
  2036.  
  2037. function genAlbumHeader() {
  2038. return !isVA && artists[0].length >= 3 ? '[size=4]' +
  2039. joinArtists(artists[0], artist => '[artist]' + artist + '[/artist]') + ' – ' + release.album + '[/size]\n\n' : '';
  2040. }
  2041.  
  2042. function findPreviousUploads() {
  2043. let search = new URLSearchParams(document.location.search);
  2044. if (search.get('groupid')) localFetch('/torrents.php?action=grouplog&groupid=' + search.get('groupid')).then(function(dom) {
  2045. dom.querySelectorAll('table > tbody > tr.rowa').forEach(function(tr) {
  2046. if (/^\s*deleted\b/i.test(tr.children[3].textContent))
  2047. scanLog('Torrent ' + tr.children[1].firstChild.textContent);
  2048. });
  2049. }); else {
  2050. let query = '';
  2051. if (!isVA && artists[0].length >= 1 && artists[0].length <= 3) query = artists[0].join(', ') + ' - ';
  2052. query += release.album;
  2053. scanLog(query);
  2054. }
  2055.  
  2056. function scanLog(query) {
  2057. localFetch('/log.php?search=' + encodeURIComponent(query)).then(function(dom) {
  2058. dom.querySelectorAll('table > tbody > tr.rowb').forEach(function(tr) {
  2059. var size, msg = tr.children[1].textContent.trim();
  2060. if (/\b[\d\s]+(?:\.\d+)?\s*(?:([KMGT])I?)?B\b/.test(msg)) size = get_size_from_string(RegExp.lastMatch);
  2061. if (!msg.includes('deleted') || (/\[(.*)\/(.*)\/(.*)\]/.test(msg) ?
  2062. !release.codec || release.codec != RegExp.$1
  2063. //|| !release.encoding || release.encoding != RegExp.$2
  2064. || !media || media != RegExp.$3 :
  2065. !size || !albumSize || Math.abs(albumSize / size - 1) >= 0.1)) return;
  2066. addMessage('possibly same release previously uploaded and deleted: ' + msg, 'warning');
  2067. });
  2068. });
  2069. }
  2070.  
  2071. function get_size_from_string(str) {
  2072. var matches = /\b([\d\s]+(?:\.\d+)?)\s*(?:([KMGT])I?)?B\b/.exec(str.replace(',', '.').toUpperCase());
  2073. if (!matches) return null;
  2074. var size = parseFloat(matches[1].replace(/\s+/g, ''));
  2075. if (matches[2] == 'K') { size *= Math.pow(1024, 1) }
  2076. else if (matches[2] == 'M') { size *= Math.pow(1024, 2) }
  2077. else if (matches[2] == 'G') { size *= Math.pow(1024, 3) }
  2078. else if (matches[2] == 'T') { size *= Math.pow(1024, 4) }
  2079. return Math.round(size);
  2080. }
  2081. }
  2082.  
  2083. function getHomoIdentifier(id) {
  2084. id = id.toUpperCase();
  2085. return tracks.every((elem, ndx, arr) => elem.identifiers[id] != undefined
  2086. && elem.identifiers[id] === arr[0].identifiers[id]) ? tracks[0].identifiers[id] : undefined;
  2087. }
  2088.  
  2089. function getStoreUrl() {
  2090. for (var it of [
  2091. ['ACOUSTICSOUNDS_ID', 'https://store.acousticsounds.com/d/{ID}/'],
  2092. ['AMAZON_ID', 'https://www.amazon.com/gp/product/{ID}'],
  2093. ['APPLE_ID', 'https://music.apple.com/album/{ID}'],
  2094. ['ASIN', 'https://www.amazon.com/gp/product/{ID}'],
  2095. ['BEATPORT_ID', 'https://www.beatport.com/release/2/{ID}'],
  2096. ['DEEZER_ID', deezerAlbumPrefix + '{ID}'],
  2097. ['DISCOGS_ID', discogsOrigin + '/release/{ID}'],
  2098. ['EONKYO_ID', 'https://www.e-onkyo.com/music/album/{ID}/'],
  2099. ['GOOGLE_ID', 'https://play.google.com/store/music/album/?id={ID}'],
  2100. ['INDIESSCOPE_ID', 'https://www.indies.eu/alba/{ID}/'],
  2101. ['ITUNES_ID', 'https://music.apple.com/album/{ID}'],
  2102. ['JUNODOWNLOAD_ID', 'https://www.junodownload.com/products/{ID}'],
  2103. ['MBID', mbrRlsPrefix + '{ID}'],
  2104. ['PROSTUDIOMASTERS_ID', 'https://www.prostudiomasters.com/album/page/{ID}'],
  2105. ['SPOTIFY_ID', 'https://open.spotify.com/album/{ID}'],
  2106. ['TRAXSOURCE_ID', 'https://www.traxsource.com/title/{ID}/'],
  2107. ['VGMDB_ID', 'https://vgmdb.net/album/{ID}'],
  2108. ['TIDAL_ID', 'https://listen.tidal.com/album/{ID}'],
  2109. ]) {
  2110. let ID = getHomoIdentifier(it[0]);
  2111. if (ID) return it[1].replace('{ID}', ID);
  2112. }
  2113. return undefined;
  2114. }
  2115.  
  2116. function getCoverOnline() {
  2117. var url = sourceUrl || release.urls[0], apiFirst;
  2118. if (i = getHomoIdentifier('APPLE_ID') || getHomoIdentifier('ITUNES_ID')
  2119. || /^https?:\/\/(?:\w+\.)*apple\.com\/.*\/(\d+)(?=$|\?)/i.test(url) && (i = parseInt(RegExp.$1))) {
  2120. apiFirst = queryItunesAPI('lookup', { id: i })
  2121. .then(result => result.resultCount > 0 ? setItunesImage(result.results[0]) : Promise.reject('no cover'));
  2122. } else if (i = getHomoIdentifier('DEEZER_ID')
  2123. || /^https:\/\/(?:\w+\.)*deezer\.com\/(\w+\/)*album\/(\d+)$/i.test(url) && (i = parseInt(RegExp.$1))) {
  2124. apiFirst = queryDeezerAPI('album/' + i)
  2125. .then(result => result.id ? setDeezerImage(result) : Promise.reject('No cover'));
  2126. } else if ((prefs.discogs_key && prefs.discogs_secret || discogs_token)
  2127. && (i = getHomoIdentifier('DISCOGS_ID') || dcRlsParser.test(url) && RegExp.$1)) {
  2128. apiFirst = queryDiscogsAPI('releases/' + i).then(function(release) {
  2129. return release.images.length > 0 ? setCover(release.images[0].uri) : Promise.reject('No cover');
  2130. });
  2131. } else if ((i = getHomoIdentifier('MBID') || mbrRlsParser.test(url) && RegExp.$1)) {
  2132. apiFirst = getMusicBrainzCovers(i).then(function(covers) {
  2133. return covers != null ? setCover(covers[1][0]) : Promise.reject('No cover');
  2134. });
  2135. } else if (i = getHomoIdentifier('TIDAL_ID') || /^https:\/\/(?:\w+\.)*tidal\.com\//.test(url)
  2136. && (/\/album\/(\d+)(?:\/|$)/i.test(url) || /\/album(?:\/|\?).*\b(?:albumId)=(\d+)\b/i.test(url))
  2137. && (i = parseInt(RegExp.$1)) > 0) {
  2138. apiFirst = queryTidalAPI('album', { albumId: i }).then(function(album) {
  2139. for (var row of album.rows) {
  2140. var albumHeader = row.modules.find(module => module.type == 'ALBUM_HEADER');
  2141. if (albumHeader != undefined && albumHeader.album.cover) return 'https://resources.tidal.com/images/'
  2142. .concat(albumHeader.album.cover.replace(/-/g, '/'), '/1280x1280.jpg');
  2143. }
  2144. return Promise.reject('Image not found');
  2145. });
  2146. } else apiFirst = Promise.reject('No known API binding');
  2147. return apiFirst.catch(function(reason) {
  2148. if (!urlParser.test(url)) return Promise.reject('No valid URL to parse');
  2149. if (dcRlsParser.test(url)) url = discogsOrigin + '/release/' + RegExp.$1 + '/images';
  2150. return globalFetch(url).then(function(response) {
  2151. var ref, hostname = new URL(response.finalUrl).hostname;
  2152. function testDomain(url, selector) {
  2153. return hostname.includes(url.toLowerCase()) ? response.document.querySelector(selector) : null;
  2154. }
  2155. if ((ref = testDomain('qobuz.com', 'div.album-cover > img')) != null)
  2156. return setCover(ref.src.replace(/_\d{3}(?=\.\w+$)/, '_max')).catch(reason => setCover(ref.src));
  2157. if ((ref = testDomain('highresaudio.com', 'div.albumbody > img.cover[data-pin-media]')) != null) {
  2158. ref = ref.dataset.pinMedia;
  2159. } else if ((ref = testDomain('bandcamp.com', 'div#tralbumArt > a.popupImage')) != null) {
  2160. ref = ref.href;
  2161. } else if ((ref = testDomain('7digital.com', 'span.release-packshot-image > img[itemprop="image"]')) != null) {
  2162. ref = ref.src;
  2163. } else if ((ref = testDomain('hdtracks.', 'p.product-image > img')) != null) {
  2164. ref = ref.src;
  2165. } else if ((ref = testDomain('discogs.com', 'div#view_images > p:first-of-type > span > img')) != null) {
  2166. ref = ref.src;
  2167. } else if ((ref = testDomain('prestomusic.com', 'div.c-product-block__aside > a')) != null) {
  2168. ref = ref.href.replace(/\?\d+$/, '');
  2169. } else if ((ref = testDomain('bontonland.cz', 'a.detailzoom')) != null) {
  2170. ref = ref.href;
  2171. } else if ((ref = testDomain('nativedsd.com', 'a#album-cover')) != null) {
  2172. ref = ref.href;
  2173. } else if ((ref = testDomain('prostudiomasters.com', 'img.album-art')) != null) {
  2174. ref = ref.currentSrc || ref.src;
  2175. } else if ((ref = testDomain('e-onkyo.com', 'figure > a.colorbox')) != null) {
  2176. ref = new URL(response.finalUrl).origin + ref.pathname;
  2177. } else if ((ref = testDomain('store.acousticsounds.com', 'div#detail > link[rel="image_src"]')) != null) {
  2178. ref = ref.href.replace(/\/medium\//i, '/large/');
  2179. } else if ((ref = testDomain('indies.eu', 'div.obrazekDetail > img')) != null) {
  2180. ref = ref.src;
  2181. } else if ((ref = testDomain('beatport.com', 'div > img.interior-release-chart-artwork')) != null) {
  2182. ref = ref.src;
  2183. } else if ((ref = testDomain('supraphonline.cz', 'meta[itemprop="image"]')) != null) {
  2184. ref = ref.content.replace(/\?.*$/, '');
  2185. } else if ((ref = testDomain('vgmdb.net', 'div#coverart')) != null
  2186. && /\burl\s*\(\"(.*)"\)/i.test(ref.style['background-image'])) {
  2187. ref = RegExp.$1;
  2188. } else if ((ref = response.document.querySelector('meta[property="og:image"]')
  2189. || response.document.querySelector('meta[itemprop="image"]')) != null && ref.content) {
  2190. ref = ref.content;
  2191. }
  2192. return urlParser.test(ref) ? setCover(ref) : Promise.reject('No URL to parse');
  2193. });
  2194. });
  2195. }
  2196.  
  2197. function searchCoverOnline() {
  2198. switch (typeof prefs.cover_lookup_provider == 'string' && prefs.cover_lookup_provider.toLowerCase()) {
  2199. case 'itunes': return searchCoverOnline_iTunes();
  2200. case 'deezer': return searchCoverOnline_Deezer();
  2201. case 'google': return searchCoverOnline_GooglePlay();
  2202. case 'musicbrainz': return searchCoverOnline_MBR();
  2203. case 'lastfm': return searchCoverOnline_LastFM();
  2204. case 'qobuz': return searchCoverOnline_Qobuz();
  2205. case 'all': return searchCoverOnline_iTunes()
  2206. .catch(searchCoverOnline_LastFM)
  2207. .catch(searchCoverOnline_Deezer)
  2208. .catch(searchCoverOnline_MBR)
  2209. .catch(searchCoverOnline_Qobuz)
  2210. .catch(searchCoverOnline_GooglePlay);
  2211. }
  2212. return Promise.reject('No valid service selected');
  2213.  
  2214. function searchCoverOnline_iTunes() {
  2215. return amLookup().then(function(album) {
  2216. return setItunesImage(album)
  2217. .then(imgUrl => { info('Apple Music', album.collectionViewUrl, album.collectionId) });
  2218. });
  2219. }
  2220. function searchCoverOnline_Deezer() {
  2221. return deezerLookup().then(function(album) {
  2222. return setDeezerImage(album)
  2223. .then(imgUrl => { info('Deezer', deezerAlbumPrefix + album.id, album.id) });
  2224. });
  2225. }
  2226. function searchCoverOnline_GooglePlay() {
  2227. return globalFetch('https://play.google.com/store/search?' + new URLSearchParams({
  2228. q: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2229. c: 'music',
  2230. }).toString()).then(function(response) {
  2231. var results = response.document.querySelectorAll('div:first-of-type + div[jscontroller]:last-of-type');
  2232. if (results.length > 0) for (var ndx = 0; ndx < results.length; ++ndx) {
  2233. let items = [];
  2234. results[ndx].querySelectorAll(':scope > div').forEach(function(result) {
  2235. var img = result.querySelector('span > span > img');
  2236. img = img != null ? (img.src || img.dataset.src).replace(/=[a-z]\d+$/, '=w0') : null;
  2237. var album = result.querySelector('a > div[title]');
  2238. if (album == null) return;
  2239. var artist = album.parentNode.parentNode.parentNode.querySelector('a > div:not([title])')
  2240. artist = artist != null ? artist.textContent.trim() : null;
  2241. var url = album.parentNode.href;
  2242. var id = /\?id=(\w+)\b/i.test(album.parentNode.href) && RegExp.$1 || null;
  2243. album = album.textContent.trim();
  2244. items.push({ id: id, url: url, artist: artist, album: album, imgUrl: img });
  2245. });
  2246. for (i = 0; i < 3; ++i) {
  2247. var f = items.filter(release => releasesMatch(release.artist, release.album, i));
  2248. if (f.length > 1) return Promise.reject('Google Play Music: ambiguity');
  2249. if (f.length == 1) break;
  2250. }
  2251. if (i >= 3) return Promise.reject('Google Play Music: no matches');
  2252. if (i == 2) console.debug('Google Play Music fuzzy match:', release, '==', f[0]);
  2253. if (f[0].imgUrl) return setCover(f[0].imgUrl)
  2254. .then(release => { info('Google Play Music', f[0].url, f[0].id) });
  2255. }
  2256. });
  2257. return Promise.reject('Google Play Music: no matches');
  2258. }
  2259. function searchCoverOnline_MBR() {
  2260. return mbLookupByBarcode().catch(mbLookupByASIN).catch(mbLookupByTOC)
  2261. .catch(reason => mbLookup().then(release => [release]))
  2262. .catch(mbLookupByAutoTOC)
  2263. .then(releases => Promise.all(releases.map(release => getMusicBrainzCovers(release.id))))
  2264. .then(function(releases) {
  2265. for (var rls of releases) if (rls != null) return _setCover(rls);
  2266. return Promise.reject('MusicBrainz: no covers found');
  2267. });
  2268.  
  2269. function _setCover(rls) {
  2270. return setCover(rls[1][0]).then(function(imgUrl) {
  2271. if (/\/release\/(\S+)$/i.test(rls[0])) info('Musicbrains', rls[0], RegExp.$1);
  2272. return imgUrl;
  2273. });
  2274. }
  2275. }
  2276. function searchCoverOnline_LastFM() {
  2277. return queryLastFmAPI('album.getinfo', {
  2278. artist: (isVA ? VA : release.artist),
  2279. album: release.album,
  2280. }).then(function(result) {
  2281. if (result.error) return Promise.reject('Last.fm: '.concat(result.message));
  2282. var image = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
  2283. return acc || result.album.image.find(image => image.size == size && urlParser.test(image['#text']));
  2284. }, undefined);
  2285. if (!image) return Promise.reject('Last.fm: no cover for matched album');
  2286. image = image['#text'];
  2287. return setCover(image.replace(/\/\d+(?:x\d+|s)\//i, '/')).catch(reason => setCover(image))
  2288. .then(() => { info('Last.fm', result.album.url, result.album.id || result.album.mbid || '#N/A') });
  2289. });
  2290. }
  2291. function searchCoverOnline_Qobuz() {
  2292. qbLookup().then(function(album) {
  2293. return setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_max'))
  2294. .catch(reason => setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_600')))
  2295. .catch(reason => setCover(album.imgUrl))
  2296. .then(function(imgUrl) {
  2297. info('Qobuz', album.href, album.id);
  2298. return imgUrl;
  2299. });
  2300. });
  2301. }
  2302. function info(service, url, id) {
  2303. addMessage(new HTML('used cover image from ' + service + ' release Id ' +
  2304. '<a style="color: #00f3ff;" target="_blank" href="'+ url + '">' + id + '</a>'), 'info');
  2305. }
  2306. }
  2307.  
  2308. function setItunesImage(album) {
  2309. return urlParser.test(album.artworkUrl100) ?
  2310. setCover(album.artworkUrl100.replace('100x100bb', '100000x100000-999'))
  2311. .catch(reason => setCover(album.artworkUrl100)) : Promise.reject('Apple Music image not valid URL');
  2312. }
  2313. function setDeezerImage(album) {
  2314. return urlParser.test(album.cover_xl) ?
  2315. setCover(album.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0'))
  2316. .catch(reason => setCover(album.cover_xl)) : Promise.reject('Deezer image not valid URL');
  2317. }
  2318.  
  2319. function completeFromOnlineSource(onlineTracks) {
  2320. fillMissingValue(document.getElementById('media'), 'media');
  2321. fillMissingValue(document.getElementById('year'), 'album_year');
  2322. ref = document.getElementById('remaster_year') || !isUpload && document.querySelector('input[name="year"]');
  2323. if (ref != null && !ref.disabled && (ref.value == '' || !isRED && ref.value == '---')) {
  2324. var value = getHomoValue('release_date');
  2325. if (value != null) ref.value = extractYear(value);
  2326. }
  2327. fillMissingValue(document.getElementById('remaster_record_label')
  2328. || document.querySelector('input[name="recordlabel"]'), 'label');
  2329. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  2330. || document.querySelector('input[name="cataloguenumber"]'))) {
  2331. let catNo = getHomoValue('catalog');
  2332. if (!catNo && onlineTracks.every(track => track.identifiers.BARCODE
  2333. && track.identifiers.BARCODE == onlineTracks[0].identifiers.BARCODE)) {
  2334. catNo = parseInt(onlineTracks[0].identifiers.BARCODE.replace(/\s+/g, ''));
  2335. }
  2336. if (catNo) ref.value = catNo;
  2337. }
  2338. return onlineTracks;
  2339.  
  2340. function getHomoValue(propName) {
  2341. return onlineTracks[0][propName] && onlineTracks.map(track => track[propName]).homogeneous() ?
  2342. onlineTracks[0][propName] : null;
  2343. }
  2344. function fillMissingValue(node, propName) {
  2345. if (!node || node.disabled || node.value != '' && (isRED || node.value != '---')) return;
  2346. var value = getHomoValue(propName);
  2347. if (value != null) node.value = value;
  2348. }
  2349. }
  2350.  
  2351. function onlineCheck(onlineTracks) {
  2352. if (!Array.isArray(onlineTracks) || onlineTracks.length <= 0) {
  2353. addMessage('online check not performed (empty tracklist)', 'notice');
  2354. return Promise.reject('No tracks');
  2355. }
  2356. var issueCounter = 0, hiresTimes = onlineTracks.every(track => track.identifiers.SPOTIFY_ID);
  2357. if (onlineTracks[0].artist && onlineTracks.map(track => track.artist).homogeneous()
  2358. && (isVA ? !vaParser.test(onlineTracks[0].artist) : mainArtistMismatch())) {
  2359. ++issueCounter;
  2360. addMessage(new HTML('online album main artist mismatch ("' +
  2361. safeText(release.artist).bold() + '" ≠ "' + safeText(onlineTracks[0].artist).bold() + '")'), 'warning');
  2362. }
  2363. if (onlineTracks[0].album && onlineTracks.map(track => track.album).homogeneous()
  2364. && mismatch(release.album, onlineTracks[0].album)
  2365. && mismatch(release.album, removeFeatArtists(onlineTracks[0].album))) {
  2366. ++issueCounter;
  2367. addMessage(new HTML('online album title mismatch ("' +
  2368. safeText(release.album).bold() + '" ≠ "' + safeText(onlineTracks[0].album).bold() + '")'), 'warning');
  2369. }
  2370. if (onlineTracks[0].label && onlineTracks.map(track => track.label).homogeneous()
  2371. && mismatch(release.label, onlineTracks[0].label, /-/g)) {
  2372. ++issueCounter;
  2373. addMessage(new HTML('online album label mismatch ("' +
  2374. safeText(release.label).bold() + '" ≠ "' + safeText(onlineTracks[0].label).bold() + '")'), 'notice');
  2375. }
  2376. if (release.catalogs.length == 1
  2377. && onlineTracks[0].catalog && onlineTracks.map(track => track.catalog).homogeneous()
  2378. && mismatch(release.catalogs[0], onlineTracks[0].catalog, /[\s\-]/g)) {
  2379. ++issueCounter;
  2380. addMessage(new HTML('online album catalogue# mismatch ("' +
  2381. safeText(release.catalogs[0]).bold() + '" ≠ "' + safeText(onlineTracks[0].catalog).bold() + '")'), 'notice');
  2382. }
  2383. if (onlineTracks[0].album_year && onlineTracks.map(track => track.album_year).homogeneous()
  2384. && release.album_year != onlineTracks[0].album_year) {
  2385. ++issueCounter;
  2386. addMessage(new HTML('online album year mismatch (' +
  2387. (release.album_year || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].album_year.toString().bold() + ')'), 'warning');
  2388. }
  2389. if (onlineTracks[0].release_date && onlineTracks.map(track => track.release_date).homogeneous()
  2390. && new Date(release.release_date.toString()).getDateValue()
  2391. != new Date(onlineTracks[0].release_date.toString()).getDateValue()) {
  2392. ++issueCounter;
  2393. addMessage(new HTML('online album release date mismatch (' +
  2394. (release.release_date || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].release_date.toString().bold() + ')'), 'notice');
  2395. }
  2396. if (tracks.length != onlineTracks.length) {
  2397. ++issueCounter;
  2398. addMessage(new HTML('online album different tracklist length (' + tracks.length.toString().bold() +
  2399. ' ≠ ' + onlineTracks.length.toString().bold() + ')'), 'warning');
  2400. }
  2401. if (totalTime > 0) {
  2402. let ttOnline = onlineTracks.reduce((acc, track) => acc + (track.duration || NaN), 0);
  2403. if (ttOnline > 0 && Math.abs(totalTime - ttOnline) * 100 / ttOnline > (media == 'Vinyl' ?
  2404. prefs.vinyl_duration_divergency : hiresTimes ? 0.1 : prefs.duration_divergency)) {
  2405. ++issueCounter;
  2406. addMessage(new HTML('online album duration mismatch (' + makeTimeString(totalTime).bold() +
  2407. ' ≠ ' + makeTimeString(ttOnline).bold() + ')'), 'warning');
  2408. }
  2409. }
  2410. for (let ndx = 0; ndx < tracks.length; ++ndx) {
  2411. if (ndx >= onlineTracks.length) {
  2412. addMessage('end of online tracklist reached, tracks from #' + (ndx + 1) + ' to end will not be checked', 'notice');
  2413. break;
  2414. }
  2415. if (mismatch(tracks[ndx].title, onlineTracks[ndx].title)
  2416. && mismatch(tracks[ndx].title, removeFeatArtists(onlineTracks[ndx].title))) {
  2417. ++issueCounter;
  2418. addMessage('online track #' + (ndx + 1) + ' title mismatch ("' +
  2419. (tracks[ndx].title || '') + '" ≠ "' + (onlineTracks[ndx].title || '') + '")', 'warning');
  2420. }
  2421. if (onlineTracks[ndx].track_artist && onlineTracks[ndx].track_artist != onlineTracks[ndx].artist) {
  2422. var trackArtists = getArtists(tracks[ndx].track_artist);
  2423. var onlineSrackArtists = getArtists(onlineTracks[ndx].track_artist);
  2424. if (!trackArtists[0].equalCaselessTo(onlineSrackArtists[0])
  2425. || !trackArtists[1].equalCaselessTo(onlineSrackArtists[1])) {
  2426. ++issueCounter;
  2427. addMessage('online track #' + (ndx + 1) + ' track artist mismatch ("' +
  2428. (tracks[ndx].track_artist || '') + '" ≠ "' + (onlineTracks[ndx].track_artist || '') + '")', 'notice');
  2429. }
  2430. }
  2431. if (onlineTracks[ndx].tracknumber && tracks[ndx].tracknumber != onlineTracks[ndx].tracknumber) {
  2432. ++issueCounter;
  2433. addMessage('online track #' + (ndx + 1) + ' track number mismatch (' +
  2434. (tracks[ndx].tracknumber || '<unset>') + ' ≠ ' + onlineTracks[ndx].tracknumber + ')',
  2435. release.totaldiscs > 1 ? 'notice' : 'warning');
  2436. }
  2437. if (onlineTracks[ndx].discnumber && (onlineTracks[ndx].discnumber > 1 || tracks[ndx].discnumber)
  2438. && tracks[ndx].discnumber != onlineTracks[ndx].discnumber) {
  2439. ++issueCounter;
  2440. addMessage('online track #' + (ndx + 1) + ' disc number mismatch (' +
  2441. (tracks[ndx].discnumber || '<unset>') + ' ≠ ' + onlineTracks[ndx].discnumber + ')', 'warning');
  2442. }
  2443. if (onlineTracks[ndx].discsubtitle && mismatch(tracks[ndx].discsubtitle, onlineTracks[ndx].discsubtitle)) {
  2444. ++issueCounter;
  2445. addMessage('online track #' + (ndx + 1) + ' disc subtitle mismatch ("' +
  2446. (tracks[ndx].discsubtitle || '') + '" ≠ "' + onlineTracks[ndx].discsubtitle + '")', 'notice');
  2447. }
  2448. let timeDif = tracks[ndx].duration && onlineTracks[ndx].duration
  2449. && Math.abs(tracks[ndx].duration - onlineTracks[ndx].duration);
  2450. if (timeDif >= (media != 'Vinyl' ? 1.5 : hiresTimes ? 0.1 : 5)) {
  2451. ++issueCounter;
  2452. addMessage('online track #' + (ndx + 1) + ' duration mismatch (' +
  2453. makeTimeString(tracks[ndx].duration) + ' ≠ ' + makeTimeString(onlineTracks[ndx].duration) + ')',
  2454. (timeDif >= (media != 'Vinyl' ? 3 : hiresTimes ? 0.2 : 8) ? 'warning' : 'notice'));
  2455. }
  2456. }
  2457. if (issueCounter == 0) {
  2458. i = 'online check completed without remarks';
  2459. if (prefs.messages_verbosity >= 1) addMessage(i, 'info'); else console.debug(i);
  2460. }
  2461.  
  2462. function mainArtistMismatch() {
  2463. var onlineMainArtists = getArtists(onlineTracks[0].artist);
  2464. return !onlineMainArtists[0].equalCaselessTo(artists[0]) || !onlineMainArtists[1].equalCaselessTo(albumGuests);
  2465. }
  2466. function removeFeatArtists(title) {
  2467. return featParsers.slice(3).reduce(function(acc, rx, ndx) {
  2468. return rx.test(acc) && (ndx < 5 || splitArtists(RegExp.$1).every((artist, ndx) => looksLikeTrueName(artist, 1))) ?
  2469. acc.replace(rx, '') : acc;
  2470. }, title || '')
  2471. }
  2472. function mismatch(localStr, onlineStr, rx) {
  2473. return normalize(localStr) != normalize(onlineStr);
  2474.  
  2475. function normalize(val) {
  2476. if (val == undefined || val == null) return '';
  2477. if (typeof val != 'string') val = val.toString();
  2478. if (rx instanceof RegExp || typeof rx == 'string') val = val.replace(rx, '');
  2479. val = val.replace(/[\(\)\-\s]+/g, '');
  2480. return prefs.strict_online_check ? val : val.toLowerCase();
  2481. }
  2482. }
  2483. }
  2484.  
  2485. function lookupOnlineSource() {
  2486. const commonMedia = !media || ['CD', 'WEB'].includes(media);
  2487. const noMultivolume = !release.totaldiscs || release.totaldiscs < 2;
  2488. var workers = [
  2489. /* 0 */ barCode ? querySpotifyAPI('search', { q: 'barcode:' + barCode, type: 'album' })
  2490. .then(result => result.albums.total > 0 ? result.albums.items : Promise.reject('Spotify: no matches'))
  2491. : Promise.reject('Spotify: unknown barcode'),
  2492. /* 1 */ commonMedia ? spotifyLookup() : Promise.reject('Spotify: different media'),
  2493. /* 2 */ mbLookupByBarcode(),
  2494. /* 3 */ mbLookupByASIN(),
  2495. /* 4 */ mbLookupByTOC(),
  2496. /* 5 */ mbLookup(),
  2497. /* 6 */ commonMedia && noMultivolume ? deezerLookup() : Promise.reject('Deezer: different media'),
  2498. /* 7 */ commonMedia ? amLookup() : Promise.reject('Apple Music: different media'),
  2499. /* 8 */ dcLookup(),
  2500. /* 9 */ mbLookupByAutoTOC(),
  2501. /* 10 */ commonMedia && noMultivolume ? queryLastFmAPI('album.getinfo', {
  2502. artist: (isVA ? VA : release.artist),
  2503. album: release.album,
  2504. }).then(result => result.error ? Promise.reject('Last.fm: '.concat(result.message)) : result.album)
  2505. : Promise.reject('Last.fm: different media'),
  2506. ];
  2507. // workers.forEach(function(worker, ndx) {
  2508. // worker.then(result => { console.debug('Worker[' + ndx + '] matched:', result) })
  2509. // .catch(reason => { console.debug('Worker[' + ndx + '] failed:', reason) });
  2510. // });
  2511. return workers[0].then(function(albums) {
  2512. console.debug('Spotify lookup by barcode successfull:', barCode, ' matches:', albums.length);
  2513. info('Spotify', albums[0].external_urls.spotify, albums[0].id);
  2514. return albums[0].href;
  2515. }).catch(reason => workers[1].then(function(album) {
  2516. info('Spotify', album.external_urls.spotify, album.id);
  2517. return album.href;
  2518. })).catch(reason => workers[2].then(mbEpilogue))
  2519. .catch(reason => workers[3].then(mbEpilogue))
  2520. .catch(reason => workers[4].then(mbEpilogue))
  2521. .catch(reason => workers[5].then(function(release) {
  2522. info('MusicBrainz', mbrRlsPrefix + release.id, release.id);
  2523. return mbrRlsPrefix.concat(release.id);
  2524. })).catch(reason => workers[6].then(function(album) {
  2525. info('Deezer', deezerAlbumPrefix.concat(album.id), album.id);
  2526. return 'https://api.deezer.com/album/'.concat(album.id);
  2527. })).catch(reason => workers[7].then(function(collection) {
  2528. info('Apple Music', collection.collectionViewUrl, collection.collectionId);
  2529. return collection.collectionViewUrl;
  2530. })).catch(reason => workers[8].then(function(releases) {
  2531. info('Discogs', discogsOrigin.concat(releases[0].uri), releases[0].id);
  2532. return releases[0].resource_url;
  2533. })).catch(reason => workers[9].then(mbEpilogue))
  2534. .catch(reason => workers[10].then(function(album) {
  2535. info('Last.fm', album.url, album.id || album.mbid || '#N/A');
  2536. return album; // return object
  2537. })).catch(function(reason) {
  2538. reason = 'online check not performed (no matches for this release)';
  2539. if (prefs.check_integrity_online) addMessage(reason, 'notice');
  2540. return Promise.reject(reason);
  2541. });
  2542.  
  2543. function mbEpilogue(releases) {
  2544. info('MusicBrainz', mbrRlsPrefix + releases[0].id, releases[0].id);
  2545. return mbrRlsPrefix + releases[0].id;
  2546. }
  2547. function info(service, url, id) {
  2548. if (prefs.check_integrity_online) addMessage(new HTML('checking online against ' + service +
  2549. ' release Id <a style="color: #00f3ff;" target="_blank" href="' + url + '">' + id + '</a>'), 'info');
  2550. }
  2551. }
  2552.  
  2553. function spotifyLookup() {
  2554. return querySpotifyAPI('search', {
  2555. q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
  2556. type: 'album',
  2557. limit: 50,
  2558. }).then(function(result) {
  2559. if (result.albums.total <= 0) return Promise.reject('Spotify: no matches');
  2560. for (i = 0; i < 3; ++i) {
  2561. var f = filter(i);
  2562. if (f.length > 1) return Promise.reject('Spotify: ambiguity');
  2563. if (f.length == 1) break;
  2564. }
  2565. if (i >= 3) return Promise.reject('Spotify: no matches');
  2566. if (i == 2) console.debug('Spotify fuzzy match:', release, '==', f[0]);
  2567. return f[0];
  2568.  
  2569. function filter(level) {
  2570. return result.albums.items.filter(function(album) {
  2571. return (album.album_type == 'single' ? ['Single', 'EP'].some(rt => releaseType == getReleaseIndex(rt))
  2572. : releaseType != getReleaseIndex('Single'))
  2573. && releasesMatch(album.artists.map(artist => artist.name), album.name, level);
  2574. });
  2575. }
  2576. })
  2577. }
  2578. function deezerLookup() {
  2579. return queryDeezerAPI('search', {
  2580. q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
  2581. strict: 'on',
  2582. order: 'RANKING',
  2583. }).then(function(result) {
  2584. if (result.total <= 0) return Promise.reject('Deezer: no matches');
  2585. for (i = 0; i < 3; ++i) {
  2586. var f = filter(i);
  2587. if (f.length > 1) return Promise.reject('Deezer: ambiguity');
  2588. if (f.length == 1) break;
  2589. }
  2590. if (i >= 3) return Promise.reject('Deezer: no matches');
  2591. if (i == 2) console.debug('Deezer fuzzy match:', release, '==', f[0]);
  2592. return f[0];
  2593.  
  2594. function filter(level) {
  2595. var albums = [];
  2596. result.data.forEach(function(match) {
  2597. if (!releasesMatch(match.artist.name, match.album.title, level)) return;
  2598. if (albums.find(album => album.id == match.album.id) == undefined) albums.push(match.album);
  2599. });
  2600. return albums;
  2601. }
  2602. });
  2603. }
  2604. function amLookup() {
  2605. return queryItunesAPI('search', {
  2606. term: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2607. media: 'music',
  2608. entity: 'album',
  2609. //country: 'US',
  2610. }).then(function(result) {
  2611. if (result.resultCount <= 0) return Promise.reject('Apple Music: no matches');
  2612. for (i = 0; i < 3; ++i) {
  2613. var f = filter(i);
  2614. if (f.length > 1) return Promise.reject('Apple Music: ambiguity');
  2615. if (f.length == 1) break;
  2616. }
  2617. if (i >= 3) return Promise.reject('Apple Music: no matches');
  2618. if (i == 2) console.debug('Apple Music fuzzy match:', release, '==', f[0]);
  2619. return f[0];
  2620.  
  2621. function filter(level) {
  2622. var preFilter = result.results.filter(function(collection) {
  2623. var isSingle = collection.collectionName.endsWith(' - Single');
  2624. if (isSingle) collection.collectionName = collection.collectionName.slice(0, -9);
  2625. var isEP = collection.collectionName.endsWith(' - EP');
  2626. if (isEP) collection.collectionName = collection.collectionName.slice(0, -5);
  2627. isSingle = isSingle || collection.collectionType == 'Single';
  2628. isEP = !isSingle && (isEP || collection.collectionType == 'EP');
  2629. return (releaseType == getReleaseIndex('Single')) == isSingle
  2630. && (!isEP || releaseType == getReleaseIndex('EP'))
  2631. && releasesMatch(collection.artistName, collection.collectionName, level);
  2632. });
  2633. return preFilter.some(collection => /\b(?:explicit)/i.test(collection.collectionExplicitness)) ?
  2634. preFilter.filter(collection => !/\b(?:clean)/i.test(collection.collectionExplicitness)) : preFilter;
  2635. }
  2636. });
  2637. }
  2638. function mbLookupByBarcode() {
  2639. if (!barCode) return Promise.reject('MusicBrainz: unknown barcode');
  2640. return queryMusicBrainzAPI('release', { query: 'barcode:' + barCode }).then(function(result) {
  2641. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2642. console.debug('MusicBrainz lookup by barcode successfull: ' + barCode + '; matches: ' + result.count);
  2643. return result.releases;
  2644. });
  2645. }
  2646. function mbLookupByASIN() {
  2647. var asin = getHomoIdentifier('ASIN');
  2648. if (!asin) return Promise.reject('MusicBrainz: unknown ASIN');
  2649. asin = asin.replace(/\s+/g, '');
  2650. return queryMusicBrainzAPI('release', { query: 'asin:' + asin }).then(function(result) {
  2651. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2652. console.debug('MusicBrainz lookup by ASIN successfull: ' + asin + '; matches: ' + result.count);
  2653. return result.releases;
  2654. });
  2655. }
  2656. function mbLookupByTOC() {
  2657. var TOC;
  2658. if (TOC = getHomoIdentifier('ITUNES_TOC')) { // iTunes scheme
  2659. TOC = TOC.split('+').map(index => parseInt(index));
  2660. TOC = [1, TOC[2], TOC[1]].concat(TOC.slice(3));
  2661. } else if (TOC = getHomoIdentifier('CT_TOC')) { // CUETools scheme
  2662. TOC = TOC.split('+').map(index => parseInt(index, 16));
  2663. TOC = [1, TOC.shift(), TOC.pop()].concat(TOC);
  2664. }
  2665. return mbLookupByDiscID(TOC);
  2666. }
  2667. function mbLookupByAutoTOC() {
  2668. if (release.totaldiscs > 1) return Promise.reject('TOC lookup not possible for multidisc release');
  2669. if (!tracks.every(track => track.sr > 0 && track.samples > 0))
  2670. return Promise.reject('MusicBrainz: insufficient information for TOC calculation');
  2671. var lastFrame = 0;
  2672. var TOC = [0].concat(tracks.map(track => (lastFrame += Math.round(track.samples * 75 / track.sr))))
  2673. .map(offset => 150 + offset);
  2674. TOC.unshift(TOC.pop());
  2675. return mbLookupByDiscID([1, tracks.length].concat(TOC));
  2676. }
  2677. function mbComputeDiscID(mbTOC) {
  2678. if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4 || mbTOC[1] - mbTOC[0] > 98) return null;
  2679. var tocStr = [mbTOC[0], mbTOC[1]].map(track => track.toString(16).padStart(2, '0'))
  2680. .concat(mbTOC.slice(2).map(offset => offset.toString(16).padStart(8, '0'))).join('')
  2681. .concat('0'.repeat(98 + mbTOC[0] - mbTOC[1] << 3)).toUpperCase();
  2682. var digest = sha1.digest(tocStr);
  2683. return btoa(String.fromCharCode(...digest)).replace(/\+/g, '.').replace(/\//g, '_').replace(/\=/g, '-');
  2684. }
  2685. function mbLookupByDiscID(mbTOC) {
  2686. if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4)
  2687. return Promise.reject('MusicBrainz: missing or invalid TOC');
  2688. var mbDscId = mbComputeDiscID(mbTOC);
  2689. var params = { toc: mbTOC.join('+') };
  2690. if (media != 'CD') params['media-format'] = 'all';
  2691. return queryMusicBrainzAPI('discid/'.concat(mbDscId || '-'), params).then(function(result) {
  2692. if (Array.isArray(result.releases) && result.releases.length > 0) var matches = result.releases;
  2693. if (result.id && result.title) matches = [result];
  2694. if (!Array.isArray(matches)) return Promise.reject('MusicBrainz: no matches');
  2695. console.debug('MusicBrainz lookup by discId/TOC successfull:', mbDscId, '/', params, 'matches:', matches.length);
  2696. return matches;
  2697. });
  2698. }
  2699. function mbLookup() {
  2700. return queryMusicBrainzAPI('release', {
  2701. query: 'release:"' + release.album + '" AND artist:"' + (isVA ? VA : release.artist) + '"',
  2702. }).then(function(result) {
  2703. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2704. for (i = 0; i < 3; ++i) {
  2705. var f = filter(i);
  2706. if (f.length > 1) return Promise.reject('MusicBrainz: ambiguity');
  2707. if (f.length == 1) break;
  2708. }
  2709. if (i >= 3) return Promise.reject('MusicBrainz: no matches');
  2710. if (i == 2) console.debug('MusicBrainz fuzzy match:', release, '==', f[0]);
  2711. return f[0];
  2712.  
  2713. function filter(level) {
  2714. return result.releases.filter(function(release) {
  2715. return release.quality != 'low'
  2716. && (media ? [media] : tracks.some(notRedBook) ? ['WEB'] : ['CD', 'WEB'])
  2717. .some(_media => release.media.map(media => remapMedia(media.format)).includes(_media)
  2718. && releasesMatch(release['artist-credit'].map(artist => artist.name), release.title, level));
  2719. });
  2720.  
  2721. function remapMedia(MBmedia) {
  2722. return [
  2723. ['Digital Media', 'WEB'],
  2724. ].reduce((acc, subst) => acc.toLowerCase() == subst[0].toLowerCase() ? subst[1] : acc, MBmedia);
  2725. }
  2726. }
  2727. });
  2728. }
  2729. function dcLookup() {
  2730. var query = { type: 'release' };
  2731. if (barCode) query.barcode = barCode; else {
  2732. query.artist = '"' + release.artist + '"';
  2733. query.release_title = '"' + release.album + '"';
  2734. //if (release.catalogs.length > 0) query.catno = release.catalogs.join('; ');
  2735. }
  2736. return queryDiscogsAPI('database/search', query).then(function(result) {
  2737. if (result.results.length <= 0) return Promise.reject('Discogs: no matches');
  2738. if (barCode) {
  2739. //if (result.results.length > 1) return Promise.reject('Discogs: ambiguity');
  2740. console.debug('Discogs lookup by barcode successfull: ' + barCode + '; matches: ' + result.results.length);
  2741. var f = result.results;
  2742. } else {
  2743. for (i = 0; i < 3; ++i) {
  2744. f = filter(i);
  2745. if (f.length > 1) return Promise.reject('Discogs: ambiguity');
  2746. if (f.length == 1) break;
  2747. }
  2748. if (i >= 3) return Promise.reject('Discogs: no matches');
  2749. if (i == 2) console.debug('Discogs fuzzy match:', release, '==', f[0]);
  2750. }
  2751. return f;
  2752.  
  2753. function filter(level) {
  2754. return result.results.filter(function(album) {
  2755. if (media ? Array.isArray(album.format)
  2756. && !album.format.some(format => dcFmtToGazelle(format) == media)
  2757. : !album.format.some(format => ['CD', 'WEB'].includes(dcFmtToGazelle(format)))) return false;
  2758. if (/^(.*?)\s+\(\d+\) - (.*)$/.test(album.title) || !/^(.*?) - (.*)$/.test(album.title))
  2759. return releasesMatch(RegExp.$1, RegExp.$2, level);
  2760. console.warn('Failed to parse Discogs title:', album.title);
  2761. return false;
  2762. });
  2763. }
  2764. });
  2765. }
  2766. function qbLookup() {
  2767. var params = new URLSearchParams({
  2768. q: (isVA ? VA : release.artist) + ' ' + release.album,
  2769. //s: 'rdc', // descending sort by release date
  2770. i: 'boutique',
  2771. });
  2772. return globalFetch('https://www.qobuz.com/search?' + params).then(function(response) {
  2773. var results = response.document.querySelectorAll('div.search-results > div.product');
  2774. if (results.length <= 0) return Promise.reject('Qobuz: no matches');
  2775. for (i = 0; i < 3; ++i) {
  2776. var f = filter(i);
  2777. if (f.length > 1) return Promise.reject('Qobuz: ambiguity');
  2778. if (f.length == 1) break;
  2779. }
  2780. if (i >= 3) return Promise.reject('Qobuz: no matches');
  2781. if (i == 2) console.debug('Qobuz fuzzy match:', release, '==', f[0]);
  2782. return f[0];
  2783.  
  2784. function filter(level) {
  2785. var _results = [];
  2786. results.forEach(function(result) {
  2787. var _result = {};
  2788. _result.artist = result.querySelector('div.artist-name > a');
  2789. if (_result.artist != null) _result.artist = _result.artist.textContent.trim();
  2790. _result.title = result.querySelector('div.album-title > a');
  2791. if (_result.title != null) {
  2792. _result.id = _result.title.pathname.replace(/^.*\//, '');
  2793. _result.href = 'https://www.qobuz.com' + _result.title.pathname;
  2794. _result.title = _result.title.textContent.trim();
  2795. }
  2796. _result.imgUrl = result.querySelector('div.album-cover > a > img');
  2797. if (_result.imgUrl != null) _result.imgUrl = _result.imgUrl.dataset.src || _result.imgUrl.src;
  2798. if (_result.artist && _result.title && _result.imgUrl
  2799. && releasesMatch(_result.artist, _result.title, level, 0.75)) _results.push(_result);
  2800. });
  2801. return _results;
  2802. }
  2803. });
  2804. }
  2805.  
  2806. function ruleLink(rule) {
  2807. return ' (<a href="https://redacted.ch/rules.php?p=upload#r' + rule + '" target="_blank">' + rule + '</a>)';
  2808. }
  2809.  
  2810. function releasesMatch(remoteArtist, remoteTitle, relaxLevel = 0, minSimilarity = 0.75, minFullSimilarity) {
  2811. if (typeof remoteArtist == 'string') {
  2812. if (isVA != vaParser.test(remoteArtist)) return false;
  2813. if (!isVA) remoteArtist = getArtists(remoteArtist)[0];
  2814. } else if (!Array.isArray(remoteArtist)) return false;
  2815. if (!isVA && !artists[0].equalCaselessTo(remoteArtist)
  2816. && !artists[0].map(name => name.toASCII()).equalCaselessTo(remoteArtist.map(name => name.toASCII())))
  2817. return false;
  2818. if (!remoteTitle) return true;
  2819. if (typeof remoteTitle != 'string') return false;
  2820. var localTitle = release.album.toLowerCase();
  2821. if (localTitle == (remoteTitle = remoteTitle.toLowerCase())) return true;
  2822. if (editionTitle) var fullLocalTitle = localTitle.concat(' (', editionTitle.toLowerCase(), ')');
  2823. if (fullLocalTitle === remoteTitle) return true;
  2824. if (localTitle.toASCII() == remoteTitle.toASCII()
  2825. || fullLocalTitle && fullLocalTitle.toASCII() == remoteTitle.toASCII()) return true;
  2826. if (relaxLevel <= 0) return false;
  2827. if ([
  2828. /[\s\,\.\-\!\(\)\!\?]+/g,
  2829. /\s+\(([^\(\)]+)\)$/,
  2830. /\s+\[([^\[\]]+)\]$/,
  2831. /\s+\{([^\{\}]+)\}$/,
  2832. ].reduce(function(acc, rx) {
  2833. return acc || localTitle.replace(rx, '') == remoteTitle.replace(rx, '')
  2834. || fullLocalTitle && fullLocalTitle.replace(rx, '') == remoteTitle.replace(rx, '');
  2835. }, false)) return true;
  2836. if (relaxLevel <= 1) return false;
  2837. var similarity = cosineSimilarity(localTitle, remoteTitle);
  2838. if (similarity >= Math.min(minSimilarity, 1)) {
  2839. console.debug('Cosine similarity accepted: "' + localTitle + '"=="' + remoteTitle + '" (' + similarity+ ')');
  2840. return true;
  2841. }
  2842. similarity = cosineSimilarity(fullLocalTitle, remoteTitle);
  2843. if (fullLocalTitle && similarity >= Math.min(minFullSimilarity || minSimilarity + 0.05, 1)) {
  2844. console.debug('Cosine similarity accepted: "' + fullLocalTitle + '"=="' + remoteTitle + '" (' + similarity+ ')');
  2845. return true;
  2846. }
  2847. if (relaxLevel <= 2) return false;
  2848. if (localTitle.includes(remoteTitle) || remoteTitle.includes(localTitle)) return true;
  2849. return false;
  2850. }
  2851.  
  2852. function trackComparer(a, b) {
  2853. var cmp;
  2854. if (release.totaldiscs > 1) {
  2855. cmp = a.discnumber - b.discnumber;
  2856. if (!isNaN(cmp) && cmp != 0) return cmp;
  2857. } else {
  2858. cmp = (a.discsubtitle || '').localeCompare(b.discsubtitle || '');
  2859. //if (cmp != 0) return cmp;
  2860. }
  2861. cmp = parseInt(a.tracknumber) - parseInt(b.tracknumber);
  2862. if (!isNaN(cmp)) return cmp;
  2863. var m1 = vinyltrackParser.exec(a.tracknumber.toUpperCase());
  2864. var m2 = vinyltrackParser.exec(b.tracknumber.toUpperCase());
  2865. return m1 != null && m2 != null ?
  2866. m1[1].localeCompare(m2[1]) || parseFloat(m1[2]) - parseFloat(m2[2]) :
  2867. a.tracknumber.toUpperCase().localeCompare(b.tracknumber.toUpperCase());
  2868. }
  2869.  
  2870. function reqSelectFormats(...vals) {
  2871. vals.forEach(function(val) {
  2872. ['MP3', 'FLAC', 'AAC', 'AC3', 'DTS'].forEach(function(fmt, ndx) {
  2873. if (val.toLowerCase() == fmt.toLowerCase() && (ref = document.getElementById('format_' + ndx)) != null) {
  2874. ref.checked = true;
  2875. ref.onchange();
  2876. }
  2877. });
  2878. });
  2879. }
  2880.  
  2881. function reqSelectBitrates(...vals) {
  2882. const bitrateSet = !isOPS ? [
  2883. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  2884. 'V0 (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  2885. ] : [
  2886. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  2887. 'V0 (VBR)', 'q8.x (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  2888. ];
  2889. vals.forEach(function(val) {
  2890. var ndx = 10;
  2891. bitrateSet.forEach((it, _ndx) => { if (val.toString().toLowerCase() == it.toString().toLowerCase()) ndx = _ndx });
  2892. if ((ref = document.getElementById('bitrate_' + ndx)) != null) {
  2893. ref.checked = true;
  2894. ref.onchange();
  2895. }
  2896. });
  2897. }
  2898.  
  2899. function reqSelectMedias(...vals) {
  2900. const mediaSet = !isOPS ? ['CD', 'DVD', 'Vinyl', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB', 'Blu-Ray']
  2901. : ['CD', 'DVD', 'Vinyl', 'BD', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB'];
  2902. vals.forEach(function(val) {
  2903. mediaSet.forEach(function(med, ndx) {
  2904. if (val == med && (ref = document.getElementById('media_' + ndx)) != null) {
  2905. ref.checked = true;
  2906. ref.onchange();
  2907. }
  2908. });
  2909. if (val == 'CD') {
  2910. if ((ref = document.getElementById('needlog')) != null) {
  2911. ref.checked = true;
  2912. ref.onchange();
  2913. if ((ref = document.getElementById('minlogscore')) != null) ref.value = 100;
  2914. }
  2915. if ((ref = document.getElementById('needcue')) != null) ref.checked = true;
  2916. //if ((ref = document.getElementById('needchecksum')) != null) ref.checked = true;
  2917. }
  2918. });
  2919. }
  2920.  
  2921. function getReleaseIndex(str) {
  2922. var ndx;
  2923. [
  2924. ['Album', 1],
  2925. ['Soundtrack', 3],
  2926. ['EP', 5],
  2927. ['Anthology', 6],
  2928. ['Compilation', 7],
  2929. ['Single', 9],
  2930. ['Live album', 11],
  2931. ['Remix', 13],
  2932. ['Bootleg', 14],
  2933. ['Interview', 15],
  2934. ['Mixtape', 16],
  2935. [isOPS ? 'DJ Mix' : 'Demo', 17],
  2936. ['Concert Recording', 18],
  2937. ['DJ Mix', 19],
  2938. ['Unknown', 21],
  2939. ].forEach(k => { if (str.toLowerCase() == k[0].toLowerCase()) ndx = k[1] });
  2940. return ndx || 21;
  2941. }
  2942.  
  2943. function getChanString(n) {
  2944. if (!n) return null;
  2945. const chanmap = [
  2946. 'mono',
  2947. 'stereo',
  2948. '2.1',
  2949. '4.0 surround sound',
  2950. '5.0 surround sound',
  2951. '5.1 surround sound',
  2952. '7.0 surround sound',
  2953. '7.1 surround sound',
  2954. ];
  2955. return n >= 1 && n <= 8 ? chanmap[n - 1] : n + 'chn surround sound';
  2956. }
  2957. } // parseTracks
  2958.  
  2959. function fetchOnline_Music(url, weak = false) {
  2960. if (!urlParser.test(url)) return Promise.reject('Invalid URL');
  2961. const discParser = /^(?:CD|DIS[CK]\s+|VOLUME\s+|DISCO\s+|DISQUE\s+)(\d+)(?:\s+of\s+(\d+))?$/i;
  2962. var ref, dom, artist, album, albumYear, releaseDate, channels, label, composer, bd, sr = 44.1,
  2963. description, compiler, producer, totalTracks, discSubtitle, discNumber, trackNumber, totalDiscs,
  2964. title, trackArtist, catalogue, encoding, format, bitrate, duration, country, media = 'WEB', imgUrl,
  2965. genres = [], trs, tracks = [], identifiers = {}, trackIdentifiers = {};
  2966. if (url.toLowerCase().includes('qobuz.com/')) return globalFetch(url).then(function(response) {
  2967. const error = new Error('Failed to parse Qobus release page');
  2968. var mainArtist;
  2969. if ((ref = response.document.querySelector('div.album-meta > h2.album-meta__artist')) == null) throw error;
  2970. artist = ref.title || ref.textContent.trim();
  2971. if ((ref = response.document.querySelector('div.album-meta > h1.album-meta__title')) == null) throw error;
  2972. album = ref.title || ref.textContent.trim();
  2973. ref = response.document.querySelector('div.album-meta > ul > li:first-of-type');
  2974. if (ref != null) releaseDate = normalizeDate(ref.textContent);
  2975. ref = response.document.querySelector('div.album-meta > ul > li:nth-of-type(2) > a');
  2976. if (ref != null) mainArtist = ref.title || ref.textContent.trim();
  2977. //ref = response.document.querySelector('p.album-about__copyright');
  2978. //if (ref != null) albumYear = extractYear(ref.textContent);
  2979. response.document.querySelectorAll('section#about > ul > li').forEach(function(it) {
  2980. function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) }
  2981. if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1);
  2982. if (/\b(\d+)\s*(?:track|pist[ae]|tracce|traccia)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1);
  2983. if (['Label', 'Etichetta', 'Sello'].some(l => it.textContent.trimLeft().startsWith(l))) {
  2984. label = it.firstElementChild.textContent.replace(/\s+/g, ' ').trim();
  2985. }
  2986. else if (['Composer', 'Compositeur', 'Komponist', 'Compositore', 'Compositor'].some(matchLabel)) {
  2987. composer = it.firstElementChild.textContent.trim();
  2988. if (pseudoArtistParsers.some(rx => rx.test(composer))) composer = undefined;
  2989. } else if (['Genre', 'Genere', 'Género'].some(g => it.textContent.startsWith(g)) && it.childElementCount > 0) {
  2990. genres = Array.from(it.querySelectorAll('a')).map(elem => elem.textContent.trim());
  2991. /*
  2992. if (genres.length >= 1 && ['Pop/Rock'].includes(genres[0])) genres.shift();
  2993. if (genres.length >= 2 && ['Alternative & Indie'].includes(genres[genres.length - 1])) genres.shift();
  2994. if (genres.length >= 1 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) {
  2995. while (genres.length > 1) genres.shift();
  2996. }
  2997. */
  2998. while (genres.length > 1) genres.shift();
  2999. }
  3000. });
  3001. if (totalTracks > 50) addMessage('long album, only first 50 tracks can be captured from Qobuz, which will result in incmplete release description', 'notice');
  3002. bd = 16; channels = 2; // defaults to CD quality
  3003. response.document.querySelectorAll('span.album-quality__info').forEach(function(k) {
  3004. if (/\b([\d\.\,]+)\s*kHz\b/i.test(k.textContent) != null) sr = parseFloat(RegExp.$1.replace(',', '.'));
  3005. if (/\b(\d+)[\-\s]*Bits?\b/i.test(k.textContent) != null) bd = parseInt(RegExp.$1);
  3006. if (/\b(?:Stereo)\b/i.test(k.textContent)) channels = 2;
  3007. if (/\b(\d)\.(\d)\b/.test(k.textContent)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  3008. });
  3009. getDescription(response, 'section#description > p', true);
  3010. if ((ref = response.document.querySelector('a[title="Qobuzissime"]')) != null) {
  3011. if (description) description += '\n';
  3012. description += '[align=center][url=https://www.qobuz.com' + ref.pathname +
  3013. '][img]https://ptpimg.me/4z35uj.png[/img][/url][/align]';
  3014. }
  3015. if ((ref = response.document.querySelector('div.album-cover > img')) != null) {
  3016. imgUrl = ref.src.replace(/_\d{3}(?=\.\w+$)/, '_max');
  3017. }
  3018. trs = response.document.querySelectorAll('div.player__item > div.player__tracks > div.track > div.track__items');
  3019. if (!totalTracks) totalTracks = trs.length;
  3020. return Array.from(trs).map(function(tr) {
  3021. trackIdentifiers = { TRACK_ID: tr.parentNode.dataset.track };
  3022. if (tr.parentNode.dataset.gtm) try {
  3023. let gtm = JSON.parse(tr.parentNode.dataset.gtm);
  3024. if (gtm.product.id) trackIdentifiers.QOBUZ_ID = gtm.product.id;
  3025. //if (gtm.product.type) trackIdentifiers.RELEASETYPE = gtm.product.type;
  3026. if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory];
  3027. } catch(e) { console.warn(e) }
  3028. if ((ref = tr.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) {
  3029. discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim();
  3030. guessDiscNumber();
  3031. }
  3032. return {
  3033. artist: artist,
  3034. album: album,
  3035. album_year: albumYear,
  3036. release_date: releaseDate,
  3037. label: label,
  3038. encoding: 'lossless',
  3039. codec: 'FLAC',
  3040. bd: bd || undefined,
  3041. sr: sr * 1000 || undefined,
  3042. channels: channels || undefined,
  3043. media: media,
  3044. genre: genres.join('; '),
  3045. discnumber: discNumber || 1,
  3046. totaldiscs: totalDiscs,
  3047. discsubtitle: discSubtitle,
  3048. tracknumber: parseInt(tr.querySelector('span[itemprop="position"]').textContent),
  3049. totaltracks: totalTracks,
  3050. title: (tr.querySelector('div.track__item--name > span') || tr.querySelector('span.track__item--name'))
  3051. .textContent.trim().replace(/\s+/g, ' '),
  3052. composer: composer,
  3053. duration: timeStringToTime(tr.querySelector('span.track__item--duration').textContent),
  3054. url: response.finalUrl,
  3055. description: description,
  3056. identifiers: mergeIds(),
  3057. cover_url: imgUrl,
  3058. };
  3059. });
  3060. });
  3061. else if (url.toLowerCase().includes('highresaudio.com/')) return globalFetch(url).then(function(response) {
  3062. if ((ref = response.document.querySelector('h1 > span.artist')) != null) artist = ref.textContent.trim();
  3063. if ((ref = response.document.getElementById('h1-album-title')) != null) album = ref.firstChild.textContent.trim();
  3064. response.document.querySelectorAll('div.album-col-info-data > div > p').forEach(function(k) {
  3065. if (/\b(?:Genre|Subgenre)\b/i.test(k.firstChild.textContent)) genres.push(k.lastChild.textContent.trim());
  3066. if (/\b(?:Label)\b/i.test(k.firstChild.textContent)) label = k.lastChild.textContent.trim();
  3067. if (/\b(?:Album[\s\-]Release)\b/i.test(k.firstChild.textContent)) {
  3068. albumYear = extractYear(k.lastChild.textContent);
  3069. }
  3070. if (/\b(?:HRA[\s\-]Release)\b/i.test(k.firstChild.textContent)) {
  3071. releaseDate = normalizeDate(k.lastChild.textContent);
  3072. }
  3073. });
  3074. i = 0;
  3075. response.document.querySelectorAll('tbody > tr > td.col-format').forEach(function(format) {
  3076. if (!/^(FLAC)\s*(\d+(?:[\.\,]\d+)?)\b/.test(format.textContent)) return;
  3077. format = RegExp.$1;
  3078. sr = parseFloat(RegExp.$2.replace(',', '.'));
  3079. ++i;
  3080. });
  3081. if (i > 1) sr = undefined; // ambiguous
  3082. getDescription(response, 'div#albumtab-info > p', true);
  3083. if ((ref = response.document.querySelector('div.albumbody > img.cover[data-pin-media]')) != null)
  3084. imgUrl = ref.dataset.pinMedia;
  3085. trs = response.document.querySelectorAll('ul.playlist > li.pltrack');
  3086. return Array.from(trs).map(function(tr) {
  3087. discNumber = undefined; discSubtitle = tr;
  3088. while ((discSubtitle = discSubtitle.previousElementSibling) != null) {
  3089. if (discSubtitle.nodeName == 'LI' && discSubtitle.className == 'plinfo') {
  3090. discSubtitle = discSubtitle.textContent.replace(/\s*:$/, '').trim();
  3091. guessDiscNumber();
  3092. break;
  3093. }
  3094. }
  3095. return {
  3096. artist: artist,
  3097. album: album,
  3098. album_year: albumYear,
  3099. release_date: releaseDate,
  3100. label: label,
  3101. encoding: 'lossless',
  3102. codec: 'FLAC',
  3103. bd: 24,
  3104. sr: sr * 1000,
  3105. media: media,
  3106. genre: genres.join('; '),
  3107. discnumber: discNumber,
  3108. totaldiscs: totalDiscs,
  3109. discsubtitle: discSubtitle || undefined,
  3110. tracknumber: parseInt(tr.querySelector('span.track').textContent),
  3111. totaltracks: trs.length,
  3112. title: tr.querySelector('span.title').textContent.trim().replace(/\s+/g, ' '),
  3113. duration: timeStringToTime(tr.querySelector('span.time').textContent),
  3114. url: response.finalUrl,
  3115. description: description,
  3116. identifiers: mergeIds(),
  3117. cover_url: imgUrl,
  3118. };
  3119. });
  3120. });
  3121. else if (url.toLowerCase().includes('bandcamp.com/')) return globalFetch(url).then(function(response) {
  3122. if ((ref = response.document.querySelector('span[itemprop="byArtist"] > a')) != null) artist = ref.textContent.trim();
  3123. if ((ref = response.document.querySelector('h2[itemprop="name"]')) != null) album = ref.textContent.trim();
  3124. ref = response.document.querySelector('div.tralbum-credits');
  3125. if (ref != null && /\brelease[ds]\s+(.*?\b\d{4})\b/i.test(ref.textContent)) releaseDate = RegExp.$1;
  3126. ref = response.document.querySelector('span.back-link-text > br');
  3127. if (ref != null && ref.nextSibling != null) label = ref.nextSibling.textContent.trim(); else {
  3128. ref = response.document.querySelector('p#band-name-location > span.title');
  3129. if (ref != null) label = ref.textContent.trim();
  3130. }
  3131. let tags = new TagManager;
  3132. response.document.querySelectorAll('div.tralbum-tags > a.tag').forEach(function(tag) {
  3133. if ([
  3134. artist,
  3135. ].every(t => tag.textContent.trim().toLowerCase() != t.toLowerCase())) tags.add(tag.textContent.trim());
  3136. });
  3137. description = [];
  3138. response.document.querySelectorAll('div.tralbumData').forEach(function(div) {
  3139. if (!div.classList.contains('tralbum-tags')) description.push(html2php(div, response.finalUrl).trim());
  3140. });
  3141. description = description.filter(p => p).join('\n\n');
  3142. if ((ref = response.document.querySelector('div#tralbumArt > a.popupImage')) != null) imgUrl = ref.href;
  3143. trs = response.document.querySelectorAll('table.track_list > tbody > tr[itemprop="tracks"]');
  3144. return Array.from(trs).map(tr => ({
  3145. artist: artist,
  3146. album: album,
  3147. //album_year: extractYear(releaseDate),
  3148. release_date: releaseDate,
  3149. label: label,
  3150. media: media,
  3151. genre: tags.toString(),
  3152. discnumber: discNumber,
  3153. totaldiscs: totalDiscs,
  3154. tracknumber: parseInt(tr.querySelector('div.track_number').textContent),
  3155. totaltracks: trs.length,
  3156. title: (tr.querySelector('div.title span.track-title')
  3157. || tr.querySelector('div.title span[itemprop="name"]')).textContent.trim().replace(/\s+/g, ' '),
  3158. duration: (ref = tr.querySelector('span.time')) != null && timeStringToTime(ref.textContent) || undefined,
  3159. url: response.finalUrl,
  3160. description: description,
  3161. identifiers: mergeIds(),
  3162. cover_url: imgUrl,
  3163. }));
  3164. });
  3165. else if (url.toLowerCase().includes('prestomusic.com/')) return globalFetch(url).then(function(response) {
  3166. artist = getArtists(response.document.querySelectorAll('div.c-product-block__contributors > p'));
  3167. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3168. if (isVA) artist = [];
  3169. ref = response.document.querySelector('h1.c-product-block__title');
  3170. if (ref != null) album = ref.lastChild.textContent.trim();
  3171. response.document.querySelectorAll('div.c-product-block__metadata > ul > li').forEach(function(li) {
  3172. if (li.firstChild.textContent.includes('Release Date')) {
  3173. releaseDate = extractYear(li.lastChild.textContent);
  3174. } else if (li.firstChild.textContent.includes('Label')) {
  3175. label = li.lastChild.textContent.trim();
  3176. } else if (li.firstChild.textContent.includes('Catalogue No')) {
  3177. catalogue = li.lastChild.textContent.trim();
  3178. }
  3179. });
  3180. composer = [];
  3181. response.document.querySelectorAll('div#related > div > ul > li').forEach(function(li) {
  3182. if (li.parentNode.previousElementSibling.textContent.includes('Composers')) {
  3183. composer.push(li.firstChild.textContent.trim().replace(/^(.*?)\s*,\s+(.*)$/, '$2 $1'));
  3184. }
  3185. });
  3186. composer = composer.join(', ') || undefined;
  3187. genres = undefined;
  3188. if (/\/jazz\//i.test(response.finalUrl)) genres = 'Jazz';
  3189. if (/\/classical\//i.test(response.finalUrl)) genres = 'Classical';
  3190. getDescription(response, 'div#about > div > p', true);
  3191. if ((ref = response.document.querySelector('div.c-product-block__aside > a')) != null)
  3192. imgUrl = ref.href.replace(/\?\d+$/, '');
  3193. trs = response.document.querySelectorAll('div.has--sample');
  3194. trackNumber = 0;
  3195. return Array.from(trs).map(function(tr) {
  3196. discNumber = discSubtitle = undefined;
  3197. var parent = tr;
  3198. if (tr.classList.contains('c-track')) {
  3199. parent = tr.parentNode.parentNode;
  3200. if (parent.classList.contains('c-expander')) parent = parent.parentNode;
  3201. if ((ref = parent.querySelector(':scope > div > div > div > p.c-track__title')) != null) {
  3202. discSubtitle = ref.textContent.trim().replace(/\s+/g, ' ');
  3203. guessDiscNumber();
  3204. }
  3205. }
  3206. trackArtist = getArtists(parent.querySelectorAll(':scope > div.c-track__details > ul > li'));
  3207. if (trackArtist.equalCaselessTo(artist)) trackArtist = [];
  3208. return {
  3209. artist: isVA ? VA : artist.join('; '),
  3210. album: album,
  3211. //album_year: extractYear(releaseDate),
  3212. release_date: releaseDate,
  3213. label: label,
  3214. catalog: catalogue,
  3215. media: 'WEB',
  3216. genre: genres,
  3217. discnumber: discNumber,
  3218. totaldiscs: totalDiscs,
  3219. discsubtitle: discSubtitle,
  3220. tracknumber: ++trackNumber,
  3221. totaltracks: trs.length,
  3222. title: (ref = tr.querySelector('p.c-track__title')) ? ref.textContent.trim().replace(/\s+/g, ' ') : undefined,
  3223. track_artist: joinArtists(trackArtist),
  3224. composer: composer,
  3225. duration: timeStringToTime(tr.querySelector('div.c-track__duration').textContent),
  3226. url: response.finalUrl,
  3227. description: description,
  3228. identifiers: mergeIds(),
  3229. };
  3230. });
  3231.  
  3232. function getArtists(nodeList) {
  3233. var artists = [];
  3234. nodeList.forEach(function(_artists) {
  3235. _artists = _artists.textContent.trim();
  3236. if (_artists.startsWith('Record')) return;
  3237. splitArtists(_artists).forEach(artist => { artists.push(artist.replace(/\s*\([^\(\)]*\)$/, '')) });
  3238. });
  3239. return artists.filter(artist => artist.length > 0);
  3240. }
  3241. });
  3242. else if (url.toLowerCase().includes('discogs.com/') && /\/releases?\/(\d+)\b/i.test(url)) {
  3243. return queryDiscogsAPI('releases/' + RegExp.$1).then(function(release) {
  3244. const removeArtistNdx = /\s*\(\d+\)$/;
  3245. const editionTest = /^(?:.+?\s+Edition|Remaster(?:ed)|Remasterizado|Remasterisée|Reissue|.+?\s+Release|Enhanced|Promo)$/;
  3246. media = undefined;
  3247. identifiers.DISCOGS_ID = release.id;
  3248. var master = release.master_url ? globalFetch(release.master_url, { responseType: 'json' })
  3249. .then(response => response.response) : Promise.reject('master release not available');
  3250. var albumArtists = getArtists(release);
  3251. if (albumArtists[0].length > 0) {
  3252. artist = albumArtists[0].join('; ');
  3253. if (albumArtists[1].length > 0) artist += ' feat. ' + albumArtists[1].join('; ');
  3254. }
  3255. album = release.title;
  3256. var editions = [];
  3257. label = []; catalogue = [];
  3258. release.labels.forEach(function(it) {
  3259. //if (it.entity_type_name != 'Label') return;
  3260. //if (!/^Not On Label\b/i.test(it.name)) label.pushUniqueCaseless(it.name.replace(removeArtistNdx, ''));
  3261. catalogue.pushUniqueCaseless(it.catno);
  3262. });
  3263. description = '';
  3264. if (release.companies && release.companies.length > 0) {
  3265. description = '[b]Companies, etc.[/b]\n';
  3266. let type_names = new Set(release.companies.map(it => it.entity_type_name));
  3267. type_names.forEach(function(type_name) {
  3268. description += '\n' + type_name + ' – ' + release.companies
  3269. .filter(it => it.entity_type_name == type_name)
  3270. .map(function(it) {
  3271. var result = '[url=' + discogsOrigin + '/label/' + it.id + ']' +
  3272. it.name.replace(removeArtistNdx, '') + '[/url]';
  3273. if (it.catno) result += ' – ' + it.catno;
  3274. return result;
  3275. })
  3276. .join(', ');
  3277. });
  3278. }
  3279. if (release.extraartists && release.extraartists.length > 0) {
  3280. if (description) description += '\n\n';
  3281. description += '[b]Credits[/b]\n';
  3282. let roles = new Set(release.extraartists.map(it => it.role));
  3283. roles.forEach(function(role) {
  3284. description += '\n' + role + ' – ' + release.extraartists
  3285. .filter(artist => artist.role == role)
  3286. .map(function(artist) {
  3287. var result = '[url=' + discogsOrigin + '/artist/' + artist.id + ']' +
  3288. (artist.anv || artist.name).replace(removeArtistNdx, '') + '[/url]';
  3289. if (artist.tracks) result += ' (tracks: ' + artist.tracks + ')';
  3290. return result;
  3291. })
  3292. .join(', ');
  3293. });
  3294. }
  3295. if (release.notes) {
  3296. if (description) description += '\n\n';
  3297. description += '[b]Notes[/b]\n\n' + release.notes.trim();
  3298. }
  3299. if (Array.isArray(release.identifiers) && release.identifiers.length > 0) {
  3300. if (description) description += '\n\n';
  3301. description += '[b]Barcode and Other Identifiers[/b]\n';
  3302. release.identifiers.forEach(function(it) {
  3303. description += '\n' + it.type;
  3304. if (it.description) description += ' (' + it.description + ')';
  3305. description += ': ' + it.value;
  3306. });
  3307. }
  3308. [
  3309. ['Single', 'Single'],
  3310. ['EP', 'EP'],
  3311. ['Compilation', 'Compilation'],
  3312. ['Soundtrack', 'Soundtrack'],
  3313. ].forEach(function(k) {
  3314. if (release.formats.every(it => Array.isArray(it.descriptions) && it.descriptions.includesCaseless(k[0]))) {
  3315. identifiers.RELEASETYPE = k[1];
  3316. }
  3317. });
  3318. release.identifiers.forEach(function(id) {
  3319. identifiers[id.type.toUpperCase().replace(/\s*\/\s*/g, '-').replace(/\W/g, '_')] = id.value;
  3320. });
  3321. release.formats.forEach(function(fmt) {
  3322. if (editionTest.test(fmt.text)) editions.push(fmt.text);
  3323. if (Array.isArray(fmt.descriptions)) fmt.descriptions.forEach(function(desc) {
  3324. if (editionTest.test(desc)) editions.push(desc);
  3325. });
  3326. if (media) return;
  3327. if (/\bFile\b/.test(fmt.name)) {
  3328. media = 'WEB';
  3329. if (['FLAC', 'WAV', 'AIF', 'AIFF', 'AIFC', 'PCM', 'ALAC', 'APE', 'WavPack']
  3330. .some(k => fmt.descriptions.includes(k))) {
  3331. encoding = 'lossless'; format = 'FLAC';
  3332. } else if (fmt.descriptions.includes('AAC')) {
  3333. encoding = 'lossy'; format = 'AAC'; bd = undefined;
  3334. if (/(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
  3335. } else if (fmt.descriptions.includes('MP3')) {
  3336. encoding = 'lossy'; format = 'MP3'; bd = undefined;
  3337. if (/\b(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
  3338. } else if (['DFF', 'DSD'].some(k => fmt.descriptions.includes(k))) {
  3339. encoding = 'lossless';
  3340. } else if (['AMR', 'MP2', 'ogg-vorbis', 'Opus', 'SHN', 'WMA'].some(k => fmt.descriptions.includes(k))) {
  3341. encoding = 'lossy';
  3342. }
  3343. } else media = dcFmtToGazelle(fmt.name) || undefined;
  3344. });
  3345. if (editions.length > 0) album += ' (' + editions.join(' / ') + ')';
  3346. if (Array.isArray(release.images) && release.images[0] && release.images[0].resource_url/*uri*/) {
  3347. imgUrl = release.images[0].resource_url/*uri*/;
  3348. }
  3349. totalTracks = release.tracklist.filter(track => track.type_.toLowerCase() == 'track').length;
  3350. return master.then(enumTracks, function(e) {
  3351. if (prefs.messages_verbosity >= 1) addMessage(e, 'notice');
  3352. return enumTracks({});
  3353. });
  3354.  
  3355. function getArtists(root) {
  3356. function filterArtists(rx, anv = true) {
  3357. return Array.isArray(root.extraartists) && rx instanceof RegExp ?
  3358. root.extraartists.filter(it => rx.test(it.role))
  3359. .map(it => (anv && it.anv || it.name || '').replace(removeArtistNdx, '')) : [];
  3360. }
  3361. var artists = [];
  3362. for (var ndx = 0; ndx < 7; ++ndx) artists[ndx] = [];
  3363. ndx = 0;
  3364. if (root.artists) root.artists.forEach(function(it) {
  3365. artists[ndx].push((it.anv || it.name).replace(removeArtistNdx, ''));
  3366. if (/^feat/i.test(it.join)) ndx = 1;
  3367. });
  3368. return [
  3369. artists[0],
  3370. artists[1].concat(filterArtists(/^(?:featuring)$/i)),
  3371. artists[2].concat(filterArtists(/\b(?:Remixed[\s\-]By|Remixer)\b/i)),
  3372. artists[3].concat(filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, false)),
  3373. artists[4].concat(filterArtists(/\b(?:Conducted[\s\-]By|Conductor)\b/i)),
  3374. artists[5].concat(filterArtists(/\b(?:Compiled[\s\-]By|Compiler)\b/i)),
  3375. artists[6].concat(filterArtists(/\b(?:Produced[\s\-]By|Producer)\b/i)),
  3376. // filter off from performers
  3377. filterArtists(/\b(?:(?:Mixed)[\s\-]By|Mixer)\b/i),
  3378. filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, true),
  3379. ];
  3380. }
  3381.  
  3382. function enumTracks(master) {
  3383. var tags = new TagManager();
  3384. if (release.genres) tags.add(...release.genres);
  3385. if (release.styles) tags.add(...release.styles);
  3386. if (master.genres) tags.add(...master.genres);
  3387. if (master.styles) tags.add(...master.styles);
  3388. release.tracklist.forEach(function(track) {
  3389. switch (track.type_.toLowerCase()) {
  3390. case 'heading':
  3391. discSubtitle = track.title;
  3392. break;
  3393. case 'track': {
  3394. trackIdentifiers = {};
  3395. if (/^([a-zA-Z]+)?(\d+)-(\w+)$/.test(track.position)) {
  3396. if (RegExp.$1) trackIdentifiers.VOL_MEDIA = RegExp.$1;
  3397. discNumber = RegExp.$2;
  3398. trackNumber = RegExp.$3;
  3399. } else {
  3400. discNumber = undefined;
  3401. trackNumber = track.position;
  3402. }
  3403. let trackArtists = getArtists(track);
  3404. if (trackArtists[0].length > 0 && !trackArtists[0].equalCaselessTo(albumArtists[0])
  3405. || trackArtists[1].length > 0 && !trackArtists[1].equalCaselessTo(albumArtists[1])) {
  3406. trackArtist = (trackArtists[0].length > 0 ? trackArtists : albumArtists)[0].join('; ');
  3407. if (trackArtists[1].length > 0) trackArtist += ' feat. ' + trackArtists[1].join('; ');
  3408. } else trackArtist = null;
  3409. let performer = Array.isArray(track.extraartists) && track.extraartists
  3410. .map(artist => (artist.anv || artist.name).replace(removeArtistNdx, ''))
  3411. .filter(function(artist) {
  3412. return !albumArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
  3413. && !trackArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
  3414. });
  3415. tracks.push({
  3416. artist: artist,
  3417. album: album,
  3418. album_year: master.year,
  3419. release_date: release.released,
  3420. label: label.join(' / '),
  3421. catalog: catalogue.join(' / '),
  3422. country: release.country,
  3423. encoding: encoding,
  3424. codec: format,
  3425. bitrate: bitrate,
  3426. bd: bd,
  3427. media: media,
  3428. genre: tags.toString(),
  3429. discnumber: discNumber,
  3430. totaldiscs: release.format_quantity,
  3431. discsubtitle: discSubtitle,
  3432. tracknumber: trackNumber,
  3433. totaltracks: totalTracks,
  3434. title: track.title,
  3435. track_artist: trackArtist,
  3436. performer: Array.isArray(performer) && performer.join('; ') || undefined,
  3437. composer: stringyfyRole(3),
  3438. conductor: stringyfyRole(4),
  3439. remixer: stringyfyRole(2),
  3440. compiler: stringyfyRole(5),
  3441. producer: stringyfyRole(6),
  3442. duration: timeStringToTime(track.duration),
  3443. description: description,
  3444. identifiers: mergeIds(),
  3445. cover_url: imgUrl,
  3446. });
  3447.  
  3448. function stringyfyRole(ndx) {
  3449. return (Array.isArray(trackArtists[ndx]) && trackArtists[ndx].length > 0 ?
  3450. trackArtists : albumArtists)[ndx].join('; ');
  3451. }
  3452. }
  3453. }
  3454. });
  3455. return tracks;
  3456. }
  3457. });
  3458. } else if (url.toLowerCase().includes('supraphonline.cz/')) return globalFetch(url.replace(/\?.*$/, '')).then(function(response) {
  3459. const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
  3460. var ndx, conductor = [], origin = new URL(response.finalUrl).origin;
  3461. genres = undefined; artist = [];
  3462. response.document.querySelectorAll('h2.album-artist > a').forEach(function(it) {
  3463. artist.pushUnique(it.title);
  3464. });
  3465. if (artist.length == 0 && (ref = response.document.querySelector('h2.album-artist[title]')) != null) {
  3466. isVA = vaParser.test(ref.title);
  3467. }
  3468. ref = response.document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]');
  3469. if (ref != null && vaParser.test(ref.content)) isVA = true;
  3470. if (isVA) artist = [];
  3471. if ((ref = response.document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
  3472. if ((ref = response.document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  3473. if ((ref = response.document.querySelector('meta[itemprop="genre"]')) != null) genres = ref.content;
  3474. if ((ref = response.document.querySelector('li.album-version > div.selected > div')) != null) {
  3475. if (/\b(?:MP3)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossy'; format = 'MP3'; }
  3476. if (/\b(?:FLAC)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 16; }
  3477. if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 24; }
  3478. if (/\b(?:CD)\b/.test(ref.textContent)) { media = 'CD'; }
  3479. if (/\b(?:LP)\b/.test(ref.textContent)) { media = 'Vinyl'; }
  3480. }
  3481. response.document.querySelectorAll('ul.summary > li').forEach(function(it) {
  3482. if (it.childElementCount <= 0) return;
  3483. if (it.firstElementChild.textContent.includes('Nosič')) media = it.lastChild.textContent.trim();
  3484. if (it.firstElementChild.textContent.includes('Datum vydání')) releaseDate = normalizeDate(it.lastChild.textContent);
  3485. if (it.firstElementChild.textContent.includes('První vydání')) albumYear = extractYear(it.lastChild.data);
  3486. //if (it.firstElementChild.textContent.includes('Žánr')) genre = it.lastChild.textContent.trim();
  3487. if (it.firstElementChild.textContent.includes('Vydavatel')) label = it.lastChild.textContent.trim();
  3488. if (it.firstElementChild.textContent.includes('Katalogové číslo')) catalogue = it.lastChild.textContent.trim();
  3489. if (it.firstElementChild.textContent.includes('Formát')) {
  3490. if (/\b(?:FLAC|WAV|AIFF?)\b/.test(it.lastChild.textContent)) { encoding = 'lossless'; format = 'FLAC'; }
  3491. if (/\b(\d+)[\-\s]?bits?\b/i.test(it.lastChild.textContent)) bd = parseInt(RegExp.$1);
  3492. if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(it.lastChild.textContent)) sr = parseFloat(RegExp.$1.replace(',', '.'));
  3493. }
  3494. if (it.firstElementChild.textContent.includes('Celková stopáž')) totalTime = timeStringToTime(it.lastChild.textContent.trim());
  3495. if (copyrightParser.test(it.firstElementChild.textContent) && !albumYear) albumYear = extractYear(it.lastChild.data);
  3496. });
  3497. const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
  3498. artists = [];
  3499. for (i = 0; i < 4; ++i) artists[i] = {};
  3500. response.document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
  3501. if ((ref = it.querySelector('h3')) != null) {
  3502. ndx = undefined;
  3503. creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
  3504. } else {
  3505. if (typeof ndx != 'number') return;
  3506. let role;
  3507. if (ndx == 2) role = 'ensemble';
  3508. else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
  3509. if ((ref = it.querySelector('a')) != null) {
  3510. if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
  3511. var href = new URL(ref.href);
  3512. artists[ndx][role].pushUnique([ref.textContent.trim(), origin + href.pathname]);
  3513. }
  3514. }
  3515. });
  3516. getDescription(response, 'div[itemprop="description"] p', true);
  3517. composer = [];
  3518. var performers = [], DJs = [];
  3519. function dumpArtist(ndx, role) {
  3520. if (!role || role == 'undefined') return;
  3521. if (description.length > 0) description += '\n' ;
  3522. description += '[color=#9576b1]' + role + '[/color] – ';
  3523. //description += artists[ndx][role].map(artist => '[artist]' + artist[0] + '[/artist]').join(', ');
  3524. description += artists[ndx][role].map(artist => '[url=' + artist[1] + ']' + artist[0] + '[/url]').join(', ');
  3525. }
  3526. for (i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
  3527. var a = artists[i][role].map(a => a[0]);
  3528. artist.pushUnique(...a);
  3529. (['conductor', 'choirmaster'].includes(role) ? conductor : role == 'DJ' ? DJs : performers).pushUnique(...a);
  3530. if (i != 2) dumpArtist(i, role);
  3531. });
  3532. Object.keys(artists[0]).forEach(function(role) { // composers
  3533. composer.pushUnique(...artists[0][role].map(it => it[0])
  3534. .filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
  3535. dumpArtist(0, role);
  3536. });
  3537. Object.keys(artists[3]).forEach(role => { dumpArtist(3, role) }); // ADC & mastering
  3538. ref = response.document.querySelector('meta[itemprop="image"]');
  3539. if (ref != null)imgUrl = ref.content.replace(/\?.*$/, '');
  3540. var promises = [];
  3541. response.document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(row) {
  3542. promises.push(row.id && (ref = row.querySelector('td > a.trackdetail')) != null ?
  3543. globalFetch(origin + ref.pathname + ref.search, { context: parseInt(row.id.replace(/^track-/i, '')) })
  3544. .then(function(response) {
  3545. var track = response.document.getElementById('track-' + response.context);
  3546. if (track == null) return Promise.reject('Track detail not located');
  3547. return [track, response.document.querySelector('div[data-swap="trackdetail-' +
  3548. response.context + '"] > div > div.row')];
  3549. })
  3550. : Promise.resolve([row, null]));
  3551. });
  3552. return Promise.all(promises).then(function(rows) {
  3553. rows.forEach(function(tr) {
  3554. if (!(tr[0] instanceof HTMLElement)) throw new Error('Assertion failed: tr[0] != HTMLElement');
  3555. if (tr[0].id && tr[0].classList.contains('track')) {
  3556. tr[2] = [];
  3557. for (i = 0; i < 8; ++i) tr[2][i] = [];
  3558. if (!(tr[1] instanceof HTMLElement)) return;
  3559. tr[1].querySelectorAll('div[class]:nth-of-type(2) > ul > li > span').forEach(function(li) {
  3560. function oneOf(...arr) { return arr.some(role => key == role) }
  3561. var key = translateRole(li);
  3562. var val = li.nextElementSibling.textContent.trim();
  3563. if (pseudoArtistParsers.some(rx => rx.test(val))) return;
  3564. if (key.startsWith('remix')) {
  3565. tr[2][2].pushUnique(val);
  3566. } else if (oneOf('music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto', 'music improvisation', 'author')) {
  3567. tr[2][3].pushUnique(val);
  3568. } else if (oneOf('conductor', 'choirmaster')) {
  3569. tr[2][4].pushUnique(val);
  3570. } else if (key == 'DJ') {
  3571. tr[2][5].pushUnique(val);
  3572. } else if (key == 'produced by') {
  3573. tr[2][6].pushUnique(val);
  3574. } else if (key == 'recorded by') {
  3575. } else {
  3576. tr[2][7].pushUnique(val);
  3577. }
  3578. });
  3579. }
  3580. });
  3581. var guests = rows.filter(tr => tr.length >= 3).map(it => it[2][7])
  3582. .reduce((acc, trpf) => trpf.filter(trpf => acc.includes(trpf)))
  3583. .filter(it => !artist.includes(it));
  3584. rows.forEach(function(tr) {
  3585. if (tr[0].classList.contains('cd-header')) {
  3586. discNumber = /\b\d+\b/.test(tr[0].querySelector('h3').firstChild.data.trim())
  3587. && parseInt(RegExp.lastMatch) || undefined;
  3588. }
  3589. if (tr[0].classList.contains('song-header')) discSubtitle = tr[0].firstElementChild.title.trim() || undefined;
  3590. if (tr[0].id && tr[0].classList.contains('track')) {
  3591. var copyright, trackGenre, trackYear, recordPlace, recordDate, trackIdentifiers = {};
  3592. if (/^track-(\d+)$/i.test(tr[0].id)) trackIdentifiers.TRACK_ID = RegExp.$1;
  3593. if (tr[1] instanceof HTMLElement) {
  3594. tr[1].querySelectorAll('div[class]:nth-of-type(1) > ul > li > span').forEach(function(li) {
  3595. if (li.textContent.startsWith('Nahrávka dokončena')) {
  3596. trackIdentifiers.RECYEAR = extractYear(recordDate = li.nextSibling.data.trim());
  3597. }
  3598. if (li.textContent.startsWith('Místo nahrání')) {
  3599. recordPlace = li.nextSibling.data.trim();
  3600. }
  3601. if (li.textContent.startsWith('Rok prvního vydání')) {
  3602. trackIdentifiers.PUBYEAR = (trackYear = parseInt(li.nextSibling.data));
  3603. }
  3604. //if (copyrightParser.test(li.textContent)) copyright = li.nextSibling.data.trim();
  3605. if (li.textContent.startsWith('Žánr')) trackGenre = li.nextSibling.data.trim();
  3606. });
  3607. }
  3608. if (!isVA && tr[2][0].equalCaselessTo(artist)) tr[2][0] = [];
  3609. tracks.push({
  3610. artist: isVA ? VA : artist.join('; '),
  3611. album: album,
  3612. album_year: /*trackYear || */albumYear || undefined,
  3613. release_date: releaseDate,
  3614. label: label,
  3615. catalog: catalogue,
  3616. encoding: encoding,
  3617. codec: format,
  3618. bd: bd,
  3619. sr: sr * 1000,
  3620. media: media,
  3621. genre: translateGenre(genres) + ' | ' + translateGenre(trackGenre),
  3622. discnumber: discNumber,
  3623. totaldiscs: totalDiscs,
  3624. discsubtitle: discSubtitle,
  3625. tracknumber: /^\s*(\d+)\.?\s*$/.test(tr[0].firstElementChild.firstChild.textContent) ?
  3626. parseInt(RegExp.$1) : undefined,
  3627. totaltracks: totalTracks,
  3628. title: tr[0].querySelector('meta[itemprop="name"]').content,
  3629. track_artist: joinArtists(tr[2][0]),
  3630. performer: tr[2][7].join('; ') || performers.join('; '),
  3631. composer: tr[2][3].join(', ') || composer.join(', '),
  3632. conductor: tr[2][4].join('; ') || conductor.join('; '),
  3633. remixer: tr[2][2].join('; '),
  3634. compiler: tr[2][5].join('; ') || DJs.join('; '),
  3635. producer: tr[2][6].join('; '),
  3636. duration: durationFromMeta(tr[0]),
  3637. url: response.finalUrl,
  3638. description: description,
  3639. identifiers: mergeIds(),
  3640. cover_url: imgUrl,
  3641. });
  3642. }
  3643. });
  3644. return tracks;
  3645. });
  3646.  
  3647. function translateGenre(genre) {
  3648. if (!genre || typeof genre != 'string') return undefined;
  3649. [
  3650. ['Orchestrální hudba', 'Orchestral Music'],
  3651. ['Komorní hudba', 'Chamber Music'],
  3652. ['Vokální', 'Classical, Vocal'],
  3653. ['Klasická hudba', 'Classical'],
  3654. ['Melodram', 'Classical, Melodram'],
  3655. ['Symfonie', 'Symphony'],
  3656. ['Vánoční hudba', 'Christmas Music'],
  3657. [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
  3658. ['Dechová hudba', 'Brass Music'],
  3659. ['Elektronika', 'Electronic'],
  3660. ['Folklor', 'Folclore, World Music'],
  3661. ['Instrumentální hudba', 'Instrumental'],
  3662. ['Latinské rytmy', 'Latin'],
  3663. ['Meditační hudba', 'Meditative'],
  3664. ['Vojenská hudba', 'Military Music'],
  3665. ['Pro děti', 'Children'],
  3666. ['Pro dospělé', 'Adult'],
  3667. ['Mluvené slovo', 'Spoken Word'],
  3668. ['Audiokniha', 'audiobook'],
  3669. ['Humor', 'humour'],
  3670. ['Pohádka', 'Fairy-Tale'],
  3671. ].forEach(function(subst) {
  3672. if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
  3673. || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
  3674. });
  3675. return genre;
  3676. }
  3677. function translateRole(elem) {
  3678. if (!(elem instanceof HTMLElement)) return undefined;
  3679. var role = elem.textContent.trim().toLowerCase().replace(/\s*:.*$/, '');
  3680. [
  3681. [/\b(?:klavír)\b/, 'piano'],
  3682. [/\b(?:housle)\b/, 'violin'],
  3683. [/\b(?:varhany)\b/, 'organ'],
  3684. [/\b(?:cembalo)\b/, 'harpsichord'],
  3685. [/\b(?:trubka)\b/, 'trumpet'],
  3686. [/\b(?:soprán)\b/, 'soprano'],
  3687. [/\b(?:alt)\b/, 'alto'],
  3688. [/\b(?:baryton)\b/, 'baritone'],
  3689. [/\b(?:bas)\b/, 'basso'],
  3690. [/\b(?:syntezátor)\b/, 'synthesizer'],
  3691. [/\b(?:zpěv)\b/, 'vocals'],
  3692. [/^(?:čte|četba)$/, 'narration'],
  3693. ['vypravuje', 'narration'],
  3694. ['komentář', 'commentary'],
  3695. ['hovoří', 'spoken by'],
  3696. ['hovoří a zpívá', 'speaks and sings'],
  3697. ['improvizace', 'improvisation'],
  3698. ['hudební těleso', 'ensemble'],
  3699. ['hudba', 'music'],
  3700. ['text', 'lyrics'],
  3701. ['hudba+text', 'music+lyrics'],
  3702. ['původní text', 'original lyrics'],
  3703. ['český text', 'czech lyrics'],
  3704. ['hudební improvizace', 'music improvisation'],
  3705. ['autor', 'author'],
  3706. ['účinkuje', 'participating'],
  3707. ['řídí', 'conductor'],
  3708. ['dirigent', 'conductor'],
  3709. ['sbormistr', 'choirmaster'],
  3710. ['produkce', 'produced by'],
  3711. ['nahrál', 'recorded by'],
  3712. ['digitální přepis', 'A/D transfer'],
  3713. ].forEach(function(subst) {
  3714. if (typeof subst[0] == 'string' && role.toLowerCase() == subst[0].toLowerCase()
  3715. || subst[0] instanceof RegExp && subst[0].test(role)) role = role.replace(subst[0], subst[1]);
  3716. });
  3717. return role;
  3718. }
  3719. });
  3720. else if (url.toLowerCase().includes('bontonland.cz/')) return globalFetch(url).then(function(response) {
  3721. ref = response.document.querySelector('div#detailheader > h1');
  3722. if (ref != null && /^(.*?)\s*:\s*(.*)$/.test(ref.textContent.trim())) {
  3723. artist = RegExp.$1;
  3724. isVA = vaParser.test(artist);
  3725. album = RegExp.$2;
  3726. }
  3727. media = 'CD';
  3728. response.document.querySelectorAll('table > tbody > tr > td.nazevparametru').forEach(function(it) {
  3729. if (it.textContent.includes('Datum vydání')) {
  3730. releaseDate = normalizeDate(it.nextElementSibling.textContent);
  3731. albumYear = extractYear(it.nextElementSibling.textContent);
  3732. } else if (it.textContent.includes('Nosič / počet')) {
  3733. if (/^(.*?)\s*\/\s*(.*)$/.test(it.nextElementSibling.textContent)) {
  3734. media = RegExp.$1;
  3735. totalDiscs = RegExp.$2;
  3736. }
  3737. } else if (it.textContent.includes('Interpret')) {
  3738. artist = it.nextElementSibling.textContent.trim();
  3739. } else if (it.textContent.includes('EAN')) {
  3740. identifiers.BARCODE = it.nextElementSibling.textContent.trim();
  3741. }
  3742. });
  3743. getDescription(response, 'div#detailtabpopis > div[class^="pravy"] > div > p:not(:last-of-type)', true);
  3744. if ((ref = response.document.querySelector('a.detailzoom')) != null) imgUrl = ref.href;
  3745. const plParser = /^(\d+)(?:\s*[\/\.\-\:\)])?\s+(.*?)(?:\s+((?:(?:\d+:)?\d+:)?\d+))?$/;
  3746. ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > p:last-of-type');
  3747. if (ref == null) throw new Error('Playlist not located');
  3748. var trackList = html2php(ref, response.finalUrl).trim().split(/[\r\n]+/);
  3749. trackList = trackList.filter(it => plParser.test(it.trim())).map(it => plParser.exec(it.trim()));
  3750. return Array.from(trackList).map(track => ({
  3751. artist: isVA ? VA : artist,
  3752. album: album,
  3753. //album_year: extractYear(releaseDate),
  3754. release_date: releaseDate,
  3755. label: label,
  3756. media: media,
  3757. tracknumber: track[1],
  3758. totaltracks: trackList.length,
  3759. title: track[2],
  3760. duration: timeStringToTime(track[3]),
  3761. url: response.finalUrl,
  3762. description: description,
  3763. identifiers: mergeIds(),
  3764. cover_url: imgUrl,
  3765. }));
  3766. });
  3767. else if (url.toLowerCase().includes('nativedsd.com/')) return globalFetch(url).then(function(response) {
  3768. if ((ref = response.document.querySelector('div.the-content > header > h2')) != null)
  3769. artist = ref.firstChild.data.trim();
  3770. isVA = vaParser.test(artist);
  3771. if ((ref = response.document.querySelector('div.the-content > header > h1')) != null)
  3772. album = ref.firstChild.data.trim();
  3773. if ((ref = response.document.querySelector('div.the-content > header > h3')) != null)
  3774. composer = ref.firstChild.data.trim();
  3775. if ((ref = response.document.querySelector('div.the-content > header > h1 > small')) != null)
  3776. albumYear = extractYear(ref.firstChild.data);
  3777. releaseDate = albumYear; // weak
  3778. ref = response.document.querySelector('div#breadcrumbs > div[class] > a:nth-of-type(2)');
  3779. if (ref != null) label = ref.firstChild.data.trim();
  3780. if (label == 'Albums') label = undefined;
  3781. if ((ref = response.document.querySelector('h2#sku')) != null) {
  3782. if (/^Catalog Number: (.*)$/m.test(ref.firstChild.textContent)) catalogue = RegExp.$1;
  3783. if (/^ID: (.*)$/m.test(ref.lastChild.textContent)) identifiers.NATIVEDSD_ID = RegExp.$1;
  3784. }
  3785. identifiers.ORIGINALFORMAT = 'DSD';
  3786. getDescription(response, 'div.the-content > div.entry > p', false);
  3787. if ((ref = response.document.querySelector('div#repertoire > div > p')) != null) {
  3788. let repertoire = html2php(ref, url);
  3789. if (description) description += '\n\n';
  3790. let ndx = repertoire.indexOf('\n[b]Track');
  3791. description += (ndx >= 0 ? repertoire.slice(0, ndx) : repertoire).trim().flatten();
  3792. }
  3793. ref = response.document.querySelectorAll('div#techspecs > table > tbody > tr');
  3794. if (ref.length > 0) {
  3795. if (description) description += '\n\n';
  3796. description += '[b][u]Tech specs[/u][/b]';
  3797. ref.forEach(function(it) {
  3798. description += '\n[b]'.concat(it.children[0].textContent.trim(), '[/b] ', it.children[1].textContent.trim());
  3799. });
  3800. }
  3801. if ((ref = response.document.querySelector('a#album-cover')) != null) imgUrl = ref.href;
  3802. trs = response.document.querySelectorAll('div#track-list > table > tbody > tr[id^="track"]');
  3803. return Array.from(trs).map(function(tr) {
  3804. title = undefined;
  3805. trackIdentifiers = { TRACK_ID: tr.id.replace(/^track-/i, '') };
  3806. var trackComposer;
  3807. if ((ref = tr.children[1]) != null) {
  3808. title = ref.firstChild.textContent.trim();
  3809. trackComposer = ref.childNodes[2] && ref.childNodes[2].textContent.trim() || undefined;
  3810. }
  3811. return {
  3812. artist: isVA ? VA : artist,
  3813. album: album,
  3814. album_year: albumYear,
  3815. release_date: releaseDate,
  3816. label: label,
  3817. catalog: catalogue,
  3818. encoding: 'lossless', // encoding
  3819. codec: 'FLAC', // format
  3820. bd: 24,
  3821. sr: 88200,
  3822. media: media,
  3823. genre: genres.join('; '), // 'Jazz'
  3824. discnumber: discNumber,
  3825. totaldiscs: totalDiscs,
  3826. discsubtitle: discSubtitle,
  3827. tracknumber: (ref = tr.firstElementChild.firstElementChild) != null ?
  3828. parseInt(ref.firstChild.data.trim().replace(/\..*$/, '')) : undefined,
  3829. totaltracks: trs.length,
  3830. title: title,
  3831. composer: trackComposer || composer,
  3832. duration: (ref = tr.children[2]) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  3833. url: response.finalUrl,
  3834. description: description,
  3835. identifiers: mergeIds(),
  3836. cover_url: imgUrl,
  3837. };
  3838. });
  3839. });
  3840. else if (url.toLowerCase().includes('junodownload.com/')) return globalFetch(url).then(function(response) {
  3841. if (/\/([\d\-]+)\/?$/.test(response.finalUrl)) identifiers.JUNODOWNLOAD_ID = RegExp.$1;
  3842. var productArtist;
  3843. if ((ref = response.document.querySelectorAll('div.breadcrumb_text > span:not([class])')).length == 4) {
  3844. artist = Array.from(ref[ref.length - 1].querySelectorAll('a')).map(elem => elem.textContent.trim());
  3845. productArtist = ref[ref.length - 1].textContent.trim();
  3846. } else if ((ref = response.document.querySelector('h2.product-artist')) != null) {
  3847. artist = Array.from(ref.querySelectorAll('a')).map(elem => elem.textContent.trim().titleCase());
  3848. productArtist = ref.textContent.trim().titleCase();
  3849. }
  3850. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3851. if (isVA) artist = [];
  3852. if ((ref = response.document.querySelector('meta[itemprop="name"]')) != null) album = ref.content.trim();
  3853. if ((ref = response.document.querySelector('meta[itemprop="author"]')) != null) label = ref.content.trim();
  3854. if ((ref = response.document.querySelector('span[itemprop="datePublished"]')) != null)
  3855. releaseDate = ref.firstChild.data.trim();
  3856. response.document.querySelectorAll('div.mb-3 > strong').forEach(function(it) {
  3857. if (it.textContent.startsWith('Genre')) {
  3858. ref = it;
  3859. while ((ref = ref.nextElementSibling) != null && ref.nodeName == 'A') genres.push(ref.textContent.trim());
  3860. } else if (it.textContent.startsWith('Cat')) {
  3861. if ((ref = it.nextSibling) != null && ref.nodeType == Node.TEXT_NODE) catalogue = ref.wholeText;
  3862. }
  3863. });
  3864. getDescription(response, 'div[itemprop="review"]');
  3865. if ((ref = response.document.querySelector('meta[property="og:image"]')) != null) imgUrl = ref.content;
  3866. trs = response.document.querySelectorAll('div.product-tracklist > div[itemprop="track"]');
  3867. return Array.from(trs).map(function(tr) {
  3868. trackIdentifiers = { BPM: tr.children[2].textContent.trim() };
  3869. trackNumber = undefined;
  3870. tr.querySelector('div.track-title').childNodes.forEach(function(n) {
  3871. if (trackNumber || n.nodeType != Node.TEXT_NODE) return;
  3872. trackNumber = n.data.trim().replace(/\s*\..*$/, '');
  3873. });
  3874. trackArtist = (ref = tr.querySelector('meta[itemprop="byArtist"]')) != null ? ref.content : undefined;
  3875. title = (ref = tr.querySelector('span[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  3876. if (title && trackArtist && title.startsWith(trackArtist + ' - ')) title = title.slice(trackArtist.length + 3);
  3877. if (trackArtist && trackArtist == productArtist) trackArtist = undefined;
  3878. return {
  3879. artist: isVA ? VA : artist.join('; '),
  3880. album: album,
  3881. album_year: extractYear(releaseDate),
  3882. release_date: releaseDate,
  3883. label: label,
  3884. catalog: catalogue,
  3885. media: media,
  3886. genre: genres.join('; '),
  3887. discnumber: discNumber,
  3888. totaldiscs: totalDiscs,
  3889. discsubtitle: discSubtitle,
  3890. tracknumber: trackNumber,
  3891. totaltracks: trs.length,
  3892. title: title,
  3893. track_artist: trackArtist,
  3894. duration: durationFromMeta(tr),
  3895. url: !identifiers.JUNODOWNLOAD_ID ? response.finalUrl : undefined,
  3896. description: description,
  3897. identifiers: mergeIds(),
  3898. cover_url: imgUrl,
  3899. };
  3900. });
  3901. });
  3902. else if (/\bhdtracks(?:\.\w+)+\//i.test(url)) return globalFetch(url).then(function(response) {
  3903. response.document.querySelectorAll('div.album-main-details > ul > li > span').forEach(function(it) {
  3904. if (it.textContent.startsWith('Title')) album = it.nextSibling.data.trim();
  3905. if (it.textContent.startsWith('Artist')) artist = it.nextElementSibling.textContent.trim();
  3906. if (it.textContent.startsWith('Genre')) {
  3907. ref = it;
  3908. while ((ref = ref.nextElementSibling) != null) genres.push(ref.textContent.trim());
  3909. }
  3910. if (it.textContent.startsWith('Label')) label = it.nextElementSibling.textContent.trim();
  3911. if (it.textContent.startsWith('Release Date')) releaseDate = normalizeDate(it.nextSibling.data.trim());
  3912. });
  3913. isVA = vaParser.test(artist);
  3914. if ((ref = response.document.querySelector('p.product-image > img')) != null) imgUrl = ref.src;
  3915. trs = response.document.querySelectorAll('table#track-table > tbody > tr[id^="track"]');
  3916. return Array.from(trs).map(function(tr) {
  3917. format = tr.querySelector('td:nth-of-type(4) > span').textContent.trim();
  3918. sr = tr.querySelector('td:nth-of-type(5)').textContent.trim().replace(/\/.*/, '');
  3919. if (/^([\d\.\,]+)\s*\/\s*(\d+)$/.test(sr)) {
  3920. sr = Math.round(parseFloat(RegExp.$1.replace(',', '.')) * 1000);
  3921. bd = parseInt(RegExp.$2);
  3922. } else sr = Math.round(parseFloat(sr) * 1000);
  3923. return {
  3924. artist: isVA ? VA : artist,
  3925. album: album,
  3926. //album_year: extractYear(releaseDate),
  3927. release_date: releaseDate,
  3928. label: label,
  3929. catalog: catalogue,
  3930. encoding: 'lossless',
  3931. be: bd || 24,
  3932. sr: sr || undefined,
  3933. media: media,
  3934. genre: genres.join('; '),
  3935. //discnumber: discNumber,
  3936. //totaldiscs: totaldiscs,
  3937. //discsubtitle: discSubtitle,
  3938. tracknumber: (ref = tr.querySelector('td:first-of-type')) != null ? parseInt(ref.textContent.trim()) : undefined,
  3939. totaltracks: trs.length,
  3940. title: (ref = tr.querySelector('td.track-name')) != null ? ref.textContent.trim() : undefined,
  3941. duration: (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
  3942. url: response.finalUrl,
  3943. identifiers: mergeIds(),
  3944. cover_url: imgUrl,
  3945. }
  3946. });
  3947. });
  3948. else if (/^https?:\/\/(?:\w+\.)?deezer\.com\/(?:\w+\/)*album\/(\d+)/i.test(url)) {
  3949. return queryDeezerAPI('album/' + RegExp.$1).then(function(release) {
  3950. isVA = vaParser.test(release.artist.name);
  3951. identifiers.DEEZER_ID = release.id;
  3952. identifiers.RELEASETYPE = release.record_type;
  3953. if (release.upc) identifiers.BARCODE = release.upc;
  3954. if (release.cover_xl) imgUrl = release.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0');
  3955. return release.tracks.data.map(function(track, ndx) {
  3956. trackIdentifiers = { TRACK_ID: track.id };
  3957. trackArtist = track.artist.name;
  3958. if (!isVA && trackArtist && trackArtist == release.artist.name) trackArtist = undefined;
  3959. return {
  3960. artist: isVA ? VA : release.artist.name,
  3961. album: release.title,
  3962. release_date: release.release_date,
  3963. label: release.label,
  3964. media: media,
  3965. genre: release.genres.data.map(it => it.name).join('; '),
  3966. tracknumber: ndx + 1,
  3967. totaltracks: release.nb_tracks,
  3968. title: track.title,
  3969. track_artist: trackArtist,
  3970. duration: track.duration,
  3971. //url: deezerAlbumPrefix + release.id,
  3972. identifiers: mergeIds(),
  3973. cover_url: imgUrl,
  3974. };
  3975. });
  3976. });
  3977. } else if (url.toLowerCase().includes('spotify.com/') && /\/albums?\/(\w+)$/i.test(url)) {
  3978. return querySpotifyAPI('albums/' + RegExp.$1).then(function(release) {
  3979. artist = release.artists.map(artist => artist.name);
  3980. isVA = release.artists.length == 0 || release.artists.length == 1 && vaParser.test(release.artists[0].name);
  3981. totalDiscs = release.tracks.items.reduce((acc, track) => Math.max(acc, track.disc_number), 0);
  3982. identifiers.SPOTIFY_ID = release.id;
  3983. identifiers.RELEASETYPE = release.album_type;
  3984. identifiers.BARCODE = release.external_ids.upc;
  3985. var image = release.images.reduce((acc, image) => image.width * image.height > acc.width * acc.height ? image : acc);
  3986. return release.tracks.items.map(function(track, ndx) {
  3987. trackIdentifiers = {
  3988. TRACK_ID: track.id,
  3989. EXPLICIT: Number(track.explicit),
  3990. };
  3991. trackArtist = track.artists.map(artist => artist.name);
  3992. if (!isVA && trackArtist.equalCaselessTo(artist)) trackArtist = [];
  3993. return {
  3994. artist: isVA ? VA : joinArtists(artist),
  3995. album: release.name,
  3996. release_date: release.release_date,
  3997. label: release.label,
  3998. media: media,
  3999. genre: release.genres.join('; '),
  4000. discnumber: track.disc_number,
  4001. totaldiscs: totalDiscs,
  4002. discsubtitle: discSubtitle,
  4003. tracknumber: track.track_number,
  4004. totaltracks: release.total_tracks,
  4005. title: track.name,
  4006. track_artist: joinArtists(trackArtist),
  4007. duration: track.duration_ms / 1000,
  4008. //url: 'https://open.spotify.com/album/' + release.id,
  4009. identifiers: mergeIds(),
  4010. cover_url: image ? image.url : undefined,
  4011. };
  4012. });
  4013. });
  4014. } else if (url.toLowerCase().includes('prostudiomasters.com/')) return globalFetch(url).then(function(response) {
  4015. if (/\/page\/(\d+)$/i.test(response.finalUrl)) identifiers.PROSTUDIOMASTERS_ID = RegExp.$1;
  4016. artist = Array.from(dresponse.documentom.querySelectorAll('h2.ArtistName > a'))
  4017. .map(node => node.textContent.trim());
  4018. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4019. if (isVA) artist = [];
  4020. if ((ref = response.document.querySelector('h3.AlbumName')) != null) album = ref.textContent.trim();
  4021. if ((ref = response.document.querySelector('div.pline')) != null
  4022. && /^(?:[℗©]\s*)+(\d{4})\s+(.+)/.test(ref.textContent.trim())) {
  4023. releaseDate = RegExp.$1;
  4024. label = RegExp.$2;
  4025. }
  4026. getDescription(response, 'div.album-info', false);
  4027. if ((ref = response.document.querySelector('img.album-art')) != null) imgUrl = ref.currentSrc || ref.src;
  4028. trs = response.document.querySelectorAll('div.album-tracks > div.tracks > table > tbody > tr');
  4029. totalTracks = Array.from(trs).filter(tr => tr.classList.contains('track-playable')).length;
  4030. discNumber = 0;
  4031. trs.forEach(function(tr) {
  4032. if (tr.classList.contains('track-playable')) {
  4033. trackArtist = []; sr = bd = format = title = undefined; trackIdentifiers = {};
  4034. if (ref = tr.getAttribute('data-track-id')) trackIdentifiers.TRACK_ID = ref;
  4035. trackNumber = (ref = tr.querySelector('div.num')) != null ? parseInt(ref.firstChild.textContent.trim()) : undefined;
  4036. if (trackNumber == 1) ++discNumber;
  4037. if ((ref = tr.querySelector('td.track-name > div.name')) != null) {
  4038. title = ref.firstChild.textContent.trim();
  4039. if ((ref = ref.querySelector(':scope small')) != null) {
  4040. trackArtist = splitArtists(ref.firstChild.textContent);
  4041. if (!isVA && trackArtist.equalCaselessTo(artist)) trackArtist = [];
  4042. }
  4043. }
  4044. if ((ref = tr.querySelector('span.track-format')) != null && /^(\d+(?:[,\.]\d+)?)\s*([kMG]Hz)(?:\s+(\d+)-bit)?\s*\|\s*(\S+)$/i.test(ref.textContent.trim())) {
  4045. sr = parseFloat(RegExp.$1);
  4046. ['khz', 'mhz', 'ghz'].forEach((unit, ndx) => { if (RegExp.$2.toLowerCase() == unit) sr *= 1000 ** (ndx + 1) });
  4047. sr = Math.round(sr) || undefined;
  4048. bd = parseInt(RegExp.$3) || undefined;
  4049. format = RegExp.$4;
  4050. }
  4051. tracks.push({
  4052. artist: isVA ? VA : artist.join('; '),
  4053. album: album,
  4054. //album_year: extractYear(releaseDate),
  4055. release_date: releaseDate,
  4056. label: label,
  4057. catalog: catalogue,
  4058. codec: format,
  4059. bd: bd,
  4060. sr: sr,
  4061. media: media,
  4062. discnumber: discNumber,
  4063. totaldiscs: totalDiscs,
  4064. discsubtitle: discSubtitle,
  4065. tracknumber: trackNumber,
  4066. totaltracks: totalTracks,
  4067. title: title,
  4068. track_artist: joinArtists(trackArtist),
  4069. duration: (ref = tr.querySelector('td:last-of-type')) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  4070. url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
  4071. description: description,
  4072. identifiers: mergeIds(),
  4073. cover_url: imgUrl,
  4074. });
  4075. } else if ((ref = tr.querySelector('div.grouping-title')) != null) {
  4076. discSubtitle = ref.textContent.trim();
  4077. guessDiscNumber();
  4078. }
  4079. });
  4080. return tracks;
  4081. });
  4082. /*
  4083. else if (url.toLowerCase().includes('soundcloud.com/') && prefs.soundcloud_clientid) {
  4084. SC.initialize({
  4085. client_id: prefs.soundcloud_clientid,
  4086. redirect_uri: 'https://dont.spam.me/',
  4087. });
  4088. SC.connect().then(function() { return SC.resolve(url) }).then(function(release) {
  4089. isVA = vaParser.test(release.artist.name);
  4090. identifiers.SOUNDCLOUD_ID = release.id;
  4091. identifiers.RELEASETYPE = release.record_type;
  4092. release.tracks.data.forEach(function(track, ndx) {
  4093. trackIdentifiers = { TRACK_ID: track.id };
  4094. trackArtist = track.artist.name;
  4095. if (!isVA && trackArtist && trackArtist == release.artist.name) trackArtist = undefined;
  4096. tracks.push({});
  4097. });
  4098. return tracks;
  4099. });
  4100. return true;
  4101. */
  4102. else if (url.toLowerCase().includes('play.google.com/store/music/album/')) return globalFetch(url).then(function(response) {
  4103. var search = new URLSearchParams(new URL(response.finalUrl).search);
  4104. var ID = search.get('id'), trackID, aggregateRating;
  4105. if (ID) identifiers.GOOGLE_ID = ID;
  4106. var root = response.document.querySelector('div[itemtype="https://schema.org/MusicAlbum"]');
  4107. if (root == null) throw new Error('Unexpected Google Play metadata structure');
  4108. if ((ref = root.querySelector('div[itemprop="byArtist"]')) != null) {
  4109. artist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
  4110. isVA = artist.length == 1 && vaParser.test(artist[0]);
  4111. }
  4112. if ((ref = root.querySelector('meta[itemprop="name"]')) != null) album = ref.content;
  4113. genres = Array.from(root.querySelectorAll('meta[itemprop="genre"]')).map(elem => elem.content);
  4114. if ((ref = root.querySelector('meta[itemprop="datePublished"]')) != null) releaseDate = ref.content;
  4115. if ((ref = root.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  4116. if ((ref = root.querySelector('meta[itemprop="ratingValue"]')) != null) aggregateRating = parseFloat(ref.content);
  4117. //getDescription(response, '???', false);
  4118. if ((ref = response.document.querySelector('h1[class][itemprop="name"] > span')) != null
  4119. && (ref = ref.parentNode.parentNode.querySelector('div[class] > span[class]')) != null
  4120. && /\bExplicit/i.test(ref.textContent)) identifiers.EXPLICIT = 1;
  4121. if ((ref = response.document.querySelector('span > a[itemprop="genre"]')) != null) try {
  4122. label = ref.parentNode.nextElementSibling.textContent.trim().replace(/^(?:[©℗]|\([cCpP]\))\s*\d{4}\s+/, '');
  4123. } catch(e) { console.warn('Unexpected HTML structure (' + e + ')') }
  4124. if ((ref = response.document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content;
  4125. var volumes = response.document.querySelectorAll('c-wiz > div > h2');
  4126. if (volumes.length <= 0) {
  4127. //response.document.querySelectorAll('c-wiz > div > table > tbody > tr[class]').forEach(scanPlaylist);
  4128. trackNumber = 0;
  4129. root.querySelectorAll('div[itemprop="track"]').forEach(function(tr) {
  4130. trackArtist = undefined; trackIdentifiers = {};
  4131. if ((ref = tr.querySelector('meta[itemprop="url"]')) != null) {
  4132. search = new URLSearchParams(new URL(ref.content).search);
  4133. let trackID = search.get('tid');
  4134. if (trackID) trackIdentifiers.TRACK_ID = trackID;
  4135. }
  4136. ++trackNumber;
  4137. title = (ref = tr.querySelector('meta[itemprop="name"]')) != null ? ref.content : undefined;
  4138. if ((ref = tr.querySelector('div[itemprop="byArtist"]')) != null) {
  4139. trackArtist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
  4140. trackArtist = (isVA || !Array.isArray(artist) || !trackArtist.equalCaselessTo(artist)) && joinArtists(trackArtist) || undefined;
  4141. }
  4142. duration = durationFromMeta(tr);
  4143. addTrack();
  4144. });
  4145. } else volumes.forEach(function(volume) {
  4146. discNumber = undefined; discSubtitle = volume.textContent.trim();
  4147. guessDiscNumber();
  4148. volume.nextElementSibling.querySelectorAll('tbody > tr[class]').forEach(scanPlaylist);
  4149. });
  4150. return tracks;
  4151.  
  4152. function scanPlaylist(tr) {
  4153. trackNumber = (ref = tr.querySelector('td:nth-of-type(1) > div')) != null ? parseInt(ref.textContent) : undefined;
  4154. title = (ref = tr.querySelector('td[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  4155. duration = (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined;
  4156. trackArtist = Array.from(tr.querySelectorAll('td:nth-of-type(4) > a')).map(it => it.textContent.trim());
  4157. trackArtist = (isVA || !Array.isArray(artist) || !trackArtist.equalCaselessTo(artist))
  4158. && joinArtists(trackArtist) || undefined;
  4159. addTrack();
  4160. }
  4161. function addTrack() {
  4162. tracks.push({
  4163. artist: isVA ? VA : artist.join('; '),
  4164. album: album,
  4165. //album_year: extractYear(releaseDate),
  4166. release_date: releaseDate,
  4167. label: label,
  4168. catalog: catalogue,
  4169. media: media,
  4170. genre: genres.join('; '),
  4171. discnumber: discNumber,
  4172. totaldiscs: totalDiscs,
  4173. discsubtitle: discSubtitle,
  4174. tracknumber: trackNumber,
  4175. totaltracks: totalTracks,
  4176. title: title,
  4177. track_artist: trackArtist,
  4178. duration: duration,
  4179. url: identifiers.GOOGLE_ID ? undefined : response.finalUrl,
  4180. //description: description,
  4181. identifiers: mergeIds(),
  4182. cover_url: imgUrl,
  4183. });
  4184. }
  4185. });
  4186. else if (url.toLowerCase().includes('7digital.com/')) return globalFetch(url).then(function(response) {
  4187. if ((ref = response.document.querySelector('table.release-track-list')) != null)
  4188. identifiers['7DIGITAL_ID'] = ref.dataset.releaseid;
  4189. artist = Array.from(response.document.querySelectorAll('h2.release-info-artist > span[itemprop="byArtist"] > meta[itemprop="name"]'))
  4190. .map(node => node.content);
  4191. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4192. if (isVA) artist = [];
  4193. if ((ref = response.document.querySelector('h1.release-info-title')) != null) album = ref.textContent.trim();
  4194. if ((ref = response.document.querySelector('div.release-date-info > p')) != null) releaseDate = normalizeDate(ref.textContent);
  4195. if ((ref = response.document.querySelector('div.release-label-info > p')) != null) label = ref.textContent.trim();
  4196. response.document.querySelectorAll('dl.release-data > dt.release-data-label').forEach(function(dt) {
  4197. if (/\bGenres?:/.test(dt.textContent)) genres = Array.from(dt.nextElementSibling.querySelectorAll('a')).map(a => a.textContent.trim());
  4198. });
  4199. //getDescription(response, 'div.album-info', false);
  4200. if ((ref = response.document.querySelector('span.release-packshot-image > img[itemprop="image"]')) != null)
  4201. imgUrl = ref.src;
  4202. totalTracks = response.document.querySelectorAll('table.release-track-list > tbody > tr.release-track').length;
  4203. response.document.querySelectorAll('table.release-track-list').forEach(function(table) {
  4204. discSubtitle = discNumber = undefined;
  4205. if ((ref = table.querySelector('caption > h4.release-disc-info')) != null) {
  4206. discSubtitle = ref.textContent.trim();
  4207. guessDiscNumber();
  4208. }
  4209. table.querySelectorAll('tbody > tr.release-track').forEach(function(tr) {
  4210. trackIdentifiers = {};
  4211. if (tr.dataset.trackid) trackIdentifiers.TRACK_ID = tr.dataset.trackid;
  4212. tracks.push({
  4213. artist: isVA ? VA : artist.join('; '),
  4214. album: album,
  4215. //album_year: extractYear(releaseDate),
  4216. release_date: releaseDate,
  4217. label: label,
  4218. catalog: catalogue,
  4219. media: media,
  4220. genre: genres.join('; '),
  4221. discnumber: discNumber,
  4222. totaldiscs: totalDiscs,
  4223. discsubtitle: discSubtitle,
  4224. tracknumber: (ref = tr.querySelector('td.release-track-preview > em.release-track-preview-text')) != null ?
  4225. ref.textContent.trim() : undefined,
  4226. totaltracks: totalTracks,
  4227. title: (ref = tr.querySelector('td.release-track-name > meta[itemprop="name"]')) != null ?
  4228. ref.content : undefined,
  4229. duration: durationFromMeta(tr),
  4230. url: !identifiers['7DIGITAL_ID'] ? response.finalUrl : undefined,
  4231. description: description,
  4232. identifiers: mergeIds(),
  4233. cover_url: imgUrl,
  4234. });
  4235. });
  4236. });
  4237. return tracks;
  4238. });
  4239. else if (url.toLowerCase().includes('e-onkyo.com/')) return globalFetch(url).then(function(response) {
  4240. if (/\/album\/(\w+)\/?$/.test(response.finalUrl)) identifiers.EONKYO_ID = RegExp.$1;
  4241. artist = Array.from(response.document.querySelectorAll('div.jacketDetailArea p.artistsName > a'))
  4242. .map(node => node.textContent.trim());
  4243. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4244. if (isVA) artist = [];
  4245. if ((ref = response.document.querySelector('div.jacketDetailArea p.packageTtl')) != null) album = ref.textContent.trim();
  4246. if ((ref = response.document.querySelector('div.jacketDetailArea p.recordlabelName > a')) != null) label = ref.textContent.trim();
  4247. if ((ref = response.document.querySelector('div.jacketDetailArea p.releaseDay > a')) != null) releaseDate = normalizeDate(ref.textContent);
  4248. if ((ref = response.document.querySelector('div.jacketDetailArea p.packageNoteDetail')) != null
  4249. && /^\s*(?:\(C\)|©)\s+(\d{4})\b/i.test(ref.lastChild.textContent)) albumYear = parseInt(RegExp.$1);
  4250. //getDescription(response, 'div#credit', true);
  4251. if (/\s+\(\s*(?:(\d+)[\-\s]*bit)?\s*\/?\s*(?:(\d+(?:\.\d+)?)\s*kHz)?\s*\)\s*$/i.test(album)) {
  4252. album = RegExp.leftContext;
  4253. bd = parseInt(RegExp.$1) || undefined;
  4254. sr = parseFloat(RegExp.$2);
  4255. }
  4256. if ((ref = response.document.querySelector('figure > a.colorbox')) != null)
  4257. imgUrl = new URL(response.finalUrl).origin + ref.pathname;
  4258. trs = response.document.querySelectorAll('dl.musicList > dd.musicBox');
  4259. return Array.from(trs).map(tr => ({
  4260. //var trackId = tr.dataset.trackid;
  4261. //if (trackId) trackId = 'TRACK_ID=' + trackId;
  4262. //trackArtist = tr.children[5].textContent.trim();
  4263. //if (trackArtist == artist.join(', ')) trackArtist = undefined;
  4264. artist: isVA ? VA : artist.join('; '),
  4265. album: album,
  4266. album_year: albumYear,
  4267. release_date: releaseDate,
  4268. label: label,
  4269. catalog: catalogue,
  4270. encoding: 'lossless',
  4271. codec: 'FLAC',
  4272. bd: bd,
  4273. sr: sr * 1000 || undefined,
  4274. media: media,
  4275. //discnumber: discNumber,
  4276. //totaldiscs: totalDiscs,
  4277. //discsubtitle: discSubtitle,
  4278. tracknumber: (ref = tr.querySelector('div.musicListNo')) != null ? ref.textContent.trim() : undefined,
  4279. totaltracks: trs.length,
  4280. title: (ref = tr.querySelector('div.musicTtl > span')) != null ? ref.title : undefined,
  4281. duration: (ref = tr.querySelector('div.musicTime')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
  4282. url: !identifiers.EONKYO_ID ? response.finalUrl : undefined,
  4283. description: description,
  4284. identifiers: mergeIds(),
  4285. cover_url: imgUrl,
  4286. }));
  4287. });
  4288. else if (url.toLowerCase().includes('store.acousticsounds.com/')) return globalFetch(url).then(function(response) {
  4289. if (/\/(\d+)\/$/.test(response.finalUrl)) identifiers.ACOUSTICSOUNDS_ID = RegExp.$1;
  4290. artist = Array.from(response.document.querySelectorAll('div > h1 > a')).map(node => node.textContent.trim());
  4291. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4292. if (isVA) artist = [];
  4293. if ((ref = response.document.querySelector('div > h1')) != null) album = ref.lastChild.wholeText.trim().replace(/\s*-\s*/, '');
  4294. response.document.querySelectorAll('div > p > table > tbody > tr > td:first-of-type').forEach(function(td) {
  4295. if (/^(?:Label):/i.test(td.textContent)) label = td.nextElementSibling.textContent.trim();
  4296. if (/^(?:Genre):/i.test(td.textContent)) genres[0] = td.nextElementSibling.textContent.trim();
  4297. if (/^(?:Product\s+No):/i.test(td.textContent)) catalogue = td.nextElementSibling.textContent.trim();
  4298. if (/^(?:Category):/i.test(td.textContent)
  4299. && /^(.+)\s+(\d+(?:\.\d+)?)\s*kHz(?:\s*\/\s*(\d+)[\s\-]?bit)?\s+Download\b/.test(td.nextElementSibling.textContent.trim())) {
  4300. format = RegExp.$1;
  4301. sr = parseFloat(RegExp.$2) * 1000;
  4302. bd = parseInt(RegExp.$3);
  4303. }
  4304. });
  4305. getDescription(response, 'div#description > p', true);
  4306. if ((ref = response.document.querySelector('div#detail > link[rel="image_src"]')) != null) {
  4307. imgUrl = ref.href.replace(/\/medium\//i, '/large/');
  4308. }
  4309. trs = response.document.querySelectorAll('div#tracks > table > tbody > tr');
  4310. trackNumber = 0;
  4311. return Array.from(trs).map(tr => ({
  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. encoding: ['FLAC', 'DSD'].includes(format) ? 'lossless' : undefined,
  4319. codec: format,
  4320. bd: bd,
  4321. sr: sr,
  4322. media: media,
  4323. genre: genres.join('; '),
  4324. //discnumber: discNumber,
  4325. //totaldiscs: totalDiscs,
  4326. //discsubtitle: discSubtitle,
  4327. tracknumber: ++trackNumber,
  4328. totaltracks: trs.length,
  4329. title: (ref = tr.querySelector('td[nowrap]')) != null ? ref.textContent.trim() : undefined,
  4330. url: !identifiers.ACOUSTICSOUNDS_ID ? response.finalUrl : undefined,
  4331. description: description,
  4332. identifiers: mergeIds(),
  4333. cover_url: imgUrl,
  4334. }));
  4335. });
  4336. else if (url.toLowerCase().includes('indies.eu/')) return globalFetch(url).then(function(response) {
  4337. if (/\/alba\/(\d+)\//.test(response.finalUrl)) identifiers.INDIESSCOPE_ID = RegExp.$1;
  4338. ref = response.document.querySelector(':root > body > div > div > div > h2');
  4339. if (ref != null) artist = Array.from(ref.childNodes).map(node => node.textContent.trim());
  4340. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4341. if (isVA) artist = [];
  4342. if ((ref = response.document.querySelector(':root > body > div > div > div > h1')) != null)
  4343. album = ref.textContent.trim();
  4344. if ((ref = response.document.querySelector('div.infoBox')) != null) {
  4345. let ndx = 0;
  4346. ref.childNodes.forEach(function(child) {
  4347. if (child.nodeName == 'BR') { ++ndx; return; }
  4348. switch (ndx) {
  4349. case 0:
  4350. if (child.nodeType == Node.TEXT_NODE) {
  4351. label = child.wholeText.trim();
  4352. if (/^(.*)\s+\/\s+(\d{4})$/.test(label)) {
  4353. label = RegExp.$1;
  4354. releaseDate = RegExp.$2;
  4355. }
  4356. }
  4357. break;
  4358. case 1:
  4359. if (child.nodeType == Node.ELEMENT_NODE) genres.push(child.textContent.trim());
  4360. break;
  4361. case 2:
  4362. if (child.nodeType == Node.ELEMENT_NODE) catalogue = child.textContent.trim();
  4363. break;
  4364. }
  4365. });
  4366. }
  4367. getDescription(response, 'div.popis > section', true);
  4368. if ((ref = response.document.querySelector('div.obrazekDetail > img')) != null) imgUrl = ref.src;
  4369. trs = response.document.querySelectorAll('table.skladby > tbody > tr');
  4370. return Array.from(trs).map(function(tr) {
  4371. title = undefined;
  4372. if ((ref = tr.querySelector('td.nazev')) != null) {
  4373. trackNumber = parseInt(ref.firstChild.wholeText);
  4374. title = ref.querySelector('strong').textContent.trim();
  4375. }
  4376. return {
  4377. artist: isVA ? VA : artist.join('; '),
  4378. album: album,
  4379. //album_year: extractYear(releaseDate),
  4380. release_date: releaseDate,
  4381. label: label,
  4382. catalog: catalogue,
  4383. codec: format,
  4384. media: media,
  4385. genre: genres.join('; '),
  4386. //discnumber: discNumber,
  4387. //totaldiscs: totalDiscs,
  4388. //discsubtitle: discSubtitle,
  4389. tracknumber: trackNumber,
  4390. totaltracks: trs.length,
  4391. title: title,
  4392. duration: (ref = tr.querySelector('td:nth-of-type(4)')) != null ? timeStringToTime(ref.textContent) : undefined,
  4393. identifiers: !identifiers.INDIESSCOPE_ID ? response.finalUrl : undefined,
  4394. description: description,
  4395. identifiers: mergeIds(),
  4396. cover_url: imgUrl,
  4397. };
  4398. });
  4399. });
  4400. else if (url.toLowerCase().includes('beatport.com/')) return globalFetch(url).then(function(response) {
  4401. if (/\/release\/(?:\d\/)?(?:\S+-)?(\d+)\b/i.test(response.finalUrl)) identifiers.BEATPORT_ID = RegExp.$1;
  4402. artist = Array.from(response.document.querySelectorAll('span > a[data-artist]')).map(node => node.textContent.trim());
  4403. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4404. if (isVA) artist = [];
  4405. if ((ref = response.document.querySelector('div > h1')) != null) album = ref.textContent.trim();
  4406. response.document.querySelectorAll('ul > li > span.category').forEach(function(span) {
  4407. if (/^(?:Release\s+Date)/i.test(span.textContent)) releaseDate = span.nextElementSibling.textContent.trim();
  4408. if (/^(?:Label)/i.test(span.textContent)) label = span.nextElementSibling.textContent.trim();
  4409. if (/^(?:Catalog)/i.test(span.textContent)) catalogue = span.nextElementSibling.textContent.trim();
  4410. });
  4411. getDescription(response, 'div.interior-expandable', true);
  4412. if ((ref = response.document.querySelector('div > img.interior-release-chart-artwork')) != null) imgUrl = ref.src;
  4413. trs = response.document.querySelectorAll('div.tracks > ul > li.track');
  4414. return Array.from(trs).map(function(tr) {
  4415. title = undefined; trackIdentifiers = {};
  4416. if ((ref = tr.querySelector('span.buk-track-primary-title')) != null) {
  4417. title = ref.title || ref.textContent.trim();
  4418. if ((ref = tr.querySelector('span.buk-track-remixed')) != null) title += ' (' + ref.textContent.trim() + ')';
  4419. }
  4420. trackArtist = Array.from(tr.querySelectorAll('p.buk-track-artists > a')).map(a => a.textContent.trim());
  4421. if (!isVA && trackArtist.equalCaselessTo(artist)) trackArtist = [];
  4422. if ((ref = tr.querySelector('p.buk-track-bpm')) != null) trackIdentifiers.BPM = ref.textContent;
  4423. return {
  4424. artist: isVA ? VA : artist.join('; '),
  4425. album: album,
  4426. //album_year: extractYear(releaseDate),
  4427. release_date: releaseDate,
  4428. label: (ref = tr.querySelector('p.buk-track-labels')) != null ? ref.textContent.trim() : label,
  4429. catalog: catalogue,
  4430. codec: format,
  4431. media: media,
  4432. genre: Array.from(tr.querySelectorAll('p.buk-track-genre > a')).map(a => a.textContent).join('; '),
  4433. //discnumber: discNumber,
  4434. //totaldiscs: totalDiscs,
  4435. //discsubtitle: discSubtitle,
  4436. tracknumber: (ref = tr.querySelector('div.buk-track-num')) != null ? ref.textContent.trim() : undefined,
  4437. totaltracks: trs.length,
  4438. title: title,
  4439. track_artist: joinArtists(trackArtist),
  4440. remixer: Array.from(tr.querySelectorAll('p.buk-track-remixers > a')).map(a => a.textContent.trim()).join('; '),
  4441. duration: (ref = tr.querySelector('p.buk-track-length')) != null ? timeStringToTime(ref.textContent) : undefined,
  4442. url: !identifiers.BEATPORT_ID ? response.finalUrl : undefined,
  4443. description: description,
  4444. identifiers: mergeIds(),
  4445. cover_url: imgUrl,
  4446. };
  4447. });
  4448. });
  4449. else if (url.toLowerCase().includes('traxsource.com/')) return globalFetch(url).then(function(response) {
  4450. if (/\/title\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.TRAXSOURCE_ID = RegExp.$1;
  4451. artist = Array.from(response.document.querySelectorAll('h1.artists > a.com-artists')).map(node => node.textContent.trim());
  4452. if (artist.length <= 0 && (ref = response.document.querySelector('h1.artists')) != null) artist = [ref.textContent.trim()];
  4453. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4454. if (isVA) artist = [];
  4455. if ((ref = response.document.querySelector('h1.title')) != null) album = ref.textContent.trim();
  4456. if ((ref = response.document.querySelector('a.com-label')) != null) label = ref.textContent.trim();
  4457. if ((ref = response.document.querySelector('div.cat-rdate')) != null) {
  4458. catalogue = ref.textContent.trim();
  4459. if (/\s*\|\s*(\S+)$/.test(catalogue)) {
  4460. catalogue = RegExp.leftContent;
  4461. releaseDate = normalizeDate(RegExp.$1);
  4462. }
  4463. }
  4464. getDescription(response, 'div.desc', true);
  4465. if ((ref = response.document.querySelector('meta[property="og:image"]')) != null) imgUrl = ref.content;
  4466. trs = response.document.querySelectorAll('div.trklist > div.trk-row');
  4467. return Array.from(trs).map(function(tr) {
  4468. trackIdentifiers = {};
  4469. title = (ref = tr.querySelector('div.title > a')) != null ? ref.textContent.trim() : undefined;
  4470. if (title && (ref = tr.querySelector('span.version')) != null ) {
  4471. if (ref.firstChild.nodeType == Node.TEXT_NODE
  4472. && (i = ref.firstChild.wholeText.trim()).length > 0) title += ` (${i})`;
  4473. }
  4474. trackArtist = Array.from(tr.querySelectorAll('div.artists a.com-artists')).map(a => a.textContent.trim());
  4475. if (!isVA && trackArtist.equalCaselessTo(artist)) trackArtist = [];
  4476. return {
  4477. artist: isVA ? VA : artist.join('; '),
  4478. album: album,
  4479. //album_year: extractYear(releaseDate),
  4480. release_date: releaseDate,
  4481. label: label,
  4482. catalog: catalogue,
  4483. media: media,
  4484. genre: Array.from(tr.querySelectorAll('div.genre > a')).map(a => a.textContent.trim()).join('; '),
  4485. //discnumber: discNumber,
  4486. //totaldiscs: totalDiscs,
  4487. //discsubtitle: discSubtitle,
  4488. tracknumber: (ref = tr.querySelector('div.tnum')) != null ? ref.textContent.trim() : undefined,
  4489. totaltracks: trs.length,
  4490. title: title,
  4491. track_artist: joinArtists(trackArtist),
  4492. remixer: Array.from(tr.querySelectorAll('div.artists a.com-remixers')).map(a => a.textContent.trim()).join('; '),
  4493. duration: (ref = tr.querySelector('span.duration')) != null ? timeStringToTime(ref.textContent) : undefined,
  4494. url: !identifiers.TRAXSOURCE_ID ? response.finalUrl : undefined,
  4495. description: description,
  4496. identifiers: mergeIds(),
  4497. cover_url: imgUrl,
  4498. };
  4499. });
  4500. });
  4501. else if (url.toLowerCase().includes('music.apple.com/')) return globalFetch(url).then(function(response) {
  4502. if (/\/(\d+)(?=$|\?)/.test(response.finalUrl)) identifiers.APPLE_ID = RegExp.$1;
  4503. artist = Array.from(response.document.querySelectorAll('span.product-header__identity > a.link')).map(a => a.textContent.trim());
  4504. if (artist.length <= 0 && (ref = response.document.querySelector('span.product-header__identity')) != null) {
  4505. artist = [ref.textContent.trim()];
  4506. }
  4507. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4508. if ((ref = response.document.querySelector('h1 > span.product-header__title')) != null) {
  4509. album = ref.textContent.trim();
  4510. if (album.endsWith(' - Single')) {
  4511. identifiers.RELEASETYPE = 'Single';
  4512. album = album.slice(0, -9);
  4513. }
  4514. }
  4515. genres = Array.from(response.document.querySelectorAll('ul.inline-list > li:first-of-type > a')).map(a => a.textContent.trim());
  4516. if ((ref = response.document.querySelector('meta[property="music:release_date"]')) != null) releaseDate = ref.content;
  4517. if ((ref = response.document.querySelector('li.link-list__item--copyright')) != null) {
  4518. label = ref.textContent.replace(/^.*[©℗]\s+\d{4}\s+/, '');
  4519. }
  4520. description = html2php(response.document.querySelector('section.product-hero-desc__section'), response.finalUrl);
  4521. if (description && !description.includes('[quote]')) {
  4522. description = '[quote]' + description.collapseGaps() + '[/quote]';
  4523. }
  4524. discNumber = 0;
  4525. trs = response.document.querySelectorAll('table > tbody > tr[id]');
  4526. return Array.from(trs).map(function(tr) {
  4527. trackIdentifiers = {};
  4528. trackNumber = (ref = tr.querySelector('span.table__row__number')) != null ? parseInt(ref.textContent) : undefined;
  4529. if (trackNumber == 1) ++discNumber;
  4530. trackArtist = /*(ref = tr.querySelector('div.table__row__titles > div:last-of-type')) != null ?
  4531. ref.textContent.trim() : */undefined;
  4532. if (!isVA && trackArtist == joinArtists(artist)) trackArtist = undefined;
  4533. if ((ref = tr.querySelector('time.table__row__duration-counter')) != null
  4534. && /^PT(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/.test(ref.dateTime)) {
  4535. duration = (parseInt(RegExp.$1) * 60**2 || 0) + (parseInt(RegExp.$2) * 60 || 0) + (parseInt(RegExp.$3) || 0);
  4536. } else duration = undefined;
  4537. return {
  4538. artist: isVA ? VA : artist.join('; '),
  4539. album: album,
  4540. //album_year: extractYear(releaseDate),
  4541. release_date: releaseDate,
  4542. label: label,
  4543. media: media,
  4544. genre: genres.join('; '),
  4545. discnumber: discNumber,
  4546. tracknumber: trackNumber,
  4547. totaltracks: trs.length,
  4548. title: (ref = tr.querySelector('div.table__row__headline')) != null ? ref.textContent.trim() : undefined,
  4549. track_artist: trackArtist,
  4550. duration: duration,
  4551. description: description,
  4552. url: !identifiers.APPLE_ID ? response.finalUrl : undefined,
  4553. identifiers: mergeIds(),
  4554. };
  4555. });
  4556. });
  4557. else if (mbrRlsParser.test(url)) { // MusicBrainz
  4558. var entities = [
  4559. 'aliases', 'annotation', 'artist-credits', 'artists', 'collections', 'discids', 'genres',
  4560. 'isrcs', 'labels', 'media', 'ratings', 'recordings', 'release-groups', 'tags', 'url-rels',
  4561. ];
  4562. return queryMusicBrainzAPI('release/' + RegExp.$1, { inc: entities.join('+') }).then(function(release) {
  4563. if (release.error) return Promise.reject(release.error);
  4564. identifiers.MBID = release.id;
  4565. if (release.barcode) identifiers.BARCODE = release.barcode;
  4566. if (release.asin) identifiers.ASIN = release.asin;
  4567. if (release['release-group']['primary-type']) identifiers.RELEASETYPE = release['release-group']['primary-type'];
  4568. artist = Array.isArray(release['artist-credit']) ? release['artist-credit'].map(artist => artist.name) : [];
  4569. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4570. if (Array.isArray(release.genres)) genres = release.genres.map(genre => genre.name);
  4571. if (Array.isArray(release.tags)) Array.prototype.push.apply(genres, release.tags.map(tag => tag.name));
  4572. if (genres.length <= 0) {
  4573. if (Array.isArray(release['release-group'].genres)) {
  4574. Array.prototype.push.apply(genres, release['release-group'].genres.map(tag => tag.name));
  4575. }
  4576. if (Array.isArray(release['release-group'].tags)) {
  4577. Array.prototype.push.apply(genres, release['release-group'].tags.map(tag => tag.name));
  4578. }
  4579. }
  4580. label = release['label-info'].map(label => label.label.name);
  4581. catalogue = release['label-info'].map(label => label['catalog-number']);
  4582. release.media.forEach(function(medium, ndx) {
  4583. medium.tracks.forEach(function(track, ndx) {
  4584. trackIdentifiers = { TRACK_ID: track.id };
  4585. if (Array.isArray(track['artist-credit'])) {
  4586. trackArtist = track['artist-credit'].map(artist => artist.name);
  4587. trackArtist = trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist));
  4588. } else trackArtist = false;
  4589. tracks.push({
  4590. artist: isVA ? VA : artist.join(', '),
  4591. album: /*release['release-group'].title || */release.title,
  4592. album_year: extractYear(release['release-group']['first-release-date']),
  4593. release_date: release.date,
  4594. genre: genres.join('; '),
  4595. label: label.filter(label => label).join(' / '),
  4596. catalog: catalogue.filter(catno => catno).join(' / '),
  4597. media: medium.format,
  4598. discnumber: medium.position,
  4599. discsubtitle: medium.title,
  4600. totaldiscs: release.media.length,
  4601. tracknumber: track.number,
  4602. title: track.title,
  4603. track_artist: trackArtist ? track['artist-credit']
  4604. .map(artist => artist.name.concat(artist.joinphrase)).join('') : undefined,
  4605. duration: track.length / 1000,
  4606. //country: release.country,
  4607. description: release.annotation,
  4608. identifiers: mergeIds(),
  4609. });
  4610. });
  4611. });
  4612. return tracks;
  4613. });
  4614. }
  4615. else if (url.toLowerCase().includes('vgmdb.net/')) return globalFetch(url).then(function(response) {
  4616. if (/\/album\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.VGMDB_ID = RegExp.$1;
  4617. if ((ref = response.document.querySelector('h1 > span.albumtitle[style="display:inline"]')) != null) {
  4618. album = ref.innerText.trim();
  4619. if (ref.lang == 'en'
  4620. && (ref = response.document.querySelector('div > span.albumtitle[style="display:inline"]')) != null
  4621. && ref.firstChild != null && ref.firstChild.nodeType == Node.TEXT_NODE)
  4622. album += ' ('.concat(ref.firstChild.wholeText.trim(), ')');
  4623. }
  4624. composer = [];
  4625. response.document.querySelectorAll('table#album_infobit_large > tbody > tr > td > span.label > b').forEach(function(key) {
  4626. var value = key.parentNode.parentNode.nextElementSibling;
  4627. switch (key.innerText.trim().toLowerCase()) {
  4628. case 'catalog number':
  4629. catalogue = value.textContent.trim().replace(/\s*\([^\(\)]+\)$/, '');
  4630. break;
  4631. case 'release date':
  4632. if (value.firstElementChild != null) releaseDate = value.firstElementChild.innerText.trim();
  4633. break;
  4634. case 'media format':
  4635. media = value.textContent.trim();
  4636. break;
  4637. case 'classification':
  4638. genres = value.textContent.trim().split(/\s*,\*/);
  4639. break;
  4640. case 'published by':
  4641. label = Array.from(value.querySelectorAll('a > span.productname:first-of-type'))
  4642. .map(span => span.innerText.trim()).join(' / ');
  4643. break;
  4644. case 'composed by':
  4645. case 'lyrics by':
  4646. getArtists(value).forEach(artist => { composer.pushUniqueCaseless(artist) });
  4647. break;
  4648. case 'performed by':
  4649. artist = getArtists(value);
  4650. break;
  4651. case 'arranged by':
  4652. var arrangers = getArtists(value);
  4653. break;
  4654. }
  4655. });
  4656. if (!artist || artist.length <= 0) artist = composer;
  4657. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4658. genres.pushUniqueCaseless('Soundtrack');
  4659. response.document.querySelectorAll('td#rightcolumn > div > div > div > b.label').forEach(function(key) {
  4660. var value = key.parentNode.lastChild;
  4661. if (key.innerText.toLowerCase() == 'category' && value != null)
  4662. genres.pushUniqueCaseless(value.textContent.trim());
  4663. });
  4664. getDescription(response, 'div#notes', false);
  4665. if ((ref = response.document.querySelector('div#coverart')) != null
  4666. && /\burl\s*\(\"(.*)"\)/i.test(ref.style['background-image'])) imgUrl = RegExp.$1;
  4667. response.document.querySelectorAll('div#tracklist > span > span > b').forEach(function(node) {
  4668. discSubtitle = node.innerText.trim();
  4669. guessDiscNumber();
  4670. node = node.parentNode;
  4671. while (node != null && node.nodeName != 'TABLE') node = node.nextElementSibling;
  4672. if (node != null) addVolume(node);
  4673. });
  4674. var tl = Array.from(response.document.querySelectorAll('ul#tlnav > li > a'));
  4675. if (tl.length <= 1) return tracks;
  4676. if ((i = tracks.length / tl.length) != Math.floor(i)) {
  4677. console.warn('Unexpected vgmdb.net tracklist length:', i, tracks);
  4678. return tracks;
  4679. }
  4680. let enIndex = tl.findIndex(l => /^English\b/i.test(l.innerText.trim()));
  4681. if (enIndex < 0) enIndex = tl.findIndex(l => /^Romaji\b/i.test(l.innerText.trim()));
  4682. if (enIndex < 0) return tracks.slice(0, i);
  4683. let jpIndex = tl.findIndex(l => /^Japanese\b/i.test(l.innerText.trim()));
  4684. if (jpIndex < 0) jpIndex = enIndex > 0 ? 0 : 1;
  4685. return tracks.slice(enIndex * i, (enIndex + 1) * i).map(function(track, ndx) {
  4686. const rx = /^(.+?)(?:\s+\(([^\(\)]+)\))?$/;
  4687. if (!track.title) track.title = tracks[jpIndex * i + ndx].title;
  4688. else if ((jpTitle = tracks[jpIndex * i + ndx].title) != track.title) {
  4689. track.title += ' (';
  4690. var enTitle = rx.exec(track.title), jpTitle = rx.exec(jpTitle);
  4691. if (jpTitle[1] != enTitle[1]) {
  4692. track.title += jpTitle[1];
  4693. if (jpTitle[2] && jpTitle[2] != enTitle[2]) track.title += ' ('.concat(jpTitle[2], ')');
  4694. } else track.title += jpTitle[2];
  4695. track.title += ')';
  4696. }
  4697. return track;
  4698. });
  4699.  
  4700. function addVolume(node) {
  4701. Array.prototype.push.apply(tracks, Array.from(node.querySelectorAll('tbody > tr')).map(tr => ({
  4702. artist: isVA ? VA : artist.join('; '),
  4703. album: album,
  4704. //album_year: extractYear(releaseDate),
  4705. release_date: releaseDate,
  4706. label: label,
  4707. catalog: catalogue,
  4708. media: media,
  4709. genre: genres.join('; '),
  4710. discnumber: discNumber,
  4711. //totaldiscs: totalDiscs,
  4712. discsubtitle: discSubtitle,
  4713. tracknumber: (ref = tr.querySelector('span.label')) != null ? parseInt(ref.innerText) : undefined,
  4714. //totaltracks: trs.length,
  4715. title: tr.children[1].innerText.trim(),
  4716. //track_artist: joinArtists(trackArtist),
  4717. composer: composer.join(', '),
  4718. duration: (ref = tr.querySelector('span.time')) != null ? timeStringToTime(ref.innerText) : undefined,
  4719. url: !identifiers.VGMDB_ID ? response.finalUrl : undefined,
  4720. description: description,
  4721. identifiers: mergeIds(),
  4722. cover_url: imgUrl,
  4723. })));
  4724. }
  4725. function getArtists(node) {
  4726. var artists = [];
  4727. node.childNodes.forEach(function(node) {
  4728. switch (node.nodeType) {
  4729. case Node.ELEMENT_NODE:
  4730. if ((i = node.querySelectorAll('span.artistname')).length > 0) {
  4731. var artist = i[0].innerText.trim();
  4732. if (i.length > 1 && i[0].lang == 'en') artist += ' ('.concat(i[1].innerText.trim(), ')');
  4733. } else artist = node.innerText.trim();
  4734. if (artist) artists.push(artist);
  4735. break;
  4736. case Node.TEXT_NODE:
  4737. artist = node.wholeText.trim().replace(/^\s*,\s*|\s*,\s*$/g, '');
  4738. if (/^[\(\)]+$/.test(artist)) return;
  4739. if (artist) Array.prototype.push.apply(artists, artist.split(/\s*,\s*/));
  4740. break;
  4741. }
  4742. });
  4743. return artists;
  4744. }
  4745. });
  4746. else if (url.toLowerCase().includes('tidal.com/')
  4747. && (/\/album\/(\d+)(?:\/|$)/i.test(url) || /\b(?:albumId)=(\d+)\b/i.test(url))) {
  4748. return queryTidalAPI('album', { albumId: RegExp.$1 }).then(function(album) {
  4749. var albumHeader = findModule('ALBUM_HEADER');
  4750. if (albumHeader == null) return Promise.reject('Album header not found');
  4751. var albumItems = findModule('ALBUM_ITEMS');
  4752. if (albumItems == null) return Promise.reject('Album items not found');
  4753. artist = albumHeader.album.artists.map(artist => artist.name);
  4754. isVA = artist.length == 0 || artist.length == 1 && vaParser.test(artist[0].name);
  4755. identifiers.TIDAL_ID = albumHeader.album.id;
  4756. identifiers.RELEASETYPE = albumHeader.album.type;
  4757. if (/^(?:(?:\([PC]\)|©|℗)\s+)?(?:(\d{4})\s+)?(.*)/.test(albumHeader.album.copyright)) {
  4758. //if (RegExp.$1) albumYear = parseInt(RegExp.$1);
  4759. label = RegExp.$2;
  4760. }
  4761. var channels;
  4762. description = albumHeader.description;
  4763. if (albumHeader.review.text) {
  4764. if (description) description += '\n\n';
  4765. if (!albumHeader.review.source) description += '[b]Album Review[/b]\n\n';
  4766. description += '[quote';
  4767. if (albumHeader.review.source) description += '=Album review from '.concat(albumHeader.review.source);
  4768. description += ']'.concat(albumHeader.review.text, '[/quote]');
  4769. description = description
  4770. .replace(/\[wimpLink\s+artistId="(\d+)"\]/g, '[url=https://listen.tidal.com/artist/$1]')
  4771. .replace(/\[wimpLink\s+albumId="(\d+)"\]/g, '[url=https://listen.tidal.com/album/$1]')
  4772. .replace(/\[\/wimpLink\]/g, '[/url]');
  4773. }
  4774. if (Array.isArray(albumHeader.credits.items) && albumHeader.credits.items.length > 0) {
  4775. let ac = '';
  4776. albumHeader.credits.items.forEach(function(credit) {
  4777. if (/^Primary Artist$/i.test(credit.type)) return;
  4778. // if (/^Record label$/i.test(credit.type)) {
  4779. // label = credit.contributors.map(contributor => contributor.name).join(' / ');
  4780. // return;
  4781. // }
  4782. ac += '\n'.concat(credit.type, ' – ', joinArtists(credit.contributors.map(contributor =>
  4783. !contributor.id ? contributor.name :
  4784. '[url=https://listen.tidal.com/artist/'.concat(contributor.id, ']', contributor.name, '[/url]'))));
  4785. });
  4786. if (ac.length > 0) {
  4787. if (description) {
  4788. if (!albumHeader.review.text) description += '\n';
  4789. description += '\n';
  4790. }
  4791. description += '[b]Additional Credits[/b]\n'.concat(ac);
  4792. }
  4793. }
  4794. if (albumHeader.album.cover)
  4795. imgUrl = 'https://resources.tidal.com/images/'.concat(albumHeader.album.cover.replace(/-/g, '/'), '/1280x1280.jpg');
  4796. return albumItems.pagedList.items.map(function(track, ndx) {
  4797. if (track.type != 'track') return;
  4798. trackIdentifiers = {
  4799. TRACK_ID: track.item.id,
  4800. EXPLICIT: Number(track.item.explicit),
  4801. };
  4802. trackArtist = track.item.artists.map(artist => artist.name);
  4803. if (!isVA && trackArtist.equalCaselessTo(artist)) trackArtist = [];
  4804. channels = undefined;
  4805. track.item.audioModes.forEach(function(audioMode) {
  4806. switch (audioMode.toLowerCase()) {
  4807. case 'stereo': channels = 2; break;
  4808. default: if (/\b(\d+)\.(\d+)\b/.test(audioMode)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  4809. }
  4810. });
  4811. return {
  4812. artist: isVA ? VA : joinArtists(artist),
  4813. album: albumHeader.album.title,
  4814. album_year: albumYear,
  4815. release_date: albumHeader.album.releaseDate,
  4816. label: label,
  4817. media: media,
  4818. discnumber: track.item.volumeNumber,
  4819. totaldiscs: albumHeader.album.numberOfVolumes,
  4820. //discsubtitle: discSubtitle,
  4821. tracknumber: track.item.trackNumber,
  4822. totaltracks: albumHeader.album.numberOfTracks,
  4823. title: track.item.title,
  4824. track_artist: joinArtists(trackArtist),
  4825. encoding: ['HI_RES', 'LOSSLESS'].includes(track.item.audioQuality) ? 'lossless' : undefined,
  4826. duration: track.item.duration,
  4827. channels: channels,
  4828. description: description,
  4829. url: !identifiers.TIDAL_ID ? albumHeader.album.url : undefined,
  4830. identifiers: mergeIds(),
  4831. cover_url: imgUrl,
  4832. ag: track.replayGain ? track.replayGain.toString().concat(' dB') : undefined,
  4833. };
  4834. });
  4835.  
  4836. function findModule(type) {
  4837. for (var row of album.rows) {
  4838. var result = row.modules.find(module => module.type == type);
  4839. if (result != undefined) return result;
  4840. }
  4841. return null;
  4842. }
  4843. });
  4844. }
  4845. if (!weak) clipBoard.value = '';
  4846. return Promise.reject(new URL(url).hostname + ' not supported');
  4847.  
  4848. function mergeIds() {
  4849. var r = Object.assign(identifiers, trackIdentifiers);
  4850. trackIdentifiers = {};
  4851. return r;
  4852. }
  4853.  
  4854. function getDescription(response, selector, quote = false) {
  4855. description = [];
  4856. response.document.querySelectorAll(selector).forEach(function(node) {
  4857. var p = html2php(node, response.finalUrl).trim();
  4858. if (p) description.push(p);
  4859. });
  4860. description = description.join('\n\n');
  4861. if (quote && description.length > 0 && !description.includes('[quote]')) {
  4862. description = '[quote]' + description + '[/quote]';
  4863. }
  4864. }
  4865.  
  4866. function durationFromMeta(elem) {
  4867. var m = elem.querySelector('meta[itemprop="duration"]');
  4868. if (m == null) return undefined;
  4869. if (/^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.test(m.content))
  4870. return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
  4871. m = timeStringToTime(m.content);
  4872. return m != null ? m : undefined;
  4873. }
  4874.  
  4875. function guessDiscNumber() {
  4876. if (discParser.test(discSubtitle)) {
  4877. discSubtitle = undefined;
  4878. discNumber = parseInt(RegExp.$1);
  4879. }
  4880. }
  4881. } // fetchOnline_Music
  4882.  
  4883. function parseLastFm(album) {
  4884. if (typeof album != 'object') return Promise.reject('invalid object')
  4885. var identifiers = {}, description = [];
  4886. if (album.id) identifiers.LASTFM_ID = album.id;
  4887. if (album.mbid) identifiers.MBID = album.mbid;
  4888. if (album.wiki && album.wiki.summary) description.push(album.wiki.summary);
  4889. if (album.wiki && album.wiki.content) description.push(album.wiki.content);
  4890. var genres = album.tags.tag.map(tag => tag.name);
  4891. description = description.join('\n\n');
  4892. var imgUrl = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
  4893. return acc || album.image.find(image => image.size == size && urlParser.test(image['#text']));
  4894. }, undefined);
  4895. if (imgUrl) imgUrl = imgUrl['#text'].replace(/\/\d+(?:x\d+|s)\//i, '/');
  4896. return album.tracks.track.map((track, ndx) => ({
  4897. artist: album.artist,
  4898. album: album.name,
  4899. genre: genres.join('; ') || undefined,
  4900. title: track.name,
  4901. tracknumber: ndx + 1,
  4902. track_artist: track.artist.name != album.artist ? track.artist.name : undefined,
  4903. duration: parseFloat(track.duration) || undefined,
  4904. url: album.url,
  4905. description: description || undefined,
  4906. identifiers: identifiers,
  4907. cover_url: imgUrl,
  4908. }));
  4909. }
  4910.  
  4911. function joinArtists(arr, decorator = artist => artist) {
  4912. if (!Array.isArray(arr)) return null;
  4913. if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
  4914. if (arr.length < 3) return arr.map(decorator).join(' & ');
  4915. return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
  4916. }
  4917. } // fillFromText_Music
  4918.  
  4919. function fillFromText_Apps(weak = false) {
  4920. if (messages != null) messages.parentNode.removeChild(messages);
  4921. if (!urlParser.test(clipBoard.value)) {
  4922. addMessage('valid URL accepted for this category', 'critical');
  4923. return false;
  4924. }
  4925. sourceUrl = RegExp.$1;
  4926. var description, tags = new TagManager();
  4927. if (sourceUrl.toLowerCase().includes('://sanet')) return globalFetch(sourceUrl).then(function(response) {
  4928. i = response.document.querySelector('h1.item_title > span');
  4929. var title = i == null ? undefined : i.textContent
  4930. .replace(/\s+\((?:x|ia|em)(?:64)\)/ig, ' (64-bit)')
  4931. .replace(/\s+\(x(?:86|32)\)/ig, ' (32-bit)')
  4932. .replace(/\s+(?:Build)\s+(\d+)\b/g, ' build $1')
  4933. .replace(/\s+(?:Multilingual|Multi(?:-|\s)*lang(?:uage)?)\b/g, ' multilingual');
  4934. description = html2php(response.document.querySelector('section.descr'), response.finalUrl).trim();
  4935. if (/\s*^[ \t]*(?:\[i\]\[\/i\])?Homepage\s*$.*/im.test(description)) description = RegExp.leftContext;
  4936. description = description.split(/[ \t]*\r?\n/).slice(6).map(line => line.trim()).join('\n')
  4937. .replace(/^[ \t]*(?:\[i\]\[\/i\])?Screenshots:?\s*/igm, '')
  4938. .replace(/^[ \t]*(?:\[i\]\[\/i\])?(\[b\]Release\s+Notes:?\[\/b\])(?:[ \t]*\r?\n)+/igm, '$1\n')
  4939. .replace(/\[hr\]/ig, '\n');
  4940. ref = response.document.querySelector('section.descr > div.release-info');
  4941. var releaseInfo = ref != null && ref.textContent.trim();
  4942. if (/\b(?:Languages?)\s*:\s*(.*?)\s*(?:$|\|)/i.exec(releaseInfo) != null)
  4943. description += '\n\n[b]Languages:[/b]\n' + RegExp.$1;
  4944. if ((ref = response.document.querySelector('div.txtleft > a')) != null) {
  4945. description += '\n\n[b]Product page:[/b]\n[url]' +
  4946. removeRedirect(ref.pathname.toLowerCase().startsWith('/confirm/url/') && urlParser.test(ref.textContent) ?
  4947. ref.textContent.trim() : ref.href) + '[/url]';
  4948. }
  4949. writeDescription(description.collapseGaps());
  4950. if ((ref = response.document.querySelector('section.descr > div.center > a.mfp-image')) != null) {
  4951. setCover(ref.href);
  4952. } else {
  4953. ref = response.document.querySelector('section.descr > div.center > img[data-src]');
  4954. if (ref != null) setCover(ref.dataset.src);
  4955. }
  4956. var internalTags = Array.from(response.document.querySelectorAll('ul.item_tags_list > li > a[rel="tag"]'))
  4957. .map(elem => elem.textContent.toLowerCase().trim());
  4958. if ((ref = response.document.querySelector('a.cat:last-of-type > span')) != null) {
  4959. if (ref.textContent.toLowerCase() == 'windows') {
  4960. tags.add('apps.windows');
  4961. if (/\b(?:(?:x|ia|em)64)\b/i.test(releaseInfo) || /\(64[-\s]*bit\)/i.test(title)) tags.add('win64');
  4962. if (/\b(?:x86|x32)\b/i.test(releaseInfo) || /\(32[-\s]*bit\)/i.test(title)) tags.add('win32');
  4963. }
  4964. if (ref.textContent.toLowerCase() == 'macos') tags.add('apps.mac');
  4965. if (ref.textContent.toLowerCase() == 'linux' || ref.textContent.toLowerCase() == 'unix') tags.add('apps.linux');
  4966. if (ref.textContent.toLowerCase() == 'android') tags.add('apps.android');
  4967. if (ref.textContent.toLowerCase() == 'ios') tags.add('apps.ios');
  4968. }
  4969. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  4970. if (title && !/\(\d+-?bit\)/i.test(title)) {
  4971. if (tags.includes('win64') && !tags.includes('win32')) title += ' (64-bit)';
  4972. if (tags.includes('win32') && !tags.includes('win64')) title += ' (32-bit)';
  4973. }
  4974. if (elementWritable(ref = document.getElementById('title'))) ref.value = title || '';
  4975. });
  4976. if (!weak) {
  4977. addMessage('this domain not supported', 'critical');
  4978. clipBoard.value = '';
  4979. }
  4980. return Promise.reject('this domain not supported');
  4981. } // fillFromText_Apps
  4982.  
  4983. function fillFromText_Ebooks(weak = false) {
  4984. if (messages != null) messages.parentNode.removeChild(messages);
  4985. if (!urlParser.test(clipBoard.value)) {
  4986. addMessage('only URL accepted for this category', 'critical');
  4987. return Promise.reject('only URL accepted for this category');
  4988. }
  4989. sourceUrl = RegExp.$1;
  4990. var description, tags = new TagManager();
  4991. if (sourceUrl.toLowerCase().includes('martinus.cz') || sourceUrl.toLowerCase().includes('martinus.sk'))
  4992. return globalFetch(sourceUrl).then(function(response) {
  4993. function get_detail(x, y) {
  4994. var ref = response.document.querySelector('section#details > div > div > div:first-of-type > div:nth-child(' +
  4995. x + ') > dl:nth-child(' + y + ') > dd');
  4996. return ref != null ? ref.textContent.trim() : null;
  4997. }
  4998.  
  4999. i = response.document.querySelectorAll('article > ul > li > a');
  5000. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  5001. description = joinAuthors(i);
  5002. if ((i = response.document.querySelector('article > h1')) != null) description += ' – ' + i.textContent.trim();
  5003. i = response.document.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
  5004. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  5005. ref.value = description;
  5006. }
  5007.  
  5008. ref = response.document.querySelector('section#description > div');
  5009. if (ref != null) description = html2php(ref).replace(/^\s*\[img\].*?\[\/img\]\s*/i, '').trim();
  5010. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  5011. const translation_map = [
  5012. [/\b(?:originál)/i, 'Original title'],
  5013. [/\b(?:datum|dátum|rok)\b/i, 'Release date'],
  5014. [/\b(?:katalog|katalóg)/i, 'Catalogue #'],
  5015. [/\b(?:stran|strán)\b/i, 'Page count'],
  5016. [/\bjazyk/i, 'Language'],
  5017. [/\b(?:nakladatel|vydavatel)/i, 'Publisher'],
  5018. [/\b(?:doporuč|ODPORÚČ)/i, 'Age rating'],
  5019. ];
  5020. response.document.querySelectorAll('section#details > div > div > div:first-of-type > div > dl').forEach(function(detail) {
  5021. var lbl = detail.children[0].textContent.trim();
  5022. var val = detail.children[1].textContent.trim();
  5023. if (/\b(?:rozm)/i.test(lbl) || /\b(?:vazba|vázba)\b/i.test(lbl)) return;
  5024. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  5025. if (/\b(?:ISBN)\b/i.test(lbl)) {
  5026. sourceUrl = new URL('https://www.worldcat.org/isbn/' + detail.children[1].textContent.trim());
  5027. val = '[url=' + sourceUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
  5028. findOCLC(sourceUrl);
  5029. // } else if (/\b(?:ISBN)\b/i.test(lbl)) {
  5030. // val = '[url=https://www.goodreads.com/search/search?q=' + detail.children[1].textContent.trim() +
  5031. // '&search_type=books]' + detail.children[1].textContent.trim() + '[/url]';
  5032. }
  5033. description += '\n[b]' + lbl + ':[/b] ' + val;
  5034. });
  5035. sourceUrl = new URL(response.finalUrl);
  5036. description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.href + '[/url]';
  5037. writeDescription(description.collapseGaps());
  5038.  
  5039. if ((i = response.document.querySelector('a.mj-product-preview > img')) != null) {
  5040. setCover(i.src.replace(/\?.*/, ''));
  5041. } else if ((i = response.document.querySelector('head > meta[property="og:image"]')) != null) {
  5042. setCover(i.content.replace(/\?.*/, ''));
  5043. }
  5044.  
  5045. response.document.querySelectorAll('dd > ul > li > a').forEach(x => { tags.add(x.textContent) });
  5046. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) {
  5047. ref.value = tags.toString();
  5048. }
  5049. });
  5050. else if (sourceUrl.toLowerCase().includes('goodreads.com')) return globalFetch(sourceUrl).then(function(response) {
  5051. i = response.document.querySelectorAll('a.authorName > span');
  5052. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  5053. description = joinAuthors(i);
  5054. if ((i = response.document.querySelector('h1#bookTitle')) != null) description += ' – ' + i.textContent.trim();
  5055. if ((i = response.document.querySelector('div#details > div.row:nth-of-type(2)')) != null
  5056. && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  5057. ref.value = description;
  5058. }
  5059.  
  5060. var description = [];
  5061. response.document.querySelectorAll('div#description span:last-of-type').forEach(function(node) {
  5062. description = html2php(node, sourceUrl).trim();
  5063. });
  5064. if (description.length > 0 && !description.includes('[quote]')) {
  5065. description = '[quote]' + description.trim() + '[/quote]';
  5066. }
  5067.  
  5068. function strip(str) {
  5069. return typeof str == 'string' ?
  5070. str.replace(/\s{2,}/g, ' ').replace(/[\n\r]+/, '').replace(/\s*\.{3}(?:less|more)\b/g, '').trim() : null;
  5071. }
  5072.  
  5073. response.document.querySelectorAll('div#details > div.row').forEach(k => { description += '\n' + strip(k.innerText) });
  5074. description += '\n';
  5075.  
  5076. response.document.querySelectorAll('div#bookDataBox > div.clearFloats').forEach(function(detail) {
  5077. var lbl = detail.children[0].textContent.trim();
  5078. var val = strip(detail.children[1].textContent);
  5079. if (/\b(?:ISBN)\b/i.test(lbl) && (/\b(\d{13})\b/.test(val) || /\b(\d{10})\b/.test(val))) {
  5080. sourceUrl = new URL('https://www.worldcat.org/isbn/' + RegExp.$1);
  5081. val = '[url=' + sourceUrl.href + ']' + strip(detail.children[1].textContent) + '[/url]';
  5082. findOCLC(sourceUrl);
  5083. }
  5084. description += '\n[b]' + lbl + ':[/b] ' + val;
  5085. });
  5086. if ((ref = response.document.querySelector('span[itemprop="ratingValue"]')) != null) {
  5087. description += '\n[b]Rating:[/b] ' + Math.round(parseFloat(ref.firstChild.textContent) * 20) + '%';
  5088. }
  5089. sourceUrl = new URL(response.finalUrl);
  5090. // if ((ref = response.document.querySelector('div#buyButtonContainer > ul > li > a.buttonBar')) != null) {
  5091. // let u = new URL(ref.href);
  5092. // description += '\n[url=' + sourceUrl.origin + u.pathname + '?' + u.search + ']Libraries[/url]';
  5093. // }
  5094. description += '\n\n[b]More info and reviews:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
  5095. response.document.querySelectorAll('div.clearFloats.bigBox').forEach(function(bigBox) {
  5096. if (bigBox.id == 'aboutAuthor' && (ref = bigBox.querySelector('h2 > a')) != null) {
  5097. description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  5098. if ((ref = bigBox.querySelector('div.bigBoxBody a > div[style*="background-image"]')) != null) {
  5099. }
  5100. if ((ref = bigBox.querySelector('div.bookAuthorProfile__about > span[id]:last-of-type')) != null) {
  5101. description += '\n' + html2php(ref, sourceUrl).trim().replace(/^\[i\]Librarian\s+Note:.*?\[\/i\]\s+/i, '');
  5102. }
  5103. } else if ((ref = bigBox.querySelector('h2 > a[href^="/trivia/"]')) != null) {
  5104. description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  5105. if ((ref = bigBox.querySelector('div.bigBoxContent > div.mediumText')) != null) {
  5106. description += '\n' + ref.firstChild.textContent.trim();
  5107. }
  5108. // } else if ((ref = bigBox.querySelector('h2 > a[href^="/work/quotes/"]')) != null) {
  5109. // description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  5110. // bigBox.querySelectorAll('div.bigBoxContent > div.stacked > span.readable').forEach(function(quote) {
  5111. // description += '\n' + ref.firstChild.textContent.trim();
  5112. // });
  5113. }
  5114. });
  5115. writeDescription(description.collapseGaps());
  5116. if ((ref = response.document.querySelector('div.editionCover > img')) != null) setCover(ref.src.replace(/\?.*/, ''));
  5117. response.document.querySelectorAll('div.elementList > div.left').forEach(tag => { tags.add(tag.textContent.trim()) });
  5118. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  5119. }); else if (sourceUrl.toLowerCase().includes('databazeknih.cz')) {
  5120. if (!sourceUrl.toLowerCase().includes('show=alldesc')) {
  5121. if (!sourceUrl.includes('?')) { sourceUrl += '?show=alldesc' } else { sourceUrl += '&show=alldesc' }
  5122. }
  5123. return globalFetch(sourceUrl).then(function(response) {
  5124. i = response.document.querySelectorAll('span[itemprop="author"] > a');
  5125. if (i != null && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  5126. description = joinAuthors(i);
  5127. if ((i = response.document.querySelector('h1[itemprop="name"]')) != null)
  5128. description += ' – ' + i.textContent.trim();
  5129. i = response.document.querySelector('span[itemprop="datePublished"]');
  5130. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  5131. ref.value = description;
  5132. }
  5133.  
  5134. ref = response.document.querySelector('p[itemprop="description"]');
  5135. if (ref != null) description = html2php(ref, sourceUrl).trim();
  5136. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  5137. const translation_map = [
  5138. [/\b(?:orig)/i, 'Original title'],
  5139. [/\b(?:série)\b/i, 'Series'],
  5140. [/\b(?:vydáno)\b/i, 'Released'],
  5141. [/\b(?:stran)\b/i, 'Page count'],
  5142. [/\b(?:jazyk)\b/i, 'Language'],
  5143. [/\b(?:překlad)/i, 'Translation'],
  5144. [/\b(?:autor obálky)\b/i, 'Cover author'],
  5145. ];
  5146. response.document.querySelectorAll('table.bdetail tr').forEach(function(detail) {
  5147. var lbl = detail.children[0].textContent.trim();
  5148. var val = detail.children[1].textContent.trim();
  5149. if (/(?:žánr|\bvazba)\b/i.test(lbl)) return;
  5150. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  5151. if (/\b(?:ISBN)\b/i.test(lbl) && /\b(\d+(?:-\d+)*)\b/.exec(val) != null) {
  5152. sourceUrl = new URL('https://www.worldcat.org/isbn/' + RegExp.$1.replace(/-/g, ''));
  5153. val = '[url=' + sourceUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
  5154. findOCLC(sourceUrl);
  5155. }
  5156. description += '\n[b]' + lbl + '[/b] ' + val;
  5157. });
  5158.  
  5159. sourceUrl = new URL(response.finalUrl);
  5160. description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
  5161. writeDescription(description.collapseGaps());
  5162.  
  5163. if ((ref = response.document.querySelector('div#icover_mid > a')) != null) setCover(ref.href.replace(/\?.*/, ''));
  5164. if ((ref = response.document.querySelector('div#lbImage')) != null && /\burl\("(.*)"\)/i.test(i.style.backgroundImage)) {
  5165. setCover(RegExp.$1.replace(/\?.*/, ''));
  5166. }
  5167.  
  5168. response.document.querySelectorAll('h5[itemprop="genre"] > a').forEach(tag => { tags.add(tag.textContent.trim()) });
  5169. response.document.querySelectorAll('a.tag').forEach(tag => { tags.add(tag.textContent.trim()) });
  5170. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  5171. });
  5172. }
  5173. if (!weak) {
  5174. addMessage('domain not supported', 'critical');
  5175. clipBoard.value = '';
  5176. }
  5177. return Promise.reject('domain not supported');
  5178.  
  5179. function joinAuthors(nodeList) {
  5180. if (typeof nodeList != 'object') return null;
  5181. return Array.from(nodeList).map(it => it.textContent.trim()).join(' & ');
  5182. }
  5183.  
  5184. function findOCLC(url) {
  5185. if (!url) return false;
  5186. var oclc = document.querySelector('input[name="oclc"]');
  5187. if (!elementWritable(oclc)) return false;
  5188. globalFetch(url).then(function(dom) {
  5189. var ref = dom.querySelector('tr#details-oclcno > td:last-of-type');
  5190. if (ref != null) oclc.value = ref.textContent.trim();
  5191. });
  5192. return true;
  5193. }
  5194. } // fillFromText_Ebooks
  5195.  
  5196. function preview(n) {
  5197. if (!prefs.auto_preview) return;
  5198. var btn = document.querySelector('input.button_preview_' + n + '[type="button"][value="Preview"]');
  5199. if (btn != null) btn.click();
  5200. }
  5201.  
  5202. function writeDescription(desc) {
  5203. if (typeof desc != 'string') return;
  5204. if (elementWritable(ref = document.querySelector('textarea#desc')
  5205. || document.querySelector('textarea#description'))) ref.value = desc;
  5206. if ((ref = document.getElementById('body')) != null && !ref.disabled) {
  5207. if (ref.value.length > 0) ref.value += '\n\n';
  5208. ref.value += desc;
  5209. }
  5210. }
  5211.  
  5212. function queryItunesAPI(key, params) {
  5213. return queryGenericAPI('itunes.apple.com', key, params);
  5214. }
  5215. function queryDeezerAPI(key, params) {
  5216. return queryGenericAPI('api.deezer.com', key, params);
  5217. }
  5218. function queryDiscogsAPI(key, params) {
  5219. if (prefs.discogs_key && prefs.discogs_secret) {
  5220. var hdr = { Authorization: 'Discogs key=' + prefs.discogs_key + ', secret=' + prefs.discogs_secret };
  5221. } else if (discogs_token) hdr = { Authorization: 'Discogs token=' + discogs_token };
  5222. return queryGenericAPI('api.discogs.com', key, params, hdr);
  5223. }
  5224. function queryMusicBrainzAPI(key, params) {
  5225. return queryGenericAPI('musicbrainz.org', 'ws/2/' + key + '/', Object.assign({ fmt: 'json' }, params));
  5226. }
  5227. function querySpotifyAPI(key, params) {
  5228. return key ? setOauth2Token().then(credentials => queryGenericAPI('api.spotify.com', 'v1/' + key, params, {
  5229. 'Authorization': credentials.token_type + ' ' + credentials.access_token,
  5230. })) : Promise.reject('No API expression');
  5231.  
  5232. function setOauth2Token() {
  5233. if (isTokenValid()) return Promise.resolve(spotifyCredentials);
  5234. if (!spotify_clientid || !spotify_clientsecret) return Promise.reject('Spotify credentials not configured');
  5235. const data = new URLSearchParams({
  5236. 'grant_type': 'client_credentials',
  5237. });
  5238. return globalFetch('https://accounts.spotify.com/api/token', { responseType: 'json', headers: {
  5239. 'Authorization': 'Basic ' + btoa(spotify_clientid + ':' + spotify_clientsecret),
  5240. } }, data).then(function(response) {
  5241. spotifyCredentials = response.response;
  5242. spotifyCredentials.expires = new Date().getTime() + spotifyCredentials.expires_in;
  5243. return isTokenValid() ? spotifyCredentials : Promise.reject('Invalid token');
  5244. });
  5245. }
  5246.  
  5247. function isTokenValid() {
  5248. return spotifyCredentials.token_type && spotifyCredentials.access_token
  5249. && spotifyCredentials.expires >= (new Date().getTime()) / 1000 + 30;
  5250. }
  5251. }
  5252. function queryLastFmAPI(method, params) {
  5253. return lastfm_api_key ? queryGenericAPI('ws.audioscrobbler.com', '2.0/', Object.assign({
  5254. method: method,
  5255. api_key: lastfm_api_key,
  5256. format: 'json',
  5257. }, params || {})) : Promise.reject('Last.fm API key not configured');
  5258. }
  5259. function queryTidalAPI(key, params, countryCode) {
  5260. if (!key) return Promise.reject('API action not defined');
  5261. if (typeof params != 'object') params = {};
  5262. params.deviceType = 'BROWSER';
  5263. params.countryCode = countryCode;
  5264. return setOauth2Token().then(function(token) {
  5265. if (!params.countryCode) params.countryCode = token.user.countryCode || 'US';
  5266. return queryGenericAPI('listen.tidal.com', 'v1/pages/'.concat(key), params, {
  5267. 'Authorization': token.token_type + ' ' + token.access_token,
  5268. });
  5269. }).catch(function(reason) {
  5270. console.warn('Tidal Oauth2 failed:', reason);
  5271. return setSession().then(function(session) {
  5272. if (!params.countryCode) params.countryCode = session.countryCode || 'US';
  5273. return queryGenericAPI('listen.tidal.com', 'v1/pages/'.concat(key), params, {
  5274. 'X-Tidal-SessionId': session.sessionId,
  5275. });
  5276. });
  5277. });
  5278.  
  5279. function setOauth2Token() {
  5280. if (isTokenValid()) return Promise.resolve(tidalToken);
  5281. return Promise.reject('Not implemented');
  5282. const code_verifier = randomString(128);
  5283. const state = 'TIDAL_'.concat(randomString(169));
  5284. const scope = 'r_usr w_usr';
  5285. var data;
  5286. return (prefs.tidal_clientid ? Promise.resolve(prefs.tidal_clientid) : getTidalSecrets().then(function(response) {
  5287. const rx = /"(\w{40})":"(\w{16})"/g;
  5288. if ((i = response.responseText.match(rx)) == null || !rx.test(i.shift())) return Promise.reject('not found');
  5289. GM_setValue('tidal_clientid', prefs.tidal_clientid = RegExp.$2);
  5290. console.debug('Successfully configured Tidal client id:', prefs.tidal_clientid);
  5291. return prefs.tidal_clientid;
  5292. }).catcj(function(reason) {
  5293. reason = 'Client Id auto detection failed ('.concat(reason, '), set it manually (tidal_clientid)');
  5294. alert(reason);
  5295. return Promise.reject(reason);
  5296. })).then(function(client_id) {
  5297. data = new URLSearchParams({
  5298. appMode: 'WEB',
  5299. client_id: client_id,
  5300. client_unique_key: getClientKey(),
  5301. code_challenge: generateCodeChallenge(code_verifier),
  5302. code_challenge_method: 'S256',
  5303. lang: 'en',
  5304. redirect_uri: 'https://listen.tidal.com/login/auth',
  5305. response_type: 'code',
  5306. restrictSignup: true,
  5307. scope: scope,
  5308. state: state,
  5309. utm_banner: 'na',
  5310. utm_campaign: 'na',
  5311. utm_content: 'left_menu',
  5312. utm_medium: 'web_player',
  5313. utm_source: 'tidal',
  5314. });
  5315. return globalFetch('https://login.tidal.com/authorize?'.concat(data.toString()), { method: 'HEAD' });
  5316. }).then(function(response) {
  5317. if (!/\btdluo=(.*?);/im.test(response.responseHeaders)) return Promise.reject('Header parsing error');
  5318. response = JSON.parse(atob(RegExp.$1));
  5319. var oauth2Client = response.oauth2Client;
  5320. //var accessToken = JSON.parse(atoB(response.accessToken));
  5321. var code = btoa(JSON.stringify({
  5322. alg: 'HS256',
  5323. type: 'o2_code',
  5324. //uid: ???,
  5325. scope: scope,
  5326. exp: new Date().getTime() + oauth2Client.expiresIn, // ?
  5327. //callbackUriId: ???,
  5328. //cid: ???,
  5329. cuk: oauth2Client.clientUniqueKey,
  5330. //challengeId: ???,
  5331. }));
  5332. data = new URLSearchParams({
  5333. code: code,
  5334. state: oauth2Client.state,
  5335. lang: response.lang,
  5336. appMode: 'web',
  5337. });
  5338. return globalFetch(response.oauth2Client.redirectUri + '?' + data.toString(), { method: 'HEAD' }).then(response => code);
  5339. }).then(function(code) {
  5340. data = new URLSearchParams({
  5341. client_id: oauth2Client.clientId,
  5342. client_unique_key: oauth2Client.clientUniqueKey,
  5343. code: code,
  5344. code_verifier: code_verifier,
  5345. grant_type: 'authorization_code',
  5346. redirect_uri: 'https://listen.tidal.com/login/auth',
  5347. scope: scope,
  5348. });
  5349. return globalFetch('https://login.tidal.com/oauth2/token', { responseType: 'json' }, data);
  5350. }).then(function(response) {
  5351. if (typeof response.response != 'object') return Promise.reject('Invalid token');
  5352. tidalToken = response.response;
  5353. tidalToken.expires = new Date().getTime() + tidalToken.expires_in;
  5354. if (!isTokenValid()) return Promise.reject('Invalid token');
  5355. console.debug('Tidal token successfully set:', tidalToken);
  5356. return tidalToken;
  5357. });
  5358. }
  5359. function setSession() {
  5360. if (isSessionValid(tidalSession)) return Promise.resolve(tidalSession);
  5361. if (!prefs.tidal_userid || !prefs.tidal_userpassword) return Promise.reject('Insufficient Tidal credentials');
  5362. const deviceTokens = [
  5363. /* 0 */ 'BI218mwp9ERZ3PFI', // browser | Streams lossless quality
  5364. /* 1 */ 'wdgaB1CilGA-S_s2', // browser | Streams HIGH/LOW Quality over RTMP, FLAC and Videos over HTTP, but many Lossless Streams are encrypted.
  5365. /* 2 */ '4zx46pyr9o8qZNRw', // browser(?) | other quality
  5366. /* 3 */ 'kgsOOmYk3zShYrNP', // Android | All Streams are HTTP Streams. Correct numberOfVideos in Playlists (best Token to use)
  5367. /* 4 */ 'GvFhCVAYp3n43EN3', // iOS | Same as Android Token, but uses ALAC instead of FLAC
  5368. /* 5 */ '_DSTon1kC8pABnTw', // iOS | Same as Android Token, but uses ALAC instead of FLAC
  5369. /* 6 */ '4zx46pyr9o8qZNRw', // native | Same as Android Token, but FLAC streams are encrypted
  5370. /* 7 */ 'BI218mwp9ERZ3PFI', // audirvana | Like Android Token, supports MQA, but returns 'numberOfVideos = 0' in Playlists
  5371. /* 8 */ 'wc8j_yBJd20zOmx0', // amarra | Like Android Token, but returns 'numberOfVideos = 0' in Playlists
  5372. /* 9 */ 'P5Xbeo5LFvESeDy6', // Like Android Token, but returns 'numberOfVideos = 0' in Playlists
  5373. /* 10 */ '_KM2HixcUBZtmktH', // Same as previous
  5374. /* 11 */ 'oIaGpqT_vQPnTr0Q', // Same, but uses RTMP for HIGH/LOW Quality
  5375. ];
  5376. return (prefs.tidal_token ? Promise.resolve(prefs.tidal_token) : getTidalSecrets().then(function(response) {
  5377. if (!/"(\w{40})":"(\w{40})"/.test(response.responseText)) return Promise.reject('Token not found');
  5378. GM_setValue('tidal_token', prefs.tidal_token = RegExp.$2);
  5379. console.debug('Successfully configured Tidal token:', prefs.tidal_token);
  5380. return prefs.tidal_token;
  5381. }).catch(function(reason) {
  5382. console.warn('Tidal token detection fail (' + reason + ')');
  5383. return undefined;
  5384. })).then(function(tidal_token) {
  5385. const data = new URLSearchParams({
  5386. username: prefs.tidal_userid,
  5387. password: prefs.tidal_userpassword,
  5388. clientUniqueKey: getClientKey(),
  5389. clientVersion: '1.0',
  5390. token: deviceTokens[2],
  5391. });
  5392. return globalFetch('https://api.tidalhifi.com/v1/login/username', { responseType: 'json', headers: {
  5393. 'X-Tidal-Token': tidal_token,
  5394. } }, data);
  5395. }).then(function(response) {
  5396. if (!isSessionValid(response.response)) return Promise.reject('Invalid session');
  5397. tidalSession = response.response;
  5398. console.debug('Tidal session successfully established:', tidalSession);
  5399. return tidalSession;
  5400. });
  5401. }
  5402. function uuidv4() {
  5403. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  5404. var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
  5405. return v.toString(16);
  5406. });
  5407. }
  5408. function randomString(length) {
  5409. const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  5410. var text = "";
  5411. for (var i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length));
  5412. return text;
  5413. }
  5414. function generateCodeChallenge(code_verifier) {
  5415. return code_challenge = base64URL(CryptoJS.SHA256(code_verifier))
  5416. }
  5417. function base64URL(string) {
  5418. return string.toString(CryptoJS.enc.Base64).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
  5419. }
  5420. function getClientKey() {
  5421. if (!(prefs.tidal_clientkey = GM_getValue('tidal_clientkey'))) GM_setValue('tidal_clientkey', prefs.tidal_clientkey = uuidv4());
  5422. return prefs.tidal_clientkey;
  5423. }
  5424. function getTidalSecrets() {
  5425. return globalFetch('https://listen.tidal.com/app.61d2e10fa09e5075c5ad.chunk.js', { responseType: 'text' });
  5426. }
  5427. function isTokenValid() {
  5428. return tidalToken.token_type && tidalToken.access_token && tidalToken.expires >= (new Date().getTime()) / 1000 + 30;
  5429. }
  5430. function isSessionValid(session) {
  5431. return typeof session == 'object' && session.userId > 0 && session.sessionId;
  5432. }
  5433. }
  5434.  
  5435. function queryGenericAPI(domain, key, params, headers) {
  5436. if (!key) return Promise.reject(new Error('Keyword missing'));
  5437. var retryCount = 0;
  5438. return new Promise(function(resolve, reject) {
  5439. var url = 'https://' + domain + '/' + key;
  5440. var query = new URLSearchParams(params || undefined).toString();
  5441. if (query.length > 0) url += '?' + query;
  5442. if (typeof headers != 'object') headers = {};
  5443. headers.Accept = 'application/json';
  5444. queryInternal();
  5445.  
  5446. function queryInternal() {
  5447. GM_xmlhttpRequest({
  5448. method: 'GET',
  5449. url: url,
  5450. responseType: 'json',
  5451. headers: headers,
  5452. onload: function(response) {
  5453. if (response.status == 503) return http503Handler(1000, response, 'onload');
  5454. if (response.status >= 200 && response.status < 400) resolve(response.response);
  5455. else reject(defaultErrorHandler(response));
  5456. },
  5457. onerror: error => error.status == 503 ? http503Handler(1000, error, 'onerror')
  5458. : reject(defaultErrorHandler(error)),
  5459. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  5460. });
  5461. }
  5462. function http503Handler(delay, response, event) {
  5463. if (retryCount++ > 10) reject(defaultErrorHandler(response));
  5464. setTimeout(function() { queryInternal() }, delay);
  5465. console.debug('[UA] queryGenericAPI encountered HTTP/503 error for url ' + url + '; event: ' + event);
  5466. }
  5467. });
  5468. }
  5469.  
  5470. function getMusicBrainzCovers(mbid) {
  5471. return searchInternal('release', mbid).then(covers => covers || searchMaster(), searchMaster);
  5472.  
  5473. function searchInternal(entity, mbid) {
  5474. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  5475. method: 'GET',
  5476. url: 'https://coverartarchive.org/'.concat(entity, '/', mbid),
  5477. responseType: 'json',
  5478. onload: function(response) {
  5479. if (response.status == 404) return resolve(null);
  5480. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  5481. var images = response.response.images
  5482. .filter(image => urlParser.test(image.image) && image.isfront
  5483. || Array.isArray(image.types) && image.types.includesCaseless('Front'))
  5484. .map(image => image.image);
  5485. resolve(images.length > 0 ? [response.response.release, images] : null);
  5486. },
  5487. onerror: error => reject(defaultErrorHandler(error)),
  5488. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  5489. }));
  5490. }
  5491. function searchMaster() {
  5492. return queryMusicBrainzAPI('release/' + mbid, { inc: 'release-groups' })
  5493. .then(release => searchInternal('release-group', release['release-group'].id));
  5494. }
  5495. }
  5496.  
  5497. function setCover(url) {
  5498. if (!urlParser.test(url)) return Promise.reject('Image url not valid');
  5499. var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  5500. if (!elementWritable(image)) return Promise.reject('Image input not available');
  5501. return testImageUrl(url).then(function(url) {
  5502. if (!isNWCD) {
  5503. image.value = url;
  5504. coverPreview(image, url);
  5505. if (prefs.auto_rehost_cover && !url.toLowerCase().startsWith(imghostOrigin)) {
  5506. //if (rehostItBtn != null) rehostItBtn.click(); else {
  5507. image.disabled = true;
  5508. rehost2PTPIMG([url])
  5509. .then(urls => urls.length > 0 ? (image.value = urls[0]) : url)
  5510. .catch(reason => { /*alert(reason)*/ addMessage('PTPIMG '.concat(reason), 'warning') })
  5511. .then(url => { image.disabled = false; return url });
  5512. //}
  5513. }
  5514. return url;
  5515. } else return uploadToImagehost(url).then(function(result) {
  5516. image.value = result.url;
  5517. setTimeout(function() { coverPreview(image, result.url) }, 2000);
  5518. return result.url;
  5519. });
  5520. });
  5521. }
  5522.  
  5523. function elementWritable(elem) {
  5524. return elem != null && !elem.disabled && (overwrite || elem.value == '' || !isRED && elem.value == '---');
  5525. }
  5526. } // fillFromText
  5527.  
  5528. function addMessage(text, cls) {
  5529. switch (cls) {
  5530. case 'info': var prefix = 'Info'; break;
  5531. case 'notice': prefix = 'Notice'; break;
  5532. case 'warning': prefix = 'Warning'; break;
  5533. case 'critical': prefix = 'FATAL'; break;
  5534. default: return null;
  5535. }
  5536. if ((messages = document.getElementById('UA-messages')) == null) {
  5537. let ua = document.getElementById('upload assistant');
  5538. if (ua == null) return null;
  5539. let tr = document.createElement('TR');
  5540. tr.id = 'UA-messages';
  5541. ua.firstElementChild.append(tr);
  5542. var td = document.createElement('TD');
  5543. td.colSpan = 2;
  5544. td.className = 'ua-messages-bg';
  5545. tr.append(td);
  5546. } else {
  5547. td = messages.firstElementChild;
  5548. if (td == null) return null;
  5549. }
  5550. var div = document.createElement('DIV');
  5551. div.classList.add('ua-messages', 'ua-'.concat(cls));
  5552. div[text instanceof HTML ? 'innerHTML' : 'textContent'] = prefix.concat(': ', text);
  5553. return td.appendChild(div);
  5554. }
  5555.  
  5556. function defaultErrorHandler(response) {
  5557. var e = 'XHR: error readyState=' + response.readyState + ', status=' + response.status;
  5558. if (response.statusText) e += ' (' + response.statusText + ')';
  5559. if (response.error) e += ' (' + response.error + ')';
  5560. console.error('XHR error:', response);
  5561. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  5562. return e;
  5563. }
  5564. function defaultTimeoutHandler(response) {
  5565. const e = 'XHR: timeout';
  5566. console.error('XHR timeout:', response);
  5567. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  5568. return e;
  5569. }
  5570.  
  5571. function setHandlers() {
  5572. if (prefs.cleanup_descriptions) ['form.create_form', 'form.edit_form', 'form#request_form'].forEach(function(sel) {
  5573. if ((ref = document.querySelector(sel)) != null) ref.addEventListener('submit', cleanupDescriptions);
  5574. });
  5575.  
  5576. if ((ref = document.getElementById('yadg_input')) != null) ref.ondrop = clear0;
  5577.  
  5578. if (!isNWCD) {
  5579. if ((ref = document.getElementById('image') || document.querySelector('input[name="image"]')) != null) {
  5580. ref.ondragover = voidDragHandler0;
  5581. ref.ondblclick = imageClear;
  5582. ref.ondrop = imageDropHandler;
  5583. ref.onpaste = imagePasteHandler;
  5584. }
  5585.  
  5586. rehostItBtn = document.querySelector('input.rehost_it_cover[type="button"]');
  5587. if (prefs.dragdrop_patch_to_ptpimgit && rehostItBtn != null) {
  5588. rehostItBtn.dataset.caption = rehostItBtn.value;
  5589. rehostItBtn.ondragover = voidDragHandler0;
  5590. rehostItBtn.ondrop = ptpimgItHandler;
  5591. }
  5592. }
  5593. // Now rape OPS upload form, but only gently
  5594. if (isOPS && isUpload && (ref = document.getElementById('remaster')) != null) {
  5595. ref.checked = true;
  5596. if (!isAddFormat && prefs.ops_always_edition) {
  5597. elem = ref.parentNode.parentNode;
  5598. elem.style.display = 'none';
  5599. if ((ref = document.querySelector('span#year_label_not_remaster')) != null) ref.textContent = 'Initial year:';
  5600. if ((ref = document.querySelector('tr#edition_year > td.label')) != null) ref.textContent = 'Edition year:';
  5601. if ((ref = document.querySelector('tr#edition_title > td.label')) != null) ref.textContent = 'Edition title:';
  5602. if ((ref = document.getElementById('label_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  5603. if ((ref = document.getElementById('catalogue_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  5604. document.querySelectorAll('table#edition_information > tbody > tr')
  5605. .forEach(tr => { elem.parentNode.insertBefore(tr, elem) });
  5606. } else Remaster();
  5607. }
  5608.  
  5609. Array.from(document.getElementsByTagName('textarea')).forEach(function(textArea) {
  5610. if (textArea.className == 'ua-input') return;
  5611. textArea.ondragover = voidDragHandler0;
  5612. textArea.ondrop = descDropHandler;
  5613. textArea.onpaste = descPasteHandler;
  5614. });
  5615. }
  5616.  
  5617. function html2php(node, url, tagChain = []) {
  5618. if (!node || typeof node != 'object') return null;
  5619. switch (node.nodeType) {
  5620. case Node.ELEMENT_NODE: {
  5621. let tags = [], _tags = [], text = [];
  5622. for (let i = 0; i < 5; ++i) text[i] = '';
  5623. switch (node.nodeName) {
  5624. case 'P':
  5625. text[0] = '\n'; text[4] = '\n';
  5626. break;
  5627. case 'DIV':
  5628. text[0] = '\n\n'; text[4] = '\n\n';
  5629. break;
  5630. case 'DT':
  5631. text[4] = '\n';
  5632. break;
  5633. case 'DD':
  5634. text[4] = '\n';
  5635. if (isRED) addTag('pad=0|0|0|30'); else text[0] = ' ';
  5636. break;
  5637. case 'LABEL':
  5638. addTag('b');
  5639. text[0] = '\n\n';
  5640. break;
  5641. case 'BR':
  5642. return '\n';
  5643. case 'HR':
  5644. return isRED ? '[hr]' : '\n';
  5645. case 'B': case 'STRONG':
  5646. addTag('b');
  5647. break;
  5648. case 'I': case 'EM': case 'DFN': case 'CITE': case 'VAR':
  5649. addTag('i');
  5650. break;
  5651. case 'U': case 'INS':
  5652. addTag('u');
  5653. break;
  5654. case 'DEL':
  5655. addTag('s');
  5656. break;
  5657. case 'CODE': case 'SAMP': case 'KBD':
  5658. addTag('code');
  5659. text[2] = node.textContent;
  5660. break;
  5661. case 'PRE':
  5662. addTag('pre');
  5663. text[2] = node.textContent;
  5664. break;
  5665. case 'BLOCKQUOTE': case 'QUOTE':
  5666. addTag('quote');
  5667. break;
  5668. case 'Q':
  5669. text[1] = '"'; text[3] = '"';
  5670. break;
  5671. case 'H1':
  5672. addTag('size=5'); addTag('b');
  5673. text[0] = '\n\n'; text[4] = '\n\n';
  5674. break;
  5675. case 'H2':
  5676. addTag('size=4'); addTag('b');
  5677. text[0] = '\n\n'; text[4] = '\n\n';
  5678. break;
  5679. case 'H3':
  5680. addTag('size=3'); addTag('b');
  5681. text[0] = '\n\n'; text[4] = '\n\n';
  5682. break;
  5683. case 'H4': case 'H5': case 'H6':
  5684. addTag('b');
  5685. text[0] = '\n\n'; text[4] = '\n\n';
  5686. break;
  5687. case 'SMALL':
  5688. addTag('size=1');
  5689. break;
  5690. case 'OL': case 'UL':
  5691. _tags.push(node.nodeName.toLowerCase());
  5692. break;
  5693. case 'DL':
  5694. _tags.push(node.nodeName.toLowerCase());
  5695. break;
  5696. case 'LI':
  5697. switch (tagChain.reverse().find(tag => /^[ou]l$/.test(tag))) {
  5698. case 'ol': text[0] = '[#] '; text[4] = '\n'; break;
  5699. case 'ul': text[0] = '[*] '; text[4] = '\n'; break;
  5700. default: return '';
  5701. }
  5702. break;
  5703. case 'A': {
  5704. addTag('url=' + removeRedirect(node.href));
  5705. break;
  5706. }
  5707. case 'IMG':
  5708. addTag('img');
  5709. text[2] = node.dataset.src || node.src;
  5710. break;
  5711. case 'DETAILS': {
  5712. let summary = node.querySelector('summary');
  5713. summary = summary != null ? '='.concat(summary.textContent.trim()) : '';
  5714. addTag('hide' + summary);
  5715. break;
  5716. }
  5717. case 'AUDIO': case 'BASE': case 'BUTTON': case 'CANVAS': case 'COL': case 'COLGROUP': case 'DATALIST':
  5718. case 'DIALOG': case 'EMBED': case 'FIELDSET': case 'FORM': case 'HEAD': case 'INPUT': case 'LEGEND':
  5719. case 'LINK': case 'MAP': case 'META': case 'METER': case 'NOSCRIPT': case 'OBJECT': case 'OPTGROUP':
  5720. case 'OPTION': case 'PARAM': case 'PROGRESS': case 'SELECT': case 'SOURCE': case 'STYLE': case 'SUMMARY':
  5721. case 'SVG': case 'TEMPLATE': case 'TEXTAREA': case 'TITLE': case 'TRACK': case 'VIDEO':
  5722. return '';
  5723. }
  5724. if (['left', 'center', 'right'].some(al => node.style.textAlign.toLowerCase() == al)) {
  5725. addTag('align=' + node.style.textAlign.toLowerCase());
  5726. }
  5727. if (node.style.fontWeight >= 700) addTag('b');
  5728. switch (node.style.fontStyle.toLowerCase()) {
  5729. case 'italic': addTag('i'); break;
  5730. }
  5731. switch (node.style.textDecorationLine.toLowerCase()) {
  5732. case 'underline': addTag('u'); break;
  5733. case 'line-through': addTag('s'); break;
  5734. }
  5735. if (node.style.color) {
  5736. ctxt.fillStyle = elem.style.color;
  5737. if (ctxt.fillStyle != '#000000' && /^#(?:[a-f0-8]{2}){3,4}$/i.test(ctxt.fillStyle)) {
  5738. addTag('color=' + ctxt.fillStyle);
  5739. }
  5740. }
  5741. if (!text[2]) node.childNodes.forEach(function(node) {
  5742. text[2] += html2php(node, url, tagChain.concat(tags.concat(_tags).map(tag => tag.replace(/=.*$/, ''))));
  5743. });
  5744. if (node.nodeName = 'A' && text[2].trim().length <= 0) {
  5745. text[2] = removeRedirect(node.href);
  5746. tags.splice(-1, 1, 'url');
  5747. }
  5748. return text[0].concat((text[1] || text[2] || text[3] ? tags.map(tag => '[' + tag + ']').join('').concat(text[1],
  5749. text[2], text[3], tags.reverse().map(tag => '[/' + tag.replace(/=.*$/, '') + ']').join('')) : ''), text[4]);
  5750.  
  5751. function addTag(tag) {
  5752. if (tagChain.concat(tags.map(tag => tag.replace(/=.*$/, ''))).includesCaseless(tag.replace(/=.*$/, ''))) return;
  5753. tags.push(tag);
  5754. }
  5755. }
  5756. case Node.TEXT_NODE:
  5757. return node.wholeText.replace(/\s+/g, ' ');
  5758. case Node.DOCUMENT_NODE:
  5759. return html2php(node.body, url);
  5760. }
  5761. return '';
  5762. }
  5763.  
  5764. function coverPreview(anchor, src, size) {
  5765. if (!prefs.auto_preview_cover || anchor.parentNode.previousElementSibling == null) return;
  5766. if ((child = document.getElementById('cover-preview')) == null) {
  5767. if (!(anchor instanceof HTMLElement)) return;
  5768. elem = document.createElement('div');
  5769. elem.style = 'padding-top: 10px; float: right; width: 90%;';
  5770. child = document.createElement('img');
  5771. child.id = 'cover-preview';
  5772. elem.append(child);
  5773. var div = document.createElement('div');
  5774. div.id = 'cover-size';
  5775. elem.append(div);
  5776. anchor.parentNode.previousElementSibling.append(document.createElement('br'));
  5777. anchor.parentNode.previousElementSibling.append(elem);
  5778. }
  5779. div = div || document.getElementById('cover-size');
  5780. if (urlParser.test(src)) {
  5781. child.onload = function(evt) {
  5782. this.onload = null;
  5783. if (!this.naturalWidth || !this.naturalHeight) return; // invalid image
  5784. (size > 0 ? Promise.resolve(size) : getRemoteFileSize(src)).then(function(size) {
  5785. var warn = prefs.huge_image_warning && size > prefs.huge_image_warning * 2**20;
  5786. var html = warn ? '<strong style="color: #ff4c4c;">' + formattedSize(size) + '</strong>' : formattedSize(size);
  5787. div.innerHTML = this.naturalWidth + '×' + this.naturalHeight + ' (' + html + ')';
  5788. if (!warn) return;
  5789. addMessage('high cover size (' + formattedSize(size) + ')', 'notice');
  5790. }.bind(this)).catch(reason => { div.textContent = this.naturalWidth + '×' + this.naturalHeight });
  5791. };
  5792. child.src = src;
  5793. } else div.textContent = child.src = '';
  5794. }
  5795.  
  5796. function getRemoteFileSize(url) {
  5797. return new Promise(function(resolve, reject) {
  5798. var imageSize, abort = GM_xmlhttpRequest({
  5799. method: 'GET', url: url, responseType: 'arraybuffer',
  5800. onreadystatechange: function(response) {
  5801. if (imageSize || response.readyState < XMLHttpRequest.HEADERS_RECEIVED
  5802. || !/^Content-Length:\s*(\d+)\b/im.test(response.responseHeaders)) return;
  5803. if (!(imageSize = parseInt(RegExp.$1))) return;
  5804. resolve(imageSize);
  5805. abort.abort();
  5806. },
  5807. onload: function(response) { // fail-safe
  5808. if (imageSize) return;
  5809. if (response.status < 200 || response.status >= 400) return reject(new Error('Image not accessible'));
  5810. resolve(response.responseText.length /*response.response.byteLength*/);
  5811. },
  5812. onerror: response => reject(new Error('Image not accessible')),
  5813. ontimeout: response => reject(new Error('Image not accessible')),
  5814. });
  5815. });
  5816. }
  5817.  
  5818. function removeRedirect(uri) {
  5819. return typeof uri != 'string' ? null : [
  5820. 'anonymz.com/?', 'anonymz.com?',
  5821. 'anonym.to/?', 'anonym.to?',
  5822. 'nullrefer.com/?',
  5823. 'dereferer.me/?',
  5824. 'reho.st/',
  5825. ].reduce(function(acc, it) {
  5826. if (acc.toLowerCase().startsWith('https://' + it)) return acc.slice(it.length + 8);
  5827. if (acc.toLowerCase().startsWith('http://' + it)) return acc.slice(it.length + 7);
  5828. return acc;
  5829. }, uri);
  5830. }
  5831.  
  5832. function cleanupDescriptions(evt) {
  5833. descriptionFields.forEach(function(ID) {
  5834. if ((ref = evt.target.querySelector('textarea#' + ID)) == null || ref.value.length <= 0) return;
  5835. var clean = ref.value
  5836. .replace(/[ \t]*Vinyl rip by \[color=\S+\]\[\/color\]\s*/im, '')
  5837. .replace(/\[u\]Lineage:\[\/u\]\n\n/i, '')
  5838. for (var i = 0; i < 3; ++i) clean = clean.replace(/\s*\[(\w+)(?:=([^\[\]]*))?\]\[\/\1\]/gm, '');
  5839. const drMatch = [
  5840. /(^| \| )DR(\d+)$\s+/m,
  5841. /(?:^| \| )DR(\d+)(?=$| \| )/gm,
  5842. ];
  5843. var m = /\[hide=DR(\d+)?\]\[pre\]/i.exec(clean);
  5844. //if (m != null && drMatch[0].test(clean) && RegExp.$2 == m[1]) clean = clean.replace(drMatch[0], '$1');
  5845. if (m != null && drMatch[1].test(clean) && RegExp.$1 == m[1]) clean = clean.replace(drMatch[1], '');
  5846. ref.value = clean.replace(/(?:[ \t]*\r?\n){3,}/g, '\n\n').replace(/[ \t]+$/gm, '').trim();
  5847. });
  5848. return true;
  5849. }
  5850.  
  5851. function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*'.concat(expr, '[^\\(\\)]*\\)$'), 'i') }
  5852. function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*'.concat(expr, '[^\\[\\]]*\\]$'), 'i') }
  5853.  
  5854. function notMonospaced(str) {
  5855. return /[\u0080-\u009F]/.test(str)
  5856. // || /[\u0000-\u001F]/.test(str) // Control character
  5857. // || /[\u0020-\u007F]/.test(str) // Basic Latin
  5858. // || /[\u0080-\u00FF]/.test(str) // Latin-1 Supplement
  5859. // || /[\u0100-\u017F]/.test(str) // Latin Extended-A
  5860. // || /[\u0180-\u024F]/.test(str) // Latin Extended-B
  5861. // || /[\u0250-\u02AF]/.test(str) // IPA Extensions
  5862. || /[\u02B0-\u02FF]/.test(str) // Spacing Modifier Letters
  5863. || /[\u0300-\u036F]/.test(str) // Combining Diacritical Marks
  5864. || /[\u0370-\u03FF]/.test(str) // Greek and Coptic
  5865. || /[\u0400-\u04FF]/.test(str) // Cyrillic
  5866. || /[\u0500-\u052F]/.test(str) // Cyrillic Supplement
  5867. || /[\u0530-\u058F]/.test(str) // Armenian
  5868. || /[\u0590-\u05FF]/.test(str) // Hebrew
  5869. || /[\u0600-\u06FF]/.test(str) // Arabic
  5870. || /[\u0700-\u074F]/.test(str) // Syriac
  5871. || /[\u0750-\u077F]/.test(str) // Arabic Supplement
  5872. || /[\u0780-\u07BF]/.test(str) // Thaana
  5873. || /[\u07C0-\u07FF]/.test(str) // NKo
  5874. || /[\u0800-\u083F]/.test(str) // Samaritan
  5875. || /[\u0840-\u085F]/.test(str) // Mandaic
  5876. || /[\u0860-\u086F]/.test(str) // Syriac Supplement
  5877. || /[\u08A0-\u08FF]/.test(str) // Arabic Extended-A
  5878. || /[\u0900-\u097F]/.test(str) // Devanagari
  5879. || /[\u0980-\u09FF]/.test(str) // Bengali
  5880. || /[\u0A00-\u0A7F]/.test(str) // Gurmukhi
  5881. || /[\u0A80-\u0AFF]/.test(str) // Gujarati
  5882. || /[\u0B00-\u0B7F]/.test(str) // Oriya
  5883. || /[\u0B80-\u0BFF]/.test(str) // Tamil
  5884. || /[\u0C00-\u0C7F]/.test(str) // Telugu
  5885. || /[\u0C80-\u0CFF]/.test(str) // Kannada
  5886. || /[\u0D00-\u0D7F]/.test(str) // Malayalam
  5887. || /[\u0D80-\u0DFF]/.test(str) // Sinhala
  5888. || /[\u0E00-\u0E7F]/.test(str) // Thai
  5889. || /[\u0E80-\u0EFF]/.test(str) // Lao
  5890. || /[\u0F00-\u0FFF]/.test(str) // Tibetan
  5891. || /[\u1000-\u109F]/.test(str) // Myanmar
  5892. || /[\u10A0-\u10FF]/.test(str) // Georgian
  5893. || /[\u1100-\u11FF]/.test(str) // Hangul Jamo
  5894. || /[\u1200-\u137F]/.test(str) // Ethiopic
  5895. || /[\u1380-\u139F]/.test(str) // Ethiopic Supplement
  5896. || /[\u13A0-\u13FF]/.test(str) // Cherokee
  5897. || /[\u1400-\u167F]/.test(str) // Unified Canadian Aboriginal Syllabics
  5898. || /[\u1680-\u169F]/.test(str) // Ogham
  5899. || /[\u16A0-\u16FF]/.test(str) // Runic
  5900. || /[\u1700-\u171F]/.test(str) // Tagalog
  5901. || /[\u1720-\u173F]/.test(str) // Hanunoo
  5902. || /[\u1740-\u175F]/.test(str) // Buhid
  5903. || /[\u1760-\u177F]/.test(str) // Tagbanwa
  5904. || /[\u1780-\u17FF]/.test(str) // Khmer
  5905. || /[\u1800-\u18AF]/.test(str) // Mongolian
  5906. || /[\u18B0-\u18FF]/.test(str) // Unified Canadian Aboriginal Syllabics Extended
  5907. || /[\u1900-\u194F]/.test(str) // Limbu
  5908. || /[\u1950-\u197F]/.test(str) // Tai Le
  5909. || /[\u1980-\u19DF]/.test(str) // New Tai Lue
  5910. || /[\u19E0-\u19FF]/.test(str) // Khmer Symbols
  5911. || /[\u1A00-\u1A1F]/.test(str) // Buginese
  5912. || /[\u1A20-\u1AAF]/.test(str) // Tai Tham
  5913. || /[\u1AB0-\u1AFF]/.test(str) // Combining Diacritical Marks Extended
  5914. || /[\u1B00-\u1B7F]/.test(str) // Balinese
  5915. || /[\u1B80-\u1BBF]/.test(str) // Sundanese
  5916. || /[\u1BC0-\u1BFF]/.test(str) // Batak
  5917. || /[\u1C00-\u1C4F]/.test(str) // Lepcha
  5918. || /[\u1C50-\u1C7F]/.test(str) // Ol Chiki
  5919. || /[\u1C80-\u1C8F]/.test(str) // Cyrillic Extended C
  5920. || /[\u1CC0-\u1CCF]/.test(str) // Sundanese Supplement
  5921. || /[\u1CD0-\u1CFF]/.test(str) // Vedic Extensions
  5922. || /[\u1D00-\u1D7F]/.test(str) // Phonetic Extensions
  5923. || /[\u1D80-\u1DBF]/.test(str) // Phonetic Extensions Supplement
  5924. || /[\u1DC0-\u1DFF]/.test(str) // Combining Diacritical Marks Supplement
  5925. // || /[\u1E00-\u1EFF]/.test(str) // Latin Extended Additional
  5926. || /[\u1F00-\u1FFF]/.test(str) // Greek Extended
  5927. || /[\u200B-\u200F\u2028\u2029\u203B\u202A-\u202E\u2060-\u206F]/.test(str) //|| /[\u2000-\u206F]/.test(str) // General Punctuation
  5928. || /[\u2070-\u209F]/.test(str) // Superscripts and Subscripts
  5929. // || /[\u20A0-\u20CF]/.test(str) // Currency Symbols
  5930. || /[\u20D0-\u20FF]/.test(str) // Combining Diacritical Marks for Symbols
  5931. // || /[\u2100-\u214F]/.test(str) // Letterlike Symbols
  5932. || /[\u2150-\u218F]/.test(str) // Number Forms
  5933. // || /[\u2190-\u21FF]/.test(str) // Arrows
  5934. || /[\u2200-\u22FF]/.test(str) // Mathematical Operators
  5935. || /[\u2300-\u23FF]/.test(str) // Miscellaneous Technical
  5936. || /[\u2400-\u243F]/.test(str) // Control Pictures
  5937. // || /[\u2440-\u245F]/.test(str) // Optical Character Recognition
  5938. || /[\u2460-\u24FF]/.test(str) // Enclosed Alphanumerics
  5939. || /[\u2500-\u257F]/.test(str) // Box Drawing
  5940. // || /[\u2580-\u259F]/.test(str) // Block Elements
  5941. || /[\u25A0-\u25FF]/.test(str) // Geometric Shapes
  5942. || /[\u2600-\u26FF]/.test(str) // Miscellaneous Symbols
  5943. || /[\u2700-\u27BF]/.test(str) // Dingbats
  5944. || /[\u27C0-\u27EF]/.test(str) // Miscellaneous Mathematical Symbols-A
  5945. || /[\u27F0-\u27FF]/.test(str) // Supplemental Arrows-A
  5946. || /[\u2800-\u28FF]/.test(str) // Braille Patterns
  5947. || /[\u2900-\u297F]/.test(str) // Supplemental Arrows-B
  5948. // || /[\u2980-\u29FF]/.test(str) // Miscellaneous Mathematical Symbols-B
  5949. // || /[\u2A00-\u2AFF]/.test(str) // Supplemental Mathematical Operators
  5950. || /[\u2B00-\u2BFF]/.test(str) // Miscellaneous Symbols and Arrows
  5951. || /[\u2C00-\u2C5F]/.test(str) // Glagolitic
  5952. // || /[\u2C60-\u2C7F]/.test(str) // Latin Extended-C
  5953. || /[\u2C80-\u2CFF]/.test(str) // Coptic
  5954. || /[\u2D00-\u2D2F]/.test(str) // Georgian Supplement
  5955. || /[\u2D30-\u2D7F]/.test(str) // Tifinagh
  5956. || /[\u2D80-\u2DDF]/.test(str) // Ethiopic Extended
  5957. || /[\u2DE0-\u2DFF]/.test(str) // Cyrillic Extended-A
  5958. || /[\u2E00-\u2E7F]/.test(str) // Supplemental Punctuation
  5959. || /[\u2E80-\u2EFF]/.test(str) // CJK Radicals Supplement
  5960. || /[\u2F00-\u2FDF]/.test(str) // Kangxi Radicals
  5961. || /[\u2FF0-\u2FFF]/.test(str) // Ideographic Description Characters
  5962. || /[\u3000-\u303F]/.test(str) // CJK Symbols and Punctuation
  5963. || /[\u3040-\u309F]/.test(str) // Hiragana
  5964. || /[\u30A0-\u30FF]/.test(str) // Katakana
  5965. || /[\u3100-\u312F]/.test(str) // Bopomofo
  5966. || /[\u3130-\u318F]/.test(str) // Hangul Compatibility Jamo
  5967. || /[\u3190-\u319F]/.test(str) // Kanbun
  5968. || /[\u31A0-\u31BF]/.test(str) // Bopomofo Extended
  5969. || /[\u31C0-\u31EF]/.test(str) // CJK Strokes
  5970. || /[\u31F0-\u31FF]/.test(str) // Katakana Phonetic Extensions
  5971. || /[\u3200-\u32FF]/.test(str) // Enclosed CJK Letters and Months
  5972. || /[\u3300-\u33FF]/.test(str) // CJK Compatibility
  5973. || /[\u3400-\u4DBF]/.test(str) // CJK Unified Ideographs Extension A
  5974. || /[\u4DC0-\u4DFF]/.test(str) // Yijing Hexagram Symbols
  5975. || /[\u4E00-\u9FFF]/.test(str) // CJK Unified Ideographs
  5976. // || /[\uA000-\uA48F]/.test(str) // Yi Syllables
  5977. // || /[\uA490-\uA4CF]/.test(str) // Yi Radicals
  5978. || /[\uA4D0-\uA4FF]/.test(str) // Lisu
  5979. || /[\uA500-\uA63F]/.test(str) // Vai
  5980. || /[\uA640-\uA69F]/.test(str) // Cyrillic Extended-B
  5981. || /[\uA6A0-\uA6FF]/.test(str) // Bamum
  5982. || /[\uA700-\uA71F]/.test(str) // Modifier Tone Letters
  5983. || /[\uA720-\uA7FF]/.test(str) // Latin Extended-D
  5984. || /[\uA800-\uA82F]/.test(str) // Syloti Nagri
  5985. || /[\uA830-\uA83F]/.test(str) // Common Indic Number Forms
  5986. || /[\uA840-\uA87F]/.test(str) // Phags-pa
  5987. || /[\uA880-\uA8DF]/.test(str) // Saurashtra
  5988. || /[\uA8E0-\uA8FF]/.test(str) // Devanagari Extended
  5989. || /[\uA900-\uA92F]/.test(str) // Kayah Li
  5990. || /[\uA930-\uA95F]/.test(str) // Rejang
  5991. || /[\uA960-\uA97F]/.test(str) // Hangul Jamo Extended-A
  5992. || /[\uA980-\uA9DF]/.test(str) // Javanese
  5993. || /[\uA9E0-\uA9FF]/.test(str) // Myanmar Extended-B
  5994. || /[\uAA00-\uAA5F]/.test(str) // Cham
  5995. || /[\uAA60-\uAA7F]/.test(str) // Myanmar Extended-A
  5996. || /[\uAA80-\uAADF]/.test(str) // Tai Viet
  5997. || /[\uAAE0-\uAAFF]/.test(str) // Meetei Mayek Extensions
  5998. || /[\uAB00-\uAB2F]/.test(str) // Ethiopic Extended-A
  5999. // || /[\uAB30-\uAB6F]/.test(str) // Latin Extended-E
  6000. || /[\uAB70-\uABBF]/.test(str) // Cherokee Supplement
  6001. || /[\uABC0-\uABFF]/.test(str) // Meetei Mayek
  6002. || /[\uAC00-\uD7AF]/.test(str) // Hangul Syllables
  6003. || /[\uD7B0-\uD7FF]/.test(str) // Hangul Jamo Extended-B
  6004. || /[\uD800-\uDB7F]/.test(str) // High Surrogates
  6005. // || /[\uDB80-\uDBFF]/.test(str) // High Private Use Surrogates
  6006. || /[\uDC00-\uDFFF]/.test(str) // Low Surrogates
  6007. || /[\uE000-\uF8FF]/.test(str) // Private Use Area
  6008. || /[\uF900-\uFAFF]/.test(str) // CJK Compatibility Ideographs
  6009. || /[\uFB00-\uFB4F]/.test(str) // Alphabetic Presentation Forms
  6010. || /[\uFB50-\uFDFF]/.test(str) // Arabic Presentation Forms-A
  6011. || /[\uFE00-\uFE0F]/.test(str) // Variation Selectors
  6012. || /[\uFE10-\uFE1F]/.test(str) // Vertical Forms
  6013. || /[\uFE20-\uFE2F]/.test(str) // Combining Half Marks
  6014. || /[\uFE30-\uFE4F]/.test(str) // CJK Compatibility Forms
  6015. || /[\uFE50-\uFE6F]/.test(str) // Small Form Variants
  6016. || /[\uFE70-\uFEFF]/.test(str) // Arabic Presentation Forms-B
  6017. || /[\uFF00-\uFFEF]/.test(str) // Halfwidth and Fullwidth Forms
  6018. || /[\uFFF0-\uFFFF]/.test(str) // Specials
  6019. // || /[\u10000-\uFFFFF]/.test(str) // Others
  6020. }
  6021.  
  6022. function makeTimeString(duration) {
  6023. let t = Math.abs(Math.round(duration));
  6024. let H = Math.floor(t / 60 ** 2);
  6025. let M = Math.floor(t / 60 % 60);
  6026. let S = t % 60;
  6027. return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
  6028. ':' + S.toString().padStart(2, '0');
  6029. }
  6030.  
  6031. function timeStringToTime(str) {
  6032. if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
  6033. var t = 0, a = RegExp.$2.split(':');
  6034. while (a.length > 0) t = t * 60 + parseFloat(a.shift());
  6035. return RegExp.$1 ? -t : t;
  6036. }
  6037.  
  6038. function normalizeDate(str) {
  6039. if (typeof str != 'string') return null;
  6040. 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)
  6041. if (/\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // UK, IRL, FR
  6042. if (/\b(\d{1,2})-(\d{1,2})-(\d{2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // NL
  6043. 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
  6044. if (/\b(\d{4})\.\s?(\d{1,2})\.\s?(\d{1,2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1; // JP
  6045. return extractYear(str);
  6046. }
  6047.  
  6048. function extractYear(expr) {
  6049. if (typeof expr == 'number') return Math.round(expr);
  6050. if (typeof expr != 'string') return null;
  6051. if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
  6052. var d = new Date(expr);
  6053. return parseInt(isNaN(d) ? expr : d.getFullYear());
  6054. }
  6055.  
  6056. function formattedSize(size) {
  6057. return size < 1024**1 ? Math.round(size) + ' B'
  6058. : size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB'
  6059. : size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB'
  6060. : size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB'
  6061. : size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB'
  6062. : (Math.round(size * 100 / 2**50) / 100) + ' PiB';
  6063. }
  6064.  
  6065. function safeText(unsafeText) {
  6066. let div = document.createElement('div');
  6067. div.innerText = unsafeText || '';
  6068. return div.innerHTML;
  6069. }
  6070.  
  6071. function testImageUrl(url, strict = false) {
  6072. if (!urlParser.test(url)) return Promise.reject('not an image link');
  6073. if (!strict && imageExtensions.some(function(ext) {
  6074. return url.toLowerCase().endsWith('.'.concat(ext));
  6075. })) return Promise.resolve(url); // weak quick test
  6076. return new Promise(function(resolve, reject) {
  6077. var img = new Image();
  6078. img.onload = function() { resolve(this.src) };
  6079. img.onerror = img.ontimeout = error => { reject(url.concat(' not valid image')) };
  6080. img.src = url;
  6081. });
  6082. }
  6083. function testImageUrls(urls) {
  6084. return Array.isArray(urls) ? Promise.all(urls.map(testImageUrl)) : Promise.reject('URLs not an array');
  6085. }
  6086.  
  6087. function imageClear(evt) {
  6088. evt.target.value = '';
  6089. coverPreview(evt.target, null);
  6090. }
  6091.  
  6092. function imageDropHandler(evt) { return imageDataHandler(evt, evt.dataTransfer) }
  6093. function imagePasteHandler(evt) { return imageDataHandler(evt, evt.clipboardData) }
  6094. function imageDataHandler(evt, data) {
  6095. if (!data) return true;
  6096. if (data.files.length > 0 && data.files[0].type.toLowerCase().startsWith('image/')) {
  6097. evt.target.disabled = true;
  6098. if (evt.target.hTimer) {
  6099. clearTimeout(evt.target.hTimer);
  6100. delete evt.target.hTimer;
  6101. }
  6102. evt.target.style.backgroundColor = '#800000';
  6103. let size = data.files[0].size;
  6104. uploadFiles(upload2PTPIMG, [data.files[0]]).catch(function(reason) {
  6105. addMessage('Upload to PTPIMG failed (' + reason + '), falling back to malzo.com', 'warning');
  6106. return uploadFiles(upload2Malzo, [data.files[0]]);
  6107. }).catch(function(reason) {
  6108. addMessage('Upload to malzo.com failed (' + reason + '), falling back to pixhost.to', 'warning');
  6109. return uploadFiles(upload2PixHost, [data.files[0]]);
  6110. }).catch(function(reason) {
  6111. addMessage('Upload to pixhost.to failed (' + reason + '), falling back to catbox.moe', 'warning');
  6112. return uploadFiles(upload2Catbox, [data.files[0]]);
  6113. }).then(function(urls) {
  6114. evt.target.value = urls[0];
  6115. evt.target.style.backgroundColor = '#008000';
  6116. evt.target.style.color = 'white';
  6117. evt.target.hTimer = setTimeout(function() {
  6118. evt.target.style.backgroundColor = null;
  6119. evt.target.style.color = null;
  6120. delete evt.target.hTimer;
  6121. }, 10000);
  6122. coverPreview(evt.target, urls[0], size);
  6123. }).catch(function(error) {
  6124. evt.target.style.backgroundColor = null;
  6125. imageClear(evt);
  6126. alert(error);
  6127. }).then(function() { evt.target.disabled = false });
  6128. return false;
  6129. } else if (data.items.length > 0) {
  6130. testImageUrl((data.getData('text/uri-list') || data.getData('text/plain')).split(/\r?\n/)[0]).then(function(url) {
  6131. evt.target.value = url;
  6132. coverPreview(evt.target, url);
  6133. if (!prefs.auto_rehost_cover || url.toLowerCase().startsWith(imghostOrigin)) return;
  6134. //if (rehostItBtn != null) return rehostItBtn.click();
  6135. evt.target.disabled = true;
  6136. rehost2PTPIMG([url])
  6137. .then(function(urls) { if (urls.length > 0) evt.target.value = urls[0] })
  6138. .catch(e => { alert(e) })
  6139. .then(function() { evt.target.disabled = false });
  6140. }).catch(e => { console.warn(e) });
  6141. return false;
  6142. }
  6143. return true;
  6144. }
  6145.  
  6146. function descDropHandler(evt) {
  6147. if (evt.dataTransfer == null || evt.shiftKey) return true;
  6148. if (evt.dataTransfer.files.length > 0) {
  6149. let images = [];
  6150. Array.from(evt.dataTransfer.files).forEach(function(file) {
  6151. switch (file.type) {
  6152. case '':
  6153. if (!['log'/*, 'nfo'*/].some(ext => file.name.toLowerCase().endsWith('.' + ext))) break;
  6154. case 'text/plain':
  6155. //case 'text/nfo': // malformed encoding
  6156. case 'text/log':
  6157. evt.target.disabled = true;
  6158. file.getText(file.name.toLowerCase().endsWith('.nfo') ? 'ibm850' : 'utf-8').then(function(text) {
  6159. var isDR = file.name.toLowerCase().endsWith('foo_dr.txt') && /^Official DR value:\s*DR(\d+)\b/im.test(text);
  6160. if (isDR) var DR = parseInt(RegExp.$1);
  6161. var tag = isDR || file.name.toLowerCase().endsWith('.nfo') ? 'pre' : 'code';
  6162. var php = isDR ? '[hide=DR' + RegExp.$1 + '][' + tag + ']' + text + '[/' + tag + '][/hide]'
  6163. : '[hide=' + file.name + '][' + tag + ']' + text + '[/' + tag + '][/hide]';
  6164. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  6165. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) +
  6166. php + evt.target.value.slice(evt.rangeOffset);
  6167. } else if (isDR && /\[hide=DR\d*\]\[pre\]\[\/pre\]/i.test(evt.target.value)) {
  6168. evt.target.value = RegExp.leftContext + php.slice(0, -7) + RegExp.rightContext;
  6169. } else if (isDR && /\[hide=DR(\d*)\]((?:\[pre\](foobar2000[\s\S]+?)^\[\/pre\]\s*)+)(?:\[pre\]\[\/pre\])?/im.test(evt.target.value)) {
  6170. php = '[hide=DR';
  6171. if (parseInt(RegExp.$1) == DR) php += RegExp.$1;
  6172. evt.target.value = RegExp.leftContext.concat(php, ']', RegExp.$2.trim(), '\n[pre]', text, '[/pre]', RegExp.rightContext);
  6173. } else if (!isDR && /\[hide\](?:\[code\]\[\/code\])?\[\/hide\]/i.test(evt.target.value)) {
  6174. evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
  6175. } else if (!isDR && /(\[hide=[^\]]+\])(?:\[code\]\[\/code\])?(\[\/hide\])/i.test(evt.target.value)) {
  6176. evt.target.value = RegExp.leftContext.concat(RegExp.$1, '[code]', text, '[/code]', RegExp.$2, RegExp.rightContext);
  6177. } else evt.target.value += '\n\n'.concat(php);
  6178. }).catch(function(e) { alert(e) }).then(function() {
  6179. if (!evt.target.style.background) evt.target.disabled = false;
  6180. });
  6181. break;
  6182. case 'image/png':
  6183. case 'image/jpeg':
  6184. case 'image/gif':
  6185. case 'image/bmp':
  6186. //case 'image/webp':
  6187. //case 'image/svg+xml':
  6188. images.push(file);
  6189. break;
  6190. }
  6191. });
  6192. if (images.length > 0) {
  6193. evt.target.disabled = true;
  6194. evt.target.style.background = '#FF000040 no-repeat center center url(' + ulImgData +')';
  6195. //evt.target.style.background = '#FF000040 no-repeat center center url(https://svgshare.com/i/H16.svg)';
  6196. uploadFiles(upload2PTPIMG, images)
  6197. .catch(reason => uploadFiles(upload2Malzo, images))
  6198. .catch(reason => uploadFiles(upload2PixHost, images))
  6199. .catch(reason => uploadFiles(upload2Catbox, images))
  6200. .then(urlHandler.bind({ tag: 'img' }))
  6201. .catch(error => { alert(error) })
  6202. .then(function() {
  6203. evt.target.style.background = null;
  6204. evt.target.disabled = false;
  6205. });
  6206. }
  6207. return false;
  6208. } else if (evt.dataTransfer.items.length > 0) {
  6209. let content = evt.dataTransfer.getData('text/uri-list');
  6210. if (content) {
  6211. content = content.split(/\r?\n/);
  6212. testImageUrls(content).then(function(urls) {
  6213. if (prefs.auto_rehost_cover) {
  6214. evt.target.disabled = true;
  6215. rehost2PTPIMG(urls).then(urlHandler.bind({ tag: 'img' })).catch(e => { alert(e) }).then(function() {
  6216. evt.target.disabled = false;
  6217. });
  6218. } else urlHandler.bind({ tag: 'img' })(content);
  6219. }).catch(function(e) {
  6220. let as = domParser.parseFromString(evt.dataTransfer.getData('text/html'), 'text/html').body.querySelectorAll('a');
  6221. urlHandler.bind({ tag: 'url', titles: Array.from(as).map(a => a.textContent.trim()) })(content);
  6222. });
  6223. } else if (content = evt.dataTransfer.getData('text/html')) {
  6224. textHandler(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
  6225. } else if (content = evt.dataTransfer.getData('text/plain')) {
  6226. textHandler(content);
  6227. }
  6228. return false;
  6229. }
  6230. return true;
  6231.  
  6232. function urlHandler(urls) {
  6233. const rx = new RegExp('\\[' + this.tag + '\\]\\[\\/' + this.tag + '\\]', 'i');
  6234. urls.forEach(function(url, ndx) {
  6235. if (url.length <= 0 || !urlParser.test(urls)) return;
  6236. var php = '[' + this.tag;
  6237. php += Array.isArray(this.titles) && this.titles[ndx] ? '=' + url + ']' + this.titles[ndx] : ']' + url;
  6238. php += '[/' + this.tag + ']';
  6239. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  6240. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + php + evt.target.value.slice(evt.rangeOffset);
  6241. } else if (rx.test(evt.target.value)) {
  6242. evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
  6243. } else evt.target.value += '\n\n'.concat(php);
  6244. }.bind(this));
  6245. }
  6246. function textHandler(php) {
  6247. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  6248. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + php + evt.target.value.slice(evt.rangeOffset);
  6249. } else evt.target.value += '\n\n'.concat(php);
  6250. }
  6251. }
  6252.  
  6253. function descPasteHandler(evt) {
  6254. if (evt.clipboardData == null || evt.clipboardData.items.length <= 0) return true;
  6255. var content = evt.clipboardData.getData('text/html');
  6256. if (!content) return true;
  6257. content = html2php(domParser.parseFromString(content, 'text/html')).collapseGaps();
  6258. var selStart = evt.target.selectionStart;
  6259. evt.target.value = evt.target.value.slice(0, evt.target.selectionStart)
  6260. .concat(content, evt.target.value.slice(evt.target.selectionEnd));
  6261. evt.target.setSelectionRange(selStart + content.length, selStart + content.length);
  6262. return false;
  6263. }
  6264.  
  6265. function ptpimgItHandler(evt) {
  6266. if (evt.dataTransfer == null) return false;
  6267. var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  6268. if (image == null) return false;
  6269. if (evt.dataTransfer.files.length > 0) {
  6270. evt.preventDefault();
  6271. evt.stopPropagation();
  6272. evt.currentTarget.disabled = true;
  6273. if (evt.currentTarget.hTimer) {
  6274. clearTimeout(evt.currentTarget.hTimer);
  6275. delete evt.currentTarget.hTimer;
  6276. }
  6277. evt.currentTarget.value = 'Uploading...';
  6278. evt.currentTarget.style.backgroundColor = '#A00000';
  6279. var evtSrc = evt.currentTarget;
  6280. uploadFiles(upload2PTPIMG, evt.dataTransfer.files)
  6281. .catch(reason => uploadFiles(upload2Malzo, evt.dataTransfer.files))
  6282. .catch(reason => uploadFiles(upload2PixHost, evt.dataTransfer.files))
  6283. .catch(reason => uploadFiles(upload2Catbox, evt.dataTransfer.files))
  6284. .then(function(results) {
  6285. if (urlParser.test(results[0])) {
  6286. image.value = results[0];
  6287. evtSrc.style.backgroundColor = '#008000';
  6288. evtSrc.hTimer = setTimeout(function() {
  6289. evtSrc.style.backgroundColor = null;
  6290. delete evtSrc.hTimer;
  6291. }, 10000);
  6292. coverPreview(image, results[0], evt.dataTransfer.files[0].size);
  6293. } else evtSrc.style.backgroundColor = null;
  6294. }).catch(function(error) {
  6295. evtSrc.style.backgroundColor = null;
  6296. alert(error);
  6297. }).then(function() {
  6298. evtSrc.value = evtSrc.dataset.caption;
  6299. evtSrc.disabled = false;
  6300. });
  6301. } else if (evt.dataTransfer.items.length > 0) {
  6302. testImageUrl((evt.dataTransfer.getData('text/uri-list')
  6303. || evt.dataTransfer.getData('text/plain')).split(/\r?\n/)[0]).then(function(url) {
  6304. evt.preventDefault();
  6305. evt.stopPropagation();
  6306. image.value = url;
  6307. coverPreview(image, url);
  6308. if (url.toLowerCase().startsWith(imghostOrigin)) return;
  6309. image.disabled = true;
  6310. rehost2PTPIMG([url])
  6311. .then(function(urls) { if (urls.length > 0) image.value = urls[0] })
  6312. .catch(e => { alert(e) })
  6313. .then(function() { image.disabled = false });
  6314. return false;
  6315. }).catch(e => { console.warn(e) });
  6316. }
  6317. return false;
  6318. }
  6319.  
  6320. function uaInsert(evt) {
  6321. if (evt.clipboardData) evt.target.value = '';
  6322. if (!(prefs.autfill_delay > 0)) return true;
  6323. autofill = true;
  6324. setTimeout(fillFromText, prefs.autfill_delay);
  6325. }
  6326.  
  6327. // Firefox accepts dropped playlist in malformed form, try to detect and correct it
  6328. function fixFirefoxDropBug(evt) {
  6329. if (evt.target == null || evt.target.value.length <= 0) return true;
  6330. var tl = (Math.sqrt(4 * evt.target.value.split('\n').length - 3) + 1) / 2;
  6331. if (tl < 2 || tl != Math.floor(tl) || evt.target.value.length % tl != 0) return true;
  6332. var l = evt.target.value.length / tl;
  6333. var s = evt.target.value.slice(0, l);
  6334. for (var i = 1; i < tl; ++i) if (evt.target.value.slice(i * l, (i + 1) * l) != s) return true;
  6335. evt.target.value = s;
  6336. return true;
  6337. }
  6338.  
  6339. function clear0(evt) { if (evt.target.value.length > 0) evt.target.value = '' }
  6340. function clear1(evt) { if (evt.buttons == 4) clear0(evt) }
  6341. function voidDragHandler0(evt) { return false }
  6342. function voidDragHandler1(evt) {
  6343. return !evt.dataTransfer.types.includes('Files') || evt.target.nodeName == 'TEXTAREA'
  6344. || evt.target.nodeName == 'INPUT' && evt.target.type == 'file'
  6345. }
  6346.  
  6347. function uploadFiles(uploader, files, elem) {
  6348. if (typeof files != 'object') return Promise.reject('Invalid argument');
  6349. var frs = Array.from(files).filter(function(file) {
  6350. return file instanceof File && imageExtensions.some(ext => file.type == 'image/' + ext);
  6351. }).sort((file1, file2) => file1.name.localeCompare(file2.name)).map(file => new Promise(function(resolve, reject) {
  6352. var reader = new FileReader();
  6353. reader.onload = function() { resolve({ name: file.name, type: file.type, data: reader.result }) };
  6354. reader.onerror = reader.ontimeout = error => { reject('FileReader error (' + file.name + ')') };
  6355. reader.readAsBinaryString(file);
  6356. }));
  6357. return frs.length > 0 ? Promise.all(frs).then(images => uploader(images, elem))
  6358. : Promise.reject('Nothing to upload');
  6359. }
  6360.  
  6361. function upload2PTPIMG(images, elem) {
  6362. if (!Array.isArray(images)) return Promise.reject('Invalid argument');
  6363. if (images.length <= 0) return Promise.reject('Nothing to upload'); //Promise.resolve([]);
  6364. return getPTPIMGapiKey().then(apiKey => new Promise(function(resolve, reject) {
  6365. const now = new Date().getTime();
  6366. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  6367. var data = '--' + boundary + '\r\n';
  6368. images.forEach(function(image, ndx) {
  6369. data += 'Content-Disposition: form-data; name="file-upload[' + ndx + ']"; ' +
  6370. 'filename="' + image.name.toASCII() + '"\r\n';
  6371. data += 'Content-Type: ' + image.type + '\r\n\r\n';
  6372. data += image.data + '\r\n';
  6373. data += '--' + boundary + '\r\n';
  6374. });
  6375. data += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
  6376. data += apiKey + '\r\n';
  6377. data += '--' + boundary + '--\r\n';
  6378. GM_xmlhttpRequest({
  6379. method: 'POST',
  6380. url: imghostOrigin + '/upload.php',
  6381. responseType: 'json',
  6382. headers: {
  6383. 'Accept': 'application/json',
  6384. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  6385. 'Content-Length': data.length,
  6386. 'Referer': imghostOrigin + '/upload.php',
  6387. },
  6388. data: data,
  6389. binary: true,
  6390. onload: function(response) {
  6391. if (response.status >= 200 && response.status < 400) {
  6392. if (response.response) resolve(response.response.map(item => imghostOrigin + '/' + item.code + '.' + item.ext));
  6393. else reject('void response');
  6394. } else {
  6395. reject(`Response error ${response.readyState}/${response.status} (${response.statusText})`);
  6396. }
  6397. },
  6398. onprogress: elem instanceof HTMLInputElement ?
  6399. progress => { elem.value = 'Uploading... (' + progress.position + '%)' } : undefined,
  6400. onerror: error => reject(defaultErrorHandler(error)),
  6401. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6402. });
  6403. }));
  6404. }
  6405.  
  6406. function rehost2PTPIMG(urls) {
  6407. return Promise.all(urls.map(url => testImageUrl(url).then(function(imgUrl) {
  6408. const hostName = new URL(imgUrl).hostname;
  6409. if (hostName.endsWith('discogs.com')) {
  6410. return testImageUrl('https://reho.st/'.concat(imgUrl), true)
  6411. .catch(reason => rehost2CatBox(imgUrl))
  6412. .catch(reason => rehost2PixHost(imgUrl))
  6413. //.catch(reason => rehost2Malzo(imgUrl))
  6414. .catch(reason => reupload2PTPIMG(imgUrl));
  6415. } else if (!imageExtensions.some(ext => imgUrl.toLowerCase().endsWith('.'.concat(ext)))) {
  6416. return testImageUrl(imgUrl.concat('#.jpg'), true).catch(reason => rehost2Imgur(imgUrl));
  6417. }
  6418. return imgUrl;
  6419. }))).then(function(imgUrls) {
  6420. return imgUrls.length <= 0 ? [] : getPTPIMGapiKey().then(function(apiKey) {
  6421. var formData = new URLSearchParams({
  6422. 'link-upload': imgUrls.join('\r\n'),
  6423. 'api_key': apiKey,
  6424. });
  6425. return globalFetch(imghostOrigin + '/upload.php', {
  6426. responseType: 'json',
  6427. headers: { 'Referer': imghostOrigin + '/upload.php' },
  6428. }, formData).then(function(response) {
  6429. if (!response.response) return Promise.reject('void response');
  6430. return response.response.map(item => imghostOrigin.concat('/', item.code, '.', item.ext));
  6431. });
  6432. });
  6433. });
  6434. }
  6435.  
  6436. function reupload2PTPIMG(imgUrl) {
  6437. console.debug('PTPIMG rehoster fallback to local reupload');
  6438. return globalFetch(imgUrl, { responseType: 'blob' }).then(function(response) {
  6439. var image = {
  6440. name: imgUrl.replace(/^.*\//, ''),
  6441. data: response.responseText,
  6442. };
  6443. switch (imgUrl.replace(/^.*\./, '').toLowerCase()) {
  6444. case 'jpg': case 'jpeg': case 'jfif': image.type = 'image/jpeg'; break;
  6445. case 'png': image.type = 'image/png'; break;
  6446. case 'gif': image.type = 'image/gif'; break;
  6447. case 'bmp': image.type = 'image/bmp'; break;
  6448. default: return Promise.reject('Unsupported extension');
  6449. }
  6450. return upload2PTPIMG([image]).then(imgUrls => imgUrls[0]);
  6451. });
  6452. }
  6453.  
  6454. function getPTPIMGapiKey() {
  6455. try {
  6456. var apiKey = prefs.ptpimg_api_key || JSON.parse(window.localStorage.ptpimg_it).api_key;
  6457. if (apiKey) return Promise.resolve(apiKey);
  6458. } catch(e) { console.log('getPTPIMGapiKey', e) }
  6459. return globalFetch(imghostOrigin).then(function(response) {
  6460. if ((apiKey = response.document.getElementById('api_key')) != null) {
  6461. if (!apiKey.value) return Promise.reject('Assertion failed: API key empty');
  6462. GM_setValue('ptpimg_api_key', prefs.ptpimg_api_key = apiKey.value);
  6463. Promise.resolve(apiKey.value)
  6464. .then(apiKey => { alert(`Your PTPIMG API key [${apiKey}] was successfully configured`) });
  6465. return apiKey.value;
  6466. } else return Promise.reject(`
  6467. PTPIMG API key isn\'t configured.
  6468. Please login to ${imghostOrigin}/ and repeat the action.
  6469.  
  6470. If you don\'t have PTPIMG account, to avoid this warning in
  6471. future consider to set auto_rehost_cover to 0 in preferences
  6472. (Tampermonkey extension menu -> right click to Upload Assistant -> Storage tab)
  6473. `);
  6474. });
  6475. }
  6476.  
  6477. function upload2Malzo(images, elem) {
  6478. if (!Array.isArray(images)) return Promise.reject('Invalid argument');
  6479. if (images.length <= 0) return Promise.reject('Nothing to upload'); //Promise.resolve([]);
  6480. return setMalzoSession().then(authToken => Promise.all(images.map(image => new Promise(function(resolve, reject) {
  6481. const now = new Date().getTime();
  6482. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  6483. var data = '--' + boundary + '\r\n';
  6484. data += 'Content-Disposition: form-data; name="source"; filename="' + image.name.toASCII() + '"\r\n';
  6485. data += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n--' + boundary + '\r\n';
  6486. data += formField('type', 'file').concat('\r\n');
  6487. data += formField('action', 'upload').concat('\r\n');
  6488. data += formField('timestamp', now).concat('\r\n');
  6489. data += formField('auth_token', authToken).concat('\r\n');
  6490. data += formField('nsfw', 0).concat('--\r\n');
  6491. GM_xmlhttpRequest({
  6492. method: 'POST',
  6493. url: 'https://malzo.com/json',
  6494. responseType: 'json',
  6495. headers: {
  6496. 'Accept': 'application/json',
  6497. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  6498. 'Content-Length': data.length,
  6499. },
  6500. data: data,
  6501. binary: true,
  6502. onload: function(response) {
  6503. if (response.status >= 200 && response.status < 400) resolve(response.response.image.url);
  6504. else reject(`Response error ${response.readyState}/${response.status} (${response.statusText})`);
  6505. },
  6506. onprogress: elem instanceof HTMLInputElement ?
  6507. progress => { elem.value = 'Uploading... (' + progress.position + '%)' } : undefined,
  6508. onerror: error => reject(defaultErrorHandler(error)),
  6509. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6510. });
  6511.  
  6512. function formField(key, value) {
  6513. return 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n--' + boundary;
  6514. }
  6515. }))));
  6516. }
  6517.  
  6518. function rehost2Malzo(url) {
  6519. return testImageUrl(url).then(imgUrl => setMalzoSession().then(function(authToken) {
  6520. var formData = new URLSearchParams({
  6521. source: imgUrl,
  6522. type: 'url',
  6523. action: 'upload',
  6524. timestamp: new Date().getTime(),
  6525. auth_token: authToken,
  6526. nsfw: 0,
  6527. });
  6528. return globalFetch('https://malzo.com/json', {
  6529. responseType: 'json',
  6530. headers: { 'Referer': 'https://malzo.com/im/uLO' },
  6531. }, formData).then(response => response.response.result.url);
  6532. }));
  6533. }
  6534.  
  6535. function setMalzoSession() {
  6536. return globalFetch('https://malzo.com/').then(function(response) {
  6537. const tokenParser = /\b(?:auth_token)\s*=\s*"(\w+)"/;
  6538. if (!tokenParser.test(response.responseText)) return Promise.reject('Auth token not found');
  6539. console.debug('Malzo.com auth token found:', RegExp.$1);
  6540. var authToken = RegExp.$1;
  6541. if (!prefs.malzo_uid || !prefs.malzo_password) return Promise.resolve(authToken); // continue unauthorized
  6542. if (response.document.querySelector('li#top-bar-user') != null) return Promise.resolve(authToken);
  6543. var formData = new URLSearchParams({
  6544. 'login-subject': prefs.malzo_uid,
  6545. 'password': prefs.malzo_password,
  6546. 'auth_token': authToken,
  6547. });
  6548. return new Promise(function(resolve, reject) {
  6549. GM_xmlhttpRequest({ method: 'POST', url: 'https://malzo.com/login', headers: {
  6550. 'Accept': '*/*',
  6551. 'Content-Type': 'application/x-www-form-urlencoded',
  6552. 'Content-Length': formData.toString().length,
  6553. }, data: formData.toString(), onload: function(response) {
  6554. // Server response HTTP/400 - "Bad Request" even if the login succeeds
  6555. if (response.status < 200 || response.status > 400) reject(response); else resolve(authToken);
  6556. },
  6557. onerror: error => reject(defaultErrorHandler(error)),
  6558. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6559. });
  6560. });
  6561. });
  6562. }
  6563.  
  6564. function upload2PixHost(images, elem) {
  6565. if (!Array.isArray(images)) return Promise.reject('Invalid argument');
  6566. if (images.length <= 0) return Promise.reject('Nothing to upload');
  6567. return Promise.all(images.map(image => new Promise(function(resolve, reject) {
  6568. const now = new Date().getTime();
  6569. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  6570. var data = '--' + boundary + '\r\n';
  6571. data += 'Content-Disposition: form-data; name="img"; filename="' + image.name.toASCII() + '"\r\n';
  6572. data += 'Content-Type: ' + image.type + '\r\n\r\n';
  6573. data += image.data + '\r\n';
  6574. data += '--' + boundary + '\r\n';
  6575. data += 'Content-Disposition: form-data; name="content_type"\r\n\r\n';
  6576. data += '0\r\n';
  6577. data += '--' + boundary + '--\r\n';
  6578. GM_xmlhttpRequest({
  6579. method: 'POST',
  6580. url: 'https://api.pixhost.to/images',
  6581. responseType: 'json',
  6582. headers: {
  6583. 'Accept': 'application/json',
  6584. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  6585. 'Content-Length': data.length,
  6586. },
  6587. data: data,
  6588. binary: true,
  6589. onload: function(response) {
  6590. if (response.status >= 200 && response.status < 400) resolve(response.response.show_url);
  6591. else reject(`Response error ${response.readyState}/${response.status} (${response.statusText})`);
  6592. },
  6593. onprogress: elem instanceof HTMLInputElement ?
  6594. progress => { elem.value = 'Uploading... (' + progress.position + '%)' } : undefined,
  6595. onerror: error => reject(defaultErrorHandler(error)),
  6596. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6597. });
  6598. }).then(pixHostImgExtractor)));
  6599. }
  6600.  
  6601. function rehost2PixHost(url) {
  6602. var form = new URLSearchParams({ imgs: url, content_type: 0, tos: 'on' });
  6603. return globalFetch('https://pixhost.to/remote/', { responseType: 'text' }, form)
  6604. .then(response => /\bupload_results\s*=\s*(\{.*\});$/m.test(response.responseText) ?
  6605. pixHostImgExtractor(JSON.parse(RegExp.$1).images[0].show_url) : Promise.reject('Page parsing error'));
  6606. }
  6607.  
  6608. function pixHostImgExtractor(showUrl) {
  6609. return urlParser.test(showUrl) ? globalFetch(showUrl).then(function(response) {
  6610. var img = response.document.getElementById('image');
  6611. return img != null ? img.src : Promise.reject('Page parsing error');
  6612. }) : Promise.reject('Invalid argument');
  6613. }
  6614.  
  6615. function upload2Catbox(images) {
  6616. if (!Array.isArray(images)) return Promise.reject('Invalid argument');
  6617. if (images.length <= 0) return Promise.reject('Nothing to upload'); //Promise.resolve([]);
  6618. return getCatBoxUserHash().catch(reason => undefined).then(userHash => Promise.all(images.map(image => new Promise(function(resolve, reject) {
  6619. const now = new Date().getTime();
  6620. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  6621. var data = '--' + boundary + '\r\n';
  6622. data += 'Content-Disposition: form-data; name="reqtype"\r\n\r\n';
  6623. data += 'fileupload\r\n';
  6624. if (userHash) {
  6625. data += '--' + boundary + '\r\n';
  6626. data += 'Content-Disposition: form-data; name="userhash"\r\n\r\n';
  6627. data += userHash + '\r\n';
  6628. }
  6629. data += '--' + boundary + '\r\n';
  6630. data += 'Content-Disposition: form-data; name="fileToUpload"; filename="' + image.name.toASCII() + '"\r\n';
  6631. data += 'Content-Type: ' + image.type + '\r\n\r\n';
  6632. data += image.data + '\r\n';
  6633. data += '--' + boundary + '--\r\n';
  6634. GM_xmlhttpRequest({
  6635. method: 'POST',
  6636. url: 'https://catbox.moe/user/api.php',
  6637. responseType: 'text',
  6638. headers: {
  6639. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  6640. 'Content-Length': data.length,
  6641. },
  6642. data: data,
  6643. binary: true,
  6644. onload: function(response) {
  6645. if (response.status >= 200 && response.status < 400) resolve(response.response);
  6646. else reject('Response error ' + response.status + ' (' + response.statusText + ')');
  6647. },
  6648. onerror: error => reject(defaultErrorHandler(error)),
  6649. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6650. });
  6651. }))));
  6652. }
  6653.  
  6654. function rehost2CatBox(imageUrl) {
  6655. return testImageUrl(imageUrl).then(imageUrl => getCatBoxUserHash().catch(reason => undefined).then(function(userHash) {
  6656. var formData = new URLSearchParams({
  6657. reqtype: 'urlupload',
  6658. url: imageUrl,
  6659. });
  6660. if (userHash) formData.set('userhash', userHash);
  6661. return globalFetch('https://catbox.moe/user/api.php', { responseType: 'text' }, formData)
  6662. .then(response => response.responseText);
  6663. }));
  6664. }
  6665.  
  6666. function getCatBoxUserHash() {
  6667. return prefs.catbox_userhash ? Promise.resolve(prefs.catbox_userhash) : globalFetch('https://catbox.moe/').then(function(response) {
  6668. var userHash = response.document.querySelector('input[name="userhash"][value]');
  6669. return userHash != null && userHash.value ? userHash.value : Promise.reject('CatBox.moe: not logged in or userhash not found');
  6670. });
  6671. }
  6672.  
  6673. function rehost2Imgur(url) {
  6674. const requestUrl = 'https://imgur.com/upload';
  6675. var form = new URLSearchParams({ url: url });
  6676. return globalFetch(requestUrl, { responseType: 'json', headers: { Referer: requestUrl } }, form).then(function(result) {
  6677. if (!result.response.success) return Promise.reject(result.response.status);
  6678. return 'https://i.imgur.com/'.concat(result.response.data.hash, result.response.data.ext);
  6679. });
  6680. }
  6681.  
  6682. function dcFmtToGazelle(format) {
  6683. if (/^(?:CD|CDi|CDr|HDCD)\b/.test(format)) return 'CD';
  6684. 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';
  6685. if (/^(?:Vinyl|LP|\d+(?:\.\d+)?\s*")$/.test(format)) return 'Vinyl';
  6686. if (/\b(?:SACD|Hybrid)\b/.test(format)) return 'SACD';
  6687. if (/^(?:Blu[ \-]?ray)\b/i.test(format)) return isOPS ? 'BD' : 'Blu-Ray';
  6688. if (/^(?:DVD|HD\s+DVD)/.test(format)) return 'DVD';
  6689. if (/^(?:Cassette|Microcassette)$/i.test(format)) return 'Cassette';
  6690. if (/^(?:DAT)$/.test(format)) return 'DAT';
  6691. if (/^(?:Soundboard)$/i.test(format)) return 'Soundboard';
  6692. //if (/^(?:Memory\s+Stick)$/i.test(format)) return ??
  6693. return null;
  6694. }
  6695.  
  6696. function queryAjaxAPI(action, params) {
  6697. if (!action) return Promise.reject('Action missing');
  6698. var retryCount = 0;
  6699. return new Promise(function(resolve, reject) {
  6700. params = new URLSearchParams(params || undefined);
  6701. params.set('action', action);
  6702. var url = '/ajax.php?'.concat(params);
  6703. var xhr = new XMLHttpRequest();
  6704. queryInternal();
  6705.  
  6706. function queryInternal() {
  6707. var now = new Date().getTime();
  6708. if (!gazelleApiTimeFrame.timeStamp || now > gazelleApiTimeFrame.timeStamp + 10100) {
  6709. gazelleApiTimeFrame.timeStamp = now;
  6710. gazelleApiTimeFrame.requestCounter = 0;
  6711. };
  6712. if (++gazelleApiTimeFrame.requestCounter <= 5) {
  6713. xhr.open('GET', url, true);
  6714. xhr.setRequestHeader('Accept', 'application/json');
  6715. xhr.responseType = 'json';
  6716. xhr.onload = function() {
  6717. if (xhr.status == 503) return http503Handler(3333, 'onload');
  6718. if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
  6719. if (xhr.response.status == 'success') resolve(xhr.response.response);
  6720. else reject(xhr.response.status);
  6721. };
  6722. xhr.onerror = function() {
  6723. if (xhr.status == 503) http503Handler(3333, xhr, 'onerror'); else reject(defaultErrorHandler(xhr));
  6724. };
  6725. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  6726. xhr.timeout = 10000;
  6727. xhr.send();
  6728. /*
  6729. GM_xmlhttpRequest({
  6730. method: 'GET',
  6731. url: url,
  6732. responseType: 'json',
  6733. headers: { 'Accept': 'application/json' },
  6734. onload: function(response) {
  6735. if (response.status == 503) return http503Handler(3333, response, 'onload');
  6736. if (response.status >= 200 && response.status < 400) resolve(response.response);
  6737. else reject(defaultErrorHandler(response));
  6738. },
  6739. onerror: error => error.status == 503 ? http503Handler(3333, error, 'onerror')
  6740. : reject(defaultErrorHandler(error)),
  6741. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6742. });
  6743. */
  6744. } else {
  6745. setTimeout(queryInternal, gazelleApiTimeFrame.timeStamp + 10100 - now);
  6746. console.debug('AJAX API request quota exceeded: /ajax.php?action=' + action + ' (' +
  6747. gazelleApiTimeFrame.requestCounter + ')');
  6748. if (prefs.messages_verbosity >= 1) {
  6749. addMessage('AJAX API request exceeding time frame: action=' +
  6750. action + ' (' + gazelleApiTimeFrame.requestCounter + ')', 'notice');
  6751. } else addMessage('please wait for next AJAX timeframe', 'notice');
  6752. }
  6753.  
  6754. function http503Handler(delay, /*response, */event) {
  6755. if (retryCount++ <= 10) setTimeout(queryInternal, delay); else reject(defaultErrorHandler(xhr));
  6756. console.debug('[UA] queryAjaxAPI encountered HTTP/503 error for url ' + url + '; event: ' + event);
  6757. }
  6758. }
  6759. });
  6760. }
  6761.  
  6762. function validataTorrentFile(torrent) {
  6763. tfMessages.forEach(node => { node.remove() });
  6764. tfMessages = [];
  6765. var fr = new FileReader();
  6766. fr.onload = function(evt) {
  6767. torrent = bdecode(new Uint8Array(fr.result));
  6768. torrent.info.files.forEach(function(file) {
  6769. var folderName = decodeURIComponent(escape(torrent.info.name));
  6770. var fileName = decodeURIComponent(escape(file.path[0]));
  6771. var totalLen = folderName.trueLength() + 1 + fileName.trueLength();
  6772. if (totalLen > 180) tfMessages.push(addMessage(new HTML('file "' + safeText(fileName).bold() +
  6773. '" exceeding allowed length (' + totalLen + ' > 180)'), 'warning'));
  6774. if (/\.(?:torrent|\!ut|\!qb|url|lnk)$/i.test(fileName)) {
  6775. tfMessages.push(addMessage(new HTML('forbidden file "' + safeText(fileName).bold() + '"'), 'warning'));
  6776. }
  6777. });
  6778. ref = document.querySelector('td.ua-messages-bg');
  6779. if (ref != null && ref.childElementCount <= 0) ref.parentNode.remove();
  6780. };
  6781. fr.onerror = fr.ontimeout = error => { console.error('FileReader error (' + torrent.name + ')') };
  6782. fr.readAsArrayBuffer(torrent);
  6783.  
  6784. function bdecode(str) {
  6785. var pos = 0, infoBegin = 0, infoEnd = 0;
  6786. return bdecodeInternal(str);
  6787.  
  6788. function bdecodeInternal(str) {
  6789. if (pos > str.length) return null;
  6790. switch (str[pos]) {
  6791. case 100: // char code for 'd'
  6792. ++pos;
  6793. var retval = [];
  6794. while (str[pos] != 101){ // char code for 'e'
  6795. var key = bdecodeInternal(str);
  6796. var val = bdecodeInternal(str);
  6797. if (key === null || val === null) break;
  6798. retval[key] = val;
  6799. }
  6800. if(infoEnd == -1) infoEnd = pos + 1;
  6801. retval.isDct = true;
  6802. ++pos;
  6803. return retval;
  6804. case 108: // char code for 'l'
  6805. ++pos;
  6806. retval = [];
  6807. while (str[pos] != 101){ // char code for 'e'
  6808. let val = bdecodeInternal(str);
  6809. if (val === null) break;
  6810. retval.push(val);
  6811. }
  6812. ++pos;
  6813. return retval;
  6814. case 105: // char code for 'i'
  6815. ++pos;
  6816. var digits = Array.prototype.indexOf.call(str, 101, pos) - pos; // 101 = char code for 'e'
  6817. val = '';
  6818. for (var i = pos; i < digits + pos; ++i) val += String.fromCharCode(str[i]);
  6819. val = Math.round(parseFloat(val));
  6820. pos += digits + 1;
  6821. return val;
  6822. default:
  6823. digits = Array.prototype.indexOf.call(str, 58, pos) - pos; // 58 = char code for ':'
  6824. if (digits < 0 || digits > 20) return null;
  6825. var len = '';
  6826. for (i = pos; i < digits + pos; ++i) len += String.fromCharCode(str[i]);
  6827. len = parseInt(len);
  6828. pos += digits + 1;
  6829. var fstring = '';
  6830. for (i = pos; i < len + pos; ++i) fstring += String.fromCharCode(str[i]);
  6831. pos += len;
  6832. if(fstring == 'info') {
  6833. infoBegin = pos;
  6834. infoEnd = -1;
  6835. }
  6836. return fstring;
  6837. }
  6838. }
  6839. }
  6840. }
  6841.  
  6842. function localFetch(url, params, data) {
  6843. return url ? new Promise(function(resolve, reject) {
  6844. var xhr = new XMLHttpRequest();
  6845. xhr.open(getParam('method') || 'GET', url, true);
  6846. if ((xhr.responseType = getParam('responseType') || 'document') == 'json') {
  6847. xhr.setRequestHeader('Accept', 'application/json');
  6848. }
  6849. var headers = getParam('headers');
  6850. if (typeof headers == 'object') Object.keys(headers).forEach(key => { xhr.setRequestHeader(key, headers[key]) });
  6851. xhr.onload = function() {
  6852. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.response); else reject(defaultErrorHandler(xhr));
  6853. };
  6854. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  6855. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  6856. xhr.timeout = 10000;
  6857. xhr.send(data || getParam('body'));
  6858. }) : Promise.reject(new Error('URL missing'));
  6859.  
  6860. function getParam(key) {
  6861. if (!key || typeof key != 'string' || typeof params != 'object') return undefined;
  6862. key = Object.keys(params).find(_key => _key.toLowerCase() == key.toLowerCase());
  6863. return key && params[key] || undefined;
  6864. }
  6865. }
  6866.  
  6867. function globalFetch(url, params, data) {
  6868. return url ? new Promise(function(resolve, reject) {
  6869. params = Object.assign({}, params || {}, { url: url });
  6870. if (!params.method) params.method = data ? 'POST' : 'GET';
  6871. if (!params.responseType) params.responseType = 'document';
  6872. if (typeof params.headers != 'object') params.headers = {};
  6873. switch (params.responseType.toLowerCase()) {
  6874. case 'document': params.headers.Accept = 'text/html'; break;
  6875. case 'xml': params.headers.Accept = 'text/xml'; break;
  6876. case 'json': params.headers.Accept = 'application/json'; break;
  6877. default: params.headers.Accept = '*/*';
  6878. }
  6879. if (typeof data == 'string') params.headers['Content-Length'] = data.length;
  6880. else if (data instanceof URLSearchParams) params.headers['Content-Length'] = data.toString().length;
  6881. else if (data instanceof ArrayBuffer) params.headers['Content-Length'] = data.byteLength;
  6882. if (data) params.data = data instanceof URLSearchParams ? data.toString() : data;
  6883. if (data instanceof URLSearchParams) params.headers['Content-Type'] = 'application/x-www-form-urlencoded';
  6884. params.onload = function(response) {
  6885. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  6886. switch (params.responseType.toLowerCase()) {
  6887. case 'document':
  6888. case 'html':
  6889. response.document = domParser.parseFromString(response.responseText, 'text/html');
  6890. break;
  6891. case 'xml':
  6892. response.document = domParser.parseFromString(response.responseText, 'text/xml');
  6893. break;
  6894. }
  6895. resolve(response);
  6896. };
  6897. if (params.method == 'HEAD') params.onreadystatechange = function(response) {
  6898. if (response.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  6899. resolve(response);
  6900. hXHR.abort();
  6901. };
  6902. params.onerror = error => reject(defaultErrorHandler(error));
  6903. params.ontimeout = timeout => reject(defaultTimeoutHandler(timeout));
  6904. var hXHR = GM_xmlhttpRequest(params);
  6905. }) : Promise.reject(new Error('URL missing'));
  6906. }