@vicociv/instaloader 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/index.d.mts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +166 -101
- package/dist/index.mjs +166 -101
- package/package.json +38 -14
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @vicociv/instaloader
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@vicociv/instaloader)
|
|
4
|
+
[](https://www.npmjs.com/package/@vicociv/instaloader)
|
|
5
|
+
[](https://bundlephobia.com/package/@vicociv/instaloader)
|
|
6
|
+
[](https://github.com/star8ks/instaloader.js/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/star8ks/instaloader.js)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://nodejs.org/)
|
|
11
|
+
|
|
3
12
|
TypeScript port of [instaloader](https://github.com/instaloader/instaloader) - Download Instagram content (posts, stories, profiles) with metadata.
|
|
4
13
|
|
|
5
14
|
## Installation
|
|
@@ -384,3 +393,9 @@ import {
|
|
|
384
393
|
## License
|
|
385
394
|
|
|
386
395
|
MIT
|
|
396
|
+
|
|
397
|
+
## Buy Me a Coffee
|
|
398
|
+
|
|
399
|
+
If you find this project helpful, consider supporting its development:
|
|
400
|
+
|
|
401
|
+
[](https://ko-fi.com/nickyoung)
|
package/dist/index.d.mts
CHANGED
|
@@ -543,7 +543,7 @@ declare class InstaloaderContext {
|
|
|
543
543
|
readonly quiet: boolean;
|
|
544
544
|
readonly iphoneSupport: boolean;
|
|
545
545
|
readonly fatalStatusCodes: number[];
|
|
546
|
-
private
|
|
546
|
+
private _cookieStore;
|
|
547
547
|
private _csrfToken;
|
|
548
548
|
private _username;
|
|
549
549
|
private _userId;
|
|
@@ -610,7 +610,7 @@ declare class InstaloaderContext {
|
|
|
610
610
|
*/
|
|
611
611
|
updateCookies(cookies: CookieData): void;
|
|
612
612
|
/**
|
|
613
|
-
* Build cookie header string from cookie
|
|
613
|
+
* Build cookie header string from cookie store.
|
|
614
614
|
*/
|
|
615
615
|
private _getCookieHeader;
|
|
616
616
|
/**
|
|
@@ -637,9 +637,11 @@ declare class InstaloaderContext {
|
|
|
637
637
|
*/
|
|
638
638
|
graphql_query(queryHash: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
|
|
639
639
|
/**
|
|
640
|
-
* Do a doc_id-based GraphQL Query
|
|
641
|
-
*
|
|
642
|
-
*
|
|
640
|
+
* Do a doc_id-based GraphQL Query.
|
|
641
|
+
*
|
|
642
|
+
* Uses GET for anonymous requests (works without login) and POST for authenticated
|
|
643
|
+
* requests. This is the correct behavior - Instagram's API accepts GET for public
|
|
644
|
+
* data but requires POST with session for authenticated operations.
|
|
643
645
|
*/
|
|
644
646
|
doc_id_graphql_query(docId: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
|
|
645
647
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -543,7 +543,7 @@ declare class InstaloaderContext {
|
|
|
543
543
|
readonly quiet: boolean;
|
|
544
544
|
readonly iphoneSupport: boolean;
|
|
545
545
|
readonly fatalStatusCodes: number[];
|
|
546
|
-
private
|
|
546
|
+
private _cookieStore;
|
|
547
547
|
private _csrfToken;
|
|
548
548
|
private _username;
|
|
549
549
|
private _userId;
|
|
@@ -610,7 +610,7 @@ declare class InstaloaderContext {
|
|
|
610
610
|
*/
|
|
611
611
|
updateCookies(cookies: CookieData): void;
|
|
612
612
|
/**
|
|
613
|
-
* Build cookie header string from cookie
|
|
613
|
+
* Build cookie header string from cookie store.
|
|
614
614
|
*/
|
|
615
615
|
private _getCookieHeader;
|
|
616
616
|
/**
|
|
@@ -637,9 +637,11 @@ declare class InstaloaderContext {
|
|
|
637
637
|
*/
|
|
638
638
|
graphql_query(queryHash: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
|
|
639
639
|
/**
|
|
640
|
-
* Do a doc_id-based GraphQL Query
|
|
641
|
-
*
|
|
642
|
-
*
|
|
640
|
+
* Do a doc_id-based GraphQL Query.
|
|
641
|
+
*
|
|
642
|
+
* Uses GET for anonymous requests (works without login) and POST for authenticated
|
|
643
|
+
* requests. This is the correct behavior - Instagram's API accepts GET for public
|
|
644
|
+
* data but requires POST with session for authenticated operations.
|
|
643
645
|
*/
|
|
644
646
|
doc_id_graphql_query(docId: string, variables: JsonObject, referer?: string): Promise<JsonObject>;
|
|
645
647
|
/**
|
package/dist/index.js
CHANGED
|
@@ -220,9 +220,113 @@ var InvalidIteratorException = class extends InstaloaderException {
|
|
|
220
220
|
}
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
+
// src/utils.ts
|
|
224
|
+
function generateUUID() {
|
|
225
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
226
|
+
return crypto.randomUUID();
|
|
227
|
+
}
|
|
228
|
+
const bytes = new Uint8Array(16);
|
|
229
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
230
|
+
crypto.getRandomValues(bytes);
|
|
231
|
+
} else {
|
|
232
|
+
for (let i = 0; i < 16; i++) {
|
|
233
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
237
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
238
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
239
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
240
|
+
}
|
|
241
|
+
var SimpleCookieStore = class {
|
|
242
|
+
cookies = /* @__PURE__ */ new Map();
|
|
243
|
+
/**
|
|
244
|
+
* Get cookies for a URL as a key-value object.
|
|
245
|
+
*/
|
|
246
|
+
getCookies(url) {
|
|
247
|
+
const domain = this.extractDomain(url);
|
|
248
|
+
const domainCookies = this.cookies.get(domain);
|
|
249
|
+
if (!domainCookies) {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
return Object.fromEntries(domainCookies);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Set cookies from a key-value object.
|
|
256
|
+
*/
|
|
257
|
+
setCookies(cookies, url) {
|
|
258
|
+
const domain = this.extractDomain(url);
|
|
259
|
+
let domainCookies = this.cookies.get(domain);
|
|
260
|
+
if (!domainCookies) {
|
|
261
|
+
domainCookies = /* @__PURE__ */ new Map();
|
|
262
|
+
this.cookies.set(domain, domainCookies);
|
|
263
|
+
}
|
|
264
|
+
for (const [key, value] of Object.entries(cookies)) {
|
|
265
|
+
domainCookies.set(key, value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Parse and store cookies from Set-Cookie headers.
|
|
270
|
+
*/
|
|
271
|
+
parseSetCookieHeaders(headers, url) {
|
|
272
|
+
const setCookies = headers.getSetCookie?.() || [];
|
|
273
|
+
const domain = this.extractDomain(url);
|
|
274
|
+
let domainCookies = this.cookies.get(domain);
|
|
275
|
+
if (!domainCookies) {
|
|
276
|
+
domainCookies = /* @__PURE__ */ new Map();
|
|
277
|
+
this.cookies.set(domain, domainCookies);
|
|
278
|
+
}
|
|
279
|
+
for (const cookieStr of setCookies) {
|
|
280
|
+
const parsed = this.parseSetCookie(cookieStr);
|
|
281
|
+
if (parsed) {
|
|
282
|
+
domainCookies.set(parsed.name, parsed.value);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generate Cookie header string for a URL.
|
|
288
|
+
*/
|
|
289
|
+
getCookieHeader(url) {
|
|
290
|
+
const cookies = this.getCookies(url);
|
|
291
|
+
return Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Clear all cookies.
|
|
295
|
+
*/
|
|
296
|
+
clear() {
|
|
297
|
+
this.cookies.clear();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Clear cookies for a specific domain.
|
|
301
|
+
*/
|
|
302
|
+
clearDomain(url) {
|
|
303
|
+
const domain = this.extractDomain(url);
|
|
304
|
+
this.cookies.delete(domain);
|
|
305
|
+
}
|
|
306
|
+
extractDomain(url) {
|
|
307
|
+
try {
|
|
308
|
+
return new URL(url).hostname;
|
|
309
|
+
} catch {
|
|
310
|
+
return url;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
parseSetCookie(cookieStr) {
|
|
314
|
+
const firstSemi = cookieStr.indexOf(";");
|
|
315
|
+
const nameValue = firstSemi === -1 ? cookieStr : cookieStr.slice(0, firstSemi);
|
|
316
|
+
const eqIndex = nameValue.indexOf("=");
|
|
317
|
+
if (eqIndex === -1) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const name = nameValue.slice(0, eqIndex).trim();
|
|
321
|
+
const value = nameValue.slice(eqIndex + 1).trim();
|
|
322
|
+
if (!name) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return { name, value };
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
223
329
|
// src/instaloadercontext.ts
|
|
224
|
-
var import_tough_cookie = require("tough-cookie");
|
|
225
|
-
var import_uuid = require("uuid");
|
|
226
330
|
function defaultUserAgent() {
|
|
227
331
|
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36";
|
|
228
332
|
}
|
|
@@ -250,7 +354,7 @@ function defaultIphoneHeaders() {
|
|
|
250
354
|
"x-ig-mapped-locale": "en-US",
|
|
251
355
|
"x-ig-timezone-offset": String(timezoneOffset),
|
|
252
356
|
"x-ig-www-claim": "0",
|
|
253
|
-
"x-pigeon-session-id": (
|
|
357
|
+
"x-pigeon-session-id": generateUUID(),
|
|
254
358
|
"x-tigon-is-retry": "False",
|
|
255
359
|
"x-whatsapp": "0"
|
|
256
360
|
};
|
|
@@ -259,7 +363,7 @@ function getPerRequestHeaders() {
|
|
|
259
363
|
return {
|
|
260
364
|
"x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6),
|
|
261
365
|
"x-ig-connection-speed": `${Math.floor(Math.random() * 19e3) + 1e3}kbps`,
|
|
262
|
-
"x-pigeon-session-id": (
|
|
366
|
+
"x-pigeon-session-id": generateUUID()
|
|
263
367
|
};
|
|
264
368
|
}
|
|
265
369
|
function sleep(ms) {
|
|
@@ -312,16 +416,10 @@ var RateController = class {
|
|
|
312
416
|
this._queryTimestamps.set(queryType, []);
|
|
313
417
|
}
|
|
314
418
|
const timestamps = this._queryTimestamps.get(queryType);
|
|
315
|
-
const filteredTimestamps = timestamps.filter(
|
|
316
|
-
(t) => t > currentTime - 60 * 60
|
|
317
|
-
);
|
|
419
|
+
const filteredTimestamps = timestamps.filter((t) => t > currentTime - 60 * 60);
|
|
318
420
|
this._queryTimestamps.set(queryType, filteredTimestamps);
|
|
319
421
|
const perTypeNextRequestTime = () => {
|
|
320
|
-
const reqs = this._reqsInSlidingWindow(
|
|
321
|
-
queryType,
|
|
322
|
-
currentTime,
|
|
323
|
-
perTypeSlidingWindow
|
|
324
|
-
);
|
|
422
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, perTypeSlidingWindow);
|
|
325
423
|
if (reqs.length < this.countPerSlidingWindow(queryType)) {
|
|
326
424
|
return 0;
|
|
327
425
|
} else {
|
|
@@ -334,11 +432,7 @@ var RateController = class {
|
|
|
334
432
|
}
|
|
335
433
|
const gqlAccumulatedSlidingWindow = 600;
|
|
336
434
|
const gqlAccumulatedMaxCount = 275;
|
|
337
|
-
const reqs = this._reqsInSlidingWindow(
|
|
338
|
-
null,
|
|
339
|
-
currentTime,
|
|
340
|
-
gqlAccumulatedSlidingWindow
|
|
341
|
-
);
|
|
435
|
+
const reqs = this._reqsInSlidingWindow(null, currentTime, gqlAccumulatedSlidingWindow);
|
|
342
436
|
if (reqs.length < gqlAccumulatedMaxCount) {
|
|
343
437
|
return 0;
|
|
344
438
|
} else {
|
|
@@ -348,33 +442,18 @@ var RateController = class {
|
|
|
348
442
|
const untrackedNextRequestTime = () => {
|
|
349
443
|
if (untrackedQueries) {
|
|
350
444
|
if (queryType === "iphone") {
|
|
351
|
-
const reqs = this._reqsInSlidingWindow(
|
|
352
|
-
queryType,
|
|
353
|
-
currentTime,
|
|
354
|
-
iphoneSlidingWindow
|
|
355
|
-
);
|
|
445
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, iphoneSlidingWindow);
|
|
356
446
|
this._iphoneEarliestNextRequestTime = Math.min(...reqs) + iphoneSlidingWindow + 18;
|
|
357
447
|
} else {
|
|
358
|
-
const reqs = this._reqsInSlidingWindow(
|
|
359
|
-
queryType,
|
|
360
|
-
currentTime,
|
|
361
|
-
perTypeSlidingWindow
|
|
362
|
-
);
|
|
448
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, perTypeSlidingWindow);
|
|
363
449
|
this._earliestNextRequestTime = Math.min(...reqs) + perTypeSlidingWindow + 6;
|
|
364
450
|
}
|
|
365
451
|
}
|
|
366
|
-
return Math.max(
|
|
367
|
-
this._iphoneEarliestNextRequestTime,
|
|
368
|
-
this._earliestNextRequestTime
|
|
369
|
-
);
|
|
452
|
+
return Math.max(this._iphoneEarliestNextRequestTime, this._earliestNextRequestTime);
|
|
370
453
|
};
|
|
371
454
|
const iphoneNextRequest = () => {
|
|
372
455
|
if (queryType === "iphone") {
|
|
373
|
-
const reqs = this._reqsInSlidingWindow(
|
|
374
|
-
queryType,
|
|
375
|
-
currentTime,
|
|
376
|
-
iphoneSlidingWindow
|
|
377
|
-
);
|
|
456
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, iphoneSlidingWindow);
|
|
378
457
|
if (reqs.length >= 199) {
|
|
379
458
|
return Math.min(...reqs) + iphoneSlidingWindow + 18;
|
|
380
459
|
}
|
|
@@ -442,7 +521,7 @@ var InstaloaderContext = class {
|
|
|
442
521
|
quiet;
|
|
443
522
|
iphoneSupport;
|
|
444
523
|
fatalStatusCodes;
|
|
445
|
-
|
|
524
|
+
_cookieStore;
|
|
446
525
|
_csrfToken = null;
|
|
447
526
|
_username = null;
|
|
448
527
|
_userId = null;
|
|
@@ -463,13 +542,13 @@ var InstaloaderContext = class {
|
|
|
463
542
|
this.quiet = options.quiet ?? false;
|
|
464
543
|
this.iphoneSupport = options.iphoneSupport ?? true;
|
|
465
544
|
this.fatalStatusCodes = options.fatalStatusCodes ?? [];
|
|
466
|
-
this.
|
|
545
|
+
this._cookieStore = new SimpleCookieStore();
|
|
467
546
|
this._iphoneHeaders = defaultIphoneHeaders();
|
|
468
547
|
this._initAnonymousCookies();
|
|
469
548
|
this._rateController = options.rateController ? options.rateController(this) : new RateController(this);
|
|
470
549
|
}
|
|
471
550
|
_initAnonymousCookies() {
|
|
472
|
-
const
|
|
551
|
+
const url = "https://www.instagram.com/";
|
|
473
552
|
const defaultCookies = {
|
|
474
553
|
sessionid: "",
|
|
475
554
|
mid: "",
|
|
@@ -479,15 +558,7 @@ var InstaloaderContext = class {
|
|
|
479
558
|
s_network: "",
|
|
480
559
|
ds_user_id: ""
|
|
481
560
|
};
|
|
482
|
-
|
|
483
|
-
const cookie = new import_tough_cookie.Cookie({
|
|
484
|
-
key: name,
|
|
485
|
-
value,
|
|
486
|
-
domain,
|
|
487
|
-
path: "/"
|
|
488
|
-
});
|
|
489
|
-
this._cookieJar.setCookieSync(cookie, `https://${domain}/`);
|
|
490
|
-
}
|
|
561
|
+
this._cookieStore.setCookies(defaultCookies, url);
|
|
491
562
|
}
|
|
492
563
|
/** True if this instance is logged in. */
|
|
493
564
|
get is_logged_in() {
|
|
@@ -580,27 +651,13 @@ var InstaloaderContext = class {
|
|
|
580
651
|
* Get cookies as a plain object.
|
|
581
652
|
*/
|
|
582
653
|
getCookies(url = "https://www.instagram.com/") {
|
|
583
|
-
|
|
584
|
-
const result = {};
|
|
585
|
-
for (const cookie of cookies) {
|
|
586
|
-
result[cookie.key] = cookie.value;
|
|
587
|
-
}
|
|
588
|
-
return result;
|
|
654
|
+
return this._cookieStore.getCookies(url);
|
|
589
655
|
}
|
|
590
656
|
/**
|
|
591
657
|
* Set cookies from a plain object.
|
|
592
658
|
*/
|
|
593
659
|
setCookies(cookies, url = "https://www.instagram.com/") {
|
|
594
|
-
|
|
595
|
-
for (const [name, value] of Object.entries(cookies)) {
|
|
596
|
-
const cookie = new import_tough_cookie.Cookie({
|
|
597
|
-
key: name,
|
|
598
|
-
value,
|
|
599
|
-
domain,
|
|
600
|
-
path: "/"
|
|
601
|
-
});
|
|
602
|
-
this._cookieJar.setCookieSync(cookie, url);
|
|
603
|
-
}
|
|
660
|
+
this._cookieStore.setCookies(cookies, url);
|
|
604
661
|
}
|
|
605
662
|
/**
|
|
606
663
|
* Save session data for later restoration.
|
|
@@ -612,7 +669,7 @@ var InstaloaderContext = class {
|
|
|
612
669
|
* Load session data from a saved session.
|
|
613
670
|
*/
|
|
614
671
|
loadSession(username, sessionData) {
|
|
615
|
-
this.
|
|
672
|
+
this._cookieStore = new SimpleCookieStore();
|
|
616
673
|
this.setCookies(sessionData);
|
|
617
674
|
this._csrfToken = sessionData["csrftoken"] || null;
|
|
618
675
|
this._username = username;
|
|
@@ -625,26 +682,16 @@ var InstaloaderContext = class {
|
|
|
625
682
|
this.setCookies(cookies);
|
|
626
683
|
}
|
|
627
684
|
/**
|
|
628
|
-
* Build cookie header string from cookie
|
|
685
|
+
* Build cookie header string from cookie store.
|
|
629
686
|
*/
|
|
630
687
|
_getCookieHeader(url) {
|
|
631
|
-
|
|
632
|
-
return cookies.map((c) => `${c.key}=${c.value}`).join("; ");
|
|
688
|
+
return this._cookieStore.getCookieHeader(url);
|
|
633
689
|
}
|
|
634
690
|
/**
|
|
635
691
|
* Parse and store cookies from Set-Cookie headers.
|
|
636
692
|
*/
|
|
637
693
|
_storeCookies(url, headers) {
|
|
638
|
-
|
|
639
|
-
for (const cookieStr of setCookies) {
|
|
640
|
-
try {
|
|
641
|
-
const cookie = import_tough_cookie.Cookie.parse(cookieStr);
|
|
642
|
-
if (cookie) {
|
|
643
|
-
this._cookieJar.setCookieSync(cookie, url);
|
|
644
|
-
}
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
647
|
-
}
|
|
694
|
+
this._cookieStore.parseSetCookieHeaders(headers, url);
|
|
648
695
|
}
|
|
649
696
|
/**
|
|
650
697
|
* Format response error message.
|
|
@@ -722,10 +769,7 @@ var InstaloaderContext = class {
|
|
|
722
769
|
});
|
|
723
770
|
} else {
|
|
724
771
|
for (const [key, value] of Object.entries(params)) {
|
|
725
|
-
url.searchParams.set(
|
|
726
|
-
key,
|
|
727
|
-
typeof value === "string" ? value : JSON.stringify(value)
|
|
728
|
-
);
|
|
772
|
+
url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
729
773
|
}
|
|
730
774
|
response = await fetch(url.toString(), {
|
|
731
775
|
method: "GET",
|
|
@@ -748,9 +792,7 @@ var InstaloaderContext = class {
|
|
|
748
792
|
HTTP redirect from ${url} to ${redirectUrl}`);
|
|
749
793
|
if (redirectUrl.startsWith("https://www.instagram.com/accounts/login") || redirectUrl.startsWith("https://i.instagram.com/accounts/login")) {
|
|
750
794
|
if (!this.is_logged_in) {
|
|
751
|
-
throw new LoginRequiredException(
|
|
752
|
-
"Redirected to login page. Use login() first."
|
|
753
|
-
);
|
|
795
|
+
throw new LoginRequiredException("Redirected to login page. Use login() first.");
|
|
754
796
|
}
|
|
755
797
|
throw new AbortDownloadException(
|
|
756
798
|
"Redirected to login page. You've been logged out, please wait some time, recreate the session and try again"
|
|
@@ -857,9 +899,11 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
857
899
|
return respJson;
|
|
858
900
|
}
|
|
859
901
|
/**
|
|
860
|
-
* Do a doc_id-based GraphQL Query
|
|
861
|
-
*
|
|
862
|
-
*
|
|
902
|
+
* Do a doc_id-based GraphQL Query.
|
|
903
|
+
*
|
|
904
|
+
* Uses GET for anonymous requests (works without login) and POST for authenticated
|
|
905
|
+
* requests. This is the correct behavior - Instagram's API accepts GET for public
|
|
906
|
+
* data but requires POST with session for authenticated operations.
|
|
863
907
|
*/
|
|
864
908
|
async doc_id_graphql_query(docId, variables, referer) {
|
|
865
909
|
const perRequestHeaders = getPerRequestHeaders();
|
|
@@ -876,10 +920,11 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
876
920
|
headers["referer"] = encodeURIComponent(referer);
|
|
877
921
|
}
|
|
878
922
|
const variablesJson = JSON.stringify(variables);
|
|
923
|
+
const usePost = this.is_logged_in;
|
|
879
924
|
const respJson = await this.getJson(
|
|
880
925
|
"graphql/query",
|
|
881
926
|
{ variables: variablesJson, doc_id: docId, server_timestamps: "true" },
|
|
882
|
-
{ usePost
|
|
927
|
+
{ usePost, headers }
|
|
883
928
|
);
|
|
884
929
|
if (!("status" in respJson)) {
|
|
885
930
|
this.error('GraphQL response did not contain a "status" field.');
|
|
@@ -950,9 +995,7 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
950
995
|
this._responseError(response.status, response.statusText, url)
|
|
951
996
|
);
|
|
952
997
|
}
|
|
953
|
-
throw new ConnectionException(
|
|
954
|
-
this._responseError(response.status, response.statusText, url)
|
|
955
|
-
);
|
|
998
|
+
throw new ConnectionException(this._responseError(response.status, response.statusText, url));
|
|
956
999
|
}
|
|
957
1000
|
/**
|
|
958
1001
|
* Test if logged in by querying the current user.
|
|
@@ -974,7 +1017,7 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
974
1017
|
* Login to Instagram.
|
|
975
1018
|
*/
|
|
976
1019
|
async login(username, password) {
|
|
977
|
-
this.
|
|
1020
|
+
this._cookieStore = new SimpleCookieStore();
|
|
978
1021
|
this._initAnonymousCookies();
|
|
979
1022
|
const initUrl = "https://www.instagram.com/";
|
|
980
1023
|
const initResponse = await fetch(initUrl, {
|
|
@@ -1094,7 +1137,7 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
1094
1137
|
throw new InvalidArgumentException("No two-factor authentication pending.");
|
|
1095
1138
|
}
|
|
1096
1139
|
const { csrfToken, cookies, username, twoFactorId } = this._twoFactorAuthPending;
|
|
1097
|
-
this.
|
|
1140
|
+
this._cookieStore = new SimpleCookieStore();
|
|
1098
1141
|
this.setCookies(cookies);
|
|
1099
1142
|
const loginUrl = "https://www.instagram.com/accounts/login/ajax/two_factor/";
|
|
1100
1143
|
const response = await fetch(loginUrl, {
|
|
@@ -3280,7 +3323,9 @@ var Instaloader = class {
|
|
|
3280
3323
|
...options.sleep !== void 0 && { sleep: options.sleep },
|
|
3281
3324
|
...options.quiet !== void 0 && { quiet: options.quiet },
|
|
3282
3325
|
...options.userAgent !== void 0 && { userAgent: options.userAgent },
|
|
3283
|
-
...options.maxConnectionAttempts !== void 0 && {
|
|
3326
|
+
...options.maxConnectionAttempts !== void 0 && {
|
|
3327
|
+
maxConnectionAttempts: options.maxConnectionAttempts
|
|
3328
|
+
},
|
|
3284
3329
|
...options.requestTimeout !== void 0 && { requestTimeout: options.requestTimeout },
|
|
3285
3330
|
...options.rateController !== void 0 && { rateController: options.rateController },
|
|
3286
3331
|
...options.fatalStatusCodes !== void 0 && { fatalStatusCodes: options.fatalStatusCodes },
|
|
@@ -3385,7 +3430,7 @@ var Instaloader = class {
|
|
|
3385
3430
|
* Load session from file.
|
|
3386
3431
|
*/
|
|
3387
3432
|
async loadSessionFromFile(username, filename) {
|
|
3388
|
-
|
|
3433
|
+
const targetFile = filename ?? getDefaultSessionFilename(username);
|
|
3389
3434
|
if (!fs.existsSync(targetFile)) {
|
|
3390
3435
|
throw new Error(`Session file not found: ${targetFile}`);
|
|
3391
3436
|
}
|
|
@@ -3440,7 +3485,9 @@ var Instaloader = class {
|
|
|
3440
3485
|
}
|
|
3441
3486
|
});
|
|
3442
3487
|
if (!response.ok) {
|
|
3443
|
-
throw new ConnectionException(
|
|
3488
|
+
throw new ConnectionException(
|
|
3489
|
+
`Failed to download: ${response.status} ${response.statusText}`
|
|
3490
|
+
);
|
|
3444
3491
|
}
|
|
3445
3492
|
let finalFilename = nominalFilename;
|
|
3446
3493
|
const contentType = response.headers.get("Content-Type");
|
|
@@ -3539,11 +3586,21 @@ var Instaloader = class {
|
|
|
3539
3586
|
}
|
|
3540
3587
|
if (node.is_video) {
|
|
3541
3588
|
if (this.downloadVideos && node.video_url) {
|
|
3542
|
-
const dl = await this.downloadPic(
|
|
3589
|
+
const dl = await this.downloadPic(
|
|
3590
|
+
filename,
|
|
3591
|
+
node.video_url,
|
|
3592
|
+
post.date_utc,
|
|
3593
|
+
`${index + 1}`
|
|
3594
|
+
);
|
|
3543
3595
|
downloaded = downloaded || dl;
|
|
3544
3596
|
}
|
|
3545
3597
|
} else if (node.display_url) {
|
|
3546
|
-
const dl = await this.downloadPic(
|
|
3598
|
+
const dl = await this.downloadPic(
|
|
3599
|
+
filename,
|
|
3600
|
+
node.display_url,
|
|
3601
|
+
post.date_utc,
|
|
3602
|
+
`${index + 1}`
|
|
3603
|
+
);
|
|
3547
3604
|
downloaded = downloaded || dl;
|
|
3548
3605
|
}
|
|
3549
3606
|
index++;
|
|
@@ -3600,7 +3657,10 @@ var Instaloader = class {
|
|
|
3600
3657
|
}
|
|
3601
3658
|
if (displayedCount !== void 0) {
|
|
3602
3659
|
const width = displayedCount.toString().length;
|
|
3603
|
-
this.context.log(
|
|
3660
|
+
this.context.log(
|
|
3661
|
+
`[${number.toString().padStart(width)}/${displayedCount.toString().padStart(width)}] `,
|
|
3662
|
+
false
|
|
3663
|
+
);
|
|
3604
3664
|
} else {
|
|
3605
3665
|
this.context.log(`[${number.toString().padStart(3)}] `, false);
|
|
3606
3666
|
}
|
|
@@ -3682,7 +3742,12 @@ var Instaloader = class {
|
|
|
3682
3742
|
async downloadProfilePic(profile) {
|
|
3683
3743
|
const url = await profile.getProfilePicUrl();
|
|
3684
3744
|
if (!url) return;
|
|
3685
|
-
const dirname2 = formatFilename(
|
|
3745
|
+
const dirname2 = formatFilename(
|
|
3746
|
+
this.dirnamePattern,
|
|
3747
|
+
profile,
|
|
3748
|
+
profile.username.toLowerCase(),
|
|
3749
|
+
this.sanitizePaths
|
|
3750
|
+
);
|
|
3686
3751
|
const filename = path.join(dirname2, `${profile.username.toLowerCase()}_profile_pic`);
|
|
3687
3752
|
await this.downloadPic(filename, url, /* @__PURE__ */ new Date());
|
|
3688
3753
|
this.context.log("");
|
package/dist/index.mjs
CHANGED
|
@@ -132,9 +132,113 @@ var InvalidIteratorException = class extends InstaloaderException {
|
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
+
// src/utils.ts
|
|
136
|
+
function generateUUID() {
|
|
137
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
138
|
+
return crypto.randomUUID();
|
|
139
|
+
}
|
|
140
|
+
const bytes = new Uint8Array(16);
|
|
141
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
142
|
+
crypto.getRandomValues(bytes);
|
|
143
|
+
} else {
|
|
144
|
+
for (let i = 0; i < 16; i++) {
|
|
145
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
149
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
150
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
151
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
152
|
+
}
|
|
153
|
+
var SimpleCookieStore = class {
|
|
154
|
+
cookies = /* @__PURE__ */ new Map();
|
|
155
|
+
/**
|
|
156
|
+
* Get cookies for a URL as a key-value object.
|
|
157
|
+
*/
|
|
158
|
+
getCookies(url) {
|
|
159
|
+
const domain = this.extractDomain(url);
|
|
160
|
+
const domainCookies = this.cookies.get(domain);
|
|
161
|
+
if (!domainCookies) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
return Object.fromEntries(domainCookies);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Set cookies from a key-value object.
|
|
168
|
+
*/
|
|
169
|
+
setCookies(cookies, url) {
|
|
170
|
+
const domain = this.extractDomain(url);
|
|
171
|
+
let domainCookies = this.cookies.get(domain);
|
|
172
|
+
if (!domainCookies) {
|
|
173
|
+
domainCookies = /* @__PURE__ */ new Map();
|
|
174
|
+
this.cookies.set(domain, domainCookies);
|
|
175
|
+
}
|
|
176
|
+
for (const [key, value] of Object.entries(cookies)) {
|
|
177
|
+
domainCookies.set(key, value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Parse and store cookies from Set-Cookie headers.
|
|
182
|
+
*/
|
|
183
|
+
parseSetCookieHeaders(headers, url) {
|
|
184
|
+
const setCookies = headers.getSetCookie?.() || [];
|
|
185
|
+
const domain = this.extractDomain(url);
|
|
186
|
+
let domainCookies = this.cookies.get(domain);
|
|
187
|
+
if (!domainCookies) {
|
|
188
|
+
domainCookies = /* @__PURE__ */ new Map();
|
|
189
|
+
this.cookies.set(domain, domainCookies);
|
|
190
|
+
}
|
|
191
|
+
for (const cookieStr of setCookies) {
|
|
192
|
+
const parsed = this.parseSetCookie(cookieStr);
|
|
193
|
+
if (parsed) {
|
|
194
|
+
domainCookies.set(parsed.name, parsed.value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Generate Cookie header string for a URL.
|
|
200
|
+
*/
|
|
201
|
+
getCookieHeader(url) {
|
|
202
|
+
const cookies = this.getCookies(url);
|
|
203
|
+
return Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Clear all cookies.
|
|
207
|
+
*/
|
|
208
|
+
clear() {
|
|
209
|
+
this.cookies.clear();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Clear cookies for a specific domain.
|
|
213
|
+
*/
|
|
214
|
+
clearDomain(url) {
|
|
215
|
+
const domain = this.extractDomain(url);
|
|
216
|
+
this.cookies.delete(domain);
|
|
217
|
+
}
|
|
218
|
+
extractDomain(url) {
|
|
219
|
+
try {
|
|
220
|
+
return new URL(url).hostname;
|
|
221
|
+
} catch {
|
|
222
|
+
return url;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
parseSetCookie(cookieStr) {
|
|
226
|
+
const firstSemi = cookieStr.indexOf(";");
|
|
227
|
+
const nameValue = firstSemi === -1 ? cookieStr : cookieStr.slice(0, firstSemi);
|
|
228
|
+
const eqIndex = nameValue.indexOf("=");
|
|
229
|
+
if (eqIndex === -1) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const name = nameValue.slice(0, eqIndex).trim();
|
|
233
|
+
const value = nameValue.slice(eqIndex + 1).trim();
|
|
234
|
+
if (!name) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return { name, value };
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
135
241
|
// src/instaloadercontext.ts
|
|
136
|
-
import { CookieJar, Cookie } from "tough-cookie";
|
|
137
|
-
import { v4 as uuidv4 } from "uuid";
|
|
138
242
|
function defaultUserAgent() {
|
|
139
243
|
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36";
|
|
140
244
|
}
|
|
@@ -162,7 +266,7 @@ function defaultIphoneHeaders() {
|
|
|
162
266
|
"x-ig-mapped-locale": "en-US",
|
|
163
267
|
"x-ig-timezone-offset": String(timezoneOffset),
|
|
164
268
|
"x-ig-www-claim": "0",
|
|
165
|
-
"x-pigeon-session-id":
|
|
269
|
+
"x-pigeon-session-id": generateUUID(),
|
|
166
270
|
"x-tigon-is-retry": "False",
|
|
167
271
|
"x-whatsapp": "0"
|
|
168
272
|
};
|
|
@@ -171,7 +275,7 @@ function getPerRequestHeaders() {
|
|
|
171
275
|
return {
|
|
172
276
|
"x-pigeon-rawclienttime": (Date.now() / 1e3).toFixed(6),
|
|
173
277
|
"x-ig-connection-speed": `${Math.floor(Math.random() * 19e3) + 1e3}kbps`,
|
|
174
|
-
"x-pigeon-session-id":
|
|
278
|
+
"x-pigeon-session-id": generateUUID()
|
|
175
279
|
};
|
|
176
280
|
}
|
|
177
281
|
function sleep(ms) {
|
|
@@ -224,16 +328,10 @@ var RateController = class {
|
|
|
224
328
|
this._queryTimestamps.set(queryType, []);
|
|
225
329
|
}
|
|
226
330
|
const timestamps = this._queryTimestamps.get(queryType);
|
|
227
|
-
const filteredTimestamps = timestamps.filter(
|
|
228
|
-
(t) => t > currentTime - 60 * 60
|
|
229
|
-
);
|
|
331
|
+
const filteredTimestamps = timestamps.filter((t) => t > currentTime - 60 * 60);
|
|
230
332
|
this._queryTimestamps.set(queryType, filteredTimestamps);
|
|
231
333
|
const perTypeNextRequestTime = () => {
|
|
232
|
-
const reqs = this._reqsInSlidingWindow(
|
|
233
|
-
queryType,
|
|
234
|
-
currentTime,
|
|
235
|
-
perTypeSlidingWindow
|
|
236
|
-
);
|
|
334
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, perTypeSlidingWindow);
|
|
237
335
|
if (reqs.length < this.countPerSlidingWindow(queryType)) {
|
|
238
336
|
return 0;
|
|
239
337
|
} else {
|
|
@@ -246,11 +344,7 @@ var RateController = class {
|
|
|
246
344
|
}
|
|
247
345
|
const gqlAccumulatedSlidingWindow = 600;
|
|
248
346
|
const gqlAccumulatedMaxCount = 275;
|
|
249
|
-
const reqs = this._reqsInSlidingWindow(
|
|
250
|
-
null,
|
|
251
|
-
currentTime,
|
|
252
|
-
gqlAccumulatedSlidingWindow
|
|
253
|
-
);
|
|
347
|
+
const reqs = this._reqsInSlidingWindow(null, currentTime, gqlAccumulatedSlidingWindow);
|
|
254
348
|
if (reqs.length < gqlAccumulatedMaxCount) {
|
|
255
349
|
return 0;
|
|
256
350
|
} else {
|
|
@@ -260,33 +354,18 @@ var RateController = class {
|
|
|
260
354
|
const untrackedNextRequestTime = () => {
|
|
261
355
|
if (untrackedQueries) {
|
|
262
356
|
if (queryType === "iphone") {
|
|
263
|
-
const reqs = this._reqsInSlidingWindow(
|
|
264
|
-
queryType,
|
|
265
|
-
currentTime,
|
|
266
|
-
iphoneSlidingWindow
|
|
267
|
-
);
|
|
357
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, iphoneSlidingWindow);
|
|
268
358
|
this._iphoneEarliestNextRequestTime = Math.min(...reqs) + iphoneSlidingWindow + 18;
|
|
269
359
|
} else {
|
|
270
|
-
const reqs = this._reqsInSlidingWindow(
|
|
271
|
-
queryType,
|
|
272
|
-
currentTime,
|
|
273
|
-
perTypeSlidingWindow
|
|
274
|
-
);
|
|
360
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, perTypeSlidingWindow);
|
|
275
361
|
this._earliestNextRequestTime = Math.min(...reqs) + perTypeSlidingWindow + 6;
|
|
276
362
|
}
|
|
277
363
|
}
|
|
278
|
-
return Math.max(
|
|
279
|
-
this._iphoneEarliestNextRequestTime,
|
|
280
|
-
this._earliestNextRequestTime
|
|
281
|
-
);
|
|
364
|
+
return Math.max(this._iphoneEarliestNextRequestTime, this._earliestNextRequestTime);
|
|
282
365
|
};
|
|
283
366
|
const iphoneNextRequest = () => {
|
|
284
367
|
if (queryType === "iphone") {
|
|
285
|
-
const reqs = this._reqsInSlidingWindow(
|
|
286
|
-
queryType,
|
|
287
|
-
currentTime,
|
|
288
|
-
iphoneSlidingWindow
|
|
289
|
-
);
|
|
368
|
+
const reqs = this._reqsInSlidingWindow(queryType, currentTime, iphoneSlidingWindow);
|
|
290
369
|
if (reqs.length >= 199) {
|
|
291
370
|
return Math.min(...reqs) + iphoneSlidingWindow + 18;
|
|
292
371
|
}
|
|
@@ -354,7 +433,7 @@ var InstaloaderContext = class {
|
|
|
354
433
|
quiet;
|
|
355
434
|
iphoneSupport;
|
|
356
435
|
fatalStatusCodes;
|
|
357
|
-
|
|
436
|
+
_cookieStore;
|
|
358
437
|
_csrfToken = null;
|
|
359
438
|
_username = null;
|
|
360
439
|
_userId = null;
|
|
@@ -375,13 +454,13 @@ var InstaloaderContext = class {
|
|
|
375
454
|
this.quiet = options.quiet ?? false;
|
|
376
455
|
this.iphoneSupport = options.iphoneSupport ?? true;
|
|
377
456
|
this.fatalStatusCodes = options.fatalStatusCodes ?? [];
|
|
378
|
-
this.
|
|
457
|
+
this._cookieStore = new SimpleCookieStore();
|
|
379
458
|
this._iphoneHeaders = defaultIphoneHeaders();
|
|
380
459
|
this._initAnonymousCookies();
|
|
381
460
|
this._rateController = options.rateController ? options.rateController(this) : new RateController(this);
|
|
382
461
|
}
|
|
383
462
|
_initAnonymousCookies() {
|
|
384
|
-
const
|
|
463
|
+
const url = "https://www.instagram.com/";
|
|
385
464
|
const defaultCookies = {
|
|
386
465
|
sessionid: "",
|
|
387
466
|
mid: "",
|
|
@@ -391,15 +470,7 @@ var InstaloaderContext = class {
|
|
|
391
470
|
s_network: "",
|
|
392
471
|
ds_user_id: ""
|
|
393
472
|
};
|
|
394
|
-
|
|
395
|
-
const cookie = new Cookie({
|
|
396
|
-
key: name,
|
|
397
|
-
value,
|
|
398
|
-
domain,
|
|
399
|
-
path: "/"
|
|
400
|
-
});
|
|
401
|
-
this._cookieJar.setCookieSync(cookie, `https://${domain}/`);
|
|
402
|
-
}
|
|
473
|
+
this._cookieStore.setCookies(defaultCookies, url);
|
|
403
474
|
}
|
|
404
475
|
/** True if this instance is logged in. */
|
|
405
476
|
get is_logged_in() {
|
|
@@ -492,27 +563,13 @@ var InstaloaderContext = class {
|
|
|
492
563
|
* Get cookies as a plain object.
|
|
493
564
|
*/
|
|
494
565
|
getCookies(url = "https://www.instagram.com/") {
|
|
495
|
-
|
|
496
|
-
const result = {};
|
|
497
|
-
for (const cookie of cookies) {
|
|
498
|
-
result[cookie.key] = cookie.value;
|
|
499
|
-
}
|
|
500
|
-
return result;
|
|
566
|
+
return this._cookieStore.getCookies(url);
|
|
501
567
|
}
|
|
502
568
|
/**
|
|
503
569
|
* Set cookies from a plain object.
|
|
504
570
|
*/
|
|
505
571
|
setCookies(cookies, url = "https://www.instagram.com/") {
|
|
506
|
-
|
|
507
|
-
for (const [name, value] of Object.entries(cookies)) {
|
|
508
|
-
const cookie = new Cookie({
|
|
509
|
-
key: name,
|
|
510
|
-
value,
|
|
511
|
-
domain,
|
|
512
|
-
path: "/"
|
|
513
|
-
});
|
|
514
|
-
this._cookieJar.setCookieSync(cookie, url);
|
|
515
|
-
}
|
|
572
|
+
this._cookieStore.setCookies(cookies, url);
|
|
516
573
|
}
|
|
517
574
|
/**
|
|
518
575
|
* Save session data for later restoration.
|
|
@@ -524,7 +581,7 @@ var InstaloaderContext = class {
|
|
|
524
581
|
* Load session data from a saved session.
|
|
525
582
|
*/
|
|
526
583
|
loadSession(username, sessionData) {
|
|
527
|
-
this.
|
|
584
|
+
this._cookieStore = new SimpleCookieStore();
|
|
528
585
|
this.setCookies(sessionData);
|
|
529
586
|
this._csrfToken = sessionData["csrftoken"] || null;
|
|
530
587
|
this._username = username;
|
|
@@ -537,26 +594,16 @@ var InstaloaderContext = class {
|
|
|
537
594
|
this.setCookies(cookies);
|
|
538
595
|
}
|
|
539
596
|
/**
|
|
540
|
-
* Build cookie header string from cookie
|
|
597
|
+
* Build cookie header string from cookie store.
|
|
541
598
|
*/
|
|
542
599
|
_getCookieHeader(url) {
|
|
543
|
-
|
|
544
|
-
return cookies.map((c) => `${c.key}=${c.value}`).join("; ");
|
|
600
|
+
return this._cookieStore.getCookieHeader(url);
|
|
545
601
|
}
|
|
546
602
|
/**
|
|
547
603
|
* Parse and store cookies from Set-Cookie headers.
|
|
548
604
|
*/
|
|
549
605
|
_storeCookies(url, headers) {
|
|
550
|
-
|
|
551
|
-
for (const cookieStr of setCookies) {
|
|
552
|
-
try {
|
|
553
|
-
const cookie = Cookie.parse(cookieStr);
|
|
554
|
-
if (cookie) {
|
|
555
|
-
this._cookieJar.setCookieSync(cookie, url);
|
|
556
|
-
}
|
|
557
|
-
} catch {
|
|
558
|
-
}
|
|
559
|
-
}
|
|
606
|
+
this._cookieStore.parseSetCookieHeaders(headers, url);
|
|
560
607
|
}
|
|
561
608
|
/**
|
|
562
609
|
* Format response error message.
|
|
@@ -634,10 +681,7 @@ var InstaloaderContext = class {
|
|
|
634
681
|
});
|
|
635
682
|
} else {
|
|
636
683
|
for (const [key, value] of Object.entries(params)) {
|
|
637
|
-
url.searchParams.set(
|
|
638
|
-
key,
|
|
639
|
-
typeof value === "string" ? value : JSON.stringify(value)
|
|
640
|
-
);
|
|
684
|
+
url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
641
685
|
}
|
|
642
686
|
response = await fetch(url.toString(), {
|
|
643
687
|
method: "GET",
|
|
@@ -660,9 +704,7 @@ var InstaloaderContext = class {
|
|
|
660
704
|
HTTP redirect from ${url} to ${redirectUrl}`);
|
|
661
705
|
if (redirectUrl.startsWith("https://www.instagram.com/accounts/login") || redirectUrl.startsWith("https://i.instagram.com/accounts/login")) {
|
|
662
706
|
if (!this.is_logged_in) {
|
|
663
|
-
throw new LoginRequiredException(
|
|
664
|
-
"Redirected to login page. Use login() first."
|
|
665
|
-
);
|
|
707
|
+
throw new LoginRequiredException("Redirected to login page. Use login() first.");
|
|
666
708
|
}
|
|
667
709
|
throw new AbortDownloadException(
|
|
668
710
|
"Redirected to login page. You've been logged out, please wait some time, recreate the session and try again"
|
|
@@ -769,9 +811,11 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
769
811
|
return respJson;
|
|
770
812
|
}
|
|
771
813
|
/**
|
|
772
|
-
* Do a doc_id-based GraphQL Query
|
|
773
|
-
*
|
|
774
|
-
*
|
|
814
|
+
* Do a doc_id-based GraphQL Query.
|
|
815
|
+
*
|
|
816
|
+
* Uses GET for anonymous requests (works without login) and POST for authenticated
|
|
817
|
+
* requests. This is the correct behavior - Instagram's API accepts GET for public
|
|
818
|
+
* data but requires POST with session for authenticated operations.
|
|
775
819
|
*/
|
|
776
820
|
async doc_id_graphql_query(docId, variables, referer) {
|
|
777
821
|
const perRequestHeaders = getPerRequestHeaders();
|
|
@@ -788,10 +832,11 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
788
832
|
headers["referer"] = encodeURIComponent(referer);
|
|
789
833
|
}
|
|
790
834
|
const variablesJson = JSON.stringify(variables);
|
|
835
|
+
const usePost = this.is_logged_in;
|
|
791
836
|
const respJson = await this.getJson(
|
|
792
837
|
"graphql/query",
|
|
793
838
|
{ variables: variablesJson, doc_id: docId, server_timestamps: "true" },
|
|
794
|
-
{ usePost
|
|
839
|
+
{ usePost, headers }
|
|
795
840
|
);
|
|
796
841
|
if (!("status" in respJson)) {
|
|
797
842
|
this.error('GraphQL response did not contain a "status" field.');
|
|
@@ -862,9 +907,7 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
862
907
|
this._responseError(response.status, response.statusText, url)
|
|
863
908
|
);
|
|
864
909
|
}
|
|
865
|
-
throw new ConnectionException(
|
|
866
|
-
this._responseError(response.status, response.statusText, url)
|
|
867
|
-
);
|
|
910
|
+
throw new ConnectionException(this._responseError(response.status, response.statusText, url));
|
|
868
911
|
}
|
|
869
912
|
/**
|
|
870
913
|
* Test if logged in by querying the current user.
|
|
@@ -886,7 +929,7 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
886
929
|
* Login to Instagram.
|
|
887
930
|
*/
|
|
888
931
|
async login(username, password) {
|
|
889
|
-
this.
|
|
932
|
+
this._cookieStore = new SimpleCookieStore();
|
|
890
933
|
this._initAnonymousCookies();
|
|
891
934
|
const initUrl = "https://www.instagram.com/";
|
|
892
935
|
const initResponse = await fetch(initUrl, {
|
|
@@ -1006,7 +1049,7 @@ HTTP redirect from ${url} to ${redirectUrl}`);
|
|
|
1006
1049
|
throw new InvalidArgumentException("No two-factor authentication pending.");
|
|
1007
1050
|
}
|
|
1008
1051
|
const { csrfToken, cookies, username, twoFactorId } = this._twoFactorAuthPending;
|
|
1009
|
-
this.
|
|
1052
|
+
this._cookieStore = new SimpleCookieStore();
|
|
1010
1053
|
this.setCookies(cookies);
|
|
1011
1054
|
const loginUrl = "https://www.instagram.com/accounts/login/ajax/two_factor/";
|
|
1012
1055
|
const response = await fetch(loginUrl, {
|
|
@@ -3192,7 +3235,9 @@ var Instaloader = class {
|
|
|
3192
3235
|
...options.sleep !== void 0 && { sleep: options.sleep },
|
|
3193
3236
|
...options.quiet !== void 0 && { quiet: options.quiet },
|
|
3194
3237
|
...options.userAgent !== void 0 && { userAgent: options.userAgent },
|
|
3195
|
-
...options.maxConnectionAttempts !== void 0 && {
|
|
3238
|
+
...options.maxConnectionAttempts !== void 0 && {
|
|
3239
|
+
maxConnectionAttempts: options.maxConnectionAttempts
|
|
3240
|
+
},
|
|
3196
3241
|
...options.requestTimeout !== void 0 && { requestTimeout: options.requestTimeout },
|
|
3197
3242
|
...options.rateController !== void 0 && { rateController: options.rateController },
|
|
3198
3243
|
...options.fatalStatusCodes !== void 0 && { fatalStatusCodes: options.fatalStatusCodes },
|
|
@@ -3297,7 +3342,7 @@ var Instaloader = class {
|
|
|
3297
3342
|
* Load session from file.
|
|
3298
3343
|
*/
|
|
3299
3344
|
async loadSessionFromFile(username, filename) {
|
|
3300
|
-
|
|
3345
|
+
const targetFile = filename ?? getDefaultSessionFilename(username);
|
|
3301
3346
|
if (!fs.existsSync(targetFile)) {
|
|
3302
3347
|
throw new Error(`Session file not found: ${targetFile}`);
|
|
3303
3348
|
}
|
|
@@ -3352,7 +3397,9 @@ var Instaloader = class {
|
|
|
3352
3397
|
}
|
|
3353
3398
|
});
|
|
3354
3399
|
if (!response.ok) {
|
|
3355
|
-
throw new ConnectionException(
|
|
3400
|
+
throw new ConnectionException(
|
|
3401
|
+
`Failed to download: ${response.status} ${response.statusText}`
|
|
3402
|
+
);
|
|
3356
3403
|
}
|
|
3357
3404
|
let finalFilename = nominalFilename;
|
|
3358
3405
|
const contentType = response.headers.get("Content-Type");
|
|
@@ -3451,11 +3498,21 @@ var Instaloader = class {
|
|
|
3451
3498
|
}
|
|
3452
3499
|
if (node.is_video) {
|
|
3453
3500
|
if (this.downloadVideos && node.video_url) {
|
|
3454
|
-
const dl = await this.downloadPic(
|
|
3501
|
+
const dl = await this.downloadPic(
|
|
3502
|
+
filename,
|
|
3503
|
+
node.video_url,
|
|
3504
|
+
post.date_utc,
|
|
3505
|
+
`${index + 1}`
|
|
3506
|
+
);
|
|
3455
3507
|
downloaded = downloaded || dl;
|
|
3456
3508
|
}
|
|
3457
3509
|
} else if (node.display_url) {
|
|
3458
|
-
const dl = await this.downloadPic(
|
|
3510
|
+
const dl = await this.downloadPic(
|
|
3511
|
+
filename,
|
|
3512
|
+
node.display_url,
|
|
3513
|
+
post.date_utc,
|
|
3514
|
+
`${index + 1}`
|
|
3515
|
+
);
|
|
3459
3516
|
downloaded = downloaded || dl;
|
|
3460
3517
|
}
|
|
3461
3518
|
index++;
|
|
@@ -3512,7 +3569,10 @@ var Instaloader = class {
|
|
|
3512
3569
|
}
|
|
3513
3570
|
if (displayedCount !== void 0) {
|
|
3514
3571
|
const width = displayedCount.toString().length;
|
|
3515
|
-
this.context.log(
|
|
3572
|
+
this.context.log(
|
|
3573
|
+
`[${number.toString().padStart(width)}/${displayedCount.toString().padStart(width)}] `,
|
|
3574
|
+
false
|
|
3575
|
+
);
|
|
3516
3576
|
} else {
|
|
3517
3577
|
this.context.log(`[${number.toString().padStart(3)}] `, false);
|
|
3518
3578
|
}
|
|
@@ -3594,7 +3654,12 @@ var Instaloader = class {
|
|
|
3594
3654
|
async downloadProfilePic(profile) {
|
|
3595
3655
|
const url = await profile.getProfilePicUrl();
|
|
3596
3656
|
if (!url) return;
|
|
3597
|
-
const dirname2 = formatFilename(
|
|
3657
|
+
const dirname2 = formatFilename(
|
|
3658
|
+
this.dirnamePattern,
|
|
3659
|
+
profile,
|
|
3660
|
+
profile.username.toLowerCase(),
|
|
3661
|
+
this.sanitizePaths
|
|
3662
|
+
);
|
|
3598
3663
|
const filename = path.join(dirname2, `${profile.username.toLowerCase()}_profile_pic`);
|
|
3599
3664
|
await this.downloadPic(filename, url, /* @__PURE__ */ new Date());
|
|
3600
3665
|
this.context.log("");
|
package/package.json
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vicociv/instaloader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TypeScript port of instaloader - Download pictures (or videos) along with their captions and other metadata from Instagram.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
10
11
|
"import": "./dist/index.mjs",
|
|
11
|
-
"require": "./dist/index.js"
|
|
12
|
-
"types": "./dist/index.d.ts"
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"sideEffects": false,
|
|
15
16
|
"files": [
|
|
16
17
|
"dist"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
20
|
+
"access": "public",
|
|
21
|
+
"provenance": true
|
|
20
22
|
},
|
|
21
23
|
"scripts": {
|
|
22
24
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
23
25
|
"test": "vitest run",
|
|
24
26
|
"test:watch": "vitest",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
25
28
|
"lint": "eslint src --ext .ts",
|
|
29
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
30
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
31
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
26
32
|
"typecheck": "tsc --noEmit",
|
|
27
|
-
"prepublishOnly": "npm run build"
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
34
|
+
"prepare": "husky",
|
|
35
|
+
"changeset": "changeset",
|
|
36
|
+
"version": "changeset version",
|
|
37
|
+
"release": "npm run build && changeset publish"
|
|
38
|
+
},
|
|
39
|
+
"lint-staged": {
|
|
40
|
+
"*.ts": [
|
|
41
|
+
"eslint --fix",
|
|
42
|
+
"prettier --write"
|
|
43
|
+
]
|
|
28
44
|
},
|
|
29
45
|
"keywords": [
|
|
30
46
|
"instagram",
|
|
31
47
|
"instagram-scraper",
|
|
32
48
|
"instagram-client",
|
|
49
|
+
"instagram-api",
|
|
33
50
|
"instagram-feed",
|
|
34
51
|
"downloader",
|
|
35
52
|
"videos",
|
|
@@ -40,30 +57,37 @@
|
|
|
40
57
|
"instagram-metadata",
|
|
41
58
|
"instagram-downloader",
|
|
42
59
|
"instagram-stories",
|
|
43
|
-
"typescript"
|
|
60
|
+
"typescript",
|
|
61
|
+
"nodejs"
|
|
44
62
|
],
|
|
45
|
-
"author": "",
|
|
63
|
+
"author": "star8ks",
|
|
46
64
|
"license": "MIT",
|
|
47
65
|
"repository": {
|
|
48
66
|
"type": "git",
|
|
49
|
-
"url": ""
|
|
67
|
+
"url": "git+https://github.com/star8ks/instaloader.js.git"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/star8ks/instaloader.js/issues"
|
|
50
71
|
},
|
|
72
|
+
"homepage": "https://github.com/star8ks/instaloader.js#readme",
|
|
51
73
|
"engines": {
|
|
52
74
|
"node": ">=18.0.0"
|
|
53
75
|
},
|
|
54
76
|
"devDependencies": {
|
|
77
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
78
|
+
"@changesets/cli": "^2.29.8",
|
|
79
|
+
"@commitlint/cli": "^20.2.0",
|
|
80
|
+
"@commitlint/config-conventional": "^20.2.0",
|
|
55
81
|
"@types/node": "^20.10.0",
|
|
56
|
-
"@types/tough-cookie": "^4.0.5",
|
|
57
|
-
"@types/uuid": "^10.0.0",
|
|
58
82
|
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
59
83
|
"@typescript-eslint/parser": "^6.13.0",
|
|
84
|
+
"@vitest/coverage-v8": "^1.6.1",
|
|
60
85
|
"eslint": "^8.55.0",
|
|
86
|
+
"husky": "^9.1.7",
|
|
87
|
+
"lint-staged": "^16.2.7",
|
|
88
|
+
"prettier": "^3.7.4",
|
|
61
89
|
"tsup": "^8.0.1",
|
|
62
90
|
"typescript": "^5.3.2",
|
|
63
91
|
"vitest": "^1.0.4"
|
|
64
|
-
},
|
|
65
|
-
"dependencies": {
|
|
66
|
-
"tough-cookie": "^4.1.3",
|
|
67
|
-
"uuid": "^13.0.0"
|
|
68
92
|
}
|
|
69
93
|
}
|