Instagram Downloader

Direct image links for sites that obfuscate the image from easy downloading.

目前為 2021-12-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Instagram Downloader
// @namespace    https://lawrenzo.com/p/direct-picture-link
// @version      0.5
// @description  Direct image links for sites that obfuscate the image from easy downloading.
// @author       Lawrence Sim
// @license      WTFPL (http://www.wtfpl.net)
// @match        https://*.instagram.com/*
// @grant        none
// ==/UserScript==
(function() {

    var contextMenu,
        styles = {
            'position': 'absolute',
            'background': '#efefef',
            'border-radius': '0.2em',
            'box-shadow': '1px 1px 3px 1px rgba(0,0,0,0.4)',
            'padding': '0.2em 0.5em',
            'font-size': '0.85em',
            'z-index': '999999'
        };
    var openContextMenu = (evt, src, type) => {
        let a;
        if(!contextMenu) {
            contextMenu = document.createElement("div");
            for(let key in styles) contextMenu.style[key] = styles[key];
            a = document.createElement("a");
            a.innerHTML = "> view direct source";
            a.setAttribute("target", "_blank");
            contextMenu.append(a);
            document.body.append(contextMenu);
        }
        a = a || contextMenu.querySelector("a");
        a.setAttribute("href", src);
        if(type === "video") {
            a.setAttribute("download", "video");
            a.innerHTML = "> view direct video";
        } else {
            a.setAttribute("download", "");
            a.innerHTML = "> view direct image";
        }
        contextMenu.style.left = `${evt.pageX}px`;
        contextMenu.style.top = `${evt.pageY}px`;
    };
    document.body.addEventListener("click", () => {
        if(!contextMenu) return;
        contextMenu.remove();
        contextMenu = null;
    });

    var postRegExp = /https?:\/\/(?:www\.)?instagram\.com\/p\/([A-Za-z0-9\-\_]+)\//,
        mediaUrlMap = {};
    var getMediaUrls = (postID, onComplete) => {
        return new Promise((resolve, reject) => {
            if(mediaUrlMap[postID]) {
                resolve(mediaUrlMap[postID]);
                return;
            }
            var req = new XMLHttpRequest();
            req.onerror = () => reject(req);
            req.onreadystatechange = () => {
                if(req.readyState == XMLHttpRequest.DONE && req.status == 200) {
                    let json, shortcodeMedia;
                    try {
                        json = JSON.parse(req.responseText);
                        shortcodeMedia = json.graphql.shortcode_media;
                    } catch(e) { }
                    if(!shortcodeMedia) return;
                    let mediaUrls;
                    switch(shortcodeMedia.__typename) {
                        case 'GraphImage':
                            mediaUrls = [shortcodeMedia.display_url];
                            break;
                        case 'GraphVideo':
                            mediaUrls = [shortcodeMedia.video_url];
                            break;
                        default:
                            var edges = (shortcodeMedia.edge_sidecar_to_children && shortcodeMedia.edge_sidecar_to_children.edges) || [];
                            mediaUrls = edges.map(edge => {
                                if(!edge.node.is_video) return edge.node.display_url;
                                let url = edge.node.video_url;
                                return "https://scontent"+url.slice(url.indexOf(".cdninstagram.com"));
                            });
                            break;
                    }
                    mediaUrlMap[postID] = mediaUrls;
                    resolve(mediaUrlMap[postID]);
                }
            };
            req.open("GET", "https://www.instagram.com/p/"+postID+"/?__a=1");
            req.send();
        });
    };

    var link = (mutated, observer) => {
        mutated = mutated || [{target: document.body}];
        let matchPost = window.location.href.match(postRegExp);
        matchPost ? linkPost(mutated, matchPost[1]) : linkFeed(mutated);
    };

    var linkPicture = (img, src, multi) => {
        let btn = multi ? multi.querySelector("div[role='button']") : img.closest("div[role='button']");
        if(!btn) return;
        btn.addEventListener("contextmenu", evt => {
            evt.preventDefault();
            openContextMenu(evt, src || img.getAttribute("src"));
        });
        img.setAttribute("ilnkd", 1);
    };
    var linkVideo = (vid, src, multi) => {
        let btn = multi ? multi.firstChild : vid.parentNode.parentNode.parentNode.parentNode;
        if(!btn) return;
        btn.addEventListener("contextmenu", evt => {
            evt.preventDefault();
            openContextMenu(evt, src, "video");
        });
        vid.setAttribute("ilnkd", 1);
    };

    var linkFeed = mutated => {
        mutated.forEach(mutant => {
            mutant.target.querySelectorAll("article img").forEach(img => {
                if(img.getAttribute("ilnkd")) return;
                linkPicture(img, img.getAttribute("src"), img.closest("li > div"));
            });
            //mutant.target.querySelectorAll("article video").forEach(vid => {
            //    if(vid.getAttribute("ilnkd")) return;
            //    linkVideo(vid, vid.getAttribute("src"), vid.closest("li > div"));
            //});
        });
    };

    var linkPost = async function(mutated, postID, onComplete) {
        var mediaUrls = await getMediaUrls(postID);
        mutated.forEach(mutant => {
            mutant.target.querySelectorAll("li").forEach(li => {
                let media = li.querySelector("img, video");
                if(!media || media.getAttribute("ilnkd") !== null) return;
                let thisIndex = 0;
                [li.previousSibling, li.nextSibling].find((sibling, i) => {
                    if(!sibling) return;
                    let siblingMedia = sibling && sibling.querySelector("img, video"),
                        nearIndex = parseInt(siblingMedia && siblingMedia.getAttribute("ilnkd") || -1);
                    if(~nearIndex) {
                        thisIndex = nearIndex + (i ? -1 : 1);
                        return true;
                    }
                });
                if(media.tagName === "IMG") {
                    linkPicture(media, mediaUrls[thisIndex], media.closest("li > div"));
                } else {
                    linkVideo(media, mediaUrls[thisIndex], media.closest("li > div"));
                }
                media.setAttribute("ilnkd", thisIndex);
            });
            let media = mutant.target.querySelector("article img, article video");
            if(media.getAttribute("ilnkd")) return;
            if(media.tagName === "IMG") {
                linkPicture(media, mediaUrls[0]);
            } else {
                linkVideo(media, mediaUrls[0]);
            }
        });
    };


    link();
    var obs = new MutationObserver(link);
    obs.observe(document.body, {childList:true, subtree:true});

})();