Collage Extensions for Gazelle Music Trackers

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

当前为 2020-12-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Collage Extensions for Gazelle Music Trackers
  3. // @version 1.20
  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. // @match https://*/collages.php?*id=*
  11. // @match https://*/artist.php?*id=*
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // ==/UserScript==
  15.  
  16. 'use strict';
  17.  
  18. var auth = document.querySelector('input[name="auth"][value]');
  19. if (auth != null) auth = auth.value; else {
  20. auth = document.querySelector('li#nav_logout > a');
  21. if (auth != null && /\b(?:auth)=(\w+)\b/.test(auth.search)) auth = RegExp.$1; else throw 'Auth not found';
  22. }
  23. let userId = document.querySelector('li#nav_userinfo > a.username');
  24. if (userId != null) {
  25. userId = new URLSearchParams(userId.search);
  26. userId = parseInt(userId.get('id'));
  27. }
  28.  
  29. const siteApiTimeframeStorageKey = document.location.hostname + ' API time frame';
  30. const gazelleApiFrame = 10500;
  31. if (typeof GM_getValue == 'function') var redacted_api_key = GM_getValue('redacted_api_key');
  32.  
  33. function queryAjaxAPI(action, params) {
  34. if (!action) return Promise.reject('Action missing');
  35. let retryCount = 0;
  36. return new Promise(function(resolve, reject) {
  37. params = new URLSearchParams(params || undefined);
  38. params.set('action', action);
  39. let url = '/ajax.php?' + params, xhr = new XMLHttpRequest;
  40. queryInternal();
  41.  
  42. function queryInternal() {
  43. let now = Date.now();
  44. try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
  45. if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
  46. apiTimeFrame.timeStamp = now;
  47. apiTimeFrame.requestCounter = 1;
  48. } else ++apiTimeFrame.requestCounter;
  49. window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
  50. if (apiTimeFrame.requestCounter <= 5) {
  51. xhr.open('GET', url, true);
  52. xhr.setRequestHeader('Accept', 'application/json');
  53. if (redacted_api_key) xhr.setRequestHeader('Authorization', redacted_api_key);
  54. xhr.responseType = 'json';
  55. //xhr.timeout = 5 * 60 * 1000;
  56. xhr.onload = function() {
  57. if (xhr.status == 404) return reject('not found');
  58. if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
  59. if (xhr.response.status == 'success') return resolve(xhr.response.response);
  60. if (xhr.response.error == 'not found') return reject(xhr.response.error);
  61. console.warn('queryAjaxAPI.queryInternal(...) response:', xhr, xhr.response);
  62. if (xhr.response.error == 'rate limit exceeded') {
  63. console.warn('queryAjaxAPI.queryInternal(...) ' + xhr.response.error + ':', apiTimeFrame, now, retryCount);
  64. if (retryCount++ <= 10) return setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  65. }
  66. reject('API ' + xhr.response.status + ': ' + xhr.response.error);
  67. };
  68. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  69. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  70. xhr.send();
  71. } else {
  72. setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
  73. console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
  74. action + ' (' + apiTimeFrame.requestCounter + ')');
  75. }
  76. }
  77. });
  78. }
  79.  
  80. function addToTorrentCollage(collageId, torrentGroupId) {
  81. return collageId ? torrentGroupId ? queryAjaxAPI('collage', { id: collageId }).then(
  82. collage => !collage.torrentGroupIDList.map(groupId => parseInt(groupId)).includes(torrentGroupId) ? collageId
  83. : Promise.reject('already in collage')
  84. ).then(collageId => new Promise(function(resolve, reject) {
  85. let xhr = new XMLHttpRequest, formData = new URLSearchParams({
  86. action: 'add_torrent',
  87. collageid: collageId,
  88. groupid: torrentGroupId,
  89. url: document.location.origin.concat('/torrents.php?id=', torrentGroupId),
  90. auth: auth,
  91. });
  92. xhr.open('POST', '/collages.php', true);
  93. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  94. xhr.onreadystatechange = function() {
  95. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  96. if (xhr.status >= 200 && xhr.status < 400) resolve(collageId); else reject(defaultErrorHandler(xhr));
  97. xhr.abort();
  98. };
  99. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  100. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  101. xhr.send(formData);
  102. })).then(collageId => queryAjaxAPI('collage', { id: collageId }).then(
  103. collage => collage.torrentGroupIDList.map(groupId => parseInt(groupId)).includes(torrentGroupId) ? collage
  104. : Promise.reject('Error: not added for unknown reason')
  105. )) : Promise.reject('torrent group id not defined') : Promise.reject('collage id not defined');
  106. }
  107.  
  108. function removeFromTorrentCollage(collageId, torrentGroupId, question) {
  109. if (!confirm(question)) return Promise.reject('Cancelled');
  110. return new Promise(function(resolve, reject) {
  111. let xhr = new XMLHttpRequest, formData = new URLSearchParams({
  112. action: 'manage_handle',
  113. collageid: collageId,
  114. groupid: torrentGroupId,
  115. auth: auth,
  116. submit: 'Remove',
  117. });
  118. xhr.open('POST', '/collages.php', true);
  119. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  120. xhr.onreadystatechange = function() {
  121. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  122. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
  123. xhr.abort();
  124. };
  125. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  126. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  127. xhr.send(formData);
  128. });
  129. }
  130.  
  131. function addToArtistCollage(collageId, artistId) {
  132. return collageId ? artistId ? queryAjaxAPI('collage', { id: collageId }).then(
  133. collage => !collage.artists.map(artist => parseInt(artist.id)).includes(artistId) ? collageId
  134. : Promise.reject('already in collage')
  135. ).then(collageId => new Promise(function(resolve, reject) {
  136. let xhr = new XMLHttpRequest, formData = new URLSearchParams({
  137. action: 'add_artist',
  138. collageid: collageId,
  139. artistid: artistId,
  140. url: document.location.origin.concat('/artist.php?id=', artistId),
  141. auth: auth,
  142. });
  143. xhr.open('POST', '/collages.php', true);
  144. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  145. xhr.onreadystatechange = function() {
  146. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  147. if (xhr.status >= 200 && xhr.status < 400) resolve(collageId); else reject(defaultErrorHandler(xhr));
  148. xhr.abort();
  149. };
  150. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  151. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  152. xhr.send(formData);
  153. })).then(collageId => queryAjaxAPI('collage', { id: collageId }).then(
  154. collage => collage.artists.map(artist => parseInt(artist.id)).includes(artistId) ? collage
  155. : Promise.reject('Error: not added for unknown reason')
  156. )) : Promise.reject('artist id not defined') : Promise.reject('collage id not defined');
  157. }
  158.  
  159. function removeFromArtistCollage(collageId, artistId, question) {
  160. if (!confirm(question)) return Promise.reject('Cancelled');
  161. return new Promise(function(resolve, reject) {
  162. let xhr = new XMLHttpRequest, formData = new URLSearchParams({
  163. action: 'manage_artists_handle',
  164. collageid: collageId,
  165. artistid: artistId,
  166. auth: auth,
  167. submit: 'Remove',
  168. });
  169. xhr.open('POST', '/collages.php', true);
  170. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  171. xhr.onreadystatechange = function() {
  172. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  173. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
  174. xhr.abort();
  175. };
  176. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  177. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  178. xhr.send(formData);
  179. });
  180. }
  181.  
  182. function defaultErrorHandler(response) {
  183. console.error('HTTP error:', response);
  184. let e = 'HTTP error ' + response.status;
  185. if (response.statusText) e += ' (' + response.statusText + ')';
  186. if (response.error) e += ' (' + response.error + ')';
  187. return e;
  188. }
  189.  
  190. function defaultTimeoutHandler(response) {
  191. console.error('HTTP timeout:', response);
  192. const e = 'HTTP timeout';
  193. return e;
  194. }
  195.  
  196. function addQuickAddForm() {
  197. if (!userId || !torrentGroupId && !artistId) return; // User id missing
  198. let ref = document.querySelector('div.sidebar');
  199. if (ref == null) return; // Sidebar missing
  200. const addSuccess = 'Successfully added to collage.';
  201. const alreadyInCollage = 'Error: This ' +
  202. (torrentGroupId ? 'torrent group' : artistId ? 'artist' : null) + ' is already in this collage';
  203. new Promise(function(resolve, reject) {
  204. try {
  205. var categories = JSON.parse(GM_getValue(document.location.hostname + '-categories'));
  206. if (categories.length > 0) resolve(categories); else throw 'empty list cached';
  207. } catch(e) {
  208. let xhr = new XMLHttpRequest;
  209. xhr.open('GET', '/collages.php', true);
  210. xhr.responseType = 'document';
  211. xhr.onload = function() {
  212. if (xhr.status >= 200 && xhr.status < 400) {
  213. categories = [ ];
  214. xhr.response.querySelectorAll('tr#categories > td > label').forEach(function(label, index) {
  215. let input = xhr.response.querySelector('tr#categories > td > input#' + label.htmlFor);
  216. categories[input != null && /\[(\d+)\]/.test(input.name) ? parseInt(RegExp.$1) : index] = label.textContent.trim();
  217. });
  218. if (categories.length > 0) {
  219. GM_setValue(document.location.hostname + '-categories', JSON.stringify(categories));
  220. resolve(categories);
  221. } else reject('Site categories could not be extracted');
  222. } else reject(defaultErrorHandler(xhr));
  223. };
  224. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  225. xhr.ontimeout = function() { reject(defaultTimeoutHandler()) };
  226. xhr.send();
  227. }
  228. }).then(function(categories) {
  229. const artistsIndex = categories.indexOf('Artists');
  230. if (artistId && artistsIndex < 0) throw 'Artists index not found';
  231. document.head.appendChild(document.createElement('style')).innerHTML = `
  232. form#addtocollage optgroup { background-color: slategray; color: white; }
  233. form#addtocollage option { background-color: white; color: black; max-width: 290pt; }
  234. div.box_addtocollage > form { padding: 0px 10px; }
  235. `;
  236. let elem = document.createElement('div');
  237. elem.className = 'box box_addtocollage';
  238. elem.style = 'padding: 0 0 10px;';
  239. elem.innerHTML = `
  240. <div class="head" style="margin-bottom: 5px;"><strong>Add to Collage</strong></div>
  241. <div id="ajax_message" class="hidden center" style="padding: 7px 0px;"></div>
  242. <form id="searchcollages">
  243. <input id="searchforcollage" placeholder="Collage search" type="text" style="max-width: 10em;">
  244. <input id="searchforcollagebutton" value="Search" type="submit" style="max-width: 4em;">
  245. </form>
  246. <form id="addtocollage" class="add_form" name="addtocollage">
  247. <select name="collageid" id="matchedcollages" class="add_to_collage_select" style="width: 96%;">
  248. <input id="opencollage-btn" value="Open collage" type="button">
  249. <input id="addtocollage-btn" value="Add to collage" type="button">
  250. </form>
  251. `;
  252. ref.append(elem);
  253. let ajaxMessage = document.getElementById('ajax_message');
  254. let srchForm = document.getElementById('searchcollages');
  255. if (srchForm == null) throw new Error('#searchcollages missing');
  256. let searchText = document.getElementById('searchforcollage');
  257. if (searchText == null) throw new Error('#searchforcollage missing');
  258. let dropDown = document.getElementById('matchedcollages');
  259. if (dropDown == null) throw new Error('#matchedcollages missing');
  260. let doOpen = document.getElementById('opencollage-btn');
  261. let doAdd = document.getElementById('addtocollage-btn');
  262. if (doAdd == null) throw new Error('#addtocollage-btn missing');
  263. srchForm.onsubmit = searchSubmit;
  264. searchText.ondrop = evt => dataHandler(evt.target, evt.dataTransfer);
  265. searchText.onpaste = evt => dataHandler(evt.target, evt.clipboardData);
  266. if (doOpen != null) doOpen.onclick = openCollage;
  267. doAdd.onclick = addToCollage;
  268. let initTimeCap = GM_getValue('max_preload_time', 0); // max time in ms to preload the dropdown
  269. if (initTimeCap > 0) findCollages({ userid: userId, contrib: 1 }, initTimeCap);
  270.  
  271. function clearList() {
  272. while (dropDown.childElementCount > 0) dropDown.removeChild(dropDown.firstElementChild);
  273. }
  274.  
  275. function findCollages(query, maxSearchTime) {
  276. return typeof query == 'object' ? new Promise(function(resolve, reject) {
  277. let start = Date.now();
  278. searchFormEnable(false);
  279. clearList();
  280. elem = document.createElement('option');
  281. elem.text = 'Searching...';
  282. dropDown.add(elem);
  283. dropDown.selectedIndex = 0;
  284. let retryCount = 0, options = [ ];
  285. searchInternal();
  286.  
  287. function searchInternal(page) {
  288. if (maxSearchTime > 0 && Date.now() - start > maxSearchTime) {
  289. reject('Time limit exceeded');
  290. return;
  291. }
  292. let xhr = new XMLHttpRequest, _query = new URLSearchParams(query);
  293. if (!page) page = 1;
  294. _query.set('page', page);
  295. xhr.open('GET', '/collages.php?' + _query, true);
  296. xhr.responseType = 'document';
  297. xhr.onload = function() {
  298. if (xhr.status < 200 || xhr.status >= 400) throw defaultErrorHandler(xhr);
  299. xhr.response.querySelectorAll('table.collage_table > tbody > tr[class^="row"]').forEach(function(tr, rowNdx) {
  300. if ((ref = tr.querySelector(':scope > td:nth-of-type(1) > a')) == null) {
  301. console.warn('Page parsing error');
  302. return;
  303. }
  304. elem = document.createElement('option');
  305. if ((elem.category = categories.findIndex(category => category.toLowerCase() == ref.textContent.toLowerCase())) < 0
  306. && /\b(?:cats)\[(\d+)\]/i.test(ref.search)) elem.category = parseInt(RegExp.$1); // unsafe due to site bug
  307. if ((ref = tr.querySelector(':scope > td:nth-of-type(2) > a')) == null || !/\b(?:id)=(\d+)\b/i.test(ref.search)) {
  308. console.warn(`Unknown collage id (${xhr.responseURL}/${rowNdx})`);
  309. return;
  310. }
  311. elem.value = elem.collageId = parseInt(RegExp.$1);
  312. elem.text = elem.title = ref.textContent.trim();
  313. if ((ref = tr.querySelector(':scope > td:nth-of-type(3)')) != null) elem.size = parseInt(ref.textContent);
  314. if ((ref = tr.querySelector(':scope > td:nth-of-type(4)')) != null) elem.subscribers = parseInt(ref.textContent);
  315. if ((ref = tr.querySelector(':scope > td:nth-of-type(6) > a')) != null
  316. && /\b(?:id)=(\d+)\b/i.test(ref.search)) elem.author = parseInt(RegExp.$1);
  317. if (elem.category < categories.length
  318. && (torrentGroupId && elem.category != artistsIndex || artistId && elem.category == artistsIndex)
  319. && (elem.category != 0 || elem.author == userId)) options.push(elem);
  320. });
  321. if (xhr.response.querySelector('div.linkbox > a.pager_next') != null) searchInternal(page + 1); else {
  322. if (!Object.keys(query).includes('order'))
  323. options.sort((a, b) => (b.size || 0) - (a.size || 0)/*a.title.localeCompare(b.title)*/);
  324. resolve(options);
  325. }
  326. };
  327. xhr.onerror = function() {
  328. if (xhr.status == 0 && retryCount++ <= 10) setTimeout(function() { searchInternal(page) }, 200);
  329. else reject(defaultErrorHandler(xhr));
  330. };
  331. xhr.ontimeout = function() { reject(defaultTimeoutHandler()) };
  332. xhr.send();
  333. }
  334. }).then(function(options) {
  335. clearList();
  336. categories.forEach(function(category, ndx) {
  337. let _category = options.filter(option => option.category == ndx);
  338. if (_category.length <= 0) return;
  339. elem = document.createElement('optgroup');
  340. elem.label = category;
  341. elem.append(..._category);
  342. dropDown.add(elem);
  343. });
  344. dropDown.selectedIndex = 0;
  345. searchFormEnable(true);
  346. return options;
  347. }).catch(function(reason) {
  348. clearList();
  349. searchFormEnable(true);
  350. console.warn(reason);
  351. }) : Promise.reject('Invalid parameter');
  352. }
  353.  
  354. function searchFormEnable(enabled) {
  355. for (let i = 0; i < srchForm.length; ++i) srchForm[i].disabled = !enabled;
  356. }
  357.  
  358. function searchSubmit(evt) {
  359. let searchTerm = searchText.value.trim();
  360. if (searchTerm.length <= 0) return false;
  361. let query = {
  362. action: 'search',
  363. search: searchTerm,
  364. type: 'c.name',
  365. order: 'Updated',
  366. sort: 'desc',
  367. order_way: 'Descending',
  368. };
  369. categories.map((category, ndx) => 'cats[' + ndx + ']')
  370. .filter((category, ndx) => torrentGroupId && ndx != artistsIndex || artistId && ndx == artistsIndex)
  371. .forEach(param => { query[param] = 1 });
  372. findCollages(query);
  373. return false;
  374. }
  375.  
  376. function addToCollage(evt) {
  377. (function() {
  378. evt.target.disabled = true;
  379. if (ajaxMessage != null) ajaxMessage.classList.add('hidden');
  380. let collageId = parseInt(dropDown.value);
  381. if (!collageId) return Promise.reject('No collage selected');
  382. /*
  383. if (Array.from(document.querySelectorAll('table.collage_table > tbody > tr:not([class="colhead"]) > td > a'))
  384. .map(node => /\b(?:id)=(\d+)\b/i.test(node.search) && parseInt(RegExp.$1)).includes(collageId))
  385. return Promise.reject(alreadyInCollage);
  386. */
  387. if (torrentGroupId) return addToTorrentCollage(collageId, torrentGroupId);
  388. if (artistId) return addToArtistCollage(collageId, artistId);
  389. return Promise.reject('munknown page class');
  390. })().then(function(collage) {
  391. if (ajaxMessage != null) {
  392. ajaxMessage.innerHTML = '<span style="color: #0A0;">' + addSuccess + '</span>';
  393. ajaxMessage.classList.remove('hidden');
  394. }
  395. evt.target.disabled = false;
  396. let mainColumn = document.querySelector('div.main_column');
  397. if (mainColumn == null) return collage;
  398. let tableName = collage.collageCategoryID != 0 ? 'collages' : 'personal_collages'
  399. let tbody = mainColumn.querySelector('table#' + tableName + ' > tbody');
  400. if (tbody == null) {
  401. tbody = document.createElement('tbody');
  402. tbody.innerHTML = '<tr class="colhead"><td width="85%"><a href="#">↑</a>&nbsp;</td><td># torrents</td></tr>';
  403. elem = document.createElement('table');
  404. elem.id = tableName;
  405. elem.className = 'collage_table';
  406. elem.append(tbody);
  407. mainColumn.insertBefore(elem, [
  408. 'table#personal_collages', 'table#vote_matches', 'div.torrent_description',
  409. 'div#similar_artist_map', 'div#artist_information',
  410. ].reduce((acc, selector) => acc || document.querySelector(selector), null));
  411. }
  412. tableName = '\xA0This ' + (collage.collageCategoryID != artistsIndex ? 'album' : 'artist') + ' is in ' +
  413. tbody.childElementCount + ' ' + (collage.collageCategoryID != 0 ? 'collage' : 'personal collage');
  414. if (tbody.childElementCount > 1) tableName += 's';
  415. tbody.firstElementChild.firstElementChild.childNodes[1].data = tableName;
  416. elem = document.createElement('tr');
  417. elem.className = 'collage_rows';
  418. if (tbody.querySelector('tr.collage_rows.hidden') != null) elem.classList.add('hidden');
  419. elem.innerHTML = '<td><a href="/collages.php?id=' + collage.id + '">' + collage.name + '</a></td><td class="number_column">' +
  420. (collage.collageCategoryID != artistsIndex ? collage.torrentgroups : collage.artists).length + '</td>';
  421. tbody.append(elem);
  422. return collage;
  423. }).catch(function(reason) {
  424. evt.target.disabled = false;
  425. if (ajaxMessage == null) return;
  426. ajaxMessage.innerHTML = '<span style="color: #A00;">' + reason.toString() + '</span>';
  427. ajaxMessage.classList.remove('hidden');
  428. });
  429. }
  430.  
  431. function openCollage(evt) {
  432. let collageId = parseInt(dropDown.value);
  433. if (collageId <= 0) return false;
  434. let win = window.open('/collages.php?id=' + collageId, '_blank');
  435. win.focus();
  436. }
  437.  
  438. function dataHandler(target, data) {
  439. var text = data.getData('text/plain');
  440. if (!text) return true;
  441. target.value = text;
  442. srchForm.onsubmit();
  443. return false;
  444. }
  445. });
  446. }
  447.  
  448. switch (document.location.pathname) {
  449. case '/torrents.php': {
  450. var torrentGroupId = new URLSearchParams(document.location.search).get('id'), collages;
  451. if (torrentGroupId) torrentGroupId = parseInt(torrentGroupId); else break; // Unexpected URL format
  452. const searchforcollage = document.getElementById('searchforcollage');
  453. if (searchforcollage != null) {
  454. if (typeof SearchCollage == 'function') SearchCollage = () => {
  455. const searchTerm = $('#searchforcollage').val(),
  456. personalCollages = $('#personalcollages');
  457. ajax.get(`ajax.php?action=collages&search=${encodeURIComponent(searchTerm)}`, responseText => {
  458. const { response, status } = JSON.parse(responseText);
  459. if (status !== 'success') return;
  460. const categories = response.reduce((accumulator, item) => {
  461. const { collageCategoryName } = item;
  462. accumulator[collageCategoryName] = (accumulator[collageCategoryName] || []).concat(item);
  463. return accumulator;
  464. }, {});
  465. personalCollages.children().remove();
  466. Object.entries(categories).forEach(([category, collages]) => {
  467. console.log(collages);
  468. personalCollages.append(`
  469. <optgroup label="${category}">
  470. ${collages.reduce((accumulator, { id, name }) =>
  471. `${accumulator}<option value="${id}">${name}</option>`
  472. ,'')}
  473. </optgroup>
  474. `);
  475. });
  476. });
  477. };
  478.  
  479. function inputHandler(evt, key) {
  480. const data = evt[key].getData('text/plain').trim();
  481. if (!data) return true;
  482. evt.target.value = data;
  483. SearchCollage();
  484. setTimeout(function() {
  485. const add_to_collage_select = document.querySelector('select.add_to_collage_select');
  486. if (add_to_collage_select != null && add_to_collage_select.options.length > 1) {
  487. // TODO: expand
  488. }
  489. }, 3000);
  490. return false;
  491. }
  492. searchforcollage.onpaste = evt => inputHandler(evt, 'clipboardData');
  493. searchforcollage.ondrop = evt => inputHandler(evt, 'dataTransfer');
  494. searchforcollage.onkeypress = evt => { if (evt.key == 'Enter') SearchCollage() };
  495. } else addQuickAddForm();
  496.  
  497. try { collages = JSON.parse(window.sessionStorage.collages) } catch(e) { collages = { } }
  498. if (!collages[document.domain]) collages[document.domain] = { };
  499.  
  500. document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) {
  501. if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return;
  502. let collageId = parseInt(RegExp.$1), toggle, navLinks = [],
  503. numberColumn = link.parentNode.parentNode.querySelector('td.number_column');
  504. link.onclick = function(evt) {
  505. return evt.button == 0 && evt.altKey ? removeFromTorrentCollage(collageId, torrentGroupId,
  506. 'Are you sure to remove this group from collage "' + link.textContent.trim() + '"?')
  507. .then(status => { link.parentNode.parentNode.remove() }) : true;
  508. };
  509. link.title = 'Use Alt + left click to remove from this collage';
  510. if (numberColumn != null) {
  511. numberColumn.style.cursor = 'pointer';
  512. numberColumn.onclick = loadCollage;
  513. numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing';
  514. }
  515. if (collages[document.domain][collageId]) {
  516. expandSection();
  517. addCollageLinks(collages[document.domain][collageId]);
  518. }
  519.  
  520. function addCollageLinks(collage) {
  521. var index = collage.torrentgroups.findIndex(group => group.id == torrentGroupId);
  522. if (index < 0) {
  523. console.warn('Assertion failed: torrent', torrentGroupId, 'not found in the collage', collage);
  524. return false;
  525. }
  526. link.style.color = 'white';
  527. link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;';
  528. var stats = document.createElement('span');
  529. stats.textContent = `${index + 1} / ${collage.torrentgroups.length}`;
  530. stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;';
  531. navLinks.push(stats);
  532. link.parentNode.append(stats);
  533. if (collage.torrentgroups[index - 1]) {
  534. var a = document.createElement('a');
  535. a.href = '/torrents.php?id=' + collage.torrentgroups[index - 1].id;
  536. a.textContent = '[\xA0<\xA0]';
  537. a.title = getTitle(index - 1);
  538. a.style = 'color: chartreuse; margin-right: 10px;';
  539. navLinks.push(a);
  540. link.parentNode.prepend(a);
  541. a = document.createElement('a');
  542. a.href = '/torrents.php?id=' + collage.torrentgroups[0].id;
  543. a.textContent = '[\xA0<<\xA0]';
  544. a.title = getTitle(0);
  545. a.style = 'color: chartreuse; margin-right: 5px;';
  546. navLinks.push(a);
  547. link.parentNode.prepend(a);
  548. }
  549. if (collage.torrentgroups[index + 1]) {
  550. a = document.createElement('a');
  551. a.href = '/torrents.php?id=' + collage.torrentgroups[index + 1].id;
  552. a.textContent = '[\xA0>\xA0]';
  553. a.title = getTitle(index + 1);
  554. a.style = 'color: chartreuse; margin-left: 10px;';
  555. navLinks.push(a);
  556. link.parentNode.append(a);
  557. a = document.createElement('a');
  558. a.href = '/torrents.php?id=' + collage.torrentgroups[collage.torrentgroups.length - 1].id;
  559. a.textContent = '[\xA0>>\xA0]';
  560. a.title = getTitle(collage.torrentgroups.length - 1);
  561. a.style = 'color: chartreuse; margin-left: 5px;';
  562. navLinks.push(a);
  563. link.parentNode.append(a);
  564. }
  565. return true;
  566.  
  567. function getTitle(index) {
  568. if (typeof index != 'number' || index < 0 || index >= collage.torrentgroups.length) return undefined;
  569. let title = collage.torrentgroups[index].musicInfo && Array.isArray(collage.torrentgroups[index].musicInfo.artists) ?
  570. collage.torrentgroups[index].musicInfo.artists.map(artist => artist.name).join(', ') + ' - ' : '';
  571. if (collage.torrentgroups[index].name) title += collage.torrentgroups[index].name;
  572. if (collage.torrentgroups[index].year) title += ' (' + collage.torrentgroups[index].year + ')';
  573. return title;
  574. }
  575. }
  576.  
  577. function expandSection() {
  578. if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]');
  579. if (toggle === null || toggle.dataset.expanded) return false;
  580. toggle.dataset.expanded = true;
  581. toggle.click();
  582. return true;
  583. }
  584.  
  585. function loadCollage(evt) {
  586. evt.target.disabled = true;
  587. navLinks.forEach(a => { a.remove() });
  588. navLinks = [];
  589. var span = document.createElement('span');
  590. span.textContent = '[\xA0loading...\xA0]';
  591. span.style = 'color: red; background-color: white; margin-left: 10px;';
  592. link.parentNode.append(span);
  593. queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
  594. span.remove();
  595. cacheCollage(collage);
  596. addCollageLinks(collage);
  597. evt.target.disabled = false;
  598. }, function(reason) {
  599. span.remove();
  600. evt.target.disabled = false;
  601. });
  602. return false;
  603. }
  604. });
  605.  
  606. function cacheCollage(collage) {
  607. collages[document.domain][collage.id] = {
  608. id: collage.id,
  609. name: collage.name,
  610. torrentgroups: collage.torrentgroups.map(group => ({
  611. id: group.id,
  612. musicInfo: group.musicInfo ? {
  613. artists: Array.isArray(group.musicInfo.artists) ?
  614. group.musicInfo.artists.map(artist => ({ name: artist.name })) : undefined,
  615. } : undefined,
  616. name: group.name,
  617. year: group.year,
  618. })),
  619. };
  620. window.sessionStorage.collages = JSON.stringify(collages);
  621. }
  622.  
  623. break;
  624. }
  625. case '/artist.php': {
  626. var artistId = new URLSearchParams(document.location.search).get('id');
  627. if (artistId) artistId = parseInt(artistId); else break; // Unexpected URL format
  628. addQuickAddForm();
  629. break;
  630. }
  631. case '/collages.php': {
  632. var collageId = new URLSearchParams(document.location.search).get('id');
  633. if (collageId) collageId = parseInt(collageId); else break; // Collage id missing
  634. let category = document.querySelector('div.box_category > div.pad > a');
  635. category = category != null ? category.textContent : undefined;
  636.  
  637. function scanPage() {
  638. if (category != 'Artists') {
  639. var sel = [
  640. 'tr.group > td[colspan] > strong > a[href^="torrents.php?id="]',
  641. 'ul.collage_images > li > a[href^="torrents.php?id="]',
  642. ]
  643. document.querySelectorAll(sel.join(', ')).forEach(function(a) {
  644. a.onclick = function(evt) {
  645. if (evt.button != 0 || !evt.altKey) return true;
  646. let torrentGroupId = new URLSearchParams(a.search);
  647. torrentGroupId = torrentGroupId.get('id');
  648. if (torrentGroupId) torrentGroupId = parseInt(torrentGroupId); else {
  649. console.warn('Assertion failed: no id', a);
  650. throw 'no id';
  651. }
  652. removeFromTorrentCollage(collageId, torrentGroupId, 'Are you sure to remove selected group from this collage?').then(function(status) {
  653. document.querySelectorAll(sel.join(', ')).forEach(function(a) {
  654. if (parseInt(new URLSearchParams(a.search).get('id')) == torrentGroupId) switch (a.parentNode.nodeName) {
  655. case 'STRONG': a.parentNode.parentNode.parentNode.remove(); break;
  656. case 'LI': a.parentNode.remove(); break;
  657. }
  658. });
  659. });
  660. };
  661. });
  662. } else {
  663. sel = [
  664. 'table#discog_table > tbody > tr > td > a[href^="artist.php?id="]',
  665. 'ul.collage_images > li > a[href^="artist.php?id="]',
  666. ];
  667. document.querySelectorAll(sel.join(', ')).forEach(function(a) {
  668. a.onclick = function(evt) {
  669. if (evt.button != 0 || !evt.altKey) return true;
  670. let artistId = new URLSearchParams(a.search);
  671. artistId = artistId.get('id');
  672. if (artistId) artistId = parseInt(artistId); else {
  673. console.warn('Assertion failed: no id', a);
  674. throw 'no id';
  675. }
  676. removeFromArtistCollage(collageId, artistId, 'Are you sure to remove selected artist from this collage?').then(function(status) {
  677. document.querySelectorAll(sel.join(', ')).forEach(function(a) {
  678. if (parseInt(new URLSearchParams(a.search).get('id')) == artistId) switch (a.parentNode.nodeName) {
  679. case 'TD': a.parentNode.parentNode.remove(); break;
  680. case 'LI': a.parentNode.remove(); break;
  681. }
  682. });
  683. });
  684. };
  685. });
  686. }
  687. }
  688.  
  689. scanPage();
  690. document.querySelectorAll('div#pageslinksdiv > span > a.pageslink')
  691. .forEach(a => { a.addEventListener('click', evt => { setTimeout(scanPage, 1000) }) });
  692. break;
  693. }
  694. }