RedGifs Downloader

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/*
// @grant none
// @version 1.5
// @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==

// Object to store cached GIF information
const gifCache = {};

async function getGif(gifId) {
    // Check if the GIF information exists in the cache
    if (gifCache[gifId]) {
        return gifCache[gifId]; // Return cached data if available
    }

    // If not cached, fetch GIF info from the API
    const token = JSON.parse(localStorage.getItem('session_data')).token;
    let gifReq = await fetch(`https://api.redgifs.com/v2/gifs/${gifId}`, {
        method: 'GET',
        headers: {
            Authorization: `Bearer ${token}`
        }
    });

    let gifInfo = await gifReq.json();

    // Cache the fetched GIF information
    gifCache[gifId] = gifInfo;

    return gifInfo;
}

async function download(gifInfo) {
    let quality = localStorage.getItem('gifQuality');
    if (quality) {
        quality = quality.replace(/^"|"$/g, ''); // remove double quotes (i.e. '"hd"' => "hd")
    }
    let dlLink = gifInfo.gif.urls[quality ? quality : 'sd'];

    // Fetch the video data
    let response = await fetch(dlLink);
    let 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.setAttribute('class', "download");

    document.body.appendChild(link);
    link.click();

    // Clean up
    document.body.removeChild(link);
    URL.revokeObjectURL(link.href);
}

async function addButton (target, gifInfo) {
  // Check if the download button already exists for this GIF
  if (document.querySelector(`#DL_Btn_${gifInfo.gif.id}`)) {
      return;
  }

  // Sidebar item and button
  let sb_itm = document.createElement('li')
  sb_itm.setAttribute('class', 'SideBar-Item')
  let dl_btn = document.createElement('button')
  dl_btn.setAttribute('class', 'DL_Btn')
  dl_btn.setAttribute('id', `DL_Btn_${gifInfo.gif.id}`); // Unique ID for each button

  // Button icon
  let dl_icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  dl_icon.setAttribute('width', '24')
  dl_icon.setAttribute('height', '24')
  dl_icon.setAttribute('viewBox', '0 0 24 24')
  dl_icon.setAttribute('fill', 'white')
  dl_icon.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>"

  dl_btn.appendChild(dl_icon)
  sb_itm.appendChild(dl_btn)

  let parent_node = await waitForElm('.SideBar', target)
  let sibling_node = await waitForElm('.FSButton', target)

  // Copy styles of other button
  const styles = window.getComputedStyle(sibling_node);
  let cssText = styles.cssText;
  if (!cssText) {
      cssText = Array.from(styles).reduce((str, property) => {
          return `${str}${property}:${styles.getPropertyValue(property)};`;
      }, '');
  }
  dl_btn.style.cssText = cssText;

  // Insert button into sidebar
  sibling_node = sibling_node.parentNode
  parent_node.insertBefore(sb_itm, sibling_node.nextSibling)

  dl_btn.addEventListener('click', function(){ download(gifInfo) } )
}

function waitForElm(selector, target=document) {
    return new Promise(resolve => {
        if (target.querySelector(selector)) {
            return resolve(target.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (target.querySelector(selector)) {
                observer.disconnect();
                resolve(target.querySelector(selector));
            }
        });

        observer.observe(target.body, {
            childList: true,
            subtree: true
        });
    });
}

async function init() {
    let activeGif = null;

    while (true) {
        // Wait for an active GIF element
        const gif = await waitForElm('.GifPreview_isActive');

        // If the new active GIF is different from the previous one, update the button
        if (gif !== activeGif) {
            activeGif = gif;
            const info = await getGif(gif.id.split('_')[1]);
            addButton(gif, info);
        }

        // Introduce a delay before the next check (e.g., 100 milliseconds)
        await new Promise(resolve => setTimeout(resolve, 30));
    }
}

window.addEventListener('load', () => {
    init()
})