@chilfish/gallery-dl-instagram 0.2.0 → 0.2.3

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;
@@ -1071,6 +907,7 @@ function parsePostRest(post, cfg) {
1071
907
  owner_id: owner.pk,
1072
908
  username: owner.username ?? "",
1073
909
  fullname: owner.full_name ?? "",
910
+ user: owner,
1074
911
  post_date: date,
1075
912
  date,
1076
913
  description: caption ? caption.text : "",
@@ -1082,10 +919,9 @@ function parsePostRest(post, cfg) {
1082
919
  if (tags.length > 0) data.tags = [...new Set(tags)].sort();
1083
920
  if (post.location) {
1084
921
  const loc = post.location;
1085
- const slug = loc.short_name.replace(/\s+/g, "-").toLowerCase();
1086
922
  data.location_id = loc.pk;
1087
- data.location_slug = slug;
1088
- data.location_url = `${cfg.root}/explore/locations/${loc.pk}/${slug}/`;
923
+ data.location_slug = loc.short_name.replace(/\s+/g, "-").toLowerCase();
924
+ data.location_url = `${cfg.root}/explore/locations/${loc.pk}/${data.location_slug}/`;
1089
925
  }
1090
926
  if (post.coauthor_producers) data.coauthors = post.coauthor_producers.map((u) => ({
1091
927
  id: u.pk,
@@ -1117,7 +953,7 @@ function parsePostRest(post, cfg) {
1117
953
  if (post.music_metadata) {
1118
954
  const info = post.music_metadata.music_info;
1119
955
  if (info) {
1120
- const audio = extractAudio(post, data, { music_asset_info: info }, cfg);
956
+ const audio = extractAudio(post, data, info, cfg);
1121
957
  if (audio) {
1122
958
  audio.num = items.length;
1123
959
  data._files.push(audio);
@@ -1132,7 +968,7 @@ function parsePostRest(post, cfg) {
1132
968
  if (post.subscription_media_visibility) data.subscription = post.subscription_media_visibility;
1133
969
  return data;
1134
970
  }
1135
- /** Story / highlight */
971
+ /** Parse a story or highlight REST response. */
1136
972
  function parseStoryRest(post, cfg) {
1137
973
  const items = post.items;
1138
974
  const reelId = String(post.id).split(":").pop() ?? "0";
@@ -1158,9 +994,8 @@ function parseStoryRest(post, cfg) {
1158
994
  expires: expires ? cfg.parseTimestamp(expires) : void 0,
1159
995
  user: post.user
1160
996
  };
1161
- if (!isStory) {
1162
- if (post.title) data.highlight_title = post.title;
1163
- } else if (!post.seen) post.seen = expires - 86400;
997
+ if (!isStory && post.title) data.highlight_title = post.title;
998
+ else if (!post.seen) post.seen = expires - 86400;
1164
999
  for (let num = 0; num < items.length; num++) {
1165
1000
  const item = items[num];
1166
1001
  const media = parseMediaItem(item, post, cfg, num + 1);
@@ -1170,7 +1005,7 @@ function parseStoryRest(post, cfg) {
1170
1005
  }
1171
1006
  return data;
1172
1007
  }
1173
- /** Single media item */
1008
+ /** Parse a single media item (image/video) from a carousel or story. */
1174
1009
  function parseMediaItem(item, parent, cfg, num) {
1175
1010
  let image;
1176
1011
  try {
@@ -1229,7 +1064,7 @@ function parseMediaItem(item, parent, cfg, num) {
1229
1064
  if (itemRec.audience) media.audience = itemRec.audience;
1230
1065
  return media;
1231
1066
  }
1232
- /** Tagged users */
1067
+ /** Extract tagged users from various field formats. */
1233
1068
  function extractTaggedUsers(src, dest) {
1234
1069
  dest.tagged_users = [];
1235
1070
  const edges = src.edge_media_to_tagged_user;
@@ -1272,22 +1107,23 @@ function extractTaggedUsers(src, dest) {
1272
1107
  }
1273
1108
  }
1274
1109
  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
- });
1110
+ dest.tagged_users = dest.tagged_users.filter((t) => seen.has(t.id) ? false : (seen.add(t.id), true));
1280
1111
  }
1281
- /** Audio / music extraction */
1112
+ /** Extract audio/music metadata from a story sticker. */
1282
1113
  function extractAudio(src, dest, sticker, cfg) {
1283
1114
  const info = sticker.music_asset_info;
1284
1115
  if (!info) return null;
1285
1116
  const cinfo = sticker.music_consumption_info ?? info;
1286
- dest.audio_title = info.title;
1117
+ dest.audio_title = info.title ?? info.sanitized_title;
1118
+ dest.audio_subtitle = info.subtitle;
1287
1119
  dest.audio_duration = (info.duration_in_ms ?? 0) / 1e3;
1288
1120
  dest.audio_timestamps = info.highlight_start_times_in_ms;
1289
1121
  dest.audio_artist = info.display_artist ?? cinfo.display_artist;
1290
1122
  dest.audio_user = info.ig_artist ?? cinfo.ig_artist;
1123
+ dest.audio_has_lyrics = info.has_lyrics;
1124
+ dest.audio_is_explicit = info.is_explicit;
1125
+ dest.audio_cover_artwork_uri = info.cover_artwork_uri;
1126
+ dest.audio_cover_artwork_thumbnail_uri = info.cover_artwork_thumbnail_uri;
1291
1127
  const url = info.progressive_download_url;
1292
1128
  if (!url) return null;
1293
1129
  return {
@@ -1304,13 +1140,25 @@ function extractAudio(src, dest, sticker, cfg) {
1304
1140
  height_original: 0,
1305
1141
  tagged_users: [],
1306
1142
  audio_user: info.ig_artist ?? cinfo.ig_artist,
1307
- audio_title: info.title,
1143
+ audio_title: info.title ?? info.sanitized_title,
1144
+ audio_subtitle: info.subtitle,
1308
1145
  audio_artist: info.display_artist ?? cinfo.display_artist,
1309
1146
  audio_duration: (info.duration_in_ms ?? 0) / 1e3,
1310
- audio_timestamps: info.highlight_start_times_in_ms
1147
+ audio_timestamps: info.highlight_start_times_in_ms,
1148
+ audio_cover_artwork_uri: info.cover_artwork_uri,
1149
+ audio_cover_artwork_thumbnail_uri: info.cover_artwork_thumbnail_uri,
1150
+ audio_has_lyrics: info.has_lyrics,
1151
+ audio_is_explicit: info.is_explicit
1311
1152
  };
1312
1153
  }
1313
- /** GraphQL parser */
1154
+ function extractPinned(post) {
1155
+ if (post.timeline_pinned_user_ids) return post.timeline_pinned_user_ids;
1156
+ if (post.clips_tab_pinned_user_ids) return post.clips_tab_pinned_user_ids;
1157
+ return [];
1158
+ }
1159
+ //#endregion
1160
+ //#region src/instagram/parsers/graphql.ts
1161
+ /** Parse a GraphQL post/edge response. */
1314
1162
  function parsePostGraphql(post, cfg) {
1315
1163
  const typename = post.__typename ?? "GraphImage";
1316
1164
  const owner = post.owner;
@@ -1323,6 +1171,7 @@ function parsePostGraphql(post, cfg) {
1323
1171
  owner_id: owner.id ?? owner.pk,
1324
1172
  username: owner.username ?? "",
1325
1173
  fullname: owner.full_name ?? "",
1174
+ user: owner,
1326
1175
  post_id: post.id,
1327
1176
  post_shortcode: post.shortcode,
1328
1177
  post_url: `${cfg.root}/p/${post.shortcode}/`,
@@ -1395,11 +1244,6 @@ function parsePostGraphql(post, cfg) {
1395
1244
  }
1396
1245
  return data;
1397
1246
  }
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
1247
  function parseUnicodeEscapes(text) {
1404
1248
  if (!text.includes("\\u")) return text;
1405
1249
  return text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
@@ -1558,12 +1402,123 @@ var InstagramExtractor = class extends Extractor {
1558
1402
  }
1559
1403
  };
1560
1404
  //#endregion
1561
- //#region src/instagram/extractors.ts
1562
- const BASE_RE = /^(?:https?:\/\/)?(?:www\.)?instagram\.com/;
1563
- function re(base, path) {
1564
- const pathSrc = typeof path === "string" ? path : path.source;
1565
- return new RegExp(base.source + pathSrc, "i");
1566
- }
1405
+ //#region src/instagram/extractors/helpers.ts
1406
+ /** Shared regex utilities for Instagram extractor URL patterns. */
1407
+ const BASE_RE = /^(?:https?:\/\/)?(?:www\.)?instagram\.com/;
1408
+ function re(base, path) {
1409
+ const pathSrc = typeof path === "string" ? path : path.source;
1410
+ return new RegExp(base.source + pathSrc, "i");
1411
+ }
1412
+ //#endregion
1413
+ //#region src/instagram/extractors/registry.ts
1414
+ const _registry = /* @__PURE__ */ new Map();
1415
+ function register(subcategory, cls) {
1416
+ _registry.set(subcategory, cls);
1417
+ }
1418
+ function get(subcategory) {
1419
+ return _registry.get(subcategory);
1420
+ }
1421
+ //#endregion
1422
+ //#region src/instagram/extractors/avatar.ts
1423
+ var InstagramAvatarExtractor = class InstagramAvatarExtractor extends InstagramExtractor {
1424
+ static subcategory = "avatar";
1425
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/avatar/);
1426
+ subcategory = InstagramAvatarExtractor.subcategory;
1427
+ constructor(opts) {
1428
+ super(opts);
1429
+ }
1430
+ static fromURL(url, opts) {
1431
+ const match = InstagramAvatarExtractor.pattern.exec(url);
1432
+ if (!match) return null;
1433
+ return new InstagramAvatarExtractor({
1434
+ ...opts,
1435
+ url,
1436
+ match
1437
+ });
1438
+ }
1439
+ async *posts() {
1440
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1441
+ let user;
1442
+ if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1443
+ else user = await this.api.userByScreenName(screenName);
1444
+ const avatar = user.hd_profile_pic_url_info ?? user.hd_profile_pic_versions?.[user.hd_profile_pic_versions.length - 1] ?? {
1445
+ url: user.profile_pic_url ?? "",
1446
+ width: 0,
1447
+ height: 0
1448
+ };
1449
+ let pk = user.profile_pic_id?.split("_")[0];
1450
+ let code;
1451
+ if (pk) code = shortcodeFromId(pk);
1452
+ else {
1453
+ pk = `avatar:${user.pk}`;
1454
+ code = pk;
1455
+ }
1456
+ yield {
1457
+ pk,
1458
+ code,
1459
+ user,
1460
+ caption: null,
1461
+ like_count: 0,
1462
+ image_versions2: { candidates: [avatar] }
1463
+ };
1464
+ }
1465
+ };
1466
+ register(InstagramAvatarExtractor.subcategory, InstagramAvatarExtractor);
1467
+ //#endregion
1468
+ //#region src/instagram/extractors/highlights.ts
1469
+ var InstagramHighlightsExtractor = class InstagramHighlightsExtractor extends InstagramExtractor {
1470
+ static subcategory = "highlights";
1471
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/highlights/);
1472
+ subcategory = InstagramHighlightsExtractor.subcategory;
1473
+ constructor(opts) {
1474
+ super(opts);
1475
+ }
1476
+ static fromURL(url, opts) {
1477
+ const match = InstagramHighlightsExtractor.pattern.exec(url);
1478
+ if (!match) return null;
1479
+ return new InstagramHighlightsExtractor({
1480
+ ...opts,
1481
+ url,
1482
+ match
1483
+ });
1484
+ }
1485
+ async *posts() {
1486
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1487
+ const uid = await this.api.userId(screenName);
1488
+ yield* this.api.highlightsMedia(uid);
1489
+ }
1490
+ };
1491
+ register(InstagramHighlightsExtractor.subcategory, InstagramHighlightsExtractor);
1492
+ //#endregion
1493
+ //#region src/instagram/extractors/info.ts
1494
+ var InstagramInfoExtractor = class InstagramInfoExtractor extends InstagramExtractor {
1495
+ static subcategory = "info";
1496
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/info/);
1497
+ subcategory = InstagramInfoExtractor.subcategory;
1498
+ constructor(opts) {
1499
+ super(opts);
1500
+ }
1501
+ static fromURL(url, opts) {
1502
+ const match = InstagramInfoExtractor.pattern.exec(url);
1503
+ if (!match) return null;
1504
+ return new InstagramInfoExtractor({
1505
+ ...opts,
1506
+ url,
1507
+ match
1508
+ });
1509
+ }
1510
+ async *items() {
1511
+ const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1512
+ let user;
1513
+ if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1514
+ else user = await this.api.userByScreenName(screenName);
1515
+ yield directory(user);
1516
+ }
1517
+ async *posts() {}
1518
+ };
1519
+ register(InstagramInfoExtractor.subcategory, InstagramInfoExtractor);
1520
+ //#endregion
1521
+ //#region src/instagram/extractors/post.ts
1567
1522
  var InstagramPostExtractor = class InstagramPostExtractor extends InstagramExtractor {
1568
1523
  static subcategory = "post";
1569
1524
  static pattern = re(/^(?:https?:\/\/)?(?:www\.)?instagram\.com\//, /(?:share(?:\/(?:p|tv|reels?))?|(?:[^/?#]+\/)?(?:p|tv|reels?))\/([^/?#]+)/);
@@ -1598,75 +1553,9 @@ var InstagramPostExtractor = class InstagramPostExtractor extends InstagramExtra
1598
1553
  yield* this.api.media(shortcode);
1599
1554
  }
1600
1555
  };
1601
- var InstagramUserExtractor = class InstagramUserExtractor extends InstagramExtractor {
1602
- static subcategory = "user";
1603
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/?(?:$|[?#])/);
1604
- subcategory = InstagramUserExtractor.subcategory;
1605
- constructor(opts) {
1606
- super(opts);
1607
- }
1608
- static fromURL(url, opts) {
1609
- const match = InstagramUserExtractor.pattern.exec(url);
1610
- if (!match) return null;
1611
- return new InstagramUserExtractor({
1612
- ...opts,
1613
- url,
1614
- match
1615
- });
1616
- }
1617
- 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}'`);
1666
- }
1667
- }
1668
- async *posts() {}
1669
- };
1556
+ register(InstagramPostExtractor.subcategory, InstagramPostExtractor);
1557
+ //#endregion
1558
+ //#region src/instagram/extractors/posts-list.ts
1670
1559
  var InstagramPostsExtractor = class InstagramPostsExtractor extends InstagramExtractor {
1671
1560
  static subcategory = "posts";
1672
1561
  static pattern = re(BASE_RE, /(\/[^/?#]+)\/posts/);
@@ -1689,6 +1578,9 @@ var InstagramPostsExtractor = class InstagramPostsExtractor extends InstagramExt
1689
1578
  yield* this.api.userFeed(uid);
1690
1579
  }
1691
1580
  };
1581
+ register(InstagramPostsExtractor.subcategory, InstagramPostsExtractor);
1582
+ //#endregion
1583
+ //#region src/instagram/extractors/reels-list.ts
1692
1584
  var InstagramReelsExtractor = class InstagramReelsExtractor extends InstagramExtractor {
1693
1585
  static subcategory = "reels";
1694
1586
  static pattern = re(BASE_RE, /(\/[^/?#]+)\/reels/);
@@ -1711,44 +1603,32 @@ var InstagramReelsExtractor = class InstagramReelsExtractor extends InstagramExt
1711
1603
  yield* this.api.userClips(uid);
1712
1604
  }
1713
1605
  };
1714
- var InstagramTaggedExtractor = class InstagramTaggedExtractor extends InstagramExtractor {
1715
- static subcategory = "tagged";
1716
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/tagged/);
1717
- subcategory = InstagramTaggedExtractor.subcategory;
1718
- _taggedUserId = "";
1606
+ register(InstagramReelsExtractor.subcategory, InstagramReelsExtractor);
1607
+ //#endregion
1608
+ //#region src/instagram/extractors/saved.ts
1609
+ var InstagramSavedExtractor = class InstagramSavedExtractor extends InstagramExtractor {
1610
+ static subcategory = "saved";
1611
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/saved(?:\/all-posts)?\/?$/);
1612
+ subcategory = InstagramSavedExtractor.subcategory;
1719
1613
  constructor(opts) {
1720
1614
  super(opts);
1721
1615
  }
1722
1616
  static fromURL(url, opts) {
1723
- const match = InstagramTaggedExtractor.pattern.exec(url);
1617
+ const match = InstagramSavedExtractor.pattern.exec(url);
1724
1618
  if (!match) return null;
1725
- return new InstagramTaggedExtractor({
1619
+ return new InstagramSavedExtractor({
1726
1620
  ...opts,
1727
1621
  url,
1728
1622
  match
1729
1623
  });
1730
1624
  }
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
1625
  async *posts() {
1748
- if (!this._taggedUserId) await this.metadata();
1749
- yield* this.api.userTagged(this._taggedUserId);
1626
+ yield* this.api.userSaved();
1750
1627
  }
1751
1628
  };
1629
+ register(InstagramSavedExtractor.subcategory, InstagramSavedExtractor);
1630
+ //#endregion
1631
+ //#region src/instagram/extractors/stories.ts
1752
1632
  var InstagramStoriesExtractor = class InstagramStoriesExtractor extends InstagramExtractor {
1753
1633
  static subcategory = "stories";
1754
1634
  static pattern = /^(?:https?:\/\/)?(?:www\.)?instagram\.com\/(?:stories\/(?:highlights\/(\d+)|([^/?#]+)(?:\/(\d+))?)|\/(aGlnaGxpZ2h0[^?#]+)(?:\?story_media_id=(\d+))?)/;
@@ -1804,28 +1684,9 @@ var InstagramStoriesExtractor = class InstagramStoriesExtractor extends Instagra
1804
1684
  } else yield* reels;
1805
1685
  }
1806
1686
  };
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
- };
1687
+ register(InstagramStoriesExtractor.subcategory, InstagramStoriesExtractor);
1688
+ //#endregion
1689
+ //#region src/instagram/extractors/tag.ts
1829
1690
  var InstagramTagExtractor = class InstagramTagExtractor extends InstagramExtractor {
1830
1691
  static subcategory = "tag";
1831
1692
  static pattern = re(BASE_RE, /\/explore\/tags\/([^/?#]+)/);
@@ -1851,94 +1712,219 @@ var InstagramTagExtractor = class InstagramTagExtractor extends InstagramExtract
1851
1712
  yield* this.api.tagsMedia(decodeURIComponent(tag));
1852
1713
  }
1853
1714
  };
1854
- var InstagramInfoExtractor = class InstagramInfoExtractor extends InstagramExtractor {
1855
- static subcategory = "info";
1856
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/info/);
1857
- subcategory = InstagramInfoExtractor.subcategory;
1715
+ register(InstagramTagExtractor.subcategory, InstagramTagExtractor);
1716
+ //#endregion
1717
+ //#region src/instagram/extractors/tagged.ts
1718
+ var InstagramTaggedExtractor = class InstagramTaggedExtractor extends InstagramExtractor {
1719
+ static subcategory = "tagged";
1720
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/tagged/);
1721
+ subcategory = InstagramTaggedExtractor.subcategory;
1722
+ _taggedUserId = "";
1858
1723
  constructor(opts) {
1859
1724
  super(opts);
1860
1725
  }
1861
1726
  static fromURL(url, opts) {
1862
- const match = InstagramInfoExtractor.pattern.exec(url);
1727
+ const match = InstagramTaggedExtractor.pattern.exec(url);
1863
1728
  if (!match) return null;
1864
- return new InstagramInfoExtractor({
1729
+ return new InstagramTaggedExtractor({
1865
1730
  ...opts,
1866
1731
  url,
1867
1732
  match
1868
1733
  });
1869
1734
  }
1870
- async *items() {
1735
+ async metadata() {
1871
1736
  const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1872
1737
  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);
1738
+ if (screenName.startsWith("id:")) {
1739
+ this._taggedUserId = screenName.slice(3);
1740
+ user = await this.api.userById(screenName.slice(3));
1741
+ } else {
1742
+ this._taggedUserId = await this.api.userId(screenName);
1743
+ user = await this.api.userByScreenName(screenName);
1744
+ }
1745
+ return {
1746
+ tagged_owner_id: user.id ?? user.pk,
1747
+ tagged_username: user.username,
1748
+ tagged_full_name: user.full_name
1749
+ };
1750
+ }
1751
+ async *posts() {
1752
+ if (!this._taggedUserId) await this.metadata();
1753
+ yield* this.api.userTagged(this._taggedUserId);
1876
1754
  }
1877
- async *posts() {}
1878
1755
  };
1879
- var InstagramAvatarExtractor = class InstagramAvatarExtractor extends InstagramExtractor {
1880
- static subcategory = "avatar";
1881
- static pattern = re(BASE_RE, /(\/[^/?#]+)\/avatar/);
1882
- subcategory = InstagramAvatarExtractor.subcategory;
1756
+ register(InstagramTaggedExtractor.subcategory, InstagramTaggedExtractor);
1757
+ //#endregion
1758
+ //#region src/instagram/extractors/user.ts
1759
+ var InstagramUserExtractor = class InstagramUserExtractor extends InstagramExtractor {
1760
+ static subcategory = "user";
1761
+ static pattern = re(BASE_RE, /(\/[^/?#]+)\/?(?:$|[?#])/);
1762
+ subcategory = InstagramUserExtractor.subcategory;
1883
1763
  constructor(opts) {
1884
1764
  super(opts);
1885
1765
  }
1886
1766
  static fromURL(url, opts) {
1887
- const match = InstagramAvatarExtractor.pattern.exec(url);
1767
+ const match = InstagramUserExtractor.pattern.exec(url);
1888
1768
  if (!match) return null;
1889
- return new InstagramAvatarExtractor({
1769
+ return new InstagramUserExtractor({
1890
1770
  ...opts,
1891
1771
  url,
1892
1772
  match
1893
1773
  });
1894
1774
  }
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
1775
+ async *items() {
1776
+ await this.login();
1777
+ const userPath = this.groups[0] ?? "/";
1778
+ const base = `${this.root}${userPath}/`;
1779
+ const storiesUrl = `${this.root}/stories/${userPath.slice(1)}/`;
1780
+ const include = this._cfg("include", ["posts"]);
1781
+ const categories = include === "all" ? [
1782
+ "posts",
1783
+ "reels",
1784
+ "tagged",
1785
+ "stories",
1786
+ "highlights",
1787
+ "info",
1788
+ "avatar"
1789
+ ] : typeof include === "string" ? include.replace(/\s+/g, "").split(",") : include;
1790
+ const urls = {
1791
+ info: `${base}info/`,
1792
+ avatar: `${base}avatar/`,
1793
+ stories: storiesUrl,
1794
+ highlights: `${base}highlights/`,
1795
+ posts: `${base}posts/`,
1796
+ reels: `${base}reels/`,
1797
+ tagged: `${base}tagged/`
1904
1798
  };
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;
1799
+ for (const cat of categories) {
1800
+ const cls = get(cat);
1801
+ const url = urls[cat];
1802
+ if (cls && url) yield queue(url, { _extractor: cls });
1803
+ else this.log.warn(`Invalid include '${cat}'`);
1911
1804
  }
1912
- yield {
1913
- pk,
1914
- code,
1915
- user,
1916
- caption: null,
1917
- like_count: 0,
1918
- image_versions2: { candidates: [avatar] }
1919
- };
1920
1805
  }
1806
+ async *posts() {}
1921
1807
  };
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();
1808
+ register(InstagramUserExtractor.subcategory, InstagramUserExtractor);
1809
+ //#endregion
1810
+ //#region src/fetcher.ts
1811
+ /** Build URL with query params appended as URLSearchParams. */
1812
+ function buildUrl(base, params) {
1813
+ if (!params) return base;
1814
+ const cleaned = {};
1815
+ for (const [k, v] of Object.entries(params)) if (v != null) cleaned[k] = String(v);
1816
+ const entries = Object.entries(cleaned);
1817
+ if (entries.length === 0) return base;
1818
+ const qs = new URLSearchParams(entries).toString();
1819
+ return `${base}${base.includes("?") ? "&" : "?"}${qs}`;
1820
+ }
1821
+ /** Merge cookie strings with append semantics: a=1 + b=2 → a=1; b=2 */
1822
+ function mergeCookie(base, extra) {
1823
+ if (!base) return extra;
1824
+ return `${base}; ${extra}`;
1825
+ }
1826
+ /** Extract csrftoken value from a Cookie header string. */
1827
+ function extractCsrf(cookies) {
1828
+ return cookies.match(/(?:^|;\s*)csrftoken=([^;]+)/)?.[1] ?? "";
1829
+ }
1830
+ /** Convert fetch Headers to a plain Record. */
1831
+ function headersToRecord(headers) {
1832
+ const rec = {};
1833
+ headers.forEach((v, k) => {
1834
+ rec[k] = v;
1835
+ });
1836
+ return rec;
1837
+ }
1838
+ /** Read response body according to the requested type. */
1839
+ async function readBody(resp, responseType) {
1840
+ switch (responseType) {
1841
+ case "arraybuffer": {
1842
+ const buf = await resp.arrayBuffer();
1843
+ return Buffer.from(buf);
1844
+ }
1845
+ case "text": return resp.text();
1846
+ default: return resp.json();
1940
1847
  }
1941
- };
1848
+ }
1849
+ /** Serialize a request body value for fetch. */
1850
+ function serializeBody(data) {
1851
+ if (data == null) return void 0;
1852
+ if (typeof data === "string") return data;
1853
+ if (data instanceof URLSearchParams) return data;
1854
+ return JSON.stringify(data);
1855
+ }
1856
+ 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";
1857
+ /**
1858
+ * Create a platform-agnostic HttpClient backed by native ``fetch``.
1859
+ *
1860
+ * Zero dependencies — works in Node.js 18+, browsers, Deno, and Edge.
1861
+ *
1862
+ * @example Plain (no cookies)
1863
+ * ```ts
1864
+ * const http = createFetchHttpClient()
1865
+ * ```
1866
+ *
1867
+ * @example With static cookies (CLI session mode)
1868
+ * ```ts
1869
+ * const http = createFetchHttpClient({ cookie: 'sessionid=abc; csrftoken=xyz' })
1870
+ * ```
1871
+ *
1872
+ * @example With cookie jar (anonymous session)
1873
+ * ```ts
1874
+ * const jar = createCookieJar()
1875
+ * const http = createFetchHttpClient({
1876
+ * cookieProvider: () => jar.getCookieHeader(),
1877
+ * onResponse: (headers) => jar.setFromResponse(headers),
1878
+ * })
1879
+ * ```
1880
+ */
1881
+ function createFetchHttpClient(opts = {}) {
1882
+ const { cookie, cookieProvider, userAgent = UA, timeout = 3e4, onResponse } = opts;
1883
+ return { async request(config) {
1884
+ const method = config.method ?? "GET";
1885
+ const url = buildUrl(config.url, config.params);
1886
+ const headers = new Headers(config.headers);
1887
+ const reqCookie = cookieProvider?.() ?? cookie;
1888
+ if (reqCookie) {
1889
+ const existing = headers.get("Cookie");
1890
+ headers.set("Cookie", existing ? mergeCookie(reqCookie, existing) : reqCookie);
1891
+ }
1892
+ if (!headers.has("User-Agent")) headers.set("User-Agent", userAgent);
1893
+ const body = serializeBody(config.data);
1894
+ if (typeof body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
1895
+ let controller = null;
1896
+ let timer = null;
1897
+ let signal = config.signal ?? null;
1898
+ const timeoutMs = config.timeout ?? timeout;
1899
+ if (!signal) {
1900
+ controller = new AbortController();
1901
+ timer = setTimeout(() => controller.abort(), timeoutMs);
1902
+ signal = controller.signal;
1903
+ }
1904
+ try {
1905
+ const resp = await fetch(url, {
1906
+ method,
1907
+ headers,
1908
+ body,
1909
+ signal
1910
+ });
1911
+ onResponse?.(headersToRecord(resp.headers));
1912
+ const data = await readBody(resp, config.responseType);
1913
+ return {
1914
+ status: resp.status,
1915
+ data,
1916
+ headers: headersToRecord(resp.headers),
1917
+ url: resp.url
1918
+ };
1919
+ } catch (err) {
1920
+ if (controller?.signal.aborted && !config.signal?.aborted) throw new Error(`Request timeout after ${timeoutMs}ms: ${url}`);
1921
+ 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.");
1922
+ throw err;
1923
+ } finally {
1924
+ if (timer) clearTimeout(timer);
1925
+ }
1926
+ } };
1927
+ }
1942
1928
  //#endregion
1943
1929
  //#region src/sdk.ts
1944
1930
  var InstagramSDK = class {
@@ -1947,8 +1933,8 @@ var InstagramSDK = class {
1947
1933
  log;
1948
1934
  config;
1949
1935
  _csrfToken;
1950
- constructor(opts) {
1951
- this.http = opts.http;
1936
+ constructor(opts = {}) {
1937
+ this.http = opts.http ?? createFetchHttpClient();
1952
1938
  this.storage = opts.storage ?? void 0;
1953
1939
  this.log = opts.log ?? noopLogger;
1954
1940
  this.config = new ConfigManager();
@@ -2016,4 +2002,4 @@ var InstagramSDK = class {
2016
2002
  }
2017
2003
  };
2018
2004
  //#endregion
2019
- export { directory as A, nameExtFromURL as C, unquote as D, unescape as E, PrintJob as F, Extractor as I, noopLogger as L, url as M, DownloadJob as N, idFromShortcode as O, Job as P, ConfigManager as R, findTags as S, parseUnicodeEscapes$1 as T, parsePostRest as _, InstagramPostExtractor as a, extr as b, InstagramSavedExtractor as c, InstagramTaggedExtractor as d, InstagramUserExtractor as f, parsePostGraphql as g, extractTaggedUsers as h, InstagramInfoExtractor as i, queue as j, shortcodeFromId as k, InstagramStoriesExtractor as l, extractAudio as m, InstagramAvatarExtractor as n, InstagramPostsExtractor as o, InstagramExtractor as p, InstagramHighlightsExtractor as r, InstagramReelsExtractor as s, InstagramSDK as t, InstagramTagExtractor as u, InstagramRestAPI as v, parseInt as w, extract as x, ensureHttpScheme as y };
2005
+ export { directory as A, _YELLOW as B, extract as C, parseUnicodeEscapes$1 as D, parseInt as E, Extractor as F, pad as G, c as H, noopLogger as I, ConfigManager as K, DownloadJob as L, url as M, idFromShortcode as N, unescape as O, shortcodeFromId as P, Job as R, extr as S, nameExtFromURL as T, dim as U, b as V, g as W, extractAudio as _, InstagramTaggedExtractor as a, InstagramRestAPI as b, InstagramSavedExtractor as c, InstagramPostExtractor as d, InstagramInfoExtractor as f, parsePostGraphql as g, InstagramExtractor as h, InstagramUserExtractor as i, queue as j, unquote as k, InstagramReelsExtractor as l, InstagramAvatarExtractor as m, createFetchHttpClient as n, InstagramTagExtractor as o, InstagramHighlightsExtractor as p, extractCsrf as r, InstagramStoriesExtractor as s, InstagramSDK as t, InstagramPostsExtractor as u, extractTaggedUsers as v, findTags as w, ensureHttpScheme as x, parsePostRest as y, _RESET as z };