Greasy Fork is available in English.

Instagram Download Button

Add the download button and the open button to download or open profile picture and media in the posts, stories, and highlights in Instagram

Version vom 01.04.2021. Aktuellste Version

  1. // ==UserScript==
  2. // @name Instagram Download Button
  3. // @name:zh-TW Instagram 下載器
  4. // @name:zh-CN Instagram 下载器
  5. // @name:ja Instagram ダウンローダー
  6. // @name:ko Instagram 다운로더
  7. // @name:es Descargador de Instagram
  8. // @name:fr Téléchargeur Instagram
  9. // @name:hi इंस्टाग्राम डाउनलोडर
  10. // @name:ru Загрузчик Instagram
  11. // @namespace https://github.com/y252328/Instagram_Download_Button
  12. // @version 1.9.4
  13. // @compatible chrome
  14. // @compatible firefox
  15. // @compatible edge
  16. // @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
  17. // @description:zh-TW 在Instagram頁面加入下載按鈕與開啟按鈕,透過這些按鈕可以下載或開啟大頭貼與貼文、限時動態、Highlight中的照片或影片
  18. // @description:zh-CN 在Instagram页面加入下载按钮与开启按钮,透过这些按钮可以下载或开启大头贴与贴文、限时动态、Highlight中的照片或影片
  19. // @description:ja メディアをダウンロードまたは開くためのボタンを追加します
  20. // @description:ko 미디어를 다운로드하거나 여는 버튼을 추가합니다
  21. // @description:es Agregue botones para descargar o abrir medios
  22. // @description:fr Ajoutez des boutons pour télécharger ou ouvrir des médias
  23. // @description:hi मीडिया को डाउनलोड या खोलने के लिए बटन जोड़ें।
  24. // @description:ru Добавьте кнопки для загрузки или открытия медиа
  25. // @author ZhiYu
  26. // @match https://www.instagram.com/*
  27. // @grant none
  28. // @license MIT
  29. // ==/UserScript==
  30.  
  31. (function () {
  32. 'use strict';
  33. // =================
  34. // = Options =
  35. // =================
  36. const attachLink = true; // add link into the button elements
  37. const postFilenameTemplate = "%id%-%datetime%-%medianame%.%ext%";
  38. const storyFilenameTemplate = postFilenameTemplate;
  39.  
  40. // ==================
  41.  
  42. function yyyymmdd(date) {
  43. // ref: https://stackoverflow.com/questions/3066586/get-string-in-yyyymmdd-format-from-js-date-object?page=1&tab=votes#tab-top
  44. var mm = date.getMonth() + 1; // getMonth() is zero-based
  45. var dd = date.getDate();
  46.  
  47. return [date.getFullYear(),
  48. (mm > 9 ? '' : '0') + mm,
  49. (dd > 9 ? '' : '0') + dd
  50. ].join('');
  51. }
  52.  
  53. var svgDownloadBtn =
  54. `<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"
  55. viewBox="0 0 477.867 477.867" style="fill:%color;" xml:space="preserve">
  56. <g>
  57. <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
  58. 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
  59. 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"/>
  60. </g>
  61. <g>
  62. <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
  63. 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
  64. 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
  65. C342.915,312.486,342.727,301.682,335.947,295.134z"/>
  66. </g>
  67. </svg>`;
  68.  
  69. var svgNewtabBtn =
  70. `<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">
  71. <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"/>
  72. </svg>`;
  73.  
  74. document.addEventListener('keydown', keyDownHandler);
  75.  
  76. function keyDownHandler(event) {
  77. if (window.location.href === 'https://www.instagram.com/') return;
  78.  
  79. if (event.altKey && event.key === 'k') {
  80. let buttons = document.getElementsByClassName('download-btn');
  81. if (buttons.length > 0) {
  82. let mockEvent = { currentTarget: buttons[buttons.length-1] };
  83. if (attachLink) onMouseInHandler(mockEvent);
  84. onClickHandler(mockEvent);
  85. }
  86. }
  87. if (event.altKey && event.key === 'i') {
  88. let buttons = document.getElementsByClassName('newtab-btn');
  89. if (buttons.length > 0) {
  90. let mockEvent = { currentTarget: buttons[buttons.length-1] };
  91. if (attachLink) onMouseInHandler(mockEvent);
  92. onClickHandler(mockEvent);
  93. }
  94. }
  95.  
  96. if (event.altKey && event.key === 'l') {
  97. // right arrow
  98. let buttons = document.getElementsByClassName('coreSpriteRightChevron');
  99. if (buttons.length > 0) {
  100. buttons[0].click();
  101. }
  102. }
  103.  
  104. if (event.altKey && event.key === 'j') {
  105. // left arrow
  106. let buttons = document.getElementsByClassName('coreSpriteLeftChevron');
  107. if (buttons.length > 0) {
  108. buttons[0].click();
  109. }
  110. }
  111. }
  112.  
  113. var checkExistTimer = setInterval(function () {
  114. let sharePostSelector = "article section span button";
  115. let menuSeletor = "header button > span";
  116. let storySeletor = "header button > span";
  117. let profileSelector = "header section svg circle";
  118.  
  119. // check story
  120. if (document.getElementsByClassName("custom-btn").length === 0) {
  121. if (document.querySelector(menuSeletor)) {
  122. addCustomBtn(document.querySelector(storySeletor), "white", append2Post);
  123. }
  124. }
  125.  
  126. // check post
  127. let articleList = document.querySelectorAll("article");
  128. for (let i = 0; i < articleList.length; i++) {
  129. if (articleList[i].querySelector(sharePostSelector) &&
  130. articleList[i].getElementsByClassName("custom-btn").length === 0) {
  131. addCustomBtn(articleList[i].querySelector(sharePostSelector), "black", append2Post);
  132. }
  133. }
  134.  
  135. // check profile
  136. if (document.getElementsByClassName("custom-btn").length === 0) {
  137. if (document.querySelector(profileSelector)) {
  138. addCustomBtn(document.querySelector(profileSelector), "black", append2Header);
  139. }
  140. }
  141. }, 500);
  142.  
  143. function append2Header(node, btn) {
  144. node.parentNode.parentNode.parentNode.appendChild(btn, node.parentNode.parentNode);
  145. }
  146.  
  147. function append2Post(node, btn) {
  148. node.parentNode.parentNode.appendChild(btn);
  149. }
  150.  
  151. function addCustomBtn(node, iconColor, appendNode) {
  152. // add download button and set onclick handler
  153. // add newtab button
  154. let newtabBtn = createCustomBtn(svgNewtabBtn, iconColor, "newtab-btn", "16px");
  155. appendNode(node, newtabBtn);
  156.  
  157. // add download button
  158. let downloadBtn = createCustomBtn(svgDownloadBtn, iconColor, "download-btn", "14px");
  159. appendNode(node, downloadBtn);
  160. }
  161.  
  162. function createCustomBtn(svg, iconColor, className, marginLeft) {
  163. let newBtn = document.createElement("a");
  164. newBtn.innerHTML = svg.replace('%color', iconColor);
  165. newBtn.setAttribute("class", "custom-btn " + className);
  166. newBtn.setAttribute("target", "_blank");
  167. newBtn.setAttribute("style", "cursor: pointer;margin-left: " + marginLeft + ";margin-top: 8px;");
  168. newBtn.onclick = onClickHandler;
  169. if (attachLink) newBtn.onmouseenter = onMouseInHandler;
  170. if (className.includes("newtab")) {
  171. newBtn.setAttribute("title", "Open in new tab");
  172. } else {
  173. newBtn.setAttribute("title", "Download");
  174. }
  175. return newBtn;
  176. }
  177.  
  178. function onClickHandler(e) {
  179. // handle button click
  180. let target = e.currentTarget;
  181. e.stopPropagation();
  182. e.preventDefault();
  183. if (window.location.pathname.includes('stories')) {
  184. storyOnClicked(target);
  185. } else if (document.querySelector('header') &&
  186. document.querySelector('header').contains(target)) {
  187. profileOnClicked(target);
  188. } else {
  189. postOnClicked(target);
  190. }
  191. }
  192.  
  193. function onMouseInHandler(e) {
  194. let target = e.currentTarget;
  195. if (!attachLink) return;
  196. if (window.location.pathname.includes('stories')) {
  197. storyOnMouseIn(target);
  198. } else if (document.querySelector('header') &&
  199. document.querySelector('header').contains(target)) {
  200. profileOnMouseIn(target);
  201. } else {
  202. postOnMouseIn(target);
  203. }
  204. }
  205.  
  206. function profileOnMouseIn(target) {
  207. let url = profileGetUrl(target);
  208. target.setAttribute("href", url);
  209. }
  210.  
  211. function profileOnClicked(target) {
  212. // extract profile picture url and download or open it
  213. let url = profileGetUrl(target);
  214. let filename = '.png';
  215.  
  216. if (url.length > 0) {
  217. // check url
  218. if (target.getAttribute("class").includes("download-btn")) {
  219. // generate filename
  220. let posterName = document.querySelector('header h2').textContent;
  221. filename = posterName + filename;
  222. downloadResource(url, filename);
  223. } else {
  224. // open url in new tab
  225. openResource(url);
  226. }
  227. }
  228. }
  229.  
  230. function profileGetUrl(target) {
  231. let img = document.querySelector('header img');
  232. let url = img.getAttribute('src');
  233. return url;
  234. }
  235.  
  236. async function postOnMouseIn(target) {
  237. let articleNode = postGetArticleNode(target);
  238. let url = await postGetUrl(target, articleNode);
  239. target.setAttribute("href", url);
  240. }
  241.  
  242. async function postOnClicked(target) {
  243. // extract url from target post and download or open it
  244. let articleNode = postGetArticleNode(target);
  245. let url = await postGetUrl(target, articleNode);
  246.  
  247. // ==============================
  248. // = download or open media url =
  249. // ==============================
  250. if (url.length > 0) {
  251. // check url
  252. if (target.getAttribute("class").includes("download-btn")) {
  253. let mediaName = url.split('?')[0].split('\\').pop().split('/').pop();
  254. let ext = mediaName.substr(mediaName.lastIndexOf('.') + 1);
  255. mediaName = mediaName.substring(0, mediaName.lastIndexOf('.') + 1);
  256. let datetime = new Date(articleNode.querySelector('time').getAttribute('datetime'));
  257. datetime = yyyymmdd(datetime) + '_' + datetime.toTimeString().split(' ')[0].replace(/:/g, '');
  258. let posterName = articleNode.querySelector('header a').getAttribute('href').replace(/\//g, '');
  259. let filename = filenameFormat(postFilenameTemplate, posterName, datetime, mediaName, ext);
  260. downloadResource(url, filename);
  261. } else {
  262. // open url in new tab
  263. openResource(url);
  264. }
  265. }
  266. }
  267.  
  268. function postGetArticleNode(target) {
  269. let articleNode = target;
  270. while (articleNode && articleNode.tagName !== "ARTICLE") {
  271. articleNode = articleNode.parentNode;
  272. }
  273. return articleNode;
  274. }
  275.  
  276. async function postGetUrl(target, articleNode) {
  277. // meta[property="og:video"]
  278. let list = articleNode.querySelectorAll('li[style][class]');
  279. let url = "";
  280. if (list.length === 0) {
  281. // single img or video
  282. if (articleNode.querySelector('article div > video')) {
  283. let videoElem = articleNode.querySelector('article div > video');
  284. url = videoElem.getAttribute('src');
  285. if (videoElem.hasAttribute('videoURL')) {
  286. url = videoElem.getAttribute('videoURL');
  287. } else if (url === null || url.includes('blob')) {
  288. url = await fetchVideoURL(articleNode, videoElem);
  289. }
  290. } else if (articleNode.querySelector('article div[role] div > img')) {
  291. url = articleNode.querySelector('article div[role] div > img').getAttribute('src');
  292. } else {
  293. console.log("Err: not find media at handle post single");
  294. }
  295. } else {
  296. // multiple imgs or videos
  297. let idx = 0;
  298. // check current index
  299. if (!articleNode.querySelector('.coreSpriteLeftChevron')) {
  300. idx = 0;
  301. } else if (!articleNode.querySelector('.coreSpriteRightChevron')) {
  302. idx = list.length - 1;
  303. } else idx = 1;
  304.  
  305. let node = list[idx];
  306. if (node.querySelector('video')) {
  307. let videoElem = node.querySelector('video');
  308. url = videoElem.getAttribute('src');
  309. if (videoElem.hasAttribute('videoURL')) {
  310. url = videoElem.getAttribute('videoURL');
  311. } else if (url === null || url.includes('blob')) {
  312. url = await fetchVideoURL(articleNode, videoElem);
  313. }
  314. } else if (node.querySelector('img')) {
  315. url = node.querySelector('img').getAttribute('src');
  316. }
  317. }
  318. return url
  319. }
  320.  
  321. async function fetchVideoURL(articleNode, videoElem) {
  322. let poster = videoElem.getAttribute('poster');
  323. let timeNodes = articleNode.querySelectorAll('time');
  324. let posterUrl = timeNodes[timeNodes.length-1].parentNode.href;
  325. let posterPattern = /\/([^\/?]*)\?/;
  326. let posterMatch = poster.match(posterPattern);
  327. let postFileName = posterMatch[1];
  328. let pattern = new RegExp(`${postFileName}.*?video_url":("[^"]*")`, 's');
  329. let resp = await fetch(posterUrl);
  330. let content = await resp.text();
  331. let match = content.match(pattern);
  332. let videoUrl = JSON.parse(match[1]);
  333. videoElem.setAttribute('videoURL', videoUrl)
  334. return videoUrl;
  335. }
  336.  
  337. function storyOnMouseIn(target) {
  338. let sectionNode = storyGetSectionNode(target);
  339. let url = storyGetUrl(target, sectionNode);
  340. target.setAttribute('href', url);
  341. }
  342.  
  343. function storyOnClicked(target) {
  344. // extract url from target story and download or open it
  345. let sectionNode = storyGetSectionNode(target);
  346. let url = storyGetUrl(target, sectionNode);
  347.  
  348. // ==============================
  349. // = download or open media url =
  350. // ==============================
  351. if (target.getAttribute("class").includes("download-btn")) {
  352. let mediaName = url.split('?')[0].split('\\').pop().split('/').pop();
  353. let ext = mediaName.substr(mediaName.lastIndexOf('.') + 1);
  354. mediaName = mediaName.substring(0, mediaName.lastIndexOf('.') + 1);
  355. let datetime = new Date(sectionNode.querySelector('time').getAttribute('datetime'));
  356. datetime = yyyymmdd(datetime) + '_' + datetime.toTimeString().split(' ')[0].replace(/:/g, '');
  357. let posterName = sectionNode.querySelector('header a').getAttribute('href').replace(/\//g, '');
  358.  
  359. let filename = filenameFormat(storyFilenameTemplate, posterName, datetime, mediaName, ext);
  360. downloadResource(url, filename);
  361. } else {
  362. // open url in new tab
  363. openResource(url);
  364. }
  365. }
  366.  
  367. function storyGetSectionNode(target) {
  368. let sectionNode = target;
  369. while (sectionNode && sectionNode.tagName !== "SECTION") {
  370. sectionNode = sectionNode.parentNode;
  371. }
  372. return sectionNode;
  373. }
  374.  
  375. function storyGetUrl(target, sectionNode) {
  376. let url = "";
  377. if (sectionNode.querySelector('video > source')) {
  378. url = sectionNode.querySelector('video > source').getAttribute('src');
  379. } else if (sectionNode.querySelector('img[decoding="sync"]')) {
  380. let img = sectionNode.querySelector('img[decoding="sync"]');
  381. url = img.srcset.split(/ \d+w/g)[0].trim(); // extract first src from srcset attr. of img
  382. if (url.length > 0) {
  383. return url;
  384. }
  385. url = sectionNode.querySelector('img[decoding="sync"]').getAttribute('src');
  386. }
  387. return url;
  388. }
  389.  
  390. function filenameFormat(template, id, datetime, medianame, ext) {
  391. let filename = template;
  392. filename = filename.replaceAll("%id%", id);
  393. filename = filename.replaceAll("%datetime%", datetime);
  394. filename = filename.replaceAll("%medianame%", medianame);
  395. filename = filename.replaceAll("%ext%", ext);
  396. return filename;
  397. }
  398.  
  399. function openResource(url) {
  400. // open url in new tab
  401. var a = document.createElement('a');
  402. a.href = url;
  403. a.setAttribute("target", "_blank");
  404. document.body.appendChild(a);
  405. a.click();
  406. a.remove();
  407. }
  408.  
  409. function forceDownload(blob, filename) {
  410. // ref: https://stackoverflow.com/questions/49474775/chrome-65-blocks-cross-origin-a-download-client-side-workaround-to-force-down
  411. var a = document.createElement('a');
  412. a.download = filename;
  413. a.href = blob;
  414. // For Firefox https://stackoverflow.com/a/32226068
  415. document.body.appendChild(a);
  416. a.click();
  417. a.remove();
  418. }
  419.  
  420. // Current blob size limit is around 500MB for browsers
  421. function downloadResource(url, filename) {
  422. // ref: https://stackoverflow.com/questions/49474775/chrome-65-blocks-cross-origin-a-download-client-side-workaround-to-force-down
  423. if (!filename) filename = url.split('\\').pop().split('/').pop();
  424. fetch(url, {
  425. headers: new Headers({
  426. 'Origin': location.origin
  427. }),
  428. mode: 'cors'
  429. })
  430. .then(response => response.blob())
  431. .then(blob => {
  432. let blobUrl = window.URL.createObjectURL(blob);
  433. forceDownload(blobUrl, filename);
  434. })
  435. .catch(e => console.error(e));
  436. }
  437. })();