IG小助手

一键下载对方 Instagram 帖子中的相片、视频甚至是他们的快拍、Reels及头像图片!

当前为 2024-07-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name IG Helper
  3. // @name:zh-TW IG小精靈
  4. // @name:zh-CN IG小助手
  5. // @name:ja IG助手
  6. // @name:ko IG조수
  7. // @namespace https://github.snkms.com/
  8. // @version 2.25.3
  9. // @description Downloading is possible for both photos and videos from posts, as well as for stories, reels or profile picture.
  10. // @description:zh-TW 一鍵下載對方 Instagram 貼文中的相片、影片甚至是他們的限時動態、連續短片及大頭貼圖片!
  11. // @description:zh-CN 一键下载对方 Instagram 帖子中的相片、视频甚至是他们的快拍、Reels及头像图片!
  12. // @description:ja 投稿の写真と動画だけでなく、ストーリー、リール、プロフィール写真もダウンロードできます。
  13. // @description:ko 게시물의 사진과 동영상뿐만 아니라 스토리, 릴 또는 프로필 사진도 다운로드할 수 있습니다.
  14. // @description:ro Descărcarea este posibilă atât pentru fotografiile și videoclipurile din postări, cât și pentru storyuri, reels sau poze de profil.
  15. // @author SN-Koarashi (5026)
  16. // @match https://*.instagram.com/*
  17. // @grant GM_addStyle
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM_xmlhttpRequest
  21. // @grant GM_registerMenuCommand
  22. // @grant GM_getResourceText
  23. // @grant GM_openInTab
  24. // @connect i.instagram.com
  25. // @require https://code.jquery.com/jquery-3.7.1.min.js#sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=
  26. // @resource INTERNAL_CSS https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/style.css
  27. // @resource LOCATE_DATE_LIST_TEXT https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/date_locate.json
  28. // @resource LOCALE_TEXT https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/locale.json
  29. // @supportURL https://github.com/SN-Koarashi/ig-helper/
  30. // @contributionURL https://ko-fi.com/snkoarashi
  31. // @icon https://www.google.com/s2/favicons?domain=www.instagram.com
  32. // @compatible firefox >= 87
  33. // @compatible chrome >= 90
  34. // @compatible edge >= 90
  35. // @license GPL-3.0-only
  36. // @run-at document-idle
  37. // ==/UserScript==
  38.  
  39. (function($) {
  40. 'use strict';
  41.  
  42. /******** USER SETTINGS ********/
  43. // !!! DO NOT CHANGE THIS AREA !!!
  44. // PLEASE CHANGE SETTING WITH MENU
  45. const USER_SETTING = {
  46. 'AUTO_RENAME': true,
  47. 'RENAME_PUBLISH_DATE': true,
  48. 'DISABLE_VIDEO_LOOPING': false,
  49. 'REDIRECT_RIGHT_CLICK_USER_STORY_PICTURE': false,
  50. 'FORCE_FETCH_ALL_RESOURCES': false,
  51. 'DIRECT_DOWNLOAD_VISIBLE_RESOURCE': false,
  52. 'DIRECT_DOWNLOAD_ALL': false,
  53. 'MODIFY_VIDEO_VOLUME': false,
  54. 'SCROLL_BUTTON': true,
  55. 'FORCE_RESOURCE_VIA_MEDIA': false,
  56. 'USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT': false,
  57. 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST': false
  58. };
  59. const CHILD_NODES = ['RENAME_PUBLISH_DATE', 'USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT', 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST'];
  60. const LOCATE_DATE_LIST = JSON.parse(GM_getResourceText('LOCATE_DATE_LIST_TEXT'));
  61. var VIDEO_VOLUME = (GM_getValue('G_VIDEO_VOLUME'))?GM_getValue('G_VIDEO_VOLUME'):1;
  62. var LOCATE_DATE_FORMAT = (GM_getValue('G_LOCATE_DATE_FORMAT'))? GM_getValue('G_LOCATE_DATE_FORMAT') : GM_getValue('lang') || navigator.language || navigator.userLanguage;
  63. var TEMP_FETCH_RATE_LITMIT = false;
  64. var RENAME_FORMAT = (GM_getValue('G_RENAME_FORMAT'))? GM_getValue('G_RENAME_FORMAT') : '%USERNAME%-%SOURCE_TYPE%-%SHORTCODE%-%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%%SECOND%';
  65. /*******************************/
  66.  
  67. // Icon download by https://www.flaticon.com/authors/pixel-perfect
  68. const SVG = {
  69. DOWNLOAD: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g><g><path d="M382.56,233.376C379.968,227.648,374.272,224,368,224h-64V16c0-8.832-7.168-16-16-16h-64c-8.832,0-16,7.168-16,16v208h-64 c-6.272,0-11.968,3.68-14.56,9.376c-2.624,5.728-1.6,12.416,2.528,17.152l112,128c3.04,3.488,7.424,5.472,12.032,5.472 c4.608,0,8.992-2.016,12.032-5.472l112-128C384.192,245.824,385.152,239.104,382.56,233.376z"/></g></g><g><g><path d="M432,352v96H80v-96H16v128c0,17.696,14.336,32,32,32h416c17.696,0,32-14.304,32-32V352H432z"/></g></g>',
  70. NEW_TAB: '<svg width="16" height="16" viewBox="3 3 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 14a1 1 0 0 0-1 1v3.077c0 .459-.022.57-.082.684a.363.363 0 0 1-.157.157c-.113.06-.225.082-.684.082H5.923c-.459 0-.571-.022-.684-.082a.363.363 0 0 1-.157-.157c-.06-.113-.082-.225-.082-.684L4.999 5.5a.5.5 0 0 1 .5-.5l3.5.005a1 1 0 1 0 .002-2L5.501 3a2.5 2.5 0 0 0-2.502 2.5v12.577c0 .76.083 1.185.32 1.627.223.419.558.753.977.977.442.237.866.319 1.627.319h12.154c.76 0 1.185-.082 1.627-.319.419-.224.753-.558.977-.977.237-.442.319-.866.319-1.627V15a1 1 0 0 0-1-1zm-2-9.055v-.291l-.39.09A10 10 0 0 1 15.36 5H14a1 1 0 1 1 0-2l5.5.003a1.5 1.5 0 0 1 1.5 1.5V10a1 1 0 1 1-2 0V8.639c0-.757.086-1.511.256-2.249l.09-.39h-.295a10 10 0 0 1-1.411 1.775l-5.933 5.932a1 1 0 0 1-1.414-1.414l5.944-5.944A10 10 0 0 1 18 4.945z" fill="currentColor"/></svg>',
  71. THUMBNAIL: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512"><circle cx="8.25" cy="5.25" r=".5"/><path d="m8.25 6.5c-.689 0-1.25-.561-1.25-1.25s.561-1.25 1.25-1.25 1.25.561 1.25 1.25-.561 1.25-1.25 1.25zm0-1.5c-.138 0-.25.112-.25.25 0 .275.5.275.5 0 0-.138-.112-.25-.25-.25z"/><path d="m7.25 11.25 2-2.5 2.25 1.5 2.25-3.5 3 4.5z"/><path d="m16.75 12h-9.5c-.288 0-.551-.165-.676-.425s-.09-.568.09-.793l2-2.5c.243-.304.678-.372 1.002-.156l1.616 1.077 1.837-2.859c.137-.212.372-.342.625-.344.246-.026.49.123.63.334l3 4.5c.153.23.168.526.037.77-.13.244-.385.396-.661.396zm-4.519-1.5h3.118l-1.587-2.381zm-3.42 0h1.712l-1.117-.745z"/><path d="m22.25 14h-2.756c-.778 0-1.452.501-1.676 1.247l-.859 2.862c-.16.533-.641.891-1.197.891h-7.524c-.556 0-1.037-.358-1.197-.891l-.859-2.861c-.224-.747-.897-1.248-1.676-1.248h-2.756c-.965 0-1.75.785-1.75 1.75v5.5c0 1.517 1.233 2.75 2.75 2.75h18.5c1.517 0 2.75-1.233 2.75-2.75v-5.5c0-.965-.785-1.75-1.75-1.75z"/><path d="m4 12c-.552 0-1-.448-1-1v-8c0-1.654 1.346-3 3-3h12c1.654 0 3 1.346 3 3v8c0 .552-.448 1-1 1s-1-.448-1-1v-8c0-.551-.449-1-1-1h-12c-.551 0-1 .449-1 1v8c0 .552-.448 1-1 1z"/></svg>',
  72. CLOSE: '<svg width="26" height="26" xmlns="http://www.w3.org/2000/svg" id="bold" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="m14.828 12 5.303-5.303c.586-.586.586-1.536 0-2.121l-.707-.707c-.586-.586-1.536-.586-2.121 0l-5.303 5.303-5.303-5.304c-.586-.586-1.536-.586-2.121 0l-.708.707c-.586.586-.586 1.536 0 2.121l5.304 5.304-5.303 5.303c-.586.586-.586 1.536 0 2.121l.707.707c.586.586 1.536.586 2.121 0l5.303-5.303 5.303 5.303c.586.586 1.536.586 2.121 0l.707-.707c.586-.586.586-1.536 0-2.121z"></path></svg>'
  73. };
  74.  
  75. const checkInterval = 250;
  76. const style = GM_getResourceText("INTERNAL_CSS");
  77. const locale = JSON.parse(GM_getResourceText("LOCALE_TEXT"));
  78.  
  79. var lang = GM_getValue('lang') || navigator.language || navigator.userLanguage;
  80. var currentURL = location.href;
  81. var firstStarted = false;
  82. var pageLoaded = false;
  83.  
  84. var GL_postPath;
  85. var GL_username;
  86. var GL_repeat;
  87. var GL_dataCache = {
  88. stories: {},
  89. highlights: {}
  90. };
  91. var GL_observer = new MutationObserver(function (mutation, owner) {
  92. onReadyMyDW();
  93. });
  94.  
  95. initSettings();
  96. GM_addStyle(style);
  97. GM_registerMenuCommand(_i18n('SETTING'), () => {
  98. showSetting();
  99. },{
  100. accessKey: "w"
  101. });
  102. GM_registerMenuCommand(_i18n('DONATE'), () => {
  103. GM_openInTab("https://ko-fi.com/snkoarashi", {active: true});
  104. },{
  105. accessKey: "d"
  106. });
  107. GM_registerMenuCommand(_i18n('DEBUG'), () => {
  108. showDebugDOM();
  109. },{
  110. accessKey: "z"
  111. });
  112. GM_registerMenuCommand(_i18n('FEEDBACK'), () => {
  113. GM_openInTab("https://greasyfork.org/zh-TW/scripts/404535-ig-helper/feedback", {active: true});
  114. },{
  115. accessKey: "f"
  116. });
  117. GM_registerMenuCommand(_i18n('RELOAD_SCRIPT'), () => {
  118. reloadScript();
  119. },{
  120. accessKey: "r"
  121. });
  122.  
  123. // Main Timer
  124. var timer = setInterval(function(){
  125. // page loading
  126. if($('div#splash-screen').length > 0 && !$('div#splash-screen').is(':hidden')) return;
  127.  
  128. if(currentURL != location.href || !firstStarted || !pageLoaded){
  129. console.log('Main Timer', 'trigging');
  130.  
  131. clearInterval(GL_repeat);
  132. pageLoaded = false;
  133. firstStarted = true;
  134. currentURL = location.href;
  135. GL_observer.disconnect();
  136.  
  137. if(location.href.startsWith("https://www.instagram.com/p/") || location.pathname.match(/^\/(.*?)\/p\//ig) || location.href.startsWith("https://www.instagram.com/reel/")){
  138. GL_dataCache.stories = {};
  139.  
  140. console.log('isDialog');
  141.  
  142. // This is to prevent the detection of the "Modify Video Volume" setting from being too slow.
  143. setTimeout(()=>{
  144. onReadyMyDW(false);
  145. }, 5);
  146.  
  147. // This is a delayed function call that prevents the dialog element from appearing before the function is called.
  148. setTimeout(()=>{
  149. onReadyMyDW(false);
  150. }, 250);
  151. setTimeout(()=>{
  152. onReadyMyDW(false);
  153. }, 450);
  154.  
  155. pageLoaded = true;
  156. }
  157.  
  158. if(location.href.startsWith("https://www.instagram.com/reels/")){
  159. console.log('isReels');
  160. setTimeout(()=>{
  161. onReels(false);
  162. },150);
  163. pageLoaded = true;
  164. }
  165.  
  166. if(location.href.split("?")[0] == "https://www.instagram.com/"){
  167. GL_dataCache.stories = {};
  168.  
  169. console.log('isHomepage');
  170. setTimeout(()=>{
  171. onReadyMyDW(false);
  172.  
  173. const element = $('div[id^="mount"] > div > div div > section > main div:not([class]):not([style]) > div > article')?.parent()[0];
  174. if(element){
  175. GL_observer.observe(element, {
  176. childList: true
  177. });
  178. }
  179. },150);
  180.  
  181. pageLoaded = true;
  182. }
  183. if($('div[id^="mount"] > div > div > div').first().is(':hidden') && $('canvas._aarh, canvas[class][style][height][width], div._aadm, header a[role="link"][style][href="/'+location.pathname.split('/').filter(s => s.length > 0).at(0)+'/"]').length && location.pathname.match(/^(\/)([0-9A-Za-z\.\-_]+)\/?(tagged|reels)?\/?$/ig) && !location.pathname.match(/^(\/(stories|explore)\/?)/ig)){
  184. console.log('isProfile');
  185. setTimeout(()=>{
  186. onProfileAvatar(false);
  187. },150);
  188. pageLoaded = true;
  189. }
  190.  
  191. if(!pageLoaded){
  192. // Call Instagram stories function
  193. if(location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/highlights\/)/ig)){
  194. GL_dataCache.highlights = {};
  195.  
  196. console.log('isHighlightsStory');
  197. onHighlightsStory(false);
  198. GL_repeat = setInterval(()=>{
  199. onHighlightsStoryThumbnail(false);
  200. },checkInterval);
  201.  
  202. if($(".IG_DWHISTORY").length) setTimeout(()=>{pageLoaded = true;},150);
  203. }
  204. else if(location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/)/ig)){
  205. console.log('isStory');
  206.  
  207. if($('body div[id^="mount"] > div > div > div[class]').length > 2){
  208. $('.IG_DWSTORY').remove();
  209. $('.IG_DWNEWTAB').remove();
  210.  
  211. onStory(false);
  212. }
  213.  
  214. if($(".IG_DWSTORY").length) setTimeout(()=>{pageLoaded = true;},150);
  215. }
  216. else{
  217. pageLoaded = false;
  218. // Remove icons
  219. $('.IG_DWSTORY').remove();
  220. $('.IG_DWNEWTAB').remove();
  221. $('.IG_DWSTORY_THUMBNAIL').remove();
  222. }
  223. }
  224.  
  225. }
  226. },checkInterval);
  227.  
  228. /**
  229. * onProfileAvatar
  230. * Trigger user avatar download event or button display event.
  231. *
  232. * @param {Boolean} isDownload - Check if it is a download operation
  233. * @return {void}
  234. */
  235. async function onProfileAvatar(isDownload){
  236. if(isDownload){
  237. updateLoadingBar(true);
  238.  
  239. let date = new Date().getTime();
  240. let timestamp = Math.floor(date / 1000);
  241. let username = location.pathname.replaceAll(/(reels|tagged)\/$/ig,'').split('/').filter(s => s.length > 0).at(-1);
  242. let userInfo = await getUserId(username);
  243.  
  244. try{
  245. let dataURL = await getUserHighSizeProfile(userInfo.user.pk);
  246. saveFiles(dataURL,username,"avatar",timestamp,'jpg');
  247. }
  248. catch(err){
  249. saveFiles(userInfo.user.profile_pic_url,username,"avatar",timestamp,'jpg');
  250. }
  251.  
  252. updateLoadingBar(false);
  253. }
  254. else{
  255. // Add the profile download button
  256. if(!$('.IG_DWPROFILE').length){
  257. let profileTimer = setInterval(()=>{
  258. if($('.IG_DWPROFILE').length){
  259. clearInterval(profileTimer);
  260. return;
  261. }
  262.  
  263. $('body > div main canvas._aarh, body > div main canvas[class][style][height][width], body > div main div._aadm, header a[role="link"][style][href="/'+location.pathname.split('/').filter(s => s.length > 0).at(0)+'/"]').parent().append(`<div title="${_i18n("DW")}" class="IG_DWPROFILE">${SVG.DOWNLOAD}</div>`);
  264. $('body > div main canvas._aarh, body > div main canvas[class][style][height][width], body > div main div._aadm, header a[role="link"][style][href="/'+location.pathname.split('/').filter(s => s.length > 0).at(0)+'/"]').parent().css('position','relative');
  265. },150);
  266. }
  267. }
  268. }
  269.  
  270. /**
  271. * onHighlightsStory
  272. * Trigger user's highlight download event or button display event.
  273. *
  274. * @param {Boolean} isDownload - Check if it is a download operation
  275. * @param {Boolean} isPreview - Check if it is need to open new tab
  276. * @return {void}
  277. */
  278. async function onHighlightsStory(isDownload, isPreview){
  279. if(isDownload){
  280. let date = new Date().getTime();
  281. let timestamp = Math.floor(date / 1000);
  282. let highlightId = location.href.replace(/\/$/ig,'').split('/').at(-1);
  283. let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length ||
  284. $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length ||
  285. $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length;
  286. let username = "";
  287. let target = 0;
  288.  
  289. updateLoadingBar(true);
  290.  
  291. if(GL_dataCache.highlights[highlightId]){
  292. console.log('Fetch from memory cache:', highlightId);
  293.  
  294. let totIndex = GL_dataCache.highlights[highlightId].data.reels_media[0].items.length;
  295. username = GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username;
  296. target = GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex-nowIndex];
  297. }
  298. else{
  299. let highStories = await getHighlightStories(highlightId);
  300. let totIndex = highStories.data.reels_media[0].items.length;
  301. username = highStories.data.reels_media[0].owner.username;
  302. target = highStories.data.reels_media[0].items[totIndex-nowIndex];
  303.  
  304. GL_dataCache.highlights[highlightId] = highStories;
  305. }
  306.  
  307. if(USER_SETTING.RENAME_PUBLISH_DATE){
  308. timestamp = target.taken_at_timestamp;
  309. }
  310.  
  311. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LITMIT){
  312. let result = await getMediaInfo(target.id);
  313.  
  314. if(result.status === 'ok'){
  315. if(result.items[0].video_versions){
  316. if(isPreview){
  317. openNewTab(result.items[0].video_versions[0].url);
  318. }
  319. else{
  320. saveFiles(result.items[0].video_versions[0].url, username,"highlights",timestamp,'mp4');
  321. }
  322. }
  323. else{
  324. if(isPreview){
  325. openNewTab(result.items[0].image_versions2.candidates[0].url);
  326. }
  327. else{
  328. saveFiles(result.items[0].image_versions2.candidates[0].url, username,"highlights",timestamp,'jpg');
  329. }
  330. }
  331. }
  332. else{
  333. if(USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT){
  334. delete GL_dataCache.highlights[highlightId];
  335. TEMP_FETCH_RATE_LITMIT = true;
  336.  
  337. onHighlightsStory(true, isPreview);
  338. }
  339. else{
  340. alert('Fetch failed from Media API. API response message: ' + result.message);
  341. }
  342.  
  343. console.log(result);
  344. }
  345. }
  346. else{
  347. if(target.is_video){
  348. if(isPreview){
  349. openNewTab(target.video_resources.at(-1).src,username);
  350. }
  351. else{
  352. saveFiles(target.video_resources.at(-1).src,username,"highlights",timestamp,'mp4', highlightId);
  353. }
  354. }
  355. else{
  356. if(isPreview){
  357. openNewTab(target.display_resources.at(-1).src,username);
  358. }
  359. else{
  360. saveFiles(target.display_resources.at(-1).src,username,"highlights",timestamp,'jpg', highlightId);
  361. }
  362. }
  363.  
  364. TEMP_FETCH_RATE_LITMIT = false;
  365. }
  366.  
  367. updateLoadingBar(false);
  368. }
  369. else{
  370. // Add the stories download button
  371. if(!$('.IG_DWHISTORY').length){
  372. let $element = null;
  373.  
  374. // Default detecter (section layout mode)
  375. if($('body > div section._ac0a').length > 0){
  376. $element = $('body > div section:visible._ac0a');
  377. }
  378. else{
  379. $element = $('body > div section:visible > div > div[style]:not([class])');
  380. $element.css('position','relative');
  381. }
  382.  
  383. // Detecter for div layout mode
  384. // GitHub issue #3: DiceMast3r
  385. if($element.length === 0){
  386. let $$element = $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div');
  387. let nowSize = 0;
  388.  
  389. $$element.each(function(){
  390. if($(this).width() > nowSize){
  391. nowSize = $(this).width();
  392. $element = $(this);
  393. }
  394. });
  395. }
  396.  
  397.  
  398. if($element != null){
  399. //$element.css('position','relative');
  400. $element.append(`<div title="${_i18n("DW")}" class="IG_DWHISTORY">${SVG.DOWNLOAD}</div>`);
  401. $element.append(`<div title="${_i18n("NEW_TAB")}" class="IG_DWHINEWTAB">${SVG.NEW_TAB}</div>`);
  402. }
  403. }
  404. }
  405. }
  406.  
  407. /**
  408. * onHighlightsStoryThumbnail
  409. * Trigger user's highlight video thumbnail download event or button display event.
  410. *
  411. * @param {Boolean} isDownload - Check if it is a download operation
  412. * @return {void}
  413. */
  414. async function onHighlightsStoryThumbnail(isDownload){
  415. if(isDownload){
  416. let date = new Date().getTime();
  417. let timestamp = Math.floor(date / 1000);
  418. let highlightId = location.href.replace(/\/$/ig,'').split('/').at(-1);
  419. let username = "";
  420. let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length ||
  421. $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length ||
  422. $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length;
  423. let target = "";
  424.  
  425. updateLoadingBar(true);
  426.  
  427. if(GL_dataCache.highlights[highlightId]){
  428. console.log('Fetch from memory cache:', highlightId);
  429.  
  430. let totIndex = GL_dataCache.highlights[highlightId].data.reels_media[0].items.length;
  431. username = GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username;
  432. target = GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex-nowIndex];
  433. }
  434. else{
  435. let highStories = await getHighlightStories(highlightId);
  436. let totIndex = highStories.data.reels_media[0].items.length;
  437. username = highStories.data.reels_media[0].owner.username;
  438. target = highStories.data.reels_media[0].items[totIndex-nowIndex];
  439.  
  440. GL_dataCache.highlights[highlightId] = highStories;
  441. }
  442.  
  443. if(USER_SETTING.RENAME_PUBLISH_DATE){
  444. timestamp = target.taken_at_timestamp;
  445. }
  446.  
  447. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LITMIT){
  448. let result = await getMediaInfo(target.id);
  449.  
  450. if(result.status === 'ok'){
  451. saveFiles(result.items[0].image_versions2.candidates[0].url, username,"highlights",timestamp,'jpg');
  452. }
  453. else{
  454. if(USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT){
  455. delete GL_dataCache.highlights[highlightId];
  456. TEMP_FETCH_RATE_LITMIT = true;
  457.  
  458. onHighlightsStoryThumbnail(true);
  459. }
  460. else{
  461. alert('Fetch failed from Media API. API response message: ' + result.message);
  462. }
  463.  
  464. console.log(result);
  465. }
  466. }
  467. else{
  468. saveFiles(target.display_resources.at(-1).src,username,"highlights",timestamp,'jpg', highlightId);
  469. TEMP_FETCH_RATE_LITMIT= false;
  470. }
  471.  
  472. updateLoadingBar(false);
  473. }
  474. else{
  475. if($('body > div section video.xh8yej3').length){
  476. // Add the stories thumbnail download button
  477. if(!$('.IG_DWHISTORY_THUMBNAIL').length){
  478. let $element = null;
  479.  
  480. // Default detecter (section layout mode)
  481. if($('body > div section._ac0a').length > 0){
  482. $element = $('body > div section:visible._ac0a');
  483. }
  484. else{
  485. $element = $('body > div section:visible > div > div[style]:not([class])');
  486. $element.css('position','relative');
  487. }
  488.  
  489. // Detecter for div layout mode
  490. // GitHub issue #3: DiceMast3r
  491. if($element.length === 0){
  492. let $$element = $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div');
  493. let nowSize = 0;
  494.  
  495. $$element.each(function(){
  496. if($(this).width() > nowSize){
  497. nowSize = $(this).width();
  498. $element = $(this);
  499. }
  500. });
  501. }
  502.  
  503. if($element != null){
  504. $element.append(`<div title="${_i18n("THUMBNAIL_INTRO")}" class="IG_DWHISTORY_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  505. }
  506. }
  507. }
  508. else{
  509. $('.IG_DWHISTORY_THUMBNAIL').remove();
  510. }
  511. }
  512. }
  513.  
  514. /**
  515. * onStory
  516. * Trigger user's story download event or button display event.
  517. *
  518. * @param {Boolean} isDownload - Check if it is a download operation
  519. * @param {Boolean} isForce - Check if downloading directly from API instead of cache
  520. * @param {Boolean} isPreview - Check if it is need to open new tab
  521. * @return {void}
  522. */
  523. async function onStory(isDownload,isForce,isPreview){
  524. if(isDownload){
  525. let date = new Date().getTime();
  526. let timestamp = Math.floor(date / 1000);
  527. let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split('/').at(2);
  528.  
  529. updateLoadingBar(true);
  530. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LITMIT){
  531. let mediaId = null;
  532.  
  533. let userInfo = await getUserId(username);
  534. let userId = userInfo.user.pk;
  535. let stories = await getStories(userId);
  536.  
  537. // appear in from profile page to story page
  538. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function(index){
  539. if($(this).hasClass('x1lix1fw')){
  540. if($(this).children().length > 0){
  541. mediaId = stories.data.reels_media[0].items[index].id;
  542. }
  543. }
  544. });
  545.  
  546. // appear in from home page to story page
  547. $('body > div section:visible ._ac0k > ._ac3r > div').each(function(index){
  548. if($(this).children().hasClass('_ac3q')){
  549. mediaId = stories.data.reels_media[0].items[index].id;
  550. }
  551. });
  552.  
  553. if(mediaId == null){
  554. mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  555. }
  556.  
  557. let result = await getMediaInfo(mediaId);
  558.  
  559. if(USER_SETTING.RENAME_PUBLISH_DATE){
  560. timestamp = result.items[0].taken_at;
  561. }
  562.  
  563. if(result.status === 'ok'){
  564. if(result.items[0].video_versions){
  565. if(isPreview){
  566. openNewTab(result.items[0].video_versions[0].url);
  567. }
  568. else{
  569. saveFiles(result.items[0].video_versions[0].url, username,"stories",timestamp,'mp4');
  570. }
  571. }
  572. else{
  573. if(isPreview){
  574. openNewTab(result.items[0].image_versions2.candidates[0].url);
  575. }
  576. else{
  577. saveFiles(result.items[0].image_versions2.candidates[0].url, username,"stories",timestamp,'jpg');
  578. }
  579. }
  580. }
  581. else{
  582. if(USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT){
  583. TEMP_FETCH_RATE_LITMIT = true;
  584. onStory(isDownload,isForce,isPreview);
  585. }
  586. else{
  587. alert('Fetch failed from Media API. API response message: ' + result.message);
  588. }
  589. console.log(result);
  590. }
  591.  
  592. updateLoadingBar(false);
  593. return;
  594. }
  595.  
  596. if($('body > div section:visible video[playsinline]').length > 0){
  597. // Download stories if it is video
  598. let type = "mp4";
  599. let videoURL = "";
  600. let targetURL = location.pathname.replace(/\/$/ig,'').split("/").at(-1);
  601.  
  602. if(GL_dataCache.stories[username] && !isForce){
  603. console.log('Fetch from memory cache:', username);
  604. GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => {
  605. if(item.id == targetURL){
  606. videoURL = item.video_resources[0].src;
  607. if(USER_SETTING.RENAME_PUBLISH_DATE){
  608. timestamp = item.taken_at_timestamp;
  609. }
  610. }
  611. });
  612.  
  613. if(videoURL.length == 0){
  614. console.log('Memory cache not found, try fetch from API:', username);
  615. onStory(true,true);
  616. return;
  617. }
  618. }
  619. else{
  620. let userInfo = await getUserId(username);
  621. let userId = userInfo.user.pk;
  622. let stories = await getStories(userId);
  623.  
  624. stories.data.reels_media[0].items.forEach(item => {
  625. if(item.id == targetURL){
  626. videoURL = item.video_resources[0].src;
  627. if(USER_SETTING.RENAME_PUBLISH_DATE){
  628. timestamp = item.taken_at_timestamp;
  629. }
  630. }
  631. });
  632.  
  633. // GitHub issue #4: thinkpad4
  634. if(videoURL.length == 0){
  635. // appear in from profile page to story page
  636. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function(index){
  637. if($(this).hasClass('x1lix1fw')){
  638. if($(this).children().length > 0){
  639. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  640. if(USER_SETTING.RENAME_PUBLISH_DATE){
  641. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  642. }
  643. }
  644. }
  645. });
  646.  
  647. // appear in from home page to story page
  648. $('body > div section:visible ._ac0k > ._ac3r > div').each(function(index){
  649. if($(this).children().hasClass('_ac3q')){
  650. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  651. if(USER_SETTING.RENAME_PUBLISH_DATE){
  652. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  653. }
  654. }
  655. });
  656. }
  657.  
  658. GL_dataCache.stories[username] = stories;
  659. }
  660.  
  661. if(videoURL.length == 0){
  662. alert(_i18n("NO_VID_URL"));
  663. }
  664. else{
  665. if(isPreview){
  666. openNewTab(videoURL);
  667. }
  668. else{
  669. saveFiles(videoURL,username,"stories",timestamp,type);
  670. }
  671. }
  672. }
  673. else{
  674. // Download stories if it is image
  675. let srcset = $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').attr('srcset')?.split(',')[0]?.split(' ')[0];
  676. let link = (srcset)?srcset:$('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').attr('src');
  677.  
  678. if(!link){
  679. // _aa63 mean stories picture in stories page (not avatar)
  680. let $element = $('body > div section:visible img._aa63');
  681. link = ($element.attr('srcset'))?$element.attr('srcset')?.split(',')[0]?.split(' ')[0]:$element.attr('src');
  682. }
  683.  
  684. if(USER_SETTING.RENAME_PUBLISH_DATE){
  685. timestamp = new Date($('body > div section:visible time[datetime][class]').first().attr('datetime')).getTime();
  686. }
  687.  
  688. let downloadLink = link;
  689. let type = 'jpg';
  690.  
  691. if(isPreview){
  692. openNewTab(downloadLink);
  693. }
  694. else{
  695. saveFiles(downloadLink,username,"stories",timestamp,type);
  696. }
  697. }
  698.  
  699. TEMP_FETCH_RATE_LITMIT = false;
  700. updateLoadingBar(false);
  701. }
  702. else{
  703. // Add the stories download button
  704. let style = "position: absolute;right:-40px;top:15px;padding:5px;line-height:1;background:#fff;border-radius: 5px;cursor:pointer;";
  705. if(!$('.IG_DWSTORY').length){
  706. GL_dataCache.stories = {};
  707. let $element = null;
  708. // Default detecter (section layout mode)
  709. if($('body > div section._ac0a').length > 0){
  710. $element = $('body > div section:visible._ac0a');
  711. }
  712. // detecter (single story layout mode)
  713. else{
  714. $element = $('body > div section:visible > div > div[style]:not([class])');
  715. $element.css('position','relative');
  716. }
  717.  
  718.  
  719. if($element.length === 0){
  720. $element = $('body div[id^="mount"] > div > div > div[class]').last().find('section:visible > div > div[style]:not([class])');
  721. $element.css('position','relative');
  722. }
  723.  
  724. if($element.length === 0){
  725. $element = $('body div[id^="mount"] > div > div > div[class]').last().find('section:visible > div div[style]:not([class]) > div');
  726. $element.css('position','relative');
  727. }
  728.  
  729.  
  730. // Detecter for div layout mode
  731. // GitHub issue #3: DiceMast3r
  732. if($element.length === 0){
  733. let $$element = $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div');
  734.  
  735. let nowSize = 0;
  736.  
  737. $$element.each(function(){
  738. if($(this).width() > nowSize){
  739. nowSize = $(this).width();
  740. $element = $(this);
  741. }
  742. });
  743. }
  744.  
  745.  
  746. if($element != null){
  747. $element.first().css('position','relative');
  748. $element.first().append(`<div title="${_i18n("DW")}" class="IG_DWSTORY">${SVG.DOWNLOAD}</div>`);
  749. $element.first().append(`<div title="${_i18n("NEW_TAB")}" class="IG_DWNEWTAB">${SVG.NEW_TAB}</div>`);
  750.  
  751. onStoryThumbnail(false);
  752. }
  753. }
  754. }
  755. }
  756.  
  757. /**
  758. * onStoryThumbnail
  759. * Trigger user's story video thumbnail download event or button display event.
  760. *
  761. * @param {Boolean} isDownload - Check if it is a download operation
  762. * @param {Boolean} isForce - Check if downloading directly from API instead of cache
  763. * @return {void}
  764. */
  765. async function onStoryThumbnail(isDownload,isForce){
  766. if(isDownload){
  767. // Download stories if it is video
  768. let date = new Date().getTime();
  769. let timestamp = Math.floor(date / 1000);
  770. let type = 'jpg';
  771. let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split('/').at(2);
  772. let style = 'margin:5px 0px;padding:5px 0px;color:#111;font-size:1rem;line-height:1rem;text-align:center;border:1px solid #000;border-radius: 5px;';
  773. // Download thumbnail
  774. let targetURL = location.pathname.replace(/\/$/ig,'').split("/").at(-1);
  775. let videoThumbnailURL = "";
  776.  
  777. updateLoadingBar(true);
  778.  
  779. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LITMIT){
  780. let mediaId = null;
  781.  
  782. let userInfo = await getUserId(username);
  783. let userId = userInfo.user.pk;
  784. let stories = await getStories(userId);
  785.  
  786. // appear in from profile page to story page
  787. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function(index){
  788. if($(this).hasClass('x1lix1fw')){
  789. if($(this).children().length > 0){
  790. mediaId = stories.data.reels_media[0].items[index].id;
  791. }
  792. }
  793. });
  794.  
  795. // appear in from home page to story page
  796. $('body > div section:visible ._ac0k > ._ac3r > div').each(function(index){
  797. if($(this).children().hasClass('_ac3q')){
  798. mediaId = stories.data.reels_media[0].items[index].id;
  799. }
  800. });
  801.  
  802. if(mediaId == null){
  803. mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  804. }
  805.  
  806. let result = await getMediaInfo(mediaId);
  807.  
  808. if(USER_SETTING.RENAME_PUBLISH_DATE){
  809. timestamp = result.items[0].taken_at;
  810. }
  811.  
  812. if(result.status === 'ok'){
  813. saveFiles(result.items[0].image_versions2.candidates[0].url, username,"stories",timestamp,'jpg');
  814.  
  815. }
  816. else{
  817. if(USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT){
  818. TEMP_FETCH_RATE_LITMIT = true;
  819. onStoryThumbnail(true, isForce);
  820. }
  821. else{
  822. alert('Fetch failed from Media API. API response message: ' + result.message);
  823. }
  824.  
  825. console.log(result);
  826. }
  827.  
  828. updateLoadingBar(false);
  829. return;
  830. }
  831.  
  832. if(GL_dataCache.stories[username] && !isForce){
  833. console.log('Fetch from memory cache:', username);
  834. GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => {
  835. if(item.id == targetURL){
  836. videoThumbnailURL = item.display_url;
  837. if(USER_SETTING.RENAME_PUBLISH_DATE){
  838. timestamp = item.taken_at_timestamp;
  839. }
  840. }
  841. });
  842.  
  843. if(videoThumbnailURL.length == 0){
  844. console.log('Memory cache not found, try fetch from API:', username);
  845. onStoryThumbnail(true,true);
  846. return;
  847. }
  848. }
  849. else{
  850. let userInfo = await getUserId(username);
  851. let userId = userInfo.user.pk;
  852. let stories = await getStories(userId);
  853.  
  854. stories.data.reels_media[0].items.forEach(item => {
  855. if(item.id == targetURL){
  856. videoThumbnailURL = item.display_url;
  857. if(USER_SETTING.RENAME_PUBLISH_DATE){
  858. timestamp = item.taken_at_timestamp;
  859. }
  860. }
  861. });
  862.  
  863. // GitHub issue #4: thinkpad4
  864. if(videoThumbnailURL.length == 0){
  865. // appear in from profile page to story page
  866. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function(index){
  867. if($(this).hasClass('x1lix1fw')){
  868. if($(this).children().length > 0){
  869. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  870. if(USER_SETTING.RENAME_PUBLISH_DATE){
  871. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  872. }
  873. }
  874. }
  875. });
  876.  
  877. // appear in from home page to story page
  878. $('body > div section:visible ._ac0k > ._ac3r > div').each(function(index){
  879. if($(this).children().hasClass('_ac3q')){
  880. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  881. if(USER_SETTING.RENAME_PUBLISH_DATE){
  882. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  883. }
  884. }
  885. });
  886. }
  887. }
  888.  
  889. saveFiles(videoThumbnailURL,username,"thumbnail",timestamp,type);
  890. TEMP_FETCH_RATE_LITMIT= false;
  891. updateLoadingBar(false);
  892. }
  893. else{
  894. if($('body > div div.IG_DWSTORY').parent().find('video[class]').length){
  895. $('.IG_DWSTORY_THUMBNAIL').remove();
  896.  
  897. // Add the stories download button
  898. let $element = null;
  899. // Default detecter (section layout mode)
  900. if($('body > div section._ac0a').length > 0){
  901. $element = $('body > div section:visible._ac0a');
  902. }
  903. // detecter (single story layout mode)
  904. else{
  905. $element = $('body > div section:visible > div > div[style]:not([class])');
  906. $element.css('position','relative');
  907. }
  908.  
  909. if($element.length === 0){
  910. $element = $('body div[id^="mount"] > div > div > div[class]').last().find('section:visible > div > div[style]:not([class])');
  911. $element.css('position','relative');
  912. }
  913.  
  914. if($element.length === 0){
  915. $element = $('body div[id^="mount"] > div > div > div[class]').last().find('section:visible > div div[style]:not([class]) > div');
  916. $element.css('position','relative');
  917. }
  918.  
  919. // Detecter for div layout mode
  920. // GitHub issue #3: DiceMast3r
  921. if($element.length === 0){
  922. let $$element = $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div');
  923. let nowSize = 0;
  924.  
  925. $$element.each(function(){
  926. if($(this).width() > nowSize){
  927. nowSize = $(this).width();
  928. $element = $(this);
  929. }
  930. });
  931. }
  932.  
  933.  
  934. if($element != null){
  935. $element.first().css('position','relative');
  936. $element.first().append(`<div title="${_i18n("THUMBNAIL_INTRO")}" class="IG_DWSTORY_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  937. }
  938.  
  939. }
  940. else{
  941. $('.IG_DWSTORY_THUMBNAIL').remove();
  942. }
  943. }
  944. }
  945.  
  946. /**
  947. * onReels
  948. * Trigger user's reels download event or button display event.
  949. *
  950. * @param {Boolean} isDownload - Check if it is a download operation
  951. * @param {Boolean} isVideo - Check if reel is a video element
  952. * @param {Boolean} isPreview - Check if it is need to open new tab
  953. * @return {void}
  954. */
  955. async function onReels(isDownload, isVideo, isPreview){
  956. if(isDownload){
  957. updateLoadingBar(true);
  958.  
  959. let reelsPath = location.href.split('?').at(0).split('instagram.com/reels/').at(-1).replaceAll('/','');
  960. let data = await getBlobMedia(reelsPath);
  961.  
  962. let timestamp = new Date().getTime();
  963.  
  964. if(USER_SETTING.RENAME_PUBLISH_DATE){
  965. timestamp = data.shortcode_media.taken_at_timestamp;
  966. }
  967.  
  968. if(isVideo && data.shortcode_media.is_video){
  969. if(isPreview){
  970. openNewTab(data.shortcode_media.video_url);
  971. }
  972. else{
  973. let type = 'mp4';
  974. saveFiles(data.shortcode_media.video_url,data.shortcode_media.owner.username,"reels",timestamp,type,reelsPath);
  975. }
  976. }
  977. else{
  978. if(isPreview){
  979. openNewTab(data.shortcode_media.display_resources.at(-1).src);
  980. }
  981. else{
  982. let type = 'jpg';
  983. saveFiles(data.shortcode_media.display_resources.at(-1).src,data.shortcode_media.owner.username,"reels",timestamp,type,reelsPath);
  984. }
  985. }
  986.  
  987. updateLoadingBar(false);
  988. }
  989. else{
  990. //$('.IG_REELS_THUMBNAIL, .IG_REELS').remove();
  991. var timer = setInterval(()=>{
  992. if($('section > main[role="main"] > div div.x1qjc9v5 video').length > 0){
  993. clearInterval(timer);
  994.  
  995. if(USER_SETTING.SCROLL_BUTTON){
  996. $('#scrollWrapper').remove();
  997. $('section > main[role="main"]').append('<section id="scrollWrapper"></section>');
  998. $('section > main[role="main"] > #scrollWrapper').append('<div class="button-up"><div></div></div>');
  999. $('section > main[role="main"] > #scrollWrapper').append('<div class="button-down"><div></div></div>');
  1000.  
  1001. $('section > main[role="main"] > #scrollWrapper > .button-up').on('click',function(){
  1002. $('section > main[role="main"] > div')[0].scrollBy({top: -30, behavior: "smooth"});
  1003. });
  1004. $('section > main[role="main"] > #scrollWrapper > .button-down').on('click',function(){
  1005. $('section > main[role="main"] > div')[0].scrollBy({top: 30, behavior: "smooth"});
  1006. });
  1007. }
  1008.  
  1009. // reels scroll has [tabindex] but header not.
  1010. $('section > main[role="main"] > div[tabindex]').children('div').each(function(){
  1011. if($(this).children().length > 0){
  1012. if(!$(this).children().find('.IG_REELS').length){
  1013. $(this).children().css('position','relative');
  1014.  
  1015. $(this).children().append(`<div title="${_i18n("DW")}" class="IG_REELS">${SVG.DOWNLOAD}</div>`);
  1016. $(this).children().append(`<div title="${_i18n("NEW_TAB")}" class="IG_REELS_NEWTAB">${SVG.NEW_TAB}</div>`);
  1017. $(this).children().append(`<div title="${_i18n("THUMBNAIL_INTRO")}" class="IG_REELS_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  1018.  
  1019. // Disable video autoplay
  1020. if(USER_SETTING.DISABLE_VIDEO_LOOPING){
  1021. $(this).find('video').each(function(){
  1022. if(!$(this).data('loop')){
  1023. console.log('(reel) Added video event listener #loop');
  1024. $(this).on('ended',function(){
  1025. $(this).attr('data-loop', true);
  1026. let $element = $(this).next().find('div[role="presentation"] > div > div:last-child');
  1027.  
  1028. if($element.length > 0){
  1029. $element.click();
  1030. console.log('paused click()');
  1031. }
  1032. else{
  1033. $(this).parent().find('.xpgaw4o').removeAttr('style');
  1034. this.pause();
  1035. console.log('paused pause()');
  1036. }
  1037. });
  1038. }
  1039. });
  1040. }
  1041. // Modify Video Volume
  1042. if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  1043. $(this).find('video').each(function(){
  1044. if(!$(this).data('modify')){
  1045. console.log('(reel) Added video event listener #modify');
  1046. this.volume = VIDEO_VOLUME;
  1047.  
  1048. $(this).on('play',function(){
  1049. this.volume = VIDEO_VOLUME;
  1050. });
  1051. $(this).on('playing',function(){
  1052. this.volume = VIDEO_VOLUME;
  1053. });
  1054.  
  1055. $(this).attr('data-modify', true);
  1056. }
  1057. });
  1058. }
  1059. }
  1060. }
  1061. });
  1062. }
  1063. },250);
  1064. }
  1065. }
  1066.  
  1067. /**
  1068. * getHighlightStories
  1069. * Get a list of all stories in highlight Id.
  1070. *
  1071. * @param {Integer} highlightId
  1072. * @return {Object}
  1073. */
  1074. function getHighlightStories(highlightId){
  1075. return new Promise((resolve,reject)=>{
  1076. let getURL = `https://www.instagram.com/graphql/query/?query_hash=45246d3fe16ccc6577e0bd297a5db1ab&variables=%7B%22highlight_reel_ids%22:%5B%22${highlightId}%22%5D,%22precomposed_overlay%22:false%7D`;
  1077.  
  1078. GM_xmlhttpRequest({
  1079. method: "GET",
  1080. url: getURL,
  1081. onload: function(response) {
  1082. let obj = JSON.parse(response.response);
  1083. resolve(obj);
  1084. },
  1085. onerror: function(err){
  1086. reject(err);
  1087. }
  1088. });
  1089. });
  1090. }
  1091.  
  1092. /**
  1093. * getStories
  1094. * Get a list of all stories in user Id.
  1095. *
  1096. * @param {Integer} userId
  1097. * @return {Object}
  1098. */
  1099. function getStories(userId){
  1100. return new Promise((resolve,reject)=>{
  1101. let getURL = `https://www.instagram.com/graphql/query/?query_hash=15463e8449a83d3d60b06be7e90627c7&variables=%7B%22reel_ids%22:%5B%22${userId}%22%5D,%22precomposed_overlay%22:false%7D`;
  1102.  
  1103. GM_xmlhttpRequest({
  1104. method: "GET",
  1105. url: getURL,
  1106. onload: function(response) {
  1107. let obj = JSON.parse(response.response);
  1108. resolve(obj);
  1109. },
  1110. onerror: function(err){
  1111. reject(err);
  1112. }
  1113. });
  1114. });
  1115. }
  1116.  
  1117. /**
  1118. * getUserId
  1119. * Get user's id with username
  1120. *
  1121. * @param {String} username
  1122. * @return {Integer}
  1123. */
  1124. function getUserId(username){
  1125. return new Promise((resolve,reject)=>{
  1126. let getURL = `https://www.instagram.com/web/search/topsearch/?query=${username}`;
  1127.  
  1128. GM_xmlhttpRequest({
  1129. method: "GET",
  1130. url: getURL,
  1131. onload: function(response) {
  1132. // Fix search issue by Discord: sno_w_
  1133. let obj = JSON.parse(response.response);
  1134. let result = null;
  1135. obj.users.forEach(pos => {
  1136. if(pos.user.username === username){
  1137. result = pos;
  1138. }
  1139. });
  1140.  
  1141. if(result != null){
  1142. resolve(result);
  1143. }
  1144. else{
  1145. alert("Can not find user info from getUserId()");
  1146. }
  1147. },
  1148. onerror: function(err){
  1149. reject(err);
  1150. }
  1151. });
  1152. });
  1153. }
  1154.  
  1155. /**
  1156. * getUserHighSizeProfile
  1157. * Get user's high quality avatar image.
  1158. *
  1159. * @param {Integer} userId
  1160. * @return {String}
  1161. */
  1162. function getUserHighSizeProfile(userId){
  1163. return new Promise((resolve,reject)=>{
  1164. let getURL = `https://i.instagram.com/api/v1/users/${userId}/info/`;
  1165.  
  1166. GM_xmlhttpRequest({
  1167. method: "GET",
  1168. url: getURL,
  1169. headers: {
  1170. 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Pixel 7 XL)Build/RP1A.20845.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 Chrome/117.0.5938.60 Mobile Safari/537.36 Instagram 307.0.0.34.111'
  1171. },
  1172. onload: function(response) {
  1173. let obj = JSON.parse(response.response);
  1174. if(obj.status !== 'ok'){
  1175. reject('faild');
  1176. }
  1177. else{
  1178. resolve(obj.user.hd_profile_pic_url_info?.url);
  1179. }
  1180. },
  1181. onerror: function(err){
  1182. reject(err);
  1183. }
  1184. });
  1185. });
  1186. }
  1187.  
  1188. /**
  1189. * getPostOwner
  1190. * Get post's author with post shortcode
  1191. *
  1192. * @param {String} postPath
  1193. * @return {String}
  1194. */
  1195. function getPostOwner(postPath){
  1196. return new Promise((resolve,reject)=>{
  1197. if(!postPath) reject("NOPATH");
  1198. let postShortCode = postPath;
  1199. let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`;
  1200.  
  1201. GM_xmlhttpRequest({
  1202. method: "GET",
  1203. url: getURL,
  1204. onload: function(response) {
  1205. let obj = JSON.parse(response.response);
  1206. resolve(obj.data.shortcode_media.owner.username);
  1207. },
  1208. onerror: function(err){
  1209. reject(err);
  1210. }
  1211. });
  1212. });
  1213. }
  1214.  
  1215. /**
  1216. * getBlobMedia
  1217. * Get list of all media files in post with post shortcode
  1218. *
  1219. * @param {String} postPath
  1220. * @return {Object}
  1221. */
  1222. function getBlobMedia(postPath){
  1223. return new Promise((resolve,reject)=>{
  1224. if(!postPath) reject("NOPATH");
  1225. let postShortCode = postPath;
  1226. let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`;
  1227.  
  1228. GM_xmlhttpRequest({
  1229. method: "GET",
  1230. url: getURL,
  1231. onload: function(response) {
  1232. let obj = JSON.parse(response.response);
  1233. console.log(obj);
  1234. resolve(obj.data);
  1235. },
  1236. onerror: function(err){
  1237. reject(err);
  1238. }
  1239. });
  1240. });
  1241. }
  1242.  
  1243. /**
  1244. * onReadyMyDW
  1245. * Create an event entry point for the download button for the post
  1246. *
  1247. * @param {Boolean} NoDialog - Check if it not showing the dialog
  1248. * @return {void}
  1249. */
  1250. function onReadyMyDW(NoDialog){
  1251. // Whether is Instagram dialog?
  1252. if(NoDialog == false){
  1253. const maxCall = 100;
  1254. let i = 0;
  1255. var repeat = setInterval(() => {
  1256. // div.xdt5ytf << (sigle post in top, not floating) >>
  1257. if(i > maxCall || $('article[data-snig="canDownload"], section:visible > main > div > div.xdt5ytf[data-snig="canDownload"], div[id^="mount"] > div > div > div.x1n2onr6.x1vjfegm div[data-snig="canDownload"]').length > 0){
  1258. clearInterval(repeat);
  1259.  
  1260. if(i > maxCall){
  1261. //alert('Trying to call button creation method reached to maximum try times. If you want to re-register method, please open script menu and press "Reload Script" button or hotkey "R" to reload main timer.');
  1262. console.warn('onReadyMyDW() Timer', 'maximum number of repetitions reached, terminated');
  1263. }
  1264. }
  1265.  
  1266. console.log('onReadyMyDW() Timer', 'repeating to call detection createDownloadButton()');
  1267. createDownloadButton();
  1268. i++;
  1269. },50);
  1270. }
  1271. else{
  1272. createDownloadButton();
  1273. }
  1274. }
  1275.  
  1276. /**
  1277. * getAppID
  1278. * Get Instagram App ID
  1279. *
  1280. * @return {?integer}
  1281. */
  1282. function getAppID(){
  1283. let result = null;
  1284. $('script[type="application/json"]').each(function(){
  1285. const regexp = /"APP_ID":"([0-9]+)"/ig;
  1286. const matcher = $(this).text().match(regexp);
  1287. if(matcher != null && result == null){
  1288. result = [...$(this).text().matchAll(regexp)];
  1289. }
  1290. })
  1291.  
  1292. return (result)?result.at(0).at(-1):null;
  1293. }
  1294.  
  1295. /**
  1296. * updateLoadingBar
  1297. * Update loading state
  1298. *
  1299. * @param {Boolean} isLoading - Check if loading state
  1300. * @return {void}
  1301. */
  1302. function updateLoadingBar(isLoading){
  1303. if(isLoading){
  1304. $('div[id^="mount"] > div > div > div:first').removeClass('x1s85apg');
  1305. $('div[id^="mount"] > div > div > div:first').css('z-index','20000');
  1306. }
  1307. else{
  1308. $('div[id^="mount"] > div > div > div:first').addClass('x1s85apg');
  1309. $('div[id^="mount"] > div > div > div:first').css('z-index','');
  1310. }
  1311. }
  1312.  
  1313. /**
  1314. * getMediaInfo
  1315. * Get Instagram Media object
  1316. *
  1317. * @param {String} mediaId
  1318. * @return {Object}
  1319. */
  1320. function getMediaInfo(mediaId){
  1321. return new Promise((resolve,reject)=>{
  1322. let getURL = `https://i.instagram.com/api/v1/media/${mediaId}/info/`;
  1323.  
  1324. if(mediaId == null){
  1325. alert("Can not call Media API because of the media id is invalid.");
  1326.  
  1327. updateLoadingBar(false);
  1328. reject(-1);
  1329. return;
  1330. }
  1331. if(getAppID() == null){
  1332. alert("Can not call Media API because of the app id is invalid.");
  1333.  
  1334. updateLoadingBar(false);
  1335. reject(-1);
  1336. return;
  1337. }
  1338.  
  1339. GM_xmlhttpRequest({
  1340. method: "GET",
  1341. url: getURL,
  1342. headers: {
  1343. "User-Agent": window.navigator.userAgent,
  1344. "Accept": "*/*",
  1345. 'X-IG-App-ID': getAppID()
  1346. },
  1347. onload: function(response) {
  1348. if(response.finalUrl == getURL){
  1349. let obj = JSON.parse(response.response);
  1350. resolve(obj);
  1351. }
  1352. else{
  1353. let finalURL = new URL(response.finalUrl);
  1354. if(finalURL.pathname.startsWith('/accounts/login')){
  1355. alert("The account must be logged in to access Media API.");
  1356. }
  1357. else{
  1358. alert('Unable to retrieve content because the API was redirected to "'+response.finalUrl+'"');
  1359. }
  1360. updateLoadingBar(false);
  1361. reject(-1);
  1362. }
  1363. },
  1364. onerror: function(err){
  1365. resolve(err);
  1366. }
  1367. });
  1368. });
  1369. }
  1370.  
  1371. /**
  1372. * getVisibleNodeIndex
  1373. * Get element visible node
  1374. *
  1375. * @param {Object} $main
  1376. * @return {Integer}
  1377. */
  1378. function getVisibleNodeIndex($main){
  1379. var index = 0;
  1380. // homepage classList
  1381. var $dot = $main.find('.x1iyjqo2 > div > div:last-child > div');
  1382.  
  1383. // dialog classList, main top classList
  1384. if($dot == null || !$dot.hasClass('_acnb')){
  1385. $dot = $main.find('._aatk > div > div:last-child').eq(0).children('div');
  1386. }
  1387.  
  1388. $dot.filter('._acnb').each(function(sIndex){
  1389. if($(this).hasClass('_acnf')){
  1390. index = sIndex;
  1391. }
  1392. });
  1393.  
  1394. return index;
  1395. }
  1396.  
  1397. /**
  1398. * createDownloadButton
  1399. * Create a download button in the upper right corner of each post
  1400. *
  1401. * @return {void}
  1402. */
  1403. function createDownloadButton(){
  1404. // Add download icon per each posts
  1405. $('article, section:visible > main > div > div.xdt5ytf, div._aap0[role="presentation"]').each(function(index){
  1406. // If it is have not download icon
  1407. // class x1iyjqo2 mean user profile pages post list container
  1408. if(!$(this).attr('data-snig') && !$(this).hasClass('x1iyjqo2') && !$(this).children('article')?.hasClass('x1iyjqo2')){
  1409. console.log("Found post container", $(this));
  1410.  
  1411. var rightPos = 15;
  1412. var topPos = 15;
  1413. var $mainElement = $(this);
  1414. var tagName = this.tagName;
  1415.  
  1416. // not loop each in single top post
  1417. if(tagName === "DIV" && index != 0){
  1418. return;
  1419. }
  1420.  
  1421. // New post UI by Discord: ken
  1422. // NOT WORKING
  1423. /*
  1424. if(tagName === "DIV" && $(this).attr('role') === "presentation"){
  1425. rightPos = 28;
  1426. topPos = 75;
  1427. $mainElement = $('div._aap0[role="presentation"]').parents('div._aamm').parent().parent().parent().parent().parent();
  1428. }
  1429. */
  1430.  
  1431. const $childElement = $mainElement.children("div").children("div");
  1432.  
  1433. if($childElement.length === 0) return;
  1434.  
  1435. console.log("Found insert point", $childElement);
  1436.  
  1437. // Has counter?!
  1438. if($mainElement.find('._aao_ + div.x6s0dn4').length > 0){
  1439. $mainElement.find('._aao_ + div.x6s0dn4').css('top', '35px');
  1440.  
  1441. const observeNode = $mainElement.find('._aao_ + div.x6s0dn4').first().parent()[0];
  1442. var observer = new MutationObserver(function (mutation, owner) {
  1443. $mainElement.find('._aao_ + div.x6s0dn4').css('top', '35px');
  1444. });
  1445.  
  1446. observer.observe(observeNode, {
  1447. childList: true
  1448. });
  1449. }
  1450.  
  1451. // Add icons
  1452. const DownloadElement = `<div title="${_i18n("DW")}" class="SNKMS_IG_DW_MAIN" style="right:${rightPos}px;top:${topPos}px;">${SVG.DOWNLOAD}</div>`;
  1453. const NewTabElement = `<div title="${_i18n("NEW_TAB")}" class="SNKMS_IG_NEWTAB_MAIN" style="right:${rightPos + 35}px;top:${topPos}px;">${SVG.NEW_TAB}</div>`;
  1454. const ThumbnailElement = `<div title="${_i18n("THUMBNAIL_INTRO")}" class="SNKMS_IG_THUMBNAIL_MAIN" style="right:${rightPos + 70}px;top:${topPos}px;">${SVG.THUMBNAIL}</div>`;
  1455.  
  1456. $childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).append(DownloadElement);
  1457. $childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).append(NewTabElement);
  1458.  
  1459. setTimeout(()=>{
  1460. // Check if visible post is video
  1461. if($childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).find('div > ul li._acaz').length === 0){
  1462. if($childElement.find('video').length > 0){
  1463. $childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).append(ThumbnailElement);
  1464. }
  1465. }
  1466. else{
  1467. const checkVideoNode = function(target){
  1468. if(target){
  1469. var k = $(target).find('li._acaz').length;
  1470. var $targetNode = null;
  1471.  
  1472. if(k == 2){
  1473. var index = getVisibleNodeIndex($mainElement);
  1474. // First node
  1475. if(index === 0){
  1476. $targetNode = $(target).find('li._acaz').first();
  1477. }
  1478. // Last node
  1479. else{
  1480. $targetNode = $(target).find('li._acaz').last();
  1481. }
  1482. }
  1483. // Middle node
  1484. else{
  1485. $targetNode = $(target).find('li._acaz').eq(1);
  1486. }
  1487.  
  1488. // Check if video?
  1489. if($targetNode != null && $targetNode.length > 0 && $targetNode.find('video').length > 0){
  1490. $childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).append(ThumbnailElement);
  1491. }
  1492. else{
  1493. $childElement.find('.SNKMS_IG_THUMBNAIL_MAIN')?.remove();
  1494. }
  1495. }
  1496. };
  1497.  
  1498. var observer = new MutationObserver(function (mutation, owner) {
  1499. var target = mutation.at(0)?.target;
  1500. checkVideoNode(target);
  1501. });
  1502.  
  1503. const element = $childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).find('div > ul li._acaz')?.parent()[0];
  1504. const elementAttr = $childElement.eq((tagName === "DIV")? 0 : $childElement.length - 2).find('div > ul li._acaz')?.parent().parent()[0];
  1505.  
  1506. if(element){
  1507. checkVideoNode(element);
  1508. observer.observe(element, {
  1509. childList: true
  1510. });
  1511. }
  1512.  
  1513. if(elementAttr){
  1514. observer.observe(elementAttr, {
  1515. attributes: true
  1516. });
  1517. }
  1518. }
  1519. }, 50);
  1520.  
  1521.  
  1522. $childElement.css('position','relative');
  1523.  
  1524. // Disable video autoplay
  1525. if(USER_SETTING.DISABLE_VIDEO_LOOPING){
  1526. $(this).find('video').each(function(){
  1527. if(!$(this).data('loop')){
  1528. console.log('(post) Added video event listener #loop');
  1529. $(this).on('ended',function(){
  1530. $(this).attr('data-loop', true);
  1531. this.pause();
  1532. });
  1533. }
  1534. });
  1535. }
  1536.  
  1537. // Modify Video Volume
  1538. if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  1539. $(this).find('video').each(function(){
  1540. if(!$(this).data('modify')){
  1541. console.log('(post) Added video event listener #modify');
  1542. this.volume = VIDEO_VOLUME;
  1543.  
  1544. $(this).on('play',function(){
  1545. this.volume = VIDEO_VOLUME;
  1546. });
  1547. $(this).on('playing',function(){
  1548. this.volume = VIDEO_VOLUME;
  1549. });
  1550.  
  1551. $(this).attr('data-modify', true);
  1552. }
  1553. });
  1554. }
  1555.  
  1556. $(this).on('click', '.SNKMS_IG_THUMBNAIL_MAIN', function(e){
  1557. updateLoadingBar(true);
  1558.  
  1559. GL_username = $(this).parent().parent().parent().attr('data-username');
  1560. GL_postPath = location.pathname.replace(/\/$/,'').split('/').at(-1) || $(this).parent().parent().parent().find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1561.  
  1562. var $main = $(this).parent().parent().parent();
  1563. var index = getVisibleNodeIndex($main);
  1564.  
  1565. IG_createDM(true, false);
  1566.  
  1567. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", "").then(()=>{
  1568. let checkBlob = setInterval(()=>{
  1569. if($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0){
  1570. clearInterval(checkBlob);
  1571. var $videoThumbnail = $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="'+(index+1)+'"]')?.parent().find('.videoThumbnail')?.first();
  1572.  
  1573. if($videoThumbnail != null && $videoThumbnail.length > 0){
  1574. $videoThumbnail.click();
  1575. }
  1576. else{
  1577. alert('Can not find thumbnail url.');
  1578. }
  1579.  
  1580. updateLoadingBar(false);
  1581. $('.IG_SN_DIG').remove();
  1582. }
  1583. },250);
  1584. });
  1585. });
  1586.  
  1587. $(this).on('click', '.SNKMS_IG_NEWTAB_MAIN', function(e){
  1588. updateLoadingBar(true);
  1589.  
  1590. GL_username = $(this).parent().parent().parent().attr('data-username');
  1591. GL_postPath = location.pathname.replace(/\/$/,'').split('/').at(-1) || $(this).parent().parent().parent().find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1592.  
  1593. var $main = $(this).parent().parent().parent();
  1594. var index = getVisibleNodeIndex($main);
  1595.  
  1596. IG_createDM(true, false);
  1597.  
  1598. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", "").then(()=>{
  1599. let checkBlob = setInterval(()=>{
  1600. if($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0){
  1601. clearInterval(checkBlob);
  1602. var $linkElement = $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="'+(index+1)+'"]');
  1603.  
  1604. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST){
  1605. triggerLinkElement( $linkElement.first()[0], true);
  1606. }
  1607. else{
  1608. let href = $linkElement?.attr('data-href');
  1609. if(href){
  1610. // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video)
  1611. var urlObj = new URL(href);
  1612. urlObj.host = 'scontent.cdninstagram.com';
  1613.  
  1614. openNewTab(urlObj.href);
  1615. }
  1616. else{
  1617. alert('Can not find open tab url.');
  1618. }
  1619. }
  1620.  
  1621. updateLoadingBar(false);
  1622. $('.IG_SN_DIG').remove();
  1623. }
  1624. },250);
  1625. });
  1626. });
  1627.  
  1628. // Running if user click the download icon
  1629. $(this).on('click','.SNKMS_IG_DW_MAIN', async function(e){
  1630. GL_username = $(this).parent().parent().parent().attr('data-username');
  1631. GL_postPath = location.pathname.replace(/\/$/,'').split('/').at(-1) || $(this).parent().parent().parent().find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1632.  
  1633. // Create element that download dailog
  1634. IG_createDM(USER_SETTING.DIRECT_DOWNLOAD_ALL, true);
  1635.  
  1636. $("#article-id").html(`<a href="https://www.instagram.com/p/${GL_postPath}">${GL_postPath}</a>`);
  1637.  
  1638. if(USER_SETTING.DIRECT_DOWNLOAD_VISIBLE_RESOURCE){
  1639. updateLoadingBar(true);
  1640. IG_setDM(true);
  1641.  
  1642. var index = getVisibleNodeIndex($(this).parent().parent().parent());
  1643.  
  1644. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", "").then(()=>{
  1645. let checkBlob = setInterval(()=>{
  1646. if($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0){
  1647. clearInterval(checkBlob);
  1648. var href = $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="'+(index+1)+'"]')?.attr('data-href');
  1649.  
  1650. if(href){
  1651. updateLoadingBar(false);
  1652. $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="'+(index+1)+'"]')?.click();
  1653. }
  1654. else{
  1655. alert('Can not find download url.');
  1656. }
  1657.  
  1658. $('.IG_SN_DIG').remove();
  1659. }
  1660. },250);
  1661. });
  1662.  
  1663. return;
  1664. }
  1665.  
  1666. if(!USER_SETTING.DIRECT_DOWNLOAD_ALL){
  1667. // Find video/image element and add the download icon
  1668. var s = 0;
  1669. var multiple = $(this).parent().parent().find('._aap0 ._acaz').length;
  1670. var pathname = window.location.pathname;
  1671. var fullpathname = "/"+pathname.split('/')[1]+"/"+pathname.split('/')[2]+"/";
  1672. var blob = USER_SETTING.FORCE_FETCH_ALL_RESOURCES;
  1673. var publish_time = new Date($(this).parent().parent().find('a[href^="/p/"] time[datetime]').first().attr('datetime')).getTime();
  1674.  
  1675. // If posts have more than one images or videos.
  1676. if(multiple){
  1677. $(this).parent().find('._aap0 ._acaz').each(function(){
  1678. let element_videos = $(this).parent().parent().find('video');
  1679. //if(element_videos && element_videos.attr('src') && element_videos.attr('src').match(/^blob:/ig)){
  1680. if(element_videos && element_videos.attr('src')){
  1681. blob = true;
  1682. }
  1683. });
  1684.  
  1685.  
  1686. if(blob || USER_SETTING.FORCE_RESOURCE_VIA_MEDIA){
  1687. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY",_i18n("LOAD_BLOB_MULTIPLE"));
  1688. }
  1689. else{
  1690. $(this).parent().find('._aap0 ._acaz').each(function(){
  1691. s++;
  1692. let element_videos = $(this).find('video');
  1693. let element_images = $(this).find('._aagv img');
  1694. let imgLink = (element_images.attr('srcset'))?element_images.attr('srcset').split(" ")[0]:element_images.attr('src');
  1695.  
  1696. if(element_videos && element_videos.attr('src')){
  1697. blob = true;
  1698. }
  1699. if(element_images && imgLink){
  1700. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY').append(`<a datetime="${publish_time}" data-needed="direct" data-path="${GL_postPath}" data-name="photo" data-type="jpg" data-globalIndex="${s}" href="javascript:;" data-href="${imgLink}"><img width="100" src="${imgLink}" /><br/>- ${_i18n("IMG")} ${s} -</a>`);
  1701. }
  1702.  
  1703. });
  1704.  
  1705. if(blob){
  1706. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY",_i18n("LOAD_BLOB_RELOAD"));
  1707. }
  1708. }
  1709. }
  1710. else{
  1711. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA){
  1712. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY",_i18n("LOAD_BLOB_MULTIPLE"));
  1713. }
  1714. else{
  1715. s++;
  1716. let element_videos = $(this).parent().parent().find('video');
  1717. let element_images = $(this).parent().parent().find('._aagv img');
  1718. let imgLink = (element_images.attr('srcset'))?element_images.attr('srcset').split(" ")[0]:element_images.attr('src');
  1719.  
  1720.  
  1721. if(element_videos && element_videos.attr('src')){
  1722. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY",_i18n("LOAD_BLOB_ONE"));
  1723. }
  1724. if(element_images && imgLink){
  1725. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY').append(`<a datetime="${publish_time}" data-needed="direct" data-path="${GL_postPath}" data-name="photo" data-type="jpg" data-globalIndex="${s}" href="javascript:;" href="" data-href="${imgLink}"><img width="100" src="${imgLink}" /><br/>- ${_i18n("IMG")} ${s} -</a>`);
  1726. }
  1727. }
  1728. }
  1729. }
  1730.  
  1731. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function(){
  1732. $(this).wrap('<div></div>');
  1733. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  1734. $(this).after(`<div title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  1735.  
  1736. if($(this).attr('data-name') == 'video'){
  1737. $(this).after(`<div title="${_i18n("THUMBNAIL_INTRO")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  1738. }
  1739. });
  1740.  
  1741. if(USER_SETTING.DIRECT_DOWNLOAD_ALL){
  1742. createMediaListDOM(GL_postPath,".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY",_i18n("LOAD_BLOB_MULTIPLE")).then(()=>{
  1743. let checkBlob = setInterval(()=>{
  1744. if($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0){
  1745. clearInterval(checkBlob);
  1746. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function(){
  1747. $(this).click();
  1748. });
  1749.  
  1750. $('.IG_SN_DIG').remove();
  1751. }
  1752. },250);
  1753. });
  1754. }
  1755. });
  1756.  
  1757. // Add the mark that download is ready
  1758. var username = $(this).find("header > div:last-child > div:first-child span a").first().text();
  1759.  
  1760. $(this).attr('data-snig','canDownload');
  1761. $(this).attr('data-username',username);
  1762. }
  1763. });
  1764. }
  1765.  
  1766. /**
  1767. * createMediaListDOM
  1768. * Create a list of media elements from post URLs
  1769. *
  1770. * @param {String} postURL
  1771. * @param {String} selector - Use CSS element selectors to choose where it appears.
  1772. * @param {String} message - i18n display loading message
  1773. * @return {void}
  1774. */
  1775. function createMediaListDOM(postURL,selector,message){
  1776. return new Promise(async (resolve) => {
  1777. $(`${selector} a`).remove();
  1778. $(selector).append('<p id="_SNLOAD">'+ message +'</p>');
  1779. let media = await getBlobMedia(postURL);
  1780.  
  1781. let idx = 1;
  1782. let resource = media.shortcode_media;
  1783.  
  1784. // GraphVideo
  1785. if(resource.__typename == "GraphVideo" && resource.video_url){
  1786. $(selector).append(`<a media-id="${resource.id}" datetime="${resource.taken_at_timestamp}" data-blob="true" data-needed="direct" data-path="${resource.shortcode}" data-name="video" data-type="mp4" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${resource.video_url}"><img width="100" src="${resource.display_resources[1].src}" /><br/>- ${_i18n("VID")} ${idx} -</a>`);
  1787. idx++;
  1788. }
  1789. // GraphImage
  1790. if(resource.__typename == "GraphImage"){
  1791. $(selector).append(`<a media-id="${resource.id}" datetime="${resource.taken_at_timestamp}" data-blob="true" data-needed="direct" data-path="${resource.shortcode}" data-name="photo" data-type="jpg" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${resource.display_resources[resource.display_resources.length - 1].src}"><img width="100" src="${resource.display_resources[1].src}" /><br/>- ${_i18n("IMG")} ${idx} -</a>`);
  1792. idx++;
  1793. }
  1794. // GraphSidecar
  1795. if(resource.__typename == "GraphSidecar" && resource.edge_sidecar_to_children){
  1796. for(let e of resource.edge_sidecar_to_children.edges){
  1797. if(e.node.__typename == "GraphVideo"){
  1798. $(selector).append(`<a media-id="${e.node.id}" datetime="${resource.taken_at_timestamp}" data-blob="true" data-needed="direct" data-path="${resource.shortcode}" data-name="video" data-type="mp4" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${e.node.video_url}"><img width="100" src="${e.node.display_resources[1].src}" /><br/>- ${_i18n("VID")} ${idx} -</a>`);
  1799. }
  1800.  
  1801. if(e.node.__typename == "GraphImage"){
  1802. $(selector).append(`<a media-id="${e.node.id}" datetime="${resource.taken_at_timestamp}" data-blob="true" data-needed="direct" data-path="${resource.shortcode}" data-name="photo" data-type="jpg" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${e.node.display_resources[e.node.display_resources.length - 1].src}"><img width="100" src="${e.node.display_resources[1].src}" /><br/>- ${_i18n("IMG")} ${idx} -</a>`);
  1803. }
  1804. idx++;
  1805. }
  1806. }
  1807.  
  1808. $("#_SNLOAD").remove();
  1809. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function(){
  1810. $(this).wrap('<div></div>');
  1811. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  1812. $(this).after(`<div title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  1813.  
  1814. if($(this).attr('data-name') == 'video'){
  1815. $(this).after(`<div title="${_i18n("THUMBNAIL_INTRO")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  1816. }
  1817. });
  1818.  
  1819. resolve(true);
  1820. });
  1821. }
  1822.  
  1823. /**
  1824. * IG_createDM
  1825. * A dialog showing a list of all media files in the post
  1826. *
  1827. * @param {Boolean} hasHidden
  1828. * @param {Boolean} hasCheckbox
  1829. * @return {void}
  1830. */
  1831. function IG_createDM(hasHidden, hasCheckbox){
  1832. let isHidden = (hasHidden)?"hidden":"";
  1833. $('body').append('<div class="IG_SN_DIG '+isHidden+'"><div class="IG_SN_DIG_BG"></div><div class="IG_SN_DIG_MAIN"><div class="IG_SN_DIG_TITLE"></div><div class="IG_SN_DIG_BODY"></div></div></div>');
  1834. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE').append(`<div style="position:relative;min-height:36px;text-align:center;margin-bottom: 7px;"><div style="position:absolute;left:0px;line-height: 18px;"><kbd>Alt</kbd>+<kbd>Q</kbd> [${_i18n("CLOSE")}]</div><div style="line-height: 18px;">IG Helper</div><div id="post_info" style="line-height: 14px;font-size:14px;">Post ID: <span id="article-id"></span></div><div class="IG_SN_DIG_BTN">${SVG.CLOSE}</div></div>`);
  1835.  
  1836. if(hasCheckbox){
  1837. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE').append(`<div style="text-align: center;" id="button_group"></div>`);
  1838. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE > div#button_group').append(`<button id="batch_download_selected">${_i18n('BATCH_DOWNLOAD_SELECTED')}</button>`);
  1839. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE > div#button_group').append(`<button id="batch_download_direct">${_i18n('BATCH_DOWNLOAD_DIRECT')}</button>`);
  1840. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE').append(`<label class="checkbox"><input value="yes" type="checkbox" />${_i18n('ALL_CHECK')}</label>`);
  1841. }
  1842. }
  1843.  
  1844. /**
  1845. * IG_setDM
  1846. * Set a dialog status
  1847. *
  1848. * @param {Boolean} hasHidden
  1849. * @return {void}
  1850. */
  1851. function IG_setDM(hasHidden){
  1852. if($('.IG_SN_DIG').length){
  1853. if(hasHidden){
  1854. $('.IG_SN_DIG').addClass("hidden");
  1855. }
  1856. else{
  1857. $('.IG_SN_DIG').removeClass("hidden");
  1858. }
  1859. }
  1860. }
  1861.  
  1862. /**
  1863. * saveFiles
  1864. * Download the specified media URL to the computer
  1865. *
  1866. * @param {String} downloadLink
  1867. * @param {String} username
  1868. * @param {String} sourceType
  1869. * @param {Integer} timestamp
  1870. * @param {String} filetype
  1871. * @param {String} shortcode
  1872. * @return {void}
  1873. */
  1874. function saveFiles(downloadLink,username,sourceType,timestamp,filetype,shortcode){
  1875. setTimeout(()=>{
  1876. updateLoadingBar(true);
  1877. fetch(downloadLink).then(res => {
  1878. return res.blob().then(dwel => {
  1879. updateLoadingBar(false);
  1880. createSaveFileElement(downloadLink,dwel,username,sourceType,timestamp,filetype,shortcode);
  1881. });
  1882. });
  1883. }, 50);
  1884. }
  1885.  
  1886. /**
  1887. * createSaveFileElement
  1888. * Download the specified media with link element
  1889. *
  1890. * @param {String} downloadLink
  1891. * @param {Object} object
  1892. * @param {String} username
  1893. * @param {String} sourceType
  1894. * @param {Integer} timestamp
  1895. * @param {String} filetype
  1896. * @param {String} shortcode
  1897. * @return {void}
  1898. */
  1899. function createSaveFileElement(downloadLink,object,username,sourceType,timestamp,filetype,shortcode) {
  1900. timestamp = parseInt(timestamp.toString().padEnd(13, '0'));
  1901.  
  1902. if(USER_SETTING.RENAME_PUBLISH_DATE){
  1903. timestamp = parseInt(timestamp.toString().padEnd(13, '0'));
  1904. }
  1905.  
  1906. const date = new Date(timestamp);
  1907.  
  1908. const a = document.createElement("a");
  1909. const original_name = new URL(downloadLink).pathname.split('/').at(-1).split('.').slice(0,-1).join('.');
  1910. const year = date.getFullYear().toString();
  1911. const month = (date.getMonth()+1).toString().padStart(2,'0');
  1912. const day = date.getDate().toString().padStart(2,'0');
  1913. const hour = date.getHours().toString().padStart(2,'0');
  1914. const minute = date.getMinutes().toString().padStart(2,'0');
  1915. const second = date.getSeconds().toString().padStart(2,'0');
  1916.  
  1917. var filename = RENAME_FORMAT.toUpperCase();
  1918. var replacements = {
  1919. '%USERNAME%': username,
  1920. '%SOURCE_TYPE%': sourceType,
  1921. '%SHORTCODE%': (shortcode)?shortcode:'NONE',
  1922. '%YEAR%': year,
  1923. '%MONTH%': month,
  1924. '%DAY%': day,
  1925. '%HOUR%': hour,
  1926. '%MINUTE%': minute,
  1927. '%SECOND%': second,
  1928. '%ORIGINAL_NAME%': original_name
  1929. };
  1930.  
  1931. filename = filename.replace(/%\w+%/g, function(str) {
  1932. return replacements[str] || str;
  1933. });
  1934.  
  1935. const originally = username + '_' + original_name + '.' + filetype;
  1936.  
  1937. a.href = URL.createObjectURL(object);
  1938. a.setAttribute("download", (USER_SETTING.AUTO_RENAME)?filename+'.'+filetype:originally);
  1939. a.click();
  1940. a.remove();
  1941. }
  1942.  
  1943. /**
  1944. * triggerLinkElement
  1945. * Trigger the link element to start downloading the resource
  1946. *
  1947. * @param {Object} element
  1948. * @return {void}
  1949. */
  1950. async function triggerLinkElement(element, isPreview) {
  1951. let date = new Date().getTime();
  1952. let timestamp = Math.floor(date / 1000);
  1953. let username = ($(element).attr('data-username')) ? $(element).attr('data-username') : GL_username;
  1954.  
  1955. if(!username && $(element).attr('data-path')){
  1956. console.log('catching owner name from shortcode:',$(element).attr('data-href'));
  1957. username = await getPostOwner($(element).attr('data-path'));
  1958. }
  1959.  
  1960. if(USER_SETTING.RENAME_PUBLISH_DATE && $(element).attr('datetime')){
  1961. timestamp = parseInt($(element).attr('datetime'));
  1962. }
  1963.  
  1964. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA){
  1965. updateLoadingBar(true);
  1966. let result = await getMediaInfo($(element).attr('media-id'));
  1967. updateLoadingBar(false);
  1968.  
  1969. if(result.status === 'ok'){
  1970. var resource_url = null;
  1971. if(result.items[0].video_versions){
  1972. resource_url = result.items[0].video_versions[0].url;
  1973. }
  1974. else{
  1975. resource_url = result.items[0].image_versions2.candidates[0].url;
  1976. }
  1977.  
  1978. if(isPreview){
  1979. let urlObj = new URL(resource_url);
  1980. urlObj.host = 'scontent.cdninstagram.com';
  1981.  
  1982. openNewTab(urlObj.href);
  1983. }
  1984. else{
  1985. saveFiles(resource_url, username, $(element).attr('data-name'),timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  1986. }
  1987. }
  1988. else{
  1989. if(USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT){
  1990. if(isPreview){
  1991. let urlObj = new URL($(element).attr('data-href'));
  1992. urlObj.host = 'scontent.cdninstagram.com';
  1993.  
  1994. openNewTab(urlObj.href);
  1995. }
  1996. else{
  1997. saveFiles($(element).attr('data-href'),username,$(element).attr('data-name'),timestamp,$(element).attr('data-type'), $(element).attr('data-path'));
  1998. }
  1999. }
  2000. else{
  2001. alert('Fetch failed from Media API. API response message: ' + result.message);
  2002. }
  2003. console.log(result);
  2004. }
  2005. }
  2006. else{
  2007. saveFiles($(element).attr('data-href'),username,$(element).attr('data-name'),timestamp,$(element).attr('data-type'), $(element).attr('data-path'));
  2008. }
  2009. }
  2010.  
  2011. /**
  2012. * translateText
  2013. * i18n translation text
  2014. *
  2015. * @param {String} lang
  2016. * @return {void}
  2017. */
  2018. function translateText(lang){
  2019. var eLocale = {
  2020. "en-US": {
  2021. "SELECT_LANG": "English",
  2022. "RELOAD_SCRIPT": "Reload Script",
  2023. "DONATE": "Donate",
  2024. "FEEDBACK": "Feedback",
  2025. "NEW_TAB": "Open in new tab",
  2026. "SHOW_DOM_TREE": "Show DOM Tree",
  2027. "SELECT_AND_COPY": "Select All and Copy of the Input Box",
  2028. "DOWNLOAD_DOM_TREE": "Download DOM Tree as Text File",
  2029. "REPORT_GITHUB": "Report Issue On GitHub",
  2030. "REPORT_DISCORD": "Report Issue On Discord Support Server",
  2031. "DEBUG": "Debug Window",
  2032. "CLOSE": "Close",
  2033. "ALL_CHECK": "Select All",
  2034. "BATCH_DOWNLOAD_SELECTED": "Download Selected Resources",
  2035. "BATCH_DOWNLOAD_DIRECT": "Download All Resources",
  2036. "IMG": "Image",
  2037. "VID": "Video",
  2038. "DW": "Download",
  2039. "THUMBNAIL_INTRO": "Download video thumbnail",
  2040. "LOAD_BLOB_ONE": "Loading Blob Media...",
  2041. "LOAD_BLOB_MULTIPLE": "Loading Blob Media and others...",
  2042. "LOAD_BLOB_RELOAD": "Detect Blob Media, now reloading...",
  2043. "NO_CHECK_RESOURCE": "You need to check resource to download.",
  2044. "NO_VID_URL": "Can not find video url.",
  2045. "SETTING": "Settings",
  2046. "AUTO_RENAME": "Automatically Rename Files (Right-Click To Set)",
  2047. "RENAME_SHORTCODE": "Rename The File and Include Shortcode",
  2048. "RENAME_PUBLISH_DATE": "Set Rename File Timestamp to Resource Publish Date",
  2049. "RENAME_LOCATE_DATE": "Modify Renamed File Timestamp Date Format (Right-Click To Set)",
  2050. "DISABLE_VIDEO_LOOPING": "Disable Video Auto-looping",
  2051. "REDIRECT_RIGHT_CLICK_USER_STORY_PICTURE": "Redirect When Right-Clicking User Story Picture",
  2052. "FORCE_FETCH_ALL_RESOURCES": "Forcing Fetch All Resources In the Post",
  2053. "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Directly Download the Visible Resources In the Post",
  2054. "DIRECT_DOWNLOAD_ALL": "Directly Download All Resources In the Post",
  2055. "MODIFY_VIDEO_VOLUME": "Modify Video Volume (Right-Click To Set)",
  2056. "SCROLL_BUTTON": "Enable Scroll Buttons For Reels Page",
  2057. "FORCE_RESOURCE_VIA_MEDIA": "Force Fetch Resource via Media API",
  2058. "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT": "Use Other Methods to Download When the Media API is Not Accessible",
  2059. "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": '"Open in new tab" in posts always uses Media API',
  2060. "AUTO_RENAME_INTRO": "Auto rename file to custom format\nCustom Format List: \n%USERNAME% - Username\n%SOURCE_TYPE% - Download source\n%SHORTCODE% - Post Shortcode\n%YEAR% - Year when downloaded/published\n%MONTH% - Month when downloaded/published\n%DAY% - Day when downloaded/published\n%HOUR% - Hour when downloaded/published\n%MINUTE% - Minute when downloaded/published\n%SECOND% - Second when downloaded/published\n%ORIGINAL_NAME% - Original name of downloaded file\n\nIf set to false, the file name will remain as it is.\nExample: instagram_321565527_679025940443063_4318007696887450953_n.jpg",
  2061. "RENAME_SHORTCODE_INTRO": "Auto rename file to format type following:\nUSERNAME-TYPE-SHORTCODE-TIMESTAMP.FILETYPE\nExample: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nIt will ONLY work in [Automatically Rename Files] setting to TRUE.",
  2062. "RENAME_PUBLISH_DATE_INTRO": "Sets the timestamp in the file rename format to the resource publish date (browser time zone)\n\nThis feature only works when [Automatically Rename Files] is set to TRUE.",
  2063. "RENAME_LOCATE_DATE_INTRO": "Modify the rename file timestamp date format to the browser's local time, and format it to the regional date format of your choice.\n\nThis feature only works when [Automatically Rename Files] is set to TRUE.",
  2064. "DISABLE_VIDEO_LOOPING_INTRO": "Disable video auto-looping in reels and posts.",
  2065. "REDIRECT_RIGHT_CLICK_USER_STORY_PICTURE_INTRO": "Redirect to a user's profile page when right-clicking on their user avatar in the story area on the homepage.",
  2066. "FORCE_FETCH_ALL_RESOURCES_INTRO": "Force fetching of all resources (photos and videos) in a post via the Instagram API to remove the limit of three resources per post.",
  2067. "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Directly download the current resources in the post.",
  2068. "DIRECT_DOWNLOAD_ALL_INTRO": "When you click the download button, all resources in the post will be directly forced to be fetched and downloaded.",
  2069. "MODIFY_VIDEO_VOLUME_INTRO": "Modify the video playback volume in Reels and Posts (right-click to open the volume setting slider).",
  2070. "SCROLL_BUTTON_INTRO": "Enable scroll buttons for the lower right corner of Reels page.",
  2071. "FORCE_RESOURCE_VIA_MEDIA_INTRO": "The Media API will try to get the highest quality photo or video possible, but it will take longer to load.",
  2072. "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LITMIT_INTRO": "When the Media API reaches the rate limit or cannot be used for other reasons, the Forced Fetch API is used to download resources (the resource quality is slightly lower).",
  2073. "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "[Open in new tab] button in posts will always use the Media API to obtain high-resolution resources."
  2074. }
  2075. };
  2076.  
  2077. var resultUnsorted = Object.assign({}, eLocale, locale);
  2078. var resultSorted = Object.keys(resultUnsorted).sort().reduce(
  2079. (obj, key) => {
  2080. obj[key] = resultUnsorted[key];
  2081. return obj;
  2082. }, {}
  2083. );
  2084.  
  2085. return resultSorted;
  2086. }
  2087.  
  2088. /**
  2089. * _i18n
  2090. * Perform i18n translation
  2091. *
  2092. * @param {String} text
  2093. * @return {void}
  2094. */
  2095. function _i18n(text){
  2096. const translate = translateText();
  2097.  
  2098. if(translate[lang] != undefined && translate[lang][text] != undefined){
  2099. return translate[lang][text];
  2100. }
  2101. else{
  2102. return translate["en-US"][text];
  2103. }
  2104. }
  2105.  
  2106. /**
  2107. * showSetting
  2108. * Show script settings window
  2109. *
  2110. * @return {void}
  2111. */
  2112. function showSetting(){
  2113. $('.IG_SN_DIG').remove();
  2114. IG_createDM();
  2115. $('.IG_SN_DIG #post_info').text('Preference Settings');
  2116.  
  2117. $('.IG_SN_DIG .IG_SN_DIG_TITLE > div').append('<select id="langSelect"></select><div style="font-size: 12px;">The newly selected language will be applied after refreshing the page.</div>');
  2118.  
  2119. for(let o in translateText()){
  2120. $('.IG_SN_DIG .IG_SN_DIG_TITLE > div #langSelect').append(`<option value="${o}" ${(lang == o)?'selected':''}>${translateText()[o].SELECT_LANG}</option>`);
  2121. }
  2122.  
  2123. for(let name in USER_SETTING){
  2124. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<label class="globalSettings${(CHILD_NODES.includes(name))?' child':''}" title="${_i18n(name+'_INTRO')}"><span>${_i18n(name)}</span> <input id="${name}" value="box" type="checkbox" ${(USER_SETTING[name] === true)?'checked':''}><div class="chbtn"><div class="rounds"></div></div></label>`);
  2125.  
  2126. if(name === 'MODIFY_VIDEO_VOLUME'){
  2127. $('.IG_SN_DIG .IG_SN_DIG_BODY input[id="'+name+'"]').parent('label').on('contextmenu', function(e){
  2128. e.preventDefault();
  2129. if($(this).find('#tempWrapper').length === 0){
  2130. $(this).append('<div id="tempWrapper"></div>');
  2131. $(this).children('#tempWrapper').append('<input value="' + VIDEO_VOLUME + '" type="range" min="0" max="1" step="0.05" />');
  2132. $(this).children('#tempWrapper').append('<input value="' + VIDEO_VOLUME + '" step="0.05" type="number" />');
  2133. $(this).children('#tempWrapper').append(`<div class="IG_SN_DIG_BTN">${SVG.CLOSE}</div>`);
  2134. }
  2135. });
  2136. }
  2137.  
  2138. if(name === 'AUTO_RENAME'){
  2139. $('.IG_SN_DIG .IG_SN_DIG_BODY input[id="'+name+'"]').parent('label').on('contextmenu', function(e){
  2140. e.preventDefault();
  2141. if($(this).find('#tempWrapper').length === 0){
  2142. $(this).append('<div id="tempWrapper"></div>');
  2143.  
  2144. $(this).children('#tempWrapper').append('<input id="date_format" value="' + RENAME_FORMAT + '" />');
  2145. $(this).children('#tempWrapper').append(`<div class="IG_SN_DIG_BTN">${SVG.CLOSE}</div>`);
  2146. }
  2147. });
  2148. }
  2149. }
  2150. }
  2151.  
  2152. /**
  2153. * showDebugDOM
  2154. * Show full DOM tree
  2155. *
  2156. * @return {void}
  2157. */
  2158. function showDebugDOM(){
  2159. $('.IG_SN_DIG').remove();
  2160. IG_createDM();
  2161. $('.IG_SN_DIG #post_info').text('IG Debug DOM Tree');
  2162.  
  2163. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<textarea style="font-family: monospace;width:100%;box-sizing: border-box;height:300px;background: transparent;" readonly></textarea>`);
  2164. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<span style="display:block;text-align:center;">`);
  2165. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_DISPLAY_DOM_TREE"><a>${_i18n('SHOW_DOM_TREE')}</a></button>`);
  2166. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_SELECT_DOM_TREE"><a>${_i18n('SELECT_AND_COPY')}</a></button>`);
  2167. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_DOWNLOAD_DOM_TREE"><a>${_i18n('DOWNLOAD_DOM_TREE')}</a></button><br/>`);
  2168. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_GITHUB"><a href="https://github.com/SN-Koarashi/ig-helper/issues" target="_blank">${_i18n('REPORT_GITHUB')}</a></button>`);
  2169. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_DISCORD"><a href="https://discord.gg/Sh8HJ4d" target="_blank">${_i18n('REPORT_DISCORD')}</a></button>`);
  2170. }
  2171.  
  2172. /**
  2173. * openNewTab
  2174. * Open url in new tab
  2175. *
  2176. * @param {String} link
  2177. * @return {void}
  2178. */
  2179. function openNewTab(link){
  2180. var a = document.createElement('a');
  2181. a.href = link;
  2182. a.target = '_blank';
  2183.  
  2184. document.body.appendChild(a);
  2185. a.click();
  2186. a.remove();
  2187. }
  2188.  
  2189. /**
  2190. * reloadScript
  2191. * Re-register main timer
  2192. *
  2193. * @return {void}
  2194. */
  2195. function reloadScript(){
  2196. clearInterval(GL_repeat);
  2197. pageLoaded = false;
  2198. firstStarted = false;
  2199. currentURL = location.href;
  2200. GL_observer.disconnect();
  2201.  
  2202. console.log('main timer re-register completed');
  2203. }
  2204.  
  2205. /**
  2206. * initSettings
  2207. * Initialize preferences
  2208. *
  2209. * @return {void}
  2210. */
  2211. function initSettings(){
  2212. for(let name in USER_SETTING){
  2213. if(GM_getValue(name) != null && typeof GM_getValue(name) === 'boolean'){
  2214. USER_SETTING[name] = GM_getValue(name);
  2215. }
  2216. }
  2217. }
  2218.  
  2219. // Running if document is ready
  2220. $(function(){
  2221. $('body').on('click','.IG_SN_DIG .IG_SN_DIG_BODY .IG_DISPLAY_DOM_TREE',function(){
  2222. let text = $('div[id^="mount"]')[0];
  2223. $('.IG_SN_DIG .IG_SN_DIG_BODY textarea').text("Location: " + location.pathname + "\nDOM Tree:\n" + text.innerHTML);
  2224. });
  2225.  
  2226. $('body').on('click','.IG_SN_DIG .IG_SN_DIG_BODY .IG_SELECT_DOM_TREE',function(){
  2227. $('.IG_SN_DIG .IG_SN_DIG_BODY textarea').select();
  2228. document.execCommand('copy');
  2229. });
  2230.  
  2231. $('body').on('click','.IG_SN_DIG .IG_SN_DIG_BODY .IG_DOWNLOAD_DOM_TREE',function(){
  2232. var text = ($('.IG_SN_DIG .IG_SN_DIG_BODY textarea').text().length > 0)?$('.IG_SN_DIG .IG_SN_DIG_BODY textarea').text():"Location: " + location.pathname + "\nDOM Tree:\n" +$('div[id^="mount"]')[0].innerHTML;
  2233. var a = document.createElement("a");
  2234. var file = new Blob([text], {type: "text/plain"});
  2235. a.href = URL.createObjectURL(file);
  2236. a.download = "DOMTree.txt";
  2237.  
  2238. document.body.appendChild(a);
  2239. a.click();
  2240. a.remove();
  2241. });
  2242.  
  2243. // Close the download dialog if user click the close icon
  2244. $('body').on('click','.IG_SN_DIG_BTN, .IG_SN_DIG_BG',function(){
  2245. if($(this).parent('#tempWrapper').length > 0){
  2246. $(this).parent('#tempWrapper').fadeOut(250, function(){
  2247. $(this).remove();
  2248. });
  2249. }
  2250. else{
  2251. $('.IG_SN_DIG').remove();
  2252. }
  2253. });
  2254.  
  2255. $(window).keydown(function(e){
  2256. // Hot key [Alt+Q] to close the download dialog
  2257. if (e.keyCode == '81' && e.altKey){
  2258. $('.IG_SN_DIG').remove();
  2259. e.preventDefault();
  2260. }
  2261. // Hot key [Alt+W] to open the settings dialog
  2262. if (e.keyCode == '87' && e.altKey){
  2263. showSetting();
  2264. e.preventDefault();
  2265. }
  2266.  
  2267. // Hot key [Alt+Z] to open the settings dialog
  2268. if (e.keyCode == '90' && e.altKey){
  2269. showDebugDOM();
  2270. e.preventDefault();
  2271. }
  2272.  
  2273. // Hot key [Alt+R] to open the settings dialog
  2274. if (e.keyCode == '82' && e.altKey){
  2275. reloadScript();
  2276. e.preventDefault();
  2277. }
  2278. });
  2279.  
  2280. $('body').on('change', '.IG_SN_DIG input',function(e){
  2281. var name = $(this).attr('id');
  2282.  
  2283. if(name && USER_SETTING[name] !== undefined){
  2284. let isChecked = $(this).prop('checked');
  2285. GM_setValue(name, isChecked);
  2286. USER_SETTING[name] = isChecked;
  2287.  
  2288. console.log('user settings', name, isChecked);
  2289. }
  2290. });
  2291.  
  2292. $('body').on('click', '.IG_SN_DIG .globalSettings',function(e){
  2293. if($(this).find('#tempWrapper').length > 0){
  2294. e.preventDefault();
  2295. }
  2296. });
  2297.  
  2298. $('body').on('change', '.IG_SN_DIG #tempWrapper input:not(#date_format)',function(){
  2299. let value = $(this).val();
  2300.  
  2301. if($(this).attr('type') == 'range'){
  2302. $(this).next().val(value);
  2303. }
  2304. else{
  2305. $(this).prev().val(value);
  2306. }
  2307.  
  2308. if(value >= 0 && value <= 1){
  2309. VIDEO_VOLUME = value;
  2310. GM_setValue('G_VIDEO_VOLUME', value);
  2311. }
  2312. });
  2313.  
  2314. $('body').on('input', '.IG_SN_DIG #tempWrapper input:not(#date_format)',function(e){
  2315. if($(this).attr('type') == 'range'){
  2316. let value = $(this).val();
  2317. $(this).next().val(value);
  2318. }
  2319. else{
  2320. let value = $(this).val();
  2321. if(value >= 0 && value <= 1){
  2322. $(this).prev().val(value);
  2323. }
  2324. else{
  2325. if(value < 0){
  2326. $(this).val(0);
  2327. }
  2328. else{
  2329. $(this).val(1);
  2330. }
  2331. }
  2332. }
  2333. });
  2334.  
  2335. $('body').on('input', '.IG_SN_DIG #tempWrapper input#date_format',function(e){
  2336. GM_setValue('G_RENAME_FORMAT', $(this).val());
  2337. RENAME_FORMAT = $(this).val();
  2338. });
  2339.  
  2340. $('body').on('click','a[data-needed="direct"]', function(e){
  2341. e.preventDefault();
  2342. triggerLinkElement(this);
  2343. });
  2344.  
  2345. $('body').on('click','.IG_SN_DIG_BODY .newTab', function(){
  2346. // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video)
  2347.  
  2348. if(USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST){
  2349. triggerLinkElement( $(this).parent().children('a').first()[0], true);
  2350. }
  2351. else{
  2352. var urlObj = new URL($(this).parent().children('a').attr('data-href'));
  2353. urlObj.host = 'scontent.cdninstagram.com';
  2354.  
  2355. openNewTab(urlObj.href);
  2356. }
  2357. });
  2358.  
  2359. $('body').on('click','.IG_SN_DIG_BODY .videoThumbnail', function(){
  2360. saveFiles($(this).parent().children('a').find('img').first().attr('src'), $(this).parent().children('a').attr('data-username'), 'thumbnail', new Date().getTime(), 'jpg', $('#article-id').text());
  2361. });
  2362.  
  2363. // Running if user left-click download icon in stories
  2364. $('body').on('click','.IG_DWSTORY',function(){
  2365. onStory(true);
  2366. });
  2367.  
  2368. // Running if user left-click 'open in new tab' icon in stories
  2369. $('body').on('click','.IG_DWNEWTAB',function(e){
  2370. e.preventDefault();
  2371. onStory(true, true, true);
  2372. });
  2373.  
  2374. // Running if user left-click download thumbnail icon in stories
  2375. $('body').on('click','.IG_DWSTORY_THUMBNAIL',function(){
  2376. onStoryThumbnail(true);
  2377. });
  2378.  
  2379. // Running if user left-click download icon in profile
  2380. $('body').on('click','.IG_DWPROFILE',function(e){
  2381. e.stopPropagation();
  2382. onProfileAvatar(true);
  2383. });
  2384.  
  2385. // Running if user left-click download icon in highlight stories
  2386. $('body').on('click','.IG_DWHISTORY',function(){
  2387. onHighlightsStory(true);
  2388. });
  2389. // Running if user left-click 'open in new tab' icon in highlight stories
  2390. $('body').on('click','.IG_DWHINEWTAB',function(e){
  2391. e.preventDefault();
  2392. onHighlightsStory(true, true);
  2393. });
  2394.  
  2395. // Running if user left-click thumbnail download icon in highlight stories
  2396. $('body').on('click','.IG_DWHISTORY_THUMBNAIL',function(){
  2397. onHighlightsStoryThumbnail(true);
  2398. });
  2399.  
  2400. // Running if user left-click download icon in reels
  2401. $('body').on('click','.IG_REELS',function(){
  2402. onReels(true,true);
  2403. });
  2404.  
  2405. // Running if user left-click newtab icon in reels
  2406. $('body').on('click','.IG_REELS_NEWTAB',function(){
  2407. onReels(true,true,true);
  2408. });
  2409.  
  2410. // Running if user left-click download icon in reels
  2411. $('body').on('click','.IG_REELS_THUMBNAIL',function(){
  2412. onReels(true,false);
  2413. });
  2414.  
  2415. // Running if user right-click profile picture in stories area
  2416. $('body').on('contextmenu','button[role="menuitem"]',function(){
  2417. if(location.href === 'https://www.instagram.com/' && USER_SETTING.REDIRECT_RIGHT_CLICK_USER_STORY_PICTURE){
  2418. if($(this).find('canvas._aarh').length > 0){
  2419. location.href = 'https://www.instagram.com/'+$(this).children('div').last().text();
  2420. }
  2421. }
  2422. });
  2423.  
  2424. $('body').on('change', '.IG_SN_DIG_TITLE .checkbox', function(){
  2425. var isChecked = $(this).find('input').prop('checked');
  2426. $('.IG_SN_DIG_BODY .inner_box').each(function(){
  2427. $(this).prop('checked', isChecked);
  2428. });
  2429. });
  2430.  
  2431. $('body').on('change', '.IG_SN_DIG_BODY .inner_box', function(){
  2432. var checked = $('.IG_SN_DIG_BODY .inner_box:checked').length;
  2433. var total = $('.IG_SN_DIG_BODY .inner_box').length;
  2434.  
  2435.  
  2436. $('.IG_SN_DIG_TITLE .checkbox').find('input').prop('checked', checked == total);
  2437. });
  2438.  
  2439. $('body').on('click', '.IG_SN_DIG_TITLE #batch_download_selected', function(){
  2440. let index = 0;
  2441. $('.IG_SN_DIG_BODY a[data-needed="direct"]').each(function(){
  2442. if($(this).prev().children('input').prop('checked')){
  2443. $(this).click();
  2444. index++;
  2445. }
  2446. });
  2447.  
  2448. if(index == 0){
  2449. alert(_i18n('NO_CHECK_RESOURCE'));
  2450. }
  2451. });
  2452.  
  2453. $('body').on('change', '.IG_SN_DIG_TITLE #langSelect', function(){
  2454. GM_setValue('lang', $(this).val());
  2455. lang = $(this).val();
  2456.  
  2457. showSetting();
  2458. });
  2459.  
  2460. $('body').on('change', '.IG_SN_DIG_BODY #locateSelect', function(){
  2461. $('#locatePreview').text(`${(new Date().toLocaleString($(this).val(), {hour12: false, second: "2-digit" ,minute: "2-digit", hour: "2-digit", month: "2-digit", day: "2-digit", year: "numeric"})).replaceAll('/','-')}`);
  2462. LOCATE_DATE_FORMAT = $(this).val();
  2463. GM_setValue('G_LOCATE_DATE_FORMAT', $(this).val());
  2464. });
  2465.  
  2466. $('body').on('click', '.IG_SN_DIG_TITLE #batch_download_direct', function(){
  2467. $('.IG_SN_DIG_BODY a[data-needed="direct"]').each(function(){
  2468. $(this).click();
  2469. });
  2470. });
  2471. });
  2472. })(jQuery);