Instagram Image Downloader

Adds a download icon to download Instagram images in full resolution

// ==UserScript==
// @name         Instagram Image Downloader
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Adds a download icon to download Instagram images in full resolution
// @license      MIT
// @author       nereids
// @icon         https://icons.duckduckgo.com/ip3/instagram.com.ico
// @match        https://www.instagram.com/*
// @grant        GM_xmlhttpRequest
// @connect      www.instagram.com
// @connect      *.fna.fbcdn.net
// ==/UserScript==

(function() {
    'use strict';

    // Track image indices per post
    const imageIndices = new Map();

    // Function to create SVG download icon with customizable path
    function createDownloadIcon(customPath = 'M3.46447 3.46447C2 4.92893 2 7.28595 2 12c0 4.714 0 7.0711 1.46447 8.5355C4.92893 22 7.28595 22 12 22c4.714 0 7.0711 0 8.5355 -1.4645C22 19.0711 22 16.714 22 12c0 -4.71405 0 -7.07107 -1.4645 -8.53553C19.0711 2 16.714 2 12 2 7.28595 2 4.92893 2 3.46447 3.46447ZM12 7.25c0.4142 0 0.75 0.33579 0.75 0.75v6.1893l1.7197 -1.7196c0.2929 -0.2929 0.7677 -0.2929 1.0606 0 0.2929 0.2929 0.2929 0.7677 0 1.0606l-3 3c-0.1406 0.1407 -0.3314 0.2197 -0.5303 0.2197 -0.1989 0 -0.3897 -0.079 -0.5303 -0.2197l-3.00003 -3c-0.29289 -0.2929 -0.29289 -0.7677 0 -1.0606 0.29289 -0.2929 0.76777 -0.2929 1.06066 0L11.25 14.1893V8c0 -0.41421 0.3358 -0.75 0.75 -0.75Z') {
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '24');
        svg.setAttribute('height', '24');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('fill', 'white');
        svg.setAttribute('fill-rule','evenodd')
        svg.setAttribute('clip-rule','evenodd')
        svg.style.position = 'absolute';
        svg.style.left = '5px';
        svg.style.top = '5px';
        //svg.style.left = '50%';
        //svg.style.transform = 'translateX(-50%)';
        svg.style.cursor = 'pointer';
        svg.style.zIndex = '1000';
        svg.style.opacity = '0.65'; // Set initial opacity

        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', customPath); // Allow custom path
        svg.appendChild(path);

        return svg;
    }

    // Function to trigger download using blob
    function downloadImage(url, filename) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob',
            onload: function(response) {
                const blob = response.response;
                const a = document.createElement('a');
                a.href = window.URL.createObjectURL(blob);
                a.download = filename || 'instagram-image.jpg';
                document.body.appendChild(a);
                a.click();
                window.URL.revokeObjectURL(a.href);
                document.body.removeChild(a);
            },
            onerror: function(err) {
                console.error('Download failed:', err);
                alert('Failed to download image. Check console for details.');
            }
        });
    }

    // Function to get the highest resolution image URL
    function getHighResImageUrl(imgElement) {
        let highResUrl = imgElement.src;
        if (highResUrl.includes('fna.fbcdn.net')) {
            highResUrl = highResUrl.replace(/&stp=dst-jpg(_e\d+)?(_p\d+x\d+)?(_tt\d+)?/, '&stp=dst-jpegr_e35');
            console.log('High-res URL:', highResUrl);
        }
        return highResUrl;
    }

    // Function to get profile name
    function getProfileName() {
        let profileName = document.querySelector('h2, h1')?.textContent.trim();
        if (!profileName) {
            profileName = document.querySelector('a[href*="/"] span, a[href*="/"] div')?.textContent.trim();
        }
        return profileName ? profileName.replace(/[^a-zA-Z0-9._]/g, '') : 'unknown';
    }

    // Function to get post date from DOM or by fetching the post page HTML
    async function getPostDate(imgElement) {
        const timeElement = imgElement.closest('article')?.querySelector('time');
        if (timeElement && timeElement.dateTime) {
            const date = new Date(timeElement.dateTime);
            return date.toISOString().split('T')[0]; // Format as YYYY-MM-DD
        }

        // Fetch the full post page if not found
        const postId = getPostId(imgElement);
        if (postId === 'unknown') return 'unknown';

        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://www.instagram.com/p/${postId}/`,
                    onload: resolve,
                    onerror: reject
                });
            });
            const html = response.responseText;
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const timeElementFetched = doc.querySelector('time');
            if (timeElementFetched && timeElementFetched.dateTime) {
                const date = new Date(timeElementFetched.dateTime);
                return date.toISOString().split('T')[0];
            }
        } catch (err) {
            console.error('Failed to fetch post page for date:', err);
        }
        return 'unknown'; // Return 'unknown' if no date is found
    }

    // Function to get post ID
    function getPostId(imgElement) {
        const postLink = imgElement.closest('a[href*="/p/"]')?.href || imgElement.closest('article')?.querySelector('a[href*="/p/"]')?.href;
        if (postLink) {
            const match = postLink.match(/\/p\/([^/?]+)/);
            return match ? match[1] : 'unknown';
        }
        // Fallback for thumbnails: extract from data attributes or parent link
        const thumbnailLink = imgElement.closest('a')?.getAttribute('href');
        if (thumbnailLink && thumbnailLink.includes('/p/')) {
            const thumbnailMatch = thumbnailLink.match(/\/p\/([^/?]+)/);
            return thumbnailMatch ? thumbnailMatch[1] : 'unknown';
        }
        return 'unknown';
    }

    // Function to get image index for the post
    function getImageIndex(postId, imgElement) {
        if (!imageIndices.has(postId)) {
            imageIndices.set(postId, 0);
        }
        const currentIndex = imageIndices.get(postId) + 1;
        imageIndices.set(postId, currentIndex);
        return currentIndex;
    }

    // Function to add download icon to images
    function addDownloadIcons() {
        const images = document.querySelectorAll('img[src*="fna.fbcdn.net"]');
        console.log('Found images:', images.length);
        images.forEach(img => {
            if (img.naturalWidth < 200 || img.naturalHeight < 200) return;
            const container = img.parentElement;
            if (!container.querySelector('.download-icon')) {
                container.style.position = 'relative';
                const downloadIcon = createDownloadIcon(); // Use default path or custom one
                downloadIcon.classList.add('download-icon');
                downloadIcon.addEventListener('click', async (e) => {
                    e.stopPropagation();
                    e.preventDefault();
                    const profileName = getProfileName();
                    const postDate = await getPostDate(img); // Await the async date fetch
                    const postId = getPostId(img);
                    const imageIndex = getImageIndex(postId, img);
                    const filename = `${profileName}-${postDate}_${postId}_${imageIndex}.jpg`;
                    const highResUrl = getHighResImageUrl(img);
                    console.log('Downloading:', highResUrl, 'as', filename);
                    downloadImage(highResUrl, filename);
                });
                container.appendChild(downloadIcon);
            }
        });
    }

    // Function to check for images with retry
    function checkForImages(retryCount = 0, maxRetries = 5) {
        const images = document.querySelectorAll('img[src*="fna.fbcdn.net"]');
        if (images.length > 0) {
            addDownloadIcons();
        } else if (retryCount < maxRetries) {
            console.log(`No images found, retrying (${retryCount + 1}/${maxRetries})...`);
            setTimeout(() => checkForImages(retryCount + 1, maxRetries), 1000);
        }
    }

    // Observe DOM changes for dynamic content
    const observer = new MutationObserver((mutations) => {
        let shouldCheck = false;
        mutations.forEach((mutation) => {
            if (mutation.type === 'childList' && mutation.addedNodes.length) {
                shouldCheck = true;
            }
        });
        if (shouldCheck) {
            setTimeout(addDownloadIcons, 500);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Initial check with retries
    setTimeout(() => checkForImages(), 2000);

    // Periodic check for dynamic content
    setInterval(addDownloadIcons, 3000);
})();

QingJ © 2025

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