@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 +386 -0
- package/dist/index.d.mts +67 -1
- package/dist/index.d.ts +67 -1
- package/dist/index.js +77 -5
- package/dist/index.mjs +73 -5
- package/package.json +1 -1
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
|
-
...
|
|
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
|
-
|
|
871
|
-
"
|
|
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
|
-
...
|
|
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
|
-
|
|
787
|
-
"
|
|
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.
|
|
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",
|