Instagram Saved Collection Downloader

Download all images and videos from your Instagram saved collections

Size

13.4 KB

Version

1.1.1

Created

Mar 4, 2026

Updated

17 days ago

1// ==UserScript==
2// @name		Instagram Saved Collection Downloader
3// @description		Download all images and videos from your Instagram saved collections
4// @version		1.1.1
5// @match		https://www.instagram.com/*
6// @icon		https://robomonkey.io/favicon.ico
7// @grant		GM.xmlhttpRequest
8// @grant		GM.getValue
9// @grant		GM.setValue
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    console.log('Instagram Saved Collection Downloader initialized');
15
16    // Utility function to debounce
17    function debounce(func, wait) {
18        let timeout;
19        return function executedFunction(...args) {
20            const later = () => {
21                clearTimeout(timeout);
22                func(...args);
23            };
24            clearTimeout(timeout);
25            timeout = setTimeout(later, wait);
26        };
27    }
28
29    // Function to download a file
30    async function downloadFile(url, filename) {
31        try {
32            console.log('Downloading file:', filename);
33            const response = await GM.xmlhttpRequest({
34                method: 'GET',
35                url: url,
36                responseType: 'blob',
37                headers: {
38                    'User-Agent': navigator.userAgent
39                }
40            });
41
42            const blob = response.response;
43            const blobUrl = URL.createObjectURL(blob);
44            
45            const a = document.createElement('a');
46            a.href = blobUrl;
47            a.download = filename;
48            document.body.appendChild(a);
49            a.click();
50            document.body.removeChild(a);
51            
52            setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
53            console.log('Downloaded:', filename);
54            return true;
55        } catch (error) {
56            console.error('Error downloading file:', filename, error);
57            return false;
58        }
59    }
60
61    // Function to extract media URLs from Instagram's internal data
62    async function extractMediaFromPost(postUrl) {
63        try {
64            console.log('Extracting media from post:', postUrl);
65            
66            // Open the post in a new request to get its data
67            const response = await GM.xmlhttpRequest({
68                method: 'GET',
69                url: postUrl,
70                headers: {
71                    'User-Agent': navigator.userAgent
72                }
73            });
74
75            const html = response.responseText;
76            
77            // Extract JSON data from the page
78            const scriptRegex = /<script type="application\/ld\+json">({.*?})<\/script>/gs;
79            const matches = html.matchAll(scriptRegex);
80            
81            const mediaUrls = [];
82            
83            for (const match of matches) {
84                try {
85                    const data = JSON.parse(match[1]);
86                    if (data.video && data.video.contentUrl) {
87                        mediaUrls.push({ url: data.video.contentUrl, type: 'video' });
88                    }
89                    if (data.image) {
90                        mediaUrls.push({ url: data.image, type: 'image' });
91                    }
92                } catch (e) {
93                    // Continue if JSON parsing fails
94                }
95            }
96
97            // Also try to extract from window._sharedData
98            const sharedDataRegex = /window\._sharedData = ({.*?});<\/script>/s;
99            const sharedDataMatch = html.match(sharedDataRegex);
100            
101            if (sharedDataMatch) {
102                try {
103                    const sharedData = JSON.parse(sharedDataMatch[1]);
104                    const postData = sharedData?.entry_data?.PostPage?.[0]?.graphql?.shortcode_media;
105                    
106                    if (postData) {
107                        // Handle single image/video
108                        if (postData.display_url) {
109                            mediaUrls.push({ url: postData.display_url, type: 'image' });
110                        }
111                        if (postData.video_url) {
112                            mediaUrls.push({ url: postData.video_url, type: 'video' });
113                        }
114                        
115                        // Handle carousel (multiple images/videos)
116                        if (postData.edge_sidecar_to_children?.edges) {
117                            for (const edge of postData.edge_sidecar_to_children.edges) {
118                                const node = edge.node;
119                                if (node.video_url) {
120                                    mediaUrls.push({ url: node.video_url, type: 'video' });
121                                } else if (node.display_url) {
122                                    mediaUrls.push({ url: node.display_url, type: 'image' });
123                                }
124                            }
125                        }
126                    }
127                } catch (e) {
128                    console.error('Error parsing shared data:', e);
129                }
130            }
131
132            // Try to extract from img and video tags as fallback
133            const imgRegex = /<img[^>]+src="([^"]+)"[^>]*class="[^"]*x5yr21d[^"]*"/g;
134            const imgMatches = html.matchAll(imgRegex);
135            for (const match of imgMatches) {
136                if (match[1] && !match[1].includes('profile') && !match[1].includes('avatar')) {
137                    mediaUrls.push({ url: match[1], type: 'image' });
138                }
139            }
140
141            const videoRegex = /<video[^>]+src="([^"]+)"/g;
142            const videoMatches = html.matchAll(videoRegex);
143            for (const match of videoMatches) {
144                mediaUrls.push({ url: match[1], type: 'video' });
145            }
146
147            // Remove duplicates
148            const uniqueUrls = Array.from(new Set(mediaUrls.map(m => m.url)))
149                .map(url => mediaUrls.find(m => m.url === url));
150
151            console.log('Extracted media URLs:', uniqueUrls);
152            return uniqueUrls;
153        } catch (error) {
154            console.error('Error extracting media from post:', error);
155            return [];
156        }
157    }
158
159    // Function to get all saved posts from the current page
160    function getSavedPostLinks() {
161        const links = [];
162        const postLinks = document.querySelectorAll('a[href*="/p/"]');
163        
164        postLinks.forEach(link => {
165            const href = link.getAttribute('href');
166            if (href && href.includes('/p/')) {
167                const fullUrl = href.startsWith('http') ? href : `https://www.instagram.com${href}`;
168                if (!links.includes(fullUrl)) {
169                    links.push(fullUrl);
170                }
171            }
172        });
173        
174        console.log('Found saved post links:', links.length);
175        return links;
176    }
177
178    // Function to scroll and load all posts
179    async function scrollAndLoadAll(progressCallback) {
180        let lastHeight = document.body.scrollHeight;
181        let noChangeCount = 0;
182        let totalPosts = getSavedPostLinks().length;
183        
184        while (noChangeCount < 3) {
185            window.scrollTo(0, document.body.scrollHeight);
186            progressCallback(`Loading posts... Found ${totalPosts} posts so far`);
187            
188            await new Promise(resolve => setTimeout(resolve, 2000));
189            
190            const newHeight = document.body.scrollHeight;
191            const newTotalPosts = getSavedPostLinks().length;
192            
193            if (newHeight === lastHeight && newTotalPosts === totalPosts) {
194                noChangeCount++;
195            } else {
196                noChangeCount = 0;
197                totalPosts = newTotalPosts;
198            }
199            
200            lastHeight = newHeight;
201        }
202        
203        window.scrollTo(0, 0);
204        return getSavedPostLinks();
205    }
206
207    // Main download function
208    async function downloadAllSavedMedia() {
209        const button = document.getElementById('ig-download-btn');
210        if (!button) return;
211
212        const originalText = button.textContent;
213        button.disabled = true;
214        button.style.opacity = '0.6';
215        button.style.cursor = 'not-allowed';
216
217        try {
218            // Update button text with progress
219            const updateProgress = (text) => {
220                button.textContent = text;
221            };
222
223            updateProgress('Loading all posts...');
224            const postLinks = await scrollAndLoadAll(updateProgress);
225            
226            if (postLinks.length === 0) {
227                alert('No saved posts found!');
228                return;
229            }
230
231            updateProgress(`Found ${postLinks.length} posts. Starting download...`);
232            
233            let downloadedCount = 0;
234            let failedCount = 0;
235
236            for (let i = 0; i < postLinks.length; i++) {
237                const postUrl = postLinks[i];
238                updateProgress(`Processing ${i + 1}/${postLinks.length}...`);
239                
240                const mediaItems = await extractMediaFromPost(postUrl);
241                
242                if (mediaItems.length === 0) {
243                    console.warn('No media found for post:', postUrl);
244                    failedCount++;
245                    continue;
246                }
247
248                // Extract post ID from URL
249                const postIdMatch = postUrl.match(/\/p\/([^\/]+)/);
250                const postId = postIdMatch ? postIdMatch[1] : `post_${i}`;
251
252                for (let j = 0; j < mediaItems.length; j++) {
253                    const media = mediaItems[j];
254                    const extension = media.type === 'video' ? 'mp4' : 'jpg';
255                    const filename = mediaItems.length > 1 
256                        ? `instagram_${postId}_${j + 1}.${extension}`
257                        : `instagram_${postId}.${extension}`;
258                    
259                    const success = await downloadFile(media.url, filename);
260                    if (success) {
261                        downloadedCount++;
262                    } else {
263                        failedCount++;
264                    }
265                    
266                    // Small delay between downloads
267                    await new Promise(resolve => setTimeout(resolve, 500));
268                }
269            }
270
271            alert(`Download complete!\n\nSuccessfully downloaded: ${downloadedCount} files\nFailed: ${failedCount} files`);
272            
273        } catch (error) {
274            console.error('Error during download:', error);
275            alert('An error occurred during download. Check console for details.');
276        } finally {
277            button.textContent = originalText;
278            button.disabled = false;
279            button.style.opacity = '1';
280            button.style.cursor = 'pointer';
281        }
282    }
283
284    // Function to add download button to the page
285    function addDownloadButton() {
286        // Check if we're on a saved collection page
287        if (!window.location.pathname.includes('/saved')) {
288            return;
289        }
290
291        // Check if button already exists
292        if (document.getElementById('ig-download-btn')) {
293            return;
294        }
295
296        console.log('Adding download button to page');
297
298        // Create download button
299        const button = document.createElement('button');
300        button.id = 'ig-download-btn';
301        button.textContent = '⬇️ Download All';
302        button.style.cssText = `
303            position: fixed;
304            top: 80px;
305            right: 20px;
306            z-index: 9999;
307            background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
308            color: white;
309            border: none;
310            padding: 12px 24px;
311            border-radius: 8px;
312            font-size: 14px;
313            font-weight: 600;
314            cursor: pointer;
315            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
316            transition: all 0.3s ease;
317            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
318        `;
319
320        button.addEventListener('mouseenter', () => {
321            if (!button.disabled) {
322                button.style.transform = 'translateY(-2px)';
323                button.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.4)';
324            }
325        });
326
327        button.addEventListener('mouseleave', () => {
328            button.style.transform = 'translateY(0)';
329            button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
330        });
331
332        button.addEventListener('click', downloadAllSavedMedia);
333
334        document.body.appendChild(button);
335        console.log('Download button added successfully');
336    }
337
338    // Initialize the extension
339    function init() {
340        console.log('Initializing Instagram Saved Collection Downloader');
341        
342        // Add button when page loads
343        if (document.readyState === 'loading') {
344            document.addEventListener('DOMContentLoaded', addDownloadButton);
345        } else {
346            addDownloadButton();
347        }
348
349        // Watch for navigation changes (Instagram is a SPA)
350        const debouncedAddButton = debounce(addDownloadButton, 1000);
351        
352        const observer = new MutationObserver(debouncedAddButton);
353        observer.observe(document.body, {
354            childList: true,
355            subtree: true
356        });
357
358        // Also listen for URL changes
359        let lastUrl = location.href;
360        new MutationObserver(() => {
361            const url = location.href;
362            if (url !== lastUrl) {
363                lastUrl = url;
364                setTimeout(addDownloadButton, 1000);
365            }
366        }).observe(document, { subtree: true, childList: true });
367    }
368
369    // Start the extension
370    init();
371})();