Google DWIMages

Direct links to images and pages on Google Images

当前为 2020-02-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          Google DWIMages
// @description   Direct links to images and pages on Google Images
// @author        chocolateboy
// @copyright     chocolateboy
// @version       2.0.0
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL: http://www.gnu.org/copyleft/gpl.html
// @include       https://www.google.tld/*tbm=isch*
// @include       https://encrypted.google.tld/*tbm=isch*
// @require       https://code.jquery.com/jquery-3.4.1.min.js
// @require       https://cdn.jsdelivr.net/gh/eclecto/jQuery-onMutate@79bbb2b8caccabfc9b9ade046fe63f15f593fef6/src/jquery.onmutate.min.js
// @grant         GM_log
// @inject-into   content
// ==/UserScript==

// XXX note: the unused grant is a workaround for a Greasemonkey bug:
// https://github.com/greasemonkey/greasemonkey/issues/1614

let INITIALIZED = false, METADATA

// return the image metadata subtree (array) of the full metadata tree
function imageMetadata (tree) {
    return tree[31][0][12][2]
}

// register the listener for metadata requests and parse the data for the first ≈
// 100 images out of the SCRIPT element embedded in the page
function init () {
    window.XMLHttpRequest.prototype.open = hookXHROpen(window.XMLHttpRequest.prototype.open)

    const scripts = Array.from(document.scripts)
    const callbacks = scripts.filter(script => /^AF_initDataCallback\b/.test(script.text))

    try {
        const callback = callbacks.pop().text
        METADATA = imageMetadata(parseMetadata(callback))
    } finally {
        INITIALIZED = true
    }
}

// determine whether an XHR request is for another batch of image metadata
function isImageDataRequest (args) {
    return (args.length >= 2)
        && (args[0] === 'POST')
        && /\/batchexecute\?rpcids=/.test(String(args[1]))
}

// return the URL for the nth image (0-based)
function nthImageUrl (index) {
    return METADATA[index][1][3][0]
}

// return a version of XmlHttpRequest#open which checks for and intercepts image
// metadata requests before delegating to the original method
//
// if the URL matches, we append the new metadata to a global store
function hookXHROpen (oldOpen) {
    return function open (...args) {
        if (isImageDataRequest(args)) {
            this.addEventListener('load', () => {
                let parsed

                try {
                    const stringified = this.responseText.match(/("\[[\s\S]+\](?:\\n)?")/)[1]
                    const json = JSON.parse(stringified)
                    parsed = JSON.parse(json)
                } catch (e) {
                    console.error("Can't parse response:", e)
                    return
                }

                try {
                    METADATA = METADATA.concat(imageMetadata(parsed))

                    // run once against the new images
                    $.onCreate('div[data-ri][data-ved][jsaction]', onResults)
                } catch (e) {
                    console.error("Can't merge new metadata:", e)
                }
            })
        }

        return oldOpen.apply(this, args)
    }
}

// process a new batch of results (DIVs), assigning the image URL to the first
// link and disabling trackers
//
// used to process the original batch of results as well as the lazily-loaded
// updates
function onResults ($results) {
    $results.each(function () {
        if (!INITIALIZED) {
            try {
                init()
            } catch (e) {
                console.error("Can't parse metadata:", e)
                return false // i.e. break out of the +each+ loop
            }
        }

        if (!METADATA) {
            return false // break
        }

        // grab the metadata for this result
        const $result = $(this)
        const index = $result.data('ri') // 0-based index of the result

        let imageUrl

        try {
            imageUrl = nthImageUrl(index)
        } catch (e) {
            console.warn(`Can't find image URL for image #${index + 1}`)
            return // continue
        }

        // prevent new trackers being registered on this DIV and its descendant
        // elements
        $result.find('*').addBack().removeAttr('jsaction')

        // assign the correct/missing URI to the image link
        const $links = $result.find('a')
        const $imageLink = $links.eq(0)
        const $pageLink = $links.eq(1)

        $imageLink.attr('href', imageUrl)

        // pre-empt the existing trackers on elements which don't already have
        // direct listeners (the result element and the image link)
        $result.on('click focus mousedown', stopPropagation)
        $imageLink.on('click focus mousedown', stopPropagation)

        // and blast the trackers off the element which does (the page link)
        $pageLink.replaceWith($pageLink.clone())
    })
}

// extract the image metadata for the original batch of results from the
// contents of the SCRIPT tag
function parseMetadata (fragment) {
    // XXX not all browsers support the ES2018 /s (matchAll) flag
    // const json = callback.match(/(\[.+\])/s)[1]
    const json = fragment.match(/(\[[\s\S]+\])/)[1]
    return JSON.parse(json)
}

// event handler for images links, page links and results which prevents their
// click/mousedown events being hijacked for tracking
function stopPropagation (e) {
    e.stopPropagation()
}

$.onCreate('div[data-ri][data-ved]', onResults) // run once against the initial images