@chilfish/gallery-dl-instagram 0.2.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.
@@ -52,129 +52,8 @@ var ConfigManager = class {
52
52
  }
53
53
  };
54
54
  //#endregion
55
- //#region src/core/extractor.ts
56
- /** A no-op logger */
57
- const noopLogger = {
58
- debug: () => {},
59
- info: () => {},
60
- warn: () => {},
61
- error: () => {}
62
- };
63
- var Extractor = class {
64
- /** Regex pattern to match against URLs */
65
- static pattern = /^$/;
66
- /** The input URL */
67
- url;
68
- /** Regex match groups from ``fromURL`` */
69
- groups;
70
- config;
71
- /** HTTP client — public so Job can access for downloads */
72
- http;
73
- /** Storage backend — public so Job can access for writes */
74
- storage;
75
- /** Logger instance — public so Job can access for reporting */
76
- log;
77
- /** Delay range in seconds — random between [min, max] before each request */
78
- requestInterval = [6, 12];
79
- _initialized = false;
80
- constructor(opts) {
81
- this.url = opts.url;
82
- this.groups = opts.match ? [...opts.match].slice(1) : [];
83
- this.config = opts.config;
84
- this.http = opts.http;
85
- this.storage = opts.storage;
86
- this.log = opts.log;
87
- }
88
- /** Initialization */
89
- /**
90
- * One-time async setup (cookies, session, internal state).
91
- * Safe to call multiple times — after the first call it becomes a no-op.
92
- */
93
- async initialize() {
94
- if (this._initialized) return;
95
- await this._init();
96
- this._initialized = true;
97
- this.initialize = async () => {};
98
- }
99
- /**
100
- * Subclass hook for one-time setup.
101
- */
102
- async _init() {}
103
- /** Async iteration */
104
- async *[Symbol.asyncIterator]() {
105
- await this.initialize();
106
- yield* this.items();
107
- }
108
- /** Config helpers */
109
- /**
110
- * Read a config value using the interpolated hierarchy.
111
- */
112
- _cfg(key, defaultVal) {
113
- const path = [
114
- "extractor",
115
- this.category,
116
- this.subcategory
117
- ];
118
- return this.config.interpolate(path, key, defaultVal);
119
- }
120
- /** HTTP */
121
- _lastRequestTime = 0;
122
- /**
123
- * Rate-limited HTTP request wrapper.
124
- */
125
- async request(url, cfg = {}) {
126
- await this._throttle();
127
- const response = await this.http.request({
128
- url,
129
- ...cfg
130
- });
131
- this._lastRequestTime = Date.now();
132
- return response;
133
- }
134
- /**
135
- * Convenience: request + parse JSON body.
136
- */
137
- async requestJSON(url, cfg = {}) {
138
- const resp = await this.request(url, cfg);
139
- if (typeof resp.data === "object") return resp.data;
140
- try {
141
- return JSON.parse(resp.data);
142
- } catch {
143
- return {};
144
- }
145
- }
146
- /** Rate limiting */
147
- /**
148
- * Sleep long enough to keep the minimum interval between requests.
149
- */
150
- async _throttle() {
151
- const elapsed = Date.now() - this._lastRequestTime;
152
- const [min, max] = this.requestInterval;
153
- const target = min + Math.random() * (max - min);
154
- const waitMs = Math.max(0, target * 1e3 - elapsed);
155
- if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs));
156
- }
157
- /** Utility */
158
- /**
159
- * Convert a Unix timestamp (seconds or ms) to an ISO-8601 string.
160
- */
161
- parseTimestamp(ts) {
162
- if (ts == null) return "";
163
- const asMs = ts > 25e8 ? ts : ts * 1e3;
164
- return new Date(asMs).toISOString();
165
- }
166
- /**
167
- * Generate a random hex token (used for CSRF).
168
- */
169
- static generateToken(size = 16) {
170
- const bytes = new Uint8Array(size);
171
- if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(bytes);
172
- else for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
173
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
174
- }
175
- };
176
- //#endregion
177
- //#region src/core/job.ts
55
+ //#region src/core/format.ts
56
+ /** Shared ANSI formatting and display utilities. */
178
57
  function formatBytes(bytes) {
179
58
  if (bytes === 0) return "0 B";
180
59
  const units = [
@@ -204,19 +83,20 @@ function c(s) {
204
83
  function g(s) {
205
84
  return `${GREEN}${s}${RESET}`;
206
85
  }
86
+ const _YELLOW = YELLOW;
87
+ const _RESET = RESET;
207
88
  function pad(s, n) {
208
89
  return s.length >= n ? s : s + " ".repeat(n - s.length);
209
90
  }
91
+ //#endregion
92
+ //#region src/core/job.ts
210
93
  var Job = class {
211
94
  extractor;
212
95
  status = 0;
213
96
  constructor(extractor) {
214
97
  this.extractor = extractor;
215
98
  }
216
- /**
217
- * Main entry point. Calls ``extractor[Symbol.asyncIterator]()`` and
218
- * dispatches every yielded message.
219
- */
99
+ /** Main entry point. Dispatches every yielded message. */
220
100
  async run() {
221
101
  this.extractor.log.info(`Starting ${this.extractor.category}/${this.extractor.subcategory} — ${this.extractor.url}`);
222
102
  await this.extractor.initialize();
@@ -237,6 +117,8 @@ var Job = class {
237
117
  /** Override in subclasses to print a summary. */
238
118
  _report() {}
239
119
  };
120
+ //#endregion
121
+ //#region src/core/download-job.ts
240
122
  var DownloadJob = class DownloadJob extends Job {
241
123
  /** Base output directory (prepended to all paths). */
242
124
  basePath = "";
@@ -244,10 +126,6 @@ var DownloadJob = class DownloadJob extends Job {
244
126
  _currentDir = {};
245
127
  /** In-memory archive keyed by archive format. */
246
128
  archive = /* @__PURE__ */ new Map();
247
- /**
248
- * Registry of per-category "archive formats" — the key is formed
249
- * by interpolating this format string over the metadata.
250
- */
251
129
  _archiveFmts = /* @__PURE__ */ new Map();
252
130
  _postCount = 0;
253
131
  _fileCount = 0;
@@ -256,23 +134,18 @@ var DownloadJob = class DownloadJob extends Job {
256
134
  registerArchive(category, format) {
257
135
  this._archiveFmts.set(category, format);
258
136
  }
259
- /** Simple format-string interpolation for archive keys. */
260
137
  _interp(fmt, meta) {
261
138
  return fmt.replace(/\{(\w+)\}/g, (_, key) => {
262
139
  const v = meta[key];
263
140
  return v == null ? "" : String(v);
264
141
  });
265
142
  }
266
- /** Check whether this URL has already been downloaded (and skip). */
267
143
  _isArchived(meta) {
268
144
  const cat = meta.category ?? this.extractor.category;
269
145
  const fmt = this._archiveFmts.get(cat) ?? "{media_id}";
270
146
  const key = this._interp(fmt, meta);
271
- const set = this.archive.get(cat);
272
- if (set && set.has(key)) return true;
273
- return false;
147
+ return !!this.archive.get(cat)?.has(key);
274
148
  }
275
- /** Mark a post/media as archived. */
276
149
  _archive(meta) {
277
150
  const cat = meta.category ?? this.extractor.category;
278
151
  const fmt = this._archiveFmts.get(cat) ?? "{media_id}";
@@ -284,7 +157,6 @@ var DownloadJob = class DownloadJob extends Job {
284
157
  }
285
158
  set.add(key);
286
159
  }
287
- /** Handlers */
288
160
  async handleDirectory(msg) {
289
161
  this._currentDir = { ...msg.metadata };
290
162
  this._postCount++;
@@ -313,7 +185,6 @@ var DownloadJob = class DownloadJob extends Job {
313
185
  if (resp.data instanceof Uint8Array) data = resp.data;
314
186
  else if (resp.data instanceof ArrayBuffer) data = new Uint8Array(resp.data);
315
187
  else if (typeof resp.data === "string") data = resp.data;
316
- else if (typeof resp.data === "object" && resp.data != null && "type" in resp.data && resp.data.type === "Buffer") data = new Uint8Array(resp.data);
317
188
  else data = JSON.stringify(resp.data);
318
189
  await this.extractor.storage.write(fullPath, data);
319
190
  this._fileCount++;
@@ -357,13 +228,11 @@ var DownloadJob = class DownloadJob extends Job {
357
228
  else this.archive.set(cat, set);
358
229
  }
359
230
  }
360
- /** Report */
361
231
  _report() {
362
232
  const log = this.extractor.log;
363
233
  log.info(`Done — ${this._postCount} post(s), ${this._fileCount} file(s) downloaded (${formatBytes(this._downloadedBytes)})`);
364
234
  if (this._skippedCount > 0) log.info(` ${this._skippedCount} file(s) skipped (already archived)`);
365
235
  }
366
- /** Path builders */
367
236
  _buildDirPath(meta) {
368
237
  return `${meta.category ?? this.extractor.category}/${meta.username ?? "_"}`;
369
238
  }
@@ -373,184 +242,129 @@ var DownloadJob = class DownloadJob extends Job {
373
242
  return `${mid}${meta.num ? `_${meta.num}` : ""}.${ext}`;
374
243
  }
375
244
  };
376
- var PrintJob = class PrintJob extends Job {
377
- _currentDir = {};
378
- _files = [];
379
- _postCount = 0;
380
- _fileCount = 0;
381
- _width;
382
- constructor(extractor) {
383
- super(extractor);
384
- this._width = Math.min(process.stdout.columns ?? 80, 100);
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;
385
278
  }
386
- async handleDirectory(msg) {
387
- if (this._postCount > 0) this._flushPost();
388
- this._currentDir = { ...msg.metadata };
389
- this._postCount++;
390
- this._files = [];
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 () => {};
391
289
  }
392
- async handleUrl(msg) {
393
- const meta = {
394
- ...this._currentDir,
395
- ...msg.metadata
396
- };
397
- this._fileCount++;
398
- const ext = meta.extension ?? "jpg";
399
- const mid = meta.media_id ?? "?";
400
- this._files.push({
401
- num: meta.num ?? this._files.length + 1,
402
- filename: `${mid}.${ext}`,
403
- width: meta.width ?? 0,
404
- height: meta.height ?? 0,
405
- videoUrl: meta.video_url ?? null,
406
- audioUrl: meta.audio_url ?? null
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
407
321
  });
322
+ this._lastRequestTime = Date.now();
323
+ return response;
408
324
  }
409
- async handleQueue(msg) {
410
- if (this._files.length > 0 || this._postCount > 0) this._flushPost();
411
- this._postCount = 0;
412
- this._files = [];
413
- const extrClass = {
414
- ...this._currentDir,
415
- ...msg.metadata
416
- }._extractor;
417
- if (!extrClass || typeof extrClass !== "object") return;
418
- const cls = extrClass;
419
- const match = cls.pattern.exec(msg.url);
420
- if (!match) return;
421
- const parentExtr = this.extractor;
422
- const childJob = new PrintJob(Reflect.construct(cls, [{
423
- url: msg.url,
424
- match,
425
- config: parentExtr.config,
426
- http: parentExtr.http,
427
- storage: parentExtr.storage,
428
- log: parentExtr.log
429
- }]));
430
- const childStatus = await childJob.run();
431
- this.status |= childStatus;
432
- this._postCount += childJob._postCount;
433
- this._fileCount += childJob._fileCount;
434
- }
435
- /** Output */
436
- _flushPost() {
437
- const m = this._currentDir;
438
- if (Object.keys(m).length === 0) return;
439
- const w = this._width;
440
- const labelW = 14;
441
- const shortcode = m.post_shortcode ?? "?";
442
- const header = ` Post #${this._postCount}: ${shortcode} `;
443
- const padTotal = w - 2 - header.length;
444
- const padL = Math.floor(padTotal / 2);
445
- const padR = padTotal - padL;
446
- process.stdout.write(`\n${dim("┌")}${"─".repeat(padL)}${b(header)}${"─".repeat(padR)}${dim("┐")}\n`);
447
- const row = (label, value, color) => {
448
- const colored = typeof color === "function" ? color(value) : color ? `${color}${value}${RESET}` : value;
449
- process.stdout.write(` ${dim("│")} ${c(pad(label, labelW))} ${colored}\n`);
450
- };
451
- const username = m.username ?? "?";
452
- const fullname = m.fullname ?? "";
453
- row("Author:", fullname ? `${username} (${fullname})` : username, g);
454
- row("Date:", m.date ?? m.post_date ?? "?");
455
- row("Likes:", `${typeof m.likes === "number" ? m.likes.toLocaleString() : "?"} | Liked: ${m.liked ? "yes" : "no"}`);
456
- row("Type:", `${m.type ?? "?"} (${this._files.length} files)`);
457
- row("URL:", m.post_url ?? "?");
458
- const desc = m.description ?? "";
459
- if (desc) {
460
- process.stdout.write(` ${dim("│")}\n`);
461
- process.stdout.write(` ${dim("│")} ${b("Description:")}\n`);
462
- const lines = desc.split("\n");
463
- for (const line of lines) {
464
- const wrapped = this._wrap(line, w - 8);
465
- for (const wl of wrapped) process.stdout.write(` ${dim("│")} ${dim(wl)}\n`);
466
- }
467
- }
468
- const tags = m.tags;
469
- if (tags && tags.length > 0) {
470
- process.stdout.write(` ${dim("│")}\n`);
471
- process.stdout.write(` ${dim("│")} ${b("Tags:")} ${dim(tags.map((t) => `#${t}`).join(" "))}\n`);
472
- }
473
- const locName = m.location_slug ?? "";
474
- const locId = m.location_id ?? "";
475
- if (locName || locId) row("Location:", locId ? `${locName} (ID: ${locId})` : locName);
476
- const coauthors = m.coauthors;
477
- if (coauthors && coauthors.length > 0) row("Co-authors:", coauthors.map((c) => c.full_name ? `${c.username} (${c.full_name})` : c.username).join(", "));
478
- const pinned = m.pinned;
479
- if (pinned && pinned.length > 0) row("Pinned:", pinned.join(", "));
480
- const expires = m.expires;
481
- if (expires) row("Expires:", expires, YELLOW);
482
- const hlTitle = m.highlight_title;
483
- if (hlTitle) row("Highlight:", hlTitle);
484
- const taggedUser = m.tagged_username ?? "";
485
- if (taggedUser) {
486
- const taggedFull = m.tagged_full_name ?? "";
487
- row("Tagged by:", taggedFull ? `${taggedUser} (${taggedFull})` : taggedUser);
488
- }
489
- if (this._files.length > 0) {
490
- process.stdout.write(` ${dim("│")}\n`);
491
- process.stdout.write(` ${dim("│")} ${b(`Media (${this._files.length} files):`)}\n`);
492
- const maxNumW = String(this._files.length).length;
493
- const maxFileW = Math.max(...this._files.map((f) => f.filename.length));
494
- const dimW = Math.min(maxFileW, 40);
495
- for (const f of this._files) {
496
- const numStr = `[${String(f.num).padStart(maxNumW)}]`;
497
- const dimStr = f.filename.length > 40 ? `${f.filename.slice(0, 37)}...` : pad(f.filename, dimW);
498
- const res = f.width ? `${f.width}x${f.height}` : "?x?";
499
- const badges = [];
500
- if (f.videoUrl) badges.push("video");
501
- if (f.audioUrl) badges.push("audio");
502
- let line = ` ${dim("│")} ${g(numStr)} ${dimStr} ${res}`;
503
- if (badges.length > 0) line += ` ${YELLOW}(${badges.join("+")})${RESET}`;
504
- process.stdout.write(`${line}\n`);
505
- }
506
- }
507
- process.stdout.write(` ${dim("└")}${"─".repeat(w - 2)}${dim("┘")}\n`);
508
- }
509
- _wrap(text, maxLen) {
510
- if (text.length <= maxLen) return [text];
511
- const lines = [];
512
- let remaining = text;
513
- while (remaining.length > maxLen) {
514
- let cut = maxLen;
515
- while (cut > 0 && remaining[cut] !== " ") cut--;
516
- if (cut === 0) cut = maxLen;
517
- lines.push(remaining.slice(0, cut).trimEnd());
518
- remaining = remaining.slice(cut).trimStart();
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 {};
519
335
  }
520
- if (remaining) lines.push(remaining);
521
- return lines;
522
336
  }
523
- _report() {
524
- this._flushPost();
525
- process.stdout.write(`\n${dim("──")} ${b("Summary")} ${dim("───")}\n`);
526
- process.stdout.write(` Posts: ${g(String(this._postCount))}\n`);
527
- process.stdout.write(` Files: ${g(String(this._fileCount))}\n`);
528
- process.stdout.write(`\n`);
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("");
529
365
  }
530
366
  };
531
367
  //#endregion
532
- //#region src/message.ts
533
- function directory(metadata = {}) {
534
- return {
535
- type: "directory",
536
- metadata
537
- };
538
- }
539
- function url(u, metadata = {}) {
540
- return {
541
- type: "url",
542
- url: u,
543
- metadata
544
- };
545
- }
546
- function queue(u, metadata = {}) {
547
- return {
548
- type: "queue",
549
- url: u,
550
- metadata
551
- };
552
- }
553
- //#endregion
554
368
  //#region src/utils/id-codec.ts
555
369
  /**
556
370
  * Instagram-style Base64-variant ID ↔ shortcode conversion.
@@ -582,6 +396,28 @@ function shortcodeFromId(postId) {
582
396
  return chars.reverse().join("");
583
397
  }
584
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
585
421
  //#region src/utils/text.ts
586
422
  /**
587
423
  * Text utilities ported from gallery-dl's ``text`` module.
@@ -1053,8 +889,8 @@ var InstagramRestAPI = class {
1053
889
  }
1054
890
  };
1055
891
  //#endregion
1056
- //#region src/instagram/parsers.ts
1057
- /** Main entry — REST */
892
+ //#region src/instagram/parsers/rest.ts
893
+ /** Main entry — parse a REST post response. */
1058
894
  function parsePostRest(post, cfg) {
1059
895
  if (post.items) return parseStoryRest(post, cfg);
1060
896
  const owner = post.user;
@@ -1082,10 +918,9 @@ function parsePostRest(post, cfg) {
1082
918
  if (tags.length > 0) data.tags = [...new Set(tags)].sort();
1083
919
  if (post.location) {
1084
920
  const loc = post.location;
1085
- const slug = loc.short_name.replace(/\s+/g, "-").toLowerCase();
1086
921
  data.location_id = loc.pk;
1087
- data.location_slug = slug;
1088
- data.location_url = `${cfg.root}/explore/locations/${loc.pk}/${slug}/`;
922
+ data.location_slug = loc.short_name.replace(/\s+/g, "-").toLowerCase();
923
+ data.location_url = `${cfg.root}/explore/locations/${loc.pk}/${data.location_slug}/`;
1089
924
  }
1090
925
  if (post.coauthor_producers) data.coauthors = post.coauthor_producers.map((u) => ({
1091
926
  id: u.pk,
@@ -1132,7 +967,7 @@ function parsePostRest(post, cfg) {
1132
967
  if (post.subscription_media_visibility) data.subscription = post.subscription_media_visibility;
1133
968
  return data;
1134
969
  }
1135
- /** Story / highlight */
970
+ /** Parse a story or highlight REST response. */
1136
971
  function parseStoryRest(post, cfg) {
1137
972
  const items = post.items;
1138
973
  const reelId = String(post.id).split(":").pop() ?? "0";
@@ -1158,9 +993,8 @@ function parseStoryRest(post, cfg) {
1158
993
  expires: expires ? cfg.parseTimestamp(expires) : void 0,
1159
994
  user: post.user
1160
995
  };
1161
- if (!isStory) {
1162
- if (post.title) data.highlight_title = post.title;
1163
- } else if (!post.seen) post.seen = expires - 86400;
996
+ if (!isStory && post.title) data.highlight_title = post.title;
997
+ else if (!post.seen) post.seen = expires - 86400;
1164
998
  for (let num = 0; num < items.length; num++) {
1165
999
  const item = items[num];
1166
1000
  const media = parseMediaItem(item, post, cfg, num + 1);
@@ -1170,7 +1004,7 @@ function parseStoryRest(post, cfg) {
1170
1004
  }
1171
1005
  return data;
1172
1006
  }
1173
- /** Single media item */
1007
+ /** Parse a single media item (image/video) from a carousel or story. */
1174
1008
  function parseMediaItem(item, parent, cfg, num) {
1175
1009
  let image;
1176
1010
  try {
@@ -1229,7 +1063,7 @@ function parseMediaItem(item, parent, cfg, num) {
1229
1063
  if (itemRec.audience) media.audience = itemRec.audience;
1230
1064
  return media;
1231
1065
  }
1232
- /** Tagged users */
1066
+ /** Extract tagged users from various field formats. */
1233
1067
  function extractTaggedUsers(src, dest) {
1234
1068
  dest.tagged_users = [];
1235
1069
  const edges = src.edge_media_to_tagged_user;
@@ -1272,13 +1106,9 @@ function extractTaggedUsers(src, dest) {
1272
1106
  }
1273
1107
  }
1274
1108
  const seen = /* @__PURE__ */ new Set();
1275
- dest.tagged_users = dest.tagged_users.filter((t) => {
1276
- if (seen.has(t.id)) return false;
1277
- seen.add(t.id);
1278
- return true;
1279
- });
1109
+ dest.tagged_users = dest.tagged_users.filter((t) => seen.has(t.id) ? false : (seen.add(t.id), true));
1280
1110
  }
1281
- /** Audio / music extraction */
1111
+ /** Extract audio/music metadata from a story sticker. */
1282
1112
  function extractAudio(src, dest, sticker, cfg) {
1283
1113
  const info = sticker.music_asset_info;
1284
1114
  if (!info) return null;
@@ -1310,7 +1140,14 @@ function extractAudio(src, dest, sticker, cfg) {
1310
1140
  audio_timestamps: info.highlight_start_times_in_ms
1311
1141
  };
1312
1142
  }
1313
- /** GraphQL parser */
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. */
1314
1151
  function parsePostGraphql(post, cfg) {
1315
1152
  const typename = post.__typename ?? "GraphImage";
1316
1153
  const owner = post.owner;
@@ -1395,11 +1232,6 @@ function parsePostGraphql(post, cfg) {
1395
1232
  }
1396
1233
  return data;
1397
1234
  }
1398
- function extractPinned(post) {
1399
- if (post.timeline_pinned_user_ids) return post.timeline_pinned_user_ids;
1400
- if (post.clips_tab_pinned_user_ids) return post.clips_tab_pinned_user_ids;
1401
- return [];
1402
- }
1403
1235
  function parseUnicodeEscapes(text) {
1404
1236
  if (!text.includes("\\u")) return text;
1405
1237
  return text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
@@ -1558,115 +1390,160 @@ var InstagramExtractor = class extends Extractor {
1558
1390
  }
1559
1391
  };
1560
1392
  //#endregion
1561
- //#region src/instagram/extractors.ts
1393
+ //#region src/instagram/extractors/helpers.ts
1394
+ /** Shared regex utilities for Instagram extractor URL patterns. */
1562
1395
  const BASE_RE = /^(?:https?:\/\/)?(?:www\.)?instagram\.com/;
1563
1396
  function re(base, path) {
1564
1397
  const pathSrc = typeof path === "string" ? path : path.source;
1565
1398
  return new RegExp(base.source + pathSrc, "i");
1566
1399
  }
1567
- var InstagramPostExtractor = class InstagramPostExtractor extends InstagramExtractor {
1568
- static subcategory = "post";
1569
- static pattern = re(/^(?:https?:\/\/)?(?:www\.)?instagram\.com\//, /(?:share(?:\/(?:p|tv|reels?))?|(?:[^/?#]+\/)?(?:p|tv|reels?))\/([^/?#]+)/);
1570
- subcategory = InstagramPostExtractor.subcategory;
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;
1571
1415
  constructor(opts) {
1572
1416
  super(opts);
1573
- if (opts.match[2] != null || opts.match[3] != null) this.subcategory = "reel";
1574
1417
  }
1575
1418
  static fromURL(url, opts) {
1576
- const match = InstagramPostExtractor.pattern.exec(url);
1419
+ const match = InstagramAvatarExtractor.pattern.exec(url);
1577
1420
  if (!match) return null;
1578
- return new InstagramPostExtractor({
1421
+ return new InstagramAvatarExtractor({
1579
1422
  ...opts,
1580
1423
  url,
1581
1424
  match
1582
1425
  });
1583
1426
  }
1584
1427
  async *posts() {
1585
- const groups = this.groups;
1586
- let shortcode = groups[0];
1587
- if (!shortcode) return;
1588
- if (groups[1] === "") {
1589
- this.log.info(`Resolving share link: ${this.url}`);
1590
- const parts = (await this.request(ensureHttpScheme(this.url), { headers: {
1591
- "Sec-Fetch-Dest": "empty",
1592
- "Sec-Fetch-Mode": "navigate",
1593
- "Sec-Fetch-Site": "same-origin"
1594
- } })).url?.split("/");
1595
- shortcode = parts?.[parts.length - 2] ?? shortcode;
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;
1596
1443
  }
1597
- this.log.debug(`Fetching post: ${shortcode}`);
1598
- yield* this.api.media(shortcode);
1444
+ yield {
1445
+ pk,
1446
+ code,
1447
+ user,
1448
+ caption: null,
1449
+ like_count: 0,
1450
+ image_versions2: { candidates: [avatar] }
1451
+ };
1599
1452
  }
1600
1453
  };
1601
- var InstagramUserExtractor = class InstagramUserExtractor extends InstagramExtractor {
1602
- static subcategory = "user";
1603
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/?(?:$|[?#])/);
1604
- subcategory = InstagramUserExtractor.subcategory;
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;
1605
1461
  constructor(opts) {
1606
1462
  super(opts);
1607
1463
  }
1608
1464
  static fromURL(url, opts) {
1609
- const match = InstagramUserExtractor.pattern.exec(url);
1465
+ const match = InstagramHighlightsExtractor.pattern.exec(url);
1610
1466
  if (!match) return null;
1611
- return new InstagramUserExtractor({
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({
1612
1493
  ...opts,
1613
1494
  url,
1614
1495
  match
1615
1496
  });
1616
1497
  }
1617
1498
  async *items() {
1618
- await this.login();
1619
- const userPath = this.groups[0] ?? "/";
1620
- const base = `${this.root}${userPath}/`;
1621
- const storiesUrl = `${this.root}/stories/${userPath.slice(1)}/`;
1622
- const include = this._cfg("include", ["posts"]);
1623
- const categories = include === "all" ? [
1624
- "posts",
1625
- "reels",
1626
- "tagged",
1627
- "stories",
1628
- "highlights",
1629
- "info",
1630
- "avatar"
1631
- ] : typeof include === "string" ? include.replace(/\s+/g, "").split(",") : include;
1632
- const extractors = {
1633
- info: {
1634
- cls: InstagramInfoExtractor,
1635
- url: `${base}info/`
1636
- },
1637
- avatar: {
1638
- cls: InstagramAvatarExtractor,
1639
- url: `${base}avatar/`
1640
- },
1641
- stories: {
1642
- cls: InstagramStoriesExtractor,
1643
- url: storiesUrl
1644
- },
1645
- highlights: {
1646
- cls: InstagramHighlightsExtractor,
1647
- url: `${base}highlights/`
1648
- },
1649
- posts: {
1650
- cls: InstagramPostsExtractor,
1651
- url: `${base}posts/`
1652
- },
1653
- reels: {
1654
- cls: InstagramReelsExtractor,
1655
- url: `${base}reels/`
1656
- },
1657
- tagged: {
1658
- cls: InstagramTaggedExtractor,
1659
- url: `${base}tagged/`
1660
- }
1661
- };
1662
- for (const cat of categories) {
1663
- const entry = extractors[cat];
1664
- if (entry) yield queue(entry.url, { _extractor: entry.cls });
1665
- else this.log.warn(`Invalid include '${cat}'`);
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;
1666
1539
  }
1540
+ this.log.debug(`Fetching post: ${shortcode}`);
1541
+ yield* this.api.media(shortcode);
1667
1542
  }
1668
- async *posts() {}
1669
1543
  };
1544
+ register(InstagramPostExtractor.subcategory, InstagramPostExtractor);
1545
+ //#endregion
1546
+ //#region src/instagram/extractors/posts-list.ts
1670
1547
  var InstagramPostsExtractor = class InstagramPostsExtractor extends InstagramExtractor {
1671
1548
  static subcategory = "posts";
1672
1549
  static pattern = re(BASE_RE, /(\/[^/?#]+)\/posts/);
@@ -1689,6 +1566,9 @@ var InstagramPostsExtractor = class InstagramPostsExtractor extends InstagramExt
1689
1566
  yield* this.api.userFeed(uid);
1690
1567
  }
1691
1568
  };
1569
+ register(InstagramPostsExtractor.subcategory, InstagramPostsExtractor);
1570
+ //#endregion
1571
+ //#region src/instagram/extractors/reels-list.ts
1692
1572
  var InstagramReelsExtractor = class InstagramReelsExtractor extends InstagramExtractor {
1693
1573
  static subcategory = "reels";
1694
1574
  static pattern = re(BASE_RE, /(\/[^/?#]+)\/reels/);
@@ -1711,44 +1591,32 @@ var InstagramReelsExtractor = class InstagramReelsExtractor extends InstagramExt
1711
1591
  yield* this.api.userClips(uid);
1712
1592
  }
1713
1593
  };
1714
- var InstagramTaggedExtractor = class InstagramTaggedExtractor extends InstagramExtractor {
1715
- static subcategory = "tagged";
1716
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/tagged/);
1717
- subcategory = InstagramTaggedExtractor.subcategory;
1718
- _taggedUserId = "";
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;
1719
1601
  constructor(opts) {
1720
1602
  super(opts);
1721
1603
  }
1722
1604
  static fromURL(url, opts) {
1723
- const match = InstagramTaggedExtractor.pattern.exec(url);
1605
+ const match = InstagramSavedExtractor.pattern.exec(url);
1724
1606
  if (!match) return null;
1725
- return new InstagramTaggedExtractor({
1607
+ return new InstagramSavedExtractor({
1726
1608
  ...opts,
1727
1609
  url,
1728
1610
  match
1729
1611
  });
1730
1612
  }
1731
- async metadata() {
1732
- const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1733
- let user;
1734
- if (screenName.startsWith("id:")) {
1735
- this._taggedUserId = screenName.slice(3);
1736
- user = await this.api.userById(screenName.slice(3));
1737
- } else {
1738
- this._taggedUserId = await this.api.userId(screenName);
1739
- user = await this.api.userByScreenName(screenName);
1740
- }
1741
- return {
1742
- tagged_owner_id: user.id ?? user.pk,
1743
- tagged_username: user.username,
1744
- tagged_full_name: user.full_name
1745
- };
1746
- }
1747
1613
  async *posts() {
1748
- if (!this._taggedUserId) await this.metadata();
1749
- yield* this.api.userTagged(this._taggedUserId);
1614
+ yield* this.api.userSaved();
1750
1615
  }
1751
1616
  };
1617
+ register(InstagramSavedExtractor.subcategory, InstagramSavedExtractor);
1618
+ //#endregion
1619
+ //#region src/instagram/extractors/stories.ts
1752
1620
  var InstagramStoriesExtractor = class InstagramStoriesExtractor extends InstagramExtractor {
1753
1621
  static subcategory = "stories";
1754
1622
  static pattern = /^(?:https?:\/\/)?(?:www\.)?instagram\.com\/(?:stories\/(?:highlights\/(\d+)|([^/?#]+)(?:\/(\d+))?)|\/(aGlnaGxpZ2h0[^?#]+)(?:\?story_media_id=(\d+))?)/;
@@ -1804,28 +1672,9 @@ var InstagramStoriesExtractor = class InstagramStoriesExtractor extends Instagra
1804
1672
  } else yield* reels;
1805
1673
  }
1806
1674
  };
1807
- var InstagramHighlightsExtractor = class InstagramHighlightsExtractor extends InstagramExtractor {
1808
- static subcategory = "highlights";
1809
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/highlights/);
1810
- subcategory = InstagramHighlightsExtractor.subcategory;
1811
- constructor(opts) {
1812
- super(opts);
1813
- }
1814
- static fromURL(url, opts) {
1815
- const match = InstagramHighlightsExtractor.pattern.exec(url);
1816
- if (!match) return null;
1817
- return new InstagramHighlightsExtractor({
1818
- ...opts,
1819
- url,
1820
- match
1821
- });
1822
- }
1823
- async *posts() {
1824
- const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1825
- const uid = await this.api.userId(screenName);
1826
- yield* this.api.highlightsMedia(uid);
1827
- }
1828
- };
1675
+ register(InstagramStoriesExtractor.subcategory, InstagramStoriesExtractor);
1676
+ //#endregion
1677
+ //#region src/instagram/extractors/tag.ts
1829
1678
  var InstagramTagExtractor = class InstagramTagExtractor extends InstagramExtractor {
1830
1679
  static subcategory = "tag";
1831
1680
  static pattern = re(BASE_RE, /\/explore\/tags\/([^/?#]+)/);
@@ -1851,94 +1700,219 @@ var InstagramTagExtractor = class InstagramTagExtractor extends InstagramExtract
1851
1700
  yield* this.api.tagsMedia(decodeURIComponent(tag));
1852
1701
  }
1853
1702
  };
1854
- var InstagramInfoExtractor = class InstagramInfoExtractor extends InstagramExtractor {
1855
- static subcategory = "info";
1856
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/info/);
1857
- subcategory = InstagramInfoExtractor.subcategory;
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 = "";
1858
1711
  constructor(opts) {
1859
1712
  super(opts);
1860
1713
  }
1861
1714
  static fromURL(url, opts) {
1862
- const match = InstagramInfoExtractor.pattern.exec(url);
1715
+ const match = InstagramTaggedExtractor.pattern.exec(url);
1863
1716
  if (!match) return null;
1864
- return new InstagramInfoExtractor({
1717
+ return new InstagramTaggedExtractor({
1865
1718
  ...opts,
1866
1719
  url,
1867
1720
  match
1868
1721
  });
1869
1722
  }
1870
- async *items() {
1723
+ async metadata() {
1871
1724
  const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1872
1725
  let user;
1873
- if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1874
- else user = await this.api.userByScreenName(screenName);
1875
- yield directory(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);
1876
1742
  }
1877
- async *posts() {}
1878
1743
  };
1879
- var InstagramAvatarExtractor = class InstagramAvatarExtractor extends InstagramExtractor {
1880
- static subcategory = "avatar";
1881
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/avatar/);
1882
- subcategory = InstagramAvatarExtractor.subcategory;
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;
1883
1751
  constructor(opts) {
1884
1752
  super(opts);
1885
1753
  }
1886
1754
  static fromURL(url, opts) {
1887
- const match = InstagramAvatarExtractor.pattern.exec(url);
1755
+ const match = InstagramUserExtractor.pattern.exec(url);
1888
1756
  if (!match) return null;
1889
- return new InstagramAvatarExtractor({
1757
+ return new InstagramUserExtractor({
1890
1758
  ...opts,
1891
1759
  url,
1892
1760
  match
1893
1761
  });
1894
1762
  }
1895
- async *posts() {
1896
- const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1897
- let user;
1898
- if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1899
- else user = await this.api.userByScreenName(screenName);
1900
- const avatar = user.hd_profile_pic_url_info ?? user.hd_profile_pic_versions?.[user.hd_profile_pic_versions.length - 1] ?? {
1901
- url: user.profile_pic_url ?? "",
1902
- width: 0,
1903
- height: 0
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/`
1904
1786
  };
1905
- let pk = user.profile_pic_id?.split("_")[0];
1906
- let code;
1907
- if (pk) code = shortcodeFromId(pk);
1908
- else {
1909
- pk = `avatar:${user.pk}`;
1910
- code = pk;
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}'`);
1911
1792
  }
1912
- yield {
1913
- pk,
1914
- code,
1915
- user,
1916
- caption: null,
1917
- like_count: 0,
1918
- image_versions2: { candidates: [avatar] }
1919
- };
1920
1793
  }
1794
+ async *posts() {}
1921
1795
  };
1922
- var InstagramSavedExtractor = class InstagramSavedExtractor extends InstagramExtractor {
1923
- static subcategory = "saved";
1924
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/saved(?:\/all-posts)?\/?$/);
1925
- subcategory = InstagramSavedExtractor.subcategory;
1926
- constructor(opts) {
1927
- super(opts);
1928
- }
1929
- static fromURL(url, opts) {
1930
- const match = InstagramSavedExtractor.pattern.exec(url);
1931
- if (!match) return null;
1932
- return new InstagramSavedExtractor({
1933
- ...opts,
1934
- url,
1935
- match
1936
- });
1937
- }
1938
- async *posts() {
1939
- yield* this.api.userSaved();
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();
1940
1835
  }
1941
- };
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
+ }
1942
1916
  //#endregion
1943
1917
  //#region src/sdk.ts
1944
1918
  var InstagramSDK = class {
@@ -1947,8 +1921,8 @@ var InstagramSDK = class {
1947
1921
  log;
1948
1922
  config;
1949
1923
  _csrfToken;
1950
- constructor(opts) {
1951
- this.http = opts.http;
1924
+ constructor(opts = {}) {
1925
+ this.http = opts.http ?? createFetchHttpClient();
1952
1926
  this.storage = opts.storage ?? void 0;
1953
1927
  this.log = opts.log ?? noopLogger;
1954
1928
  this.config = new ConfigManager();
@@ -2124,10 +2098,40 @@ Object.defineProperty(exports, "Job", {
2124
2098
  return Job;
2125
2099
  }
2126
2100
  });
2127
- Object.defineProperty(exports, "PrintJob", {
2101
+ Object.defineProperty(exports, "_RESET", {
2128
2102
  enumerable: true,
2129
2103
  get: function() {
2130
- return PrintJob;
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;
2131
2135
  }
2132
2136
  });
2133
2137
  Object.defineProperty(exports, "directory", {
@@ -2160,6 +2164,12 @@ Object.defineProperty(exports, "extractAudio", {
2160
2164
  return extractAudio;
2161
2165
  }
2162
2166
  });
2167
+ Object.defineProperty(exports, "extractCsrf", {
2168
+ enumerable: true,
2169
+ get: function() {
2170
+ return extractCsrf;
2171
+ }
2172
+ });
2163
2173
  Object.defineProperty(exports, "extractTaggedUsers", {
2164
2174
  enumerable: true,
2165
2175
  get: function() {
@@ -2172,6 +2182,12 @@ Object.defineProperty(exports, "findTags", {
2172
2182
  return findTags;
2173
2183
  }
2174
2184
  });
2185
+ Object.defineProperty(exports, "g", {
2186
+ enumerable: true,
2187
+ get: function() {
2188
+ return g;
2189
+ }
2190
+ });
2175
2191
  Object.defineProperty(exports, "idFromShortcode", {
2176
2192
  enumerable: true,
2177
2193
  get: function() {
@@ -2190,6 +2206,12 @@ Object.defineProperty(exports, "noopLogger", {
2190
2206
  return noopLogger;
2191
2207
  }
2192
2208
  });
2209
+ Object.defineProperty(exports, "pad", {
2210
+ enumerable: true,
2211
+ get: function() {
2212
+ return pad;
2213
+ }
2214
+ });
2193
2215
  Object.defineProperty(exports, "parseInt", {
2194
2216
  enumerable: true,
2195
2217
  get: function() {