open-nami 1.2.0 → 1.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.
Files changed (2) hide show
  1. package/dist/cli.js +228 -53
  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.2",
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";
@@ -1210,6 +1211,9 @@ var IG_QUERIES = {
1210
1211
  PolarisProfilePageContentQuery: "28649589871350527"
1211
1212
  };
1212
1213
  var IG_POLARIS_DOC_ID = IG_QUERIES.PolarisProfilePostsTabContentQuery_connection;
1214
+ function isExplicitAuthError(msg) {
1215
+ return msg.includes("401") || msg.includes("403") || msg.includes("login_required") || msg.includes("checkpoint_required") || msg.includes("not-logged-in");
1216
+ }
1213
1217
  function logExtractorDebug(endpoint, err) {
1214
1218
  if (process.env.DEBUG || process.env.OPEN_NAMI_DEBUG) {
1215
1219
  const message = err instanceof Error ? err.message : String(err);
@@ -1378,39 +1382,63 @@ var InstagramExtractor = class {
1378
1382
  }
1379
1383
  return user;
1380
1384
  }
1381
- async fetchHighlightsTray(userId, options) {
1385
+ async fetchHighlightsTray(userId, options, tokens) {
1386
+ let authError = null;
1382
1387
  try {
1383
1388
  const variables = JSON.stringify({ user_id: userId });
1384
- const body = new URLSearchParams({
1385
- doc_id: IG_QUERIES.PolarisProfileStoryHighlightsTrayContentQuery,
1386
- variables
1387
- }).toString();
1389
+ const bodyParams = new URLSearchParams({
1390
+ av: "17841415137994167",
1391
+ __d: "www",
1392
+ __user: "0",
1393
+ __a: "1",
1394
+ __req: "0",
1395
+ dpr: "1",
1396
+ fb_api_caller_class: "RelayModern",
1397
+ fb_api_req_friendly_name: "PolarisProfileStoryHighlightsTrayContentQuery",
1398
+ server_timestamps: "true",
1399
+ variables,
1400
+ doc_id: IG_QUERIES.PolarisProfileStoryHighlightsTrayContentQuery
1401
+ });
1402
+ if (tokens?.fbDtsg) bodyParams.set("fb_dtsg", tokens.fbDtsg);
1403
+ if (tokens?.lsd) bodyParams.set("lsd", tokens.lsd);
1404
+ const headers = {
1405
+ "Accept": "*/*",
1406
+ "Content-Type": "application/x-www-form-urlencoded",
1407
+ "X-IG-App-ID": IG_APP_ID,
1408
+ "X-FB-Friendly-Name": "PolarisProfileStoryHighlightsTrayContentQuery",
1409
+ "Referer": "https://www.instagram.com/",
1410
+ "Origin": "https://www.instagram.com",
1411
+ "X-ASBD-ID": "359341"
1412
+ };
1413
+ if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
1414
+ if (tokens?.lsd) headers["X-FB-LSD"] = tokens.lsd;
1388
1415
  const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
1389
1416
  method: "POST",
1390
- body,
1417
+ body: bodyParams.toString(),
1391
1418
  cookiePath: options.cookiePath,
1392
1419
  cookieDomain: "instagram.com",
1393
1420
  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
- }
1421
+ headers
1400
1422
  });
1401
1423
  const edges = res?.data?.highlights?.edges;
1402
- if (Array.isArray(edges) && edges.length > 0) {
1424
+ if (Array.isArray(edges)) {
1403
1425
  if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
1404
1426
  logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery partial field errors", res.errors);
1405
1427
  }
1406
- return edges.map((e) => e.node);
1428
+ return edges.map((e) => e.node).filter(Boolean);
1407
1429
  }
1408
1430
  if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
1409
1431
  const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
1410
- throw new Error(`Instagram GraphQL query error: ${errMsg}`);
1432
+ if (isExplicitAuthError(errMsg)) {
1433
+ authError = new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1434
+ }
1411
1435
  }
1412
1436
  } catch (err) {
1413
1437
  logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery", err);
1438
+ const msg = err instanceof Error ? err.message : String(err);
1439
+ if (isExplicitAuthError(msg)) {
1440
+ authError = new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1441
+ }
1414
1442
  }
1415
1443
  const url = `https://www.instagram.com/api/v1/highlights/${userId}/highlights_tray/`;
1416
1444
  try {
@@ -1419,17 +1447,84 @@ var InstagramExtractor = class {
1419
1447
  cookieDomain: "instagram.com",
1420
1448
  signal: options.signal,
1421
1449
  headers: {
1422
- "X-IG-App-ID": IG_APP_ID
1450
+ "X-IG-App-ID": IG_APP_ID,
1451
+ "Referer": "https://www.instagram.com/"
1423
1452
  }
1424
1453
  });
1425
- return res.tray ?? [];
1454
+ if (res?.message === "login_required" || res?.message === "checkpoint_required") {
1455
+ throw new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1456
+ }
1457
+ return res?.tray ?? [];
1426
1458
  } catch (err) {
1427
1459
  logExtractorDebug("Highlights legacy REST", err);
1460
+ const msg = err instanceof Error ? err.message : String(err);
1461
+ if (isExplicitAuthError(msg)) {
1462
+ throw new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1463
+ }
1464
+ if (authError) throw authError;
1428
1465
  return [];
1429
1466
  }
1430
1467
  }
1431
- async fetchReelsMedia(reelIds, options) {
1468
+ async fetchReelsMediaGraphQL(reelIds, options, tokens) {
1432
1469
  if (reelIds.length === 0) return [];
1470
+ try {
1471
+ const variables = JSON.stringify({
1472
+ initial_reel_id: reelIds[0],
1473
+ reel_ids: reelIds,
1474
+ first: 3,
1475
+ last: 2,
1476
+ __relay_internal__pv__PolarisCommunityNoteStoriesLabelEnabledrelayprovider: true
1477
+ });
1478
+ const bodyParams = new URLSearchParams({
1479
+ av: "17841415137994167",
1480
+ __d: "www",
1481
+ __user: "0",
1482
+ __a: "1",
1483
+ __req: "0",
1484
+ dpr: "1",
1485
+ fb_api_caller_class: "RelayModern",
1486
+ fb_api_req_friendly_name: "PolarisStoriesV3ReelPageGalleryQuery",
1487
+ server_timestamps: "true",
1488
+ variables,
1489
+ doc_id: IG_QUERIES.PolarisStoriesV3ReelPageGalleryQuery
1490
+ });
1491
+ if (tokens?.fbDtsg) bodyParams.set("fb_dtsg", tokens.fbDtsg);
1492
+ if (tokens?.lsd) bodyParams.set("lsd", tokens.lsd);
1493
+ const headers = {
1494
+ "Accept": "*/*",
1495
+ "Content-Type": "application/x-www-form-urlencoded",
1496
+ "X-IG-App-ID": IG_APP_ID,
1497
+ "X-FB-Friendly-Name": "PolarisStoriesV3ReelPageGalleryQuery",
1498
+ "Referer": "https://www.instagram.com/",
1499
+ "Origin": "https://www.instagram.com",
1500
+ "X-ASBD-ID": "359341"
1501
+ };
1502
+ if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
1503
+ if (tokens?.lsd) headers["X-FB-LSD"] = tokens.lsd;
1504
+ const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
1505
+ method: "POST",
1506
+ body: bodyParams.toString(),
1507
+ cookiePath: options.cookiePath,
1508
+ cookieDomain: "instagram.com",
1509
+ signal: options.signal,
1510
+ headers
1511
+ });
1512
+ const connection = res?.data?.xdt_api__v1__feed__reels_media__connection;
1513
+ const edges = connection?.edges;
1514
+ if (Array.isArray(edges) && edges.length > 0) {
1515
+ return edges.map((e) => e.node).filter(Boolean);
1516
+ }
1517
+ } catch (err) {
1518
+ logExtractorDebug("PolarisStoriesV3ReelPageGalleryQuery", err);
1519
+ }
1520
+ return [];
1521
+ }
1522
+ async fetchReelsMedia(reelIds, options, tokens) {
1523
+ if (reelIds.length === 0) return [];
1524
+ const gqlReels = await this.fetchReelsMediaGraphQL(reelIds, options, tokens);
1525
+ if (gqlReels.length > 0) {
1526
+ return gqlReels;
1527
+ }
1433
1528
  const idsParam = reelIds.join(",");
1434
1529
  const url = `https://www.instagram.com/api/v1/feed/reels_media/?reel_ids=${encodeURIComponent(idsParam)}`;
1435
1530
  try {
@@ -1438,13 +1533,22 @@ var InstagramExtractor = class {
1438
1533
  cookieDomain: "instagram.com",
1439
1534
  signal: options.signal,
1440
1535
  headers: {
1441
- "X-IG-App-ID": IG_APP_ID
1536
+ "X-IG-App-ID": IG_APP_ID,
1537
+ "Referer": "https://www.instagram.com/"
1442
1538
  }
1443
1539
  });
1444
- if (res.reels_media) return res.reels_media;
1445
- if (res.reels) return Object.values(res.reels);
1540
+ if (res?.message === "login_required" || res?.message === "checkpoint_required") {
1541
+ throw new Error("Instagram Stories require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1542
+ }
1543
+ if (res?.reels_media) return res.reels_media;
1544
+ if (res?.reels) return Object.values(res.reels);
1446
1545
  return [];
1447
- } catch {
1546
+ } catch (err) {
1547
+ const msg = err instanceof Error ? err.message : String(err);
1548
+ if (isExplicitAuthError(msg)) {
1549
+ throw new Error("Instagram Stories require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
1550
+ }
1551
+ logExtractorDebug("Reels media legacy REST", err);
1448
1552
  return [];
1449
1553
  }
1450
1554
  }
@@ -1570,16 +1674,23 @@ var InstagramExtractor = class {
1570
1674
  for (const edge of connection.edges) {
1571
1675
  const node = edge?.node;
1572
1676
  if (node?.owner_id) {
1573
- resolvedUserId = String(node.owner_id);
1574
- break;
1677
+ if (typeof node.owner_id === "object" && node.owner_id !== null) {
1678
+ resolvedUserId = String(node.owner_id.pk || node.owner_id.id || "");
1679
+ } else {
1680
+ resolvedUserId = String(node.owner_id);
1681
+ }
1682
+ if (resolvedUserId && resolvedUserId !== "[object Object]") break;
1575
1683
  }
1576
1684
  if (node?.user?.pk || node?.user?.id) {
1577
1685
  resolvedUserId = String(node.user.pk || node.user.id);
1578
- break;
1686
+ if (resolvedUserId && resolvedUserId !== "[object Object]") break;
1579
1687
  }
1580
1688
  if (typeof node?.id === "string" && node.id.includes("_")) {
1581
- resolvedUserId = node.id.split("_")[1];
1582
- break;
1689
+ const candidate = node.id.split("_")[1];
1690
+ if (candidate && candidate !== "[object Object]") {
1691
+ resolvedUserId = candidate;
1692
+ break;
1693
+ }
1583
1694
  }
1584
1695
  }
1585
1696
  return {
@@ -1739,10 +1850,10 @@ var InstagramExtractor = class {
1739
1850
  logExtractorDebug("fetchWebTokens", err);
1740
1851
  }
1741
1852
  let userId = user?.id || webTokens.userId;
1742
- if (!userId && (subDir === "Stories" || subDir === "Highlights")) {
1853
+ if ((!userId || userId === "[object Object]") && (subDir === "Stories" || subDir === "Highlights")) {
1743
1854
  try {
1744
1855
  const probe = await this.fetchPostsTabGraphQL(username, void 0, webTokens, options);
1745
- if (probe.userId) {
1856
+ if (probe.userId && probe.userId !== "[object Object]") {
1746
1857
  userId = probe.userId;
1747
1858
  }
1748
1859
  } catch (err) {
@@ -1750,44 +1861,81 @@ var InstagramExtractor = class {
1750
1861
  }
1751
1862
  }
1752
1863
  if (subDir === "Stories") {
1753
- if (!userId) return;
1864
+ if (!userId || userId === "[object Object]") {
1865
+ throw new Error(`Could not resolve user ID for @${username}. Profile may be private or unavailable.`);
1866
+ }
1867
+ if (user?.latest_reel_media === 0) {
1868
+ options.onProgress?.({
1869
+ stage: "Stories",
1870
+ found: 0,
1871
+ message: `Profile @${username} has no active stories in the last 24 hours.`
1872
+ });
1873
+ return;
1874
+ }
1754
1875
  options.onProgress?.({
1755
1876
  stage: "Stories",
1756
1877
  found: 0,
1757
1878
  message: `Fetching stories for @${username}...`
1758
1879
  });
1759
- const reels = await this.fetchReelsMedia([userId], options);
1880
+ const reels = await this.fetchReelsMedia([userId], options, webTokens);
1881
+ let count = 0;
1760
1882
  for (const reel of reels) {
1761
1883
  for (const rawItem of reel.items || []) {
1762
1884
  const item = parseReelItemToMediaItem(rawItem, username);
1763
- if (item) yield item;
1885
+ if (item) {
1886
+ count++;
1887
+ yield item;
1888
+ }
1764
1889
  }
1765
1890
  }
1891
+ if (count === 0) {
1892
+ options.onProgress?.({
1893
+ stage: "Stories",
1894
+ found: 0,
1895
+ message: `Profile @${username} has no active stories in the last 24 hours.`
1896
+ });
1897
+ }
1766
1898
  return;
1767
1899
  }
1768
1900
  if (subDir === "Highlights") {
1769
- if (!userId) return;
1901
+ if (!userId || userId === "[object Object]") {
1902
+ throw new Error(`Could not resolve user ID for @${username}. Profile may be private or unavailable.`);
1903
+ }
1904
+ if (user?.highlight_reel_count === 0) {
1905
+ options.onProgress?.({
1906
+ stage: "Highlights",
1907
+ found: 0,
1908
+ message: `Profile @${username} has no available highlights.`
1909
+ });
1910
+ return;
1911
+ }
1770
1912
  options.onProgress?.({
1771
1913
  stage: "Highlights",
1772
1914
  found: 0,
1773
1915
  message: `Fetching highlights tray for @${username}...`
1774
1916
  });
1775
- const tray = await this.fetchHighlightsTray(userId, options);
1917
+ const tray = await this.fetchHighlightsTray(userId, options, webTokens);
1776
1918
  const highlightIds = tray.map((t) => t.id).filter(Boolean);
1777
- if (highlightIds.length > 0) {
1778
- const chunkSize = 5;
1779
- for (let i = 0; i < highlightIds.length; i += chunkSize) {
1780
- if (options.signal?.aborted) break;
1781
- const chunk = highlightIds.slice(i, i + chunkSize);
1782
- const reels = await this.fetchReelsMedia(chunk, options);
1783
- for (const reel of reels) {
1784
- for (const rawItem of reel.items || []) {
1785
- const item = parseReelItemToMediaItem(rawItem, username);
1786
- if (item) yield item;
1787
- }
1919
+ if (highlightIds.length === 0) {
1920
+ options.onProgress?.({
1921
+ stage: "Highlights",
1922
+ found: 0,
1923
+ message: `Profile @${username} has no available highlights.`
1924
+ });
1925
+ return;
1926
+ }
1927
+ const chunkSize = 5;
1928
+ for (let i = 0; i < highlightIds.length; i += chunkSize) {
1929
+ if (options.signal?.aborted) break;
1930
+ const chunk = highlightIds.slice(i, i + chunkSize);
1931
+ const reels = await this.fetchReelsMedia(chunk, options, webTokens);
1932
+ for (const reel of reels) {
1933
+ for (const rawItem of reel.items || []) {
1934
+ const item = parseReelItemToMediaItem(rawItem, username);
1935
+ if (item) yield item;
1788
1936
  }
1789
- await sleep(1e3, 1500, options.signal);
1790
1937
  }
1938
+ await sleep(1e3, 1500, options.signal);
1791
1939
  }
1792
1940
  return;
1793
1941
  }
@@ -2967,11 +3115,12 @@ async function downloadMediaStream(items, options) {
2967
3115
  }
2968
3116
  return arch;
2969
3117
  };
2970
- await getArchiveForDir(destDir);
2971
- if (options.destDirMap) {
3118
+ if (options.destDirMap && Object.values(options.destDirMap).some(Boolean)) {
2972
3119
  for (const d of Object.values(options.destDirMap)) {
2973
3120
  if (d) await getArchiveForDir(d);
2974
3121
  }
3122
+ } else {
3123
+ await getArchiveForDir(destDir);
2975
3124
  }
2976
3125
  let downloaded = 0;
2977
3126
  let skipped = 0;
@@ -3605,7 +3754,7 @@ function buildGalleryDlArgs(options) {
3605
3754
  }
3606
3755
  async function runGalleryDl(options) {
3607
3756
  const { profile, targetDir, subDir, cookiePath, onProgress } = options;
3608
- const destDir = path8.join(targetDir, subDir);
3757
+ const destDir = subDir === "All" ? targetDir : path8.join(targetDir, subDir);
3609
3758
  await fs8.mkdir(destDir, { recursive: true });
3610
3759
  const args = buildGalleryDlArgs({
3611
3760
  destDir,
@@ -3909,6 +4058,30 @@ function resolveDefaultDownloadDir(platform, username, baseDir) {
3909
4058
  const root = resolveDownloadBaseDir(baseDir);
3910
4059
  return path10.join(root, platform, username);
3911
4060
  }
4061
+ async function cleanupEmptyCategoryDirs(targetDir) {
4062
+ const candidateDirs = ["All", "Stories", "Highlights", "Photos", "Videos"];
4063
+ for (const dirName of candidateDirs) {
4064
+ const fullPath = path10.join(targetDir, dirName);
4065
+ try {
4066
+ if (!fsSync.existsSync(fullPath)) continue;
4067
+ const entries = await fs10.readdir(fullPath);
4068
+ const nonArchiveEntries = entries.filter(
4069
+ (e) => e !== ".open-nami-archive.json" && e !== "archive.txt" && e !== "archive_yt-dlp.txt" && e !== "archive_gallery-dl.sqlite3"
4070
+ );
4071
+ if (nonArchiveEntries.length === 0) {
4072
+ for (const entry of entries) {
4073
+ try {
4074
+ await fs10.unlink(path10.join(fullPath, entry));
4075
+ } catch {
4076
+ }
4077
+ }
4078
+ await fs10.rmdir(fullPath).catch(() => {
4079
+ });
4080
+ }
4081
+ } catch {
4082
+ }
4083
+ }
4084
+ }
3912
4085
  async function dispatchDownload(options) {
3913
4086
  const { profile, choice, onProgress } = options;
3914
4087
  const targetDir = options.outputDir || resolveDefaultDownloadDir(profile.platform, profile.username);
@@ -3941,7 +4114,7 @@ async function dispatchDownload(options) {
3941
4114
  const savedCheckpoint = options.resume ? await loadCheckpoint(targetDir) : null;
3942
4115
  const currentCursors = savedCheckpoint?.cursors ? { ...savedCheckpoint.cursors } : {};
3943
4116
  const defaultRunNative = async (subDir) => {
3944
- const dest = path10.join(targetDir, subDir);
4117
+ const dest = subDir === "All" ? targetDir : path10.join(targetDir, subDir);
3945
4118
  const remainingLimit = globalLimit !== void 0 ? Math.max(0, globalLimit - totalDownloaded) : void 0;
3946
4119
  if (remainingLimit === 0) {
3947
4120
  return { success: true, downloaded: 0, skipped: 0 };
@@ -4076,7 +4249,8 @@ async function dispatchDownload(options) {
4076
4249
  } else if (job.type === "photos" || job.type === "stories" || job.type === "highlights") {
4077
4250
  const subDir = job.type === "photos" ? "Photos" : job.type === "stories" ? "Stories" : "Highlights";
4078
4251
  let res = await runNative(subDir);
4079
- if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
4252
+ const isAuthError = Boolean(res.error && (res.error.includes("cookie") || res.error.includes("authenticated session") || res.error.includes("Settings (^s)")));
4253
+ if (!isAuthError && (!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
4080
4254
  onProgress?.({
4081
4255
  job: subDir,
4082
4256
  downloadedCount: totalDownloaded,
@@ -4084,7 +4258,7 @@ async function dispatchDownload(options) {
4084
4258
  statusText: "switching to gallery-dl engine\u2026"
4085
4259
  });
4086
4260
  const gdlRes = await runGdl(subDir);
4087
- if (gdlRes.downloaded > 0 || gdlRes.skipped > 0 || gdlRes.success) {
4261
+ if (gdlRes.downloaded > 0 || gdlRes.skipped > 0 || gdlRes.success && !res.error) {
4088
4262
  res = gdlRes;
4089
4263
  }
4090
4264
  }
@@ -4123,6 +4297,7 @@ async function dispatchDownload(options) {
4123
4297
  }
4124
4298
  }
4125
4299
  }
4300
+ await cleanupEmptyCategoryDirs(targetDir);
4126
4301
  if (errors.length === 0 && !options.signal?.aborted) {
4127
4302
  await clearCheckpoint(targetDir);
4128
4303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-nami",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
5
5
  "type": "module",
6
6
  "bin": {