· 5 years ago · May 28, 2020, 10:32 PM
1// ==UserScript==
2// @name [RED/OPS/NWCD] Upload Assistant
3// @namespace https://greasyfork.org/users/321857-anakunda
4// @version 1.222
5// @description Accurate filling of new upload/request and group/request edit forms based on foobar2000's playlist selection or web link, offline and online release integrity check, tracklist format customization, featured artists extraction, classical works formatting, cover art fetching from store, checking for previous upload, form enhancements and more
6// @author Anakunda
7// @iconURL https://redacted.ch/favicon.ico
8// @match https://redacted.ch/upload.php*
9// @match https://redacted.ch/torrents.php?action=editgroup&*
10// @match https://redacted.ch/torrents.php?action=edit&*
11// @match https://redacted.ch/requests.php?action=new*
12// @match https://redacted.ch/requests.php?action=edit*
13// @match https://notwhat.cd/upload.php*
14// @match https://notwhat.cd/torrents.php?action=editgroup&*
15// @match https://notwhat.cd/torrents.php?action=edit&*
16// @match https://notwhat.cd/requests.php?action=new*
17// @match https://notwhat.cd/requests.php?action=edit*
18// @match https://orpheus.network/upload.php*
19// @match https://orpheus.network/torrents.php?action=editgroup&*
20// @match https://orpheus.network/torrents.php?action=edit&*
21// @match https://orpheus.network/requests.php?action=new*
22// @match https://orpheus.network/requests.php?action=edit*
23// @match https://bemaniso.ws/upload.php*
24// @match https://bemaniso.ws/torrents.php?action=editgroup&*
25// @match https://bemaniso.ws/torrents.php?action=edit&*
26// @match https://bemaniso.ws/requests.php?action=new*
27// @match https://bemaniso.ws/requests.php?action=edit*
28// @connect file://*
29// @connect *
30// @grant GM_xmlhttpRequest
31// @grant GM_getValue
32// @grant GM_setValue
33// @grant GM_deleteValue
34// @require https://greasyfork.org/scripts/393837-qobuzlib/code/QobuzLib.js
35// @require https://greasyfork.org/scripts/401725-xhrlib/code/xhrLib.js
36// @require https://greasyfork.org/scripts/394414-ua-resource/code/UA-resource.js
37// @require https://greasyfork.org/scripts/396340-js-sha1/code/js-sha1.js
38// @require https://greasyfork.org/scripts/396360-js-stringdistance/code/js-stringdistance.js
39// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
40// //@require https://connect.soundcloud.com/sdk/sdk-3.3.2.js
41// ==/UserScript==
42
43// Additional setup: to work, set the pattern below as built-in foobar2000 copy command or custom Text Tools plugin quick copy command
44// $replace($replace([%album artist%]$char(30)[%album%]$char(30)[$if3(%date%,%ORIGINAL RELEASE DATE%,%year%)]$char(30)[$if3(%releasedate%,%retail date%,%date%,%year%)]$char(30)[$if2(%label%,%publisher%)]$char(30)[$if3(%catalog%,%CATALOGNUMBER%,%CATALOG NUMBER%,%labelno%,%catalog #%,%SKU%)]$char(30)[%country%]$char(30)%__encoding%$char(30)%__codec%$char(30)[%__codec_profile%]$char(30)[%__bitrate%]$char(30)[%__bitspersample%]$char(30)[%__samplerate%]$char(30)[%__channels%]$char(30)[$if3(%media%,%format%,%source%,%MEDIATYPE%,%SOURCEMEDIA%,%discogs_format%)]$char(30)[%genre%[|%style%]]$char(30)[%discnumber%]$char(30)[$if2(%totaldiscs%,%disctotal%)]$char(30)[%discsubtitle%]$char(30)[%track number%]$char(30)[$if2(%totaltracks%,%TRACKTOTAL%)]$char(30)[%title%]$char(30)[%track artist%]$char(30)[$if($strcmp(%performer%,%artist%),,%performer%)]$char(30)[$if3(%composer%,%writer%,%SONGWRITER%,%author%,%LYRICIST%)]$char(30)[%conductor%]$char(30)[%remixer%]$char(30)[$if2(%compiler%,%mixer%)]$char(30)[$if2(%producer%,%producedby%)]$char(30)[%length_seconds_fp%]$char(30)[%length_samples%]$char(30)[%filesize%]$char(30)[%replaygain_album_gain%]$char(30)[%album dynamic range%]$char(30)[%__tool%][ | $if2(%MQAENCODER%,%ENCODER%)][ | %ENCODER_OPTIONS%]$char(30)[$if2(%url%,%www%)]$char(30)[$directory_path(%path%)]$char(30)[$if2(%comment%,%description%)]$char(30)$trim([BARCODE=$trim($replace($if3(%barcode%,%UPC%,%EAN%,%MCN%), ,)) ][DISCID=$trim(%DISCID%) ][ASIN=$trim(%ASIN%) ][ISRC=$trim(%ISRC%) ][ISWC=$trim(%ISWC%) ][DISCOGS_ID=$trim(%discogs_release_id%) ][MBID=$trim(%MUSICBRAINZ_ALBUMID%) ][ACCURATERIPCRC=$trim(%ACCURATERIPCRC%) ][ACCURATERIPDISCID=$trim(%ACCURATERIPDISCID%) ][ACCURATERIPID=$trim(%ACCURATERIPID%) ][SOURCEID=$trim($replace(%SOURCEID%, ,_)) ][CT_TOC=$trim(%CDTOC%) ][ITUNES_TOC=$trim(%ITUNES_CDDB_1%) ][RELEASETYPE=$replace($if2(%RELEASETYPE%,%RELEASE TYPE%), ,_) ][COMPILATION=$trim(%compilation%) ][EXPLICIT=$trim(%EXPLICIT%) ]SCENE=$if($and(%ENCODER%,%LANGUAGE%,%MEDIA%,%PUBLISHER%,%RELEASE TYPE%,%RETAIL DATE%,%RIP DATE%,%RIPPING TOOL%),1,0) [ORIGINALFORMAT=$trim($replace(%ORIGINALFORMAT%, ,_)) ][BPM=$trim(%BPM%) ]),$char(13),$char(29)),$char(10),$char(28))
45//
46// As alternative to pasted playlist, e.g. requests creation, valid URL to page on supported web can be used.
47// List of supported domains:
48//
49// For music releases:
50// - qobuz.com
51// - highresaudio.com
52// - bandcamp.com
53// - prestomusic.com
54// - discogs.com
55// - supraphonline.cz
56// - bontonland.cz (closing soon)
57// - nativedsd.com
58// - junodownload.com
59// - hdtracks.com
60// - deezer.com
61// - spotify.com
62// - prostudiomasters.com
63// - play.google.com
64// - 7digital.com
65// - e-onkyo.com
66// - acousticsounds.com
67// - indies.eu
68// - beatport.com
69// - traxsource.com
70// - musicbrainz.org
71// - music.apple.com
72// - vgmdb.net
73// - tidal.com (requires account)
74// - ototoy.jp
75// - music.yandex.ru
76// - mora.jp
77// - allmusic.com
78//
79// For e-bbook releases:
80// - martinus.cz, martinus.sk
81// - goodreads.com
82// - databazeknih.cz
83//
84// For application releases:
85// - sanet.st
86
87'use strict';
88
89const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || window.InstallTrigger;
90
91function testDomain(domain) {
92 return document.location.hostname.toLowerCase() == domain.toLowerCase();
93}
94function testPath(path, query) {
95 return document.location.pathname.toLowerCase() == '/'.concat(path.toLowerCase(), '.php')
96 && (!query || document.location.search.toLowerCase().startsWith('?'.concat(query.toLowerCase())));
97}
98
99const isRED = testDomain('redacted.ch');
100const isNWCD = testDomain('notwhat.cd');
101const isOPS = testDomain('orpheus.network');
102
103const isUpload = testPath('upload');
104const isEdit = testPath('torrents', 'action=editgroup&');
105const isRequestNew = testPath('requests', 'action=new');
106const isRequestEdit = testPath('requests', 'action=edit&');
107const isAddFormat = isUpload && /\bgroupid=(\d+)\b/i.test(document.location.search);
108
109const urlParser = /^\s*(https?:\/\/\S+)\s*$/i;
110const dcRlsParser = /^https?:\/\/(?:\w+\.)*discogs\.com\/releases?\/(\d+)(?=$|\/|\?)/i;
111const itunesRlsParser = /^https?:\/\/(?:\w+\.)*apple\.com\/.*\/(\d+)(?=$|\?)/i;
112const mbrRlsParser = /^https?:\/\/(?:beta\.)?musicbrainz\.org\/(?:\w+\/)*release\/([\w\-]+)/i;
113const dzrRlsParser = /^https?:\/\/(?:\w+\.)*deezer\.com\/(\w+\/)*album\/(\d+)$/i;
114const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'];
115const ptpimgOrigin = 'https://ptpimg.me';
116const mbrRlsPrefix = 'https://musicbrainz.org/release/';
117const discogsOrigin = 'https://www.discogs.com';
118const deezerAlbumPrefix = 'https://www.deezer.com/album/';
119const descriptionFields = ['album_desc', 'body', 'description', 'release_desc', 'release_lineage'];
120const siteApiTimeframeStorageKey = document.location.hostname.concat(' API time frame');
121
122const spotify_clientid = '6d358a207c634b1ebac640149a6090da';
123const spotify_clientsecret = '4c59880a4ec241ed9c89a24e66468c64';
124const discogs_token = 'CISOUfiQctZCkUedWJzPhzTXxRYihifZgflZAfEm';
125const lastfm_api_key = 'b9f26370d7266fbb3151b2ad4f7a74c9';
126
127const ulTimeFactor = 192;
128const rehostTimeout = 30000;
129const gazelleApiFrame = 10500;
130const ctxt = document.createElement('canvas').getContext('2d');
131
132var prefs = {
133 autfill_delay: 500, // delay in ms to autofill form after pasting text into box, 0 to disable
134 clean_on_apply: false, // clean the input box on successfull fill
135 cleanup_descriptions: true, // pre-submit cleanup to all description fields (remove empty placeholders, redundant info and garbage like empty tag pairs etc.)
136 keep_meaningles_composers: false, // keep composers from file tags also for non-composer emphasing genres
137 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)
138 single_threshold: 10 * 60, // For autodetection of release type: max length of single in s
139 EP_threshold: 30 * 60, // For autodetection of release type: max time of EP in s
140 auto_rehost_cover: true, // PTPIMG / using 3rd party script
141 auto_preview_cover: true,
142 huge_image_warning: 5, // threshold in MB for making bandwith stressing cover size warning // 0 to disable
143 cover_lookup_provider: 'all', // itunes | lastfm | deezer | musicbrainz | qobuz | google | all | empty for no lookup
144 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
145 estimate_decade_tag: true, // deduce decade tag (1980s, etc.) from album year for regular albums
146 check_whitespace: true, // check tags for leading/trailing spaces and unreadable characters
147 honour_rg: false, // do a reminder on missing RG info; off by default
148 honour_dr: false, // do a reminder on missing DR info (only for Hi-Res tracks); off by default
149 honour_url: false, // do a reminder on missing source URL (tag URL)
150 ops_always_edition: true, // (only new uploads) don't use original release but always specific edition (unify with other trackers)
151 sacd_decoder: 'foobar2000\'s SACD decoder (DSD2PCM direct / 64fp / 30kHz lowpass)',
152 use_store_logos: true, // use online source's pictograsm instead of url in textual form (if defined)
153 insert_release_date: true, // ..to rls description
154 selfrelease_label: 'self-released',
155 upcoming_tags: '', // add this tag(s) to upcoming releases (requests); empty to disable
156 remap_texttools_newlines: false, // convert underscores to linebreaks (ambiguous)
157 messages_verbosity: 0,
158 diag_mode: false,
159 // online parsers specific
160 apple_offer_alt_cover: true, // usually smaller version of preloaded cover
161 use_kana: false, // include Kana(JP) version in artist/title names; applies to mora.jp online parser
162 // online service credentials
163 redacted_api_key: '',
164 ptpimg_api_key: '',
165 discogs_key: '', // Applicxation/Consumer Key
166 discogs_secret: '', // Application/Consumer Secret
167 //soundcloud_clientid: '',
168 tidal_userid: '',
169 tidal_userpassword: '',
170 malzo_uid: '',
171 malzo_password: '',
172 imgbb_uid: '',
173 imgbb_password: '',
174 catbox_userhash: '',
175 imagevenue_uid: '',
176 imagevenue_password: '',
177 imgbox_uid: '',
178 imgbox_password: '',
179 // request specific
180 request_default_bounty: 0, // set this bounty in MB after successfull fill of request form / 0 for disable
181 always_request_perfect_flac: false,
182 include_tracklist_in_request: false, // false: include one line summary only; true: include full tracklisting
183 // tracklist specific
184 tracklist_style: 1, // 1: classic with components colouring, 2: propertional font right-justified, 3: classic center aligned
185 singles_conventional_format: false, // force one track singles to be formatted same way as albums with numbered tracklist
186 colorless_tracklist: false, // Strip all colours from tracklist
187 sort_tracklist: true,
188 reformat_trackartist: true, // (if track artist differs from main artist) rebuild track artist from partial track artists, turn off if generating wrong track artists
189 max_tracklist_width: 80, // right margin of the right aligned tracklist. should not exceed the group description width on any device
190 tracklist_size: 2, // PHPBB font size
191 title_separator: '. ', // divisor of track# and title
192 pad_leader: ' ',
193 bpm_summary: true,
194 tracklist_head_color: '#778899', // #4682B4 / #a7bdd0
195 // classical tracklist only components colouring
196 tracklist_disctitle_color: '#2bb7b7', // #bb831c
197 tracklist_work_color: '#98984d', // #b16890
198 tracklist_tracknumber_color: '#8899AA',
199 tracklist_artist_color: '#8a6995',
200 tracklist_composer_color: '#8ca014',
201 tracklist_duration_color: '#33a6cc', // #2196f3
202 // online check paramaters
203 check_integrity_online: true, // If provided URL tag, compare local release with release online and lookup for discrepancies
204 strict_online_check: false, // set to true for strict online check (metadata comparison is case sensitive)
205 duration_divergency: 0.75, // maximum tolerated playlists difference in %
206 vinyl_duration_divergency: 2.5, // maximum tolerated playlists difference in % for vinyl releases
207
208 save: function() {
209 for (var key in this) {
210 if (typeof this[key] != 'function' && this[key] != undefined) GM_setValue(key, this[key]);
211 }
212 },
213};
214Object.keys(prefs).forEach(key => { prefs[key] = GM_getValue(key, prefs[key]) });
215
216document.head.appendChild(document.createElement('style')).innerHTML = `
217.ua-messages {
218 text-indent: -2em;
219 margin-left: 2em;
220 font: 8pt Verdana, Tahoma, sans-serif;
221}
222.ua-messages-bg { padding: 15px; text-align: left; background-color: darkslategray; }
223
224.ua-critical { color: red; font-weight: bold; font-size: 10pt; }
225.ua-warning { color: #ff8d00; font-weight: 500; font-size: 9pt; }
226.ua-notice { color: #e3d67b; }
227.ua-info { color: white; }
228
229.ua-button { vertical-align: middle; background-color: transparent; }
230.ua-button2 { /*color: beige; */width: 13em; font: 300 x-small "Segoe UI", Calibri, sans-serif; }
231.ua-input {
232 font: 600 x-small "Segoe UI", Calibri, sans-serif;
233 color: slategray; background-color: antiquewhite;
234 width: 620px; height: 40px;
235 margin-top: 8px; margin-bottom: 8px;
236}
237.ua-input:focus { color: black; }
238
239#cover-preview {
240 width: 100%;
241 /*box-shadow: 3px 3px 3px;*/
242}
243#cover-size {
244 width: 100%;
245 color: white; background-color: #0a4a75;
246 font: 8.5pt Verdana, Tahoma, sans-serif;
247 text-align: center;
248 /*padding-top: 5px;*/
249}
250
251::placeholder {
252 font: bold 12pt Calibri, "Segoe UI", sans-serif;
253 color: #808080;
254 /*text-shadow: 0px 0px 3px #b4b4b4;*/
255}
256`;
257
258var ref, tbl, elem, child, messages = null, autofill = false, dom;
259var tfMessages = [], siteArtistsCache = {}, notSiteArtistsCache = [];
260
261if (isUpload) {
262 if ((ref = document.querySelector('form#upload_table > div#dynamic_form')) == null) return;
263 common1();
264 let x = [];
265 x.push(document.createElement('tr'));
266 x[0].classList.add('ua-button');
267 child = document.createElement('input');
268 child.id = 'fill-from-text';
269 child.value = 'Fill form (overwrite)';
270 child.type = 'button';
271 child.className = 'ua-button2';
272 child.onclick = fillFromText;
273 x[0].append(child);
274 elem.append(x[0]);
275 x.push(document.createElement('tr'));
276 x[1].classList.add('ua-button');
277 child = document.createElement('input');
278 child.id = 'fill-from-text-weak';
279 child.value = 'Fill form (keep values)';
280 child.type = 'button';
281 child.className = 'ua-button2';
282 child.onclick = fillFromText;
283 x[1].append(child);
284 elem.append(x[1]);
285 common2();
286 ref.parentNode.insertBefore(tbl, ref);
287} else if (isEdit) {
288 if ((ref = document.querySelector('form.edit_form > div > div > input[type="submit"]')) == null) return;
289 ref = ref.parentNode;
290 ref.parentNode.insertBefore(document.createElement('br'), ref);
291 common1();
292 child = document.createElement('input');
293 child.id = 'append-from-text';
294 child.value = 'Fill from text (append)';
295 child.type = 'button';
296 child.className = 'ua-button2';
297 child.style.height = '52px';
298 child.onclick = fillFromText;
299 elem.append(child);
300 common2();
301 tbl.style.marginBottom = '10px';
302 ref.parentNode.insertBefore(tbl, ref);
303} else if (isRequestNew) {
304 if ((ref = document.getElementById('categories')) == null) return;
305 ref = ref.parentNode.parentNode.nextElementSibling;
306 ref.parentNode.insertBefore(document.createElement('br'), ref);
307 common1();
308 child = document.createElement('input');
309 child.id = 'fill-from-text-weak';
310 child.value = 'Fill from URL';
311 child.type = 'button';
312 child.className = 'ua-button2';
313 child.style.height = '52px';
314 child.onclick = fillFromText;
315 elem.append(child);
316 common2();
317 child = document.createElement('td');
318 child.colSpan = 2;
319 child.append(tbl);
320 elem = document.createElement('tr');
321 elem.append(child);
322 ref.parentNode.insertBefore(elem, ref);
323} else if (isRequestEdit) {
324 if ((ref = document.querySelector('input#button[type="submit"]')) == null) return;
325 ref = ref.parentNode.parentNode;
326 ref.parentNode.insertBefore(document.createElement('br'), ref);
327 common1();
328 child = document.createElement('input');
329 child.id = 'append-from-text';
330 child.value = 'Fill from text (append)';
331 child.type = 'button';
332 child.className = 'ua-button2';
333 child.style.height = '52px';
334 child.onclick = fillFromText;
335 elem.append(child);
336 common2();
337 tbl.style.marginBottom = '10px';
338 elem = document.createElement('tr');
339 child = document.createElement('td');
340 child.colSpan = 2;
341 child.append(tbl);
342 elem.append(child);
343 ref.parentNode.insertBefore(elem, ref);
344}
345
346function common1() {
347 tbl = document.createElement('tr');
348 tbl.style.backgroundColor = 'darkgoldenrod';
349 tbl.style.verticalAlign = 'middle';
350 elem = document.createElement('td');
351 elem.style.textAlign = 'center';
352 child = document.createElement('textarea');
353 child.id = 'UA-data';
354 child.name = 'UA-data';
355 child.className = 'ua-input';
356 child.spellcheck = false;
357 child.placeholder = 'Paste/drop album from foobar2000 or release URL here';
358 child.onpaste = uaInsert;
359 if (!isNWCD) {
360 child.ondrop = uaInsert;
361 child.ondragover = clear0;
362 if (isFirefox) child.oninput = fixFirefoxDropBug;
363 } else child.ondrop = child.ondragstart = child.ondragover = function(evt) {
364 evt.preventDefault();
365 evt.stopPropagation();
366 return false;
367 };
368 var desc = document.getElementById('body');
369 if (desc != null && urlParser.test(desc.value)) {
370 child.value = RegExp.$1;
371 desc.value = '';
372 if (prefs.autfill_delay > 0) {
373 autofill = true;
374 setTimeout(fillFromText, prefs.autfill_delay);
375 };
376 }
377 elem.append(child);
378 tbl.append(elem);
379 elem = document.createElement('td');
380 elem.style.textAlign = 'center';
381}
382function common2() {
383 tbl.append(elem);
384 var tb = document.createElement('tbody');
385 tb.append(tbl);
386 tbl = document.createElement('table');
387 tbl.id = 'upload assistant';
388 tbl.append(tb);
389}
390
391if ((ref = document.getElementById('categories')) != null) {
392 ref.addEventListener('change', function(e) {
393 elem = document.getElementById('upload assistant');
394 if (elem != null) elem.style.visibility = this.value < 4
395 || ['Music', 'Applications', 'E-Books', 'Audiobooks'].includes(this.value) ? 'visible' : 'collapse';
396 setTimeout(setHandlers, 2000);
397 });
398}
399
400if ((ref = document.getElementById('upload-table') || document.querySelector('form.edit_form')
401 || document.getElementById('upload_table') || document.getElementById('request_form')) != null) {
402 ref.ondragover = voidDragHandler1;
403 ref.ondrop = voidDragHandler1;
404}
405setHandlers();
406if ((ref = isUpload ? document.getElementById('file') : null) != null) {
407 ref.oninput = function(evt) { if (evt.target.files.length > 0) validataTorrentFile(evt.target.files[0]) };
408 if (ref.files.length > 0) validataTorrentFile(ref.files[0]);
409}
410if (!isRED && (ref = document.querySelector('table#dnulist')) != null) {
411 function toggleVisibility() {
412 var show = ref.style.display.toLowerCase() == 'none';
413 ref.style.display = show ? 'block' : 'none';
414 ref.previousElementSibling.style.display = show ? 'block' : 'none';
415 }
416 toggleVisibility();
417 if ((ref = document.querySelector('h3#dnu_header')) != null) {
418 elem = ref.parentNode;
419 child = document.createElement('a');
420 child.href = '#';
421 child.onclick = function(evt) {
422 if ((ref = document.querySelector('table#dnulist')) != null) toggleVisibility();
423 };
424 child.append(ref);
425 elem.prepend(child);
426 }
427}
428
429if (isRequestNew) {
430 let title = document.querySelector('input[name="title"]');
431 if (title != null) for (i = 1; i < 6; ++i) setTimeout(function(e) { title.readOnly = false }, i * 1000);
432}
433
434Array.prototype.includesCaseless = function(str) {
435 if (typeof str != 'string') return false;
436 str = str.toLowerCase();
437 return this.find(elem => typeof elem == 'string' && elem.toLowerCase() == str) != undefined;
438};
439Array.prototype.pushUnique = function(...items) {
440 items.forEach(it => { if (!this.includes(it)) this.push(it) });
441 return this.length;
442};
443Array.prototype.pushUniqueCaseless = function(...items) {
444 items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
445 return this.length;
446};
447// Array.prototype.getUnique = function(prop) {
448// return this.every((it) => it[prop] && it[prop] == this[0][prop]) ? this[0][prop] : null;
449// };
450Array.prototype.equalTo = function(arr) {
451 return Array.isArray(arr) && arr.length == this.length
452 && Array.from(arr).sort().toString() == Array.from(this).sort().toString();
453};
454Array.prototype.equalCaselessTo = function(arr) {
455 function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem }
456 return Array.isArray(arr) && arr.length == this.length
457 && arr.map(adjust).sort().toString() == this.map(adjust).sort().toString();
458};
459Array.prototype.homogeneous = function() {
460 return this.every(elem => elem === this[0]);
461}
462Array.prototype.flatten = function() {
463 return this.reduce(function(flat, toFlatten) {
464 return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
465 }, []);
466}
467
468String.prototype.toASCII = function() {
469 return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
470};
471String.prototype.trueLength = function() {
472 return Array.from(this).length;
473 //return this.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').length;
474// var index = 0, width = 0, len = 0;
475// while (index < this.length) {
476// var point = this.codePointAt(index);
477// width = 0;
478// while (point) {
479// ++width;
480// point = point >> 8;
481// }
482// index += Math.round(width / 2);
483// ++len;
484// }
485// return len;
486};
487String.prototype.flatten = function() {
488 return this.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D');
489};
490String.prototype.expand = function() {
491 return this.replace(/\x1D/g, '\r').replace(/\x1C/g, '\n');
492};
493String.prototype.titleCase = function() {
494 return this.toLowerCase().split(' ').map(x => x[0].toUpperCase() + x.slice(1)).join(' ');
495};
496String.prototype.collapseGaps = function() {
497 return this.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/\[(\w+)\]\[\/\1\]/ig,'').trim();
498};
499Date.prototype.getDateValue = function() {
500 return Math.floor((this.getTime() / 1000 / 60 - this.getTimezoneOffset()) / 60 / 24);
501};
502Date.prototype.isExactDate = function() {
503 return this.getUTCMilliseconds() > 0 || this.getUTCSeconds() > 0 || this.getUTCMinutes() > 0 || this.getUTCHours() > 0
504 || this.getUTCDate() > 1 || this.getUTCMonth() > 0;
505};
506File.prototype.getText = function(encoding) {
507 return new Promise(function(resolve, reject) {
508 var reader = new FileReader();
509 reader.onload = function() { resolve(reader.result) };
510 reader.onerror = reader.ontimeout = error => { reject('FileReader error (' + this.name + ')') };
511 reader.readAsText(this, encoding);
512 }.bind(this));
513};
514class HTML extends String { };
515
516const excludedCountries = [
517 /\b(?:United\s+States|USA?)\b/,
518 /\b(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|UK)\b/,
519 /\b(?:Europe|European\s+Union|EU)\b/,
520 /\b(?:Unknown)\b/,
521];
522
523class TagManager extends Array {
524 constructor(...tags) {
525 super();
526 this.presubstitutions = [
527 [/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
528 [/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
529 [/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
530 [/\s*,\s*(?:&\s*|and\s+)/i, ' & '],
531 ];
532 this.substitutions = [
533 [/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
534 [/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
535 [/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
536 ['AOR', 'album.oriented.rock'],
537 [/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
538 [/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
539 [/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
540 ['World', 'world.music'],
541 [/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
542 [/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhytm.and.blues'],
543 [/\b(?:Soundtracks?)$/i, 'score'],
544 ['Electro', 'electronic'],
545 ['Metal', 'heavy.metal'],
546 ['NonFiction', 'non.fiction'],
547 ['Rap', 'hip.hop'],
548 ['NeoSoul', 'neo.soul'],
549 ['NuJazz', 'nu.jazz'],
550 [/^J[\s\-]Pop$/i, 'jpop'],
551 [/^K[\s\-]Pop$/i, 'jpop'],
552 [/^J[\s\-]Rock$/i, 'jrock'],
553 ['Hardcore', 'hardcore.punk'],
554 ['Garage', 'garage.rock'],
555 [/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
556 [/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
557 [/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
558 [/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
559 [/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
560 ['GoaTrance', 'goa.trance'],
561 [/^Female\s+Vocal\w*$/i, 'female.vocalist'],
562 ['Contemporary R&B', 'contemporary.rhytm.and.blues'],
563 // Country aliases
564 ['Canada', 'canadian'],
565 ['Australia', 'australian'],
566 ['New Zealand', 'new.zealander'],
567 ['Japan', 'japanese'],
568 ['Taiwan', 'thai'],
569 ['China', 'chinese'],
570 ['Singapore', 'singaporean'],
571 [/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
572 ['Turkey', 'turkish'],
573 ['Israel', 'israeli'],
574 ['France', 'french'],
575 ['Germany', 'german'],
576 ['Spain', 'spanish'],
577 ['Italy', 'italian'],
578 ['Sweden', 'swedish'],
579 ['Norway', 'norwegian'],
580 ['Finland', 'finnish'],
581 ['Greece', 'greek'],
582 [/^(?:Netherlands|Holland)$/i, 'dutch'],
583 ['Belgium', 'belgian'],
584 ['Luxembourg', 'luxembourgish'],
585 ['Denmark', 'danish'],
586 ['Switzerland', 'swiss'],
587 ['Austria', 'austrian'],
588 ['Portugal', 'portugese'],
589 ['Ireland', 'irish'],
590 ['Scotland', 'scotish'],
591 ['Iceland', 'icelandic'],
592 [/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
593 [/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
594 ['Hungary', 'hungarian'],
595 ['Poland', 'polish'],
596 ['Estonia', 'estonian'],
597 ['Latvia', 'latvian'],
598 ['Lithuania', 'lithuanian'],
599 ['Moldova', 'moldovan'],
600 ['Armenia', 'armenian'],
601 ['Ukraine', 'ukrainian'],
602 ['Yugoslavia', 'yugoslav'],
603 ['Serbia', 'serbian'],
604 ['Slovenia', 'slovenian'],
605 ['Croatia', 'croatian'],
606 ['Macedonia', 'macedonian'],
607 ['Montenegro', 'montenegrin'],
608 ['Romania', 'romanian'],
609 ['Malta', 'maltese'],
610 ['Brazil', 'brazilian'],
611 ['Mexico', 'mexican'],
612 ['Argentina', 'argentinean'],
613 ['Jamaica', 'jamaican'],
614 // Books
615 ['Beletrie', 'fiction'],
616 ['Satira', 'satire'],
617 ['Komiks', 'comics'],
618 ['Komix', 'comics'],
619 // Removals
620 ['Unknown'],
621 ['Other'],
622 ['New'],
623 ['Ostatni'],
624 ['Knihy'],
625 ['Audioknihy'],
626 ['dsbm'],
627 [/^(?:Audio\s*kniha|Audio\s*Book)$/i],
628 ].concat(excludedCountries.map(it => [it]));
629 this.splits = [
630 ['Alternative', 'Indie'],
631 ['Rock', 'Pop'],
632 ['Soul', 'Funk'],
633 ['Ska', 'Rocksteady'],
634 ['Jazz Fusion', 'Jazz Rock'],
635 ['Rock', 'Pop'],
636 ['Jazz', 'Funk'],
637 ];
638 this.additions = [
639 [/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
640 [/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Vocal|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Latin|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
641 [/^(?:Opera)$/i, 'classical'],
642 [/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
643 [/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
644 [/^(?:Symphony)$/i, 'classical'],
645 [/^(?:Sacred\s+Vocal)\b/i, 'classical'],
646 [/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
647 ];
648 if (tags.length > 0) this.add(...tags);
649 }
650
651 add(...tags) {
652 var added = 0;
653 for (var tag of tags) {
654 if (typeof tag != 'string') continue;
655 qobuzTranslations.forEach(function(it) { if (tag.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) tag = it[1] });
656 this.presubstitutions.forEach(k => { if (k[0].test(tag)) tag = tag.replace(k[0], k[1]) });
657 tag.split(/\s*[\,\/\;\>\|]+\s*/).forEach(function(tag) {
658 //qobuzTranslations.forEach(function(it) { if (tag == it[0]) tag = it[1] });
659 tag = tag.toASCII().replace(/\(.*?\)|\[.*?\]|\{.*?\}/g, '').trim();
660 if (tag.length <= 0 || tag == '?') return null;
661 function test(obj) {
662 return typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase()
663 || obj instanceof RegExp && obj.test(tag);
664 }
665 for (var k of this.substitutions) {
666 if (test(k[0])) {
667 if (k.length >= 1) added += this.add(...k.slice(1));
668 else addMessage('invalid tag \'' + tag + '\' found', 'warning');
669 return;
670 }
671 }
672 for (k of this.additions) {
673 if (test(k[0])) added += this.add(...k.slice(1));
674 }
675 for (k of this.splits) {
676 if (new RegExp('^' + k[0] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[1] + '$', 'i').test(tag)) {
677 added += this.add(k[0], k[1]); return;
678 }
679 if (new RegExp('^' + k[1] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[0] + '$', 'i').test(tag)) {
680 added += this.add(k[0], k[1]); return;
681 }
682 }
683 tag = tag.
684 replace(/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1').
685 replace(/\b(?:Alt\.)(?=\s+)/i, 'Alternative').
686 replace(/^[3-9]0s$/i, '19$0').
687 replace(/^[0-2]0s$/i, '20$0').
688 replace(/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2').
689 replace(/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and ').
690 replace(/[\s\-\−\—\–\_\.\,\'\`\~]+/g, '.').
691 replace(/[^\w\.]+/g, '').
692 toLowerCase();
693 if (tag.length >= 2 && !this.includes(tag)) {
694 this.push(tag);
695 ++added;
696 }
697 }.bind(this));
698 }
699 return added;
700 }
701 toString() { return Array.from(this).sort().join(', ') }
702};
703
704return;
705
706function fillFromText(evt) {
707 if (evt == undefined && !autofill) return;
708 autofill = false;
709 var overwrite = this.id == 'fill-from-text';
710 var clipBoard = document.getElementById('UA-data');
711 if (clipBoard == null) return false;
712 messages = document.getElementById('UA-messages');
713 //let promise = clientInformation.clipboard.readText().then(text => clipBoard = text);
714 //if (typeof clipBoard != 'string') return false;
715 var i, matches, sourceUrl, category = document.getElementById('categories'), xhr = new XMLHttpRequest();
716 if (category == null && document.getElementById('releasetype') != null
717 || category != null && (category.value == 0 || category.value == 'Music')) return fillFromText_Music();
718 if (category != null && (category.value == 1 || category.value == 'Applications')) return fillFromText_Apps();
719 if (category != null && (category.value == 2 || category.value == 3
720 || category.value == 'E-Books' || category.value == 'Audiobooks')) return fillFromText_Ebooks();
721 return category == null ? fillFromText_Apps(true).catch(reason => fillFromText_Ebooks()) : Promise.reject('no category');
722
723 function fillFromText_Music() {
724 if (messages != null) messages.parentNode.removeChild(messages);
725 const divs = ['—', '⸺', '⸻'];
726 const vaParser = /^(?:Various(?:\s+Artists)?|VA|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
727 const VA = 'Various Artists';
728 const multiArtistParsers = [
729 /\s*[,;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?\s*/,
730 /\s+[\/\|\×]\s+/,
731 ];
732 const pseudoArtistParsers = [
733 /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
734 /^(?:traditional|lidová)$/i,
735 /\b(?:traditional|lidová)$/,
736 /^(?:tradiční|lidová)\s+/,
737 /^(?:[Aa]nonym)/,
738 /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
739 /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
740 /^(?:Various\s+Composers)$/i,
741 /^(?:Guests|Friends)$/i,
742 ];
743 const ampersandParsers = [
744 /\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
745 /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
746 /\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
747 /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
748 ];
749 const featParsers = [
750 /\s+(?:meets)\s+(.*?)\s*$/i,
751 /\s+(?:[Ww]ith)\s+(?!his\b|her\b|Friends$|Strings$)(.*?)\s*$/,
752 /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:eaturing|t\.))\s+(.*?)\s*$/,
753 /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:ea)?t\.)\s+(.*?)\s*$/, // [0]
754 /\s+\[\s*f(?:eat(?:\.|uring)|t\.)\s+([^\[\]]+?)\s*\]/i, // [1]
755 /\s+\(\s*f(?:eat(?:\.|uring)|t\.)\s+([^\(\)]+?)\s*\)/i, // [2]
756 /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i, // [3]
757 /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i, // [4]
758 /\s+\[\s*with\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/, // [5]
759 /\s+\(\s*with\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/, // [6]
760 ];
761 const remixParsers = [
762 /\s+\((?:The\s+)Remix(?:e[sd])?\)/i,
763 /\s+\[(?:The\s+)Remix(?:e[sd])?\]/i,
764 /\s+(?:The\s+)Remix(?:e[sd])?\s*$/i,
765 /^(Remixes)\b/,
766 /\s+\(([^\(\)]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\)/i,
767 /\s+\[([^\[\]]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\]/i,
768 /\s+\(\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
769 /\s+\[\s*(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
770 ];
771 const otherArtistsParsers = [
772 [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
773 [/^()(.*?)\s+\(conductor\)$/i, 4],
774 //[/^()(.*?)\s+\(.*\)$/i, 1],
775 ];
776 const artistStrips = [
777 /\s+(?:aka|AKA)\.?\s+(.*)$/,
778 /\s+\(([^\(\)]+)\)$/,
779 /\s+\[([^\[\]]+)\]$/,
780 /\s+\{([^\{\}]+)\}$/,
781 ];
782 var isVA, onlineSource = urlParser.test(clipBoard.value) && RegExp.$1, ajaxRejects = 0;
783 return (onlineSource ? urlResolver(onlineSource).then(fetchOnline_Music) :
784 Promise.resolve(clipBoard.value.split(/(?:\r?\n)+/).filter(line => line.trim().length > 0).map(function(line, ndx) {
785 const fields = [
786 /* 00 */ 'artist', 'album', 'album_year', 'release_date', 'label', 'catalog', 'country', 'encoding',
787 /* 08 */ 'codec', 'codec_profile', 'bitrate', 'bd', 'sr', 'channels', 'media', 'genre', 'discnumber',
788 /* 17 */ 'totaldiscs', 'discsubtitle', 'tracknumber', 'totaltracks', 'title', 'track_artist', 'performer',
789 /* 24 */ 'composer', 'conductor', 'remixer', 'compiler', 'producer', 'duration', 'samples', 'filesize',
790 /* 32 */ 'ag', 'dr', 'vendor', 'url', 'dirpath', 'description', 'identifiers',
791 ];
792 var metaData = line.expand().split('\x1E'), track = { identifiers: {} }, identifiers = [];
793 if (metaData.length < fields.length) {
794 console.error('invalid data format for track #' + (ndx + 1) + ': length:', metaData.length,
795 '(' + fields.length + '); metaData:', metaData, '; line:', line);
796 throw 'invalid data format for track #' + (ndx + 1) + ' (see console log for details)';
797 } else if (metaData.length > fields.length) {
798 console.warn('unexpected data format for track #' + (ndx + 1) + ': length:', metaData.length,
799 '(' + fields.length + '); metaData:', metaData, '; line:', line);
800 }
801 fields.forEach(function(propName) {
802 if (propName == 'identifiers') {
803 metaData.shift().trim().split(/\s+/).forEach(function(id) {
804 if (/^([\w\-]+)[=:](\S*)$/.test(id)) track.identifiers[RegExp.$1.toUpperCase()] = RegExp.$2.replace(/\x1B/g, ' ');
805 });
806 } else {
807 track[propName] = metaData.shift();
808 if (track[propName] === '') track[propName] = undefined;
809 }
810 });
811 if (prefs.check_whitespace) Object.keys(track).forEach(function(propName) {
812 if (typeof track[propName] != 'string') return;
813 if (propName != 'description' && (track[propName].includes('\r') || track[propName].includes('\n'))) {
814 track[propName] = track[propName].replace(/[\r\n]+/g, '');
815 addMessage('track #' + (ndx + 1) + ' contains linebreaks in tag <' + propName + '>', 'warning');
816 }
817 if ((i = propName == 'description' ? /[\x00-\x08\x0B\x0C\x0E-\x19]+/g : /[\x00-\x19]+/g).test(track[propName])) {
818 track[propName] = track[propName].replace(i, '');
819 addMessage('track #' + (ndx + 1) + ' contains control codes in tag <' + propName + '>', 'warning');
820 }
821 if (/^[\s\xA0]+$/.test(track[propName])) {
822 track[propName] = undefined;
823 addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains only whitespace', 'warning');
824 } else if (/^[\s\xA0]+|[\s\xA0]+$/.test(track[propName])) {
825 track[propName] = track[propName].trim();
826 addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains leading/trailing whitespace', 'warning');
827 }
828 if (/[ \xA0]{2,}/.test(track[propName])) {
829 track[propName] = track[propName].replace(/[ \xA0]{2,}/g, ' ')
830 addMessage('track #' + (ndx + 1) + ' in tag <' + propName + '> contains multiple spaces', 'warning');
831 }
832 });
833 if (track.description == '.') track.description = undefined; else if (track.description) {
834 if (prefs.remap_texttools_newlines)
835 track.description = track.description.replace(/__/g, '\r\n').replace(/_/g, '\n') // ambiguous
836 track.description = track.description.collapseGaps();
837 }
838 ['bitrate', 'bd', 'sr', 'channels', 'totaldiscs', 'totaltracks', 'samples', 'filesize', 'dr'].forEach(function(propName) {
839 if (track[propName] !== undefined) track[propName] = parseInt(track[propName]);
840 });
841 ['duration'].forEach(function(propName) {
842 if (!isNaN(track[propName]) && track[propName] !== null) track[propName] = parseFloat(track[propName]);
843 });
844 if (track.album_year) track.album_year = extractYear(track.album_year) || NaN;
845 ['ag', 'tg', 'ap', 'tp'].forEach(function(propName) {
846 track[propName] = /^([\+\-]?\d+(?:\.\d+)?)\s*dB\b/i.test(track[propName]) ? parseFloat(RegExp.$1) : undefined;
847 });
848 return track;
849 }))
850 ).then(parseTracks).catch(e => { if (e) addMessage(e, 'critical') });
851
852 function parseTracks(tracks) {
853 if (tracks.length <= 0) {
854 clipBoard.value = '';
855 throw 'no tracks found';
856 }
857 if (prefs.diag_mode) console.debug('Parsing tracks:', tracks);
858 var albumBitrate = 0, totalTime = 0, albumSize = 0, media, release = { totaldiscs: 1, srs: [] };
859 var allowedFormats = Array.from(document.querySelectorAll('select#format > option'))
860 .filter(option => option.value.length > 0).map(option => option.value);
861 if (allowedFormats.length <= 0) allowedFormats = ["MP3", "FLAC", "AAC", "AC3", "DTS"];
862 tracks.forEach(function(track) {
863 processTrackArtists(track);
864 if (!track.artist) {
865 clipBoard.value = '';
866 throw new HTML('main artist must be defined for every track' + ruleLink('2.3.16.4'));
867 }
868 if (!track.album) {
869 clipBoard.value = '';
870 throw new HTML('album title must be defined for every track' + ruleLink('2.3.16.4'));
871 }
872 if (!track.tracknumber) {
873 clipBoard.value = '';
874 throw new HTML('all track numbers must be defined' + ruleLink('2.3.16.4'));
875 }
876 if (!track.title) {
877 clipBoard.value = '';
878 throw new HTML('all track titles must be defined' + ruleLink('2.3.16.4'));
879 }
880 if (track.duration !== undefined && track.duration !== null && isUpload && (isNaN(track.duration) || track.duration <= 0)) {
881 clipBoard.value = '';
882 throw 'invalid track #' + track.tracknumber + ' length: ' + track.duration;
883 }
884 if (!onlineSource && track.codec && !allowedFormats.includes(track.codec)) {
885 clipBoard.value = '';
886 throw 'disallowed codec present (' + track.codec + ')';
887 }
888 if (!onlineSource && /\b(?:MQAEncode) v(\d+(?:\.\d+)*)\b/.test(track.vendor)) {
889 clipBoard.value = '';
890 throw 'MQA encoded release (' + RegExp.lastMatch + ')';
891 }
892 if (/^(?:#?N\/A|No\s+Label|Not\s+On\s+Label|\[no\s+label\])$/i.test(track.label)) track.label = undefined;
893 if (/^(?:#?N\/A|none)$/i.test(track.catalog)) track.catalog = undefined;
894 if (/^(\d+)\s*[\/]\s*(\d+)$/.test(track.tracknumber)) { // track/totaltracks
895 addMessage('nonstandard track number formatting for track ' + RegExp.$1 + ': ' + track.tracknumber, 'warning');
896 track.tracknumber = RegExp.$1;
897 if (!track.totaltracks) track.totaltracks = parseInt(RegExp.$2);
898 } else if (/^(\d+)[\.\-](\d+)$/.test(track.tracknumber)) { // discnumber.tracknumber
899 addMessage('nonstandard track number formatting for track ' + RegExp.$2 + ': ' + track.tracknumber, 'warning');
900 if (!track.discnumber) track.discnumber = parseInt(RegExp.$1);
901 track.tracknumber = RegExp.$2;
902 }
903 if (track.discnumber) {
904 if (/^(\d+)\s*\/\s*(\d+)/.test(track.discnumber)) {
905 addMessage('nonstandard disc number formatting for track ' + track.tracknumber + ': ' + track.discnumber, 'warning');
906 track.discnumber = RegExp.$1;
907 if (!track.totaldiscs) track.totaldiscs = RegExp.$2;
908 } else track.discnumber = parseInt(track.discnumber);
909 if (isNaN(track.discnumber)) {
910 addMessage('invalid disc numbering for track ' + track.tracknumber, 'warning');
911 track.discnumber = undefined;
912 }
913 if (track.discnumber > release.totaldiscs) release.totaldiscs = track.discnumber;
914 }
915 totalTime += track.duration;
916 albumBitrate += track.bitrate * track.duration;
917 albumSize += track.filesize;
918 if (!onlineSource && track.bitrate > 0) {
919 let minBitrate = [24, 12];
920 switch (track.codec) {
921 case 'FLAC':
922 if (track.sr > 0 && track.bd > 0) minBitrate = [
923 Math.round(Math.max(track.sr * track.bd / 1400, 192)),
924 Math.round(Math.max(track.sr * track.bd / 2000, 192)),
925 ];
926 break;
927 case 'MP3':
928 switch (track.codec_profile) {
929 case 'VBR V0': minBitrate = [192, 96]; break;
930 case 'VBR V1': minBitrate = [160, 80]; break;
931 case 'VBR V2': minBitrate = [128, 64]; break;
932 }
933 break;
934 case 'AAC':
935 if (/\b(?:TVBR)\sq(\d+)\b/.test(track.vendor)) minBitrate = [
936 Math.round(Math.max(parseInt(RegExp.$1) * 1.9, 192)),
937 Math.round(Math.max(parseInt(RegExp.$1) * 1.4, 192)),
938 ]; else if (/\b(?:(?:CV|A|C)BR)\s(\d+)kbps\b/.test(track.vendor)) minBitrate = [
939 Math.round(Math.max(parseInt(RegExp.$1) * 0.75, 192)),
940 Math.round(Math.max(parseInt(RegExp.$1) * 0.4, 192)),
941 ];
942 break;
943 }
944 if (minBitrate && track.bitrate < minBitrate[0]) addMessage('track #' + track.tracknumber +
945 ' suspiciously low bitrate (' + track.bitrate + ' kbps)', track.bitrate < minBitrate[1] ? 'warning' : 'notice');
946 }
947 });
948 if (!tracks.every(track => track.discnumber > 0) && !tracks.every(track => !track.discnumber)) {
949 addMessage('inconsistent release (mix of tracks with and without disc number)', 'warning');
950 }
951 if (!onlineSource && release.totaldiscs > 1 && tracks.some(it => it.totaldiscs != release.totaldiscs))
952 addMessage('at least one track not having properly set TOTALDISCS (' + release.totaldiscs + ')', 'info');
953
954 function setUniqueProperty(propName, propNameLiteral) {
955 let homogeneous = new Set(tracks.map(it => it[propName]).filter(it => it != undefined && it != null));
956 if (homogeneous.size > 1) {
957 var diverses = '', it = homogeneous.values(), val;
958 while (!(val = it.next()).done) diverses += '<br>\t' + val.value;
959 clipBoard.value = '';
960 throw new HTML('mixed releases not accepted (' + propNameLiteral + ') - supposedly user compilation' + diverses);
961 }
962 release[propName] = homogeneous.values().next().value;
963 }
964 setUniqueProperty('artist', 'album artist');
965 ['artists', 'featuring_artists', 'composers', 'conductors', 'performers', 'compilers', 'remixers', 'producers'].forEach(function(role) {
966 if (tracks.every(track => Array.isArray(track[role]) && track[role].equalTo(tracks[0][role]))) release[role] = tracks[0][role];
967 });
968 setUniqueProperty('album', 'album title');
969 setUniqueProperty('album_year', 'album year');
970 setUniqueProperty('release_date', 'release date');
971 setUniqueProperty('encoding', 'encoding');
972 setUniqueProperty('codec', 'codec');
973 setUniqueProperty('codec_profile', 'codec profile');
974 setUniqueProperty('vendor', 'vendor');
975 setUniqueProperty('media', 'media');
976 setUniqueProperty('channels', 'channels');
977 setUniqueProperty('label', 'label');
978 setUniqueProperty('country', 'country');
979
980 tracks.forEach(function(iter) {
981 setProperty('trackArtists', 'track_artist');
982 setProperty('totalTracks', 'totaltracks');
983 setProperty('discSubtitles', 'discsubtitle');
984 setProperty('composers', 'composer');
985 setProperty('catalogs', 'catalog');
986 setProperty('bitrates', 'bitrate');
987 setProperty('bds', 'bd');
988 setProperty('ags', 'ag');
989 setProperty('drs', 'dr');
990 if (iter.sr) if (typeof release.srs[iter.sr] != 'number') release.srs[iter.sr] = iter.duration;
991 else release.srs[iter.sr] += iter.duration;
992 setProperty('dirpaths', 'dirpath');
993 setProperty('descriptions', 'description');
994 setProperty('genres', 'genre');
995 setProperty('urls', 'url');
996 setProperty('coverUrls', 'cover_url');
997
998 function setProperty(propName, trackProp) {
999 if (!Array.isArray(release[propName])) release[propName] = [];
1000 if (iter[trackProp] !== undefined && iter[trackProp] !== null && (typeof iter[trackProp] != 'string'
1001 || iter[trackProp].length > 0) && !release[propName].includes(iter[trackProp])) {
1002 release[propName].push(iter[trackProp]);
1003 }
1004 }
1005 });
1006 if (!release.totalTracks) addMessage('total tracks not set', 'warning');
1007 if (release.totalTracks.length > 0) {
1008 if (release.totalTracks.length > 1) {
1009 addMessage('total tracks not consistent across release: ' + release.totalTracks, 'warning');
1010 } else if (release.totalTracks[0] != tracks.length) {
1011 addMessage('total tracks not matching tracklist length: ' +
1012 release.totalTracks[0] + ' != ' + tracks.length, 'warning');
1013 }
1014 }
1015 tracks.forEach(function(track1, ndx1) {
1016 if (tracks.some((track2, ndx2) => ndx2 < ndx1 && track1.tracknumber == track2.tracknumber
1017 && track1.discnumber == track2.discnumber && track1.discsubtitle == track2.discsubtitle)) {
1018 addMessage('duplicate track ' + (track1.discnumber ? track1.discnumber + '-' : '') +
1019 (track1.discsubtitle ? track1.discsubtitle + '-' : '') + track1.tracknumber, 'warning');
1020 }
1021 });
1022 if (!onlineSource) {
1023 function validatorFunc(arr, validator, str) {
1024 if (arr.length <= 0 || !arr.some(validator)) return true;
1025 clipBoard.value = '';
1026 throw 'disallowed ' + str + ' present (' + arr.filter(validator) + ')';
1027 }
1028 validatorFunc(release.bds, bd => ![16, 24].includes(bd), 'bit depths');
1029 validatorFunc(Object.keys(release.srs),
1030 sr => sr < 44100 || sr > 192000 || sr % 44100 != 0 && sr % 48000 != 0, 'sample rates');
1031 if (prefs.honour_rg && tracks.some(track => track.ag === undefined))
1032 addMessage('at least one track is missing RG info', 'notice');
1033 if (release.ags.length > 1) addMessage('album RG differs across the release', 'notice');
1034 if (prefs.honour_dr && tracks.some(track => track.bd > 16 && track.dr === undefined))
1035 addMessage('at least one high resolution track is missing DR info', 'notice');
1036 release.urls.forEach(function(url) {
1037 if (/^https?:\/\/(\w+\.)*7digital\.com\/.*\?f=/i.test(url))
1038 addMessage('session id present in online source URL: ' + url, 'notice');
1039 });
1040 }
1041 if (elementWritable(document.getElementById('image') || document.querySelector('input[name="image"]'))) {
1042 (release.coverUrls.length > 0 ? setCover(release.coverUrls[0]) : Promise.reject('No cover URL'))
1043 .catch(getCoverOnline).catch(searchCoverOnline);
1044 }
1045 var albumBPM = Math.round(tracks.reduce(function(acc, track) {
1046 return acc + parseInt(track.identifiers.BPM) * track.duration;
1047 }, 0) / totalTime);
1048 var composerEmphasis = tracks.some(track => track.identifiers.COMPOSEREMPHASIS);
1049 var isFromDSD = false, isClassical = false;
1050 var canSort = tracks.every((tr1, ndx1) => tracks.every((tr2, ndx2) => ndx1 == ndx2
1051 || tr1.tracknumber != tr2.tracknumber || tr1.discnumber != tr2.discnumber));
1052 var yadg_prefil = '', releaseType, editionTitle, iter, rx;
1053 var barCode = getHomoIdentifier('BARCODE');
1054 if (barCode) barCode = parseInt(barCode.replace(/\s+/g, ''));
1055 if (!Number.isInteger(barCode)) {
1056 if (release.catalogs.length == 1) barCode = parseInt(release.catalogs[0].replace(/[\s\-]/g, ''));
1057 if (!Number.isInteger(barCode) || barCode < 10**10) barCode = undefined;
1058 }
1059 var tags = new TagManager();
1060 albumBitrate /= totalTime;
1061 const isCompilation = tracks.every(track => track.identifiers.COMPILATION == 1
1062 || /^(?:Compilation)$/i.test(track.identifiers.RELEASETYPE));
1063 if (tracks.every(track => /^(?:Single)$/i.test(track.identifiers.RELEASETYPE))
1064 || tracks.length == 1 && totalTime > 0 && totalTime < prefs.single_threshold) {
1065 releaseType = getReleaseIndex('Single');
1066 } else if (tracks.every(it => it.identifiers.RELEASETYPE == 'EP')) {
1067 releaseType = getReleaseIndex('EP');
1068 } else if (tracks.every(it => /^soundtrack$/i.test(it.identifiers.RELEASETYPE))) {
1069 releaseType = getReleaseIndex('Soundtrack');
1070 tags.add('score');
1071 composerEmphasis = true;
1072 }
1073 if (release.genres.length > 0) {
1074 const classicalGenreParsers = [
1075 /\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,
1076 ];
1077 release.genres.forEach(function(genre) {
1078 classicalGenreParsers.forEach(function(classicalGenreParser) {
1079 if (classicalGenreParser.test(genre) && !/\b(?:metal|rock|pop)\b/i.test(genre)) {
1080 composerEmphasis = true;
1081 isClassical = true
1082 }
1083 });
1084 if (/\b(?:Jazz|Vocal)\b/i.test(genre) && !/\b(?:Nu|Future|Acid)[\s\-\−\—\–]*Jazz\b/i.test(genre)
1085 && !/\bElectr(?:o|ic)[\s\-\−\—\–]?Swing\b/i.test(genre)) {
1086 composerEmphasis = true;
1087 }
1088 if (/\b(?:Soundtracks?|Score|Films?|Games?|Video|Series?|Theatre|Musical)\b/i.test(genre)) {
1089 if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
1090 composerEmphasis = true;
1091 }
1092 if (/\b(?:Christmas\s+Music)\b/i.test(genre)) {
1093 composerEmphasis = true;
1094 }
1095 tags.add(...genre.split(/\s*\|\s*/));
1096 });
1097 if (release.genres.length > 1) addMessage('inconsistent genre accross album: ' + release.genres.join(' / '), 'warning');
1098 }
1099 if (!onlineSource && isClassical && !tracks.every(track => track.composer)) {
1100 addMessage(new HTML('all tracks composers must be set for clasical music' + ruleLink('2.3.17')), 'warning');
1101 //return false;
1102 }
1103 // Processing artists: recognition, splitting and dividing to categores
1104 const roleCollisions = [
1105 [4, 5], // main
1106 [0, 4], // guest
1107 [], // remixer
1108 [], // composer
1109 [], // conductor
1110 [], // DJ/compiler
1111 [], // producer
1112 ];
1113 isVA = vaParser.test(release.artist);
1114 var artists = [], albumGuests = [];
1115 for (i = 0; i < 7; ++i) artists[i] = [];
1116
1117 if (!isVA) {
1118 if (Array.isArray(release.artists) && release.artists.length > 0) {
1119 artists[0] = release.artists.filter(exclusions);
1120 if (Array.isArray(release.featuring_artists)) albumGuests = release.featuring_artists;
1121 yadg_prefil = joinArtists(artists[0]);
1122 } else {
1123 yadg_prefil = spliceGuests(release.artist);
1124 addArtists(0, yadg_prefil);
1125 artists[0] = artists[0].filter(exclusions);
1126 albumGuests = Array.from(artists[1]);
1127 }
1128 if (ampersandParsers.some(rx => rx.test(yadg_prefil))) getSiteArtist(yadg_prefil); // priority cache record
1129
1130 function exclusions(artist) {
1131 return !['conductors', 'compilers']
1132 .some(category => Array.isArray(release[category]) && release[category].includesCaseless(artist));
1133 }
1134 }
1135
1136 featParsers.slice(3).forEach(function(rx, ndx) {
1137 if ((matches = rx.exec(release.album)) == null) return;
1138 if (ndx >= 5 && !splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)))
1139 .every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
1140 addArtists(1, matches[1]);
1141 artists[0].forEach(guest => { if (albumGuests.includesCaseless(guest)) albumGuests.push(guest) });
1142 addMessage('featured artist(s) in album title (' + release.album + ')', 'warning');
1143 release.album = release.album.replace(rx, '');
1144 });
1145 remixParsers.slice(4).forEach(function(rx) {
1146 if (rx.test(release.album)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
1147 })
1148 if (((matches = /^(.*?)\s+Presents\s+(.*)$/.exec(release.album)) != null
1149 || isVA && (matches = (/\s+\(compiled\s+by\s+(.*?)\)\s*$/i.exec(release.album)
1150 || /\s+compiled\s+by\s+(.*?)\s*$/i.exec(release.album))) != null) && looksLikeTrueName(matches[1])) {
1151 addArtists(5, matches[1]);
1152 if (!releaseType) releaseType = getReleaseIndex('Compilation');
1153 }
1154
1155 for (iter of tracks) {
1156 ['track_artist', 'performer', 'track_guest'].forEach(function(role) {
1157 var arrayName = role.concat('s');
1158 addTrackPerformers(Array.isArray(iter[arrayName]) && iter[arrayName].length > 0 ? iter[arrayName] : iter[role]);
1159 });
1160 [
1161 [2, 'remixer'],
1162 [3, 'composer'],
1163 [4, 'conductor'],
1164 [5, 'compiler'],
1165 [6, 'producer'],
1166 ].forEach(function(role) {
1167 var arrayName = role[1].concat('s');
1168 addArtists(role[0], Array.isArray(iter[arrayName]) && iter[arrayName].length > 0 ? iter[arrayName] : iter[role[1]]);
1169 });
1170
1171 if (iter.title) {
1172 featParsers.slice(3).forEach(function(rx, ndx) {
1173 if ((matches = rx.exec(iter.title)) == null) return;
1174 var featArtists = splitArtists(matches[1], multiArtistParsers.concat(ampersandParsers.slice(1)));
1175 if (ndx >= 5 && !featArtists.every((artist, ndx) => looksLikeTrueName(artist, 1))) return;
1176 if (Array.isArray(iter.track_artists) && iter.track_artists.length > 0) {
1177 if (!Array.isArray(iter.track_guests)) iter.track_guests = [];
1178 featArtists.forEach(function(featArtist) {
1179 if (!iter.track_artists.includesCaseless(featArtist) && !iter.track_guests.includesCaseless(featArtist))
1180 track_guests.push(featArtist);
1181 });
1182 if (!isVA && iter.track_artists.equalCaselessTo(release.artists)
1183 && iter.track_guests.equalCaselessTo(release.featuring_artists)) {
1184 iter.track_artists = iter.track_guests = iter.track_artist = undefined;
1185 } else iter.track_artist = joinArtists(iter.track_artists).concat(' feat. ', joinArtists(iter.track_guests));
1186 } else {
1187 useTA = iter.track_artist && !featArtists.some(featArtist => iter.track_artist.includes(featArtist)
1188 || Array.isArray(iter.track_artists) && iter.track_artists.includes(featArtist)
1189 || Array.isArray(iter.track_guests) && iter.track_guests.includes(featArtist));
1190 iter.track_artist = iter[useTA ? 'track_artist' : 'artist'].concat(' feat. ', matches[1]);
1191 }
1192 addArtists(1, matches[1]);
1193 addMessage('featured artist(s) in track title (#' + iter.tracknumber + ': ' + iter.title + ')', 'warning');
1194 iter.title = iter.title.replace(rx, '');
1195 });
1196 if (!iter.remixer) remixParsers.slice(4).forEach(function(rx) {
1197 if (rx.test(iter.title)) addArtists(2, RegExp.$1.replace(/\b\d{4}\b/g, '').replace(/\s{2,}/g, ' ').trim());
1198 });
1199 }
1200 if (isClassical && !iter.composer && /^([^\(\)\[\]\{\},:]+?)(?:\s*\((?:\d{4}\s*-|b\.)\s*\d{4}\))/.test(iter.discsubtitle)) {
1201 //track.composer = RegExp.$1;
1202 addArtists(3, RegExp.$1);
1203 }
1204 }
1205 for (i = 0; i < Math.round(tracks.length / 2); ++i) splitAmpersands();
1206 albumGuests = splitAmpersands(albumGuests);
1207
1208 function addArtists(ndx, _artists) {
1209 (typeof _artists == 'string' ? splitArtists(_artists) : Array.isArray(_artists) ? _artists : []).forEach(function(artist) {
1210 artist = ndx != 0 ? strip(artist) : guessOtherArtists(artist);
1211 if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
1212 && !artists[ndx].includesCaseless(artist)
1213 && !roleCollisions[ndx].some(n => artists[n].includesCaseless(artist))) artists[ndx].push(artist);
1214 });
1215 }
1216 function addTrackPerformers(_artists) {
1217 (typeof _artists == 'string' ? splitArtists(spliceGuests(_artists, 1)) : Array.isArray(_artists) ? _artists : []).forEach(function(artist) {
1218 artist = guessOtherArtists(artist);
1219 if (artist.length > 0 && !pseudoArtistParsers.some(rx => rx.test(artist))
1220 && !artists[0].includesCaseless(artist)
1221 && (isVA || !artists[1].includesCaseless(artist))) artists[isVA ? 0 : 1].push(artist);
1222 });
1223 }
1224 function splitAmpersands(_artists) {
1225 if (_artists) {
1226 let result;
1227 if (typeof _artists == 'string') result = splitArtists(_artists);
1228 else if (Array.isArray(_artists)) result = Array.from(_artists);
1229 else return [];
1230 splitInternal(result);
1231 return result;
1232 }
1233 for (let ndx = 0; ndx < artists.length; ++ndx) splitInternal(artists[ndx], roleCollisions[ndx]);
1234
1235 function splitInternal(refArr, roleCollisions) {
1236 ampersandParsers.forEach(function(ampersandParser) {
1237 for (var i = refArr.length; i > 0; --i) {
1238 var j = refArr[i - 1].split(ampersandParser).map(strip);
1239 if (j.length <= 1 || !j.some(it1 => artists.some(it2 => it2.includesCaseless(it1)))
1240 && !j.every(looksLikeTrueName) || getSiteArtist(refArr[i - 1])) continue;
1241 refArr.splice(i - 1, 1, ...j.filter(function(artist) {
1242 return !refArr.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist))
1243 && (!Array.isArray(roleCollisions) || !roleCollisions.some(n => artists[n].includesCaseless(artist)));
1244 }));
1245 }
1246 });
1247 }
1248 }
1249 function spliceGuests(str, level = 1) {
1250 (level > 0 ? featParsers.slice(level) : featParsers).forEach(function(rx, ndx) {
1251 var matches = rx.exec(str);
1252 if (matches != null && (level + ndx < 8
1253 || splitArtists(matches[1]).every((artist, ndx) => looksLikeTrueName(artist, 1)))) {
1254 addArtists(1, matches[1]);
1255 str = str.replace(rx, '');
1256 }
1257 });
1258 return str;
1259 }
1260 function guessOtherArtists(name) {
1261 otherArtistsParsers.forEach(function(it) {
1262 if (!it[0].test(name)) return;
1263 addArtists(it[1], RegExp.$2);
1264 name = RegExp.$1;
1265 });
1266 return strip(name);
1267 }
1268 function getArtists(trackArtist) {
1269 if (!trackArtist || typeof trackArtist != 'string') trackArtist = '';
1270 otherArtistsParsers.forEach(it => { if (it[0].test(trackArtist)) trackArtist = RegExp.$1 });
1271 var result = [ [], [] ];
1272 featParsers.slice(1).forEach(function(rx, ndx) {
1273 if ((matches = rx.exec(trackArtist)) == null || ndx >= 7 && !looksLikeTrueName(matches[1], 1)) return;
1274 splitAmpersands(matches[1]).forEach(artist => { result[1].pushUniqueCaseless(artist) });
1275 trackArtist = trackArtist.replace(rx, '');
1276 });
1277 splitAmpersands(trackArtist).forEach(artist => { result[0].pushUniqueCaseless(artist) });
1278 return result;
1279 }
1280 function realTrackArtist(trackArtist) {
1281 var result, trackArtists = getArtists(trackArtist);
1282 if (trackArtists[0].length > 0 && (!trackArtists[0].equalCaselessTo(artists[0]) || !trackArtists[1].equalCaselessTo(albumGuests)))
1283 if (prefs.reformat_trackartist) {
1284 result = joinArtists(trackArtists[0]);
1285 if (trackArtists[1].length > 0) result += ' feat. '.concat(joinArtists(trackArtists[1]));
1286 } else result = trackArtist;
1287 return result;
1288 }
1289
1290 if (elementWritable(document.getElementById('artist') || document.getElementById('artist_0'))) {
1291 let artistIndex = 0;
1292 const enSorter = /^(?:The)\s+/;
1293 catLoop: for (i = 0; i < artists.length; ++i) for (iter of artists[i]
1294 .filter(artist => !roleCollisions[i].some(n => artists[n].includesCaseless(artist)))
1295 .sort((a, b) => a.replace(enSorter, '').localeCompare(b.replace(enSorter, '')))) {
1296 if (isUpload) {
1297 var id = 'artist';
1298 if (artistIndex > 0) id += '_' + artistIndex;
1299 while ((ref = document.getElementById(id)) == null) AddArtistField();
1300 } else {
1301 while ((ref = document.querySelectorAll('input[name="artists[]"]')).length <= artistIndex) AddArtistField();
1302 ref = ref[artistIndex];
1303 }
1304 if (ref == null) throw new Error('Failed to allocate artist fields');
1305 ref.value = iter;
1306 ref.nextElementSibling.value = i + 1;
1307 if (++artistIndex >= 200) break catLoop;
1308 }
1309 if (overwrite && artistIndex > 0) while (document.getElementById('artist_' + artistIndex) != null) {
1310 RemoveArtistField();
1311 }
1312 }
1313
1314 // Processing album title
1315 const mediaParsers = [
1316 [/\s+(?:\[(?:LP|Vinyl|12"|7")\]|\((?:LP|Vinyl|12"|7")\))$/, 'Vinyl'],
1317 [/\s+(?:\[SA-?CD\]|\(SA-?CD\))$/, 'SACD'],
1318 [/\s+(?:\[(?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\]|\((?:Blu[\s\-\−\—\–]?Ray|BD|BRD?)\))$/, isOPS ? 'BD' : 'Blu-Ray'],
1319 [/\s+(?:\[DVD(?:-?A)?\]|\(DVD(?:-?A)?\))$/, 'DVD'],
1320 ];
1321 const releaseTypeParsers = [
1322 [/\s+(?:-\s+Single|\[Single\]|\(Single\))$/i, 'Single', true, true],
1323 [/\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/, 'EP', true, true],
1324 [/\s+\((?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\(\)]*\)$/i, 'Live album', false, false],
1325 [/\s+\[(?:Live|En\s+directo?|(?:Ao|En)\s+Vivo)\b[^\[\]]*\]$/i, 'Live album', false, false],
1326 [/(?:^Live\s+(?:[aA]t|[Ii]n)\b|^Directo?\s+[Ee]n\b|\bUnplugged\b|\bAcoustic\s+Stage\b|\s+Live$)/, 'Live album', false, false],
1327 [/\b(?:(?:Best\s+of|Greatest\s+Hits|Complete\s+(.+?\s+)(?:Albums|Recordings))\b|Collection$)|^The(\s+\w+)+Years$/i, 'Anthology', false, false],
1328 ];
1329 const editionParsers = [
1330 /\s+\(((?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissu(?:ed)?|Deluxe|Enhanced|Expanded|Limited|Version|\d+th\s+Anniversary)\b[^\(\)]*|[^\(\)]*\b(?:Edition|Version|Promo|Release|Édition|Reissue))\)$/i,
1331 /\s+\[((?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissu(?:ed)?|Deluxe|Enhanced|Expanded|Limited|Version|\d+th\s+Anniversary)\b[^\[\]]*|[^\[\]]*\b(?:Edition|Version|Promo|Release|Édition|Reissue))\]$/i,
1332 /\s+-\s+([^\[\]\(\)\-\−\—\–]*\b(?:(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Bonus\s+Track)\b[^\[\]\(\)\-\−\—\–]*|Reissue|Edition|Version|Promo|Enhanced|Release|Édition))$/i,
1333 ];
1334 var album = release.album;
1335 releaseTypeParsers.forEach(function(it) {
1336 if (it[0].test(album)) {
1337 if (it[2] || !releaseType) releaseType = getReleaseIndex(it[1]);
1338 if (it[3]) album = album.replace(it[0], '');
1339 }
1340 });
1341 rx = '\\b(?:Soundtrack|Score|Motion\\s+Picture|Series|Television|Original(?:\\s+\\w+)?\\s+Cast|Music\\s+from|(?:Musique|Bande)\\s+originale)\\b';
1342 if (reInParenthesis(rx).test(album) || reInBrackets(rx).test(album)) {
1343 if (!releaseType) releaseType = getReleaseIndex('Soundtrack');
1344 tags.add('score');
1345 composerEmphasis = true;
1346 }
1347 remixParsers.forEach(function(rx) {
1348 if (rx.test(album) && !releaseType) releaseType = getReleaseIndex('Remix');
1349 });
1350 editionParsers.forEach(function(rx) {
1351 if (rx.test(album) && (!RegExp.$1.toLowerCase().startsWith('remaster') || !release.album_year
1352 || release.album_year != extractYear(release.release_date)) && !isRequestNew && !isRequestEdit) {
1353 album = album.replace(rx, '');
1354 editionTitle = RegExp.$1;
1355 }
1356 });
1357 mediaParsers.forEach(function(it) {
1358 if (it[0].test(album)) {
1359 album = album.replace(it[0], '');
1360 media = it[1];
1361 }
1362 });
1363 if (elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
1364 ref.value = album;
1365 }
1366
1367 if (yadg_prefil) yadg_prefil += ' ';
1368 yadg_prefil += album;
1369 if (elementWritable(ref = document.getElementById('yadg_input'))) {
1370 ref.value = yadg_prefil || '';
1371 if (yadg_prefil && (ref = document.getElementById('yadg_submit')) != null && !ref.disabled) ref.click();
1372 }
1373
1374 if (!release.album_year) release.album_year = parseInt(getHomoIdentifier('PUBYEAR')) || undefined;
1375 if (elementWritable(ref = document.getElementById('year'))) {
1376 ref.value = release.album_year || '';
1377 }
1378 i = release.release_date && extractYear(release.release_date);
1379 if (elementWritable(ref = document.getElementById('remaster_year'))
1380 || !isUpload && i > 0 && (ref = document.querySelector('input[name="year"]')) != null && !ref.disabled) {
1381 ref.value = i || '';
1382 }
1383 //if (tracks.every(it => it.identifiers.EXPLICIT == 0)) editionTitle = 'Clean' + (editionTitle ? ' / ' + editionTitle : '');
1384 [/\s+\(([^\(\)]+)\)\s*$/, /\s+\[([^\[\]]+)\]\s*$/, /\s+\{([^\{\}]+)\}\s*$/].forEach(function(rx) {
1385 var version = tracks.map(track => rx.test(track.title) ? RegExp.$1 : null);
1386 version = version.homogeneous() && version[0] || undefined;
1387 if (!editionTitle && /\b(?:Remastered|Remasterisée|Remasterizado|Acoustic|Instrumental)\b/i.test(version)
1388 && releaseType != getReleaseIndex('Single')) editionTitle = version;
1389 if (!releaseType && /\b(?:Live)\b/i.test(version)) releaseType = getReleaseIndex('Live album');
1390 });
1391 if (elementWritable(ref = document.getElementById('remaster_title'))) ref.value = editionTitle || '';
1392 if (elementWritable(ref = document.getElementById('remaster_record_label')
1393 || document.querySelector('input[name="recordlabel"]'))) {
1394 ref.value = release.label ? prefs.selfrelease_label && !isVA && release.label == release.artist
1395 || /^(?:independent|vlastní\s+náklad|Self[\s\-]Released)$/i.test(release.label)
1396 || /^iMD-/.test(release.label) ? prefs.selfrelease_label : release.label.split(/\s*;\s*/g).join(' / ') : '';
1397 }
1398 if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
1399 || document.querySelector('input[name="cataloguenumber"]'))) {
1400 ref.value = release.catalogs.length >= 1
1401 && release.catalogs.map(it => it.replace(/\s*;\s*/g, ' / ')).join(' / ') || barCode || '';
1402 }
1403 var scene = getHomoIdentifier('SCENE');
1404 if (isUpload && scene != undefined && (ref = document.getElementById('scene')) != null && !ref.disabled) try {
1405 ref.checked = eval(scene.toLowerCase());
1406 } catch(e) { console.warn('Invalid SCENE value (' + scene + ')') }
1407 var br_isSet = (ref = document.getElementById('bitrate')) != null && ref.value;
1408 if (elementWritable(ref = document.getElementById('format')) && allowedFormats.includes(release.codec)) {
1409 ref.value = release.codec || (isRED ? '' : '---');
1410 ref.onchange(); //exec(function() { Format() });
1411 }
1412 if (isRequestNew) {
1413 if (prefs.always_request_perfect_flac) reqSelectFormats('FLAC');
1414 else if (release.codec) reqSelectFormats(release.codec);
1415 }
1416 var sel;
1417 if (release.encoding == 'lossless') {
1418 sel = tracks.some(track => track.bd == 24) ? '24bit Lossless' : 'Lossless';
1419 } else if (release.bitrates.length >= 1) {
1420 let lame_version = release.codec == 'MP3' && /^LAME(\d+)\.(\d+)/i.test(release.vendor) ?
1421 parseInt(RegExp.$1) * 1000 + parseInt(RegExp.$2) : undefined;
1422 if (release.codec == 'MP3' && release.codec_profile == 'VBR V0') {
1423 sel = lame_version >= 3094 ? 'V0 (VBR)' : 'APX (VBR)'
1424 } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V1') {
1425 sel = 'V1 (VBR)'
1426 } else if (release.codec == 'MP3' && release.codec_profile == 'VBR V2') {
1427 sel = lame_version >= 3094 ? sel = 'V2 (VBR)' : 'APS (VBR)'
1428 } else if (release.bitrates.length == 1 && [192, 256, 320].includes(Math.round(release.bitrates[0]))) {
1429 sel = Math.round(release.bitrates[0]);
1430 } else {
1431 sel = 'Other';
1432 }
1433 }
1434 if ((ref = document.getElementById('bitrate')) != null && !ref.disabled && (overwrite || !br_isSet)) {
1435 ref.value = sel || '';
1436 ref.onchange(); //exec(function() { Bitrate() });
1437 if (sel == 'Other' && (ref = document.getElementById('other_bitrate')) != null) {
1438 ref.value = Math.round(release.bitrates.length == 1 ? release.bitrates[0] : albumBitrate);
1439 if ((ref = document.getElementById('vbr')) != null) ref.checked = release.bitrates.length > 1;
1440 }
1441 }
1442 if (isRequestNew) {
1443 if (prefs.always_request_perfect_flac) {
1444 reqSelectBitrates('Lossless', '24bit Lossless');
1445 } else if (sel) reqSelectBitrates(sel);
1446 }
1447 if (release.media) {
1448 sel = undefined;
1449 [
1450 [/\b(?:WEB|File|Download|digital\s+media)\b|^Digital$/i, 'WEB'],
1451 [/\bCD\b/, 'CD'],
1452 [/\b(?:SA-?CD|[Hh]ybrid)\b/, 'SACD'],
1453 [/\b(?:[Bb]lu[\-\−\—\–\s]?[Rr]ay|BRD?|BD)\b/, isOPS ? 'BD' : 'Blu-Ray'],
1454 [/\bDVD(?:-?A)?\b/, 'DVD'],
1455 [/\b(?:[Vv]inyl\b|LP\b|12"|7")/, 'Vinyl'],
1456 ].forEach(k => { if (k[0].test(release.media)) sel = k[1] });
1457 media = sel || media;
1458 }
1459 if (!media) {
1460 if (tracks.every(isRedBook)) {
1461 addMessage('media not determined - CD estimated', 'info');
1462 media = 'CD';
1463 } else if (tracks.some(t => t.bd > 16 || (t.sr > 0 && t.sr != 44100) || t.samples > 0 && t.samples % 588 != 0)) {
1464 addMessage('media not determined - NOT CD', 'info');
1465 }
1466 } else if (media != 'CD' && tracks.every(isRedBook)) {
1467 addMessage('CD as source media is estimated (' + media + ')', 'info');
1468 }
1469 if (elementWritable(ref = document.getElementById('media'))) {
1470 ref.value = media || !tracks.some(notRedBook) && prefs.default_medium || (isRED ? '' : '---');
1471 }
1472 if (isRequestNew) {
1473 if (prefs.always_request_perfect_flac) reqSelectMedias('WEB', 'CD', isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD')
1474 else if (media) reqSelectMedias(media);
1475 }
1476 function isRedBook(track) {
1477 return track.bd == 16 && track.sr == 44100 && track.channels == 2 && track.samples > 0 && track.samples % 588 == 0;
1478 }
1479 function notRedBook(track) {
1480 return track.bd && track.bd != 16 || track.sr && track.sr != 44100
1481 || track.channels && track.channels != 2 || track.samples && track.samples % 588 != 0;
1482 }
1483 if (tracks.every(it => it.identifiers.ORIGINALFORMAT && it.identifiers.ORIGINALFORMAT.includes('DSD'))) {
1484 isFromDSD = true;
1485 }
1486 // Release type
1487 if (!releaseType/* || isCompilation)*/) {
1488 if (/\b(?:Mixtape)\b/i.test(release.album)) releaseType = getReleaseIndex('Mixtape');
1489 else if (isVA) releaseType = getReleaseIndex('Compilation');
1490 else if (isCompilation) releaseType = getReleaseIndex('Anthology');
1491 }
1492 if ((!releaseType || releaseType == 5) && totalTime <= prefs.EP_threshold && tracks.every(function(track) {
1493 const rxs = [/\s+\([^\(\)]+\)\s*$/, /\s+\[[^\[\]]+\]\s*$/];
1494 return rxs.reduce((acc, rx) => acc.replace(rx, ''), track.title)
1495 == rxs.reduce((acc, rx) => acc.replace(rx, ''), tracks[0].title);
1496 })) {
1497 releaseType = getReleaseIndex('Single');
1498 }
1499 if (!releaseType) if (totalTime > 0 && totalTime < prefs.single_threshold) {
1500 releaseType = getReleaseIndex('Single');
1501 } else if (totalTime > 0 && totalTime < prefs.EP_threshold) {
1502 releaseType = getReleaseIndex('EP');
1503 }
1504 if ((ref = document.getElementById('releasetype')) != null && !ref.disabled
1505 && (overwrite || ref.value == 0 || ref.value == '---')) ref.value = releaseType || getReleaseIndex('Album');
1506 // Tags
1507 if (prefs.estimate_decade_tag && (isNaN(totalTime) || totalTime < 2 * 60 * 60)
1508 && release.album_year > 1900 && [1, 3, 5, 9, 13, undefined].includes(releaseType)
1509 /*&& !/\b(?:Remaster(?:ed)?|Remasterizado|Remasterisée|Reissue|Anniversary|Collector(?:'?s)?)\b/i.test(editionTitle)*/)
1510 tags.add(Math.floor(release.album_year/10) * 10 + 's'); // experimental
1511 if (release.country) {
1512 if (!excludedCountries.some(it => it.test(release.country))) tags.add(release.country);
1513 }
1514 if (!composerEmphasis && tracks.every(track => track.identifiers.HASLYRICS == 0)) tags.add('instrumental');
1515 if (elementWritable(ref = document.getElementById('tags'))) {
1516 ref.value = tags.toString();
1517 if (artists[0].length == 1 && prefs.fetch_tags_from_artist > 0) setTimeout(function() {
1518 var artist = getSiteArtist(artists[0][0]);
1519 if (!artist) return;
1520 tags.add(...artist.tags.sort((a, b) => b.count - a.count).map(it => it.name)
1521 .slice(0, prefs.fetch_tags_from_artist));
1522 var ref = document.getElementById('tags');
1523 ref.value = tags.toString();
1524 }, 3000);
1525 }
1526 if (!composerEmphasis/* && release.genres.length > 0*/ && !prefs.keep_meaningles_composers) {
1527 document.querySelectorAll('input[name="artists[]"]').forEach(function(i) {
1528 if (['4', '5'].includes(i.nextElementSibling.value)) i.value = '';
1529 });
1530 }
1531
1532 const doubleParsParsers = [
1533 /\(+(\([^\(\)]*\))\)+/,
1534 /\[+(\[[^\[\]]*\])\]+/,
1535 /\{+(\{[^\{\}]*\})\}+/,
1536 ];
1537 tracks.forEach(function(track) {
1538 doubleParsParsers.forEach(function(rx) {
1539 if (!rx.test(track.title)) return;
1540 addMessage('doubled parentheses in track #' + track.tracknumber + ' title ("' + track.title + '")', 'warning');
1541 //track.title.replace(rx, RegExp.$1);
1542 });
1543 });
1544 if (tracks.length > 1 && tracks.map(track => track.title).homogeneous()) {
1545 addMessage('all tracks having same title: ' + tracks[0].title, 'warning');
1546 }
1547 if (isUpload && !isOPS) findPreviousUploads();
1548 // Album description
1549 sourceUrl = getStoreUrl();
1550 if ((ref = document.querySelector('tr#autofill_tr > td > select')) != null) {
1551 if (i = getHomoIdentifier('DISCOGS_ID')) {
1552 ref.value = 'discogs';
1553 ref.onchange();
1554 if (elementWritable(ref = document.getElementById('discogs'))) ref.value = i;
1555 } else if (i = getHomoIdentifier('MBID')) {
1556 ref.value = 'musicbrainz';
1557 ref.onchange();
1558 if (elementWritable(ref = document.getElementById('musicbrainz'))) ref.value = i;
1559 }
1560 }
1561 const vinylTest = /^((?:Vinyl|LP) rip by\s+)(.*)$/im;
1562 const vinyltrackParser = /^([A-Z])[\-\.\s]?((\d+)(?:\.\d+)?)$/;
1563 const classicalWorkParsers = [
1564 /^(.*\S):\s+(.*)$/,
1565 /^(.+?):\s+([IVXC]+\.\s+.*)$/,
1566 ];
1567 var description;
1568 if (isRequestNew || isRequestEdit) { // request
1569 description = [];
1570 if (release.release_date && !/^\s*\d{4}\s*$/.test(release.release_date) && !isNaN(i = new Date(release.release_date))) {
1571 let today = new Date().getDateValue();
1572 description.push((i.getDateValue() < today ? 'Released' : 'Releasing') + ' ' + i.toDateString());
1573 if (prefs.upcoming_tags && i.getDateValue() >= today && (ref = document.getElementById('tags')) != null && !ref.disabled) {
1574 let tags = new TagManager(ref.value);
1575 tags.add(prefs.upcoming_tags);
1576 ref.value = tags.toString();
1577 }
1578 }
1579 if (!prefs.include_tracklist_in_request) {
1580 let summary = '';
1581 if (release.totaldiscs > 1) summary += release.totaldiscs + ' discs, ';
1582 summary += tracks.length + ' track'; if (tracks.length > 1) summary += 's';
1583 if (totalTime > 0) summary += ', ' + makeTimeString(totalTime);
1584 description.push(summary);
1585 }
1586 if (sourceUrl || release.urls.length > 0) description.push(getUrls());
1587 if (release.catalogs.length == 1 && /^\d{10,}$/.test(release.catalogs[0]) || /^\d{10,}$/.test(barCode)) {
1588 description.push('[url=https://www.google.com/search?q=' + RegExp.lastMatch + ']Find more stores...[/url]');
1589 }
1590 if (prefs.include_tracklist_in_request) description.push(genPlaylist());
1591 if (release.descriptions.length > 0) Array.prototype.push.apply(description, release.descriptions);
1592 description = genAlbumHeader().concat(description.join('\n\n'));
1593 if (description.length > 0) {
1594 ref = document.getElementById('description') || document.querySelector('textarea[name="description"]');
1595 if (elementWritable(ref)) {
1596 ref.value = description;
1597 } else if (isRequestEdit && ref != null && !ref.disabled) {
1598 ref.value = ref.value.length > 0 ? ref.value.concat('\n\n', description) : ref.value = description;
1599 preview(0);
1600 }
1601 }
1602 } else { // upload
1603 description = '';
1604 if (prefs.bpm_summary && albumBPM > 0) description += '\n\nAverage album BPM: [code]' + albumBPM + '[/code]';
1605 /*if (release.release_date) {
1606 let rd = new Date(release.release_date);
1607 if (!isNaN(rd)) description = '\n\nRelease date: ' + rd.toDateString();
1608 }*/
1609 let vinylRipInfo;
1610 if (release.descriptions.length > 0) {
1611 description += '\n\n';
1612 if (isRED && prefs.tracklist_style == 3) description += '[pad=0|20]';
1613 if (release.descriptions.length == 1 && release.descriptions[0]
1614 && (matches = vinylTest.exec(release.descriptions[0])) != null) {
1615 vinylRipInfo = release.descriptions[0].slice(matches.index).trim().split(/(?:[ \t]*\r?\n)+/);
1616 description += release.descriptions[0].slice(0, matches.index).trim();
1617 } else description += release.descriptions.join('\n\n');
1618 if (isRED && prefs.tracklist_style == 3) description += '[/pad]';
1619 }
1620 let oa = fetchOnlineAdditions().then(t => { description += '\n\n'.concat(t) }, reason => undefined);
1621 if (elementWritable(ref = document.getElementById('album_desc'))) {
1622 ref.value = genPlaylist();
1623 finalizeDesc(ref);
1624 }
1625 if ((ref = document.getElementById('body') || document.querySelector('textarea[name="body"]')) != null && !ref.disabled) {
1626 if (ref.value.length == 0) ref.value = genPlaylist(); else {
1627 let editioninfo = '';
1628 if (editionTitle) {
1629 editioninfo = '[size=5][b]' + editionTitle;
1630 if (release.release_date && (i = extractYear(release.release_date)) > 0) editioninfo += ' (' + i + ')';
1631 editioninfo += '[/b][/size]\n\n';
1632 }
1633 ref.value = ref.value.concat('\n\n', editioninfo, genPlaylist(false, false));
1634 }
1635 finalizeDesc(ref);
1636 }
1637 function finalizeDesc(elem) {
1638 oa.then(function() {
1639 if (description) elem.value += description;
1640 preview(0);
1641 });
1642 }
1643 // Release description
1644 if (elementWritable(ref = document.getElementById('release_samplerate'))) {
1645 ref.value = Object.keys(release.srs).length == 1 && Object.keys(release.srs)[0] ?
1646 Math.floor(Object.keys(release.srs)[0] / 1000) :
1647 Object.keys(release.srs).length > 1 || isNaN(Object.keys(release.srs)[0]) ? '999' : '';
1648 }
1649 let lineage = '', rlsDesc = '';
1650 let drInfo = '[hide=DR' + (release.drs.length == 1 ? release.drs[0] : '') + '][pre][/pre]';
1651 let hasSR = Object.keys(release.srs).length > 0;
1652 let srInfo = hasSR ? Object.keys(release.srs).filter(sr => sr > 0).sort((a, b) => release.srs[b] - release.srs[a])
1653 .map(f => f / 1000).join('/').concat('kHz') : '';
1654 if (tracks.some(track => track.bd > 16)) {
1655 if ([isOPS ? 'BD' : 'Blu-Ray', 'DVD', 'SACD'].includes(media)) {
1656 if (!isNWCD) rlsDesc = srInfo;
1657 addChannelInfo();
1658 if (media == 'SACD' || isFromDSD) addDSDInfo();
1659 if (prefs.cleanup_descriptions) addDRInfo();
1660 //addRGInfo();
1661 addHybridInfo();
1662 drInfo += '[/hide]';
1663 } else if (media == 'Vinyl') {
1664 let hassr = hasSR && (!isNWCD || Object.keys(release.srs).length > 1);
1665 if (hassr) lineage = srInfo + ' ';
1666 if (vinylRipInfo) {
1667 if (vinylTest.test(vinylRipInfo[0]) && RegExp.$2.toLowerCase() != 'unknown')
1668 vinylRipInfo[0] = vinylRipInfo[0].replace(vinylTest, '$1[color=blue]$2[/color]');
1669 if (hassr) vinylRipInfo[0] = vinylRipInfo[0].replace(/^Vinyl\b/, 'vinyl');
1670 lineage += vinylRipInfo[0] + '\n\n[u]Lineage:[/u]' + vinylRipInfo.slice(1).map(l => '\n'.concat([
1671 // RuTracker translation
1672 ['Код класса состояния винила', 'Vinyl condition class'],
1673 ['Устройство воспроизведения', 'Turntable'],
1674 ['Головка звукоснимателя', 'Cartridge'],
1675 ['Картридж', 'Cartridge'],
1676 ['Предварительный усилитель', 'Preamplifier'],
1677 ['АЦП', 'ADC'],
1678 ['Программа-оцифровщик', 'Software'],
1679 ['Обработка звука', 'Audio post-processing'],
1680 ['Обработка', 'Post-processing'],
1681 ].reduce((acc, it) => acc.replace(it[0], it[1]), l))).join('');
1682 } else lineage += (hassr ? 'Vinyl' : ' vinyl') + ' rip by [color=blue][/color]\n\n[u]Lineage:[/u]\n';
1683 let imgs = '\n[img][/img]'.repeat(8);
1684 if (!isNWCD) drInfo += '\n'.concat(imgs); else lineage += '\n\n[hide]'.concat(imgs.slice(1), '[/hide]');
1685 drInfo += '[/hide]';
1686 } else { // WEB Hi-Res
1687 if (!isNWCD || Object.keys(release.srs).length > 1) rlsDesc = srInfo;
1688 if (release.channels && release.channels != 2) addChannelInfo();
1689 if (isFromDSD) addDSDInfo();
1690 if (!isFromDSD || prefs.cleanup_descriptions) addDRInfo();
1691 //addRGInfo();
1692 addHybridInfo();
1693 if (isFromDSD || prefs.cleanup_descriptions || Object.keys(release.srs).length == 1
1694 && Object.keys(release.srs)[0] == 88200) drInfo += '[/hide]'; else drInfo = null;
1695 }
1696 } else { // 16bit or lossy
1697 if (Object.keys(release.srs).some(f => f != 44100)) rlsDesc = srInfo;
1698 if (release.channels && release.channels != 2) addChannelInfo();
1699 addDRInfo();
1700 //addRGInfo();
1701 if (prefs.cleanup_descriptions) drInfo += '[/hide]'; else drInfo = null;
1702 if (release.codec == 'MP3' && release.vendor) {
1703 // TODO: parse mp3 vendor string
1704 } else if (['AAC', 'Opus', 'Vorbis'].includes(release.codec) && release.vendor) {
1705 let _encoder_settings = release.vendor;
1706 if (release.codec == 'AAC' && /^qaac\s+[\d\.]+/i.test(release.vendor)) {
1707 let enc = [];
1708 if (matches = release.vendor.match(/\bqaac\s+([\d\.]+)\b/i)) enc[0] = matches[1];
1709 if (matches = release.vendor.match(/\bCoreAudioToolbox\s+([\d\.]+)\b/i)) enc[1] = matches[1];
1710 if (matches = release.vendor.match(/\b(AAC-\S+)\s+Encoder\b/i)) enc[2] = matches[1];
1711 if (matches = release.vendor.match(/\b([TC]VBR|ABR|CBR)\s+(\S+)\b/)) { enc[3] = matches[1]; enc[4] = matches[2]; }
1712 if (matches = release.vendor.match(/\bQuality\s+(\d+)\b/i)) enc[5] = matches[1];
1713 _encoder_settings = 'Converted by Apple\'s ' + enc[2] + ' encoder (' + enc[3] + '-' + enc[4] + ')';
1714 }
1715 lineage = _encoder_settings;
1716 }
1717 }
1718 function addDSDInfo() {
1719 var nfo = ' DSD64';
1720 if (prefs.sacd_decoder) nfo += ' using ' + prefs.sacd_decoder;
1721 nfo += '\nOutput gain: [code]+0dB[/code]';
1722 if (isNWCD) lineage = 'From' .concat(nfo); else {
1723 if (rlsDesc.length > 0) rlsDesc += ' from'; else rlsDesc = 'From';
1724 rlsDesc += nfo;
1725 }
1726 }
1727 function addDRInfo() {
1728 if (release.drs.length < 1 || document.getElementById('release_dynamicrange') != null) return;
1729 var nfo = 'DR' + release.drs[0];
1730 if (release.drs[0] < 4) nfo = '[color=red]'.concat(nfo, '[/color]');
1731 if (rlsDesc.length > 0) rlsDesc += ' | ';
1732 rlsDesc += nfo;
1733 }
1734 function addRGInfo() {
1735 if (release.ags.length <= 0) return;
1736 if (rlsDesc.length > 0) rlsDesc += ' | ';
1737 rlsDesc += 'RG'; //rlsDesc += 'RG ' + ags[0];
1738 }
1739 function addChannelInfo() {
1740 if (!release.channels) return;
1741 var chi = getChanString(release.channels);
1742 if (chi.length <= 0) return;
1743 if (rlsDesc.length > 0) rlsDesc += ', '; else rlsDesc = 'Channels configuration: ';
1744 rlsDesc += chi;
1745 }
1746 function addHybridInfo() {
1747 if (release.bds.length > 1) release.bds.filter(bd => bd != 24).forEach(function(bd) {
1748 var hybrid_tracks = tracks.filter(it => it.bd == bd).sort(trackComparer).map(function(it) {
1749 return (release.totaldiscs > 1 && it.discnumber ? it.discnumber + '-' : '').concat(it.tracknumber);
1750 });
1751 if (hybrid_tracks.length < 1) return;
1752 if (rlsDesc.length > 0) rlsDesc += '\n';
1753 rlsDesc += 'Note: track';
1754 if (hybrid_tracks.length > 1) rlsDesc += 's';
1755 rlsDesc += ' #' + hybrid_tracks.join(', ') +
1756 (hybrid_tracks.length > 1 ? ' are' : ' is') + ' ' + bd + 'bit lossless';
1757 });
1758 }
1759 function addRlsDate() {
1760 if (prefs.insert_release_date && release.release_date && !/^\s*\d{4}\s*$/.test(release.release_date)
1761 && !isNaN(i = new Date(release.release_date))) rlsDesc.push('Released ' + i.toDateString());
1762 }
1763 rlsDesc = rlsDesc.length > 0 ? [rlsDesc] : [];
1764 if ((ref = document.getElementById('release_lineage')) != null) {
1765 lineage = lineage ? [lineage] : [];
1766 if (drInfo) rlsDesc.push(drInfo);
1767 addRlsDate();
1768 if (sourceUrl || release.urls.length > 0) lineage.push(getUrls());
1769 if (elementWritable(ref)) {
1770 ref.value = lineage.join('\n\n');
1771 preview(1);
1772 }
1773 } else {
1774 if (lineage.length > 0) rlsDesc.push(lineage);
1775 if (drInfo) rlsDesc.push(drInfo);
1776 addRlsDate();
1777 if (sourceUrl || release.urls.length > 0) rlsDesc.push(getUrls());
1778 }
1779 if (elementWritable(ref = document.getElementById('release_desc'))) {
1780 ref.value = rlsDesc.join('\n\n');
1781 if (rlsDesc.length > 0) preview(isNWCD ? 2 : 1);
1782 }
1783 if (release.encoding == 'lossless' && release.codec == 'FLAC'
1784 && tracks.some(track => track.bd == 24) && release.dirpaths.length == 1) {
1785 if ((ref = document.getElementById('release_desc')) != null) GM_xmlhttpRequest({
1786 method: 'GET',
1787 url: new URL('file:'.concat(release.dirpaths[0], '\\foo_dr.txt')).href,
1788 responseType: 'blob',
1789 onload: function(response) {
1790 if (response.status < 200 || response.status >= 400) return defaultErrorHandler(response);
1791 if (!/(\[hide=DR\d*\]\[pre\])\[\/pre\]/im.test(ref.value)) return;
1792 var ndx = RegExp.lastIndex + RegExp.$1.length;
1793 ref.value = ref.value.slice(0, ndx).concat(response.responseText, ref.value.slice(ndx));
1794 },
1795 onerror: error => { console.error('foo_dr.txt not exists or is forbidden to read') },
1796 ontimeout: defaultTimeoutHandler,
1797 });
1798 }
1799 }
1800 if (ajaxRejects > 0) {
1801 i = 'AJAX request(s) eliminated due to Gazelle policy. ' +
1802 'Multiple artists not split correctly? Relaunch parsing in overwrite mode without page reload';
1803 try {
1804 let delay = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]).timeStamp + gazelleApiFrame - Date.now();
1805 if (delay >= 0) {
1806 i += ' after ' + Math.ceil(delay / 1000) + 's';
1807 setTimeout(() => { addMessage('new AJAX timeframe for requery available', 'info') }, delay);
1808 }
1809 addMessage(i + '.', 'notice');
1810 } catch(e) { console.error(e) }
1811 }
1812 if (elementWritable(ref = document.getElementById('release_dynamicrange'))) {
1813 ref.value = release.drs.length == 1 ? release.drs[0] : '';
1814 }
1815 if (isRequestNew && prefs.request_default_bounty > 0) {
1816 let amount = prefs.request_default_bounty < 1024 ? prefs.request_default_bounty : prefs.request_default_bounty / 1024;
1817 if ((ref = document.getElementById('amount_box')) != null && !ref.disabled) ref.value = amount;
1818 if ((ref = document.getElementById('unit')) != null && !ref.disabled) {
1819 ref.value = prefs.request_default_bounty < 1024 ? 'mb' : 'gb';
1820 }
1821 try { Calculate() } catch(e) { /* Orpheus bug void handler */ }
1822 }
1823 if (!media && (ref = document.getElementById('media')) != null && ref.value && ref.value != '---') media = ref.value;
1824 if (!onlineSource) {
1825 if (prefs.honour_url && !sourceUrl && release.urls.length <= 0) addMessage('No lineage URL', 'notice');
1826 onlineSource = (function() {
1827 if (sourceUrl || release.urls.length > 0) return urlResolver(sourceUrl || release.urls[0])
1828 .then(sourceUrl => fetchOnline_Music(sourceUrl, true).then(completeFromOnlineSource));
1829 return Promise.reject('No lineage URL');
1830 })();
1831 if (prefs.check_integrity_online) onlineSource.catch(reason => lookupOnlineSource().then(function(result) {
1832 if (typeof result == 'object') return parseLastFm(result);
1833 if (urlParser.test(result)) return fetchOnline_Music(result, true);
1834 return Promise.reject('Unhandled format');
1835 })).then(onlineCheck).catch(function(reason) {
1836 if (!media || media == 'WEB') tracks.forEach(function(track) {
1837 if (!track.duration || track.duration < 29.6 || track.duration > 30.4) return;
1838 addMessage('track ' + track.tracknumber + ' possible track preview', 'warning');
1839 });
1840 });
1841 }
1842 if (prefs.clean_on_apply) clipBoard.value = '';
1843 prefs.save();
1844 return true;
1845
1846 // ---------------------------------------------------------------------------------------------------------------
1847
1848 function genPlaylist(pad = true, header = true) {
1849 var style = prefs.tracklist_style;
1850 if (style == 2 && (tracks.map(track => track.title).some(notMonospaced)
1851 || tracks.map(track => track.track_artist).some(notMonospaced)
1852 || composerEmphasis && tracks.map(track => track.composer).some(notMonospaced))) style = 3;
1853 if (!style || style <= 0) return null;
1854 var playlist = '';
1855 if (tracks.length > 1 || prefs.singles_conventional_format || isRequestNew || isRequestEdit) {
1856 if (style == 3) playlist = '[align=center]';
1857 if (pad && isRED) playlist += '[pad=8|0|0|0]';
1858 if (header) playlist += genAlbumHeader();
1859 playlist += '[size=4][b][color=' + prefs.tracklist_head_color + ']Tracklisting[/color][/b][/size]';
1860 if (pad && isRED) playlist += '[/pad]';
1861 playlist += '\n'; //'[hr]';
1862 let lastDisc, lastSubtitle, lastWork, lastSide, vinylTrackWidth;
1863 let block = 0, classicalWorks = new Map();
1864 if (composerEmphasis /*isClassical*/ && !tracks.some(it => it.discsubtitle)) {
1865 tracks.forEach(function(track) {
1866 if (!track.composer) return;
1867 (/*isClassical ? classicalWorkParsers : */classicalWorkParsers.slice(1)).forEach(function(classicalWorkParser) {
1868 if (track.classical_work || !classicalWorkParser.test(track.title)) return;
1869 classicalWorks.set(track.classical_work = RegExp.$1, {});
1870 track.classical_title = RegExp.$2;
1871 });
1872 });
1873 for (iter of classicalWorks.keys()) {
1874 let work = tracks.filter(track => track.classical_work == iter);
1875 if (work.length > 1 || tracks.every(track => track.classical_work)) {
1876 if (work[0].track_artist && work[0].track_artist != release.artist && work.map(track => track.track_artist).homogeneous())
1877 classicalWorks.get(iter).performer = realTrackArtist(work[0].track_artist);
1878 if (work[0].composer && release.composers.length > 1 && work.map(track => track.composer).homogeneous())
1879 classicalWorks.get(iter).composer = work[0].composer;
1880 } else {
1881 work.forEach(function(track) {
1882 delete track.classical_work;
1883 delete track.classical_title;
1884 });
1885 classicalWorks.delete(iter);
1886 }
1887 }
1888 }
1889 let track, duration, volumes = new Map(tracks.map(it => [it.discnumber, undefined])), tnOffset = 0;
1890 volumes.forEach(function(val, key) {
1891 volumes.set(key, new Set(tracks.filter(it => it.discnumber == key).map(it => it.discsubtitle)).size)
1892 });
1893 if (!tracks.every(it => !isNaN(parseInt(it.tracknumber.toString())))
1894 && !tracks.every(it => vinyltrackParser.test(it.tracknumber.toString().toUpperCase()))) {
1895 addMessage('inconsistent tracks numbering (' + tracks.map(it => it.tracknumber) + ')', 'warning');
1896 }
1897 vinylTrackWidth = tracks.reduce(function(acc, it) {
1898 return Math.max(vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) && parseInt(RegExp.$3), acc);
1899 }, 0);
1900 if (vinylTrackWidth) {
1901 vinylTrackWidth = vinylTrackWidth.toString().length;
1902 tracks.forEach(function(it) {
1903 if (vinyltrackParser.test(it.tracknumber.toString().toUpperCase()) != null)
1904 it.tracknumber = RegExp.$1 + RegExp.$3.padStart(vinylTrackWidth, '0');
1905 });
1906 ++vinylTrackWidth;
1907 }
1908 if (release.totaldiscs < 2 && tracks.reduce(computeLowestTrack, undefined) - 1)
1909 addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
1910 const padStart = '[pad=0|0|5|0]';
1911 if (canSort && prefs.sort_tracklist) tracks.sort(trackComparer);
1912 for (iter of tracks) {
1913 let title = '', trackArtist = undefined;
1914 if (iter.track_artist && iter.track_artist != release.artist) trackArtist = realTrackArtist(iter.track_artist);
1915 var ttwidth = vinylTrackWidth || (release.totaldiscs > 1 && iter.discnumber ?
1916 tracks.filter(it => it.discnumber == iter.discnumber) : tracks).reduce(function (accumulator, it) {
1917 return Math.max(accumulator, (parseInt(it.tracknumber) || it.tracknumber).toString().length);
1918 }, 2);
1919 function realTrackNumber() {
1920 var tn = parseInt(iter.tracknumber);
1921 return isNaN(tn) ? iter.tracknumber : (tn - tnOffset).toString().padStart(ttwidth, '0');
1922 }
1923 switch (style) {
1924 case 1:
1925 case 3: {
1926 prologue('[size=' + prefs.tracklist_size + ']', '[/size]\n');
1927 track = '[b][color=' + prefs.tracklist_tracknumber_color + ']';
1928 track += realTrackNumber();
1929 track += '[/color][/b]' + prefs.title_separator;
1930 if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
1931 title = '[color=' + prefs.tracklist_artist_color + ']' + trackArtist + '[/color] - ';
1932 }
1933 title += iter.classical_title || iter.title;
1934 if (iter.composer && composerEmphasis && release.composers.length != 1
1935 && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
1936 title = title.concat(' [color=', prefs.tracklist_composer_color, '](', iter.composer, ')[/color]');
1937 }
1938 playlist += track + title;
1939 if (iter.duration) playlist += ' [i][color=' + prefs.tracklist_duration_color +'][' +
1940 makeTimeString(iter.duration) + '][/color][/i]';
1941 break;
1942 }
1943 case 2: {
1944 prologue('[size=' + prefs.tracklist_size + '][pre]', '[/pre][/size]');
1945 track = realTrackNumber();
1946 track += prefs.title_separator;
1947 if (trackArtist && (!iter.classical_work || !classicalWorks.get(iter.classical_work).performer)) {
1948 title = trackArtist + ' - ';
1949 }
1950 title += iter.classical_title || iter.title;
1951 if (composerEmphasis && iter.composer && release.composers.length != 1
1952 && (!iter.classical_work || !classicalWorks.get(iter.classical_work).composer)) {
1953 title = title.concat(' (', iter.composer, ')');
1954 }
1955 let l = 0, j, left, padding, spc;
1956 duration = iter.duration ? '[' + makeTimeString(iter.duration) + ']' : null;
1957 let width = prefs.max_tracklist_width - track.length;
1958 if (duration) width -= duration.length + 1;
1959 while (title.trueLength() > 0) {
1960 j = width;
1961 if (title.trueLength() > width) {
1962 while (j > 0 && title[j] != ' ') { --j }
1963 if (j <= 0) j = width;
1964 }
1965 left = title.slice(0, j).trim();
1966 if (++l <= 1) {
1967 playlist += track + left;
1968 if (duration) {
1969 spc = width - left.trueLength();
1970 padding = (spc < 2 ? ' '.repeat(spc) : ' ' + prefs.pad_leader.repeat(spc - 1)) + ' ';
1971 playlist += padding + duration;
1972 }
1973 width = prefs.max_tracklist_width - track.length - 2;
1974 } else playlist += '\n' + ' '.repeat(track.length) + left;
1975 title = title.slice(j).trim();
1976 }
1977 break;
1978 }
1979 }
1980 }
1981 switch (style) {
1982 case 1:
1983 case 3:
1984 if (totalTime > 0) playlist += '\n\n' + divs[0].repeat(10) + '\n[color=' + prefs.tracklist_duration_color +
1985 ']Total time: [i]' + makeTimeString(totalTime) + '[/i][/color][/size]';
1986 break;
1987 case 2:
1988 if (totalTime > 0) {
1989 duration = '[' + makeTimeString(totalTime) + ']';
1990 playlist += '\n\n' + divs[0].repeat(32).padStart(prefs.max_tracklist_width);
1991 playlist += '\n' + 'Total time:'.padEnd(prefs.max_tracklist_width - duration.length) + duration;
1992 }
1993 playlist += '[/pre][/size]';
1994 break;
1995 }
1996 if (style == 3) playlist += '[/align]';
1997
1998 function computeLowestTrack(acc, track) {
1999 if (Number.isNaN(acc)) return NaN;
2000 var tn = parseInt(track.tracknumber);
2001 if (isNaN(tn)) return NaN;
2002 return isNaN(acc) || tn < acc ? tn : acc;
2003 }
2004
2005 function prologue(prefix, postfix) {
2006 function block1() {
2007 if (block == 3) playlist += postfix;
2008 playlist += '\n';
2009 if (isRED && ![1, 2].includes(block)) playlist += padStart;
2010 block = 1;
2011 }
2012 function block2() {
2013 if (block == 3) playlist += postfix;
2014 playlist += '\n';
2015 if (isRED && ![1, 2].includes(block)) playlist += padStart;
2016 block = 2;
2017 }
2018 function block3() {
2019 //if (block == 2 && isRED) playlist += '[hr]';
2020 if (isRED && [1, 2].includes(block)) playlist += '[/pad]';
2021 playlist += '\n';
2022 if (block != 3) playlist += prefix;
2023 block = 3;
2024 }
2025 if (release.totaldiscs > 1 && iter.discnumber != lastDisc) {
2026 block1();
2027 lastDisc = iter.discnumber;
2028 lastSubtitle = lastWork = undefined;
2029 playlist += '[color=' + prefs.tracklist_disctitle_color + '][size=3][b]';
2030 if (iter.identifiers.VOL_MEDIA && tracks.filter(it => it.discnumber == iter.discnumber)
2031 .every(it => it.identifiers.VOL_MEDIA == iter.identifiers.VOL_MEDIA)) {
2032 playlist += iter.identifiers.VOL_MEDIA.toUpperCase() + ' ';
2033 }
2034 playlist += 'Disc ' + iter.discnumber;
2035 if (iter.discsubtitle && (volumes.get(iter.discnumber) || 0) == 1) {
2036 playlist += ' – ' + iter.discsubtitle;
2037 lastSubtitle = iter.discsubtitle;
2038 }
2039 playlist += '[/b][/size]';
2040 duration = tracks.filter(it => it.discnumber == iter.discnumber).reduce((acc, it) => acc + it.duration, 0);
2041 if (duration > 0) playlist += ' [size=2][i][' + makeTimeString(duration) + '][/i][/size]';
2042 playlist += '[/color]';
2043 tnOffset = tracks.filter(track => track.discnumber == iter.discnumber).reduce(computeLowestTrack, undefined) - 1 || 0;
2044 if (tnOffset) addMessage('volume ' + iter.discnumber + ' track numbering not starting from 1', 'info');
2045 }
2046 if (iter.discsubtitle != lastSubtitle) {
2047 if (block != 1 || iter.discsubtitle) block1();
2048 if (iter.discsubtitle) {
2049 playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]' + iter.discsubtitle + '[/b][/size]';
2050 duration = tracks.filter(it => it.discsubtitle == iter.discsubtitle)
2051 .reduce((acc, it) => acc + it.duration, 0);
2052 if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
2053 playlist += '[/color]';
2054 }
2055 lastSubtitle = iter.discsubtitle;
2056 }
2057 if (iter.classical_work != lastWork) {
2058 if (iter.classical_work) {
2059 block2();
2060 playlist += '[color=' + prefs.tracklist_work_color + '][size=2][b]';
2061 if (release.composers.length != 1 && classicalWorks.get(iter.classical_work).composer) {
2062 playlist += classicalWorks.get(iter.classical_work).composer + ': ';
2063 }
2064 playlist += iter.classical_work;
2065 playlist += '[/b]';
2066 if (classicalWorks.get(iter.classical_work).performer
2067 && classicalWorks.get(iter.classical_work).performer != release.artist) {
2068 playlist += ' (' + classicalWorks.get(iter.classical_work).performer + ')';
2069 }
2070 playlist += '[/size]';
2071 duration = tracks.filter(it => it.classical_work == iter.classical_work)
2072 .reduce((acc, it) => acc + it.duration, 0);
2073 if (duration > 0) playlist += ' [size=1][i][' + makeTimeString(duration) + '][/i][/size]';
2074 playlist += '[/color]';
2075 } else {
2076 if (block > 2) block1();
2077 }
2078 lastWork = iter.classical_work;
2079 }
2080 if (vinyltrackParser.test(iter.tracknumber)) {
2081 if (block == 3 && lastSide && RegExp.$1 != lastSide) playlist += '\n';
2082 lastSide = RegExp.$1;
2083 }
2084 block3();
2085 } // prologue
2086 } else { // single
2087 playlist += '[align=center]';
2088 playlist += isRED ? '[pad=20|20|20|20]' : '';
2089 playlist += '[size=4][b][color=' + prefs.tracklist_artist_color + ']' + release.artist + '[/color]';
2090 playlist += isRED ? '[hr]' : '\n'.concat(divs[0].repeat(24), '\n');
2091 playlist += tracks[0].title + '[/b]';
2092 if (tracks[0].composer) {
2093 playlist += '\n[i][color=' + prefs.tracklist_composer_color + '](' + tracks[0].composer + ')[/color][/i]';
2094 }
2095 if (tracks[0].duration) playlist += '\n\n[color=' + prefs.tracklist_duration_color +
2096 '][' + makeTimeString(tracks[0].duration) + '][/color][/size]';
2097 if (isRED) playlist += '[/pad]';
2098 playlist += '[/align]';
2099 }
2100 return prefs.colorless_tracklist ? playlist.replace(/\[color=\S+?\]/ig, '').replace(/\[\/color\]/ig, '') : playlist;
2101 }
2102
2103 function getUrls() {
2104 var result = [];
2105 if (sourceUrl) result.push(sourceUrl);
2106 Array.prototype.push.apply(result, release.urls.filter(function(url) {
2107 return !sourceUrl || url.toLowerCase() != sourceUrl.toLowerCase();
2108 }));
2109 return result.map(function(url, ndx) {
2110 if (!urlParser.test(url)) return url;
2111 url = new URL(url);
2112 var logoUrl = prefs.use_store_logos ? [
2113 [/\b(?:qobuz\.com)$/i, 'https://ptpimg.me/1saep4.png'],
2114 [/\b(?:bandcamp\.com)$/i, 'https://ptpimg.me/vwki92.jpg' /*'https://ptpimg.me/7evz4g.png'*/],
2115 //[/\b(?:highresaudio\.com)$/i, 'https://ptpimg.me/65xx03.png'],
2116 [/\b(?:7digital\.com)$/i, 'https://ptpimg.me/300scj.png'],
2117 [/\b(?:mora\.jp)$/i, 'https://ptpimg.me/ma53oh.png'],
2118 [/\b(?:deezer\.com)$/i, 'https://ptpimg.me/181799.png'],
2119 [/\b(?:spotify\.com)$/i, 'https://ptpimg.me/xo5d1p.png'],
2120 [/\b(?:tidal\.com)$/i, 'https://ptpimg.me/w80424.png'],
2121 [/\b(?:music\.apple\.com)$/i, 'https://ptpimg.me/in7u5u.png'],
2122 [/\b(?:prestomusic\.com)$/i, 'https://ptpimg.me/q86vjt.png'],
2123 [/\b(?:prostudiomasters\.com)$/i, 'https://ptpimg.me/xkm0th.png'],
2124 [/\b(?:supraphonline\.cz)$/i, 'https://ptpimg.me/h85655.png'],
2125 [/^(?:(?:www\.)?hdtracks\.)/i, 'https://ptpimg.me/wx36i4.png'],
2126 [/\b(?:nativedsd\.com)$/i, 'https://ptpimg.me/m6j8gp.png'],
2127 [/\b(?:indies\.eu)$/i, 'https://ptpimg.me/8a4w49.png'],
2128 [/\b(?:e-onkyo\.com)$/i, 'https://ptpimg.me/uke3n1.png'],
2129 [/\b(?:beatport\.com)$/i, 'https://ptpimg.me/lf8q75.png'],
2130 [/\b(?:junodownload\.com)$/i, 'https://ptpimg.me/6c7y42.png'],
2131 ].reduce((acc, site) => acc || (site[0].test(url.hostname) ? site[1] : null), null) : null;
2132 return logoUrl ? '[url='.concat(url, '][img]', logoUrl, '[/img][/url]') : '[url]'.concat(url, '[/url]');
2133 }).join('\n');
2134 }
2135
2136 function genAlbumHeader() {
2137 return !isVA && artists[0].length >= 3 ? '[size=4]' +
2138 joinArtists(artists[0], artist => '[artist]' + artist + '[/artist]') + ' – ' + release.album + '[/size]\n\n' : '';
2139 }
2140
2141 function findPreviousUploads() {
2142 let search = new URLSearchParams(document.location.search);
2143 if (search.get('groupid')) localFetch('/torrents.php?action=grouplog&groupid=' + search.get('groupid')).then(function(dom) {
2144 dom.querySelectorAll('table > tbody > tr.rowa').forEach(function(tr) {
2145 if (/^\s*deleted\b/i.test(tr.children[3].textContent))
2146 scanLog('Torrent ' + tr.children[1].firstChild.textContent);
2147 });
2148 }); else {
2149 let query = release.album;
2150 if (!isVA && artists[0].length > 0 && artists[0].length < 3) query = artists[0].join(', ').concat(' - ', query);
2151 scanLog(query);
2152 }
2153
2154 function scanLog(query) {
2155 localFetch('/log.php?search=' + encodeURIComponent(query)).then(function(dom) {
2156 dom.querySelectorAll('table > tbody > tr.rowb').forEach(function(tr) {
2157 var size, msg = tr.children[1].textContent.trim();
2158 if (/\b[\d\s]+(?:\.\d+)?\s*(?:([KMGT])I?)?B\b/.test(msg)) size = get_size_from_string(RegExp.lastMatch);
2159 if (!msg.includes('deleted') || (/\[(.*)\/(.*)\/(.*)\]/.test(msg) ?
2160 !release.codec || release.codec != RegExp.$1
2161 //|| !release.encoding || release.encoding != RegExp.$2
2162 || !media || media != RegExp.$3 :
2163 !size || !albumSize || Math.abs(albumSize / size - 1) >= 0.1)) return;
2164 addMessage('possibly same release previously uploaded and deleted: ' + msg, 'warning');
2165 });
2166 });
2167 }
2168
2169 function get_size_from_string(str) {
2170 var matches = /\b([\d\s]+(?:\.\d+)?)\s*(?:([KMGT])I?)?B\b/.exec(str.replace(',', '.').toUpperCase());
2171 if (!matches) return null;
2172 var size = parseFloat(matches[1].replace(/\s+/g, ''));
2173 if (matches[2] == 'K') { size *= Math.pow(1024, 1) }
2174 else if (matches[2] == 'M') { size *= Math.pow(1024, 2) }
2175 else if (matches[2] == 'G') { size *= Math.pow(1024, 3) }
2176 else if (matches[2] == 'T') { size *= Math.pow(1024, 4) }
2177 return Math.round(size);
2178 }
2179 }
2180
2181 function getHomoIdentifier(id) {
2182 id = id.toUpperCase();
2183 return tracks.every((elem, ndx, arr) => elem.identifiers[id] != undefined
2184 && elem.identifiers[id] === arr[0].identifiers[id]) ? tracks[0].identifiers[id] : undefined;
2185 }
2186
2187 function getStoreUrl() {
2188 for (var it of [
2189 ['ACOUSTICSOUNDS_ID', 'https://store.acousticsounds.com/d/{ID}/'],
2190 ['ALLMUSIC_ID', 'https://www.allmusic.com/album/{ID}'],
2191 ['AMAZON_ID', 'https://www.amazon.com/gp/product/{ID}'],
2192 ['AMID', 'https://www.allmusic.com/album/{ID}'],
2193 ['APPLE_ID', 'https://music.apple.com/album/{ID}'],
2194 ['ASIN', 'https://www.amazon.com/gp/product/{ID}'],
2195 ['BEATPORT_ID', 'https://www.beatport.com/release/2/{ID}'],
2196 ['DEEZER_ID', deezerAlbumPrefix + '{ID}'],
2197 ['DISCOGS_ID', discogsOrigin + '/release/{ID}'],
2198 ['EONKYO_ID', 'https://www.e-onkyo.com/music/album/{ID}/'],
2199 ['GOOGLE_ID', 'https://play.google.com/store/music/album/?id={ID}'],
2200 ['INDIESSCOPE_ID', 'https://www.indies.eu/alba/{ID}/'],
2201 ['ITUNES_ID', 'https://music.apple.com/album/{ID}'],
2202 ['JUNODOWNLOAD_ID', 'https://www.junodownload.com/products/{ID}'],
2203 ['MBID', mbrRlsPrefix + '{ID}'],
2204 ['PROSTUDIOMASTERS_ID', 'https://www.prostudiomasters.com/album/page/{ID}'],
2205 ['SPOTIFY_ID', 'https://open.spotify.com/album/{ID}'],
2206 ['TRAXSOURCE_ID', 'https://www.traxsource.com/title/{ID}/'],
2207 ['VGMDB_ID', 'https://vgmdb.net/album/{ID}'],
2208 ['TIDAL_ID', 'https://listen.tidal.com/album/{ID}'],
2209 ['OTOTOY_ID', 'https://ototoy.jp/_/default/p/{ID}'],
2210 ['YANDEX_ID', 'https://music.yandex.ru/album/{ID}'],
2211 ]) {
2212 let ID = getHomoIdentifier(it[0]);
2213 if (ID) return it[1].replace('{ID}', ID);
2214 }
2215 return undefined;
2216 }
2217
2218 function getCoverOnline() {
2219 var url = sourceUrl || release.urls[0], apiFirst;
2220 if ((i = getHomoIdentifier('APPLE_ID') || getHomoIdentifier('ITUNES_ID')) || itunesRlsParser.test(url) && (i = parseInt(RegExp.$1))) {
2221 apiFirst = queryItunesAPI('lookup', { id: i })
2222 .then(lookup => lookup.resultCount > 0 ? setItunesImage(lookup.results[0]) : Promise.reject('no cover'));
2223 } else if (i = getHomoIdentifier('DEEZER_ID') || dzrRlsParser.test(url) && (i = parseInt(RegExp.$1))) {
2224 apiFirst = queryDeezerAPI('album/' + i)
2225 .then(result => result.id ? setDeezerImage(result) : Promise.reject('No cover'));
2226 } else if ((prefs.discogs_key && prefs.discogs_secret || discogs_token)
2227 && (i = getHomoIdentifier('DISCOGS_ID') || dcRlsParser.test(url) && RegExp.$1)) {
2228 apiFirst = queryDiscogsAPI('releases/' + i).then(function(release) {
2229 return release.images.length > 0 ? setCover(release.images[0].uri) : Promise.reject('No cover');
2230 });
2231 } else if ((i = getHomoIdentifier('MBID') || mbrRlsParser.test(url) && RegExp.$1)) {
2232 apiFirst = getMusicBrainzCovers(i).then(function(covers) {
2233 return covers != null ? setCover(covers[1][0]) : Promise.reject('No cover');
2234 });
2235 } else if (i = getHomoIdentifier('TIDAL_ID') || tidalRlsParser(url)
2236 && (i = parseInt(RegExp.$1)) > 0) apiFirst = queryTidalAPI('album', { albumId: i }).then(function(album) {
2237 for (var row of album.rows) {
2238 var albumHeader = row.modules.find(module => module.type == 'ALBUM_HEADER');
2239 if (albumHeader != undefined && albumHeader.album.cover) return 'https://resources.tidal.com/images/'
2240 .concat(albumHeader.album.cover.replace(/-/g, '/'), '/1280x1280.jpg');
2241 }
2242 return Promise.reject('Image not found');
2243 }); else if (url.toLowerCase().includes('mora.jp/')) apiFirst = loadMoraMetadata(url).then(function(packageMeta) {
2244 return setCover(packageMeta.packageUrl + packageMeta.fullsizeimage);
2245 }); else apiFirst = Promise.reject('No known API binding');
2246 return apiFirst.catch(reason => imageUrlResolver(url).then(setCover));
2247 }
2248
2249 function searchCoverOnline() {
2250 switch (typeof prefs.cover_lookup_provider == 'string' && prefs.cover_lookup_provider.toLowerCase()) {
2251 case 'itunes': return searchCoverOnline_iTunes();
2252 case 'deezer': return searchCoverOnline_Deezer();
2253 case 'google': return searchCoverOnline_GooglePlay();
2254 case 'musicbrainz': return searchCoverOnline_MBR();
2255 case 'lastfm': return searchCoverOnline_LastFM();
2256 case 'qobuz': return searchCoverOnline_Qobuz();
2257 case 'all': return searchCoverOnline_iTunes()
2258 .catch(searchCoverOnline_LastFM)
2259 .catch(searchCoverOnline_Deezer)
2260 .catch(searchCoverOnline_MBR)
2261 .catch(searchCoverOnline_Qobuz)
2262 .catch(searchCoverOnline_GooglePlay);
2263 }
2264 return Promise.reject('No valid service selected');
2265
2266 function searchCoverOnline_iTunes() {
2267 return amLookup().then(album => setItunesImage(album)
2268 .then(imgUrl => { info('Apple Music', album.collectionViewUrl, album.collectionId) }));
2269 }
2270 function searchCoverOnline_Deezer() {
2271 return deezerLookup().then(function(album) {
2272 return setDeezerImage(album)
2273 .then(imgUrl => { info('Deezer', deezerAlbumPrefix + album.id, album.id) });
2274 });
2275 }
2276 function searchCoverOnline_GooglePlay() {
2277 var query = new URLSearchParams({
2278 q: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
2279 c: 'music',
2280 });
2281 return globalFetch('https://play.google.com/store/search?'.concat(query)).then(function(response) {
2282 try {
2283 let _objs = loadGoogleData(response);
2284 let _results = _objs.filter(function(obj) { try { return typeof obj[0][4] == 'boolean' } catch(e) { return false } });
2285 if (_results.length == 1) _results = _results[0][0][1]; else throw 'Results metadata not found';
2286 } catch(e) { }
2287 var results = response.document.querySelectorAll('div:first-of-type + div[jscontroller]:last-of-type');
2288 if (results.length > 0) for (var ndx = 0; ndx < results.length; ++ndx) {
2289 let items = [];
2290 results[ndx].querySelectorAll(':scope > div').forEach(function(result) {
2291 var img = result.querySelector('span > span > img');
2292 img = img != null ? (img.src || img.dataset.src).replace(/=[a-z]\d+$/, '=w0') : null;
2293 var album = result.querySelector('a > div[title]');
2294 if (album == null) return;
2295 var artist = album.parentNode.parentNode.parentNode.querySelector('a > div:not([title])')
2296 artist = artist != null ? artist.textContent.trim() : null;
2297 var url = album.parentNode.href;
2298 var id = /\?id=(\w+)\b/i.test(album.parentNode.href) && RegExp.$1 || null;
2299 album = album.textContent.trim();
2300 items.push({ id: id, url: url, artist: artist, album: album, imgUrl: img });
2301 });
2302 for (i = 0; i < 3; ++i) {
2303 var f = items.filter(release => releasesMatch(release.artist, release.album, i));
2304 if (f.length > 1) return Promise.reject('Google Play Music: ambiguity');
2305 if (f.length == 1) break;
2306 }
2307 if (i >= 3) return Promise.reject('Google Play Music: no matches');
2308 if (prefs.diag_mode && i == 2) console.debug('Google Play Music fuzzy match:', release, '==', f[0]);
2309 if (f[0].imgUrl) return setCover(f[0].imgUrl)
2310 .then(release => { info('Google Play Music', f[0].url, f[0].id) });
2311 }
2312 });
2313 return Promise.reject('Google Play Music: no matches');
2314 }
2315 function searchCoverOnline_MBR() {
2316 return mbLookupByBarcode().catch(mbLookupByASIN).catch(mbLookupByTOC)
2317 .catch(reason => mbLookup().then(release => [release]))
2318 .catch(mbLookupByAutoTOC)
2319 .then(releases => Promise.all(releases.map(release => getMusicBrainzCovers(release.id))))
2320 .then(function(releases) {
2321 for (var rls of releases) if (rls != null) return _setCover(rls);
2322 return Promise.reject('MusicBrainz: no covers found');
2323 });
2324
2325 function _setCover(rls) {
2326 return setCover(rls[1][0]).then(function(imgUrl) {
2327 if (/\/release\/(\S+)$/i.test(rls[0])) info('Musicbrains', rls[0], RegExp.$1);
2328 return imgUrl;
2329 });
2330 }
2331 }
2332 function searchCoverOnline_LastFM() {
2333 return queryLastFmAPI('album.getinfo', {
2334 artist: (isVA ? VA : release.artist),
2335 album: release.album,
2336 }).then(function(result) {
2337 if (result.error) return Promise.reject('Last.fm: '.concat(result.message));
2338 var image = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
2339 return acc || result.album.image.find(image => image.size == size && urlParser.test(image['#text']));
2340 }, undefined);
2341 if (!image) return Promise.reject('Last.fm: no cover for matched album');
2342 image = image['#text'];
2343 return setCover(image.replace(/\/\d+(?:x\d+|s)\//i, '/')).catch(reason => setCover(image))
2344 .then(() => { info('Last.fm', result.album.url, result.album.id || result.album.mbid || '#N/A') });
2345 });
2346 }
2347 function searchCoverOnline_Qobuz() {
2348 qbLookup().then(function(album) {
2349 return setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_max'))
2350 .catch(reason => setCover(album.imgUrl.replace(/_\d+(?=\.\w+$)/, '_600')))
2351 .catch(reason => setCover(album.imgUrl))
2352 .then(function(imgUrl) {
2353 info('Qobuz', album.href, album.id);
2354 return imgUrl;
2355 });
2356 });
2357 }
2358 function info(service, url, id) {
2359 addMessage(new HTML('used cover image from ' + service + ' release Id ' +
2360 '<a style="color: #00f3ff;" target="_blank" href="'+ url + '">' + id + '</a>'), 'info');
2361 }
2362 }
2363
2364 function setItunesImage(album) {
2365 return urlParser.test(album.artworkUrl100) ?
2366 setCover(album.artworkUrl100.replace('100x100bb', '100000x100000-999'))
2367 .catch(reason => setCover(album.artworkUrl100)) : Promise.reject('Apple Music image not valid URL');
2368 }
2369 function setDeezerImage(album) {
2370 return urlParser.test(album.cover_xl) ?
2371 setCover(album.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0'))
2372 .catch(reason => setCover(album.cover_xl)) : Promise.reject('Deezer image not valid URL');
2373 }
2374
2375 function completeFromOnlineSource(onlineTracks) {
2376 fillMissingValue(document.getElementById('media'), 'media');
2377 fillMissingValue(document.getElementById('year'), 'album_year');
2378 ref = document.getElementById('remaster_year') || !isUpload && document.querySelector('input[name="year"]');
2379 if (ref != null && !ref.disabled && (ref.value == '' || !isRED && ref.value == '---')) {
2380 var value = getHomoValue('release_date');
2381 if (value != null) ref.value = extractYear(value);
2382 }
2383 fillMissingValue(document.getElementById('remaster_record_label')
2384 || document.querySelector('input[name="recordlabel"]'), 'label');
2385 if (elementWritable(ref = document.getElementById('remaster_catalogue_number')
2386 || document.querySelector('input[name="cataloguenumber"]'))) {
2387 let catNo = getHomoValue('catalog');
2388 if (!catNo && onlineTracks.every(track => track.identifiers.BARCODE
2389 && track.identifiers.BARCODE == onlineTracks[0].identifiers.BARCODE)) {
2390 catNo = parseInt(onlineTracks[0].identifiers.BARCODE.replace(/\s+/g, ''));
2391 }
2392 if (catNo) ref.value = catNo;
2393 }
2394 return onlineTracks;
2395
2396 function getHomoValue(propName) {
2397 return onlineTracks[0][propName] && onlineTracks.map(track => track[propName]).homogeneous() ?
2398 onlineTracks[0][propName] : null;
2399 }
2400 function fillMissingValue(node, propName) {
2401 if (!node || node.disabled || node.value != '' && (isRED || node.value != '---')) return;
2402 var value = getHomoValue(propName);
2403 if (value != null) node.value = value;
2404 }
2405 }
2406
2407 function onlineCheck(onlineTracks) {
2408 if (!Array.isArray(onlineTracks) || onlineTracks.length <= 0) {
2409 addMessage('online check not performed (empty tracklist)', 'notice');
2410 return Promise.reject('No tracks');
2411 }
2412 var issueCounter = 0, hiresTimes = onlineTracks.some(function(track) {
2413 var remainder = Math.floor((track.duration - Math.floor(track.duration)) * 1000) / 100;
2414 return remainder > Math.floor(remainder);
2415 });
2416 onlineTracks.forEach(processTrackArtists);
2417 if (onlineTracks[0].artist && onlineTracks.map(track => track.artist).homogeneous()
2418 && (isVA ? !vaParser.test(onlineTracks[0].artist) : mainArtistMismatch())) {
2419 ++issueCounter;
2420 addMessage(new HTML('online album main artist mismatch ("' +
2421 safeText(release.artist).bold() + '" ≠ "' + safeText(onlineTracks[0].artist).bold() + '")'), 'warning');
2422 }
2423 if (onlineTracks[0].album && onlineTracks.map(track => track.album).homogeneous()
2424 && mismatch(release.album, onlineTracks[0].album)
2425 && mismatch(release.album, removeFeatArtists(onlineTracks[0].album))) {
2426 ++issueCounter;
2427 addMessage(new HTML('online album title mismatch ("' +
2428 safeText(release.album).bold() + '" ≠ "' + safeText(onlineTracks[0].album).bold() + '")'), 'warning');
2429 }
2430 if (onlineTracks[0].label && onlineTracks.map(track => track.label).homogeneous()
2431 && mismatch(release.label, onlineTracks[0].label, /-/g)) {
2432 ++issueCounter;
2433 addMessage(new HTML('online album label mismatch ("' +
2434 safeText(release.label).bold() + '" ≠ "' + safeText(onlineTracks[0].label).bold() + '")'), 'notice');
2435 }
2436 if (release.catalogs.length == 1
2437 && onlineTracks[0].catalog && onlineTracks.map(track => track.catalog).homogeneous()
2438 && mismatch(release.catalogs[0], onlineTracks[0].catalog, /[\s\-]/g)) {
2439 ++issueCounter;
2440 addMessage(new HTML('online album catalogue# mismatch ("' +
2441 safeText(release.catalogs[0]).bold() + '" ≠ "' + safeText(onlineTracks[0].catalog).bold() + '")'), 'notice');
2442 }
2443 if (onlineTracks[0].album_year && onlineTracks.map(track => track.album_year).homogeneous()
2444 && release.album_year != onlineTracks[0].album_year) {
2445 ++issueCounter;
2446 addMessage(new HTML('online album year mismatch (' +
2447 (release.album_year || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].album_year.toString().bold() + ')'), 'warning');
2448 }
2449 if (onlineTracks[0].release_date && release.release_date && onlineTracks.map(track => track.release_date).homogeneous()
2450 && new Date(release.release_date.toString()).getDateValue()
2451 != new Date(onlineTracks[0].release_date.toString()).getDateValue()) {
2452 ++issueCounter;
2453 addMessage(new HTML('online album release date mismatch (' +
2454 (release.release_date || '<unset>').toString().bold() + ' ≠ ' + onlineTracks[0].release_date.toString().bold() + ')'), 'notice');
2455 }
2456 if (tracks.length != onlineTracks.length) {
2457 ++issueCounter;
2458 addMessage(new HTML('online album different tracklist length (' + tracks.length.toString().bold() +
2459 ' ≠ ' + onlineTracks.length.toString().bold() + ')'), 'warning');
2460 }
2461 if (totalTime > 0) {
2462 let ttOnline = onlineTracks.reduce((acc, track) => acc + (track.duration || NaN), 0);
2463 if (ttOnline > 0 && Math.abs(totalTime - ttOnline) * 100 / ttOnline > (media == 'Vinyl' ?
2464 prefs.vinyl_duration_divergency : hiresTimes ? 0.1 : prefs.duration_divergency)) {
2465 ++issueCounter;
2466 addMessage(new HTML('online album duration mismatch (' + makeTimeString(totalTime).bold() +
2467 ' ≠ ' + makeTimeString(ttOnline).bold() + ')'), 'warning');
2468 }
2469 }
2470 for (let ndx = 0; ndx < tracks.length; ++ndx) {
2471 if (ndx >= onlineTracks.length) {
2472 addMessage('end of online tracklist reached, tracks from #' + (ndx + 1) + ' to end will not be checked', 'notice');
2473 break;
2474 }
2475 if (mismatch(tracks[ndx].title, onlineTracks[ndx].title)
2476 && mismatch(tracks[ndx].title, removeFeatArtists(onlineTracks[ndx].title))) {
2477 ++issueCounter;
2478 addMessage('online track #' + (ndx + 1) + ' title mismatch ("' +
2479 (tracks[ndx].title || '') + '" ≠ "' + (onlineTracks[ndx].title || '') + '")', 'warning');
2480 }
2481 if (onlineTracks[ndx].track_artist && onlineTracks[ndx].track_artist != onlineTracks[ndx].artist) {
2482 var trackArtists = getArtists(tracks[ndx].track_artist);
2483 var onlineSrackArtists = getArtists(onlineTracks[ndx].track_artist);
2484 if (!trackArtists[0].equalCaselessTo(onlineSrackArtists[0])
2485 || !trackArtists[1].equalCaselessTo(onlineSrackArtists[1])) {
2486 ++issueCounter;
2487 addMessage('online track #' + (ndx + 1) + ' track artist mismatch ("' +
2488 (tracks[ndx].track_artist || '') + '" ≠ "' + (onlineTracks[ndx].track_artist || '') + '")', 'notice');
2489 }
2490 }
2491 if (onlineTracks[ndx].tracknumber && tracks[ndx].tracknumber != onlineTracks[ndx].tracknumber) {
2492 ++issueCounter;
2493 addMessage('online track #' + (ndx + 1) + ' track number mismatch (' +
2494 (tracks[ndx].tracknumber || '<unset>') + ' ≠ ' + onlineTracks[ndx].tracknumber + ')',
2495 release.totaldiscs > 1 ? 'notice' : 'warning');
2496 }
2497 if (onlineTracks[ndx].discnumber && (onlineTracks[ndx].discnumber > 1 || tracks[ndx].discnumber)
2498 && tracks[ndx].discnumber != onlineTracks[ndx].discnumber) {
2499 ++issueCounter;
2500 addMessage('online track #' + (ndx + 1) + ' disc number mismatch (' +
2501 (tracks[ndx].discnumber || '<unset>') + ' ≠ ' + onlineTracks[ndx].discnumber + ')', 'warning');
2502 }
2503 if (onlineTracks[ndx].discsubtitle && mismatch(tracks[ndx].discsubtitle, onlineTracks[ndx].discsubtitle)) {
2504 ++issueCounter;
2505 addMessage('online track #' + (ndx + 1) + ' disc subtitle mismatch ("' +
2506 (tracks[ndx].discsubtitle || '') + '" ≠ "' + onlineTracks[ndx].discsubtitle + '")', 'notice');
2507 }
2508 let timeDif = tracks[ndx].duration && onlineTracks[ndx].duration
2509 && Math.abs(tracks[ndx].duration - onlineTracks[ndx].duration);
2510 if (timeDif >= (media != 'Vinyl' ? 2.5 : hiresTimes ? 0.1 : 5)) {
2511 ++issueCounter;
2512 addMessage('online track #' + (ndx + 1) + ' duration mismatch (' +
2513 makeTimeString(tracks[ndx].duration) + ' ≠ ' + makeTimeString(onlineTracks[ndx].duration) + ')',
2514 (timeDif >= (media != 'Vinyl' ? 5 : hiresTimes ? 0.2 : 8) ? 'warning' : 'notice'));
2515 }
2516 }
2517 if (issueCounter == 0) {
2518 i = 'online check completed without remarks';
2519 if (prefs.messages_verbosity >= 1) addMessage(i, 'info'); else console.debug(i);
2520 }
2521
2522 function mainArtistMismatch() {
2523 var onlineMainArtists = getArtists(onlineTracks[0].artist);
2524 return !onlineMainArtists[0].equalCaselessTo(artists[0]) || !onlineMainArtists[1].equalCaselessTo(albumGuests);
2525 }
2526 function removeFeatArtists(title) {
2527 return featParsers.slice(3).reduce(function(acc, rx, ndx) {
2528 return rx.test(acc) && (ndx < 5 || splitArtists(RegExp.$1).every((artist, ndx) => looksLikeTrueName(artist, 1))) ?
2529 acc.replace(rx, '') : acc;
2530 }, title || '')
2531 }
2532 function mismatch(localStr, onlineStr, rx) {
2533 return normalize(localStr) != normalize(onlineStr);
2534
2535 function normalize(val) {
2536 if (val == undefined || val == null) return '';
2537 if (typeof val != 'string') val = val.toString();
2538 if (rx instanceof RegExp || typeof rx == 'string') val = val.replace(rx, '');
2539 val = val.replace(/[\(\)\-\s]+/g, '');
2540 return prefs.strict_online_check ? val : val.toLowerCase();
2541 }
2542 }
2543 }
2544
2545 function lookupOnlineSource() {
2546 const commonMedia = !media || ['CD', 'WEB'].includes(media);
2547 const noMultivolume = !release.totaldiscs || release.totaldiscs < 2;
2548 var workers = [
2549 /* 0 */ barCode ? querySpotifyAPI('search', { q: 'barcode:' + barCode, type: 'album' })
2550 .then(result => result.albums.total > 0 ? result.albums.items : Promise.reject('Spotify: no matches'))
2551 : Promise.reject('Spotify: unknown barcode'),
2552 /* 1 */ commonMedia ? spotifyLookup() : Promise.reject('Spotify: different media'),
2553 /* 2 */ mbLookupByBarcode(),
2554 /* 3 */ mbLookupByASIN(),
2555 /* 4 */ mbLookupByTOC(),
2556 /* 5 */ mbLookup(),
2557 /* 6 */ commonMedia && noMultivolume ? deezerLookup() : Promise.reject('Deezer: different media'),
2558 /* 7 */ commonMedia ? amLookup() : Promise.reject('Apple Music: different media'),
2559 /* 8 */ dcLookup(),
2560 /* 9 */ mbLookupByAutoTOC(),
2561 /* 10 */ commonMedia && noMultivolume ? queryLastFmAPI('album.getinfo', {
2562 artist: (isVA ? VA : release.artist),
2563 album: release.album,
2564 }).then(result => result.error ? Promise.reject('Last.fm: '.concat(result.message)) : result.album)
2565 : Promise.reject('Last.fm: different media'),
2566 ];
2567 if (prefs.diag_mode) workers.forEach(function(worker, ndx) {
2568 worker.then(result => { console.debug('Worker[' + ndx + '] matched:', result) })
2569 .catch(reason => { console.debug('Worker[' + ndx + '] failed:', reason) });
2570 });
2571 return workers[0].then(function(albums) {
2572 if (prefs.diag_mode) console.debug('Spotify lookup by barcode successfull:', barCode, ' matches:', albums.length);
2573 info('Spotify', albums[0].external_urls.spotify, albums[0].id);
2574 return albums[0].href;
2575 }).catch(reason => workers[1].then(function(album) {
2576 info('Spotify', album.external_urls.spotify, album.id);
2577 return album.href;
2578 })).catch(reason => workers[2].then(mbEpilogue))
2579 .catch(reason => workers[3].then(mbEpilogue))
2580 .catch(reason => workers[4].then(mbEpilogue))
2581 .catch(reason => workers[5].then(function(release) {
2582 info('MusicBrainz', mbrRlsPrefix + release.id, release.id);
2583 return mbrRlsPrefix.concat(release.id);
2584 })).catch(reason => workers[6].then(function(album) {
2585 info('Deezer', deezerAlbumPrefix.concat(album.id), album.id);
2586 return 'https://api.deezer.com/album/'.concat(album.id);
2587 })).catch(reason => workers[7].then(function(collection) {
2588 info('Apple Music', collection.collectionViewUrl, collection.collectionId);
2589 return collection.collectionViewUrl;
2590 })).catch(reason => workers[8].then(function(releases) {
2591 info('Discogs', discogsOrigin.concat(releases[0].uri), releases[0].id);
2592 return releases[0].resource_url;
2593 })).catch(reason => workers[9].then(mbEpilogue))
2594 .catch(reason => workers[10].then(function(album) {
2595 info('Last.fm', album.url, album.id || album.mbid || '#N/A');
2596 return album; // return object
2597 })).catch(function(reason) {
2598 reason = 'online check not performed (no matches for this release)';
2599 if (prefs.check_integrity_online) addMessage(reason, 'notice');
2600 return Promise.reject(reason);
2601 });
2602
2603 function mbEpilogue(releases) {
2604 info('MusicBrainz', mbrRlsPrefix + releases[0].id, releases[0].id);
2605 return mbrRlsPrefix + releases[0].id;
2606 }
2607 function info(service, url, id) {
2608 if (prefs.check_integrity_online) addMessage(new HTML('checking online against ' + service +
2609 ' release Id <a style="color: #00f3ff;" target="_blank" href="' + url + '">' + id + '</a>'), 'info');
2610 }
2611 }
2612
2613 function spotifyLookup() {
2614 return querySpotifyAPI('search', {
2615 q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
2616 type: 'album',
2617 limit: 50,
2618 }).then(function(result) {
2619 if (result.albums.total <= 0) return Promise.reject('Spotify: no matches');
2620 for (i = 0; i < 3; ++i) {
2621 var f = filter(i);
2622 if (f.length > 1) return Promise.reject('Spotify: ambiguity');
2623 if (f.length == 1) break;
2624 }
2625 if (i >= 3) return Promise.reject('Spotify: no matches');
2626 if (prefs.diag_mode && i == 2) console.debug('Spotify fuzzy match:', release, '==', f[0]);
2627 return f[0];
2628
2629 function filter(level) {
2630 return result.albums.items.filter(function(album) {
2631 return (album.album_type == 'single' ? ['Single', 'EP'].some(rt => releaseType == getReleaseIndex(rt))
2632 : releaseType != getReleaseIndex('Single'))
2633 && releasesMatch(album.artists.map(artist => artist.name), album.name, level);
2634 });
2635 }
2636 })
2637 }
2638 function deezerLookup() {
2639 return queryDeezerAPI('search', {
2640 q: 'artist:"' + release.artist + '" album:"' + release.album + '"',
2641 strict: 'on',
2642 order: 'RANKING',
2643 }).then(function(result) {
2644 if (result.total <= 0) return Promise.reject('Deezer: no matches');
2645 for (i = 0; i < 3; ++i) {
2646 var f = filter(i);
2647 if (f.length > 1) return Promise.reject('Deezer: ambiguity');
2648 if (f.length == 1) break;
2649 }
2650 if (i >= 3) return Promise.reject('Deezer: no matches');
2651 if (i == 2) console.debug('Deezer fuzzy match:', release, '==', f[0]);
2652 return f[0];
2653
2654 function filter(level) {
2655 var albums = [];
2656 result.data.forEach(function(match) {
2657 if (!releasesMatch(match.artist.name, match.album.title, level)) return;
2658 if (albums.find(album => album.id == match.album.id) == undefined) albums.push(match.album);
2659 });
2660 return albums;
2661 }
2662 });
2663 }
2664 function amLookup() {
2665 return queryItunesAPI('search', {
2666 term: '"' + (isVA ? VA : release.artist) + '" "' + release.album + '"',
2667 media: 'music',
2668 entity: 'album',
2669 //country: 'US',
2670 }).then(function(result) {
2671 if (result.resultCount <= 0) return Promise.reject('Apple Music: no matches');
2672 for (i = 0; i < 3; ++i) {
2673 var f = filter(i);
2674 if (f.length > 1) return Promise.reject('Apple Music: ambiguity');
2675 if (f.length == 1) break;
2676 }
2677 if (i >= 3) return Promise.reject('Apple Music: no matches');
2678 if (prefs.diag_mode && i == 2) console.debug('Apple Music fuzzy match:', release, '==', f[0]);
2679 return f[0];
2680
2681 function filter(level) {
2682 var preFilter = result.results.filter(function(collection) {
2683 var isSingle = collection.collectionName.endsWith(' - Single');
2684 if (isSingle) collection.collectionName = collection.collectionName.slice(0, -9);
2685 var isEP = collection.collectionName.endsWith(' - EP');
2686 if (isEP) collection.collectionName = collection.collectionName.slice(0, -5);
2687 isSingle = isSingle || collection.collectionType == 'Single';
2688 isEP = !isSingle && (isEP || collection.collectionType == 'EP');
2689 return (releaseType == getReleaseIndex('Single')) == isSingle
2690 && (!isEP || releaseType == getReleaseIndex('EP'))
2691 && releasesMatch(collection.artistName, collection.collectionName, level);
2692 });
2693 return preFilter.some(collection => /\b(?:explicit)/i.test(collection.collectionExplicitness)) ?
2694 preFilter.filter(collection => !/\b(?:clean)/i.test(collection.collectionExplicitness)) : preFilter;
2695 }
2696 });
2697 }
2698 function mbLookupByBarcode() {
2699 if (!barCode) return Promise.reject('MusicBrainz: unknown barcode');
2700 return queryMusicBrainzAPI('release', { query: 'barcode:' + barCode }).then(function(result) {
2701 if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
2702 if (prefs.diag_mode) console.debug('MusicBrainz lookup by barcode successfull: ' + barCode + '; matches: ' + result.count);
2703 return result.releases;
2704 });
2705 }
2706 function mbLookupByASIN() {
2707 var asin = getHomoIdentifier('ASIN');
2708 if (!asin) return Promise.reject('MusicBrainz: unknown ASIN');
2709 asin = asin.replace(/\s+/g, '');
2710 return queryMusicBrainzAPI('release', { query: 'asin:' + asin }).then(function(result) {
2711 if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
2712 if (prefs.diag_mode) console.debug('MusicBrainz lookup by ASIN successfull: ' + asin + '; matches: ' + result.count);
2713 return result.releases;
2714 });
2715 }
2716 function mbLookupByTOC() {
2717 var TOC;
2718 if (TOC = getHomoIdentifier('ITUNES_TOC')) { // iTunes scheme
2719 TOC = TOC.split('+').map(index => parseInt(index));
2720 TOC = [1, TOC[2], TOC[1]].concat(TOC.slice(3));
2721 } else if (TOC = getHomoIdentifier('CT_TOC')) { // CUETools scheme
2722 TOC = TOC.split('+').map(index => parseInt(index, 16));
2723 TOC = [1, TOC.shift(), TOC.pop()].concat(TOC);
2724 }
2725 return mbLookupByDiscID(TOC);
2726 }
2727 function mbLookupByAutoTOC() {
2728 if (release.totaldiscs > 1) return Promise.reject('TOC lookup not possible for multidisc release');
2729 if (!tracks.every(track => track.sr > 0 && track.samples > 0))
2730 return Promise.reject('MusicBrainz: insufficient information for TOC calculation');
2731 var lastFrame = 0;
2732 var TOC = [0].concat(tracks.map(track => (lastFrame += Math.round(track.samples * 75 / track.sr))))
2733 .map(offset => 150 + offset);
2734 TOC.unshift(TOC.pop());
2735 return mbLookupByDiscID([1, tracks.length].concat(TOC));
2736 }
2737 function mbComputeDiscID(mbTOC) {
2738 if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4 || mbTOC[1] - mbTOC[0] > 98) return null;
2739 var tocStr = [mbTOC[0], mbTOC[1]].map(track => track.toString(16).padStart(2, '0'))
2740 .concat(mbTOC.slice(2).map(offset => offset.toString(16).padStart(8, '0'))).join('')
2741 .concat('0'.repeat(98 + mbTOC[0] - mbTOC[1] << 3)).toUpperCase();
2742 var digest = sha1.digest(tocStr);
2743 return btoa(String.fromCharCode(...digest)).replace(/\+/g, '.').replace(/\//g, '_').replace(/\=/g, '-');
2744 }
2745 function mbLookupByDiscID(mbTOC) {
2746 if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4)
2747 return Promise.reject('MusicBrainz: missing or invalid TOC');
2748 var mbDscId = mbComputeDiscID(mbTOC);
2749 var params = { toc: mbTOC.join('+') };
2750 if (media != 'CD') params['media-format'] = 'all';
2751 return queryMusicBrainzAPI('discid/'.concat(mbDscId || '-'), params).then(function(result) {
2752 if (Array.isArray(result.releases) && result.releases.length > 0) var matches = result.releases;
2753 if (result.id && result.title) matches = [result];
2754 if (!Array.isArray(matches)) return Promise.reject('MusicBrainz: no matches');
2755 if (prefs.diag_mode) console.debug('MusicBrainz lookup by discId/TOC successfull:', mbDscId, '/', params, 'matches:', matches);
2756 return matches;
2757 });
2758 }
2759 function mbLookup() {
2760 return queryMusicBrainzAPI('release', {
2761 query: 'release:"' + release.album + '" AND artist:"' + (isVA ? VA : release.artist) + '"',
2762 }).then(function(result) {
2763 if (result.count <= 0) return Promise.reject('MusicBrainz: no matches');
2764 for (i = 0; i < 3; ++i) {
2765 var f = filter(i);
2766 if (f.length > 1) return Promise.reject('MusicBrainz: ambiguity');
2767 if (f.length == 1) break;
2768 }
2769 if (i >= 3) return Promise.reject('MusicBrainz: no matches');
2770 if (prefs.diag_mode && i == 2) console.debug('MusicBrainz fuzzy match:', release, '==', f[0]);
2771 return f[0];
2772
2773 function filter(level) {
2774 return result.releases.filter(function(release) {
2775 return release.quality != 'low'
2776 && (media ? [media] : tracks.some(notRedBook) ? ['WEB'] : ['CD', 'WEB'])
2777 .some(_media => release.media.map(media => remapMedia(media.format)).includes(_media)
2778 && releasesMatch(release['artist-credit'].map(artist => artist.name), release.title, level));
2779 });
2780
2781 function remapMedia(MBmedia) {
2782 return [
2783 ['Digital Media', 'WEB'],
2784 ].reduce((acc, subst) => acc.toLowerCase() == subst[0].toLowerCase() ? subst[1] : acc, MBmedia);
2785 }
2786 }
2787 });
2788 }
2789 function dcLookup() {
2790 var query = { type: 'release' };
2791 if (barCode) query.barcode = barCode; else {
2792 query.artist = '"' + release.artist + '"';
2793 query.release_title = '"' + release.album + '"';
2794 //if (release.catalogs.length > 0) query.catno = release.catalogs.join('; ');
2795 }
2796 return queryDiscogsAPI('database/search', query).then(function(result) {
2797 if (result.results.length <= 0) return Promise.reject('Discogs: no matches');
2798 if (barCode) {
2799 //if (result.results.length > 1) return Promise.reject('Discogs: ambiguity');
2800 if (prefs.diag_mode) console.debug('Discogs lookup by barcode successfull: ' +
2801 barCode + '; matches: ' + result.results.length);
2802 var f = result.results;
2803 } else {
2804 for (i = 0; i < 3; ++i) {
2805 f = filter(i);
2806 if (f.length > 1) return Promise.reject('Discogs: ambiguity');
2807 if (f.length == 1) break;
2808 }
2809 if (i >= 3) return Promise.reject('Discogs: no matches');
2810 if (prefs.diag_mode && i == 2) console.debug('Discogs fuzzy match:', release, '==', f[0]);
2811 }
2812 return f;
2813
2814 function filter(level) {
2815 return result.results.filter(function(album) {
2816 if (media ? Array.isArray(album.format)
2817 && !album.format.some(format => dcFmtToGazelle(format) == media)
2818 : !album.format.some(format => ['CD', 'WEB'].includes(dcFmtToGazelle(format)))) return false;
2819 if (/^(.*?)\s+\(\d+\) - (.*)$/.test(album.title) || !/^(.*?) - (.*)$/.test(album.title))
2820 return releasesMatch(RegExp.$1, RegExp.$2, level);
2821 console.warn('Failed to parse Discogs title:', album.title);
2822 return false;
2823 });
2824 }
2825 });
2826 }
2827 function qbLookup() {
2828 var params = new URLSearchParams({
2829 q: (isVA ? VA : release.artist) + ' ' + release.album,
2830 //s: 'rdc', // descending sort by release date
2831 i: 'boutique',
2832 });
2833 return globalFetch('https://www.qobuz.com/search?' + params).then(function(response) {
2834 var results = response.document.querySelectorAll('div.search-results > div.product');
2835 if (results.length <= 0) return Promise.reject('Qobuz: no matches');
2836 for (i = 0; i < 3; ++i) {
2837 var f = filter(i);
2838 if (f.length > 1) return Promise.reject('Qobuz: ambiguity');
2839 if (f.length == 1) break;
2840 }
2841 if (i >= 3) return Promise.reject('Qobuz: no matches');
2842 if (prefs.diag_mode && i == 2) console.debug('Qobuz fuzzy match:', release, '==', f[0]);
2843 return f[0];
2844
2845 function filter(level) {
2846 var _results = [];
2847 results.forEach(function(result) {
2848 var _result = {};
2849 _result.artist = result.querySelector('div.artist-name > a');
2850 if (_result.artist != null) _result.artist = _result.artist.textContent.trim();
2851 _result.title = result.querySelector('div.album-title > a');
2852 if (_result.title != null) {
2853 _result.id = _result.title.pathname.replace(/^.*\//, '');
2854 _result.href = 'https://www.qobuz.com' + _result.title.pathname;
2855 _result.title = _result.title.textContent.trim();
2856 }
2857 _result.imgUrl = result.querySelector('div.album-cover > a > img');
2858 if (_result.imgUrl != null) _result.imgUrl = _result.imgUrl.dataset.src || _result.imgUrl.src;
2859 if (_result.artist && _result.title && _result.imgUrl
2860 && releasesMatch(_result.artist, _result.title, level, 0.75)) _results.push(_result);
2861 });
2862 return _results;
2863 }
2864 });
2865 }
2866
2867 function ruleLink(rule) {
2868 return ' (<a href="https://redacted.ch/rules.php?p=upload#r' + rule + '" target="_blank">' + rule + '</a>)';
2869 }
2870
2871 function releasesMatch(remoteArtist, remoteTitle, relaxLevel = 0, minSimilarity = 0.75, minFullSimilarity) {
2872 if (typeof remoteArtist == 'string') {
2873 if (isVA != vaParser.test(remoteArtist)) return false;
2874 if (!isVA) remoteArtist = getArtists(remoteArtist)[0];
2875 } else if (!Array.isArray(remoteArtist)) return false;
2876 if (!isVA && !artists[0].equalCaselessTo(remoteArtist)
2877 && !artists[0].map(name => name.toASCII()).equalCaselessTo(remoteArtist.map(name => name.toASCII())))
2878 return false;
2879 if (!remoteTitle) return true;
2880 if (typeof remoteTitle != 'string') return false;
2881 var localTitle = release.album.toLowerCase();
2882 if (localTitle == (remoteTitle = remoteTitle.toLowerCase())) return true;
2883 if (editionTitle) var fullLocalTitle = localTitle.concat(' (', editionTitle.toLowerCase(), ')');
2884 if (fullLocalTitle === remoteTitle) return true;
2885 if (localTitle.toASCII() == remoteTitle.toASCII()
2886 || fullLocalTitle && fullLocalTitle.toASCII() == remoteTitle.toASCII()) return true;
2887 if (relaxLevel <= 0) return false;
2888 if ([
2889 /[\s\,\.\-\!\(\)\!\?]+/g,
2890 /\s+\(([^\(\)]+)\)$/,
2891 /\s+\[([^\[\]]+)\]$/,
2892 /\s+\{([^\{\}]+)\}$/,
2893 ].reduce(function(acc, rx) {
2894 return acc || localTitle.replace(rx, '') == remoteTitle.replace(rx, '')
2895 || fullLocalTitle && fullLocalTitle.replace(rx, '') == remoteTitle.replace(rx, '');
2896 }, false)) return true;
2897 if (relaxLevel <= 1) return false;
2898 var similarity = cosineSimilarity(localTitle, remoteTitle);
2899 if (similarity >= Math.min(minSimilarity, 1)) {
2900 if (prefs.diag_mode) console.debug('Cosine similarity accepted: "' + localTitle + '"=="' + remoteTitle + '" (' + similarity+ ')');
2901 return true;
2902 }
2903 similarity = cosineSimilarity(fullLocalTitle, remoteTitle);
2904 if (fullLocalTitle && similarity >= Math.min(minFullSimilarity || minSimilarity + 0.05, 1)) {
2905 if (prefs.diag_mode) console.debug('Cosine similarity accepted: "' + fullLocalTitle + '"=="' + remoteTitle + '" (' + similarity+ ')');
2906 return true;
2907 }
2908 if (relaxLevel <= 2) return false;
2909 if (localTitle.includes(remoteTitle) || remoteTitle.includes(localTitle)) return true;
2910 return false;
2911 }
2912
2913 function trackComparer(a, b) {
2914 var cmp;
2915 if (release.totaldiscs > 1) {
2916 cmp = a.discnumber - b.discnumber;
2917 if (!isNaN(cmp) && cmp != 0) return cmp;
2918 } else {
2919 cmp = (a.discsubtitle || '').localeCompare(b.discsubtitle || '');
2920 //if (cmp != 0) return cmp;
2921 }
2922 cmp = parseInt(a.tracknumber) - parseInt(b.tracknumber);
2923 if (!isNaN(cmp)) return cmp;
2924 var m1 = vinyltrackParser.exec(a.tracknumber.toUpperCase());
2925 var m2 = vinyltrackParser.exec(b.tracknumber.toUpperCase());
2926 return m1 != null && m2 != null ?
2927 m1[1].localeCompare(m2[1]) || parseFloat(m1[2]) - parseFloat(m2[2]) :
2928 a.tracknumber.toUpperCase().localeCompare(b.tracknumber.toUpperCase());
2929 }
2930
2931 function reqSelectFormats(...vals) {
2932 vals.forEach(function(val) {
2933 ['MP3', 'FLAC', 'AAC', 'AC3', 'DTS'].forEach(function(fmt, ndx) {
2934 if (val.toLowerCase() == fmt.toLowerCase() && (ref = document.getElementById('format_' + ndx)) != null) {
2935 ref.checked = true;
2936 ref.onchange();
2937 }
2938 });
2939 });
2940 }
2941
2942 function reqSelectBitrates(...vals) {
2943 const bitrateSet = !isOPS ? [
2944 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
2945 'V0 (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
2946 ] : [
2947 192, 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', 256, 'APX (VBR)',
2948 'V0 (VBR)', 'q8.x (VBR)', 320, 'Lossless', '24bit Lossless', 'Other',
2949 ];
2950 vals.forEach(function(val) {
2951 var ndx = 10;
2952 bitrateSet.forEach((it, _ndx) => { if (val.toString().toLowerCase() == it.toString().toLowerCase()) ndx = _ndx });
2953 if ((ref = document.getElementById('bitrate_' + ndx)) != null) {
2954 ref.checked = true;
2955 ref.onchange();
2956 }
2957 });
2958 }
2959
2960 function reqSelectMedias(...vals) {
2961 const mediaSet = !isOPS ? ['CD', 'DVD', 'Vinyl', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB', 'Blu-Ray']
2962 : ['CD', 'DVD', 'Vinyl', 'BD', 'Soundboard', 'SACD', 'DAT', 'Cassette', 'WEB'];
2963 vals.forEach(function(val) {
2964 mediaSet.forEach(function(med, ndx) {
2965 if (val == med && (ref = document.getElementById('media_' + ndx)) != null) {
2966 ref.checked = true;
2967 ref.onchange();
2968 }
2969 });
2970 if (val == 'CD') {
2971 if ((ref = document.getElementById('needlog')) != null) {
2972 ref.checked = true;
2973 ref.onchange();
2974 if ((ref = document.getElementById('minlogscore')) != null) ref.value = 100;
2975 }
2976 if ((ref = document.getElementById('needcue')) != null) ref.checked = true;
2977 //if ((ref = document.getElementById('needchecksum')) != null) ref.checked = true;
2978 }
2979 });
2980 }
2981
2982 function getReleaseIndex(str) {
2983 var ndx;
2984 [
2985 ['Album', 1],
2986 ['Soundtrack', 3],
2987 ['EP', 5],
2988 ['Anthology', 6],
2989 ['Compilation', 7],
2990 ['Single', 9],
2991 ['Live album', 11],
2992 ['Remix', 13],
2993 ['Bootleg', 14],
2994 ['Interview', 15],
2995 ['Mixtape', 16],
2996 [isOPS ? 'DJ Mix' : 'Demo', 17],
2997 ['Concert Recording', 18],
2998 ['DJ Mix', 19],
2999 ['Unknown', 21],
3000 ].forEach(k => { if (str.toLowerCase() == k[0].toLowerCase()) ndx = k[1] });
3001 return ndx || 21;
3002 }
3003
3004 function getChanString(n) {
3005 if (!n) return null;
3006 const chanmap = [
3007 'mono',
3008 'stereo',
3009 '2.1',
3010 '4.0 surround sound',
3011 '5.0 surround sound',
3012 '5.1 surround sound',
3013 '7.0 surround sound',
3014 '7.1 surround sound',
3015 ];
3016 return n >= 1 && n <= 8 ? chanmap[n - 1] : n + 'chn surround sound';
3017 }
3018
3019 function fetchOnlineAdditions() {
3020 if (onlineSource) return Promise.reject('Not offline source');
3021 var url = sourceUrl || release.urls[0];
3022 if (!urlParser.test(url)) return Promise.reject('No valid URL to parse');
3023 if (url.toLowerCase().includes('highresaudio.com/'))
3024 return globalFetch(url).then(response => hraPdfBooklet(response) || Promise.reject('No PDF booklet'));
3025 return Promise.reject('No online source containing additions');
3026 }
3027
3028 function processTrackArtists(track) {
3029 if (!track.artist && Array.isArray(track.artists) && track.artists.length > 0) {
3030 track.artist = joinArtists(track.artists);
3031 if (Array.isArray(track.featuring_artists) && track.featuring_artists.length > 0)
3032 track.artist += ' feat. '.concat(joinArtists(track.featuring_artists));
3033 }
3034 if (!track.track_artist && Array.isArray(track.track_artists) && track.track_artists.length > 0) {
3035 track.track_artist = joinArtists(track.track_artists);
3036 if (Array.isArray(track.track_guests) && track.track_guests.length > 0)
3037 track.track_artist += ' feat. '.concat(joinArtists(track.track_guests));
3038 }
3039 ['performer', 'remixer', 'composer', 'conductor', 'compiler', 'producer'].forEach(function(role) {
3040 var arrPropName = role.concat('s');
3041 if (!track[role] && Array.isArray(track[arrPropName]) && track[arrPropName].length > 0)
3042 track[role] = track[arrPropName].join(', ');
3043 });
3044 }
3045 } // parseTracks
3046
3047 function hraPdfBooklet(response) {
3048 var ref = response.document.querySelector('form#pdfjs-form-w2[action]');
3049 if (ref == null) return undefined;
3050 ref = new URLSearchParams(ref.action.replace(/^.*\?/, ''));
3051 return '[url='.concat(ref.get('file'), '][img]https://ptpimg.me/ts0fy8.png[/img][/url]');
3052 }
3053
3054 function fetchOnline_Music(url, weak = false) {
3055 if (!urlParser.test(url)) return Promise.reject('Invalid URL');
3056 const discParser = /^(?:CD|DIS[CK]\s+|VOLUME\s+|DISCO\s+|DISQUE\s+)(\d+)(?:\s+of\s+(\d+))?$/i;
3057 var ref, artist, album, albumYear, releaseDate, channels, label, composer, bd, sr = 44100, description,
3058 compiler, producer, totalTracks, discSubtitle, discNumber, trackNumber, totalDiscs, title, trackArtist,
3059 catalogue, encoding, format, bitrate, duration, country, media = 'WEB', imgUrl,
3060 genres = [], trs, tracks = [], identifiers = {}, trackIdentifiers = {};
3061 if (url.toLowerCase().includes('qobuz.com/')) return globalFetch(url).then(function(response) {
3062 const error = new Error('Failed to parse Qobus release page');
3063 identifiers.QOBUZ_ID = response.finalUrl.replace(/^.*\//, '');
3064 if ((ref = response.document.querySelector('section.album-item[data-gtm]')) != null) try {
3065 let gtm = JSON.parse(ref.dataset.gtm);
3066 if (gtm.shop.category) genres.push(gtm.shop.category);
3067 if (gtm.shop.subCategory && !genres.includes(gtm.shop.subCategory)) genres.push(gtm.shop.subCategory.replace(/-/g, ' '));
3068 } catch(e) { console.warn(e) }
3069 if ((ref = response.document.querySelector('div.album-meta > h2.album-meta__artist')) != null)
3070 artist = ref.title || ref.textContent.trim();
3071 isVA = vaParser.test(artist);
3072 album = (ref = response.document.querySelector('div.album-meta > h1.album-meta__title')) != null ?
3073 ref.title || ref.textContent.trim() : undefined;
3074 releaseDate = (ref = response.document.querySelector('div.album-meta > ul > li:first-of-type')) != null ?
3075 normalizeDate(ref.textContent) : undefined;
3076 var mainArtist = (ref = response.document.querySelector('div.album-meta > ul > li:nth-of-type(2) > a')) != null ?
3077 ref.title || ref.textContent.trim() : undefined;
3078 //ref = response.document.querySelector('p.album-about__copyright');
3079 //if (ref != null) albumYear = extractYear(ref.textContent);
3080 response.document.querySelectorAll('section#about > ul > li').forEach(function(it) {
3081 function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) }
3082 if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1);
3083 if (/\b(\d+)\s*(?:track|pist[ae]|tracce|traccia)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1);
3084 if (['Label', 'Etichetta', 'Sello'].some(l => it.textContent.trimLeft().startsWith(l))) {
3085 label = it.firstElementChild.textContent.replace(/\s+/g, ' ').trim();
3086 }
3087 else if (['Composer', 'Compositeur', 'Komponist', 'Compositore', 'Compositor'].some(matchLabel)) {
3088 composer = it.firstElementChild.textContent.trim();
3089 if (pseudoArtistParsers.some(rx => rx.test(composer))) composer = undefined;
3090 } else if (['Genre', 'Genere', 'Género'].some(g => it.textContent.startsWith(g)) && it.childElementCount > 0
3091 && genres.length <= 0) {
3092 genres = Array.from(it.querySelectorAll('a')).map(elem => elem.textContent.trim());
3093/*
3094 if (genres.length >= 1 && ['Pop/Rock'].includes(genres[0])) genres.shift();
3095 if (genres.length >= 2 && ['Alternative & Indie'].includes(genres[genres.length - 1])) genres.shift();
3096 if (genres.length >= 1 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) {
3097 while (genres.length > 1) genres.shift();
3098 }
3099*/
3100 while (genres.length > 1) genres.shift();
3101 }
3102 });
3103 bd = 16; channels = 2; // defaults to CD quality
3104 response.document.querySelectorAll('span.album-quality__info').forEach(function(k) {
3105 if (/\b([\d\.\,]+)\s*kHz\b/i.test(k.textContent) != null) sr = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
3106 if (/\b(\d+)[\-\s]*Bits?\b/i.test(k.textContent) != null) bd = parseInt(RegExp.$1);
3107 if (/\b(?:Stereo)\b/i.test(k.textContent)) channels = 2;
3108 if (/\b(\d)\.(\d)\b/.test(k.textContent)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
3109 });
3110 getDescription(response, 'section#description > p', true);
3111 if ((ref = response.document.querySelector('a[title="Qobuzissime"]')) != null) {
3112 if (description) description += '\n';
3113 description += '[align=center][url=https://www.qobuz.com'
3114 .concat(ref.pathname, '][img]https://ptpimg.me/4z35uj.png[/img][/url][/align]');
3115 }
3116 if ((ref = response.document.querySelector('div.album-cover > img')) != null)
3117 imgUrl = ref.src.replace(/_\d{3}(?=\.\w+$)/, '_max');
3118 addTracks(response.document);
3119 if (totalTracks <= 50) return tracks;
3120 var query = new URLSearchParams({
3121 albumId: identifiers.QOBUZ_ID,
3122 offset: 50,
3123 limit: 999,
3124 store: /\/(\w{2}-\w{2})\/album\//i.test(response.finalUrl) ? RegExp.$1 : 'fr-fr',
3125 });
3126 return globalFetch('https://www.qobuz.com/v4/ajax/album/load-tracks?'.concat(query), {
3127 headers: { 'X-Requested-With': 'XMLHttpRequest' },
3128 }).then(response => { addTracks(response.document) }).catch(function(reason) {
3129 console.error('globalFetch() failed:', reason);
3130 addMessage('long album, only first 50 tracks were captured from Qobuz, which will result in incmplete release description', 'notice');
3131 }).then(function() { return tracks });
3132
3133 function addTracks(document) {
3134 Array.prototype.push.apply(tracks, Array.from(document.querySelectorAll('div.player__item > div.player__tracks > div.track > div.track__items')).map(function(tr) {
3135 trackIdentifiers = { TRACK_ID: tr.parentNode.dataset.track };
3136 var trackGuests = [], trackComposers = [], trackRemixers = [], trackConductors = [], trackProducers = [], trackPerformers = [];
3137 if ((ref = tr.parentNode.querySelector('p.track__info:first-of-type')) != null) {
3138 trackArtist = [];
3139 ref.textContent.trim().split(/\s+-\s+/).map(it => it.split(/\s*,\s*/)).forEach(function(it) {
3140 var roles = it.slice(1);
3141 if (roles.some(role => /^(?:(?:Main)?Artist)$/.test(role))) trackArtist.pushUnique(it[0]);
3142 if (roles.some(role => /^(?:FeaturedArtist|)$/.test(role))) trackGuests.pushUnique(it[0]);
3143 if (roles.some(role => /^(?:AssociatedPerformer)$/.test(role))) trackPerformers.pushUnique(it[0]);
3144 if (roles.some(role => /^(?:Composer(?:Lyricist)?)$/.test(role))) trackComposers.pushUnique(it[0]);
3145 if (roles.some(role => /^(?:Conductor)$/.test(role))) trackConductors.pushUnique(it[0]);
3146 if (roles.some(role => /^(?:Remixer)$/.test(role))) trackRemixers.pushUnique(it[0]);
3147 if (roles.some(role => /^(?:Producer)$/.test(role))) trackProducers.pushUnique(it[0]);
3148 });
3149 trackArtist = trackArtist.filter(artist => !trackConductors.includes(artist));
3150 trackGuests = trackGuests.filter(artist => ![trackArtist, trackConductors].some(category => category.includes(artist)));
3151 trackPerformers = trackPerformers.filter(artist => ![trackArtist, trackConductors, trackGuests].some(category => category.includes(artist)));
3152 if ((trackArtist = joinArtists(trackArtist)) && trackGuests.length > 0)
3153 trackArtist += ' feat. '.concat(joinArtists(trackGuests));
3154 if (trackArtist && !isVA && trackArtist == artist) trackArtist = undefined;
3155 } else trackArtist = undefined;
3156 if (tr.parentNode.dataset.gtm) try {
3157 let gtm = JSON.parse(tr.parentNode.dataset.gtm);
3158 if (gtm.product.id) trackIdentifiers.QOBUZ_ID = gtm.product.id;
3159 //if (gtm.product.type) trackIdentifiers.RELEASETYPE = gtm.product.type;
3160 if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory];
3161 } catch(e) { console.warn(e) }
3162 if ((ref = tr.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) {
3163 discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim();
3164 guessDiscNumber();
3165 }
3166 return {
3167 artist: isVA ? VA : artist,
3168 album: album,
3169 album_year: albumYear,
3170 release_date: releaseDate,
3171 label: label,
3172 encoding: 'lossless',
3173 codec: 'FLAC',
3174 bd: bd || undefined,
3175 sr: sr || undefined,
3176 channels: channels || undefined,
3177 media: media,
3178 genre: genres.map(function(genre) {
3179 genre = genre.replace(/-+/g, ' ');
3180 qobuzTranslations.forEach(function(it) { if (genre.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) genre = it[1] });
3181 return genre;
3182 }).join('; '),
3183 discnumber: discNumber || 1,
3184 totaldiscs: totalDiscs,
3185 discsubtitle: discSubtitle,
3186 tracknumber: parseInt(tr.querySelector('span[itemprop="position"]').textContent),
3187 totaltracks: totalTracks,
3188 title: (tr.querySelector('div.track__item--name > span') || tr.querySelector('span.track__item--name'))
3189 .textContent.trim().replace(/\s+/g, ' '),
3190 track_artist: trackArtist,
3191 composer: trackComposers.length <= 0 ? composer : undefined,
3192 performers: trackPerformers.length > 0 ? trackPerformers : undefined,
3193 composers: trackComposers.length > 0 ? trackComposers : undefined,
3194 conductors: trackConductors.length > 0 ? trackConductors : undefined,
3195 remixers: trackRemixers.length > 0 ? trackRemixers : undefined,
3196 producers: trackProducers.length > 0 ? trackProducers : undefined,
3197 duration: timeStringToTime(tr.querySelector('span.track__item--duration').textContent),
3198 url: response.finalUrl,
3199 description: description,
3200 identifiers: mergeIds(),
3201 cover_url: imgUrl,
3202 };
3203 }));
3204 }
3205 }); else if (url.toLowerCase().includes('highresaudio.com/')) return globalFetch(url).then(function(response) {
3206 if (/\/album\/view\/(\w+)\//i.test(response.finalUrl)) identifiers.HRA_ID = RegExp.$1;
3207 artist = (ref = response.document.querySelector('h1 > span.artist')) != null ? ref.textContent.trim() : undefined;
3208 isVA = vaParser.test(artist);
3209 album = (ref = response.document.getElementById('h1-album-title')) != null ? ref.firstChild.textContent.trim() : undefined;
3210 response.document.querySelectorAll('div.album-col-info-data > div > p').forEach(function(k) {
3211 var key = k.firstChild.textContent, value = k.lastChild.textContent.trim();
3212 if (/\b(?:Genre|Subgenre)\b/i.test(key)) genres.push(value);
3213 else if (/\b(?:Label)\b/i.test(key)) label = value;
3214 else if (/\b(?:Album[\s\-]Release)\b/i.test(key)) albumYear = extractYear(value);
3215 else if (/\b(?:HRA[\s\-]Release)\b/i.test(key)) releaseDate = normalizeDate(value);
3216 });
3217 sr = undefined;
3218 response.document.querySelectorAll('tbody > tr > td.col-format').forEach(function(format) {
3219 if (!/^(FLAC)\s*(\d+(?:[\.\,]\d+)?)\b/.test(format.textContent)) return;
3220 if (sr) sr = NaN;
3221 if (sr != undefined) return;
3222 format = RegExp.$1;
3223 sr = parseFloat(RegExp.$2.replace(',', '.')) * 1000;
3224 });
3225 getDescription(response, 'div#albumtab-info > p', false);
3226 if (i = hraPdfBooklet(response)) {
3227 if (description) description += '\n\n'.concat(i); else description = i;
3228 }
3229 url = (ref = response.document.querySelector('meta[property="og:url"][content]')) != null && ref.content;
3230 if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
3231 trs = response.document.querySelectorAll('ul.playlist > li.pltrack');
3232 return Array.from(trs).map(function(tr) {
3233 discNumber = undefined; discSubtitle = tr;
3234 while ((discSubtitle = discSubtitle.previousElementSibling) != null) {
3235 if (discSubtitle.nodeName == 'LI' && discSubtitle.className == 'plinfo') {
3236 discSubtitle = discSubtitle.textContent.replace(/\s*:$/, '').trim();
3237 guessDiscNumber();
3238 break;
3239 }
3240 }
3241 return {
3242 artist: isVA ? VA : artist,
3243 album: album,
3244 album_year: albumYear,
3245 release_date: releaseDate,
3246 label: label,
3247 encoding: 'lossless',
3248 codec: 'FLAC',
3249 bd: 24,
3250 sr: sr || undefined,
3251 media: media,
3252 genre: genres.join('; '),
3253 discnumber: discNumber,
3254 totaldiscs: totalDiscs,
3255 discsubtitle: discSubtitle || undefined,
3256 tracknumber: parseInt(tr.querySelector('span.track').textContent) || tr.querySelector('span.track').textContent,
3257 totaltracks: trs.length,
3258 title: tr.querySelector('span.title').textContent.trim().replace(/\s+/g, ' '),
3259 duration: timeStringToTime(tr.querySelector('span.time').textContent),
3260 url: url || response.finalUrl,
3261 description: description,
3262 identifiers: mergeIds(),
3263 cover_url: imgUrl,
3264 };
3265 });
3266 }); else if (url.toLowerCase().includes('bandcamp.com/')) return globalFetch(url).then(function(response) {
3267 artist = Array.from(response.document.querySelectorAll('span[itemprop="byArtist"] > a')).map(a => a.textContent.trim());
3268 ref = response.document.querySelector('span.back-link-text > br');
3269 if (ref != null && ref.nextSibling != null) label = ref.nextSibling.textContent.trim(); else {
3270 ref = response.document.querySelector('p#band-name-location > span.title');
3271 if (ref != null) label = ref.textContent.trim();
3272 }
3273 var tags = new TagManager;
3274 response.document.querySelectorAll('div.tralbum-tags > a.tag').forEach(function(tag) {
3275 if (!artist.some(artist => tag.textContent.trim().toLowerCase() == artist.toLowerCase())) tags.add(tag.textContent.trim());
3276 });
3277 if ((ref = response.document.querySelector('div#tralbumArt > a.popupImage')) != null) imgUrl = ref.href;
3278 try {
3279 response.document.querySelectorAll('div#propOpenWrapper > div[id] > script').forEach(function(script) {
3280 if (!/\b(var\s+SiteData\s*=\s*\{[\S\s]+\};)/.test(script.text)) return;
3281 eval(RegExp.$1);
3282 if (typeof TralbumData != 'object') return;
3283 if (prefs.diag_mode) console.debug('BandCamp metadata loaded:', TralbumData);
3284 identifiers.BANDCAMP_ID = TralbumData.id;
3285 identifiers.RELEASETYPE = TralbumData.item_type;
3286 identifiers.BARCODE = TralbumData.current.upc/* || TralbumData.packages[0].upc*/;
3287 isVA = vaParser.test(TralbumData.artist);
3288 description = TralbumData.current.about;
3289 if (TralbumData.current.credits) if (description) description += '\n\n'.concat(TralbumData.current.credits);
3290 else description = TralbumData.current.credits;
3291 tracks = TralbumData.trackinfo.map(function(track) {
3292 trackIdentifiers = {
3293 TRACK_ID: track.track_id,
3294 //HASLYRICS: Number(track.has_lyrics) || 0,
3295 };
3296 return {
3297 artist: isVA ? VA : TralbumData.artist,
3298 album: TralbumData.current.title,
3299 release_date: TralbumData.current.release_date || TralbumData.album_release_date,
3300 description: description,
3301 label: /*TralbumData.packages[0].label || */label,
3302 //catalog: TralbumData.packages[0].sku,
3303 genre: tags.toString(),
3304 duration: track.duration || undefined,
3305 lyrics: track.lyrics || undefined,
3306 title: track.title,
3307 tracknumber: track.track_num,
3308 totaltracks: TralbumData.trackinfo.length,
3309 media: 'WEB',
3310 url: TralbumData.url ? TralbumData.url.replace(/^http\b/, 'https') : response.finalUrl,
3311 cover_url: imgUrl,
3312 identifiers: mergeIds(),
3313 };
3314 });
3315 });
3316 if (tracks.length <= 0) throw 'No tracks found';
3317 return tracks;
3318 } catch(e) {
3319 console.warn('BandCamp: falling back to HTML scraper for the reason:', e);
3320 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
3321 if ((ref = response.document.querySelector('h2[itemprop="name"]')) != null) album = ref.textContent.trim();
3322 ref = response.document.querySelector('div.tralbum-credits');
3323 if (ref != null && /\brelease[ds]\s+(.*?\b\d{4})\b/i.test(ref.textContent)) releaseDate = RegExp.$1;
3324 description = [];
3325 response.document.querySelectorAll('div.tralbumData').forEach(function(div) {
3326 if (!div.classList.contains('tralbum-tags')) description.push(html2php(div, response.finalUrl).trim());
3327 });
3328 description = description.filter(p => p).join('\n\n');
3329 if (/\bShare\.initPanel\s*\(\s*\w+\s*,\s*\d+\s*,\s*"https?(:\/\/\S+?)"\s*\);/i.test(response.responseText))
3330 var shareLink = 'https'.concat(RegExp.$1);
3331 trs = response.document.querySelectorAll('table.track_list > tbody > tr[itemprop="tracks"]');
3332 return Array.from(trs).map(tr => ({
3333 artist: isVA ? VA : undefined,
3334 artists: !isVA ? artist : undefined,
3335 album: album,
3336 //album_year: extractYear(releaseDate),
3337 release_date: releaseDate,
3338 label: label,
3339 media: media,
3340 genre: tags.toString(),
3341 discnumber: discNumber,
3342 totaldiscs: totalDiscs,
3343 tracknumber: parseInt(tr.querySelector('div.track_number').textContent) || tr.querySelector('div.track_number').textContent,
3344 totaltracks: trs.length,
3345 title: (tr.querySelector('div.title span.track-title')
3346 || tr.querySelector('div.title span[itemprop="name"]')).textContent.trim().replace(/\s+/g, ' '),
3347 duration: durationFromMeta(tr) || (ref = tr.querySelector('span.time')) != null && timeStringToTime(ref.textContent) || undefined,
3348 url: shareLink || response.finalUrl,
3349 description: description,
3350 identifiers: mergeIds(),
3351 cover_url: imgUrl,
3352 }));
3353 }
3354 }); else if (url.toLowerCase().includes('prestomusic.com/')) return globalFetch(url).then(function(response) {
3355 const parenthesesStripper = /\s+\([^\(\)]*\)/g;
3356 identifiers.COMPOSEREMPHASIS = 1;
3357 artist = getArtists(response.document.querySelectorAll('div.c-product-block__contributors > p'));
3358 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
3359 ref = response.document.querySelector('h1.c-product-block__title');
3360 if (ref != null) album = ref.lastChild.wholeText.trim();
3361 response.document.querySelectorAll('div.c-product-block__metadata > ul > li').forEach(function(li) {
3362 if (li.firstChild.textContent.includes('Release Date')) {
3363 releaseDate = li.lastChild.wholeText;
3364 if (/\b(\d+)\w*\s+(\w+)\s+(\d{4})\b/.test(releaseDate)) releaseDate = RegExp.$2 + ' ' + RegExp.$1 + ' ' + RegExp.$3;
3365 } else if (li.firstChild.textContent.includes('Label')) {
3366 label = li.lastChild.wholeText.trim();
3367 } else if (li.firstChild.textContent.includes('Catalogue No')) {
3368 catalogue = li.lastChild.wholeText.trim();
3369 }
3370 });
3371 composer = [];
3372 response.document.querySelectorAll('div#related > div > ul > li').forEach(function(li) {
3373 if (li.parentNode.previousElementSibling.textContent.includes('Composers')) {
3374 var _composer = li.firstChild.textContent.trim().replace(parenthesesStripper, '');
3375 composer.push(_composer.replace(/^(.*?)\s*,\s+(.*)$/, '$2 $1'));
3376 }
3377 });
3378 genres = undefined;
3379 if (/\/jazz\//i.test(response.finalUrl)) genres = 'Jazz';
3380 if (/\/classical\//i.test(response.finalUrl)) genres = 'Classical';
3381 getDescription(response, 'div#about > div > p', true);
3382 var personnel = [];
3383 response.document.querySelectorAll('div.c-product-block__contributors > p').forEach(function(p) {
3384 // TODO
3385 });
3386 if (personnel.length > 0) {
3387 if (description) description += '\n\n';
3388 description += personnel.join('\n');
3389 }
3390 ref = response.document.querySelector('div.c-product-block__aside > a');
3391 if (ref != null) imgUrl = ref.href.replace(/\?\d+$/, '');
3392 trs = response.document.querySelectorAll('div.has--sample');
3393 trackNumber = 0;
3394 return Array.from(trs).map(function(tr) {
3395 discNumber = discSubtitle = undefined;
3396 var parent = tr;
3397 if (tr.classList.contains('c-track')) {
3398 parent = tr.parentNode.parentNode;
3399 if (parent.classList.contains('c-expander')) parent = parent.parentNode;
3400 if ((ref = parent.querySelector(':scope > div > div > div > p.c-track__title')) != null) {
3401 discSubtitle = ref.textContent.trim().replace(/\s+/g, ' ');
3402 guessDiscNumber();
3403 }
3404 }
3405 trackArtist = getArtists(parent.querySelectorAll(':scope > div.c-track__details > ul > li'));
3406 return {
3407 artist: isVA ? VA : undefined,
3408 artists: !isVA ? artist : undefined,
3409 album: album,
3410 //album_year: extractYear(releaseDate),
3411 release_date: releaseDate,
3412 label: label,
3413 catalog: catalogue,
3414 media: media,
3415 genre: genres,
3416 discnumber: discNumber,
3417 totaldiscs: totalDiscs,
3418 discsubtitle: discSubtitle,
3419 tracknumber: ++trackNumber,
3420 totaltracks: trs.length,
3421 title: (ref = tr.querySelector('p.c-track__title')) ? ref.textContent.trim().replace(/\s+/g, ' ') : undefined,
3422 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
3423 composers: composer,
3424 duration: timeStringToTime(tr.querySelector('div.c-track__duration').textContent),
3425 description: description,
3426 url: response.finalUrl,
3427 cover_url: imgUrl,
3428 identifiers: mergeIds(),
3429 };
3430 });
3431
3432 function getArtists(nodeList) {
3433 var artists = [];
3434 nodeList.forEach(function(_artists) {
3435 _artists = _artists.textContent.trim();
3436 if (_artists.startsWith('Record')) return;
3437 Array.prototype.push.apply(artists,
3438 splitArtists(_artists.replace(parenthesesStripper, '')).filter(artist => artist.length > 0));
3439 });
3440 return artists.filter(artist => artist.length > 0);
3441 }
3442 }); else if (url.toLowerCase().includes('discogs.com/') && /\/(release|master|artist)s?\/(\d+)\b/i.test(url)) {
3443 if (RegExp.$1.toLowerCase() == 'artist') return Promise.reject('Discogs artists not parseable');
3444 if (RegExp.$1.toLowerCase() == 'master') return Promise.reject('Discogs masters as source aren\'t supported, pick a specific release');
3445 return queryDiscogsAPI('releases/' + RegExp.$2).then(function(release) {
3446 if (prefs.diag_mode) console.debug('Discogs release', release.id, 'metadata received:', release);
3447 const removeArtistNdx = /\s*\(\d+\)$/;
3448 const editionTest = /^(?:.+?\s+Edition|Remaster(?:ed)|Remasterizado|Remasterisée|Reissue|.+?\s+Release|Enhanced|Promo)$/;
3449 media = undefined;
3450 identifiers.DISCOGS_ID = release.id;
3451 var master = release.master_id ? queryDiscogsAPI('masters/' + release.master_id).then(function(master) {
3452 if (prefs.diag_mode) console.debug('Discogs master', master.id, 'metadata received:', master);
3453 return master;
3454 }) : Promise.reject('master release not available');
3455 var albumArtists = getArtists(release);
3456 if (albumArtists[0].length > 0) {
3457 artist = albumArtists[0].join(', ');
3458 if (albumArtists[1].length > 0) artist += ' feat. ' + albumArtists[1].join(', ');
3459 isVA = albumArtists[0].length == 1 && vaParser.test(albumArtists[0][0]);
3460 } else isVA = true;
3461 album = release.title;
3462 var editions = [];
3463 label = []; catalogue = [];
3464 release.labels.forEach(function(it) {
3465 if (it.entity_type != 1) return;
3466 if (it.name && !/^Not\s+On\s+Label\b/i.test(it.name)) label.pushUniqueCaseless(it.name.replace(removeArtistNdx, ''));
3467 if (it.catno) catalogue.pushUniqueCaseless(it.catno);
3468 });
3469 description = '';
3470 if (release.companies && release.companies.length > 0) {
3471 description = '[b]Companies, etc.[/b]\n';
3472 let type_names = new Set(release.companies.map(it => it.entity_type_name));
3473 type_names.forEach(function(type_name) {
3474 description += '\n' + type_name + ' – ' + release.companies
3475 .filter(it => it.entity_type_name == type_name)
3476 .map(function(it) {
3477 var result = '[url=' + discogsOrigin + '/label/' + it.id + ']' +
3478 it.name.replace(removeArtistNdx, '') + '[/url]';
3479 if (it.catno) result += ' – ' + it.catno;
3480 return result;
3481 })
3482 .join(', ');
3483 });
3484 }
3485 if (release.extraartists && release.extraartists.length > 0) {
3486 if (description) description += '\n\n';
3487 description += '[b]Credits[/b]\n';
3488 let roles = new Set(release.extraartists.map(it => it.role));
3489 roles.forEach(function(role) {
3490 description += '\n' + role + ' – ' + release.extraartists
3491 .filter(artist => artist.role == role)
3492 .map(function(artist) {
3493 var result = '[url=' + discogsOrigin + '/artist/' + artist.id + ']' +
3494 (artist.anv || artist.name).replace(removeArtistNdx, '') + '[/url]';
3495 if (artist.tracks) result += ' (tracks: ' + artist.tracks + ')';
3496 return result;
3497 })
3498 .join(', ');
3499 });
3500 }
3501 if (release.notes) {
3502 if (description) description += '\n\n';
3503 description += '[b]Notes[/b]\n\n' + release.notes.trim();
3504 }
3505 if (Array.isArray(release.identifiers) && release.identifiers.length > 0) {
3506 if (description) description += '\n\n';
3507 description += '[b]Barcode and Other Identifiers[/b]\n';
3508 release.identifiers.forEach(function(it) {
3509 description += '\n' + it.type;
3510 if (it.description) description += ' (' + it.description + ')';
3511 description += ': ' + it.value;
3512 });
3513 }
3514 [
3515 ['Single', 'Single'],
3516 ['EP', 'EP'],
3517 ['Compilation', 'Compilation'],
3518 ['Soundtrack', 'Soundtrack'],
3519 ].forEach(function(k) {
3520 if (release.formats.every(it => Array.isArray(it.descriptions) && it.descriptions.includesCaseless(k[0]))) {
3521 identifiers.RELEASETYPE = k[1];
3522 }
3523 });
3524 release.identifiers.forEach(function(id) {
3525 identifiers[id.type.toUpperCase().replace(/\s*\/\s*/g, '-').replace(/\W/g, '_')] = id.value;
3526 });
3527 release.formats.forEach(function(fmt) {
3528 if (editionTest.test(fmt.text)) editions.push(fmt.text);
3529 if (Array.isArray(fmt.descriptions)) fmt.descriptions.forEach(function(desc) {
3530 if (editionTest.test(desc)) editions.push(desc);
3531 });
3532 if (media) return;
3533 if (/\bFile\b/.test(fmt.name)) {
3534 media = 'WEB';
3535 if (['FLAC', 'WAV', 'AIF', 'AIFF', 'AIFC', 'PCM', 'ALAC', 'APE', 'WavPack']
3536 .some(k => fmt.descriptions.includes(k))) {
3537 encoding = 'lossless'; format = 'FLAC';
3538 } else if (fmt.descriptions.includes('AAC')) {
3539 encoding = 'lossy'; format = 'AAC'; bd = undefined;
3540 if (/(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
3541 } else if (fmt.descriptions.includes('MP3')) {
3542 encoding = 'lossy'; format = 'MP3'; bd = undefined;
3543 if (/\b(\d+)\s*kbps\b/i.test(fmt.text)) bitrate = parseInt(RegExp.$1);
3544 } else if (['DFF', 'DSD'].some(k => fmt.descriptions.includes(k))) {
3545 encoding = 'lossless';
3546 } else if (['AMR', 'MP2', 'ogg-vorbis', 'Opus', 'SHN', 'WMA'].some(k => fmt.descriptions.includes(k))) {
3547 encoding = 'lossy';
3548 }
3549 } else media = dcFmtToGazelle(fmt.name) || undefined;
3550 });
3551 if (editions.length > 0) album += ' (' + editions.join(' / ') + ')';
3552 totalTracks = release.tracklist.filter(track => track.type_.toLowerCase() == 'track').length;
3553 return master.catch(function(reason) {
3554 console.debug('Discogs master not received:', reason);
3555 if (prefs.messages_verbosity >= 1) addMessage(reason, 'notice');
3556 }).then(function(master) {
3557 var tags = new TagManager();
3558 if (release.genres) tags.add(...release.genres);
3559 if (release.styles) tags.add(...release.styles);
3560 if (master) {
3561 if (master.genres) tags.add(...master.genres);
3562 if (master.styles) tags.add(...master.styles);
3563 }
3564 const imageFilter = image => urlParser.test(image.resource_url || image.uri) && ['primary', 'front'].includes(image.type);
3565 imgUrl = master && Array.isArray(master.images) ? master.images.filter(imageFilter) : [];
3566 if (imgUrl.length <= 0 && Array.isArray(release.images)) imgUrl = release.images.filter(imageFilter);
3567 release.tracklist.forEach(function(track) {
3568 switch (track.type_.toLowerCase()) {
3569 case 'heading':
3570 discSubtitle = track.title;
3571 break;
3572 case 'track': {
3573 trackIdentifiers = {};
3574 if (/^([a-zA-Z]+)?(\d+)-(\w+)$/.test(track.position)) {
3575 if (RegExp.$1) trackIdentifiers.VOL_MEDIA = RegExp.$1;
3576 discNumber = RegExp.$2;
3577 trackNumber = RegExp.$3;
3578 } else {
3579 discNumber = undefined;
3580 trackNumber = track.position;
3581 }
3582 let trackArtists = getArtists(track);
3583 if (trackArtists[0].length > 0 && !trackArtists[0].equalCaselessTo(albumArtists[0])
3584 || trackArtists[1].length > 0 && !trackArtists[1].equalCaselessTo(albumArtists[1])) {
3585 trackArtist = (trackArtists[0].length > 0 ? trackArtists : albumArtists)[0].join(', ');
3586 if (trackArtists[1].length > 0) trackArtist += ' feat. ' + trackArtists[1].join(', ');
3587 } else trackArtist = undefined;
3588 let performer = Array.isArray(track.extraartists) && track.extraartists
3589 .map(artist => (artist.anv || artist.name).replace(removeArtistNdx, ''))
3590 .filter(function(artist) {
3591 return !albumArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
3592 && !trackArtists.slice(2).some(it => Array.isArray(it) && it.includes(artist))
3593 });
3594 tracks.push({
3595 artist: isVA ? VA : artist,
3596 //artists: !isVA ? albumArtists[0] : undefined,
3597 album: album,
3598 album_year: master ? master.year : undefined,
3599 release_date: release.released,
3600 label: label.join(' / '),
3601 catalog: catalogue.join(' / '),
3602 country: release.country,
3603 encoding: encoding,
3604 codec: format,
3605 bitrate: bitrate,
3606 bd: bd,
3607 media: media,
3608 genre: tags.toString(),
3609 discnumber: discNumber,
3610 totaldiscs: release.format_quantity,
3611 discsubtitle: discSubtitle,
3612 tracknumber: trackNumber,
3613 totaltracks: totalTracks,
3614 title: track.title,
3615 track_artist: trackArtist,
3616 performer: Array.isArray(performer) && performer.join('; ') || undefined,
3617 composers: stringyfyRole(3),
3618 conductors: stringyfyRole(4),
3619 remixers: stringyfyRole(2),
3620 compilers: stringyfyRole(5),
3621 producers: stringyfyRole(6),
3622 duration: timeStringToTime(track.duration),
3623 description: description,
3624 identifiers: mergeIds(),
3625 cover_url: imgUrl.length > 0 ? imgUrl[0].resource_url || imgUrl[0].uri : undefined,
3626 });
3627
3628 function stringyfyRole(ndx) {
3629 var arr = (Array.isArray(trackArtists[ndx]) && trackArtists[ndx].length > 0 ? trackArtists : albumArtists)[ndx];
3630 return arr.length > 0 ? arr : undefined;
3631 }
3632 }
3633 }
3634 });
3635 return tracks;
3636 });
3637
3638 function getArtists(root) {
3639 function filterArtists(rx, anv = true) {
3640 return Array.isArray(root.extraartists) && rx instanceof RegExp ?
3641 root.extraartists.filter(it => rx.test(it.role))
3642 .map(it => (anv && it.anv || it.name || '').replace(removeArtistNdx, '')) : [];
3643 }
3644 var artists = [];
3645 for (var ndx = 0; ndx < 7; ++ndx) artists[ndx] = [];
3646 ndx = 0;
3647 if (root.artists) root.artists.forEach(function(it) {
3648 artists[ndx].push((it.anv || it.name).replace(removeArtistNdx, ''));
3649 if (/^feat/i.test(it.join)) ndx = 1;
3650 });
3651 return [
3652 artists[0],
3653 artists[1].concat(filterArtists(/^(?:featuring)$/i)),
3654 artists[2].concat(filterArtists(/\b(?:Remixed[\s\-]By|Remixer)\b/i)),
3655 artists[3].concat(filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, false)),
3656 artists[4].concat(filterArtists(/\b(?:Conducted[\s\-]By|Conductor)\b/i)),
3657 artists[5].concat(filterArtists(/\b(?:Compiled[\s\-]By|Compiler)\b/i)),
3658 artists[6].concat(filterArtists(/\b(?:Produced[\s\-]By|Producer)\b/i)),
3659 // filter off from performers
3660 filterArtists(/\b(?:(?:Mixed)[\s\-]By|Mixer)\b/i),
3661 filterArtists(/\b(?:(?:Written|Composed)[\s\-]By|Composer)\b/i, true),
3662 ];
3663 }
3664 });
3665 } else if (url.toLowerCase().includes('supraphonline.cz/')) return globalFetch(url.replace(/\?.*$/, '')).then(function(response) {
3666 const copyrightParser = /^(?:\([PC]\)|℗|©)$/i;
3667 var ndx, conductor = [], origin = new URL(response.finalUrl).origin;
3668 genres = undefined; artist = [];
3669 response.document.querySelectorAll('h2.album-artist > a').forEach(function(it) {
3670 artist.pushUnique(it.title);
3671 });
3672 if (artist.length == 0 && (ref = response.document.querySelector('h2.album-artist[title]')) != null) {
3673 isVA = vaParser.test(ref.title);
3674 }
3675 ref = response.document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]');
3676 if (ref != null && vaParser.test(ref.content)) isVA = true;
3677 if (isVA) artist = [];
3678 if ((ref = response.document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim();
3679 if ((ref = response.document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
3680 if ((ref = response.document.querySelector('meta[itemprop="genre"]')) != null) genres = ref.content;
3681 if ((ref = response.document.querySelector('li.album-version > div.selected > div')) != null) {
3682 if (/\b(?:MP3)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossy'; format = 'MP3'; }
3683 if (/\b(?:FLAC)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 16; }
3684 if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bd = 24; }
3685 if (/\b(?:CD)\b/.test(ref.textContent)) { media = 'CD'; }
3686 if (/\b(?:LP)\b/.test(ref.textContent)) { media = 'Vinyl'; }
3687 }
3688 response.document.querySelectorAll('ul.summary > li').forEach(function(it) {
3689 if (it.childElementCount <= 0) return;
3690 if (it.firstElementChild.textContent.includes('Nosič')) media = it.lastChild.textContent.trim();
3691 if (it.firstElementChild.textContent.includes('Datum vydání')) releaseDate = normalizeDate(it.lastChild.textContent);
3692 if (it.firstElementChild.textContent.includes('První vydání')) albumYear = extractYear(it.lastChild.data);
3693 //if (it.firstElementChild.textContent.includes('Žánr')) genre = it.lastChild.textContent.trim();
3694 if (it.firstElementChild.textContent.includes('Vydavatel')) label = it.lastChild.textContent.trim();
3695 if (it.firstElementChild.textContent.includes('Katalogové číslo')) catalogue = it.lastChild.textContent.trim();
3696 if (it.firstElementChild.textContent.includes('Formát')) {
3697 if (/\b(?:FLAC|WAV|AIFF?)\b/.test(it.lastChild.textContent)) { encoding = 'lossless'; format = 'FLAC'; }
3698 if (/\b(\d+)[\-\s]?bits?\b/i.test(it.lastChild.textContent)) bd = parseInt(RegExp.$1);
3699 if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(it.lastChild.textContent)) sr = parseFloat(RegExp.$1.replace(',', '.')) * 1000;
3700 }
3701 if (it.firstElementChild.textContent.includes('Celková stopáž')) totalTime = timeStringToTime(it.lastChild.textContent.trim());
3702 if (copyrightParser.test(it.firstElementChild.textContent) && !albumYear) albumYear = extractYear(it.lastChild.data);
3703 });
3704 const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace'];
3705 artists = [];
3706 for (i = 0; i < 4; ++i) artists[i] = {};
3707 response.document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) {
3708 if ((ref = it.querySelector('h3')) != null) {
3709 ndx = undefined;
3710 creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx });
3711 } else {
3712 if (typeof ndx != 'number') return;
3713 let role;
3714 if (ndx == 2) role = 'ensemble';
3715 else if ((ref = it.querySelector('span')) != null) role = translateRole(ref);
3716 if ((ref = it.querySelector('a')) != null) {
3717 if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = [];
3718 var href = new URL(ref.href);
3719 artists[ndx][role].pushUnique([ref.textContent.trim(), origin + href.pathname]);
3720 }
3721 }
3722 });
3723 getDescription(response, 'div[itemprop="description"] p', true);
3724 composer = [];
3725 var performers = [], DJs = [];
3726 function dumpArtist(ndx, role) {
3727 if (!role || role == 'undefined') return;
3728 if (description.length > 0) description += '\n' ;
3729 description += '[color=#9576b1]' + role + '[/color] – ';
3730 //description += artists[ndx][role].map(artist => '[artist]' + artist[0] + '[/artist]').join(', ');
3731 description += artists[ndx][role].map(artist => '[url=' + artist[1] + ']' + artist[0] + '[/url]').join(', ');
3732 }
3733 for (i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers
3734 var a = artists[i][role].map(a => a[0]);
3735 artist.pushUnique(...a);
3736 (['conductor', 'choirmaster'].includes(role) ? conductor : role == 'DJ' ? DJs : performers).pushUnique(...a);
3737 if (i != 2) dumpArtist(i, role);
3738 });
3739 Object.keys(artists[0]).forEach(function(role) { // composers
3740 composer.pushUnique(...artists[0][role].map(it => it[0])
3741 .filter(it => !pseudoArtistParsers.some(rx => rx.test(it))));
3742 dumpArtist(0, role);
3743 });
3744 Object.keys(artists[3]).forEach(role => { dumpArtist(3, role) }); // ADC & mastering
3745 ref = response.document.querySelector('meta[itemprop="image"]');
3746 if (ref != null)imgUrl = ref.content.replace(/\?.*$/, '');
3747 var promises = [];
3748 response.document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(row) {
3749 promises.push(row.id && (ref = row.querySelector('td > a.trackdetail')) != null ?
3750 globalFetch(origin + ref.pathname + ref.search, { context: parseInt(row.id.replace(/^track-/i, '')) })
3751 .then(function(response) {
3752 var track = response.document.getElementById('track-' + response.context);
3753 if (track == null) return Promise.reject('Track detail not located');
3754 return [track, response.document.querySelector('div[data-swap="trackdetail-' +
3755 response.context + '"] > div > div.row')];
3756 })
3757 : Promise.resolve([row, null]));
3758 });
3759 return Promise.all(promises).then(function(rows) {
3760 rows.forEach(function(tr) {
3761 if (!(tr[0] instanceof HTMLElement)) throw new Error('Assertion failed: tr[0] != HTMLElement');
3762 if (tr[0].id && tr[0].classList.contains('track')) {
3763 tr[2] = [];
3764 for (i = 0; i < 8; ++i) tr[2][i] = [];
3765 if (!(tr[1] instanceof HTMLElement)) return;
3766 tr[1].querySelectorAll('div[class]:nth-of-type(2) > ul > li > span').forEach(function(li) {
3767 function oneOf(...arr) { return arr.some(role => key == role) }
3768 var key = translateRole(li);
3769 var val = li.nextElementSibling.textContent.trim();
3770 if (pseudoArtistParsers.some(rx => rx.test(val))) return;
3771 if (key.startsWith('remix')) {
3772 tr[2][2].pushUnique(val);
3773 } else if (oneOf('music', 'lyrics', 'music+lyrics', 'original lyrics', 'czech lyrics', 'libreto', 'music improvisation', 'author')) {
3774 tr[2][3].pushUnique(val);
3775 } else if (oneOf('conductor', 'choirmaster')) {
3776 tr[2][4].pushUnique(val);
3777 } else if (key == 'DJ') {
3778 tr[2][5].pushUnique(val);
3779 } else if (key == 'produced by') {
3780 tr[2][6].pushUnique(val);
3781 } else if (key == 'recorded by') {
3782 } else {
3783 tr[2][7].pushUnique(val);
3784 }
3785 });
3786 }
3787 });
3788 var guests = rows.filter(tr => tr.length >= 3).map(it => it[2][7])
3789 .reduce((acc, trpf) => trpf.filter(trpf => acc.includes(trpf)))
3790 .filter(it => !artist.includes(it));
3791 rows.forEach(function(tr) {
3792 if (tr[0].classList.contains('cd-header')) {
3793 discNumber = /\b\d+\b/.test(tr[0].querySelector('h3').firstChild.data.trim())
3794 && parseInt(RegExp.lastMatch) || undefined;
3795 }
3796 if (tr[0].classList.contains('song-header')) discSubtitle = tr[0].firstElementChild.title.trim() || undefined;
3797 if (tr[0].id && tr[0].classList.contains('track')) {
3798 var copyright, trackGenre, trackYear, recordPlace, recordDate, trackIdentifiers = {};
3799 if (/^track-(\d+)$/i.test(tr[0].id)) trackIdentifiers.TRACK_ID = RegExp.$1;
3800 if (tr[1] instanceof HTMLElement) {
3801 tr[1].querySelectorAll('div[class]:nth-of-type(1) > ul > li > span').forEach(function(li) {
3802 if (li.textContent.startsWith('Nahrávka dokončena')) {
3803 trackIdentifiers.RECYEAR = extractYear(recordDate = li.nextSibling.data.trim());
3804 }
3805 if (li.textContent.startsWith('Místo nahrání')) {
3806 recordPlace = li.nextSibling.data.trim();
3807 }
3808 if (li.textContent.startsWith('Rok prvního vydání')) {
3809 trackIdentifiers.PUBYEAR = (trackYear = parseInt(li.nextSibling.data));
3810 }
3811 //if (copyrightParser.test(li.textContent)) copyright = li.nextSibling.data.trim();
3812 if (li.textContent.startsWith('Žánr')) trackGenre = li.nextSibling.data.trim();
3813 });
3814 }
3815 tracks.push({
3816 artist: isVA ? VA : undefined,
3817 artists: !isVA ? artist : undefined,
3818 album: album,
3819 album_year: /*trackYear || */albumYear || undefined,
3820 release_date: releaseDate,
3821 label: label,
3822 catalog: catalogue,
3823 encoding: encoding,
3824 codec: format,
3825 bd: bd,
3826 sr: sr || undefined,
3827 media: media,
3828 genre: translateGenre(genres) + ' | ' + translateGenre(trackGenre),
3829 discnumber: discNumber,
3830 totaldiscs: totalDiscs,
3831 discsubtitle: discSubtitle,
3832 tracknumber: /^\s*(\d+)\.?\s*$/.test(tr[0].firstElementChild.firstChild.textContent) ?
3833 parseInt(RegExp.$1) || RegExp.$1 : undefined,
3834 totaltracks: totalTracks,
3835 title: tr[0].querySelector('meta[itemprop="name"]').content,
3836 track_artists: tr[2][0].length > 0 && (isVA || !tr[2][0].equalCaselessTo(artist)) ? tr[2][0] : undefined,
3837 performers: tr[2][7].length > 0 ? tr[2][7] : performers,
3838 composers: tr[2][3].length > 0 ? tr[2][3] : composer,
3839 conductors: tr[2][4].length > 0 ? tr[2][4] : conductor,
3840 remixers: tr[2][2],
3841 compilers: tr[2][5].length > 0 ? tr[2][5] : DJs,
3842 producers: tr[2][6],
3843 duration: durationFromMeta(tr[0]),
3844 url: response.finalUrl,
3845 description: description,
3846 identifiers: mergeIds(),
3847 cover_url: imgUrl,
3848 });
3849 }
3850 });
3851 return tracks;
3852 });
3853
3854 function translateGenre(genre) {
3855 if (!genre || typeof genre != 'string') return undefined;
3856 [
3857 ['Orchestrální hudba', 'Orchestral Music'],
3858 ['Komorní hudba', 'Chamber Music'],
3859 ['Vokální', 'Classical, Vocal'],
3860 ['Klasická hudba', 'Classical'],
3861 ['Melodram', 'Classical, Melodram'],
3862 ['Symfonie', 'Symphony'],
3863 ['Vánoční hudba', 'Christmas Music'],
3864 [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'],
3865 ['Dechová hudba', 'Brass Music'],
3866 ['Elektronika', 'Electronic'],
3867 ['Folklor', 'Folclore, World Music'],
3868 ['Instrumentální hudba', 'Instrumental'],
3869 ['Latinské rytmy', 'Latin'],
3870 ['Meditační hudba', 'Meditative'],
3871 ['Vojenská hudba', 'Military Music'],
3872 ['Pro děti', 'Children'],
3873 ['Pro dospělé', 'Adult'],
3874 ['Mluvené slovo', 'Spoken Word'],
3875 ['Audiokniha', 'audiobook'],
3876 ['Humor', 'humour'],
3877 ['Pohádka', 'Fairy-Tale'],
3878 ].forEach(function(subst) {
3879 if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase()
3880 || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1];
3881 });
3882 return genre;
3883 }
3884 function translateRole(elem) {
3885 if (!(elem instanceof HTMLElement)) return undefined;
3886 var role = elem.textContent.trim().toLowerCase().replace(/\s*:.*$/, '');
3887 [
3888 [/\b(?:klavír)\b/, 'piano'],
3889 [/\b(?:housle)\b/, 'violin'],
3890 [/\b(?:varhany)\b/, 'organ'],
3891 [/\b(?:cembalo)\b/, 'harpsichord'],
3892 [/\b(?:trubka)\b/, 'trumpet'],
3893 [/\b(?:soprán)\b/, 'soprano'],
3894 [/\b(?:alt)\b/, 'alto'],
3895 [/\b(?:baryton)\b/, 'baritone'],
3896 [/\b(?:bas)\b/, 'basso'],
3897 [/\b(?:syntezátor)\b/, 'synthesizer'],
3898 [/\b(?:zpěv)\b/, 'vocals'],
3899 [/^(?:čte|četba)$/, 'narration'],
3900 ['vypravuje', 'narration'],
3901 ['komentář', 'commentary'],
3902 ['hovoří', 'spoken by'],
3903 ['hovoří a zpívá', 'speaks and sings'],
3904 ['improvizace', 'improvisation'],
3905 ['hudební těleso', 'ensemble'],
3906 ['hudba', 'music'],
3907 ['text', 'lyrics'],
3908 ['hudba+text', 'music+lyrics'],
3909 ['původní text', 'original lyrics'],
3910 ['český text', 'czech lyrics'],
3911 ['hudební improvizace', 'music improvisation'],
3912 ['autor', 'author'],
3913 ['účinkuje', 'participating'],
3914 ['řídí', 'conductor'],
3915 ['dirigent', 'conductor'],
3916 ['sbormistr', 'choirmaster'],
3917 ['produkce', 'produced by'],
3918 ['nahrál', 'recorded by'],
3919 ['digitální přepis', 'A/D transfer'],
3920 ].forEach(function(subst) {
3921 if (typeof subst[0] == 'string' && role.toLowerCase() == subst[0].toLowerCase()
3922 || subst[0] instanceof RegExp && subst[0].test(role)) role = role.replace(subst[0], subst[1]);
3923 });
3924 return role;
3925 }
3926 }); else if (url.toLowerCase().includes('bontonland.cz/')) return globalFetch(url).then(function(response) {
3927 ref = response.document.querySelector('div#detailheader > h1');
3928 if (ref != null && /^(.*?)\s*:\s*(.*)$/.test(ref.textContent.trim())) {
3929 artist = RegExp.$1;
3930 isVA = vaParser.test(artist);
3931 album = RegExp.$2;
3932 }
3933 media = 'CD';
3934 response.document.querySelectorAll('table > tbody > tr > td.nazevparametru').forEach(function(it) {
3935 if (it.textContent.includes('Datum vydání')) {
3936 releaseDate = normalizeDate(it.nextElementSibling.textContent);
3937 albumYear = extractYear(it.nextElementSibling.textContent);
3938 } else if (it.textContent.includes('Nosič / počet')) {
3939 if (/^(.*?)\s*\/\s*(.*)$/.test(it.nextElementSibling.textContent)) {
3940 media = RegExp.$1;
3941 totalDiscs = RegExp.$2;
3942 }
3943 } else if (it.textContent.includes('Interpret')) {
3944 artist = it.nextElementSibling.textContent.trim();
3945 } else if (it.textContent.includes('EAN')) {
3946 identifiers.BARCODE = it.nextElementSibling.textContent.trim();
3947 }
3948 });
3949 getDescription(response, 'div#detailtabpopis > div[class^="pravy"] > div > p:not(:last-of-type)', true);
3950 if ((ref = response.document.querySelector('a.detailzoom')) != null) imgUrl = ref.href;
3951 if ((ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > ol')) != null) {
3952 return Array.from(ref.querySelectorAll('li')).map(function(track, ndx, arr) {
3953 title = track.innerText.trim();
3954 duration = undefined;
3955 if (/^(.*?)\s+\(((?:\d+:)?\d+:\d+)\)$/.test(title) || /^(.*?)\s+\(((?:\d+:)?\d+:\d+)\)$/.test(title)) {
3956 title = RegExp.$1;
3957 duration = timeStringToTime(RegExp.$2);
3958 }
3959 return {
3960 artist: isVA ? VA : artist,
3961 album: album,
3962 //album_year: extractYear(releaseDate),
3963 release_date: releaseDate,
3964 label: label,
3965 media: media,
3966 tracknumber: ndx + 1,
3967 totaltracks: arr.length,
3968 title: title,
3969 duration: duration,
3970 url: response.finalUrl.replace(/\?.*$/, ''),
3971 description: description,
3972 identifiers: mergeIds(),
3973 cover_url: imgUrl,
3974 };
3975 });
3976 } else if ((ref = response.document.querySelector('div#detailtabpopis > div[class^="pravy"] > div > p:last-of-type')) != null) {
3977 const plParser = /^(\d+)(?:\s*[\/\.\-\:\)])?\s+(.*?)(?:\s+((?:(?:\d+:)?\d+:)?\d+))?$/;
3978 var trackList = html2php(ref, response.finalUrl).trim().split(/[\r\n]+/)
3979 .filter(it => plParser.test(it.trim())).map(it => plParser.exec(it.trim()));
3980 return Array.from(trackList).map(track => ({
3981 artist: isVA ? VA : artist,
3982 album: album,
3983 //album_year: extractYear(releaseDate),
3984 release_date: releaseDate,
3985 label: label,
3986 media: media,
3987 tracknumber: track[1],
3988 totaltracks: trackList.length,
3989 title: track[2],
3990 duration: timeStringToTime(track[3]),
3991 url: response.finalUrl.replace(/\?.*$/, ''),
3992 description: description,
3993 identifiers: mergeIds(),
3994 cover_url: imgUrl,
3995 }));
3996 } else throw 'Playlist could not be located';
3997 }); else if (url.toLowerCase().includes('nativedsd.com/')) return globalFetch(url).then(function(response) {
3998 identifiers.COMPOSEREMPHASIS = 1;
3999 artist = (ref = response.document.querySelector('div.the-content > header > h2')) != null ?
4000 ref.firstChild.data.trim() : undefined;
4001 isVA = !artist || vaParser.test(artist);
4002 if ((ref = response.document.querySelector('div.the-content > header > h1')) != null) album = ref.firstChild.data.trim();
4003 if ((ref = response.document.querySelector('div.the-content > header > h3')) != null) composer = ref.firstChild.data.trim();
4004 if ((ref = response.document.querySelector('div.the-content > header > h1 > small')) != null)
4005 albumYear = extractYear(ref.firstChild.data);
4006 ref = response.document.querySelector('div#breadcrumbs > div[class] > a:nth-of-type(2)');
4007 if (ref != null) label = ref.firstChild.data.trim();
4008 if (label == 'Albums') label = undefined;
4009 if ((ref = response.document.querySelector('h2#sku')) != null) {
4010 if (/^\s*(?:Catalog\sNumber):\s*(.*?)\s*$/im.test(ref.textContent)) catalogue = RegExp.$1;
4011 if (/^\s*(?:Released\son\sNativeDSD):\s*(.*?)\s*$/im.test(ref.textContent)) releaseDate = RegExp.$1;
4012 if (/^\s*(?:ID):\s*(.*?)\s*$/im.test(ref.textContent)) identifiers.NATIVEDSD_ID = RegExp.$1;
4013 }
4014 identifiers.ORIGINALFORMAT = 'DSD';
4015 getDescription(response, 'div.the-content > div.entry > p', false);
4016 if ((ref = response.document.querySelector('div#repertoire > div > p')) != null) {
4017 let repertoire = html2php(ref, url);
4018 if (description) description += '\n\n';
4019 let ndx = repertoire.indexOf('\n[b]Track');
4020 description += (ndx >= 0 ? repertoire.slice(0, ndx) : repertoire).trim().flatten();
4021 }
4022 ref = response.document.querySelectorAll('div#techspecs > table > tbody > tr');
4023 if (ref.length > 0) {
4024 if (description) description += '\n\n';
4025 description += '[b][u]Tech specs[/u][/b]';
4026 ref.forEach(function(it) {
4027 description += '\n[b]'.concat(it.children[0].textContent.trim(), '[/b] ', it.children[1].textContent.trim());
4028 });
4029 }
4030 if ((ref = response.document.querySelector('a#album-cover')) != null) imgUrl = ref.href;
4031 trs = response.document.querySelectorAll('div#track-list > table > tbody > tr[id^="track"]');
4032 return Array.from(trs).map(function(tr) {
4033 title = undefined;
4034 trackIdentifiers = { TRACK_ID: tr.id.replace(/^track-/i, '') };
4035 var trackComposer;
4036 if ((ref = tr.children[1]) != null) {
4037 title = ref.firstChild.textContent.trim();
4038 trackComposer = ref.childNodes[2] && ref.childNodes[2].textContent.trim() || undefined;
4039 }
4040 return {
4041 artist: isVA ? VA : artist,
4042 album: album,
4043 album_year: albumYear,
4044 release_date: releaseDate,
4045 label: label,
4046 catalog: catalogue,
4047 encoding: 'lossless',
4048 codec: 'FLAC',
4049 bd: 24,
4050 sr: 88200,
4051 media: media,
4052 genre: genres.join('; '), // 'Jazz'
4053 discnumber: discNumber,
4054 totaldiscs: totalDiscs,
4055 discsubtitle: discSubtitle,
4056 tracknumber: (ref = tr.firstElementChild.firstElementChild) != null ?
4057 parseInt(ref.firstChild.data.trim().replace(/\..*$/, '')) : undefined,
4058 totaltracks: trs.length,
4059 title: title,
4060 composer: trackComposer || composer,
4061 duration: (ref = tr.children[2]) != null ? timeStringToTime(ref.firstChild.data) : undefined,
4062 url: response.finalUrl,
4063 description: description,
4064 identifiers: mergeIds(),
4065 cover_url: imgUrl,
4066 };
4067 });
4068 });/* else if (url.toLowerCase().includes('junodownload.com/') && /\/([\d\-]+)\/?$/.test(new URL(url).pathname)) {
4069 let productKey = RegExp.$1;
4070 return globalFetch('https://www.junodownload.com/api/1.2/playlist/getplaylistdetails/?product_key='.concat(productKey), {
4071 responseType: 'xml',
4072 }).then(response => Array.from(response.document.querySelectorAll('playlist > trackList > track')).map(function(track, index, trackList) {
4073 artist = Array.from(track.querySelectorAll('extension > release_artists > artist > name'))
4074 .map(artist => artist.textContent.trim());
4075 isVA = artist.length == 1 && vaParser.test(artist[0]);
4076 trackArtist = Array.from(track.querySelectorAll('extension > artists > artist > name'))
4077 .map(artist => artist.textContent.trim());
4078 trackArtist = isVA || !trackArtist.equalCaselessTo(artist) ? joinArtists(trackArtist) : undefined;
4079 title = getValue('extension > track_title');
4080 if (getValue('extension > mix_title')) title += ' (' + getValue('extension > mix_title') + ')';
4081 return {
4082 artist: isVA ? VA : artist.join(', '),
4083 album: getValue('album'),
4084 release_date: getValue('extension > relDate'),
4085 label: getValue('extension > label > name'),
4086 catalog: getValue('extension > catNumber'),
4087 media: media,
4088 genre: getValue('extension > genre'),
4089 tracknumber: parseInt(getValue('trackNum')),
4090 totaltracks: trackList.length,
4091 title: getValue('extension > track_title'),
4092 track_artist: trackArtist,
4093 duration: parseInt(getValue('extension > length')) || undefined,
4094 description: getValue('extension > rating_comment'),
4095 identifiers: { JUNODOWNLOAD_ID: productKey },
4096 cover_url: getValue('image'),
4097 };
4098
4099 function getValue(selector) {
4100 var node = track.querySelector(selector);
4101 return node != null ? node.textContent.trim() : undefined;
4102 }
4103 }));
4104 }*/ else if (url.toLowerCase().includes('junodownload.com/')) return globalFetch(url).then(function(response) {
4105 if (/'id':'([\d\-]+)'/.test(response.responseText) || /\/([\d\-]+)\/?$/.test(new URL(response.finalUrl).pathname)) {
4106 identifiers.JUNODOWNLOAD_ID = RegExp.$1;
4107 var metaData = globalFetch('https://www.junodownload.com/api/1.2/playlist/getplaylistdetails/?product_key='
4108 .concat(identifiers.JUNODOWNLOAD_ID), { responseType: 'xml' }).then(function(response) {
4109 return Array.from(response.document.querySelectorAll('playlist > trackList > track'));
4110 });
4111 } else metaData = Promise.reject('No Id');
4112 var productArtist;
4113 if ((ref = response.document.querySelectorAll('div.breadcrumb_text > span:not([class])')).length == 4) {
4114 artist = Array.from(ref[ref.length - 1].querySelectorAll('a')).map(elem => elem.textContent.trim());
4115 productArtist = ref[ref.length - 1].textContent.trim();
4116 } else if ((ref = response.document.querySelector('h2.product-artist')) != null) {
4117 artist = Array.from(ref.querySelectorAll('a')).map(elem => elem.textContent.trim().titleCase());
4118 productArtist = ref.textContent.trim().titleCase();
4119 }
4120 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4121 if ((ref = response.document.querySelector('meta[itemprop="name"]')) != null) album = ref.content.trim();
4122 if ((ref = response.document.querySelector('meta[itemprop="author"]')) != null) label = ref.content.trim();
4123 if ((ref = response.document.querySelector('span[itemprop="datePublished"]')) != null)
4124 releaseDate = ref.firstChild.data.trim();
4125 response.document.querySelectorAll('div.mb-3 > strong').forEach(function(it) {
4126 if (it.textContent.startsWith('Genre')) {
4127 ref = it;
4128 while ((ref = ref.nextElementSibling) != null && ref.nodeName == 'A') genres.push(ref.textContent.trim());
4129 } else if (it.textContent.startsWith('Cat')) {
4130 if ((ref = it.nextSibling) != null && ref.nodeType == Node.TEXT_NODE) catalogue = ref.wholeText;
4131 }
4132 });
4133 getDescription(response, 'div[itemprop="review"]');
4134 if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
4135 trs = response.document.querySelectorAll('div.product-tracklist > div[itemprop="track"]');
4136 return Array.from(trs).map(function(tr) {
4137 trackIdentifiers = { BPM: tr.children[2].textContent.trim() };
4138 trackNumber = undefined;
4139 tr.querySelector('div.track-title').childNodes.forEach(function(n) {
4140 if (trackNumber || n.nodeType != Node.TEXT_NODE) return;
4141 trackNumber = n.data.trim().replace(/\s*\..*$/, '');
4142 });
4143 trackArtist = (ref = tr.querySelector('meta[itemprop="byArtist"]')) != null ? ref.content : undefined;
4144 title = (ref = tr.querySelector('span[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
4145 if (title && trackArtist && title.startsWith(trackArtist + ' - ')) title = title.slice(trackArtist.length + 3);
4146 return {
4147 artist: isVA ? VA : productArtist,
4148 artists: !isVA ? artist : undefined,
4149 album: album,
4150 album_year: extractYear(releaseDate),
4151 release_date: releaseDate,
4152 label: label,
4153 catalog: catalogue,
4154 media: media,
4155 genre: genres.join('; '),
4156 discnumber: discNumber,
4157 totaldiscs: totalDiscs,
4158 discsubtitle: discSubtitle,
4159 tracknumber: trackNumber,
4160 totaltracks: trs.length,
4161 title: title,
4162 track_artist: trackArtist && (isVA || trackArtist.toLowerCase() != productArtist.toLowerCase()) ? trackArtist : undefined,
4163 duration: durationFromMeta(tr),
4164 url: !identifiers.JUNODOWNLOAD_ID ? response.finalUrl : undefined,
4165 description: description,
4166 identifiers: mergeIds(),
4167 cover_url: imgUrl,
4168 };
4169 });
4170 }); else if (/\b(?:hdtracks)(?:\.\w+)+\//i.test(url)) return globalFetch(url).then(function(response) {
4171 response.document.querySelectorAll('div.album-main-details > ul > li > span').forEach(function(it) {
4172 if (it.textContent.startsWith('Title')) album = it.nextSibling.data.trim();
4173 if (it.textContent.startsWith('Artist')) artist = it.nextElementSibling.textContent.trim();
4174 if (it.textContent.startsWith('Genre')) {
4175 ref = it;
4176 while ((ref = ref.nextElementSibling) != null) genres.push(ref.textContent.trim());
4177 }
4178 if (it.textContent.startsWith('Label')) label = it.nextElementSibling.textContent.trim();
4179 if (it.textContent.startsWith('Release Date')) releaseDate = normalizeDate(it.nextSibling.data.trim());
4180 });
4181 isVA = vaParser.test(artist);
4182 if ((ref = response.document.querySelector('p.product-image > img')) != null) imgUrl = ref.src;
4183 trs = response.document.querySelectorAll('table#track-table > tbody > tr[id^="track"]');
4184 return Array.from(trs).map(function(tr) {
4185 format = tr.querySelector('td:nth-of-type(4) > span').textContent.trim();
4186 sr = tr.querySelector('td:nth-of-type(5)').textContent.trim().replace(/\/.*/, '');
4187 if (/^([\d\.\,]+)\s*\/\s*(\d+)$/.test(sr)) {
4188 sr = Math.round(parseFloat(RegExp.$1.replace(',', '.')) * 1000);
4189 bd = parseInt(RegExp.$2);
4190 } else sr = Math.round(parseFloat(sr) * 1000);
4191 return {
4192 artist: isVA ? VA : artist,
4193 album: album,
4194 //album_year: extractYear(releaseDate),
4195 release_date: releaseDate,
4196 label: label,
4197 catalog: catalogue,
4198 encoding: 'lossless',
4199 be: bd || 24,
4200 sr: sr || undefined,
4201 media: media,
4202 genre: genres.join('; '),
4203 //discnumber: discNumber,
4204 //totaldiscs: totaldiscs,
4205 //discsubtitle: discSubtitle,
4206 tracknumber: (ref = tr.querySelector('td:first-of-type')) != null ?
4207 parseInt(ref.textContent) || ref.textContent.trim() : undefined,
4208 totaltracks: trs.length,
4209 title: (ref = tr.querySelector('td.track-name')) != null ? ref.textContent.trim() : undefined,
4210 duration: (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
4211 url: response.finalUrl,
4212 identifiers: mergeIds(),
4213 cover_url: imgUrl,
4214 }
4215 });
4216 }); else if (/^https?:\/\/(?:\w+\.)?deezer\.com\/(?:\w+\/)*album\/(\d+)/i.test(url)) {
4217 return queryDeezerAPI('album/' + RegExp.$1).then(function(release) {
4218 isVA = vaParser.test(release.artist.name);
4219 identifiers.DEEZER_ID = release.id;
4220 identifiers.RELEASETYPE = release.record_type;
4221 if (release.upc) identifiers.BARCODE = release.upc;
4222 if (release.cover_xl) imgUrl = release.cover_xl.replace('1000x1000-000000-80-0-0', '1400x1400-000000-100-0-0');
4223 return release.tracks.data.map(function(track, ndx) {
4224 trackIdentifiers = { TRACK_ID: track.id };
4225 return {
4226 artist: isVA ? VA : release.artist.name,
4227 album: release.title,
4228 release_date: release.release_date,
4229 label: release.label,
4230 media: media,
4231 genre: release.genres.data.map(it => it.name).join('; '),
4232 tracknumber: ndx + 1,
4233 totaltracks: release.nb_tracks,
4234 title: track.title,
4235 track_artist: track.artist.name && (isVA || track.artist.name != release.artist.name) ? track.artist.name : undefined,
4236 duration: track.duration,
4237 //url: deezerAlbumPrefix + release.id,
4238 identifiers: mergeIds(),
4239 cover_url: imgUrl,
4240 };
4241 });
4242 });
4243 } else if (url.toLowerCase().includes('spotify.com/')) {
4244 if (!/\/albums?\/(\w+)$/i.test(url)) return Promise.reject('This resource is not supported, pick a real album');
4245 return querySpotifyAPI('albums/' + RegExp.$1).then(function(release) {
4246 artist = release.artists.map(artist => artist.name);
4247 isVA = release.artists.length <= 0 || release.artists.length == 1 && vaParser.test(release.artists[0].name);
4248 totalDiscs = release.tracks.items.reduce((acc, track) => Math.max(acc, track.disc_number), 0);
4249 identifiers.SPOTIFY_ID = release.id;
4250 identifiers.RELEASETYPE = release.album_type;
4251 identifiers.BARCODE = release.external_ids.upc;
4252 var image = release.images.reduce((acc, image) => image.width * image.height > acc.width * acc.height ? image : acc);
4253 return release.tracks.items.map(function(track, ndx) {
4254 trackIdentifiers = {
4255 TRACK_ID: track.id,
4256 EXPLICIT: Number(track.explicit),
4257 };
4258 trackArtist = track.artists.map(artist => artist.name);
4259 return {
4260 artist: isVA ? VA : undefined,
4261 artists: !isVA ? artist : undefined,
4262 album: release.name,
4263 release_date: release.release_date,
4264 label: release.label,
4265 media: media,
4266 genre: release.genres.join('; '),
4267 discnumber: track.disc_number,
4268 totaldiscs: totalDiscs,
4269 discsubtitle: discSubtitle,
4270 tracknumber: track.track_number,
4271 totaltracks: release.total_tracks,
4272 title: track.name,
4273 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
4274 duration: track.duration_ms / 1000,
4275 //url: 'https://open.spotify.com/album/' + release.id,
4276 identifiers: mergeIds(),
4277 cover_url: image ? image.url : undefined,
4278 };
4279 });
4280 });
4281 } else if (url.toLowerCase().includes('prostudiomasters.com/')) return globalFetch(url).then(function(response) {
4282 if ((ref = response.document.querySelector('img.album-art')) != null) imgUrl = ref.currentSrc || ref.src;
4283 try {
4284 if ((ref = response.document.querySelector('body > script[charset]')) == null
4285 || !/\bPSM\.album\s*=\s*(\{[\S\s]+\});(?=\s*PSM\b)/.test(ref.text)) throw 'Metadata not found';
4286 let album = JSON.parse(RegExp.$1);
4287 if (prefs.diag_mode) console.debug('PSM metadata received:', album);
4288 const artistSplitter = /\s*;+\s*/;
4289 artist = album.ArtistName.split(artistSplitter);
4290 isVA = vaParser.test(album.ArtistName);
4291 if (album.id) identifiers.PROSTUDIOMASTERS_ID = parseInt(album.id) || album.id;
4292 if (album.GenreName) genres.push(album.GenreName);
4293 if (album.SubGenreName) genres.push(album.SubGenreName);
4294 if (album.genres) genres.push(album.genres);
4295 if (/^[℗©]\s*(\d{4})\b/.test(album.PLine) || /^[℗©]\s*(\d{4})\b/.test(album.CLine)) releaseDate = RegExp.$1;
4296 if (album.ICPN) identifiers.BARCODE = album.ICPN;
4297 if (/\b(\d+(?:\.\d+)?)\s*kHz\s*\/\s*(\d+)[\-\s]?bit\s+(\w+)\b/i.test(album.recording_info)) {
4298 sr = parseFloat(RegExp.$1) * 1000 || undefined;
4299 bd = parseInt(RegExp.$2) || undefined;
4300 format = RegExp.$3;
4301 if (['FLAC', 'AIFF', 'WAV', 'PCM'].includes(format)) encoding = 'lossless';
4302 }
4303 if (album.album_info) {
4304 description = html2php(domParser.parseFromString(album.album_info, 'text/html').body, response.finalUrl);
4305 if (description) description = '[quote]'.concat(description, '[/quote]');
4306 }
4307 return album.tracks.map(function(track) {
4308 trackIdentifiers = {
4309 EXPLICIT: Number(track.ExplicitLyrics == 1),
4310 ISRC: track.ISRC,
4311 TRACK_ID: parseInt(track.id) || track.id,
4312 };
4313 trackArtist = track.ArtistName.split(artistSplitter);
4314 return {
4315 artist: isVA ? VA : undefined,
4316 artists: !isVA ? artist : undefined,
4317 album: album.AlbumName,
4318 genre: genres.join('; '),
4319 release_date: releaseDate,
4320 label: label,
4321 catalog: album.CatalogNumber,
4322 codec: format,
4323 encoding: encoding,
4324 bd: bd,
4325 sr: sr,
4326 media: media,
4327 discnumber: parseInt(track.DiscSeq) || undefined,
4328 discsubtitle: track.GroupingTitle,
4329 tracknumber: parseInt(track.TrackSeq) || undefined,
4330 totaltracks: album.tracks.length,
4331 title: track.TrackName,
4332 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalTo(artist)) ? trackArtist : undefined,
4333 composers: track.composers.split(artistSplitter),
4334 duration: parseInt(track.duration) || undefined,
4335 url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
4336 description: description,
4337 identifiers: mergeIds(),
4338 cover_url: imgUrl,
4339 };
4340 });
4341 } catch(e) {
4342 console.warn('ProStudioMasters: falling back to HTML scraper for the reason', e);
4343 if (/\/page\/(\d+)$/i.test(response.finalUrl)) identifiers.PROSTUDIOMASTERS_ID = RegExp.$1;
4344 artist = Array.from(response.document.querySelectorAll('h2.ArtistName > a')).map(node => node.textContent.trim());
4345 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4346 if (isVA) artist = [];
4347 if ((ref = response.document.querySelector('h3.AlbumName')) != null) album = ref.textContent.trim();
4348 if ((ref = response.document.querySelector('div.pline')) != null
4349 && /^(?:[℗©]\s*)+(\d{4})\s+(.+)/.test(ref.textContent.trim())) {
4350 releaseDate = RegExp.$1;
4351 label = RegExp.$2;
4352 }
4353 getDescription(response, 'div.album-info', false);
4354 trs = response.document.querySelectorAll('div.album-tracks > div.tracks > table > tbody > tr');
4355 totalTracks = Array.from(trs).filter(tr => tr.classList.contains('track-playable')).length;
4356 discNumber = 0;
4357 trs.forEach(function(tr) {
4358 if (tr.classList.contains('track-playable')) {
4359 trackArtist = sr = bd = format = title = undefined; trackIdentifiers = {};
4360 if (ref = tr.getAttribute('data-track-id')) trackIdentifiers.TRACK_ID = ref;
4361 trackNumber = (ref = tr.querySelector('div.num')) != null ? parseInt(ref.firstChild.textContent.trim()) : undefined;
4362 if (trackNumber == 1) ++discNumber;
4363 if ((ref = tr.querySelector('td.track-name > div.name')) != null) {
4364 title = ref.firstChild.textContent.trim();
4365 if ((ref = ref.querySelector(':scope small')) != null) trackArtist = ref.firstChild.textContent;
4366 };
4367 if ((ref = tr.querySelector('span.track-format')) != null && /^(\d+(?:[,\.]\d+)?)\s*([kMG]?Hz)(?:\s+(\d+)-bit)?\s*\|\s*(\S+)$/i.test(ref.textContent.trim())) {
4368 sr = parseFloat(RegExp.$1);
4369 ['hz', 'khz', 'mhz', 'ghz'].forEach((unit, ndx) => { if (RegExp.$2.toLowerCase() == unit) sr *= 1000 ** ndx });
4370 sr = Math.round(sr) || undefined;
4371 bd = parseInt(RegExp.$3) || undefined;
4372 format = RegExp.$4;
4373 }
4374 tracks.push({
4375 artist: isVA ? VA : undefined,
4376 artists: !isVA ? artist : undefined,
4377 album: album,
4378 //album_year: extractYear(releaseDate),
4379 release_date: releaseDate,
4380 label: label,
4381 catalog: catalogue,
4382 codec: format,
4383 bd: bd,
4384 sr: sr,
4385 media: media,
4386 discnumber: discNumber,
4387 totaldiscs: totalDiscs,
4388 discsubtitle: discSubtitle,
4389 tracknumber: trackNumber,
4390 totaltracks: totalTracks,
4391 title: title,
4392 track_artist: trackArtist && (isVA || trackArtist != joinArtists(artist)) ? trackArtist : undefined,
4393 duration: (ref = tr.querySelector('td:last-of-type')) != null ? timeStringToTime(ref.firstChild.data) : undefined,
4394 url: !identifiers.PROSTUDIOMASTERS_ID ? response.finalUrl : undefined,
4395 description: description,
4396 identifiers: mergeIds(),
4397 cover_url: imgUrl,
4398 });
4399 } else if ((ref = tr.querySelector('div.grouping-title')) != null) {
4400 discSubtitle = ref.textContent.trim();
4401 guessDiscNumber();
4402 }
4403 });
4404 return tracks;
4405 }
4406 }); else if (url.toLowerCase().includes('play.google.com/store/music/album/')) {
4407 let _url = new URL(url), _query = new URLSearchParams(_url.search);
4408 _query.set('hl', 'en');
4409 _url.search = _query;
4410 return globalFetch(_url).then(function(response) {
4411 var search = new URLSearchParams(new URL(response.finalUrl).search);
4412 var ID = search.get('id'), trackID, aggregateRating;
4413 if (ID) identifiers.GOOGLE_ID = ID;
4414 var root = response.document.querySelector('div[itemtype="https://schema.org/MusicAlbum"]');
4415 if (root == null) throw new Error('Unexpected Google Play metadata structure');
4416 if ((ref = root.querySelector('div[itemprop="byArtist"]')) != null) {
4417 artist = Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content);
4418 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4419 }
4420 if ((ref = root.querySelector('meta[itemprop="name"]')) != null) album = ref.content;
4421 genres = Array.from(root.querySelectorAll('meta[itemprop="genre"]')).map(elem => elem.content);
4422 if ((ref = root.querySelector('meta[itemprop="datePublished"]')) != null) releaseDate = ref.content;
4423 if ((ref = root.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content);
4424 if ((ref = root.querySelector('meta[itemprop="ratingValue"]')) != null) aggregateRating = parseFloat(ref.content);
4425 if ((ref = response.document.querySelector('h1[class][itemprop="name"] > span')) != null
4426 && (ref = ref.parentNode.parentNode.querySelector('div[class] > span[class]')) != null
4427 && /\b(?:Explicit)\b/i.test(ref.textContent)) identifiers.EXPLICIT = 1;
4428 if ((ref = response.document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content;
4429 try {
4430 let _objs = loadGoogleData(response);
4431 let _albumInfo = _objs.filter(obj => { try { return obj[0].length == 22 } catch(e) { return false } });
4432 let _tracks = _objs.filter(function(obj) {
4433 try {
4434 return typeof obj[0][0][1] == 'boolean' && typeof obj[0][0][2] == 'boolean'
4435 && typeof obj[0][0][3] == 'string' && typeof obj[0][0][4] == 'string';
4436 } catch(e) { return false }
4437 });
4438 if (_albumInfo.length == 1) _albumInfo = _albumInfo[0][0]; else throw 'Album metadata not found';
4439 if (_tracks.length == 1) _tracks = _tracks[0][0][0]; else throw 'Tracks metadata not found';
4440 if (prefs.diag_mode) console.debug('Google Play objects extracted successfully:', _albumInfo, _tracks);
4441 try {
4442 artist = _albumInfo[18][0][8].map(artist => artist[1]);
4443 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4444 } catch(e) { }
4445 try { description = _albumInfo[10][0][1] } catch(e) { }
4446 try { genres = _albumInfo[18][0][6].map(genre => genre[2]) } catch(e) { }
4447 _tracks[0].forEach(function(volume) {
4448 Array.prototype.push.apply(tracks, volume[0].map(function(track) {
4449 trackArtist = track[0][8].map(ta => ta[1]);
4450 trackIdentifiers = { TRACK_ID: track[12][0] };
4451 return {
4452 artist: isVA ? VA : undefined,
4453 artists: !isVA ? artist : undefined,
4454 album: _albumInfo[0][0] || track[9][0],
4455 album_year: extractYear(_albumInfo[18][0][7][1] /*track[0][7][0]*/),
4456 release_date: _albumInfo[18][0][7][2],
4457 label: _albumInfo[18][0][4] || track[0][4],
4458 media: media,
4459 genre: genres.join('; '),
4460 discnumber: volume[1],
4461 totaldiscs: _tracks[0].length,
4462 tracknumber: track[1],
4463 totaltracks: _albumInfo[18][4],
4464 title: track[8][0],
4465 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
4466 duration: track[0][1],
4467 description: description,
4468 url: !identifiers.GOOGLE_ID ? response.finalUrl : undefined,
4469 identifiers: mergeIds(),
4470 cover_url: imgUrl,
4471 };
4472 }));
4473 });
4474 } catch(e) {
4475 console.warn('Google Play music: falling back to HTML scraper (' + e + ')');
4476 tracks = [];
4477 if ((ref = response.document.querySelector('span > a[itemprop="genre"]')) != null) try {
4478 label = ref.parentNode.nextElementSibling.textContent.trim().replace(/^(?:[©℗]|\([cCpP]\))\s*\d{4}\s+/, '');
4479 } catch(e) { console.warn('Unexpected HTML structure (' + e + ')') }
4480 //getDescription(response, '???', false);
4481 var volumes = response.document.querySelectorAll('c-wiz > div > h2');
4482 if (volumes.length <= 0) {
4483 //response.document.querySelectorAll('c-wiz > div > table > tbody > tr[class]').forEach(scanPlaylist);
4484 trackNumber = 0;
4485 root.querySelectorAll('div[itemprop="track"]').forEach(function(tr) {
4486 trackIdentifiers = {};
4487 if ((ref = tr.querySelector('meta[itemprop="url"]')) != null) {
4488 search = new URLSearchParams(new URL(ref.content).search);
4489 let trackID = search.get('tid');
4490 if (trackID) trackIdentifiers.TRACK_ID = trackID;
4491 }
4492 ++trackNumber;
4493 title = (ref = tr.querySelector('meta[itemprop="name"]')) != null ? ref.content : undefined;
4494 trackArtist = (ref = tr.querySelector('div[itemprop="byArtist"]')) != null ?
4495 Array.from(ref.querySelectorAll('meta[itemprop="name"]')).map(it => it.content) : [];
4496 duration = durationFromMeta(tr);
4497 addTrack();
4498 });
4499 } else volumes.forEach(function(volume) {
4500 discNumber = undefined; discSubtitle = volume.textContent.trim();
4501 guessDiscNumber();
4502 volume.nextElementSibling.querySelectorAll('tbody > tr[class]').forEach(scanPlaylist);
4503 });
4504
4505 function scanPlaylist(tr) {
4506 trackNumber = (ref = tr.querySelector('td:nth-of-type(1) > div')) != null ?
4507 parseInt(ref.textContent) || ref.textContent.trim() : undefined;
4508 title = (ref = tr.querySelector('td[itemprop="name"]')) != null ? ref.textContent.trim() : undefined;
4509 duration = (ref = tr.querySelector('td:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined;
4510 trackArtist = Array.from(tr.querySelectorAll('td:nth-of-type(4) > a')).map(it => it.textContent.trim());
4511 addTrack();
4512 }
4513 function addTrack() {
4514 tracks.push({
4515 artist: isVA ? VA : undefined,
4516 artists: !isVA ? artist : undefined,
4517 album: album,
4518 //album_year: extractYear(releaseDate),
4519 release_date: releaseDate,
4520 label: label,
4521 catalog: catalogue,
4522 media: media,
4523 genre: genres.join('; '),
4524 discnumber: discNumber,
4525 totaldiscs: totalDiscs,
4526 discsubtitle: discSubtitle,
4527 tracknumber: trackNumber,
4528 totaltracks: totalTracks,
4529 title: title,
4530 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
4531 duration: duration,
4532 url: identifiers.GOOGLE_ID ? undefined : response.finalUrl,
4533 description: description,
4534 identifiers: mergeIds(),
4535 cover_url: imgUrl,
4536 });
4537 }
4538 }
4539 return tracks;
4540 });
4541 } else if (url.toLowerCase().includes('7digital.com/')) return globalFetch(url).then(function(response) {
4542 if ((ref = response.document.querySelector('table.release-track-list')) != null)
4543 identifiers['7DIGITAL_ID'] = parseInt(ref.dataset.releaseid) || ref.dataset.releaseid;
4544 artist = Array.from(response.document.querySelectorAll('h2.release-info-artist > span[itemprop="byArtist"] > meta[itemprop="name"]'))
4545 .map(node => node.content);
4546 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4547 if ((ref = response.document.querySelector('h1.release-info-title')) != null) album = ref.textContent.trim();
4548 if ((ref = response.document.querySelector('div.release-date-info > p')) != null) releaseDate = normalizeDate(ref.textContent);
4549 if ((ref = response.document.querySelector('div.release-label-info > p')) != null) label = ref.textContent.trim();
4550 response.document.querySelectorAll('dl.release-data > dt.release-data-label').forEach(function(dt) {
4551 if (/\bGenres?:/.test(dt.textContent)) genres = Array.from(dt.nextElementSibling.querySelectorAll('a')).map(a => a.textContent.trim());
4552 });
4553 //getDescription(response, 'div.album-info', false);
4554 if ((ref = response.document.querySelector('span.release-packshot-image > img[itemprop="image"]')) != null)
4555 imgUrl = ref.src;
4556 totalTracks = response.document.querySelectorAll('table.release-track-list > tbody > tr.release-track').length;
4557 response.document.querySelectorAll('table.release-track-list').forEach(function(table) {
4558 discSubtitle = discNumber = undefined;
4559 if ((ref = table.querySelector('caption > h4.release-disc-info')) != null) {
4560 discSubtitle = ref.textContent.trim();
4561 guessDiscNumber();
4562 }
4563 table.querySelectorAll('tbody > tr.release-track').forEach(function(tr) {
4564 trackIdentifiers = {};
4565 if (tr.dataset.trackid) trackIdentifiers.TRACK_ID = parseInt(tr.dataset.trackid) || tr.dataset.trackid;
4566 tracks.push({
4567 artist: isVA ? VA : undefined,
4568 artists: !isVA ? artist : undefined,
4569 album: album,
4570 //album_year: extractYear(releaseDate),
4571 release_date: releaseDate,
4572 label: label,
4573 catalog: catalogue,
4574 media: media,
4575 genre: genres.join('; '),
4576 discnumber: discNumber,
4577 totaldiscs: totalDiscs,
4578 discsubtitle: discSubtitle,
4579 tracknumber: (ref = tr.querySelector('td.release-track-preview > em.release-track-preview-text')) != null ?
4580 ref.textContent.trim() : undefined,
4581 totaltracks: totalTracks,
4582 title: (ref = tr.querySelector('td.release-track-name > meta[itemprop="name"]')) != null ? ref.content : undefined,
4583 duration: durationFromMeta(tr),
4584 url: (ref = response.document.querySelector('head > meta[property="og:url"]')) != null ?
4585 ref.content : response.finalUrl.replace(/\?.*$/, ''),
4586 description: description,
4587 identifiers: mergeIds(),
4588 cover_url: imgUrl,
4589 });
4590 });
4591 });
4592 return tracks;
4593 }); else if (url.toLowerCase().includes('e-onkyo.com/')) return globalFetch(url).then(function(response) {
4594 if (/\/album\/(\w+)\/?$/.test(response.finalUrl)) identifiers.EONKYO_ID = RegExp.$1;
4595 artist = Array.from(response.document.querySelectorAll('div.jacketDetailArea p.artistsName > a'))
4596 .map(node => node.textContent.trim());
4597 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4598 if ((ref = response.document.querySelector('div.jacketDetailArea p.packageTtl')) != null) album = ref.textContent.trim();
4599 if ((ref = response.document.querySelector('div.jacketDetailArea p.recordlabelName > a')) != null) label = ref.textContent.trim();
4600 if ((ref = response.document.querySelector('div.jacketDetailArea p.releaseDay > a')) != null) releaseDate = normalizeDate(ref.textContent);
4601 if ((ref = response.document.querySelector('div.jacketDetailArea p.packageNoteDetail')) != null
4602 && /^\s*(?:\(C\)|©)\s+(\d{4})\b/i.test(ref.lastChild.textContent)) albumYear = parseInt(RegExp.$1);
4603 //getDescription(response, 'div#credit', true);
4604 if (/\s+\(\s*(?:(\d+)[\-\s]*bit)?\s*\/?\s*(?:(\d+(?:\.\d+)?)\s*kHz)?\s*\)\s*$/i.test(album)) {
4605 album = RegExp.leftContext;
4606 bd = parseInt(RegExp.$1) || undefined;
4607 sr = parseFloat(RegExp.$2) * 1000;
4608 }
4609 if ((ref = response.document.querySelector('figure > a.colorbox')) != null)
4610 imgUrl = new URL(response.finalUrl).origin + ref.pathname;
4611 trs = response.document.querySelectorAll('dl.musicList > dd.musicBox');
4612 return Array.from(trs).map(tr => ({
4613 //var trackId = tr.dataset.trackid;
4614 //if (trackId) trackId = 'TRACK_ID=' + trackId;
4615 //trackArtist = tr.children[5].textContent.trim();
4616 //if (trackArtist == artist.join(', ')) trackArtist = undefined;
4617 artist: isVA ? VA : undefined,
4618 artists: !isVA ? artist : undefined,
4619 album: album,
4620 album_year: albumYear,
4621 release_date: releaseDate,
4622 label: label,
4623 catalog: catalogue,
4624 encoding: 'lossless',
4625 codec: 'FLAC',
4626 bd: bd,
4627 sr: sr || undefined,
4628 media: media,
4629 //discnumber: discNumber,
4630 //totaldiscs: totalDiscs,
4631 //discsubtitle: discSubtitle,
4632 tracknumber: (ref = tr.querySelector('div.musicListNo')) != null ? ref.textContent.trim() : undefined,
4633 totaltracks: trs.length,
4634 title: (ref = tr.querySelector('div.musicTtl > span')) != null ? ref.title || ref.textContent.trim() : undefined,
4635 duration: (ref = tr.querySelector('div.musicTime')) != null ? timeStringToTime(ref.textContent.trim()) : undefined,
4636 url: !identifiers.EONKYO_ID ? response.finalUrl : undefined,
4637 description: description,
4638 identifiers: mergeIds(),
4639 cover_url: imgUrl,
4640 }));
4641 }); else if (url.toLowerCase().includes('store.acousticsounds.com/')) return globalFetch(url).then(function(response) {
4642 if (/\/(\d+)\/$/.test(response.finalUrl)) identifiers.ACOUSTICSOUNDS_ID = RegExp.$1;
4643 artist = Array.from(response.document.querySelectorAll('div > h1 > a')).map(node => node.textContent.trim());
4644 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4645 if (isVA) artist = [];
4646 if ((ref = response.document.querySelector('div > h1')) != null) album = ref.lastChild.wholeText.trim().replace(/\s*-\s*/, '');
4647 response.document.querySelectorAll('div > p > table > tbody > tr > td:first-of-type').forEach(function(td) {
4648 if (/^(?:Label):/i.test(td.textContent)) label = td.nextElementSibling.textContent.trim();
4649 if (/^(?:Genre):/i.test(td.textContent)) genres[0] = td.nextElementSibling.textContent.trim();
4650 if (/^(?:Product\s+No):/i.test(td.textContent)) catalogue = td.nextElementSibling.textContent.trim();
4651 if (/^(?:Category):/i.test(td.textContent)
4652 && /^(.+)\s+(\d+(?:\.\d+)?)\s*kHz(?:\s*\/\s*(\d+)[\s\-]?bit)?\s+Download\b/.test(td.nextElementSibling.textContent.trim())) {
4653 format = RegExp.$1;
4654 sr = parseFloat(RegExp.$2) * 1000;
4655 bd = parseInt(RegExp.$3);
4656 }
4657 });
4658 getDescription(response, 'div#description > p', true);
4659 if ((ref = response.document.querySelector('div#detail > link[rel="image_src"]')) != null) {
4660 imgUrl = ref.href.replace(/\/medium\//i, '/large/');
4661 }
4662 trs = response.document.querySelectorAll('div#tracks > table > tbody > tr');
4663 trackNumber = 0;
4664 return Array.from(trs).map(tr => ({
4665 artist: isVA ? VA : undefined,
4666 artists: !isVA ? artist : undefined,
4667 album: album,
4668 //album_year: extractYear(releaseDate),
4669 release_date: releaseDate,
4670 label: label,
4671 catalog: catalogue,
4672 encoding: ['FLAC', 'DSD'].includes(format) ? 'lossless' : undefined,
4673 codec: format,
4674 bd: bd,
4675 sr: sr || undefined,
4676 media: media,
4677 genre: genres.join('; '),
4678 //discnumber: discNumber,
4679 //totaldiscs: totalDiscs,
4680 //discsubtitle: discSubtitle,
4681 tracknumber: ++trackNumber,
4682 totaltracks: trs.length,
4683 title: (ref = tr.querySelector('td[nowrap]')) != null ? ref.textContent.trim() : undefined,
4684 url: !identifiers.ACOUSTICSOUNDS_ID ? response.finalUrl : undefined,
4685 description: description,
4686 identifiers: mergeIds(),
4687 cover_url: imgUrl,
4688 }));
4689 }); else if (url.toLowerCase().includes('indies.eu/')) return globalFetch(url).then(function(response) {
4690 if (/\/alba\/(\d+)\//.test(response.finalUrl)) identifiers.INDIESSCOPE_ID = parseInt(RegExp.$1);
4691 ref = response.document.querySelector(':root > body > div > div > div > h2');
4692 if (ref != null) artist = Array.from(ref.childNodes).map(node => node.textContent.trim());
4693 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4694 if ((ref = response.document.querySelector(':root > body > div > div > div > h1')) != null)
4695 album = ref.textContent.trim();
4696 if ((ref = response.document.querySelector('div.infoBox')) != null) {
4697 let ndx = 0;
4698 ref.childNodes.forEach(function(child) {
4699 if (child.nodeName == 'BR') { ++ndx; return; }
4700 switch (ndx) {
4701 case 0:
4702 if (child.nodeType == Node.TEXT_NODE) {
4703 label = child.wholeText.trim();
4704 if (/^(.*)\s+\/\s+(\d{4})$/.test(label)) {
4705 label = RegExp.$1;
4706 releaseDate = RegExp.$2;
4707 }
4708 }
4709 break;
4710 case 1:
4711 if (child.nodeType == Node.ELEMENT_NODE) genres.push(child.textContent.trim());
4712 break;
4713 case 2:
4714 if (child.nodeType == Node.ELEMENT_NODE) catalogue = child.textContent.trim();
4715 break;
4716 }
4717 });
4718 }
4719 getDescription(response, 'div.popis > section', true);
4720 if ((ref = response.document.querySelector('div.obrazekDetail > img')) != null) imgUrl = ref.src;
4721 trs = response.document.querySelectorAll('table.skladby > tbody > tr');
4722 return Array.from(trs).map(function(tr) {
4723 title = undefined;
4724 if ((ref = tr.querySelector('td.nazev')) != null) {
4725 trackNumber = parseInt(ref.firstChild.wholeText);
4726 title = ref.querySelector('strong').textContent.trim();
4727 }
4728 return {
4729 artist: isVA ? VA : undefined,
4730 artists: !isVA ? artist : undefined,
4731 album: album,
4732 //album_year: extractYear(releaseDate),
4733 release_date: releaseDate,
4734 label: label,
4735 catalog: catalogue,
4736 codec: format,
4737 media: media,
4738 genre: genres.join('; '),
4739 //discnumber: discNumber,
4740 //totaldiscs: totalDiscs,
4741 //discsubtitle: discSubtitle,
4742 tracknumber: trackNumber,
4743 totaltracks: trs.length,
4744 title: title,
4745 duration: (ref = tr.querySelector('td:nth-of-type(4)')) != null ? timeStringToTime(ref.textContent) : undefined,
4746 identifiers: !identifiers.INDIESSCOPE_ID ? response.finalUrl : undefined,
4747 description: description,
4748 identifiers: mergeIds(),
4749 cover_url: imgUrl,
4750 };
4751 });
4752 }); else if (url.toLowerCase().includes('beatport.com/')) return globalFetch(url).then(function(response) {
4753 if (/\/release\/(?:\d\/)?(?:\S+-)?(\d+)\b/i.test(response.finalUrl)) identifiers.BEATPORT_ID = RegExp.$1;
4754 artist = Array.from(response.document.querySelectorAll('span > a[data-artist]')).map(node => node.textContent.trim());
4755 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4756 if ((ref = response.document.querySelector('div > h1')) != null) album = ref.textContent.trim();
4757 response.document.querySelectorAll('ul > li > span.category').forEach(function(span) {
4758 if (/^(?:Release\s+Date)/i.test(span.textContent)) releaseDate = span.nextElementSibling.textContent.trim();
4759 if (/^(?:Label)/i.test(span.textContent)) label = span.nextElementSibling.textContent.trim();
4760 if (/^(?:Catalog)/i.test(span.textContent)) catalogue = span.nextElementSibling.textContent.trim();
4761 });
4762 getDescription(response, 'div.interior-expandable', true);
4763 if ((ref = response.document.querySelector('div > img.interior-release-chart-artwork')) != null) imgUrl = ref.src;
4764 trs = response.document.querySelectorAll('div.tracks > ul > li.track');
4765 return Array.from(trs).map(function(tr) {
4766 trackIdentifiers = { TRACK_ID: parseInt(tr.dataset.ecId) || tr.dataset.ecId };
4767 title = (ref = tr.querySelector('span.buk-track-primary-title')) != null ?
4768 ref.title || ref.textContent.trim() : tr.dataset.ecName;
4769 if (title && (ref = tr.querySelector('span.buk-track-remixed')) != null) title += ' (' + ref.textContent.trim() + ')';
4770 trackArtist = Array.from(tr.querySelectorAll('p.buk-track-artists > a')).map(a => a.textContent.trim());
4771 if ((ref = tr.querySelector('p.buk-track-bpm')) != null) trackIdentifiers.BPM = parseInt(ref.textContent);
4772 return {
4773 artist: isVA ? VA : undefined,
4774 artists: !isVA ? artist : undefined,
4775 album: album,
4776 //album_year: extractYear(releaseDate),
4777 release_date: releaseDate,
4778 label: tr.dataset.ecBrand || ((ref = tr.querySelector('p.buk-track-labels')) != null ? ref.textContent.trim() : label),
4779 catalog: catalogue,
4780 codec: format,
4781 media: media,
4782 genre: Array.from(tr.querySelectorAll('p.buk-track-genre > a')).map(a => a.textContent).join('; '),
4783 //discnumber: discNumber,
4784 //totaldiscs: totalDiscs,
4785 //discsubtitle: discSubtitle,
4786 tracknumber: tr.dataset.ecPosition || ((ref = tr.querySelector('div.buk-track-num')) != null ?
4787 ref.textContent.trim() : undefined),
4788 totaltracks: trs.length,
4789 title: title,
4790 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
4791 remixers: Array.from(tr.querySelectorAll('p.buk-track-remixers > a')).map(a => a.textContent.trim()),
4792 duration: (ref = tr.querySelector('p.buk-track-length')) != null ? timeStringToTime(ref.textContent) : undefined,
4793 description: description,
4794 url: !identifiers.BEATPORT_ID ? response.finalUrl : undefined,
4795 cover_url: imgUrl,
4796 identifiers: mergeIds(),
4797 };
4798 });
4799 }); else if (url.toLowerCase().includes('traxsource.com/')) return globalFetch(url).then(function(response) {
4800 if (/\/title\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.TRAXSOURCE_ID = RegExp.$1;
4801 artist = Array.from(response.document.querySelectorAll('h1.artists > a.com-artists')).map(node => node.textContent.trim());
4802 if (artist.length <= 0 && (ref = response.document.querySelector('h1.artists')) != null) artist = [ref.textContent.trim()];
4803 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4804 if ((ref = response.document.querySelector('h1.title')) != null) album = ref.textContent.trim();
4805 if ((ref = response.document.querySelector('a.com-label')) != null) label = ref.textContent.trim();
4806 if ((ref = response.document.querySelector('div.cat-rdate')) != null && /^(.*)\s*\|\s*(.*)$/.test(ref.textContent.trim())) {
4807 catalogue = RegExp.$1;
4808 releaseDate = normalizeDate(RegExp.$2);
4809 }
4810 getDescription(response, 'div.desc', true);
4811 if ((ref = response.document.querySelector('meta[property="og:image"][content]')) != null) imgUrl = ref.content;
4812 trs = response.document.querySelectorAll('div.trklist > div.trk-row');
4813 return Array.from(trs).map(function(tr) {
4814 trackIdentifiers = {};
4815 title = (ref = tr.querySelector('div.title > a')) != null && ref.textContent.trim() || undefined;
4816 if (title && (ref = tr.querySelector('span.version')) != null ) {
4817 if (ref.firstChild.nodeType == Node.TEXT_NODE
4818 && (i = ref.firstChild.wholeText.trim()).length > 0) title += ' ('.concat(i, ')');
4819 }
4820 trackArtist = Array.from(tr.querySelectorAll('div.artists a.com-artists')).map(a => a.textContent.trim());
4821 return {
4822 artist: isVA ? VA : undefined,
4823 artists: !isVA ? artist : undefined,
4824 album: album,
4825 //album_year: extractYear(releaseDate),
4826 release_date: releaseDate,
4827 label: label,
4828 catalog: catalogue,
4829 media: media,
4830 genre: Array.from(tr.querySelectorAll('div.genre > a')).map(a => a.textContent.trim()).join('; '),
4831 //discnumber: discNumber,
4832 //totaldiscs: totalDiscs,
4833 //discsubtitle: discSubtitle,
4834 tracknumber: (ref = tr.querySelector('div.tnum')) != null ? ref.textContent.trim() : undefined,
4835 totaltracks: trs.length,
4836 title: title,
4837 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
4838 remixers: Array.from(tr.querySelectorAll('div.artists a.com-remixers')).map(a => a.textContent.trim()),
4839 duration: (ref = tr.querySelector('span.duration')) != null ? timeStringToTime(ref.textContent) : undefined,
4840 url: !identifiers.TRAXSOURCE_ID ? response.finalUrl : undefined,
4841 description: description,
4842 identifiers: mergeIds(),
4843 cover_url: imgUrl,
4844 };
4845 });
4846 }); else if (url.toLowerCase().includes('music.apple.com/')) return loadItunesMetadata(url).then(function(album) {
4847 identifiers.APPLE_ID = parseInt(album.id) || album.id;
4848 isVA = vaParser.test(album.attributes.artistName);
4849 genres = album.attributes.genreNames.filter(genre => genre != 'Music');
4850 label = album.attributes.recordLabel;
4851 if (!label) label = album.attributes.copyright.replace(/^((?:[©℗]|\([PC]\))\s+)?(?:(\d{4})\s+)?/i, '');
4852 //identifiers.EXPLICIT = Number(/^(?:explicit)$/i.test(album.attributes.contentRating));
4853 if ('isCompilation' in album.attributes) identifiers.COMPILATION = Number(album.attributes.isCompilation);
4854 if (album.attributes.isSingle) identifiers.RELEASETYPE = 'Single';
4855 if (album.description) description = html2php(album.description, album.attributes.url);
4856 if (!description && album.attributes.editorialNotes)
4857 description = html2php(domParser.parseFromString(album.attributes.editorialNotes.standard
4858 || album.attributes.editorialNotes.short, 'text/html').body, album.attributes.url).replace(/\n/g, '\n\n');
4859 if (description && !description.includes('[/quote]')) description = '[quote]'.concat(description, '[/quote]');
4860 //if (description && !description.includes('[quote]')) description = '[quote]' + description.collapseGaps() + '[/quote]';
4861 if (album.attributes.artwork && prefs.apple_offer_alt_cover)
4862 addMessage(new HTML('<a target="_blank" href="' + album.attributes.artwork.realUrl + '">Alternate cover URL</a>'), 'info');
4863 return album.relationships.tracks.data.filter(track => track.type == 'songs').map(function(track) {
4864 trackIdentifiers = {
4865 TRACK_ID: parseInt(track.id),
4866 ISRC: track.attributes.isrc,
4867 EXPLICIT: Number(/^(?:explicit)$/i.test(track.attributes.contentRating)),
4868 HASLYRICS: Number(track.attributes.hasLyrics || false),
4869 };
4870 var trackGenres = track.attributes.genreNames.filter(genre => genre != 'Music');
4871 return {
4872 artist: isVA ? VA : album.attributes.artistName,
4873 artists: album.relationships.artists.data.map(artist => artist.attributes.name),
4874 album: album.attributes.name,
4875 release_date: album.attributes.releaseDate,
4876 label: label,
4877 media: media,
4878 genre: (trackGenres.length > 0 ? trackGenres : genres).join('; '),
4879 discnumber: track.attributes.discNumber,
4880 discsubtitle: track.attributes.workName,
4881 tracknumber: track.attributes.trackNumber,
4882 totaltracks: album.attributes.trackCount,
4883 title: track.attributes.name,
4884 track_artist: track.attributes.artistName && (isVA || track.attributes.artistName != album.attributes.artistName) ?
4885 track.attributes.artistName : undefined,
4886 composer: track.attributes.composerName,
4887 duration: track.attributes.durationInMillis / 1000 || undefined,
4888 description: description,
4889 url: !identifiers.APPLE_ID ? album.attributes.url : undefined,
4890 identifiers: mergeIds(),
4891 //cover_url: album.attributes.artwork ? album.attributes.artwork.realUrl : undefined,
4892 };
4893 });
4894 }); else if (mbrRlsParser.test(url)) { // MusicBrainz
4895 var entities = [
4896 'aliases', 'annotation', 'artist-credits', 'artists', 'collections', 'discids', 'genres',
4897 'isrcs', 'labels', 'media', 'ratings', 'recordings', 'release-groups', 'tags', 'url-rels',
4898 ];
4899 return queryMusicBrainzAPI('release/' + RegExp.$1, { inc: entities.join('+') }).then(function(release) {
4900 if (release.error) return Promise.reject(release.error);
4901 if (prefs.diag_mode) console.debug('MusicBrainz release metadata received:', release);
4902 if (release.id) identifiers.MBID = release.id;
4903 if (release.barcode) identifiers.BARCODE = release.barcode;
4904 if (release.asin) identifiers.ASIN = release.asin;
4905 if (release['release-group']['primary-type']) identifiers.RELEASETYPE = release['release-group']['primary-type'];
4906 artist = Array.isArray(release['artist-credit']) ? release['artist-credit'].map(artist => artist.name) : [];
4907 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4908 if (Array.isArray(release.genres)) genres = release.genres.map(genre => genre.name);
4909 if (Array.isArray(release.tags)) Array.prototype.push.apply(genres, release.tags.map(tag => tag.name));
4910 if (genres.length <= 0) {
4911 if (Array.isArray(release['release-group'].genres)) {
4912 Array.prototype.push.apply(genres, release['release-group'].genres.map(tag => tag.name));
4913 }
4914 if (Array.isArray(release['release-group'].tags)) {
4915 Array.prototype.push.apply(genres, release['release-group'].tags.map(tag => tag.name));
4916 }
4917 }
4918 label = release['label-info'].map(label => label.label.name);
4919 catalogue = release['label-info'].map(label => label['catalog-number']);
4920 if (release['release-group'].status && !/^(?:Official)$/i.test(release['release-group'].status))
4921 addMessage('Not an official release ('.concat(release['release-group'].status, ')'), 'warning');
4922 release.media.forEach(function(medium, ndx) {
4923 medium.tracks.forEach(function(track, ndx) {
4924 trackIdentifiers = { TRACK_ID: track.id };
4925 if (Array.isArray(track['artist-credit'])) {
4926 trackArtist = track['artist-credit'].map(artist => artist.name);
4927 trackArtist = trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist));
4928 } else trackArtist = false;
4929 tracks.push({
4930 artist: isVA ? VA : undefined,
4931 artists: !isVA ? artist : undefined,
4932 album: /*release['release-group'].title || */release.title,
4933 album_year: extractYear(release['release-group']['first-release-date']),
4934 release_date: release.date,
4935 genre: genres.join('; '),
4936 label: label.filter(label => label).join(' / '),
4937 catalog: catalogue.filter(catno => catno).join(' / '),
4938 media: medium.format,
4939 discnumber: medium.position,
4940 discsubtitle: medium.title,
4941 totaldiscs: release.media.length,
4942 tracknumber: track.number,
4943 title: track.title,
4944 track_artist: trackArtist ?
4945 track['artist-credit'].map(artist => artist.name.concat(artist.joinphrase)).join('') : undefined,
4946 duration: track.length != null ? track.length / 1000 : undefined,
4947 //country: release.country,
4948 description: release.annotation,
4949 identifiers: mergeIds(),
4950 });
4951 });
4952 });
4953 return tracks;
4954 });
4955 } else if (url.toLowerCase().includes('vgmdb.net/')) return globalFetch(url).then(function(response) {
4956 if (/\/album\/(\d+)(?=\/|$)/i.test(response.finalUrl)) identifiers.VGMDB_ID = RegExp.$1;
4957 if ((ref = response.document.querySelector('h1 > span.albumtitle[style="display:inline"]')) != null) {
4958 album = ref.innerText.trim();
4959 if (ref.lang == 'en'
4960 && (ref = response.document.querySelector('div > span.albumtitle[style="display:inline"]')) != null
4961 && ref.firstChild != null && ref.firstChild.nodeType == Node.TEXT_NODE)
4962 album += ' ('.concat(ref.firstChild.wholeText.trim(), ')');
4963 }
4964 composer = [];
4965 response.document.querySelectorAll('table#album_infobit_large > tbody > tr > td > span.label > b').forEach(function(key) {
4966 var value = key.parentNode.parentNode.nextElementSibling;
4967 switch (key.innerText.trim().toLowerCase()) {
4968 case 'catalog number':
4969 catalogue = value.textContent.trim().replace(/\s*\([^\(\)]+\)$/, '');
4970 break;
4971 case 'release date':
4972 if (value.firstElementChild != null) releaseDate = value.firstElementChild.innerText.trim();
4973 break;
4974 case 'media format':
4975 media = value.textContent.trim();
4976 break;
4977 case 'classification':
4978 genres = value.textContent.trim().split(/\s*,\*/);
4979 break;
4980 case 'published by':
4981 label = Array.from(value.querySelectorAll('a > span.productname:first-of-type'))
4982 .map(span => span.innerText.trim()).join(' / ');
4983 break;
4984 case 'composed by':
4985 case 'lyrics by':
4986 getArtists(value).forEach(artist => { composer.pushUniqueCaseless(artist) });
4987 break;
4988 case 'performed by':
4989 artist = getArtists(value);
4990 break;
4991 case 'arranged by':
4992 var arrangers = getArtists(value);
4993 break;
4994 }
4995 });
4996 if (!artist || artist.length <= 0) artist = composer;
4997 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
4998 genres.pushUniqueCaseless('Soundtrack');
4999 response.document.querySelectorAll('td#rightcolumn > div > div > div > b.label').forEach(function(key) {
5000 var value = key.parentNode.lastChild;
5001 if (key.innerText.toLowerCase() == 'category' && value != null)
5002 genres.pushUniqueCaseless(value.textContent.trim());
5003 });
5004 getDescription(response, 'div#notes', false);
5005 if ((ref = response.document.querySelector('div#coverart')) != null
5006 && /\burl\s*\(\"(.*)"\)/i.test(ref.style['background-image'])) imgUrl = RegExp.$1;
5007 response.document.querySelectorAll('div#tracklist > span > span > b').forEach(function(node) {
5008 discSubtitle = node.innerText.trim();
5009 guessDiscNumber();
5010 node = node.parentNode;
5011 while (node != null && node.nodeName != 'TABLE') node = node.nextElementSibling;
5012 if (node != null) addVolume(node);
5013 });
5014 var tl = Array.from(response.document.querySelectorAll('ul#tlnav > li > a'));
5015 if (tl.length <= 1) return tracks;
5016 if ((i = tracks.length / tl.length) != Math.floor(i)) {
5017 console.warn('Unexpected vgmdb.net tracklist length:', i, tracks);
5018 return tracks;
5019 }
5020 let enIndex = tl.findIndex(l => /^English\b/i.test(l.innerText.trim()));
5021 if (enIndex < 0) enIndex = tl.findIndex(l => /^Romaji\b/i.test(l.innerText.trim()));
5022 if (enIndex < 0) return tracks.slice(0, i);
5023 let jpIndex = tl.findIndex(l => /^Japanese\b/i.test(l.innerText.trim()));
5024 if (jpIndex < 0) jpIndex = enIndex > 0 ? 0 : 1;
5025 return tracks.slice(enIndex * i, (enIndex + 1) * i).map(function(track, ndx) {
5026 const rx = /^(.+?)(?:\s+\(([^\(\)]+)\))?$/;
5027 if (!track.title) track.title = tracks[jpIndex * i + ndx].title;
5028 else if ((jpTitle = tracks[jpIndex * i + ndx].title) != track.title) {
5029 track.title += ' (';
5030 var enTitle = rx.exec(track.title), jpTitle = rx.exec(jpTitle);
5031 if (jpTitle[1] != enTitle[1]) {
5032 track.title += jpTitle[1];
5033 if (jpTitle[2] && jpTitle[2] != enTitle[2]) track.title += ' ('.concat(jpTitle[2], ')');
5034 } else track.title += jpTitle[2];
5035 track.title += ')';
5036 }
5037 return track;
5038 });
5039
5040 function addVolume(node) {
5041 Array.prototype.push.apply(tracks, Array.from(node.querySelectorAll('tbody > tr')).map(tr => ({
5042 artist: isVA ? VA : undefined,
5043 artists: !isVA ? artist : undefined,
5044 album: album,
5045 //album_year: extractYear(releaseDate),
5046 release_date: releaseDate,
5047 label: label,
5048 catalog: catalogue,
5049 media: media,
5050 genre: genres.join('; '),
5051 discnumber: discNumber,
5052 //totaldiscs: totalDiscs,
5053 discsubtitle: discSubtitle,
5054 tracknumber: (ref = tr.querySelector('span.label')) != null ? parseInt(ref.innerText) : undefined,
5055 //totaltracks: trs.length,
5056 title: tr.children[1].innerText.trim(),
5057 //track_artist: joinArtists(trackArtist),
5058 composers: composer,
5059 duration: (ref = tr.querySelector('span.time')) != null ? timeStringToTime(ref.innerText) : undefined,
5060 url: !identifiers.VGMDB_ID ? response.finalUrl : undefined,
5061 description: description,
5062 identifiers: mergeIds(),
5063 cover_url: imgUrl,
5064 })));
5065 }
5066
5067 function getArtists(node) {
5068 var artists = [];
5069 node.childNodes.forEach(function(node) {
5070 switch (node.nodeType) {
5071 case Node.ELEMENT_NODE:
5072 if ((i = node.querySelectorAll('span.artistname')).length > 0) {
5073 var artist = i[0].innerText.trim();
5074 if (i.length > 1 && i[0].lang == 'en') artist += ' ('.concat(i[1].innerText.trim(), ')');
5075 } else artist = node.innerText.trim();
5076 if (artist) artists.push(artist);
5077 break;
5078 case Node.TEXT_NODE:
5079 artist = node.wholeText.trim().replace(/^\s*,\s*|\s*,\s*$/g, '');
5080 if (/^[\(\)]+$/.test(artist)) return;
5081 if (artist) Array.prototype.push.apply(artists, artist.split(/\s*,\s*/));
5082 break;
5083 }
5084 });
5085 return artists;
5086 }
5087 }); else if (url.toLowerCase().includes('tidal.com/')) {
5088 if (!(/\/album\/(\d+)(?:\/|$)/i.test(url) && !/\b(?:albumId)=(\d+)\b/i.test(url)))
5089 return Promise.reject('Fetching from this page is not supported');
5090 return queryTidalAPI('album', { albumId: RegExp.$1 }).then(function(album) {
5091 var albumHeader = findModule('ALBUM_HEADER');
5092 if (albumHeader == null) return Promise.reject('Album header not found');
5093 var albumItems = findModule('ALBUM_ITEMS');
5094 if (albumItems == null) return Promise.reject('Album items not found');
5095 artist = albumHeader.album.artists.map(artist => artist.name);
5096 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0].name);
5097 identifiers.TIDAL_ID = albumHeader.album.id;
5098 identifiers.RELEASETYPE = albumHeader.album.type;
5099 if (/^(?:(?:\([PC]\)|©|℗)\s+)?(?:(\d{4})\s+)?(.*)/.test(albumHeader.album.copyright)) {
5100 //if (RegExp.$1) albumYear = parseInt(RegExp.$1);
5101 label = RegExp.$2;
5102 }
5103 var channels;
5104 description = albumHeader.description;
5105 if (albumHeader.review.text) {
5106 if (description) description += '\n\n';
5107 if (!albumHeader.review.source) description += '[b]Album Review[/b]\n\n';
5108 description += '[quote';
5109 if (albumHeader.review.source) description += '=Album review from '.concat(albumHeader.review.source);
5110 description += ']'.concat(albumHeader.review.text, '[/quote]');
5111 description = description
5112 .replace(/\[wimpLink\s+artistId="(\d+)"\]/g, '[url=https://listen.tidal.com/artist/$1]')
5113 .replace(/\[wimpLink\s+albumId="(\d+)"\]/g, '[url=https://listen.tidal.com/album/$1]')
5114 .replace(/\[\/wimpLink\]/g, '[/url]');
5115 }
5116 if (Array.isArray(albumHeader.credits.items) && albumHeader.credits.items.length > 0) {
5117 let ac = '';
5118 albumHeader.credits.items.forEach(function(credit) {
5119 if (/^Primary Artist$/i.test(credit.type)) return;
5120// if (/^Record label$/i.test(credit.type)) {
5121// label = credit.contributors.map(contributor => contributor.name).join(' / ');
5122// return;
5123// }
5124 ac += '\n'.concat(credit.type, ' – ', joinArtists(credit.contributors.map(contributor =>
5125 !contributor.id ? contributor.name :
5126 '[url=https://listen.tidal.com/artist/'.concat(contributor.id, ']', contributor.name, '[/url]'))));
5127 });
5128 if (ac.length > 0) {
5129 if (description) {
5130 if (!albumHeader.review.text) description += '\n';
5131 description += '\n';
5132 }
5133 description += '[b]Additional Credits[/b]\n'.concat(ac);
5134 }
5135 }
5136 if (albumHeader.album.cover)
5137 imgUrl = 'https://resources.tidal.com/images/'.concat(albumHeader.album.cover.replace(/-/g, '/'), '/1280x1280.jpg');
5138 return albumItems.pagedList.items.map(function(track, ndx) {
5139 if (track.type != 'track') return;
5140 trackIdentifiers = {
5141 TRACK_ID: track.item.id,
5142 EXPLICIT: Number(track.item.explicit),
5143 };
5144 trackArtist = track.item.artists.map(artist => artist.name);
5145 channels = undefined;
5146 track.item.audioModes.forEach(function(audioMode) {
5147 switch (audioMode.toLowerCase()) {
5148 case 'stereo': channels = 2; break;
5149 default: if (/\b(\d+)\.(\d+)\b/.test(audioMode)) channels = parseInt(RegExp.$1) + parseInt(RegExp.$2);
5150 }
5151 });
5152 return {
5153 artist: isVA ? VA : undefined,
5154 artists: !isVA ? artist : undefined,
5155 album: albumHeader.album.title,
5156 album_year: albumYear,
5157 release_date: albumHeader.album.releaseDate,
5158 label: label,
5159 media: media,
5160 discnumber: track.item.volumeNumber,
5161 totaldiscs: albumHeader.album.numberOfVolumes,
5162 //discsubtitle: discSubtitle,
5163 tracknumber: track.item.trackNumber,
5164 totaltracks: albumHeader.album.numberOfTracks,
5165 title: track.item.title,
5166 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
5167 encoding: ['HI_RES', 'LOSSLESS'].includes(track.item.audioQuality) ? 'lossless' : undefined,
5168 duration: track.item.duration,
5169 channels: channels,
5170 description: description,
5171 url: !identifiers.TIDAL_ID ? albumHeader.album.url : undefined,
5172 identifiers: mergeIds(),
5173 cover_url: imgUrl,
5174 ag: track.replayGain ? track.replayGain.toString().concat(' dB') : undefined,
5175 };
5176 });
5177
5178 function findModule(type) {
5179 for (var row of album.rows) {
5180 var result = row.modules.find(module => module.type == type);
5181 if (result != undefined) return result;
5182 }
5183 return null;
5184 }
5185 });
5186 } else if (url.toLowerCase().includes('ototoy.jp/')) return globalFetch(url).then(function(response) {
5187 if (/\/p\/(\d+)(?=\/|\?|$)/i.test(response.finalUrl)) identifiers.OTOTOY_ID = parseInt(RegExp.$1);
5188 artist = Array.from(response.document.querySelectorAll('span.album-artist > *')).map(node => node.textContent.trim());
5189 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
5190 if ((ref = response.document.querySelector('h1.album-title')) != null) album = ref.textContent.trim();
5191 if ((ref = response.document.querySelector('p.hqd-logo > span')) != null
5192 && /Audio Format:\s*(\d+)\s*bit\s*\/\s*(\d+(?:\.\d+)?)\s*kHz\b/i.test(ref.textContent.trim())) {
5193 bd = parseInt(RegExp.$1);
5194 sr = parseFloat(RegExp.$2) * 1000;
5195 }
5196 if ((ref = response.document.querySelector('p.hqd-logo > a.lossless')) != null) encoding = 'lossless';
5197 if ((ref = response.document.querySelector('p.release-day')) != null && /\b(\d{4})-(\d{2})-(\d{2})\b/.test(ref.textContent))
5198 releaseDate = RegExp.lastMatch;
5199 label = Array.from(response.document.querySelectorAll('p.label-name > a')).map(a => a.textContent.trim()).join(' / ');
5200 if ((ref = response.document.querySelector('p.catalog-id')) != null && /\b(?:Catalog\s+number):\s*(.*)$/i.test(ref.textContent.trim()))
5201 catalogue = RegExp.$1;
5202 genres = Array.from(response.document.querySelectorAll('ul.tag-cloud > li > a.oty-btn-tag'))
5203 .map(a => a.textContent.trim()).filter(genre => genre.length > 0);
5204 getDescription(response, 'div.album-addendum', false);
5205 if ((ref = response.document.querySelector('div#jacket-full-wrapper > img')) != null) imgUrl = ref.dataset.src || ref.src;
5206 trs = response.document.querySelectorAll('table#tracklist > tbody > tr[class^="bg"]');
5207 return Array.from(trs).map(function(tr, ndx) {
5208 trackIdentifiers = {};
5209 title = (ref = tr.querySelector('td.item > span[id^="title-"]')) != null ? ref.textContent.trim() : undefined;
5210 if (ref != null && /^title-(\d+)$/.test(ref.id)) trackIdentifiers.TRACK_ID = parseInt(RegExp.$1);
5211 trackArtist = Array.from(tr.querySelectorAll('td.item > span > a.artist')).map(a => a.textContent.trim());
5212 return {
5213 artist: isVA ? VA : undefined,
5214 artists: !isVA ? artist : undefined,
5215 album: album,
5216 album_year: extractYear(releaseDate),
5217 release_date: releaseDate,
5218 label: label,
5219 catalog: catalogue,
5220 media: media,
5221 genre: genres.join('; '),
5222 discnumber: discNumber,
5223 tracknumber: ndx + 1,
5224 totaltracks: trs.length,
5225 sr: sr || undefined,
5226 bd: bd,
5227 encoding: encoding,
5228 title: title,
5229 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
5230 duration: (ref = tr.querySelector(':scope > td.item:nth-of-type(3)')) != null ? timeStringToTime(ref.textContent) : undefined,
5231 description: description,
5232 url: !identifiers.OTOTOY_ID ? response.finalUrl : undefined,
5233 cover_url: imgUrl,
5234 identifiers: mergeIds(),
5235 };
5236 });
5237 }); else if (url.toLowerCase().includes('music.yandex.ru/') && (/\/album\/(\d+)\b/i.test(url)
5238 || /\b(?:album)=(\d+)\b/i.test(url))) return globalFetch('https://music.yandex.ru/handlers/album.jsx?album=' + RegExp.$1, { responseType: 'json' }).then(function(response) {
5239 if (prefs.diag_mode) console.debug('Yandex Music metadata received:', response.response);
5240 if (response.response.metaType && response.response.metaType != 'music') throw 'Not a music release';
5241 identifiers.YANDEX_ID = response.response.id;
5242 if (response.response.type) identifiers.RELEASETYPE = response.response.type;
5243 artist = response.response.artists.filter(artist => !artist.composer).map(artist => artist.name);
5244 composer = response.response.artists.filter(artist => artist.composer).map(artist => artist.name);
5245 isVA = response.response.artists.length <= 0
5246 || response.response.artists.length == 1 && response.response.artists.some(artist => artist.various);
5247 album = response.response.title;
5248 if (response.response.version) album += ' ('.concat(response.response.version, ')');
5249 response.response.volumes.forEach(function(volume, discNumber) {
5250 Array.prototype.push.apply(tracks, volume.filter(track => track.type == 'music').map(function(track, trackNumber) {
5251 trackIdentifiers = { TRACK_ID: parseInt(/*track.realId || */track.id) };
5252 title = track.title;
5253 if (track.version) title += ' ('.concat(track.version, ')');
5254 trackArtist = track.artists.filter(artist => !artist.composer).map(artist => artist.name);
5255 var trackComposer = track.artists.filter(artist => artist.composer).map(artist => artist.name);
5256 return {
5257 artist: isVA ? VA : undefined,
5258 artists: !isVA ? artist : undefined,
5259 album: album,
5260 album_year: response.response.year,
5261 release_date: response.response.releaseDate.replace(/T.*$/, ''),
5262 label: response.response.labels.map(label => label.name).join(' / '),
5263 media: media,
5264 genre: response.response.genre,
5265 tracknumber: trackNumber + 1,
5266 totaltracks: response.response.trackCount,
5267 composers: trackComposer.length > 0 ? trackComposer : composer,
5268 discnumber: discNumber + 1,
5269 totaldiscs: response.response.volumes.length,
5270 title: title,
5271 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist)) ? trackArtist : undefined,
5272 duration: track.durationMs / 1000,
5273 cover_url: 'https://'.concat(response.response.coverUri.replace('/%%', '/m1000x1000')),
5274 identifiers: mergeIds(),
5275 tg: track.normalization ? track.normalization.gain.toString().concat(' dB') : undefined,
5276 tp: track.normalization ? track.normalization.peak.toString().concat(' dB') : undefined,
5277 };
5278 }));
5279 });
5280 return tracks;
5281 }); else if (url.toLowerCase().includes('mora.jp/') ) return loadMoraMetadata(url).then(function(packageMeta) {
5282 if (prefs.diag_mode) console.debug('Mora.jp metadata loaded:', packageMeta);
5283 if ([7].includes(packageMeta.mediaType)) throw 'Not music release (' + packageMeta.mediaType + ')';
5284 artist = fmtKanaProp(packageMeta, 'artistName');
5285 isVA = vaParser.test(artist);
5286 album = fmtKanaProp(packageMeta, 'title');
5287 if (packageMeta.bitPerSample) bd = parseInt(packageMeta.bitPerSample);
5288 if (packageMeta.samplingFreq) sr = parseInt(packageMeta.samplingFreq);
5289 if (packageMeta.channelConf) channels = parseInt(packageMeta.channelConf);
5290 if (packageMeta.materialNo) identifiers.MORA_ID = parseInt(packageMeta.materialNo);
5291 if (packageMeta.msin) identifiers.MSIN = packageMeta.msin;
5292 if (packageMeta.distPartNo) identifiers.DISTPARTNO = packageMeta.distPartNo;
5293 if (packageMeta.fullsizeimage) imgUrl = packageMeta.packageUrl + packageMeta.fullsizeimage;
5294 return packageMeta.trackList.map(function(track) {
5295 trackIdentifiers = { TACK_ID: track.musicId, MSIN: track.msin, DISTPARTNO: track.distPartNo };
5296 if (track.labelId) trackIdentifiers.LABEL_ID = track.labelId;
5297 trackArtist = fmtKanaProp(track, 'artistName');
5298 composer = fmtKanaProp(track, 'composer');
5299 var trackLyricist = fmtKanaProp(track, 'lyrics');
5300 if (trackLyricist) if (composer) composer += ' / ' + trackLyricist; else composer = trackLyricist;
5301 switch (track.mediaFormatNo) {
5302 case 10: format = 'AAC'; encoding = 'lossy'; var codecProfile = 'AAC-LC'; bitrate = 320; break;
5303 //case 11: format = 'FLAC'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
5304 case 12: format = 'FLAC'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
5305 case 13: format = 'DSD'; encoding = 'lossless'; codecProfile = undefined; bitrate = undefined; break;
5306 default: format = undefined; encoding = undefined; codecProfile = undefined; bitrate = undefined;
5307 }
5308 return {
5309 artist: isVA ? VA : artist,
5310 album: album,
5311 //album_year: extractYear(releaseDate),
5312 release_date: packageMeta.dispStartDate || packageMeta.dispStartDateStr || packageMeta.startDate,
5313 label: packageMeta.labelcompanyname || packageMeta.displayLabelname || packageMeta.labelname,
5314 catalog: packageMeta.cdPartNo || packageMeta.packageId || packageMeta.distPartNo,
5315 media: media,
5316 genre: genres.join('; '),
5317 codec: format,
5318 codec_profile: codecProfile,
5319 encoding: encoding,
5320 bitrate: /*track.bitPerSample * 1000 || */bitrate,
5321 bd: parseInt(track.bitPerSample) || bd,
5322 sr: parseInt(track.samplingFreq) || sr,
5323 channels: parseInt(track.channelConf) || channels,
5324 tracknumber: track.trackNo,
5325 totaltracks: packageMeta.trackList.length,
5326 composer: composer,
5327 producer: fmtKanaProp(track, 'producer'),
5328 arranger: fmtKanaProp(track, 'arranger'),
5329 title: fmtKanaProp(track, 'title'),
5330 track_artist: trackArtist && (isVA || trackArtist.toLowerCase() != artist.toLowerCase()) ? trackArtist : undefined,
5331 duration: track.duration,
5332 description: packageMeta.metaDescription,
5333 url: packageMeta.webUrl,
5334 cover_url: imgUrl,
5335 identifiers: mergeIds(),
5336 master: packageMeta.master,
5337 };
5338 });
5339
5340 function fmtKanaProp(obj, propName) {
5341 var result = (obj[propName] || '').trim(), kana = (obj[propName.concat('Kana')] || '').trim();
5342 if (kana && prefs.use_kana) if (result) result += ' ('.concat(kana, ')'); else result = kana;
5343 return result || undefined;
5344 }
5345 }); else if (url.toLowerCase().includes('allmusic.com/album/')) return globalFetch(url.replace(/\b(m[wr]\d{10})\b.+$/, '$1')).then(function(response) {
5346 ref = response.document.querySelector('section.main-album a.album-title');
5347 var mainAlbum = (ref != null ? globalFetch(ref.href).then((response, ref) => ({
5348 artist: Array.from(response.document.querySelectorAll('h2[class$="album-artist"] > span[itemprop="name"]'))
5349 .map(span => span.textContent.trim()),
5350 album: (ref = response.document.querySelector('h1.album-title')) != null ? ref.textContent.trim() : undefined,
5351 albumYear: (ref = response.document.querySelector('div.release-date > span')) != null ?
5352 new Date(ref.textContent).getFullYear() || parseInt(ref.textContent) : undefined,
5353 genres: Array.from(response.document.querySelectorAll('div.genre a')).map(a => a.textContent.trim()),
5354 styles: Array.from(response.document.querySelectorAll('div.styles a')).map(a => a.textContent.trim()),
5355 coverUrl: (ref = response.document.querySelector('meta[property="og:image"]')) != null ?
5356 ref.content.replace(/\bf=\d+\b/, 'f=0') : undefined,
5357 id: /\b(mw\d{10})\b/.test(response.finalUrl) && RegExp.$1 || undefined,
5358 })) : Promise.reject(null)).catch(reason => ({}));
5359 var _credits = { mainArtists: [], featured: [], credits: {} };
5360 var credits = globalFetch(response.finalUrl.concat('/credits')).then(function(response) {
5361 response.document.querySelectorAll('section.credits > table > tbody > tr').forEach(function(tr) {
5362 var name = tr.children[0].textContent.trim(), role = tr.children[1].textContent.trim();
5363 if (role == 'Primary Artist') _credits.mainArtists.push(name);
5364 else if (role == 'Featured Artist') _credits.featured.push(name);
5365 else _credits.credits[name] = role;
5366 });
5367 return _credits;
5368 }).catch(reason => _credits);
5369 if (/\b(m[wr]\d{10})\b/.test(response.finalUrl)) identifiers.ALLMUSIC_ID = RegExp.$1;
5370 artist = Array.from(response.document.querySelectorAll('h2[class$="-artist"] > span[itemprop="name"]'))
5371 .map(span => span.textContent.trim());
5372 isVA = artist.length <= 0 || artist.length == 1 && vaParser.test(artist[0]);
5373 album = (ref = response.document.querySelector('h1.release-title')
5374 || response.document.querySelector('h1.album-title')) != null ? ref.textContent.trim() : undefined;
5375 albumYear = (ref = response.document.querySelector('div.year')) != null ? parseInt(ref.textContent) : undefined;
5376 ref = response.document.querySelector('div.release-date > span');
5377 if (identifiers.ALLMUSIC_ID && identifiers.ALLMUSIC_ID.startsWith('mr')) {
5378 releaseDate = ref.textContent.trim();
5379 } else if (identifiers.ALLMUSIC_ID && identifiers.ALLMUSIC_ID.startsWith('mw')) {
5380 albumYear = new Date(ref.textContent).getFullYear() || parseInt(ref.textContent) || albumYear;
5381 }
5382 label = Array.from(response.document.querySelectorAll('div.label a')).map(a => a.textContent.trim()).join(' / ');
5383 catalogue = (ref = response.document.querySelector('div.catalog-number > span')) != null ? ref.textContent.trim() : undefined;
5384 if ((ref = response.document.querySelector('div.format > span')) != null) media = ref.textContent.trim();
5385 genres = Array.from(response.document.querySelectorAll('div.genre a')).map(a => a.textContent.trim());
5386 var styles = Array.from(response.document.querySelectorAll('div.styles a')).map(a => a.textContent.trim());
5387 getDescription(response, 'section.review', false);
5388 var releaseInfo = [];
5389 if ((ref = response.document.querySelector('div.recording-date > div')) != null)
5390 releaseInfo.push('Recording date: '.concat(ref.textContent.trim()));
5391 var locations = Array.from(response.document.querySelectorAll('div.recording-location > ul > li')).map(li => li.textContent.trim());
5392 if (locations.length > 0) releaseInfo.push('Recording location: '.concat(locations.join(' / ')));
5393 locations = Array.from(response.document.querySelectorAll('div.release-info > ul > li')).map(li => li.textContent.trim());
5394 if (locations.length > 0) releaseInfo.push('Release info: '.concat(locations.join(', ')));
5395 if (releaseInfo.length > 0) {
5396 if (description) description += '\n\n';
5397 description += releaseInfo.join('\n');
5398 }
5399 imgUrl = (ref = response.document.querySelector('meta[property="og:image"]')) != null ?
5400 ref.content.replace(/\bf=\d+\b/, 'f=0') : undefined;
5401 trs = response.document.querySelectorAll('section.track-listing table > tbody > tr.track');
5402 return Promise.all([mainAlbum, credits]).then(function(workers) {
5403 if (Object.keys(workers[1].credits).length > 0) {
5404 if (description) description += '\n\n';
5405 description = description.concat('[b]Credits:[/b]\n', Object.keys(workers[1].credits)
5406 .map(artist => '\n'.concat(artist, ' - ', workers[1].credits[artist])));
5407 }
5408 return Array.from(trs).map(function(tr, ndx) {
5409 trackArtist = Array.from(tr.querySelectorAll('td.performer div.primary > a')).map(a => a.textContent.trim());
5410 var trackGuests = Array.from(tr.querySelectorAll('td.performer div.featuring > a')).map(a => a.textContent.trim());
5411 if ((ref = tr.querySelector('div.title > a')) != null && ref.dataset.tooltip) try {
5412 trackIdentifiers = { TRACK_ID: JSON.parse(ref.dataset.tooltip).id };
5413 } catch(e) { trackIdentifiers = {} }
5414 return {
5415 artist: isVA ? VA : undefined,
5416 artists: !isVA ? artist : undefined,
5417 album: album,
5418 release_date: releaseDate,
5419 album_year: workers[0].albumYear || albumYear,
5420 genre: (workers[0].genres || []).concat((workers[0].styles || []), genres, styles).join('; '),
5421 label: label,
5422 catalog: catalogue,
5423 media: media,
5424 discnumber: (ref = tr.parentNode.parentNode.parentNode.querySelector('h3')) != null
5425 && /\b(?:Disc)\s+(\d+)\b/i.test(ref.textContent.trim()) ? parseInt(RegExp.$1) : undefined,
5426 discsubtitle: (ref = tr.parentNode.querySelector('tr.performance-title')) != null ?
5427 ref.textContent.trim() : undefined,
5428 tracknumber: (ref = tr.querySelector('td.tracknum')) != null ? ref.textContent.trim() : undefined,
5429 totaltracks: trs.length,
5430 title: (ref = tr.querySelector('div.title')) != null ? ref.textContent.trim() : undefined,
5431 track_artists: trackArtist.length > 0 && (isVA || !trackArtist.equalCaselessTo(artist) || trackGuests.length > 0) ?
5432 trackArtist.concat(trackGuests) : undefined,
5433 composers: Array.from(tr.querySelectorAll('div.composer > *')).map(node => node.textContent.trim()) || undefined,
5434 duration: (ref = tr.querySelector('td.time')) != null && timeStringToTime(ref.textContent) || undefined,
5435 description: description || undefined,
5436 url: !identifiers.ALLMUSIC_ID ?
5437 (ref = tr.querySelector('meta[property="og:url"]')) != null ? ref.content : response.finalUrl : undefined,
5438 cover_url: workers[0].coverUrl || imgUrl,
5439 identifiers: mergeIds(),
5440 };
5441 });
5442 });
5443 });
5444 if (!weak) clipBoard.value = '';
5445 return Promise.reject(new URL(url).hostname + ' not supported');
5446
5447 function mergeIds() {
5448 var r = Object.assign({}, identifiers, trackIdentifiers);
5449 trackIdentifiers = {};
5450 return r;
5451 }
5452
5453 function getDescription(response, selector, quote = false) {
5454 description = [];
5455 response.document.querySelectorAll(selector).forEach(function(node) {
5456 var p = html2php(node, response.finalUrl).trim();
5457 if (p) description.push(p);
5458 });
5459 description = description.join('\n\n').collapseGaps();
5460 if (quote && description.length > 0 && !description.includes('[quote]')) {
5461 description = '[quote]' + description + '[/quote]';
5462 }
5463 }
5464
5465 function durationFromMeta(elem) {
5466 var m = elem.querySelector('meta[itemprop="duration"]');
5467 if (m == null) return undefined;
5468 if (/^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.test(m.content))
5469 return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0);
5470 m = timeStringToTime(m.content);
5471 return m != null ? m : undefined;
5472 }
5473
5474 function guessDiscNumber() {
5475 if (discParser.test(discSubtitle)) {
5476 discSubtitle = undefined;
5477 discNumber = parseInt(RegExp.$1);
5478 }
5479 }
5480 } // fetchOnline_Music
5481
5482 function parseLastFm(album) {
5483 if (typeof album != 'object') return Promise.reject('invalid object')
5484 var identifiers = {}, description = [];
5485 if (album.id) identifiers.LASTFM_ID = album.id;
5486 if (album.mbid) identifiers.MBID = album.mbid;
5487 if (album.wiki && album.wiki.summary) description.push(album.wiki.summary);
5488 if (album.wiki && album.wiki.content) description.push(album.wiki.content);
5489 var genres = album.tags.tag.map(tag => tag.name);
5490 description = description.join('\n\n');
5491 var imgUrl = ['mega', 'extralarge', '', 'large', 'medium', 'small'].reduce(function(acc, size) {
5492 return acc || album.image.find(image => image.size == size && urlParser.test(image['#text']));
5493 }, undefined);
5494 if (imgUrl) imgUrl = imgUrl['#text'].replace(/\/\d+(?:x\d+|s)\//i, '/');
5495 return album.tracks.track.map((track, ndx) => ({
5496 artist: album.artist,
5497 album: album.name,
5498 genre: genres.join('; ') || undefined,
5499 title: track.name,
5500 tracknumber: ndx + 1,
5501 track_artist: track.artist.name != album.artist ? track.artist.name : undefined,
5502 duration: parseFloat(track.duration) || undefined,
5503 url: album.url,
5504 description: description || undefined,
5505 identifiers: identifiers,
5506 cover_url: imgUrl,
5507 }));
5508 }
5509
5510 function joinArtists(arr, decorator = artist => artist) {
5511 if (!Array.isArray(arr)) return null;
5512 if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', ');
5513 if (arr.length < 3) return arr.map(decorator).join(' & ');
5514 return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop());
5515 }
5516
5517 function loadMoraMetadata(webUrl) {
5518 return /^(?:https?):\/\/(?:\w+\.)*mora\.jp\/package\//i.test(webUrl) ? globalFetch(webUrl).then(function(response1) {
5519 var appArguments = response1.document.querySelector('meta[name="msApplication-Arguments"][content]');
5520 if (appArguments == null) return Promise.reject('Mora.jp: unexpected page format');
5521 appArguments = JSON.parse(appArguments.content);
5522 var materialNo = appArguments.materialNo.toString().padStart(10, '0'), offset = 0;
5523 var packageUrl = 'https://cf.mora.jp/contents/'.concat([
5524 appArguments.type, appArguments.mountPoint, appArguments.labelId,
5525 ].concat([4, 3, 3].map(length => materialNo.slice(offset, offset += length))).join('/'), '/');
5526 return globalFetch(packageUrl.concat('packageMeta.jsonp'), { responseType: 'text' }).then(function(response2) {
5527 return /^\s*\w+\(\s*(\{[\S\s]+\})\s*\);\s*$/.test(response2.responseText) ? Object.assign(JSON.parse(RegExp.$1), {
5528 mountPoint: appArguments.mountPoint,
5529 webUrl: response1.finalUrl.replace(/[\?\#].*$/, ''),
5530 }) : Promise.reject('Mora.jp: Unexpected package meta format');
5531 });
5532 }) : Promise.reject('Not mora.jp site URL');
5533 }
5534
5535 function splitArtists(str, parsers = multiArtistParsers) {
5536 var result = [str];
5537 parsers.forEach(function(parser) {
5538 for (var i = result.length; i > 0; --i) {
5539 var j = result[i - 1].split(parser).map(strip);
5540 if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
5541 && !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
5542 }
5543 });
5544 return result;
5545 }
5546
5547 function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
5548 function looksLikeTrueName(artist, index = 0) {
5549 return twoOrMore(artist)
5550 && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
5551 && artist.split(/\s+/).length >= 2
5552 && !pseudoArtistParsers.some(rx => rx.test(artist)) || typeof getSiteArtist(artist) == 'object';
5553 }
5554
5555 function strip(art) {
5556 return artistStrips.reduce(function(acc, rx, ndx) {
5557 return ndx != 1 || rx.test(acc) && !notMonospaced(RegExp.$1) ? acc.replace(rx, '') : acc;
5558 }, art);
5559 }
5560
5561 function getSiteArtist(artist) {
5562 //if (isOPS) return undefined;
5563 if (!artist || notSiteArtistsCache.includesCaseless(artist)) return null;
5564 var key = Object.keys(siteArtistsCache).find(it => it.toLowerCase() == artist.toLowerCase());
5565 if (key) return siteArtistsCache[key];
5566 var now = Date.now();
5567 try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
5568 if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
5569 apiTimeFrame.timeStamp = now;
5570 apiTimeFrame.requestCounter = 1;
5571 } else ++apiTimeFrame.requestCounter;
5572 window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
5573 if (apiTimeFrame.requestCounter > 5) {
5574 console.debug('getSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
5575 artist + '" (' + apiTimeFrame.requestCounter + ')');
5576 if (prefs.messages_verbosity >= 2) addMessage('AJAX API request exceeding time frame: artistname="' +
5577 artist + '" (' + apiTimeFrame.requestCounter + ')', 'notice');
5578 ++ajaxRejects;
5579 return undefined;
5580 }
5581 try {
5582 var requestUrl = '/ajax.php?action=artist&artistname='.concat(encodeURIComponent(artist));
5583 xhr.open('GET', requestUrl, false);
5584 if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
5585 xhr.send();
5586 if (xhr.status == 404) {
5587 notSiteArtistsCache.pushUniqueCaseless(artist);
5588 return null;
5589 }
5590 if (xhr.readyState != XMLHttpRequest.DONE || xhr.status < 200 || xhr.status >= 400) {
5591 console.log('getSiteArtist("' + artist + '") status:', xhr.status, 'url:', document.location.origin.concat(requestUrl));
5592 return undefined; // error
5593 }
5594 let response = JSON.parse(xhr.responseText);
5595 if (response.status != 'success') {
5596 notSiteArtistsCache.pushUniqueCaseless(artist);
5597 return null;
5598 }
5599 return (siteArtistsCache[artist] = response.response);
5600 } catch(e) {
5601 console.error('UA::getSiteArtist("' + artist + '"):', e, xhr);
5602 return undefined;
5603 }
5604 }
5605 } // fillFromText_Music
5606
5607 function fillFromText_Apps(weak = false) {
5608 if (messages != null) messages.parentNode.removeChild(messages);
5609 if (!urlParser.test(clipBoard.value)) {
5610 addMessage('valid URL accepted for this category', 'critical');
5611 return false;
5612 }
5613 sourceUrl = RegExp.$1;
5614 var description, tags = new TagManager();
5615 if (sourceUrl.toLowerCase().includes('://sanet')) return globalFetch(sourceUrl).then(function(response) {
5616 i = response.document.querySelector('h1.item_title > span');
5617 var title = i == null ? undefined : i.textContent
5618 .replace(/\s+\((?:x|ia|em)(?:64)\)/ig, ' (64-bit)')
5619 .replace(/\s+\(x(?:86|32)\)/ig, ' (32-bit)')
5620 .replace(/\s+(?:Build)\s+(\d+)\b/g, ' build $1')
5621 .replace(/\s+(?:Multilingual|Multi(?:-|\s)*lang(?:uage)?)\b/g, ' multilingual');
5622 description = html2php(response.document.querySelector('section.descr'), response.finalUrl).trim();
5623 if (/\s*^[ \t]*(?:\[i\]\[\/i\])?Homepage\s*$.*/im.test(description)) description = RegExp.leftContext;
5624 description = description.split(/[ \t]*\r?\n/).slice(6).map(line => line.trim()).join('\n')
5625 .replace(/^[ \t]*(?:\[i\]\[\/i\])?Screenshots:?\s*/igm, '')
5626 .replace(/^[ \t]*(?:\[i\]\[\/i\])?(\[b\]Release\s+Notes:?\[\/b\])(?:[ \t]*\r?\n)+/igm, '$1\n')
5627 .replace(/\[hr\]/ig, '\n');
5628 ref = response.document.querySelector('section.descr > div.release-info');
5629 var releaseInfo = ref != null && ref.textContent.trim();
5630 if (/\b(?:Languages?)\s*:\s*(.*?)\s*(?:$|\|)/i.exec(releaseInfo) != null)
5631 description += '\n\n[b]Languages:[/b]\n' + RegExp.$1;
5632 if ((ref = response.document.querySelector('div.txtleft > a')) != null) {
5633 description += '\n\n[b]Product page:[/b]\n[url]' +
5634 removeRedirect(ref.pathname.toLowerCase().startsWith('/confirm/url/') && urlParser.test(ref.textContent) ?
5635 ref.textContent.trim() : ref.href) + '[/url]';
5636 }
5637 writeDescription(description.collapseGaps());
5638 if ((ref = response.document.querySelector('section.descr > div.center > a.mfp-image')) != null) {
5639 setCover(ref.href);
5640 } else {
5641 ref = response.document.querySelector('section.descr > div.center > img[data-src]');
5642 if (ref != null) setCover(ref.dataset.src);
5643 }
5644 var internalTags = Array.from(response.document.querySelectorAll('ul.item_tags_list > li > a[rel="tag"]'))
5645 .map(elem => elem.textContent.toLowerCase().trim());
5646 if ((ref = response.document.querySelector('a.cat:last-of-type > span')) != null) {
5647 if (ref.textContent.toLowerCase() == 'windows') {
5648 tags.add('apps.windows');
5649 if (/\b(?:(?:x|ia|em)64)\b/i.test(releaseInfo) || /\(64[-\s]*bit\)/i.test(title)) tags.add('win64');
5650 if (/\b(?:x86|x32)\b/i.test(releaseInfo) || /\(32[-\s]*bit\)/i.test(title)) tags.add('win32');
5651 }
5652 if (ref.textContent.toLowerCase() == 'macos') tags.add('apps.mac');
5653 if (ref.textContent.toLowerCase() == 'linux' || ref.textContent.toLowerCase() == 'unix') tags.add('apps.linux');
5654 if (ref.textContent.toLowerCase() == 'android') tags.add('apps.android');
5655 if (ref.textContent.toLowerCase() == 'ios') tags.add('apps.ios');
5656 }
5657 if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
5658 if (title && !/\(\d+-?bit\)/i.test(title)) {
5659 if (tags.includes('win64') && !tags.includes('win32')) title += ' (64-bit)';
5660 if (tags.includes('win32') && !tags.includes('win64')) title += ' (32-bit)';
5661 }
5662 if (elementWritable(ref = document.getElementById('title'))) ref.value = title || '';
5663 });
5664 if (!weak) {
5665 addMessage('this domain not supported', 'critical');
5666 clipBoard.value = '';
5667 }
5668 return Promise.reject('this domain not supported');
5669 } // fillFromText_Apps
5670
5671 function fillFromText_Ebooks(weak = false) {
5672 if (messages != null) messages.parentNode.removeChild(messages);
5673 if (!urlParser.test(clipBoard.value)) {
5674 addMessage('only URL accepted for this category', 'critical');
5675 return Promise.reject('only URL accepted for this category');
5676 }
5677 sourceUrl = RegExp.$1;
5678 var description, tags = new TagManager();
5679 if (sourceUrl.toLowerCase().includes('martinus.cz') || sourceUrl.toLowerCase().includes('martinus.sk'))
5680 return globalFetch(sourceUrl).then(function(response) {
5681 function get_detail(x, y) {
5682 var ref = response.document.querySelector('section#details > div > div > div:first-of-type > div:nth-child(' +
5683 x + ') > dl:nth-child(' + y + ') > dd');
5684 return ref != null ? ref.textContent.trim() : null;
5685 }
5686
5687 i = response.document.querySelectorAll('article > ul > li > a');
5688 if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
5689 description = joinAuthors(i);
5690 if ((i = response.document.querySelector('article > h1')) != null) description += ' – ' + i.textContent.trim();
5691 i = response.document.querySelector('div.bar.mb-medium > div:nth-child(1) > dl > dd > span');
5692 if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
5693 ref.value = description;
5694 }
5695
5696 ref = response.document.querySelector('section#description > div');
5697 if (ref != null) description = html2php(ref).replace(/^\s*\[img\].*?\[\/img\]\s*/i, '').trim();
5698 if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
5699 const translation_map = [
5700 [/\b(?:originál)/i, 'Original title'],
5701 [/\b(?:datum|dátum|rok)\b/i, 'Release date'],
5702 [/\b(?:katalog|katalóg)/i, 'Catalogue #'],
5703 [/\b(?:stran|strán)\b/i, 'Page count'],
5704 [/\bjazyk/i, 'Language'],
5705 [/\b(?:nakladatel|vydavatel)/i, 'Publisher'],
5706 [/\b(?:doporuč|ODPORÚČ)/i, 'Age rating'],
5707 ];
5708 response.document.querySelectorAll('section#details > div > div > div:first-of-type > div > dl').forEach(function(detail) {
5709 var lbl = detail.children[0].textContent.trim();
5710 var val = detail.children[1].textContent.trim();
5711 if (/\b(?:rozm)/i.test(lbl) || /\b(?:vazba|vázba)\b/i.test(lbl)) return;
5712 translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
5713 if (/\b(?:ISBN)\b/i.test(lbl)) {
5714 sourceUrl = new URL('https://www.worldcat.org/isbn/'.concat(detail.children[1].textContent.trim()));
5715 val = '[url=' + sourceUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
5716 findOCLC(sourceUrl);
5717// } else if (/\b(?:ISBN)\b/i.test(lbl)) {
5718// val = '[url=https://www.goodreads.com/search/search?q=' + detail.children[1].textContent.trim() +
5719// '&search_type=books]' + detail.children[1].textContent.trim() + '[/url]';
5720 }
5721 description += '\n[b]' + lbl + ':[/b] ' + val;
5722 });
5723 sourceUrl = new URL(response.finalUrl);
5724 description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.href + '[/url]';
5725 writeDescription(description.collapseGaps());
5726
5727 if ((i = response.document.querySelector('a.mj-product-preview > img')) != null) {
5728 setCover(i.src.replace(/\?.*/, ''));
5729 } else if ((i = response.document.querySelector('head > meta[property="og:image"][content]')) != null) {
5730 setCover(i.content.replace(/\?.*/, ''));
5731 }
5732
5733 response.document.querySelectorAll('dd > ul > li > a').forEach(x => { tags.add(x.textContent) });
5734 if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) {
5735 ref.value = tags.toString();
5736 }
5737 });
5738 else if (sourceUrl.toLowerCase().includes('goodreads.com')) return globalFetch(sourceUrl).then(function(response) {
5739 i = response.document.querySelectorAll('a.authorName > span');
5740 if (i.length > 0 && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
5741 description = joinAuthors(i);
5742 if ((i = response.document.querySelector('h1#bookTitle')) != null) description += ' – ' + i.textContent.trim();
5743 if ((i = response.document.querySelector('div#details > div.row:nth-of-type(2)')) != null
5744 && (i = extractYear(i.textContent))) description += ' (' + i + ')';
5745 ref.value = description;
5746 }
5747
5748 var description = [];
5749 response.document.querySelectorAll('div#description span:last-of-type').forEach(function(node) {
5750 description = html2php(node, sourceUrl).trim();
5751 });
5752 if (description.length > 0 && !description.includes('[quote]')) {
5753 description = '[quote]' + description.trim() + '[/quote]';
5754 }
5755
5756 function strip(str) {
5757 return typeof str == 'string' ?
5758 str.replace(/\s{2,}/g, ' ').replace(/[\n\r]+/, '').replace(/\s*\.{3}(?:less|more)\b/g, '').trim() : null;
5759 }
5760
5761 response.document.querySelectorAll('div#details > div.row').forEach(k => { description += '\n' + strip(k.innerText) });
5762 description += '\n';
5763
5764 response.document.querySelectorAll('div#bookDataBox > div.clearFloats').forEach(function(detail) {
5765 var lbl = detail.children[0].textContent.trim();
5766 var val = strip(detail.children[1].textContent);
5767 if (/\b(?:ISBN)\b/i.test(lbl) && (/\b(\d{13})\b/.test(val) || /\b(\d{10})\b/.test(val))) {
5768 sourceUrl = new URL('https://www.worldcat.org/isbn/'.concat(RegExp.$1));
5769 val = '[url=' + sourceUrl.href + ']' + strip(detail.children[1].textContent) + '[/url]';
5770 findOCLC(sourceUrl);
5771 }
5772 description += '\n[b]' + lbl + ':[/b] ' + val;
5773 });
5774 if ((ref = response.document.querySelector('span[itemprop="ratingValue"]')) != null) {
5775 description += '\n[b]Rating:[/b] ' + Math.round(parseFloat(ref.firstChild.textContent) * 20) + '%';
5776 }
5777 sourceUrl = new URL(response.finalUrl);
5778// if ((ref = response.document.querySelector('div#buyButtonContainer > ul > li > a.buttonBar')) != null) {
5779// let u = new URL(ref.href);
5780// description += '\n[url=' + sourceUrl.origin + u.pathname + '?' + u.search + ']Libraries[/url]';
5781// }
5782 description += '\n\n[b]More info and reviews:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
5783 response.document.querySelectorAll('div.clearFloats.bigBox').forEach(function(bigBox) {
5784 if (bigBox.id == 'aboutAuthor' && (ref = bigBox.querySelector('h2 > a')) != null) {
5785 description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
5786 if ((ref = bigBox.querySelector('div.bigBoxBody a > div[style*="background-image"]')) != null) {
5787 }
5788 if ((ref = bigBox.querySelector('div.bookAuthorProfile__about > span[id]:last-of-type')) != null) {
5789 description += '\n' + html2php(ref, sourceUrl).trim().replace(/^\[i\]Librarian\s+Note:.*?\[\/i\]\s+/i, '');
5790 }
5791 } else if ((ref = bigBox.querySelector('h2 > a[href^="/trivia/"]')) != null) {
5792 description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
5793 if ((ref = bigBox.querySelector('div.bigBoxContent > div.mediumText')) != null) {
5794 description += '\n' + ref.firstChild.textContent.trim();
5795 }
5796// } else if ((ref = bigBox.querySelector('h2 > a[href^="/work/quotes/"]')) != null) {
5797// description += '\n\n[b][url=' + ref.href + ']' + ref.textContent.trim() + '[/url][/b]';
5798// bigBox.querySelectorAll('div.bigBoxContent > div.stacked > span.readable').forEach(function(quote) {
5799// description += '\n' + ref.firstChild.textContent.trim();
5800// });
5801 }
5802 });
5803 writeDescription(description.collapseGaps());
5804 if ((ref = response.document.querySelector('div.editionCover > img')) != null) setCover(ref.src.replace(/\?.*/, ''));
5805 response.document.querySelectorAll('div.elementList > div.left').forEach(tag => { tags.add(tag.textContent.trim()) });
5806 if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
5807 }); else if (sourceUrl.toLowerCase().includes('databazeknih.cz')) {
5808 if (!sourceUrl.toLowerCase().includes('show=alldesc')) {
5809 if (!sourceUrl.includes('?')) { sourceUrl += '?show=alldesc' } else { sourceUrl += '&show=alldesc' }
5810 }
5811 return globalFetch(sourceUrl).then(function(response) {
5812 i = response.document.querySelectorAll('span[itemprop="author"] > a');
5813 if (i != null && elementWritable(ref = document.getElementById('title') || document.querySelector('input[name="title"]'))) {
5814 description = joinAuthors(i);
5815 if ((i = response.document.querySelector('h1[itemprop="name"]')) != null)
5816 description += ' – ' + i.textContent.trim();
5817 i = response.document.querySelector('span[itemprop="datePublished"]');
5818 if (i != null && (i = extractYear(i.textContent))) description += ' (' + i + ')';
5819 ref.value = description;
5820 }
5821
5822 ref = response.document.querySelector('p[itemprop="description"]');
5823 if (ref != null) description = html2php(ref, sourceUrl).trim();
5824 if (description.length > 0 && !description.includes('[quote]')) description = '[quote]' + description + '[/quote]';
5825 const translation_map = [
5826 [/\b(?:orig)/i, 'Original title'],
5827 [/\b(?:série)\b/i, 'Series'],
5828 [/\b(?:vydáno)\b/i, 'Released'],
5829 [/\b(?:stran)\b/i, 'Page count'],
5830 [/\b(?:jazyk)\b/i, 'Language'],
5831 [/\b(?:překlad)/i, 'Translation'],
5832 [/\b(?:autor obálky)\b/i, 'Cover author'],
5833 ];
5834 response.document.querySelectorAll('table.bdetail tr').forEach(function(detail) {
5835 var lbl = detail.children[0].textContent.trim();
5836 var val = detail.children[1].textContent.trim();
5837 if (/(?:žánr|\bvazba)\b/i.test(lbl)) return;
5838 translation_map.forEach(k => { if (k[0].test(lbl)) lbl = k[1] });
5839 if (/\b(?:ISBN)\b/i.test(lbl) && /\b(\d+(?:-\d+)*)\b/.exec(val) != null) {
5840 sourceUrl = new URL('https://www.worldcat.org/isbn/'.concat(RegExp.$1.replace(/-/g, '')));
5841 val = '[url=' + sourceUrl.href + ']' + detail.children[1].textContent.trim() + '[/url]';
5842 findOCLC(sourceUrl);
5843 }
5844 description += '\n[b]' + lbl + '[/b] ' + val;
5845 });
5846
5847 sourceUrl = new URL(response.finalUrl);
5848 description += '\n\n[b]More info:[/b]\n[url]' + sourceUrl.origin + sourceUrl.pathname + '[/url]';
5849 writeDescription(description.collapseGaps());
5850
5851 if ((ref = response.document.querySelector('div#icover_mid > a')) != null) setCover(ref.href.replace(/\?.*/, ''));
5852 if ((ref = response.document.querySelector('div#lbImage')) != null && /\burl\("(.*)"\)/i.test(i.style.backgroundImage)) {
5853 setCover(RegExp.$1.replace(/\?.*/, ''));
5854 }
5855
5856 response.document.querySelectorAll('h5[itemprop="genre"] > a').forEach(tag => { tags.add(tag.textContent.trim()) });
5857 response.document.querySelectorAll('a.tag').forEach(tag => { tags.add(tag.textContent.trim()) });
5858 if (tags.length > 0 && elementWritable(ref = document.getElementById('tags'))) ref.value = tags.toString();
5859 });
5860 }
5861 if (!weak) {
5862 addMessage('domain not supported', 'critical');
5863 clipBoard.value = '';
5864 }
5865 return Promise.reject('domain not supported');
5866
5867 function joinAuthors(nodeList) {
5868 if (typeof nodeList != 'object') return null;
5869 return Array.from(nodeList).map(it => it.textContent.trim()).join(' & ');
5870 }
5871
5872 function findOCLC(url) {
5873 if (!url) return false;
5874 var oclc = document.querySelector('input[name="oclc"]');
5875 if (!elementWritable(oclc)) return false;
5876 globalFetch(url).then(function(dom) {
5877 var ref = dom.querySelector('tr#details-oclcno > td:last-of-type');
5878 if (ref != null) oclc.value = ref.textContent.trim();
5879 });
5880 return true;
5881 }
5882 } // fillFromText_Ebooks
5883
5884 function preview(n) {
5885 if (!prefs.auto_preview) return;
5886 var btn = document.querySelector('input.button_preview_' + n + '[type="button"][value="Preview"]');
5887 if (btn != null) btn.click();
5888 }
5889
5890 function writeDescription(desc) {
5891 if (typeof desc != 'string') return;
5892 if (elementWritable(ref = document.querySelector('textarea#desc')
5893 || document.querySelector('textarea#description') || document.querySelector('textarea#album_desc'))) ref.value = desc;
5894 if ((ref = document.getElementById('body')) != null && !ref.disabled) {
5895 if (ref.value.length > 0) ref.value += '\n\n';
5896 ref.value += desc;
5897 }
5898 }
5899
5900 function loadGoogleData(response) {
5901 const initDataParser = /\b(?:AF_initDataCallback)\s*\(\s*\{\s*key:\s*'ds:(\d+)'.*data:\s*function\(\)\s*{\s*return\s*([\S\s]+)\}\s*\}\s*\);/;
5902 return Array.from(response.document.querySelectorAll('script[nonce]'))
5903 .map(function(script) { try { return eval(initDataParser.exec(script.text)[2]) } catch(e) { return false } })
5904 .filter(obj => obj && typeof obj == 'object');
5905 }
5906
5907 function queryItunesAPI(key, params) {
5908 return queryGenericAPI('itunes.apple.com', key, params);
5909 }
5910 function queryDeezerAPI(key, params) {
5911 return queryGenericAPI('api.deezer.com', key, params);
5912 }
5913 function queryDiscogsAPI(key, params) {
5914 if (prefs.discogs_key && prefs.discogs_secret) {
5915 var hdr = { Authorization: 'Discogs key=' + prefs.discogs_key + ', secret=' + prefs.discogs_secret };
5916 } else if (discogs_token) hdr = { Authorization: 'Discogs token=' + discogs_token };
5917 return queryGenericAPI('api.discogs.com', key, params, hdr);
5918 }
5919 function queryMusicBrainzAPI(key, params) {
5920 return queryGenericAPI('musicbrainz.org', 'ws/2/' + key + '/', Object.assign({ fmt: 'json' }, params));
5921 }
5922 function querySpotifyAPI(key, params) {
5923 return key ? setOauth2Token().then(credentials => queryGenericAPI('api.spotify.com', 'v1/' + key, params, {
5924 Authorization: credentials.token_type + ' ' + credentials.access_token,
5925 })) : Promise.reject('No API keyword');
5926
5927 function setOauth2Token() {
5928 try { var accessToken = JSON.parse(window.localStorage.spotifyAccessToken) } catch(e) { }
5929 if (isTokenValid(accessToken)) {
5930 if (prefs.diag_mode) console.debug('Re-used Spotify access token:', accessToken, new Date(accessToken.expires).toLocaleTimeString());
5931 return Promise.resolve(accessToken);
5932 }
5933 if (!spotify_clientid || !spotify_clientsecret) return Promise.reject('Spotify credentials not configured');
5934 const data = new URLSearchParams({
5935 'grant_type': 'client_credentials',
5936 });
5937 return globalFetch('https://accounts.spotify.com/api/token', { responseType: 'json', headers: {
5938 Authorization: 'Basic ' + btoa(spotify_clientid + ':' + spotify_clientsecret),
5939 } }, data).then(function(response) {
5940 accessToken = response.response;
5941 accessToken.expires = Date.now() + accessToken.expires_in * 1000;
5942 if (!isTokenValid(accessToken)) return Promise.reject('Invalid token received');
5943 delete accessToken.expires_in;
5944 window.localStorage.spotifyAccessToken = JSON.stringify(accessToken);
5945 if (prefs.diag_mode) console.debug('Spotify access token successfully set:', accessToken);
5946 return accessToken;
5947 });
5948 }
5949
5950 function isTokenValid(accessToken) {
5951 return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
5952 && accessToken.expires >= Date.now() + 30000;
5953 }
5954 }
5955 function queryLastFmAPI(method, params) {
5956 return lastfm_api_key ? queryGenericAPI('ws.audioscrobbler.com', '2.0/', Object.assign({
5957 method: method,
5958 api_key: lastfm_api_key,
5959 format: 'json',
5960 }, params || {})) : Promise.reject('Last.fm API key not configured');
5961 }
5962 function queryTidalAPI(key, params, countryCode) {
5963 if (!key) return Promise.reject('API action not defined');
5964 if (typeof params != 'object') params = {};
5965 params.deviceType = 'BROWSER';
5966 params.countryCode = countryCode;
5967 return setOauth2Token().then(function(token) {
5968 if (!params.countryCode) params.countryCode = token.user.countryCode || 'US';
5969 return { Authorization: token.token_type + ' ' + token.access_token };
5970 }).catch(function(reason) {
5971 console.warn('Tidal Oauth2 failed:', reason);
5972 return setSession().then(function(session) {
5973 if (!params.countryCode) params.countryCode = session.countryCode || 'US';
5974 return { 'X-Tidal-SessionId': session.sessionId };
5975 });
5976 }).then(header => queryGenericAPI('listen.tidal.com', 'v1/pages/'.concat(key), params, header));
5977
5978 function setOauth2Token() {
5979 try { var accessToken = JSON.parse(window.localStorage.tidalAccessToken) } catch(e) { }
5980 if (isTokenValid(accessToken)) {
5981 if (prefs.diag_mode) console.debug('Re-used Tidal access token:', accessToken, new Date(accessToken.expires).toLocaleTimeString());
5982 return Promise.resolve(accessToken);
5983 }
5984 return Promise.reject('Not implemented');
5985 // TODO
5986 }
5987 function setSession() {
5988 try { var session = JSON.parse(window.sessionStorage.tidalSession) } catch(e) { }
5989 if (isSessionValid(session)) {
5990 if (prefs.diag_mode) console.debug('Re-used Tidal session:', session);
5991 return Promise.resolve(session);
5992 }
5993 if (!prefs.tidal_userid || !prefs.tidal_userpassword) return Promise.reject('Tidal user credentials not configured');
5994 const deviceTokens = [
5995 'BI218mwp9ERZ3PFI', // browser | Streams lossless quality
5996 'wdgaB1CilGA-S_s2', // browser | Streams HIGH/LOW Quality over RTMP, FLAC and Videos over HTTP, but many Lossless Streams are encrypted.
5997 '4zx46pyr9o8qZNRw', // browser(?) | other quality
5998 'kgsOOmYk3zShYrNP', // Android | All Streams are HTTP Streams. Correct numberOfVideos in Playlists (best Token to use)
5999 'GvFhCVAYp3n43EN3', // iOS | Same as Android Token, but uses ALAC instead of FLAC
6000 '_DSTon1kC8pABnTw', // iOS | Same as Android Token, but uses ALAC instead of FLAC
6001 '4zx46pyr9o8qZNRw', // native | Same as Android Token, but FLAC streams are encrypted
6002 'BI218mwp9ERZ3PFI', // audirvana | Like Android Token, supports MQA, but returns 'numberOfVideos = 0' in Playlists
6003 'wc8j_yBJd20zOmx0', // amarra | Like Android Token, but returns 'numberOfVideos = 0' in Playlists
6004 'P5Xbeo5LFvESeDy6', // Like Android Token, but returns 'numberOfVideos = 0' in Playlists
6005 '_KM2HixcUBZtmktH', // Same as previous
6006 'oIaGpqT_vQPnTr0Q', // Same, but uses RTMP for HIGH/LOW Quality
6007 ];
6008 return getClientToken().then(function(token) {
6009 const data = new URLSearchParams({
6010 username: prefs.tidal_userid,
6011 password: prefs.tidal_userpassword,
6012 clientUniqueKey: getClientKey(),
6013 clientVersion: '1.0',
6014 token: deviceTokens[2],
6015 });
6016 return globalFetch('https://api.tidalhifi.com/v1/login/username', {
6017 responseType: 'json',
6018 headers: token ? { 'X-Tidal-Token': token } : undefined,
6019 }, data);
6020 }).then(function(response) {
6021 if (!isSessionValid(session = response.response)) return Promise.reject('Invalid session');
6022 window.sessionStorage.tidalSession = JSON.stringify(session);
6023 if (prefs.diag_mode) console.debug('Tidal session successfully established:', session);
6024 return session;
6025 });
6026 }
6027 function uuidv4() {
6028 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
6029 var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
6030 return v.toString(16);
6031 });
6032 }
6033 function randomString(length) {
6034 const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
6035 var text = "";
6036 for (var i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length));
6037 return text;
6038 }
6039 function generateCodeChallenge(code_verifier) {
6040 return code_challenge = base64URL(CryptoJS.SHA256(code_verifier))
6041 }
6042 function base64URL(string) {
6043 return string.toString(CryptoJS.enc.Base64).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
6044 }
6045 function getClientId() {
6046 if (prefs.tidal_clientid || (prefs.tidal_clientid = GM_getValue('tidal_clientid'))) return Promise.resolve(prefs.tidal_clientid);
6047 return getTidalSecrets().then(function(response) {
6048 const rx = /"(\w{40})":"(\w{16})"/g;
6049 if ((i = response.responseText.match(rx)) == null || !rx.test(i.shift())) return Promise.reject('not found');
6050 GM_setValue('tidal_clientid', prefs.tidal_clientid = RegExp.$2);
6051 if (prefs.diag_mode) console.debug('Successfully configured Tidal client Id:', prefs.tidal_clientid);
6052 return prefs.tidal_clientid;
6053 }).catch(function(reason) {
6054 reason = 'Client Id auto detection failed ('.concat(reason, '), set it manually (tidal_clientid)');
6055 alert(reason);
6056 return Promise.reject(reason);
6057 });
6058 }
6059 function getClientKey() {
6060 if (!prefs.tidal_clientkey && !(prefs.tidal_clientkey = GM_getValue('tidal_clientkey')))
6061 GM_setValue('tidal_clientkey', prefs.tidal_clientkey = uuidv4());
6062 return prefs.tidal_clientkey;
6063 }
6064 function getClientToken() {
6065 if (prefs.tidal_token || (prefs.tidal_token = GM_getValue('tidal_token'))) return Promise.resolve(prefs.tidal_token);
6066 return getTidalSecrets().then(function(response) {
6067 if (!/"(\w{40})":"(\w{40})"/.test(response.responseText)) return Promise.reject('not found');
6068 GM_setValue('tidal_token', prefs.tidal_token = RegExp.$2);
6069 if (prefs.diag_mode) console.debug('Successfully configured Tidal token:', prefs.tidal_token);
6070 return prefs.tidal_token;
6071 }).catch(function(reason) {
6072 console.warn('Tidal token detection fail (' + reason + ')');
6073 return undefined;
6074 });
6075 }
6076 function getTidalSecrets() {
6077 return globalFetch('https://listen.tidal.com/app.61d2e10fa09e5075c5ad.chunk.js', { responseType: 'text' });
6078 }
6079 function isTokenValid(accessToken) {
6080 return typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token
6081 && accessToken.expires >= Date.now() + 30000;
6082 }
6083 function isSessionValid(session) {
6084 return typeof session == 'object' && session.userId > 0 && session.sessionId;
6085 }
6086 }
6087
6088 function queryGenericAPI(domain, key, params, headers) {
6089 //if (!key) return Promise.reject(new Error('Keyword missing'));
6090 var retryCount = 0;
6091 return new Promise(function(resolve, reject) {
6092 var url = 'https://' + domain + '/' + key;
6093 var query = new URLSearchParams(params || undefined).toString();
6094 if (query.length > 0) url += '?' + query;
6095 if (typeof headers != 'object') headers = {};
6096 headers.Accept = 'application/json';
6097 queryInternal();
6098
6099 function queryInternal() {
6100 GM_xmlhttpRequest({
6101 method: 'GET',
6102 url: url,
6103 responseType: 'json',
6104 headers: headers,
6105 onload: function(response) {
6106 if (response.status >= 200 && response.status < 400) resolve(response.response);
6107 else reject(defaultErrorHandler(response));
6108 },
6109 onerror: response => { reject(defaultErrorHandler(response)) },
6110 ontimeout: response => { reject(defaultTimeoutHandler(response)) },
6111 });
6112 }
6113 });
6114 }
6115
6116 function loadItunesMetadata(urlOrId) {
6117 return (function() {
6118 return /^https:\/\/apple\.co\//i.test(urlOrId) ? urlResolver(urlOrId).then(url => getAppleId(url)) : getAppleId(urlOrId);
6119
6120 function getAppleId(urlOrId) {
6121 var appleId = parseInt(urlOrId) || itunesRlsParser.test(urlOrId) && parseInt(RegExp.$1);
6122 return appleId ? Promise.resolve(appleId) : Promise.reject('Aplpe Id cannot be determined');
6123 }
6124 })().then(appleId => globalFetch('https://music.apple.com/album/'.concat(appleId)).then(function(response) {
6125 var params = response.document.querySelector('meta[name="desktop-music-app/config/environment"][content]');
6126 if (params == null) return Promise.reject('Desktop environment not located');
6127 params = JSON.parse(decodeURIComponent(params.content));
6128 if (prefs.diag_mode) console.debug('Got Apple Music desktop environment:', params);
6129 if (!params.MEDIA_API.token) return Promise.reject('Apple access token not found');
6130 var query = new URLSearchParams({
6131 include: 'tracks,artists',
6132 l: 'en-US',
6133 });
6134 return globalFetch(params.MUSIC.BASE_URL.concat('/catalog/us/albums/', appleId, '?', query), { responseType: 'json', headers: {
6135 'Referer': response.finalUrl,
6136 'Authorization': 'Bearer '.concat(params.MEDIA_API.token),
6137 } }).then(function(response2) {
6138 var album = response2.response.data[0];
6139 album.description = response.document.querySelector('div.content-modal__content-container')
6140 || response.document.querySelector('div.product-page-header__notes span'),
6141 album.url = response.finalUrl;
6142 if (album.attributes.artwork) album.attributes.artwork.realUrl = album.attributes.artwork.url
6143 .replace('{w}', album.attributes.artwork.width).replace('{h}', album.attributes.artwork.height);
6144 if (prefs.diag_mode) console.debug('Apple Music metadata received:', album);
6145// query.set('include', 'artists,albums');
6146// Promise.all(album.relationships.tracks.data.map(track => globalFetch(params.MUSIC.BASE_URL.concat('/catalog/us/songs/', track.id, '?', query), { responseType: 'json', headers: {
6147// 'Referer': response.finalUrl,
6148// 'Authorization': 'Bearer '.concat(params.MEDIA_API.token),
6149// } }).then(response => response.response))).then(tracks => { console.debug('Apple Music tracks received:', tracks) })
6150// .catch(reason => { console.error(reason) });
6151 return album;
6152 });
6153 }));
6154 }
6155
6156 function getMusicBrainzCovers(mbid) {
6157 return searchInternal('release', mbid).then(covers => covers || searchMaster(), searchMaster);
6158
6159 function searchInternal(entity, mbid) {
6160 return new Promise((resolve, reject) => GM_xmlhttpRequest({
6161 method: 'GET',
6162 url: 'https://coverartarchive.org/'.concat(entity, '/', mbid),
6163 responseType: 'json',
6164 onload: function(response) {
6165 if (response.status == 404) return resolve(null);
6166 if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
6167 var images = response.response.images
6168 .filter(image => urlParser.test(image.image) && image.isfront
6169 || Array.isArray(image.types) && image.types.includesCaseless('Front'))
6170 .map(image => image.image);
6171 resolve(images.length > 0 ? [response.response.release, images] : null);
6172 },
6173 onerror: error => reject(defaultErrorHandler(error)),
6174 ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
6175 }));
6176 }
6177 function searchMaster() {
6178 return queryMusicBrainzAPI('release/' + mbid, { inc: 'release-groups' })
6179 .then(release => searchInternal('release-group', release['release-group'].id));
6180 }
6181 }
6182
6183 function setCover(url) {
6184 if (!urlParser.test(url)) return Promise.reject('Image url not valid');
6185 var image = document.getElementById('image') || document.querySelector('input[name="image"]');
6186 if (!elementWritable(image)) return Promise.reject('Image input not available');
6187 return verifyImageUrl(url).then(function(imageUrl) {
6188 if (!isNWCD) {
6189 image.value = imageUrl;
6190 coverPreview(image, imageUrl);
6191 if (prefs.auto_rehost_cover && !imageUrl.toLowerCase().startsWith(ptpimgOrigin)) {
6192 image.disabled = true;
6193 return rehost2PTPIMG([imageUrl])
6194 .then(urls => urls.length > 0 ? (image.value = urls[0]) : imageUrl)
6195 .catch(reason => { /*alert(reason)*/ addMessage(reason.concat(' (not rehosted)'), 'warning') })
6196 .then(url => { image.disabled = false; return url });
6197 }
6198 return imageUrl;
6199 } else return uploadToImagehost(imageUrl).then(function(result) {
6200 image.value = result.url;
6201 setTimeout(function() { coverPreview(image, result.url) }, 2000);
6202 return result.url;
6203 });
6204 });
6205 }
6206
6207 function elementWritable(elem) {
6208 return elem != null && !elem.disabled && (overwrite || elem.value == '' || !isRED && elem.value == '---');
6209 }
6210} // fillFromText
6211
6212function addMessage(text, cls) {
6213 switch (cls) {
6214 case 'info': var prefix = 'Info'; break;
6215 case 'notice': prefix = 'Notice'; break;
6216 case 'warning': prefix = 'Warning'; break;
6217 case 'critical': prefix = 'FATAL'; break;
6218 default: return null;
6219 }
6220 if ((messages = document.getElementById('UA-messages')) == null) {
6221 let ua = document.getElementById('upload assistant');
6222 if (ua == null) return null;
6223 let tr = document.createElement('TR');
6224 tr.id = 'UA-messages';
6225 ua.firstElementChild.append(tr);
6226 var td = document.createElement('TD');
6227 td.colSpan = 2;
6228 td.className = 'ua-messages-bg';
6229 tr.append(td);
6230 } else {
6231 td = messages.firstElementChild;
6232 if (td == null) return null;
6233 }
6234 var div = document.createElement('DIV');
6235 div.classList.add('ua-messages', 'ua-'.concat(cls));
6236 div[text instanceof HTML ? 'innerHTML' : 'textContent'] = prefix.concat(': ', text);
6237 return td.appendChild(div);
6238}
6239
6240function setHandlers() {
6241 if (prefs.cleanup_descriptions) ['form.create_form', 'form.edit_form', 'form#request_form'].forEach(function(sel) {
6242 if ((ref = document.querySelector(sel)) != null) ref.addEventListener('submit', cleanupDescriptions);
6243 });
6244
6245 if ((ref = document.getElementById('yadg_input')) != null) ref.ondrop = clear0;
6246
6247 if (!isNWCD) {
6248 if ((ref = document.getElementById('image') || document.querySelector('input[name="image"]')) != null) {
6249 ref.ondragover = voidDragHandler0;
6250 ref.ondblclick = imageClear;
6251 ref.ondrop = imageDropHandler;
6252 ref.onpaste = imagePasteHandler;
6253 ref.placeholder = 'Paste/drop local or remote image';
6254 }
6255 }
6256 // Now rape OPS upload form, but only gently
6257 if (isOPS && isUpload && (ref = document.getElementById('remaster')) != null) {
6258 ref.checked = true;
6259 if (!isAddFormat && prefs.ops_always_edition) {
6260 elem = ref.parentNode.parentNode;
6261 elem.style.display = 'none';
6262 if ((ref = document.querySelector('span#year_label_not_remaster')) != null) ref.textContent = 'Initial year:';
6263 if ((ref = document.querySelector('tr#edition_year > td.label')) != null) ref.textContent = 'Edition year:';
6264 if ((ref = document.querySelector('tr#edition_title > td.label')) != null) ref.textContent = 'Edition title:';
6265 if ((ref = document.getElementById('label_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
6266 if ((ref = document.getElementById('catalogue_tr')) != null) /*ref.style.display = 'none'; */ref.remove();
6267 document.querySelectorAll('table#edition_information > tbody > tr')
6268 .forEach(tr => { elem.parentNode.insertBefore(tr, elem) });
6269 } else Remaster();
6270 }
6271
6272 Array.from(document.getElementsByTagName('textarea')).forEach(function(textArea) {
6273 if (textArea.className == 'ua-input') return;
6274 textArea.ondragover = voidDragHandler0;
6275 textArea.ondrop = descDropHandler;
6276 textArea.onpaste = descPasteHandler;
6277 });
6278}
6279
6280function html2php(node, url, tagChain = []) {
6281 if (!node || typeof node != 'object') return null;
6282 switch (node.nodeType) {
6283 case Node.ELEMENT_NODE: {
6284 let tags = [], _tags = [], text = [];
6285 for (let i = 0; i < 5; ++i) text[i] = '';
6286 switch (node.nodeName) {
6287 case 'P':
6288 text[0] = '\n'; text[4] = '\n';
6289 break;
6290 case 'DIV':
6291 text[0] = '\n\n'; text[4] = '\n\n';
6292 break;
6293 case 'DT':
6294 text[4] = '\n';
6295 break;
6296 case 'DD':
6297 text[4] = '\n';
6298 if (isRED) addTag('pad=0|0|0|30'); else text[0] = ' ';
6299 break;
6300 case 'LABEL':
6301 addTag('b');
6302 text[0] = '\n\n';
6303 break;
6304 case 'BR':
6305 return '\n';
6306 case 'HR':
6307 return isRED ? '[hr]' : '\n';
6308 case 'B': case 'STRONG':
6309 addTag('b');
6310 break;
6311 case 'I': case 'EM': case 'DFN': case 'CITE': case 'VAR':
6312 addTag('i');
6313 break;
6314 case 'U': case 'INS':
6315 addTag('u');
6316 break;
6317 case 'DEL':
6318 addTag('s');
6319 break;
6320 case 'CODE': case 'SAMP': case 'KBD':
6321 addTag('code');
6322 text[2] = node.textContent;
6323 break;
6324 case 'PRE':
6325 addTag('pre');
6326 text[2] = node.textContent;
6327 break;
6328 case 'BLOCKQUOTE': case 'QUOTE':
6329 addTag('quote');
6330 break;
6331 case 'Q':
6332 text[1] = '"'; text[3] = '"';
6333 break;
6334 case 'H1':
6335 addTag('size=5'); addTag('b');
6336 text[0] = '\n\n'; text[4] = '\n\n';
6337 break;
6338 case 'H2':
6339 addTag('size=4'); addTag('b');
6340 text[0] = '\n\n'; text[4] = '\n\n';
6341 break;
6342 case 'H3':
6343 addTag('size=3'); addTag('b');
6344 text[0] = '\n\n'; text[4] = '\n\n';
6345 break;
6346 case 'H4': case 'H5': case 'H6':
6347 addTag('b');
6348 text[0] = '\n\n'; text[4] = '\n\n';
6349 break;
6350 case 'SMALL':
6351 addTag('size=1');
6352 break;
6353 case 'OL': case 'UL':
6354 _tags.push(node.nodeName.toLowerCase());
6355 break;
6356 case 'DL':
6357 _tags.push(node.nodeName.toLowerCase());
6358 break;
6359 case 'LI':
6360 switch (tagChain.reverse().find(tag => /^[ou]l$/.test(tag))) {
6361 case 'ol': text[0] = '[#] '; text[4] = '\n'; break;
6362 case 'ul': text[0] = '[*] '; text[4] = '\n'; break;
6363 default: return '';
6364 }
6365 break;
6366 case 'A': {
6367 if (/^https?:$/i.test(node.protocol)) addTag('url=' + removeRedirect(node.href));
6368 break;
6369 }
6370 case 'IMG':
6371 addTag('img');
6372 text[2] = node.dataset.src || node.src;
6373 break;
6374 case 'DETAILS': {
6375 let summary = node.querySelector('summary');
6376 summary = summary != null ? '='.concat(summary.textContent.trim()) : '';
6377 addTag('hide' + summary);
6378 break;
6379 }
6380 case 'AUDIO': case 'BASE': case 'BUTTON': case 'CANVAS': case 'COL': case 'COLGROUP': case 'DATALIST':
6381 case 'DIALOG': case 'EMBED': case 'FIELDSET': case 'FORM': case 'HEAD': case 'INPUT': case 'LEGEND':
6382 case 'LINK': case 'MAP': case 'META': case 'METER': case 'NOSCRIPT': case 'OBJECT': case 'OPTGROUP':
6383 case 'OPTION': case 'PARAM': case 'PROGRESS': case 'SELECT': case 'SOURCE': case 'STYLE': case 'SUMMARY':
6384 case 'SVG': case 'TEMPLATE': case 'TEXTAREA': case 'TITLE': case 'TRACK': case 'VIDEO':
6385 return '';
6386 }
6387 if (['left', 'center', 'right'].some(al => node.style.textAlign.toLowerCase() == al)) {
6388 addTag('align=' + node.style.textAlign.toLowerCase());
6389 }
6390 if (node.style.fontWeight >= 700) addTag('b');
6391 switch (node.style.fontStyle.toLowerCase()) {
6392 case 'italic': addTag('i'); break;
6393 }
6394 switch (node.style.textDecorationLine.toLowerCase()) {
6395 case 'underline': addTag('u'); break;
6396 case 'line-through': addTag('s'); break;
6397 }
6398 if (node.style.color) {
6399 ctxt.fillStyle = elem.style.color;
6400 if (ctxt.fillStyle != '#000000' && /^#(?:[a-f0-8]{2}){3,4}$/i.test(ctxt.fillStyle)) {
6401 addTag('color=' + ctxt.fillStyle);
6402 }
6403 }
6404 if (!text[2]) node.childNodes.forEach(function(node) {
6405 text[2] += html2php(node, url, tagChain.concat(tags.concat(_tags).map(tag => tag.replace(/=.*$/, ''))));
6406 });
6407 if (node.nodeName == 'A' && text[2].trim().length <= 0) {
6408 if (/^(?:https?):$/i.test(node.protocol)) {
6409 text[2] = removeRedirect(node.href);
6410 tags.splice(-1, 1, 'url');
6411 } else text[2] = node.href.slice(node.protocol.length);
6412 }
6413 return text[0].concat((text[1] || text[2] || text[3] ? tags.map(tag => '[' + tag + ']').join('').concat(text[1],
6414 text[2], text[3], tags.reverse().map(tag => '[/' + tag.replace(/=.*$/, '') + ']').join('')) : ''), text[4]);
6415
6416 function addTag(tag) {
6417 if (tagChain.concat(tags.map(tag => tag.replace(/=.*$/, ''))).includesCaseless(tag.replace(/=.*$/, ''))) return;
6418 tags.push(tag);
6419 }
6420 }
6421 case Node.TEXT_NODE:
6422 return node.wholeText.replace(/\s+/g, ' ');
6423 case Node.DOCUMENT_NODE:
6424 return html2php(node.body, url);
6425 }
6426 return '';
6427}
6428
6429function coverPreview(input, imgUrl, size) {
6430 if (!prefs.auto_preview_cover) return;
6431 if ((child = document.getElementById('cover-preview')) == null) {
6432 if (!(input instanceof HTMLElement) || input.parentNode.previousElementSibling == null) return;
6433 elem = document.createElement('div');
6434 elem.style = 'padding-top: 10px; float: right; width: 90%;';
6435 child = document.createElement('img');
6436 child.id = 'cover-preview';
6437 elem.append(child);
6438 var div = document.createElement('div');
6439 div.id = 'cover-size';
6440 if (isRequestNew || isRequestEdit) div.style.fontSize = '7.5pt';
6441 elem.append(div);
6442 input.parentNode.previousElementSibling.append(document.createElement('br'));
6443 input.parentNode.previousElementSibling.append(elem);
6444 }
6445 if ((div = div || document.getElementById('cover-size')) == null) return;
6446 if (urlParser.test(imgUrl)) {
6447 child.onload = function(evt) {
6448 this.onload = null;
6449 if (!this.naturalWidth || !this.naturalHeight) return; // invalid image
6450 (size > 0 ? Promise.resolve(size) : getRemoteFileSize(imgUrl)).then(function(size) {
6451 var warn = prefs.huge_image_warning && size > prefs.huge_image_warning * 2**20;
6452 var html = warn ? '<strong style="color: #ff4c4c;">' + formattedSize(size) + '</strong>' : formattedSize(size);
6453 div.innerHTML = this.naturalWidth + '×' + this.naturalHeight + ' (' + html + ')';
6454 if (!warn) return;
6455 addMessage('high cover size (' + formattedSize(size) + ')', 'notice');
6456 }.bind(this)).catch(reason => { div.textContent = this.naturalWidth + '×' + this.naturalHeight });
6457 };
6458 child.onerror = function(evt) {
6459 this.onerror = null;
6460 div.textContent = this.src = '';
6461 console.warn('Image source cannot be updated:', evt, imgUrl);
6462 };
6463 child.src = imgUrl;
6464 } else div.textContent = child.src = '';
6465}
6466
6467function getRemoteFileSize(url) {
6468 return new Promise(function(resolve, reject) {
6469 var imageSize, abort = GM_xmlhttpRequest({
6470 method: 'GET', url: url, responseType: 'arraybuffer',
6471 onreadystatechange: function(response) {
6472 if (imageSize || response.readyState < XMLHttpRequest.HEADERS_RECEIVED
6473 || !/^Content-Length:\s*(\d+)\b/im.test(response.responseHeaders)) return;
6474 if (!(imageSize = parseInt(RegExp.$1))) return;
6475 resolve(imageSize);
6476 abort.abort();
6477 },
6478 onload: function(response) { // fail-safe
6479 if (imageSize) return;
6480 if (response.status >= 200 && response.status < 400) resolve(response.responseText.length /*response.response.byteLength*/);
6481 else reject(new Error('Image not accessible'));
6482 },
6483 onerror: response => reject('Image not accessible'),
6484 ontimeout: response => reject('Image not accessible'),
6485 });
6486 });
6487}
6488
6489function cleanupDescriptions(evt) {
6490 descriptionFields.forEach(function(ID) {
6491 if ((ref = evt.target.querySelector('textarea#' + ID)) == null || ref.value.length <= 0) return;
6492 var clean = ref.value
6493 .replace(/[ \t]*Vinyl rip by \[color=\S+\]\[\/color\]\s*/im, '')
6494 .replace(/\[u\]Lineage:\[\/u\]\n\n/i, '')
6495 for (var i = 0; i < 3; ++i) clean = clean.replace(/\s*\[(\w+)(?:=([^\[\]]*))?\]\[\/\1\]/gm, '');
6496 const drMatch = [
6497 /(^| \| )DR(\d+)$\s+/m,
6498 /(?:^| \| )DR(\d+)(?=$| \| )/gm,
6499 ];
6500 var m = /\[hide=DR(\d+)?\]\[pre\]/i.exec(clean);
6501 //if (m != null && drMatch[0].test(clean) && RegExp.$2 == m[1]) clean = clean.replace(drMatch[0], '$1');
6502 if (m != null && drMatch[1].test(clean) && RegExp.$1 == m[1]) clean = clean.replace(drMatch[1], '');
6503 ref.value = clean.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/[ \t\xA0]+$/gm, '').trim();
6504 });
6505 return true;
6506}
6507
6508function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*'.concat(expr, '[^\\(\\)]*\\)$'), 'i') }
6509function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*'.concat(expr, '[^\\[\\]]*\\]$'), 'i') }
6510
6511function notMonospaced(str) {
6512 return /[\u0080-\u009F]/.test(str)
6513// || /[\u0000-\u001F]/.test(str) // Control character
6514// || /[\u0020-\u007F]/.test(str) // Basic Latin
6515// || /[\u0080-\u00FF]/.test(str) // Latin-1 Supplement
6516// || /[\u0100-\u017F]/.test(str) // Latin Extended-A
6517// || /[\u0180-\u024F]/.test(str) // Latin Extended-B
6518// || /[\u0250-\u02AF]/.test(str) // IPA Extensions
6519 || /[\u02B0-\u02FF]/.test(str) // Spacing Modifier Letters
6520 || /[\u0300-\u036F]/.test(str) // Combining Diacritical Marks
6521 || /[\u0370-\u03FF]/.test(str) // Greek and Coptic
6522 || /[\u0400-\u04FF]/.test(str) // Cyrillic
6523 || /[\u0500-\u052F]/.test(str) // Cyrillic Supplement
6524 || /[\u0530-\u058F]/.test(str) // Armenian
6525 || /[\u0590-\u05FF]/.test(str) // Hebrew
6526 || /[\u0600-\u06FF]/.test(str) // Arabic
6527 || /[\u0700-\u074F]/.test(str) // Syriac
6528 || /[\u0750-\u077F]/.test(str) // Arabic Supplement
6529 || /[\u0780-\u07BF]/.test(str) // Thaana
6530 || /[\u07C0-\u07FF]/.test(str) // NKo
6531 || /[\u0800-\u083F]/.test(str) // Samaritan
6532 || /[\u0840-\u085F]/.test(str) // Mandaic
6533 || /[\u0860-\u086F]/.test(str) // Syriac Supplement
6534 || /[\u08A0-\u08FF]/.test(str) // Arabic Extended-A
6535 || /[\u0900-\u097F]/.test(str) // Devanagari
6536 || /[\u0980-\u09FF]/.test(str) // Bengali
6537 || /[\u0A00-\u0A7F]/.test(str) // Gurmukhi
6538 || /[\u0A80-\u0AFF]/.test(str) // Gujarati
6539 || /[\u0B00-\u0B7F]/.test(str) // Oriya
6540 || /[\u0B80-\u0BFF]/.test(str) // Tamil
6541 || /[\u0C00-\u0C7F]/.test(str) // Telugu
6542 || /[\u0C80-\u0CFF]/.test(str) // Kannada
6543 || /[\u0D00-\u0D7F]/.test(str) // Malayalam
6544 || /[\u0D80-\u0DFF]/.test(str) // Sinhala
6545 || /[\u0E00-\u0E7F]/.test(str) // Thai
6546 || /[\u0E80-\u0EFF]/.test(str) // Lao
6547 || /[\u0F00-\u0FFF]/.test(str) // Tibetan
6548 || /[\u1000-\u109F]/.test(str) // Myanmar
6549 || /[\u10A0-\u10FF]/.test(str) // Georgian
6550 || /[\u1100-\u11FF]/.test(str) // Hangul Jamo
6551 || /[\u1200-\u137F]/.test(str) // Ethiopic
6552 || /[\u1380-\u139F]/.test(str) // Ethiopic Supplement
6553 || /[\u13A0-\u13FF]/.test(str) // Cherokee
6554 || /[\u1400-\u167F]/.test(str) // Unified Canadian Aboriginal Syllabics
6555 || /[\u1680-\u169F]/.test(str) // Ogham
6556 || /[\u16A0-\u16FF]/.test(str) // Runic
6557 || /[\u1700-\u171F]/.test(str) // Tagalog
6558 || /[\u1720-\u173F]/.test(str) // Hanunoo
6559 || /[\u1740-\u175F]/.test(str) // Buhid
6560 || /[\u1760-\u177F]/.test(str) // Tagbanwa
6561 || /[\u1780-\u17FF]/.test(str) // Khmer
6562 || /[\u1800-\u18AF]/.test(str) // Mongolian
6563 || /[\u18B0-\u18FF]/.test(str) // Unified Canadian Aboriginal Syllabics Extended
6564 || /[\u1900-\u194F]/.test(str) // Limbu
6565 || /[\u1950-\u197F]/.test(str) // Tai Le
6566 || /[\u1980-\u19DF]/.test(str) // New Tai Lue
6567 || /[\u19E0-\u19FF]/.test(str) // Khmer Symbols
6568 || /[\u1A00-\u1A1F]/.test(str) // Buginese
6569 || /[\u1A20-\u1AAF]/.test(str) // Tai Tham
6570 || /[\u1AB0-\u1AFF]/.test(str) // Combining Diacritical Marks Extended
6571 || /[\u1B00-\u1B7F]/.test(str) // Balinese
6572 || /[\u1B80-\u1BBF]/.test(str) // Sundanese
6573 || /[\u1BC0-\u1BFF]/.test(str) // Batak
6574 || /[\u1C00-\u1C4F]/.test(str) // Lepcha
6575 || /[\u1C50-\u1C7F]/.test(str) // Ol Chiki
6576 || /[\u1C80-\u1C8F]/.test(str) // Cyrillic Extended C
6577 || /[\u1CC0-\u1CCF]/.test(str) // Sundanese Supplement
6578 || /[\u1CD0-\u1CFF]/.test(str) // Vedic Extensions
6579 || /[\u1D00-\u1D7F]/.test(str) // Phonetic Extensions
6580 || /[\u1D80-\u1DBF]/.test(str) // Phonetic Extensions Supplement
6581 || /[\u1DC0-\u1DFF]/.test(str) // Combining Diacritical Marks Supplement
6582// || /[\u1E00-\u1EFF]/.test(str) // Latin Extended Additional
6583 || /[\u1F00-\u1FFF]/.test(str) // Greek Extended
6584 || /[\u200B-\u200F\u2028\u2029\u203B\u202A-\u202E\u2060-\u206F]/.test(str) //|| /[\u2000-\u206F]/.test(str) // General Punctuation
6585 || /[\u2070-\u209F]/.test(str) // Superscripts and Subscripts
6586// || /[\u20A0-\u20CF]/.test(str) // Currency Symbols
6587 || /[\u20D0-\u20FF]/.test(str) // Combining Diacritical Marks for Symbols
6588// || /[\u2100-\u214F]/.test(str) // Letterlike Symbols
6589 || /[\u2150-\u218F]/.test(str) // Number Forms
6590// || /[\u2190-\u21FF]/.test(str) // Arrows
6591 || /[\u2200-\u22FF]/.test(str) // Mathematical Operators
6592 || /[\u2300-\u23FF]/.test(str) // Miscellaneous Technical
6593 || /[\u2400-\u243F]/.test(str) // Control Pictures
6594// || /[\u2440-\u245F]/.test(str) // Optical Character Recognition
6595 || /[\u2460-\u24FF]/.test(str) // Enclosed Alphanumerics
6596 || /[\u2500-\u257F]/.test(str) // Box Drawing
6597// || /[\u2580-\u259F]/.test(str) // Block Elements
6598 || /[\u25A0-\u25FF]/.test(str) // Geometric Shapes
6599 || /[\u2600-\u26FF]/.test(str) // Miscellaneous Symbols
6600 || /[\u2700-\u27BF]/.test(str) // Dingbats
6601 || /[\u27C0-\u27EF]/.test(str) // Miscellaneous Mathematical Symbols-A
6602 || /[\u27F0-\u27FF]/.test(str) // Supplemental Arrows-A
6603 || /[\u2800-\u28FF]/.test(str) // Braille Patterns
6604 || /[\u2900-\u297F]/.test(str) // Supplemental Arrows-B
6605// || /[\u2980-\u29FF]/.test(str) // Miscellaneous Mathematical Symbols-B
6606// || /[\u2A00-\u2AFF]/.test(str) // Supplemental Mathematical Operators
6607 || /[\u2B00-\u2BFF]/.test(str) // Miscellaneous Symbols and Arrows
6608 || /[\u2C00-\u2C5F]/.test(str) // Glagolitic
6609// || /[\u2C60-\u2C7F]/.test(str) // Latin Extended-C
6610 || /[\u2C80-\u2CFF]/.test(str) // Coptic
6611 || /[\u2D00-\u2D2F]/.test(str) // Georgian Supplement
6612 || /[\u2D30-\u2D7F]/.test(str) // Tifinagh
6613 || /[\u2D80-\u2DDF]/.test(str) // Ethiopic Extended
6614 || /[\u2DE0-\u2DFF]/.test(str) // Cyrillic Extended-A
6615 || /[\u2E00-\u2E7F]/.test(str) // Supplemental Punctuation
6616 || /[\u2E80-\u2EFF]/.test(str) // CJK Radicals Supplement
6617 || /[\u2F00-\u2FDF]/.test(str) // Kangxi Radicals
6618 || /[\u2FF0-\u2FFF]/.test(str) // Ideographic Description Characters
6619 || /[\u3000-\u303F]/.test(str) // CJK Symbols and Punctuation
6620 || /[\u3040-\u309F]/.test(str) // Hiragana
6621 || /[\u30A0-\u30FF]/.test(str) // Katakana
6622 || /[\u3100-\u312F]/.test(str) // Bopomofo
6623 || /[\u3130-\u318F]/.test(str) // Hangul Compatibility Jamo
6624 || /[\u3190-\u319F]/.test(str) // Kanbun
6625 || /[\u31A0-\u31BF]/.test(str) // Bopomofo Extended
6626 || /[\u31C0-\u31EF]/.test(str) // CJK Strokes
6627 || /[\u31F0-\u31FF]/.test(str) // Katakana Phonetic Extensions
6628 || /[\u3200-\u32FF]/.test(str) // Enclosed CJK Letters and Months
6629 || /[\u3300-\u33FF]/.test(str) // CJK Compatibility
6630 || /[\u3400-\u4DBF]/.test(str) // CJK Unified Ideographs Extension A
6631 || /[\u4DC0-\u4DFF]/.test(str) // Yijing Hexagram Symbols
6632 || /[\u4E00-\u9FFF]/.test(str) // CJK Unified Ideographs
6633// || /[\uA000-\uA48F]/.test(str) // Yi Syllables
6634// || /[\uA490-\uA4CF]/.test(str) // Yi Radicals
6635 || /[\uA4D0-\uA4FF]/.test(str) // Lisu
6636 || /[\uA500-\uA63F]/.test(str) // Vai
6637 || /[\uA640-\uA69F]/.test(str) // Cyrillic Extended-B
6638 || /[\uA6A0-\uA6FF]/.test(str) // Bamum
6639 || /[\uA700-\uA71F]/.test(str) // Modifier Tone Letters
6640 || /[\uA720-\uA7FF]/.test(str) // Latin Extended-D
6641 || /[\uA800-\uA82F]/.test(str) // Syloti Nagri
6642 || /[\uA830-\uA83F]/.test(str) // Common Indic Number Forms
6643 || /[\uA840-\uA87F]/.test(str) // Phags-pa
6644 || /[\uA880-\uA8DF]/.test(str) // Saurashtra
6645 || /[\uA8E0-\uA8FF]/.test(str) // Devanagari Extended
6646 || /[\uA900-\uA92F]/.test(str) // Kayah Li
6647 || /[\uA930-\uA95F]/.test(str) // Rejang
6648 || /[\uA960-\uA97F]/.test(str) // Hangul Jamo Extended-A
6649 || /[\uA980-\uA9DF]/.test(str) // Javanese
6650 || /[\uA9E0-\uA9FF]/.test(str) // Myanmar Extended-B
6651 || /[\uAA00-\uAA5F]/.test(str) // Cham
6652 || /[\uAA60-\uAA7F]/.test(str) // Myanmar Extended-A
6653 || /[\uAA80-\uAADF]/.test(str) // Tai Viet
6654 || /[\uAAE0-\uAAFF]/.test(str) // Meetei Mayek Extensions
6655 || /[\uAB00-\uAB2F]/.test(str) // Ethiopic Extended-A
6656// || /[\uAB30-\uAB6F]/.test(str) // Latin Extended-E
6657 || /[\uAB70-\uABBF]/.test(str) // Cherokee Supplement
6658 || /[\uABC0-\uABFF]/.test(str) // Meetei Mayek
6659 || /[\uAC00-\uD7AF]/.test(str) // Hangul Syllables
6660 || /[\uD7B0-\uD7FF]/.test(str) // Hangul Jamo Extended-B
6661 || /[\uD800-\uDB7F]/.test(str) // High Surrogates
6662// || /[\uDB80-\uDBFF]/.test(str) // High Private Use Surrogates
6663 || /[\uDC00-\uDFFF]/.test(str) // Low Surrogates
6664 || /[\uE000-\uF8FF]/.test(str) // Private Use Area
6665 || /[\uF900-\uFAFF]/.test(str) // CJK Compatibility Ideographs
6666 || /[\uFB00-\uFB4F]/.test(str) // Alphabetic Presentation Forms
6667 || /[\uFB50-\uFDFF]/.test(str) // Arabic Presentation Forms-A
6668 || /[\uFE00-\uFE0F]/.test(str) // Variation Selectors
6669 || /[\uFE10-\uFE1F]/.test(str) // Vertical Forms
6670 || /[\uFE20-\uFE2F]/.test(str) // Combining Half Marks
6671 || /[\uFE30-\uFE4F]/.test(str) // CJK Compatibility Forms
6672 || /[\uFE50-\uFE6F]/.test(str) // Small Form Variants
6673 || /[\uFE70-\uFEFF]/.test(str) // Arabic Presentation Forms-B
6674 || /[\uFF00-\uFFEF]/.test(str) // Halfwidth and Fullwidth Forms
6675 || /[\uFFF0-\uFFFF]/.test(str) // Specials
6676// || /[\u10000-\uFFFFF]/.test(str) // Others
6677}
6678
6679function makeTimeString(duration) {
6680 let t = Math.abs(Math.round(duration));
6681 let H = Math.floor(t / 60 ** 2);
6682 let M = Math.floor(t / 60 % 60);
6683 let S = t % 60;
6684 return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
6685 ':' + S.toString().padStart(2, '0');
6686}
6687
6688function timeStringToTime(str) {
6689 if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
6690 var t = 0, a = RegExp.$2.split(':');
6691 while (a.length > 0) t = t * 60 + parseFloat(a.shift());
6692 return RegExp.$1 ? -t : t;
6693}
6694
6695function normalizeDate(str) {
6696 if (typeof str != 'string') return null;
6697 if (/\b(\d{4}-\d+-\d+|\d{1,2}\/\d{1,2}\/\d{2})\b/.test(str)) return RegExp.$1; // US (clash with BE, IT)
6698 if (/\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // UK, IRL, FR
6699 if (/\b(\d{1,2})-(\d{1,2})-(\d{2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // NL
6700 if (/\b(\d{1,2})\.\s?(\d{1,2})\.\s?(\d{2}|\d{4})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$1 + '/' + RegExp.$3; // AT, CH, DE, LU, CE
6701 if (/\b(\d{4})\.\s?(\d{1,2})\.\s?(\d{1,2})\b/.test(str)) return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1; // JP
6702 return extractYear(str);
6703}
6704
6705function extractYear(expr) {
6706 if (typeof expr == 'number') return Math.round(expr);
6707 if (typeof expr != 'string') return null;
6708 if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1);
6709 var d = new Date(expr);
6710 return parseInt(isNaN(d) ? expr : d.getFullYear());
6711}
6712
6713function formattedSize(size) {
6714 return size < 1024**1 ? Math.round(size) + ' B'
6715 : size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB'
6716 : size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB'
6717 : size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB'
6718 : size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB'
6719 : (Math.round(size * 100 / 2**50) / 100) + ' PiB';
6720}
6721
6722function safeText(unsafeText) {
6723 let div = document.createElement('div');
6724 div.innerText = unsafeText || '';
6725 return div.innerHTML;
6726}
6727
6728function imageClear(evt) {
6729 evt.target.value = '';
6730 coverPreview(evt.target, null);
6731}
6732
6733function imageDropHandler(evt) { return !evt.shiftKey ? imageDataHandler(evt, evt.dataTransfer) : true }
6734function imagePasteHandler(evt) { return imageDataHandler(evt, evt.clipboardData) }
6735function imageDataHandler(evt, data) {
6736 if (!data) return true;
6737 if (data.files.length > 0) {
6738 if (!data.files[0].type.toLowerCase().startsWith('image/')) return true;
6739 evt.target.disabled = true;
6740 if (evt.target.hTimer) {
6741 clearTimeout(evt.target.hTimer);
6742 delete evt.target.hTimer;
6743 }
6744 evt.target.style.backgroundColor = '#800000';
6745 let elem = evt.target, file = data.files[0], size = data.files[0].size;
6746 uploadImages([file], evt.target).then(function(urls) {
6747 elem.value = urls[0];
6748 elem.style.backgroundColor = '#008000';
6749 elem.style.color = 'white';
6750 elem.hTimer = setTimeout(function() {
6751 elem.style.backgroundColor = null;
6752 elem.style.color = null;
6753 delete elem.hTimer;
6754 }, 10000);
6755 coverPreview(elem, urls[0], size);
6756 }).catch(function(error) {
6757 elem.style.backgroundColor = null;
6758 imageClear(evt);
6759 Promise.resolve(error).then(msg => { alert(msg) });
6760 }).then(function() { elem.disabled = false });
6761 return false;
6762 } else if (data.items.length > 0) {
6763 let links = data.getData('text/uri-list');
6764 if (links) links = links.split(/\r?\n/); else {
6765 links = data.getData('text/x-moz-url');
6766 if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
6767 else if (links = data.getData('text/plain')) links = links.split(/\r?\n/);
6768 }
6769 if (Array.isArray(links) && links.length > 0) imageUrlResolver(links[0]).then(verifyImageUrl).then(function(imageUrl) {
6770 evt.target.value = imageUrl;
6771 coverPreview(evt.target, imageUrl);
6772 if (!prefs.auto_rehost_cover || imageUrl.toLowerCase().startsWith(ptpimgOrigin)) return;
6773 evt.target.disabled = true;
6774 rehost2PTPIMG([imageUrl])
6775 .then(urls => { if (urls.length > 0 && urls[0] != evt.target.value) evt.target.value = urls[0] })
6776 .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') }) })
6777 .then(function() { evt.target.disabled = false });
6778 }).catch(function(e) {
6779 console.error(e);
6780 alert(e);
6781 });
6782 return false;
6783 }
6784 return true;
6785}
6786
6787function descDropHandler(evt) {
6788 if (evt.dataTransfer == null || evt.shiftKey) return true;
6789 if (evt.dataTransfer.files.length > 0) {
6790 let images = [];
6791 Array.from(evt.dataTransfer.files).forEach(function(file) {
6792 switch (file.type) {
6793 case '':
6794 if (!['log'/*, 'nfo'*/].some(ext => file.name.toLowerCase().endsWith('.' + ext))) break;
6795 case 'text/plain':
6796 //case 'text/nfo': // malformed encoding
6797 case 'text/log':
6798 evt.target.disabled = true;
6799 file.getText(file.name.toLowerCase().endsWith('.nfo') ? 'ibm850' : 'utf-8').then(function(text) {
6800 var isDR = file.name.toLowerCase().endsWith('foo_dr.txt') && /^Official DR value:\s*DR(\d+)\b/im.test(text);
6801 if (isDR) var DR = parseInt(RegExp.$1);
6802 var tag = isDR || file.name.toLowerCase().endsWith('.nfo') ? 'pre' : 'code';
6803 var php = isDR ? '[hide=DR' + RegExp.$1 + '][' + tag + ']' + text + '[/' + tag + '][/hide]'
6804 : '[hide=' + file.name + '][' + tag + ']' + text + '[/' + tag + '][/hide]';
6805 if (evt.target.value.length <= 0) evt.target.value = php; else if (evt.ctrlKey) {
6806 evt.target.value = evt.target.value.slice(0, evt.rangeOffset) +
6807 php + evt.target.value.slice(evt.rangeOffset);
6808 } else if (isDR && /\[hide=DR\d*\]\[pre\]\[\/pre\]/i.test(evt.target.value)) {
6809 evt.target.value = RegExp.leftContext + php.slice(0, -7) + RegExp.rightContext;
6810 } else if (isDR && /\[hide=DR(\d*)\]((?:\[pre\](foobar2000[\s\S]+?)^\[\/pre\]\s*)+)(?:\[pre\]\[\/pre\])?/im.test(evt.target.value)) {
6811 php = '[hide=DR';
6812 if (parseInt(RegExp.$1) == DR) php += RegExp.$1;
6813 evt.target.value = RegExp.leftContext.concat(php, ']', RegExp.$2.trim(), '\n[pre]', text, '[/pre]', RegExp.rightContext);
6814 } else if (!isDR && /\[hide\](?:\[code\]\[\/code\])?\[\/hide\]/i.test(evt.target.value)) {
6815 evt.target.value = RegExp.leftContext + php + RegExp.rightContext;
6816 } else if (!isDR && /(\[hide=[^\]]+\])(?:\[code\]\[\/code\])?(\[\/hide\])/i.test(evt.target.value)) {
6817 evt.target.value = RegExp.leftContext.concat(RegExp.$1, '[code]', text, '[/code]', RegExp.$2, RegExp.rightContext);
6818 } else evt.target.value += '\n\n'.concat(php);
6819 }).catch(function(e) { alert(e) }).then(function() {
6820 if (!evt.target.style.background) evt.target.disabled = false;
6821 });
6822 break;
6823 case 'image/png':
6824 case 'image/jpeg':
6825 case 'image/gif':
6826 case 'image/bmp':
6827 //case 'image/webp':
6828 //case 'image/svg+xml':
6829 images.push(file);
6830 break;
6831 }
6832 });
6833 if (images.length > 0) {
6834 evt.target.disabled = true;
6835 evt.target.style.background = '#FF000040 no-repeat center center url(' + ulImgData + ')';
6836 //evt.target.style.background = '#FF000040 no-repeat center center url(https://svgshare.com/i/H16.svg)';
6837 let elem = evt.target;
6838 uploadImages(images).then(urlHandler.bind({ tag: 'img' }))
6839 .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
6840 .then(function() {
6841 elem.style.background = null;
6842 elem.disabled = false;
6843 });
6844 }
6845 return false;
6846 } else if (evt.dataTransfer.items.length > 0) {
6847 let content = evt.dataTransfer.getData('text/uri-list');
6848 if (content) content = content.split(/\r?\n/); else {
6849 content = evt.dataTransfer.getData('text/x-moz-url');
6850 if (content) content = content.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
6851 };
6852 if (Array.isArray(content) && content.length > 0) {
6853 Promise.all(content.map(imageUrlResolver)).then(function(resolvedUrls) {
6854 if (prefs.auto_rehost_cover) {
6855 evt.target.disabled = true;
6856 rehost2PTPIMG(resolvedUrls.flatten()).catch(function(e) {
6857 addMessage('PTPimg ' + e + ' (not rehosted)', 'warning');
6858 return resolvedUrls.flatten();
6859 }).then(urlHandler.bind({ tag: 'img' })).then(() => { evt.target.disabled = false });
6860 } else urlHandler.bind({ tag: 'img' })(content);
6861 }).catch(function(e) {
6862 let as = domParser.parseFromString(evt.dataTransfer.getData('text/html'), 'text/html').body.querySelectorAll('a');
6863 Promise.all(content.map(urlResolver))
6864 .then(resolved => urlHandler.bind({ tag: 'url', titles: Array.from(as).map(a => a.textContent.trim()) })(resolved));
6865 });
6866 } else if (content = evt.dataTransfer.getData('text/html')) {
6867 textHandler(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
6868 } else if (content = evt.dataTransfer.getData('text/plain')) {
6869 textHandler(content);
6870 }
6871 return false;
6872 }
6873 return true;
6874
6875 function urlHandler(urls) {
6876 const rx = new RegExp('\\[' + this.tag + '\\]\\[\\/' + this.tag + '\\]', 'i');
6877 urls.forEach(function(url, ndx) {
6878 if (url.length <= 0 || !urlParser.test(url)) return;
6879 var phpBB = '[' + this.tag;
6880 phpBB += Array.isArray(this.titles) && this.titles[ndx] ? '=' + url + ']' + this.titles[ndx] : ']' + url;
6881 phpBB += '[/' + this.tag + ']';
6882 if (evt.target.value.trimRight().length <= 0) evt.target.value = phpBB; else if (evt.ctrlKey) {
6883 evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + phpBB + evt.target.value.slice(evt.rangeOffset);
6884 } else if (rx.test(evt.target.value)) {
6885 evt.target.value = RegExp.leftContext + phpBB + RegExp.rightContext;
6886 } else evt.target.value = evt.target.value.trimRight().concat(/*ndx <= 0 ? '\n\n' : */'\n\n', phpBB);
6887 }.bind(this));
6888 }
6889 function textHandler(phpBB) {
6890 if (evt.target.value.length <= 0) evt.target.value = phpBB; else if (evt.ctrlKey) {
6891 evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + phpBB + evt.target.value.slice(evt.rangeOffset);
6892 } else evt.target.value += '\n\n'.concat(phpBB);
6893 }
6894}
6895
6896function descPasteHandler(evt) {
6897 if (evt.clipboardData == null) return true;
6898 if (evt.clipboardData.files.length > 0) {
6899 let images = Array.from(evt.clipboardData.files)
6900 .filter(file => ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/webp', 'image/svg+xml']
6901 .some(mimeType => file.type == mimeType))
6902 if (images.length <= 0) return true;
6903 evt.target.disabled = true;
6904 evt.target.style.background = '#FF000040 no-repeat center center url(' + ulImgData + ')';
6905 uploadImages(images).then(urls => { insert(urls.map(url => '[img]'.concat(url, '[/img]')).join('\n')) })
6906 .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) }).then(function() {
6907 evt.target.style.background = null;
6908 evt.target.disabled = false;
6909 });
6910 return false;
6911 } else if (evt.clipboardData.items.length > 0) {
6912 let content = evt.clipboardData.getData('text/html');
6913 if (!content) return true;
6914 insert(html2php(domParser.parseFromString(content, 'text/html')).collapseGaps());
6915 return false;
6916 }
6917 return true;
6918
6919 function insert(phpBB) {
6920 var selStart = evt.target.selectionStart;
6921 evt.target.value = evt.target.value.slice(0, evt.target.selectionStart)
6922 .concat(phpBB, evt.target.value.slice(evt.target.selectionEnd));
6923 evt.target.setSelectionRange(selStart + phpBB.length, selStart + phpBB.length);
6924 }
6925}
6926
6927function uaInsert(evt) {
6928 if (evt.clipboardData) evt.target.value = '';
6929 if (!(prefs.autfill_delay > 0)) return true;
6930 autofill = true;
6931 setTimeout(fillFromText, prefs.autfill_delay);
6932}
6933
6934// Firefox accepts dropped playlist in malformed form, try to detect and correct it
6935function fixFirefoxDropBug(evt) {
6936 if (evt.target == null || evt.target.value.length <= 0) return true;
6937 var tl = (Math.sqrt(4 * evt.target.value.split('\n').length - 3) + 1) / 2;
6938 if (tl < 2 || tl != Math.floor(tl) || evt.target.value.length % tl != 0) return true;
6939 var l = evt.target.value.length / tl;
6940 var s = evt.target.value.slice(0, l);
6941 for (var i = 1; i < tl; ++i) if (evt.target.value.slice(i * l, (i + 1) * l) != s) return true;
6942 evt.target.value = s;
6943 return true;
6944}
6945
6946function clear0(evt) { if (evt.target.value.length > 0) evt.target.value = '' }
6947function clear1(evt) { if (evt.buttons == 4) clear0(evt) }
6948function voidDragHandler0(evt) { return false }
6949function voidDragHandler1(evt) {
6950 return !evt.dataTransfer.types.includes('Files') || evt.target.nodeName == 'TEXTAREA'
6951 || evt.target.nodeName == 'INPUT' && evt.target.type == 'file'
6952}
6953
6954function removeRedirect(uri) {
6955 return typeof uri != 'string' ? null : [
6956 'www.anonymz.com/?', 'www.anonymz.com?',
6957 'anonymz.com/?', 'anonymz.com?',
6958 'anonym.to/?', 'anonym.to?',
6959 'dereferer.me/?',
6960 'reho.st/',
6961 ].reduce(function(acc, it) {
6962 if (acc.toLowerCase().startsWith('https://' + it)) return acc.slice(it.length + 8);
6963 if (acc.toLowerCase().startsWith('http://' + it)) return acc.slice(it.length + 7);
6964 return acc;
6965 }, uri);
6966}
6967
6968function urlResolver(url) {
6969 if (!urlParser.test(url)) return Promise.reject('Invalid URL:\n\n'.concat(url));
6970 try { if (!(url instanceof URL)) url = new URL(url) } catch(e) { return Promise.reject(e) }
6971 switch (url.hostname) {
6972 case 'rutracker.org':
6973 if (url.pathname != '/forum/out.php') break;
6974 return globalFetch(url, { method: 'HEAD' }).then(response => urlResolver(response.finalUrl));
6975 case 'www.anonymz.com': case 'anonymz.com': case 'anonym.to': case 'dereferer.me':
6976 var resolved = decodeURIComponent(url.search.slice(1));
6977 return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver();
6978// case 'reho.st':
6979// resolved = url.pathname.concat(url.search, url.hash).slice(1);
6980// if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
6981// return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver();
6982 // URL shorteners
6983 case 'tinyurl.com': case 'bit.ly': case 'j.mp': case 't.co': case 'goo.gl': case 'apple.co': case 'flic.kr':
6984 case 'rebrand.ly': case 'b.link': case 't2m.io': case 'zpr.io': case 'yourls.org':
6985 return genericResolver();
6986 }
6987 return Promise.resolve(url.href);
6988
6989 function genericResolver() {
6990 return globalFetch(url).then(function(response) {
6991 var redirect = response.document.querySelector('meta[http-equiv="refresh"]');
6992 if (redirect != null && (redirect = redirect.content.replace(/^.*?\b(?:URL)\s*=\s*/i, '')) != url.href
6993 || /^ *(?:Location) *: *(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$1) != url.href
6994 || /^ *(?:Refresh) *: *(\d+); *url=(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$2) != url.href
6995 || (redirect = response.finalUrl) != url.href) return urlResolver(redirect);
6996 return Promise.resolve(url.href);
6997 });
6998 }
6999}
7000
7001function tidalRlsParser(url) {
7002 return /^https?:\/\/(?:\w+\.)*tidal\.com\//.test(url)
7003 && (/\/album\/(\d+)(?:\/|$)/i.test(url) || /\/album(?:\/|\?).*\b(?:albumId)=(\d+)\b/i.test(url));
7004}
7005
7006function verifyImageUrl(url) {
7007 return urlResolver(url).then(function(url) {
7008 //if (!strict && imageExtensions.some(ext => url.toLowerCase().endsWith('.'.concat(ext)))) return Promise.resolve(url); // weak
7009 return new Promise(function(resolve, reject) {
7010 var img = new Image();
7011 img.onload = load => { resolve(url) };
7012 img.onerror = function(error) {
7013 if (img.src.includes('?')) img.src = url.replace(/\?.*?(?=\#|$)/, '');
7014 else reject('Not valid image:\n\n'.concat(url));
7015 };
7016 img.ontimeout = timeout => { reject('Image load timed out:\n\n'.concat(url)) };
7017 img.src = url;
7018 });
7019 });
7020}
7021function verifyImageUrls(urls) {
7022 return Array.isArray(urls) ? Promise.all(urls.map(verifyImageUrl)) : Promise.reject('URLs not an array');
7023}
7024
7025function imageUrlResolver(url) {
7026 return urlResolver(url).then(url => verifyImageUrl(url).catch(function(reason) {
7027 try { url = new URL(url) } catch(e) { return Promise.reject(e) }
7028 const notFound = Promise.reject('No title image for this URL');
7029 if (url.hostname.endsWith('pinterest.com')) {
7030 if (url.pathname.startsWith('/pin/')) return globalFetch(url).then(function(response) {
7031 var initialState = response.document.querySelector('script#initial-state');
7032 if (initialState != null) try {
7033 initialState = JSON.parse(script.text);
7034 let images = Object.keys(initialState.pins).map(pin => initialState.pins[pin].images.orig.url);
7035 if (images.length == 1) return images[0]; else if (images.length > 1) return images;
7036 } catch(e) { console.warn(e, initialState) }
7037 return notFound;
7038 });
7039 } else if (url.hostname.endsWith('free-picload.com')) {
7040 if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('free-picload.com', url);
7041 } else switch (url.hostname) {
7042 // general image hostings
7043 case 'imgur.com':
7044 if (url.pathname.startsWith('/a/')) return globalFetch(url, { responseType: 'text' }).then(function(response) {
7045 if (/^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.test(response.responseText)) try {
7046 return JSON.parse(RegExp.$1).album_images.images.map(image => 'https://i.imgur.com/'.concat(image.hash, image.ext));
7047 } catch(e) { debug.warn(e) }
7048 return notFound;
7049 });
7050 return globalFetch(url).then(response => response.document.querySelector('link[rel="image_src"]').href);
7051 case 'pixhost.to':
7052 if (url.pathname.startsWith('/gallery/')) return globalFetch(url).then(response =>
7053 Promise.all(Array.from(response.document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href))));
7054 if (url.pathname.startsWith('/show/')) return globalFetch(url)
7055 .then(response => response.document.querySelector('img#image').src);
7056 break;
7057 case 'malzo.com':
7058 if (url.pathname.startsWith('/al/')) return cheveretoGalleryResolver('malzo.com', url);
7059 break;
7060 case 'imgbb.com': case 'ibb.co':
7061 if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('imgbb.com', url);
7062 break;
7063 case 'jerking.empornium.ph':
7064 if (url.pathname.startsWith('/album/')) return cheveretoGalleryResolver('jerking.empornium.ph', url);
7065 break;
7066 case 'imgbox.com':
7067 if (url.pathname.startsWith('/g/')) return globalFetch(url).then(response =>
7068 Promise.all(Array.from(response.document.querySelectorAll('div#gallery-view-content > a'))
7069 .map(a => imageUrlResolver('https://imgbox.com'.concat(a.pathname)))));
7070 break;
7071 case 'postimg.cc': case 'postimage.org':
7072 if (url.pathname.startsWith('/gallery/')) return globalFetch(url, { responseType: 'text' }).then(function(response) {
7073 if (/\bvar\s+embed_value=(\{[\S\s]+?\});/.test(response.responseText)) try {
7074 let embed_value = JSON.parse(RegExp.$1);
7075 return Object.keys(embed_value).map(key => 'https://i.postimg.cc/'
7076 .concat(embed_value[key][2], '/', embed_value[key][0], '.', embed_value[key][1]))
7077 } catch(e) { console.warn(e) }
7078 return notFound;
7079 });
7080 break;
7081 case 'www.imagevenue.com':
7082 return globalFetch(url, { headers: { Referer: 'http://www.imagevenue.com/' } }).then(function(response) {
7083 var images = Array.from(response.document.querySelectorAll('div.card img')).map(function(img) {
7084 return img.src.includes('://cdn-images') ? img.src : imageUrlResolver(img.parentNode.href);
7085 });
7086 return images.length > 1 ? Promise.all(images) : images.length == 1 ? images[0] : notFound;
7087 });
7088 case 'www.imageshack.us': case 'imageshack.us':
7089 return globalFetch(url).then(response => response.document.querySelector('a#share-dl').href);
7090 case 'www.flickr.com': case 'flickr.com':
7091 if (!url.pathname.startsWith('/photos/')) break;
7092 return globalFetch(url).then(function(response) {
7093 if (!/\b(?:modelExport)\s*:\s*(\{.+\}),/.test(response.responseText)) return notFound;
7094 var urls = JSON.parse(RegExp.$1).main['photo-models'].map(function(photoModel) {
7095 var sizes = Object.keys(photoModel.sizes).sort((a, b) => photoModel.sizes[b].width * photoModel.sizes[b].height
7096 - photoModel.sizes[a].width * photoModel.sizes[a].height);
7097 return sizes.length > 0 ? 'https:'.concat(photoModel.sizes[sizes[0]].url) : null;
7098 });
7099 return urls.length == 1 ? urls[0] : urls.length > 1 ? urls : notFound;
7100 });
7101 case 'redacted.ch':
7102 if (url.pathname != '/image.php') break;
7103 return globalFetch(url, { method: 'HEAD' }).then(response => response.finalUrl);
7104 case 'demo.cloudimg.io':
7105 if (!/\b(https?:\/\/\S+)$/.test(url.pathname.concat(url.search, url.hash))) break;
7106 var resolved = RegExp.$1;
7107 if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
7108 return imageResolver(resolved);
7109 case 'fastpic.ru':
7110 if (url.pathname.startsWith('/view/'))
7111 return globalFetch(url).then(response => imageUrlResolver(response.document.querySelector('a.img-a').href));
7112 if (url.pathname.startsWith('/fullview/')) return globalFetch(url).then(function(response) {
7113 var node = response.document.getElementById('image');
7114 if (node != null) return node.src;
7115 return /\bvar\s+loading_img\s*=\s*'(\S+?)';/.test(response.responseText) ? RegExp.$1 : notFound;
7116 });
7117 break;
7118 case 'radikal.ru': case 'a.radikal.ru':
7119 return globalFetch(url).then(response => response.document.querySelector('div.mainBlock img').src);
7120 case 'imageban.ru': case 'ibn.im':
7121 return globalFetch(url).then(response => response.document.getElementById('img_main').src /* dataset.original */);
7122 // music-related
7123 case 'www.musicbrainz.org': case 'musicbrainz.org':
7124 if (!['release', 'release-group'].some(branch => url.pathname.includes('/'.concat(branch, '/')))) break;
7125 return globalFetch(url).then(function(response) {
7126 var node = response.document.querySelector('a.artwork-image');
7127 if (node != null) return node.href;
7128 return (node = response.document.querySelector('div.cover-art > img')) != null ? node.src : notFound;
7129 });
7130 case 'music.apple.com':
7131 if (!itunesRlsParser.test(url)) break;
7132 return globalFetch(url).then(function(response) {
7133 var meta = response.document.querySelector('meta[property="og:image"][content]');
7134 if (meta == null || !meta.content) return notFound;
7135 return verifyImageUrl(meta.content.replace(/\/\d+x\d+\w*(?=\.\w+$)/, '/100000x100000-999')).catch(reason => meta.content);
7136 });
7137 case 'www.deezer.com': case 'deezer.com':
7138 return globalFetch(url).then(function(response) {
7139 var meta = response.document.querySelector('meta[property="og:image"][content]');
7140 if (meta == null || !meta.content) return notFound;
7141 return verifyImageUrl(meta.content.replace(/\/\d+x\d+\w*(?=\.\w+$)/, '/1400x1400-000000-100-0-0'))
7142 .catch(reason => meta.content);
7143 });
7144 case 'www.qobuz.com': case 'qobuz.com':
7145 if (!url.pathname.includes('/album/')) break;
7146 return globalFetch(url).then(function(response) {
7147 var img = response.document.querySelector('div.album-cover > img');
7148 if (img == null) return notFound;
7149 return verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_max')).catch(reason => img.src);
7150 });
7151 case 'www.prestomusic.com': case 'prestomusic.com':
7152 if (!url.pathname.includes('/products/')) break;
7153 return globalFetch(url)
7154 .then(response => verifyImageUrl(response.document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/)));
7155 case 'www.bontonland.cz':case 'bontonland.cz':
7156 return globalFetch(url).then(response => response.document.querySelector('a.detailzoom').href);
7157 case 'www.nativedsd.com':case 'nativedsd.com':
7158 if (!url.pathname.includes('/albums/')) break;
7159 return globalFetch(url).then(response => response.document.querySelector('a#album-cover').href);
7160 case 'www.prostudiomasters.com': case 'prostudiomasters.com':
7161 if (!url.pathname.includes('/album/')) break;
7162 return globalFetch(url).then(function(response) {
7163 var a = response.document.querySelector('img.album-art');
7164 return verifyImageUrl(a.currentSrc).catch(reason => a.src);
7165 });
7166 case 'www.e-onkyo.com': case 'e-onkyo.com':
7167 if (!url.pathname.includes('/album/')) break;
7168 return globalFetch(url).then(function(response) {
7169 var a = response.document.querySelector('figure > a.colorbox');
7170 return new URL(response.finalUrl).origin.concat(a.pathname);
7171 })
7172 case 'store.acousticsounds.com':
7173 return globalFetch(url).then(function(response) {
7174 var link = response.document.querySelector('div#detail > link[rel="image_src"]');
7175 return verifyImageUrl(link.href.replace(/\/medium\//i, '/large/')).catch(reason => link.href);
7176 });
7177 case 'www.indies.eu': case 'indies.eu':
7178 if (!url.pathname.includes('/alba/')) break;
7179 return globalFetch(url).then(response => response.document.querySelector('div.obrazekDetail > img').src);
7180 case 'www.beatport.com': case 'beatport.com':
7181 if (!url.pathname.includes('/release/')) break;
7182 return globalFetch(url).then(response => response.document.querySelector('div > img.interior-release-chart-artwork').src);
7183 case 'www.supraphonline.cz': case 'supraphonline.cz':
7184 if (!url.pathname.includes('/album/')) break;
7185 return globalFetch(url).then(function(response) {
7186 verifyImageUrl(response.document.querySelector('meta[itemprop="image"]').content.replace(/\?.*$/, '')).catch(reason => notFound);
7187 });
7188 case 'vgmdb.net':
7189 if (!url.pathname.includes('/album/')) break;
7190 return globalFetch(url).then(function(response) {
7191 var div = response.document.querySelector('div#coverart');
7192 return verifyImageUrl(/\b(?:url)\s*\(\"(.*)"\)/i.test(div.style['background-image']) && RegExp.$1).catch(reason => notFound);
7193 });
7194 case 'www.ototoy.jp': case 'ototoy.jp':
7195 return globalFetch(url).then(function(response) {
7196 var img = response.document.querySelector('div#tralbumArt > a.popupImage');
7197 return verifyImageUrl(img.dataset.src).catch(reason => img.src);
7198 });
7199 case 'music.yandex.ru':
7200 if (!url.pathname.includes('/album/')) break;
7201 return globalFetch(url).then(function(response) {
7202 var script = response.document.querySelector('script.light-data');
7203 return verifyImageUrl(JSON.parse(script.text).image).catch(reason => notFound);
7204 });
7205 case 'mora.jp/':
7206 if (!url.pathname.includes('/package/')) break;
7207 return loadMoraMetadata(url).then(packageMeta => packageMeta.packageUrl.concat(packageMeta.fullsizeimage));
7208 }
7209 return globalFetch(url, { headers: { 'Referer': url.origin } }).then(function(response) {
7210 if (url.pathname.startsWith('/album/')
7211 && response.document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
7212 return cheveretoGalleryResolver(url.hostname, url);
7213 var meta = [
7214 'head > meta[property="og:image"][content]', 'head > meta[itemprop="image"][content]', 'head > meta[name="og:image"][content]',
7215 ].reduce((acc, selector) => acc || response.document.querySelector(selector), null);
7216 return meta != null && meta.content ? meta.content : notFound;
7217 });
7218 }));
7219}
7220
7221function uploadImages(files, elem) {
7222 if (typeof files != 'object') return Promise.reject('Invalid argument');
7223 if (!Array.isArray(files)) files = Array.from(files);
7224 //if (files.length > 1) files.push(files.shift()); // Windows bug
7225 var frs = files.filter(function(file) {
7226 return file instanceof File && imageExtensions.some(ext => file.type == 'image/' + ext);
7227 })/*.sort((file1, file2) => file1.name.localeCompare(file2.name))*/.map(file => new Promise(function(resolve, reject) {
7228 var reader = new FileReader();
7229 reader.onload = function() { resolve({ name: file.name, type: file.type, data: reader.result }) };
7230 reader.onerror = reader.ontimeout = function() { reject('FileReader error (' + file.name + ')') };
7231 reader.readAsBinaryString(file);
7232 }));
7233 return frs.length > 0 ? Promise.all(frs).then(images => upload2PTPIMG(images, elem).catch(function(reason) {
7234 addMessage('Upload to PTPIMG failed (' + reason + '), falling back to malzo.com', 'warning');
7235 return upload2Chevereto('malzo.com', images, elem);
7236 }).catch(function(reason) {
7237 addMessage('Upload to malzo.com failed (' + reason + '), falling back to imgbb.com', 'warning');
7238 return upload2Chevereto('imgbb.com', images, elem);
7239 }).catch(function(reason) {
7240 addMessage('Upload to imgbb.com failed (' + reason + '), falling back to pixhost.to', 'warning');
7241 return upload2PixHost(images, elem);
7242 }).catch(function(reason) {
7243 addMessage('Upload to pixhost.to failed (' + reason + '), falling back to catbox.moe', 'warning');
7244 return upload2Catbox(images, elem);
7245 }).catch(function(reason) {
7246 addMessage('Upload to catbox.moe failed (' + reason + '), falling back to imgbox.com', 'warning');
7247 return upload2ImgBox(images, elem);
7248 }).catch(function(reason) {
7249 addMessage('Upload to imgbox.com failed (' + reason + '), falling back to jerking.empornium.ph', 'warning');
7250 return upload2Chevereto('jerking.empornium.ph', images, elem);
7251 }).catch(reason => Promise.reject('Upload failed to all hosts'))) : Promise.reject('Nothing to upload');
7252}
7253
7254function upload2PTPIMG(images, elem) {
7255 if (!Array.isArray(images)) return Promise.reject('invalid argument');
7256 if (images.length <= 0) return Promise.reject('nothing to upload');
7257 return getPTPIMGapiKey().then(apiKey => new Promise(function(resolve, reject) {
7258 var now = Date.now();
7259 const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
7260 var formData = '--' + boundary + '\r\n';
7261 images.filter(function(image) {
7262 return image.data && image.name && ['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(ext => image.type == 'image/'.concat(ext));
7263 }).forEach(function(image, ndx) {
7264 formData += 'Content-Disposition: form-data; name="file-upload[' + ndx + ']"; filename="' + image.name.toASCII() + '"\r\n';
7265 formData += 'Content-Type: ' + image.type + '\r\n\r\n';
7266 formData += image.data + '\r\n';
7267 formData += '--' + boundary + '\r\n';
7268 });
7269 formData += 'Content-Disposition: form-data; name="api_key"\r\n\r\n';
7270 formData += apiKey + '\r\n';
7271 formData += '--' + boundary + '--\r\n';
7272 GM_xmlhttpRequest({
7273 method: 'POST',
7274 url: ptpimgOrigin + '/upload.php',
7275 responseType: 'json',
7276 headers: {
7277 'Accept': 'application/json',
7278 'Content-Type': 'multipart/form-data; boundary=' + boundary,
7279 'Content-Length': formData.length,
7280 },
7281 data: formData,
7282 binary: true,
7283 timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
7284 onload: function(response) {
7285 if (response.status >= 200 && response.status < 400) {
7286 if (response.response) resolve(response.response.map(item => ptpimgOrigin + '/' + item.code + '.' + item.ext));
7287 else reject('void response');
7288 } else reject(defaultErrorHandler(response));
7289 },
7290 onprogress: elem instanceof HTMLElement && 'value' in elem ? function(progress) {
7291 var pct = progress.position * 100 / progress.total;
7292 //elem.value = 'Uploading... (' + Math.round(pct) + '%)';
7293 } : undefined,
7294 onerror: error => reject(defaultErrorHandler(error)),
7295 ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
7296 });
7297 }));
7298}
7299
7300function rehost2PTPIMG(urls) {
7301 if (!Array.isArray(urls)) return Promise.reject('Invalid parameter');
7302 if (urls.length <= 0) return Promise.resolve([]); //Promise.reject('Nothing to rehost');
7303 return Promise.all(urls.map(function(url) {
7304 if (!urlParser.test(url)) return Promise.reject('URL not valid ('.concat(url, ')'));
7305 var hostname = new URL(url).hostname;
7306 if (hostname == 'img.discogs.com' || hostname.endsWith('omdb.org')) {
7307 return verifyImageUrl('https://reho.st/'.concat(url))
7308 .catch(reason => rehost2Catbox(url))
7309 .catch(reason => rehost2PixHost(url))
7310 .catch(reason => reupload2PTPIMG(url));
7311 } else if (!['png', 'jpg', 'jpeg', 'gif', 'bmp'].some(ext => url.toLowerCase().endsWith('.'.concat(ext)))) {
7312 return verifyImageUrl(url.concat('#.jpg'))
7313 .catch(reason => rehost2Chevereto('malzo.com', url))
7314 .catch(reason => rehost2PixHost(url))
7315 .catch(reason => rehost2Chevereto('imgbb.com', url))
7316 .catch(reason => rehost2Chevereto('jerking.empornium.ph', url))
7317 .catch(reason => rehost2Chevereto('free-picload.com', url));
7318 }
7319 return verifyImageUrl(url);
7320 })).then(imageUrls => getPTPIMGapiKey().then(function(apiKey) {
7321 if (prefs.diag_mode) console.debug('rehost2PTPIMG(...) input:', imageUrls);
7322 var formData = new URLSearchParams({
7323 'link-upload': imageUrls.join('\r\n'),
7324 'api_key': apiKey,
7325 });
7326 return globalFetch(ptpimgOrigin + '/upload.php', {
7327 responseType: 'json',
7328 timeout: imageUrls.length * rehostTimeout,
7329 }, formData).then(function(response) {
7330 if (!response.response) return Promise.reject('PTPIMG void response');
7331 if (response.response.length < imageUrls.length)
7332 return Promise.reject(`not all images rehosted (${response.response.length}/${imageUrls.length})`);
7333 return response.response.map(item => ptpimgOrigin.concat('/', item.code, '.', item.ext));
7334 });
7335 }));
7336}
7337
7338function reupload2PTPIMG(imgUrl) {
7339 console.warn('PTPIMG rehoster fallback to local reupload');
7340 return globalFetch(imgUrl, { responseType: 'blob' }).then(function(response) {
7341 var image = {
7342 name: imgUrl.replace(/^.*\//, ''),
7343 data: response.responseText,
7344 };
7345 switch (imgUrl.replace(/^.*\./, '').toLowerCase()) {
7346 case 'jpg': case 'jpeg': case 'jfif': image.type = 'image/jpeg'; break;
7347 case 'png': image.type = 'image/png'; break;
7348 case 'gif': image.type = 'image/gif'; break;
7349 case 'bmp': image.type = 'image/bmp'; break;
7350 default: return Promise.reject('Unsupported extension');
7351 }
7352 return upload2PTPIMG([image]).then(imgUrls => imgUrls[0]);
7353 });
7354}
7355
7356function getPTPIMGapiKey() {
7357 if (prefs.ptpimg_api_key) return Promise.resolve(prefs.ptpimg_api_key);
7358 try {
7359 var apiKey = JSON.parse(window.localStorage.ptpimg_it).api_key;
7360 if (apiKey) {
7361 GM_setValue('ptpimg_api_key', prefs.ptpimg_api_key = apiKey);
7362 return Promise.resolve(apiKey);
7363 }
7364 } catch(e) { if (prefs.diag_mode) console.debug('getPTPIMGapiKey():', e) }
7365 return globalFetch(ptpimgOrigin).then(function(response) {
7366 if ((apiKey = response.document.getElementById('api_key')) == null) {
7367 let counter = GM_getValue('ptpimg_reminder_read', 0);
7368 if (counter < 5) {
7369 alert(`
7370PTPIMG API key could not be captured. Please login to ${ptpimgOrigin}/ and redo the action.
7371
7372If you don\'t have PTPIMG account, consider to set auto_rehost_cover to false in preferences
7373(Tampermonkey extension menu -> right click to Upload Assistant -> Storage tab)
7374
7375Direct images uploading is still available to fallback image hosts (proxied).
7376`);
7377 GM_setValue('ptpimg_reminder_read', ++counter);
7378 }
7379 return Promise.reject('PTPIMG API key not configured');
7380 }
7381 if (!apiKey.value) return Promise.reject('Assertion failed: missing PTPIMG API key');
7382 GM_setValue('ptpimg_api_key', prefs.ptpimg_api_key = apiKey.value);
7383 Promise.resolve(prefs.ptpimg_api_key).then(apiKey => { alert(`Your PTPIMG API key [${apiKey}] was successfully configured`) });
7384 return prefs.ptpimg_api_key;
7385 });
7386}
7387
7388function upload2Chevereto(hostname, images, elem) {
7389 if (!Array.isArray(images)) return Promise.reject('invalid argument');
7390 if (images.length <= 0) return Promise.reject('nothing to upload');
7391 const anonSessionLimits = {
7392 'malzo.com': 2,
7393 'imgbb.com': 2,
7394 'jerking.empornium.ph': 5,
7395 'free-picload.com': 50,
7396 };
7397 return setCheveretoSession(hostname).then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
7398 switch (hostname) {
7399 case 'malzo.com':
7400 case 'jerking.empornium.ph':
7401 case 'free-picload.com':
7402 if (!['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].some(ext => image.type == 'image/'.concat(ext)))
7403 throw 'MIME type not supported: '.concat(image.type);
7404 break;
7405 case 'imgbb.com':
7406 if (!['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tif', 'tiff', 'heic'].some(ext => image.type == 'image/'.concat(ext)))
7407 throw 'MIME type not supported: '.concat(image.type);
7408 break;
7409 }
7410 var anonSessionLimit = anonSessionLimits[hostname.toLowerCase()];
7411 if (!session.username && anonSessionLimit > 0 && image.size > anonSessionLimit * 2**20)
7412 throw 'image size exceeds anonymous upload limit';
7413 const boundary = '----WebKitFormBoundary'.concat(Date.now().toString(16).toUpperCase());
7414 var formData = '--' + boundary + '\r\n', params = Object.assign({
7415 action: 'upload',
7416 type: 'file',
7417 nsfw: 0,
7418 }, session);
7419 Object.keys(params).forEach(function(field, index, arr) {
7420 formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
7421 formData += params[field] + '\r\n';
7422 formData += '--' + boundary + '\r\n';
7423 });
7424 formData += 'Content-Disposition: form-data; name="source"; filename="' + image.name.toASCII() + '"\r\n';
7425 formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n';
7426 formData += '--' + boundary + '--\r\n';
7427 GM_xmlhttpRequest({
7428 method: 'POST',
7429 url: 'https://'.concat(hostname, '/json'),
7430 responseType: 'json',
7431 headers: {
7432 'Accept': 'application/json',
7433 'Content-Type': 'multipart/form-data; boundary=' + boundary,
7434 'Content-Length': formData.length,
7435 'Referer': 'https://'.concat(hostname, '/'),
7436 },
7437 data: formData,
7438 binary: true,
7439 timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
7440 onload: function(response) {
7441 if (response.status >= 200 && response.status < 400) {
7442 if (response.response.success) resolve(response.response.image.url);
7443 else reject(response.response.error.message.concat(' (', response.response.status_code, ')'));
7444 } else reject(defaultErrorHandler(response));
7445 },
7446 onerror: error => reject(defaultErrorHandler(error)),
7447 ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
7448 });
7449
7450 function formField(key, value) {
7451 return 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + value + '\r\n--' + boundary;
7452 }
7453 }))));
7454}
7455
7456function rehost2Chevereto(hostname, url) {
7457 return verifyImageUrl(url).then(imageUrl => setCheveretoSession(hostname).then(function(session) {
7458 var formData = new URLSearchParams(Object.assign({
7459 action: 'upload',
7460 type: 'url',
7461 nsfw: 0,
7462 source: imageUrl,
7463 }, session));
7464 return globalFetch('https://'.concat(hostname, '/json'), {
7465 responseType: 'json',
7466 headers: { 'Referer': 'https://'.concat(hostname, '/') },
7467 timeout: rehostTimeout,
7468 }, formData).then(function(response) {
7469 return response.response.success ? response.response.image.url
7470 : Promise.reject(hostname.concat(': ', response.response.error.message,' (', response.response.status_code, ')'));
7471 });
7472 }));
7473}
7474
7475function cheveretoGalleryResolver(hostname, url) {
7476 var albumId = /^\/(?:album|al)\/(\w+)\b/.test(url.pathname) && RegExp.$1;
7477 if (!albumId) return Promise.reject('Invlaid gallery URL');
7478 return setCheveretoSession(hostname).then(function(session) {
7479 var formData = new URLSearchParams(Object.assign({
7480 action: 'get-album-contents',
7481 albumid: albumId,
7482 }, session));
7483 return globalFetch(url.origin.concat('/json'), {
7484 responseType: 'json',
7485 headers: { 'Referer': url },
7486 }, formData).then(function(response) {
7487 return response.response.status_txt == 'OK' && Array.isArray(response.response.contents) ?
7488 response.response.contents.map(image => image.url)
7489 : Promise.reject(hostname.concat(': ', response.response.error.message,' (', response.response.status_code, ')'));
7490 });
7491 }).catch(function(reason) {
7492 console.warn(hostname, 'gallery couldn\'t be resolved via API:', reason, '(falling back to HTML parser)');
7493 return new Promise(function(resolve, reject) {
7494 var urls = [], domParser = new DOMParser;
7495 getPage(url);
7496
7497 function getPage(url) {
7498 GM_xmlhttpRequest({ method: 'GET', url: url, headers: { Referer: url },
7499 onload: function(response) {
7500 if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
7501 var dom = domParser.parseFromString(response.responseText, 'text/html');
7502 Array.prototype.push.apply(urls,
7503 Array.from(dom.querySelectorAll('div.list-item-image > a.image-container')).map(a => a.href));
7504 var next = dom.querySelector('a[data-pagination="next"][href]');
7505 if (next == null || !next.href) resolve(urls); else getPage(next.href);
7506 },
7507 onerror: response => { reject(defaultErrorHandler(response)) },
7508 ontimeout: response => { reject(defaultTimeoutHandler(response)) },
7509 });
7510 }
7511 }).then(urls => Promise.all(urls.map(imageUrlResolver)));
7512 });
7513}
7514
7515function setCheveretoSession(hostname) {
7516 const index = 'https://'.concat(hostname, '/');
7517 return globalFetch(index).then(function(response) {
7518 if (!/\b(?:auth_token)\s*=\s*"(\w+)"/.test(response.responseText)) return Promise.reject('Auth token detection failure');
7519 var session = {
7520 auth_token: RegExp.$1,
7521 timestamp: Date.now(),
7522 };
7523 if (getUser(response)) return session;
7524 if (hostname.toLowerCase() == 'free-picload.com') var hostPrefix = 'picload_';
7525 else if (/^([\w\-]+)(?:\.[\w\-]+)+$/.test(hostname)) hostPrefix = RegExp.$1.toLowerCase().concat('_');
7526 if (!hostPrefix || !prefs[hostPrefix.concat('uid')] || !prefs[hostPrefix.concat('password')]) return session;
7527 var formData = new URLSearchParams({
7528 'login-subject': prefs[hostPrefix.concat('uid')],
7529 'password': prefs[hostPrefix.concat('password')],
7530 'auth_token': session.auth_token,
7531 });
7532 return new Promise(function(resolve, reject) {
7533 GM_xmlhttpRequest({ method: 'POST', url: 'https://'.concat(hostname, '/login'),
7534 headers: {
7535 'Accept': '*/*',
7536 'Content-Type': 'application/x-www-form-urlencoded',
7537 'Content-Length': formData.toString().length,
7538 'Referer': 'https://'.concat(hostname, '/login'),
7539 }, data: formData.toString(),
7540 onload: function(response) {
7541 if (response.status < 200 || response.status > 400) defaultErrorHandler(response);
7542 resolve(response.status);
7543 },
7544 onerror: function(response) {
7545 reject(defaultErrorHandler(response));
7546 //resolve(response.status);
7547 },
7548 ontimeout: response => { reject(defaultTimeoutHandler(response)) },
7549 });
7550 }).then(status => globalFetch(index, { responseType: 'text' }).then(function(response) {
7551 if (getUser(response)) console.debug(hostname, 'authorized session:', session);
7552 else console.debug(hostname, 'authorization failed:', status, '(continuing anonymous)');
7553 return session;
7554 }));
7555
7556 function getUser(response) {
7557 if (/\b(?:logged_user)\s*=\s*(\{.*?\});/.test(response.responseText)) try {
7558 let logged_user = JSON.parse(RegExp.$1);
7559 session.username = logged_user.username;
7560 session.userid = logged_user.id;
7561 return Boolean(logged_user.username || logged_user.id);
7562 } catch(e) { console.warn(e) }
7563 return false;
7564 }
7565 });
7566}
7567
7568function upload2PixHost(images, elem) {
7569 if (!Array.isArray(images)) return Promise.reject('invalid argument');
7570 if (images.length <= 0) return Promise.reject('nothing to upload');
7571 return Promise.all(images.map(image => new Promise(function(resolve, reject) {
7572 if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
7573 throw 'MIME type not supported: '.concat(image.type);
7574 var now = Date.now();
7575 const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
7576 var formData = '--' + boundary + '\r\n';
7577 formData += 'Content-Disposition: form-data; name="img"; filename="' + image.name.toASCII() + '"\r\n';
7578 formData += 'Content-Type: ' + image.type + '\r\n\r\n';
7579 formData += image.data + '\r\n';
7580 formData += '--' + boundary + '\r\n';
7581 formData += 'Content-Disposition: form-data; name="content_type"\r\n\r\n';
7582 formData += '0\r\n';
7583 formData += '--' + boundary + '--\r\n';
7584 GM_xmlhttpRequest({
7585 method: 'POST',
7586 url: 'https://api.pixhost.to/images',
7587 responseType: 'json',
7588 headers: {
7589 'Accept': 'application/json',
7590 'Content-Type': 'multipart/form-data; boundary=' + boundary,
7591 'Content-Length': formData.length,
7592 },
7593 data: formData,
7594 binary: true,
7595 timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
7596 onload: function(response) {
7597 if (response.status >= 200 && response.status < 400) resolve(response.response.show_url);
7598 else reject(defaultErrorHandler(response));
7599 },
7600 onerror: error => reject(defaultErrorHandler(error)),
7601 ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
7602 });
7603 }).then(imageUrlResolver)));
7604}
7605
7606function rehost2PixHost(url) {
7607 return verifyImageUrl(url).then(function(imageUrl) {
7608 var formData = new URLSearchParams({
7609 imgs: imageUrl,
7610 content_type: 0,
7611 tos: 'on',
7612 });
7613 return globalFetch('https://pixhost.to/remote/', {
7614 responseType: 'text',
7615 timeout: rehostTimeout,
7616 }, formData).then(function(response) {
7617 if (!/\b(?:upload_results)\s*=\s*(\{.*\});$/m.test(response.responseText)) return Promise.reject('page parsing error');
7618 var images = JSON.parse(RegExp.$1).images;
7619 if (images.length < imageUrls.length) return Promise.reject('image not rehosted');
7620 return imageUrlResolver(images[0].show_url);
7621 });
7622 });
7623}
7624
7625function upload2Catbox(images, elem) {
7626 if (!Array.isArray(images)) return Promise.reject('Invalid argument');
7627 if (images.length <= 0) return Promise.reject('Nothing to upload or format not supported');
7628 return getCatboxUserHash().catch(reason => undefined).then(userHash => Promise.all(images.map(image => new Promise(function(resolve, reject) {
7629 var now = Date.now();
7630 const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
7631 var formData = '--' + boundary + '\r\n';
7632 formData += 'Content-Disposition: form-data; name="reqtype"\r\n\r\n';
7633 formData += 'fileupload\r\n';
7634 formData += '--' + boundary + '\r\n';
7635 if (userHash) {
7636 formData += 'Content-Disposition: form-data; name="userhash"\r\n\r\n';
7637 formData += userHash + '\r\n';
7638 formData += '--' + boundary + '\r\n';
7639 }
7640 formData += 'Content-Disposition: form-data; name="fileToUpload"; filename="' + image.name.toASCII() + '"\r\n';
7641 formData += 'Content-Type: ' + image.type + '\r\n\r\n';
7642 formData += image.data + '\r\n';
7643 formData += '--' + boundary + '--\r\n';
7644 GM_xmlhttpRequest({
7645 method: 'POST',
7646 url: 'https://catbox.moe/user/api.php',
7647 responseType: 'text',
7648 headers: {
7649 'Content-Type': 'multipart/form-data; boundary=' + boundary,
7650 'Content-Length': formData.length,
7651 },
7652 data: formData,
7653 binary: true,
7654 timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
7655 onload: function(response) {
7656 if (response.status >= 200 && response.status < 400) resolve(response.responseText);
7657 else reject(defaultErrorHandler(response));
7658 },
7659 onerror: error => reject(defaultErrorHandler(error)),
7660 ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
7661 });
7662 }))));
7663}
7664
7665function rehost2Catbox(url) {
7666 return verifyImageUrl(url).then(imageUrl => getCatboxUserHash().catch(reason => undefined).then(function(userHash) {
7667 var formData = new URLSearchParams({
7668 reqtype: 'urlupload',
7669 url: imageUrl,
7670 });
7671 if (userHash) formData.set('userhash', userHash);
7672 return globalFetch('https://catbox.moe/user/api.php', {
7673 responseType: 'text',
7674 timeout: rehostTimeout,
7675 }, formData).then(response => response.responseText);
7676 }));
7677}
7678
7679function getCatboxUserHash() {
7680 return prefs.catbox_userhash ? Promise.resolve(prefs.catbox_userhash) : globalFetch('https://catbox.moe/').then(function(response) {
7681 var userHash = response.document.querySelector('input[name="userhash"][value]');
7682 return userHash != null && userHash.value || Promise.reject('Catbox.moe: not logged in or userhash not found');
7683 });
7684}
7685
7686function upload2ImgBox(images, elem) {
7687 if (!Array.isArray(images)) return Promise.reject('invalid argument');
7688 if (images.length <= 0) return Promise.reject('nothing to upload');
7689 return setImgBoxSession().then(session => Promise.all(images.map(image => new Promise(function(resolve, reject) {
7690 if (!['png', 'jpg', 'jpeg', 'gif'].some(ext => image.type == 'image/'.concat(ext)))
7691 throw 'MIME type not supported: '.concat(image.type);
7692 var now = Date.now();
7693 const boundary = '----WebKitFormBoundary'.concat(now.toString(16).toUpperCase());
7694 var formData = '--' + boundary + '\r\n';
7695 Object.keys(session.params).forEach(function(field, index, arr) {
7696 formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
7697 formData += session.params[field] + '\r\n';
7698 formData += '--' + boundary + '\r\n';
7699 });
7700 formData += 'Content-Disposition: form-data; name="files[]"; filename="' + image.name.toASCII() + '"\r\n';
7701 formData += 'Content-Type: ' + image.type + '\r\n\r\n';
7702 formData += image.data + '\r\n';
7703 formData += '--' + boundary + '--\r\n';
7704 GM_xmlhttpRequest({
7705 method: 'POST',
7706 url: 'https://imgbox.com/upload/process',
7707 headers: {
7708 'Accept': 'application/json',
7709 'Content-Type': 'multipart/form-data; boundary=' + boundary,
7710 'Content-Length': formData.length,
7711 'X-CSRF-Token': session.csrf_token,
7712 },
7713 data: formData,
7714 responseType: 'json',
7715 binary: true,
7716 timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000),
7717 onload: function(response) {
7718 if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
7719 resolve(response.response.files[0].original_url);
7720 },
7721 onerror: error => reject(defaultErrorHandler(error)),
7722 ontimeout: timeout => reject(defaultTimeoutHandler(timeout)),
7723 });
7724 }))));
7725}
7726
7727function setImgBoxSession() {
7728 return globalFetch('https://imgbox.com/').then(function(response) {
7729 var csrfToken = response.document.querySelector('meta[name="csrf-token"]');
7730 if (csrfToken == null) return Promise.reject('ImgBox.com session token not found');
7731 if (prefs.diag_mode) console.debug('ImgBox.com session token:', csrfToken.content);
7732 if (response.document.querySelector('div.btn-group > ul.dropdown-menu') != null) return csrfToken.content;
7733 if (!prefs.imgbox_uid || !prefs.imgbox_password) return csrfToken.content;
7734 var formData = new URLSearchParams({
7735 "utf8": "✓",
7736 "authenticity_token": csrfToken.content,
7737 "user[login]": prefs.imgbox_uid,
7738 "user[password]": prefs.imgbox_password,
7739 });
7740 GM_xmlhttpRequest({ method: 'POST', url: 'https://imgbox.com/login', headers: {
7741 'Referer': 'https://imgbox.com/login',
7742 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
7743 'Content-Length': formData.toString().length,
7744 }, data: formData.toString() });
7745 return new Promise(function(resolve, reject) {
7746 setTimeout(() => { globalFetch('http://imgbox.com/').then(function(response) {
7747 if (response.document.querySelector('div.btn-group > ul.dropdown-menu') == null)
7748 console.warn('ImgBox.com login failed, continuing as anonymous', response);
7749 if ((csrfToken = response.document.querySelector('meta[name="csrf-token"]')) != null) {
7750 if (prefs.diag_mode) console.debug('ImgBox.com session token after login:', csrfToken.content);
7751 resolve(csrfToken.content);
7752 } else reject('ImgBox.com session token not found');
7753 }) }, 1000);
7754 });
7755 }).then(csrfToken => globalFetch('https://imgbox.com/ajax/token/generate', {
7756 method: 'POST',
7757 responseType: 'json',
7758 headers: { 'X-CSRF-Token': csrfToken },
7759 }).then(response => ({
7760 csrf_token: csrfToken,
7761 params: {
7762 token_id: response.response.token_id,
7763 token_secret: response.response.token_secret,
7764 content_type: 1,
7765 thumbnail_size: '100c',
7766 gallery_id: null,
7767 gallery_secret: null,
7768 comments_enabled: 0,
7769 },
7770 })));
7771}
7772
7773function dcFmtToGazelle(format) {
7774 if (/^(?:CD|CDi|CDr|HDCD)\b/.test(format)) return 'CD';
7775 if (/\b(?:File|AAC|AIFC|AIFF|ALAC|AMR|APE|DFF|DSD|FLAC|MP2|MP3|ogg-vorbis|Opus|SHN|WAV|WavPack|WMA|WMV)\b/.test(format)) return 'WEB';
7776 if (/^(?:Vinyl|LP|\d+(?:\.\d+)?\s*")$/.test(format)) return 'Vinyl';
7777 if (/\b(?:SACD|Hybrid)\b/.test(format)) return 'SACD';
7778 if (/^(?:Blu[ \-]?ray)\b/i.test(format)) return isOPS ? 'BD' : 'Blu-Ray';
7779 if (/^(?:DVD|HD\s+DVD)/.test(format)) return 'DVD';
7780 if (/^(?:Cassette|Microcassette)$/i.test(format)) return 'Cassette';
7781 if (/^(?:DAT)$/.test(format)) return 'DAT';
7782 if (/^(?:Soundboard)$/i.test(format)) return 'Soundboard';
7783 //if (/^(?:Memory\s+Stick)$/i.test(format)) return ??
7784 return null;
7785}
7786
7787function queryAjaxAPI(action, params) {
7788 if (!action) return Promise.reject('Action missing');
7789 var retryCount = 0;
7790 return new Promise(function(resolve, reject) {
7791 params = new URLSearchParams(params || undefined);
7792 params.set('action', action);
7793 var url = '/ajax.php?'.concat(params);
7794 var xhr = new XMLHttpRequest();
7795 queryInternal();
7796
7797 function queryInternal() {
7798 var now = Date.now();
7799 try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
7800 if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
7801 apiTimeFrame.timeStamp = now;
7802 apiTimeFrame.requestCounter = 1;
7803 } else ++apiTimeFrame.requestCounter;
7804 window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
7805 if (apiTimeFrame.requestCounter <= 5) {
7806 xhr.open('GET', url, true);
7807 xhr.setRequestHeader('Accept', 'application/json');
7808 if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
7809 xhr.responseType = 'json';
7810 xhr.onload = function() {
7811 if (xhr.status == 404) return reject('not found');
7812 if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
7813 if (xhr.response.status == 'success') return resolve(xhr.response.response);
7814 if (xhr.response.error == 'not found') return reject(xhr.response.error);
7815 console.warn('queryAjaxAPI.queryInternal(...) response:', xhr, xhr.response);
7816 if (xhr.response.error == 'rate limit exceeded') {
7817 console.warn('queryAjaxAPI.queryInternal(...) ' + xhr.response.error + ':', apiTimeFrame, now, retryCount);
7818 if (retryCount++ <= 10) return setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
7819 }
7820 reject('API '.concat(xhr.response.status, ': ', xhr.response.error));
7821 };
7822 xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
7823 xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
7824 xhr.timeout = 10000;
7825 xhr.send();
7826 } else {
7827 setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
7828 if (prefs.diag_mode) console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
7829 action + ' (' + apiTimeFrame.requestCounter + ')');
7830 if (prefs.messages_verbosity >= 1) {
7831 addMessage('AJAX API request exceeding time frame: action=' + action + ' (' + apiTimeFrame.requestCounter + ')', 'notice');
7832 } else addMessage('please wait for next AJAX timeframe', 'notice');
7833 }
7834 }
7835 });
7836}
7837
7838function validataTorrentFile(torrent) {
7839 tfMessages.forEach(node => { node.remove() });
7840 tfMessages = [];
7841 var fr = new FileReader();
7842 fr.onload = function(evt) {
7843 torrent = bdecode(new Uint8Array(fr.result));
7844 torrent.info.files.forEach(function(file) {
7845 var folderName = decodeURIComponent(escape(torrent.info.name));
7846 var fileName = decodeURIComponent(escape(file.path[0]));
7847 var totalLen = folderName.trueLength() + 1 + fileName.trueLength();
7848 if (totalLen > 180) tfMessages.push(addMessage(new HTML('file "' + safeText(fileName).bold() +
7849 '" exceeding allowed length (' + totalLen + ' > 180)'), 'warning'));
7850 if (/\.(?:torrent|\!ut|\!qb|url|lnk)$/i.test(fileName)) {
7851 tfMessages.push(addMessage(new HTML('forbidden file "' + safeText(fileName).bold() + '"'), 'warning'));
7852 }
7853 });
7854 ref = document.querySelector('td.ua-messages-bg');
7855 if (ref != null && ref.childElementCount <= 0) ref.parentNode.remove();
7856 };
7857 fr.onerror = fr.ontimeout = error => { console.error('FileReader error (' + torrent.name + ')') };
7858 fr.readAsArrayBuffer(torrent);
7859
7860 function bdecode(str) {
7861 var pos = 0, infoBegin = 0, infoEnd = 0;
7862 return bdecodeInternal(str);
7863
7864 function bdecodeInternal(str) {
7865 if (pos > str.length) return null;
7866 switch (str[pos]) {
7867 case 100: // char code for 'd'
7868 ++pos;
7869 var retval = [];
7870 while (str[pos] != 101){ // char code for 'e'
7871 var key = bdecodeInternal(str);
7872 var val = bdecodeInternal(str);
7873 if (key === null || val === null) break;
7874 retval[key] = val;
7875 }
7876 if(infoEnd == -1) infoEnd = pos + 1;
7877 retval.isDct = true;
7878 ++pos;
7879 return retval;
7880 case 108: // char code for 'l'
7881 ++pos;
7882 retval = [];
7883 while (str[pos] != 101){ // char code for 'e'
7884 let val = bdecodeInternal(str);
7885 if (val === null) break;
7886 retval.push(val);
7887 }
7888 ++pos;
7889 return retval;
7890 case 105: // char code for 'i'
7891 ++pos;
7892 var digits = Array.prototype.indexOf.call(str, 101, pos) - pos; // 101 = char code for 'e'
7893 val = '';
7894 for (var i = pos; i < digits + pos; ++i) val += String.fromCharCode(str[i]);
7895 val = Math.round(parseFloat(val));
7896 pos += digits + 1;
7897 return val;
7898 default:
7899 digits = Array.prototype.indexOf.call(str, 58, pos) - pos; // 58 = char code for ':'
7900 if (digits < 0 || digits > 20) return null;
7901 var len = '';
7902 for (i = pos; i < digits + pos; ++i) len += String.fromCharCode(str[i]);
7903 len = parseInt(len);
7904 pos += digits + 1;
7905 var fstring = '';
7906 for (i = pos; i < len + pos; ++i) fstring += String.fromCharCode(str[i]);
7907 pos += len;
7908 if(fstring == 'info') {
7909 infoBegin = pos;
7910 infoEnd = -1;
7911 }
7912 return fstring;
7913 }
7914 }
7915 }
7916}
7917
7918function defaultErrorHandler(response) {
7919 var e = 'XHR: error status=' + response.status;
7920 if (response.statusText) e += ' (' + response.statusText + ')';
7921 if (response.error) e += ' (' + response.error + ')';
7922 console.error('XHR error:', response);
7923 if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
7924 return e;
7925}
7926function defaultTimeoutHandler(response) {
7927 const e = 'XHR: timeout';
7928 console.error('XHR timeout:', response);
7929 if (prefs.messages_verbosity >= 2) addMessage(e, 'notice');
7930 return e;
7931}