Gazelle extract featured artists from description

Tries to recognize and add featured artists from selected text in description

当前为 2019-08-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Gazelle extract featured artists from description
  3. // @namespace https://greasyfork.org/cs/users/321857-anakunda
  4. // @version 1.32
  5. // @description Tries to recognize and add featured artists from selected text in description
  6. // @author Anakunda
  7. // @match https://redacted.ch/torrents.php?*id=*
  8. // @match https://orpheus.network/torrents.php?*id=*
  9. // @match https://notwhat.cd/torrents.php?*id=*
  10. // @grant RegExp
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_deleteValue
  15. // @grant GM_log
  16. // @require https://greasyfork.org/scripts/388280-xpathlib/code/XPathLib.js
  17. // ==/UserScript==
  18.  
  19. var artist_index;
  20. var modal = null, btnAdd = null, btnCustom = null, customCtrls = [], sel = null;
  21. var prefs = {
  22. set: function(prop, def) { this[prop] = GM_getValue(prop, def) }
  23. };
  24.  
  25. (function() {
  26. 'use strict';
  27.  
  28. const styleSheet = `
  29. .modal {
  30. position: fixed;
  31. left: 0;
  32. top: 0;
  33. width: 100%;
  34. height: 100%;
  35. background-color: rgba(0, 0, 0, 0.5);
  36. opacity: 0;
  37. visibility: hidden;
  38. transform: scale(1.1);
  39. transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
  40. }
  41. .modal-content {
  42. position: absolute;
  43. top: 50%;
  44. left: 50%;
  45. font-size: 17px;
  46. transform: translate(-50%, -50%);
  47. background-color: FloralWhite;
  48. color: black;
  49. width: 31rem;
  50. border-radius: 0.5rem;
  51. padding: 2rem 2rem 2rem 2rem;
  52. font-family: monospace;
  53. }
  54. .show-modal {
  55. opacity: 1;
  56. visibility: visible;
  57. transform: scale(1.0);
  58. transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
  59. }
  60. input[type="text"] { cursor: text; }
  61. input[type="radio"] { cursor: pointer; }
  62. .lbl { cursor: pointer; }
  63.  
  64. .tooltip {
  65. position: relative;
  66. }
  67.  
  68. .tooltip .tooltiptext {
  69. visibility: hidden;
  70. width: 120px;
  71. background-color: #555;
  72. color: #fff;
  73. text-align: center;
  74. border-radius: 6px;
  75. padding: 5px 0;
  76. position: absolute;
  77. z-index: 1;
  78. bottom: 125%;
  79. left: 50%;
  80. margin-left: -60px;
  81. opacity: 0;
  82. transition: opacity 0.3s;
  83. }
  84.  
  85. .tooltip .tooltiptext::after {
  86. position: absolute;
  87. top: 100%;
  88. left: 50%;
  89. margin-left: -5px;
  90. border-width: 5px;
  91. border-style: solid;
  92. border-color: #555 transparent transparent transparent;
  93. }
  94.  
  95. .tooltip:hover .tooltiptext {
  96. visibility: visible;
  97. opacity: 1;
  98. }
  99. `;
  100.  
  101. var addBox = document.querySelector('form.add_form[name="artists"]');
  102. if (addBox == null) return;
  103. btnAdd = document.createElement('input');
  104. btnAdd.id = 'add-artists-from-selection';
  105. btnAdd.value = 'Extract from selection';
  106. btnAdd.onclick = add_from_selection;
  107. btnAdd.type = 'button';
  108. btnAdd.style.marginLeft = '5px';
  109. btnAdd.style.visibility = 'hidden';
  110. addBox.appendChild(btnAdd);
  111.  
  112. var style = document.createElement('style');
  113. document.head.appendChild(style);
  114. style.id = 'artist-parser-form';
  115. style.type = 'text/css';
  116. style.innerHTML = styleSheet;
  117. var el, elem = [];
  118. elem.push(document.createElement('div'));
  119. elem[elem.length - 1].className = 'modal';
  120. elem[elem.length - 1].id = 'add-from-selection-form';
  121. modal = elem[0];
  122. elem.push(document.createElement('div'));
  123. elem[elem.length - 1].className = 'modal-content';
  124. elem.push(document.createElement('input'));
  125. elem[elem.length - 1].id = 'btnFill';
  126. elem[elem.length - 1].type = 'submit';
  127. elem[elem.length - 1].value = 'Capture';
  128. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 30px;";
  129. elem[elem.length - 1].onclick = do_parse;
  130. elem.push(document.createElement('input'));
  131. elem[elem.length - 1].id = 'btnCancel';
  132. elem[elem.length - 1].type = 'button';
  133. elem[elem.length - 1].value = 'Cancel';
  134. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 65px;";
  135. elem[elem.length - 1].onclick = closeModal;
  136. var presetIndex = 0;
  137. function addPreset(val, label = 'Custom', rx = null, order = [1, 2]) {
  138. elem.push(document.createElement('div'));
  139. el = document.createElement('input');
  140. elem[elem.length - 1].style.paddingBottom = '10px';
  141. el.id = 'parse-preset-' + val;
  142. el.name = 'parse-preset';
  143. el.value = val;
  144. if (val == 1) el.checked = true;
  145. el.type = 'radio';
  146. el.onchange = update_custom_ctrls;
  147. if (rx) {
  148. el.rx = rx;
  149. el.order = order;
  150. }
  151. if (val == 999) btnCustom = el;
  152. elem[elem.length - 1].appendChild(el);
  153. el = document.createElement('label');
  154. el.style.marginLeft = '10px';
  155. el.style.marginRight = '10px';
  156. el.htmlFor = 'parse-preset-' + val;
  157. el.className = 'lbl';
  158. el.innerHTML = label;
  159. elem[elem.length - 1].appendChild(el);
  160. if (val != 999) return;
  161. el = document.createElement('input');
  162. el.type = 'text';
  163. el.id = 'custom-pattern';
  164. el.style.width = '20rem';
  165. el.style.fontFamily = 'monospace';
  166. el.autoComplete = "on";
  167. addTooltip(el, 'RegExp to parse lines, first two captured groups are used');
  168. customCtrls.push(elem[elem.length - 1].appendChild(el));
  169. el = document.createElement('input');
  170. el.type = 'radio';
  171. el.name = 'parse-order';
  172. el.id = 'parse-order-1';
  173. el.value = 1;
  174. el.checked = true;
  175. el.style.marginLeft = '1rem';
  176. addTooltip(el, 'Captured regex groups assigned in order $1: artist(s), $2: assignment');
  177. customCtrls.push(elem[elem.length - 1].appendChild(el));
  178. el = document.createElement('label');
  179. el.htmlFor = 'parse-order-1';
  180. el.textContent = '→';
  181. el.style.marginLeft = '5px';
  182. elem[elem.length - 1].appendChild(el);
  183. el = document.createElement('input');
  184. el.type = 'radio';
  185. el.name = 'parse-order';
  186. el.id = 'parse-order-2';
  187. el.value = 2;
  188. el.style.marginLeft = '10px';
  189. addTooltip(el, 'Captured regex groups assigned in order $1: assignment, $2: artist(s)');
  190. customCtrls.push(elem[elem.length - 1].appendChild(el));
  191. el = document.createElement('label');
  192. el.htmlFor = 'parse-order-2';
  193. el.textContent = '←';
  194. el.style.marginLeft = '5px';
  195. elem[elem.length - 1].appendChild(el);
  196. }
  197. addPreset(++presetIndex, escapeHTML('<artist(s)> - <assignment>]'), /^\s*(.*?)(?:\s+[\-\−\—\~\–]+\s+(.*?))?\s*$/);
  198. addPreset(++presetIndex, escapeHTML('<artist>[, <assignment>]') +
  199. '<span style="font-family: initial;">&nbsp;&nbsp;<i>(HRA style)</i></span>',
  200. /^\s*(.*?)(?:\s*,\s*(.*?))?\s*$/);
  201. addPreset(++presetIndex, escapeHTML('<artist(s)>[: <assignment>]'), /^\s*(.*?)(?:\s*:+\s*(.*?))?\s*$/);
  202. addPreset(++presetIndex, escapeHTML('<artist(s)>[ (<assignment>)]'), /^\s*(.*?)(?:\s*(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))?\s*$/);
  203. addPreset(++presetIndex, escapeHTML('[<assignment> - ]<artist(s)>'), /^\s*(?:(.*?)\s+[\-\−\—\~\–]+\s+)?(.*?)\s*$/, [2, 1]);
  204. addPreset(++presetIndex, escapeHTML('[<assignment>: ]<artist(s)>'), /^\s*(?:(.*?)\s*:+\s*)?(.*?)\s*$/, [2, 1]);
  205. addPreset(++presetIndex, escapeHTML('<artist>[ / <assignment>]'), /^\s*(.*?)(?:\s*\/+\s*(.*?))?\s*$/);
  206. addPreset(++presetIndex, escapeHTML('<artist>[; <assignment>]'), /^\s*(.*?)(?:\s*;\s*(.*?))?\s*$/);
  207. addPreset(++presetIndex, escapeHTML('[<assignment> / ]<artist(s)>'), /^\s*(?:(.*?)\s*\/+\s*)?(.*?)\s*$/, [2, 1]);
  208. addPreset(++presetIndex, '<span style="font-family: initial;">From tracklist</span>',
  209. /^\s*\d+\s*[\-\−\—\~\–\.\:]\s*(.*?)(?:\s+[\-\−\—\~\–]|:)\s+|\s+\(feat(?:uring|\.)\s+([^\(\)]+)\)/, []);
  210. addPreset(999);
  211. elem.slice(2).forEach(k => { elem[1].appendChild(k) });
  212. elem[0].appendChild(elem[1]);
  213. document.body.appendChild(elem[0]);
  214. window.addEventListener("click", windowOnClick);
  215. document.addEventListener('selectionchange', () => {
  216. var cs = window.getComputedStyle(modal);
  217. if (!btnAdd || window.getComputedStyle(modal).visibility != 'hidden') return;
  218. var sel = document.getSelection();
  219. ShowHideAddbutton();
  220. });
  221. })();
  222.  
  223. function add_from_selection() {
  224. sel = document.getSelection();
  225. if (sel.isCollapsed || modal == null) return;
  226. prefs.set('preset', 1);
  227. prefs.set('custom_pattern', '^\\s*(.*?)(?:\\s*:+\\s*(.*?))?\\s*$');
  228. prefs.set('custom_pattern_order', 1);
  229. setRadiosValue('parse-preset', prefs.preset);
  230. customCtrls[0].value = prefs.custom_pattern;
  231. setRadiosValue('parse-order', prefs.custom_pattern_order);
  232. sel = sel.toString();
  233. update_custom_ctrls();
  234. modal.classList.add("show-modal");
  235. }
  236.  
  237. function do_parse(expr, flags = '') {
  238. closeModal();
  239. if (!sel) return;
  240. var preset = getSelectedRadio('parse-preset');
  241. if (preset == null) return;
  242. prefs.preset = preset.value;
  243. var order = preset.order;
  244. var custom_parse_order = getSelectedRadio('parse-order');
  245. var rx = preset.rx;
  246. if (!rx && preset.value == 999 && custom_parse_order != null) {
  247. rx = new RegExp(customCtrls[0].value);
  248. if (custom_parse_order != null) {
  249. order = custom_parse_order.value == 1 ? [1, 2] : custom_parse_order.value == 2 ? [2, 1] : null;
  250. } else {
  251. order = [1, 2];
  252. }
  253. }
  254. const artist_parser = /\s*(?:[\,\;\/\|]|(?:&)\s+(?!(?:The|His|Friends)\b))\s*/i;
  255. const weak_artist_parser = /\s*[\,\;\/\|]\s*/;
  256. const guest_parser = /^(.*?)(?:\s+(?:feat(?:\.|uring)|with|meets)\s+(.*))?$/;
  257. function extr_artists(kind) { return document.querySelectorAll('ul#artist_list > li.' + kind + ' > a') }
  258. var artists = [
  259. extr_artists('artist_main'),
  260. extr_artists('artist_guest'),
  261. extr_artists('artists_remix'),
  262. extr_artists('artists_composers'),
  263. extr_artists('artists_conductors'),
  264. extr_artists('artists_dj'),
  265. extr_artists('artists_producer'),
  266. ];
  267. cleanupArtistsForm();
  268. var addedartists = [];
  269. for (var i = 0; i < 7; ++i) addedartists.push([]);
  270. artist_index = 0;
  271. for (var line of sel.split(/[\r\n]+/)) {
  272. if (!line || !line.trim()) continue;
  273. if (line.search(/^\s*(?:Recorded|Mastered)\b/i) >= 0) continue;
  274. line = line.replace(/\s+\(tracks?\b[^\(\)]+\)/, '').replace(/\s+\[tracks?\b[^\[\]]+\]/, '')
  275. var matches = line.match(/^\s*Produced[ \-\−\—\~\–]by (.+?)\s*$/);
  276. if (matches) {
  277. add_artist(matches[1], 7);
  278. } else if (matches = rx.exec(line)) {
  279. var j;
  280. if (order.length < 2) {
  281. if (matches[2]) matches[2].split(weak_artist_parser).forEach(k => { add_artist(k, 2) });
  282. if (matches[1]) {
  283. matches = matches[1].match(guest_parser);
  284. matches[1].split(artist_parser).forEach(k => { add_artist(k) });
  285. if (matches[2]) matches[2].split(artist_parser).forEach(k => { add_artist(k, 2) });
  286. }
  287. } else if (matches[order[0]]) {
  288. matches[order[0]].split(artist_parser).forEach(k => { add_artist(k, deduce_artist(matches[order[1]])) });
  289. } else {
  290. matches[order[1]].split(artist_parser).forEach(k => { add_artist(k) });
  291. }
  292. }
  293. }
  294. prefs.custom_pattern = customCtrls[0].value;
  295. prefs.custom_pattern_order = custom_parse_order != null ? custom_parse_order.value : 1;
  296. for (i in prefs) { if (typeof prefs[i] != 'function') GM_setValue(i, prefs[i]) }
  297. return;
  298.  
  299. function deduce_artist(str) {
  300. var result = 2; // guest by default
  301. if (str) {
  302. if (str.search(/\b(?:remix)/i) >= 0) result = 3; // remixer
  303. if (str.search(/\b(?:composer|libretto|lyric\w*|written[ \-\−\—\~\–]by)\b/i) >= 0) result = 4; // composer
  304. if (str.search(/\b(?:conduct|rirector\b)/i) >= 0) result = 5; // conductor
  305. if (str.search(/\b(?:compiler\b)/i) >= 0) result = 5; // conductor
  306. if (str.search(/\b(?:producer\b|produced[ \-\−\—\~\–]by\b)/i) >= 0) result = 7; // producer
  307. }
  308. return result;
  309. }
  310.  
  311. function add_artist(name, type = 1) {
  312. if (!name || !type) return false;
  313. // avoid dupes
  314. var n = name.toLowerCase();
  315. for (var i of artists[0]) { if (n == i.textContent.toLowerCase()) return false }
  316. if (type >= 2) for (i of artists[type - 1]) { if (n == i.textContent.toLowerCase()) return false }
  317. for (i of addedartists[0]) { if (n == i.toLowerCase()) return false }
  318. if (type >= 2) for (i of addedartists[type - 1]) { if (n == i.toLowerCase()) return false }
  319. var id = get_artist_field(artist_index);
  320. if (id == null) {
  321. add_artist_field();
  322. id = get_artist_field(artist_index);
  323. if (id == null) return false;
  324. }
  325. id.value = name;
  326. id.nextElementSibling.value = type;
  327. addedartists[type - 1].push(name);
  328. ++artist_index;
  329. return true;
  330. }
  331. }
  332.  
  333. function add_artist_field() { exec(function() { AddArtistField() }) }
  334.  
  335. function exec(fn) {
  336. let script = document.createElement('script');
  337. script.type = 'application/javascript';
  338. script.textContent = '(' + fn + ')();';
  339. document.body.appendChild(script); // run the script
  340. document.body.removeChild(script); // clean up
  341. }
  342.  
  343. function get_artist_field(index) {
  344. var id = document.getElementById('artist');
  345. if (index <= 0) return id;
  346. for (var i = 0; i < index; ++i) {
  347. do { id = id.nextElementSibling } while (id != null && (id.localName != 'input' || id.name != 'aliasname[]'));
  348. if (id == null) break;
  349. }
  350. return id;
  351. }
  352.  
  353. function closeModal() {
  354. if (modal == null) return;
  355. ShowHideAddbutton();
  356. modal.classList.remove("show-modal");
  357. }
  358.  
  359. function windowOnClick(event) {
  360. if (modal != null && event.target === modal) closeModal();
  361. }
  362.  
  363. function update_custom_ctrls() {
  364. function en(elem) {
  365. if (elem == null || btnCustom == null) return;
  366. elem.disabled = !btnCustom.checked;
  367. elem.style.opacity = btnCustom.checked ? 1 : 0.5;
  368. }
  369. customCtrls.forEach(k => { en(k) });
  370. }
  371.  
  372. function getSelectedRadio(name) {
  373. for (var i of document.getElementsByName(name)) { if (i.checked) return i }
  374. return null;
  375. }
  376.  
  377. function setRadiosValue(name, val) {
  378. for (var i of document.getElementsByName(name)) { if (i.value == val) i.checked = true }
  379. }
  380.  
  381. function ShowHideAddbutton() {
  382. //btnAdd.style.visibility = document.getSelection().type == 'Range' ? 'visible' : 'hidden';
  383. btnAdd.style.visibility = document.getSelection().isCollapsed ? 'hidden' : 'visible';
  384. }
  385.  
  386. function escapeHTML(string) {
  387. var pre = document.createElement('pre');
  388. var text = document.createTextNode(string);
  389. pre.appendChild(text);
  390. return pre.innerHTML;
  391. }
  392.  
  393. function cleanupArtistsForm() {
  394. var id = get_artist_field(0);
  395. do {
  396. id.value = null;
  397. id = id.nextElementSibling;
  398. if (id == null) break;
  399. id.value = 1;
  400. do { id = id.nextElementSibling } while (id != null && (id.localName != 'input' || id.name != 'aliasname[]'));
  401. } while (id != null);
  402. }
  403.  
  404. function addTooltip(elem, text) {
  405. if (elem == null) return;
  406. elem.classList.add('tooltip');
  407. var tt = document.createElement('span');
  408. tt.className = 'tooltiptext';
  409. tt.textContent = text;
  410. elem.appendChild(tt);
  411. }