Torrent Collage Extensions for Gazelle

Click on collage size = browse through this collage; Alt + click on collage name = remove from this collage

当前为 2020-11-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Torrent Collage Extensions for Gazelle
  3. // @version 1.04
  4. // @description Click on collage size = browse through this collage; Alt + click on collage name = remove from this collage
  5. // @author Anakunda
  6. // @license GPL-3.0-or-later
  7. // @copyright 2020, Anakunda (https://openuserjs.org/users/Anakunda)
  8. // @namespace https://greasyfork.org/users/321857-anakunda
  9. // @match https://*/torrents.php?id=*
  10. // @grant GM_getValue
  11. // ==/UserScript==
  12.  
  13. 'use strict';
  14.  
  15. const searchforcollage = document.getElementById('searchforcollage');
  16. if (searchforcollage != null) {
  17. if (typeof SearchCollage == 'function') SearchCollage = () => {
  18. const searchTerm = $('#searchforcollage').val(),
  19. personalCollages = $('#personalcollages');
  20. ajax.get(`ajax.php?action=collages&search=${escape(searchTerm)}`, responseText => {
  21. const { response, status } = JSON.parse(responseText);
  22. if (status !== 'success') return;
  23. const categories = response.reduce((accumulator, item) => {
  24. const { collageCategoryName } = item;
  25. accumulator[collageCategoryName] = (accumulator[collageCategoryName] || []).concat(item);
  26. return accumulator;
  27. }, {});
  28. personalCollages.children().remove();
  29. Object.entries(categories).forEach(([category, collages]) => {
  30. console.log(collages);
  31. personalCollages.append(`
  32. <optgroup label="${category}">
  33. ${collages.reduce((accumulator, { id, name }) =>
  34. `${accumulator}<option value="${id}">${name}</option>`
  35. ,'')}
  36. </optgroup>
  37. `);
  38. });
  39. });
  40. };
  41.  
  42. function inputHandler(evt, key) {
  43. const data = evt[key].getData('text/plain').trim();
  44. if (!data) return true;
  45. evt.target.value = data;
  46. SearchCollage();
  47. // setTimeout(function() {
  48. // const add_to_collage_select = document.querySelector('select.add_to_collage_select');
  49. // if (add_to_collage_select != null && add_to_collage_select.getElementsByTagName('option').length == 1)
  50. // AddToCollage();
  51. // }, 2000);
  52. return false;
  53. }
  54. searchforcollage.onpaste = evt => inputHandler(evt, 'clipboardData');
  55. searchforcollage.ondrop = evt => inputHandler(evt, 'dataTransfer');
  56. }
  57.  
  58. if (!/\b(?:id)=(\d+)\b/i.test(document.location.search)) throw 'Unexpected URL format';
  59. var torrentId = parseInt(RegExp.$1), auth = document.querySelector('input[name="auth"]');
  60. if (auth == null) {
  61. auth = document.querySelector('li#nav_logout > a.user-info-bar__link');
  62. if (auth == null || !/\b(?:auth)=(\w+)\b/.test(auth.search)) throw 'Auth not found';
  63. auth = RegExp.$1;
  64. } else auth = auth.value;
  65. const siteApiTimeframeStorageKey = document.location.hostname + ' API time frame';
  66. const gazelleApiFrame = 10500;
  67. if (typeof GM_getValue == 'function') var redacted_api_key = GM_getValue('redacted_api_key');
  68. try { var collages = JSON.parse(window.sessionStorage.collages) } catch(e) { collages = {} }
  69. if (!collages[document.domain]) collages[document.domain] = {};
  70.  
  71. document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) {
  72. if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return;
  73. var collageId = parseInt(RegExp.$1), toggle, navLinks = [],
  74. numberColumn = link.parentNode.parentNode.querySelector('td.number_column');
  75. link.onclick = evt => evt.button == 0 && evt.altKey ? removeFromCollage(collageId, link) : true;
  76. link.title = 'Use Alt + left click to remove from this collage';
  77. if (numberColumn != null) {
  78. numberColumn.style.cursor = 'pointer';
  79. numberColumn.onclick = loadCollage;
  80. numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing';
  81. }
  82. if (collages[document.domain][collageId]) {
  83. expandSection();
  84. addCollageLinks(collages[document.domain][collageId]);
  85. }
  86.  
  87. function addCollageLinks(collage) {
  88. var index = collage.torrentgroups.findIndex(group => group.id == torrentId);
  89. if (index < 0) {
  90. console.warn('Assertion failed: torrent', torrentId, 'not found in the collage', collage);
  91. return false;
  92. }
  93. link.style.color = 'white';
  94. link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;';
  95. var stats = document.createElement('span');
  96. stats.textContent = `${index + 1} / ${collage.torrentgroups.length}`;
  97. stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;';
  98. navLinks.push(stats);
  99. link.parentNode.append(stats);
  100. if (collage.torrentgroups[index - 1]) {
  101. var a = document.createElement('a');
  102. a.href = '/torrents.php?id=' + collage.torrentgroups[index - 1].id;
  103. a.textContent = '[\xA0<\xA0]';
  104. a.title = getTitle(index - 1);
  105. a.style = 'color: chartreuse; margin-right: 10px;';
  106. navLinks.push(a);
  107. link.parentNode.prepend(a);
  108. a = document.createElement('a');
  109. a.href = '/torrents.php?id=' + collage.torrentgroups[0].id;
  110. a.textContent = '[\xA0<<\xA0]';
  111. a.title = getTitle(0);
  112. a.style = 'color: chartreuse; margin-right: 5px;';
  113. navLinks.push(a);
  114. link.parentNode.prepend(a);
  115. }
  116. if (collage.torrentgroups[index + 1]) {
  117. a = document.createElement('a');
  118. a.href = '/torrents.php?id=' + collage.torrentgroups[index + 1].id;
  119. a.textContent = '[\xA0>\xA0]';
  120. a.title = getTitle(index + 1);
  121. a.style = 'color: chartreuse; margin-left: 10px;';
  122. navLinks.push(a);
  123. link.parentNode.append(a);
  124. a = document.createElement('a');
  125. a.href = '/torrents.php?id=' + collage.torrentgroups[collage.torrentgroups.length - 1].id;
  126. a.textContent = '[\xA0>>\xA0]';
  127. a.title = getTitle(collage.torrentgroups.length - 1);
  128. a.style = 'color: chartreuse; margin-left: 5px;';
  129. navLinks.push(a);
  130. link.parentNode.append(a);
  131. }
  132. return true;
  133.  
  134. function getTitle(index) {
  135. if (typeof index != 'number' || index < 0 || index >= collage.torrentgroups.length) return undefined;
  136. var title = collage.torrentgroups[index].musicInfo && Array.isArray(collage.torrentgroups[index].musicInfo.artists) ?
  137. collage.torrentgroups[index].musicInfo.artists.map(artist => artist.name).join(', ') + ' - ' : '';
  138. if (collage.torrentgroups[index].name) title += collage.torrentgroups[index].name;
  139. if (collage.torrentgroups[index].year) title += ' (' + collage.torrentgroups[index].year + ')';
  140. return title;
  141. }
  142. }
  143.  
  144. function expandSection() {
  145. if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]');
  146. if (toggle === null || toggle.dataset.expanded) return false;
  147. toggle.dataset.expanded = true;
  148. toggle.click();
  149. return true;
  150. }
  151.  
  152. function loadCollage(evt) {
  153. evt.target.disabled = true;
  154. navLinks.forEach(a => { a.remove() });
  155. navLinks = [];
  156. var span = document.createElement('span');
  157. span.textContent = '[\xA0loading...\xA0]';
  158. span.style = 'color: red; background-color: white; margin-left: 10px;';
  159. link.parentNode.append(span);
  160. queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
  161. span.remove();
  162. cacheCollage(collage);
  163. addCollageLinks(collage);
  164. evt.target.disabled = false;
  165. }, function(reason) {
  166. span.remove();
  167. evt.target.disabled = false;
  168. });
  169. return false;
  170. }
  171. });
  172.  
  173. function cacheCollage(collage) {
  174. collages[document.domain][collage.id] = {
  175. id: collage.id,
  176. name: collage.name,
  177. torrentgroups: collage.torrentgroups.map(group => ({
  178. id: group.id,
  179. musicInfo: group.musicInfo ? {
  180. artists: Array.isArray(group.musicInfo.artists) ?
  181. group.musicInfo.artists.map(artist => ({ name: artist.name })) : undefined,
  182. } : undefined,
  183. name: group.name,
  184. year: group.year,
  185. })),
  186. };
  187. window.sessionStorage.collages = JSON.stringify(collages);
  188. }
  189.  
  190. function removeFromCollage(collageId, node = null) {
  191. if (!confirm('Are you sure to remove from collage "' + node.textContent.trim() + '"?')) return false;
  192. let xhr = new XMLHttpRequest, params = new URLSearchParams({
  193. action: 'manage_handle',
  194. collageid: collageId,
  195. groupid: torrentId,
  196. auth: auth,
  197. submit: 'Remove',
  198. });
  199. xhr.open('POST', '/collages.php', true);
  200. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  201. xhr.onreadystatechange = function() {
  202. if (xhr.readyState != XMLHttpRequest.HEADERS_RECEIVED) return;
  203. if (xhr.status >= 200 && xhr.status < 400) {
  204. if (node instanceof HTMLElement) node.parentNode.parentNode.remove(); else document.location.reload();
  205. } else errorHandler();
  206. xhr.abort();
  207. };
  208. xhr.onerror = errorHandler;
  209. xhr.send(params);
  210. return false;
  211.  
  212. function errorHandler() { console.error('Failed to remove', torrentId, 'from collage', collageId, xhr) }
  213. }
  214.  
  215. function queryAjaxAPI(action, params) {
  216. if (!action) return Promise.reject('Action missing');
  217. var retryCount = 0;
  218. return new Promise(function(resolve, reject) {
  219. params = new URLSearchParams(params || undefined);
  220. params.set('action', action);
  221. var url = '/ajax.php?' + params, xhr = new XMLHttpRequest;
  222. queryInternal();
  223.  
  224. function queryInternal() {
  225. var now = Date.now();
  226. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  227. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  228. apiTimeFrame.timeStamp = now;
  229. apiTimeFrame.requestCounter = 1;
  230. } else ++apiTimeFrame.requestCounter;
  231. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  232. if (apiTimeFrame.requestCounter <= 5) {
  233. xhr.open('GET', url, true);
  234. xhr.setRequestHeader('Accept', 'application/json');
  235. if (redacted_api_key) xhr.setRequestHeader('Authorization', redacted_api_key);
  236. xhr.responseType = 'json';
  237. //xhr.timeout = 5 * 60 * 1000;
  238. xhr.onload = function() {
  239. if (xhr.status == 404) return reject('not found');
  240. if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
  241. if (xhr.response.status == 'success') return resolve(xhr.response.response);
  242. if (xhr.response.error == 'not found') return reject(xhr.response.error);
  243. console.warn('queryAjaxAPI.queryInternal(...) response:', xhr, xhr.response);
  244. if (xhr.response.error == 'rate limit exceeded') {
  245. console.warn('queryAjaxAPI.queryInternal(...) ' + xhr.response.error + ':', apiTimeFrame, now, retryCount);
  246. if (retryCount++ <= 10) return setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  247. }
  248. reject('API ' + xhr.response.status + ': ' + xhr.response.error);
  249. };
  250. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  251. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  252. xhr.send();
  253. } else {
  254. setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  255. console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
  256. action + ' (' + apiTimeFrame.requestCounter + ')');
  257. }
  258. }
  259. });
  260. }
  261.  
  262. function defaultErrorHandler(response) {
  263. console.error('HTTP error:', response);
  264. var e = 'HTTP error ' + response.status;
  265. if (response.statusText) e += ' (' + response.statusText + ')';
  266. if (response.error) e += ' (' + response.error + ')';
  267. return e;
  268. }
  269.  
  270. function defaultTimeoutHandler(response) {
  271. console.error('HTTP timeout:', response);
  272. const e = 'HTTP timeout';
  273. return e;
  274. }