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

  1. // ==UserScript==
  2. // @name [RED/OPS/NWCD] Upload Assistant
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.324
  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. // @copyright 2020, Anakunda (https://greasyfork.org/users/321857-anakunda)
  8. // @license GPL-3.0-or-later
  9. // @iconURL https://redacted.ch/favicon.ico
  10. // @match https://redacted.ch/upload.php*
  11. // @match https://redacted.ch/torrents.php?action=editgroup&*
  12. // @match https://redacted.ch/torrents.php?action=edit&*
  13. // @match https://redacted.ch/requests.php?action=new*
  14. // @match https://redacted.ch/requests.php?action=edit*
  15. // @match https://notwhat.cd/upload.php*
  16. // @match https://notwhat.cd/torrents.php?action=editgroup&*
  17. // @match https://notwhat.cd/torrents.php?action=edit&*
  18. // @match https://notwhat.cd/requests.php?action=new*
  19. // @match https://notwhat.cd/requests.php?action=edit*
  20. // @match https://orpheus.network/upload.php*
  21. // @match https://orpheus.network/torrents.php?action=editgroup&*
  22. // @match https://orpheus.network/torrents.php?action=edit&*
  23. // @match https://orpheus.network/requests.php?action=new*
  24. // @match https://orpheus.network/requests.php?action=edit*
  25. // @connect file://*
  26. // @connect *
  27. // @grant GM_xmlhttpRequest
  28. // @grant GM_getValue
  29. // @grant GM_setValue
  30. // @grant GM_deleteValue
  31. // @grant GM_openInTab
  32. // @require https://greasyfork.org/scripts/406257-qobuzlib/code/QobuzLib.js
  33. // @require https://greasyfork.org/scripts/408084-xhrlib/code/xhrLib.js
  34. // @require https://greasyfork.org/scripts/401726-imagehostuploader/code/imageHostUploader.js
  35. // @require https://greasyfork.org/scripts/404516-progressbars/code/progressBars.js
  36. // @require https://greasyfork.org/scripts/408277-libstringdistance/code/libStringDistance.js
  37. // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
  38. // @require https://greasyfork.org/scripts/406786-langcodes/code/langCodes.js
  39. // ==/UserScript==
  40.  
  41. // Additional setup: to work, set the pattern below as built-in foobar2000 copy command or custom Text Tools plugin quick copy command
  42. // $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)[%__channel_mode%]$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)[%replaygain_album_peak%]$char(30)[%replaygain_track_gain%]$char(30)[%replaygain_track_peak%]$char(30)[%album dynamic range%]$char(30)[%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) [LANGUAGE=$trim($replace(%LANGUAGE%, ,_)) ][ORIGINALFORMAT=$trim($replace(%ORIGINALFORMAT%, ,_)) ][BPM=$trim(%BPM%) ][MD5=$info(md5)])$char(30)[%lyrics%],$char(13),$char(29)),$char(10),$char(28))
  43. //
  44. // As alternative to pasted playlist, e.g. requests creation, valid URL to page on supported web can be used.
  45. // List of supported domains:
  46. //
  47. // For music releases:
  48. // - qobuz.com
  49. // - highresaudio.com
  50. // - bandcamp.com
  51. // - prestomusic.com
  52. // - discogs.com
  53. // - supraphonline.cz
  54. // - bontonland.cz (closing soon)
  55. // - nativedsd.com
  56. // - junodownload.com
  57. // - hdtracks.com
  58. // - deezer.com
  59. // - spotify.com
  60. // - prostudiomasters.com
  61. // - play.google.com (music)
  62. // - 7digital.com
  63. // - e-onkyo.com
  64. // - acousticsounds.com
  65. // - indies.eu
  66. // - beatport.com
  67. // - traxsource.com
  68. // - musicbrainz.org
  69. // - music.apple.com
  70. // - vgmdb.net
  71. // - tidal.com (requires account)
  72. // - ototoy.jp
  73. // - music.yandex.ru
  74. // - mora.jp
  75. // - allmusic.com
  76. // - bleep.com
  77. // - boomkat.com
  78. // - ecmrecords.com
  79. // - actmusic.com
  80. // - jpc.de
  81. // - store.pias.com
  82. // - dominomusic.com
  83. // - kompakt.fm
  84. // - eclassical.com
  85. // - qq.com
  86. // - muziekweb.nl
  87. // - beatsource.com
  88. //
  89. // For e-bbook releases:
  90. // - martinus.cz, martinus.sk
  91. // - goodreads.com
  92. // - databazeknih.cz
  93. // - boomkat.com
  94. // - openlibrary.org
  95. // - books.google.com
  96. // - play.google.com (books)
  97. //
  98. // For application releases:
  99. // - sanet.st
  100.  
  101. 'use strict';
  102.  
  103. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
  104.  
  105. function testDomain(domain) {
  106. return document.location.hostname.toLowerCase() == domain.toLowerCase();
  107. }
  108. function testPath(path, query) {
  109. return document.location.pathname.toLowerCase() == `/${path.toLowerCase()}.php`
  110. && (!query || document.location.search.toLowerCase().startsWith('?' + query.toLowerCase()));
  111. }
  112.  
  113. const isRED = testDomain('redacted.ch');
  114. const isNWCD = testDomain('notwhat.cd');
  115. const isOPS = testDomain('orpheus.network');
  116.  
  117. const isUpload = testPath('upload');
  118. const isEdit = testPath('torrents', 'action=editgroup&');
  119. const isTorrentEdit = testPath('torrents', 'action=edit&');
  120. const isRequestNew = testPath('requests', 'action=new');
  121. const isRequestEdit = testPath('requests', 'action=edit&');
  122. const isAddFormat = isUpload && /\b(?:groupid)=(\d+)\b/i.test(document.location.search);
  123.  
  124. const dcRlsParser = /^(?:https?):\/\/(?:\w+\.)*discogs\.com\/releases?\/(\d+)(?=$|\/|\?)/i;
  125. const itunesRlsParser = /^(?:https?):\/\/(?:\w+\.)*apple\.com\/.*\/(\d+)(?=$|\?)/i;
  126. const mbrRlsParser = /^(?:https?):\/\/(?:beta\.)?musicbrainz\.org\/(?:\w+\/)*release\/([\w\-]+)/i;
  127. const dzrRlsParser = /^(?:https?):\/\/(?:\w+\.)*deezer\.com\/(\w+\/)*album\/(\d+)$/i;
  128. const hyphenCoupling = /[\w\(\)\[\]\{\}]-\s/;
  129. const imageExtensions = ['jpg', 'jpeg', 'jfif', 'png', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'];
  130. const mbrRlsPrefix = 'https://musicbrainz.org/release/';
  131. const discogsOrigin = 'https://www.discogs.com';
  132. const deezerAlbumPrefix = 'https://www.deezer.com/album/';
  133. const descriptionFields = ['album_desc', 'body', 'description', 'release_desc', 'release_lineage'];
  134. const siteApiTimeframeStorageKey = document.location.hostname + ' API time frame';
  135.  
  136. const spotify_clientid = '6d358a207c634b1ebac640149a6090da';
  137. const spotify_clientsecret = '4c59880a4ec241ed9c89a24e66468c64';
  138. const discogs_key = 'OrFLNXqtEcdKLEicmywE';
  139. const discogs_secret = 'mveXGdQOjbhPuLXEajOzrwRgQPpRFlUc';
  140. //const discogs_token = '';
  141. const lastfm_api_key = 'b9f26370d7266fbb3151b2ad4f7a74c9';
  142.  
  143. const gazelleApiFrame = 10500;
  144. const ctxt = document.createElement('canvas').getContext('2d');
  145.  
  146. var prefs = {
  147. autfill_delay: 500, // delay in ms to autofill form after pasting text into box, 0 to disable
  148. clean_on_apply: false, // clean the input box on successfull fill
  149. cleanup_descriptions: true, // pre-submit cleanup to all description fields (remove empty placeholders, redundant info and garbage like empty tag pairs etc.)
  150. keep_meaningles_composers: false, // keep composers from file tags also for non-composer emphasing genres
  151. include_all_performers: false, // include to album guests all named performers
  152. 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)
  153. single_threshold: 10 * 60, // For autodetection of release type: max length of single in s
  154. EP_threshold: 30 * 60, // For autodetection of release type: max time of EP in s
  155. anthology_threshold: 120 * 60, // For autodetection of release type: threshold time in s to consider single artist release anthology
  156. auto_rehost_cover: true, // PTPimg / using 3rd party script
  157. auto_preview_cover: true,
  158. image_size_warning: 4, // threshold in MiB for making cover size warning // 0 to disable
  159. image_size_reduce_threshold: 4, // threshold in MiB for attempt to reduce cover size // 0 to disable
  160. cover_lookup_providers: 'all', // itunes/lastfm/deezer/musicbrainz/qobuz/google/tidal/discogs in specific order or 'all' for all | empty for no lookup
  161. 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
  162. estimate_decade_tag: true, // deduce decade tag (1980s, etc.) from album year for regular albums
  163. check_whitespace: true, // check tags for leading/trailing spaces and unreadable characters
  164. assume_rg: true, // do a reminder on missing RG info; on by default
  165. assume_dr: false, // do a reminder on missing DR info (only for Hi-Res tracks); off by default
  166. assume_weblink: false, // do a reminder on missing source URL (tag URL); off by default
  167. ops_always_edition: true, // (only new uploads) don't use original release but always specific edition (unify with other trackers)
  168. sacd_decoder: 'foobar2000\'s SACD decoder (DSD2PCM direct / 64fp / 30kHz lowpass)',
  169. use_store_logos: false, // use online source's pictograsm instead of url in textual form (if defined)
  170. insert_release_date: true, // ..to rls description
  171. selfrelease_label: 'self-released',
  172. upcoming_tags: '', // add this tag(s) to upcoming releases (requests); empty to disable
  173. remap_texttools_newlines: false, // convert underscores to linebreaks (ambiguous)
  174. messages_verbosity: 0,
  175. find_relations: true, // notify about existing torrents and requests of the same release
  176. relations_check_interval: 0, // check for relations periodically after intervals in seconds / 0 = OFF
  177. check_logs: true, // search site log for deleted uploads of the same release / not working on Orpheus
  178. diag_mode: false,
  179. // online parsers specific
  180. apple_offer_alt_cover: true, // usually smaller version of preloaded cover
  181. use_kana: false, // include Kana(JP) version in artist/title names; applies to mora.jp online parser
  182. // online service credentials
  183. redacted_api_key: '',
  184. //soundcloud_clientid: '',
  185. tidal_userid: '',
  186. tidal_userpassword: '',
  187. // request specific
  188. request_default_bounty: 0, // set this bounty in MB after successfull fill of request form / 0 for disable
  189. always_request_perfect_flac: false,
  190. include_tracklist_in_request: false, // false: include one line summary only; true: include full tracklisting
  191. // tracklist specific
  192. tracklist_style: 1, // 1: classic with components colouring, 2: propertional font right-justified, 3: classic center aligned
  193. singles_conventional_format: false, // force one track singles to be formatted same way as albums with numbered tracklist
  194. colorless_tracklist: false, // Strip all colours from tracklist
  195. sort_tracklist: true,
  196. reformat_trackartist: true, // (if track artist differs from main artist) rebuild track artist from partial track artists, turn off if generating wrong track artists
  197. fix_capitalization: true, // properly fix capitalization (turn off if improperly capitalizing non-english titles)
  198. max_tracklist_width: 80, // right margin of the right aligned tracklist. should not exceed the group description width on any device
  199. tracklist_size: 2, // PHPBB font size
  200. title_separator: '. ', // divisor of track# and title
  201. pad_leader: ' ',
  202. bpm_summary: true,
  203. tracklist_head_color: '#778899', // #4682B4 / #a7bdd0
  204. // classical tracklist only components colouring
  205. tracklist_disctitle_color: '#2bb7b7', // #bb831c
  206. tracklist_work_color: '#808000', // #b16890
  207. tracklist_tracknumber_color: '#8899AA',
  208. tracklist_artist_color: '#966b00',
  209. tracklist_composer_color: '#8ca014',
  210. tracklist_duration_color: '#007ab7', // #2196f3
  211. // online check paramaters
  212. check_integrity_online: true, // If provided URL tag, compare local release with release online and lookup for discrepancies
  213. strict_online_check: false, // set to true for strict online check (metadata comparison is case sensitive)
  214. duration_divergency: 0.75, // maximum tolerated playlists difference in %
  215. vinyl_duration_divergency: 2.5, // maximum tolerated playlists difference in % for vinyl releases
  216.  
  217. save: function() {
  218. for (let key in this) {
  219. if (typeof this[key] != 'function' && this[key] != undefined) GM_setValue(key, this[key]);
  220. }
  221. },
  222. };
  223. Object.keys(prefs).forEach(key => { prefs[key] = GM_getValue(key, prefs[key]) });
  224.  
  225. const caseFixes = {
  226. en: [
  227. [
  228. new RegExp(`\\s+(${[
  229. 'A', 'An', 'And A', 'And In', 'And The', 'And', 'As A', 'As An', 'As', 'At A', 'At The', 'At',
  230. 'But', 'By A', 'By An', 'By The', 'By', 'For A', 'For An', 'For The', 'For', 'From A', 'From The',
  231. 'From', 'If', 'In A', 'In A', 'In An', 'In An', 'In The', 'In To', 'In', 'Into', 'Nor', 'Of A',
  232. 'Of A', 'Of An', 'Of The', 'Of', 'Off', 'On A', 'On An', 'On The', 'On', 'Onto', 'Or The', 'Or',
  233. 'Out Of A', 'Out Of The', 'Out Of', 'Out', 'Over', 'The', 'To A', 'To An', 'To The', 'To', 'Vs',
  234. 'With A', 'With The', 'With',
  235. ].join('|')})(?=\\s+)`, 'g'), (match, expr) => ' ' + expr.toLowerCase(),
  236. ], [
  237. new RegExp(`\\b(${['by', 'in', 'of', 'on', 'or', 'to', 'for', 'out', 'into', 'from', 'with'].join('|')})$`, 'g'),
  238. (match, expr) => ' ' + expr[0].toUpperCase() + expr.slice(1).toLowerCase(),
  239. ],
  240. [/([\-\:\&])\s+(the)(?=\s+)/g, '$1 The '],
  241. [/\b(?:Best\s+of)\b/g, 'Best Of'],
  242. ],
  243. };
  244.  
  245. var ref, tbl, elem, child, messages = null, autoFill, dom, dzApiTimeFrame = {}, relationsCheckTimer = null,
  246. tfMessages = [], siteArtistsCache = {}, notSiteArtistsCache = [], releaseTypes, artistTypes;
  247.  
  248. imageHostUploaderInit(inputDataHandler, textAreaDropHandler, textAreaPasteHandler, imageUrlResolver);
  249. insertUAControls();
  250.  
  251. if ((ref = document.getElementById('upload-table') || document.querySelector('form.edit_form')
  252. || document.getElementById('upload_table') || document.getElementById('request_form')) != null) {
  253. ref.ondragover = voidDragHandler1;
  254. ref.ondrop = voidDragHandler1;
  255. }
  256. setHandlers();
  257. if ((ref = isUpload ? document.getElementById('file') : null) != null) {
  258. ref.oninput = function(evt) { if (evt.target.files.length > 0) validateTorrentFile(evt.target.files[0]) };
  259. if (ref.files.length > 0) validateTorrentFile(ref.files[0]);
  260. }
  261. if (!isRED && (ref = document.querySelector('table#dnulist')) != null) {
  262. function toggleVisibility() {
  263. var show = ref.style.display.toLowerCase() == 'none';
  264. ref.style.display = show ? 'block' : 'none';
  265. ref.previousElementSibling.style.display = show ? 'block' : 'none';
  266. }
  267. toggleVisibility();
  268. if ((ref = document.querySelector('h3#dnu_header')) != null) {
  269. elem = ref.parentNode;
  270. child = document.createElement('a');
  271. child.href = '#';
  272. child.onclick = evt => { if ((ref = document.querySelector('table#dnulist')) != null) toggleVisibility() };
  273. child.append(ref);
  274. elem.prepend(child);
  275. }
  276. }
  277.  
  278. if (isRequestNew) {
  279. let title = document.querySelector('input[name="title"]');
  280. if (title != null) for (let i = 1; i < 6; ++i) setTimeout(function(e) { title.readOnly = false }, i * 1000);
  281. }
  282.  
  283. Array.prototype.includesCaseless = function(str) {
  284. if (typeof str != 'string') return false;
  285. str = str.toLowerCase();
  286. return this.some(elem => typeof elem == 'string' && elem.toLowerCase() == str);
  287. };
  288. Array.prototype.pushUnique = function(...items) {
  289. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) });
  290. return this.length;
  291. };
  292. Array.prototype.pushUniqueCaseless = function(...items) {
  293. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  294. return this.length;
  295. };
  296. // Array.prototype.getUnique = function(prop) {
  297. // return this.every((it) => it[prop] && it[prop] == this[0][prop]) ? this[0][prop] : null;
  298. // };
  299. Array.prototype.equalTo = function(arr) {
  300. return Array.isArray(arr) && arr.length == this.length
  301. && Array.from(arr).sort().toString() == Array.from(this).sort().toString();
  302. };
  303. Array.prototype.equalCaselessTo = function(arr) {
  304. function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
  305. return Array.isArray(arr) && arr.length == this.length
  306. && arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
  307. };
  308. Array.prototype.homogeneous = function() {
  309. return this.every(elem => elem === this[0]);
  310. }
  311. Array.prototype.flatten = function() {
  312. return this.reduce(function(flat, toFlatten) {
  313. return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
  314. }, []);
  315. };
  316.  
  317. String.prototype.trueLength = function() {
  318. return this.normalize('NFC').length;
  319. // var index = 0, width = 0, len = 0;
  320. // while (index < this.length) {
  321. // var point = this.codePointAt(index);
  322. // width = 0;
  323. // while (point) {
  324. // ++width;
  325. // point = point >> 8;
  326. // }
  327. // index += Math.round(width / 2);
  328. // ++len;
  329. // }
  330. // return len;
  331. };
  332. String.prototype.flatten = function() {
  333. return this.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
  334. };
  335. String.prototype.expand = function() {
  336. return this.replace(/\x1D/g, '\r').replace(/\x1C/g, '\n');
  337. };
  338. String.prototype.titleCase = function() {
  339. return this.toLowerCase().split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(' ');
  340. };
  341. String.prototype.collapseGaps = function() {
  342. return this.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/\[(\w+)\]\[\/\1\]/ig, '').trim();
  343. };
  344. String.prototype.properlyFixCapitalization = function(language = 'en') {
  345. if (!language) return this;
  346. language = language.toLowerCase();
  347. if (Array.isArray(caseFixes[language]))
  348. return caseFixes[language].reduce((result, replacer) => result.replace(...replacer), this);
  349. console.warn('String.prototype.properlyFixCapitalization() called with invalid language id:', language);
  350. return this;
  351. };
  352.  
  353. Date.prototype.getDateValue = function() {
  354. return Math.floor((this.getTime() / 1000 / 60 - this.getTimezoneOffset()) / 60 / 24);
  355. };
  356. Date.prototype.isExactDate = function() {
  357. return this.getUTCMilliseconds() > 0 || this.getUTCSeconds() > 0 || this.getUTCMinutes() > 0 || this.getUTCHours() > 0
  358. || this.getUTCDate() > 1 || this.getUTCMonth() > 0;
  359. };
  360.  
  361. File.prototype.getText = function(encoding) {
  362. return new Promise(function(resolve, reject) {
  363. var reader = new FileReader();
  364. reader.onload = function() { resolve(reader.result) };
  365. reader.onerror = reader.ontimeout = error => { reject('FileReader error (' + this.name + ')') };
  366. reader.readAsText(this, encoding);
  367. }.bind(this));
  368. };
  369. class HTML extends String { };
  370.  
  371. const excludedCountries = [
  372. /\b(?:United\s+States|USA?)\b/,
  373. /\b(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|UK)\b/,
  374. /\b(?:Europe|European\s+Union|EU)\b/,
  375. /\b(?:Unknown)\b/,
  376. ];
  377. const tm_presubstitutions = [
  378. [/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
  379. [/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
  380. [/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
  381. [/^(?:Psy\/Goa\s+Trance)$/i, 'psytrance, goa.trance'],
  382. [/\s*,\s*(?:&\s*|and\s+)/i, ' & '],
  383. ];
  384. const tm_substitutions = [
  385. [/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
  386. [/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
  387. [/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
  388. ['AOR', 'album.oriented.rock'],
  389. [/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
  390. [/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
  391. [/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
  392. ['World', 'world.music'],
  393. [/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
  394. [/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhytm.and.blues'],
  395. [/\b(?:Soundtracks?)$/i, 'score'],
  396. ['Electro', 'electronic'],
  397. ['Metal', 'heavy.metal'],
  398. ['NonFiction', 'non.fiction'],
  399. ['Rap', 'hip.hop'],
  400. ['NeoSoul', 'neo.soul'],
  401. ['NuJazz', 'nu.jazz'],
  402. [/^J[\s\-]Pop$/i, 'jpop'],
  403. [/^K[\s\-]Pop$/i, 'jpop'],
  404. [/^J[\s\-]Rock$/i, 'jrock'],
  405. ['Hardcore', 'hardcore.punk'],
  406. ['Garage', 'garage.rock'],
  407. [/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
  408. [/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
  409. [/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
  410. [/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
  411. [/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
  412. ['GoaTrance', 'goa.trance'],
  413. [/^Female\s+Vocal\w*$/i, 'female.vocalist'],
  414. ['Contemporary R&B', 'contemporary.rhytm.and.blues'],
  415. // Country aliases
  416. ['Canada', 'canadian'],
  417. ['Australia', 'australian'],
  418. ['New Zealand', 'new.zealander'],
  419. ['Japan', 'japanese'],
  420. ['Taiwan', 'thai'],
  421. ['China', 'chinese'],
  422. ['Singapore', 'singaporean'],
  423. [/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
  424. ['Turkey', 'turkish'],
  425. ['Israel', 'israeli'],
  426. ['France', 'french'],
  427. ['Germany', 'german'],
  428. ['Spain', 'spanish'],
  429. ['Italy', 'italian'],
  430. ['Sweden', 'swedish'],
  431. ['Norway', 'norwegian'],
  432. ['Finland', 'finnish'],
  433. ['Greece', 'greek'],
  434. [/^(?:Netherlands|Holland)$/i, 'dutch'],
  435. ['Belgium', 'belgian'],
  436. ['Luxembourg', 'luxembourgish'],
  437. ['Denmark', 'danish'],
  438. ['Switzerland', 'swiss'],
  439. ['Austria', 'austrian'],
  440. ['Portugal', 'portugese'],
  441. ['Ireland', 'irish'],
  442. ['Scotland', 'scotish'],
  443. ['Iceland', 'icelandic'],
  444. [/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
  445. [/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
  446. ['Hungary', 'hungarian'],
  447. ['Poland', 'polish'],
  448. ['Estonia', 'estonian'],
  449. ['Latvia', 'latvian'],
  450. ['Lithuania', 'lithuanian'],
  451. ['Moldova', 'moldovan'],
  452. ['Armenia', 'armenian'],
  453. ['Belarus', 'belarussian'],
  454. ['Ukraine', 'ukrainian'],
  455. ['Yugoslavia', 'yugoslav'],
  456. ['Serbia', 'serbian'],
  457. ['Slovenia', 'slovenian'],
  458. ['Croatia', 'croatian'],
  459. ['Macedonia', 'macedonian'],
  460. ['Montenegro', 'montenegrin'],
  461. ['Romania', 'romanian'],
  462. ['Malta', 'maltese'],
  463. ['Brazil', 'brazilian'],
  464. ['Mexico', 'mexican'],
  465. ['Argentina', 'argentinean'],
  466. ['Jamaica', 'jamaican'],
  467. // Books
  468. ['Beletrie', 'fiction'],
  469. ['Satira', 'satire'],
  470. ['Komiks', 'comics'],
  471. ['Komix', 'comics'],
  472. // Removals
  473. ['Indie Rock/Rock Pop'],
  474. ['Unknown'],
  475. ['Other'],
  476. ['New'],
  477. ['Ostatni'],
  478. ['Knihy'],
  479. ['Audioknihy'],
  480. ['dsbm'],
  481. [/^(?:Audio\s*kniha|Audio\s*Book)$/i],
  482. ].concat(excludedCountries.map(it => [it]));
  483. const tm_splits = [
  484. ['Alternative', 'Indie'],
  485. ['Rock', 'Pop'],
  486. ['Soul', 'Funk'],
  487. ['Ska', 'Rocksteady'],
  488. ['Jazz Fusion', 'Jazz Rock'],
  489. ['Rock', 'Pop'],
  490. ['Jazz', 'Funk'],
  491. ];
  492. const tm_additions = [
  493. [/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
  494. [/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
  495. [/^(?:Opera)$/i, 'classical'],
  496. [/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
  497. [/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
  498. [/^(?:Symphony)$/i, 'classical'],
  499. [/^(?:Sacred\s+Vocal)\b/i, 'classical'],
  500. [/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
  501. ];
  502.  
  503. class TagManager extends Array {
  504. constructor(...tags) {
  505. super();
  506. if (tags.length > 0) this.add(...tags);
  507. }
  508.  
  509. add(...tags) {
  510. var added = 0;
  511. for (var tag of tags) {
  512. if (typeof tag != 'string') continue;
  513. qobuzTranslations.forEach(function(it) { if (tag.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) tag = it[1] });
  514. tm_presubstitutions.forEach(k => { if (k[0].test(tag)) tag = tag.replace(...k) });
  515. tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(tag => {
  516. //qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
  517. tag = tag.toASCII().replace(/\(.*?\)|\[.*?\]|\{.*?\}/g, '').trim();
  518. if (tag.length <= 0 || tag == '?') return null;
  519. function test(obj) {
  520. return typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase()
  521. || obj instanceof RegExp && obj.test(tag);
  522. }
  523. for (var k of tm_substitutions) {
  524. if (!test(k[0])) continue;
  525. if (k.length >= 1) added += this.add(...k.slice(1));
  526. else addMessage('invalid tag \'' + tag + '\' found', 'warning');
  527. return;
  528. }
  529. for (k of tm_additions) if (test(k[0])) added += this.add(...k.slice(1));
  530. for (k of tm_splits) {
  531. if (new RegExp('^' + k[0] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[1] + '$', 'i').test(tag)) {
  532. added += this.add(k[0], k[1]); return;
  533. }
  534. if (new RegExp('^' + k[1] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[0] + '$', 'i').test(tag)) {
  535. added += this.add(k[0], k[1]); return;
  536. }
  537. }
  538. tag = tag.
  539. replace(/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1').
  540. replace(/\b(?:Alt\.)(?=\s+)/i, 'Alternative').
  541. replace(/^[3-9]0s$/i, '19$0').
  542. replace(/^[0-2]0s$/i, '20$0').
  543. replace(/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2').
  544. replace(/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and ').
  545. replace(/[\s\-\−\—\–\_\.\,\~]+/g, '.').
  546. replace(/[^\w\.]+/g, '').
  547. toLowerCase();
  548. if (tag.length >= 2 && !this.includes(tag)) {
  549. this.push(tag);
  550. ++added;
  551. }
  552. });
  553. }
  554. return added;
  555. }
  556. toString() { return Array.from(this).sort().join(', ') }
  557. };
  558.  
  559. function fillFromText(evt = undefined) {
  560. if (autoFill) {
  561. clearTimeout(autoFill);
  562. autoFill = undefined;
  563. }
  564. const overwrite = evt instanceof Event && evt.target.id == 'fill-from-text';
  565. const hyperlinkStyle = 'color: skyblue;';
  566. const bracketStripper = /\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\})/g,
  567. tailingBracketStripper = /(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))+\s*$/;
  568. let clipBoard = document.getElementById('UA-data');
  569. if (clipBoard == null) return false;
  570. messages = document.getElementById('UA-messages');
  571. //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
  572. //if (typeof clipBoard != 'string') return false;
  573. let i, matches, sourceUrl, category = document.getElementById('categories'),
  574. reportedTorrentCollicions = new Map(), reportedRequests = new Map();
  575. if (relationsCheckTimer) {
  576. clearInterval(relationsCheckTimer);
  577. relationsCheckTimer = null;
  578. }
  579. if (category == null) return isTorrentEdit || document.getElementById('releasetype') != null ? fillFromText_Music()
  580. : fillFromText_Apps(true).catch(reason => fillFromText_Ebooks(true)).then(lookupNonMusicRelations);
  581. if (category.value == 0 || category.value == 'Music') return fillFromText_Music();
  582. if (category.value == 1 || category.value == 'Applications') return fillFromText_Apps().then(lookupNonMusicRelations);
  583. if (category.value == 2 || category.value == 'E-Books') return fillFromText_Ebooks().then(lookupNonMusicRelations);
  584. if (category.value == 3 || category.value == 'Audiobooks') return fillFromText_Ebooks().then(lookupNonMusicRelations);
  585. return Promise.reject('Not supported category');
  586.  
  587. function fillFromText_Music() {
  588. if (messages != null) messages.remove();
  589. const divs = ['—', '⸺', '⸻'];
  590. const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  591. const VA = 'Various Artists';
  592. const multiArtistParsers = [
  593. /\s*[,;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?\s*/,
  594. /\s+[\/\|\×|meets]\s+/i,
  595. ];
  596. const featArtistParsers = [
  597. ///\s+(?:meets)\s+(.+?)\s*$/i,
  598. /* 0 */ /\s+(?:[Ww]ith|[Aa]vec)\s+(?!his\b|her\b|Friends$|Strings$)(.+?)\s*$/,
  599. /* 1 */ /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff]eaturing\s+|(?:[Ff]eat|[Ff]t|FT)\.\s*)([^\(\)\[\]\{\}]+?)(?=\s*(?:[\(\[\{].*)?$)/,
  600. /* 2 */ /\s+\[\s*f(?:eat(?:\.|uring)|t\.)\s+([^\[\]]+?)\s*\]/i,
  601. /* 3 */ /\s+\(\s*f(?:eat(?:\.|uring)|t\.)\s+([^\(\)]+?)\s*\)/i,
  602. /* 4 */ /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i,
  603. /* 5 */ /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i,
  604. /* 6 */ /\s+\[\s*(?:with|w\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/,
  605. /* 7 */ /\s+\(\s*(?:with|w\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/,
  606. ];
  607. const ampersandParsers = [
  608. /\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
  609. /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
  610. /\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
  611. /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
  612. ];
  613. const pseudoArtistParsers = [
  614. /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
  615. /^(?:traditional|trad\.|lidová)$/i,
  616. /\b(?:traditional|trad\.|lidová)$/,
  617. /^(?:tradiční|lidová)\s+/,
  618. /^(?:[Aa]nonym)/,
  619. /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  620. /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  621. /^(?:Various\s+Composers)$/i,
  622. /^(?:Guests|Friends)$/i,
  623. ];
  624. const remixParsers = [
  625. /\s+\((?:The\s+)?Remix(?:e[sd])?\)/i,
  626. /\s+\[(?:The\s+)?Remix(?:e[sd])?\]/i,
  627. /\s+(?:The\s+)?Remix(?:e[sd])?\s*$/i,
  628. /^(?:The\s+)?(?:Remixes)\b|\b(?:The\s+)?(?:Remixes)$/,
  629. /\s+\(([^\(\)]+?)[\'\’\`]s[^\(\)]*\s(?:(?:Re)?Mix|Reworx)\)/i,
  630. /\s+\[([^\[\]]+?)[\'\’\`]s[^\[\]]*\s(?:(?:Re)?Mix|Reworx)\]/i,
  631. /\s+\(([^\(\)]+?)\s+(?:(?:Extended|Enhanced)\s+)?(?:Remix|Reworx)\)/i,
  632. /\s+\[([^\[\]]+?)\s+(?:(?:Extended|Enhanced)\s+)?(?:Remix|Reworx)\]/i,
  633. /\s+\(Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
  634. /\s+\[Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
  635. ];
  636. const otherArtistsParsers = [
  637. [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
  638. [/^()(.*?)\s+\(conductor\)$/i, 4],
  639. //[/^()(.*?)\s+\(.*\)$/i, 1],
  640. ];
  641. const labelSubstitutes = [
  642. [/^(?:DG)$/, 'Deutsche Grammophon'],
  643. [/^(?:Not\s+specified)$/i, ''],
  644. //[/(?:\s*[\,\/])?\s+a\s+division\s+of\s+/i, ' / '],
  645. //[/\s+\(a\s+division\s+of\s+([^\(\)]+)\)/i, ' / $1'],
  646. ];
  647. const artistClassParsers = [
  648. /* 0 */ [/^(?:Main\s?Artist)$/i],
  649. /* 1 */ [/^(?:Featured\s?Artist)$/i],
  650. /* 2 */ [/^(?:Remix)/i],
  651. /* 3 */ [/(?:^(?:Composer|(?:Composer)?Lyricist|Author|Writer|music|written[\s\-]by|libreto|music\simprovisation)|\b(?:lyrics))$/i],
  652. /* 4 */ [/^(?:Conductor|(?:Chorus|Choir)\s?Master|Director|conducts|(?:conducted|directed)[\s\-]by)$/i],
  653. /* 5 */ [/^(?:DJ|Compiler|Compiled[\s\-]By|compiled[\s\-]by)$/],
  654. /* 6 */ [/^(?:Producer|produced[\s\-]by)$/i],
  655. /* 7 */ [/^(?:Artist|Soloist|Vocals|Ensemble|Orchestra|Choir)$/i],
  656. /* 8 */ [
  657. /\b(?:Recorded|Engineer|Producer|Mixer|Programming|Programmer|Arranger|Assistant|Translation)\b/i,
  658. /(?:PersonnelMastering)\b/i,
  659. ],
  660. ];
  661. const missingSpacesTest = /\b(?:(?:Vol|No)\.)(?:\d+|[IVXLCDM]+)\b/;
  662. var isVA, ajaxRejects = 0;
  663. try { var onlineSource = new URL(clipBoard.value) } catch(e) { }
  664. return (function() {
  665. if (onlineSource) return urlResolver(onlineSource).then(fetchOnline_Music);
  666. const fields = [
  667. /* 00 */ 'artist', 'album', 'album_year', 'release_date', 'label', 'catalog', 'country', 'encoding',
  668. /* 08 */ 'codec', 'codec_profile', 'bitrate', 'bitdepth', 'samplerate', 'channels', 'channel_mode',
  669. /* 15 */ 'media', 'genre', 'disc_number', 'total_discs', 'disc_subtitle', 'track_number',
  670. /* 21 */ 'total_tracks', 'title', 'track_artist', 'performer', 'composer', 'conductor', 'remixer',
  671. /* 28 */ 'compiler', 'producer', 'duration', 'samples', 'filesize', 'album_gain', 'album_peak',
  672. /* 35 */ 'track_gain', 'track_peak', 'album_dr', 'track_dr', 'vendor', 'url', 'dirpath',
  673. /* 42 */ 'description', 'identifiers', 'lyrics',
  674. ];
  675. return Promise.resolve(clipBoard.value.split(/(?:\r?\n)+/).filter(line => line.trim().length > 0).map(function(line, ndx) {
  676. var metaData = line.expand().split('\x1E'), track = { identifiers: {} }, identifiers = [];
  677. const patternHint = ' (see browser\'s console for details and update your player\'s format ' +
  678. 'pattern from this script header or Greasy Fork description)';
  679. if (metaData.length < fields.length) {
  680. console.error('invalid data format for track #' + (ndx + 1) + ': length:', metaData.length,
  681. '(' + fields.length + '); metaData:', metaData, '; line:', line);
  682. throw 'invalid clipboard data format for track #' + (ndx + 1) + patternHint;
  683. } else if (metaData.length > fields.length) {
  684. console.warn('unexpected data format for track #' + (ndx + 1) + ': length:', metaData.length,
  685. '(expected length: ' + fields.length + '); metaData:', metaData, '; line:', line);
  686. addMessage('unexpected clipboard data format for track #' + (ndx + 1) + patternHint, 'warning');
  687. }
  688. fields.forEach(function(propName) {
  689. if (propName == 'identifiers') {
  690. metaData.shift().trim().split(/\s+/).forEach(function(id) {
  691. if (/^([\w\-]+)[=:](\S*)$/.test(id)) track.identifiers[RegExp.$1.toUpperCase()] = RegExp.$2.replace(/\x1B/g, ' ');
  692. });
  693. } else {
  694. track[propName] = metaData.shift();
  695. if (track[propName] === '') track[propName] = undefined;
  696. }
  697. });
  698. if (prefs.check_whitespace) {
  699. Object.keys(track).forEach(function(propName) {
  700. if (typeof track[propName] != 'string') return;
  701. if (!['description', 'lyrics'].includes(propName) && (track[propName].includes('\r') || track[propName].includes('\n'))) {
  702. track[propName] = track[propName].replace(/[\r\n]+/g, '');
  703. addMessage('track #' + (ndx + 1) + ' contains linebreaks in tag <' + propName + '>', 'warning');
  704. }
  705. if ((i = ['description', 'lyrics'].includes(propName) ? /[\x00-\x08\x0B\x0C\x0E-\x19]+/g : /[\x00-\x19]+/g).test(track[propName])) {
  706. track[propName] = track[propName].replace(i, '');
  707. addMessage('track #' + (ndx + 1) + ' contains control codes in tag <' + propName + '>', 'warning');
  708. }
  709. if (/^[\s\xA0]+$/.test(track[propName])) {
  710. track[propName] = undefined;
  711. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains only whitespace', 'warning');
  712. } else if (/^[\s\xA0]+|[\s\xA0]+$/.test(track[propName])) {
  713. track[propName] = track[propName].trim();
  714. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains leading/trailing whitespace', 'warning');
  715. }
  716. if (/[ \xA0]{2,}/.test(track[propName])) {
  717. track[propName] = track[propName].replace(/[ \xA0]{2,}/g, ' ')
  718. addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains multiple spaces', 'warning');
  719. }
  720. });
  721. if (missingSpacesTest.test(track.title))
  722. addMessage('missing space in track#' + (ndx + 1) + ' title: "' + track.title + '"', 'notice');
  723. }
  724. ['description', 'lyrics'].forEach(function(propName) {
  725. if (track[propName] == '.') track[propName] = undefined; else if (track[propName]) {
  726. if (prefs.remap_texttools_newlines)
  727. track[propName] = track[propName].replace(/__/g, '\r\n').replace(/_/g, '\n') // ambiguous
  728. track[propName] = track[propName].collapseGaps();
  729. }
  730. });
  731. [
  732. 'bitrate', 'bitdepth', 'samplerate', 'channels', 'total_discs', 'total_tracks', 'samples',
  733. 'filesize', 'album_dr', 'track_dr',
  734. ].forEach(function(propName) {
  735. if (track[propName] !== undefined && typeof track[propName] != 'number')
  736. track[propName] = parseInt(track[propName]);
  737. });
  738. ['duration', 'album_peak', 'track_peak'].forEach(function(propName) {
  739. if (track[propName] !== undefined && typeof track[propName] != 'number')
  740. track[propName] = parseFloat(track[propName]);
  741. });
  742. ['album_gain', 'track_gain'].forEach(function(propName) {
  743. if (track[propName] === '') track[propName] = undefined;
  744. else if (track[propName] !== undefined && typeof track[propName] != 'number')
  745. track[propName] = parseFloat(track[propName].replace(/\s*\b(?:dB)\s*$/i, ''));
  746. });
  747. if (track.album_year) track.album_year = extractYear(track.album_year) || NaN;
  748. return track;
  749. }));
  750. })().then(parseTracks).catch(reason => { addMessage(reason, 'critical') });
  751.  
  752. function parseTracks(tracks) {
  753. if (tracks.length <= 0) {
  754. clipBoard.value = '';
  755. throw 'no tracks found';
  756. }
  757. if (prefs.diag_mode) console.debug('Parsing tracks:', tracks);
  758. const maxFuzzyLevel = 3;
  759. const selfReleaseParsers = [
  760. /^(?:Self[\s\-]Released|Not\s+On\s+Label|No\s+Label|Independent|none|vlastní\s+náklad)$/i,
  761. /^(?:\(no\s+label\)|\[no\s+label\])$/i,
  762. /^(?:iMD)\b/,
  763. ];
  764. const naParsers = [
  765. /^(?:#?N[\/\-]A)$/i,
  766. /^(?:#NA)$/,
  767. ];
  768. var albumBitrate = 0, totalTime = 0, albumSize = 0, media, release = { totalDiscs: 1, sampleRates: [] };
  769. var allowedFormats = Array.from(document.querySelectorAll('select#format > option'))
  770. .filter(option => option.value.length > 0).map(option => option.value);
  771. if (allowedFormats.length <= 0) allowedFormats = ["MP3", "FLAC", "AAC", "AC3", "DTS"];
  772. tracks.forEach(function(track, index) {
  773. let trackId = track.track_number ? track.disc_number ?
  774. track.disc_number + '/' + track.track_number : track.track_number : index + 1;
  775. if (!track.track_number) {
  776. clipBoard.value = '';
  777. throw new HTML('missing required tag track number for track #' + trackId + ruleLink('2.3.16.4'));
  778. }
  779. if (!track.title) {
  780. clipBoard.value = '';
  781. throw new HTML('missing required tag track title for track #' + trackId + ruleLink('2.3.16.4'));
  782. }
  783. if (track.duration !== undefined && track.duration !== null && isUpload && (isNaN(track.duration) || track.duration <= 0)) {
  784. clipBoard.value = '';
  785. throw 'invalid track #' + trackId + ' length: ' + track.duration;
  786. }
  787. processTrackArtists(track);
  788. if (naParsers.some(rx => rx.test(track.label))) track.label = undefined;
  789. if (naParsers.concat([/^(?:none)$/i]).some(rx => rx.test(track.catalog))) track.catalog = undefined;
  790. if (/^(\d+)\s*[\/]\s*(\d+)$/.test(track.track_number)) { // track/total_tracks
  791. addMessage('nonstandard track number formatting for track #' + trackId + ': ' + track.track_number, 'warning');
  792. track.track_number = RegExp.$1;
  793. if (!track.total_tracks) track.total_tracks = parseInt(RegExp.$2);
  794. }/* else if (/^(\d+)[\.\-](\d+)$/.test(track.track_number)) { // disc_number.track_number
  795. addMessage('nonstandard track number formatting for track #' + trackId + ': ' + track.track_number, 'warning');
  796. if (!track.disc_number) track.disc_number = parseInt(RegExp.$1);
  797. track.track_number = RegExp.$2;
  798. }*/
  799. if (track.disc_number) {
  800. if (/^(\d+)\s*\/\s*(\d+)/.test(track.disc_number)) {
  801. addMessage('nonstandard disc number formatting for track #' + trackId + ': ' + track.disc_number, 'warning');
  802. track.disc_number = RegExp.$1;
  803. if (!track.total_discs) track.total_discs = RegExp.$2;
  804. } else track.disc_number = parseInt(track.disc_number);
  805. if (isNaN(track.disc_number)) {
  806. addMessage('invalid disc numbering for track #' + trackId, 'warning');
  807. track.disc_number = undefined;
  808. }
  809. if (track.disc_number > release.totalDiscs) release.totalDiscs = track.disc_number;
  810. }
  811. totalTime += track.duration;
  812. albumBitrate += track.bitrate * track.duration;
  813. if (track.samplerate/* && track.duration*/)
  814. if (typeof release.sampleRates[track.samplerate] == 'number')
  815. release.sampleRates[track.samplerate] += track.duration || 0;
  816. else release.sampleRates[track.samplerate] = track.duration || 0;
  817. albumSize += track.filesize;
  818. if (track.codec) allowedFormats.forEach(function(codec) {
  819. if (codec.toLowerCase() == track.codec.toLowerCase()) track.codec = codec;
  820. });
  821. if (track.encoding && !['lossless', 'lossy'].includes(track.encoding = track.encoding.toLowerCase())) {
  822. addMessage('invalid encoding for track #' + trackId + ': ' + track.encoding, 'warning');
  823. track.encoding = undefined;
  824. }
  825. if (!track.encoding && track.codec) switch (track.codec) {
  826. case 'FLAC': case 'WAV': case 'AIFF': case 'APE': case 'ALAC': case 'WavPack': case 'TAK':
  827. track.encoding = 'lossless'; break;
  828. case 'MP3': case 'AAC': case 'Vorbis': case 'Opus': case 'AC3':
  829. track.encoding = 'lossy'; break;
  830. }
  831. if (track.bitrate > 0) {
  832. let triggers = [24, 12];
  833. switch (track.codec) {
  834. case 'FLAC': case 'APE': case 'ALAC': case 'WavPack': case 'TAK':
  835. if (track.samplerate > 0 && track.bitdepth > 0) triggers = [
  836. Math.round(Math.max(track.samplerate * track.bitdepth / 1800, 192)),
  837. Math.round(Math.max(track.samplerate * track.bitdepth / 2400, 192)),
  838. ];
  839. break;
  840. case 'MP3':
  841. switch (track.codec_profile) {
  842. case 'VBR V0': triggers = [192, 96]; break;
  843. case 'VBR V1': triggers = [160, 80]; break;
  844. case 'VBR V2': triggers = [128, 64]; break;
  845. }
  846. break;
  847. case 'AAC':
  848. if (/\b(?:TVBR)\sq(\d+)\b/.test(track.vendor)) triggers = [
  849. Math.round(Math.max(parseInt(RegExp.$1) * 1.9, 192)),
  850. Math.round(Math.max(parseInt(RegExp.$1) * 1.4, 192)),
  851. ]; else if (/\b(?:(?:CV|A|C)BR)\s(\d+)kbps\b/.test(track.vendor)) triggers = [
  852. Math.round(Math.max(parseInt(RegExp.$1) * 0.75, 192)),
  853. Math.round(Math.max(parseInt(RegExp.$1) * 0.4, 192)),
  854. ];
  855. break;
  856. }
  857. if (track.bitrate < triggers[0]) addMessage('track #' + trackId + ' has suspiciously low bitrate (' +
  858. track.bitrate + ' kbps)', track.bitrate < triggers[1] ? 'warning' : 'notice');
  859. }
  860. if (typeof track.identifiers.MD5 == 'string') track.identifiers.MD5 = track.identifiers.MD5.toUpperCase();
  861. });
  862. if (!onlineSource && release.totalDiscs > 1 && tracks.some(it => it.total_discs != release.totalDiscs))
  863. addMessage('at least one track not having properly set TOTALDISCS (' + release.totalDiscs + ')', 'info');
  864. [
  865. ['artist', 'album artist'],
  866. ['album', 'album title'],
  867. ['album_year', 'album year'],
  868. ['release_date', 'release date'],
  869. ['encoding', 'encoding'],
  870. ['codec', 'codec'],
  871. ['codec_profile', 'codec profile'],
  872. ['vendor', 'vendor'],
  873. ['media', 'media'],
  874. ['channels', 'channels'],
  875. ['channel_mode', 'channel_mode'],
  876. ['label', 'label'],
  877. ['country', 'country'],
  878. ['edition_title', 'edition title'],
  879. ['series', 'series'],
  880. ].forEach(function(property) {
  881. var values = new Set(tracks.map(track => track[property[0]])
  882. .filter(property => property !== undefined && property !== null));
  883. if (values.size == 1) release[property[0]] = values.values().next().value; else if (values.size > 1) {
  884. var val, diverses = '', iterator = values.values();
  885. while (!(val = iterator.next()).done) diverses += '<br>\t' + val.value;
  886. clipBoard.value = '';
  887. throw new HTML('mixed releases not accepted (' + property[1] + ') - supposedly user compilation' + diverses);
  888. }
  889. });
  890. if (!release.artist) {
  891. clipBoard.value = '';
  892. throw new HTML('missing required tag main artist' + ruleLink('2.3.16.4'));
  893. }
  894. if (!release.album) {
  895. clipBoard.value = '';
  896. throw new HTML('missing required tag album title' + ruleLink('2.3.16.4'));
  897. }
  898. if (prefs.check_whitespace && missingSpacesTest.test(release.album))
  899. addMessage('missing space in album title: "' + release.album + '"', 'notice');
  900. // ['artists', 'featured_artists', 'composers', 'conductors', 'performers', 'compilers', 'remixers', 'producers'].forEach(function(role) {
  901. // if (tracks.every(track => Array.isArray(track[role]) && track[role].equalTo(tracks[0][role]))) release[role] = Array.from(tracks[0][role]);
  902. // });
  903. [
  904. ['trackArtists', 'track_artist'],
  905. ['totalTracks', 'total_tracks'],
  906. ['discSubtitles', 'disc_subtitle'],
  907. ['composers', 'composer'],
  908. ['catalogs', 'catalog'],
  909. ['bitrates', 'bitrate'],
  910. ['bitdepths', 'bitdepth'],
  911. ['albumgains', 'album_gain'],
  912. ['albumpeaks', 'album_peak'],
  913. ['albumdrs', 'album_dr'],
  914. ['dirpaths', 'dirpath'],
  915. ['descriptions', 'description'],
  916. ['genres', 'genre'],
  917. ['urls', 'url'],
  918. ['coverUrls', 'cover_url'],
  919. ].forEach(function(property) {
  920. if (!Array.isArray(release[property[0]])) release[property[0]] = [];
  921. tracks.forEach(function(track) {
  922. if (track[property[1]] === undefined || track[property[1]] === null
  923. || (typeof track[property[1]] == 'string' && track[property[1]].length <= 0)
  924. || release[property[0]].includes(track[property[1]])) return;
  925. release[property[0]].push(track[property[1]]);
  926. });
  927. });
  928. if (release.totalTracks.length > 0) {
  929. if (release.totalTracks.length > 1)
  930. addMessage('total tracks not consistent across release: ' + release.totalTracks, 'warning');
  931. else if (release.totalTracks[0] != tracks.length)
  932. addMessage('total tracks not matching tracklist length: ' +
  933. release.totalTracks[0] + ' != ' + tracks.length, 'warning');
  934. }
  935. tracks.forEach(function(track1, ndx1) {
  936. if (tracks.some((track2, ndx2) => ndx2 < ndx1 && track1.track_number == track2.track_number
  937. && track1.disc_number == track2.disc_number && track1.disc_subtitle == track2.disc_subtitle)) {
  938. addMessage('duplicate track ' + (track1.disc_number ? track1.disc_number + '-' : '') +
  939. (track1.disc_subtitle ? track1.disc_subtitle + '-' : '') + track1.track_number, 'warning');
  940. }
  941. });
  942. if (!tracks.every(track => track.disc_number > 0) && !tracks.every(track => !track.disc_number))
  943. addMessage('inconsistent release (mix of tracks with and without disc number)', 'warning');
  944. var releaseDate = new Date(release.release_date);
  945. if (isNaN(releaseDate)) {
  946. releaseDate = normalizeDate(release.release_date);
  947. releaseDate = releaseDate && new Date(releaseDate.toString()) || NaN;
  948. }
  949. var releaseYear = !isNaN(releaseDate) && releaseDate.getFullYear() || extractYear(release.release_date);
  950. var language = getHomoIdentifier('LANGUAGE');
  951. if (language) language = langCodes.find(langCode => langCode.includesCaseless(language));
  952. if (language) language = language[0]; else language = 'en';
  953. if (!onlineSource) {
  954. if (release.codec && !allowedFormats.includes(release.codec)) {
  955. clipBoard.value = '';
  956. throw 'disallowed codec present (' + release.codec + ')';
  957. }
  958. if (!onlineSource && /\b(?:MQAEncode) v(\d+(?:\.\d+)*)\b/.test(release.vendor)) {
  959. clipBoard.value = '';
  960. throw 'MQA format detected (' + RegExp.lastMatch + '), specifically banned';
  961. }
  962. [
  963. ['bit depths', release.bitdepths, bitdepth => ![16, 24].includes(bitdepth)],
  964. [
  965. 'sample rates',
  966. Object.keys(release.sampleRates),
  967. samplerate => samplerate <= 0 || samplerate > 192000 || [44100, 48000].every(sr => samplerate % sr != 0)
  968. ],
  969. ].forEach(function(validator) {
  970. if (validator[1].length <= 0 || !validator[1].some(validator[2])) return;
  971. clipBoard.value = '';
  972. throw 'disallowed ' + validator[0] + ' present (' + validator[1].filter(validator[2]).toString() + ')';
  973. });
  974. if (!release.totalTracks) addMessage('total tracks not set', 'warning');
  975. if (release.albumgains.length > 1)
  976. addMessage('inconsistent album RG across release', release.totalDiscs > 1 ? 'notice' : 'warning')
  977. if (tracks.some(track => track.identifiers.LANGUAGE != tracks[0].identifiers.LANGUAGE))
  978. addMessage('inconsistent language across release', 'notice')
  979. if (release.albumpeaks.length > 1)
  980. addMessage('inconsistent album peak across release', release.totalDiscs > 1 ? 'notice' : 'warning')
  981. if (release.albumdrs.length > 1 && release.bitdepths.length <= 1 && Object.keys(release.sampleRates).length <= 1)
  982. addMessage('inconsistent album DR across release', release.totalDiscs > 1 ? 'notice' : 'warning')
  983. if (prefs.assume_rg && tracks.some(track => track.album_gain === undefined))
  984. addMessage('at least one track is missing RG info', 'notice');
  985. if (prefs.assume_dr && tracks.some(track => track.bitdepth > 16 && track.album_dr === undefined))
  986. addMessage('at least one high resolution track is missing DR info', 'notice');
  987. release.descriptions.forEach(function(description) {
  988. if (/^[\w\-]+\@[\w\-]+(?:\.[\w\-]+)+$|\b(?:RuTracker|FLACMANIA\.RU|24bit-music\.info|GetMetal\.CLUB|LOSSLESSBEST|flacmania\.ru)\b|~ N ~|\b[\w\-\.]+@[\w\-\.]+\.[\w\-]+\b/i.test(description))
  989. addMessage(new HTML('Advertising detected in description: ' + RegExp.lastMatch.bold()), 'warning');
  990. });
  991. release.urls.forEach(function(url) {
  992. if (/^(?:https?):\/\/(\w+\.)*7digital\.com\/.*\?f=/i.test(url))
  993. addMessage('session id present in online source URL: ' + url, 'notice');
  994. });
  995. release.dirpaths.forEach(function(dirPath) {
  996. if (hyphenCoupling.test(dirPath)) addMessage('torrent folder containing hyphen coupling ("' +
  997. dirPath + '")', 'notice');
  998. });
  999. if (tracks.some(track => track.identifiers.BPM && !(track.identifiers.BPM > 0)))
  1000. addMessage('at least one track having invalid BPM', 'notice');
  1001. }
  1002. if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))) {
  1003. (release.coverUrls.length > 0 ? setCover(release.coverUrls[0]) : Promise.reject('No cover URL'))
  1004. .catch(getCoverOnline).catch(searchCoverOnline);
  1005. }
  1006. var albumBPM = Math.round(tracks.reduce(function(acc, track) {
  1007. return acc + parseInt(track.identifiers.BPM) * track.duration;
  1008. }, 0) / totalTime);
  1009. var composerEmphasis = tracks.some(track => track.identifiers.COMPOSEREMPHASIS);
  1010. var isFromDSD = false, isClassical = false;
  1011. var canSort = tracks.every((tr1, ndx1) => tracks.every((tr2, ndx2) => ndx1 == ndx2
  1012. || tr1.track_number != tr2.track_number || tr1.disc_number != tr2.disc_number));
  1013. var yadg_prefil = '', releaseType, editionTitle = release.edition_title, iter, rx;
  1014. var barCode = getHomoIdentifier('BARCODE');
  1015. if (barCode) barCode = parseInt(barCode.toString().replace(/\s+/g, ''));
  1016. if (!Number.isInteger(barCode)) {
  1017. if (release.catalogs.length == 1) barCode = parseInt(release.catalogs[0].replace(/[\s\-]/g, ''));
  1018. if (!Number.isInteger(barCode) || barCode < 10**10) barCode = undefined;
  1019. }
  1020. var tags = new TagManager();
  1021. albumBitrate /= totalTime;
  1022. var isCompilation = tracks.every(track => track.identifiers.COMPILATION == 1);
  1023. if (i = getHomoIdentifier('RELEASETYPE') || getHomoIdentifier('RELEASE_TYPE')) {
  1024. releaseType = getReleaseTypeFromId(i) || undefined;
  1025. if (/^(?:Compilation)$/i.test(i)) isCompilation = true;
  1026. }
  1027. if ((!releaseType || releaseType == getReleaseTypeValue('EP')) && totalTime <= prefs.EP_threshold
  1028. && tracks.every(track => track.title.replace(tailingBracketStripper, '') == tracks[0].title.replace(tailingBracketStripper, '')))
  1029. releaseType = getReleaseTypeValue('Single');
  1030. if (!releaseType) if (totalTime > 0 && totalTime < prefs.single_threshold) releaseType = getReleaseTypeValue('Single');
  1031. else if (totalTime > 0 && totalTime < prefs.EP_threshold) releaseType = getReleaseTypeValue('EP');
  1032. if (release.genres.length > 0) {
  1033. const classicalGenreParsers = [
  1034. /\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,
  1035. ];
  1036. release.genres.forEach(function(genre) {
  1037. classicalGenreParsers.forEach(function(classicalGenreParser) {
  1038. if (classicalGenreParser.test(genre) && !/\b(?:metal|rock|pop)\b/i.test(genre)) {
  1039. composerEmphasis = true;
  1040. isClassical = true
  1041. }
  1042. });
  1043. if (/\b(?:Jazz|Vocal)\b/i.test(genre) && !/\b(?:Nu|Future|Acid)[\s\-\−\—\–]*Jazz\b/i.test(genre)
  1044. && !/\bElectr(?:o|ic)[\s\-\−\—\–]?Swing\b/i.test(genre)) {
  1045. composerEmphasis = true;
  1046. }
  1047. if (/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i.test(genre)) {
  1048. if (!releaseType || [1].includes(releaseType)) releaseType = getReleaseTypeValue('Soundtrack');
  1049. composerEmphasis = true;
  1050. }
  1051. if (/\b(?:Christmas\s+Music)\b/i.test(genre)) {
  1052. composerEmphasis = true;
  1053. }
  1054. tags.add(...genre.split(/\s*\|\s*/));
  1055. });
  1056. if (release.genres.length > 1) addMessage('inconsistent genre accross album: ' + release.genres.join(' / '), 'warning');
  1057. }
  1058. if (!onlineSource && isClassical && !tracks.every(track => track.composer)) {
  1059. addMessage(new HTML('all tracks composers must be set for clasical music' + ruleLink('2.3.17')), 'warning');
  1060. //return false;
  1061. }
  1062. // Processing artists: recognition, splitting and dividing to categores
  1063. const roleCollisions = [
  1064. [4, 5], // main
  1065. [0, 4], // guest
  1066. [], // remixer
  1067. [], // composer
  1068. [], // conductor
  1069. [], // DJ/compiler
  1070. [], // producer
  1071. ];
  1072. isVA = vaParser.test(release.artist);
  1073. var artists = [], albumGuests = [];
  1074. for (i = 0; i < 7; ++i) artists[i] = [];
  1075.  
  1076. if (!isVA) {
  1077. if (Array.isArray(release.artists) && release.artists.length > 0) {
  1078. artists[0] = release.artists.filter(exclusions);
  1079. if (Array.isArray(release.featured_artists)) {
  1080. albumGuests = release.featured_artists;
  1081. artists[1] = release.featured_artists.filter(exclusions);
  1082. }
  1083. yadg_prefil = joinArtists(artists[0]);
  1084. } else {
  1085. yadg_prefil = [0, 6, 7].some(ndx => featArtistParsers[ndx].test(release.artist)) && getSiteArtist(release.artist) ?
  1086. release.artist : spliceGuests(release.artist);
  1087. addArtists(0, yadg_prefil);
  1088. artists[0] = artists[0].filter(exclusions);
  1089. albumGuests = Array.from(artists[1]);
  1090. }
  1091. if (ampersandParsers.some(rx => rx.test(yadg_prefil))) getSiteArtist(yadg_prefil); // priority cache record
  1092.  
  1093. function exclusions(artist) {
  1094. return !['conductors', 'compilers']
  1095. .some(category => Array.isArray(release[category]) && release[category].includesCaseless(artist));
  1096. }
  1097. }
  1098.  
  1099. featArtistParsers.slice(1).forEach(function(rx, ndx) {
  1100. if ((matches = rx.exec(release.album)) == null) return;
  1101. if (ndx >= 5 && !splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)))
  1102. .every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
  1103. addArtists(1, matches[1]);
  1104. artists[0].forEach(guest => { if (albumGuests.includesCaseless(guest)) albumGuests.push(guest) });
  1105. addMessage('featured artist(s) in album title (' + release.album + ')', 'warning');
  1106. release.album = release.album.replace(rx, '');
  1107. });
  1108. if ((matches = remixParsers.slice(4).reduce((acc, rx) => acc || rx.exec(release.album), null)) != null)
  1109. addArtists(2, matches[1].replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1110. if (((matches = /^(.*?)\s+(?:Presents)\s+(.*)$/.exec(release.album)) != null
  1111. || isVA && (matches = (/\s+\(compiled\s+by\s+(.*?)\)\s*$/i.exec(release.album)
  1112. || /\s+(?:compiled\s+by)\s+(.*?)\s*$/i.exec(release.album))) != null) && looksLikeTrueName(matches[1])) {
  1113. addArtists(5, matches[1]);
  1114. if (!releaseType) releaseType = getReleaseTypeValue('Compilation');
  1115. }
  1116.  
  1117. for (iter of tracks) {
  1118. let categories = ['track_artist', 'track_guest'];
  1119. if (prefs.include_all_performers) categories.push('performer');
  1120. categories.forEach(function(category) {
  1121. var arrayRef = category + 's';
  1122. addTrackPerformers(iter[Array.isArray(iter[arrayRef]) && iter[arrayRef].length > 0 ? arrayRef : category]);
  1123. });
  1124. [
  1125. [2, 'remixer'],
  1126. [3, 'composer'],
  1127. [4, 'conductor'],
  1128. [5, 'compiler'],
  1129. [6, 'producer'],
  1130. ].forEach(function(category) {
  1131. var arrayRef = category[1] + 's';
  1132. addArtists(category[0], iter[Array.isArray(iter[arrayRef]) && iter[arrayRef].length > 0 ? arrayRef : category[1]]);
  1133. });
  1134.  
  1135. if (iter.title) {
  1136. featArtistParsers.slice(1).forEach(function(rx, ndx) {
  1137. if ((matches = rx.exec(iter.title)) == null) return;
  1138. let featArtists = splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)));
  1139. if (ndx >= 5 && !featArtists.every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
  1140. if (Array.isArray(iter.track_artists) && iter.track_artists.length > 0) {
  1141. if (!Array.isArray(iter.track_guests)) iter.track_guests = [];
  1142. featArtists.forEach(function(featArtist) {
  1143. if (!iter.track_artists.includesCaseless(featArtist) && !iter.track_guests.includesCaseless(featArtist))
  1144. iter.track_guests.push(featArtist);
  1145. });
  1146. if (!isVA && iter.track_artists.equalCaselessTo(release.artists)
  1147. && iter.track_guests.equalCaselessTo(release.featured_artists)) {
  1148. iter.track_artists = iter.track_guests = iter.track_artist = undefined;
  1149. } else if (iter.track_guests.length > 0)
  1150. iter.track_artist = joinArtists(iter.track_artists) + ' feat. ' + joinArtists(iter.track_guests);
  1151. } else {
  1152. let useTA = iter.track_artist && !featArtists.some(featArtist => iter.track_artist.includes(featArtist)
  1153. || Array.isArray(iter.track_artists) && iter.track_artists.includes(featArtist)
  1154. || Array.isArray(iter.track_guests) && iter.track_guests.includes(featArtist));
  1155. iter.track_artist = iter[useTA ? 'track_artist' : 'artist'] + ' feat. ' + matches[1];
  1156. }
  1157. addArtists(1, matches[1]);
  1158. addMessage('featured artist(s) in track title (#' + iter.track_number + ': ' + iter.title + ')', 'warning');
  1159. iter.title = iter.title.replace(rx, '');
  1160. });
  1161. if (!iter.remixer && (matches = remixParsers.slice(4).reduce((acc, rx) => acc || rx.exec(iter.title), null)) != null)
  1162. addArtists(2, matches[1].replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
  1163. }
  1164. if (isClassical && !iter.composer && /^([^\(\)\[\]\{\},:]+?)(?:\s*\((?:\d{4}\s*-|b\.)\s*\d{4}\))/.test(iter.disc_subtitle)) {
  1165. //track.composer = RegExp.$1;
  1166. addArtists(3, RegExp.$1);
  1167. }
  1168. }
  1169. for (i = 0; i < Math.round(tracks.length / 2); ++i) splitAmpersands();
  1170. albumGuests = splitAmpersands(albumGuests);
  1171.  
  1172. function addArtists(ndx, _artists) {
  1173. (typeof _artists == 'string' ? splitArtists(_artists) : Array.isArray(_artists) ? _artists : []).forEach(function(artist) {
  1174. artist = ndx != 0 ? strip(artist) : guessOtherArtists(artist);
  1175. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1176. && !artists[ndx].includesCaseless(artist)
  1177. && !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist))) artists[ndx].push(artist);
  1178. });
  1179. }
  1180. function addTrackPerformers(_artists) {
  1181. (typeof _artists == 'string' ? splitArtists(spliceGuests(_artists)) : Array.isArray(_artists) ? _artists : []).forEach(function(artist) {
  1182. artist = guessOtherArtists(artist);
  1183. if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
  1184. && !artists[0].includesCaseless(artist)
  1185. && (isVA || !artists[1].includesCaseless(artist))) artists[isVA ? 0 : 1].push(artist);
  1186. });
  1187. }
  1188. function spliceGuests(str, level = 0) {
  1189. (level > 0 ? featArtistParsers.slice(level) : featArtistParsers).forEach(function(rx, ndx) {
  1190. var matches = rx.exec(str);
  1191. if (matches != null && (level + ndx < 8
  1192. || splitArtists(matches[1]).every((artist, ndx) => looksLikeTrueName(artist, 1)))) {
  1193. addArtists(1, matches[1]);
  1194. str = str.replace(rx, '');
  1195. }
  1196. });
  1197. return str;
  1198. }
  1199. function guessOtherArtists(name) {
  1200. otherArtistsParsers.forEach(function(it) {
  1201. if (!it[0].test(name)) return;
  1202. addArtists(it[1], RegExp.$2);
  1203. name = RegExp.$1;
  1204. });
  1205. return strip(name);
  1206. }
  1207. function splitAmpersands(_artists = undefined) {
  1208. if (_artists !== undefined) {
  1209. let result;
  1210. if (typeof _artists == 'string') result = splitArtists(_artists);
  1211. else if (Array.isArray(_artists)) result = Array.from(_artists); else return [];
  1212. splitInternal(result);
  1213. return result;
  1214. }
  1215. for (let ndx = 0; ndx < artists.length; ++ndx) splitInternal(artists[ndx], roleCollisions[ndx]);
  1216.  
  1217. function splitInternal(refArr, roleCollisions) {
  1218. ampersandParsers.forEach(function(ampersandParser) {
  1219. for (var i = refArr.length; i > 0; --i) {
  1220. var j = refArr[i - 1].split(ampersandParser).map(strip);
  1221. if (j.length <= 1 || !j.some(it1 => artists.some(it2 => it2.includesCaseless(it1)))
  1222. && !j.every(looksLikeTrueName) || getSiteArtist(refArr[i - 1])) continue;
  1223. refArr.splice(i - 1, 1, ...j.filter(function(artist) {
  1224. return !refArr.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist))
  1225. && (!Array.isArray(roleCollisions) || !roleCollisions.some(n => artists[n].includesCaseless(artist)));
  1226. }));
  1227. }
  1228. });
  1229. }
  1230. }
  1231. function getArtists(trackArtist) {
  1232. if (!trackArtist || typeof trackArtist != 'string') trackArtist = '';
  1233. otherArtistsParsers.forEach(it => { if (it[0].test(trackArtist)) trackArtist = RegExp.$1 });
  1234. var result = [[], []];
  1235. featArtistParsers.forEach(function(rx, ndx) {
  1236. if ((matches = rx.exec(trackArtist)) == null || ndx >= 7 && !looksLikeTrueName(matches[1], 1)) return;
  1237. splitAmpersands(matches[1]).forEach(artist => { result[1].pushUniqueCaseless(artist) });
  1238. trackArtist = trackArtist.replace(rx, '');
  1239. });
  1240. splitAmpersands(trackArtist).forEach(artist => { result[0].pushUniqueCaseless(artist) });
  1241. return result;
  1242. }
  1243. function realTrackArtist(trackArtist) {
  1244. var result, trackArtists = getArtists(trackArtist);
  1245. if (trackArtists[0].length > 0 && !artistsMatch(trackArtists, [
  1246. artists[0].filter(artist => !roleCollisions[0].some(n => artists[n].includesCaseless(artist))),
  1247. albumGuests.filter(guest => !roleCollisions[1].some(n => artists[n].includesCaseless(guest))),
  1248. ])) result = prefs.reformat_trackartist ? stringifyArtists(trackArtists) : trackArtist;
  1249. return result;
  1250. }
  1251.  
  1252. if (elementWritable(document.getElementById('artist') || document.getElementById('artist_0'))) {
  1253. let artistIndex = 0;
  1254. const enSorter = /^(?:The)\s+/;
  1255. catLoop: for (i = 0; i < artists.length; ++i) for (iter of artists[i]
  1256. .filter(artist => !roleCollisions[i].some(n => artists[n].includesCaseless(artist)))
  1257. .sort((a, b) => a.replace(enSorter, '').localeCompare(b.replace(enSorter, '')))) {
  1258. if (isUpload) {
  1259. var id = 'artist';
  1260. if (artistIndex > 0) id += '_' + artistIndex;
  1261. while ((ref = document.getElementById(id)) == null) AddArtistField();
  1262. } else {
  1263. while ((ref = document.querySelectorAll('input[name="artists[]"]')).length <= artistIndex) AddArtistField();
  1264. ref = ref[artistIndex];
  1265. }
  1266. if (ref == null) throw new Error('Failed to allocate artist fields');
  1267. ref.value = iter;
  1268. ref.nextElementSibling.value = i + 1;
  1269. if (++artistIndex >= 200) break catLoop;
  1270. }
  1271. if (overwrite && artistIndex > 0) while (document.getElementById('artist_' + artistIndex) != null) {
  1272. RemoveArtistField();
  1273. }
  1274. }
  1275. // Processing album title
  1276. let album = release.album;
  1277. [ // Release type
  1278. [/\s+(?:\(Single\)|\[Single\]|(?:[\-\−\—\–]\s+)Single)$/i, 'Single', true, true],
  1279. [/\s+(?:\(EP\)|\[EP\]|(?:-\s+)?EP)$/, 'EP', true, true],
  1280. [/(?:^(?:Live)\s+(?:[aA]t|[Ii]n)\b|^Directo?\s+[Ee]n\b|\bUnplugged\b|\b(?:Acoustic\s+Stage|In\s+Concert)\b|\s+(?:Live)$)/, 'Live album', false, false],
  1281. [/\s+(?:\((?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\(\)]*\)|(?:[\-\−\—\–]\s+)(?:Live|En\s+Directo|(?:Ao|En)\s+Vivo))$/i, 'Live album', false, false],
  1282. [/\s+\[(?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\[\]]*\]$/i, 'Live album', false, false],
  1283. [/\S:\s+(?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b/i, 'Live album', false, false],
  1284. [/\b(?:(?:Best\s+Of|Greatest\s+Hits|Complete\s+(.+?\s+)?(?:Albums|Recordings))\b|Collection$)|^The(\s+\w+)+Years$|(?:^|[\:\-]\s+)(?:(?:The\s+)Essential)\b|\b(?:19|20)\d{2}(?:\s*[\-\−\—\–]\s*|\s+(?:to)\s+)(?:19|20)\d{2}\b/i, 'Anthology', false, false],
  1285. [/\s+(?:\((?:Anthology|Rarities)\)|\[(?:Anthology|Rarities)\])/i, 'Anthology', false, false],
  1286. [/\s+(?:\(Bootleg\)|\[Bootleg\]|(?:[\-\−\—\–]\s+)?Bootleg)$/i, 'Bootleg', false, true],
  1287. [/\s+(?:\(Remix(?:es)?\)|\[Remix(?:es)?\]|(?:[\-\−\—\–]\s+)?Remix(?:es)?)$/i, 'Remix', false, false],
  1288. [/\s+(?:\(Mixtape\)|\[Mixtape\]|(?:[\-\−\—\–]\s+)?Mixtape)$/i, 'Mixtape', false, true],
  1289. [/\s+(?:\(Demos?\)|\[Demos?\]|(?:[\-\−\—\–]\s+)?Demos?)$/i, 'Demo', false, true],
  1290. [/\s+(?:\(Concert\s+Recording\)|\[Concert\s+Recording\]|(?:[\-\−\—\–]\s+)Concert\s+Recording)$/i, 'Concert Recording', false, true],
  1291. [/\s+(?:\(DJ\s+Mix\)|\[DJ\s+Mix\]|(?:[\-\−\—\–]\s+)?DJ\s+Mix)$/i, 'DJ Mix', false, true],
  1292. [/\s+(?:\(Interview\)|\[Interview\]|(?:[\-\−\—\–]\s+)?Interview)$/i, 'Interview', false, false],
  1293. ].forEach(function(it) {
  1294. if ((matches = it[0].exec(album)) == null) return;
  1295. if (it[2] || !releaseType) releaseType = getReleaseTypeValue(it[1]);
  1296. if (it[3]) album = album.slice(0, matches.index);
  1297. });
  1298. rx = '\\b(?:Soundtrack|Score|Motion\\s+Picture|Series|Television|Original(?:\\s+\\w+)?\\s+Cast|Music\\s+from|(?:Musique|Bande)\\s+originale)\\b';
  1299. if (releaseType == getReleaseTypeValue('Soundtrack')
  1300. || reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
  1301. if (!releaseType) releaseType = getReleaseTypeValue('Soundtrack');
  1302. tags.add('score');
  1303. composerEmphasis = true;
  1304. }
  1305. if (!releaseType && remixParsers.some(rx => rx.test(release.album))) releaseType = getReleaseTypeValue('Remix');
  1306. if (!editionTitle && !isRequestNew && !isRequestEdit) [ // Edition
  1307. /\s+\(((?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissued?|Deluxe|Enhanced|Expanded|Limited|Version|\d+th\s+Anniversary)\b[^\(\)]*|[^\(\)]*\b(?:Edition|Version|Promo|Releasedition|Reissue))\)$/i,
  1308. /\s+\[((?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissued?|Deluxe|Enhanced|Expanded|Limited|Version|\d+th\s+Anniversary)\b[^\[\]]*|[^\[\]]*\b(?:Edition|Version|Promo|Release|Édition|Reissue))\]$/i,
  1309. /\s+-\s+([^\[\]\(\)\-\−\—\–]*\b(?:(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Bonus\s+Track)\b[^\[\]\(\)\-\−\—\–]*|Reissued?|Edition|Version|Promo|Enhanced|Release|Édition))$/i,
  1310. ].forEach(function(rx) {
  1311. if ((matches = rx.exec(album)) == null || release.album_year > 0 && release.album_year == releaseYear
  1312. && /\b(?:remaster|reissue|anniversary)\b/i.test(matches[1])) return;
  1313. album = album.slice(0, matches.index);
  1314. editionTitle = matches[1];
  1315. });
  1316. [ // Media
  1317. [/\s+(?:\[(?:LP|Vinyl|12"|7")\]|\((?:LP|Vinyl|12"|7")\))$/, 'Vinyl'],
  1318. [/\s+(?:\[SA-?CD\]|\(SA-?CD\))$/, 'SACD'],
  1319. [/\s+(?:\[(?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\]|\((?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\))$/, isOPS ? 'BD' : 'Blu-Ray'],
  1320. [/\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/, 'DVD'],
  1321. ].forEach(function(it) {
  1322. if ((matches = it[0].exec(album)) == null) return;
  1323. media = it[1];
  1324. album = album.slice(0, matches.index);
  1325. });
  1326. if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]')))
  1327. ref.value = prefs.fix_capitalization ? album.properlyFixCapitalization(language) : album;
  1328. if (yadg_prefil) yadg_prefil += ' ';
  1329. yadg_prefil += album;
  1330. if (elementWritable(ref = document.getElementById('yadg_input'))) {
  1331. ref.value = yadg_prefil || '';
  1332. if (yadg_prefil && (ref = document.getElementById('yadg_submit')) != null && !ref.disabled) ref.click();
  1333. }
  1334. if (!release.album_year) release.album_year = parseInt(getHomoIdentifier('PUBYEAR')) || undefined;
  1335. if (elementWritable(ref = document.getElementById('year'))) {
  1336. ref.value = release.album_year || '';
  1337. }
  1338. if (elementWritable(ref = document.getElementById('remaster_year'))
  1339. || !isUpload && i > 0 && (ref = document.querySelector('input[name="year"]')) != null && !ref.disabled)
  1340. ref.value = releaseYear || '';
  1341. //if (!editionTitle && tracks.every(it => it.identifiers.EXPLICIT == 0)) editionTitle = 'Clean' + (editionTitle ? ' / ' + editionTitle : '');
  1342. [
  1343. /\s+\(([^\(\)]+)\)\s*$/,
  1344. /\s+\[([^\[\]]+)\]\s*$/,
  1345. /\s+\{([^\{\}]+)\}\s*$/,
  1346. ].forEach(function(rx) {
  1347. var version = tracks.map(track => rx.test(track.title) ? RegExp.$1 : null);
  1348. if (!(version = version.homogeneous() && version[0])) return;
  1349. if (!editionTitle && /\b(?:Remastered|Remasterisée|Remasterizado|Acoustic|Instrumental)\b/i.test(version)
  1350. && releaseType != getReleaseTypeValue('Single')) editionTitle = version;
  1351. if (!releaseType && /^(?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b/i.test(version))
  1352. releaseType = getReleaseTypeValue('Live album');
  1353. });
  1354. let dualMono = getHomoIdentifier('DUALMONO') == 1 || /\b(?:Mono)\b/i.test(release.channel_mode);
  1355. if (elementWritable(ref = document.getElementById('remaster_title'))) {
  1356. ref.value = editionTitle || '';
  1357. if (dualMono) if (ref.value) ref.value += ' / MONO'; else ref.value = 'MONO';
  1358. }
  1359. if (elementWritable(ref = document.getElementById('remaster_record_label')
  1360. || document.querySelector('input[name="recordlabel"]')))
  1361. ref.value = release.label ? (function() {
  1362. if (prefs.selfrelease_label && (!isVA && release.label.toLowerCase() == release.artist.toLowerCase()
  1363. || selfReleaseParsers.some(rx => rx.test(release.label)))) return prefs.selfrelease_label;
  1364. return release.label.split(/\s*[\;\/]\s*|\s+\-\s+/)
  1365. .map(label => labelSubstitutes.reduce((l, def) => l.replace(...def), label)).filter(Boolean).join(' / ');
  1366. })() : '';
  1367. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  1368. || document.querySelector('input[name="cataloguenumber"]')))
  1369. ref.value = release.catalogs.length >= 1
  1370. && release.catalogs.map(it => it.replace(/\s*;\s*/g, ' / ')).join(' / ') || barCode || '';
  1371. var scene = getHomoIdentifier('SCENE');
  1372. if (isUpload && scene != undefined && (ref = document.getElementById('scene')) != null && !ref.disabled) try {
  1373. ref.checked = eval(scene.toLowerCase());
  1374. } catch(e) { console.warn('Invalid SCENE value (' + scene + ')') }
  1375. var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
  1376. if (elementWritable(ref = document.getElementById('format'))) {
  1377. ref.value = allowedFormats.includes(release.codec) ? release.codec : (isRED ? '' : '---');
  1378. ref.onchange(); //exec(function() { Format() });
  1379. }
  1380. if (isRequestNew) if (prefs.always_request_perfect_flac) reqSelectFormats('FLAC');
  1381. else if (release.codec) reqSelectFormats(release.codec);
  1382. var encoding;
  1383. if (release.encoding == 'lossless') {
  1384. if (release.bitdepths.includes(24)) encoding = '24bit Lossless';
  1385. else if (release.bitdepths.some(bitdepth => bitdepth > 0)) encoding = 'Lossless';
  1386. } else if (release.encoding == 'lossy' && release.bitrates.length > 0) {
  1387. let lame_version = release.codec == 'MP3' && /^LAME(\d+)\.(\d+)/i.test(release.vendor) ?
  1388. parseInt(RegExp.$1) * 1000 + parseInt(RegExp.$2) : undefined;
  1389. if (release.codec == 'MP3' && release.codec_profile == 'VBR V0') {
  1390. encoding = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
  1391. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V1') {
  1392. encoding = 'V1 (VBR)'
  1393. } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V2') {
  1394. encoding = lame_version >= 3094 ? encoding = 'V2 (VBR)' : 'APS (VBR)'
  1395. } else if (release.bitrates.length == 1 && [192, 256, 320].includes(Math.round(release.bitrates[0]))) {
  1396. encoding = Math.round(release.bitrates[0]);
  1397. } else encoding = 'Other';
  1398. }
  1399. if ((ref = document.getElementById('bitrate')) != null && !ref.disabled && (overwrite || !br_isSet)) {
  1400. ref.value = encoding || '';
  1401. ref.onchange(); //exec(function() { Bitrate() });
  1402. if (encoding == 'Other' && (ref = document.getElementById('other_bitrate')) != null) {
  1403. ref.value = Math.round(release.bitrates.length == 1 ? release.bitrates[0] : albumBitrate);
  1404. if ((ref = document.getElementById('vbr')) != null) ref.checked = release.bitrates.length > 1;
  1405. }
  1406. }
  1407. if (isRequestNew) {
  1408. if (prefs.always_request_perfect_flac) {
  1409. reqSelectBitrates('Lossless', '24bit Lossless');
  1410. } else if (encoding) reqSelectBitrates(encoding);
  1411. }
  1412. if (release.media) media = estimateMedia(release.media) || media;
  1413. const vinylTest = /^((?:Vinyl|LP) rip by\s+)(.*)$/im,
  1414. vinyltrackParser = /^([A-Z])(?:[\-\.\s]?((\d+)(\.?\S+)?))?$/i;
  1415. if (!media) {
  1416. if (tracks.every(isRedBook)) {
  1417. addMessage('media not determined - CD estimated', 'info');
  1418. media = 'CD';
  1419. } else if (tracks.every(track => vinyltrackParser.test(track.track_number))) {
  1420. addMessage('media not determined - vinyl estimated', 'info');
  1421. media = 'Vinyl';
  1422. } else if (tracks.some(t => t.bitdepth > 16 || (t.samplerate > 0 && t.samplerate != 44100)
  1423. || t.samples > 0 && t.samples % 588 != 0)) addMessage('media not determined - NOT CD', 'info');
  1424. } else if (media != 'CD' && tracks.every(isRedBook))
  1425. addMessage('CD as source media is estimated (' + media + ')', 'info');
  1426. if (elementWritable(ref = document.getElementById('media')))
  1427. ref.value = media || !tracks.some(notRedBook) && prefs.default_medium || (isRED ? '' : '---');
  1428. if (media == 'Vinyl') {
  1429. let badTracks = tracks.filter(track => !vinyltrackParser.test(track.track_number) && isNaN(parseInt(track.track_number)));
  1430. if (badTracks.length > 0) addMessage('at one or more vinyl tracks having invalid track# format: ' +
  1431. badTracks.map(track => track.track_number), 'warning');
  1432. }
  1433. if (isRequestNew) {
  1434. if (prefs.always_request_perfect_flac) reqSelectMedias('WEB', 'CD', isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD')
  1435. else if (media) reqSelectMedias(media);
  1436. }
  1437. function isRedBook(track) {
  1438. return track.bitdepth == 16 && track.samplerate == 44100 && track.channels == 2 && track.samples > 0 && track.samples % 588 == 0;
  1439. }
  1440. function notRedBook(track) {
  1441. return track.bitdepth && track.bitdepth != 16 || track.samplerate && track.samplerate != 44100
  1442. || track.channels && track.channels != 2 || track.samples && track.samples % 588 != 0;
  1443. }
  1444. if (tracks.every(it => it.identifiers.ORIGINALFORMAT && it.identifiers.ORIGINALFORMAT.includes('DSD')))
  1445. isFromDSD = true;
  1446. // Release type
  1447. if (!releaseType/* || isCompilation)*/)
  1448. if (isVA) releaseType = getReleaseTypeValue('Compilation');
  1449. else if (isCompilation || totalTime > 0 && totalTime >= prefs.anthology_threshold)
  1450. releaseType = getReleaseTypeValue('Anthology');
  1451. if ((ref = document.getElementById('releasetype')) != null)
  1452. if (!ref.disabled && (overwrite || ref.value == 0 || ref.value == '---'))
  1453. ref.value = releaseType || getReleaseTypeValue('Album');
  1454. else if (/*isUpload && */ref.value > 0 && document.location.search.includes('groupid='))
  1455. releaseType = parseInt(ref.value);
  1456. // Tags
  1457. if (prefs.estimate_decade_tag && (!totalTime || totalTime < 2 * 60 * 60) && !isClassical && release.album_year > 1900
  1458. && (!releaseType || ['Album', 'Soundtrack', 'EP', 'Single', 'Mixtape', 'Interview', 'Demo']
  1459. .some(rt => releaseType == getReleaseTypeValue(rt)))) //&& !/\b(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissue|Anniversary|Collector(?:'?s)?)\b/i.test(editionTitle)*/)
  1460. tags.add(Math.floor(release.album_year / 10) * 10 + 's'); // experimental
  1461. if (release.country) {
  1462. if (!excludedCountries.some(it => it.test(release.country))) tags.add(release.country);
  1463. }
  1464. if (!composerEmphasis && tracks.every(track => track.identifiers.HASLYRICS == 0)) tags.add('instrumental');
  1465. if (elementWritable(ref = document.getElementById('tags'))) {
  1466. ref.value = tags.toString();
  1467. if (artists[0].length == 1 && prefs.fetch_tags_from_artist > 0) setTimeout(function() {
  1468. var artist = getSiteArtist(artists[0][0]);
  1469. if (!artist) return;
  1470. tags.add(...artist.tags.sort((a, b) => b.count - a.count).map(it => it.name)
  1471. .slice(0, prefs.fetch_tags_from_artist));
  1472. var ref = document.getElementById('tags');
  1473. ref.value = tags.toString();
  1474. }, 3000);
  1475. }
  1476. if (!composerEmphasis/* && release.genres.length > 0*/ && !prefs.keep_meaningles_composers) {
  1477. document.querySelectorAll('input[name="artists[]"]').forEach(function(i) {
  1478. if (['4', '5'].includes(i.nextElementSibling.value)) i.value = '';
  1479. });
  1480. }
  1481.  
  1482. const doubleParsParsers = [
  1483. /\(+(\([^\(\)]*\))\)+/,
  1484. /\[+(\[[^\[\]]*\])\]+/,
  1485. /\{+(\{[^\{\}]*\})\}+/,
  1486. ];
  1487. tracks.forEach(function(track) {
  1488. doubleParsParsers.forEach(function(rx) {
  1489. if (!rx.test(track.title)) return;
  1490. addMessage('doubled parentheses in track #' + track.track_number + ' title ("' + track.title + '")', 'warning');
  1491. //track.title.replace(rx, RegExp.$1);
  1492. });
  1493. });
  1494. if (tracks.length > 1 && tracks.map(track => track.title).homogeneous())
  1495. addMessage('all tracks having same title: ' + tracks[0].title, 'warning');
  1496. if (prefs.check_logs && isUpload && !isOPS) findPreviousUploads();
  1497. // Album description
  1498. sourceUrl = getStoreUrl();
  1499. if ((ref = document.querySelector('tr#autofill_tr > td > select')) != null) {
  1500. if (i = getHomoIdentifier('DISCOGS_ID')) {
  1501. ref.value = 'discogs';
  1502. ref.onchange();
  1503. if (elementWritable(ref = document.getElementById('discogs'))) ref.value = i;
  1504. } else if (i = getHomoIdentifier('MBID')) {
  1505. ref.value = 'musicbrainz';
  1506. ref.onchange();
  1507. if (elementWritable(ref = document.getElementById('musicbrainz'))) ref.value = i;
  1508. }
  1509. }
  1510. if (!media && (ref = document.getElementById('media')) != null && ref.value && ref.value != '---')
  1511. media = ref.value;
  1512. const classicalWorkParsers = [
  1513. /^(.*?\S):\s+(.+)$/,
  1514. /^(.+?)(?::|\s-)\s+([CDILMVX]+(?:\.|\s-)\s+.+)$/,
  1515. /^(.+?)(?::|\s-)\s+((?:No\.\s*)?\d+(?:\.|\s-)\s+.+)$/,
  1516. ];
  1517. var description;
  1518. if (isRequestNew || isRequestEdit) { // request
  1519. description = [];
  1520. if (!isNaN(releaseDate) && !/^\s*\d{4}\s*$/.test(release.release_date)) {
  1521. let today = new Date().getDateValue();
  1522. description.push((releaseDate.getDateValue() < today ? 'Released' : 'Releasing') + ' ' + releaseDate.toDateString());
  1523. if (prefs.upcoming_tags && releaseDate.getDateValue() >= today
  1524. && (ref = document.getElementById('tags')) != null && !ref.disabled) {
  1525. let tags = new TagManager(ref.value);
  1526. tags.add(prefs.upcoming_tags);
  1527. ref.value = tags.toString();
  1528. }
  1529. }
  1530. if (!prefs.include_tracklist_in_request) {
  1531. let summary = '';
  1532. if (release.totalDiscs > 1) summary += release.totalDiscs + ' discs, ';
  1533. summary += tracks.length + ' track'; if (tracks.length > 1) summary += 's';
  1534. if (totalTime > 0) summary += ', ' + makeTimeString(totalTime);
  1535. description.push(summary);
  1536. }
  1537. if (sourceUrl || release.urls.length > 0) description.push(getUrls());
  1538. if (release.catalogs.length == 1 && /^\d{10,}$/.test(release.catalogs[0]) || /^\d{10,}$/.test(barCode)) {
  1539. description.push('[url=https://www.google.com/search?q=' + RegExp.lastMatch + ']Find more stores...[/url]');
  1540. }
  1541. if (prefs.include_tracklist_in_request) description.push(genPlaylist());
  1542. if (release.descriptions.length > 0) Array.prototype.push.apply(description, release.descriptions);
  1543. description = genAlbumHeader() + description.join('\n\n');
  1544. if (description.length > 0) {
  1545. ref = document.getElementById('description') || document.querySelector('textarea[name="description"]');
  1546. if (elementWritable(ref)) {
  1547. ref.value = description;
  1548. } else if (isRequestEdit && ref != null && !ref.disabled) {
  1549. ref.value = ref.value.length > 0 ? ref.value + '\n\n' + description : ref.value = description;
  1550. preview(0);
  1551. }
  1552. }
  1553. if (isRequestNew && prefs.request_default_bounty > 0) {
  1554. let amount = prefs.request_default_bounty < 1024 ? prefs.request_default_bounty : prefs.request_default_bounty / 1024;
  1555. if ((ref = document.getElementById('amount_box')) != null && !ref.disabled) ref.value = amount;
  1556. if ((ref = document.getElementById('unit')) != null && !ref.disabled) {
  1557. ref.value = prefs.request_default_bounty < 1024 ? 'mb' : 'gb';
  1558. }
  1559. try { Calculate() } catch(e) { /* Orpheus bug void handler */ }
  1560. }
  1561. } else { // upload
  1562. description = '';
  1563. if (prefs.bpm_summary && albumBPM > 0) description += '\n\nAverage album BPM: [code]' + albumBPM + '[/code]';
  1564. // if (!isNaN(releaseDate)) {
  1565. // if (!isNaN(rd)) description = '\n\nRelease date: ' + releaseDate.toDateString();
  1566. // }
  1567. let vinylRipInfo;
  1568. if (release.descriptions.length > 0) {
  1569. description += '\n\n';
  1570. if (isRED && prefs.tracklist_style == 3) description += '[pad=0|20]';
  1571. if (release.descriptions.length == 1 && release.descriptions[0]
  1572. && (matches = vinylTest.exec(release.descriptions[0])) != null) {
  1573. vinylRipInfo = release.descriptions[0].slice(matches.index).trim().split(/(?:[ \t]*\r?\n)+/);
  1574. description += release.descriptions[0].slice(0, matches.index).trim();
  1575. } else description += release.descriptions.join('\n\n');
  1576. if (isRED && prefs.tracklist_style == 3) description += '[/pad]';
  1577. }
  1578. let oa = fetchOnlineAdditions().then(t => { description += '\n\n' + t }, reason => undefined);
  1579. if (elementWritable(ref = document.getElementById('album_desc'))) {
  1580. ref.value = genPlaylist();
  1581. finalizeDesc(ref);
  1582. }
  1583. if ((ref = document.getElementById('body') || document.querySelector('textarea[name="body"]')) != null && !ref.disabled) {
  1584. if (overwrite || ref.value.length == 0) ref.value = genPlaylist(); else {
  1585. let eT;
  1586. if (editionTitle) {
  1587. eT = prefs.fix_capitalization ? editionTitle.properlyFixCapitalization(language) : editionTitle;
  1588. if (releaseYear > 0) eT += ' (' + releaseYear + ')';
  1589. }
  1590. ref.value += '\n\n' + genPlaylist(false, false, eT);
  1591. }
  1592. finalizeDesc(ref);
  1593. }
  1594. function finalizeDesc(elem) {
  1595. oa.then(function() {
  1596. if (description) elem.value += description;
  1597. preview(0);
  1598. });
  1599. }
  1600. // Release description
  1601. if (elementWritable(ref = document.getElementById('release_samplerate'))) {
  1602. ref.value = Object.keys(release.sampleRates).length == 1 && Object.keys(release.sampleRates)[0] ?
  1603. Math.floor(Object.keys(release.sampleRates)[0] / 1000) :
  1604. Object.keys(release.sampleRates).length > 1 || isNaN(Object.keys(release.sampleRates)[0]) ? '999' : '';
  1605. }
  1606. let lineage = '', rlsDesc = '';
  1607. let drInfo = '[hide=DR' + (release.albumdrs.length == 1 ? release.albumdrs[0] : '') + '][pre][/pre]';
  1608. let hasSR = Object.keys(release.sampleRates).length > 0;
  1609. let srInfo = hasSR ? Object.keys(release.sampleRates).filter(samplerate => samplerate > 0)
  1610. .sort((a, b) => release.sampleRates[b] - release.sampleRates[a])
  1611. .map(f => f / 1000).join('/') + 'kHz' : '';
  1612. if (['BD', 'Blu-Ray', 'DVD', 'SACD'].includes(media)) {
  1613. if (!isNWCD) rlsDesc = srInfo;
  1614. addChannelInfo();
  1615. if (media == 'SACD' || isFromDSD) addDSDInfo();
  1616. if (prefs.cleanup_descriptions) addDRInfo();
  1617. //addRGInfo();
  1618. addHybridInfo();
  1619. drInfo += '[/hide]';
  1620. } else if (media == 'Vinyl') {
  1621. let hassr = hasSR && (!isNWCD || Object.keys(release.sampleRates).length > 1);
  1622. if (hassr) lineage = srInfo + ' ';
  1623. if (vinylRipInfo) {
  1624. if (vinylTest.test(vinylRipInfo[0]) && RegExp.$2.toLowerCase() != 'unknown')
  1625. vinylRipInfo[0] = vinylRipInfo[0].replace(vinylTest, '$1[color=blue]$2[/color]');
  1626. if (hassr) vinylRipInfo[0] = vinylRipInfo[0].replace(/^Vinyl\b/, 'vinyl');
  1627. lineage += vinylRipInfo[0];
  1628. lineage += '\n\n[u]Lineage:[/u]' + vinylRipInfo.slice(1).map(l => '\n' + [
  1629. // RuTracker translation
  1630. ['Код класса состояния винила', 'Vinyl condition class'],
  1631. ['Устройство воспроизведения', 'Turntable'],
  1632. ['Головка звукоснимателя', 'Cartridge'],
  1633. ['Картридж', 'Cartridge'],
  1634. ['Предварительный усилитель', 'Preamplifier'],
  1635. ['АЦП', 'ADC'],
  1636. ['Программа-оцифровщик', 'Software'],
  1637. ['Обработка звука', 'Audio post-processing'],
  1638. ['Обработка', 'Post-processing'],
  1639. ].reduce((acc, it) => acc.replace(...it), l)).join('');
  1640. } else lineage += `${hassr ? ' vinyl' : 'Vinyl'} rip by [color=blue][/color]\n\n[u]Lineage:[/u]\n`;
  1641. let imgs = '\n[img][/img]'.repeat(8);
  1642. if (!isNWCD) drInfo += '\n' + imgs; else lineage += '\n\n[hide]' + imgs.slice(1) + '[/hide]';
  1643. drInfo += '[/hide]';
  1644. } else if (tracks.some(track => track.bitdepth > 16)) { // other Hi-Res
  1645. if (!isNWCD || Object.keys(release.sampleRates).length > 1) rlsDesc = srInfo;
  1646. if (release.channels && release.channels != 2 || dualMono) addChannelInfo();
  1647. if (isFromDSD) addDSDInfo();
  1648. if (!isFromDSD || prefs.cleanup_descriptions) addDRInfo();
  1649. //addRGInfo();
  1650. addHybridInfo();
  1651. if (isFromDSD || prefs.cleanup_descriptions || Object.keys(release.sampleRates).length == 1
  1652. && Object.keys(release.sampleRates)[0] == 88200) drInfo += '[/hide]'; else drInfo = null;
  1653. } else { // 16bit and lossy
  1654. if (Object.keys(release.sampleRates).some(f => f != 44100)) rlsDesc = srInfo;
  1655. if (release.channels && release.channels != 2 || dualMono) addChannelInfo();
  1656. addDRInfo();
  1657. //addRGInfo();
  1658. if (prefs.cleanup_descriptions) drInfo += '[/hide]'; else drInfo = null;
  1659. if (release.codec == 'MP3' && release.vendor) {
  1660. // TODO: parse mp3 vendor string
  1661. } else if (['AAC', 'Opus', 'Vorbis'].includes(release.codec) && release.vendor) {
  1662. let _encoder_settings = release.vendor;
  1663. if (release.codec == 'AAC' && /^(?:qaac)\s+[\d\.]+/i.test(release.vendor)) {
  1664. let enc = [];
  1665. if (matches = release.vendor.match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
  1666. if (matches = release.vendor.match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
  1667. if (matches = release.vendor.match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
  1668. if (matches = release.vendor.match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
  1669. if (matches = release.vendor.match(/\bQuality\s+(\d+)\b/i)) enc[5] = matches[1];
  1670. _encoder_settings = 'Converted by Apple\'s ' + enc[2] + ' encoder (' + enc[3] + '-' + enc[4] + ')';
  1671. }
  1672. lineage = _encoder_settings;
  1673. }
  1674. }
  1675. function addDSDInfo() {
  1676. var nfo = ' DSD64';
  1677. if (prefs.sacd_decoder) nfo += ' using ' + prefs.sacd_decoder;
  1678. nfo += '\nOutput gain: [code]+0dB[/code]';
  1679. if (isNWCD) lineage = 'From' + nfo; else {
  1680. if (rlsDesc.length > 0) rlsDesc += ' from'; else rlsDesc = 'From';
  1681. rlsDesc += nfo;
  1682. }
  1683. }
  1684. function addDRInfo() {
  1685. if (release.albumdrs.length < 1 || document.getElementById('release_dynamicrange') != null) return;
  1686. var nfo = 'DR' + release.albumdrs[0];
  1687. if (release.albumdrs[0] < 4) nfo = '[color=red]' + nfo + '[/color]';
  1688. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1689. rlsDesc += nfo;
  1690. }
  1691. function addRGInfo() {
  1692. if (release.albumgains.length <= 0) return;
  1693. if (rlsDesc.length > 0) rlsDesc += ' | ';
  1694. rlsDesc += 'RG'; //rlsDesc += 'RG ' + albumgains[0];
  1695. }
  1696. function addChannelInfo() {
  1697. if (release.channel_mode) var chi = release.channel_mode;
  1698. else if (getHomoIdentifier('DUAL_MONO')) chi = 'dual mono';
  1699. else if (release.channels) chi = getChanString(release.channels);
  1700. if (chi.length <= 0) return;
  1701. if (rlsDesc.length > 0) rlsDesc += ', '; else rlsDesc = 'Channels configuration: ';
  1702. rlsDesc += chi;
  1703. }
  1704. function addHybridInfo() {
  1705. if (release.bitdepths.length > 1) release.bitdepths.filter(bitdepth => bitdepth != 24).forEach(function(bitdepth) {
  1706. var hybrid_tracks = tracks.filter(it => it.bitdepth == bitdepth).sort(trackComparer).map(function(it) {
  1707. return (release.totalDiscs > 1 && it.disc_number ? it.disc_number + '-' : '') + it.track_number;
  1708. });
  1709. if (hybrid_tracks.length < 1) return;
  1710. if (rlsDesc.length > 0) rlsDesc += '\n';
  1711. rlsDesc += 'Note: track';
  1712. if (hybrid_tracks.length > 1) rlsDesc += 's';
  1713. rlsDesc += ' #' + hybrid_tracks.join(', ') +
  1714. (hybrid_tracks.length > 1 ? ' are' : ' is') + ' ' + bitdepth + 'bit lossless';
  1715. });
  1716. }
  1717. function addRlsDate() {
  1718. if (prefs.insert_release_date && !isNaN(releaseDate) && !/^\s*\d{4}\s*$/.test(release.release_date))
  1719. rlsDesc.push('Released ' + releaseDate.toDateString());
  1720. }
  1721. rlsDesc = rlsDesc.length > 0 ? [rlsDesc] : [];
  1722. if ((ref = document.getElementById('release_lineage')) != null) {
  1723. lineage = lineage ? [lineage] : [];
  1724. if (drInfo) rlsDesc.push(drInfo);
  1725. addRlsDate();
  1726. if (sourceUrl || release.urls.length > 0) lineage.push(getUrls());
  1727. if (elementWritable(ref)) {
  1728. ref.value = lineage.join('\n\n');
  1729. preview(1);
  1730. }
  1731. } else {
  1732. if (lineage.length > 0) rlsDesc.push(lineage);
  1733. if (drInfo) rlsDesc.push(drInfo);
  1734. addRlsDate();
  1735. if (sourceUrl || release.urls.length > 0) rlsDesc.push(getUrls());
  1736. }
  1737. if (elementWritable(ref = document.getElementById('release_desc'))) {
  1738. ref.value = rlsDesc.join('\n\n');
  1739. if (rlsDesc.length > 0) preview(isNWCD ? 2 : 1);
  1740. }
  1741. if (release.encoding == 'lossless' && tracks.some(track => track.bitdepth == 24) && release.dirpaths.length == 1) {
  1742. if ((ref = document.getElementById('release_desc')) != null) GM_xmlhttpRequest({
  1743. method: 'GET',
  1744. url: new URL('file:' + release.dirpaths[0] + '\\foo_dr.txt').href,
  1745. responseType: 'blob',
  1746. onload: function(response) {
  1747. if (response.status < 200 || response.status >= 400) return defaultErrorHandler(response);
  1748. if (!/(\[hide=DR\d*\]\[pre\])\[\/pre\]/im.test(ref.value)) return;
  1749. var ndx = RegExp.lastIndex + RegExp.$1.length;
  1750. ref.value = ref.value.slice(0, ndx) + response.responseText + ref.value.slice(ndx);
  1751. },
  1752. onerror: error => { console.error('foo_dr.txt not exists or is forbidden to read') },
  1753. ontimeout: defaultTimeoutHandler,
  1754. });
  1755. }
  1756. if (elementWritable(ref = document.getElementById('release_dynamicrange')))
  1757. ref.value = release.albumdrs.length == 1 ? release.albumdrs[0] : '';
  1758. // Compare to online source
  1759. if (!onlineSource) {
  1760. if (prefs.assume_weblink && !sourceUrl && release.urls.length <= 0) addMessage('No lineage URL', 'notice');
  1761. onlineSource = (function() {
  1762. if (sourceUrl || release.urls.length > 0) return urlResolver(sourceUrl || release.urls[0])
  1763. .then(sourceUrl => fetchOnline_Music(sourceUrl, true).then(completeFromOnlineSource));
  1764. return Promise.reject('No lineage URL');
  1765. })();
  1766. if (prefs.check_integrity_online) onlineSource.catch(reason => lookupOnlineSource().then(function(result) {
  1767. if (typeof result == 'object') return parseLastFm(result);
  1768. if (urlParser.test(result)) return fetchOnline_Music(result, true);
  1769. return Promise.reject('Unhandled format');
  1770. })).then(onlineCheck).catch(function(reason) {
  1771. if (!media || media == 'WEB') tracks.forEach(function(track) {
  1772. if (!track.duration || track.duration < 29.6 || track.duration > 30.4) return;
  1773. addMessage('track ' + track.track_number + ' possible track preview', 'warning');
  1774. });
  1775. });
  1776. }
  1777. } // upload
  1778. if ((isUpload || isRequestNew) && prefs.find_relations) lookupMusicRelations();
  1779. if (ajaxRejects > 0) {
  1780. let msg = 'AJAX request(s) eliminated due to Gazelle policy. ' +
  1781. 'Multiple artists not split correctly? Relaunch parsing in overwrite mode without page reload';
  1782. try {
  1783. let delay = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]).timeStamp + gazelleApiFrame - Date.now();
  1784. if (delay >= 0) {
  1785. msg += ' after ' + Math.ceil(delay / 1000) + 's';
  1786. setTimeout(() => { addMessage('new AJAX timeframe for requery available', 'info') }, delay);
  1787. }
  1788. addMessage(msg + '.', 'notice');
  1789. } catch(e) { console.error(e) }
  1790. }
  1791. if (prefs.clean_on_apply) clipBoard.value = '';
  1792. prefs.save();
  1793. return true;
  1794.  
  1795. // ---------------------------------------------------------------------------------------------------------------
  1796.  
  1797. function genPlaylist(pad = true, header = true, title = undefined) {
  1798. var style = prefs.tracklist_style;
  1799. if (style == 2) {
  1800. if (!tracks.every(track => track.duration)) style = 1;
  1801. else if (tracks.map(track => track.title).some(notMonospaced)
  1802. || tracks.map(track => track.track_artist).some(notMonospaced)
  1803. || composerEmphasis && tracks.map(track => track.composer).some(notMonospaced)) style = 3;
  1804. }
  1805. if (!(style > 0)) return null;
  1806. if (!isRED) pad = false;
  1807. let playlist = '';
  1808. if (tracks.length > 1 || prefs.singles_conventional_format || isRequestNew || isRequestEdit) {
  1809. if (header) playlist += genAlbumHeader();
  1810. playlist += '[size=4][b][color=' + prefs.tracklist_head_color + ']' +
  1811. (title || 'Tracklisting') + '[/color][/b][/size]';
  1812. playlist += '\n'; //'[hr]';
  1813. let lastDisc, lastSubtitle, lastClassicalWork, lastSide, vinylTrackWidth;
  1814. let block = 0, classicalWorks = new Map();
  1815. if (composerEmphasis /*isClassical*/ && !tracks.some(it => it.disc_subtitle)) {
  1816. tracks.forEach(function(track) {
  1817. if (!track.composer) return;
  1818. (/*isClassical ? classicalWorkParsers : */classicalWorkParsers.slice(1)).forEach(function(classicalWorkParser) {
  1819. if (track.classical_work || !classicalWorkParser.test(track.title)) return;
  1820. classicalWorks.set(track.classical_work = RegExp.$1, {});
  1821. track.classical_title = prefs.fix_capitalization ?
  1822. RegExp.$2.properlyFixCapitalization(language) : RegExp.$2;
  1823. });
  1824. });
  1825. for (iter of classicalWorks.keys()) {
  1826. let work = tracks.filter(track => track.classical_work == iter);
  1827. if (work.length > 1 || tracks.every(track => track.classical_work)) {
  1828. if (work[0].track_artist && work[0].track_artist != release.artist && work.map(track => track.track_artist).homogeneous())
  1829. classicalWorks.get(iter).performer = realTrackArtist(work[0].track_artist);
  1830. if (work[0].composer && release.composers.length > 1 && work.map(track => track.composer).homogeneous())
  1831. classicalWorks.get(iter).composer = work[0].composer;
  1832. } else {
  1833. work.forEach(function(track) {
  1834. delete track.classical_work;
  1835. delete track.classical_title;
  1836. });
  1837. classicalWorks.delete(iter);
  1838. }
  1839. }
  1840. }
  1841. let track, duration, volumes = new Map(tracks.map(it => [it.disc_number, undefined])),
  1842. tnOffset = 0, ignoreTrackartist = false, ignoreComposer = false;
  1843. volumes.forEach(function(val, key) {
  1844. volumes.set(key, new Set(tracks.filter(it => it.disc_number == key).map(it => it.disc_subtitle)).size)
  1845. });
  1846. if (!tracks.every(it => !isNaN(parseInt(it.track_number.toString())))
  1847. && !tracks.every(it => vinyltrackParser.test(it.track_number.toString().toUpperCase()))) {
  1848. addMessage('inconsistent tracks numbering (' + tracks.map(it => it.track_number) + ')', 'warning');
  1849. }
  1850. vinylTrackWidth = tracks.reduce((acc, it) => vinyltrackParser.test(it.track_number.toString().toUpperCase()) ?
  1851. Math.max(parseInt(RegExp.$3) || 0, acc) : acc, -1);
  1852. if (vinylTrackWidth >= 0) {
  1853. vinylTrackWidth = vinylTrackWidth.toString().length;
  1854. tracks.forEach(function(track) {
  1855. if ((matches = vinyltrackParser.exec(track.track_number.toString())) == null) return;
  1856. track.track_number = matches[1].toUpperCase();
  1857. if (matches[3]) track.track_number += matches[3].padStart(vinylTrackWidth, '0');
  1858. if (matches[4]) track.track_number += matches[4];
  1859. });
  1860. ++vinylTrackWidth;
  1861. }
  1862. if (release.totalDiscs < 2 && tracks.reduce(computeLowestTrack, undefined) - 1)
  1863. addMessage('track numbering not starting from 1', 'info');
  1864. const padUnit = isRED ? ['[pad=0|0|5|0]', '[/pad]'] : undefined;
  1865. if (canSort && prefs.sort_tracklist) tracks.sort(trackComparer);
  1866. tracks.forEach(function(_track) {
  1867. let title = '', trackArtist = undefined;
  1868. if (_track.track_artist && _track.track_artist != release.artist)
  1869. trackArtist = realTrackArtist(_track.track_artist);
  1870. let sameMedia = (release.totalDiscs > 1 && _track.disc_number ?
  1871. tracks.filter(track => track.disc_number == _track.disc_number) : tracks);
  1872. let ttwidth = sameMedia.every(t => t.track_number && parseInt(t.track_number) == t.track_number) ?
  1873. sameMedia.reduce((acc, track) => Math.max(acc, parseInt(track.track_number).toString().length), 2) : 0;
  1874.  
  1875. function realTrackNumber() {
  1876. return ttwidth > 0 && !(vinylTrackWidth >= 0) ?
  1877. parseInt(_track.track_number).toString().padStart(ttwidth, '0') : _track.track_number;
  1878. }
  1879. function prologue(prefix, postfix) {
  1880. function block1() {
  1881. if (block == 3) playlist += postfix;
  1882. playlist += '\n';
  1883. if (padUnit && ![1, 2].includes(block)) playlist += padUnit[0];
  1884. block = 1;
  1885. ignoreTrackartist = ignoreComposer = false;
  1886. }
  1887. function block2() {
  1888. if (block == 3) playlist += postfix;
  1889. playlist += '\n';
  1890. if (padUnit && ![1, 2].includes(block)) playlist += padUnit[0];
  1891. block = 2;
  1892. }
  1893. function block3() {
  1894. //if (block == 2 && isRED) playlist += '[hr]';
  1895. if (padUnit && [1, 2].includes(block)) playlist += padUnit[1];
  1896. playlist += '\n';
  1897. if (block != 3) playlist += prefix;
  1898. block = 3;
  1899. }
  1900. if (release.totalDiscs > 1 && _track.disc_number != lastDisc) {
  1901. block1();
  1902. lastDisc = _track.disc_number;
  1903. lastSubtitle = lastClassicalWork = undefined;
  1904. playlist += '[color=' + prefs.tracklist_disctitle_color + '][size=3][b]';
  1905. if (_track.identifiers.VOL_MEDIA && tracks.filter(it => it.disc_number == _track.disc_number)
  1906. .every(it => it.identifiers.VOL_MEDIA == _track.identifiers.VOL_MEDIA)) {
  1907. playlist += _track.identifiers.VOL_MEDIA.toUpperCase() + ' ';
  1908. } else playlist += 'Disc ' + _track.disc_number.toString();
  1909. if (_track.disc_subtitle && (volumes.get(_track.disc_number) || 0) == 1) {
  1910. playlist += ' – ' + (prefs.fix_capitalization ?
  1911. _track.disc_subtitle.properlyFixCapitalization(language) : _track.disc_subtitle);
  1912. lastSubtitle = _track.disc_subtitle;
  1913. }
  1914. playlist += '[/b][/size]';
  1915. duration = tracks.filter(it => it.disc_number == _track.disc_number).reduce((acc, it) => acc + it.duration, 0);
  1916. if (duration > 0) playlist += ' [size=2][i][' + makeTimeString(duration) + '][/i][/size]';
  1917. playlist += '[/color]';
  1918. tnOffset = tracks.filter(track => track.disc_number == _track.disc_number)
  1919. .reduce(computeLowestTrack, undefined) - 1 || 0;
  1920. if (tnOffset) addMessage('volume ' + _track.disc_number + ' track numbering not starting from 1', 'info');
  1921. }
  1922. if ((_track.disc_subtitle || undefined) != (lastSubtitle || undefined)) {
  1923. if (block != 1 || _track.disc_subtitle) block1();
  1924. if (_track.disc_subtitle) {
  1925. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
  1926. if (trackArtist && tracks.filter(track => track.disc_subtitle == _track.disc_subtitle)
  1927. .map(track => realTrackArtist(track.track_artist)).homogeneous()) {
  1928. playlist += trackArtist + ' - ';
  1929. ignoreTrackartist = true;
  1930. }
  1931. playlist += prefs.fix_capitalization ?
  1932. _track.disc_subtitle.properlyFixCapitalization(language) : _track.disc_subtitle;
  1933. if (_track.composer && composerEmphasis && release.composers.length != 1
  1934. && tracks.filter(track => track.disc_subtitle == _track.disc_subtitle)
  1935. .map(track => realTrackArtist(track.composer)).homogeneous()) {
  1936. playlist += ' (' + _track.composer + ')';
  1937. ignoreComposer = true;
  1938. }
  1939. playlist += '[/b][/size]';
  1940. duration = tracks.filter(it => it.disc_subtitle == _track.disc_subtitle)
  1941. .reduce((acc, it) => acc + it.duration, 0);
  1942. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  1943. playlist += '[/color]';
  1944. }
  1945. lastSubtitle = _track.disc_subtitle;
  1946. }
  1947. if (_track.classical_work != lastClassicalWork) {
  1948. if (_track.classical_work) {
  1949. block2();
  1950. playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
  1951. if (release.composers.length != 1 && classicalWorks.get(_track.classical_work).composer) {
  1952. playlist += classicalWorks.get(_track.classical_work).composer + ': ';
  1953. }
  1954. playlist += prefs.fix_capitalization ?
  1955. _track.classical_work.properlyFixCapitalization(language) : _track.classical_work;
  1956. playlist += '[/b]';
  1957. if (classicalWorks.get(_track.classical_work).performer
  1958. && classicalWorks.get(_track.classical_work).performer != release.artist) {
  1959. playlist += ' (' + classicalWorks.get(_track.classical_work).performer + ')';
  1960. }
  1961. playlist += '[/size]';
  1962. duration = tracks.filter(it => it.classical_work == _track.classical_work)
  1963. .reduce((acc, it) => acc + it.duration, 0);
  1964. if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
  1965. playlist += '[/color]';
  1966. } else if (block > 2) block1();
  1967. lastClassicalWork = _track.classical_work;
  1968. }
  1969. if (vinylTrackWidth >= 0) {
  1970. let vinylTrack = vinyltrackParser.test(_track.track_number);
  1971. if (block == 3 && lastSide && (vinylTrack ? RegExp.$1 != lastSide : _track.track_number == 1))
  1972. playlist += '\n';
  1973. lastSide = RegExp.$1;
  1974. }
  1975. block3();
  1976. } // prologue
  1977.  
  1978. switch (style) {
  1979. case 1:
  1980. case 3: {
  1981. prologue('[size=' + prefs.tracklist_size + ']', '[/size]\n');
  1982. track = '[b][color=' + prefs.tracklist_tracknumber_color + ']';
  1983. track += realTrackNumber();
  1984. track += '[/color][/b]' + prefs.title_separator;
  1985. if (!ignoreTrackartist && trackArtist
  1986. && (!_track.classical_work || !classicalWorks.get(_track.classical_work).performer))
  1987. title = '[color=' + prefs.tracklist_artist_color + ']' + trackArtist + '[/color] - ';
  1988. title += _track.classical_title || (prefs.fix_capitalization ?
  1989. _track.title.properlyFixCapitalization(language) : _track.title);
  1990. if (!ignoreComposer && _track.composer && composerEmphasis && release.composers.length != 1
  1991. && (!_track.classical_work || !classicalWorks.get(_track.classical_work).composer)) {
  1992. title = title + ' [color=' + prefs.tracklist_composer_color + '](' + _track.composer + ')[/color]';
  1993. }
  1994. playlist += track + title;
  1995. if (_track.duration) playlist += ' [i][color=' + prefs.tracklist_duration_color +'][' +
  1996. makeTimeString(_track.duration) + '][/color][/i]';
  1997. if (_track.lyrics) playlist += ' [size=1][hide=lyrics]' + _track.lyrics + '[/hide][/size]';
  1998. break;
  1999. }
  2000. case 2: {
  2001. prologue('[size=' + prefs.tracklist_size + '][pre]', '[/pre][/size]');
  2002. track = realTrackNumber();
  2003. track += prefs.title_separator;
  2004. if (!ignoreTrackartist && trackArtist
  2005. && (!_track.classical_work || !classicalWorks.get(_track.classical_work).performer))
  2006. title = trackArtist + ' - ';
  2007. title += _track.classical_title || (prefs.fix_capitalization ?
  2008. _track.title.properlyFixCapitalization(language) : _track.title);
  2009. if (!ignoreComposer && _track.composer && composerEmphasis && release.composers.length != 1
  2010. && (!_track.classical_work || !classicalWorks.get(_track.classical_work).composer))
  2011. title = title + ' (' + _track.composer + ')';
  2012. let l = 0, j, left, padding, spc;
  2013. duration = _track.duration ? ' [' + makeTimeString(_track.duration) + ']' : null;
  2014. let width = prefs.max_tracklist_width - track.length;
  2015. if (duration) width -= duration.length + 1;
  2016. while (title.trueLength() > 0) {
  2017. j = width;
  2018. if (title.trueLength() > width) {
  2019. while (j > 0 && title[j] != ' ') { --j }
  2020. if (j <= 0) j = width;
  2021. }
  2022. left = title.slice(0, j).trim();
  2023. if (++l <= 1) {
  2024. playlist += track + left;
  2025. if (duration) {
  2026. spc = width - left.trueLength();
  2027. padding = (spc < 2 ? ' '.repeat(spc) : ' ' + prefs.pad_leader.repeat(spc - 1)) + ' ';
  2028. playlist += padding + duration;
  2029. }
  2030. width = prefs.max_tracklist_width - track.length;
  2031. } else playlist += '\n' + ' '.repeat(track.length - 1) + left;
  2032. title = title.slice(j).trim();
  2033. }
  2034. break;
  2035. }
  2036. }
  2037. });
  2038. switch (style) {
  2039. case 1:
  2040. case 3:
  2041. if (totalTime > 0) playlist += '\n\n' + divs[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
  2042. ']Total time: [i]' + makeTimeString(totalTime) + '[/i][/color][/size]';
  2043. break;
  2044. case 2:
  2045. if (totalTime > 0) {
  2046. duration = '[' + makeTimeString(totalTime) + ']';
  2047. playlist += '\n\n' + divs[0].repeat(32).padStart(prefs.max_tracklist_width);
  2048. playlist += '\n' + 'Total time:'.padEnd(prefs.max_tracklist_width - duration.length) + duration;
  2049. }
  2050. playlist += '[/pre][/size]';
  2051. break;
  2052. }
  2053. if (pad) playlist = '[pad=8|0|8|0]' + playlist + '[/pad]';
  2054. if (style == 3) playlist = '[align=center]' + playlist + '[/align]';
  2055.  
  2056. function computeLowestTrack(acc, track) {
  2057. if (Number.isNaN(acc)) return NaN;
  2058. var tn = parseInt(track.track_number);
  2059. if (isNaN(tn)) return NaN;
  2060. return isNaN(acc) || tn < acc ? tn : acc;
  2061. }
  2062. } else { // single
  2063. playlist += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + release.artist + '[/color]';
  2064. playlist += isRED ? '[hr]' : '\n' + divs[0].repeat(24) + '\n';
  2065. playlist += tracks[0].title + '[/b]';
  2066. if (tracks[0].composer) {
  2067. playlist += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
  2068. }
  2069. if (tracks[0].duration) playlist += '\n\n[color=' + prefs.tracklist_duration_color +
  2070. '][' + makeTimeString(tracks[0].duration) + '][/color][/size]';
  2071. if (isRED) playlist = '[pad=20|20|20|20]' + playlist + '[/pad]';
  2072. playlist = '[align=center]' + playlist + '[/align]';
  2073. }
  2074. if (prefs.colorless_tracklist) playlist = playlist.replace(/\[color=\S+?\]/ig, '').replace(/\[\/color\]/ig, '');
  2075. return playlist;
  2076. }
  2077.  
  2078. function getUrls() {
  2079. var urls = [];
  2080. if (sourceUrl) urls.push(sourceUrl);
  2081. Array.prototype.push.apply(urls, release.urls.filter(url =>
  2082. urlParser.test(url) && (!sourceUrl || url.toLowerCase() != sourceUrl.toLowerCase())));
  2083. return urls.map(function(url) {
  2084. url = new URL(url);
  2085. let logoUrl = prefs.use_store_logos && [
  2086. ['qobuz.com', 'https://ptpimg.me/1saep4.png'],
  2087. ['bandcamp.com', 'https://ptpimg.me/vwki92.jpg' /*'https://ptpimg.me/7evz4g.png'*/],
  2088. //['highresaudio.com', 'https://ptpimg.me/65xx03.png'],
  2089. ['7digital.com', 'https://ptpimg.me/300scj.png'],
  2090. ['mora.jp', 'https://ptpimg.me/9rg495.png'],
  2091. ['deezer.com', 'https://ptpimg.me/181799.png'],
  2092. ['spotify.com', 'https://ptpimg.me/xo5d1p.png'],
  2093. ['tidal.com', 'https://ptpimg.me/w80424.png'],
  2094. ['music.apple.com', 'https://ptpimg.me/in7u5u.png'],
  2095. ['prestomusic.com', 'https://ptpimg.me/q86vjt.png'],
  2096. ['prostudiomasters.com', 'https://ptpimg.me/xkm0th.png'],
  2097. ['supraphonline.cz', 'https://ptpimg.me/h85655.png'],
  2098. ['hdtracks.com', 'https://ptpimg.me/eurm85.png'/*'https://ptpimg.me/wx36i4.png'*/],
  2099. ['nativedsd.com', 'https://ptpimg.me/m6j8gp.png'],
  2100. ['indies.eu', 'https://ptpimg.me/8a4w49.png'],
  2101. ['e-onkyo.com', 'https://ptpimg.me/uke3n1.png'],
  2102. ['beatport.com', 'https://ptpimg.me/lf8q75.png'],
  2103. ['junodownload.com', 'https://ptpimg.me/6c7y42.png'],
  2104. ['discogs.com', 'https://ptpimg.me/57y9c3.png'], // https://ptpimg.me/n5kmu7.png
  2105. ['musicbrainz.org', 'https://ptpimg.me/4m45i9.png'],
  2106. ].reduce((acc, site) => acc || url.hostname.endsWith(site[0]) && site[1], false);
  2107. return logoUrl ? `[url=${url}][img]${logoUrl}[/img][/url]` : '[url]' + url + '[/url]';
  2108. }).join('\n');
  2109. }
  2110.  
  2111. function genAlbumHeader() {
  2112. return !isVA && artists[0].length >= 3 ? '[size=4]' +
  2113. joinArtists(artists[0], artist => '[artist]' + artist + '[/artist]') + ' – ' + release.album + '[/size]\n\n' : '';
  2114. }
  2115.  
  2116. function findPreviousUploads() {
  2117. function searchLog(searchTerm) {
  2118. localFetch('/log.php?search=' + encodeURIComponent(searchTerm)).then(function(dom) {
  2119. dom.querySelectorAll('table > tbody > tr.rowb').forEach(function(tr) {
  2120. var msg = tr.children[1].textContent.trim();
  2121. if (!msg.includes('was deleted')) return;
  2122. if (release.codec && media && (matches = /\[(\w+)\/([^\[\]]+)\/([^\[\]]+)\]/.exec(msg)) != null) {
  2123. if (media != matches[3] || release.codec != matches[1] || encoding && encoding != matches[2]) return;
  2124. } else {
  2125. let torrentSize = getSizeFromString(msg, 'B');
  2126. if (!(torrentSize > 0 && albumSize > 0) || Math.abs(albumSize / torrentSize - 1) > 0.1) return;
  2127. }
  2128. addMessage('possibly same release previously deleted: ' + msg, 'notice');
  2129. });
  2130. });
  2131. }
  2132.  
  2133. let id = parseInt(new URLSearchParams(document.location.search).get('groupid'));
  2134. if (id > 0) localFetch('/torrents.php?action=grouplog&groupid=' + id).then(function(dom) {
  2135. dom.querySelectorAll('table > tbody > tr.rowa').forEach(function(tr) {
  2136. if (/^(?:deleted)\b/i.test(tr.lastElementChild.textContent.trim())) {
  2137. if ((id = parseInt(tr.children[1].firstElementChild.textContent)) > 0) searchLog('Torrent ' + id);
  2138. }
  2139. });
  2140. }); else {
  2141. let searchTerm = release.album;
  2142. if (!isVA && artists[0].length > 0 && artists[0].length < 3)
  2143. searchTerm = artists[0].join(' & ') + ' - ' + searchTerm;
  2144. searchLog(searchTerm);
  2145. }
  2146. }
  2147.  
  2148. function lookupMusicRelations() {
  2149. queryAjaxAPI('artist', { artistname: artists[0][0] }).then(function(artistGroup) {
  2150. // Find existing torrents
  2151. function searchTorrents(matchReleaseType = true) {
  2152. var torrents = [];
  2153. artistGroup.torrentgroup.filter(function(torrentGroup) {
  2154. if (matchReleaseType && releaseType && torrentGroup.releaseType != releaseType) return false;
  2155. if (release.album_year > 0 && torrentGroup.groupYear != release.album_year) return false;
  2156. return titlesMatch(decodeHTML(torrentGroup.groupName), 5, 0.8);
  2157. }).forEach(torrentGroup => { Array.prototype.push.apply(torrents, torrentGroup.torrent.filter(function(torrent) {
  2158. if (torrents.some(_torrent => _torrent.id == torrent.id)) return;
  2159. if (torrent.trumpable && (isUpload || isRequestNew && prefs.always_request_perfect_flac)) return false;
  2160. if (!isRequestNew || !prefs.always_request_perfect_flac ? media && torrent.media != media
  2161. : !['WEB', 'CD', 'Blu-Ray', 'SACD', 'DVD'].includes(torrent.media)) return false;
  2162. if (releaseYear > 0 && torrent.remasterYear != releaseYear) return false;
  2163. //if (editionTitle && torrent.remasterTitle.toLowerCase() != editionTitle.toLowerCase()) return false;
  2164. //if (release.label && torrent.remasterRecordLabel.toLowerCase() != release.label.toLowerCase()) return false;
  2165. if (!isRequestNew || !prefs.always_request_perfect_flac ? release.codec && torrent.format != release.codec
  2166. : torrent.format != 'FLAC') return false;
  2167. if (!isRequestNew || !prefs.always_request_perfect_flac ? encoding && torrent.encoding != encoding
  2168. : !['Lossless', '24bit Lossless'].includes(torrent.encoding)) return false;
  2169. if (isRequestNew && prefs.always_request_perfect_flac && torrent.media == 'CD'
  2170. && (!torrent.hasLog || torrent.logScore < 100 || !torrent.hasCue)) return false;
  2171. torrent.torrentGroup = torrentGroup;
  2172. return true;
  2173. })) });
  2174. return torrents;
  2175. }
  2176. let torrents = searchTorrents(true);
  2177. if (torrents.length > 0) torrents.forEach(function(torrent) {
  2178. if (reportedTorrentCollicions.has(torrent.id)) return;
  2179. if (isUpload)
  2180. reportedTorrentCollicions.set(torrent.id, addMessage(new HTML('possible dupe to torrent ' +
  2181. getTorrentRef(torrent) + ' ' + getFriendlyTime(torrent.time)), 'warning'));
  2182. else if (isRequestNew)
  2183. reportedTorrentCollicions.set(torrent.id, addMessage(new HTML('requested release possibly already on site: ' +
  2184. getTorrentRef(torrent) + ' ' + getFriendlyTime(torrent.time)), 'notice'));
  2185. }); else searchTorrents(false).forEach(function(torrent) {
  2186. if (reportedTorrentCollicions.has(torrent.id)) return;
  2187. reportedTorrentCollicions.set(torrent.id,
  2188. addMessage(new HTML('existing similar release in different category (' +
  2189. torrent.torrentGroup.releaseType + '): ' + getTorrentRef(torrent)), 'notice'));
  2190. });
  2191. // Find open requests
  2192. function searchRequests(matchReleaseType) {
  2193. return Promise.all(artistGroup.requests.filter(function(request) {
  2194. if (request.categoryId != 1) return false; // assertion
  2195. if (release.album_year && request.year != release.album_year) return false;
  2196. return titlesMatch(decodeHTML(request.title), 5, 0.8);
  2197. }).map(request => queryAjaxAPI('request', { id: request.requestId }).then(function(request) {
  2198. if (request.isFilled) return null;
  2199. if (request.categoryName != 'Music') return null; // assertion
  2200. if (matchReleaseType && releaseType && request.releaseType != releaseType) return null;
  2201. if (releaseYear > 0 && request.year != releaseYear) return null;
  2202. // if (editionTitle && torrent.remasterTitle.toLowerCase() != editionTitle.toLowerCase()) return false;
  2203. // if (release.label && torrent.remasterRecordLabel.toLowerCase() != release.label.toLowerCase()) return false;
  2204. if (Array.isArray(request.mediaList) && !request.mediaList.includes('Any')
  2205. && (isRequestNew && prefs.always_request_perfect_flac ?
  2206. !['WEB', 'CD', 'Blu-Ray', 'DVD', 'SACD'].some(media => request.mediaList.includes(media))
  2207. : media && !request.mediaList.includes(media))) return null;
  2208. if (Array.isArray(request.formatList) && !request.formatList.includes('Any')
  2209. && (isRequestNew && prefs.always_request_perfect_flac ?
  2210. !['FLAC'].some(format => request.formatList.includes(format))
  2211. : release.codec && !request.formatList.includes(release.codec))) return null;
  2212. if (Array.isArray(request.bitrateList) && !request.bitrateList.includes('Any')
  2213. && (isRequestNew && prefs.always_request_perfect_flac ?
  2214. !['Lossless', '24bit Lossless'].some(encoding => request.bitrateList.includes(encoding))
  2215. : encoding && !request.bitrateList.includes(encoding))) return null;
  2216. // if ((!isRequestNew || !prefs.always_request_perfect_flac) && media == 'CD'
  2217. // && !torrent.mediaList.includes('CD') && (!request.hasLog || request.logScore < 100 || !request.hasCue)) return null;
  2218. return request;
  2219. }))).then(requests => requests.filter(Boolean));
  2220. }
  2221. searchRequests(true).then(function(requests) {
  2222. if (requests.length > 0) requests.forEach(function(request) {
  2223. if (reportedRequests.has(request.requestId)) return;
  2224. if (isUpload) reportedRequests.set(request.requestId, addMessage(new HTML('open request ' +
  2225. getRequestRef(request) + ' ' + getRequestInfo(request) + ' possibly fillable by this release'), 'info'));
  2226. else if (isRequestNew) reportedRequests.set(request.requestId,
  2227. addMessage(new HTML('release possibly already requested: ' + getRequestRef(request)), 'info'));
  2228. }); else return searchRequests(false).then(requests => { requests.forEach(function(request) {
  2229. if (reportedRequests.has(request.requestId)) return;
  2230. if (isUpload) reportedRequests.set(request.requestId,
  2231. addMessage(new HTML('existing request ' + getRequestRef(request) + ' in different category'), 'info'));
  2232. else if (isRequestNew) reportedRequests.set(request.requestId,
  2233. addMessage(new HTML('release possibly already requested in different category: ' + getRequestRef(request)), 'info'));
  2234. }) });
  2235. }).catch(reason => { console.error('searchRequests:', reason) });
  2236. if (!relationsCheckTimer && prefs.relations_check_interval > 0)
  2237. relationsCheckTimer = setInterval(lookupMusicRelations, prefs.relations_check_interval * 1000);
  2238. });
  2239. }
  2240.  
  2241. function getHomoIdentifier(id, _tracks = tracks) {
  2242. if (typeof id != 'string') return undefined;
  2243. id = id.toUpperCase();
  2244. return _tracks.every((elem, ndx, arr) => elem.identifiers[id] != undefined
  2245. && elem.identifiers[id] === arr[0].identifiers[id]) ? _tracks[0].identifiers[id] : undefined;
  2246. }
  2247.  
  2248. function getReleaseTypeFromId(id) {
  2249. var result = 0;
  2250. if (/^(?:Album|LP)$/i.test(id)) result = getReleaseTypeValue('Album');
  2251. if (/^(?:Live(?:\sAlbum))$/i.test(id)) result = getReleaseTypeValue('Live album');
  2252. if (/^(?:(?:Maxi[\-\s]?)?Single|(?:7|10)")$/i.test(id)) result = getReleaseTypeValue('Single');
  2253. if (/^(?:EP|(?:12)")$/i.test(id)) result = getReleaseTypeValue('EP');
  2254. if (/\b(?:Soundtrack)\b/i.test(id)) result = getReleaseTypeValue('Soundtrack');
  2255. if (/^(?:Anthology)$/i.test(id)) result = getReleaseTypeValue('Anthology');
  2256. //if (/^(?:Compilation)$/i.test(id)) result = getReleaseTypeValue('Compilation');
  2257. if (/^(?:Remix)$/i.test(id)) result = getReleaseTypeValue('Remix');
  2258. if (/^(?:Bootleg)$/i.test(id)) result = getReleaseTypeValue('Bootleg');
  2259. if (/^(?:Mixtape)$/i.test(id)) result = getReleaseTypeValue('Mixtape');
  2260. if (/^(?:Demo)$/i.test(id)) result = getReleaseTypeValue('Demo');
  2261. if (/^(?:Concert\sRecording)$/i.test(id)) result = getReleaseTypeValue('Concert Recording');
  2262. if (/^(?:DJ\sMix)$/i.test(id)) result = getReleaseTypeValue('DJ Mix');
  2263. if (/^(?:Interview)$/i.test(id)) result = getReleaseTypeValue('Interview');
  2264. return result;
  2265. }
  2266.  
  2267. function getStoreUrl() {
  2268. return [
  2269. ['ACOUSTICSOUNDS_ID', 'https://store.acousticsounds.com/d/{ID}/'],
  2270. ['ALLMUSIC_ID', 'https://www.allmusic.com/album/{ID}'],
  2271. ['AMAZON_ID', 'https://www.amazon.com/gp/product/{ID}'],
  2272. ['AMID', 'https://www.allmusic.com/album/{ID}'],
  2273. ['APPLE_ID', 'https://music.apple.com/album/{ID}'],
  2274. ['ASIN', 'https://www.amazon.com/gp/product/{ID}'],
  2275. ['BEATPORT_ID', 'https://www.beatport.com/release/2/{ID}'],
  2276. ['BLEEP_ID', 'https://bleep.com/release/{ID}'],
  2277. ['BOOMKAT_ID', 'https://boomkat.com/products/{ID}'],
  2278. ['DEEZER_ID', deezerAlbumPrefix + '{ID}'],
  2279. ['DISCOGS_ID', discogsOrigin + '/release/{ID}'],
  2280. ['ECM_ID', 'https://www.ecmrecords.com/catalogue/{ID}'],
  2281. ['EONKYO_ID', 'https://www.e-onkyo.com/music/album/{ID}/'],
  2282. ['GOOGLE_ID', 'https://play.google.com/store/music/album/?id={ID}'],
  2283. ['HDTRACKS_ID', 'https://www.hdtracks.com/#/album/{ID}'],
  2284. ['INDIESSCOPE_ID', 'https://www.indies.eu/alba/{ID}/'],
  2285. ['ITUNES_ID', 'https://music.apple.com/album/{ID}'],
  2286. ['JUNODOWNLOAD_ID', 'https://www.junodownload.com/products/{ID}'],
  2287. ['MBID', mbrRlsPrefix + '{ID}'],
  2288. ['MUZIEKWEB_ID', 'https://www.muziekweb.nl/en/Link/{ID}/'],
  2289. ['PIAS_ID', 'https://store.pias.com/release/{ID}'],
  2290. ['PROSTUDIOMASTERS_ID', 'https://www.prostudiomasters.com/album/page/{ID}'],
  2291. ['QQMUSIC_ID', 'https://y.qq.com/n/yqq/album/{ID}.html'],
  2292. ['SPOTIFY_ID', 'https://open.spotify.com/album/{ID}'],
  2293. ['TRAXSOURCE_ID', 'https://www.traxsource.com/title/{ID}/'],
  2294. ['VGMDB_ID', 'https://vgmdb.net/album/{ID}'],
  2295. ['TIDAL_ID', 'https://listen.tidal.com/album/{ID}'],
  2296. ['OTOTOY_ID', 'https://ototoy.jp/_/default/p/{ID}'],
  2297. ['YANDEX_ID', 'https://music.yandex.ru/album/{ID}'],
  2298. ].reduce((u, def) => u || ((u = getHomoIdentifier(def[0])) ? def[1].replace('{ID}', u) : undefined), undefined);
  2299. }
  2300.  
  2301. function getCoverOnline() {
  2302. try { var url = new URL(sourceUrl || release.urls[0]), apiFirst } catch(e) { }
  2303. if ((i = getHomoIdentifier('APPLE_ID') || getHomoIdentifier('ITUNES_ID'))
  2304. || itunesRlsParser.test(url) && (i = parseInt(RegExp.$1)))
  2305. apiFirst = queryItunesAPI('lookup', { id: i })
  2306. .then(lookup => lookup.resultCount > 0 ? setItunesImage(lookup.results[0]) : Promise.reject('no cover'));
  2307. else if (i = getHomoIdentifier('DEEZER_ID') || dzrRlsParser.test(url) && (i = parseInt(RegExp.$1)))
  2308. apiFirst = queryDeezerAPI('album', i)
  2309. .then(result => result.id ? setDeezerImage(result) : Promise.reject('No cover'));
  2310. else if ((i = getHomoIdentifier('DISCOGS_ID')) || dcRlsParser.test(url) && (i = parseInt(RegExp.$1)))
  2311. apiFirst = queryDiscogsAPI('releases/' + i).then(function(release) {
  2312. (release.master_id ? queryDiscogsAPI('masters/' + release.master_id).then(master => master.images || [])
  2313. : Promise.resolve([])).then(function(masterImages) {
  2314. var result = masterImages.concat(release.images || [])
  2315. .filter(image => urlParser.test(image.resource_url || image.uri)
  2316. && ['primary', 'front'].includes(image.type));
  2317. result = result.length > 0 && (result[0].resource_url || result[0].uri) || undefined;
  2318. return result ? setCover(/^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/(\S+?\.\w+)\b/i.test(result) ?
  2319. 'https://www.discogs.com/image/' + RegExp.$1 : result) : Promise.reject('No cover');
  2320. });
  2321. });
  2322. else if ((i = getHomoIdentifier('MBID') || mbrRlsParser.test(url) && (i = RegExp.$1)))
  2323. apiFirst = getMusicBrainzCovers(i).then(function(covers) {
  2324. return covers != null ? setCover(covers[1][0]) : Promise.reject('No cover');
  2325. });
  2326. else if (i = getHomoIdentifier('TIDAL_ID') || tidalRlsParser(url) && (i = parseInt(RegExp.$1)) > 0)
  2327. apiFirst = queryTidalAPI('pages/album', { albumId: i }).then(function(album) {
  2328. for (var row of album.rows) {
  2329. var albumHeader = row.modules.find(module => module.type == 'ALBUM_HEADER');
  2330. if (albumHeader != undefined && albumHeader.album.cover)
  2331. return 'https://resources.tidal.com/images/' + albumHeader.album.cover.replace(/-/g, '/') + '/1280x1280.jpg';
  2332. }
  2333. return Promise.reject('Image not found');
  2334. });
  2335. else if (url && url.hostname.endsWith('mora.jp'))
  2336. apiFirst = loadMoraMetadata(url).then(function(packageMeta) {
  2337. return setCover(packageMeta.packageUrl + packageMeta.fullsizeimage);
  2338. });
  2339. else if (url && url.hostname.endsWith('hdtracks.com'))
  2340. apiFirst = loadHDtracksMetadata(url).then(album => setCover(album.cover));
  2341. else if ((i = parseInt(getHomoIdentifier('BEATSOURCE_ID'))) || url && url.hostname.endsWith('beatsource.com')
  2342. && /\/releases?\/(?:.+\/)?(\d+)(?=\/|$)/i.test(url.pathname) && (i = parseInt(RegExp.$1)))
  2343. apiFirst = queryBeatsourceAPI('releases/' + i).then(release => setCover(release.image.uri));
  2344. else apiFirst = Promise.reject('No known API binding');
  2345. return apiFirst.catch(reason => url ? imageUrlResolver(url).then(setCover) : Promise.reject('No source URLs'));
  2346. }
  2347.  
  2348. function searchCoverOnline() {
  2349. function info(service, url, id) {
  2350. addMessage(new HTML('used cover image from ' + service + ' release id ' +
  2351. '<a href="'+ url + '" target="_blank" style="' + hyperlinkStyle + '">' + id + '</a>'), 'info');
  2352. }
  2353.  
  2354. const lookupProviders = {
  2355. 'deezer': () => dzLookup().then(album => setDeezerImage(album).then(function(imgUrl) {
  2356. info('Deezer', deezerAlbumPrefix + album.id, album.id);
  2357. return imgUrl;
  2358. })),
  2359. 'qobuz': () => qbLookup().then(function(album) {
  2360. const resMatch = /_\d+(?=\.\w+$)/;
  2361. return setCover(album.imgUrl.replace(resMatch, '_org'))
  2362. .catch(reason => setCover(album.imgUrl.replace(resMatch, '_max')))
  2363. .catch(reason => setCover(album.imgUrl.replace(resMatch, '_600')))
  2364. .catch(reason => setCover(album.imgUrl))
  2365. .then(function(imgUrl) {
  2366. info('Qobuz', album.href, album.id);
  2367. return imgUrl;
  2368. });
  2369. }),
  2370. 'itunes': () => amLookup().then(album => setItunesImage(album).then(function(imgUrl) {
  2371. info('Apple Music', album.collectionViewUrl, album.collectionId);
  2372. return imgUrl;
  2373. })),
  2374. 'tidal': () => tidalLookup().then(album => album.cover ?
  2375. setCover('https://resources.tidal.com/images/' + album.cover.replace(/-/g, '/') + '/1280x1280.jpg').then(function(imgUrl) {
  2376. info('Tidal', album.url, album.id);
  2377. return imgUrl;
  2378. }) : Promise.reject('no cover for this album')),
  2379. 'musicbrainz': () => mbLookupByBarcode().catch(mbLookupByASIN)
  2380. .catch(reason => mbLookup().then(release => [release])).catch(mbLookupByTOC)
  2381. .then(releases => Promise.all(releases.map(release => getMusicBrainzCovers(release.id))))
  2382. .then(function(releases) {
  2383. let release = releases.find(release => release != null);
  2384. return release != undefined ? setCover(release[1][0]).then(function(imgUrl) {
  2385. if (/\/release\/(\S+)(?=[\/\?\#]|$)/i.test(release[0])) info('Musicbrains', release[0], RegExp.$1);
  2386. return imgUrl;
  2387. }) : Promise.reject('no covers found');
  2388. }),
  2389. 'discogs': () => dcLookup().then(function(releases) {
  2390. if (!Array.isArray(releases) || releases.length <= 0 || !releases[0].cover_image)
  2391. return Promise.reject('no cover for this release')
  2392. if (/^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/(\S+?\.\w+)\b/i.test(releases[0].cover_image))
  2393. releases[0].cover_image = 'https://www.discogs.com/image/' + RegExp.$1;
  2394. return setCover(releases[0].cover_image).then(function(imgUrl) {
  2395. info('Discogs', 'https://www.discogs.com' + releases[0].uri, releases[0].id);
  2396. return imgUrl;
  2397. });
  2398. }),
  2399. 'beatsource': () => bsLookup().then(release => setCover(release.image.uri).then(function(imgUrl) {
  2400. info('Beatsource', `https://www.beatsource.com/release/${release.slug}/${release.id}`, release.id);
  2401. return imgUrl;
  2402. })),
  2403. 'lastfm': () => queryLastFmAPI('album.getinfo', {
  2404. artist: (isVA ? VA : release.artist),
  2405. album: release.album,
  2406. }).then(function(result) {
  2407. if (result.error) return Promise.reject(result.message);
  2408. let image = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
  2409. return acc || result.album.image.find(image => image.size === size && urlParser.test(image['#text']));
  2410. }, undefined);
  2411. if (!image) return Promise.reject('no cover for matched album');
  2412. image = image['#text'];
  2413. return setCover(image.replace(/\/\d+(?:x\d+|s)\//i, '/')).catch(reason => setCover(image)).then(function(imgUrl) {
  2414. info('Last.fm', result.album.url, result.album.id || result.album.mbid || '#N/A');
  2415. return imgUrl;
  2416. });
  2417. }),
  2418. 'google': function() {
  2419. let query = new URLSearchParams({
  2420. q: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2421. c: 'music',
  2422. });
  2423. return globalFetch('https://play.google.com/store/search?' + query).then(function(response) {
  2424. try {
  2425. let _objs = loadGoogleMetadata(response);
  2426. let _results = _objs.filter(function(obj) { try { return typeof obj[0][4] == 'boolean' } catch(e) { return false } });
  2427. if (_results.length == 1) _results = _results[0][0][1]; else throw 'results metadata not found';
  2428. } catch(e) { }
  2429. let results = response.document.querySelectorAll('div:first-of-type + div[jscontroller]:last-of-type');
  2430. if (results.length > 0) for (let ndx = 0; ndx < results.length; ++ndx) {
  2431. let items = [];
  2432. results[ndx].querySelectorAll(':scope > div').forEach(function(result) {
  2433. let img = result.querySelector('span > span > img');
  2434. img = img != null ? (img.src || img.dataset.src).replace(/=[a-z]\d+$/, '=w0') : null;
  2435. let album = result.querySelector('a > div[title]');
  2436. if (album == null) return;
  2437. let artist = album.parentNode.parentNode.parentNode.querySelector('a > div:not([title])')
  2438. artist = artist != null ? artist.textContent.trim() : null;
  2439. let url = album.parentNode.href;
  2440. let id = /\?id=(\w+)\b/i.test(album.parentNode.href) && RegExp.$1 || null;
  2441. album = album.textContent.trim();
  2442. items.push({ id: id, url: url, artist: artist, album: album, imgUrl: img });
  2443. });
  2444. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  2445. var f = items.filter(release => releasesMatch(release.artist, release.album, i));
  2446. if (f.length > 1) return Promise.reject('ambiguity');
  2447. if (f.length == 1) break;
  2448. }
  2449. if (i > maxFuzzyLevel) return Promise.reject('no matches');
  2450. if (prefs.diag_mode && i >= 2) console.debug('Google Play Music fuzzy match:', release, '≈', f[0]);
  2451. if (f[0].imgUrl) return setCover(f[0].imgUrl).then(function(imgUrl) {
  2452. info('Google Play Music', f[0].url, f[0].id);
  2453. return imgUrl;
  2454. });
  2455. }
  2456. });
  2457. return Promise.reject('no matches');
  2458. },
  2459. };
  2460. let lookupChain;
  2461. if (typeof prefs.cover_lookup_providers == 'string') {
  2462. if (prefs.cover_lookup_providers.toLowerCase() == 'all') lookupChain = Object.keys(lookupProviders);
  2463. else lookupChain = prefs.cover_lookup_providers.match(/\b(\w+)\b/g).map(s => s.toLowerCase());
  2464. }
  2465.  
  2466. function lookupProvider(index = 0) {
  2467. if (!(index < lookupChain.length)) return Promise.reject('Provider index out of bounds (' + index + ')');
  2468. return (function() {
  2469. return lookupChain[index] in lookupProviders ? lookupProviders[lookupChain[index]]()
  2470. : Promise.reject('unknown provider');
  2471. })().catch(function(reason) {
  2472. if (prefs.diag_mode) console.debug('Cover lookup failed for', lookupChain[index], ':', reason);
  2473. return ++index < lookupChain.length ? lookupProvider(index)
  2474. : Promise.reject('no online resource matched this release');
  2475. });
  2476. }
  2477.  
  2478. return Array.isArray(lookupChain) && lookupChain.length > 0 ? lookupProvider().catch(function(reason) {
  2479. addMessage('cover lookup failed (' + reason + ')', 'notice');
  2480. return Promise.reject(reason);
  2481. }) : Promise.reject('No valid cover provider selected');
  2482. }
  2483.  
  2484. function setItunesImage(album) {
  2485. return urlParser.test(album.artworkUrl100) ?
  2486. setCover(album.artworkUrl100.replace('100x100bb', '100000x100000-999'))
  2487. .catch(reason => setCover(album.artworkUrl100)) : Promise.reject('Apple Music image not valid URL');
  2488. }
  2489. function setDeezerImage(album) {
  2490. return urlParser.test(album.cover_xl) ?
  2491. setCover(album.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0'))
  2492. .catch(reason => setCover(album.cover_xl)) : Promise.reject('Deezer image not valid URL');
  2493. }
  2494.  
  2495. function completeFromOnlineSource(onlineTracks) {
  2496. fillMissingValue(document.getElementById('media'), 'media');
  2497. fillMissingValue(document.getElementById('year'), 'album_year');
  2498. ref = document.getElementById('remaster_year') || !isUpload && document.querySelector('input[name="year"]');
  2499. if (ref != null && !ref.disabled && (ref.value == '' || !isRED && ref.value == '---')) {
  2500. var value = getHomoValue('release_date');
  2501. if (value != null) ref.value = extractYear(value);
  2502. }
  2503. fillMissingValue(document.getElementById('remaster_record_label')
  2504. || document.querySelector('input[name="recordlabel"]'), 'label');
  2505. if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
  2506. || document.querySelector('input[name="cataloguenumber"]'))) {
  2507. let catNo = getHomoValue('catalog');
  2508. if (!catNo && onlineTracks.every(track => track.identifiers.BARCODE
  2509. && track.identifiers.BARCODE == onlineTracks[0].identifiers.BARCODE)) {
  2510. catNo = parseInt(onlineTracks[0].identifiers.BARCODE.toString().replace(/\s+/g, ''));
  2511. }
  2512. if (catNo) ref.value = catNo;
  2513. }
  2514. return onlineTracks;
  2515.  
  2516. function getHomoValue(propName) {
  2517. return onlineTracks[0][propName] && onlineTracks.map(track => track[propName]).homogeneous() ?
  2518. onlineTracks[0][propName] : null;
  2519. }
  2520. function fillMissingValue(node, propName) {
  2521. if (!node || node.disabled || node.value != '' && (isRED || node.value != '---')) return;
  2522. var value = getHomoValue(propName);
  2523. if (value != null) node.value = value;
  2524. }
  2525. }
  2526.  
  2527. function onlineCheck(onlineTracks) {
  2528. if (!Array.isArray(onlineTracks) || onlineTracks.length <= 0) {
  2529. addMessage('online check not performed (empty tracklist)', 'notice');
  2530. return Promise.reject('No tracks');
  2531. }
  2532. if (prefs.diag_mode) console.debug('Checking against online tracks:', onlineTracks);
  2533. var issueCounter = 0, hiresTimes = onlineTracks.some(function(track) {
  2534. var remainder = Math.floor((track.duration - Math.floor(track.duration)) * 1000) / 100;
  2535. return remainder > Math.floor(remainder);
  2536. });
  2537. const arrayCompare = prefs.strict_online_check ? Array.prototype.equalTo : Array.prototype.equalCaselessTo;
  2538. onlineTracks.forEach(processTrackArtists);
  2539. if (onlineTracks[0].artist && onlineTracks.map(track => track.artist).homogeneous()
  2540. && (isVA ? !vaParser.test(onlineTracks[0].artist) : mainArtistMismatch())) {
  2541. ++issueCounter;
  2542. addMessage(new HTML('online album main artist mismatch ("' +
  2543. safeText(release.artist).bold() + '" ≠ "' + safeText(onlineTracks[0].artist).bold() + '")'), 'warning');
  2544. }
  2545. if (onlineTracks[0].album && onlineTracks.map(track => track.album).homogeneous()
  2546. && mismatch(release.album, onlineTracks[0].album) && mismatch(album, onlineTracks[0].album)
  2547. && mismatch(release.album, removeFeatArtists(onlineTracks[0].album))) {
  2548. ++issueCounter;
  2549. addMessage(new HTML('online album title mismatch ("' +
  2550. safeText(release.album).bold() + '" ≠ "' + safeText(onlineTracks[0].album).bold() + '")'), 'warning');
  2551. }
  2552. if (onlineTracks[0].label && onlineTracks.map(track => track.label).homogeneous()
  2553. && mismatch(release.label, onlineTracks[0].label, /-|\s+(?:Records|Recordings)$/ig)) {
  2554. ++issueCounter;
  2555. addMessage(new HTML('online album label mismatch ("' +
  2556. safeText(release.label).bold() + '" ≠ "' + safeText(onlineTracks[0].label).bold() + '")'), 'notice');
  2557. }
  2558. if (release.catalogs.length == 1
  2559. && onlineTracks[0].catalog && onlineTracks.map(track => track.catalog).homogeneous()
  2560. && mismatch(release.catalogs[0], onlineTracks[0].catalog, /[\s\-]/g)) {
  2561. ++issueCounter;
  2562. addMessage(new HTML('online album catalogue# mismatch ("' +
  2563. safeText(release.catalogs[0]).bold() + '" ≠ "' + safeText(onlineTracks[0].catalog).bold() + '")'), 'notice');
  2564. }
  2565. if (onlineTracks[0].album_year && onlineTracks.map(track => track.album_year).homogeneous()
  2566. && release.album_year != onlineTracks[0].album_year) {
  2567. ++issueCounter;
  2568. addMessage(new HTML('online album year mismatch (' +
  2569. (release.album_year || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].album_year.toString().bold() + ')'), 'warning');
  2570. }
  2571. if (onlineTracks[0].release_date && !isNaN(releaseDate) && onlineTracks.map(track => track.release_date).homogeneous()
  2572. && releaseDate.getDateValue() != new Date(onlineTracks[0].release_date.toString()).getDateValue()) {
  2573. ++issueCounter;
  2574. addMessage(new HTML('online album release date mismatch (' +
  2575. (release.release_date || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].release_date.toString().bold() + ')'), 'notice');
  2576. }
  2577. if (tracks.length != onlineTracks.length) {
  2578. ++issueCounter;
  2579. addMessage(new HTML('online album different tracklist length (' + tracks.length.toString().bold() +
  2580. ' ≠ ' + onlineTracks.length.toString().bold() + ')'), 'warning');
  2581. }
  2582. if (totalTime > 0) {
  2583. let ttOnline = onlineTracks.reduce((acc, track) => acc + (track.duration || NaN), 0);
  2584. if (ttOnline > 0 && Math.abs(totalTime - ttOnline) * 100 / ttOnline > (media == 'Vinyl' ?
  2585. prefs.vinyl_duration_divergency : hiresTimes ? 0.1 : prefs.duration_divergency)) {
  2586. ++issueCounter;
  2587. addMessage(new HTML('online album duration mismatch (' + makeTimeString(totalTime).bold() +
  2588. ' ≠ ' + makeTimeString(ttOnline).bold() + ')'), 'warning');
  2589. }
  2590. }
  2591. if (releaseType > 0) {
  2592. let rt = getHomoIdentifier('RELEASETYPE', onlineTracks) || getHomoIdentifier('RELEASE_TYPE', onlineTracks);
  2593. if (rt && (rt = getReleaseTypeFromId(rt)) > 0 && rt != releaseType)
  2594. addMessage(new HTML('online album release type mismatch (' +
  2595. safeText(stringifyReleaseType(releaseType) || releaseType).bold() + ' ≠ ' +
  2596. safeText(stringifyReleaseType(rt) || rt).bold() + ')'), 'warning');
  2597. }
  2598. for (let ndx = 0; ndx < tracks.length; ++ndx) {
  2599. if (ndx >= onlineTracks.length) {
  2600. addMessage('end of online tracklist reached, tracks from #' + (ndx + 1) + ' to end will not be checked', 'notice');
  2601. break;
  2602. }
  2603. if (mismatch(tracks[ndx].title, onlineTracks[ndx].title)
  2604. && mismatch(tracks[ndx].title, removeFeatArtists(onlineTracks[ndx].title))) {
  2605. ++issueCounter;
  2606. addMessage('online track #' + (ndx + 1) + ' title mismatch ("' +
  2607. (tracks[ndx].title || '') + '" ≠ "' + (onlineTracks[ndx].title || '') + '")', 'warning');
  2608. }
  2609. if (onlineTracks[ndx].track_artist && mismatch(tracks[ndx].track_artist, onlineTracks[ndx].track_artist)) {
  2610. let trackArtists = Array.isArray(tracks[ndx].track_artists) && tracks[ndx].track_artists.length > 0 ?
  2611. [tracks[ndx].track_artists, tracks[ndx].track_guests] : getArtists(tracks[ndx].track_artist);
  2612. let onlineSrackArtists = Array.isArray(onlineTracks[ndx].track_artists) && onlineTracks[ndx].track_artists.length > 0 ?
  2613. [onlineTracks[ndx].track_artists, onlineTracks[ndx].track_guests] : getArtists(onlineTracks[ndx].track_artist);
  2614. if (!artistsMatch(trackArtists, onlineSrackArtists)) {
  2615. ++issueCounter;
  2616. addMessage('online track #' + (ndx + 1) + ' track artist mismatch ("' +
  2617. (tracks[ndx].track_artist || '') + '" ≠ "' + (onlineTracks[ndx].track_artist || '') + '")', 'notice');
  2618. }
  2619. }
  2620. if (onlineTracks[ndx].track_number && tracks[ndx].track_number != onlineTracks[ndx].track_number) {
  2621. ++issueCounter;
  2622. addMessage('online track #' + (ndx + 1) + ' track number mismatch (' +
  2623. (tracks[ndx].track_number || '<unset>') + ' ≠ ' + onlineTracks[ndx].track_number + ')',
  2624. release.totalDiscs > 1 ? 'notice' : 'warning');
  2625. }
  2626. if (onlineTracks[ndx].disc_number && (onlineTracks[ndx].disc_number > 1 || tracks[ndx].disc_number)
  2627. && tracks[ndx].disc_number != onlineTracks[ndx].disc_number) {
  2628. ++issueCounter;
  2629. addMessage('online track #' + (ndx + 1) + ' disc number mismatch (' +
  2630. (tracks[ndx].disc_number || '<unset>') + ' ≠ ' + onlineTracks[ndx].disc_number + ')', 'warning');
  2631. }
  2632. if (onlineTracks[ndx].disc_subtitle && mismatch(tracks[ndx].disc_subtitle, onlineTracks[ndx].disc_subtitle)) {
  2633. ++issueCounter;
  2634. addMessage('online track #' + (ndx + 1) + ' disc subtitle mismatch ("' +
  2635. (tracks[ndx].disc_subtitle || '') + '" ≠ "' + onlineTracks[ndx].disc_subtitle + '")', 'notice');
  2636. }
  2637. let timeDif = tracks[ndx].duration && onlineTracks[ndx].duration
  2638. && Math.abs(tracks[ndx].duration - onlineTracks[ndx].duration);
  2639. if (timeDif >= (media != 'Vinyl' ? 2.5 : hiresTimes ? 0.1 : 5)) {
  2640. ++issueCounter;
  2641. addMessage('online track #' + (ndx + 1) + ' duration mismatch (' +
  2642. makeTimeString(tracks[ndx].duration) + ' ≠ ' + makeTimeString(onlineTracks[ndx].duration) + ')',
  2643. (timeDif >= (media != 'Vinyl' ? 5 : hiresTimes ? 0.2 : 8) ? 'warning' : 'notice'));
  2644. }
  2645. if (tracks[ndx].identifiers.MD5 && onlineTracks[ndx].identifiers.MD5
  2646. && tracks[ndx].identifiers.MD5 != onlineTracks[ndx].identifiers.MD5.toUpperCase())
  2647. addMessage('online track #' + (ndx + 1) + ' MD5 mismatch (' + tracks[ndx].identifiers.MD5 + ' ≠ ' +
  2648. onlineTracks[ndx].identifiers.MD5.toUpperCase() + ')', 'warning');
  2649. }
  2650. if (issueCounter == 0) {
  2651. i = 'online check completed without remarks';
  2652. if (prefs.messages_verbosity >= 1) addMessage(i, 'info'); else console.debug(i);
  2653. }
  2654.  
  2655. function mainArtistMismatch() {
  2656. return !artistsMatch([artists[0], albumGuests], Array.isArray(onlineTracks[0].artists)
  2657. && onlineTracks[0].artists.length > 0 ? [onlineTracks[0].artists, onlineTracks[0].featured_artists]
  2658. : getArtists(onlineTracks[0].artist));
  2659. }
  2660. function removeFeatArtists(title) {
  2661. return featArtistParsers.slice(1).reduce(function(acc, rx, ndx) {
  2662. return rx.test(acc) && (ndx < 5 || splitArtists(RegExp.$1).every((artist, ndx) => looksLikeTrueName(artist, 1))) ?
  2663. acc.replace(rx, '') : acc;
  2664. }, title || '')
  2665. }
  2666. function mismatch(localStr, onlineStr, rx) {
  2667. return normalize(localStr) != normalize(onlineStr);
  2668.  
  2669. function normalize(val) {
  2670. if (val == undefined || val == null) return '';
  2671. if (typeof val != 'string') val = val.toString();
  2672. if (rx instanceof RegExp || typeof rx == 'string') val = val.replace(rx, '');
  2673. val = val.replace(/[\(\)\-\s]+/g, '');
  2674. return prefs.strict_online_check ? val : val.toLowerCase();
  2675. }
  2676. }
  2677. }
  2678.  
  2679. function lookupOnlineSource() {
  2680. function info(service, url, id) {
  2681. if (prefs.check_integrity_online) addMessage(new HTML('checking online against ' + service +
  2682. ' release id <a href="' + url + '" target="_blank" style="' + hyperlinkStyle + '">' + id + '</a>'), 'info');
  2683. }
  2684. function mbEpilogue(releases) {
  2685. info('MusicBrainz', mbrRlsPrefix + releases[0].id, releases[0].id);
  2686. return mbrRlsPrefix + releases[0].id;
  2687. }
  2688.  
  2689. const commonMedia = !media || ['CD', 'WEB'].includes(media),
  2690. singleVolume = !release.totalDiscs || release.totalDiscs < 2;
  2691. let lookupProviders = [];
  2692. if (barCode) lookupProviders.push([
  2693. () => querySpotifyAPI('search', { q: 'barcode:' + barCode, type: 'album' })
  2694. .then(result => result.albums.total > 0 ? result.albums.items : Promise.reject('Spotify: no matches')),
  2695. function(albums) {
  2696. if (prefs.diag_mode) console.debug('Spotify lookup by barcode successfull:', barCode, 'matches:', albums);
  2697. info('Spotify', albums[0].external_urls.spotify, albums[0].id);
  2698. return albums[0].href;
  2699. }
  2700. ]);
  2701. if (commonMedia) lookupProviders.push([spotifyLookup, function(album) {
  2702. info('Spotify', album.external_urls.spotify, album.id);
  2703. return album.href;
  2704. }]);
  2705. if (barCode) lookupProviders.push([mbLookupByBarcode, mbEpilogue]);
  2706. if (getHomoIdentifier('ASIN')) lookupProviders.push([mbLookupByASIN, mbEpilogue]);
  2707. lookupProviders.push([mbLookup, function(release) {
  2708. info('MusicBrainz', mbrRlsPrefix + release.id, release.id);
  2709. return mbrRlsPrefix + release.id;
  2710. }]);
  2711. if (commonMedia) lookupProviders.push([amLookup, function(collection) {
  2712. info('Apple Music', collection.collectionViewUrl, collection.collectionId);
  2713. return collection.collectionViewUrl;
  2714. }]);
  2715. if (commonMedia && singleVolume) lookupProviders.push([dzLookup, function(album) {
  2716. info('Deezer', deezerAlbumPrefix + album.id, album.id);
  2717. return 'https://api.deezer.com/album/' + album.id;
  2718. }]);
  2719. if (commonMedia) lookupProviders.push([qbLookup, function(album) {
  2720. info('Qobuz', album.href, album.id);
  2721. return album.href;
  2722. }]);
  2723. lookupProviders.push([dcLookup, function(releases) {
  2724. info('Discogs', discogsOrigin + releases[0].uri, releases[0].id);
  2725. return releases[0].resource_url;
  2726. }]);
  2727. if (commonMedia) lookupProviders.push([tidalLookup, function(album) {
  2728. info('Tidal', album.url, album.id);
  2729. return album.url;
  2730. }]);
  2731. lookupProviders.push([suphonLookup, function(album) {
  2732. info('Supraphonline', album.url, album.id);
  2733. return album.url;
  2734. }]);
  2735. if (commonMedia && singleVolume) lookupProviders.push([bsLookup, function(release) {
  2736. info('Beatsource', `https://www.beatsource.com/release/${release.slug}/${release.id}`, release.id);
  2737. return release.url;
  2738. }]);
  2739. if (commonMedia && singleVolume) lookupProviders.push([tsLookup, function(album) {
  2740. info('TraxSource', album.url, album.id);
  2741. return album.url;
  2742. }]);
  2743. if (singleVolume) lookupProviders.push([mbLookupByTOC, mbEpilogue]);
  2744. if (commonMedia && singleVolume) lookupProviders.push([
  2745. () => queryLastFmAPI('album.getinfo', {
  2746. artist: (isVA ? VA : release.artist),
  2747. album: release.album,
  2748. }).then(result => result.error ? Promise.reject('Last.fm: ' + result.message) : result.album),
  2749. function(album) {
  2750. info('Last.fm', album.url, album.id || album.mbid || '#N/A');
  2751. return album; // return object
  2752. }
  2753. ]);
  2754.  
  2755. function lookupProvider(index = 0) {
  2756. if (!(index < lookupProviders.length)) return Promise.reject('no metadata provider for this release');
  2757. return lookupProviders[index][0]().then(lookupProviders[index][1]).catch(function(reason) {
  2758. return ++index < lookupProviders.length ? lookupProvider(index)
  2759. : Promise.reject('no online resource matched this release');
  2760. });
  2761. }
  2762.  
  2763. if (prefs.diag_mode) lookupProviders.forEach(function(lookupProvider, index) {
  2764. lookupProvider[0]().then(result => { console.debug('metaLookupProviders[', index, '] match:', result) },
  2765. reason => { console.debug('metaLookupProviders[', index, '] failed:', reason) });
  2766. });
  2767. return lookupProvider().catch(function(reason) {
  2768. addMessage('online check not performed (' + reason + ')', 'notice');
  2769. return Promise.reject('lookupOnlineSource: ' + reason);
  2770. });
  2771. }
  2772.  
  2773. function spotifyLookup() {
  2774. var searchTerm = 'album:"' + release.album.replace(tailingBracketStripper, '') + '"';
  2775. //searchTerm = 'artist:"' + (isVA ? VA : release.artist) + '" ' + searchTerm;
  2776. if (!isVA) searchTerm = 'artist:"' + release.artist + '" ' + searchTerm;
  2777. return querySpotifyAPI('search', {
  2778. q: searchTerm,
  2779. type: 'album',
  2780. limit: 50,
  2781. }).then(function(result) {
  2782. if (result.albums.total <= 0) return Promise.reject('Spotify: no matches');
  2783. if (prefs.diag_mode) console.debug('Spotify search results:', result.albums);
  2784. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  2785. var f = filter(i);
  2786. if (f.length > 1) return Promise.reject('Spotify: ambiguity');
  2787. if (f.length == 1) break;
  2788. }
  2789. if (i > maxFuzzyLevel) return Promise.reject('Spotify: no matches');
  2790. if (prefs.diag_mode && i >= 2) console.debug('Spotify fuzzy match:', release, '≈', f[0]);
  2791. return f[0];
  2792.  
  2793. function filter(level) {
  2794. return result.albums.items.filter(function(album) {
  2795. return (album.album_type == 'single' ? ['Single', 'EP'].some(rt => releaseType == getReleaseTypeValue(rt))
  2796. : releaseType != getReleaseTypeValue('Single'))
  2797. && releasesMatch(album.artists.map(artist => artist.name), album.name, level);
  2798. });
  2799. }
  2800. })
  2801. }
  2802.  
  2803. function dzLookup() {
  2804. var searchTerm = 'album:"' + release.album.replace(tailingBracketStripper, '') + '"';
  2805. //searchTerm = 'artist:"' + (isVA ? VA : release.artist) + '" ' + searchTerm;
  2806. if (!isVA) searchTerm = 'artist:"' + release.artist + '" ' + searchTerm;
  2807. return queryDeezerAPI('search', {
  2808. q: searchTerm,
  2809. //q: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
  2810. strict: 'on',
  2811. order: 'RANKING',
  2812. }).then(function(result) {
  2813. if (result.total <= 0) return Promise.reject('Deezer: no matches');
  2814. if (prefs.diag_mode) console.debug('Deezer search results:', result.data);
  2815. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  2816. var f = filter(i);
  2817. if (f.length > 1) return Promise.reject('Deezer: ambiguity');
  2818. if (f.length == 1) break;
  2819. }
  2820. if (i > maxFuzzyLevel) return Promise.reject('Deezer: no matches');
  2821. if (i >= 2) console.debug('Deezer fuzzy match:', release, '≈', f[0]);
  2822. return f[0];
  2823.  
  2824. function filter(level) {
  2825. var albums = [];
  2826. result.data.forEach(function(match) {
  2827. if (!releasesMatch(match.artist.name, match.album.title, level)) return;
  2828. if (!albums.some(album => album.id == match.album.id)) albums.push(match.album);
  2829. });
  2830. return albums;
  2831. }
  2832. });
  2833. }
  2834.  
  2835. function amLookup() {
  2836. var searchTerm = '"' + release.album.replace(tailingBracketStripper, '') + '"';
  2837. //searchTerm = '"' + (isVA ? VA : release.artist) + '" ' + searchTerm;
  2838. if (!isVA) searchTerm = '"' + release.artist + '" ' + searchTerm;
  2839. return queryItunesAPI('search', {
  2840. term: searchTerm,
  2841. media: 'music',
  2842. entity: 'album',
  2843. //country: 'US',
  2844. }).then(function(result) {
  2845. if (result.resultCount <= 0) return Promise.reject('Apple Music: no matches');
  2846. if (prefs.diag_mode) console.debug('Apple Music search results:', result.results);
  2847. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  2848. var f = filter(i);
  2849. if (f.length > 1) return Promise.reject('Apple Music: ambiguity');
  2850. if (f.length == 1) break;
  2851. }
  2852. if (i > maxFuzzyLevel) return Promise.reject('Apple Music: no matches');
  2853. if (prefs.diag_mode && i >= 2) console.debug('Apple Music fuzzy match:', release, '≈', f[0]);
  2854. return f[0];
  2855.  
  2856. function filter(level) {
  2857. var preFilter = result.results.filter(function(collection) {
  2858. var isSingle = collection.collectionName.endsWith(' - Single');
  2859. if (isSingle) collection.collectionName = collection.collectionName.slice(0, -9);
  2860. var isEP = collection.collectionName.endsWith(' - EP');
  2861. if (isEP) collection.collectionName = collection.collectionName.slice(0, -5);
  2862. isSingle = isSingle || collection.collectionType == 'Single';
  2863. isEP = !isSingle && (isEP || collection.collectionType == 'EP');
  2864. return (releaseType == getReleaseTypeValue('Single')) == isSingle
  2865. && (!isEP || releaseType == getReleaseTypeValue('EP'))
  2866. && releasesMatch(collection.artistName, collection.collectionName, level);
  2867. });
  2868. return preFilter.length > 1 && preFilter.some(collection => /\b(?:explicit)/i.test(collection.collectionExplicitness)) ?
  2869. preFilter.filter(collection => !/\b(?:clean)/i.test(collection.collectionExplicitness)) : preFilter;
  2870. }
  2871. });
  2872. }
  2873.  
  2874. function mbLookup() {
  2875. var queryParams = {
  2876. //'artist': isVA ? VA : release.artist,
  2877. 'release': release.album.replace(tailingBracketStripper, ''),
  2878. };
  2879. if (!isVA) queryParams.artist = release.artist;
  2880. return queryMusicBrainzAPI('release', {
  2881. query: Object.keys(queryParams).map(key => key + ':"' + queryParams[key] + '"').join(' AND '),
  2882. }).then(function(result) {
  2883. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2884. if (prefs.diag_mode) console.debug('MusicBrainz search results:', result.releases);
  2885. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  2886. var f = filter(i);
  2887. if (f.length > 1) return Promise.reject('MusicBrainz: ambiguity');
  2888. if (f.length == 1) break;
  2889. }
  2890. if (i > maxFuzzyLevel) return Promise.reject('MusicBrainz: no matches');
  2891. if (prefs.diag_mode && i >= 2) console.debug('MusicBrainz fuzzy match:', release, '≈', f[0]);
  2892. return f[0];
  2893.  
  2894. function filter(level) {
  2895. return result.releases.filter(function(release) {
  2896. return release.quality != 'low'
  2897. && (media ? [media] : tracks.some(notRedBook) ? ['WEB'] : ['CD', 'WEB'])
  2898. .some(_media => release.media.map(media => estimateMedia(media.format) || media.format).includes(_media)
  2899. && releasesMatch(release['artist-credit'].map(artist => artist.name), release.title, level));
  2900. });
  2901. }
  2902. });
  2903. }
  2904. function mbLookupByBarcode() {
  2905. if (!barCode) return Promise.reject('MusicBrainz: unknown barcode');
  2906. return queryMusicBrainzAPI('release', { query: 'barcode:' + barCode }).then(function(result) {
  2907. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2908. if (prefs.diag_mode) console.debug('MusicBrainz lookup by barcode successfull: ' + barCode + '; matches: ' + result.count);
  2909. return result.releases;
  2910. });
  2911. }
  2912. function mbLookupByASIN() {
  2913. var asin = getHomoIdentifier('ASIN');
  2914. if (!asin) return Promise.reject('MusicBrainz: unknown ASIN');
  2915. asin = asin.replace(/\s+/g, '');
  2916. return queryMusicBrainzAPI('release', { query: 'asin:' + asin }).then(function(result) {
  2917. if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
  2918. if (prefs.diag_mode) console.debug('MusicBrainz lookup by ASIN successfull: ' + asin + '; matches: ' + result.count);
  2919. return result.releases;
  2920. });
  2921. }
  2922. function mbComputeDiscID(mbTOC) {
  2923. if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4 || mbTOC[1] - mbTOC[0] > 98) return null;
  2924. let tocStr = ([mbTOC[0], mbTOC[1]].map(track => track.toString(16).padStart(2, '0'))
  2925. .concat(mbTOC.slice(2).map(offset => offset.toString(16).padStart(8, '0'))).join('') +
  2926. '0'.repeat(98 + mbTOC[0] - mbTOC[1] << 3)).toUpperCase();
  2927. return CryptoJS.SHA1(tocStr).toString(CryptoJS.enc.Base64)
  2928. .replace(/\=/g, '-').replace(/\+/g, '.').replace(/\//g, '_');
  2929. }
  2930. function mbLookupByDiscID(mbTOC) {
  2931. if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4)
  2932. return Promise.reject('mbLookupByDiscID(...): missing or invalid TOC');
  2933. let mbDscId = mbComputeDiscID(mbTOC);
  2934. let params = {
  2935. toc: mbTOC.join('+'),
  2936. inc: ['artists'].join('+'),
  2937. };
  2938. if (media != 'CD') params['media-format'] = 'all';
  2939. return queryMusicBrainzAPI('discid/' + (mbDscId || '-'), params).then(function(result) {
  2940. var matches = Array.isArray(result.releases) ? result.releases
  2941. : 'id' in result && 'title' in result ? [result] : null;
  2942. if (!Array.isArray(matches) || matches.length <= 0) return Promise.reject('MusicBrainz: no matches');
  2943. if (prefs.diag_mode) console.debug('MusicBrainz lookup by discId/TOC successfull:', mbDscId, '/', params, 'matches:', matches);
  2944. let minSimilarity = 0.90 - Math.min(tracks.length, 30) / 100;
  2945. let optedOut = matches.filter(match => titlesMatch(match.title, 3, minSimilarity));
  2946. return optedOut.length > 0 ? optedOut : matches;
  2947. });
  2948. }
  2949. function mbLookupByMetaTOC() {
  2950. if (release.totalDiscs > 1) return Promise.reject('TOC lookup not possible for multidisc release');
  2951. if (tracks.length < 3) return Promise.reject('TOC lookup given up for insufficient tracklist length');
  2952. let TOC;
  2953. if (TOC = getHomoIdentifier('ITUNES_TOC')) { // iTunes scheme
  2954. TOC = TOC.split('+').map(index => parseInt(index));
  2955. TOC = [1, TOC[2], TOC[1]].concat(TOC.slice(3));
  2956. } else if (TOC = getHomoIdentifier('CT_TOC')) { // CUETools scheme
  2957. TOC = TOC.split('+').map(index => parseInt(index, 16));
  2958. TOC = [1, TOC.shift(), TOC.pop()].concat(TOC);
  2959. }
  2960. return mbLookupByDiscID(TOC);
  2961. }
  2962. function mbLookupByAutoTOC() {
  2963. if (release.totalDiscs > 1) return Promise.reject('AutoTOC lookup not possible for multidisc release');
  2964. if (tracks.length < 3) return Promise.reject('AutoTOC lookup given up for insufficient tracklist length');
  2965. if (!tracks.every(track => track.samplerate > 0 && track.samples > 0))
  2966. return Promise.reject('MusicBrainz: insufficient information for TOC calculation');
  2967. let lastFrame = 0;
  2968. let TOC = [0].concat(tracks.map(track => (lastFrame += Math.round(track.samples * 75 / track.samplerate))))
  2969. .map(offset => 150 + offset);
  2970. TOC.unshift(TOC.pop());
  2971. return mbLookupByDiscID([1, tracks.length].concat(TOC));
  2972. }
  2973. function mbLookupByTOC() {
  2974. return mbLookupByMetaTOC().catch(reason => typeof reason == 'string' && !reason.includes('no matches') ?
  2975. mbLookupByAutoTOC() : Promise.reject(reason));
  2976. }
  2977.  
  2978. function dcLookup() {
  2979. let query = { type: 'release' };
  2980. if (barCode) query.barcode = barCode; else {
  2981. //query.artist = '"' + (isVA ? VA : release.artist) + '"';
  2982. if (!isVA) query.artist = '"' + release.artist + '"';
  2983. query.release_title = '"' + release.album.replace(tailingBracketStripper, '') + '"';
  2984. //if (release.catalogs.length > 0) query.catno = release.catalogs.join('; ');
  2985. }
  2986. return queryDiscogsAPI('database/search', query).then(function(result) {
  2987. if (result.results.length <= 0) return Promise.reject('Discogs: no matches');
  2988. if (prefs.diag_mode) console.debug('Discogs search results:', result.results);
  2989. if (barCode) {
  2990. //if (result.results.length > 1) return Promise.reject('Discogs: ambiguity');
  2991. if (prefs.diag_mode) console.debug('Discogs lookup by barcode successfull: ' +
  2992. barCode + '; matches: ' + result.results.length);
  2993. return result.results;
  2994. }
  2995. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  2996. var f = filter(i);
  2997. if (f.length > 1) return Promise.reject('Discogs: ambiguity');
  2998. if (f.length == 1) break;
  2999. }
  3000. if (i > maxFuzzyLevel) return Promise.reject('Discogs: no matches');
  3001. if (prefs.diag_mode && i >= 2) console.debug('Discogs fuzzy match:', release, '≈', f[0]);
  3002. return f;
  3003.  
  3004. function filter(level) {
  3005. return result.results.filter(function(album) {
  3006. if (media ? Array.isArray(album.format)
  3007. && !album.format.some(format => estimateMedia(format) == media)
  3008. : !album.format.some(format => ['CD', 'WEB'].includes(estimateMedia(format)))) return false;
  3009. if (/^(.+?)\s+\(\d+\)\s+-\s+(.+)$/.test(album.title) || /^(.+?)\s+-\s+(.+)$/.test(album.title))
  3010. return releasesMatch(RegExp.$1, RegExp.$2, level);
  3011. console.warn('Failed to parse Discogs title:', album.title);
  3012. return false;
  3013. });
  3014. }
  3015. });
  3016. }
  3017.  
  3018. function qbLookup(market) {
  3019. var searchTerm = release.album.replace(tailingBracketStripper, '');
  3020. //searchTerm = (isVA ? VA : release.artist) + ' ' + searchTerm;
  3021. if (!isVA) searchTerm = release.artist + ' ' + searchTerm;
  3022. var params = new URLSearchParams({
  3023. q: searchTerm,
  3024. //s: 'rdc', // descending sort by release date
  3025. i: 'boutique',
  3026. });
  3027. let url = new URL('https://www.qobuz.com');
  3028. if (market) url.pathname += market + '/';
  3029. url.pathname += 'search';
  3030. url.search = params;
  3031. return globalFetch(url).then(function(response) {
  3032. var results = response.document.querySelectorAll('div.search-results > div.product');
  3033. if (results.length <= 0) return Promise.reject('Qobuz: no matches');
  3034. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  3035. var f = filter(i);
  3036. if (f.length > 1) return Promise.reject('Qobuz: ambiguity');
  3037. if (f.length == 1) break;
  3038. }
  3039. if (i > maxFuzzyLevel) return Promise.reject('Qobuz: no matches');
  3040. if (prefs.diag_mode && i >= 2) console.debug('Qobuz fuzzy match:', release, '≈', f[0]);
  3041. return f[0];
  3042.  
  3043. function filter(level) {
  3044. var _results = [];
  3045. results.forEach(function(result) {
  3046. var _result = {};
  3047. _result.artist = result.querySelector('div.artist-name > a');
  3048. if (_result.artist != null) _result.artist = _result.artist.textContent.trim();
  3049. _result.title = result.querySelector('div.album-title > a');
  3050. if (_result.title != null) {
  3051. _result.id = _result.title.pathname.replace(/^.*\//, '');
  3052. _result.href = 'https://www.qobuz.com' + _result.title.pathname;
  3053. _result.title = _result.title.textContent.trim();
  3054. }
  3055. _result.imgUrl = result.querySelector('div.album-cover > a > img');
  3056. if (_result.imgUrl != null) _result.imgUrl = _result.imgUrl.dataset.src || _result.imgUrl.src;
  3057. if (_result.id && _result.artist && _result.title && _result.imgUrl
  3058. && !_results.some(album => album.id == _result.id)
  3059. && releasesMatch(_result.artist, _result.title, level)) _results.push(_result);
  3060. });
  3061. return _results;
  3062. }
  3063. });
  3064. }
  3065.  
  3066. function tidalLookup() {
  3067. var searchTerm = '"' + release.album.replace(tailingBracketStripper, '') + '"';
  3068. //searchTerm = '"' + (isVA ? VA : release.artist) + '" ' + searchTerm;
  3069. if (!isVA) searchTerm = '"' + release.artist + '" ' + searchTerm;
  3070. return tracks.length > 1 ? queryTidalAPI('search/albums', { query: searchTerm, limit: 25 }).then(function(result) {
  3071. if (result.totalNumberOfItems <= 0) return Promise.reject('Tidal: no matches');
  3072. if (prefs.diag_mode) console.debug('Tidal search results:', result.items);
  3073. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  3074. var f = filter(i);
  3075. if (f.length > 1) return Promise.reject('Tidal: ambiguity');
  3076. if (f.length == 1) break;
  3077. }
  3078. if (i > maxFuzzyLevel) return Promise.reject('Tidal: no matches');
  3079. if (prefs.diag_mode && i >= 2) console.debug('Tidal fuzzy match:', release, '≈', f[0]);
  3080. return f[0];
  3081.  
  3082. function filter(level) {
  3083. let preFilter = result.items.filter(function(item) {
  3084. return releasesMatch(item.artists.filter(artist => artist.type == 'MAIN').map(artist => artist.name),
  3085. item.title, level);
  3086. });
  3087. return preFilter.length > 1 && preFilter.some(item => item.explicit) ?
  3088. preFilter.filter(item => item.explicit) : preFilter;
  3089. }
  3090. }) : queryTidalAPI('search/tracks', { query: searchTerm, limit: 25 }).then(function(result) {
  3091. if (result.totalNumberOfItems <= 0) return Promise.reject('Tidal: no matches');
  3092. if (prefs.diag_mode) console.debug('Tidal search results:', result.items);
  3093. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  3094. var f = filter(i);
  3095. if (f.length > 1) return Promise.reject('Tidal: ambiguity');
  3096. if (f.length == 1) break;
  3097. }
  3098. if (i > maxFuzzyLevel) return Promise.reject('Tidal: no matches');
  3099. if (prefs.diag_mode && i >= 2) console.debug('Tidal fuzzy match:', release, '≈', f[0]);
  3100. return f[0];
  3101.  
  3102. function filter(level) {
  3103. let albums = [];
  3104. result.items.forEach(function(item) {
  3105. if (!releasesMatch(item.artists.filter(artist => artist.type == 'MAIN').map(artist => artist.name),
  3106. item.album.title, level) || albums.findIndex(album => album.id == item.album.id) >= 0) return;
  3107. item.album.explicit = item.explicit;
  3108. item.album.url = 'https://www.tidal.com/album/' + item.album.id;
  3109. albums.push(item.album);
  3110. });
  3111. return albums.some(album => album.explicit) ? albums.filter(album => album.explicit) : albums;
  3112. }
  3113. });
  3114. }
  3115.  
  3116. function tsLookup() {
  3117. let query = new URLSearchParams({ 'term': '"' + release.album.replace(tailingBracketStripper, '') + '"' });
  3118. return globalFetch('https://www.traxsource.com/search/titles?' + query).then(function(response) {
  3119. var results = Array.from(response.document.querySelectorAll('div.release-grid div.grid-page > div.grid-item'))
  3120. .map(function(div) {
  3121. var result = { id: parseInt(div.dataset.tid) }, elem = div.querySelector('div.ellip');
  3122. if (elem != null) result.artist = elem.childNodes[2].textContent.trim();
  3123. if ((elem = div.querySelector('div.ellip a.com-title')) != null) {
  3124. result.album = elem.textContent.trim();
  3125. result.url = 'https://www.traxsource.com' + elem.pathname;
  3126. }
  3127. if ((elem = div.querySelector('div.ellip a.com-label')) != null)
  3128. result.label = elem.textContent.trim();
  3129. if ((elem = div.querySelector('div.grid-image img')) != null)
  3130. result.cover = elem.src.replace(/\/scripts\/.+\/\d+x\d+\//i, '/files/images/');
  3131. return result;
  3132. });
  3133. if (results.length <= 0) return Promise.reject('TraxSource: no matches');
  3134. if (prefs.diag_mode) console.debug('TraxSource search results:', results);
  3135. const filter = level => results.filter(result => releasesMatch(result.artist, result.album, level));
  3136. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  3137. var f = filter(i);
  3138. if (f.length > 1) return Promise.reject('TraxSource: ambiguity');
  3139. if (f.length == 1) break;
  3140. }
  3141. if (i > maxFuzzyLevel) return Promise.reject('TraxSource: no matches');
  3142. if (prefs.diag_mode && i >= 2) console.debug('TraxSource fuzzy match:', release, '≈', f[0]);
  3143. return f[0];
  3144. });
  3145. }
  3146.  
  3147. function suphonLookup() {
  3148. var searchTerm = '"' + release.album.replace(tailingBracketStripper, '') + '"';
  3149. //searchTerm = '"' + (isVA ? VA : release.artist) + '" ' + searchTerm;
  3150. if (!isVA) searchTerm = '"' + release.artist + '" ' + searchTerm;
  3151. return globalFetch('https://www.supraphonline.cz/vyhledavani?q=' + encodeURIComponent(searchTerm)).then(function(response) {
  3152. var results = Array.from(response.document.querySelectorAll('div.albumlist > ul > li'))
  3153. .map(function(div) {
  3154. var result = { }, elem = div.querySelector('div.title a');
  3155. if (elem != null) {
  3156. if (/\/album\/(\d+)\b/i.test(elem.pathname)) result.id = parseInt(RegExp.$1);
  3157. result.album = elem.title || elem.textContent.trim();
  3158. result.url = 'https://www.supraphonline.cz' + elem.pathname;
  3159. }
  3160. if ((elem = div.querySelector('div.subtitle')) != null)
  3161. result.artist = elem.title || elem.textContent.trim();
  3162. if ((elem = div.querySelector('span.image img')) != null) result.cover = elem.src;
  3163. return result;
  3164. });
  3165. if (results.length <= 0) return Promise.reject('Supraphonline: no matches');
  3166. if (prefs.diag_mode) console.debug('Supraphonline search results:', results);
  3167. const filter = level => results.filter(result => releasesMatch(result.artist, result.album, level));
  3168. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  3169. var f = filter(i);
  3170. if (f.length > 1) return Promise.reject('Supraphonline: ambiguity');
  3171. if (f.length == 1) break;
  3172. }
  3173. if (i > maxFuzzyLevel) return Promise.reject('Supraphonline: no matches');
  3174. if (prefs.diag_mode && i >= 2) console.debug('Supraphonline fuzzy match:', release, '≈', f[0]);
  3175. return f[0];
  3176. });
  3177. }
  3178.  
  3179. function bsLookup() {
  3180. var searchTerm = '"' + release.album.replace(tailingBracketStripper, '') + '"';
  3181. //searchTerm = '"' + (isVA ? VA : release.artist) + '" ' + searchTerm;
  3182. if (!isVA) searchTerm = '"' + release.artist + '" ' + searchTerm;
  3183. return queryBeatsourceAPI('search', {
  3184. 'q': searchTerm,
  3185. 'type': 'releases',
  3186. 'per_page': 30,
  3187. //'order_by': '-publish_date',
  3188. }).then(function(result) {
  3189. if (!Array.isArray(result.releases) || result.releases.length <= 0)
  3190. return Promise.reject('Beatsource: no matches');
  3191. if (prefs.diag_mode) console.debug('Beatsource search results:', result.releases);
  3192. for (var i = 0; i <= maxFuzzyLevel; ++i) {
  3193. var f = filter(i);
  3194. if (f.length > 1) return Promise.reject('Beatsource: ambiguity');
  3195. if (f.length == 1) break;
  3196. }
  3197. if (i > maxFuzzyLevel) return Promise.reject('Beatsource: no matches');
  3198. if (prefs.diag_mode && i >= 2) console.debug('Beatsource fuzzy match:', release, '≈', f[0]);
  3199. return f[0];
  3200.  
  3201. function filter(level) {
  3202. var preFilter = result.releases
  3203. .filter(release => releasesMatch(release.artists.map(artist => artist.name), release.name, level));
  3204. return preFilter.length > 1 && preFilter.some(release => release.is_explicit) ?
  3205. preFilter.filter(release => release.is_explicit) : preFilter;
  3206. }
  3207. });
  3208. }
  3209. function ruleLink(rule) {
  3210. return ' (<a href="/rules.php?p=upload#r' + rule + '" target="_blank" style="' +
  3211. hyperlinkStyle + '">' + rule + '</a>)';
  3212. }
  3213.  
  3214. function releasesMatch(remoteArtist, remoteTitle, relaxLevel = 0, minSimilarity = 0.9, minFullSimilarity) {
  3215. if (typeof remoteArtist == 'string') {
  3216. if (isVA != vaParser.test(remoteArtist)) return false;
  3217. if (!isVA) remoteArtist = getArtists(remoteArtist)[0];
  3218. } else if (!Array.isArray(remoteArtist)) return false;
  3219. if (!isVA && !artists[0].equalCaselessTo(remoteArtist)
  3220. && !artists[0].map(name => name.toASCII()).equalCaselessTo(remoteArtist.map(name => name.toASCII())))
  3221. return false;
  3222. return titlesMatch(remoteTitle, relaxLevel, minSimilarity, minFullSimilarity);
  3223. }
  3224.  
  3225. function titlesMatch(remoteTitle, relaxLevel = 0, minSimilarity = undefined, minStrippedSimilarity = undefined) {
  3226. if (!remoteTitle) return false;
  3227. if (typeof remoteTitle == 'string') remoteTitle = remoteTitle.toLowerCase(); else return false;
  3228. let localTitles = [album.toLowerCase(), release.album.toLowerCase()];
  3229. if (localTitles[0] == remoteTitle || localTitles[1] == remoteTitle) return true;
  3230. if (relaxLevel <= 0) return false;
  3231. if (localTitles[0].toASCII() == remoteTitle.toASCII()
  3232. || localTitles[1].toASCII() == remoteTitle.toASCII()) return true;
  3233. if (relaxLevel <= 1) return false;
  3234. let strippedTitles = [release.album.toLowerCase(), remoteTitle]
  3235. .map(title => title.replace(tailingBracketStripper, ''));
  3236. if (strippedTitles[0] == strippedTitles[1]) return true;
  3237. if (relaxLevel <= 2) return false;
  3238. if (!(minSimilarity > 0)) minSimilarity = 0.90;
  3239. let similarity = jaroWrinkerSimilarity(localTitles[0], remoteTitle);
  3240. if (minSimilarity < 1 && similarity >= minSimilarity) {
  3241. if (prefs.diag_mode) console.debug('Fuzzy similarity accepted: "' +
  3242. localTitles[0] + '" ≈ "' + remoteTitle + '" (' + Math.round(similarity * 1000) / 1000 + ')');
  3243. return true;
  3244. }
  3245. if (relaxLevel <= 3) return false;
  3246. if (!(minStrippedSimilarity > 0)) minStrippedSimilarity = minSimilarity + 0.05;
  3247. if (minStrippedSimilarity < 1) {
  3248. similarity = jaroWrinkerSimilarity(localTitles[1], remoteTitle);
  3249. if (similarity >= minStrippedSimilarity) {
  3250. if (prefs.diag_mode) console.debug('Fuzzy similarity accepted: "' +
  3251. fullLocalTitle + '" ≈ "' + remoteTitle + '" (' + Math.round(similarity * 1000) / 1000 + ')');
  3252. return true;
  3253. }
  3254. similarity = jaroWrinkerSimilarity(strippedTitles[0], strippedTitles[1]);
  3255. if (similarity >= minStrippedSimilarity) {
  3256. if (prefs.diag_mode) console.debug('Fuzzy similarity accepted: "' +
  3257. strippedTitles[0] + '" ≈ "' + strippedTitles[1] + '" (' + Math.round(similarity * 1000) / 1000 + ')');
  3258. return true;
  3259. }
  3260. }
  3261. if (relaxLevel <= 4) return false;
  3262. if (localTitles[0].includes(remoteTitle) || remoteTitle.includes(localTitles[0])
  3263. || localTitles[1].includes(remoteTitle) || remoteTitle.includes(localTitles[1])) return true;
  3264. return false;
  3265. }
  3266.  
  3267. function trackComparer(a, b) {
  3268. var cmp;
  3269. if (release.totalDiscs > 1) {
  3270. cmp = a.disc_number - b.disc_number;
  3271. if (!isNaN(cmp) && cmp != 0) return cmp;
  3272. } else {
  3273. cmp = (a.disc_subtitle || '').localeCompare(b.disc_subtitle || '');
  3274. //if (cmp != 0) return cmp;
  3275. }
  3276. cmp = parseInt(a.track_number) - parseInt(b.track_number);
  3277. if (!isNaN(cmp)) return cmp;
  3278. var m1 = vinyltrackParser.exec(a.track_number.toUpperCase());
  3279. var m2 = vinyltrackParser.exec(b.track_number.toUpperCase());
  3280. return m1 != null && m2 != null ?
  3281. m1[1].localeCompare(m2[1]) || parseFloat(m1[2]) - parseFloat(m2[2]) :
  3282. a.track_number.toUpperCase().localeCompare(b.track_number.toUpperCase());
  3283. }
  3284.  
  3285. function reqSelectFormats(...vals) {
  3286. vals.forEach(function(val) {
  3287. ['MP3', 'FLAC', 'AAC', 'AC3', 'DTS'].forEach(function(fmt, ndx) {
  3288. if (val.toLowerCase() == fmt.toLowerCase() && (ref = document.getElementById('format_' + ndx)) != null) {
  3289. ref.checked = true;
  3290. ref.onchange();
  3291. }
  3292. });
  3293. });
  3294. }
  3295.  
  3296. function reqSelectBitrates(...vals) {
  3297. const bitrateSet = !isOPS ? [
  3298. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  3299. 'V0 (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  3300. ] : [
  3301. 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
  3302. 'V0 (VBR)', 'q8.x (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
  3303. ];
  3304. vals.forEach(function(val) {
  3305. var ndx = 10;
  3306. bitrateSet.forEach((it, _ndx) => { if (val.toString().toLowerCase() == it.toString().toLowerCase()) ndx = _ndx });
  3307. if ((ref = document.getElementById('bitrate_' + ndx)) != null) {
  3308. ref.checked = true;
  3309. ref.onchange();
  3310. }
  3311. });
  3312. }
  3313.  
  3314. function reqSelectMedias(...vals) {
  3315. const mediaSet = !isOPS ? ['CD', 'DVD', 'Vinyl', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB', 'Blu-Ray']
  3316. : ['CD', 'DVD', 'Vinyl', 'BD', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB'];
  3317. vals.forEach(function(val) {
  3318. mediaSet.forEach(function(med, ndx) {
  3319. if (val == med && (ref = document.getElementById('media_' + ndx)) != null) {
  3320. ref.checked = true;
  3321. ref.onchange();
  3322. }
  3323. });
  3324. if (val == 'CD') {
  3325. if ((ref = document.getElementById('needlog')) != null) {
  3326. ref.checked = true;
  3327. ref.onchange();
  3328. if ((ref = document.getElementById('minlogscore')) != null) ref.value = 100;
  3329. }
  3330. if ((ref = document.getElementById('needcue')) != null) ref.checked = true;
  3331. //if ((ref = document.getElementById('needchecksum')) != null) ref.checked = true;
  3332. }
  3333. });
  3334. }
  3335.  
  3336. function getReleaseTypeValue(str) {
  3337. if (!str || typeof str != 'string') return 0;
  3338. if (!Array.isArray(releaseTypes) || releaseTypes.length <= 0)
  3339. releaseTypes = Array.from(document.querySelectorAll('select#releasetype > option[value]'))
  3340. .map(option => [option.text.trim(), parseInt(option.value)]);
  3341. if (!Array.isArray(releaseTypes) || releaseTypes.length <= 0) releaseTypes = [
  3342. ['Album', 1],
  3343. ['Soundtrack', 3],
  3344. ['EP', 5],
  3345. ['Anthology', 6],
  3346. ['Compilation', 7],
  3347. ['Single', 9],
  3348. ['Live album', 11],
  3349. ['Remix', 13],
  3350. ['Bootleg', 14],
  3351. ['Interview', 15],
  3352. ['Mixtape', 16],
  3353. [isOPS ? 'DJ Mix' : 'Demo', 17],
  3354. ['Concert Recording', 18],
  3355. ['DJ Mix', 19],
  3356. ['Unknown', 21],
  3357. ];
  3358. let index = releaseTypes.findIndex(releaseType => str.toLowerCase() == releaseType[0].toLowerCase());
  3359. return index >= 0 ? releaseTypes[index][1] : 0;
  3360. }
  3361.  
  3362. function stringifyReleaseType(releaseType) {
  3363. if (!Array.isArray(releaseTypes) || releaseTypes.length <= 0 || !(releaseType > 0)) return null;
  3364. let index = releaseTypes.findIndex(_releaseType => releaseType == _releaseType[1]);
  3365. return index >= 0 ? releaseTypes[index][0] : null;
  3366. }
  3367.  
  3368. function getArtistTypeValue(str) {
  3369. if (!str || typeof str != 'string') return 0;
  3370. if (!Array.isArray(artistTypes) || artistTypes.length <= 0)
  3371. artistTypes = Array.from(document.querySelectorAll('select#importance > option[value]'))
  3372. .map(option => [option.text.trim(), parseInt(option.value)]);
  3373. if (!Array.isArray(artistTypes) || artistTypes.length <= 0) artistTypes = [
  3374. ['Main', 1],
  3375. ['Guest', 2],
  3376. ['Composer', 4],
  3377. ['Conductor', 5],
  3378. ['DJ / Compiler', 6],
  3379. ['Remixer', 3],
  3380. ['Producer', 7],
  3381. ];
  3382. let index = artistTypes.findIndex(artistType => str.toLowerCase() == artistType[0].toLowerCase());
  3383. return index >= 0 ? artistTypes[index][1] : 0;
  3384. }
  3385.  
  3386. function getChanString(n) {
  3387. if (!n) return null;
  3388. const chanmap = [
  3389. 'mono',
  3390. 'stereo',
  3391. '2.1',
  3392. '4.0 surround sound',
  3393. '5.0 surround sound',
  3394. '5.1 surround sound',
  3395. '7.0 surround sound',
  3396. '7.1 surround sound',
  3397. ];
  3398. return n >= 1 && n <= 8 ? chanmap[n - 1] : n + 'chn surround sound';
  3399. }
  3400.  
  3401. function fetchOnlineAdditions() {
  3402. if (onlineSource) return Promise.reject('Not offline source');
  3403. var url = sourceUrl || release.urls[0];
  3404. if (!urlParser.test(url)) return Promise.reject('No valid URL to parse');
  3405. if (url.toLowerCase().includes('highresaudio.com/'))
  3406. return globalFetch(url).then(response => hraPdfBooklet(response) || Promise.reject('No PDF booklet'));
  3407. else if (url.toLowerCase().includes('actmusic.com/')) return globalFetch(url.replace('actmusic.com/de', 'actmusic.com/en')).then(function(response) {
  3408. if ((ref = response.document.querySelector('div.sh3 > h1.header_title > a.btn-arrow-right')) == null)
  3409. return Promise.reject('Release full info not found');
  3410. return globalFetch('https://www.actmusic.com' + ref.pathname).then(actPdfBooklet);
  3411. }); else if (url.toLowerCase().includes('eclassical.com/'))
  3412. return globalFetch(url).then(response => eclassicalBooklets(response) || Promise.reject('No PDF booklet'));
  3413. return Promise.reject('No online source containing additions');
  3414. }
  3415.  
  3416. function processTrackArtists(track) {
  3417. if (!track.artist && Array.isArray(track.artists) && track.artists.length > 0) {
  3418. track.artist = joinArtists(track.artists);
  3419. if (Array.isArray(track.featured_artists) && track.featured_artists.length > 0)
  3420. track.artist += ' feat. ' + joinArtists(track.featured_artists);
  3421. }
  3422. if (!track.track_artist && Array.isArray(track.track_artists) && track.track_artists.length > 0) {
  3423. track.track_artist = joinArtists(track.track_artists);
  3424. if (Array.isArray(track.track_guests) && track.track_guests.length > 0)
  3425. track.track_artist += ' feat. ' + joinArtists(track.track_guests);
  3426. }
  3427. ['performer', 'remixer', 'composer', 'conductor', 'compiler', 'producer'].forEach(function(role) {
  3428. var arrPropName = role + 's';
  3429. if (!track[role] && Array.isArray(track[arrPropName]) && track[arrPropName].length > 0)
  3430. track[role] = track[arrPropName].join(role == 'composer' ? ', ' : '; ');
  3431. });
  3432. }
  3433. } // parseTracks
  3434.  
  3435. function estimateMedia(mediaStr) {
  3436. return typeof mediaStr == 'string' && [
  3437. [/\b(?:BR?D|BR)\b/, isOPS ? 'BD' : 'Blu-Ray'],
  3438. [/\b(?:Blu[\-\s]?Ray)\b/i, isOPS ? 'BD' : 'Blu-Ray'],
  3439. [/\b(?:SA-?CD)\b/, 'SACD'],
  3440. //[/\b(?:Hybrid)\b/i, 'SACD'],
  3441. [/\b(?:(?:HD[\-\s]?)?DVD(?:\-?A)?)\b/, 'DVD'],
  3442. [/\b(?:Vinyl)\b/i, 'Vinyl'],
  3443. [/\b(?:[LS]P\b|(?:5|6|7|8|9|10|12)")/, 'Vinyl'],
  3444. [/\b(?:(?:Micro)?Cassette)/i, 'Cassette'],
  3445. [/\b(?:WEB|File|Download|Digital\s+(?:Media|Distribution))\b|^Digital$/i, 'WEB'],
  3446. [/\b(?:AAC|AIFC|AIFF|ALAC|AMR|APE|DFF|DSD|FLAC|MP2|MP3|ogg-vorbis|Opus|SHN|WAV|WavPack|WMA|WMV)\b/i, 'WEB'],
  3447. //[/\b(?:DAT)\b/, 'DAT'],
  3448. [/\b(?:Soundboard)\b/i, 'Soundboard'],
  3449. [/\b(?:(?:HD[\-\s]?)?CD|CD[IiRr])\b/, 'CD'],
  3450. ].reduce((media, def) => media || def[0].test(mediaStr) && def[1], false) || undefined;
  3451. }
  3452.  
  3453. function hraPdfBooklet(response) {
  3454. var ref = response.document.querySelector('form#pdfjs-form-w2[action]');
  3455. if (ref == null) return undefined;
  3456. ref = new URLSearchParams(ref.action.replace(/^.*\?/, ''));
  3457. return `[url=${ref.get('file')}][img]https://ptpimg.me/ts0fy8.png[/img][/url]`;
  3458. }
  3459.  
  3460. function actPdfBooklet(response) {
  3461. var link;
  3462. response.document.querySelectorAll('ul.linklist > li > a').forEach(function(a) {
  3463. if (!a.pathname.endsWith('.pdf')) return;
  3464. if (!link || a.textContent.toLowerCase().includes('english')) link = a.pathname;
  3465. });
  3466. return link ? `[url=https://www.actmusic.com${link}][img]https://ptpimg.me/ts0fy8.png[/img][/url]` : undefined;
  3467. }
  3468.  
  3469. function eclassicalBooklets(response) {
  3470. var origin = new URL(response.finalUrl).origin;
  3471. var booklets = Array.from(response.document.querySelectorAll('div.articleAttachmentsContainer > ul > li > a'))
  3472. .filter(a => a.href.endsWith('.pdf')).map(a => origin + a.pathname + a.search);
  3473. return booklets.map(url => `[url=${url}][img]https://ptpimg.me/ts0fy8.png[/img][/url]`).join(' ') || undefined;
  3474. }
  3475.  
  3476. function fetchOnline_Music(url, weak = false) {
  3477. if (!urlParser.test(url)) return Promise.reject('Invalid URL');
  3478. if (!(url instanceof URL)) url = new URL(url);
  3479. const discParser = /^(?:CD|DIS[CK]\s+|VOLUME\s+|DISCO\s+|DISQUE\s+)(\d+)(?:\s+of\s+(\d+))?$/i;
  3480. let ref, artist, album, albumYear, releaseDate, channels, label, composer, bitdepth, samplerate = 44100,
  3481. description, compiler, producer, totalTracks, discSubtitle, discNumber, trackNumber, totalDiscs,
  3482. title, trackArtist, catalogue, encoding, format, bitrate, duration, country, media = 'WEB', imgUrl,
  3483. matches, genres = [], trs, tracks = [], identifiers = {}, trackIdentifiers = {};
  3484. if (url.hostname.endsWith('qobuz.com')) return globalFetch(url).then(function(response) {
  3485. const error = new Error('Failed to parse Qobus release page');
  3486. identifiers.QOBUZ_ID = response.finalUrl.replace(/^.*\//, '');
  3487. if ((ref = response.document.querySelector('section.album-item[data-gtm]')) != null) try {
  3488. let gtm = JSON.parse(ref.dataset.gtm);
  3489. if (gtm.shop.category) genres.push(gtm.shop.category);
  3490. if (gtm.shop.subCategory && !genres.includes(gtm.shop.subCategory)) genres.push(gtm.shop.subCategory.replace(/-/g, ' '));
  3491. } catch(e) { console.warn(e) }
  3492. if ((ref = response.document.querySelector('div.album-meta > h2.album-meta__artist')) != null)
  3493. artist = ref.title || ref.textContent.trim();
  3494. isVA = vaParser.test(artist);
  3495. album = (ref = response.document.querySelector('div.album-meta > h1.album-meta__title')) != null ?
  3496. ref.title || ref.textContent.trim() : undefined;
  3497. if ((ref = response.document.querySelector('div.album-meta > ul > li:first-of-type')) != null)
  3498. releaseDate = normalizeDate(ref.textContent, /\/([a-z]{2})-[a-z]{2}\//i.test(url.pathname) ? RegExp.$1 : 'fr');
  3499. var mainArtist = (ref = response.document.querySelector('div.album-meta > ul > li:nth-of-type(2) > a')) != null ?
  3500. ref.title || ref.textContent.trim() : undefined;
  3501. //ref = response.document.querySelector('p.album-about__copyright');
  3502. //if (ref != null) albumYear = extractYear(ref.textContent);
  3503. response.document.querySelectorAll('section#about > ul > li').forEach(function(it) {
  3504. function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) }
  3505. if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1);
  3506. if (/\b(\d+)\s*(?:track|pist[ae]|tracce|traccia)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1);
  3507. if (['Label', 'Etichetta', 'Sello'].some(l => it.textContent.trimLeft().startsWith(l))) {
  3508. label = it.firstElementChild.textContent.replace(/\s+/g, ' ').trim();
  3509. }
  3510. else if (['Composer', 'Compositeur', 'Komponist', 'Compositore', 'Compositor'].some(matchLabel)) {
  3511. composer = it.firstElementChild.textContent.trim();
  3512. if (pseudoArtistParsers.some(rx => rx.test(composer))) composer = undefined;
  3513. } else if (['Genre', 'Genere', 'Género'].some(g => it.textContent.startsWith(g)) && it.childElementCount > 0
  3514. && genres.length <= 0) {
  3515. genres = Array.from(it.querySelectorAll('a')).map(elem => elem.textContent.trim());
  3516. /*
  3517. if (genres.length >= 1 && ['Pop/Rock'].includes(genres[0])) genres.shift();
  3518. if (genres.length >= 2 && ['Alternative & Indie'].includes(genres[genres.length - 1])) genres.shift();
  3519. if (genres.length >= 1 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) {
  3520. while (genres.length > 1) genres.shift();
  3521. }
  3522. */
  3523. while (genres.length > 1) genres.shift();
  3524. }
  3525. });
  3526. response.document.querySelectorAll('span.album-quality__info').forEach(function(span) {
  3527. if (/\b(\d+(?:[\,\.]\d+)?)\s*(?:kHz)\b/i.test(span.textContent))
  3528. samplerate = Math.round(parseFloat(RegExp.$1.replace(',', '.')) * 1000);
  3529. if (/\b(\d+)[\-\s]*(?:Bits?)\b/i.test(span.textContent)) bitdepth = parseInt(RegExp.$1);
  3530. if (/\b(?:Stereo)\b/i.test(span.textContent)) channels = 2;
  3531. else if (/\b(\d)\.(\d)\b/.test(span.textContent)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  3532. });
  3533. getDescription(response, 'section#description > p', true);
  3534. if ((ref = response.document.querySelector('a[title="Qobuzissime"]')) != null) {
  3535. if (description) description += '\n';
  3536. description += '[align=center][url=https://www.qobuz.com' + ref.pathname +
  3537. '][img]https://ptpimg.me/4z35uj.png[/img][/url][/align]';
  3538. }
  3539. if ((ref = response.document.querySelector('div.album-cover > img')) != null)
  3540. imgUrl = ref.src.replace(/_\d{3}(?=\.\w+$)/, '_org');
  3541. addTracks(response.document);
  3542. if (totalTracks <= 50) return finalizeTracks();
  3543. var params = new URLSearchParams({
  3544. albumId: identifiers.QOBUZ_ID,
  3545. offset: 50,
  3546. limit: 999,
  3547. store: /\/(\w{2}-\w{2})\/album\//i.test(response.finalUrl) ? RegExp.$1 : 'fr-fr',
  3548. });
  3549. return globalFetch('https://www.qobuz.com/v4/ajax/album/load-tracks?' + params)
  3550. .then(response => { addTracks(response.document) }, function(reason) {
  3551. console.error('globalFetch() failed:', reason);
  3552. addMessage('failed to load all tracks for long album, only first 50 tracks were extracted from HTML, which will result in incmplete release description', 'notice');
  3553. }).then(() => finalizeTracks());
  3554.  
  3555. function addTracks(dom) {
  3556. Array.prototype.push.apply(tracks, Array.from(dom.querySelectorAll('div.player__item > div.player__tracks > div.track > div.track__items')).map(function(tr, index) {
  3557. trackArtist = false; trackIdentifiers = { TRACK_ID: tr.parentNode.dataset.track };
  3558. var trackArtists = [];
  3559. for (i = 0; i < qobuzArtistLabels.length + 1; ++i) trackArtists[i] = [];
  3560. if ((ref = tr.parentNode.querySelector('p.track__info[itemprop="byArtist"]')) != null) {
  3561. ref.textContent.trim().split(/\s+-\s+/).map(it => it.split(/\s*,\s*/)).forEach(function(it) {
  3562. if (it.length > 1) qobuzArtistLabels.forEach(function(artistLabels, index) {
  3563. if (artistLabels.some(role => it.slice(1).includes(role))) trackArtists[index].pushUniqueCaseless(it[0]);
  3564. }); else {
  3565. trackArtists[qobuzArtistLabels.length].pushUnique(it[0]);
  3566. if (prefs.diag_mode) console.debug('Qobuz uncategorized personnel:', it[0]);
  3567. }
  3568. });
  3569. //console.debug('Qobuz track', index + 1, trackArtists);
  3570. //trackArtists[0] = trackArtists[0].filter(artist => !trackArtists[4].includes(artist));
  3571. trackArtists[1] = trackArtists[1].filter(artist => ![0, 4].some(index => trackArtists[index].includes(artist)));
  3572. trackArtist = isVA || !artistsMatch(trackArtists, artist);
  3573. //console.debug('\tFiltered:', trackArtists[0], trackArtists[1]);
  3574. }
  3575. if (tr.parentNode.dataset.gtm) try {
  3576. let gtm = JSON.parse(tr.parentNode.dataset.gtm);
  3577. if (gtm.product.id) trackIdentifiers.QOBUZ_ID = gtm.product.id;
  3578. //if (gtm.product.type) trackIdentifiers.RELEASETYPE = gtm.product.type;
  3579. if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory];
  3580. } catch(e) { console.warn(e) }
  3581. if ((ref = tr.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) {
  3582. discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim();
  3583. guessDiscNumber();
  3584. }
  3585. return {
  3586. artist: isVA ? VA : artist,
  3587. album: album,
  3588. album_year: albumYear,
  3589. release_date: releaseDate,
  3590. label: label,
  3591. encoding: 'lossless',
  3592. codec: 'FLAC',
  3593. bitdepth: bitdepth || undefined,
  3594. samplerate: samplerate || undefined,
  3595. channels: channels || undefined,
  3596. media: media,
  3597. genre: genres.map(function(genre) {
  3598. genre = genre.replace(/-+/g, ' ');
  3599. qobuzTranslations.forEach(function(it) { if (genre.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) genre = it[1] });
  3600. return genre;
  3601. }).join('; '),
  3602. disc_number: discNumber || 1,
  3603. total_discs: totalDiscs,
  3604. disc_subtitle: discSubtitle,
  3605. track_number: parseInt(tr.querySelector('span[itemprop="position"]').textContent),
  3606. total_tracks: totalTracks,
  3607. title: (tr.querySelector('div.track__item--name > span') || tr.querySelector('span.track__item--name'))
  3608. .textContent.trim().replace(/\s+/g, ' '),
  3609. track_artists: trackArtist ? trackArtists[0] : undefined,
  3610. track_guests: trackArtist ? trackArtists[1] : undefined,
  3611. composer: trackArtists[3].length <= 0 ? composer : undefined,
  3612. composers: trackArtists[3],
  3613. performers: trackArtists[2],
  3614. conductors: trackArtists[4],
  3615. remixers: trackArtists[5],
  3616. producers: trackArtists[6],
  3617. duration: timeStringToTime(tr.querySelector('span.track__item--duration').textContent),
  3618. url: response.finalUrl,
  3619. description: description,
  3620. identifiers: mergeIds(),
  3621. cover_url: imgUrl,
  3622. };
  3623. }));
  3624. }
  3625. }); else if (url.hostname.endsWith('highresaudio.com') && url.pathname.includes('/album/view/')) return globalFetch(url).then(function(response) {
  3626. if (/\/album\/view\/(\w+)\//i.test(response.finalUrl)) identifiers.HRA_ID = RegExp.$1;
  3627. if (/\b(?:ClassHraJWP)\("hratrackplayer"\)\.init\((\[.+\])\);/m.test(response.responseText)) try {
  3628. var hraTrackPlayer = JSON.parse(RegExp.$1);
  3629. if (prefs.diag_mode) console.debug('hraTrackPlayer:', hraTrackPlayer);
  3630. } catch(e) { console.warn(e) }
  3631. if ((ref = response.document.querySelector('h1 > span.artist')) != null) artist = ref.textContent.trim();
  3632. album = (ref = response.document.getElementById('h1-album-title')) != null ? ref.firstChild.textContent.trim() : undefined;
  3633. response.document.querySelectorAll('div.album-col-info-data > div > p').forEach(function(p) {
  3634. var key = p.firstChild.textContent, value = p.lastChild.textContent.trim();
  3635. if (/^(?:Album[\s\-]Release)\b/i.test(key)) albumYear = extractYear(value);
  3636. else if (/^(?:HRA[\s\-]Release)\b/i.test(key)) releaseDate = normalizeDate(value, 'de');
  3637. else if (/^(?:Label)\b/i.test(key)) label = value;
  3638. else if (/^(?:Genre|Subgenre)\b/i.test(key)) genres.push(value);
  3639. else if (/^(?:Artist)\b/i.test(key)) {
  3640. /*artist = Array.from(p.getElementsByTagName('a')).map(a => a.textContent.trim());
  3641. if (artist.length > 0) isVA = artist.length == 1 && vaParser.test(artist[0]); else */artist = value;
  3642. } else if (/^(?:Composer)\b/i.test(key)) composer = value.split(/\s*,\s*/)
  3643. .map(composer => composer.replace(tailingBracketStripper, ''));
  3644. });
  3645. isVA = vaParser.test(artist);
  3646. samplerate = undefined;
  3647. response.document.querySelectorAll('tbody > tr > td.col-format').forEach(function(td) {
  3648. processFormat(/\b(FLAC)\s*(\d+(?:[\.\,]\d+)?)\b/, 24);
  3649. processFormat(/\b(DSD)\b/, 1);
  3650.  
  3651. function processFormat(rx, bd) {
  3652. if (!rx.test(td.textContent)) return;
  3653. if (format === undefined) format = RegExp.$1; else if (format != RegExp.$1) format = NaN;
  3654. var sr = parseFloat(RegExp.$2.replace(',', '.')) * 1000;
  3655. if (samplerate === undefined) samplerate = sr; else if (samplerate != sr) samplerate = NaN;
  3656. if (bitdepth === undefined) bitdepth = bd; else if (bitdepth != bd) bitdepth = NaN;
  3657. }
  3658. });
  3659. getDescription(response, 'div#albumtab-info > p', false);
  3660. if (i = hraPdfBooklet(response)) if (description) description += '\n\n' + i; else description = i;
  3661. url = (ref = response.document.querySelector('meta[property="og:url"][content]')) != null && ref.content;
  3662. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  3663. totalTracks = response.document.querySelectorAll('ul.playlist > li.pltrack').length;
  3664. response.document.querySelectorAll('ul.playlist > li').forEach(function(li, index) {
  3665. if (li.classList.contains('plinfo')) {
  3666. discSubtitle = li.textContent.trim().replace(/\s*:$/, '');
  3667. guessDiscNumber();
  3668. }
  3669. if (li.classList.contains('pltrack')) {
  3670. title = (ref = li.querySelector('span.title')) != null ?
  3671. ref.textContent.trim().replace(/\s+/g, ' ') : undefined;
  3672. if (title && discSubtitle && title.startsWith(discSubtitle))
  3673. title = title.slice(discSubtitle.lrngth).replace(/^\s*[\:\-\,\;]\s*/, '') || discSubtitle;
  3674. tracks.push({
  3675. artist: isVA ? VA : typeof artist == 'string' ? artist : undefined,
  3676. artists: !isVA && Array.isArray(artist) && artist.length > 0 ? artist : undefined,
  3677. album: album,
  3678. album_year: albumYear,
  3679. release_date: releaseDate,
  3680. label: label,
  3681. encoding: 'lossless',
  3682. codec: format || undefined,
  3683. bitdepth: bitdepth || undefined,
  3684. samplerate: samplerate || undefined,
  3685. media: media,
  3686. genre: genres.join('; '),
  3687. disc_number: discNumber,
  3688. disc_subtitle: discSubtitle || undefined,
  3689. total_discs: totalDiscs,
  3690. track_number: (ref = li.querySelector('span.track')) != null ?
  3691. parseInt(ref.textContent) || ref.textContent.trim() : undefined,
  3692. total_tracks: totalTracks,
  3693. title: title,
  3694. composers: Array.isArray(composer) && composer.length > 0 ? composer : undefined,
  3695. duration: (ref = li.querySelector('span.time')) != null && timeStringToTime(ref.textContent) || undefined,
  3696. url: url || response.finalUrl,
  3697. description: description,
  3698. cover_url: imgUrl,
  3699. identifiers: mergeIds(),
  3700. });
  3701. }
  3702. });
  3703. return tracks;
  3704. }); else if (url.hostname.endsWith('bandcamp.com')) return globalFetch(url).then(function(response) {
  3705. artist = Array.from(response.document.querySelectorAll('span[itemprop="byArtist"] > a')).map(a => a.textContent.trim());
  3706. ref = response.document.querySelector('span.back-link-text > br:last-of-type');
  3707. if (ref != null && ref.nextSibling != null) label = ref.nextSibling.textContent.trim(); else {
  3708. ref = response.document.querySelector('p#band-name-location > span.title');
  3709. if (ref != null) label = ref.textContent.trim();
  3710. }
  3711. var tags = new TagManager;
  3712. response.document.querySelectorAll('div.tralbum-tags > a.tag').forEach(function(tag) {
  3713. if (!artist.some(artist => tag.textContent.trim().toLowerCase() == artist.toLowerCase())) tags.add(tag.textContent.trim());
  3714. });
  3715. if ((ref = response.document.querySelector('div#tralbumArt > a.popupImage')) != null) ref = ref.href;
  3716. else if ((ref = response.document.querySelector('meta[property="og:image"]')) != null) ref = ref.conent;
  3717. if (ref) imgUrl = ref.replace(/_\d+(?=\.\w+$)/, '_0');
  3718. let playerData = (ref = response.document.querySelector('meta[property="og:video"][content]')) != null ?
  3719. globalFetch(ref.content, { responseType: 'text' }).then(function(response) {
  3720. if (!/^\s*(var\s+playerdata\s*=\s*(\{.+\});)\s*$/m.test(response.responseText))
  3721. return Promise.reject('External metadata not found');
  3722. try { return JSON.parse(RegExp.$2) } catch(e) { eval(RegExp.$1); return playerdata; }
  3723. }) : Promise.reject('External metadata missing');
  3724. if (prefs.diag_mode) playerData.then(playerdata => { console.debug('BandCamp playerdata loaded:', playerdata) })
  3725. .catch(reason => { console.warn('BandCamp playerdata load failed:', reason) });
  3726. try {
  3727. response.document.querySelectorAll('div#propOpenWrapper > div[id] > script').forEach(function(script) {
  3728. if (!/\b(var\s+SiteData\s*=\s*\{[\S\s]+\};)/.test(script.text)) return;
  3729. eval(RegExp.$1);
  3730. if (typeof TralbumData != 'object') return;
  3731. if (prefs.diag_mode) console.debug('BandCamp metadata loaded:', TralbumData);
  3732. identifiers.BANDCAMP_ID = TralbumData.id;
  3733. identifiers.RELEASETYPE = TralbumData.item_type;
  3734. identifiers.BARCODE = TralbumData.current.upc/* || TralbumData.packages[0].upc*/;
  3735. isVA = vaParser.test(TralbumData.artist);
  3736. description = TralbumData.current.about;
  3737. if (TralbumData.current.credits) if (description) description += '\n\n' + TralbumData.current.credits;
  3738. else description = TralbumData.current.credits;
  3739. tracks = TralbumData.trackinfo.map(function(track) {
  3740. trackIdentifiers = {
  3741. TRACK_ID: track.track_id,
  3742. //HASLYRICS: Number(track.has_lyrics) || 0,
  3743. };
  3744. return {
  3745. artist: isVA ? VA : TralbumData.artist,
  3746. album: TralbumData.current.title,
  3747. release_date: TralbumData.current.release_date || TralbumData.album_release_date,
  3748. description: description,
  3749. label: /*TralbumData.packages[0].label || */label,
  3750. //catalog: TralbumData.packages[0].sku,
  3751. genre: tags.toString(),
  3752. duration: track.duration || undefined,
  3753. lyrics: track.lyrics || undefined,
  3754. title: track.title,
  3755. track_number: track.track_num,
  3756. total_tracks: TralbumData.trackinfo.length,
  3757. media: 'WEB',
  3758. url: TralbumData.url ? TralbumData.url.replace(/^http\b/, 'https') : response.finalUrl,
  3759. cover_url: imgUrl,
  3760. identifiers: mergeIds(),
  3761. };
  3762. });
  3763. });
  3764. if (tracks.length <= 0) throw 'No tracks found';
  3765. return tracks;
  3766. } catch(e) {
  3767. console.warn('BandCamp: falling back to HTML scraper for the reason:', e);
  3768. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3769. if ((ref = response.document.querySelector('h2[itemprop="name"]')) != null) album = ref.textContent.trim();
  3770. ref = response.document.querySelector('div.tralbum-credits');
  3771. if (ref != null && /\brelease[ds]\s+(.*?\b\d{4})\b/i.test(ref.textContent)) releaseDate = RegExp.$1;
  3772. description = [];
  3773. response.document.querySelectorAll('div.tralbumData').forEach(function(div) {
  3774. if (!div.classList.contains('tralbum-tags')) description.push(html2php(div, response.finalUrl).trim());
  3775. });
  3776. description = description.filter(p => p).join('\n\n');
  3777. if (/\bShare\.initPanel\s*\(\s*\w+\s*,\s*\d+\s*,\s*"https?(:\/\/\S+?)"\s*\);/i.test(response.responseText))
  3778. var shareLink = 'https' + RegExp.$1;
  3779. trs = response.document.querySelectorAll('table.track_list > tbody > tr[itemprop="tracks"]');
  3780. return Array.from(trs).map(tr => ({
  3781. artist: isVA ? VA : undefined,
  3782. artists: !isVA ? artist : undefined,
  3783. album: album,
  3784. //album_year: extractYear(releaseDate),
  3785. release_date: releaseDate,
  3786. label: label,
  3787. media: media,
  3788. genre: tags.toString(),
  3789. disc_number: discNumber,
  3790. total_discs: totalDiscs,
  3791. track_number: parseInt(tr.querySelector('div.track_number').textContent) || tr.querySelector('div.track_number').textContent,
  3792. total_tracks: trs.length,
  3793. title: (tr.querySelector('div.title span.track-title')
  3794. || tr.querySelector('div.title span[itemprop="name"]')).textContent.trim().replace(/\s+/g, ' '),
  3795. duration: durationFromMeta(tr) || (ref = tr.querySelector('span.time')) != null && timeStringToTime(ref.textContent) || undefined,
  3796. url: shareLink || response.finalUrl,
  3797. description: description,
  3798. identifiers: mergeIds(),
  3799. cover_url: imgUrl,
  3800. }));
  3801. }
  3802. }); else if (url.hostname.endsWith('prestomusic.com')) return globalFetch(url).then(function(response) {
  3803. identifiers.COMPOSEREMPHASIS = 1;
  3804. if (/\/products\/(\d+)\b/i.test(url.pathname)) identifiers.PRESTOMUSIC_ID = parseInt(RegExp.$1);
  3805. var conductors = [], performers = [];
  3806. artist = [], composer = [];
  3807. response.document.querySelectorAll('div#related > div > ul > li').forEach(li => {[
  3808. ['Composers', composer],
  3809. ['Artists', artist],
  3810. ['Groups & Artists', artist],
  3811. ['Groups', artist],
  3812. ['Ensembles', artist],
  3813. ['Conductors', conductors],
  3814. ['Performers', performers],
  3815. ].forEach(function(def) {
  3816. try {
  3817. if (li.parentNode.previousElementSibling.textContent.trim() == def[0])
  3818. def[1].pushUniqueCaseless(li.textContent.trim()
  3819. .replace(tailingBracketStripper, '').replace(/^(.+?),\s+(.+)$/, '$2 $1'));
  3820. } catch(e) { console.error(e) }
  3821. }) });
  3822. // questionable
  3823. Array.prototype.pushUniqueCaseless.apply(artist,
  3824. getArtists(response.document.querySelectorAll('div.c-product-block__contributors > p')));
  3825. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  3826. if ((ref = response.document.querySelector('h1.c-product-block__title')) != null)
  3827. album = ref.lastChild.wholeText.trim();
  3828. response.document.querySelectorAll('div.c-product-block__metadata > ul > li').forEach(function(li) {
  3829. if (li.firstChild.textContent.includes('Release Date')) {
  3830. releaseDate = li.lastChild.wholeText;
  3831. if (/\b(\d+)\w*\s+(\w+)\s+(\d{4})\b/.test(releaseDate)) releaseDate = RegExp.$2 + ' ' + RegExp.$1 + ' ' + RegExp.$3;
  3832. } else if (li.firstChild.textContent.includes('Label'))
  3833. label = labelSubstitutes.reduce((l, def) => l.replace(...def), li.lastChild.wholeText.trim());
  3834. else if (li.firstChild.textContent.includes('Catalogue No')) catalogue = li.lastChild.wholeText.trim();
  3835. });
  3836. genres = undefined;
  3837. if (/\/jazz\//i.test(response.finalUrl)) genres = 'Jazz';
  3838. if (/\/classical\//i.test(response.finalUrl)) genres = 'Classical';
  3839. getDescription(response, 'div#about > div > p', true);
  3840. var personnel = [];
  3841. response.document.querySelectorAll('div.c-product-block__contributors > p').forEach(function(p) {
  3842. // TODO
  3843. });
  3844. let reviews = Array.from(response.document.querySelectorAll('div#reviews > div > div.c-product__product-review'))
  3845. .map(div => html2php(div, response.finalUrl).trim()).join('\n\n');
  3846. if (reviews) description += '\n\n[hide=Reviews]' + reviews + '[/hide]';
  3847. if (personnel.length > 0) {
  3848. if (description) description += '\n\n';
  3849. description += personnel.join('\n');
  3850. }
  3851. if ((ref = response.document.querySelector('div.c-product-block__aside > a')) != null)
  3852. imgUrl = ref.href.replace(/\?\d+$/, '');
  3853. trackNumber = 0;
  3854. response.document.querySelectorAll('div.c-tracklist div.c-tracklist__work').forEach(function(div) {
  3855. trs = div.querySelectorAll(':scope > div.c-track__details > ul > li');
  3856. trackArtist = getArtists(trs, false);
  3857. let workConductors = getArtists(trs, true);
  3858.  
  3859. function addTracks(selector) {
  3860. Array.prototype.push.apply(tracks, Array.from(div.querySelectorAll(selector)).map(node => ({
  3861. artist: isVA ? VA : undefined,
  3862. artists: !isVA ? artist : undefined,
  3863. album: album,
  3864. release_date: releaseDate,
  3865. label: label,
  3866. catalog: catalogue,
  3867. media: media,
  3868. genre: genres,
  3869. disc_number: discNumber,
  3870. disc_subtitle: discSubtitle,
  3871. track_number: ++trackNumber,
  3872. title: (ref = node.querySelector('p.c-track__title')) != null ?
  3873. ref.textContent.trim().replace(/\s+/g, ' ') : undefined,
  3874. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  3875. trackArtist : undefined,
  3876. composers: composer,
  3877. conductors: workConductors.length > 0 ? workConductors : undefined,
  3878. performers: performers,
  3879. duration: (ref = node.querySelector('div.c-track__duration')) != null ?
  3880. timeStringToTime(ref.textContent) : undefined,
  3881. description: description.collapseGaps(),
  3882. url: response.finalUrl,
  3883. cover_url: imgUrl,
  3884. identifiers: mergeIds(),
  3885. })));
  3886. }
  3887.  
  3888. if (/*!div.classList.contains('has--tracks')*/div.querySelector('div.c-tracklist__initial-tracks') == null) {
  3889. discNumber = discSubtitle = undefined;
  3890. addTracks('div.c-track');
  3891. } else {
  3892. if ((ref = div.querySelector('div.c-track p.c-track__title')) != null) {
  3893. discSubtitle = ref.textContent.trim().replace(/\s+/g, ' ');
  3894. guessDiscNumber();
  3895. } else {
  3896. discNumber = discSubtitle = undefined;
  3897. console.warn('Presto Music work title missing:', div);
  3898. }
  3899. addTracks('div.c-tracklist__initial-tracks > div.c-track, div.c-tracklist__remaining-tracks > div.c-track');
  3900. }
  3901. });
  3902. return finalizeTracks();
  3903.  
  3904. function getArtists(nodeList, _conductors = false) {
  3905. var artists = [];
  3906. nodeList.forEach(function(_artists) {
  3907. _artists = _artists.textContent.trim();
  3908. if (_artists.startsWith('Record')) return;
  3909. Array.prototype.push.apply(artists, splitAmpersands(_artists.replace(bracketStripper, '').replace(/;\s*/g, ''))
  3910. .filter(artist => artist.length > 0 && !/^[a-z]/.test(artist)));
  3911. });
  3912. return artists.filter(artist => artist.length > 0 && conductors.includesCaseless(artist) == _conductors);
  3913. }
  3914. }); else if (url.hostname.endsWith('discogs.com') && /\/(release|master|artist)s?\/(\d+)\b/i.test(url.pathname)) {
  3915. if (RegExp.$1 == 'artist') return Promise.reject('Discogs artists not parseable');
  3916. if (RegExp.$1 == 'master') return Promise.reject('Discogs masters as source aren\'t supported, pick a specific release');
  3917. return queryDiscogsAPI('releases/' + RegExp.$2).then(function(release) {
  3918. if (prefs.diag_mode) console.debug('Discogs release', release.id, 'metadata received:', release);
  3919. const removeArtistNdx = /\s*\(\d+\)$/;
  3920. const editionTests = [
  3921. /^(?:Remaster(?:ed)|Remasterizado|Remasterisée|Enhanced|Extended)\b/i,
  3922. /^(?:Reissue|Repress|Promo|(?:Partially\s)?Mixed|Numbered|Misprint|Mispress|\w+\sPressing|Advance|Single\s(?:Sided)|Etched|Card\sBacked)$/i,
  3923. /\b(?:Unofficial)\b/i,
  3924. /\b(?:Edition|Release)$/i,
  3925. ];
  3926. identifiers.DISCOGS_ID = release.id;
  3927. var master = release.master_id ? queryDiscogsAPI('masters/' + release.master_id).then(function(master) {
  3928. if (prefs.diag_mode) console.debug('Discogs master', master.id, 'metadata received:', master);
  3929. return master;
  3930. }) : Promise.resolve(null);
  3931. artist = getArtists(release);
  3932. isVA = artist[0].length == 1 && vaParser.test(artist[0][0])
  3933. album = release.title;
  3934. label = []; catalogue = [];
  3935. release.labels.forEach(function(lbl) {
  3936. if (lbl.entity_type != 1) return;
  3937. if (lbl.name) label.pushUniqueCaseless(lbl.name.replace(removeArtistNdx, ''));
  3938. if (lbl.catno && !/^(?:none)$/.test(lbl.catno)) catalogue.pushUniqueCaseless(lbl.catno);
  3939. });
  3940. description = '';
  3941. if (Array.isArray(release.companies) && release.companies.length > 0) {
  3942. description = '[b]Companies, etc.[/b]\n';
  3943. let type_names = new Set(release.companies.map(it => it.entity_type_name));
  3944. type_names.forEach(function(type_name) {
  3945. description += '\n' + type_name + ' – ' + release.companies
  3946. .filter(it => it.entity_type_name == type_name)
  3947. .map(function(it) {
  3948. var result = '[url=' + discogsOrigin + '/label/' + it.id + ']' +
  3949. it.name.replace(removeArtistNdx, '') + '[/url]';
  3950. if (it.catno) result += ' – ' + it.catno;
  3951. return result;
  3952. })
  3953. .join(', ');
  3954. });
  3955. }
  3956. if (Array.isArray(release.extraartists) && release.extraartists.length > 0) {
  3957. if (description) description += '\n\n';
  3958. description += '[b]Credits[/b]\n';
  3959. let roles = new Set(release.extraartists.map(it => it.role));
  3960. roles.forEach(function(role) {
  3961. description += '\n' + role + ' – ' + release.extraartists
  3962. .filter(artist => artist.role == role)
  3963. .map(function(artist) {
  3964. var result = '[url=' + discogsOrigin + '/artist/' + artist.id + ']' +
  3965. (artist.anv || artist.name).replace(removeArtistNdx, '') + '[/url]';
  3966. if (artist.tracks) result += ' (tracks: ' + artist.tracks + ')';
  3967. return result;
  3968. }).join(', ');
  3969. });
  3970. }
  3971. if ('notes' in release && release.notes.trim()) {
  3972. if (description) description += '\n\n';
  3973. description += '[b]Notes[/b]\n\n' + release.notes.trim();
  3974. }
  3975. if (Array.isArray(release.identifiers) && release.identifiers.length > 0) {
  3976. if (description) description += '\n\n';
  3977. description += '[b]Barcode and Other Identifiers[/b]\n';
  3978. release.identifiers.forEach(function(it) {
  3979. description += '\n' + it.type;
  3980. if (it.description) description += ' (' + it.description + ')';
  3981. description += ': ' + it.value;
  3982. });
  3983. }
  3984. [
  3985. ['Single', 'Single', 'Maxi-Single', 'Maxi'],
  3986. ['EP', 'EP'],
  3987. ['Album', 'Album', 'LP', 'MiniAlbum'],
  3988. //['Anthology', 'Compilation', 'Box Set'],
  3989. ['Compilation', 'Sampler'],
  3990. ['Mixtape', 'Mixtape'],
  3991. ].forEach(function(k) {
  3992. if (release.formats.every(format => format.name == 'All Media' || Array.isArray(format.descriptions)
  3993. && k.slice(1).some(k => format.descriptions.includesCaseless(k)))) identifiers.RELEASETYPE = k[0];
  3994. });
  3995. var channelModes = [];
  3996. [
  3997. ['mono', 'Mono'],
  3998. ['stereo', 'Stereo'],
  3999. ['Quadraphonic', '4.0'],
  4000. ].forEach(function(k) {
  4001. release.formats.forEach(function(format) {
  4002. if (!Array.isArray(format.descriptions)) return;
  4003. if (k.slice(1).some(k => format.descriptions.includesCaseless(k))) channelModes.pushUnique(k[0]);
  4004. });
  4005. });
  4006. release.identifiers.forEach(function(id) {
  4007. identifiers[id.type.toUpperCase().replace(/\s*\/\s*/g, '-').replace(/\W/g, '_')] = id.value;
  4008. });
  4009. var editionDescriptors = [];
  4010. media = new Set();
  4011. release.formats.forEach(function(format) {
  4012. if (editionTests.some(rx => rx.test(format.text))) editionDescriptors.push(format.text);
  4013. if (Array.isArray(format.descriptions)) format.descriptions.forEach(function(descriptions) {
  4014. if (editionTests.some(rx => rx.test(descriptions))) editionDescriptors.push(descriptions);
  4015. });
  4016. if (format.name == 'All Media') return;
  4017. var _media = estimateMedia(format.name);
  4018. if (_media) media.add(_media);
  4019. if (!/\b(?:File)\b/.test(format.name)) return;
  4020. if (['FLAC', 'WAV', 'AIF', 'AIFF', 'AIFC', 'PCM', 'ALAC', 'APE', 'WavPack', 'DFF', 'DSD']
  4021. .some(k => format.descriptions.includes(k))) {
  4022. encoding = 'lossless'; format = 'FLAC';
  4023. } else if (format.descriptions.includes('MP3')) {
  4024. encoding = 'lossy'; format = 'MP3'; bitdepth = undefined;
  4025. if (/\b(\d+)\s*kbps\b/i.test(format.text)) bitrate = parseInt(RegExp.$1);
  4026. } else if (format.descriptions.includes('AAC')) {
  4027. encoding = 'lossy'; format = 'AAC'; bitdepth = undefined;
  4028. if (/(\d+)\s*kbps\b/i.test(format.text)) bitrate = parseInt(RegExp.$1);
  4029. } else if (['AMR', 'MP2', 'ogg-vorbis', 'Opus', 'SHN', 'WMA'].some(k => formatformat.descriptions.includes(k)))
  4030. encoding = 'lossy';
  4031. });
  4032. function trackCounter(root) {
  4033. return Array.isArray(root) ? root.reduce(function(acc, track) {
  4034. switch (track.type_) {
  4035. case 'track': var count = Number(track.position != 'Video'); break;
  4036. case 'index': count = trackCounter(track.sub_tracks); break;
  4037. }
  4038. return acc + (count || 0);
  4039. }, 0) : 0;
  4040. }
  4041. totalTracks = trackCounter(release.tracklist);
  4042. return master.catch(function(reason) {
  4043. console.debug('Discogs master not received:', reason);
  4044. if (prefs.messages_verbosity >= 1) addMessage(reason, 'notice');
  4045. return null;
  4046. }).then(function(master) {
  4047. var tags = new TagManager();
  4048. if (Array.isArray(release.genres)) tags.add(...release.genres);
  4049. if (Array.isArray(release.styles)) tags.add(...release.styles);
  4050. if (master) {
  4051. if (Array.isArray(master.genres)) tags.add(...master.genres);
  4052. if (Array.isArray(master.styles)) tags.add(...master.styles);
  4053. }
  4054. imgUrl = (master && master.images || []).concat(release.images || []).filter(function(image) {
  4055. return urlParser.test(image.resource_url || image.uri) && ['primary', 'front'].includes(image.type);
  4056. });
  4057. imgUrl = imgUrl.length > 0 ? imgUrl[0].resource_url || imgUrl[0].uri : undefined;
  4058. if (imgUrl && /^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/(\S+?\.\w+)\b/i.test(imgUrl))
  4059. imgUrl = 'https://www.discogs.com/image/' + RegExp.$1;
  4060. var trackCounter = 0, discCounter = 0, _media;
  4061. release.tracklist.forEach(function(track) {
  4062. switch (track.type_.toLowerCase()) {
  4063. case 'heading':
  4064. discSubtitle = track.title;
  4065. break;
  4066. case 'track':
  4067. if (track.position != 'Video') addTrack(track);
  4068. break;
  4069. case 'index':
  4070. if (track.sub_tracks.every(subTrack => /^\s*[CDILMVX]+(?:[\:\.]| -)?\s+/.test(subTrack.title))
  4071. || track.sub_tracks.every(subTrack => /^\s*\d+(?:[\:\.]| -)?\s+/.test(subTrack.title)))
  4072. track.sub_tracks.forEach(function(subTrack) {
  4073. subTrack.title = subTrack.title.replace(/^\s*[CDILMVX\d]+(?:[\:\.]| -)?\s+/, '');
  4074. });
  4075. track.sub_tracks.filter(subTrack => subTrack.type_ == 'track' && subTrack.position != 'Video').map(function(subTrack, index) {
  4076. if (subTrack.position) var position = subTrack.position;
  4077. subTrack.title = (/*position || */convertToRoman(index + 1)).toString() + '. ' + subTrack.title.trim();
  4078. if (track.title) subTrack.title = track.title + ': ' + subTrack.title;
  4079. subTrack = Object.assign({}, track, subTrack);
  4080. //delete subTrack.position;
  4081. delete subTrack.sub_tracks;
  4082. return subTrack;
  4083. }).forEach(addTrack);
  4084. break;
  4085. }
  4086. });
  4087. return tracks;
  4088.  
  4089. function addTrack(track) {
  4090. if (track.type_ != 'track' || track.position == 'Video') return;
  4091. trackIdentifiers = {};
  4092. ++trackCounter;
  4093. if ((matches = /^(([A-Z]+)?(\d+)?)[\-\.](\S+)$/.exec(track.position)) != null && matches[1]) {
  4094. if (_media === undefined || matches[1] !== _media) ++discCounter;
  4095. if (matches[2]) trackIdentifiers.VOL_MEDIA = matches[2] + (matches[3] || discCounter).toString();
  4096. if (matches[3]) discNumber = matches[3];
  4097. trackNumber = matches[4];
  4098. _media = matches[1];
  4099. } else {
  4100. if (_media === undefined || _media !== '') ++discCounter;
  4101. trackNumber = track.position || trackCounter;
  4102. _media = '';
  4103. }
  4104. let trackArtists = getArtists(track);
  4105. trackArtist = isVA || !artistsMatch(trackArtists, artist);
  4106. let trackPerformers = trackArtists[0].concat(trackArtists[1]);
  4107. if (Array.isArray(track.extraartists)) trackPerformers.pushUniqueCaseless(...track.extraartists
  4108. .map(performer => (performer.anv || performer.name).replace(removeArtistNdx, '')));
  4109. tracks.push({
  4110. artist: isVA ? VA : undefined,
  4111. artists: !isVA ? artist[0] : undefined,
  4112. featured_artists: !isVA && artist[1].length > 0 ? artist[1] : undefined,
  4113. album: album,
  4114. album_year: master ? master.year : undefined,
  4115. release_date: release.released,
  4116. label: label.join(' / ') || undefined,
  4117. catalog: catalogue.join(' / ') || undefined,
  4118. country: release.country,
  4119. encoding: media.size == 1 ? encoding : undefined,
  4120. codec: media.size == 1 ? format : undefined,
  4121. bitrate: media.size == 1 ? bitrate : undefined,
  4122. bitdepth: media.size == 1 ? bitdepth : undefined,
  4123. channel_mode: channelModes.length == 1 ? channelModes[0] : undefined,
  4124. media: media.size == 1 ? media.keys().next().value : undefined,
  4125. genre: tags.toString(),
  4126. disc_number: discCounter, //discNumber,
  4127. total_discs: Math.max(release.format_quantity, 1),
  4128. disc_subtitle: discSubtitle,
  4129. edition_title: editionDescriptors.join(' / ') || undefined,
  4130. series: release.series || undefined,
  4131. track_number: trackNumber,
  4132. total_tracks: totalTracks,
  4133. title: track.title.trim(),
  4134. track_artists: trackArtist ? trackArtists[0] : undefined,
  4135. track_guests: trackArtist ? trackArtists[1] : undefined,
  4136. composers: role(3, true),
  4137. conductors: role(4, true),
  4138. compilers: role(5, true),
  4139. remixers: role(2),
  4140. producers: role(6, true),
  4141. mixers: role(7),
  4142. performers: role(8, true), //trackPerformers,
  4143. duration: timeStringToTime(track.duration) || undefined,
  4144. description: description,
  4145. identifiers: mergeIds(),
  4146. //url: release.uri,
  4147. cover_url: imgUrl,
  4148. });
  4149.  
  4150. function role(index, defaultToAlbumArtist = false) {
  4151. return trackArtists[index].length > 0 ? trackArtists[index]
  4152. : defaultToAlbumArtist && artist[index].length > 0 ? artist[index] : undefined;
  4153. }
  4154. }
  4155. });
  4156.  
  4157. function getArtists(root, anv = false) {
  4158. var artists = [];
  4159. for (var ndx = 0; ndx < 10; ++ndx) artists[ndx] = [];
  4160. ndx = 0;
  4161. if (Array.isArray(root.artists)) root.artists.forEach(function(artist) {
  4162. artists[/^(?:Conducts)$/i.test(artist.join) ? 4 : ndx].push((anv && artist.anv || artist.name).replace(removeArtistNdx, ''));
  4163. if (/^(?:feat(?:uring)?|ft|with)\b/i.test(artist.join)) ndx = 1;
  4164. });
  4165. if (Array.isArray(root.extraartists)) [
  4166. /* 0 */ [/^(?:Ensemble|Orchestra|Choir)\b/i, anv],
  4167. /* 1 */ [/^(?:Feat(?:uring)?|Ft|With)\b/i, anv],
  4168. /* 2 */ [/^(?:Remix(?:ed[\s\-]By|er)?)\b/i, anv],
  4169. /* 3 */ [/^(?:(?:Written|Composed|Libretto|Music)[\s\-]By|Composer|(?:Composer)?Lyricist|Writer|Author)\b/i, false],
  4170. /* 4 */ [/^(?:Conducted[\s\-]By|Conductor|Chorus\sMaster)\b/i, anv],
  4171. /* 5 */ [/^(?:Compiled[\s\-]By|Compiler)\b/i, anv],
  4172. /* 6 */ [/^(?:Produced[\s\-]By|Producer)\b/i, anv],
  4173. /* 7 */ [/^(?:(?:Mixed)[\s\-]By|Mixer)\b/i, anv],
  4174. /* 8 */ [/^(?:Performer|Musician|(?:Backing\s)?Vocals|Solo\sVocal|Voice|(?:\w+\s)?Guitar|(?:\w+\s)?Bass|Piano|Drums|Percussion|Timpani|Shaker|Synthesizer|Synth|Keyboards|(?:\w+\s)?Saxophone|Trumpet|Banjo|Harmonica|Accordion|Harmonium|Organ|Violin|Viola|Cello|Clarinet|Trombone|Glockenspiel|Vibraphone|Fiddle|Cornet\Star|Tambourine|Loops|Mellotron|Tabla|Saw|Congas|Bongos|Flute|Harp|Tambura|Flute|Sarangi|Cabasa|Handclaps|Kalimba|Vocoder|Sounds|Whistling|Other)\b/i, anv],
  4175. /* 9 */ [/^(?:(?:Written|Composed)[\s\-]By|Composer|Lyricist|Writer)\b/i, true],
  4176. ].forEach(function(def, index) {
  4177. artists[index].pushUniqueCaseless(...root.extraartists
  4178. .filter(extraArtist => extraArtist.role.split(/,\s+/).some(role => def[0].test(role)))
  4179. .map(extraArtist => (def[1] && extraArtist.anv || extraArtist.name || '').replace(removeArtistNdx, '')));
  4180. }); else if (artists[0].length <= 0 && artists[1].length > 0) artists[0] = artist[0];
  4181. return artists;
  4182. }
  4183. });
  4184. } else if (url.hostname.endsWith('supraphonline.cz')) {
  4185. url.search = '';
  4186. return globalFetch(url.href).then(function(response) {
  4187. if (/\/album\/(\d+)\b/i.test(response.finalUrl)) identifiers.SUPRAPHONLINE_ID = parseInt(RegExp.$1);
  4188. artist = Array.from(response.document.querySelectorAll('div.visible-lg-block > h2.album-artist > a'))
  4189. .map(a => a.title || a.textContent.trim());
  4190. isVA = (ref = response.document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]')) != null ?
  4191. vaParser.test(ref.content) : artist.length <= 0;
  4192. if ((ref = response.document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
  4193. if ((ref = response.document.querySelector('meta[itemprop="numTracks"]')) != null)
  4194. totalTracks = parseInt(ref.content);
  4195. genres = (ref = response.document.querySelector('meta[itemprop="genre"]')) != null ? ref.content : undefined;
  4196. if ((ref = response.document.querySelector('li.album-version > div.selected > div')) != null) {
  4197. if (/\b(?:MP3)\b/.test(ref.textContent)) {
  4198. media = 'WEB'; encoding = 'lossy'; format = 'MP3';
  4199. }
  4200. if (/\b(?:FLAC)\b/.test(ref.textContent)) {
  4201. media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 16;
  4202. }
  4203. if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) {
  4204. media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 24;
  4205. }
  4206. if (/\b(?:CD)\b/.test(ref.textContent)) media = 'CD';
  4207. if (/\b(?:LP)\b/.test(ref.textContent)) media = 'Vinyl';
  4208. }
  4209. const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
  4210. response.document.querySelectorAll('ul.summary > li').forEach(function(li) {
  4211. if (li.childElementCount <= 0) return;
  4212. let key = li.firstElementChild.textContent, value = li.lastChild.textContent.trim();
  4213. if (key.includes('Nosič')) media = value;
  4214. if (key.includes('Datum vydání')) releaseDate = normalizeDate(value, 'cs');
  4215. if (key.includes('První vydání')) albumYear = extractYear(value);
  4216. if (key.includes('Žánr')) genres = translateGenre(value);
  4217. if (key.includes('Vydavatel')) label = value;
  4218. if (key.includes('Katalogové číslo')) catalogue = value;
  4219. if (key.includes('Formát')) {
  4220. if (/\b(?:FLAC|WAV|AIFF?)\b/.test(value)) { encoding = 'lossless'; format = 'FLAC' }
  4221. if (/\b(\d+)[\-\s]?bits?\b/i.test(value)) bitdepth = parseInt(RegExp.$1);
  4222. if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(value)) samplerate = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
  4223. }
  4224. //if (key.includes('Celková stopáž')) totalTime = timeStringToTime(value);
  4225. if (copyrightParser.test(key) && !albumYear) albumYear = extractYear(value);
  4226. });
  4227. const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
  4228. let artists = [], ndx;
  4229. for (let i = 0; i < creators.length; ++i) artists[i] = {};
  4230. response.document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
  4231. if ((ref = it.querySelector('h3')) != null) {
  4232. ndx = undefined;
  4233. creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
  4234. } else {
  4235. if (typeof ndx != 'number') return;
  4236. if (ndx == 2) var role = 'ensemble';
  4237. else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
  4238. if ((ref = it.querySelector('a')) != null) {
  4239. if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
  4240. artists[ndx][role].pushUnique([ref.textContent.trim(), url.origin + ref.pathname]);
  4241. }
  4242. }
  4243. });
  4244. getDescription(response, 'div[itemprop="description"] p', true);
  4245. composer = [];
  4246. let performers = [], conductor = [], DJs = [], albumGuests = [], volMedia;
  4247. function dumpArtist(ndx, role) {
  4248. if (!role || role == 'undefined') return;
  4249. if (description.length > 0) description += '\n' ;
  4250. description += '[color=#9576b1]' + role + '[/color] – ';
  4251. //description += artists[ndx][role].map(artist => '[artist]' + artist[0] + '[/artist]').join(', ');
  4252. description += artists[ndx][role].map(artist => '[url=' + artist[1] + ']' + artist[0] + '[/url]').join(', ');
  4253. }
  4254. for (let i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
  4255. var a = artists[i][role].map(a => a[0]);
  4256. ([
  4257. 'conductor', 'choirmaster', 'director',
  4258. ].includes(role) ? conductor : role == 'DJ' ? DJs : [
  4259. 'FeaturedArtist',
  4260. ].includes(role) ? albumGuests : artist).pushUnique(...a);
  4261. if (i != 2) dumpArtist(i, role);
  4262. });
  4263. Object.keys(artists[0]).forEach(function(role) { // composers
  4264. composer.pushUnique(...artists[0][role].map(it => it[0])
  4265. .filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
  4266. dumpArtist(0, role);
  4267. });
  4268. Object.keys(artists[3]).forEach(role => { dumpArtist(3, role) }); // ADC & mastering
  4269. if ((ref = response.document.querySelector('meta[itemprop="image"]')) != null)
  4270. imgUrl = ref.content.replace(/\?.*$/, '');
  4271. response.document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(tr, index) {
  4272. if (tr.classList.contains('cd-header') && (ref = tr.querySelector('td > h3')) != null
  4273. && /\b(?:(\S*?)\s*)?(\d+)\b/.test(ref.textContent)) {
  4274. volMedia = RegExp.$1 ? RegExp.lastMatch : undefined;
  4275. discNumber = parseInt(RegExp.$2) || undefined;
  4276. }
  4277. if (tr.classList.contains('song-header') && (ref = tr.querySelector('td')) != null)
  4278. discSubtitle = ref.title || ref.textContent.trim();
  4279. if (tr.classList.contains('track') && tr.id) {
  4280. trackIdentifiers = {
  4281. TRACK_ID: /^(?:track)-(\d+)$/i.test(tr.id) ? parseInt(RegExp.$1) : undefined,
  4282. };
  4283. if (volMedia) trackIdentifiers.VOL_MEDIA = volMedia;
  4284. let track = {
  4285. artist: isVA ? VA : undefined,
  4286. artists: !isVA && artist.length > 0 ? artist : undefined,
  4287. //featured_artists: albumGuests.length > 0 ? albumGuests : undefined,
  4288. album: album,
  4289. album_year: /*trackYear || */albumYear || undefined,
  4290. release_date: releaseDate,
  4291. label: label,
  4292. catalog: catalogue,
  4293. encoding: encoding,
  4294. codec: format,
  4295. bitdepth: bitdepth,
  4296. samplerate: samplerate || undefined,
  4297. media: media,
  4298. genre: genres,
  4299. disc_number: discNumber,
  4300. total_discs: totalDiscs,
  4301. disc_subtitle: discSubtitle,
  4302. track_number: /^\s*(\d+)\.?\s*$/.test(tr.children[0].firstChild.textContent) ?
  4303. parseInt(RegExp.$1) || RegExp.$1 : undefined,
  4304. total_tracks: totalTracks,
  4305. title: (ref = tr.querySelector('meta[itemprop="name"][content]')) != null ? ref.content
  4306. : (ref = tr.querySelector('td > a.trackdetail')) != null ? ref.textContent.trim() : undefined,
  4307. performers: performers.length > 0 ? performers : undefined,
  4308. composers: composer.length > 0 ? composer : undefined,
  4309. conductors: conductor.length > 0 ? conductor : undefined,
  4310. compilers: DJs.length > 0 ? DJs : undefined,
  4311. duration: durationFromMeta(tr),
  4312. url: response.finalUrl,
  4313. description: description,
  4314. identifiers: mergeIds(),
  4315. cover_url: imgUrl,
  4316. };
  4317. tracks.push((function() {
  4318. if ((ref = tr.querySelector('td > a.trackdetail')) == null) return Promise.reject('link not found');
  4319. return globalFetch(url.origin + ref.pathname + ref.search).then(function(response) {
  4320. var detail = response.document.querySelector('div[data-swap="trackdetail-' +
  4321. track.identifiers.TRACK_ID + '"] > div > div.row');
  4322. if (detail == null) return Promise.reject('element not found');
  4323. detail.querySelectorAll('div[class]:nth-of-type(1) > ul > li').forEach(function(li) {
  4324. var key = li.querySelector('span'), value = li.lastChild;
  4325. if (key == null || value.nodeType != Node.TEXT_NODE) return;
  4326. key = key.textContent.trim(); value = value.wholeText.trim();
  4327. if (!key || !value) return;
  4328. if (key.startsWith('Žánr')) track.genre = value;
  4329. if (key.startsWith('Nahrávka dokončena')) track.rec_year = extractYear(value);
  4330. if (key.startsWith('Místo nahrání')) track.venue = value;
  4331. if (key.startsWith('Rok prvního vydání')) track.pub_year = extractYear(value);
  4332. if (copyrightParser.test(key)) track.copyright = value;
  4333. });
  4334. let trackArtists = [];
  4335. for (let i = 0; i < 8; ++i) trackArtists[i] = [];
  4336. detail.querySelectorAll('div[class]:nth-of-type(2) > ul > li').forEach(function(li) {
  4337. var role = li.querySelector('span');
  4338. var artists = Array.from(li.getElementsByTagName('a')).map(a => a.textContent.trim())
  4339. .filter(artist => !pseudoArtistParsers.some(rx => rx.test(artist)));
  4340. if (role != null && artists.length > 0) role = translateRole(role); else return;
  4341. if (artistClassParsers[2].some(rx => rx.test(role)))
  4342. trackArtists[2].pushUnique(...artists);
  4343. else if (artistClassParsers[3].some(rx => rx.test(role)))
  4344. trackArtists[3].pushUnique(...artists);
  4345. else if (artistClassParsers[5].some(rx => rx.test(role)))
  4346. trackArtists[5].pushUnique(...artists);
  4347. else if (artistClassParsers[6].some(rx => rx.test(role)))
  4348. trackArtists[6].pushUnique(...artists);
  4349. else if (role.toLowerCase() == 'performer' || !artistClassParsers[8].some(rx => rx.test(role))) {
  4350. if (artistClassParsers[0].some(rx => rx.test(role)))
  4351. trackArtists[0].pushUnique(...artists);
  4352. else if (artistClassParsers[1].some(rx => rx.test(role)))
  4353. trackArtists[1].pushUnique(...artists);
  4354. else if (artistClassParsers[4].some(rx => rx.test(role)))
  4355. trackArtists[4].pushUnique(...artists);
  4356. else artists.forEach(_artist => {
  4357. if (artist.includesCaseless(_artist)) trackArtists[0].pushUnique(_artist);
  4358. else if (artistClassParsers[7].some(rx => rx.test(role))) trackArtists[1].pushUnique(_artist);
  4359. });
  4360. trackArtists[7].pushUnique(...artists.map(artist => artist + ' (' + role + ')'));
  4361. }
  4362. });
  4363. if (trackArtists[1].length > 0 && trackArtists[0].length <= 0) {
  4364. trackArtists[0] = trackArtists[1]; trackArtists[1] = [];
  4365. }
  4366. if (trackArtists[0].length > 0 && (isVA || !trackArtists[0].equalCaselessTo(artist)
  4367. || trackArtists[1].length > 0/*!trackArtists[1].equalCaselessTo(albumGuests)*/)) {
  4368. track.track_artists = trackArtists[0];
  4369. if (trackArtists[1].length > 0) track.track_guests = trackArtists[1];
  4370. }
  4371. [
  4372. [3, 'composer'],
  4373. [4, 'conductor'],
  4374. [2, 'remixer'],
  4375. [5, 'compiler'],
  4376. //[6, 'producer'],
  4377. [7, 'performer'],
  4378. ].forEach(def => { if (trackArtists[def[0]].length > 0) track[def[1] + 's'] = trackArtists[def[0]] })
  4379. return track;
  4380. });
  4381. })().catch(function(reason) {
  4382. console.error('Supraphonline parser failed to get track', index + 1, 'detail:', reason);
  4383. return Promise.resolve(track);
  4384. }));
  4385. } // track
  4386. });
  4387. return Promise.all(tracks);
  4388.  
  4389. function translateGenre(genre) {
  4390. if (!genre || typeof genre != 'string') return undefined;
  4391. [
  4392. ['Orchestrální hudba', 'Orchestral Music'],
  4393. ['Komorní hudba', 'Chamber Music'],
  4394. ['Vokální', 'Classical, Vocal'],
  4395. ['Klasická hudba', 'Classical'],
  4396. ['Melodram', 'Classical, Melodram'],
  4397. ['Symfonie', 'Symphony'],
  4398. ['Vánoční hudba', 'Christmas Music'],
  4399. [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
  4400. ['Dechová hudba', 'Brass Music'],
  4401. ['Elektronika', 'Electronic'],
  4402. ['Folklor', 'Folclore, World Music'],
  4403. ['Instrumentální hudba', 'Instrumental'],
  4404. ['Latinské rytmy', 'Latin'],
  4405. ['Meditační hudba', 'Meditative'],
  4406. ['Vojenská hudba', 'Military Music'],
  4407. ['Pro děti', 'Children'],
  4408. ['Pro dospělé', 'Adult'],
  4409. ['Mluvené slovo', 'Spoken Word'],
  4410. ['Audiokniha', 'audiobook'],
  4411. ['Humor', 'humour'],
  4412. ['Pohádka', 'Fairy-Tale'],
  4413. ].forEach(function(subst) {
  4414. if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
  4415. || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
  4416. });
  4417. return genre;
  4418. }
  4419. function translateRole(elem) {
  4420. return elem instanceof HTMLElement ? [
  4421. [/\b(?:klavír)\b/ig, 'piano'],
  4422. [/\b(?:housle)\b/ig, 'violin'],
  4423. [/\b(?:violoncello)\b/ig, 'cello'],
  4424. [/\b(?:viola)\b/ig, 'alto'],
  4425. [/\b(?:varhany)\b/ig, 'organ'],
  4426. [/\b(?:cembalo)\b/ig, 'harpsichord'],
  4427. [/\b(?:trubka)\b/ig, 'trumpet'],
  4428. [/\b(?:soprán)\b/ig, 'soprano'],
  4429. [/\b(?:alt)\b/ig, 'alto'],
  4430. [/\b(?:baryton)\b/ig, 'baritone'],
  4431. [/\b(?:bas)\b/ig, 'basso'],
  4432. [/\b(?:akordeon)\b/ig, 'accordion'],
  4433. [/\b(?:syntezátor)\b/ig, 'synthesizer'],
  4434. [/\b(?:klávesové nástroje)\b/ig, 'keyboards'],
  4435. [/\b(?:bicí)\b/ig, 'drums'],
  4436. [/\b(?:kontrabas)\b/ig, 'double-bass'],
  4437. [/\b(?:zpěv|vokál)\b/ig, 'vocals'],
  4438. [/\b(?:baskytara)\b/ig, 'bass guitar'],
  4439. [/\b(?:havajská kytara)\b/ig, 'steel guitar'],
  4440. [/\b(?:akustická kytara)\b/ig, 'acoustic guitar'],
  4441. [/\b(?:kytara)\b/ig, 'guitar'],
  4442. [/\b(?:kytary)\b/ig, 'guitars'],
  4443. [/(?:čte|četba)\b/ig, 'narration'],
  4444. [/\b(?:vypravuje)\b/ig, 'narration'],
  4445. [/\b(?:hudební těleso)\b/ig, 'ensemble'],
  4446. [/\b(?:Umělec)\b/ig, 'Artist'],
  4447. [/\b(?:improvizace)\b/ig, 'improvisation'],
  4448. ['český', 'czech'],
  4449. ['původní', 'original'],
  4450. [/\b(?:text)\b/ig, 'lyrics'],
  4451. [/\b(?:hudba)\b/ig, 'music'],
  4452. ['hudební', 'music'],
  4453. [/\b(?:autor)\b/ig, 'author'],
  4454. [/\b(?:překlad)\b/ig, 'translation'],
  4455. ['účinkuje', 'participating'],
  4456. ['hovoří a zpívá', 'speaks and sings'],
  4457. ['hovoří', 'spoken by'],
  4458. ['komentář', 'commentary'],
  4459. [/\b(?:dirigent)\b/ig, 'conductor'],
  4460. ['řídí', 'director'],
  4461. [/\b(?:sbormistr)\b/ig, 'choirmaster'],
  4462. ['programování', 'programming'],
  4463. [/\b(?:produkce)\b/ig, 'produced by'],
  4464. ['nahrál', 'recorded by'],
  4465. [/\b(?:digitální přepis)\b/ig, 'A/D transfer'],
  4466. ].reduce((r, def) => r.replace(...def), elem.textContent.trim().replace(/\s*:.*$/, '')) : undefined;
  4467. }
  4468. });
  4469. } else if (url.hostname.endsWith('bontonland.cz')) return globalFetch(url).then(function(response) {
  4470. ref = response.document.querySelector('div#detailheader > h1');
  4471. if (ref != null && /^(.*?)\s*:\s*(.*)$/.test(ref.textContent.trim())) {
  4472. artist = RegExp.$1;
  4473. isVA = vaParser.test(artist);
  4474. album = RegExp.$2;
  4475. }
  4476. media = 'CD';
  4477. response.document.querySelectorAll('table > tbody > tr > td.nazevparametru').forEach(function(it) {
  4478. if (it.textContent.includes('Datum vydání')) {
  4479. releaseDate = normalizeDate(it.nextElementSibling.textContent, 'cs');
  4480. albumYear = extractYear(it.nextElementSibling.textContent);
  4481. } else if (it.textContent.includes('Nosič / počet')) {
  4482. if (/^(.*?)\s*\/\s*(.*)$/.test(it.nextElementSibling.textContent)) {
  4483. media = RegExp.$1;
  4484. totalDiscs = RegExp.$2;
  4485. }
  4486. } else if (it.textContent.includes('Interpret')) artist = it.nextElementSibling.textContent.trim();
  4487. else if (it.textContent.includes('EAN')) identifiers.BARCODE = it.nextElementSibling.textContent.trim();
  4488. });
  4489. getDescription(response, 'div#detailtabpopis > div[class^="pravy"] > div > p:not(:last-of-type)', true);
  4490. if (description.startsWith('[quote]Tracklist:')) description = undefined;
  4491. if ((ref = response.document.querySelector('a.detailzoom')) != null) imgUrl = ref.href;
  4492. if ((ref = response.document.querySelector('img#lbImage')) != null) imgUrl = ref.src;
  4493. if ((ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > ol')) != null) {
  4494. return Array.from(ref.querySelectorAll('li')).map(function(track, ndx, arr) {
  4495. title = track.innerText.trim();
  4496. duration = undefined;
  4497. if (/^(.*?)\s+\(((?:\d+:)?\d+:\d+)\)$/.test(title) || /^(.*?)\s+\(((?:\d+:)?\d+:\d+)\)$/.test(title)) {
  4498. title = RegExp.$1;
  4499. duration = timeStringToTime(RegExp.$2);
  4500. }
  4501. return {
  4502. artist: isVA ? VA : artist,
  4503. album: album,
  4504. //album_year: extractYear(releaseDate),
  4505. release_date: releaseDate,
  4506. label: label,
  4507. media: media,
  4508. track_number: ndx + 1,
  4509. total_tracks: arr.length,
  4510. title: title,
  4511. duration: duration,
  4512. url: response.finalUrl.replace(/\?.*$/, ''),
  4513. description: description,
  4514. identifiers: mergeIds(),
  4515. cover_url: imgUrl,
  4516. };
  4517. });
  4518. } else if ((ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > p:last-of-type')) != null) {
  4519. var trackList = ref.textContent.trim().split(/(?:\r?\n)+/).map(tr => tr.trim());
  4520. trackNumber = 0;
  4521. trackList.forEach(function(track) {
  4522. if (!/^(?:(\d+)(?:\s*[\/\.\-\:\)])?\s+)?(.+?)(?:\s+((?:\d+:)?\d+:\d+))?$/.test(track)) return;
  4523. ++trackNumber;
  4524. tracks.push({
  4525. artist: isVA ? VA : artist,
  4526. album: album,
  4527. //album_year: extractYear(releaseDate),
  4528. release_date: releaseDate,
  4529. label: label,
  4530. media: media,
  4531. track_number: parseInt(RegExp.$1) || RegExp.$1 || trackNumber,
  4532. total_tracks: trackList.length,
  4533. title: RegExp.$2,
  4534. duration: timeStringToTime(RegExp.$3) || undefined,
  4535. url: response.finalUrl.replace(/\?.*$/, ''),
  4536. description: description,
  4537. identifiers: mergeIds(),
  4538. cover_url: imgUrl,
  4539. });
  4540. });
  4541. return tracks;
  4542. } else throw 'Playlist could not be located';
  4543. }); else if (url.hostname.endsWith('nativedsd.com')) return globalFetch(url).then(function(response) {
  4544. identifiers.COMPOSEREMPHASIS = 1;
  4545. artist = (ref = response.document.querySelector('div.the-content > header > h2')) != null ?
  4546. ref.firstChild.data.trim() : undefined;
  4547. isVA = !artist || vaParser.test(artist);
  4548. if ((ref = response.document.querySelector('div.the-content > header > h1')) != null) album = ref.firstChild.data.trim();
  4549. if ((ref = response.document.querySelector('div.the-content > header > h3')) != null) composer = ref.firstChild.data.trim();
  4550. if ((ref = response.document.querySelector('div.the-content > header > h1 > small')) != null)
  4551. albumYear = extractYear(ref.firstChild.data);
  4552. ref = response.document.querySelector('div#breadcrumbs > div[class] > a:nth-of-type(2)');
  4553. if (ref != null) label = ref.firstChild.data.trim();
  4554. if (label == 'Albums') label = undefined;
  4555. if ((ref = response.document.querySelector('h2#sku')) != null) {
  4556. if (/^\s*(?:Catalog\sNumber):\s*(.*?)\s*$/im.test(ref.textContent)) catalogue = RegExp.$1;
  4557. if (/^\s*(?:Released\son\sNativeDSD):\s*(.*?)\s*$/im.test(ref.textContent)) releaseDate = RegExp.$1;
  4558. if (/^\s*(?:ID):\s*(.*?)\s*$/im.test(ref.textContent)) identifiers.NATIVEDSD_ID = RegExp.$1;
  4559. }
  4560. identifiers.ORIGINALFORMAT = 'DSD';
  4561. getDescription(response, 'div.the-content > div.entry > p', false);
  4562. if ((ref = response.document.querySelector('div#repertoire > div > p')) != null) {
  4563. let repertoire = html2php(ref, response.finalUrl);
  4564. if (description) description += '\n\n';
  4565. let ndx = repertoire.indexOf('\n[b]Track');
  4566. description += (ndx >= 0 ? repertoire.slice(0, ndx) : repertoire).trim().flatten();
  4567. }
  4568. ref = response.document.querySelectorAll('div#techspecs > table > tbody > tr');
  4569. if (ref.length > 0) {
  4570. if (description) description += '\n\n';
  4571. description += '[b][u]Tech specs[/u][/b]';
  4572. ref.forEach(function(it) {
  4573. description += `\n[b]${it.children[0].textContent.trim()}[/b]${it.children[1].textContent.trim()}`;
  4574. });
  4575. }
  4576. if ((ref = response.document.querySelector('a#album-cover')) != null) imgUrl = ref.href;
  4577. trs = response.document.querySelectorAll('div#track-list > table > tbody > tr[id^="track"]');
  4578. return Array.from(trs).map(function(tr) {
  4579. title = undefined;
  4580. trackIdentifiers = { TRACK_ID: tr.id.replace(/^track-/i, '') };
  4581. var trackComposer;
  4582. if ((ref = tr.children[1]) != null) {
  4583. title = ref.firstChild.textContent.trim();
  4584. trackComposer = ref.childNodes[2] && ref.childNodes[2].textContent.trim() || undefined;
  4585. }
  4586. return {
  4587. artist: isVA ? VA : artist,
  4588. album: album,
  4589. album_year: albumYear,
  4590. release_date: releaseDate,
  4591. label: label,
  4592. catalog: catalogue,
  4593. encoding: 'lossless',
  4594. codec: 'FLAC',
  4595. bitdepth: 24,
  4596. samplerate: 88200,
  4597. media: media,
  4598. genre: genres.join('; '), // 'Jazz'
  4599. disc_number: discNumber,
  4600. total_discs: totalDiscs,
  4601. disc_subtitle: discSubtitle,
  4602. track_number: (ref = tr.firstElementChild.firstElementChild) != null ?
  4603. parseInt(ref.firstChild.data.trim().replace(/\..*$/, '')) : undefined,
  4604. total_tracks: trs.length,
  4605. title: title,
  4606. composer: trackComposer || composer,
  4607. duration: (ref = tr.children[2]) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  4608. url: response.finalUrl,
  4609. description: description,
  4610. identifiers: mergeIds(),
  4611. cover_url: imgUrl,
  4612. };
  4613. });
  4614. });/* else if (url.hostname.endsWith('junodownload.com') && /\/([\d\-]+)\/?$/.test(url.pathname)) {
  4615. let productKey = RegExp.$1;
  4616. return globalFetch('https://www.junodownload.com/api/1.2/playlist/getplaylistdetails/?product_key='.concat(productKey), {
  4617. responseType: 'xml',
  4618. }).then(response => Array.from(response.document.querySelectorAll('playlist > trackList > track')).map(function(track, index, trackList) {
  4619. artist = Array.from(track.querySelectorAll('extension > release_artists > artist > name'))
  4620. .map(artist => artist.textContent.trim());
  4621. isVA = artist.length == 1 && vaParser.test(artist[0]);
  4622. trackArtist = Array.from(track.querySelectorAll('extension > artists > artist > name'))
  4623. .map(artist => artist.textContent.trim());
  4624. trackArtist = isVA || !trackArtist.equalCaselessTo(artist) ? joinArtists(trackArtist) : undefined;
  4625. title = getValue('extension > track_title');
  4626. if (getValue('extension > mix_title')) title += ' (' + getValue('extension > mix_title') + ')';
  4627. return {
  4628. artist: isVA ? VA : artist.join(', '),
  4629. album: getValue('album'),
  4630. release_date: getValue('extension > relDate'),
  4631. label: getValue('extension > label > name'),
  4632. catalog: getValue('extension > catNumber'),
  4633. media: media,
  4634. genre: getValue('extension > genre'),
  4635. track_number: parseInt(getValue('trackNum')),
  4636. total_tracks: trackList.length,
  4637. title: getValue('extension > track_title'),
  4638. track_artist: trackArtist,
  4639. duration: parseInt(getValue('extension > length')) || undefined,
  4640. description: getValue('extension > rating_comment'),
  4641. identifiers: { JUNODOWNLOAD_ID: productKey },
  4642. cover_url: getValue('image'),
  4643. };
  4644.  
  4645. function getValue(selector) {
  4646. var node = track.querySelector(selector);
  4647. return node != null ? node.textContent.trim() : undefined;
  4648. }
  4649. }));
  4650. }*/ else if (url.hostname.endsWith('junodownload.com')) return globalFetch(url).then(function(response) {
  4651. if (/'id':'([\d\-]+)'/.test(response.responseText) || /\/([\d\-]+)\/?$/.test(new URL(response.finalUrl).pathname)) {
  4652. identifiers.JUNODOWNLOAD_ID = RegExp.$1;
  4653. var metaData = globalFetch('https://www.junodownload.com/api/1.2/playlist/getplaylistdetails/?product_key=' +
  4654. identifiers.JUNODOWNLOAD_ID, { responseType: 'xml' })
  4655. .then(response => Array.from(response.document.querySelectorAll('playlist > trackList > track')));
  4656. } else metaData = Promise.reject('No Id');
  4657. var productArtist;
  4658. if ((ref = response.document.querySelectorAll('div.breadcrumb_text > span:not([class])')).length == 4) {
  4659. artist = Array.from(ref[ref.length - 1].querySelectorAll('a')).map(elem => elem.textContent.trim());
  4660. productArtist = ref[ref.length - 1].textContent.trim();
  4661. } else if ((ref = response.document.querySelector('h2.product-artist')) != null) {
  4662. artist = Array.from(ref.querySelectorAll('a')).map(elem => elem.textContent.trim().titleCase());
  4663. productArtist = ref.textContent.trim().titleCase();
  4664. }
  4665. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4666. if ((ref = response.document.querySelector('meta[itemprop="name"]')) != null) album = ref.content.trim();
  4667. if ((ref = response.document.querySelector('meta[itemprop="author"]')) != null) label = ref.content.trim();
  4668. if ((ref = response.document.querySelector('span[itemprop="datePublished"]')) != null)
  4669. releaseDate = ref.firstChild.data.trim();
  4670. response.document.querySelectorAll('div.mb-3 > strong').forEach(function(it) {
  4671. if (it.textContent.startsWith('Genre')) {
  4672. ref = it;
  4673. while ((ref = ref.nextElementSibling) != null && ref.nodeName == 'A') genres.push(ref.textContent.trim());
  4674. } else if (it.textContent.startsWith('Cat')) {
  4675. if ((ref = it.nextSibling) != null && ref.nodeType == Node.TEXT_NODE) catalogue = ref.wholeText.trim();
  4676. }
  4677. });
  4678. getDescription(response, 'div[itemprop="review"]');
  4679. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  4680. trs = response.document.querySelectorAll('div.product-tracklist > div[itemprop="track"]');
  4681. return Array.from(trs).map(function(tr) {
  4682. trackIdentifiers = { BPM: tr.children[2].textContent.trim() };
  4683. trackNumber = undefined;
  4684. tr.querySelector('div.track-title').childNodes.forEach(function(n) {
  4685. if (trackNumber || n.nodeType != Node.TEXT_NODE) return;
  4686. trackNumber = n.data.trim().replace(/\s*\..*$/, '');
  4687. });
  4688. trackArtist = (ref = tr.querySelector('meta[itemprop="byArtist"]')) != null ? ref.content : undefined;
  4689. title = (ref = tr.querySelector('span[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  4690. if (title && trackArtist && title.startsWith(trackArtist + ' - ')) title = title.slice(trackArtist.length + 3);
  4691. return {
  4692. artist: isVA ? VA : productArtist,
  4693. artists: !isVA ? artist : undefined,
  4694. album: album,
  4695. album_year: extractYear(releaseDate),
  4696. release_date: releaseDate,
  4697. label: label,
  4698. catalog: catalogue,
  4699. media: media,
  4700. genre: genres.join('; '),
  4701. disc_number: discNumber,
  4702. total_discs: totalDiscs,
  4703. disc_subtitle: discSubtitle,
  4704. track_number: trackNumber,
  4705. total_tracks: trs.length,
  4706. title: title,
  4707. track_artist: trackArtist && (isVA || trackArtist.toLowerCase() != productArtist.toLowerCase()) ? trackArtist : undefined,
  4708. duration: durationFromMeta(tr),
  4709. url: !identifiers.JUNODOWNLOAD_ID ? response.finalUrl : undefined,
  4710. description: description,
  4711. identifiers: mergeIds(),
  4712. cover_url: imgUrl,
  4713. };
  4714. });
  4715. }); else if (url.hostname.endsWith('hdtracks.com')) return loadHDtracksMetadata(url).then(function(album) {
  4716. identifiers.HDTRACKS_ID = album.id || album.productId;
  4717. if (album.upc) identifiers.BARCODE = album.upc;
  4718. if (album.parentalWarning == 'NotExplicit') identifiers.EXPLICIT = 0;
  4719. else if (album.parentalWarning == 'Explicit') identifiers.EXPLICIT = 1;
  4720. isVA = album.artists.length <= 0 || vaParser.test(album.mainArtist);
  4721. var guests = [], composers = [], producers = [];
  4722. if (album.credits) album.credits.split(/\r?\n/).forEach(function(credit) {
  4723. if (!/^(.*)\s*:\s*(.*)$/.test(credit)) return;
  4724. var role = RegExp.$1, name = RegExp.$2;
  4725. if (role == 'Artist' && name.toLowerCase() != album.mainArtist.toLowerCase()) guests.pushUniqueCaseless(name);
  4726. else if (role == 'Composer') composers.pushUniqueCaseless(name);
  4727. else if (/\b(?:Producer)$/.test(role)) producers.pushUniqueCaseless(name);
  4728. });
  4729. //var albumGuests = guests.length > 0 ? ' feat. ' + joinArtists(guests) : '';
  4730. return Promise.all(album.trackIds.map((trackId, index) => loadHDtracksMetadata(trackId, 'track').catch(function(reason) {
  4731. console.warn('Fetching details from HDtracks failed at least for one track:', reason);
  4732. return album.tracks[index];
  4733. }).then(function(track) {
  4734. trackIdentifiers = {
  4735. ISRC: track.isrc,
  4736. TRACK_ID: track.id,
  4737. MD5: track.md5,
  4738. };
  4739. if (track.upc) trackIdentifiers.BARCODE = track.upc;
  4740. var mainArtists = splitAmpersands(track.mainArtist),
  4741. trackComposers = [], trackProducers = [], trackGuests = [];
  4742. if (track.credits) track.credits.split(/\r?\n/).forEach(function(credit) {
  4743. if (!/^(.*)\s*:\s*(.*)$/.test(credit)) return;
  4744. var role = RegExp.$1, name = RegExp.$2;
  4745. if (role == 'Artist' && !mainArtists.includesCaseless(name)) trackGuests.pushUniqueCaseless(name);
  4746. else if (role == 'Composer') trackComposers.pushUniqueCaseless(name);
  4747. else if (/\b(?:Producer)$/.test(role)) trackProducers.pushUniqueCaseless(name);
  4748. });
  4749. if (track.mainArtist && trackGuests.length > 0) track.mainArtist += ' feat. ' + joinArtists(trackGuests);
  4750. return {
  4751. artist: isVA ? VA : undefined,
  4752. artists: !isVA ? album.artists : undefined,
  4753. featured_artists: guests,
  4754. album: album.name,
  4755. release_date: track.release || album.release,
  4756. album_year: album.originalRelease ? extractYear(album.originalRelease) : undefined,
  4757. label: track.label || album.label,
  4758. distributor: track.distributor || album.distributor,
  4759. media: media,
  4760. samplerate: track.rate || album.rate || undefined,
  4761. bitdepth: track.resolution || album.resolution || undefined,
  4762. genre: track.genre || album.genre,
  4763. total_discs: album.discs,
  4764. track_number: track.index,
  4765. total_tracks: album.tracksCount, //album.tracks.length
  4766. composers: trackComposers.length > 0 ? trackComposers : composers,
  4767. //producers: trackProducers.length > 0 ? trackProducers : producers,
  4768. title: track.name,
  4769. track_artist: track.mainArtist && (isVA || !artistsMatch(track.mainArtist, album.mainArtist)) ?
  4770. track.mainArtist : undefined,
  4771. duration: track.duration,
  4772. url: !identifiers.HDTRACKS_ID ? response.finalUrl : undefined,
  4773. identifiers: mergeIds(),
  4774. cover_url: /*track.cover || */album.cover,
  4775. };
  4776. })));
  4777. }); else if (url.hostname.endsWith('deezer.com') && /\/album\/(\d+)\b/i.test(url.pathname)) {
  4778. return queryDeezerAPI('album', RegExp.$1).then(function(release) {
  4779. isVA = vaParser.test(release.artist.name);
  4780. identifiers.DEEZER_ID = release.id;
  4781. identifiers.RELEASETYPE = release.record_type;
  4782. if (release.upc) identifiers.BARCODE = release.upc;
  4783. if (release.cover_xl) imgUrl = release.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0');
  4784. return release.tracks.data.map(function(track, ndx) {
  4785. trackIdentifiers = { TRACK_ID: track.id };
  4786. return {
  4787. artist: isVA ? VA : release.artist.name,
  4788. album: release.title,
  4789. release_date: release.release_date,
  4790. label: release.label,
  4791. media: media,
  4792. genre: release.genres.data.map(it => it.name).join('; '),
  4793. track_number: ndx + 1,
  4794. total_tracks: release.nb_tracks,
  4795. title: track.title,
  4796. track_artist: track.artist.name && (isVA || track.artist.name != release.artist.name) ?
  4797. track.artist.name : undefined,
  4798. duration: track.duration,
  4799. //url: deezerAlbumPrefix + release.id,
  4800. identifiers: mergeIds(),
  4801. cover_url: imgUrl,
  4802. };
  4803. });
  4804. });
  4805. } else if (url.hostname.endsWith('spotify.com')) {
  4806. return /\/albums?\/(\w+)$/i.test(url.pathname) ? querySpotifyAPI('albums/' + RegExp.$1).then(function(release) {
  4807. if (prefs.diag_mode) console.debug('Spotify metadata loaded:', release);
  4808. artist = release.artists.map(artist => artist.name);
  4809. isVA = release.artists.length <= 0 || release.artists.length == 1 && vaParser.test(release.artists[0].name);
  4810. totalDiscs = release.tracks.items.reduce((acc, track) => Math.max(acc, track.disc_number), 0);
  4811. identifiers.SPOTIFY_ID = release.id;
  4812. identifiers.RELEASETYPE = release.album_type;
  4813. identifiers.BARCODE = release.external_ids.upc;
  4814. releaseDate = release.release_date;
  4815. if (release.release_date_precision == 'year') releaseDate = extractYear(releaseDate);
  4816. else if (release.release_date_precision == 'month' && /\b(\d{4}-\d{2})\b/.test(releaseDate))
  4817. releaseDate = RegExp.$1;
  4818. var image = release.images
  4819. .reduce((acc, image) => image.width * image.height > acc.width * acc.height ? image : acc);
  4820. return release.tracks.items.map(function(track, ndx) {
  4821. trackIdentifiers = {
  4822. TRACK_ID: track.id,
  4823. EXPLICIT: Number(track.explicit),
  4824. };
  4825. trackArtist = track.artists.map(artist => artist.name);
  4826. return {
  4827. artist: isVA ? VA : undefined,
  4828. artists: !isVA ? artist : undefined,
  4829. album: release.name,
  4830. release_date: releaseDate,
  4831. label: release.label,
  4832. media: media,
  4833. genre: release.genres.join('; '),
  4834. disc_number: track.disc_number,
  4835. total_discs: totalDiscs,
  4836. disc_subtitle: discSubtitle,
  4837. track_number: track.track_number,
  4838. total_tracks: release.total_tracks,
  4839. title: track.name,
  4840. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  4841. trackArtist : undefined,
  4842. duration: track.duration_ms / 1000,
  4843. //url: 'https://open.spotify.com/album/' + release.id,
  4844. identifiers: mergeIds(),
  4845. cover_url: image ? image.url : undefined,
  4846. };
  4847. });
  4848. }) : Promise.reject('This resource is not supported, pick a real album');
  4849. } else if (url.hostname.endsWith('prostudiomasters.com')) return globalFetch(url).then(function(response) {
  4850. if ((ref = response.document.querySelector('img.album-art')) != null) imgUrl = ref.currentSrc || ref.src;
  4851. try {
  4852. if ((ref = response.document.querySelector('body > script[charset]')) == null
  4853. || !/\b(?:PSM\.album)\s*=\s*(\{[\S\s]+\});(?=\s*(?:PSM)\b)/.test(ref.text)) throw 'Metadata not found';
  4854. let album = JSON.parse(RegExp.$1);
  4855. if (prefs.diag_mode) console.debug('PSM metadata received:', album);
  4856. const artistSplitter = /\s*;+\s*/;
  4857. artist = album.ArtistName.split(artistSplitter);
  4858. isVA = vaParser.test(album.ArtistName);
  4859. if (album.id) identifiers.PROSTUDIOMASTERS_ID = parseInt(album.id) || album.id;
  4860. if (album.GenreName) genres.push(album.GenreName);
  4861. if (album.SubGenreName) genres.push(album.SubGenreName);
  4862. if (album.genres) genres.push(album.genres);
  4863. if (/^[℗©]\s*(\d{4})\b/.test(album.PLine) || /^[℗©]\s*(\d{4})\b/.test(album.CLine)) releaseDate = RegExp.$1;
  4864. if (album.ICPN) identifiers.BARCODE = album.ICPN;
  4865. if (/\b(\d+(?:\.\d+)?)\s*kHz\s*\/\s*(\d+)[\-\s]?bit\s+(\w+)\b/i.test(album.recording_info)) {
  4866. samplerate = parseFloat(RegExp.$1) * 1000 || undefined;
  4867. bitdepth = parseInt(RegExp.$2) || undefined;
  4868. format = RegExp.$3;
  4869. if (['FLAC', 'AIFF', 'WAV', 'PCM'].includes(format)) encoding = 'lossless';
  4870. }
  4871. if (album.album_info) {
  4872. description = html2php(domParser.parseFromString(album.album_info, 'text/html').body, response.finalUrl);
  4873. if (description) description = `[quote]${description}[/quote]`;
  4874. }
  4875. return album.tracks.filter(track => track.duration !== '0' && track.ISRC != 'Digital Booklet').map(function(track) {
  4876. trackIdentifiers = {
  4877. EXPLICIT: Number(track.ExplicitLyrics == 1),
  4878. ISRC: track.ISRC,
  4879. TRACK_ID: parseInt(track.id) || track.id,
  4880. };
  4881. trackArtist = track.ArtistName.split(artistSplitter);
  4882. return {
  4883. artist: isVA ? VA : undefined,
  4884. artists: !isVA ? artist : undefined,
  4885. album: album.AlbumName,
  4886. genre: genres.join('; '),
  4887. release_date: releaseDate,
  4888. label: label,
  4889. catalog: album.CatalogNumber,
  4890. codec: format,
  4891. encoding: encoding,
  4892. bitdepth: bitdepth,
  4893. samplerate: samplerate,
  4894. media: media,
  4895. disc_number: parseInt(track.DiscSeq) || undefined,
  4896. disc_subtitle: track.GroupingTitle,
  4897. track_number: parseInt(track.TrackSeq) || undefined,
  4898. total_tracks: album.tracks.length,
  4899. title: track.TrackName,
  4900. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalTo(artist)) ? trackArtist : undefined,
  4901. composers: track.composers.split(artistSplitter),
  4902. duration: parseInt(track.duration) || undefined,
  4903. url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
  4904. description: description,
  4905. identifiers: mergeIds(),
  4906. cover_url: imgUrl,
  4907. };
  4908. });
  4909. } catch(e) {
  4910. console.warn('ProStudioMasters: falling back to HTML scraper for the reason', e);
  4911. if (/\/page\/(\d+)$/i.test(response.finalUrl)) identifiers.PROSTUDIOMASTERS_ID = RegExp.$1;
  4912. artist = Array.from(response.document.querySelectorAll('h2.ArtistName > a')).map(node => node.textContent.trim());
  4913. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4914. if (isVA) artist = [];
  4915. if ((ref = response.document.querySelector('h3.AlbumName')) != null) album = ref.textContent.trim();
  4916. if ((ref = response.document.querySelector('div.pline')) != null
  4917. && /^(?:[℗©]\s*)+(\d{4})\s+(.+)/.test(ref.textContent.trim())) {
  4918. releaseDate = RegExp.$1;
  4919. label = RegExp.$2;
  4920. }
  4921. getDescription(response, 'div.album-info', false);
  4922. trs = response.document.querySelectorAll('div.album-tracks > div.tracks > table > tbody > tr');
  4923. totalTracks = Array.from(trs).filter(tr => tr.classList.contains('track-playable')).length;
  4924. discNumber = 0;
  4925. trs.forEach(function(tr) {
  4926. if (tr.classList.contains('track-playable')) {
  4927. trackArtist = samplerate = bitdepth = format = title = undefined; trackIdentifiers = {};
  4928. if (ref = tr.getAttribute('data-track-id')) trackIdentifiers.TRACK_ID = ref;
  4929. trackNumber = (ref = tr.querySelector('div.num')) != null ? parseInt(ref.firstChild.textContent.trim()) : undefined;
  4930. if (trackNumber == 1) ++discNumber;
  4931. if ((ref = tr.querySelector('td.track-name > div.name')) != null) {
  4932. title = ref.firstChild.textContent.trim();
  4933. if ((ref = ref.querySelector(':scope small')) != null) trackArtist = ref.firstChild.textContent;
  4934. };
  4935. if ((ref = tr.querySelector('span.track-format')) != null && /^(\d+(?:[,\.]\d+)?)\s*([kMG]?Hz)(?:\s+(\d+)-bit)?\s*\|\s*(\S+)$/i.test(ref.textContent.trim())) {
  4936. samplerate = parseFloat(RegExp.$1);
  4937. ['hz', 'khz', 'mhz', 'ghz'].forEach((unit, ndx) => {
  4938. if (RegExp.$2.toLowerCase() == unit) samplerate *= 1000 ** ndx;
  4939. });
  4940. samplerate = Math.round(samplerate) || undefined;
  4941. bitdepth = parseInt(RegExp.$3) || undefined;
  4942. format = RegExp.$4;
  4943. }
  4944. tracks.push({
  4945. artist: isVA ? VA : undefined,
  4946. artists: !isVA ? artist : undefined,
  4947. album: album,
  4948. //album_year: extractYear(releaseDate),
  4949. release_date: releaseDate,
  4950. label: label,
  4951. catalog: catalogue,
  4952. codec: format,
  4953. bitdepth: bitdepth,
  4954. samplerate: samplerate,
  4955. media: media,
  4956. disc_number: discNumber,
  4957. total_discs: totalDiscs,
  4958. disc_subtitle: discSubtitle,
  4959. track_number: trackNumber,
  4960. total_tracks: totalTracks,
  4961. title: title,
  4962. track_artist: trackArtist && (isVA || !artistsMatch(trackArtist, artist)) ? trackArtist : undefined,
  4963. duration: (ref = tr.querySelector('td:last-of-type')) != null ? timeStringToTime(ref.firstChild.data) : undefined,
  4964. url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
  4965. description: description,
  4966. identifiers: mergeIds(),
  4967. cover_url: imgUrl,
  4968. });
  4969. } else if ((ref = tr.querySelector('div.grouping-title')) != null) {
  4970. discSubtitle = ref.textContent.trim();
  4971. guessDiscNumber();
  4972. }
  4973. });
  4974. return tracks;
  4975. }
  4976. }); else if (url.hostname.endsWith('play.google.com') && url.pathname.startsWith('/store/music/album/')) {
  4977. let _query = new URLSearchParams(url.search);
  4978. _query.set('hl', 'en');
  4979. url.search = _query;
  4980. return globalFetch(url).then(function(response) {
  4981. var search = new URLSearchParams(new URL(response.finalUrl).search);
  4982. var ID = search.get('id'), trackID, aggregateRating;
  4983. if (ID) identifiers.GOOGLE_ID = ID;
  4984. var root = response.document.querySelector('div[itemtype="https://schema.org/MusicAlbum"]');
  4985. if (root == null) throw new Error('Unexpected Google Play metadata structure');
  4986. if ((ref = root.querySelector('div[itemprop="byArtist"]')) != null) {
  4987. artist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
  4988. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  4989. }
  4990. if ((ref = root.querySelector('meta[itemprop="name"]')) != null) album = ref.content;
  4991. genres = Array.from(root.querySelectorAll('meta[itemprop="genre"]')).map(elem => elem.content);
  4992. if ((ref = root.querySelector('meta[itemprop="datePublished"]')) != null) releaseDate = ref.content;
  4993. if ((ref = root.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
  4994. if ((ref = root.querySelector('meta[itemprop="ratingValue"]')) != null) aggregateRating = parseFloat(ref.content);
  4995. if ((ref = response.document.querySelector('h1[class][itemprop="name"] > span')) != null
  4996. && (ref = ref.parentNode.parentNode.querySelector('div[class] > span[class]')) != null
  4997. && /\b(?:Explicit)\b/i.test(ref.textContent)) identifiers.EXPLICIT = 1;
  4998. if ((ref = response.document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content;
  4999. try {
  5000. let _objs = loadGoogleMetadata(response);
  5001. let _albumInfo = _objs.filter(obj => { try { return obj[0].length == 22 } catch(e) { return false } });
  5002. let _tracks = _objs.filter(function(obj) {
  5003. try {
  5004. return typeof obj[0][0][1] == 'boolean' && typeof obj[0][0][2] == 'boolean'
  5005. && typeof obj[0][0][3] == 'string' && typeof obj[0][0][4] == 'string';
  5006. } catch(e) { return false }
  5007. });
  5008. if (_albumInfo.length == 1) _albumInfo = _albumInfo[0][0]; else throw 'Album metadata not found';
  5009. if (_tracks.length == 1) _tracks = _tracks[0][0][0]; else throw 'Tracks metadata not found';
  5010. if (prefs.diag_mode) console.debug('Google Play objects extracted successfully:', _albumInfo, _tracks);
  5011. try {
  5012. artist = _albumInfo[18][0][8].map(artist => artist[1]);
  5013. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5014. } catch(e) { }
  5015. try { description = _albumInfo[10][0][1] } catch(e) { }
  5016. try { genres = _albumInfo[18][0][6].map(genre => genre[2]) } catch(e) { }
  5017. _tracks[0].forEach(function(volume) {
  5018. Array.prototype.push.apply(tracks, volume[0].map(function(track) {
  5019. trackArtist = track[0][8].map(ta => ta[1]);
  5020. trackIdentifiers = { TRACK_ID: track[12][0] };
  5021. return {
  5022. artist: isVA ? VA : undefined,
  5023. artists: !isVA ? artist : undefined,
  5024. album: _albumInfo[0][0] || track[9][0],
  5025. album_year: extractYear(_albumInfo[18][0][7][1] /*track[0][7][0]*/),
  5026. release_date: _albumInfo[18][0][7][2],
  5027. label: _albumInfo[18][0][4] || track[0][4],
  5028. media: media,
  5029. genre: genres.join('; '),
  5030. disc_number: volume[1],
  5031. total_discs: _tracks[0].length,
  5032. track_number: track[1],
  5033. total_tracks: _albumInfo[18][4],
  5034. title: track[8][0],
  5035. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5036. trackArtist : undefined,
  5037. duration: track[0][1],
  5038. description: description,
  5039. url: !identifiers.GOOGLE_ID ? response.finalUrl : undefined,
  5040. identifiers: mergeIds(),
  5041. cover_url: imgUrl,
  5042. };
  5043. }));
  5044. });
  5045. } catch(e) {
  5046. console.warn('Google Play music: falling back to HTML scraper (' + e + ')');
  5047. tracks = [];
  5048. if ((ref = response.document.querySelector('span > a[itemprop="genre"]')) != null) try {
  5049. label = ref.parentNode.nextElementSibling.textContent.trim().replace(/^(?:[©℗]|\([cCpP]\))\s*\d{4}\s+/, '');
  5050. } catch(e) { console.warn('Unexpected HTML structure (' + e + ')') }
  5051. //getDescription(response, '???', false);
  5052. var volumes = response.document.querySelectorAll('c-wiz > div > h2');
  5053. if (volumes.length <= 0) {
  5054. //response.document.querySelectorAll('c-wiz > div > table > tbody > tr[class]').forEach(scanPlaylist);
  5055. trackNumber = 0;
  5056. root.querySelectorAll('div[itemprop="track"]').forEach(function(tr) {
  5057. trackIdentifiers = {};
  5058. if ((ref = tr.querySelector('meta[itemprop="url"]')) != null) {
  5059. search = new URLSearchParams(new URL(ref.content).search);
  5060. let trackID = search.get('tid');
  5061. if (trackID) trackIdentifiers.TRACK_ID = trackID;
  5062. }
  5063. ++trackNumber;
  5064. title = (ref = tr.querySelector('meta[itemprop="name"]')) != null ? ref.content : undefined;
  5065. trackArtist = (ref = tr.querySelector('div[itemprop="byArtist"]')) != null ?
  5066. Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content) : [];
  5067. duration = durationFromMeta(tr);
  5068. addTrack();
  5069. });
  5070. } else volumes.forEach(function(volume) {
  5071. discNumber = undefined; discSubtitle = volume.textContent.trim();
  5072. guessDiscNumber();
  5073. volume.nextElementSibling.querySelectorAll('tbody > tr[class]').forEach(scanPlaylist);
  5074. });
  5075.  
  5076. function scanPlaylist(tr) {
  5077. trackNumber = (ref = tr.querySelector('td:nth-of-type(1) > div')) != null ?
  5078. parseInt(ref.textContent) || ref.textContent.trim() : undefined;
  5079. title = (ref = tr.querySelector('td[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
  5080. duration = (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined;
  5081. trackArtist = Array.from(tr.querySelectorAll('td:nth-of-type(4) > a')).map(it => it.textContent.trim());
  5082. addTrack();
  5083. }
  5084. function addTrack() {
  5085. tracks.push({
  5086. artist: isVA ? VA : undefined,
  5087. artists: !isVA ? artist : undefined,
  5088. album: album,
  5089. //album_year: extractYear(releaseDate),
  5090. release_date: releaseDate,
  5091. label: label,
  5092. catalog: catalogue,
  5093. media: media,
  5094. genre: genres.join('; '),
  5095. disc_number: discNumber,
  5096. total_discs: totalDiscs,
  5097. disc_subtitle: discSubtitle,
  5098. track_number: trackNumber,
  5099. total_tracks: totalTracks,
  5100. title: title,
  5101. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5102. trackArtist : undefined,
  5103. duration: duration,
  5104. url: identifiers.GOOGLE_ID ? undefined : response.finalUrl,
  5105. description: description,
  5106. identifiers: mergeIds(),
  5107. cover_url: imgUrl,
  5108. });
  5109. }
  5110. }
  5111. return tracks;
  5112. });
  5113. } else if (url.hostname.endsWith('7digital.com')) return globalFetch(url).then(function(response) {
  5114. if ((ref = response.document.querySelector('table.release-track-list')) != null)
  5115. identifiers['7DIGITAL_ID'] = parseInt(ref.dataset.releaseid) || ref.dataset.releaseid;
  5116. artist = Array.from(response.document.querySelectorAll('h2.release-info-artist > span[itemprop="byArtist"] > meta[itemprop="name"]'))
  5117. .map(node => node.content);
  5118. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5119. if ((ref = response.document.querySelector('h1.release-info-title')) != null) album = ref.textContent.trim();
  5120. if ((ref = response.document.querySelector('div.release-date-info > p')) != null) releaseDate = normalizeDate(ref.textContent);
  5121. if ((ref = response.document.querySelector('div.release-label-info > p')) != null) label = ref.textContent.trim();
  5122. response.document.querySelectorAll('dl.release-data > dt.release-data-label').forEach(function(dt) {
  5123. if (/\bGenres?:/.test(dt.textContent)) genres = Array.from(dt.nextElementSibling.querySelectorAll('a')).map(a => a.textContent.trim());
  5124. });
  5125. //getDescription(response, 'div.album-info', false);
  5126. if ((ref = response.document.querySelector('img[itemprop="image"]')) != null) imgUrl = ref.src;
  5127. totalTracks = response.document.querySelectorAll('table.release-track-list > tbody > tr.release-track').length;
  5128. response.document.querySelectorAll('table.release-track-list').forEach(function(table) {
  5129. discSubtitle = discNumber = undefined;
  5130. if ((ref = table.querySelector('caption > h4.release-disc-info')) != null) {
  5131. discSubtitle = ref.textContent.trim();
  5132. guessDiscNumber();
  5133. }
  5134. table.querySelectorAll('tbody > tr.release-track').forEach(function(tr) {
  5135. trackIdentifiers = {};
  5136. if (tr.dataset.trackid) trackIdentifiers.TRACK_ID = parseInt(tr.dataset.trackid) || tr.dataset.trackid;
  5137. tracks.push({
  5138. artist: isVA ? VA : undefined,
  5139. artists: !isVA ? artist : undefined,
  5140. album: album,
  5141. //album_year: extractYear(releaseDate),
  5142. release_date: releaseDate,
  5143. label: label,
  5144. catalog: catalogue,
  5145. media: media,
  5146. genre: genres.join('; '),
  5147. disc_number: discNumber,
  5148. total_discs: totalDiscs,
  5149. disc_subtitle: discSubtitle,
  5150. track_number: (ref = tr.querySelector('td.release-track-preview > em.release-track-preview-text')) != null ?
  5151. ref.textContent.trim() : undefined,
  5152. total_tracks: totalTracks,
  5153. title: (ref = tr.querySelector('td.release-track-name > meta[itemprop="name"]')) != null ? ref.content : undefined,
  5154. duration: durationFromMeta(tr),
  5155. url: (ref = response.document.querySelector('head > meta[property="og:url"]')) != null ?
  5156. ref.content : response.finalUrl.replace(/\?.*$/, ''),
  5157. description: description,
  5158. identifiers: mergeIds(),
  5159. cover_url: imgUrl,
  5160. });
  5161. });
  5162. });
  5163. return tracks;
  5164. }); else if (url.hostname.endsWith('e-onkyo.com')) return globalFetch(url).then(function(response) {
  5165. if (/\/album\/(\w+)\/?$/.test(response.finalUrl)) identifiers.EONKYO_ID = RegExp.$1;
  5166. artist = Array.from(response.document.querySelectorAll('div.jacketDetailArea p.artistsName > a'))
  5167. .map(node => node.textContent.trim());
  5168. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5169. if ((ref = response.document.querySelector('div.jacketDetailArea p.packageTtl')) != null)
  5170. album = ref.firstChild.wholeText.trim();
  5171. if ((ref = response.document.querySelector('div.jacketDetailArea p.recordlabelName > a')) != null)
  5172. label = ref.textContent.trim();
  5173. if ((ref = response.document.querySelector('div.jacketDetailArea p.releaseDay > a')) != null)
  5174. releaseDate = normalizeDate(ref.textContent, 'jp');
  5175. if ((ref = response.document.querySelector('div.jacketDetailArea p.packageNoteDetail')) != null
  5176. && /^\s*(?:\(C\)|©)\s+(\d{4})\b/i.test(ref.lastChild.textContent)) albumYear = parseInt(RegExp.$1);
  5177. //getDescription(response, 'div#credit', true);
  5178. if (/\s+\(\s*(?:(\d+)[\-\s]*bit)?\s*\/?\s*(?:(\d+(?:\.\d+)?)\s*kHz)?\s*\)\s*$/i.test(album)) {
  5179. album = RegExp.leftContext;
  5180. bitdepth = parseInt(RegExp.$1) || undefined;
  5181. samplerate = parseFloat(RegExp.$2) * 1000;
  5182. }
  5183. let formats = [];
  5184. function enumFormats(elem) {
  5185. if ((matches = /(\w+)\s+(\d+(?:\.\d+)?)\s*([kM]Hz)\s*\/\s*(\d+)[\s\-]?bits?\b/.exec(elem.textContent)) == null)
  5186. return;
  5187. formats.push([
  5188. matches[1].toUpperCase(),
  5189. parseFloat(matches[2].replace(',', '.')) * 10**(matches[3] == 'kHz' ? 3 : matches[3] == 'MHz' ? 6 : 0),
  5190. parseInt(matches[4]),
  5191. ]);
  5192. }
  5193. response.document.querySelectorAll('div.purchaseInr > dl > dd > p.musicspec').forEach(enumFormats);
  5194. if (formats.length <= 0) response.document.querySelectorAll('select#ddlFileTypeCD > option').forEach(enumFormats);
  5195. getDescription(response, 'div#info > div.infoTxtArea', true);
  5196. let credits = [];
  5197. response.document.querySelectorAll('div#credit > p').forEach(function(p) {
  5198. var trackNumber = parseInt(p.firstChild.wholeText);
  5199. if (!(trackNumber > 0)) return;
  5200. let artists = {};
  5201. Array.from(p.getElementsByTagName('a')).map(a => a.textContent.trim()).forEach(function(artist) {
  5202. if (/^(.+?)\s*\[([^\[\]]+)\]$/.test(artist)) {
  5203. artist = RegExp.$1;
  5204. var role = RegExp.$2;
  5205. }
  5206. if (/^(?:(?:Background\s+)?(?:Vocals?|Vocalist)|(?:\w+\s)?Guitar|Bass|Drums|Piano|Keyboards|Strings|Percussion|Violin|Viola|Cello|Mellotron|Synthesizer)\b/i.test(role))
  5207. role = 'Performer';
  5208. if (/^(?:Author|(?:Composer)?Lyricist|Writer)$/i.test(role)) role = 'Composer';
  5209. if (/^(?:Executive\sProducer)$/i.test(role)) role = 'Producer';
  5210. if (artists[role] == undefined) artists[role] = [];
  5211. artists[role].pushUniqueCaseless(artist);
  5212. });
  5213. credits[trackNumber - 1] = artists;
  5214. });
  5215. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  5216. imgUrl = ref.content.replace(/\/s\d+\//, '/s0/');
  5217. trs = response.document.querySelectorAll('dl.musicList > dd.musicBox');
  5218. tracks = Array.from(trs).map(function(tr, index) {
  5219. trackNumber = (ref = tr.querySelector('div.musicListNo')) != null ? ref.textContent.trim() : index + 1;
  5220. let trackPerformers = [];
  5221. try {
  5222. let trackArtists = credits[trackNumber - 1];
  5223. trackArtist = trackArtists.MainArtist ? trackArtists.MainArtist : [];
  5224. var trackGuests = trackArtists.FeaturedArtist ?
  5225. trackArtists.FeaturedArtist.filter(artist => !trackArtist.includesCaseless(artist)) : [];
  5226. producer = trackArtists.Producer ? trackArtists.Producer : [];
  5227. composer = trackArtists.Composer ? trackArtists.Composer : [];
  5228. trackPerformers = trackArtists.Performer ? trackArtists.Performer : [];
  5229. } catch(e) { trackArtist = []; trackGuests = []; producer = []; composer = [] }
  5230. if (!isVA && artistsMatch([trackArtist, trackGuests], [artist, []])) { trackArtist = []; trackGuests = [] }
  5231. return {
  5232. artist: isVA ? VA : undefined,
  5233. artists: !isVA ? artist : undefined,
  5234. album: album,
  5235. album_year: albumYear,
  5236. release_date: releaseDate,
  5237. label: label,
  5238. catalog: catalogue,
  5239. encoding: 'lossless',
  5240. codec: formats.length > 0 && formats.map(format => format[0]).homogeneous() ? formats[0][0] : undefined,
  5241. samplerate: formats.length > 0 && formats.map(format => format[1]).homogeneous() ? formats[0][1] : undefined,
  5242. bitdepth: formats.length > 0 && formats.map(format => format[2]).homogeneous() ? formats[0][2] : undefined,
  5243. media: media,
  5244. track_number: trackNumber,
  5245. total_tracks: trs.length,
  5246. title: (ref = tr.querySelector('div.musicTtl > span')) != null ? ref.title || ref.textContent.trim() : undefined,
  5247. track_artists: trackArtist.length > 0 ? trackArtist : undefined,
  5248. track_guests: trackGuests.length > 0 ? trackGuests : undefined,
  5249. composers: composer.length > 0 ? composer : undefined,
  5250. producers: producer.length > 0 ? producer : undefined,
  5251. performers: trackPerformers.length > 0 ? trackPerformers : undefined,
  5252. duration: (ref = tr.querySelector('div.musicTime')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
  5253. url: !identifiers.EONKYO_ID ? response.finalUrl : undefined,
  5254. description: description,
  5255. identifiers: mergeIds(),
  5256. cover_url: imgUrl,
  5257. };
  5258. });
  5259. return finalizeTracks();
  5260. }); else if (url.hostname.endsWith('store.acousticsounds.com')) return globalFetch(url).then(function(response) {
  5261. if (/\/(\d+)\/$/.test(response.finalUrl)) identifiers.ACOUSTICSOUNDS_ID = RegExp.$1;
  5262. artist = Array.from(response.document.querySelectorAll('div > h1 > a')).map(node => node.textContent.trim());
  5263. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5264. if (isVA) artist = [];
  5265. if ((ref = response.document.querySelector('div > h1')) != null) album = ref.lastChild.wholeText.trim().replace(/\s*-\s*/, '');
  5266. response.document.querySelectorAll('div > p > table > tbody > tr > td:first-of-type').forEach(function(td) {
  5267. if (/^(?:Label)\b/i.test(td.textContent)) label = td.nextElementSibling.textContent.trim();
  5268. if (/^(?:Genre)\b/i.test(td.textContent)) genres[0] = td.nextElementSibling.textContent.trim();
  5269. if (/^(?:Product\s+No)\b/i.test(td.textContent)) catalogue = td.nextElementSibling.textContent.trim();
  5270. if (/^(?:Category)\b/i.test(td.textContent)) {
  5271. if (/\b(\d+(?:\.\d+)?)\s*(?:kHz)\b/.test(td.nextElementSibling.textContent))
  5272. samplerate = parseFloat(RegExp.$1) * 1000;
  5273. if (/\b(\d+)[\s\-]?(?:bits?)\b/i.test(td.nextElementSibling.textContent))
  5274. bitdepth = parseInt(RegExp.$1);
  5275. if (/\b(FLAC|ALAC|WAV|DSD|AIFF)\b/i.test(td.nextElementSibling.textContent)) {
  5276. format = RegExp.$1;
  5277. encoding = 'lossless';
  5278. }
  5279. }
  5280. });
  5281. getDescription(response, 'div#description > p', true);
  5282. if ((ref = response.document.querySelector('div#detail > link[rel="image_src"]')) != null)
  5283. imgUrl = ref.href.replace(/\/medium\//i, '/xlarge/');
  5284. trs = response.document.querySelectorAll('div#tracks > table > tbody > tr');
  5285. return Array.from(trs).map(function(tr, index) {
  5286. title = (ref = tr.querySelector('td[nowrap]')) != null ? ref.textContent.trim() : undefined;
  5287. if ((matches = /^(\d+)(?:\s+\-|\.)\s+(.+)$/.exec(title)) != null) {
  5288. trackNumber = matches[1];
  5289. title = matches[2];
  5290. } else trackNumber = undefined;
  5291. return {
  5292. artist: isVA ? VA : undefined,
  5293. artists: !isVA ? artist : undefined,
  5294. album: album,
  5295. release_date: releaseDate,
  5296. label: label,
  5297. catalog: catalogue,
  5298. encoding: encoding,
  5299. codec: format,
  5300. bitdepth: bitdepth,
  5301. samplerate: samplerate || undefined,
  5302. media: media,
  5303. genre: genres.join('; '),
  5304. track_number: trackNumber || index + 1,
  5305. total_tracks: trs.length,
  5306. title: title,
  5307. url: !identifiers.ACOUSTICSOUNDS_ID ? response.finalUrl : undefined,
  5308. description: description,
  5309. identifiers: mergeIds(),
  5310. cover_url: imgUrl,
  5311. };
  5312. });
  5313. }); else if (url.hostname.endsWith('indies.eu')) return globalFetch(url).then(function(response) {
  5314. if (/\/alba\/(\d+)\//.test(response.finalUrl)) identifiers.INDIESSCOPE_ID = parseInt(RegExp.$1);
  5315. ref = response.document.querySelector(':root > body > div > div > div > h2');
  5316. if (ref != null) artist = Array.from(ref.childNodes).map(node => node.textContent.trim());
  5317. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5318. if ((ref = response.document.querySelector(':root > body > div > div > div > h1')) != null)
  5319. album = ref.textContent.trim();
  5320. if ((ref = response.document.querySelector('div.infoBox')) != null) {
  5321. let ndx = 0;
  5322. ref.childNodes.forEach(function(child) {
  5323. if (child.nodeName == 'BR') { ++ndx; return; }
  5324. switch (ndx) {
  5325. case 0:
  5326. if (child.nodeType == Node.TEXT_NODE) {
  5327. label = child.wholeText.trim();
  5328. if (/^(.*)\s+\/\s+(\d{4})$/.test(label)) {
  5329. label = RegExp.$1;
  5330. releaseDate = RegExp.$2;
  5331. }
  5332. }
  5333. break;
  5334. case 1:
  5335. if (child.nodeType == Node.ELEMENT_NODE) genres.push(child.textContent.trim());
  5336. break;
  5337. case 2:
  5338. if (child.nodeType == Node.ELEMENT_NODE) catalogue = child.textContent.trim();
  5339. break;
  5340. }
  5341. });
  5342. }
  5343. getDescription(response, 'div.popis > section', true);
  5344. if ((ref = response.document.querySelector('div.obrazekDetail > img')) != null) imgUrl = ref.src;
  5345. trs = response.document.querySelectorAll('table.skladby > tbody > tr');
  5346. return Array.from(trs).map(function(tr) {
  5347. title = undefined;
  5348. if ((ref = tr.querySelector('td.nazev')) != null) {
  5349. trackNumber = parseInt(ref.firstChild.wholeText);
  5350. title = ref.querySelector('strong').textContent.trim();
  5351. }
  5352. return {
  5353. artist: isVA ? VA : undefined,
  5354. artists: !isVA ? artist : undefined,
  5355. album: album,
  5356. release_date: releaseDate,
  5357. label: label,
  5358. catalog: catalogue,
  5359. codec: format,
  5360. media: media,
  5361. genre: genres.join('; '),
  5362. track_number: trackNumber,
  5363. total_tracks: trs.length,
  5364. title: title,
  5365. duration: (ref = tr.querySelector('td:nth-of-type(4)')) != null ? timeStringToTime(ref.textContent) : undefined,
  5366. identifiers: !identifiers.INDIESSCOPE_ID ? response.finalUrl : undefined,
  5367. description: description,
  5368. identifiers: mergeIds(),
  5369. cover_url: imgUrl,
  5370. };
  5371. });
  5372. }); else if (url.hostname.endsWith('classic.beatport.com')) return globalFetch(url).then(function(response) {
  5373. if (/\/release\/\S+\/(\d+)\b/i.test(response.finalUrl)) identifiers.BEATPORT_ID = parseInt(RegExp.$1);
  5374. artist = Array.from(response.document.querySelectorAll('div.release-detail div.block a[title]'))
  5375. .map(node => node.title || node.textContent.trim());
  5376. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5377. if ((ref = response.document.querySelector('div.release-detail h2')) != null) album = ref.textContent.trim();
  5378. response.document.querySelectorAll('table.meta-data > tbody > tr').forEach(function(tr) {
  5379. var key = tr.querySelector('td.meta-data-label'), value = tr.querySelector('td.meta-data-value');
  5380. if (key == null || value == null) return;
  5381. if (/^(?:Release\s+Date)\b/i.test(key.textContent)) releaseDate = value.textContent.trim();
  5382. if (/^(?:Label)/i.test(key.textContent))
  5383. label = Array.from(value.getElementsByTagName('a')).map(a => a.textContent.trim()).join(' / ');
  5384. if (/^(?:Catalog)/i.test(key.textContent)) catalogue = value.textContent.trim();
  5385. });
  5386. getDescription(response, 'p.description', true);
  5387. if ((ref = response.document.querySelector('meta[name="og:image"][content]')) != null) imgUrl = ref.content;
  5388. else if ((ref = response.document.querySelector('div.artwork')) != null)
  5389. imgUrl = 'https:' + ref.dataset.modalArtwork;
  5390. if (imgUrl) imgUrl = imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/image/');
  5391. trs = response.document.querySelectorAll('table.track-grid > tbody > tr.track-grid-content');
  5392. return Array.from(trs).map(function(tr) {
  5393. if ((ref = tr.querySelector('span[data-json]')) != null) try {
  5394. var trackMeta = JSON.parse(ref.dataset.json);
  5395. if (trackMeta.type != 'track') console.warn('BeatPort invalid track type:', trackMeta);
  5396. } catch(e) {
  5397. trackMeta = {};
  5398. console.warn(e);
  5399. }
  5400. trackIdentifiers = {
  5401. TRACK_ID: trackMeta.id,
  5402. BPM: trackMeta.bpm,
  5403. };
  5404. if (!(title = trackMeta.title) && (ref = tr.querySelector('td.titleColumn span')) != null) {
  5405. title = ref.textContent.trim();
  5406. if (title && (ref = tr.querySelector('td.titleColumn span.padL')) != null)
  5407. title += ' (' + ref.textContent.trim() + ')';
  5408. }
  5409. if (trackMeta.artists) {
  5410. trackArtist = trackMeta.artists.filter(artist => artist.type == 'artist').map(artist => artist.name);
  5411. let unknownTypes = new Set(trackMeta.artists.map(artist => artist.type)
  5412. .filter(type => !['artist', 'remixer'].includes(type)));
  5413. if (unknownTypes.size > 0) console.warn('Beatport unknown artist types:', Array.from(unknownTypes.keys()));
  5414. } else trackArtist = Array.from(tr.querySelectorAll('td.titleColumn > span.artistList > a'))
  5415. .map(a => a.title || a.textContent.trim());
  5416. if ((ref = tr.querySelector(':scope > td:nth-of-type(3) > span')) != null
  5417. && /\b((?:\d+:)?\d+:\d+)\b(?:\s*\/\s*(\d+)\s*(?:BPM)\b)?/i.test(ref.textContent)) {
  5418. duration = timeStringToTime(RegExp.$1);
  5419. if (!trackIdentifiers.BPM) trackIdentifiers.BPM = parseInt(RegExp.$2);
  5420. } else duration = undefined;
  5421. return {
  5422. artist: isVA ? VA : undefined,
  5423. artists: !isVA ? artist : undefined,
  5424. album: trackMeta.release ? trackMeta.release.name : album,
  5425. release_date: trackMeta.releaseDate || releaseDate || trackMeta.publishDate,
  5426. label: trackMeta.label ? trackMeta.label.name : label,
  5427. catalog: catalogue,
  5428. media: media,
  5429. genre: (trackMeta.genres ? trackMeta.genres.map(genre => genre.name)
  5430. : Array.from(tr.querySelectorAll('span.genreList > a'))
  5431. .map(a => a.title || a.textContent.trim())).join('; ') || undefined,
  5432. track_number: parseInt(tr.dataset.index) || tr.dataset.index
  5433. || ((ref = tr.querySelector('div.playColumn > span')) != null ?
  5434. parseInt(ref.textContent) || ref.textContent.trim() : undefined),
  5435. total_tracks: trs.length,
  5436. title: title,
  5437. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5438. trackArtist : undefined,
  5439. remixers: trackMeta.artists ?
  5440. trackMeta.artists.filter(artist => artist.type == 'remixer').map(artist => artist.name) : undefined,
  5441. duration: trackMeta.lengthMs ? trackMeta.lengthMs / 1000 : duration,
  5442. description: description,
  5443. url: response.finalUrl,
  5444. cover_url: imgUrl,
  5445. identifiers: mergeIds(),
  5446. };
  5447. });
  5448. }); else if (url.hostname.endsWith('beatport.com')) return globalFetch(url).then(function(response) {
  5449. if (/\/release\/(?:\d\/)?(?:\S+-)?(\d+)\b/i.test(response.finalUrl)) identifiers.BEATPORT_ID = RegExp.$1;
  5450. artist = Array.from(response.document.querySelectorAll('span > a[data-artist]')).map(node => node.textContent.trim());
  5451. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5452. if ((ref = response.document.querySelector('div > h1')) != null) album = ref.textContent.trim();
  5453. response.document.querySelectorAll('ul > li > span.category').forEach(function(span) {
  5454. if (/^(?:Release\s+Date)/i.test(span.textContent)) releaseDate = span.nextElementSibling.textContent.trim();
  5455. if (/^(?:Label)/i.test(span.textContent)) label = span.nextElementSibling.textContent.trim();
  5456. if (/^(?:Catalog)/i.test(span.textContent)) catalogue = span.nextElementSibling.textContent.trim();
  5457. });
  5458. getDescription(response, 'div.interior-expandable', true);
  5459. if ((ref = response.document.querySelector('div > img.interior-release-chart-artwork')) != null) imgUrl = ref.src;
  5460. trs = response.document.querySelectorAll('div.tracks > ul > li.track');
  5461. return Array.from(trs).map(function(tr) {
  5462. trackIdentifiers = { TRACK_ID: parseInt(tr.dataset.ecId) || tr.dataset.ecId };
  5463. title = (ref = tr.querySelector('span.buk-track-primary-title')) != null ?
  5464. ref.title || ref.textContent.trim() : tr.dataset.ecName;
  5465. if (title && (ref = tr.querySelector('span.buk-track-remixed')) != null) title += ' (' + ref.textContent.trim() + ')';
  5466. trackArtist = Array.from(tr.querySelectorAll('p.buk-track-artists > a')).map(a => a.textContent.trim());
  5467. if ((ref = tr.querySelector('p.buk-track-bpm')) != null) trackIdentifiers.BPM = parseInt(ref.textContent);
  5468. return {
  5469. artist: isVA ? VA : undefined,
  5470. artists: !isVA ? artist : undefined,
  5471. album: album,
  5472. release_date: releaseDate,
  5473. label: tr.dataset.ecBrand || ((ref = tr.querySelector('p.buk-track-labels')) != null ? ref.textContent.trim() : label),
  5474. catalog: catalogue,
  5475. codec: format,
  5476. media: media,
  5477. genre: Array.from(tr.querySelectorAll('p.buk-track-genre > a')).map(a => a.textContent).join('; '),
  5478. track_number: tr.dataset.ecPosition || ((ref = tr.querySelector('div.buk-track-num')) != null ?
  5479. ref.textContent.trim() : undefined),
  5480. total_tracks: trs.length,
  5481. title: title,
  5482. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5483. trackArtist : undefined,
  5484. remixers: Array.from(tr.querySelectorAll('p.buk-track-remixers > a')).map(a => a.textContent.trim()),
  5485. duration: (ref = tr.querySelector('p.buk-track-length')) != null ? timeStringToTime(ref.textContent) : undefined,
  5486. description: description,
  5487. url: !identifiers.BEATPORT_ID ? response.finalUrl : undefined,
  5488. cover_url: imgUrl,
  5489. identifiers: mergeIds(),
  5490. };
  5491. });
  5492. }); else if (url.hostname.endsWith('traxsource.com')) return globalFetch(url).then(function(response) {
  5493. if (/\/title\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.TRAXSOURCE_ID = RegExp.$1;
  5494. artist = Array.from(response.document.querySelectorAll('h1.artists > a.com-artists')).map(node => node.textContent.trim());
  5495. if (artist.length <= 0 && (ref = response.document.querySelector('h1.artists')) != null) artist = [ref.textContent.trim()];
  5496. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5497. if ((ref = response.document.querySelector('h1.title')) != null) album = ref.textContent.trim();
  5498. if ((ref = response.document.querySelector('a.com-label')) != null) label = ref.textContent.trim();
  5499. if ((ref = response.document.querySelector('div.cat-rdate')) != null && /^(.*)\s*\|\s*(.*)$/.test(ref.textContent.trim())) {
  5500. catalogue = RegExp.$1;
  5501. releaseDate = normalizeDate(RegExp.$2);
  5502. }
  5503. getDescription(response, 'div.desc', true);
  5504. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  5505. trs = response.document.querySelectorAll('div.trklist > div.trk-row');
  5506. return Array.from(trs).map(function(tr) {
  5507. trackIdentifiers = {};
  5508. title = (ref = tr.querySelector('div.title > a')) != null && ref.textContent.trim() || undefined;
  5509. if (title && (ref = tr.querySelector('span.version')) != null ) {
  5510. if (ref.firstChild.nodeType == Node.TEXT_NODE
  5511. && (i = ref.firstChild.wholeText.trim()).length > 0) title += ' (' + i + ')';
  5512. }
  5513. trackArtist = Array.from(tr.querySelectorAll('div.artists a.com-artists')).map(a => a.textContent.trim());
  5514. return {
  5515. artist: isVA ? VA : undefined,
  5516. artists: !isVA ? artist : undefined,
  5517. album: album,
  5518. release_date: releaseDate,
  5519. label: label,
  5520. catalog: catalogue,
  5521. media: media,
  5522. genre: Array.from(tr.querySelectorAll('div.genre > a')).map(a => a.textContent.trim()).join('; '),
  5523. track_number: (ref = tr.querySelector('div.tnum')) != null ? ref.textContent.trim() : undefined,
  5524. total_tracks: trs.length,
  5525. title: title,
  5526. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5527. trackArtist : undefined,
  5528. remixers: Array.from(tr.querySelectorAll('div.artists a.com-remixers')).map(a => a.textContent.trim()),
  5529. duration: (ref = tr.querySelector('span.duration')) != null ? timeStringToTime(ref.textContent) : undefined,
  5530. url: !identifiers.TRAXSOURCE_ID ? response.finalUrl : undefined,
  5531. description: description,
  5532. identifiers: mergeIds(),
  5533. cover_url: imgUrl,
  5534. };
  5535. });
  5536. }); else if (url.hostname.endsWith('music.apple.com')) return loadItunesMetadata(url).then(function(album) {
  5537. identifiers.APPLE_ID = parseInt(album.id) || album.id;
  5538. isVA = vaParser.test(album.attributes.artistName);
  5539. genres = album.attributes.genreNames.filter(genre => genre != 'Music');
  5540. label = album.attributes.recordLabel;
  5541. if (!label) label = album.attributes.copyright.replace(/^((?:[©℗]|\([PC]\))\s+)?(?:(\d{4})\s+)?/i, '');
  5542. //identifiers.EXPLICIT = Number(/^(?:explicit)$/i.test(album.attributes.contentRating));
  5543. if ('isCompilation' in album.attributes) identifiers.COMPILATION = Number(album.attributes.isCompilation);
  5544. if (album.attributes.isSingle) identifiers.RELEASETYPE = 'Single';
  5545. if (album.description) description = html2php(album.description, album.attributes.url).collapseGaps();
  5546. if (!description && album.attributes.editorialNotes)
  5547. description = html2php(domParser.parseFromString(album.attributes.editorialNotes.standard
  5548. || album.attributes.editorialNotes.short, 'text/html').body, album.attributes.url).replace(/\n/g, '\n\n')
  5549. .collapseGaps();
  5550. if (description && !description.includes('[/quote]')) description = '[quote]' + description + '[/quote]';
  5551. //if (description && !description.includes('[quote]')) description = '[quote]' + description.collapseGaps() + '[/quote]';
  5552. if (album.attributes.artwork && prefs.apple_offer_alt_cover)
  5553. addMessage(new HTML('<a href="' + album.attributes.artwork.realUrl +
  5554. '" target="_blank" style="' + hyperlinkStyle + '">Alternate cover URL</a>'), 'info');
  5555. return album.relationships.tracks.data.filter(track => track.type == 'songs').map(function(track) {
  5556. trackIdentifiers = {
  5557. TRACK_ID: parseInt(track.id),
  5558. ISRC: track.attributes.isrc,
  5559. EXPLICIT: Number(/^(?:explicit)$/i.test(track.attributes.contentRating)),
  5560. HASLYRICS: Number(track.attributes.hasLyrics || false),
  5561. };
  5562. var trackGenres = track.attributes.genreNames.filter(genre => genre != 'Music');
  5563. return {
  5564. artist: isVA ? VA : album.attributes.artistName,
  5565. artists: album.relationships.artists.data.map(artist => artist.attributes.name),
  5566. album: album.attributes.name,
  5567. release_date: album.attributes.releaseDate,
  5568. label: label,
  5569. media: media,
  5570. genre: (trackGenres.length > 0 ? trackGenres : genres).join('; '),
  5571. disc_number: track.attributes.discNumber,
  5572. disc_subtitle: track.attributes.workName,
  5573. track_number: track.attributes.trackNumber,
  5574. total_tracks: album.attributes.trackCount,
  5575. title: track.attributes.name,
  5576. track_artist: track.attributes.artistName
  5577. && (isVA || !artistsMatch(track.attributes.artistName, album.attributes.artistName)) ?
  5578. track.attributes.artistName : undefined,
  5579. composer: track.attributes.composerName,
  5580. duration: track.attributes.durationInMillis / 1000 || undefined,
  5581. description: description,
  5582. url: !identifiers.APPLE_ID ? album.attributes.url : undefined,
  5583. identifiers: mergeIds(),
  5584. //cover_url: album.attributes.artwork ? album.attributes.artwork.realUrl : undefined,
  5585. };
  5586. });
  5587. }); else if (url.hostname.endsWith('musicbrainz.org')) {
  5588. const entities = [
  5589. 'aliases', 'annotation', 'artist-credits', 'artists', 'collections', 'discids', 'genres',
  5590. 'isrcs', 'labels', 'media', 'ratings', 'recordings', 'release-groups', 'tags', 'url-rels',
  5591. ];
  5592. if (!mbrRlsParser.test(url)) return Promise.reject('Invalid MusicBrainz link - pick specific release');
  5593. return queryMusicBrainzAPI('release/' + RegExp.$1, { inc: entities.join('+') }).then(function(release) {
  5594. if (release.error) return Promise.reject(release.error);
  5595. if (prefs.diag_mode) console.debug('MusicBrainz release metadata received:', release);
  5596. if (release.id) identifiers.MBID = release.id;
  5597. if (release.barcode) identifiers.BARCODE = release.barcode;
  5598. if (release.asin) identifiers.ASIN = release.asin;
  5599. if (release['release-group']['primary-type']) identifiers.RELEASETYPE = release['release-group']['primary-type'];
  5600. artist = Array.isArray(release['artist-credit']) ? release['artist-credit'].map(artist => artist.name) : [];
  5601. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5602. if (Array.isArray(release.genres)) genres = release.genres.map(genre => genre.name);
  5603. if (Array.isArray(release.tags)) Array.prototype.push.apply(genres, release.tags.map(tag => tag.name));
  5604. if (genres.length <= 0) {
  5605. if (Array.isArray(release['release-group'].genres)) {
  5606. Array.prototype.push.apply(genres, release['release-group'].genres.map(tag => tag.name));
  5607. }
  5608. if (Array.isArray(release['release-group'].tags)) {
  5609. Array.prototype.push.apply(genres, release['release-group'].tags.map(tag => tag.name));
  5610. }
  5611. }
  5612. label = release['label-info'].map(label => label.label.name);
  5613. catalogue = release['label-info'].map(label => label['catalog-number']);
  5614. if (release['release-group'].status && !/^(?:Official)$/i.test(release['release-group'].status))
  5615. addMessage('Not an official release (' + release['release-group'].status + ')', 'warning');
  5616. release.media.forEach(function(medium, ndx) {
  5617. medium.tracks.forEach(function(track, ndx) {
  5618. trackIdentifiers = { TRACK_ID: track.id };
  5619. if (Array.isArray(track['artist-credit'])) {
  5620. trackArtist = track['artist-credit'].map(artist => artist.name);
  5621. trackArtist = trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist));
  5622. } else trackArtist = false;
  5623. tracks.push({
  5624. artist: isVA ? VA : undefined,
  5625. artists: !isVA ? artist : undefined,
  5626. album: /*release['release-group'].title || */release.title,
  5627. album_year: extractYear(release['release-group']['first-release-date']),
  5628. release_date: release.date,
  5629. genre: genres.join('; '),
  5630. label: label.filter(label => label).join(' / '),
  5631. catalog: catalogue.filter(catno => catno).join(' / '),
  5632. media: medium.format,
  5633. disc_number: medium.position,
  5634. disc_subtitle: medium.title,
  5635. total_discs: release.media.length,
  5636. track_number: track.number,
  5637. title: track.title,
  5638. track_artist: trackArtist ?
  5639. track['artist-credit'].map(artist => artist.name + artist.joinphrase).join('') : undefined,
  5640. duration: track.length != null ? track.length / 1000 : undefined,
  5641. //country: release.country,
  5642. description: release.annotation,
  5643. identifiers: mergeIds(),
  5644. });
  5645. });
  5646. });
  5647. return tracks;
  5648. });
  5649. } else if (url.hostname.endsWith('vgmdb.net')) return globalFetch(url).then(function(response) {
  5650. if (/\/album\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.VGMDB_ID = RegExp.$1;
  5651. if ((ref = response.document.querySelector('h1 > span.albumtitle[style="display:inline"]')) != null) {
  5652. album = ref.innerText.trim();
  5653. if (ref.lang == 'en'
  5654. && (ref = response.document.querySelector('div > span.albumtitle[style="display:inline"]')) != null
  5655. && ref.firstChild != null && ref.firstChild.nodeType == Node.TEXT_NODE)
  5656. album += ' (' + ref.firstChild.wholeText.trim() + ')';
  5657. }
  5658. composer = [];
  5659. response.document.querySelectorAll('table#album_infobit_large > tbody > tr > td > span.label > b').forEach(function(key) {
  5660. var value = key.parentNode.parentNode.nextElementSibling;
  5661. switch (key.innerText.trim().toLowerCase()) {
  5662. case 'catalog number':
  5663. catalogue = value.textContent.trim().replace(/\s*\([^\(\)]+\)$/, '');
  5664. break;
  5665. case 'release date':
  5666. if (value.firstElementChild != null) releaseDate = value.firstElementChild.innerText.trim();
  5667. break;
  5668. case 'media format':
  5669. media = value.textContent.trim();
  5670. break;
  5671. case 'classification':
  5672. genres = value.textContent.trim().split(/\s*,\*/);
  5673. break;
  5674. case 'published by':
  5675. label = Array.from(value.querySelectorAll('a > span.productname:first-of-type'))
  5676. .map(span => span.innerText.trim()).join(' / ');
  5677. break;
  5678. case 'composed by':
  5679. case 'lyrics by':
  5680. getArtists(value).forEach(artist => { composer.pushUniqueCaseless(artist) });
  5681. break;
  5682. case 'performed by':
  5683. artist = getArtists(value);
  5684. break;
  5685. case 'arranged by':
  5686. var arrangers = getArtists(value);
  5687. break;
  5688. }
  5689. });
  5690. if (!artist || artist.length <= 0) artist = composer;
  5691. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5692. genres.pushUniqueCaseless('Soundtrack');
  5693. response.document.querySelectorAll('td#rightcolumn > div > div > div > b.label').forEach(function(key) {
  5694. var value = key.parentNode.lastChild;
  5695. if (key.innerText.toLowerCase() == 'category' && value != null)
  5696. genres.pushUniqueCaseless(value.textContent.trim());
  5697. });
  5698. getDescription(response, 'div#notes', false);
  5699. if ((ref = response.document.querySelector('div#coverart')) != null
  5700. && /\burl\s*\(\"(.*)"\)/i.test(ref.style['background-image'])) imgUrl = RegExp.$1;
  5701. response.document.querySelectorAll('div#tracklist > span > span > b').forEach(function(node) {
  5702. discSubtitle = node.innerText.trim();
  5703. guessDiscNumber();
  5704. node = node.parentNode;
  5705. while (node != null && node.nodeName != 'TABLE') node = node.nextElementSibling;
  5706. if (node != null) addVolume(node);
  5707. });
  5708. var tl = Array.from(response.document.querySelectorAll('ul#tlnav > li > a'));
  5709. if (tl.length <= 1) return tracks;
  5710. if ((i = tracks.length / tl.length) != Math.floor(i)) {
  5711. console.warn('Unexpected vgmdb.net tracklist length:', i, tracks);
  5712. return tracks;
  5713. }
  5714. let enIndex = tl.findIndex(l => /^(?:English)\b/i.test(l.innerText.trim()));
  5715. if (enIndex < 0) enIndex = tl.findIndex(l => /^(?:Romaji)\b/i.test(l.innerText.trim()));
  5716. if (enIndex < 0) return tracks.slice(0, i);
  5717. let jpIndex = tl.findIndex(l => /^(?:Japanese)\b/i.test(l.innerText.trim()));
  5718. if (jpIndex < 0) jpIndex = enIndex > 0 ? 0 : 1;
  5719. return tracks.slice(enIndex * i, (enIndex + 1) * i).map(function(track, ndx) {
  5720. const rx = /^(.+?)(?:\s+\(([^\(\)]+)\))?$/;
  5721. if (!track.title) track.title = tracks[jpIndex * i + ndx].title;
  5722. else if ((jpTitle = tracks[jpIndex * i + ndx].title) != track.title) {
  5723. track.title += ' (';
  5724. var enTitle = rx.exec(track.title), jpTitle = rx.exec(jpTitle);
  5725. if (jpTitle[1] != enTitle[1]) {
  5726. track.title += jpTitle[1];
  5727. if (jpTitle[2] && jpTitle[2] != enTitle[2]) track.title += ' (' + jpTitle[2] + ')';
  5728. } else track.title += jpTitle[2];
  5729. track.title += ')';
  5730. }
  5731. return track;
  5732. });
  5733.  
  5734. function addVolume(node) {
  5735. Array.prototype.push.apply(tracks, Array.from(node.querySelectorAll('tbody > tr')).map(tr => ({
  5736. artist: isVA ? VA : undefined,
  5737. artists: !isVA ? artist : undefined,
  5738. album: album,
  5739. //album_year: extractYear(releaseDate),
  5740. release_date: releaseDate,
  5741. label: label,
  5742. catalog: catalogue,
  5743. media: media,
  5744. genre: genres.join('; '),
  5745. disc_number: discNumber,
  5746. //total_discs: totalDiscs,
  5747. disc_subtitle: discSubtitle,
  5748. track_number: (ref = tr.querySelector('span.label')) != null ? parseInt(ref.innerText) : undefined,
  5749. //total_tracks: trs.length,
  5750. title: tr.children[1].innerText.trim(),
  5751. //track_artist: joinArtists(trackArtist),
  5752. composers: composer,
  5753. duration: (ref = tr.querySelector('span.time')) != null ? timeStringToTime(ref.innerText) : undefined,
  5754. url: !identifiers.VGMDB_ID ? response.finalUrl : undefined,
  5755. description: description,
  5756. identifiers: mergeIds(),
  5757. cover_url: imgUrl,
  5758. })));
  5759. }
  5760.  
  5761. function getArtists(node) {
  5762. var artists = [];
  5763. node.childNodes.forEach(function(node) {
  5764. switch (node.nodeType) {
  5765. case Node.ELEMENT_NODE:
  5766. if ((i = node.querySelectorAll('span.artistname')).length > 0) {
  5767. var artist = i[0].innerText.trim();
  5768. if (i.length > 1 && i[0].lang == 'en') artist += ' (' + i[1].innerText.trim() + ')';
  5769. } else artist = node.innerText.trim();
  5770. if (artist) artists.push(artist);
  5771. break;
  5772. case Node.TEXT_NODE:
  5773. artist = node.wholeText.trim().replace(/^\s*,\s*|\s*,\s*$/g, '');
  5774. if (/^[\(\)]+$/.test(artist)) return;
  5775. if (artist) Array.prototype.push.apply(artists, artist.split(/\s*,\s*/));
  5776. break;
  5777. }
  5778. });
  5779. return artists;
  5780. }
  5781. }); else if (url.hostname.endsWith('tidal.com')) {
  5782. if (!(/\/album\/(\d+)(?:\/|$)/i.test(url) && !/\b(?:albumId)=(\d+)\b/i.test(url)))
  5783. return Promise.reject('Fetching from this page is not supported');
  5784. return queryTidalAPI('pages/album', { albumId: RegExp.$1 }).then(function(album) {
  5785. var albumHeader = findModule('ALBUM_HEADER');
  5786. if (albumHeader == null) return Promise.reject('Album header not found');
  5787. var albumItems = findModule('ALBUM_ITEMS');
  5788. if (albumItems == null) return Promise.reject('Album items not found');
  5789. identifiers.TIDAL_ID = albumHeader.album.id;
  5790. artist = albumHeader.album.artists.map(artist => artist.name);
  5791. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5792. identifiers.RELEASETYPE = albumHeader.album.type;
  5793. if (/^(?:(?:\([PC]\)|©|℗)\s+)?(?:(\d{4})\s+)?(.*)/.test(albumHeader.album.copyright)) {
  5794. //if (RegExp.$1) albumYear = parseInt(RegExp.$1);
  5795. label = RegExp.$2;
  5796. }
  5797. var channels;
  5798. description = albumHeader.description;
  5799. if (albumHeader.review.text) {
  5800. if (description) description += '\n\n';
  5801. if (!albumHeader.review.source) description += '[b]Album Review[/b]\n\n';
  5802. description += '[quote';
  5803. if (albumHeader.review.source) description += '=Album review from ' + albumHeader.review.source;
  5804. description += ']' + albumHeader.review.text + '[/quote]';
  5805. description = description
  5806. .replace(/\[wimpLink\s+artistId="(\d+)"\]/g, '[url=https://listen.tidal.com/artist/$1]')
  5807. .replace(/\[wimpLink\s+albumId="(\d+)"\]/g, '[url=https://listen.tidal.com/album/$1]')
  5808. .replace(/\[\/wimpLink\]/g, '[/url]');
  5809. }
  5810. if (Array.isArray(albumHeader.credits.items) && albumHeader.credits.items.length > 0) {
  5811. let ac = '';
  5812. albumHeader.credits.items.forEach(function(credit) {
  5813. if (/^Primary Artist$/i.test(credit.type)) return;
  5814. // if (/^Record label$/i.test(credit.type)) {
  5815. // label = credit.contributors.map(contributor => contributor.name).join(' / ');
  5816. // return;
  5817. // }
  5818. ac += '\n' + credit.type + ' – ' + joinArtists(credit.contributors.map(contributor =>
  5819. !contributor.id ? contributor.name :
  5820. '[url=https://listen.tidal.com/artist/' + contributor.id + ']' + contributor.name + '[/url]'));
  5821. });
  5822. if (ac.length > 0) {
  5823. if (description) {
  5824. if (!albumHeader.review.text) description += '\n';
  5825. description += '\n';
  5826. }
  5827. description += '[b]Additional Credits[/b]\n' + ac;
  5828. }
  5829. }
  5830. if (albumHeader.album.cover)
  5831. imgUrl = 'https://resources.tidal.com/images/' + albumHeader.album.cover.replace(/-/g, '/') + '/1280x1280.jpg';
  5832. return albumItems.pagedList.items.map(function(track, ndx) {
  5833. if (track.type != 'track') return;
  5834. trackIdentifiers = {
  5835. TRACK_ID: track.item.id,
  5836. EXPLICIT: Number(track.item.explicit),
  5837. };
  5838. trackArtist = track.item.artists.map(artist => artist.name);
  5839. channels = undefined;
  5840. track.item.audioModes.forEach(function(audioMode) {
  5841. switch (audioMode.toLowerCase()) {
  5842. case 'stereo': channels = 2; break;
  5843. default: if (/\b(\d+)\.(\d+)\b/.test(audioMode)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
  5844. }
  5845. });
  5846. return {
  5847. artist: isVA ? VA : undefined,
  5848. artists: !isVA ? artist : undefined,
  5849. album: albumHeader.album.title,
  5850. album_year: albumYear,
  5851. release_date: albumHeader.album.releaseDate,
  5852. label: label,
  5853. media: media,
  5854. disc_number: track.item.volumeNumber,
  5855. total_discs: albumHeader.album.numberOfVolumes,
  5856. //disc_subtitle: discSubtitle,
  5857. track_number: track.item.trackNumber,
  5858. total_tracks: albumHeader.album.numberOfTracks,
  5859. title: track.item.title,
  5860. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5861. trackArtist : undefined,
  5862. encoding: ['HI_RES', 'LOSSLESS'].includes(track.item.audioQuality) ? 'lossless' : undefined,
  5863. duration: track.item.duration,
  5864. channels: channels,
  5865. album_gain: track.replayGain ? track.replayGain.toString() + ' dB' : undefined,
  5866. description: description,
  5867. url: !identifiers.TIDAL_ID ? albumHeader.album.url : undefined,
  5868. identifiers: mergeIds(),
  5869. cover_url: imgUrl,
  5870. };
  5871. });
  5872.  
  5873. function findModule(type) {
  5874. for (let row of album.rows) {
  5875. var result = row.modules.find(module => module.type == type);
  5876. if (result != undefined) return result;
  5877. }
  5878. return null;
  5879. }
  5880. });
  5881. } else if (url.hostname.endsWith('ototoy.jp')) return globalFetch(url).then(function(response) {
  5882. if (/\/p\/(\d+)(?=\/|\?|$)/i.test(response.finalUrl)) identifiers.OTOTOY_ID = parseInt(RegExp.$1);
  5883. artist = Array.from(response.document.querySelectorAll('span.album-artist > *'))
  5884. .map(node => node.textContent.trim());
  5885. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  5886. if ((ref = response.document.querySelector('h1.album-title')) != null) album = ref.textContent.trim();
  5887. if ((ref = response.document.querySelector('p.hqd-logo > span')) != null && (matches = /^(?:Audio\s+Format)\s*:\s*(.+)$/i.exec(ref.textContent.trim())) != null) {
  5888. if (/\b(\d+)[\s\-]?bit\s*\b/i.test(matches[1])) bitdepth = parseInt(RegExp.$1);
  5889. if (/\b(\d+(?:\.\d+)?)\s*kHz\b/i.test(matches[1])) samplerate = parseFloat(RegExp.$1) * 1000;
  5890. }
  5891. if (bitdepth >= 16) encoding = 'lossless';
  5892. if ((ref = response.document.querySelector('p.hqd-logo > a.lossless')) != null) encoding = 'lossless';
  5893. if ((ref = response.document.querySelector('p.release-day')) != null && /\b(\d{4})-(\d{2})-(\d{2})\b/.test(ref.textContent))
  5894. releaseDate = RegExp.lastMatch;
  5895. label = Array.from(response.document.querySelectorAll('p.label-name > a')).map(a => a.textContent.trim()).join(' / ');
  5896. if ((ref = response.document.querySelector('p.catalog-id')) != null && /\b(?:Catalog\s+number):\s*(.*)$/i.test(ref.textContent.trim()))
  5897. catalogue = RegExp.$1;
  5898. genres = Array.from(response.document.querySelectorAll('ul.tag-cloud > li > a.oty-btn-tag'))
  5899. .map(a => a.textContent.trim()).filter(genre => genre.length > 0);
  5900. getDescription(response, 'div.album-addendum > div.addendum-box', false);
  5901. if ((ref = response.document.querySelector('div#jacket-full-wrapper > img')) != null) imgUrl = ref.dataset.src || ref.src;
  5902. trs = response.document.querySelectorAll('table#tracklist > tbody > tr[class^="bg"]');
  5903. return Array.from(trs).map(function(tr, ndx) {
  5904. trackIdentifiers = {};
  5905. title = (ref = tr.querySelector('td.item > span[id^="title-"]')) != null ? ref.textContent.trim() : undefined;
  5906. if (ref != null && /^title-(\d+)$/.test(ref.id)) trackIdentifiers.TRACK_ID = parseInt(RegExp.$1);
  5907. trackArtist = Array.from(tr.querySelectorAll('td.item > span > a.artist')).map(a => a.textContent.trim());
  5908. return {
  5909. artist: isVA ? VA : undefined,
  5910. artists: !isVA ? artist : undefined,
  5911. album: album,
  5912. album_year: extractYear(releaseDate),
  5913. release_date: releaseDate,
  5914. label: label,
  5915. catalog: catalogue,
  5916. media: media,
  5917. genre: genres.join('; '),
  5918. disc_number: discNumber,
  5919. track_number: ndx + 1,
  5920. total_tracks: trs.length,
  5921. samplerate: samplerate || undefined,
  5922. bitdepth: bitdepth,
  5923. encoding: encoding,
  5924. title: title,
  5925. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5926. trackArtist : undefined,
  5927. duration: (ref = tr.querySelector(':scope > td.item:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined,
  5928. description: description,
  5929. url: !identifiers.OTOTOY_ID ? response.finalUrl : undefined,
  5930. cover_url: imgUrl,
  5931. identifiers: mergeIds(),
  5932. };
  5933. });
  5934. }); else if (url.hostname.endsWith('music.yandex.ru') && (/\/album\/(\d+)\b/i.test(url.pathname)
  5935. || /\b(?:album)=(\d+)\b/i.test(url.search))) return globalFetch('https://music.yandex.ru/handlers/album.jsx?album=' + RegExp.$1, { responseType: 'json' }).then(function(response) {
  5936. if (prefs.diag_mode) console.debug('Yandex Music metadata received:', response.response);
  5937. if (response.response.metaType && response.response.metaType != 'music') throw 'Not a music release';
  5938. identifiers.YANDEX_ID = response.response.id;
  5939. if (response.response.type) identifiers.RELEASETYPE = response.response.type;
  5940. artist = response.response.artists.filter(artist => !artist.composer).map(artist => artist.name);
  5941. composer = response.response.artists.filter(artist => artist.composer).map(artist => artist.name);
  5942. isVA = response.response.artists.length <= 0
  5943. || response.response.artists.length == 1 && response.response.artists.some(artist => artist.various);
  5944. album = response.response.title;
  5945. if (response.response.version) album += ' (' + response.response.version + ')';
  5946. response.response.volumes.forEach(function(volume, discNumber) {
  5947. Array.prototype.push.apply(tracks, volume.filter(track => track.type == 'music').map(function(track, trackNumber) {
  5948. trackIdentifiers = { TRACK_ID: parseInt(/*track.realId || */track.id) };
  5949. title = track.title;
  5950. if (track.version) title += ' (' + track.version + ')';
  5951. trackArtist = track.artists.filter(artist => !artist.composer).map(artist => artist.name);
  5952. var trackComposer = track.artists.filter(artist => artist.composer).map(artist => artist.name);
  5953. return {
  5954. artist: isVA ? VA : undefined,
  5955. artists: !isVA ? artist : undefined,
  5956. album: album,
  5957. album_year: response.response.year,
  5958. release_date: response.response.releaseDate.replace(/T.*$/, ''),
  5959. label: response.response.labels.map(label => label.name).join(' / '),
  5960. media: media,
  5961. genre: response.response.genre,
  5962. track_number: trackNumber + 1,
  5963. total_tracks: response.response.trackCount,
  5964. composers: trackComposer.length > 0 ? trackComposer : composer,
  5965. disc_number: discNumber + 1,
  5966. total_discs: response.response.volumes.length,
  5967. title: title,
  5968. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  5969. trackArtist : undefined,
  5970. duration: track.durationMs / 1000,
  5971. track_gain: track.normalization ? track.normalization.gain.toString() + ' dB' : undefined,
  5972. track_peak: track.normalization ? track.normalization.peak : undefined,
  5973. cover_url: 'https://' + response.response.coverUri.replace('/%%', '/m1000x1000'),
  5974. identifiers: mergeIds(),
  5975. };
  5976. }));
  5977. });
  5978. return tracks;
  5979. }); else if (url.hostname.endsWith('mora.jp') ) return loadMoraMetadata(url).then(function(packageMeta) {
  5980. if (prefs.diag_mode) console.debug('Mora.jp metadata loaded:', packageMeta);
  5981. if ([7].includes(packageMeta.mediaType)) throw 'Not music release (' + packageMeta.mediaType + ')';
  5982. artist = fmtKanaProp(packageMeta, 'artistName');
  5983. isVA = vaParser.test(artist);
  5984. album = fmtKanaProp(packageMeta, 'title');
  5985. if (packageMeta.bitPerSample) bitdepth = parseInt(packageMeta.bitPerSample);
  5986. if (packageMeta.samplingFreq) samplerate = parseInt(packageMeta.samplingFreq);
  5987. if (packageMeta.channelConf) channels = parseInt(packageMeta.channelConf);
  5988. if (packageMeta.materialNo) identifiers.MORA_ID = parseInt(packageMeta.materialNo);
  5989. if (packageMeta.msin) identifiers.MSIN = packageMeta.msin;
  5990. if (packageMeta.distPartNo) identifiers.DISTPARTNO = packageMeta.distPartNo;
  5991. if (packageMeta.fullsizeimage) imgUrl = packageMeta.packageUrl + packageMeta.fullsizeimage;
  5992. return packageMeta.trackList.map(function(track) {
  5993. trackIdentifiers = { TACK_ID: track.musicId, MSIN: track.msin, DISTPARTNO: track.distPartNo };
  5994. if (track.labelId) trackIdentifiers.LABEL_ID = track.labelId;
  5995. trackArtist = fmtKanaProp(track, 'artistName');
  5996. composer = fmtKanaProp(track, 'composer');
  5997. var trackLyricist = fmtKanaProp(track, 'lyrics');
  5998. if (trackLyricist) if (composer) composer += ' / ' + trackLyricist; else composer = trackLyricist;
  5999. switch (track.mediaFormatNo) {
  6000. case 10: format = 'AAC'; encoding = 'lossy'; var codecProfile = 'AAC-LC'; bitrate = 320; break;
  6001. //case 11: format = 'FLAC'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
  6002. case 12: format = 'FLAC'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
  6003. case 13: format = 'DSD'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
  6004. default: format = undefined; encoding = undefined; codecProfile = undefined; bitrate = undefined;
  6005. }
  6006. return {
  6007. artist: isVA ? VA : artist,
  6008. album: album,
  6009. //album_year: extractYear(releaseDate),
  6010. release_date: packageMeta.dispStartDate || packageMeta.dispStartDateStr || packageMeta.startDate,
  6011. label: packageMeta.labelcompanyname || packageMeta.displayLabelname || packageMeta.labelname,
  6012. catalog: packageMeta.cdPartNo/* || packageMeta.packageId */|| packageMeta.distPartNo,
  6013. media: media,
  6014. genre: genres.join('; '),
  6015. codec: format,
  6016. codec_profile: codecProfile,
  6017. encoding: encoding,
  6018. bitrate: /*track.bitPerSample * 1000 || */bitrate,
  6019. bitdepth: parseInt(track.bitPerSample) || bitdepth,
  6020. samplerate: parseInt(track.samplingFreq) || samplerate,
  6021. channels: parseInt(track.channelConf) || channels,
  6022. track_number: track.trackNo,
  6023. total_tracks: packageMeta.trackList.length,
  6024. composer: composer,
  6025. producer: fmtKanaProp(track, 'producer'),
  6026. arranger: fmtKanaProp(track, 'arranger'),
  6027. title: fmtKanaProp(track, 'title'),
  6028. track_artist: trackArtist && (isVA || !artistsMatch(trackArtist, artist)) ? trackArtist : undefined,
  6029. duration: track.duration,
  6030. description: packageMeta.metaDescription,
  6031. url: packageMeta.webUrl,
  6032. cover_url: imgUrl,
  6033. identifiers: mergeIds(),
  6034. master: packageMeta.master,
  6035. };
  6036. });
  6037.  
  6038. function fmtKanaProp(obj, propName) {
  6039. var result = (obj[propName] || '').trim(), kana = (obj[propName + 'Kana'] || '').trim();
  6040. if (kana && prefs.use_kana) if (result) result += ' (' + kana + ')'; else result = kana;
  6041. return result || undefined;
  6042. }
  6043. }); else if (url.hostname.endsWith('allmusic.com') && url.pathname.startsWith('/album/')) {
  6044. return globalFetch(url.href.replace(/\b(m[wr]\d{10})\b.+$/, '$1')).then(function(response) {
  6045. ref = response.document.querySelector('section.main-album a.album-title');
  6046. var mainAlbum = (ref != null ? globalFetch(ref.href).then((response, ref) => ({
  6047. artist: Array.from(response.document.querySelectorAll('h2[class$="album-artist"] > span[itemprop="name"]'))
  6048. .map(span => span.textContent.trim()),
  6049. album: (ref = response.document.querySelector('h1.album-title')) != null ? ref.textContent.trim() : undefined,
  6050. albumYear: (ref = response.document.querySelector('div.release-date > span')) != null ?
  6051. new Date(ref.textContent).getFullYear() || parseInt(ref.textContent) : undefined,
  6052. genres: Array.from(response.document.querySelectorAll('div.genre a')).map(a => a.textContent.trim()),
  6053. styles: Array.from(response.document.querySelectorAll('div.styles a')).map(a => a.textContent.trim()),
  6054. coverUrl: (ref = response.document.querySelector('meta[property="og:image"]')) != null ?
  6055. ref.content.replace(/\bf=\d+\b/, 'f=0') : undefined,
  6056. id: /\b(mw\d{10})\b/.test(response.finalUrl) && RegExp.$1 || undefined,
  6057. })) : Promise.reject(null)).catch(reason => ({}));
  6058. var _credits = { mainArtists: [], featured: [], credits: {} };
  6059. var credits = globalFetch(response.finalUrl + '/credits').then(function(response) {
  6060. response.document.querySelectorAll('section.credits > table > tbody > tr').forEach(function(tr) {
  6061. var name = tr.children[0].textContent.trim(), role = tr.children[1].textContent.trim();
  6062. if (role == 'Primary Artist') _credits.mainArtists.push(name);
  6063. else if (role == 'Featured Artist') _credits.featured.push(name);
  6064. else _credits.credits[name] = role;
  6065. });
  6066. return _credits;
  6067. }).catch(reason => _credits);
  6068. if (/\b(m[wr]\d{10})\b/.test(response.finalUrl)) identifiers.ALLMUSIC_ID = RegExp.$1;
  6069. artist = Array.from(response.document.querySelectorAll('h2[class$="-artist"] > span[itemprop="name"]'))
  6070. .map(span => span.textContent.trim());
  6071. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6072. album = (ref = response.document.querySelector('h1.release-title')
  6073. || response.document.querySelector('h1.album-title')) != null ? ref.textContent.trim() : undefined;
  6074. albumYear = (ref = response.document.querySelector('div.year')) != null ? parseInt(ref.textContent) : undefined;
  6075. ref = response.document.querySelector('div.release-date > span');
  6076. if (identifiers.ALLMUSIC_ID && identifiers.ALLMUSIC_ID.startsWith('mr')) {
  6077. releaseDate = ref.textContent.trim();
  6078. } else if (identifiers.ALLMUSIC_ID && identifiers.ALLMUSIC_ID.startsWith('mw')) {
  6079. albumYear = new Date(ref.textContent).getFullYear() || parseInt(ref.textContent) || albumYear;
  6080. }
  6081. label = Array.from(response.document.querySelectorAll('div.label a')).map(a => a.textContent.trim()).join(' / ');
  6082. catalogue = (ref = response.document.querySelector('div.catalog-number > span')) != null ? ref.textContent.trim() : undefined;
  6083. if ((ref = response.document.querySelector('div.format > span')) != null) media = ref.textContent.trim();
  6084. genres = Array.from(response.document.querySelectorAll('div.genre a')).map(a => a.textContent.trim());
  6085. var styles = Array.from(response.document.querySelectorAll('div.styles a')).map(a => a.textContent.trim());
  6086. getDescription(response, 'section.review', false);
  6087. var releaseInfo = [];
  6088. if ((ref = response.document.querySelector('div.recording-date > div')) != null)
  6089. releaseInfo.push('Recording date: ' + ref.textContent.trim());
  6090. var locations = Array.from(response.document.querySelectorAll('div.recording-location > ul > li')).map(li => li.textContent.trim());
  6091. if (locations.length > 0) releaseInfo.push('Recording location: ' + locations.join(' / '));
  6092. locations = Array.from(response.document.querySelectorAll('div.release-info > ul > li')).map(li => li.textContent.trim());
  6093. if (locations.length > 0) releaseInfo.push('Release info: ' + locations.join(', '));
  6094. if (releaseInfo.length > 0) {
  6095. if (description) description += '\n\n';
  6096. description += releaseInfo.join('\n');
  6097. }
  6098. imgUrl = (ref = response.document.querySelector('meta[property="og:image"]')) != null ?
  6099. ref.content.replace(/\bf=\d+\b/, 'f=0') : undefined;
  6100. trs = response.document.querySelectorAll('section.track-listing table > tbody > tr.track');
  6101. return Promise.all([mainAlbum, credits]).then(function(workers) {
  6102. if (Object.keys(workers[1].credits).length > 0) {
  6103. if (description) description += '\n\n';
  6104. description = description + '[b]Credits:[/b]\n' + Object.keys(workers[1].credits)
  6105. .map(artist => artist + ' - ' + workers[1].credits[artist]).join('\n');
  6106. }
  6107. return Array.from(trs).map(function(tr, ndx) {
  6108. trackArtist = Array.from(tr.querySelectorAll('td.performer div.primary > a')).map(a => a.textContent.trim());
  6109. var trackGuests = Array.from(tr.querySelectorAll('td.performer div.featuring > a')).map(a => a.textContent.trim());
  6110. var ta = trackArtist.length > 0 && (isVA || !artistsMatch([trackArtist, trackGuests], [artist]));
  6111. if ((ref = tr.querySelector('div.title > a')) != null && ref.dataset.tooltip) try {
  6112. trackIdentifiers = { TRACK_ID: JSON.parse(ref.dataset.tooltip).id };
  6113. } catch(e) { trackIdentifiers = {} }
  6114. return {
  6115. artist: isVA ? VA : undefined,
  6116. artists: !isVA ? artist : undefined,
  6117. album: album,
  6118. release_date: releaseDate,
  6119. album_year: workers[0].albumYear || albumYear,
  6120. genre: (workers[0].genres || []).concat((workers[0].styles || []), genres, styles).join('; '),
  6121. label: label,
  6122. catalog: catalogue,
  6123. media: media,
  6124. disc_number: (ref = tr.parentNode.parentNode.parentNode.querySelector('h3')) != null
  6125. && /\b(?:Disc)\s+(\d+)\b/i.test(ref.textContent.trim()) ? parseInt(RegExp.$1) : undefined,
  6126. disc_subtitle: (ref = tr.parentNode.querySelector('tr.performance-title')) != null ?
  6127. ref.textContent.trim() : undefined,
  6128. track_number: (ref = tr.querySelector('td.tracknum')) != null ? ref.textContent.trim() : undefined,
  6129. total_tracks: trs.length,
  6130. title: (ref = tr.querySelector('div.title')) != null ? ref.textContent.trim() : undefined,
  6131. track_artists: ta ? trackArtist : undefined,
  6132. track_guests: ta ? trackGuests : undefined,
  6133. composers: Array.from(tr.querySelectorAll('div.composer > *')).map(node => node.textContent.trim()) || undefined,
  6134. duration: (ref = tr.querySelector('td.time')) != null && timeStringToTime(ref.textContent) || undefined,
  6135. description: description || undefined,
  6136. url: !identifiers.ALLMUSIC_ID ?
  6137. (ref = tr.querySelector('meta[property="og:url"]')) != null ? ref.content : response.finalUrl : undefined,
  6138. cover_url: workers[0].coverUrl || imgUrl,
  6139. identifiers: mergeIds(),
  6140. };
  6141. });
  6142. });
  6143. });
  6144. } else if (url.hostname.endsWith('bleep.com')) return globalFetch(url).then(function(response) {
  6145. if (/\/release\/(\d+)/i.test(response.finalUrl)) identifiers.BLEEP_ID = parseInt(RegExp.$1);
  6146. artist = Array.from(response.document.querySelectorAll('div.product-details dl > dd.artist > a'))
  6147. .map(a => a.title || a.textContent.trim());
  6148. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6149. if ((ref = response.document.querySelector('div.product-details dl > dd.release-title')) != null)
  6150. album = ref.textContent.trim();
  6151. label = Array.from(response.document.querySelectorAll('div.product-details dl > dd.label > a'))
  6152. .map(a => a.title || a.textContent.trim()).join(' / ');
  6153. if ((ref = response.document.querySelector('div.product-details dl > dd.catalogue-number')) != null)
  6154. catalogue = ref.textContent.trim();
  6155. if ((ref = response.document.querySelector('div.product-details dl > dd.product-release-date')) != null)
  6156. releaseDate = normalizeDate(ref.textContent.trim());
  6157. genres = Array.from(response.document.querySelectorAll('ul.tag-list > li > a.tag'))
  6158. .map(a => a.textContent.trim()).filter(genre => !genre.startsWith('Album of'));
  6159. getDescription(response, 'article[itemprop="description"]', false);
  6160. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  6161. imgUrl = ref.content.replace(/\/r\/[a-z]\//i, '/r/');
  6162. trs = response.document.querySelectorAll('ol#track-list > li.track');
  6163. return Array.from(trs).map(function(tr, ndx) {
  6164. trackIdentifiers = {};
  6165. trackArtist = Array.from(tr.querySelectorAll('span.track-artist > a[itemprop="byArtist"]'))
  6166. .map(a => a.title || a.textContent.trim());
  6167. return {
  6168. artist: isVA ? VA : undefined,
  6169. artists: !isVA ? artist : undefined,
  6170. album: album,
  6171. release_date: releaseDate,
  6172. label: label,
  6173. catalog: catalogue,
  6174. genre: genres.join('; '),
  6175. media: media,
  6176. track_number: (ref = tr.querySelector('span.track-number')) != null ? parseInt(ref.textContent) : undefined,
  6177. total_tracks: trs.length,
  6178. title: (ref = tr.querySelector('span.track-name span[itemprop="name"]')) != null ?
  6179. ref.textContent.trim() : undefined,
  6180. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  6181. trackArtist : undefined,
  6182. duration: (ref = tr.querySelector('span.track-duration')) != null ?
  6183. timeStringToTime(ref.textContent) : undefined,
  6184. description: description,
  6185. url: !identifiers.BLEEP_ID ? response.finalUrl : undefined,
  6186. cover_url: imgUrl,
  6187. identifiers: mergeIds(),
  6188. };
  6189. });
  6190. }); else if (url.hostname.endsWith('boomkat.com') && url.pathnname.startsWith('/products/')) return globalFetch(url).then(function(response) {
  6191. artist = Array.from(response.document.querySelectorAll('div#right_content > h1.detail--artists > a'))
  6192. .map(a => a.textContent.trim());
  6193. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6194. if ((ref = response.document.querySelector('div#right_content > h2.detail_album')) != null) album = ref.textContent.trim();
  6195. genres = Array.from(response.document.querySelectorAll('div#right_content > div.product-note > span:last-of-type'))
  6196. .map(a => a.textContent.trim().replace(/^(?:Genre)\s*:\s*/i, ''));
  6197. getDescription(response, response.document.querySelector('div.show-for-medium-up > div.product-review'), true);
  6198. if ((ref = response.document.querySelector('img[itemprop="image"]')) != null)
  6199. imgUrl = ref.src.replace(/\/(?:large)\//i, '/original/');
  6200. var m = /#v\d+/.exec(url);
  6201. if (m == null) return Promise.reject('Use tab link for specific medium');
  6202. if ((ref = response.document.querySelector('li.tab-title > a[href="' + m[0] + '"]')) != null) {
  6203. releaseDate = ref.dataset.releaseDate;
  6204. label = ref.dataset.label;
  6205. catalogue = ref.dataset.catalogueNumber;
  6206. switch (ref.textContent.trim()) {
  6207. case 'FLAC': media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 16; break;
  6208. case 'WAV': media = 'WEB'; encoding = 'lossless'; format = 'WAV'; bitdepth = 16; break;
  6209. case 'MP3': media = 'WEB'; encoding = 'lossy'; format = 'MP3'; break;
  6210. case 'CD': media = 'CD'; break;
  6211. case 'Cassette': media = 'Cassette'; break;
  6212. default:
  6213. if (/(?:LP)$/.test(ref.textContent)) media = 'Vinyl'; break;
  6214. }
  6215. }
  6216. if (media == 'WEB' && (ref = response.document.querySelector('div' + m[0] + ' p.product-extra-info')) != null
  6217. && /\b(\d+)\s+bit\s+audio\b/i.test(ref.textContent)) bitdepth = parseInt(RegExp.$1);
  6218. if ((ref = response.document.querySelector('div' + m[0] + ' div.product-track-listing')) == null)
  6219. return Promise.reject('invalid media link');
  6220. return globalFetch('https://boomkat.com/tracklist/' + ref.dataset.releaseFormatId).then(function(response) {
  6221. trs = response.document.querySelectorAll('div.tracklist > div.track > a.table-cell');
  6222. return Array.from(trs).map(function(tr, ndx) {
  6223. trackIdentifiers = {
  6224. BOOMKAT_ID: parseInt(tr.dataset.audioPlayerRelease),
  6225. MEDIA_ID: parseInt(tr.dataset.audioPlayerReleaseFormat),
  6226. TRACK_ID: parseInt(tr.dataset.audioPlayerTrack),
  6227. };
  6228. trackArtist = tr.dataset.artist;
  6229. return {
  6230. artist: isVA ? VA : undefined,
  6231. artists: !isVA ? artist : undefined,
  6232. album: album,
  6233. release_date: releaseDate,
  6234. label: label,
  6235. catalog: catalogue,
  6236. genre: genres.join('; '),
  6237. media: media,
  6238. encoding: encoding,
  6239. codec: format,
  6240. bitdepth: bitdepth,
  6241. samplerate: samplerate,
  6242. track_number: ndx + 1,
  6243. total_tracks: trs.length,
  6244. title: tr.dataset.name,
  6245. track_artist: trackArtist && (isVA || !artistsMatch(trackArtist, [artist])) ? trackArtist : undefined,
  6246. duration: parseFloat(tr.dataset.duration) || undefined,
  6247. description: description,
  6248. url: !trackIdentifiers.BOOMKAT_ID ? response.finalUrl : undefined,
  6249. cover_url: imgUrl,
  6250. identifiers: mergeIds(),
  6251. };
  6252. });
  6253. });
  6254. }); else if (url.hostname.endsWith('ecmrecords.com') && /^\/(?:catalogue|shop)\/(\d+)\b/i.test(url.pathname)) {
  6255. const appLink = 'https://www.ecmrecords.com/app';
  6256. let serial = parseInt(RegExp.$1), referer = 'https://www.ecmrecords.com/catalogue/' + serial;
  6257. return Promise.all([
  6258. globalFetch(`${appLink}/core/server_load.php?r=default&page=catalogue&serial=${serial}`, {
  6259. responseType: 'json',
  6260. headers: { 'Referer': referer },
  6261. }).then(response => response.response.items[0]),
  6262. globalFetch(`${appLink}/ajax/get_related_artists.php?serial=${serial}&targetvar=related_artists_and_products`, {
  6263. responseType: 'json',
  6264. headers: { 'Referer': referer },
  6265. }).then(response => response.response.related_artists),
  6266. globalFetch(`${appLink}/ajax/get_related_tracks.php?serial=${serial}&targetvar=parse_tracks`, {
  6267. responseType: 'json',
  6268. headers: { 'Referer': referer },
  6269. }).then(response => response.response),
  6270. ]).then(function(metaData) {
  6271. if (prefs.diag_mode) console.debug('ECM metadata loaded:', metaData);
  6272. identifiers.ECM_ID = metaData[0].serial;
  6273. if (metaData[0].barcode) identifiers.BARCODE = metaData[0].barcode; else {
  6274. i = metaData[0].multi_barcodes.toString().split('^');
  6275. if (i.homogeneous()) identifiers.BARCODE = i[0];
  6276. }
  6277. artist = metaData[1].map(relArtist => relArtist.name);
  6278. isVA = vaParser.test(metaData[0].main_artist);
  6279. i = metaData[0].multi_articlecodes.toString().split('^');
  6280. releaseDate = metaData[0].date_release || metaData[0].date_release_digital || metaData[0].date_release_presale
  6281. || metaData[0].date_release_expected || metaData[0].date_release_usa || metaData[0].date_release_uk
  6282. || metaData[0].date_release_jap || metaData[0].date_release_fr || metaData[0].date_release_de
  6283. || metaData[0].date_release_other; // ambiguity
  6284. if (i.homogeneous()) catalogue = i[0];
  6285. if (metaData[0].description) description = '[quote]' +
  6286. html2php(domParser.parseFromString(metaData[0].description, 'text/html').body, referer) + '[/quote]';
  6287. else description = '';
  6288. if (metaData[0].extra_data) try {
  6289. } catch(e) { console.debug(e) }
  6290. if (Array.isArray(metaData[0].related_press)) metaData[0].related_press.forEach(function(article) {
  6291. if (description) description += '\n';
  6292. var by = (article.writer + ' / ' + article.magazine).replace(/^ \/ $|^ \/|\/ $/g, '');
  6293. if (by) by = '\n\nby ' + by;
  6294. description += '[hide=' + article.title + ']' +
  6295. html2php(domParser.parseFromString(article.description, 'text/html').body, referer) + by + '[/hide]\n';
  6296. });
  6297. if (metaData[1].length > 0) {
  6298. if (description) description += '\n';
  6299. description += '[b]Personnel:[/b]\n' + metaData[1]
  6300. .map(relArtist => `[url=https://www.ecmrecords.com/${relArtist.link}]${relArtist.name}[/url]: ${relArtist.instrument || relArtist.role}`)
  6301. .join('\n');
  6302. }
  6303. return metaData[2].map(function(track) {
  6304. trackIdentifiers = { TRACK_ID: track.serial };
  6305. trackArtist = track.participants;
  6306. if (trackArtist && !isVA && artistsMatch(trackArtist, metaData[0].main_artist)) trackArtist = undefined;
  6307. return {
  6308. artist: isVA ? VA : metaData[0].main_artist,
  6309. album: metaData[0].title,
  6310. release_date: releaseDate ? normalizeDate(releaseDate) : undefined,
  6311. label: 'ECM Records',
  6312. catalog: catalogue || `${metaData[0].prefix} ${metaData[0].suffix}`,
  6313. genre: genres.join('; '),
  6314. track_number: parseInt(track.track_nr) || undefined,
  6315. disc_number: parseInt(track.cd_nr) || undefined,
  6316. disc_subtitle: track.movement ? track.title : undefined,
  6317. composer: track.composer,
  6318. track_artist: trackArtist,
  6319. performers: !isVA ? artist : undefined,
  6320. title: track.movement || track.title,
  6321. description: description.collapseGaps(),
  6322. url: !trackIdentifiers.ECM_ID ? referer : undefined,
  6323. cover_url: metaData[0].image_01_full,
  6324. identifiers: mergeIds(),
  6325. };
  6326. });
  6327. });
  6328. } else if (url.hostname.endsWith('actmusic.com')) return globalFetch(url.href.replace('actmusic.com/de/', 'actmusic.com/en/')).then(function(response) {
  6329. var enLink;
  6330. response.document.querySelectorAll('li > a.metanav_languageSwitch')
  6331. .forEach(a => { if (a.textContent.trim() == 'EN') enLink = 'https://www.actmusic.com' + a.pathname });
  6332. return enLink ? globalFetch(enLink) : response;
  6333. }).then(function(response) {
  6334. if ((ref = response.document.querySelector('h1.album-detail_artisthead')) != null)
  6335. artist = ref.textContent.trim();
  6336. isVA = vaParser.test(artist);
  6337. if ((ref = response.document.querySelector('h2.album-detail_albumhead')) != null)
  6338. album = ref.textContent.trim().replace(/ - (?:CD|LP|Vinyl)$/, '');
  6339. response.document.querySelectorAll('ul.release-format-info > li').forEach(function(li) {
  6340. try {
  6341. var key = li.querySelector('span.release-format-info_label').textContent.trim().replace(/\s*:\s*$/, ''),
  6342. value = li.querySelector('span.release-format-info_value').textContent.trim();
  6343. } catch(e) {
  6344. console.debug(e);
  6345. return;
  6346. }
  6347. switch (key.toLowerCase()) {
  6348. case 'format': media = value; break;
  6349. case 'cat no.': catalogue = value; break;
  6350. case 'barcode': identifiers.BARCODE = value; break;
  6351. case 'release': case 'german release': releaseDate = normalizeDate(value, 'de'); break;
  6352. case 'genre': genres = value.split(/\s*,\s*/); break;
  6353. }
  6354. });
  6355. if ((ref = response.document.querySelector('div.album_cover_image')) != null
  6356. || /^url\([\'\"](.+)[\'\"]\)$/.test(ref.style.backgroundImage)) imgUrl = RegExp.$1;
  6357. trs = response.document.querySelectorAll('ol.tracklist > li');
  6358. return (function() {
  6359. if ((ref = response.document.querySelector('div.sh3 > h1.header_title > a.btn-arrow-right')) == null) {
  6360. getDescription(response, 'div.col-infos', false);
  6361. return Promise.resolve(description);
  6362. }
  6363. return globalFetch('https://www.actmusic.com' + ref.pathname).then(function(response) {
  6364. description = [
  6365. html2php(response.document.querySelector('div.c-bio-wrap > div.c-bio-text'), response.finalUrl),
  6366. html2php(response.document.querySelector('div.c-bio-wrap > div.c-bio-sidebar'), response.finalUrl),
  6367. ].filter(description => Boolean(description)).join('\n\n').collapseGaps(), pdf = actPdfBooklet(response);
  6368. if (pdf) description += '\n\n' + pdf;
  6369. return description;
  6370. });
  6371. })().then(description => Array.from(trs).map((tr, ndx) => ({
  6372. artist: isVA ? VA : artist,
  6373. album: album,
  6374. release_date: releaseDate,
  6375. label: 'ACT Music',
  6376. catalog: catalogue,
  6377. genre: genres.join('; '),
  6378. media: media,
  6379. track_number: (ref = tr.querySelector('span.tracklist_tracknumber')) != null ?
  6380. parseInt(ref.textContent) : undefined,
  6381. total_tracks: trs.length,
  6382. title: (ref = tr.querySelector('span.tracklist_tracktitle')) != null ? ref.textContent.trim() : undefined,
  6383. composer: (ref = tr.querySelector('span.tracklist_credits')) != null
  6384. && /^\s*\(\s*(.+?)\s*\)\s*$/.test(ref.textContent) ? RegExp.$1 : undefined,
  6385. duration: (ref = tr.querySelector('span.tracklist_trackduration')) != null ?
  6386. timeStringToTime(ref.textContent) : undefined,
  6387. description: description,
  6388. url: response.finalUrl,
  6389. cover_url: imgUrl,
  6390. identifiers: mergeIds(),
  6391. })));
  6392. }); else if (url.hostname.endsWith('jpc.de') && url.pathname.startsWith('/jpcng/')) {
  6393. let params = new URLSearchParams(url.search);
  6394. params.set('lang', 'en');
  6395. url.search = params;
  6396. return globalFetch(url).then(function(response) {
  6397. if ((ref = response.document.querySelector('div.box.by > a')) != null) artist = ref.textContent.trim();
  6398. isVA = vaParser.test(artist);
  6399. if ((ref = response.document.querySelector('div.box.title')) != null) album = ref.textContent.trim();
  6400. if ((ref = response.document.querySelector('div.box.medium > em')) != null) media = ref.textContent.trim();
  6401. response.document.querySelectorAll('div.box.detailinfo > ul > li > b').forEach(function(b) {
  6402. switch (b.textContent.trim().toLowerCase()) {
  6403. case 'label:': label = b.nextElementSibling.textContent.trim(); break;
  6404. case 'bestellnummer:': case 'order number:': catalogue = b.nextElementSibling.textContent.trim(); break;
  6405. case 'erscheinungstermin:': case 'release date:': releaseDate = normalizeDate(b.nextSibling.textContent, 'de'); break;
  6406. }
  6407. });
  6408. getDescription(response, 'div.box.textlink > div[data-pd="j"]', true);
  6409. if (description && (ref = response.document.querySelector('div.rear-image > a.mfp-image')) != null)
  6410. description += `\n\n[img]${ref.href.replace(/\/w\d+\//i, '/w9999/')}[/img]`;
  6411. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  6412. imgUrl = ref.content.replace(/\/w\d+\//i, '/w9999/');
  6413. trs = response.document.querySelectorAll('div.playlist > ol > li[itemprop="track"]');
  6414. response.document.querySelectorAll('div.playlist').forEach(function(playlist, discNumber, nl) {
  6415. discSubtitle = (ref = playlist.querySelector(':scope > h4')) != null ? ref.textContent.trim() : undefined;
  6416. Array.prototype.push.apply(tracks, Array.from(playlist.querySelectorAll('ol > li[itemprop="track"]')).map((tr, ndx) => ({
  6417. artist: isVA ? VA : artist,
  6418. album: album,
  6419. release_date: releaseDate,
  6420. label: label,
  6421. catalog: catalogue,
  6422. media: media,
  6423. disc_number: discNumber + 1,
  6424. total_discs: nl.length,
  6425. disc_subtitle: discSubtitle,
  6426. track_number: (ref = tr.querySelector('strong')) != null ? parseInt(ref.textContent) : ndx + 1,
  6427. total_tracks: trs.length,
  6428. title: (ref = tr.querySelector('small[itemprop="name"]')) != null ? ref.textContent.trim() : undefined,
  6429. description: description,
  6430. url: response.finalUrl,
  6431. cover_url: imgUrl,
  6432. identifiers: mergeIds(),
  6433. })));
  6434. });
  6435. return tracks;
  6436. });
  6437. } else if (url.hostname.endsWith('pias.com') && url.pathname.startsWith('/release/')) return globalFetch(url).then(function(response) {
  6438. if (/\/release\/(\d+)\b/i.test(url.pathname)) identifiers.PIAS_ID = parseInt(RegExp.$1);
  6439. artist = Array.from(response.document.querySelectorAll('div.product-details > div.product-info > dl > dd.artist > a'))
  6440. .map(a => a.title || a.textContent.trim());
  6441. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6442. if ((ref = response.document.querySelector('div.product-details > div.product-info > dl > dd.release-title')) != null)
  6443. album = ref.textContent.trim();
  6444. label = Array.from(response.document.querySelectorAll('div.product-details > div.product-info > dl > dd.label > a'))
  6445. .map(a => a.title || a.textContent.trim()).join(' / ');
  6446. if ((ref = response.document.querySelector('div.product-details > div.product-info > dl > dd.catalogue-number')) != null)
  6447. catalogue = ref.textContent.trim();
  6448. if ((ref = response.document.querySelector('div.product-details > div.product-info > dl > dd.product-release-date')) != null)
  6449. releaseDate = normalizeDate(ref.textContent, 'be');
  6450. //getDescription(response, 'div.box.textlink > div[data-pd="j"]', true);
  6451. description = imageHosts.rehostImages(Array.from(response.document.querySelectorAll('ul.product-image-list > li.product-image-item > a > img.product-image'))
  6452. .map(img => img.src.replace(/\/[bl]\//i, '/'))).catch(reason => [])
  6453. .then(results => results.map(result => '[img]' + (typeof result == 'string' ? result
  6454. : typeof result == 'object' && result.original ? result.original : null) + '[/img]').join('\n'));
  6455. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  6456. imgUrl = ref.content.replace(/\/[sbl]\//i, '/');
  6457. else if ((ref = response.document.querySelector('img[itemprop="image"]')) != null)
  6458. imgUrl = ref.src.replace(/\/[sbl]\//i, '/');
  6459. trs = response.document.querySelectorAll('ol.track-list > li.track');
  6460. return description.then(description => Array.from(trs).map(function(li, index) {
  6461. trackIdentifiers = { TRACK_ID: li.dataset.id };
  6462. return {
  6463. artist: isVA ? VA : undefined,
  6464. artists: !isVA ? artist : undefined,
  6465. album: album,
  6466. release_date: releaseDate,
  6467. label: label,
  6468. catalog: catalogue,
  6469. media: media,
  6470. //disc_number: discNumber + 1,
  6471. //total_discs: nl.length,
  6472. //disc_subtitle: discSubtitle,
  6473. track_number: (ref = li.querySelector('span.track-number')) != null ? parseInt(ref.textContent) : ndx + 1,
  6474. total_tracks: trs.length,
  6475. title: (ref = li.querySelector('span[itemprop="name"]')) != null ?
  6476. ref.title || ref.textContent.trim() : undefined,
  6477. duration: (ref = li.querySelector('span.track-duration')) != null ?
  6478. timeStringToTime(ref.textContent) : undefined,
  6479. description: description || undefined,
  6480. url: !identifiers.PIAS_ID ? response.finalUrl : undefined,
  6481. cover_url: imgUrl,
  6482. identifiers: mergeIds(),
  6483. };
  6484. }));
  6485. }); else if (url.hostname.endsWith('hearnow.com')) return globalFetch(url).then(function(response) {
  6486. artist = (ref = response.document.querySelector('div.artist_name > a.artist_page_link')) != null ?
  6487. ref.textContent.trim() : undefined;
  6488. isVA = vaParser.test(artist);
  6489. if ((ref = response.document.querySelector('div.album_name_large')) != null)
  6490. album = ref.textContent.trim();
  6491. if ((ref = response.document.querySelector('div.release_date')) != null)
  6492. releaseDate = normalizeDate(ref.textContent.trim().replace(/^Released\s+/i, ''));
  6493. if ((ref = response.document.querySelector('div.album_cover > img.album_cover_image')) != null) imgUrl = ref.src;
  6494. trs = response.document.querySelectorAll('section#tracks > ul.playlinks > li');
  6495. return Array.from(trs).map(function(li, ndx) {
  6496. trackIdentifiers = { ISRC: li.dataset.isrc };
  6497. trackArtist = (ref = li.querySelector('div.track_artist_name')) != null ? ref.textContent.trim() : undefined;
  6498. return {
  6499. artist: isVA ? VA : artist,
  6500. album: album,
  6501. release_date: releaseDate,
  6502. media: media,
  6503. track_number: parseInt(li.dataset.tracknumber) || ndx + 1,
  6504. total_tracks: trs.length,
  6505. title: (ref = li.querySelector('div.track_name')) != null ? ref.textContent.trim() : undefined,
  6506. track_artist: trackArtist && (isVA || !artistsMatch(trackArtist, artist)) ? trackArtist : undefined,
  6507. duration: (ref = li.querySelector('div.track_duration')) != null ?
  6508. timeStringToTime(ref.textContent) : undefined,
  6509. url: response.finalUrl,
  6510. cover_url: imgUrl,
  6511. identifiers: mergeIds(),
  6512. };
  6513. });
  6514. }); else if (url.hostname.endsWith('dominomusic.com') && url.pathname.startsWith('/releases/')) return globalFetch(url, { responseType: 'text' }).then(function(response) {
  6515. if (!/\b(?:selected):\s*(\d+)\b/m.test(response.responseText)) throw 'Invalid page format';
  6516. identifiers.DOMINO_ID = parseInt(RegExp.$1);
  6517. if (!/\b(?:data):\s*({"releases":.*?}),$/m.test(response.responseText)) throw 'Invalid page format';
  6518. var release = JSON.parse(RegExp.$1).releases.filter(release => release.id == identifiers.DOMINO_ID);
  6519. if (release.length <= 0) throw 'Assertion failed: release not found';
  6520. identifiers.RELEASETYPE = release[0].release_type;
  6521. isVA = vaParser.test(release[0].artist);
  6522. description = html2php(domParser.parseFromString(release[0].description, 'text/html').body, response.finalUrl)
  6523. .collapseGaps();
  6524. if (Array.isArray(release[0].images))
  6525. imgUrl = Object.keys(release[0].images[0])
  6526. .reduce((acc, key) => release[0].images[0][key].width * release[0].images[0][key].height
  6527. > release[0].images[0][acc].width * release[0].images[0][acc].height ? key : acc);
  6528. var isLP = /\b(?:LP)\b/.test(release[0].format);
  6529. release[0].tracklisting.tracks.forEach(function(volume, volNdx) {
  6530. Array.prototype.push.apply(tracks, volume.tracklisting.map(track => ({
  6531. artist: isVA ? VA : release[0].artist,
  6532. album: release[0].title,
  6533. release_date: release[0].released_at.replace(/^(\d+)\w+\b/, '$1'),
  6534. label: 'Domino Recording',
  6535. catalog: release[0].sku,
  6536. media: release[0].format,
  6537. disc_number: !isLP ? volNdx + 1 : Math.round((volNdx + 1) / 2),
  6538. disc_subtitle: volume.title || undefined,
  6539. total_discs: isLP ? release[0].tracklisting.tracks.length : Math.ceil(release[0].tracklisting.tracks.length),
  6540. track_number: track.number,
  6541. total_tracks: release[0].tracklisting.tracks.reduce((acc, volume) => acc + volume.tracklisting.length, 0),
  6542. title: track.title,
  6543. url: response.finalUrl,
  6544. description: description,
  6545. cover_url: imgUrl ? release[0].images[0][imgUrl].url : undefined,
  6546. identifiers: mergeIds(),
  6547. })));
  6548. });
  6549. return tracks;
  6550. }); else if (url.hostname.endsWith('kompakt.fm')) return globalFetch(url).then(function(response) {
  6551. if ((ref = response.document.querySelector('div.player-data > ul.release > li.id')) != null)
  6552. identifiers.KOMPAKT_ID = ref.textContent;
  6553. if ((ref = response.document.querySelector('div.player-data > ul.release > li.artist')) != null)
  6554. artist = ref.textContent;
  6555. isVA = (ref = response.document.querySelector('div.player-data > ul.release > li.various-artists')) != null ?
  6556. eval(ref.textContent) : vaParser.test(artist);
  6557. if ((ref = response.document.querySelector('div.player-data > ul.release > li.title')) != null)
  6558. album = ref.textContent;
  6559. response.document.querySelectorAll('div.mt-3 > div > div.mt-2').forEach(function(div) {
  6560. var key = div.querySelector(':scope > span:nth-of-type(1)'),
  6561. value = div.querySelector(':scope > span:nth-of-type(2)');
  6562. if (key == null || value == null) return;
  6563. key = key.textContent.trim(); value = value.textContent.trim();
  6564. switch(key.replace(/\s*:\s*$/, '').toLowerCase()) {
  6565. case 'label': label = value; break;
  6566. case 'release date': releaseDate = value; break;
  6567. case 'cat no': catalogue = value; break;
  6568. case 'barcode': identifiers.BARCODE = value; break;
  6569. }
  6570. });
  6571. getDescription(response, 'div.toggable-level-1 > div.container-fluid > div.mt-3', true);
  6572. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
  6573. trs = response.document.querySelectorAll('div.player-data > ul.tracks > li.track');
  6574. return Array.from(trs).map(function(li, ndx) {
  6575. trackIdentifiers = {
  6576. TRACK_ID: (ref = li.querySelector('span.position')) != null ? ref.textContent : undefined,
  6577. };
  6578. trackArtist = (ref = li.querySelector('li.artist')) != null ? ref.textContent : undefined;
  6579. return {
  6580. artist: isVA ? VA : artist,
  6581. album: album,
  6582. release_date: releaseDate,
  6583. label: label,
  6584. catalog: catalogue,
  6585. media: media,
  6586. track_number: (ref = li.querySelector('li.position')) != null && parseInt(ref.textContent) || ndx + 1,
  6587. total_tracks: trs.length,
  6588. title: (ref = li.querySelector('li.title')) != null ? ref.textContent : undefined,
  6589. track_artist: trackArtist && (isVA || !artistsMatch(trackArtist, artist)) ? trackArtist : undefined,
  6590. duration: (ref = li.querySelector('li.duration')) != null ? timeStringToTime(ref.textContent) : undefined,
  6591. url: (ref = response.document.querySelector('meta[property="og:url"][content]')) != null ?
  6592. ref.content : response.finalUrl,
  6593. description: description,
  6594. cover_url: imgUrl,
  6595. identifiers: mergeIds(),
  6596. };
  6597. });
  6598. }); else if (url.hostname.endsWith('eclassical.com')) return globalFetch(url).then(function(response) {
  6599. if ((ref = response.document.querySelector('h1.articleName')) != null) album = ref.textContent;
  6600. artist = []; composer = []; genres = ['Classical']; label = [];
  6601. var conductors = [];
  6602. iterArtprop('div#articlePageContents', function(title, value) {
  6603. switch (title.toLowerCase()) {
  6604. case 'composers':
  6605. Array.prototype.push.apply(composer, Array.from(value.querySelectorAll('div > a'))
  6606. .map(a => a.textContent.trim().replace(/^(.+?),\s+(.+)$/, '$2 $1')));
  6607. break;
  6608. case 'performers':
  6609. Array.prototype.push.apply(artist, Array.from(value.querySelectorAll('div > a'))
  6610. .map(a => a.textContent.trim().replace(/^(.+?),\s+(.+)$/, '$2 $1')));
  6611. break;
  6612. case 'orchestras / ensembles': case 'orchestras': case 'ensembles':
  6613. Array.prototype.push.apply(artist, Array.from(value.querySelectorAll('div > a'))
  6614. .map(a => a.textContent.trim()));
  6615. break;
  6616. case 'conductors':
  6617. Array.prototype.push.apply(conductors, Array.from(value.querySelectorAll('div > a'))
  6618. .map(a => a.textContent.trim().replace(/^(.+?),\s+(.+)$/, '$2 $1')));
  6619. break;
  6620. case 'genres':
  6621. case 'instruments':
  6622. case 'periods':
  6623. Array.prototype.push.apply(genres, Array.from(value.querySelectorAll('div > a'))
  6624. .map(a => a.textContent.trim()));
  6625. break;
  6626. case 'label':
  6627. Array.prototype.push.apply(label, Array.from(value.querySelectorAll('div > a'))
  6628. .map(a => a.textContent.trim()));
  6629. break;
  6630. case 'catalogue number':
  6631. catalogue = value.textContent.trim();
  6632. break;
  6633. case 'release date':
  6634. releaseDate = value.textContent.trim();
  6635. break;
  6636. case 'discs':
  6637. totalDiscs = parseInt(value.textContent) || 1;
  6638. break;
  6639. case 'original sample rate': case 'orig. sample rate': case 'sample rate':
  6640. if (/\b(\d+)(?:\s*(?:Hz)\b)/.test(value.textContent)) samplerate = parseInt(RegExp.$1);
  6641. break;
  6642. }
  6643. });
  6644. isVA = artist.length == 1 && vaParser.test(artist[0]);
  6645. getDescription(response, 'div#articleText', false);
  6646. iterArtprop('div[id$="album-castlist"]', function(title, value, index) {
  6647. if (description) description += index <= 0 ? '\n\n' : '\n'
  6648. if (index <= 0) description += '[b]Cast:[/b]\n';
  6649. description += title + ' - ' + joinTextChilds(value, '; ', t => t.replace(/^(.+?),\s+(.+)$/, '$2 $1'));
  6650. });
  6651. if (ref = eclassicalBooklets(response)) if (description) description += '\n\n' + ref; else description = ref;
  6652. if ((ref = response.document.querySelector('div#articleImage > a')) != null) imgUrl = ref.href;
  6653. totalTracks = response.document.querySelectorAll('table.tracklistTable > tbody > tr.trackRow').length;
  6654. var workTitle, workComposers = [];
  6655. response.document.querySelectorAll('table.tracklistTable > tbody > tr').forEach(function(tr, ndx) {
  6656. if (tr.classList.contains('tracklistDiscNumberRow') && (ref = tr.querySelector('div.tracklistDiscHeader')) != null) {
  6657. discSubtitle = ref.textContent.trim();
  6658. guessDiscNumber();
  6659. }
  6660. if (tr.classList.contains('tracklistRowDivider')) {
  6661. workComposers = [];
  6662. workTitle = undefined;
  6663. }
  6664. if (tr.classList.contains('tracklistRowVerkComposerName'))
  6665. workComposers = Array.from(tr.querySelectorAll('td > span.tracklistComposerVerkName'))
  6666. .map(span => span.textContent.trim().replace(/^(?:Composer)\s*:\s*/i, '').replace(/^(.+?),\s+(.+)$/, '$2 $1'));
  6667. if (tr.classList.contains('tracklistRowVerkName')
  6668. && (ref = tr.querySelector('span.tracklistVerkName')) != null) workTitle = joinTextChilds(ref);
  6669. if (tr.classList.contains('trackRow')) {
  6670. discSubtitle = workTitle || '';
  6671. if (discSubtitle && workComposers.length > 0 && !workComposers.equalCaselessTo(composer))
  6672. discSubtitle = workComposers.join(', ') + ': ' + discSubtitle;
  6673. if ((title = joinTextChilds(tr.querySelector('td.trackName > a')))
  6674. && title.startsWith(workTitle))
  6675. title = title.slice(workTitle.length).replace(/^\s*[\:\-\,\;]\s*/, '') || workTitle;
  6676. tracks.push({
  6677. artist: isVA ? VA : undefined,
  6678. artists: !isVA ? artist : undefined,
  6679. album: album,
  6680. release_date: releaseDate,
  6681. genre: genres.join('; '),
  6682. label: label.join(' / ') || undefined,
  6683. catalog: catalogue,
  6684. media: media,
  6685. samplerate: samplerate,
  6686. disc_subtitle: discSubtitle || undefined,
  6687. disc_number: discNumber,
  6688. total_discs: totalDiscs,
  6689. track_number: (ref = tr.querySelector('td.trackNumber')) != null ? parseInt(ref.textContent) : undefined,
  6690. total_tracks: totalTracks,
  6691. title: title,
  6692. composers: workComposers.length > 0 ? workComposers : composer,
  6693. conductors: conductors,
  6694. duration: (ref = tr.querySelector('td.trackLength')) != null ? timeStringToTime(ref.textContent) : undefined,
  6695. description: description,
  6696. url: response.finalUrl,
  6697. cover_url: imgUrl,
  6698. identifiers: identifiers,
  6699. });
  6700. }
  6701. });
  6702. return tracks;
  6703.  
  6704. function joinTextChilds(node, junction = undefined, transform = undefined) {
  6705. if (!(node instanceof Node)) return undefined;
  6706. return Array.from(node.childNodes).filter(childNode => childNode.nodeType == Node.TEXT_NODE)
  6707. .map(childNode => (transform || (t => t))(childNode.wholeText.trim())).join(junction || ' ') || undefined;
  6708. }
  6709.  
  6710. function iterArtprop(root, callback) {
  6711. if (typeof callback != 'function') return;
  6712. response.document.querySelectorAll(root + '> div.artprop > table > tbody > tr').forEach(function(tr, index) {
  6713. var title = tr.querySelector('td.property_title'), value = tr.querySelector('td.property_value');
  6714. if (title != null && value != null) callback(title.textContent.trim(), value, index);
  6715. else console.warn('Unexpected artprop structure:', tr);
  6716. });
  6717. }
  6718. }); else if (url.hostname.endsWith('qq.com') && url.pathname.includes('/album/')) return globalFetch(url).then(function(response) {
  6719. if (/\/album\/(\S+)\./.test(url.pathname)) identifiers.QQMUSIC_ID = RegExp.$1;
  6720. artist = Array.from(response.document.querySelectorAll('div.data__cont > div.data__singer > a[itemprop="byArtist"]'))
  6721. .map(a => a.title || a.textContent.trim());
  6722. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6723. if ((ref = response.document.querySelector('div.data__cont > div.data__name > h1')) != null)
  6724. album = ref.title || ref.textContent.trim();
  6725. const datainfoStripper = /^.*:\s*|[\u0080-\uFFFF]+/g;
  6726. if ((ref = response.document.querySelector('div.data__cont > ul.data__info > li.data_info__item:nth-of-type(1)')) != null)
  6727. genres = [ref.textContent.trim().replace(datainfoStripper, '')];
  6728. if ((ref = response.document.querySelector('div.data__cont > ul.data__info > li.data_info__item:nth-of-type(3)')) != null)
  6729. releaseDate = ref.textContent.trim().replace(datainfoStripper, '');
  6730. if ((ref = response.document.querySelector('img#albumImg')) != null)
  6731. imgUrl = ref.src.replace(/(?:_\d+)?(\.\w+)(?:\?.*)?$/, '$1').replace(/R\d+x\d+/, '');
  6732. trs = response.document.querySelectorAll('ul#song_box > li[mid]');
  6733. return Array.from(trs).map(function(li, index) {
  6734. trackIdentifiers = {
  6735. TRACK_ID: parseInt(li.getAttribute('mid')) || li.getAttribute('mid') || undefined,
  6736. };
  6737. trackArtist = Array.from(li.querySelectorAll('div.songlist__artist > a'))
  6738. .map(a => a.title || a.textContent.trim());
  6739. return {
  6740. artist: isVA ? VA : undefined,
  6741. artists: !isVA ? artist : undefined,
  6742. album: album,
  6743. release_date: releaseDate,
  6744. genre: genres.join('; '),
  6745. label: label,
  6746. media: media,
  6747. track_number: (ref = li.querySelector('div.songlist__number')) != null && parseInt(ref.textContent)
  6748. || index + 1,
  6749. total_tracks: trs.length,
  6750. title: (ref = li.querySelector('div.songlist__songname > span > a')) != null ?
  6751. ref.title || ref.textContent.trim() : undefined,
  6752. track_artists: isVA || !trackArtist.equalCaselessTo(artist) ? trackArtist : undefined,
  6753. duration: (ref = li.querySelector('div.songlist__time')) != null ?
  6754. timeStringToTime(ref.textContent) : undefined,
  6755. url: !identifiers.QQMUSIC_ID ? response.finalUrl : undefined,
  6756. cover_url: imgUrl,
  6757. identifiers: mergeIds(),
  6758. };
  6759. });
  6760. }); else if (url.hostname.endsWith('muziekweb.nl') && url.pathname.includes('/Link/')) return globalFetch(url).then(function(response) {
  6761. if (/\/Link\/(\w+)\b/i.test(url.pathname)) identifiers.MUZIEKWEB_ID = RegExp.$1;
  6762. artist = Array.from(response.document.querySelectorAll('ul.cat-performers > li[itemprop="byArtist"] > a > span'))
  6763. .map(span => span.textContent.trim());
  6764. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6765. if ((ref = response.document.querySelector('h1.cat-albumtitle')) != null) album = ref.textContent.trim();
  6766. if ((ref = response.document.querySelector('span[itemprop="catalogNumber"]')) != null)
  6767. catalogue = ref.textContent.trim();
  6768. if ((ref = response.document.querySelector('span[itemprop="recordLabel"]')) != null)
  6769. label = ref.textContent.trim();
  6770. genres = Array.from(response.document.querySelectorAll('ul.cat-genres span[itemprop="genre"]'))
  6771. .map(span => span.textContent.trim());
  6772. if ((ref = response.document.querySelector('div.cat-albumrelease > meta[itemprop="datePublished"][content]')) != null)
  6773. releaseDate = ref.content;
  6774. if ((ref = response.document.querySelector('span[itemprop="musicReleaseFormat"]')) != null) {
  6775. if (/\b(?:compact\s+disc)/i.test(ref.textContent)) media = 'CD';
  6776. }
  6777. getDescription(response, 'div#album-info div.cat-article-text', true);
  6778. // if (!description && (ref = response.document.querySelector('meta[property="og:description"]')) != null)
  6779. // description = ref.content;
  6780. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  6781. imgUrl = ref.content.replace(/\/COVER\/\w+\b/i, '/COVER/SUPERLARGE');
  6782. trs = response.document.querySelectorAll('ul.cat-tracklist > li[itemprop="itemListElement"] > div.cat-track-item');
  6783. return Array.from(trs).map(function(div, index) {
  6784. trackIdentifiers = {
  6785. TRACK_ID: (ref = div.querySelector('div.cat-track-playbuttons > div[id]')) != null ? ref.id : undefined,
  6786. };
  6787. trackArtist = Array.from(div.querySelectorAll('span[itemprop="byArtist"] meta[itemprop="name"][content]'))
  6788. .map(meta => meta.content);
  6789. return {
  6790. artist: isVA ? VA : undefined,
  6791. artists: !isVA ? artist : undefined,
  6792. album: album,
  6793. release_date: releaseDate,
  6794. genre: genres.join('; '),
  6795. label: label,
  6796. catalog: catalogue,
  6797. media: media,
  6798. track_number: (ref = div.querySelector('div.cat-track-number')) != null
  6799. && (parseInt(ref.textContent) || ref.textContent.trim()) || index + 1,
  6800. total_tracks: trs.length,
  6801. title: (ref = div.querySelector('div.cat-track[title]')) != null ? ref.title
  6802. : (ref = div.querySelector('div.cat-track-title')) != null ? ref.textContent.trim() : undefined,
  6803. track_artists: isVA || !trackArtist.equalCaselessTo(artist) ? trackArtist : undefined,
  6804. duration: (ref = div.querySelector('div.cat-track-playtime')) != null ?
  6805. timeStringToTime(ref.textContent) : undefined,
  6806. description: description,
  6807. url: identifiers.MUZIEKWEB_ID ? undefined
  6808. : (ref = response.document.querySelector('meta[property="og:url"]')) != null ?
  6809. ref.content : response.finalUrl,
  6810. cover_url: imgUrl,
  6811. identifiers: mergeIds(),
  6812. };
  6813. });
  6814. }); else if (url.hostname.endsWith('beatsource.com')) return /\/releases?\/(?:.+\/)?(\d+)(?=\/|$)/i.test(url.pathname) ?
  6815. queryBeatsourceAPI('releases/' + RegExp.$1).then(function(release) {
  6816. if (prefs.diag_mode) console.debug('Beatsource release metadata received:', release);
  6817. identifiers.BEATSOURCE_ID = release.id;
  6818. artist = release.artists.map(artist => artist.name);
  6819. isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
  6820. if (release.type.name) identifiers.RELEASETYPE = release.type.name;
  6821. if (release.upc) identifiers.BARCODE = release.upc;
  6822. if ('is_explicit' in release) identifiers.EXPLICIT = Number(release.is_explicit);
  6823.  
  6824. function trackMapper(track, index) {
  6825. trackIdentifiers = { TRACK_ID: track.id };
  6826. if (track.isrc) trackIdentifiers.ISRC = track.isrc;
  6827. if ('is_explicit' in track) trackIdentifiers.EXPLICIT = Number(track.is_explicit);
  6828. if (track.bpm) trackIdentifiers.BPM = track.bpm;
  6829. trackArtist = track.artists.map(artist => artist.name);
  6830. if ((title = track.name) && track.mix_name && track.mix_name != 'Original Mix')
  6831. title += ' (' + track.mix_name + ')';
  6832. try { genres = [track.genre.name] } catch(e) { genres = [] }
  6833. if (track.sub_genre) try { genres.push(track.sub_genre.name) } catch(e) { }
  6834. return {
  6835. artist: isVA ? VA : undefined,
  6836. artists: artist.length > 0 ? artist : undefined,
  6837. album: release.name,
  6838. release_date: release.new_release_date || release.publish_date/* ||
  6839. track.new_release_date || track.publish_date*/ || undefined,
  6840. genre: genres.join('; ') || undefined,
  6841. label: release.label.name,
  6842. catalog: release.catalog_number || track.catalog_number || undefined,
  6843. media: media,
  6844. track_number: track.number || index + 1,
  6845. total_tracks: release.track_count,
  6846. title: title,
  6847. track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ?
  6848. trackArtist : undefined,
  6849. remixers: track.remixers.length > 0 ? track.remixers.map(remixer => remixer.name)
  6850. :/* release.remixers.length > 0 ? release.remixers.map(remixer => remixer.name) :*/ undefined,
  6851. duration: track.length_ms > 0 ? track.length_ms / 1000 : undefined,
  6852. description: release.desc || undefined,
  6853. url: release.slug ? `https://www.beatsource.com/release/${release.slug}/${release.id}` : url,
  6854. identifiers: mergeIds(),
  6855. cover_url: release.image.uri || undefined,
  6856. };
  6857. }
  6858.  
  6859. return queryBeatsourceAPI(`releases/${release.id}/tracks`, { per_page: 9999 }).then(function(tracks) {
  6860. if (prefs.diag_mode) console.debug('Beatsource tracks metadata received:', tracks.results);
  6861. return tracks.count == release.track_count ? tracks.results.map(trackMapper)
  6862. : Promise.reject('Track counts inconsistency');
  6863. }).catch(function(reason) {
  6864. console.warn('Beatsource release tracks failed:', reason);
  6865. return Promise.all(release.tracks.map(track => queryBeatsourceAPI(track)))
  6866. .then(tracks => tracks.map(trackMapper));
  6867. });
  6868. }) : Promise.reject('This URL doesnot refer to Beatsource release');
  6869. if (!weak) clipBoard.value = '';
  6870. return Promise.reject(url.hostname + ' not supported');
  6871.  
  6872. function mergeIds() {
  6873. var r = Object.assign({}, identifiers, trackIdentifiers);
  6874. trackIdentifiers = {};
  6875. return r;
  6876. }
  6877.  
  6878. function getDescription(response, selectorOrNode, quote = false) {
  6879. description = [];
  6880. if (selectorOrNode instanceof HTMLElement) addFromNode(selectorOrNode);
  6881. else if (typeof selectorOrNode == 'string')
  6882. response.document.querySelectorAll(selectorOrNode).forEach(addFromNode);
  6883. description = description.join('\n\n').collapseGaps();
  6884. if (quote && description.length > 0 && !description.includes('[quote]'))
  6885. description = '[quote]' + description + '[/quote]';
  6886.  
  6887. function addFromNode(node) {
  6888. var p = html2php(node, response.finalUrl).trim();
  6889. if (p) description.push(p);
  6890. }
  6891. }
  6892.  
  6893. function durationFromMeta(elem) {
  6894. if (!(elem instanceof HTMLElement)) return undefined;
  6895. let meta = elem.querySelector('meta[itemprop="duration"][content]');
  6896. if (meta == null) return undefined;
  6897. let m = /^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.exec(meta.content);
  6898. if (m != null)
  6899. return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
  6900. m = timeStringToTime(meta.content);
  6901. return m != null ? m : undefined;
  6902. }
  6903.  
  6904. function guessDiscNumber() {
  6905. if (discParser.test(discSubtitle)) {
  6906. discSubtitle = undefined;
  6907. discNumber = parseInt(RegExp.$1);
  6908. }
  6909. }
  6910.  
  6911. function finalizeTracks() {
  6912. if (isVA || !tracks.every(function(track) {
  6913. return Array.isArray(track.track_artists) && track.track_artists.equalCaselessTo(tracks[0].track_artists)
  6914. && (Array.isArray(track.track_guests) ? track.track_guests.equalCaselessTo(tracks[0].track_guests)
  6915. : !Array.isArray(tracks[0].track_guests))
  6916. || track.track_artist && track.track_artist == tracks[0].track_artist;
  6917. })) return tracks;
  6918. tracks.forEach(function(track) {
  6919. if (Array.isArray(track.track_artists)) {
  6920. track.artists = track.track_artists;
  6921. if (Array.isArray(track.track_guests)) track.featured_artists = track.track_guests;
  6922. delete track.artist;
  6923. } else if (track.track_artist) track.artist = track.track_artist;
  6924. delete track.track_artists;
  6925. delete track.track_guests;
  6926. delete track.track_artist;
  6927. });
  6928. return tracks;
  6929. }
  6930. } // fetchOnline_Music
  6931.  
  6932. function joinArtists(arr, decorator = artist => artist) {
  6933. if (!Array.isArray(arr)) return null;
  6934. if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
  6935. if (arr.length < 3) return arr.map(decorator).join(' & ');
  6936. return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
  6937. }
  6938.  
  6939. function stringifyArtists(artists) {
  6940. if (Array.isArray(artists)) try {
  6941. if (artists[0].length <= 0) return null;
  6942. var result = joinArtists(artists[0]);
  6943. if (artists[1].length > 0) result += ' feat. ' + joinArtists(artists[1]);
  6944. return result;
  6945. } catch(e) { console.error('stringifyArtists(...):', e) }
  6946. return null;
  6947. }
  6948.  
  6949. function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
  6950. function looksLikeTrueName(artist, index = 0) {
  6951. return twoOrMore(artist)
  6952. && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
  6953. && artist.split(/\s+/).length >= 2
  6954. && !pseudoArtistParsers.some(rx => rx.test(artist)) || getSiteArtist(artist);
  6955. }
  6956.  
  6957. function strip(art) {
  6958. return [
  6959. /\s+(?:aka|AKA)\.?\s+(.*)$/g,
  6960. tailingBracketStripper,
  6961. ].reduce((acc, rx, ndx) => ndx != 1 || rx.test(acc) && !notMonospaced(RegExp.$1) ? acc.replace(rx, '') : acc, art);
  6962. }
  6963.  
  6964. function getSiteArtist(artist, asynchronous = false) {
  6965. //if (isOPS) return undefined;
  6966. if (!artist || notSiteArtistsCache.includesCaseless(artist)) return null;
  6967. var key = Object.keys(siteArtistsCache).find(it => it.toLowerCase() == artist.toLowerCase());
  6968. if (key) return siteArtistsCache[key];
  6969. var now = Date.now();
  6970. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  6971. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  6972. apiTimeFrame.timeStamp = now;
  6973. apiTimeFrame.requestCounter = 1;
  6974. } else ++apiTimeFrame.requestCounter;
  6975. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  6976. if (apiTimeFrame.requestCounter > 5) {
  6977. console.debug('getSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
  6978. artist + '" (' + apiTimeFrame.requestCounter + ')');
  6979. if (prefs.messages_verbosity >= 2) addMessage('AJAX API request exceeding time frame: artistname="' +
  6980. artist + '" (' + apiTimeFrame.requestCounter + ')', 'notice');
  6981. ++ajaxRejects;
  6982. return undefined;
  6983. }
  6984. try {
  6985. var requestUrl = '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist);
  6986. var xhr = new XMLHttpRequest;
  6987. xhr.open('GET', requestUrl, asynchronous);
  6988. if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
  6989. xhr.send();
  6990. if (xhr.status == 404) {
  6991. notSiteArtistsCache.pushUniqueCaseless(artist);
  6992. return null;
  6993. }
  6994. if (xhr.readyState != XMLHttpRequest.DONE || xhr.status < 200 || xhr.status >= 400) {
  6995. console.warn('getSiteArtist("' + artist + '") error:', xhr, 'url:', document.location.origin + requestUrl);
  6996. return undefined; // error
  6997. }
  6998. let response = JSON.parse(xhr.responseText);
  6999. if (response.status != 'success') {
  7000. notSiteArtistsCache.pushUniqueCaseless(artist);
  7001. return null;
  7002. }
  7003. siteArtistsCache[artist] = response.response;
  7004. if (prefs.diag_mode) console.log('getSiteArtist("' + artist + '") success:', siteArtistsCache[artist]);
  7005. return (siteArtistsCache[artist]);
  7006. } catch(e) {
  7007. console.error('UA::getSiteArtist("' + artist + '"):', e, xhr);
  7008. return undefined;
  7009. }
  7010. }
  7011.  
  7012. function splitArtists(str, parsers = multiArtistParsers) {
  7013. var result = [str];
  7014. parsers.forEach(function(parser) {
  7015. for (let i = result.length; i > 0; --i) {
  7016. let j = result[i - 1].split(parser).map(strip);
  7017. if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
  7018. && !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
  7019. }
  7020. });
  7021. return result;
  7022. }
  7023.  
  7024. function splitAmpersands(artists) {
  7025. if (typeof artists == 'string') var result = splitArtists(artists);
  7026. else if (Array.isArray(artists)) result = Array.from(artists); else return [];
  7027. ampersandParsers.forEach(function(ampersandParser) {
  7028. for (let i = result.length; i > 0; --i) {
  7029. let j = result[i - 1].split(ampersandParser).map(strip);
  7030. if (j.length <= 1 || !j.every(looksLikeTrueName) || getSiteArtist(result[i - 1])) continue;
  7031. result.splice(i - 1, 1, ...j.filter(function(artist) {
  7032. return !result.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist));
  7033. }));
  7034. }
  7035. });
  7036. return result;
  7037. }
  7038.  
  7039. function getArtists(trackArtist) {
  7040. if (!trackArtist || typeof trackArtist != 'string') trackArtist = '';
  7041. otherArtistsParsers.forEach(it => { if (it[0].test(trackArtist)) trackArtist = RegExp.$1 });
  7042. var result = [[], []];
  7043. featArtistParsers.forEach(function(rx, ndx) {
  7044. var matches = rx.exec(trackArtist);
  7045. if (matches == null || ndx >= 7 && !looksLikeTrueName(matches[1], 1)) return;
  7046. splitAmpersands(matches[1]).forEach(artist => { result[1].pushUniqueCaseless(artist) });
  7047. trackArtist = trackArtist.replace(rx, '');
  7048. });
  7049. splitAmpersands(trackArtist).forEach(artist => { result[0].pushUniqueCaseless(artist) });
  7050. return result;
  7051. }
  7052.  
  7053. function artistsMatch(artist1, artist2) {
  7054. if (!artist1 && !artist2) return true;
  7055. if (!artist1 || !artist2) return false;
  7056. if (typeof artist1 == 'string' && typeof artist2 == 'string'
  7057. && artist1.toLowerCase() == artist2.toLowerCase()) return true;
  7058. if (Array.isArray(artist1)) {
  7059. var _artist1 = getStringVariants(artist1);
  7060. try { if (_artist1.some(artist => artist == artist2.toLowerCase())) return true } catch(e) { }
  7061. }
  7062. if (Array.isArray(artist2)) {
  7063. var _artist2 = getStringVariants(artist2);
  7064. try { if (_artist2.some(artist => artist == artist1.toLowerCase())) return true } catch(e) { }
  7065. }
  7066. if (_artist1 && _artist2 && _artist1.some(artist => _artist2.includes(artist))) return true;
  7067. if (typeof artist1 == 'string') artist1 = getArtists(artist1);
  7068. if (typeof artist2 == 'string') artist2 = getArtists(artist2);
  7069. if (!Array.isArray(artist1) || !Array.isArray(artist2)) {
  7070. console.warn('artistsMatch: assertion failed', artist1, artist2);
  7071. return false;
  7072. }
  7073. return Array.isArray(artist1[0]) && Array.isArray(artist2[0]) && artist1[0].equalCaselessTo(artist2[0])
  7074. && ((!Array.isArray(artist1[1]) || artist1[1].length <= 0) && (!Array.isArray(artist2[1]) || artist2[1].length <= 0)
  7075. || Array.isArray(artist1[1]) && artist1[1].equalCaselessTo(artist2[1]));
  7076. }
  7077.  
  7078. function getStringVariants(arr) {
  7079. if (!Array.isArray(arr)) return null;
  7080. var result = [arr[0].join(', '), joinArtists(arr[0])];
  7081. if (Array.isArray(arr[1]) && arr[1].length > 0) {
  7082. result[0] += ' feat. ' + arr[1].join(', ');
  7083. result[1] += ' feat. ' + joinArtists(arr[1]);
  7084. result = result.concat(result.map(a => a.replace(' feat. ', ' ft. ')))
  7085. .concat(result.map(a => a.replace(' feat. ', ' featuring ')))
  7086. .concat(result.map(a => a.replace(' feat. ', ' with ')))
  7087. .concat(result.map(a => a.replace(' feat. ', ' avec ')));
  7088. }
  7089. return result.map(a => a.toLowerCase());
  7090. }
  7091.  
  7092. function queryItunesAPI(key, params) {
  7093. return queryGenericAPI('itunes.apple.com', key, params);
  7094. }
  7095. function queryDeezerAPI(action, params) {
  7096. return action ? new Promise(function(resolve, reject) {
  7097. const t0 = Date.now(), safeTimeFrame = 5000 + GM_getValue('deezer_quota_reserve', 500);
  7098. let dzUrl = 'https://api.deezer.com/' + action, retryCounter = 0, quotaCounter = 0;
  7099. if (params && typeof params == 'object') try {
  7100. params = new URLSearchParams(params);
  7101. dzUrl += '?' + params.toString();
  7102. } catch(e) { console.error(e, params) } else if (params != undefined) dzUrl += '/' + params.toString();
  7103. //console.debug('Deezer query URL:', url);
  7104. requestInternal();
  7105.  
  7106. function requestInternal() {
  7107. const requestStart = Date.now();
  7108. if (!dzApiTimeFrame.timeStamp || requestStart - dzApiTimeFrame.timeStamp > safeTimeFrame) {
  7109. dzApiTimeFrame.timeStamp = requestStart;
  7110. dzApiTimeFrame.requestCounter = 1;
  7111. } else ++dzApiTimeFrame.requestCounter;
  7112. const queueSnapshot = {
  7113. frameStart: dzApiTimeFrame.timeStamp,
  7114. position: dzApiTimeFrame.requestCounter,
  7115. timeDistance: requestStart - dzApiTimeFrame.timeStamp,
  7116. frameLength: safeTimeFrame,
  7117. };
  7118. if (dzApiTimeFrame.requestCounter <= 50) GM_xmlhttpRequest({
  7119. method: 'GET',
  7120. url: dzUrl,
  7121. responseType: 'json',
  7122. headers: {
  7123. 'Accept': 'application/json',
  7124. 'Accept-Language': 'en-US, en',
  7125. 'X-Requested-With': 'XMLHttpRequest',
  7126. },
  7127. onload: function(response) {
  7128. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  7129. if (!response.response.error) {
  7130. let dt = Date.now() - t0;
  7131. resolve(response.response);
  7132. if (retryCounter > 0) console.debug('Deezer API request fulfilled after',
  7133. retryCounter, 'retries and', quotaCounter, 'postponements in', dt, 'ms');
  7134. } else if (response.response.error.code == 4) {
  7135. setTimeout(requestInternal, 100);
  7136. console.warn('Deezer API semaphore failed:', queueSnapshot, dzApiTimeFrame, ++retryCounter);
  7137. } else reject(response.response.error.message);
  7138. },
  7139. onerror: response => { reject(defaultErrorHandler(response)) },
  7140. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  7141. }); else {
  7142. setTimeout(requestInternal, dzApiTimeFrame.timeStamp + safeTimeFrame - requestStart);
  7143. ++quotaCounter;
  7144. }
  7145. }
  7146. }) : Promise.reject('Action missing');
  7147. }
  7148. function queryDiscogsAPI(key, params) {
  7149. return setSession().then(auth => queryGenericAPI('api.discogs.com', key, params, { 'Authorization': auth }));
  7150.  
  7151. function setSession() {
  7152. return Promise.resolve('Discogs key="' + discogs_key + '", secret="' + discogs_secret + '"');
  7153. //return Promise.resolve('Discogs token="' + discogs_token + '"');
  7154. const oauthNonce = randomString(64), userAgent = 'Upload Assistant.js/1.0';
  7155. // https://www.discogs.com/developers#page:authentication,header:authentication-discogs-auth-flow
  7156. return globalFetch('https://api.discogs.com/oauth/request_token', { method: 'HEAD', headers: {
  7157. 'Content-Type': 'application/x-www-form-urlencoded',
  7158. 'Authorization': 'OAuth oauth_consumer_key="' + discogs_key + '", oauth_nonce="' + oauthNonce + '", ' +
  7159. 'oauth_signature="' + discogs_secret + '&", oauth_signature_method="PLAINTEXT", ' +
  7160. 'oauth_timestamp="' + Date.now() + '"',
  7161. 'User-Agent': userAgent,
  7162. } }).then(function(response) {
  7163. if (!/^(?:oauth_token)\s*=\s*(\S+)\b/im.text(response.responseHeaders)) return Promise.reject('invalid header');
  7164. var accessToken = RegExp.$1;
  7165. if (!/^(?:oauth_token_secret)\s*=\s*(\S+)\b/im.text(response.responseHeaders))
  7166. return Promise.reject('invalid header');
  7167. var accessTokenSecret = RegExp.$1;
  7168. return new Promise(function(resolve, reject) {
  7169. GM_openInTab('https://discogs.com/oauth/authorize?oauth_token=' + accessToken, {
  7170. active: true,
  7171. insert: true,
  7172. setParent: true,
  7173. }).onclose = function() {
  7174. // TODO: get verifier code
  7175. resolve(oauth_verifier);
  7176. };
  7177. }).then(oauth_verifier => globalFetch('https://api.discogs.com/oauth/access_token', {method: 'POST', headers: {
  7178. 'Content-Type': 'application/x-www-form-urlencoded',
  7179. 'Authorization': 'OAuth oauth_consumer_key="' + discogs_key + '", oauth_nonce="' + oauthNonce + '", ' +
  7180. 'oauth_token="' + accessToken + '", oauth_signature="' + discogs_secret + '&", ' +
  7181. 'oauth_signature_method="PLAINTEXT", oauth_timestamp="' + Date.now() + '", ' +
  7182. 'oauth_verifier="' + oauth_verifier + '"',
  7183. 'User-Agent': userAgent,
  7184. } })).then(function(response) {
  7185. if (!/^(?:oauth_token)\s*=\s*(\S+)\b/im.text(response.responseHeaders)) return Promise.reject('invalid header');
  7186. accessToken = RegExp.$1;
  7187. if (!/^(?:oauth_token_secret)\s*=\s*(\S+)\b/im.text(response.responseHeaders))
  7188. return Promise.reject('invalid header');
  7189. accessTokenSecret = RegExp.$1;
  7190. return 'oauth_token="' + accessToken + '", oauth_token_secret="' + accessTokenSecret + '"';
  7191. });
  7192. });
  7193. }
  7194. }
  7195. function queryMusicBrainzAPI(key, params) {
  7196. return queryGenericAPI('musicbrainz.org', 'ws/2/' + key + '/', Object.assign({ fmt: 'json' }, params));
  7197. }
  7198. function querySpotifyAPI(key, params) {
  7199. return key ? setOauth2Token().then(credentials => queryGenericAPI('api.spotify.com', 'v1/' + key, params, {
  7200. Authorization: credentials.token_type + ' ' + credentials.access_token,
  7201. })) : Promise.reject('No API keyword');
  7202.  
  7203. function setOauth2Token() {
  7204. try { var accessToken = JSON.parse(window.localStorage.spotifyAccessToken) } catch(e) { }
  7205. if (isTokenValid(accessToken)) {
  7206. if (prefs.diag_mode) console.debug('Re-used Spotify access token:', accessToken, new Date(accessToken.expires_at).toLocaleTimeString());
  7207. return Promise.resolve(accessToken);
  7208. }
  7209. if (!spotify_clientid || !spotify_clientsecret) return Promise.reject('Spotify credentials not configured');
  7210. const data = new URLSearchParams({
  7211. 'grant_type': 'client_credentials',
  7212. });
  7213. return globalFetch('https://accounts.spotify.com/api/token', { responseType: 'json', headers: {
  7214. Authorization: 'Basic ' + btoa(spotify_clientid + ':' + spotify_clientsecret),
  7215. } }, data).then(function(response) {
  7216. accessToken = response.response;
  7217. accessToken.expires_at = Date.now() + accessToken.expires_in * 1000;
  7218. if (!isTokenValid(accessToken)) return Promise.reject('Invalid token received');
  7219. delete accessToken.expires_in;
  7220. window.localStorage.spotifyAccessToken = JSON.stringify(accessToken);
  7221. if (prefs.diag_mode) console.debug('Spotify access token successfully set:', accessToken);
  7222. return accessToken;
  7223. });
  7224. }
  7225.  
  7226. function isTokenValid(accessToken) {
  7227. return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
  7228. && accessToken.expires_at >= Date.now() + 30000;
  7229. }
  7230. }
  7231. function queryLastFmAPI(method, params) {
  7232. return lastfm_api_key ? queryGenericAPI('ws.audioscrobbler.com', '2.0/', Object.assign({
  7233. method: method,
  7234. api_key: lastfm_api_key,
  7235. format: 'json',
  7236. }, params || {})) : Promise.reject('Last.fm API key not configured');
  7237. }
  7238. function queryTidalAPI(key, params, countryCode) {
  7239. if (!key) return Promise.reject('API action not defined');
  7240. if (typeof params != 'object') params = {};
  7241. params.deviceType = 'BROWSER';
  7242. params.countryCode = countryCode;
  7243. return setSession().then(function(session) {
  7244. if (!params.countryCode) params.countryCode = session.countryCode || 'US';
  7245. return { 'X-Tidal-SessionId': session.sessionId };
  7246. }).catch(function(reason) {
  7247. console.warn('Tidal login failed:', reason);
  7248. return setOauth2Token().then(function(token) {
  7249. if (!params.countryCode) params.countryCode = token.user.countryCode || 'US';
  7250. return { 'Authorization': token.token_type + ' ' + token.access_token };
  7251. });
  7252. }).then(header => queryGenericAPI('listen.tidal.com', 'v1/' + key, params, header));
  7253.  
  7254. function setOauth2Token() {
  7255. try {
  7256. var accessToken = JSON.parse(window.localStorage.tidalAccessToken);
  7257. if (isTokenValid(accessToken)) {
  7258. if (prefs.diag_mode) console.debug('Re-used Tidal access token:',
  7259. accessToken, new Date(accessToken.expires_at).toLocaleTimeString());
  7260. return Promise.resolve(accessToken);
  7261. }
  7262. } catch(e) { }
  7263. if (!prefs.tidal_userid || !prefs.tidal_userpassword) return Promise.reject('incomplete account configuration');
  7264. return getClientId().then(function(clientId) {
  7265. const clientKey = getClientKey(),
  7266. state = 'TIDAL_' + Date.now() + '_' + randomString(64),
  7267. //urlEncode(btoa(String.fromCharCode.apply(null, crypto.getRandomValues(new Uint8Array(64))))),
  7268. codeVerifier = randomString(128), //urlEncode(btoa(crypto.getRandomValues(new Uint8Array(100)))).slice(0, 128),
  7269. codeChallenge = urlEncode(CryptoJS.SHA256(codeVerifier).toString(CryptoJS.enc.Base64));
  7270. var params = new URLSearchParams({
  7271. app_mode: 'desktop',
  7272. client_id: clientId,
  7273. client_unique_key: clientKey,
  7274. code_challenge: codeChallenge,
  7275. code_challenge_method: 'S256',
  7276. state: state,
  7277. scope: 'r_usr w_usr',
  7278. response_type: 'code',
  7279. redirect_uri: 'https://listen.tidal.com/login/auth',
  7280. restrict_signup: false,
  7281. lang: 'en',
  7282. }), formData, referer;
  7283. return globalFetch('https://login.tidal.com/authorize?' + params, { method: 'HEAD' }).then(function(response) {
  7284. if (!/^(?:token)=([^;\s$]+)/im.test(response.responseHeaders)) return Promise.reject('invalid header');
  7285. referer = response.finalUrl;
  7286. formData = new URLSearchParams({
  7287. _csrf: RegExp.$1,
  7288. email: prefs.tidal_userid,
  7289. password: prefs.tidal_userpassword,
  7290. });
  7291. // return globalFetch('https://login.tidal.com/email?' + params, {
  7292. // responseType: 'json',
  7293. // headers: { 'Referer': referer },
  7294. // }, formData);
  7295. // }).then(function(response) {
  7296. // if (!/^(?:token)=([^;\s$]+)/im.test(response.responseHeaders)) return Promise.reject('invalid header');
  7297. // formData.set('_csrf', RegExp.$1);
  7298. return globalFetch('https://login.tidal.com/email/user/existing?' + params, {
  7299. responseType: 'json',
  7300. headers: { 'Referer': referer },
  7301. }, formData);
  7302. }).then(function(response) {
  7303. var redirectUrl = new URL(response.response.redirectUri);
  7304. params = new URLSearchParams(redirectUrl.search);
  7305. formData = new URLSearchParams({
  7306. client_id: clientId,
  7307. client_unique_key: clientKey,
  7308. code: params.get('code'),
  7309. code_verifier: codeVerifier,
  7310. scope: 'r_usr w_usr',
  7311. grant_type: 'authorization_code',
  7312. redirect_uri: redirectUrl.href,
  7313. });
  7314. return globalFetch('https://login.tidal.com/oauth2/token', {
  7315. responseType: 'json',
  7316. headers: { 'Referer': referer },
  7317. }, formData);
  7318. }).then(function(response) {
  7319. if (typeof response.response != 'object') return Promise.reject('invalid token');
  7320. accessToken = response.response;
  7321. accessToken.expires_at = Date.now() + accessToken.expires_in * 1000;
  7322. if (!isTokenValid(accessToken)) return Promise.reject('Received invalid token');
  7323. delete accessToken.expires_in;
  7324. window.sessionStorage.tidalAccessToken = JSON.stringify(accessToken);
  7325. console.debug('Tidal access token successfully set:', accessToken);
  7326. return accessToken;
  7327. });
  7328. });
  7329. }
  7330. function setSession() {
  7331. try { var session = JSON.parse(window.sessionStorage.tidalSession) } catch(e) { }
  7332. if (isSessionValid(session)) {
  7333. if (prefs.diag_mode) console.debug('Re-used Tidal session:', session);
  7334. return Promise.resolve(session);
  7335. }
  7336. if (!prefs.tidal_userid || !prefs.tidal_userpassword) return Promise.reject('Tidal user credentials not configured');
  7337. const deviceTokens = [
  7338. /* 0 */ 'wdgaB1CilGA-S_s2', // browser | Streams HIGH/LOW Quality over RTMP, FLAC and Videos over HTTP, but many Lossless Streams are encrypted.
  7339. /* 1 */ '4zx46pyr9o8qZNRw', // browser(?) | other quality
  7340. /* 2 */ 'kgsOOmYk3zShYrNP', // Android | All Streams are HTTP Streams. Correct numberOfVideos in Playlists (best Token to use)
  7341. /* 3 */ 'GvFhCVAYp3n43EN3', // iOS | Same as Android Token, but uses ALAC instead of FLAC
  7342. /* 4 */ '_DSTon1kC8pABnTw', // iOS | Same as Android Token, but uses ALAC instead of FLAC
  7343. /* 5 */ '4zx46pyr9o8qZNRw', // native | Same as Android Token, but FLAC streams are encrypted
  7344. /* 6 */ 'BI218mwp9ERZ3PFI', // audirvana | Like Android Token, supports MQA, but returns 'numberOfVideos = 0' in Playlists
  7345. /* 7 */ 'wc8j_yBJd20zOmx0', // amarra | Like Android Token, but returns 'numberOfVideos = 0' in Playlists
  7346. /* 8 */ 'P5Xbeo5LFvESeDy6', // Like Android Token, but returns 'numberOfVideos = 0' in Playlists
  7347. /* 9 */ '_KM2HixcUBZtmktH', // Same as previous
  7348. /* 10 */ 'oIaGpqT_vQPnTr0Q', // Same, but uses RTMP for HIGH/LOW Quality
  7349. ];
  7350. var formData = new URLSearchParams({
  7351. username: prefs.tidal_userid,
  7352. password: prefs.tidal_userpassword,
  7353. clientUniqueKey: getClientKey(),
  7354. clientVersion: '1.0',
  7355. token: deviceTokens[7],
  7356. });
  7357. return globalFetch('https://api.tidal.com/v1/login/username', { // 'https://api.tidalhifi.com/v1/login/username'
  7358. responseType: 'json',
  7359. //headers: { 'X-Tidal-Token': xTidalToken },
  7360. }, formData).then(function(response) {
  7361. if (!isSessionValid(session = response.response)) return Promise.reject('invalid session');
  7362. if (prefs.diag_mode) console.debug('Tidal session successfully established:', session);
  7363. window.sessionStorage.tidalSession = JSON.stringify(session);
  7364. return session;
  7365. });
  7366. }
  7367. function uuidv4() {
  7368. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  7369. var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
  7370. return v.toString(16);
  7371. });
  7372. }
  7373. function urlEncode(b64str) {
  7374. return b64str.replace(/\=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  7375. }
  7376. function getClientId() {
  7377. if (prefs.tidal_clientid || (prefs.tidal_clientid = GM_getValue('tidal_clientid')))
  7378. return Promise.resolve(prefs.tidal_clientid);
  7379. return getTidalSecrets().then(function(response) {
  7380. const rx = /"(\w{40})":"(\w{16})"/g;
  7381. if ((i = response.responseText.match(rx)) == null || !rx.test(i.shift()))
  7382. return Promise.reject('client id detection fail');
  7383. GM_setValue('tidal_clientid', prefs.tidal_clientid = RegExp.$2);
  7384. if (prefs.diag_mode) console.debug('Successfully configured Tidal client Id:', prefs.tidal_clientid);
  7385. return prefs.tidal_clientid;
  7386. }).catch(function(reason) {
  7387. reason = `Client Id auto detection failed (${reason}), set it manually (tidal_clientid)`;
  7388. alert(reason);
  7389. return Promise.reject(reason);
  7390. });
  7391. }
  7392. function getClientKey() {
  7393. if (!prefs.tidal_clientkey && !(prefs.tidal_clientkey = GM_getValue('tidal_clientkey')))
  7394. GM_setValue('tidal_clientkey', prefs.tidal_clientkey = uuidv4());
  7395. return prefs.tidal_clientkey;
  7396. }
  7397. function getTidalSecrets() {
  7398. const origin = 'https://listen.tidal.com';
  7399. return globalFetch(origin + '/login').then(function(response) {
  7400. var appLink = response.document.querySelector('body > script[src]:last-of-type');
  7401. return appLink == null ? Promise.reject('invalid document format')
  7402. : globalFetch(origin + appLink.src.replace(document.location.origin, ''), { responseType: 'text' });
  7403. });
  7404. }
  7405. function isTokenValid(accessToken) {
  7406. return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
  7407. && accessToken.expires_at >= Date.now() + 30000;
  7408. }
  7409. function isSessionValid(session) {
  7410. return session && typeof session == 'object' && session.userId > 0 && session.sessionId;
  7411. }
  7412. function getClientToken() {
  7413. if (prefs.tidal_token || (prefs.tidal_token = GM_getValue('tidal_token'))) return Promise.resolve(prefs.tidal_token);
  7414. return getTidalSecrets().then(function(response) {
  7415. if (!/"(\w{40})":"(\w{40})"/.test(response.responseText)) return Promise.reject('not found');
  7416. GM_setValue('tidal_token', prefs.tidal_token = RegExp.$2);
  7417. if (prefs.diag_mode) console.debug('Successfully configured Tidal token:', prefs.tidal_token);
  7418. return prefs.tidal_token;
  7419. }).catch(function(reason) {
  7420. console.warn('Tidal token detection fail (' + reason + ')');
  7421. return undefined;
  7422. });
  7423. }
  7424. }
  7425. function queryBeatsourceAPI(key, params, countryCode) {
  7426. if (!key) return Promise.reject('API endpoint not defined')
  7427. if (!urlParser.test(key)) key = 'v4/catalog/' + key;
  7428. return setOauth2Token().then(token => queryGenericAPI('api.beatsource.com', key, params, {
  7429. 'Authorization': token.token_type + ' ' + token.access_token,
  7430. }));
  7431.  
  7432. function setOauth2Token() {
  7433. try {
  7434. var accessToken = JSON.parse(window.localStorage.beatsourceAccessToken);
  7435. if (isTokenValid(accessToken)) {
  7436. if (prefs.diag_mode) console.debug('Re-used Beatsource access token:',
  7437. accessToken, new Date(accessToken.expires_at).toLocaleTimeString());
  7438. return Promise.resolve(accessToken);
  7439. }
  7440. } catch(e) { }
  7441. return globalFetch('https://www.beatsource.com/').then(function(response) {
  7442. accessToken = response.document.getElementById('__NEXT_DATA__');
  7443. if (accessToken == null) return Promise.reject('Beatsource access token is missing');
  7444. accessToken = JSON.parse(accessToken.text).props.rootStore.authStore.user;
  7445. const tzOffset = new Date().getTimezoneOffset() * 60 * 1000;
  7446. accessToken.timestamp -= tzOffset;
  7447. accessToken.expires_at -= tzOffset;
  7448. console.debug('Beatsource access token received:', accessToken, Date.now());
  7449. if (!isTokenValid(accessToken)) return Promise.reject('Received Beatsource invalid token');
  7450. window.localStorage.beatsourceAccessToken = JSON.stringify(accessToken);
  7451. return accessToken;
  7452. });
  7453. }
  7454. function isTokenValid(accessToken) {
  7455. return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
  7456. && accessToken.expires_at >= Date.now() + 30000;
  7457. }
  7458. }
  7459. function queryGenericAPI(hostName, endPoint, params, headers) {
  7460. return endPoint ? new Promise(function(resolve, reject) {
  7461. var url = urlParser.test(endPoint) ? new URL(endPoint) : new URL(endPoint, 'https://' + hostName),
  7462. query = new URLSearchParams(params);
  7463. if (Array.from(query).length > 0) url.search = query;
  7464. if (typeof headers != 'object') headers = {};
  7465. headers.Accept = 'application/json';
  7466. //if (prefs.diag_mode) console.debug('queryGenericAPI(...) requesting URL', url.href);
  7467. queryInternal();
  7468.  
  7469. function queryInternal() {
  7470. GM_xmlhttpRequest({
  7471. method: 'GET',
  7472. url: url,
  7473. responseType: 'json',
  7474. headers: headers,
  7475. onload: function(response) {
  7476. //if (prefs.diag_mode) console.debug('queryGenericAPI', domain, key, params, headers, response);
  7477. if (response.status >= 200 && response.status < 400) resolve(response.response);
  7478. else reject(defaultErrorHandler(response));
  7479. },
  7480. onerror: response => { reject(defaultErrorHandler(response)) },
  7481. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  7482. });
  7483. }
  7484. }) : Promise.reject(new Error('Keyword missing'));
  7485. }
  7486.  
  7487. function loadItunesMetadata(urlOrId) {
  7488. return (function() {
  7489. return /^https:\/\/apple\.co\//i.test(urlOrId) ? urlResolver(urlOrId).then(url => getAppleId(url)) : getAppleId(urlOrId);
  7490.  
  7491. function getAppleId(urlOrId) {
  7492. var appleId = parseInt(urlOrId) || itunesRlsParser.test(urlOrId) && parseInt(RegExp.$1);
  7493. return appleId ? Promise.resolve(appleId) : Promise.reject('Aplpe Id cannot be determined');
  7494. }
  7495. })().then(appleId => globalFetch('https://music.apple.com/album/' + appleId).then(function(response) {
  7496. var params = response.document.querySelector('meta[name="desktop-music-app/config/environment"][content]');
  7497. if (params != null) try { params = JSON.parse(decodeURIComponent(params.content)) } catch(e) {
  7498. console.warn('Apple desktop environment invalid format:', params.content);
  7499. return Promise.reject('Apple desktop environment invalid format');
  7500. } else return Promise.reject('Desktop environment not located');
  7501. if (prefs.diag_mode) console.debug('Got Apple Music desktop environment:', params);
  7502. if (!params.MEDIA_API.token) return Promise.reject('Apple access token not found');
  7503. var query = new URLSearchParams({
  7504. include: 'tracks,artists',
  7505. l: 'en-US',
  7506. });
  7507. return globalFetch(params.MUSIC.BASE_URL + '/catalog/us/albums/' + appleId + '?' + query, { responseType: 'json', headers: {
  7508. 'Referer': response.finalUrl,
  7509. 'Authorization': 'Bearer ' + params.MEDIA_API.token,
  7510. } }).then(function(response2) {
  7511. var album = response2.response.data[0];
  7512. album.description = response.document.querySelector('div.content-modal__content-container')
  7513. || response.document.querySelector('div.product-page-header__notes span'),
  7514. album.url = response.finalUrl;
  7515. if (album.attributes.artwork) album.attributes.artwork.realUrl = album.attributes.artwork.url
  7516. .replace('{w}', album.attributes.artwork.width).replace('{h}', album.attributes.artwork.height);
  7517. if (prefs.diag_mode) console.debug('Apple Music metadata received:', album);
  7518. // query.set('include', 'artists,albums');
  7519. // Promise.all(album.relationships.tracks.data.map(track => globalFetch(params.MUSIC.BASE_URL + '/catalog/us/songs/' + track.id + '?' + query, { responseType: 'json', headers: {
  7520. // 'Referer': response.finalUrl,
  7521. // 'Authorization': 'Bearer ' + params.MEDIA_API.token,
  7522. // } }).then(response => response.response))).then(tracks => { console.debug('Apple Music tracks received:', tracks) })
  7523. // .catch(reason => { console.error(reason) });
  7524. return album;
  7525. });
  7526. }));
  7527. }
  7528. function loadHDtracksMetadata(urlOrId, entity = undefined) {
  7529. if (!urlOrId) return Promise.reject('invalid argument');
  7530. if (/^\w+$/.test(urlOrId)) var id = RegExp.lastMatch.toString();
  7531. if (!id || !entity) try {
  7532. if (!(urlOrId instanceof URL)) urlOrId = new URL(urlOrId);
  7533. if (['hdtracks.com', 'www.hdtracks.com'].some(hostname => urlOrId.hostname == hostname)
  7534. && /^#\/(\w+)\/(\w+)\b/i.test(urlOrId.hash)) { entity = RegExp.$1; id = RegExp.$2 }
  7535. } catch(e) { console.warn(e) }
  7536. if (!id || !entity) return Promise.reject('invalid arguments');
  7537. return setSession().then(function(session) {
  7538. urlOrId = 'https://hdtracks.azurewebsites.net/api/v1/' + entity + '/' + id;
  7539. if (Object.keys(session).length > 0) urlOrId += '&' + new URLSearchParams(session);
  7540. return fetch(urlOrId).then(response => response.json()).catch(function(reason) {
  7541. console.warn('fetch(...) failed:', reason);
  7542. return globalFetch(urlOrId, { responseType: 'json', fetch: true }).then(response => response.response);
  7543. }).then(function(result) {
  7544. if (result.status.toLowerCase() != 'ok') return Promise.reject(result.status);
  7545. if (prefs.diag_mode) console.debug('HDtracks', entity, 'info loaded:', result);
  7546. return result;
  7547. });
  7548. });
  7549.  
  7550. function setSession() {
  7551. return Promise.resolve({
  7552. //token: 123456789,
  7553. });
  7554. }
  7555. }
  7556. function loadMoraMetadata(webUrl) {
  7557. return /^(?:https?):\/\/(?:\w+\.)*mora\.jp\/package\//i.test(webUrl) ? globalFetch(webUrl).then(function(response1) {
  7558. var appArguments = response1.document.querySelector('meta[name="msApplication-Arguments"][content]');
  7559. if (appArguments == null) return Promise.reject('Mora.jp: unexpected page format');
  7560. appArguments = JSON.parse(appArguments.content);
  7561. var materialNo = appArguments.materialNo.toString().padStart(10, '0'), offset = 0;
  7562. var packageUrl = 'https://cf.mora.jp/contents/' + [
  7563. appArguments.type, appArguments.mountPoint, appArguments.labelId,
  7564. ].concat([4, 3, 3].map(length => materialNo.slice(offset, offset += length))).join('/') + '/';
  7565. return globalFetch(packageUrl + 'packageMeta.jsonp', { responseType: 'text' }).then(function(response2) {
  7566. return /^\s*\w+\(\s*(\{[\S\s]+\})\s*\);\s*$/.test(response2.responseText) ? Object.assign(JSON.parse(RegExp.$1), {
  7567. mountPoint: appArguments.mountPoint,
  7568. webUrl: response1.finalUrl.replace(/[\?\#].*$/, ''),
  7569. }) : Promise.reject('Mora.jp: Unexpected package meta format');
  7570. });
  7571. }) : Promise.reject('Not mora.jp site URL');
  7572. }
  7573. function parseLastFm(album) {
  7574. if (typeof album != 'object') return Promise.reject('invalid object')
  7575. var identifiers = {}, description = [];
  7576. if (album.id) identifiers.LASTFM_ID = album.id;
  7577. if (album.mbid) identifiers.MBID = album.mbid;
  7578. if (album.wiki && album.wiki.summary) description.push(album.wiki.summary);
  7579. if (album.wiki && album.wiki.content) description.push(album.wiki.content);
  7580. description = description.join('\n\n');
  7581. var genres = album.tags.tag.map(tag => tag.name);
  7582. var imgUrl = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
  7583. return acc || album.image.find(image => image.size == size && urlParser.test(image['#text']));
  7584. }, undefined);
  7585. if (imgUrl) imgUrl = imgUrl['#text'].replace(/\/\d+(?:x\d+|s)\//i, '/');
  7586. return album.tracks.track.map((track, ndx) => ({
  7587. artist: album.artist,
  7588. album: album.name,
  7589. genre: genres.join('; ') || undefined,
  7590. title: track.name,
  7591. track_number: ndx + 1,
  7592. track_artist: !artistsMatch(track.artist.name, album.artist) ? track.artist.name : undefined,
  7593. duration: parseFloat(track.duration) || undefined,
  7594. url: album.url,
  7595. description: description || undefined,
  7596. identifiers: identifiers,
  7597. cover_url: imgUrl,
  7598. }));
  7599. }
  7600.  
  7601. function getMusicBrainzCovers(mbid) {
  7602. return searchInternal('release', mbid).then(covers => covers || searchMaster(), searchMaster);
  7603.  
  7604. function searchInternal(entity, mbid) {
  7605. return new Promise((resolve, reject) => GM_xmlhttpRequest({
  7606. method: 'GET',
  7607. url: 'https://coverartarchive.org/' + entity + '/' + mbid,
  7608. responseType: 'json',
  7609. onload: function(response) {
  7610. if (response.status == 404) return resolve(null);
  7611. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  7612. var images = response.response.images
  7613. .filter(image => urlParser.test(image.image) && image.isfront
  7614. || Array.isArray(image.types) && image.types.includesCaseless('Front'))
  7615. .map(image => image.image);
  7616. resolve(images.length > 0 ? [response.response.release, images] : null);
  7617. },
  7618. onerror: error => reject(defaultErrorHandler(error)),
  7619. ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
  7620. }));
  7621. }
  7622. function searchMaster() {
  7623. return queryMusicBrainzAPI('release/' + mbid, { inc: 'release-groups' })
  7624. .then(release => searchInternal('release-group', release['release-group'].id));
  7625. }
  7626. }
  7627.  
  7628. function tidalRlsParser(url) {
  7629. return /^https?:\/\/(?:\w+\.)*tidal\.com\//.test(url)
  7630. && (/\/album\/(\d+)(?:\/|$)/i.test(url) || /\/album(?:\/|\?).*\b(?:albumId)=(\d+)\b/i.test(url));
  7631. }
  7632. } // fillFromText_Music
  7633.  
  7634. function fillFromText_Apps(weak = false) {
  7635. if (messages != null) messages.parentNode.removeChild(messages);
  7636. if (!urlParser.test(clipBoard.value)) {
  7637. addMessage('valid URL accepted for this category', 'critical');
  7638. return false;
  7639. }
  7640. sourceUrl = RegExp.$1;
  7641. var description, tags = new TagManager();
  7642. if (sourceUrl.toLowerCase().includes('://sanet')) return globalFetch(sourceUrl).then(function(response) {
  7643. i = response.document.querySelector('h1.item_title > span');
  7644. var title = i == null ? undefined : i.textContent
  7645. .replace(/\s+\((?:x|ia|em)(?:64)\)/ig, ' (64-bit)')
  7646. .replace(/\s+\(x(?:86|32)\)/ig, ' (32-bit)')
  7647. .replace(/\s+(?:Build)\s+(\d+)\b/g, ' build $1')
  7648. .replace(/\s+(?:Multilingual|Multi(?:-|\s)*lang(?:uage)?)\b/g, ' multilingual');
  7649. description = html2php(response.document.querySelector('section.descr'), response.finalUrl).trim();
  7650. if (/\s*^[ \t]*(?:\[i\]\[\/i\])?Homepage\s*$.*/im.test(description)) description = RegExp.leftContext;
  7651. description = description.split(/[ \t]*\r?\n/).slice(6).map(line => line.trim()).join('\n')
  7652. .replace(/^[ \t]*(?:\[i\]\[\/i\])?Screenshots:?\s*/igm, '')
  7653. .replace(/^[ \t]*(?:\[i\]\[\/i\])?(\[b\]Release\s+Notes:?\[\/b\])(?:[ \t]*\r?\n)+/igm, '$1\n')
  7654. .replace(/\[hr\]/ig, '\n');
  7655. ref = response.document.querySelector('section.descr > div.release-info');
  7656. var releaseInfo = ref != null && ref.textContent.trim();
  7657. if (/\b(?:Languages?)\s*:\s*(.*?)\s*(?:$|\|)/i.exec(releaseInfo) != null)
  7658. description += '\n\n[b]Languages:[/b]\n' + RegExp.$1;
  7659. if ((ref = response.document.querySelector('div.txtleft > a')) != null) {
  7660. description += '\n\n[b]Product page:[/b]\n[url]' +
  7661. removeRedirect(ref.pathname.toLowerCase().startsWith('/confirm/url/') && urlParser.test(ref.textContent) ?
  7662. ref.textContent.trim() : ref.href) + '[/url]';
  7663. }
  7664. writeDescription(description.collapseGaps());
  7665. if ((ref = response.document.querySelector('section.descr > div.center > a.mfp-image')) != null) {
  7666. setCover(ref.href);
  7667. } else {
  7668. ref = response.document.querySelector('section.descr > div.center > img[data-src]');
  7669. if (ref != null) setCover(ref.dataset.src);
  7670. }
  7671. var internalTags = Array.from(response.document.querySelectorAll('ul.item_tags_list > li > a[rel="tag"]'))
  7672. .map(elem => elem.textContent.toLowerCase().trim());
  7673. if ((ref = response.document.querySelector('a.cat:last-of-type > span')) != null) {
  7674. if (ref.textContent.toLowerCase() == 'windows') {
  7675. tags.add('apps.windows');
  7676. if (/\b(?:(?:x|ia|em)64)\b/i.test(releaseInfo) || /\(64[-\s]*bit\)/i.test(title)) tags.add('win64');
  7677. if (/\b(?:x86|x32)\b/i.test(releaseInfo) || /\(32[-\s]*bit\)/i.test(title)) tags.add('win32');
  7678. }
  7679. if (ref.textContent.toLowerCase() == 'macos') tags.add('apps.mac');
  7680. if (ref.textContent.toLowerCase() == 'linux' || ref.textContent.toLowerCase() == 'unix') tags.add('apps.linux');
  7681. if (ref.textContent.toLowerCase() == 'android') tags.add('apps.android');
  7682. if (ref.textContent.toLowerCase() == 'ios') tags.add('apps.ios');
  7683. }
  7684. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  7685. if (title && !/\(\d+-?bit\)/i.test(title)) {
  7686. if (tags.includes('win64') && !tags.includes('win32')) title += ' (64-bit)';
  7687. if (tags.includes('win32') && !tags.includes('win64')) title += ' (32-bit)';
  7688. }
  7689. if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]')))
  7690. ref.value = title || '';
  7691. });
  7692. if (!weak) {
  7693. addMessage('this domain not supported', 'critical');
  7694. clipBoard.value = '';
  7695. }
  7696. return Promise.reject('this domain not supported');
  7697. } // fillFromText_Apps
  7698.  
  7699. function fillFromText_Ebooks(weak = false) {
  7700. if (messages != null) messages.parentNode.removeChild(messages);
  7701. if (!urlParser.test(clipBoard.value)) {
  7702. addMessage('only URL accepted for this category', 'critical');
  7703. return Promise.reject('only URL accepted for this category');
  7704. }
  7705. sourceUrl = new URL(RegExp.$1);
  7706. var params = new URLSearchParams(sourceUrl.search), description, tags = new TagManager;
  7707. if (sourceUrl.hostname.endsWith('martinus.cz') || sourceUrl.hostname.endsWith('martinus.sk')) return globalFetch(sourceUrl).then(function(response) {
  7708. function get_detail(x, y) {
  7709. var ref = response.document.querySelector('section#details > div > div > div:first-of-type > div:nth-child(' +
  7710. x + ') > dl:nth-child(' + y + ') > dd');
  7711. return ref != null ? ref.textContent.trim() : null;
  7712. }
  7713.  
  7714. i = response.document.querySelectorAll('article > ul > li > a');
  7715. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  7716. description = joinAuthors(i);
  7717. if ((i = response.document.querySelector('article > h1')) != null) description += ' – ' + i.textContent.trim();
  7718. i = response.document.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
  7719. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  7720. ref.value = description;
  7721. }
  7722. ref = response.document.querySelector('section#description > div');
  7723. if (ref != null) description = html2php(ref, response.finalUrl).replace(/^\s*\[img\].*?\[\/img\]\s*/i, '').trim();
  7724. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  7725. const translation_map = [
  7726. [/\b(?:originál)/i, 'Original title'],
  7727. [/\b(?:datum|dátum|rok)\b/i, 'Release date'],
  7728. [/\b(?:katalog|katalóg)/i, 'Catalogue #'],
  7729. [/\b(?:stran|strán)\b/i, 'Page count'],
  7730. [/\bjazyk/i, 'Language'],
  7731. [/\b(?:nakladatel|vydavatel)/i, 'Publisher'],
  7732. [/\b(?:doporuč|ODPORÚČ)/i, 'Age rating'],
  7733. ];
  7734. response.document.querySelectorAll('section#details > div > div > div:first-of-type > div > dl').forEach(function(detail) {
  7735. var lbl = detail.children[0].textContent.trim();
  7736. var val = detail.children[1].textContent.trim();
  7737. if (/\b(?:rozm)/i.test(lbl) || /\b(?:vazba|vázba)\b/i.test(lbl)) return;
  7738. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  7739. if (/\b(?:ISBN)\b/i.test(lbl)) {
  7740. let wc = 'https://www.worldcat.org/isbn/' + detail.children[1].textContent.trim();
  7741. val = '[url=' + wc + ']' + detail.children[1].textContent.trim() + '[/url]';
  7742. findOCLC(wc);
  7743. // } else if (/\b(?:ISBN)\b/i.test(lbl)) {
  7744. // val = '[url=https://www.goodreads.com/search/search?q=' + detail.children[1].textContent.trim() +
  7745. // '&search_type=books]' + detail.children[1].textContent.trim() + '[/url]';
  7746. }
  7747. description += '\n[b]' + lbl + ':[/b] ' + val;
  7748. });
  7749. description += `\n\n[b]More info and reviews:[/b]\n[url]${response.finalUrl}[/url]`;
  7750. writeDescription(description.collapseGaps());
  7751. if ((i = response.document.querySelector('a.mj-product-preview > img')) != null)
  7752. setCover(i.src.replace(/\?.*/, ''));
  7753. else if ((i = response.document.querySelector('head > meta[property="og:image"][content]')) != null)
  7754. setCover(i.content.replace(/\?.*/, ''));
  7755. response.document.querySelectorAll('dd > ul > li > a').forEach(x => { tags.add(x.textContent) });
  7756. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  7757. }); if (sourceUrl.hostname.endsWith('goodreads.com')) return globalFetch(sourceUrl).then(function(response) {
  7758. var authors = response.document.querySelectorAll('div#bookAuthors > span[itemprop="author"] a.authorName > span[itemprop="name"]');
  7759. if (authors.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  7760. let titleAuthors = Array.from(authors).filter(span => span.parentNode.parentNode.querySelector('span.role') == null);
  7761. if (titleAuthors.length <= 0) titleAuthors = Array.from(authors);
  7762. description = titleAuthors.length > 0 ? joinAuthors(titleAuthors) + ' – ' : '';
  7763. if ((i = response.document.querySelector('h2#bookSeries')) != null
  7764. && (i = i.textContent.trim().replace(/^\((.*)\)$/, '$1'))) description += i + ': ';
  7765. if ((i = response.document.querySelector('h1#bookTitle')) != null) {
  7766. description += i.textContent.trim();
  7767. ref.title = i.textContent.trim();
  7768. }
  7769. if ((i = response.document.querySelector('div#details > div.row:nth-of-type(2)')) != null
  7770. && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  7771. ref.value = description;
  7772. }
  7773. var otherAuthors = [];
  7774. authors.forEach(function(span) {
  7775. var role = span.parentNode.parentNode.querySelector('span.role');
  7776. if (role == null) return;
  7777. role = role.textContent.trim();
  7778. if (/^\((.+)\)$/.test(role)) role = RegExp.$1;
  7779. otherAuthors.push(`[b]${role}:[/b] [url=https://www.goodreads.com${span.parentNode.pathname}]${span.textContent.trim()}[/url]`);
  7780. });
  7781. description = undefined;
  7782. response.document.querySelectorAll('div#description span:last-of-type')
  7783. .forEach(node => { description = html2php(node, response.finalUrl).trim() });
  7784. if (description && !description.includes('[quote]'))
  7785. description = '[quote]' + description.collapseGaps() + '[/quote]';
  7786. response.document.querySelectorAll('div#details > div.row')
  7787. .forEach(div => { description += '\n' + strip(div.textContent) });
  7788. if (description) description += '\n'; else description = '';
  7789. if (otherAuthors.length > 0) description += '\n' + otherAuthors.join('\n');
  7790. response.document.querySelectorAll('div#bookDataBox > div.clearFloats').forEach(function(detail) {
  7791. var key = detail.querySelector('div.infoBoxRowTitle'), desc = detail.querySelector('div.infoBoxRowItem');
  7792. if (key == null || desc == null) {
  7793. console.warn('Goodreads assertion failed:', detail);
  7794. return;
  7795. }
  7796. key = key.textContent.trim();
  7797. var value = strip(desc.textContent);
  7798. if (/\b(?:ISBN)\b/i.test(key) && (/\b(\d{13})\b/.test(value) || /\b(\d{10})\b/.test(value))) {
  7799. let wc = 'https://www.worldcat.org/isbn/' + RegExp.$1;
  7800. value = `[url=${wc}]${value}[/url]`;
  7801. findOCLC(wc);
  7802. } else value = strip(html2php(desc, response.finalUrl), ', ');
  7803. if (value) description += `\n[b]${key}:[/b] ${value}`;
  7804. });
  7805. if ((ref = response.document.querySelector('span[itemprop="ratingValue"]')) != null)
  7806. description += `\n[b]Rating:[/b] ${Math.round(parseFloat(ref.firstChild.textContent) * 20)}%`;
  7807. sourceUrl = new URL(response.finalUrl);
  7808. // if ((ref = response.document.querySelector('div#buyButtonContainer > ul > li > a.buttonBar')) != null) {
  7809. // let u = new URL(ref.href);
  7810. // description += '\n[url=' + sourceUrl.origin + u.pathname + '?' + u.search + ']Libraries[/url]';
  7811. // }
  7812. description += `\n\n[b]More info and reviews:[/b]\n[url]${sourceUrl.origin}${sourceUrl.pathname}[/url]`;
  7813. response.document.querySelectorAll('div.clearFloats.bigBox').forEach(function(bigBox) {
  7814. if (bigBox.id == 'aboutAuthor' && (ref = bigBox.querySelector('h2 > a')) != null) {
  7815. description += `\n\n[b][url=https://www.goodreads.com${ref.pathname}]${ref.textContent.trim()}[/url][/b]`;
  7816. //if ((ref = bigBox.querySelector('div.bigBoxBody a > div[style*="background-image"]')) != null)
  7817. if ((ref = bigBox.querySelector('div.bookAuthorProfile__about > span[id]:last-of-type')) != null)
  7818. description += '\n' + html2php(ref, response.finalUrl).trim().replace(/^\[i\]Librarian\s+Note:.*?\[\/i\]\s+/i, '');
  7819. // } else if ((ref = bigBox.querySelector('h2 > a[href^="/trivia/"]')) != null) {
  7820. // description += `\n\n[b][url=https://www.goodreads.com${ref.pathname}]${ref.textContent.trim()}[/url][/b]`;
  7821. // if ((ref = bigBox.querySelector('div.bigBoxContent > div.mediumText')) != null
  7822. // && !/^\s*(?:No trivia)\b/.test(ref.textContent)) description += '\n' + ref.firstChild.textContent.trim();
  7823. // } else if ((ref = bigBox.querySelector('h2 > a[href^="/work/quotes/"]')) != null) {
  7824. // description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
  7825. // bigBox.querySelectorAll('div.bigBoxContent > div.stacked > span.readable').forEach(function(quote) {
  7826. // description += '\n' + ref.firstChild.textContent.trim();
  7827. // });
  7828. }
  7829. });
  7830. writeDescription(description.collapseGaps());
  7831. if ((ref = response.document.querySelector('div.editionCover > img')) != null)
  7832. setCover(ref.src.replace(/\?.*/, ''));
  7833. response.document.querySelectorAll('div.elementList > div.left')
  7834. .forEach(tag => { tags.add(tag.textContent.trim()) });
  7835. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  7836.  
  7837. function strip(str, joiner = undefined) {
  7838. //console.debug('Stripping', str);
  7839. return str.replace(/[\r\n]+/, ' ')
  7840. .replace(/\s*\[url(?:=\S+?)?\]\s*\.{3,}(?:less|more)\s*\[\/url\]\s*/g, joiner || ' ')
  7841. .replace(/\s*\.{3,}(?:less|more)\s*/g, joiner || ' ').replace(/\s{2,}/g, ' ').trim();
  7842. }
  7843. }); else if (sourceUrl.hostname.endsWith('databazeknih.cz')) {
  7844. params.set('show', 'alldesc');
  7845. sourceUrl.search = params;
  7846. return globalFetch(sourceUrl).then(function(response) {
  7847. i = response.document.querySelectorAll('span[itemprop="author"] > a');
  7848. if (i != null && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  7849. description = joinAuthors(i);
  7850. if ((i = response.document.querySelector('h1[itemprop="name"]')) != null)
  7851. description += ' – ' + i.textContent.trim();
  7852. i = response.document.querySelector('span[itemprop="datePublished"]');
  7853. if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
  7854. ref.value = description;
  7855. }
  7856.  
  7857. ref = response.document.querySelector('p[itemprop="description"]');
  7858. if (ref != null) description = html2php(ref, response.finalUrl).trim();
  7859. if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
  7860. const translation_map = [
  7861. [/\b(?:orig)/i, 'Original title'],
  7862. [/\b(?:série)\b/i, 'Series'],
  7863. [/\b(?:vydáno)\b/i, 'Released'],
  7864. [/\b(?:stran)\b/i, 'Page count'],
  7865. [/\b(?:jazyk)\b/i, 'Language'],
  7866. [/\b(?:překlad)/i, 'Translation'],
  7867. [/\b(?:autor obálky)\b/i, 'Cover author'],
  7868. ];
  7869. response.document.querySelectorAll('table.bdetail tr').forEach(function(detail) {
  7870. var lbl = detail.children[0].textContent.trim();
  7871. var val = detail.children[1].textContent.trim();
  7872. if (/(?:žánr|\bvazba)\b/i.test(lbl)) return;
  7873. translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
  7874. if (/\b(?:ISBN)\b/i.test(lbl) && /\b(\d+(?:-\d+)*)\b/.exec(val) != null) {
  7875. let wc = 'https://www.worldcat.org/isbn/' + RegExp.$1.replace(/-/g, '');
  7876. val = `[url=${wc}]${detail.children[1].textContent.trim()}[/url]`;
  7877. findOCLC(wc);
  7878. }
  7879. description += '\n[b]' + lbl + '[/b] ' + val;
  7880. });
  7881.  
  7882. sourceUrl = new URL(response.finalUrl);
  7883. description += `\n\n[b]More info:[/b]\n[url]${sourceUrl.origin}${sourceUrl.pathname}[/url]`;
  7884. writeDescription(description.collapseGaps());
  7885. if ((ref = response.document.querySelector('div#icover_mid > a')) != null) setCover(ref.href.replace(/\?.*/, ''));
  7886. else if ((ref = response.document.querySelector('div#lbImage')) != null
  7887. && /\burl\("(.*)"\)/i.test(i.style.backgroundImage)) setCover(RegExp.$1.replace(/\?.*/, ''));
  7888. response.document.querySelectorAll('h5[itemprop="genre"] > a')
  7889. .forEach(tag => { tags.add(tag.textContent.trim()) });
  7890. response.document.querySelectorAll('a.tag').forEach(tag => { tags.add(tag.textContent.trim()) });
  7891. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  7892. });
  7893. } else if (sourceUrl.hostname.endsWith('boomkat.com') && sourceUrl.pathname.startsWith('/products/')) return globalFetch(sourceUrl).then(function(response) {
  7894. i = Array.from(response.document.querySelectorAll('ul.product-page-tabs > li.tab-title > a'))
  7895. .filter(a => a.textContent.trim() == 'Book');
  7896. if (i.length <= 0) return Promise.reject('This doesn\'t appear as a book');
  7897. var releaseDate = i[0].dataset.releaseDate,
  7898. publisher = i[0].dataset.label,
  7899. catalogue = i[0].dataset.catalogueNumber;
  7900. description = (ref = response.document.querySelector('div' + i[0].hash + ' p.product-extra-info')) != null ?
  7901. ref.textContent.trim() + '\n\n' : '';
  7902. if (releaseDate) description += '[b]Release date:[/b] ' + releaseDate + '\n';
  7903. if (publisher) description += '[b]Publisher:[/b] ' + publisher + '\n';
  7904. if (catalogue) description += '[b]Catalogue №:[/b] ' + catalogue + '\n';
  7905. if ((ref = response.document.querySelector('div.show-for-medium-up > div.product-review')) != null) {
  7906. if (description.length > 0) description += '\n';
  7907. description += '[quote]' + html2php(ref).trim() + '[/quote]';
  7908. }
  7909. if (description) writeDescription(description.collapseGaps());
  7910. i = response.document.querySelectorAll('div#right_content > h1.detail--artists > a');
  7911. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  7912. description = joinAuthors(Array.from(i));
  7913. if ((i = response.document.querySelector('div#right_content > h2.detail_album')) != null)
  7914. description += ' – ' + i.textContent.trim();
  7915. if ((i = extractYear(releaseDate)) > 0) description += ' (' + i + ')';
  7916. ref.value = description;
  7917. }
  7918. if ((ref = response.document.querySelector('img[itemprop="image"]')) != null)
  7919. setCover(ref.src.replace(/\/large\//i, '/original/'));
  7920. }); else if (sourceUrl.hostname.endsWith('openlibrary.org')
  7921. && ['books', 'works'].some(p => sourceUrl.pathname.startsWith('/' + p + '/'))) return globalFetch(sourceUrl, {
  7922. headers: { 'Accept-Language': 'en-US, en' },
  7923. }).then(function(response) {
  7924. i = response.document.querySelectorAll('div.editionAbout h2.edition-byline > a[itemprop="author"]');
  7925. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  7926. description = joinAuthors(i);
  7927. if ((i = response.document.querySelector('div.editionAbout h1.work-title')) != null) {
  7928. description += ' – ' + i.textContent.trim();
  7929. if ((i = response.document.querySelector('div.editionAbout h2.work-subtitle')) != null
  7930. && i.textContent.trim().length > 0) description += ': ' + i.textContent.trim();
  7931. }
  7932. if ((i = response.document.querySelector('strong[itemprop="datePublished"]')) != null)
  7933. description += ' (' + extractYear(i.textContent) + ')';
  7934. ref.value = description;
  7935. }
  7936. description = '';
  7937. response.document.querySelectorAll('div.work-description > p')
  7938. .forEach(p => { description += '\n\n' + html2php(p, response.finalUrl).trim() });
  7939. if (!description) response.document.querySelectorAll('div.book-description-content > p')
  7940. .forEach(p => { description += '\n\n' + html2php(p, response.finalUrl).trim() });
  7941. if (!description) response.document.querySelectorAll('div.editionAbout > p')
  7942. .forEach(p => { description += '\n\n' + html2php(p, response.finalUrl).trim() });
  7943. if (description && !description.includes('[quote]'))
  7944. description = '[quote]' + description.collapseGaps() + '[/quote]';
  7945. response.document.querySelectorAll('div.editionAbout > div.section').forEach(function(div) {
  7946. if (div.classList.length > 1) return;
  7947. description += '\n\n' + html2php(div, response.finalUrl);
  7948. });
  7949. response.document.querySelectorAll('div.tab-section > div.section > h6').forEach(function(h6) {
  7950. i = h6.parentNode.querySelectorAll('span > a');
  7951. if (i.length > 0) description += '\n\n[b]' + h6.textContent.trim() + ':[/b] ' + Array.from(i)
  7952. .map(a => `[url=https://openlibrary.org${a.pathname}]${a.textContent.trim()}[/url]`).join(', ');
  7953. });
  7954. if ((ref = response.document.querySelector('div.editionAbout h4.publisher')) != null)
  7955. description = html2php(ref, response.finalUrl).trim() + description;
  7956. if ((ref = response.document.querySelector('span[itemprop="ratingValue"]')) != null)
  7957. description += `\n\n[b]Rating:[/b] ${Math.round(parseFloat(ref.textContent) * 20)}%`;
  7958. description += '\n';
  7959. var worldCat;
  7960. response.document.querySelectorAll('div.section > dl.meta > dt').forEach(function(dt) {
  7961. if (dt.nextElementSibling == null || dt.nextElementSibling.nodeName != 'DD') return;
  7962. var desc = html2php(dt.nextElementSibling, response.finalUrl).trim();
  7963. if (desc) description += '\n[b]' + dt.textContent.trim() + '[/b]: ' + desc;
  7964. if ((ref = dt.nextElementSibling.querySelector('a')) != null
  7965. && ref.href.startsWith('https://www.worldcat.org')) worldCat = ref.href;
  7966. });
  7967. if ((ref = response.document.querySelector('meta[property="og:url"][content]')) != null)
  7968. description += `\n\n[b]More info and reviews:[/b]\n[url]${ref.content}[/url]`;
  7969. writeDescription(description.collapseGaps());
  7970. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) setCover(ref.content);
  7971. response.document.querySelectorAll('div.section.link-box > div > span > a').forEach(function(a) {
  7972. if (!a.pathname.startsWith('/subjects/') || a.pathname.includes(':')) return;
  7973. tags.add(a.textContent.trim());
  7974. });
  7975. if (tags.length <= 0) response.document.querySelectorAll('div.subjects span > a').forEach(function(a) {
  7976. if (!a.pathname.startsWith('/subjects/') || a.pathname.includes(':')) return;
  7977. tags.add(a.textContent.trim());
  7978. });
  7979. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  7980. if (!worldCat) response.document.querySelectorAll('div.section > dl.meta > dd[itemprop="isbn"]').forEach(function(dd) {
  7981. if (/^(?:\d{13}|\d{10})$/.test(dd.textContent.trim())) i = 'https://www.worldcat.org/isbn/' + RegExp.$1;
  7982. });
  7983. if (worldCat) findOCLC(worldCat);
  7984. }); else if (sourceUrl.hostname.startsWith('books.google.')) {
  7985. params.set('hl', 'en');
  7986. sourceUrl.search = params;
  7987. return globalFetch(sourceUrl).then(function(response) {
  7988. i = response.document.querySelectorAll('td#bookinfo > div.bookinfo_sectionwrap > div:first-of-type > a.secondary');
  7989. if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
  7990. description = joinAuthors(i);
  7991. if ((i = response.document.querySelector('td#bookinfo > h1.booktitle > span.fn')) != null) {
  7992. description += ' – ' + i.textContent.trim();
  7993. if ((i = i.parentNode.querySelector('span.subtitle')) != null && i.textContent.trim().length > 0)
  7994. description += ': ' + i.textContent.trim();
  7995. }
  7996. if ((i = response.document.querySelector('td#bookinfo > div.bookinfo_sectionwrap > div:nth-of-type(2)')) != null)
  7997. description += ' (' + extractYear(i.textContent) + ')';
  7998. ref.value = description;
  7999. }
  8000. description = (ref = response.document.querySelector('div#synopsistext')) != null ?
  8001. html2php(ref, response.finalUrl).trim() : '';
  8002. if (description && !description.includes('[quote]'))
  8003. description = '[quote]' + description.trim() + '[/quote]';
  8004. response.document.querySelectorAll('table#metadata_content_table > tbody > tr.metadata_row').forEach(function(tr) {
  8005. var key = tr.querySelector('td.metadata_label'), value = tr.querySelector('td.metadata_value');
  8006. if (key == null || value == null) {
  8007. console.warn('Google Books assertion failed:', tr);
  8008. return;
  8009. }
  8010. key = key.textContent.trim(); value = value.textContent.trim();
  8011. if (key.toLowerCase() == 'subjects') {
  8012. tr.querySelectorAll('td.metadata_value span[itemprop="title"]')
  8013. .forEach(span => { tags.add(span.textContent.trim()) });
  8014. return;
  8015. }
  8016. if (key.toLowerCase() == 'isbn' && (/\b(\d{13})\b/.test(value) || /\b(\d{10})\b/.test(value))) {
  8017. let wc = 'https://www.worldcat.org/isbn/' + RegExp.$1;
  8018. value = `[url=${wc}]${value}[/url]`;
  8019. findOCLC(wc);
  8020. }
  8021. description += `\n[b]${key}:[/b] ${value}`;
  8022. });
  8023. if ((ref = response.document.querySelector('td#bookinfo > div.bookinfo_sectionwrap span.rating > span.value-title[title]')) != null)
  8024. description += `\n[b]Rating:[/b] ${Math.round(parseFloat(ref.title) * 20)}%`;
  8025. if ((ref = response.document.querySelector('meta[property="og:url"][content]')) != null)
  8026. description += `\n\n[b]More info and reviews:[/b]\n[url]${ref.content}[/url]`;
  8027. if ((ref = response.document.querySelector('div#about_author')) != null)
  8028. description += '\n\n[b]About the author:[/b]\n' + html2php(ref, response.finalUrl).trim();
  8029. writeDescription(description.collapseGaps());
  8030. if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  8031. setCover(ref.content + '=s0');
  8032. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  8033. });
  8034. } else if (sourceUrl.hostname.endsWith('play.google.com') && sourceUrl.pathname.startsWith('/store/books/details/')) {
  8035. params.set('hl', 'en');
  8036. sourceUrl.search = params;
  8037. return globalFetch(sourceUrl).then(function(response) {
  8038. var metaData;
  8039. response.document.querySelectorAll('script[type="application/ld+json"]').forEach(function(script) {
  8040. if (!metaData) try {
  8041. metaData = JSON.parse(script.text);
  8042. if (metaData['@type'] != 'Book') metaData = undefined;
  8043. } catch(e) { }
  8044. });
  8045. if (!metaData) throw 'Invalid document format';
  8046. if (prefs.diag_mode) console.debug('Google Play Books metadata loaded:', metaData);
  8047. var initDataCallback;
  8048. loadGoogleMetadata(response).forEach(function(pattern) {
  8049. if (initDataCallback || !Array.isArray(pattern) || pattern.length != 1 || !Array.isArray(pattern[0])
  8050. || pattern[0].length != 22) return;
  8051. initDataCallback = pattern[0];
  8052. });
  8053. if (!initDataCallback) throw 'Invalid document format';
  8054. if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]')))
  8055. ref.value = `${initDataCallback[8][1][4]} ${initDataCallback[0][0]} (${extractYear(initDataCallback[8][3][1])})`;
  8056. if (prefs.diag_mode) console.debug('Google Play Books metadata loaded:', initDataCallback);
  8057. description = initDataCallback[8][0][1] ?
  8058. html2php(domParser.parseFromString(initDataCallback[8][0][1], 'text/html').body, response.finalUrl).trim() : '';
  8059. if (description && !description.includes('[quote]'))
  8060. description = '[quote]' + description.trim() + '[/quote]';
  8061. description += '\n' + initDataCallback[2].map(function(elem) {
  8062. var value;
  8063. if (elem[0] == 'ISBN' && (/\b(\d{13})\b/.test(elem[1][0][0][1]) || /\b(\d{10})\b/.test(elem[1][0][0][1])))
  8064. value = `[url=https://www.worldcat.org/isbn/${RegExp.$1}]${elem[1][0][0][1]}[/url]`;
  8065. else value = elem[1].map(el => html2php(domParser.parseFromString(el[0][1], 'text/html').body,
  8066. response.finalUrl).trim()).join(', ');
  8067. if (elem[0] == 'Genres') elem[1]
  8068. .forEach(el => { tags.add(...domParser.parseFromString(el[0][1], 'text/html').body.textContent.trim().split(/\s*\/\s*/)) });
  8069. return `\n[b]${elem[0]}:[/b] ${value}`;
  8070. }).join('');
  8071. if (metaData.aggregateRating && metaData.aggregateRating.ratingValue)
  8072. description += `\n[b]Rating:[/b] ${Math.round(parseFloat(metaData.aggregateRating.ratingValue) * 20)}%`;
  8073. if (initDataCallback[8][1][0] && initDataCallback[8][1][0][1]) description += '\n\n[b]About the author:[/b]\n' +
  8074. html2php(domParser.parseFromString(initDataCallback[8][1][0][1], 'text/html').body, response.finalUrl).trim();
  8075. writeDescription(description.collapseGaps());
  8076. if (urlParser.test(initDataCallback[8][4][3][2])) setCover(initDataCallback[8][4][3][2] + '=s0');
  8077. else if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null)
  8078. setCover(ref.content + '=s0');
  8079. if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
  8080. if (metaData.workExample && metaData.workExample.isbn)
  8081. findOCLC('https://www.worldcat.org/isbn/' + metaData.workExample.isbn);
  8082. });
  8083. }
  8084. if (!weak) {
  8085. addMessage('domain not supported', 'critical');
  8086. clipBoard.value = '';
  8087. }
  8088. return Promise.reject('domain not supported');
  8089.  
  8090. function joinAuthors(nodeList) {
  8091. if (typeof nodeList != 'object' && !Array.isArray(nodeList)) return null;
  8092. return Array.from(nodeList).map(node => node.textContent.trim()).join(' & ');
  8093. }
  8094.  
  8095. function findOCLC(url) {
  8096. if (!url) return false;
  8097. var oclc = document.querySelector('input[name="oclc"]');
  8098. if (!elementWritable(oclc)) return false;
  8099. globalFetch(url).then(function(response) {
  8100. var ref = response.document.querySelector('tr#details-oclcno > td:last-of-type');
  8101. if (ref != null) oclc.value = ref.textContent.trim();
  8102. });
  8103. return true;
  8104. }
  8105. } // fillFromText_Ebooks
  8106.  
  8107. function preview(n) {
  8108. if (!prefs.auto_preview) return;
  8109. var btn = document.querySelector('input.button_preview_' + n + '[type="button"][value="Preview"]');
  8110. if (btn != null) btn.click();
  8111. }
  8112.  
  8113. function writeDescription(desc) {
  8114. if (typeof desc != 'string') return;
  8115. if (elementWritable(ref = document.querySelector('textarea#desc')
  8116. || document.querySelector('textarea#description') || document.querySelector('textarea#album_desc'))) ref.value = desc;
  8117. if ((ref = document.getElementById('body')) != null && !ref.disabled) {
  8118. if (ref.value.length > 0) ref.value += '\n\n';
  8119. ref.value += desc;
  8120. }
  8121. }
  8122.  
  8123. function queryAjaxAPI(action, params) {
  8124. if (!action) return Promise.reject('Action missing');
  8125. let retryCount = 0;
  8126. return new Promise(function(resolve, reject) {
  8127. params = new URLSearchParams(params || undefined);
  8128. params.set('action', action);
  8129. let url = '/ajax.php?' + params, xhr = new XMLHttpRequest;
  8130. queryInternal();
  8131.  
  8132. function queryInternal() {
  8133. let now = Date.now();
  8134. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  8135. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  8136. apiTimeFrame.timeStamp = now;
  8137. apiTimeFrame.requestCounter = 1;
  8138. } else ++apiTimeFrame.requestCounter;
  8139. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  8140. if (apiTimeFrame.requestCounter <= 5) {
  8141. xhr.open('GET', url, true);
  8142. xhr.setRequestHeader('Accept', 'application/json');
  8143. if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
  8144. xhr.responseType = 'json';
  8145. xhr.onload = function() {
  8146. if (xhr.status == 404) return reject('not found');
  8147. if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
  8148. if (xhr.response.status == 'success') return resolve(xhr.response.response);
  8149. if (xhr.response.error == 'not found') return reject(xhr.response.error);
  8150. console.warn('queryAjaxAPI.queryInternal(...) response:', xhr, xhr.response);
  8151. if (xhr.response.error == 'rate limit exceeded') {
  8152. console.warn('queryAjaxAPI.queryInternal(...) ' + xhr.response.error + ':', apiTimeFrame, now, retryCount);
  8153. if (retryCount++ <= 10) return setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  8154. }
  8155. reject('API ' + xhr.response.status + ': ' + xhr.response.error);
  8156. };
  8157. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  8158. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  8159. xhr.timeout = 10000;
  8160. xhr.send();
  8161. } else {
  8162. let delay = apiTimeFrame.timeStamp + gazelleApiFrame - now;
  8163. setTimeout(queryInternal, delay);
  8164. if (prefs.diag_mode) console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
  8165. action + ' (' + apiTimeFrame.requestCounter + ')');
  8166. let msg = 'waiting ' + Math.ceil(delay / 1000) + 's for next AJAX timeframe';
  8167. if (prefs.messages_verbosity >= 1)
  8168. addMessage(msg + '; action=' + action + ' (' + apiTimeFrame.requestCounter + ')', 'notice');
  8169. else addMessage(msg, 'notice');
  8170. }
  8171. }
  8172. });
  8173. }
  8174.  
  8175. function setCover(url) {
  8176. if (!urlParser.test(url)) return Promise.reject('Image url not valid');
  8177. var image = document.getElementById('image') || document.querySelector('input[name="image"]');
  8178. if (!elementWritable(image)) return Promise.reject('Image input not available');
  8179. return verifyImageUrl(url).then(function(imageUrl) {
  8180. image.value = imageUrl;
  8181. coverPreview(image, imageUrl);
  8182. checkImageSize(imageUrl, image).then(function(imageUrl) {
  8183. if (!prefs.auto_rehost_cover && !isNWCD) return;
  8184. image.disabled = true;
  8185. return imageHosts.rehostImages([imageUrl]).then(singleImageGetter)
  8186. .then(imgUrl => { if (imgUrl != null) image.value = imgUrl },
  8187. reason => { addMessage(reason + ' (not rehosted)', 'warning') })
  8188. .then(() => { image.disabled = false; return imageUrl; });
  8189. });
  8190. });
  8191. }
  8192.  
  8193. function elementWritable(elem) {
  8194. return elem != null && !elem.disabled && (overwrite || elem.value == '' || !isRED && elem.value == '---');
  8195. }
  8196.  
  8197. function loadGoogleMetadata(response) {
  8198. const initDataParser = /\b(?:AF_initDataCallback)\s*\(\s*\{\s*key:\s*'ds:(\d+)'.*data:\s*function\(\)\s*{\s*return\s*([\S\s]+)\}\s*\}\s*\);/;
  8199. return Array.from(response.document.querySelectorAll('script[nonce]'))
  8200. .map(function(script) { try { return eval(initDataParser.exec(script.text)[2]) } catch(e) { return false } })
  8201. .filter(obj => obj && typeof obj == 'object');
  8202. }
  8203.  
  8204. function getFriendlyTime(timeStr) {
  8205. let now = Date.now(), timeStamp = timeStr.split(/\D+/).map(a => parseInt(a)); --timeStamp[1];
  8206. timeStamp = Date.UTC(...timeStamp);
  8207. if (isNaN(timeStamp)) {
  8208. console.error('Date string could not be converted to UTC time:', timeStr);
  8209. return '[invalid time]';
  8210. }
  8211. let offset = Math.round((now - timeStamp) / 1000);
  8212. if (offset < 60) timeStamp = offset.toString() + ' seconds ago';
  8213. else if (offset < 60 * 60) timeStamp = Math.round(offset / 60).toString() + ' minutes ago';
  8214. else if (offset < 12 * 60**2) timeStamp = Math.round(offset / 60**2).toString() + ' hours ago';
  8215. else {
  8216. timeStamp = new Date(timeStamp);
  8217. timeStamp = new Date(now).getDateValue() != timeStamp.getDateValue() ?
  8218. 'on ' + timeStamp.toDateString() : 'at ' + timeStamp.toTimeString();
  8219. }
  8220. if (timeStamp.startsWith('1 ')) timeStamp = timeStamp.replace('s ago', ' ago');
  8221. return timeStamp;
  8222. }
  8223. function getGroupRef(torrent) {
  8224. return '<a href="/torrents.php?id=' + torrent.groupId +
  8225. '" target="_blank" style="' + hyperlinkStyle + '">' + torrent.groupName + '</a>';
  8226. }
  8227. function getTorrentRef(torrent) {
  8228. return '<a href="/torrents.php?id=' + torrent.groupId + '&torrentid=' + torrent.id +
  8229. '" target="_blank" style="' + hyperlinkStyle + '">' + torrent.id + '</a>';
  8230. }
  8231. function getRequestRef(request) {
  8232. return '<a href="/requests.php?action=view&id=' + request.requestId +
  8233. '" target="_blank" style="' + hyperlinkStyle + '">' + (request.title || request.requestId) + '</a>';
  8234. }
  8235. function getUserRef(torrent) {
  8236. return '<a href="/users.php?id=' + torrent.userId + '" target="_blank" style="' +
  8237. hyperlinkStyle + '">' + torrent.username + '</a>';
  8238. }
  8239. function getRequestInfo(request) {
  8240. var totalBounty = request.totalBounty || request.bounty;
  8241. if (!(totalBounty > 0)) {
  8242. console.warn('Failed to get request bounty:', request);
  8243. return '???';
  8244. }
  8245. const voteGlyph = '<img src="https://ptpimg.me/3s2w7o.png" style="height: 8px; margin-right: 3px;" />'
  8246. if (totalBounty >= 2**30) totalBounty = (Math.round(totalBounty * 100 / 2**30) / 100).toString() + ' GiB';
  8247. else totalBounty = Math.round(totalBounty / 2**20).toString() + ' MiB';
  8248. return `(${voteGlyph}${request.voteCount} / ${totalBounty})`;
  8249. }
  8250.  
  8251. function lookupNonMusicRelations() {
  8252. if (!prefs.find_relations || category == null || category.selectedIndex == 0) return;
  8253. let title = document.getElementById('title') || document.querySelector('input[name="title"]');
  8254. if (title == null || !title.value) return;
  8255. const similarityThreshold = 0.70;
  8256. const titleStrippers = [
  8257. [bracketStripper, ''],
  8258. [/[\-\−\—\–\:\|\/\<\>]+/g, ' '],
  8259. [/[\"]+/g, ''],
  8260. [/\s{2,}/g, ' '],
  8261. ];
  8262. const altTitleStrippers = [
  8263. [/^(?:[^\-\−\—\–]+?)\s+[\-\−\—\–]\s+/, ''],
  8264. //[/^(?:[^:]+?):\s+/, ''],
  8265. ];
  8266. function getAltSearchTerm() {
  8267. return title.title ? titleStrippers.reduce((r, def) => r.replace(...def), title.title)
  8268. : altTitleStrippers.concat(titleStrippers).reduce((r, def) => r.replace(...def), title.value);
  8269. };
  8270. let searchTerm = titleStrippers.reduce((m, substDef) => m.replace(...substDef), title.value);
  8271. // Find existing torrents
  8272. function searchTorrents(searchTerm) {
  8273. return queryAjaxAPI('browse', {
  8274. //groupname: title.value,
  8275. searchstr: searchTerm,
  8276. //order_by: 'time',
  8277. //order_way: 'desc',
  8278. ['filter_cat[' + (category.selectedIndex + 1) + ']']: 1,
  8279. });
  8280. }
  8281. searchTorrents(searchTerm).then(function(response) {
  8282. function printResults(results) {
  8283. results.forEach(function(torrent) {
  8284. if (reportedTorrentCollicions.has(torrent.id)) return;
  8285. let time = new Date(parseInt(torrent.groupTime) * 1000);
  8286. time = !isNaN(time) ? time.toISOString() : torrent.groupTime;
  8287. if (isUpload) reportedTorrentCollicions.set(torrent.id,
  8288. addMessage(new HTML('possible dupe to torrent ' + getGroupRef(torrent) + ' ' + getFriendlyTime(time)), 'warning'));
  8289. else if (isRequestNew) reportedTorrentCollicions.set(torrent.id,
  8290. addMessage(new HTML('requested release possibly already on site: ' +
  8291. getGroupRef(torrent) + ' ' + getFriendlyTime(time)), 'notice'));
  8292. });
  8293. }
  8294. if (response.results.length > 0) return printResults(response.results);
  8295. else if (!title.title && !altTitleStrippers.reduce((r, rx) => r || rx[0].test(title.value), false)) return;
  8296. let altSearchTerm = getAltSearchTerm();
  8297. return searchTorrents(altSearchTerm).then(response => { printResults(response.results.filter(function(torrent) {
  8298. let torrentTitle = titleStrippers.reduce((r, substDef) => r.replace(...substDef), torrent.groupName);
  8299. let similarity = jaroWrinkerSimilarity(torrentTitle, altSearchTerm);
  8300. if (prefs.diag_mode) console.debug(`similarity("${torrentTitle}", "${altSearchTerm}") =`, similarity);
  8301. return similarity >= similarityThreshold;
  8302. })) });
  8303. }).catch(reason => { console.error('searchTorrents:', reason) });
  8304. // Find open requests
  8305. function searchRequests(searchTerm) {
  8306. return queryAjaxAPI('requests', {
  8307. search: searchTerm,
  8308. showall: 'on',
  8309. ['filter_cat[' + (category.selectedIndex + 1) + ']']: 1,
  8310. });
  8311. }
  8312. searchRequests(searchTerm).then(function(response) {
  8313. function printResults(results) {
  8314. results.forEach(function(request) {
  8315. if (reportedRequests.has(request.requestId)) return;
  8316. if (request.categoryId != category.selectedIndex + 1) return;
  8317. if (isUpload) reportedRequests.set(request.requestId, addMessage(new HTML('open request ' +
  8318. getRequestRef(request) + ' ' + getRequestInfo(request) + ' possibly fillable by this release'), 'info'));
  8319. else if (isRequestNew) reportedRequests.set(request.requestId,
  8320. addMessage(new HTML('release possibly already requested: ' + getRequestRef(request)), 'info'));
  8321. });
  8322. }
  8323. if (response.results.length > 0) return printResults(response.results);
  8324. else if (!title.title && !altTitleStrippers.reduce((r, rx) => r || rx[0].test(title.value), false)) return;
  8325. let altSearchTerm = getAltSearchTerm();
  8326. return searchRequests(altSearchTerm).then(response => { printResults(response.results.filter(function(request) {
  8327. let requestTitle = titleStrippers.reduce((r, substDef) => r.replace(...substDef), request.title);
  8328. let similarity = jaroWrinkerSimilarity(requestTitle, altSearchTerm);
  8329. if (prefs.diag_mode) console.debug(`similarity("${requestTitle}", "${altSearchTerm}") =`, similarity);
  8330. return similarity >= similarityThreshold;
  8331. })) });
  8332. }).catch(reason => { console.error('searchRequests:', reason) });
  8333. // if (!relationsCheckTimer && prefs.relations_check_interval > 0)
  8334. // relationsCheckTimer = setInterval(lookupNonMusicRelations, prefs.relations_check_interval * 1000);
  8335. }
  8336. } // fillFromText
  8337.  
  8338. function addMessage(text, cls) {
  8339. switch (cls) {
  8340. case 'info': var prefix = 'Info'; break;
  8341. case 'notice': prefix = 'Notice'; break;
  8342. case 'warning': prefix = 'Warning'; break;
  8343. case 'critical': prefix = 'FATAL'; break;
  8344. default: return null;
  8345. }
  8346. if ((messages = document.getElementById('UA-messages')) == null) {
  8347. let ua = document.getElementById('upload assistant');
  8348. if (ua == null) return null;
  8349. let tr = document.createElement('TR');
  8350. tr.id = 'UA-messages';
  8351. ua.firstElementChild.append(tr);
  8352. var td = document.createElement('TD');
  8353. td.colSpan = 2;
  8354. td.className = 'ua-messages-bg';
  8355. tr.append(td);
  8356. } else {
  8357. td = messages.firstElementChild;
  8358. if (td == null) return null;
  8359. }
  8360. var div = document.createElement('DIV');
  8361. div.classList.add('ua-messages', 'ua-' + cls);
  8362. div[text instanceof HTML ? 'innerHTML' : 'textContent'] = prefix + ': ' + text;
  8363. return td.appendChild(div);
  8364. }
  8365.  
  8366. function setHandlers() {
  8367. if ((ref = document.getElementById('image') || document.querySelector('input[name="image"]')) != null)
  8368. setInputHandlers(ref);
  8369. Array.from(document.getElementsByTagName('textarea'))
  8370. .forEach(textArea => { if (textArea.className != 'ua-input') setTextAreahandlers(textArea) });
  8371. if (prefs.cleanup_descriptions) ['form.create_form', 'form.edit_form', 'form#request_form'].forEach(function(sel) {
  8372. if ((ref = document.querySelector(sel)) != null) ref.addEventListener('submit', cleanupDescriptions);
  8373. });
  8374. if ((ref = document.getElementById('yadg_input')) != null) ref.ondrop = clear0;
  8375. // Now rape OPS upload form, but only gently
  8376. if (isOPS && isUpload && (ref = document.getElementById('remaster')) != null) {
  8377. ref.checked = true;
  8378. if (!isAddFormat && prefs.ops_always_edition) {
  8379. elem = ref.parentNode.parentNode;
  8380. elem.style.display = 'none';
  8381. if ((ref = document.querySelector('span#year_label_not_remaster')) != null) ref.textContent = 'Initial year:';
  8382. if ((ref = document.querySelector('tr#edition_year > td.label')) != null) ref.textContent = 'Edition year:';
  8383. if ((ref = document.querySelector('tr#edition_title > td.label')) != null) ref.textContent = 'Edition title:';
  8384. if ((ref = document.getElementById('label_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  8385. if ((ref = document.getElementById('catalogue_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
  8386. document.querySelectorAll('table#edition_information > tbody > tr')
  8387. .forEach(tr => { elem.parentNode.insertBefore(tr, elem) });
  8388. } else Remaster();
  8389. }
  8390. }
  8391.  
  8392. function html2php(node, docUrl) {
  8393. docUrl = urlParser.test(docUrl) ? new URL(docUrl).origin : null;
  8394. const realUrl = a => a.origin == document.location.origin && docUrl ? docUrl + a.pathname + a.search + a.hash : a.href;
  8395. return parseFromNode(node);
  8396.  
  8397. function parseFromNode(node, tagChain = []) {
  8398. if (!(node instanceof Node)) return null;
  8399. switch (node.nodeType) {
  8400. case Node.ELEMENT_NODE: {
  8401. let tags = [], _tags = [], text = [];
  8402. for (let i = 0; i < 5; ++i) text[i] = '';
  8403. switch (node.nodeName) {
  8404. case 'P':
  8405. text[0] = '\n'; text[4] = '\n';
  8406. break;
  8407. case 'DIV':
  8408. text[0] = '\n\n'; text[4] = '\n\n';
  8409. break;
  8410. case 'DT':
  8411. //text[4] = '\n';
  8412. addTag('b'); text[3] = ':';
  8413. break;
  8414. case 'DD':
  8415. //if (isRED) addTag('pad=0|0|0|30'); else text[0] = ' ';
  8416. text[1] = '\t'; text[4] = '\n';
  8417. break;
  8418. case 'LABEL':
  8419. addTag('b');
  8420. text[0] = '\n\n';
  8421. break;
  8422. case 'BR':
  8423. return '\n';
  8424. case 'HR':
  8425. return isRED ? '[hr]' : '\n';
  8426. case 'B': case 'STRONG':
  8427. addTag('b');
  8428. break;
  8429. case 'I': case 'EM': case 'DFN': case 'CITE': case 'VAR':
  8430. addTag('i');
  8431. break;
  8432. case 'U': case 'INS':
  8433. addTag('u');
  8434. break;
  8435. case 'DEL':
  8436. addTag('s');
  8437. break;
  8438. case 'CODE': case 'SAMP': case 'KBD':
  8439. addTag('code');
  8440. text[2] = node.textContent;
  8441. break;
  8442. case 'PRE':
  8443. addTag('pre');
  8444. text[2] = node.textContent;
  8445. break;
  8446. case 'BLOCKQUOTE': case 'QUOTE':
  8447. addTag('quote');
  8448. break;
  8449. case 'Q':
  8450. text[1] = '"'; text[3] = '"';
  8451. break;
  8452. case 'H1':
  8453. addTag('size=5'); addTag('b');
  8454. text[0] = '\n\n'; text[4] = '\n\n';
  8455. break;
  8456. case 'H2':
  8457. addTag('size=4'); addTag('b');
  8458. text[0] = '\n\n'; text[4] = '\n\n';
  8459. break;
  8460. case 'H3':
  8461. addTag('size=3'); addTag('b');
  8462. text[0] = '\n\n'; text[4] = '\n\n';
  8463. break;
  8464. case 'H4': case 'H5': case 'H6':
  8465. addTag('b');
  8466. text[0] = '\n\n'; text[4] = '\n\n';
  8467. break;
  8468. case 'SMALL':
  8469. addTag('size=1');
  8470. break;
  8471. case 'OL': case 'UL':
  8472. _tags.push(node.nodeName.toLowerCase());
  8473. break;
  8474. case 'DL':
  8475. _tags.push(node.nodeName.toLowerCase());
  8476. break;
  8477. case 'LI':
  8478. switch (tagChain.reverse().find(tag => /^[ou]l$/.test(tag))) {
  8479. case 'ol': text[0] = '[#] '; text[4] = '\n'; break;
  8480. case 'ul': text[0] = '[*] '; text[4] = '\n'; break;
  8481. default: return '';
  8482. }
  8483. break;
  8484. case 'TR':
  8485. text[4] = '\n';
  8486. break;
  8487. case 'TD':
  8488. text[1] = '\t';
  8489. break;
  8490. case 'A': {
  8491. if (/^https?:$/i.test(node.protocol)) addTag('url=' + removeRedirect(realUrl(node)));
  8492. break;
  8493. }
  8494. case 'IMG':
  8495. addTag('img');
  8496. text[2] = node.dataset.src || node.src;
  8497. break;
  8498. case 'DETAILS': {
  8499. let summary = node.querySelector('summary');
  8500. summary = summary != null ? '=' + summary.textContent.trim() : '';
  8501. addTag('hide' + summary);
  8502. break;
  8503. }
  8504. case 'AUDIO': case 'BASE': case 'BUTTON': case 'CANVAS': case 'COL': case 'COLGROUP': case 'DATALIST':
  8505. case 'DIALOG': case 'EMBED': case 'FIELDSET': case 'FORM': case 'HEAD': case 'INPUT': case 'LEGEND':
  8506. case 'LINK': case 'MAP': case 'META': case 'METER': case 'NOSCRIPT': case 'OBJECT': case 'OPTGROUP':
  8507. case 'OPTION': case 'PARAM': case 'PROGRESS': case 'SELECT': case 'SOURCE': case 'STYLE': case 'SUMMARY':
  8508. case 'SVG': case 'TEMPLATE': case 'TEXTAREA': case 'TITLE': case 'TRACK': case 'VIDEO':
  8509. return '';
  8510. }
  8511. if (['left', 'center', 'right'].some(al => node.style.textAlign.toLowerCase() == al)) {
  8512. addTag('align=' + node.style.textAlign.toLowerCase());
  8513. }
  8514. if (node.style.fontWeight >= 700) addTag('b');
  8515. switch (node.style.fontStyle.toLowerCase()) {
  8516. case 'italic': addTag('i'); break;
  8517. }
  8518. switch (node.style.textDecorationLine.toLowerCase()) {
  8519. case 'underline': addTag('u'); break;
  8520. case 'line-through': addTag('s'); break;
  8521. }
  8522. if (node.style.color) {
  8523. ctxt.fillStyle = elem.style.color;
  8524. if (ctxt.fillStyle != '#000000' && /^#(?:[a-f0-8]{2}){3,4}$/i.test(ctxt.fillStyle)) {
  8525. addTag('color=' + ctxt.fillStyle);
  8526. }
  8527. }
  8528. if (!text[2]) node.childNodes.forEach(function(node) {
  8529. var childContent = parseFromNode(node, tagChain.concat(tags.concat(_tags).map(tag => tag.replace(/=.*$/, ''))));
  8530. text[2] += childContent;
  8531. });
  8532. if (node.nodeName == 'A' && text[2].trim().length <= 0) {
  8533. if (/^(?:https?):$/i.test(node.protocol)) {
  8534. text[2] = removeRedirect(realUrl(node));
  8535. tags.splice(-1, 1, 'url');
  8536. } else text[2] = node.href.slice(node.protocol.length);
  8537. }
  8538. return text[0] + (text[1] || text[2] || text[3] ? tags.map(tag => '[' + tag + ']').join('').concat(text[1],
  8539. text[2], text[3], tags.reverse().map(tag => '[/' + tag.replace(/=.*$/, '') + ']').join('')) : '') + text[4];
  8540.  
  8541. function addTag(tag) {
  8542. if (tagChain.concat(tags.map(tag => tag.replace(/=.*$/, ''))).includesCaseless(tag.replace(/=.*$/, ''))) return;
  8543. tags.push(tag);
  8544. }
  8545. }
  8546. case Node.TEXT_NODE:
  8547. return node.wholeText.replace(/\s+/g, ' ');
  8548. case Node.DOCUMENT_NODE:
  8549. return parseFromNode(node.body);
  8550. }
  8551. return '';
  8552. }
  8553. }
  8554.  
  8555. function coverPreview(input, imgUrl, size) {
  8556. if (!prefs.auto_preview_cover) return;
  8557. if ((child = document.getElementById('cover-preview')) == null) {
  8558. if (!(input instanceof HTMLElement) || input.parentNode.previousElementSibling == null) return;
  8559. elem = document.createElement('div');
  8560. elem.style = 'padding-top: 10px; float: right; width: 90%;';
  8561. child = document.createElement('img');
  8562. child.id = 'cover-preview';
  8563. elem.append(child);
  8564. var div = document.createElement('div');
  8565. div.id = 'cover-size';
  8566. if (isRequestNew || isRequestEdit) div.style.fontSize = '7.5pt';
  8567. elem.append(div);
  8568. input.parentNode.previousElementSibling.append(document.createElement('br'));
  8569. input.parentNode.previousElementSibling.append(elem);
  8570. }
  8571. if ((div = div || document.getElementById('cover-size')) == null) return;
  8572. if (urlParser.test(imgUrl)) {
  8573. child.onload = function(evt) {
  8574. this.onload = null;
  8575. if (!this.naturalWidth || !this.naturalHeight) return; // invalid image
  8576. (size > 0 ? Promise.resolve(size) : getRemoteFileSize(imgUrl)).then(function(size) {
  8577. var warn = prefs.image_size_warning && size > prefs.image_size_warning * 2**20;
  8578. var html = warn ? '<strong style="color: #ff4c4c;">' + formattedSize(size) + '</strong>' : formattedSize(size);
  8579. div.innerHTML = this.naturalWidth + '×' + this.naturalHeight + ' (' + html + ')';
  8580. if (!warn) return;
  8581. addMessage('high cover size (' + formattedSize(size) + ')', 'notice');
  8582. }.bind(this)).catch(reason => { div.textContent = this.naturalWidth + '×' + this.naturalHeight });
  8583. };
  8584. child.onerror = function(evt) {
  8585. this.onerror = null;
  8586. div.textContent = this.src = '';
  8587. console.warn('Image source cannot be updated:', evt, imgUrl);
  8588. };
  8589. child.src = imgUrl;
  8590. } else div.textContent = child.src = '';
  8591. }
  8592.  
  8593. function cleanupDescriptions(evt) {
  8594. descriptionFields.forEach(function(ID) {
  8595. if ((ref = evt.target.querySelector('textarea#' + ID)) == null || ref.value.length <= 0) return;
  8596. var clean = ref.value
  8597. .replace(/[ \t]*Vinyl rip by \[color=\S+\]\[\/color\]\s*/im, '')
  8598. .replace(/\[u\]Lineage:\[\/u\]\n\n/i, '')
  8599. for (var i = 0; i < 3; ++i) clean = clean.replace(/\s*\[(\w+)(?:=([^\[\]]*))?\]\[\/\1\]/gm, '');
  8600. const drMatch = [
  8601. /(^| \| )DR(\d+)$\s+/m,
  8602. /(?:^| \| )DR(\d+)(?=$| \| )/gm,
  8603. ];
  8604. var m = /\[hide=DR(\d+)?\]\[pre\]/i.exec(clean);
  8605. //if (m != null && drMatch[0].test(clean) && RegExp.$2 == m[1]) clean = clean.replace(drMatch[0], '$1');
  8606. if (m != null && drMatch[1].test(clean) && RegExp.$1 == m[1]) clean = clean.replace(drMatch[1], '');
  8607. ref.value = clean.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/[ \t\xA0]+$/gm, '').trim();
  8608. });
  8609. return true;
  8610. }
  8611.  
  8612. function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*' + expr + '[^\\(\\)]*\\)$', 'i') }
  8613. function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*' + expr + '[^\\[\\]]*\\]$', 'i') }
  8614.  
  8615. function notMonospaced(str) {
  8616. return /[\u0080-\u009F]/.test(str)
  8617. // || /[\u0000-\u001F]/.test(str) // Control character
  8618. // || /[\u0020-\u007F]/.test(str) // Basic Latin
  8619. // || /[\u0080-\u00FF]/.test(str) // Latin-1 Supplement
  8620. // || /[\u0100-\u017F]/.test(str) // Latin Extended-A
  8621. // || /[\u0180-\u024F]/.test(str) // Latin Extended-B
  8622. // || /[\u0250-\u02AF]/.test(str) // IPA Extensions
  8623. || /[\u02B0-\u02FF]/.test(str) // Spacing Modifier Letters
  8624. || /[\u0300-\u036F]/.test(str) // Combining Diacritical Marks
  8625. || /[\u0370-\u03FF]/.test(str) // Greek and Coptic
  8626. || /[\u0400-\u04FF]/.test(str) // Cyrillic
  8627. || /[\u0500-\u052F]/.test(str) // Cyrillic Supplement
  8628. || /[\u0530-\u058F]/.test(str) // Armenian
  8629. || /[\u0590-\u05FF]/.test(str) // Hebrew
  8630. || /[\u0600-\u06FF]/.test(str) // Arabic
  8631. || /[\u0700-\u074F]/.test(str) // Syriac
  8632. || /[\u0750-\u077F]/.test(str) // Arabic Supplement
  8633. || /[\u0780-\u07BF]/.test(str) // Thaana
  8634. || /[\u07C0-\u07FF]/.test(str) // NKo
  8635. || /[\u0800-\u083F]/.test(str) // Samaritan
  8636. || /[\u0840-\u085F]/.test(str) // Mandaic
  8637. || /[\u0860-\u086F]/.test(str) // Syriac Supplement
  8638. || /[\u08A0-\u08FF]/.test(str) // Arabic Extended-A
  8639. || /[\u0900-\u097F]/.test(str) // Devanagari
  8640. || /[\u0980-\u09FF]/.test(str) // Bengali
  8641. || /[\u0A00-\u0A7F]/.test(str) // Gurmukhi
  8642. || /[\u0A80-\u0AFF]/.test(str) // Gujarati
  8643. || /[\u0B00-\u0B7F]/.test(str) // Oriya
  8644. || /[\u0B80-\u0BFF]/.test(str) // Tamil
  8645. || /[\u0C00-\u0C7F]/.test(str) // Telugu
  8646. || /[\u0C80-\u0CFF]/.test(str) // Kannada
  8647. || /[\u0D00-\u0D7F]/.test(str) // Malayalam
  8648. || /[\u0D80-\u0DFF]/.test(str) // Sinhala
  8649. || /[\u0E00-\u0E7F]/.test(str) // Thai
  8650. || /[\u0E80-\u0EFF]/.test(str) // Lao
  8651. || /[\u0F00-\u0FFF]/.test(str) // Tibetan
  8652. || /[\u1000-\u109F]/.test(str) // Myanmar
  8653. || /[\u10A0-\u10FF]/.test(str) // Georgian
  8654. || /[\u1100-\u11FF]/.test(str) // Hangul Jamo
  8655. || /[\u1200-\u137F]/.test(str) // Ethiopic
  8656. || /[\u1380-\u139F]/.test(str) // Ethiopic Supplement
  8657. || /[\u13A0-\u13FF]/.test(str) // Cherokee
  8658. || /[\u1400-\u167F]/.test(str) // Unified Canadian Aboriginal Syllabics
  8659. || /[\u1680-\u169F]/.test(str) // Ogham
  8660. || /[\u16A0-\u16FF]/.test(str) // Runic
  8661. || /[\u1700-\u171F]/.test(str) // Tagalog
  8662. || /[\u1720-\u173F]/.test(str) // Hanunoo
  8663. || /[\u1740-\u175F]/.test(str) // Buhid
  8664. || /[\u1760-\u177F]/.test(str) // Tagbanwa
  8665. || /[\u1780-\u17FF]/.test(str) // Khmer
  8666. || /[\u1800-\u18AF]/.test(str) // Mongolian
  8667. || /[\u18B0-\u18FF]/.test(str) // Unified Canadian Aboriginal Syllabics Extended
  8668. || /[\u1900-\u194F]/.test(str) // Limbu
  8669. || /[\u1950-\u197F]/.test(str) // Tai Le
  8670. || /[\u1980-\u19DF]/.test(str) // New Tai Lue
  8671. || /[\u19E0-\u19FF]/.test(str) // Khmer Symbols
  8672. || /[\u1A00-\u1A1F]/.test(str) // Buginese
  8673. || /[\u1A20-\u1AAF]/.test(str) // Tai Tham
  8674. || /[\u1AB0-\u1AFF]/.test(str) // Combining Diacritical Marks Extended
  8675. || /[\u1B00-\u1B7F]/.test(str) // Balinese
  8676. || /[\u1B80-\u1BBF]/.test(str) // Sundanese
  8677. || /[\u1BC0-\u1BFF]/.test(str) // Batak
  8678. || /[\u1C00-\u1C4F]/.test(str) // Lepcha
  8679. || /[\u1C50-\u1C7F]/.test(str) // Ol Chiki
  8680. || /[\u1C80-\u1C8F]/.test(str) // Cyrillic Extended C
  8681. || /[\u1CC0-\u1CCF]/.test(str) // Sundanese Supplement
  8682. || /[\u1CD0-\u1CFF]/.test(str) // Vedic Extensions
  8683. || /[\u1D00-\u1D7F]/.test(str) // Phonetic Extensions
  8684. || /[\u1D80-\u1DBF]/.test(str) // Phonetic Extensions Supplement
  8685. || /[\u1DC0-\u1DFF]/.test(str) // Combining Diacritical Marks Supplement
  8686. // || /[\u1E00-\u1EFF]/.test(str) // Latin Extended Additional
  8687. || /[\u1F00-\u1FFF]/.test(str) // Greek Extended
  8688. || /[\u200B-\u200F\u2028\u2029\u203B\u202A-\u202E\u2060-\u206F]/.test(str) //|| /[\u2000-\u206F]/.test(str) // General Punctuation
  8689. || /[\u2070-\u209F]/.test(str) // Superscripts and Subscripts
  8690. // || /[\u20A0-\u20CF]/.test(str) // Currency Symbols
  8691. || /[\u20D0-\u20FF]/.test(str) // Combining Diacritical Marks for Symbols
  8692. // || /[\u2100-\u214F]/.test(str) // Letterlike Symbols
  8693. || /[\u2150-\u218F]/.test(str) // Number Forms
  8694. // || /[\u2190-\u21FF]/.test(str) // Arrows
  8695. || /[\u2200-\u22FF]/.test(str) // Mathematical Operators
  8696. || /[\u2300-\u23FF]/.test(str) // Miscellaneous Technical
  8697. || /[\u2400-\u243F]/.test(str) // Control Pictures
  8698. // || /[\u2440-\u245F]/.test(str) // Optical Character Recognition
  8699. || /[\u2460-\u24FF]/.test(str) // Enclosed Alphanumerics
  8700. || /[\u2500-\u257F]/.test(str) // Box Drawing
  8701. // || /[\u2580-\u259F]/.test(str) // Block Elements
  8702. || /[\u25A0-\u25FF]/.test(str) // Geometric Shapes
  8703. || /[\u2600-\u26FF]/.test(str) // Miscellaneous Symbols
  8704. || /[\u2700-\u27BF]/.test(str) // Dingbats
  8705. || /[\u27C0-\u27EF]/.test(str) // Miscellaneous Mathematical Symbols-A
  8706. || /[\u27F0-\u27FF]/.test(str) // Supplemental Arrows-A
  8707. || /[\u2800-\u28FF]/.test(str) // Braille Patterns
  8708. || /[\u2900-\u297F]/.test(str) // Supplemental Arrows-B
  8709. // || /[\u2980-\u29FF]/.test(str) // Miscellaneous Mathematical Symbols-B
  8710. // || /[\u2A00-\u2AFF]/.test(str) // Supplemental Mathematical Operators
  8711. || /[\u2B00-\u2BFF]/.test(str) // Miscellaneous Symbols and Arrows
  8712. || /[\u2C00-\u2C5F]/.test(str) // Glagolitic
  8713. // || /[\u2C60-\u2C7F]/.test(str) // Latin Extended-C
  8714. || /[\u2C80-\u2CFF]/.test(str) // Coptic
  8715. || /[\u2D00-\u2D2F]/.test(str) // Georgian Supplement
  8716. || /[\u2D30-\u2D7F]/.test(str) // Tifinagh
  8717. || /[\u2D80-\u2DDF]/.test(str) // Ethiopic Extended
  8718. || /[\u2DE0-\u2DFF]/.test(str) // Cyrillic Extended-A
  8719. || /[\u2E00-\u2E7F]/.test(str) // Supplemental Punctuation
  8720. || /[\u2E80-\u2EFF]/.test(str) // CJK Radicals Supplement
  8721. || /[\u2F00-\u2FDF]/.test(str) // Kangxi Radicals
  8722. || /[\u2FF0-\u2FFF]/.test(str) // Ideographic Description Characters
  8723. || /[\u3000-\u303F]/.test(str) // CJK Symbols and Punctuation
  8724. || /[\u3040-\u309F]/.test(str) // Hiragana
  8725. || /[\u30A0-\u30FF]/.test(str) // Katakana
  8726. || /[\u3100-\u312F]/.test(str) // Bopomofo
  8727. || /[\u3130-\u318F]/.test(str) // Hangul Compatibility Jamo
  8728. || /[\u3190-\u319F]/.test(str) // Kanbun
  8729. || /[\u31A0-\u31BF]/.test(str) // Bopomofo Extended
  8730. || /[\u31C0-\u31EF]/.test(str) // CJK Strokes
  8731. || /[\u31F0-\u31FF]/.test(str) // Katakana Phonetic Extensions
  8732. || /[\u3200-\u32FF]/.test(str) // Enclosed CJK Letters and Months
  8733. || /[\u3300-\u33FF]/.test(str) // CJK Compatibility
  8734. || /[\u3400-\u4DBF]/.test(str) // CJK Unified Ideographs Extension A
  8735. || /[\u4DC0-\u4DFF]/.test(str) // Yijing Hexagram Symbols
  8736. || /[\u4E00-\u9FFF]/.test(str) // CJK Unified Ideographs
  8737. // || /[\uA000-\uA48F]/.test(str) // Yi Syllables
  8738. // || /[\uA490-\uA4CF]/.test(str) // Yi Radicals
  8739. || /[\uA4D0-\uA4FF]/.test(str) // Lisu
  8740. || /[\uA500-\uA63F]/.test(str) // Vai
  8741. || /[\uA640-\uA69F]/.test(str) // Cyrillic Extended-B
  8742. || /[\uA6A0-\uA6FF]/.test(str) // Bamum
  8743. || /[\uA700-\uA71F]/.test(str) // Modifier Tone Letters
  8744. || /[\uA720-\uA7FF]/.test(str) // Latin Extended-D
  8745. || /[\uA800-\uA82F]/.test(str) // Syloti Nagri
  8746. || /[\uA830-\uA83F]/.test(str) // Common Indic Number Forms
  8747. || /[\uA840-\uA87F]/.test(str) // Phags-pa
  8748. || /[\uA880-\uA8DF]/.test(str) // Saurashtra
  8749. || /[\uA8E0-\uA8FF]/.test(str) // Devanagari Extended
  8750. || /[\uA900-\uA92F]/.test(str) // Kayah Li
  8751. || /[\uA930-\uA95F]/.test(str) // Rejang
  8752. || /[\uA960-\uA97F]/.test(str) // Hangul Jamo Extended-A
  8753. || /[\uA980-\uA9DF]/.test(str) // Javanese
  8754. || /[\uA9E0-\uA9FF]/.test(str) // Myanmar Extended-B
  8755. || /[\uAA00-\uAA5F]/.test(str) // Cham
  8756. || /[\uAA60-\uAA7F]/.test(str) // Myanmar Extended-A
  8757. || /[\uAA80-\uAADF]/.test(str) // Tai Viet
  8758. || /[\uAAE0-\uAAFF]/.test(str) // Meetei Mayek Extensions
  8759. || /[\uAB00-\uAB2F]/.test(str) // Ethiopic Extended-A
  8760. // || /[\uAB30-\uAB6F]/.test(str) // Latin Extended-E
  8761. || /[\uAB70-\uABBF]/.test(str) // Cherokee Supplement
  8762. || /[\uABC0-\uABFF]/.test(str) // Meetei Mayek
  8763. || /[\uAC00-\uD7AF]/.test(str) // Hangul Syllables
  8764. || /[\uD7B0-\uD7FF]/.test(str) // Hangul Jamo Extended-B
  8765. || /[\uD800-\uDB7F]/.test(str) // High Surrogates
  8766. // || /[\uDB80-\uDBFF]/.test(str) // High Private Use Surrogates
  8767. || /[\uDC00-\uDFFF]/.test(str) // Low Surrogates
  8768. || /[\uE000-\uF8FF]/.test(str) // Private Use Area
  8769. || /[\uF900-\uFAFF]/.test(str) // CJK Compatibility Ideographs
  8770. || /[\uFB00-\uFB4F]/.test(str) // Alphabetic Presentation Forms
  8771. || /[\uFB50-\uFDFF]/.test(str) // Arabic Presentation Forms-A
  8772. || /[\uFE00-\uFE0F]/.test(str) // Variation Selectors
  8773. || /[\uFE10-\uFE1F]/.test(str) // Vertical Forms
  8774. || /[\uFE20-\uFE2F]/.test(str) // Combining Half Marks
  8775. || /[\uFE30-\uFE4F]/.test(str) // CJK Compatibility Forms
  8776. || /[\uFE50-\uFE6F]/.test(str) // Small Form Variants
  8777. || /[\uFE70-\uFEFF]/.test(str) // Arabic Presentation Forms-B
  8778. || /[\uFF00-\uFFEF]/.test(str) // Halfwidth and Fullwidth Forms
  8779. || /[\uFFF0-\uFFFF]/.test(str) // Specials
  8780. // || /[\u10000-\uFFFFF]/.test(str) // Others
  8781. }
  8782.  
  8783. function getSizeFromString(str, returnAs = undefined) {
  8784. if (typeof str != 'string') return 0;
  8785. let matches = /\b(\d+(?:\.\d+)?)\s*([KMGTPEZY]?)I?B\b/.exec(str.replace(',', '.').toUpperCase());
  8786. if (matches == null) return 0;
  8787. const prefixes = Array.from('KMGTPEZY');
  8788. let size = parseFloat(matches[1]);
  8789. let fromIndex = prefixes.indexOf(matches[2]);
  8790. let toIndex = /^([KMGTPEZY]?)(?:i?B)?$/i.test(returnAs) ? prefixes.indexOf(RegExp.$1.toUpperCase()) : 1;
  8791. let result = size * Math.pow(2, (fromIndex - toIndex) * 10);
  8792. return toIndex >= 0 ? result : Math.round(result);
  8793. }
  8794.  
  8795. function makeTimeString(duration) {
  8796. let t = Math.abs(Math.round(duration));
  8797. let H = Math.floor(t / 60 ** 2);
  8798. let M = Math.floor(t / 60 % 60);
  8799. let S = t % 60;
  8800. return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
  8801. ':' + S.toString().padStart(2, '0');
  8802. }
  8803.  
  8804. function timeStringToTime(str) {
  8805. if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
  8806. var t = 0, a = RegExp.$2.split(':');
  8807. while (a.length > 0) t = t * 60 + parseFloat(a.shift());
  8808. return RegExp.$1 ? -t : t;
  8809. }
  8810.  
  8811. function normalizeDate(str, countryCode = undefined) {
  8812. if (typeof str != 'string') return null;
  8813. var match;
  8814. function formatOutput(yearIndex, montHindex, dayIndex) {
  8815. var year = parseInt(match[yearIndex]), month = parseInt(match[montHindex]), day = parseInt(match[dayIndex]);
  8816. if (year < 30) year += 2000; else if (year < 100) year += 1900;
  8817. if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 0 || day > 31) return null;
  8818. return year.toString() + '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0');
  8819. }
  8820. if ((match = /\b(\d{4})-(\d{1,2})-(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // US
  8821. if ((match = /\b(\d{4})\/(\d{1,2})\/(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3);
  8822. if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null
  8823. && (parseInt(match[1]) > 12 || /\b(?:be|it)/.test(countryCode))) return formatOutput(3, 2, 1); // BE, IT
  8824. if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null) return formatOutput(3, 1, 2); // US
  8825. if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // UK, IE, FR, ES
  8826. if ((match = /\b(\d{1,2})-(\d{1,2})-((?:\d{2}|\d{4}))\b/.exec(str)) != null) return formatOutput(3, 2, 1); // NL
  8827. if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // CZ, DE
  8828. if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{2})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // AT, CH, DE, LU
  8829. if ((match = /\b(\d{4})\. *(\d{1,2})\. *(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // JP
  8830. return extractYear(str);
  8831. }
  8832.  
  8833. function extractYear(expr) {
  8834. if (typeof expr == 'number') return Math.round(expr);
  8835. if (typeof expr != 'string') return null;
  8836. if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
  8837. var d = new Date(expr);
  8838. return parseInt(isNaN(d) ? expr : d.getFullYear());
  8839. }
  8840.  
  8841. function safeText(unsafeText) {
  8842. let div = document.createElement('div');
  8843. div.innerText = unsafeText || '';
  8844. return div.innerHTML;
  8845. }
  8846.  
  8847. function decodeHTML(html) {
  8848. var elem = document.createElement("textarea");
  8849. elem.innerHTML = html;
  8850. return elem.value;
  8851. }
  8852.  
  8853. function convertToRoman(num) {
  8854. var roman = {
  8855. M: 1000,
  8856. CM: 900,
  8857. D: 500,
  8858. CD: 400,
  8859. C: 100,
  8860. XC: 90,
  8861. L: 50,
  8862. XL: 40,
  8863. X: 10,
  8864. IX: 9,
  8865. V: 5,
  8866. IV: 4,
  8867. I: 1,
  8868. };
  8869. var str = '';
  8870. for (var i of Object.keys(roman)) {
  8871. var q = Math.floor(num / roman[i]);
  8872. num -= q * roman[i];
  8873. str += i.repeat(q);
  8874. }
  8875. return str;
  8876. }
  8877.  
  8878. function inputDataHandler(evt, data) {
  8879. if (!data) return true;
  8880. if (data.files.length > 0) {
  8881. if (!data.files[0].type || !data.files[0].type.toLowerCase().startsWith('image/')) return true;
  8882. evt.target.disabled = true;
  8883. if (evt.target.hTimer) {
  8884. clearTimeout(evt.target.hTimer);
  8885. delete evt.target.hTimer;
  8886. }
  8887. evt.target.style.color = 'white';
  8888. evt.target.style.backgroundColor = 'darkred';
  8889. let size = data.files[0].size, lastPct, lastUpdate, current;
  8890. imageHosts.uploadImages([data.files[0]], function(worker, param = null) {
  8891. if (param && typeof param == 'object') {
  8892. if (param.readyState > 1 || current != undefined && worker !== current) return;
  8893. if (Date.now() - 100 < lastUpdate) return;
  8894. let pct = Math.floor(Math.min(param.done * 100 / param.total, 100));
  8895. if (pct <= lastPct) return;
  8896. lastPct = pct;
  8897. evt.target.value = 'Uploading... [' + pct + '%]';
  8898. lastUpdate = Date.now();
  8899. } else if (typeof param != 'number') {
  8900. lastPct = lastUpdate = undefined;
  8901. current = worker;
  8902. evt.target.value = 'Uploading...';
  8903. }
  8904. }).then(singleImageGetter).then(function(imgUrl) {
  8905. if (imgUrl == null) return;
  8906. evt.target.value = imgUrl;
  8907. evt.target.style.backgroundColor = '#008000';
  8908. evt.target.hTimer = setTimeout(function() {
  8909. evt.target.style.backgroundColor = null;
  8910. evt.target.style.color = null;
  8911. delete evt.target.hTimer;
  8912. }, 10000);
  8913. coverPreview(evt.target, imgUrl, size);
  8914. // checkImageSize(imgUrl, evt.target, size).then(function(imageUrl) {
  8915. // evt.target.disabled = true;
  8916. // imageHosts.rehostImages([imageUrl]).then(singleImageGetter)
  8917. // .then(imgUrl => { if (imgUrl != null) evt.target.value = imgUrl },
  8918. // reason => { Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') }) })
  8919. // .then(() => { evt.target.disabled = false });
  8920. // });
  8921. }, function(reason) {
  8922. inputClear(evt);
  8923. evt.target.style.backgroundColor = null;
  8924. evt.target.style.color = null;
  8925. Promise.resolve(reason).then(msg => { alert(msg) });
  8926. }).then(() => { evt.target.disabled = false });
  8927. return false;
  8928. } else if (data.items.length > 0) {
  8929. let links = data.getData('text/uri-list');
  8930. if (links) links = links.split(/\r?\n/); else {
  8931. links = data.getData('text/x-moz-url');
  8932. if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  8933. else if (links = data.getData('text/plain')) links = links.split(/\r?\n/);
  8934. }
  8935. if (Array.isArray(links) && links.length > 0) imageUrlResolver(links[0]).then(verifyImageUrl).then(function(imageUrl) {
  8936. evt.target.value = imageUrl;
  8937. coverPreview(evt.target, imageUrl);
  8938. checkImageSize(imageUrl, evt.target).then(function(imageUrl) {
  8939. if (!prefs.auto_rehost_cover && !isNWCD) return;
  8940. evt.target.disabled = true;
  8941. imageHosts.rehostImages([imageUrl]).then(singleImageGetter)
  8942. .then(imgUrl => { if (imgUrl != null) evt.target.value = imgUrl },
  8943. reason => { Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') }) })
  8944. .then(() => { evt.target.disabled = false });
  8945. });
  8946. }).catch(function(e) {
  8947. console.error(e);
  8948. alert(e);
  8949. });
  8950. return false;
  8951. }
  8952. return true;
  8953. }
  8954.  
  8955. function textAreaDropHandler(evt) {
  8956. if (evt.dataTransfer == null || evt.shiftKey) return true;
  8957. if (evt.dataTransfer.files.length > 0) {
  8958. let images = [];
  8959. Array.from(evt.dataTransfer.files).forEach(function(file) {
  8960. switch (file.type) {
  8961. case '':
  8962. if (!['log'/*, 'nfo'*/].some(ext => file.name.toLowerCase().endsWith('.' + ext))) break;
  8963. case 'text/plain':
  8964. //case 'text/nfo': // malformed encoding
  8965. case 'text/log':
  8966. evt.target.disabled = true;
  8967. file.getText(file.name.toLowerCase().endsWith('.nfo') ? 'ibm850' : 'utf-8').then(function(text) {
  8968. var isDR = file.name.toLowerCase().endsWith('foo_dr.txt') && /^Official DR value:\s*DR(\d+)\b/im.test(text);
  8969. if (isDR) var DR = parseInt(RegExp.$1);
  8970. var tag = isDR || file.name.toLowerCase().endsWith('.nfo') ? 'pre' : 'code';
  8971. var php = isDR ? '[hide=DR' + RegExp.$1 + '][' + tag + ']' + text + '[/' + tag + '][/hide]'
  8972. : '[hide=' + file.name + '][' + tag + ']' + text + '[/' + tag + '][/hide]';
  8973. if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
  8974. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) +
  8975. php + evt.target.value.slice(evt.rangeOffset);
  8976. } else if (isDR && /\[hide=DR\d*\]\[pre\]\[\/pre\]/i.test(evt.target.value)) {
  8977. evt.target.value = RegExp.leftContext + php.slice(0, -7) + RegExp.rightContext;
  8978. } else if (isDR && /\[hide=DR(\d*)\]((?:\[pre\](foobar2000[\s\S]+?)^\[\/pre\]\s*)+)(?:\[pre\]\[\/pre\])?/im.test(evt.target.value)) {
  8979. php = '[hide=DR';
  8980. if (parseInt(RegExp.$1) == DR) php += RegExp.$1;
  8981. evt.target.value = `#{RegExp.leftContext}${php}]${RegExp.$2.trim()}\n[pre]${text}[/pre]${RegExp.rightContext}`;
  8982. } else if (!isDR && /\[hide\](?:\[code\]\[\/code\])?\[\/hide\]/i.test(evt.target.value)) {
  8983. evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
  8984. } else if (!isDR && /(\[hide=[^\]]+\])(?:\[code\]\[\/code\])?(\[\/hide\])/i.test(evt.target.value)) {
  8985. evt.target.value = `${RegExp.leftContext}${RegExp.$1}[code]${text}[/code]${RegExp.$2}${RegExp.rightContext}`;
  8986. } else evt.target.value += '\n\n' + php;
  8987. }).catch(function(e) { alert(e) }).then(function() {
  8988. if (!evt.target.style.background) evt.target.disabled = false;
  8989. });
  8990. break;
  8991. default:
  8992. if (file.type && file.type.startsWith('image/')) images.push(file);
  8993. }
  8994. });
  8995. if (images.length > 0) {
  8996. evt.target.disabled = true;
  8997. if (!isNWCD) var progressBar = new ULProgressBar(evt.target, images.map(image => image.size));
  8998. imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar))
  8999. .then(urlHandler.bind({ tag: 'img' }), reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
  9000. .then(function() {
  9001. ULProgressBar.prototype.cleanUp.call(progressBar);
  9002. evt.target.disabled = false;
  9003. });
  9004. }
  9005. evt.stopPropagation();
  9006. return false;
  9007. } else if (evt.dataTransfer.items.length > 0) {
  9008. let content = evt.dataTransfer.getData('text/uri-list');
  9009. if (content) content = content.split(/\r?\n/); else {
  9010. content = evt.dataTransfer.getData('text/x-moz-url');
  9011. if (content) content = content.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  9012. };
  9013. if (Array.isArray(content) && content.length > 0) {
  9014. Promise.all(content.map(imageUrlResolver)).then(function(resolved) {
  9015. var resolvedUrls = resolved.flatten();
  9016. if (prefs.auto_rehost_cover || isNWCD) {
  9017. evt.target.disabled = true;
  9018. if (resolvedUrls.length > 1 && !isNWCD) {
  9019. progressBar = new RHProgressBar(evt.target);
  9020. progressBar.update(0, false);
  9021. }
  9022. imageHosts.rehostImages(resolvedUrls, progressBar ? (param = true) => progressBar.update(0, param) : null)
  9023. .catch(function(reason) {
  9024. addMessage(`${reason} (not rehosted)`, 'warning');
  9025. RHProgressBar.prototype.update.call(progressBar, -1, false);
  9026. return verifyImageUrls(resolvedUrls);
  9027. }).then(function(results) {
  9028. urlHandler.bind({ tag: 'img' })(results, arrayGrouping(resolved).flatten());
  9029. RHProgressBar.prototype.cleanUp.call(progressBar);
  9030. evt.target.disabled = false;
  9031. });
  9032. } else urlHandler.bind({ tag: 'img' })(resolvedUrls. arrayGrouping(resolved).flatten());
  9033. }).catch(function(e) {
  9034. let as = domParser.parseFromString(evt.dataTransfer.getData('text/html'), 'text/html').body.querySelectorAll('a');
  9035. Promise.all(content.map(urlResolver))
  9036. .then(resolved => urlHandler.bind({ tag: 'url', titles: Array.from(as).map(a => a.textContent.trim()) })(resolved.flatten()));
  9037. });
  9038. } else if (content = evt.dataTransfer.getData('text/html')) {
  9039. insert(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
  9040. } else if (content = evt.dataTransfer.getData('text/plain')) {
  9041. insert(content);
  9042. }
  9043. evt.stopPropagation();
  9044. return false;
  9045. }
  9046. return true;
  9047.  
  9048. function urlHandler(results, groups = undefined) {
  9049. if (typeof this.tag != 'string' || this.tag.length <= 0) throw 'Invalid argument';
  9050. const tagName = this.tag.toLowerCase(), rx = new RegExp('\\[' + tagName + '\\]\\[\\/' + tagName + '\\]', 'i');
  9051. var phpBB = '';
  9052. results.forEach((result, index) => {
  9053. if (tagName == 'img') {
  9054. var thumb = evt.altKey && !evt.target.noPhpBB && typeof result == 'object'
  9055. && urlParser.test(result.original) && urlParser.test(result.thumb);
  9056. if (typeof result == 'object' && result.original) var url = result.original;
  9057. else if (typeof result == 'string') url = result;
  9058. else throw 'Invalid result format';
  9059. } else if (result.length > 0 && urlParser.test(result)) url = result; else return;
  9060. if (thumb) var _phpBB = '[url=' + url + '][' + tagName + ']' + result.thumb + '[/' + tagName + '][/url]'; else {
  9061. _phpBB = '[' + tagName;
  9062. _phpBB += Array.isArray(this.titles) && this.titles[index] ? '=' + url + ']' + this.titles[index] : ']' + url;
  9063. _phpBB += '[/' + tagName + ']';
  9064. }
  9065. if (rx.test(evt.target.value)) evt.target.value = RegExp.leftContext + _phpBB + RegExp.rightContext; else {
  9066. if (index > 0) phpBB += isGroupBoundary(groups, index) ? thumb ? '\n' : '\n\n' : thumb ? ' ' : '\n';
  9067. phpBB += evt.target.noPhpBB ? url : _phpBB;
  9068. }
  9069. });
  9070. insert(phpBB);
  9071. }
  9072.  
  9073. function insert(phpBB) {
  9074. if (typeof phpBB != 'string' || phpBB.length <= 0) return;
  9075. if (evt.target.value.trimRight().length <= 0) evt.target.value = phpBB; else if (evt.ctrlKey) {
  9076. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + phpBB + evt.target.value.slice(evt.rangeOffset);
  9077. } else evt.target.value = evt.target.value.trimRight() + /*ndx <= 0 ? '\n\n' : */'\n\n' + phpBB;
  9078. }
  9079. }
  9080.  
  9081. function textAreaPasteHandler(evt) {
  9082. if (evt.clipboardData == null) return true;
  9083. if (evt.clipboardData.files.length > 0) {
  9084. let images = Array.from(evt.clipboardData.files).filter(file => file.type && file.type.startsWith('image/'));
  9085. if (images.length <= 0) return true;
  9086. evt.target.disabled = true;
  9087. if (!isNWCD) var progressBar = new ULProgressBar(evt.target, images.map(image => image.size));
  9088. imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(function(results) {
  9089. var phpBB = '';
  9090. results.forEach(function(result, index) {
  9091. var thumb = evt.altKey && !evt.target.noPhpBB && typeof result == 'object'
  9092. && urlParser.test(result.original) && urlParser.test(result.thumb);
  9093. if (typeof result == 'object' && result.original) var imgUrl = result.original;
  9094. else if (typeof result == 'string') imgUrl = result;
  9095. else throw 'Invalid result format';
  9096. if (index > 0) phpBB += thumb ? ' ' : '\n';
  9097. phpBB += evt.target.noPhpBB ? phpBB += imgUrl : !thumb ? '[img]' + imgUrl+ '[/img]'
  9098. : '[url=' + imgUrl + '][img]' + result.thumb + '[/img][/url]';
  9099. });
  9100. insert(phpBB);
  9101. }, reason => { Promise.resolve(reason).then(msg => { alert(msg) }) }).then(function() {
  9102. ULProgressBar.prototype.cleanUp.call(progressBar);
  9103. evt.target.disabled = false;
  9104. });
  9105. evt.stopPropagation();
  9106. return false;
  9107. } else if (evt.clipboardData.items.length > 0) {
  9108. let content = evt.clipboardData.getData('text/html');
  9109. if (!content) return true;
  9110. insert(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
  9111. return false;
  9112. }
  9113. return true;
  9114.  
  9115. function insert(phpBB) {
  9116. if (typeof phpBB != 'string' || phpBB.length <= 0) return;
  9117. var selStart = evt.target.selectionStart;
  9118. evt.target.value = evt.target.value.slice(0, selStart) + phpBB + evt.target.value.slice(evt.target.selectionEnd);
  9119. evt.target.setSelectionRange(selStart + phpBB.length, selStart + phpBB.length);
  9120. }
  9121. }
  9122.  
  9123. function arrayGrouping(arr) {
  9124. return Array.isArray(arr) ? arr.map(function(elem) {
  9125. if (!Array.isArray(elem)) return 1;
  9126. return elem.every(elem => !Array.isArray(elem)) ? elem.length : arrayGrouping(elem);
  9127. }) : null;
  9128. }
  9129. function isGroupBoundary(groups, index) {
  9130. return index > 0 && Array.isArray(groups)
  9131. && groups.some((len, ndx, arr) => index == arr.slice(0, ndx).reduce((acc, len) => acc + len, 0));
  9132. }
  9133.  
  9134. function uaInsert(evt) {
  9135. if ((!evt.clipboardData || evt.clipboardData.items.length <= 0)
  9136. && (!evt.dataTransfer || evt.dataTransfer.items.length <= 0)) return true;
  9137. evt.target.value = '';
  9138. if (prefs.autfill_delay > 0) {
  9139. if (autoFill) clearTimeout(autoFill);
  9140. autoFill = setTimeout(fillFromText, prefs.autfill_delay);
  9141. }
  9142. }
  9143.  
  9144. // Firefox accepts dropped playlist in malformed form, try to detect and correct it
  9145. function fixFirefoxDropBug(evt) {
  9146. if (evt.target == null || evt.target.value.length <= 0) return true;
  9147. var tl = (Math.sqrt(4 * evt.target.value.split('\n').length - 3) + 1) / 2;
  9148. if (tl < 2 || tl != Math.floor(tl) || evt.target.value.length % tl != 0) return true;
  9149. var l = evt.target.value.length / tl;
  9150. var s = evt.target.value.slice(0, l);
  9151. for (var i = 1; i < tl; ++i) if (evt.target.value.slice(i * l, (i + 1) * l) != s) return true;
  9152. evt.target.value = s;
  9153. return true;
  9154. }
  9155.  
  9156. function clear0(evt) { if (evt.target.value.length > 0) evt.target.value = '' }
  9157. function clear1(evt) { if (evt.buttons == 4) clear0(evt) }
  9158. function voidDragHandler1(evt) {
  9159. return !evt.dataTransfer.types.includes('Files') || evt.target.nodeName == 'TEXTAREA'
  9160. || evt.target.nodeName == 'INPUT' && evt.target.type == 'file'
  9161. }
  9162.  
  9163. function removeRedirect(uri) {
  9164. return typeof uri != 'string' ? null : [
  9165. 'www.anonymz.com/?', 'www.anonymz.com?',
  9166. 'anonymz.com/?', 'anonymz.com?',
  9167. 'anonym.to/?', 'anonym.to?',
  9168. 'dereferer.me/?',
  9169. 'reho.st/',
  9170. ].reduce(function(acc, it) {
  9171. if (acc.toLowerCase().startsWith('https://' + it)) return acc.slice(it.length + 8);
  9172. if (acc.toLowerCase().startsWith('http://' + it)) return acc.slice(it.length + 7);
  9173. return acc;
  9174. }, uri);
  9175. }
  9176.  
  9177. function imageUrlResolver(url) {
  9178. return urlResolver(url).then(url => verifyImageUrl(url).catch(function(reason) {
  9179. const notFound = Promise.reject('No title image for this URL');
  9180. function getFromMeta(root) {
  9181. var meta = root instanceof Document || root instanceof Element ? [
  9182. 'meta[property="og:image"][content]',
  9183. 'meta[name="og:image"][content]',
  9184. 'meta[itemprop="og:image"][content]',
  9185. 'meta[itemprop="image"][content]',
  9186. ].reduce((elem, selector) => elem || root.querySelector(selector), null) : null;
  9187. return meta != null && urlParser.test(meta.content) ? meta.content : undefined;
  9188. }
  9189.  
  9190. try { url = new URL(url) } catch(e) { return Promise.reject(e) }
  9191. if (url.hostname.endsWith('pinterest.com'))
  9192. return pinterestResolver(url);
  9193. else if (url.hostname.endsWith('free-picload.com')) {
  9194. if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('free-picload.com', url);
  9195. } else if (url.hostname.endsWith('bandcamp.com')) return globalFetch(url).then(function(response) {
  9196. var ref = response.document.querySelector('div#tralbumArt > a.popupImage');
  9197. ref = ref != null ? ref.href : getFromMeta(response.document);
  9198. return ref ? Promise.resolve(ref.replace(/_\d+(?=\.\w+$)/, '_0')) : notFound;
  9199. }); else if (url.hostname.endsWith('7digital.com') && url.pathname.startsWith('/artist/'))
  9200. return globalFetch(url).then(function(response) {
  9201. var img = response.document.querySelector('img[itemprop="image"]');
  9202. return img != null ? img.src : notFound;
  9203. });
  9204. else if (url.hostname.endsWith('geekpic.net')) return globalFetch(url).then(function(response) {
  9205. var a = response.document.querySelector('div.img-upload > a.mb');
  9206. return a != null ? a.href : notFound;
  9207. }); else if (url.hostname.endsWith('qq.com') && url.pathname.includes('/album/')) return globalFetch(url).then(function(response) {
  9208. var img = response.document.querySelector('img#albumImg');
  9209. return img != null ? img.src.replace(/(?:_\d+)?(\.\w+)(?:\?.*)?$/, '$1').replace(/R\d+x\d+/, '') : notFound;
  9210. }); else switch (url.hostname) {
  9211. // general image hostings
  9212. case 'www.imgur.com': case 'imgur.com':
  9213. if (url.pathname.startsWith('/a/')) return globalFetch(url, { responseType: 'text' }).then(function(response) {
  9214. if (/^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.test(response.responseText)) try {
  9215. return JSON.parse(RegExp.$1).album_images.images.map(image => 'https://i.imgur.com/' + image.hash + image.ext);
  9216. } catch(e) { debug.warn(e) }
  9217. return notFound;
  9218. });
  9219. return globalFetch(url).then(response => response.document.querySelector('link[rel="image_src"]').href);
  9220. case 'pixhost.to':
  9221. if (url.pathname.startsWith('/gallery/')) return globalFetch(url).then(response =>
  9222. Promise.all(Array.from(response.document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href))));
  9223. if (url.pathname.startsWith('/show/')) return globalFetch(url)
  9224. .then(response => response.document.querySelector('img#image').src);
  9225. break;
  9226. case 'malzo.com':
  9227. if (url.pathname.startsWith('/al/')) return cheveretoGalleryResolver('malzo.com', url);
  9228. break;
  9229. case 'imgbb.com': case 'ibb.co':
  9230. if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('imgbb.com', url);
  9231. break;
  9232. case 'jerking.empornium.ph':
  9233. if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('jerking.empornium.ph', url);
  9234. break;
  9235. case 'imgbox.com':
  9236. if (url.pathname.startsWith('/g/')) return globalFetch(url).then(response =>
  9237. Promise.all(Array.from(response.document.querySelectorAll('div#gallery-view-content > a'))
  9238. .map(a => imageUrlResolver('https://imgbox.com' + a.pathname))));
  9239. break;
  9240. case 'postimage.org': case 'postimg.cc':
  9241. if (!url.pathname.startsWith('/gallery/')) break;
  9242. return PostImage.galleryResolver(url);
  9243. case 'www.imagevenue.com': case 'imagevenue.com':
  9244. return globalFetch(url, { headers: { Referer: 'http://www.imagevenue.com/' } }).then(function(response) {
  9245. var images = Array.from(response.document.querySelectorAll('div.card img')).map(function(img) {
  9246. return img.src.includes('://cdn-images') ? Promise.resolve(img.src) : imageUrlResolver(img.parentNode.href);
  9247. });
  9248. return images.length > 1 ? Promise.all(images) : images.length == 1 ? images[0] : notFound;
  9249. });
  9250. case 'www.imageshack.us': case 'imageshack.us':
  9251. return globalFetch(url).then(response => response.document.querySelector('a#share-dl').href);
  9252. case 'www.flickr.com': case 'flickr.com':
  9253. if (!url.pathname.startsWith('/photos/')) break;
  9254. return globalFetch(url).then(function(response) {
  9255. if (/\b(?:modelExport)\s*:\s*(\{.+\}),/.test(response.responseText)) try {
  9256. var urls = JSON.parse(RegExp.$1).main['photo-models'].map(function(photoModel) {
  9257. var sizes = Object.keys(photoModel.sizes).sort((a, b) => photoModel.sizes[b].width * photoModel.sizes[b].height
  9258. - photoModel.sizes[a].width * photoModel.sizes[a].height);
  9259. return sizes.length > 0 ? 'https:'.concat(photoModel.sizes[sizes[0]].url) : null;
  9260. });
  9261. if (urls.length == 1) return urls[0]; else if (urls.length > 1) return urls;
  9262. } catch(e) { console.warn(e) }
  9263. return notFound;
  9264. });
  9265. case 'photos.google.com':
  9266. return googlePhotosResolver(url);
  9267. case 'www.500px.com': case 'web.500px.com': case '500px.com':
  9268. if (/^\/photo\/(\d+)\b/i.test(url.pathname))
  9269. return _500pxUrlHandler('photos?ids='.concat(RegExp.$1));
  9270. else if (/\/galleries\/([\w\-]+)/i.test(url.pathname)) {
  9271. let galleryId = RegExp.$1;
  9272. return globalFetch(url, { rsponseType: 'text' }).then(function(response) {
  9273. if (!/\b(?:App\.CuratorId)\s*=\s*"(\d+)"/.test(response.responseText)) return Promise.reject('Unexpected page structure');
  9274. return _500pxUrlHandler('users/' + RegExp.$1 + '/galleries/' + galleryId + '/items?sort=position&sort_direction=asc&rpp=999');
  9275. });
  9276. }
  9277. break;
  9278. case 'www.pxhere.com': case 'pxhere.com':
  9279. if (url.pathname.includes('/photo/')) return globalFetch(url).then(response =>
  9280. JSON.parse(response.document.querySelector('div.hub-media-content > script[type="application/ld+json"]').text).contentUrl);
  9281. else if (url.pathname.includes('/collection/')) return pxhereCollectionResolver(url);
  9282. break;
  9283. case 'www.unsplash.com': case 'unsplash.com':
  9284. if (url.pathname.startsWith('/photos/')) return globalFetch(url.origin + url.pathname + '/download', { method: 'HEAD' })
  9285. .then(response => response.finalUrl.replace(/\?.*$/, ''));
  9286. else if (url.pathname.includes('/collections/')) return unsplashCollectionResolver(url);
  9287. break;
  9288. case 'www.pexels.com': case 'pexels.com':
  9289. if (url.pathname.startsWith('/photo/')) return globalFetch(url)
  9290. .then(response => response.document.querySelector('meta[property="og:image"][content]').content.replace(/\?.*$/, ''));
  9291. else if (url.pathname.startsWith('/collections/')) return pexelsCollectionResolver(url);
  9292. break;
  9293. case 'www.piwigo.org': case 'piwigo.org':
  9294. /*if (url.pathname.includes('/picture/')) */return globalFetch(url, { responseType: 'text' }).then(function(response) {
  9295. if (/^(?:RVAS)\s*=\s*(\{[\S\s]+?\})$/m.test(response.responseText)) try {
  9296. var derivatives = eval('(' + RegExp.$1 + ')').derivatives.sort((a, b) => b.w * b.h - a.w * a.h);
  9297. return derivatives.length > 0 ? 'https://piwigo.org/demo/'.concat(derivatives[0].url) : notFound;
  9298. } catch(e) { console.warn(e) }
  9299. return Promise.reject('Unexpected page structure');
  9300. });
  9301. break;
  9302. case 'www.freeimages.com': case 'freeimages.com':
  9303. if (url.pathname.startsWith('/photo/')) return globalFetch(url).then(function(response) {
  9304. var types = Array.from(response.document.querySelectorAll('ul.download-type > li > span.reso'))
  9305. .sort((a, b) => eval(b.textContent.replace('x', '*')) - eval(a.textContent.replace('x', '*')));
  9306. return types.length > 0 ? url.origin.concat(types[0].parentNode.querySelector('a').pathname) : notFound;
  9307. });
  9308. break;
  9309. case 'redacted.ch':
  9310. if (url.pathname != '/image.php') break;
  9311. return globalFetch(url, { method: 'HEAD' }).then(response => response.finalUrl);
  9312. case 'demo.cloudimg.io':
  9313. if (!/\b(https?:\/\/\S+)$/.test(url.pathname.concat(url.search, url.hash))) break;
  9314. var resolved = RegExp.$1;
  9315. if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
  9316. return imageUrlResolver(resolved);
  9317. case 'www.pimpandhost.com': case 'pimpandhost.com':
  9318. if (!url.pathname.startsWith('/image/')) break;
  9319. return globalFetch(url).then(function(response) {
  9320. var elem = resopnse.document.querySelector('div.main-image-wrapper');
  9321. if (elem != null && elem.dataset.src) return 'https:'.concat(elem.dataset.src);
  9322. elem = resopnse.document.querySelector('div.img-wrapper > a > img');
  9323. return elem != null ? 'https:'.concat(elem.src) : notFound;
  9324. });
  9325. case 'www.screencast.com': case 'screencast.com':
  9326. return globalFetch(url).then(function(response) {
  9327. var ref = response.document.querySelectorAll('ul#containerContent > li a.media-link');
  9328. if (ref.length <= 0) return getFromMeta(response.document) || notFound;
  9329. return Promise.all(Array.from(ref).map(a => imageUrlResolver('https://www.screencast.com' + a.href)));
  9330. });
  9331. case 'abload.de':
  9332. if (!url.pathname.startsWith('/image.php')) break;
  9333. return globalFetch(url).then(function(response) {
  9334. var elem = response.document.querySelector('img#image');
  9335. if (elem == null) return notFound;
  9336. var src = new URL(elem.src);
  9337. return imageHostHandlers.abload.origin + src.pathname + src.search;
  9338. });
  9339. case 'fastpic.ru':
  9340. if (url.pathname.startsWith('/view/'))
  9341. return globalFetch(url).then(response => imageUrlResolver(response.document.querySelector('a.img-a').href));
  9342. if (url.pathname.startsWith('/fullview/')) return globalFetch(url).then(function(response) {
  9343. var node = response.document.getElementById('image');
  9344. if (node != null) return node.src;
  9345. return /\bvar\s+loading_img\s*=\s*'(\S+?)';/.test(response.responseText) ? RegExp.$1 : notFound;
  9346. });
  9347. break;
  9348. case 'www.radikal.ru': case 'radikal.ru': case 'a.radikal.ru':
  9349. return globalFetch(url).then(response => response.document.querySelector('div.mainBlock img').src);
  9350. case 'imageban.ru': case 'ibn.im':
  9351. return globalFetch(url).then(response => response.document.querySelector('a[download]').href);
  9352. case 'slow.pics':
  9353. if (!url.pathname.startsWith('/c/')) break;
  9354. return globalFetch(url).then(function(response) {
  9355. var nodes = response.document.querySelectorAll('img.card-img-top');
  9356. if (nodes.length > 1) return Array.from(nodes).map(img => img.src);
  9357. else if (nodes.length > 0) return nodes[0].src;
  9358. nodes = response.document.querySelectorAll('a#comparisons + div.dropdown-menu > a.dropdown-item');
  9359. if (nodes.length > 0) return Promise.all(Array.from(nodes).map(a => globalFetch(url.origin + a.pathname).then(response =>
  9360. Array.from(response.document.querySelectorAll('div#preload-images > img')).map(img => img.src))))
  9361. .then(imgUrls => imgUrls.flatten());
  9362. return notFound;
  9363. });
  9364. // music-related
  9365. case 'www.musicbrainz.org': case 'musicbrainz.org':
  9366. if (!['release', 'release-group'].some(branch => url.pathname.includes('/' + branch + '/'))) break;
  9367. return globalFetch(url).then(function(response) {
  9368. var node = response.document.querySelector('a.artwork-image');
  9369. if (node != null) return node.href;
  9370. return (node = response.document.querySelector('div.cover-art > img')) != null ? node.src : notFound;
  9371. });
  9372. case 'music.apple.com':
  9373. if (!itunesRlsParser.test(url)) break;
  9374. return globalFetch(url).then(function(response) {
  9375. var meta = response.document.querySelector('meta[property="og:image"][content]');
  9376. if (meta == null || !meta.content) return notFound;
  9377. return verifyImageUrl(meta.content.replace(/\/\d+x\d+\w*(?=\.\w+$)/, '/100000x100000-999')).catch(reason => meta.content);
  9378. });
  9379. case 'www.deezer.com': case 'deezer.com':
  9380. return globalFetch(url).then(function(response) {
  9381. var meta = response.document.querySelector('meta[property="og:image"][content]');
  9382. if (meta == null || !meta.content) return notFound;
  9383. return verifyImageUrl(meta.content.replace(/\/\d+x\d+\w*(?=\.\w+$)/, '/1400x1400-000000-100-0-0'))
  9384. .catch(reason => meta.content);
  9385. });
  9386. case 'www.qobuz.com': case 'qobuz.com':
  9387. if (!url.pathname.includes('/album/')) break;
  9388. return globalFetch(url).then(function(response) {
  9389. var img = response.document.querySelector('div.album-cover > img');
  9390. if (img == null) return notFound;
  9391. return verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_org'))
  9392. .catch(reason => verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_max'))).catch(reason => img.src);
  9393. });
  9394. case 'www.discogs.com': case 'discogs.com':
  9395. return globalFetch(url).then(function(response) {
  9396. const dcOrigin = 'https://www.discogs.com';
  9397. let master = response.document.getElementById('all-versions-link');
  9398. return (master != null ?
  9399. globalFetch(dcOrigin + master.pathname).then(response => getFromMeta(response.document) || notFound)
  9400. : Promise.reject('no master')).catch(reason => getFromMeta(response.document) || notFound)
  9401. .then(imgUrl => /^(?:https?):\/\/img\.discogs\.com\/.+\/(\S+?\.\w+)\b/i.test(imgUrl) ?
  9402. dcOrigin + '/image/' + RegExp.$1 : imgUrl);
  9403. });
  9404. case 'www.boomkat.com': case 'boomkat.com':
  9405. if (!url.pathname.startsWith('/products/')) break;
  9406. return globalFetch(url).then(function(response) {
  9407. var img = response.document.querySelector('img[itemprop="image"]');
  9408. if (img == null) return notFound;
  9409. return verifyImageUrl(img.src.replace(/\/large\//i, '/original/')).catch(reason => img.src);
  9410. });
  9411. case 'www.bleep.com': case 'bleep.com':
  9412. if (!url.pathname.startsWith('/release/')) break;
  9413. return globalFetch(url).then(function(response) {
  9414. var meta = getFromMeta(response.document);
  9415. return meta ? verifyImageUrl(meta.replace(/\/r\/[a-z]\//i, '/r/')).catch(reason => meta) : notFound;
  9416. });
  9417. case 'www.soundcloud.com': case 'soundcloud.com':
  9418. return globalFetch(url).then(function(response) {
  9419. var meta = getFromMeta(response.document);
  9420. return meta ? verifyImageUrl(meta.replace(/\bt\d+x\d+(?=\.\w+$)/, 'original')).catch(reason => meta) : notFound;
  9421. });
  9422. case 'www.prestomusic.com': case 'prestomusic.com':
  9423. if (!url.pathname.includes('/products/')) break;
  9424. return globalFetch(url)
  9425. .then(response => verifyImageUrl(response.document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/)));
  9426. case 'www.bontonland.cz':case 'bontonland.cz':
  9427. return globalFetch(url).then(response => response.document.querySelector('a.detailzoom').href);
  9428. case 'www.nativedsd.com':case 'nativedsd.com':
  9429. if (!url.pathname.includes('/albums/')) break;
  9430. return globalFetch(url).then(response => response.document.querySelector('a#album-cover').href);
  9431. case 'www.prostudiomasters.com': case 'prostudiomasters.com':
  9432. if (!url.pathname.includes('/album/')) break;
  9433. return globalFetch(url).then(function(response) {
  9434. var a = response.document.querySelector('img.album-art');
  9435. return verifyImageUrl(a.currentSrc).catch(reason => a.src);
  9436. });
  9437. case 'www.e-onkyo.com': case 'e-onkyo.com':
  9438. if (!url.pathname.includes('/album/')) break;
  9439. return globalFetch(url).then(function(response) {
  9440. var meta = getFromMeta(response.document);
  9441. return meta ? meta.replace(/\/s\d+\//, '/s0/') : notFound;
  9442. })
  9443. case 'store.acousticsounds.com':
  9444. return globalFetch(url).then(function(response) {
  9445. var link = response.document.querySelector('div#detail > link[rel="image_src"]');
  9446. return verifyImageUrl(link.href.replace(/\/medium\//i, '/xlarge/')).catch(reason => link.href);
  9447. });
  9448. case 'www.indies.eu': case 'indies.eu':
  9449. if (!url.pathname.includes('/alba/')) break;
  9450. return globalFetch(url).then(response => verifyImageUrl)(response.document.querySelector('div.obrazekDetail > img').src);
  9451. case 'www.beatport.com': case 'classic.beatport.com': case 'beatport.com':
  9452. if (!url.pathname.includes('/release/')) break;
  9453. return globalFetch(url).then(function(response) {
  9454. var elem = getFromMeta(response.document);
  9455. return elem || ((elem = response.document.querySelector('div.artwork')) != null ?
  9456. 'https:' + elem.dataset.modalArtwork : notFound);
  9457. }).then(imgUrl => imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/image/'));
  9458. case 'www.supraphonline.cz': case 'supraphonline.cz':
  9459. if (!url.pathname.includes('/album/')) break;
  9460. return globalFetch(url).then(response => verifyImageUrl(response.document.querySelector('meta[itemprop="image"]')
  9461. .content.replace(/\?.*$/, '')).catch(reason => notFound));
  9462. case 'vgmdb.net':
  9463. if (!url.pathname.includes('/album/')) break;
  9464. return globalFetch(url).then(function(response) {
  9465. var div = response.document.querySelector('div#coverart');
  9466. return verifyImageUrl(/\b(?:url)\s*\(\"(.*)"\)/i.test(div.style['background-image']) && RegExp.$1).catch(reason => notFound);
  9467. });
  9468. case 'www.ototoy.jp': case 'ototoy.jp':
  9469. return globalFetch(url).then(function(response) {
  9470. var img = response.document.querySelector('div#tralbumArt > a.popupImage');
  9471. return verifyImageUrl(img.dataset.src).catch(reason => img.src);
  9472. });
  9473. case 'music.yandex.ru':
  9474. if (!url.pathname.includes('/album/')) break;
  9475. return globalFetch(url).then(function(response) {
  9476. var script = response.document.querySelector('script.light-data');
  9477. return verifyImageUrl(JSON.parse(script.text).image).catch(reason => notFound);
  9478. });
  9479. // case 'www.mora.jp': case 'mora.jp':
  9480. // if (!url.pathname.includes('/package/')) break;
  9481. // return loadMoraMetadata(url).then(packageMeta => packageMeta.packageUrl + packageMeta.fullsizeimage);
  9482. case 'www.pias.com': case 'store.pias.com': case 'pias.com':
  9483. return globalFetch(url).then(function(response) {
  9484. var node = getFromMeta(response.document);
  9485. if (node) return verifyImage(node.replace(/\/[sbl]\//i, '/')).catch(reason => node);
  9486. node = response.document.querySelector('img[itemprop="image"]');
  9487. return node != null ? verifyImage(node.src.replace(/\/[sbl]\//i, '/')).catch(reason => node.src) : notFound;
  9488. });
  9489. case 'www.eclassical.com': case 'eclassical.com':
  9490. return globalFetch(url).then(function(response) {
  9491. var a = response.document.querySelector('div#articleImage > a');
  9492. return a != null ? a.href : notFound;
  9493. });
  9494. case 'www.hdtracks.com': case 'hdtracks.com':
  9495. if (!/\/album\/(\w+)\b/.test(url)) break;
  9496. return fetch('https://hdtracks.azurewebsites.net/api/v1/album/' + RegExp.$1).then(response => response.json())
  9497. .then(result => result.status.toLowerCase() == 'ok' ? result.cover : Promise.reject(result.status));
  9498. case 'www.muziekweb.nl': case 'muziekweb.nl':
  9499. if (!/\/Link\/(\w+)\b/i.test(url)) break;
  9500. return globalFetch(url).then(function(response) {
  9501. let meta = getFromMeta(response.document)
  9502. return meta ? meta.replace(/\/COVER\/\w+\b/i, '/COVER/SUPERLARGE') : notFound;
  9503. });
  9504. }
  9505. return globalFetch(url, { headers: { 'Referer': url.origin } }).then(function(response) {
  9506. if (url.pathname.startsWith('/album/')
  9507. && response.document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
  9508. return cheveretoGalleryResolver(url.hostname, url);
  9509. return getFromMeta(response.document) || notFound;
  9510. });
  9511. }));
  9512. }
  9513.  
  9514. PTPimg.prototype.setSession = function() {
  9515. return this.apiKey ? Promise.resolve(this.apiKey) : globalFetch(this.origin).then(response => {
  9516. var apiKey = response.document.getElementById('api_key');
  9517. if (apiKey == null) {
  9518. let counter = GM_getValue('ptpimg_reminder_read', 0);
  9519. if (counter < 3) {
  9520. alert(`
  9521. PTPimg API key could not be captured. Please login to ${this.origin}/ and redo the action.
  9522.  
  9523. If you don\'t have PTPimg account at your disposal and not using the script on OPS,
  9524. consider to set "auto_rehost_cover" config entry to false.
  9525. (Tampermonkey extension menu -> right click to Upload Assistant -> Storage tab)
  9526.  
  9527. Local images uploading is still available to fallback image hosts (proxied on RED).
  9528. `);
  9529. GM_setValue('ptpimg_reminder_read', ++counter);
  9530. }
  9531. return Promise.reject('PTPimg API key not configured');
  9532. }
  9533. if (!(this.apiKey = apiKey.value)) return Promise.reject('Assertion failed: empty PTPimg API key');
  9534. GM_setValue('ptpimg_api_key', this.apiKey);
  9535. Promise.resolve(this.apiKey)
  9536. .then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) });
  9537. return this.apiKey;
  9538. });
  9539. }
  9540.  
  9541. var imageHosts = new ImageHostManager(
  9542. message => { addMessage(message, 'warning') },
  9543. ['PTPimg', 'NWCD', 'ImgBB', 'PixHost', 'ImgBox', 'PostImage', 'FunkyIMG', 'Slowpoke', 'VgyMe', 'Abload'],
  9544. isRED ? ['PTPimg'] : isNWCD ? ['NWCD']
  9545. : ['PTPimg', 'ImgBB', 'PixHost', 'PostImage', 'FunkyIMG', 'Abload', 'Radikal', 'PicaBox', 'Jerking', 'Imgur']
  9546. );
  9547.  
  9548. function checkImageSize(imageUrl, node, size) {
  9549. if (!prefs.image_size_reduce_threshold) return Promise.resolve(imageUrl);
  9550. if (!(node instanceof Node)) node = null;
  9551. if (node != null) node.disabled = true;
  9552. return (size > 0 ? Promise.resolve(size) : getRemoteFileSize(imageUrl)).then(function(size) {
  9553. if (size <= prefs.image_size_reduce_threshold * 2**20) return Promise.resolve(imageUrl);
  9554. return reduceImageSize(imageUrl, GM_getValue('image_reduce_maxheight', 2160),
  9555. GM_getValue('image_reduce_jpegquality', 90)).then(function(output) {
  9556. if (node != null) node.value = output.uri;
  9557. Promise.resolve(output.size).then(reducedSize => {
  9558. addMessage('cover size reduced by ' + Math.round((size - reducedSize) * 100 / size) +
  9559. '% (' + Math.ceil(size / 2**10) + ' → ' + Math.ceil(reducedSize / 2**10) + ' KiB)', 'info');
  9560. });
  9561. return output.uri;
  9562. });
  9563. }).catch(function(reason) {
  9564. addMessage('failed to get remote image size or optimize the image: ' + reason +
  9565. ', size reduction was not performed', 'warning');
  9566. return imageUrl;
  9567. }).then(function(finalUrl) {
  9568. if (node != null) node.disabled = false;
  9569. return finalUrl;
  9570. });
  9571. }
  9572.  
  9573. function validateTorrentFile(torrent) {
  9574. tfMessages.forEach(node => { node.remove() });
  9575. tfMessages = [];
  9576. var fr = new FileReader;
  9577. fr.onload = function(evt) {
  9578. torrent = bdecode(new Uint8Array(fr.result));
  9579. if (!torrent || typeof torrent != 'object') {
  9580. console.warn('Assertion failed:', torrent);
  9581. return;
  9582. }
  9583. var rootImageCount = 0, category = document.getElementById('categories'),
  9584. isMusicUpload = category == null || category.value === '0' || category.value == 'Music',
  9585. rootFolderName = decodeURIComponent(escape(torrent.info.name));
  9586. if (hyphenCoupling.test(rootFolderName)) tfMessages.push(addMessage('torrent folder hyphen coupling ("' +
  9587. rootFolderName + '")', 'notice'));
  9588. torrent.info.files.forEach(function(file) {
  9589. var fullPath = decodeURIComponent(escape(file.path.join('/')));
  9590. if (/\s{2,}/.test(fullPath))
  9591. tfMessages.push(addMessage('excessive whitespace in file path: ' + filepath, 'warning'));
  9592. if (file.path.some(folderName => /^\s+|\s+$/.test(folderName)))
  9593. tfMessages.push(addMessage('leading/tailing whitespace in path component: ' + filepath, 'warning'));
  9594. var fileName = decodeURIComponent(escape(file.path.pop())),
  9595. totalLen = rootFolderName.trueLength() + 1 + fullPath.trueLength();
  9596. if (totalLen > 180) tfMessages.push(addMessage(new HTML('file "' +
  9597. safeText(fullPath.normalize('NFC').slice(0, Math.max(179 - rootFolderName.trueLength(), 0))) +
  9598. safeText(fullPath.normalize('NFC').slice(Math.max(179 - rootFolderName.trueLength(), 0))).bold() +
  9599. '" exceeding allowed length (' + totalLen + ' > 180)'), 'warning'));
  9600. if (file.path.length <= 0 && imageExtensions.some(ext => fileName.toLowerCase().endsWith('.' + ext))) {
  9601. ++rootImageCount;
  9602. if (!/^(?:cover|artworks?|sleeve|artist|(?:front|back|rear)(?: cover)?)\.\w+$/i.test(fileName) && isMusicUpload)
  9603. tfMessages.push(addMessage('Nonstandard cover image name: ' + fileName, 'notice'));
  9604. }
  9605. if (/(?:\.(?:torrent|\!ut|\!qb|url|lnk|tmp|bak)|^Thumbs\.db)$/i.test(fileName))
  9606. tfMessages.push(addMessage(new HTML('garbage file "' + safeText(fullPath).bold() + '"'), 'warning'));
  9607. if (/^(?:(?:MediaInfo)\.txt|(?:Lossless Audio Checker|results|auCDtect|audiochecker)\.log)$/i.test(fileName))
  9608. tfMessages.push(addMessage('Auxiliary text file in torrent: ' + fullPath, 'notice'));
  9609. if (/^(?:thumb\.jpg)$/i.test(fileName)) tfMessages.push(addMessage('thumb.jpg in torrent', 'notice'));
  9610. if (/^(?:DR\d+\.txt)$/i.test(fileName))
  9611. tfMessages.push(addMessage(`Nonstandard DR report in torrent (${$fileName})`, 'notice'));
  9612. if ([
  9613. 'm3u', 'm3u8', 'pls', 'fpl', 'wpl', 'asx', 'b4s', 'bpl', 'm4u', 'ram', 'plp',
  9614. 'kpl', 'plist', 'xml', 'rmp', 'xspf', 'smi', 'smil', 'wax', 'wvx', 'wmx', 'pla',
  9615. ].some(ext => fileName.toLowerCase().endsWith('.' + ext)))
  9616. tfMessages.push(addMessage('Disposable playlist found: ' + fullPath, 'notice'));
  9617. if (hyphenCoupling.test(fullPath))
  9618. tfMessages.push(addMessage('file path hyphen coupling ("' + fullPath + '")', 'notice'));
  9619. });
  9620. if (isMusicUpload) {
  9621. if (rootImageCount > 1) tfMessages.push(addMessage(`More images (${rootImageCount}) in root folder`, 'notice'));
  9622. if (rootImageCount <= 0) tfMessages.push(addMessage('No cover image in root folder', 'notice'));
  9623. }
  9624. ref = document.querySelector('td.ua-messages-bg');
  9625. if (ref != null && ref.childElementCount <= 0) ref.parentNode.remove();
  9626. };
  9627. fr.onerror = fr.ontimeout = error => { console.error('FileReader error (' + torrent.name + ')') };
  9628. fr.readAsArrayBuffer(torrent);
  9629.  
  9630. function bdecode(str) {
  9631. var pos = 0, infoBegin = 0, infoEnd = 0;
  9632. return bdecodeInternal(str);
  9633.  
  9634. function bdecodeInternal(str) {
  9635. if (pos >= str.length) return null;
  9636. switch (str[pos]) {
  9637. case 100: // char code for 'd'
  9638. ++pos;
  9639. var retval = [];
  9640. while (str[pos] != 101) { // char code for 'e'
  9641. let key = bdecodeInternal(str), val = bdecodeInternal(str);
  9642. if (key === null || val === null) break;
  9643. retval[key] = val;
  9644. }
  9645. if (infoEnd == -1) infoEnd = pos + 1;
  9646. retval.isDct = true;
  9647. ++pos;
  9648. return retval;
  9649. case 105: // char code for 'i'
  9650. ++pos;
  9651. var digits = Array.prototype.indexOf.call(str, 101, pos) - pos; // 101 = char code for 'e'
  9652. val = '';
  9653. for (var i = pos; i < digits + pos; ++i) val += String.fromCharCode(str[i]);
  9654. val = Math.round(parseFloat(val));
  9655. pos += digits + 1;
  9656. return val;
  9657. case 108: // char code for 'l'
  9658. ++pos;
  9659. retval = [];
  9660. while (str[pos] != 101) { // char code for 'e'
  9661. let val = bdecodeInternal(str);
  9662. if (val === null) break;
  9663. retval.push(val);
  9664. }
  9665. ++pos;
  9666. return retval;
  9667. default: {
  9668. digits = Array.prototype.indexOf.call(str, 58, pos) - pos; // 58 = char code for ':'
  9669. if (digits < 0 || digits > 20) return null;
  9670. var len = '';
  9671. for (i = pos; i < digits + pos; ++i) len += String.fromCharCode(str[i]);
  9672. len = parseInt(len);
  9673. pos += digits + 1;
  9674. let fstring = '';
  9675. for (i = pos; i < len + pos; ++i) fstring += String.fromCharCode(str[i]);
  9676. pos += len;
  9677. if (fstring == 'info') {
  9678. infoBegin = pos;
  9679. infoEnd = -1;
  9680. }
  9681. return fstring;
  9682. }
  9683. }
  9684. }
  9685. }
  9686. }
  9687.  
  9688. function defaultErrorHandler(response) {
  9689. console.error('HTTP error:', response);
  9690. var e = 'HTTP error ' + response.status;
  9691. if (response.statusText) e += ' (' + response.statusText + ')';
  9692. if (response.error) e += ' (' + response.error + ')';
  9693. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  9694. return e;
  9695. }
  9696.  
  9697. function defaultTimeoutHandler(response) {
  9698. console.error('HTTP timeout:', response);
  9699. var e = 'HTTP timeout';
  9700. if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
  9701. return e;
  9702. }
  9703.  
  9704. function insertUAControls() {
  9705. document.head.appendChild(document.createElement('style')).innerHTML = `
  9706. .ua-messages {
  9707. text-indent: -2em;
  9708. margin-left: 2em;
  9709. font: 8pt Verdana, Tahoma, sans-serif;
  9710. }
  9711. .ua-messages-bg {
  9712. padding: 15px;
  9713. text-align: left;
  9714. background-color: darkslategray;
  9715. }
  9716.  
  9717. .ua-critical { color: red; font-weight: bold; font-size: 10pt; }
  9718. .ua-warning { color: #ff8d00; font-weight: 500; font-size: 9pt; }
  9719. .ua-notice { color: #e3d67b; }
  9720. .ua-info { color: white; }
  9721.  
  9722. .ua-button { vertical-align: middle; background-color: transparent; }
  9723. input.ua-button2 {
  9724. /*color: white;*/
  9725. /*background-color: #725200;*/
  9726. width: 13em;
  9727. font: 500 x-small "Segoe UI", Calibri, sans-serif;
  9728. }
  9729. .ua-input {
  9730. font: 600 x-small "Segoe UI", Calibri, sans-serif;
  9731. color: slategray; background-color: antiquewhite;
  9732. width: 620px; height: 40px;
  9733. margin-top: 8px; margin-bottom: 8px;
  9734. }
  9735. .ua-input:focus { color: black; }
  9736.  
  9737. #cover-preview {
  9738. width: 100%;
  9739. /*box-shadow: 3px 3px 3px;*/
  9740. }
  9741. #cover-size {
  9742. width: 100%;
  9743. color: white; background-color: #0a4a75;
  9744. font: 8.5pt Verdana, Tahoma, sans-serif;
  9745. text-align: center;
  9746. /*padding-top: 5px;*/
  9747. }
  9748.  
  9749. ::placeholder {
  9750. font: bold 12pt Calibri, "Segoe UI", sans-serif;
  9751. color: #808080;
  9752. /*text-shadow: 0px 0px 3px #b4b4b4;*/
  9753. }
  9754. `;
  9755.  
  9756. if (isUpload) {
  9757. if ((ref = document.querySelector('form#upload_table > div#dynamic_form')) == null) return;
  9758. common1();
  9759. common4('Autofill form (overwrite)', 'Autofill form (keep values)');
  9760. common2();
  9761. ref.parentNode.insertBefore(tbl, ref);
  9762. } else if (isEdit) {
  9763. if ((ref = document.querySelector('form.edit_form > div > div > input[type="submit"]')) == null) return;
  9764. ref = ref.parentNode;
  9765. ref.parentNode.insertBefore(document.createElement('br'), ref);
  9766. common1();
  9767. common4('Autofill (overwrite)', 'Autofill (keep values)');
  9768. //common3('Autofill form (keep values)');
  9769. common2();
  9770. tbl.style.marginBottom = '10px';
  9771. ref.parentNode.insertBefore(tbl, ref);
  9772. } else if (isTorrentEdit) {
  9773. if ((ref = document.querySelector('form#upload_table')) == null) return;
  9774. common1();
  9775. common4('Autofill (overwrite)', 'Autofill (keep values)');
  9776. //common3('Autofill form (keep values)');
  9777. common2();
  9778. tbl.style.marginBottom = '10px';
  9779. ref.append(tbl);
  9780. } else if (isRequestNew) {
  9781. if ((ref = document.getElementById('categories')) == null) return;
  9782. ref = ref.parentNode.parentNode.nextElementSibling;
  9783. ref.parentNode.insertBefore(document.createElement('br'), ref);
  9784. common1();
  9785. //common3('Autofill from URL');
  9786. common4('Autofill form (overwrite)', 'Autofill form (keep values)');
  9787. common2();
  9788. child = document.createElement('td');
  9789. child.colSpan = 2;
  9790. child.append(tbl);
  9791. elem = document.createElement('tr');
  9792. elem.append(child);
  9793. ref.parentNode.insertBefore(elem, ref);
  9794. } else if (isRequestEdit) {
  9795. if ((ref = document.querySelector('input#button[type="submit"]')) == null) return;
  9796. ref = ref.parentNode.parentNode;
  9797. ref.parentNode.insertBefore(document.createElement('br'), ref);
  9798. common1();
  9799. //common3('Autofill form (keep values)');
  9800. common4('Autofill (overwrite)', 'Autofill (keep values)');
  9801. common2();
  9802. tbl.style.marginBottom = '10px';
  9803. elem = document.createElement('tr');
  9804. child = document.createElement('td');
  9805. child.colSpan = 2;
  9806. child.append(tbl);
  9807. elem.append(child);
  9808. ref.parentNode.insertBefore(elem, ref);
  9809. }
  9810.  
  9811. if ((ref = document.getElementById('categories')) != null) {
  9812. ref.addEventListener('change', function(e) {
  9813. elem = document.getElementById('upload assistant');
  9814. if (elem != null) elem.style.visibility = this.value < 4
  9815. || ['Music', 'Applications', 'E-Books', 'Audiobooks'].includes(this.value) ? 'visible' : 'collapse';
  9816. setTimeout(setHandlers, 2000);
  9817. });
  9818. }
  9819.  
  9820. function common1() {
  9821. tbl = document.createElement('tr');
  9822. tbl.style.backgroundColor = 'darkgoldenrod';
  9823. tbl.style.verticalAlign = 'middle';
  9824. elem = document.createElement('td');
  9825. elem.style.textAlign = 'center';
  9826. child = document.createElement('textarea');
  9827. child.id = 'UA-data';
  9828. child.name = 'UA-data';
  9829. child.className = 'ua-input';
  9830. child.spellcheck = false;
  9831. child.placeholder = 'Paste/drop album from foobar2000 or release URL here';
  9832. child.onpaste = uaInsert;
  9833. if (!isNWCD) {
  9834. child.ondrop = uaInsert;
  9835. child.ondragover = clear0;
  9836. if (isFirefox) child.oninput = fixFirefoxDropBug;
  9837. } else child.ondrop = child.ondragstart = child.ondragover = function(evt) {
  9838. evt.preventDefault();
  9839. evt.stopPropagation();
  9840. return false;
  9841. };
  9842. var desc = document.getElementById('body');
  9843. if (desc != null && urlParser.test(desc.value)) {
  9844. child.value = RegExp.$1;
  9845. desc.value = '';
  9846. if (prefs.autfill_delay > 0) autoFill = setTimeout(fillFromText, prefs.autfill_delay);
  9847. }
  9848. elem.append(child);
  9849. tbl.append(elem);
  9850. elem = document.createElement('td');
  9851. elem.style.textAlign = 'center';
  9852. }
  9853.  
  9854. function common2() {
  9855. tbl.append(elem);
  9856. var tb = document.createElement('tbody');
  9857. tb.append(tbl);
  9858. tbl = document.createElement('table');
  9859. tbl.id = 'upload assistant';
  9860. tbl.append(tb);
  9861. }
  9862.  
  9863. function common3(caption) {
  9864. child = document.createElement('input');
  9865. child.id = 'append-from-text';
  9866. child.value = caption;
  9867. child.type = 'button';
  9868. child.className = 'ua-button2';
  9869. child.style.height = '52px';
  9870. child.onclick = fillFromText;
  9871. elem.append(child);
  9872. }
  9873. function common4(caption1, caption2) {
  9874. var x = [];
  9875. x.push(document.createElement('tr'));
  9876. x[0].classList.add('ua-button');
  9877. child = document.createElement('input');
  9878. child.id = 'fill-from-text';
  9879. child.value = caption1;
  9880. child.type = 'button';
  9881. child.className = 'ua-button2';
  9882. child.onclick = fillFromText;
  9883. x[0].append(child);
  9884. elem.append(x[0]);
  9885. x.push(document.createElement('tr'));
  9886. x[1].classList.add('ua-button');
  9887. child = document.createElement('input');
  9888. child.id = 'fill-from-text-weak';
  9889. child.value = caption2;
  9890. child.type = 'button';
  9891. child.className = 'ua-button2';
  9892. child.onclick = fillFromText;
  9893. x[1].append(child);
  9894. elem.append(x[1]);
  9895. }
  9896. }