[RED] Import music release details from Bandcamp

Lets find music release on Bandcamp and imports text description, artist credits, image and tags into existing release group.

目前為 2022-10-25 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         [RED] Import music release details from Bandcamp
// @namespace    https://greasyfork.org/users/321857-anakunda
// @version      0.21.0
// @match        https://redacted.ch/upload.php
// @match        https://redacted.ch/torrents.php?id=*
// @match        https://redacted.ch/torrents.php?page=*&id=*
// @match        https://orpheus.network/upload.php
// @match        https://orpheus.network/torrents.php?id=*
// @match        https://orpheus.network/torrents.php?page=*&id=*
// @run-at       document-end
// @iconURL      https://s4.bcbits.com/img/favicon/favicon-32x32.png
// @author       Anakunda
// @description  Lets find music release on Bandcamp and imports text description, artist credits, image and tags into existing release group.
// @copyright    2022, Anakunda (https://greasyfork.org/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/QobuzLib.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/GazelleTagManager.min.js
// ==/UserScript==

'use strict';

const imageHostHelper = (function() {
	const input = document.head.querySelector('meta[name="ImageHostHelper"]');
	return (input != null ? Promise.resolve(input) : new Promise(function(resolve, reject) {
		const mo = new MutationObserver(function(mutationsList, mo) {
			for (let mutation of mutationsList) for (let node of mutation.addedNodes) {
				if (node.nodeName != 'META' || node.name != 'ImageHostHelper') continue;
				clearTimeout(timer); mo.disconnect();
				return resolve(node);
			}
		}), timer = setTimeout(function(mo) {
			mo.disconnect();
			reject('Timeout reached');
		}, 15000, mo);
		mo.observe(document.head, { childList: true });
	})).then(function(node) {
		console.assert(node instanceof HTMLElement);
		const propName = node.getAttribute('propertyname');
		console.assert(propName);
		return unsafeWindow[propName] || Promise.reject(`Assertion failed: '${propName}' not in unsafeWindow`);
	});
})();

function fetchBandcampDetails(artists, album) {
	function tryQuery(query) {
		if (!query) throw 'Invalid qrgument';
		const url = new URL('https://bandcamp.com/search');
		url.searchParams.set('q', query);
		url.searchParams.set('item_type', 'a');
		return (function getPage(page = 1) {
			url.searchParams.set('page', page);
			return globalXHR(url).then(function({document}) {
				const results = Array.from(document.body.querySelectorAll('div.search ul.result-items > li.searchresult'));
				const nextLink = document.body.querySelector('div.pager > a.next');
				return nextLink != null ? getPage(page + 1).then(_results => results.concat(_results)) : results;
			});
		})().then(results => results.length > 0 ? results : Promise.reject('Nothing found'));
	}

	if (album) album = [
		/\s+(?:EP|E\.\s?P\.|-\s+(?:EP|E\.\s?P\.))$/i,
		/\s+\((?:EP|E\.\s?P\.|Live)\)$/i, /\s+\[(?:EP|E\.\s?P\.|Live)\]$/i,
		/\s+\((?:feat\.|ft\.|featuring\s).+\)$/i, /\s+\[(?:feat\.|ft\.|featuring\s).+\]$/i,
	].reduce((title, rx) => title.replace(rx, ''), album.trim()); else throw 'Invalid argument';
	const nothingFound = 'Nothing found', bracketStripper = /(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))+$/g;
	return (function() {
		if (!Array.isArray(artists) || artists.length <= 0) return Promise.reject(nothingFound);
		return tryQuery(artists.map(artist => '"' + artist + '"').join(' ') + ' "' + album + '"');
	})().catch(function(reason) {
		if (reason != nothingFound) return Promise.reject(reason);
		return tryQuery('"' + album + '"');
	}).catch(function(reason) {
		if (reason != nothingFound) return Promise.reject(reason);
		if (!Array.isArray(artists) || artists.length <= 0) return Promise.reject(nothingFound);
		if (!artists.some(artist => bracketStripper.test(artist))
				&& !bracketStripper.test(album)) return Promise.reject(nothingFound);
		return tryQuery(artists.map(artist => '"' + artist.replace(bracketStripper, '') + '"').join(' ') +
			' "' + album.replace(bracketStripper, '') + '"');
	}).catch(function(reason) {
		if (reason != nothingFound) return Promise.reject(reason);
		if (!bracketStripper.test(album)) return Promise.reject(nothingFound);
		return tryQuery('"' + album.replace(bracketStripper, '') + '"');
	}).then(searchResults => new Promise(function(resolve, reject) {
		console.assert(searchResults.length > 0);
		let selectedRow = null, dialog = document.createElement('DIALOG');
		dialog.innerHTML = `
<form method="dialog">
	<div style="margin-bottom: 10pt; padding: 4px; background-color: #111; box-shadow: 1pt 1pt 5px #bbb inset;">
		<ul id="bandcamp-search-results" style="list-style-type: none; width: 645px; max-height: 70vw; overflow-y: auto; overscroll-behavior-y: none; scrollbar-gutter: stable; scroll-behavior: auto; scrollbar-color: #444 #222;" />
	</div>
	<input value="Import details" type="button" disabled><input value="Cancel" type="button" style="margin-left: 5pt;">
</form>`;
		dialog.style = 'position: fixed; top: 5%; left: 0; right: 0; padding: 10pt; margin-left: auto; margin-right: auto; background-color: gray; box-shadow: 5px 5px 10px; z-index: 9999;';
		dialog.oncancel = evt => { reject('Cancelled') };
		dialog.onclose = function(evt) {
			if (!evt.currentTarget.returnValue) reject('Cancelled');
			document.body.removeChild(evt.currentTarget);
		};
		const ul = dialog.querySelector('ul#bandcamp-search-results'), buttons = dialog.querySelectorAll('input[type="button"]');
		for (let li of searchResults) {
			for (let a of li.getElementsByTagName('A')) {
				a.onclick = evt => { if (!evt.ctrlKey && !evt.shiftKey) return false };
				a.search = '';
			}
			for (let styleSheet of [
				['.searchresult .art img', 'max-height: 145px; max-width: 145px;'],
				['.result-info', 'display: inline-block; color: white; padding: 5pt 10pt; box-sizing: border-box; vertical-align: top; width: 475px; line-height: 1.4em;'],
				['.itemtype', 'font-size: 10px; color: #999; margin-bottom: 0.5em; padding: 0;'],
				['.heading', 'font-size: 16px; margin-bottom: 0.1em; padding: 0;'],
				['.subhead', 'font-size: 13px; margin-bottom: 0.3em; padding: 0;'],
				['.released', 'font-size: 11px; padding: 0;'],
				['.itemurl', 'font-size: 11px; padding: 0;'],
				['.itemurl a', 'color: #84c67d;'],
				['.tags', 'color: #aaa; font-size: 11px; padding: 0;'],
			]) for (let elem of li.querySelectorAll(styleSheet[0])) elem.style = styleSheet[1];
			li.style = 'cursor: pointer; margin: 0; padding: 4px;';
			for (let child of li.children) child.style.display = 'inline-block';
			li.children[1].removeChild(li.children[1].children[0]);
			li.onclick = function(evt) {
				if (selectedRow != null) selectedRow.style.backgroundColor = null;
				(selectedRow = evt.currentTarget).style.backgroundColor = '#066';
				buttons[0].disabled = false;
			};
			ul.append(li);
		}
		buttons[0].onclick = function(evt) {
			console.assert(selectedRow instanceof HTMLTableRowElement);
			evt.currentTarget.disabled = true;
			const a = selectedRow.querySelector('div.result-info > div.heading > a');
			if (a != null) globalXHR(a.href).then(function({document}) {
				const details = { };
				let elem = document.head.querySelector('script[data-tralbum]');
				if (elem == null) throw 'tralbum data not found';
				const tralbum = JSON.parse(elem.dataset.tralbum);
				if (typeof tralbum != 'object') throw 'invalid tralbum format';
				if (Array.isArray(tralbum.packages) && tralbum.packages.length > 0) for (let key in tralbum.packages[0])
					if (!tralbum.current[key] && tralbum.packages.every(pkg => pkg[key] == tralbum.packages[0][key]))
						tralbum.current[key] = tralbum.packages[0][key];
				const meta2 = (elem = document.head.querySelector(':scope > script[type="application/ld+json"]')) && JSON.parse(elem.text);
				if (meta2 != null && meta2.image) details.image = meta2.image;
				else if ((elem = document.head.querySelector('meta[property="og:image"][content]')) != null) details.image = elem.content;
				else if ((elem = document.querySelector('div#tralbumArt > a.popupImage')) != null) details.image = elem.href;
				if (details.image) details.image = details.image.replace(/_\d+(?=\.\w+$)/, '_10');
				if (tralbum.url) details.url = tralbum.url;
				if (tralbum.current.about) details.description = tralbum.current.about.replace(/\r\n/g, '\n');
				if (tralbum.current.credits) details.credits = tralbum.current.credits.replace(/\r\n/g, '\n');
				details.tags = new TagManager(...Array.from(document.querySelectorAll('div.tralbumData.tralbum-tags > a.tag'),
					a => a.textContent.trim()));
				if (tralbum.current.minimum_price <= 0) details.tags.add('freely.available');
				resolve(details);
			}, reject); else reject('BC release link not found');
			dialog.close(a != null ? a.href : '');
		};
		buttons[1].onclick = evt => { dialog.close() };
		document.body.append(dialog);
		dialog.showModal();
	}));
}

function getVerifiedTags(tags, confidencyThreshold = GM_getValue('tags_confidency_threshold', 1)) {
	if (!Array.isArray(tags)) throw 'Invalid argument';
	return Promise.all(tags.map(function(tag) {
		if (!(confidencyThreshold > 0) || tmWhitelist.includes(tag) || GM_getValue('tags_whitelist', { })[tag] >= confidencyThreshold)
			return Promise.resolve(tag);
		return queryAjaxAPICached('browse', { taglist: tag }).then(function(response) {
			if (response.results.length < confidencyThreshold) return false;
			GM_setValue('tags_whitelist', Object.assign(GM_getValue('tags_whitelist', { }), { [tag]: response.results.length }));
			return tag;
		}, reason => false);
	})).then(results => results.filter(Boolean));
}

switch (document.location.pathname) {
	case '/torrents.php': {
		if (document.querySelector('div.sidebar > div.box_artists') == null) break; // Nothing to do here - not music torrent
		//if (!ajaxApiKey) throw 'AJAX API key not configured';
		const urlParams = new URLSearchParams(document.location.search), groupId = parseInt(urlParams.get('id'));
		if (!(groupId > 0)) throw 'Invalid group id';
		const linkBox = document.body.querySelector('div.header > div.linkbox');
		if (linkBox == null) throw 'LinkBox not found';
		const a = document.createElement('A');
		a.textContent = 'Bandcamp import';
		a.href = '#';
		a.title = 'Import album textual description, tags and cover image from Bandcamp release page';
		a.className = 'brackets';
		a.onclick = function(evt) {
			if (!this.disabled) this.disabled = true; else return false;
			this.style.color = 'orange';
			queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup =>
					fetchBandcampDetails(torrentGroup.group.releaseType != 6 ?
						torrentGroup.group.musicInfo.artists.map(artist => artist.name).slice(0, 3) : null, torrentGroup.group.name)
							.then(function(details) {
				const rehostWorker = details.image ? imageHostHelper.then(ihh => ihh.rehostImageLinks([details.image])
					.then(ihh.singleImageGetter)).catch(reason => details.image) : Promise.resolve(null);
				const updateWorkers = [ ];
				updateWorkers.push(localXHR('/torrents.php?' + new URLSearchParams({ action: 'editgroup', groupid: torrentGroup.group.id })).then(function(document) {
					const form = document.querySelector('form.edit_form');
					if (form == null) throw 'Edit form not found';
					let image = form.elements.namedItem('image').value, body = form.elements.namedItem('body').value.trim();
					if (details.description && !body.includes(details.description)) {
						if (body.length <= 0) body = '[quote]' + details.description + '[/quote]';
						else if (/^\[pad=\d+\|\d+\]/i.test(body))
							body = RegExp.leftContext + RegExp.lastMatch + '[quote]' + details.description + '[/quote]\n' + RegExp.rightContext;
						else body += '\n\n[quote]' + details.description + '[/quote]';
					}
					if (details.credits && !body.includes(details.credits)) {
						const credits = '[hide=Credits]' + details.credits + '[/hide]';
						if (body.length <= 0) body = credits;
						else if (/\[\/size\]\[\/pad\]$/i.test(body))
							body = RegExp.leftContext + '\n\n' + credits + RegExp.lastMatch + RegExp.rightContext;
						else body += '\n\n' + credits;
					}
					if (details.url && !body.includes(details.url)) {
						const url = '[url=' + details.url + ']Bandcamp[/url]';
						if (body.length <= 0) body = url;
						else if (/\[\/size\]\[\/pad\]$/i.test(body))
							body = RegExp.leftContext + '\n\n' + url + RegExp.lastMatch + RegExp.rightContext;
						else body += '\n\n' + url;
					}
					return rehostWorker.then(function(rehostedImageUrl) {
						if (rehostedImageUrl != null && rehostedImageUrl != image || body != form.elements.namedItem('body').value.trim()) {
							const formData = new FormData;
							formData.set('action', 'takegroupedit');
							formData.set('groupid', form.elements.namedItem('groupid').value);
							formData.set('image', rehostedImageUrl || image);
							formData.set('body', body.replace(/(\[\/quote\])\n{2,}/i, '$1\n'));
							formData.set('groupeditnotes', form.elements.namedItem('groupeditnotes').value);
							formData.set('releasetype', form.elements.namedItem('releasetype').value);
							formData.set('summary', 'Image/description update from Bandcamp');
							formData.set('auth', form.elements.namedItem('auth').value);
							return localXHR('/torrents.php', { responseType: null }, formData);
						} else return false;
					});
				}));
				if (details.tags instanceof TagManager) {
					let bcTags = [ ];
					if (torrentGroup.group.musicInfo) for (let importance of Object.keys(torrentGroup.group.musicInfo))
						if (Array.isArray(torrentGroup.group.musicInfo[importance]))
							Array.prototype.push.apply(bcTags, torrentGroup.group.musicInfo[importance].map(artist => artist.name));
					if (Array.isArray(torrentGroup.torrents)) for (let torrent of torrentGroup.torrents) {
						if (!torrent.remasterRecordLabel) continue;
						const labels = torrent.remasterRecordLabel.split('/').map(label => label.trim());
						if (labels.length > 0) {
							Array.prototype.push.apply(bcTags, labels);
							Array.prototype.push.apply(bcTags, labels.map(function(label) {
								const bareLabel = label.replace(/(?:\s+(?:under license.+|Records|Recordings|(?:Ltd|Inc)\.?))+$/, '');
								if (bareLabel != label) return bareLabel;
							}).filter(Boolean));
						}
					}
					bcTags = new TagManager(...bcTags);
					bcTags = Array.from(details.tags).filter(tag => !bcTags.includes(tag));
					if (bcTags.length > 0) updateWorkers.push(getVerifiedTags(bcTags, 5).then(function(verifiedBcTags) {
						if (verifiedBcTags.length <= 0) return false;
						let userAuth = document.body.querySelector('input[name="auth"][value]');
						if (userAuth != null) userAuth = userAuth.value; else throw 'Failed to capture user auth';
						const updateWorkers = [ ];
						const releaseTags = Array.from(document.body.querySelectorAll('div.box_tags ul > li'), function(li) {
							const tag = { name: li.querySelector(':scope > a'), id: li.querySelector('span.remove_tag > a') };
							if (tag.name != null) tag.name = tag.name.textContent.trim();
							if (tag.id != null) tag.id = parseInt(new URLSearchParams(tag.id.search).get('tagid'));
							return tag.name && tag.id ? tag : null;
						}).filter(Boolean);
						const addTags = verifiedBcTags.filter(tag => !releaseTags.map(tag => tag.name).includes(tag));
						if (addTags.length > 0) Array.prototype.push.apply(updateWorkers, addTags.map(tag => localXHR('/torrents.php', { responseType: null }, new URLSearchParams({
							action: 'add_tag',
							groupid: torrentGroup.group.id,
							tagname: tag,
							auth: userAuth,
						}))));
						const deleteTags = releaseTags.filter(tag => !verifiedBcTags.includes(tag.name)).map(tag => tag.id);
						if (deleteTags.length > 0) Array.prototype.push.apply(updateWorkers, deleteTags.map(tagId => localXHR('/torrents.php?' + new URLSearchParams({
							action: 'delete_tag',
							groupid: torrentGroup.group.id,
							tagid: tagId,
							auth: userAuth,
						}), { responseType: null })));
						return updateWorkers.length > 0 ? Promise.all(updateWorkers.map(updateWorker =>
								updateWorker.then(response => true, reason => reason))).then(function(results) {
							if (!results.some(result => result === true))
								return Promise.reject(`All of ${results.length} tags update workers failed (see browser console for more details)`);
							return results;
						}) : false;
					}));
				}
				// Update by API is broken
				// if (details.image) updateWorkers.push(imageHostHelper.then(ihh => ihh.rehostImageLinks([details.image])
				// 		.then(ihh.singleImageGetter)).catch(reason => details.image).then(function(imageUrl) {
				// 	if (imageUrl == torrentGroup.group.wikiImage) return false;
				// 	return queryAjaxAPI('groupedit', { id: torrentGroup.group.id }, {
				// 		image: imageUrl,
				// 		summary: 'Cover update from Bandcamp',
				// 	});
				// }));
				// const ta = document.createElement('TEXTAREA');
				// ta.innerHTML = torrentGroup.group.bbBody;
				// let body = ta.textContent.trim();
				// if (details.description && !body.includes(details.description)) {
				// 	if (body.length <= 0) body = '[quote]' + details.description + '[/quote]';
				// 	else if (/^\[pad=\d+\|\d+\]/i.test(body))
				// 		body = RegExp.leftContext + RegExp.lastMatch + '[quote]' + details.description + '[/quote]\n' + RegExp.rightContext;
				// 	else body += '\n\n[quote]' + details.description + '[/quote]';
				// }
				// if (details.credits && !body.includes(details.credits)) {
				// 	const credits = '[hide=Credits]' + details.credits + '[/hide]';
				// 	if (body.length <= 0) body = credits;
				// 	else if (/\[\/size\]\[\/pad\]$/i.test(body))
				// 		body = RegExp.leftContext + '\n\n' + credits + RegExp.lastMatch + RegExp.rightContext;
				// 	else body += '\n\n' + credits;
				// }
				// if (details.url && !body.includes(details.url)) {
				// 	const url = '[url=' + details.url + ']Bandcamp[/url]';
				// 	if (body.length <= 0) body = url;
				// 	else if (/\[\/size\]\[\/pad\]$/i.test(body))
				// 		body = RegExp.leftContext + '\n\n' + url + RegExp.lastMatch + RegExp.rightContext;
				// 	else body += '\n\n' + url;
				// }
				// if (body != ta.textContent) {
				// 	const formData = new FormData;
				// 	formData.set('body', body.replace(/(\[\/quote\])\n{2,}/i, '$1\n'));
				// 	formData.set('summary', 'Description update from Bandcamp');
				// 	updateWorkers.push(queryAjaxAPI('groupedit', { id: groupId }, formData);
				// }
				if (updateWorkers.length > 0) return Promise.all(updateWorkers.map(updateWorker =>
						updateWorker.then(response => Boolean(response), reason => reason))).then(function(results) {
					if (results.every(result => !result)) return;
					if (!results.some(result => result === true))
						return Promise.reject(`All of ${results.length} update workers failed (see browser console for more details)`);
					document.location.reload();
				});
			})).catch(reason => { if (!['Cancelled'].includes(reason)) alert(reason) }).then(() => {
				this.style.color = null;
				this.disabled = false;
			});
			return false;
		};
		linkBox.append(' ', a);
		break;
	}
	case '/upload.php': {
		function hasStyleSheet(name) {
			if (name) name = name.toLowerCase(); else throw 'Invalid argument';
			const hrefRx = new RegExp('\\/' + name + '\\b', 'i');
			if (document.styleSheets) for (let styleSheet of document.styleSheets)
				if (styleSheet.title && styleSheet.title.toLowerCase() == name) return true;
					else if (styleSheet.href && hrefRx.test(styleSheet.href)) return true;
			return false;
		}

		const checkFields = function() {
			const visible = ['0', 'Music'].includes(categories.value) && title.textLength > 0;
			if (div.hidden != !visible) div.hidden = !visible;
		};
		const categories = document.getElementById('categories');
		if (categories == null) throw 'Categories select not found';
		let title = document.getElementById('title');
		if (title != null) title.addEventListener('input', checkFields); else throw 'Title select not found';
		const dynaForm = document.getElementById('dynamic_form');
		if (dynaForm != null) new MutationObserver(function(ml, mo) {
			for (let mutation of ml) if (mutation.addedNodes.length > 0) {
				if (title != null) title.removeEventListener('input', checkFields);
				if ((title = document.getElementById('title')) != null) title.addEventListener('input', checkFields);
					else throw 'Assertion failed: title input not found!';
				div.hidden = true;
			}
		}).observe(dynaForm, { childList: true });
		const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light', '2iUn3'].some(hasStyleSheet);
		if (isLightTheme) console.log('Light Gazelle theme detected');
		const isDarkTheme = ['kuro', 'minimal', 'red_dark', 'Vinyl'].some(hasStyleSheet);
		if (isDarkTheme) console.log('Dark Gazelle theme detected');
		const div = document.createElement('DIV');
		div.style = 'position: fixed; top: 64pt; right: 10pt; padding: 5pt; border-radius: 50%; z-index: 999;';
		div.style.backgroundColor = `#${isDarkTheme ? '2f4f4f' : 'b8860b'}80`;
		const bcButton = document.createElement('BUTTON'), img = document.createElement('IMG');
		bcButton.id = 'import-from-bandcamp';
		bcButton.style = `
padding: 10px; color: white; background-color: white; cursor: pointer;
border: none; border-radius: 50%; transition: background-color 200ms;
`;
		bcButton.dataset.backgroundColor = bcButton.style.backgroundColor;
		bcButton.setDisabled = function(disabled = true) {
			this.disabled = disabled;
			this.style.opacity = disabled ? 0.5 : 1;
			this.style.cursor = disabled ? 'not-allowed' : 'pointer';
		};
		bcButton.onclick = function(evt) {
			this.setDisabled(true);
			this.style.backgroundColor = 'red';
			const artists = Array.from(document.body.querySelectorAll('tr#artist_tr input[name="artists[]"]'), function(input) {
				const artist = input.value.trim();
				return input.nextElementSibling.value == 1 && artist;
			}).filter(Boolean);
			const releaseType = document.getElementById('releasetype');

			fetchBandcampDetails(releaseType == null || releaseType.value != 7 ? artists.slice(0, 3) : null, title.value.trim()).then(function(details) {
				const tags = document.getElementById('tags'),
							image = document.getElementById('image'),
							description = document.getElementById('album_desc');
				if (tags != null && details.tags instanceof TagManager) {
					let bcTags = Array.from(document.body.querySelectorAll('tr#artist_tr input[name="artists[]"]'),
						input => input.value.trim()).filter(Boolean);
					let labels = document.getElementById('remaster_record_label');
					if (labels != null) {
						labels = labels.value.trim().split('/').map(label => label.trim());
						if (labels.length > 0) {
							Array.prototype.push.apply(bcTags, labels);
							Array.prototype.push.apply(bcTags, labels.map(function(label) {
								const bareLabel = label.replace(/(?:\s+(?:under license.+|Records|Recordings|(?:Ltd|Inc)\.?))+$/, '');
								if (bareLabel != label) return bareLabel;
							}).filter(Boolean));
						}
					}
					bcTags = new TagManager(...bcTags);
					bcTags = Array.from(details.tags).filter(tag => !bcTags.includes(tag));
					getVerifiedTags(bcTags).then(bcVerifiedTags => { if (bcVerifiedTags.length > 0) tags.value = bcVerifiedTags.join(', ') });
				}
				if (image != null && details.image) {
					image.value = details.image;
					imageHostHelper.then(function(ihh) {
						ihh.rehostImageLinks([details.image]).then(ihh.singleImageGetter).then(rehostedUrl => { image.value = rehostedUrl });
					});
				}
				if (description != null) {
					let body = description.value.trim();
					if (details.description && !body.includes(details.description)) {
						if (body.length <= 0) body = '[quote]' + details.description + '[/quote]';
						else if (/^\[pad=\d+\|\d+\]/i.test(body))
							body = RegExp.leftContext + RegExp.lastMatch + '[quote]' + details.description + '[/quote]\n' + RegExp.rightContext;
						else body += '\n\n[quote]' + details.description + '[/quote]';
					}
					if (details.credits && !body.includes(details.credits)) {
						const credits = '[hide=Credits]' + details.credits + '[/hide]';
						if (body.length <= 0) body = credits;
						else if (/\[\/size\]\[\/pad\]$/i.test(body))
							body = RegExp.leftContext + '\n\n' + credits + RegExp.lastMatch + RegExp.rightContext;
						else body += '\n\n' + credits;
					}
					if (details.url && !body.includes(details.url)) {
						const url = '[url=' + details.url + ']Bandcamp[/url]';
						if (body.length <= 0) body = url;
						else if (/\[\/size\]\[\/pad\]$/i.test(body))
							body = RegExp.leftContext + '\n\n' + url + RegExp.lastMatch + RegExp.rightContext;
						else body += '\n\n' + url;
					}
					description.value = body.replace(/(\[\/quote\])\n{2,}/i, '$1\n');
				}
			}, reason => { if (!['Cancelled'].includes(reason)) alert(reason) }).then(() => {
				this.style.backgroundColor = this.dataset.backgroundColor;
				this.setDisabled(false);
			});
		};
		bcButton.onmouseenter = bcButton.onmouseleave = function(evt) {
			if (evt.relatedTarget == evt.currentTarget || evt.currentTarget.disabled) return false;
			evt.currentTarget.style.backgroundColor = evt.type == 'mouseenter' ? 'orange'
				: evt.currentTarget.dataset.backgroundColor || null;
		};
		bcButton.title = 'Import description, cover image and tags from Bandcamp';
		img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAbFBMVEX////3///v///v9//m9//m9/fe9/fe7/fW7/fO7/fF7/fF5ve95u+15u+13u+t3u+l3u+l3uac1uaU1uaM1uaMzuaEzt57zt57xd5zxd5rxd5jvdZavdZSvdZStdZKtdZKtc5Ctc5Crc4ZpcWSSeq2AAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEgAACxIB0t1+/AAAAoBJREFUeJzt2mtvgjAUBuCC93sUdR/2/3/bsqlbNqfzVpgGExXa02N5ZVly+gVBpU+a0retVtUfl6oABCAAAQhAAAIQgAAEIAABCEAAArjjs60oUSpI0pNCx/jFBxA8Ve7QkmV9eXkHYAKrX7/5ACptVP3xSvsAprAGSGZXJ2xAo4GqX39dn7EBY1gDqHcfQKuGql5/3JxyAZPw/CIJCh7Vpw+gG56/r4oe4/ntnZkAXA+Iv30AA1T1Si8yF3iAIawBDisfwAhVvdLz7BUWoA9rgN3GBzBBVa/0LHeJAQg6pwYo/Pyfjpu9DyCdBiDGAf2av7sbUGu6jbyiV4kPADcPUfkewAA066jqb2OYDXhUDHMBndDxAXbJxDAXEMHmAZkYZgK6IeT5P5ZDNoV4gEsPKDoOJJkY5gFwMbw3PYJuAC6G9Y8P4JExzALgYni79QFMA+LNu8r1YpAPqLRhg9BaW98iAGkKIcYBwzyEATjHMGAeEC8NMewGAFfDlkGQBjRxQ4A5BFyAMS6FzDHoAHRg22faOA1wAiLcYtA4EXIB+rh5CN0ANsCogpoHaEsM04AhZh2gqBQiAQNYD9jbYpgERKjqyUGYAPRwMbzzAZQSwwQAF8MxEcMEAJhC7gYwAOrpnixgHNBLBjIPOK+GEeMAFcNWAHBPloxhKwCXQnQM2wDdsmLYBhiVFcMWAC6G95wemAcAGyC7J8sCDEH7gcRqmAYcYxg1D7AuBilAVGYKmQA9WBc07MkyAMAY5vaAG0C5MWwAlBvDecCjfhplA57THgAYB0JeCmQBC9iutGspYAGw0htf/tV/SAQgAAEIQAACEIAABCAAAQhAAAJ4SPkFdtLUKHgfCmoAAAAASUVORK5CYII=' // https://s4.bcbits.com/img/favicon/apple-touch-icon.png
		img.width = 32;
		bcButton.append(img);
		div.append(bcButton);
		checkFields();
		document.body.append(div);
		break;
	}
}