Sleazy Fork is available in English.
Creates a sidebar button to download the currently playing gif in the currently selected quality.
// ==UserScript==
// @name RedGifs Downloader
// @namespace burrito.scripts
// @match http*://*.redgifs.com/*
// @match http*://redgifs.com/*
// @run-at document-idle
// @grant GM_addStyle
// @version 1.13
// @author hunkyburrito
// @description Creates a sidebar button to download the currently playing gif in the currently selected quality.
// @homepage https://gist.github.com/hunkyburrito/f588fa77e75e29f9eeabcd24b21e35f8#file-redgif_downloader-js
// @license GNU GPLv3
// ==/UserScript==
// Remove Ads
GM_addStyle(`
.injection,.liveAdButton,.InformationBar,.OnlyFansCreatorsSidebar{
display: none !important;
}
`);
// Cache with TTL
const cacheTTL = 5 * 60 * 1000; // 5 minutes
const gifCache = new Map(); // { id: { data, timestamp } }
function getCachedGif(gifId) {
const entry = gifCache.get(gifId);
if (!entry) return null;
if (Date.now() - entry.timestamp > cacheTTL) {
gifCache.delete(gifId); // auto-expire
return null;
}
return entry.data;
}
function setCachedGif(gifId, data) {
gifCache.set(gifId, { data, timestamp: Date.now() });
}
// Fetch Gif
async function getGif(gifId) {
const cached = getCachedGif(gifId);
if (cached) return cached;
try {
const sessionData = localStorage.getItem('session_data');
if (!sessionData) throw new Error('No session data found');
const token = JSON.parse(sessionData).token;
if (!token) throw new Error('Missing auth token');
const res = await fetch(`https://api.redgifs.com/v2/gifs/${gifId}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
const gifInfo = await res.json();
setCachedGif(gifId, gifInfo);
return gifInfo;
} catch (err) {
console.error('[RedGifs Downloader] Failed to fetch GIF info:', err);
throw err;
}
}
// Download Gif
async function download(gifId) {
const gifInfo = await getGif(gifId);
const dlLink = gifInfo.gif.urls['hd'];
// Fetch video data
const response = await fetch(dlLink);
const data = await response.blob();
// Create a link element and trigger the download
const link = document.createElement('a');
link.href = URL.createObjectURL(data);
link.download = `${gifInfo.gif.id}.mp4`;
link.className = 'download';
document.body.appendChild(link);
link.click();
// Clean up
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
// Cache Styles
let cachedButtonStyle = null;
function getButtonStyleReference() {
if (cachedButtonStyle) return cachedButtonStyle;
const refBtn = document.querySelector('.FSButton');
if (!refBtn) return null;
const styles = window.getComputedStyle(refBtn);
// Prefer cssText; fallback to serialization
cachedButtonStyle = styles.cssText || Array.from(styles).reduce((str, prop) => {
return `${str}${prop}:${styles.getPropertyValue(prop)};`;
}, '');
return cachedButtonStyle;
}
// Icon
const dlIconTemplate = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
dlIconTemplate.setAttribute('width', '24');
dlIconTemplate.setAttribute('height', '24');
dlIconTemplate.setAttribute('viewBox', '0 0 24 24');
dlIconTemplate.setAttribute('fill', 'white');
dlIconTemplate.innerHTML = "<path d='M11.29 15.71a1 1 0 0 0 .33.21 1 1 0 0 0 .76 0 1 1 0 0 0 .33-.21l3-3a1 1 0 0 0-1.42-1.42L13 12.59V9a1 1 0 0 0-2 0v3.59l-1.29-1.3a1 1 0 0 0-1.42 0 1 1 0 0 0 0 1.42zM12 22A10 10 0 1 0 2 12a10 10 0 0 0 10 10zm0-18a8 8 0 1 1-8 8 8 8 0 0 1 8-8z' stroke='currentColor' stroke-width='0.5' stroke-linecap='round' stroke-linejoin='round'></path>";
// Insert download button
async function addButton (target, gifId) {
// Check if download button already exists
if (document.getElementById(`DL_Btn_${gifId}`)) return;
// Sidebar item and button
const sbItem = document.createElement('li');
sbItem.className = 'sideBarItem';
const dlBtn = document.createElement('button');
dlBtn.className = 'DL_Btn';
dlBtn.id = `DL_Btn_${gifId}`; // Unique ID for each button
// Button icon
const dlIcon = dlIconTemplate.cloneNode(true); // Reuse pre-created SVG icon
dlBtn.appendChild(dlIcon);
sbItem.appendChild(dlBtn);
const sibling = await waitForElm('.LikeButton', target, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
// Copy other button styles
dlBtn.style.cssText = getButtonStyleReference();
// Insert button into sidebar
const siblingNode = sibling.parentNode;
const parent = siblingNode.parentNode;
parent.insertBefore(sbItem, siblingNode);
dlBtn.addEventListener('click', function(){ download(gifId) } );
}
// Generic observer
function waitForElm(selector, root = document, config = {childList: true, subtree: true}) {
return new Promise(resolve => {
const existing = root.querySelector(selector);
if (existing) return resolve(existing);
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
const target = root.querySelector(selector);
if (target) {
observer.disconnect();
resolve(target);
}
});
});
observer.observe(root, config);
});
}
async function handleFeedUpdate(mutation) {
const target = mutation.target;
if (target.classList.contains('GifPreview_isActive')) { // gifs
const gifId = target.id.split('_')[1];
addButton(target, gifId);
} else if (target.childNodes.length){
if (target.childNodes[0].classList.contains('_StreamateCamera_1eekr_1')) { // streams
target.parentNode.removeChild(target);
}
}
}
const feedObserver = new MutationObserver(mutations => {
mutations.forEach(handleFeedUpdate);
});
async function init() {
const first = await waitForElm('div.GifPreview', document, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
const feed = document.getElementsByClassName('previewFeed')[0];
const gifPreviews = feed.getElementsByClassName('GifPreview');
for (let gif of gifPreviews) {
feedObserver.observe(gif, {
attributes: true,
attributeFilter: ['class']
});
}
}
let navDebounce;
async function handleNavigate() {
const routeWrapper = await waitForElm('div.routeWrapper');
const navObserver = new MutationObserver(mutations => {
if (navDebounce) clearTimeout(navDebounce);
navDebounce = setTimeout(() => {
feedObserver.disconnect();
gifCache.clear();
init();
}, 50);
});
navObserver.observe(routeWrapper, {childList: true});
}
handleNavigate();
init();