Extract artists from description for Gazelle

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

当前为 2021-07-03 提交的版本,查看 最新版本

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