RedGifs Improver

Adds buttons for direct links and downloading to the sidebar.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         RedGifs Improver
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Adds buttons for direct links and downloading to the sidebar.
// @license MIT
// @match        https://www.redgifs.com/*
// @connect      media.redgifs.com
// @connect      v3.redgifs.com
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// ==/UserScript==


(function() {
    'use strict';

    // Global download queue for rate-limiting bulk downloads
    const downloadQueue = [];
    let isProcessingQueue = false;
    const DOWNLOAD_DELAY_MS = 5000;

    function processDownloadQueue() {
        if (isProcessingQueue) return;
        if (downloadQueue.length === 0) return;

        isProcessingQueue = true;
        const { mp4Url, filename } = downloadQueue.shift();

        triggerAutoDownload(mp4Url, filename, () => {
            setTimeout(() => {
                isProcessingQueue = false;
                processDownloadQueue();
            }, DOWNLOAD_DELAY_MS);
        });
    }

    function enqueueDownload(mp4Url, filename) {
        if (!downloadQueue.some(item => item.mp4Url === mp4Url)) {
            downloadQueue.push({ mp4Url, filename });
            processDownloadQueue();
        }
    }

    function allowRightClick(element) {
        if (!element) return;
        ['contextmenu', 'mousedown'].forEach(eventType => {
            element.addEventListener(eventType, function(e) {
                if (e.target.closest('.sideBar, .GifPreview-SideBarWrap, .custom-sidebar-item, .custom-tile-overlay-container')) {
                    return;
                }
                if (e.type === 'contextmenu' || e.button === 2) {
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                }
            }, true);
        });
    }

    function triggerAutoDownload(mp4Url, filename, onComplete) {
        if (typeof GM_download === 'function') {
            GM_download({
                url: mp4Url,
                name: filename,
                saveAs: false,
                onload: () => {
                    if (typeof onComplete === 'function') onComplete();
                },
                onerror: (err) => {
                    console.warn('GM_download failed, attempting blob fallback:', err);
                    fallbackBlobDownload(mp4Url, filename, onComplete);
                }
            });
        } else {
            fallbackBlobDownload(mp4Url, filename, onComplete);
        }
    }

    function fallbackBlobDownload(mp4Url, filename, onComplete) {
        if (typeof GM_xmlhttpRequest === 'function') {
            GM_xmlhttpRequest({
                method: 'GET',
                url: mp4Url,
                responseType: 'blob',
                onload: function(response) {
                    if (response.status === 200) {
                        const blob = response.response;
                        const blobUrl = URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = blobUrl;
                        a.download = filename;
                        a.style.display = 'none';
                        document.body.appendChild(a);
                        a.click();
                        setTimeout(() => {
                            document.body.removeChild(a);
                            URL.revokeObjectURL(blobUrl);
                            if (typeof onComplete === 'function') onComplete();
                        }, 1000);
                    } else {
                        if (typeof GM_openInTab === 'function') {
                            GM_openInTab(mp4Url, { active: true, insert: true });
                        }
                        if (typeof onComplete === 'function') onComplete();
                    }
                },
                onerror: () => {
                    if (typeof GM_openInTab === 'function') {
                        GM_openInTab(mp4Url, { active: true, insert: true });
                    }
                    if (typeof onComplete === 'function') onComplete();
                }
            });
        } else {
            if (typeof GM_openInTab === 'function') {
                GM_openInTab(mp4Url, { active: true, insert: true });
            }
            if (typeof onComplete === 'function') onComplete();
        }
    }

    function extractCdnUrl(tile) {
        const img = tile.querySelector('img.thumbnail, img[src*="redgifs.com"]');
        if (img && img.src) {
            const match = img.src.match(/\/([A-Za-z0-9]+)-(?:mobile|large|poster)/i);
            if (match && match[1]) {
                return `https://media.redgifs.com/${match[1]}.mp4`;
            }
        }

        let id = tile.getAttribute('data-feed-item-id') ||
                 tile.getAttribute('data-id') ||
                 tile.dataset?.feedItemId ||
                 tile.dataset?.id;

        if (id) {
            const formatted = id.charAt(0).toUpperCase() + id.slice(1);
            return `https://media.redgifs.com/${formatted}.mp4`;
        }

        return null;
    }

    function extractWatchId(tile) {
        let id = tile.getAttribute('data-feed-item-id') ||
                 tile.getAttribute('data-id') ||
                 tile.dataset?.feedItemId ||
                 tile.dataset?.id;
        if (id) return id;

        const img = tile.querySelector('img.thumbnail, img[src*="redgifs.com"]');
        if (img && img.src) {
            const match = img.src.match(/\/([A-Za-z0-9]+)-(?:mobile|large|poster)/i);
            if (match && match[1]) return match[1];
        }

        return null;
    }

    function getTileTimestamp(tile) {
        const img = tile.querySelector('img.thumbnail, img[alt*="uploaded by"]');
        if (!img || !img.alt) return 0;

        const match = img.alt.match(/(\d{2})\/(\d{2})\/(\d{4}),?\s*(\d{2}):(\d{2}):(\d{2})/);
        if (!match) return 0;

        const [, day, month, year, hours, minutes, seconds] = match;
        return new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`).getTime() || 0;
    }

    function sortTilesByNewest() {
        const tiles = Array.from(document.querySelectorAll('.tileItem, .GifPreview'));
        if (tiles.length < 2) return;

        const parent = tiles[0].parentElement;
        if (!parent) return;

        let needsSort = false;
        const sortedTiles = [...tiles].sort((a, b) => {
            const timeA = getTileTimestamp(a);
            const timeB = getTileTimestamp(b);
            return timeB - timeA;
        });

        for (let i = 0; i < tiles.length; i++) {
            if (tiles[i] !== sortedTiles[i]) {
                needsSort = true;
                break;
            }
        }

        if (needsSort) {
            sortedTiles.forEach(tile => parent.appendChild(tile));
        }
    }

    function processTile(tile) {
        const id = extractWatchId(tile);
        if (!id) return;

        const mp4Url = extractCdnUrl(tile);
        const watchUrl = `https://www.redgifs.com/watch/${id.toLowerCase()}`;

        allowRightClick(tile);

        let overlay = tile.querySelector('.custom-tile-overlay-container');

        if (tile.classList.contains('tileItem') && !tile.querySelector('.sideBar') && !tile.querySelector('.GifPreview-SideBarWrap')) {
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'custom-tile-overlay-container';
                overlay.style.cssText = 'position: absolute !important; top: 8px !important; right: 8px !important; z-index: 10 !important; display: flex !important; gap: 4px !important; pointer-events: auto !important;';

                if (getComputedStyle(tile).position === 'static') {
                    tile.style.position = 'relative';
                }
                tile.appendChild(overlay);
            } else {
                // Ensure existing container is explicitly positioned at top right
                overlay.style.cssText = 'position: absolute !important; top: 8px !important; right: 8px !important; z-index: 10 !important; display: flex !important; gap: 4px !important; pointer-events: auto !important;';
            }

            if (!overlay.querySelector('.custom-tile-link-btn')) {
                const linkAnchor = document.createElement('a');
                linkAnchor.href = watchUrl;
                linkAnchor.className = 'custom-tile-btn custom-tile-link-btn';
                linkAnchor.title = `Direct link to watch/${id}`;
                linkAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(235, 250, 99, 0.2) !important;
                    border: 1px solid #EBFA63 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                linkAnchor.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M12.083 4.16666H12.5C15.7217 4.16666 18.333 6.77801 18.333 9.99999C18.333 13.222 15.7217 15.8333 12.5 15.8333H12.083M7.91634 4.16666H7.49967C4.27801 4.16666 1.66634 6.77801 1.66634 9.99999C1.66634 13.222 4.27801 15.8333 7.49967 15.8333H7.91634M6.66634 9.99999H13.333" stroke="#EBFA63" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
                    </svg>
                `;
                linkAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    if (typeof GM_openInTab === 'function') {
                        GM_openInTab(watchUrl, { active: true, insert: true });
                    } else {
                        window.location.href = watchUrl;
                    }
                }, true);
                overlay.appendChild(linkAnchor);
            }

            if (mp4Url && !overlay.querySelector('.custom-tile-dl-btn')) {
                const dlAnchor = document.createElement('a');
                dlAnchor.href = mp4Url;
                dlAnchor.className = 'custom-tile-btn custom-tile-dl-btn';
                dlAnchor.title = `Queue download for ${id}.mp4`;
                dlAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(99, 250, 148, 0.2) !important;
                    border: 1px solid #63FA94 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                dlAnchor.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10 3.33334V13.3333M10 13.3333L14.1667 9.16667M10 13.3333L5.83334 9.16667M3.33334 16.6667H16.6667" stroke="#63FA94" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
                    </svg>
                `;
                dlAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    enqueueDownload(mp4Url, `${id}.mp4`);
                }, true);
                overlay.appendChild(dlAnchor);
            }
        } else {
            let sidebar = tile.querySelector('.sideBar, .GifPreview-SideBarWrap ul');
            if (!sidebar) return;

            if (!sidebar.querySelector('.custom-sidebar-link-item')) {
                const linkLi = document.createElement('li');
                linkLi.className = 'sideBarItem custom-sidebar-item custom-sidebar-link-item';
                linkLi.style.cssText = 'margin-bottom: 4px !important;';

                const linkAnchor = document.createElement('a');
                linkAnchor.href = watchUrl;
                linkAnchor.title = `Direct link to watch/${id}`;
                linkAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(235, 250, 99, 0.2) !important;
                    border: 1px solid #EBFA63 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                linkAnchor.innerHTML = `
                    <svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M12.083 4.16666H12.5C15.7217 4.16666 18.333 6.77801 18.333 9.99999C18.333 13.222 15.7217 15.8333 12.5 15.8333H12.083M7.91634 4.16666H7.49967C4.27801 4.16666 1.66634 6.77801 1.66634 9.99999C1.66634 13.222 4.27801 15.8333 7.49967 15.8333H7.91634M6.66634 9.99999H13.333" stroke="#EBFA63" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                `;

                linkAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    if (typeof GM_openInTab === 'function') {
                        GM_openInTab(watchUrl, { active: true, insert: true });
                    } else {
                        window.location.href = watchUrl;
                    }
                }, true);

                linkLi.appendChild(linkAnchor);
                sidebar.prepend(linkLi);
            }

            if (mp4Url && !sidebar.querySelector('.custom-sidebar-download-item')) {
                const dlLi = document.createElement('li');
                dlLi.className = 'sideBarItem custom-sidebar-item custom-sidebar-download-item';
                dlLi.style.cssText = 'margin-bottom: 4px !important;';

                const dlAnchor = document.createElement('a');
                dlAnchor.href = mp4Url;
                dlAnchor.title = `Queue download for ${id}.mp4`;
                dlAnchor.style.cssText = `
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    width: 32px !important;
                    height: 32px !important;
                    background: rgba(99, 250, 148, 0.2) !important;
                    border: 1px solid #63FA94 !important;
                    border-radius: 50% !important;
                    cursor: pointer !important;
                    box-sizing: border-box !important;
                `;
                dlAnchor.innerHTML = `
                    <svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M10 3.33334V13.3333M10 13.3333L14.1667 9.16667M10 13.3333L5.83334 9.16667M3.33334 16.6667H16.6667" stroke="#63FA94" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                `;

                dlAnchor.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    enqueueDownload(mp4Url, `${id}.mp4`);
                }, true);

                dlLi.appendChild(dlAnchor);
                sidebar.prepend(dlLi);
            }
        }

        const mediaElements = tile.querySelectorAll('video, img, .Player-Video, .thumbnail');
        mediaElements.forEach(el => allowRightClick(el));
    }

    function scanAll() {
        const targets = document.querySelectorAll('.tileItem, .GifPreview, [class*="GifPreview"]');
        targets.forEach(tile => processTile(tile));

        sortTilesByNewest();
    }

    setInterval(scanAll, 500);
})();