@chilfish/gallery-dl-instagram 0.1.0 → 0.2.2

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.
@@ -0,0 +1,2268 @@
1
+ //#region src/config.ts
2
+ var ConfigManager = class {
3
+ data;
4
+ constructor(data = {}) {
5
+ this.data = data;
6
+ }
7
+ /**
8
+ * Read a value at a dot-path like ``'extractor.instagram.videos'``.
9
+ * Returns ``undefined`` when the path doesn't exist.
10
+ */
11
+ get(path, defaultValue) {
12
+ const keys = path.split(".");
13
+ let node = this.data;
14
+ for (const key of keys) {
15
+ if (node == null || typeof node !== "object" || Array.isArray(node)) return defaultValue;
16
+ node = node[key];
17
+ }
18
+ if (node === void 0) return defaultValue;
19
+ return node;
20
+ }
21
+ /**
22
+ * Interpolate a config key through a hierarchy of paths.
23
+ */
24
+ interpolate(cfgPath, key, defaultVal) {
25
+ let node = this.data;
26
+ for (let i = 0; i < cfgPath.length; i++) {
27
+ if (node != null && typeof node === "object" && !Array.isArray(node)) {
28
+ const v = node[key];
29
+ if (v !== void 0) return v;
30
+ }
31
+ if (node == null || typeof node !== "object" || Array.isArray(node)) break;
32
+ node = node[cfgPath[i]];
33
+ }
34
+ return defaultVal;
35
+ }
36
+ /**
37
+ * Mutate the config at a given dot-path.
38
+ */
39
+ set(path, value) {
40
+ const keys = path.split(".");
41
+ let node = this.data;
42
+ for (let i = 0; i < keys.length - 1; i++) {
43
+ const key = keys[i];
44
+ let child = node[key];
45
+ if (child == null || typeof child !== "object" || Array.isArray(child)) {
46
+ child = {};
47
+ node[key] = child;
48
+ }
49
+ node = child;
50
+ }
51
+ node[keys[keys.length - 1]] = value;
52
+ }
53
+ };
54
+ //#endregion
55
+ //#region src/core/format.ts
56
+ /** Shared ANSI formatting and display utilities. */
57
+ function formatBytes(bytes) {
58
+ if (bytes === 0) return "0 B";
59
+ const units = [
60
+ "B",
61
+ "KB",
62
+ "MB",
63
+ "GB"
64
+ ];
65
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
66
+ return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
67
+ }
68
+ const BOLD = "\x1B[1m";
69
+ const DIM = "\x1B[2m";
70
+ const CYAN = "\x1B[36m";
71
+ const GREEN = "\x1B[32m";
72
+ const YELLOW = "\x1B[33m";
73
+ const RESET = "\x1B[0m";
74
+ function b(s) {
75
+ return `${BOLD}${s}${RESET}`;
76
+ }
77
+ function dim(s) {
78
+ return `${DIM}${s}${RESET}`;
79
+ }
80
+ function c(s) {
81
+ return `${CYAN}${s}${RESET}`;
82
+ }
83
+ function g(s) {
84
+ return `${GREEN}${s}${RESET}`;
85
+ }
86
+ const _YELLOW = YELLOW;
87
+ const _RESET = RESET;
88
+ function pad(s, n) {
89
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
90
+ }
91
+ //#endregion
92
+ //#region src/core/job.ts
93
+ var Job = class {
94
+ extractor;
95
+ status = 0;
96
+ constructor(extractor) {
97
+ this.extractor = extractor;
98
+ }
99
+ /** Main entry point. Dispatches every yielded message. */
100
+ async run() {
101
+ this.extractor.log.info(`Starting ${this.extractor.category}/${this.extractor.subcategory} — ${this.extractor.url}`);
102
+ await this.extractor.initialize();
103
+ for await (const msg of this.extractor) switch (msg.type) {
104
+ case "directory":
105
+ await this.handleDirectory(msg);
106
+ break;
107
+ case "url":
108
+ await this.handleUrl(msg);
109
+ break;
110
+ case "queue":
111
+ await this.handleQueue(msg);
112
+ break;
113
+ }
114
+ this._report();
115
+ return this.status;
116
+ }
117
+ /** Override in subclasses to print a summary. */
118
+ _report() {}
119
+ };
120
+ //#endregion
121
+ //#region src/core/download-job.ts
122
+ var DownloadJob = class DownloadJob extends Job {
123
+ /** Base output directory (prepended to all paths). */
124
+ basePath = "";
125
+ /** Current target directory metadata (set by directory messages). */
126
+ _currentDir = {};
127
+ /** In-memory archive keyed by archive format. */
128
+ archive = /* @__PURE__ */ new Map();
129
+ _archiveFmts = /* @__PURE__ */ new Map();
130
+ _postCount = 0;
131
+ _fileCount = 0;
132
+ _downloadedBytes = 0;
133
+ _skippedCount = 0;
134
+ registerArchive(category, format) {
135
+ this._archiveFmts.set(category, format);
136
+ }
137
+ _interp(fmt, meta) {
138
+ return fmt.replace(/\{(\w+)\}/g, (_, key) => {
139
+ const v = meta[key];
140
+ return v == null ? "" : String(v);
141
+ });
142
+ }
143
+ _isArchived(meta) {
144
+ const cat = meta.category ?? this.extractor.category;
145
+ const fmt = this._archiveFmts.get(cat) ?? "{media_id}";
146
+ const key = this._interp(fmt, meta);
147
+ return !!this.archive.get(cat)?.has(key);
148
+ }
149
+ _archive(meta) {
150
+ const cat = meta.category ?? this.extractor.category;
151
+ const fmt = this._archiveFmts.get(cat) ?? "{media_id}";
152
+ const key = this._interp(fmt, meta);
153
+ let set = this.archive.get(cat);
154
+ if (!set) {
155
+ set = /* @__PURE__ */ new Set();
156
+ this.archive.set(cat, set);
157
+ }
158
+ set.add(key);
159
+ }
160
+ async handleDirectory(msg) {
161
+ this._currentDir = { ...msg.metadata };
162
+ this._postCount++;
163
+ const dirPath = this.basePath ? `${this.basePath}/${this._buildDirPath(msg.metadata)}` : this._buildDirPath(msg.metadata);
164
+ await this.extractor.storage.mkdir(dirPath);
165
+ this.extractor.log.info(`#${this._postCount} ${msg.metadata.username ?? "?"}/${msg.metadata.post_shortcode ?? "?"} → ${dirPath}/`);
166
+ }
167
+ async handleUrl(msg) {
168
+ const meta = {
169
+ ...this._currentDir,
170
+ ...msg.metadata
171
+ };
172
+ if (this._isArchived(meta)) {
173
+ this._skippedCount++;
174
+ return;
175
+ }
176
+ const filename = this._buildFilename(meta);
177
+ const fullPath = `${this.basePath ? `${this.basePath}/${this._buildDirPath(meta)}` : this._buildDirPath(meta)}/${filename}`;
178
+ try {
179
+ const resp = await this.extractor.http.request({
180
+ url: msg.url,
181
+ method: "GET",
182
+ responseType: "arraybuffer"
183
+ });
184
+ let data;
185
+ if (resp.data instanceof Uint8Array) data = resp.data;
186
+ else if (resp.data instanceof ArrayBuffer) data = new Uint8Array(resp.data);
187
+ else if (typeof resp.data === "string") data = resp.data;
188
+ else data = JSON.stringify(resp.data);
189
+ await this.extractor.storage.write(fullPath, data);
190
+ this._fileCount++;
191
+ const size = data instanceof Uint8Array ? data.byteLength : data.length;
192
+ this._downloadedBytes += size;
193
+ this.extractor.log.info(` └─ ${filename} (${formatBytes(size)})`);
194
+ this._archive(meta);
195
+ } catch (err) {
196
+ this.extractor.log.error(`Failed to download ${filename}: ${String(err)}`);
197
+ this.status |= 4;
198
+ }
199
+ }
200
+ async handleQueue(msg) {
201
+ const meta = {
202
+ ...this._currentDir,
203
+ ...msg.metadata
204
+ };
205
+ const extrClass = meta._extractor;
206
+ if (!extrClass || typeof extrClass !== "object") return;
207
+ const cls = extrClass;
208
+ const match = cls.pattern.exec(msg.url);
209
+ if (!match) return;
210
+ const parentExtr = this.extractor;
211
+ const childJob = new DownloadJob(Reflect.construct(cls, [{
212
+ url: msg.url,
213
+ match,
214
+ config: parentExtr.config,
215
+ http: parentExtr.http,
216
+ storage: parentExtr.storage,
217
+ log: parentExtr.log
218
+ }]));
219
+ childJob.basePath = this.basePath;
220
+ childJob._currentDir = meta;
221
+ for (const [cat, set] of this.archive) childJob.archive.set(cat, new Set(set));
222
+ for (const [cat, fmt] of this._archiveFmts) childJob._archiveFmts.set(cat, fmt);
223
+ const childStatus = await childJob.run();
224
+ this.status |= childStatus;
225
+ for (const [cat, set] of childJob.archive) {
226
+ const mine = this.archive.get(cat);
227
+ if (mine) for (const k of set) mine.add(k);
228
+ else this.archive.set(cat, set);
229
+ }
230
+ }
231
+ _report() {
232
+ const log = this.extractor.log;
233
+ log.info(`Done — ${this._postCount} post(s), ${this._fileCount} file(s) downloaded (${formatBytes(this._downloadedBytes)})`);
234
+ if (this._skippedCount > 0) log.info(` ${this._skippedCount} file(s) skipped (already archived)`);
235
+ }
236
+ _buildDirPath(meta) {
237
+ return `${meta.category ?? this.extractor.category}/${meta.username ?? "_"}`;
238
+ }
239
+ _buildFilename(meta) {
240
+ const mid = meta.media_id ?? "0";
241
+ const ext = meta.extension ?? "jpg";
242
+ return `${mid}${meta.num ? `_${meta.num}` : ""}.${ext}`;
243
+ }
244
+ };
245
+ //#endregion
246
+ //#region src/core/extractor.ts
247
+ /** A no-op logger */
248
+ const noopLogger = {
249
+ debug: () => {},
250
+ info: () => {},
251
+ warn: () => {},
252
+ error: () => {}
253
+ };
254
+ var Extractor = class {
255
+ /** Regex pattern to match against URLs */
256
+ static pattern = /^$/;
257
+ /** The input URL */
258
+ url;
259
+ /** Regex match groups from ``fromURL`` */
260
+ groups;
261
+ config;
262
+ /** HTTP client — public so Job can access for downloads */
263
+ http;
264
+ /** Storage backend — public so Job can access for writes */
265
+ storage;
266
+ /** Logger instance — public so Job can access for reporting */
267
+ log;
268
+ /** Delay range in seconds — random between [min, max] before each request */
269
+ requestInterval = [6, 12];
270
+ _initialized = false;
271
+ constructor(opts) {
272
+ this.url = opts.url;
273
+ this.groups = opts.match ? [...opts.match].slice(1) : [];
274
+ this.config = opts.config;
275
+ this.http = opts.http;
276
+ this.storage = opts.storage;
277
+ this.log = opts.log;
278
+ }
279
+ /** Initialization */
280
+ /**
281
+ * One-time async setup (cookies, session, internal state).
282
+ * Safe to call multiple times — after the first call it becomes a no-op.
283
+ */
284
+ async initialize() {
285
+ if (this._initialized) return;
286
+ await this._init();
287
+ this._initialized = true;
288
+ this.initialize = async () => {};
289
+ }
290
+ /**
291
+ * Subclass hook for one-time setup.
292
+ */
293
+ async _init() {}
294
+ /** Async iteration */
295
+ async *[Symbol.asyncIterator]() {
296
+ await this.initialize();
297
+ yield* this.items();
298
+ }
299
+ /** Config helpers */
300
+ /**
301
+ * Read a config value using the interpolated hierarchy.
302
+ */
303
+ _cfg(key, defaultVal) {
304
+ const path = [
305
+ "extractor",
306
+ this.category,
307
+ this.subcategory
308
+ ];
309
+ return this.config.interpolate(path, key, defaultVal);
310
+ }
311
+ /** HTTP */
312
+ _lastRequestTime = 0;
313
+ /**
314
+ * Rate-limited HTTP request wrapper.
315
+ */
316
+ async request(url, cfg = {}) {
317
+ await this._throttle();
318
+ const response = await this.http.request({
319
+ url,
320
+ ...cfg
321
+ });
322
+ this._lastRequestTime = Date.now();
323
+ return response;
324
+ }
325
+ /**
326
+ * Convenience: request + parse JSON body.
327
+ */
328
+ async requestJSON(url, cfg = {}) {
329
+ const resp = await this.request(url, cfg);
330
+ if (typeof resp.data === "object") return resp.data;
331
+ try {
332
+ return JSON.parse(resp.data);
333
+ } catch {
334
+ return {};
335
+ }
336
+ }
337
+ /** Rate limiting */
338
+ /**
339
+ * Sleep long enough to keep the minimum interval between requests.
340
+ */
341
+ async _throttle() {
342
+ const elapsed = Date.now() - this._lastRequestTime;
343
+ const [min, max] = this.requestInterval;
344
+ const target = min + Math.random() * (max - min);
345
+ const waitMs = Math.max(0, target * 1e3 - elapsed);
346
+ if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs));
347
+ }
348
+ /** Utility */
349
+ /**
350
+ * Convert a Unix timestamp (seconds or ms) to an ISO-8601 string.
351
+ */
352
+ parseTimestamp(ts) {
353
+ if (ts == null) return "";
354
+ const asMs = ts > 25e8 ? ts : ts * 1e3;
355
+ return new Date(asMs).toISOString();
356
+ }
357
+ /**
358
+ * Generate a random hex token (used for CSRF).
359
+ */
360
+ static generateToken(size = 16) {
361
+ const bytes = new Uint8Array(size);
362
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(bytes);
363
+ else for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
364
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
365
+ }
366
+ };
367
+ //#endregion
368
+ //#region src/utils/id-codec.ts
369
+ /**
370
+ * Instagram-style Base64-variant ID ↔ shortcode conversion.
371
+ */
372
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
373
+ /** Pre-built index for O(1) character lookup during decode. */
374
+ const CHAR_INDEX = {};
375
+ for (let i = 0; i < 64; i++) CHAR_INDEX[ALPHABET[i]] = i;
376
+ const BASE = BigInt(64);
377
+ /**
378
+ * Decode an Instagram shortcode into its numeric post ID.
379
+ */
380
+ function idFromShortcode(shortcode) {
381
+ let num = 0n;
382
+ for (const ch of shortcode) num = num * BASE + BigInt(CHAR_INDEX[ch] ?? 0);
383
+ return num.toString();
384
+ }
385
+ /**
386
+ * Encode a numeric post ID into an Instagram shortcode.
387
+ */
388
+ function shortcodeFromId(postId) {
389
+ let num = BigInt(postId);
390
+ const chars = [];
391
+ while (num > 0n) {
392
+ const remainder = Number(num % BASE);
393
+ chars.push(ALPHABET[remainder]);
394
+ num = num / BASE;
395
+ }
396
+ return chars.reverse().join("");
397
+ }
398
+ //#endregion
399
+ //#region src/message.ts
400
+ function directory(metadata = {}) {
401
+ return {
402
+ type: "directory",
403
+ metadata
404
+ };
405
+ }
406
+ function url(u, metadata = {}) {
407
+ return {
408
+ type: "url",
409
+ url: u,
410
+ metadata
411
+ };
412
+ }
413
+ function queue(u, metadata = {}) {
414
+ return {
415
+ type: "queue",
416
+ url: u,
417
+ metadata
418
+ };
419
+ }
420
+ //#endregion
421
+ //#region src/utils/text.ts
422
+ /**
423
+ * Text utilities ported from gallery-dl's ``text`` module.
424
+ *
425
+ * All functions are pure and environment-agnostic.
426
+ */
427
+ /** String extraction */
428
+ /**
429
+ * Extract the substring between ``begin`` and ``end`` from ``txt``.
430
+ * Returns the substring or ``null`` if either delimiter is missing.
431
+ */
432
+ function extract(txt, begin, end) {
433
+ const first = txt.indexOf(begin);
434
+ if (first < 0) return null;
435
+ const start = first + begin.length;
436
+ const last = txt.indexOf(end, start);
437
+ if (last < 0) return null;
438
+ return txt.slice(start, last);
439
+ }
440
+ /**
441
+ * Shorthand: same as ``extract`` but returns ``default_`` on failure.
442
+ * Mirrors the Python ``extr()`` function.
443
+ */
444
+ function extr(txt, begin, end, default_ = "") {
445
+ return extract(txt, begin, end) ?? default_;
446
+ }
447
+ /** Unicode / HTML */
448
+ /**
449
+ * Decode ``\\uXXXX`` escape sequences in a string.
450
+ */
451
+ function parseUnicodeEscapes$1(text) {
452
+ if (!text.includes("\\u")) return text;
453
+ return text.replace(/\\u([0-9a-fA-F]{4})/g, (_m, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
454
+ }
455
+ /**
456
+ * HTML entity decode.
457
+ *
458
+ * In Node.js we could use a DOM parser, but since this library is
459
+ * environment-agnostic we ship a minimal covering the common cases.
460
+ */
461
+ const HTML_ENTITIES = {
462
+ "amp": "&",
463
+ "lt": "<",
464
+ "gt": ">",
465
+ "quot": "\"",
466
+ "apos": "'",
467
+ "nbsp": "\xA0",
468
+ "#x27": "'",
469
+ "#x2F": "/",
470
+ "#39": "'",
471
+ "#47": "/"
472
+ };
473
+ const RE_ENTITY = /&([^;]+);/g;
474
+ function unescape(text) {
475
+ return text.replace(RE_ENTITY, (m, name) => {
476
+ const ch = HTML_ENTITIES[name];
477
+ if (ch !== void 0) return ch;
478
+ if (name.startsWith("#")) {
479
+ const cp = name[1] === "x" || name[1] === "X" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10);
480
+ if (Number.isSafeInteger(cp)) return String.fromCodePoint(cp);
481
+ }
482
+ return m;
483
+ });
484
+ }
485
+ /** URL helpers */
486
+ /**
487
+ * URL-decode a string.
488
+ */
489
+ function unquote(text) {
490
+ try {
491
+ return decodeURIComponent(text);
492
+ } catch {
493
+ return text.replace(/%[0-9a-f]{2}/gi, (m) => {
494
+ try {
495
+ return decodeURIComponent(m);
496
+ } catch {
497
+ return m;
498
+ }
499
+ });
500
+ }
501
+ }
502
+ /**
503
+ * Ensure a URL starts with ``https://`` (or ``http://``).
504
+ */
505
+ function ensureHttpScheme(url, scheme = "https://") {
506
+ if (!url) return url;
507
+ if (url.startsWith("https://") || url.startsWith("http://")) return url;
508
+ return scheme + url.replace(/^[/:]+/, "");
509
+ }
510
+ /**
511
+ * Extract filename + extension from a URL and write into ``meta``.
512
+ */
513
+ function nameExtFromURL(url, meta) {
514
+ const filename = filenameFromURL(url);
515
+ const dot = filename.lastIndexOf(".");
516
+ if (dot > 0 && filename.length - dot - 1 <= 16) {
517
+ meta.filename = unquote(filename.slice(0, dot));
518
+ meta.extension = unquote(filename.slice(dot + 1)).toLowerCase();
519
+ } else {
520
+ meta.filename = unquote(filename);
521
+ meta.extension = "";
522
+ }
523
+ }
524
+ /**
525
+ * Extract the file-name portion of a URL (before query string).
526
+ */
527
+ function filenameFromURL(url) {
528
+ try {
529
+ return url.split("?")[0].split("/").pop() ?? "";
530
+ } catch {
531
+ return "";
532
+ }
533
+ }
534
+ /**
535
+ * Parse an integer from a possibly-null value. Returns ``default_`` on failure.
536
+ */
537
+ function parseInt(value, default_ = 0) {
538
+ if (value == null) return default_;
539
+ const n = typeof value === "number" ? value : Number.parseInt(String(value), 10);
540
+ return Number.isFinite(n) ? n : default_;
541
+ }
542
+ function tagRe(pattern) {
543
+ const re = new RegExp(pattern, "g");
544
+ return (text) => {
545
+ const matches = text.match(re);
546
+ return matches ? [...new Set(matches)] : [];
547
+ };
548
+ }
549
+ /** Pre-configured hashtag regex. */
550
+ const findTags = tagRe("#\\w+");
551
+ //#endregion
552
+ //#region src/instagram/api.ts
553
+ const APP_ID = "936619743392459";
554
+ const ASBD_ID = "129477";
555
+ var InstagramRestAPI = class {
556
+ http;
557
+ root;
558
+ getCsrf;
559
+ getWwwClaim;
560
+ setWwwClaim;
561
+ setCsrf;
562
+ /** A ref to the extractor's cursor. */
563
+ getCursor;
564
+ setCursor;
565
+ constructor(opts) {
566
+ this.http = opts.http;
567
+ this.root = opts.root;
568
+ this.getCsrf = () => opts.csrfToken.value;
569
+ this.setCsrf = (v) => {
570
+ opts.csrfToken.value = v;
571
+ };
572
+ this.getWwwClaim = () => opts.wwwClaim.value;
573
+ this.setWwwClaim = (v) => {
574
+ opts.wwwClaim.value = v;
575
+ };
576
+ this.getCursor = () => opts.cursor.value;
577
+ this.setCursor = (v) => {
578
+ opts.cursor.value = v;
579
+ return v;
580
+ };
581
+ }
582
+ /** Public endpoint methods */
583
+ /** Single post by shortcode. */
584
+ async *media(shortcode) {
585
+ const endpoint = `/v1/media/${idFromShortcode(shortcode.length > 28 ? shortcode.slice(0, -28) : shortcode)}/info/`;
586
+ yield* this._pagination(endpoint);
587
+ }
588
+ /** Paginated user feed. */
589
+ userFeed(userId) {
590
+ return this._pagination(`/v1/feed/user/${userId}/`, { count: 30 });
591
+ }
592
+ /** Paginated user reels (POST endpoint). */
593
+ userClips(userId) {
594
+ const data = {
595
+ target_user_id: userId,
596
+ page_size: "50",
597
+ max_id: null,
598
+ include_feed_video: "true"
599
+ };
600
+ return this._paginationPost("/v1/clips/user/", data);
601
+ }
602
+ /** Paginated tagged posts. */
603
+ userTagged(userId) {
604
+ return this._pagination(`/v1/usertags/${userId}/feed/`, { count: 20 });
605
+ }
606
+ /** Paginated saved posts (media wrapper). */
607
+ userSaved() {
608
+ return this._pagination("/v1/feed/saved/posts/", { count: 50 }, true);
609
+ }
610
+ /** Paginated collection. */
611
+ userCollection(collectionId) {
612
+ return this._pagination(`/v1/feed/collection/${collectionId}/posts/`, { count: 50 }, true);
613
+ }
614
+ /** Reels media — batch call, returns full reel objects. */
615
+ async reelsMedia(reelIds) {
616
+ const data = await this._call("/v1/feed/reels_media/", { params: { reel_ids: reelIds } });
617
+ if (data && typeof data === "object") {
618
+ const reels = data.reels_media;
619
+ if (Array.isArray(reels)) return reels;
620
+ }
621
+ throw new Error("Auth required — authenticated cookies needed for reels");
622
+ }
623
+ /** Story tray. */
624
+ async reelsTray() {
625
+ const data = await this._call("/v1/feed/reels_tray/");
626
+ if (data && typeof data === "object") {
627
+ const tray = data.tray;
628
+ if (Array.isArray(tray)) return tray;
629
+ }
630
+ return [];
631
+ }
632
+ /** Highlights list (tray). */
633
+ async highlightsTray(userId) {
634
+ const data = await this._call(`/v1/highlights/${userId}/highlights_tray/`);
635
+ if (data && typeof data === "object") return data.tray ?? [];
636
+ return [];
637
+ }
638
+ /** All highlights' media batched by ``chunkSize``. */
639
+ async *highlightsMedia(userId, chunkSize = 5) {
640
+ const ids = (await this.highlightsTray(userId)).map((hl) => hl.id);
641
+ for (let i = 0; i < ids.length; i += chunkSize) {
642
+ const chunk = ids.slice(i, i + chunkSize);
643
+ yield* await this.reelsMedia(chunk);
644
+ }
645
+ }
646
+ /** Hashtag posts (via sections). */
647
+ async *tagsMedia(tag) {
648
+ for await (const section of this.tagsSections(tag)) {
649
+ const medias = section.layout_content?.medias ?? [];
650
+ for (const m of medias) if (m.media) yield m.media;
651
+ }
652
+ }
653
+ async *tagsSections(tag) {
654
+ yield* this._paginationSections(`/v1/tags/${tag}/sections/`, {
655
+ include_persistent: "0",
656
+ max_id: null,
657
+ page: null,
658
+ surface: "grid",
659
+ tab: "recent"
660
+ });
661
+ }
662
+ /** User by numeric ID. */
663
+ async userById(userId) {
664
+ const data = await this._call(`/v1/users/${userId}/info/`);
665
+ if (data && typeof data === "object") return data.user;
666
+ throw new Error("User not found");
667
+ }
668
+ /** User by username (web_profile_info). */
669
+ async userByName(username) {
670
+ const data = await this._call("/v1/users/web_profile_info/", { params: { username } });
671
+ if (data && typeof data === "object") return data.data;
672
+ throw new Error("User not found");
673
+ }
674
+ /** Search user by username. */
675
+ async userBySearch(username) {
676
+ const data = await this._call("https://www.instagram.com/web/search/topsearch/", { params: { query: username } });
677
+ if (data && typeof data === "object") {
678
+ const users = data.users;
679
+ if (users) {
680
+ const name = username.toLowerCase();
681
+ for (const result of users) if (result.user.username.toLowerCase() === name) return result.user;
682
+ }
683
+ }
684
+ throw new Error("User not found");
685
+ }
686
+ /** Scrape user ID from HTML profile page. */
687
+ async userByWeb(username) {
688
+ const resp = await this.http.request({
689
+ url: `https://www.instagram.com/${username}`,
690
+ headers: {
691
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
692
+ "Accept-Language": "en-US,en;q=0.5",
693
+ "Accept-Encoding": "gzip, deflate, br, zstd",
694
+ "Alt-Used": "www.instagram.com",
695
+ "Connection": "keep-alive",
696
+ "Sec-Fetch-Dest": "document",
697
+ "Sec-Fetch-Mode": "navigate",
698
+ "Sec-Fetch-Site": "none",
699
+ "Priority": "u=0, i"
700
+ }
701
+ });
702
+ const text = typeof resp.data === "string" ? resp.data : "";
703
+ const idx = text.indexOf("\"profile_id\":\"");
704
+ if (idx >= 0) {
705
+ const start = idx + 15;
706
+ const end = text.indexOf("\"", start);
707
+ if (end > start) return { id: text.slice(start, end) };
708
+ }
709
+ throw new Error("User not found");
710
+ }
711
+ /** Resolve screen name via fallback chain: search → info → web. */
712
+ async userByScreenName(screenName) {
713
+ for (const strategy of [
714
+ "search",
715
+ "info",
716
+ "web"
717
+ ]) try {
718
+ if (strategy === "search") return await this.userBySearch(screenName);
719
+ if (strategy === "info") return await this.userByName(screenName);
720
+ if (strategy === "web") {
721
+ const result = await this.userByWeb(screenName);
722
+ return {
723
+ pk: result.id,
724
+ id: result.id,
725
+ username: screenName,
726
+ full_name: ""
727
+ };
728
+ }
729
+ } catch {}
730
+ throw new Error("User not found");
731
+ }
732
+ /** Resolve username/id to numeric user ID string. */
733
+ async userId(screenName, checkPrivate = true) {
734
+ if (screenName.startsWith("id:")) return screenName.slice(3);
735
+ const user = await this.userByScreenName(screenName);
736
+ if (checkPrivate && user.is_private && !user.followed_by_viewer) {}
737
+ return user.id ?? user.pk;
738
+ }
739
+ /** Followers (paginated). */
740
+ async *userFollowers(userId) {
741
+ yield* this._paginationFollowing(`/v1/friendships/${userId}/followers/`, {
742
+ count: 12,
743
+ max_id: null
744
+ });
745
+ }
746
+ /** Following (paginated). */
747
+ async *userFollowing(userId) {
748
+ yield* this._paginationFollowing(`/v1/friendships/${userId}/following/`, {
749
+ count: 12,
750
+ max_id: null
751
+ });
752
+ }
753
+ /** Internal — HTTP call */
754
+ async _call(endpoint, opts = {}) {
755
+ const url = endpoint.startsWith("/") ? `https://www.instagram.com/api${endpoint}` : endpoint;
756
+ const csrf = this.getCsrf();
757
+ const headers = {
758
+ "Accept": "*/*",
759
+ "Cookie": `csrftoken=${csrf}`,
760
+ "X-CSRFToken": csrf,
761
+ "X-IG-App-ID": APP_ID,
762
+ "X-ASBD-ID": ASBD_ID,
763
+ "X-IG-WWW-Claim": this.getWwwClaim(),
764
+ "X-Requested-With": "XMLHttpRequest",
765
+ "Connection": "keep-alive",
766
+ "Referer": `${this.root}/`,
767
+ "Sec-Fetch-Dest": "empty",
768
+ "Sec-Fetch-Mode": "cors",
769
+ "Sec-Fetch-Site": "same-origin"
770
+ };
771
+ const resp = await this.http.request({
772
+ url,
773
+ method: opts.method ?? "GET",
774
+ headers,
775
+ params: opts.params ? Object.fromEntries(Object.entries(opts.params).filter(([, v]) => v != null)) : void 0,
776
+ data: opts.data
777
+ });
778
+ const finalUrl = resp.url;
779
+ if (finalUrl.includes("/accounts/login/")) throw new Error("Instagram redirected to login page — you need a valid sessionid. Export it from your browser (F12 → Application → Cookies → sessionid) and pass --sessionid=<value> or set INSTAGRAM_SESSIONID env var.");
780
+ if (finalUrl.includes("/challenge/")) throw new Error("Instagram redirected to challenge page — account flagged. Log in via browser to resolve the challenge, then export a fresh sessionid.");
781
+ const rawCookie = resp.headers["set-cookie"];
782
+ const csrfCookie = (Array.isArray(rawCookie) ? rawCookie.join("; ") : rawCookie ?? "").split(";").find((c) => c.trim().startsWith("csrftoken="));
783
+ if (csrfCookie) {
784
+ const val = csrfCookie.split("=")[1]?.trim();
785
+ if (val) this.setCsrf(val);
786
+ }
787
+ const claim = resp.headers["x-ig-set-www-claim"];
788
+ if (claim != null) this.setWwwClaim(String(claim));
789
+ return resp.data;
790
+ }
791
+ /** Pagination engines */
792
+ async *_pagination(endpoint, params = {}, media = false) {
793
+ let maxId = this.getCursor();
794
+ const reqParams = { ...params };
795
+ while (true) {
796
+ reqParams.max_id = maxId;
797
+ const data = await this._call(endpoint, { params: reqParams });
798
+ if (data) {
799
+ const items = data.items;
800
+ if (items) for (const item of items) if (media) yield item.media ?? item;
801
+ else yield item;
802
+ if (!data.more_available) {
803
+ this.setCursor(null);
804
+ return;
805
+ }
806
+ maxId = this.setCursor(data.next_max_id);
807
+ } else {
808
+ this.setCursor(null);
809
+ return;
810
+ }
811
+ }
812
+ }
813
+ async *_paginationPost(endpoint, reqData) {
814
+ let maxId = this.getCursor();
815
+ const data = { ...reqData };
816
+ while (true) {
817
+ data.max_id = maxId;
818
+ const resp = await this._call(endpoint, {
819
+ method: "POST",
820
+ data
821
+ });
822
+ if (resp) {
823
+ const items = resp.items;
824
+ if (items) for (const item of items) yield item.media ?? item;
825
+ const info = resp.paging_info;
826
+ if (!info || !info.more_available) {
827
+ this.setCursor(null);
828
+ return;
829
+ }
830
+ maxId = this.setCursor(info.max_id);
831
+ } else {
832
+ this.setCursor(null);
833
+ return;
834
+ }
835
+ }
836
+ }
837
+ async *_paginationSections(endpoint, reqData) {
838
+ let maxId = this.getCursor();
839
+ let page = null;
840
+ const data = { ...reqData };
841
+ while (true) {
842
+ data.max_id = maxId;
843
+ data.page = page;
844
+ const info = await this._call(endpoint, {
845
+ method: "POST",
846
+ data
847
+ });
848
+ if (info) {
849
+ const sections = info.sections;
850
+ if (sections) yield* sections;
851
+ if (!info.more_available) {
852
+ this.setCursor(null);
853
+ return;
854
+ }
855
+ page = info.next_page;
856
+ maxId = this.setCursor(info.next_max_id);
857
+ } else {
858
+ this.setCursor(null);
859
+ return;
860
+ }
861
+ }
862
+ }
863
+ async *_paginationFollowing(endpoint, params) {
864
+ let maxId = this._parseIntCursor(this.getCursor());
865
+ const reqParams = { ...params };
866
+ while (true) {
867
+ reqParams.max_id = maxId;
868
+ const data = await this._call(endpoint, { params: reqParams });
869
+ if (data) {
870
+ const users = data.users;
871
+ if (users) yield* users;
872
+ const nextMaxId = data.next_max_id;
873
+ if (nextMaxId == null) {
874
+ this.setCursor(null);
875
+ return;
876
+ }
877
+ maxId = this._parseIntCursor(String(nextMaxId));
878
+ this.setCursor(String(maxId));
879
+ } else {
880
+ this.setCursor(null);
881
+ return;
882
+ }
883
+ }
884
+ }
885
+ _parseIntCursor(v) {
886
+ if (v == null || v === "") return null;
887
+ const n = Number(v);
888
+ return Number.isFinite(n) ? n : null;
889
+ }
890
+ };
891
+ //#endregion
892
+ //#region src/instagram/parsers/rest.ts
893
+ /** Main entry — parse a REST post response. */
894
+ function parsePostRest(post, cfg) {
895
+ if (post.items) return parseStoryRest(post, cfg);
896
+ const owner = post.user;
897
+ const caption = post.caption;
898
+ const ts = post.taken_at ?? post.created_at;
899
+ const date = cfg.parseTimestamp(ts ?? null);
900
+ const data = {
901
+ post_id: post.pk,
902
+ post_shortcode: post.code,
903
+ post_url: `${cfg.root}/p/${post.code}/`,
904
+ likes: post.like_count ?? 0,
905
+ liked: post.has_liked ?? false,
906
+ pinned: extractPinned(post),
907
+ owner_id: owner.pk,
908
+ username: owner.username ?? "",
909
+ fullname: owner.full_name ?? "",
910
+ post_date: date,
911
+ date,
912
+ description: caption ? caption.text : "",
913
+ type: "post",
914
+ count: 0,
915
+ _files: []
916
+ };
917
+ const tags = cfg.findTags(data.description);
918
+ if (tags.length > 0) data.tags = [...new Set(tags)].sort();
919
+ if (post.location) {
920
+ const loc = post.location;
921
+ data.location_id = loc.pk;
922
+ data.location_slug = loc.short_name.replace(/\s+/g, "-").toLowerCase();
923
+ data.location_url = `${cfg.root}/explore/locations/${loc.pk}/${data.location_slug}/`;
924
+ }
925
+ if (post.coauthor_producers) data.coauthors = post.coauthor_producers.map((u) => ({
926
+ id: u.pk,
927
+ username: u.username,
928
+ full_name: u.full_name
929
+ }));
930
+ let items;
931
+ if (post.carousel_media?.length) {
932
+ data.sidecar_media_id = data.post_id;
933
+ data.sidecar_shortcode = data.post_shortcode;
934
+ items = post.carousel_media;
935
+ } else items = [post];
936
+ for (let num = 0; num < items.length; num++) {
937
+ const item = items[num];
938
+ const media = parseMediaItem(item, post, cfg, num + 1);
939
+ if (!media) continue;
940
+ const itemRec = item;
941
+ extractTaggedUsers(itemRec, media);
942
+ data._files.push(media);
943
+ const stickers = itemRec.story_music_stickers;
944
+ if (stickers?.[0]) {
945
+ const audio = extractAudio(itemRec, data, stickers[0], cfg);
946
+ if (audio) {
947
+ audio.num = num + 1;
948
+ data._files.push(audio);
949
+ }
950
+ }
951
+ }
952
+ if (post.music_metadata) {
953
+ const info = post.music_metadata.music_info;
954
+ if (info) {
955
+ const audio = extractAudio(post, data, { music_asset_info: info }, cfg);
956
+ if (audio) {
957
+ audio.num = items.length;
958
+ data._files.push(audio);
959
+ }
960
+ }
961
+ }
962
+ const files = data._files;
963
+ if (files.length === 1 && files[0].video_url) {
964
+ data.type = "reel";
965
+ data.post_url = `${cfg.root}/reel/${post.code}/`;
966
+ }
967
+ if (post.subscription_media_visibility) data.subscription = post.subscription_media_visibility;
968
+ return data;
969
+ }
970
+ /** Parse a story or highlight REST response. */
971
+ function parseStoryRest(post, cfg) {
972
+ const items = post.items;
973
+ const reelId = String(post.id).split(":").pop() ?? "0";
974
+ const date = cfg.parseTimestamp(post.taken_at ?? post.created_at ?? post.seen ?? null);
975
+ const expires = post.expiring_at;
976
+ const isStory = !!expires;
977
+ const data = {
978
+ post_id: reelId,
979
+ post_shortcode: shortcodeFromId(reelId),
980
+ post_url: isStory ? `${cfg.root}/stories/${post.user.username}/` : `${cfg.root}/stories/highlights/${reelId}/`,
981
+ likes: 0,
982
+ liked: false,
983
+ pinned: [],
984
+ owner_id: post.user.pk,
985
+ username: post.user.username ?? "",
986
+ fullname: post.user.full_name ?? "",
987
+ post_date: date,
988
+ date,
989
+ description: "",
990
+ type: isStory ? "story" : "highlight",
991
+ count: 0,
992
+ _files: [],
993
+ expires: expires ? cfg.parseTimestamp(expires) : void 0,
994
+ user: post.user
995
+ };
996
+ if (!isStory && post.title) data.highlight_title = post.title;
997
+ else if (!post.seen) post.seen = expires - 86400;
998
+ for (let num = 0; num < items.length; num++) {
999
+ const item = items[num];
1000
+ const media = parseMediaItem(item, post, cfg, num + 1);
1001
+ if (!media) continue;
1002
+ extractTaggedUsers(item, media);
1003
+ data._files.push(media);
1004
+ }
1005
+ return data;
1006
+ }
1007
+ /** Parse a single media item (image/video) from a carousel or story. */
1008
+ function parseMediaItem(item, parent, cfg, num) {
1009
+ let image;
1010
+ try {
1011
+ image = item.image_versions2.candidates[0];
1012
+ } catch {
1013
+ return null;
1014
+ }
1015
+ const itemRec = item;
1016
+ if (!cfg.staticVideo && item.original_media_type != null && item.original_media_type === 1 && item.original_media_type !== item.media_type) {
1017
+ delete itemRec.video_versions;
1018
+ if (image) {
1019
+ item.original_width = image.width;
1020
+ item.original_height = image.height;
1021
+ }
1022
+ }
1023
+ const widthOrig = item.original_width ?? 0;
1024
+ const heightOrig = item.original_height ?? 0;
1025
+ let video = null;
1026
+ let manifest = null;
1027
+ let width;
1028
+ let height;
1029
+ if (item.video_versions?.length) {
1030
+ video = item.video_versions.reduce((best, v) => v.width * v.height * v.type > best.width * best.height * best.type ? v : best);
1031
+ if (item.video_dash_manifest && cfg.videosDash) {
1032
+ manifest = item.video_dash_manifest;
1033
+ width = widthOrig;
1034
+ height = heightOrig;
1035
+ } else {
1036
+ width = video.width;
1037
+ height = video.height;
1038
+ }
1039
+ } else {
1040
+ video = null;
1041
+ manifest = null;
1042
+ width = image.width;
1043
+ height = image.height;
1044
+ }
1045
+ const media = {
1046
+ num,
1047
+ date: cfg.parseTimestamp(itemRec.taken_at ?? video?.taken_at ?? parent.taken_at ?? null),
1048
+ media_id: item.pk,
1049
+ shortcode: item.code ?? shortcodeFromId(item.pk),
1050
+ display_url: image.url,
1051
+ video_url: video?.url ?? null,
1052
+ width,
1053
+ width_original: widthOrig,
1054
+ height,
1055
+ height_original: heightOrig,
1056
+ tagged_users: []
1057
+ };
1058
+ if (manifest != null) media._ytdl_manifest_data = manifest;
1059
+ if (item.owner) media.owner = item.owner;
1060
+ if (item.reshared_story_media_author) media.author = item.reshared_story_media_author;
1061
+ if (item.expiring_at != null) media.expires = cfg.parseTimestamp(item.expiring_at);
1062
+ if (item.subscription_media_visibility) media.subscription = item.subscription_media_visibility;
1063
+ if (itemRec.audience) media.audience = itemRec.audience;
1064
+ return media;
1065
+ }
1066
+ /** Extract tagged users from various field formats. */
1067
+ function extractTaggedUsers(src, dest) {
1068
+ dest.tagged_users = [];
1069
+ const edges = src.edge_media_to_tagged_user;
1070
+ if (edges?.edges) for (const edge of edges.edges) {
1071
+ const u = edge.node.user;
1072
+ dest.tagged_users.push({
1073
+ id: u.id ?? u.pk,
1074
+ username: u.username,
1075
+ full_name: u.full_name
1076
+ });
1077
+ }
1078
+ const usertags = src.usertags;
1079
+ if (usertags?.in) for (const tag of usertags.in) {
1080
+ const u = tag.user;
1081
+ dest.tagged_users.push({
1082
+ id: u.pk,
1083
+ username: u.username,
1084
+ full_name: u.full_name
1085
+ });
1086
+ }
1087
+ const mentions = src.reel_mentions;
1088
+ if (mentions) for (const m of mentions) {
1089
+ const u = m.user;
1090
+ dest.tagged_users.push({
1091
+ id: u.pk ?? u.id ?? "",
1092
+ username: u.username,
1093
+ full_name: u.full_name
1094
+ });
1095
+ }
1096
+ const bloks = src.story_bloks_stickers;
1097
+ if (bloks) for (const sticker of bloks) {
1098
+ const s = sticker.bloks_sticker;
1099
+ if (s.bloks_sticker_type === "mention") {
1100
+ const m = s.sticker_data.ig_mention;
1101
+ dest.tagged_users.push({
1102
+ id: m.account_id,
1103
+ username: m.username,
1104
+ full_name: m.full_name
1105
+ });
1106
+ }
1107
+ }
1108
+ const seen = /* @__PURE__ */ new Set();
1109
+ dest.tagged_users = dest.tagged_users.filter((t) => seen.has(t.id) ? false : (seen.add(t.id), true));
1110
+ }
1111
+ /** Extract audio/music metadata from a story sticker. */
1112
+ function extractAudio(src, dest, sticker, cfg) {
1113
+ const info = sticker.music_asset_info;
1114
+ if (!info) return null;
1115
+ const cinfo = sticker.music_consumption_info ?? info;
1116
+ dest.audio_title = info.title;
1117
+ dest.audio_duration = (info.duration_in_ms ?? 0) / 1e3;
1118
+ dest.audio_timestamps = info.highlight_start_times_in_ms;
1119
+ dest.audio_artist = info.display_artist ?? cinfo.display_artist;
1120
+ dest.audio_user = info.ig_artist ?? cinfo.ig_artist;
1121
+ const url = info.progressive_download_url;
1122
+ if (!url) return null;
1123
+ return {
1124
+ num: 0,
1125
+ date: cfg.parseTimestamp(src.taken_at ?? null),
1126
+ media_id: info.id,
1127
+ shortcode: shortcodeFromId(info.id),
1128
+ display_url: info.cover_artwork_uri ?? "",
1129
+ video_url: null,
1130
+ audio_url: url,
1131
+ width: 0,
1132
+ width_original: 0,
1133
+ height: 0,
1134
+ height_original: 0,
1135
+ tagged_users: [],
1136
+ audio_user: info.ig_artist ?? cinfo.ig_artist,
1137
+ audio_title: info.title,
1138
+ audio_artist: info.display_artist ?? cinfo.display_artist,
1139
+ audio_duration: (info.duration_in_ms ?? 0) / 1e3,
1140
+ audio_timestamps: info.highlight_start_times_in_ms
1141
+ };
1142
+ }
1143
+ function extractPinned(post) {
1144
+ if (post.timeline_pinned_user_ids) return post.timeline_pinned_user_ids;
1145
+ if (post.clips_tab_pinned_user_ids) return post.clips_tab_pinned_user_ids;
1146
+ return [];
1147
+ }
1148
+ //#endregion
1149
+ //#region src/instagram/parsers/graphql.ts
1150
+ /** Parse a GraphQL post/edge response. */
1151
+ function parsePostGraphql(post, cfg) {
1152
+ const typename = post.__typename ?? "GraphImage";
1153
+ const owner = post.owner;
1154
+ const date = cfg.parseTimestamp(post.taken_at_timestamp);
1155
+ const data = {
1156
+ typename,
1157
+ likes: post.edge_media_preview_like?.count ?? 0,
1158
+ liked: post.viewer_has_liked ?? false,
1159
+ pinned: post.pinned_for_users?.map((u) => Number(u.id)) ?? [],
1160
+ owner_id: owner.id ?? owner.pk,
1161
+ username: owner.username ?? "",
1162
+ fullname: owner.full_name ?? "",
1163
+ post_id: post.id,
1164
+ post_shortcode: post.shortcode,
1165
+ post_url: `${cfg.root}/p/${post.shortcode}/`,
1166
+ post_date: date,
1167
+ date,
1168
+ description: "",
1169
+ type: "post",
1170
+ count: 0,
1171
+ _files: []
1172
+ };
1173
+ data.description = post.edge_media_to_caption?.edges?.map((e) => e.node.text).join("\n") ?? "";
1174
+ data.description = parseUnicodeEscapes(data.description);
1175
+ const tags = cfg.findTags(data.description);
1176
+ if (tags.length > 0) data.tags = [...new Set(tags)].sort();
1177
+ const location = post.location;
1178
+ if (location) {
1179
+ data.location_id = location.pk;
1180
+ data.location_slug = location.short_name;
1181
+ data.location_url = `${cfg.root}/explore/locations/${location.pk}/${location.short_name}/`;
1182
+ }
1183
+ const coauthors = post.coauthor_producers;
1184
+ if (coauthors?.length) data.coauthors = coauthors.map((u) => ({
1185
+ id: u.id ?? u.pk,
1186
+ username: u.username
1187
+ }));
1188
+ const sidecar = post.edge_sidecar_to_children;
1189
+ if (sidecar?.edges) {
1190
+ data.sidecar_media_id = data.post_id;
1191
+ data.sidecar_shortcode = data.post_shortcode;
1192
+ let num = 0;
1193
+ for (const edge of sidecar.edges) {
1194
+ num++;
1195
+ const node = edge.node;
1196
+ const dimensions = node.dimensions;
1197
+ const media = {
1198
+ num,
1199
+ date: data.date,
1200
+ media_id: node.id,
1201
+ shortcode: node.shortcode ?? shortcodeFromId(node.id),
1202
+ display_url: node.display_url,
1203
+ video_url: node.video_url ?? null,
1204
+ width: dimensions.width,
1205
+ height: dimensions.height,
1206
+ sidecar_media_id: data.post_id,
1207
+ sidecar_shortcode: data.post_shortcode,
1208
+ tagged_users: [],
1209
+ width_original: dimensions.width,
1210
+ height_original: dimensions.height
1211
+ };
1212
+ extractTaggedUsers(node, media);
1213
+ data._files.push(media);
1214
+ }
1215
+ } else {
1216
+ const dimensions = post.dimensions;
1217
+ const media = {
1218
+ num: 1,
1219
+ date: data.date,
1220
+ media_id: post.id,
1221
+ shortcode: post.shortcode,
1222
+ display_url: post.display_url,
1223
+ video_url: post.video_url ?? null,
1224
+ width: dimensions.width,
1225
+ height: dimensions.height,
1226
+ tagged_users: [],
1227
+ width_original: dimensions.width,
1228
+ height_original: dimensions.height
1229
+ };
1230
+ extractTaggedUsers(post, media);
1231
+ data._files.push(media);
1232
+ }
1233
+ return data;
1234
+ }
1235
+ function parseUnicodeEscapes(text) {
1236
+ if (!text.includes("\\u")) return text;
1237
+ return text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
1238
+ }
1239
+ //#endregion
1240
+ //#region src/instagram/base.ts
1241
+ var Ref = class {
1242
+ value;
1243
+ constructor(v) {
1244
+ this.value = v;
1245
+ }
1246
+ };
1247
+ var InstagramExtractor = class extends Extractor {
1248
+ category = "instagram";
1249
+ root = "https://www.instagram.com";
1250
+ api;
1251
+ csrfToken = new Ref("");
1252
+ wwwClaim = new Ref("0");
1253
+ cursor = new Ref(null);
1254
+ _loggedIn = true;
1255
+ _user = null;
1256
+ _findTags = findTags;
1257
+ _csrfSeed;
1258
+ constructor(opts) {
1259
+ super(opts);
1260
+ this._csrfSeed = opts.csrfToken;
1261
+ }
1262
+ /** Initialization */
1263
+ async _init() {
1264
+ this.csrfToken.value = this._csrfSeed || Extractor.generateToken(16);
1265
+ this.api = new InstagramRestAPI({
1266
+ http: this.http,
1267
+ root: this.root,
1268
+ csrfToken: this.csrfToken,
1269
+ wwwClaim: this.wwwClaim,
1270
+ cursor: this.cursor
1271
+ });
1272
+ }
1273
+ /** Request override */
1274
+ async request(url, cfg = {}) {
1275
+ const response = await super.request(url, cfg);
1276
+ const finalUrl = response.url;
1277
+ if (finalUrl.includes("/accounts/login/")) throw new Error("HTTP redirect to login page — cookies expired or invalid");
1278
+ if (finalUrl.includes("/challenge/")) throw new Error("HTTP redirect to challenge page — account flagged");
1279
+ const claim = response.headers["x-ig-set-www-claim"];
1280
+ if (claim != null) this.wwwClaim.value = String(claim);
1281
+ return response;
1282
+ }
1283
+ /** Login */
1284
+ async login() {
1285
+ this._loggedIn = true;
1286
+ }
1287
+ /** Core pipeline */
1288
+ async *items() {
1289
+ await this.login();
1290
+ const meta = await this.metadata() ?? {};
1291
+ const videos = this._cfg("videos", true);
1292
+ const videosDash = videos !== "merged";
1293
+ const shouldDownloadVideos = !!videos;
1294
+ const previews = this._cfg("previews", false);
1295
+ const previewsVid = typeof previews === "object" ? previews.includes("video") : false;
1296
+ const previewsAud = typeof previews === "object" ? previews.includes("audio") : false;
1297
+ const audio = this._cfg("audio", false);
1298
+ const maxPosts = this._cfg("max-posts");
1299
+ const orderFiles = this._cfg("order-files");
1300
+ const reverse = orderFiles ? ["r", "d"].includes(orderFiles[0]) : false;
1301
+ const parserCfg = {
1302
+ root: this.root,
1303
+ findTags: this._findTags,
1304
+ parseTimestamp: this.parseTimestamp.bind(this),
1305
+ staticVideo: this._cfg("static-videos", true) ?? true,
1306
+ warnVideo: !previews && shouldDownloadVideos,
1307
+ warnImage: 1,
1308
+ videosDash
1309
+ };
1310
+ this.log.debug(`cfg: videos=${shouldDownloadVideos} previews=${!!previews} audio=${audio} maxPosts=${maxPosts ?? "∞"} staticVideos=${parserCfg.staticVideo}`);
1311
+ let count = 0;
1312
+ for await (const post of this.posts()) {
1313
+ if (maxPosts != null && count >= maxPosts) break;
1314
+ count++;
1315
+ const parsed = "__typename" in post ? parsePostGraphql(post, parserCfg) : parsePostRest(post, parserCfg);
1316
+ if (this._user) parsed.user = this._user;
1317
+ Object.assign(parsed, meta);
1318
+ const files = parsed._files;
1319
+ parsed.count = files.length;
1320
+ yield {
1321
+ type: "directory",
1322
+ metadata: parsed
1323
+ };
1324
+ const ordered = reverse ? [...files].reverse() : files;
1325
+ for (const file of ordered) {
1326
+ const combined = {
1327
+ ...parsed,
1328
+ ...file
1329
+ };
1330
+ if (file.audio_url) {
1331
+ if (audio) {
1332
+ nameExtFromURL(file.audio_url, combined);
1333
+ yield url(file.audio_url, combined);
1334
+ }
1335
+ if (previewsAud) combined.media_id = `${combined.media_id}p`;
1336
+ else continue;
1337
+ }
1338
+ if (file.video_url) {
1339
+ if (shouldDownloadVideos) {
1340
+ nameExtFromURL(file.video_url, combined);
1341
+ yield url(file.video_url, combined);
1342
+ }
1343
+ if (previewsVid) combined.media_id = `${combined.media_id}p`;
1344
+ else continue;
1345
+ }
1346
+ const imgUrl = file.display_url;
1347
+ nameExtFromURL(imgUrl, combined);
1348
+ if (combined.extension === "webp" && imgUrl.includes("stp=dst-jpg")) combined.extension = "jpg";
1349
+ yield url(imgUrl, combined);
1350
+ }
1351
+ }
1352
+ if (count === 0) this.log.warn("No posts returned — API may have returned empty data (check sessionid or post visibility)");
1353
+ }
1354
+ /** Subclass hooks */
1355
+ /** @virtual */
1356
+ async metadata() {
1357
+ return {};
1358
+ }
1359
+ /** Cursor management */
1360
+ _initCursor() {
1361
+ const cursor = this._cfg("cursor", true);
1362
+ if (cursor === true) return null;
1363
+ if (!cursor) return null;
1364
+ return cursor;
1365
+ }
1366
+ _updateCursor(cursor) {
1367
+ if (cursor) this.log.debug(`Cursor: ${cursor}`);
1368
+ this.cursor.value = cursor;
1369
+ return cursor;
1370
+ }
1371
+ /** User assignment */
1372
+ _assignUser(user) {
1373
+ this._user = user;
1374
+ const mappings = [
1375
+ ["count_media", "edge_owner_to_timeline_media"],
1376
+ ["count_video", "edge_felix_video_timeline"],
1377
+ ["count_saved", "edge_saved_media"],
1378
+ ["count_mutual", "edge_mutual_followed_by"],
1379
+ ["count_follow", "edge_follow"],
1380
+ ["count_followed", "edge_followed_by"],
1381
+ ["count_collection", "edge_media_collections"]
1382
+ ];
1383
+ const rec = user;
1384
+ for (const [newKey, oldKey] of mappings) try {
1385
+ rec[newKey] = rec[oldKey]?.count ?? 0;
1386
+ delete rec[oldKey];
1387
+ } catch {
1388
+ rec[newKey] = 0;
1389
+ }
1390
+ }
1391
+ };
1392
+ //#endregion
1393
+ //#region src/instagram/extractors/helpers.ts
1394
+ /** Shared regex utilities for Instagram extractor URL patterns. */
1395
+ const BASE_RE = /^(?:https?:\/\/)?(?:www\.)?instagram\.com/;
1396
+ function re(base, path) {
1397
+ const pathSrc = typeof path === "string" ? path : path.source;
1398
+ return new RegExp(base.source + pathSrc, "i");
1399
+ }
1400
+ //#endregion
1401
+ //#region src/instagram/extractors/registry.ts
1402
+ const _registry = /* @__PURE__ */ new Map();
1403
+ function register(subcategory, cls) {
1404
+ _registry.set(subcategory, cls);
1405
+ }
1406
+ function get(subcategory) {
1407
+ return _registry.get(subcategory);
1408
+ }
1409
+ //#endregion
1410
+ //#region src/instagram/extractors/avatar.ts
1411
+ var InstagramAvatarExtractor = class InstagramAvatarExtractor extends InstagramExtractor {
1412
+ static subcategory = "avatar";
1413
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/avatar/);
1414
+ subcategory = InstagramAvatarExtractor.subcategory;
1415
+ constructor(opts) {
1416
+ super(opts);
1417
+ }
1418
+ static fromURL(url, opts) {
1419
+ const match = InstagramAvatarExtractor.pattern.exec(url);
1420
+ if (!match) return null;
1421
+ return new InstagramAvatarExtractor({
1422
+ ...opts,
1423
+ url,
1424
+ match
1425
+ });
1426
+ }
1427
+ async *posts() {
1428
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1429
+ let user;
1430
+ if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1431
+ else user = await this.api.userByScreenName(screenName);
1432
+ const avatar = user.hd_profile_pic_url_info ?? user.hd_profile_pic_versions?.[user.hd_profile_pic_versions.length - 1] ?? {
1433
+ url: user.profile_pic_url ?? "",
1434
+ width: 0,
1435
+ height: 0
1436
+ };
1437
+ let pk = user.profile_pic_id?.split("_")[0];
1438
+ let code;
1439
+ if (pk) code = shortcodeFromId(pk);
1440
+ else {
1441
+ pk = `avatar:${user.pk}`;
1442
+ code = pk;
1443
+ }
1444
+ yield {
1445
+ pk,
1446
+ code,
1447
+ user,
1448
+ caption: null,
1449
+ like_count: 0,
1450
+ image_versions2: { candidates: [avatar] }
1451
+ };
1452
+ }
1453
+ };
1454
+ register(InstagramAvatarExtractor.subcategory, InstagramAvatarExtractor);
1455
+ //#endregion
1456
+ //#region src/instagram/extractors/highlights.ts
1457
+ var InstagramHighlightsExtractor = class InstagramHighlightsExtractor extends InstagramExtractor {
1458
+ static subcategory = "highlights";
1459
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/highlights/);
1460
+ subcategory = InstagramHighlightsExtractor.subcategory;
1461
+ constructor(opts) {
1462
+ super(opts);
1463
+ }
1464
+ static fromURL(url, opts) {
1465
+ const match = InstagramHighlightsExtractor.pattern.exec(url);
1466
+ if (!match) return null;
1467
+ return new InstagramHighlightsExtractor({
1468
+ ...opts,
1469
+ url,
1470
+ match
1471
+ });
1472
+ }
1473
+ async *posts() {
1474
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1475
+ const uid = await this.api.userId(screenName);
1476
+ yield* this.api.highlightsMedia(uid);
1477
+ }
1478
+ };
1479
+ register(InstagramHighlightsExtractor.subcategory, InstagramHighlightsExtractor);
1480
+ //#endregion
1481
+ //#region src/instagram/extractors/info.ts
1482
+ var InstagramInfoExtractor = class InstagramInfoExtractor extends InstagramExtractor {
1483
+ static subcategory = "info";
1484
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/info/);
1485
+ subcategory = InstagramInfoExtractor.subcategory;
1486
+ constructor(opts) {
1487
+ super(opts);
1488
+ }
1489
+ static fromURL(url, opts) {
1490
+ const match = InstagramInfoExtractor.pattern.exec(url);
1491
+ if (!match) return null;
1492
+ return new InstagramInfoExtractor({
1493
+ ...opts,
1494
+ url,
1495
+ match
1496
+ });
1497
+ }
1498
+ async *items() {
1499
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1500
+ let user;
1501
+ if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1502
+ else user = await this.api.userByScreenName(screenName);
1503
+ yield directory(user);
1504
+ }
1505
+ async *posts() {}
1506
+ };
1507
+ register(InstagramInfoExtractor.subcategory, InstagramInfoExtractor);
1508
+ //#endregion
1509
+ //#region src/instagram/extractors/post.ts
1510
+ var InstagramPostExtractor = class InstagramPostExtractor extends InstagramExtractor {
1511
+ static subcategory = "post";
1512
+ static pattern = re(/^(?:https?:\/\/)?(?:www\.)?instagram\.com\//, /(?:share(?:\/(?:p|tv|reels?))?|(?:[^/?#]+\/)?(?:p|tv|reels?))\/([^/?#]+)/);
1513
+ subcategory = InstagramPostExtractor.subcategory;
1514
+ constructor(opts) {
1515
+ super(opts);
1516
+ if (opts.match[2] != null || opts.match[3] != null) this.subcategory = "reel";
1517
+ }
1518
+ static fromURL(url, opts) {
1519
+ const match = InstagramPostExtractor.pattern.exec(url);
1520
+ if (!match) return null;
1521
+ return new InstagramPostExtractor({
1522
+ ...opts,
1523
+ url,
1524
+ match
1525
+ });
1526
+ }
1527
+ async *posts() {
1528
+ const groups = this.groups;
1529
+ let shortcode = groups[0];
1530
+ if (!shortcode) return;
1531
+ if (groups[1] === "") {
1532
+ this.log.info(`Resolving share link: ${this.url}`);
1533
+ const parts = (await this.request(ensureHttpScheme(this.url), { headers: {
1534
+ "Sec-Fetch-Dest": "empty",
1535
+ "Sec-Fetch-Mode": "navigate",
1536
+ "Sec-Fetch-Site": "same-origin"
1537
+ } })).url?.split("/");
1538
+ shortcode = parts?.[parts.length - 2] ?? shortcode;
1539
+ }
1540
+ this.log.debug(`Fetching post: ${shortcode}`);
1541
+ yield* this.api.media(shortcode);
1542
+ }
1543
+ };
1544
+ register(InstagramPostExtractor.subcategory, InstagramPostExtractor);
1545
+ //#endregion
1546
+ //#region src/instagram/extractors/posts-list.ts
1547
+ var InstagramPostsExtractor = class InstagramPostsExtractor extends InstagramExtractor {
1548
+ static subcategory = "posts";
1549
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/posts/);
1550
+ subcategory = InstagramPostsExtractor.subcategory;
1551
+ constructor(opts) {
1552
+ super(opts);
1553
+ }
1554
+ static fromURL(url, opts) {
1555
+ const match = InstagramPostsExtractor.pattern.exec(url);
1556
+ if (!match) return null;
1557
+ return new InstagramPostsExtractor({
1558
+ ...opts,
1559
+ url,
1560
+ match
1561
+ });
1562
+ }
1563
+ async *posts() {
1564
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1565
+ const uid = await this.api.userId(screenName);
1566
+ yield* this.api.userFeed(uid);
1567
+ }
1568
+ };
1569
+ register(InstagramPostsExtractor.subcategory, InstagramPostsExtractor);
1570
+ //#endregion
1571
+ //#region src/instagram/extractors/reels-list.ts
1572
+ var InstagramReelsExtractor = class InstagramReelsExtractor extends InstagramExtractor {
1573
+ static subcategory = "reels";
1574
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/reels/);
1575
+ subcategory = InstagramReelsExtractor.subcategory;
1576
+ constructor(opts) {
1577
+ super(opts);
1578
+ }
1579
+ static fromURL(url, opts) {
1580
+ const match = InstagramReelsExtractor.pattern.exec(url);
1581
+ if (!match) return null;
1582
+ return new InstagramReelsExtractor({
1583
+ ...opts,
1584
+ url,
1585
+ match
1586
+ });
1587
+ }
1588
+ async *posts() {
1589
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1590
+ const uid = await this.api.userId(screenName);
1591
+ yield* this.api.userClips(uid);
1592
+ }
1593
+ };
1594
+ register(InstagramReelsExtractor.subcategory, InstagramReelsExtractor);
1595
+ //#endregion
1596
+ //#region src/instagram/extractors/saved.ts
1597
+ var InstagramSavedExtractor = class InstagramSavedExtractor extends InstagramExtractor {
1598
+ static subcategory = "saved";
1599
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/saved(?:\/all-posts)?\/?$/);
1600
+ subcategory = InstagramSavedExtractor.subcategory;
1601
+ constructor(opts) {
1602
+ super(opts);
1603
+ }
1604
+ static fromURL(url, opts) {
1605
+ const match = InstagramSavedExtractor.pattern.exec(url);
1606
+ if (!match) return null;
1607
+ return new InstagramSavedExtractor({
1608
+ ...opts,
1609
+ url,
1610
+ match
1611
+ });
1612
+ }
1613
+ async *posts() {
1614
+ yield* this.api.userSaved();
1615
+ }
1616
+ };
1617
+ register(InstagramSavedExtractor.subcategory, InstagramSavedExtractor);
1618
+ //#endregion
1619
+ //#region src/instagram/extractors/stories.ts
1620
+ var InstagramStoriesExtractor = class InstagramStoriesExtractor extends InstagramExtractor {
1621
+ static subcategory = "stories";
1622
+ static pattern = /^(?:https?:\/\/)?(?:www\.)?instagram\.com\/(?:stories\/(?:highlights\/(\d+)|([^/?#]+)(?:\/(\d+))?)|\/(aGlnaGxpZ2h0[^?#]+)(?:\?story_media_id=(\d+))?)/;
1623
+ subcategory = InstagramStoriesExtractor.subcategory;
1624
+ highlightId = null;
1625
+ mediaId = null;
1626
+ constructor(opts) {
1627
+ super(opts);
1628
+ const groups = this.groups;
1629
+ const h1 = groups[0];
1630
+ const user = groups[1];
1631
+ const m1 = groups[2];
1632
+ const h2 = groups[3];
1633
+ const m2 = groups[4];
1634
+ if (user) {
1635
+ this.subcategory = "stories";
1636
+ this.highlightId = null;
1637
+ } else {
1638
+ this.subcategory = "highlights";
1639
+ this.highlightId = h1 ? `highlight:${h1}` : `highlight:${Buffer.from(h2 ?? "", "base64").toString("utf-8")}`;
1640
+ }
1641
+ this.mediaId = m1 ?? m2 ?? null;
1642
+ }
1643
+ static fromURL(url, opts) {
1644
+ const match = InstagramStoriesExtractor.pattern.exec(url);
1645
+ if (!match) return null;
1646
+ return new InstagramStoriesExtractor({
1647
+ ...opts,
1648
+ url,
1649
+ match
1650
+ });
1651
+ }
1652
+ async *posts() {
1653
+ const reelId = this.highlightId ? this.highlightId : await this.api.userId((this.groups[1] ?? "").toString());
1654
+ const reels = await this.api.reelsMedia([reelId]);
1655
+ if (!reels.length) return;
1656
+ if (this.mediaId) {
1657
+ const reel = reels[0];
1658
+ for (const item of reel.items ?? []) if (item.pk === this.mediaId) {
1659
+ reel.items = [item];
1660
+ break;
1661
+ }
1662
+ yield reel;
1663
+ return;
1664
+ }
1665
+ if (this._cfg("split", false)) {
1666
+ const reel = reels[0];
1667
+ for (const item of reel.items ?? []) {
1668
+ const copy = { ...reel };
1669
+ copy.items = [item];
1670
+ yield copy;
1671
+ }
1672
+ } else yield* reels;
1673
+ }
1674
+ };
1675
+ register(InstagramStoriesExtractor.subcategory, InstagramStoriesExtractor);
1676
+ //#endregion
1677
+ //#region src/instagram/extractors/tag.ts
1678
+ var InstagramTagExtractor = class InstagramTagExtractor extends InstagramExtractor {
1679
+ static subcategory = "tag";
1680
+ static pattern = re(BASE_RE, /\/explore\/tags\/([^/?#]+)/);
1681
+ subcategory = InstagramTagExtractor.subcategory;
1682
+ constructor(opts) {
1683
+ super(opts);
1684
+ }
1685
+ static fromURL(url, opts) {
1686
+ const match = InstagramTagExtractor.pattern.exec(url);
1687
+ if (!match) return null;
1688
+ return new InstagramTagExtractor({
1689
+ ...opts,
1690
+ url,
1691
+ match
1692
+ });
1693
+ }
1694
+ async metadata() {
1695
+ const tag = this.groups[0] ?? "";
1696
+ return { tag: decodeURIComponent(tag) };
1697
+ }
1698
+ async *posts() {
1699
+ const tag = this.groups[0] ?? "";
1700
+ yield* this.api.tagsMedia(decodeURIComponent(tag));
1701
+ }
1702
+ };
1703
+ register(InstagramTagExtractor.subcategory, InstagramTagExtractor);
1704
+ //#endregion
1705
+ //#region src/instagram/extractors/tagged.ts
1706
+ var InstagramTaggedExtractor = class InstagramTaggedExtractor extends InstagramExtractor {
1707
+ static subcategory = "tagged";
1708
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/tagged/);
1709
+ subcategory = InstagramTaggedExtractor.subcategory;
1710
+ _taggedUserId = "";
1711
+ constructor(opts) {
1712
+ super(opts);
1713
+ }
1714
+ static fromURL(url, opts) {
1715
+ const match = InstagramTaggedExtractor.pattern.exec(url);
1716
+ if (!match) return null;
1717
+ return new InstagramTaggedExtractor({
1718
+ ...opts,
1719
+ url,
1720
+ match
1721
+ });
1722
+ }
1723
+ async metadata() {
1724
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1725
+ let user;
1726
+ if (screenName.startsWith("id:")) {
1727
+ this._taggedUserId = screenName.slice(3);
1728
+ user = await this.api.userById(screenName.slice(3));
1729
+ } else {
1730
+ this._taggedUserId = await this.api.userId(screenName);
1731
+ user = await this.api.userByScreenName(screenName);
1732
+ }
1733
+ return {
1734
+ tagged_owner_id: user.id ?? user.pk,
1735
+ tagged_username: user.username,
1736
+ tagged_full_name: user.full_name
1737
+ };
1738
+ }
1739
+ async *posts() {
1740
+ if (!this._taggedUserId) await this.metadata();
1741
+ yield* this.api.userTagged(this._taggedUserId);
1742
+ }
1743
+ };
1744
+ register(InstagramTaggedExtractor.subcategory, InstagramTaggedExtractor);
1745
+ //#endregion
1746
+ //#region src/instagram/extractors/user.ts
1747
+ var InstagramUserExtractor = class InstagramUserExtractor extends InstagramExtractor {
1748
+ static subcategory = "user";
1749
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/?(?:$|[?#])/);
1750
+ subcategory = InstagramUserExtractor.subcategory;
1751
+ constructor(opts) {
1752
+ super(opts);
1753
+ }
1754
+ static fromURL(url, opts) {
1755
+ const match = InstagramUserExtractor.pattern.exec(url);
1756
+ if (!match) return null;
1757
+ return new InstagramUserExtractor({
1758
+ ...opts,
1759
+ url,
1760
+ match
1761
+ });
1762
+ }
1763
+ async *items() {
1764
+ await this.login();
1765
+ const userPath = this.groups[0] ?? "/";
1766
+ const base = `${this.root}${userPath}/`;
1767
+ const storiesUrl = `${this.root}/stories/${userPath.slice(1)}/`;
1768
+ const include = this._cfg("include", ["posts"]);
1769
+ const categories = include === "all" ? [
1770
+ "posts",
1771
+ "reels",
1772
+ "tagged",
1773
+ "stories",
1774
+ "highlights",
1775
+ "info",
1776
+ "avatar"
1777
+ ] : typeof include === "string" ? include.replace(/\s+/g, "").split(",") : include;
1778
+ const urls = {
1779
+ info: `${base}info/`,
1780
+ avatar: `${base}avatar/`,
1781
+ stories: storiesUrl,
1782
+ highlights: `${base}highlights/`,
1783
+ posts: `${base}posts/`,
1784
+ reels: `${base}reels/`,
1785
+ tagged: `${base}tagged/`
1786
+ };
1787
+ for (const cat of categories) {
1788
+ const cls = get(cat);
1789
+ const url = urls[cat];
1790
+ if (cls && url) yield queue(url, { _extractor: cls });
1791
+ else this.log.warn(`Invalid include '${cat}'`);
1792
+ }
1793
+ }
1794
+ async *posts() {}
1795
+ };
1796
+ register(InstagramUserExtractor.subcategory, InstagramUserExtractor);
1797
+ //#endregion
1798
+ //#region src/fetcher.ts
1799
+ /** Build URL with query params appended as URLSearchParams. */
1800
+ function buildUrl(base, params) {
1801
+ if (!params) return base;
1802
+ const cleaned = {};
1803
+ for (const [k, v] of Object.entries(params)) if (v != null) cleaned[k] = String(v);
1804
+ const entries = Object.entries(cleaned);
1805
+ if (entries.length === 0) return base;
1806
+ const qs = new URLSearchParams(entries).toString();
1807
+ return `${base}${base.includes("?") ? "&" : "?"}${qs}`;
1808
+ }
1809
+ /** Merge cookie strings with append semantics: a=1 + b=2 → a=1; b=2 */
1810
+ function mergeCookie(base, extra) {
1811
+ if (!base) return extra;
1812
+ return `${base}; ${extra}`;
1813
+ }
1814
+ /** Extract csrftoken value from a Cookie header string. */
1815
+ function extractCsrf(cookies) {
1816
+ return cookies.match(/(?:^|;\s*)csrftoken=([^;]+)/)?.[1] ?? "";
1817
+ }
1818
+ /** Convert fetch Headers to a plain Record. */
1819
+ function headersToRecord(headers) {
1820
+ const rec = {};
1821
+ headers.forEach((v, k) => {
1822
+ rec[k] = v;
1823
+ });
1824
+ return rec;
1825
+ }
1826
+ /** Read response body according to the requested type. */
1827
+ async function readBody(resp, responseType) {
1828
+ switch (responseType) {
1829
+ case "arraybuffer": {
1830
+ const buf = await resp.arrayBuffer();
1831
+ return Buffer.from(buf);
1832
+ }
1833
+ case "text": return resp.text();
1834
+ default: return resp.json();
1835
+ }
1836
+ }
1837
+ /** Serialize a request body value for fetch. */
1838
+ function serializeBody(data) {
1839
+ if (data == null) return void 0;
1840
+ if (typeof data === "string") return data;
1841
+ if (data instanceof URLSearchParams) return data;
1842
+ return JSON.stringify(data);
1843
+ }
1844
+ const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
1845
+ /**
1846
+ * Create a platform-agnostic HttpClient backed by native ``fetch``.
1847
+ *
1848
+ * Zero dependencies — works in Node.js 18+, browsers, Deno, and Edge.
1849
+ *
1850
+ * @example Plain (no cookies)
1851
+ * ```ts
1852
+ * const http = createFetchHttpClient()
1853
+ * ```
1854
+ *
1855
+ * @example With static cookies (CLI session mode)
1856
+ * ```ts
1857
+ * const http = createFetchHttpClient({ cookie: 'sessionid=abc; csrftoken=xyz' })
1858
+ * ```
1859
+ *
1860
+ * @example With cookie jar (anonymous session)
1861
+ * ```ts
1862
+ * const jar = createCookieJar()
1863
+ * const http = createFetchHttpClient({
1864
+ * cookieProvider: () => jar.getCookieHeader(),
1865
+ * onResponse: (headers) => jar.setFromResponse(headers),
1866
+ * })
1867
+ * ```
1868
+ */
1869
+ function createFetchHttpClient(opts = {}) {
1870
+ const { cookie, cookieProvider, userAgent = UA, timeout = 3e4, onResponse } = opts;
1871
+ return { async request(config) {
1872
+ const method = config.method ?? "GET";
1873
+ const url = buildUrl(config.url, config.params);
1874
+ const headers = new Headers(config.headers);
1875
+ const reqCookie = cookieProvider?.() ?? cookie;
1876
+ if (reqCookie) {
1877
+ const existing = headers.get("Cookie");
1878
+ headers.set("Cookie", existing ? mergeCookie(reqCookie, existing) : reqCookie);
1879
+ }
1880
+ if (!headers.has("User-Agent")) headers.set("User-Agent", userAgent);
1881
+ const body = serializeBody(config.data);
1882
+ if (typeof body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
1883
+ let controller = null;
1884
+ let timer = null;
1885
+ let signal = config.signal ?? null;
1886
+ const timeoutMs = config.timeout ?? timeout;
1887
+ if (!signal) {
1888
+ controller = new AbortController();
1889
+ timer = setTimeout(() => controller.abort(), timeoutMs);
1890
+ signal = controller.signal;
1891
+ }
1892
+ try {
1893
+ const resp = await fetch(url, {
1894
+ method,
1895
+ headers,
1896
+ body,
1897
+ signal
1898
+ });
1899
+ onResponse?.(headersToRecord(resp.headers));
1900
+ const data = await readBody(resp, config.responseType);
1901
+ return {
1902
+ status: resp.status,
1903
+ data,
1904
+ headers: headersToRecord(resp.headers),
1905
+ url: resp.url
1906
+ };
1907
+ } catch (err) {
1908
+ if (controller?.signal.aborted && !config.signal?.aborted) throw new Error(`Request timeout after ${timeoutMs}ms: ${url}`);
1909
+ if (String(err).includes("too many redirect")) throw new Error("Too many redirects — session may be expired or invalid. Export a fresh session from your browser.");
1910
+ throw err;
1911
+ } finally {
1912
+ if (timer) clearTimeout(timer);
1913
+ }
1914
+ } };
1915
+ }
1916
+ //#endregion
1917
+ //#region src/sdk.ts
1918
+ var InstagramSDK = class {
1919
+ http;
1920
+ storage;
1921
+ log;
1922
+ config;
1923
+ _csrfToken;
1924
+ constructor(opts = {}) {
1925
+ this.http = opts.http ?? createFetchHttpClient();
1926
+ this.storage = opts.storage ?? void 0;
1927
+ this.log = opts.log ?? noopLogger;
1928
+ this.config = new ConfigManager();
1929
+ this._csrfToken = opts.csrfToken ?? "";
1930
+ }
1931
+ /**
1932
+ * Extract messages from an Instagram URL without downloading.
1933
+ *
1934
+ * Returns an async generator yielding Directory / Url / Queue messages.
1935
+ * Each ``url`` message includes full metadata (post_id, username, dimensions, etc.).
1936
+ */
1937
+ async *extract(url) {
1938
+ const extractor = this._resolve(url);
1939
+ await extractor.initialize();
1940
+ yield* extractor;
1941
+ }
1942
+ /**
1943
+ * Download all media from an Instagram URL.
1944
+ *
1945
+ * Uses the built-in DownloadJob + Storage to save files to disk.
1946
+ * Requires ``storage`` to be set in constructor options.
1947
+ *
1948
+ * ```ts
1949
+ * const stats = await ig.download('https://www.instagram.com/p/.../', './my-downloads')
1950
+ * // → { posts: 1, files: 9, bytes: 4500000 }
1951
+ * ```
1952
+ */
1953
+ async download(url, outputDir = "./data") {
1954
+ const job = new DownloadJob(this._resolve(url));
1955
+ job.basePath = outputDir;
1956
+ await job.run();
1957
+ return {
1958
+ posts: job._postCount ?? 0,
1959
+ files: job._fileCount ?? 0,
1960
+ bytes: job._downloadedBytes ?? 0
1961
+ };
1962
+ }
1963
+ /** Resolve a URL to an Extractor instance via pattern matching. */
1964
+ _resolve(url) {
1965
+ for (const Cls of [
1966
+ InstagramPostExtractor,
1967
+ InstagramStoriesExtractor,
1968
+ InstagramHighlightsExtractor,
1969
+ InstagramTagExtractor,
1970
+ InstagramSavedExtractor,
1971
+ InstagramPostsExtractor,
1972
+ InstagramReelsExtractor,
1973
+ InstagramTaggedExtractor,
1974
+ InstagramInfoExtractor,
1975
+ InstagramAvatarExtractor,
1976
+ InstagramUserExtractor
1977
+ ]) {
1978
+ const match = Cls.pattern.exec(url);
1979
+ if (match) return Reflect.construct(Cls, [{
1980
+ url,
1981
+ match,
1982
+ config: this.config,
1983
+ http: this.http,
1984
+ storage: this.storage,
1985
+ log: this.log,
1986
+ csrfToken: this._csrfToken
1987
+ }]);
1988
+ }
1989
+ throw new Error(`No extractor matched URL: ${url}. Supported: /p/, /reel/, /{user}/, /stories/, /highlights/, /explore/tags/, /saved/`);
1990
+ }
1991
+ };
1992
+ //#endregion
1993
+ Object.defineProperty(exports, "ConfigManager", {
1994
+ enumerable: true,
1995
+ get: function() {
1996
+ return ConfigManager;
1997
+ }
1998
+ });
1999
+ Object.defineProperty(exports, "DownloadJob", {
2000
+ enumerable: true,
2001
+ get: function() {
2002
+ return DownloadJob;
2003
+ }
2004
+ });
2005
+ Object.defineProperty(exports, "Extractor", {
2006
+ enumerable: true,
2007
+ get: function() {
2008
+ return Extractor;
2009
+ }
2010
+ });
2011
+ Object.defineProperty(exports, "InstagramAvatarExtractor", {
2012
+ enumerable: true,
2013
+ get: function() {
2014
+ return InstagramAvatarExtractor;
2015
+ }
2016
+ });
2017
+ Object.defineProperty(exports, "InstagramExtractor", {
2018
+ enumerable: true,
2019
+ get: function() {
2020
+ return InstagramExtractor;
2021
+ }
2022
+ });
2023
+ Object.defineProperty(exports, "InstagramHighlightsExtractor", {
2024
+ enumerable: true,
2025
+ get: function() {
2026
+ return InstagramHighlightsExtractor;
2027
+ }
2028
+ });
2029
+ Object.defineProperty(exports, "InstagramInfoExtractor", {
2030
+ enumerable: true,
2031
+ get: function() {
2032
+ return InstagramInfoExtractor;
2033
+ }
2034
+ });
2035
+ Object.defineProperty(exports, "InstagramPostExtractor", {
2036
+ enumerable: true,
2037
+ get: function() {
2038
+ return InstagramPostExtractor;
2039
+ }
2040
+ });
2041
+ Object.defineProperty(exports, "InstagramPostsExtractor", {
2042
+ enumerable: true,
2043
+ get: function() {
2044
+ return InstagramPostsExtractor;
2045
+ }
2046
+ });
2047
+ Object.defineProperty(exports, "InstagramReelsExtractor", {
2048
+ enumerable: true,
2049
+ get: function() {
2050
+ return InstagramReelsExtractor;
2051
+ }
2052
+ });
2053
+ Object.defineProperty(exports, "InstagramRestAPI", {
2054
+ enumerable: true,
2055
+ get: function() {
2056
+ return InstagramRestAPI;
2057
+ }
2058
+ });
2059
+ Object.defineProperty(exports, "InstagramSDK", {
2060
+ enumerable: true,
2061
+ get: function() {
2062
+ return InstagramSDK;
2063
+ }
2064
+ });
2065
+ Object.defineProperty(exports, "InstagramSavedExtractor", {
2066
+ enumerable: true,
2067
+ get: function() {
2068
+ return InstagramSavedExtractor;
2069
+ }
2070
+ });
2071
+ Object.defineProperty(exports, "InstagramStoriesExtractor", {
2072
+ enumerable: true,
2073
+ get: function() {
2074
+ return InstagramStoriesExtractor;
2075
+ }
2076
+ });
2077
+ Object.defineProperty(exports, "InstagramTagExtractor", {
2078
+ enumerable: true,
2079
+ get: function() {
2080
+ return InstagramTagExtractor;
2081
+ }
2082
+ });
2083
+ Object.defineProperty(exports, "InstagramTaggedExtractor", {
2084
+ enumerable: true,
2085
+ get: function() {
2086
+ return InstagramTaggedExtractor;
2087
+ }
2088
+ });
2089
+ Object.defineProperty(exports, "InstagramUserExtractor", {
2090
+ enumerable: true,
2091
+ get: function() {
2092
+ return InstagramUserExtractor;
2093
+ }
2094
+ });
2095
+ Object.defineProperty(exports, "Job", {
2096
+ enumerable: true,
2097
+ get: function() {
2098
+ return Job;
2099
+ }
2100
+ });
2101
+ Object.defineProperty(exports, "_RESET", {
2102
+ enumerable: true,
2103
+ get: function() {
2104
+ return _RESET;
2105
+ }
2106
+ });
2107
+ Object.defineProperty(exports, "_YELLOW", {
2108
+ enumerable: true,
2109
+ get: function() {
2110
+ return _YELLOW;
2111
+ }
2112
+ });
2113
+ Object.defineProperty(exports, "b", {
2114
+ enumerable: true,
2115
+ get: function() {
2116
+ return b;
2117
+ }
2118
+ });
2119
+ Object.defineProperty(exports, "c", {
2120
+ enumerable: true,
2121
+ get: function() {
2122
+ return c;
2123
+ }
2124
+ });
2125
+ Object.defineProperty(exports, "createFetchHttpClient", {
2126
+ enumerable: true,
2127
+ get: function() {
2128
+ return createFetchHttpClient;
2129
+ }
2130
+ });
2131
+ Object.defineProperty(exports, "dim", {
2132
+ enumerable: true,
2133
+ get: function() {
2134
+ return dim;
2135
+ }
2136
+ });
2137
+ Object.defineProperty(exports, "directory", {
2138
+ enumerable: true,
2139
+ get: function() {
2140
+ return directory;
2141
+ }
2142
+ });
2143
+ Object.defineProperty(exports, "ensureHttpScheme", {
2144
+ enumerable: true,
2145
+ get: function() {
2146
+ return ensureHttpScheme;
2147
+ }
2148
+ });
2149
+ Object.defineProperty(exports, "extr", {
2150
+ enumerable: true,
2151
+ get: function() {
2152
+ return extr;
2153
+ }
2154
+ });
2155
+ Object.defineProperty(exports, "extract", {
2156
+ enumerable: true,
2157
+ get: function() {
2158
+ return extract;
2159
+ }
2160
+ });
2161
+ Object.defineProperty(exports, "extractAudio", {
2162
+ enumerable: true,
2163
+ get: function() {
2164
+ return extractAudio;
2165
+ }
2166
+ });
2167
+ Object.defineProperty(exports, "extractCsrf", {
2168
+ enumerable: true,
2169
+ get: function() {
2170
+ return extractCsrf;
2171
+ }
2172
+ });
2173
+ Object.defineProperty(exports, "extractTaggedUsers", {
2174
+ enumerable: true,
2175
+ get: function() {
2176
+ return extractTaggedUsers;
2177
+ }
2178
+ });
2179
+ Object.defineProperty(exports, "findTags", {
2180
+ enumerable: true,
2181
+ get: function() {
2182
+ return findTags;
2183
+ }
2184
+ });
2185
+ Object.defineProperty(exports, "g", {
2186
+ enumerable: true,
2187
+ get: function() {
2188
+ return g;
2189
+ }
2190
+ });
2191
+ Object.defineProperty(exports, "idFromShortcode", {
2192
+ enumerable: true,
2193
+ get: function() {
2194
+ return idFromShortcode;
2195
+ }
2196
+ });
2197
+ Object.defineProperty(exports, "nameExtFromURL", {
2198
+ enumerable: true,
2199
+ get: function() {
2200
+ return nameExtFromURL;
2201
+ }
2202
+ });
2203
+ Object.defineProperty(exports, "noopLogger", {
2204
+ enumerable: true,
2205
+ get: function() {
2206
+ return noopLogger;
2207
+ }
2208
+ });
2209
+ Object.defineProperty(exports, "pad", {
2210
+ enumerable: true,
2211
+ get: function() {
2212
+ return pad;
2213
+ }
2214
+ });
2215
+ Object.defineProperty(exports, "parseInt", {
2216
+ enumerable: true,
2217
+ get: function() {
2218
+ return parseInt;
2219
+ }
2220
+ });
2221
+ Object.defineProperty(exports, "parsePostGraphql", {
2222
+ enumerable: true,
2223
+ get: function() {
2224
+ return parsePostGraphql;
2225
+ }
2226
+ });
2227
+ Object.defineProperty(exports, "parsePostRest", {
2228
+ enumerable: true,
2229
+ get: function() {
2230
+ return parsePostRest;
2231
+ }
2232
+ });
2233
+ Object.defineProperty(exports, "parseUnicodeEscapes", {
2234
+ enumerable: true,
2235
+ get: function() {
2236
+ return parseUnicodeEscapes$1;
2237
+ }
2238
+ });
2239
+ Object.defineProperty(exports, "queue", {
2240
+ enumerable: true,
2241
+ get: function() {
2242
+ return queue;
2243
+ }
2244
+ });
2245
+ Object.defineProperty(exports, "shortcodeFromId", {
2246
+ enumerable: true,
2247
+ get: function() {
2248
+ return shortcodeFromId;
2249
+ }
2250
+ });
2251
+ Object.defineProperty(exports, "unescape", {
2252
+ enumerable: true,
2253
+ get: function() {
2254
+ return unescape;
2255
+ }
2256
+ });
2257
+ Object.defineProperty(exports, "unquote", {
2258
+ enumerable: true,
2259
+ get: function() {
2260
+ return unquote;
2261
+ }
2262
+ });
2263
+ Object.defineProperty(exports, "url", {
2264
+ enumerable: true,
2265
+ get: function() {
2266
+ return url;
2267
+ }
2268
+ });