[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-05-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name [RED/OPS/NWCD] Upload Assistant
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.227
  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://greasyfork.org/scripts/393837-qobuzlib/code/QobuzLib.js
  30. // @require https://greasyfork.org/scripts/401725-xhrlib/code/xhrLib.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. // //@require https://connect.soundcloud.com/sdk/sdk-3.3.2.js
  36. // ==/UserScript==
  37.  
  38. // Additional setup: to work, set the pattern below as built-in foobar2000 copy command or custom Text Tools plugin quick copy command
  39. // $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%) ][MD5=$info(md5)])$char(30)[%lyrics%],$char(13),$char(29)),$char(10),$char(28))
  40. //
  41. // As alternative to pasted playlist, e.g. requests creation, valid URL to page on supported web can be used.
  42. // List of supported domains:
  43. //
  44. // For music releases:
  45. // - qobuz.com
  46. // - highresaudio.com
  47. // - bandcamp.com
  48. // - prestomusic.com
  49. // - discogs.com
  50. // - supraphonline.cz
  51. // - bontonland.cz (closing soon)
  52. // - nativedsd.com
  53. // - junodownload.com
  54. // - hdtracks.com
  55. // - deezer.com
  56. // - spotify.com
  57. // - prostudiomasters.com
  58. // - play.google.com
  59. // - 7digital.com
  60. // - e-onkyo.com
  61. // - acousticsounds.com
  62. // - indies.eu
  63. // - beatport.com
  64. // - traxsource.com
  65. // - musicbrainz.org
  66. // - music.apple.com
  67. // - vgmdb.net
  68. // - tidal.com (requires account)
  69. // - ototoy.jp
  70. // - music.yandex.ru
  71. // - mora.jp
  72. // - allmusic.com
  73. //
  74. // For e-bbook releases:
  75. // - martinus.cz, martinus.sk
  76. // - goodreads.com
  77. // - databazeknih.cz
  78. //
  79. // For application releases:
  80. // - sanet.st
  81.  
  82. 'use strict';
  83.  
  84. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || window.InstallTrigger;
  85.  
  86. function testDomain(domain) {
  87. return document.location.hostname.toLowerCase() == domain.toLowerCase();
  88. }
  89. function testPath(path, query) {
  90. return document.location.pathname.toLowerCase() == '/'.concat(path.toLowerCase(), '.php')
  91. && (!query || document.location.search.toLowerCase().startsWith('?'.concat(query.toLowerCase())));
  92. }
  93.  
  94. const isRED = testDomain('redacted.ch');
  95. const isNWCD = testDomain('notwhat.cd');
  96. const isOPS = testDomain('orpheus.network');
  97.  
  98. const isUpload = testPath('upload');
  99. const isEdit = testPath('torrents', 'action=editgroup&');
  100. const isRequestNew = testPath('requests', 'action=new');
  101. const isRequestEdit = testPath('requests', 'action=edit&');
  102. const isAddFormat = isUpload && /\bgroupid=(\d+)\b/i.test(document.location.search);
  103.  
  104. const urlParser = /^\s*(https?:\/\/\S+)\s*$/i;
  105. const dcRlsParser = /^https?:\/\/(?:\w+\.)*discogs\.com\/releases?\/(\d+)(?=$|\/|\?)/i;
  106. const itunesRlsParser = /^https?:\/\/(?:\w+\.)*apple\.com\/.*\/(\d+)(?=$|\?)/i;
  107. const mbrRlsParser = /^https?:\/\/(?:beta\.)?musicbrainz\.org\/(?:\w+\/)*release\/([\w\-]+)/i;
  108. const dzrRlsParser = /^https?:\/\/(?:\w+\.)*deezer\.com\/(\w+\/)*album\/(\d+)$/i;
  109. const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'];
  110. const ptpimgOrigin = 'https://ptpimg.me';
  111. const mbrRlsPrefix = 'https://musicbrainz.org/release/';
  112. const discogsOrigin = 'https://www.discogs.com';
  113. const deezerAlbumPrefix = 'https://www.deezer.com/album/';
  114. const descriptionFields = ['album_desc', 'body', 'description', 'release_desc', 'release_lineage'];
  115. const siteApiTimeframeStorageKey = document.location.hostname.concat(' API time frame');
  116.  
  117. const spotify_clientid = '6d358a207c634b1ebac640149a6090da';
  118. const spotify_clientsecret = '4c59880a4ec241ed9c89a24e66468c64';
  119. const discogs_token = 'CISOUfiQctZCkUedWJzPhzTXxRYihifZgflZAfEm';
  120. const lastfm_api_key = 'b9f26370d7266fbb3151b2ad4f7a74c9';
  121.  
  122. const ulTimeFactor = 8;
  123. const rehostTimeout = 30000;
  124. const gazelleApiFrame = 10500;
  125. const ctxt = document.createElement('canvas').getContext('2d');
  126.  
  127. var prefs = {
  128. autfill_delay: 500, // delay in ms to autofill form after pasting text into box, 0 to disable
  129. clean_on_apply: false, // clean the input box on successfull fill
  130. cleanup_descriptions: true, // pre-submit cleanup to all description fields (remove empty placeholders, redundant info and garbage like empty tag pairs etc.)
  131. keep_meaningles_composers: false, // keep composers from file tags also for non-composer emphasing genres
  132. 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)
  133. single_threshold: 10 * 60, // For autodetection of release type: max length of single in s
  134. EP_threshold: 30 * 60, // For autodetection of release type: max time of EP in s
  135. auto_rehost_cover: true, // PTPIMG / using 3rd party script
  136. auto_preview_cover: true,
  137. huge_image_warning: 5, // threshold in MB for making bandwith stressing cover size warning // 0 to disable
  138. cover_lookup_provider: 'all', // itunes | lastfm | deezer | musicbrainz | qobuz | google | all | empty for no lookup
  139. 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
  140. estimate_decade_tag: true, // deduce decade tag (1980s, etc.) from album year for regular albums
  141. check_whitespace: true, // check tags for leading/trailing spaces and unreadable characters
  142. honour_rg: false, // do a reminder on missing RG info; off by default
  143. honour_dr: false, // do a reminder on missing DR info (only for Hi-Res tracks); off by default
  144. honour_url: false, // do a reminder on missing source URL (tag URL)
  145. ops_always_edition: true, // (only new uploads) don't use original release but always specific edition (unify with other trackers)
  146. sacd_decoder: 'foobar2000\'s SACD decoder (DSD2PCM direct / 64fp / 30kHz lowpass)',
  147. use_store_logos: true, // use online source's pictograsm instead of url in textual form (if defined)
  148. insert_release_date: true, // ..to rls description
  149. selfrelease_label: 'self-released',
  150. upcoming_tags: '', // add this tag(s) to upcoming releases (requests); empty to disable
  151. remap_texttools_newlines: false, // convert underscores to linebreaks (ambiguous)
  152. messages_verbosity: 0,
  153. diag_mode: false,
  154. // online parsers specific
  155. apple_offer_alt_cover: true, // usually smaller version of preloaded cover
  156. use_kana: false, // include Kana(JP) version in artist/title names; applies to mora.jp online parser
  157. // online service credentials
  158. redacted_api_key: '',
  159. ptpimg_api_key: '',
  160. discogs_key: '', // Applicxation/Consumer Key
  161. discogs_secret: '', // Application/Consumer Secret
  162. //soundcloud_clientid: '',
  163. tidal_userid: '',
  164. tidal_userpassword: '',
  165. malzo_uid: '',
  166. malzo_password: '',
  167. imgbb_uid: '',
  168. imgbb_password: '',
  169. catbox_userhash: '',
  170. imagevenue_uid: '',
  171. imagevenue_password: '',
  172. imgbox_uid: '',
  173. imgbox_password: '',
  174. // request specific
  175. request_default_bounty: 0, // set this bounty in MB after successfull fill of request form / 0 for disable
  176. always_request_perfect_flac: false,
  177. include_tracklist_in_request: false, // false: include one line summary only; true: include full tracklisting
  178. // tracklist specific
  179. tracklist_style: 1, // 1: classic with components colouring, 2: propertional font right-justified, 3: classic center aligned
  180. singles_conventional_format: false, // force one track singles to be formatted same way as albums with numbered tracklist
  181. colorless_tracklist: false, // Strip all colours from tracklist
  182. sort_tracklist: true,
  183. reformat_trackartist: true, // (if track artist differs from main artist) rebuild track artist from partial track artists, turn off if generating wrong track artists
  184. max_tracklist_width: 80, // right margin of the right aligned tracklist. should not exceed the group description width on any device
  185. tracklist_size: 2, // PHPBB font size
  186. title_separator: '. ', // divisor of track# and title
  187. pad_leader: ' ',
  188. bpm_summary: true,
  189. tracklist_head_color: '#778899', // #4682B4 / #a7bdd0
  190. // classical tracklist only components colouring
  191. tracklist_disctitle_color: '#2bb7b7', // #bb831c
  192. tracklist_work_color: '#98984d', // #b16890
  193. tracklist_tracknumber_color: '#8899AA',
  194. tracklist_artist_color: '#8a6995',
  195. tracklist_composer_color: '#8ca014',
  196. tracklist_duration_color: '#33a6cc', // #2196f3
  197. // online check paramaters
  198. check_integrity_online: true, // If provided URL tag, compare local release with release online and lookup for discrepancies
  199. strict_online_check: false, // set to true for strict online check (metadata comparison is case sensitive)
  200. duration_divergency: 0.75, // maximum tolerated playlists difference in %
  201. vinyl_duration_divergency: 2.5, // maximum tolerated playlists difference in % for vinyl releases
  202.  
  203. save: function() {
  204. for (var key in this) {
  205. if (typeof this[key] != 'function' && this[key] != undefined) GM_setValue(key, this[key]);
  206. }
  207. },
  208. };
  209. Object.keys(prefs).forEach(key => { prefs[key] = GM_getValue(key, prefs[key]) });
  210.  
  211. document.head.appendChild(document.createElement('style')).innerHTML = `
  212. .ua-messages {
  213. text-indent: -2em;
  214. margin-left: 2em;
  215. font: 8pt Verdana, Tahoma, sans-serif;
  216. }
  217. .ua-messages-bg { padding: 15px; text-align: left; background-color: darkslategray; }
  218.  
  219. .ua-critical { color: red; font-weight: bold; font-size: 10pt; }
  220. .ua-warning { color: #ff8d00; font-weight: 500; font-size: 9pt; }
  221. .ua-notice { color: #e3d67b; }
  222. .ua-info { color: white; }
  223.  
  224. .ua-button { vertical-align: middle; background-color: transparent; }
  225. .ua-button2 { /*color: beige; */width: 13em; font: 300 x-small "Segoe UI", Calibri, sans-serif; }
  226. .ua-input {
  227. font: 600 x-small "Segoe UI", Calibri, sans-serif;
  228. color: slategray; background-color: antiquewhite;
  229. width: 620px; height: 40px;
  230. margin-top: 8px; margin-bottom: 8px;
  231. }
  232. .ua-input:focus { color: black; }
  233.  
  234. #cover-preview {
  235. width: 100%;
  236. /*box-shadow: 3px 3px 3px;*/
  237. }
  238. #cover-size {
  239. width: 100%;
  240. color: white; background-color: #0a4a75;
  241. font: 8.5pt Verdana, Tahoma, sans-serif;
  242. text-align: center;
  243. /*padding-top: 5px;*/
  244. }
  245.  
  246. ::placeholder {
  247. font: bold 12pt Calibri, "Segoe UI", sans-serif;
  248. color: #808080;
  249. /*text-shadow: 0px 0px 3px #b4b4b4;*/
  250. }
  251. `;
  252.  
  253. var ref, tbl, elem, child, messages = null, autofill = false, dom;
  254. var tfMessages = [], siteArtistsCache = {}, notSiteArtistsCache = [];
  255.  
  256. if (isUpload) {
  257. if ((ref = document.querySelector('form#upload_table > div#dynamic_form')) == null) return;
  258. common1();
  259. let x = [];
  260. x.push(document.createElement('tr'));
  261. x[0].classList.add('ua-button');
  262. child = document.createElement('input');
  263. child.id = 'fill-from-text';
  264. child.value = 'Fill form (overwrite)';
  265. child.type = 'button';
  266. child.className = 'ua-button2';
  267. child.onclick = fillFromText;
  268. x[0].append(child);
  269. elem.append(x[0]);
  270. x.push(document.createElement('tr'));
  271. x[1].classList.add('ua-button');
  272. child = document.createElement('input');
  273. child.id = 'fill-from-text-weak';
  274. child.value = 'Fill form (keep values)';
  275. child.type = 'button';
  276. child.className = 'ua-button2';
  277. child.onclick = fillFromText;
  278. x[1].append(child);
  279. elem.append(x[1]);
  280. common2();
  281. ref.parentNode.insertBefore(tbl, ref);
  282. } else if (isEdit) {
  283. if ((ref = document.querySelector('form.edit_form > div > div > input[type="submit"]')) == null) return;
  284. ref = ref.parentNode;
  285. ref.parentNode.insertBefore(document.createElement('br'), ref);
  286. common1();
  287. child = document.createElement('input');
  288. child.id = 'append-from-text';
  289. child.value = 'Fill from text (append)';
  290. child.type = 'button';
  291. child.className = 'ua-button2';
  292. child.style.height = '52px';
  293. child.onclick = fillFromText;
  294. elem.append(child);
  295. common2();
  296. tbl.style.marginBottom = '10px';
  297. ref.parentNode.insertBefore(tbl, ref);
  298. } else if (isRequestNew) {
  299. if ((ref = document.getElementById('categories')) == null) return;
  300. ref = ref.parentNode.parentNode.nextElementSibling;
  301. ref.parentNode.insertBefore(document.createElement('br'), ref);
  302. common1();
  303. child = document.createElement('input');
  304. child.id = 'fill-from-text-weak';
  305. child.value = 'Fill from URL';
  306. child.type = 'button';
  307. child.className = 'ua-button2';
  308. child.style.height = '52px';
  309. child.onclick = fillFromText;
  310. elem.append(child);
  311. common2();
  312. child = document.createElement('td');
  313. child.colSpan = 2;
  314. child.append(tbl);
  315. elem = document.createElement('tr');
  316. elem.append(child);
  317. ref.parentNode.insertBefore(elem, ref);
  318. } else if (isRequestEdit) {
  319. if ((ref = document.querySelector('input#button[type="submit"]')) == null) return;
  320. ref = ref.parentNode.parentNode;
  321. ref.parentNode.insertBefore(document.createElement('br'), ref);
  322. common1();
  323. child = document.createElement('input');
  324. child.id = 'append-from-text';
  325. child.value = 'Fill from text (append)';
  326. child.type = 'button';
  327. child.className = 'ua-button2';
  328. child.style.height = '52px';
  329. child.onclick = fillFromText;
  330. elem.append(child);
  331. common2();
  332. tbl.style.marginBottom = '10px';
  333. elem = document.createElement('tr');
  334. child = document.createElement('td');
  335. child.colSpan = 2;
  336. child.append(tbl);
  337. elem.append(child);
  338. ref.parentNode.insertBefore(elem, ref);
  339. }
  340.  
  341. function common1() {
  342. tbl = document.createElement('tr');
  343. tbl.style.backgroundColor = 'darkgoldenrod';
  344. tbl.style.verticalAlign = 'middle';
  345. elem = document.createElement('td');
  346. elem.style.textAlign = 'center';
  347. child = document.createElement('textarea');
  348. child.id = 'UA-data';
  349. child.name = 'UA-data';
  350. child.className = 'ua-input';
  351. child.spellcheck = false;
  352. child.placeholder = 'Paste/drop album from foobar2000 or release URL here';
  353. child.onpaste = uaInsert;
  354. if (!isNWCD) {
  355. child.ondrop = uaInsert;
  356. child.ondragover = clear0;
  357. if (isFirefox) child.oninput = fixFirefoxDropBug;
  358. } else child.ondrop = child.ondragstart = child.ondragover = function(evt) {
  359. evt.preventDefault();
  360. evt.stopPropagation();
  361. return false;
  362. };
  363. var desc = document.getElementById('body');
  364. if (desc != null && urlParser.test(desc.value)) {
  365. child.value = RegExp.$1;
  366. desc.value = '';
  367. if (prefs.autfill_delay > 0) {
  368. autofill = true;
  369. setTimeout(fillFromText, prefs.autfill_delay);
  370. };
  371. }
  372. elem.append(child);
  373. tbl.append(elem);
  374. elem = document.createElement('td');
  375. elem.style.textAlign = 'center';
  376. }
  377. function common2() {
  378. tbl.append(elem);
  379. var tb = document.createElement('tbody');
  380. tb.append(tbl);
  381. tbl = document.createElement('table');
  382. tbl.id = 'upload assistant';
  383. tbl.append(tb);
  384. }
  385.  
  386. if ((ref = document.getElementById('categories')) != null) {
  387. ref.addEventListener('change', function(e) {
  388. elem = document.getElementById('upload assistant');
  389. if (elem != null) elem.style.visibility = this.value < 4
  390. || ['Music', 'Applications', 'E-Books', 'Audiobooks'].includes(this.value) ? 'visible' : 'collapse';
  391. setTimeout(setHandlers, 2000);
  392. });
  393. }
  394.  
  395. if ((ref = document.getElementById('upload-table') || document.querySelector('form.edit_form')
  396. || document.getElementById('upload_table') || document.getElementById('request_form')) != null) {
  397. ref.ondragover = voidDragHandler1;
  398. ref.ondrop = voidDragHandler1;
  399. }
  400. setHandlers();
  401. if ((ref = isUpload ? document.getElementById('file') : null) != null) {
  402. ref.oninput = function(evt) { if (evt.target.files.length > 0) validataTorrentFile(evt.target.files[0]) };
  403. if (ref.files.length > 0) validataTorrentFile(ref.files[0]);
  404. }
  405. if (!isRED && (ref = document.querySelector('table#dnulist')) != null) {
  406. function toggleVisibility() {
  407. var show = ref.style.display.toLowerCase() == 'none';
  408. ref.style.display = show ? 'block' : 'none';
  409. ref.previousElementSibling.style.display = show ? 'block' : 'none';
  410. }
  411. toggleVisibility();
  412. if ((ref = document.querySelector('h3#dnu_header')) != null) {
  413. elem = ref.parentNode;
  414. child = document.createElement('a');
  415. child.href = '#';
  416. child.onclick = function(evt) {
  417. if ((ref = document.querySelector('table#dnulist')) != null) toggleVisibility();
  418. };
  419. child.append(ref);
  420. elem.prepend(child);
  421. }
  422. }
  423.  
  424. if (isRequestNew) {
  425. let title = document.querySelector('input[name="title"]');
  426. if (title != null) for (i = 1; i < 6; ++i) setTimeout(function(e) { title.readOnly = false }, i * 1000);
  427. }
  428.  
  429. Array.prototype.includesCaseless = function(str) {
  430. if (typeof str != 'string') return false;
  431. str = str.toLowerCase();
  432. return this.find(elem => typeof elem == 'string' && elem.toLowerCase() == str) != undefined;
  433. };
  434. Array.prototype.pushUnique = function(...items) {
  435. items.forEach(it => { if (!this.includes(it)) this.push(it) });
  436. return this.length;
  437. };
  438. Array.prototype.pushUniqueCaseless = function(...items) {
  439. items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  440. return this.length;
  441. };
  442. // Array.prototype.getUnique = function(prop) {
  443. // return this.every((it) => it[prop] && it[prop] == this[0][prop]) ? this[0][prop] : null;
  444. // };
  445. Array.prototype.equalTo = function(arr) {
  446. return Array.isArray(arr) && arr.length == this.length
  447. && Array.from(arr).sort().toString() == Array.from(this).sort().toString();
  448. };
  449. Array.prototype.equalCaselessTo = function(arr) {
  450. function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
  451. return Array.isArray(arr) && arr.length == this.length
  452. && arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
  453. };
  454. Array.prototype.homogeneous = function() {
  455. return this.every(elem => elem === this[0]);
  456. }
  457. Array.prototype.flatten = function() {
  458. return this.reduce(function(flat, toFlatten) {
  459. return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
  460. }, []);
  461. }
  462.  
  463. String.prototype.toASCII = function() {
  464. return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
  465. };
  466. String.prototype.trueLength = function() {
  467. return Array.from(this).length;
  468. //return this.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').length;
  469. // var index = 0, width = 0, len = 0;
  470. // while (index < this.length) {
  471. // var point = this.codePointAt(index);
  472. // width = 0;
  473. // while (point) {
  474. // ++width;
  475. // point = point >> 8;
  476. // }
  477. // index += Math.round(width / 2);
  478. // ++len;
  479. // }
  480. // return len;
  481. };
  482. String.prototype.flatten = function() {
  483. return this.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
  484. };
  485. String.prototype.expand = function() {
  486. return this.replace(/\x1D/g, '\r').replace(/\x1C/g, '\n');
  487. };
  488. String.prototype.titleCase = function() {
  489. return this.toLowerCase().split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(' ');
  490. };
  491. String.prototype.collapseGaps = function() {
  492. return this.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/\[(\w+)\]\[\/\1\]/ig,'').trim();
  493. };
  494. Date.prototype.getDateValue = function() {
  495. return Math.floor((this.getTime() / 1000 / 60 - this.getTimezoneOffset()) / 60 / 24);
  496. };
  497. Date.prototype.isExactDate = function() {
  498. return this.getUTCMilliseconds() > 0 || this.getUTCSeconds() > 0 || this.getUTCMinutes() > 0 || this.getUTCHours() > 0
  499. || this.getUTCDate() > 1 || this.getUTCMonth() > 0;
  500. };
  501. File.prototype.getText = function(encoding) {
  502. return new Promise(function(resolve, reject) {
  503. var reader = new FileReader();
  504. reader.onload = function() { resolve(reader.result) };
  505. reader.onerror = reader.ontimeout = error => { reject('FileReader error (' + this.name + ')') };
  506. reader.readAsText(this, encoding);
  507. }.bind(this));
  508. };
  509. class HTML extends String { };
  510.  
  511. const excludedCountries = [
  512. /\b(?:United\s+States|USA?)\b/,
  513. /\b(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|UK)\b/,
  514. /\b(?:Europe|European\s+Union|EU)\b/,
  515. /\b(?:Unknown)\b/,
  516. ];
  517.  
  518. class TagManager extends Array {
  519. constructor(...tags) {
  520. super();
  521. this.presubstitutions = [
  522. [/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
  523. [/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
  524. [/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
  525. [/\s*,\s*(?:&\s*|and\s+)/i, ' & '],
  526. ];
  527. this.substitutions = [
  528. [/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
  529. [/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
  530. [/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
  531. ['AOR', 'album.oriented.rock'],
  532. [/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
  533. [/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
  534. [/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
  535. ['World', 'world.music'],
  536. [/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
  537. [/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhytm.and.blues'],
  538. [/\b(?:Soundtracks?)$/i, 'score'],
  539. ['Electro', 'electronic'],
  540. ['Metal', 'heavy.metal'],
  541. ['NonFiction', 'non.fiction'],
  542. ['Rap', 'hip.hop'],
  543. ['NeoSoul', 'neo.soul'],
  544. ['NuJazz', 'nu.jazz'],
  545. [/^J[\s\-]Pop$/i, 'jpop'],
  546. [/^K[\s\-]Pop$/i, 'jpop'],
  547. [/^J[\s\-]Rock$/i, 'jrock'],
  548. ['Hardcore', 'hardcore.punk'],
  549. ['Garage', 'garage.rock'],
  550. [/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
  551. [/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
  552. [/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
  553. [/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
  554. [/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
  555. ['GoaTrance', 'goa.trance'],
  556. [/^Female\s+Vocal\w*$/i, 'female.vocalist'],
  557. ['Contemporary R&B', 'contemporary.rhytm.and.blues'],
  558. // Country aliases
  559. ['Canada', 'canadian'],
  560. ['Australia', 'australian'],
  561. ['New Zealand', 'new.zealander'],
  562. ['Japan', 'japanese'],
  563. ['Taiwan', 'thai'],
  564. ['China', 'chinese'],
  565. ['Singapore', 'singaporean'],
  566. [/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
  567. ['Turkey', 'turkish'],
  568. ['Israel', 'israeli'],
  569. ['France', 'french'],
  570. ['Germany', 'german'],
  571. ['Spain', 'spanish'],
  572. ['Italy', 'italian'],
  573. ['Sweden', 'swedish'],
  574. ['Norway', 'norwegian'],
  575. ['Finland', 'finnish'],
  576. ['Greece', 'greek'],
  577. [/^(?:Netherlands|Holland)$/i, 'dutch'],
  578. ['Belgium', 'belgian'],
  579. ['Luxembourg', 'luxembourgish'],
  580. ['Denmark', 'danish'],
  581. ['Switzerland', 'swiss'],
  582. ['Austria', 'austrian'],
  583. ['Portugal', 'portugese'],
  584. ['Ireland', 'irish'],
  585. ['Scotland', 'scotish'],
  586. ['Iceland', 'icelandic'],
  587. [/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
  588. [/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
  589. ['Hungary', 'hungarian'],
  590. ['Poland', 'polish'],
  591. ['Estonia', 'estonian'],
  592. ['Latvia', 'latvian'],
  593. ['Lithuania', 'lithuanian'],
  594. ['Moldova', 'moldovan'],
  595. ['Armenia', 'armenian'],
  596. ['Ukraine', 'ukrainian'],
  597. ['Yugoslavia', 'yugoslav'],
  598. ['Serbia', 'serbian'],
  599. ['Slovenia', 'slovenian'],
  600. ['Croatia', 'croatian'],
  601. ['Macedonia', 'macedonian'],
  602. ['Montenegro', 'montenegrin'],
  603. ['Romania', 'romanian'],
  604. ['Malta', 'maltese'],
  605. ['Brazil', 'brazilian'],
  606. ['Mexico', 'mexican'],
  607. ['Argentina', 'argentinean'],
  608. ['Jamaica', 'jamaican'],
  609. // Books
  610. ['Beletrie', 'fiction'],
  611. ['Satira', 'satire'],
  612. ['Komiks', 'comics'],
  613. ['Komix', 'comics'],
  614. // Removals
  615. ['Unknown'],
  616. ['Other'],
  617. ['New'],
  618. ['Ostatni'],
  619. ['Knihy'],
  620. ['Audioknihy'],
  621. ['dsbm'],
  622. [/^(?:Audio\s*kniha|Audio\s*Book)$/i],
  623. ].concat(excludedCountries.map(it => [it]));
  624. this.splits = [
  625. ['Alternative', 'Indie'],
  626. ['Rock', 'Pop'],
  627. ['Soul', 'Funk'],
  628. ['Ska', 'Rocksteady'],
  629. ['Jazz Fusion', 'Jazz Rock'],
  630. ['Rock', 'Pop'],
  631. ['Jazz', 'Funk'],
  632. ];
  633. this.additions = [
  634. [/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
  635. [/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Vocal|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Latin|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
  636. [/^(?:Opera)$/i, 'classical'],
  637. [/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
  638. [/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
  639. [/^(?:Symphony)$/i, 'classical'],
  640. [/^(?:Sacred\s+Vocal)\b/i, 'classical'],
  641. [/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
  642. ];
  643. if (tags.length > 0) this.add(...tags);
  644. }
  645.  
  646. add(...tags) {
  647. var added = 0;
  648. for (var tag of tags) {
  649. if (typeof tag != 'string') continue;
  650. qobuzTranslations.forEach(function(it) { if (tag.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) tag = it[1] });
  651. this.presubstitutions.forEach(k => { if (k[0].test(tag)) tag = tag.replace(k[0], k[1]) });
  652. tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(function(tag) {
  653. //qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
  654. tag = tag.toASCII().replace(/\(.*?\)|\[.*?\]|\{.*?\}/g, '').trim();
  655. if (tag.length <= 0 || tag == '?') return null;
  656. function test(obj) {
  657. return typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase()
  658. || obj instanceof RegExp && obj.test(tag);
  659. }
  660. for (var k of this.substitutions) {
  661. if (test(k[0])) {
  662. if (k.length >= 1) added += this.add(...k.slice(1));
  663. else addMessage('invalid tag \'' + tag + '\' found', 'warning');
  664. return;
  665. }
  666. }
  667. for (k of this.additions) {
  668. if (test(k[0])) added += this.add(...k.slice(1));
  669. }
  670. for (k of this.splits) {
  671. if (new RegExp('^' + k[0] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[1] + '$', 'i').test(tag)) {
  672. added += this.add(k[0], k[1]); return;
  673. }
  674. if (new RegExp('^' + k[1] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[0] + '$', 'i').test(tag)) {
  675. added += this.add(k[0], k[1]); return;
  676. }
  677. }
  678. tag = tag.
  679. replace(/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1').
  680. replace(/\b(?:Alt\.)(?=\s+)/i, 'Alternative').
  681. replace(/^[3-9]0s$/i, '19$0').
  682. replace(/^[0-2]0s$/i, '20$0').
  683. replace(/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2').
  684. replace(/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and ').
  685. replace(/[\s\-\−\—\–\_\.\,\'\`\~]+/g, '.').
  686. replace(/[^\w\.]+/g, '').
  687. toLowerCase();
  688. if (tag.length >= 2 && !this.includes(tag)) {
  689. this.push(tag);
  690. ++added;
  691. }
  692. }.bind(this));
  693. }
  694. return added;
  695. }
  696. toString() { return Array.from(this).sort().join(', ') }
  697. };
  698.  
  699. return;
  700.  
  701. function fillFromText(evt) {
  702. if (evt == undefined && !autofill) return;
  703. autofill = false;
  704. var overwrite = this.id == 'fill-from-text';
  705. var clipBoard = document.getElementById('UA-data');
  706. if (clipBoard == null) return false;
  707. messages = document.getElementById('UA-messages');
  708. //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
  709. //if (typeof clipBoard != 'string') return false;
  710. var i, matches, sourceUrl, category = document.getElementById('categories'), xhr = new XMLHttpRequest();
  711. if (category == null && document.getElementById('releasetype') != null
  712. || category != null && (category.value == 0 || category.value == 'Music')) return fillFromText_Music();
  713. if (category != null && (category.value == 1 || category.value == 'Applications')) return fillFromText_Apps();
  714. if (category != null && (category.value == 2 || category.value == 3
  715. || category.value == 'E-Books' || category.value == 'Audiobooks')) return fillFromText_Ebooks();
  716. return category == null ? fillFromText_Apps(true).catch(reason => fillFromText_Ebooks()) : Promise.reject('no category');
  717.  
  718. function fillFromText_Music() {
  719. if (messages != null) messages.parentNode.removeChild(messages);
  720. const divs = ['—', '⸺', '⸻'];
  721. const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  722. const VA = 'Various Artists';
  723. const multiArtistParsers = [
  724. /\s*[,;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?\s*/,
  725. /\s+[\/\|\×]\s+/,
  726. ];
  727. const pseudoArtistParsers = [
  728. /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
  729. /^(?:traditional|lidová)$/i,
  730. /\b(?:traditional|lidová)$/,
  731. /^(?:tradiční|lidová)\s+/,
  732. /^(?:[Aa]nonym)/,
  733. /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  734. /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  735. /^(?:Various\s+Composers)$/i,
  736. /^(?:Guests|Friends)$/i,
  737. ];
  738. const ampersandParsers = [
  739. /\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
  740. /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
  741. /\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
  742. /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
  743. ];
  744. const featParsers = [
  745. /\s+(?:meets)\s+(.*?)\s*$/i,
  746. /\s+(?:[Ww]ith)\s+(?!his\b|her\b|Friends$|Strings$)(.*?)\s*$/,
  747. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:eaturing|t\.))\s+(.*?)\s*$/,
  748. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:ea)?t\.)\s+(.*?)\s*$/, // [0]
  749. /\s+\[\s*f(?:eat(?:\.|uring)|t\.)\s+([^\[\]]+?)\s*\]/i, // [1]
  750. /\s+\(\s*f(?:eat(?:\.|uring)|t\.)\s+([^\(\)]+?)\s*\)/i, // [2]
  751. /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i, // [3]
  752. /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i, // [4]
  753. /\s+\[\s*with\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/, // [5]
  754. /\s+\(\s*with\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/, // [6]
  755. ];
  756. const remixParsers = [
  757. /\s+\((?:The\s+)Remix(?:e[sd])?\)/i,
  758. /\s+\[(?:The\s+)Remix(?:e[sd])?\]/i,
  759. /\s+(?:The\s+)Remix(?:e[sd])?\s*$/i,
  760. /^(Remixes)\b/,
  761. /\s+\(([^\(\)]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\)/i,
  762. /\s+\[([^\[\]]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\]/i,
  763. /\s+\(\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
  764. /\s+\[\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
  765. ];
  766. const otherArtistsParsers = [
  767. [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
  768. [/^()(.*?)\s+\(conductor\)$/i, 4],
  769. //[/^()(.*?)\s+\(.*\)$/i, 1],
  770. ];
  771. const artistStrips = [
  772. /\s+(?:aka|AKA)\.?\s+(.*)$/,
  773. /\s+\(([^\(\)]+)\)$/,
  774. /\s+\[([^\[\]]+)\]$/,
  775. /\s+\{([^\{\}]+)\}$/,
  776. ];
  777. var isVA, onlineSource = urlParser.test(clipBoard.value) && RegExp.$1, ajaxRejects = 0;
  778. return (onlineSource ? urlResolver(onlineSource).then(fetchOnline_Music) :
  779. Promise.resolve(clipBoard.value.split(/(?:\r?\n)+/).filter(line => line.trim().length > 0).map(function(line, ndx) {
  780. const fields = [
  781. /* 00 */ 'artist', 'album', 'album_year', 'release_date', 'label', 'catalog', 'country', 'encoding',
  782. /* 08 */ 'codec', 'codec_profile', 'bitrate', 'bd', 'sr', 'channels', 'media', 'genre', 'discnumber',
  783. /* 17 */ 'totaldiscs', 'discsubtitle', 'tracknumber', 'totaltracks', 'title', 'track_artist', 'performer',
  784. /* 24 */ 'composer', 'conductor', 'remixer', 'compiler', 'producer', 'duration', 'samples', 'filesize',
  785. /* 32 */ 'ag', 'dr', 'vendor', 'url', 'dirpath', 'description', 'identifiers', 'lyrics',
  786. ];
  787. var metaData = line.expand().split('\x1E'), track = { identifiers: {} }, identifiers = [];
  788. if (metaData.length < fields.length) {
  789. console.error('invalid data format for track #' + (ndx + 1) + ': length:', metaData.length,
  790. '(' + fields.length + '); metaData:', metaData, '; line:', line);
  791. throw 'invalid data format for track #' + (ndx + 1) + ' (see console log for details)';
  792. } else if (metaData.length > fields.length) {
  793. console.warn('unexpected data format for track #' + (ndx + 1) + ': length:', metaData.length,
  794. '(' + fields.length + '); metaData:', metaData, '; line:', line);
  795. }
  796. fields.forEach(function(propName) {
  797. if (propName == 'identifiers') {
  798. metaData.shift().trim().split(/\s+/).forEach(function(id) {
  799. if (/^([\w\-]+)[=:](\S*)$/.test(id)) track.identifiers[RegExp.$1.toUpperCase()] = RegExp.$2.replace(/\x1B/g, ' ');
  800. });
  801. } else {
  802. track[propName] = metaData.shift();
  803. if (track[propName] === '') track[propName] = undefined;
  804. }
  805. });
  806. if (prefs.check_whitespace) Object.keys(track).forEach(function(propName) {
  807. if (typeof track[propName] != 'string') return;
  808. if (!['description', 'lyrics'].includes(propName) && (track[propName].includes('\r') || track[propName].includes('\n'))) {
  809. track[propName] = track[propName].replace(/[\r\n]+/g, '');
  810. addMessage('track #' + (ndx + 1) + ' contains linebreaks in tag <' + propName + '>', 'warning');
  811. }
  812. if ((i = ['description', 'lyrics'].includes(propName) ? /[\x00-\x08\x0B\x0C\x0E-\x19]+/g : /[\x00-\x19]+/g).test(track[propName])) {
  813. track[propName] = track[propName].replace(i, '');
  814. addMessage('track #' + (ndx + 1) + ' contains control codes in tag <' + propName + '>', 'warning');
  815. }
  816. if (/^[\s\xA0]+$/.test(track[propName])) {
  817. track[propName] = undefined;
  818. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains only whitespace', 'warning');
  819. } else if (/^[\s\xA0]+|[\s\xA0]+$/.test(track[propName])) {
  820. track[propName] = track[propName].trim();
  821. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains leading/trailing whitespace', 'warning');
  822. }
  823. if (/[ \xA0]{2,}/.test(track[propName])) {
  824. track[propName] = track[propName].replace(/[ \xA0]{2,}/g, ' ')
  825. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains multiple spaces', 'warning');
  826. }
  827. });
  828. ['description', 'lyrics'].forEach(function(propName) {
  829. if (track[propName] == '.') track[propName] = undefined; else if (track[propName]) {
  830. if (prefs.remap_texttools_newlines)
  831. track[propName] = track[propName].replace(/__/g, '\r\n').replace(/_/g, '\n') // ambiguous
  832. track[propName] = track[propName].collapseGaps();
  833. }
  834. });
  835. ['bitrate', 'bd', 'sr', 'channels', 'totaldiscs', 'totaltracks', 'samples', 'filesize', 'dr'].forEach(function(propName) {
  836. if (track[propName] !== undefined) track[propName] = parseInt(track[propName]);
  837. });
  838. ['duration'].forEach(function(propName) {
  839. if (!isNaN(track[propName]) && track[propName] !== null) track[propName] = parseFloat(track[propName]);
  840. });
  841. if (track.album_year) track.album_year = extractYear(track.album_year) || NaN;
  842. ['ag', 'tg', 'ap', 'tp'].forEach(function(propName) {
  843. track[propName] = /^([\+\-]?\d+(?:\.\d+)?)\s*dB\b/i.test(track[propName]) ? parseFloat(RegExp.$1) : undefined;
  844. });
  845. return track;
  846. }))
  847. ).then(parseTracks).catch(e => { if (e) addMessage(e, 'critical') });
  848.  
  849. function parseTracks(tracks) {
  850. if (tracks.length <= 0) {
  851. clipBoard.value = '';
  852. throw 'no tracks found';
  853. }
  854. if (prefs.diag_mode) console.debug('Parsing tracks:', tracks);
  855. var albumBitrate = 0, totalTime = 0, albumSize = 0, media, release = { totaldiscs: 1, srs: [] };
  856. var allowedFormats = Array.from(document.querySelectorAll('select#format > option'))
  857. .filter(option => option.value.length > 0).map(option => option.value);
  858. if (allowedFormats.length <= 0) allowedFormats = ["MP3", "FLAC", "AAC", "AC3", "DTS"];
  859. tracks.forEach(function(track) {
  860. processTrackArtists(track);
  861. if (!track.artist) {
  862. clipBoard.value = '';
  863. throw new HTML('main artist must be defined for every track' + ruleLink('2.3.16.4'));
  864. }
  865. if (!track.album) {
  866. clipBoard.value = '';
  867. throw new HTML('album title must be defined for every track' + ruleLink('2.3.16.4'));
  868. }
  869. if (!track.tracknumber) {
  870. clipBoard.value = '';
  871. throw new HTML('all track numbers must be defined' + ruleLink('2.3.16.4'));
  872. }
  873. if (!track.title) {
  874. clipBoard.value = '';
  875. throw new HTML('all track titles must be defined' + ruleLink('2.3.16.4'));
  876. }
  877. if (track.duration !== undefined && track.duration !== null && isUpload && (isNaN(track.duration) || track.duration <= 0)) {
  878. clipBoard.value = '';
  879. throw 'invalid track #' + track.tracknumber + ' length: ' + track.duration;
  880. }
  881. if (!onlineSource && track.codec && !allowedFormats.includes(track.codec)) {
  882. clipBoard.value = '';
  883. throw 'disallowed codec present (' + track.codec + ')';
  884. }
  885. if (!onlineSource && /\b(?:MQAEncode) v(\d+(?:\.\d+)*)\b/.test(track.vendor)) {
  886. clipBoard.value = '';
  887. throw 'MQA encoded release (' + RegExp.lastMatch + ')';
  888. }
  889. if (/^(?:#?N\/A|No\s+Label|Not\s+On\s+Label|\[no\s+label\])$/i.test(track.label)) track.label = undefined;
  890. if (/^(?:#?N\/A|none)$/i.test(track.catalog)) track.catalog = undefined;
  891. if (/^(\d+)\s*[\/]\s*(\d+)$/.test(track.tracknumber)) { // track/totaltracks
  892. addMessage('nonstandard track number formatting for track ' + RegExp.$1 + ': ' + track.tracknumber, 'warning');
  893. track.tracknumber = RegExp.$1;
  894. if (!track.totaltracks) track.totaltracks = parseInt(RegExp.$2);
  895. } else if (/^(\d+)[\.\-](\d+)$/.test(track.tracknumber)) { // discnumber.tracknumber
  896. addMessage('nonstandard track number formatting for track ' + RegExp.$2 + ': ' + track.tracknumber, 'warning');
  897. if (!track.discnumber) track.discnumber = parseInt(RegExp.$1);
  898. track.tracknumber = RegExp.$2;
  899. }
  900. if (track.discnumber) {
  901. if (/^(\d+)\s*\/\s*(\d+)/.test(track.discnumber)) {
  902. addMessage('nonstandard disc number formatting for track ' + track.tracknumber + ': ' + track.discnumber, 'warning');
  903. track.discnumber = RegExp.$1;
  904. if (!track.totaldiscs) track.totaldiscs = RegExp.$2;
  905. } else track.discnumber = parseInt(track.discnumber);
  906. if (isNaN(track.discnumber)) {
  907. addMessage('invalid disc numbering for track ' + track.tracknumber, 'warning');
  908. track.discnumber = undefined;
  909. }
  910. if (track.discnumber > release.totaldiscs) release.totaldiscs = track.discnumber;
  911. }
  912. totalTime += track.duration;
  913. albumBitrate += track.bitrate * track.duration;
  914. albumSize += track.filesize;
  915. if (!onlineSource && track.bitrate > 0) {
  916. let triggers = [24, 12];
  917. switch (track.codec) {
  918. case 'FLAC':
  919. if (track.sr > 0 && track.bd > 0) triggers = [
  920. Math.round(Math.max(track.sr * track.bd / 1800, 192)),
  921. Math.round(Math.max(track.sr * track.bd / 2400, 192)),
  922. ];
  923. break;
  924. case 'MP3':
  925. switch (track.codec_profile) {
  926. case 'VBR V0': triggers = [192, 96]; break;
  927. case 'VBR V1': triggers = [160, 80]; break;
  928. case 'VBR V2': triggers = [128, 64]; break;
  929. }
  930. break;
  931. case 'AAC':
  932. if (/\b(?:TVBR)\sq(\d+)\b/.test(track.vendor)) triggers = [
  933. Math.round(Math.max(parseInt(RegExp.$1) * 1.9, 192)),
  934. Math.round(Math.max(parseInt(RegExp.$1) * 1.4, 192)),
  935. ]; else if (/\b(?:(?:CV|A|C)BR)\s(\d+)kbps\b/.test(track.vendor)) triggers = [
  936. Math.round(Math.max(parseInt(RegExp.$1) * 0.75, 192)),
  937. Math.round(Math.max(parseInt(RegExp.$1) * 0.4, 192)),
  938. ];
  939. break;
  940. }
  941. if (track.bitrate < triggers[0]) addMessage('track #' + track.tracknumber +
  942. ' suspiciously low bitrate (' + track.bitrate + ' kbps)', track.bitrate < triggers[1] ? 'warning' : 'notice');
  943. }
  944. });
  945. if (!tracks.every(track => track.discnumber > 0) && !tracks.every(track => !track.discnumber)) {
  946. addMessage('inconsistent release (mix of tracks with and without disc number)', 'warning');
  947. }
  948. if (!onlineSource && release.totaldiscs > 1 && tracks.some(it => it.totaldiscs != release.totaldiscs))
  949. addMessage('at least one track not having properly set TOTALDISCS (' + release.totaldiscs + ')', 'info');
  950.  
  951. function setUniqueProperty(propName, propNameLiteral) {
  952. let homogeneous = new Set(tracks.map(it => it[propName]).filter(it => it != undefined && it != null));
  953. if (homogeneous.size > 1) {
  954. var diverses = '', it = homogeneous.values(), val;
  955. while (!(val = it.next()).done) diverses += '<br>\t' + val.value;
  956. clipBoard.value = '';
  957. throw new HTML('mixed releases not accepted (' + propNameLiteral + ') - supposedly user compilation' + diverses);
  958. }
  959. release[propName] = homogeneous.values().next().value;
  960. }
  961. setUniqueProperty('artist', 'album artist');
  962. ['artists', 'featuring_artists', 'composers', 'conductors', 'performers', 'compilers', 'remixers', 'producers'].forEach(function(role) {
  963. if (tracks.every(track => Array.isArray(track[role]) && track[role].equalTo(tracks[0][role]))) release[role] = tracks[0][role];
  964. });
  965. setUniqueProperty('album', 'album title');
  966. setUniqueProperty('album_year', 'album year');
  967. setUniqueProperty('release_date', 'release date');
  968. setUniqueProperty('encoding', 'encoding');
  969. setUniqueProperty('codec', 'codec');
  970. setUniqueProperty('codec_profile', 'codec profile');
  971. setUniqueProperty('vendor', 'vendor');
  972. setUniqueProperty('media', 'media');
  973. setUniqueProperty('channels', 'channels');
  974. setUniqueProperty('label', 'label');
  975. setUniqueProperty('country', 'country');
  976.  
  977. tracks.forEach(function(iter) {
  978. setProperty('trackArtists', 'track_artist');
  979. setProperty('totalTracks', 'totaltracks');
  980. setProperty('discSubtitles', 'discsubtitle');
  981. setProperty('composers', 'composer');
  982. setProperty('catalogs', 'catalog');
  983. setProperty('bitrates', 'bitrate');
  984. setProperty('bds', 'bd');
  985. setProperty('ags', 'ag');
  986. setProperty('drs', 'dr');
  987. if (iter.sr) if (typeof release.srs[iter.sr] != 'number') release.srs[iter.sr] = iter.duration;
  988. else release.srs[iter.sr] += iter.duration;
  989. setProperty('dirpaths', 'dirpath');
  990. setProperty('descriptions', 'description');
  991. setProperty('genres', 'genre');
  992. setProperty('urls', 'url');
  993. setProperty('coverUrls', 'cover_url');
  994.  
  995. function setProperty(propName, trackProp) {
  996. if (!Array.isArray(release[propName])) release[propName] = [];
  997. if (iter[trackProp] !== undefined && iter[trackProp] !== null && (typeof iter[trackProp] != 'string'
  998. || iter[trackProp].length > 0) && !release[propName].includes(iter[trackProp])) {
  999. release[propName].push(iter[trackProp]);
  1000. }
  1001. }
  1002. });
  1003. if (!release.totalTracks) addMessage('total tracks not set', 'warning');
  1004. if (release.totalTracks.length > 0) {
  1005. if (release.totalTracks.length > 1) {
  1006. addMessage('total tracks not consistent across release: ' + release.totalTracks, 'warning');
  1007. } else if (release.totalTracks[0] != tracks.length) {
  1008. addMessage('total tracks not matching tracklist length: ' +
  1009. release.totalTracks[0] + ' != ' + tracks.length, 'warning');
  1010. }
  1011. }
  1012. tracks.forEach(function(track1, ndx1) {
  1013. if (tracks.some((track2, ndx2) => ndx2 < ndx1 && track1.tracknumber == track2.tracknumber
  1014. && track1.discnumber == track2.discnumber && track1.discsubtitle == track2.discsubtitle)) {
  1015. addMessage('duplicate track ' + (track1.discnumber ? track1.discnumber + '-' : '') +
  1016. (track1.discsubtitle ? track1.discsubtitle + '-' : '') + track1.tracknumber, 'warning');
  1017. }
  1018. });
  1019. if (!onlineSource) {
  1020. function validatorFunc(arr, validator, str) {
  1021. if (arr.length <= 0 || !arr.some(validator)) return true;
  1022. clipBoard.value = '';
  1023. throw 'disallowed ' + str + ' present (' + arr.filter(validator) + ')';
  1024. }
  1025. validatorFunc(release.bds, bd => ![16, 24].includes(bd), 'bit depths');
  1026. validatorFunc(Object.keys(release.srs),
  1027. sr => sr < 44100 || sr > 192000 || sr % 44100 != 0 && sr % 48000 != 0, 'sample rates');
  1028. if (prefs.honour_rg && tracks.some(track => track.ag === undefined))
  1029. addMessage('at least one track is missing RG info', 'notice');
  1030. if (release.ags.length > 1) addMessage('album RG differs across the release', 'notice');
  1031. if (prefs.honour_dr && tracks.some(track => track.bd > 16 && track.dr === undefined))
  1032. addMessage('at least one high resolution track is missing DR info', 'notice');
  1033. release.descriptions.forEach(function(description) {
  1034. if (/^[\w\-]+\@[\w\-]+(?:\.[\w\-]+)+$|\b(?:RuTracker|FLACMANIA\.RU|24bit-music\.info|GetMetal\.CLUB|LOSSLESSBEST|flacmania\.ru)\b|~ N ~/i.test(description))
  1035. addMessage(new HTML('Advertising detected in description: ' + RegExp.lastMatch.bold()), 'warning');
  1036. });
  1037. release.urls.forEach(function(url) {
  1038. if (/^https?:\/\/(\w+\.)*7digital\.com\/.*\?f=/i.test(url))
  1039. addMessage('session id present in online source URL: ' + url, 'notice');
  1040. });
  1041. }
  1042. if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))) {
  1043. (release.coverUrls.length > 0 ? setCover(release.coverUrls[0]) : Promise.reject('No cover URL'))
  1044. .catch(getCoverOnline).catch(searchCoverOnline);
  1045. }
  1046. var albumBPM = Math.round(tracks.reduce(function(acc, track) {
  1047. return acc + parseInt(track.identifiers.BPM) * track.duration;
  1048. }, 0) / totalTime);
  1049. var composerEmphasis = tracks.some(track => track.identifiers.COMPOSEREMPHASIS);
  1050. var isFromDSD = false, isClassical = false;
  1051. var canSort = tracks.every((tr1, ndx1) => tracks.every((tr2, ndx2) => ndx1 == ndx2
  1052. || tr1.tracknumber != tr2.tracknumber || tr1.discnumber != tr2.discnumber));
  1053. var yadg_prefil = '', releaseType, editionTitle, iter, rx;
  1054. var barCode = getHomoIdentifier('BARCODE');
  1055. if (barCode) barCode = parseInt(barCode.replace(/\s+/g, ''));
  1056. if (!Number.isInteger(barCode)) {
  1057. if (release.catalogs.length == 1) barCode = parseInt(release.catalogs[0].replace(/[\s\-]/g, ''));
  1058. if (!Number.isInteger(barCode) || barCode < 10**10) barCode = undefined;
  1059. }
  1060. var tags = new TagManager();
  1061. albumBitrate /= totalTime;
  1062. const isCompilation = tracks.every(track => track.identifiers.COMPILATION == 1
  1063. || /^(?:Compilation)$/i.test(track.identifiers.RELEASETYPE));
  1064. if (tracks.every(track => /^(?:Single)$/i.test(track.identifiers.RELEASETYPE))
  1065. || tracks.length == 1 && totalTime > 0 && totalTime < prefs.single_threshold) {
  1066. releaseType = getReleaseIndex('Single');
  1067. } else if (tracks.every(it => it.identifiers.RELEASETYPE == 'EP')) {
  1068. releaseType = getReleaseIndex('EP');
  1069. } else if (tracks.every(it => /^soundtrack$/i.test(it.identifiers.RELEASETYPE))) {
  1070. releaseType = getReleaseIndex('Soundtrack');
  1071. tags.add('score');
  1072. composerEmphasis = true;
  1073. }
  1074. if (release.genres.length > 0) {
  1075. const classicalGenreParsers = [
  1076. /\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,
  1077. ];
  1078. release.genres.forEach(function(genre) {
  1079. classicalGenreParsers.forEach(function(classicalGenreParser) {
  1080. if (classicalGenreParser.test(genre) && !/\b(?:metal|rock|pop)\b/i.test(genre)) {
  1081. composerEmphasis = true;
  1082. isClassical = true
  1083. }
  1084. });
  1085. if (/\b(?:Jazz|Vocal)\b/i.test(genre) && !/\b(?:Nu|Future|Acid)[\s\-\−\—\–]*Jazz\b/i.test(genre)
  1086. && !/\bElectr(?:o|ic)[\s\-\−\—\–]?Swing\b/i.test(genre)) {
  1087. composerEmphasis = true;
  1088. }
  1089. if (/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i.test(genre)) {
  1090. if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
  1091. composerEmphasis = true;
  1092. }
  1093. if (/\b(?:Christmas\s+Music)\b/i.test(genre)) {
  1094. composerEmphasis = true;
  1095. }
  1096. tags.add(...genre.split(/\s*\|\s*/));
  1097. });
  1098. if (release.genres.length > 1) addMessage('inconsistent genre accross album: ' + release.genres.join(' / '), 'warning');
  1099. }
  1100. if (!onlineSource && isClassical && !tracks.every(track => track.composer)) {
  1101. addMessage(new HTML('all tracks composers must be set for clasical music' + ruleLink('2.3.17')), 'warning');
  1102. //return false;
  1103. }
  1104. // Processing artists: recognition, splitting and dividing to categores
  1105. const roleCollisions = [
  1106. [4, 5], // main
  1107. [0, 4], // guest
  1108. [], // remixer
  1109. [], // composer
  1110. [], // conductor
  1111. [], // DJ/compiler
  1112. [], // producer
  1113. ];
  1114. isVA = vaParser.test(release.artist);
  1115. var artists = [], albumGuests = [];
  1116. for (i = 0; i < 7; ++i) artists[i] = [];
  1117.  
  1118. if (!isVA) {
  1119. if (Array.isArray(release.artists) && release.artists.length > 0) {
  1120. artists[0] = release.artists.filter(exclusions);
  1121. if (Array.isArray(release.featuring_artists)) {
  1122. albumGuests = release.featuring_artists;
  1123. artists[1] = release.featuring_artists.filter(exclusions);
  1124. }
  1125. yadg_prefil = joinArtists(artists[0]);
  1126. } else {
  1127. yadg_prefil = spliceGuests(release.artist);
  1128. addArtists(0, yadg_prefil);
  1129. artists[0] = artists[0].filter(exclusions);
  1130. albumGuests = Array.from(artists[1]);
  1131. }
  1132. if (ampersandParsers.some(rx => rx.test(yadg_prefil))) getSiteArtist(yadg_prefil); // priority cache record
  1133.  
  1134. function exclusions(artist) {
  1135. return !['conductors', 'compilers']
  1136. .some(category => Array.isArray(release[category]) && release[category].includesCaseless(artist));
  1137. }
  1138. }
  1139.  
  1140. featParsers.slice(3).forEach(function(rx, ndx) {
  1141. if ((matches = rx.exec(release.album)) == null) return;
  1142. if (ndx >= 5 && !splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)))
  1143. .every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
  1144. addArtists(1, matches[1]);
  1145. artists[0].forEach(guest => { if (albumGuests.includesCaseless(guest)) albumGuests.push(guest) });
  1146. addMessage('featured artist(s) in album title (' + release.album + ')', 'warning');
  1147. release.album = release.album.replace(rx, '');
  1148. });
  1149. remixParsers.slice(4).forEach(function(rx) {
  1150. if (rx.test(release.album)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1151. })
  1152. if (((matches = /^(.*?)\s+Presents\s+(.*)$/.exec(release.album)) != null
  1153. || isVA && (matches = (/\s+\(compiled\s+by\s+(.*?)\)\s*$/i.exec(release.album)
  1154. || /\s+compiled\s+by\s+(.*?)\s*$/i.exec(release.album))) != null) && looksLikeTrueName(matches[1])) {
  1155. addArtists(5, matches[1]);
  1156. if (!releaseType) releaseType = getReleaseIndex('Compilation');
  1157. }
  1158.  
  1159. for (iter of tracks) {
  1160. ['track_artist', 'performer', 'track_guest'].forEach(function(role) {
  1161. var arrayName = role.concat('s');
  1162. addTrackPerformers(Array.isArray(iter[arrayName]) && iter[arrayName].length > 0 ? iter[arrayName] : iter[role]);
  1163. });
  1164. [
  1165. [2, 'remixer'],
  1166. [3, 'composer'],
  1167. [4, 'conductor'],
  1168. [5, 'compiler'],
  1169. [6, 'producer'],
  1170. ].forEach(function(role) {
  1171. var arrayName = role[1].concat('s');
  1172. addArtists(role[0], Array.isArray(iter[arrayName]) && iter[arrayName].length > 0 ? iter[arrayName] : iter[role[1]]);
  1173. });
  1174.  
  1175. if (iter.title) {
  1176. featParsers.slice(3).forEach(function(rx, ndx) {
  1177. if ((matches = rx.exec(iter.title)) == null) return;
  1178. var featArtists = splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)));
  1179. if (ndx >= 5 && !featArtists.every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
  1180. if (Array.isArray(iter.track_artists) && iter.track_artists.length > 0) {
  1181. if (!Array.isArray(iter.track_guests)) iter.track_guests = [];
  1182. featArtists.forEach(function(featArtist) {
  1183. if (!iter.track_artists.includesCaseless(featArtist) && !iter.track_guests.includesCaseless(featArtist))
  1184. track_guests.push(featArtist);
  1185. });
  1186. if (!isVA && iter.track_artists.equalCaselessTo(release.artists)
  1187. && iter.track_guests.equalCaselessTo(release.featuring_artists)) {
  1188. iter.track_artists = iter.track_guests = iter.track_artist = undefined;
  1189. } else iter.track_artist = joinArtists(iter.track_artists).concat(' feat. ', joinArtists(iter.track_guests));
  1190. } else {
  1191. useTA = iter.track_artist && !featArtists.some(featArtist => iter.track_artist.includes(featArtist)
  1192. || Array.isArray(iter.track_artists) && iter.track_artists.includes(featArtist)
  1193. || Array.isArray(iter.track_guests) && iter.track_guests.includes(featArtist));
  1194. iter.track_artist = iter[useTA ? 'track_artist' : 'artist'].concat(' feat. ', matches[1]);
  1195. }
  1196. addArtists(1, matches[1]);
  1197. addMessage('featured artist(s) in track title (#' + iter.tracknumber + ': ' + iter.title + ')', 'warning');
  1198. iter.title = iter.title.replace(rx, '');
  1199. });
  1200. if (!iter.remixer) remixParsers.slice(4).forEach(function(rx) {
  1201. if (rx.test(iter.title)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1202. });
  1203. }
  1204. if (isClassical && !iter.composer && /^([^\(\)\[\]\{\},:]+?)(?:\s*\((?:\d{4}\s*-|b\.)\s*\d{4}\))/.test(iter.discsubtitle)) {
  1205. //track.composer = RegExp.$1;
  1206. addArtists(3, RegExp.$1);
  1207. }
  1208. }
  1209. for (i = 0; i < Math.round(tracks.length / 2); ++i) splitAmpersands();
  1210. albumGuests = splitAmpersands(albumGuests);
  1211.  
  1212. function addArtists(ndx, _artists) {
  1213. (typeof _artists == 'string' ? splitArtists(_artists) : Array.isArray(_artists) ? _artists : []).forEach(function(artist) {
  1214. artist = ndx != 0 ? strip(artist) : guessOtherArtists(artist);
  1215. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1216. && !artists[ndx].includesCaseless(artist)
  1217. && !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist))) artists[ndx].push(artist);
  1218. });
  1219. }
  1220. function addTrackPerformers(_artists) {
  1221. (typeof _artists == 'string' ? splitArtists(spliceGuests(_artists, 1)) : Array.isArray(_artists) ? _artists : []).forEach(function(artist) {
  1222. artist = guessOtherArtists(artist);
  1223. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1224. && !artists[0].includesCaseless(artist)
  1225. && (isVA || !artists[1].includesCaseless(artist))) artists[isVA ? 0 : 1].push(artist);
  1226. });
  1227. }
  1228. function splitAmpersands(_artists) {
  1229. if (_artists) {
  1230. let result;
  1231. if (typeof _artists == 'string') result = splitArtists(_artists);
  1232. else if (Array.isArray(_artists)) result = Array.from(_artists);
  1233. else return [];
  1234. splitInternal(result);
  1235. return result;
  1236. }
  1237. for (let ndx = 0; ndx < artists.length; ++ndx) splitInternal(artists[ndx], roleCollisions[ndx]);
  1238.  
  1239. function splitInternal(refArr, roleCollisions) {
  1240. ampersandParsers.forEach(function(ampersandParser) {
  1241. for (var i = refArr.length; i > 0; --i) {
  1242. var j = refArr[i - 1].split(ampersandParser).map(strip);
  1243. if (j.length <= 1 || !j.some(it1 => artists.some(it2 => it2.includesCaseless(it1)))
  1244. && !j.every(looksLikeTrueName) || getSiteArtist(refArr[i - 1])) continue;
  1245. refArr.splice(i - 1, 1, ...j.filter(function(artist) {
  1246. return !refArr.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist))
  1247. && (!Array.isArray(roleCollisions) || !roleCollisions.some(n => artists[n].includesCaseless(artist)));
  1248. }));
  1249. }
  1250. });
  1251. }
  1252. }
  1253. function spliceGuests(str, level = 1) {
  1254. (level > 0 ? featParsers.slice(level) : featParsers).forEach(function(rx, ndx) {
  1255. var matches = rx.exec(str);
  1256. if (matches != null && (level + ndx < 8
  1257. || splitArtists(matches[1]).every((artist, ndx) => looksLikeTrueName(artist, 1)))) {
  1258. addArtists(1, matches[1]);
  1259. str = str.replace(rx, '');
  1260. }
  1261. });
  1262. return str;
  1263. }
  1264. function guessOtherArtists(name) {
  1265. otherArtistsParsers.forEach(function(it) {
  1266. if (!it[0].test(name)) return;
  1267. addArtists(it[1], RegExp.$2);
  1268. name = RegExp.$1;
  1269. });
  1270. return strip(name);
  1271. }
  1272. function getArtists(trackArtist) {
  1273. if (!trackArtist || typeof trackArtist != 'string') trackArtist = '';
  1274. otherArtistsParsers.forEach(it => { if (it[0].test(trackArtist)) trackArtist = RegExp.$1 });
  1275. var result = [ [], [] ];
  1276. featParsers.slice(1).forEach(function(rx, ndx) {
  1277. if ((matches = rx.exec(trackArtist)) == null || ndx >= 7 && !looksLikeTrueName(matches[1], 1)) return;
  1278. splitAmpersands(matches[1]).forEach(artist => { result[1].pushUniqueCaseless(artist) });
  1279. trackArtist = trackArtist.replace(rx, '');
  1280. });
  1281. splitAmpersands(trackArtist).forEach(artist => { result[0].pushUniqueCaseless(artist) });
  1282. return result;
  1283. }
  1284. function realTrackArtist(trackArtist) {
  1285. var result, trackArtists = getArtists(trackArtist);
  1286. if (trackArtists[0].length > 0 && (!trackArtists[0].equalCaselessTo(artists[0]) || !trackArtists[1].equalCaselessTo(albumGuests)))
  1287. if (prefs.reformat_trackartist) {
  1288. result = joinArtists(trackArtists[0]);
  1289. if (trackArtists[1].length > 0) result += ' feat. '.concat(joinArtists(trackArtists[1]));
  1290. } else result = trackArtist;
  1291. return result;
  1292. }
  1293.  
  1294. if (elementWritable(document.getElementById('artist') || document.getElementById('artist_0'))) {
  1295. let artistIndex = 0;
  1296. const enSorter = /^(?:The)\s+/;
  1297. catLoop: for (i = 0; i < artists.length; ++i) for (iter of artists[i]
  1298. .filter(artist => !roleCollisions[i].some(n => artists[n].includesCaseless(artist)))
  1299. .sort((a, b) => a.replace(enSorter, '').localeCompare(b.replace(enSorter, '')))) {
  1300. if (isUpload) {
  1301. var id = 'artist';
  1302. if (artistIndex > 0) id += '_' + artistIndex;
  1303. while ((ref = document.getElementById(id)) == null) AddArtistField();
  1304. } else {
  1305. while ((ref = document.querySelectorAll('input[name="artists[]"]')).length <= artistIndex) AddArtistField();
  1306. ref = ref[artistIndex];
  1307. }
  1308. if (ref == null) throw new Error('Failed to allocate artist fields');
  1309. ref.value = iter;
  1310. ref.nextElementSibling.value = i + 1;
  1311. if (++artistIndex >= 200) break catLoop;
  1312. }
  1313. if (overwrite && artistIndex > 0) while (document.getElementById('artist_' + artistIndex) != null) {
  1314. RemoveArtistField();
  1315. }
  1316. }
  1317.  
  1318. // Processing album title
  1319. const mediaParsers = [
  1320. [/\s+(?:\[(?:LP|Vinyl|12"|7")\]|\((?:LP|Vinyl|12"|7")\))$/, 'Vinyl'],
  1321. [/\s+(?:\[SA-?CD\]|\(SA-?CD\))$/, 'SACD'],
  1322. [/\s+(?:\[(?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\]|\((?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\))$/, isOPS ? 'BD' : 'Blu-Ray'],
  1323. [/\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/, 'DVD'],
  1324. ];
  1325. const releaseTypeParsers = [
  1326. [/\s+(?:-\s+Single|\[Single\]|\(Single\))$/i, 'Single', true, true],
  1327. [/\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/, 'EP', true, true],
  1328. [/\s+\((?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\(\)]*\)$/i, 'Live album', false, false],
  1329. [/\s+\[(?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\[\]]*\]$/i, 'Live album', false, false],
  1330. [/(?:^Live\s+(?:[aA]t|[Ii]n)\b|^Directo?\s+[Ee]n\b|\bUnplugged\b|\bAcoustic\s+Stage\b|\s+Live$)/, 'Live album', false, false],
  1331. [/\b(?:(?:Best\s+of|Greatest\s+Hits|Complete\s+(.+?\s+)(?:Albums|Recordings))\b|Collection$)|^The(\s+\w+)+Years$/i, 'Anthology', false, false],
  1332. ];
  1333. const editionParsers = [
  1334. /\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,
  1335. /\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,
  1336. /\s+-\s+([^\[\]\(\)\-\−\—\–]*\b(?:(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Bonus\s+Track)\b[^\[\]\(\)\-\−\—\–]*|Reissue|Edition|Version|Promo|Enhanced|Release|Édition))$/i,
  1337. ];
  1338. var album = release.album;
  1339. releaseTypeParsers.forEach(function(it) {
  1340. if (it[0].test(album)) {
  1341. if (it[2] || !releaseType) releaseType = getReleaseIndex(it[1]);
  1342. if (it[3]) album = album.replace(it[0], '');
  1343. }
  1344. });
  1345. rx = '\\b(?:Soundtrack|Score|Motion\\s+Picture|Series|Television|Original(?:\\s+\\w+)?\\s+Cast|Music\\s+from|(?:Musique|Bande)\\s+originale)\\b';
  1346. if (reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
  1347. if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
  1348. tags.add('score');
  1349. composerEmphasis = true;
  1350. }
  1351. remixParsers.forEach(function(rx) {
  1352. if (rx.test(album) && !releaseType) releaseType = getReleaseIndex('Remix');
  1353. });
  1354. editionParsers.forEach(function(rx) {
  1355. if (rx.test(album) && (!RegExp.$1.toLowerCase().startsWith('remaster') || !release.album_year
  1356. || release.album_year != extractYear(release.release_date)) && !isRequestNew && !isRequestEdit) {
  1357. album = album.replace(rx, '');
  1358. editionTitle = RegExp.$1;
  1359. }
  1360. });
  1361. mediaParsers.forEach(function(it) {
  1362. if (it[0].test(album)) {
  1363. album = album.replace(it[0], '');
  1364. media = it[1];
  1365. }
  1366. });
  1367. if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  1368. ref.value = album;
  1369. }
  1370.  
  1371. if (yadg_prefil) yadg_prefil += ' ';
  1372. yadg_prefil += album;
  1373. if (elementWritable(ref = document.getElementById('yadg_input'))) {
  1374. ref.value = yadg_prefil || '';
  1375. if (yadg_prefil && (ref = document.getElementById('yadg_submit')) != null && !ref.disabled) ref.click();
  1376. }
  1377.  
  1378. if (!release.album_year) release.album_year = parseInt(getHomoIdentifier('PUBYEAR')) || undefined;
  1379. if (elementWritable(ref = document.getElementById('year'))) {
  1380. ref.value = release.album_year || '';
  1381. }
  1382. i = release.release_date && extractYear(release.release_date);
  1383. if (elementWritable(ref = document.getElementById('remaster_year'))
  1384. || !isUpload && i > 0 && (ref = document.querySelector('input[name="year"]')) != null && !ref.disabled) {
  1385. ref.value = i || '';
  1386. }
  1387. //if (tracks.every(it => it.identifiers.EXPLICIT == 0)) editionTitle = 'Clean' + (editionTitle ? ' / ' + editionTitle : '');
  1388. [/\s+\(([^\(\)]+)\)\s*$/, /\s+\[([^\[\]]+)\]\s*$/, /\s+\{([^\{\}]+)\}\s*$/].forEach(function(rx) {
  1389. var version = tracks.map(track => rx.test(track.title) ? RegExp.$1 : null);
  1390. version = version.homogeneous() && version[0] || undefined;
  1391. if (!editionTitle && /\b(?:Remastered|Remasterisée|Remasterizado|Acoustic|Instrumental)\b/i.test(version)
  1392. && releaseType != getReleaseIndex('Single')) editionTitle = version;
  1393. if (!releaseType && /\b(?:Live)\b/i.test(version)) releaseType = getReleaseIndex('Live album');
  1394. });
  1395. if (elementWritable(ref = document.getElementById('remaster_title'))) ref.value = editionTitle || '';
  1396. if (elementWritable(ref = document.getElementById('remaster_record_label')
  1397. || document.querySelector('input[name="recordlabel"]'))) {
  1398. ref.value = release.label ? prefs.selfrelease_label && !isVA && release.label == release.artist
  1399. || /^(?:independent|vlastní\s+náklad|Self[\s\-]Released)$/i.test(release.label)
  1400. || /^iMD-/.test(release.label) ? prefs.selfrelease_label : release.label.split(/\s*;\s*/g).join(' / ') : '';
  1401. }
  1402. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  1403. || document.querySelector('input[name="cataloguenumber"]'))) {
  1404. ref.value = release.catalogs.length >= 1
  1405. && release.catalogs.map(it => it.replace(/\s*;\s*/g, ' / ')).join(' / ') || barCode || '';
  1406. }
  1407. var scene = getHomoIdentifier('SCENE');
  1408. if (isUpload && scene != undefined && (ref = document.getElementById('scene')) != null && !ref.disabled) try {
  1409. ref.checked = eval(scene.toLowerCase());
  1410. } catch(e) { console.warn('Invalid SCENE value (' + scene + ')') }
  1411. var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
  1412. if (elementWritable(ref = document.getElementById('format')) && allowedFormats.includes(release.codec)) {
  1413. ref.value = release.codec || (isRED ? '' : '---');
  1414. ref.onchange(); //exec(function() { Format() });
  1415. }
  1416. if (isRequestNew) {
  1417. if (prefs.always_request_perfect_flac) reqSelectFormats('FLAC');
  1418. else if (release.codec) reqSelectFormats(release.codec);
  1419. }
  1420. var sel;
  1421. if (release.encoding == 'lossless') {
  1422. sel = tracks.some(track => track.bd == 24) ? '24bit Lossless' : 'Lossless';
  1423. } else if (release.bitrates.length >= 1) {
  1424. let lame_version = release.codec == 'MP3' && /^LAME(\d+)\.(\d+)/i.test(release.vendor) ?
  1425. parseInt(RegExp.$1) * 1000 + parseInt(RegExp.$2) : undefined;
  1426. if (release.codec == 'MP3' && release.codec_profile == 'VBR V0') {
  1427. sel = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
  1428. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V1') {
  1429. sel = 'V1 (VBR)'
  1430. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V2') {
  1431. sel = lame_version >= 3094 ? sel = 'V2 (VBR)' : 'APS (VBR)'
  1432. } else if (release.bitrates.length == 1 && [192, 256, 320].includes(Math.round(release.bitrates[0]))) {
  1433. sel = Math.round(release.bitrates[0]);
  1434. } else {
  1435. sel = 'Other';
  1436. }
  1437. }
  1438. if ((ref = document.getElementById('bitrate')) != null && !ref.disabled && (overwrite || !br_isSet)) {
  1439. ref.value = sel || '';
  1440. ref.onchange(); //exec(function() { Bitrate() });
  1441. if (sel == 'Other' && (ref = document.getElementById('other_bitrate')) != null) {
  1442. ref.value = Math.round(release.bitrates.length == 1 ? release.bitrates[0] : albumBitrate);
  1443. if ((ref = document.getElementById('vbr')) != null) ref.checked = release.bitrates.length > 1;
  1444. }
  1445. }
  1446. if (isRequestNew) {
  1447. if (prefs.always_request_perfect_flac) {
  1448. reqSelectBitrates('Lossless', '24bit Lossless');
  1449. } else if (sel) reqSelectBitrates(sel);
  1450. }
  1451. if (release.media) {
  1452. sel = undefined;
  1453. [
  1454. [/\b(?:WEB|File|Download|digital\s+media)\b|^Digital$/i, 'WEB'],
  1455. [/\bCD\b/, 'CD'],
  1456. [/\b(?:SA-?CD|[Hh]ybrid)\b/, 'SACD'],
  1457. [/\b(?:[Bb]lu[\-\−\—\–\s]?[Rr]ay|BRD?|BD)\b/, isOPS ? 'BD' : 'Blu-Ray'],
  1458. [/\bDVD(?:-?A)?\b/, 'DVD'],
  1459. [/\b(?:[Vv]inyl\b|LP\b|12"|7")/, 'Vinyl'],
  1460. ].forEach(k => { if (k[0].test(release.media)) sel = k[1] });
  1461. media = sel || media;
  1462. }
  1463. if (!media) {
  1464. if (tracks.every(isRedBook)) {
  1465. addMessage('media not determined - CD estimated', 'info');
  1466. media = 'CD';
  1467. } else if (tracks.some(t => t.bd > 16 || (t.sr > 0 && t.sr != 44100) || t.samples > 0 && t.samples % 588 != 0)) {
  1468. addMessage('media not determined - NOT CD', 'info');
  1469. }
  1470. } else if (media != 'CD' && tracks.every(isRedBook)) {
  1471. addMessage('CD as source media is estimated (' + media + ')', 'info');
  1472. }
  1473. if (elementWritable(ref = document.getElementById('media'))) {
  1474. ref.value = media || !tracks.some(notRedBook) && prefs.default_medium || (isRED ? '' : '---');
  1475. }
  1476. if (isRequestNew) {
  1477. if (prefs.always_request_perfect_flac) reqSelectMedias('WEB', 'CD', isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD')
  1478. else if (media) reqSelectMedias(media);
  1479. }
  1480. function isRedBook(track) {
  1481. return track.bd == 16 && track.sr == 44100 && track.channels == 2 && track.samples > 0 && track.samples % 588 == 0;
  1482. }
  1483. function notRedBook(track) {
  1484. return track.bd && track.bd != 16 || track.sr && track.sr != 44100
  1485. || track.channels && track.channels != 2 || track.samples && track.samples % 588 != 0;
  1486. }
  1487. if (tracks.every(it => it.identifiers.ORIGINALFORMAT && it.identifiers.ORIGINALFORMAT.includes('DSD'))) {
  1488. isFromDSD = true;
  1489. }
  1490. // Release type
  1491. if (!releaseType/* || isCompilation)*/) {
  1492. if (/\b(?:Mixtape)\b/i.test(release.album)) releaseType = getReleaseIndex('Mixtape');
  1493. else if (isVA) releaseType = getReleaseIndex('Compilation');
  1494. else if (isCompilation) releaseType = getReleaseIndex('Anthology');
  1495. }
  1496. if ((!releaseType || releaseType == 5) && totalTime <= prefs.EP_threshold && tracks.every(function(track) {
  1497. const rxs = [/\s+\([^\(\)]+\)\s*$/, /\s+\[[^\[\]]+\]\s*$/];
  1498. return rxs.reduce((acc, rx) => acc.replace(rx, ''), track.title)
  1499. == rxs.reduce((acc, rx) => acc.replace(rx, ''), tracks[0].title);
  1500. })) {
  1501. releaseType = getReleaseIndex('Single');
  1502. }
  1503. if (!releaseType) if (totalTime > 0 && totalTime < prefs.single_threshold) {
  1504. releaseType = getReleaseIndex('Single');
  1505. } else if (totalTime > 0 && totalTime < prefs.EP_threshold) {
  1506. releaseType = getReleaseIndex('EP');
  1507. }
  1508. if ((ref = document.getElementById('releasetype')) != null && !ref.disabled
  1509. && (overwrite || ref.value == 0 || ref.value == '---')) ref.value = releaseType || getReleaseIndex('Album');
  1510. // Tags
  1511. if (prefs.estimate_decade_tag && (isNaN(totalTime) || totalTime < 2 * 60 * 60)
  1512. && release.album_year > 1900 && [1, 3, 5, 9, 13, undefined].includes(releaseType)
  1513. /*&& !/\b(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissue|Anniversary|Collector(?:'?s)?)\b/i.test(editionTitle)*/)
  1514. tags.add(Math.floor(release.album_year/10) * 10 + 's'); // experimental
  1515. if (release.country) {
  1516. if (!excludedCountries.some(it => it.test(release.country))) tags.add(release.country);
  1517. }
  1518. if (!composerEmphasis && tracks.every(track => track.identifiers.HASLYRICS == 0)) tags.add('instrumental');
  1519. if (elementWritable(ref = document.getElementById('tags'))) {
  1520. ref.value = tags.toString();
  1521. if (artists[0].length == 1 && prefs.fetch_tags_from_artist > 0) setTimeout(function() {
  1522. var artist = getSiteArtist(artists[0][0]);
  1523. if (!artist) return;
  1524. tags.add(...artist.tags.sort((a, b) => b.count - a.count).map(it => it.name)
  1525. .slice(0, prefs.fetch_tags_from_artist));
  1526. var ref = document.getElementById('tags');
  1527. ref.value = tags.toString();
  1528. }, 3000);
  1529. }
  1530. if (!composerEmphasis/* && release.genres.length > 0*/ && !prefs.keep_meaningles_composers) {
  1531. document.querySelectorAll('input[name="artists[]"]').forEach(function(i) {
  1532. if (['4', '5'].includes(i.nextElementSibling.value)) i.value = '';
  1533. });
  1534. }
  1535.  
  1536. const doubleParsParsers = [
  1537. /\(+(\([^\(\)]*\))\)+/,
  1538. /\[+(\[[^\[\]]*\])\]+/,
  1539. /\{+(\{[^\{\}]*\})\}+/,
  1540. ];
  1541. tracks.forEach(function(track) {
  1542. doubleParsParsers.forEach(function(rx) {
  1543. if (!rx.test(track.title)) return;
  1544. addMessage('doubled parentheses in track #' + track.tracknumber + ' title ("' + track.title + '")', 'warning');
  1545. //track.title.replace(rx, RegExp.$1);
  1546. });
  1547. });
  1548. if (tracks.length > 1 && tracks.map(track => track.title).homogeneous()) {
  1549. addMessage('all tracks having same title: ' + tracks[0].title, 'warning');
  1550. }
  1551. if (isUpload && !isOPS) findPreviousUploads();
  1552. // Album description
  1553. sourceUrl = getStoreUrl();
  1554. if ((ref = document.querySelector('tr#autofill_tr > td > select')) != null) {
  1555. if (i = getHomoIdentifier('DISCOGS_ID')) {
  1556. ref.value = 'discogs';
  1557. ref.onchange();
  1558. if (elementWritable(ref = document.getElementById('discogs'))) ref.value = i;
  1559. } else if (i = getHomoIdentifier('MBID')) {
  1560. ref.value = 'musicbrainz';
  1561. ref.onchange();
  1562. if (elementWritable(ref = document.getElementById('musicbrainz'))) ref.value = i;
  1563. }
  1564. }
  1565. const vinylTest = /^((?:Vinyl|LP) rip by\s+)(.*)$/im;
  1566. const vinyltrackParser = /^([A-Z])[\-\.\s]?((\d+)(?:\.\d+)?)$/;
  1567. const classicalWorkParsers = [
  1568. /^(.*\S):\s+(.*)$/,
  1569. /^(.+?):\s+([IVXC]+\.\s+.*)$/,
  1570. ];
  1571. var description;
  1572. if (isRequestNew || isRequestEdit) { // request
  1573. description = [];
  1574. if (release.release_date && !/^\s*\d{4}\s*$/.test(release.release_date) && !isNaN(i = new Date(release.release_date))) {
  1575. let today = new Date().getDateValue();
  1576. description.push((i.getDateValue() < today ? 'Released' : 'Releasing') + ' ' + i.toDateString());
  1577. if (prefs.upcoming_tags && i.getDateValue() >= today && (ref = document.getElementById('tags')) != null && !ref.disabled) {
  1578. let tags = new TagManager(ref.value);
  1579. tags.add(prefs.upcoming_tags);
  1580. ref.value = tags.toString();
  1581. }
  1582. }
  1583. if (!prefs.include_tracklist_in_request) {
  1584. let summary = '';
  1585. if (release.totaldiscs > 1) summary += release.totaldiscs + ' discs, ';
  1586. summary += tracks.length + ' track'; if (tracks.length > 1) summary += 's';
  1587. if (totalTime > 0) summary += ', ' + makeTimeString(totalTime);
  1588. description.push(summary);
  1589. }
  1590. if (sourceUrl || release.urls.length > 0) description.push(getUrls());
  1591. if (release.catalogs.length == 1 && /^\d{10,}$/.test(release.catalogs[0]) || /^\d{10,}$/.test(barCode)) {
  1592. description.push('[url=https://www.google.com/search?q=' + RegExp.lastMatch + ']Find more stores...[/url]');
  1593. }
  1594. if (prefs.include_tracklist_in_request) description.push(genPlaylist());
  1595. if (release.descriptions.length > 0) Array.prototype.push.apply(description, release.descriptions);
  1596. description = genAlbumHeader().concat(description.join('\n\n'));
  1597. if (description.length > 0) {
  1598. ref = document.getElementById('description') || document.querySelector('textarea[name="description"]');
  1599. if (elementWritable(ref)) {
  1600. ref.value = description;
  1601. } else if (isRequestEdit && ref != null && !ref.disabled) {
  1602. ref.value = ref.value.length > 0 ? ref.value.concat('\n\n', description) : ref.value = description;
  1603. preview(0);
  1604. }
  1605. }
  1606. } else { // upload
  1607. description = '';
  1608. if (prefs.bpm_summary && albumBPM > 0) description += '\n\nAverage album BPM: [code]' + albumBPM + '[/code]';
  1609. /*if (release.release_date) {
  1610. let rd = new Date(release.release_date);
  1611. if (!isNaN(rd)) description = '\n\nRelease date: ' + rd.toDateString();
  1612. }*/
  1613. let vinylRipInfo;
  1614. if (release.descriptions.length > 0) {
  1615. description += '\n\n';
  1616. if (isRED && prefs.tracklist_style == 3) description += '[pad=0|20]';
  1617. if (release.descriptions.length == 1 && release.descriptions[0]
  1618. && (matches = vinylTest.exec(release.descriptions[0])) != null) {
  1619. vinylRipInfo = release.descriptions[0].slice(matches.index).trim().split(/(?:[ \t]*\r?\n)+/);
  1620. description += release.descriptions[0].slice(0, matches.index).trim();
  1621. } else description += release.descriptions.join('\n\n');
  1622. if (isRED && prefs.tracklist_style == 3) description += '[/pad]';
  1623. }
  1624. let oa = fetchOnlineAdditions().then(t => { description += '\n\n'.concat(t) }, reason => undefined);
  1625. if (elementWritable(ref = document.getElementById('album_desc'))) {
  1626. ref.value = genPlaylist();
  1627. finalizeDesc(ref);
  1628. }
  1629. if ((ref = document.getElementById('body') || document.querySelector('textarea[name="body"]')) != null && !ref.disabled) {
  1630. if (ref.value.length == 0) ref.value = genPlaylist(); else {
  1631. let editioninfo = '';
  1632. if (editionTitle) {
  1633. editioninfo = '[size=5][b]' + editionTitle;
  1634. if (release.release_date && (i = extractYear(release.release_date)) > 0) editioninfo += ' (' + i + ')';
  1635. editioninfo += '[/b][/size]\n\n';
  1636. }
  1637. ref.value = ref.value.concat('\n\n', editioninfo, genPlaylist(false, false));
  1638. }
  1639. finalizeDesc(ref);
  1640. }
  1641. function finalizeDesc(elem) {
  1642. oa.then(function() {
  1643. if (description) elem.value += description;
  1644. preview(0);
  1645. });
  1646. }
  1647. // Release description
  1648. if (elementWritable(ref = document.getElementById('release_samplerate'))) {
  1649. ref.value = Object.keys(release.srs).length == 1 && Object.keys(release.srs)[0] ?
  1650. Math.floor(Object.keys(release.srs)[0] / 1000) :
  1651. Object.keys(release.srs).length > 1 || isNaN(Object.keys(release.srs)[0]) ? '999' : '';
  1652. }
  1653. let lineage = '', rlsDesc = '';
  1654. let drInfo = '[hide=DR' + (release.drs.length == 1 ? release.drs[0] : '') + '][pre][/pre]';
  1655. let hasSR = Object.keys(release.srs).length > 0;
  1656. let srInfo = hasSR ? Object.keys(release.srs).filter(sr => sr > 0).sort((a, b) => release.srs[b] - release.srs[a])
  1657. .map(f => f / 1000).join('/').concat('kHz') : '';
  1658. if (tracks.some(track => track.bd > 16)) {
  1659. if ([isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD'].includes(media)) {
  1660. if (!isNWCD) rlsDesc = srInfo;
  1661. addChannelInfo();
  1662. if (media == 'SACD' || isFromDSD) addDSDInfo();
  1663. if (prefs.cleanup_descriptions) addDRInfo();
  1664. //addRGInfo();
  1665. addHybridInfo();
  1666. drInfo += '[/hide]';
  1667. } else if (media == 'Vinyl') {
  1668. let hassr = hasSR && (!isNWCD || Object.keys(release.srs).length > 1);
  1669. if (hassr) lineage = srInfo + ' ';
  1670. if (vinylRipInfo) {
  1671. if (vinylTest.test(vinylRipInfo[0]) && RegExp.$2.toLowerCase() != 'unknown')
  1672. vinylRipInfo[0] = vinylRipInfo[0].replace(vinylTest, '$1[color=blue]$2[/color]');
  1673. if (hassr) vinylRipInfo[0] = vinylRipInfo[0].replace(/^Vinyl\b/, 'vinyl');
  1674. lineage += vinylRipInfo[0] + '\n\n[u]Lineage:[/u]' + vinylRipInfo.slice(1).map(l => '\n'.concat([
  1675. // RuTracker translation
  1676. ['Код класса состояния винила', 'Vinyl condition class'],
  1677. ['Устройство воспроизведения', 'Turntable'],
  1678. ['Головка звукоснимателя', 'Cartridge'],
  1679. ['Картридж', 'Cartridge'],
  1680. ['Предварительный усилитель', 'Preamplifier'],
  1681. ['АЦП', 'ADC'],
  1682. ['Программа-оцифровщик', 'Software'],
  1683. ['Обработка звука', 'Audio post-processing'],
  1684. ['Обработка', 'Post-processing'],
  1685. ].reduce((acc, it) => acc.replace(it[0], it[1]), l))).join('');
  1686. } else lineage += (hassr ? 'Vinyl' : ' vinyl') + ' rip by [color=blue][/color]\n\n[u]Lineage:[/u]\n';
  1687. let imgs = '\n[img][/img]'.repeat(8);
  1688. if (!isNWCD) drInfo += '\n'.concat(imgs); else lineage += '\n\n[hide]'.concat(imgs.slice(1), '[/hide]');
  1689. drInfo += '[/hide]';
  1690. } else { // WEB Hi-Res
  1691. if (!isNWCD || Object.keys(release.srs).length > 1) rlsDesc = srInfo;
  1692. if (release.channels && release.channels != 2) addChannelInfo();
  1693. if (isFromDSD) addDSDInfo();
  1694. if (!isFromDSD || prefs.cleanup_descriptions) addDRInfo();
  1695. //addRGInfo();
  1696. addHybridInfo();
  1697. if (isFromDSD || prefs.cleanup_descriptions || Object.keys(release.srs).length == 1
  1698. && Object.keys(release.srs)[0] == 88200) drInfo += '[/hide]'; else drInfo = null;
  1699. }
  1700. } else { // 16bit or lossy
  1701. if (Object.keys(release.srs).some(f => f != 44100)) rlsDesc = srInfo;
  1702. if (release.channels && release.channels != 2) addChannelInfo();
  1703. addDRInfo();
  1704. //addRGInfo();
  1705. if (prefs.cleanup_descriptions) drInfo += '[/hide]'; else drInfo = null;
  1706. if (release.codec == 'MP3' && release.vendor) {
  1707. // TODO: parse mp3 vendor string
  1708. } else if (['AAC', 'Opus', 'Vorbis'].includes(release.codec) && release.vendor) {
  1709. let _encoder_settings = release.vendor;
  1710. if (release.codec == 'AAC' && /^qaac\s+[\d\.]+/i.test(release.vendor)) {
  1711. let enc = [];
  1712. if (matches = release.vendor.match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
  1713. if (matches = release.vendor.match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
  1714. if (matches = release.vendor.match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
  1715. if (matches = release.vendor.match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
  1716. if (matches = release.vendor.match(/\bQuality\s+(\d+)\b/i)) enc[5] = matches[1];
  1717. _encoder_settings = 'Converted by Apple\'s ' + enc[2] + ' encoder (' + enc[3] + '-' + enc[4] + ')';
  1718. }
  1719. lineage = _encoder_settings;
  1720. }
  1721. }
  1722. function addDSDInfo() {
  1723. var nfo = ' DSD64';
  1724. if (prefs.sacd_decoder) nfo += ' using ' + prefs.sacd_decoder;
  1725. nfo += '\nOutput gain: [code]+0dB[/code]';
  1726. if (isNWCD) lineage = 'From' .concat(nfo); else {
  1727. if (rlsDesc.length > 0) rlsDesc += ' from'; else rlsDesc = 'From';
  1728. rlsDesc += nfo;
  1729. }
  1730. }
  1731. function addDRInfo() {
  1732. if (release.drs.length < 1 || document.getElementById('release_dynamicrange') != null) return;
  1733. var nfo = 'DR' + release.drs[0];
  1734. if (release.drs[0] < 4) nfo = '[color=red]'.concat(nfo, '[/color]');
  1735. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1736. rlsDesc += nfo;
  1737. }
  1738. function addRGInfo() {
  1739. if (release.ags.length <= 0) return;
  1740. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1741. rlsDesc += 'RG'; //rlsDesc += 'RG ' + ags[0];
  1742. }
  1743. function addChannelInfo() {
  1744. if (!release.channels) return;
  1745. var chi = getChanString(release.channels);
  1746. if (chi.length <= 0) return;
  1747. if (rlsDesc.length > 0) rlsDesc += ', '; else rlsDesc = 'Channels configuration: ';
  1748. rlsDesc += chi;
  1749. }
  1750. function addHybridInfo() {
  1751. if (release.bds.length > 1) release.bds.filter(bd => bd != 24).forEach(function(bd) {
  1752. var hybrid_tracks = tracks.filter(it => it.bd == bd).sort(trackComparer).map(function(it) {
  1753. return (release.totaldiscs > 1 && it.discnumber ? it.discnumber + '-' : '').concat(it.tracknumber);
  1754. });
  1755. if (hybrid_tracks.length < 1) return;
  1756. if (rlsDesc.length > 0) rlsDesc += '\n';
  1757. rlsDesc += 'Note: track';
  1758. if (hybrid_tracks.length > 1) rlsDesc += 's';
  1759. rlsDesc += ' #' + hybrid_tracks.join(', ') +
  1760. (hybrid_tracks.length > 1 ? ' are' : ' is') + ' ' + bd + 'bit lossless';
  1761. });
  1762. }
  1763. function addRlsDate() {
  1764. if (prefs.insert_release_date && release.release_date && !/^\s*\d{4}\s*$/.test(release.release_date)
  1765. && !isNaN(i = new Date(release.release_date))) rlsDesc.push('Released ' + i.toDateString());
  1766. }
  1767. rlsDesc = rlsDesc.length > 0 ? [rlsDesc] : [];
  1768. if ((ref = document.getElementById('release_lineage')) != null) {
  1769. lineage = lineage ? [lineage] : [];
  1770. if (drInfo) rlsDesc.push(drInfo);
  1771. addRlsDate();
  1772. if (sourceUrl || release.urls.length > 0) lineage.push(getUrls());
  1773. if (elementWritable(ref)) {
  1774. ref.value = lineage.join('\n\n');
  1775. preview(1);
  1776. }
  1777. } else {
  1778. if (lineage.length > 0) rlsDesc.push(lineage);
  1779. if (drInfo) rlsDesc.push(drInfo);
  1780. addRlsDate();
  1781. if (sourceUrl || release.urls.length > 0) rlsDesc.push(getUrls());
  1782. }
  1783. if (elementWritable(ref = document.getElementById('release_desc'))) {
  1784. ref.value = rlsDesc.join('\n\n');
  1785. if (rlsDesc.length > 0) preview(isNWCD ? 2 : 1);
  1786. }
  1787. if (release.encoding == 'lossless' && release.codec == 'FLAC'
  1788. && tracks.some(track => track.bd == 24) && release.dirpaths.length == 1) {
  1789. if ((ref = document.getElementById('release_desc')) != null) GM_xmlhttpRequest({
  1790. method: 'GET',
  1791. url: new URL('file:'.concat(release.dirpaths[0], '\\foo_dr.txt')).href,
  1792. responseType: 'blob',
  1793. onload: function(response) {
  1794. if (response.status < 200 || response.status >= 400) return defaultErrorHandler(response);
  1795. if (!/(\[hide=DR\d*\]\[pre\])\[\/pre\]/im.test(ref.value)) return;
  1796. var ndx = RegExp.lastIndex + RegExp.$1.length;
  1797. ref.value = ref.value.slice(0, ndx).concat(response.responseText, ref.value.slice(ndx));
  1798. },
  1799. onerror: error => { console.error('foo_dr.txt not exists or is forbidden to read') },
  1800. ontimeout: defaultTimeoutHandler,
  1801. });
  1802. }
  1803. }
  1804. if (ajaxRejects > 0) {
  1805. i = 'AJAX request(s) eliminated due to Gazelle policy. ' +
  1806. 'Multiple artists not split correctly? Relaunch parsing in overwrite mode without page reload';
  1807. try {
  1808. let delay = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]).timeStamp + gazelleApiFrame - Date.now();
  1809. if (delay >= 0) {
  1810. i += ' after ' + Math.ceil(delay / 1000) + 's';
  1811. setTimeout(() => { addMessage('new AJAX timeframe for requery available', 'info') }, delay);
  1812. }
  1813. addMessage(i + '.', 'notice');
  1814. } catch(e) { console.error(e) }
  1815. }
  1816. if (elementWritable(ref = document.getElementById('release_dynamicrange'))) {
  1817. ref.value = release.drs.length == 1 ? release.drs[0] : '';
  1818. }
  1819. if (isRequestNew && prefs.request_default_bounty > 0) {
  1820. let amount = prefs.request_default_bounty < 1024 ? prefs.request_default_bounty : prefs.request_default_bounty / 1024;
  1821. if ((ref = document.getElementById('amount_box')) != null && !ref.disabled) ref.value = amount;
  1822. if ((ref = document.getElementById('unit')) != null && !ref.disabled) {
  1823. ref.value = prefs.request_default_bounty < 1024 ? 'mb' : 'gb';
  1824. }
  1825. try { Calculate() } catch(e) { /* Orpheus bug void handler */ }
  1826. }
  1827. if (!media && (ref = document.getElementById('media')) != null && ref.value && ref.value != '---') media = ref.value;
  1828. if (!onlineSource) {
  1829. if (prefs.honour_url && !sourceUrl && release.urls.length <= 0) addMessage('No lineage URL', 'notice');
  1830. onlineSource = (function() {
  1831. if (sourceUrl || release.urls.length > 0) return urlResolver(sourceUrl || release.urls[0])
  1832. .then(sourceUrl => fetchOnline_Music(sourceUrl, true).then(completeFromOnlineSource));
  1833. return Promise.reject('No lineage URL');
  1834. })();
  1835. if (prefs.check_integrity_online) onlineSource.catch(reason => lookupOnlineSource().then(function(result) {
  1836. if (typeof result == 'object') return parseLastFm(result);
  1837. if (urlParser.test(result)) return fetchOnline_Music(result, true);
  1838. return Promise.reject('Unhandled format');
  1839. })).then(onlineCheck).catch(function(reason) {
  1840. if (!media || media == 'WEB') tracks.forEach(function(track) {
  1841. if (!track.duration || track.duration < 29.6 || track.duration > 30.4) return;
  1842. addMessage('track ' + track.tracknumber + ' possible track preview', 'warning');
  1843. });
  1844. });
  1845. }
  1846. if (prefs.clean_on_apply) clipBoard.value = '';
  1847. prefs.save();
  1848. return true;
  1849.  
  1850. // ---------------------------------------------------------------------------------------------------------------
  1851.  
  1852. function genPlaylist(pad = true, header = true) {
  1853. var style = prefs.tracklist_style;
  1854. if (style == 2 && (tracks.map(track => track.title).some(notMonospaced)
  1855. || tracks.map(track => track.track_artist).some(notMonospaced)
  1856. || composerEmphasis && tracks.map(track => track.composer).some(notMonospaced))) style = 3;
  1857. if (!style || style <= 0) return null;
  1858. var playlist = '';
  1859. if (tracks.length > 1 || prefs.singles_conventional_format || isRequestNew || isRequestEdit) {
  1860. if (style == 3) playlist = '[align=center]';
  1861. if (pad && isRED) playlist += '[pad=8|0|0|0]';
  1862. if (header) playlist += genAlbumHeader();
  1863. playlist += '[size=4][b][color=' + prefs.tracklist_head_color + ']Tracklisting[/color][/b][/size]';
  1864. if (pad && isRED) playlist += '[/pad]';
  1865. playlist += '\n'; //'[hr]';
  1866. let lastDisc, lastSubtitle, lastWork, lastSide, vinylTrackWidth;
  1867. let block = 0, classicalWorks = new Map();
  1868. if (composerEmphasis /*isClassical*/ && !tracks.some(it => it.discsubtitle)) {
  1869. tracks.forEach(function(track) {
  1870. if (!track.composer) return;
  1871. (/*isClassical ? classicalWorkParsers : */classicalWorkParsers.slice(1)).forEach(function(classicalWorkParser) {
  1872. if (track.classical_work || !classicalWorkParser.test(track.title)) return;
  1873. classicalWorks.set(track.classical_work = RegExp.$1, {});
  1874. track.classical_title = RegExp.$2;
  1875. });
  1876. });
  1877. for (iter of classicalWorks.keys()) {
  1878. let work = tracks.filter(track => track.classical_work == iter);
  1879. if (work.length > 1 || tracks.every(track => track.classical_work)) {
  1880. if (work[0].track_artist && work[0].track_artist != release.artist && work.map(track => track.track_artist).homogeneous())
  1881. classicalWorks.get(iter).performer = realTrackArtist(work[0].track_artist);
  1882. if (work[0].composer && release.composers.length > 1 && work.map(track => track.composer).homogeneous())
  1883. classicalWorks.get(iter).composer = work[0].composer;
  1884. } else {
  1885. work.forEach(function(track) {
  1886. delete track.classical_work;
  1887. delete track.classical_title;
  1888. });
  1889. classicalWorks.delete(iter);
  1890. }
  1891. }
  1892. }
  1893. let track, duration, volumes = new Map(tracks.map(it => [it.discnumber, undefined])), tnOffset = 0;
  1894. volumes.forEach(function(val, key) {
  1895. volumes.set(key, new Set(tracks.filter(it => it.discnumber == key).map(it => it.discsubtitle)).size)
  1896. });
  1897. if (!tracks.every(it => !isNaN(parseInt(it.tracknumber.toString())))
  1898. && !tracks.every(it => vinyltrackParser.test(it.tracknumber.toString().toUpperCase()))) {
  1899. addMessage('inconsistent tracks numbering (' + tracks.map(it => it.tracknumber) + ')', 'warning');
  1900. }
  1901. vinylTrackWidth = tracks.reduce(function(acc, it) {
  1902. return Math.max(vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) && parseInt(RegExp.$3), acc);
  1903. }, 0);
  1904. if (vinylTrackWidth) {
  1905. vinylTrackWidth = vinylTrackWidth.toString().length;
  1906. tracks.forEach(function(it) {
  1907. if (vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) != null)
  1908. it.tracknumber = RegExp.$1 + RegExp.$3.padStart(vinylTrackWidth, '0');
  1909. });
  1910. ++vinylTrackWidth;
  1911. }
  1912. if (release.totaldiscs < 2 && tracks.reduce(computeLowestTrack, undefined) - 1)
  1913. addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
  1914. const padStart = '[pad=0|0|5|0]';
  1915. if (canSort && prefs.sort_tracklist) tracks.sort(trackComparer);
  1916. for (iter of tracks) {
  1917. let title = '', trackArtist = undefined;
  1918. if (iter.track_artist && iter.track_artist != release.artist) trackArtist = realTrackArtist(iter.track_artist);
  1919. var ttwidth = vinylTrackWidth || (release.totaldiscs > 1 && iter.discnumber ?
  1920. tracks.filter(it => it.discnumber == iter.discnumber) : tracks).reduce(function (accumulator, it) {
  1921. return Math.max(accumulator, (parseInt(it.tracknumber) || it.tracknumber).toString().length);
  1922. }, 2);
  1923. function realTrackNumber() {
  1924. var tn = parseInt(iter.tracknumber);
  1925. return isNaN(tn) ? iter.tracknumber : (tn - tnOffset).toString().padStart(ttwidth, '0');
  1926. }
  1927. switch (style) {
  1928. case 1:
  1929. case 3: {
  1930. prologue('[size=' + prefs.tracklist_size + ']', '[/size]\n');
  1931. track = '[b][color=' + prefs.tracklist_tracknumber_color + ']';
  1932. track += realTrackNumber();
  1933. track += '[/color][/b]' + prefs.title_separator;
  1934. if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
  1935. title = '[color=' + prefs.tracklist_artist_color + ']' + trackArtist + '[/color] - ';
  1936. }
  1937. title += iter.classical_title || iter.title;
  1938. if (iter.composer && composerEmphasis && release.composers.length != 1
  1939. && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
  1940. title = title.concat(' [color=', prefs.tracklist_composer_color, '](', iter.composer, ')[/color]');
  1941. }
  1942. playlist += track + title;
  1943. if (iter.duration) playlist += ' [i][color=' + prefs.tracklist_duration_color +'][' +
  1944. makeTimeString(iter.duration) + '][/color][/i]';
  1945. if (iter.lyrics) playlist += ' [size=1][hide=lyrics]'.concat(iter.lyrics, '[/hide][/size]');
  1946. break;
  1947. }
  1948. case 2: {
  1949. prologue('[size=' + prefs.tracklist_size + '][pre]', '[/pre][/size]');
  1950. track = realTrackNumber();
  1951. track += prefs.title_separator;
  1952. if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
  1953. title = trackArtist + ' - ';
  1954. }
  1955. title += iter.classical_title || iter.title;
  1956. if (composerEmphasis && iter.composer && release.composers.length != 1
  1957. && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
  1958. title = title.concat(' (', iter.composer, ')');
  1959. }
  1960. let l = 0, j, left, padding, spc;
  1961. duration = iter.duration ? '[' + makeTimeString(iter.duration) + ']' : null;
  1962. let width = prefs.max_tracklist_width - track.length;
  1963. if (duration) width -= duration.length + 1;
  1964. while (title.trueLength() > 0) {
  1965. j = width;
  1966. if (title.trueLength() > width) {
  1967. while (j > 0 && title[j] != ' ') { --j }
  1968. if (j <= 0) j = width;
  1969. }
  1970. left = title.slice(0, j).trim();
  1971. if (++l <= 1) {
  1972. playlist += track + left;
  1973. if (duration) {
  1974. spc = width - left.trueLength();
  1975. padding = (spc < 2 ? ' '.repeat(spc) : ' ' + prefs.pad_leader.repeat(spc - 1)) + ' ';
  1976. playlist += padding + duration;
  1977. }
  1978. width = prefs.max_tracklist_width - track.length - 2;
  1979. } else playlist += '\n' + ' '.repeat(track.length) + left;
  1980. title = title.slice(j).trim();
  1981. }
  1982. break;
  1983. }
  1984. }
  1985. }
  1986. switch (style) {
  1987. case 1:
  1988. case 3:
  1989. if (totalTime > 0) playlist += '\n\n' + divs[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
  1990. ']Total time: [i]' + makeTimeString(totalTime) + '[/i][/color][/size]';
  1991. break;
  1992. case 2:
  1993. if (totalTime > 0) {
  1994. duration = '[' + makeTimeString(totalTime) + ']';
  1995. playlist += '\n\n' + divs[0].repeat(32).padStart(prefs.max_tracklist_width);
  1996. playlist += '\n' + 'Total time:'.padEnd(prefs.max_tracklist_width - duration.length) + duration;
  1997. }
  1998. playlist += '[/pre][/size]';
  1999. break;
  2000. }
  2001. if (style == 3) playlist += '[/align]';
  2002.  
  2003. function computeLowestTrack(acc, track) {
  2004. if (Number.isNaN(acc)) return NaN;
  2005. var tn = parseInt(track.tracknumber);
  2006. if (isNaN(tn)) return NaN;
  2007. return isNaN(acc) || tn < acc ? tn : acc;
  2008. }
  2009.  
  2010. function prologue(prefix, postfix) {
  2011. function block1() {
  2012. if (block == 3) playlist += postfix;
  2013. playlist += '\n';
  2014. if (isRED && ![1, 2].includes(block)) playlist += padStart;
  2015. block = 1;
  2016. }
  2017. function block2() {
  2018. if (block == 3) playlist += postfix;
  2019. playlist += '\n';
  2020. if (isRED && ![1, 2].includes(block)) playlist += padStart;
  2021. block = 2;
  2022. }
  2023. function block3() {
  2024. //if (block == 2 && isRED) playlist += '[hr]';
  2025. if (isRED && [1, 2].includes(block)) playlist += '[/pad]';
  2026. playlist += '\n';
  2027. if (block != 3) playlist += prefix;
  2028. block = 3;
  2029. }
  2030. if (release.totaldiscs > 1 && iter.discnumber != lastDisc) {
  2031. block1();
  2032. lastDisc = iter.discnumber;
  2033. lastSubtitle = lastWork = undefined;
  2034. playlist += '[color=' + prefs.tracklist_disctitle_color + '][size=3][b]';
  2035. if (iter.identifiers.VOL_MEDIA && tracks.filter(it => it.discnumber == iter.discnumber)
  2036. .every(it => it.identifiers.VOL_MEDIA == iter.identifiers.VOL_MEDIA)) {
  2037. playlist += iter.identifiers.VOL_MEDIA.toUpperCase() + ' ';
  2038. }
  2039. playlist += 'Disc ' + iter.discnumber;
  2040. if (iter.discsubtitle && (volumes.get(iter.discnumber) || 0) == 1) {
  2041. playlist += ' – ' + iter.discsubtitle;
  2042. lastSubtitle = iter.discsubtitle;
  2043. }
  2044. playlist += '[/b][/size]';
  2045. duration = tracks.filter(it => it.discnumber == iter.discnumber).reduce((acc, it) => acc + it.duration, 0);
  2046. if (duration > 0) playlist += ' [size=2][i][' + makeTimeString(duration) + '][/i][/size]';
  2047. playlist += '[/color]';
  2048. tnOffset = tracks.filter(track => track.discnumber == iter.discnumber).reduce(computeLowestTrack, undefined) - 1 || 0;
  2049. if (tnOffset) addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
  2050. }
  2051. if (iter.discsubtitle != lastSubtitle) {
  2052. if (block != 1 || iter.discsubtitle) block1();
  2053. if (iter.discsubtitle) {
  2054. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]' + iter.discsubtitle + '[/b][/size]';
  2055. duration = tracks.filter(it => it.discsubtitle == iter.discsubtitle)
  2056. .reduce((acc, it) => acc + it.duration, 0);
  2057. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  2058. playlist += '[/color]';
  2059. }
  2060. lastSubtitle = iter.discsubtitle;
  2061. }
  2062. if (iter.classical_work != lastWork) {
  2063. if (iter.classical_work) {
  2064. block2();
  2065. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
  2066. if (release.composers.length != 1 && classicalWorks.get(iter.classical_work).composer) {
  2067. playlist += classicalWorks.get(iter.classical_work).composer + ': ';
  2068. }
  2069. playlist += iter.classical_work;
  2070. playlist += '[/b]';
  2071. if (classicalWorks.get(iter.classical_work).performer
  2072. && classicalWorks.get(iter.classical_work).performer != release.artist) {
  2073. playlist += ' (' + classicalWorks.get(iter.classical_work).performer + ')';
  2074. }
  2075. playlist += '[/size]';
  2076. duration = tracks.filter(it => it.classical_work == iter.classical_work)
  2077. .reduce((acc, it) => acc + it.duration, 0);
  2078. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  2079. playlist += '[/color]';
  2080. } else {
  2081. if (block > 2) block1();
  2082. }
  2083. lastWork = iter.classical_work;
  2084. }
  2085. if (vinyltrackParser.test(iter.tracknumber)) {
  2086. if (block == 3 && lastSide && RegExp.$1 != lastSide) playlist += '\n';
  2087. lastSide = RegExp.$1;
  2088. }
  2089. block3();
  2090. } // prologue
  2091. } else { // single
  2092. playlist += '[align=center]';
  2093. playlist += isRED ? '[pad=20|20|20|20]' : '';
  2094. playlist += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + release.artist + '[/color]';
  2095. playlist += isRED ? '[hr]' : '\n'.concat(divs[0].repeat(24), '\n');
  2096. playlist += tracks[0].title + '[/b]';
  2097. if (tracks[0].composer) {
  2098. playlist += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
  2099. }
  2100. if (tracks[0].duration) playlist += '\n\n[color=' + prefs.tracklist_duration_color +
  2101. '][' + makeTimeString(tracks[0].duration) + '][/color][/size]';
  2102. if (isRED) playlist += '[/pad]';
  2103. playlist += '[/align]';
  2104. }
  2105. return prefs.colorless_tracklist ? playlist.replace(/\[color=\S+?\]/ig, '').replace(/\[\/color\]/ig, '') : playlist;
  2106. }
  2107.  
  2108. function getUrls() {
  2109. var result = [];
  2110. if (sourceUrl) result.push(sourceUrl);
  2111. Array.prototype.push.apply(result, release.urls.filter(function(url) {
  2112. return !sourceUrl || url.toLowerCase() != sourceUrl.toLowerCase();
  2113. }));
  2114. return result.map(function(url, ndx) {
  2115. if (!urlParser.test(url)) return url;
  2116. url = new URL(url);
  2117. var logoUrl = prefs.use_store_logos ? [
  2118. [/\b(?:qobuz\.com)$/i, 'https://ptpimg.me/1saep4.png'],
  2119. [/\b(?:bandcamp\.com)$/i, 'https://ptpimg.me/vwki92.jpg' /*'https://ptpimg.me/7evz4g.png'*/],
  2120. //[/\b(?:highresaudio\.com)$/i, 'https://ptpimg.me/65xx03.png'],
  2121. [/\b(?:7digital\.com)$/i, 'https://ptpimg.me/300scj.png'],
  2122. [/\b(?:mora\.jp)$/i, 'https://ptpimg.me/ma53oh.png'],
  2123. [/\b(?:deezer\.com)$/i, 'https://ptpimg.me/181799.png'],
  2124. [/\b(?:spotify\.com)$/i, 'https://ptpimg.me/xo5d1p.png'],
  2125. [/\b(?:tidal\.com)$/i, 'https://ptpimg.me/w80424.png'],
  2126. [/\b(?:music\.apple\.com)$/i, 'https://ptpimg.me/in7u5u.png'],
  2127. [/\b(?:prestomusic\.com)$/i, 'https://ptpimg.me/q86vjt.png'],
  2128. [/\b(?:prostudiomasters\.com)$/i, 'https://ptpimg.me/xkm0th.png'],
  2129. [/\b(?:supraphonline\.cz)$/i, 'https://ptpimg.me/h85655.png'],
  2130. [/^(?:(?:www\.)?hdtracks\.)/i, 'https://ptpimg.me/eurm85.png'/*'https://ptpimg.me/wx36i4.png'*/],
  2131. [/\b(?:nativedsd\.com)$/i, 'https://ptpimg.me/m6j8gp.png'],
  2132. [/\b(?:indies\.eu)$/i, 'https://ptpimg.me/8a4w49.png'],
  2133. [/\b(?:e-onkyo\.com)$/i, 'https://ptpimg.me/uke3n1.png'],
  2134. [/\b(?:beatport\.com)$/i, 'https://ptpimg.me/lf8q75.png'],
  2135. [/\b(?:junodownload\.com)$/i, 'https://ptpimg.me/6c7y42.png'],
  2136. ].reduce((acc, site) => acc || (site[0].test(url.hostname) ? site[1] : null), null) : null;
  2137. return logoUrl ? '[url='.concat(url, '][img]', logoUrl, '[/img][/url]') : '[url]'.concat(url, '[/url]');
  2138. }).join('\n');
  2139. }
  2140.  
  2141. function genAlbumHeader() {
  2142. return !isVA && artists[0].length >= 3 ? '[size=4]' +
  2143. joinArtists(artists[0], artist => '[artist]' + artist + '[/artist]') + ' – ' + release.album + '[/size]\n\n' : '';
  2144. }
  2145.  
  2146. function findPreviousUploads() {
  2147. let search = new URLSearchParams(document.location.search);
  2148. if (search.get('groupid')) localFetch('/torrents.php?action=grouplog&groupid=' + search.get('groupid')).then(function(dom) {
  2149. dom.querySelectorAll('table > tbody > tr.rowa').forEach(function(tr) {
  2150. if (/^\s*deleted\b/i.test(tr.children[3].textContent))
  2151. scanLog('Torrent ' + tr.children[1].firstChild.textContent);
  2152. });
  2153. }); else {
  2154. let query = release.album;
  2155. if (!isVA && artists[0].length > 0 && artists[0].length < 3) query = artists[0].join(', ').concat(' - ', query);
  2156. scanLog(query);
  2157. }
  2158.  
  2159. function scanLog(query) {
  2160. localFetch('/log.php?search=' + encodeURIComponent(query)).then(function(dom) {
  2161. dom.querySelectorAll('table > tbody > tr.rowb').forEach(function(tr) {
  2162. var size, msg = tr.children[1].textContent.trim();
  2163. if (/\b[\d\s]+(?:\.\d+)?\s*(?:([KMGT])I?)?B\b/.test(msg)) size = get_size_from_string(RegExp.lastMatch);
  2164. if (!msg.includes('deleted') || (/\[(.*)\/(.*)\/(.*)\]/.test(msg) ?
  2165. !release.codec || release.codec != RegExp.$1
  2166. //|| !release.encoding || release.encoding != RegExp.$2
  2167. || !media || media != RegExp.$3 :
  2168. !size || !albumSize || Math.abs(albumSize / size - 1) >= 0.1)) return;
  2169. addMessage('possibly same release previously uploaded and deleted: ' + msg, 'warning');
  2170. });
  2171. });
  2172. }
  2173.  
  2174. function get_size_from_string(str) {
  2175. var matches = /\b([\d\s]+(?:\.\d+)?)\s*(?:([KMGT])I?)?B\b/.exec(str.replace(',', '.').toUpperCase());
  2176. if (!matches) return null;
  2177. var size = parseFloat(matches[1].replace(/\s+/g, ''));
  2178. if (matches[2] == 'K') { size *= Math.pow(1024, 1) }
  2179. else if (matches[2] == 'M') { size *= Math.pow(1024, 2) }
  2180. else if (matches[2] == 'G') { size *= Math.pow(1024, 3) }
  2181. else if (matches[2] == 'T') { size *= Math.pow(1024, 4) }
  2182. return Math.round(size);
  2183. }
  2184. }
  2185.  
  2186. function getHomoIdentifier(id) {
  2187. id = id.toUpperCase();
  2188. return tracks.every((elem, ndx, arr) => elem.identifiers[id] != undefined
  2189. && elem.identifiers[id] === arr[0].identifiers[id]) ? tracks[0].identifiers[id] : undefined;
  2190. }
  2191.  
  2192. function getStoreUrl() {
  2193. for (var it of [
  2194. ['ACOUSTICSOUNDS_ID', 'https://store.acousticsounds.com/d/{ID}/'],
  2195. ['ALLMUSIC_ID', 'https://www.allmusic.com/album/{ID}'],
  2196. ['AMAZON_ID', 'https://www.amazon.com/gp/product/{ID}'],
  2197. ['AMID', 'https://www.allmusic.com/album/{ID}'],
  2198. ['APPLE_ID', 'https://music.apple.com/album/{ID}'],
  2199. ['ASIN', 'https://www.amazon.com/gp/product/{ID}'],
  2200. ['BEATPORT_ID', 'https://www.beatport.com/release/2/{ID}'],
  2201. ['DEEZER_ID', deezerAlbumPrefix + '{ID}'],
  2202. ['DISCOGS_ID', discogsOrigin + '/release/{ID}'],
  2203. ['EONKYO_ID', 'https://www.e-onkyo.com/music/album/{ID}/'],
  2204. ['GOOGLE_ID', 'https://play.google.com/store/music/album/?id={ID}'],
  2205. ['HDTRACKS_ID', 'https://www.hdtracks.com/#/album/{ID}'],
  2206. ['INDIESSCOPE_ID', 'https://www.indies.eu/alba/{ID}/'],
  2207. ['ITUNES_ID', 'https://music.apple.com/album/{ID}'],
  2208. ['JUNODOWNLOAD_ID', 'https://www.junodownload.com/products/{ID}'],
  2209. ['MBID', mbrRlsPrefix + '{ID}'],
  2210. ['PROSTUDIOMASTERS_ID', 'https://www.prostudiomasters.com/album/page/{ID}'],
  2211. ['SPOTIFY_ID', 'https://open.spotify.com/album/{ID}'],
  2212. ['TRAXSOURCE_ID', 'https://www.traxsource.com/title/{ID}/'],
  2213. ['VGMDB_ID', 'https://vgmdb.net/album/{ID}'],
  2214. ['TIDAL_ID', 'https://listen.tidal.com/album/{ID}'],
  2215. ['OTOTOY_ID', 'https://ototoy.jp/_/default/p/{ID}'],
  2216. ['YANDEX_ID', 'https://music.yandex.ru/album/{ID}'],
  2217. ]) {
  2218. let ID = getHomoIdentifier(it[0]);
  2219. if (ID) return it[1].replace('{ID}', ID);
  2220. }
  2221. return undefined;
  2222. }
  2223.  
  2224. function getCoverOnline() {
  2225. var url = sourceUrl || release.urls[0], apiFirst;
  2226. if ((i = getHomoIdentifier('APPLE_ID') || getHomoIdentifier('ITUNES_ID')) || itunesRlsParser.test(url) && (i = parseInt(RegExp.$1))) {
  2227. apiFirst = queryItunesAPI('lookup', { id: i })
  2228. .then(lookup => lookup.resultCount > 0 ? setItunesImage(lookup.results[0]) : Promise.reject('no cover'));
  2229. } else if (i = getHomoIdentifier('DEEZER_ID') || dzrRlsParser.test(url) && (i = parseInt(RegExp.$1))) {
  2230. apiFirst = queryDeezerAPI('album/' + i)
  2231. .then(result => result.id ? setDeezerImage(result) : Promise.reject('No cover'));
  2232. } else if ((prefs.discogs_key && prefs.discogs_secret || discogs_token)
  2233. && (i = getHomoIdentifier('DISCOGS_ID') || dcRlsParser.test(url) && RegExp.$1)) {
  2234. apiFirst = queryDiscogsAPI('releases/' + i).then(function(release) {
  2235. return release.images.length > 0 ? setCover(release.images[0].uri) : Promise.reject('No cover');
  2236. });
  2237. } else if ((i = getHomoIdentifier('MBID') || mbrRlsParser.test(url) && RegExp.$1)) {
  2238. apiFirst = getMusicBrainzCovers(i).then(function(covers) {
  2239. return covers != null ? setCover(covers[1][0]) : Promise.reject('No cover');
  2240. });
  2241. } else if (i = getHomoIdentifier('TIDAL_ID') || tidalRlsParser(url)
  2242. && (i = parseInt(RegExp.$1)) > 0) apiFirst = queryTidalAPI('album', { albumId: i }).then(function(album) {
  2243. for (var row of album.rows) {
  2244. var albumHeader = row.modules.find(module => module.type == 'ALBUM_HEADER');
  2245. if (albumHeader != undefined && albumHeader.album.cover) return 'https://resources.tidal.com/images/'
  2246. .concat(albumHeader.album.cover.replace(/-/g, '/'), '/1280x1280.jpg');
  2247. }
  2248. return Promise.reject('Image not found');
  2249. }); else if (url.toLowerCase().includes('mora.jp/')) apiFirst = loadMoraMetadata(url).then(function(packageMeta) {
  2250. return setCover(packageMeta.packageUrl + packageMeta.fullsizeimage);
  2251. }); else if (/\/\/(?:www\.)?(?:hdtracks)(?:\.\w+)+\//i.test(url))
  2252. apiFirst = queryHdtracksApi(url).then(album => setCover(album.cover));
  2253. else apiFirst = Promise.reject('No known API binding');
  2254. return apiFirst.catch(reason => imageUrlResolver(url).then(setCover));
  2255. }
  2256.  
  2257. function searchCoverOnline() {
  2258. switch (typeof prefs.cover_lookup_provider == 'string' && prefs.cover_lookup_provider.toLowerCase()) {
  2259. case 'itunes': return searchCoverOnline_iTunes();
  2260. case 'deezer': return searchCoverOnline_Deezer();
  2261. case 'google': return searchCoverOnline_GooglePlay();
  2262. case 'musicbrainz': return searchCoverOnline_MBR();
  2263. case 'lastfm': return searchCoverOnline_LastFM();
  2264. case 'qobuz': return searchCoverOnline_Qobuz();
  2265. case 'all': return searchCoverOnline_iTunes()
  2266. .catch(searchCoverOnline_LastFM)
  2267. .catch(searchCoverOnline_Deezer)
  2268. .catch(searchCoverOnline_MBR)
  2269. .catch(searchCoverOnline_Qobuz)
  2270. .catch(searchCoverOnline_GooglePlay);
  2271. }
  2272. return Promise.reject('No valid service selected');
  2273.  
  2274. function searchCoverOnline_iTunes() {
  2275. return amLookup().then(album => setItunesImage(album)
  2276. .then(imgUrl => { info('Apple Music', album.collectionViewUrl, album.collectionId) }));
  2277. }
  2278. function searchCoverOnline_Deezer() {
  2279. return deezerLookup().then(function(album) {
  2280. return setDeezerImage(album)
  2281. .then(imgUrl => { info('Deezer', deezerAlbumPrefix + album.id, album.id) });
  2282. });
  2283. }
  2284. function searchCoverOnline_GooglePlay() {
  2285. var query = new URLSearchParams({
  2286. q: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2287. c: 'music',
  2288. });
  2289. return globalFetch('https://play.google.com/store/search?'.concat(query)).then(function(response) {
  2290. try {
  2291. let _objs = loadGoogleData(response);
  2292. let _results = _objs.filter(function(obj) { try { return typeof obj[0][4] == 'boolean' } catch(e) { return false } });
  2293. if (_results.length == 1) _results = _results[0][0][1]; else throw 'Results metadata not found';
  2294. } catch(e) { }
  2295. var results = response.document.querySelectorAll('div:first-of-type + div[jscontroller]:last-of-type');
  2296. if (results.length > 0) for (var ndx = 0; ndx < results.length; ++ndx) {
  2297. let items = [];
  2298. results[ndx].querySelectorAll(':scope > div').forEach(function(result) {
  2299. var img = result.querySelector('span > span > img');
  2300. img = img != null ? (img.src || img.dataset.src).replace(/=[a-z]\d+$/, '=w0') : null;
  2301. var album = result.querySelector('a > div[title]');
  2302. if (album == null) return;
  2303. var artist = album.parentNode.parentNode.parentNode.querySelector('a > div:not([title])')
  2304. artist = artist != null ? artist.textContent.trim() : null;
  2305. var url = album.parentNode.href;
  2306. var id = /\?id=(\w+)\b/i.test(album.parentNode.href) && RegExp.$1 || null;
  2307. album = album.textContent.trim();
  2308. items.push({ id: id, url: url, artist: artist, album: album, imgUrl: img });
  2309. });
  2310. for (i = 0; i < 3; ++i) {
  2311. var f = items.filter(release => releasesMatch(release.artist, release.album, i));
  2312. if (f.length > 1) return Promise.reject('Google Play Music: ambiguity');
  2313. if (f.length == 1) break;
  2314. }
  2315. if (i >= 3) return Promise.reject('Google Play Music: no matches');
  2316. if (prefs.diag_mode && i == 2) console.debug('Google Play Music fuzzy match:', release, '==', f[0]);
  2317. if (f[0].imgUrl) return setCover(f[0].imgUrl)
  2318. .then(release => { info('Google Play Music', f[0].url, f[0].id) });
  2319. }
  2320. });
  2321. return Promise.reject('Google Play Music: no matches');
  2322. }
  2323. function searchCoverOnline_MBR() {
  2324. return mbLookupByBarcode().catch(mbLookupByASIN).catch(mbLookupByTOC)
  2325. .catch(reason => mbLookup().then(release => [release]))
  2326. .catch(mbLookupByAutoTOC)
  2327. .then(releases => Promise.all(releases.map(release => getMusicBrainzCovers(release.id))))
  2328. .then(function(releases) {
  2329. for (var rls of releases) if (rls != null) return _setCover(rls);
  2330. return Promise.reject('MusicBrainz: no covers found');
  2331. });
  2332.  
  2333. function _setCover(rls) {
  2334. return setCover(rls[1][0]).then(function(imgUrl) {
  2335. if (/\/release\/(\S+)$/i.test(rls[0])) info('Musicbrains', rls[0], RegExp.$1);
  2336. return imgUrl;
  2337. });
  2338. }
  2339. }
  2340. function searchCoverOnline_LastFM() {
  2341. return queryLastFmAPI('album.getinfo', {
  2342. artist: (isVA ? VA : release.artist),
  2343. album: release.album,
  2344. }).then(function(result) {
  2345. if (result.error) return Promise.reject('Last.fm: '.concat(result.message));
  2346. var image = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
  2347. return acc || result.album.image.find(image => image.size == size && urlParser.test(image['#text']));
  2348. }, undefined);
  2349. if (!image) return Promise.reject('Last.fm: no cover for matched album');
  2350. image = image['#text'];
  2351. return setCover(image.replace(/\/\d+(?:x\d+|s)\//i, '/')).catch(reason => setCover(image))
  2352. .then(() => { info('Last.fm', result.album.url, result.album.id || result.album.mbid || '#N/A') });
  2353. });
  2354. }
  2355. function searchCoverOnline_Qobuz() {
  2356. qbLookup().then(function(album) {
  2357. return setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_max'))
  2358. .catch(reason => setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_600')))
  2359. .catch(reason => setCover(album.imgUrl))
  2360. .then(function(imgUrl) {
  2361. info('Qobuz', album.href, album.id);
  2362. return imgUrl;
  2363. });
  2364. });
  2365. }
  2366. function info(service, url, id) {
  2367. addMessage(new HTML('used cover image from ' + service + ' release Id ' +
  2368. '<a style="color: #00f3ff;" target="_blank" href="'+ url + '">' + id + '</a>'), 'info');
  2369. }
  2370. }
  2371.  
  2372. function setItunesImage(album) {
  2373. return urlParser.test(album.artworkUrl100) ?
  2374. setCover(album.artworkUrl100.replace('100x100bb', '100000x100000-999'))
  2375. .catch(reason => setCover(album.artworkUrl100)) : Promise.reject('Apple Music image not valid URL');
  2376. }
  2377. function setDeezerImage(album) {
  2378. return urlParser.test(album.cover_xl) ?
  2379. setCover(album.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0'))
  2380. .catch(reason => setCover(album.cover_xl)) : Promise.reject('Deezer image not valid URL');
  2381. }
  2382.  
  2383. function completeFromOnlineSource(onlineTracks) {
  2384. fillMissingValue(document.getElementById('media'), 'media');
  2385. fillMissingValue(document.getElementById('year'), 'album_year');
  2386. ref = document.getElementById('remaster_year') || !isUpload && document.querySelector('input[name="year"]');
  2387. if (ref != null && !ref.disabled && (ref.value == '' || !isRED && ref.value == '---')) {
  2388. var value = getHomoValue('release_date');
  2389. if (value != null) ref.value = extractYear(value);
  2390. }
  2391. fillMissingValue(document.getElementById('remaster_record_label')
  2392. || document.querySelector('input[name="recordlabel"]'), 'label');
  2393. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  2394. || document.querySelector('input[name="cataloguenumber"]'))) {
  2395. let catNo = getHomoValue('catalog');
  2396. if (!catNo && onlineTracks.every(track => track.identifiers.BARCODE
  2397. && track.identifiers.BARCODE == onlineTracks[0].identifiers.BARCODE)) {
  2398. catNo = parseInt(onlineTracks[0].identifiers.BARCODE.replace(/\s+/g, ''));
  2399. }
  2400. if (catNo) ref.value = catNo;
  2401. }
  2402. return onlineTracks;
  2403.  
  2404. function getHomoValue(propName) {
  2405. return onlineTracks[0][propName] && onlineTracks.map(track => track[propName]).homogeneous() ?
  2406. onlineTracks[0][propName] : null;
  2407. }
  2408. function fillMissingValue(node, propName) {
  2409. if (!node || node.disabled || node.value != '' && (isRED || node.value != '---')) return;
  2410. var value = getHomoValue(propName);
  2411. if (value != null) node.value = value;
  2412. }
  2413. }
  2414.  
  2415. function onlineCheck(onlineTracks) {
  2416. if (!Array.isArray(onlineTracks) || onlineTracks.length <= 0) {
  2417. addMessage('online check not performed (empty tracklist)', 'notice');
  2418. return Promise.reject('No tracks');
  2419. }
  2420. var issueCounter = 0, hiresTimes = onlineTracks.some(function(track) {
  2421. var remainder = Math.floor((track.duration - Math.floor(track.duration)) * 1000) / 100;
  2422. return remainder > Math.floor(remainder);
  2423. });
  2424. onlineTracks.forEach(processTrackArtists);
  2425. if (onlineTracks[0].artist && onlineTracks.map(track => track.artist).homogeneous()
  2426. && (isVA ? !vaParser.test(onlineTracks[0].artist) : mainArtistMismatch())) {
  2427. ++issueCounter;
  2428. addMessage(new HTML('online album main artist mismatch ("' +
  2429. safeText(release.artist).bold() + '" ≠ "' + safeText(onlineTracks[0].artist).bold() + '")'), 'warning');
  2430. }
  2431. if (onlineTracks[0].album && onlineTracks.map(track => track.album).homogeneous()
  2432. && mismatch(release.album, onlineTracks[0].album)
  2433. && mismatch(release.album, removeFeatArtists(onlineTracks[0].album))) {
  2434. ++issueCounter;
  2435. addMessage(new HTML('online album title mismatch ("' +
  2436. safeText(release.album).bold() + '" ≠ "' + safeText(onlineTracks[0].album).bold() + '")'), 'warning');
  2437. }
  2438. if (onlineTracks[0].label && onlineTracks.map(track => track.label).homogeneous()
  2439. && mismatch(release.label, onlineTracks[0].label, /-/g)) {
  2440. ++issueCounter;
  2441. addMessage(new HTML('online album label mismatch ("' +
  2442. safeText(release.label).bold() + '" ≠ "' + safeText(onlineTracks[0].label).bold() + '")'), 'notice');
  2443. }
  2444. if (release.catalogs.length == 1
  2445. && onlineTracks[0].catalog && onlineTracks.map(track => track.catalog).homogeneous()
  2446. && mismatch(release.catalogs[0], onlineTracks[0].catalog, /[\s\-]/g)) {
  2447. ++issueCounter;
  2448. addMessage(new HTML('online album catalogue# mismatch ("' +
  2449. safeText(release.catalogs[0]).bold() + '" ≠ "' + safeText(onlineTracks[0].catalog).bold() + '")'), 'notice');
  2450. }
  2451. if (onlineTracks[0].album_year && onlineTracks.map(track => track.album_year).homogeneous()
  2452. && release.album_year != onlineTracks[0].album_year) {
  2453. ++issueCounter;
  2454. addMessage(new HTML('online album year mismatch (' +
  2455. (release.album_year || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].album_year.toString().bold() + ')'), 'warning');
  2456. }
  2457. if (onlineTracks[0].release_date && release.release_date && onlineTracks.map(track => track.release_date).homogeneous()
  2458. && new Date(release.release_date.toString()).getDateValue()
  2459. != new Date(onlineTracks[0].release_date.toString()).getDateValue()) {
  2460. ++issueCounter;
  2461. addMessage(new HTML('online album release date mismatch (' +
  2462. (release.release_date || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].release_date.toString().bold() + ')'), 'notice');
  2463. }
  2464. if (tracks.length != onlineTracks.length) {
  2465. ++issueCounter;
  2466. addMessage(new HTML('online album different tracklist length (' + tracks.length.toString().bold() +
  2467. ' ≠ ' + onlineTracks.length.toString().bold() + ')'), 'warning');
  2468. }
  2469. if (totalTime > 0) {
  2470. let ttOnline = onlineTracks.reduce((acc, track) => acc + (track.duration || NaN), 0);
  2471. if (ttOnline > 0 && Math.abs(totalTime - ttOnline) * 100 / ttOnline > (media == 'Vinyl' ?
  2472. prefs.vinyl_duration_divergency : hiresTimes ? 0.1 : prefs.duration_divergency)) {
  2473. ++issueCounter;
  2474. addMessage(new HTML('online album duration mismatch (' + makeTimeString(totalTime).bold() +
  2475. ' ≠ ' + makeTimeString(ttOnline).bold() + ')'), 'warning');
  2476. }
  2477. }
  2478. for (let ndx = 0; ndx < tracks.length; ++ndx) {
  2479. if (ndx >= onlineTracks.length) {
  2480. addMessage('end of online tracklist reached, tracks from #' + (ndx + 1) + ' to end will not be checked', 'notice');
  2481. break;
  2482. }
  2483. if (mismatch(tracks[ndx].title, onlineTracks[ndx].title)
  2484. && mismatch(tracks[ndx].title, removeFeatArtists(onlineTracks[ndx].title))) {
  2485. ++issueCounter;
  2486. addMessage('online track #' + (ndx + 1) + ' title mismatch ("' +
  2487. (tracks[ndx].title || '') + '" ≠ "' + (onlineTracks[ndx].title || '') + '")', 'warning');
  2488. }
  2489. if (onlineTracks[ndx].track_artist && onlineTracks[ndx].track_artist != onlineTracks[ndx].artist) {
  2490. var trackArtists = getArtists(tracks[ndx].track_artist);
  2491. var onlineSrackArtists = getArtists(onlineTracks[ndx].track_artist);
  2492. if (!trackArtists[0].equalCaselessTo(onlineSrackArtists[0])
  2493. || !trackArtists[1].equalCaselessTo(onlineSrackArtists[1])) {
  2494. ++issueCounter;
  2495. addMessage('online track #' + (ndx + 1) + ' track artist mismatch ("' +
  2496. (tracks[ndx].track_artist || '') + '" ≠ "' + (onlineTracks[ndx].track_artist || '') + '")', 'notice');
  2497. }
  2498. }
  2499. if (onlineTracks[ndx].tracknumber && tracks[ndx].tracknumber != onlineTracks[ndx].tracknumber) {
  2500. ++issueCounter;
  2501. addMessage('online track #' + (ndx + 1) + ' track number mismatch (' +
  2502. (tracks[ndx].tracknumber || '<unset>') + ' ≠ ' + onlineTracks[ndx].tracknumber + ')',
  2503. release.totaldiscs > 1 ? 'notice' : 'warning');
  2504. }
  2505. if (onlineTracks[ndx].discnumber && (onlineTracks[ndx].discnumber > 1 || tracks[ndx].discnumber)
  2506. && tracks[ndx].discnumber != onlineTracks[ndx].discnumber) {
  2507. ++issueCounter;
  2508. addMessage('online track #' + (ndx + 1) + ' disc number mismatch (' +
  2509. (tracks[ndx].discnumber || '<unset>') + ' ≠ ' + onlineTracks[ndx].discnumber + ')', 'warning');
  2510. }
  2511. if (onlineTracks[ndx].discsubtitle && mismatch(tracks[ndx].discsubtitle, onlineTracks[ndx].discsubtitle)) {
  2512. ++issueCounter;
  2513. addMessage('online track #' + (ndx + 1) + ' disc subtitle mismatch ("' +
  2514. (tracks[ndx].discsubtitle || '') + '" ≠ "' + onlineTracks[ndx].discsubtitle + '")', 'notice');
  2515. }
  2516. let timeDif = tracks[ndx].duration && onlineTracks[ndx].duration
  2517. && Math.abs(tracks[ndx].duration - onlineTracks[ndx].duration);
  2518. if (timeDif >= (media != 'Vinyl' ? 2.5 : hiresTimes ? 0.1 : 5)) {
  2519. ++issueCounter;
  2520. addMessage('online track #' + (ndx + 1) + ' duration mismatch (' +
  2521. makeTimeString(tracks[ndx].duration) + ' ≠ ' + makeTimeString(onlineTracks[ndx].duration) + ')',
  2522. (timeDif >= (media != 'Vinyl' ? 5 : hiresTimes ? 0.2 : 8) ? 'warning' : 'notice'));
  2523. }
  2524. if (tracks[ndx].identifiers.MD5 && onlineTracks[ndx].identifiers.MD5 &&
  2525. tracks[ndx].identifiers.MD5.toUpperCase() != onlineTracks[ndx].identifiers.MD5.toUpperCase())
  2526. addMessage('online track #' + (ndx + 1) + ' MD5 mismatch ("' +
  2527. tracks[ndx].MD5.toUpperCase() + '" ≠ "' + onlineTracks[ndx].md5.toUpperCase() + '")', 'warning');
  2528. }
  2529. if (issueCounter == 0) {
  2530. i = 'online check completed without remarks';
  2531. if (prefs.messages_verbosity >= 1) addMessage(i, 'info'); else console.debug(i);
  2532. }
  2533.  
  2534. function mainArtistMismatch() {
  2535. var onlineMainArtists = getArtists(onlineTracks[0].artist);
  2536. return !onlineMainArtists[0].equalCaselessTo(artists[0]) || !onlineMainArtists[1].equalCaselessTo(albumGuests);
  2537. }
  2538. function removeFeatArtists(title) {
  2539. return featParsers.slice(3).reduce(function(acc, rx, ndx) {
  2540. return rx.test(acc) && (ndx < 5 || splitArtists(RegExp.$1).every((artist, ndx) => looksLikeTrueName(artist, 1))) ?
  2541. acc.replace(rx, '') : acc;
  2542. }, title || '')
  2543. }
  2544. function mismatch(localStr, onlineStr, rx) {
  2545. return normalize(localStr) != normalize(onlineStr);
  2546.  
  2547. function normalize(val) {
  2548. if (val == undefined || val == null) return '';
  2549. if (typeof val != 'string') val = val.toString();
  2550. if (rx instanceof RegExp || typeof rx == 'string') val = val.replace(rx, '');
  2551. val = val.replace(/[\(\)\-\s]+/g, '');
  2552. return prefs.strict_online_check ? val : val.toLowerCase();
  2553. }
  2554. }
  2555. }
  2556.  
  2557. function lookupOnlineSource() {
  2558. const commonMedia = !media || ['CD', 'WEB'].includes(media);
  2559. const noMultivolume = !release.totaldiscs || release.totaldiscs < 2;
  2560. var workers = [
  2561. /* 0 */ barCode ? querySpotifyAPI('search', { q: 'barcode:' + barCode, type: 'album' })
  2562. .then(result => result.albums.total > 0 ? result.albums.items : Promise.reject('Spotify: no matches'))
  2563. : Promise.reject('Spotify: unknown barcode'),
  2564. /* 1 */ commonMedia ? spotifyLookup() : Promise.reject('Spotify: different media'),
  2565. /* 2 */ mbLookupByBarcode(),
  2566. /* 3 */ mbLookupByASIN(),
  2567. /* 4 */ mbLookupByTOC(),
  2568. /* 5 */ mbLookup(),
  2569. /* 6 */ commonMedia && noMultivolume ? deezerLookup() : Promise.reject('Deezer: different media'),
  2570. /* 7 */ commonMedia ? amLookup() : Promise.reject('Apple Music: different media'),
  2571. /* 8 */ dcLookup(),
  2572. /* 9 */ mbLookupByAutoTOC(),
  2573. /* 10 */ commonMedia && noMultivolume ? queryLastFmAPI('album.getinfo', {
  2574. artist: (isVA ? VA : release.artist),
  2575. album: release.album,
  2576. }).then(result => result.error ? Promise.reject('Last.fm: '.concat(result.message)) : result.album)
  2577. : Promise.reject('Last.fm: different media'),
  2578. ];
  2579. if (prefs.diag_mode) workers.forEach(function(worker, ndx) {
  2580. worker.then(result => { console.debug('Worker[' + ndx + '] matched:', result) })
  2581. .catch(reason => { console.debug('Worker[' + ndx + '] failed:', reason) });
  2582. });
  2583. return workers[0].then(function(albums) {
  2584. if (prefs.diag_mode) console.debug('Spotify lookup by barcode successfull:', barCode, ' matches:', albums.length);
  2585. info('Spotify', albums[0].external_urls.spotify, albums[0].id);
  2586. return albums[0].href;
  2587. }).catch(reason => workers[1].then(function(album) {
  2588. info('Spotify', album.external_urls.spotify, album.id);
  2589. return album.href;
  2590. })).catch(reason => workers[2].then(mbEpilogue))
  2591. .catch(reason => workers[3].then(mbEpilogue))
  2592. .catch(reason => workers[4].then(mbEpilogue))
  2593. .catch(reason => workers[5].then(function(release) {
  2594. info('MusicBrainz', mbrRlsPrefix + release.id, release.id);
  2595. return mbrRlsPrefix.concat(release.id);
  2596. })).catch(reason => workers[6].then(function(album) {
  2597. info('Deezer', deezerAlbumPrefix.concat(album.id), album.id);
  2598. return 'https://api.deezer.com/album/'.concat(album.id);
  2599. })).catch(reason => workers[7].then(function(collection) {
  2600. info('Apple Music', collection.collectionViewUrl, collection.collectionId);
  2601. return collection.collectionViewUrl;
  2602. })).catch(reason => workers[8].then(function(releases) {
  2603. info('Discogs', discogsOrigin.concat(releases[0].uri), releases[0].id);
  2604. return releases[0].resource_url;
  2605. })).catch(reason => workers[9].then(mbEpilogue))
  2606. .catch(reason => workers[10].then(function(album) {
  2607. info('Last.fm', album.url, album.id || album.mbid || '#N/A');
  2608. return album; // return object
  2609. })).catch(function(reason) {
  2610. reason = 'online check not performed (no matches for this release)';
  2611. if (prefs.check_integrity_online) addMessage(reason, 'notice');
  2612. return Promise.reject(reason);
  2613. });
  2614.  
  2615. function mbEpilogue(releases) {
  2616. info('MusicBrainz', mbrRlsPrefix + releases[0].id, releases[0].id);
  2617. return mbrRlsPrefix + releases[0].id;
  2618. }
  2619. function info(service, url, id) {
  2620. if (prefs.check_integrity_online) addMessage(new HTML('checking online against ' + service +
  2621. ' release Id <a style="color: #00f3ff;" target="_blank" href="' + url + '">' + id + '</a>'), 'info');
  2622. }
  2623. }
  2624.  
  2625. function spotifyLookup() {
  2626. return querySpotifyAPI('search', {
  2627. q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
  2628. type: 'album',
  2629. limit: 50,
  2630. }).then(function(result) {
  2631. if (result.albums.total <= 0) return Promise.reject('Spotify: no matches');
  2632. for (i = 0; i < 3; ++i) {
  2633. var f = filter(i);
  2634. if (f.length > 1) return Promise.reject('Spotify: ambiguity');
  2635. if (f.length == 1) break;
  2636. }
  2637. if (i >= 3) return Promise.reject('Spotify: no matches');
  2638. if (prefs.diag_mode && i == 2) console.debug('Spotify fuzzy match:', release, '==', f[0]);
  2639. return f[0];
  2640.  
  2641. function filter(level) {
  2642. return result.albums.items.filter(function(album) {
  2643. return (album.album_type == 'single' ? ['Single', 'EP'].some(rt => releaseType == getReleaseIndex(rt))
  2644. : releaseType != getReleaseIndex('Single'))
  2645. && releasesMatch(album.artists.map(artist => artist.name), album.name, level);
  2646. });
  2647. }
  2648. })
  2649. }
  2650. function deezerLookup() {
  2651. return queryDeezerAPI('search', {
  2652. q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
  2653. strict: 'on',
  2654. order: 'RANKING',
  2655. }).then(function(result) {
  2656. if (result.total <= 0) return Promise.reject('Deezer: no matches');
  2657. for (i = 0; i < 3; ++i) {
  2658. var f = filter(i);
  2659. if (f.length > 1) return Promise.reject('Deezer: ambiguity');
  2660. if (f.length == 1) break;
  2661. }
  2662. if (i >= 3) return Promise.reject('Deezer: no matches');
  2663. if (i == 2) console.debug('Deezer fuzzy match:', release, '==', f[0]);
  2664. return f[0];
  2665.  
  2666. function filter(level) {
  2667. var albums = [];
  2668. result.data.forEach(function(match) {
  2669. if (!releasesMatch(match.artist.name, match.album.title, level)) return;
  2670. if (albums.find(album => album.id == match.album.id) == undefined) albums.push(match.album);
  2671. });
  2672. return albums;
  2673. }
  2674. });
  2675. }
  2676. function amLookup() {
  2677. return queryItunesAPI('search', {
  2678. term: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2679. media: 'music',
  2680. entity: 'album',
  2681. //country: 'US',
  2682. }).then(function(result) {
  2683. if (result.resultCount <= 0) return Promise.reject('Apple Music: no matches');
  2684. for (i = 0; i < 3; ++i) {
  2685. var f = filter(i);
  2686. if (f.length > 1) return Promise.reject('Apple Music: ambiguity');
  2687. if (f.length == 1) break;
  2688. }
  2689. if (i >= 3) return Promise.reject('Apple Music: no matches');
  2690. if (prefs.diag_mode && i == 2) console.debug('Apple Music fuzzy match:', release, '==', f[0]);
  2691. return f[0];
  2692.  
  2693. function filter(level) {
  2694. var preFilter = result.results.filter(function(collection) {
  2695. var isSingle = collection.collectionName.endsWith(' - Single');
  2696. if (isSingle) collection.collectionName = collection.collectionName.slice(0, -9);
  2697. var isEP = collection.collectionName.endsWith(' - EP');
  2698. if (isEP) collection.collectionName = collection.collectionName.slice(0, -5);
  2699. isSingle = isSingle || collection.collectionType == 'Single';
  2700. isEP = !isSingle && (isEP || collection.collectionType == 'EP');
  2701. return (releaseType == getReleaseIndex('Single')) == isSingle
  2702. && (!isEP || releaseType == getReleaseIndex('EP'))
  2703. && releasesMatch(collection.artistName, collection.collectionName, level);
  2704. });
  2705. return preFilter.some(collection => /\b(?:explicit)/i.test(collection.collectionExplicitness)) ?
  2706. preFilter.filter(collection => !/\b(?:clean)/i.test(collection.collectionExplicitness)) : preFilter;
  2707. }
  2708. });
  2709. }
  2710. function mbLookupByBarcode() {
  2711. if (!barCode) return Promise.reject('MusicBrainz: unknown barcode');
  2712. return queryMusicBrainzAPI('release', { query: 'barcode:' + barCode }).then(function(result) {
  2713. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2714. if (prefs.diag_mode) console.debug('MusicBrainz lookup by barcode successfull: ' + barCode + '; matches: ' + result.count);
  2715. return result.releases;
  2716. });
  2717. }
  2718. function mbLookupByASIN() {
  2719. var asin = getHomoIdentifier('ASIN');
  2720. if (!asin) return Promise.reject('MusicBrainz: unknown ASIN');
  2721. asin = asin.replace(/\s+/g, '');
  2722. return queryMusicBrainzAPI('release', { query: 'asin:' + asin }).then(function(result) {
  2723. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2724. if (prefs.diag_mode) console.debug('MusicBrainz lookup by ASIN successfull: ' + asin + '; matches: ' + result.count);
  2725. return result.releases;
  2726. });
  2727. }
  2728. function mbLookupByTOC() {
  2729. var TOC;
  2730. if (TOC = getHomoIdentifier('ITUNES_TOC')) { // iTunes scheme
  2731. TOC = TOC.split('+').map(index => parseInt(index));
  2732. TOC = [1, TOC[2], TOC[1]].concat(TOC.slice(3));
  2733. } else if (TOC = getHomoIdentifier('CT_TOC')) { // CUETools scheme
  2734. TOC = TOC.split('+').map(index => parseInt(index, 16));
  2735. TOC = [1, TOC.shift(), TOC.pop()].concat(TOC);
  2736. }
  2737. return mbLookupByDiscID(TOC);
  2738. }
  2739. function mbLookupByAutoTOC() {
  2740. if (release.totaldiscs > 1) return Promise.reject('TOC lookup not possible for multidisc release');
  2741. if (!tracks.every(track => track.sr > 0 && track.samples > 0))
  2742. return Promise.reject('MusicBrainz: insufficient information for TOC calculation');
  2743. var lastFrame = 0;
  2744. var TOC = [0].concat(tracks.map(track => (lastFrame += Math.round(track.samples * 75 / track.sr))))
  2745. .map(offset => 150 + offset);
  2746. TOC.unshift(TOC.pop());
  2747. return mbLookupByDiscID([1, tracks.length].concat(TOC));
  2748. }
  2749. function mbComputeDiscID(mbTOC) {
  2750. if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4 || mbTOC[1] - mbTOC[0] > 98) return null;
  2751. var tocStr = [mbTOC[0], mbTOC[1]].map(track => track.toString(16).padStart(2, '0'))
  2752. .concat(mbTOC.slice(2).map(offset => offset.toString(16).padStart(8, '0'))).join('')
  2753. .concat('0'.repeat(98 + mbTOC[0] - mbTOC[1] << 3)).toUpperCase();
  2754. var digest = sha1.digest(tocStr);
  2755. return btoa(String.fromCharCode(...digest)).replace(/\+/g, '.').replace(/\//g, '_').replace(/\=/g, '-');
  2756. }
  2757. function mbLookupByDiscID(mbTOC) {
  2758. if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4)
  2759. return Promise.reject('MusicBrainz: missing or invalid TOC');
  2760. var mbDscId = mbComputeDiscID(mbTOC);
  2761. var params = { toc: mbTOC.join('+') };
  2762. if (media != 'CD') params['media-format'] = 'all';
  2763. return queryMusicBrainzAPI('discid/'.concat(mbDscId || '-'), params).then(function(result) {
  2764. if (Array.isArray(result.releases) && result.releases.length > 0) var matches = result.releases;
  2765. if (result.id && result.title) matches = [result];
  2766. if (!Array.isArray(matches)) return Promise.reject('MusicBrainz: no matches');
  2767. if (prefs.diag_mode) console.debug('MusicBrainz lookup by discId/TOC successfull:', mbDscId, '/', params, 'matches:', matches);
  2768. return matches;
  2769. });
  2770. }
  2771. function mbLookup() {
  2772. return queryMusicBrainzAPI('release', {
  2773. query: 'release:"' + release.album + '" AND artist:"' + (isVA ? VA : release.artist) + '"',
  2774. }).then(function(result) {
  2775. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2776. for (i = 0; i < 3; ++i) {
  2777. var f = filter(i);
  2778. if (f.length > 1) return Promise.reject('MusicBrainz: ambiguity');
  2779. if (f.length == 1) break;
  2780. }
  2781. if (i >= 3) return Promise.reject('MusicBrainz: no matches');
  2782. if (prefs.diag_mode && i == 2) console.debug('MusicBrainz fuzzy match:', release, '==', f[0]);
  2783. return f[0];
  2784.  
  2785. function filter(level) {
  2786. return result.releases.filter(function(release) {
  2787. return release.quality != 'low'
  2788. && (media ? [media] : tracks.some(notRedBook) ? ['WEB'] : ['CD', 'WEB'])
  2789. .some(_media => release.media.map(media => remapMedia(media.format)).includes(_media)
  2790. && releasesMatch(release['artist-credit'].map(artist => artist.name), release.title, level));
  2791. });
  2792.  
  2793. function remapMedia(MBmedia) {
  2794. return [
  2795. ['Digital Media', 'WEB'],
  2796. ].reduce((acc, subst) => acc.toLowerCase() == subst[0].toLowerCase() ? subst[1] : acc, MBmedia);
  2797. }
  2798. }
  2799. });
  2800. }
  2801. function dcLookup() {
  2802. var query = { type: 'release' };
  2803. if (barCode) query.barcode = barCode; else {
  2804. query.artist = '"' + release.artist + '"';
  2805. query.release_title = '"' + release.album + '"';
  2806. //if (release.catalogs.length > 0) query.catno = release.catalogs.join('; ');
  2807. }
  2808. return queryDiscogsAPI('database/search', query).then(function(result) {
  2809. if (result.results.length <= 0) return Promise.reject('Discogs: no matches');
  2810. if (barCode) {
  2811. //if (result.results.length > 1) return Promise.reject('Discogs: ambiguity');
  2812. if (prefs.diag_mode) console.debug('Discogs lookup by barcode successfull: ' +
  2813. barCode + '; matches: ' + result.results.length);
  2814. var f = result.results;
  2815. } else {
  2816. for (i = 0; i < 3; ++i) {
  2817. f = filter(i);
  2818. if (f.length > 1) return Promise.reject('Discogs: ambiguity');
  2819. if (f.length == 1) break;
  2820. }
  2821. if (i >= 3) return Promise.reject('Discogs: no matches');
  2822. if (prefs.diag_mode && i == 2) console.debug('Discogs fuzzy match:', release, '==', f[0]);
  2823. }
  2824. return f;
  2825.  
  2826. function filter(level) {
  2827. return result.results.filter(function(album) {
  2828. if (media ? Array.isArray(album.format)
  2829. && !album.format.some(format => dcFmtToGazelle(format) == media)
  2830. : !album.format.some(format => ['CD', 'WEB'].includes(dcFmtToGazelle(format)))) return false;
  2831. if (/^(.*?)\s+\(\d+\) - (.*)$/.test(album.title) || !/^(.*?) - (.*)$/.test(album.title))
  2832. return releasesMatch(RegExp.$1, RegExp.$2, level);
  2833. console.warn('Failed to parse Discogs title:', album.title);
  2834. return false;
  2835. });
  2836. }
  2837. });
  2838. }
  2839. function qbLookup() {
  2840. var params = new URLSearchParams({
  2841. q: (isVA ? VA : release.artist) + ' ' + release.album,
  2842. //s: 'rdc', // descending sort by release date
  2843. i: 'boutique',
  2844. });
  2845. return globalFetch('https://www.qobuz.com/search?' + params).then(function(response) {
  2846. var results = response.document.querySelectorAll('div.search-results > div.product');
  2847. if (results.length <= 0) return Promise.reject('Qobuz: no matches');
  2848. for (i = 0; i < 3; ++i) {
  2849. var f = filter(i);
  2850. if (f.length > 1) return Promise.reject('Qobuz: ambiguity');
  2851. if (f.length == 1) break;
  2852. }
  2853. if (i >= 3) return Promise.reject('Qobuz: no matches');
  2854. if (prefs.diag_mode && i == 2) console.debug('Qobuz fuzzy match:', release, '==', f[0]);
  2855. return f[0];
  2856.  
  2857. function filter(level) {
  2858. var _results = [];
  2859. results.forEach(function(result) {
  2860. var _result = {};
  2861. _result.artist = result.querySelector('div.artist-name > a');
  2862. if (_result.artist != null) _result.artist = _result.artist.textContent.trim();
  2863. _result.title = result.querySelector('div.album-title > a');
  2864. if (_result.title != null) {
  2865. _result.id = _result.title.pathname.replace(/^.*\//, '');
  2866. _result.href = 'https://www.qobuz.com' + _result.title.pathname;
  2867. _result.title = _result.title.textContent.trim();
  2868. }
  2869. _result.imgUrl = result.querySelector('div.album-cover > a > img');
  2870. if (_result.imgUrl != null) _result.imgUrl = _result.imgUrl.dataset.src || _result.imgUrl.src;
  2871. if (_result.artist && _result.title && _result.imgUrl
  2872. && releasesMatch(_result.artist, _result.title, level, 0.75)) _results.push(_result);
  2873. });
  2874. return _results;
  2875. }
  2876. });
  2877. }
  2878.  
  2879. function ruleLink(rule) {
  2880. return ' (<a href="https://redacted.ch/rules.php?p=upload#r' + rule + '" target="_blank">' + rule + '</a>)';
  2881. }
  2882.  
  2883. function releasesMatch(remoteArtist, remoteTitle, relaxLevel = 0, minSimilarity = 0.75, minFullSimilarity) {
  2884. if (typeof remoteArtist == 'string') {
  2885. if (isVA != vaParser.test(remoteArtist)) return false;
  2886. if (!isVA) remoteArtist = getArtists(remoteArtist)[0];
  2887. } else if (!Array.isArray(remoteArtist)) return false;
  2888. if (!isVA && !artists[0].equalCaselessTo(remoteArtist)
  2889. && !artists[0].map(name => name.toASCII()).equalCaselessTo(remoteArtist.map(name => name.toASCII())))
  2890. return false;
  2891. if (!remoteTitle) return true;
  2892. if (typeof remoteTitle != 'string') return false;
  2893. var localTitle = release.album.toLowerCase();
  2894. if (localTitle == (remoteTitle = remoteTitle.toLowerCase())) return true;
  2895. if (editionTitle) var fullLocalTitle = localTitle.concat(' (', editionTitle.toLowerCase(), ')');
  2896. if (fullLocalTitle === remoteTitle) return true;
  2897. if (localTitle.toASCII() == remoteTitle.toASCII()
  2898. || fullLocalTitle && fullLocalTitle.toASCII() == remoteTitle.toASCII()) return true;
  2899. if (relaxLevel <= 0) return false;
  2900. if ([
  2901. /[\s\,\.\-\!\(\)\!\?]+/g,
  2902. /\s+\(([^\(\)]+)\)$/,
  2903. /\s+\[([^\[\]]+)\]$/,
  2904. /\s+\{([^\{\}]+)\}$/,
  2905. ].reduce(function(acc, rx) {
  2906. return acc || localTitle.replace(rx, '') == remoteTitle.replace(rx, '')
  2907. || fullLocalTitle && fullLocalTitle.replace(rx, '') == remoteTitle.replace(rx, '');
  2908. }, false)) return true;
  2909. if (relaxLevel <= 1) return false;
  2910. var similarity = cosineSimilarity(localTitle, remoteTitle);
  2911. if (similarity >= Math.min(minSimilarity, 1)) {
  2912. if (prefs.diag_mode) console.debug('Cosine similarity accepted: "' + localTitle + '"=="' + remoteTitle + '" (' + similarity+ ')');
  2913. return true;
  2914. }
  2915. similarity = cosineSimilarity(fullLocalTitle, remoteTitle);
  2916. if (fullLocalTitle && similarity >= Math.min(minFullSimilarity || minSimilarity + 0.05, 1)) {
  2917. if (prefs.diag_mode) console.debug('Cosine similarity accepted: "' + fullLocalTitle + '"=="' + remoteTitle + '" (' + similarity+ ')');
  2918. return true;
  2919. }
  2920. if (relaxLevel <= 2) return false;
  2921. if (localTitle.includes(remoteTitle) || remoteTitle.includes(localTitle)) return true;
  2922. return false;
  2923. }
  2924.  
  2925. function trackComparer(a, b) {
  2926. var cmp;
  2927. if (release.totaldiscs > 1) {
  2928. cmp = a.discnumber - b.discnumber;
  2929. if (!isNaN(cmp) && cmp != 0) return cmp;
  2930. } else {
  2931. cmp = (a.discsubtitle || '').localeCompare(b.discsubtitle || '');
  2932. //if (cmp != 0) return cmp;
  2933. }
  2934. cmp = parseInt(a.tracknumber) - parseInt(b.tracknumber);
  2935. if (!isNaN(cmp)) return cmp;
  2936. var m1 = vinyltrackParser.exec(a.tracknumber.toUpperCase());
  2937. var m2 = vinyltrackParser.exec(b.tracknumber.toUpperCase());
  2938. return m1 != null && m2 != null ?
  2939. m1[1].localeCompare(m2[1]) || parseFloat(m1[2]) - parseFloat(m2[2]) :
  2940. a.tracknumber.toUpperCase().localeCompare(b.tracknumber.toUpperCase());
  2941. }
  2942.  
  2943. function reqSelectFormats(...vals) {
  2944. vals.forEach(function(val) {
  2945. ['MP3', 'FLAC', 'AAC', 'AC3', 'DTS'].forEach(function(fmt, ndx) {
  2946. if (val.toLowerCase() == fmt.toLowerCase() && (ref = document.getElementById('format_' + ndx)) != null) {
  2947. ref.checked = true;
  2948. ref.onchange();
  2949. }
  2950. });
  2951. });
  2952. }
  2953.  
  2954. function reqSelectBitrates(...vals) {
  2955. const bitrateSet = !isOPS ? [
  2956. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  2957. 'V0 (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  2958. ] : [
  2959. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  2960. 'V0 (VBR)', 'q8.x (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  2961. ];
  2962. vals.forEach(function(val) {
  2963. var ndx = 10;
  2964. bitrateSet.forEach((it, _ndx) => { if (val.toString().toLowerCase() == it.toString().toLowerCase()) ndx = _ndx });
  2965. if ((ref = document.getElementById('bitrate_' + ndx)) != null) {
  2966. ref.checked = true;
  2967. ref.onchange();
  2968. }
  2969. });
  2970. }
  2971.  
  2972. function reqSelectMedias(...vals) {
  2973. const mediaSet = !isOPS ? ['CD', 'DVD', 'Vinyl', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB', 'Blu-Ray']
  2974. : ['CD', 'DVD', 'Vinyl', 'BD', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB'];
  2975. vals.forEach(function(val) {
  2976. mediaSet.forEach(function(med, ndx) {
  2977. if (val == med && (ref = document.getElementById('media_' + ndx)) != null) {
  2978. ref.checked = true;
  2979. ref.onchange();
  2980. }
  2981. });
  2982. if (val == 'CD') {
  2983. if ((ref = document.getElementById('needlog')) != null) {
  2984. ref.checked = true;
  2985. ref.onchange();
  2986. if ((ref = document.getElementById('minlogscore')) != null) ref.value = 100;
  2987. }
  2988. if ((ref = document.getElementById('needcue')) != null) ref.checked = true;
  2989. //if ((ref = document.getElementById('needchecksum')) != null) ref.checked = true;
  2990. }
  2991. });
  2992. }
  2993.  
  2994. function getReleaseIndex(str) {
  2995. var ndx;
  2996. [
  2997. ['Album', 1],
  2998. ['Soundtrack', 3],
  2999. ['EP', 5],
  3000. ['Anthology', 6],
  3001. ['Compilation', 7],
  3002. ['Single', 9],
  3003. ['Live album', 11],
  3004. ['Remix', 13],
  3005. ['Bootleg', 14],
  3006. ['Interview', 15],
  3007. ['Mixtape', 16],
  3008. [isOPS ? 'DJ Mix' : 'Demo', 17],
  3009. ['Concert Recording', 18],
  3010. ['DJ Mix', 19],
  3011. ['Unknown', 21],
  3012. ].forEach(k => { if (str.toLowerCase() == k[0].toLowerCase()) ndx = k[1] });
  3013. return ndx || 21;
  3014. }
  3015.  
  3016. function getChanString(n) {
  3017. if (!n) return null;
  3018. const chanmap = [
  3019. 'mono',
  3020. 'stereo',
  3021. '2.1',
  3022. '4.0 surround sound',
  3023. '5.0 surround sound',
  3024. '5.1 surround sound',
  3025. '7.0 surround sound',
  3026. '7.1 surround sound',
  3027. ];
  3028. return n >= 1 && n <= 8 ? chanmap[n - 1] : n + 'chn surround sound';
  3029. }
  3030.  
  3031. function fetchOnlineAdditions() {
  3032. if (onlineSource) return Promise.reject('Not offline source');
  3033. var url = sourceUrl || release.urls[0];
  3034. if (!urlParser.test(url)) return Promise.reject('No valid URL to parse');
  3035. if (url.toLowerCase().includes('highresaudio.com/'))
  3036. return globalFetch(url).then(response => hraPdfBooklet(response) || Promise.reject('No PDF booklet'));
  3037. return Promise.reject('No online source containing additions');
  3038. }
  3039.  
  3040. function processTrackArtists(track) {
  3041. if (!track.artist && Array.isArray(track.artists) && track.artists.length > 0) {
  3042. track.artist = joinArtists(track.artists);
  3043. if (Array.isArray(track.featuring_artists) && track.featuring_artists.length > 0)
  3044. track.artist += ' feat. '.concat(joinArtists(track.featuring_artists));
  3045. }
  3046. if (!track.track_artist && Array.isArray(track.track_artists) && track.track_artists.length > 0) {
  3047. track.track_artist = joinArtists(track.track_artists);
  3048. if (Array.isArray(track.track_guests) && track.track_guests.length > 0)
  3049. track.track_artist += ' feat. '.concat(joinArtists(track.track_guests));
  3050. }
  3051. ['performer', 'remixer', 'composer', 'conductor', 'compiler', 'producer'].forEach(function(role) {
  3052. var arrPropName = role.concat('s');
  3053. if (!track[role] && Array.isArray(track[arrPropName]) && track[arrPropName].length > 0)
  3054. track[role] = track[arrPropName].join(', ');
  3055. });
  3056. }
  3057. } // parseTracks
  3058.  
  3059. function hraPdfBooklet(response) {
  3060. var ref = response.document.querySelector('form#pdfjs-form-w2[action]');
  3061. if (ref == null) return undefined;
  3062. ref = new URLSearchParams(ref.action.replace(/^.*\?/, ''));
  3063. return '[url='.concat(ref.get('file'), '][img]https://ptpimg.me/ts0fy8.png[/img][/url]');
  3064. }
  3065.  
  3066. function fetchOnline_Music(url, weak = false) {
  3067. if (!urlParser.test(url)) return Promise.reject('Invalid URL');
  3068. const discParser = /^(?:CD|DIS[CK]\s+|VOLUME\s+|DISCO\s+|DISQUE\s+)(\d+)(?:\s+of\s+(\d+))?$/i;
  3069. var ref, artist, album, albumYear, releaseDate, channels, label, composer, bd, sr = 44100, description,
  3070. compiler, producer, totalTracks, discSubtitle, discNumber, trackNumber, totalDiscs, title, trackArtist,
  3071. catalogue, encoding, format, bitrate, duration, country, media = 'WEB', imgUrl,
  3072. genres = [], trs, tracks = [], identifiers = {}, trackIdentifiers = {};
  3073. if (url.toLowerCase().includes('qobuz.com/')) return globalFetch(url).then(function(response) {
  3074. const error = new Error('Failed to parse Qobus release page');
  3075. identifiers.QOBUZ_ID = response.finalUrl.replace(/^.*\//, '');
  3076. if ((ref = response.document.querySelector('section.album-item[data-gtm]')) != null) try {
  3077. let gtm = JSON.parse(ref.dataset.gtm);
  3078. if (gtm.shop.category) genres.push(gtm.shop.category);
  3079. if (gtm.shop.subCategory && !genres.includes(gtm.shop.subCategory)) genres.push(gtm.shop.subCategory.replace(/-/g, ' '));
  3080. } catch(e) { console.warn(e) }
  3081. if ((ref = response.document.querySelector('div.album-meta > h2.album-meta__artist')) != null)
  3082. artist = ref.title || ref.textContent.trim();
  3083. isVA = vaParser.test(artist);
  3084. album = (ref = response.document.querySelector('div.album-meta > h1.album-meta__title')) != null ?
  3085. ref.title || ref.textContent.trim() : undefined;
  3086. releaseDate = (ref = response.document.querySelector('div.album-meta > ul > li:first-of-type')) != null ?
  3087. normalizeDate(ref.textContent) : undefined;
  3088. var mainArtist = (ref = response.document.querySelector('div.album-meta > ul > li:nth-of-type(2) > a')) != null ?
  3089. ref.title || ref.textContent.trim() : undefined;
  3090. //ref = response.document.querySelector('p.album-about__copyright');
  3091. //if (ref != null) albumYear = extractYear(ref.textContent);
  3092. response.document.querySelectorAll('section#about > ul > li').forEach(function(it) {
  3093. function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) }
  3094. if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1);
  3095. if (/\b(\d+)\s*(?:track|pist[ae]|tracce|traccia)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1);
  3096. if (['Label', 'Etichetta', 'Sello'].some(l => it.textContent.trimLeft().startsWith(l))) {
  3097. label = it.firstElementChild.textContent.replace(/\s+/g, ' ').trim();
  3098. }
  3099. else if (['Composer', 'Compositeur', 'Komponist', 'Compositore', 'Compositor'].some(matchLabel)) {
  3100. composer = it.firstElementChild.textContent.trim();
  3101. if (pseudoArtistParsers.some(rx => rx.test(composer))) composer = undefined;
  3102. } else if (['Genre', 'Genere', 'Género'].some(g => it.textContent.startsWith(g)) && it.childElementCount > 0
  3103. && genres.length <= 0) {
  3104. genres = Array.from(it.querySelectorAll('a')).map(elem => elem.textContent.trim());
  3105. /*
  3106. if (genres.length >= 1 && ['Pop/Rock'].includes(genres[0])) genres.shift();
  3107. if (genres.length >= 2 && ['Alternative & Indie'].includes(genres[genres.length - 1])) genres.shift();
  3108. if (genres.length >= 1 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) {
  3109. while (genres.length > 1) genres.shift();
  3110. }
  3111. */
  3112. while (genres.length > 1) genres.shift();
  3113. }
  3114. });
  3115. bd = 16; channels = 2; // defaults to CD quality
  3116. response.document.querySelectorAll('span.album-quality__info').forEach(function(k) {
  3117. if (/\b([\d\.\,]+)\s*kHz\b/i.test(k.textContent) != null) sr = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
  3118. if (/\b(\d+)[\-\s]*Bits?\b/i.test(k.textContent) != null) bd = parseInt(RegExp.$1);
  3119. if (/\b(?:Stereo)\b/i.test(k.textContent)) channels = 2;
  3120. if (/\b(\d)\.(\d)\b/.test(k.textContent)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  3121. });
  3122. getDescription(response, 'section#description > p', true);
  3123. if ((ref = response.document.querySelector('a[title="Qobuzissime"]')) != null) {
  3124. if (description) description += '\n';
  3125. description += '[align=center][url=https://www.qobuz.com'
  3126. .concat(ref.pathname, '][img]https://ptpimg.me/4z35uj.png[/img][/url][/align]');
  3127. }
  3128. if ((ref = response.document.querySelector('div.album-cover > img')) != null)
  3129. imgUrl = ref.src.replace(/_\d{3}(?=\.\w+$)/, '_max');
  3130. addTracks(response.document);
  3131. if (totalTracks <= 50) return tracks;
  3132. var query = new URLSearchParams({
  3133. albumId: identifiers.QOBUZ_ID,
  3134. offset: 50,
  3135. limit: 999,
  3136. store: /\/(\w{2}-\w{2})\/album\//i.test(response.finalUrl) ? RegExp.$1 : 'fr-fr',
  3137. });
  3138. return globalFetch('https://www.qobuz.com/v4/ajax/album/load-tracks?'.concat(query), {
  3139. headers: { 'X-Requested-With': 'XMLHttpRequest' },
  3140. }).then(response => { addTracks(response.document) }).catch(function(reason) {
  3141. console.error('globalFetch() failed:', reason);
  3142. addMessage('long album, only first 50 tracks were captured from Qobuz, which will result in incmplete release description', 'notice');
  3143. }).then(function() { return tracks });
  3144.  
  3145. function addTracks(document) {
  3146. Array.prototype.push.apply(tracks, Array.from(document.querySelectorAll('div.player__item > div.player__tracks > div.track > div.track__items')).map(function(tr) {
  3147. trackIdentifiers = { TRACK_ID: tr.parentNode.dataset.track };
  3148. var trackGuests = [], trackComposers = [], trackRemixers = [], trackConductors = [], trackProducers = [], trackPerformers = [];
  3149. if ((ref = tr.parentNode.querySelector('p.track__info:first-of-type')) != null) {
  3150. trackArtist = [];
  3151. ref.textContent.trim().split(/\s+-\s+/).map(it => it.split(/\s*,\s*/)).forEach(function(it) {
  3152. var roles = it.slice(1);
  3153. if (roles.some(role => /^(?:(?:Main)?Artist)$/.test(role))) trackArtist.pushUnique(it[0]);
  3154. if (roles.some(role => /^(?:FeaturedArtist|)$/.test(role))) trackGuests.pushUnique(it[0]);
  3155. if (roles.some(role => /^(?:AssociatedPerformer)$/.test(role))) trackPerformers.pushUnique(it[0]);
  3156. if (roles.some(role => /^(?:Composer(?:Lyricist)?|Writer)$/.test(role))) trackComposers.pushUnique(it[0]);
  3157. if (roles.some(role => /^(?:Conductor)$/.test(role))) trackConductors.pushUnique(it[0]);
  3158. if (roles.some(role => /^(?:Remixer)$/.test(role))) trackRemixers.pushUnique(it[0]);
  3159. if (roles.some(role => /^(?:Producer)$/.test(role))) trackProducers.pushUnique(it[0]);
  3160. });
  3161. trackArtist = trackArtist.filter(artist => !trackConductors.includes(artist));
  3162. trackGuests = trackGuests.filter(artist => ![trackArtist, trackConductors].some(category => category.includes(artist)));
  3163. trackPerformers = trackPerformers.filter(artist => ![trackArtist, trackConductors, trackGuests].some(category => category.includes(artist)));
  3164. if ((trackArtist = joinArtists(trackArtist)) && trackGuests.length > 0)
  3165. trackArtist += ' feat. '.concat(joinArtists(trackGuests));
  3166. if (trackArtist && !isVA && trackArtist == artist) trackArtist = undefined;
  3167. } else trackArtist = undefined;
  3168. if (tr.parentNode.dataset.gtm) try {
  3169. let gtm = JSON.parse(tr.parentNode.dataset.gtm);
  3170. if (gtm.product.id) trackIdentifiers.QOBUZ_ID = gtm.product.id;
  3171. //if (gtm.product.type) trackIdentifiers.RELEASETYPE = gtm.product.type;
  3172. if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory];
  3173. } catch(e) { console.warn(e) }
  3174. if ((ref = tr.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) {
  3175. discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim();
  3176. guessDiscNumber();
  3177. }
  3178. return {
  3179. artist: isVA ? VA : artist,
  3180. album: album,
  3181. album_year: albumYear,
  3182. release_date: releaseDate,
  3183. label: label,
  3184. encoding: 'lossless',
  3185. codec: 'FLAC',
  3186. bd: bd || undefined,
  3187. sr: sr || undefined,
  3188. channels: channels || undefined,
  3189. media: media,
  3190. genre: genres.map(function(genre) {
  3191. genre = genre.replace(/-+/g, ' ');
  3192. qobuzTranslations.forEach(function(it) { if (genre.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) genre = it[1] });
  3193. return genre;
  3194. }).join('; '),
  3195. discnumber: discNumber || 1,
  3196. totaldiscs: totalDiscs,
  3197. discsubtitle: discSubtitle,
  3198. tracknumber: parseInt(tr.querySelector('span[itemprop="position"]').textContent),
  3199. totaltracks: totalTracks,
  3200. title: (tr.querySelector('div.track__item--name > span') || tr.querySelector('span.track__item--name'))
  3201. .textContent.trim().replace(/\s+/g, ' '),
  3202. track_artist: trackArtist,
  3203. composer: trackComposers.length <= 0 ? composer : undefined,
  3204. performers: trackPerformers.length > 0 ? trackPerformers : undefined,
  3205. composers: trackComposers.length > 0 ? trackComposers : undefined,
  3206. conductors: trackConductors.length > 0 ? trackConductors : undefined,
  3207. remixers: trackRemixers.length > 0 ? trackRemixers : undefined,
  3208. producers: trackProducers.length > 0 ? trackProducers : undefined,
  3209. duration: timeStringToTime(tr.querySelector('span.track__item--duration').textContent),
  3210. url: response.finalUrl,
  3211. description: description,
  3212. identifiers: mergeIds(),
  3213. cover_url: imgUrl,
  3214. };
  3215. }));
  3216. }
  3217. }); else if (url.toLowerCase().includes('highresaudio.com/')) return globalFetch(url).then(function(response) {
  3218. if (/\/album\/view\/(\w+)\//i.test(response.finalUrl)) identifiers.HRA_ID = RegExp.$1;
  3219. artist = (ref = response.document.querySelector('h1 > span.artist')) != null ? ref.textContent.trim() : undefined;
  3220. isVA = vaParser.test(artist);
  3221. album = (ref = response.document.getElementById('h1-album-title')) != null ? ref.firstChild.textContent.trim() : undefined;
  3222. response.document.querySelectorAll('div.album-col-info-data > div > p').forEach(function(k) {
  3223. var key = k.firstChild.textContent, value = k.lastChild.textContent.trim();
  3224. if (/\b(?:Genre|Subgenre)\b/i.test(key)) genres.push(value);
  3225. else if (/\b(?:Label)\b/i.test(key)) label = value;
  3226. else if (/\b(?:Album[\s\-]Release)\b/i.test(key)) albumYear = extractYear(value);
  3227. else if (/\b(?:HRA[\s\-]Release)\b/i.test(key)) releaseDate = normalizeDate(value);
  3228. });
  3229. sr = undefined;
  3230. response.document.querySelectorAll('tbody > tr > td.col-format').forEach(function(format) {
  3231. if (!/^(FLAC)\s*(\d+(?:[\.\,]\d+)?)\b/.test(format.textContent)) return;
  3232. if (sr) sr = NaN;
  3233. if (sr != undefined) return;
  3234. format = RegExp.$1;
  3235. sr = parseFloat(RegExp.$2.replace(',', '.')) * 1000;
  3236. });
  3237. getDescription(response, 'div#albumtab-info > p', false);
  3238. if (i = hraPdfBooklet(response)) {
  3239. if (description) description += '\n\n'.concat(i); else description = i;
  3240. }
  3241. url = (ref = response.document.querySelector('meta[property="og:url"][content]')) != null && ref.content;
  3242. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  3243. trs = response.document.querySelectorAll('ul.playlist > li.pltrack');
  3244. return Array.from(trs).map(function(tr) {
  3245. discNumber = undefined; discSubtitle = tr;
  3246. while ((discSubtitle = discSubtitle.previousElementSibling) != null) {
  3247. if (discSubtitle.nodeName == 'LI' && discSubtitle.className == 'plinfo') {
  3248. discSubtitle = discSubtitle.textContent.replace(/\s*:$/, '').trim();
  3249. guessDiscNumber();
  3250. break;
  3251. }
  3252. }
  3253. return {
  3254. artist: isVA ? VA : artist,
  3255. album: album,
  3256. album_year: albumYear,
  3257. release_date: releaseDate,
  3258. label: label,
  3259. encoding: 'lossless',
  3260. codec: 'FLAC',
  3261. bd: 24,
  3262. sr: sr || undefined,
  3263. media: media,
  3264. genre: genres.join('; '),
  3265. discnumber: discNumber,
  3266. totaldiscs: totalDiscs,
  3267. discsubtitle: discSubtitle || undefined,
  3268. tracknumber: parseInt(tr.querySelector('span.track').textContent) || tr.querySelector('span.track').textContent,
  3269. totaltracks: trs.length,
  3270. title: tr.querySelector('span.title').textContent.trim().replace(/\s+/g, ' '),
  3271. duration: timeStringToTime(tr.querySelector('span.time').textContent),
  3272. url: url || response.finalUrl,
  3273. description: description,
  3274. identifiers: mergeIds(),
  3275. cover_url: imgUrl,
  3276. };
  3277. });
  3278. }); else if (url.toLowerCase().includes('bandcamp.com/')) return globalFetch(url).then(function(response) {
  3279. artist = Array.from(response.document.querySelectorAll('span[itemprop="byArtist"] > a')).map(a => a.textContent.trim());
  3280. ref = response.document.querySelector('span.back-link-text > br');
  3281. if (ref != null && ref.nextSibling != null) label = ref.nextSibling.textContent.trim(); else {
  3282. ref = response.document.querySelector('p#band-name-location > span.title');
  3283. if (ref != null) label = ref.textContent.trim();
  3284. }
  3285. var tags = new TagManager;
  3286. response.document.querySelectorAll('div.tralbum-tags > a.tag').forEach(function(tag) {
  3287. if (!artist.some(artist => tag.textContent.trim().toLowerCase() == artist.toLowerCase())) tags.add(tag.textContent.trim());
  3288. });
  3289. if ((ref = response.document.querySelector('div#tralbumArt > a.popupImage')) != null) imgUrl = ref.href;
  3290. try {
  3291. response.document.querySelectorAll('div#propOpenWrapper > div[id] > script').forEach(function(script) {
  3292. if (!/\b(var\s+SiteData\s*=\s*\{[\S\s]+\};)/.test(script.text)) return;
  3293. eval(RegExp.$1);
  3294. if (typeof TralbumData != 'object') return;
  3295. if (prefs.diag_mode) console.debug('BandCamp metadata loaded:', TralbumData);
  3296. identifiers.BANDCAMP_ID = TralbumData.id;
  3297. identifiers.RELEASETYPE = TralbumData.item_type;
  3298. identifiers.BARCODE = TralbumData.current.upc/* || TralbumData.packages[0].upc*/;
  3299. isVA = vaParser.test(TralbumData.artist);
  3300. description = TralbumData.current.about;
  3301. if (TralbumData.current.credits) if (description) description += '\n\n'.concat(TralbumData.current.credits);
  3302. else description = TralbumData.current.credits;
  3303. tracks = TralbumData.trackinfo.map(function(track) {
  3304. trackIdentifiers = {
  3305. TRACK_ID: track.track_id,
  3306. //HASLYRICS: Number(track.has_lyrics) || 0,
  3307. };
  3308. return {
  3309. artist: isVA ? VA : TralbumData.artist,
  3310. album: TralbumData.current.title,
  3311. release_date: TralbumData.current.release_date || TralbumData.album_release_date,
  3312. description: description,
  3313. label: /*TralbumData.packages[0].label || */label,
  3314. //catalog: TralbumData.packages[0].sku,
  3315. genre: tags.toString(),
  3316. duration: track.duration || undefined,
  3317. lyrics: track.lyrics || undefined,
  3318. title: track.title,
  3319. tracknumber: track.track_num,
  3320. totaltracks: TralbumData.trackinfo.length,
  3321. media: 'WEB',
  3322. url: TralbumData.url ? TralbumData.url.replace(/^http\b/, 'https') : response.finalUrl,
  3323. cover_url: imgUrl,
  3324. identifiers: mergeIds(),
  3325. };
  3326. });
  3327. });
  3328. if (tracks.length <= 0) throw 'No tracks found';
  3329. return tracks;
  3330. } catch(e) {
  3331. console.warn('BandCamp: falling back to HTML scraper for the reason:', e);
  3332. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3333. if ((ref = response.document.querySelector('h2[itemprop="name"]')) != null) album = ref.textContent.trim();
  3334. ref = response.document.querySelector('div.tralbum-credits');
  3335. if (ref != null && /\brelease[ds]\s+(.*?\b\d{4})\b/i.test(ref.textContent)) releaseDate = RegExp.$1;
  3336. description = [];
  3337. response.document.querySelectorAll('div.tralbumData').forEach(function(div) {
  3338. if (!div.classList.contains('tralbum-tags')) description.push(html2php(div, response.finalUrl).trim());
  3339. });
  3340. description = description.filter(p => p).join('\n\n');
  3341. if (/\bShare\.initPanel\s*\(\s*\w+\s*,\s*\d+\s*,\s*"https?(:\/\/\S+?)"\s*\);/i.test(response.responseText))
  3342. var shareLink = 'https'.concat(RegExp.$1);
  3343. trs = response.document.querySelectorAll('table.track_list > tbody > tr[itemprop="tracks"]');
  3344. return Array.from(trs).map(tr => ({
  3345. artist: isVA ? VA : undefined,
  3346. artists: !isVA ? artist : undefined,
  3347. album: album,
  3348. //album_year: extractYear(releaseDate),
  3349. release_date: releaseDate,
  3350. label: label,
  3351. media: media,
  3352. genre: tags.toString(),
  3353. discnumber: discNumber,
  3354. totaldiscs: totalDiscs,
  3355. tracknumber: parseInt(tr.querySelector('div.track_number').textContent) || tr.querySelector('div.track_number').textContent,
  3356. totaltracks: trs.length,
  3357. title: (tr.querySelector('div.title span.track-title')
  3358. || tr.querySelector('div.title span[itemprop="name"]')).textContent.trim().replace(/\s+/g, ' '),
  3359. duration: durationFromMeta(tr) || (ref = tr.querySelector('span.time')) != null && timeStringToTime(ref.textContent) || undefined,
  3360. url: shareLink || response.finalUrl,
  3361. description: description,
  3362. identifiers: mergeIds(),
  3363. cover_url: imgUrl,
  3364. }));
  3365. }
  3366. }); else if (url.toLowerCase().includes('prestomusic.com/')) return globalFetch(url).then(function(response) {
  3367. const parenthesesStripper = /\s+\([^\(\)]*\)/g;
  3368. identifiers.COMPOSEREMPHASIS = 1;
  3369. artist = getArtists(response.document.querySelectorAll('div.c-product-block__contributors > p'));
  3370. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3371. ref = response.document.querySelector('h1.c-product-block__title');
  3372. if (ref != null) album = ref.lastChild.wholeText.trim();
  3373. response.document.querySelectorAll('div.c-product-block__metadata > ul > li').forEach(function(li) {
  3374. if (li.firstChild.textContent.includes('Release Date')) {
  3375. releaseDate = li.lastChild.wholeText;
  3376. if (/\b(\d+)\w*\s+(\w+)\s+(\d{4})\b/.test(releaseDate)) releaseDate = RegExp.$2 + ' ' + RegExp.$1 + ' ' + RegExp.$3;
  3377. } else if (li.firstChild.textContent.includes('Label')) {
  3378. label = li.lastChild.wholeText.trim();
  3379. } else if (li.firstChild.textContent.includes('Catalogue No')) {
  3380. catalogue = li.lastChild.wholeText.trim();
  3381. }
  3382. });
  3383. composer = [];
  3384. response.document.querySelectorAll('div#related > div > ul > li').forEach(function(li) {
  3385. if (li.parentNode.previousElementSibling.textContent.includes('Composers')) {
  3386. var _composer = li.firstChild.textContent.trim().replace(parenthesesStripper, '');
  3387. composer.push(_composer.replace(/^(.*?)\s*,\s+(.*)$/, '$2 $1'));
  3388. }
  3389. });
  3390. genres = undefined;
  3391. if (/\/jazz\//i.test(response.finalUrl)) genres = 'Jazz';
  3392. if (/\/classical\//i.test(response.finalUrl)) genres = 'Classical';
  3393. getDescription(response, 'div#about > div > p', true);
  3394. var personnel = [];
  3395. response.document.querySelectorAll('div.c-product-block__contributors > p').forEach(function(p) {
  3396. // TODO
  3397. });
  3398. if (personnel.length > 0) {
  3399. if (description) description += '\n\n';
  3400. description += personnel.join('\n');
  3401. }
  3402. ref = response.document.querySelector('div.c-product-block__aside > a');
  3403. if (ref != null) imgUrl = ref.href.replace(/\?\d+$/, '');
  3404. trs = response.document.querySelectorAll('div.has--sample');
  3405. trackNumber = 0;
  3406. return Array.from(trs).map(function(tr) {
  3407. discNumber = discSubtitle = undefined;
  3408. var parent = tr;
  3409. if (tr.classList.contains('c-track')) {
  3410. parent = tr.parentNode.parentNode;
  3411. if (parent.classList.contains('c-expander')) parent = parent.parentNode;
  3412. if ((ref = parent.querySelector(':scope > div > div > div > p.c-track__title')) != null) {
  3413. discSubtitle = ref.textContent.trim().replace(/\s+/g, ' ');
  3414. guessDiscNumber();
  3415. }
  3416. }
  3417. trackArtist = getArtists(parent.querySelectorAll(':scope > div.c-track__details > ul > li'));
  3418. return {
  3419. artist: isVA ? VA : undefined,
  3420. artists: !isVA ? artist : undefined,
  3421. album: album,
  3422. //album_year: extractYear(releaseDate),
  3423. release_date: releaseDate,
  3424. label: label,
  3425. catalog: catalogue,
  3426. media: media,
  3427. genre: genres,
  3428. discnumber: discNumber,
  3429. totaldiscs: totalDiscs,
  3430. discsubtitle: discSubtitle,
  3431. tracknumber: ++trackNumber,
  3432. totaltracks: trs.length,
  3433. title: (ref = tr.querySelector('p.c-track__title')) ? ref.textContent.trim().replace(/\s+/g, ' ') : undefined,
  3434. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  3435. composers: composer,
  3436. duration: timeStringToTime(tr.querySelector('div.c-track__duration').textContent),
  3437. description: description,
  3438. url: response.finalUrl,
  3439. cover_url: imgUrl,
  3440. identifiers: mergeIds(),
  3441. };
  3442. });
  3443.  
  3444. function getArtists(nodeList) {
  3445. var artists = [];
  3446. nodeList.forEach(function(_artists) {
  3447. _artists = _artists.textContent.trim();
  3448. if (_artists.startsWith('Record')) return;
  3449. Array.prototype.push.apply(artists,
  3450. splitArtists(_artists.replace(parenthesesStripper, '')).filter(artist => artist.length > 0));
  3451. });
  3452. return artists.filter(artist => artist.length > 0);
  3453. }
  3454. }); else if (url.toLowerCase().includes('discogs.com/') && /\/(release|master|artist)s?\/(\d+)\b/i.test(url)) {
  3455. if (RegExp.$1.toLowerCase() == 'artist') return Promise.reject('Discogs artists not parseable');
  3456. if (RegExp.$1.toLowerCase() == 'master') return Promise.reject('Discogs masters as source aren\'t supported, pick a specific release');
  3457. return queryDiscogsAPI('releases/' + RegExp.$2).then(function(release) {
  3458. if (prefs.diag_mode) console.debug('Discogs release', release.id, 'metadata received:', release);
  3459. const removeArtistNdx = /\s*\(\d+\)$/;
  3460. const editionTest = /^(?:.+?\s+Edition|Remaster(?:ed)|Remasterizado|Remasterisée|Reissue|.+?\s+Release|Enhanced|Promo)$/;
  3461. media = undefined;
  3462. identifiers.DISCOGS_ID = release.id;
  3463. var master = release.master_id ? queryDiscogsAPI('masters/' + release.master_id).then(function(master) {
  3464. if (prefs.diag_mode) console.debug('Discogs master', master.id, 'metadata received:', master);
  3465. return master;
  3466. }) : Promise.reject('master release not available');
  3467. var albumArtists = getArtists(release);
  3468. if (albumArtists[0].length > 0) {
  3469. artist = albumArtists[0].join(', ');
  3470. if (albumArtists[1].length > 0) artist += ' feat. ' + albumArtists[1].join(', ');
  3471. isVA = albumArtists[0].length == 1 && vaParser.test(albumArtists[0][0]);
  3472. } else isVA = true;
  3473. album = release.title;
  3474. var editions = [];
  3475. label = []; catalogue = [];
  3476. release.labels.forEach(function(it) {
  3477. if (it.entity_type != 1) return;
  3478. if (it.name && !/^Not\s+On\s+Label\b/i.test(it.name)) label.pushUniqueCaseless(it.name.replace(removeArtistNdx, ''));
  3479. if (it.catno) catalogue.pushUniqueCaseless(it.catno);
  3480. });
  3481. description = '';
  3482. if (release.companies && release.companies.length > 0) {
  3483. description = '[b]Companies, etc.[/b]\n';
  3484. let type_names = new Set(release.companies.map(it => it.entity_type_name));
  3485. type_names.forEach(function(type_name) {
  3486. description += '\n' + type_name + ' – ' + release.companies
  3487. .filter(it => it.entity_type_name == type_name)
  3488. .map(function(it) {
  3489. var result = '[url=' + discogsOrigin + '/label/' + it.id + ']' +
  3490. it.name.replace(removeArtistNdx, '') + '[/url]';
  3491. if (it.catno) result += ' – ' + it.catno;
  3492. return result;
  3493. })
  3494. .join(', ');
  3495. });
  3496. }
  3497. if (release.extraartists && release.extraartists.length > 0) {
  3498. if (description) description += '\n\n';
  3499. description += '[b]Credits[/b]\n';
  3500. let roles = new Set(release.extraartists.map(it => it.role));
  3501. roles.forEach(function(role) {
  3502. description += '\n' + role + ' – ' + release.extraartists
  3503. .filter(artist => artist.role == role)
  3504. .map(function(artist) {
  3505. var result = '[url=' + discogsOrigin + '/artist/' + artist.id + ']' +
  3506. (artist.anv || artist.name).replace(removeArtistNdx, '') + '[/url]';
  3507. if (artist.tracks) result += ' (tracks: ' + artist.tracks + ')';
  3508. return result;
  3509. })
  3510. .join(', ');
  3511. });
  3512. }
  3513. if (release.notes) {
  3514. if (description) description += '\n\n';
  3515. description += '[b]Notes[/b]\n\n' + release.notes.trim();
  3516. }
  3517. if (Array.isArray(release.identifiers) && release.identifiers.length > 0) {
  3518. if (description) description += '\n\n';
  3519. description += '[b]Barcode and Other Identifiers[/b]\n';
  3520. release.identifiers.forEach(function(it) {
  3521. description += '\n' + it.type;
  3522. if (it.description) description += ' (' + it.description + ')';
  3523. description += ': ' + it.value;
  3524. });
  3525. }
  3526. [
  3527. ['Single', 'Single'],
  3528. ['EP', 'EP'],
  3529. ['Compilation', 'Compilation'],
  3530. ['Soundtrack', 'Soundtrack'],
  3531. ].forEach(function(k) {
  3532. if (release.formats.every(it => Array.isArray(it.descriptions) && it.descriptions.includesCaseless(k[0]))) {
  3533. identifiers.RELEASETYPE = k[1];
  3534. }
  3535. });
  3536. release.identifiers.forEach(function(id) {
  3537. identifiers[id.type.toUpperCase().replace(/\s*\/\s*/g, '-').replace(/\W/g, '_')] = id.value;
  3538. });
  3539. release.formats.forEach(function(fmt) {
  3540. if (editionTest.test(fmt.text)) editions.push(fmt.text);
  3541. if (Array.isArray(fmt.descriptions)) fmt.descriptions.forEach(function(desc) {
  3542. if (editionTest.test(desc)) editions.push(desc);
  3543. });
  3544. if (media) return;
  3545. if (/\bFile\b/.test(fmt.name)) {
  3546. media = 'WEB';
  3547. if (['FLAC', 'WAV', 'AIF', 'AIFF', 'AIFC', 'PCM', 'ALAC', 'APE', 'WavPack']
  3548. .some(k => fmt.descriptions.includes(k))) {
  3549. encoding = 'lossless'; format = 'FLAC';
  3550. } else if (fmt.descriptions.includes('AAC')) {
  3551. encoding = 'lossy'; format = 'AAC'; bd = undefined;
  3552. if (/(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
  3553. } else if (fmt.descriptions.includes('MP3')) {
  3554. encoding = 'lossy'; format = 'MP3'; bd = undefined;
  3555. if (/\b(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
  3556. } else if (['DFF', 'DSD'].some(k => fmt.descriptions.includes(k))) {
  3557. encoding = 'lossless';
  3558. } else if (['AMR', 'MP2', 'ogg-vorbis', 'Opus', 'SHN', 'WMA'].some(k => fmt.descriptions.includes(k))) {
  3559. encoding = 'lossy';
  3560. }
  3561. } else media = dcFmtToGazelle(fmt.name) || undefined;
  3562. });
  3563. if (editions.length > 0) album += ' (' + editions.join(' / ') + ')';
  3564. totalTracks = release.tracklist.filter(track => track.type_.toLowerCase() == 'track').length;
  3565. return master.catch(function(reason) {
  3566. console.debug('Discogs master not received:', reason);
  3567. if (prefs.messages_verbosity >= 1) addMessage(reason, 'notice');
  3568. }).then(function(master) {
  3569. var tags = new TagManager();
  3570. if (release.genres) tags.add(...release.genres);
  3571. if (release.styles) tags.add(...release.styles);
  3572. if (master) {
  3573. if (master.genres) tags.add(...master.genres);
  3574. if (master.styles) tags.add(...master.styles);
  3575. }
  3576. const imageFilter = image => urlParser.test(image.resource_url || image.uri) && ['primary', 'front'].includes(image.type);
  3577. imgUrl = master && Array.isArray(master.images) ? master.images.filter(imageFilter) : [];
  3578. if (imgUrl.length <= 0 && Array.isArray(release.images)) imgUrl = release.images.filter(imageFilter);
  3579. release.tracklist.forEach(function(track) {
  3580. switch (track.type_.toLowerCase()) {
  3581. case 'heading':
  3582. discSubtitle = track.title;
  3583. break;
  3584. case 'track': {
  3585. trackIdentifiers = {};
  3586. if (/^([a-zA-Z]+)?(\d+)-(\w+)$/.test(track.position)) {
  3587. if (RegExp.$1) trackIdentifiers.VOL_MEDIA = RegExp.$1;
  3588. discNumber = RegExp.$2;
  3589. trackNumber = RegExp.$3;
  3590. } else {
  3591. discNumber = undefined;
  3592. trackNumber = track.position;
  3593. }
  3594. let trackArtists = getArtists(track);
  3595. if (trackArtists[0].length > 0 && !trackArtists[0].equalCaselessTo(albumArtists[0])
  3596. || trackArtists[1].length > 0 && !trackArtists[1].equalCaselessTo(albumArtists[1])) {
  3597. trackArtist = (trackArtists[0].length > 0 ? trackArtists : albumArtists)[0].join(', ');
  3598. if (trackArtists[1].length > 0) trackArtist += ' feat. ' + trackArtists[1].join(', ');
  3599. } else trackArtist = undefined;
  3600. let performer = Array.isArray(track.extraartists) && track.extraartists
  3601. .map(artist => (artist.anv || artist.name).replace(removeArtistNdx, ''))
  3602. .filter(function(artist) {
  3603. return !albumArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
  3604. && !trackArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
  3605. });
  3606. tracks.push({
  3607. artist: isVA ? VA : artist,
  3608. //artists: !isVA ? albumArtists[0] : undefined,
  3609. album: album,
  3610. album_year: master ? master.year : undefined,
  3611. release_date: release.released,
  3612. label: label.join(' / '),
  3613. catalog: catalogue.join(' / '),
  3614. country: release.country,
  3615. encoding: encoding,
  3616. codec: format,
  3617. bitrate: bitrate,
  3618. bd: bd,
  3619. media: media,
  3620. genre: tags.toString(),
  3621. discnumber: discNumber,
  3622. totaldiscs: release.format_quantity,
  3623. discsubtitle: discSubtitle,
  3624. tracknumber: trackNumber,
  3625. totaltracks: totalTracks,
  3626. title: track.title,
  3627. track_artist: trackArtist,
  3628. performer: Array.isArray(performer) && performer.join('; ') || undefined,
  3629. composers: stringyfyRole(3),
  3630. conductors: stringyfyRole(4),
  3631. remixers: stringyfyRole(2),
  3632. compilers: stringyfyRole(5),
  3633. producers: stringyfyRole(6),
  3634. duration: timeStringToTime(track.duration),
  3635. description: description,
  3636. identifiers: mergeIds(),
  3637. cover_url: imgUrl.length > 0 ? imgUrl[0].resource_url || imgUrl[0].uri : undefined,
  3638. });
  3639.  
  3640. function stringyfyRole(ndx) {
  3641. var arr = (Array.isArray(trackArtists[ndx]) && trackArtists[ndx].length > 0 ? trackArtists : albumArtists)[ndx];
  3642. return arr.length > 0 ? arr : undefined;
  3643. }
  3644. }
  3645. }
  3646. });
  3647. return tracks;
  3648. });
  3649.  
  3650. function getArtists(root) {
  3651. function filterArtists(rx, anv = true) {
  3652. return Array.isArray(root.extraartists) && rx instanceof RegExp ?
  3653. root.extraartists.filter(it => rx.test(it.role))
  3654. .map(it => (anv && it.anv || it.name || '').replace(removeArtistNdx, '')) : [];
  3655. }
  3656. var artists = [];
  3657. for (var ndx = 0; ndx < 7; ++ndx) artists[ndx] = [];
  3658. ndx = 0;
  3659. if (root.artists) root.artists.forEach(function(it) {
  3660. artists[ndx].push((it.anv || it.name).replace(removeArtistNdx, ''));
  3661. if (/^feat/i.test(it.join)) ndx = 1;
  3662. });
  3663. return [
  3664. artists[0],
  3665. artists[1].concat(filterArtists(/^(?:featuring)$/i)),
  3666. artists[2].concat(filterArtists(/\b(?:Remixed[\s\-]By|Remixer)\b/i)),
  3667. artists[3].concat(filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, false)),
  3668. artists[4].concat(filterArtists(/\b(?:Conducted[\s\-]By|Conductor)\b/i)),
  3669. artists[5].concat(filterArtists(/\b(?:Compiled[\s\-]By|Compiler)\b/i)),
  3670. artists[6].concat(filterArtists(/\b(?:Produced[\s\-]By|Producer)\b/i)),
  3671. // filter off from performers
  3672. filterArtists(/\b(?:(?:Mixed)[\s\-]By|Mixer)\b/i),
  3673. filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, true),
  3674. ];
  3675. }
  3676. });
  3677. } else if (url.toLowerCase().includes('supraphonline.cz/')) return globalFetch(url.replace(/\?.*$/, '')).then(function(response) {
  3678. const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
  3679. var ndx, conductor = [], origin = new URL(response.finalUrl).origin;
  3680. genres = undefined; artist = [];
  3681. response.document.querySelectorAll('h2.album-artist > a').forEach(function(it) {
  3682. artist.pushUnique(it.title);
  3683. });
  3684. if (artist.length == 0 && (ref = response.document.querySelector('h2.album-artist[title]')) != null) {
  3685. isVA = vaParser.test(ref.title);
  3686. }
  3687. ref = response.document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]');
  3688. if (ref != null && vaParser.test(ref.content)) isVA = true;
  3689. if (isVA) artist = [];
  3690. if ((ref = response.document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
  3691. if ((ref = response.document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  3692. if ((ref = response.document.querySelector('meta[itemprop="genre"]')) != null) genres = ref.content;
  3693. if ((ref = response.document.querySelector('li.album-version > div.selected > div')) != null) {
  3694. if (/\b(?:MP3)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossy'; format = 'MP3'; }
  3695. if (/\b(?:FLAC)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 16; }
  3696. if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 24; }
  3697. if (/\b(?:CD)\b/.test(ref.textContent)) { media = 'CD'; }
  3698. if (/\b(?:LP)\b/.test(ref.textContent)) { media = 'Vinyl'; }
  3699. }
  3700. response.document.querySelectorAll('ul.summary > li').forEach(function(it) {
  3701. if (it.childElementCount <= 0) return;
  3702. if (it.firstElementChild.textContent.includes('Nosič')) media = it.lastChild.textContent.trim();
  3703. if (it.firstElementChild.textContent.includes('Datum vydání')) releaseDate = normalizeDate(it.lastChild.textContent);
  3704. if (it.firstElementChild.textContent.includes('První vydání')) albumYear = extractYear(it.lastChild.data);
  3705. //if (it.firstElementChild.textContent.includes('Žánr')) genre = it.lastChild.textContent.trim();
  3706. if (it.firstElementChild.textContent.includes('Vydavatel')) label = it.lastChild.textContent.trim();
  3707. if (it.firstElementChild.textContent.includes('Katalogové číslo')) catalogue = it.lastChild.textContent.trim();
  3708. if (it.firstElementChild.textContent.includes('Formát')) {
  3709. if (/\b(?:FLAC|WAV|AIFF?)\b/.test(it.lastChild.textContent)) { encoding = 'lossless'; format = 'FLAC'; }
  3710. if (/\b(\d+)[\-\s]?bits?\b/i.test(it.lastChild.textContent)) bd = parseInt(RegExp.$1);
  3711. if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(it.lastChild.textContent)) sr = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
  3712. }
  3713. if (it.firstElementChild.textContent.includes('Celková stopáž')) totalTime = timeStringToTime(it.lastChild.textContent.trim());
  3714. if (copyrightParser.test(it.firstElementChild.textContent) && !albumYear) albumYear = extractYear(it.lastChild.data);
  3715. });
  3716. const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
  3717. artists = [];
  3718. for (i = 0; i < 4; ++i) artists[i] = {};
  3719. response.document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
  3720. if ((ref = it.querySelector('h3')) != null) {
  3721. ndx = undefined;
  3722. creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
  3723. } else {
  3724. if (typeof ndx != 'number') return;
  3725. let role;
  3726. if (ndx == 2) role = 'ensemble';
  3727. else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
  3728. if ((ref = it.querySelector('a')) != null) {
  3729. if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
  3730. var href = new URL(ref.href);
  3731. artists[ndx][role].pushUnique([ref.textContent.trim(), origin + href.pathname]);
  3732. }
  3733. }
  3734. });
  3735. getDescription(response, 'div[itemprop="description"] p', true);
  3736. composer = [];
  3737. var performers = [], DJs = [];
  3738. function dumpArtist(ndx, role) {
  3739. if (!role || role == 'undefined') return;
  3740. if (description.length > 0) description += '\n' ;
  3741. description += '[color=#9576b1]' + role + '[/color] – ';
  3742. //description += artists[ndx][role].map(artist => '[artist]' + artist[0] + '[/artist]').join(', ');
  3743. description += artists[ndx][role].map(artist => '[url=' + artist[1] + ']' + artist[0] + '[/url]').join(', ');
  3744. }
  3745. for (i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
  3746. var a = artists[i][role].map(a => a[0]);
  3747. artist.pushUnique(...a);
  3748. (['conductor', 'choirmaster'].includes(role) ? conductor : role == 'DJ' ? DJs : performers).pushUnique(...a);
  3749. if (i != 2) dumpArtist(i, role);
  3750. });
  3751. Object.keys(artists[0]).forEach(function(role) { // composers
  3752. composer.pushUnique(...artists[0][role].map(it => it[0])
  3753. .filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
  3754. dumpArtist(0, role);
  3755. });
  3756. Object.keys(artists[3]).forEach(role => { dumpArtist(3, role) }); // ADC & mastering
  3757. ref = response.document.querySelector('meta[itemprop="image"]');
  3758. if (ref != null)imgUrl = ref.content.replace(/\?.*$/, '');
  3759. var promises = [];
  3760. response.document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(row) {
  3761. promises.push(row.id && (ref = row.querySelector('td > a.trackdetail')) != null ?
  3762. globalFetch(origin + ref.pathname + ref.search, { context: parseInt(row.id.replace(/^track-/i, '')) })
  3763. .then(function(response) {
  3764. var track = response.document.getElementById('track-' + response.context);
  3765. if (track == null) return Promise.reject('Track detail not located');
  3766. return [track, response.document.querySelector('div[data-swap="trackdetail-' +
  3767. response.context + '"] > div > div.row')];
  3768. })
  3769. : Promise.resolve([row, null]));
  3770. });
  3771. return Promise.all(promises).then(function(rows) {
  3772. rows.forEach(function(tr) {
  3773. if (!(tr[0] instanceof HTMLElement)) throw new Error('Assertion failed: tr[0] != HTMLElement');
  3774. if (tr[0].id && tr[0].classList.contains('track')) {
  3775. tr[2] = [];
  3776. for (i = 0; i < 8; ++i) tr[2][i] = [];
  3777. if (!(tr[1] instanceof HTMLElement)) return;
  3778. tr[1].querySelectorAll('div[class]:nth-of-type(2) > ul > li > span').forEach(function(li) {
  3779. function oneOf(...arr) { return arr.some(role => key == role) }
  3780. var key = translateRole(li);
  3781. var val = li.nextElementSibling.textContent.trim();
  3782. if (pseudoArtistParsers.some(rx => rx.test(val))) return;
  3783. if (key.startsWith('remix')) {
  3784. tr[2][2].pushUnique(val);
  3785. } else if (oneOf('music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto', 'music improvisation', 'author')) {
  3786. tr[2][3].pushUnique(val);
  3787. } else if (oneOf('conductor', 'choirmaster')) {
  3788. tr[2][4].pushUnique(val);
  3789. } else if (key == 'DJ') {
  3790. tr[2][5].pushUnique(val);
  3791. } else if (key == 'produced by') {
  3792. tr[2][6].pushUnique(val);
  3793. } else if (key == 'recorded by') {
  3794. } else {
  3795. tr[2][7].pushUnique(val);
  3796. }
  3797. });
  3798. }
  3799. });
  3800. var guests = rows.filter(tr => tr.length >= 3).map(it => it[2][7])
  3801. .reduce((acc, trpf) => trpf.filter(trpf => acc.includes(trpf)))
  3802. .filter(it => !artist.includes(it));
  3803. rows.forEach(function(tr) {
  3804. if (tr[0].classList.contains('cd-header')) {
  3805. discNumber = /\b\d+\b/.test(tr[0].querySelector('h3').firstChild.data.trim())
  3806. && parseInt(RegExp.lastMatch) || undefined;
  3807. }
  3808. if (tr[0].classList.contains('song-header')) discSubtitle = tr[0].firstElementChild.title.trim() || undefined;
  3809. if (tr[0].id && tr[0].classList.contains('track')) {
  3810. var copyright, trackGenre, trackYear, recordPlace, recordDate, trackIdentifiers = {};
  3811. if (/^track-(\d+)$/i.test(tr[0].id)) trackIdentifiers.TRACK_ID = RegExp.$1;
  3812. if (tr[1] instanceof HTMLElement) {
  3813. tr[1].querySelectorAll('div[class]:nth-of-type(1) > ul > li > span').forEach(function(li) {
  3814. if (li.textContent.startsWith('Nahrávka dokončena')) {
  3815. trackIdentifiers.RECYEAR = extractYear(recordDate = li.nextSibling.data.trim());
  3816. }
  3817. if (li.textContent.startsWith('Místo nahrání')) {
  3818. recordPlace = li.nextSibling.data.trim();
  3819. }
  3820. if (li.textContent.startsWith('Rok prvního vydání')) {
  3821. trackIdentifiers.PUBYEAR = (trackYear = parseInt(li.nextSibling.data));
  3822. }
  3823. //if (copyrightParser.test(li.textContent)) copyright = li.nextSibling.data.trim();
  3824. if (li.textContent.startsWith('Žánr')) trackGenre = li.nextSibling.data.trim();
  3825. });
  3826. }
  3827. tracks.push({
  3828. artist: isVA ? VA : undefined,
  3829. artists: !isVA ? artist : undefined,
  3830. album: album,
  3831. album_year: /*trackYear || */albumYear || undefined,
  3832. release_date: releaseDate,
  3833. label: label,
  3834. catalog: catalogue,
  3835. encoding: encoding,
  3836. codec: format,
  3837. bd: bd,
  3838. sr: sr || undefined,
  3839. media: media,
  3840. genre: translateGenre(genres) + ' | ' + translateGenre(trackGenre),
  3841. discnumber: discNumber,
  3842. totaldiscs: totalDiscs,
  3843. discsubtitle: discSubtitle,
  3844. tracknumber: /^\s*(\d+)\.?\s*$/.test(tr[0].firstElementChild.firstChild.textContent) ?
  3845. parseInt(RegExp.$1) || RegExp.$1 : undefined,
  3846. totaltracks: totalTracks,
  3847. title: tr[0].querySelector('meta[itemprop="name"]').content,
  3848. track_artists: tr[2][0].length > 0 && (isVA || !tr[2][0].equalCaselessTo(artist)) ? tr[2][0] : undefined,
  3849. performers: tr[2][7].length > 0 ? tr[2][7] : performers,
  3850. composers: tr[2][3].length > 0 ? tr[2][3] : composer,
  3851. conductors: tr[2][4].length > 0 ? tr[2][4] : conductor,
  3852. remixers: tr[2][2],
  3853. compilers: tr[2][5].length > 0 ? tr[2][5] : DJs,
  3854. producers: tr[2][6],
  3855. duration: durationFromMeta(tr[0]),
  3856. url: response.finalUrl,
  3857. description: description,
  3858. identifiers: mergeIds(),
  3859. cover_url: imgUrl,
  3860. });
  3861. }
  3862. });
  3863. return tracks;
  3864. });
  3865.  
  3866. function translateGenre(genre) {
  3867. if (!genre || typeof genre != 'string') return undefined;
  3868. [
  3869. ['Orchestrální hudba', 'Orchestral Music'],
  3870. ['Komorní hudba', 'Chamber Music'],
  3871. ['Vokální', 'Classical, Vocal'],
  3872. ['Klasická hudba', 'Classical'],
  3873. ['Melodram', 'Classical, Melodram'],
  3874. ['Symfonie', 'Symphony'],
  3875. ['Vánoční hudba', 'Christmas Music'],
  3876. [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
  3877. ['Dechová hudba', 'Brass Music'],
  3878. ['Elektronika', 'Electronic'],
  3879. ['Folklor', 'Folclore, World Music'],
  3880. ['Instrumentální hudba', 'Instrumental'],
  3881. ['Latinské rytmy', 'Latin'],
  3882. ['Meditační hudba', 'Meditative'],
  3883. ['Vojenská hudba', 'Military Music'],
  3884. ['Pro děti', 'Children'],
  3885. ['Pro dospělé', 'Adult'],
  3886. ['Mluvené slovo', 'Spoken Word'],
  3887. ['Audiokniha', 'audiobook'],
  3888. ['Humor', 'humour'],
  3889. ['Pohádka', 'Fairy-Tale'],
  3890. ].forEach(function(subst) {
  3891. if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
  3892. || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
  3893. });
  3894. return genre;
  3895. }
  3896. function translateRole(elem) {
  3897. if (!(elem instanceof HTMLElement)) return undefined;
  3898. var role = elem.textContent.trim().toLowerCase().replace(/\s*:.*$/, '');
  3899. [
  3900. [/\b(?:klavír)\b/, 'piano'],
  3901. [/\b(?:housle)\b/, 'violin'],
  3902. [/\b(?:varhany)\b/, 'organ'],
  3903. [/\b(?:cembalo)\b/, 'harpsichord'],
  3904. [/\b(?:trubka)\b/, 'trumpet'],
  3905. [/\b(?:soprán)\b/, 'soprano'],
  3906. [/\b(?:alt)\b/, 'alto'],
  3907. [/\b(?:baryton)\b/, 'baritone'],
  3908. [/\b(?:bas)\b/, 'basso'],
  3909. [/\b(?:syntezátor)\b/, 'synthesizer'],
  3910. [/\b(?:zpěv)\b/, 'vocals'],
  3911. [/^(?:čte|četba)$/, 'narration'],
  3912. ['vypravuje', 'narration'],
  3913. ['komentář', 'commentary'],
  3914. ['hovoří', 'spoken by'],
  3915. ['hovoří a zpívá', 'speaks and sings'],
  3916. ['improvizace', 'improvisation'],
  3917. ['hudební těleso', 'ensemble'],
  3918. ['hudba', 'music'],
  3919. ['text', 'lyrics'],
  3920. ['hudba+text', 'music+lyrics'],
  3921. ['původní text', 'original lyrics'],
  3922. ['český text', 'czech lyrics'],
  3923. ['hudební improvizace', 'music improvisation'],
  3924. ['autor', 'author'],
  3925. ['účinkuje', 'participating'],
  3926. ['řídí', 'conductor'],
  3927. ['dirigent', 'conductor'],
  3928. ['sbormistr', 'choirmaster'],
  3929. ['produkce', 'produced by'],
  3930. ['nahrál', 'recorded by'],
  3931. ['digitální přepis', 'A/D transfer'],
  3932. ].forEach(function(subst) {
  3933. if (typeof subst[0] == 'string' && role.toLowerCase() == subst[0].toLowerCase()
  3934. || subst[0] instanceof RegExp && subst[0].test(role)) role = role.replace(subst[0], subst[1]);
  3935. });
  3936. return role;
  3937. }
  3938. }); else if (url.toLowerCase().includes('bontonland.cz/')) return globalFetch(url).then(function(response) {
  3939. ref = response.document.querySelector('div#detailheader > h1');
  3940. if (ref != null && /^(.*?)\s*:\s*(.*)$/.test(ref.textContent.trim())) {
  3941. artist = RegExp.$1;
  3942. isVA = vaParser.test(artist);
  3943. album = RegExp.$2;
  3944. }
  3945. media = 'CD';
  3946. response.document.querySelectorAll('table > tbody > tr > td.nazevparametru').forEach(function(it) {
  3947. if (it.textContent.includes('Datum vydání')) {
  3948. releaseDate = normalizeDate(it.nextElementSibling.textContent);
  3949. albumYear = extractYear(it.nextElementSibling.textContent);
  3950. } else if (it.textContent.includes('Nosič / počet')) {
  3951. if (/^(.*?)\s*\/\s*(.*)$/.test(it.nextElementSibling.textContent)) {
  3952. media = RegExp.$1;
  3953. totalDiscs = RegExp.$2;
  3954. }
  3955. } else if (it.textContent.includes('Interpret')) {
  3956. artist = it.nextElementSibling.textContent.trim();
  3957. } else if (it.textContent.includes('EAN')) {
  3958. identifiers.BARCODE = it.nextElementSibling.textContent.trim();
  3959. }
  3960. });
  3961. getDescription(response, 'div#detailtabpopis > div[class^="pravy"] > div > p:not(:last-of-type)', true);
  3962. if ((ref = response.document.querySelector('a.detailzoom')) != null) imgUrl = ref.href;
  3963. if ((ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > ol')) != null) {
  3964. return Array.from(ref.querySelectorAll('li')).map(function(track, ndx, arr) {
  3965. title = track.innerText.trim();
  3966. duration = undefined;
  3967. if (/^(.*?)\s+\(((?:\d+:)?\d+:\d+)\)$/.test(title) || /^(.*?)\s+\(((?:\d+:)?\d+:\d+)\)$/.test(title)) {
  3968. title = RegExp.$1;
  3969. duration = timeStringToTime(RegExp.$2);
  3970. }
  3971. return {
  3972. artist: isVA ? VA : artist,
  3973. album: album,
  3974. //album_year: extractYear(releaseDate),
  3975. release_date: releaseDate,
  3976. label: label,
  3977. media: media,
  3978. tracknumber: ndx + 1,
  3979. totaltracks: arr.length,
  3980. title: title,
  3981. duration: duration,
  3982. url: response.finalUrl.replace(/\?.*$/, ''),
  3983. description: description,
  3984. identifiers: mergeIds(),
  3985. cover_url: imgUrl,
  3986. };
  3987. });
  3988. } else if ((ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > p:last-of-type')) != null) {
  3989. const plParser = /^(\d+)(?:\s*[\/\.\-\:\)])?\s+(.*?)(?:\s+((?:(?:\d+:)?\d+:)?\d+))?$/;
  3990. var trackList = html2php(ref, response.finalUrl).trim().split(/[\r\n]+/)
  3991. .filter(it => plParser.test(it.trim())).map(it => plParser.exec(it.trim()));
  3992. return Array.from(trackList).map(track => ({
  3993. artist: isVA ? VA : artist,
  3994. album: album,
  3995. //album_year: extractYear(releaseDate),
  3996. release_date: releaseDate,
  3997. label: label,
  3998. media: media,
  3999. tracknumber: track[1],
  4000. totaltracks: trackList.length,
  4001. title: track[2],
  4002. duration: timeStringToTime(track[3]),
  4003. url: response.finalUrl.replace(/\?.*$/, ''),
  4004. description: description,
  4005. identifiers: mergeIds(),
  4006. cover_url: imgUrl,
  4007. }));
  4008. } else throw 'Playlist could not be located';
  4009. }); else if (url.toLowerCase().includes('nativedsd.com/')) return globalFetch(url).then(function(response) {
  4010. identifiers.COMPOSEREMPHASIS = 1;
  4011. artist = (ref = response.document.querySelector('div.the-content > header > h2')) != null ?
  4012. ref.firstChild.data.trim() : undefined;
  4013. isVA = !artist || vaParser.test(artist);
  4014. if ((ref = response.document.querySelector('div.the-content > header > h1')) != null) album = ref.firstChild.data.trim();
  4015. if ((ref = response.document.querySelector('div.the-content > header > h3')) != null) composer = ref.firstChild.data.trim();
  4016. if ((ref = response.document.querySelector('div.the-content > header > h1 > small')) != null)
  4017. albumYear = extractYear(ref.firstChild.data);
  4018. ref = response.document.querySelector('div#breadcrumbs > div[class] > a:nth-of-type(2)');
  4019. if (ref != null) label = ref.firstChild.data.trim();
  4020. if (label == 'Albums') label = undefined;
  4021. if ((ref = response.document.querySelector('h2#sku')) != null) {
  4022. if (/^\s*(?:Catalog\sNumber):\s*(.*?)\s*$/im.test(ref.textContent)) catalogue = RegExp.$1;
  4023. if (/^\s*(?:Released\son\sNativeDSD):\s*(.*?)\s*$/im.test(ref.textContent)) releaseDate = RegExp.$1;
  4024. if (/^\s*(?:ID):\s*(.*?)\s*$/im.test(ref.textContent)) identifiers.NATIVEDSD_ID = RegExp.$1;
  4025. }
  4026. identifiers.ORIGINALFORMAT = 'DSD';
  4027. getDescription(response, 'div.the-content > div.entry > p', false);
  4028. if ((ref = response.document.querySelector('div#repertoire > div > p')) != null) {
  4029. let repertoire = html2php(ref, url);
  4030. if (description) description += '\n\n';
  4031. let ndx = repertoire.indexOf('\n[b]Track');
  4032. description += (ndx >= 0 ? repertoire.slice(0, ndx) : repertoire).trim().flatten();
  4033. }
  4034. ref = response.document.querySelectorAll('div#techspecs > table > tbody > tr');
  4035. if (ref.length > 0) {
  4036. if (description) description += '\n\n';
  4037. description += '[b][u]Tech specs[/u][/b]';
  4038. ref.forEach(function(it) {
  4039. description += '\n[b]'.concat(it.children[0].textContent.trim(), '[/b] ', it.children[1].textContent.trim());
  4040. });
  4041. }
  4042. if ((ref = response.document.querySelector('a#album-cover')) != null) imgUrl = ref.href;
  4043. trs = response.document.querySelectorAll('div#track-list > table > tbody > tr[id^="track"]');
  4044. return Array.from(trs).map(function(tr) {
  4045. title = undefined;
  4046. trackIdentifiers = { TRACK_ID: tr.id.replace(/^track-/i, '') };
  4047. var trackComposer;
  4048. if ((ref = tr.children[1]) != null) {
  4049. title = ref.firstChild.textContent.trim();
  4050. trackComposer = ref.childNodes[2] && ref.childNodes[2].textContent.trim() || undefined;
  4051. }
  4052. return {
  4053. artist: isVA ? VA : artist,
  4054. album: album,
  4055. album_year: albumYear,
  4056. release_date: releaseDate,
  4057. label: label,
  4058. catalog: catalogue,
  4059. encoding: 'lossless',
  4060. codec: 'FLAC',
  4061. bd: 24,
  4062. sr: 88200,
  4063. media: media,
  4064. genre: genres.join('; '), // 'Jazz'
  4065. discnumber: discNumber,
  4066. totaldiscs: totalDiscs,
  4067. discsubtitle: discSubtitle,
  4068. tracknumber: (ref = tr.firstElementChild.firstElementChild) != null ?
  4069. parseInt(ref.firstChild.data.trim().replace(/\..*$/, '')) : undefined,
  4070. totaltracks: trs.length,
  4071. title: title,
  4072. composer: trackComposer || composer,
  4073. duration: (ref = tr.children[2]) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  4074. url: response.finalUrl,
  4075. description: description,
  4076. identifiers: mergeIds(),
  4077. cover_url: imgUrl,
  4078. };
  4079. });
  4080. });/* else if (url.toLowerCase().includes('junodownload.com/') && /\/([\d\-]+)\/?$/.test(new URL(url).pathname)) {
  4081. let productKey = RegExp.$1;
  4082. return globalFetch('https://www.junodownload.com/api/1.2/playlist/getplaylistdetails/?product_key='.concat(productKey), {
  4083. responseType: 'xml',
  4084. }).then(response => Array.from(response.document.querySelectorAll('playlist > trackList > track')).map(function(track, index, trackList) {
  4085. artist = Array.from(track.querySelectorAll('extension > release_artists > artist > name'))
  4086. .map(artist => artist.textContent.trim());
  4087. isVA = artist.length == 1 && vaParser.test(artist[0]);
  4088. trackArtist = Array.from(track.querySelectorAll('extension > artists > artist > name'))
  4089. .map(artist => artist.textContent.trim());
  4090. trackArtist = isVA || !trackArtist.equalCaselessTo(artist) ? joinArtists(trackArtist) : undefined;
  4091. title = getValue('extension > track_title');
  4092. if (getValue('extension > mix_title')) title += ' (' + getValue('extension > mix_title') + ')';
  4093. return {
  4094. artist: isVA ? VA : artist.join(', '),
  4095. album: getValue('album'),
  4096. release_date: getValue('extension > relDate'),
  4097. label: getValue('extension > label > name'),
  4098. catalog: getValue('extension > catNumber'),
  4099. media: media,
  4100. genre: getValue('extension > genre'),
  4101. tracknumber: parseInt(getValue('trackNum')),
  4102. totaltracks: trackList.length,
  4103. title: getValue('extension > track_title'),
  4104. track_artist: trackArtist,
  4105. duration: parseInt(getValue('extension > length')) || undefined,
  4106. description: getValue('extension > rating_comment'),
  4107. identifiers: { JUNODOWNLOAD_ID: productKey },
  4108. cover_url: getValue('image'),
  4109. };
  4110.  
  4111. function getValue(selector) {
  4112. var node = track.querySelector(selector);
  4113. return node != null ? node.textContent.trim() : undefined;
  4114. }
  4115. }));
  4116. }*/ else if (url.toLowerCase().includes('junodownload.com/')) return globalFetch(url).then(function(response) {
  4117. if (/'id':'([\d\-]+)'/.test(response.responseText) || /\/([\d\-]+)\/?$/.test(new URL(response.finalUrl).pathname)) {
  4118. identifiers.JUNODOWNLOAD_ID = RegExp.$1;
  4119. var metaData = globalFetch('https://www.junodownload.com/api/1.2/playlist/getplaylistdetails/?product_key='
  4120. .concat(identifiers.JUNODOWNLOAD_ID), { responseType: 'xml' }).then(function(response) {
  4121. return Array.from(response.document.querySelectorAll('playlist > trackList > track'));
  4122. });
  4123. } else metaData = Promise.reject('No Id');
  4124. var productArtist;
  4125. if ((ref = response.document.querySelectorAll('div.breadcrumb_text > span:not([class])')).length == 4) {
  4126. artist = Array.from(ref[ref.length - 1].querySelectorAll('a')).map(elem => elem.textContent.trim());
  4127. productArtist = ref[ref.length - 1].textContent.trim();
  4128. } else if ((ref = response.document.querySelector('h2.product-artist')) != null) {
  4129. artist = Array.from(ref.querySelectorAll('a')).map(elem => elem.textContent.trim().titleCase());
  4130. productArtist = ref.textContent.trim().titleCase();
  4131. }
  4132. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4133. if ((ref = response.document.querySelector('meta[itemprop="name"]')) != null) album = ref.content.trim();
  4134. if ((ref = response.document.querySelector('meta[itemprop="author"]')) != null) label = ref.content.trim();
  4135. if ((ref = response.document.querySelector('span[itemprop="datePublished"]')) != null)
  4136. releaseDate = ref.firstChild.data.trim();
  4137. response.document.querySelectorAll('div.mb-3 > strong').forEach(function(it) {
  4138. if (it.textContent.startsWith('Genre')) {
  4139. ref = it;
  4140. while ((ref = ref.nextElementSibling) != null && ref.nodeName == 'A') genres.push(ref.textContent.trim());
  4141. } else if (it.textContent.startsWith('Cat')) {
  4142. if ((ref = it.nextSibling) != null && ref.nodeType == Node.TEXT_NODE) catalogue = ref.wholeText;
  4143. }
  4144. });
  4145. getDescription(response, 'div[itemprop="review"]');
  4146. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  4147. trs = response.document.querySelectorAll('div.product-tracklist > div[itemprop="track"]');
  4148. return Array.from(trs).map(function(tr) {
  4149. trackIdentifiers = { BPM: tr.children[2].textContent.trim() };
  4150. trackNumber = undefined;
  4151. tr.querySelector('div.track-title').childNodes.forEach(function(n) {
  4152. if (trackNumber || n.nodeType != Node.TEXT_NODE) return;
  4153. trackNumber = n.data.trim().replace(/\s*\..*$/, '');
  4154. });
  4155. trackArtist = (ref = tr.querySelector('meta[itemprop="byArtist"]')) != null ? ref.content : undefined;
  4156. title = (ref = tr.querySelector('span[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  4157. if (title && trackArtist && title.startsWith(trackArtist + ' - ')) title = title.slice(trackArtist.length + 3);
  4158. return {
  4159. artist: isVA ? VA : productArtist,
  4160. artists: !isVA ? artist : undefined,
  4161. album: album,
  4162. album_year: extractYear(releaseDate),
  4163. release_date: releaseDate,
  4164. label: label,
  4165. catalog: catalogue,
  4166. media: media,
  4167. genre: genres.join('; '),
  4168. discnumber: discNumber,
  4169. totaldiscs: totalDiscs,
  4170. discsubtitle: discSubtitle,
  4171. tracknumber: trackNumber,
  4172. totaltracks: trs.length,
  4173. title: title,
  4174. track_artist: trackArtist && (isVA || trackArtist.toLowerCase() != productArtist.toLowerCase()) ? trackArtist : undefined,
  4175. duration: durationFromMeta(tr),
  4176. url: !identifiers.JUNODOWNLOAD_ID ? response.finalUrl : undefined,
  4177. description: description,
  4178. identifiers: mergeIds(),
  4179. cover_url: imgUrl,
  4180. };
  4181. });
  4182. }); else if (/\/\/(?:www\.)?(?:hdtracks)(?:\.\w+)+\//i.test(url)) return queryHdtracksApi(url).then(function(album) {
  4183. if (prefs.diag_mode) console.debug('HDtracks album metadata loaded:', album);
  4184. identifiers.HDTRACKS_ID = album.id || album.productId;
  4185. if (album.upc) identifiers.BARCODE = album.upc;
  4186. if (album.parentalWarning == 'NotExplicit') identifiers.EXPLICIT = 0;
  4187. else if (album.parentalWarning == 'Explicit') identifiers.EXPLICIT = 1;
  4188. isVA = album.artists.length <= 0 || vaParser.test(album.mainArtist);
  4189. var guests = [], composers = [], producers = [];
  4190. if (album.credits) album.credits.split(/\r?\n/).forEach(function(credit) {
  4191. if (!/^(.*)\s*:\s*(.*)$/.test(credit)) return;
  4192. var role = RegExp.$1, name = RegExp.$2;
  4193. if (role == 'Artist' && name.toLowerCase() != album.mainArtist.toLowerCase()) guests.pushUniqueCaseless(name);
  4194. else if (role == 'Composer') composers.pushUniqueCaseless(name);
  4195. else if (/\b(?:Producer)$/.test(role)) producers.pushUniqueCaseless(name);
  4196. });
  4197. var albumGuests = guests.length > 0 ? ' feat. '.concat(joinArtists(guests)) : '';
  4198. return Promise.all(album.trackIds.map(trackId => queryHdtracksApi(trackId, 'track').catch(function(reason) {
  4199. console.warn('Fetching details from HDtracks failed at least for one track:', reason);
  4200. return album.tracks;
  4201. }).then(function(track) {
  4202. if (prefs.diag_mode) console.debug('HDtracks track metadata loaded:', track);
  4203. trackIdentifiers = {
  4204. ISRC: track.isrc,
  4205. TRACK_ID: track.id,
  4206. MD5: track.md5,
  4207. };
  4208. if (track.upc) trackIdentifiers.BARCODE = track.upc;
  4209. var trackGuests = [], trackComposers = [], trackProducers = [];
  4210. if (track.credits) track.credits.split(/\r?\n/).forEach(function(credit) {
  4211. if (!/^(.*)\s*:\s*(.*)$/.test(credit)) return;
  4212. var role = RegExp.$1, name = RegExp.$2;
  4213. if (role == 'Artist' && (!track.mainArtist || name.toLowerCase() != track.mainArtist.toLowerCase()))
  4214. trackGuests.pushUniqueCaseless(name);
  4215. else if (role == 'Composer') trackComposers.pushUniqueCaseless(name);
  4216. else if (/\b(?:Producer)$/.test(role)) trackProducers.pushUniqueCaseless(name);
  4217. });
  4218. if (track.mainArtist && !isVA && track.mainArtist.toLowerCase() == album.mainArtist.toLowerCase()
  4219. && trackGuests.equalCaselessTo(guests)) track.mainArtist = undefined;
  4220. if (track.mainArtist && trackGuests.length > 0) track.mainArtist += ' feat. '.concat(joinArtists(trackGuests));
  4221. return {
  4222. artist: album.mainArtist.concat(albumGuests),
  4223. artists: album.artists,
  4224. featuring_artists: guests,
  4225. album: album.name,
  4226. release_date: track.release || album.release,
  4227. album_year: album.originalRelease ? extractYear(album.originalRelease) : undefined,
  4228. label: track.label || album.label,
  4229. distributor: track.distributor || album.distributor,
  4230. media: media,
  4231. sr: track.rate || album.rate || undefined,
  4232. bd: track.resolution || album.resolution || undefined,
  4233. genre: track.genre || album.genre,
  4234. totaldiscs: album.discs,
  4235. tracknumber: track.index,
  4236. totaltracks: album.tracksCount, //album.tracks.length
  4237. composers: trackComposers.length > 0 ? trackComposers : composers,
  4238. //producers: trackProducers.length > 0 ? trackProducers : producers,
  4239. title: track.name,
  4240. track_artist: !isVA && track.mainArtist != album.mainArtist ? track.mainArtist : undefined,
  4241. duration: track.duration,
  4242. url: !identifiers.HDTRACKS_ID ? response.finalUrl : undefined,
  4243. identifiers: mergeIds(),
  4244. cover_url: /*track.cover || */album.cover,
  4245. };
  4246. })));
  4247. }); else if (/^https?:\/\/(?:\w+\.)?deezer\.com\/(?:\w+\/)*album\/(\d+)/i.test(url)) {
  4248. return queryDeezerAPI('album/' + RegExp.$1).then(function(release) {
  4249. isVA = vaParser.test(release.artist.name);
  4250. identifiers.DEEZER_ID = release.id;
  4251. identifiers.RELEASETYPE = release.record_type;
  4252. if (release.upc) identifiers.BARCODE = release.upc;
  4253. if (release.cover_xl) imgUrl = release.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0');
  4254. return release.tracks.data.map(function(track, ndx) {
  4255. trackIdentifiers = { TRACK_ID: track.id };
  4256. return {
  4257. artist: isVA ? VA : release.artist.name,
  4258. album: release.title,
  4259. release_date: release.release_date,
  4260. label: release.label,
  4261. media: media,
  4262. genre: release.genres.data.map(it => it.name).join('; '),
  4263. tracknumber: ndx + 1,
  4264. totaltracks: release.nb_tracks,
  4265. title: track.title,
  4266. track_artist: track.artist.name && (isVA || track.artist.name != release.artist.name) ? track.artist.name : undefined,
  4267. duration: track.duration,
  4268. //url: deezerAlbumPrefix + release.id,
  4269. identifiers: mergeIds(),
  4270. cover_url: imgUrl,
  4271. };
  4272. });
  4273. });
  4274. } else if (url.toLowerCase().includes('spotify.com/')) {
  4275. if (!/\/albums?\/(\w+)$/i.test(url)) return Promise.reject('This resource is not supported, pick a real album');
  4276. return querySpotifyAPI('albums/' + RegExp.$1).then(function(release) {
  4277. artist = release.artists.map(artist => artist.name);
  4278. isVA = release.artists.length <= 0 || release.artists.length == 1 && vaParser.test(release.artists[0].name);
  4279. totalDiscs = release.tracks.items.reduce((acc, track) => Math.max(acc, track.disc_number), 0);
  4280. identifiers.SPOTIFY_ID = release.id;
  4281. identifiers.RELEASETYPE = release.album_type;
  4282. identifiers.BARCODE = release.external_ids.upc;
  4283. var image = release.images.reduce((acc, image) => image.width * image.height > acc.width * acc.height ? image : acc);
  4284. return release.tracks.items.map(function(track, ndx) {
  4285. trackIdentifiers = {
  4286. TRACK_ID: track.id,
  4287. EXPLICIT: Number(track.explicit),
  4288. };
  4289. trackArtist = track.artists.map(artist => artist.name);
  4290. return {
  4291. artist: isVA ? VA : undefined,
  4292. artists: !isVA ? artist : undefined,
  4293. album: release.name,
  4294. release_date: release.release_date,
  4295. label: release.label,
  4296. media: media,
  4297. genre: release.genres.join('; '),
  4298. discnumber: track.disc_number,
  4299. totaldiscs: totalDiscs,
  4300. discsubtitle: discSubtitle,
  4301. tracknumber: track.track_number,
  4302. totaltracks: release.total_tracks,
  4303. title: track.name,
  4304. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  4305. duration: track.duration_ms / 1000,
  4306. //url: 'https://open.spotify.com/album/' + release.id,
  4307. identifiers: mergeIds(),
  4308. cover_url: image ? image.url : undefined,
  4309. };
  4310. });
  4311. });
  4312. } else if (url.toLowerCase().includes('prostudiomasters.com/')) return globalFetch(url).then(function(response) {
  4313. if ((ref = response.document.querySelector('img.album-art')) != null) imgUrl = ref.currentSrc || ref.src;
  4314. try {
  4315. if ((ref = response.document.querySelector('body > script[charset]')) == null
  4316. || !/\bPSM\.album\s*=\s*(\{[\S\s]+\});(?=\s*PSM\b)/.test(ref.text)) throw 'Metadata not found';
  4317. let album = JSON.parse(RegExp.$1);
  4318. if (prefs.diag_mode) console.debug('PSM metadata received:', album);
  4319. const artistSplitter = /\s*;+\s*/;
  4320. artist = album.ArtistName.split(artistSplitter);
  4321. isVA = vaParser.test(album.ArtistName);
  4322. if (album.id) identifiers.PROSTUDIOMASTERS_ID = parseInt(album.id) || album.id;
  4323. if (album.GenreName) genres.push(album.GenreName);
  4324. if (album.SubGenreName) genres.push(album.SubGenreName);
  4325. if (album.genres) genres.push(album.genres);
  4326. if (/^[℗©]\s*(\d{4})\b/.test(album.PLine) || /^[℗©]\s*(\d{4})\b/.test(album.CLine)) releaseDate = RegExp.$1;
  4327. if (album.ICPN) identifiers.BARCODE = album.ICPN;
  4328. if (/\b(\d+(?:\.\d+)?)\s*kHz\s*\/\s*(\d+)[\-\s]?bit\s+(\w+)\b/i.test(album.recording_info)) {
  4329. sr = parseFloat(RegExp.$1) * 1000 || undefined;
  4330. bd = parseInt(RegExp.$2) || undefined;
  4331. format = RegExp.$3;
  4332. if (['FLAC', 'AIFF', 'WAV', 'PCM'].includes(format)) encoding = 'lossless';
  4333. }
  4334. if (album.album_info) {
  4335. description = html2php(domParser.parseFromString(album.album_info, 'text/html').body, response.finalUrl);
  4336. if (description) description = '[quote]'.concat(description, '[/quote]');
  4337. }
  4338. return album.tracks.map(function(track) {
  4339. trackIdentifiers = {
  4340. EXPLICIT: Number(track.ExplicitLyrics == 1),
  4341. ISRC: track.ISRC,
  4342. TRACK_ID: parseInt(track.id) || track.id,
  4343. };
  4344. trackArtist = track.ArtistName.split(artistSplitter);
  4345. return {
  4346. artist: isVA ? VA : undefined,
  4347. artists: !isVA ? artist : undefined,
  4348. album: album.AlbumName,
  4349. genre: genres.join('; '),
  4350. release_date: releaseDate,
  4351. label: label,
  4352. catalog: album.CatalogNumber,
  4353. codec: format,
  4354. encoding: encoding,
  4355. bd: bd,
  4356. sr: sr,
  4357. media: media,
  4358. discnumber: parseInt(track.DiscSeq) || undefined,
  4359. discsubtitle: track.GroupingTitle,
  4360. tracknumber: parseInt(track.TrackSeq) || undefined,
  4361. totaltracks: album.tracks.length,
  4362. title: track.TrackName,
  4363. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalTo(artist)) ? trackArtist : undefined,
  4364. composers: track.composers.split(artistSplitter),
  4365. duration: parseInt(track.duration) || undefined,
  4366. url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
  4367. description: description,
  4368. identifiers: mergeIds(),
  4369. cover_url: imgUrl,
  4370. };
  4371. });
  4372. } catch(e) {
  4373. console.warn('ProStudioMasters: falling back to HTML scraper for the reason', e);
  4374. if (/\/page\/(\d+)$/i.test(response.finalUrl)) identifiers.PROSTUDIOMASTERS_ID = RegExp.$1;
  4375. artist = Array.from(response.document.querySelectorAll('h2.ArtistName > a')).map(node => node.textContent.trim());
  4376. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4377. if (isVA) artist = [];
  4378. if ((ref = response.document.querySelector('h3.AlbumName')) != null) album = ref.textContent.trim();
  4379. if ((ref = response.document.querySelector('div.pline')) != null
  4380. && /^(?:[℗©]\s*)+(\d{4})\s+(.+)/.test(ref.textContent.trim())) {
  4381. releaseDate = RegExp.$1;
  4382. label = RegExp.$2;
  4383. }
  4384. getDescription(response, 'div.album-info', false);
  4385. trs = response.document.querySelectorAll('div.album-tracks > div.tracks > table > tbody > tr');
  4386. totalTracks = Array.from(trs).filter(tr => tr.classList.contains('track-playable')).length;
  4387. discNumber = 0;
  4388. trs.forEach(function(tr) {
  4389. if (tr.classList.contains('track-playable')) {
  4390. trackArtist = sr = bd = format = title = undefined; trackIdentifiers = {};
  4391. if (ref = tr.getAttribute('data-track-id')) trackIdentifiers.TRACK_ID = ref;
  4392. trackNumber = (ref = tr.querySelector('div.num')) != null ? parseInt(ref.firstChild.textContent.trim()) : undefined;
  4393. if (trackNumber == 1) ++discNumber;
  4394. if ((ref = tr.querySelector('td.track-name > div.name')) != null) {
  4395. title = ref.firstChild.textContent.trim();
  4396. if ((ref = ref.querySelector(':scope small')) != null) trackArtist = ref.firstChild.textContent;
  4397. };
  4398. if ((ref = tr.querySelector('span.track-format')) != null && /^(\d+(?:[,\.]\d+)?)\s*([kMG]?Hz)(?:\s+(\d+)-bit)?\s*\|\s*(\S+)$/i.test(ref.textContent.trim())) {
  4399. sr = parseFloat(RegExp.$1);
  4400. ['hz', 'khz', 'mhz', 'ghz'].forEach((unit, ndx) => { if (RegExp.$2.toLowerCase() == unit) sr *= 1000 ** ndx });
  4401. sr = Math.round(sr) || undefined;
  4402. bd = parseInt(RegExp.$3) || undefined;
  4403. format = RegExp.$4;
  4404. }
  4405. tracks.push({
  4406. artist: isVA ? VA : undefined,
  4407. artists: !isVA ? artist : undefined,
  4408. album: album,
  4409. //album_year: extractYear(releaseDate),
  4410. release_date: releaseDate,
  4411. label: label,
  4412. catalog: catalogue,
  4413. codec: format,
  4414. bd: bd,
  4415. sr: sr,
  4416. media: media,
  4417. discnumber: discNumber,
  4418. totaldiscs: totalDiscs,
  4419. discsubtitle: discSubtitle,
  4420. tracknumber: trackNumber,
  4421. totaltracks: totalTracks,
  4422. title: title,
  4423. track_artist: trackArtist && (isVA || trackArtist != joinArtists(artist)) ? trackArtist : undefined,
  4424. duration: (ref = tr.querySelector('td:last-of-type')) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  4425. url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
  4426. description: description,
  4427. identifiers: mergeIds(),
  4428. cover_url: imgUrl,
  4429. });
  4430. } else if ((ref = tr.querySelector('div.grouping-title')) != null) {
  4431. discSubtitle = ref.textContent.trim();
  4432. guessDiscNumber();
  4433. }
  4434. });
  4435. return tracks;
  4436. }
  4437. }); else if (url.toLowerCase().includes('play.google.com/store/music/album/')) {
  4438. let _url = new URL(url), _query = new URLSearchParams(_url.search);
  4439. _query.set('hl', 'en');
  4440. _url.search = _query;
  4441. return globalFetch(_url).then(function(response) {
  4442. var search = new URLSearchParams(new URL(response.finalUrl).search);
  4443. var ID = search.get('id'), trackID, aggregateRating;
  4444. if (ID) identifiers.GOOGLE_ID = ID;
  4445. var root = response.document.querySelector('div[itemtype="https://schema.org/MusicAlbum"]');
  4446. if (root == null) throw new Error('Unexpected Google Play metadata structure');
  4447. if ((ref = root.querySelector('div[itemprop="byArtist"]')) != null) {
  4448. artist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
  4449. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4450. }
  4451. if ((ref = root.querySelector('meta[itemprop="name"]')) != null) album = ref.content;
  4452. genres = Array.from(root.querySelectorAll('meta[itemprop="genre"]')).map(elem => elem.content);
  4453. if ((ref = root.querySelector('meta[itemprop="datePublished"]')) != null) releaseDate = ref.content;
  4454. if ((ref = root.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  4455. if ((ref = root.querySelector('meta[itemprop="ratingValue"]')) != null) aggregateRating = parseFloat(ref.content);
  4456. if ((ref = response.document.querySelector('h1[class][itemprop="name"] > span')) != null
  4457. && (ref = ref.parentNode.parentNode.querySelector('div[class] > span[class]')) != null
  4458. && /\b(?:Explicit)\b/i.test(ref.textContent)) identifiers.EXPLICIT = 1;
  4459. if ((ref = response.document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content;
  4460. try {
  4461. let _objs = loadGoogleData(response);
  4462. let _albumInfo = _objs.filter(obj => { try { return obj[0].length == 22 } catch(e) { return false } });
  4463. let _tracks = _objs.filter(function(obj) {
  4464. try {
  4465. return typeof obj[0][0][1] == 'boolean' && typeof obj[0][0][2] == 'boolean'
  4466. && typeof obj[0][0][3] == 'string' && typeof obj[0][0][4] == 'string';
  4467. } catch(e) { return false }
  4468. });
  4469. if (_albumInfo.length == 1) _albumInfo = _albumInfo[0][0]; else throw 'Album metadata not found';
  4470. if (_tracks.length == 1) _tracks = _tracks[0][0][0]; else throw 'Tracks metadata not found';
  4471. if (prefs.diag_mode) console.debug('Google Play objects extracted successfully:', _albumInfo, _tracks);
  4472. try {
  4473. artist = _albumInfo[18][0][8].map(artist => artist[1]);
  4474. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4475. } catch(e) { }
  4476. try { description = _albumInfo[10][0][1] } catch(e) { }
  4477. try { genres = _albumInfo[18][0][6].map(genre => genre[2]) } catch(e) { }
  4478. _tracks[0].forEach(function(volume) {
  4479. Array.prototype.push.apply(tracks, volume[0].map(function(track) {
  4480. trackArtist = track[0][8].map(ta => ta[1]);
  4481. trackIdentifiers = { TRACK_ID: track[12][0] };
  4482. return {
  4483. artist: isVA ? VA : undefined,
  4484. artists: !isVA ? artist : undefined,
  4485. album: _albumInfo[0][0] || track[9][0],
  4486. album_year: extractYear(_albumInfo[18][0][7][1] /*track[0][7][0]*/),
  4487. release_date: _albumInfo[18][0][7][2],
  4488. label: _albumInfo[18][0][4] || track[0][4],
  4489. media: media,
  4490. genre: genres.join('; '),
  4491. discnumber: volume[1],
  4492. totaldiscs: _tracks[0].length,
  4493. tracknumber: track[1],
  4494. totaltracks: _albumInfo[18][4],
  4495. title: track[8][0],
  4496. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  4497. duration: track[0][1],
  4498. description: description,
  4499. url: !identifiers.GOOGLE_ID ? response.finalUrl : undefined,
  4500. identifiers: mergeIds(),
  4501. cover_url: imgUrl,
  4502. };
  4503. }));
  4504. });
  4505. } catch(e) {
  4506. console.warn('Google Play music: falling back to HTML scraper (' + e + ')');
  4507. tracks = [];
  4508. if ((ref = response.document.querySelector('span > a[itemprop="genre"]')) != null) try {
  4509. label = ref.parentNode.nextElementSibling.textContent.trim().replace(/^(?:[©℗]|\([cCpP]\))\s*\d{4}\s+/, '');
  4510. } catch(e) { console.warn('Unexpected HTML structure (' + e + ')') }
  4511. //getDescription(response, '???', false);
  4512. var volumes = response.document.querySelectorAll('c-wiz > div > h2');
  4513. if (volumes.length <= 0) {
  4514. //response.document.querySelectorAll('c-wiz > div > table > tbody > tr[class]').forEach(scanPlaylist);
  4515. trackNumber = 0;
  4516. root.querySelectorAll('div[itemprop="track"]').forEach(function(tr) {
  4517. trackIdentifiers = {};
  4518. if ((ref = tr.querySelector('meta[itemprop="url"]')) != null) {
  4519. search = new URLSearchParams(new URL(ref.content).search);
  4520. let trackID = search.get('tid');
  4521. if (trackID) trackIdentifiers.TRACK_ID = trackID;
  4522. }
  4523. ++trackNumber;
  4524. title = (ref = tr.querySelector('meta[itemprop="name"]')) != null ? ref.content : undefined;
  4525. trackArtist = (ref = tr.querySelector('div[itemprop="byArtist"]')) != null ?
  4526. Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content) : [];
  4527. duration = durationFromMeta(tr);
  4528. addTrack();
  4529. });
  4530. } else volumes.forEach(function(volume) {
  4531. discNumber = undefined; discSubtitle = volume.textContent.trim();
  4532. guessDiscNumber();
  4533. volume.nextElementSibling.querySelectorAll('tbody > tr[class]').forEach(scanPlaylist);
  4534. });
  4535.  
  4536. function scanPlaylist(tr) {
  4537. trackNumber = (ref = tr.querySelector('td:nth-of-type(1) > div')) != null ?
  4538. parseInt(ref.textContent) || ref.textContent.trim() : undefined;
  4539. title = (ref = tr.querySelector('td[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  4540. duration = (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined;
  4541. trackArtist = Array.from(tr.querySelectorAll('td:nth-of-type(4) > a')).map(it => it.textContent.trim());
  4542. addTrack();
  4543. }
  4544. function addTrack() {
  4545. tracks.push({
  4546. artist: isVA ? VA : undefined,
  4547. artists: !isVA ? artist : undefined,
  4548. album: album,
  4549. //album_year: extractYear(releaseDate),
  4550. release_date: releaseDate,
  4551. label: label,
  4552. catalog: catalogue,
  4553. media: media,
  4554. genre: genres.join('; '),
  4555. discnumber: discNumber,
  4556. totaldiscs: totalDiscs,
  4557. discsubtitle: discSubtitle,
  4558. tracknumber: trackNumber,
  4559. totaltracks: totalTracks,
  4560. title: title,
  4561. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  4562. duration: duration,
  4563. url: identifiers.GOOGLE_ID ? undefined : response.finalUrl,
  4564. description: description,
  4565. identifiers: mergeIds(),
  4566. cover_url: imgUrl,
  4567. });
  4568. }
  4569. }
  4570. return tracks;
  4571. });
  4572. } else if (url.toLowerCase().includes('7digital.com/')) return globalFetch(url).then(function(response) {
  4573. if ((ref = response.document.querySelector('table.release-track-list')) != null)
  4574. identifiers['7DIGITAL_ID'] = parseInt(ref.dataset.releaseid) || ref.dataset.releaseid;
  4575. artist = Array.from(response.document.querySelectorAll('h2.release-info-artist > span[itemprop="byArtist"] > meta[itemprop="name"]'))
  4576. .map(node => node.content);
  4577. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4578. if ((ref = response.document.querySelector('h1.release-info-title')) != null) album = ref.textContent.trim();
  4579. if ((ref = response.document.querySelector('div.release-date-info > p')) != null) releaseDate = normalizeDate(ref.textContent);
  4580. if ((ref = response.document.querySelector('div.release-label-info > p')) != null) label = ref.textContent.trim();
  4581. response.document.querySelectorAll('dl.release-data > dt.release-data-label').forEach(function(dt) {
  4582. if (/\bGenres?:/.test(dt.textContent)) genres = Array.from(dt.nextElementSibling.querySelectorAll('a')).map(a => a.textContent.trim());
  4583. });
  4584. //getDescription(response, 'div.album-info', false);
  4585. if ((ref = response.document.querySelector('span.release-packshot-image > img[itemprop="image"]')) != null)
  4586. imgUrl = ref.src;
  4587. totalTracks = response.document.querySelectorAll('table.release-track-list > tbody > tr.release-track').length;
  4588. response.document.querySelectorAll('table.release-track-list').forEach(function(table) {
  4589. discSubtitle = discNumber = undefined;
  4590. if ((ref = table.querySelector('caption > h4.release-disc-info')) != null) {
  4591. discSubtitle = ref.textContent.trim();
  4592. guessDiscNumber();
  4593. }
  4594. table.querySelectorAll('tbody > tr.release-track').forEach(function(tr) {
  4595. trackIdentifiers = {};
  4596. if (tr.dataset.trackid) trackIdentifiers.TRACK_ID = parseInt(tr.dataset.trackid) || tr.dataset.trackid;
  4597. tracks.push({
  4598. artist: isVA ? VA : undefined,
  4599. artists: !isVA ? artist : undefined,
  4600. album: album,
  4601. //album_year: extractYear(releaseDate),
  4602. release_date: releaseDate,
  4603. label: label,
  4604. catalog: catalogue,
  4605. media: media,
  4606. genre: genres.join('; '),
  4607. discnumber: discNumber,
  4608. totaldiscs: totalDiscs,
  4609. discsubtitle: discSubtitle,
  4610. tracknumber: (ref = tr.querySelector('td.release-track-preview > em.release-track-preview-text')) != null ?
  4611. ref.textContent.trim() : undefined,
  4612. totaltracks: totalTracks,
  4613. title: (ref = tr.querySelector('td.release-track-name > meta[itemprop="name"]')) != null ? ref.content : undefined,
  4614. duration: durationFromMeta(tr),
  4615. url: (ref = response.document.querySelector('head > meta[property="og:url"]')) != null ?
  4616. ref.content : response.finalUrl.replace(/\?.*$/, ''),
  4617. description: description,
  4618. identifiers: mergeIds(),
  4619. cover_url: imgUrl,
  4620. });
  4621. });
  4622. });
  4623. return tracks;
  4624. }); else if (url.toLowerCase().includes('e-onkyo.com/')) return globalFetch(url).then(function(response) {
  4625. if (/\/album\/(\w+)\/?$/.test(response.finalUrl)) identifiers.EONKYO_ID = RegExp.$1;
  4626. artist = Array.from(response.document.querySelectorAll('div.jacketDetailArea p.artistsName > a'))
  4627. .map(node => node.textContent.trim());
  4628. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4629. if ((ref = response.document.querySelector('div.jacketDetailArea p.packageTtl')) != null) album = ref.textContent.trim();
  4630. if ((ref = response.document.querySelector('div.jacketDetailArea p.recordlabelName > a')) != null) label = ref.textContent.trim();
  4631. if ((ref = response.document.querySelector('div.jacketDetailArea p.releaseDay > a')) != null) releaseDate = normalizeDate(ref.textContent);
  4632. if ((ref = response.document.querySelector('div.jacketDetailArea p.packageNoteDetail')) != null
  4633. && /^\s*(?:\(C\)|©)\s+(\d{4})\b/i.test(ref.lastChild.textContent)) albumYear = parseInt(RegExp.$1);
  4634. //getDescription(response, 'div#credit', true);
  4635. if (/\s+\(\s*(?:(\d+)[\-\s]*bit)?\s*\/?\s*(?:(\d+(?:\.\d+)?)\s*kHz)?\s*\)\s*$/i.test(album)) {
  4636. album = RegExp.leftContext;
  4637. bd = parseInt(RegExp.$1) || undefined;
  4638. sr = parseFloat(RegExp.$2) * 1000;
  4639. }
  4640. if ((ref = response.document.querySelector('figure > a.colorbox')) != null)
  4641. imgUrl = new URL(response.finalUrl).origin + ref.pathname;
  4642. trs = response.document.querySelectorAll('dl.musicList > dd.musicBox');
  4643. return Array.from(trs).map(tr => ({
  4644. //var trackId = tr.dataset.trackid;
  4645. //if (trackId) trackId = 'TRACK_ID=' + trackId;
  4646. //trackArtist = tr.children[5].textContent.trim();
  4647. //if (trackArtist == artist.join(', ')) trackArtist = undefined;
  4648. artist: isVA ? VA : undefined,
  4649. artists: !isVA ? artist : undefined,
  4650. album: album,
  4651. album_year: albumYear,
  4652. release_date: releaseDate,
  4653. label: label,
  4654. catalog: catalogue,
  4655. encoding: 'lossless',
  4656. codec: 'FLAC',
  4657. bd: bd,
  4658. sr: sr || undefined,
  4659. media: media,
  4660. //discnumber: discNumber,
  4661. //totaldiscs: totalDiscs,
  4662. //discsubtitle: discSubtitle,
  4663. tracknumber: (ref = tr.querySelector('div.musicListNo')) != null ? ref.textContent.trim() : undefined,
  4664. totaltracks: trs.length,
  4665. title: (ref = tr.querySelector('div.musicTtl > span')) != null ? ref.title || ref.textContent.trim() : undefined,
  4666. duration: (ref = tr.querySelector('div.musicTime')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
  4667. url: !identifiers.EONKYO_ID ? response.finalUrl : undefined,
  4668. description: description,
  4669. identifiers: mergeIds(),
  4670. cover_url: imgUrl,
  4671. }));
  4672. }); else if (url.toLowerCase().includes('store.acousticsounds.com/')) return globalFetch(url).then(function(response) {
  4673. if (/\/(\d+)\/$/.test(response.finalUrl)) identifiers.ACOUSTICSOUNDS_ID = RegExp.$1;
  4674. artist = Array.from(response.document.querySelectorAll('div > h1 > a')).map(node => node.textContent.trim());
  4675. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4676. if (isVA) artist = [];
  4677. if ((ref = response.document.querySelector('div > h1')) != null) album = ref.lastChild.wholeText.trim().replace(/\s*-\s*/, '');
  4678. response.document.querySelectorAll('div > p > table > tbody > tr > td:first-of-type').forEach(function(td) {
  4679. if (/^(?:Label):/i.test(td.textContent)) label = td.nextElementSibling.textContent.trim();
  4680. if (/^(?:Genre):/i.test(td.textContent)) genres[0] = td.nextElementSibling.textContent.trim();
  4681. if (/^(?:Product\s+No):/i.test(td.textContent)) catalogue = td.nextElementSibling.textContent.trim();
  4682. if (/^(?:Category):/i.test(td.textContent)
  4683. && /^(.+)\s+(\d+(?:\.\d+)?)\s*kHz(?:\s*\/\s*(\d+)[\s\-]?bit)?\s+Download\b/.test(td.nextElementSibling.textContent.trim())) {
  4684. format = RegExp.$1;
  4685. sr = parseFloat(RegExp.$2) * 1000;
  4686. bd = parseInt(RegExp.$3);
  4687. }
  4688. });
  4689. getDescription(response, 'div#description > p', true);
  4690. if ((ref = response.document.querySelector('div#detail > link[rel="image_src"]')) != null) {
  4691. imgUrl = ref.href.replace(/\/medium\//i, '/large/');
  4692. }
  4693. trs = response.document.querySelectorAll('div#tracks > table > tbody > tr');
  4694. trackNumber = 0;
  4695. return Array.from(trs).map(tr => ({
  4696. artist: isVA ? VA : undefined,
  4697. artists: !isVA ? artist : undefined,
  4698. album: album,
  4699. //album_year: extractYear(releaseDate),
  4700. release_date: releaseDate,
  4701. label: label,
  4702. catalog: catalogue,
  4703. encoding: ['FLAC', 'DSD'].includes(format) ? 'lossless' : undefined,
  4704. codec: format,
  4705. bd: bd,
  4706. sr: sr || undefined,
  4707. media: media,
  4708. genre: genres.join('; '),
  4709. //discnumber: discNumber,
  4710. //totaldiscs: totalDiscs,
  4711. //discsubtitle: discSubtitle,
  4712. tracknumber: ++trackNumber,
  4713. totaltracks: trs.length,
  4714. title: (ref = tr.querySelector('td[nowrap]')) != null ? ref.textContent.trim() : undefined,
  4715. url: !identifiers.ACOUSTICSOUNDS_ID ? response.finalUrl : undefined,
  4716. description: description,
  4717. identifiers: mergeIds(),
  4718. cover_url: imgUrl,
  4719. }));
  4720. }); else if (url.toLowerCase().includes('indies.eu/')) return globalFetch(url).then(function(response) {
  4721. if (/\/alba\/(\d+)\//.test(response.finalUrl)) identifiers.INDIESSCOPE_ID = parseInt(RegExp.$1);
  4722. ref = response.document.querySelector(':root > body > div > div > div > h2');
  4723. if (ref != null) artist = Array.from(ref.childNodes).map(node => node.textContent.trim());
  4724. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4725. if ((ref = response.document.querySelector(':root > body > div > div > div > h1')) != null)
  4726. album = ref.textContent.trim();
  4727. if ((ref = response.document.querySelector('div.infoBox')) != null) {
  4728. let ndx = 0;
  4729. ref.childNodes.forEach(function(child) {
  4730. if (child.nodeName == 'BR') { ++ndx; return; }
  4731. switch (ndx) {
  4732. case 0:
  4733. if (child.nodeType == Node.TEXT_NODE) {
  4734. label = child.wholeText.trim();
  4735. if (/^(.*)\s+\/\s+(\d{4})$/.test(label)) {
  4736. label = RegExp.$1;
  4737. releaseDate = RegExp.$2;
  4738. }
  4739. }
  4740. break;
  4741. case 1:
  4742. if (child.nodeType == Node.ELEMENT_NODE) genres.push(child.textContent.trim());
  4743. break;
  4744. case 2:
  4745. if (child.nodeType == Node.ELEMENT_NODE) catalogue = child.textContent.trim();
  4746. break;
  4747. }
  4748. });
  4749. }
  4750. getDescription(response, 'div.popis > section', true);
  4751. if ((ref = response.document.querySelector('div.obrazekDetail > img')) != null) imgUrl = ref.src;
  4752. trs = response.document.querySelectorAll('table.skladby > tbody > tr');
  4753. return Array.from(trs).map(function(tr) {
  4754. title = undefined;
  4755. if ((ref = tr.querySelector('td.nazev')) != null) {
  4756. trackNumber = parseInt(ref.firstChild.wholeText);
  4757. title = ref.querySelector('strong').textContent.trim();
  4758. }
  4759. return {
  4760. artist: isVA ? VA : undefined,
  4761. artists: !isVA ? artist : undefined,
  4762. album: album,
  4763. //album_year: extractYear(releaseDate),
  4764. release_date: releaseDate,
  4765. label: label,
  4766. catalog: catalogue,
  4767. codec: format,
  4768. media: media,
  4769. genre: genres.join('; '),
  4770. //discnumber: discNumber,
  4771. //totaldiscs: totalDiscs,
  4772. //discsubtitle: discSubtitle,
  4773. tracknumber: trackNumber,
  4774. totaltracks: trs.length,
  4775. title: title,
  4776. duration: (ref = tr.querySelector('td:nth-of-type(4)')) != null ? timeStringToTime(ref.textContent) : undefined,
  4777. identifiers: !identifiers.INDIESSCOPE_ID ? response.finalUrl : undefined,
  4778. description: description,
  4779. identifiers: mergeIds(),
  4780. cover_url: imgUrl,
  4781. };
  4782. });
  4783. }); else if (url.toLowerCase().includes('beatport.com/')) return globalFetch(url).then(function(response) {
  4784. if (/\/release\/(?:\d\/)?(?:\S+-)?(\d+)\b/i.test(response.finalUrl)) identifiers.BEATPORT_ID = RegExp.$1;
  4785. artist = Array.from(response.document.querySelectorAll('span > a[data-artist]')).map(node => node.textContent.trim());
  4786. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4787. if ((ref = response.document.querySelector('div > h1')) != null) album = ref.textContent.trim();
  4788. response.document.querySelectorAll('ul > li > span.category').forEach(function(span) {
  4789. if (/^(?:Release\s+Date)/i.test(span.textContent)) releaseDate = span.nextElementSibling.textContent.trim();
  4790. if (/^(?:Label)/i.test(span.textContent)) label = span.nextElementSibling.textContent.trim();
  4791. if (/^(?:Catalog)/i.test(span.textContent)) catalogue = span.nextElementSibling.textContent.trim();
  4792. });
  4793. getDescription(response, 'div.interior-expandable', true);
  4794. if ((ref = response.document.querySelector('div > img.interior-release-chart-artwork')) != null) imgUrl = ref.src;
  4795. trs = response.document.querySelectorAll('div.tracks > ul > li.track');
  4796. return Array.from(trs).map(function(tr) {
  4797. trackIdentifiers = { TRACK_ID: parseInt(tr.dataset.ecId) || tr.dataset.ecId };
  4798. title = (ref = tr.querySelector('span.buk-track-primary-title')) != null ?
  4799. ref.title || ref.textContent.trim() : tr.dataset.ecName;
  4800. if (title && (ref = tr.querySelector('span.buk-track-remixed')) != null) title += ' (' + ref.textContent.trim() + ')';
  4801. trackArtist = Array.from(tr.querySelectorAll('p.buk-track-artists > a')).map(a => a.textContent.trim());
  4802. if ((ref = tr.querySelector('p.buk-track-bpm')) != null) trackIdentifiers.BPM = parseInt(ref.textContent);
  4803. return {
  4804. artist: isVA ? VA : undefined,
  4805. artists: !isVA ? artist : undefined,
  4806. album: album,
  4807. //album_year: extractYear(releaseDate),
  4808. release_date: releaseDate,
  4809. label: tr.dataset.ecBrand || ((ref = tr.querySelector('p.buk-track-labels')) != null ? ref.textContent.trim() : label),
  4810. catalog: catalogue,
  4811. codec: format,
  4812. media: media,
  4813. genre: Array.from(tr.querySelectorAll('p.buk-track-genre > a')).map(a => a.textContent).join('; '),
  4814. //discnumber: discNumber,
  4815. //totaldiscs: totalDiscs,
  4816. //discsubtitle: discSubtitle,
  4817. tracknumber: tr.dataset.ecPosition || ((ref = tr.querySelector('div.buk-track-num')) != null ?
  4818. ref.textContent.trim() : undefined),
  4819. totaltracks: trs.length,
  4820. title: title,
  4821. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  4822. remixers: Array.from(tr.querySelectorAll('p.buk-track-remixers > a')).map(a => a.textContent.trim()),
  4823. duration: (ref = tr.querySelector('p.buk-track-length')) != null ? timeStringToTime(ref.textContent) : undefined,
  4824. description: description,
  4825. url: !identifiers.BEATPORT_ID ? response.finalUrl : undefined,
  4826. cover_url: imgUrl,
  4827. identifiers: mergeIds(),
  4828. };
  4829. });
  4830. }); else if (url.toLowerCase().includes('traxsource.com/')) return globalFetch(url).then(function(response) {
  4831. if (/\/title\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.TRAXSOURCE_ID = RegExp.$1;
  4832. artist = Array.from(response.document.querySelectorAll('h1.artists > a.com-artists')).map(node => node.textContent.trim());
  4833. if (artist.length <= 0 && (ref = response.document.querySelector('h1.artists')) != null) artist = [ref.textContent.trim()];
  4834. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4835. if ((ref = response.document.querySelector('h1.title')) != null) album = ref.textContent.trim();
  4836. if ((ref = response.document.querySelector('a.com-label')) != null) label = ref.textContent.trim();
  4837. if ((ref = response.document.querySelector('div.cat-rdate')) != null && /^(.*)\s*\|\s*(.*)$/.test(ref.textContent.trim())) {
  4838. catalogue = RegExp.$1;
  4839. releaseDate = normalizeDate(RegExp.$2);
  4840. }
  4841. getDescription(response, 'div.desc', true);
  4842. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  4843. trs = response.document.querySelectorAll('div.trklist > div.trk-row');
  4844. return Array.from(trs).map(function(tr) {
  4845. trackIdentifiers = {};
  4846. title = (ref = tr.querySelector('div.title > a')) != null && ref.textContent.trim() || undefined;
  4847. if (title && (ref = tr.querySelector('span.version')) != null ) {
  4848. if (ref.firstChild.nodeType == Node.TEXT_NODE
  4849. && (i = ref.firstChild.wholeText.trim()).length > 0) title += ' ('.concat(i, ')');
  4850. }
  4851. trackArtist = Array.from(tr.querySelectorAll('div.artists a.com-artists')).map(a => a.textContent.trim());
  4852. return {
  4853. artist: isVA ? VA : undefined,
  4854. artists: !isVA ? artist : undefined,
  4855. album: album,
  4856. //album_year: extractYear(releaseDate),
  4857. release_date: releaseDate,
  4858. label: label,
  4859. catalog: catalogue,
  4860. media: media,
  4861. genre: Array.from(tr.querySelectorAll('div.genre > a')).map(a => a.textContent.trim()).join('; '),
  4862. //discnumber: discNumber,
  4863. //totaldiscs: totalDiscs,
  4864. //discsubtitle: discSubtitle,
  4865. tracknumber: (ref = tr.querySelector('div.tnum')) != null ? ref.textContent.trim() : undefined,
  4866. totaltracks: trs.length,
  4867. title: title,
  4868. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  4869. remixers: Array.from(tr.querySelectorAll('div.artists a.com-remixers')).map(a => a.textContent.trim()),
  4870. duration: (ref = tr.querySelector('span.duration')) != null ? timeStringToTime(ref.textContent) : undefined,
  4871. url: !identifiers.TRAXSOURCE_ID ? response.finalUrl : undefined,
  4872. description: description,
  4873. identifiers: mergeIds(),
  4874. cover_url: imgUrl,
  4875. };
  4876. });
  4877. }); else if (url.toLowerCase().includes('music.apple.com/')) return loadItunesMetadata(url).then(function(album) {
  4878. identifiers.APPLE_ID = parseInt(album.id) || album.id;
  4879. isVA = vaParser.test(album.attributes.artistName);
  4880. genres = album.attributes.genreNames.filter(genre => genre != 'Music');
  4881. label = album.attributes.recordLabel;
  4882. if (!label) label = album.attributes.copyright.replace(/^((?:[©℗]|\([PC]\))\s+)?(?:(\d{4})\s+)?/i, '');
  4883. //identifiers.EXPLICIT = Number(/^(?:explicit)$/i.test(album.attributes.contentRating));
  4884. if ('isCompilation' in album.attributes) identifiers.COMPILATION = Number(album.attributes.isCompilation);
  4885. if (album.attributes.isSingle) identifiers.RELEASETYPE = 'Single';
  4886. if (album.description) description = html2php(album.description, album.attributes.url);
  4887. if (!description && album.attributes.editorialNotes)
  4888. description = html2php(domParser.parseFromString(album.attributes.editorialNotes.standard
  4889. || album.attributes.editorialNotes.short, 'text/html').body, album.attributes.url).replace(/\n/g, '\n\n');
  4890. if (description && !description.includes('[/quote]')) description = '[quote]'.concat(description, '[/quote]');
  4891. //if (description && !description.includes('[quote]')) description = '[quote]' + description.collapseGaps() + '[/quote]';
  4892. if (album.attributes.artwork && prefs.apple_offer_alt_cover)
  4893. addMessage(new HTML('<a target="_blank" href="' + album.attributes.artwork.realUrl + '">Alternate cover URL</a>'), 'info');
  4894. return album.relationships.tracks.data.filter(track => track.type == 'songs').map(function(track) {
  4895. trackIdentifiers = {
  4896. TRACK_ID: parseInt(track.id),
  4897. ISRC: track.attributes.isrc,
  4898. EXPLICIT: Number(/^(?:explicit)$/i.test(track.attributes.contentRating)),
  4899. HASLYRICS: Number(track.attributes.hasLyrics || false),
  4900. };
  4901. var trackGenres = track.attributes.genreNames.filter(genre => genre != 'Music');
  4902. return {
  4903. artist: isVA ? VA : album.attributes.artistName,
  4904. artists: album.relationships.artists.data.map(artist => artist.attributes.name),
  4905. album: album.attributes.name,
  4906. release_date: album.attributes.releaseDate,
  4907. label: label,
  4908. media: media,
  4909. genre: (trackGenres.length > 0 ? trackGenres : genres).join('; '),
  4910. discnumber: track.attributes.discNumber,
  4911. discsubtitle: track.attributes.workName,
  4912. tracknumber: track.attributes.trackNumber,
  4913. totaltracks: album.attributes.trackCount,
  4914. title: track.attributes.name,
  4915. track_artist: track.attributes.artistName && (isVA || track.attributes.artistName != album.attributes.artistName) ?
  4916. track.attributes.artistName : undefined,
  4917. composer: track.attributes.composerName,
  4918. duration: track.attributes.durationInMillis / 1000 || undefined,
  4919. description: description,
  4920. url: !identifiers.APPLE_ID ? album.attributes.url : undefined,
  4921. identifiers: mergeIds(),
  4922. //cover_url: album.attributes.artwork ? album.attributes.artwork.realUrl : undefined,
  4923. };
  4924. });
  4925. }); else if (mbrRlsParser.test(url)) { // MusicBrainz
  4926. var entities = [
  4927. 'aliases', 'annotation', 'artist-credits', 'artists', 'collections', 'discids', 'genres',
  4928. 'isrcs', 'labels', 'media', 'ratings', 'recordings', 'release-groups', 'tags', 'url-rels',
  4929. ];
  4930. return queryMusicBrainzAPI('release/' + RegExp.$1, { inc: entities.join('+') }).then(function(release) {
  4931. if (release.error) return Promise.reject(release.error);
  4932. if (prefs.diag_mode) console.debug('MusicBrainz release metadata received:', release);
  4933. if (release.id) identifiers.MBID = release.id;
  4934. if (release.barcode) identifiers.BARCODE = release.barcode;
  4935. if (release.asin) identifiers.ASIN = release.asin;
  4936. if (release['release-group']['primary-type']) identifiers.RELEASETYPE = release['release-group']['primary-type'];
  4937. artist = Array.isArray(release['artist-credit']) ? release['artist-credit'].map(artist => artist.name) : [];
  4938. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4939. if (Array.isArray(release.genres)) genres = release.genres.map(genre => genre.name);
  4940. if (Array.isArray(release.tags)) Array.prototype.push.apply(genres, release.tags.map(tag => tag.name));
  4941. if (genres.length <= 0) {
  4942. if (Array.isArray(release['release-group'].genres)) {
  4943. Array.prototype.push.apply(genres, release['release-group'].genres.map(tag => tag.name));
  4944. }
  4945. if (Array.isArray(release['release-group'].tags)) {
  4946. Array.prototype.push.apply(genres, release['release-group'].tags.map(tag => tag.name));
  4947. }
  4948. }
  4949. label = release['label-info'].map(label => label.label.name);
  4950. catalogue = release['label-info'].map(label => label['catalog-number']);
  4951. if (release['release-group'].status && !/^(?:Official)$/i.test(release['release-group'].status))
  4952. addMessage('Not an official release ('.concat(release['release-group'].status, ')'), 'warning');
  4953. release.media.forEach(function(medium, ndx) {
  4954. medium.tracks.forEach(function(track, ndx) {
  4955. trackIdentifiers = { TRACK_ID: track.id };
  4956. if (Array.isArray(track['artist-credit'])) {
  4957. trackArtist = track['artist-credit'].map(artist => artist.name);
  4958. trackArtist = trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist));
  4959. } else trackArtist = false;
  4960. tracks.push({
  4961. artist: isVA ? VA : undefined,
  4962. artists: !isVA ? artist : undefined,
  4963. album: /*release['release-group'].title || */release.title,
  4964. album_year: extractYear(release['release-group']['first-release-date']),
  4965. release_date: release.date,
  4966. genre: genres.join('; '),
  4967. label: label.filter(label => label).join(' / '),
  4968. catalog: catalogue.filter(catno => catno).join(' / '),
  4969. media: medium.format,
  4970. discnumber: medium.position,
  4971. discsubtitle: medium.title,
  4972. totaldiscs: release.media.length,
  4973. tracknumber: track.number,
  4974. title: track.title,
  4975. track_artist: trackArtist ?
  4976. track['artist-credit'].map(artist => artist.name.concat(artist.joinphrase)).join('') : undefined,
  4977. duration: track.length != null ? track.length / 1000 : undefined,
  4978. //country: release.country,
  4979. description: release.annotation,
  4980. identifiers: mergeIds(),
  4981. });
  4982. });
  4983. });
  4984. return tracks;
  4985. });
  4986. } else if (url.toLowerCase().includes('vgmdb.net/')) return globalFetch(url).then(function(response) {
  4987. if (/\/album\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.VGMDB_ID = RegExp.$1;
  4988. if ((ref = response.document.querySelector('h1 > span.albumtitle[style="display:inline"]')) != null) {
  4989. album = ref.innerText.trim();
  4990. if (ref.lang == 'en'
  4991. && (ref = response.document.querySelector('div > span.albumtitle[style="display:inline"]')) != null
  4992. && ref.firstChild != null && ref.firstChild.nodeType == Node.TEXT_NODE)
  4993. album += ' ('.concat(ref.firstChild.wholeText.trim(), ')');
  4994. }
  4995. composer = [];
  4996. response.document.querySelectorAll('table#album_infobit_large > tbody > tr > td > span.label > b').forEach(function(key) {
  4997. var value = key.parentNode.parentNode.nextElementSibling;
  4998. switch (key.innerText.trim().toLowerCase()) {
  4999. case 'catalog number':
  5000. catalogue = value.textContent.trim().replace(/\s*\([^\(\)]+\)$/, '');
  5001. break;
  5002. case 'release date':
  5003. if (value.firstElementChild != null) releaseDate = value.firstElementChild.innerText.trim();
  5004. break;
  5005. case 'media format':
  5006. media = value.textContent.trim();
  5007. break;
  5008. case 'classification':
  5009. genres = value.textContent.trim().split(/\s*,\*/);
  5010. break;
  5011. case 'published by':
  5012. label = Array.from(value.querySelectorAll('a > span.productname:first-of-type'))
  5013. .map(span => span.innerText.trim()).join(' / ');
  5014. break;
  5015. case 'composed by':
  5016. case 'lyrics by':
  5017. getArtists(value).forEach(artist => { composer.pushUniqueCaseless(artist) });
  5018. break;
  5019. case 'performed by':
  5020. artist = getArtists(value);
  5021. break;
  5022. case 'arranged by':
  5023. var arrangers = getArtists(value);
  5024. break;
  5025. }
  5026. });
  5027. if (!artist || artist.length <= 0) artist = composer;
  5028. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5029. genres.pushUniqueCaseless('Soundtrack');
  5030. response.document.querySelectorAll('td#rightcolumn > div > div > div > b.label').forEach(function(key) {
  5031. var value = key.parentNode.lastChild;
  5032. if (key.innerText.toLowerCase() == 'category' && value != null)
  5033. genres.pushUniqueCaseless(value.textContent.trim());
  5034. });
  5035. getDescription(response, 'div#notes', false);
  5036. if ((ref = response.document.querySelector('div#coverart')) != null
  5037. && /\burl\s*\(\"(.*)"\)/i.test(ref.style['background-image'])) imgUrl = RegExp.$1;
  5038. response.document.querySelectorAll('div#tracklist > span > span > b').forEach(function(node) {
  5039. discSubtitle = node.innerText.trim();
  5040. guessDiscNumber();
  5041. node = node.parentNode;
  5042. while (node != null && node.nodeName != 'TABLE') node = node.nextElementSibling;
  5043. if (node != null) addVolume(node);
  5044. });
  5045. var tl = Array.from(response.document.querySelectorAll('ul#tlnav > li > a'));
  5046. if (tl.length <= 1) return tracks;
  5047. if ((i = tracks.length / tl.length) != Math.floor(i)) {
  5048. console.warn('Unexpected vgmdb.net tracklist length:', i, tracks);
  5049. return tracks;
  5050. }
  5051. let enIndex = tl.findIndex(l => /^English\b/i.test(l.innerText.trim()));
  5052. if (enIndex < 0) enIndex = tl.findIndex(l => /^Romaji\b/i.test(l.innerText.trim()));
  5053. if (enIndex < 0) return tracks.slice(0, i);
  5054. let jpIndex = tl.findIndex(l => /^Japanese\b/i.test(l.innerText.trim()));
  5055. if (jpIndex < 0) jpIndex = enIndex > 0 ? 0 : 1;
  5056. return tracks.slice(enIndex * i, (enIndex + 1) * i).map(function(track, ndx) {
  5057. const rx = /^(.+?)(?:\s+\(([^\(\)]+)\))?$/;
  5058. if (!track.title) track.title = tracks[jpIndex * i + ndx].title;
  5059. else if ((jpTitle = tracks[jpIndex * i + ndx].title) != track.title) {
  5060. track.title += ' (';
  5061. var enTitle = rx.exec(track.title), jpTitle = rx.exec(jpTitle);
  5062. if (jpTitle[1] != enTitle[1]) {
  5063. track.title += jpTitle[1];
  5064. if (jpTitle[2] && jpTitle[2] != enTitle[2]) track.title += ' ('.concat(jpTitle[2], ')');
  5065. } else track.title += jpTitle[2];
  5066. track.title += ')';
  5067. }
  5068. return track;
  5069. });
  5070.  
  5071. function addVolume(node) {
  5072. Array.prototype.push.apply(tracks, Array.from(node.querySelectorAll('tbody > tr')).map(tr => ({
  5073. artist: isVA ? VA : undefined,
  5074. artists: !isVA ? artist : undefined,
  5075. album: album,
  5076. //album_year: extractYear(releaseDate),
  5077. release_date: releaseDate,
  5078. label: label,
  5079. catalog: catalogue,
  5080. media: media,
  5081. genre: genres.join('; '),
  5082. discnumber: discNumber,
  5083. //totaldiscs: totalDiscs,
  5084. discsubtitle: discSubtitle,
  5085. tracknumber: (ref = tr.querySelector('span.label')) != null ? parseInt(ref.innerText) : undefined,
  5086. //totaltracks: trs.length,
  5087. title: tr.children[1].innerText.trim(),
  5088. //track_artist: joinArtists(trackArtist),
  5089. composers: composer,
  5090. duration: (ref = tr.querySelector('span.time')) != null ? timeStringToTime(ref.innerText) : undefined,
  5091. url: !identifiers.VGMDB_ID ? response.finalUrl : undefined,
  5092. description: description,
  5093. identifiers: mergeIds(),
  5094. cover_url: imgUrl,
  5095. })));
  5096. }
  5097.  
  5098. function getArtists(node) {
  5099. var artists = [];
  5100. node.childNodes.forEach(function(node) {
  5101. switch (node.nodeType) {
  5102. case Node.ELEMENT_NODE:
  5103. if ((i = node.querySelectorAll('span.artistname')).length > 0) {
  5104. var artist = i[0].innerText.trim();
  5105. if (i.length > 1 && i[0].lang == 'en') artist += ' ('.concat(i[1].innerText.trim(), ')');
  5106. } else artist = node.innerText.trim();
  5107. if (artist) artists.push(artist);
  5108. break;
  5109. case Node.TEXT_NODE:
  5110. artist = node.wholeText.trim().replace(/^\s*,\s*|\s*,\s*$/g, '');
  5111. if (/^[\(\)]+$/.test(artist)) return;
  5112. if (artist) Array.prototype.push.apply(artists, artist.split(/\s*,\s*/));
  5113. break;
  5114. }
  5115. });
  5116. return artists;
  5117. }
  5118. }); else if (url.toLowerCase().includes('tidal.com/')) {
  5119. if (!(/\/album\/(\d+)(?:\/|$)/i.test(url) && !/\b(?:albumId)=(\d+)\b/i.test(url)))
  5120. return Promise.reject('Fetching from this page is not supported');
  5121. return queryTidalAPI('album', { albumId: RegExp.$1 }).then(function(album) {
  5122. var albumHeader = findModule('ALBUM_HEADER');
  5123. if (albumHeader == null) return Promise.reject('Album header not found');
  5124. var albumItems = findModule('ALBUM_ITEMS');
  5125. if (albumItems == null) return Promise.reject('Album items not found');
  5126. artist = albumHeader.album.artists.map(artist => artist.name);
  5127. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0].name);
  5128. identifiers.TIDAL_ID = albumHeader.album.id;
  5129. identifiers.RELEASETYPE = albumHeader.album.type;
  5130. if (/^(?:(?:\([PC]\)|©|℗)\s+)?(?:(\d{4})\s+)?(.*)/.test(albumHeader.album.copyright)) {
  5131. //if (RegExp.$1) albumYear = parseInt(RegExp.$1);
  5132. label = RegExp.$2;
  5133. }
  5134. var channels;
  5135. description = albumHeader.description;
  5136. if (albumHeader.review.text) {
  5137. if (description) description += '\n\n';
  5138. if (!albumHeader.review.source) description += '[b]Album Review[/b]\n\n';
  5139. description += '[quote';
  5140. if (albumHeader.review.source) description += '=Album review from '.concat(albumHeader.review.source);
  5141. description += ']'.concat(albumHeader.review.text, '[/quote]');
  5142. description = description
  5143. .replace(/\[wimpLink\s+artistId="(\d+)"\]/g, '[url=https://listen.tidal.com/artist/$1]')
  5144. .replace(/\[wimpLink\s+albumId="(\d+)"\]/g, '[url=https://listen.tidal.com/album/$1]')
  5145. .replace(/\[\/wimpLink\]/g, '[/url]');
  5146. }
  5147. if (Array.isArray(albumHeader.credits.items) && albumHeader.credits.items.length > 0) {
  5148. let ac = '';
  5149. albumHeader.credits.items.forEach(function(credit) {
  5150. if (/^Primary Artist$/i.test(credit.type)) return;
  5151. // if (/^Record label$/i.test(credit.type)) {
  5152. // label = credit.contributors.map(contributor => contributor.name).join(' / ');
  5153. // return;
  5154. // }
  5155. ac += '\n'.concat(credit.type, ' – ', joinArtists(credit.contributors.map(contributor =>
  5156. !contributor.id ? contributor.name :
  5157. '[url=https://listen.tidal.com/artist/'.concat(contributor.id, ']', contributor.name, '[/url]'))));
  5158. });
  5159. if (ac.length > 0) {
  5160. if (description) {
  5161. if (!albumHeader.review.text) description += '\n';
  5162. description += '\n';
  5163. }
  5164. description += '[b]Additional Credits[/b]\n'.concat(ac);
  5165. }
  5166. }
  5167. if (albumHeader.album.cover)
  5168. imgUrl = 'https://resources.tidal.com/images/'.concat(albumHeader.album.cover.replace(/-/g, '/'), '/1280x1280.jpg');
  5169. return albumItems.pagedList.items.map(function(track, ndx) {
  5170. if (track.type != 'track') return;
  5171. trackIdentifiers = {
  5172. TRACK_ID: track.item.id,
  5173. EXPLICIT: Number(track.item.explicit),
  5174. };
  5175. trackArtist = track.item.artists.map(artist => artist.name);
  5176. channels = undefined;
  5177. track.item.audioModes.forEach(function(audioMode) {
  5178. switch (audioMode.toLowerCase()) {
  5179. case 'stereo': channels = 2; break;
  5180. default: if (/\b(\d+)\.(\d+)\b/.test(audioMode)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  5181. }
  5182. });
  5183. return {
  5184. artist: isVA ? VA : undefined,
  5185. artists: !isVA ? artist : undefined,
  5186. album: albumHeader.album.title,
  5187. album_year: albumYear,
  5188. release_date: albumHeader.album.releaseDate,
  5189. label: label,
  5190. media: media,
  5191. discnumber: track.item.volumeNumber,
  5192. totaldiscs: albumHeader.album.numberOfVolumes,
  5193. //discsubtitle: discSubtitle,
  5194. tracknumber: track.item.trackNumber,
  5195. totaltracks: albumHeader.album.numberOfTracks,
  5196. title: track.item.title,
  5197. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  5198. encoding: ['HI_RES', 'LOSSLESS'].includes(track.item.audioQuality) ? 'lossless' : undefined,
  5199. duration: track.item.duration,
  5200. channels: channels,
  5201. description: description,
  5202. url: !identifiers.TIDAL_ID ? albumHeader.album.url : undefined,
  5203. identifiers: mergeIds(),
  5204. cover_url: imgUrl,
  5205. ag: track.replayGain ? track.replayGain.toString().concat(' dB') : undefined,
  5206. };
  5207. });
  5208.  
  5209. function findModule(type) {
  5210. for (var row of album.rows) {
  5211. var result = row.modules.find(module => module.type == type);
  5212. if (result != undefined) return result;
  5213. }
  5214. return null;
  5215. }
  5216. });
  5217. } else if (url.toLowerCase().includes('ototoy.jp/')) return globalFetch(url).then(function(response) {
  5218. if (/\/p\/(\d+)(?=\/|\?|$)/i.test(response.finalUrl)) identifiers.OTOTOY_ID = parseInt(RegExp.$1);
  5219. artist = Array.from(response.document.querySelectorAll('span.album-artist > *')).map(node => node.textContent.trim());
  5220. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5221. if ((ref = response.document.querySelector('h1.album-title')) != null) album = ref.textContent.trim();
  5222. if ((ref = response.document.querySelector('p.hqd-logo > span')) != null
  5223. && /Audio Format:\s*(\d+)\s*bit\s*\/\s*(\d+(?:\.\d+)?)\s*kHz\b/i.test(ref.textContent.trim())) {
  5224. bd = parseInt(RegExp.$1);
  5225. sr = parseFloat(RegExp.$2) * 1000;
  5226. }
  5227. if ((ref = response.document.querySelector('p.hqd-logo > a.lossless')) != null) encoding = 'lossless';
  5228. if ((ref = response.document.querySelector('p.release-day')) != null && /\b(\d{4})-(\d{2})-(\d{2})\b/.test(ref.textContent))
  5229. releaseDate = RegExp.lastMatch;
  5230. label = Array.from(response.document.querySelectorAll('p.label-name > a')).map(a => a.textContent.trim()).join(' / ');
  5231. if ((ref = response.document.querySelector('p.catalog-id')) != null && /\b(?:Catalog\s+number):\s*(.*)$/i.test(ref.textContent.trim()))
  5232. catalogue = RegExp.$1;
  5233. genres = Array.from(response.document.querySelectorAll('ul.tag-cloud > li > a.oty-btn-tag'))
  5234. .map(a => a.textContent.trim()).filter(genre => genre.length > 0);
  5235. getDescription(response, 'div.album-addendum', false);
  5236. if ((ref = response.document.querySelector('div#jacket-full-wrapper > img')) != null) imgUrl = ref.dataset.src || ref.src;
  5237. trs = response.document.querySelectorAll('table#tracklist > tbody > tr[class^="bg"]');
  5238. return Array.from(trs).map(function(tr, ndx) {
  5239. trackIdentifiers = {};
  5240. title = (ref = tr.querySelector('td.item > span[id^="title-"]')) != null ? ref.textContent.trim() : undefined;
  5241. if (ref != null && /^title-(\d+)$/.test(ref.id)) trackIdentifiers.TRACK_ID = parseInt(RegExp.$1);
  5242. trackArtist = Array.from(tr.querySelectorAll('td.item > span > a.artist')).map(a => a.textContent.trim());
  5243. return {
  5244. artist: isVA ? VA : undefined,
  5245. artists: !isVA ? artist : undefined,
  5246. album: album,
  5247. album_year: extractYear(releaseDate),
  5248. release_date: releaseDate,
  5249. label: label,
  5250. catalog: catalogue,
  5251. media: media,
  5252. genre: genres.join('; '),
  5253. discnumber: discNumber,
  5254. tracknumber: ndx + 1,
  5255. totaltracks: trs.length,
  5256. sr: sr || undefined,
  5257. bd: bd,
  5258. encoding: encoding,
  5259. title: title,
  5260. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  5261. duration: (ref = tr.querySelector(':scope > td.item:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined,
  5262. description: description,
  5263. url: !identifiers.OTOTOY_ID ? response.finalUrl : undefined,
  5264. cover_url: imgUrl,
  5265. identifiers: mergeIds(),
  5266. };
  5267. });
  5268. }); else if (url.toLowerCase().includes('music.yandex.ru/') && (/\/album\/(\d+)\b/i.test(url)
  5269. || /\b(?:album)=(\d+)\b/i.test(url))) return globalFetch('https://music.yandex.ru/handlers/album.jsx?album=' + RegExp.$1, { responseType: 'json' }).then(function(response) {
  5270. if (prefs.diag_mode) console.debug('Yandex Music metadata received:', response.response);
  5271. if (response.response.metaType && response.response.metaType != 'music') throw 'Not a music release';
  5272. identifiers.YANDEX_ID = response.response.id;
  5273. if (response.response.type) identifiers.RELEASETYPE = response.response.type;
  5274. artist = response.response.artists.filter(artist => !artist.composer).map(artist => artist.name);
  5275. composer = response.response.artists.filter(artist => artist.composer).map(artist => artist.name);
  5276. isVA = response.response.artists.length <= 0
  5277. || response.response.artists.length == 1 && response.response.artists.some(artist => artist.various);
  5278. album = response.response.title;
  5279. if (response.response.version) album += ' ('.concat(response.response.version, ')');
  5280. response.response.volumes.forEach(function(volume, discNumber) {
  5281. Array.prototype.push.apply(tracks, volume.filter(track => track.type == 'music').map(function(track, trackNumber) {
  5282. trackIdentifiers = { TRACK_ID: parseInt(/*track.realId || */track.id) };
  5283. title = track.title;
  5284. if (track.version) title += ' ('.concat(track.version, ')');
  5285. trackArtist = track.artists.filter(artist => !artist.composer).map(artist => artist.name);
  5286. var trackComposer = track.artists.filter(artist => artist.composer).map(artist => artist.name);
  5287. return {
  5288. artist: isVA ? VA : undefined,
  5289. artists: !isVA ? artist : undefined,
  5290. album: album,
  5291. album_year: response.response.year,
  5292. release_date: response.response.releaseDate.replace(/T.*$/, ''),
  5293. label: response.response.labels.map(label => label.name).join(' / '),
  5294. media: media,
  5295. genre: response.response.genre,
  5296. tracknumber: trackNumber + 1,
  5297. totaltracks: response.response.trackCount,
  5298. composers: trackComposer.length > 0 ? trackComposer : composer,
  5299. discnumber: discNumber + 1,
  5300. totaldiscs: response.response.volumes.length,
  5301. title: title,
  5302. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
  5303. duration: track.durationMs / 1000,
  5304. cover_url: 'https://'.concat(response.response.coverUri.replace('/%%', '/m1000x1000')),
  5305. identifiers: mergeIds(),
  5306. tg: track.normalization ? track.normalization.gain.toString().concat(' dB') : undefined,
  5307. tp: track.normalization ? track.normalization.peak.toString().concat(' dB') : undefined,
  5308. };
  5309. }));
  5310. });
  5311. return tracks;
  5312. }); else if (url.toLowerCase().includes('mora.jp/') ) return loadMoraMetadata(url).then(function(packageMeta) {
  5313. if (prefs.diag_mode) console.debug('Mora.jp metadata loaded:', packageMeta);
  5314. if ([7].includes(packageMeta.mediaType)) throw 'Not music release (' + packageMeta.mediaType + ')';
  5315. artist = fmtKanaProp(packageMeta, 'artistName');
  5316. isVA = vaParser.test(artist);
  5317. album = fmtKanaProp(packageMeta, 'title');
  5318. if (packageMeta.bitPerSample) bd = parseInt(packageMeta.bitPerSample);
  5319. if (packageMeta.samplingFreq) sr = parseInt(packageMeta.samplingFreq);
  5320. if (packageMeta.channelConf) channels = parseInt(packageMeta.channelConf);
  5321. if (packageMeta.materialNo) identifiers.MORA_ID = parseInt(packageMeta.materialNo);
  5322. if (packageMeta.msin) identifiers.MSIN = packageMeta.msin;
  5323. if (packageMeta.distPartNo) identifiers.DISTPARTNO = packageMeta.distPartNo;
  5324. if (packageMeta.fullsizeimage) imgUrl = packageMeta.packageUrl + packageMeta.fullsizeimage;
  5325. return packageMeta.trackList.map(function(track) {
  5326. trackIdentifiers = { TACK_ID: track.musicId, MSIN: track.msin, DISTPARTNO: track.distPartNo };
  5327. if (track.labelId) trackIdentifiers.LABEL_ID = track.labelId;
  5328. trackArtist = fmtKanaProp(track, 'artistName');
  5329. composer = fmtKanaProp(track, 'composer');
  5330. var trackLyricist = fmtKanaProp(track, 'lyrics');
  5331. if (trackLyricist) if (composer) composer += ' / ' + trackLyricist; else composer = trackLyricist;
  5332. switch (track.mediaFormatNo) {
  5333. case 10: format = 'AAC'; encoding = 'lossy'; var codecProfile = 'AAC-LC'; bitrate = 320; break;
  5334. //case 11: format = 'FLAC'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
  5335. case 12: format = 'FLAC'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
  5336. case 13: format = 'DSD'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
  5337. default: format = undefined; encoding = undefined; codecProfile = undefined; bitrate = undefined;
  5338. }
  5339. return {
  5340. artist: isVA ? VA : artist,
  5341. album: album,
  5342. //album_year: extractYear(releaseDate),
  5343. release_date: packageMeta.dispStartDate || packageMeta.dispStartDateStr || packageMeta.startDate,
  5344. label: packageMeta.labelcompanyname || packageMeta.displayLabelname || packageMeta.labelname,
  5345. catalog: packageMeta.cdPartNo || packageMeta.packageId || packageMeta.distPartNo,
  5346. media: media,
  5347. genre: genres.join('; '),
  5348. codec: format,
  5349. codec_profile: codecProfile,
  5350. encoding: encoding,
  5351. bitrate: /*track.bitPerSample * 1000 || */bitrate,
  5352. bd: parseInt(track.bitPerSample) || bd,
  5353. sr: parseInt(track.samplingFreq) || sr,
  5354. channels: parseInt(track.channelConf) || channels,
  5355. tracknumber: track.trackNo,
  5356. totaltracks: packageMeta.trackList.length,
  5357. composer: composer,
  5358. producer: fmtKanaProp(track, 'producer'),
  5359. arranger: fmtKanaProp(track, 'arranger'),
  5360. title: fmtKanaProp(track, 'title'),
  5361. track_artist: trackArtist && (isVA || trackArtist.toLowerCase() != artist.toLowerCase()) ? trackArtist : undefined,
  5362. duration: track.duration,
  5363. description: packageMeta.metaDescription,
  5364. url: packageMeta.webUrl,
  5365. cover_url: imgUrl,
  5366. identifiers: mergeIds(),
  5367. master: packageMeta.master,
  5368. };
  5369. });
  5370.  
  5371. function fmtKanaProp(obj, propName) {
  5372. var result = (obj[propName] || '').trim(), kana = (obj[propName.concat('Kana')] || '').trim();
  5373. if (kana && prefs.use_kana) if (result) result += ' ('.concat(kana, ')'); else result = kana;
  5374. return result || undefined;
  5375. }
  5376. }); else if (url.toLowerCase().includes('allmusic.com/album/')) return globalFetch(url.replace(/\b(m[wr]\d{10})\b.+$/, '$1')).then(function(response) {
  5377. ref = response.document.querySelector('section.main-album a.album-title');
  5378. var mainAlbum = (ref != null ? globalFetch(ref.href).then((response, ref) => ({
  5379. artist: Array.from(response.document.querySelectorAll('h2[class$="album-artist"] > span[itemprop="name"]'))
  5380. .map(span => span.textContent.trim()),
  5381. album: (ref = response.document.querySelector('h1.album-title')) != null ? ref.textContent.trim() : undefined,
  5382. albumYear: (ref = response.document.querySelector('div.release-date > span')) != null ?
  5383. new Date(ref.textContent).getFullYear() || parseInt(ref.textContent) : undefined,
  5384. genres: Array.from(response.document.querySelectorAll('div.genre a')).map(a => a.textContent.trim()),
  5385. styles: Array.from(response.document.querySelectorAll('div.styles a')).map(a => a.textContent.trim()),
  5386. coverUrl: (ref = response.document.querySelector('meta[property="og:image"]')) != null ?
  5387. ref.content.replace(/\bf=\d+\b/, 'f=0') : undefined,
  5388. id: /\b(mw\d{10})\b/.test(response.finalUrl) && RegExp.$1 || undefined,
  5389. })) : Promise.reject(null)).catch(reason => ({}));
  5390. var _credits = { mainArtists: [], featured: [], credits: {} };
  5391. var credits = globalFetch(response.finalUrl.concat('/credits')).then(function(response) {
  5392. response.document.querySelectorAll('section.credits > table > tbody > tr').forEach(function(tr) {
  5393. var name = tr.children[0].textContent.trim(), role = tr.children[1].textContent.trim();
  5394. if (role == 'Primary Artist') _credits.mainArtists.push(name);
  5395. else if (role == 'Featured Artist') _credits.featured.push(name);
  5396. else _credits.credits[name] = role;
  5397. });
  5398. return _credits;
  5399. }).catch(reason => _credits);
  5400. if (/\b(m[wr]\d{10})\b/.test(response.finalUrl)) identifiers.ALLMUSIC_ID = RegExp.$1;
  5401. artist = Array.from(response.document.querySelectorAll('h2[class$="-artist"] > span[itemprop="name"]'))
  5402. .map(span => span.textContent.trim());
  5403. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5404. album = (ref = response.document.querySelector('h1.release-title')
  5405. || response.document.querySelector('h1.album-title')) != null ? ref.textContent.trim() : undefined;
  5406. albumYear = (ref = response.document.querySelector('div.year')) != null ? parseInt(ref.textContent) : undefined;
  5407. ref = response.document.querySelector('div.release-date > span');
  5408. if (identifiers.ALLMUSIC_ID && identifiers.ALLMUSIC_ID.startsWith('mr')) {
  5409. releaseDate = ref.textContent.trim();
  5410. } else if (identifiers.ALLMUSIC_ID && identifiers.ALLMUSIC_ID.startsWith('mw')) {
  5411. albumYear = new Date(ref.textContent).getFullYear() || parseInt(ref.textContent) || albumYear;
  5412. }
  5413. label = Array.from(response.document.querySelectorAll('div.label a')).map(a => a.textContent.trim()).join(' / ');
  5414. catalogue = (ref = response.document.querySelector('div.catalog-number > span')) != null ? ref.textContent.trim() : undefined;
  5415. if ((ref = response.document.querySelector('div.format > span')) != null) media = ref.textContent.trim();
  5416. genres = Array.from(response.document.querySelectorAll('div.genre a')).map(a => a.textContent.trim());
  5417. var styles = Array.from(response.document.querySelectorAll('div.styles a')).map(a => a.textContent.trim());
  5418. getDescription(response, 'section.review', false);
  5419. var releaseInfo = [];
  5420. if ((ref = response.document.querySelector('div.recording-date > div')) != null)
  5421. releaseInfo.push('Recording date: '.concat(ref.textContent.trim()));
  5422. var locations = Array.from(response.document.querySelectorAll('div.recording-location > ul > li')).map(li => li.textContent.trim());
  5423. if (locations.length > 0) releaseInfo.push('Recording location: '.concat(locations.join(' / ')));
  5424. locations = Array.from(response.document.querySelectorAll('div.release-info > ul > li')).map(li => li.textContent.trim());
  5425. if (locations.length > 0) releaseInfo.push('Release info: '.concat(locations.join(', ')));
  5426. if (releaseInfo.length > 0) {
  5427. if (description) description += '\n\n';
  5428. description += releaseInfo.join('\n');
  5429. }
  5430. imgUrl = (ref = response.document.querySelector('meta[property="og:image"]')) != null ?
  5431. ref.content.replace(/\bf=\d+\b/, 'f=0') : undefined;
  5432. trs = response.document.querySelectorAll('section.track-listing table > tbody > tr.track');
  5433. return Promise.all([mainAlbum, credits]).then(function(workers) {
  5434. if (Object.keys(workers[1].credits).length > 0) {
  5435. if (description) description += '\n\n';
  5436. description = description.concat('[b]Credits:[/b]\n', Object.keys(workers[1].credits)
  5437. .map(artist => '\n'.concat(artist, ' - ', workers[1].credits[artist])));
  5438. }
  5439. return Array.from(trs).map(function(tr, ndx) {
  5440. trackArtist = Array.from(tr.querySelectorAll('td.performer div.primary > a')).map(a => a.textContent.trim());
  5441. var trackGuests = Array.from(tr.querySelectorAll('td.performer div.featuring > a')).map(a => a.textContent.trim());
  5442. if ((ref = tr.querySelector('div.title > a')) != null && ref.dataset.tooltip) try {
  5443. trackIdentifiers = { TRACK_ID: JSON.parse(ref.dataset.tooltip).id };
  5444. } catch(e) { trackIdentifiers = {} }
  5445. return {
  5446. artist: isVA ? VA : undefined,
  5447. artists: !isVA ? artist : undefined,
  5448. album: album,
  5449. release_date: releaseDate,
  5450. album_year: workers[0].albumYear || albumYear,
  5451. genre: (workers[0].genres || []).concat((workers[0].styles || []), genres, styles).join('; '),
  5452. label: label,
  5453. catalog: catalogue,
  5454. media: media,
  5455. discnumber: (ref = tr.parentNode.parentNode.parentNode.querySelector('h3')) != null
  5456. && /\b(?:Disc)\s+(\d+)\b/i.test(ref.textContent.trim()) ? parseInt(RegExp.$1) : undefined,
  5457. discsubtitle: (ref = tr.parentNode.querySelector('tr.performance-title')) != null ?
  5458. ref.textContent.trim() : undefined,
  5459. tracknumber: (ref = tr.querySelector('td.tracknum')) != null ? ref.textContent.trim() : undefined,
  5460. totaltracks: trs.length,
  5461. title: (ref = tr.querySelector('div.title')) != null ? ref.textContent.trim() : undefined,
  5462. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist) || trackGuests.length > 0) ?
  5463. trackArtist.concat(trackGuests) : undefined,
  5464. composers: Array.from(tr.querySelectorAll('div.composer > *')).map(node => node.textContent.trim()) || undefined,
  5465. duration: (ref = tr.querySelector('td.time')) != null && timeStringToTime(ref.textContent) || undefined,
  5466. description: description || undefined,
  5467. url: !identifiers.ALLMUSIC_ID ?
  5468. (ref = tr.querySelector('meta[property="og:url"]')) != null ? ref.content : response.finalUrl : undefined,
  5469. cover_url: workers[0].coverUrl || imgUrl,
  5470. identifiers: mergeIds(),
  5471. };
  5472. });
  5473. });
  5474. });
  5475. if (!weak) clipBoard.value = '';
  5476. return Promise.reject(new URL(url).hostname + ' not supported');
  5477.  
  5478. function mergeIds() {
  5479. var r = Object.assign({}, identifiers, trackIdentifiers);
  5480. trackIdentifiers = {};
  5481. return r;
  5482. }
  5483.  
  5484. function getDescription(response, selector, quote = false) {
  5485. description = [];
  5486. response.document.querySelectorAll(selector).forEach(function(node) {
  5487. var p = html2php(node, response.finalUrl).trim();
  5488. if (p) description.push(p);
  5489. });
  5490. description = description.join('\n\n').collapseGaps();
  5491. if (quote && description.length > 0 && !description.includes('[quote]')) {
  5492. description = '[quote]' + description + '[/quote]';
  5493. }
  5494. }
  5495.  
  5496. function durationFromMeta(elem) {
  5497. var m = elem.querySelector('meta[itemprop="duration"]');
  5498. if (m == null) return undefined;
  5499. if (/^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.test(m.content))
  5500. return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
  5501. m = timeStringToTime(m.content);
  5502. return m != null ? m : undefined;
  5503. }
  5504.  
  5505. function guessDiscNumber() {
  5506. if (discParser.test(discSubtitle)) {
  5507. discSubtitle = undefined;
  5508. discNumber = parseInt(RegExp.$1);
  5509. }
  5510. }
  5511. } // fetchOnline_Music
  5512.  
  5513. function parseLastFm(album) {
  5514. if (typeof album != 'object') return Promise.reject('invalid object')
  5515. var identifiers = {}, description = [];
  5516. if (album.id) identifiers.LASTFM_ID = album.id;
  5517. if (album.mbid) identifiers.MBID = album.mbid;
  5518. if (album.wiki && album.wiki.summary) description.push(album.wiki.summary);
  5519. if (album.wiki && album.wiki.content) description.push(album.wiki.content);
  5520. var genres = album.tags.tag.map(tag => tag.name);
  5521. description = description.join('\n\n');
  5522. var imgUrl = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
  5523. return acc || album.image.find(image => image.size == size && urlParser.test(image['#text']));
  5524. }, undefined);
  5525. if (imgUrl) imgUrl = imgUrl['#text'].replace(/\/\d+(?:x\d+|s)\//i, '/');
  5526. return album.tracks.track.map((track, ndx) => ({
  5527. artist: album.artist,
  5528. album: album.name,
  5529. genre: genres.join('; ') || undefined,
  5530. title: track.name,
  5531. tracknumber: ndx + 1,
  5532. track_artist: track.artist.name != album.artist ? track.artist.name : undefined,
  5533. duration: parseFloat(track.duration) || undefined,
  5534. url: album.url,
  5535. description: description || undefined,
  5536. identifiers: identifiers,
  5537. cover_url: imgUrl,
  5538. }));
  5539. }
  5540.  
  5541. function joinArtists(arr, decorator = artist => artist) {
  5542. if (!Array.isArray(arr)) return null;
  5543. if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
  5544. if (arr.length < 3) return arr.map(decorator).join(' & ');
  5545. return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
  5546. }
  5547.  
  5548. function loadMoraMetadata(webUrl) {
  5549. return /^(?:https?):\/\/(?:\w+\.)*mora\.jp\/package\//i.test(webUrl) ? globalFetch(webUrl).then(function(response1) {
  5550. var appArguments = response1.document.querySelector('meta[name="msApplication-Arguments"][content]');
  5551. if (appArguments == null) return Promise.reject('Mora.jp: unexpected page format');
  5552. appArguments = JSON.parse(appArguments.content);
  5553. var materialNo = appArguments.materialNo.toString().padStart(10, '0'), offset = 0;
  5554. var packageUrl = 'https://cf.mora.jp/contents/'.concat([
  5555. appArguments.type, appArguments.mountPoint, appArguments.labelId,
  5556. ].concat([4, 3, 3].map(length => materialNo.slice(offset, offset += length))).join('/'), '/');
  5557. return globalFetch(packageUrl.concat('packageMeta.jsonp'), { responseType: 'text' }).then(function(response2) {
  5558. return /^\s*\w+\(\s*(\{[\S\s]+\})\s*\);\s*$/.test(response2.responseText) ? Object.assign(JSON.parse(RegExp.$1), {
  5559. mountPoint: appArguments.mountPoint,
  5560. webUrl: response1.finalUrl.replace(/[\?\#].*$/, ''),
  5561. }) : Promise.reject('Mora.jp: Unexpected package meta format');
  5562. });
  5563. }) : Promise.reject('Not mora.jp site URL');
  5564. }
  5565.  
  5566. function splitArtists(str, parsers = multiArtistParsers) {
  5567. var result = [str];
  5568. parsers.forEach(function(parser) {
  5569. for (var i = result.length; i > 0; --i) {
  5570. var j = result[i - 1].split(parser).map(strip);
  5571. if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
  5572. && !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
  5573. }
  5574. });
  5575. return result;
  5576. }
  5577.  
  5578. function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
  5579. function looksLikeTrueName(artist, index = 0) {
  5580. return twoOrMore(artist)
  5581. && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
  5582. && artist.split(/\s+/).length >= 2
  5583. && !pseudoArtistParsers.some(rx => rx.test(artist)) || typeof getSiteArtist(artist) == 'object';
  5584. }
  5585.  
  5586. function strip(art) {
  5587. return artistStrips.reduce(function(acc, rx, ndx) {
  5588. return ndx != 1 || rx.test(acc) && !notMonospaced(RegExp.$1) ? acc.replace(rx, '') : acc;
  5589. }, art);
  5590. }
  5591.  
  5592. function getSiteArtist(artist) {
  5593. //if (isOPS) return undefined;
  5594. if (!artist || notSiteArtistsCache.includesCaseless(artist)) return null;
  5595. var key = Object.keys(siteArtistsCache).find(it => it.toLowerCase() == artist.toLowerCase());
  5596. if (key) return siteArtistsCache[key];
  5597. var now = Date.now();
  5598. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  5599. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  5600. apiTimeFrame.timeStamp = now;
  5601. apiTimeFrame.requestCounter = 1;
  5602. } else ++apiTimeFrame.requestCounter;
  5603. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  5604. if (apiTimeFrame.requestCounter > 5) {
  5605. console.debug('getSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
  5606. artist + '" (' + apiTimeFrame.requestCounter + ')');
  5607. if (prefs.messages_verbosity >= 2) addMessage('AJAX API request exceeding time frame: artistname="' +
  5608. artist + '" (' + apiTimeFrame.requestCounter + ')', 'notice');
  5609. ++ajaxRejects;
  5610. return undefined;
  5611. }
  5612. try {
  5613. var requestUrl = '/ajax.php?action=artist&artistname='.concat(encodeURIComponent(artist));
  5614. xhr.open('GET', requestUrl, false);
  5615. if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
  5616. xhr.send();
  5617. if (xhr.status == 404) {
  5618. notSiteArtistsCache.pushUniqueCaseless(artist);
  5619. return null;
  5620. }
  5621. if (xhr.readyState != XMLHttpRequest.DONE || xhr.status < 200 || xhr.status >= 400) {
  5622. console.log('getSiteArtist("' + artist + '") status:', xhr.status, 'url:', document.location.origin.concat(requestUrl));
  5623. return undefined; // error
  5624. }
  5625. let response = JSON.parse(xhr.responseText);
  5626. if (response.status != 'success') {
  5627. notSiteArtistsCache.pushUniqueCaseless(artist);
  5628. return null;
  5629. }
  5630. return (siteArtistsCache[artist] = response.response);
  5631. } catch(e) {
  5632. console.error('UA::getSiteArtist("' + artist + '"):', e, xhr);
  5633. return undefined;
  5634. }
  5635. }
  5636. } // fillFromText_Music
  5637.  
  5638. function fillFromText_Apps(weak = false) {
  5639. if (messages != null) messages.parentNode.removeChild(messages);
  5640. if (!urlParser.test(clipBoard.value)) {
  5641. addMessage('valid URL accepted for this category', 'critical');
  5642. return false;
  5643. }
  5644. sourceUrl = RegExp.$1;
  5645. var description, tags = new TagManager();
  5646. if (sourceUrl.toLowerCase().includes('://sanet')) return globalFetch(sourceUrl).then(function(response) {
  5647. i = response.document.querySelector('h1.item_title > span');
  5648. var title = i == null ? undefined : i.textContent
  5649. .replace(/\s+\((?:x|ia|em)(?:64)\)/ig, ' (64-bit)')
  5650. .replace(/\s+\(x(?:86|32)\)/ig, ' (32-bit)')
  5651. .replace(/\s+(?:Build)\s+(\d+)\b/g, ' build $1')
  5652. .replace(/\s+(?:Multilingual|Multi(?:-|\s)*lang(?:uage)?)\b/g, ' multilingual');
  5653. description = html2php(response.document.querySelector('section.descr'), response.finalUrl).trim();
  5654. if (/\s*^[ \t]*(?:\[i\]\[\/i\])?Homepage\s*$.*/im.test(description)) description = RegExp.leftContext;
  5655. description = description.split(/[ \t]*\r?\n/).slice(6).map(line => line.trim()).join('\n')
  5656. .replace(/^[ \t]*(?:\[i\]\[\/i\])?Screenshots:?\s*/igm, '')
  5657. .replace(/^[ \t]*(?:\[i\]\[\/i\])?(\[b\]Release\s+Notes:?\[\/b\])(?:[ \t]*\r?\n)+/igm, '$1\n')
  5658. .replace(/\[hr\]/ig, '\n');
  5659. ref = response.document.querySelector('section.descr > div.release-info');
  5660. var releaseInfo = ref != null && ref.textContent.trim();
  5661. if (/\b(?:Languages?)\s*:\s*(.*?)\s*(?:$|\|)/i.exec(releaseInfo) != null)
  5662. description += '\n\n[b]Languages:[/b]\n' + RegExp.$1;
  5663. if ((ref = response.document.querySelector('div.txtleft > a')) != null) {
  5664. description += '\n\n[b]Product page:[/b]\n[url]' +
  5665. removeRedirect(ref.pathname.toLowerCase().startsWith('/confirm/url/') && urlParser.test(ref.textContent) ?
  5666. ref.textContent.trim() : ref.href) + '[/url]';
  5667. }
  5668. writeDescription(description.collapseGaps());
  5669. if ((ref = response.document.querySelector('section.descr > div.center > a.mfp-image')) != null) {
  5670. setCover(ref.href);
  5671. } else {
  5672. ref = response.document.querySelector('section.descr > div.center > img[data-src]');
  5673. if (ref != null) setCover(ref.dataset.src);
  5674. }
  5675. var internalTags = Array.from(response.document.querySelectorAll('ul.item_tags_list > li > a[rel="tag"]'))
  5676. .map(elem => elem.textContent.toLowerCase().trim());
  5677. if ((ref = response.document.querySelector('a.cat:last-of-type > span')) != null) {
  5678. if (ref.textContent.toLowerCase() == 'windows') {
  5679. tags.add('apps.windows');
  5680. if (/\b(?:(?:x|ia|em)64)\b/i.test(releaseInfo) || /\(64[-\s]*bit\)/i.test(title)) tags.add('win64');
  5681. if (/\b(?:x86|x32)\b/i.test(releaseInfo) || /\(32[-\s]*bit\)/i.test(title)) tags.add('win32');
  5682. }
  5683. if (ref.textContent.toLowerCase() == 'macos') tags.add('apps.mac');
  5684. if (ref.textContent.toLowerCase() == 'linux' || ref.textContent.toLowerCase() == 'unix') tags.add('apps.linux');
  5685. if (ref.textContent.toLowerCase() == 'android') tags.add('apps.android');
  5686. if (ref.textContent.toLowerCase() == 'ios') tags.add('apps.ios');
  5687. }
  5688. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  5689. if (title && !/\(\d+-?bit\)/i.test(title)) {
  5690. if (tags.includes('win64') && !tags.includes('win32')) title += ' (64-bit)';
  5691. if (tags.includes('win32') && !tags.includes('win64')) title += ' (32-bit)';
  5692. }
  5693. if (elementWritable(ref = document.getElementById('title'))) ref.value = title || '';
  5694. });
  5695. if (!weak) {
  5696. addMessage('this domain not supported', 'critical');
  5697. clipBoard.value = '';
  5698. }
  5699. return Promise.reject('this domain not supported');
  5700. } // fillFromText_Apps
  5701.  
  5702. function fillFromText_Ebooks(weak = false) {
  5703. if (messages != null) messages.parentNode.removeChild(messages);
  5704. if (!urlParser.test(clipBoard.value)) {
  5705. addMessage('only URL accepted for this category', 'critical');
  5706. return Promise.reject('only URL accepted for this category');
  5707. }
  5708. sourceUrl = RegExp.$1;
  5709. var description, tags = new TagManager();
  5710. if (sourceUrl.toLowerCase().includes('martinus.cz') || sourceUrl.toLowerCase().includes('martinus.sk'))
  5711. return globalFetch(sourceUrl).then(function(response) {
  5712. function get_detail(x, y) {
  5713. var ref = response.document.querySelector('section#details > div > div > div:first-of-type > div:nth-child(' +
  5714. x + ') > dl:nth-child(' + y + ') > dd');
  5715. return ref != null ? ref.textContent.trim() : null;
  5716. }
  5717.  
  5718. i = response.document.querySelectorAll('article > ul > li > a');
  5719. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  5720. description = joinAuthors(i);
  5721. if ((i = response.document.querySelector('article > h1')) != null) description += ' – ' + i.textContent.trim();
  5722. i = response.document.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
  5723. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  5724. ref.value = description;
  5725. }
  5726.  
  5727. ref = response.document.querySelector('section#description > div');
  5728. if (ref != null) description = html2php(ref).replace(/^\s*\[img\].*?\[\/img\]\s*/i, '').trim();
  5729. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  5730. const translation_map = [
  5731. [/\b(?:originál)/i, 'Original title'],
  5732. [/\b(?:datum|dátum|rok)\b/i, 'Release date'],
  5733. [/\b(?:katalog|katalóg)/i, 'Catalogue #'],
  5734. [/\b(?:stran|strán)\b/i, 'Page count'],
  5735. [/\bjazyk/i, 'Language'],
  5736. [/\b(?:nakladatel|vydavatel)/i, 'Publisher'],
  5737. [/\b(?:doporuč|ODPORÚČ)/i, 'Age rating'],
  5738. ];
  5739. response.document.querySelectorAll('section#details > div > div > div:first-of-type > div > dl').forEach(function(detail) {
  5740. var lbl = detail.children[0].textContent.trim();
  5741. var val = detail.children[1].textContent.trim();
  5742. if (/\b(?:rozm)/i.test(lbl) || /\b(?:vazba|vázba)\b/i.test(lbl)) return;
  5743. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  5744. if (/\b(?:ISBN)\b/i.test(lbl)) {
  5745. sourceUrl = new URL('https://www.worldcat.org/isbn/'.concat(detail.children[1].textContent.trim()));
  5746. val = '[url=' + sourceUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
  5747. findOCLC(sourceUrl);
  5748. // } else if (/\b(?:ISBN)\b/i.test(lbl)) {
  5749. // val = '[url=https://www.goodreads.com/search/search?q=' + detail.children[1].textContent.trim() +
  5750. // '&search_type=books]' + detail.children[1].textContent.trim() + '[/url]';
  5751. }
  5752. description += '\n[b]' + lbl + ':[/b] ' + val;
  5753. });
  5754. sourceUrl = new URL(response.finalUrl);
  5755. description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.href + '[/url]';
  5756. writeDescription(description.collapseGaps());
  5757.  
  5758. if ((i = response.document.querySelector('a.mj-product-preview > img')) != null) {
  5759. setCover(i.src.replace(/\?.*/, ''));
  5760. } else if ((i = response.document.querySelector('head > meta[property="og:image"][content]')) != null) {
  5761. setCover(i.content.replace(/\?.*/, ''));
  5762. }
  5763.  
  5764. response.document.querySelectorAll('dd > ul > li > a').forEach(x => { tags.add(x.textContent) });
  5765. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) {
  5766. ref.value = tags.toString();
  5767. }
  5768. });
  5769. else if (sourceUrl.toLowerCase().includes('goodreads.com')) return globalFetch(sourceUrl).then(function(response) {
  5770. i = response.document.querySelectorAll('a.authorName > span');
  5771. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  5772. description = joinAuthors(i);
  5773. if ((i = response.document.querySelector('h1#bookTitle')) != null) description += ' – ' + i.textContent.trim();
  5774. if ((i = response.document.querySelector('div#details > div.row:nth-of-type(2)')) != null
  5775. && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  5776. ref.value = description;
  5777. }
  5778.  
  5779. var description = [];
  5780. response.document.querySelectorAll('div#description span:last-of-type').forEach(function(node) {
  5781. description = html2php(node, sourceUrl).trim();
  5782. });
  5783. if (description.length > 0 && !description.includes('[quote]')) {
  5784. description = '[quote]' + description.trim() + '[/quote]';
  5785. }
  5786.  
  5787. function strip(str) {
  5788. return typeof str == 'string' ?
  5789. str.replace(/\s{2,}/g, ' ').replace(/[\n\r]+/, '').replace(/\s*\.{3}(?:less|more)\b/g, '').trim() : null;
  5790. }
  5791.  
  5792. response.document.querySelectorAll('div#details > div.row').forEach(k => { description += '\n' + strip(k.innerText) });
  5793. description += '\n';
  5794.  
  5795. response.document.querySelectorAll('div#bookDataBox > div.clearFloats').forEach(function(detail) {
  5796. var lbl = detail.children[0].textContent.trim();
  5797. var val = strip(detail.children[1].textContent);
  5798. if (/\b(?:ISBN)\b/i.test(lbl) && (/\b(\d{13})\b/.test(val) || /\b(\d{10})\b/.test(val))) {
  5799. sourceUrl = new URL('https://www.worldcat.org/isbn/'.concat(RegExp.$1));
  5800. val = '[url=' + sourceUrl.href + ']' + strip(detail.children[1].textContent) + '[/url]';
  5801. findOCLC(sourceUrl);
  5802. }
  5803. description += '\n[b]' + lbl + ':[/b] ' + val;
  5804. });
  5805. if ((ref = response.document.querySelector('span[itemprop="ratingValue"]')) != null) {
  5806. description += '\n[b]Rating:[/b] ' + Math.round(parseFloat(ref.firstChild.textContent) * 20) + '%';
  5807. }
  5808. sourceUrl = new URL(response.finalUrl);
  5809. // if ((ref = response.document.querySelector('div#buyButtonContainer > ul > li > a.buttonBar')) != null) {
  5810. // let u = new URL(ref.href);
  5811. // description += '\n[url=' + sourceUrl.origin + u.pathname + '?' + u.search + ']Libraries[/url]';
  5812. // }
  5813. description += '\n\n[b]More info and reviews:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
  5814. response.document.querySelectorAll('div.clearFloats.bigBox').forEach(function(bigBox) {
  5815. if (bigBox.id == 'aboutAuthor' && (ref = bigBox.querySelector('h2 > a')) != null) {
  5816. description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  5817. if ((ref = bigBox.querySelector('div.bigBoxBody a > div[style*="background-image"]')) != null) {
  5818. }
  5819. if ((ref = bigBox.querySelector('div.bookAuthorProfile__about > span[id]:last-of-type')) != null) {
  5820. description += '\n' + html2php(ref, sourceUrl).trim().replace(/^\[i\]Librarian\s+Note:.*?\[\/i\]\s+/i, '');
  5821. }
  5822. } else if ((ref = bigBox.querySelector('h2 > a[href^="/trivia/"]')) != null) {
  5823. description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  5824. if ((ref = bigBox.querySelector('div.bigBoxContent > div.mediumText')) != null) {
  5825. description += '\n' + ref.firstChild.textContent.trim();
  5826. }
  5827. // } else if ((ref = bigBox.querySelector('h2 > a[href^="/work/quotes/"]')) != null) {
  5828. // description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  5829. // bigBox.querySelectorAll('div.bigBoxContent > div.stacked > span.readable').forEach(function(quote) {
  5830. // description += '\n' + ref.firstChild.textContent.trim();
  5831. // });
  5832. }
  5833. });
  5834. writeDescription(description.collapseGaps());
  5835. if ((ref = response.document.querySelector('div.editionCover > img')) != null) setCover(ref.src.replace(/\?.*/, ''));
  5836. response.document.querySelectorAll('div.elementList > div.left').forEach(tag => { tags.add(tag.textContent.trim()) });
  5837. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  5838. }); else if (sourceUrl.toLowerCase().includes('databazeknih.cz')) {
  5839. if (!sourceUrl.toLowerCase().includes('show=alldesc')) {
  5840. if (!sourceUrl.includes('?')) { sourceUrl += '?show=alldesc' } else { sourceUrl += '&show=alldesc' }
  5841. }
  5842. return globalFetch(sourceUrl).then(function(response) {
  5843. i = response.document.querySelectorAll('span[itemprop="author"] > a');
  5844. if (i != null && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  5845. description = joinAuthors(i);
  5846. if ((i = response.document.querySelector('h1[itemprop="name"]')) != null)
  5847. description += ' – ' + i.textContent.trim();
  5848. i = response.document.querySelector('span[itemprop="datePublished"]');
  5849. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  5850. ref.value = description;
  5851. }
  5852.  
  5853. ref = response.document.querySelector('p[itemprop="description"]');
  5854. if (ref != null) description = html2php(ref, sourceUrl).trim();
  5855. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  5856. const translation_map = [
  5857. [/\b(?:orig)/i, 'Original title'],
  5858. [/\b(?:série)\b/i, 'Series'],
  5859. [/\b(?:vydáno)\b/i, 'Released'],
  5860. [/\b(?:stran)\b/i, 'Page count'],
  5861. [/\b(?:jazyk)\b/i, 'Language'],
  5862. [/\b(?:překlad)/i, 'Translation'],
  5863. [/\b(?:autor obálky)\b/i, 'Cover author'],
  5864. ];
  5865. response.document.querySelectorAll('table.bdetail tr').forEach(function(detail) {
  5866. var lbl = detail.children[0].textContent.trim();
  5867. var val = detail.children[1].textContent.trim();
  5868. if (/(?:žánr|\bvazba)\b/i.test(lbl)) return;
  5869. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  5870. if (/\b(?:ISBN)\b/i.test(lbl) && /\b(\d+(?:-\d+)*)\b/.exec(val) != null) {
  5871. sourceUrl = new URL('https://www.worldcat.org/isbn/'.concat(RegExp.$1.replace(/-/g, '')));
  5872. val = '[url=' + sourceUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
  5873. findOCLC(sourceUrl);
  5874. }
  5875. description += '\n[b]' + lbl + '[/b] ' + val;
  5876. });
  5877.  
  5878. sourceUrl = new URL(response.finalUrl);
  5879. description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
  5880. writeDescription(description.collapseGaps());
  5881.  
  5882. if ((ref = response.document.querySelector('div#icover_mid > a')) != null) setCover(ref.href.replace(/\?.*/, ''));
  5883. if ((ref = response.document.querySelector('div#lbImage')) != null && /\burl\("(.*)"\)/i.test(i.style.backgroundImage)) {
  5884. setCover(RegExp.$1.replace(/\?.*/, ''));
  5885. }
  5886.  
  5887. response.document.querySelectorAll('h5[itemprop="genre"] > a').forEach(tag => { tags.add(tag.textContent.trim()) });
  5888. response.document.querySelectorAll('a.tag').forEach(tag => { tags.add(tag.textContent.trim()) });
  5889. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  5890. });
  5891. }
  5892. if (!weak) {
  5893. addMessage('domain not supported', 'critical');
  5894. clipBoard.value = '';
  5895. }
  5896. return Promise.reject('domain not supported');
  5897.  
  5898. function joinAuthors(nodeList) {
  5899. if (typeof nodeList != 'object') return null;
  5900. return Array.from(nodeList).map(it => it.textContent.trim()).join(' & ');
  5901. }
  5902.  
  5903. function findOCLC(url) {
  5904. if (!url) return false;
  5905. var oclc = document.querySelector('input[name="oclc"]');
  5906. if (!elementWritable(oclc)) return false;
  5907. globalFetch(url).then(function(dom) {
  5908. var ref = dom.querySelector('tr#details-oclcno > td:last-of-type');
  5909. if (ref != null) oclc.value = ref.textContent.trim();
  5910. });
  5911. return true;
  5912. }
  5913. } // fillFromText_Ebooks
  5914.  
  5915. function preview(n) {
  5916. if (!prefs.auto_preview) return;
  5917. var btn = document.querySelector('input.button_preview_' + n + '[type="button"][value="Preview"]');
  5918. if (btn != null) btn.click();
  5919. }
  5920.  
  5921. function writeDescription(desc) {
  5922. if (typeof desc != 'string') return;
  5923. if (elementWritable(ref = document.querySelector('textarea#desc')
  5924. || document.querySelector('textarea#description') || document.querySelector('textarea#album_desc'))) ref.value = desc;
  5925. if ((ref = document.getElementById('body')) != null && !ref.disabled) {
  5926. if (ref.value.length > 0) ref.value += '\n\n';
  5927. ref.value += desc;
  5928. }
  5929. }
  5930.  
  5931. function loadGoogleData(response) {
  5932. const initDataParser = /\b(?:AF_initDataCallback)\s*\(\s*\{\s*key:\s*'ds:(\d+)'.*data:\s*function\(\)\s*{\s*return\s*([\S\s]+)\}\s*\}\s*\);/;
  5933. return Array.from(response.document.querySelectorAll('script[nonce]'))
  5934. .map(function(script) { try { return eval(initDataParser.exec(script.text)[2]) } catch(e) { return false } })
  5935. .filter(obj => obj && typeof obj == 'object');
  5936. }
  5937.  
  5938. function queryItunesAPI(key, params) {
  5939. return queryGenericAPI('itunes.apple.com', key, params);
  5940. }
  5941. function queryDeezerAPI(key, params) {
  5942. return queryGenericAPI('api.deezer.com', key, params);
  5943. }
  5944. function queryDiscogsAPI(key, params) {
  5945. if (prefs.discogs_key && prefs.discogs_secret) {
  5946. var hdr = { Authorization: 'Discogs key=' + prefs.discogs_key + ', secret=' + prefs.discogs_secret };
  5947. } else if (discogs_token) hdr = { Authorization: 'Discogs token=' + discogs_token };
  5948. return queryGenericAPI('api.discogs.com', key, params, hdr);
  5949. }
  5950. function queryMusicBrainzAPI(key, params) {
  5951. return queryGenericAPI('musicbrainz.org', 'ws/2/' + key + '/', Object.assign({ fmt: 'json' }, params));
  5952. }
  5953. function querySpotifyAPI(key, params) {
  5954. return key ? setOauth2Token().then(credentials => queryGenericAPI('api.spotify.com', 'v1/' + key, params, {
  5955. Authorization: credentials.token_type + ' ' + credentials.access_token,
  5956. })) : Promise.reject('No API keyword');
  5957.  
  5958. function setOauth2Token() {
  5959. try { var accessToken = JSON.parse(window.localStorage.spotifyAccessToken) } catch(e) { }
  5960. if (isTokenValid(accessToken)) {
  5961. if (prefs.diag_mode) console.debug('Re-used Spotify access token:', accessToken, new Date(accessToken.expires).toLocaleTimeString());
  5962. return Promise.resolve(accessToken);
  5963. }
  5964. if (!spotify_clientid || !spotify_clientsecret) return Promise.reject('Spotify credentials not configured');
  5965. const data = new URLSearchParams({
  5966. 'grant_type': 'client_credentials',
  5967. });
  5968. return globalFetch('https://accounts.spotify.com/api/token', { responseType: 'json', headers: {
  5969. Authorization: 'Basic ' + btoa(spotify_clientid + ':' + spotify_clientsecret),
  5970. } }, data).then(function(response) {
  5971. accessToken = response.response;
  5972. accessToken.expires = Date.now() + accessToken.expires_in * 1000;
  5973. if (!isTokenValid(accessToken)) return Promise.reject('Invalid token received');
  5974. delete accessToken.expires_in;
  5975. window.localStorage.spotifyAccessToken = JSON.stringify(accessToken);
  5976. if (prefs.diag_mode) console.debug('Spotify access token successfully set:', accessToken);
  5977. return accessToken;
  5978. });
  5979. }
  5980.  
  5981. function isTokenValid(accessToken) {
  5982. return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
  5983. && accessToken.expires >= Date.now() + 30000;
  5984. }
  5985. }
  5986. function queryLastFmAPI(method, params) {
  5987. return lastfm_api_key ? queryGenericAPI('ws.audioscrobbler.com', '2.0/', Object.assign({
  5988. method: method,
  5989. api_key: lastfm_api_key,
  5990. format: 'json',
  5991. }, params || {})) : Promise.reject('Last.fm API key not configured');
  5992. }
  5993. function queryTidalAPI(key, params, countryCode) {
  5994. if (!key) return Promise.reject('API action not defined');
  5995. if (typeof params != 'object') params = {};
  5996. params.deviceType = 'BROWSER';
  5997. params.countryCode = countryCode;
  5998. return setOauth2Token().then(function(token) {
  5999. if (!params.countryCode) params.countryCode = token.user.countryCode || 'US';
  6000. return { Authorization: token.token_type + ' ' + token.access_token };
  6001. }).catch(function(reason) {
  6002. console.warn('Tidal Oauth2 failed:', reason);
  6003. return setSession().then(function(session) {
  6004. if (!params.countryCode) params.countryCode = session.countryCode || 'US';
  6005. return { 'X-Tidal-SessionId': session.sessionId };
  6006. });
  6007. }).then(header => queryGenericAPI('listen.tidal.com', 'v1/pages/'.concat(key), params, header));
  6008.  
  6009. function setOauth2Token() {
  6010. try { var accessToken = JSON.parse(window.localStorage.tidalAccessToken) } catch(e) { }
  6011. if (isTokenValid(accessToken)) {
  6012. if (prefs.diag_mode) console.debug('Re-used Tidal access token:', accessToken, new Date(accessToken.expires).toLocaleTimeString());
  6013. return Promise.resolve(accessToken);
  6014. }
  6015. return Promise.reject('Not implemented');
  6016. // TODO
  6017. }
  6018. function setSession() {
  6019. try { var session = JSON.parse(window.sessionStorage.tidalSession) } catch(e) { }
  6020. if (isSessionValid(session)) {
  6021. if (prefs.diag_mode) console.debug('Re-used Tidal session:', session);
  6022. return Promise.resolve(session);
  6023. }
  6024. if (!prefs.tidal_userid || !prefs.tidal_userpassword) return Promise.reject('Tidal user credentials not configured');
  6025. const deviceTokens = [
  6026. 'BI218mwp9ERZ3PFI', // browser | Streams lossless quality
  6027. 'wdgaB1CilGA-S_s2', // browser | Streams HIGH/LOW Quality over RTMP, FLAC and Videos over HTTP, but many Lossless Streams are encrypted.
  6028. '4zx46pyr9o8qZNRw', // browser(?) | other quality
  6029. 'kgsOOmYk3zShYrNP', // Android | All Streams are HTTP Streams. Correct numberOfVideos in Playlists (best Token to use)
  6030. 'GvFhCVAYp3n43EN3', // iOS | Same as Android Token, but uses ALAC instead of FLAC
  6031. '_DSTon1kC8pABnTw', // iOS | Same as Android Token, but uses ALAC instead of FLAC
  6032. '4zx46pyr9o8qZNRw', // native | Same as Android Token, but FLAC streams are encrypted
  6033. 'BI218mwp9ERZ3PFI', // audirvana | Like Android Token, supports MQA, but returns 'numberOfVideos = 0' in Playlists
  6034. 'wc8j_yBJd20zOmx0', // amarra | Like Android Token, but returns 'numberOfVideos = 0' in Playlists
  6035. 'P5Xbeo5LFvESeDy6', // Like Android Token, but returns 'numberOfVideos = 0' in Playlists
  6036. '_KM2HixcUBZtmktH', // Same as previous
  6037. 'oIaGpqT_vQPnTr0Q', // Same, but uses RTMP for HIGH/LOW Quality
  6038. ];
  6039. return getClientToken().then(function(token) {
  6040. const data = new URLSearchParams({
  6041. username: prefs.tidal_userid,
  6042. password: prefs.tidal_userpassword,
  6043. clientUniqueKey: getClientKey(),
  6044. clientVersion: '1.0',
  6045. token: deviceTokens[2],
  6046. });
  6047. return globalFetch('https://api.tidalhifi.com/v1/login/username', {
  6048. responseType: 'json',
  6049. headers: token ? { 'X-Tidal-Token': token } : undefined,
  6050. }, data);
  6051. }).then(function(response) {
  6052. if (!isSessionValid(session = response.response)) return Promise.reject('Invalid session');
  6053. window.sessionStorage.tidalSession = JSON.stringify(session);
  6054. if (prefs.diag_mode) console.debug('Tidal session successfully established:', session);
  6055. return session;
  6056. });
  6057. }
  6058. function uuidv4() {
  6059. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  6060. var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
  6061. return v.toString(16);
  6062. });
  6063. }
  6064. function randomString(length) {
  6065. const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  6066. var text = "";
  6067. for (var i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length));
  6068. return text;
  6069. }
  6070. function generateCodeChallenge(code_verifier) {
  6071. return code_challenge = base64URL(CryptoJS.SHA256(code_verifier))
  6072. }
  6073. function base64URL(string) {
  6074. return string.toString(CryptoJS.enc.Base64).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
  6075. }
  6076. function getClientId() {
  6077. if (prefs.tidal_clientid || (prefs.tidal_clientid = GM_getValue('tidal_clientid'))) return Promise.resolve(prefs.tidal_clientid);
  6078. return getTidalSecrets().then(function(response) {
  6079. const rx = /"(\w{40})":"(\w{16})"/g;
  6080. if ((i = response.responseText.match(rx)) == null || !rx.test(i.shift())) return Promise.reject('not found');
  6081. GM_setValue('tidal_clientid', prefs.tidal_clientid = RegExp.$2);
  6082. if (prefs.diag_mode) console.debug('Successfully configured Tidal client Id:', prefs.tidal_clientid);
  6083. return prefs.tidal_clientid;
  6084. }).catch(function(reason) {
  6085. reason = 'Client Id auto detection failed ('.concat(reason, '), set it manually (tidal_clientid)');
  6086. alert(reason);
  6087. return Promise.reject(reason);
  6088. });
  6089. }
  6090. function getClientKey() {
  6091. if (!prefs.tidal_clientkey && !(prefs.tidal_clientkey = GM_getValue('tidal_clientkey')))
  6092. GM_setValue('tidal_clientkey', prefs.tidal_clientkey = uuidv4());
  6093. return prefs.tidal_clientkey;
  6094. }
  6095. function getClientToken() {
  6096. if (prefs.tidal_token || (prefs.tidal_token = GM_getValue('tidal_token'))) return Promise.resolve(prefs.tidal_token);
  6097. return getTidalSecrets().then(function(response) {
  6098. if (!/"(\w{40})":"(\w{40})"/.test(response.responseText)) return Promise.reject('not found');
  6099. GM_setValue('tidal_token', prefs.tidal_token = RegExp.$2);
  6100. if (prefs.diag_mode) console.debug('Successfully configured Tidal token:', prefs.tidal_token);
  6101. return prefs.tidal_token;
  6102. }).catch(function(reason) {
  6103. console.warn('Tidal token detection fail (' + reason + ')');
  6104. return undefined;
  6105. });
  6106. }
  6107. function getTidalSecrets() {
  6108. return globalFetch('https://listen.tidal.com/app.61d2e10fa09e5075c5ad.chunk.js', { responseType: 'text' });
  6109. }
  6110. function isTokenValid(accessToken) {
  6111. return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
  6112. && accessToken.expires >= Date.now() + 30000;
  6113. }
  6114. function isSessionValid(session) {
  6115. return typeof session == 'object' && session.userId > 0 && session.sessionId;
  6116. }
  6117. }
  6118.  
  6119. function queryGenericAPI(domain, key, params, headers) {
  6120. //if (!key) return Promise.reject(new Error('Keyword missing'));
  6121. var retryCount = 0;
  6122. return new Promise(function(resolve, reject) {
  6123. var url = 'https://' + domain + '/' + key;
  6124. var query = new URLSearchParams(params || undefined).toString();
  6125. if (query.length > 0) url += '?' + query;
  6126. if (typeof headers != 'object') headers = {};
  6127. headers.Accept = 'application/json';
  6128. queryInternal();
  6129.  
  6130. function queryInternal() {
  6131. GM_xmlhttpRequest({
  6132. method: 'GET',
  6133. url: url,
  6134. responseType: 'json',
  6135. headers: headers,
  6136. onload: function(response) {
  6137. if (response.status >= 200 && response.status < 400) resolve(response.response);
  6138. else reject(defaultErrorHandler(response));
  6139. },
  6140. onerror: response => { reject(defaultErrorHandler(response)) },
  6141. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  6142. });
  6143. }
  6144. });
  6145. }
  6146.  
  6147. function loadItunesMetadata(urlOrId) {
  6148. return (function() {
  6149. return /^https:\/\/apple\.co\//i.test(urlOrId) ? urlResolver(urlOrId).then(url => getAppleId(url)) : getAppleId(urlOrId);
  6150.  
  6151. function getAppleId(urlOrId) {
  6152. var appleId = parseInt(urlOrId) || itunesRlsParser.test(urlOrId) && parseInt(RegExp.$1);
  6153. return appleId ? Promise.resolve(appleId) : Promise.reject('Aplpe Id cannot be determined');
  6154. }
  6155. })().then(appleId => globalFetch('https://music.apple.com/album/'.concat(appleId)).then(function(response) {
  6156. var params = response.document.querySelector('meta[name="desktop-music-app/config/environment"][content]');
  6157. if (params == null) return Promise.reject('Desktop environment not located');
  6158. params = JSON.parse(decodeURIComponent(params.content));
  6159. if (prefs.diag_mode) console.debug('Got Apple Music desktop environment:', params);
  6160. if (!params.MEDIA_API.token) return Promise.reject('Apple access token not found');
  6161. var query = new URLSearchParams({
  6162. include: 'tracks,artists',
  6163. l: 'en-US',
  6164. });
  6165. return globalFetch(params.MUSIC.BASE_URL.concat('/catalog/us/albums/', appleId, '?', query), { responseType: 'json', headers: {
  6166. 'Referer': response.finalUrl,
  6167. 'Authorization': 'Bearer '.concat(params.MEDIA_API.token),
  6168. } }).then(function(response2) {
  6169. var album = response2.response.data[0];
  6170. album.description = response.document.querySelector('div.content-modal__content-container')
  6171. || response.document.querySelector('div.product-page-header__notes span'),
  6172. album.url = response.finalUrl;
  6173. if (album.attributes.artwork) album.attributes.artwork.realUrl = album.attributes.artwork.url
  6174. .replace('{w}', album.attributes.artwork.width).replace('{h}', album.attributes.artwork.height);
  6175. if (prefs.diag_mode) console.debug('Apple Music metadata received:', album);
  6176. // query.set('include', 'artists,albums');
  6177. // Promise.all(album.relationships.tracks.data.map(track => globalFetch(params.MUSIC.BASE_URL.concat('/catalog/us/songs/', track.id, '?', query), { responseType: 'json', headers: {
  6178. // 'Referer': response.finalUrl,
  6179. // 'Authorization': 'Bearer '.concat(params.MEDIA_API.token),
  6180. // } }).then(response => response.response))).then(tracks => { console.debug('Apple Music tracks received:', tracks) })
  6181. // .catch(reason => { console.error(reason) });
  6182. return album;
  6183. });
  6184. }));
  6185. }
  6186.  
  6187. function queryHdtracksApi(urlOrId, entity) {
  6188. if (!urlOrId) return Promise.reject('invalid arguments');
  6189. if (/^(\w+)$/.test(urlOrId)) var id = RegExp.$1;
  6190. if (!id) try {
  6191. urlOrId = new URL(urlOrId);
  6192. if ((urlOrId.hostname.startsWith('hdtracks.') || urlOrId.hostname.startsWith('www.hdtracks.'))
  6193. && /^#\/(\w+)\/(\w+)\b/i.test(urlOrId.hash)) {
  6194. entity = RegExp.$1;
  6195. id = RegExp.$2;
  6196. }
  6197. } catch(e) { console.warn(e) }
  6198. if (!id || !entity) return Promise.reject('invalid arguments');
  6199. return setSession().then(function(session) {
  6200. urlOrId = `https://hdtracks.azurewebsites.net/api/v1/${entity}/${id}`;
  6201. if (Object.keys(session).length > 0) urlOrId += '&'.concat(new URLSearchParams(session));
  6202. return globalFetch(urlOrId, { responseType: 'json' })
  6203. .then(response => response.response.status.toLowerCase() == 'ok' ? response.response
  6204. : Promise.reject(response.response.status), reason => fetch(urlOrId).then(response => response.json()));
  6205. });
  6206.  
  6207. function setSession() {
  6208. return Promise.resolve({
  6209. //token: 123456789,
  6210. });
  6211. }
  6212. }
  6213.  
  6214. function getMusicBrainzCovers(mbid) {
  6215. return searchInternal('release', mbid).then(covers => covers || searchMaster(), searchMaster);
  6216.  
  6217. function searchInternal(entity, mbid) {
  6218. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  6219. method: 'GET',
  6220. url: 'https://coverartarchive.org/'.concat(entity, '/', mbid),
  6221. responseType: 'json',
  6222. onload: function(response) {
  6223. if (response.status == 404) return resolve(null);
  6224. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  6225. var images = response.response.images
  6226. .filter(image => urlParser.test(image.image) && image.isfront
  6227. || Array.isArray(image.types) && image.types.includesCaseless('Front'))
  6228. .map(image => image.image);
  6229. resolve(images.length > 0 ? [response.response.release, images] : null);
  6230. },
  6231. onerror: error => reject(defaultErrorHandler(error)),
  6232. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  6233. }));
  6234. }
  6235. function searchMaster() {
  6236. return queryMusicBrainzAPI('release/' + mbid, { inc: 'release-groups' })
  6237. .then(release => searchInternal('release-group', release['release-group'].id));
  6238. }
  6239. }
  6240.  
  6241. function setCover(url) {
  6242. if (!urlParser.test(url)) return Promise.reject('Image url not valid');
  6243. var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  6244. if (!elementWritable(image)) return Promise.reject('Image input not available');
  6245. return verifyImageUrl(url).then(function(imageUrl) {
  6246. if (!isNWCD) {
  6247. image.value = imageUrl;
  6248. coverPreview(image, imageUrl);
  6249. if (prefs.auto_rehost_cover && !imageUrl.toLowerCase().startsWith(ptpimgOrigin)) {
  6250. image.disabled = true;
  6251. return rehost2PTPIMG([imageUrl])
  6252. .then(urls => urls.length > 0 ? (image.value = urls[0]) : imageUrl)
  6253. .catch(reason => { /*alert(reason)*/ addMessage(reason.concat(' (not rehosted)'), 'warning') })
  6254. .then(url => { image.disabled = false; return url });
  6255. }
  6256. return imageUrl;
  6257. } else return uploadToImagehost(imageUrl).then(function(result) {
  6258. image.value = result.url;
  6259. setTimeout(function() { coverPreview(image, result.url) }, 2000);
  6260. return result.url;
  6261. });
  6262. });
  6263. }
  6264.  
  6265. function elementWritable(elem) {
  6266. return elem != null && !elem.disabled && (overwrite || elem.value == '' || !isRED && elem.value == '---');
  6267. }
  6268. } // fillFromText
  6269.  
  6270. function addMessage(text, cls) {
  6271. switch (cls) {
  6272. case 'info': var prefix = 'Info'; break;
  6273. case 'notice': prefix = 'Notice'; break;
  6274. case 'warning': prefix = 'Warning'; break;
  6275. case 'critical': prefix = 'FATAL'; break;
  6276. default: return null;
  6277. }
  6278. if ((messages = document.getElementById('UA-messages')) == null) {
  6279. let ua = document.getElementById('upload assistant');
  6280. if (ua == null) return null;
  6281. let tr = document.createElement('TR');
  6282. tr.id = 'UA-messages';
  6283. ua.firstElementChild.append(tr);
  6284. var td = document.createElement('TD');
  6285. td.colSpan = 2;
  6286. td.className = 'ua-messages-bg';
  6287. tr.append(td);
  6288. } else {
  6289. td = messages.firstElementChild;
  6290. if (td == null) return null;
  6291. }
  6292. var div = document.createElement('DIV');
  6293. div.classList.add('ua-messages', 'ua-'.concat(cls));
  6294. div[text instanceof HTML ? 'innerHTML' : 'textContent'] = prefix.concat(': ', text);
  6295. return td.appendChild(div);
  6296. }
  6297.  
  6298. function setHandlers() {
  6299. if (prefs.cleanup_descriptions) ['form.create_form', 'form.edit_form', 'form#request_form'].forEach(function(sel) {
  6300. if ((ref = document.querySelector(sel)) != null) ref.addEventListener('submit', cleanupDescriptions);
  6301. });
  6302.  
  6303. if ((ref = document.getElementById('yadg_input')) != null) ref.ondrop = clear0;
  6304.  
  6305. if (!isNWCD && (ref = document.getElementById('image') || document.querySelector('input[name="image"]')) != null) {
  6306. ref.ondragover = voidDragHandler0;
  6307. ref.ondblclick = imageClear;
  6308. ref.ondrop = imageDropHandler;
  6309. ref.onpaste = imagePasteHandler;
  6310. ref.placeholder = 'Paste/drop local or remote image';
  6311. }
  6312. // Now rape OPS upload form, but only gently
  6313. if (isOPS && isUpload && (ref = document.getElementById('remaster')) != null) {
  6314. ref.checked = true;
  6315. if (!isAddFormat && prefs.ops_always_edition) {
  6316. elem = ref.parentNode.parentNode;
  6317. elem.style.display = 'none';
  6318. if ((ref = document.querySelector('span#year_label_not_remaster')) != null) ref.textContent = 'Initial year:';
  6319. if ((ref = document.querySelector('tr#edition_year > td.label')) != null) ref.textContent = 'Edition year:';
  6320. if ((ref = document.querySelector('tr#edition_title > td.label')) != null) ref.textContent = 'Edition title:';
  6321. if ((ref = document.getElementById('label_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  6322. if ((ref = document.getElementById('catalogue_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  6323. document.querySelectorAll('table#edition_information > tbody > tr')
  6324. .forEach(tr => { elem.parentNode.insertBefore(tr, elem) });
  6325. } else Remaster();
  6326. }
  6327.  
  6328. if (!isNWCD) Array.from(document.getElementsByTagName('textarea')).forEach(function(textArea) {
  6329. if (textArea.className == 'ua-input') return;
  6330. textArea.ondragover = voidDragHandler0;
  6331. textArea.ondrop = descDropHandler;
  6332. textArea.onpaste = descPasteHandler;
  6333. });
  6334. }
  6335.  
  6336. function html2php(node, url, tagChain = []) {
  6337. if (!node || typeof node != 'object') return null;
  6338. switch (node.nodeType) {
  6339. case Node.ELEMENT_NODE: {
  6340. let tags = [], _tags = [], text = [];
  6341. for (let i = 0; i < 5; ++i) text[i] = '';
  6342. switch (node.nodeName) {
  6343. case 'P':
  6344. text[0] = '\n'; text[4] = '\n';
  6345. break;
  6346. case 'DIV':
  6347. text[0] = '\n\n'; text[4] = '\n\n';
  6348. break;
  6349. case 'DT':
  6350. text[4] = '\n';
  6351. break;
  6352. case 'DD':
  6353. text[4] = '\n';
  6354. if (isRED) addTag('pad=0|0|0|30'); else text[0] = ' ';
  6355. break;
  6356. case 'LABEL':
  6357. addTag('b');
  6358. text[0] = '\n\n';
  6359. break;
  6360. case 'BR':
  6361. return '\n';
  6362. case 'HR':
  6363. return isRED ? '[hr]' : '\n';
  6364. case 'B': case 'STRONG':
  6365. addTag('b');
  6366. break;
  6367. case 'I': case 'EM': case 'DFN': case 'CITE': case 'VAR':
  6368. addTag('i');
  6369. break;
  6370. case 'U': case 'INS':
  6371. addTag('u');
  6372. break;
  6373. case 'DEL':
  6374. addTag('s');
  6375. break;
  6376. case 'CODE': case 'SAMP': case 'KBD':
  6377. addTag('code');
  6378. text[2] = node.textContent;
  6379. break;
  6380. case 'PRE':
  6381. addTag('pre');
  6382. text[2] = node.textContent;
  6383. break;
  6384. case 'BLOCKQUOTE': case 'QUOTE':
  6385. addTag('quote');
  6386. break;
  6387. case 'Q':
  6388. text[1] = '"'; text[3] = '"';
  6389. break;
  6390. case 'H1':
  6391. addTag('size=5'); addTag('b');
  6392. text[0] = '\n\n'; text[4] = '\n\n';
  6393. break;
  6394. case 'H2':
  6395. addTag('size=4'); addTag('b');
  6396. text[0] = '\n\n'; text[4] = '\n\n';
  6397. break;
  6398. case 'H3':
  6399. addTag('size=3'); addTag('b');
  6400. text[0] = '\n\n'; text[4] = '\n\n';
  6401. break;
  6402. case 'H4': case 'H5': case 'H6':
  6403. addTag('b');
  6404. text[0] = '\n\n'; text[4] = '\n\n';
  6405. break;
  6406. case 'SMALL':
  6407. addTag('size=1');
  6408. break;
  6409. case 'OL': case 'UL':
  6410. _tags.push(node.nodeName.toLowerCase());
  6411. break;
  6412. case 'DL':
  6413. _tags.push(node.nodeName.toLowerCase());
  6414. break;
  6415. case 'LI':
  6416. switch (tagChain.reverse().find(tag => /^[ou]l$/.test(tag))) {
  6417. case 'ol': text[0] = '[#] '; text[4] = '\n'; break;
  6418. case 'ul': text[0] = '[*] '; text[4] = '\n'; break;
  6419. default: return '';
  6420. }
  6421. break;
  6422. case 'A': {
  6423. if (/^https?:$/i.test(node.protocol)) addTag('url=' + removeRedirect(node.href));
  6424. break;
  6425. }
  6426. case 'IMG':
  6427. addTag('img');
  6428. text[2] = node.dataset.src || node.src;
  6429. break;
  6430. case 'DETAILS': {
  6431. let summary = node.querySelector('summary');
  6432. summary = summary != null ? '='.concat(summary.textContent.trim()) : '';
  6433. addTag('hide' + summary);
  6434. break;
  6435. }
  6436. case 'AUDIO': case 'BASE': case 'BUTTON': case 'CANVAS': case 'COL': case 'COLGROUP': case 'DATALIST':
  6437. case 'DIALOG': case 'EMBED': case 'FIELDSET': case 'FORM': case 'HEAD': case 'INPUT': case 'LEGEND':
  6438. case 'LINK': case 'MAP': case 'META': case 'METER': case 'NOSCRIPT': case 'OBJECT': case 'OPTGROUP':
  6439. case 'OPTION': case 'PARAM': case 'PROGRESS': case 'SELECT': case 'SOURCE': case 'STYLE': case 'SUMMARY':
  6440. case 'SVG': case 'TEMPLATE': case 'TEXTAREA': case 'TITLE': case 'TRACK': case 'VIDEO':
  6441. return '';
  6442. }
  6443. if (['left', 'center', 'right'].some(al => node.style.textAlign.toLowerCase() == al)) {
  6444. addTag('align=' + node.style.textAlign.toLowerCase());
  6445. }
  6446. if (node.style.fontWeight >= 700) addTag('b');
  6447. switch (node.style.fontStyle.toLowerCase()) {
  6448. case 'italic': addTag('i'); break;
  6449. }
  6450. switch (node.style.textDecorationLine.toLowerCase()) {
  6451. case 'underline': addTag('u'); break;
  6452. case 'line-through': addTag('s'); break;
  6453. }
  6454. if (node.style.color) {
  6455. ctxt.fillStyle = elem.style.color;
  6456. if (ctxt.fillStyle != '#000000' && /^#(?:[a-f0-8]{2}){3,4}$/i.test(ctxt.fillStyle)) {
  6457. addTag('color=' + ctxt.fillStyle);
  6458. }
  6459. }
  6460. if (!text[2]) node.childNodes.forEach(function(node) {
  6461. text[2] += html2php(node, url, tagChain.concat(tags.concat(_tags).map(tag => tag.replace(/=.*$/, ''))));
  6462. });
  6463. if (node.nodeName == 'A' && text[2].trim().length <= 0) {
  6464. if (/^(?:https?):$/i.test(node.protocol)) {
  6465. text[2] = removeRedirect(node.href);
  6466. tags.splice(-1, 1, 'url');
  6467. } else text[2] = node.href.slice(node.protocol.length);
  6468. }
  6469. return text[0].concat((text[1] || text[2] || text[3] ? tags.map(tag => '[' + tag + ']').join('').concat(text[1],
  6470. text[2], text[3], tags.reverse().map(tag => '[/' + tag.replace(/=.*$/, '') + ']').join('')) : ''), text[4]);
  6471.  
  6472. function addTag(tag) {
  6473. if (tagChain.concat(tags.map(tag => tag.replace(/=.*$/, ''))).includesCaseless(tag.replace(/=.*$/, ''))) return;
  6474. tags.push(tag);
  6475. }
  6476. }
  6477. case Node.TEXT_NODE:
  6478. return node.wholeText.replace(/\s+/g, ' ');
  6479. case Node.DOCUMENT_NODE:
  6480. return html2php(node.body, url);
  6481. }
  6482. return '';
  6483. }
  6484.  
  6485. function coverPreview(input, imgUrl, size) {
  6486. if (!prefs.auto_preview_cover) return;
  6487. if ((child = document.getElementById('cover-preview')) == null) {
  6488. if (!(input instanceof HTMLElement) || input.parentNode.previousElementSibling == null) return;
  6489. elem = document.createElement('div');
  6490. elem.style = 'padding-top: 10px; float: right; width: 90%;';
  6491. child = document.createElement('img');
  6492. child.id = 'cover-preview';
  6493. elem.append(child);
  6494. var div = document.createElement('div');
  6495. div.id = 'cover-size';
  6496. if (isRequestNew || isRequestEdit) div.style.fontSize = '7.5pt';
  6497. elem.append(div);
  6498. input.parentNode.previousElementSibling.append(document.createElement('br'));
  6499. input.parentNode.previousElementSibling.append(elem);
  6500. }
  6501. if ((div = div || document.getElementById('cover-size')) == null) return;
  6502. if (urlParser.test(imgUrl)) {
  6503. child.onload = function(evt) {
  6504. this.onload = null;
  6505. if (!this.naturalWidth || !this.naturalHeight) return; // invalid image
  6506. (size > 0 ? Promise.resolve(size) : getRemoteFileSize(imgUrl)).then(function(size) {
  6507. var warn = prefs.huge_image_warning && size > prefs.huge_image_warning * 2**20;
  6508. var html = warn ? '<strong style="color: #ff4c4c;">' + formattedSize(size) + '</strong>' : formattedSize(size);
  6509. div.innerHTML = this.naturalWidth + '×' + this.naturalHeight + ' (' + html + ')';
  6510. if (!warn) return;
  6511. addMessage('high cover size (' + formattedSize(size) + ')', 'notice');
  6512. }.bind(this)).catch(reason => { div.textContent = this.naturalWidth + '×' + this.naturalHeight });
  6513. };
  6514. child.onerror = function(evt) {
  6515. this.onerror = null;
  6516. div.textContent = this.src = '';
  6517. console.warn('Image source cannot be updated:', evt, imgUrl);
  6518. };
  6519. child.src = imgUrl;
  6520. } else div.textContent = child.src = '';
  6521. }
  6522.  
  6523. function getRemoteFileSize(url) {
  6524. return new Promise(function(resolve, reject) {
  6525. var imageSize, abort = GM_xmlhttpRequest({
  6526. method: 'GET', url: url, responseType: 'arraybuffer',
  6527. onreadystatechange: function(response) {
  6528. if (imageSize || response.readyState < XMLHttpRequest.HEADERS_RECEIVED
  6529. || !/^Content-Length:\s*(\d+)\b/im.test(response.responseHeaders)) return;
  6530. if (!(imageSize = parseInt(RegExp.$1))) return;
  6531. resolve(imageSize);
  6532. abort.abort();
  6533. },
  6534. onload: function(response) { // fail-safe
  6535. if (imageSize) return;
  6536. if (response.status >= 200 && response.status < 400) resolve(response.responseText.length /*response.response.byteLength*/);
  6537. else reject(new Error('Image not accessible'));
  6538. },
  6539. onerror: response => reject('Image not accessible'),
  6540. ontimeout: response => reject('Image not accessible'),
  6541. });
  6542. });
  6543. }
  6544.  
  6545. function cleanupDescriptions(evt) {
  6546. descriptionFields.forEach(function(ID) {
  6547. if ((ref = evt.target.querySelector('textarea#' + ID)) == null || ref.value.length <= 0) return;
  6548. var clean = ref.value
  6549. .replace(/[ \t]*Vinyl rip by \[color=\S+\]\[\/color\]\s*/im, '')
  6550. .replace(/\[u\]Lineage:\[\/u\]\n\n/i, '')
  6551. for (var i = 0; i < 3; ++i) clean = clean.replace(/\s*\[(\w+)(?:=([^\[\]]*))?\]\[\/\1\]/gm, '');
  6552. const drMatch = [
  6553. /(^| \| )DR(\d+)$\s+/m,
  6554. /(?:^| \| )DR(\d+)(?=$| \| )/gm,
  6555. ];
  6556. var m = /\[hide=DR(\d+)?\]\[pre\]/i.exec(clean);
  6557. //if (m != null && drMatch[0].test(clean) && RegExp.$2 == m[1]) clean = clean.replace(drMatch[0], '$1');
  6558. if (m != null && drMatch[1].test(clean) && RegExp.$1 == m[1]) clean = clean.replace(drMatch[1], '');
  6559. ref.value = clean.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/[ \t\xA0]+$/gm, '').trim();
  6560. });
  6561. return true;
  6562. }
  6563.  
  6564. function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*'.concat(expr, '[^\\(\\)]*\\)$'), 'i') }
  6565. function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*'.concat(expr, '[^\\[\\]]*\\]$'), 'i') }
  6566.  
  6567. function notMonospaced(str) {
  6568. return /[\u0080-\u009F]/.test(str)
  6569. // || /[\u0000-\u001F]/.test(str) // Control character
  6570. // || /[\u0020-\u007F]/.test(str) // Basic Latin
  6571. // || /[\u0080-\u00FF]/.test(str) // Latin-1 Supplement
  6572. // || /[\u0100-\u017F]/.test(str) // Latin Extended-A
  6573. // || /[\u0180-\u024F]/.test(str) // Latin Extended-B
  6574. // || /[\u0250-\u02AF]/.test(str) // IPA Extensions
  6575. || /[\u02B0-\u02FF]/.test(str) // Spacing Modifier Letters
  6576. || /[\u0300-\u036F]/.test(str) // Combining Diacritical Marks
  6577. || /[\u0370-\u03FF]/.test(str) // Greek and Coptic
  6578. || /[\u0400-\u04FF]/.test(str) // Cyrillic
  6579. || /[\u0500-\u052F]/.test(str) // Cyrillic Supplement
  6580. || /[\u0530-\u058F]/.test(str) // Armenian
  6581. || /[\u0590-\u05FF]/.test(str) // Hebrew
  6582. || /[\u0600-\u06FF]/.test(str) // Arabic
  6583. || /[\u0700-\u074F]/.test(str) // Syriac
  6584. || /[\u0750-\u077F]/.test(str) // Arabic Supplement
  6585. || /[\u0780-\u07BF]/.test(str) // Thaana
  6586. || /[\u07C0-\u07FF]/.test(str) // NKo
  6587. || /[\u0800-\u083F]/.test(str) // Samaritan
  6588. || /[\u0840-\u085F]/.test(str) // Mandaic
  6589. || /[\u0860-\u086F]/.test(str) // Syriac Supplement
  6590. || /[\u08A0-\u08FF]/.test(str) // Arabic Extended-A
  6591. || /[\u0900-\u097F]/.test(str) // Devanagari
  6592. || /[\u0980-\u09FF]/.test(str) // Bengali
  6593. || /[\u0A00-\u0A7F]/.test(str) // Gurmukhi
  6594. || /[\u0A80-\u0AFF]/.test(str) // Gujarati
  6595. || /[\u0B00-\u0B7F]/.test(str) // Oriya
  6596. || /[\u0B80-\u0BFF]/.test(str) // Tamil
  6597. || /[\u0C00-\u0C7F]/.test(str) // Telugu
  6598. || /[\u0C80-\u0CFF]/.test(str) // Kannada
  6599. || /[\u0D00-\u0D7F]/.test(str) // Malayalam
  6600. || /[\u0D80-\u0DFF]/.test(str) // Sinhala
  6601. || /[\u0E00-\u0E7F]/.test(str) // Thai
  6602. || /[\u0E80-\u0EFF]/.test(str) // Lao
  6603. || /[\u0F00-\u0FFF]/.test(str) // Tibetan
  6604. || /[\u1000-\u109F]/.test(str) // Myanmar
  6605. || /[\u10A0-\u10FF]/.test(str) // Georgian
  6606. || /[\u1100-\u11FF]/.test(str) // Hangul Jamo
  6607. || /[\u1200-\u137F]/.test(str) // Ethiopic
  6608. || /[\u1380-\u139F]/.test(str) // Ethiopic Supplement
  6609. || /[\u13A0-\u13FF]/.test(str) // Cherokee
  6610. || /[\u1400-\u167F]/.test(str) // Unified Canadian Aboriginal Syllabics
  6611. || /[\u1680-\u169F]/.test(str) // Ogham
  6612. || /[\u16A0-\u16FF]/.test(str) // Runic
  6613. || /[\u1700-\u171F]/.test(str) // Tagalog
  6614. || /[\u1720-\u173F]/.test(str) // Hanunoo
  6615. || /[\u1740-\u175F]/.test(str) // Buhid
  6616. || /[\u1760-\u177F]/.test(str) // Tagbanwa
  6617. || /[\u1780-\u17FF]/.test(str) // Khmer
  6618. || /[\u1800-\u18AF]/.test(str) // Mongolian
  6619. || /[\u18B0-\u18FF]/.test(str) // Unified Canadian Aboriginal Syllabics Extended
  6620. || /[\u1900-\u194F]/.test(str) // Limbu
  6621. || /[\u1950-\u197F]/.test(str) // Tai Le
  6622. || /[\u1980-\u19DF]/.test(str) // New Tai Lue
  6623. || /[\u19E0-\u19FF]/.test(str) // Khmer Symbols
  6624. || /[\u1A00-\u1A1F]/.test(str) // Buginese
  6625. || /[\u1A20-\u1AAF]/.test(str) // Tai Tham
  6626. || /[\u1AB0-\u1AFF]/.test(str) // Combining Diacritical Marks Extended
  6627. || /[\u1B00-\u1B7F]/.test(str) // Balinese
  6628. || /[\u1B80-\u1BBF]/.test(str) // Sundanese
  6629. || /[\u1BC0-\u1BFF]/.test(str) // Batak
  6630. || /[\u1C00-\u1C4F]/.test(str) // Lepcha
  6631. || /[\u1C50-\u1C7F]/.test(str) // Ol Chiki
  6632. || /[\u1C80-\u1C8F]/.test(str) // Cyrillic Extended C
  6633. || /[\u1CC0-\u1CCF]/.test(str) // Sundanese Supplement
  6634. || /[\u1CD0-\u1CFF]/.test(str) // Vedic Extensions
  6635. || /[\u1D00-\u1D7F]/.test(str) // Phonetic Extensions
  6636. || /[\u1D80-\u1DBF]/.test(str) // Phonetic Extensions Supplement
  6637. || /[\u1DC0-\u1DFF]/.test(str) // Combining Diacritical Marks Supplement
  6638. // || /[\u1E00-\u1EFF]/.test(str) // Latin Extended Additional
  6639. || /[\u1F00-\u1FFF]/.test(str) // Greek Extended
  6640. || /[\u200B-\u200F\u2028\u2029\u203B\u202A-\u202E\u2060-\u206F]/.test(str) //|| /[\u2000-\u206F]/.test(str) // General Punctuation
  6641. || /[\u2070-\u209F]/.test(str) // Superscripts and Subscripts
  6642. // || /[\u20A0-\u20CF]/.test(str) // Currency Symbols
  6643. || /[\u20D0-\u20FF]/.test(str) // Combining Diacritical Marks for Symbols
  6644. // || /[\u2100-\u214F]/.test(str) // Letterlike Symbols
  6645. || /[\u2150-\u218F]/.test(str) // Number Forms
  6646. // || /[\u2190-\u21FF]/.test(str) // Arrows
  6647. || /[\u2200-\u22FF]/.test(str) // Mathematical Operators
  6648. || /[\u2300-\u23FF]/.test(str) // Miscellaneous Technical
  6649. || /[\u2400-\u243F]/.test(str) // Control Pictures
  6650. // || /[\u2440-\u245F]/.test(str) // Optical Character Recognition
  6651. || /[\u2460-\u24FF]/.test(str) // Enclosed Alphanumerics
  6652. || /[\u2500-\u257F]/.test(str) // Box Drawing
  6653. // || /[\u2580-\u259F]/.test(str) // Block Elements
  6654. || /[\u25A0-\u25FF]/.test(str) // Geometric Shapes
  6655. || /[\u2600-\u26FF]/.test(str) // Miscellaneous Symbols
  6656. || /[\u2700-\u27BF]/.test(str) // Dingbats
  6657. || /[\u27C0-\u27EF]/.test(str) // Miscellaneous Mathematical Symbols-A
  6658. || /[\u27F0-\u27FF]/.test(str) // Supplemental Arrows-A
  6659. || /[\u2800-\u28FF]/.test(str) // Braille Patterns
  6660. || /[\u2900-\u297F]/.test(str) // Supplemental Arrows-B
  6661. // || /[\u2980-\u29FF]/.test(str) // Miscellaneous Mathematical Symbols-B
  6662. // || /[\u2A00-\u2AFF]/.test(str) // Supplemental Mathematical Operators
  6663. || /[\u2B00-\u2BFF]/.test(str) // Miscellaneous Symbols and Arrows
  6664. || /[\u2C00-\u2C5F]/.test(str) // Glagolitic
  6665. // || /[\u2C60-\u2C7F]/.test(str) // Latin Extended-C
  6666. || /[\u2C80-\u2CFF]/.test(str) // Coptic
  6667. || /[\u2D00-\u2D2F]/.test(str) // Georgian Supplement
  6668. || /[\u2D30-\u2D7F]/.test(str) // Tifinagh
  6669. || /[\u2D80-\u2DDF]/.test(str) // Ethiopic Extended
  6670. || /[\u2DE0-\u2DFF]/.test(str) // Cyrillic Extended-A
  6671. || /[\u2E00-\u2E7F]/.test(str) // Supplemental Punctuation
  6672. || /[\u2E80-\u2EFF]/.test(str) // CJK Radicals Supplement
  6673. || /[\u2F00-\u2FDF]/.test(str) // Kangxi Radicals
  6674. || /[\u2FF0-\u2FFF]/.test(str) // Ideographic Description Characters
  6675. || /[\u3000-\u303F]/.test(str) // CJK Symbols and Punctuation
  6676. || /[\u3040-\u309F]/.test(str) // Hiragana
  6677. || /[\u30A0-\u30FF]/.test(str) // Katakana
  6678. || /[\u3100-\u312F]/.test(str) // Bopomofo
  6679. || /[\u3130-\u318F]/.test(str) // Hangul Compatibility Jamo
  6680. || /[\u3190-\u319F]/.test(str) // Kanbun
  6681. || /[\u31A0-\u31BF]/.test(str) // Bopomofo Extended
  6682. || /[\u31C0-\u31EF]/.test(str) // CJK Strokes
  6683. || /[\u31F0-\u31FF]/.test(str) // Katakana Phonetic Extensions
  6684. || /[\u3200-\u32FF]/.test(str) // Enclosed CJK Letters and Months
  6685. || /[\u3300-\u33FF]/.test(str) // CJK Compatibility
  6686. || /[\u3400-\u4DBF]/.test(str) // CJK Unified Ideographs Extension A
  6687. || /[\u4DC0-\u4DFF]/.test(str) // Yijing Hexagram Symbols
  6688. || /[\u4E00-\u9FFF]/.test(str) // CJK Unified Ideographs
  6689. // || /[\uA000-\uA48F]/.test(str) // Yi Syllables
  6690. // || /[\uA490-\uA4CF]/.test(str) // Yi Radicals
  6691. || /[\uA4D0-\uA4FF]/.test(str) // Lisu
  6692. || /[\uA500-\uA63F]/.test(str) // Vai
  6693. || /[\uA640-\uA69F]/.test(str) // Cyrillic Extended-B
  6694. || /[\uA6A0-\uA6FF]/.test(str) // Bamum
  6695. || /[\uA700-\uA71F]/.test(str) // Modifier Tone Letters
  6696. || /[\uA720-\uA7FF]/.test(str) // Latin Extended-D
  6697. || /[\uA800-\uA82F]/.test(str) // Syloti Nagri
  6698. || /[\uA830-\uA83F]/.test(str) // Common Indic Number Forms
  6699. || /[\uA840-\uA87F]/.test(str) // Phags-pa
  6700. || /[\uA880-\uA8DF]/.test(str) // Saurashtra
  6701. || /[\uA8E0-\uA8FF]/.test(str) // Devanagari Extended
  6702. || /[\uA900-\uA92F]/.test(str) // Kayah Li
  6703. || /[\uA930-\uA95F]/.test(str) // Rejang
  6704. || /[\uA960-\uA97F]/.test(str) // Hangul Jamo Extended-A
  6705. || /[\uA980-\uA9DF]/.test(str) // Javanese
  6706. || /[\uA9E0-\uA9FF]/.test(str) // Myanmar Extended-B
  6707. || /[\uAA00-\uAA5F]/.test(str) // Cham
  6708. || /[\uAA60-\uAA7F]/.test(str) // Myanmar Extended-A
  6709. || /[\uAA80-\uAADF]/.test(str) // Tai Viet
  6710. || /[\uAAE0-\uAAFF]/.test(str) // Meetei Mayek Extensions
  6711. || /[\uAB00-\uAB2F]/.test(str) // Ethiopic Extended-A
  6712. // || /[\uAB30-\uAB6F]/.test(str) // Latin Extended-E
  6713. || /[\uAB70-\uABBF]/.test(str) // Cherokee Supplement
  6714. || /[\uABC0-\uABFF]/.test(str) // Meetei Mayek
  6715. || /[\uAC00-\uD7AF]/.test(str) // Hangul Syllables
  6716. || /[\uD7B0-\uD7FF]/.test(str) // Hangul Jamo Extended-B
  6717. || /[\uD800-\uDB7F]/.test(str) // High Surrogates
  6718. // || /[\uDB80-\uDBFF]/.test(str) // High Private Use Surrogates
  6719. || /[\uDC00-\uDFFF]/.test(str) // Low Surrogates
  6720. || /[\uE000-\uF8FF]/.test(str) // Private Use Area
  6721. || /[\uF900-\uFAFF]/.test(str) // CJK Compatibility Ideographs
  6722. || /[\uFB00-\uFB4F]/.test(str) // Alphabetic Presentation Forms
  6723. || /[\uFB50-\uFDFF]/.test(str) // Arabic Presentation Forms-A
  6724. || /[\uFE00-\uFE0F]/.test(str) // Variation Selectors
  6725. || /[\uFE10-\uFE1F]/.test(str) // Vertical Forms
  6726. || /[\uFE20-\uFE2F]/.test(str) // Combining Half Marks
  6727. || /[\uFE30-\uFE4F]/.test(str) // CJK Compatibility Forms
  6728. || /[\uFE50-\uFE6F]/.test(str) // Small Form Variants
  6729. || /[\uFE70-\uFEFF]/.test(str) // Arabic Presentation Forms-B
  6730. || /[\uFF00-\uFFEF]/.test(str) // Halfwidth and Fullwidth Forms
  6731. || /[\uFFF0-\uFFFF]/.test(str) // Specials
  6732. // || /[\u10000-\uFFFFF]/.test(str) // Others
  6733. }
  6734.  
  6735. function makeTimeString(duration) {
  6736. let t = Math.abs(Math.round(duration));
  6737. let H = Math.floor(t / 60 ** 2);
  6738. let M = Math.floor(t / 60 % 60);
  6739. let S = t % 60;
  6740. return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
  6741. ':' + S.toString().padStart(2, '0');
  6742. }
  6743.  
  6744. function timeStringToTime(str) {
  6745. if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
  6746. var t = 0, a = RegExp.$2.split(':');
  6747. while (a.length > 0) t = t * 60 + parseFloat(a.shift());
  6748. return RegExp.$1 ? -t : t;
  6749. }
  6750.  
  6751. function normalizeDate(str) {
  6752. if (typeof str != 'string') return null;
  6753. 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)
  6754. if (/\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // UK, IRL, FR
  6755. if (/\b(\d{1,2})-(\d{1,2})-(\d{2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // NL
  6756. 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
  6757. if (/\b(\d{4})\.\s?(\d{1,2})\.\s?(\d{1,2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1; // JP
  6758. return extractYear(str);
  6759. }
  6760.  
  6761. function extractYear(expr) {
  6762. if (typeof expr == 'number') return Math.round(expr);
  6763. if (typeof expr != 'string') return null;
  6764. if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
  6765. var d = new Date(expr);
  6766. return parseInt(isNaN(d) ? expr : d.getFullYear());
  6767. }
  6768.  
  6769. function formattedSize(size) {
  6770. return size < 1024**1 ? Math.round(size) + ' B'
  6771. : size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB'
  6772. : size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB'
  6773. : size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB'
  6774. : size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB'
  6775. : (Math.round(size * 100 / 2**50) / 100) + ' PiB';
  6776. }
  6777.  
  6778. function safeText(unsafeText) {
  6779. let div = document.createElement('div');
  6780. div.innerText = unsafeText || '';
  6781. return div.innerHTML;
  6782. }
  6783.  
  6784. function imageClear(evt) {
  6785. evt.target.value = '';
  6786. coverPreview(evt.target, null);
  6787. }
  6788.  
  6789. function imageDropHandler(evt) { return !evt.shiftKey ? imageDataHandler(evt, evt.dataTransfer) : true }
  6790. function imagePasteHandler(evt) { return imageDataHandler(evt, evt.clipboardData) }
  6791. function imageDataHandler(evt, data) {
  6792. if (!data) return true;
  6793. if (data.files.length > 0) {
  6794. if (!data.files[0].type.toLowerCase().startsWith('image/')) return true;
  6795. evt.target.disabled = true;
  6796. if (evt.target.hTimer) {
  6797. clearTimeout(evt.target.hTimer);
  6798. delete evt.target.hTimer;
  6799. }
  6800. evt.target.style.backgroundColor = '#800000';
  6801. let elem = evt.target, file = data.files[0], size = data.files[0].size;
  6802. uploadImages([file], evt.target).then(function(urls) {
  6803. elem.value = urls[0];
  6804. elem.style.backgroundColor = '#008000';
  6805. elem.style.color = 'white';
  6806. elem.hTimer = setTimeout(function() {
  6807. elem.style.backgroundColor = null;
  6808. elem.style.color = null;
  6809. delete elem.hTimer;
  6810. }, 10000);
  6811. coverPreview(elem, urls[0], size);
  6812. }).catch(function(error) {
  6813. elem.style.backgroundColor = null;
  6814. imageClear(evt);
  6815. Promise.resolve(error).then(msg => { alert(msg) });
  6816. }).then(function() { elem.disabled = false });
  6817. return false;
  6818. } else if (data.items.length > 0) {
  6819. let links = data.getData('text/uri-list');
  6820. if (links) links = links.split(/\r?\n/); else {
  6821. links = data.getData('text/x-moz-url');
  6822. if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  6823. else if (links = data.getData('text/plain')) links = links.split(/\r?\n/);
  6824. }
  6825. if (Array.isArray(links) && links.length > 0) imageUrlResolver(links[0]).then(verifyImageUrl).then(function(imageUrl) {
  6826. if (!isNWCD) {
  6827. evt.target.value = imageUrl;
  6828. coverPreview(evt.target, imageUrl);
  6829. if (!prefs.auto_rehost_cover || imageUrl.toLowerCase().startsWith(ptpimgOrigin)) return;
  6830. evt.target.disabled = true;
  6831. rehost2PTPIMG([imageUrl])
  6832. .then(urls => { if (urls.length > 0 && urls[0] != evt.target.value) evt.target.value = urls[0] })
  6833. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') }) })
  6834. .then(function() { evt.target.disabled = false });
  6835. } else {
  6836. evt.target.disabled = true;
  6837. uploadToImagehost(imageUrl).then(function(result) {
  6838. evt.target.value = result.url;
  6839. coverPreview(evt.target, result.url);
  6840. }).catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) }).then(function() { evt.target.disabled = false });
  6841. }
  6842. }).catch(function(e) {
  6843. console.error(e);
  6844. alert(e);
  6845. });
  6846. return false;
  6847. }
  6848. return true;
  6849. }
  6850.  
  6851. function descDropHandler(evt) {
  6852. if (evt.dataTransfer == null || evt.shiftKey) return true;
  6853. if (evt.dataTransfer.files.length > 0) {
  6854. let images = [];
  6855. Array.from(evt.dataTransfer.files).forEach(function(file) {
  6856. switch (file.type) {
  6857. case '':
  6858. if (!['log'/*, 'nfo'*/].some(ext => file.name.toLowerCase().endsWith('.' + ext))) break;
  6859. case 'text/plain':
  6860. //case 'text/nfo': // malformed encoding
  6861. case 'text/log':
  6862. evt.target.disabled = true;
  6863. file.getText(file.name.toLowerCase().endsWith('.nfo') ? 'ibm850' : 'utf-8').then(function(text) {
  6864. var isDR = file.name.toLowerCase().endsWith('foo_dr.txt') && /^Official DR value:\s*DR(\d+)\b/im.test(text);
  6865. if (isDR) var DR = parseInt(RegExp.$1);
  6866. var tag = isDR || file.name.toLowerCase().endsWith('.nfo') ? 'pre' : 'code';
  6867. var php = isDR ? '[hide=DR' + RegExp.$1 + '][' + tag + ']' + text + '[/' + tag + '][/hide]'
  6868. : '[hide=' + file.name + '][' + tag + ']' + text + '[/' + tag + '][/hide]';
  6869. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  6870. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) +
  6871. php + evt.target.value.slice(evt.rangeOffset);
  6872. } else if (isDR && /\[hide=DR\d*\]\[pre\]\[\/pre\]/i.test(evt.target.value)) {
  6873. evt.target.value = RegExp.leftContext + php.slice(0, -7) + RegExp.rightContext;
  6874. } else if (isDR && /\[hide=DR(\d*)\]((?:\[pre\](foobar2000[\s\S]+?)^\[\/pre\]\s*)+)(?:\[pre\]\[\/pre\])?/im.test(evt.target.value)) {
  6875. php = '[hide=DR';
  6876. if (parseInt(RegExp.$1) == DR) php += RegExp.$1;
  6877. evt.target.value = RegExp.leftContext.concat(php, ']', RegExp.$2.trim(), '\n[pre]', text, '[/pre]', RegExp.rightContext);
  6878. } else if (!isDR && /\[hide\](?:\[code\]\[\/code\])?\[\/hide\]/i.test(evt.target.value)) {
  6879. evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
  6880. } else if (!isDR && /(\[hide=[^\]]+\])(?:\[code\]\[\/code\])?(\[\/hide\])/i.test(evt.target.value)) {
  6881. evt.target.value = RegExp.leftContext.concat(RegExp.$1, '[code]', text, '[/code]', RegExp.$2, RegExp.rightContext);
  6882. } else evt.target.value += '\n\n'.concat(php);
  6883. }).catch(function(e) { alert(e) }).then(function() {
  6884. if (!evt.target.style.background) evt.target.disabled = false;
  6885. });
  6886. break;
  6887. case 'image/png':
  6888. case 'image/jpeg':
  6889. case 'image/gif':
  6890. case 'image/bmp':
  6891. //case 'image/webp':
  6892. //case 'image/svg+xml':
  6893. images.push(file);
  6894. break;
  6895. }
  6896. });
  6897. if (images.length > 0) {
  6898. evt.target.disabled = true;
  6899. evt.target.style.background = '#600000 no-repeat center center url(' + ulImgData + ')';
  6900. //evt.target.style.background = '#FF000040 no-repeat center center url(https://svgshare.com/i/H16.svg)';
  6901. let elem = evt.target;
  6902. uploadImages(images).then(urlHandler.bind({ tag: 'img' }))
  6903. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
  6904. .then(function() {
  6905. elem.style.background = null;
  6906. elem.disabled = false;
  6907. });
  6908. }
  6909. evt.stopPropagation();
  6910. return false;
  6911. } else if (evt.dataTransfer.items.length > 0) {
  6912. let content = evt.dataTransfer.getData('text/uri-list');
  6913. if (content) content = content.split(/\r?\n/); else {
  6914. content = evt.dataTransfer.getData('text/x-moz-url');
  6915. if (content) content = content.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  6916. };
  6917. if (Array.isArray(content) && content.length > 0) {
  6918. Promise.all(content.map(imageUrlResolver)).then(function(resolvedUrls) {
  6919. if (!isNWCD) {
  6920. if (prefs.auto_rehost_cover) {
  6921. evt.target.disabled = true;
  6922. rehost2PTPIMG(resolvedUrls.flatten()).catch(function(e) {
  6923. addMessage('PTPimg ' + e + ' (not rehosted)', 'warning');
  6924. return resolvedUrls.flatten();
  6925. }).then(urlHandler.bind({ tag: 'img' })).then(() => { evt.target.disabled = false });
  6926. } else urlHandler.bind({ tag: 'img' })(content);
  6927. } else {
  6928. evt.target.disabled = true;
  6929. Promise.all(resolvedUrls.map(resolvedUrl => uploadToImagehost(resolvedUrl))).then(function(result) {
  6930. }).catch(function(e) {
  6931. addMessage(e, 'warning');
  6932. return resolvedUrls.flatten();
  6933. }).then(urlHandler.bind({ tag: 'img' })).then(() => { evt.target.disabled = false });
  6934. }
  6935. }).catch(function(e) {
  6936. let as = domParser.parseFromString(evt.dataTransfer.getData('text/html'), 'text/html').body.querySelectorAll('a');
  6937. Promise.all(content.map(urlResolver))
  6938. .then(resolved => urlHandler.bind({ tag: 'url', titles: Array.from(as).map(a => a.textContent.trim()) })(resolved));
  6939. });
  6940. } else if (content = evt.dataTransfer.getData('text/html')) {
  6941. textHandler(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
  6942. } else if (content = evt.dataTransfer.getData('text/plain')) {
  6943. textHandler(content);
  6944. }
  6945. evt.stopPropagation();
  6946. return false;
  6947. }
  6948. return true;
  6949.  
  6950. function urlHandler(urls) {
  6951. const rx = new RegExp('\\[' + this.tag + '\\]\\[\\/' + this.tag + '\\]', 'i');
  6952. urls.forEach(function(url, ndx) {
  6953. if (url.length <= 0 || !urlParser.test(url)) return;
  6954. var phpBB = '[' + this.tag;
  6955. phpBB += Array.isArray(this.titles) && this.titles[ndx] ? '=' + url + ']' + this.titles[ndx] : ']' + url;
  6956. phpBB += '[/' + this.tag + ']';
  6957. if (evt.target.value.trimRight().length <= 0) evt.target.value = phpBB; else if (evt.ctrlKey) {
  6958. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + phpBB + evt.target.value.slice(evt.rangeOffset);
  6959. } else if (rx.test(evt.target.value)) {
  6960. evt.target.value = RegExp.leftContext + phpBB + RegExp.rightContext;
  6961. } else evt.target.value = evt.target.value.trimRight().concat(/*ndx <= 0 ? '\n\n' : */'\n\n', phpBB);
  6962. }.bind(this));
  6963. }
  6964. function textHandler(phpBB) {
  6965. if (evt.target.value.length <= 0) evt.target.value = phpBB; else if (evt.ctrlKey) {
  6966. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + phpBB + evt.target.value.slice(evt.rangeOffset);
  6967. } else evt.target.value += '\n\n'.concat(phpBB);
  6968. }
  6969. }
  6970.  
  6971. function descPasteHandler(evt) {
  6972. if (evt.clipboardData == null) return true;
  6973. if (evt.clipboardData.files.length > 0) {
  6974. let images = Array.from(evt.clipboardData.files)
  6975. .filter(file => ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/webp', 'image/svg+xml']
  6976. .some(mimeType => file.type == mimeType))
  6977. if (images.length <= 0) return true;
  6978. evt.target.disabled = true;
  6979. evt.target.style.background = '#600000 no-repeat center center url(' + ulImgData + ')';
  6980. uploadImages(images).then(urls => { insert(urls.map(url => '[img]'.concat(url, '[/img]')).join('\n')) })
  6981. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) }).then(function() {
  6982. evt.target.style.background = null;
  6983. evt.target.disabled = false;
  6984. });
  6985. evt.stopPropagation();
  6986. return false;
  6987. } else if (evt.clipboardData.items.length > 0) {
  6988. let content = evt.clipboardData.getData('text/html');
  6989. if (!content) return true;
  6990. insert(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
  6991. return false;
  6992. }
  6993. return true;
  6994.  
  6995. function insert(phpBB) {
  6996. var selStart = evt.target.selectionStart;
  6997. evt.target.value = evt.target.value.slice(0, evt.target.selectionStart)
  6998. .concat(phpBB, evt.target.value.slice(evt.target.selectionEnd));
  6999. evt.target.setSelectionRange(selStart + phpBB.length, selStart + phpBB.length);
  7000. }
  7001. }
  7002.  
  7003. function uaInsert(evt) {
  7004. if (evt.clipboardData) evt.target.value = '';
  7005. if (!(prefs.autfill_delay > 0)) return true;
  7006. autofill = true;
  7007. setTimeout(fillFromText, prefs.autfill_delay);
  7008. }
  7009.  
  7010. // Firefox accepts dropped playlist in malformed form, try to detect and correct it
  7011. function fixFirefoxDropBug(evt) {
  7012. if (evt.target == null || evt.target.value.length <= 0) return true;
  7013. var tl = (Math.sqrt(4 * evt.target.value.split('\n').length - 3) + 1) / 2;
  7014. if (tl < 2 || tl != Math.floor(tl) || evt.target.value.length % tl != 0) return true;
  7015. var l = evt.target.value.length / tl;
  7016. var s = evt.target.value.slice(0, l);
  7017. for (var i = 1; i < tl; ++i) if (evt.target.value.slice(i * l, (i + 1) * l) != s) return true;
  7018. evt.target.value = s;
  7019. return true;
  7020. }
  7021.  
  7022. function clear0(evt) { if (evt.target.value.length > 0) evt.target.value = '' }
  7023. function clear1(evt) { if (evt.buttons == 4) clear0(evt) }
  7024. function voidDragHandler0(evt) { return false }
  7025. function voidDragHandler1(evt) {
  7026. return !evt.dataTransfer.types.includes('Files') || evt.target.nodeName == 'TEXTAREA'
  7027. || evt.target.nodeName == 'INPUT' && evt.target.type == 'file'
  7028. }
  7029.  
  7030. function removeRedirect(uri) {
  7031. return typeof uri != 'string' ? null : [
  7032. 'www.anonymz.com/?', 'www.anonymz.com?',
  7033. 'anonymz.com/?', 'anonymz.com?',
  7034. 'anonym.to/?', 'anonym.to?',
  7035. 'dereferer.me/?',
  7036. 'reho.st/',
  7037. ].reduce(function(acc, it) {
  7038. if (acc.toLowerCase().startsWith('https://' + it)) return acc.slice(it.length + 8);
  7039. if (acc.toLowerCase().startsWith('http://' + it)) return acc.slice(it.length + 7);
  7040. return acc;
  7041. }, uri);
  7042. }
  7043.  
  7044. function urlResolver(url) {
  7045. if (!urlParser.test(url)) return Promise.reject('Invalid URL:\n\n'.concat(url));
  7046. try { if (!(url instanceof URL)) url = new URL(url) } catch(e) { return Promise.reject(e) }
  7047. switch (url.hostname) {
  7048. case 'rutracker.org':
  7049. if (url.pathname != '/forum/out.php') break;
  7050. return globalFetch(url, { method: 'HEAD' }).then(response => urlResolver(response.finalUrl));
  7051. case 'www.anonymz.com': case 'anonymz.com': case 'anonym.to': case 'dereferer.me':
  7052. var resolved = decodeURIComponent(url.search.slice(1));
  7053. return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver();
  7054. // case 'reho.st':
  7055. // resolved = url.pathname.concat(url.search, url.hash).slice(1);
  7056. // if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
  7057. // return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver();
  7058. // URL shorteners
  7059. case 'tinyurl.com': case 'bit.ly': case 'j.mp': case 't.co': case 'goo.gl': case 'apple.co': case 'flic.kr':
  7060. case 'rebrand.ly': case 'b.link': case 't2m.io': case 'zpr.io': case 'yourls.org':
  7061. return genericResolver();
  7062. }
  7063. return Promise.resolve(url.href);
  7064.  
  7065. function genericResolver() {
  7066. return globalFetch(url).then(function(response) {
  7067. var redirect = response.document.querySelector('meta[http-equiv="refresh"]');
  7068. if (redirect != null && (redirect = redirect.content.replace(/^.*?\b(?:URL)\s*=\s*/i, '')) != url.href
  7069. || /^ *(?:Location) *: *(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$1) != url.href
  7070. || /^ *(?:Refresh) *: *(\d+); *url=(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$2) != url.href
  7071. || (redirect = response.finalUrl) != url.href) return urlResolver(redirect);
  7072. return Promise.resolve(url.href);
  7073. });
  7074. }
  7075. }
  7076.  
  7077. function tidalRlsParser(url) {
  7078. return /^https?:\/\/(?:\w+\.)*tidal\.com\//.test(url)
  7079. && (/\/album\/(\d+)(?:\/|$)/i.test(url) || /\/album(?:\/|\?).*\b(?:albumId)=(\d+)\b/i.test(url));
  7080. }
  7081.  
  7082. function verifyImageUrl(url) {
  7083. return urlResolver(url).then(function(url) {
  7084. //if (!strict && imageExtensions.some(ext => url.toLowerCase().endsWith('.'.concat(ext)))) return Promise.resolve(url); // weak
  7085. return new Promise(function(resolve, reject) {
  7086. var img = new Image();
  7087. img.onload = load => { resolve(url) };
  7088. img.onerror = function(error) {
  7089. if (img.src.includes('?')) img.src = url.replace(/\?.*?(?=\#|$)/, '');
  7090. else reject('Not valid image:\n\n'.concat(url));
  7091. };
  7092. img.ontimeout = timeout => { reject('Image load timed out:\n\n'.concat(url)) };
  7093. img.src = url;
  7094. });
  7095. });
  7096. }
  7097. function verifyImageUrls(urls) {
  7098. return Array.isArray(urls) ? Promise.all(urls.map(verifyImageUrl)) : Promise.reject('URLs not an array');
  7099. }
  7100.  
  7101. function imageUrlResolver(url) {
  7102. return urlResolver(url).then(url => verifyImageUrl(url).catch(function(reason) {
  7103. try { url = new URL(url) } catch(e) { return Promise.reject(e) }
  7104. const notFound = Promise.reject('No title image for this URL');
  7105. if (url.hostname.endsWith('pinterest.com'))
  7106. return pinterestResolver(url);
  7107. else if (url.hostname.endsWith('free-picload.com')) {
  7108. if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('free-picload.com', url);
  7109. } else switch (url.hostname) {
  7110. // general image hostings
  7111. case 'www.imgur.com': case 'imgur.com':
  7112. if (url.pathname.startsWith('/a/')) return globalFetch(url, { responseType: 'text' }).then(function(response) {
  7113. if (/^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.test(response.responseText)) try {
  7114. return JSON.parse(RegExp.$1).album_images.images.map(image => 'https://i.imgur.com/'.concat(image.hash, image.ext));
  7115. } catch(e) { debug.warn(e) }
  7116. return notFound;
  7117. });
  7118. return globalFetch(url).then(response => response.document.querySelector('link[rel="image_src"]').href);
  7119. case 'pixhost.to':
  7120. if (url.pathname.startsWith('/gallery/')) return globalFetch(url).then(response =>
  7121. Promise.all(Array.from(response.document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href))));
  7122. if (url.pathname.startsWith('/show/')) return globalFetch(url)
  7123. .then(response => response.document.querySelector('img#image').src);
  7124. break;
  7125. case 'malzo.com':
  7126. if (url.pathname.startsWith('/al/')) return cheveretoGalleryResolver('malzo.com', url);
  7127. break;
  7128. case 'imgbb.com': case 'ibb.co':
  7129. if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('imgbb.com', url);
  7130. break;
  7131. case 'jerking.empornium.ph':
  7132. if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('jerking.empornium.ph', url);
  7133. break;
  7134. case 'imgbox.com':
  7135. if (url.pathname.startsWith('/g/')) return globalFetch(url).then(response =>
  7136. Promise.all(Array.from(response.document.querySelectorAll('div#gallery-view-content > a'))
  7137. .map(a => imageUrlResolver('https://imgbox.com'.concat(a.pathname)))));
  7138. break;
  7139. case 'postimage.org': case 'postimg.cc':
  7140. if (url.pathname.startsWith('/gallery/')) return globalFetch(url, { responseType: 'text' }).then(function(response) {
  7141. if (/\bvar\s+embed_value=(\{[\S\s]+?\});/.test(response.responseText)) try {
  7142. let embed_value = JSON.parse(RegExp.$1);
  7143. return Object.keys(embed_value).map(key => 'https://i.postimg.cc/'
  7144. .concat(embed_value[key][2], '/', embed_value[key][0], '.', embed_value[key][1]))
  7145. } catch(e) { console.warn(e) }
  7146. return notFound;
  7147. });
  7148. break;
  7149. case 'www.imagevenue.com': case 'imagevenue.com':
  7150. return globalFetch(url, { headers: { Referer: 'http://www.imagevenue.com/' } }).then(function(response) {
  7151. var images = Array.from(response.document.querySelectorAll('div.card img')).map(function(img) {
  7152. return img.src.includes('://cdn-images') ? Promise.resolve(img.src) : imageUrlResolver(img.parentNode.href);
  7153. });
  7154. return images.length > 1 ? Promise.all(images) : images.length == 1 ? images[0] : notFound;
  7155. });
  7156. case 'www.imageshack.us': case 'imageshack.us':
  7157. return globalFetch(url).then(response => response.document.querySelector('a#share-dl').href);
  7158. case 'redacted.ch':
  7159. if (url.pathname != '/image.php') break;
  7160. return globalFetch(url, { method: 'HEAD' }).then(response => response.finalUrl);
  7161. case 'demo.cloudimg.io':
  7162. if (!/\b(https?:\/\/\S+)$/.test(url.pathname.concat(url.search, url.hash))) break;
  7163. var resolved = RegExp.$1;
  7164. if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
  7165. return imageResolver(resolved);
  7166. case 'fastpic.ru':
  7167. if (url.pathname.startsWith('/view/'))
  7168. return globalFetch(url).then(response => imageUrlResolver(response.document.querySelector('a.img-a').href));
  7169. if (url.pathname.startsWith('/fullview/')) return globalFetch(url).then(function(response) {
  7170. var node = response.document.getElementById('image');
  7171. if (node != null) return node.src;
  7172. return /\bvar\s+loading_img\s*=\s*'(\S+?)';/.test(response.responseText) ? RegExp.$1 : notFound;
  7173. });
  7174. break;
  7175. case 'www.radikal.ru': case 'radikal.ru': case 'a.radikal.ru':
  7176. return globalFetch(url).then(response => response.document.querySelector('div.mainBlock img').src);
  7177. case 'imageban.ru': case 'ibn.im':
  7178. return globalFetch(url).then(response => response.document.getElementById('img_main').src /* dataset.original */);
  7179. // music-related
  7180. case 'www.musicbrainz.org': case 'musicbrainz.org':
  7181. if (!['release', 'release-group'].some(branch => url.pathname.includes('/'.concat(branch, '/')))) break;
  7182. return globalFetch(url).then(function(response) {
  7183. var node = response.document.querySelector('a.artwork-image');
  7184. if (node != null) return node.href;
  7185. return (node = response.document.querySelector('div.cover-art > img')) != null ? node.src : notFound;
  7186. });
  7187. case 'music.apple.com':
  7188. if (!itunesRlsParser.test(url)) break;
  7189. return globalFetch(url).then(function(response) {
  7190. var meta = response.document.querySelector('meta[property="og:image"][content]');
  7191. if (meta == null || !meta.content) return notFound;
  7192. return verifyImageUrl(meta.content.replace(/\/\d+x\d+\w*(?=\.\w+$)/, '/100000x100000-999')).catch(reason => meta.content);
  7193. });
  7194. case 'www.deezer.com': case 'deezer.com':
  7195. return globalFetch(url).then(function(response) {
  7196. var meta = response.document.querySelector('meta[property="og:image"][content]');
  7197. if (meta == null || !meta.content) return notFound;
  7198. return verifyImageUrl(meta.content.replace(/\/\d+x\d+\w*(?=\.\w+$)/, '/1400x1400-000000-100-0-0'))
  7199. .catch(reason => meta.content);
  7200. });
  7201. case 'www.qobuz.com': case 'qobuz.com':
  7202. if (!url.pathname.includes('/album/')) break;
  7203. return globalFetch(url).then(function(response) {
  7204. var img = response.document.querySelector('div.album-cover > img');
  7205. if (img == null) return notFound;
  7206. return verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_max')).catch(reason => img.src);
  7207. });
  7208. case 'www.prestomusic.com': case 'prestomusic.com':
  7209. if (!url.pathname.includes('/products/')) break;
  7210. return globalFetch(url)
  7211. .then(response => verifyImageUrl(response.document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/)));
  7212. case 'www.bontonland.cz':case 'bontonland.cz':
  7213. return globalFetch(url).then(response => response.document.querySelector('a.detailzoom').href);
  7214. case 'www.nativedsd.com':case 'nativedsd.com':
  7215. if (!url.pathname.includes('/albums/')) break;
  7216. return globalFetch(url).then(response => response.document.querySelector('a#album-cover').href);
  7217. case 'www.prostudiomasters.com': case 'prostudiomasters.com':
  7218. if (!url.pathname.includes('/album/')) break;
  7219. return globalFetch(url).then(function(response) {
  7220. var a = response.document.querySelector('img.album-art');
  7221. return verifyImageUrl(a.currentSrc).catch(reason => a.src);
  7222. });
  7223. case 'www.e-onkyo.com': case 'e-onkyo.com':
  7224. if (!url.pathname.includes('/album/')) break;
  7225. return globalFetch(url).then(function(response) {
  7226. var a = response.document.querySelector('figure > a.colorbox');
  7227. return new URL(response.finalUrl).origin.concat(a.pathname);
  7228. })
  7229. case 'store.acousticsounds.com':
  7230. return globalFetch(url).then(function(response) {
  7231. var link = response.document.querySelector('div#detail > link[rel="image_src"]');
  7232. return verifyImageUrl(link.href.replace(/\/medium\//i, '/large/')).catch(reason => link.href);
  7233. });
  7234. case 'www.indies.eu': case 'indies.eu':
  7235. if (!url.pathname.includes('/alba/')) break;
  7236. return globalFetch(url).then(response => verifyImageUrl)(response.document.querySelector('div.obrazekDetail > img').src);
  7237. case 'www.beatport.com': case 'beatport.com':
  7238. if (!url.pathname.includes('/release/')) break;
  7239. return globalFetch(url).then(response =>
  7240. verifyImageUrl(response.document.querySelector('div > img.interior-release-chart-artwork').src));
  7241. case 'www.supraphonline.cz': case 'supraphonline.cz':
  7242. if (!url.pathname.includes('/album/')) break;
  7243. return globalFetch(url).then(function(response) {
  7244. verifyImageUrl(response.document.querySelector('meta[itemprop="image"]').content.replace(/\?.*$/, '')).catch(reason => notFound);
  7245. });
  7246. case 'vgmdb.net':
  7247. if (!url.pathname.includes('/album/')) break;
  7248. return globalFetch(url).then(function(response) {
  7249. var div = response.document.querySelector('div#coverart');
  7250. return verifyImageUrl(/\b(?:url)\s*\(\"(.*)"\)/i.test(div.style['background-image']) && RegExp.$1).catch(reason => notFound);
  7251. });
  7252. case 'www.ototoy.jp': case 'ototoy.jp':
  7253. return globalFetch(url).then(function(response) {
  7254. var img = response.document.querySelector('div#tralbumArt > a.popupImage');
  7255. return verifyImageUrl(img.dataset.src).catch(reason => img.src);
  7256. });
  7257. case 'music.yandex.ru':
  7258. if (!url.pathname.includes('/album/')) break;
  7259. return globalFetch(url).then(function(response) {
  7260. var script = response.document.querySelector('script.light-data');
  7261. return verifyImageUrl(JSON.parse(script.text).image).catch(reason => notFound);
  7262. });
  7263. case 'mora.jp/':
  7264. if (!url.pathname.includes('/package/')) break;
  7265. return loadMoraMetadata(url).then(packageMeta => packageMeta.packageUrl.concat(packageMeta.fullsizeimage));
  7266. }
  7267. return globalFetch(url, { headers: { 'Referer': url.origin } }).then(function(response) {
  7268. if (url.pathname.startsWith('/album/')
  7269. && response.document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
  7270. return cheveretoGalleryResolver(url.hostname, url);
  7271. var meta = [
  7272. 'head > meta[property="og:image"][content]', 'head > meta[itemprop="image"][content]', 'head > meta[name="og:image"][content]',
  7273. ].reduce((acc, selector) => acc || response.document.querySelector(selector), null);
  7274. return meta != null && meta.content ? meta.content : notFound;
  7275. });
  7276. }));
  7277. }
  7278.  
  7279. function uploadImages(files, elem) {
  7280. if (typeof files != 'object') return Promise.reject('Invalid argument');
  7281. if (!Array.isArray(files)) files = Array.from(files);
  7282. //if (files.length > 1) files.push(files.shift()); // Windows bug
  7283. var frs = files.filter(function(file) {
  7284. return file instanceof File && imageExtensions.some(ext => file.type == 'image/' + ext);
  7285. })/*.sort((file1, file2) => file1.name.localeCompare(file2.name))*/.map(file => new Promise(function(resolve, reject) {
  7286. var reader = new FileReader();
  7287. reader.onload = function() { resolve({ name: file.name, type: file.type, data: reader.result }) };
  7288. reader.onerror = reader.ontimeout = function() { reject('FileReader error (' + file.name + ')') };
  7289. reader.readAsBinaryString(file);
  7290. }));
  7291. return frs.length > 0 ? Promise.all(frs).then(images => upload2PTPIMG(images, elem).catch(function(reason) {
  7292. addMessage('Upload to PTPIMG failed (' + reason + '), falling back to malzo.com', 'warning');
  7293. return upload2Chevereto('malzo.com', images, elem);
  7294. }).catch(function(reason) {
  7295. addMessage('Upload to malzo.com failed (' + reason + '), falling back to imgbb.com', 'warning');
  7296. return upload2Chevereto('imgbb.com', images, elem);
  7297. }).catch(function(reason) {
  7298. addMessage('Upload to imgbb.com failed (' + reason + '), falling back to pixhost.to', 'warning');
  7299. return upload2PixHost(images, elem);
  7300. }).catch(function(reason) {
  7301. addMessage('Upload to pixhost.to failed (' + reason + '), falling back to catbox.moe', 'warning');
  7302. return upload2Catbox(images, elem);
  7303. }).catch(function(reason) {
  7304. addMessage('Upload to catbox.moe failed (' + reason + '), falling back to imgbox.com', 'warning');
  7305. return upload2ImgBox(images, elem);
  7306. }).catch(function(reason) {
  7307. addMessage('Upload to imgbox.com failed (' + reason + '), falling back to jerking.empornium.ph', 'warning');
  7308. return upload2Chevereto('jerking.empornium.ph', images, elem);
  7309. }).catch(reason => Promise.reject('Upload failed to all hosts'))) : Promise.reject('Nothing to upload');
  7310. }
  7311.  
  7312. function upload2PTPIMG(images, elem) {
  7313. if (!Array.isArray(images)) return Promise.reject('invalid argument');
  7314. if (images.length <= 0) return Promise.reject('nothing to upload');
  7315. return getPTPIMGapiKey().then(apiKey => new Promise(function(resolve, reject) {
  7316. var now = Date.now();
  7317. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  7318. var formData = '--' + boundary + '\r\n';
  7319. images.filter(function(image) {
  7320. return image.data && image.name && ['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(ext => image.type == 'image/'.concat(ext));
  7321. }).forEach(function(image, ndx) {
  7322. formData += 'Content-Disposition: form-data; name="file-upload[' + ndx + ']"; filename="' + image.name.toASCII() + '"\r\n';
  7323. formData += 'Content-Type: ' + image.type + '\r\n\r\n';
  7324. formData += image.data + '\r\n';
  7325. formData += '--' + boundary + '\r\n';
  7326. });
  7327. formData += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
  7328. formData += apiKey + '\r\n';
  7329. formData += '--' + boundary + '--\r\n';
  7330. GM_xmlhttpRequest({
  7331. method: 'POST',
  7332. url: ptpimgOrigin + '/upload.php',
  7333. responseType: 'json',
  7334. headers: {
  7335. 'Accept': 'application/json',
  7336. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  7337. 'Content-Length': formData.length,
  7338. },
  7339. data: formData,
  7340. binary: true,
  7341. timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
  7342. onload: function(response) {
  7343. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  7344. if (!response.response) return reject('void response');
  7345. if (response.response.length < images.length)
  7346. return reject(`not all images uploaded (${response.response.length}/${images.length})`)
  7347. if (response.response.length > images.length)
  7348. console.warn('PTPimg returns more links than expected (', response.response, images, ')');
  7349. resolve(response.response.map(function(item, ndx) {
  7350. if (!item.ext && /\.([a-z]+)(?=$|[\#\?])/i.test(images[ndx].name)) item.ext = RegExp.$1;
  7351. return ptpimgOrigin.concat('/', item.code, '.', item.ext);
  7352. }));
  7353. },
  7354. onprogress: elem instanceof HTMLElement && 'value' in elem ? function(progress) {
  7355. var pct = progress.position * 100 / progress.total;
  7356. //elem.value = 'Uploading... (' + Math.round(pct) + '%)';
  7357. } : undefined,
  7358. onerror: error => reject(defaultErrorHandler(error)),
  7359. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  7360. });
  7361. }));
  7362. }
  7363.  
  7364. function rehost2PTPIMG(urls) {
  7365. if (!Array.isArray(urls)) return Promise.reject('Invalid parameter');
  7366. if (urls.length <= 0) return Promise.resolve([]); //Promise.reject('Nothing to rehost');
  7367. return Promise.all(urls.map(function(url) {
  7368. if (!urlParser.test(url)) return Promise.reject('URL not valid ('.concat(url, ')'));
  7369. var hostname = new URL(url).hostname;
  7370. if (hostname == 'img.discogs.com' || hostname.endsWith('omdb.org')) {
  7371. return verifyImageUrl('https://reho.st/'.concat(url))
  7372. .catch(reason => rehost2Catbox(url))
  7373. .catch(reason => rehost2PixHost(url))
  7374. .catch(reason => reupload2PTPIMG(url));
  7375. } else if (!['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(ext => url.toLowerCase().endsWith('.'.concat(ext)))) {
  7376. return verifyImageUrl(url.concat('#.jpg'))
  7377. .catch(reason => rehost2Chevereto('malzo.com', url))
  7378. .catch(reason => rehost2PixHost(url))
  7379. .catch(reason => rehost2Chevereto('imgbb.com', url))
  7380. .catch(reason => rehost2Chevereto('jerking.empornium.ph', url))
  7381. .catch(reason => rehost2Chevereto('free-picload.com', url));
  7382. }
  7383. return verifyImageUrl(url);
  7384. })).then(imageUrls => getPTPIMGapiKey().then(function(apiKey) {
  7385. if (prefs.diag_mode) console.debug('rehost2PTPIMG(...) input:', imageUrls);
  7386. var formData = new URLSearchParams({
  7387. 'link-upload': imageUrls.join('\r\n'),
  7388. 'api_key': apiKey,
  7389. });
  7390. return globalFetch(ptpimgOrigin + '/upload.php', {
  7391. responseType: 'json',
  7392. timeout: imageUrls.length * rehostTimeout,
  7393. }, formData).then(function(response) {
  7394. if (!response.response) return Promise.reject('void response');
  7395. if (response.response.length < imageUrls.length)
  7396. return Promise.reject(`not all images rehosted to (${response.response.length}/${imageUrls.length})`)
  7397. if (response.response.length > imageUrls.length)
  7398. console.warn('PTPimg returns more links than expected (', response.response, imageUrls, ')');
  7399. return response.response.map(function(item, ndx) {
  7400. if (!item.ext && /\.([a-z]+)(?=$|[\#\?])/i.test(imageUrls[ndx])) item.ext = RegExp.$1;
  7401. return ptpimgOrigin.concat('/', item.code, '.', item.ext);
  7402. });
  7403. });
  7404. }));
  7405. }
  7406.  
  7407. function reupload2PTPIMG(imgUrl) {
  7408. console.warn('PTPIMG rehoster fallback to local reupload');
  7409. return globalFetch(imgUrl, { responseType: 'blob' }).then(function(response) {
  7410. var image = {
  7411. name: imgUrl.replace(/^.*\//, ''),
  7412. data: response.responseText,
  7413. size: response.responseText.size,
  7414. };
  7415. switch (imgUrl.replace(/^.*\./, '').toLowerCase()) {
  7416. case 'jpg': case 'jpeg': case 'jfif': image.type = 'image/jpeg'; break;
  7417. case 'png': image.type = 'image/png'; break;
  7418. case 'gif': image.type = 'image/gif'; break;
  7419. case 'bmp': image.type = 'image/bmp'; break;
  7420. default: return Promise.reject('Unsupported extension');
  7421. }
  7422. return upload2PTPIMG([image]).then(imgUrls => imgUrls[0]);
  7423. });
  7424. }
  7425.  
  7426. function getPTPIMGapiKey() {
  7427. if (prefs.ptpimg_api_key) return Promise.resolve(prefs.ptpimg_api_key);
  7428. try {
  7429. var apiKey = JSON.parse(window.localStorage.ptpimg_it).api_key;
  7430. if (apiKey) {
  7431. GM_setValue('ptpimg_api_key', prefs.ptpimg_api_key = apiKey);
  7432. return Promise.resolve(apiKey);
  7433. }
  7434. } catch(e) { if (prefs.diag_mode) console.debug('getPTPIMGapiKey():', e) }
  7435. return globalFetch(ptpimgOrigin).then(function(response) {
  7436. if ((apiKey = response.document.getElementById('api_key')) == null) {
  7437. let counter = GM_getValue('ptpimg_reminder_read', 0);
  7438. if (counter < 5) {
  7439. alert(`
  7440. PTPIMG API key could not be captured. Please login to ${ptpimgOrigin}/ and redo the action.
  7441.  
  7442. If you don\'t have PTPIMG account, consider to set auto_rehost_cover to false in preferences
  7443. (Tampermonkey extension menu -> right click to Upload Assistant -> Storage tab)
  7444.  
  7445. Direct images uploading is still available to fallback image hosts (proxied).
  7446. `);
  7447. GM_setValue('ptpimg_reminder_read', ++counter);
  7448. }
  7449. return Promise.reject('PTPIMG API key not configured');
  7450. }
  7451. if (!apiKey.value) return Promise.reject('Assertion failed: missing PTPIMG API key');
  7452. GM_setValue('ptpimg_api_key', prefs.ptpimg_api_key = apiKey.value);
  7453. Promise.resolve(prefs.ptpimg_api_key).then(apiKey => { alert(`Your PTPIMG API key [${apiKey}] was successfully configured`) });
  7454. return prefs.ptpimg_api_key;
  7455. });
  7456. }
  7457.  
  7458. function upload2Chevereto(hostname, images, elem) {
  7459. if (!Array.isArray(images)) return Promise.reject('invalid argument');
  7460. if (images.length <= 0) return Promise.reject('nothing to upload');
  7461. const anonSessionLimits = {
  7462. 'malzo.com': 2,
  7463. 'imgbb.com': 2,
  7464. 'jerking.empornium.ph': 5,
  7465. 'free-picload.com': 50,
  7466. };
  7467. return setCheveretoSession(hostname).then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
  7468. switch (hostname) {
  7469. case 'malzo.com':
  7470. case 'jerking.empornium.ph':
  7471. case 'free-picload.com':
  7472. if (!['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].some(ext => image.type == 'image/'.concat(ext)))
  7473. throw 'MIME type not supported: '.concat(image.type);
  7474. break;
  7475. case 'imgbb.com':
  7476. if (!['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'].some(ext => image.type == 'image/'.concat(ext)))
  7477. throw 'MIME type not supported: '.concat(image.type);
  7478. break;
  7479. }
  7480. var anonSessionLimit = anonSessionLimits[hostname.toLowerCase()];
  7481. if (!session.username && anonSessionLimit > 0 && image.size > anonSessionLimit * 2**20)
  7482. throw 'image size exceeds anonymous upload limit';
  7483. const boundary = '----WebKitFormBoundary'.concat(Date.now().toString(16).toUpperCase());
  7484. var formData = '--' + boundary + '\r\n', params = Object.assign({
  7485. action: 'upload',
  7486. type: 'file',
  7487. nsfw: 0,
  7488. }, session);
  7489. Object.keys(params).forEach(function(field, index, arr) {
  7490. formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
  7491. formData += params[field] + '\r\n';
  7492. formData += '--' + boundary + '\r\n';
  7493. });
  7494. formData += 'Content-Disposition: form-data; name="source"; filename="' + image.name.toASCII() + '"\r\n';
  7495. formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n';
  7496. formData += '--' + boundary + '--\r\n';
  7497. GM_xmlhttpRequest({
  7498. method: 'POST',
  7499. url: 'https://'.concat(hostname, '/json'),
  7500. responseType: 'json',
  7501. headers: {
  7502. 'Accept': 'application/json',
  7503. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  7504. 'Content-Length': formData.length,
  7505. 'Referer': 'https://'.concat(hostname, '/'),
  7506. },
  7507. data: formData,
  7508. binary: true,
  7509. timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
  7510. onload: function(response) {
  7511. if (response.status >= 200 && response.status < 400) {
  7512. if (response.response.success) resolve(response.response.image.url);
  7513. else reject(response.response.error.message.concat(' (', response.response.status_code, ')'));
  7514. } else reject(defaultErrorHandler(response));
  7515. },
  7516. onerror: error => reject(defaultErrorHandler(error)),
  7517. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  7518. });
  7519.  
  7520. function formField(key, value) {
  7521. return 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n--' + boundary;
  7522. }
  7523. }))));
  7524. }
  7525.  
  7526. function rehost2Chevereto(hostname, url) {
  7527. return verifyImageUrl(url).then(imageUrl => setCheveretoSession(hostname).then(function(session) {
  7528. var formData = new URLSearchParams(Object.assign({
  7529. action: 'upload',
  7530. type: 'url',
  7531. nsfw: 0,
  7532. source: imageUrl,
  7533. }, session));
  7534. return globalFetch('https://'.concat(hostname, '/json'), {
  7535. responseType: 'json',
  7536. headers: { 'Referer': 'https://'.concat(hostname, '/') },
  7537. timeout: rehostTimeout,
  7538. }, formData).then(function(response) {
  7539. return response.response.success ? response.response.image.url
  7540. : Promise.reject(hostname.concat(': ', response.response.error.message,' (', response.response.status_code, ')'));
  7541. });
  7542. }));
  7543. }
  7544.  
  7545. function cheveretoGalleryResolver(hostname, url) {
  7546. var albumId = /^\/(?:album|al)\/(\w+)\b/.test(url.pathname) && RegExp.$1;
  7547. if (!albumId) return Promise.reject('Invlaid gallery URL');
  7548. return setCheveretoSession(hostname).then(function(session) {
  7549. var formData = new URLSearchParams(Object.assign({
  7550. action: 'get-album-contents',
  7551. albumid: albumId,
  7552. }, session));
  7553. return globalFetch(url.origin.concat('/json'), {
  7554. responseType: 'json',
  7555. headers: { 'Referer': url },
  7556. }, formData).then(function(response) {
  7557. return response.response.status_txt == 'OK' && Array.isArray(response.response.contents) ?
  7558. response.response.contents.map(image => image.url)
  7559. : Promise.reject(hostname.concat(': ', response.response.error.message,' (', response.response.status_code, ')'));
  7560. });
  7561. }).catch(function(reason) {
  7562. console.warn(hostname, 'gallery couldn\'t be resolved via API:', reason, '(falling back to HTML parser)');
  7563. return new Promise(function(resolve, reject) {
  7564. var urls = [], domParser = new DOMParser;
  7565. getPage(url);
  7566.  
  7567. function getPage(url) {
  7568. GM_xmlhttpRequest({ method: 'GET', url: url, headers: { Referer: url },
  7569. onload: function(response) {
  7570. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  7571. var dom = domParser.parseFromString(response.responseText, 'text/html');
  7572. Array.prototype.push.apply(urls,
  7573. Array.from(dom.querySelectorAll('div.list-item-image > a.image-container')).map(a => a.href));
  7574. var next = dom.querySelector('a[data-pagination="next"][href]');
  7575. if (next == null || !next.href) resolve(urls); else getPage(next.href);
  7576. },
  7577. onerror: response => { reject(defaultErrorHandler(response)) },
  7578. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  7579. });
  7580. }
  7581. }).then(urls => Promise.all(urls.map(imageUrlResolver)));
  7582. });
  7583. }
  7584.  
  7585. function setCheveretoSession(hostname) {
  7586. const index = 'https://'.concat(hostname, '/');
  7587. return globalFetch(index).then(function(response) {
  7588. if (!/\b(?:auth_token)\s*=\s*"(\w+)"/.test(response.responseText)) return Promise.reject('Auth token detection failure');
  7589. var session = {
  7590. auth_token: RegExp.$1,
  7591. timestamp: Date.now(),
  7592. };
  7593. if (getUser(response)) return session;
  7594. if (hostname.toLowerCase() == 'free-picload.com') var hostPrefix = 'picload_';
  7595. else if (/^([\w\-]+)(?:\.[\w\-]+)+$/.test(hostname)) hostPrefix = RegExp.$1.toLowerCase().concat('_');
  7596. if (!hostPrefix || !prefs[hostPrefix.concat('uid')] || !prefs[hostPrefix.concat('password')]) return session;
  7597. var formData = new URLSearchParams({
  7598. 'login-subject': prefs[hostPrefix.concat('uid')],
  7599. 'password': prefs[hostPrefix.concat('password')],
  7600. 'auth_token': session.auth_token,
  7601. });
  7602. return new Promise(function(resolve, reject) {
  7603. GM_xmlhttpRequest({ method: 'POST', url: 'https://'.concat(hostname, '/login'),
  7604. headers: {
  7605. 'Accept': '*/*',
  7606. 'Content-Type': 'application/x-www-form-urlencoded',
  7607. 'Content-Length': formData.toString().length,
  7608. 'Referer': 'https://'.concat(hostname, '/login'),
  7609. }, data: formData.toString(),
  7610. onload: function(response) {
  7611. if (response.status < 200 || response.status > 400) defaultErrorHandler(response);
  7612. resolve(response.status);
  7613. },
  7614. onerror: function(response) {
  7615. reject(defaultErrorHandler(response));
  7616. //resolve(response.status);
  7617. },
  7618. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  7619. });
  7620. }).then(status => globalFetch(index, { responseType: 'text' }).then(function(response) {
  7621. if (getUser(response)) console.debug(hostname, 'authorized session:', session);
  7622. else console.debug(hostname, 'authorization failed:', status, '(continuing anonymous)');
  7623. return session;
  7624. }));
  7625.  
  7626. function getUser(response) {
  7627. if (/\b(?:logged_user)\s*=\s*(\{.*?\});/.test(response.responseText)) try {
  7628. let logged_user = JSON.parse(RegExp.$1);
  7629. session.username = logged_user.username;
  7630. session.userid = logged_user.id;
  7631. return Boolean(logged_user.username || logged_user.id);
  7632. } catch(e) { console.warn(e) }
  7633. return false;
  7634. }
  7635. });
  7636. }
  7637.  
  7638. function upload2PixHost(images, elem) {
  7639. if (!Array.isArray(images)) return Promise.reject('invalid argument');
  7640. if (images.length <= 0) return Promise.reject('nothing to upload');
  7641. return Promise.all(images.map(image => new Promise(function(resolve, reject) {
  7642. if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
  7643. throw 'MIME type not supported: '.concat(image.type);
  7644. var now = Date.now();
  7645. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  7646. var formData = '--' + boundary + '\r\n';
  7647. formData += 'Content-Disposition: form-data; name="img"; filename="' + image.name.toASCII() + '"\r\n';
  7648. formData += 'Content-Type: ' + image.type + '\r\n\r\n';
  7649. formData += image.data + '\r\n';
  7650. formData += '--' + boundary + '\r\n';
  7651. formData += 'Content-Disposition: form-data; name="content_type"\r\n\r\n';
  7652. formData += '0\r\n';
  7653. formData += '--' + boundary + '--\r\n';
  7654. GM_xmlhttpRequest({
  7655. method: 'POST',
  7656. url: 'https://api.pixhost.to/images',
  7657. responseType: 'json',
  7658. headers: {
  7659. 'Accept': 'application/json',
  7660. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  7661. 'Content-Length': formData.length,
  7662. },
  7663. data: formData,
  7664. binary: true,
  7665. timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
  7666. onload: function(response) {
  7667. if (response.status >= 200 && response.status < 400) resolve(response.response.show_url);
  7668. else reject(defaultErrorHandler(response));
  7669. },
  7670. onerror: error => reject(defaultErrorHandler(error)),
  7671. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  7672. });
  7673. }).then(imageUrlResolver)));
  7674. }
  7675.  
  7676. function rehost2PixHost(url) {
  7677. return verifyImageUrl(url).then(function(imageUrl) {
  7678. var formData = new URLSearchParams({
  7679. imgs: imageUrl,
  7680. content_type: 0,
  7681. tos: 'on',
  7682. });
  7683. return globalFetch('https://pixhost.to/remote/', {
  7684. responseType: 'text',
  7685. timeout: rehostTimeout,
  7686. }, formData).then(function(response) {
  7687. if (!/\b(?:upload_results)\s*=\s*(\{.*\});$/m.test(response.responseText)) return Promise.reject('page parsing error');
  7688. var images = JSON.parse(RegExp.$1).images;
  7689. if (images.length < imageUrls.length) return Promise.reject('image not rehosted');
  7690. return imageUrlResolver(images[0].show_url);
  7691. });
  7692. });
  7693. }
  7694.  
  7695. function upload2Catbox(images, elem) {
  7696. if (!Array.isArray(images)) return Promise.reject('Invalid argument');
  7697. if (images.length <= 0) return Promise.reject('Nothing to upload or format not supported');
  7698. return getCatboxUserHash().catch(reason => undefined).then(userHash => Promise.all(images.map(image => new Promise(function(resolve, reject) {
  7699. var now = Date.now();
  7700. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  7701. var formData = '--' + boundary + '\r\n';
  7702. formData += 'Content-Disposition: form-data; name="reqtype"\r\n\r\n';
  7703. formData += 'fileupload\r\n';
  7704. formData += '--' + boundary + '\r\n';
  7705. if (userHash) {
  7706. formData += 'Content-Disposition: form-data; name="userhash"\r\n\r\n';
  7707. formData += userHash + '\r\n';
  7708. formData += '--' + boundary + '\r\n';
  7709. }
  7710. formData += 'Content-Disposition: form-data; name="fileToUpload"; filename="' + image.name.toASCII() + '"\r\n';
  7711. formData += 'Content-Type: ' + image.type + '\r\n\r\n';
  7712. formData += image.data + '\r\n';
  7713. formData += '--' + boundary + '--\r\n';
  7714. GM_xmlhttpRequest({
  7715. method: 'POST',
  7716. url: 'https://catbox.moe/user/api.php',
  7717. responseType: 'text',
  7718. headers: {
  7719. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  7720. 'Content-Length': formData.length,
  7721. },
  7722. data: formData,
  7723. binary: true,
  7724. timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
  7725. onload: function(response) {
  7726. if (response.status >= 200 && response.status < 400) resolve(response.responseText);
  7727. else reject(defaultErrorHandler(response));
  7728. },
  7729. onerror: error => reject(defaultErrorHandler(error)),
  7730. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  7731. });
  7732. }))));
  7733. }
  7734.  
  7735. function rehost2Catbox(url) {
  7736. return verifyImageUrl(url).then(imageUrl => getCatboxUserHash().catch(reason => undefined).then(function(userHash) {
  7737. var formData = new URLSearchParams({
  7738. reqtype: 'urlupload',
  7739. url: imageUrl,
  7740. });
  7741. if (userHash) formData.set('userhash', userHash);
  7742. return globalFetch('https://catbox.moe/user/api.php', {
  7743. responseType: 'text',
  7744. timeout: rehostTimeout,
  7745. }, formData).then(response => response.responseText);
  7746. }));
  7747. }
  7748.  
  7749. function getCatboxUserHash() {
  7750. return prefs.catbox_userhash ? Promise.resolve(prefs.catbox_userhash) : globalFetch('https://catbox.moe/').then(function(response) {
  7751. var userHash = response.document.querySelector('input[name="userhash"][value]');
  7752. return userHash != null && userHash.value || Promise.reject('Catbox.moe: not logged in or userhash not found');
  7753. });
  7754. }
  7755.  
  7756. function upload2ImgBox(images, elem) {
  7757. if (!Array.isArray(images)) return Promise.reject('invalid argument');
  7758. if (images.length <= 0) return Promise.reject('nothing to upload');
  7759. return setImgBoxSession().then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
  7760. if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
  7761. throw 'MIME type not supported: '.concat(image.type);
  7762. var now = Date.now();
  7763. const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
  7764. var formData = '--' + boundary + '\r\n';
  7765. Object.keys(session.params).forEach(function(field, index, arr) {
  7766. formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
  7767. formData += session.params[field] + '\r\n';
  7768. formData += '--' + boundary + '\r\n';
  7769. });
  7770. formData += 'Content-Disposition: form-data; name="files[]"; filename="' + image.name.toASCII() + '"\r\n';
  7771. formData += 'Content-Type: ' + image.type + '\r\n\r\n';
  7772. formData += image.data + '\r\n';
  7773. formData += '--' + boundary + '--\r\n';
  7774. GM_xmlhttpRequest({
  7775. method: 'POST',
  7776. url: 'https://imgbox.com/upload/process',
  7777. headers: {
  7778. 'Accept': 'application/json',
  7779. 'Content-Type': 'multipart/form-data; boundary=' + boundary,
  7780. 'Content-Length': formData.length,
  7781. 'X-CSRF-Token': session.csrf_token,
  7782. },
  7783. data: formData,
  7784. responseType: 'json',
  7785. binary: true,
  7786. timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
  7787. onload: function(response) {
  7788. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  7789. resolve(response.response.files[0].original_url);
  7790. },
  7791. onerror: error => reject(defaultErrorHandler(error)),
  7792. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  7793. });
  7794. }))));
  7795. }
  7796.  
  7797. function setImgBoxSession() {
  7798. return globalFetch('https://imgbox.com/').then(function(response) {
  7799. var csrfToken = response.document.querySelector('meta[name="csrf-token"]');
  7800. if (csrfToken == null) return Promise.reject('ImgBox.com session token not found');
  7801. if (prefs.diag_mode) console.debug('ImgBox.com session token:', csrfToken.content);
  7802. if (response.document.querySelector('div.btn-group > ul.dropdown-menu') != null) return csrfToken.content;
  7803. if (!prefs.imgbox_uid || !prefs.imgbox_password) return csrfToken.content;
  7804. var formData = new URLSearchParams({
  7805. "utf8": "✓",
  7806. "authenticity_token": csrfToken.content,
  7807. "user[login]": prefs.imgbox_uid,
  7808. "user[password]": prefs.imgbox_password,
  7809. });
  7810. GM_xmlhttpRequest({ method: 'POST', url: 'https://imgbox.com/login', headers: {
  7811. 'Referer': 'https://imgbox.com/login',
  7812. 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
  7813. 'Content-Length': formData.toString().length,
  7814. }, data: formData.toString() });
  7815. return new Promise(function(resolve, reject) {
  7816. setTimeout(() => { globalFetch('http://imgbox.com/').then(function(response) {
  7817. if (response.document.querySelector('div.btn-group > ul.dropdown-menu') == null)
  7818. console.warn('ImgBox.com login failed, continuing as anonymous', response);
  7819. if ((csrfToken = response.document.querySelector('meta[name="csrf-token"]')) != null) {
  7820. if (prefs.diag_mode) console.debug('ImgBox.com session token after login:', csrfToken.content);
  7821. resolve(csrfToken.content);
  7822. } else reject('ImgBox.com session token not found');
  7823. }) }, 1000);
  7824. });
  7825. }).then(csrfToken => globalFetch('https://imgbox.com/ajax/token/generate', {
  7826. method: 'POST',
  7827. responseType: 'json',
  7828. headers: { 'X-CSRF-Token': csrfToken },
  7829. }).then(response => ({
  7830. csrf_token: csrfToken,
  7831. params: {
  7832. token_id: response.response.token_id,
  7833. token_secret: response.response.token_secret,
  7834. content_type: 1,
  7835. thumbnail_size: '100c',
  7836. gallery_id: null,
  7837. gallery_secret: null,
  7838. comments_enabled: 0,
  7839. },
  7840. })));
  7841. }
  7842.  
  7843. function dcFmtToGazelle(format) {
  7844. if (/^(?:CD|CDi|CDr|HDCD)\b/.test(format)) return 'CD';
  7845. 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';
  7846. if (/^(?:Vinyl|LP|\d+(?:\.\d+)?\s*")$/.test(format)) return 'Vinyl';
  7847. if (/\b(?:SACD|Hybrid)\b/.test(format)) return 'SACD';
  7848. if (/^(?:Blu[ \-]?ray)\b/i.test(format)) return isOPS ? 'BD' : 'Blu-Ray';
  7849. if (/^(?:DVD|HD\s+DVD)/.test(format)) return 'DVD';
  7850. if (/^(?:Cassette|Microcassette)$/i.test(format)) return 'Cassette';
  7851. if (/^(?:DAT)$/.test(format)) return 'DAT';
  7852. if (/^(?:Soundboard)$/i.test(format)) return 'Soundboard';
  7853. //if (/^(?:Memory\s+Stick)$/i.test(format)) return ??
  7854. return null;
  7855. }
  7856.  
  7857. function queryAjaxAPI(action, params) {
  7858. if (!action) return Promise.reject('Action missing');
  7859. var retryCount = 0;
  7860. return new Promise(function(resolve, reject) {
  7861. params = new URLSearchParams(params || undefined);
  7862. params.set('action', action);
  7863. var url = '/ajax.php?'.concat(params);
  7864. var xhr = new XMLHttpRequest();
  7865. queryInternal();
  7866.  
  7867. function queryInternal() {
  7868. var now = Date.now();
  7869. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  7870. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  7871. apiTimeFrame.timeStamp = now;
  7872. apiTimeFrame.requestCounter = 1;
  7873. } else ++apiTimeFrame.requestCounter;
  7874. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  7875. if (apiTimeFrame.requestCounter <= 5) {
  7876. xhr.open('GET', url, true);
  7877. xhr.setRequestHeader('Accept', 'application/json');
  7878. if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
  7879. xhr.responseType = 'json';
  7880. xhr.onload = function() {
  7881. if (xhr.status == 404) return reject('not found');
  7882. if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
  7883. if (xhr.response.status == 'success') return resolve(xhr.response.response);
  7884. if (xhr.response.error == 'not found') return reject(xhr.response.error);
  7885. console.warn('queryAjaxAPI.queryInternal(...) response:', xhr, xhr.response);
  7886. if (xhr.response.error == 'rate limit exceeded') {
  7887. console.warn('queryAjaxAPI.queryInternal(...) ' + xhr.response.error + ':', apiTimeFrame, now, retryCount);
  7888. if (retryCount++ <= 10) return setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  7889. }
  7890. reject('API '.concat(xhr.response.status, ': ', xhr.response.error));
  7891. };
  7892. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  7893. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  7894. xhr.timeout = 10000;
  7895. xhr.send();
  7896. } else {
  7897. setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  7898. if (prefs.diag_mode) console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
  7899. action + ' (' + apiTimeFrame.requestCounter + ')');
  7900. if (prefs.messages_verbosity >= 1) {
  7901. addMessage('AJAX API request exceeding time frame: action=' + action + ' (' + apiTimeFrame.requestCounter + ')', 'notice');
  7902. } else addMessage('please wait for next AJAX timeframe', 'notice');
  7903. }
  7904. }
  7905. });
  7906. }
  7907.  
  7908. function validataTorrentFile(torrent) {
  7909. tfMessages.forEach(node => { node.remove() });
  7910. tfMessages = [];
  7911. var fr = new FileReader;
  7912. fr.onload = function(evt) {
  7913. torrent = bdecode(new Uint8Array(fr.result));
  7914. var rootImageCount = 0, category = document.getElementById('categories');
  7915. torrent.info.files.forEach(function(file) {
  7916. var rootFolderName = decodeURIComponent(escape(torrent.info.name)),
  7917. fullPath = decodeURIComponent(escape(file.path.join('/'))),
  7918. fileName = decodeURIComponent(file.path[file.path.length - 1]),
  7919. totalLen = rootFolderName.trueLength() + 1 + fullPath.trueLength();
  7920. if (totalLen > 180) tfMessages.push(addMessage(new HTML('file "' + safeText(fullPath).bold() +
  7921. '" exceeding allowed length (' + totalLen + ' > 180)'), 'warning'));
  7922. if (file.path.length == 1 && imageExtensions.some(ext => fileName.toLowerCase().endsWith('.' + ext))) {
  7923. ++rootImageCount;
  7924. if (!/^(?:cover|front|artworks)\.\w+$/i.test(fileName) && category != null && category.value == 'Music')
  7925. tfMessages.push(addMessage('Nonstandard cover image name: ' + fileName, 'notice'));
  7926. }
  7927. if (/\.(?:torrent|\!ut|\!qb|url|lnk)$/i.test(fileName))
  7928. tfMessages.push(addMessage(new HTML('forbidden file "' + safeText(fullPath).bold() + '"'), 'warning'));
  7929. if (/^(?:(?:MediaInfo)\.txt|(?:Lossless Audio Checker|audiochecker)\.log)$/i.test(fileName))
  7930. tfMessages.push(addMessage('Auxiliary text file in torrent: ' + fullPath, 'notice'));
  7931. if (/^(?:thumb\.jpg)$/i.test(fileName)) tfMessages.push(addMessage('thumb.jpg in torrent', 'notice'));
  7932. });
  7933. if (category != null && category.value == 'Music') {
  7934. if (rootImageCount > 1) tfMessages.push(addMessage(`More images (${rootImageCount}) in root folder`, 'notice'));
  7935. if (rootImageCount <= 0) tfMessages.push(addMessage('No cover image in root folder', 'notice'));
  7936. }
  7937. ref = document.querySelector('td.ua-messages-bg');
  7938. if (ref != null && ref.childElementCount <= 0) ref.parentNode.remove();
  7939. };
  7940. fr.onerror = fr.ontimeout = error => { console.error('FileReader error (' + torrent.name + ')') };
  7941. fr.readAsArrayBuffer(torrent);
  7942.  
  7943. function bdecode(str) {
  7944. var pos = 0, infoBegin = 0, infoEnd = 0;
  7945. return bdecodeInternal(str);
  7946.  
  7947. function bdecodeInternal(str) {
  7948. if (pos > str.length) return null;
  7949. switch (str[pos]) {
  7950. case 100: // char code for 'd'
  7951. ++pos;
  7952. var retval = [];
  7953. while (str[pos] != 101) { // char code for 'e'
  7954. let key = bdecodeInternal(str), val = bdecodeInternal(str);
  7955. if (key === null || val === null) break;
  7956. retval[key] = val;
  7957. }
  7958. if (infoEnd == -1) infoEnd = pos + 1;
  7959. retval.isDct = true;
  7960. ++pos;
  7961. return retval;
  7962. case 105: // char code for 'i'
  7963. ++pos;
  7964. var digits = Array.prototype.indexOf.call(str, 101, pos) - pos; // 101 = char code for 'e'
  7965. val = '';
  7966. for (var i = pos; i < digits + pos; ++i) val += String.fromCharCode(str[i]);
  7967. val = Math.round(parseFloat(val));
  7968. pos += digits + 1;
  7969. return val;
  7970. case 108: // char code for 'l'
  7971. ++pos;
  7972. retval = [];
  7973. while (str[pos] != 101) { // char code for 'e'
  7974. let val = bdecodeInternal(str);
  7975. if (val === null) break;
  7976. retval.push(val);
  7977. }
  7978. ++pos;
  7979. return retval;
  7980. default:
  7981. digits = Array.prototype.indexOf.call(str, 58, pos) - pos; // 58 = char code for ':'
  7982. if (digits < 0 || digits > 20) return null;
  7983. var len = '';
  7984. for (i = pos; i < digits + pos; ++i) len += String.fromCharCode(str[i]);
  7985. len = parseInt(len);
  7986. pos += digits + 1;
  7987. var fstring = '';
  7988. for (i = pos; i < len + pos; ++i) fstring += String.fromCharCode(str[i]);
  7989. pos += len;
  7990. if (fstring == 'info') {
  7991. infoBegin = pos;
  7992. infoEnd = -1;
  7993. }
  7994. return fstring;
  7995. }
  7996. }
  7997. }
  7998. }
  7999.  
  8000. function defaultErrorHandler(response) {
  8001. console.error('HTTP error:', response);
  8002. var e = 'HTTP error ' + response.status;
  8003. if (response.statusText) e += ' (' + response.statusText + ')';
  8004. if (response.error) e += ' (' + response.error + ')';
  8005. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  8006. return e;
  8007. }
  8008.  
  8009. function defaultTimeoutHandler(response) {
  8010. console.error('HTTP timeout:', response);
  8011. var e = 'HTTP timeout';
  8012. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  8013. return e;
  8014. }