Greasy Fork is available in English.

InstaMagnify: Instagram Media Downloader

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://greasyfork.org/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://greasyfork.org/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;
                }
            }
        } );
    }
})();