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})();