[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-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name [RED] Import music release details from Bandcamp
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 0.22.1
  5. // @match https://redacted.ch/upload.php
  6. // @match https://redacted.ch/requests.php?action=new
  7. // @match https://redacted.ch/requests.php?action=new&groupid=*
  8. // @match https://redacted.ch/torrents.php?id=*
  9. // @match https://redacted.ch/torrents.php?page=*&id=*
  10. // @match https://orpheus.network/upload.php
  11. // @match https://orpheus.network/requests.php?action=new
  12. // @match https://orpheus.network/requests.php?action=new&groupid=*
  13. // @match https://orpheus.network/torrents.php?id=*
  14. // @match https://orpheus.network/torrents.php?page=*&id=*
  15. // @run-at document-end
  16. // @iconURL https://s4.bcbits.com/img/favicon/favicon-32x32.png
  17. // @author Anakunda
  18. // @description Lets find music release on Bandcamp and imports text description, artist credits, image and tags into existing release group.
  19. // @copyright 2022, Anakunda (https://greasyfork.org/users/321857-anakunda)
  20. // @license GPL-3.0-or-later
  21. // @connect *
  22. // @grant GM_xmlhttpRequest
  23. // @grant GM_getValue
  24. // @grant GM_setValue
  25. // @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
  26. // @require https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
  27. // @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
  28. // @require https://openuserjs.org/src/libs/Anakunda/QobuzLib.min.js
  29. // @require https://openuserjs.org/src/libs/Anakunda/GazelleTagManager.min.js
  30. // ==/UserScript==
  31.  
  32. 'use strict';
  33.  
  34. const imageHostHelper = (function() {
  35. const input = document.head.querySelector('meta[name="ImageHostHelper"]');
  36. return (input != null ? Promise.resolve(input) : new Promise(function(resolve, reject) {
  37. const mo = new MutationObserver(function(mutationsList, mo) {
  38. for (let mutation of mutationsList) for (let node of mutation.addedNodes) {
  39. if (node.nodeName != 'META' || node.name != 'ImageHostHelper') continue;
  40. clearTimeout(timer); mo.disconnect();
  41. return resolve(node);
  42. }
  43. }), timer = setTimeout(function(mo) {
  44. mo.disconnect();
  45. reject('Timeout reached');
  46. }, 15000, mo);
  47. mo.observe(document.head, { childList: true });
  48. })).then(function(node) {
  49. console.assert(node instanceof HTMLElement);
  50. const propName = node.getAttribute('propertyname');
  51. console.assert(propName);
  52. return unsafeWindow[propName] || Promise.reject(`Assertion failed: '${propName}' not in unsafeWindow`);
  53. });
  54. })();
  55.  
  56. function fetchBandcampDetails(artists, album, isSingle = false) {
  57. function tryQuery(terms) {
  58. if (!Array.isArray(terms)) throw 'Invalid qrgument';
  59. if (terms.length <= 0) return Promise.reject('Nothing found');
  60. const url = new URL('https://bandcamp.com/search');
  61. url.searchParams.set('q', terms.map(term => '"' + term + '"').join(' '));
  62. const searchType = itemType => (function getPage(page = 1) {
  63. url.searchParams.set('item_type', itemType);
  64. url.searchParams.set('page', page);
  65. return globalXHR(url).then(function({document}) {
  66. const results = Array.from(document.body.querySelectorAll('div.search ul.result-items > li.searchresult'));
  67. const nextLink = document.body.querySelector('div.pager > a.next');
  68. return nextLink != null ? getPage(page + 1, itemType).then(_results => results.concat(_results)) : results;
  69. });
  70. })().then(results => results.length > 0 ? results : Promise.reject('Nothing found'));
  71. return searchType('a').catch(reason => isSingle && reason == 'Nothing found' ? searchType('t') : Promise.reject(reason));
  72. }
  73.  
  74. if (album) album = [
  75. /\s+(?:EP|E\.\s?P\.|-\s+(?:EP|E\.\s?P\.))$/i,
  76. /\s+\((?:EP|E\.\s?P\.|Live)\)$/i, /\s+\[(?:EP|E\.\s?P\.|Live)\]$/i,
  77. /\s+\((?:feat\.|ft\.|featuring\s).+\)$/i, /\s+\[(?:feat\.|ft\.|featuring\s).+\]$/i,
  78. ].reduce((title, rx) => title.replace(rx, ''), album.trim()); else throw 'Invalid argument';
  79. const nothingFound = 'Nothing found', bracketStripper = /(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))+$/g;
  80. return (
  81. Array.isArray(artists) && artists.length > 0 ? tryQuery(artists.concat(album)) : Promise.reject(nothingFound)
  82. ).catch(function(reason) {
  83. if (reason != nothingFound) return Promise.reject(reason);
  84. if (!Array.isArray(artists) || artists.length <= 0) return Promise.reject(nothingFound);
  85. if (!artists.some(artist => bracketStripper.test(artist)) && !bracketStripper.test(album)) return Promise.reject(nothingFound);
  86. return tryQuery(artists.map(artist => artist.replace(bracketStripper, '')).concat(album.replace(bracketStripper, '')));
  87. }).catch(function(reason) {
  88. return reason == nothingFound ? tryQuery([album]) : Promise.reject(reason);
  89. }).catch(function(reason) {
  90. if (reason != nothingFound) return Promise.reject(reason);
  91. return bracketStripper.test(album) ? tryQuery([album.replace(bracketStripper, '')]) : Promise.reject(nothingFound);
  92. }).then(searchResults => new Promise(function(resolve, reject) {
  93. console.assert(searchResults.length > 0);
  94. let selectedRow = null, dialog = document.createElement('DIALOG');
  95. dialog.innerHTML = `
  96. <form method="dialog">
  97. <div style="margin-bottom: 10pt; padding: 4px; background-color: #111; box-shadow: 1pt 1pt 5px #bbb inset;">
  98. <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;" />
  99. </div>
  100. <input value="Import details" type="button" disabled><input value="Cancel" type="button" style="margin-left: 5pt;">
  101. </form>`;
  102. 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;';
  103. dialog.oncancel = evt => { reject('Cancelled') };
  104. dialog.onclose = function(evt) {
  105. if (!evt.currentTarget.returnValue) reject('Cancelled');
  106. document.body.removeChild(evt.currentTarget);
  107. };
  108. const ul = dialog.querySelector('ul#bandcamp-search-results'), buttons = dialog.querySelectorAll('input[type="button"]');
  109. for (let li of searchResults) {
  110. for (let a of li.getElementsByTagName('A')) {
  111. a.onclick = evt => { if (!evt.ctrlKey && !evt.shiftKey) return false };
  112. a.search = '';
  113. }
  114. for (let styleSheet of [
  115. ['.searchresult .art img', 'max-height: 145px; max-width: 145px;'],
  116. ['.result-info', 'display: inline-block; color: white; padding: 5pt 10pt; box-sizing: border-box; vertical-align: top; width: 475px; line-height: 1.4em;'],
  117. ['.itemtype', 'font-size: 10px; color: #999; margin-bottom: 0.5em; padding: 0;'],
  118. ['.heading', 'font-size: 16px; margin-bottom: 0.1em; padding: 0;'],
  119. ['.subhead', 'font-size: 13px; margin-bottom: 0.3em; padding: 0;'],
  120. ['.released', 'font-size: 11px; padding: 0;'],
  121. ['.itemurl', 'font-size: 11px; padding: 0;'],
  122. ['.itemurl a', 'color: #84c67d;'],
  123. ['.tags', 'color: #aaa; font-size: 11px; padding: 0;'],
  124. ]) for (let elem of li.querySelectorAll(styleSheet[0])) elem.style = styleSheet[1];
  125. li.style = 'cursor: pointer; margin: 0; padding: 4px;';
  126. for (let child of li.children) child.style.display = 'inline-block';
  127. li.children[1].removeChild(li.children[1].children[0]);
  128. li.onclick = function(evt) {
  129. if (selectedRow != null) selectedRow.style.backgroundColor = null;
  130. (selectedRow = evt.currentTarget).style.backgroundColor = '#066';
  131. buttons[0].disabled = false;
  132. };
  133. ul.append(li);
  134. }
  135. buttons[0].onclick = function(evt) {
  136. evt.currentTarget.disabled = true;
  137. console.assert(selectedRow instanceof HTMLLIElement);
  138. const a = selectedRow.querySelector('div.result-info > div.heading > a');
  139. if (a != null) globalXHR(a.href).then(function({document}) {
  140. function safeParse(serialized) {
  141. if (serialized) try { return JSON.parse(serialized) } catch (e) { console.warn('BC meta invalid: %s', e, serialized) }
  142. return null;
  143. }
  144.  
  145. const details = { }, stripText = text => text ? [
  146. [/\r\n/gm, '\n'], [/[^\S\n]+$/gm, ''], [/\n{3,}/gm, '\n\n'],
  147. ].reduce((text, subst) => text.replace(...subst), text.trim()) : '';
  148. let elem = document.head.querySelector(':scope > script[type="application/ld+json"]');
  149. const releaseMeta = elem && safeParse(elem.text);
  150. const tralbum = (elem = document.head.querySelector('script[data-tralbum]')) && safeParse(elem.dataset.tralbum);
  151. if (tralbum != null && Array.isArray(tralbum.packages) && tralbum.packages.length > 0) for (let key in tralbum.packages[0])
  152. if (!tralbum.current[key] && tralbum.packages.every(pkg => pkg[key] == tralbum.packages[0][key]))
  153. tralbum.current[key] = tralbum.packages[0][key];
  154. if (releaseMeta != null && releaseMeta.byArtist) details.artist = releaseMeta.byArtist.name;
  155. if (releaseMeta != null && releaseMeta.name) details.title = releaseMeta.name;
  156. if (releaseMeta != null && releaseMeta.numTracks) details.numTracks = releaseMeta.numTracks;
  157. if (releaseMeta != null && releaseMeta.datePublished) details.releaseDate = new Date(releaseMeta.datePublished);
  158. if (releaseMeta != null && releaseMeta.publisher) details.publisher = releaseMeta.publisher.name;
  159. if (releaseMeta != null && releaseMeta.image) details.image = releaseMeta.image;
  160. else if ((elem = document.head.querySelector('meta[property="og:image"][content]')) != null) details.image = elem.content;
  161. else if ((elem = document.querySelector('div#tralbumArt > a.popupImage')) != null) details.image = elem.href;
  162. if (details.image) details.image = details.image.replace(/_\d+(?=\.\w+$)/, '_10');
  163. details.tags = releaseMeta != null && Array.isArray(releaseMeta.keywords) ? new TagManager(...releaseMeta.keywords)
  164. : new TagManager(...Array.from(document.querySelectorAll('div.tralbum-tags > a.tag'), a => a.textContent.trim()));
  165. if (details.tags.length < 0) delete details.tags;
  166. if (tralbum != null && tralbum.current.minimum_price <= 0) details.tags.add('freely.available');
  167. if (releaseMeta != null && releaseMeta.description) details.description = releaseMeta.description;
  168. else if (tralbum != null && tralbum.current.about) details.description = tralbum.current.about;
  169. if (details.description) details.description = stripText(details.description)
  170. .replace(/^24[^\S\n]*bits?[^\S\n]*\/[^\S\n]*\d+(?:\.\d+)?[^\S\n]*k(?:Hz)?$\n+/m, '');
  171. if (releaseMeta != null && releaseMeta.creditText) details.credits = tralbum.current.credits;
  172. else if (tralbum != null && tralbum.current.credits) details.credits = tralbum.current.credits;
  173. if (details.credits) details.credits = stripText(details.credits);
  174. if (releaseMeta != null && releaseMeta.mainEntityOfPage) details.url = releaseMeta.mainEntityOfPage;
  175. else if (tralbum != null && tralbum.url) details.url = tralbum.url;
  176. resolve(details);
  177. }, reject); else reject('Assertion failed: BC release link not found');
  178. dialog.close(a != null ? a.href : '');
  179. };
  180. buttons[1].onclick = evt => { dialog.close() };
  181. document.body.append(dialog);
  182. dialog.showModal();
  183. }));
  184. }
  185.  
  186. const siteTagsCache = 'siteTagsCache' in localStorage ? (function(serialized) {
  187. try { return JSON.parse(serialized) } catch(e) { return { } }
  188. })(localStorage.getItem('siteTagsCache')) : { };
  189. function getVerifiedTags(tags, confidencyThreshold = GM_getValue('tags_confidency_threshold', 1)) {
  190. if (!Array.isArray(tags)) throw 'Invalid argument';
  191. return Promise.all(tags.map(function(tag) {
  192. if (!(confidencyThreshold > 0) || tmWhitelist.includes(tag) || siteTagsCache[tag] >= confidencyThreshold)
  193. return Promise.resolve(tag);
  194. return queryAjaxAPICached('browse', { taglist: tag }).then(function(response) {
  195. const usage = response.pages > 1 ? (response.pages - 1) * 50 + 1 : response.results.length;
  196. if (usage < confidencyThreshold) return false;
  197. siteTagsCache[tag] = usage;
  198. Promise.resolve(siteTagsCache).then(cache => { localStorage.setItem('siteTagsCache', JSON.stringify(cache)) });
  199. return tag;
  200. }, reason => false);
  201. })).then(results => results.filter(Boolean));
  202. }
  203.  
  204. switch (document.location.pathname) {
  205. case '/torrents.php': {
  206. if (document.querySelector('div.sidebar > div.box_artists') == null) break; // Nothing to do here - not music torrent
  207. //if (!ajaxApiKey) throw 'AJAX API key not configured';
  208. const urlParams = new URLSearchParams(document.location.search), groupId = parseInt(urlParams.get('id'));
  209. if (!(groupId > 0)) throw 'Invalid group id';
  210. const linkBox = document.body.querySelector('div.header > div.linkbox');
  211. if (linkBox == null) throw 'LinkBox not found';
  212. const a = document.createElement('A');
  213. a.textContent = 'Bandcamp import';
  214. a.href = '#';
  215. a.title = 'Import album textual description, tags and cover image from Bandcamp release page';
  216. a.className = 'brackets';
  217. a.onclick = function(evt) {
  218. if (!this.disabled) this.disabled = true; else return false;
  219. this.style.color = 'orange';
  220. queryAjaxAPI('torrentgroup', { id: groupId }).then(torrentGroup =>
  221. fetchBandcampDetails(torrentGroup.group.releaseType != 6 ?
  222. torrentGroup.group.musicInfo.artists.map(artist => artist.name).slice(0, 3) : null,
  223. torrentGroup.group.name, torrentGroup.group.releaseType == 9).then(function(details) {
  224. const rehostWorker = details.image ? imageHostHelper.then(ihh => ihh.rehostImageLinks([details.image])
  225. .then(ihh.singleImageGetter)).catch(reason => details.image) : Promise.resolve(null);
  226. const updateWorkers = [ ];
  227. updateWorkers.push(localXHR('/torrents.php?' + new URLSearchParams({ action: 'editgroup', groupid: torrentGroup.group.id })).then(function(document) {
  228. const editForm = document.querySelector('form.edit_form');
  229. if (editForm == null) throw 'Edit form not found';
  230. let image = editForm.elements.namedItem('image').value, body = editForm.elements.namedItem('body').value.trim();
  231. if (details.description && !body.includes(details.description)) {
  232. if (body.length <= 0) body = '[quote]' + details.description + '[/quote]';
  233. else if (/^\[pad=\d+\|\d+\]/i.test(body))
  234. body = RegExp.leftContext + RegExp.lastMatch + '[quote]' + details.description + '[/quote]\n' + RegExp.rightContext;
  235. else body += '\n\n[quote]' + details.description + '[/quote]';
  236. }
  237. if (details.credits && !body.includes(details.credits)) {
  238. const credits = '[hide=Credits]' + details.credits + '[/hide]';
  239. if (body.length <= 0) body = credits;
  240. else if (/\[\/size\]\[\/pad\]$/i.test(body))
  241. body = RegExp.leftContext + '\n\n' + credits + RegExp.lastMatch + RegExp.rightContext;
  242. else body += '\n\n' + credits;
  243. }
  244. if (details.url && !body.includes(details.url)) {
  245. const url = '[url=' + details.url + ']Bandcamp[/url]';
  246. if (body.length <= 0) body = url;
  247. else if (/\[\/size\]\[\/pad\]$/i.test(body))
  248. body = RegExp.leftContext + '\n\n' + url + RegExp.lastMatch + RegExp.rightContext;
  249. else body += '\n\n' + url;
  250. }
  251. return rehostWorker.then(function(rehostedImageUrl) {
  252. if (rehostedImageUrl != null && rehostedImageUrl != image || body != editForm.elements.namedItem('body').value.trim()) {
  253. const formData = new FormData;
  254. formData.set('action', 'takegroupedit');
  255. formData.set('groupid', editForm.elements.namedItem('groupid').value);
  256. formData.set('image', rehostedImageUrl || image);
  257. formData.set('body', body.replace(/(\[\/quote\])\n{2,}/i, '$1\n'));
  258. formData.set('groupeditnotes', editForm.elements.namedItem('groupeditnotes').value);
  259. formData.set('releasetype', editForm.elements.namedItem('releasetype').value);
  260. formData.set('summary', 'Cover/additional description import from Bandcamp');
  261. formData.set('auth', editForm.elements.namedItem('auth').value);
  262. return localXHR('/torrents.php', { responseType: null }, formData);
  263. } else return false;
  264. });
  265. }));
  266. if (details.tags instanceof TagManager) {
  267. let bcTags = [ ];
  268. if (torrentGroup.group.musicInfo) for (let importance of Object.keys(torrentGroup.group.musicInfo))
  269. if (Array.isArray(torrentGroup.group.musicInfo[importance]))
  270. Array.prototype.push.apply(bcTags, torrentGroup.group.musicInfo[importance].map(artist => artist.name));
  271. if (Array.isArray(torrentGroup.torrents)) for (let torrent of torrentGroup.torrents) {
  272. if (!torrent.remasterRecordLabel) continue;
  273. const labels = torrent.remasterRecordLabel.split('/').map(label => label.trim());
  274. if (labels.length > 0) {
  275. Array.prototype.push.apply(bcTags, labels);
  276. Array.prototype.push.apply(bcTags, labels.map(function(label) {
  277. const bareLabel = label.replace(/(?:\s+(?:under license.+|Records|Recordings|(?:Ltd|Inc)\.?))+$/, '');
  278. if (bareLabel != label) return bareLabel;
  279. }).filter(Boolean));
  280. }
  281. }
  282. bcTags = new TagManager(...bcTags);
  283. bcTags = Array.from(details.tags).filter(tag => !bcTags.includes(tag));
  284. if (bcTags.length > 0) updateWorkers.push(getVerifiedTags(bcTags, 3).then(function(verifiedBcTags) {
  285. if (verifiedBcTags.length <= 0) return false;
  286. let userAuth = document.body.querySelector('input[name="auth"][value]');
  287. if (userAuth != null) userAuth = userAuth.value; else throw 'Failed to capture user auth';
  288. const updateWorkers = [ ];
  289. const releaseTags = Array.from(document.body.querySelectorAll('div.box_tags ul > li'), function(li) {
  290. const tag = { name: li.querySelector(':scope > a'), id: li.querySelector('span.remove_tag > a') };
  291. if (tag.name != null) tag.name = tag.name.textContent.trim();
  292. if (tag.id != null) tag.id = parseInt(new URLSearchParams(tag.id.search).get('tagid'));
  293. return tag.name && tag.id ? tag : null;
  294. }).filter(Boolean);
  295. const addTags = verifiedBcTags.filter(tag => !releaseTags.map(tag => tag.name).includes(tag));
  296. if (addTags.length > 0) Array.prototype.push.apply(updateWorkers, addTags.map(tag => localXHR('/torrents.php', { responseType: null }, new URLSearchParams({
  297. action: 'add_tag',
  298. groupid: torrentGroup.group.id,
  299. tagname: tag,
  300. auth: userAuth,
  301. }))));
  302. const deleteTags = releaseTags.filter(tag => !verifiedBcTags.includes(tag.name)).map(tag => tag.id);
  303. if (deleteTags.length > 0) Array.prototype.push.apply(updateWorkers, deleteTags.map(tagId => localXHR('/torrents.php?' + new URLSearchParams({
  304. action: 'delete_tag',
  305. groupid: torrentGroup.group.id,
  306. tagid: tagId,
  307. auth: userAuth,
  308. }), { responseType: null })));
  309. return updateWorkers.length > 0 ? Promise.all(updateWorkers.map(updateWorker =>
  310. updateWorker.then(response => true, reason => reason))).then(function(results) {
  311. if (!results.some(result => result === true))
  312. return Promise.reject(`All of ${results.length} tags update workers failed (see browser console for more details)`);
  313. return results;
  314. }) : false;
  315. }));
  316. }
  317. // Update by API is broken
  318. // if (details.image) updateWorkers.push(imageHostHelper.then(ihh => ihh.rehostImageLinks([details.image])
  319. // .then(ihh.singleImageGetter)).catch(reason => details.image).then(function(imageUrl) {
  320. // if (imageUrl == torrentGroup.group.wikiImage) return false;
  321. // return queryAjaxAPI('groupedit', { id: torrentGroup.group.id }, {
  322. // image: imageUrl,
  323. // summary: 'Cover update from Bandcamp',
  324. // });
  325. // }));
  326. // const ta = document.createElement('TEXTAREA');
  327. // ta.innerHTML = torrentGroup.group.bbBody;
  328. // let body = ta.textContent.trim();
  329. // if (details.description && !body.includes(details.description)) {
  330. // if (body.length <= 0) body = '[quote]' + details.description + '[/quote]';
  331. // else if (/^\[pad=\d+\|\d+\]/i.test(body))
  332. // body = RegExp.leftContext + RegExp.lastMatch + '[quote]' + details.description + '[/quote]\n' + RegExp.rightContext;
  333. // else body += '\n\n[quote]' + details.description + '[/quote]';
  334. // }
  335. // if (details.credits && !body.includes(details.credits)) {
  336. // const credits = '[hide=Credits]' + details.credits + '[/hide]';
  337. // if (body.length <= 0) body = credits;
  338. // else if (/\[\/size\]\[\/pad\]$/i.test(body))
  339. // body = RegExp.leftContext + '\n\n' + credits + RegExp.lastMatch + RegExp.rightContext;
  340. // else body += '\n\n' + credits;
  341. // }
  342. // if (details.url && !body.includes(details.url)) {
  343. // const url = '[url=' + details.url + ']Bandcamp[/url]';
  344. // if (body.length <= 0) body = url;
  345. // else if (/\[\/size\]\[\/pad\]$/i.test(body))
  346. // body = RegExp.leftContext + '\n\n' + url + RegExp.lastMatch + RegExp.rightContext;
  347. // else body += '\n\n' + url;
  348. // }
  349. // if (body != ta.textContent) {
  350. // const formData = new FormData;
  351. // formData.set('body', body.replace(/(\[\/quote\])\n{2,}/i, '$1\n'));
  352. // formData.set('summary', 'Description update from Bandcamp');
  353. // updateWorkers.push(queryAjaxAPI('groupedit', { id: groupId }, formData);
  354. // }
  355. if (updateWorkers.length > 0) return Promise.all(updateWorkers.map(updateWorker =>
  356. updateWorker.then(response => Boolean(response), reason => reason))).then(function(results) {
  357. if (results.every(result => !result)) return;
  358. if (!results.some(result => result === true))
  359. return Promise.reject(`All of ${results.length} update workers failed (see browser console for more details)`);
  360. document.location.reload();
  361. });
  362. })).catch(reason => { if (!['Cancelled'].includes(reason)) alert(reason) }).then(() => {
  363. this.style.color = null;
  364. this.disabled = false;
  365. });
  366. return false;
  367. };
  368. linkBox.append(' ', a);
  369. break;
  370. }
  371. case '/upload.php':
  372. case '/requests.php': {
  373. function hasStyleSheet(name) {
  374. if (name) name = name.toLowerCase(); else throw 'Invalid argument';
  375. const hrefRx = new RegExp('\\/' + name + '\\b', 'i');
  376. if (document.styleSheets) for (let styleSheet of document.styleSheets)
  377. if (styleSheet.title && styleSheet.title.toLowerCase() == name) return true;
  378. else if (styleSheet.href && hrefRx.test(styleSheet.href)) return true;
  379. return false;
  380. }
  381. function checkFields() {
  382. const visible = ['0', 'Music'].includes(categories.value) && title.textLength > 0;
  383. if (container.hidden != !visible) container.hidden = !visible;
  384. }
  385.  
  386. const categories = document.getElementById('categories');
  387. if (categories == null) throw 'Categories select not found';
  388. const form = document.getElementById('upload_table') || document.getElementById('request_form');
  389. if (form == null) throw 'Main form not found';
  390. let title = form.elements.namedItem('title');
  391. if (title != null) title.addEventListener('input', checkFields); else throw 'Title select not found';
  392. const dynaForm = document.getElementById('dynamic_form');
  393. if (dynaForm != null) new MutationObserver(function(ml, mo) {
  394. for (let mutation of ml) if (mutation.addedNodes.length > 0) {
  395. if (title != null) title.removeEventListener('input', checkFields);
  396. if ((title = document.getElementById('title')) != null) title.addEventListener('input', checkFields);
  397. else throw 'Assertion failed: title input not found!';
  398. container.hidden = true;
  399. }
  400. }).observe(dynaForm, { childList: true });
  401. const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light', '2iUn3'].some(hasStyleSheet);
  402. if (isLightTheme) console.log('Light Gazelle theme detected');
  403. const isDarkTheme = ['kuro', 'minimal', 'red_dark', 'Vinyl'].some(hasStyleSheet);
  404. if (isDarkTheme) console.log('Dark Gazelle theme detected');
  405. const container = document.createElement('DIV');
  406. container.style = 'position: fixed; top: 64pt; right: 10pt; padding: 5pt; border-radius: 50%; z-index: 999;';
  407. container.style.backgroundColor = `#${isDarkTheme ? '2f4f4f' : 'b8860b'}80`;
  408. const bcButton = document.createElement('BUTTON'), img = document.createElement('IMG');
  409. bcButton.id = 'import-from-bandcamp';
  410. bcButton.style = `
  411. padding: 10px; color: white; background-color: white; cursor: pointer;
  412. border: none; border-radius: 50%; transition: background-color 200ms;
  413. `;
  414. bcButton.dataset.backgroundColor = bcButton.style.backgroundColor;
  415. bcButton.setDisabled = function(disabled = true) {
  416. this.disabled = disabled;
  417. this.style.opacity = disabled ? 0.5 : 1;
  418. this.style.cursor = disabled ? 'not-allowed' : 'pointer';
  419. };
  420. bcButton.onclick = function(evt) {
  421. this.setDisabled(true);
  422. this.style.backgroundColor = 'red';
  423. const artists = Array.from(form.querySelectorAll('input[name="artists[]"]'), function(input) {
  424. const artist = input.value.trim();
  425. return input.nextElementSibling.value == 1 && artist;
  426. }).filter(Boolean);
  427. const releaseType = form.elements.namedItem('releasetype');
  428.  
  429. fetchBandcampDetails(releaseType == null || releaseType.value != 7 ? artists.slice(0, 3) : null,
  430. title.value.trim(), releaseType != null && releaseType.value == 9).then(function(details) {
  431. const tags = form.elements.namedItem('tags'), image = form.elements.namedItem('image'),
  432. description = form.elements.namedItem('album_desc') || form.elements.namedItem('description');
  433. if (tags != null && details.tags instanceof TagManager) {
  434. let bcTags = Array.from(form.querySelectorAll('input[name="artists[]"]'), input => input.value.trim()).filter(Boolean);
  435. let labels = form.elements.namedItem('remaster_record_label') || form.elements.namedItem('record_label');
  436. if (labels != null && (labels = labels.value.trim().split('/').map(label => label.trim())).length > 0) {
  437. Array.prototype.push.apply(bcTags, labels);
  438. Array.prototype.push.apply(bcTags, labels.map(function(label) {
  439. const bareLabel = label.replace(/(?:\s+(?:under license.+|Records|Recordings|(?:Ltd|Inc)\.?))+$/, '');
  440. if (bareLabel != label) return bareLabel;
  441. }).filter(Boolean));
  442. }
  443. bcTags = new TagManager(...bcTags);
  444. bcTags = Array.from(details.tags).filter(tag => !bcTags.includes(tag));
  445. getVerifiedTags(bcTags).then(bcVerifiedTags => { if (bcVerifiedTags.length > 0) tags.value = bcVerifiedTags.join(', ') });
  446. }
  447. if (image != null && details.image) {
  448. image.value = details.image;
  449. imageHostHelper.then(function(ihh) {
  450. ihh.rehostImageLinks([details.image]).then(ihh.singleImageGetter).then(rehostedUrl => { image.value = rehostedUrl });
  451. });
  452. }
  453. if (description != null) {
  454. let body = description.value.trim();
  455. if (details.description && !body.includes(details.description)) {
  456. if (body.length <= 0) body = '[quote]' + details.description + '[/quote]';
  457. else if (/^Releas(?:ing|ed) .+\d{4}$\n{2,}/im.test(body) || /^\[pad=\d+\|\d+\]/i.test(body))
  458. body = RegExp.leftContext + RegExp.lastMatch + '[quote]' + details.description + '[/quote]\n' + RegExp.rightContext;
  459. else body += '\n\n[quote]' + details.description + '[/quote]';
  460. }
  461. if (details.credits && document.location.pathname == '/upload.php' && !body.includes(details.credits)) {
  462. const credits = '[hide=Credits]' + details.credits + '[/hide]';
  463. if (body.length <= 0) body = credits;
  464. else if (/\[\/size\]\[\/pad\]$/i.test(body))
  465. body = RegExp.leftContext + '\n\n' + credits + RegExp.lastMatch + RegExp.rightContext;
  466. else body += '\n\n' + credits;
  467. }
  468. if (details.url && !body.includes(details.url)) {
  469. const url = '[url=' + details.url + ']Bandcamp[/url]';
  470. if (body.length <= 0) body = url;
  471. else if (/\[\/size\]\[\/pad\]$/i.test(body))
  472. body = RegExp.leftContext + '\n\n' + url + RegExp.lastMatch + RegExp.rightContext;
  473. else body += '\n\n' + url;
  474. }
  475. description.value = body.replace(/(\[\/quote\])\n{2,}/i, '$1\n');
  476. }
  477. }, reason => { if (!['Cancelled'].includes(reason)) alert(reason) }).then(() => {
  478. this.style.backgroundColor = this.dataset.backgroundColor;
  479. this.setDisabled(false);
  480. });
  481. };
  482. bcButton.onmouseenter = bcButton.onmouseleave = function(evt) {
  483. if (evt.relatedTarget == evt.currentTarget || evt.currentTarget.disabled) return false;
  484. evt.currentTarget.style.backgroundColor = evt.type == 'mouseenter' ? 'orange'
  485. : evt.currentTarget.dataset.backgroundColor || null;
  486. };
  487. bcButton.title = 'Import description, cover image and tags from Bandcamp';
  488. 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
  489. img.width = 32;
  490. bcButton.append(img);
  491. container.append(bcButton);
  492. checkFields();
  493. document.body.append(container);
  494. break;
  495. }
  496. }