@vicociv/instaloader 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,3641 @@
1
+ // src/exceptions.ts
2
+ var InstaloaderException = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "InstaloaderException";
6
+ Object.setPrototypeOf(this, new.target.prototype);
7
+ }
8
+ };
9
+ var QueryReturnedBadRequestException = class extends InstaloaderException {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "QueryReturnedBadRequestException";
13
+ }
14
+ };
15
+ var QueryReturnedForbiddenException = class extends InstaloaderException {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "QueryReturnedForbiddenException";
19
+ }
20
+ };
21
+ var ProfileNotExistsException = class extends InstaloaderException {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "ProfileNotExistsException";
25
+ }
26
+ };
27
+ var ProfileHasNoPicsException = class extends InstaloaderException {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "ProfileHasNoPicsException";
31
+ }
32
+ };
33
+ var PrivateProfileNotFollowedException = class extends InstaloaderException {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "PrivateProfileNotFollowedException";
37
+ }
38
+ };
39
+ var LoginRequiredException = class extends InstaloaderException {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "LoginRequiredException";
43
+ }
44
+ };
45
+ var LoginException = class extends InstaloaderException {
46
+ constructor(message) {
47
+ super(message);
48
+ this.name = "LoginException";
49
+ }
50
+ };
51
+ var TwoFactorAuthRequiredException = class extends LoginException {
52
+ twoFactorInfo;
53
+ constructor(twoFactorInfo, message) {
54
+ super(message ?? "Two-factor authentication required");
55
+ this.name = "TwoFactorAuthRequiredException";
56
+ this.twoFactorInfo = twoFactorInfo;
57
+ }
58
+ };
59
+ var InvalidArgumentException = class extends InstaloaderException {
60
+ constructor(message) {
61
+ super(message);
62
+ this.name = "InvalidArgumentException";
63
+ }
64
+ };
65
+ var BadResponseException = class extends InstaloaderException {
66
+ constructor(message) {
67
+ super(message);
68
+ this.name = "BadResponseException";
69
+ }
70
+ };
71
+ var BadCredentialsException = class extends LoginException {
72
+ constructor(message) {
73
+ super(message);
74
+ this.name = "BadCredentialsException";
75
+ }
76
+ };
77
+ var ConnectionException = class extends InstaloaderException {
78
+ constructor(message) {
79
+ super(message);
80
+ this.name = "ConnectionException";
81
+ }
82
+ };
83
+ var PostChangedException = class extends InstaloaderException {
84
+ constructor(message) {
85
+ super(message);
86
+ this.name = "PostChangedException";
87
+ }
88
+ };
89
+ var QueryReturnedNotFoundException = class extends ConnectionException {
90
+ constructor(message) {
91
+ super(message);
92
+ this.name = "QueryReturnedNotFoundException";
93
+ }
94
+ };
95
+ var TooManyRequestsException = class extends ConnectionException {
96
+ constructor(message) {
97
+ super(message);
98
+ this.name = "TooManyRequestsException";
99
+ }
100
+ };
101
+ var IPhoneSupportDisabledException = class extends InstaloaderException {
102
+ constructor(message) {
103
+ super(message);
104
+ this.name = "IPhoneSupportDisabledException";
105
+ }
106
+ };
107
+ var AbortDownloadException = class extends Error {
108
+ constructor(message) {
109
+ super(message);
110
+ this.name = "AbortDownloadException";
111
+ Object.setPrototypeOf(this, new.target.prototype);
112
+ }
113
+ };
114
+ var SessionNotFoundException = class extends InstaloaderException {
115
+ constructor(message) {
116
+ super(message);
117
+ this.name = "SessionNotFoundException";
118
+ }
119
+ };
120
+ var CheckpointRequiredException = class extends ConnectionException {
121
+ checkpointUrl;
122
+ constructor(message, checkpointUrl) {
123
+ super(message);
124
+ this.name = "CheckpointRequiredException";
125
+ this.checkpointUrl = checkpointUrl;
126
+ }
127
+ };
128
+ var InvalidIteratorException = class extends InstaloaderException {
129
+ constructor(message) {
130
+ super(message);
131
+ this.name = "InvalidIteratorException";
132
+ }
133
+ };
134
+
135
+ // src/instaloadercontext.ts
136
+ import { CookieJar, Cookie } from "tough-cookie";
137
+ import { v4 as uuidv4 } from "uuid";
138
+ function defaultUserAgent() {
139
+ return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36";
140
+ }
141
+ function defaultIphoneHeaders() {
142
+ const timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset() * -60;
143
+ return {
144
+ "User-Agent": "Instagram 361.0.0.35.82 (iPad13,8; iOS 18_0; en_US; en-US; scale=2.00; 2048x2732; 674117118) AppleWebKit/420+",
145
+ "x-ads-opt-out": "1",
146
+ "x-bloks-is-panorama-enabled": "true",
147
+ "x-bloks-version-id": "16b7bd25c6c06886d57c4d455265669345a2d96625385b8ee30026ac2dc5ed97",
148
+ "x-fb-client-ip": "True",
149
+ "x-fb-connection-type": "wifi",
150
+ "x-fb-http-engine": "Liger",
151
+ "x-fb-server-cluster": "True",
152
+ "x-fb": "1",
153
+ "x-ig-abr-connection-speed-kbps": "2",
154
+ "x-ig-app-id": "124024574287414",
155
+ "x-ig-app-locale": "en-US",
156
+ "x-ig-app-startup-country": "US",
157
+ "x-ig-bandwidth-speed-kbps": "0.000",
158
+ "x-ig-capabilities": "36r/F/8=",
159
+ "x-ig-connection-speed": `${Math.floor(Math.random() * 19e3) + 1e3}kbps`,
160
+ "x-ig-connection-type": "WiFi",
161
+ "x-ig-device-locale": "en-US",
162
+ "x-ig-mapped-locale": "en-US",
163
+ "x-ig-timezone-offset": String(timezoneOffset),
164
+ "x-ig-www-claim": "0",
165
+ "x-pigeon-session-id": uuidv4(),
166
+ "x-tigon-is-retry": "False",
167
+ "x-whatsapp": "0"
168
+ };
169
+ }
170
+ function sleep(ms) {
171
+ return new Promise((resolve) => setTimeout(resolve, ms));
172
+ }
173
+ function exponentialVariate(lambda) {
174
+ return -Math.log(1 - Math.random()) / lambda;
175
+ }
176
+ var RateController = class {
177
+ _context;
178
+ _queryTimestamps = /* @__PURE__ */ new Map();
179
+ _earliestNextRequestTime = 0;
180
+ _iphoneEarliestNextRequestTime = 0;
181
+ constructor(context) {
182
+ this._context = context;
183
+ }
184
+ /**
185
+ * Wait given number of seconds.
186
+ */
187
+ async sleep(secs) {
188
+ await sleep(secs * 1e3);
189
+ }
190
+ /**
191
+ * Return how many requests of the given type can be done within a sliding window of 11 minutes.
192
+ */
193
+ countPerSlidingWindow(queryType) {
194
+ return queryType === "other" ? 75 : 200;
195
+ }
196
+ _reqsInSlidingWindow(queryType, currentTime, window) {
197
+ if (queryType !== null) {
198
+ const timestamps = this._queryTimestamps.get(queryType) || [];
199
+ return timestamps.filter((t) => t > currentTime - window);
200
+ } else {
201
+ const allTimestamps = [];
202
+ for (const [type, times] of this._queryTimestamps) {
203
+ if (type !== "iphone" && type !== "other") {
204
+ allTimestamps.push(...times.filter((t) => t > currentTime - window));
205
+ }
206
+ }
207
+ return allTimestamps;
208
+ }
209
+ }
210
+ /**
211
+ * Calculate time needed to wait before query can be executed.
212
+ */
213
+ queryWaittime(queryType, currentTime, untrackedQueries = false) {
214
+ const perTypeSlidingWindow = 660;
215
+ const iphoneSlidingWindow = 1800;
216
+ if (!this._queryTimestamps.has(queryType)) {
217
+ this._queryTimestamps.set(queryType, []);
218
+ }
219
+ const timestamps = this._queryTimestamps.get(queryType);
220
+ const filteredTimestamps = timestamps.filter(
221
+ (t) => t > currentTime - 60 * 60
222
+ );
223
+ this._queryTimestamps.set(queryType, filteredTimestamps);
224
+ const perTypeNextRequestTime = () => {
225
+ const reqs = this._reqsInSlidingWindow(
226
+ queryType,
227
+ currentTime,
228
+ perTypeSlidingWindow
229
+ );
230
+ if (reqs.length < this.countPerSlidingWindow(queryType)) {
231
+ return 0;
232
+ } else {
233
+ return Math.min(...reqs) + perTypeSlidingWindow + 6;
234
+ }
235
+ };
236
+ const gqlAccumulatedNextRequestTime = () => {
237
+ if (queryType === "iphone" || queryType === "other") {
238
+ return 0;
239
+ }
240
+ const gqlAccumulatedSlidingWindow = 600;
241
+ const gqlAccumulatedMaxCount = 275;
242
+ const reqs = this._reqsInSlidingWindow(
243
+ null,
244
+ currentTime,
245
+ gqlAccumulatedSlidingWindow
246
+ );
247
+ if (reqs.length < gqlAccumulatedMaxCount) {
248
+ return 0;
249
+ } else {
250
+ return Math.min(...reqs) + gqlAccumulatedSlidingWindow;
251
+ }
252
+ };
253
+ const untrackedNextRequestTime = () => {
254
+ if (untrackedQueries) {
255
+ if (queryType === "iphone") {
256
+ const reqs = this._reqsInSlidingWindow(
257
+ queryType,
258
+ currentTime,
259
+ iphoneSlidingWindow
260
+ );
261
+ this._iphoneEarliestNextRequestTime = Math.min(...reqs) + iphoneSlidingWindow + 18;
262
+ } else {
263
+ const reqs = this._reqsInSlidingWindow(
264
+ queryType,
265
+ currentTime,
266
+ perTypeSlidingWindow
267
+ );
268
+ this._earliestNextRequestTime = Math.min(...reqs) + perTypeSlidingWindow + 6;
269
+ }
270
+ }
271
+ return Math.max(
272
+ this._iphoneEarliestNextRequestTime,
273
+ this._earliestNextRequestTime
274
+ );
275
+ };
276
+ const iphoneNextRequest = () => {
277
+ if (queryType === "iphone") {
278
+ const reqs = this._reqsInSlidingWindow(
279
+ queryType,
280
+ currentTime,
281
+ iphoneSlidingWindow
282
+ );
283
+ if (reqs.length >= 199) {
284
+ return Math.min(...reqs) + iphoneSlidingWindow + 18;
285
+ }
286
+ }
287
+ return 0;
288
+ };
289
+ return Math.max(
290
+ 0,
291
+ Math.max(
292
+ perTypeNextRequestTime(),
293
+ gqlAccumulatedNextRequestTime(),
294
+ untrackedNextRequestTime(),
295
+ iphoneNextRequest()
296
+ ) - currentTime
297
+ );
298
+ }
299
+ /**
300
+ * Called before a query to Instagram.
301
+ */
302
+ async waitBeforeQuery(queryType) {
303
+ const currentTime = Date.now() / 1e3;
304
+ const waittime = this.queryWaittime(queryType, currentTime, false);
305
+ if (waittime > 15) {
306
+ const formattedWaittime = waittime <= 666 ? `${Math.round(waittime)} seconds` : `${Math.round(waittime / 60)} minutes`;
307
+ const resumeTime = new Date(Date.now() + waittime * 1e3);
308
+ this._context.log(
309
+ `
310
+ Too many queries in the last time. Need to wait ${formattedWaittime}, until ${resumeTime.toLocaleTimeString()}.`
311
+ );
312
+ }
313
+ if (waittime > 0) {
314
+ await this.sleep(waittime);
315
+ }
316
+ if (!this._queryTimestamps.has(queryType)) {
317
+ this._queryTimestamps.set(queryType, [Date.now() / 1e3]);
318
+ } else {
319
+ this._queryTimestamps.get(queryType).push(Date.now() / 1e3);
320
+ }
321
+ }
322
+ /**
323
+ * Handle a 429 Too Many Requests response.
324
+ */
325
+ async handle429(queryType) {
326
+ const currentTime = Date.now() / 1e3;
327
+ const waittime = this.queryWaittime(queryType, currentTime, true);
328
+ const text429 = 'Instagram responded with HTTP error "429 - Too Many Requests". Please do not run multiple instances of Instaloader in parallel or within short sequence. Also, do not use any Instagram App while Instaloader is running.';
329
+ this._context.error(text429);
330
+ if (waittime > 1.5) {
331
+ const formattedWaittime = waittime <= 666 ? `${Math.round(waittime)} seconds` : `${Math.round(waittime / 60)} minutes`;
332
+ const resumeTime = new Date(Date.now() + waittime * 1e3);
333
+ this._context.error(
334
+ `The request will be retried in ${formattedWaittime}, at ${resumeTime.toLocaleTimeString()}.`
335
+ );
336
+ }
337
+ if (waittime > 0) {
338
+ await this.sleep(waittime);
339
+ }
340
+ }
341
+ };
342
+ var InstaloaderContext = class {
343
+ userAgent;
344
+ requestTimeout;
345
+ maxConnectionAttempts;
346
+ sleep;
347
+ quiet;
348
+ iphoneSupport;
349
+ fatalStatusCodes;
350
+ _cookieJar;
351
+ _csrfToken = null;
352
+ _username = null;
353
+ _userId = null;
354
+ _iphoneHeaders;
355
+ _rateController;
356
+ _errorLog = [];
357
+ _twoFactorAuthPending = null;
358
+ /** Raise all errors instead of catching them (for testing) */
359
+ raiseAllErrors = false;
360
+ /** Cache profile from id (mapping from id to Profile) */
361
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
+ profile_id_cache = /* @__PURE__ */ new Map();
363
+ constructor(options = {}) {
364
+ this.userAgent = options.userAgent ?? defaultUserAgent();
365
+ this.requestTimeout = options.requestTimeout ?? 3e5;
366
+ this.maxConnectionAttempts = options.maxConnectionAttempts ?? 3;
367
+ this.sleep = options.sleep ?? true;
368
+ this.quiet = options.quiet ?? false;
369
+ this.iphoneSupport = options.iphoneSupport ?? true;
370
+ this.fatalStatusCodes = options.fatalStatusCodes ?? [];
371
+ this._cookieJar = new CookieJar();
372
+ this._iphoneHeaders = defaultIphoneHeaders();
373
+ this._initAnonymousCookies();
374
+ this._rateController = options.rateController ? options.rateController(this) : new RateController(this);
375
+ }
376
+ _initAnonymousCookies() {
377
+ const domain = "www.instagram.com";
378
+ const defaultCookies = {
379
+ sessionid: "",
380
+ mid: "",
381
+ ig_pr: "1",
382
+ ig_vw: "1920",
383
+ csrftoken: "",
384
+ s_network: "",
385
+ ds_user_id: ""
386
+ };
387
+ for (const [name, value] of Object.entries(defaultCookies)) {
388
+ const cookie = new Cookie({
389
+ key: name,
390
+ value,
391
+ domain,
392
+ path: "/"
393
+ });
394
+ this._cookieJar.setCookieSync(cookie, `https://${domain}/`);
395
+ }
396
+ }
397
+ /** True if this instance is logged in. */
398
+ get is_logged_in() {
399
+ return this._username !== null;
400
+ }
401
+ /** The username of the logged-in user, or null. */
402
+ get username() {
403
+ return this._username;
404
+ }
405
+ /** The user ID of the logged-in user, or null. */
406
+ get userId() {
407
+ return this._userId;
408
+ }
409
+ /** iPhone headers for API requests. */
410
+ get iphone_headers() {
411
+ return this._iphoneHeaders;
412
+ }
413
+ /** Whether any error has been reported and stored. */
414
+ get hasStoredErrors() {
415
+ return this._errorLog.length > 0;
416
+ }
417
+ /**
418
+ * Log a message to stdout (can be suppressed with quiet option).
419
+ * @param message - The message to log
420
+ * @param newline - Whether to add a newline (default true, false for inline messages)
421
+ */
422
+ log(message, newline = true) {
423
+ if (!this.quiet) {
424
+ if (newline) {
425
+ console.log(message);
426
+ } else {
427
+ process.stdout.write(message);
428
+ }
429
+ }
430
+ }
431
+ /**
432
+ * Log a non-fatal error message to stderr.
433
+ */
434
+ error(message, repeatAtEnd = true) {
435
+ console.error(message);
436
+ if (repeatAtEnd) {
437
+ this._errorLog.push(message);
438
+ }
439
+ }
440
+ /**
441
+ * Close the context and print any stored errors.
442
+ */
443
+ close() {
444
+ if (this._errorLog.length > 0 && !this.quiet) {
445
+ console.error("\nErrors or warnings occurred:");
446
+ for (const err of this._errorLog) {
447
+ console.error(err);
448
+ }
449
+ }
450
+ }
451
+ /**
452
+ * Returns default HTTP headers for requests.
453
+ */
454
+ _defaultHttpHeader(emptySessionOnly = false) {
455
+ const header = {
456
+ "Accept-Encoding": "gzip, deflate",
457
+ "Accept-Language": "en-US,en;q=0.8",
458
+ Connection: "keep-alive",
459
+ "Content-Length": "0",
460
+ Host: "www.instagram.com",
461
+ Origin: "https://www.instagram.com",
462
+ Referer: "https://www.instagram.com/",
463
+ "User-Agent": this.userAgent,
464
+ "X-Instagram-AJAX": "1",
465
+ "X-Requested-With": "XMLHttpRequest"
466
+ };
467
+ if (emptySessionOnly) {
468
+ delete header["Host"];
469
+ delete header["Origin"];
470
+ delete header["X-Instagram-AJAX"];
471
+ delete header["X-Requested-With"];
472
+ }
473
+ return header;
474
+ }
475
+ /**
476
+ * Sleep a short time if sleep is enabled.
477
+ */
478
+ async doSleep() {
479
+ if (this.sleep) {
480
+ const sleepTime = Math.min(exponentialVariate(0.6), 15);
481
+ await sleep(sleepTime * 1e3);
482
+ }
483
+ }
484
+ /**
485
+ * Get cookies as a plain object.
486
+ */
487
+ getCookies(url = "https://www.instagram.com/") {
488
+ const cookies = this._cookieJar.getCookiesSync(url);
489
+ const result = {};
490
+ for (const cookie of cookies) {
491
+ result[cookie.key] = cookie.value;
492
+ }
493
+ return result;
494
+ }
495
+ /**
496
+ * Set cookies from a plain object.
497
+ */
498
+ setCookies(cookies, url = "https://www.instagram.com/") {
499
+ const domain = new URL(url).hostname;
500
+ for (const [name, value] of Object.entries(cookies)) {
501
+ const cookie = new Cookie({
502
+ key: name,
503
+ value,
504
+ domain,
505
+ path: "/"
506
+ });
507
+ this._cookieJar.setCookieSync(cookie, url);
508
+ }
509
+ }
510
+ /**
511
+ * Save session data for later restoration.
512
+ */
513
+ saveSession() {
514
+ return this.getCookies();
515
+ }
516
+ /**
517
+ * Load session data from a saved session.
518
+ */
519
+ loadSession(username, sessionData) {
520
+ this._cookieJar = new CookieJar();
521
+ this.setCookies(sessionData);
522
+ this._csrfToken = sessionData["csrftoken"] || null;
523
+ this._username = username;
524
+ this._userId = sessionData["ds_user_id"] || null;
525
+ }
526
+ /**
527
+ * Update cookies with new values.
528
+ */
529
+ updateCookies(cookies) {
530
+ this.setCookies(cookies);
531
+ }
532
+ /**
533
+ * Build cookie header string from cookie jar.
534
+ */
535
+ _getCookieHeader(url) {
536
+ const cookies = this._cookieJar.getCookiesSync(url);
537
+ return cookies.map((c) => `${c.key}=${c.value}`).join("; ");
538
+ }
539
+ /**
540
+ * Parse and store cookies from Set-Cookie headers.
541
+ */
542
+ _storeCookies(url, headers) {
543
+ const setCookies = headers.getSetCookie?.() || [];
544
+ for (const cookieStr of setCookies) {
545
+ try {
546
+ const cookie = Cookie.parse(cookieStr);
547
+ if (cookie) {
548
+ this._cookieJar.setCookieSync(cookie, url);
549
+ }
550
+ } catch {
551
+ }
552
+ }
553
+ }
554
+ /**
555
+ * Format response error message.
556
+ */
557
+ _responseError(status, statusText, url, respJson) {
558
+ let extraFromJson = null;
559
+ if (respJson && "status" in respJson) {
560
+ if ("message" in respJson) {
561
+ extraFromJson = `"${respJson["status"]}" status, message "${respJson["message"]}"`;
562
+ } else {
563
+ extraFromJson = `"${respJson["status"]}" status`;
564
+ }
565
+ }
566
+ return `${status} ${statusText}${extraFromJson ? ` - ${extraFromJson}` : ""} when accessing ${url}`;
567
+ }
568
+ /**
569
+ * Make a JSON request to Instagram.
570
+ */
571
+ async getJson(path2, params, options = {}) {
572
+ const {
573
+ host = "www.instagram.com",
574
+ usePost = false,
575
+ attempt = 1,
576
+ headers: extraHeaders
577
+ } = options;
578
+ const isGraphqlQuery = "query_hash" in params && path2.includes("graphql/query");
579
+ const isDocIdQuery = "doc_id" in params && path2.includes("graphql/query");
580
+ const isIphoneQuery = host === "i.instagram.com";
581
+ const isOtherQuery = !isGraphqlQuery && !isDocIdQuery && host === "www.instagram.com";
582
+ try {
583
+ await this.doSleep();
584
+ if (isGraphqlQuery) {
585
+ await this._rateController.waitBeforeQuery(params["query_hash"]);
586
+ }
587
+ if (isDocIdQuery) {
588
+ await this._rateController.waitBeforeQuery(params["doc_id"]);
589
+ }
590
+ if (isIphoneQuery) {
591
+ await this._rateController.waitBeforeQuery("iphone");
592
+ }
593
+ if (isOtherQuery) {
594
+ await this._rateController.waitBeforeQuery("other");
595
+ }
596
+ const url = new URL(`https://${host}/${path2}`);
597
+ const headers = {
598
+ ...this._defaultHttpHeader(true),
599
+ Cookie: this._getCookieHeader(url.toString()),
600
+ ...extraHeaders
601
+ };
602
+ if (this._csrfToken) {
603
+ headers["X-CSRFToken"] = this._csrfToken;
604
+ }
605
+ let response;
606
+ if (usePost) {
607
+ const body = new URLSearchParams();
608
+ for (const [key, value] of Object.entries(params)) {
609
+ body.append(key, typeof value === "string" ? value : JSON.stringify(value));
610
+ }
611
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
612
+ delete headers["Content-Length"];
613
+ response = await fetch(url.toString(), {
614
+ method: "POST",
615
+ headers,
616
+ body,
617
+ redirect: "manual",
618
+ signal: AbortSignal.timeout(this.requestTimeout)
619
+ });
620
+ } else {
621
+ for (const [key, value] of Object.entries(params)) {
622
+ url.searchParams.set(
623
+ key,
624
+ typeof value === "string" ? value : JSON.stringify(value)
625
+ );
626
+ }
627
+ response = await fetch(url.toString(), {
628
+ method: "GET",
629
+ headers,
630
+ redirect: "manual",
631
+ signal: AbortSignal.timeout(this.requestTimeout)
632
+ });
633
+ }
634
+ this._storeCookies(url.toString(), response.headers);
635
+ if (this.fatalStatusCodes.includes(response.status)) {
636
+ const redirect = response.headers.get("location") ? ` redirect to ${response.headers.get("location")}` : "";
637
+ throw new AbortDownloadException(
638
+ `Query to ${url} responded with "${response.status} ${response.statusText}"${redirect}`
639
+ );
640
+ }
641
+ if (response.status >= 300 && response.status < 400) {
642
+ const redirectUrl = response.headers.get("location");
643
+ if (redirectUrl) {
644
+ this.log(`
645
+ HTTP redirect from ${url} to ${redirectUrl}`);
646
+ if (redirectUrl.startsWith("https://www.instagram.com/accounts/login") || redirectUrl.startsWith("https://i.instagram.com/accounts/login")) {
647
+ if (!this.is_logged_in) {
648
+ throw new LoginRequiredException(
649
+ "Redirected to login page. Use login() first."
650
+ );
651
+ }
652
+ throw new AbortDownloadException(
653
+ "Redirected to login page. You've been logged out, please wait some time, recreate the session and try again"
654
+ );
655
+ }
656
+ }
657
+ }
658
+ if (response.status === 400) {
659
+ let respJson2;
660
+ try {
661
+ respJson2 = await response.json();
662
+ const message = respJson2["message"];
663
+ if (message === "feedback_required" || message === "checkpoint_required" || message === "challenge_required") {
664
+ throw new AbortDownloadException(
665
+ this._responseError(response.status, response.statusText, url.toString(), respJson2)
666
+ );
667
+ }
668
+ } catch (e) {
669
+ if (e instanceof AbortDownloadException) throw e;
670
+ }
671
+ throw new QueryReturnedBadRequestException(
672
+ this._responseError(response.status, response.statusText, url.toString(), respJson2)
673
+ );
674
+ }
675
+ if (response.status === 404) {
676
+ throw new QueryReturnedNotFoundException(
677
+ this._responseError(response.status, response.statusText, url.toString())
678
+ );
679
+ }
680
+ if (response.status === 429) {
681
+ throw new TooManyRequestsException(
682
+ this._responseError(response.status, response.statusText, url.toString())
683
+ );
684
+ }
685
+ if (response.status !== 200) {
686
+ throw new ConnectionException(
687
+ this._responseError(response.status, response.statusText, url.toString())
688
+ );
689
+ }
690
+ const respJson = await response.json();
691
+ if ("status" in respJson && respJson["status"] !== "ok") {
692
+ throw new ConnectionException(
693
+ this._responseError(response.status, response.statusText, url.toString(), respJson)
694
+ );
695
+ }
696
+ return respJson;
697
+ } catch (err) {
698
+ const errorString = `JSON Query to ${path2}: ${err}`;
699
+ if (attempt >= this.maxConnectionAttempts) {
700
+ if (err instanceof QueryReturnedNotFoundException) {
701
+ throw new QueryReturnedNotFoundException(errorString);
702
+ }
703
+ throw new ConnectionException(errorString);
704
+ }
705
+ this.error(`${errorString} [retrying]`, false);
706
+ if (err instanceof TooManyRequestsException) {
707
+ if (isGraphqlQuery) {
708
+ await this._rateController.handle429(params["query_hash"]);
709
+ }
710
+ if (isDocIdQuery) {
711
+ await this._rateController.handle429(params["doc_id"]);
712
+ }
713
+ if (isIphoneQuery) {
714
+ await this._rateController.handle429("iphone");
715
+ }
716
+ if (isOtherQuery) {
717
+ await this._rateController.handle429("other");
718
+ }
719
+ }
720
+ return this.getJson(path2, params, {
721
+ host,
722
+ usePost,
723
+ attempt: attempt + 1,
724
+ ...extraHeaders !== void 0 && { headers: extraHeaders }
725
+ });
726
+ }
727
+ }
728
+ /**
729
+ * Do a GraphQL Query.
730
+ */
731
+ async graphql_query(queryHash, variables, referer) {
732
+ const headers = {
733
+ ...this._defaultHttpHeader(true),
734
+ authority: "www.instagram.com",
735
+ scheme: "https",
736
+ accept: "*/*"
737
+ };
738
+ delete headers["Connection"];
739
+ delete headers["Content-Length"];
740
+ if (referer) {
741
+ headers["referer"] = encodeURIComponent(referer);
742
+ }
743
+ const variablesJson = JSON.stringify(variables);
744
+ const respJson = await this.getJson(
745
+ "graphql/query",
746
+ { query_hash: queryHash, variables: variablesJson },
747
+ { headers }
748
+ );
749
+ if (!("status" in respJson)) {
750
+ this.error('GraphQL response did not contain a "status" field.');
751
+ }
752
+ return respJson;
753
+ }
754
+ /**
755
+ * Do a doc_id-based GraphQL Query using POST.
756
+ */
757
+ async doc_id_graphql_query(docId, variables, referer) {
758
+ const headers = {
759
+ ...this._defaultHttpHeader(true),
760
+ authority: "www.instagram.com",
761
+ scheme: "https",
762
+ accept: "*/*"
763
+ };
764
+ delete headers["Connection"];
765
+ delete headers["Content-Length"];
766
+ if (referer) {
767
+ headers["referer"] = encodeURIComponent(referer);
768
+ }
769
+ const variablesJson = JSON.stringify(variables);
770
+ const respJson = await this.getJson(
771
+ "graphql/query",
772
+ { variables: variablesJson, doc_id: docId, server_timestamps: "true" },
773
+ { usePost: true, headers }
774
+ );
775
+ if (!("status" in respJson)) {
776
+ this.error('GraphQL response did not contain a "status" field.');
777
+ }
778
+ return respJson;
779
+ }
780
+ /**
781
+ * JSON request to i.instagram.com.
782
+ */
783
+ async get_iphone_json(path2, params) {
784
+ const headers = {
785
+ ...this._iphoneHeaders,
786
+ "ig-intended-user-id": this._userId || "",
787
+ "x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6)
788
+ };
789
+ const cookies = this.getCookies("https://i.instagram.com/");
790
+ const headerCookiesMapping = {
791
+ "x-mid": "mid",
792
+ "ig-u-ds-user-id": "ds_user_id",
793
+ "x-ig-device-id": "ig_did",
794
+ "x-ig-family-device-id": "ig_did",
795
+ family_device_id: "ig_did"
796
+ };
797
+ for (const [headerKey, cookieKey] of Object.entries(headerCookiesMapping)) {
798
+ if (cookieKey in cookies && !(headerKey in headers)) {
799
+ headers[headerKey] = cookies[cookieKey];
800
+ }
801
+ }
802
+ if ("rur" in cookies && !("ig-u-rur" in headers)) {
803
+ const rurValue = cookies["rur"];
804
+ headers["ig-u-rur"] = rurValue.replace(/^"|"$/g, "");
805
+ }
806
+ const response = await this.getJson(path2, params, {
807
+ host: "i.instagram.com",
808
+ headers
809
+ });
810
+ return response;
811
+ }
812
+ /**
813
+ * HEAD a URL anonymously.
814
+ */
815
+ async head(url, options = {}) {
816
+ const { allowRedirects = false } = options;
817
+ const response = await fetch(url, {
818
+ method: "HEAD",
819
+ redirect: allowRedirects ? "follow" : "manual",
820
+ headers: {
821
+ "User-Agent": this.userAgent
822
+ },
823
+ signal: AbortSignal.timeout(this.requestTimeout)
824
+ });
825
+ if (response.status === 200 || response.status >= 300 && response.status < 400) {
826
+ const headers = /* @__PURE__ */ new Map();
827
+ response.headers.forEach((value, key) => {
828
+ headers.set(key, value);
829
+ });
830
+ return { headers };
831
+ }
832
+ if (response.status === 403) {
833
+ throw new QueryReturnedForbiddenException(
834
+ this._responseError(response.status, response.statusText, url)
835
+ );
836
+ }
837
+ if (response.status === 404) {
838
+ throw new QueryReturnedNotFoundException(
839
+ this._responseError(response.status, response.statusText, url)
840
+ );
841
+ }
842
+ throw new ConnectionException(
843
+ this._responseError(response.status, response.statusText, url)
844
+ );
845
+ }
846
+ /**
847
+ * Test if logged in by querying the current user.
848
+ */
849
+ async testLogin() {
850
+ try {
851
+ const data = await this.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {});
852
+ const userData = data["data"]?.["user"];
853
+ return userData ? userData["username"] : null;
854
+ } catch (err) {
855
+ if (err instanceof AbortDownloadException || err instanceof ConnectionException) {
856
+ this.error(`Error when checking if logged in: ${err}`);
857
+ return null;
858
+ }
859
+ throw err;
860
+ }
861
+ }
862
+ /**
863
+ * Login to Instagram.
864
+ */
865
+ async login(username, password) {
866
+ this._cookieJar = new CookieJar();
867
+ this._initAnonymousCookies();
868
+ const initUrl = "https://www.instagram.com/";
869
+ const initResponse = await fetch(initUrl, {
870
+ headers: {
871
+ "User-Agent": this.userAgent,
872
+ Cookie: this._getCookieHeader(initUrl)
873
+ },
874
+ signal: AbortSignal.timeout(this.requestTimeout)
875
+ });
876
+ this._storeCookies(initUrl, initResponse.headers);
877
+ const cookies = this.getCookies();
878
+ const csrfToken = cookies["csrftoken"];
879
+ if (!csrfToken) {
880
+ throw new LoginException("Failed to get CSRF token from Instagram.");
881
+ }
882
+ this._csrfToken = csrfToken;
883
+ await this.doSleep();
884
+ const encPassword = `#PWD_INSTAGRAM_BROWSER:0:${Math.floor(Date.now() / 1e3)}:${password}`;
885
+ const loginUrl = "https://www.instagram.com/api/v1/web/accounts/login/ajax/";
886
+ const loginResponse = await fetch(loginUrl, {
887
+ method: "POST",
888
+ headers: {
889
+ "User-Agent": this.userAgent,
890
+ "Content-Type": "application/x-www-form-urlencoded",
891
+ "X-CSRFToken": csrfToken,
892
+ Cookie: this._getCookieHeader(loginUrl),
893
+ Referer: "https://www.instagram.com/"
894
+ },
895
+ body: new URLSearchParams({
896
+ enc_password: encPassword,
897
+ username
898
+ }),
899
+ redirect: "follow",
900
+ signal: AbortSignal.timeout(this.requestTimeout)
901
+ });
902
+ this._storeCookies(loginUrl, loginResponse.headers);
903
+ let respJson;
904
+ try {
905
+ respJson = await loginResponse.json();
906
+ } catch {
907
+ throw new LoginException(
908
+ `Login error: JSON decode fail, ${loginResponse.status} - ${loginResponse.statusText}.`
909
+ );
910
+ }
911
+ if (respJson["two_factor_required"]) {
912
+ const twoFactorInfoResp = respJson["two_factor_info"];
913
+ const twoFactorId = twoFactorInfoResp["two_factor_identifier"];
914
+ this._twoFactorAuthPending = {
915
+ csrfToken,
916
+ cookies: this.getCookies(),
917
+ username,
918
+ twoFactorId
919
+ };
920
+ const twoFactorInfo = {
921
+ username,
922
+ identifier: twoFactorId
923
+ };
924
+ if (twoFactorInfoResp["obfuscated_phone_number"] !== void 0) {
925
+ twoFactorInfo.obfuscatedPhoneNumber = twoFactorInfoResp["obfuscated_phone_number"];
926
+ }
927
+ if (twoFactorInfoResp["show_messenger_code_option"] !== void 0) {
928
+ twoFactorInfo.showMessengerCodeOption = twoFactorInfoResp["show_messenger_code_option"];
929
+ }
930
+ if (twoFactorInfoResp["show_new_login_screen"] !== void 0) {
931
+ twoFactorInfo.showNewLoginScreen = twoFactorInfoResp["show_new_login_screen"];
932
+ }
933
+ if (twoFactorInfoResp["show_trusted_device_option"] !== void 0) {
934
+ twoFactorInfo.showTrustedDeviceOption = twoFactorInfoResp["show_trusted_device_option"];
935
+ }
936
+ if (twoFactorInfoResp["pending_trusted_notification_polling"] !== void 0) {
937
+ twoFactorInfo.pendingTrustedNotificationPolling = twoFactorInfoResp["pending_trusted_notification_polling"];
938
+ }
939
+ throw new TwoFactorAuthRequiredException(
940
+ twoFactorInfo,
941
+ "Login error: two-factor authentication required."
942
+ );
943
+ }
944
+ if (respJson["checkpoint_url"]) {
945
+ throw new LoginException(
946
+ `Login: Checkpoint required. Point your browser to ${respJson["checkpoint_url"]} - follow the instructions, then retry.`
947
+ );
948
+ }
949
+ if (respJson["status"] !== "ok") {
950
+ const message = respJson["message"];
951
+ if (message) {
952
+ throw new LoginException(
953
+ `Login error: "${respJson["status"]}" status, message "${message}".`
954
+ );
955
+ }
956
+ throw new LoginException(`Login error: "${respJson["status"]}" status.`);
957
+ }
958
+ if (!("authenticated" in respJson)) {
959
+ const message = respJson["message"];
960
+ if (message) {
961
+ throw new LoginException(`Login error: Unexpected response, "${message}".`);
962
+ }
963
+ throw new LoginException(
964
+ "Login error: Unexpected response, this might indicate a blocked IP."
965
+ );
966
+ }
967
+ if (!respJson["authenticated"]) {
968
+ if (respJson["user"]) {
969
+ throw new BadCredentialsException("Login error: Wrong password.");
970
+ }
971
+ throw new LoginException(`Login error: User ${username} does not exist.`);
972
+ }
973
+ const newCookies = this.getCookies();
974
+ this._csrfToken = newCookies["csrftoken"] || csrfToken;
975
+ this._username = username;
976
+ this._userId = respJson["userId"] || newCookies["ds_user_id"] || null;
977
+ }
978
+ /**
979
+ * Second step of login if 2FA is enabled.
980
+ */
981
+ async twoFactorLogin(twoFactorCode) {
982
+ if (!this._twoFactorAuthPending) {
983
+ throw new InvalidArgumentException("No two-factor authentication pending.");
984
+ }
985
+ const { csrfToken, cookies, username, twoFactorId } = this._twoFactorAuthPending;
986
+ this._cookieJar = new CookieJar();
987
+ this.setCookies(cookies);
988
+ const loginUrl = "https://www.instagram.com/accounts/login/ajax/two_factor/";
989
+ const response = await fetch(loginUrl, {
990
+ method: "POST",
991
+ headers: {
992
+ "User-Agent": this.userAgent,
993
+ "Content-Type": "application/x-www-form-urlencoded",
994
+ "X-CSRFToken": csrfToken,
995
+ Cookie: this._getCookieHeader(loginUrl),
996
+ Referer: "https://www.instagram.com/"
997
+ },
998
+ body: new URLSearchParams({
999
+ username,
1000
+ verificationCode: twoFactorCode,
1001
+ identifier: twoFactorId
1002
+ }),
1003
+ redirect: "follow",
1004
+ signal: AbortSignal.timeout(this.requestTimeout)
1005
+ });
1006
+ this._storeCookies(loginUrl, response.headers);
1007
+ const respJson = await response.json();
1008
+ if (respJson["status"] !== "ok") {
1009
+ const message = respJson["message"];
1010
+ if (message) {
1011
+ throw new BadCredentialsException(`2FA error: ${message}`);
1012
+ }
1013
+ throw new BadCredentialsException(`2FA error: "${respJson["status"]}" status.`);
1014
+ }
1015
+ const newCookies = this.getCookies();
1016
+ this._csrfToken = newCookies["csrftoken"] || csrfToken;
1017
+ this._username = username;
1018
+ this._userId = newCookies["ds_user_id"] || null;
1019
+ this._twoFactorAuthPending = null;
1020
+ }
1021
+ };
1022
+
1023
+ // src/nodeiterator.ts
1024
+ import { createHash } from "crypto";
1025
+ var FrozenNodeIterator = class _FrozenNodeIterator {
1026
+ queryHash;
1027
+ queryVariables;
1028
+ queryReferer;
1029
+ contextUsername;
1030
+ totalIndex;
1031
+ bestBefore;
1032
+ remainingData;
1033
+ firstNode;
1034
+ docId;
1035
+ constructor(params) {
1036
+ this.queryHash = params.queryHash;
1037
+ this.queryVariables = params.queryVariables;
1038
+ this.queryReferer = params.queryReferer;
1039
+ this.contextUsername = params.contextUsername;
1040
+ this.totalIndex = params.totalIndex;
1041
+ this.bestBefore = params.bestBefore;
1042
+ this.remainingData = params.remainingData;
1043
+ this.firstNode = params.firstNode;
1044
+ this.docId = params.docId;
1045
+ }
1046
+ /**
1047
+ * Convert to plain object for JSON serialization.
1048
+ */
1049
+ toObject() {
1050
+ return {
1051
+ queryHash: this.queryHash,
1052
+ queryVariables: this.queryVariables,
1053
+ queryReferer: this.queryReferer,
1054
+ contextUsername: this.contextUsername,
1055
+ totalIndex: this.totalIndex,
1056
+ bestBefore: this.bestBefore,
1057
+ remainingData: this.remainingData,
1058
+ firstNode: this.firstNode,
1059
+ docId: this.docId
1060
+ };
1061
+ }
1062
+ /**
1063
+ * Create from plain object (JSON deserialization).
1064
+ */
1065
+ static fromObject(obj) {
1066
+ return new _FrozenNodeIterator(obj);
1067
+ }
1068
+ };
1069
+ var NodeIterator = class _NodeIterator {
1070
+ static _graphql_page_length = 12;
1071
+ static _shelf_life_days = 29;
1072
+ _context;
1073
+ _queryHash;
1074
+ _docId;
1075
+ _edgeExtractor;
1076
+ _nodeWrapper;
1077
+ _queryVariables;
1078
+ _queryReferer;
1079
+ _isFirst;
1080
+ _pageIndex = 0;
1081
+ _totalIndex = 0;
1082
+ _data = null;
1083
+ _bestBefore = null;
1084
+ _firstNode = null;
1085
+ _initialized = false;
1086
+ _initPromise = null;
1087
+ constructor(options) {
1088
+ this._context = options.context;
1089
+ this._queryHash = options.queryHash;
1090
+ this._docId = options.docId;
1091
+ this._edgeExtractor = options.edgeExtractor;
1092
+ this._nodeWrapper = options.nodeWrapper;
1093
+ this._queryVariables = options.queryVariables ?? {};
1094
+ this._queryReferer = options.queryReferer ?? null;
1095
+ this._isFirst = options.isFirst;
1096
+ if (options.firstData !== void 0) {
1097
+ this._data = options.firstData;
1098
+ this._bestBefore = new Date(Date.now() + _NodeIterator._shelf_life_days * 24 * 60 * 60 * 1e3);
1099
+ this._initialized = true;
1100
+ } else {
1101
+ this._initPromise = this._initialize();
1102
+ }
1103
+ }
1104
+ async _initialize() {
1105
+ if (this._initialized) return;
1106
+ this._data = await this._query();
1107
+ this._initialized = true;
1108
+ }
1109
+ async _ensureInitialized() {
1110
+ if (!this._initialized && this._initPromise) {
1111
+ await this._initPromise;
1112
+ }
1113
+ }
1114
+ async _query(after) {
1115
+ if (this._docId !== void 0) {
1116
+ return this._queryDocId(this._docId, after);
1117
+ } else {
1118
+ if (this._queryHash === null) {
1119
+ throw new Error("Either queryHash or docId must be provided");
1120
+ }
1121
+ return this._queryQueryHash(this._queryHash, after);
1122
+ }
1123
+ }
1124
+ async _queryDocId(docId, after) {
1125
+ const paginationVariables = {
1126
+ __relay_internal__pv__PolarisFeedShareMenurelayprovider: false
1127
+ };
1128
+ if (after !== void 0) {
1129
+ paginationVariables["after"] = after;
1130
+ paginationVariables["before"] = null;
1131
+ paginationVariables["first"] = 12;
1132
+ paginationVariables["last"] = null;
1133
+ }
1134
+ const response = await this._context.doc_id_graphql_query(
1135
+ docId,
1136
+ { ...this._queryVariables, ...paginationVariables },
1137
+ this._queryReferer ?? void 0
1138
+ );
1139
+ const data = this._edgeExtractor(response);
1140
+ this._bestBefore = new Date(Date.now() + _NodeIterator._shelf_life_days * 24 * 60 * 60 * 1e3);
1141
+ return data;
1142
+ }
1143
+ async _queryQueryHash(queryHash, after) {
1144
+ const paginationVariables = {
1145
+ first: _NodeIterator._graphql_page_length
1146
+ };
1147
+ if (after !== void 0) {
1148
+ paginationVariables["after"] = after;
1149
+ }
1150
+ const response = await this._context.graphql_query(
1151
+ queryHash,
1152
+ { ...this._queryVariables, ...paginationVariables },
1153
+ this._queryReferer ?? void 0
1154
+ );
1155
+ const data = this._edgeExtractor(response);
1156
+ this._bestBefore = new Date(Date.now() + _NodeIterator._shelf_life_days * 24 * 60 * 60 * 1e3);
1157
+ return data;
1158
+ }
1159
+ /**
1160
+ * The count as returned by Instagram.
1161
+ * This is not always the total count this iterator will yield.
1162
+ */
1163
+ get count() {
1164
+ return this._data?.count;
1165
+ }
1166
+ /**
1167
+ * Number of items that have already been returned.
1168
+ */
1169
+ get totalIndex() {
1170
+ return this._totalIndex;
1171
+ }
1172
+ /**
1173
+ * Magic string for easily identifying a matching iterator file for resuming.
1174
+ * Two NodeIterators are matching if and only if they have the same magic.
1175
+ */
1176
+ get magic() {
1177
+ const data = JSON.stringify([
1178
+ this._queryHash,
1179
+ this._queryVariables,
1180
+ this._queryReferer,
1181
+ this._context.username
1182
+ ]);
1183
+ const hash = createHash("sha256").update(data).digest();
1184
+ return hash.subarray(0, 6).toString("base64url");
1185
+ }
1186
+ /**
1187
+ * If this iterator has produced any items, returns the first item produced.
1188
+ *
1189
+ * It is possible to override what is considered the first item by passing
1190
+ * a callback function as the `isFirst` parameter when creating the class.
1191
+ */
1192
+ get firstItem() {
1193
+ return this._firstNode !== null ? this._nodeWrapper(this._firstNode) : null;
1194
+ }
1195
+ /**
1196
+ * Static page length used for pagination.
1197
+ */
1198
+ static pageLength() {
1199
+ return _NodeIterator._graphql_page_length;
1200
+ }
1201
+ /**
1202
+ * Freeze the iterator for later resuming.
1203
+ */
1204
+ freeze() {
1205
+ let remainingData = null;
1206
+ if (this._data !== null) {
1207
+ remainingData = {
1208
+ ...this._data,
1209
+ edges: this._data.edges.slice(Math.max(this._pageIndex - 1, 0))
1210
+ };
1211
+ }
1212
+ return new FrozenNodeIterator({
1213
+ queryHash: this._queryHash,
1214
+ queryVariables: this._queryVariables,
1215
+ queryReferer: this._queryReferer,
1216
+ contextUsername: this._context.username,
1217
+ totalIndex: Math.max(this._totalIndex - 1, 0),
1218
+ bestBefore: this._bestBefore ? this._bestBefore.getTime() / 1e3 : null,
1219
+ remainingData,
1220
+ firstNode: this._firstNode,
1221
+ docId: this._docId
1222
+ });
1223
+ }
1224
+ /**
1225
+ * Use this iterator for resuming from earlier iteration.
1226
+ *
1227
+ * @throws InvalidArgumentException if the iterator has already been used or the frozen state doesn't match
1228
+ */
1229
+ thaw(frozen) {
1230
+ if (this._totalIndex || this._pageIndex) {
1231
+ throw new InvalidArgumentException("thaw() called on already-used iterator.");
1232
+ }
1233
+ if (this._queryHash !== frozen.queryHash || JSON.stringify(this._queryVariables) !== JSON.stringify(frozen.queryVariables) || this._queryReferer !== frozen.queryReferer || this._context.username !== frozen.contextUsername || this._docId !== frozen.docId) {
1234
+ throw new InvalidArgumentException("Mismatching resume information.");
1235
+ }
1236
+ if (!frozen.bestBefore) {
1237
+ throw new InvalidArgumentException('"best before" date missing.');
1238
+ }
1239
+ if (frozen.remainingData === null) {
1240
+ throw new InvalidArgumentException('"remaining_data" missing.');
1241
+ }
1242
+ this._totalIndex = frozen.totalIndex;
1243
+ this._bestBefore = new Date(frozen.bestBefore * 1e3);
1244
+ this._data = frozen.remainingData;
1245
+ this._initialized = true;
1246
+ if (frozen.firstNode !== null) {
1247
+ this._firstNode = frozen.firstNode;
1248
+ }
1249
+ }
1250
+ /**
1251
+ * Async iterator implementation.
1252
+ */
1253
+ async *[Symbol.asyncIterator]() {
1254
+ await this._ensureInitialized();
1255
+ while (true) {
1256
+ if (this._data === null) {
1257
+ return;
1258
+ }
1259
+ while (this._pageIndex < this._data.edges.length) {
1260
+ const edge = this._data.edges[this._pageIndex];
1261
+ if (!edge) break;
1262
+ const node = edge.node;
1263
+ this._pageIndex += 1;
1264
+ this._totalIndex += 1;
1265
+ const item = this._nodeWrapper(node);
1266
+ if (this._isFirst !== void 0) {
1267
+ if (this._isFirst(item, this.firstItem)) {
1268
+ this._firstNode = node;
1269
+ }
1270
+ } else {
1271
+ if (this._firstNode === null) {
1272
+ this._firstNode = node;
1273
+ }
1274
+ }
1275
+ yield item;
1276
+ }
1277
+ if (this._data.page_info?.has_next_page && this._data.page_info.end_cursor) {
1278
+ const queryResponse = await this._query(this._data.page_info.end_cursor);
1279
+ if (JSON.stringify(this._data.edges) !== JSON.stringify(queryResponse.edges) && queryResponse.edges.length > 0) {
1280
+ this._pageIndex = 0;
1281
+ this._data = queryResponse;
1282
+ continue;
1283
+ }
1284
+ }
1285
+ return;
1286
+ }
1287
+ }
1288
+ };
1289
+ async function resumableIteration(options) {
1290
+ const { context, iterator, load, save: _save, formatPath, checkBbd = true, enabled = true } = options;
1291
+ if (!enabled || !(iterator instanceof NodeIterator)) {
1292
+ return { isResuming: false, startIndex: 0 };
1293
+ }
1294
+ const nodeIterator = iterator;
1295
+ let isResuming = false;
1296
+ let startIndex = 0;
1297
+ const resumeFilePath = formatPath(nodeIterator.magic);
1298
+ try {
1299
+ const fni = await Promise.resolve(load(context, resumeFilePath));
1300
+ if (!(fni instanceof FrozenNodeIterator)) {
1301
+ throw new InvalidArgumentException("Invalid type.");
1302
+ }
1303
+ if (checkBbd && fni.bestBefore && new Date(fni.bestBefore * 1e3) < /* @__PURE__ */ new Date()) {
1304
+ throw new InvalidArgumentException('"Best before" date exceeded.');
1305
+ }
1306
+ nodeIterator.thaw(fni);
1307
+ isResuming = true;
1308
+ startIndex = nodeIterator.totalIndex;
1309
+ context.log(`Resuming from ${resumeFilePath}.`);
1310
+ } catch (e) {
1311
+ if (e instanceof InvalidArgumentException) {
1312
+ context.error(`Warning: Not resuming from ${resumeFilePath}: ${e.message}`);
1313
+ }
1314
+ }
1315
+ return { isResuming, startIndex };
1316
+ }
1317
+
1318
+ // src/structures.ts
1319
+ var HASHTAG_REGEX = /(?:#)(\w{1,150})/g;
1320
+ var MENTION_REGEX = /(?:^|[^\w\n]|_)(?:@)(\w(?:(?:\w|(?:\.(?!\.))){0,28}(?:\w))?)/g;
1321
+ function optionalNormalize(str) {
1322
+ if (str != null) {
1323
+ return str.normalize("NFC");
1324
+ }
1325
+ return null;
1326
+ }
1327
+ function shortcodeToMediaid(code) {
1328
+ if (code.length > 11) {
1329
+ throw new InvalidArgumentException(
1330
+ `Wrong shortcode "${code}", unable to convert to mediaid.`
1331
+ );
1332
+ }
1333
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1334
+ let mediaid = BigInt(0);
1335
+ for (const char of code) {
1336
+ mediaid = mediaid * BigInt(64) + BigInt(alphabet.indexOf(char));
1337
+ }
1338
+ return mediaid;
1339
+ }
1340
+ function mediaidToShortcode(mediaid) {
1341
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1342
+ let shortcode = "";
1343
+ let id = mediaid;
1344
+ while (id > 0) {
1345
+ shortcode = alphabet[Number(id % BigInt(64))] + shortcode;
1346
+ id = id / BigInt(64);
1347
+ }
1348
+ return shortcode || "A";
1349
+ }
1350
+ function extractHashtags(text) {
1351
+ const matches = [];
1352
+ let match;
1353
+ const regex = new RegExp(HASHTAG_REGEX.source, "g");
1354
+ while ((match = regex.exec(text.toLowerCase())) !== null) {
1355
+ matches.push(match[1]);
1356
+ }
1357
+ return matches;
1358
+ }
1359
+ function extractMentions(text) {
1360
+ const matches = [];
1361
+ let match;
1362
+ const regex = new RegExp(MENTION_REGEX.source, "g");
1363
+ while ((match = regex.exec(text.toLowerCase())) !== null) {
1364
+ matches.push(match[1]);
1365
+ }
1366
+ return matches;
1367
+ }
1368
+ function ellipsifyCaption(caption) {
1369
+ const pcaption = caption.split("\n").filter((s) => s).map((s) => s.replace(/\//g, "\u2215")).join(" ").trim();
1370
+ return pcaption.length > 31 ? pcaption.slice(0, 30) + "\u2026" : pcaption;
1371
+ }
1372
+ var PostComment = class _PostComment {
1373
+ _context;
1374
+ _node;
1375
+ _answers;
1376
+ _post;
1377
+ constructor(context, node, answers, post) {
1378
+ this._context = context;
1379
+ this._node = node;
1380
+ this._answers = answers;
1381
+ this._post = post;
1382
+ }
1383
+ /**
1384
+ * Create a PostComment from an iPhone API struct.
1385
+ */
1386
+ static fromIphoneStruct(context, media, answers, post) {
1387
+ return new _PostComment(
1388
+ context,
1389
+ {
1390
+ id: media["pk"],
1391
+ created_at: media["created_at"],
1392
+ text: media["text"],
1393
+ edge_liked_by: {
1394
+ count: media["comment_like_count"]
1395
+ },
1396
+ iphone_struct: media
1397
+ },
1398
+ answers,
1399
+ post
1400
+ );
1401
+ }
1402
+ /** ID number of comment. */
1403
+ get id() {
1404
+ return this._node["id"];
1405
+ }
1406
+ /** Timestamp when comment was created (UTC). */
1407
+ get created_at_utc() {
1408
+ return new Date(this._node["created_at"] * 1e3);
1409
+ }
1410
+ /** Comment text. */
1411
+ get text() {
1412
+ return this._node["text"];
1413
+ }
1414
+ /** Owner Profile of the comment. */
1415
+ get owner() {
1416
+ if ("iphone_struct" in this._node) {
1417
+ const iphoneStruct = this._node["iphone_struct"];
1418
+ return Profile.fromIphoneStruct(
1419
+ this._context,
1420
+ iphoneStruct["user"]
1421
+ );
1422
+ }
1423
+ return new Profile(this._context, this._node["owner"]);
1424
+ }
1425
+ /** Number of likes on comment. */
1426
+ get likes_count() {
1427
+ const edgeLikedBy = this._node["edge_liked_by"];
1428
+ return edgeLikedBy?.["count"] ?? 0;
1429
+ }
1430
+ /** Iterator which yields all PostCommentAnswer for the comment. */
1431
+ get answers() {
1432
+ return this._answers;
1433
+ }
1434
+ toString() {
1435
+ return `<PostComment ${this.id} of ${this._post.shortcode}>`;
1436
+ }
1437
+ };
1438
+ var Post = class _Post {
1439
+ _context;
1440
+ _node;
1441
+ _owner_profile;
1442
+ _full_metadata_dict = null;
1443
+ _location = null;
1444
+ _iphone_struct_ = null;
1445
+ /**
1446
+ * @param context InstaloaderContext used for additional queries if necessary.
1447
+ * @param node Node structure, as returned by Instagram.
1448
+ * @param owner_profile The Profile of the owner, if already known at creation.
1449
+ */
1450
+ constructor(context, node, owner_profile = null) {
1451
+ if (!("shortcode" in node) && !("code" in node)) {
1452
+ throw new Error("Node must contain 'shortcode' or 'code'");
1453
+ }
1454
+ this._context = context;
1455
+ this._node = node;
1456
+ this._owner_profile = owner_profile;
1457
+ if ("iphone_struct" in node) {
1458
+ this._iphone_struct_ = node["iphone_struct"];
1459
+ }
1460
+ }
1461
+ /**
1462
+ * Create a post object from a given shortcode.
1463
+ */
1464
+ static async fromShortcode(context, shortcode) {
1465
+ const post = new _Post(context, { shortcode });
1466
+ post._node = await post._getFullMetadata();
1467
+ return post;
1468
+ }
1469
+ /**
1470
+ * Create a post object from a given mediaid.
1471
+ */
1472
+ static async fromMediaid(context, mediaid) {
1473
+ return _Post.fromShortcode(context, mediaidToShortcode(mediaid));
1474
+ }
1475
+ /**
1476
+ * Create a post from a given iphone_struct.
1477
+ */
1478
+ static fromIphoneStruct(context, media) {
1479
+ const mediaTypes = {
1480
+ 1: "GraphImage",
1481
+ 2: "GraphVideo",
1482
+ 8: "GraphSidecar"
1483
+ };
1484
+ const mediaType = media["media_type"];
1485
+ const typename = mediaTypes[mediaType] ?? "GraphImage";
1486
+ const caption = media["caption"];
1487
+ const fakeNode = {
1488
+ shortcode: media["code"],
1489
+ id: media["pk"],
1490
+ __typename: typename,
1491
+ is_video: typename === "GraphVideo",
1492
+ date: media["taken_at"],
1493
+ caption: caption?.["text"] ?? null,
1494
+ title: media["title"] ?? null,
1495
+ viewer_has_liked: media["has_liked"],
1496
+ edge_media_preview_like: { count: media["like_count"] },
1497
+ accessibility_caption: media["accessibility_caption"] ?? null,
1498
+ comments: media["comment_count"] ?? 0,
1499
+ iphone_struct: media
1500
+ };
1501
+ try {
1502
+ const imageVersions = media["image_versions2"];
1503
+ const candidates = imageVersions?.["candidates"];
1504
+ if (candidates && candidates.length > 0) {
1505
+ const firstCandidate = candidates[0];
1506
+ fakeNode["display_url"] = firstCandidate["url"];
1507
+ }
1508
+ } catch {
1509
+ }
1510
+ try {
1511
+ const videoVersions = media["video_versions"];
1512
+ if (videoVersions && videoVersions.length > 0) {
1513
+ const lastVideo = videoVersions[videoVersions.length - 1];
1514
+ fakeNode["video_url"] = lastVideo["url"];
1515
+ fakeNode["video_duration"] = media["video_duration"];
1516
+ fakeNode["video_view_count"] = media["view_count"];
1517
+ }
1518
+ } catch {
1519
+ }
1520
+ try {
1521
+ const carouselMedia = media["carousel_media"];
1522
+ if (carouselMedia) {
1523
+ fakeNode["edge_sidecar_to_children"] = {
1524
+ edges: carouselMedia.map(
1525
+ (node) => ({ node: _Post._convertIphoneCarousel(node, mediaTypes) })
1526
+ )
1527
+ };
1528
+ }
1529
+ } catch {
1530
+ }
1531
+ const ownerProfile = "user" in media ? Profile.fromIphoneStruct(context, media["user"]) : null;
1532
+ return new _Post(context, fakeNode, ownerProfile);
1533
+ }
1534
+ static _convertIphoneCarousel(iphoneNode, mediaTypes) {
1535
+ const mediaType = iphoneNode["media_type"];
1536
+ const imageVersions = iphoneNode["image_versions2"];
1537
+ const candidates = imageVersions["candidates"];
1538
+ const firstCandidate = candidates[0];
1539
+ const fakeNode = {
1540
+ display_url: firstCandidate["url"],
1541
+ is_video: mediaTypes[mediaType] === "GraphVideo"
1542
+ };
1543
+ const videoVersions = iphoneNode["video_versions"];
1544
+ if (videoVersions && videoVersions.length > 0) {
1545
+ const firstVideo = videoVersions[0];
1546
+ fakeNode["video_url"] = firstVideo["url"];
1547
+ }
1548
+ return fakeNode;
1549
+ }
1550
+ /** The values of __typename fields that the Post class can handle. */
1551
+ static supportedGraphqlTypes() {
1552
+ return ["GraphImage", "GraphVideo", "GraphSidecar"];
1553
+ }
1554
+ /** Media shortcode. URL of the post is instagram.com/p/<shortcode>/. */
1555
+ get shortcode() {
1556
+ return this._node["shortcode"] ?? this._node["code"];
1557
+ }
1558
+ /** The mediaid is a decimal representation of the media shortcode. */
1559
+ get mediaid() {
1560
+ return BigInt(this._node["id"]);
1561
+ }
1562
+ /** Title of post */
1563
+ get title() {
1564
+ try {
1565
+ return this._field("title");
1566
+ } catch {
1567
+ return null;
1568
+ }
1569
+ }
1570
+ toString() {
1571
+ return `<Post ${this.shortcode}>`;
1572
+ }
1573
+ equals(other) {
1574
+ return this.shortcode === other.shortcode;
1575
+ }
1576
+ async _obtainMetadata() {
1577
+ if (!this._full_metadata_dict) {
1578
+ const result = await this._context.doc_id_graphql_query(
1579
+ "8845758582119845",
1580
+ { shortcode: this.shortcode }
1581
+ );
1582
+ const data = result["data"];
1583
+ const picJson = data["xdt_shortcode_media"];
1584
+ if (picJson === null) {
1585
+ throw new BadResponseException("Fetching Post metadata failed.");
1586
+ }
1587
+ const xdtTypes = {
1588
+ XDTGraphImage: "GraphImage",
1589
+ XDTGraphVideo: "GraphVideo",
1590
+ XDTGraphSidecar: "GraphSidecar"
1591
+ };
1592
+ const typename = picJson["__typename"];
1593
+ if (!(typename in xdtTypes)) {
1594
+ throw new BadResponseException(
1595
+ `Unknown __typename in metadata: ${typename}.`
1596
+ );
1597
+ }
1598
+ picJson["__typename"] = xdtTypes[typename];
1599
+ this._full_metadata_dict = picJson;
1600
+ if (this.shortcode !== this._full_metadata_dict["shortcode"]) {
1601
+ Object.assign(this._node, this._full_metadata_dict);
1602
+ throw new PostChangedException();
1603
+ }
1604
+ }
1605
+ }
1606
+ async _getFullMetadata() {
1607
+ await this._obtainMetadata();
1608
+ return this._full_metadata_dict;
1609
+ }
1610
+ /**
1611
+ * Get iPhone struct for high quality media.
1612
+ * Reserved for future use to fetch HQ images.
1613
+ */
1614
+ // @ts-expect-error Reserved for future HQ image fetching
1615
+ async _getIphoneStruct() {
1616
+ if (!this._context.iphoneSupport) {
1617
+ throw new IPhoneSupportDisabledException("iPhone support is disabled.");
1618
+ }
1619
+ if (!this._context.is_logged_in) {
1620
+ throw new LoginRequiredException(
1621
+ "Login required to access iPhone media info endpoint."
1622
+ );
1623
+ }
1624
+ if (!this._iphone_struct_) {
1625
+ const data = await this._context.get_iphone_json(
1626
+ `api/v1/media/${this.mediaid}/info/`,
1627
+ {}
1628
+ );
1629
+ const items = data["items"];
1630
+ this._iphone_struct_ = items[0];
1631
+ }
1632
+ return this._iphone_struct_;
1633
+ }
1634
+ /**
1635
+ * Lookup fields in _node, and if not found in _full_metadata.
1636
+ * Throws if not found anywhere.
1637
+ */
1638
+ _field(...keys) {
1639
+ try {
1640
+ let d = this._node;
1641
+ for (const key of keys) {
1642
+ if (typeof d !== "object" || d === null || Array.isArray(d)) {
1643
+ throw new Error("Key not found");
1644
+ }
1645
+ d = d[key];
1646
+ if (d === void 0) {
1647
+ throw new Error("Key not found");
1648
+ }
1649
+ }
1650
+ return d;
1651
+ } catch {
1652
+ throw new Error(
1653
+ `Field ${keys.join(".")} not found. Use async method for full metadata.`
1654
+ );
1655
+ }
1656
+ }
1657
+ /**
1658
+ * Async version of _field that can fetch full metadata if needed.
1659
+ */
1660
+ async getField(...keys) {
1661
+ try {
1662
+ let d = this._node;
1663
+ for (const key of keys) {
1664
+ if (typeof d !== "object" || d === null || Array.isArray(d)) {
1665
+ throw new Error("Key not found");
1666
+ }
1667
+ d = d[key];
1668
+ if (d === void 0) {
1669
+ throw new Error("Key not found");
1670
+ }
1671
+ }
1672
+ return d;
1673
+ } catch {
1674
+ const fullMetadata = await this._getFullMetadata();
1675
+ let d = fullMetadata;
1676
+ for (const key of keys) {
1677
+ if (typeof d !== "object" || d === null || Array.isArray(d)) {
1678
+ throw new Error("Key not found");
1679
+ }
1680
+ d = d[key];
1681
+ if (d === void 0) {
1682
+ throw new Error("Key not found");
1683
+ }
1684
+ }
1685
+ return d;
1686
+ }
1687
+ }
1688
+ /** Profile instance of the Post's owner. */
1689
+ async getOwnerProfile() {
1690
+ if (!this._owner_profile) {
1691
+ const owner = this._node["owner"];
1692
+ if (owner && "username" in owner) {
1693
+ this._owner_profile = new Profile(this._context, owner);
1694
+ } else {
1695
+ const fullMetadata = await this._getFullMetadata();
1696
+ const ownerStruct = fullMetadata["owner"];
1697
+ this._owner_profile = new Profile(this._context, ownerStruct);
1698
+ }
1699
+ }
1700
+ return this._owner_profile;
1701
+ }
1702
+ /** The Post's lowercase owner name. */
1703
+ async getOwnerUsername() {
1704
+ return (await this.getOwnerProfile()).username;
1705
+ }
1706
+ /** The ID of the Post's owner. */
1707
+ get owner_id() {
1708
+ const owner = this._node["owner"];
1709
+ if (owner && "id" in owner) {
1710
+ return Number(owner["id"]);
1711
+ }
1712
+ return null;
1713
+ }
1714
+ /** Timestamp when the post was created (local time zone). */
1715
+ get date_local() {
1716
+ return new Date(this._getTimestampDateCreated() * 1e3);
1717
+ }
1718
+ /** Timestamp when the post was created (UTC). */
1719
+ get date_utc() {
1720
+ return new Date(this._getTimestampDateCreated() * 1e3);
1721
+ }
1722
+ /** Synonym to date_utc */
1723
+ get date() {
1724
+ return this.date_utc;
1725
+ }
1726
+ _getTimestampDateCreated() {
1727
+ return this._node["date"] ?? this._node["taken_at_timestamp"];
1728
+ }
1729
+ /** URL of the picture / video thumbnail of the post */
1730
+ get url() {
1731
+ return this._node["display_url"] ?? this._node["display_src"];
1732
+ }
1733
+ /** Type of post: GraphImage, GraphVideo or GraphSidecar */
1734
+ get typename() {
1735
+ return this._field("__typename");
1736
+ }
1737
+ /** The number of media in a sidecar Post, or 1 if not a sidecar. */
1738
+ get mediacount() {
1739
+ if (this.typename === "GraphSidecar") {
1740
+ try {
1741
+ const edges = this._field(
1742
+ "edge_sidecar_to_children",
1743
+ "edges"
1744
+ );
1745
+ return edges.length;
1746
+ } catch {
1747
+ return 1;
1748
+ }
1749
+ }
1750
+ return 1;
1751
+ }
1752
+ /** Caption. */
1753
+ get caption() {
1754
+ const edgeMediaToCaption = this._node["edge_media_to_caption"];
1755
+ if (edgeMediaToCaption) {
1756
+ const edges = edgeMediaToCaption["edges"];
1757
+ if (edges && edges.length > 0) {
1758
+ const firstEdge = edges[0];
1759
+ const node = firstEdge["node"];
1760
+ return optionalNormalize(node["text"]);
1761
+ }
1762
+ }
1763
+ if ("caption" in this._node) {
1764
+ return optionalNormalize(this._node["caption"]);
1765
+ }
1766
+ return null;
1767
+ }
1768
+ /** List of all lowercased hashtags (without preceding #) that occur in the Post's caption. */
1769
+ get caption_hashtags() {
1770
+ if (!this.caption) {
1771
+ return [];
1772
+ }
1773
+ return extractHashtags(this.caption);
1774
+ }
1775
+ /** List of all lowercased profiles that are mentioned in the Post's caption, without preceding @. */
1776
+ get caption_mentions() {
1777
+ if (!this.caption) {
1778
+ return [];
1779
+ }
1780
+ return extractMentions(this.caption);
1781
+ }
1782
+ /** Printable caption, useful as a format specifier for --filename-pattern. */
1783
+ get pcaption() {
1784
+ return this.caption ? ellipsifyCaption(this.caption) : "";
1785
+ }
1786
+ /** Accessibility caption of the post, if available. */
1787
+ get accessibility_caption() {
1788
+ try {
1789
+ return this._field("accessibility_caption");
1790
+ } catch {
1791
+ return null;
1792
+ }
1793
+ }
1794
+ /** List of all lowercased users that are tagged in the Post. */
1795
+ get tagged_users() {
1796
+ try {
1797
+ const edges = this._field(
1798
+ "edge_media_to_tagged_user",
1799
+ "edges"
1800
+ );
1801
+ return edges.map((edge) => {
1802
+ const e = edge;
1803
+ const node = e["node"];
1804
+ const user = node["user"];
1805
+ return user["username"].toLowerCase();
1806
+ });
1807
+ } catch {
1808
+ return [];
1809
+ }
1810
+ }
1811
+ /** True if the Post is a video. */
1812
+ get is_video() {
1813
+ return this._node["is_video"];
1814
+ }
1815
+ /** URL of the video, or null. */
1816
+ get video_url() {
1817
+ if (this.is_video) {
1818
+ try {
1819
+ return this._field("video_url");
1820
+ } catch {
1821
+ return null;
1822
+ }
1823
+ }
1824
+ return null;
1825
+ }
1826
+ /** View count of the video, or null. */
1827
+ get video_view_count() {
1828
+ if (this.is_video) {
1829
+ try {
1830
+ return this._field("video_view_count");
1831
+ } catch {
1832
+ return null;
1833
+ }
1834
+ }
1835
+ return null;
1836
+ }
1837
+ /** Duration of the video in seconds, or null. */
1838
+ get video_duration() {
1839
+ if (this.is_video) {
1840
+ try {
1841
+ return this._field("video_duration");
1842
+ } catch {
1843
+ return null;
1844
+ }
1845
+ }
1846
+ return null;
1847
+ }
1848
+ /** Whether the viewer has liked the post, or null if not logged in. */
1849
+ get viewer_has_liked() {
1850
+ if (!this._context.is_logged_in) {
1851
+ return null;
1852
+ }
1853
+ const likes = this._node["likes"];
1854
+ if (likes && "viewer_has_liked" in likes) {
1855
+ return likes["viewer_has_liked"];
1856
+ }
1857
+ try {
1858
+ return this._field("viewer_has_liked");
1859
+ } catch {
1860
+ return null;
1861
+ }
1862
+ }
1863
+ /** Likes count */
1864
+ get likes() {
1865
+ try {
1866
+ return this._field("edge_media_preview_like", "count");
1867
+ } catch {
1868
+ return 0;
1869
+ }
1870
+ }
1871
+ /** Comment count including answers */
1872
+ get comments() {
1873
+ const edgeMediaToComment = this._node["edge_media_to_comment"];
1874
+ if (edgeMediaToComment && "count" in edgeMediaToComment) {
1875
+ return edgeMediaToComment["count"];
1876
+ }
1877
+ try {
1878
+ return this._field("edge_media_to_parent_comment", "count");
1879
+ } catch {
1880
+ try {
1881
+ return this._field("edge_media_to_comment", "count");
1882
+ } catch {
1883
+ return 0;
1884
+ }
1885
+ }
1886
+ }
1887
+ /** Whether Post is a sponsored post. */
1888
+ get is_sponsored() {
1889
+ try {
1890
+ const sponsorEdges = this._field(
1891
+ "edge_media_to_sponsor_user",
1892
+ "edges"
1893
+ );
1894
+ return sponsorEdges.length > 0;
1895
+ } catch {
1896
+ return false;
1897
+ }
1898
+ }
1899
+ /** Returns true if is_video for each media in sidecar. */
1900
+ getIsVideos() {
1901
+ if (this.typename === "GraphSidecar") {
1902
+ try {
1903
+ const edges = this._field(
1904
+ "edge_sidecar_to_children",
1905
+ "edges"
1906
+ );
1907
+ return edges.map((edge) => {
1908
+ const e = edge;
1909
+ const node = e["node"];
1910
+ return node["is_video"];
1911
+ });
1912
+ } catch {
1913
+ return [this.is_video];
1914
+ }
1915
+ }
1916
+ return [this.is_video];
1917
+ }
1918
+ /** Sidecar nodes of a Post with typename==GraphSidecar. */
1919
+ *getSidecarNodes(start = 0, end = -1) {
1920
+ if (this.typename !== "GraphSidecar") {
1921
+ return;
1922
+ }
1923
+ try {
1924
+ const edges = this._field(
1925
+ "edge_sidecar_to_children",
1926
+ "edges"
1927
+ );
1928
+ const actualEnd = end < 0 ? edges.length - 1 : end;
1929
+ const actualStart = start < 0 ? edges.length - 1 : start;
1930
+ for (let idx = 0; idx < edges.length; idx++) {
1931
+ if (idx >= actualStart && idx <= actualEnd) {
1932
+ const edge = edges[idx];
1933
+ const node = edge["node"];
1934
+ const isVideo = node["is_video"];
1935
+ const displayUrl = node["display_url"];
1936
+ const videoUrl = isVideo ? node["video_url"] : null;
1937
+ yield {
1938
+ is_video: isVideo,
1939
+ display_url: displayUrl,
1940
+ video_url: videoUrl
1941
+ };
1942
+ }
1943
+ }
1944
+ } catch {
1945
+ }
1946
+ }
1947
+ /** Returns Post as a JSON-serializable object. */
1948
+ toJSON() {
1949
+ const node = { ...this._node };
1950
+ if (this._full_metadata_dict) {
1951
+ Object.assign(node, this._full_metadata_dict);
1952
+ }
1953
+ if (this._location) {
1954
+ node["location"] = this._location;
1955
+ }
1956
+ if (this._iphone_struct_) {
1957
+ node["iphone_struct"] = this._iphone_struct_;
1958
+ }
1959
+ return node;
1960
+ }
1961
+ };
1962
+ var Profile = class _Profile {
1963
+ _context;
1964
+ _node;
1965
+ // Reserved for future has_public_story implementation
1966
+ // @ts-expect-error Reserved for future use
1967
+ _has_public_story = null;
1968
+ _has_full_metadata = false;
1969
+ _iphone_struct_ = null;
1970
+ constructor(context, node) {
1971
+ if (!("username" in node)) {
1972
+ throw new Error("Node must contain 'username'");
1973
+ }
1974
+ this._context = context;
1975
+ this._node = node;
1976
+ if ("iphone_struct" in node) {
1977
+ this._iphone_struct_ = node["iphone_struct"];
1978
+ }
1979
+ }
1980
+ /**
1981
+ * Create a Profile instance from a given username.
1982
+ * Raises exception if it does not exist.
1983
+ */
1984
+ static async fromUsername(context, username) {
1985
+ const profile = new _Profile(context, { username: username.toLowerCase() });
1986
+ await profile._obtainMetadata();
1987
+ return profile;
1988
+ }
1989
+ /**
1990
+ * Create a Profile instance from a given userid.
1991
+ * If possible, use fromUsername or constructor instead.
1992
+ */
1993
+ static async fromId(context, profileId) {
1994
+ const cached = context.profile_id_cache.get(profileId);
1995
+ if (cached) {
1996
+ return cached;
1997
+ }
1998
+ const data = await context.graphql_query("7c16654f22c819fb63d1183034a5162f", {
1999
+ user_id: String(profileId),
2000
+ include_chaining: false,
2001
+ include_reel: true,
2002
+ include_suggested_users: false,
2003
+ include_logged_out_extras: false,
2004
+ include_highlight_reels: false
2005
+ });
2006
+ const userData = data["data"]["user"];
2007
+ if (userData) {
2008
+ const reel = userData["reel"];
2009
+ const owner = reel["owner"];
2010
+ const profile = new _Profile(context, owner);
2011
+ context.profile_id_cache.set(profileId, profile);
2012
+ return profile;
2013
+ }
2014
+ throw new ProfileNotExistsException(
2015
+ `No profile found, the user may have blocked you (ID: ${profileId}).`
2016
+ );
2017
+ }
2018
+ /**
2019
+ * Create a profile from a given iphone_struct.
2020
+ */
2021
+ static fromIphoneStruct(context, media) {
2022
+ return new _Profile(context, {
2023
+ id: media["pk"],
2024
+ username: media["username"],
2025
+ is_private: media["is_private"],
2026
+ full_name: media["full_name"],
2027
+ profile_pic_url_hd: media["profile_pic_url"],
2028
+ iphone_struct: media
2029
+ });
2030
+ }
2031
+ /**
2032
+ * Return own profile if logged-in.
2033
+ */
2034
+ static async ownProfile(context) {
2035
+ if (!context.is_logged_in) {
2036
+ throw new LoginRequiredException("Login required to access own profile.");
2037
+ }
2038
+ const data = await context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {});
2039
+ const userData = data["data"]["user"];
2040
+ return new _Profile(context, userData);
2041
+ }
2042
+ async _obtainMetadata() {
2043
+ try {
2044
+ if (!this._has_full_metadata) {
2045
+ const metadata = await this._context.get_iphone_json(
2046
+ `api/v1/users/web_profile_info/?username=${this.username}`,
2047
+ {}
2048
+ );
2049
+ const data = metadata["data"];
2050
+ const user = data["user"];
2051
+ if (user === null) {
2052
+ throw new ProfileNotExistsException(
2053
+ `Profile ${this.username} does not exist.`
2054
+ );
2055
+ }
2056
+ this._node = user;
2057
+ this._has_full_metadata = true;
2058
+ }
2059
+ } catch (err) {
2060
+ if (err instanceof QueryReturnedNotFoundException || err instanceof TypeError) {
2061
+ const topSearch = new TopSearchResults(this._context, this.username);
2062
+ const profiles = [];
2063
+ for await (const profile of topSearch.getProfiles()) {
2064
+ profiles.push(profile.username);
2065
+ if (profiles.length >= 5) break;
2066
+ }
2067
+ if (profiles.length > 0) {
2068
+ if (profiles.includes(this.username)) {
2069
+ throw new ProfileNotExistsException(
2070
+ `Profile ${this.username} seems to exist, but could not be loaded.`
2071
+ );
2072
+ }
2073
+ const plural = profiles.length > 1 ? "s are" : " is";
2074
+ throw new ProfileNotExistsException(
2075
+ `Profile ${this.username} does not exist.
2076
+ The most similar profile${plural}: ${profiles.slice(0, 5).join(", ")}.`
2077
+ );
2078
+ }
2079
+ throw new ProfileNotExistsException(
2080
+ `Profile ${this.username} does not exist.`
2081
+ );
2082
+ }
2083
+ throw err;
2084
+ }
2085
+ }
2086
+ _metadata(...keys) {
2087
+ let d = this._node;
2088
+ for (const key of keys) {
2089
+ if (typeof d !== "object" || d === null || Array.isArray(d)) {
2090
+ throw new Error("Key not found");
2091
+ }
2092
+ d = d[key];
2093
+ if (d === void 0) {
2094
+ throw new Error("Key not found");
2095
+ }
2096
+ }
2097
+ return d;
2098
+ }
2099
+ /**
2100
+ * Async version that can fetch full metadata if needed.
2101
+ */
2102
+ async getMetadata(...keys) {
2103
+ try {
2104
+ return this._metadata(...keys);
2105
+ } catch {
2106
+ await this._obtainMetadata();
2107
+ return this._metadata(...keys);
2108
+ }
2109
+ }
2110
+ /** User ID */
2111
+ get userid() {
2112
+ return Number(this._metadata("id"));
2113
+ }
2114
+ /** Profile Name (lowercase) */
2115
+ get username() {
2116
+ return this._metadata("username").toLowerCase();
2117
+ }
2118
+ toString() {
2119
+ try {
2120
+ return `<Profile ${this.username} (${this.userid})>`;
2121
+ } catch {
2122
+ return `<Profile ${this.username}>`;
2123
+ }
2124
+ }
2125
+ equals(other) {
2126
+ try {
2127
+ return this.userid === other.userid;
2128
+ } catch {
2129
+ return this.username === other.username;
2130
+ }
2131
+ }
2132
+ /** Whether this is a private profile */
2133
+ get is_private() {
2134
+ return this._metadata("is_private");
2135
+ }
2136
+ /** Whether the viewer follows this profile */
2137
+ get followed_by_viewer() {
2138
+ try {
2139
+ return this._metadata("followed_by_viewer");
2140
+ } catch {
2141
+ return false;
2142
+ }
2143
+ }
2144
+ /** Number of posts */
2145
+ async getMediacount() {
2146
+ return await this.getMetadata("edge_owner_to_timeline_media", "count");
2147
+ }
2148
+ /** Number of followers */
2149
+ async getFollowers() {
2150
+ return await this.getMetadata("edge_followed_by", "count");
2151
+ }
2152
+ /** Number of followees */
2153
+ async getFollowees() {
2154
+ return await this.getMetadata("edge_follow", "count");
2155
+ }
2156
+ /** External URL in bio */
2157
+ async getExternalUrl() {
2158
+ try {
2159
+ return await this.getMetadata("external_url");
2160
+ } catch {
2161
+ return null;
2162
+ }
2163
+ }
2164
+ /** Whether this is a business account */
2165
+ async getIsBusinessAccount() {
2166
+ try {
2167
+ return await this.getMetadata("is_business_account");
2168
+ } catch {
2169
+ return false;
2170
+ }
2171
+ }
2172
+ /** Business category name */
2173
+ async getBusinessCategoryName() {
2174
+ try {
2175
+ return await this.getMetadata("business_category_name");
2176
+ } catch {
2177
+ return null;
2178
+ }
2179
+ }
2180
+ /** Biography (normalized) */
2181
+ async getBiography() {
2182
+ const bio = await this.getMetadata("biography");
2183
+ return bio.normalize("NFC");
2184
+ }
2185
+ /** Full name */
2186
+ async getFullName() {
2187
+ return await this.getMetadata("full_name");
2188
+ }
2189
+ /** Whether the viewer blocked this profile */
2190
+ get blocked_by_viewer() {
2191
+ try {
2192
+ return this._metadata("blocked_by_viewer");
2193
+ } catch {
2194
+ return false;
2195
+ }
2196
+ }
2197
+ /** Whether this profile follows the viewer */
2198
+ get follows_viewer() {
2199
+ try {
2200
+ return this._metadata("follows_viewer");
2201
+ } catch {
2202
+ return false;
2203
+ }
2204
+ }
2205
+ /** Whether this profile has blocked the viewer */
2206
+ get has_blocked_viewer() {
2207
+ try {
2208
+ return this._metadata("has_blocked_viewer");
2209
+ } catch {
2210
+ return false;
2211
+ }
2212
+ }
2213
+ /** Whether this profile is verified */
2214
+ async getIsVerified() {
2215
+ try {
2216
+ return await this.getMetadata("is_verified");
2217
+ } catch {
2218
+ return false;
2219
+ }
2220
+ }
2221
+ /** Profile picture URL */
2222
+ async getProfilePicUrl() {
2223
+ if (this._context.iphoneSupport && this._context.is_logged_in) {
2224
+ try {
2225
+ if (!this._iphone_struct_) {
2226
+ const data = await this._context.get_iphone_json(
2227
+ `api/v1/users/${this.userid}/info/`,
2228
+ {}
2229
+ );
2230
+ this._iphone_struct_ = data["user"];
2231
+ }
2232
+ const hdInfo = this._iphone_struct_["hd_profile_pic_url_info"];
2233
+ return hdInfo["url"];
2234
+ } catch (err) {
2235
+ this._context.error(`Unable to fetch high quality profile pic: ${err}`);
2236
+ return await this.getMetadata("profile_pic_url_hd");
2237
+ }
2238
+ }
2239
+ return await this.getMetadata("profile_pic_url_hd");
2240
+ }
2241
+ /**
2242
+ * Helper to check if a post is newer than the current first post.
2243
+ * Used for determining the "first" (newest) post in an iteration.
2244
+ */
2245
+ static _makeIsNewestChecker() {
2246
+ return (item, currentFirst) => {
2247
+ if (currentFirst === null) {
2248
+ return true;
2249
+ }
2250
+ return item.date.getTime() > currentFirst.date.getTime();
2251
+ };
2252
+ }
2253
+ /**
2254
+ * Retrieve all posts from a profile.
2255
+ *
2256
+ * @returns NodeIterator[Post]
2257
+ */
2258
+ getPosts() {
2259
+ const loggedIn = this._context.is_logged_in;
2260
+ let firstData;
2261
+ if (!loggedIn) {
2262
+ try {
2263
+ const edgeData = this._metadata("edge_owner_to_timeline_media");
2264
+ const countValue = edgeData["count"];
2265
+ const baseData = {
2266
+ edges: (edgeData["edges"] || []).map((e) => ({ node: e["node"] || e })),
2267
+ page_info: edgeData["page_info"] || { has_next_page: false, end_cursor: null }
2268
+ };
2269
+ firstData = typeof countValue === "number" ? { ...baseData, count: countValue } : baseData;
2270
+ } catch {
2271
+ }
2272
+ }
2273
+ const baseOptions = {
2274
+ context: this._context,
2275
+ queryHash: null,
2276
+ edgeExtractor: loggedIn ? (d) => d["data"]["xdt_api__v1__feed__user_timeline_graphql_connection"] : (d) => d["data"]["user"]["edge_owner_to_timeline_media"],
2277
+ nodeWrapper: loggedIn ? (n) => Post.fromIphoneStruct(this._context, n) : (n) => new Post(this._context, n, this),
2278
+ queryVariables: {
2279
+ data: {
2280
+ count: 12,
2281
+ include_relationship_info: true,
2282
+ latest_besties_reel_media: true,
2283
+ latest_reel_media: true
2284
+ },
2285
+ ...loggedIn ? { username: this.username } : { id: this.userid }
2286
+ },
2287
+ queryReferer: `https://www.instagram.com/${this.username}/`,
2288
+ isFirst: _Profile._makeIsNewestChecker(),
2289
+ docId: loggedIn ? "7898261790222653" : "7950326061742207"
2290
+ };
2291
+ if (firstData !== void 0) {
2292
+ return new NodeIterator({ ...baseOptions, firstData });
2293
+ }
2294
+ return new NodeIterator(baseOptions);
2295
+ }
2296
+ /**
2297
+ * Get Posts that are marked as saved by the user.
2298
+ *
2299
+ * @returns NodeIterator[Post]
2300
+ * @throws LoginRequiredException if not logged in as the target profile
2301
+ */
2302
+ getSavedPosts() {
2303
+ if (this.username !== this._context.username) {
2304
+ throw new LoginRequiredException(
2305
+ `Login as ${this.username} required to get that profile's saved posts.`
2306
+ );
2307
+ }
2308
+ return new NodeIterator({
2309
+ context: this._context,
2310
+ queryHash: "f883d95537fbcd400f466f63d42bd8a1",
2311
+ edgeExtractor: (d) => d["data"]["user"]["edge_saved_media"],
2312
+ nodeWrapper: (n) => new Post(this._context, n),
2313
+ queryVariables: { id: this.userid },
2314
+ queryReferer: `https://www.instagram.com/${this.username}/`
2315
+ });
2316
+ }
2317
+ /**
2318
+ * Retrieve all posts where a profile is tagged.
2319
+ *
2320
+ * @returns NodeIterator[Post]
2321
+ */
2322
+ getTaggedPosts() {
2323
+ return new NodeIterator({
2324
+ context: this._context,
2325
+ queryHash: "e31a871f7301132ceaab56507a66bbb7",
2326
+ edgeExtractor: (d) => d["data"]["user"]["edge_user_to_photos_of_you"],
2327
+ nodeWrapper: (n) => {
2328
+ const ownerId = Number(n["owner"]["id"]);
2329
+ const ownerProfile = ownerId === this.userid ? this : void 0;
2330
+ return new Post(this._context, n, ownerProfile);
2331
+ },
2332
+ queryVariables: { id: this.userid },
2333
+ queryReferer: `https://www.instagram.com/${this.username}/`,
2334
+ isFirst: _Profile._makeIsNewestChecker()
2335
+ });
2336
+ }
2337
+ /** Returns Profile as a JSON-serializable object. */
2338
+ toJSON() {
2339
+ const jsonNode = { ...this._node };
2340
+ delete jsonNode["edge_media_collections"];
2341
+ delete jsonNode["edge_owner_to_timeline_media"];
2342
+ delete jsonNode["edge_saved_media"];
2343
+ delete jsonNode["edge_felix_video_timeline"];
2344
+ if (this._iphone_struct_) {
2345
+ jsonNode["iphone_struct"] = this._iphone_struct_;
2346
+ }
2347
+ return jsonNode;
2348
+ }
2349
+ };
2350
+ var StoryItem = class _StoryItem {
2351
+ _context;
2352
+ _node;
2353
+ _owner_profile;
2354
+ _iphone_struct_ = null;
2355
+ constructor(context, node, owner_profile = null) {
2356
+ this._context = context;
2357
+ this._node = node;
2358
+ this._owner_profile = owner_profile;
2359
+ if ("iphone_struct" in node) {
2360
+ this._iphone_struct_ = node["iphone_struct"];
2361
+ }
2362
+ }
2363
+ /**
2364
+ * Create a StoryItem object from a given mediaid.
2365
+ */
2366
+ static async fromMediaid(context, mediaid) {
2367
+ const picJson = await context.graphql_query(
2368
+ "2b0673e0dc4580674a88d426fe00ea90",
2369
+ { shortcode: mediaidToShortcode(mediaid) }
2370
+ );
2371
+ const shortcodeMedia = picJson["data"]["shortcode_media"];
2372
+ if (shortcodeMedia === null) {
2373
+ throw new BadResponseException("Fetching StoryItem metadata failed.");
2374
+ }
2375
+ return new _StoryItem(context, shortcodeMedia);
2376
+ }
2377
+ /** The mediaid is a decimal representation of the media shortcode. */
2378
+ get mediaid() {
2379
+ return BigInt(this._node["id"]);
2380
+ }
2381
+ /** Convert mediaid to a shortcode-like string. */
2382
+ get shortcode() {
2383
+ return mediaidToShortcode(this.mediaid);
2384
+ }
2385
+ toString() {
2386
+ return `<StoryItem ${this.mediaid}>`;
2387
+ }
2388
+ equals(other) {
2389
+ return this.mediaid === other.mediaid;
2390
+ }
2391
+ /** Profile instance of the story item's owner. */
2392
+ async getOwnerProfile() {
2393
+ if (!this._owner_profile) {
2394
+ const owner = this._node["owner"];
2395
+ this._owner_profile = await Profile.fromId(
2396
+ this._context,
2397
+ Number(owner["id"])
2398
+ );
2399
+ }
2400
+ return this._owner_profile;
2401
+ }
2402
+ /** The StoryItem owner's lowercase name. */
2403
+ async getOwnerUsername() {
2404
+ return (await this.getOwnerProfile()).username;
2405
+ }
2406
+ /** The ID of the StoryItem owner. */
2407
+ async getOwnerId() {
2408
+ return (await this.getOwnerProfile()).userid;
2409
+ }
2410
+ /** Timestamp when the StoryItem was created (local time zone). */
2411
+ get date_local() {
2412
+ return new Date(this._node["taken_at_timestamp"] * 1e3);
2413
+ }
2414
+ /** Timestamp when the StoryItem was created (UTC). */
2415
+ get date_utc() {
2416
+ return new Date(this._node["taken_at_timestamp"] * 1e3);
2417
+ }
2418
+ /** Synonym to date_utc */
2419
+ get date() {
2420
+ return this.date_utc;
2421
+ }
2422
+ /** Timestamp when the StoryItem will get unavailable (local time zone). */
2423
+ get expiring_local() {
2424
+ return new Date(this._node["expiring_at_timestamp"] * 1e3);
2425
+ }
2426
+ /** Timestamp when the StoryItem will get unavailable (UTC). */
2427
+ get expiring_utc() {
2428
+ return new Date(this._node["expiring_at_timestamp"] * 1e3);
2429
+ }
2430
+ /** URL of the picture / video thumbnail of the StoryItem */
2431
+ get url() {
2432
+ const displayResources = this._node["display_resources"];
2433
+ const lastResource = displayResources[displayResources.length - 1];
2434
+ return lastResource["src"];
2435
+ }
2436
+ /** Type of story item: GraphStoryImage or GraphStoryVideo */
2437
+ get typename() {
2438
+ return this._node["__typename"];
2439
+ }
2440
+ /** Caption. */
2441
+ get caption() {
2442
+ const edgeMediaToCaption = this._node["edge_media_to_caption"];
2443
+ if (edgeMediaToCaption) {
2444
+ const edges = edgeMediaToCaption["edges"];
2445
+ if (edges && edges.length > 0) {
2446
+ const firstEdge = edges[0];
2447
+ const node = firstEdge["node"];
2448
+ return optionalNormalize(node["text"]);
2449
+ }
2450
+ }
2451
+ if ("caption" in this._node) {
2452
+ return optionalNormalize(this._node["caption"]);
2453
+ }
2454
+ return null;
2455
+ }
2456
+ /** List of all lowercased hashtags in the StoryItem's caption. */
2457
+ get caption_hashtags() {
2458
+ if (!this.caption) {
2459
+ return [];
2460
+ }
2461
+ return extractHashtags(this.caption);
2462
+ }
2463
+ /** List of all lowercased profiles mentioned in the StoryItem's caption. */
2464
+ get caption_mentions() {
2465
+ if (!this.caption) {
2466
+ return [];
2467
+ }
2468
+ return extractMentions(this.caption);
2469
+ }
2470
+ /** Printable caption. */
2471
+ get pcaption() {
2472
+ return this.caption ? ellipsifyCaption(this.caption) : "";
2473
+ }
2474
+ /** True if the StoryItem is a video. */
2475
+ get is_video() {
2476
+ return this._node["is_video"];
2477
+ }
2478
+ /** URL of the video, or null. */
2479
+ get video_url() {
2480
+ if (this.is_video) {
2481
+ try {
2482
+ const videoResources = this._node["video_resources"];
2483
+ const lastResource = videoResources[videoResources.length - 1];
2484
+ return lastResource["src"];
2485
+ } catch {
2486
+ return null;
2487
+ }
2488
+ }
2489
+ return null;
2490
+ }
2491
+ /** Returns StoryItem as a JSON-serializable object. */
2492
+ toJSON() {
2493
+ const node = { ...this._node };
2494
+ if (this._owner_profile) {
2495
+ node["owner"] = this._owner_profile.toJSON();
2496
+ }
2497
+ if (this._iphone_struct_) {
2498
+ node["iphone_struct"] = this._iphone_struct_;
2499
+ }
2500
+ return node;
2501
+ }
2502
+ };
2503
+ var Story = class {
2504
+ _context;
2505
+ _node;
2506
+ _unique_id = null;
2507
+ _owner_profile = null;
2508
+ _iphone_struct_ = null;
2509
+ constructor(context, node) {
2510
+ this._context = context;
2511
+ this._node = node;
2512
+ }
2513
+ toString() {
2514
+ const date = this.latest_media_utc;
2515
+ return `<Story by ${this.owner_username} changed ${date.toISOString()}>`;
2516
+ }
2517
+ equals(other) {
2518
+ return this.unique_id === other.unique_id;
2519
+ }
2520
+ /**
2521
+ * This ID only equals amongst Story instances which have the same owner
2522
+ * and the same set of StoryItem.
2523
+ */
2524
+ get unique_id() {
2525
+ if (!this._unique_id) {
2526
+ const items = this._node["items"];
2527
+ const idList = items.map((item) => BigInt(item["id"])).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2528
+ this._unique_id = String(this.owner_id) + idList.map(String).join("");
2529
+ }
2530
+ return this._unique_id;
2531
+ }
2532
+ /** Timestamp of the most recent StoryItem that has been watched or null (local time zone). */
2533
+ get last_seen_local() {
2534
+ const seen = this._node["seen"];
2535
+ if (seen) {
2536
+ return new Date(seen * 1e3);
2537
+ }
2538
+ return null;
2539
+ }
2540
+ /** Timestamp of the most recent StoryItem that has been watched or null (UTC). */
2541
+ get last_seen_utc() {
2542
+ return this.last_seen_local;
2543
+ }
2544
+ /** Timestamp when the last item of the story was created (local time zone). */
2545
+ get latest_media_local() {
2546
+ return new Date(this._node["latest_reel_media"] * 1e3);
2547
+ }
2548
+ /** Timestamp when the last item of the story was created (UTC). */
2549
+ get latest_media_utc() {
2550
+ return new Date(this._node["latest_reel_media"] * 1e3);
2551
+ }
2552
+ /** Count of items associated with the Story instance. */
2553
+ get itemcount() {
2554
+ return this._node["items"].length;
2555
+ }
2556
+ /** Profile instance of the story owner. */
2557
+ get owner_profile() {
2558
+ if (!this._owner_profile) {
2559
+ this._owner_profile = new Profile(
2560
+ this._context,
2561
+ this._node["user"]
2562
+ );
2563
+ }
2564
+ return this._owner_profile;
2565
+ }
2566
+ /** The story owner's lowercase username. */
2567
+ get owner_username() {
2568
+ return this.owner_profile.username;
2569
+ }
2570
+ /** The story owner's ID. */
2571
+ get owner_id() {
2572
+ return this.owner_profile.userid;
2573
+ }
2574
+ async _fetchIphoneStruct() {
2575
+ if (this._context.iphoneSupport && this._context.is_logged_in && !this._iphone_struct_) {
2576
+ const data = await this._context.get_iphone_json(
2577
+ `api/v1/feed/reels_media/?reel_ids=${this.owner_id}`,
2578
+ {}
2579
+ );
2580
+ const reels = data["reels"];
2581
+ this._iphone_struct_ = reels[String(this.owner_id)];
2582
+ }
2583
+ }
2584
+ /** Retrieve all items from a story. */
2585
+ async *getItems() {
2586
+ await this._fetchIphoneStruct();
2587
+ const items = [...this._node["items"]].reverse();
2588
+ for (const item of items) {
2589
+ const itemObj = { ...item };
2590
+ if (this._iphone_struct_ !== null) {
2591
+ const iphoneItems = this._iphone_struct_["items"];
2592
+ for (const iphoneItem of iphoneItems) {
2593
+ const iphoneObj = iphoneItem;
2594
+ if (String(iphoneObj["pk"]) === String(itemObj["id"])) {
2595
+ itemObj["iphone_struct"] = iphoneObj;
2596
+ break;
2597
+ }
2598
+ }
2599
+ }
2600
+ yield new StoryItem(this._context, itemObj, this.owner_profile);
2601
+ }
2602
+ }
2603
+ };
2604
+ var Highlight = class extends Story {
2605
+ _items = null;
2606
+ constructor(context, node, owner = null) {
2607
+ super(context, node);
2608
+ this._owner_profile = owner;
2609
+ }
2610
+ toString() {
2611
+ return `<Highlight by ${this.owner_username}: ${this.title}>`;
2612
+ }
2613
+ /** A unique ID identifying this set of highlights. */
2614
+ get unique_id() {
2615
+ return String(this._node["id"]);
2616
+ }
2617
+ /** Profile instance of the highlights' owner. */
2618
+ get owner_profile() {
2619
+ if (!this._owner_profile) {
2620
+ this._owner_profile = new Profile(
2621
+ this._context,
2622
+ this._node["owner"]
2623
+ );
2624
+ }
2625
+ return this._owner_profile;
2626
+ }
2627
+ /** The title of these highlights. */
2628
+ get title() {
2629
+ return this._node["title"];
2630
+ }
2631
+ /** URL of the highlights' cover. */
2632
+ get cover_url() {
2633
+ const coverMedia = this._node["cover_media"];
2634
+ return coverMedia["thumbnail_src"];
2635
+ }
2636
+ /** URL of the cropped version of the cover. */
2637
+ get cover_cropped_url() {
2638
+ const coverMedia = this._node["cover_media_cropped_thumbnail"];
2639
+ return coverMedia["url"];
2640
+ }
2641
+ async _fetchItems() {
2642
+ if (!this._items) {
2643
+ const data = await this._context.graphql_query(
2644
+ "45246d3fe16ccc6577e0bd297a5db1ab",
2645
+ {
2646
+ reel_ids: [],
2647
+ tag_names: [],
2648
+ location_ids: [],
2649
+ highlight_reel_ids: [String(this.unique_id)],
2650
+ precomposed_overlay: false
2651
+ }
2652
+ );
2653
+ const dataObj = data["data"];
2654
+ const reelsMedia = dataObj["reels_media"];
2655
+ const firstReel = reelsMedia[0];
2656
+ this._items = firstReel["items"];
2657
+ }
2658
+ }
2659
+ async _fetchIphoneStruct() {
2660
+ if (this._context.iphoneSupport && this._context.is_logged_in && !this._iphone_struct_) {
2661
+ const data = await this._context.get_iphone_json(
2662
+ `api/v1/feed/reels_media/?reel_ids=highlight:${this.unique_id}`,
2663
+ {}
2664
+ );
2665
+ const reels = data["reels"];
2666
+ this._iphone_struct_ = reels[`highlight:${this.unique_id}`];
2667
+ }
2668
+ }
2669
+ /** Count of items associated with the Highlight instance. */
2670
+ get itemcount() {
2671
+ if (this._items) {
2672
+ return this._items.length;
2673
+ }
2674
+ const items = this._node["items"];
2675
+ return items?.length ?? 0;
2676
+ }
2677
+ /** Get accurate item count (async). */
2678
+ async getItemcount() {
2679
+ await this._fetchItems();
2680
+ return this._items.length;
2681
+ }
2682
+ /** Retrieve all associated highlight items. */
2683
+ async *getItems() {
2684
+ await this._fetchItems();
2685
+ await this._fetchIphoneStruct();
2686
+ for (const item of this._items) {
2687
+ const itemObj = { ...item };
2688
+ if (this._iphone_struct_ !== null) {
2689
+ const iphoneItems = this._iphone_struct_["items"];
2690
+ for (const iphoneItem of iphoneItems) {
2691
+ const iphoneObj = iphoneItem;
2692
+ if (String(iphoneObj["pk"]) === String(itemObj["id"])) {
2693
+ itemObj["iphone_struct"] = iphoneObj;
2694
+ break;
2695
+ }
2696
+ }
2697
+ }
2698
+ yield new StoryItem(this._context, itemObj, this.owner_profile);
2699
+ }
2700
+ }
2701
+ };
2702
+ var Hashtag = class _Hashtag {
2703
+ _context;
2704
+ _node;
2705
+ _has_full_metadata = false;
2706
+ constructor(context, node) {
2707
+ if (!("name" in node)) {
2708
+ throw new Error("Node must contain 'name'");
2709
+ }
2710
+ this._context = context;
2711
+ this._node = node;
2712
+ }
2713
+ /**
2714
+ * Create a Hashtag instance from a given hashtag name, without preceding '#'.
2715
+ */
2716
+ static async fromName(context, name) {
2717
+ const hashtag = new _Hashtag(context, { name: name.toLowerCase() });
2718
+ await hashtag._obtainMetadata();
2719
+ return hashtag;
2720
+ }
2721
+ /** Hashtag name lowercased, without preceding '#' */
2722
+ get name() {
2723
+ return this._node["name"].toLowerCase();
2724
+ }
2725
+ async _query(params) {
2726
+ const jsonResponse = await this._context.get_iphone_json(
2727
+ "api/v1/tags/web_info/",
2728
+ { ...params, tag_name: this.name }
2729
+ );
2730
+ if ("graphql" in jsonResponse) {
2731
+ return jsonResponse["graphql"]["hashtag"];
2732
+ }
2733
+ return jsonResponse["data"];
2734
+ }
2735
+ async _obtainMetadata() {
2736
+ if (!this._has_full_metadata) {
2737
+ this._node = await this._query({ __a: 1, __d: "dis" });
2738
+ this._has_full_metadata = true;
2739
+ }
2740
+ }
2741
+ _metadata(...keys) {
2742
+ let d = this._node;
2743
+ for (const key of keys) {
2744
+ if (typeof d !== "object" || d === null || Array.isArray(d)) {
2745
+ throw new Error("Key not found");
2746
+ }
2747
+ d = d[key];
2748
+ if (d === void 0) {
2749
+ throw new Error("Key not found");
2750
+ }
2751
+ }
2752
+ return d;
2753
+ }
2754
+ async getMetadata(...keys) {
2755
+ try {
2756
+ return this._metadata(...keys);
2757
+ } catch {
2758
+ await this._obtainMetadata();
2759
+ return this._metadata(...keys);
2760
+ }
2761
+ }
2762
+ toString() {
2763
+ return `<Hashtag #${this.name}>`;
2764
+ }
2765
+ equals(other) {
2766
+ return this.name === other.name;
2767
+ }
2768
+ /** Hashtag ID */
2769
+ async getHashtagId() {
2770
+ return Number(await this.getMetadata("id"));
2771
+ }
2772
+ /** Profile picture URL of the hashtag */
2773
+ async getProfilePicUrl() {
2774
+ return await this.getMetadata("profile_pic_url");
2775
+ }
2776
+ /** Hashtag description */
2777
+ async getDescription() {
2778
+ try {
2779
+ return await this.getMetadata("description");
2780
+ } catch {
2781
+ return null;
2782
+ }
2783
+ }
2784
+ /** Whether following is allowed */
2785
+ async getAllowFollowing() {
2786
+ return Boolean(await this.getMetadata("allow_following"));
2787
+ }
2788
+ /** Whether the current user is following this hashtag */
2789
+ async getIsFollowing() {
2790
+ try {
2791
+ return await this.getMetadata("is_following");
2792
+ } catch {
2793
+ return Boolean(await this.getMetadata("following"));
2794
+ }
2795
+ }
2796
+ /** Count of all media associated with this hashtag */
2797
+ async getMediacount() {
2798
+ try {
2799
+ return await this.getMetadata("edge_hashtag_to_media", "count");
2800
+ } catch {
2801
+ return await this.getMetadata("media_count");
2802
+ }
2803
+ }
2804
+ /** Yields the top posts of the hashtag. */
2805
+ async *getTopPosts() {
2806
+ try {
2807
+ const edges = await this.getMetadata(
2808
+ "edge_hashtag_to_top_posts",
2809
+ "edges"
2810
+ );
2811
+ for (const edge of edges) {
2812
+ const e = edge;
2813
+ yield new Post(this._context, e["node"]);
2814
+ }
2815
+ } catch {
2816
+ }
2817
+ }
2818
+ /**
2819
+ * Yields the recent posts associated with this hashtag.
2820
+ *
2821
+ * @deprecated Use getPostsResumable() as this method may return incorrect results.
2822
+ */
2823
+ async *getPosts() {
2824
+ try {
2825
+ let conn = await this.getMetadata("edge_hashtag_to_media");
2826
+ const edges = conn["edges"];
2827
+ for (const edge of edges) {
2828
+ yield new Post(this._context, edge["node"]);
2829
+ }
2830
+ let pageInfo = conn["page_info"];
2831
+ while (pageInfo["has_next_page"]) {
2832
+ const data = await this._query({
2833
+ __a: 1,
2834
+ max_id: pageInfo["end_cursor"]
2835
+ });
2836
+ conn = data["edge_hashtag_to_media"];
2837
+ const newEdges = conn["edges"];
2838
+ for (const edge of newEdges) {
2839
+ yield new Post(this._context, edge["node"]);
2840
+ }
2841
+ pageInfo = conn["page_info"];
2842
+ }
2843
+ } catch {
2844
+ }
2845
+ }
2846
+ /**
2847
+ * Get the recent posts of the hashtag in a resumable fashion.
2848
+ *
2849
+ * @returns NodeIterator[Post]
2850
+ */
2851
+ getPostsResumable() {
2852
+ return new NodeIterator({
2853
+ context: this._context,
2854
+ queryHash: "9b498c08113f1e09617a1703c22b2f32",
2855
+ edgeExtractor: (d) => d["data"]["hashtag"]["edge_hashtag_to_media"],
2856
+ nodeWrapper: (n) => new Post(this._context, n),
2857
+ queryVariables: { tag_name: this.name },
2858
+ queryReferer: `https://www.instagram.com/explore/tags/${this.name}/`
2859
+ });
2860
+ }
2861
+ /** Returns Hashtag as a JSON-serializable object. */
2862
+ toJSON() {
2863
+ const jsonNode = { ...this._node };
2864
+ delete jsonNode["edge_hashtag_to_top_posts"];
2865
+ delete jsonNode["top"];
2866
+ delete jsonNode["edge_hashtag_to_media"];
2867
+ delete jsonNode["recent"];
2868
+ return jsonNode;
2869
+ }
2870
+ };
2871
+ var TopSearchResults = class {
2872
+ _context;
2873
+ _searchstring;
2874
+ _node = null;
2875
+ constructor(context, searchstring) {
2876
+ this._context = context;
2877
+ this._searchstring = searchstring;
2878
+ }
2879
+ async _ensureLoaded() {
2880
+ if (!this._node) {
2881
+ this._node = await this._context.getJson("web/search/topsearch/", {
2882
+ context: "blended",
2883
+ query: this._searchstring,
2884
+ include_reel: false,
2885
+ __a: 1
2886
+ });
2887
+ }
2888
+ return this._node;
2889
+ }
2890
+ /** Provides the Profile instances from the search result. */
2891
+ async *getProfiles() {
2892
+ const node = await this._ensureLoaded();
2893
+ const users = node["users"] ?? [];
2894
+ for (const user of users) {
2895
+ const userObj = user;
2896
+ const userNode = userObj["user"];
2897
+ if ("pk" in userNode) {
2898
+ userNode["id"] = userNode["pk"];
2899
+ }
2900
+ yield new Profile(this._context, userNode);
2901
+ }
2902
+ }
2903
+ /** Provides all profile names from the search result that start with the search string. */
2904
+ async *getPrefixedUsernames() {
2905
+ const node = await this._ensureLoaded();
2906
+ const users = node["users"] ?? [];
2907
+ for (const user of users) {
2908
+ const userObj = user;
2909
+ const innerUser = userObj["user"];
2910
+ const username = innerUser["username"] ?? "";
2911
+ if (username.startsWith(this._searchstring)) {
2912
+ yield username;
2913
+ }
2914
+ }
2915
+ }
2916
+ /** Provides instances of PostLocation from the search result. */
2917
+ async *getLocations() {
2918
+ const node = await this._ensureLoaded();
2919
+ const places = node["places"] ?? [];
2920
+ for (const location of places) {
2921
+ const locationObj = location;
2922
+ const place = locationObj["place"];
2923
+ const slug = place["slug"];
2924
+ const loc = place["location"];
2925
+ yield {
2926
+ id: Number(loc["pk"]),
2927
+ name: loc["name"],
2928
+ slug,
2929
+ has_public_page: null,
2930
+ lat: loc["lat"] ?? null,
2931
+ lng: loc["lng"] ?? null
2932
+ };
2933
+ }
2934
+ }
2935
+ /** Provides the hashtags from the search result as strings. */
2936
+ async *getHashtagStrings() {
2937
+ const node = await this._ensureLoaded();
2938
+ const hashtags = node["hashtags"] ?? [];
2939
+ for (const hashtag of hashtags) {
2940
+ const hashtagObj = hashtag;
2941
+ const inner = hashtagObj["hashtag"];
2942
+ const name = inner["name"];
2943
+ if (name) {
2944
+ yield name;
2945
+ }
2946
+ }
2947
+ }
2948
+ /** Provides the hashtags from the search result. */
2949
+ async *getHashtags() {
2950
+ const node = await this._ensureLoaded();
2951
+ const hashtags = node["hashtags"] ?? [];
2952
+ for (const hashtag of hashtags) {
2953
+ const hashtagObj = hashtag;
2954
+ const inner = hashtagObj["hashtag"];
2955
+ if ("name" in inner) {
2956
+ yield new Hashtag(this._context, inner);
2957
+ }
2958
+ }
2959
+ }
2960
+ /** The string that was searched for on Instagram. */
2961
+ get searchstring() {
2962
+ return this._searchstring;
2963
+ }
2964
+ };
2965
+ function getJsonStructure(structure) {
2966
+ return {
2967
+ node: structure.toJSON(),
2968
+ instaloader: {
2969
+ version: "4.15.0",
2970
+ // Match Python version
2971
+ node_type: structure.constructor.name
2972
+ }
2973
+ };
2974
+ }
2975
+ function loadStructure(context, jsonStructure) {
2976
+ if ("node" in jsonStructure && "instaloader" in jsonStructure) {
2977
+ const instaloader = jsonStructure["instaloader"];
2978
+ if ("node_type" in instaloader) {
2979
+ const nodeType = instaloader["node_type"];
2980
+ const node = jsonStructure["node"];
2981
+ switch (nodeType) {
2982
+ case "Post":
2983
+ return new Post(context, node);
2984
+ case "Profile":
2985
+ return new Profile(context, node);
2986
+ case "StoryItem":
2987
+ return new StoryItem(context, node);
2988
+ case "Hashtag":
2989
+ return new Hashtag(context, node);
2990
+ }
2991
+ }
2992
+ }
2993
+ if ("shortcode" in jsonStructure) {
2994
+ return new Post(context, jsonStructure);
2995
+ }
2996
+ throw new InvalidArgumentException(
2997
+ "Passed json structure is not an Instaloader JSON"
2998
+ );
2999
+ }
3000
+
3001
+ // src/instaloader.ts
3002
+ import * as fs from "fs";
3003
+ import * as path from "path";
3004
+ import * as os from "os";
3005
+ function getConfigDir() {
3006
+ if (process.platform === "win32") {
3007
+ const localAppData = process.env["LOCALAPPDATA"];
3008
+ if (localAppData) {
3009
+ return path.join(localAppData, "Instaloader");
3010
+ }
3011
+ return path.join(os.tmpdir(), `.instaloader-${os.userInfo().username}`);
3012
+ }
3013
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] || path.join(os.homedir(), ".config");
3014
+ return path.join(xdgConfig, "instaloader");
3015
+ }
3016
+ function getDefaultSessionFilename(username) {
3017
+ return path.join(getConfigDir(), `session-${username}`);
3018
+ }
3019
+ function getDefaultStampsFilename() {
3020
+ return path.join(getConfigDir(), "latest-stamps.ini");
3021
+ }
3022
+ function formatStringContainsKey(formatString, key) {
3023
+ const pattern = new RegExp(`\\{${key}(?:\\.[^:}]*)?(?::[^}]*)?\\}`, "g");
3024
+ return pattern.test(formatString);
3025
+ }
3026
+ function sanitizePath(str, forceWindowsPath = false) {
3027
+ let result = str.replace(/\//g, "\u2215");
3028
+ if (result.startsWith(".")) {
3029
+ result = "\u2024" + result.slice(1);
3030
+ }
3031
+ if (forceWindowsPath || process.platform === "win32") {
3032
+ result = result.replace(/:/g, "\uFF1A").replace(/</g, "\uFE64").replace(/>/g, "\uFE65").replace(/"/g, "\uFF02").replace(/\\/g, "\uFE68").replace(/\|/g, "\uFF5C").replace(/\?/g, "\uFE16").replace(/\*/g, "\uFF0A").replace(/\n/g, " ").replace(/\r/g, " ");
3033
+ const reserved = /* @__PURE__ */ new Set([
3034
+ "CON",
3035
+ "PRN",
3036
+ "AUX",
3037
+ "NUL",
3038
+ "COM1",
3039
+ "COM2",
3040
+ "COM3",
3041
+ "COM4",
3042
+ "COM5",
3043
+ "COM6",
3044
+ "COM7",
3045
+ "COM8",
3046
+ "COM9",
3047
+ "LPT1",
3048
+ "LPT2",
3049
+ "LPT3",
3050
+ "LPT4",
3051
+ "LPT5",
3052
+ "LPT6",
3053
+ "LPT7",
3054
+ "LPT8",
3055
+ "LPT9"
3056
+ ]);
3057
+ const ext = path.extname(result);
3058
+ let root = result.slice(0, result.length - ext.length);
3059
+ if (reserved.has(root.toUpperCase())) {
3060
+ root += "_";
3061
+ }
3062
+ const finalExt = ext === "." ? "\u2024" : ext;
3063
+ result = root + finalExt;
3064
+ }
3065
+ return result;
3066
+ }
3067
+ function formatFilename(pattern, item, target, sanitize = false) {
3068
+ let result = pattern;
3069
+ const replacements = {
3070
+ target
3071
+ };
3072
+ if (item instanceof Profile) {
3073
+ replacements["profile"] = item.username;
3074
+ }
3075
+ if (item instanceof Post) {
3076
+ replacements["date_utc"] = formatDate(item.date_utc);
3077
+ replacements["date_local"] = formatDate(item.date_local);
3078
+ replacements["shortcode"] = item.shortcode;
3079
+ replacements["mediaid"] = item.mediaid.toString();
3080
+ replacements["typename"] = item.typename;
3081
+ } else if (item instanceof StoryItem) {
3082
+ replacements["date_utc"] = formatDate(item.date_utc);
3083
+ replacements["date_local"] = formatDate(item.date_local);
3084
+ replacements["mediaid"] = item.mediaid?.toString();
3085
+ replacements["typename"] = item.typename;
3086
+ }
3087
+ for (const [key, value] of Object.entries(replacements)) {
3088
+ if (value !== void 0) {
3089
+ const placeholder = new RegExp(`\\{${key}(?::[^}]*)?\\}`, "g");
3090
+ let replacement = value;
3091
+ if (sanitize) {
3092
+ replacement = sanitizePath(value);
3093
+ }
3094
+ result = result.replace(placeholder, replacement);
3095
+ }
3096
+ }
3097
+ return result;
3098
+ }
3099
+ function formatDate(date) {
3100
+ if (!date) return "";
3101
+ return date.toISOString().replace(/[:.]/g, "-").slice(0, 19);
3102
+ }
3103
+ var Instaloader = class {
3104
+ /** The associated InstaloaderContext for low-level operations */
3105
+ context;
3106
+ // Configuration
3107
+ dirnamePattern;
3108
+ filenamePattern;
3109
+ titlePattern;
3110
+ downloadPictures;
3111
+ downloadVideos;
3112
+ downloadVideoThumbnails;
3113
+ downloadGeotags;
3114
+ downloadComments;
3115
+ saveMetadata;
3116
+ compressJson;
3117
+ postMetadataTxtPattern;
3118
+ storyitemMetadataTxtPattern;
3119
+ resumePrefix;
3120
+ checkResumeBbd;
3121
+ sanitizePaths;
3122
+ // Slide configuration
3123
+ slide;
3124
+ slideStart;
3125
+ slideEnd;
3126
+ constructor(options = {}) {
3127
+ this.context = new InstaloaderContext({
3128
+ ...options.sleep !== void 0 && { sleep: options.sleep },
3129
+ ...options.quiet !== void 0 && { quiet: options.quiet },
3130
+ ...options.userAgent !== void 0 && { userAgent: options.userAgent },
3131
+ ...options.maxConnectionAttempts !== void 0 && { maxConnectionAttempts: options.maxConnectionAttempts },
3132
+ ...options.requestTimeout !== void 0 && { requestTimeout: options.requestTimeout },
3133
+ ...options.rateController !== void 0 && { rateController: options.rateController },
3134
+ ...options.fatalStatusCodes !== void 0 && { fatalStatusCodes: options.fatalStatusCodes },
3135
+ ...options.iphoneSupport !== void 0 && { iphoneSupport: options.iphoneSupport }
3136
+ });
3137
+ this.dirnamePattern = options.dirnamePattern ?? "{target}";
3138
+ this.filenamePattern = options.filenamePattern ?? "{date_utc}_UTC";
3139
+ if (options.titlePattern !== void 0) {
3140
+ this.titlePattern = options.titlePattern;
3141
+ } else if (formatStringContainsKey(this.dirnamePattern, "profile") || formatStringContainsKey(this.dirnamePattern, "target")) {
3142
+ this.titlePattern = "{date_utc}_UTC_{typename}";
3143
+ } else {
3144
+ this.titlePattern = "{target}_{date_utc}_UTC_{typename}";
3145
+ }
3146
+ this.sanitizePaths = options.sanitizePaths ?? false;
3147
+ this.downloadPictures = options.downloadPictures ?? true;
3148
+ this.downloadVideos = options.downloadVideos ?? true;
3149
+ this.downloadVideoThumbnails = options.downloadVideoThumbnails ?? true;
3150
+ this.downloadGeotags = options.downloadGeotags ?? false;
3151
+ this.downloadComments = options.downloadComments ?? false;
3152
+ this.saveMetadata = options.saveMetadata ?? true;
3153
+ this.compressJson = options.compressJson ?? true;
3154
+ this.postMetadataTxtPattern = options.postMetadataTxtPattern ?? "{caption}";
3155
+ this.storyitemMetadataTxtPattern = options.storyitemMetadataTxtPattern ?? "";
3156
+ this.resumePrefix = options.resumePrefix === null ? null : options.resumePrefix ?? "iterator";
3157
+ this.checkResumeBbd = options.checkResumeBbd ?? true;
3158
+ this.slide = options.slide ?? "";
3159
+ this.slideStart = 0;
3160
+ this.slideEnd = -1;
3161
+ if (this.slide !== "") {
3162
+ const parts = this.slide.split("-");
3163
+ if (parts.length === 1) {
3164
+ if (parts[0] === "last") {
3165
+ this.slideStart = -1;
3166
+ } else {
3167
+ const num = parseInt(parts[0], 10);
3168
+ if (num > 0) {
3169
+ this.slideStart = this.slideEnd = num - 1;
3170
+ } else {
3171
+ throw new InvalidArgumentException("--slide parameter must be greater than 0.");
3172
+ }
3173
+ }
3174
+ } else if (parts.length === 2) {
3175
+ if (parts[1] === "last") {
3176
+ this.slideStart = parseInt(parts[0], 10) - 1;
3177
+ } else {
3178
+ const start = parseInt(parts[0], 10);
3179
+ const end = parseInt(parts[1], 10);
3180
+ if (start > 0 && start < end) {
3181
+ this.slideStart = start - 1;
3182
+ this.slideEnd = end - 1;
3183
+ } else {
3184
+ throw new InvalidArgumentException("Invalid data for --slide parameter.");
3185
+ }
3186
+ }
3187
+ } else {
3188
+ throw new InvalidArgumentException("Invalid data for --slide parameter.");
3189
+ }
3190
+ }
3191
+ }
3192
+ /**
3193
+ * Close the session and clean up resources.
3194
+ */
3195
+ close() {
3196
+ this.context.close();
3197
+ }
3198
+ // ================== Session Management ==================
3199
+ /**
3200
+ * Save session to a dictionary.
3201
+ */
3202
+ saveSession() {
3203
+ if (!this.context.is_logged_in) {
3204
+ throw new LoginRequiredException("Login required.");
3205
+ }
3206
+ return this.context.saveSession();
3207
+ }
3208
+ /**
3209
+ * Load session from a dictionary.
3210
+ */
3211
+ loadSession(username, sessionData) {
3212
+ this.context.loadSession(username, sessionData);
3213
+ }
3214
+ /**
3215
+ * Save session to file.
3216
+ */
3217
+ async saveSessionToFile(filename) {
3218
+ if (!this.context.is_logged_in) {
3219
+ throw new LoginRequiredException("Login required.");
3220
+ }
3221
+ const targetFile = filename ?? getDefaultSessionFilename(this.context.username);
3222
+ const dir = path.dirname(targetFile);
3223
+ if (dir !== "" && !fs.existsSync(dir)) {
3224
+ await fs.promises.mkdir(dir, { recursive: true, mode: 448 });
3225
+ }
3226
+ const sessionData = this.saveSession();
3227
+ await fs.promises.writeFile(targetFile, JSON.stringify(sessionData), {
3228
+ mode: 384
3229
+ });
3230
+ this.context.log(`Saved session to ${targetFile}.`);
3231
+ }
3232
+ /**
3233
+ * Load session from file.
3234
+ */
3235
+ async loadSessionFromFile(username, filename) {
3236
+ let targetFile = filename ?? getDefaultSessionFilename(username);
3237
+ if (!fs.existsSync(targetFile)) {
3238
+ throw new Error(`Session file not found: ${targetFile}`);
3239
+ }
3240
+ const content = await fs.promises.readFile(targetFile, "utf-8");
3241
+ const sessionData = JSON.parse(content);
3242
+ this.loadSession(username, sessionData);
3243
+ this.context.log(`Loaded session from ${targetFile}.`);
3244
+ }
3245
+ /**
3246
+ * Test if the current session is valid.
3247
+ * Returns the username if logged in, null otherwise.
3248
+ */
3249
+ async testLogin() {
3250
+ return this.context.testLogin();
3251
+ }
3252
+ /**
3253
+ * Login with username and password.
3254
+ */
3255
+ async login(user, passwd) {
3256
+ await this.context.login(user, passwd);
3257
+ }
3258
+ /**
3259
+ * Complete two-factor authentication.
3260
+ */
3261
+ async twoFactorLogin(twoFactorCode) {
3262
+ await this.context.twoFactorLogin(twoFactorCode);
3263
+ }
3264
+ // ================== Download Methods ==================
3265
+ /**
3266
+ * Download a picture or video from a URL.
3267
+ *
3268
+ * @param filename - Base filename (without extension)
3269
+ * @param url - URL to download from
3270
+ * @param mtime - Modification time to set on the file
3271
+ * @param filenameSuffix - Optional suffix to add before extension
3272
+ * @returns True if file was downloaded, false if it already existed
3273
+ */
3274
+ async downloadPic(filename, url, mtime, filenameSuffix) {
3275
+ if (filenameSuffix) {
3276
+ filename += "_" + filenameSuffix;
3277
+ }
3278
+ const urlMatch = url.match(/\.([a-z0-9]+)\?/i);
3279
+ const fileExtension = urlMatch ? urlMatch[1] : url.slice(-3);
3280
+ const nominalFilename = `${filename}.${fileExtension}`;
3281
+ if (fs.existsSync(nominalFilename)) {
3282
+ this.context.log(`${nominalFilename} exists`, false);
3283
+ return false;
3284
+ }
3285
+ const response = await fetch(url, {
3286
+ headers: {
3287
+ "User-Agent": this.context.userAgent
3288
+ }
3289
+ });
3290
+ if (!response.ok) {
3291
+ throw new ConnectionException(`Failed to download: ${response.status} ${response.statusText}`);
3292
+ }
3293
+ let finalFilename = nominalFilename;
3294
+ const contentType = response.headers.get("Content-Type");
3295
+ if (contentType) {
3296
+ let headerExtension = contentType.split(";")[0].split("/").pop().toLowerCase();
3297
+ if (headerExtension === "jpeg") {
3298
+ headerExtension = "jpg";
3299
+ }
3300
+ finalFilename = `${filename}.${headerExtension}`;
3301
+ }
3302
+ if (finalFilename !== nominalFilename && fs.existsSync(finalFilename)) {
3303
+ this.context.log(`${finalFilename} exists`, false);
3304
+ return false;
3305
+ }
3306
+ const dir = path.dirname(finalFilename);
3307
+ if (dir && !fs.existsSync(dir)) {
3308
+ await fs.promises.mkdir(dir, { recursive: true });
3309
+ }
3310
+ const buffer = Buffer.from(await response.arrayBuffer());
3311
+ await fs.promises.writeFile(finalFilename, buffer);
3312
+ await fs.promises.utimes(finalFilename, /* @__PURE__ */ new Date(), mtime);
3313
+ return true;
3314
+ }
3315
+ /**
3316
+ * Save metadata JSON for a structure.
3317
+ */
3318
+ async saveMetadataJson(filename, structure) {
3319
+ const jsonFilename = this.compressJson ? `${filename}.json.xz` : `${filename}.json`;
3320
+ const dir = path.dirname(jsonFilename);
3321
+ if (dir && !fs.existsSync(dir)) {
3322
+ await fs.promises.mkdir(dir, { recursive: true });
3323
+ }
3324
+ const data = getJsonStructure(structure);
3325
+ const json = JSON.stringify(data, null, 2);
3326
+ if (this.compressJson) {
3327
+ await fs.promises.writeFile(jsonFilename.replace(".xz", ""), json);
3328
+ } else {
3329
+ await fs.promises.writeFile(jsonFilename, json);
3330
+ }
3331
+ if (structure instanceof Post || structure instanceof StoryItem) {
3332
+ this.context.log("json", false);
3333
+ }
3334
+ }
3335
+ /**
3336
+ * Save caption to a text file.
3337
+ */
3338
+ async saveCaption(filename, mtime, caption) {
3339
+ const txtFilename = `${filename}.txt`;
3340
+ const content = caption + "\n";
3341
+ if (fs.existsSync(txtFilename)) {
3342
+ const existing = await fs.promises.readFile(txtFilename, "utf-8");
3343
+ if (existing.replace(/\r\n/g, "\n") === content.replace(/\r\n/g, "\n")) {
3344
+ this.context.log("txt unchanged", false);
3345
+ return;
3346
+ }
3347
+ this.context.log("txt updated", false);
3348
+ } else {
3349
+ const preview = caption.replace(/\n/g, " ").trim();
3350
+ const ellipsified = preview.length > 31 ? `[${preview.slice(0, 29)}\u2026]` : `[${preview}]`;
3351
+ this.context.log(ellipsified, false);
3352
+ }
3353
+ const dir = path.dirname(txtFilename);
3354
+ if (dir && !fs.existsSync(dir)) {
3355
+ await fs.promises.mkdir(dir, { recursive: true });
3356
+ }
3357
+ await fs.promises.writeFile(txtFilename, content, "utf-8");
3358
+ await fs.promises.utimes(txtFilename, /* @__PURE__ */ new Date(), mtime);
3359
+ }
3360
+ /**
3361
+ * Download a single post.
3362
+ *
3363
+ * @param post - Post to download
3364
+ * @param target - Target directory name
3365
+ * @returns True if something was downloaded
3366
+ */
3367
+ async downloadPost(post, target) {
3368
+ const dirname2 = formatFilename(this.dirnamePattern, post, target, this.sanitizePaths);
3369
+ const filename = path.join(
3370
+ dirname2,
3371
+ formatFilename(this.filenamePattern, post, target, this.sanitizePaths)
3372
+ );
3373
+ let downloaded = false;
3374
+ if (this.downloadPictures) {
3375
+ if (post.typename === "GraphSidecar") {
3376
+ const sidecarNodes = post.getSidecarNodes();
3377
+ let index = 0;
3378
+ for await (const node of sidecarNodes) {
3379
+ if (this.slideStart >= 0) {
3380
+ if (index < this.slideStart) {
3381
+ index++;
3382
+ continue;
3383
+ }
3384
+ if (this.slideEnd >= 0 && index > this.slideEnd) {
3385
+ break;
3386
+ }
3387
+ }
3388
+ if (node.is_video) {
3389
+ if (this.downloadVideos && node.video_url) {
3390
+ const dl = await this.downloadPic(filename, node.video_url, post.date_utc, `${index + 1}`);
3391
+ downloaded = downloaded || dl;
3392
+ }
3393
+ } else if (node.display_url) {
3394
+ const dl = await this.downloadPic(filename, node.display_url, post.date_utc, `${index + 1}`);
3395
+ downloaded = downloaded || dl;
3396
+ }
3397
+ index++;
3398
+ }
3399
+ } else if (post.is_video) {
3400
+ if (this.downloadVideos) {
3401
+ const videoUrl = post.video_url;
3402
+ if (videoUrl) {
3403
+ const dl = await this.downloadPic(filename, videoUrl, post.date_utc);
3404
+ downloaded = downloaded || dl;
3405
+ }
3406
+ }
3407
+ if (this.downloadVideoThumbnails && post.url) {
3408
+ await this.downloadPic(filename, post.url, post.date_utc, "thumb");
3409
+ }
3410
+ } else {
3411
+ if (post.url) {
3412
+ const dl = await this.downloadPic(filename, post.url, post.date_utc);
3413
+ downloaded = downloaded || dl;
3414
+ }
3415
+ }
3416
+ }
3417
+ if (this.saveMetadata) {
3418
+ await this.saveMetadataJson(filename, post);
3419
+ }
3420
+ if (this.postMetadataTxtPattern && post.caption) {
3421
+ await this.saveCaption(filename, post.date_utc, post.caption);
3422
+ }
3423
+ this.context.log("");
3424
+ return downloaded;
3425
+ }
3426
+ /**
3427
+ * Download posts from an iterator.
3428
+ *
3429
+ * @param posts - Iterator of posts
3430
+ * @param target - Target directory name
3431
+ * @param options - Download options
3432
+ */
3433
+ async downloadPosts(posts, target, options = {}) {
3434
+ const {
3435
+ fastUpdate = false,
3436
+ postFilter,
3437
+ maxCount,
3438
+ totalCount,
3439
+ // ownerProfile is reserved for future use (resume file naming)
3440
+ possiblyPinned = 0
3441
+ } = options;
3442
+ const displayedCount = maxCount !== void 0 && (totalCount === void 0 || maxCount < totalCount) ? maxCount : totalCount;
3443
+ let number = 0;
3444
+ for await (const post of posts) {
3445
+ number++;
3446
+ if (maxCount !== void 0 && number > maxCount) {
3447
+ break;
3448
+ }
3449
+ if (displayedCount !== void 0) {
3450
+ const width = displayedCount.toString().length;
3451
+ this.context.log(`[${number.toString().padStart(width)}/${displayedCount.toString().padStart(width)}] `, false);
3452
+ } else {
3453
+ this.context.log(`[${number.toString().padStart(3)}] `, false);
3454
+ }
3455
+ if (postFilter) {
3456
+ try {
3457
+ if (!postFilter(post)) {
3458
+ this.context.log(`${post} skipped`);
3459
+ continue;
3460
+ }
3461
+ } catch (err) {
3462
+ this.context.error(`${post} skipped. Filter evaluation failed: ${err}`);
3463
+ continue;
3464
+ }
3465
+ }
3466
+ try {
3467
+ const downloaded = await this.downloadPost(post, target);
3468
+ if (fastUpdate && !downloaded && number > possiblyPinned) {
3469
+ break;
3470
+ }
3471
+ } catch (err) {
3472
+ if (err instanceof PostChangedException) {
3473
+ continue;
3474
+ }
3475
+ this.context.error(`Download ${post} of ${target}: ${err}`);
3476
+ }
3477
+ }
3478
+ }
3479
+ /**
3480
+ * Download a profile's posts.
3481
+ */
3482
+ async downloadProfile(profile, options = {}) {
3483
+ const {
3484
+ fastUpdate = false,
3485
+ postFilter,
3486
+ maxCount,
3487
+ downloadProfilePic = true,
3488
+ downloadStories = false,
3489
+ downloadHighlights = false
3490
+ } = options;
3491
+ const target = profile.username.toLowerCase();
3492
+ this.context.log(`Downloading profile ${target}...`);
3493
+ if (downloadProfilePic) {
3494
+ await this.downloadProfilePic(profile);
3495
+ }
3496
+ let count = 0;
3497
+ const postIterator = profile.getPosts();
3498
+ for await (const post of postIterator) {
3499
+ if (maxCount !== void 0 && count >= maxCount) {
3500
+ break;
3501
+ }
3502
+ if (postFilter && !postFilter(post)) {
3503
+ continue;
3504
+ }
3505
+ if (fastUpdate) {
3506
+ const dirname2 = formatFilename(this.dirnamePattern, post, target, this.sanitizePaths);
3507
+ const postBasename = formatFilename(this.filenamePattern, post, target, this.sanitizePaths);
3508
+ const metadataPath = path.join(dirname2, `${postBasename}.json`);
3509
+ try {
3510
+ await fs.promises.access(metadataPath);
3511
+ this.context.log(`Fast update: Stopping at already downloaded post ${post.shortcode}`);
3512
+ break;
3513
+ } catch {
3514
+ }
3515
+ }
3516
+ await this.downloadPost(post, target);
3517
+ count++;
3518
+ }
3519
+ this.context.log(`Downloaded ${count} posts from ${target}`);
3520
+ if (downloadStories && this.context.is_logged_in) {
3521
+ this.context.log(`Downloading stories of ${target}...`);
3522
+ }
3523
+ if (downloadHighlights && this.context.is_logged_in) {
3524
+ this.context.log(`Downloading highlights of ${target}...`);
3525
+ }
3526
+ }
3527
+ /**
3528
+ * Download a profile's profile picture.
3529
+ */
3530
+ async downloadProfilePic(profile) {
3531
+ const url = await profile.getProfilePicUrl();
3532
+ if (!url) return;
3533
+ const dirname2 = formatFilename(this.dirnamePattern, profile, profile.username.toLowerCase(), this.sanitizePaths);
3534
+ const filename = path.join(dirname2, `${profile.username.toLowerCase()}_profile_pic`);
3535
+ await this.downloadPic(filename, url, /* @__PURE__ */ new Date());
3536
+ this.context.log("");
3537
+ }
3538
+ /**
3539
+ * Download posts for a hashtag.
3540
+ */
3541
+ async downloadHashtag(hashtag, options = {}) {
3542
+ const { maxCount, postFilter, resumable = true } = options;
3543
+ const target = `#${hashtag.name}`;
3544
+ this.context.log(`Downloading hashtag ${target}...`);
3545
+ let count = 0;
3546
+ if (resumable) {
3547
+ const postIterator = hashtag.getPostsResumable();
3548
+ for await (const post of postIterator) {
3549
+ if (maxCount !== void 0 && count >= maxCount) {
3550
+ break;
3551
+ }
3552
+ if (postFilter && !postFilter(post)) {
3553
+ continue;
3554
+ }
3555
+ await this.downloadPost(post, hashtag.name);
3556
+ count++;
3557
+ }
3558
+ } else {
3559
+ for await (const post of hashtag.getPosts()) {
3560
+ if (maxCount !== void 0 && count >= maxCount) {
3561
+ break;
3562
+ }
3563
+ if (postFilter && !postFilter(post)) {
3564
+ continue;
3565
+ }
3566
+ await this.downloadPost(post, hashtag.name);
3567
+ count++;
3568
+ }
3569
+ }
3570
+ this.context.log(`Downloaded ${count} posts from ${target}`);
3571
+ }
3572
+ /**
3573
+ * Get a profile by username.
3574
+ */
3575
+ async getProfile(username) {
3576
+ return Profile.fromUsername(this.context, username);
3577
+ }
3578
+ /**
3579
+ * Get a post by shortcode.
3580
+ */
3581
+ async getPost(shortcode) {
3582
+ return Post.fromShortcode(this.context, shortcode);
3583
+ }
3584
+ /**
3585
+ * Get a hashtag by name.
3586
+ */
3587
+ async getHashtag(name) {
3588
+ return Hashtag.fromName(this.context, name);
3589
+ }
3590
+ };
3591
+ export {
3592
+ AbortDownloadException,
3593
+ BadCredentialsException,
3594
+ BadResponseException,
3595
+ CheckpointRequiredException,
3596
+ ConnectionException,
3597
+ FrozenNodeIterator,
3598
+ Hashtag,
3599
+ Highlight,
3600
+ IPhoneSupportDisabledException,
3601
+ Instaloader,
3602
+ InstaloaderContext,
3603
+ InstaloaderException,
3604
+ InvalidArgumentException,
3605
+ InvalidIteratorException,
3606
+ LoginException,
3607
+ LoginRequiredException,
3608
+ NodeIterator,
3609
+ Post,
3610
+ PostChangedException,
3611
+ PostComment,
3612
+ PrivateProfileNotFollowedException,
3613
+ Profile,
3614
+ ProfileHasNoPicsException,
3615
+ ProfileNotExistsException,
3616
+ QueryReturnedBadRequestException,
3617
+ QueryReturnedForbiddenException,
3618
+ QueryReturnedNotFoundException,
3619
+ RateController,
3620
+ SessionNotFoundException,
3621
+ Story,
3622
+ StoryItem,
3623
+ TooManyRequestsException,
3624
+ TopSearchResults,
3625
+ TwoFactorAuthRequiredException,
3626
+ defaultIphoneHeaders,
3627
+ defaultUserAgent,
3628
+ extractHashtags,
3629
+ extractMentions,
3630
+ formatFilename,
3631
+ formatStringContainsKey,
3632
+ getConfigDir,
3633
+ getDefaultSessionFilename,
3634
+ getDefaultStampsFilename,
3635
+ getJsonStructure,
3636
+ loadStructure,
3637
+ mediaidToShortcode,
3638
+ resumableIteration,
3639
+ sanitizePath,
3640
+ shortcodeToMediaid
3641
+ };