Download all story highlights from Instagram profiles with one click
Size
11.6 KB
Version
1.0.1
Created
Jan 21, 2026
Updated
27 days ago
1// ==UserScript==
2// @name Instagram Highlights Downloader
3// @description Download all story highlights from Instagram profiles with one click
4// @version 1.0.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 Highlights Downloader initialized');
12
13 // Utility function to wait for element
14 function waitForElement(selector, timeout = 10000) {
15 return new Promise((resolve, reject) => {
16 if (document.querySelector(selector)) {
17 return resolve(document.querySelector(selector));
18 }
19
20 const observer = new MutationObserver(() => {
21 if (document.querySelector(selector)) {
22 observer.disconnect();
23 resolve(document.querySelector(selector));
24 }
25 });
26
27 observer.observe(document.body, {
28 childList: true,
29 subtree: true
30 });
31
32 setTimeout(() => {
33 observer.disconnect();
34 reject(new Error('Element not found: ' + selector));
35 }, timeout);
36 });
37 }
38
39 // Function to download a file
40 async function downloadFile(url, filename) {
41 try {
42 const response = await GM.xmlhttpRequest({
43 method: 'GET',
44 url: url,
45 responseType: 'blob'
46 });
47
48 const blob = response.response;
49 const blobUrl = URL.createObjectURL(blob);
50 const a = document.createElement('a');
51 a.href = blobUrl;
52 a.download = filename;
53 document.body.appendChild(a);
54 a.click();
55 document.body.removeChild(a);
56 URL.revokeObjectURL(blobUrl);
57 console.log('Downloaded:', filename);
58 } catch (error) {
59 console.error('Error downloading file:', error);
60 }
61 }
62
63 // Function to extract highlight data from Instagram's internal data
64 async function getHighlightsData(username) {
65 try {
66 // Try to get data from Instagram's internal state
67 const scripts = document.querySelectorAll('script[type="application/ld+json"]');
68 for (const script of scripts) {
69 try {
70 const data = JSON.parse(script.textContent);
71 console.log('Found JSON-LD data:', data);
72 } catch (e) {}
73 }
74
75 // Look for highlights in the page
76 const highlightElements = document.querySelectorAll('ul._acay li._acaz');
77 console.log('Found highlight elements:', highlightElements.length);
78
79 return Array.from(highlightElements);
80 } catch (error) {
81 console.error('Error getting highlights data:', error);
82 return [];
83 }
84 }
85
86 // Function to click and extract stories from a highlight
87 async function extractStoriesFromHighlight(highlightElement) {
88 try {
89 // Click the highlight to open it
90 const clickableElement = highlightElement.querySelector('div.x1i10hfl, a');
91 if (!clickableElement) {
92 console.error('No clickable element found in highlight');
93 return [];
94 }
95
96 console.log('Clicking highlight...');
97 clickableElement.click();
98
99 // Wait for the story viewer to open
100 await new Promise(resolve => setTimeout(resolve, 2000));
101
102 const stories = [];
103 let hasMoreStories = true;
104 let storyCount = 0;
105
106 while (hasMoreStories && storyCount < 100) {
107 // Find the current story media (image or video)
108 const videoElement = document.querySelector('video[class*="x1lliihq"]');
109 const imageElement = document.querySelector('img[class*="x5yr21d"][style*="object-fit"]');
110
111 if (videoElement && videoElement.src) {
112 console.log('Found video story:', videoElement.src);
113 stories.push({ type: 'video', url: videoElement.src });
114 } else if (imageElement && imageElement.src) {
115 console.log('Found image story:', imageElement.src);
116 stories.push({ type: 'image', url: imageElement.src });
117 }
118
119 storyCount++;
120
121 // Try to click next button
122 const nextButton = document.querySelector('button[aria-label="Next"], button[aria-label="Next story"]');
123 if (nextButton) {
124 nextButton.click();
125 await new Promise(resolve => setTimeout(resolve, 1500));
126 } else {
127 hasMoreStories = false;
128 }
129 }
130
131 // Close the story viewer
132 const closeButton = document.querySelector('button[aria-label="Close"], svg[aria-label="Close"]');
133 if (closeButton) {
134 if (closeButton.tagName === 'BUTTON') {
135 closeButton.click();
136 } else {
137 closeButton.closest('button')?.click();
138 }
139 }
140
141 await new Promise(resolve => setTimeout(resolve, 1000));
142
143 return stories;
144 } catch (error) {
145 console.error('Error extracting stories from highlight:', error);
146 return [];
147 }
148 }
149
150 // Main download function
151 async function downloadAllHighlights() {
152 try {
153 const button = document.getElementById('rm-download-highlights-btn');
154 if (button) {
155 button.textContent = 'Downloading...';
156 button.disabled = true;
157 }
158
159 // Get username from URL
160 const username = window.location.pathname.split('/')[1];
161 console.log('Downloading highlights for:', username);
162
163 // Get all highlights
164 const highlightElements = await getHighlightsData(username);
165
166 if (highlightElements.length === 0) {
167 alert('No highlights found on this profile.');
168 if (button) {
169 button.textContent = 'Download All Highlights';
170 button.disabled = false;
171 }
172 return;
173 }
174
175 console.log(`Found ${highlightElements.length} highlights`);
176
177 let totalDownloaded = 0;
178
179 // Process each highlight
180 for (let i = 0; i < highlightElements.length; i++) {
181 const highlight = highlightElements[i];
182 console.log(`Processing highlight ${i + 1}/${highlightElements.length}`);
183
184 if (button) {
185 button.textContent = `Processing ${i + 1}/${highlightElements.length}...`;
186 }
187
188 const stories = await extractStoriesFromHighlight(highlight);
189
190 // Download each story
191 for (let j = 0; j < stories.length; j++) {
192 const story = stories[j];
193 const extension = story.type === 'video' ? 'mp4' : 'jpg';
194 const filename = `${username}_highlight_${i + 1}_story_${j + 1}.${extension}`;
195 await downloadFile(story.url, filename);
196 totalDownloaded++;
197 await new Promise(resolve => setTimeout(resolve, 500));
198 }
199
200 // Wait between highlights
201 await new Promise(resolve => setTimeout(resolve, 1000));
202 }
203
204 alert(`Successfully downloaded ${totalDownloaded} stories from ${highlightElements.length} highlights!`);
205
206 if (button) {
207 button.textContent = 'Download All Highlights';
208 button.disabled = false;
209 }
210 } catch (error) {
211 console.error('Error downloading highlights:', error);
212 alert('Error downloading highlights. Please check the console for details.');
213 const button = document.getElementById('rm-download-highlights-btn');
214 if (button) {
215 button.textContent = 'Download All Highlights';
216 button.disabled = false;
217 }
218 }
219 }
220
221 // Function to create and add the download button
222 function addDownloadButton() {
223 // Check if we're on a profile page
224 const isProfilePage = /^\/[^\/]+\/?$/.test(window.location.pathname);
225 if (!isProfilePage) {
226 console.log('Not on a profile page');
227 return;
228 }
229
230 // Check if button already exists
231 if (document.getElementById('rm-download-highlights-btn')) {
232 return;
233 }
234
235 // Wait for the profile header to load
236 waitForElement('header section').then(() => {
237 // Find the profile action buttons area
238 const profileSection = document.querySelector('header section');
239 if (!profileSection) {
240 console.log('Profile section not found');
241 return;
242 }
243
244 // Create the download button
245 const downloadButton = document.createElement('button');
246 downloadButton.id = 'rm-download-highlights-btn';
247 downloadButton.textContent = 'Download All Highlights';
248 downloadButton.style.cssText = `
249 background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
250 color: white;
251 border: none;
252 padding: 8px 16px;
253 border-radius: 8px;
254 font-weight: 600;
255 font-size: 14px;
256 cursor: pointer;
257 margin-left: 8px;
258 transition: opacity 0.2s;
259 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
260 `;
261
262 downloadButton.addEventListener('mouseenter', () => {
263 downloadButton.style.opacity = '0.8';
264 });
265
266 downloadButton.addEventListener('mouseleave', () => {
267 downloadButton.style.opacity = '1';
268 });
269
270 downloadButton.addEventListener('click', downloadAllHighlights);
271
272 // Insert the button next to other profile buttons
273 const buttonContainer = profileSection.querySelector('div[class*="x1iyjqo2"]');
274 if (buttonContainer) {
275 buttonContainer.appendChild(downloadButton);
276 console.log('Download button added to profile');
277 } else {
278 // Fallback: add to profile section
279 profileSection.appendChild(downloadButton);
280 console.log('Download button added to profile section (fallback)');
281 }
282 }).catch(error => {
283 console.error('Error adding download button:', error);
284 });
285 }
286
287 // Initialize
288 function init() {
289 console.log('Initializing Instagram Highlights Downloader');
290
291 // Add button on page load
292 addDownloadButton();
293
294 // Re-add button on navigation (Instagram is a SPA)
295 let lastUrl = location.href;
296 new MutationObserver(() => {
297 const url = location.href;
298 if (url !== lastUrl) {
299 lastUrl = url;
300 console.log('URL changed to:', url);
301 setTimeout(addDownloadButton, 2000);
302 }
303 }).observe(document.body, { subtree: true, childList: true });
304 }
305
306 // Start when DOM is ready
307 if (document.readyState === 'loading') {
308 document.addEventListener('DOMContentLoaded', init);
309 } else {
310 init();
311 }
312})();