Gazelle extract featured artists from description

Tries to recognize and add featured artists from selected text in group description

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

  1. // ==UserScript==
  2. // @name Gazelle extract featured artists from description
  3. // @namespace https://greasyfork.org/cs/users/321857-anakunda
  4. // @version 1.36
  5. // @description Tries to recognize and add featured artists from selected text in group description
  6. // @author Anakunda
  7. // @copyright 2020, Anakunda (https://greasyfork.org/cs/users/321857-anakunda)
  8. // @license GPL-3.0-or-later
  9. // @match https://redacted.ch/torrents.php?*id=*
  10. // @match https://orpheus.network/torrents.php?*id=*
  11. // @match https://notwhat.cd/torrents.php?*id=*
  12. // @grant RegExp
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_deleteValue
  17. // @grant GM_log
  18. // @require https://greasyfork.org/scripts/388280-xpathlib/code/XPathLib.js
  19. // ==/UserScript==
  20.  
  21. const multiArtistParsers = [
  22. /\s*[,;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*[Aa]nd\s+)?\s*/,
  23. /\s+[\/\|\×]\s+/,
  24. ];
  25. const pseudoArtistParsers = [
  26. /^(?:#??N[\/\-]?A|[JS]r\.?)$/i,
  27. /^(?:traditional|lidová)$/i,
  28. /\b(?:traditional|lidová)$/,
  29. /^(?:tradiční|lidová)\s+/,
  30. /^(?:[Aa]nonym)/,
  31. /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  32. /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  33. /^(?:Various\s+Composers)$/i,
  34. /^(?:Guests|Friends)$/i,
  35. ];
  36. const ampersandParsers = [
  37. /\s+(?:vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
  38. /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
  39. /\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
  40. /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
  41. ];
  42. const featParsers = [
  43. /\s+(?:[Ww]ith)\s+(?!his\b|her\b|Friends$|Strings$)(.*?)\s*$/,
  44. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:eaturing|t\.))\s+(.*?)\s*$/,
  45. /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff](?:ea)?t\.)\s+(.*?)\s*$/,
  46. /\s+\[\s*f(?:eat(?:\.|uring)|t\.)\s+([^\[\]]+?)\s*\]/i,
  47. /\s+\(\s*f(?:eat(?:\.|uring)|t\.)\s+([^\(\)]+?)\s*\)/i,
  48. /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i,
  49. /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i,
  50. /\s+\[\s*(?:with|w\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/,
  51. /\s+\(\s*(?:with|w\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/,
  52. ];
  53. const remixParsers = [
  54. /\s+\((?:The\s+)Remix(?:e[sd])?\)/i,
  55. /\s+\[(?:The\s+)Remix(?:e[sd])?\]/i,
  56. /\s+(?:The\s+)Remix(?:e[sd])?\s*$/i,
  57. /^(Remixes)\b/,
  58. /\s+\(([^\(\)]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\)/i,
  59. /\s+\[([^\[\]]+?)(?:[\'\’\`]s)?\s+(?:(?:Extended|Enhanced)\s+)?Remix\]/i,
  60. /\s+\([^\(\)]*\b(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\(\)]+)\)/i,
  61. /\s+\[[^\[\]]*\b(?:(Extended|Enhanced)\s+)?Remix(?:ed)?\s+by\s+([^\[\]]+)\]/i,
  62. ];
  63. const otherArtistsParsers = [
  64. [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
  65. [/^()(.*?)\s+\(conductor\)$/i, 4],
  66. //[/^()(.*?)\s+\(.*\)$/i, 1],
  67. ];
  68. const artistStrips = [
  69. /\s+(?:aka|AKA)\.?\s+(.*)$/,
  70. /\s+\(([^\(\)]+)\)$/,
  71. /\s+\[([^\[\]]+)\]$/,
  72. /\s+\{([^\{\}]+)\}$/,
  73. ];
  74.  
  75. const siteApiTimeframeStorageKey = document.location.hostname + ' API time frame';
  76. const gazelleApiFrame = 10500;
  77. var artist_index, siteArtistsCache = {}, notSiteArtistsCache = [], xhr = new XMLHttpRequest;
  78. var modal = null, btnAdd = null, btnCustom = null, customCtrls = [], sel = null, ajaxRejects = 0;
  79. var prefs = {
  80. set: function(prop, def) { this[prop] = GM_getValue(prop, def) }
  81. };
  82.  
  83. Array.prototype.includesCaseless = function(str) {
  84. if (typeof str != 'string') return false;
  85. str = str.toLowerCase();
  86. return this.find(elem => typeof elem == 'string' && elem.toLowerCase() == str) != undefined;
  87. };
  88. Array.prototype.pushUnique = function(...items) {
  89. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) });
  90. return this.length;
  91. };
  92. Array.prototype.pushUniqueCaseless = function(...items) {
  93. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  94. return this.length;
  95. };
  96.  
  97. (function() {
  98. 'use strict';
  99.  
  100. const styleSheet = `
  101. .modal {
  102. position: fixed;
  103. left: 0;
  104. top: 0;
  105. width: 100%;
  106. height: 100%;
  107. background-color: rgba(0, 0, 0, 0.5);
  108. opacity: 0;
  109. visibility: hidden;
  110. transform: scale(1.1);
  111. transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
  112. }
  113. .modal-content {
  114. position: absolute;
  115. top: 50%;
  116. left: 50%;
  117. font-size: 17px;
  118. transform: translate(-50%, -50%);
  119. background-color: FloralWhite;
  120. color: black;
  121. width: 31rem;
  122. border-radius: 0.5rem;
  123. padding: 2rem 2rem 2rem 2rem;
  124. font-family: monospace;
  125. }
  126. .show-modal {
  127. opacity: 1;
  128. visibility: visible;
  129. transform: scale(1.0);
  130. transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
  131. }
  132. input[type="text"] { cursor: text; }
  133. input[type="radio"] { cursor: pointer; }
  134. .lbl { cursor: pointer; }
  135.  
  136. .tooltip {
  137. position: relative;
  138. }
  139.  
  140. .tooltip .tooltiptext {
  141. visibility: hidden;
  142. width: 120px;
  143. background-color: #555;
  144. color: #fff;
  145. text-align: center;
  146. border-radius: 6px;
  147. padding: 5px 0;
  148. position: absolute;
  149. z-index: 1;
  150. bottom: 125%;
  151. left: 50%;
  152. margin-left: -60px;
  153. opacity: 0;
  154. transition: opacity 0.3s;
  155. }
  156.  
  157. .tooltip .tooltiptext::after {
  158. position: absolute;
  159. top: 100%;
  160. left: 50%;
  161. margin-left: -5px;
  162. border-width: 5px;
  163. border-style: solid;
  164. border-color: #555 transparent transparent transparent;
  165. }
  166.  
  167. .tooltip:hover .tooltiptext {
  168. visibility: visible;
  169. opacity: 1;
  170. }
  171. `;
  172.  
  173. var addBox = document.querySelector('form.add_form[name="artists"]');
  174. if (addBox == null) return;
  175. btnAdd = document.createElement('input');
  176. btnAdd.id = 'add-artists-from-selection';
  177. btnAdd.value = 'Extract from selection';
  178. btnAdd.onclick = add_from_selection;
  179. btnAdd.type = 'button';
  180. btnAdd.style.marginLeft = '5px';
  181. btnAdd.style.visibility = 'hidden';
  182. addBox.appendChild(btnAdd);
  183.  
  184. var style = document.createElement('style');
  185. document.head.appendChild(style);
  186. style.id = 'artist-parser-form';
  187. style.type = 'text/css';
  188. style.innerHTML = styleSheet;
  189. var el, elem = [];
  190. elem.push(document.createElement('div'));
  191. elem[elem.length - 1].className = 'modal';
  192. elem[elem.length - 1].id = 'add-from-selection-form';
  193. modal = elem[0];
  194. elem.push(document.createElement('div'));
  195. elem[elem.length - 1].className = 'modal-content';
  196. elem.push(document.createElement('input'));
  197. elem[elem.length - 1].id = 'btnFill';
  198. elem[elem.length - 1].type = 'submit';
  199. elem[elem.length - 1].value = 'Capture';
  200. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 30px;";
  201. elem[elem.length - 1].onclick = do_parse;
  202. elem.push(document.createElement('input'));
  203. elem[elem.length - 1].id = 'btnCancel';
  204. elem[elem.length - 1].type = 'button';
  205. elem[elem.length - 1].value = 'Cancel';
  206. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 65px;";
  207. elem[elem.length - 1].onclick = closeModal;
  208. var presetIndex = 0;
  209. function addPreset(val, label = 'Custom', rx = null, order = [1, 2]) {
  210. elem.push(document.createElement('div'));
  211. el = document.createElement('input');
  212. elem[elem.length - 1].style.paddingBottom = '10px';
  213. el.id = 'parse-preset-' + val;
  214. el.name = 'parse-preset';
  215. el.value = val;
  216. if (val == 1) el.checked = true;
  217. el.type = 'radio';
  218. el.onchange = update_custom_ctrls;
  219. if (rx) {
  220. el.rx = rx;
  221. el.order = order;
  222. }
  223. if (val == 999) btnCustom = el;
  224. elem[elem.length - 1].appendChild(el);
  225. el = document.createElement('label');
  226. el.style.marginLeft = '10px';
  227. el.style.marginRight = '10px';
  228. el.htmlFor = 'parse-preset-' + val;
  229. el.className = 'lbl';
  230. el.innerHTML = label;
  231. elem[elem.length - 1].appendChild(el);
  232. if (val != 999) return;
  233. el = document.createElement('input');
  234. el.type = 'text';
  235. el.id = 'custom-pattern';
  236. el.style.width = '20rem';
  237. el.style.fontFamily = 'monospace';
  238. el.autoComplete = "on";
  239. addTooltip(el, 'RegExp to parse lines, first two captured groups are used');
  240. customCtrls.push(elem[elem.length - 1].appendChild(el));
  241. el = document.createElement('input');
  242. el.type = 'radio';
  243. el.name = 'parse-order';
  244. el.id = 'parse-order-1';
  245. el.value = 1;
  246. el.checked = true;
  247. el.style.marginLeft = '1rem';
  248. addTooltip(el, 'Captured regex groups assigned in order $1: artist(s), $2: assignment');
  249. customCtrls.push(elem[elem.length - 1].appendChild(el));
  250. el = document.createElement('label');
  251. el.htmlFor = 'parse-order-1';
  252. el.textContent = '→';
  253. el.style.marginLeft = '5px';
  254. elem[elem.length - 1].appendChild(el);
  255. el = document.createElement('input');
  256. el.type = 'radio';
  257. el.name = 'parse-order';
  258. el.id = 'parse-order-2';
  259. el.value = 2;
  260. el.style.marginLeft = '10px';
  261. addTooltip(el, 'Captured regex groups assigned in order $1: assignment, $2: artist(s)');
  262. customCtrls.push(elem[elem.length - 1].appendChild(el));
  263. el = document.createElement('label');
  264. el.htmlFor = 'parse-order-2';
  265. el.textContent = '←';
  266. el.style.marginLeft = '5px';
  267. elem[elem.length - 1].appendChild(el);
  268. }
  269. addPreset(++presetIndex, escapeHTML('<artist(s)> - <assignment>]'), /^\s*(.+?)(?:\:|\s+[\-\−\—\~\–]+\s+(.*?))?\s*$/);
  270. addPreset(++presetIndex, escapeHTML('<artist>[, <assignment>]') +
  271. '<span style="font-family: initial;">&nbsp;&nbsp;<i>(HRA style)</i></span>', /^\s*(.+?)(?:\:|\s*,\s*(.*?))?\s*$/);
  272. addPreset(++presetIndex, escapeHTML('<artist(s)>[: <assignment>]'), /^\s*(.+?)(?:\:|\s*:+\s*(.*?))?(?:\s*,)?\s*$/);
  273. addPreset(++presetIndex, escapeHTML('<artist(s)>[ (<assignment>)]'), /^\s*(.+?)(?:\:|\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))?(?:\s*,)?\s*$/);
  274. addPreset(++presetIndex, escapeHTML('[<assignment> - ]<artist(s)>'), /^\s*(?:(.*?)\s+[\-\−\—\~\–]+\s+)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  275. addPreset(++presetIndex, escapeHTML('[<assignment>: ]<artist(s)>'), /^\s*(?:(.*?)\s*:+\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  276. addPreset(++presetIndex, escapeHTML('<artist>[ / <assignment>]'), /^\s*(.+?)(?:\:|\s*\/+\s*(.*?))?(?:\s*,)?\s*$/);
  277. addPreset(++presetIndex, escapeHTML('<artist>[; <assignment>]'), /^\s*(.+?)(?:\:|\s*;\s*(.*?))?(?:\s*,)?\s*$/);
  278. addPreset(++presetIndex, escapeHTML('[<assignment> / ]<artist(s)>'), /^\s*(?:(.*?)\s*\/+\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  279. addPreset(++presetIndex, '<span style="font-family: initial;">From tracklist</span>', /^\s*\d+(?:\s*[\-\−\—\~\–\.\:]\s*|\s+)(.+?)\s*$/, []);
  280. addPreset(999);
  281. elem.slice(2).forEach(k => { elem[1].appendChild(k) });
  282. elem[0].appendChild(elem[1]);
  283. document.body.appendChild(elem[0]);
  284. window.addEventListener("click", windowOnClick);
  285. document.addEventListener('selectionchange', () => {
  286. var cs = window.getComputedStyle(modal);
  287. if (!btnAdd || window.getComputedStyle(modal).visibility != 'hidden') return;
  288. var sel = document.getSelection();
  289. ShowHideAddbutton();
  290. });
  291. })();
  292.  
  293. function add_from_selection() {
  294. sel = document.getSelection();
  295. if (sel.isCollapsed || modal == null) return;
  296. prefs.set('preset', 1);
  297. prefs.set('custom_pattern', '^\\s*(.+?)(?:\\s*:+\\s*(.*?)|\\:)?\\s*$');
  298. prefs.set('custom_pattern_order', 1);
  299. setRadiosValue('parse-preset', prefs.preset);
  300. customCtrls[0].value = prefs.custom_pattern;
  301. setRadiosValue('parse-order', prefs.custom_pattern_order);
  302. sel = sel.toString();
  303. update_custom_ctrls();
  304. modal.classList.add("show-modal");
  305. }
  306.  
  307. function do_parse(expr, flags = '') {
  308. closeModal();
  309. if (!sel) return;
  310. var preset = getSelectedRadio('parse-preset');
  311. if (preset == null) return;
  312. prefs.preset = preset.value;
  313. var order = preset.order;
  314. var custom_parse_order = getSelectedRadio('parse-order');
  315. var rx = preset.rx;
  316. if (!rx && preset.value == 999 && custom_parse_order != null) {
  317. rx = [new RegExp(customCtrls[0].value)];
  318. order = custom_parse_order != null ?
  319. custom_parse_order.value == 1 ? [1, 2] : custom_parse_order.value == 2 ? [2, 1] : null : [1, 2];
  320. }
  321. const guest_parser = /^(.*?)(?:\s+(?:feat(?:\.|uring)|with|meets)\s+(.*))?$/;
  322. function extr_artists(kind) { return document.querySelectorAll('ul#artist_list > li.' + kind + ' > a') }
  323. var artists = [
  324. extr_artists('artist_main'),
  325. extr_artists('artist_guest'),
  326. extr_artists('artists_remix'),
  327. extr_artists('artists_composers'),
  328. extr_artists('artists_conductors'),
  329. extr_artists('artists_dj'),
  330. extr_artists('artists_producer'),
  331. ];
  332. cleanupArtistsForm();
  333. var addedartists = [];
  334. for (var i = 0; i < artists.length; ++i) addedartists[i] = [];
  335. artist_index = 0;
  336. sel.split(/(?:\r?\n)+/).forEach(function(line) {
  337. if (!line || !line.trim()) return;
  338. if (line.search(/^\s*(?:Recorded|Mastered)\b/i) >= 0) return;
  339. line = line.replace(/\s+\(tracks?\b[^\(\)]+\)/, '').replace(/\s+\[tracks?\b[^\[\]]+\]/, '')
  340. let matches = /^\s*(?:Produced)[ \-\−\—\~\–]by (.+?)\s*$/.exec(line);
  341. if (matches != null) splitAmpersands(matches[1]).forEach(producer => { add_artist(producer, 7) });
  342. else if (rx instanceof RegExp && (matches = rx.exec(line)) != null) {
  343. if (!Array.isArray(order) || order.length < 2) {
  344. let title = matches[1];
  345. if (/^(.+?) - /.test(title)) {
  346. let artist = RegExp.$1;
  347. if ((matches = featParsers.slice(0, 3).reduce((m, rx) => m || rx.exec(artist), null)) != null) {
  348. splitAmpersands(artist.slice(0, matches.index)).forEach(artist => { add_artist(artist, 1) });
  349. splitAmpersands(matches[1]).forEach(artist => { add_artist(artist, 2) });
  350. } else splitAmpersands(artist).forEach(artist => { add_artist(artist, 1) });
  351. }
  352. if ((matches = featParsers.slice(1).reduce((m, rx) => m || rx.exec(title), null)) != null)
  353. splitAmpersands(matches[1]).forEach(artist => { add_artist(artist, 2) });
  354. } else if (matches[order[0]]) {
  355. let role = deduce_artist(matches[order[1]]);
  356. splitAmpersands(matches[order[0]]).forEach(artist => { add_artist(artist, role) });
  357. } else splitAmpersands(matches[order[1]]).forEach(artist => { add_artist(artist, 2) });
  358. }
  359. });
  360. prefs.custom_pattern = customCtrls[0].value;
  361. prefs.custom_pattern_order = custom_parse_order != null ? custom_parse_order.value : 1;
  362. for (i in prefs) { if (typeof prefs[i] != 'function') GM_setValue(i, prefs[i]) }
  363. return;
  364.  
  365. function deduce_artist(str) {
  366. if (/\b(?:remix)/i.test(str)) return 3; // remixer
  367. if (/\b(?:composer|libretto|lyric\w*|written[ \-\−\—\~\–]by)\b/i.test(str)) return 4; // composer
  368. if (/\b(?:conduct|rirector\b)/i.test(str)) return 5; // conductor
  369. if (/\b(?:compiler\b)/i.test(str)) return 6; // compiler
  370. if (/\b(?:producer\b|produced[ \-\−\—\~\–]by\b)/i.test(str)) return 7; // producer
  371. return 2;
  372. }
  373.  
  374. function add_artist(name, type = 1) {
  375. if (!name || !type) return false;
  376. if (/^(?:(?:Special\s+)?Guests?):?$/i.test(name)) return false;
  377. // avoid dupes
  378. var n = name.toLowerCase();
  379. for (var i of artists[0]) { if (n == i.textContent.toLowerCase()) return false }
  380. if (type >= 2) for (i of artists[type - 1]) { if (n == i.textContent.toLowerCase()) return false }
  381. for (i of addedartists[0]) { if (n == i.toLowerCase()) return false }
  382. if (type >= 2) for (i of addedartists[type - 1]) { if (n == i.toLowerCase()) return false }
  383. var id = get_artist_field(artist_index);
  384. if (id == null) {
  385. AddArtistField();
  386. id = get_artist_field(artist_index);
  387. if (id == null) return false;
  388. }
  389. id.value = name;
  390. id.nextElementSibling.value = type;
  391. addedartists[type - 1].push(name);
  392. ++artist_index;
  393. return true;
  394. }
  395. }
  396.  
  397. function get_artist_field(index) {
  398. var id = document.getElementById('artist');
  399. if (index <= 0) return id;
  400. for (var i = 0; i < index; ++i) {
  401. do { id = id.nextElementSibling } while (id != null && (id.localName != 'input' || id.name != 'aliasname[]'));
  402. if (id == null) break;
  403. }
  404. return id;
  405. }
  406.  
  407. function closeModal() {
  408. if (modal == null) return;
  409. ShowHideAddbutton();
  410. modal.classList.remove("show-modal");
  411. }
  412.  
  413. function windowOnClick(event) {
  414. if (modal != null && event.target === modal) closeModal();
  415. }
  416.  
  417. function update_custom_ctrls() {
  418. function en(elem) {
  419. if (elem == null || btnCustom == null) return;
  420. elem.disabled = !btnCustom.checked;
  421. elem.style.opacity = btnCustom.checked ? 1 : 0.5;
  422. }
  423. customCtrls.forEach(k => { en(k) });
  424. }
  425.  
  426. function getSelectedRadio(name) {
  427. for (var i of document.getElementsByName(name)) { if (i.checked) return i }
  428. return null;
  429. }
  430.  
  431. function setRadiosValue(name, val) {
  432. for (var i of document.getElementsByName(name)) { if (i.value == val) i.checked = true }
  433. }
  434.  
  435. function ShowHideAddbutton() {
  436. //btnAdd.style.visibility = document.getSelection().type == 'Range' ? 'visible' : 'hidden';
  437. btnAdd.style.visibility = document.getSelection().isCollapsed ? 'hidden' : 'visible';
  438. }
  439.  
  440. function escapeHTML(string) {
  441. var pre = document.createElement('pre');
  442. var text = document.createTextNode(string);
  443. pre.appendChild(text);
  444. return pre.innerHTML;
  445. }
  446.  
  447. function cleanupArtistsForm() {
  448. var id = get_artist_field(0);
  449. do {
  450. id.value = null;
  451. id = id.nextElementSibling;
  452. if (id == null) break;
  453. id.value = 1;
  454. do { id = id.nextElementSibling } while (id != null && (id.localName != 'input' || id.name != 'aliasname[]'));
  455. } while (id != null);
  456. }
  457.  
  458. function addTooltip(elem, text) {
  459. if (elem == null) return;
  460. elem.classList.add('tooltip');
  461. var tt = document.createElement('span');
  462. tt.className = 'tooltiptext';
  463. tt.textContent = text;
  464. elem.appendChild(tt);
  465. }
  466.  
  467. function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
  468. function looksLikeTrueName(artist, index = 0) {
  469. return twoOrMore(artist)
  470. && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
  471. && artist.split(/\s+/).length >= 2
  472. && !pseudoArtistParsers.some(rx => rx.test(artist)) || getSiteArtist(artist);
  473. }
  474.  
  475. function strip(art) {
  476. return artistStrips.reduce(function(acc, rx, ndx) {
  477. return ndx != 1 || rx.test(acc)/* && !notMonospaced(RegExp.$1)*/ ? acc.replace(rx, '') : acc;
  478. }, art);
  479. }
  480.  
  481. function getSiteArtist(artist) {
  482. //if (isOPS) return undefined;
  483. if (!artist || notSiteArtistsCache.includesCaseless(artist)) return null;
  484. var key = Object.keys(siteArtistsCache).find(it => it.toLowerCase() == artist.toLowerCase());
  485. if (key) return siteArtistsCache[key];
  486. var now = Date.now();
  487. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  488. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  489. apiTimeFrame.timeStamp = now;
  490. apiTimeFrame.requestCounter = 1;
  491. } else ++apiTimeFrame.requestCounter;
  492. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  493. if (apiTimeFrame.requestCounter > 5) {
  494. console.debug('getSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
  495. artist + '" (' + apiTimeFrame.requestCounter + ')');
  496. ++ajaxRejects;
  497. return undefined;
  498. }
  499. try {
  500. var requestUrl = '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist);
  501. xhr.open('GET', requestUrl, false);
  502. //if (isRED && prefs.redacted_api_key) xhr.setRequestHeader('Authorization', prefs.redacted_api_key);
  503. xhr.send();
  504. if (xhr.status == 404) {
  505. notSiteArtistsCache.pushUniqueCaseless(artist);
  506. return null;
  507. }
  508. if (xhr.readyState != XMLHttpRequest.DONE || xhr.status < 200 || xhr.status >= 400) {
  509. console.warn('getSiteArtist("' + artist + '") error:', xhr, 'url:', document.location.origin + requestUrl);
  510. return undefined; // error
  511. }
  512. let response = JSON.parse(xhr.responseText);
  513. if (response.status != 'success') {
  514. notSiteArtistsCache.pushUniqueCaseless(artist);
  515. return null;
  516. }
  517. siteArtistsCache[artist] = response.response;
  518. if (prefs.diag_mode) console.log('getSiteArtist("' + artist + '") success:', siteArtistsCache[artist]);
  519. return (siteArtistsCache[artist]);
  520. } catch(e) {
  521. console.error('UA::getSiteArtist("' + artist + '"):', e, xhr);
  522. return undefined;
  523. }
  524. }
  525.  
  526. function splitArtists(str, parsers = multiArtistParsers) {
  527. var result = [str];
  528. parsers.forEach(function(parser) {
  529. for (var i = result.length; i > 0; --i) {
  530. var j = result[i - 1].split(parser).map(strip);
  531. if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
  532. && !getSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
  533. }
  534. });
  535. return result;
  536. }
  537.  
  538. function splitAmpersands(artists) {
  539. if (typeof artists == 'string') var result = splitArtists(artists);
  540. else if (Array.isArray(artists)) result = Array.from(artists); else return [];
  541. ampersandParsers.forEach(function(ampersandParser) {
  542. for (let i = result.length; i > 0; --i) {
  543. let j = result[i - 1].split(ampersandParser).map(strip);
  544. if (j.length <= 1 || !j.every(looksLikeTrueName) || getSiteArtist(result[i - 1])) continue;
  545. result.splice(i - 1, 1, ...j.filter(function(artist) {
  546. return !result.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist));
  547. }));
  548. }
  549. });
  550. return result;
  551. }