Collage Extensions for Gazelle Music Trackers

Direct browsing from torrent pages; quick groups removal, custom quick Add To Collage form

目前為 2021-02-05 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Collage Extensions for Gazelle Music Trackers
// @version      1.23.0
// @description  Direct browsing from torrent pages; quick groups removal, custom quick Add To Collage form
// @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=*
// @match        https://*/artist.php?*id=*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/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';
}
let userId = document.querySelector('li#nav_userinfo > a.username');
if (userId != null) {
	userId = new URLSearchParams(userId.search);
	userId = parseInt(userId.get('id'));
}

const siteApiTimeframeStorageKey = 'AJAX time frame', gazelleApiFrame = 10500;
switch (document.domain) {
	case 'redacted.ch': var apiKey = GM_getValue('redacted_api_key'); break;
}

function queryAjaxAPI(action, params, postData) {
	if (!action) return Promise.reject('Action missing');
	let 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;
		if (postData) {
			switch (typeof postData) {
				case 'object': if (!(postData instanceof URLSearchParams)) postData = new URLSearchParams(postData); break;
				case 'string': try { postData = new URLSearchParams(JSON.parse(postData)) } catch(e) { };  break;
			}
		}
		postData = postData instanceof URLSearchParams ? postData.toString() : undefined;
		queryInternal();

		function queryInternal() {
			let 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(postData ? 'POST' : 'GET', url, true);
				xhr.setRequestHeader('Accept', 'application/json');
				if (postData) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
				if (apiKey) xhr.setRequestHeader('Authorization', apiKey);
				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(postData);
			} else {
				setTimeout(queryInternal, apiTimeFrame.timeStamp + gazelleApiFrame - now);
				console.debug('AJAX API request quota exceeded: /ajax.php?action=' +
					action + ' (' + apiTimeFrame.requestCounter + ')');
			}
		}
	});
}

function addToTorrentCollage(collageId, torrentGroupId) {
	if (!collageId) return Promise.reject('collage id not defined');
	if (!torrentGroupId) return Promise.reject('torrent group id not defined');
	return (apiKey ? queryAjaxAPI('addtocollage', { id: collageId }, { groupids: torrentGroupId }).then(function(response) {
		if (!response.groupsadded.includes(torrentGroupId)) return Promise.reject('Error: ' + JSON.stringify(response));
	}) : queryAjaxAPI('collage', { id: collageId }).then(
		collage => !collage.torrentGroupIDList.map(groupId => parseInt(groupId)).includes(torrentGroupId) ? collageId
			: Promise.reject('already in collage')
	).then(collageId => new Promise(function(resolve, reject) {
		let xhr = new XMLHttpRequest, formData = new URLSearchParams({
			action: 'add_torrent',
			collageid: collageId,
			groupid: torrentGroupId,
			url: document.location.origin.concat('/torrents.php?id=', torrentGroupId),
			auth: auth,
		});
		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.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		xhr.send(formData);
	}))).then(collageId => queryAjaxAPI('collage', { id: collageId }).then(
		collage => collage.torrentGroupIDList.map(groupId => parseInt(groupId)).includes(torrentGroupId) ? collage
			: Promise.reject('Error: not added for unknown reason')
	));
}

function removeFromTorrentCollage(collageId, torrentGroupId, question) {
	if (!confirm(question)) return Promise.reject('Cancelled');
	return new Promise(function(resolve, reject) {
		let xhr = new XMLHttpRequest, formData = new URLSearchParams({
			action: 'manage_handle',
			collageid: collageId,
			groupid: torrentGroupId,
			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(formData);
	});
}

function addToArtistCollage(collageId, artistId) {
	if (!collageId) return Promise.reject('collage id not defined');
	if (!artistId) return Promise.reject('artist id not defined');
	return (apiKey ? queryAjaxAPI('addtocollage', { id: collageId }, { groupids: artistId }).then(function(response) {
		if (!response.groupsadded.includes(artistId)) return Promise.reject('Error: ' + JSON.stringify(response));
	}) : queryAjaxAPI('collage', { id: collageId }).then(
		collage => !collage.artists.map(artist => parseInt(artist.id)).includes(artistId) ? collageId
			: Promise.reject('already in collage')
	).then(collageId => new Promise(function(resolve, reject) {
		let xhr = new XMLHttpRequest, formData = new URLSearchParams({
			action: 'add_artist',
			collageid: collageId,
			artistid: artistId,
			url: document.location.origin.concat('/artist.php?id=', artistId),
			auth: auth,
		});
		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.onerror = function() { reject(defaultErrorHandler(xhr)) };
		xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
		xhr.send(formData);
	}))).then(collageId => queryAjaxAPI('collage', { id: collageId }).then(
		collage => collage.artists.map(artist => parseInt(artist.id)).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, formData = 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(formData);
	});
}

function defaultErrorHandler(response) {
	console.error('HTTP error:', response);
	let 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;
}

function addQuickAddForm() {
	if (!userId || !torrentGroupId && !artistId) return; // User id missing
	let ref = document.querySelector('div.sidebar');
	if (ref == null) return; // Sidebar missing
	const addSuccess = 'Successfully added to collage.';
	const alreadyInCollage = 'Error: This ' +
		(torrentGroupId ? 'torrent group' : artistId ? 'artist' : null) + ' is already in this collage';
	new Promise(function(resolve, reject) {
		try {
			var categories = JSON.parse(GM_getValue(document.location.hostname + '-categories'));
			if (categories.length > 0) resolve(categories); else throw 'empty list cached';
		} catch(e) {
			let xhr = new XMLHttpRequest;
			xhr.open('GET', '/collages.php', true);
			xhr.responseType = 'document';
			xhr.onload = function() {
				if (xhr.status >= 200 && xhr.status < 400) {
					categories = [ ];
					xhr.response.querySelectorAll('tr#categories > td > label').forEach(function(label, index) {
						let input = xhr.response.querySelector('tr#categories > td > input#' + label.htmlFor);
						categories[input != null && /\[(\d+)\]/.test(input.name) ? parseInt(RegExp.$1) : index] = label.textContent.trim();
					});
					if (categories.length > 0) {
						GM_setValue(document.location.hostname + '-categories', JSON.stringify(categories));
						resolve(categories);
					} else reject('Site categories could not be extracted');
				} else reject(defaultErrorHandler(xhr));
			};
			xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
			xhr.ontimeout = function() { reject(defaultTimeoutHandler()) };
			xhr.send();
		}
	}).then(function(categories) {
		const artistsIndexes = categories
			.map((category, index) => /^(?:Artists)$/i.test(category) ? index : -1)
			.filter(index => index >= 0);
		if (artistId && artistsIndexes.length <= 0) throw 'Artists index not found';
		const isCompatibleCategory = categoryId => categoryId >= 0 && categoryId < categories.length
			&& (torrentGroupId && !artistsIndexes.includes(categoryId) || artistId && artistsIndexes.includes(categoryId));
		document.head.appendChild(document.createElement('style')).innerHTML = `
form#addtocollage optgroup { background-color: slategray; color: white; }
form#addtocollage option { background-color: white; color: black; max-width: 290pt; }
div.box_addtocollage > form { padding: 0px 10px; }
`;
		let elem = document.createElement('div');
		elem.className = 'box box_addtocollage';
		elem.style = 'padding: 0 0 10px;';
		elem.innerHTML = `
<div class="head" style="margin-bottom: 5px;"><strong>Add to Collage</strong></div>
<div id="ajax_message" class="hidden center" style="padding: 7px 0px;"></div>
<form id="searchcollages">
	<input id="searchforcollage" placeholder="Collage search" type="text" style="max-width: 10em;">
	<input id="searchforcollagebutton" value="Search" type="submit" style="max-width: 4em;">
</form>
<form id="addtocollage" class="add_form" name="addtocollage">
	<select name="collageid" id="matchedcollages" class="add_to_collage_select" style="width: 96%;">
	<input id="opencollage-btn" value="Open collage" type="button">
	<input id="addtocollage-btn" value="Add to collage" type="button">
</form>
`;
		ref.append(elem);
		let ajaxMessage = document.getElementById('ajax_message');
		let srchForm = document.getElementById('searchcollages');
		if (srchForm == null) throw new Error('#searchcollages missing');
		let searchText = document.getElementById('searchforcollage');
		if (searchText == null) throw new Error('#searchforcollage missing');
		let dropDown = document.getElementById('matchedcollages');
		if (dropDown == null) throw new Error('#matchedcollages missing');
		let doOpen = document.getElementById('opencollage-btn');
		let doAdd = document.getElementById('addtocollage-btn');
		if (doAdd == null) throw new Error('#addtocollage-btn missing');
		srchForm.onsubmit = searchSubmit;
		searchText.ondrop = evt => dataHandler(evt.currentTarget, evt.dataTransfer);
		searchText.onpaste = evt => dataHandler(evt.currentTarget, evt.clipboardData);
		if (doOpen != null) doOpen.onclick = openCollage;
		doAdd.onclick = addToCollage;
		let initTimeCap = GM_getValue('max_preload_time', 0); // max time in ms to preload the dropdown
		if (initTimeCap > 0) findCollages({ userid: userId, contrib: 1 }, initTimeCap);

		function clearList() {
			while (dropDown.childElementCount > 0) dropDown.removeChild(dropDown.firstElementChild);
		}

		function findCollages(query, maxSearchTime) {
			return typeof query == 'object' ? new Promise(function(resolve, reject) {
				let start = Date.now();
				searchFormEnable(false);
				clearList();
				elem = document.createElement('option');
				elem.text = 'Searching...';
				dropDown.add(elem);
				dropDown.selectedIndex = 0;
				let retryCount = 0, options = [ ];
				searchInternal();

				function searchInternal(page) {
					if (maxSearchTime > 0 && Date.now() - start > maxSearchTime) {
						reject('Time limit exceeded');
						return;
					}
					let xhr = new XMLHttpRequest, _query = new URLSearchParams(query);
					if (!page) page = 1;
					_query.set('page', page);
					xhr.open('GET', '/collages.php?' + _query, true);
					xhr.responseType = 'document';
					xhr.onload = function() {
						if (xhr.status < 200 || xhr.status >= 400) throw defaultErrorHandler(xhr);
						xhr.response.querySelectorAll('table.collage_table > tbody > tr[class^="row"]').forEach(function(tr, rowNdx) {
							if ((ref = tr.querySelector(':scope > td:nth-of-type(1) > a')) == null) {
								console.warn('Page parsing error');
								return;
							}
							elem = document.createElement('option');
							if ((elem.category = categories.findIndex(category => category.toLowerCase() == ref.textContent.toLowerCase())) < 0
									&& /\b(?:cats)\[(\d+)\]/i.test(ref.search)) elem.category = parseInt(RegExp.$1); // unsafe due to site bug
							if ((ref = tr.querySelector(':scope > td:nth-of-type(2) > a')) == null || !/\b(?:id)=(\d+)\b/i.test(ref.search)) {
								console.warn(`Unknown collage id (${xhr.responseURL}/${rowNdx})`);
								return;
							}
							elem.value = elem.collageId = parseInt(RegExp.$1);
							elem.text = elem.title = ref.textContent.trim();
							if ((ref = tr.querySelector(':scope > td:nth-of-type(3)')) != null) elem.size = parseInt(ref.textContent);
							if ((ref = tr.querySelector(':scope > td:nth-of-type(4)')) != null) elem.subscribers = parseInt(ref.textContent);
							if ((ref = tr.querySelector(':scope > td:nth-of-type(6) > a')) != null
									&& /\b(?:id)=(\d+)\b/i.test(ref.search)) elem.author = parseInt(RegExp.$1);
							if (isCompatibleCategory(elem.category) && (elem.category != 0 || elem.author == userId)) options.push(elem);
						});
						if (xhr.response.querySelector('div.linkbox > a.pager_next') != null) searchInternal(page + 1); else {
							if (!Object.keys(query).includes('order'))
								options.sort((a, b) => (b.size || 0) - (a.size || 0)/*a.title.localeCompare(b.title)*/);
							resolve(options);
						}
					};
					xhr.onerror = function() {
						if (xhr.status == 0 && retryCount++ <= 10) setTimeout(function() { searchInternal(page) }, 200);
						else reject(defaultErrorHandler(xhr));
					};
					xhr.ontimeout = function() { reject(defaultTimeoutHandler()) };
					xhr.send();
				}
			}).then(function(options) {
				clearList();
				categories.forEach(function(category, ndx) {
					let _category = options.filter(option => option.category == ndx);
					if (_category.length <= 0) return;
					elem = document.createElement('optgroup');
					elem.label = category;
					elem.append(..._category);
					dropDown.add(elem);
				});
				dropDown.selectedIndex = 0;
				searchFormEnable(true);
				return options;
			}).catch(function(reason) {
				clearList();
				searchFormEnable(true);
				console.warn(reason);
			}) : Promise.reject('Invalid parameter');
		}

		function searchFormEnable(enabled) {
			for (let i = 0; i < srchForm.length; ++i) srchForm[i].disabled = !enabled;
		}

		function searchSubmit(evt) {
			let searchTerm = searchText.value.trim();
			if (searchTerm.length <= 0) return false;
			let query = {
				action: 'search',
				search: searchTerm,
				type: 'c.name',
				order: 'Updated',
				sort: 'desc',
				order_way: 'Descending',
			};
			categories.map((category, index) => 'cats[' + index + ']')
				.filter((category, index) => isCompatibleCategory(index))
				.forEach(index => { query[index] = 1 });
			findCollages(query);
			return false;
		}

		function addToCollage(evt) {
			(function() {
				evt.currentTarget.disabled = true;
				if (ajaxMessage != null) ajaxMessage.classList.add('hidden');
				let collageId = parseInt(dropDown.value);
				if (!collageId) return Promise.reject('No collage selected');
/*
				if (Array.from(document.querySelectorAll('table.collage_table > tbody > tr:not([class="colhead"]) > td > a'))
						.map(node => /\b(?:id)=(\d+)\b/i.test(node.search) && parseInt(RegExp.$1)).includes(collageId))
					return Promise.reject(alreadyInCollage);
*/
				if (torrentGroupId) return addToTorrentCollage(collageId, torrentGroupId);
				if (artistId) return addToArtistCollage(collageId, artistId);
				return Promise.reject('munknown page class');
			})().then(function(collage) {
				if (ajaxMessage != null) {
					ajaxMessage.innerHTML = '<span style="color: #0A0;">' + addSuccess + '</span>';
					ajaxMessage.classList.remove('hidden');
				}
				evt.currentTarget.disabled = false;
				let mainColumn = document.querySelector('div.main_column');
				if (mainColumn == null) return collage;
				let tableName = collage.collageCategoryID != 0 ? 'collages' : 'personal_collages'
				let tbody = mainColumn.querySelector('table#' + tableName + ' > tbody');
				if (tbody == null) {
					tbody = document.createElement('tbody');
					tbody.innerHTML = '<tr class="colhead"><td width="85%"><a href="#">↑</a>&nbsp;</td><td># torrents</td></tr>';
					elem = document.createElement('table');
					elem.id = tableName;
					elem.className = 'collage_table';
					elem.append(tbody);
					mainColumn.insertBefore(elem, [
						'table#personal_collages', 'table#vote_matches', 'div.torrent_description',
						'div#similar_artist_map', 'div#artist_information',
					].reduce((acc, selector) => acc || document.querySelector(selector), null));
				}
				tableName = '\xA0This ' + (artistsIndexes.includes(collage.collageCategoryID) ? 'artist' : 'album') + ' is in ' +
					tbody.childElementCount + ' ' + (collage.collageCategoryID != 0 ? 'collage' : 'personal collage');
				if (tbody.childElementCount > 1) tableName += 's';
				tbody.firstElementChild.firstElementChild.childNodes[1].data = tableName;
				elem = document.createElement('tr');
				elem.className = 'collage_rows';
				if (tbody.querySelector('tr.collage_rows.hidden') != null) elem.classList.add('hidden');
				elem.innerHTML = '<td><a href="/collages.php?id=' + collage.id + '">' + collage.name + '</a></td><td class="number_column">' +
					collage[artistsIndexes.includes(collage.collageCategoryID) ? 'artists' : 'torrentgroups'].length + '</td>';
				tbody.append(elem);
				return collage;
			}).catch(function(reason) {
				evt.currentTarget.disabled = false;
				if (ajaxMessage == null) return;
				ajaxMessage.innerHTML = '<span style="color: #A00;">' + reason.toString() + '</span>';
				ajaxMessage.classList.remove('hidden');
			});
		}

		function openCollage(evt) {
			let collageId = parseInt(dropDown.value);
			if (collageId <= 0) return false;
			let win = window.open('/collages.php?id=' + collageId, '_blank');
			win.focus();
		}

		function dataHandler(target, data) {
			var text = data.getData('text/plain');
			if (!text) return true;
			target.value = text;
			srchForm.onsubmit();
			return false;
		}
	});
}

const contextId = 'context-9b7e0e42-1e35-4518-ac5f-b6bb31cce23f';
let menu = document.createElement('menu');
menu.type = 'context';
menu.id = contextId;
function contextUpdater(evt) { menu = evt.currentTarget }
menu.innerHTML = '<menuitem label="Remove from this collage" icon="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAADaElEQVQ4y0XTT0xcVRQG8O+ce+fNAOUN7QiFFoHSNq2SIDQ2QGfQ0hYtSatpuvFPjK0mxtDWRF24MG7UbVeasDGmiTrWgG2NjRlaUyAsirrQoCZVF5QZhsKUP/NmKMwM993jwlG/zfdtT3J+IGYCwPE3zye+f//d6UrmMAAwkUI5/+6qilD4q7Z90x9tjyQIYAIIAHjo1ZfjMn5dNm9+LdfeOjdZQeSCCEzETMQAUBl03C/6j0wmD0Vl7kCnfFgXiQNgFT/32ndnTp84lVnK+A+8rHTubW7piNT2Xfvp5xEDFABIheLwZ6dPjR4u+odWvKxvQ0Hpcd32RrFdvD1cXV9az8Ffz4sqFdT8fNr0P76/O372hYQDbHFEtnz+4vOJJ6C7M/fmTSjoKBYrOuAgEnDqqZI5HH/97LcDPe29CytLJmCtLpV8U1dVo6+M3R6DdnCirr4vOTFhKiortEBMrVJ6ODU3eSGVOkkAUKmU++WZ50aPdu7pvp/NmwAcbQzZ6mA12VUPmbFb4hAYzCbCrK+m01ODs8mni0BOMZEqiRS++eX3kQ5365PtO3c059dLVvuai+tFyf34A3SxwBxQ9iHF6mo6fXtwNjlQBHIMKCWAMBEbSOHqb3fie0NVsUdrd+za9MWaP+6wZBZJBQO2CsSXU+mJ88nUUwZYY4AtYDUAQAQgghWB2SiCfIGZT8OfS0E7DhiAIoEAsOXfkHIrJlIWZEMk4Uv9x248+9iBWHZ12dq//mSGWMUkmsDCsNFtNbsaoY6MetkRHyj8d0KI4H7S1zc68Ehbz8pazmBmRmFjw1bpAAUJJAxRAi5AzMEat7mBAodveN6wDxRUiBAeisWun9zfFl1ZXzOcWdT+6rKJBIPqcjI9/msufze6raa1ADEk0EWI6drqNkVEx2553hX9aTQ6fnTPvo7lnGf0xgNtlpZMJODo4dT81Nup5DMA4LC6+VJTQ3ceviGBzlnfvNJc3yuWxzmTzy+YnAdd3CB/6b4fUUoPp+9NXZi9e9yA1gyw9sbszPFLyYUpl5UWgh9gULZgsVjaXAAB/F7r7ngyGpPF7i75uKll0gFcACiLYwAIgNyLD7dOer09MnuwSwYjO+MAGGWS/EFdQ2KosWU6RPQPZ+B/zuUdJApfbNw9/U5dUwJlzn8DiOOFPhubkGUAAAAASUVORK5CYII=" /><menuitem label="-" />';

switch (document.location.pathname) {
	case '/torrents.php': {
		var torrentGroupId = new URLSearchParams(document.location.search).get('id'), collages;
		if (torrentGroupId) torrentGroupId = parseInt(torrentGroupId); else break; // Unexpected URL format
		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.currentTarget.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');
			searchforcollage.onkeypress = evt => { if (evt.key == 'Enter') SearchCollage() };
		} else addQuickAddForm();

		try { collages = JSON.parse(window.sessionStorage.collages) } catch(e) { collages = { } }
		if (!collages[document.domain]) collages[document.domain] = { };

		function callback(evt) {
			switch (evt.currentTarget.nodeName) {
				case 'A':
					if (evt.button != 0 || !evt.altKey) return true;
					var link = evt.currentTarget;
					break;
				case 'MENUITEM':
					link = menu || evt.relatedTarget || document.activeElement;
					break;
			}
			if (!(link instanceof HTMLAnchorElement)) return true;
			let collageId = parseInt(new URLSearchParams(link.search).get('id'));
			if (!collageId) {
				console.warn('Assertion failed: no collage id', link);
				throw 'no id';
			}
			return removeFromTorrentCollage(collageId, torrentGroupId,
					'Are you sure to remove this group from collage "' + link.textContent.trim() + '"?')
				.then(status => { link.parentElement.parentElement.remove() });
		}
		menu.children[0].onclick = callback;
		let subscribeCmd = document.createElement('menuitem');
		subscribeCmd.label = 'Subscribe to this collage - toggle (!)';
		subscribeCmd.title = 'Use with care - toggling command; on already subscribed collages performs unsubscribe';
		subscribeCmd.onclick = function(evt) {
			let link = menu || evt.relatedTarget || document.activeElement;
			if (!(link instanceof HTMLAnchorElement)) return true;
			let collageId = parseInt(new URLSearchParams(link.search).get('id'));
			if (!collageId) {
				console.warn('Assertion failed: no collage id', link);
				throw 'no id';
			}
			let xhr = new XMLHttpRequest;
			xhr.open('GET', '/userhistory.php?' + new URLSearchParams({
				action: 'collage_subscribe',
				collageid: collageId,
				auth: auth,
			}), true);
			xhr.onreadystatechange = function() {
				if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
				if (xhr.status >= 200 && xhr.status < 400) { console.info('Subscribed to collage id', collageId) }
					else console.error(defaultErrorHandler(xhr));
				xhr.abort();
			};
			xhr.send();
		};
		menu.insertBefore(subscribeCmd, menu.children[1]);
		document.body.append(menu);

		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 = callback;
			link.oncontextmenu = contextUpdater;
			link.setAttribute('contextmenu', contextId);
			link.title = 'Use Alt + left click or context menu(FF) 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 == torrentGroupId);
				if (index < 0) {
					console.warn('Assertion failed: torrent', torrentGroupId, '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.currentTarget.disabled = true;
				navLinks.forEach(a => { a.remove() });
				navLinks = [];
				let 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.currentTarget.disabled = false;
				}, function(reason) {
					span.remove();
					evt.currentTarget.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 '/artist.php': {
		var artistId = new URLSearchParams(document.location.search).get('id');
		if (artistId) artistId = parseInt(artistId); else break; // Unexpected URL format
		addQuickAddForm();
		break;
	}
	case '/collages.php': {
		var collageId = new URLSearchParams(document.location.search).get('id');
		if (collageId) collageId = parseInt(collageId); else break; // Collage id missing
		let category = document.querySelector('div.box_category > div.pad > a'), selectors, callback;
		category = category != null ? category.textContent : undefined;
		console.assert(category, 'category != undefined');

		if (category != 'Artists') {
			selectors = [
				'tr.group > td[colspan] > strong > a[href^="torrents.php?id="]',
				'ul.collage_images > li > a[href^="torrents.php?id="]',
			];
			callback = function(evt) {
				switch (evt.currentTarget.nodeName) {
					case 'A':
						if (evt.button != 0 || !evt.altKey) return true;
						var link = evt.currentTarget;
						break;
					case 'MENUITEM':
						link = menu || evt.relatedTarget || document.activeElement;
						break;
				}
				if (!(link instanceof HTMLAnchorElement)) return true;
				let torrentGroupId = parseInt(new URLSearchParams(link.search).get('id'));
				if (!torrentGroupId) {
					console.warn('Assertion failed: no id', link);
					throw 'no id';
				}
				removeFromTorrentCollage(collageId, torrentGroupId, 'Are you sure to remove selected group from this collage?').then(function(status) {
					document.querySelectorAll(selectors.join(', ')).forEach(function(a) {
						if (parseInt(new URLSearchParams(a.search).get('id')) == torrentGroupId) switch (a.parentNode.nodeName) {
							case 'STRONG': a.parentNode.parentNode.parentNode.remove(); break;
							case 'LI': a.parentNode.remove(); break;
						}
					});
				});
			};
		} else {
			selectors = [
				'table#discog_table > tbody > tr > td > a[href^="artist.php?id="]',
				'ul.collage_images > li > a[href^="artist.php?id="]',
			];
			callback = function(evt) {
				switch (evt.currentTarget.nodeName) {
					case 'A':
						if (evt.button != 0 || !evt.altKey) return true;
						var link = evt.currentTarget;
						break;
					case 'MENUITEM':
						link = menu || evt.relatedTarget || document.activeElement;
						break;
				}
				if (!(link instanceof HTMLAnchorElement)) return true;
				let artistId = parseInt(new URLSearchParams(link.search).get('id'));
				if (!artistId) {
					console.warn('Assertion failed: no id', evt.currentTarget);
					throw 'no id';
				}
				removeFromArtistCollage(collageId, artistId, 'Are you sure to remove selected artist from this collage?').then(function(status) {
					document.querySelectorAll(selectors.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;
						}
					});
				});
			};
			let artistLink = document.querySelector('form.add_form[name="artist"] input#artist');
			if (artistLink != null) {
				let ref = document.querySelector('form.add_form[name="artist"] > div.submit_div');
				let searchBtn = document.createElement('input');
				searchBtn.value = 'Look up';
				searchBtn.type = 'button';
				searchBtn.onclick = function(evt) {
					let xhr = new XMLHttpRequest;
					xhr.open('HEAD', '/artist.php?artistname=' + encodeURIComponent(artistLink.value.trim()), true);
					xhr.onreadystatechange = function() {
						if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
						artistLink.value = xhr.responseURL.includes('/artist.php?id=') ? xhr.responseURL : '';
					};
					xhr.send();
				};
				ref.append(searchBtn);
			}
		}
		menu.children[0].onclick = callback;
		document.body.append(menu);
		function handlerInstaller(a) {
			a.onclick = callback;
			a.oncontextmenu = contextUpdater;
			a.setAttribute('contextmenu', contextId);
		}
		document.querySelectorAll(selectors.join(', ')).forEach(handlerInstaller);
		let coverart = document.getElementById('coverart');
		if (coverart != null) new MutationObserver(function(mutationsList) {
			mutationsList.forEach(function(mutation) {
				if (mutation.type == 'childList') mutation.addedNodes.forEach(function(node) {
					if (node.nodeName != 'UL' || !node.classList.contains('collage_images')) return;
					node.querySelectorAll('li > a').forEach(handlerInstaller);
				});
			});
		}).observe(coverart, { childList: true });
		break;
	}
}