Sleazy Fork is available in English.

RedGIFs Download Button

Adds polished download buttons on RedGIFs watch pages and batch buttons on user profiles. Loading indicator, custom filename, dark mode, accessibility included.

// ==UserScript==
// @name         RedGIFs Download Button
// @namespace    http://tampermonkey.net/
// @version      6.0
// @description  Adds polished download buttons on RedGIFs watch pages and batch buttons on user profiles. Loading indicator, custom filename, dark mode, accessibility included.
// @author       Slagger122
// @match        https://www.redgifs.com/watch/*
// @match        https://www.redgifs.com/users/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // Icons (SVG alternatives recommended later)
    const ICON_SOUND_ON = '🔊';
    const ICON_SOUND_OFF = '🔇';
    const ICON_DOWNLOAD = '⬇';

    // Utility: Create styled button element
    function createButton(text, title, id, disabled = false) {
        const btn = document.createElement('button');
        btn.id = id;
        btn.type = 'button';
        btn.disabled = disabled;
        btn.setAttribute('aria-label', title);
        btn.title = title;
        btn.tabIndex = 0;
        btn.style.cssText = `
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 8px 14px;
            background: var(--btn-bg, #e50914);
            color: var(--btn-color, white);
            font-weight: 600;
            border: none;
            border-radius: 8px;
            font-family: Arial, sans-serif;
            font-size: 14px;
            cursor: ${disabled ? 'not-allowed' : 'pointer'};
            user-select: none;
            box-shadow: 0 0 8px rgba(0,0,0,0.3);
            transition: background 0.3s ease;
            outline-offset: 2px;
        `;

        btn.addEventListener('mouseenter', () => {
            if (!disabled) btn.style.background = 'var(--btn-bg-hover, #b0060f)';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.background = 'var(--btn-bg, #e50914)';
        });

        btn.innerHTML = text;
        return btn;
    }

    // Detect dark mode and set CSS variables accordingly
    function applyDarkModeStyles() {
        const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        if (dark) {
            document.documentElement.style.setProperty('--btn-bg', '#bb2525');
            document.documentElement.style.setProperty('--btn-bg-hover', '#7f1b1b');
            document.documentElement.style.setProperty('--btn-color', '#fff');
        }
    }

    // Get video ID from URL path (watch pages)
    function getVideoId() {
        const m = window.location.pathname.match(/^\/watch\/([^/#?]+)/);
        return m ? m[1] : null;
    }

    // Get meta video URL from og:video tag
    function getMetaVideoUrl() {
        const meta = document.querySelector('meta[property="og:video"]');
        return meta ? meta.content : null;
    }

    // Test if a URL exists by HEAD request
    function testUrl(url, cb) {
        fetch(url, { method: 'HEAD' })
            .then(res => cb(res.ok ? url : null))
            .catch(() => cb(null));
    }

    // Auto-download helper
    function triggerDownload(url, filename) {
        const a = document.createElement('a');
        a.href = url;
        a.download = filename || '';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }

    // Insert the main page download button
    function insertMainDownloadButton(videoId, url, hasAudio) {
        if (document.getElementById('redgifs-download-btn')) return;

        const fileName = `${videoId}.mp4`;
        const icon = hasAudio ? ICON_SOUND_ON : ICON_SOUND_OFF;
        const tooltip = hasAudio
            ? 'Click to download video with audio'
            : 'Silent video — audio not available';

        const btn = createButton(`${icon} Download MP4`, tooltip, 'redgifs-download-btn', !hasAudio);
        btn.style.position = 'fixed';
        btn.style.top = '20px';
        btn.style.right = '20px';
        btn.style.zIndex = 9999;
        btn.style.fontSize = '16px';

        if (hasAudio) {
            btn.addEventListener('click', () => triggerDownload(url, fileName));
        }

        document.body.appendChild(btn);
    }

    // Show loading indicator button
    function insertLoadingButton() {
        if (document.getElementById('redgifs-download-btn')) return;

        const btn = createButton('⏳ Checking audio...', 'Loading video info', 'redgifs-download-btn', true);
        btn.style.position = 'fixed';
        btn.style.top = '20px';
        btn.style.right = '20px';
        btn.style.zIndex = 9999;
        btn.style.fontSize = '16px';
        document.body.appendChild(btn);
    }

    // Main logic for watch page
    function handleWatchPage() {
        const videoId = getVideoId();
        if (!videoId) return;

        const originalUrl = getMetaVideoUrl();
        if (!originalUrl) {
            console.warn('[RedGIFs Downloader] No meta video URL found.');
            return;
        }

        insertLoadingButton();

        if (originalUrl.endsWith('-silent.mp4')) {
            const modifiedUrl = originalUrl.replace('-silent.mp4', '.mp4');
            testUrl(modifiedUrl, (validUrl) => {
                const btn = document.getElementById('redgifs-download-btn');
                if (btn) btn.remove();

                if (validUrl) {
                    insertMainDownloadButton(videoId, validUrl, true);
                    console.log(`[RedGIFs Downloader] ✅ Using audio URL: ${validUrl}`);
                } else {
                    insertMainDownloadButton(videoId, originalUrl, false);
                    console.log(`[RedGIFs Downloader] 🔇 Silent only available: ${originalUrl}`);
                }
            });
        } else {
            const btn = document.getElementById('redgifs-download-btn');
            if (btn) btn.remove();
            insertMainDownloadButton(videoId, originalUrl, true);
            console.log(`[RedGIFs Downloader] ✅ Using provided URL: ${originalUrl}`);
        }
    }

    // Batch download buttons for user profiles
    function handleUserProfilePage() {
        // Container for batch buttons - if needed for "Download All"
        // For now, we just add individual download buttons per video thumbnail.

        const containerSelector = '.gif-grid .gif-grid-item a[href^="/watch/"]';
        const links = document.querySelectorAll(containerSelector);
        if (!links.length) return;

        // Add a style for small batch buttons
        const style = document.createElement('style');
        style.textContent = `
          .batch-download-btn {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(229, 9, 20, 0.85);
            border-radius: 4px;
            padding: 4px 7px;
            font-size: 12px;
            color: white;
            font-weight: bold;
            cursor: pointer;
            user-select: none;
            z-index: 100;
            text-shadow: 0 0 2px black;
            transition: background 0.3s ease;
          }
          .batch-download-btn:hover {
            background: rgba(176, 0, 15, 0.85);
          }
          .gif-grid-item {
            position: relative;
          }
        `;
        document.head.appendChild(style);

        links.forEach(link => {
            const href = link.getAttribute('href');
            if (!href) return;

            const videoIdMatch = href.match(/\/watch\/([^/#?]+)/);
            if (!videoIdMatch) return;
            const videoId = videoIdMatch[1];

            // Skip if button already added
            if (link.parentElement.querySelector('.batch-download-btn')) return;

            const btn = document.createElement('div');
            btn.className = 'batch-download-btn';
            btn.title = 'Download MP4';
            btn.textContent = ICON_DOWNLOAD;

            btn.addEventListener('click', async (e) => {
                e.stopPropagation();
                e.preventDefault();

                // Fetch the meta tag for this video by fetching its page HTML
                try {
                    const response = await fetch(`https://www.redgifs.com/watch/${videoId}`);
                    if (!response.ok) throw new Error('Failed to fetch video page');

                    const text = await response.text();

                    // Parse meta tag content from the fetched HTML text
                    const metaMatch = text.match(/<meta property="og:video" content="([^"]+)"/);
                    if (!metaMatch) throw new Error('Meta video tag not found');

                    let videoUrl = metaMatch[1];
                    if (videoUrl.endsWith('-silent.mp4')) {
                        // try removing -silent for audio version
                        const audioUrl = videoUrl.replace('-silent.mp4', '.mp4');

                        // test if audio URL exists by HEAD
                        const testResp = await fetch(audioUrl, { method: 'HEAD' });
                        if (testResp.ok) videoUrl = audioUrl;
                    }

                    triggerDownload(videoUrl, `${videoId}.mp4`);
                } catch (err) {
                    alert('Download failed: ' + err.message);
                    console.error('Batch download error:', err);
                }
            });

            // Append the button inside the gif-grid-item parent
            link.parentElement.style.position = 'relative';
            link.parentElement.appendChild(btn);
        });
    }

    // Entry point
    function init() {
        applyDarkModeStyles();

        if (window.location.pathname.startsWith('/watch/')) {
            handleWatchPage();
        } else if (window.location.pathname.startsWith('/users/')) {
            handleUserProfilePage();
        }
    }

    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();