RedGifs Improver

Adds buttons for direct links and downloading to the sidebar.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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