GIPHY | Direct GIF Image Download Button

Adds a button for directly downloding the original GIF images to the side menu. Uses the given GIF title as the local filename.

// ==UserScript==
// @name            GIPHY | Direct GIF Image Download Button
// @namespace       de.sidneys.userscripts
// @homepage        https://gist.githubusercontent.com/sidneys/13433a0c726895597fb4571405c7ca15/raw/
// @version         0.9.1
// @description     Adds a button for directly downloding the original GIF images to the side menu. Uses the given GIF title as the local filename.
// @author          sidneys
// @icon            https://giphy.com/static/img/favicon.png
// @include         http*://giphy.com/gifs/*
// @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
// @connect         giphy.com
// @grant           GM.addStyle
// @grant           GM.download
// @run-at          document-end
// ==/UserScript==

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


/**
 * @callback saveAsCallback
 * @param {Error} error - Error
 * @param {Number} progress - Progress fraction
 * @param {Boolean} complete - Completion Yes/No
 */

/**
 * Download File via Greasemonkey
 * @param {String} url - Target URL
 * @param {String} fileName - Target Filename
 * @param {saveAsCallback} callback - Callback
 */
let saveAs = (url, fileName, callback = () => {}) => {
    console.debug('saveAs')

    // Parse URL
    const urlObject = new URL(url)
    const urlHref = urlObject.href

    // Download
    // noinspection JSValidateTypes
    GM.download({
        url: urlHref,
        name: fileName,
        saveAs: true,
        onerror: (download) => {
            console.debug('saveAs', 'onerror')

            callback(new Error(download.error ? download.error.toUpperCase() : 'Unknown'))
        },
        onload: () => {
            console.debug('saveAs', 'onload')

            callback(null)
        },
        ontimeout: () => {
            console.debug('saveAs', 'ontimeout')

            callback(new Error('Network timeout'))
        }
    })
}

/**
 * Get JSON Schema
 * @return {Object|void} - Giphy JSON Schema
 */
let getSchema = () => {
    console.debug('getSchema')

    let schemaObject

    // Read JSON Schema 'giphy-schema' from <script> tags
    try {
        // Parse property '.image.url' from Schema
        const schemaText = document.querySelector('script[name="giphy-schema"]').textContent
        schemaObject = JSON.parse(schemaText)
    } catch (error) {
        console.error(error)
        return
    }

    // Return
    return schemaObject
}

/**
 * Get GIF image URL
 * @return {String|void} - GIF Image URL
 */
let getGifImageUrl = () => {
    console.debug('getGifImageUrl')

    // Lookup Schema
    const schemaObject = getSchema()

    // Parse Media URL
    const gifMediaUrlObject = new URL(schemaObject.image.url)

    // Extract unique Image Identifier from Media URL, abort on failure
    const matchList = (new RegExp('media/(.*)/giphy.gif')).exec(gifMediaUrlObject.pathname) || []
    if (matchList.length === 0) { return }

    // Construct GIF Direct Download URL from Image Identifier ('https://i.giphy.com/identifier.gif')
    const imageId = matchList[1]
    const gifDirectUrl = `https://i.giphy.com/${imageId}.gif`

    // Status
    console.info('GIF Direct Download Image URL', gifDirectUrl)

    // Return
    return gifDirectUrl
}

/**
 * Get GIF Title
 * @return {String|void} - GIF Image URL
 */
let getGifTitle = () => {
    console.debug('getGifTitle')

    // Lookup Schema
    const schemaObject = getSchema()

    // Return
    return schemaObject.headline
}

/**
 * Add a button in menu on  the right hand side
 * @param {Element} referenceElement - Reference Menu Element
 * @param {String} targetUrl - Target URL for Download
 * @param {String} fileName - Downloaded Filename
 * @param {String} label - Button label
 */
let addMenuButton = (referenceElement, targetUrl, fileName, label) => {
    console.debug('addMenuButton')

    // Create Button: Duplicate previous menu button
    const buttonElement = referenceElement.cloneNode(true)
    referenceElement.parentElement.appendChild(buttonElement)

    // Create Icon
    const iconElement = buttonElement.querySelector('i')
    iconElement.style.float = 'left'
    iconElement.style.background = 'no-repeat center/80% url("https://raw.githubusercontent.com/google/material-design-icons/master/action/2x_web/ic_get_app_white_36dp.png")'
    iconElement.style.animation = 'none'

    // Create Link
    const anchorElement = document.createElement('a')
    anchorElement.target = '_blank'
    anchorElement.href = '#'
    anchorElement.rel = 'noopener noreferrer'
    anchorElement.type = 'image/gif'
    anchorElement.style.display = 'block'
    anchorElement.style.float = 'left'
    buttonElement.appendChild(anchorElement)

    // Create Label
    const textElement = buttonElement.querySelector('span')
    anchorElement.style.color = 'rgb(240, 22, 196)'
    textElement.textContent = label
    anchorElement.appendChild(textElement)

    // Register Download Button Events
    anchorElement.onclick = (event) => {
        // Cancel regular download
        event.preventDefault()

        // Status
        console.info('Downloading:', targetUrl, 'to:', fileName)

        // Start download
        saveAs(targetUrl, fileName, (error) => {
            // Error
            if (error) {
                console.error(error)
                return
            }

            // Status
            console.info('Complete:', targetUrl, 'to:', fileName)
        })
    }
}


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

    // Wait for default right-hand side menu (Favorite / Copy link / Media / Embed)
    onElementReady('.gif-detail-page > div > div:nth-child(4) > div > div:nth-child(2) > div > div:nth-child(2) > div:nth-child(2) > div:last-child', false, (element) => {

        const gifImageUrl = getGifImageUrl()
        const gifTitle = getGifTitle()

        if (!gifImageUrl || !gifTitle) {
            console.warning('Could not find GIF Direct Download URL, aborting.')
            return
        }

        // Add download button to menu
        addMenuButton(element, gifImageUrl, `${gifTitle}.gif`, 'Download GIF')
    })
}

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

    init()
})