Instagram Media Downloader

Download all high-quality images and videos from any Instagram profile

Size

21.8 KB

Version

1.1.1

Created

Mar 21, 2026

Updated

26 days ago

1// ==UserScript==
2// @name		Instagram Media Downloader
3// @description		Download all high-quality images and videos from any Instagram profile
4// @version		1.1.1
5// @match		https://*.instagram.com/*
6// @icon		https://static.cdninstagram.com/rsrc.php/y4/r/QaBlI0OZiks.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('Instagram Media Downloader extension loaded');
12
13    // Utility function to debounce
14    function debounce(func, wait) {
15        let timeout;
16        return function executedFunction(...args) {
17            const later = () => {
18                clearTimeout(timeout);
19                func(...args);
20            };
21            clearTimeout(timeout);
22            timeout = setTimeout(later, wait);
23        };
24    }
25
26    // Add custom styles for the download button and progress panel
27    TM_addStyle(`
28        .ig-download-btn {
29            background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
30            color: white;
31            border: none;
32            padding: 8px 16px;
33            border-radius: 8px;
34            font-weight: 600;
35            font-size: 14px;
36            cursor: pointer;
37            margin-left: 8px;
38            transition: transform 0.2s, box-shadow 0.2s;
39            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
40        }
41        
42        .ig-download-btn:hover {
43            transform: translateY(-2px);
44            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
45        }
46        
47        .ig-download-btn:disabled {
48            opacity: 0.6;
49            cursor: not-allowed;
50            transform: none;
51        }
52        
53        .ig-download-panel {
54            position: fixed;
55            top: 80px;
56            right: 20px;
57            width: 350px;
58            max-height: 500px;
59            background: white;
60            border-radius: 12px;
61            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
62            z-index: 9999;
63            overflow: hidden;
64            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
65        }
66        
67        .ig-download-panel-header {
68            background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
69            color: white;
70            padding: 16px;
71            font-weight: 600;
72            font-size: 16px;
73            display: flex;
74            justify-content: space-between;
75            align-items: center;
76        }
77        
78        .ig-download-panel-close {
79            background: rgba(255, 255, 255, 0.2);
80            border: none;
81            color: white;
82            width: 28px;
83            height: 28px;
84            border-radius: 50%;
85            cursor: pointer;
86            font-size: 18px;
87            line-height: 1;
88            transition: background 0.2s;
89        }
90        
91        .ig-download-panel-close:hover {
92            background: rgba(255, 255, 255, 0.3);
93        }
94        
95        .ig-download-panel-body {
96            padding: 16px;
97            max-height: 400px;
98            overflow-y: auto;
99        }
100        
101        .ig-download-status {
102            margin-bottom: 16px;
103            padding: 12px;
104            background: #f0f0f0;
105            border-radius: 8px;
106            font-size: 14px;
107            color: #262626;
108        }
109        
110        .ig-download-progress {
111            margin-top: 8px;
112        }
113        
114        .ig-download-progress-bar {
115            width: 100%;
116            height: 8px;
117            background: #dbdbdb;
118            border-radius: 4px;
119            overflow: hidden;
120        }
121        
122        .ig-download-progress-fill {
123            height: 100%;
124            background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
125            transition: width 0.3s;
126        }
127        
128        .ig-download-item {
129            display: flex;
130            align-items: center;
131            padding: 8px;
132            margin-bottom: 8px;
133            background: #fafafa;
134            border-radius: 6px;
135            font-size: 13px;
136        }
137        
138        .ig-download-item-icon {
139            width: 40px;
140            height: 40px;
141            margin-right: 12px;
142            border-radius: 4px;
143            object-fit: cover;
144        }
145        
146        .ig-download-item-info {
147            flex: 1;
148            color: #262626;
149        }
150        
151        .ig-download-item-status {
152            font-size: 11px;
153            color: #8e8e8e;
154            margin-top: 4px;
155        }
156        
157        .ig-download-item-success {
158            color: #00c853;
159        }
160        
161        .ig-download-item-error {
162            color: #d32f2f;
163        }
164    `);
165
166    let downloadPanel = null;
167    let isDownloading = false;
168
169    // Create download panel
170    function createDownloadPanel() {
171        if (downloadPanel) {
172            downloadPanel.remove();
173        }
174
175        downloadPanel = document.createElement('div');
176        downloadPanel.className = 'ig-download-panel';
177        downloadPanel.innerHTML = `
178            <div class="ig-download-panel-header">
179                <span>Media Downloader</span>
180                <button class="ig-download-panel-close">×</button>
181            </div>
182            <div class="ig-download-panel-body">
183                <div class="ig-download-status">
184                    <div id="ig-download-status-text">Initializing...</div>
185                    <div class="ig-download-progress">
186                        <div class="ig-download-progress-bar">
187                            <div class="ig-download-progress-fill" id="ig-download-progress-fill" style="width: 0%"></div>
188                        </div>
189                    </div>
190                </div>
191                <div id="ig-download-items"></div>
192            </div>
193        `;
194
195        document.body.appendChild(downloadPanel);
196
197        // Close button handler
198        downloadPanel.querySelector('.ig-download-panel-close').addEventListener('click', () => {
199            downloadPanel.remove();
200            downloadPanel = null;
201        });
202
203        return downloadPanel;
204    }
205
206    // Update download status
207    function updateDownloadStatus(text, progress) {
208        if (!downloadPanel) return;
209        
210        const statusText = downloadPanel.querySelector('#ig-download-status-text');
211        const progressFill = downloadPanel.querySelector('#ig-download-progress-fill');
212        
213        if (statusText) statusText.textContent = text;
214        if (progressFill && progress !== undefined) {
215            progressFill.style.width = `${progress}%`;
216        }
217    }
218
219    // Add download item to panel
220    function addDownloadItem(thumbnail, filename, status) {
221        if (!downloadPanel) return;
222        
223        const itemsContainer = downloadPanel.querySelector('#ig-download-items');
224        const item = document.createElement('div');
225        item.className = 'ig-download-item';
226        item.innerHTML = `
227            <img class="ig-download-item-icon" src="${thumbnail}" alt="">
228            <div class="ig-download-item-info">
229                <div>${filename}</div>
230                <div class="ig-download-item-status ${status === 'success' ? 'ig-download-item-success' : status === 'error' ? 'ig-download-item-error' : ''}">${status}</div>
231            </div>
232        `;
233        itemsContainer.appendChild(item);
234        
235        // Auto-scroll to bottom
236        itemsContainer.scrollTop = itemsContainer.scrollHeight;
237    }
238
239    // Extract username from URL or page
240    function getUsername() {
241        const urlMatch = window.location.pathname.match(/^\/([^\/]+)/);
242        if (urlMatch && urlMatch[1] && !['explore', 'reels', 'direct', 'stories'].includes(urlMatch[1])) {
243            return urlMatch[1];
244        }
245        return null;
246    }
247
248    // Fetch user data from Instagram's internal API
249    async function fetchUserData(username) {
250        try {
251            console.log('Fetching user data for:', username);
252            const response = await fetch(`https://www.instagram.com/api/v1/users/web_profile_info/?username=${username}`, {
253                headers: {
254                    'x-ig-app-id': '936619743392459',
255                    'x-requested-with': 'XMLHttpRequest'
256                }
257            });
258            
259            if (!response.ok) {
260                throw new Error('Failed to fetch user data');
261            }
262            
263            const data = await response.json();
264            return data.data.user;
265        } catch (error) {
266            console.error('Error fetching user data:', error);
267            throw error;
268        }
269    }
270
271    // Fetch all posts from a user - Updated method
272    async function fetchAllPosts(userId) {
273        const posts = [];
274        
275        try {
276            // Scroll and collect posts from the page
277            console.log('Attempting to collect posts by scrolling...');
278            updateDownloadStatus('Collecting posts from page...', 5);
279            
280            // Get all post links and their images from the page
281            const postData = new Map();
282            let lastCount = 0;
283            let noChangeCount = 0;
284            
285            // Scroll and collect posts
286            while (noChangeCount < 3) {
287                // Find all post links with their images
288                const postElements = document.querySelectorAll('a[href*="/p/"], a[href*="/reel/"]');
289                postElements.forEach(link => {
290                    const href = link.getAttribute('href');
291                    if (href && (href.includes('/p/') || href.includes('/reel/'))) {
292                        const shortcodeMatch = href.match(/\/(p|reel)\/([^\/\?]+)/);
293                        if (shortcodeMatch && shortcodeMatch[2]) {
294                            const shortcode = shortcodeMatch[2];
295                            
296                            // Try to find the image within this link
297                            const img = link.querySelector('img');
298                            if (img && img.src && !postData.has(shortcode)) {
299                                postData.set(shortcode, {
300                                    shortcode: shortcode,
301                                    thumbnail: img.src,
302                                    type: href.includes('/reel/') ? 'video' : 'image',
303                                    url: href
304                                });
305                            }
306                        }
307                    }
308                });
309                
310                const currentCount = postData.size;
311                console.log(`Found ${currentCount} unique posts`);
312                updateDownloadStatus(`Found ${currentCount} posts...`, 10);
313                
314                if (currentCount === lastCount) {
315                    noChangeCount++;
316                } else {
317                    noChangeCount = 0;
318                    lastCount = currentCount;
319                }
320                
321                // Scroll down
322                window.scrollTo(0, document.body.scrollHeight);
323                await new Promise(resolve => setTimeout(resolve, 2000));
324            }
325            
326            console.log(`Total unique posts found: ${postData.size}`);
327            
328            // Convert to posts array format
329            postData.forEach((data, shortcode) => {
330                posts.push({
331                    node: {
332                        shortcode: shortcode,
333                        thumbnail_src: data.thumbnail,
334                        __typename: data.type === 'video' ? 'GraphVideo' : 'GraphImage',
335                        id: shortcode,
336                        postUrl: `https://www.instagram.com${data.url}`
337                    }
338                });
339            });
340            
341        } catch (error) {
342            console.error('Error fetching posts:', error);
343        }
344        
345        return posts;
346    }
347    
348    // Fetch individual post data by opening it in a new context
349    async function fetchPostData(postUrl) {
350        try {
351            console.log(`Fetching post data from: ${postUrl}`);
352            
353            // Open the post URL and extract media
354            const response = await fetch(postUrl);
355            const html = await response.text();
356            
357            // Extract media URLs from the HTML
358            const mediaUrls = [];
359            
360            // Look for video URLs
361            const videoMatches = html.matchAll(/"video_url":"([^"]+)"/g);
362            for (const match of videoMatches) {
363                const url = match[1].replace(/\\u0026/g, '&');
364                if (url && !mediaUrls.includes(url)) {
365                    mediaUrls.push({ type: 'video', url: url });
366                }
367            }
368            
369            // Look for high-res image URLs
370            const imageMatches = html.matchAll(/"display_url":"([^"]+)"/g);
371            for (const match of imageMatches) {
372                const url = match[1].replace(/\\u0026/g, '&');
373                if (url && !mediaUrls.includes(url) && url.includes('instagram')) {
374                    mediaUrls.push({ type: 'image', url: url });
375                }
376            }
377            
378            return mediaUrls.length > 0 ? mediaUrls : null;
379        } catch (error) {
380            console.error('Error fetching post data:', error);
381            return null;
382        }
383    }
384
385    // Extract media URLs from posts
386    function extractMediaFromPosts(posts) {
387        // Return posts as-is, we'll fetch media URLs during download
388        return posts.map((edge, index) => ({
389            shortcode: edge.node.shortcode,
390            thumbnail: edge.node.thumbnail_src,
391            postUrl: edge.node.postUrl,
392            index: index
393        }));
394    }
395
396    // Download a single media file
397    async function downloadMedia(mediaItem, username, itemNumber, totalItems) {
398        try {
399            console.log(`Processing ${itemNumber}/${totalItems}: ${mediaItem.shortcode}`);
400            
401            // Fetch the post to get media URLs
402            const mediaUrls = await fetchPostData(mediaItem.postUrl);
403            
404            if (!mediaUrls || mediaUrls.length === 0) {
405                console.error(`No media found for ${mediaItem.shortcode}`);
406                addDownloadItem(mediaItem.thumbnail, `${mediaItem.shortcode}`, 'error - no media found');
407                return false;
408            }
409            
410            // Download all media from this post
411            let successCount = 0;
412            for (let i = 0; i < mediaUrls.length; i++) {
413                const media = mediaUrls[i];
414                const extension = media.type === 'video' ? 'mp4' : 'jpg';
415                const filename = `${username}_${mediaItem.shortcode}_${i}.${extension}`;
416                
417                try {
418                    const response = await GM.xmlhttpRequest({
419                        method: 'GET',
420                        url: media.url,
421                        responseType: 'blob'
422                    });
423                    
424                    // Create blob URL and trigger download
425                    const blob = response.response;
426                    const blobUrl = URL.createObjectURL(blob);
427                    
428                    const a = document.createElement('a');
429                    a.href = blobUrl;
430                    a.download = filename;
431                    document.body.appendChild(a);
432                    a.click();
433                    document.body.removeChild(a);
434                    
435                    // Clean up blob URL after a delay
436                    setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
437                    
438                    addDownloadItem(mediaItem.thumbnail, filename, 'success');
439                    successCount++;
440                    
441                    // Small delay between downloads from same post
442                    await new Promise(resolve => setTimeout(resolve, 300));
443                } catch (error) {
444                    console.error(`Error downloading ${filename}:`, error);
445                    addDownloadItem(mediaItem.thumbnail, filename, 'error');
446                }
447            }
448            
449            return successCount > 0;
450        } catch (error) {
451            console.error(`Error processing ${mediaItem.shortcode}:`, error);
452            addDownloadItem(mediaItem.thumbnail, mediaItem.shortcode, 'error');
453            return false;
454        }
455    }
456
457    // Main download function
458    async function startDownload() {
459        if (isDownloading) {
460            alert('Download already in progress!');
461            return;
462        }
463        
464        const username = getUsername();
465        if (!username) {
466            alert('Please navigate to an Instagram profile page to download media.');
467            return;
468        }
469        
470        isDownloading = true;
471        const downloadBtn = document.querySelector('.ig-download-btn');
472        if (downloadBtn) {
473            downloadBtn.disabled = true;
474            downloadBtn.textContent = 'Downloading...';
475        }
476        
477        // Create download panel
478        createDownloadPanel();
479        
480        try {
481            // Step 1: Fetch user data
482            updateDownloadStatus('Fetching profile information...', 5);
483            const userData = await fetchUserData(username);
484            console.log('User data:', userData);
485            
486            // Step 2: Fetch all posts
487            updateDownloadStatus('Scanning all posts...', 10);
488            const posts = await fetchAllPosts(userData.id);
489            console.log(`Found ${posts.length} posts`);
490            
491            if (posts.length === 0) {
492                updateDownloadStatus('No posts found on this profile.', 100);
493                return;
494            }
495            
496            // Step 3: Extract media URLs
497            updateDownloadStatus('Extracting media files...', 20);
498            const mediaItems = extractMediaFromPosts(posts);
499            console.log(`Extracted ${mediaItems.length} media items`);
500            
501            if (mediaItems.length === 0) {
502                updateDownloadStatus('No media files found.', 100);
503                return;
504            }
505            
506            updateDownloadStatus(`Found ${mediaItems.length} media files. Starting download...`, 25);
507            
508            // Step 4: Download all media
509            let successCount = 0;
510            let errorCount = 0;
511            
512            for (let i = 0; i < mediaItems.length; i++) {
513                const progress = 25 + ((i + 1) / mediaItems.length) * 75;
514                updateDownloadStatus(`Downloading ${i + 1}/${mediaItems.length} files...`, progress);
515                
516                const success = await downloadMedia(mediaItems[i], username, i + 1, mediaItems.length);
517                if (success) {
518                    successCount++;
519                } else {
520                    errorCount++;
521                }
522                
523                // Delay between downloads to avoid overwhelming the browser
524                await new Promise(resolve => setTimeout(resolve, 500));
525            }
526            
527            // Final status
528            updateDownloadStatus(`Complete! Downloaded ${successCount} files. ${errorCount > 0 ? `${errorCount} failed.` : ''}`, 100);
529            console.log(`Download complete: ${successCount} success, ${errorCount} errors`);
530            
531        } catch (error) {
532            console.error('Download error:', error);
533            updateDownloadStatus(`Error: ${error.message}`, 0);
534            alert(`Error downloading media: ${error.message}`);
535        } finally {
536            isDownloading = false;
537            if (downloadBtn) {
538                downloadBtn.disabled = false;
539                downloadBtn.textContent = 'Download All Media';
540            }
541        }
542    }
543
544    // Add download button to profile page
545    function addDownloadButton() {
546        // Check if we're on a profile page
547        const username = getUsername();
548        if (!username) {
549            console.log('Not on a profile page');
550            return;
551        }
552        
553        // Check if button already exists
554        if (document.querySelector('.ig-download-btn')) {
555            return;
556        }
557        
558        // Find the profile header section with action buttons
559        const headerSection = document.querySelector('header section');
560        if (!headerSection) {
561            console.log('Header section not found');
562            return;
563        }
564        
565        // Find the button container (usually contains Follow/Message buttons)
566        const buttonContainer = headerSection.querySelector('div[class*="x6s0dn4"][class*="x78zum5"]');
567        if (!buttonContainer) {
568            console.log('Button container not found');
569            return;
570        }
571        
572        // Create download button
573        const downloadBtn = document.createElement('button');
574        downloadBtn.className = 'ig-download-btn';
575        downloadBtn.textContent = 'Download All Media';
576        downloadBtn.addEventListener('click', startDownload);
577        
578        // Insert button
579        buttonContainer.appendChild(downloadBtn);
580        console.log('Download button added successfully');
581    }
582
583    // Initialize the extension
584    function init() {
585        console.log('Initializing Instagram Media Downloader');
586        
587        // Wait for page to load
588        if (document.readyState === 'loading') {
589            document.addEventListener('DOMContentLoaded', addDownloadButton);
590        } else {
591            addDownloadButton();
592        }
593        
594        // Watch for navigation changes (Instagram is a SPA)
595        const observer = new MutationObserver(debounce(() => {
596            addDownloadButton();
597        }, 1000));
598        
599        observer.observe(document.body, {
600            childList: true,
601            subtree: true
602        });
603        
604        // Also listen for URL changes
605        let lastUrl = location.href;
606        new MutationObserver(() => {
607            const url = location.href;
608            if (url !== lastUrl) {
609                lastUrl = url;
610                setTimeout(addDownloadButton, 2000);
611            }
612        }).observe(document, { subtree: true, childList: true });
613    }
614
615    // Start the extension
616    init();
617
618})();
Instagram Media Downloader | Robomonkey