open-nami 1.2.0 → 1.2.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +113 -33
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ var require_package = __commonJS({
9
9
  "package.json"(exports, module) {
10
10
  module.exports = {
11
11
  name: "open-nami",
12
- version: "1.2.0",
12
+ version: "1.2.1",
13
13
  description: "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
14
14
  type: "module",
15
15
  bin: {
@@ -729,6 +729,7 @@ async function clearCheckpoint(destDir) {
729
729
  import path10 from "path";
730
730
  import os5 from "os";
731
731
  import fs10 from "fs/promises";
732
+ import fsSync from "fs";
732
733
 
733
734
  // src/lib/cookies.ts
734
735
  import fs2 from "fs";
@@ -1378,25 +1379,43 @@ var InstagramExtractor = class {
1378
1379
  }
1379
1380
  return user;
1380
1381
  }
1381
- async fetchHighlightsTray(userId, options) {
1382
+ async fetchHighlightsTray(userId, options, tokens) {
1383
+ let authError = null;
1382
1384
  try {
1383
1385
  const variables = JSON.stringify({ user_id: userId });
1384
- const body = new URLSearchParams({
1385
- doc_id: IG_QUERIES.PolarisProfileStoryHighlightsTrayContentQuery,
1386
- variables
1387
- }).toString();
1386
+ const bodyParams = new URLSearchParams({
1387
+ av: "17841415137994167",
1388
+ __d: "www",
1389
+ __user: "0",
1390
+ __a: "1",
1391
+ __req: "0",
1392
+ dpr: "1",
1393
+ fb_api_caller_class: "RelayModern",
1394
+ fb_api_req_friendly_name: "PolarisProfileStoryHighlightsTrayContentQuery",
1395
+ server_timestamps: "true",
1396
+ variables,
1397
+ doc_id: IG_QUERIES.PolarisProfileStoryHighlightsTrayContentQuery
1398
+ });
1399
+ if (tokens?.fbDtsg) bodyParams.set("fb_dtsg", tokens.fbDtsg);
1400
+ if (tokens?.lsd) bodyParams.set("lsd", tokens.lsd);
1401
+ const headers = {
1402
+ "Accept": "*/*",
1403
+ "Content-Type": "application/x-www-form-urlencoded",
1404
+ "X-IG-App-ID": IG_APP_ID,
1405
+ "X-FB-Friendly-Name": "PolarisProfileStoryHighlightsTrayContentQuery",
1406
+ "Referer": "https://www.instagram.com/",
1407
+ "Origin": "https://www.instagram.com",
1408
+ "X-ASBD-ID": "359341"
1409
+ };
1410
+ if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
1411
+ if (tokens?.lsd) headers["X-FB-LSD"] = tokens.lsd;
1388
1412
  const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
1389
1413
  method: "POST",
1390
- body,
1414
+ body: bodyParams.toString(),
1391
1415
  cookiePath: options.cookiePath,
1392
1416
  cookieDomain: "instagram.com",
1393
1417
  signal: options.signal,
1394
- headers: {
1395
- "Content-Type": "application/x-www-form-urlencoded",
1396
- "X-IG-App-ID": IG_APP_ID,
1397
- "X-FB-Friendly-Name": "PolarisProfileStoryHighlightsTrayContentQuery",
1398
- "Referer": "https://www.instagram.com/"
1399
- }
1418
+ headers
1400
1419
  });
1401
1420
  const edges = res?.data?.highlights?.edges;
1402
1421
  if (Array.isArray(edges) && edges.length > 0) {
@@ -1407,10 +1426,16 @@ var InstagramExtractor = class {
1407
1426
  }
1408
1427
  if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
1409
1428
  const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
1410
- throw new Error(`Instagram GraphQL query error: ${errMsg}`);
1429
+ if (errMsg.includes("login") || errMsg.includes("auth") || errMsg.includes("checkpoint")) {
1430
+ authError = new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1431
+ }
1411
1432
  }
1412
1433
  } catch (err) {
1413
1434
  logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery", err);
1435
+ const msg = err instanceof Error ? err.message : String(err);
1436
+ if (msg.includes("401") || msg.includes("403") || msg.includes("login") || msg.includes("not-logged-in") || msg.includes("<!DOCTYPE")) {
1437
+ authError = new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1438
+ }
1414
1439
  }
1415
1440
  const url = `https://www.instagram.com/api/v1/highlights/${userId}/highlights_tray/`;
1416
1441
  try {
@@ -1419,12 +1444,21 @@ var InstagramExtractor = class {
1419
1444
  cookieDomain: "instagram.com",
1420
1445
  signal: options.signal,
1421
1446
  headers: {
1422
- "X-IG-App-ID": IG_APP_ID
1447
+ "X-IG-App-ID": IG_APP_ID,
1448
+ "Referer": "https://www.instagram.com/"
1423
1449
  }
1424
1450
  });
1451
+ if (res.message === "login_required" || res.message === "checkpoint_required") {
1452
+ throw new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1453
+ }
1425
1454
  return res.tray ?? [];
1426
1455
  } catch (err) {
1427
1456
  logExtractorDebug("Highlights legacy REST", err);
1457
+ const msg = err instanceof Error ? err.message : String(err);
1458
+ if (msg.includes("401") || msg.includes("403") || msg.includes("login") || msg.includes("not-logged-in") || msg.includes("<!DOCTYPE") || msg.includes("Unexpected token")) {
1459
+ throw new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1460
+ }
1461
+ if (authError) throw authError;
1428
1462
  return [];
1429
1463
  }
1430
1464
  }
@@ -1438,13 +1472,21 @@ var InstagramExtractor = class {
1438
1472
  cookieDomain: "instagram.com",
1439
1473
  signal: options.signal,
1440
1474
  headers: {
1441
- "X-IG-App-ID": IG_APP_ID
1475
+ "X-IG-App-ID": IG_APP_ID,
1476
+ "Referer": "https://www.instagram.com/"
1442
1477
  }
1443
1478
  });
1479
+ if (res.message === "login_required" || res.message === "checkpoint_required") {
1480
+ throw new Error("Instagram Stories require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1481
+ }
1444
1482
  if (res.reels_media) return res.reels_media;
1445
1483
  if (res.reels) return Object.values(res.reels);
1446
1484
  return [];
1447
- } catch {
1485
+ } catch (err) {
1486
+ const msg = err instanceof Error ? err.message : String(err);
1487
+ if (msg.includes("401") || msg.includes("403") || msg.includes("login") || msg.includes("not-logged-in") || msg.includes("<!DOCTYPE") || msg.includes("Unexpected token")) {
1488
+ throw new Error("Instagram Stories require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1489
+ }
1448
1490
  return [];
1449
1491
  }
1450
1492
  }
@@ -1570,16 +1612,23 @@ var InstagramExtractor = class {
1570
1612
  for (const edge of connection.edges) {
1571
1613
  const node = edge?.node;
1572
1614
  if (node?.owner_id) {
1573
- resolvedUserId = String(node.owner_id);
1574
- break;
1615
+ if (typeof node.owner_id === "object" && node.owner_id !== null) {
1616
+ resolvedUserId = String(node.owner_id.pk || node.owner_id.id || "");
1617
+ } else {
1618
+ resolvedUserId = String(node.owner_id);
1619
+ }
1620
+ if (resolvedUserId && resolvedUserId !== "[object Object]") break;
1575
1621
  }
1576
1622
  if (node?.user?.pk || node?.user?.id) {
1577
1623
  resolvedUserId = String(node.user.pk || node.user.id);
1578
- break;
1624
+ if (resolvedUserId && resolvedUserId !== "[object Object]") break;
1579
1625
  }
1580
1626
  if (typeof node?.id === "string" && node.id.includes("_")) {
1581
- resolvedUserId = node.id.split("_")[1];
1582
- break;
1627
+ const candidate = node.id.split("_")[1];
1628
+ if (candidate && candidate !== "[object Object]") {
1629
+ resolvedUserId = candidate;
1630
+ break;
1631
+ }
1583
1632
  }
1584
1633
  }
1585
1634
  return {
@@ -1739,10 +1788,10 @@ var InstagramExtractor = class {
1739
1788
  logExtractorDebug("fetchWebTokens", err);
1740
1789
  }
1741
1790
  let userId = user?.id || webTokens.userId;
1742
- if (!userId && (subDir === "Stories" || subDir === "Highlights")) {
1791
+ if ((!userId || userId === "[object Object]") && (subDir === "Stories" || subDir === "Highlights")) {
1743
1792
  try {
1744
1793
  const probe = await this.fetchPostsTabGraphQL(username, void 0, webTokens, options);
1745
- if (probe.userId) {
1794
+ if (probe.userId && probe.userId !== "[object Object]") {
1746
1795
  userId = probe.userId;
1747
1796
  }
1748
1797
  } catch (err) {
@@ -1750,7 +1799,9 @@ var InstagramExtractor = class {
1750
1799
  }
1751
1800
  }
1752
1801
  if (subDir === "Stories") {
1753
- if (!userId) return;
1802
+ if (!userId || userId === "[object Object]") {
1803
+ throw new Error(`Could not resolve user ID for @${username}. Profile may be private or unavailable.`);
1804
+ }
1754
1805
  options.onProgress?.({
1755
1806
  stage: "Stories",
1756
1807
  found: 0,
@@ -1766,13 +1817,15 @@ var InstagramExtractor = class {
1766
1817
  return;
1767
1818
  }
1768
1819
  if (subDir === "Highlights") {
1769
- if (!userId) return;
1820
+ if (!userId || userId === "[object Object]") {
1821
+ throw new Error(`Could not resolve user ID for @${username}. Profile may be private or unavailable.`);
1822
+ }
1770
1823
  options.onProgress?.({
1771
1824
  stage: "Highlights",
1772
1825
  found: 0,
1773
1826
  message: `Fetching highlights tray for @${username}...`
1774
1827
  });
1775
- const tray = await this.fetchHighlightsTray(userId, options);
1828
+ const tray = await this.fetchHighlightsTray(userId, options, webTokens);
1776
1829
  const highlightIds = tray.map((t) => t.id).filter(Boolean);
1777
1830
  if (highlightIds.length > 0) {
1778
1831
  const chunkSize = 5;
@@ -2967,11 +3020,12 @@ async function downloadMediaStream(items, options) {
2967
3020
  }
2968
3021
  return arch;
2969
3022
  };
2970
- await getArchiveForDir(destDir);
2971
- if (options.destDirMap) {
3023
+ if (options.destDirMap && Object.values(options.destDirMap).some(Boolean)) {
2972
3024
  for (const d of Object.values(options.destDirMap)) {
2973
3025
  if (d) await getArchiveForDir(d);
2974
3026
  }
3027
+ } else {
3028
+ await getArchiveForDir(destDir);
2975
3029
  }
2976
3030
  let downloaded = 0;
2977
3031
  let skipped = 0;
@@ -3605,7 +3659,7 @@ function buildGalleryDlArgs(options) {
3605
3659
  }
3606
3660
  async function runGalleryDl(options) {
3607
3661
  const { profile, targetDir, subDir, cookiePath, onProgress } = options;
3608
- const destDir = path8.join(targetDir, subDir);
3662
+ const destDir = subDir === "All" ? targetDir : path8.join(targetDir, subDir);
3609
3663
  await fs8.mkdir(destDir, { recursive: true });
3610
3664
  const args = buildGalleryDlArgs({
3611
3665
  destDir,
@@ -3909,6 +3963,30 @@ function resolveDefaultDownloadDir(platform, username, baseDir) {
3909
3963
  const root = resolveDownloadBaseDir(baseDir);
3910
3964
  return path10.join(root, platform, username);
3911
3965
  }
3966
+ async function cleanupEmptyCategoryDirs(targetDir) {
3967
+ const candidateDirs = ["All", "Stories", "Highlights", "Photos", "Videos"];
3968
+ for (const dirName of candidateDirs) {
3969
+ const fullPath = path10.join(targetDir, dirName);
3970
+ try {
3971
+ if (!fsSync.existsSync(fullPath)) continue;
3972
+ const entries = await fs10.readdir(fullPath);
3973
+ const nonArchiveEntries = entries.filter(
3974
+ (e) => e !== ".open-nami-archive.json" && e !== "archive.txt" && e !== "archive_yt-dlp.txt" && e !== "archive_gallery-dl.sqlite3"
3975
+ );
3976
+ if (nonArchiveEntries.length === 0) {
3977
+ for (const entry of entries) {
3978
+ try {
3979
+ await fs10.unlink(path10.join(fullPath, entry));
3980
+ } catch {
3981
+ }
3982
+ }
3983
+ await fs10.rmdir(fullPath).catch(() => {
3984
+ });
3985
+ }
3986
+ } catch {
3987
+ }
3988
+ }
3989
+ }
3912
3990
  async function dispatchDownload(options) {
3913
3991
  const { profile, choice, onProgress } = options;
3914
3992
  const targetDir = options.outputDir || resolveDefaultDownloadDir(profile.platform, profile.username);
@@ -3941,7 +4019,7 @@ async function dispatchDownload(options) {
3941
4019
  const savedCheckpoint = options.resume ? await loadCheckpoint(targetDir) : null;
3942
4020
  const currentCursors = savedCheckpoint?.cursors ? { ...savedCheckpoint.cursors } : {};
3943
4021
  const defaultRunNative = async (subDir) => {
3944
- const dest = path10.join(targetDir, subDir);
4022
+ const dest = subDir === "All" ? targetDir : path10.join(targetDir, subDir);
3945
4023
  const remainingLimit = globalLimit !== void 0 ? Math.max(0, globalLimit - totalDownloaded) : void 0;
3946
4024
  if (remainingLimit === 0) {
3947
4025
  return { success: true, downloaded: 0, skipped: 0 };
@@ -4076,7 +4154,8 @@ async function dispatchDownload(options) {
4076
4154
  } else if (job.type === "photos" || job.type === "stories" || job.type === "highlights") {
4077
4155
  const subDir = job.type === "photos" ? "Photos" : job.type === "stories" ? "Stories" : "Highlights";
4078
4156
  let res = await runNative(subDir);
4079
- if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
4157
+ const isAuthError = Boolean(res.error && (res.error.includes("cookie") || res.error.includes("authenticated session") || res.error.includes("Settings (^s)")));
4158
+ if (!isAuthError && (!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
4080
4159
  onProgress?.({
4081
4160
  job: subDir,
4082
4161
  downloadedCount: totalDownloaded,
@@ -4084,7 +4163,7 @@ async function dispatchDownload(options) {
4084
4163
  statusText: "switching to gallery-dl engine\u2026"
4085
4164
  });
4086
4165
  const gdlRes = await runGdl(subDir);
4087
- if (gdlRes.downloaded > 0 || gdlRes.skipped > 0 || gdlRes.success) {
4166
+ if (gdlRes.downloaded > 0 || gdlRes.skipped > 0 || gdlRes.success && !res.error) {
4088
4167
  res = gdlRes;
4089
4168
  }
4090
4169
  }
@@ -4123,6 +4202,7 @@ async function dispatchDownload(options) {
4123
4202
  }
4124
4203
  }
4125
4204
  }
4205
+ await cleanupEmptyCategoryDirs(targetDir);
4126
4206
  if (errors.length === 0 && !options.signal?.aborted) {
4127
4207
  await clearCheckpoint(targetDir);
4128
4208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-nami",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
5
5
  "type": "module",
6
6
  "bin": {