[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

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