[GMT] Extract artists from description & transfer artists between pages

Tries to extract artists from selected text or tracklist in group description, easy batch transfer of artist between different pages

当前为 2021-11-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name [GMT] Extract artists from description & transfer artists between pages
  3. // @namespace https://greasyfork.org/cs/users/321857-anakunda
  4. // @run-at document-end
  5. // @version 1.44.2
  6. // @description Tries to extract artists from selected text or tracklist in group description, easy batch transfer of artist between different pages
  7. // @author Anakunda
  8. // @copyright 2020-21, Anakunda (https://greasyfork.org/cs/users/321857-anakunda)
  9. // @license GPL-3.0-or-later
  10. // @match https://*/torrents.php?id=*
  11. // @match https://*/torrents.php?*&id=*
  12. // @match https://*/upload.php*
  13. // @match https://*/requests.php?action=new*
  14. // @match https://*/requests.php?action=view&id=*
  15. // @match https://*/requests.php?action=edit&id=*
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM_deleteValue
  19. // @grant GM_getTab
  20. // @grant GM_saveTab
  21. // @grant GM_getTabs
  22. // ==/UserScript==
  23.  
  24. 'use strict';
  25.  
  26. const hasStyleSheet = styleSheet => document.styleSheetSets && document.styleSheetSets.contains(styleSheet);
  27. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
  28. const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
  29. const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);
  30.  
  31. const artistClasses = [
  32. 'artist_main', 'artist_guest', 'artists_remix', 'artists_composers',
  33. 'artists_conductors', 'artists_dj', 'artists_producer', 'artists_arranger',
  34. ];
  35. const bcName = 'gazelle-artists', bcr = new BroadcastChannel(bcName), bcs = new BroadcastChannel(bcName);
  36. let groupArtists, saveBtn, loadBtn, span;
  37. function saveData(artists) {
  38. if (artists.length > 0) bcs.postMessage(artists.sort(function(a, b) {
  39. const c = (a[1] > 0 ? a[1] : Infinity) - (b[1] > 0 ? b[1] : Infinity);
  40. return c != 0 ? c : a[0].toLowerCase().localeCompare(b[0].toLowerCase());
  41. }));
  42. if (typeof GM_getTab == 'function' && typeof GM_saveTab == 'function') GM_getTab(function(tab) {
  43. if (artists.length > 0) {
  44. tab.artists = artists;
  45. tab.saveTimestamp = Date.now();
  46. } else delete tab.artists;
  47. GM_saveTab(tab);
  48. });
  49. }
  50. function setLoadData() {
  51. if (typeof GM_getTabs == 'function') GM_getTabs(function(tabs) {
  52. let artists = new Map;
  53. for (let tab in tabs) if (tabs[tab].saveTimestamp && tabs[tab].artists) artists.set(tabs[tab].saveTimestamp, tab);
  54. if (artists.size <= 0) return;
  55. loadBtn.artists = tabs[artists.get(Math.max(...artists.keys()))].artists;
  56. loadBtn.style.visibility = 'visible';
  57. });
  58. bcr.addEventListener('message', function(message) {
  59. console.assert(message instanceof MessageEvent && Array.isArray(message.data),
  60. 'message instanceof MessageEvent && Array.isArray(message.data)');
  61. if (!(message instanceof MessageEvent) || !Array.isArray(message.data)) return false;
  62. loadBtn.artists = message.data;
  63. loadBtn.style.visibility = 'visible';
  64. //saveData(message.data);
  65. });
  66. }
  67. if ((groupArtists = document.body.querySelector('td#artistfields')) != null) {
  68. function addControls(root = document.body.querySelector('td#artistfields')) {
  69. if (!(root instanceof HTMLElement)
  70. || (root = [':scope > a.brackets:last-of-type', ':scope > select[name="importance[]"]']
  71. .reduce((elem, selector) => elem || root.querySelector(selector), null)) == null) return;
  72. root.style.marginRight = '2em';
  73.  
  74. if (!new URLSearchParams(document.location.search).has('groupid')) {
  75. loadBtn = document.createElement('A');
  76. loadBtn.id = 'load-gazelle-artists';
  77. loadBtn.textContent = 'Load';
  78. loadBtn.className = 'brackets';
  79. loadBtn.style.visibility = 'hidden';
  80. loadBtn.href = '#';
  81. loadBtn.onclick = function(evt) {
  82. if (!Array.isArray(evt.currentTarget.artists) || evt.currentTarget.artists.length <= 0) return;
  83. let artists = evt.currentTarget.artists.filter(artist => artist.length == 2), artistFields;
  84. while ((artistFields = document.body.querySelectorAll('input[name="artists[]"]')).length != artists.length)
  85. if (artistFields.length < artists.length) AddArtistField(); else RemoveArtistField();
  86. artists.forEach(function(artist, ndx) {
  87. artistFields[ndx].value = artist[0];
  88. artistFields[ndx].nextElementSibling.value = artist[1];
  89. });
  90. return false;
  91. };
  92. setLoadData();
  93. root.insertAdjacentElement('afterend', loadBtn);
  94. }
  95.  
  96. saveBtn = document.createElement('A');
  97. saveBtn.id = 'save-gazelle-artists';
  98. saveBtn.textContent = 'Save';
  99. saveBtn.className = 'brackets';
  100. saveBtn.style = 'margin-right: 3px;';
  101. saveBtn.href = '#';
  102. saveBtn.onclick = function(evt) {
  103. evt.currentTarget.style.color = null;
  104. saveData(Array.from(document.body.querySelectorAll('input[name="artists[]"]'))
  105. .filter(artist => artist.value.trim().length > 0)
  106. .map(artist => [artist.value.trim(), artist.nextElementSibling.value]));
  107. evt.currentTarget.style.color = isDarkTheme ? 'lightgreen' : 'darkgreen';
  108. setTimeout(elem => { elem.style.color = null }, 1000, evt.currentTarget);
  109. return false;
  110. };
  111. root.insertAdjacentElement('afterend', saveBtn);
  112. }
  113.  
  114. addControls(groupArtists);
  115. const categories = document.getElementById('categories'), dynamicForm = document.getElementById('dynamic_form');
  116. if (dynamicForm != null) new MutationObserver(function(mutationsList) {
  117. if (categories != null && !['0', 'Music'].includes(categories.value)) return;
  118. for (let mutation of mutationsList) for (let node of mutation.addedNodes)
  119. if (node.nodeName == 'TABLE' && node.classList.contains('layout')) return addControls();
  120. }).observe(dynamicForm, { childList: true });
  121. return;
  122. }
  123. if ((groupArtists = document.body.querySelector('div.box_artists > div.head')) != null) {
  124. loadBtn = groupArtists.querySelector('span.edit_artists');
  125. span = document.createElement('SPAN');
  126. span.style.float = 'right';
  127. if (loadBtn != null) span.style.marginRight = '1em';
  128. saveBtn = document.createElement('A');
  129. saveBtn.id = 'save-gazelle-artists';
  130. saveBtn.textContent = 'Save';
  131. saveBtn.className = 'brackets';
  132. saveBtn.href = '#';
  133. saveBtn.onclick = function(evt) {
  134. evt.currentTarget.style.color = null;
  135. const ac = artistClasses.concat(['artists_main', 'artists_guest']);
  136. saveData(Array.from(document.body.querySelectorAll('div.box_artists > ul > li > a:first-of-type'))
  137. .map(artist => [artist.textContent.trim(), ac.indexOf(artist.parentNode.className) % 8 + 1]));
  138. evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
  139. setTimeout(elem => { elem.style.color = null }, 1000, evt.currentTarget);
  140. return false;
  141. }
  142. span.append(saveBtn);
  143. groupArtists.append(span);
  144. if (loadBtn != null) {
  145. span = document.createElement('SPAN');
  146. span.style = 'float: right; margin-left: 3pt;';
  147. saveBtn = document.createElement('A');
  148. saveBtn.id = 'remove-all-artists';
  149. saveBtn.title = 'Kill \'Em All';
  150. saveBtn.textContent = 'X';
  151. saveBtn.className = 'brackets';
  152. saveBtn.style.color = 'red';
  153. saveBtn.href = '#';
  154. saveBtn.onclick = function(evt) {
  155. if (confirm('Kill \'Em All?'))
  156. for (let a of document.querySelectorAll('ul#artist_list > li > span.remove_artist > a')) a.click();
  157. return false;
  158. }
  159. span.append(saveBtn);
  160. groupArtists.insertBefore(span, loadBtn);
  161. }
  162. }
  163. if ((groupArtists = document.body.querySelector('div.box_addartists > div.head')) != null) {
  164. span = document.createElement('SPAN');
  165. span.style = 'float: right; margin-right: 1em;';
  166. loadBtn = document.createElement('A');
  167. loadBtn.id = 'load-gazelle-artists';
  168. loadBtn.textContent = 'Load';
  169. loadBtn.className = 'brackets';
  170. loadBtn.style.visibility = 'hidden';
  171. loadBtn.href = '#';
  172. loadBtn.onclick = function(evt) {
  173. let artists = evt.currentTarget.artists.filter(artist => artist.length == 2), artistFields;
  174. while ((artistFields = document.body.querySelectorAll('input[name="aliasname[]"]')).length < artists.length)
  175. AddArtistField();
  176. artistFields.forEach(function(elem, ndx) {
  177. elem.value = ndx < artists.length ? artists[ndx][0] : '';
  178. elem.nextElementSibling.value = ndx < artists.length ? artists[ndx][1] : 0;
  179. });
  180. return false;
  181. }
  182. setLoadData();
  183. span.append(loadBtn);
  184. groupArtists.append(span);
  185.  
  186. span = document.createElement('SPAN');
  187. span.style = 'float: right; margin-right: 3px;';
  188. saveBtn = document.createElement('A');
  189. saveBtn.id = 'save-gazelle-artists';
  190. saveBtn.textContent = 'Save';
  191. saveBtn.className = 'brackets';
  192. saveBtn.href = '#';
  193. saveBtn.onclick = function(evt) {
  194. evt.currentTarget.style.color = null;
  195. saveData(Array.from(document.body.querySelectorAll('input[name="aliasname[]"]'))
  196. .filter(artist => artist.value.trim().length > 0)
  197. .map(artist => [artist.value.trim(), artist.nextElementSibling.value]));
  198. evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
  199. setTimeout(elem => { elem.style.color = null }, 1000, evt.currentTarget);
  200. return false;
  201. }
  202. span.append(saveBtn);
  203. groupArtists.append(span);
  204. }
  205.  
  206. for (let selector of ['div.box_addartists', 'div.box_artists']) {
  207. if ((groupArtists = document.body.querySelector(selector)) == null) continue;
  208. groupArtists.ondragover = evt => false;
  209. groupArtists.ondragenter = evt => { evt.currentTarget.style.backgroundColor = 'lawngreen' };
  210. groupArtists[isFirefox ? 'ondragexit' : 'ondragleave'] = evt => { evt.currentTarget.style.backgroundColor = null };
  211. groupArtists.ondrop = function(evt) {
  212. evt.currentTarget.style.backgroundColor = null;
  213. if (evt.target.nodeName == 'INPUT' && evt.target.type == 'text') return true;
  214. showDialog(evt.dataTransfer.getData('text/plain').trim());
  215. return false;
  216. };
  217. }
  218.  
  219. const addBox = document.body.querySelector('form.add_form[name="artists"]');
  220. if (addBox == null) return;
  221.  
  222. String.prototype.consolidateWhitespace = function() {
  223. return this.replace(/\s+/ig, ' ');
  224. };
  225.  
  226. Array.prototype.includesCaseless = function(str) {
  227. if (typeof str != 'string') return false;
  228. str = str.toLowerCase();
  229. return this.find(elem => typeof elem == 'string' && elem.toLowerCase() == str) != undefined;
  230. };
  231. Array.prototype.pushUnique = function(...items) {
  232. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) });
  233. return this.length;
  234. };
  235. Array.prototype.pushUniqueCaseless = function(...items) {
  236. if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) });
  237. return this.length;
  238. };
  239.  
  240. const siteApiTimeframeStorageKey = 'AJAX time frame', gazelleApiFrame = 10500;
  241. let xhr = new XMLHttpRequest, modal = null, btnAdd = null, btnCustom = null,
  242. customCtrls = [ ], sel = null, ajaxRejects = 0;
  243. let prefs = {
  244. set: function(prop, def) { this[prop] = GM_getValue(prop, def) }
  245. };
  246. let redacted_api_key = GM_getValue('redacted_api_key');
  247. try { var siteArtistsCache = JSON.parse(sessionStorage.siteArtistsCache) } catch(e) { siteArtistsCache = [ ] }
  248. try { var notSiteArtistsCache = JSON.parse(sessionStorage.notSiteArtistsCache) } catch(e) { notSiteArtistsCache = [ ] }
  249.  
  250. const styleSheet = `
  251. .modal {
  252. position: fixed;
  253. left: 0;
  254. top: 0;
  255. width: 100%;
  256. height: 100%;
  257. background-color: rgba(0, 0, 0, 0.5);
  258. opacity: 0;
  259. visibility: hidden;
  260. transform: scale(1.1);
  261. transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
  262. z-index: 999;
  263. }
  264.  
  265. .modal-content {
  266. position: absolute;
  267. top: 50%;
  268. left: 50%;
  269. font-size: 17px;
  270. transform: translate(-50%, -50%);
  271. background-color: FloralWhite;
  272. color: black;
  273. width: 31rem;
  274. border-radius: 0.5rem;
  275. padding: 2rem 2rem 2rem 2rem;
  276. font-family: monospace;
  277. }
  278.  
  279. .show-modal {
  280. opacity: 1;
  281. visibility: visible;
  282. transform: scale(1.0);
  283. transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
  284. }
  285.  
  286. input[type="text"] { cursor: text; }
  287. input[type="radio"] { cursor: pointer; }
  288. .lbl { cursor: pointer; }
  289.  
  290. .tooltip {
  291. position: relative;
  292. }
  293.  
  294. .tooltip .tooltiptext {
  295. visibility: hidden;
  296. width: 120px;
  297. background-color: #555;
  298. color: #fff;
  299. text-align: center;
  300. border-radius: 6px;
  301. padding: 5px 0;
  302. position: absolute;
  303. z-index: 1;
  304. bottom: 125%;
  305. left: 50%;
  306. margin-left: -60px;
  307. opacity: 0;
  308. transition: opacity 0.3s;
  309. }
  310.  
  311. .tooltip .tooltiptext::after {
  312. position: absolute;
  313. top: 100%;
  314. left: 50%;
  315. margin-left: -5px;
  316. border-width: 5px;
  317. border-style: solid;
  318. border-color: #555 transparent transparent transparent;
  319. }
  320.  
  321. .tooltip:hover .tooltiptext {
  322. visibility: visible;
  323. opacity: 1;
  324. }
  325.  
  326. button.splitter {
  327. position: relative;
  328. width: 20pt;
  329. height: 20pt;
  330. text-align: center;
  331. font-weight: bold;
  332. font-size: 10pt;
  333. top: -1pt;
  334. background-color: darkolivegreen;
  335. color: white;
  336. }
  337. `;
  338.  
  339. btnAdd = document.createElement('input');
  340. btnAdd.id = 'add-artists-from-selection';
  341. btnAdd.value = 'Extract from selection';
  342. btnAdd.onclick = add_from_selection;
  343. btnAdd.type = 'button';
  344. btnAdd.style.marginLeft = '5px';
  345. btnAdd.style.visibility = 'hidden';
  346. addBox.append(btnAdd);
  347.  
  348. let style = document.createElement('style');
  349. document.head.appendChild(style);
  350. style.id = 'artist-parser-form';
  351. style.type = 'text/css';
  352. style.innerHTML = styleSheet;
  353. let el, elem = [ ];
  354. elem.push(document.createElement('div'));
  355. elem[elem.length - 1].className = 'modal';
  356. elem[elem.length - 1].id = 'add-from-selection-form';
  357. modal = elem[0];
  358. elem.push(document.createElement('div'));
  359. elem[elem.length - 1].className = 'modal-content';
  360. elem.push(document.createElement('input'));
  361. elem[elem.length - 1].id = 'btnFill';
  362. elem[elem.length - 1].type = 'submit';
  363. elem[elem.length - 1].value = 'Capture';
  364. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 30px;";
  365. elem[elem.length - 1].onclick = doParse;
  366. elem.push(document.createElement('input'));
  367. elem[elem.length - 1].id = 'btnCancel';
  368. elem[elem.length - 1].type = 'button';
  369. elem[elem.length - 1].value = 'Cancel';
  370. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 65px;";
  371. elem[elem.length - 1].onclick = closeModal;
  372.  
  373. let presetIndex = 0;
  374. function addPreset(val, label = 'Custom', rx = null, order = [1, 2]) {
  375. elem.push(document.createElement('div'));
  376. el = document.createElement('input');
  377. elem[elem.length - 1].style.paddingBottom = '10px';
  378. el.id = 'parse-preset-' + val;
  379. el.name = 'parse-preset';
  380. el.value = val;
  381. if (val == 1) el.checked = true;
  382. el.type = 'radio';
  383. el.onchange = update_custom_ctrls;
  384. if (rx) {
  385. el.rx = rx;
  386. el.order = order;
  387. }
  388. if (val == 999) btnCustom = el;
  389. elem[elem.length - 1].appendChild(el);
  390. el = document.createElement('label');
  391. el.style.marginLeft = '10px';
  392. el.style.marginRight = '10px';
  393. el.htmlFor = 'parse-preset-' + val;
  394. el.className = 'lbl';
  395. el.innerHTML = label;
  396. elem[elem.length - 1].appendChild(el);
  397. if (val != 999) return;
  398. el = document.createElement('input');
  399. el.type = 'text';
  400. el.id = 'custom-pattern';
  401. el.style.width = '20rem';
  402. el.style.fontFamily = 'monospace';
  403. el.autoComplete = "on";
  404. addTooltip(el, 'RegExp to parse lines, first two captured groups are used');
  405. customCtrls.push(elem[elem.length - 1].appendChild(el));
  406. el = document.createElement('input');
  407. el.type = 'radio';
  408. el.name = 'parse-order';
  409. el.id = 'parse-order-1';
  410. el.value = 1;
  411. el.checked = true;
  412. el.style.marginLeft = '1rem';
  413. addTooltip(el, 'Captured regex groups assigned in order $1: artist(s), $2: assignment');
  414. customCtrls.push(elem[elem.length - 1].appendChild(el));
  415. el = document.createElement('label');
  416. el.htmlFor = 'parse-order-1';
  417. el.textContent = '→';
  418. el.style.marginLeft = '5px';
  419. elem[elem.length - 1].appendChild(el);
  420. el = document.createElement('input');
  421. el.type = 'radio';
  422. el.name = 'parse-order';
  423. el.id = 'parse-order-2';
  424. el.value = 2;
  425. el.style.marginLeft = '10px';
  426. addTooltip(el, 'Captured regex groups assigned in order $1: assignment, $2: artist(s)');
  427. customCtrls.push(elem[elem.length - 1].appendChild(el));
  428. el = document.createElement('label');
  429. el.htmlFor = 'parse-order-2';
  430. el.textContent = '←';
  431. el.style.marginLeft = '5px';
  432. elem[elem.length - 1].appendChild(el);
  433. }
  434. addPreset(++presetIndex, escapeHTML('<artist(s)>[ - <assignment>]'), /^\s*(.+?)(?:\:|\s+[\-\−\—\~\–]+\s+(.*?))?\s*$/);
  435. addPreset(++presetIndex, escapeHTML('<artist>[, <assignment>]') +
  436. '<span style="font-family: initial;">&nbsp;&nbsp;<i>(HRA style)</i></span>', /^\s*(.+?)(?:\:|\s*,\s*(.*?))?\s*$/);
  437. addPreset(++presetIndex, escapeHTML('<artist(s)>[: <assignment>]'), /^\s*(.+?)(?:\:|\s*:+\s*(.*?))?(?:\s*,)?\s*$/);
  438. addPreset(++presetIndex, escapeHTML('<artist(s)>[ (<assignment>)]'), /^\s*(.+?)(?:\:|\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))?(?:\s*,)?\s*$/);
  439. addPreset(++presetIndex, escapeHTML('<artist(s)>[ | <assignment>]'), /^\s*(.+?)(?:\s*\|\s*(.*?))?(?:\s*,)?\s*$/);
  440. addPreset(++presetIndex, escapeHTML('[<assignment> - ]<artist(s)>'), /^\s*(?:(.*?)\s+[\-\−\—\~\–]+\s+)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  441. addPreset(++presetIndex, escapeHTML('[<assignment>: ]<artist(s)>'), /^\s*(?:(.*?)\s*:+\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  442. addPreset(++presetIndex, escapeHTML('[<assignment> | ]<artist(s)>'), /^\s*(?:(.*?)\s*\|\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  443. addPreset(++presetIndex, escapeHTML('<artist>[ / <assignment>]'), /^\s*(.+?)(?:\:|\s*\/+\s*(.*?))?(?:\s*,)?\s*$/);
  444. addPreset(++presetIndex, escapeHTML('<artist>[; <assignment>]'), /^\s*(.+?)(?:\:|\s*;\s*(.*?))?(?:\s*,)?\s*$/);
  445. addPreset(++presetIndex, escapeHTML('[<assignment> / ]<artist(s)>'), /^\s*(?:(.*?)\s*\/+\s*)?(.+?)\:?(?:\s*,)?\s*$/, [2, 1]);
  446. addPreset(++presetIndex, '<span style="font-family: initial;">From tracklist</span>',
  447. /^\s*((?:\d+|[A-Z](?:\d+)?)(?:[\-\.](?:\d+|[A-Za-z])|[A-Za-z])?)(?:\s*[\-\−\—\~\–\.\:]\s*|\s+)(.+?)(?:\s+(\((?:\d+:)?\d+:\d+\)|\[(?:\d+:)?\d+:\d+\]))?\s*$/, []);
  448. addPreset(999);
  449. elem.slice(2).forEach(k => { elem[1].appendChild(k) });
  450. elem[0].appendChild(elem[1]);
  451. document.body.appendChild(elem[0]);
  452. window.addEventListener("click", windowOnClick);
  453. document.addEventListener('selectionchange', function(evt) {
  454. let cs = window.getComputedStyle(modal);
  455. if (!btnAdd || window.getComputedStyle(modal).visibility != 'hidden') return;
  456. showHideAddbutton();
  457. });
  458.  
  459. const vaParser = /^(?:Various(?:\s+Artists)?|Varios(?:\s+Artistas)?|V\/?A|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i;
  460. const multiArtistParsers = [
  461. /\s*[\,\;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*(?:[Aa]nd|\&)\s+)?\s*/,
  462. /\s+(?:[\/\|\×]|meets)\s+/i,
  463. ];
  464. const ampersandParsers = [
  465. /\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i,
  466. /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i,
  467. /(?:\s*,)?\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i,
  468. /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i,
  469. ];
  470. const featArtistParsers = [
  471. ///\s+(?:meets)\s+(.+?)\s*$/i,
  472. /* 0 */ /\s+(?:[Ww](?:ith|\.?\/)|[Aa]vec)\s+(?!his\b|her\b|Friends$|Strings$)(.+?)\s*$/,
  473. /* 1 */ /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff]eaturing\s+|(?:(?:[Ff]eat\.?|(?:[Ff]t|FT)\.))\s*|[Ff]\.?\/\s+)([^\(\)\[\]\{\}]+?)(?=\s*(?:[\(\[\{].*)?$)/,
  474. /* 2 */ /\s+\[\s*f(?:eat(?:\.?|uring)|t\.|\.?\/)\s+([^\[\]]+?)\s*\]/i,
  475. /* 3 */ /\s+\(\s*f(?:eat(?:\.?|uring)|t\.|\.?\/)\s+([^\(\)]+?)\s*\)/i,
  476. /* 4 */ /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i,
  477. /* 5 */ /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i,
  478. /* 6 */ /\s+\[\s*(?:with|[Ww]\.?\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/,
  479. /* 7 */ /\s+\(\s*(?:with|[Ww]\.?\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/,
  480. ];
  481. const remixParsers = [
  482. /\s+\((?:The\s+)?(?:Remix|RMX)(?:e[sd])?\)/i,
  483. /\s+\[(?:The\s+)?(?:Remix|RMX)(?:e[sd])?\]/i,
  484. /\s+(?:The\s+)?(?:Remix|RMX)(?:e[sd])?\s*$/i,
  485. /^(?:The\s+)?(?:(?:Remix|RMX)s)\b|\b(?:The\s+)?(?:Remixes)$/,
  486. /\s+\(([^\(\)]+?)[\'\’\`]s[^\(\)]*\s(?:(?:Re)?Mix|RMX|Reworx)\)/i,
  487. /\s+\[([^\[\]]+?)[\'\’\`]s[^\[\]]*\s(?:(?:Re)?Mix|RMX|Reworx)\]/i,
  488. /\s+\(([^\(\)]+?)\s+(?:(?:Extended|Enhanced)\s+)?(?:Remix|RMX|Reworx)\)/i,
  489. /\s+\[([^\[\]]+?)\s+(?:(?:Extended|Enhanced)\s+)?(?:Remix|RMX|Reworx)\]/i,
  490. /\s+\((?:Remix|RMX)(?:ed)?\s+by\s+([^\(\)]+)\)/i,
  491. /\s+\[(?:Remix|RMX)(?:ed)?\s+by\s+([^\[\]]+)\]/i,
  492. /(?:\s+[\-\−\—\–]|:)\s+(.+?)\s+(?:Remix|RMX)$/i,
  493. ];
  494. const arrParsers = [
  495. /\s+\(arr(?:anged\s+by|\.)\s+([^\(\)]+?)\s*\)/i,
  496. /\s+\[arr(?:anged\s+by|\.)\s+([^\[\]]+?)\s*\]/i,
  497. ];
  498. const prodParsers = [
  499. /\s+\(prod(?:uced(?:\s+by)?|\.\s+by)\s+([^\(\)]+?)\s*\)/i,
  500. /\s+\[prod(?:uced(?:\s+by)?|\.\s+by)\s+([^\[\]]+?)\s*\]/i,
  501. ];
  502. const otherArtistsParsers = [
  503. [/^(.*?)\s+(?:under|(?:conducted)\s+by)\s+(.*)$/, 4],
  504. [/^()(.*?)\s+\(conductor\)$/i, 4],
  505. //[/^()(.*?)\s+\(.*\)$/i, 1],
  506. ];
  507. const pseudoArtistParsers = [
  508. /* 0 */ vaParser,
  509. /* 1 */ /^(?:#??N[\/\-]?A|[JS]r\.?|Unknown(?:\s+Artist)?)$/i,
  510. /* 2 */ /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/,
  511. /* 3 */ /^(?:(Special\s+)??Guests?|Friends|(?:Studio\s+)?Orchestra)$/i,
  512. /* 4 */ /^(?:Various\s+Composers)$/i,
  513. /* 5 */ /^(?:[Aa]nonym)/,
  514. /* 6 */ /^(?:traditional|trad\.|lidová)$/i,
  515. /* 7 */ /\b(?:traditional|trad\.|lidová)$/,
  516. /* 8 */ /^(?:tradiční|lidová)\s+/,
  517. /* 9 */ /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/,
  518. ];
  519. const trimRemixers = str => [
  520. /^(?:f(?:eat(?:\.?|uring)|t\.|\.?\/))\s+-\s+/i,
  521. /(?:(?:\s+|^)(?:Original|Extended|Enhanced|Radio|Dance|Club|Session|Raw|Vocal|Dub|Soulful|\d{4}))+$/i,
  522. ].reduce((r, rx) => r.replace(rx, ''), str.trim().consolidateWhitespace());
  523.  
  524. function showDialog(text) {
  525. if (!text) return;
  526. prefs.set('preset', 1);
  527. prefs.set('custom_pattern', '^\\s*(.+?)(?:\\s*:+\\s*(.*?)|\\:)?\\s*$');
  528. prefs.set('custom_pattern_order', 1);
  529. setRadiosValue('parse-preset', prefs.preset);
  530. customCtrls[0].value = prefs.custom_pattern;
  531. setRadiosValue('parse-order', prefs.custom_pattern_order);
  532. sel = text;
  533. update_custom_ctrls();
  534. modal.classList.add("show-modal");
  535. }
  536.  
  537. function add_from_selection() {
  538. sel = document.getSelection();
  539. if (!sel.isCollapsed && modal != null) showDialog(sel.toString().trim());
  540. }
  541.  
  542. function doParse(expr, flags = '') {
  543. closeModal();
  544. if (!sel) return;
  545. let preset = getSelectedRadio('parse-preset');
  546. if (preset == null) return;
  547. prefs.preset = preset.value;
  548. let order = preset.order,
  549. custom_parse_order = getSelectedRadio('parse-order'),
  550. rx = preset.rx;
  551. if (!rx && preset.value == 999 && custom_parse_order != null) {
  552. rx = new RegExp(customCtrls[0].value);
  553. order = custom_parse_order != null ?
  554. custom_parse_order.value == 1 ? [1, 2] : custom_parse_order.value == 2 ? [2, 1] : null : [1, 2];
  555. }
  556. groupArtists = artistClasses.map(category => Array.from(document.querySelectorAll(`ul#artist_list > li.${category} > a`)).map(a => a.textContent.trim()));
  557. cleanupArtistsForm();
  558. sel.split(/(?:\r?\n)+/).forEach(function(line) {
  559. line = line.trim().replace(/\s+\(tracks?\b[^\(\)]+\)/, '').replace(/\s+\[tracks?\b[^\[\]]+\]/, '');
  560. if (!line || /^\s*(?:Recorded|Mastered)\b/i.test(line)) return;
  561. let matches = /^\s*(?:Produced)[ \-\−\—\~\–]by (.+?)\s*$/.exec(line);
  562. if (matches != null) splitAmpersands(matches[1]).forEach(producer => { addArtist(producer, 7) });
  563. else if (rx instanceof RegExp && (matches = rx.exec(line)) != null) {
  564. if (!Array.isArray(order) || order.length < 2) {
  565. let title = matches[2];
  566. if (/^(.+?) [\-\−\—\–] /.test(title)) {
  567. let artist = RegExp.$1;
  568. if ((matches = featArtistParsers.slice(0, 2).reduce((m, rx) => m || rx.exec(artist), null)) != null) {
  569. splitAmpersands(artist.slice(0, matches.index)).forEach(artist => { addArtist(artist, 1) });
  570. splitAmpersands(matches[1]).forEach(artist => { addArtist(artist, 2) });
  571. } else splitAmpersands(artist).forEach(artist => { addArtist(artist, 1) });
  572. }
  573. if ((matches = featArtistParsers.slice(1).reduce((m, rx) => m || rx.exec(title), null)) != null)
  574. splitAmpersands(matches[1]).forEach(guest => { addArtist(guest, 2) });
  575. if ((matches = remixParsers.slice(4).reduce((m, rx) => m || rx.exec(title), null)) != null)
  576. splitAmpersands(trimRemixers(matches[1])).forEach(remixer => { addArtist(remixer, 3) });
  577. if ((matches = prodParsers.reduce((m, rx) => m || rx.exec(title), null)) != null)
  578. splitAmpersands(matches[1]).forEach(producer => { addArtist(producer, 7) });
  579. // if ((matches = arrParsers.reduce((m, rx) => m || rx.exec(title), null)) != null)
  580. // splitAmpersands(matches[1]).forEach(arranger => { addArtist(arranger, 8) });
  581. } else if (matches[order[0]]) {
  582. let role = deduceArtist(matches[order[1]]);
  583. splitAmpersands(matches[order[0]]).forEach(artist => { addArtist(artist, role) });
  584. } else splitAmpersands(matches[order[1]]).forEach(artist => { addArtist(artist, 2) });
  585. }
  586. });
  587. prefs.custom_pattern = customCtrls[0].value;
  588. prefs.custom_pattern_order = custom_parse_order != null ? custom_parse_order.value : 1;
  589. for (let i in prefs) { if (typeof prefs[i] != 'function') GM_setValue(i, prefs[i]) }
  590. if (siteArtistsCache.length > 0) sessionStorage.siteArtistsCache = JSON.stringify(siteArtistsCache);
  591. if (notSiteArtistsCache.length > 0) sessionStorage.notSiteArtistsCache = JSON.stringify(notSiteArtistsCache);
  592.  
  593. function deduceArtist(str) {
  594. if (/\b(?:remix)/i.test(str)) return 3; // remixer
  595. if (/\b(?:composer|libretto|lyric\w*|written[ \-\−\—\~\–]by)\b/i.test(str)) return 4; // composer
  596. if (/\b(?:conduct|director\b|direction\b)/i.test(str)) return 5; // conductor
  597. if (/\b(?:compiler\b)/i.test(str)) return 6; // compiler
  598. if (/\b(?:producer\b|produced[ \-\−\—\~\–]by\b)/i.test(str)) return 7; // producer
  599. return 2;
  600. }
  601.  
  602. function addArtist(name, type = 1) {
  603. if (!name || !(type > 0) || pseudoArtistParsers.some(rx => rx.test(name))) return false;
  604. // avoid dupes
  605. if (groupArtists[type - 1].includesCaseless(name)) return false;
  606. switch (type) {
  607. case 1: if ([4, 5].some(cat => groupArtists[cat].includesCaseless(name))) return false; break;
  608. case 2: if ([0, 4, 5].some(cat => groupArtists[cat].includesCaseless(name))) return false; break;
  609. }
  610. let input = assignFreeArtistField();
  611. if (input == null) throw 'could not allocate free artist slot';
  612. input.value = name;
  613. const importance = input.nextElementSibling;
  614. importance.value = type;
  615. groupArtists[type - 1].push(name);
  616. if (ampersandParsers.some(rx => rx.test(name))) {
  617. let button = document.createElement('button');
  618. button.className = 'splitter';
  619. button.textContent = '↔';
  620. button.onclick = function(evt) {
  621. let artists = [input.value];
  622. ampersandParsers.forEach(function(rx) {
  623. for (let index = artists.length - 1; index >= 0; --index) {
  624. artists.splice(index, 1, ...artists[index].split(rx));
  625. }
  626. });
  627. if (artists.length > 1) {
  628. const artistUsed = artist => artist && parseInt(importance.value) > 0
  629. && groupArtists[parseInt(importance.value) - 1].includesCaseless(artist);
  630. input.value = !artistUsed(artists[0]) ? artists[0] : '';
  631. artists.slice(1).forEach(function(artist) {
  632. if (artistUsed(artist)) return;
  633. let input = assignFreeArtistField();
  634. if (input == null) throw 'could not allocate free artist slot';
  635. input.value = artist;
  636. input.nextElementSibling.value = importance.value;
  637. });
  638. }
  639. evt.target.remove();
  640. };
  641. input.insertAdjacentElement('afterend', button);
  642. }
  643. return true;
  644. }
  645. }
  646.  
  647. function closeModal() {
  648. if (modal == null) return;
  649. showHideAddbutton();
  650. modal.classList.remove("show-modal");
  651. }
  652.  
  653. function windowOnClick(event) {
  654. if (modal != null && event.target === modal) closeModal();
  655. }
  656.  
  657. function update_custom_ctrls() {
  658. function en(elem) {
  659. if (elem == null || btnCustom == null) return;
  660. elem.disabled = !btnCustom.checked;
  661. elem.style.opacity = btnCustom.checked ? 1 : 0.5;
  662. }
  663. customCtrls.forEach(k => { en(k) });
  664. }
  665.  
  666. function getSelectedRadio(name) {
  667. for (let i of document.getElementsByName(name)) { if (i.checked) return i }
  668. return null;
  669. }
  670.  
  671. function setRadiosValue(name, val) {
  672. for (let i of document.getElementsByName(name)) { if (i.value == val) i.checked = true }
  673. }
  674.  
  675. function showHideAddbutton() {
  676. //btnAdd.style.visibility = document.getSelection().type == 'Range' ? 'visible' : 'hidden';
  677. btnAdd.style.visibility = document.getSelection().isCollapsed ? 'hidden' : 'visible';
  678. }
  679.  
  680. function escapeHTML(string) {
  681. let pre = document.createElement('pre'), text = document.createTextNode(string);
  682. pre.appendChild(text);
  683. return pre.innerHTML;
  684. }
  685.  
  686. function cleanupArtistsForm() {
  687. document.querySelectorAll('div#AddArtists > input[name="aliasname[]"]').forEach(function(input) {
  688. input.value = '';
  689. input.nextElementSibling.value = 1;
  690. });
  691. document.querySelectorAll('div#AddArtists > button.splitter').forEach(button => { button.remove() });
  692. }
  693.  
  694. function assignFreeArtistField() {
  695. function findFreeSlot() {
  696. for (let input of document.querySelectorAll('div#AddArtists > input[name="aliasname[]"]'))
  697. if (input.value.length <= 0) return input;
  698. return null;
  699. }
  700. return findFreeSlot() || (AddArtistField(), findFreeSlot());
  701. }
  702.  
  703. function addTooltip(elem, text) {
  704. if (elem == null) return;
  705. elem.classList.add('tooltip');
  706. var tt = document.createElement('span');
  707. tt.className = 'tooltiptext';
  708. tt.textContent = text;
  709. elem.appendChild(tt);
  710. }
  711.  
  712. function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) };
  713. function looksLikeTrueName(artist, index = 0) {
  714. return twoOrMore(artist)
  715. && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist))
  716. && artist.split(/\s+/).length >= 2
  717. && !pseudoArtistParsers.some(rx => rx.test(artist)) || isSiteArtist(artist);
  718. }
  719.  
  720. function strip(art, level = 0) {
  721. return typeof art == 'string' ? [
  722. /\s+(?:aka|AKA|Aka)\.?\s+(.*)$/g,
  723. /\s*\([^\(\)]+\)/g,
  724. /\s*\[[^\[\]]+\]/g,
  725. /\s*\{[^\{\}]+\}/g,
  726. ].slice(level).reduce((acc, rx) => acc.replace(rx, ''), art) : undefined;
  727. }
  728.  
  729. function isSiteArtist(artist) {
  730. if (!artist || notSiteArtistsCache.includesCaseless(artist)) return false;
  731. if (siteArtistsCache.includesCaseless(artist)) return true;
  732. let now = Date.now();
  733. try { var apiTimeFrame = JSON.parse(localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = { } }
  734. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  735. apiTimeFrame.timeStamp = now;
  736. apiTimeFrame.requestCounter = 1;
  737. } else ++apiTimeFrame.requestCounter;
  738. localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  739. if (apiTimeFrame.requestCounter > 5) {
  740. if (groupArtists.some(art => art.includesCaseless(artist))) return true;
  741. console.debug('isSiteArtist() request exceeding AJAX API time frame: /ajax.php?action=artist&artistname="' +
  742. artist + '" (' + apiTimeFrame.requestCounter + ')');
  743. ++ajaxRejects;
  744. btnAdd.disabled = true;
  745. setTimeout(() => { btnAdd.disabled = false }, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  746. return undefined;
  747. }
  748. try {
  749. let requestUrl = '/ajax.php?action=artist&artistname=' + encodeURIComponent(artist);
  750. xhr.open('GET', requestUrl, false);
  751. if (document.location.hostname == 'redacted.ch' && redacted_api_key) xhr.setRequestHeader('Authorization', redacted_api_key);
  752. xhr.send();
  753. if (xhr.status == 404) {
  754. notSiteArtistsCache.push(artist);
  755. return false;
  756. }
  757. if (xhr.readyState != XMLHttpRequest.DONE || xhr.status < 200 || xhr.status >= 400) {
  758. console.warn('isSiteArtist("' + artist + '") error:', xhr, 'url:', document.location.origin + requestUrl);
  759. return undefined; // error
  760. }
  761. let response = JSON.parse(xhr.responseText);
  762. if (response.status != 'success') {
  763. notSiteArtistsCache.push(artist);
  764. return false;
  765. }
  766. if (!response.response) return false;
  767. siteArtistsCache.push(artist);
  768. return true;
  769. } catch(e) {
  770. console.error('isSiteArtist("' + artist + '"):', e, xhr);
  771. return undefined;
  772. }
  773. }
  774.  
  775. function splitArtists(str, parsers = multiArtistParsers) {
  776. let result = [str];
  777. parsers.forEach(function(parser) {
  778. for (let i = result.length; i > 0; --i) {
  779. let j = result[i - 1].split(parser).map(strip);
  780. if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))
  781. && !isSiteArtist(result[i - 1])) result.splice(i - 1, 1, ...j);
  782. }
  783. });
  784. return result;
  785. }
  786.  
  787. function splitAmpersands(artists) {
  788. if (!artists) return [ ];
  789. if (typeof artists == 'string') var result = splitArtists(strip(artists, 1));
  790. else if (Array.isArray(artists)) result = Array.from(artists); else return [];
  791. ampersandParsers.forEach(function(ampersandParser) {
  792. for (let i = result.length; i > 0; --i) {
  793. let j = result[i - 1].split(ampersandParser).map(strip);
  794. if (j.length <= 1 || isSiteArtist(result[i - 1]) || !j.every(looksLikeTrueName)) continue;
  795. result.splice(i - 1, 1, ...j.filter(function(artist) {
  796. return !result.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist));
  797. }));
  798. }
  799. });
  800. return result;
  801. }