Collage Extensions for Gazelle Music Trackers

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

Ekde 2021/01/23. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Collage Extensions for Gazelle Music Trackers
// @version      1.22.1
// @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';
const gazelleApiFrame = 10500;
try { var redacted_api_key = GM_getValue('redacted_api_key') } catch(e) { }

function queryAjaxAPI(action, params) {
	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;
		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('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, torrentGroupId) {
	return collageId ? torrentGroupId ? 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')
	)) : Promise.reject('torrent group id not defined') : Promise.reject('collage id not defined');
}

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) {
	return collageId ? artistId ? 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')
	)) : Promise.reject('artist id not defined') : Promise.reject('collage id not defined');
}

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;
		}
	});
}

let menu = document.createElement('menu'), currentContextElement;
menu.type = 'context';
menu.id = 'context-c34101ff-0b15-41d8-9c10-33bebf9c5660';
menu.innerHTML = '<menuitem label="Remove from 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="-" />';
function contextUpdater(evt) { currentContextElement = evt.currentTarget }

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 = currentContextElement || 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';
			}
			return removeFromTorrentCollage(collageId, torrentGroupId,
					'Are you sure to remove this group from collage "' + link.textContent.trim() + '"?')
				.then(status => { link.parentElement.parentElement.remove() });
		}

		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', 'context-c34101ff-0b15-41d8-9c10-33bebf9c5660');
			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;
			}
		});
		menu.children[0].onclick = callback;
		document.body.append(menu);

		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 = currentContextElement || 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 = currentContextElement || 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);
		document.querySelectorAll(selectors.join(', ')).forEach(function(a) {
			a.onclick = callback;
			a.oncontextmenu = contextUpdater;
			a.setAttribute('contextmenu', 'context-c34101ff-0b15-41d8-9c10-33bebf9c5660');
		});
		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(a => { a.onclick = callback });
				});
			});
		}).observe(coverart, { childList: true });
		break;
	}
}