您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Lets find music release on Bandcamp and imports text description, artist credits, image and tags into existing release group.
当前为
// ==UserScript== // @name [RED] Import music release details from bandcamp // @namespace http://greasyfork.icu/users/321857-anakunda // @version 0.10.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 // @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 (http://greasyfork.icu/users/321857-anakunda) // @license GPL-3.0-or-later // @connect bandcamp.com // @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 globalXHR(url).then(function({document}) { const results = document.body.querySelectorAll('div.search ul.result-items > li.searchresult'); return results.length > 0 ? results : Promise.reject('Not 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'; return (Array.isArray(artists) && artists.length > 0 ? tryQuery(artists.map(artist => '"' + artist + '"').join(' ') + ' "' + album + '"') : Promise.reject('No artist')).catch(reason => tryQuery('"' + album + '"')).then(searchResults => new Promise(function(resolve, reject) { console.assert(searchResults.length > 0); let selectedRow = null; const list = document.createElement('UL'); for (let li of searchResults) { for (let a of li.getElementsByTagName('A')) a.onclick = evt => { if (!evt.ctrlKey && !evt.shiftKey) return false }; for (let styleSheet of [ ['.searchresult .art img', 'max-height: 145px; max-width: 145px;'], ['.result-info', 'display: inline-block; color: #595959; vertical-align: top; width: 475px; margin-left: 1.3em; line-height: 1.4em;'], ['.itemtype', 'font-size: 10px; color: #999; margin-bottom: 0.5em;'], ['.heading', 'font-size: 16px; margin-bottom: 0.1em;'], ['.subhead', 'font-size: 13px; margin-bottom: 0.3em;'], ['.released', 'font-size: 11px;'], ['.itemurl', 'color: #999; font-size: 11px;'], ['.itemurl a', 'color: #84c67d;'], ['.tags', 'color: #999; font-size: 11px;'], ]) for (let elem of li.querySelectorAll(styleSheet[0])) elem.style = styleSheet[1]; li.style = 'cursor: pointer; margin: 0; padding: 8px;'; 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 = 'cornsilk'; buttons[0].disabled = false; }; list.append(li); } list.style = 'width: 665px; max-height: 70vw; background-color: white; padding: 5px; overflow-y: auto; overscroll-behavior-y: none; scrollbar-gutter: stable; scroll-behavior: auto; margin-bottom: 10pt; list-style-type: none; box-shadow: 1px 1px 5px #555 inset;'; const dialog = document.createElement('DIALOG'); dialog.innerHTML = ` <form id="bandcamp-search-results" method="dialog"> <input value="Import details" type="button" disabled><input value="Cancel" type="button" style="margin-left: 5pt;"> </form>`; dialog.style = 'padding: 1rem; position: fixed; top: 5%; left: 0; right: 0; margin-left: auto; margin-right: auto; background-color: lightgray; z-index: 9999;'; dialog.onclose = evt => { document.body.removeChild(evt.currentTarget) }; const form = dialog.querySelector('form#bandcamp-search-results'); form.prepend(list); const buttons = dialog.querySelectorAll('input[type="button"]'); 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.replace(/\?.*$/, '')).then(function({document}) { const details = { tags: new TagManager(...Array.from(document.querySelectorAll('div.tralbumData.tralbum-tags > a.tag'), a => a.textContent.trim())), }; let elem = document.querySelector('div#tralbumArt > a.popupImage'); if (elem != null) details.image = elem.href; else if ((elem = document.head.querySelector('meta[property="og:image"]')) != null) details.image = elem.content; if (details.image) details.image = details.image.replace(/_\d+(?=\.\w+$)/, '_10'); if ((elem = document.head.querySelector('script[data-tralbum]')) == 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]; if (tralbum.current.minimum_price <= 0) details.tags.add('freely.available'); 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'); resolve(details); }, reject); else reject('Link to album could not be found'); dialog.close(); }; buttons[1].onclick = function(evt) { reject('Cancelled'); dialog.close(); }; document.body.append(dialog); dialog.showModal(); })); } 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 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'; const rehostWorker = (details.image ? imageHostHelper.then(ihh => ihh.rehostImageLinks([details.image]) .then(ihh.singleImageGetter)).catch(reason => details.image) : Promise.resolve(null)); 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('auth', form.elements.namedItem('auth').value); formData.set('groupid', form.elements.namedItem('groupid').value); formData.set('image', rehostedImageUrl || image); formData.set('body', body); formData.set('groupeditnotes', form.elements.namedItem('groupeditnotes').value); formData.set('releasetype', form.elements.namedItem('releasetype').value); formData.set('summary', 'Image/description update from Bandcamp'); return localXHR('/torrents.php', { responseType: null }, formData).then(response => true, reason => reason); } else return 'No changes made'; }); })); if (details.tags instanceof TagManager) { let userAuth = document.body.querySelector('input[name="auth"][value]'); if (userAuth != null) { userAuth = userAuth.value; let tags = 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 = Array.from(details.tags).filter(tag => !tags.map(tag => tag.name).includes(tag)); if (addTags.length > 0) updateWorkers.push(localXHR('/torrents.php', { responseType: null }, new URLSearchParams({ action: 'add_tag', groupid: torrentGroup.group.id, tagname: addTags.join(', '), auth: userAuth, })).then(response => true, reason => reason)); const deleteTags = tags.filter(tag => !details.tags.includes(tag.name)); if (deleteTags.length > 0) Array.prototype.push.apply(updateWorkers, deleteTags.map(tag => localXHR('/torrents.php?' + new URLSearchParams({ action: 'delete_tag', groupid: torrentGroup.group.id, tagid: tag.id, auth: userAuth, }), { responseType: null }).then(response => true, reason => reason))); } } // 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', // }).then(response => true, reason => reason); // })); // 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); // formData.set('summary', 'Description update from Bandcamp'); // updateWorkers.push(queryAjaxAPI('groupedit', { id: groupId }, formData).then(response => true, reason => reason)); // } if (updateWorkers.length > 0) return Promise.all(updateWorkers).then(function(results) { if (results.filter(result => result === true).length > 0) document.location.reload(); else return Promise.reject(`All of ${results.length} update workers failed (see browser console for more details)`); }); })).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) : [ ], 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) tags.value = details.tags.toString(); 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; } }, 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; } }