Download Instagram photos and videos from posts.
// ==UserScript== // @name Instagram Downloader - HISHTNIK // @namespace http://tampermonkey.net/ // @license MIT // @version 1.6 // @description Download Instagram photos and videos from posts. // @author You // @include /^http.*:\/\/(?:www\.)?instagram\.com\/.*$/ // @icon https://www.google.com/s2/favicons?domain=instagram.com // @grant GM_download // ==/UserScript== (function() { 'use strict'; // VIDEO DOWNLOADS TEMPORARILY NOT WORKING. ALTERNATIVE: // Firefox addon => Video Download Helper (with companion app installed) // Video Download Helper > Settings > Behaviour > Download Processor > Companion App (make sure you save) // To download videos, copy the link and open it in a new tab. Then click on the Video Download Helper Icon // Gradual download progress with videos: Tampermonkey > Settings > Advanced > Download Mode > Browser API /* Originally developed in Firefox but works in Chromium-based browsers too. Every 300 milliseconds, the script attempts to add buttons to the elements inside of a post SAVED_VIDEO_DOWNL_LINKS_OBJ stores the download links of fetched videos (so they don't have to be fetched again): {thumbFilename:videoDownloadLink, ....} (could reach limit faster) Saved video links are search based on the thumbnail filename (that on page vs saved) Photos are downloaded from their src attribute of the <img> element on page (since that seems to be the highest quality one) */ let BTNS_WRAPPER_HTML_CLASS_STR = "hishtnikBtnsWrapper", BTN_HTML_CLASS_STR = `hishtnikBtn`, DOWNL_VIDEO_BTN_HTML_CLASS_STR = `hishtnikDownlVidBtn`, DOWNL_PHOTO_BTN_HTML_CLASS_STR = `hishtnikDownlPhotoBtn`, DOWNL_THUMB_BTN_HTML_CLASS_STR = `hishtnikDownlThumbBtn`, OPEN_THUMB_BTN_HTML_CLASS_STR = `hishtnikOpenThumbBtn`; let POST_MEDIA_ELEMS_CSS_SELECTOR_STR = `video, div[role="button"] img[style="object-fit: cover;"]`, POST_OP_USERNAME_CSS_SELECTOR_STR = `.UE9AK .Jv7Aj.mArmR.MqpiF`; let STYLE_HTML_STR = `<style> /* Download / Open thumb buttons */ .${BTNS_WRAPPER_HTML_CLASS_STR} {display: flex !important; width: 100%; flex-direction: row !important; justify-content: space-between; z-index:9999999; position:absolute !important; top:0;} .${BTN_HTML_CLASS_STR} {width: auto; cursor:pointer; padding:5px; font-weight:bold; color:#ff2d2d; background:black; border:1px solid;} /* add borders and background to albums labers (easier to see) */ .CzVzU > div, ._aatp > div {padding: 5px !important;} .CzVzU > div, ._aatp > div, button[aria-label="Go Back"], button[aria-label="Next"] {background: #951111 !important; border: 2px solid #979085 !important;} </style`; let SAVED_VIDEO_DOWNL_LINKS_OBJ = {}, MAX_SAVED_VIDEO_LINKS_INT = 100; run(); async function run() { document.querySelector("html").appendChild(str_to_html_elem(STYLE_HTML_STR)); document.addEventListener("dblclick", (event)=>{event.stopPropagation(); event.preventDefault();}, true); // prevent double click like on post items document.addEventListener("click", click_handler); while(1==1) {if (window.location.href.match(/^.+\/p\/.+$/)) add_btns(); await delay(300);} } function add_btns() { let htmlMediaElemsInPost = document.querySelectorAll(POST_MEDIA_ELEMS_CSS_SELECTOR_STR); for (let htmlMediaElem of htmlMediaElemsInPost) { let htmlMediaElemWrapper = htmlMediaElem.parentElement; // the buttons wrapper elem will become a sibiling to the media elem if (htmlMediaElemWrapper.querySelector(`.${BTNS_WRAPPER_HTML_CLASS_STR}`)) continue; // already added let btnsHtmlStr = ``; if (htmlMediaElem.nodeName == "VIDEO") { btnsHtmlStr += `<button class="${BTN_HTML_CLASS_STR} ${DOWNL_VIDEO_BTN_HTML_CLASS_STR}">DOWNL VIDEO</button>`; btnsHtmlStr += `<button class="${BTN_HTML_CLASS_STR} ${DOWNL_THUMB_BTN_HTML_CLASS_STR}">DOWNL THUMB</button>`; btnsHtmlStr += `<button class="${BTN_HTML_CLASS_STR} ${OPEN_THUMB_BTN_HTML_CLASS_STR}">OPEN THUMB</button>`; } else if (htmlMediaElem.nodeName == "IMG") { btnsHtmlStr += `<button class="${BTN_HTML_CLASS_STR} ${DOWNL_PHOTO_BTN_HTML_CLASS_STR}">DOWNL PHOTO</button>`; } btnsHtmlStr = `<div class="${BTNS_WRAPPER_HTML_CLASS_STR}">` + btnsHtmlStr + `</div>`; htmlMediaElemWrapper.appendChild(str_to_html_elem(btnsHtmlStr)); } } function click_handler(event) { if (!event.target.classList.contains(BTN_HTML_CLASS_STR)) return; let htmlBtnElemClicked = event.target; if (htmlBtnElemClicked.classList.contains(OPEN_THUMB_BTN_HTML_CLASS_STR)) open_thumb(htmlBtnElemClicked); else if (htmlBtnElemClicked.classList.contains(DOWNL_THUMB_BTN_HTML_CLASS_STR)) downl_img("thumb", htmlBtnElemClicked); else if (htmlBtnElemClicked.classList.contains(DOWNL_PHOTO_BTN_HTML_CLASS_STR)) downl_img("photo", htmlBtnElemClicked); else if (htmlBtnElemClicked.classList.contains(DOWNL_VIDEO_BTN_HTML_CLASS_STR)) downl_video(htmlBtnElemClicked); } function open_thumb(htmlBtnElem) { let htmlMediaElemWrapper = htmlBtnElem.parentElement.parentElement; try {window.open(htmlMediaElemWrapper.querySelector("video").getAttribute("poster"))} catch(err) {alert("Failed to open thumbnail.")} } function downl_img(typeOfImgStr, htmlBtnElem) { let downlLinkStr; try { let htmlMediaElemWrapper = htmlBtnElem.parentElement.parentElement; downlLinkStr = (typeOfImgStr == "photo") ? htmlMediaElemWrapper.querySelector("img").src : htmlMediaElemWrapper.querySelector("video").poster; } catch(err) {alert("Couldn't get download link. Download failed."); return;} let authorStr = get_post_author(); if (!authorStr) alert("Downloaded filename will not have the author's username due to an error. Please inform developer."); GM_download(downlLinkStr, generate_cust_filename(authorStr, downlLinkStr)); } async function downl_video(htmlBtnElem) { let authorStr = get_post_author(); // author name is defined here first, in case the post is closed while fetching let targetVidThumbFilenameStr; try {targetVidThumbFilenameStr = get_filename_from_url(htmlBtnElem.parentElement.parentElement.querySelector("video").poster)} catch(err) {alert("Error initializing the video download process. Download failed."); return;} let downlLinkStr = SAVED_VIDEO_DOWNL_LINKS_OBJ[targetVidThumbFilenameStr] || await get_newly_fetched_download_link(); if (!downlLinkStr) {alert("Error getting video download link. Download failed."); return;} if (!authorStr) alert("Downloaded filename will not have the author's username due to an error. Please inform developer."); GM_download(downlLinkStr, generate_cust_filename(authorStr, downlLinkStr)); async function get_newly_fetched_download_link() { try { let fetchResponseObj = await fetch(window.location.href + "?__a=1"); let postInfoObj = (await fetchResponseObj.json())["items"][0]; // get the data from the response and advance to the meaningful part let mediaInfosArr = postInfoObj["carousel_media"] || [postInfoObj]; // album test - if not, put it in array so you can loop for (let mediaInfoObj of mediaInfosArr) { try { let videoSrcStr = mediaInfoObj["video_versions"][0]["url"]; // the first version seems to be the highest quality one let thumbFilenameStr = get_filename_from_url(mediaInfoObj["image_versions2"]["candidates"][0]["url"]); if (Object.keys(SAVED_VIDEO_DOWNL_LINKS_OBJ).length == MAX_SAVED_VIDEO_LINKS_INT) delete SAVED_VIDEO_DOWNL_LINKS_OBJ[Object.keys(SAVED_VIDEO_DOWNL_LINKS_OBJ)[0]]; SAVED_VIDEO_DOWNL_LINKS_OBJ[thumbFilenameStr] = videoSrcStr; } catch(err){} } return SAVED_VIDEO_DOWNL_LINKS_OBJ[targetVidThumbFilenameStr]; } catch(err) {return false} } } function delay(durationMs) {return new Promise(resolve => setTimeout(resolve, durationMs));} function get_post_author() { if (document.title.includes("@")) { // within profile try {return document.title.split("@").pop().split(")")[0].split(" ")[0]} catch(err) {return ""} } else { // individual post opened try {return document.querySelector(POST_OP_USERNAME_CSS_SELECTOR_STR).innerText.trim()} catch(err) {return ""} } } function generate_cust_filename(authorStr, downlLinkStr) { if (authorStr) authorStr += "_"; return authorStr + get_filename_from_url(downlLinkStr); } function get_filename_from_url(url) { let filename = url.split("?")[0].split("/").pop(); return filename.replace(/\.jpg\.webp$|\.webp$/, ".jpg"); // change .webp to .jpg. Also helps with inconsistencies between fetched thumb and thumbs on page } function str_to_html_elem(str) { let htmlWrapperElem = document.createElement("div"); htmlWrapperElem.innerHTML = str; if (htmlWrapperElem.childElementCount == 1) htmlWrapperElem = htmlWrapperElem.firstChild; // only keep the wrapper if there are multiple direct children return htmlWrapperElem; } })();