IG小助手

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

当前为 2024-10-03 提交的版本,查看 最新版本

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

QingJ © 2025

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