GFYCAT | Show all Download Links (GIF, MP4, WEBP, WEBM)

Adds direct links to all image (.gif, .webp) and video (.mp4, .webm) formats and sizes on Gfycat.

// ==UserScript==
// @name            GFYCAT | Show all Download Links (GIF, MP4, WEBP, WEBM)
// @namespace       de.sidneys.userscripts
// @homepage        https://gist.githubusercontent.com/sidneys/1af7b31282fa5019b6213d48e3b47c88/raw/
// @version         2.0.0
// @description     Adds direct links to all image (.gif, .webp) and video (.mp4, .webm) formats and sizes on Gfycat.
// @author          sidneys
// @icon            https://gfycat.com/assets/apple-touch-icon/apple-touch-icon-180x180.png
// @include         http*://*.gfycat.com/*
// @include         http*://gfycat.com/*
// @include         http*://redgifs.com/*
// @include         http*://*.redgifs.com/*
// @require         https://greasyfork.org/scripts/38888-greasemonkey-color-log/code/Greasemonkey%20%7C%20Color%20Log.js
// @require         https://greasyfork.org/scripts/374849-library-onelementready-es6/code/Library%20%7C%20onElementReady%20ES6.js
// @grant           GM.addStyle
// @grant           unsafeWindow
// @run-at          document-idle
// ==/UserScript==

/**
 * ESLint
 * @global
 */
/* global onElementReady */
Debug = false


/**
 * Inject Stylesheet
 */
let injectStylesheet = () => {
    console.debug('injectStylesheet')

    GM.addStyle(`
        /* Container
           ======================================= */

        .gif-info__direct-download-links
        {
            display: block;
            animation: var(--animation-fade-in);
        }


        /* Header
           ======================================= */

        .gif-info__direct-download-links h4
        {
            margin: 1rem 0 0.5rem 0;
            font-weight: 700;
        }

        /* Data
           ======================================= */

        .gif-info__direct-download-links p,
        .gif-info__direct-download-links a
        {
            margin: 0;
        }

        .gif-info__direct-download-links a
        {
            display: inline;
            list-style: none;
            transition: all 150ms ease-in-out;
        }

        .gif-info__direct-download-links a:after
        {
            content: "\\A";
            white-space: pre;
        }

        .gif-info__direct-download-links a:hover
        {
            text-decoration: underline;
            color: white;
        }


        /* Animations
           ======================================= */
        :root
        {
            --animation-fade-in: 'fade-in' 500ms ease-in-out 0s 1 normal forwards running;
        }

        @keyframes fade-in {
            from {
                filter: opacity(0);
            }
            to {
                filter: opacity(1);
            }
        }
    `)
}

/**
 * Convert filesize in bytes to human-readable format
 * @param {Number} bytes - Filesize in bytes
 * @return {String} - Filesize, human-readable
 */
let bytesToSize = (bytes = 0) => {
    console.debug('bytesToSize')

    const sizeList = ['B', 'K', 'M', 'G', 'T']
    const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))

    return `${Math.round(bytes / Math.pow(1024, i), 2)}${sizeList[i]}`
}

/**
 * Get GIF Info
 * @return {Object} - GIF Info
 */
let getGifInfo = () => {
    console.debug('getGifInfo')

    // Get url of current GIF
    const canonicalHref = document.querySelector('link[rel="canonical"]').href

    // Parse id of current GIF, removing trailing tags
    const gifIdFull = canonicalHref.split(/[\\/]/).pop()
    const gifId = gifIdFull.split('-')[0]

    // Use GIF Id to lookup GIF Info
    const cachedGifsMap = new Map(Object.entries(unsafeWindow.___INITIAL_STATE__.cache.gifs))
    const gifInfo = cachedGifsMap.get(gifId)

    return gifInfo
}

/**
 * Add buttons
 * @param {Element} targetElement - Target Element
 * @param {Object} gifInfo - GIF Info
 * @param {function=} callback - Callback
 */
let addButtons = (targetElement, gifInfo, callback = () => {}) => {
    console.debug('addButtons')

    // Create link menu from gifInfo.content_urls property
    const containerElement = document.createElement('div')
    containerElement.className = 'gif-views gif-info__direct-download-links'
    containerElement.innerHTML = `
        <h4>GIF Name:</h4>
        <p>${gifInfo.gfyName}</p>
        <h4>GIF Download Links:</h4>
        <a href="${gifInfo.max5mbGif}" target="_blank" type="image/gif">GIF (${!!gifInfo.content_urls.largeGif ? bytesToSize(gifInfo.content_urls.largeGif.size) : 'large'})</a>
        <a href="${gifInfo.max2mbGif}" target="_blank" type="image/gif">GIF (${!!gifInfo.content_urls.max2mbGif ? bytesToSize(gifInfo.content_urls.max2mbGif.size) : '< 2M'})</a>
        <a href="${gifInfo.max1mbGif}" target="_blank" type="image/gif">GIF (${!!gifInfo.content_urls.max1mbGif ? bytesToSize(gifInfo.content_urls.max1mbGif.size) : '< 1M'})</a>
        <a href="${gifInfo.mp4Url}" target="_blank" type="video/mp4">MP4 (${!!gifInfo.mp4Size ? bytesToSize(gifInfo.mp4Size) : 'large'})</a>
        <a href="${gifInfo.mobileUrl}" target="_blank" type="video/mp4">MP4 (${!!gifInfo.content_urls.mobile ? bytesToSize(gifInfo.content_urls.mobile.size) : 'mobile'})</a>
        <a href="${gifInfo.webpUrl}" target="_blank" type="image/webp">WEBP (${!!gifInfo.content_urls.webp ? bytesToSize(gifInfo.content_urls.webp.size) : ''})</a>
        <a href="${gifInfo.webmUrl}" target="_blank" type="video/webm">WEBM ${!!gifInfo.webmSize ? '(' + bytesToSize(gifInfo.webmSize) + ')' : ''}</a>
    `

    // Render link menu
    targetElement.insertBefore(containerElement, targetElement.firstChild.nextSibling)

    // DEBUG
    console.debug('buttons added:', `${document.querySelectorAll('.gif-info__direct-download-links a').length}`)

    // Callback
    callback()
}


/**
 * Init
 */
let init = () => {
    console.debug('init')

    // Add Stylesheet
    injectStylesheet()

    // Wait for button container
    //onElementReady('.share-desktop-container .actual-gif-image', false, () => {
    onElementReady('link[rel="canonical"]', false, () => {
        console.debug('onElementReady', 'link[rel="canonical"]')

        // Lookup GIF info
        const gifInfo = getGifInfo()

        if (!gifInfo) {
            console.error('Could not find GIF info, aborting.')
            return
        }

        // Add buttons to container element
        addButtons(document.querySelector('.share-desktop-container .gif-info'), gifInfo, () => {
            // Status
            console.info('Added Download Links for GIF:', gifInfo.gfyName)
        })
    })
}

/**
 * @listens window:Event#load
 */
window.addEventListener('load', () => {
    console.debug('window#load')

    init()
})