Torrent Collage Extensions for Gazelle

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

目前为 2020-12-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         Torrent Collage Extensions for Gazelle
// @version      1.12
// @description  Click on collage size = browse through this collage; Alt + click on collage name = remove from this collage
// @author       Anakunda
// @license      GPL-3.0-or-later
// @copyright    2020, Anakunda (https://openuserjs.org/users/Anakunda)
// @namespace    https://greasyfork.org/users/321857-anakunda
// @match        https://*/torrents.php?id=*
// @match        https://*/collages.php?*id=*
// @grant        GM_getValue
// ==/UserScript==

'use strict';

var auth = document.querySelector('input[name="auth"][value]');
if (auth != null) auth = auth.value; else {
	auth = document.querySelector('li#nav_logout > a');
	if (auth != null && /\b(?:auth)=(\w+)\b/.test(auth.search)) auth = RegExp.$1; else throw 'Auth not found';
}

const siteApiTimeframeStorageKey = document.location.hostname + ' API time frame';
const gazelleApiFrame = 10500;
if (typeof GM_getValue == 'function') var redacted_api_key = GM_getValue('redacted_api_key');

function queryAjaxAPI(action, params) {
	if (!action) return Promise.reject('Action missing');
	var retryCount = 0;
	return new Promise(function(resolve, reject) {
		params = new URLSearchParams(params || undefined);
		params.set('action', action);
		let url = '/ajax.php?' + params, xhr = new XMLHttpRequest;
		queryInternal();

		function queryInternal() {
			var now = Date.now();
			try { var apiTimeFrame = JSON.parse(window.localStorage[siteApiTimeframeStorageKey]) } catch(e) { apiTimeFrame = {} }
			if (!apiTimeFrame.timeStamp || now > apiTimeFrame.timeStamp + gazelleApiFrame) {
				apiTimeFrame.timeStamp = now;
				apiTimeFrame.requestCounter = 1;
			} else ++apiTimeFrame.requestCounter;
			window.localStorage[siteApiTimeframeStorageKey] = JSON.stringify(apiTimeFrame);
			if (apiTimeFrame.requestCounter <= 5) {
				xhr.open('GET', url, true);
				xhr.setRequestHeader('Accept', 'application/json');
				if (redacted_api_key) xhr.setRequestHeader('Authorization', redacted_api_key);
				xhr.responseType = 'json';
				//xhr.timeout = 5 * 60 * 1000;
				xhr.onload = function() {
					if (xhr.status == 404) return reject('not found');
					if (xhr.status < 200 || xhr.status >= 400) return reject(defaultErrorHandler(xhr));
					if (xhr.response.status == 'success') return resolve(xhr.response.response);
					if (xhr.response.error == 'not found') return reject(xhr.response.error);
					console.warn('queryAjaxAPI.queryInternal(...) response:', xhr, xhr.response);
					if (xhr.response.error == 'rate limit exceeded') {
						console.warn('queryAjaxAPI.queryInternal(...) ' + xhr.response.error + ':', apiTimeFrame, now, retryCount);
						if (retryCount++ <= 10) return setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
					}
					reject('API ' + xhr.response.status + ': ' + xhr.response.error);
				};
				xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
				xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
				xhr.send();
			} else {
				setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
				console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
					action + ' (' + apiTimeFrame.requestCounter + ')');
			}
		}
	});
}

function addToTorrentCollage(collageId, groupId) {
	return (function() {
		if (!collageId) return Promise.reject('collage not defined');
		if (!groupId) return Promise.reject('group not defined');
		return queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
			return !collage.torrentGroupIDList.map(parseInt).includes(groupId) ?
				collageId : Promise.reject('already in collage');
		});
	})().then(collageId => new Promise(function(resolve, reject) {
		let form = new URLSearchParams({
			auth: auth,
			collageid: collageId,
		});
		if (groupId) {
			form.set('action', 'add_torrent');
			form.set('url', document.location.origin.concat('/torrents.php?id=', groupId));
			form.set('groupid', groupId);
		}
		let xhr = new XMLHttpRequest();
		xhr.open('POST', '/collages.php', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		xhr.onreadystatechange = function() {
			if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
			if (xhr.status >= 200 && xhr.status < 400) resolve(collageId); else reject(defaultErrorHandler(xhr));
			xhr.abort();
		};
		xhr.timeout = timeout;
		xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		xhr.send(form.toString());
	})).then(collageId => queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
		return collage.torrentGroupIDList.map(parseInt).includes(groupId) ?
			collage : Promise.reject('Error: not added for unknown reason');
	}));
}

function removeFromTorrentCollage(collageId, groupId, question) {
	if (!confirm(question)) return Promise.reject('Cancelled');
	return new Promise(function(resolve, reject) {
		let xhr = new XMLHttpRequest, params = new URLSearchParams({
			action: 'manage_handle',
			collageid: collageId,
			groupid: groupId,
			auth: auth,
			submit: 'Remove',
		});
		xhr.open('POST', '/collages.php', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		xhr.onreadystatechange = function() {
			if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
			if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
			xhr.abort();
		};
		xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		xhr.send(params);
	});
}

function addToArtistCollage(collageId, artistId) {
	return (function() {
		if (!collageId) return Promise.reject('collage not defined');
		if (!artistId) return Promise.reject('artist not defined');
		return queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
			return !collage.artists.map(parseInt).includes(artistId) ? collageId : Promise.reject('already in collage');
		});
	})().then(collageId => new Promise(function(resolve, reject) {
		let form = new URLSearchParams({
			auth: auth,
			collageid: collageId,
		});
		if (artistId) {
			form.set('action', 'add_artist');
			form.set('url', document.location.origin.concat('/artist.php?id=', artistId));
			form.set('artistid', artistId);
		}
		let xhr = new XMLHttpRequest();
		xhr.open('POST', '/collages.php', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		xhr.onreadystatechange = function() {
			if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
			if (xhr.status >= 200 && xhr.status < 400) resolve(collageId); else reject(defaultErrorHandler(xhr));
			xhr.abort();
		};
		xhr.timeout = timeout;
		xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		xhr.send(form.toString());
	})).then(collageId => queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
		return collage.artists.map(parseInt).includes(artistId) ?
			collage : Promise.reject('Error: not added for unknown reason');
	}));
}

function removeFromArtistCollage(collageId, artistId, question) {
	if (!confirm(question)) return Promise.reject('Cancelled');
	return new Promise(function(resolve, reject) {
		let xhr = new XMLHttpRequest, params = new URLSearchParams({
			action: 'manage_artists_handle',
			collageid: collageId,
			artistid: artistId,
			auth: auth,
			submit: 'Remove',
		});
		xhr.open('POST', '/collages.php', true);
		xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		xhr.onreadystatechange = function() {
			if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
			if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
			xhr.abort();
		};
		xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		xhr.send(params);
	});
}

function defaultErrorHandler(response) {
	console.error('HTTP error:', response);
	var e = 'HTTP error ' + response.status;
	if (response.statusText) e += ' (' + response.statusText + ')';
	if (response.error) e += ' (' + response.error + ')';
	return e;
}

function defaultTimeoutHandler(response) {
	console.error('HTTP timeout:', response);
	const e = 'HTTP timeout';
	return e;
}

switch (document.location.pathname) {
	case '/torrents.php': {
		const searchforcollage = document.getElementById('searchforcollage');
		if (searchforcollage != null) {
			if (typeof SearchCollage == 'function') SearchCollage = () => {
				const searchTerm = $('#searchforcollage').val(),
							personalCollages = $('#personalcollages');
				ajax.get(`ajax.php?action=collages&search=${encodeURIComponent(searchTerm)}`, responseText => {
					const { response, status } = JSON.parse(responseText);
					if (status !== 'success') return;
					const categories = response.reduce((accumulator, item) => {
						const { collageCategoryName } = item;
						accumulator[collageCategoryName] = (accumulator[collageCategoryName] || []).concat(item);
						return accumulator;
					}, {});
					personalCollages.children().remove();
					Object.entries(categories).forEach(([category, collages]) => {
						console.log(collages);
						personalCollages.append(`
<optgroup label="${category}">
${collages.reduce((accumulator, { id, name }) =>
	`${accumulator}<option value="${id}">${name}</option>`
	,'')}
</optgroup>
		`);
					});
				});
			};

			function inputHandler(evt, key) {
				const data = evt[key].getData('text/plain').trim();
				if (!data) return true;
				evt.target.value = data;
				SearchCollage();
				setTimeout(function() {
					const add_to_collage_select = document.querySelector('select.add_to_collage_select');
					if (add_to_collage_select != null && add_to_collage_select.options.length > 1) {
						// TODO: expand
					}
				}, 3000);
				return false;
			}
			searchforcollage.onpaste = evt => inputHandler(evt, 'clipboardData');
			searchforcollage.ondrop = evt => inputHandler(evt, 'dataTransfer');
		}

		let torrentId = new URLSearchParams(document.location.search).get('id'), collages;
		if (torrentId) torrentId = parseInt(torrentId); else throw 'Unexpected URL format';
		try { collages = JSON.parse(window.sessionStorage.collages) } catch(e) { collages = { } }
		if (!collages[document.domain]) collages[document.domain] = {};

		document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) {
			if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return;
			let collageId = parseInt(RegExp.$1), toggle, navLinks = [],
					numberColumn = link.parentNode.parentNode.querySelector('td.number_column');
			link.onclick = function(evt) {
				return evt.button == 0 && evt.altKey ? removeFromTorrentCollage(collageId, torrentId,
						'Are you sure to remove this group from collage "' + link.textContent.trim() + '"?')
					.then(status => { link.parentNode.parentNode.remove() }) : true;
			};
			link.title = 'Use Alt + left click to remove from this collage';
			if (numberColumn != null) {
				numberColumn.style.cursor = 'pointer';
				numberColumn.onclick = loadCollage;
				numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing';
			}
			if (collages[document.domain][collageId]) {
				expandSection();
				addCollageLinks(collages[document.domain][collageId]);
			}

			function addCollageLinks(collage) {
				var index = collage.torrentgroups.findIndex(group => group.id == torrentId);
				if (index < 0) {
					console.warn('Assertion failed: torrent', torrentId, 'not found in the collage', collage);
					return false;
				}
				link.style.color = 'white';
				link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;';
				var stats = document.createElement('span');
				stats.textContent = `${index + 1} / ${collage.torrentgroups.length}`;
				stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;';
				navLinks.push(stats);
				link.parentNode.append(stats);
				if (collage.torrentgroups[index - 1]) {
					var a = document.createElement('a');
					a.href = '/torrents.php?id=' + collage.torrentgroups[index - 1].id;
					a.textContent = '[\xA0<\xA0]';
					a.title = getTitle(index - 1);
					a.style = 'color: chartreuse; margin-right: 10px;';
					navLinks.push(a);
					link.parentNode.prepend(a);
					a = document.createElement('a');
					a.href = '/torrents.php?id=' + collage.torrentgroups[0].id;
					a.textContent = '[\xA0<<\xA0]';
					a.title = getTitle(0);
					a.style = 'color: chartreuse; margin-right: 5px;';
					navLinks.push(a);
					link.parentNode.prepend(a);
				}
				if (collage.torrentgroups[index + 1]) {
					a = document.createElement('a');
					a.href = '/torrents.php?id=' + collage.torrentgroups[index + 1].id;
					a.textContent = '[\xA0>\xA0]';
					a.title = getTitle(index + 1);
					a.style = 'color: chartreuse; margin-left: 10px;';
					navLinks.push(a);
					link.parentNode.append(a);
					a = document.createElement('a');
					a.href = '/torrents.php?id=' + collage.torrentgroups[collage.torrentgroups.length - 1].id;
					a.textContent = '[\xA0>>\xA0]';
					a.title = getTitle(collage.torrentgroups.length - 1);
					a.style = 'color: chartreuse; margin-left: 5px;';
					navLinks.push(a);
					link.parentNode.append(a);
				}
				return true;

				function getTitle(index) {
					if (typeof index != 'number' || index < 0 || index >= collage.torrentgroups.length) return undefined;
					let title = collage.torrentgroups[index].musicInfo && Array.isArray(collage.torrentgroups[index].musicInfo.artists) ?
						collage.torrentgroups[index].musicInfo.artists.map(artist => artist.name).join(', ') + ' - ' : '';
					if (collage.torrentgroups[index].name) title += collage.torrentgroups[index].name;
					if (collage.torrentgroups[index].year) title += ' (' + collage.torrentgroups[index].year + ')';
					return title;
				}
			}

			function expandSection() {
				if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]');
				if (toggle === null || toggle.dataset.expanded) return false;
				toggle.dataset.expanded = true;
				toggle.click();
				return true;
			}

			function loadCollage(evt) {
				evt.target.disabled = true;
				navLinks.forEach(a => { a.remove() });
				navLinks = [];
				var span = document.createElement('span');
				span.textContent = '[\xA0loading...\xA0]';
				span.style = 'color: red; background-color: white; margin-left: 10px;';
				link.parentNode.append(span);
				queryAjaxAPI('collage', { id: collageId }).then(function(collage) {
					span.remove();
					cacheCollage(collage);
					addCollageLinks(collage);
					evt.target.disabled = false;
				}, function(reason) {
					span.remove();
					evt.target.disabled = false;
				});
				return false;
			}
		});

		function cacheCollage(collage) {
			collages[document.domain][collage.id] = {
				id: collage.id,
				name: collage.name,
				torrentgroups: collage.torrentgroups.map(group => ({
					id: group.id,
					musicInfo: group.musicInfo ? {
						artists: Array.isArray(group.musicInfo.artists) ?
						group.musicInfo.artists.map(artist => ({ name: artist.name })) : undefined,
					} : undefined,
					name: group.name,
					year: group.year,
				})),
			};
			window.sessionStorage.collages = JSON.stringify(collages);
		}
		break;
	}
	case '/collages.php': {
		let collageId = /\b(?:id)=(\d+)\b/i.exec(document.location.search);
		if (collageId != null) collageId = parseInt(collageId[1]); else throw 'Collage id missing';
		const selTorrents = [
			'tr.group > td[colspan] > strong > a[href^="torrents.php?id="]',
			'ul.collage_images > li > a[href^="torrents.php?id="]',
		], selArtists = [
			'table#discog_table > tbody > tr > td > a[href^="artist.php?id="]',
			'ul.collage_images > li > a[href^="artist.php?id="]',
		];
		function scanPage() {
			document.querySelectorAll(selTorrents.join(', ')).forEach(function(a) {
				a.onclick = function(evt) {
					if (evt.button != 0 || !evt.altKey) return true;
					let torrentId = new URLSearchParams(a.search);
					torrentId = torrentId.get('id');
					if (torrentId) torrentId = parseInt(torrentId); else {
						console.warn('Assertion failed: no id', a);
						throw 'no id';
					}
					removeFromTorrentCollage(collageId, torrentId, 'Are you sure to remove selected group from this collage?').then(function(status) {
						document.querySelectorAll(selTorrents.join(', ')).forEach(function(a) {
							if (parseInt(new URLSearchParams(a.search).get('id')) == torrentId) switch (a.parentNode.nodeName) {
								case 'STRONG': a.parentNode.parentNode.parentNode.remove(); break;
								case 'LI': a.parentNode.remove(); break;
							}
						});
					});
				};
			});
			document.querySelectorAll(selArtists.join(', ')).forEach(function(a) {
				a.onclick = function(evt) {
					if (evt.button != 0 || !evt.altKey) return true;
					let artistId = new URLSearchParams(a.search);
					artistId = artistId.get('id');
					if (artistId) artistId = parseInt(artistId); else {
						console.warn('Assertion failed: no id', a);
						throw 'no id';
					}
					removeFromArtistCollage(collageId, artistId, 'Are you sure to remove selected artist from this collage?').then(function(status) {
						document.querySelectorAll(selArtists.join(', ')).forEach(function(a) {
							if (parseInt(new URLSearchParams(a.search).get('id')) == artistId) switch (a.parentNode.nodeName) {
								case 'TD': a.parentNode.parentNode.remove(); break;
								case 'LI': a.parentNode.remove(); break;
							}
						});
					});
				};
			});
		}

		scanPage();
		document.querySelectorAll('div#pageslinksdiv > span > a.pageslink')
			.forEach(a => { a.addEventListener('click', evt => { setTimeout(scanPage, 1000) }) });
		break;
	}
}