Image Host Helper

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

  1. // ==UserScript==
  2. // @name Image Host Helper
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.91.7
  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-24, Anakunda (https://greasyfork.org/users/321857-anakunda)
  9. // @license GPL-3.0-or-later
  10. // @match https://passthepopcorn.me/*
  11. // @match https://redacted.sh/*
  12. // @match https://orpheus.network/*
  13. // @match https://broadcasthe.net/*
  14. // @match https://notwhat.cd/*
  15. // @match https://dicmusic.club/*
  16. // @match https://dicmusic.com/*
  17. // @match https://*/torrents.php?id=*
  18. // @match https://*/artist.php?id=*
  19. // @match https://*/artist.php?action=edit&artistid=*
  20. // @match https://*/reportsv2.php?action=report&id=*
  21. // @match https://*/forums.php?action=new*
  22. // @match https://*/forums.php?*action=viewthread*
  23. // @match https://*/requests.php?action=view*
  24. // @match https://*/collages.php?id=*
  25. // @match https://*/collages.php?page=*&id=*
  26. // @match https://*/collages.php?action=edit&collageid=*
  27. // @match https://*/collages.php?action=comments&collageid=*
  28. // @match https://*/collages.php?action=new
  29. // @match https://*/collage.php?id=*
  30. // @match https://*/collage.php?page=*&id=*
  31. // @match https://*/collage.php?action=edit&collageid=*
  32. // @match https://*/collage.php?action=comments&collageid=*
  33. // @match https://*/collage.php?action=new
  34. // @match http*://tracker.czech-server.com/*
  35. // @connect *
  36. // @grant GM_xmlhttpRequest
  37. // @grant GM_getValue
  38. // @grant GM_setValue
  39. // @grant GM_deleteValue
  40. // @grant GM_registerMenuCommand
  41. // @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
  42. // @require https://openuserjs.org/src/libs/Anakunda/libCtxtMenu.min.js
  43. // @require https://openuserjs.org/src/libs/Anakunda/progressBars.min.js
  44. // @require https://openuserjs.org/src/libs/Anakunda/imageHostUploader.min.js
  45. // ==/UserScript==
  46.  
  47. 'use strict';
  48.  
  49. const previewDelay = GM_getValue('preview_delay', 12);
  50. const amEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*apple\.com\/(?:\S+\/)?(album|artist|playlist)\/(?:[\w\%\-]+\/)?(?:id)?(\d+)\b/i;
  51. const itunesImageMax = [/\/(\d+x\d+)\w*\.(\w+)$/, '/100000x100000-999.' +
  52. (GM_getValue('apple_get_png_cover', false) ? 'png' : '$2')];
  53. const dzrEntityParser = /^(?:https?):\/\/(?:[\w\%\-]+\.)*deezer\.com\/(?:\S+\/)?(album|artist|track|comment|playlist|radio|user)\/(\d+)\b/i;
  54. const dzrImageMax = GM_getValue('deezer_get_png_cover', false) ? [/\/(\d+x\d+)(?:\-\d+)*\.\w+$/, '/1400x1400.png']
  55. : [/\/(\d+x\d+)(?:\-\d+)*(?=\.\w+$)/, '/1400x1400-000000-' + (parseInt(GM_getValue('deezer_jpeg_quality')) || 100) + '-0-0'];
  56. const discogsOrigin = 'https://www.discogs.com';
  57. const discogsKey = GM_getValue('discogsKey', 'LWiNvIWBobGMRhfSCAiC');
  58. const discogsSecret = GM_getValue('discogsSecret', 'HAQUKFmebpCSLyRNwjmSgOMgbnxsVQcp');
  59. const lfmApiKey = GM_getValue('lfmApiKey', '920db0d2f86108f2fbe1917b53d63858');
  60.  
  61. function getDiscogsImageMax(imageUrl) {
  62. if (!httpParser.test(imageUrl)) return Promise.reject('Image URL is not valid');
  63. if (imageUrl.endsWith('/images/spacer.gif')) return Promise.reject('Dummy image (placeholder)');
  64. const matches = [
  65. /^(?:https?):\/\/(?:(?:img|i)\.discogs\.com)\/.+\/([\w\%\-]+\.\w+)\b(?:\.\w+)*$/i,
  66. ].map(rx => rx.exec(imageUrl));
  67. if (matches[0] != null) return verifyImageUrl(discogsOrigin + '/image/' + matches[0][1]).catch(reason => imageUrl);
  68. return Promise.resolve(imageUrl);
  69. }
  70.  
  71. function getDeezerImageMax(imageUrl) {
  72. if (!httpParser.test(imageUrl)) return Promise.reject('invalid image URL');
  73. const dzrImgResParser = /\/(\d+x\d+)(?:\-\d+)*\.(\w+)$/;
  74. let ext = dzrImgResParser.exec(imageUrl);
  75. if (ext != null) ext = GM_getValue('deezer_get_png_cover', false) ? 'png' : ext[2]; else {
  76. console.warn('Unscalable Deezer image, returning unchanged:', imageUrl);
  77. return Promise.resolve(imageUrl);
  78. }
  79. const urlByResolution = resolution => imageUrl.replace(dzrImgResParser, '/' + resolution + 'x' + resolution) +
  80. (/^j(?:pe?g|fif)$/i.test(ext) ? `-000000-${parseInt(GM_getValue('deezer_jpeg_quality')) || 100}-0-0.${ext}` : '.' + ext);
  81. const deezerHighestResolution = Math.max(parseInt(GM_getValue('deezer_highest_resolution')) || 1500, 1200);
  82. const defaultMax = (res = deezerHighestResolution) => verifyImageUrl(urlByResolution(res)).catch(reason => imageUrl);
  83. const resolutions = [/*1200, */1400, 1425, 1440, 1500, 1600, 1800, 1920].filter(size => size <= deezerHighestResolution);
  84. return Math.max(...resolutions) > 1400 ? Promise.all(resolutions.map(res => new Promise(function(resolve, reject) {
  85. const img = document.createElement('img');
  86. img.onload = load => { resolve(load.target.naturalWidth * load.target.naturalHeight) };
  87. img.onerror = (message, source, lineno, colno, error) => { reject(message) };
  88. img.src = imageUrl.replace(dzrImgResParser, '/' + res + 'x' + res + '.png');
  89. }).catch(reason => -Infinity))).then(function(pixTotals) {
  90. let maxArea = Math.max(...pixTotals);
  91. if (maxArea <= 0) {
  92. console.warn('Deezer: no max variant returns valid image', pixTotals, imageUrl);
  93. return Promise.reject('all size variants failed to load'); //defaultMax()
  94. }
  95. return urlByResolution(resolutions[pixTotals.indexOf(maxArea)]);
  96. }) : defaultMax(deezerHighestResolution);
  97. }
  98.  
  99. function logFail(message, timeout = 30) {
  100. if (!message) return;
  101. let console = document.getElementById('ihh-console');
  102. if (console == null) {
  103. console = document.createElement('DIV');
  104. console.id = 'ihh-console';
  105. console.style = `
  106. position: fixed; bottom: 20px; right: 20px; width: fit-content; max-width: 66%; max-height: 66%; z-index: 10000001;
  107. padding: 10px; overflow-y: auto; overscroll-behavior-y: none; scrollbar-gutter: stable;
  108. border: solid lightsalmon 4px;
  109. color: #c00; background-color: antiquewhite; opacity: 1;
  110. font: 600 10pt "Segoe UI", Verdana, sans-serif; text-align: left; line-height: 1em;
  111. transition: opacity 1000ms linear; -webkit-transition: opacity 1000ms linear;
  112. `;
  113. document.body.append(console);
  114. } else if ('hTimer' in console) {
  115. if (console.hTimer) clearTimeout(console.hTimer);
  116. console.style.opacity = 1;
  117. }
  118. const entry = document.createElement('DIV');
  119. entry.class = 'ihh-console-entry';
  120. entry.style = 'display: block;';
  121. if (console.childElementCount > 0) entry.style.marginTop = '3pt';
  122. entry.textContent = message;
  123. console.append(entry);
  124. console.scrollTop = console.scrollHeight;
  125. if (timeout > 0) console.hTimer = setTimeout(function(elem) {
  126. elem.style.opacity = 0;
  127. elem.hTimer = setTimeout(elem => { elem.remove() }, Math.min(timeout, 1) * 1000, elem);
  128. }, Math.max(timeout - 1, 1) * 1000, console);
  129. }
  130.  
  131. let cheveretoCustomHosts = GM_getValue('chevereto_custom_hosts');
  132. if (cheveretoCustomHosts) {
  133. if (!Array.isArray(cheveretoCustomHosts)) try {
  134. cheveretoCustomHosts = JSON.parse(cheveretoCustomHosts);
  135. if (Array.isArray(cheveretoCustomHosts)) GM_setValue('chevereto_custom_hosts', cheveretoCustomHosts)
  136. } catch(e) { console.warnd(e) }
  137. if (Array.isArray(cheveretoCustomHosts)) for (let siteDef of cheveretoCustomHosts)
  138. if (siteDef.host_name && siteDef.alias) {
  139. const key = siteDef.alias.replace(nonWordStripper, '').toLowerCase();
  140. imageHostHandlers[key] = new Chevereto(siteDef.host_name, siteDef.alias, siteDef.types, siteDef.size_limit, {
  141. sizeLimitAnonymous: siteDef.size_limit_anonymous,
  142. configPrefix: siteDef.config_prefix,
  143. apiEndpoint: siteDef.api_endpoint,
  144. apiFieldName: siteDef.api_field_name,
  145. apiResultKey: siteDef.api_result_key,
  146. jsonEndpoint: siteDef.json_endpoint,
  147. });
  148. } else console.warn('Incomplete Chevereto custom site definition (excluded from chaining):', siteDef);
  149. else {
  150. console.warn('chevereto_custom_hosts invalid format (', cheveretoCustomHosts, ')');
  151. //GM_deleteValue('chevereto_custom_hosts');
  152. }
  153. }
  154. console.log('Image host handlers:', imageHostHandlers);
  155.  
  156. PTPimg.prototype.setSession = function() {
  157. return this.apiKey ? Promise.resolve(this.apiKey) : globalXHR(this.origin).then(({document}) => {
  158. var apiKey = document.getElementById('api_key');
  159. if (apiKey == null) {
  160. let counter = GM_getValue('ptpimg_reminder_read', 0);
  161. if (counter < 3) {
  162. alert(`
  163. ${this.alias} API key could not be captured. Please login to ${this.origin}/ and redo the action.
  164. If you don\'t have PTPimg account or don\'t want to use it, consider to remove PTPimg from
  165. 'upload_hosts' and 'rehost_hosts' storage entries, and all sites' local hostlists where does it appear.
  166. `);
  167. GM_setValue('ptpimg_reminder_read', ++counter);
  168. }
  169. return Promise.reject('API key not configured');
  170. } else if (!(this.apiKey = apiKey.value)) return Promise.reject('assertion failed: empty PTPimg API key');
  171. GM_setValue('ptpimg_api_key', this.apiKey);
  172. Promise.resolve(this.apiKey)
  173. .then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) });
  174. return this.apiKey;
  175. });
  176. }
  177.  
  178. const generalImgHosts = [
  179. 'ImgBB', 'PixHost', 'ImgBox', 'Slowpoke', 'PostImage', 'Gifyu',
  180. 'Ra', 'Abload', 'VgyMe', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS',
  181. 'PomfCat', 'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr', 'LinkPicture',
  182. 'Imgur', 'Catbox', 'ImageVenue', 'GetaPic', 'FastPic', 'SVGshare',
  183. ];
  184. ['upload_hosts', 'rehost_hosts'].forEach(propName => { if (!GM_getValue(propName))
  185. GM_setValue(propName, ['PTPimg'].concat(generalImgHosts).join(', ')) });
  186. [
  187. ['passthepopcorn.me', [
  188. 'PTPimg', 'ImgBB', 'PixHost', 'ImgBox', 'Slowpoke', 'Gifyu',
  189. 'Ra', 'Abload', 'VgyMe', 'GeekPic', 'LightShot', 'ImgURL', 'Radikal', 'Z4A', 'PicaBox', 'PimpAndHost', 'SMMS',
  190. 'PomfCat', 'CasImages', 'CubeUpload', 'GooPics', 'ImageBan', 'UuploadIr',
  191. 'Catbox', 'ImageVenue', 'GetaPic',
  192. ]],
  193. ['notwhat.cd', ['NWCD']],
  194. ['forum.mobilism.org', ['Mobilism'].concat(generalImgHosts)],
  195. ['forum.mobilism.me', ['Mobilism'].concat(generalImgHosts)],
  196. ].forEach(hostDefaults => { if (!GM_getValue(hostDefaults[0])) GM_setValue(hostDefaults[0], hostDefaults[1].join(', ')) });
  197.  
  198. var imageHosts = new ImageHostManager(logFail,
  199. GM_getValue(document.domain) || GM_getValue('upload_hosts'),
  200. GM_getValue(document.domain) || GM_getValue('rehost_hosts'));
  201.  
  202. const queryAppleAPI = (endPoint, params, market = 'us') => endPoint ? (function() {
  203. const configValidator = config => config && config.MEDIA_API && config.MEDIA_API.token
  204. && (!config.timeStamp || config.timeStamp + 7 * 24 * 60*60*1000 >= Date.now() + 30 * 1000);
  205. if ('appleMusicDesktopConfig' in localStorage) try {
  206. var config = JSON.parse(localStorage.getItem('appleMusicDesktopConfig'));
  207. if (!configValidator(config)) throw 'Expired or incomplete cached Apple Music desktop environment';
  208. console.info('Re-using cached Apple Music desktop environment:', config);
  209. return Promise.resolve(config);
  210. } catch(e) {
  211. console.info(e, localStorage.appleMusicDesktopConfig);
  212. localStorage.removeItem('appleMusicDesktopConfig');
  213. }
  214. const timeStamp = Date.now();
  215. return globalXHR('https://music.apple.com/').then(function({document}) {
  216. if ((config = document.head.querySelector('meta[name="desktop-music-app/config/environment"][content]')) != null) try {
  217. (config = JSON.parse(decodeURIComponent(config.content))).timeStamp = timeStamp;
  218. if (configValidator(config)) return config;
  219. } catch(e) { console.warn('Invalid Apple Music desktop environment format:', e, config.content) }
  220. if ((config = document.head.querySelector('script[type="module"][src]')) != null)
  221. return globalXHR(new URL(config.getAttribute('src'), 'https://music.apple.com'), { responseType: 'text' }).then(({responseText}) =>
  222. (config = /\b(?:const\s+kd\s*=\s*['"]([^\s'"]{64,}?)|\w+\s*=\s*['"]([^\s'"]{268}))['"]/.exec(responseText)) != null
  223. && configValidator(config = {
  224. MEDIA_API : { token: config[1] || config[2] },
  225. timeStamp: timeStamp,
  226. }) ? config : Promise.reject('Missing Apple Music OAuth2 token'));
  227. return Promise.reject('Missing Apple Music OAuth2 token');
  228. }).then(function(config) {
  229. console.info('Apple Music OAuth2 token successfully extracted:', config.MEDIA_API.token);
  230. localStorage.setItem('appleMusicDesktopConfig', JSON.stringify(config));
  231. return config;
  232. });
  233. })().then(function request(config) {
  234. if (!config.retryCounter) config.retryCounter = 0;
  235. let url = config.MUSIC && config.MUSIC.BASE_URL || 'https://amp-api.music.apple.com/v1';
  236. url = new URL(`${url}/catalog/${market || 'us'}/${endPoint.replace(/^\/+|\/+$/g, '')}`);
  237. if (params) url.search = new URLSearchParams(params);
  238. url.searchParams.set('omit[resource]', 'relationships,views,meta,autos');
  239. url.searchParams.set('l', config.i18n && config.i18n.defaultLocale || 'en-us');
  240. url.searchParams.set('platform', 'web');
  241. return globalXHR(url, {
  242. responseType: 'json',
  243. headers: {
  244. Referer: 'https://music.apple.com/',
  245. Origin: 'https://music.apple.com',
  246. Host: url.hostname,
  247. Authorization: 'Bearer ' + config.MEDIA_API.token,
  248. },
  249. }).then(({response}) => response, function(reason) {
  250. let status = /^HTTP error (\d+)\b/.exec(reason);
  251. if (status != null) status = parseInt(status[1]);
  252. if ([400, 401, 403].includes(status)) {
  253. localStorage.removeItem('appleMusicDesktopConfig');
  254. if (config.retryCounter++ <= 0) return request(config);
  255. alert('Apple Music request problem:\n' + reason + '\n(retry with new token)');
  256. //return queryAppleAPI(endPoint, params);
  257. }
  258. return Promise.reject(reason);
  259. });
  260. }) : Promise.reject('Endpoint is missing');
  261.  
  262. const tidalAccess = {
  263. apiBase: 'https://api.tidal.com/v1',
  264. clientId: GM_getValue('tidal_clientid', localStorage.getItem('tidalClientId')
  265. || 'zU4XHVVkc2tDPo4t' || '7m7Ap0JC9j1cOM3n'),
  266. clientSecret: GM_getValue('tidal_clientsecret', localStorage.getItem('tidalClientSecret')
  267. || 'VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=' || 'vRAdA108tlvkJpTsGZS8rGZ7xTlbJ0qaZ2K9saEzsgY='),
  268. auth: null,
  269.  
  270. authorize: function() {
  271. const oAuth2base = 'https://auth.tidal.com/v1/oauth2',
  272. devAuthEndpoint = oAuth2base + '/device_authorization',
  273. tokenEndpoint = oAuth2base + '/token',
  274. scopes = ['r_usr', 'w_usr', 'w_sub'],
  275. oAuth2timeReserve = 30; // reserve this time (s) for upcoming authorized request
  276. const isTokenValid = accessToken => typeof accessToken == 'object' && accessToken.token_type
  277. && accessToken.access_token && accessToken.expires_at >= Date.now() + oAuth2timeReserve * 1000;
  278. const isSessionValid = session => session && typeof session == 'object' && session.userId > 0 && session.sessionId;
  279. const authMethods = {
  280. 'SessionId': () => Promise.reject('Method removed'),
  281. 'DeviceToken': () => Promise.reject('Method disabled'), //Promise.resolve([undefined, { 'token': this.clientId }]),
  282. 'OAuth2': () => (function() {
  283. if ('tidalAccessToken' in localStorage) try {
  284. var accessToken = JSON.parse(localStorage.getItem('tidalAccessToken'));
  285. if (isTokenValid(accessToken)) return Promise.resolve(accessToken);
  286. } catch(e) { localStorage.removeItem('tidalAccessToken') }
  287. if (!this.clientId || !this.clientSecret)
  288. return Promise.reject('Tidal credentials not configured (OAuth2-deviceFlow)');
  289. let timeStamp;
  290. return (accessToken && accessToken.refresh_token ? (function() {
  291. timeStamp = Date.now();
  292. return globalXHR(tokenEndpoint, { responseType: 'json' }, new URLSearchParams({
  293. grant_type: 'refresh_token',
  294. refresh_token: accessToken.refresh_token,
  295. client_id: this.clientId,
  296. client_secret: this.clientSecret,
  297. })).then(({response}) => {
  298. if (!response.refresh_token) response.refresh_token = accessToken.refresh_token;
  299. return response;
  300. });
  301. }).call(this) : Promise.reject('Cached token not available')).then(response => {
  302. if (typeof response != 'object') throw 'invalid response';
  303. console.assert(timeStamp > 0, 'timeStamp > 0');
  304. const tzOffset = new Date().getTimezoneOffset() * 60 * 1000;
  305. accessToken = response;
  306. if (!accessToken.timestamp) accessToken.timestamp = timeStamp;
  307. if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp +
  308. (accessToken.expires_in_ms || accessToken.expires_in * 1000);
  309. if (!isTokenValid(accessToken)) {
  310. console.warn('Ivalid Tidal token received:', accessToken);
  311. return Promise.reject('invalid token received');
  312. }
  313. localStorage.setItem('tidalAccessToken', JSON.stringify(accessToken));
  314. return accessToken;
  315. });
  316. }).call(this).then(accessToken => [{ Authorization: `${accessToken.token_type} ${accessToken.access_token}` }]),
  317. };
  318. const authSequence = [/*'SessionId', 'DeviceToken'*/];
  319. authSequence['tidalAccessToken' in localStorage ? 'unshift' : 'push']('OAuth2');
  320. return (this.auth || (this.auth = (function tidalAuth(index = 0) {
  321. const method = authMethods[authSequence[index]];
  322. if (typeof method == 'function') return method.call(this).catch(reason => {
  323. console.warn('Tidal ' + authSequence[index] + ' auth metod failed:', reason);
  324. return tidalAuth.call(this, index + 1);
  325. });
  326. //this.auth = null;
  327. localStorage.setItem('tidalLoginSuccess', false);
  328. return Promise.reject('all auth methods failed');
  329. }).call(this)));
  330. },
  331. requestAPI: function(endPoint, params, countryCode = 'US') {
  332. if (!endPoint) return Promise.reject('No API endpoint');
  333. const weakRequest = /^(?:search)\//i.test(endPoint);
  334. return (function apiCall() {
  335. return this.authorize().then(credentials => {
  336. if ('tidalLoginSuccess' in localStorage) localStorage.removeItem('tidalLoginSuccess');
  337. setTimeout(() => { this.auth = null }, 5000);
  338. return globalXHR(this.apiBase + '/' + endPoint + '?' + new URLSearchParams(Object.assign({ }, params || { }, {
  339. deviceType: 'BROWSER',
  340. locale: 'en_US',
  341. countryCode: countryCode,
  342. }, credentials[1] || { })).toString(), {
  343. responseType: 'json',
  344. headers: credentials[0],
  345. }).then(({response}) => response, reason => {
  346. if (!/^(?:HTTP error (401))\b/i.test(reason) || !('tidalAccessToken' in localStorage))
  347. return Promise.reject(reason);
  348. localStorage.removeItem('tidalAccessToken');
  349. if (weakRequest) return Promise.reject(reason);
  350. this.auth = null;
  351. return apiCall.call(this);
  352. });
  353. });
  354. }).call(this);
  355. },
  356. };
  357.  
  358. const mixcloudQuery = (query, variables) => ('mixcloudCsrfToken' in sessionStorage ?
  359. Promise.resolve(sessionStorage.getItem('mixcloudCsrfToken')) : globalXHR('https://www.mixcloud.com/', { method: 'HEAD' }).then(function(response) {
  360. let csrfToken = /^set-cookie:.*\b(?:csrftoken)\s*=\s*(\w+)\b/im.exec(response.responseHeaders);
  361. if (csrfToken != null) csrfToken = csrfToken[1]; else return Promise.reject('No CSRF token returned');
  362. sessionStorage.setItem('mixcloudCsrfToken', csrfToken);
  363. return csrfToken;
  364. })).then(csrfToken => globalXHR('https://www.mixcloud.com/graphql', {
  365. responseType: 'json',
  366. headers: { 'X-CSRFToken': csrfToken },
  367. }, { query: query || { }, variables: variables || { } })).then(({response}) => response.data);
  368.  
  369. const getAmazonCfg = (url = 'https://www.amazon.com/') => globalXHR(url = new URL(url), { headers: { 'User-Agent': navigator.userAgent } }).then(function(response) {
  370. let preConnect = response.document.head.querySelector('link[rel="preconnect"]');
  371. //preConnect = 'https://na.web.skill.music.a2z.com/'
  372. if (preConnect != null) preConnect = preConnect.href; else throw 'Assertion failed: preConnect != null';
  373. for (var appConfig of response.document.head.getElementsByTagName('SCRIPT'))
  374. if ((appConfig = /^\s*(?:window\.amznMusic)\s*=\s*(\{[\S\s]+\});\s*$/.exec(appConfig.text)) != null) try {
  375. appConfig = eval('(' + appConfig[1] + ')').appConfig;
  376. break;
  377. } catch (e) { console.warn(e) }
  378. if (!appConfig) throw 'Assertion failed: amznMusic != null';
  379. sessionStorage.setItem('amznAppConfig', JSON.stringify(appConfig));
  380. return {
  381. urlBase: preConnect,
  382. headers: {
  383. //'User-Agent': UA,
  384. 'Referer': url.href,
  385. 'x-amzn-authentication': JSON.stringify({
  386. interface: 'ClientAuthenticationInterface.v1_0.ClientTokenElement',
  387. accessToken: appConfig.accessToken,
  388. }),
  389. 'x-amzn-request-id': 'f4a75e51-e7ef-4080-986a-4041738b1198',
  390. 'x-amzn-session-id': appConfig.sessionId,
  391. 'x-amzn-timestamp': Date.now(),
  392. 'x-amzn-page-url': url.href,
  393. 'x-amzn-csrf': JSON.stringify({
  394. interface: 'CSRFInterface.v1_0.CSRFHeaderElement',
  395. token: appConfig.csrf.token,
  396. timestamp: appConfig.csrf.ts,
  397. rndNonce: appConfig.csrf.rnd,
  398. }),
  399. 'x-amzn-application-version': appConfig.version,
  400. 'x-amzn-currency-of-preference': 'USD' || appConfig.currencyOfPreference,
  401. 'x-amzn-device-family': 'RetailWebPlayer.web',
  402. 'x-amzn-device-model': 'WEBPLAYER',
  403. 'x-amzn-device-type': appConfig.deviceType,
  404. 'x-amzn-device-id': appConfig.deviceId,
  405. 'x-amzn-device-language': 'en_US' || appConfig.displayLanguage,
  406. 'x-amzn-device-time-zone': 'Etc/UTC',
  407. 'x-amzn-os-version': '1.0',
  408. 'x-amzn-device-width': 1920,
  409. 'x-amzn-device-height': 1080,
  410. 'x-amzn-user-agent': navigator.userAgent,
  411. 'x-amzn-affiliate-tags': '',
  412. 'x-amzn-ref-marker': '',
  413. 'x-amzn-music-domain': url.hostname,
  414. 'x-amzn-referer': url.href,
  415. 'x-amzn-page-url': url.href,
  416. 'x-amzn-weblab-id-overrides': '',
  417. 'x-amzn-video-player-token': '',
  418. 'x-amzn-feature-flags': 'hd-supported',
  419. },
  420. };
  421. return Promise.reject('Config could not be extracted');
  422. });
  423.  
  424. function imagePreview(imgUrl, size) {
  425. if (previewDelay <= 0) return;
  426. let div = document.getElementById('image-preview');
  427. if (div != null) document.body.removeChild(div);
  428. if (!httpParser.test(imgUrl)) return;
  429. div = document.createElement('div');
  430. div.id = 'image-preview';
  431. div.style = 'position: fixed; bottom: 20px; right: 20px; border: thin solid silver; ' +
  432. 'background-color: #8888; padding: 10px; opacity: 0; transition: opacity 1s ease-in-out; z-index: 999999999;';
  433. const cleanUp = function(div) {
  434. if (div.parentNode == null) return;
  435. div.style.opacity = 0;
  436. setTimeout(div => { document.body.removeChild(div) }, 1000, div);
  437. };
  438. div.ondblclick = evt => { cleanUp(evt.currentTarget) };
  439. let img = document.createElement('img');
  440. img.style = 'width: 225px;';
  441. img.onload = function(evt) {
  442. if (evt.currentTarget.parentNode.parentNode == null) document.body.append(evt.currentTarget.parentNode);
  443. setTimeout(div => { div.style.opacity = 1 }, 0, evt.currentTarget.parentNode);
  444. setTimeout(cleanUp, (previewDelay || 12) * 1000, evt.currentTarget.parentNode);
  445. if (!evt.currentTarget.naturalWidth || !evt.currentTarget.naturalHeight) return; // invalid image
  446. let info = document.createElement('div');
  447. info.id = 'image-info';
  448. info.style = 'text-align: center; background-color: #29434b; padding: 5px; color: white;' +
  449. 'font: 500 10pt "Segoe UI", Verdana, sans-serif;';
  450. evt.currentTarget.parentNode.append(info);
  451. const resolution = evt.currentTarget.naturalWidth + '×' + evt.currentTarget.naturalHeight;
  452. (size > 0 ? Promise.resolve(size) : size instanceof Promise ? size : getRemoteFileSize(imgUrl)).then(function(size) {
  453. if (!(size >= 0)) throw 'invalid size';
  454. let imageSizeLimit = GM_getValue('image_size_reduce_threshold'),
  455. html = resolution + ' (<span id="image-size"';
  456. if (imageSizeLimit > 0 && size > imageSizeLimit * 2**10) html += ' style="color: red;"';
  457. html += '>' + formattedSize(size) + '</span>)';
  458. info.innerHTML = html;
  459. }).catch(reason => { info.textContent = resolution });
  460. };
  461. img.onerror = evt => { console.warn('Image source couldnot be loaded:', evt, imgUrl) };
  462. img.src = imgUrl;
  463. div.append(img);
  464. }
  465.  
  466. function checkImageSize(image, elem = null, param) {
  467. let imageSizeLimit = GM_getValue('image_size_reduce_threshold');
  468. if (!(imageSizeLimit > 0)) return Promise.resolve(image);
  469. if (!(elem instanceof HTMLElement)) elem = null;
  470. if (elem != null) elem.disabled = true;
  471. return (image instanceof File ? Promise.resolve(image.size) : param > 0 ? Promise.resolve(param)
  472. : param instanceof Promise ? param : getRemoteFileSize(image)).then(function(size) {
  473. if (size <= imageSizeLimit * 2**10) return image;
  474. const haveRhHosts = Array.isArray(imageHosts.rhHostChain) && imageHosts.rhHostChain.length > 0;
  475. if (!haveRhHosts && !GM_getValue('force_reduce', true)) return Promise.reject('no hosts to upload result');
  476. return reduceImageSize(image, GM_getValue('image_reduce_maxheight', 2160),
  477. GM_getValue('image_reduce_jpegquality', 90), typeof param == 'function' ? param : null).then(function(output) {
  478. if (elem != null) {
  479. elem.value = output.uri;
  480. if (image instanceof File) imagePreview(output.uri, output.size);
  481. }
  482. Promise.resolve(output.size).then(reducedSize => {
  483. console.log('cover size reduced by ' + Math.round((size - reducedSize) * 100 / size) +
  484. '% (' + Math.ceil(size / 2**10) + ' → ' + Math.ceil(reducedSize / 2**10) + ' KiB)');
  485. });
  486. return haveRhHosts ? output.uri : forcedRehost(output.uri);
  487. });
  488. }).catch(function(reason) {
  489. logFail('failed to get remote image size or optimize the image: ' + reason + ' (size reduction was not performed)');
  490. return image;
  491. }).then(function(finalResult) {
  492. if (elem != null) {
  493. if (httpParser.test(finalResult)) {
  494. if (finalResult != elem.value) elem.value = finalResult;
  495. } else elem.value = '';
  496. elem.disabled = false;
  497. }
  498. return finalResult;
  499. });
  500. }
  501.  
  502. // Export public API
  503. if (typeof unsafeWindow == 'object') {
  504. Object.defineProperty(unsafeWindow, 'imageHostHelper', { value: Object.freeze({
  505. uploadFiles: function uploadImages(files, checkSize = true, preview = false) {
  506. if (files instanceof Blob) files = [files];
  507. if (!Array.isArray(files)) return Promise.reject('Invalid parameter (files)');
  508. if ((files = files.filter(file => file instanceof File && file.type.startsWith('image/'))).length <= 0)
  509. return Promise.reject('Invalid parameter (no valid images passed)');
  510. console.time('Image uploader');
  511. return checkSize || preview ? Promise.all(files.map(file => (checkSize ? checkImageSize(file).catch(function(reason) {
  512. logFail('Downsizing of source image not possible (' + reason + '), uploading original size');
  513. return file;
  514. }) : Promise.resolve(file)).then(function(result) {
  515. const uploader = file => imageHosts.uploadImages([file]).then(singleImageGetter).then(function(imageUrl) {
  516. if (preview) imagePreview(imageUrl, file.size);
  517. return imageUrl;
  518. });
  519. if (httpParser.test(result)) return imageHosts.rehostImages([result]).catch(function(reason) {
  520. logFail('Downsizing of source image failed (' + reason + '), uploading original size');
  521. return uploader(file);
  522. });
  523. if (result instanceof File) return uploader(result);
  524. console.warn('invalid checkImageSize(...) result:', result);
  525. return Promise.reject('invalid checkImageSize(...) result');
  526. }))) : imageHosts.uploadImages(files);
  527. },
  528. rehostImageLinks: function rehostImageLinks(urls, checkSize = true, preview = false, enforceRehost = false, modifiers) {
  529. if (typeof urls == 'string' && httpParser.test(urls)) urls = [urls];
  530. if (!Array.isArray(urls) || urls.length <= 0) return Promise.reject('Invalid parameter (urls)');
  531. console.time('Image URL rehoster');
  532. return Promise.all(urls.map(url => imageUrlResolver(url, {
  533. altKey: Boolean(typeof modifiers == 'object' && modifiers.altKey),
  534. ctrlKey: Boolean(typeof modifiers == 'object' && modifiers.ctrlKey),
  535. shiftKey: Boolean(typeof modifiers == 'object' && modifiers.shiftKey),
  536. }).then(verifyImageUrl).then(function(imageUrl) {
  537. if (!checkSize) return imageUrl;
  538. const size = getRemoteFileSize(imageUrl);
  539. if (preview) imagePreview(imageUrl, size);
  540. return checkImageSize(imageUrl, null, size);
  541. }))).then(imageUrls => imageHosts.rehostImages(imageUrls).then(function(rehostedImages) {
  542. console.timeEnd('Image URL rehoster');
  543. return rehostedImages;
  544. }, reason => enforceRehost ? Promise.reject(reason) : Promise.resolve(imageUrls)));
  545. },
  546. imageHostHandlers: imageHostHandlers,
  547. uploadImages: ImageHostManager.prototype.uploadImages.bind(imageHosts),
  548. rehostImages: ImageHostManager.prototype.rehostImages.bind(imageHosts),
  549. logFail: logFail,
  550. getDeezerImageMax: getDeezerImageMax,
  551. getDiscogsImageMax: getDiscogsImageMax,
  552. dzrImageMax: dzrImageMax,
  553. itunesImageMax: itunesImageMax,
  554. urlResolver: urlResolver,
  555. verifyImageUrl: verifyImageUrl,
  556. getRemoteFileType: getRemoteFileType,
  557. getRemoteFileSize: getRemoteFileSize,
  558. imageUrlResolver: imageUrlResolver,
  559. checkImageSize: checkImageSize,
  560. reduceImageSize: reduceImageSize,
  561. optiPNG: optiPNG,
  562. directLinkGetter: directLinkGetter,
  563. singleImageGetter: singleImageGetter,
  564. }) });
  565. unsafeWindow.dispatchEvent(Object.assign(new Event('imageHostHelper'), { data: unsafeWindow.imageHostHelper }));
  566. // const meta = document.createElement('META');
  567. // meta.name = 'ImageHostHelper';
  568. // meta.content = 'All endpoints exported';
  569. // meta.setAttribute('propertyname', 'imageHostHelper');
  570. // document.head.append(meta);
  571. }
  572.  
  573. function imageUrlResolver(url, modifiers = { }) {
  574. return urlResolver(url).then(url => verifyImageUrl(url).catch(function(reason) {
  575. if (/^HTTP error (\d+)\b/.test(reason) && [
  576. 401, 402, 404, 407, 408, 410, 451,
  577. 502, 503, 504, 511,
  578. ].includes(parseInt(RegExp.$1)) || /\b(?:timeout|timed out)\b/.test(reason)) return Promise.reject(reason);
  579. const notFound = Promise.reject('No title image for this URL');
  580. function getFromMeta(root) {
  581. let meta = root instanceof Document || root instanceof Element ? [
  582. 'meta[property="og:image:secure_url"][content]',
  583. 'meta[property="og:image"][content]',
  584. 'meta[name="og:image"][content]',
  585. 'meta[itemprop="og:image"][content]',
  586. 'meta[itemprop="image"][content]',
  587. ].reduce((elem, selector) => elem || root.querySelector(selector), null) : null;
  588. return meta != null && httpParser.test(meta.content) ? meta.content : undefined;
  589. }
  590.  
  591. try { url = new URL(url) } catch(e) { return Promise.reject(e) }
  592. let entryIds;
  593. if (url.hostname.endsWith('pinterest.com'))
  594. return pinterestResolver(url);
  595. else if (url.hostname.endsWith('free-picload.com')) {
  596. if (url.pathname.startsWith('/album/')) return imageHostHandlers.picload.galleryResolver(url);
  597. } else if (url.hostname.endsWith('bandcamp.com')) return globalXHR(url).then(function({document}) {
  598. let ref = document.querySelector('div#tralbumArt > a.popupImage');
  599. ref = ref != null ? ref.href : getFromMeta(document);
  600. return ref ? Promise.resolve(ref.replace(/_\d+(?=\.\w+$)/, '_0')) : notFound;
  601. }); else if (url.hostname.endsWith('7digital.com') && url.pathname.startsWith('/artist/'))
  602. return globalXHR(url).then(function({document}) {
  603. let img = document.querySelector('img[itemprop="image"]');
  604. return img != null ? img.src : notFound;
  605. });
  606. else if (url.hostname.endsWith('geekpic.net')) return globalXHR(url).then(function({document}) {
  607. let a = document.querySelector('div.img-upload > a.mb');
  608. return a != null ? a.href : notFound;
  609. }); else if (url.hostname.endsWith('qq.com') && /\/album(?:Detail)?\/(\w+)/i.test(url.pathname)) return globalXHR(url).then(function({document}) {
  610. for (let script of document.body.querySelectorAll(':scope > script'))
  611. if ((script = /\b__INITIAL_DATA__\s*=\s*({.+})/.exec(script.text)) != null)
  612. try { var initialData = eval('(' + script[1] + ')') } catch(e) { console.warn(e) }
  613. if (!initialData) throw 'Assertion failed: __INITIAL_DATA__ not triggered';
  614. if (initialData = initialData.detail.picurl) {
  615. if (!httpParser.test(initialData)) initialData = url.protocol + initialData;
  616. return initialData.replace(/\/(T\d+)?(R\d+x\d+)?(M\w+?)(_\d+)?\.(\w+(?:\.\w+)*)(\?.*)?$/, '/$1$3.$5');
  617. } else return notFound;
  618. }); else if (url.hostname.startsWith('books.google.') && url.pathname.startsWith('/books')) return globalXHR(url).then(function({document}) {
  619. let meta = getFromMeta(document);
  620. return meta != null ? meta.replace(/\b(?:zoom=1)\b/, 'zoom=0') : notFound;
  621. }); else if (/^(?:\w+\.)?amazon(?:\.\w+)+$/.test(url.hostname)) return getAmazonCfg(url).then(function(amazonCfg) {
  622. return globalXHR(amazonCfg.urlBase + 'api/showHome', { responseType: 'json', headers: amazonCfg.headers }, {
  623. deeplink: JSON.stringify({
  624. interface: 'DeeplinkInterface.v1_0.DeeplinkClientInformation',
  625. deeplink: '/' + url.pathname.split('/').filter(Boolean).slice(-2).join('/'),
  626. }),
  627. }).then(function({response}) {
  628. const method = response.methods.find(method => method.interface.endsWith('.CreateAndBindTemplateMethod'));
  629. return method && method.template && method.template.headerImage || notFound;
  630. });
  631. }).catch(reason => globalXHR(url).then(function(response) {
  632. const getFullImage = imageUrl => httpParser.test(imageUrl)
  633. && (imageUrl = imageUrl.replace(/\._\w+(?:_\w+)*_\./, '.'), !['31CTP6oiIBL.jpg', '31zMd62JpyL.jpg']
  634. .some(path => imageUrl.endsWith('/images/I/' + path))) ? imageUrl : Promise.reject('Dummy image (placeholder)');
  635. const getImgOrigin = colorImage => getFullImage(colorImage.hiRes || colorImage.large || colorImage.thumb);
  636. let obj = /^\s*(?:var\s+obj\s*=\s*jQuery\.parseJSON)\('(\{.+\})'\);/m.exec(response.responseText);
  637. if (obj != null) {
  638. try { obj = JSON.parse(obj[1]) } catch(e) { try { obj = eval('(' + obj[1] + ')') } catch(e) { obj = { } } }
  639. let variants = Object.keys(obj.colorImages);
  640. if (variants.length > 0) return Promise.all(variants.map(key =>
  641. Promise.all(obj.colorImages[key].map(getImgOrigin))));
  642. }
  643. let colorImages = /^\s*'colorImages':\s*(\{.+\}),?$/m.exec(response.responseText);
  644. if (colorImages != null) {
  645. try { colorImages = JSON.parse(colorImages[1].replace(/'/g, '"')) }
  646. catch(e) { try { colorImages = eval('(' + colorImages[1] + ')') } catch(e) { colorImages = { } } }
  647. if (Array.isArray(colorImages.initial) && colorImages.initial.length > 0)
  648. return Promise.all(colorImages.initial.map(getImgOrigin));
  649. }
  650. let img = ['div#ppd-left img', 'img#igImage', 'img#imgBlkFront']
  651. .reduce((acc, sel) => acc || response.document.querySelector(sel), null);
  652. if (img == null) return notFound;
  653. if (img.dataset.aDynamicImage) try {
  654. let imgUrl = Object.keys(JSON.parse(img.dataset.aDynamicImage))[0];
  655. if (httpParser.test(imgUrl)) return getFullImage(imgUrl);
  656. } catch(e) { }
  657. return getFullImage(img.src);
  658. })); else switch (url.hostname) {
  659. // general image hostings
  660. case 'www.imgur.com': case 'imgur.com':
  661. return (entryIds = /^\/(?:(a)\/)?(\w+)\b/.exec(url.pathname)) != null ? imageHostHandlers.imgur.setSession().then(clientId =>
  662. globalXHR(`https://api.imgur.com/post/v1/${entryIds[1] == 'a' ? 'albums' : 'media'}/${entryIds[2]}?${new URLSearchParams({
  663. client_id: clientId,
  664. include: 'media',
  665. }).toString()}`, { responseType: 'json' }).then(({response}) => response.media.map(media => media.url))).catch(reason => globalXHR(url, { responseType: 'text' }).then(function({responseText}) {
  666. let image = /^\s*(?:image)\s*:\s*(\{.+\}),\s*$/m.exec(responseText);
  667. if (image != null) try {
  668. return JSON.parse(image[1]).album_images.images.map(image => 'https://i.imgur.com/' + image.hash + image.ext);
  669. } catch(e) { console.warn(e) }
  670. return notFound;
  671. })) : globalXHR(url).then(function({document}) {
  672. let link = document.querySelector('link[rel="image_src"]');
  673. return link != null ? link.href : notFound;
  674. });
  675. case 'pixhost.to':
  676. if (url.pathname.startsWith('/gallery/')) return globalXHR(url).then(({document}) =>
  677. Promise.all(Array.from(document.querySelectorAll('div.images > a')).map(a => imageUrlResolver(a.href, modifiers))));
  678. if (url.pathname.startsWith('/show/')) return globalXHR(url)
  679. .then(({document}) => document.querySelector('img#image').src);
  680. break;
  681. case 'malzo.com':
  682. if (url.pathname.startsWith('/al/')) return imageHostHandlers.malzo.galleryResolver(url); else break;
  683. case 'imgbb.com': case 'ibb.co':
  684. if (url.pathname.startsWith('/album/')) return imageHostHandlers.imgbb.galleryResolver(url); else break;
  685. case 'jerking.empornium.ph':
  686. if (url.pathname.startsWith('/album/')) return imageHostHandlers.jerking.galleryResolver(url); else break;
  687. case 'imgbox.com':
  688. if (url.pathname.startsWith('/g/')) return globalXHR(url).then(({document}) =>
  689. Promise.all(Array.from(document.querySelectorAll('div#gallery-view-content > a'))
  690. .map(a => imageUrlResolver('https://imgbox.com' + a.pathname, modifiers))));
  691. break;
  692. case 'postimage.org': case 'postimg.cc':
  693. if (url.pathname.startsWith('/gallery/'))
  694. return PostImage.resultsHandler(url).then(results => results.map(result => result.original));
  695. return globalXHR(url).then(function({document}) {
  696. const elem = document.body.querySelector('a#download');
  697. return elem != null ? elem.href : getFromMeta(document.head) || notFound;
  698. });
  699. case 'www.imagevenue.com': case 'imagevenue.com':
  700. return globalXHR(url, { headers: { Referer: 'http://www.imagevenue.com/' } }).then(function({document}) {
  701. let images = Array.from(document.querySelectorAll('div.card img')).map(function(img) {
  702. return img.src.includes('://cdn-images') ? Promise.resolve(img.src) : imageUrlResolver(img.parentNode.href, modifiers);
  703. });
  704. return images.length > 1 ? Promise.all(images) : images.length == 1 ? images[0] : notFound;
  705. });
  706. case 'www.imageshack.us': case 'imageshack.us':
  707. return globalXHR(url).then(({document}) => document.querySelector('a#share-dl').href);
  708. case 'www.flickr.com': case 'flickr.com':
  709. if (url.pathname.startsWith('/photos/')) return globalXHR(url, { responseType: 'text' }).then(function({responseText}) {
  710. if (/\b(?:modelExport)\s*:\s*(\{.+\}),/.test(responseText)) try {
  711. let urls = JSON.parse(RegExp.$1).main['photo-models'].map(function(photoModel) {
  712. let sizes = Object.keys(photoModel.sizes).sort((a, b) => photoModel.sizes[b].width *
  713. photoModel.sizes[b].height - photoModel.sizes[a].width * photoModel.sizes[a].height);
  714. return sizes.length > 0 ? 'https:'.concat(photoModel.sizes[sizes[0]].url) : null;
  715. });
  716. if (urls.length == 1) return urls[0]; else if (urls.length > 1) return urls;
  717. } catch(e) { console.warn(e) }
  718. return notFound;
  719. }); else break;
  720. case 'photos.google.com':
  721. return googlePhotosResolver(url);
  722. case 'www.500px.com': case 'web.500px.com': case '500px.com':
  723. if (/^\/photo\/(\d+)\b/i.test(url.pathname))
  724. return _500pxUrlHandler('photos?ids='.concat(RegExp.$1));
  725. else if (/\/galleries\/([\w\%\-]+)/i.test(url.pathname)) {
  726. let galleryId = RegExp.$1;
  727. return globalXHR(url, { rsponseType: 'text' }).then(function({responseText}) {
  728. if (!/\b(?:App\.CuratorId)\s*=\s*"(\d+)"/.test(responseText)) return Promise.reject('Unexpected page structure');
  729. return _500pxUrlHandler('users/' + RegExp.$1 + '/galleries/' + galleryId + '/items?sort=position&sort_direction=asc&rpp=999');
  730. });
  731. } else break;
  732. case 'www.pxhere.com': case 'pxhere.com':
  733. if (url.pathname.includes('/photo/')) return globalXHR(url).then(({document}) =>
  734. JSON.parse(document.querySelector('div.hub-media-content > script[type="application/ld+json"]').text).contentUrl);
  735. else if (url.pathname.includes('/collection/')) return pxhereCollectionResolver(url);
  736. break;
  737. case 'www.unsplash.com': case 'unsplash.com':
  738. if (url.pathname.startsWith('/photos/')) return globalXHR(url.origin + url.pathname + '/download', { method: 'HEAD' })
  739. .then(response => response.finalUrl.replace(/\?.*$/, ''));
  740. else if (url.pathname.includes('/collections/')) return unsplashCollectionResolver(url);
  741. break;
  742. case 'www.pexels.com': case 'pexels.com':
  743. if (url.pathname.startsWith('/photo/')) return globalXHR(url)
  744. .then(({document}) => document.querySelector('meta[property="og:image"][content]').content.replace(/\?.*$/, ''));
  745. else if (url.pathname.startsWith('/collections/')) return pexelsCollectionResolver(url);
  746. break;
  747. case 'www.piwigo.org': case 'piwigo.org':
  748. /*if (url.pathname.includes('/picture/')) */return globalXHR(url, { responseType: 'text' }).then(function({responseText}) {
  749. if (/^(?:RVAS)\s*=\s*(\{[\S\s]+?\})$/m.test(responseText)) try {
  750. let derivatives = eval('(' + RegExp.$1 + ')').derivatives.sort((a, b) => b.w * b.h - a.w * a.h);
  751. return derivatives.length > 0 ? 'https://piwigo.org/demo/'.concat(derivatives[0].url) : notFound;
  752. } catch(e) { console.warn(e) }
  753. return Promise.reject('Unexpected page structure');
  754. });
  755. case 'www.freeimages.com': case 'freeimages.com':
  756. if (url.pathname.startsWith('/photo/')) return globalXHR(url).then(function({document}) {
  757. let types = Array.from(document.querySelectorAll('ul.download-type > li > span.reso'))
  758. .sort((a, b) => eval(b.textContent.replace('x', '*')) - eval(a.textContent.replace('x', '*')));
  759. return types.length > 0 ? url.origin.concat(types[0].parentNode.querySelector('a').pathname) : notFound;
  760. }); else break;
  761. case 'redacted.sh':
  762. if (url.pathname == '/image.php') return globalXHR(url, { method: 'HEAD' }).then(response => response.finalUrl);
  763. else break;
  764. case 'demo.cloudimg.io': {
  765. if (!/\b(https?:\/\/\S+)$/.test(url.pathname.concat(url.search, url.hash))) break;
  766. let resolved = RegExp.$1;
  767. if (/\b(?:https?):\/\/(?:\w+\.)*discogs\.com\//i.test(resolved)) break;
  768. return imageUrlResolver(resolved, modifiers);
  769. }
  770. case 'www.pimpandhost.com': case 'pimpandhost.com':
  771. if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function(response) {
  772. let elem = resopnse.document.querySelector('div.main-image-wrapper');
  773. if (elem != null && elem.dataset.src) return 'https:'.concat(elem.dataset.src);
  774. elem = resopnse.document.querySelector('div.img-wrapper > a > img');
  775. return elem != null ? 'https:'.concat(elem.src) : notFound;
  776. }); else break;
  777. case 'www.screencast.com': case 'screencast.com':
  778. return globalXHR(url).then(function({document}) {
  779. let ref = document.querySelectorAll('ul#containerContent > li a.media-link');
  780. if (ref.length <= 0) return getFromMeta(document) || notFound;
  781. return Promise.all(Array.from(ref).map(a => imageUrlResolver('https://www.screencast.com' + a.href, modifiers)));
  782. });
  783. case 'abload.de':
  784. if (url.pathname.startsWith('/image.php')) return globalXHR(url).then(function({document}) {
  785. let elem = document.querySelector('img#image');
  786. if (elem == null) return notFound;
  787. let src = new URL(elem.src);
  788. return imageHostHandlers.abload.origin + src.pathname + src.search;
  789. }); else break;
  790. case 'fastpic.ru':
  791. if (url.pathname.startsWith('/view/'))
  792. return globalXHR(url).then(({document}) => imageUrlResolver(document.querySelector('a.img-a').href, modifiers));
  793. else if (url.pathname.startsWith('/fullview/')) return globalXHR(url).then(function(response) {
  794. let node = response.document.getElementById('image');
  795. if (node != null) return node.src;
  796. return /\bvar\s+loading_img\s*=\s*'(\S+?)';/.test(response.responseText) ? RegExp.$1 : notFound;
  797. }); else break;
  798. case 'www.radikal.ru': case 'radikal.ru': case 'a.radikal.ru':
  799. return globalXHR(url).then(({document}) => document.querySelector('div.mainBlock img').src);
  800. case 'imageban.ru': case 'ibn.im':
  801. return globalXHR(url).then(({document}) => document.querySelector('a[download]').href);
  802. case 'svgshare.com':
  803. return globalXHR(url).then(function({document}) {
  804. let link;
  805. document.querySelectorAll('ul#shares > li > input[type="text"]')
  806. .forEach(input => { if (!link && /^(?:https?:\/\/.+\.svg)$/.test(input.value)) link = input.value; });
  807. return link || notFound;
  808. });
  809. case 'slow.pics':
  810. if (url.pathname.startsWith('/c/')) return globalXHR(url).then(function({document}) {
  811. let nodes = document.querySelectorAll('img.card-img-top');
  812. if (nodes.length > 1) return Array.from(nodes).map(img => img.src);
  813. else if (nodes.length > 0) return nodes[0].src;
  814. nodes = document.querySelectorAll('a#comparisons + div.dropdown-menu > a.dropdown-item');
  815. if (nodes.length > 0) return Promise.all(Array.from(nodes).map(a => globalXHR(url.origin + a.pathname)
  816. .then(({document}) => Array.from(document.querySelectorAll('div#preload-images > img')).map(img => img.src))));
  817. return notFound;
  818. }); else break;
  819. case 'www.casimages.com': case 'casimages.com':
  820. if (url.pathname.startsWith('/i/')) return globalXHR(url).then(function({document}) {
  821. let elem = document.querySelector('div.logo > a');
  822. if (elem != null) return elem.href;
  823. elem = document.querySelector('div.logo img');
  824. return elem != null ? elem.src : notFound;
  825. }); else break;
  826. case 'www.getapic.me': case 'getapic.me':
  827. return globalXHR(url, { responseType: 'json' }).then(function({response}) {
  828. if (!response.result.success) return Promise.reject(response.result.errors);
  829. if (Array.isArray(response.result.data.images))
  830. return response.result.data.images.map(image => image.url);
  831. return response.result.data.image ? response.result.data.image.url : notFound;
  832. });
  833. case 'sm.ms':
  834. if (url.pathname.startsWith('/image/')) return globalXHR(url).then(function({document}) {
  835. let img = document.querySelector('img.image');
  836. return img != null ? img.src || img.parentElement.href : notFound;
  837. }); else break;
  838. case 'www.kizunaai.com': case 'kizunaai.com':
  839. //if (!url.pathname.includes('/music/')) break;
  840. return globalXHR(url).then(function({document}) {
  841. let img = document.querySelector('div.post-body span > img');
  842. return img != null ? img.src.replace(/-\d+x\d+(?=\.\w+$)/, '') : notFound;
  843. });
  844. case 'play.google.com':
  845. if (url.pathname.startsWith('/store/')) return globalXHR(url).then(function({document}) {
  846. let meta = getFromMeta(document);
  847. return meta != null ? meta.replace(/(?:=[swh]\d+.*)?$/, '=s0') : notFound;
  848. }); else break;
  849. // music-related
  850. case 'www.discogs.com': case 'discogs.com':
  851. return globalXHR(url, { anonymous: true }).then(({document}) => (function() {
  852. if (url.pathname.includes('/master/')) return Promise.reject('This is master');
  853. if (modifiers.ctrlKey) return Promise.reject('Master release inquiry avoided (force release gallery)');
  854. const master = document.body.querySelector('section#release-actions a.link_1ctor[href^="/master/"]')
  855. || document.body.querySelector('a[href^="/master/"]');
  856. if (master == null) return Promise.reject('No master release for this page');
  857. return imageUrlResolver(discogsOrigin + master.pathname, modifiers);
  858. })().catch(function(reason) {
  859. const ids = /\/(artist|master|release|label)s?\/(?:view\/)?(\d+)\b/i.exec(url.pathname);
  860. return ids != null ? getDiscogsImages(ids[1], ids[2]).catch(function(reason) {
  861. let gallery = document.querySelector('div.image_gallery_large, div.image_gallery');
  862. if (gallery != null) try {
  863. gallery = JSON.parse(gallery.dataset.images).map(image => image.full || image.thumb)
  864. .filter(RegExp.prototype.test.bind(httpParser));
  865. if (gallery.length <= 0) throw 'empty imagem list';
  866. return Promise.all(gallery.map(getDiscogsImageMax)).catch(function(reason) {
  867. console.error('One of getDiscogsImageMax workers rejected:', reason, gallery);
  868. return gallery;
  869. });
  870. } catch(e) { console.warn('Invalid Discogs image gallery:', gallery, '(' + e + ')') } else {
  871. console.warn('Missing Discogs image gallery record for', url.href);
  872. }
  873. return (gallery = getFromMeta(document)) ? getDiscogsImageMax(gallery) : notFound;
  874. }) : Promise.reject('Unsupported entity');
  875. }));
  876. case 'www.musicbrainz.org': case 'beta.musicbrainz.org': case 'musicbrainz.org':
  877. if (url.pathname.startsWith('/release/')) {
  878. if (/^\/release\/([\w\-]+)(?=\/|$)/i.test(url.pathname)) url.pathname = '/release/' + RegExp.$1 + '/cover-art';
  879. else console.warn('Unexpected MusicBrainz release url path:', url.pathname);
  880. } else if (!url.pathname.startsWith('/release-group/')) break;
  881. return globalXHR(url).then(({document}) => (function() {
  882. if (url.pathname.startsWith('/release-group/')) return Promise.reject('this is release group');
  883. if (modifiers.ctrlKey) return Promise.reject('release group inquiry avoided (force release gallery)');
  884. let releaseGroup = document.querySelector('p.subheader > span.small > a');
  885. if (releaseGroup == null) return Promise.reject('no release group for this page');
  886. return imageUrlResolver('https://musicbrainz.org' + releaseGroup.pathname, modifiers);
  887. })().catch(function(reason) {
  888. let elem = document.querySelector('head > script[type="application/ld+json"]');
  889. if (elem != null) try {
  890. if (Array.isArray(elem = JSON.parse(elem.text).image)) {
  891. if (elem.length > 0) return elem.map(image => 'https:' + image.contentUrl);
  892. } else if (elem && elem.contentUrl) return 'https:' + elem.contentUrl;
  893. } catch(e) { console.warn('MusicBrainz: invalid meta record', elem) }
  894. elem = document.querySelectorAll('div#content > div.artwork-cont span.cover-art-image > img');
  895. if (elem.length > 0) return Array.from(elem).map(img => img.src.replace(/-\d+(?=(?:\.\w+)+$)/, ''));
  896. return (elem = document.querySelector('a.artwork-image')) != null ? elem.href
  897. : (elem = document.querySelector('div.cover-art > img')) != null ? elem.src : notFound;
  898. }));
  899. case 'www.allmusic.com': case 'allmusic.com':
  900. return globalXHR(url).then(function({document}) {
  901. function imageResolver(document) {
  902. function imageMax(imageUrl) {
  903. if (imageUrl) try {
  904. imageUrl = new URL(imageUrl);
  905. imageUrl.searchParams.set('f', 0);
  906. return imageUrl.href;
  907. } catch(e) { console.warn(e) }
  908. }
  909.  
  910. const galleryExtractor = /\b(?:imageGallery) *= *(\[.+\]);?\s*$/;
  911. let imageGallery = Array.prototype.find.call(document.body.getElementsByTagName('script'),
  912. script => galleryExtractor.test(script.text));
  913. if (imageGallery) try {
  914. imageGallery = galleryExtractor.exec(imageGallery.text);
  915. console.assert(imageGallery != null);
  916. imageGallery = eval(imageGallery[1]).map(image => imageMax(image.url));
  917. if (imageGallery.length > 0) return imageGallery;
  918. } catch(e) { console.warn(e) }
  919. return imageMax(getFromMeta(document)) || notFound;
  920. }
  921.  
  922. const mainAlbum = document.querySelector('div#mainAlbumMeta a');
  923. if (mainAlbum == null || !modifiers.ctrlKey) return imageResolver(document);
  924. return globalXHR(mainAlbum).then(({document}) =>
  925. imageResolver(document)).catch(reason => imageResolver(document));
  926. });
  927. case 'music.apple.com': case 'itunes.apple.com': {
  928. if ((entryIds = amEntityParser.exec(url)) == null) break;
  929. const market = /\/([a-z]{2})\//.exec(url.pathname);
  930. return queryAppleAPI(`${entryIds[1]}s/${entryIds[2]}`, undefined, market != null ? market[1] : undefined).then(function(response) {
  931. const artwork = response.data[0].attributes.artwork;
  932. return artwork ? artwork.url.replace('{w}', artwork.width).replace('{h}', artwork.height) : notFound;
  933. });
  934. }
  935. case 'www.deezer.com': case 'deezer.com':
  936. if ((entryIds = dzrEntityParser.exec(url)) != null) return verifyImageUrl(`https://api.deezer.com/${entryIds[1]}/${entryIds[2]}/image`).catch(function(reason) {
  937. console.warn('Deezer API image retrieval failed:', reason, url);
  938. return globalXHR(url).then(({document}) => getFromMeta(document) || notFound);
  939. }).then(imageUrl => !modifiers.ctrlKey ? getDeezerImageMax(imageUrl)
  940. : verifyImageUrl(imageUrl.replace(...dzrImageMax)).catch(reason => imageUrl)); else break;
  941. case 'www.qobuz.com': case 'qobuz.com':
  942. if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
  943. let img = document.querySelector('div.album-cover > img');
  944. if (img == null) return getFromMeta(document) || notFound;
  945. return verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_org'))
  946. .catch(reason => verifyImageUrl(img.src.replace(/_\d{3}(?=\.\w+$)/, '_max')))
  947. .catch(reason => img.src);
  948. }); else if (url.pathname.includes('/interpreter/') || url.pathname.includes('/artist/')) return globalXHR(url).then(function({document}) {
  949. let img = document.querySelector('div.catalog-heading__picture')
  950. || document.querySelector('div.catalog-heading__background');
  951. if (img != null) img = /\b(?:url)\(\"(.+)\"\)/i.exec(img.style.backgroundImage);
  952. if (img != null) img = img[1]; else return getFromMeta(document) || notFound;
  953. if (!httpParser.test(img)) img = 'https:' + img;
  954. return verifyImageUrl(img.replace(/\/small\//i, '/large/')).catch(reason => img);
  955. }); else break;
  956. case 'www.boomkat.com': case 'boomkat.com':
  957. if (url.pathname.startsWith('/products/')) return globalXHR(url).then(function({document}) {
  958. let img = document.querySelector('img[itemprop="image"]');
  959. if (img == null) return notFound;
  960. return verifyImageUrl(img.src.replace(/\/large\//i, '/original/')).catch(reason => img.src);
  961. }); else break;
  962. case 'www.bleep.com': case 'bleep.com':
  963. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
  964. let image = getFromMeta(document);
  965. if (!image && (image = document.body.querySelector('a.main-product-image > img')) != null) image = image.src;
  966. return image ? verifyImageUrl(image.replace(/\/r\/[a-z]\//i, '/r/')).catch(reason => image) : notFound;
  967. }); else break;
  968. case 'www.soundcloud.com': case 'soundcloud.com':
  969. return globalXHR(url).then(function({document}) {
  970. const meta = getFromMeta(document);
  971. return meta ? verifyImageUrl(meta.replace(/-\w+(?=\.\w+$)/, '-original')).catch(reason => meta) : notFound;
  972. });
  973. case 'www.prestomusic.com': case 'prestomusic.com':
  974. if (url.pathname.includes('/products/')) return globalXHR(url).then(({document}) =>
  975. verifyImageUrl(document.querySelector('div.c-product-block__aside > a').href.replace(/\?\d+$/))); else break;
  976. case 'www.bontonland.cz':case 'bontonland.cz':
  977. return globalXHR(url).then(({document}) => document.querySelector('a.detailzoom').href);
  978. case 'www.prostudiomasters.com': case 'prostudiomasters.com':
  979. if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
  980. let a = document.querySelector('img.album-art');
  981. return verifyImageUrl(a.currentSrc).catch(reason => a.src);
  982. }); else break;
  983. case 'www.e-onkyo.com': case 'e-onkyo.com':
  984. if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
  985. let meta = getFromMeta(document);
  986. return meta ? meta.replace(/\/s\d+\//, '/s0/') : notFound;
  987. }); else break;
  988. case 'store.acousticsounds.com':
  989. return globalXHR(url).then(function({document}) {
  990. let link = document.querySelector('div#detail > link[rel="image_src"]');
  991. return verifyImageUrl(link.href.replace(/\/medium\//i, '/xlarge/')).catch(reason => link.href);
  992. });
  993. case 'www.indies.eu': case 'indies.eu':
  994. if (url.pathname.includes('/alba/')) return globalXHR(url)
  995. .then(({document}) => verifyImageUrl(document.querySelector('div.obrazekDetail > img').src)); else break;
  996. case 'www.beatport.com': case 'classic.beatport.com': case 'pro.beatport.com': case 'beatport.com':
  997. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
  998. let elem = getFromMeta(document);
  999. if (!elem && (elem = document.body.querySelector('div > img.interior-release-chart-artwork')) != null)
  1000. elem = elem.src;
  1001. if (!elem && (elem = document.body.querySelector('div.artwork')) != null && elem.dataset.modalArtwork) // BP Classic
  1002. elem = 'https:' + elem.dataset.modalArtwork;
  1003. return elem || notFound;
  1004. }).then(imgUrl => verifyImageUrl(imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/image/'))); else break;
  1005. case 'www.beatsource.com': case 'beatsource.com':
  1006. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
  1007. let imgUrl = getFromMeta(document);
  1008. return imgUrl ? imgUrl.replace(/\/image_size\/\d+x\d+\//i, '/') : notFound;
  1009. }); else break;
  1010. case 'www.supraphonline.cz': case 'supraphonline.cz':
  1011. if (!url.pathname.includes('/album/')) break;
  1012. return globalXHR(url).then(function({document}) {
  1013. let imageUrl = document.querySelector('div.sidebar div.sexycover > div.btn-group > button:last-of-type');
  1014. if (imageUrl != null && /^(?:coverzoom):(\S+)\$$/.test(imageUrl.dataset.plugin)
  1015. && (imageUrl = imageUrl.parentNode.querySelector('script[type="data-plugin/' + RegExp.$1 + '"]')) != null)
  1016. return 'https://www.supraphonline.cz' + eval(imageUrl.text);
  1017. return (imageUrl = getFromMeta(document)) ? imageUrl.replace(/\?.*$/, '') : notFound;
  1018. });
  1019. case 'vgmdb.net':
  1020. if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
  1021. let div = document.querySelector('div#coverart');
  1022. return verifyImageUrl(/\b(?:url)\s*\(\"(.*)"\)/i.test(div.style['background-image']) && RegExp.$1).catch(reason => notFound);
  1023. }); else break;
  1024. case 'www.ototoy.jp': case 'ototoy.jp':
  1025. return globalXHR(url).then(function({document}) {
  1026. let img = document.querySelector('div#jacket-full-wrapper > img'); // img[alt="album jacket"]
  1027. return img != null ? img.dataset.src || img.src : notFound;
  1028. });
  1029. case 'music.yandex.ru':
  1030. if (url.pathname.includes('/album/')) return globalXHR(url).then(function({document}) {
  1031. let script = document.querySelector('script.light-data');
  1032. return verifyImageUrl(JSON.parse(script.text).image).catch(reason => notFound);
  1033. }); else break;
  1034. case 'www.pias.com': case 'store.pias.com': case 'pias.com':
  1035. return globalXHR(url).then(function({document}) {
  1036. let node = getFromMeta(document);
  1037. if (node) return verifyImage(node.replace(/\/[sbl]\//i, '/')).catch(reason => node);
  1038. node = document.querySelector('img[itemprop="image"]');
  1039. return node != null ? verifyImage(node.src.replace(/\/[sbl]\//i, '/')).catch(reason => node.src) : notFound;
  1040. });
  1041. case 'www.eclassical.com': case 'eclassical.com':
  1042. return globalXHR(url).then(function({document}) {
  1043. let a = document.querySelector('div#articleImage > a');
  1044. return a != null ? a.href : notFound;
  1045. });
  1046. case 'www.hdtracks.com': case 'hdtracks.com':
  1047. if (!/\/album\/(\w+)\b/.test(url)) break;
  1048. return fetch('https://hdtracks.azurewebsites.net/api/v1/album/' + RegExp.$1).then(response => response.json())
  1049. .then(result => result.status.toLowerCase() == 'ok' ? result.cover : Promise.reject(result.status));
  1050. case 'www.muziekweb.nl': case 'muziekweb.nl':
  1051. if (/\/Link\/(\w+)\b/i.test(url)) return globalXHR(url).then(function({document}) {
  1052. let meta = getFromMeta(document);
  1053. return meta ? meta.replace(/\/COVER\/\w+\b/i, '/COVER/SUPERLARGE') : notFound;
  1054. }); else break;
  1055. case 'www.deejay.de': case 'deejay.de':
  1056. return globalXHR(url).then(function({document}) {
  1057. let elem = document.querySelector('div#gallery > a') || document.querySelector('div.cover a');
  1058. if (elem != null) return 'https://www.deejay.de' + elem.pathname;
  1059. return (elem = getFromMeta(document)) ? elem : notFound;
  1060. }).then(imgUrl => verifyImageUrl(imgUrl.replace(/\/images\/\w+\//i, '/images/xxl/')).catch(() => imgUrl));
  1061. case 'music.163.com':
  1062. if (!/\/album.*\b(?:id)=(\d+)\b/i.test(url.href)) break;
  1063. return globalXHR('https://music.163.com/api/album/' + RegExp.$1, { responseType: 'json' })
  1064. .then(({response}) => response.album.picUrl ?
  1065. response.album.picUrl.replace(/\?.*$/, '').replace(/\b(?:p[123])(?=\.music\.\d+\.net\b)/i, 'p4') : notFound);
  1066. case 'www.tidal.com': case 'listen.tidal.com': case 'tidal.com':
  1067. if (!(/\/album\/(\d+)(?:\/|$)/i.test(url.pathname) && !/\b(?:albumId)=(\d+)\b/i.test(url.search))) break;
  1068. return tidalAccess.requestAPI('albums/' + RegExp.$1).then(album => album.cover ?
  1069. 'https://resources.tidal.com/images/' + album.cover.replace(/-/g, '/') + '/1280x1280.jpg' : notFound);
  1070. case 'www.extrememusic.com': case 'extrememusic.com':
  1071. if (url.pathname.startsWith('/albums/')) return globalXHR(url).then(function({document}) {
  1072. let meta = getFromMeta(document);
  1073. return meta ? meta.replace(/\/album\/\w+\//i, '/album/600/') : notFound;
  1074. }); else break;
  1075. case 'www.recochoku.jp': case 'recochoku.jp':
  1076. if (url.pathname.startsWith('/album/')) return globalXHR(url).then(function({document}) {
  1077. let imgUrl = getFromMeta(document);
  1078. if (!imgUrl) return notFound;
  1079. imgUrl = new URL(imgUrl);
  1080. let params = new URLSearchParams(imgUrl.search);
  1081. params.set('FFw', 999999999); params.set('FFh', 999999999);
  1082. params.delete('h'); params.delete('option');
  1083. imgUrl.search = params;
  1084. return imgUrl;
  1085. }); else break;
  1086. case 'www.elusivedisc.com': case 'elusivedisc.com':
  1087. return globalXHR(url).then(function({document}) {
  1088. let img = document.querySelector('figure > img.zoomImg');
  1089. if (img != null) return img.src;
  1090. img = document.querySelector('section.productView-images > figure');
  1091. return img != null && img.dataset.zoomImage || notFound;
  1092. });
  1093. case 'music.youtube.com':
  1094. return globalXHR(url).then(function({document}) {
  1095. for (let script of document.querySelectorAll('body > script[nonce]')) {
  1096. let data = /\b(?:initialData\.push)\s*\(\s*\{\s*(?:path):\s*('\\\/browse'),\s*(?:params):\s*(.+?)\s*,\s*(?:data):\s*('.+?')\s*\}\s*\);/.exec(script.text);
  1097. if (data != null) try {
  1098. const imgMax = [/(?:=[swh]\d+.*)?$/, '=s0'];
  1099. data = JSON.parse(eval(data[3]));
  1100. if ('frameworkUpdates' in data) try {
  1101. data = data.frameworkUpdates.entityBatchUpdate.mutations
  1102. .find(mutation => mutation.payload && 'musicAlbumRelease' in mutation.payload);
  1103. if (data != undefined && 'thumbnailDetails' in data.payload.musicAlbumRelease)
  1104. return data.payload.musicAlbumRelease.thumbnailDetails.thumbnails[0].url.replace(...imgMax);
  1105. } catch(e) { console.warn(e) }
  1106. if ('header' in data) try {
  1107. data = data.header.musicImmersiveHeaderRenderer.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails;
  1108. if (data) return data[0].url.replace(...imgMax);
  1109. } catch(e) { console.warn(e) }
  1110. } catch(e) { console.warn(e) }
  1111. }
  1112. return notFound;
  1113. });
  1114. case 'www.kuwo.cn': case 'kuwo.cn':
  1115. if (url.pathname.startsWith('/album_detail/')) return globalXHR(url).then(function({document}) {
  1116. for (let script of document.querySelectorAll('body > script')) {
  1117. if (!/\b(?:__NUXT__)\b/.test(script.text)) continue;
  1118. if (/\b(?:pic):"(.+?)"/.test(script.text))
  1119. return eval('"' + RegExp.$1 + '"').replace(/(\/albumcover)\/\d+\//i, '$1/0/');
  1120. }
  1121. return notFound;
  1122. }); else break;
  1123. case 'www.melon.com': case 'melon.com':
  1124. /*if (url.pathname.startsWith('/album/')) */return globalXHR(url).then(function({document}) {
  1125. let imgUrl = getFromMeta(document);
  1126. if (imgUrl) imgUrl = imgUrl.replace(/\?.*$/, ''); else return notFound;
  1127. return verifyImageUrl(imgUrl.replace(/(?:_\d+)?(?=\.\w+$)/, '_1000')).catch(reason => imgUrl);
  1128. });// else break;
  1129. case 'music.bugs.co.kr':
  1130. /*if (url.pathname.startsWith('/album/')) */return globalXHR(url).then(function({document}) {
  1131. let imgUrl = getFromMeta(document);
  1132. return imgUrl ? imgUrl.replace(/(\/album\/images)\/\w+\//i, '$1/original/') : notFound;
  1133. }); //else break;
  1134. case 'www.joox.com': case 'joox.com':
  1135. if (/\/album\/([^\/\?\#]+)/i.test(url.pathname))
  1136. return globalXHR('https://api-jooxtt.sanook.com/page/albumDetail?' + new URLSearchParams({
  1137. id: RegExp.$1,
  1138. lang: 'en',
  1139. country: 'intl',
  1140. device: 'desktop',
  1141. }).toString(), { responseType: 'json' }).then(({response}) => response.albumTracks.images
  1142. && response.albumTracks.images.reduceRight((acc, img) => img.url.replace(/\/(\d+)$/, '/0'), undefined) || notFound);
  1143. case 'mixcloud.com': case 'www.mixcloud.com': {
  1144. const folders = url.pathname.split('/').filter(Boolean);
  1145. if (folders.length <= 0) break;
  1146. const query = folders.length > 1 ? `
  1147. query cloudcastQuery($lookup: CloudcastLookup!) {
  1148. cloudcast: cloudcastLookup(lookup: $lookup) {
  1149. owner { ...CloudcastBaseSidebar_user }
  1150. ...CloudcastHeadTags_cloudcast
  1151. }
  1152. }
  1153. fragment CloudcastBaseSidebar_user on User { ...UserLiveCard_user }
  1154. fragment CloudcastHeadTags_cloudcast on Cloudcast { picture { urlRoot } }
  1155. fragment UserLiveCard_user on User { liveStream { streamStatus id } }
  1156. ` : `
  1157. query userQuery($lookup: UserLookup! $bannerContentKey: String!) {
  1158. user: userLookup(lookup: $lookup) { ...UserHeadTags_user }
  1159. viewer { ...UserDashboardBanner_viewer_1HzGx id }
  1160. }
  1161. fragment UserDashboardBanner_viewer_1HzGx on Viewer { showHideableContent(contentKey: $bannerContentKey) }
  1162. fragment UserHeadTags_user on User { picture { urlRoot } }
  1163. `;
  1164. return mixcloudQuery(query, {
  1165. lookup: { username: folders[0], slug: folders[1] },
  1166. bannerContentKey: 'DASHBOARD_BANNER_PROFILE',
  1167. }).then(function(data) {
  1168. let imgUrl = 'cloudcast' in data ? data.cloudcast.picture.urlRoot
  1169. : 'user' in data ? data.user.picture.urlRoot : null;
  1170. return imgUrl ? 'https://thumbnailer.mixcloud.com/unsafe/' + imgUrl : notFound;
  1171. });
  1172. }
  1173. case 'www.metal-archives.com': case 'metal-archives.com':
  1174. if (url.pathname.startsWith('/albums/')) return globalXHR(url).then(function({document}) {
  1175. const cover = document.getElementById('cover');
  1176. return cover != null ? cover.href.replace(/\?\S*$/, '') : getFromMeta(document) || notFound;
  1177. }); else break;
  1178. case 'www.rateyourmusic.com': case 'rateyourmusic.com':
  1179. if (url.pathname.startsWith('/release/')) return globalXHR(url).then(function({document}) {
  1180. let cover = document.body.querySelector('div.page_release_art_frame img');
  1181. return cover != null ? cover.src : notFound;
  1182. }); else break;
  1183. // books-related
  1184. case 'www.goodreads.com': case 'goodreads.com':
  1185. if (url.pathname.includes('/show/')) return globalXHR(url).then(function({document}) {
  1186. let img = ['div.BookCover__image img', 'div.editionCover > img', 'img#coverImage']
  1187. .reduce((elem, selector) => elem || document.querySelector(selector), null);
  1188. img = img != null ? img.src : getFromMeta(document);
  1189. return img && !['/nophoto/', '/books/1570622405l/50809027', '/images/no-cover.png'].some(pattern =>
  1190. img.includes(pattern)) ? img.replace(/\._\w+_\./g, '.').replace(/\?.*$/, '') : notFound;
  1191. }); else break;
  1192. case 'www.databazeknih.cz': case 'databazeknih.cz':
  1193. if (url.pathname.startsWith('/knihy/')) return globalXHR(url).then(function({document}) {
  1194. let elem = document.querySelector('div#icover_mid > a');
  1195. if (elem != null) return imageUrlResolver('https://www.databazeknih.cz' + elem.pathname, modifiers);
  1196. const imageMax = imageUrl => httpParser.test(imageUrl) ? verifyImageUrl([
  1197. [/\/\d+\/([a-z]+)(?=_)/, 'big'], [/\?.*$/, ''],
  1198. ].reduce((acc, def) => acc.replace(...def), imageUrl)).catch(reason => imageUrl) : Promise.reject('invalid url');
  1199. if ((elem = document.querySelector('div#lbImage')) != null
  1200. && (elem = /\b(?:url)\("(.*)"\)/i.exec(elem.style.backgroundImage)) != null) return imageMax(elem[1]);
  1201. return (elem = document.querySelector('img.kniha_img')) != null ? imageMax(elem.src) : notFound;
  1202. }); else if (url.pathname.startsWith('/obalka-knihy/')) return globalXHR(url).then(function({document}) {
  1203. let elem = document.querySelector('img.book_cover_big');
  1204. return elem != null ? elem.src.replace(/\?.*/, '') : notFound;
  1205. }); else break;
  1206. case 'www.alza.cz': case 'alza.cz': case 'www.alza.sk': case 'alza.sk':
  1207. return globalXHR(url).then(function({document}) {
  1208. const imageMax = imgSrc => imgSrc.replace(/([\?\&])fd=(?:f\d+)\b\&?/i, '$1');
  1209. let meta = document.querySelectorAll('div#galleryPreview a.lightBoxImage');
  1210. if (meta.length > 0) return Array.from(meta)
  1211. .map(a => imageMax(a.dataset.original || a.href || a.dataset.bigimage));
  1212. meta = document.querySelector('div.detail-page > script[type="application/ld+json"]');
  1213. if (meta != null) try { meta = JSON.parse(meta.text) } catch(e) { meta = null }
  1214. if (meta != null && httpParser.test(meta.image)) return imageMax(meta.image);
  1215. return (meta = getFromMeta(document)) ? imageMax(meta) : notFound;
  1216. });
  1217. // movie-related
  1218. case 'www.imdb.com': case 'imdb.com':
  1219. if (!['title/tt', 'name/nm'].some(cat => url.pathname.startsWith('/' + cat))) break;
  1220. return globalXHR(url).then(function(response) {
  1221. const galleryDetector = /\/mediaindex(?:[\/\?].*)?$/i, imgStripper = /\._V\d+_[\w\,]*(?=\.)/;
  1222. if (!galleryDetector.test(response.finalUrl)) {
  1223. let node = response.document.head.querySelector(':scope > script[type="application/ld+json"]');
  1224. if (node != null) try {
  1225. let image = JSON.parse(node.text).image;
  1226. if (typeof image == 'string') return verifyImageUrl(image.replace(imgStripper, '')).catch(reason => notFound);
  1227. } catch(e) { console.warn(e) }
  1228. node = response.document.querySelector('meta[property="og:image"][content]');
  1229. return node != null && !/\/imdb\w*_logo\./i.test(node.content) ?
  1230. node.content.replace(imgStripper, '') : notFound;
  1231. }
  1232. var titleId = /\/title\/(tt\d+)\//i.test(response.finalUrl) && RegExp.$1;
  1233. return titleId ? globalXHR(response.finalUrl.replace(galleryDetector, '/mediaviewer'), { responseType: 'text' }).then(function({responseText}) {
  1234. if (/\b(?:window\.IMDbMediaViewerInitialState)\s*=\s*(\{.*\});/.test(responseText)) try {
  1235. let allImages = eval('(' + RegExp.$1 + ')').mediaviewer.galleries[titleId].allImages;
  1236. if (allImages.length > 0) return allImages.map(image => image.src.replace(imgStripper, ''));
  1237. } catch(e) { console.warn(e) }
  1238. return notFound;
  1239. }) : Promise.reject('title id not found');
  1240. });
  1241. case 'www.themoviedb.org': case 'themoviedb.org':
  1242. if (!['movie', 'person'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1243. return globalXHR(url).then(function({document}) {
  1244. let node = document.querySelector('meta[property="og:image"][content]');
  1245. return verifyImageUrl(node.content.replace(/\/p\/\w+\//i, '/p/original/')).catch(function(reason) {
  1246. node = document.querySelector('div.image_content > img');
  1247. return verifyImageUrl(node.dataset.src.replace(/\/p\/\w+\//i, '/p/original/'))
  1248. .catch(reason => verifyImageUrl(node.src.replace(/\/p\/\w+\//i, '/p/original/')))
  1249. .catch(reason => verifyImageUrl(dataset.src)).catch(reason => node.src);
  1250. }).catch(reason => notFound);
  1251. });
  1252. case 'www.omdb.org': case 'omdb.org':
  1253. if (!['movie', 'person'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1254. return globalXHR(url).then(function({document}) {
  1255. let node = document.querySelector('meta[property="og:image"][content]');
  1256. return node != null ? verifyImageUrl(node.content) : notFound;
  1257. });
  1258. case 'www.thetvdb.com': case 'thetvdb.com':
  1259. if (!['movies', 'series', 'people'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1260. return globalXHR(url).then(({document}) => verifyImageUrl(document.querySelector('img.img-responsive').src));
  1261. case 'www.rottentomatoes.com': case 'rottentomatoes.com':
  1262. if (!['m', 'celebrity', 'tv'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1263. return globalXHR(url).then(function({document}) {
  1264. //if (/\b(?:context\.shell)\s*=\s*(\{.+?});/.test(response.responseText)) try {
  1265. // return JSON.parse(RegExp.$1).header.certifiedMedia.certifiedFreshMovieInTheater4.media.posterImg;
  1266. //} catch(e) { console.warn(e) }
  1267. return verifyImageUrl(document.querySelector('meta[property="og:image"]').content);
  1268. });
  1269. case 'www.bcdb.com': case 'bcdb.com':
  1270. if (!['cartoon'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1271. return globalXHR(url).then(({document}) =>
  1272. verifyImageUrl(document.location.protocol.concat(document.querySelector('meta[property="og:image"]').content)));
  1273. case 'www.boxofficemojo.com': case 'boxofficemojo.com':
  1274. if (!['releasegroup'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1275. return globalXHR(url).then(({document}) => verifyImageUrl(document.querySelector('div.mojo-primary-image img').src));
  1276. case 'www.metacritic.com': case 'metacritic.com':
  1277. return globalXHR(url).then(function({document}) {
  1278. let image = document.querySelector('meta[property="og:image"]').content;
  1279. return verifyImageUrl(image.replace(/-\d+h(?=(?:\.\w+)?$)/, '')).catch(reason => image);
  1280. });
  1281. case 'www.csfd.cz': case 'csfd.cz':
  1282. if (!['film', 'tvurce'].some(cat => url.pathname.startsWith('/' + cat + '/'))) break;
  1283. return globalXHR(url).then(function(response) {
  1284. const gallerySel = 'div.ct-general.photos > div.content > ul > li > div.photo';
  1285. if (response.document.querySelectorAll(gallerySel).length > 0) return new Promise(function(resolve, reject) {
  1286. let urls = [], origin = new URL(response.finalUrl).origin;
  1287. loadPage(response.finalUrl.replace(/\/strana-\d+(?=$|\/|\?)/, ''));
  1288.  
  1289. function loadPage(url) {
  1290. GM_xmlhttpRequest({ method: 'GET', url: url,
  1291. onload: function(response) {
  1292. if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response));
  1293. let dom = domParser.parseFromString(response.responseText, 'text/html');
  1294. Array.prototype.push.apply(urls, Array.from(dom.querySelectorAll(gallerySel))
  1295. .map(div => /^(?:url)\s*\("?(.+?)"?\)$/i.test(div.style.backgroundImage) ?
  1296. 'https:'.concat(RegExp.$1).replace(/\?.*$/, '') : null));
  1297. let nextPage = dom.querySelector('div.paginator > a.next[href]');
  1298. if (nextPage != null) loadPage(origin.concat(nextPage.pathname, nextPage.search)); else resolve(urls);
  1299. },
  1300. onerror: response => { reject(defaultErrorHandler(response)) },
  1301. ontimeout: response => { reject(defaultTimeoutHandler(response)) },
  1302. });
  1303. }
  1304. });
  1305. let img = ['img.film-poster', 'img.creator-photo', 'div.image > img']
  1306. .reduce((acc, selector) => acc || response.document.querySelector(selector), null);
  1307. return img != null ? verifyImageUrl(img.src.replace(/\?.*$/, '')) : notFound;
  1308. });
  1309. case 'www.fdb.cz': case 'fdb.cz':
  1310. //if (!url.pathname.startsWith('/film/')) break;
  1311. return globalXHR(url).then(function({document}) {
  1312. let a = document.querySelector('a.boxPlakaty');
  1313. if (a == null) return Promise.reject('Invalid page structure');
  1314. a.hostname = 'www.fdb.cz';
  1315. return globalXHR(a.href).then(function({document}) {
  1316. let imgs = document.querySelectorAll('span#popup_plakaty > img');
  1317. return imgs.length > 0 ? verifyImageUrl(imgs[0].src) : notFound;
  1318. });
  1319. });
  1320. case 'www.caps-a-holic.com': case 'caps-a-holic.com':
  1321. if (url.pathname == '/c.php') return globalXHR(url).then(function(response) {
  1322. function heightExtractor(n) {
  1323. let node = response.document.querySelector('div.main > div.c_table > div[style]:nth-of-type(' + n + ')');
  1324. if (node != null && /\b(\d{3,})\s?[x×]\s?(\d{3,})\b/.test(node.textContent)) return parseInt(RegExp.$2);
  1325. console.warn(response.finalUrl, 'failed to get resolution (' + n + ')', node);
  1326. return null;
  1327. }
  1328. const baseUrl = 'https://caps-a-holic.com/c_image.php?a=0&x=0&y=0&l=1';
  1329. let result = Array.from(response.document.querySelectorAll('div.main > div[style] > a > img.thumb')).map(function(img) {
  1330. let query = new URLSearchParams(new URL(img.parentNode.href).search);
  1331. return [
  1332. `${baseUrl}&s=${parseInt(query.get('s1'))}&max_height=${heightExtractor(2)}`,
  1333. `${baseUrl}&s=${parseInt(query.get('s2'))}&max_height=${heightExtractor(3)}`,
  1334. ];
  1335. });
  1336. result.caption = Array.from(response.document.querySelectorAll('body > div.bdinfo > div.blue_bar:first-of-type')).map(function(div) {
  1337. let caption = div.childNodes[0].textContent.trim();
  1338. if (div.childNodes.length > 1) caption += ' (' + div.childNodes[1].textContent.trim() + ')';
  1339. return caption;
  1340. });
  1341. return result;
  1342. }); else break;
  1343. case 'www.screenshotcomparison.com': case 'screenshotcomparison.com':
  1344. if (url.pathname.startsWith('/comparison/')) return globalXHR(url).then(function(response) {
  1345. const origin = new URL(response.finalUrl).origin;
  1346. return Array.from(response.document.querySelectorAll('div#img_nav li > a')).map(function(a) {
  1347. return globalXHR(origin.concat(a.pathname), { responseType: 'text' }).then(({responseText}) => [
  1348. /\b(?:images)\[1\]='(\S+?)'/.test(responseText) && RegExp.$1,
  1349. /\b(?:images)\[0\]='(\S+?)'/.test(responseText) && RegExp.$1,
  1350. ].map(src => origin.concat(src)));
  1351. });
  1352. }); else break;
  1353. case 'www.dvdbeaver.com': case 'dvdbeaver.com':
  1354. if (url.pathname.startsWith('/film')) return globalXHR(url).then(function(response) {
  1355. const origin = new URL(response.finalUrl).origin;
  1356. return Array.from(response.document.querySelectorAll('div[align="center"] > table > tbody > tr > td > a[target="_blank"] > img'))
  1357. .map(img => origin.concat(img.parentNode.pathname));
  1358. }); else break;
  1359. }
  1360. return globalXHR(url, { headers: { 'Referer': url.origin } }).then(function({document}) {
  1361. if (url.pathname.startsWith('/album/')
  1362. && document.querySelector('div#tabbed-content-group > div.content-listing > div.pad-content-listing') != null)
  1363. return new Chevereto(url.hostname).galleryResolver(url);
  1364. let elem = document.querySelector('head > meta[name="generator"][content]');
  1365. if (elem != null && elem.content.toLowerCase() == 'bandcamp') {
  1366. elem = document.querySelector('div#tralbumArt > a.popupImage');
  1367. elem = elem != null ? elem.href : getFromMeta(document);
  1368. return httpParser.test(elem) ? elem.replace(/_\d+(?=\.\w+$)/, '_0') : notFound;
  1369. }
  1370. return getFromMeta(document) || notFound;
  1371. });
  1372. }));
  1373. }
  1374.  
  1375. // don't clash with Upload Assistant
  1376. if (document.getElementById('upload-assistant') != null) return imageHostUploaderInit(null, null, null, imageUrlResolver);
  1377.  
  1378. function writeInfo() {
  1379. let input = document.querySelector('input[name="summary"]');
  1380. if (input != null && !input.disabled && !input.value) input.value = 'Image update/rehost';
  1381. }
  1382.  
  1383. const safeRehostSingleImage = imageUrl => imageHosts.rehostImages([imageUrl]).then(singleImageGetter, function(reason) {
  1384. if (['redacted.sh'].includes(document.location.hostname) && imageUrl.includes('.img2go.com/dl/'))
  1385. return forcedRehost(imageUrl);
  1386. return Promise.reject(reason);
  1387. });
  1388.  
  1389. function setImage(url) {
  1390. return verifyImageUrl(url).then(imageUrl => {
  1391. this.value = imageUrl;
  1392. //this.disabled = true;
  1393. this.style.opacity = 0.75;
  1394. writeInfo();
  1395. const size = getRemoteFileSize(imageUrl);
  1396. imagePreview(imageUrl, size);
  1397. return checkImageSize(imageUrl, this, size).then(imageUrl => {
  1398. return safeRehostSingleImage(imageUrl).then(imageUrl => {
  1399. if (imageUrl == null) throw 'invalid image';
  1400. this.value = imageUrl;
  1401. });
  1402. }).catch(reason => {
  1403. this.value = imageUrl;
  1404. logFail(reason + ' (not rehosted)');
  1405. }).then(() => {
  1406. this.style.opacity = null;
  1407. this.disabled = false;
  1408. return imageUrl;
  1409. });
  1410. });
  1411. }
  1412.  
  1413. function inputDataHandler(evt, data) {
  1414. const input = evt.currentTarget;
  1415. console.assert(input instanceof HTMLInputElement, 'input instanceof HTMLInputElement');
  1416.  
  1417. const rehoster = imageUrl => safeRehostSingleImage(imageUrl).then(function(imageUrl) {
  1418. if (!httpParser.test(imageUrl)) {
  1419. console.warn('rehostImages returns invalid image URL:', imageUrl);
  1420. throw 'invalid image URL';
  1421. }
  1422. input.value = imageUrl;
  1423. writeInfo();
  1424. });
  1425.  
  1426. if (!data) return true;
  1427. if (data.files.length > 0) {
  1428. if (data.files[0].type && !data.files[0].type.startsWith('image/')) return true;
  1429. input.disabled = true;
  1430. if (input.hTimer) {
  1431. clearTimeout(input.hTimer);
  1432. delete input.hTimer;
  1433. }
  1434. input.style.color = 'white';
  1435. input.style.backgroundColor = 'darkred';
  1436. let progressBar = { };
  1437. function progressHandler(worker, param = null) {
  1438. if (param && typeof param == 'object') {
  1439. if (param.readyState > 1 || progressBar.current != undefined && worker !== progressBar.current
  1440. || Date.now() < progressBar.lastUpdate + 100) return;
  1441. let pct = Math.floor(Math.min(param.done * 100 / param.total, 100));
  1442. if (pct <= progressBar.lastPct) return;
  1443. input.value = 'Uploading... [' + (progressBar.lastPct = pct) + '%]';
  1444. progressBar.lastUpdate = Date.now();
  1445. } else if (param == null) {
  1446. progressBar = { current: worker };
  1447. input.value = 'Uploading...';
  1448. }
  1449. }
  1450. const file = data.files[0];
  1451. input.disabled = true;
  1452. checkImageSize(file, input, progressHandler).catch(function(reason) {
  1453. logFail('Downsizing of source image not possible (' + reason + '), uploading original size');
  1454. return file;
  1455. }).then(function(result) {
  1456. const uploader = file => imageHosts.uploadImages([file], progressHandler).then(singleImageGetter).then(function(imageUrl) {
  1457. input.value = imageUrl;
  1458. imagePreview(imageUrl, file.size);
  1459. writeInfo();
  1460. });
  1461.  
  1462. if (httpParser.test(result)) return rehoster(result).catch(function(reason) {
  1463. logFail('Downsizing of source image failed (' + reason + '), uploading original size');
  1464. return uploader(file);
  1465. });
  1466. if (result instanceof File) return uploader(result);
  1467. console.warn('invalid checkImageSize(...) result:', result);
  1468. return Promise.reject('invalid checkImageSize(...) result');
  1469. }).then(function() {
  1470. input.style.backgroundColor = '#008000';
  1471. input.hTimer = setTimeout(function() {
  1472. input.style.backgroundColor = null;
  1473. input.style.color = null;
  1474. delete input.hTimer;
  1475. }, 10000);
  1476. }, function(reason) {
  1477. imageClear(evt);
  1478. input.style.backgroundColor = null;
  1479. input.style.color = null;
  1480. Promise.resolve(reason).then(msg => { alert(msg) });
  1481. }).then(() => { input.disabled = false });
  1482. return false;
  1483. } else if (data.items.length > 0) {
  1484. let urls = data.getData('text/uri-list');
  1485. if (urls) urls = urls.split(/\r?\n/); else {
  1486. urls = data.getData('text/x-moz-url');
  1487. if (urls) urls = urls.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  1488. else if (urls = data.getData('text/plain')) urls = urls.split(/\r?\n/);
  1489. }
  1490. if (!Array.isArray(urls) || urls.length <= 0) return true;
  1491. input.disabled = true;
  1492. console.time('Image URL Rehoster');
  1493. imageUrlResolver(urls[0], {
  1494. altKey: evt.altKey,
  1495. ctrlKey: evt.ctrlKey != (input.name == 'image[]'),
  1496. shiftKey: evt.shiftKey,
  1497. }).then(verifyImageUrl).then(function(imageUrl) {
  1498. input.disabled = false;
  1499. input.style.opacity = 0.75;
  1500. input.value = imageUrl;
  1501. const size = getRemoteFileSize(imageUrl);
  1502. imagePreview(imageUrl, size);
  1503. checkImageSize(imageUrl, input, size).then(rehoster).catch(function(reason) {
  1504. input.value = imageUrl;
  1505. Promise.resolve(reason).then(msg => { alert(msg + ' (not rehosted)') });
  1506. }).then(() => { console.timeEnd('Image URL Rehoster') });
  1507. }).catch(reason => { Promise.resolve(reason).then(alert) }).then(function() {
  1508. input.style.opacity = null;
  1509. input.disabled = false;
  1510. });
  1511. return false;
  1512. }
  1513. return true;
  1514. }
  1515.  
  1516. function arrayGrouping(arr) {
  1517. return Array.isArray(arr) ? arr.map(function(elem) {
  1518. if (!Array.isArray(elem)) return 1;
  1519. return elem.every(elem => !Array.isArray(elem)) ? elem.length : arrayGrouping(elem);
  1520. }) : null;
  1521. }
  1522.  
  1523. function isGroupBoundary(groups, index) {
  1524. return index > 0 && Array.isArray(groups)
  1525. && groups.some((len, ndx, arr) => index == arr.slice(0, ndx).reduce((acc, len) => acc + len, 0));
  1526. }
  1527.  
  1528. let opti_PNG = GM_getValue('optipng', false);
  1529.  
  1530. function rehoster(promises, resultsHandler, target = null) {
  1531. if (!Array.isArray(promises)) throw 'invalid parameter';
  1532. console.time('Image URL Resolver');
  1533. return Promise.all(promises).then(function(resolved) {
  1534. let resolvedUrls = resolved.flatten();
  1535. if (target instanceof HTMLElement) {
  1536. target.disabled = true;
  1537. if (resolvedUrls.length > 1 && !['notwhat.cd'].some(hostname => document.domain == hostname))
  1538. var progressBar = new RHProgressBar(target, resolvedUrls.length);
  1539. }
  1540. return (function() {
  1541. if (!opti_PNG || !(target instanceof HTMLElement)) return Promise.resolve(resolvedUrls);
  1542. return Promise.all(resolvedUrls.map(resolvedUrl => optiPNG(resolvedUrl).catch(reason => resolvedUrl)));
  1543. })().then(srcUrls => imageHosts.rehostImages(srcUrls, RHProgressBar.prototype.update.bind(progressBar)).catch(function(reason) {
  1544. logFail(reason + ' (not rehosted)');
  1545. RHProgressBar.prototype.update.call(progressBar, -1, false);
  1546. return verifyImageUrls(srcUrls);
  1547. }).then(function(results) {
  1548. resolved.forEach(function(elem, index) {
  1549. if (!elem.caption) return;
  1550. if (!Array.isArray(results.captions)) results.captions = [ ];
  1551. results.captions.push(elem.caption);
  1552. });
  1553. resultsHandler(results, arrayGrouping(resolved).flatten());
  1554. }).catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })).then(function() {
  1555. RHProgressBar.prototype.cleanUp.call(progressBar);
  1556. if (target instanceof HTMLElement) target.disabled = false;
  1557. console.timeEnd('Image URL Resolver');
  1558. });
  1559. });
  1560. }
  1561.  
  1562. function textAreaDropHandler(evt) {
  1563. if (!evt.dataTransfer || evt.shiftKey) return true;
  1564. const textArea = evt.currentTarget;
  1565. console.assert(textArea instanceof HTMLTextAreaElement, 'textArea instanceof HTMLTextAreaElement');
  1566. if (evt.dataTransfer.files.length > 0) {
  1567. let images = Array.from(evt.dataTransfer.files).filter(file => !file.type || file.type.startsWith('image/'));
  1568. if (images.length <= 0) return true;
  1569. textArea.disabled = true;
  1570. if (!['notwhat.cd'].some(hostname => document.domain == hostname))
  1571. var progressBar = new ULProgressBar(textArea, images.map(image => image.size));
  1572. (function() {
  1573. if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG');
  1574. ULProgressBar.prototype.update.call(progressBar, -1);
  1575. return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) =>
  1576. ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler);
  1577. })().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler))
  1578. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
  1579. .then(function() {
  1580. ULProgressBar.prototype.cleanUp.call(progressBar);
  1581. textArea.disabled = false;
  1582. });
  1583. evt.stopPropagation();
  1584. return false;
  1585. } else if (evt.dataTransfer.items.length > 0) {
  1586. let content = evt.dataTransfer.getData('text/uri-list');
  1587. if (content) content = content.split(/(?:\r?\n)+/); else {
  1588. content = evt.dataTransfer.getData('text/x-moz-url');
  1589. if (content) content = content.split(/(?:\r?\n)+/).filter((item, ndx) => ndx % 2 == 0);
  1590. };
  1591. if (!Array.isArray(content) || content.length <= 0) return true;
  1592. rehoster(content.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, textArea).catch(function(reason) {
  1593. if (evt.ctrlKey)
  1594. textArea.value = textArea.value.slice(0, evt.rangeOffset) + content.join('\n') +
  1595. textArea.value.slice(evt.rangeOffset);
  1596. else {
  1597. if (textArea.value.length > 0) textArea.value += '\n\n';
  1598. textArea.value += content.join('\n');
  1599. }
  1600. });
  1601. evt.stopPropagation();
  1602. return false;
  1603. }
  1604. return true;
  1605.  
  1606. function resultsHandler(results, groups = undefined) {
  1607. if (results.length <= 0) return;
  1608. if (evt.altKey && !textArea.noBBCode) {
  1609. let modal = document.createElement('div');
  1610. modal.id = 'ihh-template-selector-background';
  1611. modal.style = 'position: fixed; left: 0; top: 0; width: 100%; height: 100%; background-color: #0008;' +
  1612. 'opacity: 0; transition: opacity 0.15s linear;';
  1613. modal.innerHTML = `
  1614. <form id="ihh-template-selector" style="background-color: darkslategray; position: absolute; top: 30%; left: 35%; border-radius: 0.5em; padding: 20px 30px;">
  1615. <div style="color: white; margin-bottom: 20px;">Insert as:</div>
  1616. <input id="btn-insert" type="button" value="Insert" style="margin-top: 30px"/>
  1617. <input id="btn-cancel" type="button" value="Cancel" style="margin-top: 30px"/>
  1618. </form>
  1619. `;
  1620. document.body.append(modal);
  1621. let form = document.getElementById('ihh-template-selector'),
  1622. btnInsert = form.querySelector('input#btn-insert'),
  1623. btnCancel = form.querySelector('input#btn-cancel');
  1624. if (form == null || btnInsert == null || btnCancel == null) {
  1625. console.warn('Dialog creation error');
  1626. insertResults();
  1627. return;
  1628. }
  1629. [
  1630. ['BBcode: original size', 1],
  1631. ['BBcode: thumbnails with link to original', 2],
  1632. ['BBcode: thumbnails with link to share page', 3],
  1633. ['BBcode: screenshot comparison (PTP)', 4],
  1634. ['BBcode: screenshot comparison + encode images (PTP)', 5],
  1635. ['Markdown: original size', 9],
  1636. ['HTML: original size', 6],
  1637. ['HTML: thumbnails with link to original', 7],
  1638. ['HTML: thumbnails with link to share page', 8],
  1639. ['Raw links', 0],
  1640. ].forEach(function(item) {
  1641. let radio = document.createElement('input');
  1642. radio.type = 'radio';
  1643. radio.name = 'template';
  1644. radio.value = item[1];
  1645. radio.style = 'margin: 5px 15px 5px 0px; cursor: pointer;';
  1646. let label = document.createElement('label');
  1647. label.style = 'color: white; cursor: pointer; -webkit-user-select: none; ' +
  1648. '-moz-user-select: none; -ms-user-select: none; user-select: none;';
  1649. label.append(radio);
  1650. label.append(item[0]);
  1651. form.insertBefore(label, btnInsert);
  1652. let br = document.createElement('br');
  1653. form.insertBefore(br, btnInsert);
  1654. });
  1655. if (!results.some(result => typeof result == 'object'
  1656. && httpParser.test(result.original) && httpParser.test(result.thumb))) disableItem(2, 7);
  1657. if (!results.some(result => typeof result == 'object'
  1658. && httpParser.test(result.original) && httpParser.test(result.share))) disableItem(3, 8);
  1659. if (results.length % 2 != 0) disableItem(4, 5);
  1660. form.onclick = evt => { evt.stopPropagation() };
  1661. btnInsert.onclick = function(evt) {
  1662. let template = document.querySelector('form#ihh-template-selector input[name="template"]:checked');
  1663. if (template != null) template = parseInt(template.value);
  1664. modal.remove();
  1665. insertResults(template);
  1666. };
  1667. modal.onclick = btnCancel.onclick = evt => { modal.remove() };
  1668. window.setTimeout(() => { modal.style.opacity = 1 });
  1669.  
  1670. function disableItem(...n) {
  1671. n.forEach(function(n) {
  1672. let radio = document.querySelector('div#ihh-template-selector input[type="radio"][value="' + n + '"]');
  1673. if (radio == null) return;
  1674. radio.parentNode.style.opacity = 0.5;
  1675. radio.disabled = true;
  1676. });
  1677. }
  1678. } else insertResults();
  1679.  
  1680. function insertResults(template = 1) {
  1681. if (textArea.noBBCode) template = 0;
  1682. if (typeof template != 'number' || isNaN(template)) return;
  1683. let code = '', nl = [6, 7, 8].includes(template) ? '<br>\n' : '\n', _template;
  1684. results.forEach(function(result, index) {
  1685. if (_template == 1 && /\[img\]\[\/img\]/i.test(textArea.value)) {
  1686. textArea.value = RegExp.leftContext + '[img]' + getImgUrl(result) + '[/img]' + RegExp.rightContext;
  1687. return;
  1688. }
  1689. _template = template;
  1690. if (template == 2 && (typeof result != 'object' || !httpParser.test(result.original) || !httpParser.test(result.thumb))
  1691. || template == 3 && (typeof result != 'object' || !httpParser.test(result.share) || !httpParser.test(result.thumb)))
  1692. _template = 1;
  1693. else if (template == 7 && (typeof result != 'object' || !httpParser.test(result.original) || !httpParser.test(result.thumb))
  1694. || template == 8 && (typeof result != 'object' || !httpParser.test(result.share) || !httpParser.test(result.thumb)))
  1695. _template = 6;
  1696. else _template = template;
  1697. if (index > 0) {
  1698. let thumb = [2, 3, 7, 8].includes(_template);
  1699. code += isGroupBoundary(groups, index) ? thumb ? nl : nl + nl : thumb ? ' ' : nl;
  1700. }
  1701. switch (_template) {
  1702. case 0: case 4: case 5: code += getImgUrl(result); break;
  1703. case 1: code += '[img]' + getImgUrl(result) + '[/img]'; break;
  1704. case 2: code += '[url=' + getImgUrl(result) + '][img]' + result.thumb + '[/img][/url]'; break;
  1705. case 3: code += '[url=' + result.share + '][img]' + result.thumb + '[/img][/url]'; break;
  1706. case 6: code += '<img src="' + getImgUrl(result) + '">'; break;
  1707. case 7: code += '<a href="' + getImgUrl(result) + '" target="_blank"><img src="' + result.thumb + '"></a>'; break;
  1708. case 8: code += '<a href="' + result.share + '" target="_blank"><img src="' + result.thumb + '"></a>'; break;
  1709. case 9: code += '![](' + getImgUrl(result) + ')'; break;
  1710. }
  1711. });
  1712. if ([4, 5].includes(template)) {
  1713. if (Array.isArray(results.captions)) {
  1714. var captions = results.captions.shift();
  1715. if (Array.isArray(captions)) captions = captions.join(', ');
  1716. }
  1717. code = '[comparison=' + (captions || 'Source, Encode') + ']' + code + '[/comparison]';
  1718. if (template == 5) {
  1719. code += nl;
  1720. results.forEach((result, index) => { if (index % 2 != 0) code += nl + '[img]' + getImgUrl(result) + '[/img]' });
  1721. }
  1722. }
  1723. if (textArea.value.trimRight().length <= 0) textArea.value = code; else if (evt.ctrlKey) {
  1724. textArea.value = textArea.value.slice(0, evt.rangeOffset) + code + textArea.value.slice(evt.rangeOffset);
  1725. } else textArea.value = textArea.value.trimRight() + nl + nl + code;
  1726.  
  1727. function getImgUrl(result) {
  1728. if (typeof result == 'object' && httpParser.test(result.original)) return result.original;
  1729. if (typeof result == 'string' && httpParser.test(result)) return result;
  1730. throw 'Invalid result format';
  1731. }
  1732. }
  1733. }
  1734. }
  1735.  
  1736. function textAreaPasteHandler(evt) {
  1737. if (!evt.clipboardData) return true;
  1738. const textArea = evt.currentTarget;
  1739. console.assert(textArea instanceof HTMLTextAreaElement, 'textArea instanceof HTMLTextAreaElement');
  1740. if (evt.clipboardData.files.length > 0) {
  1741. let images = Array.from(evt.clipboardData.files).filter(file => !file.type || file.type.startsWith('image/'));
  1742. if (images.length <= 0) return true;
  1743. textArea.disabled = true;
  1744. if (!['notwhat.cd'].some(hostname => document.domain == hostname))
  1745. var progressBar = new ULProgressBar(textArea, images.map(image => image.size));
  1746. (function() {
  1747. if (!opti_PNG || !images.every(image => image.type == 'image/png')) return Promise.reject('!optiPNG');
  1748. ULProgressBar.prototype.update.call(progressBar, -1);
  1749. return rehoster([Promise.all(images.map((image, index) => optiPNG(image, (param = null) =>
  1750. ULProgressBar.prototype.update.call(progressBar, -1, param, index))))], resultsHandler);
  1751. })().catch(reason => imageHosts.uploadImages(images, ULProgressBar.prototype.update.bind(progressBar)).then(resultsHandler))
  1752. .catch(reason => { Promise.resolve(reason).then(msg => { alert(msg) }) })
  1753. .then(function() { // __finally
  1754. ULProgressBar.prototype.cleanUp.call(progressBar);
  1755. textArea.disabled = false;
  1756. });
  1757. evt.stopPropagation();
  1758. return false;
  1759. } else if (evt.clipboardData.items.length > 0) {
  1760. return true;
  1761. let urls = evt.clipboardData.getData('text/plain').split(/(?:\r?\n)+/);
  1762. if (urls.length <= 0 || !urls.every(RegExp.prototype.test.bind(httpParser))) return true;
  1763. rehoster(urls.map(url => imageUrlResolver(url, { ctrlKey: !evt.ctrlKey })), resultsHandler, textArea);
  1764. evt.stopPropagation();
  1765. return false;
  1766. }
  1767. return true;
  1768.  
  1769. function resultsHandler(results, groups = undefined) {
  1770. let selStart = textArea.selectionStart, phpBB = '';
  1771. results.forEach(function(result, index) {
  1772. let thumb = evt.altKey && !textArea.noBBCode && typeof result == 'object'
  1773. && httpParser.test(result.originasl) && httpParser.test(result.thumb);
  1774. if (index > 0) phpBB += isGroupBoundary(groups, index) ? thumb ? '\n' : '\n\n' : thumb ? ' ' : '\n';
  1775. if (typeof result == 'object' && result.original) var imgUrl = result.original;
  1776. else if (typeof result == 'string') imgUrl = result;
  1777. else throw 'Invalid result format';
  1778. phpBB += textArea.noBBCode ? phpBB += imgUrl : !thumb ? '[img]' + imgUrl + '[/img]'
  1779. : '[url=' + imgUrl + '][img]' + result.thumb + '[/img][/url]';
  1780. });
  1781. if (phpBB.length <= 0) return;
  1782. textArea.value = textArea.value.slice(0, selStart) + phpBB + textArea.value.slice(textArea.selectionEnd);
  1783. textArea.setSelectionRange(selStart + phpBB.length, selStart + phpBB.length);
  1784. }
  1785. }
  1786.  
  1787. imageHostUploaderInit(inputDataHandler, textAreaDropHandler, textAreaPasteHandler, imageUrlResolver);
  1788.  
  1789. function hookToRoot(root = document.body) {
  1790. if (!(root instanceof HTMLElement)) throw 'Assertion failed: argument not HTML element';
  1791. if (root != document && root != document.body) console.log('[IHH] Attaching drop/paste handlers to', root);
  1792. // Set single input UI handlers
  1793. let imageInputMatch = GM_getValue('image_input_match', '/(?:image|img|picture|cover|photo|avatar|poster|screen)/i');
  1794. if ((imageInputMatch = /^\/(.+)\/([dgimsuy]*)$/.exec(imageInputMatch)) != null) try {
  1795. imageInputMatch = new RegExp(imageInputMatch[1], imageInputMatch[2]);
  1796. for (let input of root.getElementsByTagName('INPUT')) if (['text', 'url'].includes(input.type)
  1797. && ['id', 'name'].some(attribute => imageInputMatch.test(input[attribute] || input.getAttribute(attribute))))
  1798. setInputHandlers(input);
  1799. } catch(e) { console.warn('Image Host Helper: failed to compile image input matcher', e, imageInputMatch) }
  1800. else console.warn('Image Host Helper: custom text inputs match expression not in proper regexp format; no text inputs will be handled');
  1801. // Set multiple inputs UI handlers
  1802. for (let textArea of root.getElementsByTagName('TEXTAREA'))
  1803. if (!['ua-data'].some(id => textArea.id == id) && ![
  1804. 'no-image-host-helper', 'no-ihh', 'image-host-helper-aware', 'no-image-input',
  1805. ].some(cls => textArea.classList.contains(cls))) setTextAreahandlers(textArea);
  1806. }
  1807. hookToRoot();
  1808. let siteDynaloads = GM_getValue('site_dynaloads');
  1809. if (typeof siteDynaloads == 'string') try { siteDynaloads = JSON.parse(siteDynaloads) } catch(e) { console.warn(e) }
  1810. if (siteDynaloads && typeof siteDynaloads == 'object' && (siteDynaloads = siteDynaloads[document.location.hostname])) {
  1811. if (typeof siteDynaloads == 'string') siteDynaloads = siteDynaloads.split(/\s*[,;]\s*/);
  1812. if (Array.isArray(siteDynaloads) && siteDynaloads.length > 0) for (let selector of siteDynaloads)
  1813. for (let mountPoint of document.body.querySelectorAll(selector)) {
  1814. console.log('[IHH] Watching for added content on', mountPoint);
  1815. new MutationObserver((ml, mo) => { for (let mutation of ml) mutation.addedNodes.forEach(hookToRoot) })
  1816. .observe(mountPoint, { childList: true });
  1817. }
  1818. }
  1819.  
  1820. {
  1821. const tbody = document.body.querySelector('div#dynamic_form > table > tbody');
  1822. if (tbody != null) new MutationObserver(function(ml, mo) {
  1823. for (let mutation of ml) for (let node of mutation.addedNodes)
  1824. if (node.tagName == 'TR' && node.id.startsWith('extra_format_row'))
  1825. for (let option of node.querySelectorAll('select > option'))
  1826. if (option.label.startsWith('function(')) option.remove();
  1827. }).observe(tbody, { childList: true });
  1828. }
  1829.  
  1830. switch (document.location.pathname) {
  1831. case '/torrents.php': {
  1832. if (!document.location.search.startsWith('?id=')) break;
  1833. const addCoversForm = document.getElementById('add_cover');
  1834. if (addCoversForm != null) new MutationObserver(function(mutationsList, mo) {
  1835. for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
  1836. if (node.nodeName == 'INPUT' && node.type == 'text' && node.name == 'image[]') setInputHandlers(node);
  1837. });
  1838. }).observe(addCoversForm, { childList: true });
  1839. break;
  1840. }
  1841. case '/reportsv2.php': {
  1842. const dynaForm = document.getElementById('dynamic_form');
  1843. if (dynaForm == null) break;
  1844. function setReportHandlers(root = dynaForm) {
  1845. root.querySelectorAll('input[id*="image"]').forEach(setInputHandlers);
  1846. for (let ta of root.getElementsByTagName('TEXTAREA')) setTextAreahandlers(ta);
  1847. }
  1848. new MutationObserver(function(mutationsList, mo) {
  1849. for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
  1850. if (node.nodeType == Node.ELEMENT_NODE) setReportHandlers(node);
  1851. });
  1852. }).observe(dynaForm, { childList: true });
  1853. break;
  1854. }
  1855. case '/forums.php': {
  1856. if (!document.location.search.startsWith('?action=viewthread&')) break;
  1857. let container = document.querySelector('div#content > div.thin');
  1858. if (container != null) new MutationObserver(function(mutationsList, mo) {
  1859. for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
  1860. if (node.nodeName == 'FORM') for (let elem of node.getElementsByTagName('TEXTAREA')) setTextAreahandlers(elem);
  1861. });
  1862. }).observe(container, { childList: true, subtree: true });
  1863. break;
  1864. }
  1865. }
  1866.  
  1867. let bpAccessToken;
  1868. // site-specific extensions
  1869. switch (document.domain) {
  1870. case 'passthepopcorn.me':
  1871. // Auto-fill missing/invalid images from IMDB
  1872. if (document.location.pathname == '/artist.php' && /^\?action=edit&artistid=(\d+)\b/i.test(document.location.search)
  1873. && GM_getValue('auto_lookup_artist_image', true)) {
  1874. let artistId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]');
  1875. if (input != null) verifyImageUrl(input.value).catch(function(reason) {
  1876. if (input.value) input.value = '';
  1877. localXHR('/artist.php?id=' + artistId).then(function(dom) {
  1878. let imdb = dom.querySelector('div#artistinfo > div.panel__body > ul.list > li > a');
  1879. if (imdb != null) imageUrlResolver(imdb.href)
  1880. .then(setImage.bind(input), reason => { logFail('No IMDB photo of this artist') });
  1881. });
  1882. });
  1883. } else if (document.location.pathname == '/torrents.php'
  1884. && /^\?action=editgroup&groupid=(\d+)\b/i.test(document.location.search)
  1885. && GM_getValue('auto_lookup_artist_image', true)) {
  1886. let groupId = parseInt(RegExp.$1), input = document.querySelector('input[name="image"]');
  1887. if (input != null) verifyImageUrl(input.value).catch(function(reason) {
  1888. if (input.value) input.value = '';
  1889. localXHR('/torrents.php?id=' + groupId).then(function(dom) {
  1890. let imdb = dom.querySelector('a#imdb-title-link');
  1891. if (imdb != null) imageUrlResolver(imdb.href)
  1892. .then(setImage.bind(input), reason => { logFail('No IMDB poster for this movie') });
  1893. });
  1894. });
  1895. }
  1896. // hook to HJ Member Toolkit
  1897. new MutationObserver(function(mutationsList, mo) {
  1898. for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
  1899. if (node.nodeName != 'DIV' || !node.classList.contains('HJ-toolkit-member-toolbar')) return;
  1900. mo.disconnect();
  1901. new MutationObserver(function(mutationsList, mo) {
  1902. for (let mutation of mutationsList) mutation.addedNodes.forEach(function(node) {
  1903. if (node.nodeName != 'DIV' || !node.classList.contains('HJ-toolkit-member-toolbar-flex')) return;
  1904. mo.disconnect();
  1905. node.querySelectorAll([
  1906. //'textarea[id^="HJMA"]',
  1907. 'textarea[name="screenshots"]',
  1908. 'textarea[name="comparisons"]',
  1909. ].join(',')).forEach(setTextAreahandlers);
  1910. });
  1911. }).observe(node, { childList: true, subtree: true });
  1912. });
  1913. }).observe(document.body, { childList: true });
  1914. break;
  1915. case 'redacted.sh':
  1916. case 'orpheus.network':
  1917. case 'dicmusic.club': case 'dicmusic.com':
  1918. case 'notwhat.cd':
  1919. switch (document.location.pathname.slice(1)) {
  1920. case 'upload.php':
  1921. document.body.querySelectorAll('input[type="text"][name="verification"]').forEach(setInputHandlers);
  1922. break;
  1923. case 'artist.php':
  1924. if (document.location.search.startsWith('?action=edit&')) {
  1925. const imageInput = document.body.querySelector('input[name="image"]');
  1926. console.assert(imageInput != null, 'Image input not found!');
  1927. if (imageInput == null) break; // assertion failed
  1928. let artist = document.body.querySelector('div.header > h2 > a');
  1929. console.assert(artist != null, 'Artist title not found!');
  1930. if (artist != null) artist = artist.textContent.trim(); else break; // throw 'Artist name not found';
  1931.  
  1932. function lookupArtistImage() {
  1933. function resultsFilter(results0, nameExtractor) {
  1934. const tailingBracketStripper = [/\s*\(([^\(\)]+)\)\s*$/, ''],
  1935. norm = artist => artist && artist.replace(/\s+/g, '').toLowerCase();
  1936. const transforms = [n => n && n.replace(...tailingBracketStripper),
  1937. n => n && (n = tailingBracketStripper[0].exec(n)) && n[1]];
  1938. let results = results0.filter(function(result) {
  1939. let n = [artist, nameExtractor(result)].map(n => transforms.map(func => func(n)));
  1940. for (let i = 0; i < 2; ++i) if (n[0][i]) for (let j = 0; j < 2; ++j)
  1941. if (n[1][j] && norm(n[0][i]) == norm(n[1][j])) return true;
  1942. return norm(n[0][0].toASCII()) == norm(n[1][0].toASCII());
  1943. }), f;
  1944. if (results.length > 1) {
  1945. f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toASCII().toLowerCase()
  1946. == artist.replace(...tailingBracketStripper).toASCII().toLowerCase());
  1947. if (f.length > 0) results = f;
  1948. }
  1949. if (results.length > 1) {
  1950. f = results0.filter(result => nameExtractor(result).replace(...tailingBracketStripper).toLowerCase()
  1951. == artist.replace(...tailingBracketStripper).toLowerCase());
  1952. if (f.length > 0) results = f;
  1953. }
  1954. return results;
  1955. }
  1956.  
  1957. const lookupWorkers = [
  1958. // Qobuz
  1959. globalXHR('https://www.qobuz.com/shop', { responseType: 'text' }).then(function({responseText}) {
  1960. const rx = /^\s*(?:(?:window\.)?qobuz\.algolia(\d+))\s*=\s*(\{.*\});/gm, algolia = { };
  1961. let m;
  1962. while ((m = rx.exec(responseText)) != null) try {
  1963. const obj = JSON.parse(m[2]);
  1964. if (obj.api_key && obj.application_id) algolia[parseInt(m[1])] = obj;
  1965. } catch (e) { console.warn(e) }
  1966. return algolia[2] ? algolia : Promise.reject('unexpected page structure');
  1967. }).then(algolia => globalXHR(`https://${algolia[2].application_id.toLowerCase()}-1.algolianet.com/1/indexes/${algolia[2].index.main_artists}/query?${new URLSearchParams({
  1968. 'x-algolia-application-id': algolia[2].application_id,
  1969. 'x-algolia-api-key': algolia[2].api_key,
  1970. }).toString()}`, { responseType: 'json' }, { 'params': 'query=' + encodeURIComponent(artist) })).then(function({response}) {
  1971. if (response.nbHits <= 0) return Promise.reject('Qobuz: no matches');
  1972. let results = resultsFilter(response.hits, result => result.name);
  1973. if (results.length <= 0) return Promise.reject('Qobuz: no matches');
  1974. console.info('Qobuz search results for "' + artist + '":', results);
  1975. if (results.length > 1) return Promise.reject('Qobuz: ambiguity');
  1976. if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results);
  1977. return httpParser.test(results[0].image) ? results[0].image.replace(/(\/artists\/covers)\/\w+\//i, '$1/large/')
  1978. : Promise.reject('Qobuz: artist exists but no photo');
  1979. }),
  1980. // AllMusic
  1981. globalXHR('https://www.allmusic.com/search/artists/' + encodeURIComponent(artist)).then(function({document}) {
  1982. let results = resultsFilter(Array.from(document.body.querySelectorAll('div#resultsContainer > div > div.artist')).map(function(div) {
  1983. let result = {
  1984. name: div.querySelector('div.name > a'),
  1985. genres: div.querySelector('div.genres'),
  1986. decades: div.querySelector('div.decades'),
  1987. };
  1988. Object.keys(result).forEach(key => {
  1989. result[key] = result[key] != null ? result[key].textContent.trim() || undefined : undefined;
  1990. });
  1991. if (result.genres) result.genres = result.genres.split(',').map(genre => genre.trim());
  1992. result.url = div.querySelector('div.name > a');
  1993. result.url = result.url != null ? result.url.href : undefined;
  1994. result.id = (result.id = /-(mw\d{10})$/i.exec(result.url)) != null ? result.id[1] : undefined;
  1995. if ((result.image = div.querySelector('div.photo img')) != null) try {
  1996. result.image = new URL(result.image.src);
  1997. result.image.searchParams.set('f', 0);
  1998. result.image = result.image.href;
  1999. } catch(e) { console.warn(e) } else result.image = undefined;
  2000. return result;
  2001. }), result => result.name);
  2002. if (results.length <= 0) return Promise.reject('AllMusic: no matches');
  2003. console.info('AllMusic search results for "' + artist + '":', results);
  2004. if (results.length > 1) return Promise.reject('AllMusic: ambiguity');
  2005. if (results.length > 1) console.info('Qobuz returns ambiguous results for "' + artist + '":', results);
  2006. if (!httpParser.test(results[0].image)) return Promise.reject('AllMusic: artist exists but no photo');
  2007. return verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=6'))
  2008. .catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=0')))
  2009. .catch(reason => verifyImageUrl(results[0].image.replace(/\b(?:f)=\d+$/i, 'f=5')));
  2010. }),
  2011. // NetEase
  2012. globalXHR('https://music.163.com/api/cloudsearch/get/web?' + new URLSearchParams({
  2013. s: '"' + artist + '"',
  2014. type: 100,
  2015. limit: 50,
  2016. //csrf_token: '',
  2017. }).toString(), { responseType: 'json' }).then(function({response}) {
  2018. if (!response.result) return Promise.reject('API returns malformed result');
  2019. return !response.abroad ? response.result : (function() {
  2020. function injectScript(src, errorHandler) {
  2021. console.assert(src);
  2022. coreJS = document.createElement('script');
  2023. coreJS.id = 'netease.core.js';
  2024. coreJS.type = 'text/javascript';
  2025. coreJS.async = false;
  2026. const promise = new Promise(function(resolve, reject) {
  2027. function errorHandler(currentTarget, reason) {
  2028. console.warn('NetEase core.js (%s): %s', currentTarget.src, reason);
  2029. if (typeof errorHandler == 'function') errorHandler(resolve, reject, currentTarget);
  2030. else reject('NetEase core.js ' + reason);
  2031. }
  2032.  
  2033. coreJS.onload = function(evt) {
  2034. if ([/*'asrsea', */'settmusic'].every(function(pubSym) {
  2035. try { return typeof eval(pubSym) == 'function' } catch(e) { return false }
  2036. })) resolve(evt.currentTarget); else errorHandler(evt.currentTarget, 'public functions not accessible');
  2037. };
  2038. coreJS.onerror = evt => { errorHandler(evt.currentTarget, 'loading error') };
  2039. coreJS.src = src;
  2040. document.head.append(coreJS);
  2041. });
  2042. return (coreJS.loader = promise);
  2043. }
  2044.  
  2045. var coreJS = document.getElementById('netease.core.js');
  2046. if (coreJS != null && coreJS.loader instanceof Promise) return coreJS.loader;
  2047. return injectScript('https://s1.music.126.net/web/s/core.js', function(resolve, reject, currentScript) {
  2048. console.warn('Trying to fetch core.js url from root doc');
  2049. globalXHR('https://music.163.com/').then(function({document}) {
  2050. const script = document.body.querySelector(':scope > script[src*="/core"]');
  2051. if (script != null && script.src) {
  2052. window.document.head.removeChild(currentScript);
  2053. injectScript(script.src).then(resolve, reject);
  2054. } else reject('Invalid root document structure');
  2055. }, reject);
  2056. });
  2057. })().then(core => decodeURIComponent(settmusic(response.result, 'fuck~#$%^&*(458')));
  2058. }).then(result => JSON.parse(result)).then(result => result.artistCount > 0 ?
  2059. result.artists : Promise.reject('NetEase: no matches'), function(reason) {
  2060. console.warn('NetEase search-list method failed:', reason);
  2061. return globalXHR('https://music.163.com/api/search/suggest/web?'+ new URLSearchParams({
  2062. s: '"' + artist + '"',
  2063. type: 100,
  2064. limit: 50,
  2065. //csrf_token: '',
  2066. }, { responseType: 'json' })).then(function({response}) {
  2067. if (response.code != 200 || !response.result)
  2068. return Promise.reject('API returns malformed result (' + response.msg + ')');
  2069. return Array.isArray(response.result.artists) && response.result.artists.length > 0 ?
  2070. response.result.artists : Promise.reject('NetEase: no matches');
  2071. });
  2072. }).then(function(results) {
  2073. console.assert(Array.isArray(results) && results.length > 0, "Array.isArray(results) && results.length > 0");
  2074. if (!Array.isArray(results) || results.length <= 0) return Promise.reject('NetEase: no matches');
  2075. results = resultsFilter(results/*.filter(artist => artist.picId > 0)*/, result => result.name);
  2076. if (results.length <= 0) return Promise.reject('NetEase: no matches');
  2077. console.info('NetEase search results for "' + artist + '":', results);
  2078. if (results.length > 1) return Promise.reject('NetEase: ambiguity');
  2079. //if (results.length > 1) console.info('NetEase returns ambiguous results for "' + artist + '":', results);
  2080. const imgMax = imgUrl => imgUrl.replace(/\?.*$/, '').replace(/\b(?:p[123])(?=\.music\.\d+\.net\b)/i, 'p4');
  2081. const isDummy = imgUrl => ['/5639395138885805.jpg'].some(dummy => imgUrl.toLowerCase().endsWith(dummy));
  2082. if (results[0].picId > 0 && httpParser.test(results[0].picUrl) && !isDummy(results[0].picUrl))
  2083. return imgMax(results[0].picUrl);
  2084. if (results[0].img1v1 > 0 && httpParser.test(results[0].img1v1Url) && !isDummy(results[0].img1v1Url))
  2085. return imgMax(results[0].img1v1Url);
  2086. return Promise.reject('NetEase: artist exists but no photo');
  2087. }),
  2088. // Tidal
  2089. tidalAccess.requestAPI('search/artists', { query: artist, limit: 25 }).then(function(response) {
  2090. if (response.totalNumberOfItems <= 0) return Promise.reject('Tidal: no matches');
  2091. let results = resultsFilter(response.items, item => item.name);
  2092. if (results.length <= 0) return Promise.reject('Tidal: no matches');
  2093. console.info('Tidal search results for "' + artist + '":', results);
  2094. if (results.length > 1) return Promise.reject('Tidal: ambiguity');
  2095. if (results.length > 1) console.info('Tidal returns ambiguous results for "' + artist + '":', results);
  2096. if (!results[0].picture) return Promise.reject('Tidal: artist exists but no photo');
  2097. return 'https://resources.tidal.com/images/' + results[0].picture.replace(/-/g, '/') + '/750x750.jpg';
  2098. }),
  2099. // Discogs
  2100. globalXHR('https://api.discogs.com/database/search?' + new URLSearchParams({
  2101. title: artist,
  2102. type: 'artist',
  2103. sort: 'score',
  2104. sort_order: 'desc',
  2105. strict: false,
  2106. per_page: 100,
  2107. }).toString(), {
  2108. responseType: 'json',
  2109. headers: { 'Authorization': 'Discogs key="' + discogsKey + '", secret="' + discogsSecret + '"' },
  2110. }).then(({response}) => {
  2111. if (response.items <= 0) return Promise.reject('Discogs: no matches');
  2112. let results = resultsFilter(response.results.filter(result => result.type == 'artist'), result => result.title);
  2113. if (results.length <= 0) return Promise.reject('Discogs: no matches');
  2114. console.info('Discogs search results for "' + artist + '":', results);
  2115. //if (results.length > 1) return Promise.reject('Discogs: ambiguity');
  2116. if (results.length > 1) console.info('Discogs returns ambiguous results for "' + artist + '":', results);
  2117. return Promise.all(results.map(result => {
  2118. if (result.cover_image.includes('/spacer.gif')) return null;
  2119. return getDiscogsImageMax(result.cover_image);
  2120. }).filter(Boolean)).then(artistCovers => httpParser.test(artistCovers[0]) ?
  2121. artistCovers[0] : Promise.reject('Discogs: artist exists but no photo'));
  2122. }),
  2123. // Bandcamp
  2124. globalXHR('https://bandcamp.com/search?' + new URLSearchParams({
  2125. q: '"' + artist + '"',
  2126. item_type: 'b',
  2127. }).toString()).then(function({document}) {
  2128. const results = resultsFilter(Array.from(document.querySelectorAll('div.results > ul.result-items > li.searchresult')).map(function(li) {
  2129. try {
  2130. var result = JSON.parse(li.dataset.search);
  2131. if (result.type.toLowerCase() != 'b') return;
  2132. } catch(e) {
  2133. result = { }; // return;
  2134. console.warn('Bandcamp: could not detect search result type', li);
  2135. }
  2136. if (!result.id) try {
  2137. if (/\b(?:id)=(\d+)\b/.test(li.previousSibling.previousSibling.nodeValue))
  2138. result.id = parseInt(RegExp.$1);
  2139. } catch(e) { }
  2140. let ref = li.querySelector('div.art > img');
  2141. if (ref != null) result.imageUrl = ref.src.replace(/_\d+(?=\.\w+$)/, '_0');
  2142. if ((ref = li.querySelector('div.heading > a')) != null) {
  2143. result.url = new URL(ref);
  2144. result.url.search = '';
  2145. result.name = ref.textContent.trim();
  2146. }
  2147. if ((ref = li.querySelector('div.subhead')) != null) result.location = ref.textContent.trim();
  2148. if ((ref = li.querySelector('div.genre')) != null)
  2149. result.genre = ref.textContent.trim().replace(/^(?:Genre:\s+)/i, '');
  2150. if ((ref = li.querySelector('div.tags')) != null)
  2151. result.tags = ref.textContent.trim().replace(/^(?:tags):\s+/, '').split(/\s*,\s*/);
  2152. if (result.name) return result;
  2153. }).filter(Boolean), result => result.name);
  2154. if (results.length <= 0) return Promise.reject('Bandcamp: no matches');
  2155. console.info('Bandcamp search results for "' + artist + '":', results);
  2156. if (results.length > 1) return Promise.reject('Bandcamp: ambiguity');
  2157. if (results.length > 1) console.info('Bandcamp returns ambiguous results for "' + artist + '":', results);
  2158. return httpParser.test(results[0].imageUrl) ? results[0].imageUrl
  2159. : Promise.reject('Bandcamp: artist exists but no photo');
  2160. }),
  2161. // Beatport
  2162. (function setAccessToken() {
  2163. const isTokenValid = accessToken => accessToken && accessToken.token_type
  2164. && accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000;
  2165. return bpAccessToken instanceof Promise ? bpAccessToken.then(accessToken =>
  2166. isTokenValid(accessToken) ? accessToken : Promise.reject('expired or otherwise invalid')).catch(function(reason) {
  2167. bpAccessToken = null;
  2168. console.info('Discarding Beatsource access token:', reason);
  2169. return setAccessToken();
  2170. }) : (bpAccessToken = (function() {
  2171. if ('beatportAccessToken' in localStorage) try {
  2172. const accessToken = JSON.parse(localStorage.getItem('beatportAccessToken'));
  2173. if (!isTokenValid(accessToken)) throw 'Expired or otherwise invalid';
  2174. console.info('Re-using cached Beatport access token:', accessToken,
  2175. 'expires at', new Date(accessToken.expires_at).toTimeString(),
  2176. '(+' + ((accessToken.expires_at - Date.now()) / 1000 / 60).toFixed(2) + 'm)');
  2177. return Promise.resolve(accessToken);
  2178. } catch(e) { localStorage.removeItem('beatportAccessToken') }
  2179. const timeStamp = Date.now(), urlBase = 'https://www.beatport.com/api/auth';
  2180. return globalXHR(urlBase + '/session', { responseType: 'json' }).then(function(response) {
  2181. const getCookie = (responseHeaders, cookie) =>
  2182. (cookie = new RegExp(`^(?:set-cookie):\\s*${cookie}=(.+)$`, 'im')
  2183. .exec(responseHeaders)) && cookie[1].split(';').map(val => val.trim());
  2184. let cookie = getCookie(response.responseHeaders, '__Secure-next-auth\\.session-token');
  2185. if (cookie != null) return response.response;
  2186. const postData = { };
  2187. if ((cookie = getCookie(response.responseHeaders, '__Host-next-auth\\.csrf-token')) != null)
  2188. postData.csrfToken = cookie[0].split('|')[0];
  2189. else return Promise.reject('Cookie not received');
  2190. if ((cookie = getCookie(response.responseHeaders, '__Secure-next-auth\\.callback-url')) != null)
  2191. postData.callbackUrl = cookie[0];
  2192. else return Promise.reject('Cookie not received');
  2193. return globalXHR(urlBase + '/callback/anonymous', {
  2194. data: new URLSearchParams(Object.assign(postData, { json: true })),
  2195. }).then(({responseHeaders}) =>
  2196. (cookie = getCookie(responseHeaders, '__Secure-next-auth\\.session-token')) != null ?
  2197. cookie[0] : Promise.reject('Cookie not received'))
  2198. .then(token => globalXHR(urlBase + '/session', {
  2199. responseType: 'json',
  2200. cookie: '__Secure-next-auth.session-token=' + token,
  2201. })).then(({response}) => response);
  2202. }).then(function({token}) {
  2203. if (!(token = {
  2204. token_type: token.tokenType,
  2205. access_token: token.accessToken,
  2206. timestamp: timeStamp,
  2207. expires_in: token.expiresIn,
  2208. expires_at: token.accessTokenExpires,
  2209. }).expires_at) token.expires_at = token.timestamp + (token.expires_in_ms || token.expires_in * 1000);
  2210. if (!isTokenValid(token)) {
  2211. console.warn('Received invalid Beatport token:', token);
  2212. return Promise.reject('invalid token received');
  2213. }
  2214. try { localStorage.setItem('beatportAccessToken', JSON.stringify(token)) } catch(e) { console.warn(e) }
  2215. console.log('Beatport access token successfully set:',
  2216. token, `(+${(Date.now() - token.timestamp) / 1000}s)`);
  2217. return token;
  2218. });
  2219. })().catch(function() {
  2220. const isTokenValid = accessToken => accessToken && accessToken.token_type
  2221. && accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000;
  2222. if ('beatsourceAccessToken' in localStorage) try {
  2223. var accessToken = JSON.parse(localStorage.getItem('beatsourceAccessToken'));
  2224. if (!isTokenValid(accessToken)) throw 'Expired or otherwise invalid';
  2225. console.info('Re-using cached Beatsource access token:', accessToken,
  2226. 'expires at', new Date(accessToken.expires_at).toTimeString(),
  2227. '(+' + ((accessToken.expires_at - Date.now()) / 1000 / 60).toFixed(2) + 'm)');
  2228. return Promise.resolve(accessToken);
  2229. } catch(e) {
  2230. //console.warn('Invalid BeatSource cached access token:', e, localStorage.beatsourceAccessToken);
  2231. localStorage.removeItem('beatsourceAccessToken');
  2232. }
  2233. const root = 'https://www.beatsource.com/', timeStamp = Date.now();
  2234. return globalXHR(root).then(function(response) {
  2235. let accessToken = response.document.getElementById('__NEXT_DATA__');
  2236. if (accessToken != null) try {
  2237. accessToken = JSON.parse(accessToken.text);
  2238. return Object.assign(accessToken.props.rootStore.authStore.user, {
  2239. apiHost: accessToken.runtimeConfig.API_HOST,
  2240. clientId: accessToken.runtimeConfig.API_CLIENT_ID,
  2241. recurlyPublicKey: accessToken.runtimeConfig.RECURLY_PUBLIC_KEY,
  2242. });
  2243. } catch(e) { console.warn(e) }
  2244. if ((accessToken = /\b(?:btsrcSession)=([^\s\;]+)/m.exec(response.responseHeaders)) != null) try {
  2245. accessToken = JSON.parse(decodeURIComponent(accessToken[1]));
  2246. let sessionId = /\b(?:sessionId)=([^\s\;]+)/m.exec(response.responseHeaders);
  2247. if (sessionId != null) try { accessToken.sessionId = decodeURIComponent(sessionId[1]) }
  2248. catch(e) { console.warn(e) }
  2249. return accessToken;
  2250. } catch(e) { console.warn(e) }
  2251. return Promise.reject('Beatsource OAuth2 access token could not be extracted');
  2252. }).then(function(accessToken) {
  2253. if (!accessToken.timestamp) accessToken.timestamp = timeStamp;
  2254. if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp +
  2255. (accessToken.expires_in_ms || accessToken.expires_in * 1000);
  2256. if (!isTokenValid(accessToken)) {
  2257. console.warn('Received invalid Beatsource token:', accessToken);
  2258. return Promise.reject('invalid token received');
  2259. }
  2260. try { localStorage.setItem('beatsourceAccessToken', JSON.stringify(accessToken)) } catch(e) { console.warn(e) }
  2261. console.log('Beatsource access token successfully set:',
  2262. accessToken, `(+${(Date.now() - accessToken.timestamp) / 1000}s)`);
  2263. return accessToken;
  2264. });
  2265. }));
  2266. })().then(function(accessToken) {
  2267. const url = new URL('v4/catalog/search', 'https://api.beatport.com');
  2268. url.searchParams.set('q', '"' + artist + '"');
  2269. url.searchParams.set('type', 'artists');
  2270. url.searchParams.set('per_page', 30);
  2271. //url.searchParams.set('order_by', '-release_date');
  2272. return globalXHR(url, {
  2273. responseType: 'json',
  2274. headers: { Authorization: accessToken.token_type + ' ' + accessToken.access_token },
  2275. }).then(({response}) => response.artists);
  2276. }).then(function(results) {
  2277. if (results.length <= 0) return Promise.reject('Beatport: no matches');
  2278. console.info('Beatport search results for "' + artist + '":', results);
  2279. if (results.length > 1) return Promise.reject('Beatport: ambiguity');
  2280. if (results.length > 1) console.info('Beatport returns ambiguous results for "' + artist + '":', results);
  2281. if (!(results = results[0]).image) return Promise.reject('Beatport: no image for matched artist');
  2282. return verifyImageUrl(results.image.dynamic_uri ?
  2283. results.image.dynamic_uri.replace('/image_size/{w}x{h}/', '/')
  2284. : results.image.uri.replace(/\/image_size\/\d+x\d+\//, '/'));
  2285. }),
  2286. // SoundCloud
  2287. ('scClientId' in sessionStorage ? Promise.resolve(sessionStorage.scClientId) : globalXHR('https://soundcloud.com/').then(function({document}) {
  2288. const script = document.body.querySelector(':scope > script[crossorigin]:last-of-type');
  2289. if (script == null) return Promise.reject('SoundCloud: unexpected page structure');
  2290. return globalXHR(script.src, { responseType: 'text', accept: 'application/javascript' });
  2291. }).then(function({responseText}) {
  2292. let clientId = /\b(?:client_id)\s*:\s*"(\S{32})"/.exec(responseText);
  2293. if (clientId == null) return Promise.reject('SoundCloud: client_id was not captured');
  2294. sessionStorage.scClientId = (clientId = clientId[1]);
  2295. return clientId;
  2296. })).then(clientId => globalXHR('https://api-v2.soundcloud.com/search/users?' + new URLSearchParams({
  2297. q: artist,
  2298. limit: 30,
  2299. client_id: clientId,
  2300. app_locale: 'en',
  2301. }).toString(), { responseType: 'json' }).then(function({response}) {
  2302. if (response.total_results <= 0) return Promise.reject('SoundCloud: no matches');
  2303. let results = resultsFilter(response.collection.filter(result => result.kind == 'user'), result => result.username);
  2304. if (results.length <= 0) return Promise.reject('SoundCloud: no matches');
  2305. console.info('SoundCloud search results for "' + artist + '":', results);
  2306. //if (results.length > 1) return Promise.reject('SoundCloud: ambiguity');
  2307. if (results.length > 1) console.info('SoundCloud returns ambiguous results for "' + artist + '":', results);
  2308. return results[0].avatar_url && ![
  2309. '/images/default_avatar_large.png',
  2310. '/images/default_avatar_original.png',
  2311. '/avatars-000185010230-yq6cu2-original.jpg',
  2312. '/avatars-000185010230-yq6cu2-large.jpg',
  2313. ].some(pattern => results[0].avatar_url.endsWith(pattern)) ?
  2314. results[0].avatar_url.replace(/-\w+(?=\.\w+$)/, '-original') : Promise.reject('SoundCloud: artist found but no image');
  2315. })),
  2316. // Last.fm
  2317. globalXHR('http://ws.audioscrobbler.com/2.0/?' + new URLSearchParams({
  2318. method: 'artist.getinfo',
  2319. artist: artist,
  2320. format: 'json',
  2321. api_key: lfmApiKey,
  2322. }).toString(), { responseType: 'json' }).then(function({response}) {
  2323. if (response.error) return Promise.reject(response.message);
  2324. console.info('Last.fm search result for "' + artist + '":', response.artist);
  2325. const rx = /\/(\d+)x(\d+)\//;
  2326. let biggest = response.artist.image.map(im => im['#text']).reduce(function(a, b) {
  2327. let r = [a, b].map(RegExp.prototype.exec.bind(rx))
  2328. .map(r => r != null ? parseInt(r[1]) * parseInt(r[2]) : -Infinity);
  2329. return r[1] > r[0] ? b : a;
  2330. });
  2331. return rx.test(biggest) && !biggest.endsWith('/2a96cbd8b46e442fc41c2b86b821562f.png') ?
  2332. biggest : Promise.reject('Last.fm: artist exists but no photo');
  2333. }),
  2334. // Spotify
  2335. (function() {
  2336. const isTokenValid = accessToken => typeof accessToken == 'object' && accessToken.token_type
  2337. && accessToken.access_token && accessToken.expires_at >= Date.now() + 30 * 1000;
  2338. try {
  2339. var accessToken = JSON.parse(window.localStorage.spotifyAccessToken);
  2340. if (isTokenValid(accessToken)) return Promise.resolve(accessToken);
  2341. } catch(e) { }
  2342. const clientId = GM_getValue('spotify_clientid', '54468e0c92c24e0d86c61346155b32cd'),
  2343. clientSecret = GM_getValue('spotify_clientsecret', '38cb34c7196d4cdca6dbb35b08e29e12');
  2344. if (!clientId || !clientSecret) return Promise.reject('Spotify credentials not configured');
  2345. const data = new URLSearchParams({
  2346. 'grant_type': 'client_credentials',
  2347. }), timeStamp = Date.now();
  2348. return globalXHR('https://accounts.spotify.com/api/token', { responseType: 'json', headers: {
  2349. Authorization: 'Basic ' + btoa(clientId + ':' + clientSecret),
  2350. } }, data).then(function({response}) {
  2351. accessToken = response;
  2352. const tzOffset = new Date().getTimezoneOffset() * 60 * 1000;
  2353. if (!accessToken.timestamp) accessToken.timestamp = timeStamp;
  2354. if (!accessToken.expires_at) accessToken.expires_at = accessToken.timestamp +
  2355. (accessToken.expires_in_ms || accessToken.expires_in * 1000);
  2356. if (!isTokenValid(accessToken)) {
  2357. console.warn('Received invalid Spotify token:', accessToken);
  2358. return Promise.reject('invalid token received');
  2359. }
  2360. window.localStorage.spotifyAccessToken = JSON.stringify(accessToken);
  2361. return accessToken;
  2362. });
  2363. })().then(credentials => globalXHR('https://api.spotify.com/v1/search?' + new URLSearchParams({
  2364. q: artist,
  2365. type: 'artist',
  2366. }).toString(), {
  2367. responseType: 'json',
  2368. headers: {
  2369. Accept: 'application/json',
  2370. Authorization: credentials.token_type + ' ' + credentials.access_token,
  2371. },
  2372. })).then(function({response}) {
  2373. if (response.artists.total <= 0) return Promise.reject('Spotify: no matches');
  2374. let results = resultsFilter(response.artists.items.filter(item => item.type == 'artist'), item => item.name);
  2375. if (results.length <= 0) return Promise.reject('Spotify: no matches');
  2376. console.info('Spotify search results for "' + artist + '":', results);
  2377. if (results.length > 1) return Promise.reject('Spotify: ambiguity');
  2378. if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results);
  2379. return results[0].images && results[0].images.length > 0 ?
  2380. results[0].images.sort((a, b) => (b.width * b.height) - (a.width * a.height))[0].url
  2381. : Promise.reject('Spotify: artist exists but no photo');
  2382. }),
  2383. // Mixcloud
  2384. mixcloudQuery(`
  2385. query SearchResultsQuery(
  2386. $tagCount: Int
  2387. $term: String!
  2388. ) { viewer { search { searchQuery(term: $term) { showPurposeMessage tags(first: $tagCount) { ...SearchResultsTags_tags } } } ...SearchResultsCloudcasts_viewer_4hh6ED ...SearchResultsUsers_viewer_4hh6ED } }
  2389. fragment AudioCardDetails_cloudcast on Cloudcast { ...AudioCardTags_cloudcast }
  2390. fragment AudioCardTags_cloudcast on Cloudcast { tags(country: "GLOBAL") { tag { id } } }
  2391. fragment Hovercard_user on User { id }
  2392. fragment PlayButton_cloudcast on Cloudcast { owner { id } }
  2393. fragment RebrandFollowButton_user on User { id }
  2394. fragment RebrandFollowButton_viewer on Viewer { me { id } }
  2395. fragment RebrandUserCard_user on User { id displayName username picture { urlRoot } ...Hovercard_user ...RebrandFollowButton_user }
  2396. fragment RebrandUserCard_viewer on Viewer { id ...RebrandFollowButton_viewer }
  2397. fragment SearchAudioCard_cloudcast on Cloudcast { owner { id } ...AudioCardDetails_cloudcast ...PlayButton_cloudcast }
  2398. fragment SearchResultsCloudcasts_viewer_4hh6ED on Viewer { search { searchQuery(term: $term) { cloudcasts(first: 10) { edges { node { slug ...SearchAudioCard_cloudcast id __typename } } } } } }
  2399. fragment SearchResultsTags_tags on TagConnection { edges { node { name slug id } } }
  2400. fragment SearchResultsUsers_viewer_4hh6ED on Viewer { search { searchQuery(term: $term) { users(first: 20) { edges { node { ...RebrandUserCard_user id __typename } } } } } ...RebrandUserCard_viewer }
  2401. `, {
  2402. tagCount: 10,
  2403. term: artist,
  2404. }).then(function(data) {
  2405. data = resultsFilter(data.viewer.search.searchQuery.users.edges.map(edge => edge.node),
  2406. node => node.displayName);
  2407. if (data.length <= 0) return Promise.reject('Mixcloud: no matches');
  2408. console.info('Mixcloud search results for "' + artist + '":', data);
  2409. //if (data.length > 1) return Promise.reject('Mixcloud: ambiguity');
  2410. if (data.length > 1) console.info('Mixcloud returns ambiguous results for "' + artist + '"');
  2411. return 'https://thumbnailer.mixcloud.com/unsafe/' + data[0].picture.urlRoot;
  2412. }),
  2413. // iTunes
  2414. globalXHR('https://itunes.apple.com/search?' + new URLSearchParams({
  2415. term: '"' + artist + '"',
  2416. media: 'music',
  2417. entity: 'musicArtist',
  2418. attribute: 'artistTerm',
  2419. //country: 'US',
  2420. }).toString(), { responseType: 'json' }).then(function({response}) {
  2421. if (response.resultCount <= 0) return Promise.reject('iTunes: no matches');
  2422. let results = resultsFilter(response.results.filter(result =>
  2423. result.wrapperType == 'artist' && result.artistType == 'Artist'), result => result.artistName);
  2424. if (results.length <= 0) return Promise.reject('iTunes: no matches');
  2425. console.info('iTunes search results for "' + artist + '":', results);
  2426. //if (results.length > 1) return Promise.reject('iTunes: ambiguity');
  2427. if (results.length > 1) console.info('iTunes returns ambiguous results for "' + artist + '":', results);
  2428. return imageUrlResolver(results[0].artistLinkUrl);
  2429. }),
  2430. // Deezer
  2431. globalXHR('https://api.deezer.com/search/artist?' + new URLSearchParams({
  2432. q: artist,
  2433. order: 'RANKING',
  2434. //strict: 'on',
  2435. }).toString(), { responseType: 'json' }).then(function({response}) {
  2436. if (response.total <= 0) return Promise.reject('Deezer: no matches');
  2437. let results = resultsFilter(response.data.filter(result => result.type == 'artist'),
  2438. result => result.name);
  2439. if (results.length <= 0) return Promise.reject('Deezer: no matches');
  2440. console.info('Deezer search results for "' + artist + '":', results);
  2441. //if (results.length > 1) return Promise.reject('Deezer: ambiguity');
  2442. if (results.length > 1) console.info('Deezer returns ambiguous results for "' + artist + '":', results);
  2443. return verifyImageUrl(results[0].picture).catch(function(reason) {
  2444. console.warn('Deezer API image retrieval failed:', reason);
  2445. return ['xl', 'big', 'medium', 'small'].reduce((acc, size) =>
  2446. acc || response.data[0]['picture_' + size], null) || Promise.reject('no picture');
  2447. }).then(imageUrl => imageUrl.includes('/images/artist//') ?
  2448. Promise.reject('Deezer: artist exists but no photo') : getDeezerImageMax(imageUrl));
  2449. }),
  2450. // FLO
  2451. globalXHR('https://www.music-flo.com/api/search/v2/search?' + new URLSearchParams({
  2452. keyword: '"' + artist + '"',
  2453. searchType: 'ARTIST',
  2454. sortType: 'ACCURACY',
  2455. size: 10,
  2456. }).toString(), { responseType: 'json' }).then(function({response}) {
  2457. if (response.code != 2000000) return Promise.reject(response.message);
  2458. //if (response.data.totalCount <= 0) return Promise.reject('FLO: no matches');
  2459. console.assert(Array.isArray(response.data.list), 'Array.isArray(response.data.list)', response);
  2460. if (response.data.list.length <= 0) return Promise.reject('FLO: no matches');
  2461. let results = resultsFilter(response.data.list[0].list, result => result.name);
  2462. if (results.length <= 0) return Promise.reject('FLO: no matches');
  2463. console.info('FLO search results for "' + artist + '":', response.data);
  2464. if (results.length > 1) return Promise.reject('FLO: ambiguity');
  2465. //if (results.length > 1) console.info('FLO returns ambiguous results for "' + artist + '":', results);
  2466. const noPhoto = Promise.reject('FLO: artist exists but no photo');
  2467. if (!Array.isArray(results[0].imgList) || results[0].imgList.length <= 0) return noPhoto;
  2468. const imageUrl = results[0].imgList.reduce((acc, image) => image.url.replace(/\?.*$/, ''));
  2469. return !imageUrl.includes('/000000000/000000000.') ? imageUrl : noPhoto;
  2470. }),
  2471. // OTOTOY
  2472. globalXHR('https://ototoy.jp/find/?q=' + encodeURIComponent('"' + artist + '"')).then(function({document}) {
  2473. const results = resultsFilter(Array.from(document.querySelectorAll('div.results_box > div.find-artist div.find-candidates')).map(function(div) {
  2474. let result = {
  2475. url: div.querySelector('div.artist-name > a'),
  2476. imageUrl: div.querySelector('figure > a > img'),
  2477. };
  2478. if (result.url != null) {
  2479. result.name = result.url.title || result.url.textContent.trim();
  2480. result.url = new URL(result.url);
  2481. result.url.hostname = 'ototoy.jp';
  2482. result.id = /\/a\/(\d+)\b/i.exec(result.url.pathname);
  2483. if (result.id != null) result.id = parseInt(result.id[1]); else delete result.id;
  2484. } else delete result.url;
  2485. if (result.imageUrl != null) {
  2486. result.imageUrl = new URL(result.imageUrl.src);
  2487. result.imageUrl = result.imageUrl.origin + new URLSearchParams(result.imageUrl.search).get('image');
  2488. } else delete result.imageUrl;
  2489. if (result.name) return result;
  2490. }).filter(Boolean), result => result.name);
  2491. if (results.length <= 0) return Promise.reject('OTOTOY: no matches');
  2492. console.info('OTOTOY search results for "' + artist + '":', results);
  2493. if (results.length > 1) return Promise.reject('OTOTOY: ambiguity');
  2494. //if (results.length > 1) console.info('OTOTOY returns ambiguous results for "' + artist + '":', results);
  2495. return httpParser.test(results[0].imageUrl) && !results[0].imageUrl.endsWith('/0dc61986-bccf-49d4-8fad-6b147ea8f327.jpg') ?
  2496. results[0].imageUrl : Promise.reject('OTOTOY: artist exists but no photo');
  2497. }),
  2498. // Recochoku
  2499. globalXHR('https://recochoku.jp/search/artist?q=' + encodeURIComponent(artist)).then(({document}) =>
  2500. Array.from(document.querySelectorAll('ul#artistContents > li > a')).map(function(a) {
  2501. let result = {
  2502. url: new URL(a.pathname, 'https://recochoku.jp'),
  2503. id: /\/artist\/(\d+)\b/i.exec(a.pathname),
  2504. name: a.querySelector('div > div[class$="title"]'),
  2505. imageUrl: a.getElementsByTagName('IMG'),
  2506. };
  2507. if (result.name) result.name = result.name.textContent.trim(); else return null;
  2508. if (result.imageUrl.length > 0) {
  2509. result.imageUrl = new URL(result.imageUrl[0].dataset.src);
  2510. result.imageUrl.searchParams.set('FFw', 999999999);
  2511. result.imageUrl.searchParams.set('FFh', 999999999);
  2512. result.imageUrl.searchParams.delete('h');
  2513. result.imageUrl.searchParams.delete('option');
  2514. } else return null;
  2515. if (result.id != null) result.id = result.id[1]; else delete result.id;
  2516. return result;
  2517. }).filter(Boolean)).then(function(results) {
  2518. if (results.length <= 0) return Promise.reject('Recochoku: no matches');
  2519. console.info('Recochoku search results for "' + artist + '":', results);
  2520. results = resultsFilter(results, result => result.name);
  2521. if (results.length <= 0) return Promise.reject('Recochoku: no matches');
  2522. if (results.length > 1) return Promise.reject('Recochoku: ambiguity');
  2523. //if (results.length > 1) console.info('Recochoku returns ambiguous results for "' + artist + '":', results);
  2524. return httpParser.test(results[0].imageUrl) && !results[0].imageUrl.endsWith('/noimage_artist.png') ?
  2525. results[0].imageUrl : Promise.reject('Recochoku: artist exists but no photo');
  2526. }),
  2527. // QQ音乐
  2528. // globalXHR('https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + new URLSearchParams({
  2529. // format: 'json',
  2530. // t: 9,
  2531. // w: artist,
  2532. // inCharset: 'utf8',
  2533. // outCharset: 'utf-8',
  2534. // }).toString(), { responseType: 'json' }).then(function({response}) {
  2535. // if (response.data.singer.totalnum <= 0) return Promise.reject('QQ音乐: no matches');
  2536. // console.info('QQ音乐 search results for "' + artist + '":', response.data.singer);
  2537. // const results = resultsFilter(response.data.singer.list, singer => singer.singerName);
  2538. // if (results.length <= 0) return Promise.reject('QQ音乐: no matches');
  2539. // //if (results.length > 1) return Promise.reject('QQ音乐: ambiguity');
  2540. // if (results.length > 1) console.info('QQ音乐 returns ambiguous results for "' + artist + '":', results);
  2541. // if (/M000003\w{11}_0(?=\.jpg$)/.test(results[0].singerPic))
  2542. // return Promise.reject('QQ音乐: artist exists but no real photo');
  2543. // return verifyImageUrl(results[0].singerPic.replace(/R\d+x\d+/, ''))
  2544. // .catch(reason => verifyImageUrl(results[0].singerPic))
  2545. // .catch(reason => Promise.reject('QQ音乐: artist exists but no photo'));
  2546. // }),
  2547. // YouTube Music
  2548. (function() {
  2549. if ('ytcfg' in sessionStorage) try { return Promise.resolve(JSON.parse(sessionStorage.ytcfg)) }
  2550. catch(e) { console.warn('Invalid ytcfg format:', e) }
  2551. return globalXHR('https://music.youtube.com/').then(function({document}) {
  2552. for (let script of document.querySelectorAll('head > script[nonce]')) {
  2553. let ytcfg = /^\s*\b(?:ytcfg\.set)\s*\(\s*(\{.+\})\s*\);/m.exec(script.text);
  2554. if (ytcfg != null) try {
  2555. ytcfg = JSON.parse(ytcfg[1]);
  2556. if (ytcfg.INNERTUBE_API_KEY) {
  2557. sessionStorage.ytcfg = JSON.stringify(ytcfg);
  2558. return ytcfg;
  2559. } else console.warn('YouTube Music API key missing:', ytcfg);
  2560. } catch(e) { console.warn('Error parsing ytcfg:', ytcfg[1]) }
  2561. }
  2562. return Promise.reject('unable to extract YouTube config ot the config is invalid');
  2563. });
  2564. })().then(ytcfg => globalXHR('https://music.youtube.com/youtubei/v1/search?' + new URLSearchParams({
  2565. alt: 'json',
  2566. key: ytcfg.INNERTUBE_API_KEY,
  2567. }).toString(), {
  2568. responseType: 'json',
  2569. headers: { Referer: 'https://music.youtube.com/' },
  2570. }, {
  2571. query: artist,
  2572. params: encodeURIComponent('EgWKAQIgAWoKEAkQChADEAUQBA=='),
  2573. context: {
  2574. activePlayers: { }, capabilities: { },
  2575. client: Object.assign({
  2576. experimentIds: [ ], experimentsToken: "",
  2577. locationInfo: {
  2578. locationPermissionAuthorizationStatus: "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED",
  2579. },
  2580. musicAppInfo: {
  2581. musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE",
  2582. musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE",
  2583. pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN",
  2584. },
  2585. utcOffsetMinutes: -new Date().getTimezoneOffset(),
  2586. }, ytcfg.INNERTUBE_CONTEXT.client, { hl: 'en' }),
  2587. request: {
  2588. internalExperimentFlags: [
  2589. { key: "force_music_enable_outertube_search", value: "true" }
  2590. ],
  2591. },
  2592. user: { enableSafetyMode: false },
  2593. },
  2594. })).then(({response}) => response.contents && response.contents.sectionListRenderer ?
  2595. response.contents.sectionListRenderer.contents[0].musicShelfRenderer.contents.map(function(item) {
  2596. let result = {
  2597. id: item.musicResponsiveListItemRenderer.navigationEndpoint.browseEndpoint.browseId,
  2598. name: item.musicResponsiveListItemRenderer.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
  2599. photoUrl: item.musicResponsiveListItemRenderer.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails,
  2600. };
  2601. result.webUrl = result.id ? 'https://music.youtube.com/channel/' + result.id : undefined;
  2602. result.photoUrl = Array.isArray(result.photoUrl) && result.photoUrl.length > 0 ?
  2603. result.photoUrl[0].url.replace(/(?:=[swh]\d+.*)?$/, '=s0') : undefined;
  2604. return result;
  2605. }) : Promise.reject('YouTube Music: no matches')).then(function(results) {
  2606. if (results.length <= 0) return Promise.reject('YouTube Music: no matches');
  2607. results = resultsFilter(results, result => result.name);
  2608. if (results.length <= 0) return Promise.reject('YouTube Music: no matches');
  2609. console.info('YouTube Music search results for "' + artist + '":', results);
  2610. if (results.length > 1) return Promise.reject('YouTube Music: ambiguity');
  2611. if (results.length > 1) console.info('YouTube Music returns ambiguous results for "' + artist + '":', results);
  2612. return httpParser.test(results[0].photoUrl) ? results[0].photoUrl
  2613. : Promise.reject('YouTube Music: artist exists but no photo');
  2614. }),
  2615. ];
  2616.  
  2617. const lookUp = (index = 0) => index >= 0 && index < lookupWorkers.length ?
  2618. lookupWorkers[index].then(setImage.bind(imageInput)).catch(reason => lookUp(index + 1))
  2619. : Promise.reject('Image of this artist was not found');
  2620. lookUp().catch(logFail);
  2621. }
  2622.  
  2623. if (GM_getValue('auto_lookup_artist_image', true)) verifyImageUrl(imageInput.value).catch(function(reason) {
  2624. if (imageInput.value.length > 0) imageInput.value = '';
  2625. lookupArtistImage();
  2626. });
  2627. // <Alt+click> to lookup for image on demand
  2628. imageInput.addEventListener('click', function(evt) {
  2629. if (!evt.altKey) return;
  2630. lookupArtistImage();
  2631. return false;
  2632. });
  2633. const menu = new ContextMenu();
  2634. menu.addItem('Auto lookup artist image', lookupArtistImage);
  2635. menu.attach(imageInput);
  2636. GM_registerMenuCommand('Artist image auto lookup', lookupArtistImage, 'l');
  2637. } else {
  2638. const image = document.body.querySelector('div#lightbox > img');
  2639. if (image == null) break; // assertion failed!
  2640. image.ondragover = evt => false;
  2641. // image.ondragenter = image[`ondrag${'ondragexit' in image ? 'exit' : 'leave'}`] = function(evt) {
  2642. // if (evt.relatedTarget == evt.currentTarget) return false;
  2643. // evt.currentTarget.parentNode.style.backgroundColor = evt.type == 'dragenter' ? '#7fff0040' : null;
  2644. // };
  2645. // image.ondrop = function(evt) {
  2646. // function dataSendHandler(endPoint) {
  2647. // image.style.opacity = 0.3;
  2648. // endPoint([items[0]]).then(singleImageGetter).then(imageUrl => localXHR('/artist.php',
  2649. // { responseType: null }, new URLSearchParams({
  2650. // image: imageUrl,
  2651. // //body: ???,
  2652. // summary: 'Cover update',
  2653. // })).then(function(response) {
  2654. // console.log(response);
  2655. // image.src = imageUrl;
  2656. // image.style.opacity = 1;
  2657. // return imageUrl
  2658. // })).catch(function(reason) {
  2659. // logFail(reason);
  2660. // image.style.opacity = 1;
  2661. // });
  2662. // }
  2663.  
  2664. // evt.stopPropagation();
  2665. // let items = evt.dataTransfer.getData('text/uri-list');
  2666. // if (items) items = items.split(/\r?\n/); else {
  2667. // items = evt.dataTransfer.getData('text/x-moz-url');
  2668. // if (items) items = items.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  2669. // else if (items = evt.dataTransfer.getData('text/plain'))
  2670. // items = items.split(/\r?\n/).filter(RegExp.prototype.test.bind(httpParser));
  2671. // }
  2672. // if (Array.isArray(items) && items.length > 0) {
  2673. // if (confirm('Update artist image from the dropped URL?\n\n' + items[0]))
  2674. // dataSendHandler(imageHosts.rehostImages);
  2675. // } else if (evt.dataTransfer.files.length > 0) {
  2676. // items = Array.from(evt.dataTransfer.files)
  2677. // .filter(file => file instanceof File && file.type.startsWith('image/'));
  2678. // if (items.length > 0 && confirm('Update artist image from the dropped file?'))
  2679. // dataSendHandler(imageHosts.uploadImages);
  2680. // }
  2681. // evt.currentTarget.parentNode.style.backgroundColor = null;
  2682. // return false;
  2683. // };
  2684. }
  2685. break;
  2686. }
  2687. break;
  2688. case 'tracker.czech-server.com':
  2689. if (document.location.pathname == '/upload2.php')
  2690. document.querySelectorAll('input[type="text"][name="urlobr"]').forEach(setInputHandlers);
  2691. break;
  2692. }