Greasy Fork is available in English.
Intercepts Imgur image requests on any site and re-routes them through proxies to bypass geoblocks.
// ==UserScript==
// @name Imgur Proxy
// @namespace imgur-proxy
// @version 0.5
// @description Intercepts Imgur image requests on any site and re-routes them through proxies to bypass geoblocks.
// @author Sharknado
// @match *://*/*
// @grant GM_xmlhttpRequest
// @license MIT
// @connect i.imgur.com
// @connect imgur.com
// @connect images.weserv.nl
// @connect wsrv.nl
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// -------------------------------------------------------------------------
// Proxy fetch (userscript world — has GM_xmlhttpRequest)
// -------------------------------------------------------------------------
const IMGUR_RE = /^https?:\/\/(?:i\.)?imgur\.com\//;
// Tried in order; first success wins. Direct is cheapest but may be geoblocked.
const PROXY_STRATEGIES = [
(url) => url,
(url) => `https://images.weserv.nl/?url=${encodeURIComponent(url)}`,
(url) => `https://wsrv.nl/?url=${encodeURIComponent(url)}`,
];
function fetchViaStrategy(strategy, originalUrl) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: strategy(originalUrl),
responseType: 'blob',
timeout: 10000,
onload: (res) => {
if (res.status === 200 && res.response && res.response.size > 0) {
resolve(URL.createObjectURL(res.response));
} else {
reject(new Error(`HTTP ${res.status}`));
}
},
onerror: () => reject(new Error('network error')),
ontimeout: () => reject(new Error('timeout')),
});
});
}
// Maps original imgur URL → Promise<blobUrl> so the same image always
// resolves to the same blob URL and is never fetched more than once per session.
const blobCache = new Map();
function fetchAsBlob(originalUrl) {
if (blobCache.has(originalUrl)) return blobCache.get(originalUrl);
const promise = (async () => {
let lastErr;
for (const strategy of PROXY_STRATEGIES) {
try {
return await fetchViaStrategy(strategy, originalUrl);
} catch (e) {
lastErr = e;
console.debug(`[ImgurProxy] strategy failed for ${originalUrl}:`, e.message);
}
}
throw lastErr;
})();
blobCache.set(originalUrl, promise);
promise.catch(() => blobCache.delete(originalUrl)); // allow retry on total failure
return promise;
}
// Listen for fetch requests from the injected page-world script, proxy them,
// then return the blob URL via a response event.
window.addEventListener('__imgurproxy_req__', async (e) => {
const { id, url } = JSON.parse(e.detail);
try {
const blobUrl = await fetchAsBlob(url);
window.dispatchEvent(new CustomEvent('__imgurproxy_res__', {
detail: JSON.stringify({ id, blobUrl }),
}));
} catch {
console.warn('[ImgurProxy] all strategies failed for', url);
window.dispatchEvent(new CustomEvent('__imgurproxy_res__', {
detail: JSON.stringify({ id, blobUrl: null }),
}));
}
});
// -------------------------------------------------------------------------
// Page-world injection
// Runs in the same JS world as the site's bundle, so its prototype override
// actually intercepts new Image() / img.src assignments made by the page.
// Cannot use GM APIs — communicates back via CustomEvents.
// -------------------------------------------------------------------------
const PAGE_SCRIPT = `(function () {
const IMGUR_RE = /^https?:\\/\\/(?:i\\.)?imgur\\.com\\//;
const _srcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
const _pending = new Map();
let _id = 0;
Object.defineProperty(HTMLImageElement.prototype, 'src', {
get() { return _srcDesc.get.call(this); },
set(value) {
if (IMGUR_RE.test(value)) {
const id = ++_id;
_pending.set(id, this);
window.dispatchEvent(new CustomEvent('__imgurproxy_req__', {
detail: JSON.stringify({ id, url: value }),
}));
// Don't call original setter — prevents the browser ever requesting imgur
} else {
_srcDesc.set.call(this, value);
}
},
configurable: true,
});
window.addEventListener('__imgurproxy_res__', function (e) {
const { id, blobUrl } = JSON.parse(e.detail);
const img = _pending.get(id);
if (!img) return;
_pending.delete(id);
if (blobUrl) {
_srcDesc.set.call(img, blobUrl);
}
// blobUrl null = all strategies failed; img stays srcless rather than hitting imgur
});
})();`;
const script = document.createElement('script');
script.textContent = PAGE_SCRIPT;
(document.head || document.documentElement).appendChild(script);
script.remove();
// -------------------------------------------------------------------------
// MutationObserver — secondary coverage for setAttribute('src') and srcset
// (setAttribute bypasses the prototype setter, but fires a DOM mutation)
// -------------------------------------------------------------------------
async function patchSrcset(srcset) {
const parts = srcset.split(',').map(s => s.trim()).filter(Boolean);
const patched = await Promise.all(parts.map(async (part) => {
const spaceIdx = part.search(/\s/);
const url = spaceIdx === -1 ? part : part.slice(0, spaceIdx);
const descriptor = spaceIdx === -1 ? '' : part.slice(spaceIdx);
if (!IMGUR_RE.test(url)) return part;
try { return (await fetchAsBlob(url)) + descriptor; } catch { return part; }
}));
return patched.join(', ');
}
async function patchImg(img) {
const src = img.getAttribute('src') || '';
if (IMGUR_RE.test(src) && img.dataset.igProxied !== src) {
img.dataset.igProxied = src;
try { img.src = await fetchAsBlob(src); }
catch (e) { console.warn('[ImgurProxy] srcset all strategies failed', e.message); }
}
const srcset = img.getAttribute('srcset') || '';
if (srcset && img.dataset.igProxiedSrcset !== srcset && IMGUR_RE.test(srcset)) {
img.dataset.igProxiedSrcset = srcset;
try { img.srcset = await patchSrcset(srcset); }
catch (e) { console.warn('[ImgurProxy] srcset failed:', e.message); }
}
}
async function patchSource(source) {
const srcset = source.getAttribute('srcset') || '';
if (srcset && source.dataset.igProxied !== srcset && IMGUR_RE.test(srcset)) {
source.dataset.igProxied = srcset;
try { source.srcset = await patchSrcset(srcset); }
catch (e) { console.warn('[ImgurProxy] source srcset failed:', e.message); }
}
}
function scan(root) {
root.querySelectorAll('img').forEach(patchImg);
root.querySelectorAll('source').forEach(patchSource);
}
function onNode(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.tagName === 'IMG') patchImg(node);
else if (node.tagName === 'SOURCE') patchSource(node);
else scan(node);
}
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') m.addedNodes.forEach(onNode);
else if (m.type === 'attributes') {
const el = m.target;
if (el.tagName === 'IMG') patchImg(el);
else if (el.tagName === 'SOURCE') patchSource(el);
}
}
});
function start() {
observer.observe(document.documentElement, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['src', 'srcset'],
});
scan(document);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();