您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Best! Greatly enhance your Instagram navigation experience! And magically view or download the highest-quality, largest versions of Instagram stories, albums, images/photos, videos and profile avatars. If you have ever wanted to save a story, album, image/photo, video or avatar, then this is for you!
// ==UserScript== // @name InstaMagnify: Instagram Media Downloader // @namespace SteveJobzniak // @version 4.0.0 // @description Best! Greatly enhance your Instagram navigation experience! And magically view or download the highest-quality, largest versions of Instagram stories, albums, images/photos, videos and profile avatars. If you have ever wanted to save a story, album, image/photo, video or avatar, then this is for you! // @author SteveJobzniak // @homepage https://gf.qytechs.cn/scripts/34821-instamagnify-instagram-media-downloader // @license https://www.apache.org/licenses/LICENSE-2.0 // @contributionURL https://www.paypal.me/Armindale/0usd // @match *://*.instagram.com/* // @run-at document-start // @grant none // ==/UserScript== /* * # What is InstaMagnify? * * It's your portal to the highest-quality, largest versions of Instagram stories, albums, photos, * videos and profile avatars! * * By default, Instagram only shows you low-quality media. But by installing this utility, you'll get the * magical ability to easily _view_ or _download_ the largest and best looking versions of _any_ media! * * Liberation! * * But wait... _that's not all!_ You will _also_ discover that all profile pages (timelines) will be * enhanced with a loading indicator in the bottom right corner of the window, which shows you how much * of the media has been loaded by your scrolling so far, such as `"36 / 628 (5.7%)"`. You'll no longer * have to keep wondering how much further you have to scroll to reach the end of the timeline! ;-) * * _And_ to make your life _even easier_, it will _automatically_ click the "Load more" button for you, * so that you can effortlessly scroll and scroll and scroll and view and download media and... :-) * * Have fun and enjoy! * * # Features. * * - The most advanced code of all Instagram-related userscripts, written by the author of the * largest 3rd party Instagram API library in the world. * - View or download the highest-quality, largest versions of Instagram stories, albums, * photos, videos and profile avatars. * - Displays a timeline media scrolling progress indicator in the bottom right corner of all timelines. * - Automatically clicks on all "Load more" buttons so that you can effortlessly scroll through media. * - Automatically closes the annoying "Get the mobile app!" popup dialog which Instagram shows to some people. * - Automatically closes the even more annoying "Sign up for Instagram!" bar which is shown whenever you aren't logged in. * - Automatically closes the "Experience the best version of Instagram by getting the app" bar which is shown after logging in. * - Supports protected (signed) media URLs, such as those used by stories. * - Supports _all_ Instagram media types and media view-panels. * - Your choice of convenient mouse and/or keyboard controls, whichever you prefer the most. :-) * * # Feel like thanking me for my hard work? * * Totally optional but _truly, deeply_ appreciated and brings a great smile to my face, * and inspires me to keep working. ;-) * * - PayPal: https://www.paypal.me/Armindale/0usd * - Bitcoin: 18XF1EmrkpYi4fqkR2XcHkcJxuTMYG4bcv * * And if you really like this utility, please consider giving it a good rating by simply * adding it to your GreasyFork favorites. * * # Instructions. * * Simply hold down a modifier key and click on any Instagram photo, video or profile avatar! * * - `Shift-click`: View in the same tab. * - `Alt-click`: View in a new tab/window. * - `Shift-Alt-click`: Direct download (only if your browser supports it). * * Alternatively, you can use the keyboard controls, which are definitely a lot more convenient * if you're already using Instagram's own `left`/`right`-arrow navigation to switch between media! * * - `Shift-F`: View in the same tab. * - `Alt-F`: View in a new tab/window. * - `Shift-Alt-F`: Direct download (only if your browser supports it). * * If you ever forget the commands, simply hover your mouse cursor over the media loading indicator * in the bottom right corner of each timeline, and you'll see a tooltip with all of these commands. * * Note that the "direct download" feature requires a browser which supports the modern HTML 5 * "download" tag, such as Google Chrome and Safari! * * You should also note that _your web browser's_ "how to open new windows"-preference controls * whether the "View in a new tab/window" action will open a tab or a window. Browsers _don't_ * give scripts any control over that choice. Which means that you have to _change your browser * settings_ if you want to specifically choose which window-type you're using. * * Lastly, it's worth noting that you can actually click on media directly on the timeline grid * (not just on their media pages/media lightboxes). However, if you click via the timeline grid, * you will _only_ be able to see the first image in case of albums or the thumbnail for videos, * since the rest of the media details haven't been loaded when you haven't opened its media page. * And you _may_ not get the highest-resolution media via this method (the best quality is only * _guaranteed_ when you've clicked on the media to view it properly). But this ability to open media * directly from the timeline is still a _great_ shortcut which is definitely worth knowing about. * Have fun! * * # Want to check out my other work? * * - [GreasyFork Scripts](https://gf.qytechs.cn/users/67112-stevejobzniak) * - [GitHub](https://github.com/SteveJobzniak) * */ (function() { 'use strict'; var injectMediaMagnifier = function() { // Perform the user's desired action on a media URL. var handleMedia = function(e, url) { var i; // Do nothing if the URL isn't a string or if it's empty. if (typeof url !== 'string' || url.length < 1) { return true; // Let the default browser handler run. } // Create an anchor to allow us to easily manipulate the URL. var anchor = document.createElement('a'); anchor.href = url; // Determine if this is a protected (signed) media URL which is NOT allowed to be modified. var isProtectedUrl = !!(anchor.pathname.match(/\/vp\//) || anchor.search.match(/[?&](?:oh|oe|efg)=/)); // Attempt to extract the media filename from the URL that we've been given. var filename = null, filenameOffset = anchor.pathname.lastIndexOf('/'); if (filenameOffset >= 0) { filename = anchor.pathname.substring(filenameOffset + 1); if (filename.length < 1) { filename = null; } } // Always enforce HTTPS for download integrity (protects against sudden truncation). anchor.protocol = 'https:'; // Remove useless "se=7", "ig_tt=..." and "ig_cache_key=..." query-params if they exist. // NOTE: We can't just remove the entire query, since some media requires // special protection keys to allow the download to proceed. if (typeof anchor.search === 'string' && anchor.search.length > 0) { var queryParts = anchor.search.split('&'); for (i = queryParts.length - 1; i >= 0; --i) { if (queryParts[i].match(/^\??(?:ig_cache_key|se|ig_tt)=/)) { queryParts.splice(i, 1); } } var newQuery = queryParts.join('&'); if (newQuery.length > 0 && newQuery.charAt(0) !== '?') { newQuery = '?'+newQuery; // Only added if a search query still exists. } anchor.search = newQuery; } // Clean up the URL's PATH (via the anchor) to get the unmodified, highest quality media file: // NOTE: Protected URLs do not allow modifying ANY part of the PATH to the file. if (!isProtectedUrl) { // Remove bad flags that would cause us to retrieve modified media. // // KEEP: // - /t#.#-#/ = Media type flag. Is REQUIRED for stories. // // DELETE: // - /e#/ = Sets EXIF "FBMD" tag. // - /c#.#.#.#/ = Image cropping. // - /s#x#/ and /p#x#/ = Image downsizing. // - /sh#.#/ = Image sharpening. // - /fr/ = "Fine Resolution"? Not sure, but causes JPG artifacts. var flags = anchor.pathname.split('/'); //flags.splice(flags.length - 1, 1); // Optional: Remove filename to avoid parsing as flag. for (i = flags.length - 1; i >= 0; --i) { if (flags[i].length > 0 && flags[i].match(/^(?:e\d+|c\d+\.\d+\.\d+\.\d+|[sp]\d+x\d+|sh\d+\.?\d*|fr)$/)) { flags.splice(i, 1); } } //anchor.pathname = flags.join('/')+'/'+filename; // Optional: Re-add filename. anchor.pathname = flags.join('/'); } // The final URL is now in "anchor.href". // Perform appropriate action based on the pressed modifier keys. if (e.shiftKey && e.altKey) { // [Shift+Alt]: Download. if (!window.fetch) { // Turn the anchor into a download-anchor and just click it. // NOTE: This HTML 5 feature won't work in all browsers, and in fact CORS has been // disabled in Chrome 65+ due to security, which isn't unexpected since all other // browsers such as Safari already prevented cross-origin "download"-attr links. anchor.target = '_self'; anchor.download = filename; // Save with bare filename. anchor.click(); } else { // The browser supports window.fetch(). Perform asynchronous blob-based download. // Docs: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch // NOTE: Browsers support up to around 500MB blobs. The largest Instagram // media I've ever found was a 14MB video. Most videos are around 1-2MB. window.fetch(anchor.href, { headers: new Headers({ 'Origin': location.origin }), mode: 'cors', redirect: 'follow', referrerPolicy: 'no-referrer', // NOTE: Safari sucks at CORS caching and will re-fetch the URL every time. // But Chrome on the other hand caches the downloaded file perfectly. cache: 'force-cache' // https://fetch.spec.whatwg.org/#concept-request-cache-mode }).then(function(response) { // This triggers immediately when the headers are received (before the body). // NOTE: There's no way to show the user the download progress (unlike normal // download URLs which end up in a browser's download list and show progress // that way), but most media files are tiny and finish quickly. if (!response.ok || response.status !== 200) { throw new Error('Network response was not ok.'); } return response.blob(); }).then(function(blob) { // This triggers when the download is 100% complete. var blobUrl = URL.createObjectURL(blob), a = document.createElement('a'); // Download the blob URL via an anchor. And since a `blob:` URL doesn't // violate the CORS destination rules, this works in all modern browsers. // Verified browsers: Safari and Google Chrome. a.href = blobUrl; a.download = filename; a.click(); }).catch(function(e) { var errMsg = '"'+e+'" when downloading "'+filename+'".'; console.error(errMsg); alert(errMsg); }); } } else if (e.altKey) { // [Alt]: Open in a new tab/window. var win = window.open(anchor.href, '_blank'); win.focus(); // Bring the tab/window to the foreground. } else { // [Shift/Nothing/Anything Else]: Open in the same tab. location.href = anchor.href; } // Stop the event propagation so that nothing else runs. // And since our event handler is a capture (runs before the target element), // it means that this will prevent navigation to the clicked webpage, if any. e.stopPropagation(); // Prevent parent element event handlers from firing. e.stopImmediatePropagation(); // Prevent any further event handlers on the event-element from firing. e.preventDefault(); // Prevent default browser behavior for this event. return false; }; // Process a media element or container (from the event handlers). var handleElement = function (e, elem, isLastAttempt) { switch (elem.tagName) { case 'IMG': case 'VIDEO': case 'DIV': case 'A': // IMG: profile avatars (on both timeline and media page). // VIDEO: not used, but is here to be futureproof. // DIV: photos/album photos on media page and timeline, video thumbs on timeline. // A: videos/album videos on media page. // Determine which element to scan, and then look for photos and videos. // NOTE: Instagram puts the actual media page content as a sibling of the A/DIV // (within their mutual parent node), which is why we must get the parent. // And in case of albums, there's only 1 media item at a time (they dynamically // switch its contents to only have one IMG or VIDEO element at a time). var elemIsMedia = (elem.tagName === 'IMG' || elem.tagName === 'VIDEO'), mediaContainer = (elemIsMedia ? elem : (elem.parentNode || elem)), photos = (mediaContainer.tagName === 'IMG' ? [mediaContainer] : (elemIsMedia ? [] : mediaContainer.getElementsByTagName('img'))), videos = (mediaContainer.tagName === 'VIDEO' ? [mediaContainer] : (elemIsMedia ? [] : mediaContainer.getElementsByTagName('video'))); // Only handle the media if there's exactly 1 video or 1 photo. if (videos.length === 1 && photos.length === 0) { // NOTE: Some videos use the `src` attribute. Others (notably stories) use child `<source>` elements instead. var src = videos[0].hasAttribute('src') ? videos[0].src : null; if (typeof src !== 'string' || src.length < 1) { // If there are multiple sources, they're listed in descending quality (the first element is the best). // NOTE: There's absolutely NOTHING else (no attributes, etc) which indicates which file is the best one. var subSources = videos[0].getElementsByTagName('source'); for (var i = 0; i < subSources.length; ++i) { if (subSources[i].hasAttribute('src') && typeof subSources[i].src === 'string' && subSources[i].src.length >= 1) { src = subSources[i].src; break; // Stop searching through sources. } } } return handleMedia(e, src); } else if (photos.length === 1 && videos.length === 0) { // NOTE: Many images also have a "srcset" attribute with multiple URLs, but we just need the current "src". return handleMedia(e, photos[0].src); } else if (!isLastAttempt) { // If we didn't find anything, the user may have clicked on a story photo/video. Those have their media within TWO parent elements // rather than one. So by simply retrying once (as a "last attempt"), we will now traverse one step higher and find the media. return handleElement(e, mediaContainer, true); } } return true; // Let the default browser handler run if no valid media was found. }; // Attach the click event handler. document.addEventListener('click', function(e) { e = e || window.event; // Do nothing if none of our special keys are held while clicking. if (!e.shiftKey && !e.altKey) { return true; // Let the default browser handler run. } // Handle the click. var target = e.target || e.srcElement; return handleElement(e, target); }, true); // True = Capture BEFORE sending any click-event to the clicked element! // Attach the keyboard event handler. document.addEventListener('keydown', function(e) { e = e || window.event; // Do nothing if the user is typing in a text field. if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) { return true; // Let the default browser handler run. } // Look for any combination of Alt/Shift together with F. if ((e.shiftKey || e.altKey) && e.keyCode === 70) { // Find the media viewer and the currently viewed media. // NOTE: This is the most likely piece of code which may need future updates. // It catches all current Instagram media viewers: Lightbox media viewer, // whole-page media viewer, and the lightbox story media viewer. However, // they're all part of very complex HTML structures without many CSS landmarks, // so this may break whenever Instagram decides to change their website HTML. // But it's the best that we can do... Either way, the click-method always // remains extremely resilient, so users won't be stranded even if this // keyboard method stops working someday... ;-) var mediaPanel = document.querySelector( 'div[role="dialog"] article > div:nth-of-type(1), main[role="main"] > * article > div:nth-of-type(1), #react-root > section > div > div > section > div:nth-of-type(2)' ); if (mediaPanel) { var video = mediaPanel.querySelector('video'), photo = mediaPanel.querySelector('img'), target = (video ? video : (photo ? photo : null)); if (target) { return handleElement(e, target); } } } return true; // Let the default browser handler run if no valid keypress or no media. }, true); // True = Capture BEFORE sending any typing-event to the active element. }; var injectMediaCounter = function() { // Section state class. var SectionState = function(section) { this.section = section; this.loadedMediaIds = new Set(); this.loadedCount = -1; this.totalCount = -1; // Detect the section's media box element, which holds the rows of media. Each child of it is a 3-media row. // NOTE: This selector will need future maintenance if the site design changes. this.mediaBoxElem = section ? section.querySelector('main > article > div > div:nth-of-type(1)') : null; }; // Updates the total media (timeline post) count for the section element (profile page). SectionState.prototype.updateTotalMediaCount = function() { // NOTE: This path will need future maintenance if it ever changes. // NOTE: Media count also exists in `window._sharedData.entry_data.ProfilePage[0].user.media.count`, // but that value sadly *never* changes when navigating dynamically to other profiles! // NOTE: We look for the language-independent header's <ul> of 3x <li> elements (posts, followers, // following). It contains nested <span> elements, one of which (the last one) is the pure number of posts. var mediaCountSpans = this.section.querySelectorAll('header ul > li:nth-of-type(1) span'); for (var i = mediaCountSpans.length - 1; i >= 0; --i) { // Count backwards, since the one we want is at the end. // Clean up the count by stripping away thousands-separators (spaces, commas, etc depending on language). var count = mediaCountSpans[i].textContent.replace(/[^0-9]+/g, ''); if (count.length > 0) { try { count = parseInt(count, 10); if (Number.isInteger(count)) { // Guard against NaN. this.totalCount = count; return; } } catch (e) {} } } this.totalCount = 0; // If count doesn't exist or couldn't be parsed. }; // Updates the loaded media count for the section's media box, and clamps so it never exceeds the total. SectionState.prototype.updateLoadedMediaCount = function() { // Instagram uses a dynamic set of media "divs", and only keeps ~50 in memory (for infinite-scroll efficiency). // Therefore, the only way to detect the load progress is to count how many unique IDs we've seen in them. var count = 0; if (this.mediaBoxElem) { var mediaLinks = this.mediaBoxElem.querySelectorAll('a[href^="/p/"]'); for (var i = 0, len = mediaLinks.length; i < len; ++i) { this.loadedMediaIds.add(mediaLinks[i].pathname); // Format: "/p/<id>/". } count = this.loadedMediaIds.size; if (count > this.totalCount) { count = this.totalCount; // Ensure that it can never exceed the total. } } this.loadedCount = count; }; // Media counter class. var MediaCounter = function() { // Initialize properties. this.currentProfile = this.extractProfileName(location.pathname); this.activeState = null; this.counterElem = null; this.isCounterVisible = false; this.updateCooldownTimer = undefined; // Attach handlers and create initial state. this.createCounterElem(); this.attachReactRootObserver(); this.startWatchingPathname(); }; // Creates the media counter element. MediaCounter.prototype.createCounterElem = function() { // Create a floating container in the bottom right of the page. var floatContainer = document.createElement('div'); floatContainer.style.position = 'fixed'; floatContainer.style.bottom = 0; floatContainer.style.right = 0; floatContainer.style.zIndex = '99999'; // Add a nicely styled "media counter" container within the floating container. var counterElem = document.createElement('div'); counterElem.style.margin = '14px'; // Offsets it from the edges. counterElem.style.padding = '5px 10px'; // Empty padding around everything in the container. counterElem.style.backgroundColor = 'rgba(60,60,60,0.5)'; counterElem.style.borderRadius = '15px'; counterElem.style.font = 'bold 13px sans-serif'; counterElem.style.color = '#fff'; counterElem.style.textAlign = 'center'; counterElem.style.textShadow = '1px 1px 2px rgba(0,0,0,0.3)'; counterElem.style.display = 'none'; // Start out hidden. counterElem.title = 'Shift-[click/F]: View in the same tab.\nAlt-[click/F]: View in a new tab/window.\nShift-Alt-[click/F]: Direct download.'; floatContainer.appendChild(counterElem); // Put the floating counter as a child of the body itself. // NOTE: We don't put it inside of any specific elements, to remain fully markup-agnostic. document.body.appendChild(floatContainer); this.counterElem = counterElem; }; // Extracts the profile name from a Location pathname. MediaCounter.prototype.extractProfileName = function(pathname) { if (typeof pathname === 'string') { // NOTE: We demand a single word (as in a profile), such as "/foo/", // not "/foo/bar". That avoids most of the custom pages (such as // "/accounts/login/"). We'll also avoid "/developer/". var match = pathname.match(/^\/([^\/]+)\/?$/); // Extracts at least 1 char. if (match) { var profile = match[1]; if (profile !== 'developer') { return profile; } } } return null; }; // Watches for navigation between the profile and media overlays, and hides counter during overlays. // NOTE: We can't use HTML5 popstate events for this. They are too unreliable. We must use a timer. MediaCounter.prototype.startWatchingPathname = function() { var self = this, currentPathname = null; setInterval(function() { // Detects when we've moved to a different path on the site. if (location.pathname !== currentPathname) { currentPathname = location.pathname; // Toggle the counter visibility depending on which page we are on. // NOTE: We only unhide the counter if it already contains a value *and* we're still on the same profile! var profile = self.extractProfileName(currentPathname); if (profile !== null) { // On a profile page. if (profile === self.currentProfile && self.counterElem.textContent !== '') { // Same profile, and has existing counter. self.toggleMediaCounterVisibility(true); } else { // Different profile, or has no counter value. // NOTE: We don't hide it when navigating to a different profile. Because that's handled instantly by our react-root observer. self.currentProfile = profile; // Track the new profile instead. } } else { // On a non-profile page, such as media instead. self.toggleMediaCounterVisibility(false); } } }, 250); }; // Toggles the media counter visibility whenever we're on a non-timeline page (or overlay). MediaCounter.prototype.toggleMediaCounterVisibility = function(showCounter) { if (showCounter !== this.isCounterVisible) { this.counterElem.style.display = showCounter ? 'block' : 'none'; this.isCounterVisible = !!showCounter; } }; // Updates the media counter to the currently loaded count, and ensures that the counter is visible. MediaCounter.prototype.updateMediaCounter = function(forceUpdateTotal) { if (!this.activeState) { return; } // Update internal state to current count. if (forceUpdateTotal || this.activeState.totalCount < 0) { this.activeState.updateTotalMediaCount(); // Must be updated before loaded count. } this.activeState.updateLoadedMediaCount(); // Set the new media counter text. var percentLoaded = this.activeState.totalCount > 0 ? ((this.activeState.loadedCount / this.activeState.totalCount) * 100) : 0; // No 0-div. percentLoaded = percentLoaded.toFixed(1); // Convert to string with rounding and always 1 decimal. this.counterElem.textContent = this.activeState.loadedCount+' / '+this.activeState.totalCount+' ('+percentLoaded+'%)'; // Make sure counter is visible if we're on a profile page, or hidden otherwise. this.toggleMediaCounterVisibility(this.extractProfileName(location.pathname) !== null); }; // Adds mutation observer to section. Observes changes in the rows of media items. // NOTE: They trigger once per inserted row and insert 4 rows at once, so we use a slight // "cooldown" timer before we react, to avoid triggering rapid DOM (counter) updates. MediaCounter.prototype.observeSection = function(section) { // Create and initialize a blank state for this section. // NOTE: This is always okay (and fast) even if we already had a state for it, // because the counter updater always refreshes the state's counts on update. var state = new SectionState(section); // Abort if this section didn't contain a media box. // NOTE: This happens due to things like visiting a timeline, then going to the main timeline // which is also loaded as a <section> in the "react-root" just like regular pages. if (!state.mediaBoxElem) { // Erase the active state and hide the counter, since we've navigated away from the old section. this.activeState = null; this.toggleMediaCounterVisibility(false); return; // Abort. } // Make our (the last-called) state the new "active" state. this.activeState = state; // New profile/state. Force counter update (including new totals). this.updateMediaCounter(true); // Remove any old (outdated / old state) observer from this section, just in case any still exists. this.unobserveSection(section); // Add a new mutation observer on the section's media box. var self = this, config = { attributes: false, childList: true, characterData: false }, observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { clearTimeout(self.updateCooldownTimer); self.updateCooldownTimer = setTimeout(function() { // Update to our new count, but only if we (this section/media box) are the counter's active state. if (state === self.activeState) { self.updateMediaCounter(); } }, 150); // Wait 150ms before we perform the update. } }); }); observer.observe(state.mediaBoxElem, config); // Attach the observer property, so that we can disconnect it later. section.instaMagnifyObserver = observer; }; // Remove mutation observer from section. MediaCounter.prototype.unobserveSection = function(section) { if (section.instaMagnifyObserver) { section.instaMagnifyObserver.disconnect(); delete section.instaMagnifyObserver; } }; // Observes node additions/deletions within the react-root. MediaCounter.prototype.attachReactRootObserver = function() { var self = this, interestingChanges = ['addedNodes', 'removedNodes'], config = { attributes: false, childList: true, characterData: false }, observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { // When the "react-root" gets a new <section> element, it means we've navigated to a different profile. // NOTE: It can also mean that we've visited the "/accounts/login/" page and other similar ones. if (mutation.type === 'childList') { for (var i = 0; i < interestingChanges.length; ++i) { var field = interestingChanges[i]; for (var x = 0, len = mutation[field].length; x < len; ++x) { var node = mutation[field][x]; if (node.tagName === 'SECTION') { if (field === 'addedNodes') { self.observeSection(node); } else { self.unobserveSection(node); } } } } } }); }); // Only begin observing if we're on a React-based page. var reactRootElem = document.querySelector('span#react-root'); if (reactRootElem) { observer.observe(reactRootElem, config); // Set its current section (if any) as the observed section. var children = reactRootElem.childNodes; for (var i = 0; i < children.length; ++i) { var node = children[i]; if (node.tagName === 'SECTION') { self.observeSection(node); } } } }; // Create the media counter. var mediaCounter = new MediaCounter(); }; var injectAutoActions = function() { var autoLoadMore = function() { // Do nothing until the user has scrolled at least 500 pixels down (roughly // the 1st row of media disappearing behind the top bar of the browser window). // NOTE: This simply ensures that we don't waste people's internet bandwidth // by loading pages when they don't even scroll through the profile's media. if (window.pageYOffset <= 500) { return; } // Look for the first "Load more" button we can find, and click it. The button will vanish when clicked, // which means that we won't find anything until another button appears, which is great for performance. // NOTE: This selector is incredibly specific for performance, to avoid scanning all links. // In fact, just the `article > div > a` is enough to find the link. *That's* how specific it is! var loadMore = document.querySelectorAll('article > div > a[href*="max_id="]'); for (var i = 0; i < loadMore.length; ++i) { // NOTE: We look for a "/name/" path (a single path component) or "/explore/*" (places), with a query string that has "max_id=" in its parameters. // We can't verify by looking for any language-specific strings such as "Load more", since Instagram is multilingual. if (loadMore[i].pathname.match(/^\/(?:[^\/]+\/$|explore\/)/) && loadMore[i].search.match(/[?&]max_id=/)) { loadMore[i].click(); break; } } }; var autoCloseMobileAppDialog = function() { // The "Experience the best version of Instagram by getting the mobile app" modal dialog box // only appears when `#reactivated` is in the URL hash. Which means at least 11 characters. // NOTE: Only some accounts get this dialog. It doesn't seem related to whether the mobile app has // been used by the account, because new accounts that haven't used the mobile app don't get it. // It seems to be something about legacy accounts being "reactivated" after a long time, and // them having never used the official apps... if (location.hash.length < 11) { return; } // Proceed if we see the `#reactivated` hash. if (location.hash.indexOf('reactivated') >= 0) { // Clear the hash. Instagram doesn't use the hash for anything important, so we'll just // remove all of its contents and set it to `#` (empty hash). If we don't remove the hash, // the popup dialog box will keep re-appearing after the user watches a homepage story... location.hash = ''; // Help the user quickly close the popup box... var isClosed = false, closeMobileAppDialog = function() { if (isClosed) { return; } var dialogs = document.querySelectorAll('div[role="dialog"]'); for (var i = 0; i < dialogs.length; ++i) { var appStoreLink = dialogs[i].querySelector('a[href*="itunes.apple.com"]'); if (appStoreLink) { // The dialog has multiple close buttons. It doesn't matter which we use. Get the first one. var closeButton = dialogs[i].querySelector('button'); if (closeButton) { closeButton.click(); isClosed = true; } } } }; closeMobileAppDialog(); if (!isClosed) { var attempt = 0, closeDialogInterval = setInterval(function() { // Allow up to 30 retries (takes 7.5 seconds at 250ms each). if (isClosed || ++attempt > 30) { clearInterval(closeDialogInterval); return; } closeMobileAppDialog(); }, 250); } } }; var autoCloseAnnoyingBars = function() { var i, elem, elems; // First handle their black, modern, semi-transparent "signup" bar... This is the one they show most often. var signupBar = document.querySelector('div.coreSpriteLoggedOutGenericUpsell'); if (signupBar) { var closeButton = signupBar.parentNode.parentNode.querySelector('.coreSpriteDismissLarge[role="button"]'); if (closeButton) closeButton.click(); } // Also handle their white, old-school, opaque "Sign up to see photos from your friends" alternative bar... var whiteBar = document.querySelector('.coreSpriteGlyphGradient'); if (whiteBar) { var signupLink = whiteBar.parentNode.parentNode.parentNode.parentNode.querySelector('a[href*="signup"]'); if (signupLink) { elems = signupLink.parentNode.parentNode.childNodes; for (i = elems.length - 1; i >= 0; --i) { elem = elems[i]; if (elem.tagName === 'SPAN' && elem.textContent === '✕') { // We check this to be 100% sure we've found it. elem.click(); break; } } } } // Lastly, handle their "Experience the best version of Instagram by getting the app." bar, which is at the bottom when logged in. var getAppBar = document.querySelector('.coreSpriteAppIcon'); if (getAppBar) { var appStoreLink = getAppBar.parentNode.parentNode.querySelector('a[href*="itunes.apple.com"]'); if (appStoreLink) { elems = appStoreLink.parentNode.parentNode.parentNode.parentNode.parentNode.childNodes; for (i = elems.length - 1; i >= 0; --i) { elem = elems[i]; if (elem.tagName === 'SPAN' && elem.textContent === '✕') { // We check this to be 100% sure we've found it. elem.click(); break; } } } } }; // Perform the automatic actions at regular intervals. // NOTE: They are optimized to be fast when there's nothing to do. setInterval(function() { autoLoadMore(); autoCloseMobileAppDialog(); autoCloseAnnoyingBars(); }, 400); }; // Inject the code... var injectHandlers = function() { injectMediaMagnifier(); injectMediaCounter(); injectAutoActions(); }; if (document.readyState === 'interactive' || document.readyState === 'complete') { injectHandlers(); } else { var hasInjected = false; document.addEventListener('readystatechange', function(evt) { if (document.readyState === 'interactive' || document.readyState === 'complete') { if (!hasInjected) { injectHandlers(); hasInjected = true; } } } ); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址