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