open-nami 1.3.1 → 1.5.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.
- package/dist/cli.js +1078 -317
- 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.
|
|
12
|
+
version: "1.5.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: {
|
|
@@ -73,11 +73,11 @@ var require_package = __commonJS({
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
// src/cli.tsx
|
|
76
|
-
import { createRequire } from "module";
|
|
76
|
+
import { createRequire as createRequire2 } from "module";
|
|
77
77
|
import { render } from "ink";
|
|
78
78
|
|
|
79
79
|
// src/app.tsx
|
|
80
|
-
import { useState as
|
|
80
|
+
import { useState as useState10, useCallback as useCallback4, useRef as useRef5, useEffect as useEffect10, useMemo as useMemo3 } from "react";
|
|
81
81
|
import { Box as Box15, Text as Text17, useApp, useInput as useInput5, useStdout as useStdout3 } from "ink";
|
|
82
82
|
import Spinner4 from "ink-spinner";
|
|
83
83
|
|
|
@@ -730,16 +730,8 @@ async function clearCheckpoint(destDir) {
|
|
|
730
730
|
}
|
|
731
731
|
|
|
732
732
|
// src/lib/engines/dispatcher.ts
|
|
733
|
-
import
|
|
734
|
-
import
|
|
735
|
-
import fs10 from "fs/promises";
|
|
736
|
-
import fsSync from "fs";
|
|
737
|
-
|
|
738
|
-
// src/lib/cookies.ts
|
|
739
|
-
import fs2 from "fs";
|
|
740
|
-
import fsPromises2 from "fs/promises";
|
|
741
|
-
import path3 from "path";
|
|
742
|
-
import os2 from "os";
|
|
733
|
+
import path13 from "path";
|
|
734
|
+
import fs13 from "fs/promises";
|
|
743
735
|
|
|
744
736
|
// src/lib/config.ts
|
|
745
737
|
import fs from "fs";
|
|
@@ -757,9 +749,13 @@ function loadConfig(configPath = getDefaultConfigPath()) {
|
|
|
757
749
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
758
750
|
const parsed = JSON.parse(raw);
|
|
759
751
|
if (parsed && typeof parsed === "object") {
|
|
752
|
+
const rawBrowser = typeof parsed.cookiesFromBrowser === "string" ? parsed.cookiesFromBrowser.trim().toLowerCase() : void 0;
|
|
753
|
+
const validBrowsers = ["chrome", "edge", "brave", "firefox", "none"];
|
|
754
|
+
const cookiesFromBrowser = rawBrowser && validBrowsers.includes(rawBrowser) ? rawBrowser : void 0;
|
|
760
755
|
return {
|
|
761
756
|
theme: typeof parsed.theme === "string" ? parsed.theme : void 0,
|
|
762
757
|
cookiesDir: typeof parsed.cookiesDir === "string" ? parsed.cookiesDir.trim() : void 0,
|
|
758
|
+
cookiesFromBrowser,
|
|
763
759
|
downloadDir: typeof parsed.downloadDir === "string" ? parsed.downloadDir.trim() : void 0,
|
|
764
760
|
writeMetadata: typeof parsed.writeMetadata === "boolean" ? parsed.writeMetadata : void 0,
|
|
765
761
|
defaultLimit: typeof parsed.defaultLimit === "number" ? parsed.defaultLimit : void 0
|
|
@@ -781,22 +777,77 @@ function saveConfig(config, configPath = getDefaultConfigPath()) {
|
|
|
781
777
|
}
|
|
782
778
|
}
|
|
783
779
|
|
|
780
|
+
// src/lib/cookies.ts
|
|
781
|
+
import fs3 from "fs";
|
|
782
|
+
import fsPromises2 from "fs/promises";
|
|
783
|
+
import path4 from "path";
|
|
784
|
+
import os3 from "os";
|
|
785
|
+
|
|
786
|
+
// src/lib/cookies/browser-bridge.ts
|
|
787
|
+
import fs2 from "fs";
|
|
788
|
+
import path3 from "path";
|
|
789
|
+
import os2 from "os";
|
|
790
|
+
function listDetectedBrowsers() {
|
|
791
|
+
const isWin = process.platform === "win32";
|
|
792
|
+
const isMac = process.platform === "darwin";
|
|
793
|
+
const home = os2.homedir();
|
|
794
|
+
const candidates = [
|
|
795
|
+
{
|
|
796
|
+
id: "chrome",
|
|
797
|
+
name: "Google Chrome",
|
|
798
|
+
winDir: path3.join(process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"), "Google", "Chrome", "User Data"),
|
|
799
|
+
macDir: path3.join(home, "Library", "Application Support", "Google", "Chrome"),
|
|
800
|
+
linuxDir: path3.join(home, ".config", "google-chrome")
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
id: "edge",
|
|
804
|
+
name: "Microsoft Edge",
|
|
805
|
+
winDir: path3.join(process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"), "Microsoft", "Edge", "User Data"),
|
|
806
|
+
macDir: path3.join(home, "Library", "Application Support", "Microsoft Edge"),
|
|
807
|
+
linuxDir: path3.join(home, ".config", "microsoft-edge")
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
id: "brave",
|
|
811
|
+
name: "Brave Browser",
|
|
812
|
+
winDir: path3.join(process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local"), "BraveSoftware", "Brave-Browser", "User Data"),
|
|
813
|
+
macDir: path3.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
|
|
814
|
+
linuxDir: path3.join(home, ".config", "BraveSoftware", "Brave-Browser")
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
id: "firefox",
|
|
818
|
+
name: "Mozilla Firefox",
|
|
819
|
+
winDir: path3.join(process.env.APPDATA || path3.join(home, "AppData", "Roaming"), "Mozilla", "Firefox", "Profiles"),
|
|
820
|
+
macDir: path3.join(home, "Library", "Application Support", "Firefox", "Profiles"),
|
|
821
|
+
linuxDir: path3.join(home, ".mozilla", "firefox")
|
|
822
|
+
}
|
|
823
|
+
];
|
|
824
|
+
return candidates.map((c) => {
|
|
825
|
+
const profileDir = isWin ? c.winDir : isMac ? c.macDir : c.linuxDir;
|
|
826
|
+
return {
|
|
827
|
+
id: c.id,
|
|
828
|
+
name: c.name,
|
|
829
|
+
profileDir,
|
|
830
|
+
available: fs2.existsSync(profileDir)
|
|
831
|
+
};
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
784
835
|
// src/lib/cookies.ts
|
|
785
836
|
var CANDIDATE_DIRS = [
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
837
|
+
path4.join(os3.homedir(), ".open-nami", "cookies"),
|
|
838
|
+
path4.join(os3.homedir(), ".nami", "cookies"),
|
|
839
|
+
path4.join(process.cwd(), "cookies")
|
|
789
840
|
];
|
|
790
841
|
function getCandidateDirs(customDir) {
|
|
791
842
|
const dirs = [];
|
|
792
843
|
const configured = loadConfig().cookiesDir;
|
|
793
844
|
const custom = customDir || process.env.OPEN_NAMI_COOKIES_DIR || process.env.NAMI_COOKIES_DIR || configured;
|
|
794
845
|
if (custom && custom.trim()) {
|
|
795
|
-
dirs.push(
|
|
846
|
+
dirs.push(path4.resolve(custom.trim()));
|
|
796
847
|
}
|
|
797
|
-
const seen = new Set(dirs.map((d) =>
|
|
848
|
+
const seen = new Set(dirs.map((d) => path4.normalize(d).toLowerCase()));
|
|
798
849
|
for (const dir of CANDIDATE_DIRS) {
|
|
799
|
-
const norm =
|
|
850
|
+
const norm = path4.normalize(dir).toLowerCase();
|
|
800
851
|
if (!seen.has(norm)) {
|
|
801
852
|
seen.add(norm);
|
|
802
853
|
dirs.push(dir);
|
|
@@ -816,12 +867,12 @@ function findCookieFile(platform, customDir) {
|
|
|
816
867
|
const filenames = domainNames[platform] || [];
|
|
817
868
|
const candidateDirs = getCandidateDirs(customDir);
|
|
818
869
|
for (const dir of candidateDirs) {
|
|
819
|
-
if (!
|
|
870
|
+
if (!fs3.existsSync(dir)) continue;
|
|
820
871
|
for (const file of filenames) {
|
|
821
|
-
const fullPath =
|
|
822
|
-
if (
|
|
872
|
+
const fullPath = path4.join(dir, file);
|
|
873
|
+
if (fs3.existsSync(fullPath)) {
|
|
823
874
|
try {
|
|
824
|
-
const stats =
|
|
875
|
+
const stats = fs3.statSync(fullPath);
|
|
825
876
|
if (stats.size > 20) {
|
|
826
877
|
return fullPath;
|
|
827
878
|
}
|
|
@@ -837,14 +888,14 @@ function detectAvailableCookies(customDir) {
|
|
|
837
888
|
return platforms.filter((p) => Boolean(findCookieFile(p, customDir)));
|
|
838
889
|
}
|
|
839
890
|
async function createSafeCookieCopy(masterPath) {
|
|
840
|
-
const userTempDir =
|
|
891
|
+
const userTempDir = path4.join(os3.homedir(), ".open-nami", "temp");
|
|
841
892
|
let targetDir = userTempDir;
|
|
842
893
|
try {
|
|
843
894
|
await fsPromises2.mkdir(userTempDir, { recursive: true, mode: 448 });
|
|
844
895
|
} catch {
|
|
845
|
-
targetDir =
|
|
896
|
+
targetDir = os3.tmpdir();
|
|
846
897
|
}
|
|
847
|
-
const tempPath =
|
|
898
|
+
const tempPath = path4.join(targetDir, `nami_cookie_${Date.now()}_${Math.random().toString(36).slice(2)}.txt`);
|
|
848
899
|
await fsPromises2.copyFile(masterPath, tempPath);
|
|
849
900
|
await fsPromises2.chmod(tempPath, 384).catch(() => {
|
|
850
901
|
});
|
|
@@ -860,141 +911,6 @@ async function createSafeCookieCopy(masterPath) {
|
|
|
860
911
|
};
|
|
861
912
|
}
|
|
862
913
|
|
|
863
|
-
// src/lib/known-folders.ts
|
|
864
|
-
import { execFileSync } from "child_process";
|
|
865
|
-
import fs3 from "fs";
|
|
866
|
-
import os3 from "os";
|
|
867
|
-
import path4 from "path";
|
|
868
|
-
var KNOWN_FOLDER_DOWNLOADS_GUID = "{374DE290-123F-4565-9164-39C4925E467B}";
|
|
869
|
-
var USER_SHELL_FOLDERS_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders";
|
|
870
|
-
function expandWindowsEnv(str, env = process.env, homeDir) {
|
|
871
|
-
return str.replace(/%([^%]+)%/g, (original, name) => {
|
|
872
|
-
if (homeDir && name.toLowerCase() === "userprofile") {
|
|
873
|
-
return homeDir;
|
|
874
|
-
}
|
|
875
|
-
const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
876
|
-
if (key && env[key] !== void 0) {
|
|
877
|
-
return env[key];
|
|
878
|
-
}
|
|
879
|
-
return original;
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
function parseWindowsRegistryOutput(output, targetKey = KNOWN_FOLDER_DOWNLOADS_GUID) {
|
|
883
|
-
const lines = output.split(/\r?\n/);
|
|
884
|
-
const escaped = targetKey.replace(/[{}]/g, "\\$&");
|
|
885
|
-
const pattern = new RegExp(`^\\s*${escaped}\\s+REG_\\w+\\s+(.*)$`, "i");
|
|
886
|
-
for (const line of lines) {
|
|
887
|
-
const match = line.match(pattern);
|
|
888
|
-
if (match && match[1]) {
|
|
889
|
-
const rawPath = match[1].trim();
|
|
890
|
-
if (rawPath) return rawPath;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return void 0;
|
|
894
|
-
}
|
|
895
|
-
function parseLinuxUserDirsOutput(content) {
|
|
896
|
-
const lines = content.split(/\r?\n/);
|
|
897
|
-
for (const line of lines) {
|
|
898
|
-
const trimmed = line.trim();
|
|
899
|
-
if (trimmed.startsWith("#")) continue;
|
|
900
|
-
const match = trimmed.match(/^XDG_DOWNLOAD_DIR=["']?([^"']+)["']?$/);
|
|
901
|
-
if (match && match[1]) {
|
|
902
|
-
return match[1].trim();
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
return void 0;
|
|
906
|
-
}
|
|
907
|
-
function isPathDriveAccessible(filePath, fsExists = fs3.existsSync, platform = process.platform) {
|
|
908
|
-
try {
|
|
909
|
-
if (fsExists(filePath)) {
|
|
910
|
-
return true;
|
|
911
|
-
}
|
|
912
|
-
if (platform === "win32") {
|
|
913
|
-
const match = filePath.match(/^([a-zA-Z]:[\\/])/i);
|
|
914
|
-
const driveRoot = match ? match[1] : path4.win32.parse(path4.win32.resolve(filePath)).root;
|
|
915
|
-
return fsExists(driveRoot);
|
|
916
|
-
}
|
|
917
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
918
|
-
const mountMatch = normalized.match(/^(\/(?:media(?:\/[^/]+)?|mnt|run\/media(?:\/[^/]+)?)\/[^/]+)/);
|
|
919
|
-
if (mountMatch) {
|
|
920
|
-
return fsExists(mountMatch[1]);
|
|
921
|
-
}
|
|
922
|
-
let curr = path4.posix.dirname(normalized);
|
|
923
|
-
while (curr && curr !== "/") {
|
|
924
|
-
if (fsExists(curr)) return true;
|
|
925
|
-
curr = path4.posix.dirname(curr);
|
|
926
|
-
}
|
|
927
|
-
return fsExists("/");
|
|
928
|
-
} catch {
|
|
929
|
-
return false;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
function resolveWindowsDownloadsDir(options = {}) {
|
|
933
|
-
const homeDir = options.homeDir ?? os3.homedir();
|
|
934
|
-
const env = options.env ?? process.env;
|
|
935
|
-
const runQuery = options.regQueryFn ?? ((key, value) => {
|
|
936
|
-
try {
|
|
937
|
-
return execFileSync("reg", ["query", key, "/v", value], {
|
|
938
|
-
encoding: "utf8",
|
|
939
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
940
|
-
});
|
|
941
|
-
} catch {
|
|
942
|
-
return "";
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs3.existsSync, "win32"));
|
|
946
|
-
let raw = parseWindowsRegistryOutput(
|
|
947
|
-
runQuery(USER_SHELL_FOLDERS_REG_KEY, KNOWN_FOLDER_DOWNLOADS_GUID),
|
|
948
|
-
KNOWN_FOLDER_DOWNLOADS_GUID
|
|
949
|
-
);
|
|
950
|
-
if (!raw) {
|
|
951
|
-
raw = parseWindowsRegistryOutput(runQuery(USER_SHELL_FOLDERS_REG_KEY, "Downloads"), "Downloads");
|
|
952
|
-
}
|
|
953
|
-
if (raw) {
|
|
954
|
-
const expanded = expandWindowsEnv(raw, env, homeDir);
|
|
955
|
-
if (checkAccessible(expanded)) {
|
|
956
|
-
return path4.win32.resolve(expanded);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
return void 0;
|
|
960
|
-
}
|
|
961
|
-
function resolveLinuxDownloadsDir(options = {}) {
|
|
962
|
-
const homeDir = options.homeDir ?? os3.homedir();
|
|
963
|
-
const env = options.env ?? process.env;
|
|
964
|
-
const readFn = options.readFileFn ?? ((p) => {
|
|
965
|
-
try {
|
|
966
|
-
return fs3.readFileSync(p, "utf8");
|
|
967
|
-
} catch {
|
|
968
|
-
return "";
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs3.existsSync, "linux"));
|
|
972
|
-
const configHome = env.XDG_CONFIG_HOME || path4.join(homeDir, ".config");
|
|
973
|
-
const userDirsFile = path4.join(configHome, "user-dirs.dirs");
|
|
974
|
-
const content = readFn(userDirsFile);
|
|
975
|
-
if (!content) return void 0;
|
|
976
|
-
const raw = parseLinuxUserDirsOutput(content);
|
|
977
|
-
if (!raw) return void 0;
|
|
978
|
-
const expanded = raw.replace(/\$HOME/g, homeDir).replace(/\$\{HOME\}/g, homeDir);
|
|
979
|
-
if (checkAccessible(expanded)) {
|
|
980
|
-
return path4.posix.resolve(expanded);
|
|
981
|
-
}
|
|
982
|
-
return void 0;
|
|
983
|
-
}
|
|
984
|
-
function resolvePlatformDownloadsDir(options = {}) {
|
|
985
|
-
const platform = options.platform ?? process.platform;
|
|
986
|
-
const homeDir = options.homeDir ?? os3.homedir();
|
|
987
|
-
const defaultFallback = path4.join(homeDir, "Downloads");
|
|
988
|
-
if (platform === "win32") {
|
|
989
|
-
const resolved = resolveWindowsDownloadsDir(options);
|
|
990
|
-
if (resolved) return resolved;
|
|
991
|
-
} else if (platform === "linux") {
|
|
992
|
-
const resolved = resolveLinuxDownloadsDir(options);
|
|
993
|
-
if (resolved) return resolved;
|
|
994
|
-
}
|
|
995
|
-
return defaultFallback;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
914
|
// src/lib/engines/extractors/instagram.ts
|
|
999
915
|
import crypto from "crypto";
|
|
1000
916
|
|
|
@@ -1379,8 +1295,8 @@ var InstagramExtractor = class {
|
|
|
1379
1295
|
}
|
|
1380
1296
|
const user = res.data.user;
|
|
1381
1297
|
if (user.is_private) {
|
|
1382
|
-
const
|
|
1383
|
-
const isFollowing =
|
|
1298
|
+
const fs18 = user.friendship_status;
|
|
1299
|
+
const isFollowing = fs18 ? Boolean(fs18.following) : Boolean(user.followed_by_viewer);
|
|
1384
1300
|
if (!isFollowing) {
|
|
1385
1301
|
throw new Error(`Instagram profile @${username} is private. You must provide a session cookie jar of an account following this profile.`);
|
|
1386
1302
|
}
|
|
@@ -2947,6 +2863,30 @@ var FacebookExtractor = class {
|
|
|
2947
2863
|
}
|
|
2948
2864
|
};
|
|
2949
2865
|
|
|
2866
|
+
// src/lib/engines/core/plugin.ts
|
|
2867
|
+
var ExtractorPluginRegistry = class {
|
|
2868
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2869
|
+
register(plugin) {
|
|
2870
|
+
this.plugins.set(plugin.platform, plugin);
|
|
2871
|
+
}
|
|
2872
|
+
get(platform) {
|
|
2873
|
+
return this.plugins.get(platform);
|
|
2874
|
+
}
|
|
2875
|
+
has(platform) {
|
|
2876
|
+
return this.plugins.has(platform);
|
|
2877
|
+
}
|
|
2878
|
+
list() {
|
|
2879
|
+
return Array.from(this.plugins.values());
|
|
2880
|
+
}
|
|
2881
|
+
findByUrl(url) {
|
|
2882
|
+
return this.list().find((plugin) => plugin.canHandle(url));
|
|
2883
|
+
}
|
|
2884
|
+
clear() {
|
|
2885
|
+
this.plugins.clear();
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
var defaultPluginRegistry = new ExtractorPluginRegistry();
|
|
2889
|
+
|
|
2950
2890
|
// src/lib/engines/extractors/index.ts
|
|
2951
2891
|
function getExtractor(platform) {
|
|
2952
2892
|
switch (platform) {
|
|
@@ -3750,7 +3690,7 @@ var PLATFORM_SLEEP = {
|
|
|
3750
3690
|
facebook: "1.0-1.8"
|
|
3751
3691
|
};
|
|
3752
3692
|
function buildGalleryDlArgs(options) {
|
|
3753
|
-
const { destDir, subDir, platform, cleanUrl, username, cookiePath, writeMetadata } = options;
|
|
3693
|
+
const { destDir, subDir, platform, cleanUrl, username, cookiePath, cookiesFromBrowser, writeMetadata } = options;
|
|
3754
3694
|
const args = [
|
|
3755
3695
|
"-D",
|
|
3756
3696
|
destDir,
|
|
@@ -3801,6 +3741,9 @@ function buildGalleryDlArgs(options) {
|
|
|
3801
3741
|
if (cookiePath) {
|
|
3802
3742
|
args.push("--cookies", cookiePath);
|
|
3803
3743
|
}
|
|
3744
|
+
if (cookiesFromBrowser && cookiesFromBrowser !== "none") {
|
|
3745
|
+
args.push("--cookies-from-browser", cookiesFromBrowser);
|
|
3746
|
+
}
|
|
3804
3747
|
args.push("--", targetUrl);
|
|
3805
3748
|
return args;
|
|
3806
3749
|
}
|
|
@@ -3815,6 +3758,7 @@ async function runGalleryDl(options) {
|
|
|
3815
3758
|
cleanUrl: profile.cleanUrl,
|
|
3816
3759
|
username: profile.username,
|
|
3817
3760
|
cookiePath,
|
|
3761
|
+
cookiesFromBrowser: options.cookiesFromBrowser,
|
|
3818
3762
|
writeMetadata: options.writeMetadata
|
|
3819
3763
|
});
|
|
3820
3764
|
let downloadedCount = 0;
|
|
@@ -3923,7 +3867,7 @@ import fs9 from "fs/promises";
|
|
|
3923
3867
|
var YTDLP_ARCHIVE = "archive_yt-dlp.txt";
|
|
3924
3868
|
var UA2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
3925
3869
|
function buildYtDlpArgs(options) {
|
|
3926
|
-
const { destDir, platform, cleanUrl, cookiePath, writeMetadata } = options;
|
|
3870
|
+
const { destDir, platform, cleanUrl, cookiePath, cookiesFromBrowser, writeMetadata } = options;
|
|
3927
3871
|
const args = [
|
|
3928
3872
|
"-o",
|
|
3929
3873
|
path9.join(destDir, "%(title).150B [%(id)s].%(ext)s"),
|
|
@@ -3957,6 +3901,9 @@ function buildYtDlpArgs(options) {
|
|
|
3957
3901
|
if (cookiePath) {
|
|
3958
3902
|
args.push("--cookies", cookiePath);
|
|
3959
3903
|
}
|
|
3904
|
+
if (cookiesFromBrowser && cookiesFromBrowser !== "none") {
|
|
3905
|
+
args.push("--cookies-from-browser", cookiesFromBrowser);
|
|
3906
|
+
}
|
|
3960
3907
|
args.push("--", cleanUrl);
|
|
3961
3908
|
return args;
|
|
3962
3909
|
}
|
|
@@ -3969,6 +3916,7 @@ async function runYtDlp(options) {
|
|
|
3969
3916
|
platform: profile.platform,
|
|
3970
3917
|
cleanUrl: profile.cleanUrl,
|
|
3971
3918
|
cookiePath,
|
|
3919
|
+
cookiesFromBrowser: options.cookiesFromBrowser,
|
|
3972
3920
|
writeMetadata: options.writeMetadata
|
|
3973
3921
|
});
|
|
3974
3922
|
let downloadedCount = 0;
|
|
@@ -4075,98 +4023,507 @@ async function runYtDlp(options) {
|
|
|
4075
4023
|
});
|
|
4076
4024
|
}
|
|
4077
4025
|
|
|
4078
|
-
// src/lib/engines/
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4026
|
+
// src/lib/engines/core/task-manager.ts
|
|
4027
|
+
import path12 from "path";
|
|
4028
|
+
import os7 from "os";
|
|
4029
|
+
import fs12 from "fs/promises";
|
|
4030
|
+
import fsSync from "fs";
|
|
4031
|
+
|
|
4032
|
+
// src/lib/engines/core/event-bus.ts
|
|
4033
|
+
import { EventEmitter } from "events";
|
|
4034
|
+
var EngineEventBus = class {
|
|
4035
|
+
emitter = new EventEmitter();
|
|
4036
|
+
constructor() {
|
|
4037
|
+
this.emitter.setMaxListeners(50);
|
|
4088
4038
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
if (customBaseDir && customBaseDir.trim()) {
|
|
4093
|
-
return path10.resolve(expandPath(customBaseDir.trim()));
|
|
4039
|
+
on(event, listener) {
|
|
4040
|
+
this.emitter.on(event, listener);
|
|
4041
|
+
return () => this.emitter.off(event, listener);
|
|
4094
4042
|
}
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
return path10.resolve(expandPath(envDir.trim()));
|
|
4043
|
+
off(event, listener) {
|
|
4044
|
+
this.emitter.off(event, listener);
|
|
4098
4045
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
return path10.resolve(expandPath(configDir.trim()));
|
|
4046
|
+
once(event, listener) {
|
|
4047
|
+
this.emitter.once(event, listener);
|
|
4102
4048
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
return path10.resolve(expandPath(raw.endsWith("downloads") ? raw : path10.join(raw, "downloads")));
|
|
4049
|
+
emit(event, payload) {
|
|
4050
|
+
return this.emitter.emit(event, payload);
|
|
4106
4051
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4052
|
+
removeAllListeners(event) {
|
|
4053
|
+
if (event !== void 0) {
|
|
4054
|
+
this.emitter.removeAllListeners(event);
|
|
4055
|
+
} else {
|
|
4056
|
+
this.emitter.removeAllListeners();
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
};
|
|
4060
|
+
|
|
4061
|
+
// src/lib/engines/storage/ledger.ts
|
|
4062
|
+
import fs10 from "fs";
|
|
4063
|
+
import path10 from "path";
|
|
4064
|
+
import os5 from "os";
|
|
4065
|
+
import { createRequire } from "module";
|
|
4066
|
+
var require2 = createRequire(import.meta.url);
|
|
4067
|
+
var MediaLedger = class {
|
|
4068
|
+
db = null;
|
|
4069
|
+
inMemoryFallback = {
|
|
4070
|
+
media: /* @__PURE__ */ new Map(),
|
|
4071
|
+
checkpoints: /* @__PURE__ */ new Map()
|
|
4072
|
+
};
|
|
4073
|
+
isFallback = false;
|
|
4074
|
+
resolvedPath;
|
|
4075
|
+
constructor(dbPath, forceFallback = false) {
|
|
4076
|
+
const resolvedPath = dbPath ?? (forceFallback ? ":memory:" : path10.join(os5.homedir(), ".open-nami", "nami.db"));
|
|
4077
|
+
this.resolvedPath = resolvedPath;
|
|
4078
|
+
if (forceFallback) {
|
|
4079
|
+
this.isFallback = true;
|
|
4080
|
+
this.loadFallbackDisk();
|
|
4081
|
+
return;
|
|
4082
|
+
}
|
|
4117
4083
|
try {
|
|
4118
|
-
if (
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
);
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4084
|
+
if (resolvedPath !== ":memory:") {
|
|
4085
|
+
fs10.mkdirSync(path10.dirname(resolvedPath), { recursive: true });
|
|
4086
|
+
}
|
|
4087
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
4088
|
+
this.db = new DatabaseSync(resolvedPath);
|
|
4089
|
+
this.initTables();
|
|
4090
|
+
} catch {
|
|
4091
|
+
this.isFallback = true;
|
|
4092
|
+
this.loadFallbackDisk();
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
loadFallbackDisk() {
|
|
4096
|
+
if (this.resolvedPath === ":memory:") return;
|
|
4097
|
+
try {
|
|
4098
|
+
if (fs10.existsSync(this.resolvedPath)) {
|
|
4099
|
+
const raw = fs10.readFileSync(this.resolvedPath, "utf8");
|
|
4100
|
+
const data = JSON.parse(raw);
|
|
4101
|
+
if (data && typeof data === "object") {
|
|
4102
|
+
if (data.media && typeof data.media === "object") {
|
|
4103
|
+
for (const [k, v] of Object.entries(data.media)) {
|
|
4104
|
+
this.inMemoryFallback.media.set(k, v);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
if (data.checkpoints && typeof data.checkpoints === "object") {
|
|
4108
|
+
for (const [k, v] of Object.entries(data.checkpoints)) {
|
|
4109
|
+
this.inMemoryFallback.checkpoints.set(k, v);
|
|
4110
|
+
}
|
|
4128
4111
|
}
|
|
4129
4112
|
}
|
|
4130
|
-
await fs10.rmdir(fullPath).catch(() => {
|
|
4131
|
-
});
|
|
4132
4113
|
}
|
|
4133
4114
|
} catch {
|
|
4134
4115
|
}
|
|
4135
4116
|
}
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
try {
|
|
4147
|
-
const jobs = [];
|
|
4148
|
-
if (choice === "photos") {
|
|
4149
|
-
jobs.push({ type: "photos" });
|
|
4150
|
-
} else if (choice === "videos") {
|
|
4151
|
-
jobs.push({ type: "videos" });
|
|
4152
|
-
} else if (choice === "stories") {
|
|
4153
|
-
jobs.push({ type: "stories" });
|
|
4154
|
-
} else if (choice === "highlights") {
|
|
4155
|
-
jobs.push({ type: "highlights" });
|
|
4156
|
-
} else if (choice === "all") {
|
|
4157
|
-
jobs.push({ type: "all" });
|
|
4158
|
-
if (profile.platform === "instagram") {
|
|
4159
|
-
jobs.push({ type: "stories" });
|
|
4160
|
-
jobs.push({ type: "highlights" });
|
|
4161
|
-
}
|
|
4117
|
+
flushFallbackDisk() {
|
|
4118
|
+
if (this.resolvedPath === ":memory:") return;
|
|
4119
|
+
try {
|
|
4120
|
+
fs10.mkdirSync(path10.dirname(this.resolvedPath), { recursive: true });
|
|
4121
|
+
const data = {
|
|
4122
|
+
media: Object.fromEntries(this.inMemoryFallback.media),
|
|
4123
|
+
checkpoints: Object.fromEntries(this.inMemoryFallback.checkpoints)
|
|
4124
|
+
};
|
|
4125
|
+
fs10.writeFileSync(this.resolvedPath, JSON.stringify(data, null, 2), "utf8");
|
|
4126
|
+
} catch {
|
|
4162
4127
|
}
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4128
|
+
}
|
|
4129
|
+
initTables() {
|
|
4130
|
+
if (!this.db) return;
|
|
4131
|
+
this.db.exec(`
|
|
4132
|
+
CREATE TABLE IF NOT EXISTS media_ledger (
|
|
4133
|
+
id TEXT PRIMARY KEY,
|
|
4134
|
+
platform TEXT,
|
|
4135
|
+
author TEXT,
|
|
4136
|
+
url TEXT,
|
|
4137
|
+
filename TEXT,
|
|
4138
|
+
file_path TEXT,
|
|
4139
|
+
downloaded_at TEXT
|
|
4140
|
+
);
|
|
4141
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
4142
|
+
key TEXT PRIMARY KEY,
|
|
4143
|
+
platform TEXT,
|
|
4144
|
+
username TEXT,
|
|
4145
|
+
target_dir TEXT,
|
|
4146
|
+
cursors_json TEXT,
|
|
4147
|
+
updated_at TEXT
|
|
4148
|
+
);
|
|
4149
|
+
`);
|
|
4150
|
+
}
|
|
4151
|
+
hasMedia(id) {
|
|
4152
|
+
if (this.isFallback || !this.db) {
|
|
4153
|
+
return this.inMemoryFallback.media.has(id);
|
|
4154
|
+
}
|
|
4155
|
+
const stmt = this.db.prepare("SELECT 1 FROM media_ledger WHERE id = ?");
|
|
4156
|
+
const row = stmt.get(id);
|
|
4157
|
+
return Boolean(row);
|
|
4158
|
+
}
|
|
4159
|
+
recordMedia(media) {
|
|
4160
|
+
if (this.isFallback || !this.db) {
|
|
4161
|
+
this.inMemoryFallback.media.set(media.id, media);
|
|
4162
|
+
this.flushFallbackDisk();
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
const stmt = this.db.prepare(`
|
|
4166
|
+
INSERT OR REPLACE INTO media_ledger (id, platform, author, url, filename, file_path, downloaded_at)
|
|
4167
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4168
|
+
`);
|
|
4169
|
+
stmt.run(
|
|
4170
|
+
media.id,
|
|
4171
|
+
media.platform,
|
|
4172
|
+
media.author,
|
|
4173
|
+
media.url,
|
|
4174
|
+
media.filename,
|
|
4175
|
+
media.filePath,
|
|
4176
|
+
media.downloadedAt
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
saveCheckpoint(checkpoint) {
|
|
4180
|
+
const key = `${checkpoint.platform}:${checkpoint.username.toLowerCase()}`;
|
|
4181
|
+
if (this.isFallback || !this.db) {
|
|
4182
|
+
this.inMemoryFallback.checkpoints.set(key, checkpoint);
|
|
4183
|
+
this.flushFallbackDisk();
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
const stmt = this.db.prepare(`
|
|
4187
|
+
INSERT OR REPLACE INTO checkpoints (key, platform, username, target_dir, cursors_json, updated_at)
|
|
4188
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
4189
|
+
`);
|
|
4190
|
+
stmt.run(
|
|
4191
|
+
key,
|
|
4192
|
+
checkpoint.platform,
|
|
4193
|
+
checkpoint.username,
|
|
4194
|
+
checkpoint.targetDir,
|
|
4195
|
+
JSON.stringify(checkpoint.cursors),
|
|
4196
|
+
checkpoint.updatedAt
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
getCheckpoint(platform, username) {
|
|
4200
|
+
const key = `${platform}:${username.toLowerCase()}`;
|
|
4201
|
+
if (this.isFallback || !this.db) {
|
|
4202
|
+
return this.inMemoryFallback.checkpoints.get(key) || null;
|
|
4203
|
+
}
|
|
4204
|
+
const stmt = this.db.prepare(
|
|
4205
|
+
"SELECT platform, username, target_dir, cursors_json, updated_at FROM checkpoints WHERE key = ?"
|
|
4206
|
+
);
|
|
4207
|
+
const row = stmt.get(key);
|
|
4208
|
+
if (!row) return null;
|
|
4209
|
+
return {
|
|
4210
|
+
platform: row.platform,
|
|
4211
|
+
username: row.username,
|
|
4212
|
+
targetDir: row.target_dir,
|
|
4213
|
+
cursors: JSON.parse(row.cursors_json || "{}"),
|
|
4214
|
+
updatedAt: row.updated_at
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
clearCheckpoint(platform, username) {
|
|
4218
|
+
const key = `${platform}:${username.toLowerCase()}`;
|
|
4219
|
+
if (this.isFallback || !this.db) {
|
|
4220
|
+
this.inMemoryFallback.checkpoints.delete(key);
|
|
4221
|
+
this.flushFallbackDisk();
|
|
4222
|
+
return;
|
|
4223
|
+
}
|
|
4224
|
+
const stmt = this.db.prepare("DELETE FROM checkpoints WHERE key = ?");
|
|
4225
|
+
stmt.run(key);
|
|
4226
|
+
}
|
|
4227
|
+
close() {
|
|
4228
|
+
if (this.db) {
|
|
4229
|
+
try {
|
|
4230
|
+
this.db.close();
|
|
4231
|
+
} catch {
|
|
4232
|
+
}
|
|
4233
|
+
this.db = null;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
};
|
|
4237
|
+
|
|
4238
|
+
// src/lib/known-folders.ts
|
|
4239
|
+
import { execFileSync } from "child_process";
|
|
4240
|
+
import fs11 from "fs";
|
|
4241
|
+
import os6 from "os";
|
|
4242
|
+
import path11 from "path";
|
|
4243
|
+
var KNOWN_FOLDER_DOWNLOADS_GUID = "{374DE290-123F-4565-9164-39C4925E467B}";
|
|
4244
|
+
var USER_SHELL_FOLDERS_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders";
|
|
4245
|
+
function expandWindowsEnv(str, env = process.env, homeDir) {
|
|
4246
|
+
return str.replace(/%([^%]+)%/g, (original, name) => {
|
|
4247
|
+
if (homeDir && name.toLowerCase() === "userprofile") {
|
|
4248
|
+
return homeDir;
|
|
4249
|
+
}
|
|
4250
|
+
const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
4251
|
+
if (key && env[key] !== void 0) {
|
|
4252
|
+
return env[key];
|
|
4253
|
+
}
|
|
4254
|
+
return original;
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
4257
|
+
function parseWindowsRegistryOutput(output, targetKey = KNOWN_FOLDER_DOWNLOADS_GUID) {
|
|
4258
|
+
const lines = output.split(/\r?\n/);
|
|
4259
|
+
const escaped = targetKey.replace(/[{}]/g, "\\$&");
|
|
4260
|
+
const pattern = new RegExp(`^\\s*${escaped}\\s+REG_\\w+\\s+(.*)$`, "i");
|
|
4261
|
+
for (const line of lines) {
|
|
4262
|
+
const match = line.match(pattern);
|
|
4263
|
+
if (match && match[1]) {
|
|
4264
|
+
const rawPath = match[1].trim();
|
|
4265
|
+
if (rawPath) return rawPath;
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
return void 0;
|
|
4269
|
+
}
|
|
4270
|
+
function parseLinuxUserDirsOutput(content) {
|
|
4271
|
+
const lines = content.split(/\r?\n/);
|
|
4272
|
+
for (const line of lines) {
|
|
4273
|
+
const trimmed = line.trim();
|
|
4274
|
+
if (trimmed.startsWith("#")) continue;
|
|
4275
|
+
const match = trimmed.match(/^XDG_DOWNLOAD_DIR=["']?([^"']+)["']?$/);
|
|
4276
|
+
if (match && match[1]) {
|
|
4277
|
+
return match[1].trim();
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
return void 0;
|
|
4281
|
+
}
|
|
4282
|
+
function isPathDriveAccessible(filePath, fsExists = fs11.existsSync, platform = process.platform) {
|
|
4283
|
+
try {
|
|
4284
|
+
if (fsExists(filePath)) {
|
|
4285
|
+
return true;
|
|
4286
|
+
}
|
|
4287
|
+
if (platform === "win32") {
|
|
4288
|
+
const match = filePath.match(/^([a-zA-Z]:[\\/])/i);
|
|
4289
|
+
const driveRoot = match ? match[1] : path11.win32.parse(path11.win32.resolve(filePath)).root;
|
|
4290
|
+
return fsExists(driveRoot);
|
|
4291
|
+
}
|
|
4292
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
4293
|
+
const mountMatch = normalized.match(/^(\/(?:media(?:\/[^/]+)?|mnt|run\/media(?:\/[^/]+)?)\/[^/]+)/);
|
|
4294
|
+
if (mountMatch) {
|
|
4295
|
+
return fsExists(mountMatch[1]);
|
|
4296
|
+
}
|
|
4297
|
+
let curr = path11.posix.dirname(normalized);
|
|
4298
|
+
while (curr && curr !== "/") {
|
|
4299
|
+
if (fsExists(curr)) return true;
|
|
4300
|
+
curr = path11.posix.dirname(curr);
|
|
4301
|
+
}
|
|
4302
|
+
return fsExists("/");
|
|
4303
|
+
} catch {
|
|
4304
|
+
return false;
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
function resolveWindowsDownloadsDir(options = {}) {
|
|
4308
|
+
const homeDir = options.homeDir ?? os6.homedir();
|
|
4309
|
+
const env = options.env ?? process.env;
|
|
4310
|
+
const runQuery = options.regQueryFn ?? ((key, value) => {
|
|
4311
|
+
try {
|
|
4312
|
+
return execFileSync("reg", ["query", key, "/v", value], {
|
|
4313
|
+
encoding: "utf8",
|
|
4314
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
4315
|
+
});
|
|
4316
|
+
} catch {
|
|
4317
|
+
return "";
|
|
4318
|
+
}
|
|
4319
|
+
});
|
|
4320
|
+
const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs11.existsSync, "win32"));
|
|
4321
|
+
let raw = parseWindowsRegistryOutput(
|
|
4322
|
+
runQuery(USER_SHELL_FOLDERS_REG_KEY, KNOWN_FOLDER_DOWNLOADS_GUID),
|
|
4323
|
+
KNOWN_FOLDER_DOWNLOADS_GUID
|
|
4324
|
+
);
|
|
4325
|
+
if (!raw) {
|
|
4326
|
+
raw = parseWindowsRegistryOutput(runQuery(USER_SHELL_FOLDERS_REG_KEY, "Downloads"), "Downloads");
|
|
4327
|
+
}
|
|
4328
|
+
if (raw) {
|
|
4329
|
+
const expanded = expandWindowsEnv(raw, env, homeDir);
|
|
4330
|
+
if (checkAccessible(expanded)) {
|
|
4331
|
+
return path11.win32.resolve(expanded);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
return void 0;
|
|
4335
|
+
}
|
|
4336
|
+
function resolveLinuxDownloadsDir(options = {}) {
|
|
4337
|
+
const homeDir = options.homeDir ?? os6.homedir();
|
|
4338
|
+
const env = options.env ?? process.env;
|
|
4339
|
+
const readFn = options.readFileFn ?? ((p) => {
|
|
4340
|
+
try {
|
|
4341
|
+
return fs11.readFileSync(p, "utf8");
|
|
4342
|
+
} catch {
|
|
4343
|
+
return "";
|
|
4344
|
+
}
|
|
4345
|
+
});
|
|
4346
|
+
const checkAccessible = options.driveAccessibleFn ?? ((p) => isPathDriveAccessible(p, fs11.existsSync, "linux"));
|
|
4347
|
+
const configHome = env.XDG_CONFIG_HOME || path11.join(homeDir, ".config");
|
|
4348
|
+
const userDirsFile = path11.join(configHome, "user-dirs.dirs");
|
|
4349
|
+
const content = readFn(userDirsFile);
|
|
4350
|
+
if (!content) return void 0;
|
|
4351
|
+
const raw = parseLinuxUserDirsOutput(content);
|
|
4352
|
+
if (!raw) return void 0;
|
|
4353
|
+
const expanded = raw.replace(/\$HOME/g, homeDir).replace(/\$\{HOME\}/g, homeDir);
|
|
4354
|
+
if (checkAccessible(expanded)) {
|
|
4355
|
+
return path11.posix.resolve(expanded);
|
|
4356
|
+
}
|
|
4357
|
+
return void 0;
|
|
4358
|
+
}
|
|
4359
|
+
function resolvePlatformDownloadsDir(options = {}) {
|
|
4360
|
+
const platform = options.platform ?? process.platform;
|
|
4361
|
+
const homeDir = options.homeDir ?? os6.homedir();
|
|
4362
|
+
const defaultFallback = path11.join(homeDir, "Downloads");
|
|
4363
|
+
if (platform === "win32") {
|
|
4364
|
+
const resolved = resolveWindowsDownloadsDir(options);
|
|
4365
|
+
if (resolved) return resolved;
|
|
4366
|
+
} else if (platform === "linux") {
|
|
4367
|
+
const resolved = resolveLinuxDownloadsDir(options);
|
|
4368
|
+
if (resolved) return resolved;
|
|
4369
|
+
}
|
|
4370
|
+
return defaultFallback;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
// src/lib/engines/core/task-manager.ts
|
|
4374
|
+
function expandPath(dir) {
|
|
4375
|
+
if (dir.startsWith("~/") || dir.startsWith("~\\") || dir === "~") {
|
|
4376
|
+
return path12.join(os7.homedir(), dir.slice(1));
|
|
4377
|
+
}
|
|
4378
|
+
return dir;
|
|
4379
|
+
}
|
|
4380
|
+
function resolveDownloadBaseDir(customBaseDir) {
|
|
4381
|
+
if (customBaseDir && customBaseDir.trim()) {
|
|
4382
|
+
return path12.resolve(expandPath(customBaseDir.trim()));
|
|
4383
|
+
}
|
|
4384
|
+
const envDir = process.env.OPEN_NAMI_DIR || process.env.NAMI_DIR;
|
|
4385
|
+
if (envDir && envDir.trim()) {
|
|
4386
|
+
return path12.resolve(expandPath(envDir.trim()));
|
|
4387
|
+
}
|
|
4388
|
+
const configDir = loadConfig().downloadDir;
|
|
4389
|
+
if (configDir && configDir.trim()) {
|
|
4390
|
+
return path12.resolve(expandPath(configDir.trim()));
|
|
4391
|
+
}
|
|
4392
|
+
if (process.env.NAMI_BASE_DIR && process.env.NAMI_BASE_DIR.trim()) {
|
|
4393
|
+
const raw = process.env.NAMI_BASE_DIR.trim();
|
|
4394
|
+
return path12.resolve(expandPath(raw.endsWith("downloads") ? raw : path12.join(raw, "downloads")));
|
|
4395
|
+
}
|
|
4396
|
+
return path12.join(resolvePlatformDownloadsDir(), "Open Nami");
|
|
4397
|
+
}
|
|
4398
|
+
function resolveDefaultDownloadDir(platform, username, baseDir) {
|
|
4399
|
+
const root = resolveDownloadBaseDir(baseDir);
|
|
4400
|
+
return path12.join(root, platform, username);
|
|
4401
|
+
}
|
|
4402
|
+
async function cleanupEmptyCategoryDirs(targetDir) {
|
|
4403
|
+
const candidateDirs = ["All", "Stories", "Highlights", "Photos", "Videos"];
|
|
4404
|
+
for (const dirName of candidateDirs) {
|
|
4405
|
+
const fullPath = path12.join(targetDir, dirName);
|
|
4406
|
+
try {
|
|
4407
|
+
if (!fsSync.existsSync(fullPath)) continue;
|
|
4408
|
+
const entries = await fs12.readdir(fullPath);
|
|
4409
|
+
const nonArchiveEntries = entries.filter(
|
|
4410
|
+
(e) => e !== ".open-nami-archive.json" && e !== "archive.txt" && e !== "archive_yt-dlp.txt" && e !== "archive_gallery-dl.sqlite3"
|
|
4411
|
+
);
|
|
4412
|
+
if (nonArchiveEntries.length === 0) {
|
|
4413
|
+
for (const entry of entries) {
|
|
4414
|
+
try {
|
|
4415
|
+
await fs12.unlink(path12.join(fullPath, entry));
|
|
4416
|
+
} catch {
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
await fs12.rmdir(fullPath).catch(() => {
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
} catch {
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
var TaskManager = class {
|
|
4427
|
+
events;
|
|
4428
|
+
ledger;
|
|
4429
|
+
plugins;
|
|
4430
|
+
constructor(eventBus, ledger, plugins) {
|
|
4431
|
+
this.events = eventBus || new EngineEventBus();
|
|
4432
|
+
this.ledger = ledger || new MediaLedger();
|
|
4433
|
+
this.plugins = plugins || defaultPluginRegistry;
|
|
4434
|
+
}
|
|
4435
|
+
async execute(options) {
|
|
4436
|
+
const { profile, choice } = options;
|
|
4437
|
+
const targetDir = options.outputDir || resolveDefaultDownloadDir(profile.platform, profile.username);
|
|
4438
|
+
this.events.emit("job:start", { profile, choice, outputDir: targetDir });
|
|
4439
|
+
let downloadedCount = 0;
|
|
4440
|
+
let skippedCount = 0;
|
|
4441
|
+
const errors = [];
|
|
4442
|
+
if (options.runnerOverrides?.nativeRunner) {
|
|
4443
|
+
const subDir = choice === "all" ? "All" : choice.charAt(0).toUpperCase() + choice.slice(1);
|
|
4444
|
+
const runRes = await options.runnerOverrides.nativeRunner(subDir);
|
|
4445
|
+
downloadedCount += runRes.downloaded;
|
|
4446
|
+
skippedCount += runRes.skipped;
|
|
4447
|
+
if (!runRes.success) errors.push(runRes.error || "Execution failed");
|
|
4448
|
+
}
|
|
4449
|
+
await cleanupEmptyCategoryDirs(targetDir);
|
|
4450
|
+
const result = {
|
|
4451
|
+
success: errors.length === 0,
|
|
4452
|
+
downloadedCount,
|
|
4453
|
+
skippedCount,
|
|
4454
|
+
errors,
|
|
4455
|
+
outputDir: targetDir
|
|
4456
|
+
};
|
|
4457
|
+
this.events.emit("job:complete", { result });
|
|
4458
|
+
return result;
|
|
4459
|
+
}
|
|
4460
|
+
};
|
|
4461
|
+
var defaultTaskManager = new TaskManager();
|
|
4462
|
+
|
|
4463
|
+
// src/lib/engines/dispatcher.ts
|
|
4464
|
+
var REFERER_MAP = {
|
|
4465
|
+
tiktok: "https://www.tiktok.com/",
|
|
4466
|
+
instagram: "https://www.instagram.com/",
|
|
4467
|
+
x: "https://x.com/",
|
|
4468
|
+
facebook: "https://www.facebook.com/"
|
|
4469
|
+
};
|
|
4470
|
+
async function dispatchDownload(options) {
|
|
4471
|
+
const { profile, choice, onProgress } = options;
|
|
4472
|
+
const targetDir = options.outputDir || resolveDefaultDownloadDir(profile.platform, profile.username);
|
|
4473
|
+
await fs13.mkdir(targetDir, { recursive: true });
|
|
4474
|
+
const cookiesFromBrowser = options.cookiesFromBrowser ?? loadConfig().cookiesFromBrowser;
|
|
4475
|
+
defaultTaskManager.events.emit("job:start", { profile, choice, outputDir: targetDir });
|
|
4476
|
+
const masterCookie = findCookieFile(profile.platform);
|
|
4477
|
+
const cookieCopy = masterCookie ? await createSafeCookieCopy(masterCookie) : null;
|
|
4478
|
+
let totalDownloaded = 0;
|
|
4479
|
+
let totalSkipped = 0;
|
|
4480
|
+
const errors = [];
|
|
4481
|
+
const recordError = (err) => {
|
|
4482
|
+
errors.push(err);
|
|
4483
|
+
defaultTaskManager.events.emit("job:error", { error: err });
|
|
4484
|
+
};
|
|
4485
|
+
const handleProgress = (progress) => {
|
|
4486
|
+
defaultTaskManager.events.emit("job:progress", progress);
|
|
4487
|
+
if (progress.currentFile && progress.statusText?.startsWith("Saved:")) {
|
|
4488
|
+
try {
|
|
4489
|
+
defaultTaskManager.ledger.recordMedia({
|
|
4490
|
+
id: `${profile.platform}:${profile.username}:${progress.currentFile}`,
|
|
4491
|
+
platform: profile.platform,
|
|
4492
|
+
author: profile.username,
|
|
4493
|
+
url: profile.cleanUrl || "",
|
|
4494
|
+
filename: progress.currentFile,
|
|
4495
|
+
filePath: path13.join(targetDir, progress.currentFile),
|
|
4496
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4497
|
+
});
|
|
4498
|
+
} catch {
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
onProgress?.(progress);
|
|
4502
|
+
};
|
|
4503
|
+
try {
|
|
4504
|
+
const jobs = [];
|
|
4505
|
+
if (choice === "photos") {
|
|
4506
|
+
jobs.push({ type: "photos" });
|
|
4507
|
+
} else if (choice === "videos") {
|
|
4508
|
+
jobs.push({ type: "videos" });
|
|
4509
|
+
} else if (choice === "stories") {
|
|
4510
|
+
jobs.push({ type: "stories" });
|
|
4511
|
+
} else if (choice === "highlights") {
|
|
4512
|
+
jobs.push({ type: "highlights" });
|
|
4513
|
+
} else if (choice === "all") {
|
|
4514
|
+
jobs.push({ type: "all" });
|
|
4515
|
+
if (profile.platform === "instagram") {
|
|
4516
|
+
jobs.push({ type: "stories" });
|
|
4517
|
+
jobs.push({ type: "highlights" });
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
const sinceSeconds = parseDateBoundary(options.since, "since");
|
|
4521
|
+
const untilSeconds = parseDateBoundary(options.until, "until");
|
|
4522
|
+
const globalLimit = parseLimit(options.limit);
|
|
4523
|
+
const savedCheckpoint = options.resume ? await loadCheckpoint(targetDir) : null;
|
|
4524
|
+
const currentCursors = savedCheckpoint?.cursors ? { ...savedCheckpoint.cursors } : {};
|
|
4525
|
+
const defaultRunNative = async (subDir) => {
|
|
4526
|
+
const dest = subDir === "All" ? targetDir : path13.join(targetDir, subDir);
|
|
4170
4527
|
const remainingLimit = globalLimit !== void 0 ? Math.max(0, globalLimit - totalDownloaded) : void 0;
|
|
4171
4528
|
if (remainingLimit === 0) {
|
|
4172
4529
|
return { success: true, downloaded: 0, skipped: 0 };
|
|
@@ -4208,8 +4565,8 @@ async function dispatchDownload(options) {
|
|
|
4208
4565
|
jobName: `${profile.username} ${subDir}`,
|
|
4209
4566
|
destDir: dest,
|
|
4210
4567
|
destDirMap: subDir === "All" ? {
|
|
4211
|
-
photo:
|
|
4212
|
-
video:
|
|
4568
|
+
photo: path13.join(targetDir, "Photos"),
|
|
4569
|
+
video: path13.join(targetDir, "Videos")
|
|
4213
4570
|
} : void 0,
|
|
4214
4571
|
cookiePath: cookieCopy?.filePath,
|
|
4215
4572
|
referer: REFERER_MAP[profile.platform],
|
|
@@ -4218,7 +4575,7 @@ async function dispatchDownload(options) {
|
|
|
4218
4575
|
writeMetadata: options.writeMetadata,
|
|
4219
4576
|
defaultPlatform: profile.platform,
|
|
4220
4577
|
defaultAuthor: profile.username,
|
|
4221
|
-
onProgress
|
|
4578
|
+
onProgress: handleProgress
|
|
4222
4579
|
});
|
|
4223
4580
|
return {
|
|
4224
4581
|
success: res.errors.length === 0,
|
|
@@ -4232,7 +4589,7 @@ async function dispatchDownload(options) {
|
|
|
4232
4589
|
success: false,
|
|
4233
4590
|
downloaded: 0,
|
|
4234
4591
|
skipped: 0,
|
|
4235
|
-
error: errMsg
|
|
4592
|
+
error: errMsg || "Execution failed"
|
|
4236
4593
|
};
|
|
4237
4594
|
}
|
|
4238
4595
|
};
|
|
@@ -4242,17 +4599,19 @@ async function dispatchDownload(options) {
|
|
|
4242
4599
|
targetDir,
|
|
4243
4600
|
subDir,
|
|
4244
4601
|
cookiePath: cookieCopy?.filePath,
|
|
4602
|
+
cookiesFromBrowser,
|
|
4245
4603
|
signal: options.signal,
|
|
4246
4604
|
writeMetadata: options.writeMetadata,
|
|
4247
|
-
onProgress
|
|
4605
|
+
onProgress: handleProgress
|
|
4248
4606
|
}));
|
|
4249
4607
|
const runYt = options.runnerOverrides?.ytRunner ?? (() => runYtDlp({
|
|
4250
4608
|
profile,
|
|
4251
4609
|
targetDir,
|
|
4252
4610
|
cookiePath: cookieCopy?.filePath,
|
|
4611
|
+
cookiesFromBrowser,
|
|
4253
4612
|
signal: options.signal,
|
|
4254
4613
|
writeMetadata: options.writeMetadata,
|
|
4255
|
-
onProgress
|
|
4614
|
+
onProgress: handleProgress
|
|
4256
4615
|
}));
|
|
4257
4616
|
for (const job of jobs) {
|
|
4258
4617
|
if (options.signal?.aborted) break;
|
|
@@ -4260,7 +4619,7 @@ async function dispatchDownload(options) {
|
|
|
4260
4619
|
if (job.type === "all") {
|
|
4261
4620
|
let res = await runNative("All");
|
|
4262
4621
|
if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
|
|
4263
|
-
|
|
4622
|
+
handleProgress({
|
|
4264
4623
|
job: "Photos",
|
|
4265
4624
|
downloadedCount: totalDownloaded,
|
|
4266
4625
|
skippedCount: totalSkipped,
|
|
@@ -4269,8 +4628,8 @@ async function dispatchDownload(options) {
|
|
|
4269
4628
|
const gdlPhotos = await runGdl("Photos");
|
|
4270
4629
|
totalDownloaded += gdlPhotos.downloaded;
|
|
4271
4630
|
totalSkipped += gdlPhotos.skipped;
|
|
4272
|
-
if (!gdlPhotos.success
|
|
4273
|
-
|
|
4631
|
+
if (!gdlPhotos.success) recordError(gdlPhotos.error ? `[Photos] ${gdlPhotos.error}` : "[Photos] Execution failed");
|
|
4632
|
+
handleProgress({
|
|
4274
4633
|
job: "Videos",
|
|
4275
4634
|
downloadedCount: totalDownloaded,
|
|
4276
4635
|
skippedCount: totalSkipped,
|
|
@@ -4280,9 +4639,9 @@ async function dispatchDownload(options) {
|
|
|
4280
4639
|
if (ytVideos.downloaded > 0 || ytVideos.skipped > 0 || ytVideos.success) {
|
|
4281
4640
|
totalDownloaded += ytVideos.downloaded;
|
|
4282
4641
|
totalSkipped += ytVideos.skipped;
|
|
4283
|
-
if (!ytVideos.success
|
|
4642
|
+
if (!ytVideos.success) recordError(ytVideos.error ? `[Videos] ${ytVideos.error}` : "[Videos] Execution failed");
|
|
4284
4643
|
} else if (!options.signal?.aborted) {
|
|
4285
|
-
|
|
4644
|
+
handleProgress({
|
|
4286
4645
|
job: "Videos",
|
|
4287
4646
|
downloadedCount: totalDownloaded,
|
|
4288
4647
|
skippedCount: totalSkipped,
|
|
@@ -4291,19 +4650,19 @@ async function dispatchDownload(options) {
|
|
|
4291
4650
|
const gdlVideos = await runGdl("Videos");
|
|
4292
4651
|
totalDownloaded += gdlVideos.downloaded;
|
|
4293
4652
|
totalSkipped += gdlVideos.skipped;
|
|
4294
|
-
if (!gdlVideos.success
|
|
4653
|
+
if (!gdlVideos.success) recordError(gdlVideos.error ? `[Videos] ${gdlVideos.error}` : "[Videos] Execution failed");
|
|
4295
4654
|
}
|
|
4296
4655
|
} else {
|
|
4297
4656
|
totalDownloaded += res.downloaded;
|
|
4298
4657
|
totalSkipped += res.skipped;
|
|
4299
|
-
if (!res.success
|
|
4658
|
+
if (!res.success) recordError(res.error ? `[All] ${res.error}` : "[All] Execution failed");
|
|
4300
4659
|
}
|
|
4301
4660
|
} else if (job.type === "photos" || job.type === "stories" || job.type === "highlights") {
|
|
4302
4661
|
const subDir = job.type === "photos" ? "Photos" : job.type === "stories" ? "Stories" : "Highlights";
|
|
4303
4662
|
let res = await runNative(subDir);
|
|
4304
4663
|
const isAuthError = Boolean(res.error && (res.error.includes("cookie") || res.error.includes("authenticated session") || res.error.includes("Settings (^s)")));
|
|
4305
4664
|
if (!isAuthError && (!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
|
|
4306
|
-
|
|
4665
|
+
handleProgress({
|
|
4307
4666
|
job: subDir,
|
|
4308
4667
|
downloadedCount: totalDownloaded,
|
|
4309
4668
|
skippedCount: totalSkipped,
|
|
@@ -4316,14 +4675,14 @@ async function dispatchDownload(options) {
|
|
|
4316
4675
|
}
|
|
4317
4676
|
totalDownloaded += res.downloaded;
|
|
4318
4677
|
totalSkipped += res.skipped;
|
|
4319
|
-
if (!res.success
|
|
4678
|
+
if (!res.success) recordError(res.error ? `[${subDir}] ${res.error}` : `[${subDir}] Execution failed`);
|
|
4320
4679
|
} else if (job.type === "videos") {
|
|
4321
4680
|
let res = await runNative("Videos");
|
|
4322
4681
|
if (res.downloaded > 0 || res.skipped > 0) {
|
|
4323
4682
|
totalDownloaded += res.downloaded;
|
|
4324
4683
|
totalSkipped += res.skipped;
|
|
4325
4684
|
} else if (!options.signal?.aborted) {
|
|
4326
|
-
|
|
4685
|
+
handleProgress({
|
|
4327
4686
|
job: "Videos",
|
|
4328
4687
|
downloadedCount: totalDownloaded,
|
|
4329
4688
|
skippedCount: totalSkipped,
|
|
@@ -4333,9 +4692,9 @@ async function dispatchDownload(options) {
|
|
|
4333
4692
|
if (fbRes.downloaded > 0 || fbRes.skipped > 0 || fbRes.success) {
|
|
4334
4693
|
totalDownloaded += fbRes.downloaded;
|
|
4335
4694
|
totalSkipped += fbRes.skipped;
|
|
4336
|
-
if (!fbRes.success
|
|
4695
|
+
if (!fbRes.success) recordError(fbRes.error ? `[Videos] ${fbRes.error}` : "[Videos] Execution failed");
|
|
4337
4696
|
} else if (!options.signal?.aborted) {
|
|
4338
|
-
|
|
4697
|
+
handleProgress({
|
|
4339
4698
|
job: "Videos",
|
|
4340
4699
|
downloadedCount: totalDownloaded,
|
|
4341
4700
|
skippedCount: totalSkipped,
|
|
@@ -4344,7 +4703,7 @@ async function dispatchDownload(options) {
|
|
|
4344
4703
|
const gdlRes = await runGdl("Videos");
|
|
4345
4704
|
totalDownloaded += gdlRes.downloaded;
|
|
4346
4705
|
totalSkipped += gdlRes.skipped;
|
|
4347
|
-
if (!gdlRes.success
|
|
4706
|
+
if (!gdlRes.success) recordError(gdlRes.error ? `[Videos] ${gdlRes.error}` : "[Videos] Execution failed");
|
|
4348
4707
|
}
|
|
4349
4708
|
}
|
|
4350
4709
|
}
|
|
@@ -4353,13 +4712,19 @@ async function dispatchDownload(options) {
|
|
|
4353
4712
|
if (errors.length === 0 && !options.signal?.aborted) {
|
|
4354
4713
|
await clearCheckpoint(targetDir);
|
|
4355
4714
|
}
|
|
4356
|
-
|
|
4715
|
+
const engineResult = {
|
|
4357
4716
|
success: errors.length === 0 || totalDownloaded > 0,
|
|
4358
4717
|
downloadedCount: totalDownloaded,
|
|
4359
4718
|
skippedCount: totalSkipped,
|
|
4360
4719
|
errors,
|
|
4361
4720
|
outputDir: targetDir
|
|
4362
4721
|
};
|
|
4722
|
+
defaultTaskManager.events.emit("job:complete", { result: engineResult });
|
|
4723
|
+
return engineResult;
|
|
4724
|
+
} catch (err) {
|
|
4725
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4726
|
+
defaultTaskManager.events.emit("job:error", { error: errMsg || "Execution failed" });
|
|
4727
|
+
throw err;
|
|
4363
4728
|
} finally {
|
|
4364
4729
|
if (cookieCopy) {
|
|
4365
4730
|
await cookieCopy.cleanup();
|
|
@@ -4559,7 +4924,7 @@ function ProbingView({
|
|
|
4559
4924
|
|
|
4560
4925
|
// src/components/views/downloading-view.tsx
|
|
4561
4926
|
import { useState as useState5, useEffect as useEffect5 } from "react";
|
|
4562
|
-
import
|
|
4927
|
+
import os8 from "os";
|
|
4563
4928
|
import { Box as Box9, Text as Text11 } from "ink";
|
|
4564
4929
|
import Spinner from "ink-spinner";
|
|
4565
4930
|
|
|
@@ -4695,35 +5060,51 @@ function DownloadingView({
|
|
|
4695
5060
|
activeBadges.length > 0 ? /* @__PURE__ */ jsx12(Text11, { color: theme.gray, dimColor: theme.dimSecondary, children: activeBadges.join(" \xB7 ") }) : null,
|
|
4696
5061
|
outputDir ? /* @__PURE__ */ jsxs9(Text11, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
4697
5062
|
"saving to ",
|
|
4698
|
-
shortenPath(outputDir,
|
|
5063
|
+
shortenPath(outputDir, os8.homedir(), 54)
|
|
4699
5064
|
] }) : null
|
|
4700
5065
|
] });
|
|
4701
5066
|
}
|
|
4702
5067
|
|
|
4703
5068
|
// src/components/views/completion-view.tsx
|
|
4704
|
-
import
|
|
5069
|
+
import os9 from "os";
|
|
4705
5070
|
import { Box as Box10, Text as Text12 } from "ink";
|
|
4706
|
-
import {
|
|
5071
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
4707
5072
|
var DONE_LABEL = "\u21B5 download another";
|
|
5073
|
+
var LEDGER_INDEXED_LABEL = "\u2713 indexed in media ledger";
|
|
5074
|
+
function shouldShowLedgerBadge(result, showLedgerBadge) {
|
|
5075
|
+
return showLedgerBadge ?? result.downloadedCount > 0;
|
|
5076
|
+
}
|
|
5077
|
+
function formatCompletionSummary(result) {
|
|
5078
|
+
const isZero = result.downloadedCount === 0 && result.skippedCount === 0;
|
|
5079
|
+
const plural = result.downloadedCount === 1 ? "" : "s";
|
|
5080
|
+
const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
|
|
5081
|
+
return {
|
|
5082
|
+
isZero,
|
|
5083
|
+
headerPrefix: isZero ? "\u2713 profile scanned " : "\u2713 profile downloaded! ",
|
|
5084
|
+
headerSuffix: isZero ? ": 0 media items found" : "saved to:",
|
|
5085
|
+
itemsText: isZero ? "no photos or videos available" : `${result.downloadedCount} media item${plural} downloaded${skipText}`
|
|
5086
|
+
};
|
|
5087
|
+
}
|
|
5088
|
+
function LedgerBadge({ theme }) {
|
|
5089
|
+
return /* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: LEDGER_INDEXED_LABEL });
|
|
5090
|
+
}
|
|
4708
5091
|
function CompletionView({
|
|
4709
5092
|
profile,
|
|
4710
5093
|
result,
|
|
4711
5094
|
theme,
|
|
4712
|
-
gapComponent: Gap2
|
|
5095
|
+
gapComponent: Gap2,
|
|
5096
|
+
showLedgerBadge
|
|
4713
5097
|
}) {
|
|
4714
|
-
const
|
|
4715
|
-
const
|
|
4716
|
-
const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
|
|
5098
|
+
const { headerPrefix, headerSuffix, itemsText } = formatCompletionSummary(result);
|
|
5099
|
+
const displayLedger = shouldShowLedgerBadge(result, showLedgerBadge);
|
|
4717
5100
|
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", alignItems: "center", children: [
|
|
4718
|
-
/* @__PURE__ */
|
|
4719
|
-
/* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children:
|
|
4720
|
-
/* @__PURE__ */ jsx13(Text12, { color: theme.primary, children:
|
|
4721
|
-
] })
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
/* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: shortenPath(result.outputDir, os7.homedir(), 60) }),
|
|
4726
|
-
/* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: isZero ? "no photos or videos available" : `${result.downloadedCount} media item${plural} downloaded${skipText}` }),
|
|
5101
|
+
/* @__PURE__ */ jsxs10(Text12, { children: [
|
|
5102
|
+
/* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: headerPrefix }),
|
|
5103
|
+
/* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: headerSuffix })
|
|
5104
|
+
] }),
|
|
5105
|
+
/* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: shortenPath(result.outputDir, os9.homedir(), 60) }),
|
|
5106
|
+
/* @__PURE__ */ jsx13(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: itemsText }),
|
|
5107
|
+
displayLedger && /* @__PURE__ */ jsx13(LedgerBadge, { theme }),
|
|
4727
5108
|
/* @__PURE__ */ jsx13(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text12, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
4728
5109
|
/* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "[o]" }),
|
|
4729
5110
|
" reveal in folder"
|
|
@@ -4765,18 +5146,206 @@ function ErrorView({ message, hint, columns, theme }) {
|
|
|
4765
5146
|
}
|
|
4766
5147
|
|
|
4767
5148
|
// src/components/views/settings-view.tsx
|
|
4768
|
-
import { useState as useState6, useMemo as useMemo2, useCallback } from "react";
|
|
5149
|
+
import { useState as useState6, useMemo as useMemo2, useCallback, useEffect as useEffect6 } from "react";
|
|
4769
5150
|
import { Box as Box12, Text as Text14, useInput as useInput2 } from "ink";
|
|
4770
|
-
import
|
|
4771
|
-
import
|
|
4772
|
-
import
|
|
4773
|
-
|
|
5151
|
+
import fs15 from "fs";
|
|
5152
|
+
import path15 from "path";
|
|
5153
|
+
import os11 from "os";
|
|
5154
|
+
|
|
5155
|
+
// src/lib/diagnostics/doctor.ts
|
|
5156
|
+
import { execFile } from "child_process";
|
|
5157
|
+
import { promisify } from "util";
|
|
5158
|
+
import os10 from "os";
|
|
5159
|
+
import fs14 from "fs";
|
|
5160
|
+
import path14 from "path";
|
|
5161
|
+
var execFileAsync = promisify(execFile);
|
|
5162
|
+
async function defaultCheckBinary(bin) {
|
|
5163
|
+
const versionFlag = bin === "ffmpeg" ? "-version" : "--version";
|
|
5164
|
+
try {
|
|
5165
|
+
await execFileAsync(bin, [versionFlag], { timeout: 3e3 });
|
|
5166
|
+
return true;
|
|
5167
|
+
} catch {
|
|
5168
|
+
if (bin === "gallery-dl" || bin === "yt-dlp") {
|
|
5169
|
+
try {
|
|
5170
|
+
const resolved = resolveEngineCommand(bin);
|
|
5171
|
+
await execFileAsync(resolved.command, [...resolved.baseArgs, versionFlag], { timeout: 3e3 });
|
|
5172
|
+
return true;
|
|
5173
|
+
} catch {
|
|
5174
|
+
return false;
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
return false;
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
async function runDoctor(options = {}) {
|
|
5181
|
+
const check = options.binaryChecker ?? defaultCheckBinary;
|
|
5182
|
+
const [gdl, ytdlp, ffmpeg] = await Promise.all([
|
|
5183
|
+
check("gallery-dl"),
|
|
5184
|
+
check("yt-dlp"),
|
|
5185
|
+
check("ffmpeg")
|
|
5186
|
+
]);
|
|
5187
|
+
const availableCookies = detectAvailableCookies(options.cookiesDir);
|
|
5188
|
+
const cookieStatus = {
|
|
5189
|
+
instagram: availableCookies.includes("instagram"),
|
|
5190
|
+
tiktok: availableCookies.includes("tiktok"),
|
|
5191
|
+
facebook: availableCookies.includes("facebook"),
|
|
5192
|
+
x: availableCookies.includes("x")
|
|
5193
|
+
};
|
|
5194
|
+
let storageOk = false;
|
|
5195
|
+
try {
|
|
5196
|
+
const testDir = options.storageDir || path14.join(os10.homedir(), ".open-nami");
|
|
5197
|
+
fs14.mkdirSync(testDir, { recursive: true });
|
|
5198
|
+
const testFile = path14.join(testDir, `.doctor_test_${Date.now()}_${Math.random().toString(36).slice(2)}`);
|
|
5199
|
+
fs14.writeFileSync(testFile, "ok");
|
|
5200
|
+
fs14.unlinkSync(testFile);
|
|
5201
|
+
storageOk = true;
|
|
5202
|
+
} catch {
|
|
5203
|
+
}
|
|
5204
|
+
return {
|
|
5205
|
+
nodeVersion: process.version,
|
|
5206
|
+
binaries: {
|
|
5207
|
+
"gallery-dl": gdl,
|
|
5208
|
+
"yt-dlp": ytdlp,
|
|
5209
|
+
ffmpeg
|
|
5210
|
+
},
|
|
5211
|
+
cookies: cookieStatus,
|
|
5212
|
+
storageOk
|
|
5213
|
+
};
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
// src/components/views/settings-view.tsx
|
|
5217
|
+
import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4774
5218
|
function expandDir(dir) {
|
|
4775
5219
|
if (dir.startsWith("~/") || dir.startsWith("~\\") || dir === "~") {
|
|
4776
|
-
return
|
|
5220
|
+
return path15.join(os11.homedir(), dir.slice(1));
|
|
4777
5221
|
}
|
|
4778
5222
|
return dir;
|
|
4779
5223
|
}
|
|
5224
|
+
function resolveBrowserOptions(detected) {
|
|
5225
|
+
const available = detected.filter((b) => b.available !== false);
|
|
5226
|
+
return ["none", ...available.map((b) => b.id)];
|
|
5227
|
+
}
|
|
5228
|
+
function cycleBrowserOption(current, options, forward = true) {
|
|
5229
|
+
if (options.length === 0) return "none";
|
|
5230
|
+
const idx = options.indexOf(current);
|
|
5231
|
+
const currentIndex = idx === -1 ? 0 : idx;
|
|
5232
|
+
const nextIdx = forward ? (currentIndex + 1) % options.length : (currentIndex - 1 + options.length) % options.length;
|
|
5233
|
+
return options[nextIdx];
|
|
5234
|
+
}
|
|
5235
|
+
function formatDoctorStorage(storageOk) {
|
|
5236
|
+
return {
|
|
5237
|
+
ok: storageOk,
|
|
5238
|
+
text: storageOk ? "\u2713 SQLite ready" : "\u2717 SQLite unavailable"
|
|
5239
|
+
};
|
|
5240
|
+
}
|
|
5241
|
+
function formatDoctorCookies(cookies) {
|
|
5242
|
+
const platforms = Object.entries(cookies).filter(([_, ok]) => ok).map(([name]) => name);
|
|
5243
|
+
const count = platforms.length;
|
|
5244
|
+
const text = count > 0 ? `\u2713 ${count} (${platforms.join(", ")})` : "none detected";
|
|
5245
|
+
return { count, platforms, text };
|
|
5246
|
+
}
|
|
5247
|
+
function DoctorCard({ boxWidth, theme, report }) {
|
|
5248
|
+
const cookieSummary = report ? formatDoctorCookies(report.cookies) : { count: 0, platforms: [], text: "" };
|
|
5249
|
+
const storageSummary = report ? formatDoctorStorage(report.storageOk) : { ok: false, text: "" };
|
|
5250
|
+
return /* @__PURE__ */ jsxs12(
|
|
5251
|
+
Box12,
|
|
5252
|
+
{
|
|
5253
|
+
width: boxWidth,
|
|
5254
|
+
borderStyle: "round",
|
|
5255
|
+
borderColor: theme.gray,
|
|
5256
|
+
borderDimColor: theme.dimSecondary,
|
|
5257
|
+
paddingX: 2,
|
|
5258
|
+
flexDirection: "column",
|
|
5259
|
+
children: [
|
|
5260
|
+
/* @__PURE__ */ jsxs12(Box12, { justifyContent: "space-between", children: [
|
|
5261
|
+
/* @__PURE__ */ jsx15(Text14, { color: theme.primary, bold: true, children: "System Diagnostics" }),
|
|
5262
|
+
/* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: report ? report.nodeVersion : "checking\u2026" })
|
|
5263
|
+
] }),
|
|
5264
|
+
report ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
5265
|
+
/* @__PURE__ */ jsxs12(Box12, { marginTop: 0, children: [
|
|
5266
|
+
/* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
5267
|
+
"Engines:",
|
|
5268
|
+
" "
|
|
5269
|
+
] }),
|
|
5270
|
+
/* @__PURE__ */ jsxs12(
|
|
5271
|
+
Text14,
|
|
5272
|
+
{
|
|
5273
|
+
color: report.binaries["gallery-dl"] ? "green" : "red",
|
|
5274
|
+
dimColor: !report.binaries["gallery-dl"] && theme.dimSecondary,
|
|
5275
|
+
children: [
|
|
5276
|
+
report.binaries["gallery-dl"] ? "\u2713" : "\u2717",
|
|
5277
|
+
" gallery-dl"
|
|
5278
|
+
]
|
|
5279
|
+
}
|
|
5280
|
+
),
|
|
5281
|
+
/* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
5282
|
+
" ",
|
|
5283
|
+
"\xB7",
|
|
5284
|
+
" "
|
|
5285
|
+
] }),
|
|
5286
|
+
/* @__PURE__ */ jsxs12(
|
|
5287
|
+
Text14,
|
|
5288
|
+
{
|
|
5289
|
+
color: report.binaries["yt-dlp"] ? "green" : "red",
|
|
5290
|
+
dimColor: !report.binaries["yt-dlp"] && theme.dimSecondary,
|
|
5291
|
+
children: [
|
|
5292
|
+
report.binaries["yt-dlp"] ? "\u2713" : "\u2717",
|
|
5293
|
+
" yt-dlp"
|
|
5294
|
+
]
|
|
5295
|
+
}
|
|
5296
|
+
),
|
|
5297
|
+
/* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
5298
|
+
" ",
|
|
5299
|
+
"\xB7",
|
|
5300
|
+
" "
|
|
5301
|
+
] }),
|
|
5302
|
+
/* @__PURE__ */ jsxs12(
|
|
5303
|
+
Text14,
|
|
5304
|
+
{
|
|
5305
|
+
color: report.binaries.ffmpeg ? "green" : "red",
|
|
5306
|
+
dimColor: !report.binaries.ffmpeg && theme.dimSecondary,
|
|
5307
|
+
children: [
|
|
5308
|
+
report.binaries.ffmpeg ? "\u2713" : "\u2717",
|
|
5309
|
+
" ffmpeg"
|
|
5310
|
+
]
|
|
5311
|
+
}
|
|
5312
|
+
)
|
|
5313
|
+
] }),
|
|
5314
|
+
/* @__PURE__ */ jsxs12(Box12, { justifyContent: "space-between", marginTop: 0, children: [
|
|
5315
|
+
/* @__PURE__ */ jsxs12(Box12, { children: [
|
|
5316
|
+
/* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
5317
|
+
"Storage:",
|
|
5318
|
+
" "
|
|
5319
|
+
] }),
|
|
5320
|
+
/* @__PURE__ */ jsx15(
|
|
5321
|
+
Text14,
|
|
5322
|
+
{
|
|
5323
|
+
color: storageSummary.ok ? "green" : "red",
|
|
5324
|
+
dimColor: !storageSummary.ok && theme.dimSecondary,
|
|
5325
|
+
children: storageSummary.text
|
|
5326
|
+
}
|
|
5327
|
+
)
|
|
5328
|
+
] }),
|
|
5329
|
+
/* @__PURE__ */ jsxs12(Box12, { children: [
|
|
5330
|
+
/* @__PURE__ */ jsxs12(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: [
|
|
5331
|
+
"Cookies:",
|
|
5332
|
+
" "
|
|
5333
|
+
] }),
|
|
5334
|
+
/* @__PURE__ */ jsx15(
|
|
5335
|
+
Text14,
|
|
5336
|
+
{
|
|
5337
|
+
color: cookieSummary.count > 0 ? theme.primary : theme.gray,
|
|
5338
|
+
dimColor: cookieSummary.count === 0 && theme.dimSecondary,
|
|
5339
|
+
children: cookieSummary.text
|
|
5340
|
+
}
|
|
5341
|
+
)
|
|
5342
|
+
] })
|
|
5343
|
+
] })
|
|
5344
|
+
] }) : /* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: "checking system engines and storage\u2026" })
|
|
5345
|
+
]
|
|
5346
|
+
}
|
|
5347
|
+
);
|
|
5348
|
+
}
|
|
4780
5349
|
var LIMIT_OPTIONS = [void 0, 10, 25, 50, 100];
|
|
4781
5350
|
function SettingsView({
|
|
4782
5351
|
boxWidth,
|
|
@@ -4785,7 +5354,10 @@ function SettingsView({
|
|
|
4785
5354
|
focusedField: controlledFocus,
|
|
4786
5355
|
onFocusChange,
|
|
4787
5356
|
onSave,
|
|
4788
|
-
onCancel
|
|
5357
|
+
onCancel,
|
|
5358
|
+
doctorChecker,
|
|
5359
|
+
initialReport,
|
|
5360
|
+
browsers: browsersProp
|
|
4789
5361
|
}) {
|
|
4790
5362
|
const initialConfig = useMemo2(() => loadConfig(), []);
|
|
4791
5363
|
const [internalFocus, setInternalFocus] = useState6("cookies");
|
|
@@ -4797,16 +5369,61 @@ function SettingsView({
|
|
|
4797
5369
|
},
|
|
4798
5370
|
[onFocusChange]
|
|
4799
5371
|
);
|
|
5372
|
+
const detectedBrowsers = useMemo2(() => {
|
|
5373
|
+
return browsersProp ?? listDetectedBrowsers();
|
|
5374
|
+
}, [browsersProp]);
|
|
5375
|
+
const browserOptions = useMemo2(() => {
|
|
5376
|
+
return resolveBrowserOptions(detectedBrowsers);
|
|
5377
|
+
}, [detectedBrowsers]);
|
|
4800
5378
|
const [cookiesDir, setCookiesDir] = useState6(initialConfig.cookiesDir ?? "");
|
|
5379
|
+
const [cookiesFromBrowser, setCookiesFromBrowser] = useState6(
|
|
5380
|
+
initialConfig.cookiesFromBrowser ?? "none"
|
|
5381
|
+
);
|
|
4801
5382
|
const [downloadDir, setDownloadDir] = useState6(initialConfig.downloadDir ?? "");
|
|
4802
5383
|
const [writeMetadata, setWriteMetadata] = useState6(Boolean(initialConfig.writeMetadata));
|
|
4803
5384
|
const [defaultLimit, setDefaultLimit] = useState6(initialConfig.defaultLimit);
|
|
4804
5385
|
const [saved, setSaved] = useState6(false);
|
|
5386
|
+
const [doctorReport, setDoctorReport] = useState6(initialReport ?? null);
|
|
5387
|
+
useEffect6(() => {
|
|
5388
|
+
let cancelled = false;
|
|
5389
|
+
const fetchReport = async () => {
|
|
5390
|
+
try {
|
|
5391
|
+
const options = {
|
|
5392
|
+
cookiesDir: cookiesDir.trim() ? expandDir(cookiesDir.trim()) : void 0
|
|
5393
|
+
};
|
|
5394
|
+
let report;
|
|
5395
|
+
if (doctorChecker) {
|
|
5396
|
+
const res = await doctorChecker(options);
|
|
5397
|
+
if (res && typeof res === "object" && "binaries" in res) {
|
|
5398
|
+
report = res;
|
|
5399
|
+
} else {
|
|
5400
|
+
report = await runDoctor({
|
|
5401
|
+
...options,
|
|
5402
|
+
binaryChecker: doctorChecker
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
} else {
|
|
5406
|
+
report = await runDoctor(options);
|
|
5407
|
+
}
|
|
5408
|
+
if (!cancelled) {
|
|
5409
|
+
setDoctorReport(report);
|
|
5410
|
+
}
|
|
5411
|
+
} catch {
|
|
5412
|
+
}
|
|
5413
|
+
};
|
|
5414
|
+
if (initialReport === void 0 || doctorChecker !== void 0) {
|
|
5415
|
+
fetchReport();
|
|
5416
|
+
}
|
|
5417
|
+
return () => {
|
|
5418
|
+
cancelled = true;
|
|
5419
|
+
};
|
|
5420
|
+
}, [cookiesDir, doctorChecker, initialReport]);
|
|
4805
5421
|
const handleSave = useCallback(() => {
|
|
4806
5422
|
const existing = loadConfig();
|
|
4807
5423
|
const nextConfig = {
|
|
4808
5424
|
...existing,
|
|
4809
5425
|
cookiesDir: cookiesDir.trim() ? cookiesDir.trim() : void 0,
|
|
5426
|
+
cookiesFromBrowser,
|
|
4810
5427
|
downloadDir: downloadDir.trim() ? downloadDir.trim() : void 0,
|
|
4811
5428
|
writeMetadata,
|
|
4812
5429
|
defaultLimit
|
|
@@ -4814,7 +5431,15 @@ function SettingsView({
|
|
|
4814
5431
|
saveConfig(nextConfig);
|
|
4815
5432
|
setSaved(true);
|
|
4816
5433
|
onSave();
|
|
4817
|
-
}, [cookiesDir, downloadDir, writeMetadata, defaultLimit, onSave]);
|
|
5434
|
+
}, [cookiesDir, cookiesFromBrowser, downloadDir, writeMetadata, defaultLimit, onSave]);
|
|
5435
|
+
const browserFeedback = useMemo2(() => {
|
|
5436
|
+
if (!cookiesFromBrowser || cookiesFromBrowser === "none") {
|
|
5437
|
+
return "manual cookie files only";
|
|
5438
|
+
}
|
|
5439
|
+
const found = detectedBrowsers.find((b) => b.id === cookiesFromBrowser);
|
|
5440
|
+
const browserName = found ? found.name : cookiesFromBrowser;
|
|
5441
|
+
return `live session cookies from ${browserName}`;
|
|
5442
|
+
}, [cookiesFromBrowser, detectedBrowsers]);
|
|
4818
5443
|
const cookieFeedback = useMemo2(() => {
|
|
4819
5444
|
const dir = cookiesDir.trim();
|
|
4820
5445
|
if (!dir) {
|
|
@@ -4825,7 +5450,7 @@ function SettingsView({
|
|
|
4825
5450
|
};
|
|
4826
5451
|
}
|
|
4827
5452
|
const expanded = expandDir(dir);
|
|
4828
|
-
if (!
|
|
5453
|
+
if (!fs15.existsSync(expanded)) {
|
|
4829
5454
|
return {
|
|
4830
5455
|
ok: false,
|
|
4831
5456
|
text: "directory does not exist yet"
|
|
@@ -4848,14 +5473,20 @@ function SettingsView({
|
|
|
4848
5473
|
if (!dir) {
|
|
4849
5474
|
return "default: platform Downloads / Open Nami";
|
|
4850
5475
|
}
|
|
4851
|
-
return `target: ${
|
|
5476
|
+
return `target: ${path15.resolve(expandDir(dir))}`;
|
|
4852
5477
|
}, [downloadDir]);
|
|
4853
5478
|
const cycleFocus = useCallback((forward = true) => {
|
|
4854
|
-
const fields = ["cookies", "download", "metadata", "limit"];
|
|
5479
|
+
const fields = ["cookies", "browser", "download", "metadata", "limit"];
|
|
4855
5480
|
const idx = fields.indexOf(focusedField);
|
|
4856
5481
|
const nextIdx = forward ? (idx + 1) % fields.length : (idx - 1 + fields.length) % fields.length;
|
|
4857
5482
|
setField(fields[nextIdx]);
|
|
4858
5483
|
}, [focusedField, setField]);
|
|
5484
|
+
const cycleBrowser = useCallback((forward = true) => {
|
|
5485
|
+
setCookiesFromBrowser((prev) => {
|
|
5486
|
+
const next = cycleBrowserOption(prev ?? "none", browserOptions, forward);
|
|
5487
|
+
return next;
|
|
5488
|
+
});
|
|
5489
|
+
}, [browserOptions]);
|
|
4859
5490
|
const cycleLimit = useCallback(() => {
|
|
4860
5491
|
setDefaultLimit((prev) => {
|
|
4861
5492
|
const idx = LIMIT_OPTIONS.indexOf(prev);
|
|
@@ -4872,6 +5503,20 @@ function SettingsView({
|
|
|
4872
5503
|
handleSave();
|
|
4873
5504
|
return;
|
|
4874
5505
|
}
|
|
5506
|
+
if (focusedField === "browser") {
|
|
5507
|
+
if (input === " " || key.return) {
|
|
5508
|
+
cycleBrowser(true);
|
|
5509
|
+
return;
|
|
5510
|
+
}
|
|
5511
|
+
if (key.tab || key.downArrow) {
|
|
5512
|
+
cycleFocus(true);
|
|
5513
|
+
return;
|
|
5514
|
+
}
|
|
5515
|
+
if (key.upArrow) {
|
|
5516
|
+
cycleFocus(false);
|
|
5517
|
+
return;
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
4875
5520
|
if (focusedField === "metadata") {
|
|
4876
5521
|
if (input === " " || key.return) {
|
|
4877
5522
|
setWriteMetadata((prev) => !prev);
|
|
@@ -4912,7 +5557,7 @@ function SettingsView({
|
|
|
4912
5557
|
{
|
|
4913
5558
|
value: cookiesDir,
|
|
4914
5559
|
onChange: setCookiesDir,
|
|
4915
|
-
onSubmit: () => setField("
|
|
5560
|
+
onSubmit: () => setField("browser"),
|
|
4916
5561
|
focus: focusedField === "cookies",
|
|
4917
5562
|
onTab: () => cycleFocus(true),
|
|
4918
5563
|
onArrowDown: () => cycleFocus(true),
|
|
@@ -4925,6 +5570,26 @@ function SettingsView({
|
|
|
4925
5570
|
),
|
|
4926
5571
|
/* @__PURE__ */ jsx15(Text14, { color: cookieFeedback.ok ? theme.primary : theme.gray, dimColor: !cookieFeedback.ok && theme.dimSecondary, children: cookieFeedback.text }),
|
|
4927
5572
|
/* @__PURE__ */ jsx15(Gap2, {}),
|
|
5573
|
+
/* @__PURE__ */ jsxs12(
|
|
5574
|
+
Box12,
|
|
5575
|
+
{
|
|
5576
|
+
width: boxWidth,
|
|
5577
|
+
borderStyle: "round",
|
|
5578
|
+
borderColor: focusedField === "browser" ? theme.primary : theme.gray,
|
|
5579
|
+
borderDimColor: focusedField !== "browser",
|
|
5580
|
+
paddingX: 2,
|
|
5581
|
+
justifyContent: "space-between",
|
|
5582
|
+
children: [
|
|
5583
|
+
/* @__PURE__ */ jsxs12(Text14, { color: focusedField === "browser" ? theme.primary : theme.gray, bold: focusedField === "browser", children: [
|
|
5584
|
+
focusedField === "browser" ? "\u25CF " : "\u25CB ",
|
|
5585
|
+
"Browser Cookie Bridge"
|
|
5586
|
+
] }),
|
|
5587
|
+
/* @__PURE__ */ jsx15(Text14, { color: cookiesFromBrowser && cookiesFromBrowser !== "none" ? theme.primary : theme.gray, bold: true, children: `[${(cookiesFromBrowser ?? "none").toUpperCase()}]` })
|
|
5588
|
+
]
|
|
5589
|
+
}
|
|
5590
|
+
),
|
|
5591
|
+
/* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: browserFeedback }),
|
|
5592
|
+
/* @__PURE__ */ jsx15(Gap2, {}),
|
|
4928
5593
|
/* @__PURE__ */ jsx15(
|
|
4929
5594
|
FramedInput,
|
|
4930
5595
|
{
|
|
@@ -4986,12 +5651,14 @@ function SettingsView({
|
|
|
4986
5651
|
}
|
|
4987
5652
|
),
|
|
4988
5653
|
/* @__PURE__ */ jsx15(Gap2, {}),
|
|
5654
|
+
/* @__PURE__ */ jsx15(DoctorCard, { boxWidth, theme, report: doctorReport }),
|
|
5655
|
+
/* @__PURE__ */ jsx15(Gap2, {}),
|
|
4989
5656
|
/* @__PURE__ */ jsx15(Text14, { color: theme.gray, dimColor: theme.dimSecondary, children: saved ? "\u2713 preferences saved to ~/.open-nami/config.json" : "tab / \u2191\u2193 switch field \xB7 space toggle/cycle \xB7 ^s / \u21B5 save \xB7 esc back" })
|
|
4990
5657
|
] });
|
|
4991
5658
|
}
|
|
4992
5659
|
|
|
4993
5660
|
// src/components/views/watcher-view.tsx
|
|
4994
|
-
import { useState as useState7, useEffect as
|
|
5661
|
+
import { useState as useState7, useEffect as useEffect7, useRef as useRef3, useCallback as useCallback2 } from "react";
|
|
4995
5662
|
import { Box as Box13, Text as Text15, useInput as useInput3 } from "ink";
|
|
4996
5663
|
import Spinner2 from "ink-spinner";
|
|
4997
5664
|
|
|
@@ -5278,9 +5945,9 @@ function startClipboardWatcher(options = {}) {
|
|
|
5278
5945
|
}
|
|
5279
5946
|
|
|
5280
5947
|
// src/lib/reveal.ts
|
|
5281
|
-
import
|
|
5948
|
+
import fs16 from "fs";
|
|
5282
5949
|
import { spawn as spawn4 } from "child_process";
|
|
5283
|
-
import
|
|
5950
|
+
import path16 from "path";
|
|
5284
5951
|
function buildOpenBrowserCommand(url, platform = process.platform) {
|
|
5285
5952
|
try {
|
|
5286
5953
|
const parsed = new URL(url.trim());
|
|
@@ -5307,12 +5974,12 @@ function openBrowser(url) {
|
|
|
5307
5974
|
}
|
|
5308
5975
|
}
|
|
5309
5976
|
function getRevealCommand(targetPath, platform = process.platform, isDirectory) {
|
|
5310
|
-
const pathModule = platform === "win32" ?
|
|
5977
|
+
const pathModule = platform === "win32" ? path16.win32 : platform === "darwin" ? path16.posix : path16;
|
|
5311
5978
|
const resolved = pathModule.resolve(targetPath);
|
|
5312
5979
|
const isDir = isDirectory !== void 0 ? isDirectory : (() => {
|
|
5313
5980
|
try {
|
|
5314
|
-
if (
|
|
5315
|
-
return
|
|
5981
|
+
if (fs16.existsSync(resolved)) {
|
|
5982
|
+
return fs16.statSync(resolved).isDirectory();
|
|
5316
5983
|
}
|
|
5317
5984
|
} catch {
|
|
5318
5985
|
}
|
|
@@ -5360,13 +6027,13 @@ function WatcherView({
|
|
|
5360
6027
|
mediaChoiceRef.current = mediaChoice;
|
|
5361
6028
|
const writeMetadataRef = useRef3(writeMetadata);
|
|
5362
6029
|
writeMetadataRef.current = writeMetadata;
|
|
5363
|
-
|
|
6030
|
+
useEffect7(() => {
|
|
5364
6031
|
const pulseTimer = setInterval(() => {
|
|
5365
6032
|
setPulseTick((t) => t + 1);
|
|
5366
6033
|
}, 120);
|
|
5367
6034
|
return () => clearInterval(pulseTimer);
|
|
5368
6035
|
}, []);
|
|
5369
|
-
|
|
6036
|
+
useEffect7(() => {
|
|
5370
6037
|
const watcher = startClipboardWatcher({
|
|
5371
6038
|
choice: mediaChoiceRef.current,
|
|
5372
6039
|
getChoice: () => mediaChoiceRef.current,
|
|
@@ -5440,7 +6107,7 @@ function WatcherView({
|
|
|
5440
6107
|
const target = outputDir || resolveDefaultDownloadDir("instagram", "watcher");
|
|
5441
6108
|
revealInFileManager(target);
|
|
5442
6109
|
}, [outputDir]);
|
|
5443
|
-
|
|
6110
|
+
useEffect7(() => {
|
|
5444
6111
|
if (!onRegisterClickTargets) return;
|
|
5445
6112
|
const targets = [
|
|
5446
6113
|
{ match: "Target:", padX: 1, action: () => {
|
|
@@ -5606,10 +6273,10 @@ function WatcherView({
|
|
|
5606
6273
|
}
|
|
5607
6274
|
|
|
5608
6275
|
// src/components/views/batch-view.tsx
|
|
5609
|
-
import { useState as useState8, useRef as useRef4, useCallback as useCallback3, useEffect as
|
|
6276
|
+
import { useState as useState8, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect8 } from "react";
|
|
5610
6277
|
import { Box as Box14, Text as Text16, useInput as useInput4 } from "ink";
|
|
5611
6278
|
import Spinner3 from "ink-spinner";
|
|
5612
|
-
import
|
|
6279
|
+
import fs17 from "fs";
|
|
5613
6280
|
|
|
5614
6281
|
// src/lib/batch.ts
|
|
5615
6282
|
import fsPromises5 from "fs/promises";
|
|
@@ -5816,7 +6483,7 @@ function BatchView({
|
|
|
5816
6483
|
setErrorMessage("Please enter a valid batch file path");
|
|
5817
6484
|
return;
|
|
5818
6485
|
}
|
|
5819
|
-
if (!
|
|
6486
|
+
if (!fs17.existsSync(trimmed)) {
|
|
5820
6487
|
setErrorMessage(`File not found: ${trimmed}`);
|
|
5821
6488
|
return;
|
|
5822
6489
|
}
|
|
@@ -6148,7 +6815,7 @@ function BatchView({
|
|
|
6148
6815
|
}
|
|
6149
6816
|
}
|
|
6150
6817
|
}, { isActive: Boolean(process.stdin.isTTY) });
|
|
6151
|
-
|
|
6818
|
+
useEffect8(() => {
|
|
6152
6819
|
if (!onRegisterClickTargets) return;
|
|
6153
6820
|
const targets = [];
|
|
6154
6821
|
if (phase === "input") {
|
|
@@ -6649,6 +7316,80 @@ function frameRowSpan(row) {
|
|
|
6649
7316
|
return [first + 1, line.trimEnd().length];
|
|
6650
7317
|
}
|
|
6651
7318
|
|
|
7319
|
+
// src/lib/engines/core/use-engine-events.ts
|
|
7320
|
+
import { useState as useState9, useEffect as useEffect9 } from "react";
|
|
7321
|
+
var INITIAL_ENGINE_EVENT_STATE = {
|
|
7322
|
+
isDownloading: false,
|
|
7323
|
+
activeJob: void 0,
|
|
7324
|
+
progress: void 0,
|
|
7325
|
+
lastError: void 0,
|
|
7326
|
+
lastResult: void 0
|
|
7327
|
+
};
|
|
7328
|
+
function subscribeToEngineEvents(bus, onChange) {
|
|
7329
|
+
let state = { ...INITIAL_ENGINE_EVENT_STATE };
|
|
7330
|
+
const onJobStart = (payload) => {
|
|
7331
|
+
state = {
|
|
7332
|
+
...state,
|
|
7333
|
+
isDownloading: true,
|
|
7334
|
+
activeJob: payload,
|
|
7335
|
+
progress: void 0,
|
|
7336
|
+
lastError: void 0,
|
|
7337
|
+
lastResult: void 0
|
|
7338
|
+
};
|
|
7339
|
+
onChange(state);
|
|
7340
|
+
};
|
|
7341
|
+
const onJobProgress = (payload) => {
|
|
7342
|
+
state = {
|
|
7343
|
+
...state,
|
|
7344
|
+
isDownloading: true,
|
|
7345
|
+
progress: payload
|
|
7346
|
+
};
|
|
7347
|
+
onChange(state);
|
|
7348
|
+
};
|
|
7349
|
+
const onJobComplete = (payload) => {
|
|
7350
|
+
state = {
|
|
7351
|
+
...state,
|
|
7352
|
+
isDownloading: false,
|
|
7353
|
+
lastResult: payload.result
|
|
7354
|
+
};
|
|
7355
|
+
onChange(state);
|
|
7356
|
+
};
|
|
7357
|
+
const onJobError = (payload) => {
|
|
7358
|
+
state = {
|
|
7359
|
+
...state,
|
|
7360
|
+
isDownloading: false,
|
|
7361
|
+
lastError: payload.error
|
|
7362
|
+
};
|
|
7363
|
+
onChange(state);
|
|
7364
|
+
};
|
|
7365
|
+
const unsubStart = bus.on("job:start", onJobStart);
|
|
7366
|
+
const unsubProgress = bus.on("job:progress", onJobProgress);
|
|
7367
|
+
const unsubComplete = bus.on("job:complete", onJobComplete);
|
|
7368
|
+
const unsubError = bus.on("job:error", onJobError);
|
|
7369
|
+
return () => {
|
|
7370
|
+
if (typeof bus.off === "function") {
|
|
7371
|
+
bus.off("job:start", onJobStart);
|
|
7372
|
+
bus.off("job:progress", onJobProgress);
|
|
7373
|
+
bus.off("job:complete", onJobComplete);
|
|
7374
|
+
bus.off("job:error", onJobError);
|
|
7375
|
+
}
|
|
7376
|
+
unsubStart?.();
|
|
7377
|
+
unsubProgress?.();
|
|
7378
|
+
unsubComplete?.();
|
|
7379
|
+
unsubError?.();
|
|
7380
|
+
};
|
|
7381
|
+
}
|
|
7382
|
+
function useEngineEvents(customBus) {
|
|
7383
|
+
const bus = customBus || defaultTaskManager.events;
|
|
7384
|
+
const [state, setState] = useState9(() => ({
|
|
7385
|
+
...INITIAL_ENGINE_EVENT_STATE
|
|
7386
|
+
}));
|
|
7387
|
+
useEffect9(() => {
|
|
7388
|
+
return subscribeToEngineEvents(bus, setState);
|
|
7389
|
+
}, [bus]);
|
|
7390
|
+
return state;
|
|
7391
|
+
}
|
|
7392
|
+
|
|
6652
7393
|
// src/app.tsx
|
|
6653
7394
|
import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
6654
7395
|
var TAGLINE = "grab any profile. paste. download. done.";
|
|
@@ -6728,7 +7469,7 @@ function App({
|
|
|
6728
7469
|
writeMetadata,
|
|
6729
7470
|
version = "1.0.0"
|
|
6730
7471
|
}) {
|
|
6731
|
-
const [themeMode, setThemeMode] =
|
|
7472
|
+
const [themeMode, setThemeMode] = useState10(initialThemeMode);
|
|
6732
7473
|
const cycleTheme = useCallback4(() => {
|
|
6733
7474
|
setThemeMode(nextThemeMode);
|
|
6734
7475
|
}, []);
|
|
@@ -6763,18 +7504,19 @@ function InnerApp({
|
|
|
6763
7504
|
const theme = useTheme();
|
|
6764
7505
|
const { exit } = useApp();
|
|
6765
7506
|
const { stdout } = useStdout3();
|
|
7507
|
+
const engineEvents = useEngineEvents();
|
|
6766
7508
|
const config = useMemo3(() => loadConfig(), []);
|
|
6767
7509
|
const effectiveLimit = limit ?? config.defaultLimit;
|
|
6768
7510
|
const effectiveWriteMetadata = writeMetadata ?? Boolean(config.writeMetadata);
|
|
6769
|
-
const [urlInput, setUrlInput] =
|
|
6770
|
-
const [clipboardUrl, setClipboardUrl] =
|
|
6771
|
-
const [settingsFocus, setSettingsFocus] =
|
|
6772
|
-
const [batchClickTargets, setBatchClickTargets] =
|
|
6773
|
-
const [watcherClickTargets, setWatcherClickTargets] =
|
|
7511
|
+
const [urlInput, setUrlInput] = useState10(initialInput ?? "");
|
|
7512
|
+
const [clipboardUrl, setClipboardUrl] = useState10(void 0);
|
|
7513
|
+
const [settingsFocus, setSettingsFocus] = useState10("cookies");
|
|
7514
|
+
const [batchClickTargets, setBatchClickTargets] = useState10([]);
|
|
7515
|
+
const [watcherClickTargets, setWatcherClickTargets] = useState10([]);
|
|
6774
7516
|
const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
|
|
6775
7517
|
const boxWidth = Math.max(14, Math.min(64, columns - 6));
|
|
6776
7518
|
const contentWidth = Math.max(10, Math.min(columns - 4, 78));
|
|
6777
|
-
const [phase, setPhase] =
|
|
7519
|
+
const [phase, setPhase] = useState10(() => {
|
|
6778
7520
|
if (initialInput) {
|
|
6779
7521
|
const parsed = parseProfileInput(initialInput);
|
|
6780
7522
|
if (parsed.platform !== "unknown" && parsed.cleanUrl) {
|
|
@@ -6786,7 +7528,7 @@ function InnerApp({
|
|
|
6786
7528
|
}
|
|
6787
7529
|
return { name: "input" };
|
|
6788
7530
|
});
|
|
6789
|
-
|
|
7531
|
+
useEffect10(() => {
|
|
6790
7532
|
try {
|
|
6791
7533
|
const clip = readClipboard();
|
|
6792
7534
|
if (clip && (clip.startsWith("http://") || clip.startsWith("https://") || clip.startsWith("@"))) {
|
|
@@ -6799,7 +7541,7 @@ function InnerApp({
|
|
|
6799
7541
|
const clipboardAccepted = Boolean(clipboardUrl) && urlInput === clipboardUrl;
|
|
6800
7542
|
const abortControllerRef = useRef5(null);
|
|
6801
7543
|
const probingTimeoutRef = useRef5(null);
|
|
6802
|
-
|
|
7544
|
+
useEffect10(() => {
|
|
6803
7545
|
return () => {
|
|
6804
7546
|
abortControllerRef.current?.abort();
|
|
6805
7547
|
if (probingTimeoutRef.current) {
|
|
@@ -7159,7 +7901,7 @@ function InnerApp({
|
|
|
7159
7901
|
{
|
|
7160
7902
|
profile: phase.profile,
|
|
7161
7903
|
choice: phase.choice,
|
|
7162
|
-
progress: phase.progress,
|
|
7904
|
+
progress: engineEvents.progress ?? phase.progress,
|
|
7163
7905
|
outputDir: resolveDefaultDownloadDir(phase.profile.platform, phase.profile.username, outputDir),
|
|
7164
7906
|
writeMetadata: effectiveWriteMetadata,
|
|
7165
7907
|
limit: effectiveLimit,
|
|
@@ -7439,7 +8181,7 @@ import { jsx as jsx19 } from "react/jsx-runtime";
|
|
|
7439
8181
|
var resolveVersion = () => {
|
|
7440
8182
|
try {
|
|
7441
8183
|
if (typeof import.meta !== "undefined" && import.meta?.url) {
|
|
7442
|
-
return
|
|
8184
|
+
return createRequire2(import.meta.url)("../package.json").version;
|
|
7443
8185
|
}
|
|
7444
8186
|
} catch {
|
|
7445
8187
|
}
|
|
@@ -7463,6 +8205,7 @@ var HELP = `
|
|
|
7463
8205
|
Options:
|
|
7464
8206
|
-h, --help Show this help message
|
|
7465
8207
|
-v, --version Show version
|
|
8208
|
+
--doctor Run environment & dependency diagnostics
|
|
7466
8209
|
-o, --output Set custom output directory
|
|
7467
8210
|
-w, --watch Watch clipboard for profile links and auto-download
|
|
7468
8211
|
-b, --batch <file> Download multiple profiles listed in a text file (one URL/user per line)
|
|
@@ -7509,6 +8252,24 @@ async function main() {
|
|
|
7509
8252
|
console.log(`open-nami v${VERSION}`);
|
|
7510
8253
|
process.exit(0);
|
|
7511
8254
|
}
|
|
8255
|
+
if (args.includes("--doctor")) {
|
|
8256
|
+
const report = await runDoctor();
|
|
8257
|
+
console.log(`
|
|
8258
|
+
\x1B[36mOpen Nami Doctor\x1B[0m (v${VERSION})
|
|
8259
|
+
`);
|
|
8260
|
+
console.log(` \x1B[1mRuntime:\x1B[0m Node.js ${report.nodeVersion}`);
|
|
8261
|
+
console.log(` \x1B[1mStorage:\x1B[0m ${report.storageOk ? "\x1B[32m\u2713 Ready\x1B[0m" : "\x1B[31m\u2717 Inaccessible\x1B[0m"}`);
|
|
8262
|
+
console.log(` \x1B[1mEngines:\x1B[0m`);
|
|
8263
|
+
for (const [bin, ok] of Object.entries(report.binaries)) {
|
|
8264
|
+
console.log(` ${ok ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m"} ${bin}`);
|
|
8265
|
+
}
|
|
8266
|
+
console.log(` \x1B[1mCookies:\x1B[0m`);
|
|
8267
|
+
for (const [platform, ok] of Object.entries(report.cookies)) {
|
|
8268
|
+
console.log(` ${ok ? "\x1B[32m\u2713\x1B[0m" : "\x1B[90m\u25CB\x1B[0m"} ${platform}`);
|
|
8269
|
+
}
|
|
8270
|
+
console.log();
|
|
8271
|
+
process.exit(0);
|
|
8272
|
+
}
|
|
7512
8273
|
const completionIdx = args.findIndex((arg) => arg === "--completion" || arg.startsWith("--completion="));
|
|
7513
8274
|
if (completionIdx !== -1) {
|
|
7514
8275
|
const arg = args[completionIdx];
|