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

Per 10-05-2023. Zie de nieuwste versie.

  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.16.2
  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. // @icon https://www.google.com/s2/favicons?sz=64&domain=instagram.com
  28. // @grant none
  29. // @license MIT
  30. // ==/UserScript==
  31.  
  32. (function () {
  33. 'use strict';
  34.  
  35. // =================
  36. // = Options =
  37. // =================
  38. // Old method is faster than new method, but not work or unable get highest resolution media sometime
  39. const disableNewUrlFetchMethod = false;
  40. const prefetchAndAttachLink = false; // add link into the button elements
  41. const replaceJpegWithJpg = false;
  42. // === File name placeholders ===
  43. // %id% : the poster id
  44. // %datetime% : the media upload time
  45. // %medianame% : the original media file name
  46. // %postId% : the post id
  47. // %mediaIndex% : the media index in multiple-media posts
  48. const postFilenameTemplate = '%id%-%datetime%-%medianame%';
  49. const storyFilenameTemplate = postFilenameTemplate;
  50. // === Datetime placeholders ===
  51. // %y%: year (4 digits)
  52. // %m%: month (01-12)
  53. // %d%: day (01-31)
  54. // %H%: hour (00-23)
  55. // %M%: min (00-59)
  56. // %S%: sec (00-59)
  57. const datetimeTemplate = '%y%%m%%d%_%H%%M%%S%';
  58. // ==================
  59.  
  60. const postIdPattern = /^\/p\/([^/]+)\/$/;
  61.  
  62. 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"
  63. viewBox="0 0 477.867 477.867" style="fill:%color;" xml:space="preserve">
  64. <g>
  65. <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
  66. 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
  67. 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"/>
  68. </g>
  69. <g>
  70. <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
  71. 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
  72. 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
  73. C342.915,312.486,342.727,301.682,335.947,295.134z"/>
  74. </g>
  75. </svg>`;
  76.  
  77. 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">
  78. <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"/>
  79. </svg>`;
  80.  
  81. document.addEventListener('keydown', keyDownHandler);
  82.  
  83. function keyDownHandler(event) {
  84. if (window.location.href === 'https://www.instagram.com/') return;
  85.  
  86. const mockEventTemplate = {
  87. stopPropagation: function () { },
  88. preventDefault: function () { }
  89. };
  90.  
  91. if (event.altKey && event.key === 'k') {
  92. let buttons = document.getElementsByClassName('download-btn');
  93. if (buttons.length > 0) {
  94. let mockEvent = { ...mockEventTemplate };
  95. mockEvent.currentTarget = buttons[buttons.length - 1];
  96. if (prefetchAndAttachLink) onMouseInHandler(mockEvent);
  97. onClickHandler(mockEvent);
  98. }
  99. }
  100. if (event.altKey && event.key === 'i') {
  101. let buttons = document.getElementsByClassName('newtab-btn');
  102. if (buttons.length > 0) {
  103. let mockEvent = { ...mockEventTemplate };
  104. mockEvent.currentTarget = buttons[buttons.length - 1];
  105. if (prefetchAndAttachLink) onMouseInHandler(mockEvent);
  106. onClickHandler(mockEvent);
  107. }
  108. }
  109.  
  110. if (event.altKey && event.key === 'l') {
  111. // right arrow
  112. let buttons = document.getElementsByClassName('coreSpriteRightChevron');
  113. if (buttons.length > 0) {
  114. buttons[0].click();
  115. }
  116. }
  117.  
  118. if (event.altKey && event.key === 'j') {
  119. // left arrow
  120. let buttons = document.getElementsByClassName('coreSpriteLeftChevron');
  121. if (buttons.length > 0) {
  122. buttons[0].click();
  123. }
  124. }
  125. }
  126.  
  127. var checkExistTimer = setInterval(function () {
  128. let sharePostSelector = 'article section span button';
  129. let storySelector = 'header button > div';
  130. let profileSelector = 'header section svg circle';
  131. // Thanks for Jenie providing color check code
  132. // https://greasyfork.org/zh-TW/scripts/406535-instagram-download-button/discussions/122185
  133. let iconColor = getComputedStyle(document.body).backgroundColor === 'rgb(0, 0, 0)' ? 'white' : 'black';
  134.  
  135. // check profile
  136. if (document.getElementsByClassName('custom-btn').length === 0) {
  137. if (document.querySelector(profileSelector)) {
  138. addCustomBtn(document.querySelector(profileSelector), iconColor, append2Header);
  139. }
  140. }
  141.  
  142. // check post
  143. let articleList = document.querySelectorAll('article');
  144. for (let i = 0; i < articleList.length; i++) {
  145. if (articleList[i].querySelector(sharePostSelector) && articleList[i].getElementsByClassName('custom-btn').length === 0) {
  146. addCustomBtn(articleList[i].querySelector(sharePostSelector), iconColor, append2Post);
  147. }
  148. }
  149.  
  150. // check story
  151. if (document.getElementsByClassName('custom-btn').length === 0) {
  152. if (document.querySelector(storySelector)) {
  153. addCustomBtn(document.querySelector(storySelector), 'white', append2Post);
  154. }
  155. }
  156. }, 500);
  157.  
  158. function append2Header(node, btn) {
  159. node.parentNode.parentNode.parentNode.appendChild(btn, node.parentNode.parentNode);
  160. }
  161.  
  162. function append2Post(node, btn) {
  163. node.parentNode.parentNode.appendChild(btn);
  164. }
  165.  
  166. function addCustomBtn(node, iconColor, appendNode) {
  167. // add download button and set event handlers
  168. // add newtab button
  169. let newtabBtn = createCustomBtn(svgNewtabBtn, iconColor, 'newtab-btn', '16px');
  170. appendNode(node, newtabBtn);
  171.  
  172. // add download button
  173. let downloadBtn = createCustomBtn(svgDownloadBtn, iconColor, 'download-btn', '14px');
  174. appendNode(node, downloadBtn);
  175.  
  176. if (prefetchAndAttachLink) {
  177. onMouseInHandler({ currentTarget: newtabBtn });
  178. onMouseInHandler({ currentTarget: downloadBtn });
  179. }
  180. }
  181.  
  182. function createCustomBtn(svg, iconColor, className, marginLeft) {
  183. let newBtn = document.createElement('a');
  184. newBtn.innerHTML = svg.replace('%color', iconColor);
  185. newBtn.setAttribute('class', 'custom-btn ' + className);
  186. newBtn.setAttribute('target', '_blank');
  187. newBtn.setAttribute('style', 'cursor: pointer;margin-left: ' + marginLeft + ';margin-top: 8px;z-index: 999;');
  188. newBtn.onclick = onClickHandler;
  189. if (prefetchAndAttachLink) newBtn.onmouseenter = onMouseInHandler;
  190. if (className.includes('newtab')) {
  191. newBtn.setAttribute('title', 'Open in new tab');
  192. } else {
  193. newBtn.setAttribute('title', 'Download');
  194. }
  195. return newBtn;
  196. }
  197.  
  198. function onClickHandler(e) {
  199. // handle button click
  200. let target = e.currentTarget;
  201. e.stopPropagation();
  202. e.preventDefault();
  203. if (window.location.pathname.includes('stories')) {
  204. storyOnClicked(target);
  205. } else if (document.querySelector('header') && document.querySelector('header').contains(target)) {
  206. profileOnClicked(target);
  207. } else {
  208. postOnClicked(target);
  209. }
  210. }
  211.  
  212. function onMouseInHandler(e) {
  213. let target = e.currentTarget;
  214. if (!prefetchAndAttachLink) return;
  215. if (window.location.pathname.includes('stories')) {
  216. storyOnMouseIn(target);
  217. } else if (document.querySelector('header') && document.querySelector('header').contains(target)) {
  218. profileOnMouseIn(target);
  219. } else {
  220. postOnMouseIn(target);
  221. }
  222. }
  223.  
  224. // ================================
  225. // ==== Profile ====
  226. // ================================
  227. function profileOnMouseIn(target) {
  228. let url = profileGetUrl(target);
  229. target.setAttribute('href', url);
  230. }
  231.  
  232. function profileOnClicked(target) {
  233. // extract profile picture url and download or open it
  234. let url = profileGetUrl(target);
  235.  
  236. if (url.length > 0) {
  237. // check url
  238. if (target.getAttribute('class').includes('download-btn')) {
  239. // generate filename
  240. const filename = document.querySelector('header h2').textContent;
  241. downloadResource(url, filename);
  242. } else {
  243. // open url in new tab
  244. openResource(url);
  245. }
  246. }
  247. }
  248.  
  249. function profileGetUrl(target) {
  250. let img = document.querySelector('header img');
  251. let url = img.getAttribute('src');
  252. return url;
  253. }
  254.  
  255. // ================================
  256. // ==== Post ====
  257. // ================================
  258. async function postOnMouseIn(target) {
  259. let articleNode = postGetArticleNode(target);
  260. let { url } = await postGetUrl(target, articleNode);
  261. target.setAttribute('href', url);
  262. }
  263.  
  264. async function postOnClicked(target) {
  265. try {
  266. // extract url from target post and download or open it
  267. let articleNode = postGetArticleNode(target);
  268. let { url, mediaIndex } = await postGetUrl(target, articleNode);
  269.  
  270. // download or open media url
  271. if (url.length > 0) {
  272. // check url
  273. if (target.getAttribute('class').includes('download-btn')) {
  274. let mediaName = url
  275. .split('?')[0]
  276. .split('\\')
  277. .pop()
  278. .split('/')
  279. .pop();
  280. mediaName = mediaName.substring(0, mediaName.lastIndexOf('.'));
  281. let datetime = new Date(articleNode.querySelector('time').getAttribute('datetime'));
  282. let posterName = articleNode
  283. .querySelector('header a')
  284. .getAttribute('href')
  285. .replace(/\//g, '');
  286. let postId = findPostId(articleNode);
  287. let filename = filenameFormat(postFilenameTemplate, posterName, datetime, mediaName, postId, mediaIndex);
  288. downloadResource(url, filename);
  289. } else {
  290. // open url in new tab
  291. openResource(url);
  292. }
  293. }
  294. } catch (e) {
  295. console.log(`Uncatched in postOnClicked(): ${e}\n${e.stack}`);
  296. return null;
  297. }
  298. }
  299.  
  300. function postGetArticleNode(target) {
  301. let articleNode = target;
  302. while (articleNode && articleNode.tagName !== 'ARTICLE') {
  303. articleNode = articleNode.parentNode;
  304. }
  305. return articleNode;
  306. }
  307.  
  308. async function postGetUrl(target, articleNode) {
  309. // meta[property="og:video"]
  310. let list = articleNode.querySelectorAll('li[style][class]');
  311. let url = null;
  312. let mediaIndex = 0;
  313. if (list.length === 0) {
  314. // single img or video
  315. if (!disableNewUrlFetchMethod) url = await getUrlFromInfoApi(articleNode);
  316. if (url === null) {
  317. if (articleNode.querySelector('article div > video')) {
  318. // media type is video
  319. let videoElem = articleNode.querySelector('article div > video');
  320. url = videoElem.getAttribute('src');
  321. if (videoElem.hasAttribute('videoURL')) {
  322. url = videoElem.getAttribute('videoURL');
  323. } else if (url === null || url.includes('blob')) {
  324. url = await fetchVideoURL(articleNode, videoElem);
  325. }
  326. } else if (articleNode.querySelector('article div[role] div > img')) {
  327. // media type is image
  328. url = articleNode.querySelector('article div[role] div > img').getAttribute('src');
  329. } else {
  330. console.log('Err: not find media at handle post single');
  331. }
  332. }
  333. } else {
  334. // multiple imgs or videos
  335. const postView = location.pathname.startsWith('/p/');
  336. let dotsElements = [...articleNode.querySelector(`:scope > div > div:nth-child(${postView ? 1 : 2}) > div > div:nth-child(2)`).children];
  337. let mediaIndex = [...dotsElements].reduce((result, element, index) => (element.classList.length === 2 ? index : result), null);
  338. if (mediaIndex === null) {
  339. dotsElements = [...articleNode.querySelector(`:scope > div > div:nth-child(${!postView ? 1 : 2}) > div > div:nth-child(2)`).children];
  340. mediaIndex = [...dotsElements].reduce((result, element, index) => (element.classList.length === 2 ? index : result), null);
  341. }
  342. if (mediaIndex === null) throw 'Cannot find the media index';
  343.  
  344. if (!disableNewUrlFetchMethod) url = await getUrlFromInfoApi(articleNode, mediaIndex);
  345. if (url === null) {
  346. const listElements = [...articleNode.querySelectorAll(`:scope > div > div:nth-child(${postView ? 1 : 2}) > div > div:nth-child(1) ul li[style*="translateX"]`)];
  347. const listElementWidth = Math.max(...listElements.map(element => element.clientWidth));
  348.  
  349. const positionsMap = listElements.reduce((result, element) => {
  350. // console.log(Number(element.style.transform.match(/-?(\d+)/)[1]));
  351. const position = Math.round(Number(element.style.transform.match(/-?(\d+)/)[1]) / listElementWidth);
  352. return { ...result, [position]: element };
  353. }, {});
  354.  
  355. const node = positionsMap[mediaIndex];
  356. if (node.querySelector('video')) {
  357. // media type is video
  358. let videoElem = node.querySelector('video');
  359. url = videoElem.getAttribute('src');
  360. if (videoElem.hasAttribute('videoURL')) {
  361. url = videoElem.getAttribute('videoURL');
  362. } else if (url === null || url.includes('blob')) {
  363. url = await fetchVideoURL(articleNode, videoElem);
  364. }
  365. } else if (node.querySelector('img')) {
  366. // media type is image
  367. url = node.querySelector('img').getAttribute('src');
  368. }
  369. }
  370. }
  371. return { url, mediaIndex };
  372. }
  373.  
  374. let infoCache = {}; // key: media id, value: info json
  375. let mediaIdCache = {}; // key: post id, value: media id
  376. async function getUrlFromInfoApi(articleNode, mediaIdx = 0) {
  377. // return media url if found else return null
  378. // fetch flow:
  379. // 1. find post id
  380. // 2. use step1 post id to send request to get post page
  381. // 3. find media id from the reponse text of step2
  382. // 4. find app id in clicked page
  383. // 5. send info api request with media id and app id
  384. // 6. get media url from response json
  385. try {
  386. const appIdPattern = /"X-IG-App-ID":"([\d]+)"/
  387. const mediaIdPattern = /instagram:\/\/media\?id=(\d+)|["' ]media_id["' ]:["' ](\d+)["' ]/
  388. function findAppId() {
  389. let bodyScripts = document.querySelectorAll("body > script");
  390. for (let i = 0; i < bodyScripts.length; ++i) {
  391. let match = bodyScripts[i].text.match(appIdPattern);
  392. if (match) return match[1];
  393. }
  394. console.log("Cannot find app id");
  395. return null;
  396. }
  397.  
  398. async function findMediaId() {
  399. let match = window.location.href.match(/www.instagram.com\/stories\/[^\/]+\/(\d+)/)
  400. if (match) return match[1];
  401.  
  402. let postId = await findPostId(articleNode);
  403. if (!postId) {
  404. console.log("Cannot find post id");
  405. return null;
  406. }
  407. if (!(postId in mediaIdCache)) {
  408. let postUrl = `https://www.instagram.com/p/${postId}/`;
  409. let resp = await fetch(postUrl);
  410. let text = await resp.text();
  411. let idMatch = text.match(mediaIdPattern);
  412. let mediaId = null;
  413. for (let i = 0; i < idMatch.length; ++i) {
  414. if (idMatch[i]) mediaId = idMatch[i];
  415. }
  416. if (!mediaId) return null;
  417. mediaIdCache[postId] = mediaId;
  418. }
  419. return mediaIdCache[postId];
  420. }
  421.  
  422. function getImgOrVedioUrl(item) {
  423. if ("video_versions" in item) {
  424. return item.video_versions[0].url;
  425. } else {
  426. return item.image_versions2.candidates[0].url;
  427. }
  428. }
  429.  
  430. let appId = findAppId();
  431. if (!appId) return null;
  432. let headers = {
  433. method: 'GET',
  434. headers: {
  435. Accept: '*/*',
  436. 'X-IG-App-ID': appId
  437. },
  438. credentials: 'include',
  439. mode: 'cors'
  440. };
  441.  
  442. let mediaId = await findMediaId();
  443. if (!mediaId) {
  444. console.log("Cannot find media id");
  445. return null;
  446. }
  447. if (!(mediaId in infoCache)) {
  448. let url = 'https://i.instagram.com/api/v1/media/' + mediaId + '/info/';
  449. let resp = await fetch(url, headers);
  450. if (resp.status !== 200) {
  451. console.log(`Fetch info API failed with status code: ${resp.status}`);
  452. return null;
  453. }
  454. let respJson = await resp.json();
  455. infoCache[mediaId] = respJson;
  456. }
  457. let infoJson = infoCache[mediaId];
  458. if ('carousel_media' in infoJson.items[0]) {
  459. // multi-media post
  460. return getImgOrVedioUrl(infoJson.items[0].carousel_media[mediaIdx]);
  461. } else {
  462. // single media post
  463. return getImgOrVedioUrl(infoJson.items[0]);
  464. }
  465. } catch (e) {
  466. console.log(`Uncatched in getUrlFromInfoApi(): ${e}\n${e.stack}`);
  467. return null;
  468. }
  469. }
  470.  
  471. function findPostId(articleNode) {
  472. let aNodes = articleNode.querySelectorAll('a');
  473. for (let i = 0; i < aNodes.length; ++i) {
  474. let link = aNodes[i].getAttribute('href');
  475. if (link) {
  476. let match = link.match(postIdPattern);
  477. if (match) return match[1];
  478. }
  479. }
  480. return null;
  481. }
  482.  
  483. async function fetchVideoURL(articleNode, videoElem) {
  484. let poster = videoElem.getAttribute('poster');
  485. let timeNodes = articleNode.querySelectorAll('time');
  486. // special thanks 孙年忠 (https://greasyfork.org/en/scripts/406535-instagram-download-button/discussions/120159)
  487. let posterUrl = timeNodes[timeNodes.length - 1].parentNode.parentNode.href;
  488. const posterPattern = /\/([^\/?]*)\?/;
  489. let posterMatch = poster.match(posterPattern);
  490. let postFileName = posterMatch[1];
  491. let resp = await fetch(posterUrl);
  492. let content = await resp.text();
  493. // special thanks to 孙年忠 for the pattern (https://greasyfork.org/zh-TW/scripts/406535-instagram-download-button/discussions/116675)
  494. const pattern = new RegExp(`${postFileName}.*?video_versions.*?url":("[^"]*")`, 's');
  495. let match = content.match(pattern);
  496. let videoUrl = JSON.parse(match[1]);
  497. videoUrl = videoUrl.replace(/^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/?\n]+)/g, 'https://scontent.cdninstagram.com');
  498. videoElem.setAttribute('videoURL', videoUrl);
  499. return videoUrl;
  500. }
  501.  
  502. // ================================
  503. // ==== Story ====
  504. // ================================
  505. async function storyOnMouseIn(target) {
  506. let sectionNode = storyGetSectionNode(target);
  507. let url = await storyGetUrl(target, sectionNode);
  508. target.setAttribute('href', url);
  509. }
  510.  
  511. async function storyOnClicked(target) {
  512. // extract url from target story and download or open it
  513. let sectionNode = storyGetSectionNode(target);
  514. let url = await storyGetUrl(target, sectionNode);
  515.  
  516. // download or open media url
  517. if (target.getAttribute('class').includes('download-btn')) {
  518. let mediaName = url
  519. .split('?')[0]
  520. .split('\\')
  521. .pop()
  522. .split('/')
  523. .pop();
  524. mediaName = mediaName.substring(0, mediaName.lastIndexOf('.'));
  525. let datetime = new Date(sectionNode.querySelector('time').getAttribute('datetime'));
  526. let posterName = sectionNode
  527. .querySelector('header a')
  528. .getAttribute('href')
  529. .replace(/\//g, '');
  530.  
  531. let filename = filenameFormat(storyFilenameTemplate, posterName, datetime, mediaName);
  532. downloadResource(url, filename);
  533. } else {
  534. // open url in new tab
  535. openResource(url);
  536. }
  537. }
  538.  
  539. function storyGetSectionNode(target) {
  540. let sectionNode = target;
  541. while (sectionNode && sectionNode.tagName !== 'SECTION') {
  542. sectionNode = sectionNode.parentNode;
  543. }
  544. return sectionNode;
  545. }
  546.  
  547. async function storyGetUrl(target, sectionNode) {
  548. let url = null;
  549. if (!disableNewUrlFetchMethod) url = await getUrlFromInfoApi(target);
  550.  
  551. if (!url) {
  552. if (sectionNode.querySelector('video > source')) {
  553. url = sectionNode.querySelector('video > source').getAttribute('src');
  554. } else if (sectionNode.querySelector('img[decoding="sync"]')) {
  555. let img = sectionNode.querySelector('img[decoding="sync"]');
  556. url = img.srcset.split(/ \d+w/g)[0].trim(); // extract first src from srcset attr. of img
  557. if (url.length > 0) {
  558. return url;
  559. }
  560. url = sectionNode.querySelector('img[decoding="sync"]').getAttribute('src');
  561. } else if (sectionNode.querySelector('video')) {
  562. url = sectionNode.querySelector('video').getAttribute('src');
  563. }
  564. }
  565. return url;
  566. }
  567.  
  568. function filenameFormat(template, id, datetime, medianame, postId = +new Date(), mediaIndex = '0') {
  569. let filename = template;
  570. filename = filename.replace(/%id%/g, id);
  571. filename = filename.replace(/%datetime%/g, datetimeFormat(datetimeTemplate, datetime));
  572. filename = filename.replace(/%medianame%/g, medianame);
  573. filename = filename.replace(/%postId%/g, postId)
  574. filename = filename.replace(/%mediaIndex%/g, mediaIndex);
  575. return filename;
  576. }
  577.  
  578. function datetimeFormat(template, datetime) {
  579. let datetimeStr = template;
  580. datetimeStr = datetimeStr.replace(/%y%/g, datetime.getFullYear());
  581. datetimeStr = datetimeStr.replace(/%m%/g, fillZero((datetime.getMonth() + 1).toString()));
  582. datetimeStr = datetimeStr.replace(/%d%/g, fillZero(datetime.getDate().toString()));
  583. datetimeStr = datetimeStr.replace(/%H%/g, fillZero(datetime.getHours().toString()));
  584. datetimeStr = datetimeStr.replace(/%M%/g, fillZero(datetime.getMinutes().toString()));
  585. datetimeStr = datetimeStr.replace(/%S%/g, fillZero(datetime.getSeconds().toString()));
  586. return datetimeStr;
  587. }
  588.  
  589. function fillZero(str) {
  590. if (str.length === 1) {
  591. return '0' + str;
  592. }
  593. return str;
  594. }
  595.  
  596. function openResource(url) {
  597. // open url in new tab
  598. var a = document.createElement('a');
  599. a.href = url;
  600. a.setAttribute('target', '_blank');
  601. document.body.appendChild(a);
  602. a.click();
  603. a.remove();
  604. }
  605.  
  606. function forceDownload(blob, filename, extension) {
  607. // ref: https://stackoverflow.com/questions/49474775/chrome-65-blocks-cross-origin-a-download-client-side-workaround-to-force-down
  608. var a = document.createElement('a');
  609. if (replaceJpegWithJpg) extension = extension.replace('jpeg', 'jpg')
  610. a.download = filename + '.' + extension;
  611. a.href = blob;
  612. // For Firefox https://stackoverflow.com/a/32226068
  613. document.body.appendChild(a);
  614. a.click();
  615. a.remove();
  616. }
  617.  
  618. // Current blob size limit is around 500MB for browsers
  619. function downloadResource(url, filename) {
  620. if (url.startsWith('blob:')) {
  621. forceDownload(url, filename, 'mp4');
  622. return;
  623. }
  624. console.log(`Dowloading ${url}`);
  625. // ref: https://stackoverflow.com/questions/49474775/chrome-65-blocks-cross-origin-a-download-client-side-workaround-to-force-down
  626. if (!filename) {
  627. filename = url
  628. .split('\\')
  629. .pop()
  630. .split('/')
  631. .pop();
  632. }
  633. fetch(url, {
  634. headers: new Headers({
  635. Origin: location.origin,
  636. }),
  637. mode: 'cors',
  638. })
  639. .then(response => response.blob())
  640. .then(blob => {
  641. const extension = blob.type.split('/').pop();
  642. let blobUrl = window.URL.createObjectURL(blob);
  643. forceDownload(blobUrl, filename, extension);
  644. })
  645. .catch(e => console.error(e));
  646. }
  647. })();