open-nami 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1018 -207
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -9,8 +9,8 @@ var require_package = __commonJS({
|
|
|
9
9
|
"package.json"(exports, module) {
|
|
10
10
|
module.exports = {
|
|
11
11
|
name: "open-nami",
|
|
12
|
-
version: "1.0
|
|
13
|
-
description: "Open Nami
|
|
12
|
+
version: "1.1.0",
|
|
13
|
+
description: "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
|
|
14
14
|
type: "module",
|
|
15
15
|
bin: {
|
|
16
16
|
"open-nami": "dist/cli.js",
|
|
@@ -1437,7 +1437,7 @@ function parseProfileInput(input) {
|
|
|
1437
1437
|
}
|
|
1438
1438
|
|
|
1439
1439
|
// src/lib/engines/dispatcher.ts
|
|
1440
|
-
import
|
|
1440
|
+
import path11 from "path";
|
|
1441
1441
|
import os8 from "os";
|
|
1442
1442
|
import fs11 from "fs/promises";
|
|
1443
1443
|
|
|
@@ -1807,6 +1807,7 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1807
1807
|
const anyNode = node;
|
|
1808
1808
|
const baseId = node.shortcode || node.id || anyNode.pk;
|
|
1809
1809
|
const date = node.taken_at_timestamp || anyNode.taken_at;
|
|
1810
|
+
const caption = anyNode.edge_media_to_caption?.edges?.[0]?.node?.text || anyNode.caption?.text || (typeof anyNode.caption === "string" ? anyNode.caption : void 0);
|
|
1810
1811
|
const childrenEdges = node.edge_sidecar_to_children?.edges;
|
|
1811
1812
|
const carouselMedia = anyNode.carousel_media;
|
|
1812
1813
|
const childrenList = childrenEdges ? childrenEdges.map((e) => e.node) : Array.isArray(carouselMedia) ? carouselMedia : [];
|
|
@@ -1815,7 +1816,13 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1815
1816
|
const subItems = parseIgNodeToMediaItems(childNode, username);
|
|
1816
1817
|
for (const item of subItems) {
|
|
1817
1818
|
item.id = `${baseId}_${index + 1}`;
|
|
1818
|
-
item.filename = `${username}_${date
|
|
1819
|
+
item.filename = `${username}_${date ? `${date}_` : ""}${baseId}_${index + 1}`;
|
|
1820
|
+
item.author = username;
|
|
1821
|
+
item.platform = "instagram";
|
|
1822
|
+
item.caption = item.caption || caption;
|
|
1823
|
+
item.carouselId = baseId;
|
|
1824
|
+
item.carouselIndex = index + 1;
|
|
1825
|
+
item.carouselTotal = childrenList.length;
|
|
1819
1826
|
items.push(item);
|
|
1820
1827
|
}
|
|
1821
1828
|
});
|
|
@@ -1829,9 +1836,12 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1829
1836
|
items.push({
|
|
1830
1837
|
id: baseId,
|
|
1831
1838
|
url: videoUrl,
|
|
1832
|
-
filename: `${username}_${date
|
|
1839
|
+
filename: `${username}_${date ? `${date}_` : ""}${baseId}`,
|
|
1833
1840
|
extension: "mp4",
|
|
1834
1841
|
type: "video",
|
|
1842
|
+
caption,
|
|
1843
|
+
author: username,
|
|
1844
|
+
platform: "instagram",
|
|
1835
1845
|
date,
|
|
1836
1846
|
width: video?.width ?? node.dimensions?.width,
|
|
1837
1847
|
height: video?.height ?? node.dimensions?.height,
|
|
@@ -1844,9 +1854,12 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1844
1854
|
items.push({
|
|
1845
1855
|
id: baseId,
|
|
1846
1856
|
url: photoUrl,
|
|
1847
|
-
filename: `${username}_${date
|
|
1857
|
+
filename: `${username}_${date ? `${date}_` : ""}${baseId}`,
|
|
1848
1858
|
extension: "jpg",
|
|
1849
1859
|
type: "photo",
|
|
1860
|
+
caption,
|
|
1861
|
+
author: username,
|
|
1862
|
+
platform: "instagram",
|
|
1850
1863
|
date,
|
|
1851
1864
|
width: candidate?.width ?? node.dimensions?.width,
|
|
1852
1865
|
height: candidate?.height ?? node.dimensions?.height
|
|
@@ -1860,9 +1873,12 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1860
1873
|
items.push({
|
|
1861
1874
|
id: baseId,
|
|
1862
1875
|
url: photoUrl,
|
|
1863
|
-
filename: `${username}_${date
|
|
1876
|
+
filename: `${username}_${date ? `${date}_` : ""}${baseId}`,
|
|
1864
1877
|
extension: "jpg",
|
|
1865
1878
|
type: "photo",
|
|
1879
|
+
caption,
|
|
1880
|
+
author: username,
|
|
1881
|
+
platform: "instagram",
|
|
1866
1882
|
date,
|
|
1867
1883
|
width: candidate?.width ?? node.dimensions?.width,
|
|
1868
1884
|
height: candidate?.height ?? node.dimensions?.height
|
|
@@ -1874,6 +1890,7 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1874
1890
|
function parseReelItemToMediaItem(item, username) {
|
|
1875
1891
|
const id = item.id || item.pk || String(Date.now());
|
|
1876
1892
|
const date = item.taken_at || item.created_at;
|
|
1893
|
+
const caption = item.caption?.text || (typeof item.caption === "string" ? item.caption : void 0);
|
|
1877
1894
|
const isVideo = item.media_type === 2 || Boolean(item.video_versions && item.video_versions.length > 0);
|
|
1878
1895
|
if (isVideo) {
|
|
1879
1896
|
const video = item.video_versions?.[0];
|
|
@@ -1882,9 +1899,12 @@ function parseReelItemToMediaItem(item, username) {
|
|
|
1882
1899
|
return {
|
|
1883
1900
|
id,
|
|
1884
1901
|
url: videoUrl,
|
|
1885
|
-
filename: `${username}_${date
|
|
1902
|
+
filename: `${username}_${date ? `${date}_` : ""}${id}`,
|
|
1886
1903
|
extension: "mp4",
|
|
1887
1904
|
type: "video",
|
|
1905
|
+
caption,
|
|
1906
|
+
author: username,
|
|
1907
|
+
platform: "instagram",
|
|
1888
1908
|
date,
|
|
1889
1909
|
width: video?.width ?? item.original_width,
|
|
1890
1910
|
height: video?.height ?? item.original_height,
|
|
@@ -1897,9 +1917,12 @@ function parseReelItemToMediaItem(item, username) {
|
|
|
1897
1917
|
return {
|
|
1898
1918
|
id,
|
|
1899
1919
|
url: photoUrl,
|
|
1900
|
-
filename: `${username}_${date
|
|
1920
|
+
filename: `${username}_${date ? `${date}_` : ""}${id}`,
|
|
1901
1921
|
extension: "jpg",
|
|
1902
1922
|
type: "photo",
|
|
1923
|
+
caption,
|
|
1924
|
+
author: username,
|
|
1925
|
+
platform: "instagram",
|
|
1903
1926
|
date,
|
|
1904
1927
|
width: candidate?.width ?? item.original_width,
|
|
1905
1928
|
height: candidate?.height ?? item.original_height
|
|
@@ -1961,14 +1984,17 @@ var InstagramExtractor = class {
|
|
|
1961
1984
|
"Referer": "https://www.instagram.com/"
|
|
1962
1985
|
}
|
|
1963
1986
|
});
|
|
1964
|
-
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
1965
|
-
const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
|
|
1966
|
-
throw new Error(`Instagram GraphQL query error: ${errMsg}`);
|
|
1967
|
-
}
|
|
1968
1987
|
const edges = res?.data?.highlights?.edges;
|
|
1969
1988
|
if (Array.isArray(edges) && edges.length > 0) {
|
|
1989
|
+
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
1990
|
+
logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery partial field errors", res.errors);
|
|
1991
|
+
}
|
|
1970
1992
|
return edges.map((e) => e.node);
|
|
1971
1993
|
}
|
|
1994
|
+
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
1995
|
+
const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
|
|
1996
|
+
throw new Error(`Instagram GraphQL query error: ${errMsg}`);
|
|
1997
|
+
}
|
|
1972
1998
|
} catch (err) {
|
|
1973
1999
|
logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery", err);
|
|
1974
2000
|
}
|
|
@@ -2121,12 +2147,11 @@ var InstagramExtractor = class {
|
|
|
2121
2147
|
signal: options.signal,
|
|
2122
2148
|
headers
|
|
2123
2149
|
});
|
|
2124
|
-
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
2125
|
-
const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
|
|
2126
|
-
throw new Error(`Instagram GraphQL query error: ${errMsg}`);
|
|
2127
|
-
}
|
|
2128
2150
|
const connection = res?.data?.xdt_api__v1__feed__user_timeline_graphql_connection || res?.data?.user?.edge_owner_to_timeline_media;
|
|
2129
|
-
if (connection?.edges && Array.isArray(connection.edges)) {
|
|
2151
|
+
if (connection?.edges && Array.isArray(connection.edges) && connection.edges.length > 0) {
|
|
2152
|
+
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
2153
|
+
logExtractorDebug("PolarisProfilePostsTabContentQuery partial field errors", res.errors);
|
|
2154
|
+
}
|
|
2130
2155
|
let resolvedUserId;
|
|
2131
2156
|
for (const edge of connection.edges) {
|
|
2132
2157
|
const node = edge?.node;
|
|
@@ -2150,21 +2175,35 @@ var InstagramExtractor = class {
|
|
|
2150
2175
|
userId: resolvedUserId
|
|
2151
2176
|
};
|
|
2152
2177
|
}
|
|
2178
|
+
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
2179
|
+
const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
|
|
2180
|
+
throw new Error(`Instagram GraphQL query error: ${errMsg}`);
|
|
2181
|
+
}
|
|
2153
2182
|
return { edges: [], hasNextPage: false };
|
|
2154
2183
|
}
|
|
2155
2184
|
async fetchNextPostsPage(userId, username, cursor, options, tokens) {
|
|
2185
|
+
try {
|
|
2186
|
+
const page = await this.fetchPostsTabGraphQL(username, cursor, tokens || {}, options);
|
|
2187
|
+
if (page.edges && page.edges.length > 0) {
|
|
2188
|
+
return page;
|
|
2189
|
+
}
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
logExtractorDebug("PolarisProfilePostsTabContentQuery_connection", err);
|
|
2192
|
+
}
|
|
2156
2193
|
if (userId) {
|
|
2157
2194
|
const variables = JSON.stringify({ id: userId, first: 12, after: cursor });
|
|
2158
2195
|
const url = `https://www.instagram.com/graphql/query/?query_hash=${IG_TIMELINE_QUERY_HASH}&variables=${encodeURIComponent(variables)}`;
|
|
2159
2196
|
try {
|
|
2197
|
+
const headers = {
|
|
2198
|
+
"X-IG-App-ID": IG_APP_ID,
|
|
2199
|
+
"Referer": `https://www.instagram.com/${username}/`
|
|
2200
|
+
};
|
|
2201
|
+
if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
|
|
2160
2202
|
const res = await this.httpClient.fetchJson(url, {
|
|
2161
2203
|
cookiePath: options.cookiePath,
|
|
2162
2204
|
cookieDomain: "instagram.com",
|
|
2163
2205
|
signal: options.signal,
|
|
2164
|
-
headers
|
|
2165
|
-
"X-IG-App-ID": IG_APP_ID,
|
|
2166
|
-
"Referer": `https://www.instagram.com/${username}/`
|
|
2167
|
-
}
|
|
2206
|
+
headers
|
|
2168
2207
|
});
|
|
2169
2208
|
const data = res?.data?.user?.edge_owner_to_timeline_media;
|
|
2170
2209
|
if (data?.edges && data.edges.length > 0) {
|
|
@@ -2178,25 +2217,19 @@ var InstagramExtractor = class {
|
|
|
2178
2217
|
logExtractorDebug("edge_owner_to_timeline_media", err);
|
|
2179
2218
|
}
|
|
2180
2219
|
}
|
|
2181
|
-
try {
|
|
2182
|
-
const page = await this.fetchPostsTabGraphQL(username, cursor, tokens || {}, options);
|
|
2183
|
-
if (page.edges && page.edges.length > 0) {
|
|
2184
|
-
return page;
|
|
2185
|
-
}
|
|
2186
|
-
} catch (err) {
|
|
2187
|
-
logExtractorDebug("PolarisProfilePostsTabContentQuery_connection", err);
|
|
2188
|
-
}
|
|
2189
2220
|
if (userId) {
|
|
2190
2221
|
try {
|
|
2191
2222
|
const restUrl = `https://www.instagram.com/api/v1/feed/user/${userId}/?count=12&max_id=${encodeURIComponent(cursor)}`;
|
|
2223
|
+
const headers = {
|
|
2224
|
+
"X-IG-App-ID": IG_APP_ID,
|
|
2225
|
+
"Referer": `https://www.instagram.com/${username}/`
|
|
2226
|
+
};
|
|
2227
|
+
if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
|
|
2192
2228
|
const res = await this.httpClient.fetchJson(restUrl, {
|
|
2193
2229
|
cookiePath: options.cookiePath,
|
|
2194
2230
|
cookieDomain: "instagram.com",
|
|
2195
2231
|
signal: options.signal,
|
|
2196
|
-
headers
|
|
2197
|
-
"X-IG-App-ID": IG_APP_ID,
|
|
2198
|
-
"Referer": `https://www.instagram.com/${username}/`
|
|
2199
|
-
}
|
|
2232
|
+
headers
|
|
2200
2233
|
});
|
|
2201
2234
|
if (Array.isArray(res?.items) && res.items.length > 0) {
|
|
2202
2235
|
const edges = res.items.map((item) => ({ node: item }));
|
|
@@ -2242,10 +2275,6 @@ var InstagramExtractor = class {
|
|
|
2242
2275
|
"Referer": "https://www.instagram.com/"
|
|
2243
2276
|
}
|
|
2244
2277
|
});
|
|
2245
|
-
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
2246
|
-
const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
|
|
2247
|
-
throw new Error(`Instagram GraphQL query error: ${errMsg}`);
|
|
2248
|
-
}
|
|
2249
2278
|
const reelsData = res?.data?.fetch__XDTUserDict;
|
|
2250
2279
|
let connection = reelsData?.reels;
|
|
2251
2280
|
if (!connection && reelsData) {
|
|
@@ -2256,13 +2285,20 @@ var InstagramExtractor = class {
|
|
|
2256
2285
|
}
|
|
2257
2286
|
}
|
|
2258
2287
|
}
|
|
2259
|
-
if (connection?.edges && Array.isArray(connection.edges)) {
|
|
2288
|
+
if (connection?.edges && Array.isArray(connection.edges) && connection.edges.length > 0) {
|
|
2289
|
+
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
2290
|
+
logExtractorDebug("PolarisProfileReelsTabContentQuery partial field errors", res.errors);
|
|
2291
|
+
}
|
|
2260
2292
|
return {
|
|
2261
2293
|
edges: connection.edges,
|
|
2262
2294
|
hasNextPage: Boolean(connection.page_info?.has_next_page),
|
|
2263
2295
|
nextCursor: connection.page_info?.end_cursor
|
|
2264
2296
|
};
|
|
2265
2297
|
}
|
|
2298
|
+
if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
|
|
2299
|
+
const errMsg = res.errors.map((e) => e.message || JSON.stringify(e)).join("; ");
|
|
2300
|
+
throw new Error(`Instagram GraphQL query error: ${errMsg}`);
|
|
2301
|
+
}
|
|
2266
2302
|
} catch (err) {
|
|
2267
2303
|
logExtractorDebug("PolarisProfileReelsTabContentQuery_connection", err);
|
|
2268
2304
|
}
|
|
@@ -2283,8 +2319,10 @@ var InstagramExtractor = class {
|
|
|
2283
2319
|
logExtractorDebug("fetchUserProfile", err);
|
|
2284
2320
|
}
|
|
2285
2321
|
let webTokens = {};
|
|
2286
|
-
|
|
2322
|
+
try {
|
|
2287
2323
|
webTokens = await this.fetchWebTokens(username, options);
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
logExtractorDebug("fetchWebTokens", err);
|
|
2288
2326
|
}
|
|
2289
2327
|
let userId = user?.id || webTokens.userId;
|
|
2290
2328
|
if (!userId && (subDir === "Stories" || subDir === "Highlights")) {
|
|
@@ -2344,7 +2382,19 @@ var InstagramExtractor = class {
|
|
|
2344
2382
|
let currentEdges = user?.edge_owner_to_timeline_media?.edges ?? [];
|
|
2345
2383
|
let hasNextPage = Boolean(user?.edge_owner_to_timeline_media?.page_info?.has_next_page);
|
|
2346
2384
|
let endCursor = user?.edge_owner_to_timeline_media?.page_info?.end_cursor;
|
|
2347
|
-
if (
|
|
2385
|
+
if (options.initialCursor) {
|
|
2386
|
+
endCursor = options.initialCursor;
|
|
2387
|
+
hasNextPage = true;
|
|
2388
|
+
currentEdges = [];
|
|
2389
|
+
try {
|
|
2390
|
+
const resumedPage = await this.fetchNextPostsPage(userId || "", username, endCursor, options, webTokens);
|
|
2391
|
+
currentEdges = resumedPage.edges;
|
|
2392
|
+
hasNextPage = resumedPage.hasNextPage;
|
|
2393
|
+
endCursor = resumedPage.nextCursor;
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
logExtractorDebug("fetchNextPostsPage resume", err);
|
|
2396
|
+
}
|
|
2397
|
+
} else if (!user) {
|
|
2348
2398
|
try {
|
|
2349
2399
|
const firstPage = await this.fetchPostsTabGraphQL(username, void 0, webTokens, options);
|
|
2350
2400
|
currentEdges = firstPage.edges;
|
|
@@ -2358,11 +2408,17 @@ var InstagramExtractor = class {
|
|
|
2358
2408
|
}
|
|
2359
2409
|
}
|
|
2360
2410
|
while (true) {
|
|
2411
|
+
let reachedSinceLimit = false;
|
|
2361
2412
|
for (const edge of currentEdges) {
|
|
2362
2413
|
const items = parseIgNodeToMediaItems(edge.node, username);
|
|
2363
2414
|
for (const item of items) {
|
|
2364
2415
|
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
2365
2416
|
if (subDir === "Videos" && item.type !== "video") continue;
|
|
2417
|
+
if (options.until !== void 0 && item.date && item.date > options.until) continue;
|
|
2418
|
+
if (options.since !== void 0 && item.date && item.date < options.since) {
|
|
2419
|
+
reachedSinceLimit = true;
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2366
2422
|
if (emittedItemIds.has(item.id)) continue;
|
|
2367
2423
|
emittedItemIds.add(item.id);
|
|
2368
2424
|
found++;
|
|
@@ -2372,8 +2428,11 @@ var InstagramExtractor = class {
|
|
|
2372
2428
|
message: `Found item: ${item.filename}`
|
|
2373
2429
|
});
|
|
2374
2430
|
yield item;
|
|
2431
|
+
if (options.limit !== void 0 && found >= options.limit) return;
|
|
2375
2432
|
}
|
|
2433
|
+
if (reachedSinceLimit) break;
|
|
2376
2434
|
}
|
|
2435
|
+
if (reachedSinceLimit) break;
|
|
2377
2436
|
if (!hasNextPage || !endCursor || options.signal?.aborted) {
|
|
2378
2437
|
break;
|
|
2379
2438
|
}
|
|
@@ -2383,6 +2442,9 @@ var InstagramExtractor = class {
|
|
|
2383
2442
|
currentEdges = nextPage.edges;
|
|
2384
2443
|
hasNextPage = nextPage.hasNextPage;
|
|
2385
2444
|
endCursor = nextPage.nextCursor;
|
|
2445
|
+
if (endCursor) {
|
|
2446
|
+
options.onCheckpoint?.("timeline", endCursor);
|
|
2447
|
+
}
|
|
2386
2448
|
if (currentEdges.length === 0) {
|
|
2387
2449
|
break;
|
|
2388
2450
|
}
|
|
@@ -2390,7 +2452,7 @@ var InstagramExtractor = class {
|
|
|
2390
2452
|
break;
|
|
2391
2453
|
}
|
|
2392
2454
|
}
|
|
2393
|
-
if (subDir === "Videos" && userId) {
|
|
2455
|
+
if ((subDir === "Videos" || subDir === "All") && userId) {
|
|
2394
2456
|
const videoEdges = user?.edge_felix_video_timeline?.edges ?? [];
|
|
2395
2457
|
for (const edge of videoEdges) {
|
|
2396
2458
|
const items = parseIgNodeToMediaItems(edge.node, username);
|
|
@@ -2507,10 +2569,11 @@ function parseTweetMediaEntities(tweet, username) {
|
|
|
2507
2569
|
const entities = legacy.extended_entities?.media || legacy.entities?.media || [];
|
|
2508
2570
|
const tweetId = tweetResult.rest_id || legacy.id_str || String(Date.now());
|
|
2509
2571
|
const rawDate = legacy.created_at ? new Date(legacy.created_at).getTime() / 1e3 : void 0;
|
|
2572
|
+
const caption = legacy.full_text || legacy.text;
|
|
2510
2573
|
for (let idx = 0; idx < entities.length; idx++) {
|
|
2511
2574
|
const media = entities[idx];
|
|
2512
2575
|
const subIndex = idx + 1;
|
|
2513
|
-
const baseFilename = `${username}_${tweetId}_${subIndex}`;
|
|
2576
|
+
const baseFilename = `${username}_${rawDate ? `${rawDate}_` : ""}${tweetId}_${subIndex}`;
|
|
2514
2577
|
if (media.type === "video" || media.type === "animated_gif") {
|
|
2515
2578
|
const variants = (media.video_info?.variants || []).filter((v) => v.content_type === "video/mp4").sort((a, b) => (b.bitrate ?? 0) - (a.bitrate ?? 0));
|
|
2516
2579
|
const bestVariant = variants[0];
|
|
@@ -2521,8 +2584,16 @@ function parseTweetMediaEntities(tweet, username) {
|
|
|
2521
2584
|
filename: baseFilename,
|
|
2522
2585
|
extension: "mp4",
|
|
2523
2586
|
type: "video",
|
|
2587
|
+
caption,
|
|
2588
|
+
author: username,
|
|
2589
|
+
platform: "x",
|
|
2524
2590
|
date: rawDate,
|
|
2525
|
-
thumbnailUrl: media.media_url_https
|
|
2591
|
+
thumbnailUrl: media.media_url_https,
|
|
2592
|
+
...entities.length > 1 ? {
|
|
2593
|
+
carouselId: tweetId,
|
|
2594
|
+
carouselIndex: subIndex,
|
|
2595
|
+
carouselTotal: entities.length
|
|
2596
|
+
} : {}
|
|
2526
2597
|
});
|
|
2527
2598
|
}
|
|
2528
2599
|
} else {
|
|
@@ -2538,9 +2609,17 @@ function parseTweetMediaEntities(tweet, username) {
|
|
|
2538
2609
|
filename: baseFilename,
|
|
2539
2610
|
extension: ext,
|
|
2540
2611
|
type: "photo",
|
|
2612
|
+
caption,
|
|
2613
|
+
author: username,
|
|
2614
|
+
platform: "x",
|
|
2541
2615
|
date: rawDate,
|
|
2542
2616
|
width: media.original_info?.width,
|
|
2543
|
-
height: media.original_info?.height
|
|
2617
|
+
height: media.original_info?.height,
|
|
2618
|
+
...entities.length > 1 ? {
|
|
2619
|
+
carouselId: tweetId,
|
|
2620
|
+
carouselIndex: subIndex,
|
|
2621
|
+
carouselTotal: entities.length
|
|
2622
|
+
} : {}
|
|
2544
2623
|
});
|
|
2545
2624
|
}
|
|
2546
2625
|
}
|
|
@@ -2668,7 +2747,7 @@ var XExtractor = class {
|
|
|
2668
2747
|
});
|
|
2669
2748
|
const userId = await this.fetchUserRestId(username, options);
|
|
2670
2749
|
const endpoint = isLikes ? "https://x.com/i/api/graphql/TGEKkJG_meudeaFcqaxM-Q/Likes" : "https://x.com/i/api/graphql/jCRhbOzdgOHp6u9H4g2tEg/UserMedia";
|
|
2671
|
-
let cursor;
|
|
2750
|
+
let cursor = options.initialCursor;
|
|
2672
2751
|
let hasMore = true;
|
|
2673
2752
|
let found = 0;
|
|
2674
2753
|
while (hasMore) {
|
|
@@ -2715,10 +2794,16 @@ var XExtractor = class {
|
|
|
2715
2794
|
if (shouldSkipTweetReply(tweetContent, userId, isLikes ? "likes" : "timeline")) {
|
|
2716
2795
|
continue;
|
|
2717
2796
|
}
|
|
2797
|
+
let reachedSinceLimit = false;
|
|
2718
2798
|
const mediaItems = parseTweetMediaEntities(tweetContent, username);
|
|
2719
2799
|
for (const item of mediaItems) {
|
|
2720
2800
|
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
2721
2801
|
if (subDir === "Videos" && item.type !== "video") continue;
|
|
2802
|
+
if (options.until !== void 0 && item.date && item.date > options.until) continue;
|
|
2803
|
+
if (options.since !== void 0 && item.date && item.date < options.since) {
|
|
2804
|
+
reachedSinceLimit = true;
|
|
2805
|
+
break;
|
|
2806
|
+
}
|
|
2722
2807
|
found++;
|
|
2723
2808
|
pageItemCount++;
|
|
2724
2809
|
options.onProgress?.({
|
|
@@ -2727,14 +2812,21 @@ var XExtractor = class {
|
|
|
2727
2812
|
message: `Found item: ${item.filename}`
|
|
2728
2813
|
});
|
|
2729
2814
|
yield item;
|
|
2815
|
+
if (options.limit !== void 0 && found >= options.limit) return;
|
|
2816
|
+
}
|
|
2817
|
+
if (reachedSinceLimit) {
|
|
2818
|
+
hasMore = false;
|
|
2819
|
+
break;
|
|
2730
2820
|
}
|
|
2731
2821
|
}
|
|
2732
2822
|
}
|
|
2733
2823
|
}
|
|
2734
2824
|
if (!newCursor || newCursor === cursor || pageItemCount === 0) {
|
|
2735
2825
|
hasMore = false;
|
|
2826
|
+
options.onCheckpoint?.("timeline", "");
|
|
2736
2827
|
} else {
|
|
2737
2828
|
cursor = newCursor;
|
|
2829
|
+
options.onCheckpoint?.("timeline", cursor);
|
|
2738
2830
|
await sleep(1500, 2500, options.signal);
|
|
2739
2831
|
}
|
|
2740
2832
|
}
|
|
@@ -2837,11 +2929,16 @@ function parseTikTokItem(item, username) {
|
|
|
2837
2929
|
items.push({
|
|
2838
2930
|
id: `${id}_${idx + 1}`,
|
|
2839
2931
|
url: imgUrl,
|
|
2840
|
-
filename: `${username}_${id}_${idx + 1}`,
|
|
2932
|
+
filename: `${username}_${date ? `${date}_` : ""}${id}_${idx + 1}`,
|
|
2841
2933
|
extension: "jpg",
|
|
2842
2934
|
type: "photo",
|
|
2843
2935
|
caption,
|
|
2844
|
-
date
|
|
2936
|
+
date,
|
|
2937
|
+
author: username,
|
|
2938
|
+
platform: "tiktok",
|
|
2939
|
+
carouselId: id,
|
|
2940
|
+
carouselIndex: idx + 1,
|
|
2941
|
+
carouselTotal: imageList.length
|
|
2845
2942
|
});
|
|
2846
2943
|
}
|
|
2847
2944
|
});
|
|
@@ -2853,14 +2950,16 @@ function parseTikTokItem(item, username) {
|
|
|
2853
2950
|
items.push({
|
|
2854
2951
|
id,
|
|
2855
2952
|
url: videoUrl,
|
|
2856
|
-
filename: `${username}_${id}`,
|
|
2953
|
+
filename: `${username}_${date ? `${date}_` : ""}${id}`,
|
|
2857
2954
|
extension: "mp4",
|
|
2858
2955
|
type: "video",
|
|
2859
2956
|
caption,
|
|
2860
2957
|
date,
|
|
2861
2958
|
width: video?.width,
|
|
2862
2959
|
height: video?.height,
|
|
2863
|
-
thumbnailUrl: video?.cover || video?.originCover
|
|
2960
|
+
thumbnailUrl: video?.cover || video?.originCover,
|
|
2961
|
+
author: username,
|
|
2962
|
+
platform: "tiktok"
|
|
2864
2963
|
});
|
|
2865
2964
|
}
|
|
2866
2965
|
return items;
|
|
@@ -2912,7 +3011,7 @@ var TikTokExtractor = class {
|
|
|
2912
3011
|
if (!secUid) {
|
|
2913
3012
|
return;
|
|
2914
3013
|
}
|
|
2915
|
-
let cursor = "0";
|
|
3014
|
+
let cursor = options.initialCursor || "0";
|
|
2916
3015
|
let hasMore = true;
|
|
2917
3016
|
const visitedCursors = /* @__PURE__ */ new Set();
|
|
2918
3017
|
while (hasMore) {
|
|
@@ -2947,11 +3046,17 @@ var TikTokExtractor = class {
|
|
|
2947
3046
|
}
|
|
2948
3047
|
const items = res?.itemList || [];
|
|
2949
3048
|
if (items.length === 0) break;
|
|
3049
|
+
let reachedSinceLimit = false;
|
|
2950
3050
|
for (const rawItem of items) {
|
|
2951
3051
|
const mediaList = parseTikTokItem(rawItem, cleanUsername);
|
|
2952
3052
|
for (const item of mediaList) {
|
|
2953
3053
|
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
2954
3054
|
if (subDir === "Videos" && item.type !== "video") continue;
|
|
3055
|
+
if (options.until !== void 0 && item.date && item.date > options.until) continue;
|
|
3056
|
+
if (options.since !== void 0 && item.date && item.date < options.since) {
|
|
3057
|
+
reachedSinceLimit = true;
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
2955
3060
|
found++;
|
|
2956
3061
|
options.onProgress?.({
|
|
2957
3062
|
stage: subDir,
|
|
@@ -2959,10 +3064,18 @@ var TikTokExtractor = class {
|
|
|
2959
3064
|
message: `Found item: ${item.filename}`
|
|
2960
3065
|
});
|
|
2961
3066
|
yield item;
|
|
3067
|
+
if (options.limit !== void 0 && found >= options.limit) return;
|
|
2962
3068
|
}
|
|
3069
|
+
if (reachedSinceLimit) break;
|
|
2963
3070
|
}
|
|
3071
|
+
if (reachedSinceLimit) break;
|
|
2964
3072
|
hasMore = Boolean(res?.hasMore);
|
|
2965
3073
|
cursor = String(res?.cursor ?? "0");
|
|
3074
|
+
if (hasMore && cursor && cursor !== "0") {
|
|
3075
|
+
options.onCheckpoint?.("timeline", cursor);
|
|
3076
|
+
} else {
|
|
3077
|
+
options.onCheckpoint?.("timeline", "");
|
|
3078
|
+
}
|
|
2966
3079
|
await sleep(1e3, 2e3, options.signal);
|
|
2967
3080
|
}
|
|
2968
3081
|
}
|
|
@@ -3142,119 +3255,122 @@ var FacebookExtractor = class {
|
|
|
3142
3255
|
found: 0,
|
|
3143
3256
|
message: `Fetching Facebook profile @${username}...`
|
|
3144
3257
|
});
|
|
3145
|
-
const
|
|
3146
|
-
const
|
|
3147
|
-
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
signal: options.signal,
|
|
3156
|
-
retries: 0,
|
|
3157
|
-
timeoutMs: 4e3
|
|
3158
|
-
});
|
|
3159
|
-
} catch {
|
|
3160
|
-
html = "";
|
|
3161
|
-
}
|
|
3162
|
-
let initialItems = extractFacebookMediaFromHtml(html, username);
|
|
3163
|
-
if (initialItems.length === 0) {
|
|
3164
|
-
options.onProgress?.({
|
|
3165
|
-
stage: "Profile",
|
|
3166
|
-
found: 0,
|
|
3167
|
-
message: `Desktop yielded 0 items. Falling back to mbasic.facebook.com for @${username}...`
|
|
3168
|
-
});
|
|
3258
|
+
const pathsToScrape = subDir === "All" ? ["photos", "videos"] : [subDir === "Photos" ? "photos" : "videos"];
|
|
3259
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
3260
|
+
let found = 0;
|
|
3261
|
+
for (const targetPath of pathsToScrape) {
|
|
3262
|
+
if (options.signal?.aborted) break;
|
|
3263
|
+
const desktopUrl = `https://www.facebook.com/${encodeURIComponent(username)}/${targetPath}`;
|
|
3264
|
+
const mbasicUrl = `https://mbasic.facebook.com/${encodeURIComponent(username)}/${targetPath}`;
|
|
3265
|
+
const mobileUrl = `https://m.facebook.com/${encodeURIComponent(username)}/${targetPath}`;
|
|
3266
|
+
let currentUrl = desktopUrl;
|
|
3267
|
+
let html = "";
|
|
3169
3268
|
try {
|
|
3170
|
-
|
|
3269
|
+
html = await this.httpClient.fetchText(desktopUrl, {
|
|
3171
3270
|
cookiePath: options.cookiePath,
|
|
3172
3271
|
cookieDomain: "facebook.com",
|
|
3173
3272
|
signal: options.signal,
|
|
3174
|
-
retries:
|
|
3175
|
-
timeoutMs:
|
|
3176
|
-
headers: {
|
|
3177
|
-
"Referer": "https://mbasic.facebook.com/"
|
|
3178
|
-
}
|
|
3273
|
+
retries: 0,
|
|
3274
|
+
timeoutMs: 4e3
|
|
3179
3275
|
});
|
|
3180
|
-
const mbasicItems = extractFacebookMediaFromHtml(mbasicHtml, username);
|
|
3181
|
-
if (mbasicItems.length > 0 || mbasicHtml.length > 200) {
|
|
3182
|
-
html = mbasicHtml;
|
|
3183
|
-
currentUrl = mbasicUrl;
|
|
3184
|
-
initialItems = mbasicItems;
|
|
3185
|
-
}
|
|
3186
3276
|
} catch {
|
|
3277
|
+
html = "";
|
|
3187
3278
|
}
|
|
3279
|
+
let initialItems = extractFacebookMediaFromHtml(html, username);
|
|
3188
3280
|
if (initialItems.length === 0) {
|
|
3281
|
+
options.onProgress?.({
|
|
3282
|
+
stage: "Profile",
|
|
3283
|
+
found,
|
|
3284
|
+
message: `Desktop yielded 0 items. Falling back to mbasic.facebook.com for @${username}/${targetPath}...`
|
|
3285
|
+
});
|
|
3189
3286
|
try {
|
|
3190
|
-
const
|
|
3287
|
+
const mbasicHtml = await this.httpClient.fetchText(mbasicUrl, {
|
|
3191
3288
|
cookiePath: options.cookiePath,
|
|
3192
3289
|
cookieDomain: "facebook.com",
|
|
3193
3290
|
signal: options.signal,
|
|
3194
|
-
retries:
|
|
3291
|
+
retries: 1,
|
|
3195
3292
|
timeoutMs: 5e3,
|
|
3196
3293
|
headers: {
|
|
3197
|
-
"
|
|
3198
|
-
"Referer": "https://m.facebook.com/"
|
|
3294
|
+
"Referer": "https://mbasic.facebook.com/"
|
|
3199
3295
|
}
|
|
3200
3296
|
});
|
|
3201
|
-
const
|
|
3202
|
-
if (
|
|
3203
|
-
html =
|
|
3204
|
-
currentUrl =
|
|
3205
|
-
initialItems =
|
|
3297
|
+
const mbasicItems = extractFacebookMediaFromHtml(mbasicHtml, username);
|
|
3298
|
+
if (mbasicItems.length > 0 || mbasicHtml.length > 200) {
|
|
3299
|
+
html = mbasicHtml;
|
|
3300
|
+
currentUrl = mbasicUrl;
|
|
3301
|
+
initialItems = mbasicItems;
|
|
3206
3302
|
}
|
|
3207
3303
|
} catch {
|
|
3208
3304
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
});
|
|
3230
|
-
yield item;
|
|
3231
|
-
}
|
|
3232
|
-
if (page >= maxPages || options.signal?.aborted) {
|
|
3233
|
-
break;
|
|
3234
|
-
}
|
|
3235
|
-
const nextUrl = extractFacebookNextPageUrl(html, currentUrl);
|
|
3236
|
-
if (!nextUrl || nextUrl === currentUrl) {
|
|
3237
|
-
break;
|
|
3238
|
-
}
|
|
3239
|
-
if (pageNewCount === 0 && page > 1) {
|
|
3240
|
-
break;
|
|
3241
|
-
}
|
|
3242
|
-
await sleep(1e3, 2e3, options.signal);
|
|
3243
|
-
try {
|
|
3244
|
-
currentUrl = nextUrl;
|
|
3245
|
-
html = await this.httpClient.fetchText(currentUrl, {
|
|
3246
|
-
cookiePath: options.cookiePath,
|
|
3247
|
-
cookieDomain: "facebook.com",
|
|
3248
|
-
signal: options.signal,
|
|
3249
|
-
retries: 1,
|
|
3250
|
-
timeoutMs: 5e3,
|
|
3251
|
-
headers: {
|
|
3252
|
-
"Referer": currentUrl.includes("mbasic") ? "https://mbasic.facebook.com/" : "https://www.facebook.com/"
|
|
3305
|
+
if (initialItems.length === 0) {
|
|
3306
|
+
try {
|
|
3307
|
+
const mobileHtml = await this.httpClient.fetchText(mobileUrl, {
|
|
3308
|
+
cookiePath: options.cookiePath,
|
|
3309
|
+
cookieDomain: "facebook.com",
|
|
3310
|
+
signal: options.signal,
|
|
3311
|
+
retries: 0,
|
|
3312
|
+
timeoutMs: 5e3,
|
|
3313
|
+
headers: {
|
|
3314
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
|
3315
|
+
"Referer": "https://m.facebook.com/"
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
const mobileItems = extractFacebookMediaFromHtml(mobileHtml, username);
|
|
3319
|
+
if (mobileItems.length > 0) {
|
|
3320
|
+
html = mobileHtml;
|
|
3321
|
+
currentUrl = mobileUrl;
|
|
3322
|
+
initialItems = mobileItems;
|
|
3323
|
+
}
|
|
3324
|
+
} catch {
|
|
3253
3325
|
}
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
let page = 1;
|
|
3329
|
+
const maxPages = 5;
|
|
3330
|
+
while (true) {
|
|
3331
|
+
const items = extractFacebookMediaFromHtml(html, username);
|
|
3332
|
+
let pageNewCount = 0;
|
|
3333
|
+
for (const item of items) {
|
|
3334
|
+
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
3335
|
+
if (subDir === "Videos" && item.type !== "video") continue;
|
|
3336
|
+
if (seenUrls.has(item.url)) continue;
|
|
3337
|
+
seenUrls.add(item.url);
|
|
3338
|
+
found++;
|
|
3339
|
+
pageNewCount++;
|
|
3340
|
+
options.onProgress?.({
|
|
3341
|
+
stage: subDir === "All" ? targetPath === "photos" ? "Photos" : "Videos" : subDir,
|
|
3342
|
+
found,
|
|
3343
|
+
message: `Found item: ${item.filename}`
|
|
3344
|
+
});
|
|
3345
|
+
yield item;
|
|
3346
|
+
}
|
|
3347
|
+
if (page >= maxPages || options.signal?.aborted) {
|
|
3348
|
+
break;
|
|
3349
|
+
}
|
|
3350
|
+
const nextUrl = extractFacebookNextPageUrl(html, currentUrl);
|
|
3351
|
+
if (!nextUrl || nextUrl === currentUrl) {
|
|
3352
|
+
break;
|
|
3353
|
+
}
|
|
3354
|
+
if (pageNewCount === 0 && page > 1) {
|
|
3355
|
+
break;
|
|
3356
|
+
}
|
|
3357
|
+
await sleep(1e3, 2e3, options.signal);
|
|
3358
|
+
try {
|
|
3359
|
+
currentUrl = nextUrl;
|
|
3360
|
+
html = await this.httpClient.fetchText(currentUrl, {
|
|
3361
|
+
cookiePath: options.cookiePath,
|
|
3362
|
+
cookieDomain: "facebook.com",
|
|
3363
|
+
signal: options.signal,
|
|
3364
|
+
retries: 1,
|
|
3365
|
+
timeoutMs: 5e3,
|
|
3366
|
+
headers: {
|
|
3367
|
+
"Referer": currentUrl.includes("mbasic") ? "https://mbasic.facebook.com/" : "https://www.facebook.com/"
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
page++;
|
|
3371
|
+
} catch {
|
|
3372
|
+
break;
|
|
3373
|
+
}
|
|
3258
3374
|
}
|
|
3259
3375
|
}
|
|
3260
3376
|
}
|
|
@@ -3278,10 +3394,66 @@ function getExtractor(platform) {
|
|
|
3278
3394
|
|
|
3279
3395
|
// src/lib/engines/downloader.ts
|
|
3280
3396
|
import fs6 from "fs";
|
|
3281
|
-
import
|
|
3397
|
+
import fsPromises3 from "fs/promises";
|
|
3282
3398
|
import path5 from "path";
|
|
3283
3399
|
import { Readable } from "stream";
|
|
3284
3400
|
import { pipeline } from "stream/promises";
|
|
3401
|
+
|
|
3402
|
+
// src/lib/engines/metadata.ts
|
|
3403
|
+
import fsPromises2 from "fs/promises";
|
|
3404
|
+
function extractHashtags(caption) {
|
|
3405
|
+
if (!caption) return [];
|
|
3406
|
+
const matches = caption.match(/#([\p{L}\p{N}_]+)/gu);
|
|
3407
|
+
if (!matches) return [];
|
|
3408
|
+
const tags = matches.map((m) => m.replace(/^#/, ""));
|
|
3409
|
+
return Array.from(new Set(tags));
|
|
3410
|
+
}
|
|
3411
|
+
function buildMetadataSidecar(item, finalFilename, fallbackPlatform, fallbackAuthor) {
|
|
3412
|
+
const hashtags = extractHashtags(item.caption);
|
|
3413
|
+
const cleanExt = item.extension.replace(/^\./, "") || (item.type === "video" ? "mp4" : "jpg");
|
|
3414
|
+
const isoDate = item.date ? new Date(item.date * 1e3).toISOString() : void 0;
|
|
3415
|
+
let carousel;
|
|
3416
|
+
if (item.carouselId && item.carouselIndex !== void 0 && item.carouselTotal !== void 0) {
|
|
3417
|
+
carousel = {
|
|
3418
|
+
id: item.carouselId,
|
|
3419
|
+
index: item.carouselIndex,
|
|
3420
|
+
total: item.carouselTotal
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
return {
|
|
3424
|
+
id: item.id,
|
|
3425
|
+
platform: item.platform || fallbackPlatform,
|
|
3426
|
+
author: item.author || fallbackAuthor,
|
|
3427
|
+
url: item.url,
|
|
3428
|
+
filename: finalFilename,
|
|
3429
|
+
type: item.type,
|
|
3430
|
+
extension: cleanExt,
|
|
3431
|
+
...item.caption ? { caption: item.caption } : {},
|
|
3432
|
+
...item.date ? { date: item.date, isoDate } : {},
|
|
3433
|
+
...item.width ? { width: item.width } : {},
|
|
3434
|
+
...item.height ? { height: item.height } : {},
|
|
3435
|
+
...item.thumbnailUrl ? { thumbnailUrl: item.thumbnailUrl } : {},
|
|
3436
|
+
hashtags,
|
|
3437
|
+
...carousel ? { carousel } : {},
|
|
3438
|
+
...item.metadata ? { metadata: item.metadata } : {},
|
|
3439
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
async function writeMetadataSidecar(sidecarPath, sidecar, date) {
|
|
3443
|
+
const tmpPath = `${sidecarPath}.tmp`;
|
|
3444
|
+
const jsonContent = JSON.stringify(sidecar, null, 2);
|
|
3445
|
+
await fsPromises2.writeFile(tmpPath, jsonContent, "utf-8");
|
|
3446
|
+
await fsPromises2.rename(tmpPath, sidecarPath);
|
|
3447
|
+
if (date) {
|
|
3448
|
+
try {
|
|
3449
|
+
const mtime = new Date(date * 1e3);
|
|
3450
|
+
await fsPromises2.utimes(sidecarPath, mtime, mtime);
|
|
3451
|
+
} catch {
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// src/lib/engines/downloader.ts
|
|
3285
3457
|
var ARCHIVE_FILENAME = ".open-nami-archive.json";
|
|
3286
3458
|
var YTDLP_ARCHIVE_FILENAME = "archive_yt-dlp.txt";
|
|
3287
3459
|
var TEXT_ARCHIVE_FILENAME = "archive.txt";
|
|
@@ -3289,7 +3461,7 @@ async function loadArchive(destDir) {
|
|
|
3289
3461
|
const archive = /* @__PURE__ */ new Set();
|
|
3290
3462
|
const jsonPath = path5.join(destDir, ARCHIVE_FILENAME);
|
|
3291
3463
|
try {
|
|
3292
|
-
const raw = await
|
|
3464
|
+
const raw = await fsPromises3.readFile(jsonPath, "utf-8");
|
|
3293
3465
|
const parsed = JSON.parse(raw);
|
|
3294
3466
|
if (Array.isArray(parsed)) {
|
|
3295
3467
|
for (const id of parsed) {
|
|
@@ -3300,7 +3472,7 @@ async function loadArchive(destDir) {
|
|
|
3300
3472
|
}
|
|
3301
3473
|
const ytdlpPath = path5.join(destDir, YTDLP_ARCHIVE_FILENAME);
|
|
3302
3474
|
try {
|
|
3303
|
-
const raw = await
|
|
3475
|
+
const raw = await fsPromises3.readFile(ytdlpPath, "utf-8");
|
|
3304
3476
|
const lines = raw.split(/\r?\n/);
|
|
3305
3477
|
for (const rawLine of lines) {
|
|
3306
3478
|
const line = rawLine.trim();
|
|
@@ -3313,7 +3485,7 @@ async function loadArchive(destDir) {
|
|
|
3313
3485
|
}
|
|
3314
3486
|
const textPath = path5.join(destDir, TEXT_ARCHIVE_FILENAME);
|
|
3315
3487
|
try {
|
|
3316
|
-
const raw = await
|
|
3488
|
+
const raw = await fsPromises3.readFile(textPath, "utf-8");
|
|
3317
3489
|
const lines = raw.split(/\r?\n/);
|
|
3318
3490
|
for (const rawLine of lines) {
|
|
3319
3491
|
const line = rawLine.trim();
|
|
@@ -3330,8 +3502,8 @@ async function saveArchive(destDir, archive) {
|
|
|
3330
3502
|
const archivePath = path5.join(destDir, ARCHIVE_FILENAME);
|
|
3331
3503
|
const tmpPath = `${archivePath}.tmp`;
|
|
3332
3504
|
try {
|
|
3333
|
-
await
|
|
3334
|
-
await
|
|
3505
|
+
await fsPromises3.writeFile(tmpPath, JSON.stringify([...archive]), "utf-8");
|
|
3506
|
+
await fsPromises3.rename(tmpPath, archivePath);
|
|
3335
3507
|
} catch {
|
|
3336
3508
|
}
|
|
3337
3509
|
const textPaths = [
|
|
@@ -3342,7 +3514,7 @@ async function saveArchive(destDir, archive) {
|
|
|
3342
3514
|
`).join("");
|
|
3343
3515
|
for (const tPath of textPaths) {
|
|
3344
3516
|
try {
|
|
3345
|
-
await
|
|
3517
|
+
await fsPromises3.writeFile(tPath, content, "utf-8");
|
|
3346
3518
|
} catch {
|
|
3347
3519
|
}
|
|
3348
3520
|
}
|
|
@@ -3359,12 +3531,34 @@ function sanitizeFilename(rawFilename, fallbackId, ext) {
|
|
|
3359
3531
|
}
|
|
3360
3532
|
return sanitizedBase.includes(".") ? sanitizedBase : `${sanitizedBase}.${cleanExt}`;
|
|
3361
3533
|
}
|
|
3534
|
+
function isCdnUrl(rawUrl) {
|
|
3535
|
+
try {
|
|
3536
|
+
const hostname = new URL(rawUrl).hostname.toLowerCase();
|
|
3537
|
+
return hostname.endsWith(".cdninstagram.com") || hostname.endsWith(".fbcdn.net") || hostname.endsWith(".tiktokcdn.com") || hostname.endsWith(".akamaized.net") || hostname === "pbs.twimg.com" || hostname === "video.twimg.com";
|
|
3538
|
+
} catch {
|
|
3539
|
+
return false;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3362
3542
|
async function downloadMediaStream(items, options) {
|
|
3363
3543
|
const { jobName, destDir, signal, onProgress } = options;
|
|
3364
3544
|
const httpClient = options.httpClient ?? new HttpClient();
|
|
3365
3545
|
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
3366
|
-
|
|
3367
|
-
const
|
|
3546
|
+
const dirArchives = /* @__PURE__ */ new Map();
|
|
3547
|
+
const getArchiveForDir = async (dir) => {
|
|
3548
|
+
let arch = dirArchives.get(dir);
|
|
3549
|
+
if (!arch) {
|
|
3550
|
+
await fsPromises3.mkdir(dir, { recursive: true });
|
|
3551
|
+
arch = await loadArchive(dir);
|
|
3552
|
+
dirArchives.set(dir, arch);
|
|
3553
|
+
}
|
|
3554
|
+
return arch;
|
|
3555
|
+
};
|
|
3556
|
+
await getArchiveForDir(destDir);
|
|
3557
|
+
if (options.destDirMap) {
|
|
3558
|
+
for (const d of Object.values(options.destDirMap)) {
|
|
3559
|
+
if (d) await getArchiveForDir(d);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3368
3562
|
let downloaded = 0;
|
|
3369
3563
|
let skipped = 0;
|
|
3370
3564
|
const errors = [];
|
|
@@ -3385,6 +3579,10 @@ async function downloadMediaStream(items, options) {
|
|
|
3385
3579
|
}
|
|
3386
3580
|
async function downloadWorker() {
|
|
3387
3581
|
while (!isDone && !signal?.aborted) {
|
|
3582
|
+
if (options.limit !== void 0 && downloaded >= options.limit) {
|
|
3583
|
+
isDone = true;
|
|
3584
|
+
break;
|
|
3585
|
+
}
|
|
3388
3586
|
let item;
|
|
3389
3587
|
try {
|
|
3390
3588
|
const next = await getNextItem();
|
|
@@ -3412,10 +3610,20 @@ async function downloadMediaStream(items, options) {
|
|
|
3412
3610
|
}
|
|
3413
3611
|
continue;
|
|
3414
3612
|
}
|
|
3613
|
+
const targetDir = options.destDirMap?.[item.type] ?? destDir;
|
|
3614
|
+
const archive = await getArchiveForDir(targetDir);
|
|
3415
3615
|
const ext = item.extension.replace(/^\./, "") || (item.type === "video" ? "mp4" : "jpg");
|
|
3416
3616
|
const finalFilename = sanitizeFilename(item.filename, item.id, ext);
|
|
3417
|
-
const finalPath = path5.join(
|
|
3617
|
+
const finalPath = path5.join(targetDir, finalFilename);
|
|
3618
|
+
const sidecarPath = path5.join(targetDir, `${path5.parse(finalFilename).name}.json`);
|
|
3418
3619
|
if (archive.has(item.id) && fs6.existsSync(finalPath)) {
|
|
3620
|
+
if (options.writeMetadata && !fs6.existsSync(sidecarPath)) {
|
|
3621
|
+
try {
|
|
3622
|
+
const sidecar = buildMetadataSidecar(item, finalFilename, options.defaultPlatform, options.defaultAuthor);
|
|
3623
|
+
await writeMetadataSidecar(sidecarPath, sidecar, item.date);
|
|
3624
|
+
} catch {
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3419
3627
|
skipped++;
|
|
3420
3628
|
onProgress?.({
|
|
3421
3629
|
job: jobName,
|
|
@@ -3439,10 +3647,13 @@ async function downloadMediaStream(items, options) {
|
|
|
3439
3647
|
if (options.referer) {
|
|
3440
3648
|
reqHeaders["Referer"] = options.referer;
|
|
3441
3649
|
}
|
|
3650
|
+
const isCdn = isCdnUrl(item.url);
|
|
3442
3651
|
const response = await httpClient.request(item.url, {
|
|
3443
3652
|
signal,
|
|
3444
|
-
cookiePath: options.cookiePath,
|
|
3445
|
-
headers: reqHeaders
|
|
3653
|
+
cookiePath: isCdn ? void 0 : options.cookiePath,
|
|
3654
|
+
headers: reqHeaders,
|
|
3655
|
+
retries: 2,
|
|
3656
|
+
retryDelayMs: 1500
|
|
3446
3657
|
});
|
|
3447
3658
|
if (!response.ok || !response.body) {
|
|
3448
3659
|
try {
|
|
@@ -3456,13 +3667,24 @@ async function downloadMediaStream(items, options) {
|
|
|
3456
3667
|
if (item.date) {
|
|
3457
3668
|
try {
|
|
3458
3669
|
const mtime = new Date(item.date * 1e3);
|
|
3459
|
-
await
|
|
3670
|
+
await fsPromises3.utimes(tmpPath, mtime, mtime);
|
|
3671
|
+
} catch {
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
await fsPromises3.rename(tmpPath, finalPath);
|
|
3675
|
+
if (options.writeMetadata) {
|
|
3676
|
+
try {
|
|
3677
|
+
const sidecar = buildMetadataSidecar(item, finalFilename, options.defaultPlatform, options.defaultAuthor);
|
|
3678
|
+
await writeMetadataSidecar(sidecarPath, sidecar, item.date);
|
|
3460
3679
|
} catch {
|
|
3461
3680
|
}
|
|
3462
3681
|
}
|
|
3463
|
-
await fsPromises2.rename(tmpPath, finalPath);
|
|
3464
3682
|
archive.add(item.id);
|
|
3465
3683
|
downloaded++;
|
|
3684
|
+
consecutiveErrors = 0;
|
|
3685
|
+
if (options.limit !== void 0 && downloaded >= options.limit) {
|
|
3686
|
+
isDone = true;
|
|
3687
|
+
}
|
|
3466
3688
|
onProgress?.({
|
|
3467
3689
|
job: jobName,
|
|
3468
3690
|
downloadedCount: downloaded,
|
|
@@ -3473,7 +3695,7 @@ async function downloadMediaStream(items, options) {
|
|
|
3473
3695
|
} catch (err) {
|
|
3474
3696
|
try {
|
|
3475
3697
|
if (fs6.existsSync(tmpPath)) {
|
|
3476
|
-
await
|
|
3698
|
+
await fsPromises3.unlink(tmpPath);
|
|
3477
3699
|
}
|
|
3478
3700
|
} catch {
|
|
3479
3701
|
}
|
|
@@ -3494,33 +3716,172 @@ async function downloadMediaStream(items, options) {
|
|
|
3494
3716
|
}
|
|
3495
3717
|
const workers = Array.from({ length: concurrency }, () => downloadWorker());
|
|
3496
3718
|
await Promise.all(workers);
|
|
3497
|
-
|
|
3719
|
+
for (const [d, arch] of dirArchives.entries()) {
|
|
3720
|
+
await saveArchive(d, arch);
|
|
3721
|
+
}
|
|
3498
3722
|
return { downloaded, skipped, errors };
|
|
3499
3723
|
}
|
|
3500
3724
|
|
|
3725
|
+
// src/lib/engines/checkpoint.ts
|
|
3726
|
+
import fsPromises4 from "fs/promises";
|
|
3727
|
+
import path6 from "path";
|
|
3728
|
+
var CHECKPOINT_FILENAME = ".open-nami-checkpoint.json";
|
|
3729
|
+
async function loadCheckpoint(destDir) {
|
|
3730
|
+
const filePath = path6.join(destDir, CHECKPOINT_FILENAME);
|
|
3731
|
+
try {
|
|
3732
|
+
const raw = await fsPromises4.readFile(filePath, "utf-8");
|
|
3733
|
+
const parsed = JSON.parse(raw);
|
|
3734
|
+
if (parsed && typeof parsed === "object" && parsed.cursors) {
|
|
3735
|
+
return parsed;
|
|
3736
|
+
}
|
|
3737
|
+
} catch {
|
|
3738
|
+
}
|
|
3739
|
+
return null;
|
|
3740
|
+
}
|
|
3741
|
+
async function saveCheckpoint(destDir, checkpoint) {
|
|
3742
|
+
const filePath = path6.join(destDir, CHECKPOINT_FILENAME);
|
|
3743
|
+
const tmpPath = `${filePath}.tmp`;
|
|
3744
|
+
try {
|
|
3745
|
+
await fsPromises4.mkdir(destDir, { recursive: true });
|
|
3746
|
+
await fsPromises4.writeFile(tmpPath, JSON.stringify(checkpoint, null, 2), "utf-8");
|
|
3747
|
+
await fsPromises4.rename(tmpPath, filePath);
|
|
3748
|
+
} catch {
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
async function clearCheckpoint(destDir) {
|
|
3752
|
+
const filePath = path6.join(destDir, CHECKPOINT_FILENAME);
|
|
3753
|
+
try {
|
|
3754
|
+
await fsPromises4.unlink(filePath);
|
|
3755
|
+
} catch {
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// src/lib/engines/filters.ts
|
|
3760
|
+
function parseDateBoundary(input, boundary) {
|
|
3761
|
+
if (input === void 0 || input === null) return void 0;
|
|
3762
|
+
if (input instanceof Date) {
|
|
3763
|
+
if (isNaN(input.getTime())) return void 0;
|
|
3764
|
+
return Math.floor(input.getTime() / 1e3);
|
|
3765
|
+
}
|
|
3766
|
+
if (typeof input === "number") {
|
|
3767
|
+
if (isNaN(input) || input <= 0) return void 0;
|
|
3768
|
+
if (input > 4e9) {
|
|
3769
|
+
return Math.floor(input / 1e3);
|
|
3770
|
+
}
|
|
3771
|
+
return Math.floor(input);
|
|
3772
|
+
}
|
|
3773
|
+
const trimmed = input.trim();
|
|
3774
|
+
if (!trimmed) return void 0;
|
|
3775
|
+
const relMatch = trimmed.match(/^(\d+)\s*(s|m|h|d|w|y)$/i);
|
|
3776
|
+
if (relMatch) {
|
|
3777
|
+
const amount = parseInt(relMatch[1], 10);
|
|
3778
|
+
const unit = relMatch[2].toLowerCase();
|
|
3779
|
+
const multipliers = {
|
|
3780
|
+
s: 1,
|
|
3781
|
+
m: 60,
|
|
3782
|
+
h: 3600,
|
|
3783
|
+
d: 86400,
|
|
3784
|
+
w: 604800,
|
|
3785
|
+
y: 31536e3
|
|
3786
|
+
};
|
|
3787
|
+
const offsetSeconds = amount * (multipliers[unit] ?? 86400);
|
|
3788
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3789
|
+
return nowSeconds - offsetSeconds;
|
|
3790
|
+
}
|
|
3791
|
+
if (/^\d+$/.test(trimmed)) {
|
|
3792
|
+
if (trimmed.length === 8) {
|
|
3793
|
+
const year = parseInt(trimmed.slice(0, 4), 10);
|
|
3794
|
+
const month = parseInt(trimmed.slice(4, 6), 10) - 1;
|
|
3795
|
+
const day = parseInt(trimmed.slice(6, 8), 10);
|
|
3796
|
+
const date = new Date(Date.UTC(year, month, day, boundary === "until" ? 23 : 0, boundary === "until" ? 59 : 0, boundary === "until" ? 59 : 0));
|
|
3797
|
+
if (!isNaN(date.getTime())) {
|
|
3798
|
+
return Math.floor(date.getTime() / 1e3);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
const num = parseInt(trimmed, 10);
|
|
3802
|
+
if (num > 4e9) {
|
|
3803
|
+
return Math.floor(num / 1e3);
|
|
3804
|
+
}
|
|
3805
|
+
return num;
|
|
3806
|
+
}
|
|
3807
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
3808
|
+
const timeStr = boundary === "until" ? "T23:59:59.999Z" : "T00:00:00.000Z";
|
|
3809
|
+
const parsed2 = /* @__PURE__ */ new Date(`${trimmed}${timeStr}`);
|
|
3810
|
+
if (!isNaN(parsed2.getTime())) {
|
|
3811
|
+
return Math.floor(parsed2.getTime() / 1e3);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
const parsed = new Date(trimmed);
|
|
3815
|
+
if (!isNaN(parsed.getTime())) {
|
|
3816
|
+
return Math.floor(parsed.getTime() / 1e3);
|
|
3817
|
+
}
|
|
3818
|
+
return void 0;
|
|
3819
|
+
}
|
|
3820
|
+
function parseLimit(input) {
|
|
3821
|
+
if (input === void 0 || input === null) return void 0;
|
|
3822
|
+
const num = typeof input === "number" ? input : parseInt(String(input).trim(), 10);
|
|
3823
|
+
if (isNaN(num) || num <= 0) return void 0;
|
|
3824
|
+
return Math.floor(num);
|
|
3825
|
+
}
|
|
3826
|
+
function shouldIncludeItem(item, filters) {
|
|
3827
|
+
const reverseChrono = filters.reverseChronological ?? true;
|
|
3828
|
+
if (item.date !== void 0 && item.date !== null) {
|
|
3829
|
+
if (filters.until !== void 0 && item.date > filters.until) {
|
|
3830
|
+
return { include: false, stop: false };
|
|
3831
|
+
}
|
|
3832
|
+
if (filters.since !== void 0 && item.date < filters.since) {
|
|
3833
|
+
return { include: false, stop: reverseChrono };
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return { include: true, stop: false };
|
|
3837
|
+
}
|
|
3838
|
+
async function* filterMediaStream(items, filters) {
|
|
3839
|
+
const maxLimit = filters.limit;
|
|
3840
|
+
let count = 0;
|
|
3841
|
+
if (maxLimit !== void 0 && maxLimit <= 0) {
|
|
3842
|
+
return;
|
|
3843
|
+
}
|
|
3844
|
+
for await (const item of items) {
|
|
3845
|
+
if (maxLimit !== void 0 && count >= maxLimit) {
|
|
3846
|
+
break;
|
|
3847
|
+
}
|
|
3848
|
+
const { include, stop } = shouldIncludeItem(item, filters);
|
|
3849
|
+
if (include) {
|
|
3850
|
+
count++;
|
|
3851
|
+
yield item;
|
|
3852
|
+
if (maxLimit !== void 0 && count >= maxLimit) {
|
|
3853
|
+
break;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
if (stop) {
|
|
3857
|
+
break;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3501
3862
|
// src/lib/engines/gallery-dl.ts
|
|
3502
3863
|
import { spawn as spawn2 } from "child_process";
|
|
3503
|
-
import
|
|
3864
|
+
import path9 from "path";
|
|
3504
3865
|
import fs9 from "fs/promises";
|
|
3505
3866
|
|
|
3506
3867
|
// src/lib/engines/exec.ts
|
|
3507
3868
|
import fs8 from "fs";
|
|
3508
|
-
import
|
|
3869
|
+
import path8 from "path";
|
|
3509
3870
|
|
|
3510
3871
|
// src/lib/engines/fetcher.ts
|
|
3511
3872
|
import { spawn } from "child_process";
|
|
3512
3873
|
import { createWriteStream } from "fs";
|
|
3513
3874
|
import fs7 from "fs/promises";
|
|
3514
3875
|
import os7 from "os";
|
|
3515
|
-
import
|
|
3876
|
+
import path7 from "path";
|
|
3516
3877
|
import { Readable as Readable2 } from "stream";
|
|
3517
3878
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
3518
3879
|
function getOpenNamiBinDir(env = process.env) {
|
|
3519
3880
|
if (env.OPEN_NAMI_BIN_DIR && env.OPEN_NAMI_BIN_DIR.trim()) {
|
|
3520
|
-
return
|
|
3881
|
+
return path7.resolve(env.OPEN_NAMI_BIN_DIR.trim());
|
|
3521
3882
|
}
|
|
3522
3883
|
const base = env.OPEN_NAMI_DIR && env.OPEN_NAMI_DIR.trim() ? env.OPEN_NAMI_DIR.trim() : os7.homedir();
|
|
3523
|
-
return
|
|
3884
|
+
return path7.join(base, ".open-nami", "bin");
|
|
3524
3885
|
}
|
|
3525
3886
|
var YTDLP_RELEASE_BASE = "https://github.com/yt-dlp/yt-dlp/releases/latest/download";
|
|
3526
3887
|
var GALLERYDL_CODEBERG_API = "https://codeberg.org/api/v1/repos/mikf/gallery-dl/releases/latest";
|
|
@@ -3571,7 +3932,7 @@ function getBinaryVersion(executablePath) {
|
|
|
3571
3932
|
});
|
|
3572
3933
|
}
|
|
3573
3934
|
async function downloadBinary(url, targetFile, signal) {
|
|
3574
|
-
await fs7.mkdir(
|
|
3935
|
+
await fs7.mkdir(path7.dirname(targetFile), { recursive: true });
|
|
3575
3936
|
const response = await fetch(url, {
|
|
3576
3937
|
signal,
|
|
3577
3938
|
headers: { "User-Agent": "open-nami" }
|
|
@@ -3603,7 +3964,7 @@ async function downloadBinary(url, targetFile, signal) {
|
|
|
3603
3964
|
async function downloadLatestYtDlp(targetDir, signal, onStatus) {
|
|
3604
3965
|
const dir = targetDir || getOpenNamiBinDir();
|
|
3605
3966
|
const binaryName = process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
|
|
3606
|
-
const local =
|
|
3967
|
+
const local = path7.join(dir, binaryName);
|
|
3607
3968
|
const asset = ytDlpAssetName();
|
|
3608
3969
|
const url = `${YTDLP_RELEASE_BASE}/${asset}`;
|
|
3609
3970
|
onStatus?.(`fetching latest yt-dlp (${asset})\u2026`);
|
|
@@ -3639,7 +4000,7 @@ async function downloadLatestGalleryDl(targetDir, signal, onStatus) {
|
|
|
3639
4000
|
}
|
|
3640
4001
|
const dir = targetDir || getOpenNamiBinDir();
|
|
3641
4002
|
const binaryName = process.platform === "win32" ? "gallery-dl.exe" : "gallery-dl";
|
|
3642
|
-
const local =
|
|
4003
|
+
const local = path7.join(dir, binaryName);
|
|
3643
4004
|
const asset = galleryDlAssetName();
|
|
3644
4005
|
const candidateUrls = await getGalleryDlDownloadUrls(asset, fetch, signal);
|
|
3645
4006
|
let lastError;
|
|
@@ -3657,7 +4018,7 @@ async function downloadLatestGalleryDl(targetDir, signal, onStatus) {
|
|
|
3657
4018
|
async function ensureYtDlp(onStatus, signal) {
|
|
3658
4019
|
if (await commandWorks("yt-dlp", ["--version"])) return "yt-dlp";
|
|
3659
4020
|
const binaryName = process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
|
|
3660
|
-
const local =
|
|
4021
|
+
const local = path7.join(getOpenNamiBinDir(), binaryName);
|
|
3661
4022
|
if (await commandWorks(local, ["--version"])) return local;
|
|
3662
4023
|
if (await commandWorks("python", ["-m", "yt_dlp", "--version"])) return "python";
|
|
3663
4024
|
if (await commandWorks("python3", ["-m", "yt_dlp", "--version"])) return "python3";
|
|
@@ -3667,7 +4028,7 @@ async function ensureYtDlp(onStatus, signal) {
|
|
|
3667
4028
|
async function ensureGalleryDl(onStatus, signal) {
|
|
3668
4029
|
if (await commandWorks("gallery-dl", ["--version"])) return "gallery-dl";
|
|
3669
4030
|
const binaryName = process.platform === "win32" ? "gallery-dl.exe" : "gallery-dl";
|
|
3670
|
-
const local =
|
|
4031
|
+
const local = path7.join(getOpenNamiBinDir(), binaryName);
|
|
3671
4032
|
if (await commandWorks(local, ["--version"])) return local;
|
|
3672
4033
|
if (await commandWorks("python", ["-m", "gallery_dl", "--version"])) return "python";
|
|
3673
4034
|
if (await commandWorks("python3", ["-m", "gallery_dl", "--version"])) return "python3";
|
|
@@ -3680,7 +4041,7 @@ async function updateYtDlp(options) {
|
|
|
3680
4041
|
const signal = options?.signal;
|
|
3681
4042
|
const dir = getOpenNamiBinDir();
|
|
3682
4043
|
const binaryName = process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
|
|
3683
|
-
const local =
|
|
4044
|
+
const local = path7.join(dir, binaryName);
|
|
3684
4045
|
let previousVersion;
|
|
3685
4046
|
if (await commandWorks(local, ["--version"])) {
|
|
3686
4047
|
previousVersion = await getBinaryVersion(local);
|
|
@@ -3703,7 +4064,7 @@ async function updateGalleryDl(options) {
|
|
|
3703
4064
|
const signal = options?.signal;
|
|
3704
4065
|
const dir = getOpenNamiBinDir();
|
|
3705
4066
|
const binaryName = process.platform === "win32" ? "gallery-dl.exe" : "gallery-dl";
|
|
3706
|
-
const local =
|
|
4067
|
+
const local = path7.join(dir, binaryName);
|
|
3707
4068
|
let previousVersion;
|
|
3708
4069
|
if (await commandWorks(local, ["--version"])) {
|
|
3709
4070
|
previousVersion = await getBinaryVersion(local);
|
|
@@ -3724,13 +4085,13 @@ async function updateGalleryDl(options) {
|
|
|
3724
4085
|
// src/lib/engines/exec.ts
|
|
3725
4086
|
function findExecutable(binName, pathEnv = process.env.PATH || "") {
|
|
3726
4087
|
if (!pathEnv) return null;
|
|
3727
|
-
const delimiter =
|
|
4088
|
+
const delimiter = path8.delimiter;
|
|
3728
4089
|
const dirs = pathEnv.split(delimiter).filter(Boolean);
|
|
3729
4090
|
const isWin = process.platform === "win32";
|
|
3730
4091
|
const extensions = isWin ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
3731
4092
|
for (const dir of dirs) {
|
|
3732
4093
|
for (const ext of extensions) {
|
|
3733
|
-
const fullPath =
|
|
4094
|
+
const fullPath = path8.join(dir, `${binName}${ext}`);
|
|
3734
4095
|
try {
|
|
3735
4096
|
if (fs8.existsSync(fullPath)) {
|
|
3736
4097
|
const stat = fs8.statSync(fullPath);
|
|
@@ -3763,7 +4124,7 @@ function resolveEngineCommand(engine, options = {}) {
|
|
|
3763
4124
|
}
|
|
3764
4125
|
const binDir = getOpenNamiBinDir(env);
|
|
3765
4126
|
const isWin = process.platform === "win32";
|
|
3766
|
-
const localBin =
|
|
4127
|
+
const localBin = path8.join(binDir, `${engine}${isWin ? ".exe" : ""}`);
|
|
3767
4128
|
try {
|
|
3768
4129
|
if (fs8.existsSync(localBin)) {
|
|
3769
4130
|
const stat = fs8.statSync(localBin);
|
|
@@ -3808,7 +4169,7 @@ var PLATFORM_SLEEP = {
|
|
|
3808
4169
|
facebook: "1.0-1.8"
|
|
3809
4170
|
};
|
|
3810
4171
|
function buildGalleryDlArgs(options) {
|
|
3811
|
-
const { destDir, subDir, platform, cleanUrl, username, cookiePath } = options;
|
|
4172
|
+
const { destDir, subDir, platform, cleanUrl, username, cookiePath, writeMetadata } = options;
|
|
3812
4173
|
const args = [
|
|
3813
4174
|
"-D",
|
|
3814
4175
|
destDir,
|
|
@@ -3823,12 +4184,15 @@ function buildGalleryDlArgs(options) {
|
|
|
3823
4184
|
"-o",
|
|
3824
4185
|
'headers.sec-ch-ua-platform="Windows"',
|
|
3825
4186
|
"--download-archive",
|
|
3826
|
-
|
|
4187
|
+
path9.join(destDir, GDL_ARCHIVE),
|
|
3827
4188
|
"--retries",
|
|
3828
4189
|
"3",
|
|
3829
4190
|
"--sleep-429",
|
|
3830
4191
|
"10"
|
|
3831
4192
|
];
|
|
4193
|
+
if (writeMetadata) {
|
|
4194
|
+
args.push("--write-metadata");
|
|
4195
|
+
}
|
|
3832
4196
|
let targetUrl = cleanUrl;
|
|
3833
4197
|
const sleepDelay = PLATFORM_SLEEP[platform] || "0.8-1.5";
|
|
3834
4198
|
if (subDir === "Photos") {
|
|
@@ -3861,7 +4225,7 @@ function buildGalleryDlArgs(options) {
|
|
|
3861
4225
|
}
|
|
3862
4226
|
async function runGalleryDl(options) {
|
|
3863
4227
|
const { profile, targetDir, subDir, cookiePath, onProgress } = options;
|
|
3864
|
-
const destDir =
|
|
4228
|
+
const destDir = path9.join(targetDir, subDir);
|
|
3865
4229
|
await fs9.mkdir(destDir, { recursive: true });
|
|
3866
4230
|
const args = buildGalleryDlArgs({
|
|
3867
4231
|
destDir,
|
|
@@ -3869,7 +4233,8 @@ async function runGalleryDl(options) {
|
|
|
3869
4233
|
platform: profile.platform,
|
|
3870
4234
|
cleanUrl: profile.cleanUrl,
|
|
3871
4235
|
username: profile.username,
|
|
3872
|
-
cookiePath
|
|
4236
|
+
cookiePath,
|
|
4237
|
+
writeMetadata: options.writeMetadata
|
|
3873
4238
|
});
|
|
3874
4239
|
let downloadedCount = 0;
|
|
3875
4240
|
let skippedCount = 0;
|
|
@@ -3892,7 +4257,7 @@ async function runGalleryDl(options) {
|
|
|
3892
4257
|
error: err.message || String(err)
|
|
3893
4258
|
};
|
|
3894
4259
|
}
|
|
3895
|
-
const engineCmd = engineCmdStr.includes(
|
|
4260
|
+
const engineCmd = engineCmdStr.includes(path9.sep) ? { command: engineCmdStr, baseArgs: [] } : resolveEngineCommand("gallery-dl");
|
|
3896
4261
|
const fullArgs = [...engineCmd.baseArgs, ...args];
|
|
3897
4262
|
return new Promise((resolve) => {
|
|
3898
4263
|
onProgress?.({
|
|
@@ -3911,7 +4276,7 @@ async function runGalleryDl(options) {
|
|
|
3911
4276
|
if (!trimmed) return;
|
|
3912
4277
|
if (trimmed.startsWith("#")) {
|
|
3913
4278
|
skippedCount++;
|
|
3914
|
-
const filename =
|
|
4279
|
+
const filename = path9.basename(trimmed.slice(1).trim());
|
|
3915
4280
|
onProgress?.({
|
|
3916
4281
|
job: subDir,
|
|
3917
4282
|
downloadedCount,
|
|
@@ -3919,9 +4284,9 @@ async function runGalleryDl(options) {
|
|
|
3919
4284
|
currentFile: filename,
|
|
3920
4285
|
statusText: `Skipped: ${filename}`
|
|
3921
4286
|
});
|
|
3922
|
-
} else if (trimmed.match(/\.(jpe?g|png|gif|webp|bmp|jfif|heic|avif|tiff|svg|mp4|mov|webm|mkv|avi|flv|m4v)$/i) || trimmed.includes(
|
|
4287
|
+
} else if (trimmed.match(/\.(jpe?g|png|gif|webp|bmp|jfif|heic|avif|tiff|svg|mp4|mov|webm|mkv|avi|flv|m4v)$/i) || trimmed.includes(path9.sep)) {
|
|
3923
4288
|
downloadedCount++;
|
|
3924
|
-
const filename =
|
|
4289
|
+
const filename = path9.basename(trimmed);
|
|
3925
4290
|
onProgress?.({
|
|
3926
4291
|
job: subDir,
|
|
3927
4292
|
downloadedCount,
|
|
@@ -3972,15 +4337,15 @@ async function runGalleryDl(options) {
|
|
|
3972
4337
|
|
|
3973
4338
|
// src/lib/engines/yt-dlp.ts
|
|
3974
4339
|
import { spawn as spawn3 } from "child_process";
|
|
3975
|
-
import
|
|
4340
|
+
import path10 from "path";
|
|
3976
4341
|
import fs10 from "fs/promises";
|
|
3977
4342
|
var YTDLP_ARCHIVE = "archive_yt-dlp.txt";
|
|
3978
4343
|
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";
|
|
3979
4344
|
function buildYtDlpArgs(options) {
|
|
3980
|
-
const { destDir, platform, cleanUrl, cookiePath } = options;
|
|
4345
|
+
const { destDir, platform, cleanUrl, cookiePath, writeMetadata } = options;
|
|
3981
4346
|
const args = [
|
|
3982
4347
|
"-o",
|
|
3983
|
-
|
|
4348
|
+
path10.join(destDir, "%(title).150B [%(id)s].%(ext)s"),
|
|
3984
4349
|
"--windows-filenames",
|
|
3985
4350
|
"--newline",
|
|
3986
4351
|
"--ignore-errors",
|
|
@@ -3988,7 +4353,7 @@ function buildYtDlpArgs(options) {
|
|
|
3988
4353
|
"--user-agent",
|
|
3989
4354
|
UA2,
|
|
3990
4355
|
"--download-archive",
|
|
3991
|
-
|
|
4356
|
+
path10.join(destDir, YTDLP_ARCHIVE),
|
|
3992
4357
|
"--concurrent-fragments",
|
|
3993
4358
|
"4",
|
|
3994
4359
|
"--retries",
|
|
@@ -4005,6 +4370,9 @@ function buildYtDlpArgs(options) {
|
|
|
4005
4370
|
} else if (platform === "x") {
|
|
4006
4371
|
args.push("--sleep-requests", "0.8:1.8", "--retry-sleep", "429:exp=5:30");
|
|
4007
4372
|
}
|
|
4373
|
+
if (writeMetadata) {
|
|
4374
|
+
args.push("--write-info-json", "--write-description");
|
|
4375
|
+
}
|
|
4008
4376
|
if (cookiePath) {
|
|
4009
4377
|
args.push("--cookies", cookiePath);
|
|
4010
4378
|
}
|
|
@@ -4013,13 +4381,14 @@ function buildYtDlpArgs(options) {
|
|
|
4013
4381
|
}
|
|
4014
4382
|
async function runYtDlp(options) {
|
|
4015
4383
|
const { profile, targetDir, cookiePath, onProgress } = options;
|
|
4016
|
-
const destDir =
|
|
4384
|
+
const destDir = path10.join(targetDir, "Videos");
|
|
4017
4385
|
await fs10.mkdir(destDir, { recursive: true });
|
|
4018
4386
|
const args = buildYtDlpArgs({
|
|
4019
4387
|
destDir,
|
|
4020
4388
|
platform: profile.platform,
|
|
4021
4389
|
cleanUrl: profile.cleanUrl,
|
|
4022
|
-
cookiePath
|
|
4390
|
+
cookiePath,
|
|
4391
|
+
writeMetadata: options.writeMetadata
|
|
4023
4392
|
});
|
|
4024
4393
|
let downloadedCount = 0;
|
|
4025
4394
|
let skippedCount = 0;
|
|
@@ -4042,7 +4411,7 @@ async function runYtDlp(options) {
|
|
|
4042
4411
|
error: err.message || String(err)
|
|
4043
4412
|
};
|
|
4044
4413
|
}
|
|
4045
|
-
const engineCmd = engineCmdStr.includes(
|
|
4414
|
+
const engineCmd = engineCmdStr.includes(path10.sep) ? { command: engineCmdStr, baseArgs: [] } : resolveEngineCommand("yt-dlp");
|
|
4046
4415
|
const fullArgs = [...engineCmd.baseArgs, ...args];
|
|
4047
4416
|
return new Promise((resolve) => {
|
|
4048
4417
|
onProgress?.({
|
|
@@ -4069,7 +4438,7 @@ async function runYtDlp(options) {
|
|
|
4069
4438
|
});
|
|
4070
4439
|
} else if (trimmed.includes("[download] Destination:")) {
|
|
4071
4440
|
const dest = trimmed.replace("[download] Destination:", "").trim();
|
|
4072
|
-
const filename =
|
|
4441
|
+
const filename = path10.basename(dest);
|
|
4073
4442
|
onProgress?.({
|
|
4074
4443
|
job: "Videos",
|
|
4075
4444
|
downloadedCount,
|
|
@@ -4134,31 +4503,31 @@ var REFERER_MAP = {
|
|
|
4134
4503
|
};
|
|
4135
4504
|
function expandPath(dir) {
|
|
4136
4505
|
if (dir.startsWith("~/") || dir.startsWith("~\\") || dir === "~") {
|
|
4137
|
-
return
|
|
4506
|
+
return path11.join(os8.homedir(), dir.slice(1));
|
|
4138
4507
|
}
|
|
4139
4508
|
return dir;
|
|
4140
4509
|
}
|
|
4141
4510
|
function resolveDownloadBaseDir(customBaseDir) {
|
|
4142
4511
|
if (customBaseDir && customBaseDir.trim()) {
|
|
4143
|
-
return
|
|
4512
|
+
return path11.resolve(expandPath(customBaseDir.trim()));
|
|
4144
4513
|
}
|
|
4145
4514
|
const envDir = process.env.OPEN_NAMI_DIR || process.env.NAMI_DIR;
|
|
4146
4515
|
if (envDir && envDir.trim()) {
|
|
4147
|
-
return
|
|
4516
|
+
return path11.resolve(expandPath(envDir.trim()));
|
|
4148
4517
|
}
|
|
4149
4518
|
const configDir = loadConfig().downloadDir;
|
|
4150
4519
|
if (configDir && configDir.trim()) {
|
|
4151
|
-
return
|
|
4520
|
+
return path11.resolve(expandPath(configDir.trim()));
|
|
4152
4521
|
}
|
|
4153
4522
|
if (process.env.NAMI_BASE_DIR && process.env.NAMI_BASE_DIR.trim()) {
|
|
4154
4523
|
const raw = process.env.NAMI_BASE_DIR.trim();
|
|
4155
|
-
return
|
|
4524
|
+
return path11.resolve(expandPath(raw.endsWith("downloads") ? raw : path11.join(raw, "downloads")));
|
|
4156
4525
|
}
|
|
4157
|
-
return
|
|
4526
|
+
return path11.join(resolvePlatformDownloadsDir(), "Open Nami");
|
|
4158
4527
|
}
|
|
4159
4528
|
function resolveDefaultDownloadDir(platform, username, baseDir) {
|
|
4160
4529
|
const root = resolveDownloadBaseDir(baseDir);
|
|
4161
|
-
return
|
|
4530
|
+
return path11.join(root, platform, username);
|
|
4162
4531
|
}
|
|
4163
4532
|
async function dispatchDownload(options) {
|
|
4164
4533
|
const { profile, choice, onProgress } = options;
|
|
@@ -4180,28 +4549,70 @@ async function dispatchDownload(options) {
|
|
|
4180
4549
|
} else if (choice === "highlights") {
|
|
4181
4550
|
jobs.push({ type: "highlights" });
|
|
4182
4551
|
} else if (choice === "all") {
|
|
4183
|
-
jobs.push({ type: "
|
|
4184
|
-
jobs.push({ type: "videos" });
|
|
4552
|
+
jobs.push({ type: "all" });
|
|
4185
4553
|
if (profile.platform === "instagram") {
|
|
4186
4554
|
jobs.push({ type: "stories" });
|
|
4187
4555
|
jobs.push({ type: "highlights" });
|
|
4188
4556
|
}
|
|
4189
4557
|
}
|
|
4558
|
+
const sinceSeconds = parseDateBoundary(options.since, "since");
|
|
4559
|
+
const untilSeconds = parseDateBoundary(options.until, "until");
|
|
4560
|
+
const globalLimit = parseLimit(options.limit);
|
|
4561
|
+
const savedCheckpoint = options.resume ? await loadCheckpoint(targetDir) : null;
|
|
4562
|
+
const currentCursors = savedCheckpoint?.cursors ? { ...savedCheckpoint.cursors } : {};
|
|
4190
4563
|
const defaultRunNative = async (subDir) => {
|
|
4191
|
-
const dest =
|
|
4564
|
+
const dest = path11.join(targetDir, subDir);
|
|
4565
|
+
const remainingLimit = globalLimit !== void 0 ? Math.max(0, globalLimit - totalDownloaded) : void 0;
|
|
4566
|
+
if (remainingLimit === 0) {
|
|
4567
|
+
return { success: true, downloaded: 0, skipped: 0 };
|
|
4568
|
+
}
|
|
4192
4569
|
try {
|
|
4193
4570
|
const extractor = getExtractor(profile.platform);
|
|
4571
|
+
const subDirKey = subDir === "All" ? "timeline" : subDir.toLowerCase();
|
|
4572
|
+
const initialCursor = options.resume ? currentCursors[subDirKey] : void 0;
|
|
4194
4573
|
const items = extractor.extract(profile, {
|
|
4195
4574
|
subDir,
|
|
4196
4575
|
cookiePath: cookieCopy?.filePath,
|
|
4197
|
-
signal: options.signal
|
|
4576
|
+
signal: options.signal,
|
|
4577
|
+
since: sinceSeconds,
|
|
4578
|
+
until: untilSeconds,
|
|
4579
|
+
limit: remainingLimit,
|
|
4580
|
+
initialCursor,
|
|
4581
|
+
onCheckpoint: async (key, val) => {
|
|
4582
|
+
if (val) {
|
|
4583
|
+
currentCursors[key] = val;
|
|
4584
|
+
} else {
|
|
4585
|
+
delete currentCursors[key];
|
|
4586
|
+
}
|
|
4587
|
+
await saveCheckpoint(targetDir, {
|
|
4588
|
+
version: 1,
|
|
4589
|
+
platform: profile.platform,
|
|
4590
|
+
username: profile.username,
|
|
4591
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4592
|
+
cursors: currentCursors,
|
|
4593
|
+
completed: Object.keys(currentCursors).length === 0
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4198
4596
|
});
|
|
4199
|
-
const
|
|
4597
|
+
const filtered = filterMediaStream(items, {
|
|
4598
|
+
since: sinceSeconds,
|
|
4599
|
+
until: untilSeconds,
|
|
4600
|
+
limit: remainingLimit
|
|
4601
|
+
});
|
|
4602
|
+
const res = await downloadMediaStream(filtered, {
|
|
4200
4603
|
jobName: `${profile.username} ${subDir}`,
|
|
4201
4604
|
destDir: dest,
|
|
4605
|
+
destDirMap: subDir === "All" ? {
|
|
4606
|
+
photo: path11.join(targetDir, "Photos"),
|
|
4607
|
+
video: path11.join(targetDir, "Videos")
|
|
4608
|
+
} : void 0,
|
|
4202
4609
|
cookiePath: cookieCopy?.filePath,
|
|
4203
4610
|
referer: REFERER_MAP[profile.platform],
|
|
4204
4611
|
signal: options.signal,
|
|
4612
|
+
limit: remainingLimit,
|
|
4613
|
+
writeMetadata: options.writeMetadata,
|
|
4614
|
+
defaultPlatform: profile.platform,
|
|
4615
|
+
defaultAuthor: profile.username,
|
|
4205
4616
|
onProgress
|
|
4206
4617
|
});
|
|
4207
4618
|
return {
|
|
@@ -4227,6 +4638,7 @@ async function dispatchDownload(options) {
|
|
|
4227
4638
|
subDir,
|
|
4228
4639
|
cookiePath: cookieCopy?.filePath,
|
|
4229
4640
|
signal: options.signal,
|
|
4641
|
+
writeMetadata: options.writeMetadata,
|
|
4230
4642
|
onProgress
|
|
4231
4643
|
}));
|
|
4232
4644
|
const runYt = options.runnerOverrides?.ytRunner ?? (() => runYtDlp({
|
|
@@ -4234,11 +4646,54 @@ async function dispatchDownload(options) {
|
|
|
4234
4646
|
targetDir,
|
|
4235
4647
|
cookiePath: cookieCopy?.filePath,
|
|
4236
4648
|
signal: options.signal,
|
|
4649
|
+
writeMetadata: options.writeMetadata,
|
|
4237
4650
|
onProgress
|
|
4238
4651
|
}));
|
|
4239
4652
|
for (const job of jobs) {
|
|
4240
4653
|
if (options.signal?.aborted) break;
|
|
4241
|
-
if (
|
|
4654
|
+
if (globalLimit !== void 0 && totalDownloaded >= globalLimit) break;
|
|
4655
|
+
if (job.type === "all") {
|
|
4656
|
+
let res = await runNative("All");
|
|
4657
|
+
if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
|
|
4658
|
+
onProgress?.({
|
|
4659
|
+
job: "Photos",
|
|
4660
|
+
downloadedCount: totalDownloaded,
|
|
4661
|
+
skippedCount: totalSkipped,
|
|
4662
|
+
statusText: "switching to gallery-dl engine for photos\u2026"
|
|
4663
|
+
});
|
|
4664
|
+
const gdlPhotos = await runGdl("Photos");
|
|
4665
|
+
totalDownloaded += gdlPhotos.downloaded;
|
|
4666
|
+
totalSkipped += gdlPhotos.skipped;
|
|
4667
|
+
if (!gdlPhotos.success && gdlPhotos.error) errors.push(`[Photos] ${gdlPhotos.error}`);
|
|
4668
|
+
onProgress?.({
|
|
4669
|
+
job: "Videos",
|
|
4670
|
+
downloadedCount: totalDownloaded,
|
|
4671
|
+
skippedCount: totalSkipped,
|
|
4672
|
+
statusText: "switching to yt-dlp engine for videos\u2026"
|
|
4673
|
+
});
|
|
4674
|
+
const ytVideos = await runYt();
|
|
4675
|
+
if (ytVideos.downloaded > 0 || ytVideos.skipped > 0 || ytVideos.success) {
|
|
4676
|
+
totalDownloaded += ytVideos.downloaded;
|
|
4677
|
+
totalSkipped += ytVideos.skipped;
|
|
4678
|
+
if (!ytVideos.success && ytVideos.error) errors.push(`[Videos] ${ytVideos.error}`);
|
|
4679
|
+
} else if (!options.signal?.aborted) {
|
|
4680
|
+
onProgress?.({
|
|
4681
|
+
job: "Videos",
|
|
4682
|
+
downloadedCount: totalDownloaded,
|
|
4683
|
+
skippedCount: totalSkipped,
|
|
4684
|
+
statusText: "switching to gallery-dl engine for videos\u2026"
|
|
4685
|
+
});
|
|
4686
|
+
const gdlVideos = await runGdl("Videos");
|
|
4687
|
+
totalDownloaded += gdlVideos.downloaded;
|
|
4688
|
+
totalSkipped += gdlVideos.skipped;
|
|
4689
|
+
if (!gdlVideos.success && gdlVideos.error) errors.push(`[Videos] ${gdlVideos.error}`);
|
|
4690
|
+
}
|
|
4691
|
+
} else {
|
|
4692
|
+
totalDownloaded += res.downloaded;
|
|
4693
|
+
totalSkipped += res.skipped;
|
|
4694
|
+
if (!res.success && res.error) errors.push(`[All] ${res.error}`);
|
|
4695
|
+
}
|
|
4696
|
+
} else if (job.type === "photos" || job.type === "stories" || job.type === "highlights") {
|
|
4242
4697
|
const subDir = job.type === "photos" ? "Photos" : job.type === "stories" ? "Stories" : "Highlights";
|
|
4243
4698
|
let res = await runNative(subDir);
|
|
4244
4699
|
if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
|
|
@@ -4288,6 +4743,9 @@ async function dispatchDownload(options) {
|
|
|
4288
4743
|
}
|
|
4289
4744
|
}
|
|
4290
4745
|
}
|
|
4746
|
+
if (errors.length === 0 && !options.signal?.aborted) {
|
|
4747
|
+
await clearCheckpoint(targetDir);
|
|
4748
|
+
}
|
|
4291
4749
|
return {
|
|
4292
4750
|
success: errors.length === 0 || totalDownloaded > 0,
|
|
4293
4751
|
downloadedCount: totalDownloaded,
|
|
@@ -4325,7 +4783,7 @@ function readClipboard(execFn = execFileSync2) {
|
|
|
4325
4783
|
// src/lib/reveal.ts
|
|
4326
4784
|
import fs12 from "fs";
|
|
4327
4785
|
import { spawn as spawn4 } from "child_process";
|
|
4328
|
-
import
|
|
4786
|
+
import path12 from "path";
|
|
4329
4787
|
function buildOpenBrowserCommand(url, platform = process.platform) {
|
|
4330
4788
|
try {
|
|
4331
4789
|
const parsed = new URL(url.trim());
|
|
@@ -4352,7 +4810,7 @@ function openBrowser(url) {
|
|
|
4352
4810
|
}
|
|
4353
4811
|
}
|
|
4354
4812
|
function getRevealCommand(targetPath, platform = process.platform, isDirectory) {
|
|
4355
|
-
const pathModule = platform === "win32" ?
|
|
4813
|
+
const pathModule = platform === "win32" ? path12.win32 : platform === "darwin" ? path12.posix : path12;
|
|
4356
4814
|
const resolved = pathModule.resolve(targetPath);
|
|
4357
4815
|
const isDir = isDirectory !== void 0 ? isDirectory : (() => {
|
|
4358
4816
|
try {
|
|
@@ -4485,6 +4943,11 @@ function App({
|
|
|
4485
4943
|
initialChoice,
|
|
4486
4944
|
initialThemeMode = "auto",
|
|
4487
4945
|
outputDir,
|
|
4946
|
+
since,
|
|
4947
|
+
until,
|
|
4948
|
+
limit,
|
|
4949
|
+
resume,
|
|
4950
|
+
writeMetadata,
|
|
4488
4951
|
version = "1.0.0"
|
|
4489
4952
|
}) {
|
|
4490
4953
|
const [themeMode, setThemeMode] = useState6(initialThemeMode);
|
|
@@ -4497,6 +4960,11 @@ function App({
|
|
|
4497
4960
|
initialInput,
|
|
4498
4961
|
initialChoice,
|
|
4499
4962
|
outputDir,
|
|
4963
|
+
since,
|
|
4964
|
+
until,
|
|
4965
|
+
limit,
|
|
4966
|
+
resume,
|
|
4967
|
+
writeMetadata,
|
|
4500
4968
|
version,
|
|
4501
4969
|
cycleTheme
|
|
4502
4970
|
}
|
|
@@ -4506,6 +4974,11 @@ function InnerApp({
|
|
|
4506
4974
|
initialInput,
|
|
4507
4975
|
initialChoice,
|
|
4508
4976
|
outputDir,
|
|
4977
|
+
since,
|
|
4978
|
+
until,
|
|
4979
|
+
limit,
|
|
4980
|
+
resume,
|
|
4981
|
+
writeMetadata,
|
|
4509
4982
|
version = "1.0.0",
|
|
4510
4983
|
cycleTheme
|
|
4511
4984
|
}) {
|
|
@@ -4572,6 +5045,11 @@ function InnerApp({
|
|
|
4572
5045
|
profile,
|
|
4573
5046
|
choice,
|
|
4574
5047
|
outputDir: targetDir,
|
|
5048
|
+
since,
|
|
5049
|
+
until,
|
|
5050
|
+
limit,
|
|
5051
|
+
resume,
|
|
5052
|
+
writeMetadata,
|
|
4575
5053
|
signal: controller.signal,
|
|
4576
5054
|
onProgress: (progress) => {
|
|
4577
5055
|
if (!controller.signal.aborted) {
|
|
@@ -4599,7 +5077,7 @@ function InnerApp({
|
|
|
4599
5077
|
hint: "Check your internet connection or verify the profile exists."
|
|
4600
5078
|
});
|
|
4601
5079
|
}
|
|
4602
|
-
}, [outputDir]);
|
|
5080
|
+
}, [outputDir, since, until, limit, resume, writeMetadata]);
|
|
4603
5081
|
const handlePlatformSelect = useCallback2((platform, username, rawInput) => {
|
|
4604
5082
|
const canonical = buildCanonicalProfile(platform, username, rawInput);
|
|
4605
5083
|
setPhase({ name: "probing", target: canonical.cleanUrl, status: "probing profile\u2026" });
|
|
@@ -5086,6 +5564,198 @@ function generateCompletion(shell) {
|
|
|
5086
5564
|
}
|
|
5087
5565
|
}
|
|
5088
5566
|
|
|
5567
|
+
// src/lib/watcher.ts
|
|
5568
|
+
function startClipboardWatcher(options = {}) {
|
|
5569
|
+
const choice = options.choice || "all";
|
|
5570
|
+
const intervalMs = options.intervalMs ?? 1e3;
|
|
5571
|
+
const readClip = options.readClipboardFn ?? readClipboard;
|
|
5572
|
+
const dispatch = options.dispatchFn ?? dispatchDownload;
|
|
5573
|
+
let isRunning = true;
|
|
5574
|
+
let lastClipboardText = "";
|
|
5575
|
+
const processedUrls = /* @__PURE__ */ new Set();
|
|
5576
|
+
let timer = null;
|
|
5577
|
+
let resolvePromise = null;
|
|
5578
|
+
const stop = () => {
|
|
5579
|
+
if (!isRunning && !timer && !resolvePromise) return;
|
|
5580
|
+
isRunning = false;
|
|
5581
|
+
if (timer) {
|
|
5582
|
+
clearTimeout(timer);
|
|
5583
|
+
timer = null;
|
|
5584
|
+
}
|
|
5585
|
+
if (resolvePromise) {
|
|
5586
|
+
const resolve = resolvePromise;
|
|
5587
|
+
resolvePromise = null;
|
|
5588
|
+
resolve();
|
|
5589
|
+
}
|
|
5590
|
+
};
|
|
5591
|
+
if (options.signal) {
|
|
5592
|
+
if (options.signal.aborted) {
|
|
5593
|
+
isRunning = false;
|
|
5594
|
+
} else {
|
|
5595
|
+
options.signal.addEventListener("abort", stop, { once: true });
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
const promise = new Promise((resolve) => {
|
|
5599
|
+
resolvePromise = resolve;
|
|
5600
|
+
if (!isRunning || options.signal?.aborted) {
|
|
5601
|
+
resolvePromise = null;
|
|
5602
|
+
resolve();
|
|
5603
|
+
return;
|
|
5604
|
+
}
|
|
5605
|
+
const loop = async () => {
|
|
5606
|
+
if (!isRunning || options.signal?.aborted) {
|
|
5607
|
+
stop();
|
|
5608
|
+
return;
|
|
5609
|
+
}
|
|
5610
|
+
try {
|
|
5611
|
+
const text = readClip().trim();
|
|
5612
|
+
if (text && text !== lastClipboardText) {
|
|
5613
|
+
lastClipboardText = text;
|
|
5614
|
+
const parsed = parseProfileInput(text);
|
|
5615
|
+
if (parsed.platform !== "unknown" && parsed.cleanUrl && !processedUrls.has(parsed.cleanUrl)) {
|
|
5616
|
+
processedUrls.add(parsed.cleanUrl);
|
|
5617
|
+
options.onDetected?.(parsed);
|
|
5618
|
+
try {
|
|
5619
|
+
const res = await dispatch({
|
|
5620
|
+
profile: parsed,
|
|
5621
|
+
choice,
|
|
5622
|
+
outputDir: options.outputDir,
|
|
5623
|
+
writeMetadata: options.writeMetadata,
|
|
5624
|
+
signal: options.signal,
|
|
5625
|
+
onProgress: (p) => {
|
|
5626
|
+
if (p.statusText) {
|
|
5627
|
+
options.onProgress?.(parsed, p.statusText);
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
});
|
|
5631
|
+
options.onComplete?.(parsed, res);
|
|
5632
|
+
} catch (err) {
|
|
5633
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
5634
|
+
options.onError?.(parsed, error);
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
} catch {
|
|
5639
|
+
}
|
|
5640
|
+
if (isRunning && !options.signal?.aborted) {
|
|
5641
|
+
timer = setTimeout(loop, intervalMs);
|
|
5642
|
+
} else {
|
|
5643
|
+
stop();
|
|
5644
|
+
}
|
|
5645
|
+
};
|
|
5646
|
+
loop();
|
|
5647
|
+
});
|
|
5648
|
+
return { stop, promise };
|
|
5649
|
+
}
|
|
5650
|
+
|
|
5651
|
+
// src/lib/batch.ts
|
|
5652
|
+
import fsPromises5 from "fs/promises";
|
|
5653
|
+
function parseBatchContent(content) {
|
|
5654
|
+
const lines = content.split(/\r?\n/);
|
|
5655
|
+
const result = [];
|
|
5656
|
+
for (const rawLine of lines) {
|
|
5657
|
+
let line = rawLine.trim();
|
|
5658
|
+
if (!line || line.startsWith("#") || line.startsWith("//")) {
|
|
5659
|
+
continue;
|
|
5660
|
+
}
|
|
5661
|
+
const hashIdx = line.indexOf(" #");
|
|
5662
|
+
if (hashIdx !== -1) {
|
|
5663
|
+
line = line.slice(0, hashIdx).trim();
|
|
5664
|
+
}
|
|
5665
|
+
const slashIdx = line.indexOf(" //");
|
|
5666
|
+
if (slashIdx !== -1) {
|
|
5667
|
+
line = line.slice(0, slashIdx).trim();
|
|
5668
|
+
}
|
|
5669
|
+
if (line) {
|
|
5670
|
+
result.push(line);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
return result;
|
|
5674
|
+
}
|
|
5675
|
+
async function loadBatchFile(filePath) {
|
|
5676
|
+
const content = await fsPromises5.readFile(filePath, "utf-8");
|
|
5677
|
+
return parseBatchContent(content);
|
|
5678
|
+
}
|
|
5679
|
+
async function runBatchQueue(options) {
|
|
5680
|
+
const rawEntries = options.lines ?? (options.filePath ? await loadBatchFile(options.filePath) : []);
|
|
5681
|
+
const choice = options.choice || "all";
|
|
5682
|
+
const minDelay = options.minDelayMs ?? 2e3;
|
|
5683
|
+
const maxDelay = options.maxDelayMs ?? 5e3;
|
|
5684
|
+
const items = rawEntries.map((raw) => ({
|
|
5685
|
+
raw,
|
|
5686
|
+
profile: parseProfileInput(raw),
|
|
5687
|
+
status: "pending",
|
|
5688
|
+
downloaded: 0,
|
|
5689
|
+
skipped: 0
|
|
5690
|
+
}));
|
|
5691
|
+
let totalDownloaded = 0;
|
|
5692
|
+
let totalSkipped = 0;
|
|
5693
|
+
let succeeded = 0;
|
|
5694
|
+
let failed = 0;
|
|
5695
|
+
for (let i = 0; i < items.length; i++) {
|
|
5696
|
+
if (options.signal?.aborted) {
|
|
5697
|
+
break;
|
|
5698
|
+
}
|
|
5699
|
+
const item = items[i];
|
|
5700
|
+
item.status = "running";
|
|
5701
|
+
if (item.profile.platform === "unknown" || !item.profile.cleanUrl) {
|
|
5702
|
+
item.status = "failed";
|
|
5703
|
+
item.error = `Invalid profile URL or unsupported username: ${item.raw}`;
|
|
5704
|
+
failed++;
|
|
5705
|
+
options.onAccountError?.(item, new Error(item.error));
|
|
5706
|
+
continue;
|
|
5707
|
+
}
|
|
5708
|
+
options.onAccountStart?.(item, i, items.length);
|
|
5709
|
+
try {
|
|
5710
|
+
const res = await dispatchDownload({
|
|
5711
|
+
profile: item.profile,
|
|
5712
|
+
choice,
|
|
5713
|
+
outputDir: options.outputDir,
|
|
5714
|
+
since: options.since,
|
|
5715
|
+
until: options.until,
|
|
5716
|
+
limit: options.limit,
|
|
5717
|
+
resume: options.resume,
|
|
5718
|
+
writeMetadata: options.writeMetadata,
|
|
5719
|
+
signal: options.signal,
|
|
5720
|
+
runnerOverrides: options.runnerOverrides,
|
|
5721
|
+
onProgress: (progress) => {
|
|
5722
|
+
options.onAccountProgress?.(item, progress);
|
|
5723
|
+
}
|
|
5724
|
+
});
|
|
5725
|
+
item.downloaded = res.downloadedCount;
|
|
5726
|
+
item.skipped = res.skippedCount;
|
|
5727
|
+
totalDownloaded += res.downloadedCount;
|
|
5728
|
+
totalSkipped += res.skippedCount;
|
|
5729
|
+
if (res.success) {
|
|
5730
|
+
item.status = "success";
|
|
5731
|
+
succeeded++;
|
|
5732
|
+
} else {
|
|
5733
|
+
item.status = "failed";
|
|
5734
|
+
item.error = res.errors.join("; ") || "Download failed";
|
|
5735
|
+
failed++;
|
|
5736
|
+
}
|
|
5737
|
+
options.onAccountComplete?.(item, i, items.length);
|
|
5738
|
+
} catch (err) {
|
|
5739
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5740
|
+
item.status = "failed";
|
|
5741
|
+
item.error = errMsg;
|
|
5742
|
+
failed++;
|
|
5743
|
+
options.onAccountError?.(item, err instanceof Error ? err : new Error(errMsg));
|
|
5744
|
+
}
|
|
5745
|
+
if (i < items.length - 1 && !options.signal?.aborted) {
|
|
5746
|
+
await sleep(minDelay, maxDelay, options.signal);
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
return {
|
|
5750
|
+
total: items.length,
|
|
5751
|
+
succeeded,
|
|
5752
|
+
failed,
|
|
5753
|
+
totalDownloaded,
|
|
5754
|
+
totalSkipped,
|
|
5755
|
+
items
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5089
5759
|
// src/cli.tsx
|
|
5090
5760
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
5091
5761
|
var resolveVersion = () => {
|
|
@@ -5116,6 +5786,8 @@ var HELP = `
|
|
|
5116
5786
|
-h, --help Show this help message
|
|
5117
5787
|
-v, --version Show version
|
|
5118
5788
|
-o, --output Set custom output directory
|
|
5789
|
+
-w, --watch Watch clipboard for profile links and auto-download
|
|
5790
|
+
-b, --batch <file> Download multiple profiles listed in a text file (one URL/user per line)
|
|
5119
5791
|
-U, --update Update bundled engines (yt-dlp and gallery-dl) to latest release
|
|
5120
5792
|
--update-ytdlp Update only bundled yt-dlp binary
|
|
5121
5793
|
--update-gallerydl Update only bundled gallery-dl binary
|
|
@@ -5125,6 +5797,11 @@ var HELP = `
|
|
|
5125
5797
|
--stories Download stories only (Instagram)
|
|
5126
5798
|
--highlights Download highlights only (Instagram)
|
|
5127
5799
|
--all Download all available media
|
|
5800
|
+
--since <date> Filter posts on or after date (YYYY-MM-DD or 7d, 30d)
|
|
5801
|
+
--until <date> Filter posts on or before date (YYYY-MM-DD)
|
|
5802
|
+
-l, --limit <n> Limit maximum items to download
|
|
5803
|
+
--resume Resume download from saved pagination checkpoint
|
|
5804
|
+
--write-metadata Save post metadata & captions to sidecar .json files
|
|
5128
5805
|
--completion Generate shell completion script (bash, zsh, fish, powershell)
|
|
5129
5806
|
|
|
5130
5807
|
Hotkeys in TUI:
|
|
@@ -5211,12 +5888,21 @@ async function main() {
|
|
|
5211
5888
|
let initialChoice;
|
|
5212
5889
|
let outputDir;
|
|
5213
5890
|
let themeMode = "auto";
|
|
5891
|
+
let isWatchMode = false;
|
|
5892
|
+
let batchFile;
|
|
5893
|
+
let since;
|
|
5894
|
+
let until;
|
|
5895
|
+
let limit;
|
|
5896
|
+
let isResume = false;
|
|
5897
|
+
let writeMetadata = false;
|
|
5214
5898
|
for (let i = 0; i < args.length; i++) {
|
|
5215
5899
|
const arg = args[i];
|
|
5216
5900
|
if (arg === "-o" || arg === "--output") {
|
|
5217
5901
|
outputDir = args[++i];
|
|
5218
5902
|
} else if (arg.startsWith("--output=")) {
|
|
5219
5903
|
outputDir = arg.slice("--output=".length);
|
|
5904
|
+
} else if (arg === "-w" || arg === "--watch") {
|
|
5905
|
+
isWatchMode = true;
|
|
5220
5906
|
} else if (arg === "--theme") {
|
|
5221
5907
|
themeMode = args[++i];
|
|
5222
5908
|
} else if (arg.startsWith("--theme=")) {
|
|
@@ -5231,10 +5917,125 @@ async function main() {
|
|
|
5231
5917
|
initialChoice = "highlights";
|
|
5232
5918
|
} else if (arg === "--all") {
|
|
5233
5919
|
initialChoice = "all";
|
|
5920
|
+
} else if (arg === "--since") {
|
|
5921
|
+
since = args[++i];
|
|
5922
|
+
} else if (arg.startsWith("--since=")) {
|
|
5923
|
+
since = arg.slice("--since=".length);
|
|
5924
|
+
} else if (arg === "--until") {
|
|
5925
|
+
until = args[++i];
|
|
5926
|
+
} else if (arg.startsWith("--until=")) {
|
|
5927
|
+
until = arg.slice("--until=".length);
|
|
5928
|
+
} else if (arg === "-l" || arg === "--limit") {
|
|
5929
|
+
const parsed = parseInt(args[++i], 10);
|
|
5930
|
+
if (!isNaN(parsed) && parsed > 0) limit = parsed;
|
|
5931
|
+
} else if (arg.startsWith("--limit=")) {
|
|
5932
|
+
const parsed = parseInt(arg.slice("--limit=".length), 10);
|
|
5933
|
+
if (!isNaN(parsed) && parsed > 0) limit = parsed;
|
|
5934
|
+
} else if (arg === "--resume") {
|
|
5935
|
+
isResume = true;
|
|
5936
|
+
} else if (arg === "--write-metadata") {
|
|
5937
|
+
writeMetadata = true;
|
|
5938
|
+
} else if (arg === "-b" || arg === "--batch") {
|
|
5939
|
+
batchFile = args[++i];
|
|
5940
|
+
} else if (arg.startsWith("--batch=")) {
|
|
5941
|
+
batchFile = arg.slice("--batch=".length);
|
|
5234
5942
|
} else if (!arg.startsWith("-") && !initialInput) {
|
|
5235
5943
|
initialInput = arg;
|
|
5236
5944
|
}
|
|
5237
5945
|
}
|
|
5946
|
+
if (isWatchMode) {
|
|
5947
|
+
const choice = initialChoice || "all";
|
|
5948
|
+
console.log(`
|
|
5949
|
+
\x1B[36mOpen Nami\x1B[0m \u2014 Clipboard Watcher Active (v${VERSION})`);
|
|
5950
|
+
console.log(` \x1B[90mTarget media:\x1B[0m \x1B[33m${choice}\x1B[0m`);
|
|
5951
|
+
if (outputDir) console.log(` \x1B[90mOutput directory:\x1B[0m ${outputDir}`);
|
|
5952
|
+
console.log(` \x1B[90mMonitoring clipboard for Instagram, TikTok, Facebook & X profiles\u2026\x1B[0m`);
|
|
5953
|
+
console.log(` \x1B[90mPress Ctrl+C to stop.\x1B[0m
|
|
5954
|
+
`);
|
|
5955
|
+
const controller = new AbortController();
|
|
5956
|
+
process.on("SIGINT", () => {
|
|
5957
|
+
console.log("\n[open-nami] Stopping clipboard watcher\u2026");
|
|
5958
|
+
controller.abort();
|
|
5959
|
+
process.exit(0);
|
|
5960
|
+
});
|
|
5961
|
+
const { promise } = startClipboardWatcher({
|
|
5962
|
+
choice,
|
|
5963
|
+
outputDir,
|
|
5964
|
+
writeMetadata,
|
|
5965
|
+
signal: controller.signal,
|
|
5966
|
+
onDetected: (p) => {
|
|
5967
|
+
console.log(`
|
|
5968
|
+
\u276F \x1B[32mDetected:\x1B[0m @${p.username} on \x1B[35m${p.platform}\x1B[0m`);
|
|
5969
|
+
console.log(` \x1B[90mDownloading ${choice}\u2026\x1B[0m`);
|
|
5970
|
+
},
|
|
5971
|
+
onProgress: (_p, statusText) => {
|
|
5972
|
+
process.stdout.write(`\r \x1B[90m\u2192 ${statusText.slice(0, 70)}\x1B[0m\x1B[K`);
|
|
5973
|
+
},
|
|
5974
|
+
onComplete: (p, res) => {
|
|
5975
|
+
process.stdout.write("\r\x1B[K");
|
|
5976
|
+
console.log(`\u2713 \x1B[32mFinished @${p.username}:\x1B[0m ${res.downloadedCount} downloaded, ${res.skippedCount} skipped`);
|
|
5977
|
+
if (res.outputDir) console.log(` \x1B[90mSaved to:\x1B[0m ${res.outputDir}`);
|
|
5978
|
+
},
|
|
5979
|
+
onError: (p, err) => {
|
|
5980
|
+
process.stdout.write("\r\x1B[K");
|
|
5981
|
+
console.error(`\u2717 \x1B[31mError downloading @${p.username}:\x1B[0m ${err.message}`);
|
|
5982
|
+
}
|
|
5983
|
+
});
|
|
5984
|
+
await promise;
|
|
5985
|
+
process.exit(0);
|
|
5986
|
+
}
|
|
5987
|
+
if (batchFile) {
|
|
5988
|
+
const choice = initialChoice || "all";
|
|
5989
|
+
console.log(`
|
|
5990
|
+
\x1B[36mOpen Nami\x1B[0m \u2014 Batch Download Queue (v${VERSION})`);
|
|
5991
|
+
console.log(` \x1B[90mBatch file:\x1B[0m ${batchFile}`);
|
|
5992
|
+
console.log(` \x1B[90mTarget media:\x1B[0m \x1B[33m${choice}\x1B[0m`);
|
|
5993
|
+
if (outputDir) console.log(` \x1B[90mOutput directory:\x1B[0m ${outputDir}`);
|
|
5994
|
+
if (limit) console.log(` \x1B[90mPer-account limit:\x1B[0m ${limit}`);
|
|
5995
|
+
console.log();
|
|
5996
|
+
const controller = new AbortController();
|
|
5997
|
+
process.on("SIGINT", () => {
|
|
5998
|
+
console.log("\n[open-nami] Aborting batch queue\u2026");
|
|
5999
|
+
controller.abort();
|
|
6000
|
+
});
|
|
6001
|
+
try {
|
|
6002
|
+
const result = await runBatchQueue({
|
|
6003
|
+
filePath: batchFile,
|
|
6004
|
+
choice,
|
|
6005
|
+
outputDir,
|
|
6006
|
+
since,
|
|
6007
|
+
until,
|
|
6008
|
+
limit,
|
|
6009
|
+
resume: isResume,
|
|
6010
|
+
writeMetadata,
|
|
6011
|
+
signal: controller.signal,
|
|
6012
|
+
onAccountStart: (item, idx, total) => {
|
|
6013
|
+
console.log(`\x1B[36m[${idx + 1}/${total}]\x1B[0m \x1B[1m@${item.profile.username}\x1B[0m on \x1B[35m${item.profile.platform}\x1B[0m\u2026`);
|
|
6014
|
+
},
|
|
6015
|
+
onAccountProgress: (_item, progress) => {
|
|
6016
|
+
if (progress.currentFile) {
|
|
6017
|
+
process.stdout.write(`\r \x1B[90m\u2192 [${progress.job}] ${progress.currentFile.slice(0, 60)}\x1B[0m\x1B[K`);
|
|
6018
|
+
}
|
|
6019
|
+
},
|
|
6020
|
+
onAccountComplete: (item) => {
|
|
6021
|
+
process.stdout.write("\r\x1B[K");
|
|
6022
|
+
console.log(` \x1B[32m\u2713 @${item.profile.username}:\x1B[0m ${item.downloaded} downloaded, ${item.skipped} skipped
|
|
6023
|
+
`);
|
|
6024
|
+
},
|
|
6025
|
+
onAccountError: (item, err) => {
|
|
6026
|
+
process.stdout.write("\r\x1B[K");
|
|
6027
|
+
console.error(` \x1B[31m\u2717 @${item.profile.username || item.raw}:\x1B[0m ${err.message}
|
|
6028
|
+
`);
|
|
6029
|
+
}
|
|
6030
|
+
});
|
|
6031
|
+
console.log(`\x1B[1mBatch completed:\x1B[0m ${result.succeeded} succeeded, ${result.failed} failed of ${result.total} total accounts.`);
|
|
6032
|
+
console.log(`\x1B[32mTotal media downloaded:\x1B[0m ${result.totalDownloaded} items (${result.totalSkipped} skipped).`);
|
|
6033
|
+
process.exit(result.failed > 0 ? 1 : 0);
|
|
6034
|
+
} catch (err) {
|
|
6035
|
+
console.error(`open-nami batch error: ${err.message || String(err)}`);
|
|
6036
|
+
process.exit(1);
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
5238
6039
|
const isTTY = Boolean(process.stdout.isTTY);
|
|
5239
6040
|
if (!isTTY && initialChoice && initialInput) {
|
|
5240
6041
|
const parsed = parseProfileInput(initialInput);
|
|
@@ -5248,6 +6049,11 @@ async function main() {
|
|
|
5248
6049
|
profile: parsed,
|
|
5249
6050
|
choice: initialChoice,
|
|
5250
6051
|
outputDir,
|
|
6052
|
+
since,
|
|
6053
|
+
until,
|
|
6054
|
+
limit,
|
|
6055
|
+
resume: isResume,
|
|
6056
|
+
writeMetadata,
|
|
5251
6057
|
onProgress: (progress) => {
|
|
5252
6058
|
if (progress.currentFile) {
|
|
5253
6059
|
console.log(`[open-nami] [${progress.job}] ${progress.currentFile}`);
|
|
@@ -5281,6 +6087,11 @@ async function main() {
|
|
|
5281
6087
|
initialChoice,
|
|
5282
6088
|
initialThemeMode: themeMode,
|
|
5283
6089
|
outputDir,
|
|
6090
|
+
since,
|
|
6091
|
+
until,
|
|
6092
|
+
limit,
|
|
6093
|
+
resume: isResume,
|
|
6094
|
+
writeMetadata,
|
|
5284
6095
|
version: VERSION
|
|
5285
6096
|
}
|
|
5286
6097
|
),
|