IG小助手

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

当前为 2025-02-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name IG Helper
  3. // @name:zh-TW IG小精靈
  4. // @name:zh-CN IG小助手
  5. // @name:ja IG助手
  6. // @name:ko IG조수
  7. // @namespace https://github.snkms.com/
  8. // @version 2.43.7
  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. (function ($) {
  43. 'use strict';
  44.  
  45. /******** USER SETTINGS ********/
  46. // !!! DO NOT CHANGE THIS AREA !!!
  47. // PLEASE CHANGE SETTING WITH MENU
  48. const USER_SETTING = {
  49. 'CHECK_UPDATE': true,
  50. 'AUTO_RENAME': true,
  51. 'RENAME_PUBLISH_DATE': true,
  52. 'DISABLE_VIDEO_LOOPING': false,
  53. 'HTML5_VIDEO_CONTROL': false,
  54. 'REDIRECT_CLICK_USER_STORY_PICTURE': false,
  55. 'FORCE_FETCH_ALL_RESOURCES': false,
  56. 'DIRECT_DOWNLOAD_VISIBLE_RESOURCE': false,
  57. 'DIRECT_DOWNLOAD_ALL': false,
  58. 'MODIFY_VIDEO_VOLUME': false,
  59. 'SCROLL_BUTTON': true,
  60. 'FORCE_RESOURCE_VIA_MEDIA': false,
  61. 'USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT': false,
  62. 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST': false,
  63. 'SKIP_VIEW_STORY_CONFIRM': false
  64. };
  65. const CHILD_NODES = ['RENAME_PUBLISH_DATE', 'USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT', 'NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST'];
  66. var VIDEO_VOLUME = (GM_getValue('G_VIDEO_VOLUME')) ? GM_getValue('G_VIDEO_VOLUME') : 1;
  67. var TEMP_FETCH_RATE_LIMIT = false;
  68. var RENAME_FORMAT = (GM_getValue('G_RENAME_FORMAT')) ? GM_getValue('G_RENAME_FORMAT') : '%USERNAME%-%SOURCE_TYPE%-%SHORTCODE%-%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%%SECOND%_%ORIGINAL_NAME_FIRST%';
  69. /*******************************/
  70.  
  71. // Icon download by https://www.flaticon.com/authors/pixel-perfect
  72. const SVG = {
  73. DOWNLOAD: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g><g><path d="M382.56,233.376C379.968,227.648,374.272,224,368,224h-64V16c0-8.832-7.168-16-16-16h-64c-8.832,0-16,7.168-16,16v208h-64 c-6.272,0-11.968,3.68-14.56,9.376c-2.624,5.728-1.6,12.416,2.528,17.152l112,128c3.04,3.488,7.424,5.472,12.032,5.472 c4.608,0,8.992-2.016,12.032-5.472l112-128C384.192,245.824,385.152,239.104,382.56,233.376z"/></g></g><g><g><path d="M432,352v96H80v-96H16v128c0,17.696,14.336,32,32,32h416c17.696,0,32-14.304,32-32V352H432z"/></g></g>',
  74. NEW_TAB: '<svg width="16" height="16" viewBox="3 3 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 14a1 1 0 0 0-1 1v3.077c0 .459-.022.57-.082.684a.363.363 0 0 1-.157.157c-.113.06-.225.082-.684.082H5.923c-.459 0-.571-.022-.684-.082a.363.363 0 0 1-.157-.157c-.06-.113-.082-.225-.082-.684L4.999 5.5a.5.5 0 0 1 .5-.5l3.5.005a1 1 0 1 0 .002-2L5.501 3a2.5 2.5 0 0 0-2.502 2.5v12.577c0 .76.083 1.185.32 1.627.223.419.558.753.977.977.442.237.866.319 1.627.319h12.154c.76 0 1.185-.082 1.627-.319.419-.224.753-.558.977-.977.237-.442.319-.866.319-1.627V15a1 1 0 0 0-1-1zm-2-9.055v-.291l-.39.09A10 10 0 0 1 15.36 5H14a1 1 0 1 1 0-2l5.5.003a1.5 1.5 0 0 1 1.5 1.5V10a1 1 0 1 1-2 0V8.639c0-.757.086-1.511.256-2.249l.09-.39h-.295a10 10 0 0 1-1.411 1.775l-5.933 5.932a1 1 0 0 1-1.414-1.414l5.944-5.944A10 10 0 0 1 18 4.945z" fill="currentColor"/></svg>',
  75. THUMBNAIL: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512"><circle cx="8.25" cy="5.25" r=".5"/><path d="m8.25 6.5c-.689 0-1.25-.561-1.25-1.25s.561-1.25 1.25-1.25 1.25.561 1.25 1.25-.561 1.25-1.25 1.25zm0-1.5c-.138 0-.25.112-.25.25 0 .275.5.275.5 0 0-.138-.112-.25-.25-.25z"/><path d="m7.25 11.25 2-2.5 2.25 1.5 2.25-3.5 3 4.5z"/><path d="m16.75 12h-9.5c-.288 0-.551-.165-.676-.425s-.09-.568.09-.793l2-2.5c.243-.304.678-.372 1.002-.156l1.616 1.077 1.837-2.859c.137-.212.372-.342.625-.344.246-.026.49.123.63.334l3 4.5c.153.23.168.526.037.77-.13.244-.385.396-.661.396zm-4.519-1.5h3.118l-1.587-2.381zm-3.42 0h1.712l-1.117-.745z"/><path d="m22.25 14h-2.756c-.778 0-1.452.501-1.676 1.247l-.859 2.862c-.16.533-.641.891-1.197.891h-7.524c-.556 0-1.037-.358-1.197-.891l-.859-2.861c-.224-.747-.897-1.248-1.676-1.248h-2.756c-.965 0-1.75.785-1.75 1.75v5.5c0 1.517 1.233 2.75 2.75 2.75h18.5c1.517 0 2.75-1.233 2.75-2.75v-5.5c0-.965-.785-1.75-1.75-1.75z"/><path d="m4 12c-.552 0-1-.448-1-1v-8c0-1.654 1.346-3 3-3h12c1.654 0 3 1.346 3 3v8c0 .552-.448 1-1 1s-1-.448-1-1v-8c0-.551-.449-1-1-1h-12c-.551 0-1 .449-1 1v8c0 .552-.448 1-1 1z"/></svg>',
  76. DOWNLOAD_ALL: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g><g><path d="m191 208c-1-5-6-8-10-8l-42 0 0-184c0-8-5-15-11-15l-42 0c-6 0-11 7-11 15l0 184-42 0c-4 0-8 3-9 8c-2 6-1 12 1 16l74 105c2 3 5 5 8 5s6-2 8-5l74-105c2-4 4-9 2-16z"></g></g><g><g><path d="m486.3 208c-1-5-6-8-10-8l-42 0 0-184c0-8-5-15-11-15l-43 0c-6 0-11 7-11 15l0 184-41 0c-4 0-8 3-9 8c-2 6-1 12 1 16l74 105c2 3 5 5 8 5s6-2 8-5l74-105c2-4 4-9 2-16z"></g></g><g><g><path d="m342.3 299c-1-5-6-8-10-8l-42 0 0-275c0-8-5-15-11-15l-42 0c-6 0-11 7-11 15l0 275-42 0c-4 0-8 3-9 8c-2 6-1 12 1 16l74 105c2 3 5 5 8 5s6-2 8-5l74-105c2-4 4-9 2-16z"></g></g><g><g><path d="m422.79 380.79l0 74.12-338.83 0 0-74.12-67.34 0 0 89.45c0 23 14.73 40.89 33.67 40.89l408.28 0c18.94 0 33.67-17.89 33.67-40.89l0-89.45-69.45 0z"/></g></g></svg>',
  77. CLOSE: '<svg width="26" height="26" xmlns="http://www.w3.org/2000/svg" id="bold" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="m14.828 12 5.303-5.303c.586-.586.586-1.536 0-2.121l-.707-.707c-.586-.586-1.536-.586-2.121 0l-5.303 5.303-5.303-5.304c-.586-.586-1.536-.586-2.121 0l-.708.707c-.586.586-.586 1.536 0 2.121l5.304 5.304-5.303 5.303c-.586.586-.586 1.536 0 2.121l.707.707c.586.586 1.536.586 2.121 0l5.303-5.303 5.303 5.303c.586.586 1.536.586 2.121 0l.707-.707c.586-.586.586-1.536 0-2.121z"></path></svg>'
  78. };
  79.  
  80. const checkInterval = 250;
  81. const style = GM_getResourceText("INTERNAL_CSS");
  82. const locale_manifest = JSON.parse(GM_getResourceText("LOCALE_MANIFEST"));
  83.  
  84. var GM_menuId = [];
  85. var locale = {};
  86. var lang = GM_getValue('lang') || navigator.language || navigator.userLanguage;
  87. var currentURL = location.href;
  88. var firstStarted = false;
  89. var pageLoaded = false;
  90.  
  91. var GL_registerEventList = [];
  92. var GL_logger = [];
  93. var GL_referrer;
  94. var GL_postPath;
  95. var GL_username;
  96. var GL_repeat;
  97. var GL_dataCache = {
  98. stories: {},
  99. highlights: {}
  100. };
  101. var GL_observer = new MutationObserver(function () {
  102. onReadyMyDW();
  103. });
  104.  
  105. initSettings();
  106. GM_addStyle(style);
  107. registerMenuCommand();
  108.  
  109. getTranslationText(lang).then((res) => {
  110. locale[lang] = res;
  111. repaintingTranslations();
  112. registerMenuCommand();
  113. checkingScriptUpdate(300);
  114. }).catch((err) => {
  115. registerMenuCommand();
  116. checkingScriptUpdate(300);
  117.  
  118. if (!lang.startsWith('en')) {
  119. console.error('getTranslationText catch error:', err);
  120. }
  121. });
  122.  
  123. logger('Script Loaded', GM_info.script.name, 'version:', GM_info.script.version);
  124.  
  125. // Main Timer
  126. // eslint-disable-next-line no-unused-vars
  127. var timer = setInterval(function () {
  128. // page loading or unnecessary route
  129. if ($('div#splash-screen').length > 0 && !$('div#splash-screen').is(':hidden') ||
  130. location.pathname.match(/^\/(explore(\/.*)?|challenge\/?.*|direct\/?.*|qr\/?|accounts\/.*|emails\/.*|language\/?.*?|your_activity\/?.*|settings\/help(\/.*)?$)$/ig) ||
  131. !location.hostname.startsWith('www.')
  132. ) {
  133. pageLoaded = false;
  134. return;
  135. }
  136.  
  137. if (currentURL != location.href || !firstStarted || !pageLoaded) {
  138. console.log('Main Timer', 'trigging');
  139.  
  140. clearInterval(GL_repeat);
  141. pageLoaded = false;
  142. firstStarted = true;
  143. currentURL = location.href;
  144. GL_observer.disconnect();
  145.  
  146. if (location.href.startsWith("https://www.instagram.com/p/") || location.pathname.match(/^\/(.*?)\/(p|reel)\//ig) || location.href.startsWith("https://www.instagram.com/reel/")) {
  147. GL_dataCache.stories = {};
  148. GL_dataCache.highlights = {};
  149.  
  150. logger('isDialog');
  151.  
  152. // This is a delayed function call that prevents the dialog element from appearing before the function is called.
  153. var dialogTimer = setInterval(() => {
  154. // body > div[id^="mount"] section nav + div > article << (mobile page in single post) >>
  155. // 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) >>
  156. // 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)
  157. // section:visible > main > div > div.xdt5ytf << (former CSS selector for single foreground post in page, non-floating) >>
  158. // <hr> is much more unique element than "div.xdt5ytf"
  159. if ($(`body > div[class]:not([id^="mount"]) div div[role="dialog"] article,
  160. section:visible > main > div > div > div > div > div > hr,
  161. body > div[id^="mount"] section nav + div > article,
  162. section:visible > main > div > div > article > div > div > div > div > div > header
  163. `).length > 0) {
  164. clearInterval(dialogTimer);
  165.  
  166. // This is to prevent the detection of the "Modify Video Volume" setting from being too slow.
  167. setTimeout(() => {
  168. onReadyMyDW(false);
  169. }, 15);
  170. }
  171. }, 100);
  172.  
  173. pageLoaded = true;
  174. }
  175.  
  176. if (location.href.startsWith("https://www.instagram.com/reels/")) {
  177. logger('isReels');
  178. setTimeout(() => {
  179. onReels(false);
  180. }, 150);
  181. pageLoaded = true;
  182. }
  183.  
  184. if (location.href.split("?")[0] == "https://www.instagram.com/") {
  185. GL_dataCache.stories = {};
  186. GL_dataCache.highlights = {};
  187.  
  188. let hasReferrer = GL_referrer?.match(/^\/(stories|highlights)\//ig) != null;
  189.  
  190. logger('isHomepage', hasReferrer);
  191. setTimeout(() => {
  192. onReadyMyDW(false, hasReferrer);
  193.  
  194. const element = $('div[id^="mount"] > div > div div > section > main div:not([class]):not([style]) > div > article')?.parent()[0];
  195. if (element) {
  196. GL_observer.observe(element, {
  197. childList: true
  198. });
  199. }
  200. }, 150);
  201.  
  202. pageLoaded = true;
  203. }
  204. // eslint-disable-next-line no-useless-escape
  205. if ($('header > *[class]:first-child img[alt]').length && location.pathname.match(/^(\/)([0-9A-Za-z\.\-_]+)\/?(tagged|reels|saved)?\/?$/ig) && !location.pathname.match(/^(\/explore\/?$|\/stories(\/.*)?$|\/p\/)/ig)) {
  206. logger('isProfile');
  207. setTimeout(() => {
  208. onProfileAvatar(false);
  209. }, 150);
  210. pageLoaded = true;
  211. }
  212.  
  213. if (!pageLoaded) {
  214. // Call Instagram stories function
  215. if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/highlights\/)/ig)) {
  216. GL_dataCache.highlights = {};
  217.  
  218. logger('isHighlightsStory');
  219.  
  220. onHighlightsStory(false);
  221. GL_repeat = setInterval(() => {
  222. onHighlightsStoryThumbnail(false);
  223. }, checkInterval);
  224.  
  225. if ($(".IG_DWHISTORY").length) {
  226. setTimeout(() => {
  227. if (USER_SETTING.SKIP_VIEW_STORY_CONFIRM) {
  228. var $viewStoryButton = $('div[id^="mount"] section:last-child > div > div div[role="button"]').filter(function () {
  229. return $(this).children().length === 0 && this.textContent.trim() !== "";
  230. });
  231. $viewStoryButton?.click();
  232. }
  233.  
  234. pageLoaded = true;
  235. }, 150);
  236. }
  237. }
  238. else if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/)/ig)) {
  239. logger('isStory');
  240.  
  241. /*
  242. *
  243. * $('body div[id^="mount"] > div > div > div[class]').length >= 2 &&
  244. * $('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 &&
  245. * $('body div[id^="mount"] > div > div > div[class]').last().find('svg > polyline + line').length > 0
  246. *
  247. */
  248. if ($('div[id^="mount"] section > div > a[href="/"]').length > 0) {
  249. $('.IG_DWSTORY').remove();
  250. $('.IG_DWNEWTAB').remove();
  251. if ($('.IG_DWSTORY_THUMBNAIL').length) {
  252. $('.IG_DWSTORY_THUMBNAIL').remove();
  253. }
  254.  
  255. onStory(false);
  256.  
  257. // Prevent buttons from being eaten by black holes sometimes
  258. setTimeout(() => {
  259. onStory(false);
  260. }, 150);
  261. }
  262.  
  263. if ($(".IG_DWSTORY").length) {
  264. setTimeout(() => {
  265. if (USER_SETTING.SKIP_VIEW_STORY_CONFIRM) {
  266. var $viewStoryButton = $('div[id^="mount"] section:last-child > div > div div[role="button"]').filter(function () {
  267. return $(this).children().length === 0 && this.textContent.trim() !== "";
  268. });
  269. $viewStoryButton?.click();
  270. }
  271.  
  272. pageLoaded = true;
  273. }, 150);
  274. }
  275. }
  276. else {
  277. pageLoaded = false;
  278. // Remove icons
  279. if ($('.IG_DWSTORY').length) {
  280. $('.IG_DWSTORY').remove();
  281. }
  282. if ($('.IG_DWSTORY_ALL').length) {
  283. $('.IG_DWSTORY_ALL').remove();
  284. }
  285. if ($('.IG_DWNEWTAB').length) {
  286. $('.IG_DWNEWTAB').remove();
  287. }
  288. if ($('.IG_DWSTORY_THUMBNAIL').length) {
  289. $('.IG_DWSTORY_THUMBNAIL').remove();
  290. }
  291.  
  292. if ($('.IG_DWHISTORY').length) {
  293. $('.IG_DWHISTORY').remove();
  294. }
  295. if ($('.IG_DWHISTORY_ALL').length) {
  296. $('.IG_DWHISTORY_ALL').remove();
  297. }
  298. if ($('.IG_DWHINEWTAB').length) {
  299. $('.IG_DWHINEWTAB').remove();
  300. }
  301. if ($('.IG_DWHISTORY_THUMBNAIL').length) {
  302. $('.IG_DWHISTORY_THUMBNAIL').remove();
  303. }
  304. }
  305. }
  306.  
  307. checkingScriptUpdate(300);
  308. GL_referrer = new URL(location.href).pathname;
  309. }
  310. }, checkInterval);
  311.  
  312. /**
  313. * onProfileAvatar
  314. * @description Trigger user avatar download event or button display event.
  315. *
  316. * @param {Boolean} isDownload - Check if it is a download operation
  317. * @return {void}
  318. */
  319. async function onProfileAvatar(isDownload) {
  320. if (isDownload) {
  321. updateLoadingBar(true);
  322.  
  323. let date = new Date().getTime();
  324. let timestamp = Math.floor(date / 1000);
  325. let username = location.pathname.replaceAll(/(reels|tagged)\/$/ig, '').split('/').filter(s => s.length > 0).at(-1);
  326. let userInfo = await getUserId(username);
  327.  
  328. try {
  329. let dataURL = await getUserHighSizeProfile(userInfo.user.pk);
  330. saveFiles(dataURL, username, "avatar", timestamp, 'jpg');
  331. }
  332. // eslint-disable-next-line no-unused-vars
  333. catch (err) {
  334. saveFiles(userInfo.user.profile_pic_url, username, "avatar", timestamp, 'jpg');
  335. }
  336.  
  337. updateLoadingBar(false);
  338. }
  339. else {
  340. // Add the profile download button
  341. if (!$('.IG_DWPROFILE').length) {
  342. let profileTimer = setInterval(() => {
  343. if ($('.IG_DWPROFILE').length) {
  344. clearInterval(profileTimer);
  345. return;
  346. }
  347.  
  348. $('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>`);
  349. $('header > *[class]:first-child img[alt][draggable]').parent().parent().css('position', 'relative');
  350. $('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>`);
  351. $('header > *[class]:first-child img[alt]:not([draggable])').parent().parent().parent().css('position', 'relative');
  352. }, 150);
  353. }
  354. }
  355. }
  356.  
  357. /**
  358. * onHighlightsStoryAll
  359. * @description Trigger user's highlight all download event.
  360. *
  361. * @return {void}
  362. */
  363. async function onHighlightsStoryAll() {
  364. updateLoadingBar(true);
  365.  
  366. let date = new Date().getTime();
  367. let timestamp = Math.floor(date / 1000);
  368. let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1);
  369. let highStories = await getHighlightStories(highlightId);
  370. let username = highStories.data.reels_media[0].owner.username;
  371.  
  372. let complete = 0;
  373. setDownloadProgress(complete, highStories.data.reels_media[0].items.length);
  374.  
  375. highStories.data.reels_media[0].items.forEach((item, idx) => {
  376. setTimeout(() => {
  377. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  378. timestamp = item.taken_at_timestamp;
  379. }
  380.  
  381. item.display_resources.sort(function (a, b) {
  382. if (a.config_width < b.config_width) return 1;
  383. if (a.config_width > b.config_width) return -1;
  384. return 0;
  385. });
  386.  
  387. if (item.is_video) {
  388. saveFiles(item.video_resources[0].src, username, "stories", timestamp, 'mp4', item.id).then(() => {
  389. setDownloadProgress(++complete, highStories.data.reels_media[0].items.length);
  390. });
  391. }
  392. else {
  393. saveFiles(item.display_resources[0].src, username, "stories", timestamp, 'jpg', item.id).then(() => {
  394. setDownloadProgress(++complete, highStories.data.reels_media[0].items.length);
  395. });
  396. }
  397. }, 100 * idx);
  398. });
  399. }
  400.  
  401. /**
  402. * onHighlightsStory
  403. * @description Trigger user's highlight download event or button display event.
  404. *
  405. * @param {Boolean} isDownload - Check if it is a download operation
  406. * @param {Boolean} isPreview - Check if it is need to open new tab
  407. * @return {void}
  408. */
  409. async function onHighlightsStory(isDownload, isPreview) {
  410. var username = $('body > div section:visible a[href^="/"]').filter(function () {
  411. return $(this).attr('href').split('/').filter(e => e.length > 0).length === 1
  412. }).first().attr('href').split('/').filter(e => e.length > 0).at(0);
  413.  
  414. if (isDownload) {
  415. let date = new Date().getTime();
  416. let timestamp = Math.floor(date / 1000);
  417. let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1);
  418. let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length ||
  419. $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length ||
  420. $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length;
  421. let target = 0;
  422.  
  423. updateLoadingBar(true);
  424.  
  425. if (GL_dataCache.highlights[highlightId]) {
  426. logger('Fetch from memory cache:', highlightId);
  427.  
  428. let totIndex = GL_dataCache.highlights[highlightId].data.reels_media[0].items.length;
  429. username = GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username;
  430. target = GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex - nowIndex];
  431. }
  432. else {
  433. let highStories = await getHighlightStories(highlightId);
  434. let totIndex = highStories.data.reels_media[0].items.length;
  435. username = highStories.data.reels_media[0].owner.username;
  436. target = highStories.data.reels_media[0].items[totIndex - nowIndex];
  437.  
  438. GL_dataCache.highlights[highlightId] = highStories;
  439. }
  440.  
  441. logger('onHighlightsStory', highlightId, GL_dataCache.highlights[highlightId]);
  442.  
  443.  
  444. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  445. timestamp = target.taken_at_timestamp;
  446. }
  447.  
  448. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LIMIT) {
  449. let result = await getMediaInfo(target.id);
  450.  
  451. if (result.status === 'ok') {
  452. if (result.items[0].video_versions) {
  453. if (isPreview) {
  454. openNewTab(result.items[0].video_versions[0].url);
  455. }
  456. else {
  457. saveFiles(result.items[0].video_versions[0].url, username, "highlights", timestamp, 'mp4', highlightId);
  458. }
  459. }
  460. else {
  461. if (isPreview) {
  462. openNewTab(result.items[0].image_versions2.candidates[0].url);
  463. }
  464. else {
  465. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "highlights", timestamp, 'jpg', highlightId);
  466. }
  467. }
  468. }
  469. else {
  470. if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) {
  471. delete GL_dataCache.highlights[highlightId];
  472. TEMP_FETCH_RATE_LIMIT = true;
  473.  
  474. onHighlightsStory(true, isPreview);
  475. }
  476. else {
  477. alert('Fetch failed from Media API. API response message: ' + result.message);
  478. }
  479.  
  480. logger(result);
  481. }
  482. }
  483. else {
  484. if (target.is_video) {
  485. if (isPreview) {
  486. openNewTab(target.video_resources.at(-1).src, username);
  487. }
  488. else {
  489. saveFiles(target.video_resources.at(-1).src, username, "highlights", timestamp, 'mp4', highlightId);
  490. }
  491. }
  492. else {
  493. if (isPreview) {
  494. openNewTab(target.display_resources.at(-1).src, username);
  495. }
  496. else {
  497. saveFiles(target.display_resources.at(-1).src, username, "highlights", timestamp, 'jpg', highlightId);
  498. }
  499. }
  500.  
  501. TEMP_FETCH_RATE_LIMIT = false;
  502. }
  503.  
  504. updateLoadingBar(false);
  505. }
  506. else {
  507. // Add the stories download button
  508. if (!$('.IG_DWHISTORY').length) {
  509. let $element = null;
  510.  
  511. // Default detecter (section layout mode)
  512. if ($('body > div section._ac0a').length > 0) {
  513. $element = $('body > div section:visible._ac0a');
  514. }
  515. else {
  516. $element = $('body > div section:visible > div > div[style]:not([class])');
  517. $element.css('position', 'relative');
  518. }
  519.  
  520. // Detecter for div layout mode
  521. if ($element.length === 0) {
  522. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  523. let nowSize = 0;
  524.  
  525. $$element.each(function () {
  526. if ($(this).width() > nowSize) {
  527. nowSize = $(this).width();
  528. $element = $(this).children('div').first();
  529. }
  530. });
  531. }
  532.  
  533.  
  534. if ($element != null) {
  535. //$element.css('position','relative');
  536. $element.append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DWHISTORY">${SVG.DOWNLOAD}</div>`);
  537. $element.append(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_DWHINEWTAB">${SVG.NEW_TAB}</div>`);
  538.  
  539. let $header = getStoryProgress(username);
  540. if ($header.length > 1) {
  541. $element.append(`<div data-ih-locale-title="DW_ALL" title="${_i18n("DW_ALL")}" class="IG_DWHISTORY_ALL">${SVG.DOWNLOAD_ALL}</div>`);
  542. }
  543.  
  544. //// Modify video volume
  545. //if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  546. // $element.find('video').each(function(){
  547. // $(this).on('play playing', function(){
  548. // if(!$(this).data('modify')){
  549. // $(this).attr('data-modify', true);
  550. // this.volume = VIDEO_VOLUME;
  551. // logger('(highlight) Added video event listener #modify');
  552. // }
  553. // });
  554. // });
  555. //}
  556.  
  557. // Make sure to first remove thumbnail button if still exists and highlight is a picture
  558. $element.find('img[referrerpolicy]').each(function () {
  559. $(this).on('load', function () {
  560. if (!$(this).data('remove-thumbnail')) {
  561. if ($element.find('.IG_DWHISTORY_THUMBNAIL').length === 0) {
  562. $(this).attr('data-remove-thumbnail', true);
  563. $('.IG_DWHISTORY_THUMBNAIL').remove();
  564. logger('(highlight) Manually removing thumbnail button');
  565. }
  566. else {
  567. $(this).attr('data-remove-thumbnail', true);
  568. logger('(highlight) Thumbnail button is not present for this picture');
  569. }
  570. }
  571. });
  572. });
  573.  
  574. // Try to use event listener 'timeupdate' in order to detect if highlight is a video
  575. //$element.find('video').each(function(){
  576. // $(this).on('timeupdate',function(){
  577. // if(!$(this).data('modify-thumbnail')){
  578. // if($element.find('.IG_DWHISTORY_THUMBNAIL').length === 0){
  579. // $(this).attr('data-modify-thumbnail', true);
  580. // onHighlightsStoryThumbnail(false);
  581. // logger('(highlight) Manually inserting thumbnail button');
  582. // }
  583. // else{
  584. // $(this).attr('data-modify-thumbnail', true);
  585. // logger('(highlight) Thumbnail button already inserted');
  586. // }
  587. // }
  588. // });
  589. //});
  590. }
  591. }
  592. }
  593. }
  594.  
  595. /**
  596. * onHighlightsStoryThumbnail
  597. * @description Trigger user's highlight video thumbnail download event or button display event.
  598. *
  599. * @param {Boolean} isDownload - Check if it is a download operation
  600. * @return {void}
  601. */
  602. async function onHighlightsStoryThumbnail(isDownload) {
  603. if (isDownload) {
  604. let date = new Date().getTime();
  605. let timestamp = Math.floor(date / 1000);
  606. let highlightId = location.href.replace(/\/$/ig, '').split('/').at(-1);
  607. let username = "";
  608. let nowIndex = $("body > div section._ac0a header._ac0k > ._ac3r ._ac3n ._ac3p[style]").length ||
  609. $('body > div section:visible > div > div:not([class]) > div > div div.x1ned7t2.x78zum5 div.x1caxmr6').length ||
  610. $('body > div div:not([hidden]) section:visible > div div[style]:not([class]) > div').find('div div.x1ned7t2.x78zum5 div.x1caxmr6').length;
  611. let target = "";
  612.  
  613. updateLoadingBar(true);
  614.  
  615. if (GL_dataCache.highlights[highlightId]) {
  616. logger('Fetch from memory cache:', highlightId);
  617.  
  618. let totIndex = GL_dataCache.highlights[highlightId].data.reels_media[0].items.length;
  619. username = GL_dataCache.highlights[highlightId].data.reels_media[0].owner.username;
  620. target = GL_dataCache.highlights[highlightId].data.reels_media[0].items[totIndex - nowIndex];
  621. }
  622. else {
  623. let highStories = await getHighlightStories(highlightId);
  624. let totIndex = highStories.data.reels_media[0].items.length;
  625. username = highStories.data.reels_media[0].owner.username;
  626. target = highStories.data.reels_media[0].items[totIndex - nowIndex];
  627.  
  628. GL_dataCache.highlights[highlightId] = highStories;
  629. }
  630.  
  631. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  632. timestamp = target.taken_at_timestamp;
  633. }
  634.  
  635. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LIMIT) {
  636. let result = await getMediaInfo(target.id);
  637.  
  638. if (result.status === 'ok') {
  639. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "highlights", timestamp, 'jpg', highlightId);
  640. }
  641. else {
  642. if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) {
  643. delete GL_dataCache.highlights[highlightId];
  644. TEMP_FETCH_RATE_LIMIT = true;
  645.  
  646. onHighlightsStoryThumbnail(true);
  647. }
  648. else {
  649. alert('Fetch failed from Media API. API response message: ' + result.message);
  650. }
  651.  
  652. logger(result);
  653. }
  654. }
  655. else {
  656. saveFiles(target.display_resources.at(-1).src, username, "highlights", timestamp, 'jpg', highlightId);
  657. TEMP_FETCH_RATE_LIMIT = false;
  658. }
  659.  
  660. updateLoadingBar(false);
  661. }
  662. else {
  663. if ($('body > div section video.xh8yej3').length) {
  664. // Add the stories thumbnail download button
  665. if (!$('.IG_DWHISTORY_THUMBNAIL').length) {
  666. let $element = null;
  667.  
  668. // Default detecter (section layout mode)
  669. if ($('body > div section._ac0a').length > 0) {
  670. $element = $('body > div section:visible._ac0a');
  671. }
  672. else {
  673. $element = $('body > div section:visible > div > div[style]:not([class])');
  674. $element.css('position', 'relative');
  675. }
  676.  
  677. // Detecter for div layout mode
  678. if ($element.length === 0) {
  679. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  680. let nowSize = 0;
  681.  
  682. $$element.each(function () {
  683. if ($(this).width() > nowSize) {
  684. nowSize = $(this).width();
  685. $element = $(this).children('div').first();
  686. }
  687. });
  688. }
  689.  
  690. if ($element != null) {
  691. $element.append(`<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="IG_DWHISTORY_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  692. }
  693. }
  694. }
  695. else {
  696. $('.IG_DWHISTORY_THUMBNAIL').remove();
  697. }
  698. }
  699. }
  700.  
  701. /**
  702. * onStoryAll
  703. * @description Trigger user's story all download event.
  704. *
  705. * @return {void}
  706. */
  707. async function onStoryAll() {
  708. updateLoadingBar(true);
  709.  
  710. let date = new Date().getTime();
  711. let timestamp = Math.floor(date / 1000);
  712. let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split("/").filter(s => s.length > 0).at(1);
  713.  
  714. let userInfo = await getUserId(username);
  715. let userId = userInfo.user.pk;
  716. let stories = await getStories(userId);
  717.  
  718. let complete = 0;
  719. setDownloadProgress(complete, stories.data.reels_media[0].items.length);
  720.  
  721. stories.data.reels_media[0].items.forEach((item, idx) => {
  722. setTimeout(() => {
  723. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  724. timestamp = item.taken_at_timestamp;
  725. }
  726.  
  727. item.display_resources.sort(function (a, b) {
  728. if (a.config_width < b.config_width) return 1;
  729. if (a.config_width > b.config_width) return -1;
  730. return 0;
  731. });
  732.  
  733. if (item.is_video) {
  734. saveFiles(item.video_resources[0].src, username, "stories", timestamp, 'mp4', item.id).then(() => {
  735. setDownloadProgress(++complete, stories.data.reels_media[0].items.length);
  736. });
  737. }
  738. else {
  739. saveFiles(item.display_resources[0].src, username, "stories", timestamp, 'jpg', item.id).then(() => {
  740. setDownloadProgress(++complete, stories.data.reels_media[0].items.length);
  741. });
  742. }
  743. }, 100 * idx);
  744. });
  745. }
  746.  
  747. /**
  748. * onStory
  749. * @description Trigger user's story download event or button display event.
  750. *
  751. * @param {Boolean} isDownload - Check if it is a download operation
  752. * @param {Boolean} isForce - Check if downloading directly from API instead of cache
  753. * @param {Boolean} isPreview - Check if it is need to open new tab
  754. * @return {void}
  755. */
  756. async function onStory(isDownload, isForce, isPreview) {
  757. var username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split("/").filter(s => s.length > 0).at(1);
  758. if (isDownload) {
  759. let date = new Date().getTime();
  760. let timestamp = Math.floor(date / 1000);
  761.  
  762. updateLoadingBar(true);
  763. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LIMIT) {
  764. let mediaId = null;
  765.  
  766. let userInfo = await getUserId(username);
  767. let userId = userInfo.user.pk;
  768. let stories = await getStories(userId);
  769. let urlID = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  770.  
  771. /*
  772. let latest_reel_media = stories.data.reels_media[0].latest_reel_media;
  773. let last_seen = stories.data.reels_media[0].seen;
  774. logger(stories);
  775.  
  776. if(urlID == null){
  777. mediaId = stories.data.reels_media[0].items.filter(function(item, index){
  778. return item.taken_at_timestamp === last_seen && item.taken_at_timestamp !== latest_reel_media || last_seen === latest_reel_media && index === 0;
  779. })?.at(0)?.id;
  780. logger('nula', mediaId);
  781. }
  782. else{
  783. stories.data.reels_media[0].items.forEach(item => {
  784. if(item.id == urlID){
  785. mediaId = item.id;
  786. }
  787. });
  788. }
  789. */
  790.  
  791. stories.data.reels_media[0].items.forEach(item => {
  792. if (item.id == urlID) {
  793. mediaId = item.id;
  794. }
  795. });
  796.  
  797. if (mediaId == null) {
  798. let $header = getStoryProgress(username);
  799.  
  800. $header.each(function (index) {
  801. if ($(this).children().length > 0) {
  802. mediaId = stories.data.reels_media[0].items[index].id;
  803. }
  804. });
  805. }
  806.  
  807. if (mediaId == null) {
  808. // appear in from profile page to story page
  809. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  810. if ($(this).hasClass('x1lix1fw')) {
  811. if ($(this).children().length > 0) {
  812. mediaId = stories.data.reels_media[0].items[index].id;
  813. }
  814. }
  815. });
  816.  
  817. // appear in from home page to story page
  818. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  819. if ($(this).children().hasClass('_ac3q')) {
  820. mediaId = stories.data.reels_media[0].items[index].id;
  821. }
  822. });
  823. }
  824.  
  825. if (mediaId == null) {
  826. mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  827. }
  828.  
  829. let result = await getMediaInfo(mediaId);
  830.  
  831. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  832. timestamp = result.items[0].taken_at;
  833. }
  834.  
  835. if (result.status === 'ok') {
  836. if (result.items[0].video_versions) {
  837. if (isPreview) {
  838. openNewTab(result.items[0].video_versions[0].url);
  839. }
  840. else {
  841. saveFiles(result.items[0].video_versions[0].url, username, "stories", timestamp, 'mp4', mediaId);
  842. }
  843. }
  844. else {
  845. if (isPreview) {
  846. openNewTab(result.items[0].image_versions2.candidates[0].url);
  847. }
  848. else {
  849. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "stories", timestamp, 'jpg', mediaId);
  850. }
  851. }
  852. }
  853. else {
  854. if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) {
  855. TEMP_FETCH_RATE_LIMIT = true;
  856. onStory(isDownload, isForce, isPreview);
  857. }
  858. else {
  859. alert('Fetch failed from Media API. API response message: ' + result.message);
  860. }
  861. logger(result);
  862. }
  863.  
  864. updateLoadingBar(false);
  865. return;
  866. }
  867.  
  868. if ($('body > div section:visible video[playsinline]').length > 0) {
  869. // Download stories if it is video
  870. let type = "mp4";
  871. let videoURL = "";
  872. let targetURL = location.pathname.replace(/\/$/ig, '').split("/").at(-1);
  873. let mediaId = null;
  874.  
  875. if (GL_dataCache.stories[username] && !isForce) {
  876. logger('Fetch from memory cache:', username);
  877. GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => {
  878. if (item.id == targetURL) {
  879. videoURL = item.video_resources[0].src;
  880. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  881. timestamp = item.taken_at_timestamp;
  882. mediaId = item.id;
  883. }
  884. }
  885. });
  886.  
  887. if (videoURL.length == 0) {
  888. logger('Memory cache not found, try fetch from API:', username);
  889. onStory(true, true);
  890. return;
  891. }
  892. }
  893. else {
  894. let userInfo = await getUserId(username);
  895. let userId = userInfo.user.pk;
  896. let stories = await getStories(userId);
  897.  
  898. stories.data.reels_media[0].items.forEach(item => {
  899. if (item.id == targetURL) {
  900. videoURL = item.video_resources[0].src;
  901. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  902. timestamp = item.taken_at_timestamp;
  903. mediaId = item.id;
  904. }
  905. }
  906. });
  907.  
  908. // GitHub issue #4: thinkpad4
  909. if (videoURL.length == 0) {
  910.  
  911. let $header = getStoryProgress(username);
  912.  
  913. $header.each(function (index) {
  914. if ($(this).children().length > 0) {
  915. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  916. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  917. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  918. mediaId = stories.data.reels_media[0].items[index].id;
  919. }
  920. }
  921. });
  922.  
  923.  
  924. if (videoURL.length == 0) {
  925. // appear in from profile page to story page
  926. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  927. if ($(this).hasClass('x1lix1fw')) {
  928. if ($(this).children().length > 0) {
  929. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  930. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  931. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  932. mediaId = stories.data.reels_media[0].items[index].id;
  933. }
  934. }
  935. }
  936. });
  937.  
  938. // appear in from home page to story page
  939. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  940. if ($(this).children().hasClass('_ac3q')) {
  941. videoURL = stories.data.reels_media[0].items[index].video_resources[0].src;
  942. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  943. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  944. mediaId = stories.data.reels_media[0].items[index].id;
  945. }
  946. }
  947. });
  948. }
  949. }
  950.  
  951. GL_dataCache.stories[username] = stories;
  952. }
  953.  
  954. if (videoURL.length == 0) {
  955. alert(_i18n("NO_VID_URL"));
  956. }
  957. else {
  958. if (isPreview) {
  959. openNewTab(videoURL);
  960. }
  961. else {
  962. saveFiles(videoURL, username, "stories", timestamp, type, mediaId);
  963. }
  964. }
  965. }
  966. else {
  967. // Download stories if it is image
  968. let srcset = $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').attr('srcset')?.split(',')[0]?.split(' ')[0];
  969. let link = (srcset) ? srcset : $('body > div section:visible img[referrerpolicy][class], body > div section:visible img[crossorigin][class]:not([alt])').filter(function () {
  970. return $(this).parents('a').length === 0 && $(this).width() === $(this).parent().width();
  971. }).attr('src');
  972.  
  973. if (!link) {
  974. // _aa63 mean stories picture in stories page (not avatar)
  975. let $element = $('body > div section:visible img._aa63');
  976. link = ($element.attr('srcset')) ? $element.attr('srcset')?.split(',')[0]?.split(' ')[0] : $element.attr('src');
  977. }
  978.  
  979. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  980. timestamp = new Date($('body > div section:visible time[datetime][class]').first().attr('datetime')).getTime();
  981. }
  982.  
  983. let downloadLink = link;
  984. let type = 'jpg';
  985.  
  986. if (isPreview) {
  987. openNewTab(downloadLink);
  988. }
  989. else {
  990. saveFiles(downloadLink, username, "stories", timestamp, type, getStoryId(downloadLink) ?? "");
  991. }
  992. }
  993.  
  994. TEMP_FETCH_RATE_LIMIT = false;
  995. updateLoadingBar(false);
  996. }
  997. else {
  998. // Add the stories download button
  999. if (!$('.IG_DWSTORY').length) {
  1000. GL_dataCache.stories = {};
  1001. let $element = null;
  1002. // Default detecter (section layout mode)
  1003. if ($('body > div section._ac0a').length > 0) {
  1004. $element = $('body > div section:visible._ac0a');
  1005. }
  1006. // detecter (single story layout mode)
  1007. else {
  1008. $element = $('body > div section:visible > div > div[style]:not([class])');
  1009. $element.css('position', 'relative');
  1010. }
  1011.  
  1012.  
  1013. if ($element.length === 0) {
  1014. $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div > div[style]:not([class])');
  1015. $element.css('position', 'relative');
  1016. }
  1017.  
  1018. if ($element.length === 0) {
  1019. $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"])');
  1020. $element.css('position', 'relative');
  1021. }
  1022.  
  1023.  
  1024. // Detecter for div layout mode
  1025. if ($element.length === 0) {
  1026. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  1027. let nowSize = 0;
  1028.  
  1029. $$element.each(function () {
  1030. if ($(this).width() > nowSize) {
  1031. nowSize = $(this).width();
  1032. $element = $(this).children('div').first();
  1033. }
  1034. });
  1035. }
  1036.  
  1037.  
  1038. if ($element != null) {
  1039. $element.first().css('position', 'relative');
  1040. $element.first().append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_DWSTORY">${SVG.DOWNLOAD}</div>`);
  1041. $element.first().append(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_DWNEWTAB">${SVG.NEW_TAB}</div>`);
  1042.  
  1043. let $header = getStoryProgress(username);
  1044. if ($header.length > 1) {
  1045. $element.first().append(`<div data-ih-locale-title="DW_ALL" title="${_i18n("DW_ALL")}" class="IG_DWSTORY_ALL">${SVG.DOWNLOAD_ALL}</div>`);
  1046. }
  1047.  
  1048. // Modify video volume
  1049. //if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  1050. // $element.find('video').each(function(){
  1051. // $(this).on('play playing', function(){
  1052. // if(!$(this).data('modify')){
  1053. // $(this).attr('data-modify', true);
  1054. // this.volume = VIDEO_VOLUME;
  1055. // logger('(story) Added video event listener #modify');
  1056. // }
  1057. // });
  1058. // });
  1059. //}
  1060.  
  1061. // Make sure to first remove thumbnail button if still exists and story is a picture
  1062. $element.find('img[referrerpolicy]').each(function () {
  1063. $(this).on('load', function () {
  1064. if (!$(this).data('remove-thumbnail')) {
  1065. if ($element.find('.IG_DWSTORY_THUMBNAIL').length === 0) {
  1066. $(this).attr('data-remove-thumbnail', true);
  1067. $('.IG_DWSTORY_THUMBNAIL').remove();
  1068. logger('(story) Manually removing thumbnail button');
  1069. }
  1070. else {
  1071. $(this).attr('data-remove-thumbnail', true);
  1072. logger('(story) Thumbnail button is not present for this picture');
  1073. }
  1074. }
  1075. });
  1076. });
  1077.  
  1078. // Try to use event listener 'timeupdate' in order to detect if story is a video
  1079. //$element.find('video').each(function(){
  1080. // $(this).on('timeupdate',function(){
  1081. // if(!$(this).data('modify-thumbnail')){
  1082. // if($element.find('.IG_DWSTORY_THUMBNAIL').length === 0){
  1083. // $(this).attr('data-modify-thumbnail', true);
  1084. // onStoryThumbnail(false);
  1085. // logger('(story) Manually inserting thumbnail button');
  1086. // }
  1087. // else{
  1088. // $(this).attr('data-modify-thumbnail', true);
  1089. // logger('(story) Thumbnail button already inserted');
  1090. // }
  1091. // }
  1092. // });
  1093. //});
  1094. }
  1095. }
  1096. }
  1097. }
  1098.  
  1099. /**
  1100. * onStoryThumbnail
  1101. * @description Trigger user's story video thumbnail download event or button display event.
  1102. *
  1103. * @param {Boolean} isDownload - Check if it is a download operation
  1104. * @param {Boolean} isForce - Check if downloading directly from API instead of cache
  1105. * @return {void}
  1106. */
  1107. async function onStoryThumbnail(isDownload, isForce) {
  1108. if (isDownload) {
  1109. // Download stories if it is video
  1110. let date = new Date().getTime();
  1111. let timestamp = Math.floor(date / 1000);
  1112. let type = 'jpg';
  1113. let username = $("body > div section._ac0a header._ac0k ._ac0l a + div a").first().text() || location.pathname.split('/').at(2);
  1114. // Download thumbnail
  1115. let targetURL = location.pathname.replace(/\/$/ig, '').split("/").at(-1);
  1116. let videoThumbnailURL = "";
  1117. let mediaId = null;
  1118.  
  1119. updateLoadingBar(true);
  1120.  
  1121. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && !TEMP_FETCH_RATE_LIMIT) {
  1122. let userInfo = await getUserId(username);
  1123. let userId = userInfo.user.pk;
  1124. let stories = await getStories(userId);
  1125. let urlID = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  1126.  
  1127. stories.data.reels_media[0].items.forEach(item => {
  1128. if (item.id == urlID) {
  1129. mediaId = item.id;
  1130. }
  1131. });
  1132.  
  1133. if (mediaId == null) {
  1134. let $header = getStoryProgress(username);
  1135.  
  1136. $header.each(function (index) {
  1137. if ($(this).children().length > 0) {
  1138. mediaId = stories.data.reels_media[0].items[index].id;
  1139. }
  1140. });
  1141. }
  1142.  
  1143. if (mediaId == null) {
  1144. // appear in from profile page to story page
  1145. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  1146. if ($(this).hasClass('x1lix1fw')) {
  1147. if ($(this).children().length > 0) {
  1148. mediaId = stories.data.reels_media[0].items[index].id;
  1149. }
  1150. }
  1151. });
  1152.  
  1153. // appear in from home page to story page
  1154. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  1155. if ($(this).children().hasClass('_ac3q')) {
  1156. mediaId = stories.data.reels_media[0].items[index].id;
  1157. }
  1158. });
  1159. }
  1160.  
  1161. if (mediaId == null) {
  1162. mediaId = location.pathname.split('/').filter(s => s.length > 0 && s.match(/^([0-9]{10,})$/)).at(-1);
  1163. }
  1164.  
  1165. let result = await getMediaInfo(mediaId);
  1166.  
  1167. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1168. timestamp = result.items[0].taken_at;
  1169. }
  1170.  
  1171. if (result.status === 'ok') {
  1172. saveFiles(result.items[0].image_versions2.candidates[0].url, username, "stories", timestamp, 'jpg', mediaId);
  1173.  
  1174. }
  1175. else {
  1176. if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) {
  1177. TEMP_FETCH_RATE_LIMIT = true;
  1178. onStoryThumbnail(true, isForce);
  1179. }
  1180. else {
  1181. alert('Fetch failed from Media API. API response message: ' + result.message);
  1182. }
  1183.  
  1184. logger(result);
  1185. }
  1186.  
  1187. updateLoadingBar(false);
  1188. return;
  1189. }
  1190.  
  1191. if (GL_dataCache.stories[username] && !isForce) {
  1192. logger('Fetch from memory cache:', username);
  1193. GL_dataCache.stories[username].data.reels_media[0].items.forEach(item => {
  1194. if (item.id == targetURL) {
  1195. videoThumbnailURL = item.display_url;
  1196. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1197. timestamp = item.taken_at_timestamp;
  1198. mediaId = item.id;
  1199. }
  1200. }
  1201. });
  1202.  
  1203. if (videoThumbnailURL.length == 0) {
  1204. logger('Memory cache not found, try fetch from API:', username);
  1205. onStoryThumbnail(true, true);
  1206. return;
  1207. }
  1208. }
  1209. else {
  1210. let userInfo = await getUserId(username);
  1211. let userId = userInfo.user.pk;
  1212. let stories = await getStories(userId);
  1213.  
  1214. stories.data.reels_media[0].items.forEach(item => {
  1215. if (item.id == targetURL) {
  1216. videoThumbnailURL = item.display_url;
  1217. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1218. timestamp = item.taken_at_timestamp;
  1219. mediaId = item.id;
  1220. }
  1221. }
  1222. });
  1223.  
  1224. // GitHub issue #4: thinkpad4
  1225. if (videoThumbnailURL.length == 0) {
  1226. let $header = getStoryProgress(username);
  1227.  
  1228. $header.each(function (index) {
  1229. if ($(this).children().length > 0) {
  1230. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  1231. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1232. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  1233. mediaId = stories.data.reels_media[0].items[index].id;
  1234. }
  1235. }
  1236. });
  1237.  
  1238. if (videoThumbnailURL.length == 0) {
  1239. // appear in from profile page to story page
  1240. $('body > div section:visible div.x1ned7t2.x78zum5 > div').each(function (index) {
  1241. if ($(this).hasClass('x1lix1fw')) {
  1242. if ($(this).children().length > 0) {
  1243. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  1244. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1245. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  1246. mediaId = stories.data.reels_media[0].items[index].id;
  1247. }
  1248. }
  1249. }
  1250. });
  1251.  
  1252. // appear in from home page to story page
  1253. $('body > div section:visible ._ac0k > ._ac3r > div').each(function (index) {
  1254. if ($(this).children().hasClass('_ac3q')) {
  1255. videoThumbnailURL = stories.data.reels_media[0].items[index].display_url;
  1256. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1257. timestamp = stories.data.reels_media[0].items[index].taken_at_timestamp;
  1258. mediaId = stories.data.reels_media[0].items[index].id;
  1259. }
  1260. }
  1261. });
  1262. }
  1263. }
  1264. }
  1265.  
  1266. saveFiles(videoThumbnailURL, username, "thumbnail", timestamp, type, mediaId);
  1267. TEMP_FETCH_RATE_LIMIT = false;
  1268. updateLoadingBar(false);
  1269. }
  1270. else {
  1271. if ($('body > div div.IG_DWSTORY').parent().find('video[class]').length) {
  1272. // Add the stories download button
  1273. let $element = null;
  1274. // Default detecter (section layout mode)
  1275. if ($('body > div section._ac0a').length > 0) {
  1276. $element = $('body > div section:visible._ac0a');
  1277. }
  1278. // detecter (single story layout mode)
  1279. else {
  1280. $element = $('body > div section:visible > div > div[style]:not([class])');
  1281. $element.css('position', 'relative');
  1282. }
  1283.  
  1284. if ($element.length === 0) {
  1285. $element = $('div[id^="mount"] section > div > a[href="/"]').parent().parent().parent().find('section:visible > div > div[style]:not([class])');
  1286. $element.css('position', 'relative');
  1287. }
  1288.  
  1289. if ($element.length === 0) {
  1290. $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"])');
  1291. $element.css('position', 'relative');
  1292. }
  1293.  
  1294. // Detecter for div layout mode
  1295. if ($element.length === 0) {
  1296. let $$element = $('body > div div:not([hidden]) section:visible > div div[class][style] > div[style]:not([class])');
  1297. let nowSize = 0;
  1298.  
  1299. $$element.each(function () {
  1300. if ($(this).width() > nowSize) {
  1301. nowSize = $(this).width();
  1302. $element = $(this).children('div').first();
  1303. }
  1304. });
  1305. }
  1306.  
  1307.  
  1308. if ($element != null) {
  1309. $element.first().css('position', 'relative');
  1310. $element.first().append(`<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="IG_DWSTORY_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  1311. }
  1312.  
  1313. }
  1314. }
  1315. }
  1316.  
  1317. /**
  1318. * onReels
  1319. * @description Trigger user's reels download event or button display event.
  1320. *
  1321. * @param {Boolean} isDownload - Check if it is a download operation
  1322. * @param {Boolean} isVideo - Check if reel is a video element
  1323. * @param {Boolean} isPreview - Check if it is need to open new tab
  1324. * @return {void}
  1325. */
  1326. async function onReels(isDownload, isVideo, isPreview) {
  1327. if (isDownload) {
  1328. updateLoadingBar(true);
  1329.  
  1330. let reelsPath = location.href.split('?').at(0).split('instagram.com/reels/').at(-1).replaceAll('/', '');
  1331. let result = await getBlobMedia(reelsPath);
  1332. let media = filterResourceData(result.data);
  1333.  
  1334. let timestamp = new Date().getTime();
  1335.  
  1336. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  1337. if (result.type === 'query_hash') {
  1338. timestamp = media.shortcode_media.taken_at_timestamp;
  1339. }
  1340. else {
  1341. timestamp = media.taken_at;
  1342. }
  1343. }
  1344.  
  1345. if (result.type === 'query_hash') {
  1346. if (isVideo && media.shortcode_media.is_video) {
  1347. if (isPreview) {
  1348. openNewTab(media.shortcode_media.video_url);
  1349. }
  1350. else {
  1351. let type = 'mp4';
  1352. saveFiles(media.shortcode_media.video_url, media.shortcode_media.owner.username, "reels", timestamp, type, reelsPath);
  1353. }
  1354. }
  1355. else {
  1356. if (isPreview) {
  1357. openNewTab(media.shortcode_media.display_resources.at(-1).src);
  1358. }
  1359. else {
  1360. let type = 'jpg';
  1361. saveFiles(media.shortcode_media.display_resources.at(-1).src, media.shortcode_media.owner.username, "reels", timestamp, type, reelsPath);
  1362. }
  1363. }
  1364. }
  1365. else {
  1366. if (isVideo && media.video_versions != null) {
  1367. if (isPreview) {
  1368. openNewTab(media.video_versions[0].url);
  1369. }
  1370. else {
  1371. let type = 'mp4';
  1372. saveFiles(media.video_versions[0].url, media.owner.username, "reels", timestamp, type, reelsPath);
  1373. }
  1374. }
  1375. else {
  1376. if (isPreview) {
  1377. openNewTab(media.image_versions2.candidates[0].url);
  1378. }
  1379. else {
  1380. let type = 'jpg';
  1381. saveFiles(media.image_versions2.candidates[0].url, media.owner.username, "reels", timestamp, type, reelsPath);
  1382. }
  1383. }
  1384. }
  1385.  
  1386. updateLoadingBar(false);
  1387. }
  1388. else {
  1389. //$('.IG_REELS_THUMBNAIL, .IG_REELS').remove();
  1390. var timer = setInterval(() => {
  1391. if ($('section > main[role="main"] > div div.x1qjc9v5 video').length > 0) {
  1392. clearInterval(timer);
  1393.  
  1394. if (USER_SETTING.SCROLL_BUTTON) {
  1395. $('#scrollWrapper').remove();
  1396. $('section > main[role="main"]').append('<section id="scrollWrapper"></section>');
  1397. $('section > main[role="main"] > #scrollWrapper').append('<div class="button-up"><div></div></div>');
  1398. $('section > main[role="main"] > #scrollWrapper').append('<div class="button-down"><div></div></div>');
  1399.  
  1400. $('section > main[role="main"] > #scrollWrapper > .button-up').on('click', function () {
  1401. $('section > main[role="main"] > div')[0].scrollBy({ top: -30, behavior: "smooth" });
  1402. });
  1403. $('section > main[role="main"] > #scrollWrapper > .button-down').on('click', function () {
  1404. $('section > main[role="main"] > div')[0].scrollBy({ top: 30, behavior: "smooth" });
  1405. });
  1406. }
  1407.  
  1408. // reels scroll has [tabindex] but header not.
  1409. $('section > main[role="main"] > div[tabindex]').children('div').each(function () {
  1410. if ($(this).children().length > 0) {
  1411. if (!$(this).children().find('.IG_REELS').length) {
  1412. var $main = $(this);
  1413.  
  1414. $(this).children().css('position', 'relative');
  1415.  
  1416. $(this).children().append(`<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="IG_REELS">${SVG.DOWNLOAD}</div>`);
  1417. $(this).children().append(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="IG_REELS_NEWTAB">${SVG.NEW_TAB}</div>`);
  1418. $(this).children().append(`<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="IG_REELS_THUMBNAIL">${SVG.THUMBNAIL}</div>`);
  1419.  
  1420. // Disable video autoplay
  1421. if (USER_SETTING.DISABLE_VIDEO_LOOPING) {
  1422. $(this).find('video').each(function () {
  1423. $(this).on('ended', function () {
  1424. if (!$(this).data('loop')) {
  1425. let $element_play_button = $(this).next().find('div[role="presentation"] > div svg > path[d^="M5.888"]').parents('button[role="button"], div[role="button"]');
  1426. if ($element_play_button.length > 0) {
  1427. $(this).attr('data-loop', true);
  1428. $element_play_button.click();
  1429. logger('Adding video event listener #loop, then paused click()');
  1430. }
  1431. else {
  1432. $(this).attr('data-loop', true);
  1433. $(this).parent().find('.xpgaw4o').removeAttr('style');
  1434. this.pause();
  1435. logger('Adding video event listener #loop, then paused pause()');
  1436. }
  1437. }
  1438. });
  1439. });
  1440. }
  1441.  
  1442. // Modify video volume
  1443. //if(USER_SETTING.MODIFY_VIDEO_VOLUME){
  1444. // $(this).find('video').each(function(){
  1445. // $(this).on('play playing', function(){
  1446. // if(!$(this).data('modify')){
  1447. // $(this).attr('data-modify', true);
  1448. // this.volume = VIDEO_VOLUME;
  1449. // logger('(reel) Added video event listener #modify');
  1450. // }
  1451. // });
  1452. // });
  1453. //}
  1454.  
  1455. if (USER_SETTING.HTML5_VIDEO_CONTROL) {
  1456. $(this).find('video').each(function () {
  1457. if (!$(this).data('controls')) {
  1458. let $video = $(this);
  1459.  
  1460. logger('(reel) Added video html5 contorller #modify');
  1461.  
  1462. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  1463. this.volume = VIDEO_VOLUME;
  1464.  
  1465. $(this).on('loadstart', function () {
  1466. this.volume = VIDEO_VOLUME;
  1467. });
  1468. }
  1469.  
  1470. // Restore layout to show details interface
  1471. $(this).on('contextmenu', function (e) {
  1472. e.preventDefault();
  1473. $video.css('z-index', '-1');
  1474. $video.removeAttr('controls');
  1475. });
  1476.  
  1477. // Hide layout to show controller
  1478. $(this).parent().find('video + div div[role="button"]').filter(function () {
  1479. return $(this).parent('div[role="presentation"]').length > 0 && $(this).css('cursor') === 'pointer' && $(this).attr('style') != null;
  1480. }).first().on('contextmenu', function (e) {
  1481. e.preventDefault();
  1482. $video.css('z-index', '2');
  1483. $video.attr('controls', true);
  1484. });
  1485.  
  1486.  
  1487. $(this).on('volumechange', function () {
  1488. // eslint-disable-next-line no-unused-vars
  1489. let $element_mute_button = $(this).parent().find('video + div > div').find('button[type="button"], div[role="button"]').filter(function (idx) {
  1490. // This is mute/unmute's icon
  1491. 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;
  1492. });
  1493.  
  1494. var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0;
  1495.  
  1496. if (this.muted != is_elelment_muted) {
  1497. this.volume = VIDEO_VOLUME;
  1498. $element_mute_button?.click();
  1499. }
  1500.  
  1501. if ($(this).attr('data-completed')) {
  1502. VIDEO_VOLUME = this.volume;
  1503. GM_setValue('G_VIDEO_VOLUME', this.volume);
  1504. }
  1505.  
  1506. if (this.volume == VIDEO_VOLUME) {
  1507. $(this).attr('data-completed', true);
  1508. }
  1509. });
  1510.  
  1511. $(this).css('position', 'absolute');
  1512. $(this).css('z-index', '2');
  1513. $(this).attr('data-controls', true);
  1514. $(this).attr('controls', true);
  1515. }
  1516. });
  1517. }
  1518.  
  1519. var $videos = $main.find('video');
  1520. var $buttonParent = $(this).find('div[role="presentation"] > div[role="button"] > div').first();
  1521. toggleVolumeSilder($videos, $buttonParent, 'reel');
  1522. }
  1523. }
  1524. });
  1525. }
  1526. }, 250);
  1527. }
  1528. }
  1529.  
  1530. /**
  1531. * getStoryId
  1532. * @description Obtain the media id through the resource URL.
  1533. *
  1534. * @param {string} url
  1535. * @return {string}
  1536. */
  1537. function getStoryId(url) {
  1538. let obj = new URL(url);
  1539. let base64 = obj?.searchParams?.get('ig_cache_key')?.split('.').at(0);
  1540. if (base64) {
  1541. return atob(base64);
  1542. }
  1543. else {
  1544. return null;
  1545. }
  1546. }
  1547.  
  1548. /**
  1549. * getHighlightStories
  1550. * @description Get a list of all stories in highlight Id.
  1551. *
  1552. * @param {Integer} highlightId
  1553. * @return {Object}
  1554. */
  1555. function getHighlightStories(highlightId) {
  1556. return new Promise((resolve, reject) => {
  1557. 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`;
  1558.  
  1559. GM_xmlhttpRequest({
  1560. method: "GET",
  1561. url: getURL,
  1562. onload: function (response) {
  1563. try {
  1564. let obj = JSON.parse(response.response);
  1565. resolve(obj);
  1566. }
  1567. catch (err) {
  1568. logger('getHighlightStories()', 'reject', err.message);
  1569. reject(err);
  1570. }
  1571. },
  1572. onerror: function (err) {
  1573. logger('getHighlightStories()', 'reject', err);
  1574. reject(err);
  1575. }
  1576. });
  1577. });
  1578. }
  1579.  
  1580. /**
  1581. * getStories
  1582. * @description Get a list of all stories in user Id.
  1583. *
  1584. * @param {Integer} userId
  1585. * @return {Object}
  1586. */
  1587. function getStories(userId) {
  1588. return new Promise((resolve, reject) => {
  1589. 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`;
  1590.  
  1591. GM_xmlhttpRequest({
  1592. method: "GET",
  1593. url: getURL,
  1594. onload: function (response) {
  1595. try {
  1596. let obj = JSON.parse(response.response);
  1597. logger('getStories()', obj);
  1598. resolve(obj);
  1599. }
  1600. catch (err) {
  1601. logger('getStories()', 'reject', err.message);
  1602. reject(err);
  1603. }
  1604. },
  1605. onerror: function (err) {
  1606. logger('getStories()', 'reject', err);
  1607. reject(err);
  1608. }
  1609. });
  1610. });
  1611. }
  1612.  
  1613. /**
  1614. * getUserId
  1615. * @description Get user's id with username
  1616. *
  1617. * @param {String} username
  1618. * @return {Integer}
  1619. */
  1620. function getUserId(username) {
  1621. return new Promise((resolve, reject) => {
  1622. let getURL = `https://www.instagram.com/web/search/topsearch/?query=${username}`;
  1623.  
  1624. GM_xmlhttpRequest({
  1625. method: "GET",
  1626. url: getURL,
  1627. onload: function (response) {
  1628. // Fix search issue by Discord: sno_w_
  1629. let obj = JSON.parse(response.response);
  1630. let result = null;
  1631. obj.users.forEach(pos => {
  1632. if (pos.user.username?.toLowerCase() === username?.toLowerCase()) {
  1633. result = pos;
  1634. }
  1635. });
  1636.  
  1637. if (result != null) {
  1638. logger('getUserId()', result);
  1639. resolve(result);
  1640. }
  1641. else {
  1642. getUserIdWithAgent(username).then((result) => {
  1643. resolve(result);
  1644. // eslint-disable-next-line no-unused-vars
  1645. }).catch((err) => {
  1646. alert("Can not find user info from getUserId()");
  1647. });
  1648. }
  1649. },
  1650. onerror: function (err) {
  1651. logger('getUserId()', 'reject', err);
  1652. reject(err);
  1653. }
  1654. });
  1655. });
  1656. }
  1657.  
  1658. /**
  1659. * getUserIdWithAgent
  1660. * @description Get user's id with username
  1661. *
  1662. * @param {String} username
  1663. * @return {Integer}
  1664. */
  1665. function getUserIdWithAgent(username) {
  1666. return new Promise((resolve, reject) => {
  1667. let getURL = `https://i.instagram.com/api/v1/users/web_profile_info/?username=${username}`;
  1668.  
  1669. GM_xmlhttpRequest({
  1670. method: "GET",
  1671. url: getURL,
  1672. headers: {
  1673. 'X-IG-App-ID': getAppID()
  1674. },
  1675. onload: function (response) {
  1676. try {
  1677. let obj = JSON.parse(response.response);
  1678. let hasUser = obj?.data?.user;
  1679.  
  1680. if (hasUser != null) {
  1681. let userInfo = obj?.data;
  1682. userInfo.user.pk = userInfo.user.id;
  1683. logger('getUserIdWithAgent()', obj);
  1684. resolve(userInfo);
  1685. }
  1686. else {
  1687. logger('getUserIdWithAgent()', 'reject', 'undefined');
  1688. reject('undefined');
  1689. }
  1690. }
  1691. catch (err) {
  1692. logger('getUserIdWithAgent()', 'reject', err.message);
  1693. reject(err);
  1694. }
  1695. },
  1696. onerror: function (err) {
  1697. logger('getUserIdWithAgent()', 'reject', err);
  1698. reject(err);
  1699. }
  1700. });
  1701. });
  1702. }
  1703.  
  1704. /**
  1705. * getUserHighSizeProfile
  1706. * @description Get user's high quality avatar image.
  1707. *
  1708. * @param {Integer} userId
  1709. * @return {String}
  1710. */
  1711. function getUserHighSizeProfile(userId) {
  1712. return new Promise((resolve, reject) => {
  1713. let getURL = `https://i.instagram.com/api/v1/users/${userId}/info/`;
  1714.  
  1715. GM_xmlhttpRequest({
  1716. method: "GET",
  1717. url: getURL,
  1718. headers: {
  1719. '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'
  1720. },
  1721. onload: function (response) {
  1722. try {
  1723. let obj = JSON.parse(response.response);
  1724. if (obj.status !== 'ok') {
  1725. logger('getUserHighSizeProfile()', 'reject', obj);
  1726. reject('faild');
  1727. }
  1728. else {
  1729. logger('getUserHighSizeProfile()', obj);
  1730. resolve(obj.user.hd_profile_pic_url_info?.url);
  1731. }
  1732. }
  1733. catch (err) {
  1734. logger('getUserHighSizeProfile()', 'reject', err);
  1735. reject(err);
  1736. }
  1737. },
  1738. onerror: function (err) {
  1739. logger('getUserHighSizeProfile()', 'reject', err);
  1740. reject(err);
  1741. }
  1742. });
  1743. });
  1744. }
  1745.  
  1746. /**
  1747. * getPostOwner
  1748. * @description Get post's author with post shortcode
  1749. *
  1750. * @param {String} postPath
  1751. * @return {String}
  1752. */
  1753. function getPostOwner(postPath) {
  1754. return new Promise((resolve, reject) => {
  1755. if (!postPath) reject("NOPATH");
  1756. let postShortCode = postPath;
  1757. let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`;
  1758.  
  1759. GM_xmlhttpRequest({
  1760. method: "GET",
  1761. url: getURL,
  1762. onload: function (response) {
  1763. try {
  1764. let obj = JSON.parse(response.response);
  1765. logger('getPostOwner()', obj);
  1766. resolve(obj.data.shortcode_media.owner.username);
  1767. }
  1768. catch (err) {
  1769. logger('getPostOwner()', 'reject', err.message);
  1770. reject(err);
  1771. }
  1772. },
  1773. onerror: function (err) {
  1774. logger('getPostOwner()', 'reject', err);
  1775. reject(err);
  1776. }
  1777. });
  1778. });
  1779. }
  1780.  
  1781. /**
  1782. * getBlobMedia
  1783. * @description Get list of all media files in post with post shortcode
  1784. *
  1785. * @param {String} postPath
  1786. * @return {Object}
  1787. */
  1788. function getBlobMedia(postPath) {
  1789. return new Promise((resolve, reject) => {
  1790. if (!postPath) reject("NOPATH");
  1791. let postShortCode = postPath;
  1792. let getURL = `https://www.instagram.com/graphql/query/?query_hash=2c4c2e343a8f64c625ba02b2aa12c7f8&variables=%7B%22shortcode%22:%22${postShortCode}%22}`;
  1793.  
  1794. GM_xmlhttpRequest({
  1795. method: "GET",
  1796. url: getURL,
  1797. headers: {
  1798. "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"
  1799. },
  1800. onload: function (response) {
  1801. try {
  1802. let obj = JSON.parse(response.response);
  1803. logger(obj);
  1804.  
  1805. if (obj.status === 'fail') {
  1806. // alert(`Request failed with API response:\n${obj.message}: ${obj.feedback_message}`);
  1807. logger('Request with:', 'getBlobMediaWithQuery()', postShortCode);
  1808. getBlobMediaWithQueryID(postShortCode).then((res) => {
  1809. resolve({ type: 'query_id', data: res.xdt_api__v1__media__shortcode__web_info.items[0] });
  1810. }).catch((err) => {
  1811. reject(err);
  1812. })
  1813. }
  1814. else {
  1815. resolve({ type: 'query_hash', data: obj.data });
  1816. }
  1817. }
  1818. catch (err) {
  1819. logger('getBlobMedia()', 'reject', err.message);
  1820. reject(err);
  1821. }
  1822. },
  1823. onerror: function (err) {
  1824. logger('getBlobMedia()', 'reject', err);
  1825. reject(err);
  1826. }
  1827. });
  1828. });
  1829. }
  1830.  
  1831. /**
  1832. * getBlobMediaWithQueryID
  1833. * @description Get list of all media files in post with post shortcode
  1834. *
  1835. * @param {String} postPath
  1836. * @return {Object}
  1837. */
  1838. function getBlobMediaWithQueryID(postPath) {
  1839. return new Promise((resolve, reject) => {
  1840. if (!postPath) reject("NOPATH");
  1841. let postShortCode = postPath;
  1842. 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}`;
  1843.  
  1844. GM_xmlhttpRequest({
  1845. method: "GET",
  1846. url: getURL,
  1847. headers: {
  1848. "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",
  1849. 'X-IG-App-ID': getAppID()
  1850. },
  1851. onload: function (response) {
  1852. try {
  1853. let obj = JSON.parse(response.response);
  1854. logger(obj);
  1855.  
  1856. if (obj.status === 'fail') {
  1857. alert(`getBlobMediaWithQueryID(): Request failed with API response:\n${obj.message}: ${obj.feedback_message}`);
  1858. logger(`Request failed with API response ${obj.message}: ${obj.feedback_message}`);
  1859. reject(response);
  1860. }
  1861. else {
  1862. logger('getBlobMediaWithQueryID()', obj.data);
  1863. resolve(obj.data);
  1864. }
  1865. }
  1866. catch (err) {
  1867. logger('getBlobMediaWithQueryID()', 'reject', err.message);
  1868. reject(err);
  1869. }
  1870. },
  1871. onerror: function (err) {
  1872. logger('getBlobMediaWithQueryID()', 'reject', err);
  1873. reject(err);
  1874. }
  1875. });
  1876. });
  1877. }
  1878.  
  1879. /**
  1880. * onReadyMyDW
  1881. * @description Create an event entry point for the download button for the post
  1882. *
  1883. * @param {Boolean} NoDialog - Check if it not showing the dialog
  1884. * @param {?Boolean} hasReferrer - Check if the source of the previous page is a story page
  1885. * @return {void}
  1886. */
  1887. function onReadyMyDW(NoDialog, hasReferrer) {
  1888. if (hasReferrer === true) {
  1889. logger('hasReferrer', 'regenerated');
  1890. $('article[data-snig="canDownload"], div[data-snig="canDownload"]').filter(function () {
  1891. return $(this).find('.SNKMS_IG_DW_MAIN').length === 0
  1892. }).removeAttr('data-snig');
  1893. }
  1894.  
  1895. // Whether is Instagram dialog?
  1896. if (NoDialog == false) {
  1897. const maxCall = 100;
  1898. let i = 0;
  1899. var repeat = setInterval(() => {
  1900. // 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) >>
  1901. // section:visible > main > div > div.xdt5ytf[data-snig="canDownload"] << (former CSS selector for single foreground post in page, non-floating) >>
  1902. // <hr> is much more unique element than "div.xdt5ytf"
  1903. 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) {
  1904. clearInterval(repeat);
  1905.  
  1906. if (i > maxCall) {
  1907. //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.');
  1908. console.warn('onReadyMyDW() Timer', 'maximum number of repetitions reached, terminated');
  1909. }
  1910. }
  1911.  
  1912. logger('onReadyMyDW() Timer', 'repeating to call detection createDownloadButton()');
  1913. createDownloadButton();
  1914. i++;
  1915. }, 50);
  1916. }
  1917. else {
  1918. createDownloadButton();
  1919. }
  1920. }
  1921.  
  1922. /**
  1923. * getAppID
  1924. * @description Get Instagram App ID
  1925. *
  1926. * @return {?integer}
  1927. */
  1928. function getAppID() {
  1929. let result = null;
  1930. $('script[type="application/json"]').each(function () {
  1931. const regexp = /"APP_ID":"([0-9]+)"/ig;
  1932. const matcher = $(this).text().match(regexp);
  1933. if (matcher != null && result == null) {
  1934. result = [...$(this).text().matchAll(regexp)];
  1935. }
  1936. })
  1937.  
  1938. return (result) ? result.at(0).at(-1) : null;
  1939. }
  1940.  
  1941. /**
  1942. * updateLoadingBar
  1943. * @description Update loading state
  1944. *
  1945. * @param {Boolean} isLoading - Check if loading state
  1946. * @return {void}
  1947. */
  1948. function updateLoadingBar(isLoading) {
  1949. if (isLoading) {
  1950. $('div[id^="mount"] > div > div > div:first').removeClass('x1s85apg');
  1951. $('div[id^="mount"] > div > div > div:first').css('z-index', '20000');
  1952. }
  1953. else {
  1954. $('div[id^="mount"] > div > div > div:first').addClass('x1s85apg');
  1955. $('div[id^="mount"] > div > div > div:first').css('z-index', '');
  1956. }
  1957. }
  1958.  
  1959. /**
  1960. * getStoryProgress
  1961. * @description Get the story progress of the username (post several stories)
  1962. *
  1963. * @param {String} username - Get progress of username
  1964. * @return {Object}
  1965. */
  1966. function getStoryProgress(username) {
  1967. let $header = $('body > div section:visible a[href^="/' + (username) + '"] span').filter(function () {
  1968. return $(this).children().length === 0 && $(this).find('svg').length === 0 && $(this).text()?.toLowerCase() === username?.toLowerCase();
  1969. }).parents('div:not([class]):not([style])').filter(function () {
  1970. return $(this).text()?.toLowerCase() !== username?.toLowerCase()
  1971. }).filter(function () {
  1972. return $(this).children().length > 1
  1973. }).first();
  1974.  
  1975. if ($header.length === 0) {
  1976. $header = $('body > div section:visible a[href^="/' + (username) + '"]').filter(function () {
  1977. return $(this).find('img').length > 0
  1978. }).parents('div:not([class]):not([style])').filter(function () {
  1979. return $(this).text()?.toLowerCase() !== username?.toLowerCase()
  1980. }).filter(function () {
  1981. return $(this).children().length > 1
  1982. }).first();
  1983. }
  1984.  
  1985. return $header.children().filter(function () {
  1986. return $(this).height() < 10
  1987. }).first().children();
  1988. }
  1989.  
  1990. /**
  1991. * getMediaInfo
  1992. * @description Get Instagram Media object
  1993. *
  1994. * @param {String} mediaId
  1995. * @return {Object}
  1996. */
  1997. function getMediaInfo(mediaId) {
  1998. return new Promise((resolve, reject) => {
  1999. let getURL = `https://i.instagram.com/api/v1/media/${mediaId}/info/`;
  2000.  
  2001. if (mediaId == null) {
  2002. alert("Can not call Media API because of the media id is invalid.");
  2003. logger('getMediaInfo()', 'reject', 'Can not call Media API because of the media id is invalid.');
  2004.  
  2005. updateLoadingBar(false);
  2006. reject(-1);
  2007. return;
  2008. }
  2009. if (getAppID() == null) {
  2010. alert("Can not call Media API because of the app id is invalid.");
  2011. logger('getMediaInfo()', 'reject', 'Can not call Media API because of the app id is invalid.');
  2012. updateLoadingBar(false);
  2013. reject(-1);
  2014. return;
  2015. }
  2016.  
  2017. GM_xmlhttpRequest({
  2018. method: "GET",
  2019. url: getURL,
  2020. headers: {
  2021. "User-Agent": window.navigator.userAgent,
  2022. "Accept": "*/*",
  2023. 'X-IG-App-ID': getAppID()
  2024. },
  2025. onload: function (response) {
  2026. if (response.finalUrl == getURL) {
  2027. let obj = JSON.parse(response.response);
  2028. logger('getMediaInfo()', obj);
  2029. resolve(obj);
  2030. }
  2031. else {
  2032. let finalURL = new URL(response.finalUrl);
  2033. if (finalURL.pathname.startsWith('/accounts/login')) {
  2034. logger('getMediaInfo()', 'reject', 'The account must be logged in to access Media API.');
  2035. alert("The account must be logged in to access Media API.");
  2036. }
  2037. else {
  2038. logger('getMediaInfo()', 'reject', 'Unable to retrieve content because the API was redirected to "' + response.finalUrl + '"');
  2039. alert('Unable to retrieve content because the API was redirected to "' + response.finalUrl + '"');
  2040. }
  2041. updateLoadingBar(false);
  2042. reject(-1);
  2043. }
  2044. },
  2045. onerror: function (err) {
  2046. logger('getMediaInfo()', 'reject', err);
  2047. resolve(err);
  2048. }
  2049. });
  2050. });
  2051. }
  2052.  
  2053. /**
  2054. * getVisibleNodeIndex
  2055. * @description Get element visible node
  2056. *
  2057. * @param {Object} $main
  2058. * @return {Integer}
  2059. */
  2060. function getVisibleNodeIndex($main) {
  2061. var index = 0;
  2062. // homepage classList
  2063. var $dot = $main.find('.x1iyjqo2 > div > div:last-child > div');
  2064.  
  2065. // dialog classList, main top classList
  2066. if ($dot == null || !$dot.hasClass('_acnb')) {
  2067. $dot = $main.find('._aatk > div > div:last-child').eq(0).children('div');
  2068. }
  2069.  
  2070. $dot.filter('._acnb').each(function (sIndex) {
  2071. if ($(this).hasClass('_acnf')) {
  2072. index = sIndex;
  2073. }
  2074. });
  2075.  
  2076. return index;
  2077. }
  2078.  
  2079. /**
  2080. * setDownloadProgress
  2081. * @description Show and set download circle progress
  2082. *
  2083. * @param {Integer} now
  2084. * @param {Integer} total
  2085. * @return {Void}
  2086. */
  2087. function setDownloadProgress(now, total) {
  2088. if ($('.circle_wrapper').length) {
  2089. $('.circle_wrapper span').text(`${now}/${total}`);
  2090.  
  2091. if (now >= total) {
  2092. $('.circle_wrapper').fadeOut(250, function () {
  2093. $(this).remove();
  2094. });
  2095. }
  2096. }
  2097. else {
  2098. $('body').append(`<div class="circle_wrapper"><circle></circle><span>${now}/${total}</span></div>`);
  2099. }
  2100. }
  2101.  
  2102. /**
  2103. * initPostVideoFunction
  2104. * @description Initialize settings related to the video resources in the post
  2105. *
  2106. * @param {Object} $mainElement
  2107. * @return {Void}
  2108. */
  2109. function initPostVideoFunction($mainElement) {
  2110. // Disable video autoplay
  2111. if (USER_SETTING.DISABLE_VIDEO_LOOPING) {
  2112. $mainElement.find('video').each(function () {
  2113. $(this).on('ended', function () {
  2114. if (!$(this).data('loop')) {
  2115. $(this).attr('data-loop', true);
  2116. this.pause();
  2117. logger('(post) Added video event listener #loop');
  2118. }
  2119. });
  2120. });
  2121. }
  2122.  
  2123. // Modify video volume
  2124. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  2125. $mainElement.find('video').each(function () {
  2126. $(this).on('play playing', function () {
  2127. if (!$(this).data('modify')) {
  2128. $(this).attr('data-modify', true);
  2129. this.volume = VIDEO_VOLUME;
  2130. logger('(post) Added video event listener #modify');
  2131. }
  2132. });
  2133. });
  2134. }
  2135.  
  2136. if (USER_SETTING.HTML5_VIDEO_CONTROL) {
  2137. $mainElement.find('video').each(function () {
  2138. if (!$(this).data('controls')) {
  2139. let $video = $(this);
  2140.  
  2141. logger('(post) Added video html5 contorller #modify');
  2142.  
  2143. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  2144. this.volume = VIDEO_VOLUME;
  2145.  
  2146. $(this).on('loadstart', function () {
  2147. this.volume = VIDEO_VOLUME;
  2148. });
  2149. }
  2150.  
  2151. // Restore layout to show details interface
  2152. $(this).on('contextmenu', function (e) {
  2153. e.preventDefault();
  2154. $video.css('z-index', '-1');
  2155. $video.removeAttr('controls');
  2156. });
  2157.  
  2158. // Hide layout to show controller
  2159. $(this).parent().find('video + div > div').first().on('contextmenu', function (e) {
  2160. e.preventDefault();
  2161. $video.css('z-index', '2');
  2162. $video.attr('controls', true);
  2163. });
  2164.  
  2165. $(this).on('volumechange', function () {
  2166. // eslint-disable-next-line no-unused-vars
  2167. let $element_mute_button = $(this).parent().find('video + div > div').find('button[type="button"], div[role="button"]').filter(function (idx) {
  2168. // This is mute/unmute's icon
  2169. 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;
  2170. });
  2171.  
  2172. var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0;
  2173.  
  2174. if (this.muted != is_elelment_muted) {
  2175. this.volume = VIDEO_VOLUME;
  2176. $element_mute_button?.click();
  2177. }
  2178.  
  2179. if ($(this).attr('data-completed')) {
  2180. VIDEO_VOLUME = this.volume;
  2181. GM_setValue('G_VIDEO_VOLUME', this.volume);
  2182. }
  2183.  
  2184. if (this.volume == VIDEO_VOLUME) {
  2185. $(this).attr('data-completed', true);
  2186. }
  2187. });
  2188.  
  2189. $(this).css('position', 'absolute');
  2190. $(this).css('z-index', '2');
  2191. $(this).attr('data-controls', true);
  2192. $(this).attr('controls', true);
  2193. }
  2194. });
  2195. }
  2196.  
  2197. var $videos = $mainElement.find('video');
  2198. var $buttonParent = $mainElement.find('video + div > div').first();
  2199. toggleVolumeSilder($videos, $buttonParent, 'post', 'bottom');
  2200. };
  2201.  
  2202. /**
  2203. * createDownloadButton
  2204. * @description Create a download button in the upper right corner of each post
  2205. *
  2206. * @return {void}
  2207. */
  2208. function createDownloadButton() {
  2209. // Add download icon per each posts
  2210. // eslint-disable-next-line no-unused-vars
  2211. $('article, section:visible > main > div > div > div > div > div > hr').map(function (index) {
  2212. return $(this).is('section:visible > main > div > div > div > div > div > hr') ? $(this).parent().parent().parent().parent()[0] : this;
  2213. }).filter(function () {
  2214. return $(this).height() > 0 && $(this).width() > 0
  2215. })
  2216. .each(function (index) {
  2217. // If it is have not download icon
  2218. // class x1iyjqo2 mean user profile pages post list container
  2219. if (!$(this).attr('data-snig') && !$(this).hasClass('x1iyjqo2') && !$(this).children('article')?.hasClass('x1iyjqo2') && $(this).parents('div#scrollview').length === 0) {
  2220. logger("Found post container", $(this));
  2221.  
  2222. const $mainElement = $(this);
  2223. const tagName = this.tagName;
  2224. const resourceCountSelector = '._acay ._acaz';
  2225.  
  2226. // not loop each in single top post
  2227. if (tagName === "DIV" && index != 0) {
  2228. return;
  2229. }
  2230.  
  2231. const $childElement = $mainElement.children("div").children("div");
  2232.  
  2233. if ($childElement.length === 0) return;
  2234.  
  2235. logger("Found insert point", $childElement);
  2236.  
  2237. // Modify carousel post counter's position to not interfere with our buttons
  2238. if ($mainElement.find('._acay').length > 0) {
  2239. if ($mainElement.find('._acay + .x24i39r').length > 0) {
  2240. $mainElement.find('._acay + .x24i39r').css('top', '37px');
  2241. }
  2242.  
  2243. const observeNode = $mainElement.find('._acay').first().parent()[0];
  2244. var observer = new MutationObserver(function () {
  2245. $mainElement.find('._acay + .x24i39r').css('top', '37px');
  2246. });
  2247.  
  2248. observer.observe(observeNode, {
  2249. childList: true
  2250. });
  2251. }
  2252.  
  2253. $childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).append(`<div class="button_wrapper">`);
  2254.  
  2255. // Add icons
  2256. const DownloadElement = `<div data-ih-locale-title="DW" title="${_i18n("DW")}" class="SNKMS_IG_DW_MAIN">${SVG.DOWNLOAD}</div>`;
  2257. const NewTabElement = `<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="SNKMS_IG_NEWTAB_MAIN">${SVG.NEW_TAB}</div>`;
  2258. const ThumbnailElement = `<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="SNKMS_IG_THUMBNAIL_MAIN">${SVG.THUMBNAIL}</div>`;
  2259.  
  2260. $childElement.find(".button_wrapper").append(DownloadElement);
  2261.  
  2262. const resource_count = $mainElement.find(resourceCountSelector).length;
  2263.  
  2264. if (resource_count > 1 && USER_SETTING.DIRECT_DOWNLOAD_VISIBLE_RESOURCE && !USER_SETTING.DIRECT_DOWNLOAD_ALL) {
  2265. const DownloadAllElement = `<div data-ih-locale-title="DW_ALL" title="${_i18n("DW_ALL")}" class="SNKMS_IG_DW_ALL_MAIN">${SVG.DOWNLOAD_ALL}</div>`;
  2266. $childElement.find(".button_wrapper").append(DownloadAllElement);
  2267. }
  2268.  
  2269. $childElement.find(".button_wrapper").append(NewTabElement);
  2270.  
  2271. setTimeout(() => {
  2272. // Check if visible post is video
  2273. if ($childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).find('div > ul li._acaz').length === 0) {
  2274. if ($childElement.find('video').length > 0) {
  2275. $childElement.find(".button_wrapper").append(ThumbnailElement);
  2276. }
  2277. }
  2278. else {
  2279. // eslint-disable-next-line no-unused-vars
  2280. const checkVideoNodeCallback = (entries, observer) => {
  2281. entries.forEach((entry) => {
  2282. //logger(entry);
  2283. if (entry.isIntersecting) {
  2284. var $targetNode = $(entry.target);
  2285.  
  2286. // Check if video?
  2287. if ($targetNode.find('video').length > 0) {
  2288. $childElement.find(".button_wrapper").append(ThumbnailElement);
  2289. initPostVideoFunction($mainElement);
  2290. }
  2291. else {
  2292. $childElement.find('.SNKMS_IG_THUMBNAIL_MAIN')?.remove();
  2293. }
  2294. }
  2295. });
  2296. };
  2297.  
  2298. const observer_i = new IntersectionObserver(checkVideoNodeCallback, {
  2299. root: $mainElement.find('div > ul._acay').first().parent().parent().parent()[0],
  2300. rootMargin: "0px",
  2301. threshold: 0.1,
  2302. });
  2303.  
  2304. // trigger when switching resources
  2305. // eslint-disable-next-line no-unused-vars
  2306. const observer = new MutationObserver(function (mutation, owner) {
  2307. var target = mutation.at(0)?.target;
  2308.  
  2309. $(target).find('li._acaz').each(function () {
  2310. observer_i.observe(this);
  2311. });
  2312. });
  2313.  
  2314. // first onload
  2315. $mainElement.find('div > ul li._acaz').each(function () {
  2316. observer_i.observe(this);
  2317. });
  2318.  
  2319.  
  2320. const element = $childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).find('div > ul li._acaz')?.parent()[0];
  2321. const elementAttr = $childElement.eq((tagName === "DIV") ? 0 : $childElement.length - 2).find('div > ul li._acaz')?.parent().parent()[0];
  2322.  
  2323. if (element) {
  2324. observer.observe(element, {
  2325. childList: true
  2326. });
  2327. }
  2328.  
  2329. if (elementAttr) {
  2330. observer.observe(elementAttr, {
  2331. attributes: true
  2332. });
  2333. }
  2334. }
  2335. }, 50);
  2336.  
  2337.  
  2338. $childElement.css('position', 'relative');
  2339.  
  2340. initPostVideoFunction($mainElement);
  2341.  
  2342. GL_registerEventList.push({
  2343. element: this,
  2344. trigger: [
  2345. '.SNKMS_IG_THUMBNAIL_MAIN',
  2346. '.SNKMS_IG_NEWTAB_MAIN',
  2347. '.SNKMS_IG_DW_ALL_MAIN',
  2348. '.SNKMS_IG_DW_MAIN'
  2349. ]
  2350. });
  2351.  
  2352. $(this).on('click', '.SNKMS_IG_THUMBNAIL_MAIN', function () {
  2353. updateLoadingBar(true);
  2354.  
  2355. GL_username = $mainElement.attr('data-username');
  2356. 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);
  2357.  
  2358. var index = getVisibleNodeIndex($mainElement);
  2359.  
  2360. IG_createDM(true, false);
  2361.  
  2362. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", "").then(() => {
  2363. let checkBlob = setInterval(() => {
  2364. if ($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0) {
  2365. clearInterval(checkBlob);
  2366. var $videoThumbnail = $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="' + (index + 1) + '"]')?.parent().find('.videoThumbnail')?.first();
  2367.  
  2368. if ($videoThumbnail != null && $videoThumbnail.length > 0) {
  2369. $videoThumbnail.click();
  2370. }
  2371. else {
  2372. alert('Can not find thumbnail url.');
  2373. }
  2374.  
  2375. updateLoadingBar(false);
  2376. $('.IG_SN_DIG').remove();
  2377. }
  2378. }, 250);
  2379. });
  2380. });
  2381.  
  2382. $(this).on('click', '.SNKMS_IG_NEWTAB_MAIN', function () {
  2383. updateLoadingBar(true);
  2384.  
  2385. GL_username = $mainElement.attr('data-username');
  2386. 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);
  2387.  
  2388. var index = getVisibleNodeIndex($mainElement);
  2389.  
  2390. IG_createDM(true, false);
  2391.  
  2392. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", "").then(() => {
  2393. let checkBlob = setInterval(() => {
  2394. if ($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0) {
  2395. clearInterval(checkBlob);
  2396. var $linkElement = $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="' + (index + 1) + '"]');
  2397.  
  2398. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST) {
  2399. triggerLinkElement($linkElement.first()[0], true);
  2400. }
  2401. else {
  2402. let href = $linkElement?.attr('data-href');
  2403. if (href) {
  2404. // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video)
  2405. var urlObj = new URL(href);
  2406. urlObj.host = 'scontent.cdninstagram.com';
  2407.  
  2408. openNewTab(urlObj.href);
  2409. }
  2410. else {
  2411. alert('Can not find open tab url.');
  2412. }
  2413. }
  2414.  
  2415. updateLoadingBar(false);
  2416. $('.IG_SN_DIG').remove();
  2417. }
  2418. }, 250);
  2419. });
  2420. });
  2421.  
  2422. // Running if user click the download all icon
  2423. $(this).on('click', '.SNKMS_IG_DW_ALL_MAIN', async function () {
  2424. GL_username = $mainElement.attr('data-username');
  2425. 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);
  2426.  
  2427. // Create element that download dailog
  2428. IG_createDM(USER_SETTING.DIRECT_DOWNLOAD_ALL, true);
  2429.  
  2430. $("#article-id").html(`<a href="https://www.instagram.com/p/${GL_postPath}">${GL_postPath}</a>`);
  2431.  
  2432. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function () {
  2433. $(this).wrap('<div></div>');
  2434. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  2435. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  2436.  
  2437. if ($(this).attr('data-name') == 'video') {
  2438. $(this).after(`<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  2439. }
  2440. });
  2441.  
  2442.  
  2443. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE")).then(() => {
  2444. let checkBlob = setInterval(() => {
  2445. if ($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0) {
  2446. clearInterval(checkBlob);
  2447. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function () {
  2448. $(this).click();
  2449. });
  2450.  
  2451. $('.IG_SN_DIG').remove();
  2452. }
  2453. }, 250);
  2454. });
  2455. });
  2456.  
  2457. // Running if user click the download icon
  2458. $(this).on('click', '.SNKMS_IG_DW_MAIN', async function () {
  2459. GL_username = $mainElement.attr('data-username');
  2460. 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);
  2461.  
  2462. // Create element that download dailog
  2463. IG_createDM(USER_SETTING.DIRECT_DOWNLOAD_ALL, true);
  2464.  
  2465. $("#article-id").html(`<a href="https://www.instagram.com/p/${GL_postPath}">${GL_postPath}</a>`);
  2466.  
  2467. if (USER_SETTING.DIRECT_DOWNLOAD_VISIBLE_RESOURCE) {
  2468. updateLoadingBar(true);
  2469. IG_setDM(true);
  2470.  
  2471. var index = getVisibleNodeIndex($(this).parent().parent().parent());
  2472.  
  2473. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", "").then(() => {
  2474. let checkBlob = setInterval(() => {
  2475. if ($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0) {
  2476. clearInterval(checkBlob);
  2477. var href = $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="' + (index + 1) + '"]')?.attr('data-href');
  2478.  
  2479. if (href) {
  2480. updateLoadingBar(false);
  2481. $('.IG_SN_DIG .IG_SN_DIG_BODY a[data-globalindex="' + (index + 1) + '"]')?.click();
  2482. }
  2483. else {
  2484. alert('Can not find download url.');
  2485. }
  2486.  
  2487. $('.IG_SN_DIG').remove();
  2488. }
  2489. }, 250);
  2490. });
  2491.  
  2492. return;
  2493. }
  2494.  
  2495. if (!USER_SETTING.DIRECT_DOWNLOAD_ALL) {
  2496. // Find video/image element and add the download icon
  2497. var s = 0;
  2498. var multiple = $(this).parent().parent().find(resourceCountSelector).length;
  2499. var blob = USER_SETTING.FORCE_FETCH_ALL_RESOURCES;
  2500. var publish_time = new Date($(this).parent().parent().find('a[href^="/p/"] time[datetime]').first().attr('datetime')).getTime();
  2501.  
  2502. // If posts have more than one images or videos.
  2503. if (multiple) {
  2504. $(this).parent().parent().find(resourceCountSelector).each(function () {
  2505. let element_videos = $(this).parent().parent().parent().find('video');
  2506. //if(element_videos && element_videos.attr('src') && element_videos.attr('src').match(/^blob:/ig)){
  2507. if (element_videos && element_videos.attr('src')) {
  2508. blob = true;
  2509. }
  2510. });
  2511.  
  2512.  
  2513. if (blob || USER_SETTING.FORCE_RESOURCE_VIA_MEDIA) {
  2514. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE"));
  2515. }
  2516. else {
  2517. $(this).parent().parent().find(resourceCountSelector).each(function () {
  2518. s++;
  2519. let element_videos = $(this).find('video');
  2520. let element_images = $(this).find('._aagv img');
  2521. let imgLink = (element_images.attr('srcset')) ? element_images.attr('srcset').split(" ")[0] : element_images.attr('src');
  2522.  
  2523. if (element_videos && element_videos.attr('src')) {
  2524. blob = true;
  2525. }
  2526. if (element_images && imgLink) {
  2527. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY').append(`<a datetime="${publish_time}" data-needed="direct" data-path="${GL_postPath}" data-name="photo" data-type="jpg" data-globalIndex="${s}" href="javascript:;" data-href="${imgLink}"><img width="100" src="${imgLink}" /><br/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${s} -</a>`);
  2528. }
  2529.  
  2530. });
  2531.  
  2532. if (blob) {
  2533. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", _i18n("LOAD_BLOB_RELOAD"));
  2534. }
  2535. }
  2536. }
  2537. else {
  2538. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA) {
  2539. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE"));
  2540. }
  2541. else {
  2542. s++;
  2543. let element_videos = $(this).parent().parent().parent().find('video');
  2544. let element_images = $(this).parent().parent().parent().find('._aagv img');
  2545. let imgLink = (element_images.attr('srcset')) ? element_images.attr('srcset').split(" ")[0] : element_images.attr('src');
  2546.  
  2547.  
  2548. if (element_videos && element_videos.attr('src')) {
  2549. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", _i18n("LOAD_BLOB_ONE"));
  2550. }
  2551. if (element_images && imgLink) {
  2552. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY').append(`<a datetime="${publish_time}" data-needed="direct" data-path="${GL_postPath}" data-name="photo" data-type="jpg" data-globalIndex="${s}" href="javascript:;" href="" data-href="${imgLink}"><img width="100" src="${imgLink}" /><br/>- <span data-ih-locale="IMG">${_i18n("IMG")}</span> ${s} -</a>`);
  2553. }
  2554. }
  2555. }
  2556. }
  2557.  
  2558. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function () {
  2559. $(this).wrap('<div></div>');
  2560. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  2561. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  2562.  
  2563. if ($(this).attr('data-name') == 'video') {
  2564. $(this).after(`<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  2565. }
  2566. });
  2567.  
  2568. if (USER_SETTING.DIRECT_DOWNLOAD_ALL) {
  2569. createMediaListDOM(GL_postPath, ".IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY", _i18n("LOAD_BLOB_MULTIPLE")).then(() => {
  2570. let checkBlob = setInterval(() => {
  2571. if ($('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').length > 0) {
  2572. clearInterval(checkBlob);
  2573. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function () {
  2574. $(this).click();
  2575. });
  2576.  
  2577. $('.IG_SN_DIG').remove();
  2578. }
  2579. }, 250);
  2580. });
  2581. }
  2582. });
  2583.  
  2584. // Add the mark that download is ready
  2585. var username = $(this).find("header > div:last-child > div:first-child span a").first().text() || $(this).find('a[href^="/"]').filter(function () {
  2586. return $(this)?.text()?.length > 0;
  2587. }).first().text();
  2588.  
  2589. $(this).attr('data-snig', 'canDownload');
  2590. $(this).attr('data-username', username);
  2591. }
  2592. });
  2593. }
  2594.  
  2595. /**
  2596. * filterResourceData
  2597. * @description Standardized resource object format
  2598. *
  2599. * @param {Object} data
  2600. * @return {Object}
  2601. */
  2602. function filterResourceData(data) {
  2603. var resource = data.shortcode_media ?? data;
  2604. if (resource.owner == null && resource.user != null) {
  2605. resource.owner = resource.user;
  2606. }
  2607.  
  2608. if (resource.owner == null) {
  2609. logger('carousel_media:', 'undefined username');
  2610. alert('carousel_media: undefined username');
  2611. }
  2612.  
  2613. return resource;
  2614. }
  2615.  
  2616. /**
  2617. * createMediaListDOM
  2618. * @description Create a list of media elements from post URLs
  2619. *
  2620. * @param {String} postURL
  2621. * @param {String} selector - Use CSS element selectors to choose where it appears.
  2622. * @param {String} message - i18n display loading message
  2623. * @return {void}
  2624. */
  2625. async function createMediaListDOM(postURL, selector, message) {
  2626. try {
  2627. $(`${selector} a`).remove();
  2628. $(selector).append('<p id="_SNLOAD">' + message + '</p>');
  2629. let result = await getBlobMedia(postURL);
  2630. let resource = filterResourceData(result.data);
  2631.  
  2632. if (result.type === 'query_hash') {
  2633. let idx = 1;
  2634.  
  2635. // GraphVideo
  2636. if (resource.__typename == "GraphVideo" && resource.video_url) {
  2637. $(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>`);
  2638. idx++;
  2639. }
  2640. // GraphImage
  2641. if (resource.__typename == "GraphImage") {
  2642. $(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>`);
  2643. idx++;
  2644. }
  2645. // GraphSidecar
  2646. if (resource.__typename == "GraphSidecar" && resource.edge_sidecar_to_children) {
  2647. for (let e of resource.edge_sidecar_to_children.edges) {
  2648. if (e.node.__typename == "GraphVideo") {
  2649. $(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>`);
  2650. }
  2651.  
  2652. if (e.node.__typename == "GraphImage") {
  2653. $(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>`);
  2654. }
  2655. idx++;
  2656. }
  2657. }
  2658. }
  2659. else {
  2660. if (resource.carousel_media) {
  2661. logger('carousel_media');
  2662.  
  2663. resource.carousel_media.forEach((mda, ind) => {
  2664. let idx = ind + 1;
  2665. // Image
  2666. if (mda.video_versions == null) {
  2667. mda.image_versions2.candidates.sort(function (a, b) {
  2668. let aSTP = new URL(a.url).searchParams.get('stp');
  2669. let bSTP = new URL(b.url).searchParams.get('stp');
  2670.  
  2671. if (aSTP && bSTP) {
  2672. if (aSTP.length > bSTP.length) return 1;
  2673. if (aSTP.length < bSTP.length) return -1;
  2674. }
  2675. else {
  2676. if (a.width < b.width) return 1;
  2677. if (a.width > b.width) return -1;
  2678. }
  2679.  
  2680. return 0;
  2681. });
  2682.  
  2683. $(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>`);
  2684. }
  2685. // Video
  2686. else {
  2687. $(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>`);
  2688. }
  2689. });
  2690. }
  2691. else {
  2692. let idx = 1;
  2693. // Image
  2694. if (resource.video_versions == null) {
  2695. resource.image_versions2.candidates.sort(function (a, b) {
  2696. let aSTP = new URL(a.url).searchParams.get('stp');
  2697. let bSTP = new URL(b.url).searchParams.get('stp');
  2698.  
  2699. if (aSTP && bSTP) {
  2700. if (aSTP.length > bSTP.length) return 1;
  2701. if (aSTP.length < bSTP.length) return -1;
  2702. }
  2703. else {
  2704. if (a.width < b.width) return 1;
  2705. if (a.width > b.width) return -1;
  2706. }
  2707.  
  2708. return 0;
  2709. });
  2710.  
  2711. $(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>`);
  2712. }
  2713. // Video
  2714. else {
  2715. $(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>`);
  2716. }
  2717. }
  2718. }
  2719.  
  2720. $("#_SNLOAD").remove();
  2721. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_BODY a').each(function () {
  2722. $(this).wrap('<div></div>');
  2723. $(this).before('<label class="inner_box_wrapper"><input class="inner_box" type="checkbox"><span></span></label>');
  2724. $(this).after(`<div data-ih-locale-title="NEW_TAB" title="${_i18n("NEW_TAB")}" class="newTab">${SVG.NEW_TAB}</div>`);
  2725.  
  2726. if ($(this).attr('data-name') == 'video') {
  2727. $(this).after(`<div data-ih-locale-title="THUMBNAIL_INTRO" title="${_i18n("THUMBNAIL_INTRO")}" class="videoThumbnail">${SVG.THUMBNAIL}</div>`);
  2728. }
  2729. });
  2730. }
  2731. catch (err) {
  2732. logger('createMediaListDOM', err);
  2733. };
  2734. }
  2735.  
  2736. /**
  2737. * IG_createDM
  2738. * @description A dialog showing a list of all media files in the post
  2739. *
  2740. * @param {Boolean} hasHidden
  2741. * @param {Boolean} hasCheckbox
  2742. * @return {void}
  2743. */
  2744. function IG_createDM(hasHidden, hasCheckbox) {
  2745. let isHidden = (hasHidden) ? "hidden" : "";
  2746. $('body').append('<div class="IG_SN_DIG ' + isHidden + '"><div class="IG_SN_DIG_BG"></div><div class="IG_SN_DIG_MAIN"><div class="IG_SN_DIG_TITLE"></div><div class="IG_SN_DIG_BODY"></div></div></div>');
  2747. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE').append(`<div style="position:relative;min-height:36px;text-align:center;margin-bottom: 7px;"><div style="position:absolute;left:0px;line-height: 18px;"><kbd>Alt</kbd>+<kbd>Q</kbd> [<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_SN_DIG_BTN">${SVG.CLOSE}</div></div>`);
  2748.  
  2749. if (hasCheckbox) {
  2750. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE').append(`<div style="text-align: center;" id="button_group"></div>`);
  2751. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE > div#button_group').append(`<button id="batch_download_selected" data-ih-locale="BATCH_DOWNLOAD_SELECTED">${_i18n('BATCH_DOWNLOAD_SELECTED')}</button>`);
  2752. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE > div#button_group').append(`<button id="batch_download_direct" data-ih-locale="BATCH_DOWNLOAD_DIRECT">${_i18n('BATCH_DOWNLOAD_DIRECT')}</button>`);
  2753. $('.IG_SN_DIG .IG_SN_DIG_MAIN .IG_SN_DIG_TITLE').append(`<label class="checkbox"><input value="yes" type="checkbox" /><span data-ih-locale="ALL_CHECK">${_i18n('ALL_CHECK')}</span></label>`);
  2754. }
  2755. }
  2756.  
  2757. /**
  2758. * IG_setDM
  2759. * @description Set a dialog status
  2760. *
  2761. * @param {Boolean} hasHidden
  2762. * @return {void}
  2763. */
  2764. function IG_setDM(hasHidden) {
  2765. if ($('.IG_SN_DIG').length) {
  2766. if (hasHidden) {
  2767. $('.IG_SN_DIG').addClass("hidden");
  2768. }
  2769. else {
  2770. $('.IG_SN_DIG').removeClass("hidden");
  2771. }
  2772. }
  2773. }
  2774.  
  2775. /**
  2776. * saveFiles
  2777. * @description Download the specified media URL to the computer
  2778. *
  2779. * @param {String} downloadLink
  2780. * @param {String} username
  2781. * @param {String} sourceType
  2782. * @param {Integer} timestamp
  2783. * @param {String} filetype
  2784. * @param {String} shortcode
  2785. * @return {Promise}
  2786. */
  2787. function saveFiles(downloadLink, username, sourceType, timestamp, filetype, shortcode) {
  2788. return new Promise((resolve) => {
  2789. setTimeout(() => {
  2790. updateLoadingBar(true);
  2791. fetch(downloadLink).then(res => {
  2792. return res.blob().then(dwel => {
  2793. updateLoadingBar(false);
  2794. createSaveFileElement(downloadLink, dwel, username, sourceType, timestamp, filetype, shortcode);
  2795.  
  2796. resolve(true);
  2797. });
  2798. });
  2799. }, 50);
  2800. });
  2801. }
  2802.  
  2803. /**
  2804. * createSaveFileElement
  2805. * @description Download the specified media with link element
  2806. *
  2807. * @param {String} downloadLink
  2808. * @param {Object} object
  2809. * @param {String} username
  2810. * @param {String} sourceType
  2811. * @param {Integer} timestamp
  2812. * @param {String} filetype
  2813. * @param {String} shortcode
  2814. * @return {void}
  2815. */
  2816. function createSaveFileElement(downloadLink, object, username, sourceType, timestamp, filetype, shortcode) {
  2817. timestamp = parseInt(timestamp.toString().padEnd(13, '0'));
  2818.  
  2819. if (USER_SETTING.RENAME_PUBLISH_DATE) {
  2820. timestamp = parseInt(timestamp.toString().padEnd(13, '0'));
  2821. }
  2822.  
  2823. const date = new Date(timestamp);
  2824.  
  2825. const a = document.createElement("a");
  2826. const original_name = new URL(downloadLink).pathname.split('/').at(-1).split('.').slice(0, -1).join('.');
  2827. const year = date.getFullYear().toString();
  2828. const month = (date.getMonth() + 1).toString().padStart(2, '0');
  2829. const day = date.getDate().toString().padStart(2, '0');
  2830. const hour = date.getHours().toString().padStart(2, '0');
  2831. const minute = date.getMinutes().toString().padStart(2, '0');
  2832. const second = date.getSeconds().toString().padStart(2, '0');
  2833.  
  2834. var filename = RENAME_FORMAT.toUpperCase();
  2835. var format_shortcode = shortcode ?? "";
  2836. var replacements = {
  2837. '%USERNAME%': username,
  2838. '%SOURCE_TYPE%': sourceType,
  2839. '%SHORTCODE%': format_shortcode,
  2840. '%YEAR%': year,
  2841. '%2-YEAR%': year.substr(-2),
  2842. '%MONTH%': month,
  2843. '%DAY%': day,
  2844. '%HOUR%': hour,
  2845. '%MINUTE%': minute,
  2846. '%SECOND%': second,
  2847. '%ORIGINAL_NAME%': original_name,
  2848. '%ORIGINAL_NAME_FIRST%': original_name.split('_').at(0)
  2849. };
  2850.  
  2851. // eslint-disable-next-line no-useless-escape
  2852. filename = filename.replace(/%[\w\-]+%/g, function (str) {
  2853. return replacements[str] || str;
  2854. });
  2855.  
  2856. const originally = username + '_' + original_name + '.' + filetype;
  2857.  
  2858. a.href = URL.createObjectURL(object);
  2859. a.setAttribute("download", (USER_SETTING.AUTO_RENAME) ? filename + '.' + filetype : originally);
  2860. a.click();
  2861. a.remove();
  2862. }
  2863.  
  2864. /**
  2865. * triggerLinkElement
  2866. * @description Trigger the link element to start downloading the resource
  2867. *
  2868. * @param {Object} element
  2869. * @return {void}
  2870. */
  2871. async function triggerLinkElement(element, isPreview) {
  2872. let date = new Date().getTime();
  2873. let timestamp = Math.floor(date / 1000);
  2874. let username = ($(element).attr('data-username')) ? $(element).attr('data-username') : GL_username;
  2875.  
  2876. if (!username && $(element).attr('data-path')) {
  2877. logger('catching owner name from shortcode:', $(element).attr('data-href'));
  2878. username = await getPostOwner($(element).attr('data-path')).catch(err => {
  2879. logger('get username failed, replace with default string, error message:', err.message);
  2880. });
  2881.  
  2882. if (username == null) {
  2883. username = "NONE";
  2884. }
  2885. }
  2886.  
  2887. if (USER_SETTING.RENAME_PUBLISH_DATE && $(element).attr('datetime')) {
  2888. timestamp = parseInt($(element).attr('datetime'));
  2889. }
  2890.  
  2891. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA) {
  2892. updateLoadingBar(true);
  2893. let result = await getMediaInfo($(element).attr('media-id'));
  2894. updateLoadingBar(false);
  2895.  
  2896. if (result.status === 'ok') {
  2897. var resource_url = null;
  2898. if (result.items[0].video_versions) {
  2899. resource_url = result.items[0].video_versions[0].url;
  2900. }
  2901. else {
  2902. result.items[0].image_versions2.candidates.sort(function (a, b) {
  2903. let aSTP = new URL(a.url).searchParams.get('stp');
  2904. let bSTP = new URL(b.url).searchParams.get('stp');
  2905.  
  2906. if (aSTP && bSTP) {
  2907. if (aSTP.length > bSTP.length) return 1;
  2908. if (aSTP.length < bSTP.length) return -1;
  2909. }
  2910. else {
  2911. if (a.width < b.width) return 1;
  2912. if (a.width > b.width) return -1;
  2913. }
  2914.  
  2915. return 0;
  2916. });
  2917.  
  2918. resource_url = result.items[0].image_versions2.candidates[0].url;
  2919. }
  2920.  
  2921. if (isPreview) {
  2922. let urlObj = new URL(resource_url);
  2923. urlObj.host = 'scontent.cdninstagram.com';
  2924.  
  2925. openNewTab(urlObj.href);
  2926. }
  2927. else {
  2928. saveFiles(resource_url, username, $(element).attr('data-name'), timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  2929. }
  2930. }
  2931. else {
  2932. if (USER_SETTING.USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT) {
  2933. if (isPreview) {
  2934. let urlObj = new URL($(element).attr('data-href'));
  2935. urlObj.host = 'scontent.cdninstagram.com';
  2936.  
  2937. openNewTab(urlObj.href);
  2938. }
  2939. else {
  2940. saveFiles($(element).attr('data-href'), username, $(element).attr('data-name'), timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  2941. }
  2942. }
  2943. else {
  2944. alert('Fetch failed from Media API. API response message: ' + result.message);
  2945. }
  2946. logger(result);
  2947. }
  2948. }
  2949. else {
  2950. saveFiles($(element).attr('data-href'), username, $(element).attr('data-name'), timestamp, $(element).attr('data-type'), $(element).attr('data-path'));
  2951. }
  2952. }
  2953.  
  2954. /**
  2955. * translateText
  2956. * @description i18n translation text
  2957. *
  2958. * @return {void}
  2959. */
  2960. function translateText() {
  2961. var eLocale = {
  2962. "en-US": {
  2963. "NOTICE_UPDATE_TITLE": "Wololo! New version released.",
  2964. "NOTICE_UPDATE_CONTENT": "IG-Helper has released a new version, click here to update.",
  2965. "CHECK_UPDATE": "Checking for Script Updates",
  2966. "CHECK_UPDATE_MENU": "Checking for Updates",
  2967. "CHECK_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.",
  2968. "RELOAD_SCRIPT": "Reload Script",
  2969. "DONATE": "Donate",
  2970. "FEEDBACK": "Feedback",
  2971. "NEW_TAB": "Open in New Tab",
  2972. "SHOW_DOM_TREE": "Show DOM Tree",
  2973. "SELECT_AND_COPY": "Select All and Copy from the Input Box",
  2974. "DOWNLOAD_DOM_TREE": "Download DOM Tree as a Text File",
  2975. "REPORT_GITHUB": "Report an Issue on GitHub",
  2976. "REPORT_DISCORD": "Report an Issue on Discord Support Server",
  2977. "REPORT_FORK": "Report an Issue on Greasy Fork",
  2978. "DEBUG": "Debug Window",
  2979. "CLOSE": "Close",
  2980. "ALL_CHECK": "Select All",
  2981. "BATCH_DOWNLOAD_SELECTED": "Download Selected Resources",
  2982. "BATCH_DOWNLOAD_DIRECT": "Download All Resources",
  2983. "IMG": "Image",
  2984. "VID": "Video",
  2985. "DW": "Download",
  2986. "DW_ALL": "Download All Resources",
  2987. "THUMBNAIL_INTRO": "Download Video Thumbnail",
  2988. "LOAD_BLOB_ONE": "Loading Blob Media...",
  2989. "LOAD_BLOB_MULTIPLE": "Loading Blob Media and Others...",
  2990. "LOAD_BLOB_RELOAD": "Detecting Blob Media, reloading...",
  2991. "NO_CHECK_RESOURCE": "You need to select a resource to download.",
  2992. "NO_VID_URL": "Cannot find video URL.",
  2993. "SETTING": "Settings",
  2994. "AUTO_RENAME": "Automatically Rename Files (Right-Click to Set)",
  2995. "RENAME_SHORTCODE": "Rename the File and Include Shortcode",
  2996. "RENAME_PUBLISH_DATE": "Set Renamed File Timestamp to Resource Publish Date",
  2997. "RENAME_LOCATE_DATE": "Modify Renamed File Timestamp Date Format (Right-Click to Set)",
  2998. "DISABLE_VIDEO_LOOPING": "Disable Video Auto-looping",
  2999. "HTML5_VIDEO_CONTROL": "Display HTML5 Video Controller",
  3000. "REDIRECT_CLICK_USER_STORY_PICTURE": "Redirect When Clicking on User's Story Picture",
  3001. "FORCE_FETCH_ALL_RESOURCES": "Force Fetch All Resources in the Post",
  3002. "DIRECT_DOWNLOAD_VISIBLE_RESOURCE": "Directly Download the Visible Resources in the Post",
  3003. "DIRECT_DOWNLOAD_ALL": "Directly Download All Resources in the Post",
  3004. "MODIFY_VIDEO_VOLUME": "Modify Video Volume (Right-Click to Set)",
  3005. "SCROLL_BUTTON": "Enable Scroll Buttons for Reels Page",
  3006. "FORCE_RESOURCE_VIA_MEDIA": "Force Fetch Resource via Media API",
  3007. "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT": "Use Alternative Methods to Download When the Media API is Not Accessible",
  3008. "NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST": "Always Use Media API for 'Open in New Tab' in Posts",
  3009. "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",
  3010. "RENAME_SHORTCODE_INTRO": "Auto rename file to the following format:\nUSERNAME-TYPE-SHORTCODE-TIMESTAMP.FILETYPE\nExample: instagram-photo-CwkxyiVynpW-1670350000.jpg\n\nThis will ONLY work if [Automatically Rename Files] is set to TRUE.",
  3011. "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.",
  3012. "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.",
  3013. "DISABLE_VIDEO_LOOPING_INTRO": "Disable video auto-looping in Reels and posts.",
  3014. "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.",
  3015. "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.",
  3016. "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.",
  3017. "DIRECT_DOWNLOAD_VISIBLE_RESOURCE_INTRO": "Directly download the current resources available in the post.",
  3018. "DIRECT_DOWNLOAD_ALL_INTRO": "When you click the download button, all resources in the post will be forcibly fetched and downloaded.",
  3019. "MODIFY_VIDEO_VOLUME_INTRO": "Modify the video playback volume in Reels and posts (right-click to open the volume setting slider).",
  3020. "SCROLL_BUTTON_INTRO": "Enable scroll buttons for the lower right corner of the Reels page.",
  3021. "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.",
  3022. "USE_BLOB_FETCH_WHEN_MEDIA_RATE_LIMIT_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).",
  3023. "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.",
  3024. "SKIP_VIEW_STORY_CONFIRM": "Skip the Confirmation Page for Viewing a Story/Highlight",
  3025. "SKIP_VIEW_STORY_CONFIRM_INTRO": "Automatically skip when confirmation page is shown in story or highlight."
  3026. }
  3027. };
  3028.  
  3029. var resultUnsorted = Object.assign({}, eLocale, locale);
  3030. var resultSorted = Object.keys(resultUnsorted).sort().reduce(
  3031. (obj, key) => {
  3032. obj[key] = resultUnsorted[key];
  3033. return obj;
  3034. }, {}
  3035. );
  3036.  
  3037. return resultSorted;
  3038. }
  3039.  
  3040. /**
  3041. * getTranslationText
  3042. * @description i18n translation text
  3043. *
  3044. * @param {String} lang
  3045. * @return {Object}
  3046. */
  3047. async function getTranslationText(lang) {
  3048. return new Promise((resolve, reject) => {
  3049. GM_xmlhttpRequest({
  3050. method: "GET",
  3051. url: `https://raw.githubusercontent.com/SN-Koarashi/ig-helper/master/locale/translations/${lang}.json`,
  3052. onload: function (response) {
  3053. try {
  3054. let obj = JSON.parse(response.response);
  3055. resolve(obj);
  3056. }
  3057. catch (err) {
  3058. reject(err);
  3059. }
  3060. },
  3061. onerror: function (err) {
  3062. logger('getTranslationText()', 'reject', err);
  3063. reject(err);
  3064. }
  3065. });
  3066. });
  3067. }
  3068.  
  3069. /**
  3070. * _i18n
  3071. * @description Perform i18n translation
  3072. *
  3073. * @param {String} text
  3074. * @return {void}
  3075. */
  3076. function _i18n(text) {
  3077. const translate = translateText();
  3078.  
  3079. if (translate[lang] != undefined && translate[lang][text] != undefined) {
  3080. return translate[lang][text];
  3081. }
  3082. else {
  3083. return translate["en-US"][text];
  3084. }
  3085. }
  3086.  
  3087. /**
  3088. * repaintingTranslations
  3089. * @description Perform i18n translation
  3090. *
  3091. * @return {void}
  3092. */
  3093. function repaintingTranslations() {
  3094. $('[data-ih-locale]').each(function () {
  3095. $(this).text(_i18n($(this).attr('data-ih-locale')));
  3096. });
  3097. $('[data-ih-locale-title]').each(function () {
  3098. $(this).attr('title', _i18n($(this).attr('data-ih-locale-title')));
  3099. });
  3100. }
  3101.  
  3102. /**
  3103. * registerMenuCommand
  3104. * @description register script menu command
  3105. *
  3106. * @return {void}
  3107. */
  3108. function registerMenuCommand() {
  3109. for (let id of GM_menuId) {
  3110. logger('GM_unregisterMenuCommand', id);
  3111. GM_unregisterMenuCommand(id);
  3112. }
  3113.  
  3114. GM_menuId.push(GM_registerMenuCommand(_i18n('SETTING'), () => {
  3115. showSetting();
  3116. }, {
  3117. accessKey: "w"
  3118. }));
  3119.  
  3120. GM_menuId.push(GM_registerMenuCommand(_i18n('DONATE'), () => {
  3121. GM_openInTab("https://ko-fi.com/snkoarashi", { active: true });
  3122. }, {
  3123. accessKey: "d"
  3124. }));
  3125.  
  3126. GM_menuId.push(GM_registerMenuCommand(_i18n('DEBUG'), () => {
  3127. showDebugDOM();
  3128. }, {
  3129. accessKey: "z"
  3130. }));
  3131.  
  3132. GM_menuId.push(GM_registerMenuCommand(_i18n('FEEDBACK'), () => {
  3133. showFeedbackDOM();
  3134. }, {
  3135. accessKey: "f"
  3136. }));
  3137.  
  3138. GM_menuId.push(GM_registerMenuCommand(_i18n('CHECK_UPDATE_MENU'), () => {
  3139. callNotification();
  3140. }, {
  3141. accessKey: "c"
  3142. }));
  3143.  
  3144. GM_menuId.push(GM_registerMenuCommand(_i18n('RELOAD_SCRIPT'), () => {
  3145. reloadScript();
  3146. }, {
  3147. accessKey: "r"
  3148. }));
  3149. }
  3150.  
  3151. /**
  3152. * checkingScriptUpdate
  3153. * @description Check if there is a new version of the script and push notification
  3154. *
  3155. * @param {Integer} interval
  3156. * @return {void}
  3157. */
  3158. function checkingScriptUpdate(interval) {
  3159. if (!USER_SETTING.CHECK_UPDATE) return;
  3160.  
  3161. const check_timestamp = GM_getValue('G_CHECK_TIMESTAMP') ?? new Date().getTime();
  3162. const now_time = new Date().getTime();
  3163.  
  3164. if (now_time > (parseInt(check_timestamp) + (interval * 1000))) {
  3165. GM_setValue('G_CHECK_TIMESTAMP', new Date().getTime());
  3166. callNotification();
  3167. }
  3168. }
  3169.  
  3170. /**
  3171. * callNotification
  3172. * @description call desktop notification by browser
  3173. *
  3174. * @return {void}
  3175. */
  3176. function callNotification() {
  3177. const currentVersion = GM_info.script.version;
  3178. const remoteScriptURL = 'https://raw.githubusercontent.com/SN-Koarashi/ig-helper/refs/heads/master/main.js';
  3179.  
  3180. GM_xmlhttpRequest({
  3181. method: "GET",
  3182. url: remoteScriptURL,
  3183. onload: function (response) {
  3184. const remoteScript = response.responseText;
  3185. const match = remoteScript.match(/\/\/\s+@version\s+([0-9.\-a-zA-Z]+)/i);
  3186.  
  3187. if (match && match[1]) {
  3188. const remoteVersion = match[1];
  3189. logger('Current version: ', currentVersion, '|', 'Remote version: ', remoteVersion);
  3190.  
  3191. if (remoteVersion !== currentVersion) {
  3192. GM_notification({
  3193. text: _i18n("NOTICE_UPDATE_CONTENT"),
  3194. title: _i18n("NOTICE_UPDATE_TITLE"),
  3195. tag: 'ig_helper_notice',
  3196. highlight: true,
  3197. timeout: 5000,
  3198. zombieTimeout: 5000,
  3199. image: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Instagram_icon.png/64px-Instagram_icon.png",
  3200. onclick: (event) => {
  3201. event?.preventDefault();
  3202. var w = GM_openInTab(GM_info.script.downloadURL);
  3203. setTimeout(() => {
  3204. w.close();
  3205. }, 250);
  3206. }
  3207. });
  3208. } else {
  3209. logger('there is no new update');
  3210. }
  3211. } else {
  3212. console.error('Could not find version in the remote script.');
  3213. }
  3214. }
  3215. });
  3216. }
  3217.  
  3218. /**
  3219. * showSetting
  3220. * @description Show script settings window
  3221. *
  3222. * @return {void}
  3223. */
  3224. function showSetting() {
  3225. $('.IG_SN_DIG').remove();
  3226. IG_createDM();
  3227. $('.IG_SN_DIG #post_info').text('Preference Settings');
  3228.  
  3229. $('.IG_SN_DIG .IG_SN_DIG_TITLE > div').append('<select id="langSelect"></select><div style="font-size: 12px;">Some texts are machine-translated and may be inaccurate; translation contributions are welcome on GitHub.</div>');
  3230.  
  3231. for (let o in locale_manifest) {
  3232. $('.IG_SN_DIG .IG_SN_DIG_TITLE > div #langSelect').append(`<option value="${o}" ${(lang == o) ? 'selected' : ''}>${locale_manifest[o]}</option>`);
  3233. }
  3234.  
  3235. for (let name in USER_SETTING) {
  3236. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<label class="globalSettings${(CHILD_NODES.includes(name)) ? ' child' : ''}" title="${_i18n(name + '_INTRO')}" data-ih-locale-title="${name + '_INTRO'}"><span data-ih-locale="${name}">${_i18n(name)}</span> <input id="${name}" value="box" type="checkbox" ${(USER_SETTING[name] === true) ? 'checked' : ''}><div class="chbtn"><div class="rounds"></div></div></label>`);
  3237.  
  3238. if (name === 'MODIFY_VIDEO_VOLUME') {
  3239. $('.IG_SN_DIG .IG_SN_DIG_BODY input[id="' + name + '"]').parent('label').on('contextmenu', function (e) {
  3240. e.preventDefault();
  3241. if ($(this).find('#tempWrapper').length === 0) {
  3242. $(this).append('<div id="tempWrapper"></div>');
  3243. $(this).children('#tempWrapper').append('<input value="' + VIDEO_VOLUME + '" type="range" min="0" max="1" step="0.05" />');
  3244. $(this).children('#tempWrapper').append('<input value="' + VIDEO_VOLUME + '" step="0.05" type="number" />');
  3245. $(this).children('#tempWrapper').append(`<div class="IG_SN_DIG_BTN">${SVG.CLOSE}</div>`);
  3246. }
  3247. });
  3248. }
  3249.  
  3250. if (name === 'AUTO_RENAME') {
  3251. $('.IG_SN_DIG .IG_SN_DIG_BODY input[id="' + name + '"]').parent('label').on('contextmenu', function (e) {
  3252. e.preventDefault();
  3253. if ($(this).find('#tempWrapper').length === 0) {
  3254. $(this).append('<div id="tempWrapper"></div>');
  3255.  
  3256. $(this).children('#tempWrapper').append('<input id="date_format" value="' + RENAME_FORMAT + '" />');
  3257. $(this).children('#tempWrapper').append(`<div class="IG_SN_DIG_BTN">${SVG.CLOSE}</div>`);
  3258. }
  3259. });
  3260. }
  3261. }
  3262. }
  3263.  
  3264. /**
  3265. * showDebugDOM
  3266. * @description Show full DOM tree
  3267. *
  3268. * @return {void}
  3269. */
  3270. function showDebugDOM() {
  3271. $('.IG_SN_DIG').remove();
  3272. IG_createDM();
  3273. $('.IG_SN_DIG #post_info').text('IG Debug DOM Tree');
  3274.  
  3275. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<textarea style="font-family: monospace;width:100%;box-sizing: border-box;height:300px;background: transparent;" readonly></textarea>`);
  3276. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<span style="display:block;text-align:center;">`);
  3277. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_DISPLAY_DOM_TREE"><a>${_i18n('SHOW_DOM_TREE')}</a></button>`);
  3278. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_SELECT_DOM_TREE"><a>${_i18n('SELECT_AND_COPY')}</a></button>`);
  3279. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_DOWNLOAD_DOM_TREE"><a>${_i18n('DOWNLOAD_DOM_TREE')}</a></button><br/>`);
  3280. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_GITHUB"><a href="https://github.com/SN-Koarashi/ig-helper/issues" target="_blank">${_i18n('REPORT_GITHUB')}</a></button>`);
  3281. $('.IG_SN_DIG .IG_SN_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>`);
  3282. }
  3283.  
  3284. /**
  3285. * showFeedbackDOM
  3286. * @description Show feedback options
  3287. *
  3288. * @return {void}
  3289. */
  3290. function showFeedbackDOM() {
  3291. $('.IG_SN_DIG').remove();
  3292. IG_createDM();
  3293. $('.IG_SN_DIG #post_info').text('Feedback Options');
  3294.  
  3295. $('.IG_SN_DIG .IG_SN_DIG_BODY').append(`<span style="display:block;text-align:center;">`);
  3296. $('.IG_SN_DIG .IG_SN_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>`);
  3297. $('.IG_SN_DIG .IG_SN_DIG_BODY span').append(`<button style="margin: 3px;" class="IG_REPORT_GITHUB"><a href="https://github.com/SN-Koarashi/ig-helper/issues" target="_blank">${_i18n('REPORT_GITHUB')}</a></button>`);
  3298. $('.IG_SN_DIG .IG_SN_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>`);
  3299. }
  3300.  
  3301. /**
  3302. * openNewTab
  3303. * @description Open url in new tab
  3304. *
  3305. * @param {String} link
  3306. * @return {void}
  3307. */
  3308. function openNewTab(link) {
  3309. var a = document.createElement('a');
  3310. a.href = link;
  3311. a.target = '_blank';
  3312.  
  3313. document.body.appendChild(a);
  3314. a.click();
  3315. a.remove();
  3316. }
  3317.  
  3318. /**
  3319. * reloadScript
  3320. * @description Re-register main timer
  3321. *
  3322. * @return {void}
  3323. */
  3324. function reloadScript() {
  3325. clearInterval(GL_repeat);
  3326.  
  3327. // unregister event in post element
  3328. GL_registerEventList.forEach(item => {
  3329. item.trigger.forEach(bindElement => {
  3330. $(item.element).off('click', bindElement);
  3331. });
  3332. });
  3333. GL_registerEventList = [];
  3334.  
  3335. $('.button_wrapper').remove();
  3336. $('.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();
  3337. $('[data-snig]').removeAttr('data-snig');
  3338.  
  3339. pageLoaded = false;
  3340. firstStarted = false;
  3341. currentURL = location.href;
  3342. GL_observer.disconnect();
  3343.  
  3344. logger('main timer re-register completed');
  3345. }
  3346.  
  3347. /**
  3348. * logger
  3349. * @description event record
  3350. *
  3351. * @return {void}
  3352. */
  3353. function logger(...messages) {
  3354. var dd = new Date();
  3355. GL_logger.push({
  3356. time: dd.getTime(),
  3357. content: [...messages]
  3358. });
  3359.  
  3360. if (GL_logger.length > 1000) {
  3361. GL_logger = [{
  3362. time: dd.getTime(),
  3363. content: ['logger sliced']
  3364. }, ...GL_logger.slice(-999)];
  3365. }
  3366.  
  3367. console.log(`[${dd.toISOString()}]`, ...messages);
  3368. }
  3369.  
  3370. /**
  3371. * initSettings
  3372. * @description Initialize preferences
  3373. *
  3374. * @return {void}
  3375. */
  3376. function initSettings() {
  3377. for (let name in USER_SETTING) {
  3378. if (GM_getValue(name) != null && typeof GM_getValue(name) === 'boolean') {
  3379. USER_SETTING[name] = GM_getValue(name);
  3380.  
  3381. if (name === "MODIFY_VIDEO_VOLUME" && GM_getValue(name) !== true) {
  3382. VIDEO_VOLUME = 1;
  3383. }
  3384. }
  3385. }
  3386. }
  3387.  
  3388. /**
  3389. * toggleVolumeSilder
  3390. * @description Toggle display of custom volume slider.
  3391. *
  3392. * @param {object} $videos
  3393. * @param {object} $buttonParent
  3394. * @param {string} loggerType
  3395. * @param {string} customClass
  3396. * @return {void}
  3397. */
  3398. function toggleVolumeSilder($videos, $buttonParent, loggerType, customClass = "") {
  3399. if ($buttonParent.find('div.volume_slider').length === 0) {
  3400. $buttonParent.append(`<div class="volume_slider ${customClass}" />`);
  3401. $buttonParent.find('div.volume_slider').append(`<div><input type="range" max="1" min="0" step="0.05" value="${VIDEO_VOLUME}" /></div>`);
  3402. $buttonParent.find('div.volume_slider input').attr('style', `--ig-track-progress: ${(VIDEO_VOLUME * 100) + '%'}`);
  3403. $buttonParent.find('div.volume_slider input').on('input', function () {
  3404. var percent = ($(this).val() * 100) + '%';
  3405.  
  3406. VIDEO_VOLUME = $(this).val();
  3407. GM_setValue('G_VIDEO_VOLUME', $(this).val());
  3408.  
  3409. $(this).attr('style', `--ig-track-progress: ${percent}`);
  3410.  
  3411. $videos.each(function () {
  3412. logger(`(${loggerType})`, 'video volume changed #slider');
  3413. this.volume = VIDEO_VOLUME;
  3414. });
  3415. });
  3416.  
  3417. $buttonParent.find('div.volume_slider input').on('mouseenter', function () {
  3418. var percent = (VIDEO_VOLUME * 100) + '%';
  3419. $(this).attr('style', `--ig-track-progress: ${percent}`);
  3420. $(this).val(VIDEO_VOLUME);
  3421.  
  3422.  
  3423. $videos.each(function () {
  3424. logger(`(${loggerType})`, 'video volume changed #slider');
  3425. this.volume = VIDEO_VOLUME;
  3426. });
  3427. });
  3428.  
  3429. $buttonParent.find('div.volume_slider').on('click', function (e) {
  3430. e.stopPropagation();
  3431. e.preventDefault();
  3432. });
  3433. }
  3434. else {
  3435. $buttonParent.find('div.volume_slider').remove();
  3436. }
  3437. }
  3438.  
  3439. // Running if document is ready
  3440. $(function () {
  3441. function ConvertDOM(domEl) {
  3442. var obj = [];
  3443. for (var ele of domEl) {
  3444. obj.push({
  3445. tagName: ele.tagName,
  3446. id: ele.id,
  3447. className: ele.className
  3448. });
  3449. }
  3450.  
  3451. return obj;
  3452. }
  3453.  
  3454. function setDOMTreeContent() {
  3455. let text = $('div[id^="mount"]')[0];
  3456. var logger = "";
  3457. GL_logger.forEach(log => {
  3458. var jsonData = JSON.stringify(log.content, function (key, value) {
  3459. if (Array.isArray(this)) {
  3460. if (typeof value === "object" && value instanceof jQuery) {
  3461. return ConvertDOM(value);
  3462. }
  3463. return value;
  3464. }
  3465. else {
  3466. return value;
  3467. }
  3468. }, "\t");
  3469. logger += `${log.time}: ${jsonData}\n`
  3470. });
  3471. $('.IG_SN_DIG .IG_SN_DIG_BODY textarea').text("Logger:\n" + logger + "\n-----\n\nLocation: " + location.pathname + "\nDOM Tree with div#mount:\n" + text.innerHTML);
  3472. }
  3473.  
  3474. $('body').on('click', '.IG_SN_DIG .IG_SN_DIG_BODY .IG_DISPLAY_DOM_TREE', function () {
  3475. setDOMTreeContent();
  3476. });
  3477.  
  3478. $('body').on('click', '.IG_SN_DIG .IG_SN_DIG_BODY .IG_SELECT_DOM_TREE', function () {
  3479. $('.IG_SN_DIG .IG_SN_DIG_BODY textarea').select();
  3480. document.execCommand('copy');
  3481. });
  3482.  
  3483. $('body').on('click', '.IG_SN_DIG .IG_SN_DIG_BODY .IG_DOWNLOAD_DOM_TREE', function () {
  3484. if ($('.IG_SN_DIG .IG_SN_DIG_BODY textarea').text().length === 0) {
  3485. setDOMTreeContent();
  3486. }
  3487.  
  3488. var text = $('.IG_SN_DIG .IG_SN_DIG_BODY textarea').text();
  3489. var a = document.createElement("a");
  3490. var file = new Blob([text], { type: "text/plain" });
  3491. a.href = URL.createObjectURL(file);
  3492. a.download = "DOMTree-" + new Date().getTime() + ".txt";
  3493.  
  3494. document.body.appendChild(a);
  3495. a.click();
  3496. a.remove();
  3497. });
  3498.  
  3499. // Close the download dialog if user click the close icon
  3500. $('body').on('click', '.IG_SN_DIG_BTN, .IG_SN_DIG_BG', function () {
  3501. if ($(this).parent('#tempWrapper').length > 0) {
  3502. $(this).parent('#tempWrapper').fadeOut(250, function () {
  3503. $(this).remove();
  3504. });
  3505. }
  3506. else {
  3507. $('.IG_SN_DIG').remove();
  3508. }
  3509. });
  3510.  
  3511. $(window).keydown(function (e) {
  3512. // Hot key [Alt+Q] to close the download dialog
  3513. if (e.keyCode == '81' && e.altKey) {
  3514. $('.IG_SN_DIG').remove();
  3515. e.preventDefault();
  3516. }
  3517. // Hot key [Alt+W] to open the settings dialog
  3518. if (e.keyCode == '87' && e.altKey) {
  3519. showSetting();
  3520. e.preventDefault();
  3521. }
  3522.  
  3523. // Hot key [Alt+Z] to open the settings dialog
  3524. if (e.keyCode == '90' && e.altKey) {
  3525. showDebugDOM();
  3526. e.preventDefault();
  3527. }
  3528.  
  3529. // Hot key [Alt+R] to open the settings dialog
  3530. if (e.keyCode == '82' && e.altKey) {
  3531. reloadScript();
  3532. e.preventDefault();
  3533. }
  3534.  
  3535. // Hot key [Alt+S] to download story/highlights resource
  3536. if (e.keyCode == '83' && e.altKey) {
  3537. if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/)/ig) && $('.IG_DWSTORY').length > 0) {
  3538. $('.IG_DWSTORY')?.click();
  3539. }
  3540. if (location.href.match(/^(https:\/\/www\.instagram\.com\/stories\/highlights\/)/ig) && $('.IG_DWHISTORY').length > 0) {
  3541. $('.IG_DWHISTORY')?.click();
  3542. }
  3543. e.preventDefault();
  3544. }
  3545. });
  3546.  
  3547. $('body').on('change', '.IG_SN_DIG input', function (e) {
  3548. var name = $(this).attr('id');
  3549.  
  3550. if (name && USER_SETTING[name] !== undefined) {
  3551. let isChecked = $(this).prop('checked');
  3552. GM_setValue(name, isChecked);
  3553. USER_SETTING[name] = isChecked;
  3554.  
  3555. console.log('user settings', name, isChecked);
  3556. }
  3557. });
  3558.  
  3559. $('body').on('click', '.IG_SN_DIG .globalSettings', function (e) {
  3560. if ($(this).find('#tempWrapper').length > 0) {
  3561. e.preventDefault();
  3562. }
  3563. });
  3564.  
  3565. $('body').on('change', '.IG_SN_DIG #tempWrapper input:not(#date_format)', function () {
  3566. let value = $(this).val();
  3567.  
  3568. if ($(this).attr('type') == 'range') {
  3569. $(this).next().val(value);
  3570. }
  3571. else {
  3572. $(this).prev().val(value);
  3573. }
  3574.  
  3575. if (value >= 0 && value <= 1) {
  3576. VIDEO_VOLUME = value;
  3577. GM_setValue('G_VIDEO_VOLUME', value);
  3578. }
  3579. });
  3580.  
  3581. $('body').on('input', '.IG_SN_DIG #tempWrapper input:not(#date_format)', function (e) {
  3582. if ($(this).attr('type') == 'range') {
  3583. let value = $(this).val();
  3584. $(this).next().val(value);
  3585. }
  3586. else {
  3587. let value = $(this).val();
  3588. if (value >= 0 && value <= 1) {
  3589. $(this).prev().val(value);
  3590. }
  3591. else {
  3592. if (value < 0) {
  3593. $(this).val(0);
  3594. }
  3595. else {
  3596. $(this).val(1);
  3597. }
  3598. }
  3599. }
  3600. });
  3601.  
  3602. $('body').on('input', '.IG_SN_DIG #tempWrapper input#date_format', function (e) {
  3603. GM_setValue('G_RENAME_FORMAT', $(this).val());
  3604. RENAME_FORMAT = $(this).val();
  3605. });
  3606.  
  3607. $('body').on('click', 'a[data-needed="direct"]', function (e) {
  3608. e.preventDefault();
  3609. triggerLinkElement(this);
  3610. });
  3611.  
  3612. $('body').on('click', '.IG_SN_DIG_BODY .newTab', function () {
  3613. // replace https://instagram.ftpe8-2.fna.fbcdn.net/ to https://scontent.cdninstagram.com/ becase of same origin policy (some video)
  3614.  
  3615. if (USER_SETTING.FORCE_RESOURCE_VIA_MEDIA && USER_SETTING.NEW_TAB_ALWAYS_FORCE_MEDIA_IN_POST) {
  3616. triggerLinkElement($(this).parent().children('a').first()[0], true);
  3617. }
  3618. else {
  3619. var urlObj = new URL($(this).parent().children('a').attr('data-href'));
  3620. urlObj.host = 'scontent.cdninstagram.com';
  3621.  
  3622. openNewTab(urlObj.href);
  3623. }
  3624. });
  3625.  
  3626. $('body').on('click', '.IG_SN_DIG_BODY .videoThumbnail', function () {
  3627. let timestamp = new Date().getTime();
  3628.  
  3629. if (USER_SETTING.RENAME_PUBLISH_DATE && $(this).parent().children('a').attr('datetime')) {
  3630. timestamp = $(this).parent().children('a').attr('datetime');
  3631. }
  3632.  
  3633. let postPath = $(this).parent().children('a').attr('data-path') ?? $('#article-id').text();
  3634.  
  3635. saveFiles($(this).parent().children('a').find('img').first().attr('src'), $(this).parent().children('a').attr('data-username'), 'thumbnail', timestamp, 'jpg', postPath);
  3636. });
  3637.  
  3638. // Running if user left-click download icon in stories
  3639. $('body').on('click', '.IG_DWSTORY', function () {
  3640. onStory(true);
  3641. });
  3642.  
  3643. // Running if user left-click all download icon in stories
  3644. $('body').on('click', '.IG_DWSTORY_ALL', function () {
  3645. onStoryAll();
  3646. });
  3647.  
  3648. // Running if user left-click 'open in new tab' icon in stories
  3649. $('body').on('click', '.IG_DWNEWTAB', function (e) {
  3650. e.preventDefault();
  3651. onStory(true, true, true);
  3652. });
  3653.  
  3654. // Running if user left-click download thumbnail icon in stories
  3655. $('body').on('click', '.IG_DWSTORY_THUMBNAIL', function () {
  3656. onStoryThumbnail(true);
  3657. });
  3658.  
  3659. // Running if user left-click download icon in profile
  3660. $('body').on('click', '.IG_DWPROFILE', function (e) {
  3661. e.stopPropagation();
  3662. onProfileAvatar(true);
  3663. });
  3664.  
  3665. // Running if user left-click download icon in highlight stories
  3666. $('body').on('click', '.IG_DWHISTORY', function () {
  3667. onHighlightsStory(true);
  3668. });
  3669.  
  3670. // Running if user left-click all download icon in highlight stories
  3671. $('body').on('click', '.IG_DWHISTORY_ALL', function () {
  3672. onHighlightsStoryAll();
  3673. });
  3674.  
  3675. // Running if user left-click 'open in new tab' icon in highlight stories
  3676. $('body').on('click', '.IG_DWHINEWTAB', function (e) {
  3677. e.preventDefault();
  3678. onHighlightsStory(true, true);
  3679. });
  3680.  
  3681. // Running if user left-click thumbnail download icon in highlight stories
  3682. $('body').on('click', '.IG_DWHISTORY_THUMBNAIL', function () {
  3683. onHighlightsStoryThumbnail(true);
  3684. });
  3685.  
  3686. // Running if user left-click download icon in reels
  3687. $('body').on('click', '.IG_REELS', function () {
  3688. onReels(true, true);
  3689. });
  3690.  
  3691. // Running if user left-click newtab icon in reels
  3692. $('body').on('click', '.IG_REELS_NEWTAB', function () {
  3693. onReels(true, true, true);
  3694. });
  3695.  
  3696. // Running if user left-click download icon in reels
  3697. $('body').on('click', '.IG_REELS_THUMBNAIL', function () {
  3698. onReels(true, false);
  3699. });
  3700.  
  3701. // Running if user right-click profile picture in stories area
  3702. $('body').on('mousedown', 'button[role="menuitem"], div[role="menuitem"]', function (e) {
  3703. // Right-Click || Middle-Click
  3704. if (e.which === 3 || e.which === 2) {
  3705. if (location.href === 'https://www.instagram.com/' && USER_SETTING.REDIRECT_CLICK_USER_STORY_PICTURE) {
  3706. e.preventDefault();
  3707. if ($(this).find('canvas._aarh, canvas + span > img').length > 0) {
  3708. const targetUrl = 'https://www.instagram.com/' + $(this).children('div').last().text();
  3709. if (e.which === 2) {
  3710. GM_openInTab(targetUrl);
  3711. }
  3712. else {
  3713. location.href = targetUrl;
  3714. }
  3715. }
  3716. }
  3717. }
  3718. });
  3719.  
  3720. $('body').on('change', '.IG_SN_DIG_TITLE .checkbox', function () {
  3721. var isChecked = $(this).find('input').prop('checked');
  3722. $('.IG_SN_DIG_BODY .inner_box').each(function () {
  3723. $(this).prop('checked', isChecked);
  3724. });
  3725. });
  3726.  
  3727. $('body').on('change', '.IG_SN_DIG_BODY .inner_box', function () {
  3728. var checked = $('.IG_SN_DIG_BODY .inner_box:checked').length;
  3729. var total = $('.IG_SN_DIG_BODY .inner_box').length;
  3730.  
  3731.  
  3732. $('.IG_SN_DIG_TITLE .checkbox').find('input').prop('checked', checked == total);
  3733. });
  3734.  
  3735. $('body').on('click', '.IG_SN_DIG_TITLE #batch_download_selected', function () {
  3736. let index = 0;
  3737. $('.IG_SN_DIG_BODY a[data-needed="direct"]').each(function () {
  3738. if ($(this).prev().children('input').prop('checked')) {
  3739. $(this).click();
  3740. index++;
  3741. }
  3742. });
  3743.  
  3744. if (index == 0) {
  3745. alert(_i18n('NO_CHECK_RESOURCE'));
  3746. }
  3747. });
  3748.  
  3749. $('body').on('change', '.IG_SN_DIG_TITLE #langSelect', function () {
  3750. GM_setValue('lang', $(this).val());
  3751. lang = $(this).val();
  3752.  
  3753. if (lang?.startsWith('en') || locale[lang] != null) {
  3754. repaintingTranslations();
  3755. registerMenuCommand();
  3756. }
  3757. else {
  3758. getTranslationText(lang).then((res) => {
  3759. locale[lang] = res;
  3760. repaintingTranslations();
  3761. registerMenuCommand();
  3762. }).catch((err) => {
  3763. console.error('getTranslationText catch error:', err);
  3764. });
  3765. }
  3766. });
  3767.  
  3768. $('body').on('change', '.IG_SN_DIG_BODY #locateSelect', function () {
  3769. $('#locatePreview').text(`${(new Date().toLocaleString($(this).val(), { hour12: false, second: "2-digit", minute: "2-digit", hour: "2-digit", month: "2-digit", day: "2-digit", year: "numeric" })).replaceAll('/', '-')}`);
  3770. LOCATE_DATE_FORMAT = $(this).val();
  3771. GM_setValue('G_LOCATE_DATE_FORMAT', $(this).val());
  3772. });
  3773.  
  3774. $('body').on('click', '.IG_SN_DIG_TITLE #batch_download_direct', function () {
  3775. $('.IG_SN_DIG_BODY a[data-needed="direct"]').each(function () {
  3776. $(this).click();
  3777. });
  3778. });
  3779.  
  3780.  
  3781. const audio_observer = new MutationObserver((mutationsList) => {
  3782. for (const mutation of mutationsList) {
  3783. if (mutation.type === 'childList') {
  3784. mutation.addedNodes.forEach((node) => {
  3785. const $videos = $(node).find('video');
  3786. if ($videos.length > 0) {
  3787. // Modify video volume
  3788. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  3789. $videos.each(function () {
  3790. $(this).on('play playing', function () {
  3791. if (!$(this).data('modify')) {
  3792. $(this).attr('data-modify', true);
  3793. this.volume = VIDEO_VOLUME;
  3794. logger('(audio_observer) Added video event listener #modify');
  3795. }
  3796. });
  3797. });
  3798. }
  3799.  
  3800. if (location.pathname.match(/^(\/stories\/)/ig)) {
  3801. const isHighlight = location.pathname.match(/^(\/stories\/highlights\/)/ig) != null;
  3802. const storyType = isHighlight ? 'highlight' : 'story';
  3803.  
  3804. $videos.each(function () {
  3805. $(this).on('timeupdate', function () {
  3806. if (!$(this).data('modify-thumbnail')) {
  3807. let $video = $(this);
  3808. if ($video.parents('div[style][class]').filter(function () {
  3809. return $(this).width() == $video.width();
  3810. }).find('.IG_DWSTORY_THUMBNAIL, .IG_DWHISTORY_THUMBNAIL').length === 0) {
  3811. $(this).attr('data-modify-thumbnail', true);
  3812.  
  3813. if (isHighlight) {
  3814. onHighlightsStoryThumbnail(false);
  3815. }
  3816. else {
  3817. onStoryThumbnail(false);
  3818. }
  3819.  
  3820. logger(`(${storyType})`, 'Manually inserting thumbnail button');
  3821. }
  3822. else {
  3823. $(this).attr('data-modify-thumbnail', true);
  3824. logger(`(${storyType})`, 'Thumbnail button already inserted');
  3825. }
  3826. }
  3827. });
  3828.  
  3829. var $video = $(this);
  3830.  
  3831. if (USER_SETTING.HTML5_VIDEO_CONTROL) {
  3832. if (!$video.data('controls')) {
  3833. logger(`(${storyType})`, 'Added video html5 contorller #modify');
  3834.  
  3835. if (USER_SETTING.MODIFY_VIDEO_VOLUME) {
  3836. this.volume = VIDEO_VOLUME;
  3837.  
  3838. $video.on('loadstart', function () {
  3839. this.volume = VIDEO_VOLUME;
  3840. });
  3841. }
  3842.  
  3843. let $videoParent = $video.parents('div').filter(function () {
  3844. return $(this).attr('class') == null && $(this).attr('style') == null;
  3845. }).first();
  3846.  
  3847. // story bottom bar
  3848. let $bottomBar = $videoParent.next();
  3849. $bottomBar.hide();
  3850.  
  3851. // read more button in center
  3852. let $readMoreButton = $videoParent.find('div[class][role="button"]');
  3853. $readMoreButton.hide();
  3854.  
  3855. const hideContextmenu = function (e) {
  3856. e.preventDefault();
  3857. $video.css('z-index', '2');
  3858. $video.attr('controls', true);
  3859.  
  3860. $readMoreButton.hide();
  3861. $bottomBar.hide();
  3862.  
  3863. toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () {
  3864. return $(this).width() == $video.width();
  3865. }).first(), storyType, 'vertical');
  3866. };
  3867.  
  3868. // Hide layout to show controller
  3869. $video.parent().find('video + div').on('contextmenu', hideContextmenu);
  3870. $readMoreButton.on('contextmenu', hideContextmenu);
  3871. $bottomBar.on('contextmenu', hideContextmenu);
  3872.  
  3873. // Restore layout to show details interface
  3874. $video.on('contextmenu', function (e) {
  3875. e.preventDefault();
  3876. $video.css('z-index', '-1');
  3877. $video.removeAttr('controls');
  3878.  
  3879. $bottomBar.show();
  3880. $readMoreButton.show();
  3881.  
  3882. toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () {
  3883. return $(this).width() == $video.width();
  3884. }).first(), storyType, 'vertical');
  3885. });
  3886.  
  3887. $video.on('volumechange', function () {
  3888. // This is mute/unmute's icon
  3889. 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();
  3890.  
  3891. var is_elelment_muted = $element_mute_button.find('svg > path[d^="M16.636"]').length === 0;
  3892.  
  3893. if (this.muted != is_elelment_muted) {
  3894. this.volume = VIDEO_VOLUME;
  3895. $element_mute_button?.click();
  3896. }
  3897.  
  3898. if ($(this).attr('data-completed')) {
  3899. VIDEO_VOLUME = this.volume;
  3900. GM_setValue('G_VIDEO_VOLUME', this.volume);
  3901. }
  3902.  
  3903. if (this.volume == VIDEO_VOLUME) {
  3904. $(this).attr('data-completed', true);
  3905. }
  3906. });
  3907.  
  3908. $video.css('position', 'absolute');
  3909. $video.css('z-index', '2');
  3910. $video.attr('data-controls', true);
  3911. $video.attr('controls', true);
  3912. }
  3913. }
  3914. else {
  3915. toggleVolumeSilder($video, $video.parents('div[style][class]').filter(function () {
  3916. return $(this).width() == $video.width();
  3917. }).first(), storyType, 'vertical');
  3918. }
  3919. });
  3920. }
  3921. }
  3922. });
  3923. }
  3924. }
  3925. });
  3926.  
  3927. audio_observer.observe($('div[id^="mount"]')[0], {
  3928. childList: true,
  3929. subtree: true,
  3930. });
  3931. });
  3932. })(jQuery);