Quick 'n dirty little userscript for accessing pictures and videos off of Instagram. Everything is accessed via right-clicking image or video. Note: for videos requires viewing the actual post (not just in the feed).
// ==UserScript== // @name Instagram Downloader // @namespace https://lawrenzo.com/p/direct-picture-link // @version 1.3.2 // @description Quick 'n dirty little userscript for accessing pictures and videos off of Instagram. Everything is accessed via right-clicking image or video. Note: for videos requires viewing the actual post (not just in the feed). // @author Lawrence Sim // @license WTFPL (http://www.wtfpl.net) // @match https://*.instagram.com/* // ==/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, isVideo) => { let a; if(!contextMenu) { contextMenu = document.createElement("div"); for(let key in styles) contextMenu.style[key] = styles[key]; a = document.createElement("a"); a.setAttribute("target", "_blank"); contextMenu.append(a); document.body.append(contextMenu); } a = a || contextMenu.querySelector("a"); a.setAttribute("href", src); if(isVideo) { a.setAttribute("download", "video"); a.innerHTML = "> view video"; } else { a.setAttribute("download", ""); a.innerHTML = "> view 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|reel|tv)\/([A-Za-z0-9\-\_]+)\//, getPostId = () => (window.location.href.match(postRegExp) || [null])[1], fixMediaUrl = url => "https://scontent"+url.slice(url.indexOf(".cdninstagram.com")); var mediaUrlMap = {}; var getMediaUrls = async (postID, onComplete) => { if(mediaUrlMap[postID]) { return Promise.resolve(mediaUrlMap[postID]); } mediaUrlMap[postID] = fetch("https://www.instagram.com/p/"+postID+"/?__a=1&__d=dis") .then(res => { if(!res || !res.ok) throw Error((res && res.statusText) || "Fetch error"); return res.json(); }) .then(json => { if(!json) throw Error("Could not fetch JSON"); if(!json.items || !json.items.length) throw Error("No items found in JSON ("+(json.items && json.items.length)+")"); let medias; if(json.items[0].carousel_media) { medias = json.items[0].carousel_media; } else { medias = [json.items[0]]; } let mediaUrls = medias.map(media => { let bestMatch = null, versions = media.video_versions && media.video_versions.length ? media.video_versions : media.image_versions2.candidates; versions.find(version => { if(version.width == media.original_width) { bestMatch = version; return true; } else if(!bestMatch || version.width > bestMatch.width) { bestMatch = version; } }); return fixMediaUrl(bestMatch.url); }); return mediaUrlMap[postID] = mediaUrls; }) .catch(err => console.log(err) && null); return mediaUrlMap[postID]; }; var getMediaFrame = media => { if(media.tagName === "VIDEO") { return media.closest("[role='button']") || media.parentNode; } let galleryFrame = media.closest("li > div"); if(galleryFrame) return galleryFrame; return getComputedStyle(media)['user-select'] && media.closest("[role='button']"); }; var linkPicture = (img, src, frame) => { frame = frame || getMediaFrame(img); if(!frame) return; frame.addEventListener("contextmenu", evt => { evt.preventDefault(); openContextMenu(evt, src || img.getAttribute("src")); }); img.setAttribute("ilnkd", 1); }; var linkVideo = (vid, src, frame) => { frame = frame || getMediaFrame(vid); if(!frame) return; frame.addEventListener("contextmenu", evt => { evt.preventDefault(); openContextMenu(evt, src, true); }); vid.setAttribute("ilnkd", 1); }; var linkPost = async (mutated, postID, onComplete) => { var mediaUrls = await getMediaUrls(postID); if(!mediaUrls) return; 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") { if(media.closest("[role=\"menuitem\"]")) return media.setAttribute("ilnkd", 0); let alt = media.getAttribute("alt"); if(alt && alt.endsWith("profile picture")) return media.setAttribute("ilnkd", 0); 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, [role=\"button\"] img, article video, [role=\"button\"] video"); if(!media || media.getAttribute("ilnkd")) return; if(media.tagName === "IMG") { linkPicture(media, mediaUrls[0]); } else { linkVideo(media, mediaUrls[0]); } }); }; var link = (mutated, observer) => { mutated = mutated || [{target: document.body}]; let postId = getPostId(); if(!postId) { // feed, images only mutated.forEach(mutant => { Array.from(mutant.target.querySelectorAll("article img") || []) .filter(img => !img.getAttribute("ilinkd") && img.src) .forEach(img => linkPicture(img, fixMediaUrl(img.src))); }); } else { // post, special logic linkPost(mutated, postId); } }; link(); var obs = new MutationObserver(link); obs.observe(document.body, {childList:true, subtree:true}); })();