// ==UserScript== // @name Image Host Helper // @namespace https://greasyfork.org/users/321857-anakunda // @version 1.90.7 // @description Directly upload local / rehost remote images or galleries to whatever supported image host by dropping/pasting them to target field // @icon data:image/png;base64,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 // @author Anakunda // @copyright 2020-21, Anakunda (https://greasyfork.org/cs/users/321857-anakunda) // @license GPL-3.0-or-later // @match https://passthepopcorn.me/* // @match https://redacted.ch/* // @match https://orpheus.network/* // @match https://broadcasthe.net/* // @match https://notwhat.cd/* // @match https://dicmusic.club/* // @match https://*/torrents.php?id=* // @match https://*/artist.php?id=* // @match https://*/artist.php?action=edit&artistid=* // @match https://*/reportsv2.php?action=report&id=* // @match https://*/forums.php?action=new* // @match https://*/forums.php?*action=viewthread* // @match https://*/requests.php?action=view* // @match https://*/collages.php?id=* // @match https://*/collages.php?action=edit&collageid=* // @match https://*/collages.php?action=comments&collageid=* // @match https://*/collages.php?action=new // @match http*://tracker.czech-server.com/* // @connect * // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_info // @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js // @require https://openuserjs.org/src/libs/Anakunda/progressBars.min.js // @require https://openuserjs.org/src/libs/Anakunda/imageHostUploader.min.js // @downloadURL none // ==/UserScript== 'use strict'; if (document.getElementById('upload-assistant') != null) return; // don't clash with Upload Assistant const previewDelay = GM_getValue('preview_delay', 12); const amEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*apple\.com\/(?:\S+\/)?(album|artist|playlist)\/(?:[\w\%\-]+\/)?(?:id)?(\d+)\b/i; const itunesImageMax = [/\/(\d+x\d+)\w*\.(\w+)$/, '/100000x100000-999.' + (GM_getValue('apple_get_png_cover', false) ? 'png' : '$2')]; const dzrEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*deezer\.com\/(?:\S+\/)?(album|artist|track|comment|playlist|radio|user)\/(\d+)\b/i; const dzrImageMax = GM_getValue('deezer_get_png_cover', false) ? [/\/(\d+x\d+)(?:\-\d+)*\.\w+$/, '/1400x1400.png'] : [/\/(\d+x\d+)(?:\-\d+)*(?=\.\w+$)/, '/1400x1400-000000-' + (parseInt(GM_getValue('deezer_jpeg_quality')) || 100) + '-0-0']; const discogsOrigin = 'https://www.discogs.com'; const discogsKey = GM_getValue('discogsKey', 'LWiNvIWBobGMRhfSCAiC'); const discogsSecret = GM_getValue('discogsSecret', 'HAQUKFmebpCSLyRNwjmSgOMgbnxsVQcp'); const lfmApiKey = GM_getValue('lfmApiKey', '920db0d2f86108f2fbe1917b53d63858'); const tidalClientId = GM_getValue('tidalClientId', 'PL-KYllTy1qPbCAk'); PTPimg.prototype.setSession = function() { return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin).then(({document}) => { var apiKey = document.getElementById('api_key'); if (apiKey == null) { let counter = GM_getValue('ptpimg_reminder_read', 0); if (counter < 3) { alert(` ${this.alias} API key could not be captured. Please login to ${this.origin}/ and redo the action. If you don\'t have PTPimg account or don\'t want to use it, consider to remove PTPimg from 'upload_hosts' and 'rehost_hosts' storage entries, and all sites' local hostlists where does it appear. `); GM_setValue('ptpimg_reminder_read', ++counter); } return Promise.reject('API key not configured'); } else if (!(this.apiKey = apiKey.value)) return Promise.reject('assertion failed: empty PTPimg API key'); GM_setValue('ptpimg_api_key', this.apiKey); Promise.resolve(this.apiKey) .then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) }); return this.apiKey; }); } function getDiscogsImageMax(imageUrl) { if (!httpParser.test(imageUrl)) return Promise.reject('Image URL is not valid'); if (imageUrl.endsWith('/images/spacer.gif')) return Promise.reject('Dummy image (placeholder)'); const matches = [ /^(?:https?):\/\/(?:(?:img|i)\.discogs\.com)\/.+\/([\w\%\-]+\.\w+)\b(?:\.\w+)*$/i, ].map(rx => rx.exec(imageUrl)); if (matches[0] != null) return verifyImageUrl(discogsOrigin + '/image/' + matches[0][1]).catch(reason => imageUrl); return Promise.resolve(imageUrl); } let cheveretoCustomHosts = GM_getValue('chevereto_custom_hosts'); if (cheveretoCustomHosts) { if (!Array.isArray(cheveretoCustomHosts)) try { cheveretoCustomHosts = JSON.parse(cheveretoCustomHosts); if (Array.isArray(cheveretoCustomHosts)) GM_setValue('chevereto_custom_hosts', cheveretoCustomHosts) } catch(e) { console.warnd(e) } if (Array.isArray(cheveretoCustomHosts)) for (let siteDef of cheveretoCustomHosts) if (siteDef.host_name && siteDef.alias) { const key = siteDef.alias.replace(nonWordStripper, '').toLowerCase(); imageHostHandlers[key] = new Chevereto(siteDef.host_name, siteDef.alias, siteDef.types, siteDef.size_limit, { sizeLimitAnonymous: siteDef.size_limit_anonymous, configPrefix: siteDef.config_prefix, apiEndpoint: siteDef.api_endpoint, apiFieldName: siteDef.api_field_name, apiResultKey: siteDef.api_result_key, jsonEndpoint: siteDef.json_endpoint, }); } else console.warn('Incomplete Chevereto custom site definition (excluded from chaining):', siteDef); else { console.warn('chevereto_custom_hosts invalid format (', cheveretoCustomHosts, ')'); //GM_deleteValue('chevereto_custom_hosts'); } } console.log('Image host handlers:', imageHostHandlers); const generalImgHosts = [ 'ImgBB', 'PixHost', 'ImgBox', 'FunkyIMG', 'Slowpoke', 'PostImage', 'Jerking', 'Gifyu', 'Ra', 'Abload', 'VgyMe', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS', 'PomfCat', 'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr', 'LinkPicture', 'Imgur', 'Catbox', 'ImageVenue', 'GetaPic', 'FastPic', 'SVGshare', ]; ['upload_hosts', 'rehost_hosts'].forEach(propName => { if (!GM_getValue(propName)) GM_setValue(propName, ['PTPimg'].concat(generalImgHosts).join(', ')) }); [ ['passthepopcorn.me', [ 'PTPimg', 'ImgBB', 'PixHost', 'ImgBox', 'Slowpoke', 'FunkyIMG', 'Jerking', 'Gifyu', 'Ra', 'Abload', 'VgyMe', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS', 'PomfCat', 'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr', 'Catbox', 'ImageVenue', 'GetaPic', ]], ['notwhat.cd', ['NWCD']], ['forum.mobilism.org', ['Mobilism'].concat(generalImgHosts)], ['forum.mobilism.me', ['Mobilism'].concat(generalImgHosts)], ].forEach(hostDefaults => { if (!GM_getValue(hostDefaults[0])) GM_setValue(hostDefaults[0], hostDefaults[1].join(', ')) }); var imageHosts = new ImageHostManager(logFail, GM_getValue(document.domain) || GM_getValue('upload_hosts'), GM_getValue(document.domain) || GM_getValue('rehost_hosts')); imageHostUploaderInit(inputDataHandler, textAreaDropHandler, textAreaPasteHandler, imageUrlResolver); // Set single input UI handlers let imageInputMatch = GM_getValue('image_input_match', '/(?:image|img|picture|cover|photo|avatar|poster|screen)/i'); if ((imageInputMatch = /^\/(.+)\/([dgimsuy]*)$/.exec(imageInputMatch)) != null) try { imageInputMatch = new RegExp(imageInputMatch[1], imageInputMatch[2]); for (let input of document.body.querySelectorAll('input[type="text"]')) if (['id', 'name'].some(attribute => imageInputMatch.test(input[attribute] || input.getAttribute(attribute)))) setInputHandlers(input); } catch(e) { console.warn('Image Host Helper: failed to compile image input matcher', e, imageInputMatch) } else console.warn('Image Host Helper: custom text inputs match expression not in proper regexp format; no text inputs will be handled'); // Set multiple inputs UI handlers for (let textArea of document.body.getElementsByTagName('textarea')) if (!['ua-data'].some(id => textArea.id == id) && ![ ].some(id => textArea.id == id)) setTextAreahandlers(textArea); { const tbody = document.body.querySelector('div#dynamic_form > table > tbody'); if (tbody != null) new MutationObserver(function(ml, mo) { for (let mutation of ml) for (let node of mutation.addedNodes) if (node.tagName == 'TR' && node.id.startsWith('extra_format_row')) for (let option of node.querySelectorAll('select > option')) if (option.label.startsWith('function(')) option.remove(); }).observe(tbody, { childList: true }); } // site-specific extensions switch (document.domain) { case 'passthepopcorn.me': // Auto-fill missing/invalid images from IMDB if (document.location.pathname == '/artist.php' && /^\?action=edit&artistid=(\d+)\b/i.test(document.location.search) && GM_getValue('auto_lookup_artist_image', true)) { let artistId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]'); if (input != null) verifyImageUrl(input.value).catch(function(reason) { if (input.value) input.value = ''; localXHR('/artist.php?id=' + artistId).then(function(dom) { let imdb = dom.querySelector('div#artistinfo > div.panel__body > ul.list > li > a'); if (imdb != null) imageUrlResolver(imdb.href) .then(setCover.bind(input), reason => { logFail('No IMDB photo of this artist') }); }); }); } else if (document.location.pathname == '/torrents.php' && /^\?action=editgroup&groupid=(\d+)\b/i.test(document.location.search) && GM_getValue('auto_lookup_artist_image', true)) { let groupId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]'); if (input != null) verifyImageUrl(input.value).catch(function(reason) { if (input.value) input.value = ''; localXHR('/torrents.php?id=' + groupId).then(function(dom) { let imdb = dom.querySelector('a#imdb-title-link'); if (imdb != null) imageUrlResolver(imdb.href) .then(setCover.bind(input), reason => { logFail('No IMDB poster for this movie') }); }); }); } // hook to HJ Member Toolkit new MutationObserver(function(mutationsList, mo) { for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) { if (node.nodeName != 'DIV' || !node.classList.contains('HJ-toolkit-member-toolbar')) return; mo.disconnect(); new MutationObserver(function(mutationsList, mo) { for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) { if (node.nodeName != 'DIV' || !node.classList.contains('HJ-toolkit-member-toolbar-flex')) return; mo.disconnect(); node.querySelectorAll([ //'textarea[id^="HJMA"]', 'textarea[name="screenshots"]', 'textarea[name="comparisons"]', ].join(',')).forEach(setTextAreahandlers); }); }).observe(node, { childList: true, subtree: true }); }); }).observe(document.body, { childList: true }); break; case 'redacted.ch': case 'orpheus.network': case 'notwhat.cd': case 'dicmusic.club': if (document.location.pathname == '/upload.php') document.querySelectorAll('input[type="text"][name="verification"]').forEach(setInputHandlers); // Auto-fill missing/invalid artist images else if (document.location.pathname == '/artist.php' && document.location.search.startsWith('?action=edit&') && GM_getValue('auto_lookup_artist_image', true)) { let input = document.querySelector('input[name="image"]'); if (input != null) verifyImageUrl(input.value).catch(function() { if (input.value.length > 0) input.value = ''; let artist = document.querySelector('div.header > h2 > a'); if (artist != null) artist = artist.textContent.trim(); else throw 'Artist name not found'; function resultsFilter(results0, nameExtractor) { const tailingBracketStripper = [/\s*\(([^\(\)]+)\)\s*$/, ''], norm = artist => artist && artist.replace(/\s+/g, '').toLowerCase(); const transforms = [n => n && n.replace(...tailingBracketStripper), n => n && (n = tailingBracketStripper[0].exec(n)) && n[1]]; let results = results0.filter(function(result) { let n = [artist, nameExtractor(result)].map(n => transforms.map(func => func(n))); for (let i = 0; i < 2; ++i) if (n[0][i]) for (let j = 0; j < 2; ++j) if (n[1][j] && norm(n[0][i]) == norm(n[1][j])) return true; return norm(n[0][0].toASCII()) == norm(n[1][0].toASCII()); }), f; if (results.length > 1) { f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toASCII().toLowerCase() == artist.replace(...tailingBracketStripper).toASCII().toLowerCase()); if (f.length > 0) results = f; } if (results.length > 1) { f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toLowerCase() == artist.replace(...tailingBracketStripper).toLowerCase()); if (f.length > 0) results = f; } return results; } let lookupWorkers = [ // Qobuz globalXHR('https://www.qobuz.com/shop', { responseType: 'text' }).then(function(response) { const rx = /^\s*(?:(?:window\.)?qobuz\.algolia(\d+))\s*=\s*(\{.*\});/gm; let result = [ ], m; while ((m = rx.exec(response.responseText)) != null) { let obj = JSON.parse(m[2]); if (obj.api_key && obj.application_id) result[parseInt(m[1]) - 1] = obj; } return result[0] && result[1] ? result : Promise.reject('unexpected page structure'); }).then(algolia => globalXHR('https://' + algolia[1].application_id.toLowerCase() + '-1.algolianet.com/1/indexes/' + algolia[1].index.main_artists + '/query?' + new URLSearchParams({ 'x-algolia-application-id': algolia[1].application_id, 'x-algolia-api-key': algolia[1].api_key, }).toString(), { responseType: 'json' }, { 'params': 'query=' + encodeURIComponent(artist) })).then(function({response}) { if (response.nbHits <= 0) return Promise.reject('Qobuz: no matches'); let results = resultsFilter(response.hits, result => result.name); if (results.length <= 0) return Promise.reject('Qobuz: no matches'); console.info('Qobuz search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('Qobuz: ambiguity'); if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results); return httpParser.test(results[0].image) ? results[0].image.replace(/(\/artists\/covers)\/\w+\//i, '$1/large/') : Promise.reject('Qobuz: artist exists but no photo'); }), // AllMusic globalXHR('https://www.allmusic.com/search/artists/' + encodeURIComponent(artist)).then(function({document}) { let results = resultsFilter(Array.from(document.querySelectorAll('ul.search-results > li.artist')).map(function(li) { let result = { name: li.querySelector('div.name > a'), genres: li.querySelector('div.genres'), decades: li.querySelector('div.decades'), }; Object.keys(result).forEach(key => { result[key] = result[key] != null ? result[key].textContent.trim() || undefined : undefined; }); if (result.genres) result.genres = result.genres.split(/\s*,\s*/); result.url = li.querySelector('div.name > a'); result.url = result.url != null ? result.url.href : undefined; if (/-(mw\d+)$/i.test(result.url)) result.id = RegExp.$1; result.image = li.querySelector('div.photo img'); result.image = result.image != null ? result.image.src : undefined; return result; }), result => result.name); if (results.length <= 0) return Promise.reject('AllMusic: no matches'); console.info('AllMusic search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('AllMusic: ambiguity'); if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results); if (!httpParser.test(results[0].image)) return Promise.reject('AllMusic: artist exists but no photo'); return verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=6')) .catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=0'))) .catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=5'))); }), // NetEase globalXHR('https://music.163.com/api/cloudsearch/get/web?' + new URLSearchParams({ s: '"' + artist + '"', type: 100, limit: 25, //csrf_token: '', }).toString(), { responseType: 'json' }).then(function({response}) { if (response.code != 200 || !response.result) return Promise.reject('API returns malformed result (' + response.msg + ')'); return !response.abroad ? response.result : new Promise(function(resolve, reject) { function onCoreLoaded(elem = coreJS) { if ([/*'asrsea', */'settmusic'].every(function(pubSym) { try { return typeof eval(pubSym) == 'function' } catch(e) { return false } })) resolve(elem); else reject('core.js public functions not available'); } function injectScript(src, errorHandler = reject) { if (!httpParser.test(src)) throw 'Assertion failed: invalid src'; coreJS = document.createElement('script'); coreJS.id = 'netease.core.js'; coreJS.type = 'text/javascript'; coreJS.async = false; coreJS.onload = evt => { onCoreLoaded(evt.target) }; coreJS.onerror = function(evt) { document.head.removeChild(evt.target); console.error('Netease core.js (' + src + ') loading error', evt); if (typeof errorHandler == 'function') errorHandler(evt); }; coreJS.src = src; document.head.append(coreJS); } var coreJS = document.getElementById('netease.core.js'); if (coreJS != null) return onCoreLoaded(); injectScript('https://s3.music.126.net/web/s/core.js', function(evt) { console.warn('NetEase generic core.js load failed, trying to fetch proper url from root doc'); globalXHR('https://music.163.com/').then(function({document}) { var script = document.querySelector(':root > body > script[src*="/core"]'); if (script != null) injectScript(script.src, evt => { reject('NetEase core.js loading error') }); else reject('invalid root document structure'); }, reject); }); }).then(core => JSON.parse(decodeURIComponent(settmusic(response.result, 'fuck~#$%^&*(458')))); }).then(result => result.artistCount > 0 ? result.artists : Promise.reject('NetEase: no matches'), function(reason) { console.warn('NetEase search-list method failed:', reason); return globalXHR('https://music.163.com/api/search/suggest/web?'+ new URLSearchParams({ s: '"' + artist + '"', type: 100, limit: 25, //csrf_token: '', }, { responseType: 'json' })).then(function({response}) { if (response.code != 200 || !response.result) return Promise.reject('API returns malformed result (' + response.msg + ')'); return Array.isArray(response.result.artists) && response.result.artists.length > 0 ? response.result.artists : Promise.reject('NetEase: no matches'); }); }).then(function(artists) { console.assert(Array.isArray(artists) && artists.length > 0, "Array.isArray(artists) && artists.length > 0"); if (!Array.isArray(artists) || artists.length <= 0) return Promise.reject('NetEase: no matches'); let results = resultsFilter(artists/*.filter(artist => artist.picId > 0)*/, result => result.name); if (results.length <= 0) return Promise.reject('NetEase: no matches'); console.info('NetEase search results for "' + artist + '":', artists); if (results.length > 1) return Promise.reject('NetEase: ambiguity'); //if (results.length > 1) console.info('NetEase returns ambiguous results for "' + artist + '":', artists); const imgMax = imgUrl => imgUrl.replace(/\?.*$/, '').replace(/\b(?:p[123])(?=\.music\.\d+\.net\b)/i, 'p4'); const isDummy = imgUrl => ['/5639395138885805.jpg'].some(dummy => imgUrl.toLowerCase().endsWith(dummy)); if (artists[0].picId > 0 && httpParser.test(artists[0].picUrl) && !isDummy(artists[0].picUrl)) return imgMax(artists[0].picUrl); if (artists[0].img1v1 > 0 && httpParser.test(artists[0].img1v1Url) && !isDummy(artists[0].img1v1Url)) return imgMax(artists[0].img1v1Url); return Promise.reject('NetEase: artist exists but no photo'); }), // Tidal globalXHR('https://listen.tidal.com/v1/search/artists?' + new URLSearchParams({ query: artist, limit: 25, locale: 'en_US', countryCode: 'US', deviceType: 'BROWSER', token: tidalClientId, }), { responseType: 'json' }).then(function({response}) { if (response.totalNumberOfItems <= 0) return Promise.reject('Tidal: no matches'); let results = resultsFilter(response.items, item => item.name); if (results.length <= 0) return Promise.reject('Tidal: no matches'); console.info('Tidal search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('Tidal: ambiguity'); if (results.length > 1) console.info('Tidal returns ambiguous results for "' + artist + '":', results); if (!results[0].picture) return Promise.reject('Tidal: artist exists but no photo'); return 'https://resources.tidal.com/images/' + results[0].picture.replace(/-/g, '/') + '/750x750.jpg'; }), // Discogs globalXHR('https://api.discogs.com/database/search?' + new URLSearchParams({ query: artist, type: 'artist', sort: 'score,desc', strict: false, }).toString(), { responseType: 'json', headers: { 'Authorization': 'Discogs key="' + discogsKey + '", secret="' + discogsSecret + '"' }, }).then(({response}) => { if (response.items <= 0) return Promise.reject('Discogs: no matches'); let results = resultsFilter(response.results.filter(result => result.type == 'artist'), result => result.title); if (results.length <= 0) return Promise.reject('Discogs: no matches'); console.info('Discogs search results for "' + artist + '":', results); //if (results.length > 1) return Promise.reject('Discogs: ambiguity'); if (results.length > 1) console.info('Discogs returns ambiguous results for "' + artist + '":', results); return Promise.all(results.map(result => { if (result.cover_image.includes('/spacer.gif')) return null; return getDiscogsImageMax(result.cover_image); }).filter(Boolean)).then(artistCovers => httpParser.test(artistCovers[0]) ? artistCovers[0] : Promise.reject('Discogs: artist exists but no photo')); }), // Bandcamp globalXHR('https://bandcamp.com/search?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) { const results = resultsFilter(Array.from(document.querySelectorAll('div.results > ul.result-items > li.searchresult')).map(function(li) { try { var result = JSON.parse(li.dataset.search); if (result.type.toLowerCase() != 'b') return; } catch(e) { result = { }; // return; console.warn('Bandcamp: could not detect search result type', li); } if (!result.id) try { if (/\b(?:id)=(\d+)\b/.test(li.previousSibling.previousSibling.nodeValue)) result.id = parseInt(RegExp.$1); } catch(e) { } let ref = li.querySelector('div.art > img'); if (ref != null) result.imageUrl = ref.src.replace(/_\d+(\.\w+$)/, '_0'); if ((ref = li.querySelector('div.heading > a')) != null) { result.url = new URL(ref); result.url.search = ''; result.name = ref.textContent.trim(); } if ((ref = li.querySelector('div.subhead')) != null) result.location = ref.textContent.trim(); if ((ref = li.querySelector('div.genre')) != null) result.genre = ref.textContent.trim().replace(/^(?:Genre:\s+)/i, ''); if ((ref = li.querySelector('div.tags')) != null) result.tags = ref.textContent.trim().replace(/^(?:tags):\s+/, '').split(/\s*,\s*/); if (result.name) return result; }).filter(Boolean), result => result.name); if (results.length <= 0) return Promise.reject('Bandcamp: no matches'); console.info('Bandcamp search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('Bandcamp: ambiguity'); if (results.length > 1) console.info('Bandcamp returns ambiguous results for "' + artist + '":', results); return httpParser.test(results[0].imageUrl) ? results[0].imageUrl : Promise.reject('Bandcamp: artist exists but no photo'); }), // Beatport globalXHR('https://www.beatport.com/search/artists?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) { const results = resultsFilter(Array.from(document.querySelectorAll('div.bucket.artists > ul.bucket-items > li.bucket-item')).map(function(li) { let result = { id: li.dataset.ecId, name: li.dataset.ecName, url: li.querySelector(':scope > a'), imageUrl: li.querySelector(':scope > a > img'), }; if (result.url != null) { result.url = new URL(result.url); result.url.hostname = 'www.beatport.com'; } else delete result.url; result.imageUrl = result.imageUrl != null ? result.imageUrl.src.replace(/\/image_size\/\d+x\d+\//, '/image/') : undefined; return result; }), result => result.name); if (results.length <= 0) return Promise.reject('Beatport: no matches'); console.info('Beatport search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('Beatport: ambiguity'); if (results.length > 1) console.info('Beatport returns ambiguous results for "' + artist + '":', results); return httpParser.test(results[0].imageUrl) && ![ '/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg', '/d02c012b-67d4-4058-a75f-3fbabdb8d19d.jpg', ].some(id => results[0].imageUrl.endsWith(id)) ? results[0].imageUrl : Promise.reject('Beatport: artist exists but no photo'); }), // iTunes globalXHR('https://itunes.apple.com/search?' + new URLSearchParams({ term: '"' + artist + '"', media: 'music', entity: 'musicArtist', attribute: 'artistTerm', //country: 'US', }).toString(), { responseType: 'json' }).then(function({response}) { if (response.resultCount <= 0) return Promise.reject('iTunes: no matches'); let results = resultsFilter(response.results.filter(result => result.wrapperType == 'artist' && result.artistType == 'Artist'), result => result.artistName); if (results.length <= 0) return Promise.reject('iTunes: no matches'); console.info('iTunes search results for "' + artist + '":', results); //if (results.length > 1) return Promise.reject('iTunes: ambiguity'); if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results); return imageUrlResolver(results[0].artistLinkUrl); }), // Spotify (function() { const isTokenValid = accessToken => typeof accessToken == 'object' && accessToken.token_type && accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000; try { var accessToken = JSON.parse(window.localStorage.spotifyAccessToken); if (isTokenValid(accessToken)) return Promise.resolve(accessToken); } catch(e) { } const clientId = GM_getValue('spotify_clientid', '54468e0c92c24e0d86c61346155b32cd'), clientSecret = GM_getValue('spotify_clientsecret', '38cb34c7196d4cdca6dbb35b08e29e12'); if (!clientId || !clientSecret) return Promise.reject('Spotify credentials not configured'); const data = new URLSearchParams({ 'grant_type': 'client_credentials', }), timeStamp = Date.now(); return globalXHR('https://accounts.spotify.com/api/token', { responseType: 'json', headers: { Authorization: 'Basic ' + btoa(clientId + ':' + clientSecret), } }, data).then(function({response}) { accessToken = response; const tzOffset = new Date().getTimezoneOffset() * 60 * 1000; if (!accessToken.timestamp) accessToken.timestamp = timeStamp; //else accessToken.timestamp -= tzOffset; if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp + accessToken.expires_in * 1000; else accessToken.expires_at -= tzOffset; if (!isTokenValid(accessToken)) { console.warn('Received invalid Spotify token:', accessToken); return Promise.reject('invalid token received'); } window.localStorage.spotifyAccessToken = JSON.stringify(accessToken); return accessToken; }); })().then(credentials => globalXHR('https://api.spotify.com/v1/search?' + new URLSearchParams({ q: artist, type: 'artist', }).toString(), { responseType: 'json', headers: { Accept: 'application/json', Authorization: credentials.token_type + ' ' + credentials.access_token, }, })).then(function({response}) { if (response.artists.total <= 0) return Promise.reject('Spotify: no matches'); let results = resultsFilter(response.artists.items.filter(item => item.type == 'artist'), item => item.name); if (results.length <= 0) return Promise.reject('Spotify: no matches'); console.info('Spotify search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('Spotify: ambiguity'); if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results); return results[0].images && results[0].images.length > 0 ? results[0].images.sort((a, b) => (b.width * b.height) - (a.width * a.height))[0].url : Promise.reject('Spotify: artist exists but no photo'); }), // Deezer globalXHR('https://api.deezer.com/search/artist?' + new URLSearchParams({ q: artist, order: 'RANKING', //strict: 'on', }).toString(), { responseType: 'json' }).then(function({response}) { if (response.total <= 0) return Promise.reject('Deezer: no matches'); let results = resultsFilter(response.data.filter(result => result.type == 'artist'), result => result.name); if (results.length <= 0) return Promise.reject('Deezer: no matches'); console.info('Deezer search results for "' + artist + '":', results); //if (results.length > 1) return Promise.reject('Deezer: ambiguity'); if (results.length > 1) console.info('Deezer returns ambiguous results for "' + artist + '":', results); return verifyImageUrl(results[0].picture).catch(function(reason) { console.warn('Deezer API image retrieval failed:', reason); return ['xl', 'big', 'medium', 'small'].reduce((acc, size) => acc || response.data[0]['picture_' + size], null) || Promise.reject('no picture'); }).then(imageUrl => imageUrl.includes('/images/artist//') ? Promise.reject('Deezer: artist exists but no photo') : getDeezerImageMax(imageUrl)); }), // FLO globalXHR('https://www.music-flo.com/api/search/v2/search?' + new URLSearchParams({ keyword: '"' + artist + '"', searchType: 'ARTIST', sortType: 'ACCURACY', size: 10, }).toString(), { responseType: 'json' }).then(function({response}) { if (response.code != 2000000) return Promise.reject(response.message); //if (response.data.totalCount <= 0) return Promise.reject('FLO: no matches'); console.assert(Array.isArray(response.data.list), 'Array.isArray(response.data.list)', response); let results = resultsFilter(response.data.list[0].list, result => result.name); if (results.length <= 0) return Promise.reject('FLO: no matches'); console.info('FLO search results for "' + artist + '":', response.data); if (results.length > 1) return Promise.reject('FLO: ambiguity'); //if (results.length > 1) console.info('FLO returns ambiguous results for "' + artist + '":', results); const noPhoto = Promise.reject('FLO: artist exists but no photo'); if (!Array.isArray(results[0].imgList) || results[0].imgList.length <= 0) return noPhoto; const imageUrl = results[0].imgList.reduce((acc, image) => image.url.replace(/\?.*$/, '')); return !imageUrl.includes('/000000000/000000000.') ? imageUrl : noPhoto; }), // OTOTOY globalXHR('https://ototoy.jp/find/?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) { const results = resultsFilter(Array.from(document.querySelectorAll('div.results_box > div.find-artist div.find-candidates')).map(function(div) { let result = { url: div.querySelector('div.artist-name > a'), imageUrl: div.querySelector('figure > a > img'), }; if (result.url != null) { result.name = result.url.title || result.url.textContent.trim(); result.url = new URL(result.url); result.url.hostname = 'ototoy.jp'; result.id = /\/a\/(\d+)\b/i.exec(result.url.pathname); if (result.id != null) result.id = parseInt(result.id[1]); else delete result.id; } else delete result.url; if (result.imageUrl != null) { result.imageUrl = new URL(result.imageUrl.src); result.imageUrl = result.imageUrl.origin + new URLSearchParams(result.imageUrl.search).get('image'); } else delete result.imageUrl; if (result.name) return result; }).filter(Boolean), result => result.name); if (results.length <= 0) return Promise.reject('OTOTOY: no matches'); console.info('OTOTOY search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('OTOTOY: ambiguity'); //if (results.length > 1) console.info('OTOTOY returns ambiguous results for "' + artist + '":', results); return httpParser.test(results[0].imageUrl) && !results[0].imageUrl.endsWith('/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg') ? results[0].imageUrl : Promise.reject('OTOTOY: artist exists but no photo'); }), // Recochoku globalXHR('https://recochoku.jp/search/artist?q=' + encodeURIComponent(artist)).then(({document}) => Array.from(document.querySelectorAll('ul#artistContents > li > a')).map(function(a) { let result = { url: new URL(a.pathname, 'https://recochoku.jp'), id: /\/artist\/(\d+)\b/i.exec(a.pathname), name: a.querySelector('div > div[class$="title"]'), imageUrl: a.getElementsByTagName('IMG'), }; if (result.name) result.name = result.name.textContent.trim(); else return null; if (result.imageUrl.length > 0) { result.imageUrl = new URL(result.imageUrl[0].dataset.src); result.imageUrl.searchParams.set('FFw', 999999999); result.imageUrl.searchParams.set('FFh', 999999999); result.imageUrl.searchParams.delete('h'); result.imageUrl.searchParams.delete('option'); } else return null; if (result.id != null) result.id = result.id[1]; else delete result.id; return result; }).filter(Boolean)).then(function(results) { if (results.length <= 0) return Promise.reject('Recochoku: no matches'); console.info('Recochoku search results for "' + artist + '":', results); results = resultsFilter(results, result => result.name); if (results.length <= 0) return Promise.reject('Recochoku: no matches'); if (results.length > 1) return Promise.reject('Recochoku: ambiguity'); //if (results.length > 1) console.info('Recochoku returns ambiguous results for "' + artist + '":', results); return httpParser.test(results[0].imageUrl) && !results[0].imageUrl.endsWith('/noimage_artist.png') ? results[0].imageUrl : Promise.reject('Recochoku: artist exists but no photo'); }), // QQ音乐 globalXHR('https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + new URLSearchParams({ format: 'json', t: 9, w: artist, inCharset: 'utf8', outCharset: 'utf-8', }).toString(), { responseType: 'json' }).then(function({response}) { if (response.data.singer.totalnum <= 0) return Promise.reject('QQ音乐: no matches'); console.info('QQ音乐 search results for "' + artist + '":', response.data.singer); const results = resultsFilter(response.data.singer.list, singer => singer.singerName); if (results.length <= 0) return Promise.reject('QQ音乐: no matches'); //if (results.length > 1) return Promise.reject('QQ音乐: ambiguity'); if (results.length > 1) console.info('QQ音乐 returns ambiguous results for "' + artist + '":', results); if (results[0].singerPic && results[0].singerPic.endsWith('M000003kfNgb0XXvgV_0.jpg')) return Promise.reject('QQ音乐: artist exists but no real photo'); return verifyImageUrl(results[0].singerPic.replace(/R\d+x\d+/, '')) .catch(reason => verifyImageUrl(results[0].singerPic)) .catch(reason => Promise.reject('QQ音乐: artist exists but no photo')); }), // YouTube Music (function() { if ('ytcfg' in sessionStorage) try { return Promise.resolve(JSON.parse(sessionStorage.ytcfg)) } catch(e) { console.warn('Invalid ytcfg format:', e) } return globalXHR('https://music.youtube.com/').then(function({document}) { for (let script of document.querySelectorAll('head > script[nonce]')) { let ytcfg = /^\s*\b(?:ytcfg\.set)\s*\(\s*(\{.+\})\s*\);/m.exec(script.text); if (ytcfg != null) try { ytcfg = JSON.parse(ytcfg[1]); if (ytcfg.INNERTUBE_API_KEY) { sessionStorage.ytcfg = JSON.stringify(ytcfg); return ytcfg; } else console.warn('YouTube Music API key missing:', ytcfg); } catch(e) { console.warn('Error parsing ytcfg:', ytcfg[1]) } } return Promise.reject('unable to extract YouTube config ot the config is invalid'); }); })().then(ytcfg => globalXHR('https://music.youtube.com/youtubei/v1/search?' + new URLSearchParams({ alt: 'json', key: ytcfg.INNERTUBE_API_KEY, }).toString(), { responseType: 'json', headers: { 'Referer': 'https://music.youtube.com/' }, }, { query: artist, params: encodeURIComponent('EgWKAQIgAWoKEAkQChADEAUQBA=='), context: { activePlayers: { }, capabilities: { }, client: Object.assign({ experimentIds: [ ], experimentsToken: "", locationInfo: { locationPermissionAuthorizationStatus: "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED", }, musicAppInfo: { musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE", pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN", }, utcOffsetMinutes: -new Date().getTimezoneOffset(), }, ytcfg.INNERTUBE_CONTEXT.client, { hl: 'en' }), request: { internalExperimentFlags: [ { key: "force_music_enable_outertube_search", value: "true" } ], }, user: { enableSafetyMode: false }, }, })).then(({response}) => response.contents.sectionListRenderer.contents[0].musicShelfRenderer.contents.map(function(item) { let result = { id: item.musicResponsiveListItemRenderer.navigationEndpoint.browseEndpoint.browseId, name: item.musicResponsiveListItemRenderer.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text, photoUrl: item.musicResponsiveListItemRenderer.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, }; result.webUrl = result.id ? 'https://music.youtube.com/channel/' + result.id : undefined; result.photoUrl = Array.isArray(result.photoUrl) && result.photoUrl.length > 0 ? result.photoUrl[0].url.replace(/(?:=[swh]\d+.*)?$/, '=s0') : undefined; return result; })).then(function(results) { if (results.length <= 0) return Promise.reject('YouTube Music: no matches'); results = resultsFilter(results, result => result.name); if (results.length <= 0) return Promise.reject('YouTube Music: no matches'); console.info('YouTube Music search results for "' + artist + '":', results); if (results.length > 1) return Promise.reject('YouTube Music: ambiguity'); if (results.length > 1) console.info('YouTube Music returns ambiguous results for "' + artist + '":', results); return httpParser.test(results[0].photoUrl) ? results[0].photoUrl : Promise.reject('YouTube Music: artist exists but no photo'); }), // Last.fm globalXHR('http://ws.audioscrobbler.com/2.0/?' + new URLSearchParams({ method: 'artist.getinfo', artist: artist, format: 'json', api_key: lfmApiKey, }).toString(), { responseType: 'json' }).then(function({response}) { if (response.error) return Promise.reject(response.message); console.info('Last.fm search result for "' + artist + '":', response.artist); const rx = /\/(\d+)x(\d+)\//; let biggest = response.artist.image.map(im => im['#text']).reduce(function(a, b) { let r = [a, b].map(RegExp.prototype.exec.bind(rx)) .map(r => r != null ? parseInt(r[1]) * parseInt(r[2]) : -Infinity); return r[1] > r[0] ? b : a; }); return rx.test(biggest) && !biggest.endsWith('/2a96cbd8b46e442fc41c2b86b821562f.png') ? biggest : Promise.reject('Last.fm: artist exists but no photo'); }), ]; let imageLookupChain = GM_getValue('artist_image_lookup_providers'); if (!imageLookupChain || imageLookupChain == 'all') imageLookupChain = lookupWorkers; else { if (typeof imageLookupChain == 'string') imageLookupChain = imageLookupChain.split(/\W+/); if (!Array.isArray(imageLookupChain) || (imageLookupChain = imageLookupChain.map(key => lookupWorkers[key]).filter(Boolean)).length <= 0) return Promise.reject('No image lookup providers matching user list'); } const lookUp = (index = 0) => index >= 0 && index < imageLookupChain.length ? imageLookupChain[index].then(setCover.bind(input)).catch(reason => lookUp(index + 1)) : Promise.reject('Image of this artist was not found'); return lookUp(); }).catch(logFail); } break; case 'tracker.czech-server.com': if (document.location.pathname == '/upload2.php') document.querySelectorAll('input[type="text"][name="urlobr"]').forEach(setInputHandlers); break; } switch (document.location.pathname) { case '/torrents.php': { if (!document.location.search.startsWith('?id=')) break; const addCoversForm = document.getElementById('add_cover'); if (addCoversForm != null) new MutationObserver(function(mutationsList, mo) { for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) { if (node.nodeName == 'INPUT' && node.type == 'text' && node.name == 'image[]') setInputHandlers(node); }); }).observe(addCoversForm, { childList: true }); break; } case '/reportsv2.php': { const dynaForm = document.getElementById('dynamic_form'); if (dynaForm == null) break; function setReportHandlers(root = dynaForm) { root.querySelectorAll('input[id*="image"]').forEach(setInputHandlers); for (let ta of root.getElementsByTagName('TEXTAREA')) setTextAreahandlers(ta); } new MutationObserver(function(mutationsList, mo) { for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) { if (node.nodeType == Node.ELEMENT_NODE) setReportHandlers(node); }); }).observe(dynaForm, { childList: true }); break; } case '/forums.php': { if (!document.location.search.startsWith('?action=viewthread&')) break; let container = document.querySelector('div#content > div.thin'); if (container != null) new MutationObserver(function(mutationsList, mo) { for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) { if (node.nodeName == 'FORM') for (let elem of node.getElementsByTagName('TEXTAREA')) setTextAreahandlers(elem); }); }).observe(container, { childList: true, subtree: true }); break; } } let opti_PNG = GM_getValue('optipng', false); function imagePreview(imgUrl, size) { if (previewDelay <= 0) return; let div = document.getElementById('image-preview'); if (div != null) document.body.removeChild(div); if (!httpParser.test(imgUrl)) return; div = document.createElement('div'); div.id = 'image-preview'; div.style = 'position: fixed; bottom: 20px; right: 20px; border: thin solid silver; ' + 'background-color: #8888; padding: 10px; opacity: 0; transition: opacity 1s ease-in-out; z-index: 999999999;'; const cleanUp = function(div) { if (div.parentNode == null) return; div.style.opacity = 0; setTimeout(div => { document.body.removeChild(div) }, 1000, div); }; div.ondblclick = evt => { cleanUp(evt.currentTarget) }; let img = document.createElement('img'); img.style = 'width: 225px;'; img.onload = function(evt) { if (evt.currentTarget.parentNode.parentNode == null) document.body.append(evt.currentTarget.parentNode); setTimeout(div => { div.style.opacity = 1 }, 0, evt.currentTarget.parentNode); setTimeout(cleanUp, (previewDelay || 12) * 1000, evt.currentTarget.parentNode); if (!evt.currentTarget.naturalWidth || !evt.currentTarget.naturalHeight) return; // invalid image let info = document.createElement('div'); info.id = 'image-info'; info.style = 'text-align: center; background-color: #29434b; padding: 5px; color: white;' + 'font: 500 10pt "Segoe UI", Verdana, sans-serif;'; evt.currentTarget.parentNode.append(info); const resolution = evt.currentTarget.naturalWidth + '×' + evt.currentTarget.naturalHeight; (size > 0 ? Promise.resolve(size) : size instanceof Promise ? size : getRemoteFileSize(imgUrl)).then(function(size) { if (!(size >= 0)) throw 'invalid size'; let imageSizeLimit = GM_getValue('image_size_reduce_threshold'), html = resolution + ' ( 0 && size > imageSizeLimit * 2**10) html += ' style="color: red;"'; html += '>' + formattedSize(size) + ')'; info.innerHTML = html; }).catch(reason => { info.textContent = resolution }); }; img.onerror = evt => { console.warn('Image source couldnot be loaded:', evt, imgUrl) }; img.src = imgUrl; div.append(img); } function writeInfo() { let input = document.querySelector('input[name="summary"]'); if (input != null && !input.disabled && !input.value) input.value = 'Image update/rehost'; } function setCover(url) { return verifyImageUrl(url).then(imageUrl => { this.value = imageUrl; writeInfo(); let size = getRemoteFileSize(imageUrl); imagePreview(imageUrl, size); return checkImageSize(imageUrl, this, size).then(imageUrl => { this.disabled = true; return imageHosts.rehostImages([imageUrl]).then(singleImageGetter).then(imageUrl => { if (imageUrl == null) throw 'invalid image'; this.value = imageUrl; }); }).catch(reason => { this.value = imageUrl; logFail(reason + ' (not rehosted)'); }).then(() => { this.disabled = false; return imageUrl; }); }); } function inputDataHandler(evt, data) { const input = evt.currentTarget; console.assert(input instanceof HTMLInputElement, 'input instanceof HTMLInputElement'); const rehoster = imageUrl => imageHosts.rehostImages([imageUrl]).then(singleImageGetter).then(function(imageUrl) { if (!httpParser.test(imageUrl)) { console.warn('rehostImages returns invalid image URL:', imageUrl); throw 'invalid image URL'; } input.value = imageUrl; writeInfo(); }); if (!data) return true; if (data.files.length > 0) { if (data.files[0].type && !data.files[0].type.startsWith('image/')) return true; input.disabled = true; if (input.hTimer) { clearTimeout(input.hTimer); delete input.hTimer; } input.style.color = 'white'; input.style.backgroundColor = 'darkred'; let progressBar = { }; function progressHandler(worker, param = null) { if (param && typeof param == 'object') { if (param.readyState > 1 || progressBar.current != undefined && worker !== progressBar.current || Date.now() < progressBar.lastUpdate + 100) return; let pct = Math.floor(Math.min(param.done * 100 / param.total, 100)); if (pct <= progressBar.lastPct) return; input.value = 'Uploading... [' + (progressBar.lastPct = pct) + '%]'; progressBar.lastUpdate = Date.now(); } else if (param == null) { progressBar = { current: worker }; input.value = 'Uploading...'; } } const file = data.files[0]; input.disabled = true; checkImageSize(file, input, progressHandler).catch(function(reason) { logFail('Downsizing of source image not possible (' + reason + '), uploading original size'); return file; }).then(function(result) { const uploader = file => imageHosts.uploadImages([file], progressHandler).then(singleImageGetter).then(function(imageUrl) { input.value = imageUrl; imagePreview(imageUrl, file.size); writeInfo(); }); if (httpParser.test(result)) return rehoster(result).catch(function(reason) { logFail('Downsizing of source image failed (' + reason + '), uploading original size'); return uploader(file); }); if (result instanceof File) return uploader(result); console.warn('invalid checkImageSize(...) result:', result); return Promise.reject('invalid checkImageSize(...) result'); }).then(function() { input.style.backgroundColor = '#008000'; input.hTimer = setTimeout(function() { input.style.backgroundColor = null; input.style.color = null; delete input.hTimer; }, 10000); }, function(reason) { imageClear(evt); input.style.backgroundColor = null; input.style.color = null; Promise.resolve(reason).then(msg => { alert(msg) }); }).then(() => { input.disabled = false }); return false; } else if (data.items.length > 0) { let urls = data.getData('text/uri-list'); if (urls) urls = urls.split(/\r?\n/); else { urls = data.getData('text/x-moz-url'); if (urls) urls = urls.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0); else if (urls = data.getData('text/plain')) urls = urls.split(/\r?\n/); } if (!Array.isArray(urls) || urls.length <= 0) return true; input.disabled = true; console.time('Image URL Rehoster'); imageUrlResolver(urls[0], { altKey: evt.altKey, ctrlKey: evt.ctrlKey != (input.name == 'image[]'), shiftKey: evt.shiftKey, }).then(verifyImageUrl).then(function(imageUrl) { input.disabled = true; input.value = imageUrl; const size = getRemoteFileSize(imageUrl); imagePreview(imageUrl, size); checkImageSize(imageUrl, input, size).then(rehoster).catch(function(reason) { input.value = imageUrl; Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') }); }).then(() => { console.timeEnd('Image URL Rehoster') }); }).catch(reason => { Promise.resolve(reason).then(alert) }).then(() => { input.disabled = false }); return false; } return true; } unsafeWindow.imageHostHelper = { }; const defEndpoint = (publicName, localRef) => { unsafeWindow.imageHostHelper[publicName] = localRef }; defEndpoint('uploadFiles', function uploadImages(files, checkSize = true, preview = false) { if (!Array.isArray(files)) return Promise.reject('Invalid parameter (files)'); if ((files = files.filter(file => file instanceof File && file.type.startsWith('image/'))).length <= 0) return Promise.reject('Invalid parameter (no valid images passed)'); console.time('Image uploader'); return checkSize || preview ? Promise.all(files.map(file => (checkSize ? checkImageSize(file).catch(function(reason) { logFail('Downsizing of source image not possible (' + reason + '), uploading original size'); return file; }) : Promise.resolve(file)).then(function(result) { const uploader = file => imageHosts.uploadImages([file]).then(singleImageGetter).then(function(imageUrl) { if (preview) imagePreview(imageUrl, file.size); return imageUrl; }); if (httpParser.test(result)) return imageHosts.rehostImages([result]).catch(function(reason) { logFail('Downsizing of source image failed (' + reason + '), uploading original size'); return uploader(file); }); if (result instanceof File) return uploader(result); console.warn('invalid checkImageSize(...) result:', result); return Promise.reject('invalid checkImageSize(...) result'); }))) : imageHosts.uploadImages(files); }); defEndpoint('rehostImageLinks', function rehostImageLinks(urls, checkSize = true, preview = false, enforceRehost = true, modifiers) { if (!Array.isArray(urls) || urls.length <= 0) return Promise.reject('Invalid parameter (urls)'); console.time('Image URL rehoster'); return Promise.all(urls.map(url => imageUrlResolver(url, { altKey: Boolean(typeof modifiers == 'object' && modifiers.altKey), ctrlKey: Boolean(typeof modifiers == 'object' && modifiers.ctrlKey), shiftKey: Boolean(typeof modifiers == 'object' && modifiers.shiftKey), }).then(verifyImageUrl).then(function(imageUrl) { if (!checkSize) return imageUrl; const size = getRemoteFileSize(imageUrl); if (preview) imagePreview(imageUrl, size); return checkImageSize(imageUrl, null, size); }))).then(imageUrls => imageHosts.rehostImages(imageUrls).then(function(rehostedImages) { console.timeEnd('Image URL rehoster'); return rehostedImages; }, reason => enforceRehost ? Promise.reject(reason) : Promise.resolve(imageUrls))); }); defEndpoint('imageHostHandlers', imageHostHandlers); defEndpoint('uploadImages', ImageHostManager.prototype.uploadImages.bind(imageHosts)); defEndpoint('rehostImages', ImageHostManager.prototype.rehostImages.bind(imageHosts)); defEndpoint('logFail', logFail); defEndpoint('getDeezerImageMax', getDeezerImageMax); defEndpoint('getDiscogsImageMax', getDiscogsImageMax); defEndpoint('dzrImageMax', dzrImageMax); defEndpoint('itunesImageMax', itunesImageMax); defEndpoint('verifyImageUrl', verifyImageUrl); defEndpoint('urlResolver', urlResolver); defEndpoint('imageUrlResolver', imageUrlResolver); defEndpoint('getRemoteFileType', getRemoteFileType); defEndpoint('getRemoteFileSize', getRemoteFileSize); defEndpoint('verifyImageUrl', verifyImageUrl); defEndpoint('checkImageSize', checkImageSize); defEndpoint('reduceImageSize', reduceImageSize); defEndpoint('optiPNG', optiPNG); defEndpoint('directLinkGetter', directLinkGetter); defEndpoint('singleImageGetter', singleImageGetter); const meta = document.createElement('META'); meta.name = 'ImageHostHelper'; meta.content = 'All endpoints exported'; meta.setAttribute('propertyname', 'imageHostHelper'); document.head.append(meta); function rehoster(promises, resultsHandler, target = null) { if (!Array.isArray(promises)) throw 'invalid parameter'; console.time('Image URL Resolver'); return Promise.all(promises).then(function(resolved) { let resolvedUrls = resolved.flatten(); if (target instanceof HTMLElement) { target.disabled = true; if (resolvedUrls.length > 1 && !['notwhat.cd'].some(hostname => document.domain == hostname)) var progressBar = new RHProgressBar(target, resolvedUrls.length); } return (function() { if (!opti_PNG || !(target instanceof HTMLElement)) return Promise.resolve(resolvedUrls); return Promise.all(resolvedUrls.map(resolvedUrl => optiPNG(resolvedUrl).catch(reason => resolvedUrl))); })().then(srcUrls => imageHosts.rehostImages(srcUrls, RHProgressBar.prototype.update.bind(progressBar)).catch(function(reason) { logFail(reason + ' (not rehosted)'); RHProgressBar.prototype.update.call(progressBar, -1, false); return verifyImageUrls(srcUrls); }).then(function(results) { resolved.forEach(function(elem, index) { if (!elem.caption) return; if (!Array.isArray(results.captions)) results.captions = [ ]; results.captions.push(elem.caption); }); resultsHandler(results, arrayGrouping(resolved).flatten()); }).catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })).then(function() { RHProgressBar.prototype.cleanUp.call(progressBar); if (target instanceof HTMLElement) target.disabled = false; console.timeEnd('Image URL Resolver'); }); }); } function textAreaDropHandler(evt) { if (!evt.dataTransfer || evt.shiftKey) return true; const textArea = evt.currentTarget; console.assert(textArea instanceof HTMLTextAreaElement, 'textArea instanceof HTMLTextAreaElement'); if (evt.dataTransfer.files.length > 0) { let images = Array.from(evt.dataTransfer.files).filter(file => !file.type || file.type.startsWith('image/')); if (images.length <= 0) return true; textArea.disabled = true; if (!['notwhat.cd'].some(hostname => document.domain == hostname)) var progressBar = new ULProgressBar(textArea, images.map(image => image.size)); (function() { if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG'); ULProgressBar.prototype.update.call(progressBar, -1); return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) => ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler); })().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler)) .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) }) .then(function() { ULProgressBar.prototype.cleanUp.call(progressBar); textArea.disabled = false; }); evt.stopPropagation(); return false; } else if (evt.dataTransfer.items.length > 0) { let content = evt.dataTransfer.getData('text/uri-list'); if (content) content = content.split(/(?:\r?\n)+/); else { content = evt.dataTransfer.getData('text/x-moz-url'); if (content) content = content.split(/(?:\r?\n)+/).filter((item, ndx) => ndx % 2 == 0); }; if (!Array.isArray(content) || content.length <= 0) return true; rehoster(content.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, textArea).catch(function(reason) { if (evt.ctrlKey) textArea.value = textArea.value.slice(0, evt.rangeOffset) + content.join('\n') + textArea.value.slice(evt.rangeOffset); else { if (textArea.value.length > 0) textArea.value += '\n\n'; textArea.value += content.join('\n'); } }); evt.stopPropagation(); return false; } return true; function resultsHandler(results, groups = undefined) { if (results.length <= 0) return; if (evt.altKey && !textArea.noBBCode) { let modal = document.createElement('div'); modal.id = 'ihh-template-selector-background'; modal.style = 'position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: #0008;' + 'opacity: 0; transition: opacity 0.15s linear;'; modal.innerHTML = `
`; document.body.append(modal); let form = document.getElementById('ihh-template-selector'), btnInsert = form.querySelector('input#btn-insert'), btnCancel = form.querySelector('input#btn-cancel'); if (form == null || btnInsert == null || btnCancel == null) { console.warn('Dialog creation error'); insertResults(); return; } [ ['BBcode: original size', 1], ['BBcode: thumbnails with link to original', 2], ['BBcode: thumbnails with link to share page', 3], ['BBcode: screenshot comparison (PTP)', 4], ['BBcode: screenshot comparison + encode images (PTP)', 5], ['Markdown: original size', 9], ['HTML: original size', 6], ['HTML: thumbnails with link to original', 7], ['HTML: thumbnails with link to share page', 8], ['Raw links', 0], ].forEach(function(item) { let radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'template'; radio.value = item[1]; radio.style = 'margin: 5px 15px 5px 0px; cursor: pointer;'; let label = document.createElement('label'); label.style = 'color: white; cursor: pointer; -webkit-user-select: none; ' + '-moz-user-select: none; -ms-user-select: none; user-select: none;'; label.append(radio); label.append(item[0]); form.insertBefore(label, btnInsert); let br = document.createElement('br'); form.insertBefore(br, btnInsert); }); if (!results.some(result => typeof result == 'object' && httpParser.test(result.original) && httpParser.test(result.thumb))) disableItem(2, 7); if (!results.some(result => typeof result == 'object' && httpParser.test(result.original) && httpParser.test(result.share))) disableItem(3, 8); if (results.length % 2 != 0) disableItem(4, 5); form.onclick = evt => { evt.stopPropagation() }; btnInsert.onclick = function(evt) { let template = document.querySelector('form#ihh-template-selector input[name="template"]:checked'); if (template != null) template = parseInt(template.value); modal.remove(); insertResults(template); }; modal.onclick = btnCancel.onclick = evt => { modal.remove() }; window.setTimeout(() => { modal.style.opacity = 1 }); function disableItem(...n) { n.forEach(function(n) { let radio = document.querySelector('div#ihh-template-selector input[type="radio"][value="' + n + '"]'); if (radio == null) return; radio.parentNode.style.opacity = 0.5; radio.disabled = true; }); } } else insertResults(); function insertResults(template = 1) { if (textArea.noBBCode) template = 0; if (typeof template != 'number' || isNaN(template)) return; let code = '', nl = [6, 7, 8].includes(template) ? '