Instagram 下載器

在Instagram頁面加入下載按鈕與開啟按鈕,透過這些按鈕可以下載或開啟大頭貼與貼文、限時動態、Highlight中的照片或影片

  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.17.18
  13. // @compatible chrome
  14. // @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
  15. // @description:zh-TW 在Instagram頁面加入下載按鈕與開啟按鈕,透過這些按鈕可以下載或開啟大頭貼與貼文、限時動態、Highlight中的照片或影片
  16. // @description:zh-CN 在Instagram页面加入下载按钮与开启按钮,透过这些按钮可以下载或开启大头贴与贴文、限时动态、Highlight中的照片或影片
  17. // @description:ja メディアをダウンロードまたは開くためのボタンを追加します
  18. // @description:ko 미디어를 다운로드하거나 여는 버튼을 추가합니다
  19. // @description:es Agregue botones para descargar o abrir medios
  20. // @description:fr Ajoutez des boutons pour télécharger ou ouvrir des médias
  21. // @description:hi मीडिया को डाउनलोड या खोलने के लिए बटन जोड़ें।
  22. // @description:ru Добавьте кнопки для загрузки или открытия медиа
  23. // @author ZhiYu
  24. // @match https://www.instagram.com/*
  25. // @icon https://www.google.com/s2/favicons?sz=64&domain=instagram.com
  26. // @grant none
  27. // @license MIT
  28. // ==/UserScript==
  29.  
  30. // TO-DO:
  31. // - replace the checking timer with the observer
  32.  
  33. (function () {
  34. 'use strict';
  35.  
  36. // =================
  37. // = Options =
  38. // =================
  39. // Old method is faster than new method, but not work or unable get highest resolution media sometime
  40. const disableNewUrlFetchMethod = false;
  41. const prefetchAndAttachLink = false; // prefetch and add link into the button elements
  42. const hoverToFetchAndAttachLink = true; // fetch and add link when hover the button
  43. const replaceJpegWithJpg = false;
  44. // === File name placeholders ===
  45. // %id% : the poster id
  46. // %datetime% : the media upload time
  47. // %medianame% : the original media file name
  48. // %postId% : the post id
  49. // %mediaIndex% : the media index in multiple-media posts
  50. const postFilenameTemplate = '%id%-%datetime%-%medianame%';
  51. const storyFilenameTemplate = postFilenameTemplate;
  52. // === Datetime placeholders ===
  53. // %y%: year (4 digits)
  54. // %m%: month (01-12)
  55. // %d%: day (01-31)
  56. // %H%: hour (00-23)
  57. // %M%: min (00-59)
  58. // %S%: sec (00-59)
  59. const datetimeTemplate = '%y%%m%%d%_%H%%M%%S%';
  60. // ==================
  61.  
  62. const postIdPattern = /^\/p\/([^/]+)\//;
  63. const postUrlPattern = /instagram\.com\/p\/[\w-]+\//;
  64.  
  65. 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"
  66. viewBox="0 0 477.867 477.867" style="fill:%color;" xml:space="preserve">
  67. <g>
  68. <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
  69. 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
  70. 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"/>
  71. </g>
  72. <g>
  73. <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
  74. 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
  75. 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
  76. C342.915,312.486,342.727,301.682,335.947,295.134z"/>
  77. </g>
  78. </svg>`;
  79.  
  80. 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">
  81. <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"/>
  82. </svg>`;
  83.  
  84. var preUrl = "";
  85.  
  86. document.addEventListener('keydown', keyDownHandler);
  87.  
  88. function keyDownHandler(event) {
  89. if (window.location.href === 'https://www.instagram.com/') return;
  90.  
  91. const mockEventTemplate = {
  92. stopPropagation: function () { },
  93. preventDefault: function () { }
  94. };
  95.  
  96. if (event.altKey && (event.code === 'KeyK' || event.key == 'k')) {
  97. let buttons = document.getElementsByClassName('download-btn');
  98. if (buttons.length > 0) {
  99. let mockEvent = { ...mockEventTemplate };
  100. mockEvent.currentTarget = buttons[buttons.length - 1];
  101. if (prefetchAndAttachLink || hoverToFetchAndAttachLink) onMouseInHandler(mockEvent);
  102. onClickHandler(mockEvent);
  103. }
  104. }
  105. if (event.altKey && (event.code === 'KeyI' || event.key == 'i')) {
  106. let buttons = document.getElementsByClassName('newtab-btn');
  107. if (buttons.length > 0) {
  108. let mockEvent = { ...mockEventTemplate };
  109. mockEvent.currentTarget = buttons[buttons.length - 1];
  110. if (prefetchAndAttachLink || hoverToFetchAndAttachLink) onMouseInHandler(mockEvent);
  111. onClickHandler(mockEvent);
  112. }
  113. }
  114.  
  115. if (event.altKey && (event.code === 'KeyL' || event.key == 'l')) {
  116. // right arrow
  117. let buttons = document.getElementsByClassName('_9zm2');
  118. if (buttons.length > 0) {
  119. buttons[0].click();
  120. }
  121. }
  122.  
  123. if (event.altKey && (event.code === 'KeyJ' || event.key == 'j')) {
  124. // left arrow
  125. let buttons = document.getElementsByClassName('_9zm0');
  126. if (buttons.length > 0) {
  127. buttons[0].click();
  128. }
  129. }
  130. }
  131.  
  132. function isPostPage() {
  133. return Boolean(window.location.href.match(postUrlPattern));
  134. }
  135.  
  136. function queryHas(root, selector, has) {
  137. let nodes = root.querySelectorAll(selector);
  138. for (let i = 0; i < nodes.length; ++i) {
  139. let currentNode = nodes[i];
  140. if (currentNode.querySelector(has)) {
  141. return currentNode;
  142. }
  143. }
  144. return null;
  145. }
  146.  
  147. var checkExistTimer = setInterval(function () {
  148. const curUrl = window.location.href;
  149. const savePostSelector = 'article *:not(li)>*>*>*>div:not([class])>div[role="button"]:not([style]):not([tabindex="-1"])';
  150. const storySelector = 'section > *:not(main) header div>svg:not([aria-label=""])';
  151. const profileSelector = 'header section svg circle';
  152. const playSvgPathSelector = 'path[d="M5.888 22.5a3.46 3.46 0 0 1-1.721-.46l-.003-.002a3.451 3.451 0 0 1-1.72-2.982V4.943a3.445 3.445 0 0 1 5.163-2.987l12.226 7.059a3.444 3.444 0 0 1-.001 5.967l-12.22 7.056a3.462 3.462 0 0 1-1.724.462Z"]';
  153. const pauseSvgPathSelector = 'path[d="M15 1c-3.3 0-6 1.3-6 3v40c0 1.7 2.7 3 6 3s6-1.3 6-3V4c0-1.7-2.7-3-6-3zm18 0c-3.3 0-6 1.3-6 3v40c0 1.7 2.7 3 6 3s6-1.3 6-3V4c0-1.7-2.7-3-6-3z"]';
  154.  
  155. let rgb = getComputedStyle(document.body).backgroundColor.match(/[.?\d]+/g);
  156. let iconColor = (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) <= 150 ? 'white' : 'black'
  157.  
  158. // clear all custom buttons when url changing
  159. if (preUrl !== curUrl) {
  160. while (document.getElementsByClassName('custom-btn').length !== 0) {
  161. document.getElementsByClassName('custom-btn')[0].remove();
  162. }
  163. }
  164.  
  165. // check post
  166. let articleList = document.querySelectorAll('article');
  167. for (let i = 0; i < articleList.length; i++) {
  168. let buttonAnchor = (Array.from(articleList[i].querySelectorAll(savePostSelector))).pop();
  169. if (buttonAnchor && articleList[i].getElementsByClassName('custom-btn').length === 0) {
  170. addCustomBtn(buttonAnchor, iconColor, append2Post);
  171. }
  172. }
  173.  
  174. // check independent post page
  175. if (isPostPage()) {
  176. let savebtn = queryHas(document, 'div[role="button"] > div[role="button"]:not([style])', 'polygon[points="20 21 12 13.44 4 21 4 3 20 3 20 21"]') || queryHas(document, 'div[role="button"] > div[role="button"]:not([style])', 'path[d="M20 22a.999.999 0 0 1-.687-.273L12 14.815l-7.313 6.912A1 1 0 0 1 3 21V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1Z"]');
  177. if (document.getElementsByClassName('custom-btn').length === 0) {
  178. if (savebtn.parentNode.querySelector('svg')) {
  179. addCustomBtn(savebtn.parentNode.querySelector('svg'), iconColor, append2IndependentPost);
  180. }
  181. }
  182. }
  183.  
  184. // check profile
  185. if (document.getElementsByClassName('custom-btn').length === 0 && !curUrl.includes("stor")) {
  186. if (document.querySelector(profileSelector)) {
  187. addCustomBtn(document.querySelector(profileSelector), iconColor, append2Header);
  188. }
  189. }
  190.  
  191. // check story
  192. if (document.getElementsByClassName('custom-btn').length === 0) {
  193. let playPauseSvg = queryHas(document, 'svg', playSvgPathSelector) || queryHas(document, 'svg', pauseSvgPathSelector);
  194. if (playPauseSvg) {
  195. let buttonDiv = playPauseSvg.parentNode;
  196. addCustomBtn(buttonDiv, 'white', append2Story);
  197. }
  198. }
  199.  
  200. preUrl = curUrl;
  201. }, 500);
  202.  
  203. function append2Post(node, btn) {
  204. node.append(btn);
  205. }
  206.  
  207. function append2IndependentPost(node, btn) {
  208. node.parentNode.parentNode.append(btn);
  209. }
  210.  
  211. function append2Header(node, btn) {
  212. node.parentNode.parentNode.parentNode.appendChild(btn, node.parentNode.parentNode);
  213. }
  214.  
  215. function append2Story(node, btn) {
  216. node.parentNode.parentNode.parentNode.append(btn);
  217. }
  218.  
  219. function addCustomBtn(node, iconColor, appendNode) {
  220. // add download button and set event handlers
  221. // add newtab button
  222. let newtabBtn = createCustomBtn(svgNewtabBtn, iconColor, 'newtab-btn', '16px');
  223. appendNode(node, newtabBtn);
  224.  
  225. // add download button
  226. let downloadBtn = createCustomBtn(svgDownloadBtn, iconColor, 'download-btn', '14px');
  227. appendNode(node, downloadBtn);
  228.  
  229. if (prefetchAndAttachLink) {
  230. onMouseInHandler({ currentTarget: newtabBtn });
  231. onMouseInHandler({ currentTarget: downloadBtn });
  232. }
  233. }
  234.  
  235. function createCustomBtn(svg, iconColor, className, marginLeft) {
  236. let newBtn = document.createElement('a');
  237. newBtn.innerHTML = svg.replace('%color', iconColor);
  238. newBtn.setAttribute('class', 'custom-btn ' + className);
  239. newBtn.setAttribute('target', '_blank');
  240. newBtn.setAttribute('style', 'cursor: pointer;margin-left: ' + marginLeft + ';margin-top: 8px;z-index: 999;');
  241. newBtn.onclick = onClickHandler;
  242. if (hoverToFetchAndAttachLink) newBtn.onmouseenter = onMouseInHandler;
  243. if (className.includes('newtab')) {
  244. newBtn.setAttribute('title', 'Open in new tab');
  245. } else {
  246. newBtn.setAttribute('title', 'Download');
  247. }
  248. return newBtn;
  249. }
  250.  
  251. function onClickHandler(e) {
  252. // handle button click
  253. let target = e.currentTarget;
  254. e.stopPropagation();
  255. e.preventDefault();
  256. if (window.location.pathname.includes('stories')) {
  257. storyOnClicked(target);
  258. } else if (document.querySelector('header') && document.querySelector('header').contains(target)) {
  259. profileOnClicked(target);
  260. } else {
  261. postOnClicked(target);
  262. }
  263. }
  264.  
  265. function onMouseInHandler(e) {
  266. let target = e.currentTarget;
  267. if (!prefetchAndAttachLink && !hoverToFetchAndAttachLink) return;
  268. if (window.location.pathname.includes('stories')) {
  269. storyOnMouseIn(target);
  270. } else if (document.querySelector('header') && document.querySelector('header').contains(target)) {
  271. profileOnMouseIn(target);
  272. } else {
  273. postOnMouseIn(target);
  274. }
  275. }
  276.  
  277. // ================================
  278. // ==== Profile ====
  279. // ================================
  280. function profileOnMouseIn(target) {
  281. let url = profileGetUrl(target);
  282. target.setAttribute('href', url);
  283. }
  284.  
  285. function profileOnClicked(target) {
  286. // extract profile picture url and download or open it
  287. let url = profileGetUrl(target);
  288.  
  289. if (url.length > 0) {
  290. // check url
  291. if (target.getAttribute('class').includes('download-btn')) {
  292. // generate filename
  293. const filename = document.querySelector('header h2').textContent;
  294. downloadResource(url, filename);
  295. } else {
  296. // open url in new tab
  297. openResource(url);
  298. }
  299. }
  300. }
  301.  
  302. function profileGetUrl(target) {
  303. let img = document.querySelector('header img');
  304. let url = img.getAttribute('src');
  305. return url;
  306. }
  307.  
  308. // ================================
  309. // ==== Post ====
  310. // ================================
  311. async function postOnMouseIn(target) {
  312. let articleNode = postGetArticleNode(target);
  313. let { url } = await postGetUrl(target, articleNode);
  314. target.setAttribute('href', url);
  315. }
  316.  
  317. async function postOnClicked(target) {
  318. try {
  319. // extract url from target post and download or open it
  320. let articleNode = postGetArticleNode(target);
  321. let { url, mediaIndex } = await postGetUrl(target, articleNode);
  322.  
  323. // download or open media url
  324. if (url.length > 0) {
  325. // check url
  326. if (target.getAttribute('class').includes('download-btn')) {
  327. let mediaName = url
  328. .split('?')[0]
  329. .split('\\')
  330. .pop()
  331. .split('/')
  332. .pop();
  333. mediaName = mediaName.substring(0, mediaName.lastIndexOf('.'));
  334. let datetime = new Date(articleNode.querySelector('time').getAttribute('datetime'));
  335. let posterName = articleNode.querySelector('header a') || findPostName(articleNode);
  336. posterName = posterName.getAttribute('href').replace(/\//g, '');
  337. let postId = findPostId(articleNode);
  338. let filename = filenameFormat(postFilenameTemplate, posterName, datetime, mediaName, postId, mediaIndex);
  339. downloadResource(url, filename);
  340. } else {
  341. // open url in new tab
  342. openResource(url);
  343. }
  344. }
  345. } catch (e) {
  346. console.log(`Uncatched in postOnClicked(): ${e}\n${e.stack}`);
  347. return null;
  348. }
  349. }
  350.  
  351. function postGetArticleNode(target) {
  352. let articleNode = target;
  353. while (articleNode && articleNode.tagName !== 'ARTICLE' && articleNode.tagName !== 'MAIN') {
  354. articleNode = articleNode.parentNode;
  355. }
  356. return articleNode;
  357. }
  358.  
  359. async function postGetUrl(target, articleNode) {
  360. // meta[property="og:video"]
  361. let list = articleNode.querySelectorAll('li[style][class]');
  362. let url = null;
  363. let mediaIndex = 0;
  364. if (list.length === 0) {
  365. // single img or video
  366. if (!disableNewUrlFetchMethod) url = await getUrlFromInfoApi(articleNode);
  367. if (url === null) {
  368. let videoElem = articleNode.querySelector('video');
  369. if (videoElem) {
  370. // media type is video
  371. url = videoElem.getAttribute('src');
  372. if (videoElem.hasAttribute('videoURL')) {
  373. url = videoElem.getAttribute('videoURL');
  374. } else if (url === null || url.includes('blob')) {
  375. url = await fetchVideoURL(articleNode, videoElem);
  376. }
  377. } else if (articleNode.querySelector('article div[role] div > img')) {
  378. // media type is image
  379. url = articleNode.querySelector('article div[role] div > img').getAttribute('src');
  380. } else {
  381. console.log('Err: not find media at handle post single');
  382. }
  383. }
  384. } else {
  385. // multiple imgs or videos
  386. const postView = location.pathname.startsWith('/p/');
  387. let dotsElements = [...articleNode.querySelectorAll(`div._acnb`)];
  388. mediaIndex = [...dotsElements].reduce((result, element, index) => (element.classList.length === 2 ? index : result), null);
  389. if (mediaIndex === null) throw 'Cannot find the media index';
  390.  
  391. if (!disableNewUrlFetchMethod) url = await getUrlFromInfoApi(articleNode, mediaIndex);
  392. if (url === null) {
  393. const listElements = [...articleNode.querySelectorAll(`:scope > div > div:nth-child(${postView ? 1 : 2}) > div > div:nth-child(1) ul li[style*="translateX"]`)];
  394. const listElementWidth = Math.max(...listElements.map(element => element.clientWidth));
  395.  
  396. const positionsMap = listElements.reduce((result, element) => {
  397. const position = Math.round(Number(element.style.transform.match(/-?(\d+)/)[1]) / listElementWidth);
  398. return { ...result, [position]: element };
  399. }, {});
  400.  
  401. const node = positionsMap[mediaIndex];
  402. if (node.querySelector('video')) {
  403. // media type is video
  404. let videoElem = node.querySelector('video');
  405. url = videoElem.getAttribute('src');
  406. if (videoElem.hasAttribute('videoURL')) {
  407. url = videoElem.getAttribute('videoURL');
  408. } else if (url === null || url.includes('blob')) {
  409. url = await fetchVideoURL(articleNode, videoElem);
  410. }
  411. } else if (node.querySelector('img')) {
  412. // media type is image
  413. url = node.querySelector('img').getAttribute('src');
  414. }
  415. }
  416. }
  417. return { url, mediaIndex };
  418. }
  419.  
  420. function findHighlightsIndex() {
  421. let currentDivProgressbarDiv = document.querySelector('div[style^="transform"]').parentElement;
  422. let progressbarRootDiv = currentDivProgressbarDiv.parentElement;
  423. let progressbarDivs = progressbarRootDiv.children;
  424. return Array.from(progressbarDivs).indexOf(currentDivProgressbarDiv);
  425. }
  426.  
  427. let infoCache = {}; // key: media id, value: info json
  428. let mediaIdCache = {}; // key: post id, value: media id
  429. async function getUrlFromInfoApi(articleNode, mediaIdx = 0) {
  430. // return media url if found else return null
  431. // fetch flow:
  432. // 1. find post id
  433. // 2. use step1 post id to send request to get post page
  434. // 3. find media id from the reponse text of step2
  435. // 4. find app id in clicked page
  436. // 5. send info api request with media id and app id
  437. // 6. get media url from response json
  438. try {
  439. const appIdPattern = /"X-IG-App-ID":"([\d]+)"/;
  440. const mediaIdPattern = /instagram:\/\/media\?id=(\d+)|["' ]media_id["' ]:["' ](\d+)["' ]/;
  441. function findAppId() {
  442. let bodyScripts = document.querySelectorAll("body > script");
  443. for (let i = 0; i < bodyScripts.length; ++i) {
  444. let match = bodyScripts[i].text.match(appIdPattern);
  445. if (match) return match[1];
  446. }
  447. console.log("Cannot find app id");
  448. return null;
  449. }
  450.  
  451. async function findMediaId() {
  452. // method 1: extract from url.
  453. function method1() {
  454. let href = window.location.href;
  455. let match = href.match(/www.instagram.com\/stories\/[^\/]+\/(\d+)/);
  456. if (!href.includes('highlights') && match) return match[1];
  457. }
  458.  
  459. // method 3
  460. async function method3() {
  461. let postId = await findPostId(articleNode);
  462. if (!postId) {
  463. return null;
  464. }
  465.  
  466. if (!(postId in mediaIdCache)) {
  467. let postUrl = `https://www.instagram.com/p/${postId}/`;
  468. let resp = await fetch(postUrl);
  469. let text = await resp.text();
  470. let idMatch = text ? text.match(mediaIdPattern) : [];
  471. let mediaId = null;
  472. for (let i = 0; i < idMatch.length; ++i) {
  473. if (idMatch[i]) mediaId = idMatch[i];
  474. }
  475. if (!mediaId) return null;
  476. mediaIdCache[postId] = mediaId;
  477. }
  478. return mediaIdCache[postId];
  479. }
  480.  
  481. function method2() {
  482. let scriptJson = document.querySelectorAll('script[type="application/json"]');
  483. for (let i = 0; i < scriptJson.length; i++) {
  484. let match = scriptJson[i].text.match(/"pk":"(\d+)","id":"[\d_]+"/);
  485. if (match) {
  486. if (!window.location.href.includes('highlights')) {
  487. return match[1];
  488. }
  489. let matchs = Array.from(scriptJson[i].text.matchAll(/"pk":"(\d+)","id":"[\d_]+"/g), match => match[1]);
  490. const matchIndex = findHighlightsIndex();
  491. if (matchs.length > matchIndex) {
  492. return matchs[matchIndex];
  493. }
  494. }
  495. }
  496. }
  497.  
  498. return method1() || await method3() || method2();
  499. }
  500.  
  501. function getImgOrVedioUrl(item) {
  502. if ("video_versions" in item) {
  503. return item.video_versions[0].url;
  504. } else {
  505. return item.image_versions2.candidates[0].url;
  506. }
  507. }
  508.  
  509. let appId = findAppId();
  510. if (!appId) return null;
  511. let headers = {
  512. method: 'GET',
  513. headers: {
  514. Accept: '*/*',
  515. 'X-IG-App-ID': appId
  516. },
  517. credentials: 'include',
  518. mode: 'cors'
  519. };
  520.  
  521. let mediaId = await findMediaId();
  522. if (!mediaId) {
  523. console.log("Cannot find media id");
  524. return null;
  525. }
  526. if (!(mediaId in infoCache)) {
  527. let url = 'https://i.instagram.com/api/v1/media/' + mediaId + '/info/';
  528. let resp = await fetch(url, headers);
  529. if (resp.status !== 200) {
  530. console.log(`Fetch info API failed with status code: ${resp.status}`);
  531. return null;
  532. }
  533. let respJson = await resp.json();
  534. infoCache[mediaId] = respJson;
  535. }
  536. let infoJson = infoCache[mediaId];
  537. if ('carousel_media' in infoJson.items[0]) {
  538. // multi-media post
  539. return getImgOrVedioUrl(infoJson.items[0].carousel_media[mediaIdx]);
  540. } else {
  541. // single media post
  542. return getImgOrVedioUrl(infoJson.items[0]);
  543. }
  544. } catch (e) {
  545. console.log(`Uncatched in getUrlFromInfoApi(): ${e}\n${e.stack}`);
  546. return null;
  547. }
  548. }
  549.  
  550. function findPostName(articleNode) {
  551. // this grabs the username link that is visually in the author's post comment below the media
  552. // 'article section' includes the likes section and comment box
  553. // '+ * a' pulls the first element after the section that contains a link (comment box doesn't)
  554. // '[href^="/"][href$="/"]' requires the href attribute to begin and end with a slash to match a username
  555. let imgNoCanvas = articleNode.querySelector('article section + * a[href^="/"][href$="/"]');
  556. if (imgNoCanvas) {
  557. return imgNoCanvas;
  558. }
  559.  
  560. // videos are handled differently
  561. let imgAlt = articleNode.querySelector('canvas ~ * img');
  562. if (imgAlt) {
  563. imgAlt = imgAlt.getAttribute('alt');
  564. let links = articleNode.querySelectorAll('a');
  565. for (let i = 0; i < links.length; i++) {
  566. const posterName = links[i].getAttribute('href').replace(/\//g, '');
  567. if (imgAlt.includes(posterName)) {
  568. return links[i];
  569. }
  570. }
  571. } else {
  572. // first H2 with a direction set
  573. const el = document.querySelector('h2[dir]');
  574. return el.innerText;
  575. }
  576. }
  577.  
  578. function findPostId(articleNode) {
  579. let aNodes = articleNode.querySelectorAll('a');
  580. for (let i = 0; i < aNodes.length; ++i) {
  581. let link = aNodes[i].getAttribute('href');
  582. if (link) {
  583. let match = link.match(postIdPattern);
  584. if (match) return match[1];
  585. }
  586. }
  587. return null;
  588. }
  589.  
  590. async function fetchVideoURL(articleNode, videoElem) {
  591. let poster = videoElem.getAttribute('poster');
  592. let timeNodes = articleNode.querySelectorAll('time');
  593. // special thanks 孙年忠 (https://gf.qytechs.cn/en/scripts/406535-instagram-download-button/discussions/120159)
  594. let posterUrl = timeNodes[timeNodes.length - 1].parentNode.parentNode.href;
  595. const posterPattern = /\/([^\/?]*)\?/;
  596. let posterMatch = poster.match(posterPattern);
  597. let postFileName = posterMatch[1];
  598. let resp = await fetch(posterUrl);
  599. let content = await resp.text();
  600. // special thanks to 孙年忠 for the pattern (https://gf.qytechs.cn/zh-TW/scripts/406535-instagram-download-button/discussions/116675)
  601. const pattern = new RegExp(`${postFileName}.*?video_versions.*?url":("[^"]*")`, 's');
  602. let match = content.match(pattern);
  603. let videoUrl = JSON.parse(match[1]);
  604. videoUrl = videoUrl.replace(/^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/?\n]+)/g, 'https://scontent.cdninstagram.com');
  605. videoElem.setAttribute('videoURL', videoUrl);
  606. return videoUrl;
  607. }
  608.  
  609. // ================================
  610. // ==== Story & Highlight ====
  611. // ================================
  612. async function storyOnMouseIn(target) {
  613. let sectionNode = storyGetSectionNode(target);
  614. let url = await storyGetUrl(target, sectionNode);
  615. target.setAttribute('href', url);
  616. }
  617.  
  618. async function storyOnClicked(target) {
  619. // extract url from target story and download or open it
  620. let sectionNode = storyGetSectionNode(target);
  621. let url = await storyGetUrl(target, sectionNode);
  622. const posterUrlPat = /\/stories\/(.*)\/.*\//
  623. // download or open media url
  624. if (target.getAttribute('class').includes('download-btn')) {
  625. let mediaName = url.split('?')[0].split('\\').pop().split('/').pop();
  626. mediaName = mediaName.substring(0, mediaName.lastIndexOf('.'));
  627. let datetime = new Date(sectionNode.querySelector('time').getAttribute('datetime'));
  628. let posterName = "unkown";
  629. // method 1
  630. const posterNameHeader = sectionNode.querySelector('header a');
  631. if (posterNameHeader) {
  632. posterName = posterNameHeader.getAttribute('href').replace(/\//g, '');
  633. }
  634.  
  635. // method 2
  636. if (posterName === "unkown") {
  637. const match = window.location.pathname.match(posterUrlPat);
  638. if (match) {
  639. posterName = match[1];
  640. }
  641. }
  642. let filename = filenameFormat(storyFilenameTemplate, posterName, datetime, mediaName);
  643. downloadResource(url, filename);
  644. } else {
  645. // open url in new tab
  646. openResource(url);
  647. }
  648. }
  649.  
  650. function storyGetSectionNode(target) {
  651. let sectionNode = target;
  652. while (sectionNode && sectionNode.tagName !== 'SECTION') {
  653. sectionNode = sectionNode.parentNode;
  654. }
  655. return sectionNode;
  656. }
  657.  
  658. async function storyGetUrl(target, sectionNode) {
  659. let url = null;
  660. if (!disableNewUrlFetchMethod) url = await getUrlFromInfoApi(target);
  661.  
  662. if (!url) {
  663. if (sectionNode.querySelector('video > source')) {
  664. url = sectionNode.querySelector('video > source').getAttribute('src');
  665. } else if (sectionNode.querySelector('img[decoding="sync"]')) {
  666. let img = sectionNode.querySelector('img[decoding="sync"]');
  667. url = img.srcset.split(/ \d+w/g)[0].trim(); // extract first src from srcset attr. of img
  668. if (url.length > 0) {
  669. return url;
  670. }
  671. url = sectionNode.querySelector('img[decoding="sync"]').getAttribute('src');
  672. } else if (sectionNode.querySelector('video')) {
  673. url = sectionNode.querySelector('video').getAttribute('src');
  674. }
  675. }
  676. return url;
  677. }
  678.  
  679. function filenameFormat(template, id, datetime, medianame, postId = +new Date(), mediaIndex = '0') {
  680. let filename = template;
  681. filename = filename.replace(/%id%/g, id);
  682. filename = filename.replace(/%datetime%/g, datetimeFormat(datetimeTemplate, datetime));
  683. filename = filename.replace(/%medianame%/g, medianame);
  684. filename = filename.replace(/%postId%/g, postId);
  685. filename = filename.replace(/%mediaIndex%/g, mediaIndex);
  686. return filename;
  687. }
  688.  
  689. function datetimeFormat(template, datetime) {
  690. let datetimeStr = template;
  691. datetimeStr = datetimeStr.replace(/%y%/g, datetime.getFullYear());
  692. datetimeStr = datetimeStr.replace(/%m%/g, fillZero((datetime.getMonth() + 1).toString()));
  693. datetimeStr = datetimeStr.replace(/%d%/g, fillZero(datetime.getDate().toString()));
  694. datetimeStr = datetimeStr.replace(/%H%/g, fillZero(datetime.getHours().toString()));
  695. datetimeStr = datetimeStr.replace(/%M%/g, fillZero(datetime.getMinutes().toString()));
  696. datetimeStr = datetimeStr.replace(/%S%/g, fillZero(datetime.getSeconds().toString()));
  697. return datetimeStr;
  698. }
  699.  
  700. function fillZero(str) {
  701. if (str.length === 1) {
  702. return '0' + str;
  703. }
  704. return str;
  705. }
  706.  
  707. function openResource(url) {
  708. // open url in new tab
  709. var a = document.createElement('a');
  710. a.href = url;
  711. a.setAttribute('target', '_blank');
  712. document.body.appendChild(a);
  713. a.click();
  714. a.remove();
  715. }
  716.  
  717. function forceDownload(blob, filename, extension) {
  718. // ref: https://stackoverflow.com/questions/49474775/chrome-65-blocks-cross-origin-a-download-client-side-workaround-to-force-down
  719. var a = document.createElement('a');
  720. if (replaceJpegWithJpg) extension = extension.replace('jpeg', 'jpg');
  721. a.download = filename + '.' + extension;
  722. a.href = blob;
  723. // For Firefox https://stackoverflow.com/a/32226068
  724. document.body.appendChild(a);
  725. a.click();
  726. a.remove();
  727. }
  728.  
  729. // Current blob size limit is around 500MB for browsers
  730. function downloadResource(url, filename) {
  731. if (url.startsWith('blob:')) {
  732. forceDownload(url, filename, 'mp4');
  733. return;
  734. }
  735. console.log(`Dowloading ${url}`);
  736. // ref: https://stackoverflow.com/questions/49474775/chrome-65-blocks-cross-origin-a-download-client-side-workaround-to-force-down
  737. if (!filename) {
  738. filename = url
  739. .split('\\')
  740. .pop()
  741. .split('/')
  742. .pop();
  743. }
  744. fetch(url, {
  745. headers: new Headers({
  746. 'User-Agent': window.navigator.userAgent,
  747. Origin: location.origin,
  748. }),
  749. mode: 'cors',
  750. })
  751. .then(response => response.blob())
  752. .then(blob => {
  753. const extension = blob.type.split('/').pop();
  754. let blobUrl = window.URL.createObjectURL(blob);
  755. forceDownload(blobUrl, filename, extension);
  756. })
  757. .catch(e => console.error(e));
  758. }
  759. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址