// ==UserScript== // @name Instagram Download Button // @name:zh-TW Instagram 下載器 // @name:zh-CN Instagram 下载器 // @name:ja Instagram ダウンローダー // @name:ko Instagram 다운로더 // @name:es Descargador de Instagram // @name:fr Téléchargeur Instagram // @name:hi इंस्टाग्राम डाउनलोडर // @name:ru Загрузчик Instagram // @namespace https://www.zhaohespray.com // @version 1.9.5 // @compatible chrome // @compatible firefox // @compatible edge // @description Add the download button and the open button to download or open profile picture and media in the posts, stories, and highlights in Instagram // @description:zh-TW 在Instagram頁面加入下載按鈕與開啟按鈕,透過這些按鈕可以下載或開啟大頭貼與貼文、限時動態、Highlight中的照片或影片 // @description:zh-CN 在Instagram页面加入下载按钮与开启按钮,透过这些按钮可以下载或开启大头贴与贴文、限时动态、Highlight中的照片或影片 // @description:ja メディアをダウンロードまたは開くためのボタンを追加します // @description:ko 미디어를 다운로드하거나 여는 버튼을 추가합니다 // @description:es Agregue botones para descargar o abrir medios // @description:fr Ajoutez des boutons pour télécharger ou ouvrir des médias // @description:hi मीडिया को डाउनलोड या खोलने के लिए बटन जोड़ें। // @description:ru Добавьте кнопки для загрузки или открытия медиа // @author ZhiYu // @match https://www.instagram.com/* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ================= // = Options = // ================= const attachLink = true; // add link into the button elements const postFilenameTemplate = "%id%-%datetime%-%medianame%.%ext%"; const storyFilenameTemplate = postFilenameTemplate; // ================== function yyyymmdd(date) { // ref: https://stackoverflow.com/questions/3066586/get-string-in-yyyymmdd-format-from-js-date-object?page=1&tab=votes#tab-top var mm = date.getMonth() + 1; // getMonth() is zero-based var dd = date.getDate(); return [date.getFullYear(), (mm > 9 ? '' : '0') + mm, (dd > 9 ? '' : '0') + dd ].join(''); } var svgDownloadBtn = `<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" height="24" width="24" viewBox="0 0 477.867 477.867" style="fill:%color;" xml:space="preserve"> <g> <path d="M443.733,307.2c-9.426,0-17.067,7.641-17.067,17.067v102.4c0,9.426-7.641,17.067-17.067,17.067H68.267 c-9.426,0-17.067-7.641-17.067-17.067v-102.4c0-9.426-7.641-17.067-17.067-17.067s-17.067,7.641-17.067,17.067v102.4 c0,28.277,22.923,51.2,51.2,51.2H409.6c28.277,0,51.2-22.923,51.2-51.2v-102.4C460.8,314.841,453.159,307.2,443.733,307.2z"/> </g> <g> <path d="M335.947,295.134c-6.614-6.387-17.099-6.387-23.712,0L256,351.334V17.067C256,7.641,248.359,0,238.933,0 s-17.067,7.641-17.067,17.067v334.268l-56.201-56.201c-6.78-6.548-17.584-6.36-24.132,0.419c-6.388,6.614-6.388,17.099,0,23.713 l85.333,85.333c6.657,6.673,17.463,6.687,24.136,0.031c0.01-0.01,0.02-0.02,0.031-0.031l85.333-85.333 C342.915,312.486,342.727,301.682,335.947,295.134z"/> </g> </svg>`; var svgNewtabBtn = `<svg id="Capa_1" style="fill:%color;" viewBox="0 0 482.239 482.239" xmlns="http://www.w3.org/2000/svg" height="24" width="24"> <path d="m465.016 0h-344.456c-9.52 0-17.223 7.703-17.223 17.223v86.114h-86.114c-9.52 0-17.223 7.703-17.223 17.223v344.456c0 9.52 7.703 17.223 17.223 17.223h344.456c9.52 0 17.223-7.703 17.223-17.223v-86.114h86.114c9.52 0 17.223-7.703 17.223-17.223v-344.456c0-9.52-7.703-17.223-17.223-17.223zm-120.56 447.793h-310.01v-310.01h310.011v310.01zm103.337-103.337h-68.891v-223.896c0-9.52-7.703-17.223-17.223-17.223h-223.896v-68.891h310.011v310.01z"/> </svg>`; document.addEventListener('keydown', keyDownHandler); function keyDownHandler(event) { if (window.location.href === 'https://www.instagram.com/') return; if (event.altKey && event.key === 'k') { let buttons = document.getElementsByClassName('download-btn'); if (buttons.length > 0) { let mockEvent = { currentTarget: buttons[buttons.length - 1] }; if (attachLink) onm ouseInHandler(mockEvent); onClickHandler(mockEvent); } } if (event.altKey && event.key === 'i') { let buttons = document.getElementsByClassName('newtab-btn'); if (buttons.length > 0) { let mockEvent = { currentTarget: buttons[buttons.length - 1] }; if (attachLink) onm ouseInHandler(mockEvent); onClickHandler(mockEvent); } } if (event.altKey && event.key === 'l') { // right arrow let buttons = document.getElementsByClassName('coreSpriteRightChevron'); if (buttons.length > 0) { buttons[0].click(); } } if (event.altKey && event.key === 'j') { // left arrow let buttons = document.getElementsByClassName('coreSpriteLeftChevron'); if (buttons.length > 0) { buttons[0].click(); } } } var checkExistTimer = setInterval(function () { let sharePostSelector = "article section span button"; let storySeletor = "header button > div"; let profileSelector = "header section svg circle"; // check profile if (document.getElementsByClassName("custom-btn").length === 0) { if (document.querySelector(profileSelector)) { addCustomBtn(document.querySelector(profileSelector), "black", append2Header); } } // check story if (document.getElementsByClassName("custom-btn").length === 0) { if (document.querySelector(storySeletor)) { addCustomBtn(document.querySelector(storySeletor), "white", append2Post); } } // check post let articleList = document.querySelectorAll("article"); for (let i = 0; i < articleList.length; i++) { if (articleList[i].querySelector(sharePostSelector) && articleList[i].getElementsByClassName("custom-btn").length === 0) { addCustomBtn(articleList[i].querySelector(sharePostSelector), "black", append2Post); } } }, 500); function append2Header(node, btn) { node.parentNode.parentNode.parentNode.appendChild(btn, node.parentNode.parentNode); } function append2Post(node, btn) { node.parentNode.parentNode.appendChild(btn); } function addCustomBtn(node, iconColor, appendNode) { // add download button and set onclick handler // add newtab button let newtabBtn = createCustomBtn(svgNewtabBtn, iconColor, "newtab-btn", "16px"); appendNode(node, newtabBtn); // add download button let downloadBtn = createCustomBtn(svgDownloadBtn, iconColor, "download-btn", "14px"); appendNode(node, downloadBtn); } function createCustomBtn(svg, iconColor, className, marginLeft) { let newBtn = document.createElement("a"); newBtn.innerHTML = svg.replace('%color', iconColor); newBtn.setAttribute("class", "custom-btn " + className); newBtn.setAttribute("target", "_blank"); newBtn.setAttribute("style", "cursor: pointer;margin-left: " + marginLeft + ";margin-top: 8px;"); newBtn.onclick = onClickHandler; if (attachLink) newBtn.onmouseenter = onm ouseInHandler; if (className.includes("newtab")) { newBtn.setAttribute("title", "Open in new tab"); } else { newBtn.setAttribute("title", "Download"); } return newBtn; } function onClickHandler(e) { // handle button click let target = e.currentTarget; e.stopPropagation(); e.preventDefault(); if (window.location.pathname.includes('stories')) { storyOnClicked(target); } else if (document.querySelector('header') && document.querySelector('header').contains(target)) { profileOnClicked(target); } else { postOnClicked(target); } } function onm ouseInHandler(e) { let target = e.currentTarget; if (!attachLink) return; if (window.location.pathname.includes('stories')) { storyOnMouseIn(target); } else if (document.querySelector('header') && document.querySelector('header').contains(target)) { profileOnMouseIn(target); } else { postOnMouseIn(target); } } function profileOnMouseIn(target) { let url = profileGetUrl(target); target.setAttribute("href", url); } function profileOnClicked(target) { // extract profile picture url and download or open it let url = profileGetUrl(target); let filename = '.png'; if (url.length > 0) { // check url if (target.getAttribute("class").includes("download-btn")) { // generate filename let posterName = document.querySelector('header h2').textContent; filename = posterName + filename; downloadResource(url, filename); } else { // open url in new tab openResource(url); } } } function profileGetUrl(target) { let img = document.querySelector('header img'); let url = img.getAttribute('src'); return url; } async function postOnMouseIn(target) { let articleNode = postGetArticleNode(target); let url = await postGetUrl(target, articleNode); target.setAttribute("href", url); } async function postOnClicked(target) { // extract url from target post and download or open it let articleNode = postGetArticleNode(target); let url = await postGetUrl(target, articleNode); // ============================== // = download or open media url = // ============================== if (url.length > 0) { // check url if (target.getAttribute("class").includes("download-btn")) { let mediaName = url.split('?')[0].split('\\').pop().split('/').pop(); let ext = mediaName.substr(mediaName.lastIndexOf('.') + 1); mediaName = mediaName.substring(0, mediaName.lastIndexOf('.') + 1); let datetime = new Date(articleNode.querySelector('time').getAttribute('datetime')); datetime = yyyymmdd(datetime) + '_' + datetime.toTimeString().split(' ')[0].replace(/:/g, ''); let posterName = articleNode.querySelector('header a').getAttribute('href').replace(/\//g, ''); let filename = filenameFormat(postFilenameTemplate, posterName, datetime, mediaName, ext); downloadResource(url, filename); } else { // open url in new tab openResource(url); } } } function postGetArticleNode(target) { let articleNode = target; while (articleNode && articleNode.tagName !== "ARTICLE") { articleNode = articleNode.parentNode; } return articleNode; } async function postGetUrl(target, articleNode) { // meta[property="og:video"] let list = articleNode.querySelectorAll('li[style][class]'); let url = ""; if (list.length === 0) { // single img or video if (articleNode.querySelector('article div > video')) { let videoElem = articleNode.querySelector('article div > video'); url = videoElem.getAttribute('src'); if (videoElem.hasAttribute('videoURL')) { url = videoElem.getAttribute('videoURL'); } else if (url === null || url.includes('blob')) { url = await fetchVideoURL(articleNode, videoElem); } } else if (articleNode.querySelector('article div[role] div > img')) { url = articleNode.querySelector('article div[role] div > img').getAttribute('src'); } else { console.log("Err: not find media at handle post single"); } } else { // multiple imgs or videos let idx = 0; // check current index if (!articleNode.querySelector('.coreSpriteLeftChevron')) { idx = 0; } else if (!articleNode.querySelector('.coreSpriteRightChevron')) { idx = list.length - 1; } else idx = 1; let node = list[idx]; if (node.querySelector('video')) { let videoElem = node.querySelector('video'); url = videoElem.getAttribute('src'); if (videoElem.hasAttribute('videoURL')) { url = videoElem.getAttribute('videoURL'); } else if (url === null || url.includes('blob')) { url = await fetchVideoURL(articleNode, videoElem); } } else if (node.querySelector('img')) { url = node.querySelector('img').getAttribute('src'); } } return url } async function fetchVideoURL(articleNode, videoElem) { let poster = videoElem.getAttribute('poster'); let timeNodes = articleNode.querySelectorAll('time'); let posterUrl = timeNodes[timeNodes.length - 1].parentNode.href; let posterPattern = /\/([^\/?]*)\?/; let posterMatch = poster.match(posterPattern); let postFileName = posterMatch[1]; let pattern = new RegExp(`${postFileName}.*?video_url":("[^"]*")`, 's'); let resp = await fetch(posterUrl); let content = await resp.text(); let match = content.match(pattern); let videoUrl = JSON.parse(match[1]); videoUrl = videoUrl.replace(/^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/?\n]+)/g, 'https://scontent.cdninstagram.com'); videoElem.setAttribute('videoURL', videoUrl) return videoUrl; } function storyOnMouseIn(target) { let sectionNode = storyGetSectionNode(target); let url = storyGetUrl(target, sectionNode); target.setAttribute('href', url); } function storyOnClicked(target) { // extract url from target story and download or open it let sectionNode = storyGetSectionNode(target); let url = storyGetUrl(target, sectionNode); // ============================== // = download or open media url = // ============================== if (target.getAttribute("class").includes("download-btn")) { let mediaName = url.split('?')[0].split('\\').pop().split('/').pop(); let ext = mediaName.substr(mediaName.lastIndexOf('.') + 1); mediaName = mediaName.substring(0, mediaName.lastIndexOf('.') + 1); let datetime = new Date(sectionNode.querySelector('time').getAttribute('datetime')); datetime = yyyymmdd(datetime) + '_' + datetime.toTimeString().split(' ')[0].replace(/:/g, ''); let posterName = sectionNode.querySelector('header a').getAttribute('href').replace(/\//g, ''); let filename = filenameFormat(storyFilenameTemplate, posterName, datetime, mediaName, ext); downloadResource(url, filename); } else { // open url in new tab openResource(url); } } function storyGetSectionNode(target) { let sectionNode = target; while (sectionNode && sectionNode.tagName !== "SECTION") { sectionNode = sectionNode.parentNode; } return sectionNode; } function storyGetUrl(target, sectionNode) { let url = ""; if (sectionNode.querySelector('video > source')) { url = sectionNode.querySelector('video > source').getAttribute('src'); } else if (sectionNode.querySelector('img[decoding="sync"]')) { let img = sectionNode.querySelector('img[decoding="sync"]'); url = img.srcset.split(/ \d+w/g)[0].trim(); // extract first src from srcset attr. of img if (url.length > 0) { return url; } url = sectionNode.querySelector('img[decoding="sync"]').getAttribute('src'); } return url; } function filenameFormat(template, id, datetime, medianame, ext) { let filename = template; filename = filename.replaceAll("%id%", id); filename = filename.replaceAll("%datetime%", datetime); filename = filename.replaceAll("%medianame%", medianame); filename = filename.replaceAll("%ext%", ext); return filename; } function openResource(url) { // open url in new tab var a = document.createElement('a'); a.href = url; a.setAttribute("target", "_blank"); document.body.appendChild(a); a.click(); a.remove(); } function forceDownload(blob, filename) { // ref: https://www.zhaohespary.com var a = document.createElement('a'); a.download = filename; a.href = blob; // For Firefox https://stackoverflow.com/a/32226068 document.body.appendChild(a); a.click(); a.remove(); } // Current blob size limit is around 500MB for browsers function downloadResource(url, filename) { // ref: https://www.zhaohespary.com console.log(url); if (!filename) filename = url.split('\\').pop().split('/').pop(); fetch(url, { headers: new Headers({ 'Origin': location.origin }), mode: 'cors' }) .then(response => response.blob()) .then(blob => { let blobUrl = window.URL.createObjectURL(blob); forceDownload(blobUrl, filename); }) .catch(e => console.error(e)); } })();