Extract artists from description for Gazelle

Tries to extract artists from selected text or tracklist in group description

当前为 2020-12-28 提交的版本,查看 最新版本

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