IG小助手

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

  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 3.8.8
  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_info
  18. // @grant GM_addStyle
  19. // @grant GM_setValue
  20. // @grant GM_getValue
  21. // @grant GM_xmlhttpRequest
  22. // @grant GM_registerMenuCommand
  23. // @grant GM_unregisterMenuCommand
  24. // @grant GM_getResourceText
  25. // @grant GM_notification
  26. // @grant GM_openInTab
  27. // @connect i.instagram.com
  28. // @connect raw.githubusercontent.com
  29. // @require https://code.jquery.com/jquery-3.7.1.min.js#sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=
  30. // @resource INTERNAL_CSS https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/style.css
  31. // @resource LOCALE_MANIFEST https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/locale/manifest.json
  32. // @supportURL https://github.com/SN-Koarashi/ig-helper/
  33. // @contributionURL https://ko-fi.com/snkoarashi
  34. // @icon https://www.google.com/s2/favicons?domain=www.instagram.com&sz=32
  35. // @compatible firefox >= 100
  36. // @compatible chrome >= 100
  37. // @compatible edge >= 100
  38. // @license GPL-3.0-only
  39. // @run-at document-idle
  40. // ==/UserScript==
  41.  
  42. // eslint-disable-next-line no-unused-vars
  43. (function ($) {
  44. 'use strict';
  45.  
  46. /* initial */
  47.  
  48. /******** USER SETTINGS ********/
  49. // !!! DO NOT CHANGE THIS AREA !!!
  50. // ??? PLEASE CHANGE SETTING WITH MENU ???
  51. const USER_SETTING = {
  52. 'AUTO_RENAME': true,
  53. 'CAPTURE_IMAGE_VIA_MEDIA_CACHE': true,
  54. 'CHECK_FOR_UPDATE': true,
  55. 'DIRECT_DOWNLOAD_ALL': false,
  56. 'DIRECT_DOWNLOAD_STORY': false,
  57. 'DIRECT_DOWNLOAD_VISIBLE_RESOURCE': false,
  58. 'DISABLE_VIDEO_LOOPING': false,
  59. 'FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED': false,
  60. 'FORCE_FETCH_ALL_RESOURCES': false,
  61. 'FORCE_RESOURCE_VIA_MEDIA': false,
  62. 'HTML5_VIDEO_CONTROL': false,
  63. 'MODIFY_RESOURCE_EXIF': false,
  64. 'MODIFY_VIDEO_VOLUME': false,
  65. 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST': false,
  66. 'REDIRECT_CLICK_USER_STORY_PICTURE': false,
  67. 'RENAME_PUBLISH_DATE': true,
  68. 'SCROLL_BUTTON': true,
  69. 'SKIP_VIEW_STORY_CONFIRM': false
  70. };
  71.  
  72. const PARENT_CHILD_MAPPING = {
  73. 'AUTO_RENAME': [
  74. 'RENAME_PUBLISH_DATE'
  75. ],
  76. 'FORCE_RESOURCE_VIA_MEDIA': [
  77. 'FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED',
  78. 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST'
  79. ]
  80. };
  81. const IMAGE_CACHE_KEY = 'URLS_OF_IMAGES_TEMPORARILY_STORED';
  82. const IMAGE_CACHE_MAX_AGE = 12 * 60 * 60 * 1000; // 12h in ms
  83. const IMAGE_MAX_CACHE_ITEMS = 300;
  84. /*******************************/
  85.  
  86. // Icon download by Google Fonts Material Icon
  87. const SVG = {
  88. DOWNLOAD: '<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg>',
  89. NEW_TAB: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>',
  90. THUMBNAIL: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>',
  91. DOWNLOAD_ALL: '<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><polygon points="18,6.41 16.59,5 12,9.58 7.41,5 6,6.41 12,12.41"/><polygon points="18,13 16.59,11.59 12,16.17 7.41,11.59 6,13 12,19"/></g></g></svg>',
  92. CLOSE: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>',
  93. FULLSCREEN: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>',
  94. TURN_DEG: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#1f1f1f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.34 6.41L.86 12.9l6.49 6.48 6.49-6.48-6.5-6.49zM3.69 12.9l3.66-3.66L11 12.9l-3.66 3.66-3.65-3.66zm15.67-6.26C17.61 4.88 15.3 4 13 4V.76L8.76 5 13 9.24V6c1.79 0 3.58.68 4.95 2.05 2.73 2.73 2.73 7.17 0 9.9C16.58 19.32 14.79 20 13 20c-.97 0-1.94-.21-2.84-.61l-1.49 1.49C10.02 21.62 11.51 22 13 22c2.3 0 4.61-.88 6.36-2.64 3.52-3.51 3.52-9.21 0-12.72z"/></svg>'
  95. };
  96.  
  97. /*******************************/
  98. const checkInterval = 250;
  99. const style = GM_getResourceText("INTERNAL_CSS");
  100. const locale_manifest = JSON.parse(GM_getResourceText("LOCALE_MANIFEST"));
  101.  
  102. var state = {
  103. videoVolume: (GM_getValue('G_VIDEO_VOLUME')) ? GM_getValue('G_VIDEO_VOLUME') : 1,
  104. tempFetchRateLimit: false,
  105. fileRenameFormat: (GM_getValue('G_RENAME_FORMAT')) ? GM_getValue('G_RENAME_FORMAT') : '%USERNAME%-%SOURCE_TYPE%-%SHORTCODE%-%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%%SECOND%_%ORIGINAL_NAME_FIRST%',
  106. registerMenuIds: [],
  107. locale: {},
  108. lang: GM_getValue('UI_LANGUAGE') || navigator.language || navigator.userLanguage,
  109. currentURL: location.href,
  110. firstStarted: false,
  111. pageLoaded: false,
  112. GL_registerEventList: [],
  113. GL_logger: [],
  114. GL_referrer: null,
  115. GL_postPath: null,
  116. GL_username: null,
  117. GL_repeat: null,
  118. GL_dataCache: {
  119. stories: {},
  120. highlights: {}
  121. },
  122. GL_observer: new MutationObserver(function () {
  123. onReadyMyDW();
  124. }),
  125. GL_imageCache: GM_getValue(IMAGE_CACHE_KEY, {})
  126. };
  127. /*******************************/
  128.  
  129. // initialization script
  130. initSettings();
  131. GM_addStyle(style);
  132. registerMenuCommand();
  133.  
  134. getTranslationText(state.lang).then((res) => {
  135. state.locale[state.lang] = res;
  136. repaintingTranslations();
  137. registerMenuCommand();
  138. checkingScriptUpdate(300);
  139. }).catch((err) => {
  140. registerMenuCommand();
  141. checkingScriptUpdate(300);
  142.  
  143. if (!state.lang.startsWith('en')) {
  144. console.error('getTranslationText catch error:', err);
  145. }
  146. });
  147.  
  148. logger('Script Loaded', GM_info.script.name, 'version:', GM_info.script.version);
  149. purgeCache();
  150. /*******************************/
  151.  
  152. // Main Timer
  153. // eslint-disable-next-line no-unused-vars
  154. var timer = setInterval(function () {
  155. // page loading or unnecessary route
  156. if ($('div#splash-screen').length > 0 && !$('div#splash-screen').is(':hidden') ||
  157. location.pathname.match(/^\/(explore(\/.*)?|challenge\/?.*|direct\/?.*|qr\/?|accounts\/.*|emails\/.*|language\/?.*?|your_activity\/?.*|settings\/help(\/.*)?$)$/ig) ||
  158. !location.hostname.startsWith('www.') || location.pathname.startsWith('/auth_platform/codeentry/') || location.pathname.startsWith('/challenge/') ||
  159. location.pathname.startsWith('/consent/') || location.pathname.startsWith('/accounts/') ||
  160. ((location.pathname.endsWith('/followers/') || location.pathname.endsWith('/following/')) && ($(`body > div[class]:not([id^="mount"]) div div[role="dialog"]`).length > 0))
  161. ) {
  162. state.pageLoaded = false;
  163. return;
  164. }
  165.  
  166. if (state.currentURL != location.href || !state.firstStarted || !state.pageLoaded) {
  167. console.log('Main Timer', 'trigging');
  168.  
  169. clearInterval(state.GL_repeat);
  170. state.pageLoaded = false;
  171. state.firstStarted = true;
  172. state.currentURL = location.href;
  173. state.GL_observer.disconnect();
  174.  
  175. if (location.pathname.startsWith("/p/") || location.pathname.match(/^\/(.*?)\/(p|reel)\//ig) || location.pathname.startsWith("/reel/")) {
  176. state.GL_dataCache.stories = {};
  177. state.GL_dataCache.highlights = {};
  178.  
  179. logger('isDialog');
  180.  
  181. // This is a delayed function call that prevents the dialog element from appearing before the function is called.
  182. var dialogTimer = setInterval(() => {
  183. // body > div[id^="mount"] section nav + div > article << (mobile page in single post) >>
  184. // section:visible > main > div > div > div > div > div > hr << (single foreground post in page, non-floating // <hr> element here is literally the line beneath poster's username) >>
  185. // section:visible > main > div > div > article > div > div > div > div > div > header (is the same as above, except that this is on the route of the /{username}/p/{shortcode} structure)
  186. // section:visible > main > div > div.xdt5ytf << (former CSS selector for single foreground post in page, non-floating) >>
  187. // <hr> is much more unique element than "div.xdt5ytf"
  188. if ($(`body > div[class]:not([id^="mount"]) div div[role="dialog"] article,
  189. section:visible > main > div > div > div > div > div > hr,
  190. body > div[id^="mount"] section nav + div > article,
  191. section:visible > main > div > div > article > div > div > div > div > div > header
  192. `).length > 0) {
  193. clearInterval(dialogTimer);
  194.  
  195. // This is to prevent the detection of the "Modify Video Volume" setting from being too slow.
  196. setTimeout(() => {
  197. onReadyMyDW(false);
  198. }, 15);
  199. }
  200. }, 100);
  201.  
  202. state.pageLoaded = true;
  203. }
  204.  
  205. if (location.pathname.startsWith("/reels/")) {
  206. logger('isReelsPage');
  207. setTimeout(() => {
  208. onReels(false);
  209. }, 150);
  210. state.pageLoaded = true;
  211. }
  212.  
  213. if (location.pathname === "/") {
  214. state.GL_dataCache.stories = {};
  215. state.GL_dataCache.highlights = {};
  216.  
  217. let hasReferrer = state.GL_referrer?.match(/^\/(stories|highlights)\//ig) != null;
  218.  
  219. logger('isHomepage', hasReferrer);
  220. setTimeout(() => {
  221. onReadyMyDW(false, hasReferrer);
  222.  
  223. const element = $('div[id^="mount"] > div > div div > section > main div:not([class]):not([style]) > div > article')?.parent()[0];
  224. if (element) {
  225. state.GL_observer.observe(element, {
  226. childList: true
  227. });
  228. }
  229. }, 150);
  230.  
  231. state.pageLoaded = true;
  232. }
  233.  
  234. if (
  235. $('header > *[class]:first-child img[alt]').length &&
  236. // eslint-disable-next-line no-useless-escape
  237. location.pathname.match(/^(\/)([0-9A-Za-z\.\-_]+)\/?(tagged|reels|saved)?\/?$/ig) &&
  238. !location.pathname.match(/^(\/explore\/?$|\/stories(\/.*)?$|\/p\/)/ig)
  239. ) {
  240. logger('isProfile');
  241. setTimeout(() => {
  242. onProfileAvatar(false);
  243. }, 150);
  244. state.pageLoaded = true;
  245. }
  246.  
  247. if (!state.pageLoaded) {
  248. // Call Instagram stories function
  249. if (location.pathname.startsWith("/stories/highlights/")) {
  250. state.GL_dataCache.highlights = {};
  251.  
  252. logger('isHighlightsStory');
  253.  
  254. onHighlightsStory(false);
  255. state.GL_repeat = setInterval(() => {
  256. onHighlightsStoryThumbnail(false);
  257. }, checkInterval);
  258.  
  259. if ($(".IG_DWHISTORY").length) {
  260. setTimeout(() => {
  261. if (USER_SETTING.SKIP_VIEW_STORY_CONFIRM) {
  262. var $viewStoryButton = $('div[id^="mount"] section:last-child > div > div div[role="button"]').filter(function () {
  263. return $(this).children().length === 0 && this.textContent.trim() !== "";
  264. });
  265. $viewStoryButton?.trigger("click");
  266. }
  267.  
  268. state.pageLoaded = true;
  269. }, 150);
  270. }
  271. }
  272. else if (location.pathname.startsWith("/stories/")) {
  273. logger('isStory');
  274.  
  275. /*
  276. *
  277. * $('body div[id^="mount"] > div > div > div[class]').length >= 2 &&
  278. * $('body div[id^="mount"] > div > div > div[class]').last().find('svg > path[d^="M16.792"], svg > path[d^="M34.6 3.1c-4.5"]').length > 0 &&
  279. * $('body div[id^="mount"] > div > div > div[class]').last().find('svg > polyline + line').length > 0
  280. *
  281. */
  282. // ? detect logo element in left-top corner
  283. if ($('div[id^="mount"] section > div > a[href="/"]').length > 0 || $('div[id^="mount"] section > div > a[href^="/?hl="]').length > 0) {
  284. $('.IG_DWSTORY').remove();
  285. $('.IG_DWNEWTAB').remove();
  286. if ($('.IG_DWSTORY_THUMBNAIL').length) {
  287. $('.IG_DWSTORY_THUMBNAIL').remove();
  288. }
  289.  
  290. onStory(false);
  291.  
  292. // Prevent buttons from being eaten by black holes sometimes
  293. setTimeout(() => {
  294. onStory(false);
  295. }, 150);
  296. }
  297.  
  298. if ($(".IG_DWSTORY").length) {
  299. setTimeout(() => {
  300. if (USER_SETTING.SKIP_VIEW_STORY_CONFIRM) {
  301. var $viewStoryButton = $('div[id^="mount"] section:last-child > div > div div[role="button"]').filter(function () {
  302. return $(this).children().length === 0 && this.textContent.trim() !== "";
  303. });
  304. $viewStoryButton?.trigger("click");
  305. }
  306.  
  307. state.pageLoaded = true;
  308. }, 150);
  309. }
  310. }
  311. else {
  312. state.pageLoaded = false;
  313. // Remove icons
  314. if ($('.IG_DWSTORY').length) {
  315. $('.IG_DWSTORY').remove();
  316. }
  317. if ($('.IG_DWSTORY_ALL').length) {
  318. $('.IG_DWSTORY_ALL').remove();
  319. }
  320. if ($('.IG_DWNEWTAB').length) {
  321. $('.IG_DWNEWTAB').remove();
  322. }
  323. if ($('.IG_DWSTORY_THUMBNAIL').length) {
  324. $('.IG_DWSTORY_THUMBNAIL').remove();
  325. }
  326.  
  327. if ($('.IG_DWHISTORY').length) {
  328. $('.IG_DWHISTORY').remove();
  329. }
  330. if ($('.IG_DWHISTORY_ALL').length) {
  331. $('.IG_DWHISTORY_ALL').remove();
  332. }
  333. if ($('.IG_DWHINEWTAB').length) {
  334. $('.IG_DWHINEWTAB').remove();
  335. }
  336. if ($('.IG_DWHISTORY_THUMBNAIL').length) {
  337. $('.IG_DWHISTORY_THUMBNAIL').remove();
  338. }
  339. }
  340. }
  341.  
  342. checkingScriptUpdate(300);
  343. state.GL_referrer = new URL(location.href).pathname;
  344. }
  345. }, checkInterval);
  346.  
  347. /* Main functions */
  348.  
  349. /**
  350. * onHighlightsStoryAll
  351. * @description Trigger user's highlight all download event.
  352. *
  353. * @return {void}
  354. */
  355. async function onHighlightsStoryAll() {
  356. updateLoadingBar(true);
  357.  
  358. let date = new Date().getTime();
  359. let timestamp = Math.floor(date / 1000);
  360. let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1);
  361. let highStories = await getHighlightStories(highlightId);
  362. let username = highStories.data.reels_media[0].owner.username;
  363.  
  364. if (USER_SETTING.DIRECT_DOWNLOAD_STORY) {
  365.  
  366. let complete = 0;
  367. setDownloadProgress(complete, highStories.data.reels_media[0].items.length);
  368.  
  369. highStories.data.reels_media[0].items.forEach((item, idx) => {
  370. setTimeout(() => {
  371. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  372. timestamp = item.taken_at_timestamp;
  373. }
  374.  
  375. item.display_resources.sort(function (a, b) {
  376. if (a.config_width < b.config_width) return 1;
  377. if (a.config_width > b.config_width) return -1;
  378. return 0;
  379. });
  380.  
  381. if (item.is_video) {
  382. saveFiles(item.video_resources[0].src, username, "highlights", timestamp, 'mp4', item.id).then(() => {
  383. setDownloadProgress(++complete, highStories.data.reels_media[0].items.length);
  384. });
  385. }
  386. else {
  387. saveFiles(item.display_resources[0].src, username, "highlights", timestamp, 'jpg', item.id).then(() => {
  388. setDownloadProgress(++complete, highStories.data.reels_media[0].items.length);
  389. });
  390. }
  391. }, 100 * idx);
  392. });
  393. }
  394. else {
  395. IG_createDM(false, true);
  396. createStoryListDOM(highStories, 'highlights');
  397. }
  398. }
  399.  
  400. /**
  401. * onHighlightsStory
  402. * @description Trigger user's highlight download event or button display event.
  403. *
  404. * @param {Boolean} isDownload - Check if it is a download operation
  405. * @param {Boolean} isPreview - Check if it is need to open new tab
  406. * @return {void}
  407. */
  408. async function onHighlightsStory(isDownload, isPreview) {
  409. var username = $('body > div section:visible a[href^="/"]').filter(function () {
  410. return $(this).attr('href').split('/').filter(e => e.length > 0).length === 1
  411. }).first().attr('href').split('/').filter(e => e.length > 0).at(0);
  412.  
  413. if (isDownload) {
  414. let date = new Date().getTime();
  415. let timestamp = Math.floor(date / 1000);
  416. let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1);
  417. let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length ||
  418. $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length ||
  419. $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length;
  420. let target = 0;
  421.  
  422. updateLoadingBar(true);
  423.  
  424. if (state.GL_dataCache.highlights[highlightId]) {
  425. logger('Fetch from memory cache:', highlightId);
  426.  
  427. let totIndex = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items.length;
  428. username = state.GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username;
  429. target = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex - nowIndex];
  430. }
  431. else {
  432. let highStories = await getHighlightStories(highlightId);
  433. let totIndex = highStories.data.reels_media[0].items.length;
  434. username = highStories.data.reels_media[0].owner.username;
  435. target = highStories.data.reels_media[0].items[totIndex - nowIndex];
  436.  
  437. state.GL_dataCache.highlights[highlightId] = highStories;
  438. }
  439.  
  440. logger('onHighlightsStory', highlightId, state.GL_dataCache.highlights[highlightId]);
  441.  
  442.  
  443. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  444. timestamp = target.taken_at_timestamp;
  445. }
  446.  
  447. if (USER_SETTING.CAPTURE_IMAGE_VIA_MEDIA_CACHE) {
  448. const cached = getImageFromCache(target.id);
  449. if (cached && !state.GL_dataCache.highlights[highlightId].data.reels_media[0].items.filter(item => item.id === target.id).at(0).is_video) {
  450. logger("[Restore Cached onHighlight]", target.id);
  451. if (isPreview) {
  452. openNewTab(cached);
  453. }
  454. else {
  455. saveFiles(cached, username, "stories", timestamp, 'jpg', target.id);
  456. }
  457. return;
  458. }
  459. }
  460.  
  461. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) {
  462. let result = await getMediaInfo(target.id);
  463.  
  464. if (result.status === 'ok') {
  465. if (result.items[0].video_versions) {
  466. if (isPreview) {
  467. openNewTab(result.items[0].video_versions[0].url);
  468. }
  469. else {
  470. saveFiles(result.items[0].video_versions[0].url, username, "highlights", timestamp, 'mp4', result.items[0].id);
  471. }
  472. }
  473. else {
  474. if (isPreview) {
  475. openNewTab(result.items[0].image_versions2.candidates[0].url);
  476. }
  477. else {
  478. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "highlights", timestamp, 'jpg', result.items[0].id);
  479. }
  480. }
  481. }
  482. else {
  483. if (USER_SETTING.FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED) {
  484. delete state.GL_dataCache.highlights[highlightId];
  485. state.tempFetchRateLimit = true;
  486.  
  487. onHighlightsStory(true, isPreview);
  488. }
  489. else {
  490. alert('Fetch failed from Media API. API response message: ' + result.message);
  491. }
  492.  
  493. logger(result);
  494. }
  495. }
  496. else {
  497. if (target.is_video) {
  498. if (isPreview) {
  499. openNewTab(target.video_resources.at(-1).src, username);
  500. }
  501. else {
  502. saveFiles(target.video_resources.at(-1).src, username, "highlights", timestamp, 'mp4', target.id);
  503. }
  504. }
  505. else {
  506. if (isPreview) {
  507. openNewTab(target.display_resources.at(-1).src, username);
  508. }
  509. else {
  510. saveFiles(target.display_resources.at(-1).src, username, "highlights", timestamp, 'jpg', target.id);
  511. }
  512. }
  513.  
  514. state.tempFetchRateLimit = false;
  515. }
  516.  
  517. updateLoadingBar(false);
  518. }
  519. else {
  520. // Add the stories download button
  521. if (!$('.IG_DWHISTORY').length) {
  522. let $element = null;
  523.  
  524. // Default detecter (section layout mode)
  525. if ($('body > div section._ac0a').length > 0) {
  526. $element = $('body > div section:visible._ac0a');
  527. }
  528. else {
  529. $element = $('body > div section:visible > div > div[style]:not([class])');
  530. $element.css('position', 'relative');
  531. }
  532.  
  533. // Detecter for div layout mode
  534. if ($element.length === 0) {
  535. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  536. let nowSize = 0;
  537.  
  538. $$element.each(function () {
  539. if ($(this).width() > nowSize) {
  540. nowSize = $(this).width();
  541. $element = $(this).children('div').first();
  542. }
  543. });
  544. }
  545.  
  546.  
  547. if ($element != null) {
  548. //$element.css('position','relative');
  549. $element.append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DWHISTORY">${SVG.DOWNLOAD}</div>`);
  550. $element.append(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_DWHINEWTAB">${SVG.NEW_TAB}</div>`);
  551.  
  552. let $header = getStoryProgress(username);
  553. if ($header.length > 1) {
  554. $element.append(`<div data-ih-locale-title="DW_ALL" title="${_i18n("DW_ALL")}" class="IG_DWHISTORY_ALL">${SVG.DOWNLOAD_ALL}</div>`);
  555. }
  556.  
  557. // replace something times ago format to publish time in first init
  558. let publishTitle = $header.parents("div[class]").find("time[datetime]")?.attr('title');
  559. if (publishTitle != null) {
  560. $header.parents("div[class]").find("time[datetime]").text(publishTitle);
  561. }
  562.  
  563. //// Modify video volume
  564. //if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  565. // $element.find('video').each(function(){
  566. // $(this).on('play playing', function(){
  567. // if(!$(this).data('modify')){
  568. // $(this).attr('data-modify', true);
  569. // this.volume = VIDEO_VOLUME;
  570. // logger('(highlight) Added video event listener #modify');
  571. // }
  572. // });
  573. // });
  574. //}
  575.  
  576. // Make sure to first remove thumbnail button if still exists and highlight is a picture
  577. $element.find('img[referrerpolicy]').each(function () {
  578. $(this).on('load', function () {
  579. if (!$(this).data('remove-thumbnail')) {
  580. if ($element.find('.IG_DWHISTORY_THUMBNAIL').length === 0) {
  581. $(this).attr('data-remove-thumbnail', true);
  582. $('.IG_DWHISTORY_THUMBNAIL').remove();
  583. logger('(highlight) Manually removing thumbnail button');
  584. }
  585. else {
  586. $(this).attr('data-remove-thumbnail', true);
  587. logger('(highlight) Thumbnail button is not present for this picture');
  588. }
  589. }
  590. });
  591. });
  592.  
  593. // Try to use event listener 'timeupdate' in order to detect if highlight is a video
  594. //$element.find('video').each(function(){
  595. // $(this).on('timeupdate',function(){
  596. // if(!$(this).data('modify-thumbnail')){
  597. // if($element.find('.IG_DWHISTORY_THUMBNAIL').length === 0){
  598. // $(this).attr('data-modify-thumbnail', true);
  599. // onHighlightsStoryThumbnail(false);
  600. // logger('(highlight) Manually inserting thumbnail button');
  601. // }
  602. // else{
  603. // $(this).attr('data-modify-thumbnail', true);
  604. // logger('(highlight) Thumbnail button already inserted');
  605. // }
  606. // }
  607. // });
  608. //});
  609. }
  610. }
  611. }
  612. }
  613.  
  614. /**
  615. * onHighlightsStoryThumbnail
  616. * @description Trigger user's highlight video thumbnail download event or button display event.
  617. *
  618. * @param {Boolean} isDownload - Check if it is a download operation
  619. * @return {void}
  620. */
  621. async function onHighlightsStoryThumbnail(isDownload) {
  622. if (isDownload) {
  623. let date = new Date().getTime();
  624. let timestamp = Math.floor(date / 1000);
  625. let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1);
  626. let username = "";
  627. let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length ||
  628. $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length ||
  629. $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length;
  630. let target = "";
  631.  
  632. updateLoadingBar(true);
  633.  
  634. if (state.GL_dataCache.highlights[highlightId]) {
  635. logger('Fetch from memory cache:', highlightId);
  636.  
  637. let totIndex = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items.length;
  638. username = state.GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username;
  639. target = state.GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex - nowIndex];
  640. }
  641. else {
  642. let highStories = await getHighlightStories(highlightId);
  643. let totIndex = highStories.data.reels_media[0].items.length;
  644. username = highStories.data.reels_media[0].owner.username;
  645. target = highStories.data.reels_media[0].items[totIndex - nowIndex];
  646.  
  647. state.GL_dataCache.highlights[highlightId] = highStories;
  648. }
  649.  
  650. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  651. timestamp = target.taken_at_timestamp;
  652. }
  653.  
  654. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) {
  655. let result = await getMediaInfo(target.id);
  656.  
  657. if (result.status === 'ok') {
  658. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "highlights", timestamp, 'jpg', highlightId);
  659. }
  660. else {
  661. if (USER_SETTING.FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED) {
  662. delete state.GL_dataCache.highlights[highlightId];
  663. state.tempFetchRateLimit = true;
  664.  
  665. onHighlightsStoryThumbnail(true);
  666. }
  667. else {
  668. alert('Fetch failed from Media API. API response message: ' + result.message);
  669. }
  670.  
  671. logger(result);
  672. }
  673. }
  674. else {
  675. saveFiles(target.display_resources.at(-1).src, username, "highlights", timestamp, 'jpg', highlightId);
  676. state.tempFetchRateLimit = false;
  677. }
  678.  
  679. updateLoadingBar(false);
  680. }
  681. else {
  682. if ($('body > div section video.xh8yej3').length) {
  683. // Add the stories thumbnail download button
  684. if (!$('.IG_DWHISTORY_THUMBNAIL').length) {
  685. let $element = null;
  686.  
  687. // Default detecter (section layout mode)
  688. if ($('body > div section._ac0a').length > 0) {
  689. $element = $('body > div section:visible._ac0a');
  690. }
  691. else {
  692. $element = $('body > div section:visible > div > div[style]:not([class])');
  693. $element.css('position', 'relative');
  694. }
  695.  
  696. // Detecter for div layout mode
  697. if ($element.length === 0) {
  698. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  699. let nowSize = 0;
  700.  
  701. $$element.each(function () {
  702. if ($(this).width() > nowSize) {
  703. nowSize = $(this).width();
  704. $element = $(this).children('div').first();
  705. }
  706. });
  707. }
  708.  
  709. if ($element != null) {
  710. $element.append(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="IG_DWHISTORY_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  711. }
  712. }
  713. }
  714. else {
  715. $('.IG_DWHISTORY_THUMBNAIL').remove();
  716. }
  717. }
  718. }
  719.  
  720. /**
  721. * onReadyMyDW
  722. * @description Create an event entry point for the download button for the post.
  723. *
  724. * @param {Boolean} NoDialog - Check if it not showing the dialog
  725. * @param {?Boolean} hasReferrer - Check if the source of the previous page is a story page
  726. * @return {void}
  727. */
  728. function onReadyMyDW(NoDialog, hasReferrer) {
  729. if (hasReferrer === true) {
  730. logger('hasReferrer', 'regenerated');
  731. $('article[data-snig="canDownload"], div[data-snig="canDownload"]').filter(function () {
  732. return $(this).find('.IG_DW_MAIN').length === 0
  733. }).removeAttr('data-snig');
  734. }
  735.  
  736. // Whether is Instagram dialog?
  737. if (NoDialog == false) {
  738. const maxCall = 100;
  739. let i = 0;
  740. var repeat = setInterval(() => {
  741. // section:visible > main > div > div[data-snig="canDownload"] > div > div > div > hr << (single foreground post in page, non-floating // <hr> element here is literally the line beneath poster's username) >>
  742. // section:visible > main > div > div.xdt5ytf[data-snig="canDownload"] << (former CSS selector for single foreground post in page, non-floating) >>
  743. // <hr> is much more unique element than "div.xdt5ytf"
  744. if (i > maxCall || $('article[data-snig="canDownload"], section:visible > main > div > div[data-snig="canDownload"] > div > div > div > hr, div[id^="mount"] > div > div > div.x1n2onr6.x1vjfegm div[data-snig="canDownload"]').length > 0) {
  745. clearInterval(repeat);
  746.  
  747. if (i > maxCall) {
  748. //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.');
  749. console.warn('onReadyMyDW() Timer', 'maximum number of repetitions reached, terminated');
  750. }
  751. }
  752.  
  753. logger('onReadyMyDW() Timer', 'repeating to call detection createDownloadButton()');
  754. createDownloadButton();
  755. i++;
  756. }, 50);
  757. }
  758. else {
  759. createDownloadButton();
  760. }
  761. }
  762.  
  763.  
  764. /**
  765. * initPostVideoFunction
  766. * @description Initialize settings related to the video resources in the post.
  767. *
  768. * @param {Object} $mainElement
  769. * @return {Void}
  770. */
  771. function initPostVideoFunction($mainElement) {
  772. // Disable video autoplay
  773. if (USER_SETTING.DISABLE_VIDEO_LOOPING) {
  774. $mainElement.find('video').each(function () {
  775. $(this).on('ended', function () {
  776. if (!$(this).data('loop')) {
  777. $(this).attr('data-loop', true);
  778. this.pause();
  779. logger('(post) Added video event listener #loop');
  780. }
  781. });
  782. });
  783. }
  784.  
  785. // Modify video volume
  786. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  787. $mainElement.find('video').each(function () {
  788. $(this).on('play playing', function () {
  789. if (!$(this).data('modify')) {
  790. $(this).attr('data-modify', true);
  791. this.volume = state.videoVolume;
  792. logger('(post) Added video event listener #modify');
  793. }
  794. });
  795. });
  796. }
  797.  
  798. if (USER_SETTING.HTML5_VIDEO_CONTROL) {
  799. $mainElement.find('video').each(function () {
  800. if (!$(this).data('controls')) {
  801. let $video = $(this);
  802.  
  803. logger('(post) Added video html5 contorller #modify');
  804.  
  805. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  806. this.volume = state.videoVolume;
  807.  
  808. $(this).on('loadstart', function () {
  809. this.volume = state.videoVolume;
  810. });
  811. }
  812.  
  813. // Restore layout to show details interface
  814. $(this).on('contextmenu', function (e) {
  815. e.preventDefault();
  816. $video.css('z-index', '-1');
  817. $video.removeAttr('controls');
  818. });
  819.  
  820. // Hide layout to show controller
  821. $(this).parent().find('video + div > div').first().on('contextmenu', function (e) {
  822. e.preventDefault();
  823. $video.css('z-index', '2');
  824. $video.attr('controls', true);
  825. });
  826.  
  827. $(this).on('volumechange', function () {
  828. // eslint-disable-next-line no-unused-vars
  829. let $element_mute_button = $(this).parent().find('video + div > div').find('button[type="button"], div[role="button"]').filter(function (idx) {
  830. // This is mute/unmute's icon
  831. return $(this).width() <= 64 && $(this).height() <= 64 && $(this).find('svg > path[d^="M16.636 7.028a1.5"], svg > path[d^="M1.5 13.3c-.8"]').length > 0;
  832. });
  833.  
  834. var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0;
  835.  
  836. if (this.muted != is_elelment_muted) {
  837. this.volume = state.videoVolume;
  838. $element_mute_button?.trigger("click");
  839. }
  840.  
  841. if ($(this).attr('data-completed')) {
  842. state.videoVolume = this.volume;
  843. GM_setValue('G_VIDEO_VOLUME', this.volume);
  844. }
  845.  
  846. if (this.volume == state.videoVolume) {
  847. $(this).attr('data-completed', true);
  848. }
  849. });
  850.  
  851. $(this).css('position', 'absolute');
  852. $(this).css('z-index', '2');
  853. $(this).attr('data-controls', true);
  854. $(this).attr('controls', true);
  855. }
  856. });
  857. }
  858.  
  859. var $videos = $mainElement.find('video');
  860. var $buttonParent = $mainElement.find('video + div > div').first();
  861. toggleVolumeSilder($videos, $buttonParent, 'post', 'bottom');
  862. };
  863.  
  864. /**
  865. * createDownloadButton
  866. * @description Create a download button in the upper right corner of each post.
  867. *
  868. * @return {void}
  869. */
  870. function createDownloadButton() {
  871. // Add download icon per each posts
  872. // eslint-disable-next-line no-unused-vars
  873. $('article, section:visible > main > div > div > div > div > div > hr').map(function (index) {
  874. return $(this).is('section:visible > main > div > div > div > div > div > hr') ? $(this).parent().parent().parent().parent()[0] : this;
  875. }).filter(function () {
  876. return $(this).height() > 0 && $(this).width() > 0
  877. })
  878. .each(function (index) {
  879. // If it is have not download icon
  880. // class x1iyjqo2 mean user profile pages post list container
  881. if (!$(this).attr('data-snig') && !$(this).hasClass('x1iyjqo2') && !$(this).children('article')?.hasClass('x1iyjqo2') && $(this).parents('div#scrollview').length === 0) {
  882. logger("Found post container", $(this));
  883.  
  884. const $mainElement = $(this);
  885. const tagName = this.tagName;
  886. const resourceCountSelector = '._acay ._acaz';
  887. var displayResourceURL;
  888.  
  889. // not loop each in single top post
  890. if (tagName === "DIV" && index != 0) {
  891. return;
  892. }
  893.  
  894. const $childElement = $mainElement.children("div").children("div");
  895.  
  896. if ($childElement.length === 0) return;
  897.  
  898. logger("Found insert point", $childElement);
  899.  
  900. // Modify carousel post counter's position to not interfere with our buttons
  901. if ($mainElement.find('._acay').length > 0) {
  902. if ($mainElement.find('._acay + .x24i39r').length > 0) {
  903. $mainElement.find('._acay + .x24i39r').css('top', '37px');
  904. }
  905.  
  906. const observeNode = $mainElement.find('._acay').first().parent()[0];
  907. var observer = new MutationObserver(function () {
  908. $mainElement.find('._acay + .x24i39r').css('top', '37px');
  909. });
  910.  
  911. observer.observe(observeNode, {
  912. childList: true
  913. });
  914. }
  915.  
  916. $childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).append(`<div class="button_wrapper">`);
  917.  
  918. // Add icons
  919. const DownloadElement = `<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DW_MAIN">${SVG.DOWNLOAD}</div>`;
  920. const NewTabElement = `<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_NEWTAB_MAIN">${SVG.NEW_TAB}</div>`;
  921. const ThumbnailElement = `<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="IG_THUMBNAIL_MAIN">${SVG.THUMBNAIL}</div>`;
  922. const ViewerElement = `<div data-ih-locale-title="IMAGE_VIEWER" title="${_i18n("IMAGE_VIEWER")}" class="IG_IMAGE_VIEWER">${SVG.FULLSCREEN}</div>`;
  923.  
  924. $childElement.find(".button_wrapper").append(DownloadElement);
  925.  
  926. const resource_count = $mainElement.find(resourceCountSelector).length;
  927.  
  928. if (resource_count > 1 && USER_SETTING.DIRECT_DOWNLOAD_VISIBLE_RESOURCE && !USER_SETTING.DIRECT_DOWNLOAD_ALL) {
  929. const DownloadAllElement = `<div data-ih-locale-title="DW_ALL" title="${_i18n("DW_ALL")}" class="IG_DW_ALL_MAIN">${SVG.DOWNLOAD_ALL}</div>`;
  930. $childElement.find(".button_wrapper").append(DownloadAllElement);
  931. }
  932.  
  933. $childElement.find(".button_wrapper").append(NewTabElement);
  934.  
  935. setTimeout(() => {
  936. // Check if visible post is video
  937. if ($childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).find('div > ul li._acaz').length === 0) {
  938. if ($childElement.find('video').length > 0) {
  939. $childElement.find(".button_wrapper").append(ThumbnailElement);
  940. }
  941. else {
  942. displayResourceURL = $mainElement.find('img').filter(function () {
  943. return $(this).width() > 200 && $(this).height() > 200
  944. }).attr('src');
  945. $childElement.find(".button_wrapper").append(ViewerElement);
  946. }
  947. }
  948. else {
  949. // eslint-disable-next-line no-unused-vars
  950. const checkVideoNodeCallback = (entries, observer) => {
  951. entries.forEach((entry) => {
  952. //logger(entry);
  953. if (entry.isIntersecting) {
  954. var $targetNode = $(entry.target);
  955. $childElement.find('.IG_THUMBNAIL_MAIN')?.remove();
  956. $childElement.find('.IG_IMAGE_VIEWER')?.remove();
  957.  
  958. // Check if video?
  959. if ($targetNode.find('video').length > 0) {
  960. if ($childElement.find('.IG_THUMBNAIL_MAIN').length === 0) {
  961. $childElement.find(".button_wrapper").append(ThumbnailElement);
  962. }
  963.  
  964. initPostVideoFunction($mainElement);
  965. }
  966. // is Image
  967. else {
  968. displayResourceURL = $targetNode.find('img').attr('src');
  969. $childElement.find(".button_wrapper").append(ViewerElement);
  970. }
  971. }
  972. });
  973. };
  974.  
  975. const observer_i = new IntersectionObserver(checkVideoNodeCallback, {
  976. root: $mainElement.find('div > ul._acay').first().parent().parent().parent()[0],
  977. rootMargin: "0px",
  978. threshold: 0.1,
  979. });
  980.  
  981. // trigger when switching resources
  982. // eslint-disable-next-line no-unused-vars
  983. const observer = new MutationObserver(function (mutation, owner) {
  984. var target = mutation.at(0)?.target;
  985.  
  986. $(target).find('li._acaz').each(function () {
  987. observer_i.observe(this);
  988. });
  989. });
  990.  
  991. // first onload
  992. $mainElement.find('div > ul li._acaz').each(function () {
  993. observer_i.observe(this);
  994. });
  995.  
  996.  
  997. const element = $childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).find('div > ul li._acaz')?.parent()[0];
  998. const elementAttr = $childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).find('div > ul li._acaz')?.parent().parent()[0];
  999.  
  1000. if (element) {
  1001. observer.observe(element, {
  1002. childList: true
  1003. });
  1004. }
  1005.  
  1006. if (elementAttr) {
  1007. observer.observe(elementAttr, {
  1008. attributes: true
  1009. });
  1010. }
  1011. }
  1012. }, 50);
  1013.  
  1014.  
  1015. $childElement.css('position', 'relative');
  1016.  
  1017. initPostVideoFunction($mainElement);
  1018.  
  1019. state.GL_registerEventList.push({
  1020. element: this,
  1021. trigger: [
  1022. '.IG_THUMBNAIL_MAIN',
  1023. '.IG_NEWTAB_MAIN',
  1024. '.IG_DW_ALL_MAIN',
  1025. '.IG_DW_MAIN',
  1026. '.IG_IMAGE_VIEWER'
  1027. ]
  1028. });
  1029.  
  1030. $(this).on('click', '.IG_IMAGE_VIEWER', function () {
  1031. if (displayResourceURL != null) {
  1032. openImageViewer(displayResourceURL);
  1033. }
  1034. else {
  1035. alert("Cannot find resource url.");
  1036. }
  1037. });
  1038.  
  1039. $(this).on('click', '.IG_THUMBNAIL_MAIN', function () {
  1040. updateLoadingBar(true);
  1041.  
  1042. state.GL_username = $mainElement.attr('data-username');
  1043. state.GL_postPath = location.pathname.replace(/\/$/, '').split('/').at(-1) || $mainElement.find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1044.  
  1045. var index = getVisibleNodeIndex($mainElement);
  1046.  
  1047. IG_createDM(true, false);
  1048.  
  1049. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", "").then(() => {
  1050. let checkBlob = setInterval(() => {
  1051. if ($('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').length > 0) {
  1052. clearInterval(checkBlob);
  1053. var $videoThumbnail = $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY a[data-globalindex="' + (index + 1) + '"]')?.parent().find('.videoThumbnail')?.first();
  1054.  
  1055. if ($videoThumbnail != null && $videoThumbnail.length > 0) {
  1056. $videoThumbnail.trigger("click");
  1057. }
  1058. else {
  1059. alert('Cannot find thumbnail URL.');
  1060. }
  1061.  
  1062. updateLoadingBar(false);
  1063. $('.IG_POPUP_DIG').remove();
  1064. }
  1065. }, 250);
  1066. });
  1067. });
  1068.  
  1069. $(this).on('click', '.IG_NEWTAB_MAIN', function () {
  1070. updateLoadingBar(true);
  1071.  
  1072. state.GL_username = $mainElement.attr('data-username');
  1073. state.GL_postPath = location.pathname.replace(/\/$/, '').split('/').at(-1) || $mainElement.find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1074.  
  1075. var index = getVisibleNodeIndex($mainElement);
  1076.  
  1077. IG_createDM(true, false);
  1078.  
  1079. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", "").then(() => {
  1080. let checkBlob = setInterval(() => {
  1081. if ($('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').length > 0) {
  1082. clearInterval(checkBlob);
  1083. var $linkElement = $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY a[data-globalindex="' + (index + 1) + '"]');
  1084.  
  1085. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST) {
  1086. triggerLinkElement($linkElement.first()[0], true);
  1087. }
  1088. else {
  1089. let href = $linkElement?.attr('data-href');
  1090. if (href) {
  1091. // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video)
  1092. var urlObj = new URL(href);
  1093. urlObj.host = 'scontent.cdninstagram.com';
  1094.  
  1095. openNewTab(urlObj.href);
  1096. }
  1097. else {
  1098. alert('Cannot find open tab URL.');
  1099. }
  1100. }
  1101.  
  1102. updateLoadingBar(false);
  1103. $('.IG_POPUP_DIG').remove();
  1104. }
  1105. }, 250);
  1106. });
  1107. });
  1108.  
  1109. // Running if user click the download all icon
  1110. $(this).on('click', '.IG_DW_ALL_MAIN', async function () {
  1111. state.GL_username = $mainElement.attr('data-username');
  1112. state.GL_postPath = location.pathname.replace(/\/$/, '').split('/').at(-1) || $mainElement.find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1113.  
  1114. // Create element that download dailog
  1115. IG_createDM(USER_SETTING.DIRECT_DOWNLOAD_ALL, true);
  1116.  
  1117. $("#article-id").html(`<a href="https://www.instagram.com/p/${state.GL_postPath}">${state.GL_postPath}</a>`);
  1118.  
  1119. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').each(function () {
  1120. $(this).wrap('<div></div>');
  1121. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  1122. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  1123.  
  1124. if ($(this).attr('data-name') == 'video') {
  1125. $(this).after(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  1126. }
  1127. });
  1128.  
  1129.  
  1130. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE")).then(() => {
  1131. let checkBlob = setInterval(() => {
  1132. if ($('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').length > 0) {
  1133. clearInterval(checkBlob);
  1134. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').each(function () {
  1135. $(this).trigger("click");
  1136. });
  1137.  
  1138. $('.IG_POPUP_DIG').remove();
  1139. }
  1140. }, 250);
  1141. });
  1142. });
  1143.  
  1144. // Running if user click the download icon
  1145. $(this).on('click', '.IG_DW_MAIN', async function () {
  1146. state.GL_username = $mainElement.attr('data-username');
  1147. state.GL_postPath = location.pathname.replace(/\/$/, '').split('/').at(-1) || $mainElement.find('a[href^="/p/"]').first().attr("href").split("/").at(2) || $(this).parent().parent().parent().children("div:last-child").children("div").children("div:last-child").find('a[href^="/p/"]').last().attr("href").split("/").at(2);
  1148.  
  1149. // Create element that download dailog
  1150. IG_createDM(USER_SETTING.DIRECT_DOWNLOAD_ALL, true);
  1151.  
  1152. $("#article-id").html(`<a href="https://www.instagram.com/p/${state.GL_postPath}">${state.GL_postPath}</a>`);
  1153.  
  1154. if (USER_SETTING.DIRECT_DOWNLOAD_VISIBLE_RESOURCE) {
  1155. updateLoadingBar(true);
  1156. IG_setDM(true);
  1157.  
  1158. var index = getVisibleNodeIndex($(this).parent().parent().parent());
  1159.  
  1160. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", "").then(() => {
  1161. let checkBlob = setInterval(() => {
  1162. if ($('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').length > 0) {
  1163. clearInterval(checkBlob);
  1164. var href = $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY a[data-globalindex="' + (index + 1) + '"]')?.attr('data-href');
  1165.  
  1166. if (href) {
  1167. updateLoadingBar(false);
  1168. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY a[data-globalindex="' + (index + 1) + '"]')?.trigger("click");
  1169. }
  1170. else {
  1171. alert('Cannot find download URL.');
  1172. }
  1173.  
  1174. $('.IG_POPUP_DIG').remove();
  1175. }
  1176. }, 250);
  1177. });
  1178.  
  1179. return;
  1180. }
  1181.  
  1182. if (!USER_SETTING.DIRECT_DOWNLOAD_ALL) {
  1183. // Find video/image element and add the download icon
  1184. var s = 0;
  1185. var multiple = $(this).parent().parent().find(resourceCountSelector).length;
  1186. var blob = USER_SETTING.FORCE_FETCH_ALL_RESOURCES;
  1187. var publish_time = new Date(
  1188. $(this).parent().parent().parent().find('a[href] time[datetime]').filter(function () {
  1189. let href = $(this).parents("a[href]").attr("href");
  1190. return href?.startsWith("/p/") || href?.match(/\/([\w.\-_]+)\/p\//ig) != null;
  1191. }).first().attr('datetime')
  1192. ).getTime();
  1193.  
  1194. // If posts have more than one images or videos.
  1195. if (multiple) {
  1196. $(this).parent().parent().find(resourceCountSelector).each(function () {
  1197. let element_videos = $(this).parent().parent().parent().find('video');
  1198. //if(element_videos && element_videos.attr('src') && element_videos.attr('src').match(/^blob:/ig)){
  1199. if (element_videos && element_videos.attr('src')) {
  1200. blob = true;
  1201. }
  1202. });
  1203.  
  1204.  
  1205. if (blob || USER_SETTING.FORCE_RESOURCE_VIA_MEDIA) {
  1206. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE"));
  1207. }
  1208. else {
  1209. $(this).parent().parent().find(resourceCountSelector).each(function () {
  1210. s++;
  1211. let element_videos = $(this).find('video');
  1212. let element_images = $(this).find('._aagv img');
  1213. let imgLink = (element_images.attr('srcset')) ? element_images.attr('srcset').split(" ")[0] : element_images.attr('src');
  1214.  
  1215. if (element_videos && element_videos.attr('src')) {
  1216. blob = true;
  1217. }
  1218. if (element_images && imgLink) {
  1219. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY').append(`<a datetime="${publish_time}" data-needed="direct" data-path="${state.GL_postPath}" data-name="photo" data-type="jpg" data-globalIndex="${s}" href="javascript:;" data-href="${imgLink}"><img width="100" src="${imgLink}" /><br/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${s} -</a>`);
  1220. }
  1221.  
  1222. });
  1223.  
  1224. if (blob) {
  1225. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", _i18n("LOAD_BLOB_RELOAD"));
  1226. }
  1227. }
  1228. }
  1229. else {
  1230. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA) {
  1231. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE"));
  1232. }
  1233. else {
  1234. s++;
  1235. let element_videos = $(this).parent().parent().parent().find('video');
  1236. let element_images = $(this).parent().parent().parent().find('._aagv img');
  1237. let imgLink = (element_images.attr('srcset')) ? element_images.attr('srcset').split(" ")[0] : element_images.attr('src');
  1238.  
  1239.  
  1240. if (element_videos && element_videos.attr('src')) {
  1241. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", _i18n("LOAD_BLOB_ONE"));
  1242. }
  1243. if (element_images && imgLink) {
  1244. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY').append(`<a datetime="${publish_time}" data-needed="direct" data-path="${state.GL_postPath}" data-name="photo" data-type="jpg" data-globalIndex="${s}" href="javascript:;" href="" data-href="${imgLink}"><img width="100" src="${imgLink}" /><br/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${s} -</a>`);
  1245. }
  1246. }
  1247. }
  1248. }
  1249.  
  1250. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').each(function () {
  1251. $(this).wrap('<div></div>');
  1252. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  1253. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  1254.  
  1255. if ($(this).attr('data-name') == 'video') {
  1256. $(this).after(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  1257. }
  1258. });
  1259.  
  1260. if (USER_SETTING.DIRECT_DOWNLOAD_ALL) {
  1261. createMediaListDOM(state.GL_postPath, ".IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE")).then(() => {
  1262. let checkBlob = setInterval(() => {
  1263. if ($('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').length > 0) {
  1264. clearInterval(checkBlob);
  1265. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').each(function () {
  1266. $(this).trigger("click");
  1267. });
  1268.  
  1269. $('.IG_POPUP_DIG').remove();
  1270. }
  1271. }, 250);
  1272. });
  1273. }
  1274. });
  1275.  
  1276. // Add the mark that download is ready
  1277. var username = $(this).find("header > div:last-child > div:first-child span a").first().text() || $(this).find('a[href^="/"]').filter(function () {
  1278. return $(this)?.text()?.length > 0;
  1279. }).first().text();
  1280.  
  1281. $(this).attr('data-snig', 'canDownload');
  1282. $(this).attr('data-username', username);
  1283. }
  1284. });
  1285. }
  1286.  
  1287.  
  1288. /**
  1289. * filterResourceData
  1290. * @description Standardized resource object format.
  1291. *
  1292. * @param {Object} data
  1293. * @return {Object}
  1294. */
  1295. function filterResourceData(data) {
  1296. var resource = data.shortcode_media ?? data;
  1297. if (resource.owner == null && resource.user != null) {
  1298. resource.owner = resource.user;
  1299. }
  1300.  
  1301. if (resource.owner == null) {
  1302. logger('carousel_media:', 'undefined username');
  1303. alert('carousel_media: undefined username');
  1304. }
  1305.  
  1306. return resource;
  1307. }
  1308.  
  1309.  
  1310. /**
  1311. * createMediaListDOM
  1312. * @description Create a list of media elements from post URLs.
  1313. *
  1314. * @param {String} postURL
  1315. * @param {String} selector - Use CSS element selectors to choose where it appears.
  1316. * @param {String} message - i18n display loading message
  1317. * @return {void}
  1318. */
  1319. async function createMediaListDOM(postURL, selector, message) {
  1320. try {
  1321. $(`${selector} a`).remove();
  1322. $(selector).append('<p id="_SNLOAD">' + message + '</p>');
  1323. let result = await getBlobMedia(postURL);
  1324. let resource = filterResourceData(result.data);
  1325.  
  1326. if (result.type === 'query_hash') {
  1327. let idx = 1;
  1328.  
  1329. // GraphVideo
  1330. if (resource.__typename == "GraphVideo" && resource.video_url) {
  1331. $(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/>- <span data-ih-locale="VID">${_i18n("VID")}</span> ${idx} -</a>`);
  1332. idx++;
  1333. }
  1334. // GraphImage
  1335. if (resource.__typename == "GraphImage") {
  1336. $(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/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${idx} -</a>`);
  1337. idx++;
  1338. }
  1339. // GraphSidecar
  1340. if (resource.__typename == "GraphSidecar" && resource.edge_sidecar_to_children) {
  1341. for (let e of resource.edge_sidecar_to_children.edges) {
  1342. if (e.node.__typename == "GraphVideo") {
  1343. $(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/>- <span data-ih-locale-title="VID">${_i18n("VID")}</span> ${idx} -</a>`);
  1344. }
  1345.  
  1346. if (e.node.__typename == "GraphImage") {
  1347. $(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/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${idx} -</a>`);
  1348. }
  1349. idx++;
  1350. }
  1351. }
  1352. }
  1353. else {
  1354. if (resource.carousel_media) {
  1355. logger('carousel_media');
  1356.  
  1357. resource.carousel_media.forEach((mda, ind) => {
  1358. let idx = ind + 1;
  1359. // Image
  1360. if (mda.video_versions == null) {
  1361. mda.image_versions2.candidates.sort(function (a, b) {
  1362. let aSTP = new URL(a.url).searchParams.get('stp');
  1363. let bSTP = new URL(b.url).searchParams.get('stp');
  1364.  
  1365. if (aSTP && bSTP) {
  1366. if (aSTP.length > bSTP.length) return 1;
  1367. if (aSTP.length < bSTP.length) return -1;
  1368. }
  1369. else {
  1370. if (a.width < b.width) return 1;
  1371. if (a.width > b.width) return -1;
  1372. }
  1373.  
  1374. return 0;
  1375. });
  1376.  
  1377. $(selector).append(`<a media-id="${mda.pk}" datetime="${mda.taken_at}" data-blob="true" data-needed="direct" data-path="${resource.code}" data-name="photo" data-type="jpg" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${mda.image_versions2.candidates[0].url}"><img width="100" src="${mda.image_versions2.candidates[0].url}" /><br/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${idx} -</a>`);
  1378. }
  1379. // Video
  1380. else {
  1381. $(selector).append(`<a media-id="${mda.pk}" datetime="${mda.taken_at}" data-blob="true" data-needed="direct" data-path="${resource.code}" data-name="video" data-type="mp4" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${mda.video_versions[0].url}"><img width="100" src="${mda.image_versions2.candidates[0].url}" /><br/>- <span data-ih-locale="VID">${_i18n("VID")}</span> ${idx} -</a>`);
  1382. }
  1383. });
  1384. }
  1385. else {
  1386. let idx = 1;
  1387. // Image
  1388. if (resource.video_versions == null) {
  1389. resource.image_versions2.candidates.sort(function (a, b) {
  1390. let aSTP = new URL(a.url).searchParams.get('stp');
  1391. let bSTP = new URL(b.url).searchParams.get('stp');
  1392.  
  1393. if (aSTP && bSTP) {
  1394. if (aSTP.length > bSTP.length) return 1;
  1395. if (aSTP.length < bSTP.length) return -1;
  1396. }
  1397. else {
  1398. if (a.width < b.width) return 1;
  1399. if (a.width > b.width) return -1;
  1400. }
  1401.  
  1402. return 0;
  1403. });
  1404.  
  1405. $(selector).append(`<a media-id="${resource.pk}" datetime="${resource.taken_at}" data-blob="true" data-needed="direct" data-path="${resource.code}" data-name="photo" data-type="jpg" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${resource.image_versions2.candidates[0].url}"><img width="100" src="${resource.image_versions2.candidates[0].url}" /><br/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${idx} -</a>`);
  1406. }
  1407. // Video
  1408. else {
  1409. $(selector).append(`<a media-id="${resource.pk}" datetime="${resource.taken_at}" data-blob="true" data-needed="direct" data-path="${resource.code}" data-name="video" data-type="mp4" data-username="${resource.owner.username}" data-globalIndex="${idx}" href="javascript:;" data-href="${resource.video_versions[0].url}"><img width="100" src="${resource.image_versions2.candidates[0].url}" /><br/>- <span data-ih-locale="VID">${_i18n("VID")}</span> ${idx} -</a>`);
  1410. }
  1411. }
  1412. }
  1413.  
  1414. $("#_SNLOAD").remove();
  1415. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').each(function () {
  1416. $(this).wrap('<div></div>');
  1417. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  1418. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  1419.  
  1420. if ($(this).attr('data-name') == 'video') {
  1421. $(this).after(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  1422. }
  1423. });
  1424. }
  1425. catch (err) {
  1426. logger('createMediaListDOM', err);
  1427. };
  1428. }
  1429.  
  1430.  
  1431. /**
  1432. * getVisibleNodeIndex
  1433. * @description Get element visible node.
  1434. *
  1435. * @param {Object} $main
  1436. * @return {Integer}
  1437. */
  1438. function getVisibleNodeIndex($main) {
  1439. var index = 0;
  1440. // homepage classList
  1441. var $dot = $main.find('.x1iyjqo2 > div > div:last-child > div');
  1442.  
  1443. // dialog classList, main top classList
  1444. if ($dot == null || !$dot.hasClass('_acnb')) {
  1445. $dot = $main.find('._aatk > div > div:last-child').eq(0).children('div');
  1446. }
  1447.  
  1448. $dot.filter('._acnb').each(function (sIndex) {
  1449. if ($(this).hasClass('_acnf')) {
  1450. index = sIndex;
  1451. }
  1452. });
  1453.  
  1454. return index;
  1455. }
  1456.  
  1457. /**
  1458. * onProfileAvatar
  1459. * @description Trigger user avatar download event or button display event.
  1460. *
  1461. * @param {Boolean} isDownload - Check if it is a download operation
  1462. * @return {void}
  1463. */
  1464. async function onProfileAvatar(isDownload) {
  1465. if (isDownload) {
  1466. updateLoadingBar(true);
  1467.  
  1468. let date = new Date().getTime();
  1469. let timestamp = Math.floor(date / 1000);
  1470. let username = location.pathname.replaceAll(/(reels|tagged)\/$/ig, '').split('/').filter(s => s.length > 0).at(-1);
  1471. let userInfo = await getUserId(username);
  1472.  
  1473. try {
  1474. let dataURL = await getUserHighSizeProfile(userInfo.user.pk);
  1475. saveFiles(dataURL, username, "avatar", timestamp, 'jpg');
  1476. }
  1477. // eslint-disable-next-line no-unused-vars
  1478. catch (err) {
  1479. saveFiles(userInfo.user.profile_pic_url, username, "avatar", timestamp, 'jpg');
  1480. }
  1481.  
  1482. updateLoadingBar(false);
  1483. }
  1484. else {
  1485. // Add the profile download button
  1486. if (!$('.IG_DWPROFILE').length) {
  1487. let profileTimer = setInterval(() => {
  1488. if ($('.IG_DWPROFILE').length) {
  1489. clearInterval(profileTimer);
  1490. return;
  1491. }
  1492.  
  1493. $('header > *[class]:first-child img[alt][draggable]').parent().parent().append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DWPROFILE">${SVG.DOWNLOAD}</div>`);
  1494. $('header > *[class]:first-child img[alt][draggable]').parent().parent().css('position', 'relative');
  1495. $('header > *[class]:first-child img[alt]:not([draggable])').parent().parent().parent().append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DWPROFILE">${SVG.DOWNLOAD}</div>`);
  1496. $('header > *[class]:first-child img[alt]:not([draggable])').parent().parent().parent().css('position', 'relative');
  1497. }, 150);
  1498. }
  1499. }
  1500. }
  1501.  
  1502. /**
  1503. * onReels
  1504. * @description Trigger user's reels download event or button display event.
  1505. *
  1506. * @param {Boolean} isDownload - Check if it is a download operation
  1507. * @param {Boolean} isVideo - Check if reel is a video element
  1508. * @param {Boolean} isPreview - Check if it is need to open new tab
  1509. * @return {void}
  1510. */
  1511. async function onReels(isDownload, isVideo, isPreview) {
  1512. try {
  1513. if (isDownload) {
  1514. updateLoadingBar(true);
  1515.  
  1516. let reelsPath = location.href.split('?').at(0).split('instagram.com/reels/').at(-1).replaceAll('/', '');
  1517. let result = await getBlobMedia(reelsPath);
  1518. let media = filterResourceData(result.data);
  1519.  
  1520. let timestamp = new Date().getTime();
  1521.  
  1522. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1523. if (result.type === 'query_hash') {
  1524. timestamp = media.taken_at_timestamp;
  1525. }
  1526. else {
  1527. timestamp = media.taken_at;
  1528. }
  1529. }
  1530.  
  1531. if (result.type === 'query_hash') {
  1532. if (isVideo && media.is_video) {
  1533. if (isPreview) {
  1534. openNewTab(media.video_url);
  1535. }
  1536. else {
  1537. let type = 'mp4';
  1538. saveFiles(media.video_url, media.owner.username, "reels", timestamp, type, reelsPath);
  1539. }
  1540. }
  1541. else {
  1542. if (isPreview) {
  1543. openNewTab(media.display_resources.at(-1).src);
  1544. }
  1545. else {
  1546. let type = 'jpg';
  1547. saveFiles(media.display_resources.at(-1).src, media.owner.username, "reels", timestamp, type, reelsPath);
  1548. }
  1549. }
  1550. }
  1551. else {
  1552. if (isVideo && media.video_versions != null) {
  1553. if (isPreview) {
  1554. openNewTab(media.video_versions[0].url);
  1555. }
  1556. else {
  1557. let type = 'mp4';
  1558. saveFiles(media.video_versions[0].url, media.owner.username, "reels", timestamp, type, reelsPath);
  1559. }
  1560. }
  1561. else {
  1562. if (isPreview) {
  1563. openNewTab(media.image_versions2.candidates[0].url);
  1564. }
  1565. else {
  1566. let type = 'jpg';
  1567. saveFiles(media.image_versions2.candidates[0].url, media.owner.username, "reels", timestamp, type, reelsPath);
  1568. }
  1569. }
  1570. }
  1571.  
  1572. updateLoadingBar(false);
  1573. }
  1574. else {
  1575. //$('.IG_REELS_THUMBNAIL, .IG_REELS').remove();
  1576. var timer = setInterval(() => {
  1577. if ($('section > main[role="main"] > div div.x1qjc9v5 video').length > 0) {
  1578. clearInterval(timer);
  1579.  
  1580. if (USER_SETTING.SCROLL_BUTTON) {
  1581. $('#scrollWrapper').remove();
  1582. $('section > main[role="main"]').append('<section id="scrollWrapper"></section>');
  1583. $('section > main[role="main"] > #scrollWrapper').append('<div class="button-up"><div></div></div>');
  1584. $('section > main[role="main"] > #scrollWrapper').append('<div class="button-down"><div></div></div>');
  1585.  
  1586. $('section > main[role="main"] > #scrollWrapper > .button-up').on('click', function () {
  1587. $('section > main[role="main"] > div')[0].scrollBy({ top: -30, behavior: "smooth" });
  1588. });
  1589. $('section > main[role="main"] > #scrollWrapper > .button-down').on('click', function () {
  1590. $('section > main[role="main"] > div')[0].scrollBy({ top: 30, behavior: "smooth" });
  1591. });
  1592. }
  1593.  
  1594. // reels scroll has [tabindex] but header not.
  1595. $('section > main[role="main"] > div[tabindex], section > main[role="main"] > div[class]').children('div').each(function () {
  1596. if ($(this).children().length > 0) {
  1597. if (!$(this).children().find('.IG_REELS').length) {
  1598. var $main = $(this);
  1599.  
  1600. $(this).children().css('position', 'relative');
  1601.  
  1602. $(this).children().append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_REELS">${SVG.DOWNLOAD}</div>`);
  1603. $(this).children().append(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_REELS_NEWTAB">${SVG.NEW_TAB}</div>`);
  1604. $(this).children().append(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="IG_REELS_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  1605.  
  1606. // Disable video autoplay
  1607. if (USER_SETTING.DISABLE_VIDEO_LOOPING) {
  1608. $(this).find('video').each(function () {
  1609. $(this).on('ended', function () {
  1610. if (!$(this).data('loop')) {
  1611. let $element_play_button = $(this).next().find('div[role="presentation"] > div svg > path[d^="M5.888"]').parents('button[role="button"], div[role="button"]');
  1612. if ($element_play_button.length > 0) {
  1613. $(this).attr('data-loop', true);
  1614. $element_play_button.trigger("click");
  1615. logger('Adding video event listener #loop, then paused click()');
  1616. }
  1617. else {
  1618. $(this).attr('data-loop', true);
  1619. $(this).parent().find('.xpgaw4o').removeAttr('style');
  1620. this.pause();
  1621. logger('Adding video event listener #loop, then paused pause()');
  1622. }
  1623. }
  1624. });
  1625. });
  1626. }
  1627.  
  1628. // Modify video volume
  1629. //if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  1630. // $(this).find('video').each(function(){
  1631. // $(this).on('play playing', function(){
  1632. // if(!$(this).data('modify')){
  1633. // $(this).attr('data-modify', true);
  1634. // this.volume = VIDEO_VOLUME;
  1635. // logger('(reel) Added video event listener #modify');
  1636. // }
  1637. // });
  1638. // });
  1639. //}
  1640.  
  1641. if (USER_SETTING.HTML5_VIDEO_CONTROL) {
  1642. $(this).find('video').each(function () {
  1643. if (!$(this).data('controls')) {
  1644. let $video = $(this);
  1645.  
  1646. logger('(reel) Added video html5 contorller #modify');
  1647.  
  1648. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  1649. this.volume = state.videoVolume;
  1650.  
  1651. $(this).on('loadstart', function () {
  1652. this.volume = state.videoVolume;
  1653. });
  1654. }
  1655.  
  1656. // Restore layout to show details interface
  1657. $(this).on('contextmenu', function (e) {
  1658. e.preventDefault();
  1659. $video.css('z-index', '-1');
  1660. $video.removeAttr('controls');
  1661. });
  1662.  
  1663. // Hide layout to show controller
  1664. $(this).parent().find('video + div div[role="button"]').filter(function () {
  1665. return $(this).parent('div[role="presentation"]').length > 0 && $(this).css('cursor') === 'pointer' && $(this).attr('style') != null;
  1666. }).first().on('contextmenu', function (e) {
  1667. e.preventDefault();
  1668. $video.css('z-index', '2');
  1669. $video.attr('controls', true);
  1670. });
  1671.  
  1672.  
  1673. $(this).on('volumechange', function () {
  1674. // eslint-disable-next-line no-unused-vars
  1675. let $element_mute_button = $(this).parent().find('video + div > div').find('button[type="button"], div[role="button"]').filter(function (idx) {
  1676. // This is mute/unmute's icon
  1677. return $(this).width() <= 64 && $(this).height() <= 64 && $(this).find('svg > path[d^="M16.636 7.028a1.5"], svg > path[d^="M1.5 13.3c-.8"]').length > 0;
  1678. });
  1679.  
  1680. var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0;
  1681.  
  1682. if (this.muted != is_elelment_muted) {
  1683. this.volume = state.videoVolume;
  1684. $element_mute_button?.trigger("click");
  1685. }
  1686.  
  1687. if ($(this).attr('data-completed')) {
  1688. state.videoVolume = this.volume;
  1689. GM_setValue('G_VIDEO_VOLUME', this.volume);
  1690. }
  1691.  
  1692. if (this.volume == state.videoVolume) {
  1693. $(this).attr('data-completed', true);
  1694. }
  1695. });
  1696.  
  1697. $(this).css('position', 'absolute');
  1698. $(this).css('z-index', '2');
  1699. $(this).attr('data-controls', true);
  1700. $(this).attr('controls', true);
  1701. }
  1702. });
  1703. }
  1704.  
  1705. var $videos = $main.find('video');
  1706. var $buttonParent = $(this).find('div[role="presentation"] > div[role="button"] > div').first();
  1707. toggleVolumeSilder($videos, $buttonParent, 'reel');
  1708. }
  1709. }
  1710. });
  1711. }
  1712. }, 250);
  1713. }
  1714. }
  1715. catch (err) {
  1716. console.error("[reels]", err);
  1717. }
  1718. }
  1719.  
  1720. /**
  1721. * createStoryListDOM
  1722. * @description Create a list of story items in the popup dialog.
  1723. *
  1724. * @return {void}
  1725. */
  1726. async function createStoryListDOM(obj, type) {
  1727. try {
  1728. $('.IG_POPUP_DIG #post_info').text(`${type} ID: ${obj.data.reels_media[0].id}`);
  1729. const selector = '.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY';
  1730.  
  1731. obj.data.reels_media[0].items.forEach((item, idx) => {
  1732. let date = new Date().getTime();
  1733. let timestamp = Math.floor(date / 1000);
  1734. let username = obj.data.reels_media[0]?.user?.username || obj.data.reels_media[0]?.owner?.username;
  1735.  
  1736. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1737. timestamp = item.taken_at_timestamp;
  1738. }
  1739.  
  1740. item.display_resources.sort(function (a, b) {
  1741. if (a.config_width < b.config_width) return 1;
  1742. if (a.config_width > b.config_width) return -1;
  1743. return 0;
  1744. });
  1745.  
  1746. if (item.is_video) {
  1747. $(selector).append(`<a media-id="${item.id}" datetime="${timestamp}" data-blob="true" data-needed="direct" data-name="${type}" data-type="mp4" data-username="${username}" data-path="${item.id}" data-globalIndex="${idx + 1}" href="javascript:;" data-href="${item.video_resources[0].src}"><img width="100" src="${item.display_resources[0].src}" /><br/>- <span data-ih-locale-title="VID">${_i18n("VID")}</span> ${idx} -</a>`);
  1748. }
  1749. else {
  1750. $(selector).append(`<a media-id="${item.id}" datetime="${timestamp}" data-blob="true" data-needed="direct" data-name="${type}" data-type="jpg" data-username="${username}" data-path="${item.id}" data-globalIndex="${idx + 1}" href="javascript:;" data-href="${item.display_resources[0].src}"><img width="100" src="${item.display_resources[0].src}" /><br/>- <span data-ih-locale-title="IMG">${_i18n("IMG")}</span> ${idx} -</a>`);
  1751. }
  1752. });
  1753.  
  1754. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_BODY a').each(function () {
  1755. $(this).wrap('<div></div>');
  1756. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  1757. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  1758.  
  1759. if ($(this).attr('data-type') == 'mp4') {
  1760. $(this).after(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  1761. }
  1762. });
  1763.  
  1764. updateLoadingBar(false);
  1765. }
  1766. catch (err) {
  1767. console.error('createStoryListDOM()', err);
  1768. }
  1769. }
  1770.  
  1771. /**
  1772. * onStoryAll
  1773. * @description Trigger user's story all download event.
  1774. *
  1775. * @return {void}
  1776. */
  1777. async function onStoryAll() {
  1778. updateLoadingBar(true);
  1779.  
  1780. let date = new Date().getTime();
  1781. let timestamp = Math.floor(date / 1000);
  1782. let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split("/").filter(s => s.length > 0).at(1);
  1783.  
  1784. let userInfo = await getUserId(username);
  1785. let userId = userInfo.user.pk;
  1786. let stories = await getStories(userId);
  1787.  
  1788. if (USER_SETTING.DIRECT_DOWNLOAD_STORY) {
  1789. let complete = 0;
  1790. setDownloadProgress(complete, stories.data.reels_media[0].items.length);
  1791.  
  1792. stories.data.reels_media[0].items.forEach((item, idx) => {
  1793. setTimeout(() => {
  1794. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1795. timestamp = item.taken_at_timestamp;
  1796. }
  1797.  
  1798. item.display_resources.sort(function (a, b) {
  1799. if (a.config_width < b.config_width) return 1;
  1800. if (a.config_width > b.config_width) return -1;
  1801. return 0;
  1802. });
  1803.  
  1804. if (item.is_video) {
  1805. saveFiles(item.video_resources[0].src, username, "stories", timestamp, 'mp4', item.id).then(() => {
  1806. setDownloadProgress(++complete, stories.data.reels_media[0].items.length);
  1807. });
  1808. }
  1809. else {
  1810. saveFiles(item.display_resources[0].src, username, "stories", timestamp, 'jpg', item.id).then(() => {
  1811. setDownloadProgress(++complete, stories.data.reels_media[0].items.length);
  1812. });
  1813. }
  1814. }, 100 * idx);
  1815. });
  1816. }
  1817. else {
  1818. IG_createDM(false, true);
  1819. createStoryListDOM(stories, 'stories');
  1820. }
  1821. }
  1822.  
  1823. /**
  1824. * onStory
  1825. * @description Trigger user's story download event or button display event.
  1826. *
  1827. * @param {Boolean} isDownload - Check if it is a download operation
  1828. * @param {Boolean} isForce - Check if downloading directly from API instead of cache
  1829. * @param {Boolean} isPreview - Check if it is need to open new tab
  1830. * @return {void}
  1831. */
  1832. async function onStory(isDownload, isForce, isPreview) {
  1833. var username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split("/").filter(s => s.length > 0).at(1);
  1834. if (isDownload) {
  1835. let date = new Date().getTime();
  1836. let timestamp = Math.floor(date / 1000);
  1837.  
  1838. updateLoadingBar(true);
  1839. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) {
  1840. let mediaId = null;
  1841.  
  1842. let userInfo = await getUserId(username);
  1843. let userId = userInfo.user.pk;
  1844. let stories = await getStories(userId);
  1845. let urlID = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  1846.  
  1847. /*
  1848. let latest_reel_media = stories.data.reels_media[0].latest_reel_media;
  1849. let last_seen = stories.data.reels_media[0].seen;
  1850. logger(stories);
  1851.  
  1852. if(urlID == null){
  1853. mediaId = stories.data.reels_media[0].items.filter(function(item, index){
  1854. return item.taken_at_timestamp === last_seen && item.taken_at_timestamp !== latest_reel_media || last_seen === latest_reel_media && index === 0;
  1855. })?.at(0)?.id;
  1856. logger('nula', mediaId);
  1857. }
  1858. else{
  1859. stories.data.reels_media[0].items.forEach(item => {
  1860. if(item.id == urlID){
  1861. mediaId = item.id;
  1862. }
  1863. });
  1864. }
  1865. */
  1866.  
  1867. stories.data.reels_media[0].items.forEach(item => {
  1868. if (item.id == urlID) {
  1869. mediaId = item.id;
  1870. }
  1871. });
  1872.  
  1873. if (mediaId == null) {
  1874. let $header = getStoryProgress(username);
  1875.  
  1876. $header.each(function (index) {
  1877. if ($(this).children().length > 0) {
  1878. mediaId = stories.data.reels_media[0].items[index].id;
  1879. }
  1880. });
  1881. }
  1882.  
  1883. if (mediaId == null) {
  1884. // appear in from profile page to story page
  1885. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  1886. if ($(this).hasClass('x1lix1fw')) {
  1887. if ($(this).children().length > 0) {
  1888. mediaId = stories.data.reels_media[0].items[index].id;
  1889. }
  1890. }
  1891. });
  1892.  
  1893. // appear in from home page to story page
  1894. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  1895. if ($(this).children().hasClass('_ac3q')) {
  1896. mediaId = stories.data.reels_media[0].items[index].id;
  1897. }
  1898. });
  1899. }
  1900.  
  1901. if (mediaId == null) {
  1902. mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  1903. }
  1904.  
  1905. if (USER_SETTING.CAPTURE_IMAGE_VIA_MEDIA_CACHE) {
  1906. const cached = getImageFromCache(mediaId);
  1907. if (cached && !stories.data.reels_media[0].items.filter(item => item.id === mediaId).at(0).is_video) {
  1908. logger("[Restore Cached onStory]", mediaId);
  1909. if (isPreview) {
  1910. openNewTab(cached);
  1911. }
  1912. else {
  1913. saveFiles(cached, username, "stories", timestamp, 'jpg', mediaId);
  1914. }
  1915. return;
  1916. }
  1917. }
  1918.  
  1919. let result = await getMediaInfo(mediaId);
  1920.  
  1921. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1922. timestamp = result.items[0].taken_at;
  1923. }
  1924.  
  1925. if (result.status === 'ok') {
  1926. if (result.items[0].video_versions) {
  1927. if (isPreview) {
  1928. openNewTab(result.items[0].video_versions[0].url);
  1929. }
  1930. else {
  1931. saveFiles(result.items[0].video_versions[0].url, username, "stories", timestamp, 'mp4', mediaId);
  1932. }
  1933. }
  1934. else {
  1935. if (isPreview) {
  1936. openNewTab(result.items[0].image_versions2.candidates[0].url);
  1937. }
  1938. else {
  1939. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "stories", timestamp, 'jpg', mediaId);
  1940. }
  1941. }
  1942. }
  1943. else {
  1944. if (USER_SETTING.FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED) {
  1945. state.tempFetchRateLimit = true;
  1946. onStory(isDownload, isForce, isPreview);
  1947. }
  1948. else {
  1949. alert('Fetch failed from Media API. API response message: ' + result.message);
  1950. }
  1951. logger(result);
  1952. }
  1953.  
  1954. updateLoadingBar(false);
  1955. return;
  1956. }
  1957.  
  1958. if ($('body > div section:visible video[playsinline]').length > 0) {
  1959. // Download stories if it is video
  1960. let type = "mp4";
  1961. let videoURL = "";
  1962. let targetURL = location.pathname.replace(/\/$/ig, '').split("/").at(-1);
  1963. let mediaId = null;
  1964.  
  1965. if (state.GL_dataCache.stories[username] && !isForce) {
  1966. logger('Fetch from memory cache:', username);
  1967. state.GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => {
  1968. if (item.id == targetURL) {
  1969. videoURL = item.video_resources[0].src;
  1970. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1971. timestamp = item.taken_at_timestamp;
  1972. mediaId = item.id;
  1973. }
  1974. }
  1975. });
  1976.  
  1977. if (videoURL.length == 0) {
  1978. logger('Memory cache not found, try fetch from API:', username);
  1979. onStory(true, true);
  1980. return;
  1981. }
  1982. }
  1983. else {
  1984. let userInfo = await getUserId(username);
  1985. let userId = userInfo.user.pk;
  1986. let stories = await getStories(userId);
  1987.  
  1988. stories.data.reels_media[0].items.forEach(item => {
  1989. if (item.id == targetURL) {
  1990. videoURL = item.video_resources[0].src;
  1991. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1992. timestamp = item.taken_at_timestamp;
  1993. mediaId = item.id;
  1994. }
  1995. }
  1996. });
  1997.  
  1998. // GitHub issue #4: thinkpad4
  1999. if (videoURL.length == 0) {
  2000.  
  2001. let $header = getStoryProgress(username);
  2002.  
  2003. $header.each(function (index) {
  2004. if ($(this).children().length > 0) {
  2005. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  2006. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2007. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  2008. mediaId = stories.data.reels_media[0].items[index].id;
  2009. }
  2010. }
  2011. });
  2012.  
  2013.  
  2014. if (videoURL.length == 0) {
  2015. // appear in from profile page to story page
  2016. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  2017. if ($(this).hasClass('x1lix1fw')) {
  2018. if ($(this).children().length > 0) {
  2019. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  2020. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2021. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  2022. mediaId = stories.data.reels_media[0].items[index].id;
  2023. }
  2024. }
  2025. }
  2026. });
  2027.  
  2028. // appear in from home page to story page
  2029. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  2030. if ($(this).children().hasClass('_ac3q')) {
  2031. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  2032. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2033. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  2034. mediaId = stories.data.reels_media[0].items[index].id;
  2035. }
  2036. }
  2037. });
  2038. }
  2039. }
  2040.  
  2041. state.GL_dataCache.stories[username] = stories;
  2042. }
  2043.  
  2044. if (videoURL.length == 0) {
  2045. alert(_i18n("NO_VID_URL"));
  2046. }
  2047. else {
  2048. if (isPreview) {
  2049. openNewTab(videoURL);
  2050. }
  2051. else {
  2052. saveFiles(videoURL, username, "stories", timestamp, type, mediaId);
  2053. }
  2054. }
  2055. }
  2056. else {
  2057. // Download stories if it is image
  2058. let srcset = $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').attr('srcset')?.split(',')[0]?.split(' ')[0];
  2059. let link = (srcset) ? srcset : $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').filter(function () {
  2060. return $(this).parents('a').length === 0 && $(this).width() === $(this).parent().width();
  2061. }).attr('src');
  2062.  
  2063. if (!link) {
  2064. // _aa63 mean stories picture in stories page (not avatar)
  2065. let $element = $('body > div section:visible img._aa63');
  2066. link = ($element.attr('srcset')) ? $element.attr('srcset')?.split(',')[0]?.split(' ')[0] : $element.attr('src');
  2067. }
  2068.  
  2069. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2070. timestamp = new Date($('body > div section:visible time[datetime][class]').first().attr('datetime')).getTime();
  2071. }
  2072.  
  2073. let downloadLink = link;
  2074. let type = 'jpg';
  2075.  
  2076. const mediaId = getImageFromCache(getStoryId(downloadLink) ?? "-");
  2077.  
  2078. if (USER_SETTING.CAPTURE_IMAGE_VIA_MEDIA_CACHE) {
  2079. const cached = getImageFromCache(mediaId);
  2080. if (cached) {
  2081. if (isPreview) {
  2082. openNewTab(cached);
  2083. }
  2084. else {
  2085. saveFiles(cached, username, "stories", timestamp, 'jpg', mediaId);
  2086. }
  2087. return;
  2088. }
  2089. }
  2090.  
  2091. if (isPreview) {
  2092. openNewTab(downloadLink);
  2093. }
  2094. else {
  2095. saveFiles(downloadLink, username, "stories", timestamp, type, mediaId);
  2096. }
  2097. }
  2098.  
  2099. state.tempFetchRateLimit = false;
  2100. updateLoadingBar(false);
  2101. }
  2102. else {
  2103. // Add the stories download button
  2104. if (!$('.IG_DWSTORY').length) {
  2105. state.GL_dataCache.stories = {};
  2106. let $element = null;
  2107. // Default detecter (section layout mode)
  2108. if ($('body > div section._ac0a').length > 0) {
  2109. $element = $('body > div section:visible._ac0a');
  2110. }
  2111. // detecter (single story layout mode)
  2112. else {
  2113. $element = $('body > div section:visible > div > div[style]:not([class])');
  2114. $element.css('position', 'relative');
  2115. }
  2116.  
  2117.  
  2118. if ($element.length === 0) {
  2119. $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div > div[style]:not([class])');
  2120. $element.css('position', 'relative');
  2121. }
  2122.  
  2123. if ($element.length === 0) {
  2124. $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div div[style]:not([class]) > div:not([data-visualcompletion="loading-state"])');
  2125. $element.css('position', 'relative');
  2126. }
  2127.  
  2128.  
  2129. // Detecter for div layout mode
  2130. if ($element.length === 0) {
  2131. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  2132. let nowSize = 0;
  2133.  
  2134. $$element.each(function () {
  2135. if ($(this).width() > nowSize) {
  2136. nowSize = $(this).width();
  2137. $element = $(this).children('div').first();
  2138. }
  2139. });
  2140. }
  2141.  
  2142.  
  2143. if ($element != null) {
  2144. $element.first().css('position', 'relative');
  2145. $element.first().append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DWSTORY">${SVG.DOWNLOAD}</div>`);
  2146. $element.first().append(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_DWNEWTAB">${SVG.NEW_TAB}</div>`);
  2147.  
  2148. let $header = getStoryProgress(username);
  2149. if ($header.length > 1) {
  2150. $element.first().append(`<div data-ih-locale-title="DW_ALL" title="${_i18n("DW_ALL")}" class="IG_DWSTORY_ALL">${SVG.DOWNLOAD_ALL}</div>`);
  2151. }
  2152.  
  2153. // Modify video volume
  2154. //if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  2155. // $element.find('video').each(function(){
  2156. // $(this).on('play playing', function(){
  2157. // if(!$(this).data('modify')){
  2158. // $(this).attr('data-modify', true);
  2159. // this.volume = VIDEO_VOLUME;
  2160. // logger('(story) Added video event listener #modify');
  2161. // }
  2162. // });
  2163. // });
  2164. //}
  2165.  
  2166. // Make sure to first remove thumbnail button if still exists and story is a picture
  2167. $element.find('img[referrerpolicy]').each(function () {
  2168. $(this).on('load', function () {
  2169. if (!$(this).data('remove-thumbnail')) {
  2170. if ($element.find('.IG_DWSTORY_THUMBNAIL').length === 0) {
  2171. $(this).attr('data-remove-thumbnail', true);
  2172. $('.IG_DWSTORY_THUMBNAIL').remove();
  2173. logger('(story) Manually removing thumbnail button');
  2174. }
  2175. else {
  2176. $(this).attr('data-remove-thumbnail', true);
  2177. logger('(story) Thumbnail button is not present for this picture');
  2178. }
  2179. }
  2180. });
  2181. });
  2182.  
  2183. // Try to use event listener 'timeupdate' in order to detect if story is a video
  2184. //$element.find('video').each(function(){
  2185. // $(this).on('timeupdate',function(){
  2186. // if(!$(this).data('modify-thumbnail')){
  2187. // if($element.find('.IG_DWSTORY_THUMBNAIL').length === 0){
  2188. // $(this).attr('data-modify-thumbnail', true);
  2189. // onStoryThumbnail(false);
  2190. // logger('(story) Manually inserting thumbnail button');
  2191. // }
  2192. // else{
  2193. // $(this).attr('data-modify-thumbnail', true);
  2194. // logger('(story) Thumbnail button already inserted');
  2195. // }
  2196. // }
  2197. // });
  2198. //});
  2199. }
  2200. }
  2201. }
  2202. }
  2203.  
  2204. /**
  2205. * onStoryThumbnail
  2206. * @description Trigger user's story video thumbnail download event or button display event.
  2207. *
  2208. * @param {Boolean} isDownload - Check if it is a download operation
  2209. * @param {Boolean} isForce - Check if downloading directly from API instead of cache
  2210. * @return {void}
  2211. */
  2212. async function onStoryThumbnail(isDownload, isForce) {
  2213. if (isDownload) {
  2214. // Download stories if it is video
  2215. let date = new Date().getTime();
  2216. let timestamp = Math.floor(date / 1000);
  2217. let type = 'jpg';
  2218. let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split('/').at(2);
  2219. // Download thumbnail
  2220. let targetURL = location.pathname.replace(/\/$/ig, '').split("/").at(-1);
  2221. let videoThumbnailURL = "";
  2222. let mediaId = null;
  2223.  
  2224. updateLoadingBar(true);
  2225.  
  2226. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !state.tempFetchRateLimit) {
  2227. let userInfo = await getUserId(username);
  2228. let userId = userInfo.user.pk;
  2229. let stories = await getStories(userId);
  2230. let urlID = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  2231.  
  2232. stories.data.reels_media[0].items.forEach(item => {
  2233. if (item.id == urlID) {
  2234. mediaId = item.id;
  2235. }
  2236. });
  2237.  
  2238. if (mediaId == null) {
  2239. let $header = getStoryProgress(username);
  2240.  
  2241. $header.each(function (index) {
  2242. if ($(this).children().length > 0) {
  2243. mediaId = stories.data.reels_media[0].items[index].id;
  2244. }
  2245. });
  2246. }
  2247.  
  2248. if (mediaId == null) {
  2249. // appear in from profile page to story page
  2250. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  2251. if ($(this).hasClass('x1lix1fw')) {
  2252. if ($(this).children().length > 0) {
  2253. mediaId = stories.data.reels_media[0].items[index].id;
  2254. }
  2255. }
  2256. });
  2257.  
  2258. // appear in from home page to story page
  2259. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  2260. if ($(this).children().hasClass('_ac3q')) {
  2261. mediaId = stories.data.reels_media[0].items[index].id;
  2262. }
  2263. });
  2264. }
  2265.  
  2266. if (mediaId == null) {
  2267. mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  2268. }
  2269.  
  2270. let result = await getMediaInfo(mediaId);
  2271.  
  2272. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2273. timestamp = result.items[0].taken_at;
  2274. }
  2275.  
  2276. if (result.status === 'ok') {
  2277. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "stories", timestamp, 'jpg', mediaId);
  2278.  
  2279. }
  2280. else {
  2281. if (USER_SETTING.FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED) {
  2282. state.tempFetchRateLimit = true;
  2283. onStoryThumbnail(true, isForce);
  2284. }
  2285. else {
  2286. alert('Fetch failed from Media API. API response message: ' + result.message);
  2287. }
  2288.  
  2289. logger(result);
  2290. }
  2291.  
  2292. updateLoadingBar(false);
  2293. return;
  2294. }
  2295.  
  2296. if (state.GL_dataCache.stories[username] && !isForce) {
  2297. logger('Fetch from memory cache:', username);
  2298. state.GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => {
  2299. if (item.id == targetURL) {
  2300. videoThumbnailURL = item.display_url;
  2301. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2302. timestamp = item.taken_at_timestamp;
  2303. mediaId = item.id;
  2304. }
  2305. }
  2306. });
  2307.  
  2308. if (videoThumbnailURL.length == 0) {
  2309. logger('Memory cache not found, try fetch from API:', username);
  2310. onStoryThumbnail(true, true);
  2311. return;
  2312. }
  2313. }
  2314. else {
  2315. let userInfo = await getUserId(username);
  2316. let userId = userInfo.user.pk;
  2317. let stories = await getStories(userId);
  2318.  
  2319. stories.data.reels_media[0].items.forEach(item => {
  2320. if (item.id == targetURL) {
  2321. videoThumbnailURL = item.display_url;
  2322. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2323. timestamp = item.taken_at_timestamp;
  2324. mediaId = item.id;
  2325. }
  2326. }
  2327. });
  2328.  
  2329. // GitHub issue #4: thinkpad4
  2330. if (videoThumbnailURL.length == 0) {
  2331. let $header = getStoryProgress(username);
  2332.  
  2333. $header.each(function (index) {
  2334. if ($(this).children().length > 0) {
  2335. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  2336. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2337. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  2338. mediaId = stories.data.reels_media[0].items[index].id;
  2339. }
  2340. }
  2341. });
  2342.  
  2343. if (videoThumbnailURL.length == 0) {
  2344. // appear in from profile page to story page
  2345. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  2346. if ($(this).hasClass('x1lix1fw')) {
  2347. if ($(this).children().length > 0) {
  2348. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  2349. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2350. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  2351. mediaId = stories.data.reels_media[0].items[index].id;
  2352. }
  2353. }
  2354. }
  2355. });
  2356.  
  2357. // appear in from home page to story page
  2358. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  2359. if ($(this).children().hasClass('_ac3q')) {
  2360. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  2361. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2362. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  2363. mediaId = stories.data.reels_media[0].items[index].id;
  2364. }
  2365. }
  2366. });
  2367. }
  2368. }
  2369. }
  2370.  
  2371. saveFiles(videoThumbnailURL, username, "thumbnail", timestamp, type, mediaId);
  2372. state.tempFetchRateLimit = false;
  2373. updateLoadingBar(false);
  2374. }
  2375. else {
  2376. if ($('body > div div.IG_DWSTORY').parent().find('video[class]').length) {
  2377. // Add the stories download button
  2378. let $element = null;
  2379. // Default detecter (section layout mode)
  2380. if ($('body > div section._ac0a').length > 0) {
  2381. $element = $('body > div section:visible._ac0a');
  2382. }
  2383. // detecter (single story layout mode)
  2384. else {
  2385. $element = $('body > div section:visible > div > div[style]:not([class])');
  2386. $element.css('position', 'relative');
  2387. }
  2388.  
  2389. if ($element.length === 0) {
  2390. $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div > div[style]:not([class])');
  2391. $element.css('position', 'relative');
  2392. }
  2393.  
  2394. if ($element.length === 0) {
  2395. $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div div[style]:not([class]) > div:not([data-visualcompletion="loading-state"])');
  2396. $element.css('position', 'relative');
  2397. }
  2398.  
  2399. // Detecter for div layout mode
  2400. if ($element.length === 0) {
  2401. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  2402. let nowSize = 0;
  2403.  
  2404. $$element.each(function () {
  2405. if ($(this).width() > nowSize) {
  2406. nowSize = $(this).width();
  2407. $element = $(this).children('div').first();
  2408. }
  2409. });
  2410. }
  2411.  
  2412.  
  2413. if ($element != null) {
  2414. $element.first().css('position', 'relative');
  2415. $element.first().append(`<div data-ih-locale-title="VIDEO_THUMBNAIL" title="${_i18n("VIDEO_THUMBNAIL")}" class="IG_DWSTORY_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  2416. }
  2417.  
  2418. }
  2419. }
  2420. }
  2421.  
  2422. /* untils */
  2423.  
  2424. /**
  2425. * getHighlightStories
  2426. * @description Get a list of all stories in highlight Id.
  2427. *
  2428. * @param {Integer} highlightId
  2429. * @return {Object}
  2430. */
  2431. function getHighlightStories(highlightId) {
  2432. return new Promise((resolve, reject) => {
  2433. 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`;
  2434.  
  2435. GM_xmlhttpRequest({
  2436. method: "GET",
  2437. url: getURL,
  2438. onload: function (response) {
  2439. try {
  2440. let obj = JSON.parse(response.response);
  2441. resolve(obj);
  2442. }
  2443. catch (err) {
  2444. logger('getHighlightStories()', 'reject', err.message);
  2445. reject(err);
  2446. }
  2447. },
  2448. onerror: function (err) {
  2449. logger('getHighlightStories()', 'reject', err);
  2450. reject(err);
  2451. }
  2452. });
  2453. });
  2454. }
  2455.  
  2456. /**
  2457. * getStories
  2458. * @description Get a list of all stories in user Id.
  2459. *
  2460. * @param {Integer} userId
  2461. * @return {Object}
  2462. */
  2463. function getStories(userId) {
  2464. return new Promise((resolve, reject) => {
  2465. 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`;
  2466.  
  2467. GM_xmlhttpRequest({
  2468. method: "GET",
  2469. url: getURL,
  2470. onload: function (response) {
  2471. try {
  2472. let obj = JSON.parse(response.response);
  2473. logger('getStories()', obj);
  2474. resolve(obj);
  2475. }
  2476. catch (err) {
  2477. logger('getStories()', 'reject', err.message);
  2478. reject(err);
  2479. }
  2480. },
  2481. onerror: function (err) {
  2482. logger('getStories()', 'reject', err);
  2483. reject(err);
  2484. }
  2485. });
  2486. });
  2487. }
  2488.  
  2489. /**
  2490. * getUserId
  2491. * @description Get user's id with username.
  2492. *
  2493. * @param {String} username
  2494. * @return {Integer}
  2495. */
  2496. function getUserId(username) {
  2497. return new Promise((resolve, reject) => {
  2498. let getURL = `https://www.instagram.com/web/search/topsearch/?query=${username}`;
  2499.  
  2500. GM_xmlhttpRequest({
  2501. method: "GET",
  2502. url: getURL,
  2503. onload: function (response) {
  2504. // Fix search issue by Discord: sno_w_
  2505. let obj = JSON.parse(response.response);
  2506. let result = null;
  2507. obj.users.forEach(pos => {
  2508. if (pos.user.username?.toLowerCase() === username?.toLowerCase()) {
  2509. result = pos;
  2510. }
  2511. });
  2512.  
  2513. if (result != null) {
  2514. logger('getUserId()', result);
  2515. resolve(result);
  2516. }
  2517. else {
  2518. getUserIdWithAgent(username).then((result) => {
  2519. resolve(result);
  2520. // eslint-disable-next-line no-unused-vars
  2521. }).catch((err) => {
  2522. alert("Cannot find user info from getUserId()");
  2523. });
  2524. }
  2525. },
  2526. onerror: function (err) {
  2527. logger('getUserId()', 'reject', err);
  2528. reject(err);
  2529. }
  2530. });
  2531. });
  2532. }
  2533.  
  2534. /**
  2535. * getUserIdWithAgent
  2536. * @description Get user's id with username.
  2537. *
  2538. * @param {String} username
  2539. * @return {Integer}
  2540. */
  2541. function getUserIdWithAgent(username) {
  2542. return new Promise((resolve, reject) => {
  2543. let getURL = `https://i.instagram.com/api/v1/users/web_profile_info/?username=${username}`;
  2544.  
  2545. GM_xmlhttpRequest({
  2546. method: "GET",
  2547. url: getURL,
  2548. headers: {
  2549. 'X-IG-App-ID': getAppID()
  2550. },
  2551. onload: function (response) {
  2552. try {
  2553. let obj = JSON.parse(response.response);
  2554. let hasUser = obj?.data?.user;
  2555.  
  2556. if (hasUser != null) {
  2557. let userInfo = obj?.data;
  2558. userInfo.user.pk = userInfo.user.id;
  2559. logger('getUserIdWithAgent()', obj);
  2560. resolve(userInfo);
  2561. }
  2562. else {
  2563. logger('getUserIdWithAgent()', 'reject', 'undefined');
  2564. reject('undefined');
  2565. }
  2566. }
  2567. catch (err) {
  2568. logger('getUserIdWithAgent()', 'reject', err.message);
  2569. reject(err);
  2570. }
  2571. },
  2572. onerror: function (err) {
  2573. logger('getUserIdWithAgent()', 'reject', err);
  2574. reject(err);
  2575. }
  2576. });
  2577. });
  2578. }
  2579.  
  2580. /**
  2581. * getUserHighSizeProfile
  2582. * @description Get user's high quality avatar image.
  2583. *
  2584. * @param {Integer} userId
  2585. * @return {String}
  2586. */
  2587. function getUserHighSizeProfile(userId) {
  2588. return new Promise((resolve, reject) => {
  2589. let getURL = `https://i.instagram.com/api/v1/users/${userId}/info/`;
  2590.  
  2591. GM_xmlhttpRequest({
  2592. method: "GET",
  2593. url: getURL,
  2594. headers: {
  2595. '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'
  2596. },
  2597. onload: function (response) {
  2598. try {
  2599. let obj = JSON.parse(response.response);
  2600. if (obj.status !== 'ok') {
  2601. logger('getUserHighSizeProfile()', 'reject', obj);
  2602. reject('faild');
  2603. }
  2604. else {
  2605. logger('getUserHighSizeProfile()', obj);
  2606. resolve(obj.user.hd_profile_pic_url_info?.url);
  2607. }
  2608. }
  2609. catch (err) {
  2610. logger('getUserHighSizeProfile()', 'reject', err);
  2611. reject(err);
  2612. }
  2613. },
  2614. onerror: function (err) {
  2615. logger('getUserHighSizeProfile()', 'reject', err);
  2616. reject(err);
  2617. }
  2618. });
  2619. });
  2620. }
  2621.  
  2622. /**
  2623. * getPostOwner
  2624. * @description Get post's author with post shortcode.
  2625. *
  2626. * @param {String} postPath
  2627. * @return {String}
  2628. */
  2629. function getPostOwner(postPath) {
  2630. return new Promise((resolve, reject) => {
  2631. if (!postPath) reject("NOPATH");
  2632. let postShortCode = postPath;
  2633. let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`;
  2634.  
  2635. GM_xmlhttpRequest({
  2636. method: "GET",
  2637. url: getURL,
  2638. onload: function (response) {
  2639. try {
  2640. let obj = JSON.parse(response.response);
  2641. logger('getPostOwner()', obj);
  2642. resolve(obj.data.shortcode_media.owner.username);
  2643. }
  2644. catch (err) {
  2645. logger('getPostOwner()', 'reject', err.message);
  2646. reject(err);
  2647. }
  2648. },
  2649. onerror: function (err) {
  2650. logger('getPostOwner()', 'reject', err);
  2651. reject(err);
  2652. }
  2653. });
  2654. });
  2655. }
  2656.  
  2657. /**
  2658. * getBlobMedia
  2659. * @description Get list of all media files in post with post shortcode.
  2660. *
  2661. * @param {String} postPath
  2662. * @return {Object}
  2663. */
  2664. function getBlobMedia(postPath) {
  2665. return new Promise((resolve, reject) => {
  2666. if (!postPath) reject("NOPATH");
  2667. let postShortCode = postPath;
  2668. let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`;
  2669.  
  2670. GM_xmlhttpRequest({
  2671. method: "GET",
  2672. url: getURL,
  2673. headers: {
  2674. "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"
  2675. },
  2676. onload: function (response) {
  2677. try {
  2678. let obj = JSON.parse(response.response);
  2679. logger(obj);
  2680.  
  2681. if (obj.status === 'fail') {
  2682. // alert(`Request failed with API response:\n${obj.message}: ${obj.feedback_message}`);
  2683. logger('Request with:', 'getBlobMediaWithQuery()', postShortCode);
  2684. getBlobMediaWithQueryID(postShortCode).then((res) => {
  2685. resolve({ type: 'query_id', data: res.xdt_api__v1__media__shortcode__web_info.items[0] });
  2686. }).catch((err) => {
  2687. reject(err);
  2688. })
  2689. }
  2690. else {
  2691. resolve({ type: 'query_hash', data: obj.data });
  2692. }
  2693. }
  2694. catch (err) {
  2695. logger('getBlobMedia()', 'reject', err.message);
  2696. reject(err);
  2697. }
  2698. },
  2699. onerror: function (err) {
  2700. logger('getBlobMedia()', 'reject', err);
  2701. reject(err);
  2702. }
  2703. });
  2704. });
  2705. }
  2706.  
  2707. /**
  2708. * getBlobMediaWithQueryID
  2709. * @description Get list of all media files in post with post shortcode.
  2710. *
  2711. * @param {String} postPath
  2712. * @return {Object}
  2713. */
  2714. function getBlobMediaWithQueryID(postPath) {
  2715. return new Promise((resolve, reject) => {
  2716. if (!postPath) reject("NOPATH");
  2717. let postShortCode = postPath;
  2718. let getURL = `https://www.instagram.com/graphql/query/?query_id=9496392173716084&variables={%22shortcode%22:%22${postShortCode}%22,%22__relay_internal__pv__PolarisFeedShareMenurelayprovider%22:true,%22__relay_internal__pv__PolarisIsLoggedInrelayprovider%22:true}`;
  2719.  
  2720. GM_xmlhttpRequest({
  2721. method: "GET",
  2722. url: getURL,
  2723. headers: {
  2724. "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",
  2725. 'X-IG-App-ID': getAppID()
  2726. },
  2727. onload: function (response) {
  2728. try {
  2729. let obj = JSON.parse(response.response);
  2730. logger(obj);
  2731.  
  2732. if (obj.status === 'fail') {
  2733. alert(`getBlobMediaWithQueryID(): Request failed with API response:\n${obj.message}: ${obj.feedback_message}`);
  2734. logger(`Request failed with API response ${obj.message}: ${obj.feedback_message}`);
  2735. reject(response);
  2736. }
  2737. else {
  2738. logger('getBlobMediaWithQueryID()', obj.data);
  2739. resolve(obj.data);
  2740. }
  2741. }
  2742. catch (err) {
  2743. logger('getBlobMediaWithQueryID()', 'reject', err.message);
  2744. reject(err);
  2745. }
  2746. },
  2747. onerror: function (err) {
  2748. logger('getBlobMediaWithQueryID()', 'reject', err);
  2749. reject(err);
  2750. }
  2751. });
  2752. });
  2753. }
  2754.  
  2755. /**
  2756. * getMediaInfo
  2757. * @description Get Instagram Media object.
  2758. *
  2759. * @param {String} mediaId
  2760. * @return {Object}
  2761. */
  2762. function getMediaInfo(mediaId) {
  2763. return new Promise((resolve, reject) => {
  2764. let getURL = `https://i.instagram.com/api/v1/media/${mediaId}/info/`;
  2765.  
  2766. if (mediaId == null) {
  2767. alert("Cannot call Media API because of the media id is invalid.");
  2768. logger('getMediaInfo()', 'reject', 'Cannot call Media API because of the media id is invalid.');
  2769.  
  2770. updateLoadingBar(false);
  2771. reject(-1);
  2772. return;
  2773. }
  2774. if (getAppID() == null) {
  2775. alert("Cannot call Media API because of the app id is invalid.");
  2776. logger('getMediaInfo()', 'reject', 'Cannot call Media API because of the app id is invalid.');
  2777. updateLoadingBar(false);
  2778. reject(-1);
  2779. return;
  2780. }
  2781.  
  2782. GM_xmlhttpRequest({
  2783. method: "GET",
  2784. url: getURL,
  2785. headers: {
  2786. "User-Agent": window.navigator.userAgent,
  2787. "Accept": "*/*",
  2788. 'X-IG-App-ID': getAppID()
  2789. },
  2790. onload: function (response) {
  2791. if (response.finalUrl == getURL) {
  2792. let obj = JSON.parse(response.response);
  2793. logger('getMediaInfo()', obj);
  2794. resolve(obj);
  2795. }
  2796. else {
  2797. let finalURL = new URL(response.finalUrl);
  2798. if (finalURL.pathname.startsWith('/accounts/login')) {
  2799. logger('getMediaInfo()', 'reject', 'The account must be logged in to access Media API.');
  2800. alert("The account must be logged in to access Media API.");
  2801. }
  2802. else {
  2803. logger('getMediaInfo()', 'reject', 'Unable to retrieve content because the API was redirected to "' + response.finalUrl + '"');
  2804. alert('Unable to retrieve content because the API was redirected to "' + response.finalUrl + '"');
  2805. }
  2806. updateLoadingBar(false);
  2807. reject(-1);
  2808. }
  2809. },
  2810. onerror: function (err) {
  2811. logger('getMediaInfo()', 'reject', err);
  2812. resolve(err);
  2813. }
  2814. });
  2815. });
  2816. }
  2817.  
  2818. /**
  2819. * getStoryId
  2820. * @description Obtain the media id through the resource URL.
  2821. *
  2822. * @param {string} url
  2823. * @return {string}
  2824. */
  2825. function getStoryId(url) {
  2826. let obj = new URL(url);
  2827. let base64 = obj?.searchParams?.get('ig_cache_key')?.split('.').at(0);
  2828. if (base64) {
  2829. return atob(base64);
  2830. }
  2831. else {
  2832. return null;
  2833. }
  2834. }
  2835.  
  2836. /**
  2837. * getAppID
  2838. * @description Get Instagram App ID.
  2839. *
  2840. * @return {?integer}
  2841. */
  2842. function getAppID() {
  2843. let result = null;
  2844. $('script[type="application/json"]').each(function () {
  2845. const regexp = /"APP_ID":"([0-9]+)"/ig;
  2846. const matcher = $(this).text().match(regexp);
  2847. if (matcher != null && result == null) {
  2848. result = [...$(this).text().matchAll(regexp)];
  2849. }
  2850. })
  2851.  
  2852. return (result) ? result.at(0).at(-1) : null;
  2853. }
  2854.  
  2855.  
  2856. /**
  2857. * updateLoadingBar
  2858. * @description Update loading state.
  2859. *
  2860. * @param {Boolean} isLoading - Check if loading state
  2861. * @return {void}
  2862. */
  2863. function updateLoadingBar(isLoading) {
  2864. if (isLoading) {
  2865. $('div[id^="mount"] > div > div > div:first').removeClass('x1s85apg');
  2866. $('div[id^="mount"] > div > div > div:first').css('z-index', '20000');
  2867. }
  2868. else {
  2869. $('div[id^="mount"] > div > div > div:first').addClass('x1s85apg');
  2870. $('div[id^="mount"] > div > div > div:first').css('z-index', '');
  2871. }
  2872. }
  2873.  
  2874. /**
  2875. * getStoryProgress
  2876. * @description Get the story progress of the username (post several stories).
  2877. *
  2878. * @param {String} username - Get progress of username
  2879. * @return {Object}
  2880. */
  2881. function getStoryProgress(username) {
  2882. let $header = $('body > div section:visible a[href^="/' + (username) + '"] span').filter(function () {
  2883. return $(this).children().length === 0 && $(this).find('svg').length === 0 && $(this).text()?.toLowerCase() === username?.toLowerCase();
  2884. }).parents('div:not([class]):not([style])').filter(function () {
  2885. return $(this).text()?.toLowerCase() !== username?.toLowerCase()
  2886. }).filter(function () {
  2887. return $(this).children().length > 1
  2888. }).first();
  2889.  
  2890. if ($header.length === 0) {
  2891. $header = $('body > div section:visible a[href^="/' + (username) + '"]').filter(function () {
  2892. return $(this).find('img').length > 0
  2893. }).parents('div:not([class]):not([style])').filter(function () {
  2894. return $(this).text()?.toLowerCase() !== username?.toLowerCase()
  2895. }).filter(function () {
  2896. return $(this).children().length > 1
  2897. }).first();
  2898. }
  2899.  
  2900. return $header.children().filter(function () {
  2901. return $(this).height() < 10
  2902. }).first().children();
  2903. }
  2904.  
  2905. /**
  2906. * setDownloadProgress
  2907. * @description Show and set download circle progress.
  2908. *
  2909. * @param {Integer} now
  2910. * @param {Integer} total
  2911. * @return {Void}
  2912. */
  2913. function setDownloadProgress(now, total) {
  2914. if ($('.circle_wrapper').length) {
  2915. $('.circle_wrapper span').text(`${now}/${total}`);
  2916.  
  2917. if (now >= total) {
  2918. $('.circle_wrapper').fadeOut(250, function () {
  2919. $(this).remove();
  2920. });
  2921. }
  2922. }
  2923. else {
  2924. $('body').append(`<div class="circle_wrapper"><circle></circle><span>${now}/${total}</span></div>`);
  2925. }
  2926. }
  2927.  
  2928.  
  2929. /**
  2930. * IG_createDM
  2931. * @description A dialog showing a list of all media files in the post.
  2932. *
  2933. * @param {Boolean} hasHidden
  2934. * @param {Boolean} hasCheckbox
  2935. * @return {void}
  2936. */
  2937. function IG_createDM(hasHidden, hasCheckbox) {
  2938. let isHidden = (hasHidden) ? "hidden" : "";
  2939. $('body').append('<div class="IG_POPUP_DIG ' + isHidden + '"><div class="IG_POPUP_DIG_BG"></div><div class="IG_POPUP_DIG_MAIN"><div class="IG_POPUP_DIG_TITLE"></div><div class="IG_POPUP_DIG_BODY"></div></div></div>');
  2940. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_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> [<span data-ih-locale="CLOSE">${_i18n("CLOSE")}</span>]</div><div style="line-height: 18px;">IG Helper v${GM_info.script.version}</div><div id="post_info" style="line-height: 14px;font-size:14px;">Post ID: <span id="article-id"></span></div><div class="IG_POPUP_DIG_BTN">${SVG.CLOSE}</div></div>`);
  2941.  
  2942. if (hasCheckbox) {
  2943. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_TITLE').append(`<div style="text-align: center;" id="button_group"></div>`);
  2944. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_TITLE > div#button_group').append(`<button id="batch_download_selected" data-ih-locale="BATCH_DOWNLOAD_SELECTED">${_i18n('BATCH_DOWNLOAD_SELECTED')}</button>`);
  2945. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_TITLE > div#button_group').append(`<button id="batch_download_direct" data-ih-locale="BATCH_DOWNLOAD_DIRECT">${_i18n('BATCH_DOWNLOAD_DIRECT')}</button>`);
  2946. $('.IG_POPUP_DIG .IG_POPUP_DIG_MAIN .IG_POPUP_DIG_TITLE').append(`<label class="checkbox"><input value="yes" type="checkbox" /><span data-ih-locale="ALL_CHECK">${_i18n('ALL_CHECK')}</span></label>`);
  2947. }
  2948. }
  2949.  
  2950. /**
  2951. * IG_setDM
  2952. * @description Set a dialog status.
  2953. *
  2954. * @param {Boolean} hasHidden
  2955. * @return {void}
  2956. */
  2957. function IG_setDM(hasHidden) {
  2958. if ($('.IG_POPUP_DIG').length) {
  2959. if (hasHidden) {
  2960. $('.IG_POPUP_DIG').addClass("hidden");
  2961. }
  2962. else {
  2963. $('.IG_POPUP_DIG').removeClass("hidden");
  2964. }
  2965. }
  2966. }
  2967.  
  2968. /**
  2969. * saveFiles
  2970. * @description Download the specified media URL to the computer.
  2971. *
  2972. * @param {String} downloadLink
  2973. * @param {String} username
  2974. * @param {String} sourceType
  2975. * @param {Integer} timestamp
  2976. * @param {String} filetype
  2977. * @param {String} shortcode
  2978. * @return {Promise}
  2979. */
  2980. function saveFiles(downloadLink, username, sourceType, timestamp, filetype, shortcode) {
  2981. return new Promise((resolve) => {
  2982. setTimeout(() => {
  2983. updateLoadingBar(true);
  2984. fetch(downloadLink).then(res => {
  2985. return res.blob().then(dwel => {
  2986. updateLoadingBar(false);
  2987. createSaveFileElement(downloadLink, dwel, username, sourceType, timestamp, filetype, shortcode);
  2988.  
  2989. resolve(true);
  2990. });
  2991. });
  2992. }, 50);
  2993. });
  2994. }
  2995.  
  2996. /**
  2997. * @description Trigger download from Blob with filename.
  2998. *
  2999. * @param {Blob} blob
  3000. * @param {string} filename
  3001. */
  3002. function triggerDownload(blob, filename) {
  3003. const link = document.createElement('a');
  3004. link.href = URL.createObjectURL(blob);
  3005. link.download = filename;
  3006. link.click();
  3007. link.remove();
  3008. }
  3009.  
  3010. /**
  3011. * createSaveFileElement
  3012. * @description Download the specified media with link element.
  3013. *
  3014. * @param {String} downloadLink
  3015. * @param {Object} object
  3016. * @param {String} username
  3017. * @param {String} sourceType
  3018. * @param {Integer} timestamp
  3019. * @param {String} filetype
  3020. * @param {String} shortcode
  3021. * @return {void}
  3022. */
  3023. function createSaveFileElement(downloadLink, object, username, sourceType, timestamp, filetype, shortcode) {
  3024. timestamp = parseInt(timestamp.toString().padEnd(13, '0'));
  3025.  
  3026. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  3027. timestamp = parseInt(timestamp.toString().padEnd(13, '0'));
  3028. }
  3029.  
  3030. const date = new Date(timestamp);
  3031.  
  3032. const original_name = new URL(downloadLink).pathname.split('/').at(-1).split('.').slice(0, -1).join('.');
  3033. const year = date.getFullYear().toString();
  3034. const month = (date.getMonth() + 1).toString().padStart(2, '0');
  3035. const day = date.getDate().toString().padStart(2, '0');
  3036. const hour = date.getHours().toString().padStart(2, '0');
  3037. const minute = date.getMinutes().toString().padStart(2, '0');
  3038. const second = date.getSeconds().toString().padStart(2, '0');
  3039.  
  3040. var filename = state.fileRenameFormat.toUpperCase();
  3041. var format_shortcode = shortcode ?? "";
  3042. var replacements = {
  3043. '%USERNAME%': username,
  3044. '%SOURCE_TYPE%': sourceType,
  3045. '%SHORTCODE%': format_shortcode,
  3046. '%YEAR%': year,
  3047. '%2-YEAR%': year.substr(-2),
  3048. '%MONTH%': month,
  3049. '%DAY%': day,
  3050. '%HOUR%': hour,
  3051. '%MINUTE%': minute,
  3052. '%SECOND%': second,
  3053. '%ORIGINAL_NAME%': original_name,
  3054. '%ORIGINAL_NAME_FIRST%': original_name.split('_').at(0)
  3055. };
  3056.  
  3057. // eslint-disable-next-line no-useless-escape
  3058. filename = filename.replace(/%[\w\-]+%/g, function (str) {
  3059. return replacements[str] || str;
  3060. });
  3061.  
  3062. const originally = username + '_' + original_name + '.' + filetype;
  3063. const downloadName = USER_SETTING.AUTO_RENAME ? filename + '.' + filetype : originally;
  3064. if (USER_SETTING.MODIFY_RESOURCE_EXIF && filetype === 'jpg' && shortcode && sourceType === 'photo' && (object.type === 'image/jpeg' || object.type === 'image/webp')) {
  3065. changeExifData(object, shortcode)
  3066. .then(newBlob => triggerDownload(newBlob, downloadName))
  3067. .catch(err => {
  3068. console.error('Failed to strip EXIF and/or attach post URL to EXIF.', err);
  3069. triggerDownload(object, downloadName);
  3070. });
  3071. } else {
  3072. triggerDownload(object, downloadName);
  3073. }
  3074. }
  3075.  
  3076. /**
  3077. * changeExifData
  3078. * @description Strips EXIF metadata and attaches post URLs to the EXIF of downloaded image resources.
  3079. *
  3080. * @param {Object} blob
  3081. * @param {string} shortcode
  3082. * @return {Blob}
  3083. */
  3084. async function changeExifData(blob, shortcode) {
  3085. const concat = (...arr) => {
  3086. const len = arr.reduce((s, a) => s + a.length, 0);
  3087. const out = new Uint8Array(len);
  3088. let p = 0;
  3089. for (const a of arr) {
  3090. out.set(a, p);
  3091. p += a.length;
  3092. }
  3093. return out;
  3094. };
  3095. const u32le = v => {
  3096. const b = new Uint8Array(4);
  3097. new DataView(b.buffer).setUint32(0, v, true);
  3098. return b;
  3099. };
  3100. const enc = s => new TextEncoder().encode(s);
  3101. const fourCC = (dv, o) =>
  3102. String.fromCharCode(dv.getUint8(o), dv.getUint8(o + 1), dv.getUint8(o + 2), dv.getUint8(o + 3));
  3103.  
  3104. const head = new Uint8Array(await blob.slice(0, 12).arrayBuffer());
  3105. const isJPEG = head[0] === 0xFF && head[1] === 0xD8;
  3106. const isWEBP = head.length >= 12 &&
  3107. String.fromCharCode(...head.subarray(0, 4)) === 'RIFF' &&
  3108. String.fromCharCode(...head.subarray(8, 12)) === 'WEBP';
  3109. if (!isJPEG && !isWEBP) throw new Error('Not a JPEG or WEBP');
  3110.  
  3111. const urlBytes = enc(`https://www.instagram.com/p/${shortcode}/\0`);
  3112. const exifPrefix = enc('Exif\0\0');
  3113. const tiffHeader = Uint8Array.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00]);
  3114. const entryCount = Uint8Array.from([0x01, 0x00]);
  3115. const entry = concat(
  3116. Uint8Array.from([0x0E, 0x01, 0x02, 0x00]),
  3117. u32le(urlBytes.length),
  3118. u32le(8 + 2 + 12 + 4)
  3119. );
  3120. const tiffBody = concat(tiffHeader, entryCount, entry, u32le(0), urlBytes);
  3121.  
  3122. if (isJPEG) {
  3123. const ab = await blob.arrayBuffer();
  3124. const dv = new DataView(ab);
  3125. const app1Body = concat(exifPrefix, tiffBody);
  3126. const app1Header = new Uint8Array(4);
  3127. new DataView(app1Header.buffer).setUint16(0, 0xFFE1);
  3128. new DataView(app1Header.buffer).setUint16(2, app1Body.length + 2);
  3129. const newAPP1 = concat(app1Header, app1Body);
  3130.  
  3131. const parts = [new Uint8Array(ab, 0, 2)];
  3132. let off = 2,
  3133. added = false;
  3134. while (off < dv.byteLength) {
  3135. const marker = dv.getUint16(off);
  3136. if ((marker & 0xFF00) !== 0xFF00) break;
  3137. if (marker === 0xFFDA) {
  3138. if (!added) parts.push(newAPP1);
  3139. parts.push(new Uint8Array(ab, off));
  3140. break;
  3141. }
  3142. const len = dv.getUint16(off + 2) + 2;
  3143. if (marker === 0xFFE1) {
  3144. off += len;
  3145. continue;
  3146. }
  3147. parts.push(new Uint8Array(ab, off, len));
  3148. off += len;
  3149. }
  3150. const total = parts.reduce((s, a) => s + a.length, 0);
  3151. const out = new Uint8Array(total);
  3152. let p = 0;
  3153. parts.forEach(a => {
  3154. out.set(a, p);
  3155. p += a.length;
  3156. });
  3157. return new Blob([out], {
  3158. type: 'image/jpeg'
  3159. });
  3160. }
  3161.  
  3162. const ab = await blob.arrayBuffer();
  3163. const dv = new DataView(ab);
  3164. const chunks = [];
  3165. let vp8xIdx = -1;
  3166. let offset = 12;
  3167. while (offset < dv.byteLength) {
  3168. const cc = fourCC(dv, offset);
  3169. const sz = dv.getUint32(offset + 4, true);
  3170. const pad = sz & 1;
  3171. const full = 8 + sz + pad;
  3172. if (cc !== 'EXIF' && cc !== 'XMP ') {
  3173. chunks.push(new Uint8Array(ab, offset, full));
  3174. if (cc === 'VP8X') vp8xIdx = chunks.length - 1;
  3175. }
  3176. offset += full;
  3177. }
  3178. let exifChunk = concat(
  3179. enc('EXIF'),
  3180. u32le(exifPrefix.length + tiffBody.length),
  3181. exifPrefix,
  3182. tiffBody
  3183. );
  3184. if (exifChunk.length & 1) exifChunk = concat(exifChunk, Uint8Array.of(0));
  3185. if (vp8xIdx !== -1) {
  3186. const vp8x = new Uint8Array(chunks[vp8xIdx]);
  3187. vp8x[8] |= 0x10;
  3188. chunks[vp8xIdx] = vp8x;
  3189. chunks.splice(vp8xIdx + 1, 0, exifChunk);
  3190. } else {
  3191. chunks.push(exifChunk);
  3192. }
  3193. const payload = chunks.reduce((s, c) => s + c.length, 0);
  3194. const riffHeader = concat(enc('RIFF'), u32le(payload + 4), enc('WEBP'));
  3195. const finalBuf = concat(riffHeader, ...chunks);
  3196. return new Blob([finalBuf], {
  3197. type: 'image/webp'
  3198. });
  3199. }
  3200.  
  3201. /**
  3202. * triggerLinkElement
  3203. * @description Trigger the link element to start downloading the resource.
  3204. *
  3205. * @param {Object} element
  3206. * @return {void}
  3207. */
  3208. async function triggerLinkElement(element, isPreview) {
  3209. let date = new Date().getTime();
  3210. let timestamp = Math.floor(date / 1000);
  3211. let username = ($(element).attr('data-username')) ? $(element).attr('data-username') : state.GL_username;
  3212.  
  3213. if (!username && $(element).attr('data-path')) {
  3214. logger('catching owner name from shortcode:', $(element).attr('data-href'));
  3215. username = await getPostOwner($(element).attr('data-path')).catch(err => {
  3216. logger('get username failed, replace with default string, error message:', err.message);
  3217. });
  3218.  
  3219. if (username == null) {
  3220. username = "NONE";
  3221. }
  3222. }
  3223.  
  3224. if (USER_SETTING.RENAME_PUBLISH_DATE && $(element).attr('datetime')) {
  3225. timestamp = parseInt($(element).attr('datetime'));
  3226. }
  3227.  
  3228. let mediaId = $(element).attr('media-id');
  3229.  
  3230. if (USER_SETTING.CAPTURE_IMAGE_VIA_MEDIA_CACHE) {
  3231. const cached = getImageFromCache(mediaId);
  3232. if (cached && $(element).data('type') != "mp4") {
  3233. if (isPreview) {
  3234. openNewTab(cached);
  3235. } else {
  3236. saveFiles(cached, username, $(element).data('name'), timestamp, $(element).data('type') || 'jpg', $(element).data('path'));
  3237. }
  3238. return;
  3239. }
  3240. }
  3241.  
  3242. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA) {
  3243. updateLoadingBar(true);
  3244. let result = await getMediaInfo($(element).attr('media-id'));
  3245. updateLoadingBar(false);
  3246.  
  3247. if (result.status === 'ok') {
  3248. var resource_url = null;
  3249. if (result.items[0].video_versions) {
  3250. resource_url = result.items[0].video_versions[0].url;
  3251. }
  3252. else {
  3253. result.items[0].image_versions2.candidates.sort(function (a, b) {
  3254. let aSTP = new URL(a.url).searchParams.get('stp');
  3255. let bSTP = new URL(b.url).searchParams.get('stp');
  3256.  
  3257. if (aSTP && bSTP) {
  3258. if (aSTP.length > bSTP.length) return 1;
  3259. if (aSTP.length < bSTP.length) return -1;
  3260. }
  3261. else {
  3262. if (a.width < b.width) return 1;
  3263. if (a.width > b.width) return -1;
  3264. }
  3265.  
  3266. return 0;
  3267. });
  3268.  
  3269. resource_url = result.items[0].image_versions2.candidates[0].url;
  3270.  
  3271. const getWidthFromURL = function (obj) {
  3272. if (obj.width != null) {
  3273. return obj.width;
  3274. }
  3275.  
  3276. const url = new URL(obj.url);
  3277. const stp = url.searchParams.get('stp');
  3278.  
  3279. if (stp != null) {
  3280. return parseInt(stp.match(/_p([0-9]+)x([0-9]+)_/i)?.at(1) || -1);
  3281. }
  3282. else {
  3283. return 0;
  3284. }
  3285. }
  3286.  
  3287. const resourceWidth = getWidthFromURL(result.items[0].image_versions2.candidates[0]);
  3288. if (
  3289. result.items[0].original_width !== resourceWidth &&
  3290. resourceWidth !== -1
  3291. ) {
  3292. // alert();
  3293. }
  3294. }
  3295.  
  3296. if (isPreview) {
  3297. let urlObj = new URL(resource_url);
  3298. urlObj.host = 'scontent.cdninstagram.com';
  3299.  
  3300. openNewTab(urlObj.href);
  3301. }
  3302. else {
  3303. saveFiles(resource_url, username, $(element).attr('data-name'), timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  3304. }
  3305. }
  3306. else {
  3307. if (USER_SETTING.FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED) {
  3308. if (isPreview) {
  3309. let urlObj = new URL($(element).attr('data-href'));
  3310. urlObj.host = 'scontent.cdninstagram.com';
  3311.  
  3312. openNewTab(urlObj.href);
  3313. }
  3314. else {
  3315. saveFiles($(element).attr('data-href'), username, $(element).attr('data-name'), timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  3316. }
  3317. }
  3318. else {
  3319. alert('Fetch failed from Media API. API response message: ' + result.message);
  3320. }
  3321. logger(result);
  3322. }
  3323. }
  3324. else {
  3325. saveFiles($(element).attr('data-href'), username, $(element).attr('data-name'), timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  3326. }
  3327. }
  3328.  
  3329.  
  3330. /**
  3331. * registerMenuCommand
  3332. * @description Register script menu command.
  3333. *
  3334. * @return {void}
  3335. */
  3336. function registerMenuCommand() {
  3337. for (let id of state.registerMenuIds) {
  3338. logger('GM_unregisterMenuCommand', id);
  3339. GM_unregisterMenuCommand(id);
  3340. }
  3341.  
  3342. state.registerMenuIds.push(GM_registerMenuCommand(_i18n('SETTING'), () => {
  3343. showSetting();
  3344. }, {
  3345. accessKey: "w"
  3346. }));
  3347.  
  3348. state.registerMenuIds.push(GM_registerMenuCommand(_i18n('DONATE'), () => {
  3349. GM_openInTab("https://ko-fi.com/snkoarashi", { active: true });
  3350. }, {
  3351. accessKey: "d"
  3352. }));
  3353.  
  3354. state.registerMenuIds.push(GM_registerMenuCommand(_i18n('DEBUG'), () => {
  3355. showDebugDOM();
  3356. }, {
  3357. accessKey: "z"
  3358. }));
  3359.  
  3360. state.registerMenuIds.push(GM_registerMenuCommand(_i18n('FEEDBACK'), () => {
  3361. showFeedbackDOM();
  3362. }, {
  3363. accessKey: "f"
  3364. }));
  3365.  
  3366. state.registerMenuIds.push(GM_registerMenuCommand(_i18n('CHECK_FOR_UPDATE'), () => {
  3367. callNotification();
  3368. }, {
  3369. accessKey: "c"
  3370. }));
  3371.  
  3372. state.registerMenuIds.push(GM_registerMenuCommand(_i18n('RELOAD_SCRIPT'), () => {
  3373. reloadScript();
  3374. }, {
  3375. accessKey: "r"
  3376. }));
  3377. }
  3378.  
  3379. /**
  3380. * checkingScriptUpdate
  3381. * @description Check if there is a new version of the script and push notification.
  3382. *
  3383. * @param {Integer} interval
  3384. * @return {void}
  3385. */
  3386. function checkingScriptUpdate(interval) {
  3387. if (!USER_SETTING.CHECK_FOR_UPDATE) return;
  3388.  
  3389. const check_timestamp = GM_getValue('G_CHECK_TIMESTAMP') ?? new Date().getTime();
  3390. const now_time = new Date().getTime();
  3391.  
  3392. if (now_time > (parseInt(check_timestamp) + (interval * 1000))) {
  3393. GM_setValue('G_CHECK_TIMESTAMP', new Date().getTime());
  3394. callNotification();
  3395. }
  3396. }
  3397.  
  3398. /**
  3399. * callNotification
  3400. * @description Call desktop notification by browser.
  3401. *
  3402. * @return {void}
  3403. */
  3404. function callNotification() {
  3405. const currentVersion = GM_info.script.version;
  3406. const remoteScriptURL = 'https://raw.githubusercontent.com/SN-Koarashi/ig-helper/refs/heads/master/main.js';
  3407.  
  3408. GM_xmlhttpRequest({
  3409. method: "GET",
  3410. url: remoteScriptURL,
  3411. onload: function (response) {
  3412. const remoteScript = response.responseText;
  3413. const match = remoteScript.match(/\/\/\s+@version\s+([0-9.\-a-zA-Z]+)/i);
  3414.  
  3415. if (match && match[1]) {
  3416. const remoteVersion = match[1];
  3417. logger('Current version: ', currentVersion, '|', 'Remote version: ', remoteVersion);
  3418.  
  3419. if (remoteVersion !== currentVersion) {
  3420. GM_notification({
  3421. text: _i18n("NOTICE_UPDATE_CONTENT"),
  3422. title: _i18n("NOTICE_UPDATE_TITLE"),
  3423. tag: 'ig_helper_notice',
  3424. highlight: true,
  3425. timeout: 5000,
  3426. zombieTimeout: 5000,
  3427. image: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Instagram_icon.png/64px-Instagram_icon.png",
  3428. onclick: (event) => {
  3429. event?.preventDefault();
  3430. var w = GM_openInTab(GM_info.script.downloadURL);
  3431. setTimeout(() => {
  3432. w.close();
  3433. }, 250);
  3434. }
  3435. });
  3436. } else {
  3437. logger('there is no new update');
  3438. }
  3439. } else {
  3440. console.error('Could not find version in the remote script.');
  3441. }
  3442. }
  3443. });
  3444. }
  3445.  
  3446. /**
  3447. * showSetting
  3448. * @description Show script settings window.
  3449. *
  3450. * @return {void}
  3451. */
  3452. function showSetting() {
  3453. $('.IG_POPUP_DIG').remove();
  3454. IG_createDM();
  3455.  
  3456. $('.IG_POPUP_DIG #post_info').text('Preference Settings');
  3457. $('.IG_POPUP_DIG .IG_POPUP_DIG_TITLE > div')
  3458. .append(`
  3459. <select id="langSelect"></select>
  3460. <div style="font-size: 12px;">
  3461. Some texts are machine-translated and may be inaccurate; translation contributions are welcome on GitHub.
  3462. </div>
  3463. `);
  3464.  
  3465. for (const o in locale_manifest) {
  3466. $('#langSelect').append(
  3467. `<option value="${o}" ${(state.lang === o) ? 'selected' : ''}>${locale_manifest[o]}</option>`
  3468. );
  3469. }
  3470.  
  3471. const $body = $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY');
  3472.  
  3473. for (const name in USER_SETTING) {
  3474. $body.append(`
  3475. <label class="globalSettings"
  3476. title="${_i18n(name + '_INTRO')}"
  3477. data-ih-locale-title="${name + '_INTRO'}">
  3478.  
  3479. <span data-ih-locale="${name}">${_i18n(name)}</span>
  3480. <input id="${name}" value="box" type="checkbox"
  3481. ${USER_SETTING[name] === true ? 'checked' : ''}>
  3482. <div class="chbtn"><div class="rounds"></div></div>
  3483. </label>`
  3484. );
  3485.  
  3486. if (name === 'MODIFY_VIDEO_VOLUME') {
  3487. $body.find(`input[id="${name}"]`).parent('label').on('contextmenu', function (e) {
  3488. e.preventDefault();
  3489. if (!$(this).find('#tempWrapper').length) {
  3490. $(this).append('<div id="tempWrapper"></div>')
  3491. .children('#tempWrapper')
  3492. .append(`<input value="${state.videoVolume}" type="range" min="0" max="1" step="0.05" />`)
  3493. .append(`<input value="${state.videoVolume}" step="0.05" type="number" />`)
  3494. .append(`<div class="IG_POPUP_DIG_BTN">${SVG.CLOSE}</div>`);
  3495. }
  3496. });
  3497. }
  3498.  
  3499. if (name === 'AUTO_RENAME') {
  3500. $body.find(`input[id="${name}"]`).parent('label').on('contextmenu', function (e) {
  3501. e.preventDefault();
  3502. if (!$(this).find('#tempWrapper').length) {
  3503. $(this).append('<div id="tempWrapper"></div>')
  3504. .children('#tempWrapper')
  3505. .append(`<input id="date_format" value="${state.fileRenameFormat}" />`)
  3506. .append(`<div class="IG_POPUP_DIG_BTN">${SVG.CLOSE}</div>`);
  3507. }
  3508. });
  3509. }
  3510. }
  3511.  
  3512. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY input#CHECK_FOR_UPDATE').closest('label').prependTo('.IG_POPUP_DIG .IG_POPUP_DIG_BODY');
  3513.  
  3514. arrangeSettingHierarchy();
  3515. }
  3516.  
  3517. /**
  3518. * arrangeSettingHierarchy
  3519. * @description Arrange specific settings under the corresponding setting.
  3520. *
  3521. * @return {void}
  3522. */
  3523. function arrangeSettingHierarchy() {
  3524. Object.entries(PARENT_CHILD_MAPPING).forEach(([parent, children]) => {
  3525.  
  3526. let $prev = $(`.IG_POPUP_DIG .IG_POPUP_DIG_BODY input#${parent}`).closest('label');
  3527.  
  3528. children.forEach(child => {
  3529. const $childLbl = $(`.IG_POPUP_DIG .IG_POPUP_DIG_BODY input#${child}`).closest('label').detach();
  3530. $childLbl.addClass("child");
  3531. $prev.after($childLbl);
  3532. $prev = $childLbl;
  3533. });
  3534. });
  3535. }
  3536.  
  3537. /**
  3538. * showDebugDOM
  3539. * @description Show full DOM tree.
  3540. *
  3541. * @return {void}
  3542. */
  3543. function showDebugDOM() {
  3544. $('.IG_POPUP_DIG').remove();
  3545. IG_createDM();
  3546. $('.IG_POPUP_DIG #post_info').text('IG Debug DOM Tree');
  3547.  
  3548. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY').append(`<textarea style="font-family: monospace;width:100%;box-sizing: border-box;height:300px;background: transparent;" readonly></textarea>`);
  3549. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY').append(`<span style="display:block;text-align:center;">`);
  3550. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_DISPLAY_DOM_TREE"><a>${_i18n('SHOW_DOM_TREE')}</a></button>`);
  3551. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_SELECT_DOM_TREE"><a>${_i18n('SELECT_AND_COPY')}</a></button>`);
  3552. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_DOWNLOAD_DOM_TREE"><a>${_i18n('DOWNLOAD_DOM_TREE')}</a></button><br/>`);
  3553. $('.IG_POPUP_DIG .IG_POPUP_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>`);
  3554. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_DISCORD"><a href="https://discord.gg/q3KT4hdq8x" target="_blank">${_i18n('REPORT_DISCORD')}</a></button>`);
  3555. }
  3556.  
  3557. /**
  3558. * showFeedbackDOM
  3559. * @description Show feedback options.
  3560. *
  3561. * @return {void}
  3562. */
  3563. function showFeedbackDOM() {
  3564. $('.IG_POPUP_DIG').remove();
  3565. IG_createDM();
  3566. $('.IG_POPUP_DIG #post_info').text('Feedback Options');
  3567.  
  3568. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY').append(`<span style="display:block;text-align:center;">`);
  3569. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_FORK"><a href="https://greasyfork.org/en/scripts/404535-ig-helper/feedback" target="_blank">${_i18n('REPORT_FORK')}</a></button>`);
  3570. $('.IG_POPUP_DIG .IG_POPUP_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>`);
  3571. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_DISCORD"><a href="https://discord.gg/q3KT4hdq8x" target="_blank">${_i18n('REPORT_DISCORD')}</a></button>`);
  3572. }
  3573.  
  3574. /**
  3575. * openNewTab
  3576. * @description Open URL in new tab.
  3577. *
  3578. * @param {String} link
  3579. * @return {void}
  3580. */
  3581. function openNewTab(link) {
  3582. var a = document.createElement('a');
  3583. a.href = link;
  3584. a.target = '_blank';
  3585.  
  3586. document.body.appendChild(a);
  3587. a.click();
  3588. a.remove();
  3589.  
  3590. setTimeout(() => { updateLoadingBar(false); }, 125);
  3591. }
  3592.  
  3593. /**
  3594. * reloadScript
  3595. * @description Re-register main timer.
  3596. *
  3597. * @return {void}
  3598. */
  3599. function reloadScript() {
  3600. clearInterval(state.GL_repeat);
  3601.  
  3602. // unregister event in post element
  3603. state.GL_registerEventList.forEach(item => {
  3604. item.trigger.forEach(bindElement => {
  3605. $(item.element).off('click', bindElement);
  3606. });
  3607. });
  3608. state.GL_registerEventList = [];
  3609.  
  3610. $('.button_wrapper').remove();
  3611. $('.IG_DWPROFILE, .IG_DWPROFILE, .IG_DWSTORY, .IG_DWSTORY_ALL, .IG_DWSTORY_THUMBNAIL, .IG_DWNEWTAB, .IG_DWHISTORY, .IG_DWHISTORY_ALL, .IG_DWHINEWTAB, .IG_DWHISTORY_THUMBNAIL, .IG_REELS, .IG_REELS_NEWTAB, .IG_REELS_THUMBNAIL').remove();
  3612. $('[data-snig]').removeAttr('data-snig');
  3613.  
  3614. state.pageLoaded = false;
  3615. state.firstStarted = false;
  3616. state.currentURL = location.href;
  3617. state.GL_observer.disconnect();
  3618.  
  3619. logger('main timer re-register completed');
  3620. }
  3621.  
  3622. /**
  3623. * logger
  3624. * @description Event record.
  3625. *
  3626. * @return {void}
  3627. */
  3628. function logger(...messages) {
  3629. var dd = new Date();
  3630. state.GL_logger.push({
  3631. time: dd.getTime(),
  3632. content: [...messages]
  3633. });
  3634.  
  3635. if (state.GL_logger.length > 1000) {
  3636. state.GL_logger = [{
  3637. time: dd.getTime(),
  3638. content: ['logger sliced']
  3639. }, ...state.GL_logger.slice(-999)];
  3640. }
  3641.  
  3642. console.log(`[${dd.toISOString()}]`, ...messages);
  3643. }
  3644.  
  3645. /**
  3646. * initSettings
  3647. * @description Initialize preferences.
  3648. *
  3649. * @return {void}
  3650. */
  3651. function initSettings() {
  3652. for (let name in USER_SETTING) {
  3653. if (GM_getValue(name) != null && typeof GM_getValue(name) === 'boolean') {
  3654. USER_SETTING[name] = GM_getValue(name);
  3655.  
  3656. if (name === "MODIFY_VIDEO_VOLUME" && GM_getValue(name) !== true) {
  3657. state.videoVolume = 1;
  3658. }
  3659. }
  3660. }
  3661. }
  3662.  
  3663.  
  3664. /**
  3665. * toggleVolumeSilder
  3666. * @description Toggle display of custom volume slider.
  3667. *
  3668. * @param {object} $videos
  3669. * @param {object} $buttonParent
  3670. * @param {string} loggerType
  3671. * @param {string} customClass
  3672. * @return {void}
  3673. */
  3674. function toggleVolumeSilder($videos, $buttonParent, loggerType, customClass = "") {
  3675. if ($buttonParent.find('div.volume_slider').length === 0) {
  3676. $buttonParent.append(`<div class="volume_slider ${customClass}" />`);
  3677. $buttonParent.find('div.volume_slider').append(`<div><input type="range" max="1" min="0" step="0.05" value="${state.videoVolume}" /></div>`);
  3678. $buttonParent.find('div.volume_slider input').attr('style', `--ig-track-progress: ${(state.videoVolume * 100) + '%'}`);
  3679. $buttonParent.find('div.volume_slider input').on('input', function () {
  3680. var percent = ($(this).val() * 100) + '%';
  3681.  
  3682. state.videoVolume = $(this).val();
  3683. GM_setValue('G_VIDEO_VOLUME', $(this).val());
  3684.  
  3685. $(this).attr('style', `--ig-track-progress: ${percent}`);
  3686.  
  3687. $videos.each(function () {
  3688. logger(`(${loggerType})`, 'video volume changed #slider');
  3689. this.volume = state.videoVolume;
  3690. });
  3691. });
  3692.  
  3693. $buttonParent.find('div.volume_slider input').on('mouseenter', function () {
  3694. var percent = (state.videoVolume * 100) + '%';
  3695. $(this).attr('style', `--ig-track-progress: ${percent}`);
  3696. $(this).val(state.videoVolume);
  3697.  
  3698.  
  3699. $videos.each(function () {
  3700. logger(`(${loggerType})`, 'video volume changed #slider');
  3701. this.volume = state.videoVolume;
  3702. });
  3703. });
  3704.  
  3705. $buttonParent.find('div.volume_slider').on('click', function (e) {
  3706. e.stopPropagation();
  3707. e.preventDefault();
  3708. });
  3709. }
  3710. else {
  3711. $buttonParent.find('div.volume_slider').remove();
  3712. }
  3713. }
  3714.  
  3715. var detectMovingViewerTimer = null;
  3716.  
  3717. function openImageViewer(imageUrl) {
  3718. removeImageViewer();
  3719.  
  3720. $('body').append(
  3721. `<div id="imageViewer">
  3722. <div id="iv_header">
  3723. <div style="flex:1;">Image Viewer</div>
  3724. <div style="display: flex;filter: invert(1);gap: 8px;margin-right: 8px;">
  3725. <div id="rotate_left" style="cursor: pointer;">${SVG.TURN_DEG}</div>
  3726. <div id="rotate_right" style="transform: scaleX(-1);cursor: pointer;">${SVG.TURN_DEG}</div>
  3727. </div>
  3728. <div id="iv_close">${SVG.CLOSE}</div>
  3729. </div>
  3730. <section>
  3731. <div id="iv_transform">
  3732. <div id="iv_rotate">
  3733. <img id="iv_image" src="" />
  3734. </div>
  3735. </div>
  3736. </section>
  3737. </div>`);
  3738.  
  3739. const $container = $('#imageViewer');
  3740. const $section = $('#imageViewer > section');
  3741. const $wrapT = $('#iv_transform');
  3742. const $wrapR = $('#iv_rotate');
  3743. const $header = $('#iv_header');
  3744. const $closeIcon = $('#iv_close');
  3745. const $image = $('#iv_image');
  3746. const $rotateLeft = $('#rotate_left');
  3747. const $rotateRight = $('#rotate_right');
  3748.  
  3749. $image.attr('src', imageUrl);
  3750. $container.css('display', 'flex');
  3751. $wrapT.css('transform-origin', '0 0');
  3752. $wrapT.css('transition', `transform 0.15s ease`);
  3753. $wrapR.css('transform-origin', 'center');
  3754. $wrapR.css('transition', `transform 0.15s ease`);
  3755. $wrapT.css('will-change', 'transform');
  3756. $wrapR.css('will-change', 'transform');
  3757.  
  3758. let rotate = 0;
  3759. let scale = 1;
  3760. let posX = 0, posY = 0;
  3761. let isDragging = false;
  3762. let isMovingPhoto = false;
  3763. let startX, startY;
  3764. var previousPosition = {
  3765. x: 0,
  3766. y: 0
  3767. };
  3768.  
  3769. detectMovingViewerTimer = setInterval(() => {
  3770. const currentPosition = {
  3771. x: posX,
  3772. y: posY
  3773. };
  3774. if (currentPosition.x !== previousPosition.x || currentPosition.y !== previousPosition.y) {
  3775. isMovingPhoto = true;
  3776. } else {
  3777. isMovingPhoto = false;
  3778. }
  3779. previousPosition = currentPosition;
  3780. }, 100);
  3781.  
  3782.  
  3783. $image.on('load', () => {
  3784. posX = 0;
  3785. posY = 0;
  3786. updateImageStyle();
  3787. });
  3788.  
  3789. $image.on('dragstart drop', (e) => {
  3790. e.preventDefault();
  3791. });
  3792.  
  3793. $image.on('click', (e) => {
  3794. e.preventDefault();
  3795. e.stopPropagation();
  3796.  
  3797. if (!isMovingPhoto) {
  3798. if (scale <= 1) {
  3799. makeZoomAction(e, Math.min(Math.max(1, scale + 1.25), 5));
  3800. }
  3801. else {
  3802. scale = 1;
  3803. posX = 0;
  3804. posY = 0;
  3805. }
  3806.  
  3807. updateImageStyle();
  3808. }
  3809. });
  3810.  
  3811. $section.on('wheel', (e) => {
  3812. e.preventDefault();
  3813. makeZoomAction(e);
  3814. });
  3815.  
  3816. $container.on('wheel', (e) => {
  3817. e.preventDefault();
  3818. });
  3819.  
  3820. $image.on('mousedown', (e) => {
  3821. if (scale == 1) return;
  3822.  
  3823. isDragging = true;
  3824.  
  3825. startX = e.pageX - posX;
  3826. startY = e.pageY - posY;
  3827. $image.css('cursor', 'grabbing');
  3828. });
  3829.  
  3830. $image.on('mouseup', () => {
  3831. if (scale == 1) return;
  3832.  
  3833. isDragging = false;
  3834. $image.css('cursor', 'grab');
  3835. });
  3836.  
  3837. $rotateLeft.on('click', function () {
  3838. rotate -= 90;
  3839. updateImageStyle();
  3840. });
  3841.  
  3842. $rotateRight.on('click', function () {
  3843. rotate += 90;
  3844. updateImageStyle();
  3845. });
  3846.  
  3847. $(document).on('mousemove.igHelper', (e) => {
  3848. if (!isDragging) return;
  3849. e.preventDefault();
  3850.  
  3851. posX = e.pageX - startX;
  3852. posY = e.pageY - startY;
  3853.  
  3854. updateImageStyle();
  3855. });
  3856.  
  3857. $container.on('click', () => {
  3858. removeImageViewer();
  3859. });
  3860.  
  3861. $closeIcon.on('click', () => {
  3862. removeImageViewer();
  3863. });
  3864.  
  3865. $header.on('click', (e) => {
  3866. e.preventDefault();
  3867. e.stopPropagation();
  3868. });
  3869.  
  3870. function updateImageStyle() {
  3871. $wrapT.css('transition', isMovingPhoto ? "none" : `transform 0.15s ease`);
  3872. $wrapT.css('transform', `translate(${posX}px, ${posY}px) scale(${scale})`);
  3873. $wrapR.css('transform', `rotate(${rotate}deg)`);
  3874.  
  3875. if (scale == 1) {
  3876. $image.css('cursor', 'zoom-in');
  3877. }
  3878. else {
  3879. $image.css('cursor', 'grabbing');
  3880. }
  3881. }
  3882.  
  3883.  
  3884. function makeZoomAction(e, newScale) {
  3885. e.preventDefault();
  3886.  
  3887. let prevScale = scale;
  3888.  
  3889. // newScale should be null when passing by wheel event
  3890. if (newScale == null) {
  3891. let factor = 0.1;
  3892. let delta = e.originalEvent.deltaY < 0 ? 1 : -1;
  3893. scale = Math.min(5, Math.max(1, scale + delta * factor * scale));
  3894. }
  3895. else {
  3896. scale = newScale;
  3897. }
  3898.  
  3899.  
  3900. let rect = $section[0].getBoundingClientRect();
  3901. let mx = e.clientX - rect.left;
  3902. let my = e.clientY - rect.top;
  3903.  
  3904. let zoomTargetX = (mx - posX) / prevScale;
  3905. let zoomTargetY = (my - posY) / prevScale;
  3906.  
  3907. posX = -zoomTargetX * scale + mx;
  3908. posY = -zoomTargetY * scale + my;
  3909.  
  3910. updateImageStyle();
  3911. }
  3912. }
  3913.  
  3914. function removeImageViewer() {
  3915. clearInterval(detectMovingViewerTimer);
  3916. $('#imageViewer').remove();
  3917. $(document).off('mousemove.igHelper');
  3918. }
  3919.  
  3920. let mediaCacheDirty = false;
  3921. let mediaCacheSaveTimer = null;
  3922.  
  3923. /**
  3924. * purgeCache
  3925. * @description Purge image cache entries older than 12 hours.
  3926. *
  3927. * @return {void}
  3928. */
  3929. function purgeCache() {
  3930. const now = Date.now();
  3931. for (const id in state.GL_imageCache) {
  3932. if ((now - state.GL_imageCache[id].ts) > IMAGE_CACHE_MAX_AGE) delete state.GL_imageCache[id];
  3933. }
  3934. GM_setValue(IMAGE_CACHE_KEY, state.GL_imageCache);
  3935. }
  3936.  
  3937.  
  3938. /**
  3939. * mediaIdFromURL
  3940. * @description Decode mediaId from ig_cache_key parameter that Instagram includes in the URL.
  3941. *
  3942. * @param {string} url
  3943. * @return {?string}
  3944. */
  3945. function mediaIdFromURL(url) {
  3946. try {
  3947. const u = new URL(url);
  3948. const key = u.searchParams.get('ig_cache_key');
  3949. if (!key) return null;
  3950. const b64 = key.split('.')[0]; // Part before “.3-ccb7…”
  3951. return atob(b64); // e.g., “3670776772828545770”
  3952. } catch { return null; }
  3953. }
  3954.  
  3955. /**
  3956. * putInCache
  3957. * @description Save URL to image cache.
  3958. *
  3959. * @param {string} mediaId
  3960. * @param {string} url
  3961. * @return {void}
  3962. */
  3963. function putInCache(mediaId, url) {
  3964. if (!mediaId) return;
  3965.  
  3966. const keys = Object.keys(state.GL_imageCache);
  3967. if (keys.length >= IMAGE_MAX_CACHE_ITEMS) {
  3968. keys.sort((a, b) => state.GL_imageCache[a].ts - state.GL_imageCache[b].ts);
  3969. delete state.GL_imageCache[keys[0]];
  3970. }
  3971.  
  3972. mediaCacheDirty = true;
  3973. state.GL_imageCache[mediaId] = { url, ts: Date.now() };
  3974.  
  3975. if (!mediaCacheSaveTimer) {
  3976. mediaCacheSaveTimer = setTimeout(() => {
  3977. if (mediaCacheDirty) {
  3978. GM_setValue(IMAGE_CACHE_KEY, state.GL_imageCache);
  3979. mediaCacheDirty = false;
  3980. }
  3981. mediaCacheSaveTimer = null;
  3982. }, 500); // write in script storage per 500 ms
  3983. }
  3984. }
  3985.  
  3986. /**
  3987. * getImageFromCache
  3988. * @description Read image URL from cache; returns null if not found or expired.
  3989. *
  3990. * @param {string} mediaId
  3991. * @return {?string}
  3992. */
  3993. function getImageFromCache(mediaId) {
  3994. if (!mediaId) return null;
  3995. const entry = state.GL_imageCache[mediaId];
  3996. if (!entry) return null;
  3997. if ((Date.now() - entry.ts) > IMAGE_CACHE_MAX_AGE) { delete state.GL_imageCache[mediaId]; return null; }
  3998. return entry.url;
  3999. }
  4000.  
  4001. /**
  4002. * registerPerformanceObserver
  4003. * @description Register performance observer to document, captures any loaded image resource.
  4004. *
  4005. * @return {void}
  4006. */
  4007. function registerPerformanceObserver() {
  4008. const perfObs = new PerformanceObserver(list => {
  4009. if (!USER_SETTING.CAPTURE_IMAGE_VIA_MEDIA_CACHE) return;
  4010.  
  4011. list.getEntries().forEach(entry => {
  4012. if (entry.initiatorType === 'img') {
  4013. const u = entry.name;
  4014.  
  4015. if (!(u.includes('_e35') || u.includes('_e15') || u.includes('.webp?efg=')) || u.match(/_[sp](\d+)x\1(?!\d)/)) return;
  4016. const id = mediaIdFromURL(u);
  4017. if (id && !state.GL_imageCache[id]) putInCache(id, u);
  4018. }
  4019. });
  4020. });
  4021. perfObs.observe({ entryTypes: ['resource'] });
  4022. }
  4023.  
  4024. /**
  4025. * translateText
  4026. * @description i18n translation text.
  4027. *
  4028. * @return {void}
  4029. */
  4030. function translateText() {
  4031. var eLocale = {
  4032. "en-US": {
  4033. "NOTICE_UPDATE_TITLE": "Wololo! New version released.",
  4034. "NOTICE_UPDATE_CONTENT": "IG-Helper has released a new version, click here to update.",
  4035. "CHECK_FOR_UPDATE": "Check for Script Updates",
  4036. "RELOAD_SCRIPT": "Reload Script",
  4037. "DONATE": "Donate",
  4038. "FEEDBACK": "Feedback",
  4039. "IMAGE_VIEWER": "Open Image In Viewer",
  4040. "NEW_TAB": "Open in New Tab",
  4041. "SHOW_DOM_TREE": "Show DOM Tree",
  4042. "SELECT_AND_COPY": "Select All and Copy from the Input Box",
  4043. "DOWNLOAD_DOM_TREE": "Download DOM Tree as a Text File",
  4044. "REPORT_GITHUB": "Report an Issue on GitHub",
  4045. "REPORT_DISCORD": "Report an Issue on Discord Support Server",
  4046. "REPORT_FORK": "Report an Issue on Greasy Fork",
  4047. "DEBUG": "Debug Window",
  4048. "CLOSE": "Close",
  4049. "ALL_CHECK": "Select All",
  4050. "BATCH_DOWNLOAD_SELECTED": "Download Selected Resources",
  4051. "BATCH_DOWNLOAD_DIRECT": "Download All Resources",
  4052. "IMG": "Image",
  4053. "VID": "Video",
  4054. "DW": "Download",
  4055. "DW_ALL": "Download All Resources",
  4056. "VIDEO_THUMBNAIL": "Download Video Thumbnail",
  4057. "LOAD_BLOB_ONE": "Loading Blob Media...",
  4058. "LOAD_BLOB_MULTIPLE": "Loading Blob Media and Others...",
  4059. "LOAD_BLOB_RELOAD": "Detecting Blob Media, reloading...",
  4060. "NO_CHECK_RESOURCE": "You need to select a resource to download.",
  4061. "NO_VID_URL": "Cannot find video URL.",
  4062. "SETTING": "Settings",
  4063. "AUTO_RENAME": "Automatically Rename Files (Right-Click to Set)",
  4064. "RENAME_PUBLISH_DATE": "Set Renamed File Timestamp to Resource Publish Date",
  4065. "RENAME_LOCATE_DATE": "Modify Renamed File Timestamp Date Format (Right-Click to Set)",
  4066. "DISABLE_VIDEO_LOOPING": "Disable Video Auto-looping",
  4067. "HTML5_VIDEO_CONTROL": "Display HTML5 Video Controller",
  4068. "REDIRECT_CLICK_USER_STORY_PICTURE": "Redirect When Clicking on User's Story Picture",
  4069. "FORCE_FETCH_ALL_RESOURCES": "Force Fetch All Resources in the Post",
  4070. "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Directly Download the Visible Resources in the Post",
  4071. "DIRECT_DOWNLOAD_ALL": "Directly Download All Resources in the Post",
  4072. "DIRECT_DOWNLOAD_STORY": "Directly Download All Resources in the Story/Highlight",
  4073. "MODIFY_VIDEO_VOLUME": "Modify Video Volume (Right-Click to Set)",
  4074. "MODIFY_RESOURCE_EXIF": "Modify Resource EXIF Properties",
  4075. "SCROLL_BUTTON": "Enable Scroll Buttons for Reels Page",
  4076. "FORCE_RESOURCE_VIA_MEDIA": "Force Fetch Resource via Media API",
  4077. "FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED": "Use Alternative Methods to Download When the Media API is Not Accessible",
  4078. "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "Always Use Media API for 'Open in New Tab' in Posts",
  4079. "SKIP_VIEW_STORY_CONFIRM": "Skip the Confirmation Page for Viewing a Story/Highlight",
  4080. "CAPTURE_IMAGE_VIA_MEDIA_CACHE": "Capture Image Resource Using Media Cache",
  4081. "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%2-YEAR% - Year (last two digits) 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%ORIGINAL_NAME_FIRST% - Original name of downloaded file (first part of name)\n\nIf set to false, the file name will remain unchanged.\nExample: instagram_321565527_679025940443063_4318007696887450953_n.jpg",
  4082. "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.",
  4083. "RENAME_LOCATE_DATE_INTRO": "Modify the renamed file timestamp date format to the browser's local time, and format it to your preferred regional date format.\n\nThis feature only works when [Automatically Rename Files] is set to TRUE.",
  4084. "DISABLE_VIDEO_LOOPING_INTRO": "Disable video auto-looping in Reels and posts.",
  4085. "HTML5_VIDEO_CONTROL_INTRO": "Display the HTML5 video controller in video resource.\n\nThis will hide the custom video volume slider and replace it with the HTML5 controller. The HTML5 controller can be hidden by right-clicking on the video to reveal the original details.",
  4086. "REDIRECT_CLICK_USER_STORY_PICTURE_INTRO": "Redirect to a user's profile page when right-clicking on their avatar in the story area on the homepage.\nIf you use the middle mouse button to click, it will open in a new tab.",
  4087. "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.",
  4088. "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Directly download the current resources available in the post.",
  4089. "DIRECT_DOWNLOAD_ALL_INTRO": "When you click the download button, all resources in the post will be forcibly fetched and downloaded.",
  4090. "MODIFY_VIDEO_VOLUME_INTRO": "Modify the video playback volume in Reels and posts (right-click to open the volume setting slider).",
  4091. "SCROLL_BUTTON_INTRO": "Enable scroll buttons for the lower right corner of the Reels page.",
  4092. "FORCE_RESOURCE_VIA_MEDIA_INTRO": "The Media API will try to get the highest quality photo or video possible, but it may take longer to load.",
  4093. "FALLBACK_TO_BLOB_FETCH_IF_MEDIA_API_THROTTLED_INTRO": "When the Media API reaches its rate limit or cannot be used for other reasons, the Forced Fetch API will be used to download resources (the resource quality may be slightly lower).",
  4094. "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST_INTRO": "The [Open in New Tab] button in posts will always use the Media API to obtain high-resolution resources.",
  4095. "CHECK_FOR_UPDATE_INTRO": "Check for updates when the script is triggered (check every 300 seconds).\nUpdate notifications will be sent as desktop notifications through the browser.",
  4096. "SKIP_VIEW_STORY_CONFIRM_INTRO": "Automatically skip when confirmation page is shown in story or highlight.",
  4097. "MODIFY_RESOURCE_EXIF_INTRO": "Modify the EXIF properties of the image resource to place the post link in it.",
  4098. "DIRECT_DOWNLOAD_STORY_INTRO": "When you click Download All Resources, all stories/highlights are downloaded directly, without showing the image selection dialog.",
  4099. "CAPTURE_IMAGE_VIA_MEDIA_CACHE_INTRO": "Use a watcher to capture any high-quality image URLs in the DOM tree into the script’s storage so that they can be extracted when available and upon user input.",
  4100. }
  4101. };
  4102.  
  4103. var resultUnsorted = Object.assign({}, eLocale, state.locale);
  4104. var resultSorted = Object.keys(resultUnsorted).sort().reduce(
  4105. (obj, key) => {
  4106. obj[key] = resultUnsorted[key];
  4107. return obj;
  4108. }, {}
  4109. );
  4110.  
  4111. return resultSorted;
  4112. }
  4113.  
  4114. /**
  4115. * getTranslationText
  4116. * @description i18n translation text.
  4117. *
  4118. * @param {String} lang
  4119. * @return {Object}
  4120. */
  4121. async function getTranslationText(lang) {
  4122. return new Promise((resolve, reject) => {
  4123. GM_xmlhttpRequest({
  4124. method: "GET",
  4125. url: `https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/locale/translations/${lang}.json`,
  4126. onload: function (response) {
  4127. try {
  4128. let obj = JSON.parse(response.response);
  4129. resolve(obj);
  4130. }
  4131. catch (err) {
  4132. reject(err);
  4133. }
  4134. },
  4135. onerror: function (err) {
  4136. logger('getTranslationText()', 'reject', err);
  4137. reject(err);
  4138. }
  4139. });
  4140. });
  4141. }
  4142.  
  4143. /**
  4144. * _i18n
  4145. * @description Perform i18n translation.
  4146. *
  4147. * @param {String} text
  4148. * @return {void}
  4149. */
  4150. function _i18n(text) {
  4151. const translate = translateText();
  4152.  
  4153. if (translate[state.lang] != undefined && translate[state.lang][text] != undefined) {
  4154. return translate[state.lang][text];
  4155. }
  4156. else {
  4157. return translate["en-US"][text];
  4158. }
  4159. }
  4160.  
  4161. /**
  4162. * repaintingTranslations
  4163. * @description Perform i18n translation.
  4164. *
  4165. * @return {void}
  4166. */
  4167. function repaintingTranslations() {
  4168. $('[data-ih-locale]').each(function () {
  4169. $(this).text(_i18n($(this).attr('data-ih-locale')));
  4170. });
  4171. $('[data-ih-locale-title]').each(function () {
  4172. $(this).attr('title', _i18n($(this).attr('data-ih-locale-title')));
  4173. });
  4174. }
  4175.  
  4176. /* register all events */
  4177.  
  4178. // Running if document is ready
  4179. $(function () {
  4180. function ConvertDOM(domEl) {
  4181. var obj = [];
  4182. for (var ele of domEl) {
  4183. obj.push({
  4184. tagName: ele.tagName,
  4185. id: ele.id,
  4186. className: ele.className
  4187. });
  4188. }
  4189.  
  4190. return obj;
  4191. }
  4192.  
  4193. function setDOMTreeContent() {
  4194. let text = $('div[id^="mount"]')[0];
  4195. var logger = "";
  4196. state.GL_logger.forEach(log => {
  4197. var jsonData = JSON.stringify(log.content, function (key, value) {
  4198. if (Array.isArray(this)) {
  4199. if (typeof value === "object" && value instanceof jQuery) {
  4200. return ConvertDOM(value);
  4201. }
  4202. return value;
  4203. }
  4204. else {
  4205. return value;
  4206. }
  4207. }, "\t");
  4208. logger += `${new Date(log.time).toISOString()}: ${jsonData}\n`
  4209. });
  4210. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').text("Logger:\n" + logger + "\n-----\n\nLocation: " + location.pathname + "\nDOM Tree with div#mount:\n" + text.innerHTML);
  4211. }
  4212.  
  4213. $('body').on('click', '.IG_POPUP_DIG .IG_POPUP_DIG_BODY .IG_DISPLAY_DOM_TREE', function () {
  4214. setDOMTreeContent();
  4215. });
  4216.  
  4217. $('body').on('click', '.IG_POPUP_DIG .IG_POPUP_DIG_BODY .IG_SELECT_DOM_TREE', function () {
  4218. $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').select();
  4219. document.execCommand('copy');
  4220. });
  4221.  
  4222. $('body').on('click', '.IG_POPUP_DIG .IG_POPUP_DIG_BODY .IG_DOWNLOAD_DOM_TREE', function () {
  4223. if ($('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').text().length === 0) {
  4224. setDOMTreeContent();
  4225. }
  4226.  
  4227. var text = $('.IG_POPUP_DIG .IG_POPUP_DIG_BODY textarea').text();
  4228. var a = document.createElement("a");
  4229. var file = new Blob([text], { type: "text/plain" });
  4230. a.href = URL.createObjectURL(file);
  4231. a.download = "DOMTree-" + new Date().getTime() + ".txt";
  4232.  
  4233. document.body.appendChild(a);
  4234. a.click();
  4235. a.remove();
  4236. });
  4237.  
  4238. // Close the download dialog if user click the close icon
  4239. $('body').on('click', '.IG_POPUP_DIG_BTN, .IG_POPUP_DIG_BG', function () {
  4240. if ($(this).parent('#tempWrapper').length > 0) {
  4241. $(this).parent('#tempWrapper').fadeOut(250, function () {
  4242. $(this).remove();
  4243. });
  4244. }
  4245. else {
  4246. $('.IG_POPUP_DIG').remove();
  4247. }
  4248. });
  4249.  
  4250. $(window).on('keydown', function (e) {
  4251. // Hot key [Alt+Q] to close the download dialog
  4252. if (e.which == '81' && e.altKey) {
  4253. $('.IG_POPUP_DIG').remove();
  4254. e.preventDefault();
  4255. }
  4256. // Hot key [Alt+W] to open the settings dialog
  4257. if (e.which == '87' && e.altKey) {
  4258. showSetting();
  4259. e.preventDefault();
  4260. }
  4261.  
  4262. // Hot key [Alt+Z] to open the settings dialog
  4263. if (e.which == '90' && e.altKey) {
  4264. showDebugDOM();
  4265. e.preventDefault();
  4266. }
  4267.  
  4268. // Hot key [Alt+R] to open the settings dialog
  4269. if (e.which == '82' && e.altKey) {
  4270. reloadScript();
  4271. e.preventDefault();
  4272. }
  4273.  
  4274. // Hot key [Alt+S] to download story/highlights resource
  4275. if (e.which == '83' && e.altKey) {
  4276. if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/)/ig) && $('.IG_DWSTORY').length > 0) {
  4277. $('.IG_DWSTORY')?.trigger("click");
  4278. }
  4279. if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/highlights\/)/ig) && $('.IG_DWHISTORY').length > 0) {
  4280. $('.IG_DWHISTORY')?.trigger("click");
  4281. }
  4282. e.preventDefault();
  4283. }
  4284. });
  4285.  
  4286. $('body').on('change', '.IG_POPUP_DIG input', function () {
  4287. var name = $(this).attr('id');
  4288.  
  4289. if (name && USER_SETTING[name] !== undefined) {
  4290. let isChecked = $(this).prop('checked');
  4291. GM_setValue(name, isChecked);
  4292. USER_SETTING[name] = isChecked;
  4293.  
  4294. console.log('user settings', name, isChecked);
  4295. }
  4296. });
  4297.  
  4298. $('body').on('click', '.IG_POPUP_DIG .globalSettings', function (e) {
  4299. if ($(this).find('#tempWrapper').length > 0) {
  4300. e.preventDefault();
  4301. }
  4302. });
  4303.  
  4304. $('body').on('change', '.IG_POPUP_DIG #tempWrapper input:not(#date_format)', function () {
  4305. let value = $(this).val();
  4306.  
  4307. if ($(this).attr('type') == 'range') {
  4308. $(this).next().val(value);
  4309. }
  4310. else {
  4311. $(this).prev().val(value);
  4312. }
  4313.  
  4314. if (value >= 0 && value <= 1) {
  4315. state.videoVolume = value;
  4316. GM_setValue('G_VIDEO_VOLUME', value);
  4317. }
  4318. });
  4319.  
  4320. $('body').on('input', '.IG_POPUP_DIG #tempWrapper input:not(#date_format)', function () {
  4321. if ($(this).attr('type') == 'range') {
  4322. let value = $(this).val();
  4323. $(this).next().val(value);
  4324. }
  4325. else {
  4326. let value = $(this).val();
  4327. if (value >= 0 && value <= 1) {
  4328. $(this).prev().val(value);
  4329. }
  4330. else {
  4331. if (value < 0) {
  4332. $(this).val(0);
  4333. }
  4334. else {
  4335. $(this).val(1);
  4336. }
  4337. }
  4338. }
  4339. });
  4340.  
  4341. $('body').on('input', '.IG_POPUP_DIG #tempWrapper input#date_format', function () {
  4342. GM_setValue('G_RENAME_FORMAT', $(this).val());
  4343. state.fileRenameFormat = $(this).val();
  4344. });
  4345.  
  4346. $('body').on('click', 'a[data-needed="direct"]', function (e) {
  4347. e.preventDefault();
  4348. triggerLinkElement(this);
  4349. });
  4350.  
  4351. $('body').on('click', '.IG_POPUP_DIG_BODY .newTab', function () {
  4352. // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video)
  4353.  
  4354. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST) {
  4355. triggerLinkElement($(this).parent().children('a').first()[0], true);
  4356. }
  4357. else {
  4358. var urlObj = new URL($(this).parent().children('a').attr('data-href'));
  4359. urlObj.host = 'scontent.cdninstagram.com';
  4360.  
  4361. openNewTab(urlObj.href);
  4362. }
  4363. });
  4364.  
  4365. $('body').on('click', '.IG_POPUP_DIG_BODY .videoThumbnail', function () {
  4366. let timestamp = new Date().getTime();
  4367.  
  4368. if (USER_SETTING.RENAME_PUBLISH_DATE && $(this).parent().children('a').attr('datetime')) {
  4369. timestamp = $(this).parent().children('a').attr('datetime');
  4370. }
  4371.  
  4372. let postPath = $(this).parent().children('a').attr('data-path') ?? $('#article-id').text();
  4373.  
  4374. saveFiles($(this).parent().children('a').find('img').first().attr('src'), $(this).parent().children('a').attr('data-username'), 'thumbnail', timestamp, 'jpg', postPath);
  4375. });
  4376.  
  4377. // Running if user left-click download icon in stories
  4378. $('body').on('click', '.IG_DWSTORY', function () {
  4379. onStory(true);
  4380. });
  4381.  
  4382. // Running if user left-click all download icon in stories
  4383. $('body').on('click', '.IG_DWSTORY_ALL', function () {
  4384. onStoryAll();
  4385. });
  4386.  
  4387. // Running if user left-click 'open in new tab' icon in stories
  4388. $('body').on('click', '.IG_DWNEWTAB', function (e) {
  4389. e.preventDefault();
  4390. onStory(true, true, true);
  4391. });
  4392.  
  4393. // Running if user left-click download thumbnail icon in stories
  4394. $('body').on('click', '.IG_DWSTORY_THUMBNAIL', function () {
  4395. onStoryThumbnail(true);
  4396. });
  4397.  
  4398. // Running if user left-click download icon in profile
  4399. $('body').on('click', '.IG_DWPROFILE', function (e) {
  4400. e.stopPropagation();
  4401. onProfileAvatar(true);
  4402. });
  4403.  
  4404. // Running if user left-click download icon in highlight stories
  4405. $('body').on('click', '.IG_DWHISTORY', function () {
  4406. onHighlightsStory(true);
  4407. });
  4408.  
  4409. // Running if user left-click all download icon in highlight stories
  4410. $('body').on('click', '.IG_DWHISTORY_ALL', function () {
  4411. onHighlightsStoryAll();
  4412. });
  4413.  
  4414. // Running if user left-click 'open in new tab' icon in highlight stories
  4415. $('body').on('click', '.IG_DWHINEWTAB', function (e) {
  4416. e.preventDefault();
  4417. onHighlightsStory(true, true);
  4418. });
  4419.  
  4420. // Running if user left-click thumbnail download icon in highlight stories
  4421. $('body').on('click', '.IG_DWHISTORY_THUMBNAIL', function () {
  4422. onHighlightsStoryThumbnail(true);
  4423. });
  4424.  
  4425. // Running if user left-click download icon in reels
  4426. $('body').on('click', '.IG_REELS', function () {
  4427. onReels(true, true);
  4428. });
  4429.  
  4430. // Running if user left-click newtab icon in reels
  4431. $('body').on('click', '.IG_REELS_NEWTAB', function () {
  4432. onReels(true, true, true);
  4433. });
  4434.  
  4435. // Running if user left-click download icon in reels
  4436. $('body').on('click', '.IG_REELS_THUMBNAIL', function () {
  4437. onReels(true, false);
  4438. });
  4439.  
  4440. // Running if user right-click profile picture in stories area
  4441. $('body').on('mousedown', 'button[role="menuitem"], div[role="menuitem"], ul > li[tabindex="-1"] > div[role="button"]', function (e) {
  4442. // Right-Click || Middle-Click
  4443. if (e.which === 3 || e.which === 2) {
  4444. if (location.href === 'https://www.instagram.com/' && USER_SETTING.REDIRECT_CLICK_USER_STORY_PICTURE) {
  4445. e.preventDefault();
  4446. if ($(this).find('canvas._aarh, canvas + span > img').length > 0) {
  4447. const targetUrl = 'https://www.instagram.com/' + $(this).children('div').last().text();
  4448. if (e.which === 2) {
  4449. GM_openInTab(targetUrl);
  4450. }
  4451. else {
  4452. location.href = targetUrl;
  4453. }
  4454. }
  4455. }
  4456. }
  4457. });
  4458.  
  4459. $('body').on('change', '.IG_POPUP_DIG_TITLE .checkbox', function () {
  4460. var isChecked = $(this).find('input').prop('checked');
  4461. $('.IG_POPUP_DIG_BODY .inner_box').each(function () {
  4462. $(this).prop('checked', isChecked);
  4463. });
  4464. });
  4465.  
  4466. $('body').on('change', '.IG_POPUP_DIG_BODY .inner_box', function () {
  4467. var checked = $('.IG_POPUP_DIG_BODY .inner_box:checked').length;
  4468. var total = $('.IG_POPUP_DIG_BODY .inner_box').length;
  4469.  
  4470.  
  4471. $('.IG_POPUP_DIG_TITLE .checkbox').find('input').prop('checked', checked == total);
  4472. });
  4473.  
  4474. $('body').on('click', '.IG_POPUP_DIG_TITLE #batch_download_selected', function () {
  4475. let index = 0;
  4476. $('.IG_POPUP_DIG_BODY a[data-needed="direct"]').each(function () {
  4477. if ($(this).prev().children('input').prop('checked')) {
  4478. $(this).trigger("click");
  4479. index++;
  4480. }
  4481. });
  4482.  
  4483. if (index == 0) {
  4484. alert(_i18n('NO_CHECK_RESOURCE'));
  4485. }
  4486. });
  4487.  
  4488. $('body').on('change', '.IG_POPUP_DIG_TITLE #langSelect', function () {
  4489. GM_setValue('UI_LANGUAGE', $(this).val());
  4490. state.lang = $(this).val();
  4491.  
  4492. if (state.lang?.startsWith('en') || state.locale[state.lang] != null) {
  4493. repaintingTranslations();
  4494. registerMenuCommand();
  4495. }
  4496. else {
  4497. getTranslationText(state.lang).then((res) => {
  4498. state.locale[state.lang] = res;
  4499. repaintingTranslations();
  4500. registerMenuCommand();
  4501. }).catch((err) => {
  4502. console.error('getTranslationText catch error:', err);
  4503. });
  4504. }
  4505. });
  4506.  
  4507. $('body').on('click', '.IG_POPUP_DIG_TITLE #batch_download_direct', function () {
  4508. $('.IG_POPUP_DIG_BODY a[data-needed="direct"]').each(function () {
  4509. $(this).trigger("click");
  4510. });
  4511. });
  4512.  
  4513. registerPerformanceObserver();
  4514.  
  4515. const element_observer = new MutationObserver((mutationsList) => {
  4516. for (const mutation of mutationsList) {
  4517. if (mutation.type === 'childList') {
  4518. mutation.addedNodes.forEach((node) => {
  4519. const $videos = $(node).find('video');
  4520.  
  4521. if (location.pathname.startsWith("/stories/highlights/")) {
  4522. if (
  4523. $(node).attr("data-ih-locale-title") == null &&
  4524. $(node).attr("data-visualcompletion") == null &&
  4525. node.tagName === "DIV"
  4526. ) {
  4527. // replace something times ago format to publish time when switch highlight
  4528. var $time = $(node).find("time[datetime]");
  4529. let publishTitle = $time?.attr('title');
  4530. if (publishTitle != null) {
  4531. $time.text(publishTitle);
  4532. }
  4533. }
  4534. }
  4535.  
  4536. if ($videos.length > 0) {
  4537. // Modify video volume
  4538. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  4539. $videos.each(function () {
  4540. $(this).on('play playing', function () {
  4541. if (!$(this).data('modify')) {
  4542. $(this).attr('data-modify', true);
  4543. this.volume = state.videoVolume;
  4544. logger('(audio_observer) Added video event listener #modify');
  4545. }
  4546. });
  4547. });
  4548. }
  4549.  
  4550. if (location.pathname.match(/^(\/stories\/)/ig)) {
  4551. const isHighlight = location.pathname.match(/^(\/stories\/highlights\/)/ig) != null;
  4552. const storyType = isHighlight ? 'highlight' : 'story';
  4553.  
  4554. $videos.each(function () {
  4555. $(this).on('timeupdate', function () {
  4556. if (!$(this).data('modify-thumbnail')) {
  4557. let $video = $(this);
  4558. if ($video.parents('div[style][class]').filter(function () {
  4559. return $(this).width() == $video.width();
  4560. }).find('.IG_DWSTORY_THUMBNAIL, .IG_DWHISTORY_THUMBNAIL').length === 0) {
  4561. $(this).attr('data-modify-thumbnail', true);
  4562.  
  4563. if (isHighlight) {
  4564. onHighlightsStoryThumbnail(false);
  4565. }
  4566. else {
  4567. onStoryThumbnail(false);
  4568. }
  4569.  
  4570. logger(`(${storyType})`, 'Manually inserting thumbnail button');
  4571. }
  4572. else {
  4573. $(this).attr('data-modify-thumbnail', true);
  4574. logger(`(${storyType})`, 'Thumbnail button already inserted');
  4575. }
  4576. }
  4577. });
  4578.  
  4579. var $video = $(this);
  4580.  
  4581. if (USER_SETTING.HTML5_VIDEO_CONTROL) {
  4582. if (!$video.data('controls')) {
  4583. logger(`(${storyType})`, 'Added video html5 contorller #modify');
  4584.  
  4585. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  4586. this.volume = state.videoVolume;
  4587.  
  4588. $video.on('loadstart', function () {
  4589. this.volume = state.videoVolume;
  4590. });
  4591. }
  4592.  
  4593. let $videoParent = $video.parents('div').filter(function () {
  4594. return $(this).attr('class') == null && $(this).attr('style') == null;
  4595. }).first();
  4596.  
  4597. // story bottom bar
  4598. let $bottomBar = $videoParent.next();
  4599. $bottomBar.hide();
  4600.  
  4601. // read more button in center
  4602. let $readMoreButton = $videoParent.find('div[class][role="button"]');
  4603. $readMoreButton.hide();
  4604.  
  4605. const hideContextmenu = function (e) {
  4606. e.preventDefault();
  4607. $video.css('z-index', '2');
  4608. $video.attr('controls', true);
  4609.  
  4610. $readMoreButton.hide();
  4611. $bottomBar.hide();
  4612.  
  4613. toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () {
  4614. return $(this).width() == $video.width();
  4615. }).first(), storyType, 'vertical');
  4616. };
  4617.  
  4618. // Hide layout to show controller
  4619. $video.parent().find('video + div').on('contextmenu', hideContextmenu);
  4620. $readMoreButton.on('contextmenu', hideContextmenu);
  4621. $bottomBar.on('contextmenu', hideContextmenu);
  4622.  
  4623. // Restore layout to show details interface
  4624. $video.on('contextmenu', function (e) {
  4625. e.preventDefault();
  4626. $video.css('z-index', '-1');
  4627. $video.removeAttr('controls');
  4628.  
  4629. $bottomBar.show();
  4630. $readMoreButton.show();
  4631.  
  4632. toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () {
  4633. return $(this).width() == $video.width();
  4634. }).first(), storyType, 'vertical');
  4635. });
  4636.  
  4637. $video.on('volumechange', function () {
  4638. // This is mute/unmute's icon
  4639. let $element_mute_button = $videoParent.parent().find('svg > path[d^="M1.5 13.3c-.8 0-1.5.7-1.5 1.5v18.4c0"], svg > path[d^="M16.636 7.028a1.5 1.5"]').parents('[role="button"]').first();
  4640.  
  4641. var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0;
  4642.  
  4643. if (this.muted != is_elelment_muted) {
  4644. this.volume = state.videoVolume;
  4645. $element_mute_button?.trigger("click");
  4646. }
  4647.  
  4648. if ($(this).attr('data-completed')) {
  4649. state.videoVolume = this.volume;
  4650. GM_setValue('G_VIDEO_VOLUME', this.volume);
  4651. }
  4652.  
  4653. if (this.volume == state.videoVolume) {
  4654. $(this).attr('data-completed', true);
  4655. }
  4656. });
  4657.  
  4658. $video.css('position', 'absolute');
  4659. $video.css('z-index', '2');
  4660. $video.attr('data-controls', true);
  4661. $video.attr('controls', true);
  4662. }
  4663. }
  4664. else {
  4665. toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () {
  4666. return $(this).width() == $video.width();
  4667. }).first(), storyType, 'vertical');
  4668. }
  4669. });
  4670. }
  4671. }
  4672. });
  4673. }
  4674. }
  4675. });
  4676.  
  4677. element_observer.observe($('div[id^="mount"]')[0], {
  4678. childList: true,
  4679. subtree: true,
  4680. });
  4681. });
  4682. })(jQuery);