Gazelle extract featured artists from description

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

当前为 2020-10-20 提交的版本,查看 最新版本

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