Image Host Helper

Directly upload local / rehost remote images or galleries to whatever supported image host by dropping/pasting them to target field

当前为 2020-12-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Image Host Helper
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.085
  5. // @description Directly upload local / rehost remote images or galleries to whatever supported image host by dropping/pasting them to target field
  6. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsSAAALEgHS3X78AAAHTklEQVR4nO1aCUwUVxj+38zuyuwuRGGByHIpqEXFo1hj1SZiW1MarVqPREgkattUadPUI4iNpGrVxph6VJR4Rm3UlhrsZaBnqtXEo1qqxqJFZGFBTmHZe2fm9c3CrhzLsbtDRtP9wsLO9b/vffPPfzxGhjGG/zNkUhOQGgEBpCYgNQICSE1AagQEkJqA1AgIIDUBqREQQGoCUiMggNQEpEZAADGNFetNk77Qc5mXHuPUR3aItfFISZpt2nWcAmBVNG4exkBpWjj1TbqWPp0cpqwSk4O3EEWAa3WmEbml/ObiRn4B5pEcKATQ9tMJPBnPwCFNSStoSlrwtLwKx9rl0a2f5yTKPosMZsxicPEWfguQ/69xQfZdbr/BgSKARoDo3s9Hrl/kvFYORewpx1t+bbSnHR6Hl0+OUJb6y8db+CXAJ3eM7+Te4/dhhOR9TdwTULsQt1rR1DnXHMVnnjfNSR2quuUPJ2/hswD5940Lcu9xeWTyMtTV170EIsGhzo7iMm6yhUVy88xxGqWu3miRsRgYOQKLRs2w/o3QM3wS4Hq9OTH7Hy5fjMm7IIhQY0MJ8/5kz2sUhoZHNhzmwChkEIUNEQpHw1g1XH9ZQ/+UqqEuRwUzRnFG9VGAjaXcJvLMa3xx+94giFBugTHlQjh0K4uggmxfewwzjlVxa2OCuNJlMez+VfGyw5Fq/wOn1wKQVJfyYwO/ENEi3foucM67q+kO25U2GLX5Hr+noMa+ZM8YPuvVKNUNf8bzWoCTem4pzyOF2He/v2gLnAjuGmHKvOvsL/vHGpdkDlcX+WrPKwHqjBblhSZIEyoaqSE8LmYeDX73FvdliMw4Z36s+oIvdrwSoMyE46ptEN/NRX2E638yvgZS4Torj0Ky7nBHk0PMUxMHK+u8teGVADorH83xWI4o/xXApCwcFYK4RjumG2x+iCBkDwtK2HafXXf0BVjn7fVeCdDKIgVgcSYfzSAoelFF/9XMwaKrZmCJXZ9TKolHBY/winVNlp1JoUytN5f2W4Amk4WpsfIaf91fcHs1IXwyhYF4JeX8bB49CDbctndom7yDQMnIoiF5Dx0rFtu5H9Q01RwdhB5FBDO2vq7tU4CzOuPM03o+/UoLfolUa1rkRwAUHnmKKLBnPAMzwp8MvX5kEIkvPH+k3EH5ml4pwiuvAm/N03FbFBRnilRA1ZTBjksZWvrE3BjVxZ6u61GAK6TDI03Ozt+b8BtOt++hw/MKHIbsUUGwPE7Rabdgc3cyQz0wYfitjuuzofIEQVwhNpG/lB1DcKUVkiqrIamghl32ykPD2U+TZBtSNMqyrtd5FODUQ+OsrFvcsWYWRQkTFyPoY1LYL46Vwxbi7p6gliHnY/HyH2YoNfLgq6ehjl9oQRhE/1wPi2e1OKYdTDalL4hTdUqX3QQ4pzNNX1HCnbFiNMQfd+8IzAFM1sjgAHH93jxcy1BwYhIDr182QaPD98zQEU4TRIgmFmkzS9gClcw0+zWt6prreCcBHraYQ9+/wx4huVW8yZOIr1W23d1QRd8zmjyEhvwJDGRctwBxZVFEECDYMXEoYtVt9vDFYMt0bQjTKuzvJMDeci6rygIjxSpzhYg/hPSzZ8hdHanuv6ILtXKotmFY87cVOHGoOOFstkxo3P4KdsXWZNgt7HMLUNJgjiqsxekgQpHjHpB8lHLAByocaG+53bm9bXQQJKg8i3Gu2gGnKh1OojwRT05uBMeLRqcNxPZXpGboJsAhHbtMb4PhYrmcE8SW3oLRqQqHe9fqBEwE8Hz6zRYOCnRCPUC1qUf5mXV64KSzQKJr0y1AYS2/iMWUwvNVfoznjsbtc+plRvL2Tm8gO01hCAcGdypyC1BtpcaLFfieAbhvg1sAFY11ZoxipeEjHdwCpIbB+fMN8Db2uSJ/dkAeQ/ciq1uAt2LoY5eb+flNLEQOzGLX0wEhNYcpoMa17RZgbqz66nS9oejbGsgcSB+Q9xJnBmiZsTNIWp0Zir53bXYqhD6Mp3cW1bKLSMZWDgQXweYdAw8mEoYxPHlFV/g2iPhljRUPQN57AuHuMzJs+CBetsu1r5MAM4aqbq9JMHy0/T7ehWlxmiAXBFvCRDNvWEhT2P395LYUiUCM1SZPcI5Iqqv1I6jsKZHK+6793ZqhbckhuxvshtBDFXgjpvxYpekBztLWg1Hcfmwgpu/Um7j+e8OoTbljgvM7HvPYDh9MCckdxrRWby/jP251oEhnRSZWUyKOmX7BNfHBClyzMZHKWf1c8PGu5/S4IJIzOjg/LcJ8Pr+CXVlcj+dW29Gw9grqWUgSmFSVVm0QfpAWjgpXxskPjg1TVno6sdclsQkapS5fAzk36037jlfxGd/Vsm9WWXACjyjNwPD2HxTmq+IZKJs9lC5cqpV9PV6j0vd2fr8WRSeGq/QTw2EHaZ92iEPz6UHgHSGpCUiNgABSE5AaAQGkJiA1AgJITUBqBASQmoDUCAggNQGpERBAagJS4z/F0X/2U+WfagAAAABJRU5ErkJggg==
  7. // @author Anakunda
  8. // @copyright 2020, Anakunda (https://greasyfork.org/cs/users/321857-anakunda)
  9. // @license GPL-3.0-or-later
  10. // @match https://passthepopcorn.me/*
  11. // @match https://redacted.ch/*
  12. // @match https://orpheus.network/*
  13. // @match https://broadcasthe.net/*
  14. // @match https://notwhat.cd/*
  15. // @match https://dicmusic.club/*
  16. // @match https://*/torrents.php?id=*
  17. // @match https://*/artist.php?id=*
  18. // @match https://*/artist.php?action=edit&artistid=*
  19. // @match https://*/reportsv2.php?action=report&id=*
  20. // @match https://*/forums.php?action=new*
  21. // @match https://*/forums.php?*action=viewthread*
  22. // @match https://*/requests.php?action=view*
  23. // @match https://*/collages.php?id=*
  24. // @match https://*/collages.php?action=edit&collageid=*
  25. // @match https://*/collages.php?action=comments&collageid=*
  26. // @match https://*/collages.php?action=new
  27. // @match http*://tracker.czech-server.com/*
  28. // @connect *
  29. // @grant GM_xmlhttpRequest
  30. // @grant GM_getValue
  31. // @grant GM_setValue
  32. // @grant GM_deleteValue
  33. // @require https://greasyfork.org/scripts/408084-xhrlib/code/xhrLib.js
  34. // @require https://greasyfork.org/scripts/404516-progressbars/code/progressBars.js
  35. // @require https://greasyfork.org/scripts/401726-imagehostuploader/code/imageHostUploader.js
  36. // ==/UserScript==
  37.  
  38. 'use strict';
  39.  
  40. if (document.getElementById('upload-assistant') != null) return; // don't clash with Upload Assistant
  41.  
  42. const amEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*apple\.com\/(?:\S+\/)?(album|artist|playlist)\/(?:[\w\%\-]+\/)?(\d+)\b/i;
  43. const itunesImageMax = [/\/(\d+x\d+)\w*(?=\.\w+$)/, '/100000x100000-999'];
  44. const dzrEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*deezer\.com\/(?:\S+\/)?(album|artist|track|comment|playlist|radio|user)\/(\d+)\b/i;
  45. const dzImageMax = GM_getValue('deezer_get_png_cover', false) ? [/\/(\d+x\d+)(?:\-\d+)*\.\w+$/, '/1400x1400.png']
  46. : [/\/(\d+x\d+)(?:\-\d+)*(?=\.\w+$)/, '/1400x1400-000000-' + (parseInt(GM_getValue('deezer_jpeg_quality')) || 100) + '-0-0']
  47. const discogsKey = 'LWiNvIWBobGMRhfSCAiC';
  48. const discogsSecret = 'HAQUKFmebpCSLyRNwjmSgOMgbnxsVQcp';
  49. const lfmApiKey = '920db0d2f86108f2fbe1917b53d63858';
  50.  
  51. Array.prototype.flatten = function() {
  52. return this.reduce(function(flat, toFlatten) {
  53. return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
  54. }, []);
  55. };
  56.  
  57. PTPimg.prototype.setSession = function() {
  58. return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin).then(response => {
  59. var apiKey = response.document.getElementById('api_key');
  60. if (apiKey == null) {
  61. let counter = GM_getValue('ptpimg_reminder_read', 0);
  62. if (counter < 3) {
  63. alert(`
  64. PTPimg API key could not be captured. Please login to ${this.origin}/ and redo the action.
  65. If you don\'t have PTPimg account or don\'t want to use it, consider to remove PTPimg from
  66. 'upload_hosts' and 'rehost_hosts' storage entries.
  67. `);
  68. GM_setValue('ptpimg_reminder_read', ++counter);
  69. }
  70. return Promise.reject('API key not configured');
  71. } else if (!(this.apiKey = apiKey.value))
  72. return Promise.reject('assertion failed: empty PTPimg API key');
  73. GM_setValue('ptpimg_api_key', this.apiKey);
  74. Promise.resolve(this.apiKey)
  75. .then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) });
  76. return this.apiKey;
  77. });
  78. }
  79.  
  80. var cheveretoCustomHosts = GM_getValue('chevereto_custom_hosts');
  81. if (cheveretoCustomHosts !== undefined) try {
  82. JSON.parse(cheveretoCustomHosts).forEach(function(siteDef) {
  83. if (!siteDef.host_name || !siteDef.alias) {
  84. console.warn('Incomplete Chevereto custom site definition:', siteDef);
  85. return;
  86. }
  87. imageHostHandlers[siteDef.alias.replace(nonWordStripper, '').toLowerCase()] = new Chevereto(
  88. siteDef.host_name,
  89. siteDef.alias,
  90. siteDef.types,
  91. siteDef.size_limit, {
  92. sizeLimitAnonymous: siteDef.size_limit_anonymous,
  93. configPrefix: siteDef.config_prefix,
  94. apiEndpoint: siteDef.api_endpoint,
  95. apiFieldName: siteDef.api_field_name,
  96. apiResultKey: siteDef.api_result_key,
  97. jsonEndpoint: siteDef.json_endpoint,
  98. });
  99. });
  100. } catch (e) { console.warn(e) } else GM_setValue('chevereto_custom_hosts', '[]');
  101. console.log('Image host handlers:', imageHostHandlers);
  102.  
  103. ['upload_hosts', 'rehost_hosts'].forEach(propName => { if (!GM_getValue(propName)) GM_setValue(propName, [
  104. 'PTPimg', 'ImgBB', 'PixHost', 'ImgBox', 'Slowpoke', 'FunkyIMG', 'PostImage', 'Abload', 'VgyMe', 'Jerking',
  105. 'Gifyu', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS',
  106. 'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr',
  107. 'Imgur', 'Catbox', 'ImageVenue', 'GetaPic', 'FastPic', 'SVGshare',
  108. ].join(', ')) });
  109. [
  110. ['passthepopcorn.me', [
  111. 'PTPimg', 'ImgBB', 'PixHost', 'Slowpoke', 'ImgBox', 'FunkyIMG', 'Jerking', 'Gifyu', 'Abload', 'VgyMe', 'GeekPic',
  112. 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS', 'CasImages', 'CubeUpload', 'GooPics',
  113. 'ImageBan', 'UuploadIr', 'Catbox', 'ImageVenue', 'GetaPic',
  114. ]],
  115. ['notwhat.cd', ['NWCD']],
  116. ].forEach(hostDefaults => { if (!GM_getValue(hostDefaults[0])) GM_setValue(hostDefaults[0], hostDefaults[1].join(', ')) });
  117.  
  118. var imageHosts = new ImageHostManager(logFail,
  119. GM_getValue(document.domain) || GM_getValue('upload_hosts'),
  120. GM_getValue(document.domain) || GM_getValue('rehost_hosts'));
  121.  
  122. imageHostUploaderInit(inputDataHandler, textAreaDropHandler, textAreaPasteHandler, imageUrlResolver);
  123.  
  124. // Set single input UI handlers
  125. document.querySelectorAll([
  126. 'image', 'picture', 'cover', 'photo', 'avatar', 'poster', 'screen',
  127. ].map(pattern => ['id', 'name'].map(attr => 'input[type="text"][' + attr + '*="' + pattern + '"]')).join(','))
  128. .forEach(setInputHandlers);
  129. if (document.URL.includes('/torrents.php?id=')) {
  130. let a = document.querySelector('span.additional_add_artists > a');
  131. if (a != null) a.addEventListener('click', function() {
  132. document.querySelectorAll('input[name="image[]"]').forEach(setInputHandlers);
  133. });
  134. }
  135. // Set multiple inputs UI handlers
  136. for (let textArea of document.getElementsByTagName('textarea')) {
  137. if (textArea.className != 'ua-input') setTextAreahandlers(textArea);
  138. }
  139.  
  140. // site-specific extensions
  141. switch (document.domain) {
  142. case 'passthepopcorn.me':
  143. // Auto-fill missing/invalid images from IMDB
  144. if (/\/artist\.php\?action=edit&artistid=(\d+)\b/i.test(document.URL)) {
  145. let artistId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]');
  146. if (input != null) verifyImageUrl(input.value).catch(function(reason) {
  147. if (input.value) input.value = '';
  148. localXHR('/artist.php?id=' + artistId).then(function(dom) {
  149. let imdb = dom.querySelector('div#artistinfo > div.panel__body > ul.list > li > a');
  150. if (imdb != null) imageUrlResolver(imdb.href)
  151. .then(setCover.bind(input), reason => { logFail('No IMDB photo of this artist') });
  152. });
  153. });
  154. } else if (/\/torrents\.php??action=editgroup&groupid=(\d+)\b/i.test(document.URL)) {
  155. let groupId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]');
  156. if (input != null) verifyImageUrl(input.value).catch(function(reason) {
  157. if (input.value) input.value = '';
  158. localXHR('/torrents.php?id=' + groupId).then(function(dom) {
  159. let imdb = dom.querySelector('a#imdb-title-link');
  160. if (imdb != null) imageUrlResolver(imdb.href)
  161. .then(setCover.bind(input), reason => { logFail('No IMDB poster for this movie') });
  162. });
  163. });
  164. }
  165. // HJ Toolkit patch
  166. setTimeout(function() {
  167. if (document.querySelector('div.HJ-toolkit-badge') != null) {
  168. let hjtkTimer = setInterval(function() {
  169. document.querySelectorAll([
  170. 'textarea[id^="HJMA"]',
  171. 'textarea.form-control[name="screen"]',
  172. 'textarea.form-control[name="comp"]',
  173. ].join(',')).forEach(setTextAreahandlers);
  174. }, 1000);
  175. }
  176. }, 1000);
  177. break;
  178. case 'redacted.ch':
  179. case 'orpheus.network':
  180. case 'notwhat.cd':
  181. case 'dicmusic.club':
  182. // Auto-fill missing/invalid artist images
  183. if (document.URL.includes('/artist.php?action=edit&')) {
  184. let input = document.querySelector('input[name="image"]');
  185. if (input != null) verifyImageUrl(input.value).catch(function() {
  186. if (input.value) input.value = '';
  187. let artist = document.querySelector('div.header > h2 > a');
  188. if (artist != null) artist = artist.textContent.trim(); else throw 'Artist name not found';
  189.  
  190. function resultsFilter(results0, nameExtractor) {
  191. const tailingBracketStripper = [/\s*\([^\(\)]+\)\s*$/, ''];
  192. let results = results0, f;
  193. if (results.length > 1) {
  194. f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toASCII().replace(/\s+|/g, '').toLowerCase()
  195. == artist.replace(...tailingBracketStripper).toASCII().replace(/\s+/g, '').toLowerCase());
  196. if (f.length > 0) results = f;
  197. }
  198. if (results.length > 1) {
  199. f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toASCII().toLowerCase()
  200. == artist.replace(...tailingBracketStripper).toASCII().toLowerCase());
  201. if (f.length > 0) results = f;
  202. }
  203. if (results.length > 1) {
  204. f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toLowerCase()
  205. == artist.replace(...tailingBracketStripper).toLowerCase());
  206. if (f.length > 0) results = f;
  207. }
  208. return results;
  209. }
  210.  
  211. let lookupWorkers = [
  212. // Qobuz
  213. globalXHR('https://www.qobuz.com/shop', { responseType: 'text' }).then(function(response) {
  214. const rx = /^\s*(?:(?:window\.)?qobuz\.algolia(\d+))\s*=\s*(\{.*\});/gm;
  215. let result = [], m;
  216. while ((m = rx.exec(response.responseText)) != null) {
  217. let obj = JSON.parse(m[2]);
  218. if (obj.api_key && obj.application_id) result[parseInt(m[1]) - 1] = obj;
  219. }
  220. return result[0] && result[1] ? result : Promise.reject('unexpected page structure');
  221. }).then(algolia => globalXHR('https://' + algolia[1].application_id.toLowerCase() + '-1.algolianet.com/1/indexes/' + algolia[1].index.main_artists + '/query?' + new URLSearchParams({
  222. 'x-algolia-application-id': algolia[1].application_id,
  223. 'x-algolia-api-key': algolia[1].api_key,
  224. }).toString(), { responseType: 'json' }, { 'params': 'query=' + encodeURIComponent(artist) })).then(function(response) {
  225. if (response.response.nbHits <= 0) return Promise.reject('Qobuz: no matches');
  226. let results = resultsFilter(response.response.hits, result => result.name);
  227. if (results.length <= 0) return Promise.reject('Qobuz: no matches');
  228. //console.debug('Qobuz search results for "' + artist + '":', results);
  229. if (results.length > 1) return Promise.reject('Qobuz: ambiguity');
  230. if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results);
  231. return urlParser.test(results[0].image) ? results[0].image.replace(/(\/artists\/covers)\/\w+\//i, '$1/large/')
  232. : Promise.reject('Qobuz: artist exists but no photo');
  233. }),
  234. // AllMusic
  235. globalXHR('https://www.allmusic.com/search/artists/' + encodeURIComponent(artist)).then(function(response) {
  236. let results = resultsFilter(Array.from(response.document.querySelectorAll('ul.search-results > li.artist')).map(function(li) {
  237. let result = {
  238. name: li.querySelector('div.name > a'),
  239. genres: li.querySelector('div.genres'),
  240. decades: li.querySelector('div.decades'),
  241. };
  242. Object.keys(result).forEach(key => {
  243. result[key] = result[key] != null ? result[key].textContent.trim() || undefined : undefined;
  244. });
  245. if (result.genres) result.genres = result.genres.split(/\s*,\s*/);
  246. result.url = li.querySelector('div.name > a');
  247. result.url = result.url != null ? result.url.href : undefined;
  248. if (/-(mw\d+)$/i.test(result.url)) result.id = RegExp.$1;
  249. result.image = li.querySelector('div.photo img');
  250. result.image = result.image != null ? result.image.src : undefined;
  251. return result;
  252. }), result => result.name);
  253. if (results.length <= 0) return Promise.reject('AllMusic: no matches');
  254. console.debug('AllMusic search results for "' + artist + '":', results);
  255. if (results.length > 1) return Promise.reject('AllMusic: ambiguity');
  256. if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results);
  257. if (!urlParser.test(results[0].image)) return Promise.reject('AllMusic: artist exists but no photo');
  258. return verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=6'))
  259. .catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=0')))
  260. .catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=5')));
  261. }),
  262. // Discogs
  263. globalXHR('https://api.discogs.com/database/search?' + new URLSearchParams({
  264. query: artist,
  265. type: 'artist',
  266. sort: 'score,desc',
  267. strict: false,
  268. }).toString(), {
  269. responseType: 'json',
  270. headers: { 'Authorization': 'Discogs key="' + discogsKey + '", secret="' + discogsSecret + '"' },
  271. }).then(response => {
  272. if (response.response.items <= 0) return Promise.reject('Discogs: no matches');
  273. let results = resultsFilter(response.response.results.filter(result => result.type == 'artist'),
  274. result => result.title);
  275. if (results.length <= 0) return Promise.reject('Discogs: no matches');
  276. //console.debug('Discogs search results for "' + artist + '":', results);
  277. //if (results.length > 1) return Promise.reject('Discogs: ambiguity');
  278. if (results.length > 1) console.info('Discogs returns ambiguous results for "' + artist + '":', results);
  279. let artistCovers = results.map(result => {
  280. if (result.cover_image.includes('/spacer.gif')) return null;
  281. if (/^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/(\S+?\.\w+)\b/i.test(result.cover_image))
  282. return 'https://www.discogs.com/image/' + RegExp.$1;
  283. return result.cover_image;
  284. });
  285. return urlParser.test(artistCovers[0]) ?
  286. artistCovers[0] : Promise.reject('Discogs: artist exists but no photo');
  287. }),
  288. // iTunes
  289. globalXHR('https://itunes.apple.com/search?' + new URLSearchParams({
  290. term: '"' + artist + '"',
  291. media: 'music',
  292. entity: 'musicArtist',
  293. attribute: 'artistTerm',
  294. //country: 'US',
  295. }).toString(), { responseType: 'json' }).then(function(response) {
  296. if (response.response.resultCount <= 0) return Promise.reject('iTunes: no matches');
  297. let results = resultsFilter(response.response.results.filter(result =>
  298. result.wrapperType == 'artist' && result.artistType == 'Artist'), result => result.artistName);
  299. if (results.length <= 0) return Promise.reject('iTunes: no matches');
  300. //console.debug('iTunes search results for "' + artist + '":', results);
  301. //if (results.length > 1) return Promise.reject('iTunes: ambiguity');
  302. if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results);
  303. return imageUrlResolver(results[0].artistLinkUrl);
  304. }),
  305. // Deezer
  306. globalXHR('https://api.deezer.com/search/artist?' + new URLSearchParams({
  307. q: artist,
  308. order: 'RANKING',
  309. //strict: 'on',
  310. }).toString(), { responseType: 'json' }).then(function(response) {
  311. if (response.response.total <= 0) return Promise.reject('Deezer: no matches');
  312. let results = resultsFilter(response.response.data.filter(result => result.type == 'artist'),
  313. result => result.name);
  314. if (results.length <= 0) return Promise.reject('Deezer: no matches');
  315. //console.debug('Deezer search results for "' + artist + '":', results);
  316. //if (results.length > 1) return Promise.reject('Deezer: ambiguity');
  317. if (results.length > 1) console.info('Deezer returns ambiguous results for "' + artist + '":', results);
  318. return globalXHR(results[0].picture, {
  319. method: 'HEAD',
  320. }).then(response => verifyImageUrl(response.finalUrl)).catch(function(reason) {
  321. console.warn('Deezer API image retrieval failed:', reason);
  322. return response.response.data[0].picture_xl || response.response.data[0].picture_big
  323. || response.response.data[0].picture_medium || response.response.data[0].picture_small;
  324. }).then(function(imageUrl) {
  325. if (!urlParser.test(imageUrl) || imageUrl.includes('/images/artist//'))
  326. return Promise.reject('Deezer: artist exists but no photo');
  327. return imageUrl.replace(...dzImageMax);
  328. });
  329. }),
  330. // Last.fm
  331. globalXHR('http://ws.audioscrobbler.com/2.0/?' + new URLSearchParams({
  332. method: 'artist.getinfo',
  333. artist: artist,
  334. format: 'json',
  335. api_key: lfmApiKey,
  336. }).toString(), { responseType: 'json' }).then(function(response) {
  337. if (response.response.error) return Promise.reject(response.response.message);
  338. //console.debug('Last.fm search result for "' + artist + '":', response.response.artist);
  339. const rx = /\/(\d+)x(\d+)\//;
  340. let biggest = response.response.artist.image.map(im => im['#text']).reduce(function(a, b) {
  341. let r = [a, b].map(RegExp.prototype.exec.bind(rx))
  342. .map(r => r != null ? parseInt(r[1]) * parseInt(r[2]) : -Infinity);
  343. return r[1] > r[0] ? b : a;
  344. });
  345. return rx.test(biggest) && !biggest.endsWith('/2a96cbd8b46e442fc41c2b86b821562f.png') ?
  346. biggest : Promise.reject('Last.fm: artist exists but no photo');
  347. }),
  348. ];
  349. const lookUp = (index = 0) => index < lookupWorkers.length ?
  350. lookupWorkers[index].then(setCover.bind(input)).catch(reason => lookUp(index + 1))
  351. : Promise.reject('Image of this artist was not found');
  352. lookUp().catch(logFail);
  353. });
  354. }
  355. break;
  356. case 'tracker.czech-server.com':
  357. document.querySelectorAll('input[name="urlobr"]').forEach(setInputHandlers);
  358. break;
  359. }
  360.  
  361. if (document.location.pathname.startsWith('/reportsv2.php')) {
  362. function setReportHandlers(evt) {
  363. setTimeout(function() {
  364. document.querySelectorAll('input[id*="image"]').forEach(setInputHandlers);
  365. document.querySelectorAll('textarea').forEach(setTextAreahandlers);
  366. }, 2000);
  367. }
  368.  
  369. setReportHandlers();
  370. let reportTypeSelect = document.querySelector('select#type');
  371. if (reportTypeSelect != null) reportTypeSelect.addEventListener('change', setReportHandlers);
  372. }
  373.  
  374. var opti_PNG = GM_getValue('optipng', false);
  375.  
  376. function coverPreview(imgUrl, size) {
  377. let div = document.getElementById('image-preview');
  378. if (div != null) document.body.removeChild(div);
  379. if (!urlParser.test(imgUrl)) return;
  380. div = document.createElement('div');
  381. div.id = 'image-preview';
  382. div.style = 'position: absolute; bottom: 20px; right: 20px; border: thin solid silver; ' +
  383. 'background-color: #8888; padding: 10px; opacity: 0; transition: opacity 1s ease-in-out;';
  384. const cleanUp = () => {
  385. if (div.parentNode == null) return;
  386. div.style.opacity = 0;
  387. setTimeout(() => { document.body.removeChild(div) }, 1000);
  388. };
  389. div.ondblclick = cleanUp;
  390. let img = document.createElement('img');
  391. img.style = 'width: 225px;';
  392. img.onload = function(evt) {
  393. document.body.append(div);
  394. setTimeout(() => { div.style.opacity = 1 });
  395. setTimeout(cleanUp, 12000);
  396. if (!img.naturalWidth || !img.naturalHeight) return; // invalid image
  397. let info = document.createElement('div');
  398. info.id = 'image-info';
  399. info.style = 'text-align: center; background-color: #29434b; padding: 5px; color: white;' +
  400. 'font: 500 10pt "Segoe UI", Verdana, sans-serif;';
  401. div.append(info);
  402. const resolution = img.naturalWidth + '×' + img.naturalHeight;
  403. (size > 0 ? Promise.resolve(size) : size instanceof Promise ? size : getRemoteFileSize(imgUrl)).then(function(size) {
  404. if (!(size >= 0)) throw 'invalid size';
  405. let imageSizeLimit = GM_getValue('image_size_reduce_threshold'),
  406. html = resolution + ' (<span id="image-size"';
  407. if (imageSizeLimit > 0 && size > imageSizeLimit * 2**10) html += ' style="color: red;"';
  408. html += '>' + formattedSize(size) + '</span>)';
  409. info.innerHTML = html;
  410. }).catch(reason => { info.textContent = resolution });
  411. };
  412. img.onerror = function(evt) { console.warn('Image source couldnot be loaded:', evt, imgUrl) };
  413. img.src = imgUrl;
  414. div.append(img);
  415. }
  416.  
  417. function writeInfo() {
  418. let input = document.querySelector('input[name="summary"]');
  419. if (input != null && !input.disabled && !input.value) input.value = 'Image update/rehost';
  420. }
  421.  
  422. function setCover(url) {
  423. return verifyImageUrl(url).then(imageUrl => {
  424. this.value = imageUrl;
  425. writeInfo();
  426. let size = getRemoteFileSize(imageUrl);
  427. coverPreview(imageUrl, size);
  428. return checkImageSize(imageUrl, this, size).then(imageUrl => {
  429. this.disabled = true;
  430. return imageHosts.rehostImages([imageUrl]).then(singleImageGetter).then(imageUrl => {
  431. if (imageUrl == null) throw 'invalid image';
  432. this.value = imageUrl;
  433. });
  434. }).catch(reason => {
  435. this.value = imageUrl;
  436. logFail(reason + ' (not rehosted)');
  437. }).then(() => {
  438. this.disabled = false;
  439. return imageUrl;
  440. });
  441. });
  442. }
  443.  
  444. function inputDataHandler(evt, data) {
  445. const rehoster = imageUrl => imageHosts.rehostImages([imageUrl]).then(singleImageGetter).then(function(imageUrl) {
  446. if (!urlParser.test(imageUrl)) {
  447. console.warn('rehostImages returns invalid image URL:', imageUrl);
  448. throw 'invalid image URL';
  449. }
  450. evt.target.value = imageUrl;
  451. writeInfo();
  452. });
  453.  
  454. if (!data) return true;
  455. if (data.files.length > 0) {
  456. if (data.files[0].type && !data.files[0].type.startsWith('image/')) return true;
  457. evt.target.disabled = true;
  458. if (evt.target.hTimer) {
  459. clearTimeout(evt.target.hTimer);
  460. delete evt.target.hTimer;
  461. }
  462. evt.target.style.color = 'white';
  463. evt.target.style.backgroundColor = 'darkred';
  464. let progressBar = { };
  465. function progressHandler(worker, param = null) {
  466. if (param && typeof param == 'object') {
  467. if (param.readyState > 1 || progressBar.current != undefined && worker !== progressBar.current
  468. || Date.now() < progressBar.lastUpdate + 100) return;
  469. let pct = Math.floor(Math.min(param.done * 100 / param.total, 100));
  470. if (pct <= progressBar.lastPct) return;
  471. evt.target.value = 'Uploading... [' + (progressBar.lastPct = pct) + '%]';
  472. progressBar.lastUpdate = Date.now();
  473. } else if (param == null) {
  474. progressBar = { current: worker };
  475. evt.target.value = 'Uploading...';
  476. }
  477. }
  478. const file = data.files[0];
  479. evt.target.disabled = true;
  480. checkImageSize(file, evt.target, progressHandler).catch(function(reason) {
  481. logFail('Downsizing of source image not possible (' + reason + '), uploading original size');
  482. return file;
  483. }).then(function(result) {
  484. const uploader = file => imageHosts.uploadImages([file], progressHandler).then(singleImageGetter).then(function(imageUrl) {
  485. evt.target.value = imageUrl;
  486. coverPreview(imageUrl, file.size);
  487. writeInfo();
  488. });
  489.  
  490. if (urlParser.test(result)) return rehoster(result).catch(function(reason) {
  491. logFail('Downsizing of source image failed (' + reason + '), uploading original size');
  492. return uploader(file);
  493. });
  494. if (result instanceof File) return uploader(result);
  495. console.warn('invalid checkImageSize(...) result:', result);
  496. return Promise.reject('invalid checkImageSize(...) result');
  497. }).then(function() {
  498. evt.target.style.backgroundColor = '#008000';
  499. evt.target.hTimer = setTimeout(function() {
  500. evt.target.style.backgroundColor = null;
  501. evt.target.style.color = null;
  502. delete evt.target.hTimer;
  503. }, 10000);
  504. }, function(reason) {
  505. imageClear(evt);
  506. evt.target.style.backgroundColor = null;
  507. evt.target.style.color = null;
  508. Promise.resolve(reason).then(msg => { alert(msg) });
  509. }).then(() => { evt.target.disabled = false });
  510. return false;
  511. } else if (data.items.length > 0) {
  512. let links = data.getData('text/uri-list');
  513. if (links) links = links.split(/\r?\n/); else {
  514. links = data.getData('text/x-moz-url');
  515. if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  516. else if (links = data.getData('text/plain')) links = links.split(/\r?\n/);
  517. }
  518. if (!Array.isArray(links) || links.length <= 0) return true;
  519. imageUrlResolver(links[0], {
  520. altKey: evt.altKey,
  521. ctrlKey: evt.ctrlKey,
  522. shiftKey: evt.shiftKey,
  523. }).then(verifyImageUrl).then(function(imageUrl) {
  524. evt.target.disabled = true;
  525. evt.target.value = imageUrl;
  526. let size = getRemoteFileSize(imageUrl);
  527. coverPreview(imageUrl, size);
  528. checkImageSize(imageUrl, evt.target, size).then(rehoster).catch(function(reason) {
  529. evt.target.value = imageUrl;
  530. Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') });
  531. }).then(() => { evt.target.disabled = false });
  532. }).catch(function(e) {
  533. console.error(e);
  534. alert(e);
  535. });
  536. return false;
  537. }
  538. return true;
  539. }
  540.  
  541. function rehoster(promises, resultsHandler, target = null) {
  542. if (!Array.isArray(promises)) throw 'invalid parameter';
  543. return Promise.all(promises).then(function(resolved) {
  544. let resolvedUrls = resolved.flatten();
  545. if (target instanceof HTMLElement) {
  546. target.disabled = true;
  547. if (resolvedUrls.length > 1 && !['notwhat.cd'].some(hostname => document.domain == hostname))
  548. var progressBar = new RHProgressBar(target, resolvedUrls.length);
  549. }
  550. return (function() {
  551. if (!opti_PNG || !(target instanceof HTMLElement)) return Promise.resolve(resolvedUrls);
  552. return Promise.all(resolvedUrls.map(resolvedUrl => optiPNG(resolvedUrl).catch(reason => resolvedUrl)));
  553. })().then(srcUrls => imageHosts.rehostImages(srcUrls, RHProgressBar.prototype.update.bind(progressBar)).catch(function(reason) {
  554. logFail(reason + ' (not rehosted)');
  555. RHProgressBar.prototype.update.call(progressBar, -1, false);
  556. return verifyImageUrls(srcUrls);
  557. }).then(results => { resultsHandler(results, arrayGrouping(resolved).flatten()) })
  558. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })).then(function() {
  559. RHProgressBar.prototype.cleanUp.call(progressBar);
  560. if (target instanceof HTMLElement) target.disabled = false;
  561. });
  562. });
  563. }
  564.  
  565. function textAreaDropHandler(evt) {
  566. if (!evt.dataTransfer || evt.shiftKey) return true;
  567. if (evt.dataTransfer.files.length > 0) {
  568. let images = Array.from(evt.dataTransfer.files).filter(file => !file.type || file.type.startsWith('image/'));
  569. if (images.length <= 0) return true;
  570. evt.target.disabled = true;
  571. if (!['notwhat.cd'].some(hostname => document.domain == hostname))
  572. var progressBar = new ULProgressBar(evt.target, images.map(image => image.size));
  573. (function() {
  574. if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG');
  575. ULProgressBar.prototype.update.call(progressBar, -1);
  576. return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) =>
  577. ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler);
  578. })().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler))
  579. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
  580. .then(function() {
  581. ULProgressBar.prototype.cleanUp.call(progressBar);
  582. evt.target.disabled = false;
  583. });
  584. evt.stopPropagation();
  585. return false;
  586. } else if (evt.dataTransfer.items.length > 0) {
  587. let content = evt.dataTransfer.getData('text/uri-list');
  588. if (content) content = content.split(/(?:\r?\n)+/); else {
  589. content = evt.dataTransfer.getData('text/x-moz-url');
  590. if (content) content = content.split(/(?:\r?\n)+/).filter((item, ndx) => ndx % 2 == 0);
  591. };
  592. if (!Array.isArray(content) || content.length <= 0) return true;
  593. rehoster(content.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, evt.target);
  594. evt.stopPropagation();
  595. return false;
  596. }
  597. return true;
  598.  
  599. function resultsHandler(results, groups = undefined) {
  600. if (results.length <= 0) return;
  601. if (evt.altKey && !evt.target.noBBCode) {
  602. let modal = document.createElement('div');
  603. modal.id = 'ihh-template-selector-background';
  604. modal.style = 'position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: #0008;' +
  605. 'opacity: 0; transition: opacity 0.15s linear;';
  606. modal.innerHTML = `
  607. <div id="ihh-template-selector" style="background-color: darkslategray; position: absolute; top: 30%; left: 35%; border-radius: 0.5em; padding: 20px 30px;">
  608. <div style="color: white;margin-bottom: 20px;">Insert as:</div>
  609. <input id="btn-insert" type="button" value="Insert" style="margin-top: 30px"/>
  610. <input id="btn-cancel" type="button" value="Cancel" style="margin-top: 30px"/>
  611. </div>
  612. `;
  613. document.body.append(modal);
  614. let form = document.getElementById('ihh-template-selector'),
  615. btnInsert = document.querySelector('div#ihh-template-selector input#btn-insert'),
  616. btnCancel = document.querySelector('div#ihh-template-selector input#btn-cancel');
  617. if (form == null || btnInsert == null || btnCancel == null) {
  618. console.warn('Dialog creation error');
  619. insertResults();
  620. return;
  621. }
  622. [
  623. ['BBcode: original size', 1],
  624. ['BBcode: thumbnails with link to original', 2],
  625. ['BBcode: thumbnails with link to share page', 3],
  626. ['BBcode: screenshot comparison (PTP)', 4],
  627. ['BBcode: screenshot comparison + encode images (PTP)', 5],
  628. ['Markdown: original size', 9],
  629. ['HTML: original size', 6],
  630. ['HTML: thumbnails with link to original', 7],
  631. ['HTML: thumbnails with link to share page', 8],
  632. ['Raw links', 0],
  633. ].forEach(function(item) {
  634. let radio = document.createElement('input');
  635. radio.type = 'radio';
  636. radio.name = 'template';
  637. radio.value = item[1];
  638. radio.style = 'margin: 5px 15px 5px 0px; cursor: pointer;';
  639. let label = document.createElement('label');
  640. label.style = 'color: white; cursor: pointer; -webkit-user-select: none; ' +
  641. '-moz-user-select: none; -ms-user-select: none; user-select: none;';
  642. label.append(radio);
  643. label.append(item[0]);
  644. form.insertBefore(label, btnInsert);
  645. let br = document.createElement('br');
  646. form.insertBefore(br, btnInsert);
  647. });
  648. if (!results.some(result => typeof result == 'object'
  649. && urlParser.test(result.original) && urlParser.test(result.thumb))) disableItem(2, 7);
  650. if (!results.some(result => typeof result == 'object'
  651. && urlParser.test(result.original) && urlParser.test(result.share))) disableItem(3, 8);
  652. if (results.length % 2 != 0) disableItem(4, 5);
  653. form.onclick = evt => { evt.stopPropagation() };
  654. btnInsert.onclick = function(evt) {
  655. let template = document.querySelector('div#ihh-template-selector input[name="template"]:checked');
  656. if (template != null) template = parseInt(template.value);
  657. modal.remove();
  658. insertResults(template);
  659. };
  660. modal.onclick = btnCancel.onclick = evt => { modal.remove() };
  661. window.setTimeout(() => { modal.style.opacity = 1 });
  662.  
  663. function disableItem(...n) {
  664. n.forEach(function(n) {
  665. let radio = document.querySelector('div#ihh-template-selector input[type="radio"][value="' + n + '"]');
  666. if (radio == null) return;
  667. radio.parentNode.style.opacity = 0.5;
  668. radio.disabled = true;
  669. });
  670. }
  671. } else insertResults();
  672.  
  673. function insertResults(template = 1) {
  674. if (evt.target.noBBCode) template = 0;
  675. if (typeof template != 'number' || isNaN(template)) return;
  676. let code = '', nl = [6, 7, 8].includes(template) ? '<br>\n' : '\n', _template;
  677. results.forEach(function(result, index) {
  678. if (_template == 1 && /\[img\]\[\/img\]/i.test(evt.target.value)) {
  679. evt.target.value = RegExp.leftContext + '[img]' + getImgUrl(result) + '[/img]' + RegExp.rightContext;
  680. return;
  681. }
  682. _template = template;
  683. if (template == 2 && (typeof result != 'object' || !urlParser.test(result.original) || !urlParser.test(result.thumb))
  684. || template == 3 && (typeof result != 'object' || !urlParser.test(result.share) || !urlParser.test(result.thumb)))
  685. _template = 1;
  686. else if (template == 7 && (typeof result != 'object' || !urlParser.test(result.original) || !urlParser.test(result.thumb))
  687. || template == 8 && (typeof result != 'object' || !urlParser.test(result.share) || !urlParser.test(result.thumb)))
  688. _template = 6;
  689. else _template = template;
  690. if (index > 0) {
  691. let thumb = [2, 3, 7, 8].includes(_template);
  692. code += isGroupBoundary(groups, index) ? thumb ? nl : nl + nl : thumb ? ' ' : nl;
  693. }
  694. switch (_template) {
  695. case 0: case 4: case 5: code += getImgUrl(result); break;
  696. case 1: code += '[img]' + getImgUrl(result) + '[/img]'; break;
  697. case 2: code += '[url=' + getImgUrl(result) + '][img]' + result.thumb + '[/img][/url]'; break;
  698. case 3: code += '[url=' + result.share + '][img]' + result.thumb + '[/img][/url]'; break;
  699. case 6: code += '<img src="' + getImgUrl(result) + '">'; break;
  700. case 7: code += '<a href="' + getImgUrl(result) + '" target="_blank"><img src="' + result.thumb + '"></a>'; break;
  701. case 8: code += '<a href="' + result.share + '" target="_blank"><img src="' + result.thumb + '"></a>'; break;
  702. case 9: code += '![](' + getImgUrl(result) + ')'; break;
  703. }
  704. });
  705. if ([4, 5].includes(template)) {
  706. code = '[comparison=Source, Encode]' + code + '[/comparison]';
  707. if (template == 5) {
  708. code += nl;
  709. results.forEach((result, index) => { if (index % 2 != 0) code += nl + '[img]' + getImgUrl(result) + '[/img]' });
  710. }
  711. }
  712. if (evt.target.value.trimRight().length <= 0) evt.target.value = code; else if (evt.ctrlKey) {
  713. evt.target.value = evt.target.value.slice(0, evt.rangeOffset) + code + evt.target.value.slice(evt.rangeOffset);
  714. } else evt.target.value = evt.target.value.trimRight() + nl + nl + code;
  715.  
  716. function getImgUrl(result) {
  717. if (typeof result == 'object' && urlParser.test(result.original)) return result.original;
  718. if (typeof result == 'string' && urlParser.test(result)) return result;
  719. throw 'Invalid result format';
  720. }
  721. }
  722. }
  723. }
  724.  
  725. function textAreaPasteHandler(evt) {
  726. if (!evt.clipboardData) return true;
  727. if (evt.clipboardData.files.length > 0) {
  728. let images = Array.from(evt.clipboardData.files).filter(file => !file.type || file.type.startsWith('image/'));
  729. if (images.length <= 0) return true;
  730. evt.target.disabled = true;
  731. if (!['notwhat.cd'].some(hostname => document.domain == hostname))
  732. var progressBar = new ULProgressBar(evt.target, images.map(image => image.size));
  733. (function() {
  734. if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG');
  735. ULProgressBar.prototype.update.call(progressBar, -1);
  736. return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) =>
  737. ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler);
  738. })().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler))
  739. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
  740. .then(function() { // __finally
  741. ULProgressBar.prototype.cleanUp.call(progressBar);
  742. evt.target.disabled = false;
  743. });
  744. evt.stopPropagation();
  745. return false;
  746. } else if (evt.clipboardData.items.length > 0) {
  747. return true;
  748. let urls = evt.clipboardData.getData('text/plain').split(/(?:\r?\n)+/);
  749. if (urls.length <= 0 || !urls.every(RegExp.prototype.test.bind(urlParser))) return true;
  750. rehoster(urls.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, evt.target);
  751. evt.stopPropagation();
  752. return false;
  753. }
  754. return true;
  755.  
  756. function resultsHandler(results, groups = undefined) {
  757. let selStart = evt.target.selectionStart, phpBB = '';
  758. results.forEach(function(result, index) {
  759. let thumb = evt.altKey && !evt.target.noBBCode && typeof result == 'object'
  760. && urlParser.test(result.originasl) && urlParser.test(result.thumb);
  761. if (index > 0) phpBB += isGroupBoundary(groups, index) ? thumb ? '\n' : '\n\n' : thumb ? ' ' : '\n';
  762. if (typeof result == 'object' && result.original) var imgUrl = result.original;
  763. else if (typeof result == 'string') imgUrl = result;
  764. else throw 'Invalid result format';
  765. phpBB += evt.target.noBBCode ? phpBB += imgUrl : !thumb ? '[img]' + imgUrl + '[/img]'
  766. : '[url=' + imgUrl + '][img]' + result.thumb + '[/img][/url]';
  767. });
  768. if (phpBB.length <= 0) return;
  769. evt.target.value = evt.target.value.slice(0, selStart) + phpBB + evt.target.value.slice(evt.target.selectionEnd);
  770. evt.target.setSelectionRange(selStart + phpBB.length, selStart + phpBB.length);
  771. }
  772. }
  773.  
  774. function arrayGrouping(arr) {
  775. return Array.isArray(arr) ? arr.map(function(elem) {
  776. if (!Array.isArray(elem)) return 1;
  777. return elem.every(elem => !Array.isArray(elem)) ? elem.length : arrayGrouping(elem);
  778. }) : null;
  779. }
  780.  
  781. function isGroupBoundary(groups, index) {
  782. return index > 0 && Array.isArray(groups)
  783. && groups.some((len, ndx, arr) => index == arr.slice(0, ndx).reduce((acc, len) => acc + len, 0));
  784. }
  785.  
  786. function checkImageSize(image, elem = null, param) {
  787. let imageSizeLimit = GM_getValue('image_size_reduce_threshold');
  788. if (!(imageSizeLimit > 0)) return Promise.resolve(image);
  789. if (!(elem instanceof HTMLElement)) elem = null;
  790. if (elem != null) elem.disabled = true;
  791. return (image instanceof File ? Promise.resolve(image.size) : param > 0 ? Promise.resolve(param)
  792. : param instanceof Promise ? param : getRemoteFileSize(image)).then(function(size) {
  793. if (size <= imageSizeLimit * 2**10) return image;
  794. const haveRhHosts = Array.isArray(imageHosts.rhHostChain) && imageHosts.rhHostChain.length > 0;
  795. if (!haveRhHosts && !GM_getValue('force_reduce', true)) return Promise.reject('no hosts to upload result');
  796. return reduceImageSize(image, GM_getValue('image_reduce_maxheight', 2160),
  797. GM_getValue('image_reduce_jpegquality', 90), typeof param == 'function' ? param : null).then(function(output) {
  798. if (elem != null) {
  799. elem.value = output.uri;
  800. if (image instanceof File) coverPreview(output.uri, output.size);
  801. }
  802. Promise.resolve(output.size).then(reducedSize => {
  803. console.log('cover size reduced by ' + Math.round((size - reducedSize) * 100 / size) +
  804. '% (' + Math.ceil(size / 2**10) + ' → ' + Math.ceil(reducedSize / 2**10) + ' KiB)');
  805. });
  806. return haveRhHosts ? output.uri : (function() {
  807. let fallbackHost = new Chevereto('imgcdn.dev', 'ImgCDN',
  808. ['jpeg', 'png', 'gif', 'bmp', 'webp'], 30, { sizeLimitAnonymous: 20 });
  809. if (!fallbackHost.apiKey) fallbackHost.apiKey = '5386e05a3562c7a8f984e73401540836';
  810. return output.size > fallbackHost.sizeLimit * 2**20 ? Promise.reject('size limit exceeded')
  811. : fallbackHost.rehost([output.uri]).then(singleImageGetter);
  812. })().catch(function(reason) {
  813. console.warn('Upload to ImgCDN fail:', reason);
  814. return imageHostHandlers['pixhost'].rehost([output.uri]).then(singleImageGetter);
  815. });
  816. });
  817. }).catch(function(reason) {
  818. logFail('failed to get remote image size or optimize the image: ' + reason + ' (size reduction was not performed)');
  819. return image;
  820. }).then(function(finalResult) {
  821. if (elem != null) {
  822. if (urlParser.test(finalResult)) {
  823. if (finalResult != elem.value) elem.value = finalResult;
  824. } else elem.value = '';
  825. elem.disabled = false;
  826. }
  827. return finalResult;
  828. });
  829. }
  830.  
  831. function imageUrlResolver(url, modifiers = { }) {
  832. return urlResolver(url).then(url => verifyImageUrl(url).catch(function(reason) {
  833. const notFound = Promise.reject('No title image for this URL');
  834. function getFromMeta(root) {
  835. let meta = root instanceof Document || root instanceof Element ? [
  836. 'meta[property="og:image:secure_url"][content]',
  837. 'meta[property="og:image"][content]',
  838. 'meta[name="og:image"][content]',
  839. 'meta[itemprop="og:image"][content]',
  840. 'meta[itemprop="image"][content]',
  841. ].reduce((elem, selector) => elem || root.querySelector(selector), null) : null;
  842. return meta != null && urlParser.test(meta.content) ? meta.content : undefined;
  843. }
  844.  
  845. try { url = new URL(url) } catch(e) { return Promise.reject(e) }
  846. if (url.hostname.endsWith('pinterest.com'))
  847. return pinterestResolver(url);
  848. else if (url.hostname.endsWith('free-picload.com')) {
  849. if (url.pathname.startsWith('/album/')) return imageHostHandlers.picload.galleryResolver(url);
  850. } else if (url.hostname.endsWith('bandcamp.com')) return globalXHR(url).then(function(response) {
  851. let ref = response.document.querySelector('div#tralbumArt > a.popupImage');
  852. ref = ref != null ? ref.href : getFromMeta(response.document);
  853. return ref ? Promise.resolve(ref.replace(/_\d+(?=\.\w+$)/, '_0')) : notFound;
  854. }); else if (url.hostname.endsWith('7digital.com') && url.pathname.startsWith('/artist/'))
  855. return globalXHR(url).then(function(response) {
  856. let img = response.document.querySelector('img[itemprop="image"]');
  857. return img != null ? img.src : notFound;
  858. });
  859. else if (url.hostname.endsWith('geekpic.net')) return globalXHR(url).then(function(response) {
  860. let a = response.document.querySelector('div.img-upload > a.mb');
  861. return a != null ? a.href : notFound;
  862. }); else if (url.hostname.endsWith('qq.com') && url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
  863. let img = response.document.querySelector('img#albumImg');
  864. const rx = /\/(T\d+)?(R\d+x\d+)?(M\w+?)(_\d+)?\.(\w+(?:\.\w+)*)(\?.*)?$/;
  865. return img != null ? verifyImageUrl(img.src.replace(rx, '/$1$3.$5'))
  866. .catch(() => img.src.replace(rx, '/$1$3$4.$5')).catch(() => img.src) : notFound;
  867. }); else switch (url.hostname) {
  868. // general image hostings
  869. case 'www.imgur.com': case 'imgur.com':
  870. return url.pathname.startsWith('/a/') ? globalXHR(url, { responseType: 'text' }).then(function(response) {
  871. if (/^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.test(response.responseText)) try {
  872. return JSON.parse(RegExp.$1).album_images.images.map(image => 'https://i.imgur.com/' + image.hash + image.ext);
  873. } catch(e) { debug.warn(e) }
  874. return notFound;
  875. }) : globalXHR(url).then(response => response.document.querySelector('link[rel="image_src"]').href);
  876. case 'pixhost.to':
  877. if (url.pathname.startsWith('/gallery/')) return globalXHR(url).then(response =>
  878. Promise.all(Array.from(response.document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href, modifiers))));
  879. if (url.pathname.startsWith('/show/')) return globalXHR(url)
  880. .then(response => response.document.querySelector('img#image').src);
  881. break;
  882. case 'malzo.com':
  883. if (url.pathname.startsWith('/al/')) return imageHostHandlers.malzo.galleryResolver(url); else break;
  884. case 'imgbb.com': case 'ibb.co':
  885. if (url.pathname.startsWith('/album/')) return imageHostHandlers.imgbb.galleryResolver(url); else break;
  886. case 'jerking.empornium.ph':
  887. if (url.pathname.startsWith('/album/')) return imageHostHandlers.jerking.galleryResolver(url); else break;
  888. case 'imgbox.com':
  889. if (url.pathname.startsWith('/g/')) return globalXHR(url).then(response =>
  890. Promise.all(Array.from(response.document.querySelectorAll('div#gallery-view-content > a'))
  891. .map(a => imageUrlResolver('https://imgbox.com' + a.pathname, modifiers))));
  892. break;
  893. case 'postimage.org': case 'postimg.cc':
  894. if (!url.pathname.startsWith('/gallery/')) break;
  895. return PostImage.resultsHandler(url).then(results => results.map(result => result.original));
  896. case 'www.imagevenue.com': case 'imagevenue.com':
  897. return globalXHR(url, { headers: { Referer: 'http://www.imagevenue.com/' } }).then(function(response) {
  898. let images = Array.from(response.document.querySelectorAll('div.card img')).map(function(img) {
  899. return img.src.includes('://cdn-images') ? Promise.resolve(img.src) : imageUrlResolver(img.parentNode.href, modifiers);
  900. });
  901. return images.length > 1 ? Promise.all(images) : images.length == 1 ? images[0] : notFound;
  902. });
  903. case 'www.imageshack.us': case 'imageshack.us':
  904. return globalXHR(url).then(response => response.document.querySelector('a#share-dl').href);
  905. case 'www.flickr.com': case 'flickr.com':
  906. if (url.pathname.startsWith('/photos/')) return globalXHR(url).then(function(response) {
  907. if (/\b(?:modelExport)\s*:\s*(\{.+\}),/.test(response.responseText)) try {
  908. let urls = JSON.parse(RegExp.$1).main['photo-models'].map(function(photoModel) {
  909. let sizes = Object.keys(photoModel.sizes).sort((a, b) => photoModel.sizes[b].width *
  910. photoModel.sizes[b].height - photoModel.sizes[a].width * photoModel.sizes[a].height);
  911. return sizes.length > 0 ? 'https:'.concat(photoModel.sizes[sizes[0]].url) : null;
  912. });
  913. if (urls.length == 1) return urls[0]; else if (urls.length > 1) return urls;
  914. } catch(e) { console.warn(e) }
  915. return notFound;
  916. }); else break;
  917. case 'photos.google.com':
  918. return googlePhotosResolver(url);
  919. case 'www.500px.com': case 'web.500px.com': case '500px.com':
  920. if (/^\/photo\/(\d+)\b/i.test(url.pathname))
  921. return _500pxUrlHandler('photos?ids='.concat(RegExp.$1));
  922. else if (/\/galleries\/([\w\%\-]+)/i.test(url.pathname)) {
  923. let galleryId = RegExp.$1;
  924. return globalXHR(url, { rsponseType: 'text' }).then(function(response) {
  925. if (!/\b(?:App\.CuratorId)\s*=\s*"(\d+)"/.test(response.responseText)) return Promise.reject('Unexpected page structure');
  926. return _500pxUrlHandler('users/' + RegExp.$1 + '/galleries/' + galleryId + '/items?sort=position&sort_direction=asc&rpp=999');
  927. });
  928. } else break;
  929. case 'www.pxhere.com': case 'pxhere.com':
  930. if (url.pathname.includes('/photo/')) return globalXHR(url).then(response =>
  931. JSON.parse(response.document.querySelector('div.hub-media-content > script[type="application/ld+json"]').text).contentUrl);
  932. else if (url.pathname.includes('/collection/')) return pxhereCollectionResolver(url);
  933. break;
  934. case 'www.unsplash.com': case 'unsplash.com':
  935. if (url.pathname.startsWith('/photos/')) return globalXHR(url.origin + url.pathname + '/download', { method: 'HEAD' })
  936. .then(response => response.finalUrl.replace(/\?.*$/, ''));
  937. else if (url.pathname.includes('/collections/')) return unsplashCollectionResolver(url);
  938. break;
  939. case 'www.pexels.com': case 'pexels.com':
  940. if (url.pathname.startsWith('/photo/')) return globalXHR(url)
  941. .then(response => response.document.querySelector('meta[property="og:image"][content]').content.replace(/\?.*$/, ''));
  942. else if (url.pathname.startsWith('/collections/')) return pexelsCollectionResolver(url);
  943. break;
  944. case 'www.piwigo.org': case 'piwigo.org':
  945. /*if (url.pathname.includes('/picture/')) */return globalXHR(url, { responseType: 'text' }).then(function(response) {
  946. if (/^(?:RVAS)\s*=\s*(\{[\S\s]+?\})$/m.test(response.responseText)) try {
  947. let derivatives = eval('(' + RegExp.$1 + ')').derivatives.sort((a, b) => b.w * b.h - a.w * a.h);
  948. return derivatives.length > 0 ? 'https://piwigo.org/demo/'.concat(derivatives[0].url) : notFound;
  949. } catch(e) { console.warn(e) }
  950. return Promise.reject('Unexpected page structure');
  951. });
  952. case 'www.freeimages.com': case 'freeimages.com':
  953. if (url.pathname.startsWith('/photo/')) return globalXHR(url).then(function(response) {
  954. let types = Array.from(response.document.querySelectorAll('ul.download-type > li > span.reso'))
  955. .sort((a, b) => eval(b.textContent.replace('x', '*')) - eval(a.textContent.replace('x', '*')));
  956. return types.length > 0 ? url.origin.concat(types[0].parentNode.querySelector('a').pathname) : notFound;
  957. }); else break;
  958. case 'redacted.ch':
  959. if (url.pathname == '/image.php') return globalXHR(url, { method: 'HEAD' }).then(response => response.finalUrl);
  960. else break;
  961. case 'demo.cloudimg.io': {
  962. if (!/\b(https?:\/\/\S+)$/.test(url.pathname.concat(url.search, url.hash))) break;
  963. let resolved = RegExp.$1;
  964. if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
  965. return imageUrlResolver(resolved, modifiers);
  966. }
  967. case 'www.pimpandhost.com': case 'pimpandhost.com':
  968. if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function(response) {
  969. let elem = resopnse.document.querySelector('div.main-image-wrapper');
  970. if (elem != null && elem.dataset.src) return 'https:'.concat(elem.dataset.src);
  971. elem = resopnse.document.querySelector('div.img-wrapper > a > img');
  972. return elem != null ? 'https:'.concat(elem.src) : notFound;
  973. }); else break;
  974. case 'www.screencast.com': case 'screencast.com':
  975. return globalXHR(url).then(function(response) {
  976. let ref = response.document.querySelectorAll('ul#containerContent > li a.media-link');
  977. if (ref.length <= 0) return getFromMeta(response.document) || notFound;
  978. return Promise.all(Array.from(ref).map(a => imageUrlResolver('https://www.screencast.com' + a.href, modifiers)));
  979. });
  980. case 'abload.de':
  981. if (url.pathname.startsWith('/image.php')) return globalXHR(url).then(function(response) {
  982. let elem = response.document.querySelector('img#image');
  983. if (elem == null) return notFound;
  984. let src = new URL(elem.src);
  985. return imageHostHandlers.abload.origin + src.pathname + src.search;
  986. }); else break;
  987. case 'fastpic.ru':
  988. if (url.pathname.startsWith('/view/'))
  989. return globalXHR(url).then(response => imageUrlResolver(response.document.querySelector('a.img-a').href, modifiers));
  990. else if (url.pathname.startsWith('/fullview/')) return globalXHR(url).then(function(response) {
  991. let node = response.document.getElementById('image');
  992. if (node != null) return node.src;
  993. return /\bvar\s+loading_img\s*=\s*'(\S+?)';/.test(response.responseText) ? RegExp.$1 : notFound;
  994. }); else break;
  995. case 'www.radikal.ru': case 'radikal.ru': case 'a.radikal.ru':
  996. return globalXHR(url).then(response => response.document.querySelector('div.mainBlock img').src);
  997. case 'imageban.ru': case 'ibn.im':
  998. return globalXHR(url).then(response => response.document.querySelector('a[download]').href);
  999. case 'svgshare.com':
  1000. return globalXHR(url).then(function(response) {
  1001. let link;
  1002. response.document.querySelectorAll('ul#shares > li > input[type="text"]')
  1003. .forEach(input => { if (!link && /^(?:https?:\/\/.+\.svg)$/.test(input.value)) link = input.value; });
  1004. return link || notFound;
  1005. });
  1006. case 'slow.pics':
  1007. if (url.pathname.startsWith('/c/')) return globalXHR(url).then(function(response) {
  1008. let nodes = response.document.querySelectorAll('img.card-img-top');
  1009. if (nodes.length > 1) return Array.from(nodes).map(img => img.src);
  1010. else if (nodes.length > 0) return nodes[0].src;
  1011. nodes = response.document.querySelectorAll('a#comparisons + div.dropdown-menu > a.dropdown-item');
  1012. if (nodes.length > 0) return Promise.all(Array.from(nodes).map(a => globalXHR(url.origin + a.pathname).then(response =>
  1013. Array.from(response.document.querySelectorAll('div#preload-images > img')).map(img => img.src))));
  1014. return notFound;
  1015. }); else break;
  1016. case 'www.amazon.com': case 'amazon.com':
  1017. case 'www.amazon.ae': case 'www.amazon.com.au': case 'www.amazon.com.br': case 'www.amazon.ca':
  1018. case 'www.amazon.cn': case 'www.amazon.de': case 'www.amazon.es': case 'www.amazon.fr':
  1019. case 'www.amazon.co.uk': case 'www.amazon.in': case 'www.amazon.it': case 'www.amazon.co.jp':
  1020. case 'www.amazon.com.mx': case 'www.amazon.nl': case 'www.amazon.sa': case 'www.amazon.se':
  1021. case 'www.amazon.sg': case 'www.amazon.com.tr':
  1022. return globalXHR(url).then(function(response) {
  1023. const rx = /\._\S+?_(?=\.)/,
  1024. getImgOrigin = colorImage => (colorImage.hiRes || colorImage.large || colorImage.thumb).replace(rx, '');
  1025. let obj = /^\s*(?:var\s+obj\s*=\s*jQuery\.parseJSON)\('(\{.+\})'\);/m.exec(response.responseText);
  1026. if (obj != null) {
  1027. try { obj = JSON.parse(obj[1]) } catch(e) { try { obj = eval('(' + obj[1] + ')') } catch(e) { obj = { } } }
  1028. let variants = Object.keys(obj.colorImages);
  1029. if (variants.length > 0) return variants.map(key => obj.colorImages[key].map(getImgOrigin));
  1030. }
  1031. let colorImages = /^\s*'colorImages':\s*(\{.+\}),?$/m.exec(response.responseText);
  1032. if (colorImages != null) {
  1033. try { colorImages = JSON.parse(colorImages[1].replace(/'/g, '"')) }
  1034. catch(e) { try { colorImages = eval('(' + colorImages[1] + ')') } catch(e) { colorImages = { } } }
  1035. if (Array.isArray(colorImages.initial) && colorImages.initial.length > 0)
  1036. return colorImages.initial.map(getImgOrigin);
  1037. }
  1038. let img = ['div#ppd-left img', 'img#igImage', 'img#imgBlkFront']
  1039. .reduce((acc, sel) => acc || response.document.querySelector(sel), null);
  1040. if (img == null) return notFound;
  1041. if (img.dataset.aDynamicImage) try {
  1042. let imgUrl = Object.keys(JSON.parse(img.dataset.aDynamicImage))[0];
  1043. if (urlParser.test(imgUrl)) return imgUrl.replace(rx, '');
  1044. } catch(e) { }
  1045. return urlParser.test(img.src) ? img.src.replace(rx, '') : notFound;
  1046. });
  1047. case 'www.casimages.com': case 'casimages.com':
  1048. if (url.pathname.startsWith('/i/')) return globalXHR(url).then(function(response) {
  1049. let elem = response.document.querySelector('div.logo > a');
  1050. if (elem != null) return elem.href;
  1051. elem = response.document.querySelector('div.logo img');
  1052. return elem != null ? elem.src : notFound;
  1053. }); else break;
  1054. case 'www.getapic.me': case 'getapic.me':
  1055. return globalXHR(url, { responseType: 'json' }).then(function(response) {
  1056. if (!response.response.result.success) return Promise.reject(response.response.result.errors);
  1057. if (Array.isArray(response.response.result.data.images))
  1058. return response.response.result.data.images.map(image => image.url);
  1059. return response.response.result.data.image ? response.response.result.data.image.url : notFound;
  1060. });
  1061. case 'sm.ms':
  1062. if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function(response) {
  1063. let img = response.document.querySelector('img.image');
  1064. return img != null ? img.src || img.parentElement.href : notFound;
  1065. }); else break;
  1066. case 'www.kizunaai.com': case 'kizunaai.com':
  1067. //if (!url.pathname.includes('/music/')) break;
  1068. return globalXHR(url).then(function(response) {
  1069. let img = response.document.querySelector('div.post-body span > img');
  1070. return img != null ? img.src.replace(/-\d+x\d+(?=\.\w+$)/, '') : notFound;
  1071. });
  1072. // music-related
  1073. case 'www.discogs.com': case 'discogs.com':
  1074. return globalXHR(url).then(response => (function() {
  1075. if (url.pathname.includes('/master/')) return Promise.reject('this is master');
  1076. if (modifiers.ctrlKey) return Promise.reject('master release inquiry avoided (force release gallery)');
  1077. let master = response.document.getElementById('all-versions-link');
  1078. if (master == null) return Promise.reject('no master release for this page');
  1079. return imageUrlResolver('https://www.discogs.com' + master.pathname, modifiers);
  1080. })().catch(function(reason) {
  1081. const imgMax = [/^(?:https?):\/\/(?:img\.discogs\.com)\/.+\/(\S+?\.\w+)\b(?:\.\w+)?$/i,
  1082. 'https://www.discogs.com/image/$1'];
  1083. let elem = response.document.querySelector('div.image_gallery, div.image_gallery_large');
  1084. if (elem != null) try {
  1085. if ((elem = JSON.parse(elem.dataset.images)).length <= 0) throw 'empty';
  1086. return elem.map(image => (image.full || image.thumb).replace(...imgMax));
  1087. } catch(e) { console.warn('Invalid Discogs image gallery:', elem, '(' + e + ')') } else {
  1088. console.warn('Missing Discogs image gallery record for', url.href);
  1089. }
  1090. return (elem = getFromMeta(response.document)) ? elem.replace(...imgMax) : notFound;
  1091. }));
  1092. case 'www.musicbrainz.org': case 'musicbrainz.org':
  1093. if (url.pathname.startsWith('/release/')) {
  1094. if (/^\/release\/([\w\-]+)(?=\/|$)/i.test(url.pathname)) url.pathname = '/release/' + RegExp.$1 + '/cover-art';
  1095. else console.warn('Unexpected MusicBrainz release url path:', url.pathname);
  1096. } else if (!url.pathname.startsWith('/release-group/')) break;
  1097. return globalXHR(url).then(response => (function() {
  1098. if (url.pathname.startsWith('/release-group/')) return Promise.reject('this is release group');
  1099. if (modifiers.ctrlKey) return Promise.reject('release group inquiry avoided (force release gallery)');
  1100. let releaseGroup = response.document.querySelector('p.subheader > span.small > a');
  1101. if (releaseGroup == null) return Promise.reject('no release group for this page');
  1102. return imageUrlResolver('https://musicbrainz.org' + releaseGroup.pathname, modifiers);
  1103. })().catch(function(reason) {
  1104. let elem = response.document.querySelector('head > script[type="application/ld+json"]');
  1105. if (elem != null) try {
  1106. if (Array.isArray(elem = JSON.parse(elem.text).image)) {
  1107. if (elem.length > 0) return elem.map(image => 'https:' + image.contentUrl);
  1108. } else if (elem && elem.contentUrl) return 'https:' + elem.contentUrl;
  1109. } catch(e) { console.warn('MusicBrainz: invalid meta record', elem) }
  1110. elem = response.document.querySelectorAll('div#content > div.artwork-cont span.cover-art-image > img');
  1111. if (elem.length > 0) return Array.from(elem).map(img => img.src.replace(/-\d+(?=(?:\.\w+)+$)/, ''));
  1112. return (elem = response.document.querySelector('a.artwork-image')) != null ? elem.href
  1113. : (elem = response.document.querySelector('div.cover-art > img')) != null ? elem.src : notFound;
  1114. }));
  1115. case 'www.allmusic.com': case 'allmusic.com':
  1116. if (url.pathname.startsWith('/album/')) return globalXHR(url).then(function(response) {
  1117. const imageMax = [/\b(?:f)=(\d+)\b/i, 'f=0'];
  1118. function amImgsXtractor(dom) {
  1119. if (dom instanceof Document) try {
  1120. //eval(dom.querySelector('div[class$="-cover"] script').text);
  1121. let imageGallery = JSON.parse(/(\[.+\]);/.exec(dom.querySelector('div[class$="-cover"] script').text)[1]);
  1122. if (imageGallery.length <= 0) throw 'empty gallery';
  1123. return imageGallery.map(image => (image.zoomURL || image.url).replace(...imageMax));
  1124. } catch(e) {
  1125. let img = dom.querySelector('div[class$="-cover"] img');
  1126. if (img != null) return (img.dataset.largeurl || img.src).replace(...imageMax);
  1127. }
  1128. return notFound;
  1129. }
  1130. return (function() {
  1131. const mainAlbum = response.document.querySelector('section.main-album a.album-title');
  1132. if (mainAlbum == null) return Promise.reject('no main album');
  1133. return globalXHR(mainAlbum.href).then(response => amImgsXtractor(response.document));
  1134. })().catch(reason => amImgsXtractor(response.document));
  1135. }); else if (url.pathname.startsWith('/artist/')) return globalXHR(url).then(function(response) {
  1136. const imgMax = /\b(?:f)=(\d+)\b/i, imageMax = imgUrl => verifyImageUrl(imgUrl.replace(imgMax, 'f=6'))
  1137. .catch(() => verifyImageUrl(imgUrl.replace(imgMax, 'f=0')))
  1138. .catch(() => verifyImageUrl(imgUrl.replace(imgMax, 'f=5')));
  1139. try {
  1140. //eval(response.document.querySelector('div.sidebar > script').text);
  1141. let imageGallery = JSON.parse(/(\[.+\]);/.exec(response.document.querySelector('div.sidebar > script').text)[1]);
  1142. if (imageGallery.length <= 0) throw 'empty gallery';
  1143. return Promise.all(imageGallery.map(image => imageMax(image.zoomURL || image.url)));
  1144. } catch(e) {
  1145. let img = response.document.querySelector('div.sidebar > div.artist-image img');
  1146. if (img != null) return imageMax(img.dataset.largeurl || img.src);
  1147. }
  1148. return notFound;
  1149. }); else break;
  1150. case 'music.apple.com': case 'itunes.apple.com': {
  1151. let appleId = amEntityParser.exec(url);
  1152. if (appleId != null) return globalXHR(url).then(function(response) {
  1153. let environment = response.document.querySelector('meta[name="desktop-music-app/config/environment"][content]');
  1154. if (environment != null) environment = JSON.parse(decodeURIComponent(environment.content));
  1155. else return Promise.reject('Apple desktop environment missing');
  1156. if (!environment.MEDIA_API.token) return Promise.reject('Apple API token missing');
  1157. return globalXHR(environment.MUSIC.BASE_URL + '/catalog/us/' + appleId[1] + 's/' + parseInt(appleId[2]), {
  1158. responseType: 'json',
  1159. headers: { 'Referer': response.finalUrl, 'Authorization': 'Bearer ' + environment.MEDIA_API.token },
  1160. }).then(function(response) {
  1161. const artwork = response.response.data[0].attributes.artwork;
  1162. return artwork ? artwork.url.replace('{w}', artwork.width).replace('{h}', artwork.height) : notFound;
  1163. });
  1164. }); else break;
  1165. }
  1166. case 'www.deezer.com': case 'deezer.com':
  1167. if (dzrEntityParser.test(url)) return globalXHR('https://api.deezer.com/' + RegExp.$1 + '/' + RegExp.$2 + '/image', {
  1168. method: 'HEAD',
  1169. }).then(response => verifyImageUrl(response.finalUrl.replace(...dzImageMax))).catch(function(reason) {
  1170. console.warn('Deezer API image retrieval failed:', reason, url);
  1171. return globalXHR(url).then(function(response) {
  1172. let meta = getFromMeta(response.document);
  1173. return meta ? verifyImageUrl(meta.replace(...dzImageMax)).catch(reason => meta) : notFound;
  1174. });
  1175. }); else break;
  1176. case 'www.qobuz.com': case 'qobuz.com':
  1177. if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
  1178. let img = response.document.querySelector('div.album-cover > img');
  1179. if (img == null) return getFromMeta(response.document) || notFound;
  1180. return verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_org'))
  1181. .catch(reason => verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_max')))
  1182. .catch(reason => img.src);
  1183. }); else break;
  1184. case 'www.boomkat.com': case 'boomkat.com':
  1185. if (url.pathname.startsWith('/products/')) return globalXHR(url).then(function(response) {
  1186. let img = response.document.querySelector('img[itemprop="image"]');
  1187. if (img == null) return notFound;
  1188. return verifyImageUrl(img.src.replace(/\/large\//i, '/original/')).catch(reason => img.src);
  1189. }); else break;
  1190. case 'www.bleep.com': case 'bleep.com':
  1191. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function(response) {
  1192. let meta = getFromMeta(response.document);
  1193. return meta ? verifyImageUrl(meta.replace(/\/r\/[a-z]\//i, '/r/')).catch(reason => meta) : notFound;
  1194. }); else break;
  1195. case 'www.soundcloud.com': case 'soundcloud.com':
  1196. return globalXHR(url).then(function(response) {
  1197. let meta = getFromMeta(response.document);
  1198. return meta ? verifyImageUrl(meta.replace(/\bt\d+x\d+(?=\.\w+$)/, 'original')).catch(reason => meta) : notFound;
  1199. });
  1200. case 'www.prestomusic.com': case 'prestomusic.com':
  1201. if (url.pathname.includes('/products/')) return globalXHR(url).then(response =>
  1202. verifyImageUrl(response.document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/))); else break;
  1203. case 'www.bontonland.cz':case 'bontonland.cz':
  1204. return globalXHR(url).then(response => response.document.querySelector('a.detailzoom').href);
  1205. case 'www.prostudiomasters.com': case 'prostudiomasters.com':
  1206. if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
  1207. let a = response.document.querySelector('img.album-art');
  1208. return verifyImageUrl(a.currentSrc).catch(reason => a.src);
  1209. }); else break;
  1210. case 'www.e-onkyo.com': case 'e-onkyo.com':
  1211. if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
  1212. let meta = getFromMeta(response.document);
  1213. return meta ? meta.replace(/\/s\d+\//, '/s0/') : notFound;
  1214. }); else break;
  1215. case 'store.acousticsounds.com':
  1216. return globalXHR(url).then(function(response) {
  1217. let link = response.document.querySelector('div#detail > link[rel="image_src"]');
  1218. return verifyImageUrl(link.href.replace(/\/medium\//i, '/xlarge/')).catch(reason => link.href);
  1219. });
  1220. case 'www.indies.eu': case 'indies.eu':
  1221. if (url.pathname.includes('/alba/')) return globalXHR(url)
  1222. .then(response => verifyImageUrl(response.document.querySelector('div.obrazekDetail > img').src)); else break;
  1223. case 'www.beatport.com': case 'classic.beatport.com': case 'pro.beatport.com': case 'beatport.com':
  1224. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function(response) {
  1225. let elem = getFromMeta(response.document);
  1226. return elem || ((elem = response.document.querySelector('div.artwork')) != null ?
  1227. 'https:' + elem.dataset.modalArtwork : notFound);
  1228. }).then(imgUrl => imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/image/')); else break;
  1229. case 'www.beatsource.com': case 'beatsource.com':
  1230. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function(response) {
  1231. let imgUrl = getFromMeta(response.document);
  1232. return imgUrl ? imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/') : notFound;
  1233. }); else break;
  1234. case 'www.supraphonline.cz': case 'supraphonline.cz':
  1235. if (!url.pathname.includes('/album/')) break;
  1236. return globalXHR(url).then(response => verifyImageUrl(response.document.querySelector('meta[itemprop="image"]')
  1237. .content.replace(/\?.*$/, '')).catch(reason => notFound));
  1238. case 'vgmdb.net':
  1239. if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
  1240. let div = response.document.querySelector('div#coverart');
  1241. return verifyImageUrl(/\b(?:url)\s*\(\"(.*)"\)/i.test(div.style['background-image']) && RegExp.$1).catch(reason => notFound);
  1242. }); else break;
  1243. case 'www.ototoy.jp': case 'ototoy.jp':
  1244. return globalXHR(url).then(function(response) {
  1245. let img = response.document.querySelector('div#jacket-full-wrapper > img'); // img[alt="album jacket"]
  1246. return img != null ? img.dataset.src || img.src : notFound;
  1247. });
  1248. case 'music.yandex.ru':
  1249. if (url.pathname.includes('/album/')) return globalXHR(url).then(function(response) {
  1250. let script = response.document.querySelector('script.light-data');
  1251. return verifyImageUrl(JSON.parse(script.text).image).catch(reason => notFound);
  1252. }); else break;
  1253. case 'www.pias.com': case 'store.pias.com': case 'pias.com':
  1254. return globalXHR(url).then(function(response) {
  1255. let node = getFromMeta(response.document);
  1256. if (node) return verifyImage(node.replace(/\/[sbl]\//i, '/')).catch(reason => node);
  1257. node = response.document.querySelector('img[itemprop="image"]');
  1258. return node != null ? verifyImage(node.src.replace(/\/[sbl]\//i, '/')).catch(reason => node.src) : notFound;
  1259. });
  1260. case 'www.eclassical.com': case 'eclassical.com':
  1261. return globalXHR(url).then(function(response) {
  1262. let a = response.document.querySelector('div#articleImage > a');
  1263. return a != null ? a.href : notFound;
  1264. });
  1265. case 'www.hdtracks.com': case 'hdtracks.com':
  1266. if (!/\/album\/(\w+)\b/.test(url)) break;
  1267. return fetch('https://hdtracks.azurewebsites.net/api/v1/album/' + RegExp.$1).then(response => response.json())
  1268. .then(result => result.status.toLowerCase() == 'ok' ? result.cover : Promise.reject(result.status));
  1269. case 'www.muziekweb.nl': case 'muziekweb.nl':
  1270. if (/\/Link\/(\w+)\b/i.test(url)) return globalXHR(url).then(function(response) {
  1271. let meta = getFromMeta(response.document)
  1272. return meta ? meta.replace(/\/COVER\/\w+\b/i, '/COVER/SUPERLARGE') : notFound;
  1273. }); else break;
  1274. case 'www.deejay.de': case 'deejay.de':
  1275. return globalXHR(url).then(function(response) {
  1276. let elem = response.document.querySelector('div#gallery > a') || response.document.querySelector('div.cover a');
  1277. if (elem != null) return 'https://www.deejay.de' + elem.pathname;
  1278. return (elem = getFromMeta(response.document)) ? elem : notFound;
  1279. }).then(imgUrl => verifyImageUrl(imgUrl.replace(/\/images\/\w+\//i, '/images/xxl/')).catch(() => imgUrl));
  1280. case 'music.163.com':
  1281. if (!/\/album.*\b(?:id)=(\d+)\b/i.test(url.href)) break;
  1282. return globalXHR('https://music.163.com/api/album/' + RegExp.$1, { responseType: 'json' })
  1283. .then(response => response.response.album.picUrl ?
  1284. response.response.album.picUrl.replace(/\b(?:p[123])(?=\.music\.\d+\.net\b)/i, 'p4') : notFound);
  1285. case 'www.tidal.com': case 'tidal.com':
  1286. if (!(/\/album\/(\d+)(?:\/|$)/i.test(url.pathname) && !/\b(?:albumId)=(\d+)\b/i.test(url.search))) break;
  1287. return globalXHR('https://api.tidal.com/v1/albums/' + RegExp.$1 + '?countrycode=US&token=_DSTon1kC8pABnTw', {
  1288. responseType: 'json',
  1289. }).then(response => response.response.cover ? 'https://resources.tidal.com/images/' + response.response.cover.replace(/-/g, '/') + '/1280x1280.jpg' : notFound);
  1290. case 'www.extrememusic.com': case 'extrememusic.com':
  1291. if (url.pathname.startsWith('/albums/')) return globalXHR(url).then(function(response) {
  1292. let meta = getFromMeta(response.document);
  1293. return meta ? meta.replace(/\/album\/\w+\//i, '/album/600/') : notFound;
  1294. }); else break;
  1295. case 'www.recochoku.jp': case 'recochoku.jp':
  1296. if (url.pathname.startsWith('/album/')) return globalXHR(url).then(function(response) {
  1297. let imgUrl = getFromMeta(response.document);
  1298. if (!imgUrl) return notFound;
  1299. imgUrl = new URL(imgUrl);
  1300. let params = new URLSearchParams(imgUrl.search);
  1301. params.set('FFw', 999999999); params.set('FFh', 999999999);
  1302. params.delete('h'); params.delete('option');
  1303. imgUrl.search = params;
  1304. return imgUrl;
  1305. }); else break;
  1306. case 'www.elusivedisc.com': case 'elusivedisc.com':
  1307. return globalXHR(url).then(function(response) {
  1308. let img = response.document.querySelector('figure > img.zoomImg');
  1309. if (img != null) return img.src;
  1310. img = response.document.querySelector('section.productView-images > figure');
  1311. return img != null && img.dataset.zoomImage || notFound;
  1312. });
  1313. // movie-related
  1314. case 'www.imdb.com': case 'imdb.com':
  1315. if (!['title/tt', 'name/nm'].some(cat => url.pathname.startsWith('/' + cat))) break;
  1316. return globalXHR(url).then(function(response) {
  1317. const galleryDetector = /\/mediaindex(?:[\/\?].*)?$/i, imgStripper = /\._V\d+_[\w\,]*(?=\.)/;
  1318. if (!galleryDetector.test(response.finalUrl)) {
  1319. let node = response.document.head.querySelector(':scope > script[type="application/ld+json"]');
  1320. if (node != null) try {
  1321. let image = JSON.parse(node.text).image;
  1322. if (typeof image == 'string') return verifyImageUrl(image.replace(imgStripper, '')).catch(reason => notFound);
  1323. } catch(e) { console.warn(e) }
  1324. node = response.document.querySelector('meta[property="og:image"][content]');
  1325. return node != null && !/\/imdb\w*_logo\./i.test(node.content) ?
  1326. node.content.replace(imgStripper, '') : notFound;
  1327. }
  1328. var titleId = /\/title\/(tt\d+)\//i.test(response.finalUrl) && RegExp.$1;
  1329. return titleId ? globalXHR(response.finalUrl.replace(galleryDetector, '/mediaviewer'), { responseType: 'text' }).then(function(response) {
  1330. if (/\b(?:window\.IMDbMediaViewerInitialState)\s*=\s*(\{.*\});/.test(response.responseText)) try {
  1331. let allImages = eval('(' + RegExp.$1 + ')').mediaviewer.galleries[titleId].allImages;
  1332. if (allImages.length > 0) return allImages.map(image => image.src.replace(imgStripper, ''));
  1333. } catch(e) { console.warn(e) }
  1334. return notFound;
  1335. }) : Promise.reject('title id not found');
  1336. });
  1337. case 'www.themoviedb.org': case 'themoviedb.org':
  1338. if (!['movie', 'person'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1339. return globalXHR(url).then(function(response) {
  1340. let node = response.document.querySelector('meta[property="og:image"][content]');
  1341. return verifyImageUrl(node.content.replace(/\/p\/\w+\//i, '/p/original/')).catch(function(reason) {
  1342. node = response.document.querySelector('div.image_content > img');
  1343. return verifyImageUrl(node.dataset.src.replace(/\/p\/\w+\//i, '/p/original/'))
  1344. .catch(reason => verifyImageUrl(node.src.replace(/\/p\/\w+\//i, '/p/original/')))
  1345. .catch(reason => verifyImageUrl(dataset.src)).catch(reason => node.src);
  1346. }).catch(reason => notFound);
  1347. });
  1348. case 'www.omdb.org': case 'omdb.org':
  1349. if (!['movie', 'person'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1350. return globalXHR(url).then(function(response) {
  1351. let node = response.document.querySelector('meta[property="og:image"][content]');
  1352. return node != null ? verifyImageUrl(node.content) : notFound;
  1353. });
  1354. case 'www.thetvdb.com': case 'thetvdb.com':
  1355. if (!['movies', 'series', 'people'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1356. return globalXHR(url).then(response => verifyImageUrl(response.document.querySelector('img.img-responsive').src));
  1357. case 'www.rottentomatoes.com': case 'rottentomatoes.com':
  1358. if (!['m', 'celebrity', 'tv'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1359. return globalXHR(url).then(function(response) {
  1360. //if (/\b(?:context\.shell)\s*=\s*(\{.+?});/.test(response.responseText)) try {
  1361. // return JSON.parse(RegExp.$1).header.certifiedMedia.certifiedFreshMovieInTheater4.media.posterImg;
  1362. //} catch(e) { console.warn(e) }
  1363. return verifyImageUrl(response.document.querySelector('meta[property="og:image"]').content);
  1364. });
  1365. case 'www.bcdb.com': case 'bcdb.com':
  1366. if (!['cartoon'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1367. return globalXHR(url).then(response =>
  1368. verifyImageUrl(document.location.protocol.concat(response.document.querySelector('meta[property="og:image"]').content)));
  1369. case 'www.boxofficemojo.com': case 'boxofficemojo.com':
  1370. if (!['releasegroup'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1371. return globalXHR(url).then(response => verifyImageUrl(response.document.querySelector('div.mojo-primary-image img').src));
  1372. case 'www.metacritic.com': case 'metacritic.com':
  1373. return globalXHR(url).then(function(response) {
  1374. let image = response.document.querySelector('meta[property="og:image"]').content;
  1375. return verifyImageUrl(image.replace(/-\d+h(?=(?:\.\w+)?$)/, '')).catch(reason => image);
  1376. });
  1377. case 'www.csfd.cz': case 'csfd.cz':
  1378. if (!['film', 'tvurce'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1379. return globalXHR(url).then(function(response) {
  1380. const gallerySel = 'div.ct-general.photos > div.content > ul > li > div.photo';
  1381. if (response.document.querySelectorAll(gallerySel).length > 0) return new Promise(function(resolve, reject) {
  1382. let urls = [], origin = new URL(response.finalUrl).origin;
  1383. loadPage(response.finalUrl.replace(/\/strana-\d+(?=$|\/|\?)/, ''));
  1384.  
  1385. function loadPage(url) {
  1386. GM_xmlhttpRequest({ method: 'GET', url: url,
  1387. onload: function(response) {
  1388. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  1389. let dom = domParser.parseFromString(response.responseText, 'text/html');
  1390. Array.prototype.push.apply(urls, Array.from(dom.querySelectorAll(gallerySel))
  1391. .map(div => /^(?:url)\s*\("?(.+?)"?\)$/i.test(div.style.backgroundImage) ?
  1392. 'https:'.concat(RegExp.$1).replace(/\?.*$/, '') : null));
  1393. let nextPage = dom.querySelector('div.paginator > a.next[href]');
  1394. if (nextPage != null) loadPage(origin.concat(nextPage.pathname, nextPage.search)); else resolve(urls);
  1395. },
  1396. onerror: response => { reject(defaultErrorHandler(response)) },
  1397. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  1398. });
  1399. }
  1400. });
  1401. let img = ['img.film-poster', 'img.creator-photo', 'div.image > img']
  1402. .reduce((acc, selector) => acc || response.document.querySelector(selector), null);
  1403. return img != null ? verifyImageUrl(img.src.replace(/\?.*$/, '')) : notFound;
  1404. });
  1405. case 'www.fdb.cz': case 'fdb.cz':
  1406. //if (!url.pathname.startsWith('/film/')) break;
  1407. return globalXHR(url).then(function(response) {
  1408. let a = response.document.querySelector('a.boxPlakaty');
  1409. if (a == null) return Promise.reject('Invalid page structure');
  1410. a.hostname = 'www.fdb.cz';
  1411. return globalXHR(a.href).then(function(response) {
  1412. let imgs = response.document.querySelectorAll('span#popup_plakaty > img');
  1413. return imgs.length > 0 ? verifyImageUrl(imgs[0].src) : notFound;
  1414. });
  1415. });
  1416. case 'www.caps-a-holic.com': case 'caps-a-holic.com':
  1417. if (url.pathname == '/c.php') return globalXHR(url).then(function(response) {
  1418. function heightExtractor(n) {
  1419. let node = response.document.querySelector('div.main > div.c_table > div[style]:nth-of-type(' + n + ')');
  1420. if (node != null && /\b(\d{3,})\s?[x×]\s?(\d{3,})\b/.test(node.textContent)) return parseInt(RegExp.$2);
  1421. console.warn(response.finalUrl, 'failed to get resolution (' + n + ')', node);
  1422. return null;
  1423. }
  1424. const baseUrl = 'https://caps-a-holic.com/c_image.php?a=0&x=0&y=0&l=1';
  1425. return Array.from(response.document.querySelectorAll('div.main > div[style] > a > img.thumb')).map(function(img) {
  1426. let query = new URLSearchParams(new URL(img.parentNode.href).search);
  1427. return [
  1428. `${baseUrl}&s=${parseInt(query.get('s1'))}&max_height=${heightExtractor(2)}`,
  1429. `${baseUrl}&s=${parseInt(query.get('s2'))}&max_height=${heightExtractor(3)}`,
  1430. ];
  1431. });
  1432. }); else break;
  1433. case 'www.screenshotcomparison.com': case 'screenshotcomparison.com':
  1434. if (url.pathname.startsWith('/comparison/')) return globalXHR(url).then(function(response) {
  1435. const origin = new URL(response.finalUrl).origin;
  1436. return Array.from(response.document.querySelectorAll('div#img_nav li > a')).map(function(a) {
  1437. return globalXHR(origin.concat(a.pathname), { responseType: 'text' }).then(response => [
  1438. /\b(?:images)\[1\]='(\S+?)'/.test(response.responseText) && RegExp.$1,
  1439. /\b(?:images)\[0\]='(\S+?)'/.test(response.responseText) && RegExp.$1,
  1440. ].map(src => origin.concat(src)));
  1441. });
  1442. }); else break;
  1443. case 'www.dvdbeaver.com': case 'dvdbeaver.com':
  1444. if (url.pathname.startsWith('/film')) return globalXHR(url).then(function(response) {
  1445. const origin = new URL(response.finalUrl).origin;
  1446. return Array.from(response.document.querySelectorAll('div[align="center"] > table > tbody > tr > td > a[target="_blank"] > img'))
  1447. .map(img => origin.concat(img.parentNode.pathname));
  1448. }); else break;
  1449. }
  1450. return globalXHR(url, { headers: { 'Referer': url.origin } }).then(function(response) {
  1451. if (url.pathname.startsWith('/album/')
  1452. && response.document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
  1453. return new Chevereto(url.hostname).galleryResolver(url);
  1454. return getFromMeta(response.document) || notFound;
  1455. });
  1456. }));
  1457. }
  1458.  
  1459. function logFail(message) {
  1460. let log = document.getElementById('ihh-console');
  1461. if (log == null) {
  1462. log = document.createElement('div');
  1463. log.id = 'ihh-console';
  1464. log.style = 'position: fixed; bottom: 20px; right: 20px; width: 64em; border: solid lightsalmon 4px;' +
  1465. ' background-color: antiquewhite; padding: 10px; opacity: 1;' +
  1466. ' transition: opacity 1000ms linear; -webkit-transition: opacity 1000ms linear;';
  1467. document.body.append(log);
  1468. } else if (log.hTimer) {
  1469. clearTimeout(log.hTimer);
  1470. log.style.opacity = 1;
  1471. }
  1472. let div = document.createElement('div');
  1473. div.style = 'font: 600 9pt Verdana, sans-serif; color: red;';
  1474. div.textContent = message;
  1475. log.append(div);
  1476. log.hTimer = setTimeout(function(node) {
  1477. node.style.opacity = 0;
  1478. node.hTimer = setTimeout(function(node) { node.remove() }, 1000, node);
  1479. }, 30000, log);
  1480. }