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