Imgur Proxy

Intercepts Imgur image requests on any site and re-routes them through proxies to bypass geoblocks.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

Advertisement:

// ==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();
    }
})();