Gazelle extract featured artists from description

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

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

  1. // ==UserScript==
  2. // @name Gazelle extract featured artists from description
  3. // @namespace https://greasyfork.org/cs/users/321857-anakunda
  4. // @version 1.1
  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. content: "";
  87. position: absolute;
  88. top: 100%;
  89. left: 50%;
  90. margin-left: -5px;
  91. border-width: 5px;
  92. border-style: solid;
  93. border-color: #555 transparent transparent transparent;
  94. }
  95.  
  96. .tooltip:hover .tooltiptext {
  97. visibility: visible;
  98. opacity: 1;
  99. }
  100. `;
  101.  
  102. var addBox = document.querySelector('form.add_form[name="artists"]');
  103. if (addBox == null) return;
  104. btnAdd = document.createElement('input');
  105. btnAdd.id = 'add-artists-from-selection';
  106. btnAdd.value = 'Extract from selection';
  107. btnAdd.onclick = add_from_selection;
  108. btnAdd.type = 'button';
  109. btnAdd.style.marginLeft = '5px';
  110. btnAdd.style.visibility = 'hidden';
  111. addBox.appendChild(btnAdd);
  112.  
  113. var style = document.createElement('style');
  114. document.head.appendChild(style);
  115. style.id = 'artist-parser-form';
  116. style.type = 'text/css';
  117. style.innerHTML = styleSheet;
  118. var el, elem = [];
  119. elem.push(document.createElement('div'));
  120. elem[elem.length - 1].className = 'modal';
  121. elem[elem.length - 1].id = 'add-from-selection-form';
  122. modal = elem[0];
  123. elem.push(document.createElement('div'));
  124. elem[elem.length - 1].className = 'modal-content';
  125. elem.push(document.createElement('input'));
  126. elem[elem.length - 1].id = 'btnFill';
  127. elem[elem.length - 1].type = 'submit';
  128. elem[elem.length - 1].value = 'Capture';
  129. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 30px;";
  130. elem[elem.length - 1].onclick = do_parse;
  131. elem.push(document.createElement('input'));
  132. elem[elem.length - 1].id = 'btnCancel';
  133. elem[elem.length - 1].type = 'button';
  134. elem[elem.length - 1].value = 'Cancel';
  135. elem[elem.length - 1].style = "position: fixed; right: 30px; width: 80px; top: 65px;";
  136. elem[elem.length - 1].onclick = closeModal;
  137. var presetIndex = 0;
  138. function addPreset(val, label = 'Custom', rx = null, order = [1, 2]) {
  139. elem.push(document.createElement('div'));
  140. el = document.createElement('input');
  141. elem[elem.length - 1].style.paddingBottom = '10px';
  142. el.id = 'parse-preset-' + val;
  143. el.name = 'parse-preset';
  144. el.value = val;
  145. if (val == 1) el.checked = true;
  146. el.type = 'radio';
  147. el.onchange = update_custom_ctrls;
  148. if (rx) {
  149. el.rx = rx;
  150. el.order = order;
  151. }
  152. if (val == 999) btnCustom = el;
  153. elem[elem.length - 1].appendChild(el);
  154. el = document.createElement('label');
  155. el.style.marginLeft = '10px';
  156. el.style.marginRight = '10px';
  157. el.htmlFor = 'parse-preset-' + val;
  158. el.className = 'lbl';
  159. el.innerHTML = label;
  160. elem[elem.length - 1].appendChild(el);
  161. if (val != 999) return;
  162. el = document.createElement('input');
  163. el.type = 'text';
  164. el.id = 'custom-pattern';
  165. el.style.width = '20rem';
  166. el.style.fontFamily = 'monospace';
  167. el.autoComplete = "on";
  168. addTooltip(el, 'RegExp to parse lines, first two captured groups are used');
  169. customCtrls.push(elem[elem.length - 1].appendChild(el));
  170. el = document.createElement('input');
  171. el.type = 'radio';
  172. el.name = 'parse-order';
  173. el.id = 'parse-order-1';
  174. el.value = 1;
  175. el.checked = true;
  176. el.style.marginLeft = '1rem';
  177. addTooltip(el, 'Captured regex groups assigned in order $1: artist(s), $2: assignment');
  178. customCtrls.push(elem[elem.length - 1].appendChild(el));
  179. el = document.createElement('label');
  180. el.htmlFor = 'parse-order-1';
  181. el.textContent = '→';
  182. el.style.marginLeft = '5px';
  183. elem[elem.length - 1].appendChild(el);
  184. el = document.createElement('input');
  185. el.type = 'radio';
  186. el.name = 'parse-order';
  187. el.id = 'parse-order-2';
  188. el.value = 2;
  189. el.style.marginLeft = '10px';
  190. addTooltip(el, 'Captured regex groups assigned in order $1: assignment, $2: artist(s)');
  191. customCtrls.push(elem[elem.length - 1].appendChild(el));
  192. el = document.createElement('label');
  193. el.htmlFor = 'parse-order-2';
  194. el.textContent = '←';
  195. el.style.marginLeft = '5px';
  196. elem[elem.length - 1].appendChild(el);
  197. }
  198. addPreset(++presetIndex, escapeHTML('<artist(s)> - <assignment>]'), /^\s*(.*?)(?:\s*[\-\−\—\~\–]+\s*(.*?))?\s*$/);
  199. addPreset(++presetIndex, escapeHTML('<artist>[, <assignment>]') +
  200. '<span style="font-family: initial;">&nbsp;&nbsp;<i>(HRA style)</i></span>',
  201. /^\s*(.*?)(?:\s*,\s*(.*?))?\s*$/);
  202. addPreset(++presetIndex, escapeHTML('<artist(s)>[: <assignment>]'), /^\s*(.*?)(?:\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(999);
  209. elem.slice(2).forEach(k => { elem[1].appendChild(k) });
  210. elem[0].appendChild(elem[1]);
  211. document.body.appendChild(elem[0]);
  212. window.addEventListener("click", windowOnClick);
  213. document.addEventListener('selectionchange', () => {
  214. var cs = window.getComputedStyle(modal);
  215. if (!btnAdd || window.getComputedStyle(modal).visibility != 'hidden') return;
  216. var sel = document.getSelection();
  217. ShowHideAddbutton();
  218. });
  219. })();
  220.  
  221. function add_from_selection() {
  222. sel = document.getSelection();
  223. if (sel.isCollapsed || modal == null) return;
  224. prefs.set('preset', 1);
  225. prefs.set('custom_pattern', '^\\s*(.*?)(?:\\s*:+\\s*(.*?))?\\s*$');
  226. prefs.set('custom_pattern_order', 1);
  227. setRadiosValue('parse-preset', prefs.preset);
  228. customCtrls[0].value = prefs.custom_pattern;
  229. setRadiosValue('parse-order', prefs.custom_pattern_order);
  230. sel = sel.toString();
  231. update_custom_ctrls();
  232. modal.classList.add("show-modal");
  233. }
  234.  
  235. function do_parse(expr, flags = '') {
  236. closeModal();
  237. if (!sel) return;
  238. var preset = getSelectedRadio('parse-preset');
  239. if (preset == null) return;
  240. prefs.preset = preset.value;
  241. var order = preset.order;
  242. var custom_parse_order = getSelectedRadio('parse-order');
  243. var rx = preset.rx;
  244. if (!rx && preset.value == 999 && custom_parse_order != null) {
  245. rx = new RegExp(customCtrls[0].value);
  246. if (custom_parse_order != null) {
  247. order = custom_parse_order.value == 1 ? [1, 2] : custom_parse_order.value == 2 ? [2, 1] : null;
  248. } else {
  249. order = [1, 2];
  250. }
  251. }
  252. const artistSplit = /\s*[\,\;\/\|]\s*/;
  253. function extr_artists(kind) { return document.querySelectorAll('ul#artist_list > li.artist_' + kind + ' > a') }
  254. var artists = [
  255. extr_artists('main'),
  256. extr_artists('guest'),
  257. extr_artists('remixer'),
  258. extr_artists('composer'),
  259. extr_artists('conductor'),
  260. extr_artists('compiler'),
  261. extr_artists('producer'),
  262. ];
  263. cleanupArtistsForm();
  264. var addedartists = new Array(7);
  265. addedartists.fill([]);
  266. artist_index = 0;
  267. for (var line of sel.split(/[\r\n]+/)) {
  268. if (!line || !line.trim()) continue;
  269. if (line.search(/^\s*(?:Recorded\b)/i) >= 0) continue;
  270. var matches = line.match(/^\s*Produced by (.+?)\s*$/);
  271. if (matches) {
  272. add_artist(matches[1], 7);
  273. } else if (matches = rx.exec(line)) {
  274. if (matches[order[0]]) {
  275. matches[order[0]].split(artistSplit).forEach(k => { add_artist(k, deduce_artist(matches[order[1]])) });
  276. } else {
  277. matches[order[1]].split(artistSplit).forEach(k => { add_artist(k) });
  278. }
  279. }
  280. }
  281. prefs.custom_pattern = customCtrls[0].value;
  282. prefs.custom_pattern_order = custom_parse_order != null ? custom_parse_order.value : 1;
  283. for (var i in prefs) { if (typeof prefs[i] != 'function') GM_setValue(i, prefs[i]) }
  284. return;
  285.  
  286. function deduce_artist(str) {
  287. var result = 2; // guest by default
  288. if (str) {
  289. if (str.search(/\b(?:remix)/i) >= 0) result = 3; // remixer
  290. if (str.search(/\b(?:composer\b|libretto\b|lyric)/i) >= 0) result = 4; // composer
  291. if (str.search(/\b(?:conduct|rirector\b)/i) >= 0) result = 5; // conductor
  292. if (str.search(/\b(?:compiler\b)/i) >= 0) result = 5; // conductor
  293. if (str.search(/\b(?:producer\b|produced by\b)/i) >= 0) result = 7; // producer
  294. }
  295. return result;
  296. }
  297.  
  298. function add_artist(name, type = 1) {
  299. if (!name || !type) return false;
  300. // avoid dupes
  301. for (var i of artists[0]) { if (name == i.textContent) return false }
  302. if (type >= 2) for (i of artists[type - 1]) { if (name == i.textContent) return false }
  303. for (i of addedartists[0]) { if (name == i) return false }
  304. if (type >= 2) for (i of addedartists[type - 1]) { if (name == i) return false }
  305. var id = get_artist_field(artist_index);
  306. if (id == null) {
  307. add_artist_field();
  308. id = get_artist_field(artist_index);
  309. if (id == null) return false;
  310. }
  311. id.value = name;
  312. id.nextElementSibling.value = type;
  313. addedartists[type - 1].push(name);
  314. ++artist_index;
  315. return true;
  316. }
  317. }
  318.  
  319. function add_artist_field() { exec(function() { AddArtistField() }) }
  320.  
  321. function exec(fn) {
  322. let script = document.createElement('script');
  323. script.type = 'application/javascript';
  324. script.textContent = '(' + fn + ')();';
  325. document.body.appendChild(script); // run the script
  326. document.body.removeChild(script); // clean up
  327. }
  328.  
  329. function get_artist_field(index) {
  330. var id = document.getElementById('artist');
  331. if (index <= 0) return id;
  332. for (var i = 0; i < index; ++i) {
  333. do { id = id.nextElementSibling } while (id != null && (id.localName != 'input' || id.name != 'aliasname[]'));
  334. if (id == null) break;
  335. }
  336. return id;
  337. }
  338.  
  339. function closeModal() {
  340. if (modal == null) return;
  341. ShowHideAddbutton();
  342. modal.classList.remove("show-modal");
  343. }
  344.  
  345. function windowOnClick(event) {
  346. if (modal != null && event.target === modal) closeModal();
  347. }
  348.  
  349. function update_custom_ctrls() {
  350. function en(elem) {
  351. if (elem == null || btnCustom == null) return;
  352. elem.disabled = !btnCustom.checked;
  353. elem.style.opacity = btnCustom.checked ? 1 : 0.5;
  354. }
  355. customCtrls.forEach(k => { en(k) });
  356. }
  357.  
  358. function getSelectedRadio(name) {
  359. for (var i of document.getElementsByName(name)) { if (i.checked) return i }
  360. return null;
  361. }
  362.  
  363. function setRadiosValue(name, val) {
  364. for (var i of document.getElementsByName(name)) { if (i.value == val) i.checked = true }
  365. }
  366.  
  367. function ShowHideAddbutton() {
  368. //btnAdd.style.visibility = document.getSelection().type == 'Range' ? 'visible' : 'hidden';
  369. btnAdd.style.visibility = document.getSelection().isCollapsed ? 'hidden' : 'visible';
  370. }
  371.  
  372. function escapeHTML(string) {
  373. var pre = document.createElement('pre');
  374. var text = document.createTextNode(string);
  375. pre.appendChild(text);
  376. return pre.innerHTML;
  377. }
  378.  
  379. function cleanupArtistsForm() {
  380. var id = get_artist_field(0);
  381. do {
  382. id.value = null;
  383. id = id.nextElementSibling;
  384. if (id == null) break;
  385. id.value = 1;
  386. do { id = id.nextElementSibling } while (id != null && (id.localName != 'input' || id.name != 'aliasname[]'));
  387. } while (id != null);
  388. }
  389.  
  390. function addTooltip(elem, text) {
  391. if (elem == null) return;
  392. elem.classList.add('tooltip');
  393. var tt = document.createElement('span');
  394. tt.className = 'tooltiptext';
  395. tt.textContent = text;
  396. elem.appendChild(tt);
  397. }