@vicociv/instaloader 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # @vicociv/instaloader
2
+
3
+ TypeScript port of [instaloader](https://github.com/instaloader/instaloader) - Download Instagram content (posts, stories, profiles) with metadata.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @vicociv/instaloader
9
+ ```
10
+
11
+ **Requirements:** Node.js >= 18.0.0
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { Instaloader, Profile, Post, Hashtag } from '@vicociv/instaloader';
17
+
18
+ const L = new Instaloader();
19
+
20
+ // Get a post by shortcode (works without login)
21
+ const post = await L.getPost('DSsaqgbkhAd');
22
+ console.log(post.caption);
23
+ console.log(post.typename); // 'GraphImage' | 'GraphVideo' | 'GraphSidecar'
24
+
25
+ // Get a profile
26
+ const profile = await L.getProfile('instagram');
27
+ console.log(await profile.getFollowers()); // follower count
28
+
29
+ // Iterate posts
30
+ for await (const post of profile.getPosts()) {
31
+ console.log(post.shortcode, post.likes);
32
+ }
33
+ ```
34
+
35
+ ## Authentication
36
+
37
+ Most operations work without login. However, login is required for:
38
+ - Accessing private profiles
39
+ - Viewing stories and highlights
40
+ - Saved posts
41
+ - Some rate-limited operations
42
+
43
+ ```typescript
44
+ // Login with credentials
45
+ await L.login('username', 'password');
46
+
47
+ // Handle 2FA if required
48
+ try {
49
+ await L.login('username', 'password');
50
+ } catch (e) {
51
+ if (e instanceof TwoFactorAuthRequiredException) {
52
+ await L.twoFactorLogin('123456');
53
+ }
54
+ }
55
+
56
+ // Save/load session
57
+ await L.saveSessionToFile('session.json');
58
+ await L.loadSessionFromFile('username', 'session.json');
59
+
60
+ // Test if session is valid
61
+ const username = await L.testLogin(); // returns username or null
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### Instaloader
67
+
68
+ Main class for downloading Instagram content.
69
+
70
+ ```typescript
71
+ const L = new Instaloader(options?: InstaloaderOptions);
72
+ ```
73
+
74
+ **Options:**
75
+ | Option | Type | Default | Description |
76
+ |--------|------|---------|-------------|
77
+ | `sleep` | boolean | true | Enable rate limiting delays |
78
+ | `quiet` | boolean | false | Suppress console output |
79
+ | `userAgent` | string | - | Custom user agent |
80
+ | `downloadPictures` | boolean | true | Download images |
81
+ | `downloadVideos` | boolean | true | Download videos |
82
+ | `saveMetadata` | boolean | true | Save JSON metadata |
83
+
84
+ #### Get Content
85
+
86
+ ```typescript
87
+ // Get profile/post/hashtag
88
+ const profile = await L.getProfile('username');
89
+ const post = await L.getPost('shortcode');
90
+ const hashtag = await L.getHashtag('nature');
91
+ ```
92
+
93
+ #### Download Content
94
+
95
+ ```typescript
96
+ // Download a single post
97
+ await L.downloadPost(post, 'target_folder');
98
+
99
+ // Download profile posts
100
+ await L.downloadProfile(profile, {
101
+ maxCount: 10,
102
+ fastUpdate: true, // stop at already downloaded
103
+ postFilter: (p) => p.likes > 100,
104
+ });
105
+
106
+ // Download hashtag posts
107
+ await L.downloadHashtag(hashtag, { maxCount: 50 });
108
+ ```
109
+
110
+ ### Profile
111
+
112
+ Represents an Instagram user profile.
113
+
114
+ ```typescript
115
+ import { Profile } from '@vicociv/instaloader';
116
+
117
+ // Create from username
118
+ const profile = await Profile.fromUsername(context, 'instagram');
119
+
120
+ // Properties (sync)
121
+ profile.username // lowercase username
122
+ profile.userid // numeric user ID
123
+ profile.is_private // is private account
124
+ profile.followed_by_viewer
125
+ profile.follows_viewer
126
+
127
+ // Properties (async - may fetch metadata)
128
+ await profile.getFollowers()
129
+ await profile.getFollowees()
130
+ await profile.getMediacount()
131
+ await profile.getBiography()
132
+ await profile.getFullName()
133
+ await profile.getProfilePicUrl()
134
+ await profile.getIsVerified()
135
+ await profile.getExternalUrl()
136
+ ```
137
+
138
+ #### Iterate Posts
139
+
140
+ ```typescript
141
+ // All posts
142
+ for await (const post of profile.getPosts()) {
143
+ console.log(post.shortcode);
144
+ }
145
+
146
+ // Saved posts (requires login as owner)
147
+ for await (const post of profile.getSavedPosts()) {
148
+ console.log(post.shortcode);
149
+ }
150
+
151
+ // Tagged posts
152
+ for await (const post of profile.getTaggedPosts()) {
153
+ console.log(post.shortcode);
154
+ }
155
+ ```
156
+
157
+ ### Post
158
+
159
+ Represents an Instagram post.
160
+
161
+ ```typescript
162
+ import { Post } from '@vicociv/instaloader';
163
+
164
+ // Create from shortcode
165
+ const post = await Post.fromShortcode(context, 'ABC123');
166
+
167
+ // Properties
168
+ post.shortcode // URL shortcode
169
+ post.mediaid // BigInt media ID
170
+ post.typename // 'GraphImage' | 'GraphVideo' | 'GraphSidecar'
171
+ post.url // image/thumbnail URL
172
+ post.video_url // video URL (if video)
173
+ post.is_video // boolean
174
+ post.caption // caption text
175
+ post.caption_hashtags // ['tag1', 'tag2']
176
+ post.caption_mentions // ['user1', 'user2']
177
+ post.likes // like count
178
+ post.comments // comment count
179
+ post.date_utc // Date object
180
+ post.tagged_users // tagged usernames
181
+
182
+ // Get owner profile
183
+ const owner = await post.getOwnerProfile();
184
+
185
+ // Sidecar (carousel) posts
186
+ for (const node of post.getSidecarNodes()) {
187
+ console.log(node.is_video, node.display_url, node.video_url);
188
+ }
189
+ ```
190
+
191
+ ### Hashtag
192
+
193
+ Represents an Instagram hashtag.
194
+
195
+ ```typescript
196
+ import { Hashtag } from '@vicociv/instaloader';
197
+
198
+ const hashtag = await Hashtag.fromName(context, 'photography');
199
+
200
+ // Properties
201
+ hashtag.name // lowercase name (without #)
202
+ await hashtag.getHashtagId()
203
+ await hashtag.getMediacount()
204
+ await hashtag.getProfilePicUrl()
205
+
206
+ // Get posts (use getPostsResumable for reliable pagination)
207
+ const iterator = hashtag.getPostsResumable();
208
+ for await (const post of iterator) {
209
+ console.log(post.shortcode);
210
+ }
211
+
212
+ // Top posts
213
+ for await (const post of hashtag.getTopPosts()) {
214
+ console.log(post.shortcode);
215
+ }
216
+ ```
217
+
218
+ ### Story & StoryItem
219
+
220
+ ```typescript
221
+ import { Story, StoryItem } from '@vicociv/instaloader';
222
+
223
+ // Story contains multiple StoryItems
224
+ for await (const item of story.getItems()) {
225
+ console.log(item.mediaid);
226
+ console.log(item.typename); // 'GraphStoryImage' | 'GraphStoryVideo'
227
+ console.log(item.url); // image URL
228
+ console.log(item.video_url); // video URL (if video)
229
+ console.log(item.date_utc);
230
+ console.log(item.expiring_utc);
231
+ }
232
+ ```
233
+
234
+ ### Highlight
235
+
236
+ Extends Story for profile highlights.
237
+
238
+ ```typescript
239
+ import { Highlight } from '@vicociv/instaloader';
240
+
241
+ highlight.title // highlight title
242
+ highlight.cover_url // cover image URL
243
+
244
+ for await (const item of highlight.getItems()) {
245
+ // ...
246
+ }
247
+ ```
248
+
249
+ ### TopSearchResults
250
+
251
+ Search Instagram for profiles, hashtags, and locations.
252
+
253
+ ```typescript
254
+ import { TopSearchResults } from '@vicociv/instaloader';
255
+
256
+ const search = new TopSearchResults(context, 'query');
257
+
258
+ for await (const profile of search.getProfiles()) {
259
+ console.log(profile.username);
260
+ }
261
+
262
+ for await (const hashtag of search.getHashtags()) {
263
+ console.log(hashtag.name);
264
+ }
265
+
266
+ for await (const location of search.getLocations()) {
267
+ console.log(location.name, location.lat, location.lng);
268
+ }
269
+ ```
270
+
271
+ ### NodeIterator
272
+
273
+ Paginated iterator with resume support.
274
+
275
+ ```typescript
276
+ import { NodeIterator, FrozenNodeIterator } from '@vicociv/instaloader';
277
+
278
+ const iterator = profile.getPosts();
279
+
280
+ // Iterate
281
+ for await (const post of iterator) {
282
+ // Save state for resume
283
+ const state = iterator.freeze();
284
+ fs.writeFileSync('state.json', JSON.stringify(state));
285
+ break;
286
+ }
287
+
288
+ // Resume later
289
+ const savedState = JSON.parse(fs.readFileSync('state.json', 'utf-8'));
290
+ const frozen = new FrozenNodeIterator(savedState);
291
+ const resumed = frozen.thaw(context);
292
+ ```
293
+
294
+ ### Helper Functions
295
+
296
+ ```typescript
297
+ import {
298
+ shortcodeToMediaid,
299
+ mediaidToShortcode,
300
+ extractHashtags,
301
+ extractMentions,
302
+ parseInstagramUrl,
303
+ extractShortcode,
304
+ extractUsername,
305
+ extractHashtagFromUrl,
306
+ getJsonStructure,
307
+ loadStructure,
308
+ } from '@vicociv/instaloader';
309
+
310
+ // Convert between shortcode and mediaid
311
+ const mediaid = shortcodeToMediaid('ABC123'); // BigInt
312
+ const shortcode = mediaidToShortcode(mediaid); // string
313
+
314
+ // Extract from text
315
+ extractHashtags('Hello #world #test'); // ['world', 'test']
316
+ extractMentions('Hello @user1 @user2'); // ['user1', 'user2']
317
+
318
+ // Parse Instagram URLs
319
+ parseInstagramUrl('https://www.instagram.com/p/DSsaqgbkhAd/')
320
+ // => { type: 'post', shortcode: 'DSsaqgbkhAd' }
321
+
322
+ parseInstagramUrl('https://www.instagram.com/instagram/')
323
+ // => { type: 'profile', username: 'instagram' }
324
+
325
+ parseInstagramUrl('https://www.instagram.com/explore/tags/nature/')
326
+ // => { type: 'hashtag', hashtag: 'nature' }
327
+
328
+ // Extract specific identifiers from URLs
329
+ extractShortcode('https://www.instagram.com/p/DSsaqgbkhAd/?img_index=1')
330
+ // => 'DSsaqgbkhAd'
331
+
332
+ extractUsername('https://www.instagram.com/instagram/')
333
+ // => 'instagram'
334
+
335
+ extractHashtagFromUrl('https://www.instagram.com/explore/tags/nature/')
336
+ // => 'nature'
337
+
338
+ // JSON serialization
339
+ const json = getJsonStructure(post);
340
+ const restored = loadStructure(context, json);
341
+ ```
342
+
343
+ ### InstaloaderContext
344
+
345
+ Low-level API for direct Instagram requests. Usually accessed via `Instaloader.context`.
346
+
347
+ ```typescript
348
+ const context = L.context;
349
+
350
+ // Check login status
351
+ context.is_logged_in // boolean
352
+ context.username // string | null
353
+
354
+ // Make GraphQL queries
355
+ const data = await context.graphql_query(queryHash, variables);
356
+ const data = await context.doc_id_graphql_query(docId, variables);
357
+
358
+ // Generic JSON request
359
+ const data = await context.getJson('path', params);
360
+
361
+ // iPhone API
362
+ const data = await context.get_iphone_json('api/v1/...', params);
363
+ ```
364
+
365
+ ### Exceptions
366
+
367
+ ```typescript
368
+ import {
369
+ InstaloaderException, // Base exception
370
+ LoginException, // Login failed
371
+ LoginRequiredException, // Action requires login
372
+ TwoFactorAuthRequiredException,
373
+ BadCredentialsException, // Wrong password
374
+ ConnectionException, // Network error
375
+ TooManyRequestsException, // Rate limited (429)
376
+ ProfileNotExistsException, // Profile not found
377
+ QueryReturnedNotFoundException,
378
+ QueryReturnedForbiddenException,
379
+ PostChangedException, // Post changed during download
380
+ InvalidArgumentException,
381
+ } from '@vicociv/instaloader';
382
+ ```
383
+
384
+ ## License
385
+
386
+ MIT
package/dist/index.d.mts CHANGED
@@ -474,6 +474,8 @@ interface FrozenIteratorState {
474
474
  declare function defaultUserAgent(): string;
475
475
  /**
476
476
  * Returns default iPhone headers for API requests.
477
+ * Note: x-pigeon-session-id and x-ig-connection-speed are randomized per call
478
+ * to make each request appear as a different client (important for bypassing rate limits).
477
479
  */
478
480
  declare function defaultIphoneHeaders(): HttpHeaders;
479
481
  /**
@@ -627,6 +629,8 @@ declare class InstaloaderContext {
627
629
  usePost?: boolean;
628
630
  attempt?: number;
629
631
  headers?: HttpHeaders;
632
+ /** If true, refresh dynamic headers (timestamp, session ID, speed) on each attempt */
633
+ refreshDynamicHeaders?: boolean;
630
634
  }): Promise<JsonObject>;
631
635
  /**
632
636
  * Do a GraphQL Query.
@@ -634,10 +638,13 @@ declare class InstaloaderContext {
634
638
  graphql_query(queryHash: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
635
639
  /**
636
640
  * Do a doc_id-based GraphQL Query using POST.
641
+ * Each request uses fresh dynamic headers (timestamp, connection speed, session ID)
642
+ * to appear as different clients, which helps bypass rate limiting on retries.
637
643
  */
638
644
  doc_id_graphql_query(docId: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
639
645
  /**
640
646
  * JSON request to i.instagram.com.
647
+ * Each request uses fresh dynamic headers to appear as different clients.
641
648
  */
642
649
  get_iphone_json(path: string, params: JsonObject): Promise<JsonObject>;
643
650
  /**
@@ -964,6 +971,65 @@ declare function extractHashtags(text: string): string[];
964
971
  * Extract @mentions from text.
965
972
  */
966
973
  declare function extractMentions(text: string): string[];
974
+ /**
975
+ * Result of parsing an Instagram URL
976
+ */
977
+ interface ParsedInstagramUrl {
978
+ type: 'post' | 'profile' | 'hashtag' | 'unknown';
979
+ shortcode?: string;
980
+ username?: string;
981
+ hashtag?: string;
982
+ }
983
+ /**
984
+ * Parse an Instagram URL to extract the type and identifier.
985
+ *
986
+ * @param url - The Instagram URL to parse
987
+ * @returns Parsed URL information
988
+ *
989
+ * @example
990
+ * parseInstagramUrl('https://www.instagram.com/p/DSsaqgbkhAd/')
991
+ * // => { type: 'post', shortcode: 'DSsaqgbkhAd' }
992
+ *
993
+ * parseInstagramUrl('https://www.instagram.com/instagram/')
994
+ * // => { type: 'profile', username: 'instagram' }
995
+ *
996
+ * parseInstagramUrl('https://www.instagram.com/explore/tags/nature/')
997
+ * // => { type: 'hashtag', hashtag: 'nature' }
998
+ */
999
+ declare function parseInstagramUrl(url: string): ParsedInstagramUrl;
1000
+ /**
1001
+ * Extract shortcode from a post URL.
1002
+ *
1003
+ * @param url - The Instagram post URL
1004
+ * @returns The shortcode, or null if not found
1005
+ *
1006
+ * @example
1007
+ * extractShortcode('https://www.instagram.com/p/DSsaqgbkhAd/?img_index=1')
1008
+ * // => 'DSsaqgbkhAd'
1009
+ */
1010
+ declare function extractShortcode(url: string): string | null;
1011
+ /**
1012
+ * Extract username from a profile URL.
1013
+ *
1014
+ * @param url - The Instagram profile URL
1015
+ * @returns The username, or null if not found
1016
+ *
1017
+ * @example
1018
+ * extractUsername('https://www.instagram.com/instagram/')
1019
+ * // => 'instagram'
1020
+ */
1021
+ declare function extractUsername(url: string): string | null;
1022
+ /**
1023
+ * Extract hashtag from a hashtag URL.
1024
+ *
1025
+ * @param url - The Instagram hashtag URL
1026
+ * @returns The hashtag (without #), or null if not found
1027
+ *
1028
+ * @example
1029
+ * extractHashtagFromUrl('https://www.instagram.com/explore/tags/nature/')
1030
+ * // => 'nature'
1031
+ */
1032
+ declare function extractHashtagFromUrl(url: string): string | null;
967
1033
  /**
968
1034
  * Represents a comment on a post.
969
1035
  */
@@ -1661,4 +1727,4 @@ declare class Instaloader {
1661
1727
  getHashtag(name: string): Promise<Hashtag>;
1662
1728
  }
1663
1729
 
1664
- export { AbortDownloadException, BadCredentialsException, BadResponseException, CheckpointRequiredException, type CommentNode, ConnectionException, type CookieData, type EdgeConnection, type FrozenIteratorState, FrozenNodeIterator, type GraphQLResponse, Hashtag, type HashtagNode, Highlight, type HighlightNode, type HttpHeaders, type IPhoneHeaders, IPhoneSupportDisabledException, Instaloader, InstaloaderContext, type InstaloaderContextOptions, InstaloaderException, type InstaloaderOptions, InvalidArgumentException, InvalidIteratorException, type JsonExportable, type JsonObject, type JsonValue, type LocationNode, LoginException, LoginRequiredException, type LoginResponse, NodeIterator, type NodeIteratorOptions, type PageInfo, type PhoneVerificationSettings, Post, PostChangedException, PostComment, type PostCommentAnswer, type PostLocation, type PostNode, type PostSidecarNode, PrivateProfileNotFollowedException, Profile, ProfileHasNoPicsException, type ProfileNode, ProfileNotExistsException, QueryReturnedBadRequestException, QueryReturnedForbiddenException, QueryReturnedNotFoundException, type QueryTimestamps, RateController, type RequestOptions, type ResponseInfo, type ResumableIterationOptions, type ResumableIterationResult, type SessionData, SessionNotFoundException, Story, StoryItem, type StoryItemNode, NodeIterator as StructureNodeIterator, TooManyRequestsException, TopSearchResults, TwoFactorAuthRequiredException, type TwoFactorInfo, defaultIphoneHeaders, defaultUserAgent, extractHashtags, extractMentions, formatFilename, formatStringContainsKey, getConfigDir, getDefaultSessionFilename, getDefaultStampsFilename, getJsonStructure, loadStructure, mediaidToShortcode, resumableIteration, sanitizePath, shortcodeToMediaid };
1730
+ export { AbortDownloadException, BadCredentialsException, BadResponseException, CheckpointRequiredException, type CommentNode, ConnectionException, type CookieData, type EdgeConnection, type FrozenIteratorState, FrozenNodeIterator, type GraphQLResponse, Hashtag, type HashtagNode, Highlight, type HighlightNode, type HttpHeaders, type IPhoneHeaders, IPhoneSupportDisabledException, Instaloader, InstaloaderContext, type InstaloaderContextOptions, InstaloaderException, type InstaloaderOptions, InvalidArgumentException, InvalidIteratorException, type JsonExportable, type JsonObject, type JsonValue, type LocationNode, LoginException, LoginRequiredException, type LoginResponse, NodeIterator, type NodeIteratorOptions, type PageInfo, type ParsedInstagramUrl, type PhoneVerificationSettings, Post, PostChangedException, PostComment, type PostCommentAnswer, type PostLocation, type PostNode, type PostSidecarNode, PrivateProfileNotFollowedException, Profile, ProfileHasNoPicsException, type ProfileNode, ProfileNotExistsException, QueryReturnedBadRequestException, QueryReturnedForbiddenException, QueryReturnedNotFoundException, type QueryTimestamps, RateController, type RequestOptions, type ResponseInfo, type ResumableIterationOptions, type ResumableIterationResult, type SessionData, SessionNotFoundException, Story, StoryItem, type StoryItemNode, NodeIterator as StructureNodeIterator, TooManyRequestsException, TopSearchResults, TwoFactorAuthRequiredException, type TwoFactorInfo, defaultIphoneHeaders, defaultUserAgent, extractHashtagFromUrl, extractHashtags, extractMentions, extractShortcode, extractUsername, formatFilename, formatStringContainsKey, getConfigDir, getDefaultSessionFilename, getDefaultStampsFilename, getJsonStructure, loadStructure, mediaidToShortcode, parseInstagramUrl, resumableIteration, sanitizePath, shortcodeToMediaid };
package/dist/index.d.ts CHANGED
@@ -474,6 +474,8 @@ interface FrozenIteratorState {
474
474
  declare function defaultUserAgent(): string;
475
475
  /**
476
476
  * Returns default iPhone headers for API requests.
477
+ * Note: x-pigeon-session-id and x-ig-connection-speed are randomized per call
478
+ * to make each request appear as a different client (important for bypassing rate limits).
477
479
  */
478
480
  declare function defaultIphoneHeaders(): HttpHeaders;
479
481
  /**
@@ -627,6 +629,8 @@ declare class InstaloaderContext {
627
629
  usePost?: boolean;
628
630
  attempt?: number;
629
631
  headers?: HttpHeaders;
632
+ /** If true, refresh dynamic headers (timestamp, session ID, speed) on each attempt */
633
+ refreshDynamicHeaders?: boolean;
630
634
  }): Promise<JsonObject>;
631
635
  /**
632
636
  * Do a GraphQL Query.
@@ -634,10 +638,13 @@ declare class InstaloaderContext {
634
638
  graphql_query(queryHash: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
635
639
  /**
636
640
  * Do a doc_id-based GraphQL Query using POST.
641
+ * Each request uses fresh dynamic headers (timestamp, connection speed, session ID)
642
+ * to appear as different clients, which helps bypass rate limiting on retries.
637
643
  */
638
644
  doc_id_graphql_query(docId: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
639
645
  /**
640
646
  * JSON request to i.instagram.com.
647
+ * Each request uses fresh dynamic headers to appear as different clients.
641
648
  */
642
649
  get_iphone_json(path: string, params: JsonObject): Promise<JsonObject>;
643
650
  /**
@@ -964,6 +971,65 @@ declare function extractHashtags(text: string): string[];
964
971
  * Extract @mentions from text.
965
972
  */
966
973
  declare function extractMentions(text: string): string[];
974
+ /**
975
+ * Result of parsing an Instagram URL
976
+ */
977
+ interface ParsedInstagramUrl {
978
+ type: 'post' | 'profile' | 'hashtag' | 'unknown';
979
+ shortcode?: string;
980
+ username?: string;
981
+ hashtag?: string;
982
+ }
983
+ /**
984
+ * Parse an Instagram URL to extract the type and identifier.
985
+ *
986
+ * @param url - The Instagram URL to parse
987
+ * @returns Parsed URL information
988
+ *
989
+ * @example
990
+ * parseInstagramUrl('https://www.instagram.com/p/DSsaqgbkhAd/')
991
+ * // => { type: 'post', shortcode: 'DSsaqgbkhAd' }
992
+ *
993
+ * parseInstagramUrl('https://www.instagram.com/instagram/')
994
+ * // => { type: 'profile', username: 'instagram' }
995
+ *
996
+ * parseInstagramUrl('https://www.instagram.com/explore/tags/nature/')
997
+ * // => { type: 'hashtag', hashtag: 'nature' }
998
+ */
999
+ declare function parseInstagramUrl(url: string): ParsedInstagramUrl;
1000
+ /**
1001
+ * Extract shortcode from a post URL.
1002
+ *
1003
+ * @param url - The Instagram post URL
1004
+ * @returns The shortcode, or null if not found
1005
+ *
1006
+ * @example
1007
+ * extractShortcode('https://www.instagram.com/p/DSsaqgbkhAd/?img_index=1')
1008
+ * // => 'DSsaqgbkhAd'
1009
+ */
1010
+ declare function extractShortcode(url: string): string | null;
1011
+ /**
1012
+ * Extract username from a profile URL.
1013
+ *
1014
+ * @param url - The Instagram profile URL
1015
+ * @returns The username, or null if not found
1016
+ *
1017
+ * @example
1018
+ * extractUsername('https://www.instagram.com/instagram/')
1019
+ * // => 'instagram'
1020
+ */
1021
+ declare function extractUsername(url: string): string | null;
1022
+ /**
1023
+ * Extract hashtag from a hashtag URL.
1024
+ *
1025
+ * @param url - The Instagram hashtag URL
1026
+ * @returns The hashtag (without #), or null if not found
1027
+ *
1028
+ * @example
1029
+ * extractHashtagFromUrl('https://www.instagram.com/explore/tags/nature/')
1030
+ * // => 'nature'
1031
+ */
1032
+ declare function extractHashtagFromUrl(url: string): string | null;
967
1033
  /**
968
1034
  * Represents a comment on a post.
969
1035
  */
@@ -1661,4 +1727,4 @@ declare class Instaloader {
1661
1727
  getHashtag(name: string): Promise<Hashtag>;
1662
1728
  }
1663
1729
 
1664
- export { AbortDownloadException, BadCredentialsException, BadResponseException, CheckpointRequiredException, type CommentNode, ConnectionException, type CookieData, type EdgeConnection, type FrozenIteratorState, FrozenNodeIterator, type GraphQLResponse, Hashtag, type HashtagNode, Highlight, type HighlightNode, type HttpHeaders, type IPhoneHeaders, IPhoneSupportDisabledException, Instaloader, InstaloaderContext, type InstaloaderContextOptions, InstaloaderException, type InstaloaderOptions, InvalidArgumentException, InvalidIteratorException, type JsonExportable, type JsonObject, type JsonValue, type LocationNode, LoginException, LoginRequiredException, type LoginResponse, NodeIterator, type NodeIteratorOptions, type PageInfo, type PhoneVerificationSettings, Post, PostChangedException, PostComment, type PostCommentAnswer, type PostLocation, type PostNode, type PostSidecarNode, PrivateProfileNotFollowedException, Profile, ProfileHasNoPicsException, type ProfileNode, ProfileNotExistsException, QueryReturnedBadRequestException, QueryReturnedForbiddenException, QueryReturnedNotFoundException, type QueryTimestamps, RateController, type RequestOptions, type ResponseInfo, type ResumableIterationOptions, type ResumableIterationResult, type SessionData, SessionNotFoundException, Story, StoryItem, type StoryItemNode, NodeIterator as StructureNodeIterator, TooManyRequestsException, TopSearchResults, TwoFactorAuthRequiredException, type TwoFactorInfo, defaultIphoneHeaders, defaultUserAgent, extractHashtags, extractMentions, formatFilename, formatStringContainsKey, getConfigDir, getDefaultSessionFilename, getDefaultStampsFilename, getJsonStructure, loadStructure, mediaidToShortcode, resumableIteration, sanitizePath, shortcodeToMediaid };
1730
+ export { AbortDownloadException, BadCredentialsException, BadResponseException, CheckpointRequiredException, type CommentNode, ConnectionException, type CookieData, type EdgeConnection, type FrozenIteratorState, FrozenNodeIterator, type GraphQLResponse, Hashtag, type HashtagNode, Highlight, type HighlightNode, type HttpHeaders, type IPhoneHeaders, IPhoneSupportDisabledException, Instaloader, InstaloaderContext, type InstaloaderContextOptions, InstaloaderException, type InstaloaderOptions, InvalidArgumentException, InvalidIteratorException, type JsonExportable, type JsonObject, type JsonValue, type LocationNode, LoginException, LoginRequiredException, type LoginResponse, NodeIterator, type NodeIteratorOptions, type PageInfo, type ParsedInstagramUrl, type PhoneVerificationSettings, Post, PostChangedException, PostComment, type PostCommentAnswer, type PostLocation, type PostNode, type PostSidecarNode, PrivateProfileNotFollowedException, Profile, ProfileHasNoPicsException, type ProfileNode, ProfileNotExistsException, QueryReturnedBadRequestException, QueryReturnedForbiddenException, QueryReturnedNotFoundException, type QueryTimestamps, RateController, type RequestOptions, type ResponseInfo, type ResumableIterationOptions, type ResumableIterationResult, type SessionData, SessionNotFoundException, Story, StoryItem, type StoryItemNode, NodeIterator as StructureNodeIterator, TooManyRequestsException, TopSearchResults, TwoFactorAuthRequiredException, type TwoFactorInfo, defaultIphoneHeaders, defaultUserAgent, extractHashtagFromUrl, extractHashtags, extractMentions, extractShortcode, extractUsername, formatFilename, formatStringContainsKey, getConfigDir, getDefaultSessionFilename, getDefaultStampsFilename, getJsonStructure, loadStructure, mediaidToShortcode, parseInstagramUrl, resumableIteration, sanitizePath, shortcodeToMediaid };
package/dist/index.js CHANGED
@@ -66,8 +66,11 @@ __export(index_exports, {
66
66
  TwoFactorAuthRequiredException: () => TwoFactorAuthRequiredException,
67
67
  defaultIphoneHeaders: () => defaultIphoneHeaders,
68
68
  defaultUserAgent: () => defaultUserAgent,
69
+ extractHashtagFromUrl: () => extractHashtagFromUrl,
69
70
  extractHashtags: () => extractHashtags,
70
71
  extractMentions: () => extractMentions,
72
+ extractShortcode: () => extractShortcode,
73
+ extractUsername: () => extractUsername,
71
74
  formatFilename: () => formatFilename,
72
75
  formatStringContainsKey: () => formatStringContainsKey,
73
76
  getConfigDir: () => getConfigDir,
@@ -76,6 +79,7 @@ __export(index_exports, {
76
79
  getJsonStructure: () => getJsonStructure,
77
80
  loadStructure: () => loadStructure,
78
81
  mediaidToShortcode: () => mediaidToShortcode,
82
+ parseInstagramUrl: () => parseInstagramUrl,
79
83
  resumableIteration: () => resumableIteration,
80
84
  sanitizePath: () => sanitizePath,
81
85
  shortcodeToMediaid: () => shortcodeToMediaid
@@ -251,6 +255,13 @@ function defaultIphoneHeaders() {
251
255
  "x-whatsapp": "0"
252
256
  };
253
257
  }
258
+ function getPerRequestHeaders() {
259
+ return {
260
+ "x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6),
261
+ "x-ig-connection-speed": `${Math.floor(Math.random() * 19e3) + 1e3}kbps`,
262
+ "x-pigeon-session-id": (0, import_uuid.v4)()
263
+ };
264
+ }
254
265
  function sleep(ms) {
255
266
  return new Promise((resolve) => setTimeout(resolve, ms));
256
267
  }
@@ -657,7 +668,8 @@ var InstaloaderContext = class {
657
668
  host = "www.instagram.com",
658
669
  usePost = false,
659
670
  attempt = 1,
660
- headers: extraHeaders
671
+ headers: extraHeaders,
672
+ refreshDynamicHeaders = false
661
673
  } = options;
662
674
  const isGraphqlQuery = "query_hash" in params && path2.includes("graphql/query");
663
675
  const isDocIdQuery = "doc_id" in params && path2.includes("graphql/query");
@@ -678,10 +690,17 @@ var InstaloaderContext = class {
678
690
  await this._rateController.waitBeforeQuery("other");
679
691
  }
680
692
  const url = new URL(`https://${host}/${path2}`);
693
+ let headersToUse = extraHeaders;
694
+ if (refreshDynamicHeaders && extraHeaders) {
695
+ headersToUse = {
696
+ ...extraHeaders,
697
+ ...getPerRequestHeaders()
698
+ };
699
+ }
681
700
  const headers = {
682
701
  ...this._defaultHttpHeader(true),
683
702
  Cookie: this._getCookieHeader(url.toString()),
684
- ...extraHeaders
703
+ ...headersToUse
685
704
  };
686
705
  if (this._csrfToken) {
687
706
  headers["X-CSRFToken"] = this._csrfToken;
@@ -803,8 +822,10 @@ HTTP redirect from ${url} to ${redirectUrl}`);
803
822
  }
804
823
  return this.getJson(path2, params, {
805
824
  host,
806
- usePost,
825
+ // usePost is intentionally omitted to default to GET on retry
807
826
  attempt: attempt + 1,
827
+ refreshDynamicHeaders: true,
828
+ // Refresh headers on retry to appear as different client
808
829
  ...extraHeaders !== void 0 && { headers: extraHeaders }
809
830
  });
810
831
  }
@@ -837,10 +858,14 @@ HTTP redirect from ${url} to ${redirectUrl}`);
837
858
  }
838
859
  /**
839
860
  * Do a doc_id-based GraphQL Query using POST.
861
+ * Each request uses fresh dynamic headers (timestamp, connection speed, session ID)
862
+ * to appear as different clients, which helps bypass rate limiting on retries.
840
863
  */
841
864
  async doc_id_graphql_query(docId, variables, referer) {
865
+ const perRequestHeaders = getPerRequestHeaders();
842
866
  const headers = {
843
867
  ...this._defaultHttpHeader(true),
868
+ ...perRequestHeaders,
844
869
  authority: "www.instagram.com",
845
870
  scheme: "https",
846
871
  accept: "*/*"
@@ -863,12 +888,14 @@ HTTP redirect from ${url} to ${redirectUrl}`);
863
888
  }
864
889
  /**
865
890
  * JSON request to i.instagram.com.
891
+ * Each request uses fresh dynamic headers to appear as different clients.
866
892
  */
867
893
  async get_iphone_json(path2, params) {
894
+ const perRequestHeaders = getPerRequestHeaders();
868
895
  const headers = {
869
896
  ...this._iphoneHeaders,
870
- "ig-intended-user-id": this._userId || "",
871
- "x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6)
897
+ ...perRequestHeaders,
898
+ "ig-intended-user-id": this._userId || ""
872
899
  };
873
900
  const cookies = this.getCookies("https://i.instagram.com/");
874
901
  const headerCookiesMapping = {
@@ -1449,6 +1476,47 @@ function extractMentions(text) {
1449
1476
  }
1450
1477
  return matches;
1451
1478
  }
1479
+ var POST_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?instagram\.com\/(?:p|reel|tv)\/([A-Za-z0-9_-]+)/;
1480
+ var PROFILE_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?instagram\.com\/([A-Za-z0-9._]+)\/?(?:\?.*)?$/;
1481
+ var HASHTAG_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?instagram\.com\/explore\/tags\/([^/?]+)/;
1482
+ function parseInstagramUrl(url) {
1483
+ const postMatch = url.match(POST_URL_REGEX);
1484
+ if (postMatch && postMatch[1]) {
1485
+ return { type: "post", shortcode: postMatch[1] };
1486
+ }
1487
+ const hashtagMatch = url.match(HASHTAG_URL_REGEX);
1488
+ if (hashtagMatch && hashtagMatch[1]) {
1489
+ return { type: "hashtag", hashtag: hashtagMatch[1] };
1490
+ }
1491
+ const profileMatch = url.match(PROFILE_URL_REGEX);
1492
+ if (profileMatch && profileMatch[1]) {
1493
+ const nonProfilePaths = [
1494
+ "explore",
1495
+ "accounts",
1496
+ "directory",
1497
+ "about",
1498
+ "legal",
1499
+ "developer",
1500
+ "stories"
1501
+ ];
1502
+ if (!nonProfilePaths.includes(profileMatch[1].toLowerCase())) {
1503
+ return { type: "profile", username: profileMatch[1] };
1504
+ }
1505
+ }
1506
+ return { type: "unknown" };
1507
+ }
1508
+ function extractShortcode(url) {
1509
+ const match = url.match(POST_URL_REGEX);
1510
+ return match ? match[1] : null;
1511
+ }
1512
+ function extractUsername(url) {
1513
+ const parsed = parseInstagramUrl(url);
1514
+ return parsed.type === "profile" ? parsed.username ?? null : null;
1515
+ }
1516
+ function extractHashtagFromUrl(url) {
1517
+ const match = url.match(HASHTAG_URL_REGEX);
1518
+ return match ? match[1] : null;
1519
+ }
1452
1520
  function ellipsifyCaption(caption) {
1453
1521
  const pcaption = caption.split("\n").filter((s) => s).map((s) => s.replace(/\//g, "\u2215")).join(" ").trim();
1454
1522
  return pcaption.length > 31 ? pcaption.slice(0, 30) + "\u2026" : pcaption;
@@ -3710,8 +3778,11 @@ var Instaloader = class {
3710
3778
  TwoFactorAuthRequiredException,
3711
3779
  defaultIphoneHeaders,
3712
3780
  defaultUserAgent,
3781
+ extractHashtagFromUrl,
3713
3782
  extractHashtags,
3714
3783
  extractMentions,
3784
+ extractShortcode,
3785
+ extractUsername,
3715
3786
  formatFilename,
3716
3787
  formatStringContainsKey,
3717
3788
  getConfigDir,
@@ -3720,6 +3791,7 @@ var Instaloader = class {
3720
3791
  getJsonStructure,
3721
3792
  loadStructure,
3722
3793
  mediaidToShortcode,
3794
+ parseInstagramUrl,
3723
3795
  resumableIteration,
3724
3796
  sanitizePath,
3725
3797
  shortcodeToMediaid
package/dist/index.mjs CHANGED
@@ -167,6 +167,13 @@ function defaultIphoneHeaders() {
167
167
  "x-whatsapp": "0"
168
168
  };
169
169
  }
170
+ function getPerRequestHeaders() {
171
+ return {
172
+ "x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6),
173
+ "x-ig-connection-speed": `${Math.floor(Math.random() * 19e3) + 1e3}kbps`,
174
+ "x-pigeon-session-id": uuidv4()
175
+ };
176
+ }
170
177
  function sleep(ms) {
171
178
  return new Promise((resolve) => setTimeout(resolve, ms));
172
179
  }
@@ -573,7 +580,8 @@ var InstaloaderContext = class {
573
580
  host = "www.instagram.com",
574
581
  usePost = false,
575
582
  attempt = 1,
576
- headers: extraHeaders
583
+ headers: extraHeaders,
584
+ refreshDynamicHeaders = false
577
585
  } = options;
578
586
  const isGraphqlQuery = "query_hash" in params && path2.includes("graphql/query");
579
587
  const isDocIdQuery = "doc_id" in params && path2.includes("graphql/query");
@@ -594,10 +602,17 @@ var InstaloaderContext = class {
594
602
  await this._rateController.waitBeforeQuery("other");
595
603
  }
596
604
  const url = new URL(`https://${host}/${path2}`);
605
+ let headersToUse = extraHeaders;
606
+ if (refreshDynamicHeaders && extraHeaders) {
607
+ headersToUse = {
608
+ ...extraHeaders,
609
+ ...getPerRequestHeaders()
610
+ };
611
+ }
597
612
  const headers = {
598
613
  ...this._defaultHttpHeader(true),
599
614
  Cookie: this._getCookieHeader(url.toString()),
600
- ...extraHeaders
615
+ ...headersToUse
601
616
  };
602
617
  if (this._csrfToken) {
603
618
  headers["X-CSRFToken"] = this._csrfToken;
@@ -719,8 +734,10 @@ HTTP redirect from ${url} to ${redirectUrl}`);
719
734
  }
720
735
  return this.getJson(path2, params, {
721
736
  host,
722
- usePost,
737
+ // usePost is intentionally omitted to default to GET on retry
723
738
  attempt: attempt + 1,
739
+ refreshDynamicHeaders: true,
740
+ // Refresh headers on retry to appear as different client
724
741
  ...extraHeaders !== void 0 && { headers: extraHeaders }
725
742
  });
726
743
  }
@@ -753,10 +770,14 @@ HTTP redirect from ${url} to ${redirectUrl}`);
753
770
  }
754
771
  /**
755
772
  * Do a doc_id-based GraphQL Query using POST.
773
+ * Each request uses fresh dynamic headers (timestamp, connection speed, session ID)
774
+ * to appear as different clients, which helps bypass rate limiting on retries.
756
775
  */
757
776
  async doc_id_graphql_query(docId, variables, referer) {
777
+ const perRequestHeaders = getPerRequestHeaders();
758
778
  const headers = {
759
779
  ...this._defaultHttpHeader(true),
780
+ ...perRequestHeaders,
760
781
  authority: "www.instagram.com",
761
782
  scheme: "https",
762
783
  accept: "*/*"
@@ -779,12 +800,14 @@ HTTP redirect from ${url} to ${redirectUrl}`);
779
800
  }
780
801
  /**
781
802
  * JSON request to i.instagram.com.
803
+ * Each request uses fresh dynamic headers to appear as different clients.
782
804
  */
783
805
  async get_iphone_json(path2, params) {
806
+ const perRequestHeaders = getPerRequestHeaders();
784
807
  const headers = {
785
808
  ...this._iphoneHeaders,
786
- "ig-intended-user-id": this._userId || "",
787
- "x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6)
809
+ ...perRequestHeaders,
810
+ "ig-intended-user-id": this._userId || ""
788
811
  };
789
812
  const cookies = this.getCookies("https://i.instagram.com/");
790
813
  const headerCookiesMapping = {
@@ -1365,6 +1388,47 @@ function extractMentions(text) {
1365
1388
  }
1366
1389
  return matches;
1367
1390
  }
1391
+ var POST_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?instagram\.com\/(?:p|reel|tv)\/([A-Za-z0-9_-]+)/;
1392
+ var PROFILE_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?instagram\.com\/([A-Za-z0-9._]+)\/?(?:\?.*)?$/;
1393
+ var HASHTAG_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?instagram\.com\/explore\/tags\/([^/?]+)/;
1394
+ function parseInstagramUrl(url) {
1395
+ const postMatch = url.match(POST_URL_REGEX);
1396
+ if (postMatch && postMatch[1]) {
1397
+ return { type: "post", shortcode: postMatch[1] };
1398
+ }
1399
+ const hashtagMatch = url.match(HASHTAG_URL_REGEX);
1400
+ if (hashtagMatch && hashtagMatch[1]) {
1401
+ return { type: "hashtag", hashtag: hashtagMatch[1] };
1402
+ }
1403
+ const profileMatch = url.match(PROFILE_URL_REGEX);
1404
+ if (profileMatch && profileMatch[1]) {
1405
+ const nonProfilePaths = [
1406
+ "explore",
1407
+ "accounts",
1408
+ "directory",
1409
+ "about",
1410
+ "legal",
1411
+ "developer",
1412
+ "stories"
1413
+ ];
1414
+ if (!nonProfilePaths.includes(profileMatch[1].toLowerCase())) {
1415
+ return { type: "profile", username: profileMatch[1] };
1416
+ }
1417
+ }
1418
+ return { type: "unknown" };
1419
+ }
1420
+ function extractShortcode(url) {
1421
+ const match = url.match(POST_URL_REGEX);
1422
+ return match ? match[1] : null;
1423
+ }
1424
+ function extractUsername(url) {
1425
+ const parsed = parseInstagramUrl(url);
1426
+ return parsed.type === "profile" ? parsed.username ?? null : null;
1427
+ }
1428
+ function extractHashtagFromUrl(url) {
1429
+ const match = url.match(HASHTAG_URL_REGEX);
1430
+ return match ? match[1] : null;
1431
+ }
1368
1432
  function ellipsifyCaption(caption) {
1369
1433
  const pcaption = caption.split("\n").filter((s) => s).map((s) => s.replace(/\//g, "\u2215")).join(" ").trim();
1370
1434
  return pcaption.length > 31 ? pcaption.slice(0, 30) + "\u2026" : pcaption;
@@ -3625,8 +3689,11 @@ export {
3625
3689
  TwoFactorAuthRequiredException,
3626
3690
  defaultIphoneHeaders,
3627
3691
  defaultUserAgent,
3692
+ extractHashtagFromUrl,
3628
3693
  extractHashtags,
3629
3694
  extractMentions,
3695
+ extractShortcode,
3696
+ extractUsername,
3630
3697
  formatFilename,
3631
3698
  formatStringContainsKey,
3632
3699
  getConfigDir,
@@ -3635,6 +3702,7 @@ export {
3635
3702
  getJsonStructure,
3636
3703
  loadStructure,
3637
3704
  mediaidToShortcode,
3705
+ parseInstagramUrl,
3638
3706
  resumableIteration,
3639
3707
  sanitizePath,
3640
3708
  shortcodeToMediaid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vicociv/instaloader",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript port of instaloader - Download pictures (or videos) along with their captions and other metadata from Instagram.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",