open-nami 1.0.3 → 1.0.5
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 +1058 -210
- 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.0.
|
|
12
|
+
version: "1.0.5",
|
|
13
13
|
description: "Open Nami \u2014 terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
|
|
14
14
|
type: "module",
|
|
15
15
|
bin: {
|
|
@@ -1387,14 +1387,31 @@ function parseProfileInput(input) {
|
|
|
1387
1387
|
}
|
|
1388
1388
|
}
|
|
1389
1389
|
if (host.includes("x.com") || host.includes("twitter.com")) {
|
|
1390
|
+
if (pathParts[0]?.toLowerCase() === "i" && pathParts[1]?.toLowerCase() === "history" && pathParts[2]?.toLowerCase() === "likes") {
|
|
1391
|
+
return {
|
|
1392
|
+
platform: "x",
|
|
1393
|
+
username: "i_history_likes",
|
|
1394
|
+
cleanUrl: "https://x.com/i/history/likes",
|
|
1395
|
+
rawInput: input
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
if (pathParts[0]?.toLowerCase() === "i" && pathParts[1]?.toLowerCase() === "bookmarks") {
|
|
1399
|
+
return {
|
|
1400
|
+
platform: "x",
|
|
1401
|
+
username: "i_bookmarks",
|
|
1402
|
+
cleanUrl: "https://x.com/i/bookmarks",
|
|
1403
|
+
rawInput: input
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1390
1406
|
const firstPart = pathParts[0] || "";
|
|
1391
1407
|
const reserved = ["home", "explore", "notifications", "messages", "i", "settings"];
|
|
1392
1408
|
if (firstPart && !reserved.includes(firstPart.toLowerCase())) {
|
|
1393
1409
|
const username = firstPart.replace(/^@/, "");
|
|
1410
|
+
const isLikesUrl = pathParts[1]?.toLowerCase() === "likes";
|
|
1394
1411
|
return {
|
|
1395
1412
|
platform: "x",
|
|
1396
1413
|
username: sanitizeUsername(username),
|
|
1397
|
-
cleanUrl: `https://x.com/${username}`,
|
|
1414
|
+
cleanUrl: isLikesUrl ? `https://x.com/${username}/likes` : `https://x.com/${username}`,
|
|
1398
1415
|
rawInput: input
|
|
1399
1416
|
};
|
|
1400
1417
|
}
|
|
@@ -1559,6 +1576,9 @@ function resolvePlatformDownloadsDir(options = {}) {
|
|
|
1559
1576
|
return defaultFallback;
|
|
1560
1577
|
}
|
|
1561
1578
|
|
|
1579
|
+
// src/lib/engines/extractors/instagram.ts
|
|
1580
|
+
import crypto from "crypto";
|
|
1581
|
+
|
|
1562
1582
|
// src/lib/engines/extractors/http.ts
|
|
1563
1583
|
import fs5 from "fs/promises";
|
|
1564
1584
|
import { setTimeout as setTimeoutPromise } from "timers/promises";
|
|
@@ -1627,6 +1647,11 @@ async function sleep(minMs, maxMs, signal) {
|
|
|
1627
1647
|
throw err;
|
|
1628
1648
|
}
|
|
1629
1649
|
}
|
|
1650
|
+
function combineTimeoutSignal(signal, timeoutMs) {
|
|
1651
|
+
if (!timeoutMs || timeoutMs <= 0) return signal;
|
|
1652
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1653
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
1654
|
+
}
|
|
1630
1655
|
var HttpClient = class {
|
|
1631
1656
|
defaultCookies = {};
|
|
1632
1657
|
defaultHeaders;
|
|
@@ -1661,8 +1686,8 @@ var HttpClient = class {
|
|
|
1661
1686
|
Object.assign(this.defaultCookies, loaded);
|
|
1662
1687
|
}
|
|
1663
1688
|
async request(url, options = {}) {
|
|
1664
|
-
const retries = options.retries ??
|
|
1665
|
-
const retryDelay = options.retryDelayMs ??
|
|
1689
|
+
const retries = options.retries ?? 1;
|
|
1690
|
+
const retryDelay = options.retryDelayMs ?? 1e3;
|
|
1666
1691
|
let cookieHeader = "";
|
|
1667
1692
|
if (options.cookiePath) {
|
|
1668
1693
|
const fromPath = await this.getCachedCookies(options.cookiePath, options.cookieDomain);
|
|
@@ -1688,9 +1713,11 @@ var HttpClient = class {
|
|
|
1688
1713
|
if (options.signal?.aborted) {
|
|
1689
1714
|
throw options.signal.reason ?? new Error("Aborted");
|
|
1690
1715
|
}
|
|
1716
|
+
const requestSignal = combineTimeoutSignal(options.signal || void 0, options.timeoutMs);
|
|
1691
1717
|
try {
|
|
1692
1718
|
const response = await fetch(url, {
|
|
1693
1719
|
...options,
|
|
1720
|
+
signal: requestSignal,
|
|
1694
1721
|
headers
|
|
1695
1722
|
});
|
|
1696
1723
|
if (response.status === 429) {
|
|
@@ -1699,7 +1726,8 @@ var HttpClient = class {
|
|
|
1699
1726
|
await response.body?.cancel();
|
|
1700
1727
|
} catch {
|
|
1701
1728
|
}
|
|
1702
|
-
|
|
1729
|
+
const backoffMs = Math.min(retryDelay * (attempt + 1), 2500);
|
|
1730
|
+
await sleep(backoffMs, void 0, options.signal || void 0);
|
|
1703
1731
|
continue;
|
|
1704
1732
|
}
|
|
1705
1733
|
}
|
|
@@ -1708,7 +1736,8 @@ var HttpClient = class {
|
|
|
1708
1736
|
await response.body?.cancel();
|
|
1709
1737
|
} catch {
|
|
1710
1738
|
}
|
|
1711
|
-
|
|
1739
|
+
const backoffMs = Math.min(retryDelay * (attempt + 1), 2e3);
|
|
1740
|
+
await sleep(backoffMs, void 0, options.signal || void 0);
|
|
1712
1741
|
continue;
|
|
1713
1742
|
}
|
|
1714
1743
|
return response;
|
|
@@ -1731,13 +1760,23 @@ var HttpClient = class {
|
|
|
1731
1760
|
}
|
|
1732
1761
|
});
|
|
1733
1762
|
if (!res.ok) {
|
|
1763
|
+
try {
|
|
1764
|
+
await res.body?.cancel();
|
|
1765
|
+
} catch {
|
|
1766
|
+
}
|
|
1734
1767
|
throw new Error(`HTTP ${res.status} (${res.statusText}) for ${url}`);
|
|
1735
1768
|
}
|
|
1736
|
-
|
|
1769
|
+
const text = await res.text();
|
|
1770
|
+
const cleanText = text.replace(/^\s*(?:for\s*\(\s*;\s*;\s*\);|\)\]\}',)/, "").trim();
|
|
1771
|
+
return JSON.parse(cleanText);
|
|
1737
1772
|
}
|
|
1738
1773
|
async fetchText(url, options = {}) {
|
|
1739
1774
|
const res = await this.request(url, options);
|
|
1740
1775
|
if (!res.ok) {
|
|
1776
|
+
try {
|
|
1777
|
+
await res.body?.cancel();
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1741
1780
|
throw new Error(`HTTP ${res.status} (${res.statusText}) for ${url}`);
|
|
1742
1781
|
}
|
|
1743
1782
|
return await res.text();
|
|
@@ -1747,7 +1786,22 @@ var HttpClient = class {
|
|
|
1747
1786
|
// src/lib/engines/extractors/instagram.ts
|
|
1748
1787
|
var IG_APP_ID = "936619743392459";
|
|
1749
1788
|
var IG_TIMELINE_QUERY_HASH = "69cba40317214236af40e7efa697781d";
|
|
1750
|
-
var
|
|
1789
|
+
var IG_QUERIES = {
|
|
1790
|
+
PolarisProfilePostsTabContentQuery_connection: "39229011733413759",
|
|
1791
|
+
PolarisProfileReelsTabContentQuery_connection: "28989741540630190",
|
|
1792
|
+
PolarisProfileTaggedTabContentQuery_connection: "28794602806842110",
|
|
1793
|
+
PolarisProfileStoryHighlightsTrayContentQuery: "26970053832668570",
|
|
1794
|
+
PolarisStoriesV3ReelPageGalleryQuery: "28262315486766731",
|
|
1795
|
+
PolarisStoriesV3TrayContainerQuery: "27703822975903310",
|
|
1796
|
+
PolarisProfilePageContentQuery: "28649589871350527"
|
|
1797
|
+
};
|
|
1798
|
+
var IG_POLARIS_DOC_ID = IG_QUERIES.PolarisProfilePostsTabContentQuery_connection;
|
|
1799
|
+
function logExtractorDebug(endpoint, err) {
|
|
1800
|
+
if (process.env.DEBUG || process.env.OPEN_NAMI_DEBUG) {
|
|
1801
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1802
|
+
console.debug(`[open-nami:instagram] ${endpoint} query fallback: ${message}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1751
1805
|
function parseIgNodeToMediaItems(node, username) {
|
|
1752
1806
|
const items = [];
|
|
1753
1807
|
const anyNode = node;
|
|
@@ -1783,6 +1837,21 @@ function parseIgNodeToMediaItems(node, username) {
|
|
|
1783
1837
|
height: video?.height ?? node.dimensions?.height,
|
|
1784
1838
|
thumbnailUrl: node.display_url
|
|
1785
1839
|
});
|
|
1840
|
+
} else {
|
|
1841
|
+
const candidate = node.image_versions2?.candidates?.[0];
|
|
1842
|
+
const photoUrl = candidate?.url || node.display_url;
|
|
1843
|
+
if (photoUrl) {
|
|
1844
|
+
items.push({
|
|
1845
|
+
id: baseId,
|
|
1846
|
+
url: photoUrl,
|
|
1847
|
+
filename: `${username}_${date || baseId}`,
|
|
1848
|
+
extension: "jpg",
|
|
1849
|
+
type: "photo",
|
|
1850
|
+
date,
|
|
1851
|
+
width: candidate?.width ?? node.dimensions?.width,
|
|
1852
|
+
height: candidate?.height ?? node.dimensions?.height
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1786
1855
|
}
|
|
1787
1856
|
} else {
|
|
1788
1857
|
const candidate = node.image_versions2?.candidates?.[0];
|
|
@@ -1852,6 +1921,8 @@ var InstagramExtractor = class {
|
|
|
1852
1921
|
cookiePath: options.cookiePath,
|
|
1853
1922
|
cookieDomain: "instagram.com",
|
|
1854
1923
|
signal: options.signal,
|
|
1924
|
+
retries: 0,
|
|
1925
|
+
timeoutMs: 4e3,
|
|
1855
1926
|
headers: {
|
|
1856
1927
|
"X-IG-App-ID": IG_APP_ID,
|
|
1857
1928
|
"Referer": `https://www.instagram.com/${username}/`
|
|
@@ -1860,9 +1931,47 @@ var InstagramExtractor = class {
|
|
|
1860
1931
|
if (!res.data?.user) {
|
|
1861
1932
|
throw new Error(`Instagram user not found or private profile: ${username}`);
|
|
1862
1933
|
}
|
|
1863
|
-
|
|
1934
|
+
const user = res.data.user;
|
|
1935
|
+
if (user.is_private) {
|
|
1936
|
+
const fs13 = user.friendship_status;
|
|
1937
|
+
const isFollowing = fs13 ? Boolean(fs13.following) : Boolean(user.followed_by_viewer);
|
|
1938
|
+
if (!isFollowing) {
|
|
1939
|
+
throw new Error(`Instagram profile @${username} is private. You must provide a session cookie jar of an account following this profile.`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
return user;
|
|
1864
1943
|
}
|
|
1865
1944
|
async fetchHighlightsTray(userId, options) {
|
|
1945
|
+
try {
|
|
1946
|
+
const variables = JSON.stringify({ user_id: userId });
|
|
1947
|
+
const body = new URLSearchParams({
|
|
1948
|
+
doc_id: IG_QUERIES.PolarisProfileStoryHighlightsTrayContentQuery,
|
|
1949
|
+
variables
|
|
1950
|
+
}).toString();
|
|
1951
|
+
const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
|
|
1952
|
+
method: "POST",
|
|
1953
|
+
body,
|
|
1954
|
+
cookiePath: options.cookiePath,
|
|
1955
|
+
cookieDomain: "instagram.com",
|
|
1956
|
+
signal: options.signal,
|
|
1957
|
+
headers: {
|
|
1958
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1959
|
+
"X-IG-App-ID": IG_APP_ID,
|
|
1960
|
+
"X-FB-Friendly-Name": "PolarisProfileStoryHighlightsTrayContentQuery",
|
|
1961
|
+
"Referer": "https://www.instagram.com/"
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
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
|
+
const edges = res?.data?.highlights?.edges;
|
|
1969
|
+
if (Array.isArray(edges) && edges.length > 0) {
|
|
1970
|
+
return edges.map((e) => e.node);
|
|
1971
|
+
}
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery", err);
|
|
1974
|
+
}
|
|
1866
1975
|
const url = `https://www.instagram.com/api/v1/highlights/${userId}/highlights_tray/`;
|
|
1867
1976
|
try {
|
|
1868
1977
|
const res = await this.httpClient.fetchJson(url, {
|
|
@@ -1874,7 +1983,8 @@ var InstagramExtractor = class {
|
|
|
1874
1983
|
}
|
|
1875
1984
|
});
|
|
1876
1985
|
return res.tray ?? [];
|
|
1877
|
-
} catch {
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
logExtractorDebug("Highlights legacy REST", err);
|
|
1878
1988
|
return [];
|
|
1879
1989
|
}
|
|
1880
1990
|
}
|
|
@@ -1898,40 +2008,226 @@ var InstagramExtractor = class {
|
|
|
1898
2008
|
return [];
|
|
1899
2009
|
}
|
|
1900
2010
|
}
|
|
1901
|
-
async
|
|
1902
|
-
|
|
1903
|
-
|
|
2011
|
+
async fetchWebTokens(username, options) {
|
|
2012
|
+
let csrfToken;
|
|
2013
|
+
if (options.cookiePath) {
|
|
2014
|
+
try {
|
|
2015
|
+
const cookies = await loadCookiesFromPath(options.cookiePath, "instagram.com");
|
|
2016
|
+
csrfToken = cookies["csrftoken"];
|
|
2017
|
+
} catch {
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (!csrfToken) {
|
|
2021
|
+
csrfToken = crypto.randomBytes(16).toString("hex");
|
|
2022
|
+
}
|
|
1904
2023
|
try {
|
|
1905
|
-
const
|
|
2024
|
+
const html = await this.httpClient.fetchText(`https://www.instagram.com/${encodeURIComponent(username)}/`, {
|
|
1906
2025
|
cookiePath: options.cookiePath,
|
|
1907
2026
|
cookieDomain: "instagram.com",
|
|
1908
2027
|
signal: options.signal,
|
|
1909
2028
|
headers: {
|
|
1910
|
-
"
|
|
1911
|
-
"Referer": `https://www.instagram.com/${username}/`
|
|
2029
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
1912
2030
|
}
|
|
1913
2031
|
});
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2032
|
+
let lsd;
|
|
2033
|
+
let fbDtsg;
|
|
2034
|
+
let userId;
|
|
2035
|
+
const eqmcMatch = html.match(/id="__eqmc"[^>]*>([^<]+)<\/script>/);
|
|
2036
|
+
if (eqmcMatch) {
|
|
2037
|
+
try {
|
|
2038
|
+
const eqmc = JSON.parse(eqmcMatch[1]);
|
|
2039
|
+
if (eqmc.l) lsd = eqmc.l;
|
|
2040
|
+
if (eqmc.f) fbDtsg = eqmc.f;
|
|
2041
|
+
} catch {
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (!lsd) {
|
|
2045
|
+
const m = html.match(/"lsd":"([^"]+)"/) || html.match(/"LSD",\[\],\{"token":"([^"]+)"/);
|
|
2046
|
+
if (m) lsd = m[1];
|
|
2047
|
+
}
|
|
2048
|
+
if (!fbDtsg) {
|
|
2049
|
+
const m = html.match(/"dtsg":\{"token":"([^"]+)"/);
|
|
2050
|
+
if (m) fbDtsg = m[1];
|
|
2051
|
+
}
|
|
2052
|
+
const uMatch = html.match(/"profile_id":"([^"]+)"/) || html.match(/"user_id":"([^"]+)"/);
|
|
2053
|
+
if (uMatch) userId = uMatch[1];
|
|
2054
|
+
return { lsd, fbDtsg, csrfToken, userId };
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
logExtractorDebug("fetchWebTokens", err);
|
|
2057
|
+
return { csrfToken };
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
async fetchPostsTabGraphQL(username, cursor, tokens, options) {
|
|
2061
|
+
const postVariables = JSON.stringify({
|
|
2062
|
+
after: cursor || null,
|
|
2063
|
+
before: null,
|
|
2064
|
+
data: {
|
|
2065
|
+
count: 12,
|
|
2066
|
+
include_reel_media_seen_timestamp: true,
|
|
2067
|
+
include_relationship_info: true,
|
|
2068
|
+
latest_besties_reel_media: true,
|
|
2069
|
+
latest_reel_media: true
|
|
2070
|
+
},
|
|
2071
|
+
first: 12,
|
|
2072
|
+
include_multi_captions: true,
|
|
2073
|
+
last: null,
|
|
2074
|
+
username,
|
|
2075
|
+
__relay_internal__pv__PolarisMultiCaptionCarouselEnabledrelayprovider: true,
|
|
2076
|
+
__relay_internal__pv__PolarisShortDramaEnabledrelayprovider: true,
|
|
2077
|
+
__relay_internal__pv__PolarisThreadsStyleCommentsEnabledrelayprovider: false,
|
|
2078
|
+
__relay_internal__pv__PolarisReelsRecoDebugOverlayEnabledrelayprovider: false
|
|
2079
|
+
});
|
|
2080
|
+
const bodyParams = new URLSearchParams({
|
|
2081
|
+
av: "17841415137994167",
|
|
2082
|
+
__d: "www",
|
|
2083
|
+
__user: "0",
|
|
2084
|
+
__a: "1",
|
|
2085
|
+
__req: "0",
|
|
2086
|
+
__hs: "20731.HYP:instagram_web_pkg.2.1...0",
|
|
2087
|
+
dpr: "1",
|
|
2088
|
+
__ccg: "EXCELLENT",
|
|
2089
|
+
__rev: "1049285836",
|
|
2090
|
+
__hsi: "7693193318680449885",
|
|
2091
|
+
__comet_req: "7",
|
|
2092
|
+
jazoest: "26461",
|
|
2093
|
+
fb_api_caller_class: "RelayModern",
|
|
2094
|
+
fb_api_req_friendly_name: "PolarisProfilePostsTabContentQuery_connection",
|
|
2095
|
+
server_timestamps: "true",
|
|
2096
|
+
variables: postVariables,
|
|
2097
|
+
doc_id: IG_POLARIS_DOC_ID
|
|
2098
|
+
});
|
|
2099
|
+
if (tokens.fbDtsg) bodyParams.set("fb_dtsg", tokens.fbDtsg);
|
|
2100
|
+
if (tokens.lsd) bodyParams.set("lsd", tokens.lsd);
|
|
2101
|
+
const headers = {
|
|
2102
|
+
"Accept": "*/*",
|
|
2103
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2104
|
+
"X-IG-App-ID": IG_APP_ID,
|
|
2105
|
+
"X-IG-Max-Touch-Points": "0",
|
|
2106
|
+
"X-FB-Friendly-Name": "PolarisProfilePostsTabContentQuery_connection",
|
|
2107
|
+
"Referer": `https://www.instagram.com/${username}/`,
|
|
2108
|
+
"Origin": "https://www.instagram.com",
|
|
2109
|
+
"X-ASBD-ID": "359341",
|
|
2110
|
+
"Sec-Fetch-Dest": "empty",
|
|
2111
|
+
"Sec-Fetch-Mode": "cors",
|
|
2112
|
+
"Sec-Fetch-Site": "same-origin"
|
|
2113
|
+
};
|
|
2114
|
+
if (tokens.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
|
|
2115
|
+
if (tokens.lsd) headers["X-FB-LSD"] = tokens.lsd;
|
|
2116
|
+
const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
|
|
2117
|
+
method: "POST",
|
|
2118
|
+
body: bodyParams.toString(),
|
|
2119
|
+
cookiePath: options.cookiePath,
|
|
2120
|
+
cookieDomain: "instagram.com",
|
|
2121
|
+
signal: options.signal,
|
|
2122
|
+
headers
|
|
2123
|
+
});
|
|
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
|
+
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)) {
|
|
2130
|
+
let resolvedUserId;
|
|
2131
|
+
for (const edge of connection.edges) {
|
|
2132
|
+
const node = edge?.node;
|
|
2133
|
+
if (node?.owner_id) {
|
|
2134
|
+
resolvedUserId = String(node.owner_id);
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
if (node?.user?.pk || node?.user?.id) {
|
|
2138
|
+
resolvedUserId = String(node.user.pk || node.user.id);
|
|
2139
|
+
break;
|
|
2140
|
+
}
|
|
2141
|
+
if (typeof node?.id === "string" && node.id.includes("_")) {
|
|
2142
|
+
resolvedUserId = node.id.split("_")[1];
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
edges: connection.edges,
|
|
2148
|
+
hasNextPage: Boolean(connection.page_info?.has_next_page),
|
|
2149
|
+
nextCursor: connection.page_info?.end_cursor,
|
|
2150
|
+
userId: resolvedUserId
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
return { edges: [], hasNextPage: false };
|
|
2154
|
+
}
|
|
2155
|
+
async fetchNextPostsPage(userId, username, cursor, options, tokens) {
|
|
2156
|
+
if (userId) {
|
|
2157
|
+
const variables = JSON.stringify({ id: userId, first: 12, after: cursor });
|
|
2158
|
+
const url = `https://www.instagram.com/graphql/query/?query_hash=${IG_TIMELINE_QUERY_HASH}&variables=${encodeURIComponent(variables)}`;
|
|
2159
|
+
try {
|
|
2160
|
+
const res = await this.httpClient.fetchJson(url, {
|
|
2161
|
+
cookiePath: options.cookiePath,
|
|
2162
|
+
cookieDomain: "instagram.com",
|
|
2163
|
+
signal: options.signal,
|
|
2164
|
+
headers: {
|
|
2165
|
+
"X-IG-App-ID": IG_APP_ID,
|
|
2166
|
+
"Referer": `https://www.instagram.com/${username}/`
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
const data = res?.data?.user?.edge_owner_to_timeline_media;
|
|
2170
|
+
if (data?.edges && data.edges.length > 0) {
|
|
2171
|
+
return {
|
|
2172
|
+
edges: data.edges,
|
|
2173
|
+
hasNextPage: Boolean(data.page_info?.has_next_page),
|
|
2174
|
+
nextCursor: data.page_info?.end_cursor
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
} catch (err) {
|
|
2178
|
+
logExtractorDebug("edge_owner_to_timeline_media", err);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
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
|
+
if (userId) {
|
|
2190
|
+
try {
|
|
2191
|
+
const restUrl = `https://www.instagram.com/api/v1/feed/user/${userId}/?count=12&max_id=${encodeURIComponent(cursor)}`;
|
|
2192
|
+
const res = await this.httpClient.fetchJson(restUrl, {
|
|
2193
|
+
cookiePath: options.cookiePath,
|
|
2194
|
+
cookieDomain: "instagram.com",
|
|
2195
|
+
signal: options.signal,
|
|
2196
|
+
headers: {
|
|
2197
|
+
"X-IG-App-ID": IG_APP_ID,
|
|
2198
|
+
"Referer": `https://www.instagram.com/${username}/`
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
if (Array.isArray(res?.items) && res.items.length > 0) {
|
|
2202
|
+
const edges = res.items.map((item) => ({ node: item }));
|
|
2203
|
+
return {
|
|
2204
|
+
edges,
|
|
2205
|
+
hasNextPage: Boolean(res.more_available),
|
|
2206
|
+
nextCursor: res.next_max_id ? String(res.next_max_id) : void 0
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
logExtractorDebug("v1 user feed REST", err);
|
|
1921
2211
|
}
|
|
1922
|
-
} catch {
|
|
1923
2212
|
}
|
|
2213
|
+
return { edges: [], hasNextPage: false };
|
|
2214
|
+
}
|
|
2215
|
+
async fetchNextReelsPage(userId, cursor, options) {
|
|
1924
2216
|
try {
|
|
1925
|
-
const
|
|
2217
|
+
const variables = JSON.stringify({
|
|
1926
2218
|
after: cursor,
|
|
1927
2219
|
first: 12,
|
|
1928
|
-
|
|
1929
|
-
data: {
|
|
2220
|
+
id: userId,
|
|
2221
|
+
data: {
|
|
2222
|
+
include_feed_video: true,
|
|
2223
|
+
page_size: 12,
|
|
2224
|
+
target_user_id: userId
|
|
2225
|
+
}
|
|
1930
2226
|
});
|
|
1931
2227
|
const postUrl = "https://www.instagram.com/graphql/query";
|
|
1932
2228
|
const body = new URLSearchParams({
|
|
1933
|
-
doc_id:
|
|
1934
|
-
variables
|
|
2229
|
+
doc_id: IG_QUERIES.PolarisProfileReelsTabContentQuery_connection,
|
|
2230
|
+
variables
|
|
1935
2231
|
}).toString();
|
|
1936
2232
|
const res = await this.httpClient.fetchJson(postUrl, {
|
|
1937
2233
|
method: "POST",
|
|
@@ -1942,40 +2238,33 @@ var InstagramExtractor = class {
|
|
|
1942
2238
|
headers: {
|
|
1943
2239
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
1944
2240
|
"X-IG-App-ID": IG_APP_ID,
|
|
1945
|
-
"X-FB-Friendly-Name": "
|
|
1946
|
-
"Referer":
|
|
2241
|
+
"X-FB-Friendly-Name": "PolarisProfileReelsTabContentQuery_connection",
|
|
2242
|
+
"Referer": "https://www.instagram.com/"
|
|
1947
2243
|
}
|
|
1948
2244
|
});
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
+
const reelsData = res?.data?.fetch__XDTUserDict;
|
|
2250
|
+
let connection = reelsData?.reels;
|
|
2251
|
+
if (!connection && reelsData) {
|
|
2252
|
+
for (const val of Object.values(reelsData)) {
|
|
2253
|
+
if (val && typeof val === "object" && "edges" in val) {
|
|
2254
|
+
connection = val;
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
if (connection?.edges && Array.isArray(connection.edges)) {
|
|
1951
2260
|
return {
|
|
1952
2261
|
edges: connection.edges,
|
|
1953
2262
|
hasNextPage: Boolean(connection.page_info?.has_next_page),
|
|
1954
2263
|
nextCursor: connection.page_info?.end_cursor
|
|
1955
2264
|
};
|
|
1956
2265
|
}
|
|
1957
|
-
} catch {
|
|
1958
|
-
|
|
1959
|
-
try {
|
|
1960
|
-
const restUrl = `https://www.instagram.com/api/v1/feed/user/${userId}/?count=12&max_id=${encodeURIComponent(cursor)}`;
|
|
1961
|
-
const res = await this.httpClient.fetchJson(restUrl, {
|
|
1962
|
-
cookiePath: options.cookiePath,
|
|
1963
|
-
cookieDomain: "instagram.com",
|
|
1964
|
-
signal: options.signal,
|
|
1965
|
-
headers: {
|
|
1966
|
-
"X-IG-App-ID": IG_APP_ID,
|
|
1967
|
-
"Referer": `https://www.instagram.com/${username}/`
|
|
1968
|
-
}
|
|
1969
|
-
});
|
|
1970
|
-
if (Array.isArray(res?.items) && res.items.length > 0) {
|
|
1971
|
-
const edges = res.items.map((item) => ({ node: item }));
|
|
1972
|
-
return {
|
|
1973
|
-
edges,
|
|
1974
|
-
hasNextPage: Boolean(res.more_available),
|
|
1975
|
-
nextCursor: res.next_max_id ? String(res.next_max_id) : void 0
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
} catch {
|
|
2266
|
+
} catch (err) {
|
|
2267
|
+
logExtractorDebug("PolarisProfileReelsTabContentQuery_connection", err);
|
|
1979
2268
|
}
|
|
1980
2269
|
return { edges: [], hasNextPage: false };
|
|
1981
2270
|
}
|
|
@@ -1987,9 +2276,29 @@ var InstagramExtractor = class {
|
|
|
1987
2276
|
found: 0,
|
|
1988
2277
|
message: `Fetching Instagram profile @${username}...`
|
|
1989
2278
|
});
|
|
1990
|
-
|
|
1991
|
-
|
|
2279
|
+
let user = null;
|
|
2280
|
+
try {
|
|
2281
|
+
user = await this.fetchUserProfile(username, options);
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
logExtractorDebug("fetchUserProfile", err);
|
|
2284
|
+
}
|
|
2285
|
+
let webTokens = {};
|
|
2286
|
+
if (!user) {
|
|
2287
|
+
webTokens = await this.fetchWebTokens(username, options);
|
|
2288
|
+
}
|
|
2289
|
+
let userId = user?.id || webTokens.userId;
|
|
2290
|
+
if (!userId && (subDir === "Stories" || subDir === "Highlights")) {
|
|
2291
|
+
try {
|
|
2292
|
+
const probe = await this.fetchPostsTabGraphQL(username, void 0, webTokens, options);
|
|
2293
|
+
if (probe.userId) {
|
|
2294
|
+
userId = probe.userId;
|
|
2295
|
+
}
|
|
2296
|
+
} catch (err) {
|
|
2297
|
+
logExtractorDebug("userId probe", err);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
1992
2300
|
if (subDir === "Stories") {
|
|
2301
|
+
if (!userId) return;
|
|
1993
2302
|
options.onProgress?.({
|
|
1994
2303
|
stage: "Stories",
|
|
1995
2304
|
found: 0,
|
|
@@ -2005,6 +2314,7 @@ var InstagramExtractor = class {
|
|
|
2005
2314
|
return;
|
|
2006
2315
|
}
|
|
2007
2316
|
if (subDir === "Highlights") {
|
|
2317
|
+
if (!userId) return;
|
|
2008
2318
|
options.onProgress?.({
|
|
2009
2319
|
stage: "Highlights",
|
|
2010
2320
|
found: 0,
|
|
@@ -2030,15 +2340,31 @@ var InstagramExtractor = class {
|
|
|
2030
2340
|
return;
|
|
2031
2341
|
}
|
|
2032
2342
|
let found = 0;
|
|
2033
|
-
|
|
2034
|
-
let
|
|
2035
|
-
let
|
|
2343
|
+
const emittedItemIds = /* @__PURE__ */ new Set();
|
|
2344
|
+
let currentEdges = user?.edge_owner_to_timeline_media?.edges ?? [];
|
|
2345
|
+
let hasNextPage = Boolean(user?.edge_owner_to_timeline_media?.page_info?.has_next_page);
|
|
2346
|
+
let endCursor = user?.edge_owner_to_timeline_media?.page_info?.end_cursor;
|
|
2347
|
+
if (!user) {
|
|
2348
|
+
try {
|
|
2349
|
+
const firstPage = await this.fetchPostsTabGraphQL(username, void 0, webTokens, options);
|
|
2350
|
+
currentEdges = firstPage.edges;
|
|
2351
|
+
hasNextPage = firstPage.hasNextPage;
|
|
2352
|
+
endCursor = firstPage.nextCursor;
|
|
2353
|
+
if (!userId && firstPage.userId) {
|
|
2354
|
+
userId = firstPage.userId;
|
|
2355
|
+
}
|
|
2356
|
+
} catch (err) {
|
|
2357
|
+
logExtractorDebug("fetchPostsTabGraphQL initial", err);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2036
2360
|
while (true) {
|
|
2037
2361
|
for (const edge of currentEdges) {
|
|
2038
2362
|
const items = parseIgNodeToMediaItems(edge.node, username);
|
|
2039
2363
|
for (const item of items) {
|
|
2040
2364
|
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
2041
2365
|
if (subDir === "Videos" && item.type !== "video") continue;
|
|
2366
|
+
if (emittedItemIds.has(item.id)) continue;
|
|
2367
|
+
emittedItemIds.add(item.id);
|
|
2042
2368
|
found++;
|
|
2043
2369
|
options.onProgress?.({
|
|
2044
2370
|
stage: subDir,
|
|
@@ -2053,7 +2379,7 @@ var InstagramExtractor = class {
|
|
|
2053
2379
|
}
|
|
2054
2380
|
await sleep(1500, 2500, options.signal);
|
|
2055
2381
|
try {
|
|
2056
|
-
const nextPage = await this.fetchNextPostsPage(userId, username, endCursor, options);
|
|
2382
|
+
const nextPage = await this.fetchNextPostsPage(userId || "", username, endCursor, options, webTokens);
|
|
2057
2383
|
currentEdges = nextPage.edges;
|
|
2058
2384
|
hasNextPage = nextPage.hasNextPage;
|
|
2059
2385
|
endCursor = nextPage.nextCursor;
|
|
@@ -2064,12 +2390,14 @@ var InstagramExtractor = class {
|
|
|
2064
2390
|
break;
|
|
2065
2391
|
}
|
|
2066
2392
|
}
|
|
2067
|
-
if (subDir === "Videos") {
|
|
2068
|
-
const videoEdges = user
|
|
2393
|
+
if (subDir === "Videos" && userId) {
|
|
2394
|
+
const videoEdges = user?.edge_felix_video_timeline?.edges ?? [];
|
|
2069
2395
|
for (const edge of videoEdges) {
|
|
2070
2396
|
const items = parseIgNodeToMediaItems(edge.node, username);
|
|
2071
2397
|
for (const item of items) {
|
|
2072
2398
|
if (item.type !== "video") continue;
|
|
2399
|
+
if (emittedItemIds.has(item.id)) continue;
|
|
2400
|
+
emittedItemIds.add(item.id);
|
|
2073
2401
|
found++;
|
|
2074
2402
|
options.onProgress?.({
|
|
2075
2403
|
stage: "Videos",
|
|
@@ -2079,6 +2407,37 @@ var InstagramExtractor = class {
|
|
|
2079
2407
|
yield item;
|
|
2080
2408
|
}
|
|
2081
2409
|
}
|
|
2410
|
+
let reelsCursor = "";
|
|
2411
|
+
let hasMoreReels = true;
|
|
2412
|
+
while (hasMoreReels && !options.signal?.aborted) {
|
|
2413
|
+
try {
|
|
2414
|
+
const reelsPage = await this.fetchNextReelsPage(userId, reelsCursor, options);
|
|
2415
|
+
if (!reelsPage.edges || reelsPage.edges.length === 0) break;
|
|
2416
|
+
for (const edge of reelsPage.edges) {
|
|
2417
|
+
const items = parseIgNodeToMediaItems(edge.node, username);
|
|
2418
|
+
for (const item of items) {
|
|
2419
|
+
if (item.type !== "video") continue;
|
|
2420
|
+
if (emittedItemIds.has(item.id)) continue;
|
|
2421
|
+
emittedItemIds.add(item.id);
|
|
2422
|
+
found++;
|
|
2423
|
+
options.onProgress?.({
|
|
2424
|
+
stage: "Videos",
|
|
2425
|
+
found,
|
|
2426
|
+
message: `Found reel: ${item.filename}`
|
|
2427
|
+
});
|
|
2428
|
+
yield item;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
if (reelsPage.hasNextPage && reelsPage.nextCursor && reelsPage.nextCursor !== reelsCursor) {
|
|
2432
|
+
reelsCursor = reelsPage.nextCursor;
|
|
2433
|
+
await sleep(1500, 2500, options.signal);
|
|
2434
|
+
} else {
|
|
2435
|
+
hasMoreReels = false;
|
|
2436
|
+
}
|
|
2437
|
+
} catch {
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2082
2441
|
}
|
|
2083
2442
|
}
|
|
2084
2443
|
};
|
|
@@ -2104,8 +2463,43 @@ var TWITTER_FEATURES = {
|
|
|
2104
2463
|
responsive_web_text_conversations_enabled: false,
|
|
2105
2464
|
longform_notetweets_rich_text_read_enabled: true,
|
|
2106
2465
|
longform_notetweets_inline_media_enabled: true,
|
|
2107
|
-
responsive_web_enhance_cards_enabled: false
|
|
2466
|
+
responsive_web_enhance_cards_enabled: false,
|
|
2467
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
2468
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
2469
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
2470
|
+
articles_preview_enabled: true,
|
|
2471
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
2472
|
+
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
2473
|
+
responsive_web_jetfuel_frame: true,
|
|
2474
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
2475
|
+
responsive_web_grok_analysis_button_from_backend: true,
|
|
2476
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
2477
|
+
responsive_web_grok_imagine_annotation_enabled: true,
|
|
2478
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
2479
|
+
rweb_tipjar_consumption_enabled: true
|
|
2108
2480
|
};
|
|
2481
|
+
function shouldSkipTweetReply(tweet, targetUserId, mode = "timeline") {
|
|
2482
|
+
const tweetResult = tweet.tweet || tweet;
|
|
2483
|
+
const legacy = tweetResult.legacy || tweetResult;
|
|
2484
|
+
const replyToUserId = legacy.in_reply_to_user_id_str;
|
|
2485
|
+
if (!replyToUserId) {
|
|
2486
|
+
return false;
|
|
2487
|
+
}
|
|
2488
|
+
const authorId = legacy.user_id_str || tweetResult.core?.user_results?.result?.rest_id;
|
|
2489
|
+
if (mode === "timeline") {
|
|
2490
|
+
if (targetUserId) {
|
|
2491
|
+
if (replyToUserId !== targetUserId || authorId && authorId !== targetUserId) {
|
|
2492
|
+
return true;
|
|
2493
|
+
}
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2496
|
+
return Boolean(authorId && replyToUserId !== authorId);
|
|
2497
|
+
}
|
|
2498
|
+
if (authorId && replyToUserId === authorId) {
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
return true;
|
|
2502
|
+
}
|
|
2109
2503
|
function parseTweetMediaEntities(tweet, username) {
|
|
2110
2504
|
const items = [];
|
|
2111
2505
|
const tweetResult = tweet.tweet || tweet;
|
|
@@ -2175,6 +2569,8 @@ var XExtractor = class {
|
|
|
2175
2569
|
cookiePath: options.cookiePath,
|
|
2176
2570
|
cookieDomain: "x.com",
|
|
2177
2571
|
signal: options.signal,
|
|
2572
|
+
retries: 0,
|
|
2573
|
+
timeoutMs: 3e3,
|
|
2178
2574
|
headers: {
|
|
2179
2575
|
"Authorization": `Bearer ${TWITTER_BEARER_TOKEN}`
|
|
2180
2576
|
}
|
|
@@ -2193,6 +2589,29 @@ var XExtractor = class {
|
|
|
2193
2589
|
throw new Error("Failed to activate X/Twitter guest session: no guest token returned");
|
|
2194
2590
|
}
|
|
2195
2591
|
async fetchUserRestId(username, options) {
|
|
2592
|
+
if (username.startsWith("id:")) {
|
|
2593
|
+
return username.slice(3);
|
|
2594
|
+
}
|
|
2595
|
+
if (username === "i_history_likes") {
|
|
2596
|
+
if (options.cookiePath) {
|
|
2597
|
+
let cookies = await loadCookiesFromPath(options.cookiePath, "x.com");
|
|
2598
|
+
if (!cookies["twid"]) {
|
|
2599
|
+
cookies = await loadCookiesFromPath(options.cookiePath, "twitter.com");
|
|
2600
|
+
}
|
|
2601
|
+
if (!cookies["twid"]) {
|
|
2602
|
+
cookies = await loadCookiesFromPath(options.cookiePath);
|
|
2603
|
+
}
|
|
2604
|
+
const twid = cookies["twid"];
|
|
2605
|
+
if (twid) {
|
|
2606
|
+
const decoded = decodeURIComponent(twid).replace(/^"|"$/g, "").trim();
|
|
2607
|
+
const rawId = decoded.startsWith("u=") ? decoded.slice(2) : decoded.includes("=") ? decoded.split("=").pop()?.trim() : decoded;
|
|
2608
|
+
if (rawId && /^[a-zA-Z0-9_-]+$/.test(rawId)) {
|
|
2609
|
+
return rawId;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
throw new Error("Authentication required for X/Twitter likes history: missing 'twid' cookie");
|
|
2614
|
+
}
|
|
2196
2615
|
await this.ensureGuestToken(options);
|
|
2197
2616
|
const variables = JSON.stringify({
|
|
2198
2617
|
screen_name: username,
|
|
@@ -2204,7 +2623,9 @@ var XExtractor = class {
|
|
|
2204
2623
|
const res = await this.httpClient.fetchJson(url, {
|
|
2205
2624
|
cookiePath: options.cookiePath,
|
|
2206
2625
|
cookieDomain: "x.com",
|
|
2207
|
-
signal: options.signal
|
|
2626
|
+
signal: options.signal,
|
|
2627
|
+
retries: 0,
|
|
2628
|
+
timeoutMs: 4e3
|
|
2208
2629
|
});
|
|
2209
2630
|
const restId = res.data?.user?.result?.rest_id;
|
|
2210
2631
|
if (restId) return restId;
|
|
@@ -2212,7 +2633,11 @@ var XExtractor = class {
|
|
|
2212
2633
|
}
|
|
2213
2634
|
const syndiUrl = `https://cdn.syndication.twimg.com/widgets/followbutton/info.json?screen_names=${encodeURIComponent(username)}`;
|
|
2214
2635
|
try {
|
|
2215
|
-
const res = await this.httpClient.fetchJson(syndiUrl, {
|
|
2636
|
+
const res = await this.httpClient.fetchJson(syndiUrl, {
|
|
2637
|
+
signal: options.signal,
|
|
2638
|
+
retries: 0,
|
|
2639
|
+
timeoutMs: 3e3
|
|
2640
|
+
});
|
|
2216
2641
|
if (res[0]?.id) return String(res[0].id);
|
|
2217
2642
|
} catch {
|
|
2218
2643
|
}
|
|
@@ -2221,13 +2646,28 @@ var XExtractor = class {
|
|
|
2221
2646
|
async *extract(profile, options) {
|
|
2222
2647
|
const username = profile.username;
|
|
2223
2648
|
const subDir = options.subDir;
|
|
2649
|
+
const isLikes = username === "i_history_likes" || profile.cleanUrl?.endsWith("/likes");
|
|
2650
|
+
const authHeaders = {};
|
|
2651
|
+
if (options.cookiePath) {
|
|
2652
|
+
let cookies = await loadCookiesFromPath(options.cookiePath, "x.com");
|
|
2653
|
+
if (!cookies["ct0"] && !cookies["auth_token"]) {
|
|
2654
|
+
const twCookies = await loadCookiesFromPath(options.cookiePath, "twitter.com");
|
|
2655
|
+
cookies = { ...twCookies, ...cookies };
|
|
2656
|
+
}
|
|
2657
|
+
if (cookies["ct0"]) {
|
|
2658
|
+
authHeaders["x-csrf-token"] = cookies["ct0"];
|
|
2659
|
+
}
|
|
2660
|
+
if (cookies["auth_token"]) {
|
|
2661
|
+
authHeaders["x-twitter-auth-type"] = "OAuth2Session";
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2224
2664
|
options.onProgress?.({
|
|
2225
|
-
stage: "Profile",
|
|
2665
|
+
stage: isLikes ? "Likes" : "Profile",
|
|
2226
2666
|
found: 0,
|
|
2227
|
-
message: `Fetching X profile @${username}...`
|
|
2667
|
+
message: isLikes ? "Fetching liked posts from X..." : `Fetching X profile @${username}...`
|
|
2228
2668
|
});
|
|
2229
2669
|
const userId = await this.fetchUserRestId(username, options);
|
|
2230
|
-
const endpoint = "https://x.com/i/api/graphql/jCRhbOzdgOHp6u9H4g2tEg/UserMedia";
|
|
2670
|
+
const endpoint = isLikes ? "https://x.com/i/api/graphql/TGEKkJG_meudeaFcqaxM-Q/Likes" : "https://x.com/i/api/graphql/jCRhbOzdgOHp6u9H4g2tEg/UserMedia";
|
|
2231
2671
|
let cursor;
|
|
2232
2672
|
let hasMore = true;
|
|
2233
2673
|
let found = 0;
|
|
@@ -2251,11 +2691,15 @@ var XExtractor = class {
|
|
|
2251
2691
|
res = await this.httpClient.fetchJson(url, {
|
|
2252
2692
|
cookiePath: options.cookiePath,
|
|
2253
2693
|
cookieDomain: "x.com",
|
|
2254
|
-
signal: options.signal
|
|
2694
|
+
signal: options.signal,
|
|
2695
|
+
headers: authHeaders
|
|
2255
2696
|
});
|
|
2256
2697
|
} catch (err) {
|
|
2257
2698
|
break;
|
|
2258
2699
|
}
|
|
2700
|
+
if (res?.errors?.length && !res?.data?.user?.result) {
|
|
2701
|
+
throw new Error(`X/Twitter API error: ${res.errors[0]?.message || "GraphQL query failed"}`);
|
|
2702
|
+
}
|
|
2259
2703
|
const instructions = res?.data?.user?.result?.timeline_v2?.timeline?.instructions || [];
|
|
2260
2704
|
let newCursor;
|
|
2261
2705
|
let pageItemCount = 0;
|
|
@@ -2268,6 +2712,9 @@ var XExtractor = class {
|
|
|
2268
2712
|
}
|
|
2269
2713
|
const tweetContent = entry.content?.itemContent?.tweet_results?.result;
|
|
2270
2714
|
if (tweetContent) {
|
|
2715
|
+
if (shouldSkipTweetReply(tweetContent, userId, isLikes ? "likes" : "timeline")) {
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2271
2718
|
const mediaItems = parseTweetMediaEntities(tweetContent, username);
|
|
2272
2719
|
for (const item of mediaItems) {
|
|
2273
2720
|
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
@@ -2296,16 +2743,86 @@ var XExtractor = class {
|
|
|
2296
2743
|
|
|
2297
2744
|
// src/lib/engines/extractors/tiktok.ts
|
|
2298
2745
|
function parseTikTokRehydrationJson(html) {
|
|
2299
|
-
const
|
|
2746
|
+
const universalMatch = html.match(
|
|
2300
2747
|
/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/
|
|
2301
2748
|
);
|
|
2302
|
-
if (
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2749
|
+
if (universalMatch) {
|
|
2750
|
+
try {
|
|
2751
|
+
const raw = JSON.parse(universalMatch[1]);
|
|
2752
|
+
const scope = raw?.["__DEFAULT_SCOPE__"] || raw;
|
|
2753
|
+
if (scope) return scope;
|
|
2754
|
+
} catch {
|
|
2755
|
+
}
|
|
2308
2756
|
}
|
|
2757
|
+
const sigiTagMatch = html.match(
|
|
2758
|
+
/<script id="SIGI_STATE"[^>]*>([\s\S]*?)<\/script>/
|
|
2759
|
+
);
|
|
2760
|
+
if (sigiTagMatch) {
|
|
2761
|
+
try {
|
|
2762
|
+
const raw = JSON.parse(sigiTagMatch[1]);
|
|
2763
|
+
const user = Object.values(raw?.UserModule?.users || {})[0];
|
|
2764
|
+
const items = Object.values(raw?.ItemModule || {});
|
|
2765
|
+
return {
|
|
2766
|
+
"webapp.user-detail": {
|
|
2767
|
+
userInfo: { user },
|
|
2768
|
+
itemList: items
|
|
2769
|
+
},
|
|
2770
|
+
SIGI_STATE: raw
|
|
2771
|
+
};
|
|
2772
|
+
} catch {
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
const sigiWindowMatch = html.match(
|
|
2776
|
+
/(?:window\[['"]SIGI_STATE['"]\]|SIGI_STATE)\s*=\s*(\{[\s\S]*?\})\s*;/i
|
|
2777
|
+
);
|
|
2778
|
+
if (sigiWindowMatch) {
|
|
2779
|
+
try {
|
|
2780
|
+
const raw = JSON.parse(sigiWindowMatch[1]);
|
|
2781
|
+
const user = Object.values(raw?.UserModule?.users || {})[0];
|
|
2782
|
+
const items = Object.values(raw?.ItemModule || {});
|
|
2783
|
+
return {
|
|
2784
|
+
"webapp.user-detail": {
|
|
2785
|
+
userInfo: { user },
|
|
2786
|
+
itemList: items
|
|
2787
|
+
},
|
|
2788
|
+
SIGI_STATE: raw
|
|
2789
|
+
};
|
|
2790
|
+
} catch {
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const nextDataMatch = html.match(
|
|
2794
|
+
/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/
|
|
2795
|
+
);
|
|
2796
|
+
if (nextDataMatch) {
|
|
2797
|
+
try {
|
|
2798
|
+
const raw = JSON.parse(nextDataMatch[1]);
|
|
2799
|
+
const pageProps = raw?.props?.pageProps;
|
|
2800
|
+
if (pageProps) {
|
|
2801
|
+
return {
|
|
2802
|
+
"webapp.user-detail": {
|
|
2803
|
+
userInfo: pageProps.userInfo,
|
|
2804
|
+
itemList: pageProps.items || pageProps.itemList || []
|
|
2805
|
+
},
|
|
2806
|
+
__NEXT_DATA__: raw
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
} catch {
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
const secUidMatch = html.match(/(?:secUid|sec_uid)["':=\s]+(MS4wLjABAAAA[\w-]{32,64})/i);
|
|
2813
|
+
if (secUidMatch) {
|
|
2814
|
+
return {
|
|
2815
|
+
"webapp.user-detail": {
|
|
2816
|
+
userInfo: {
|
|
2817
|
+
user: {
|
|
2818
|
+
secUid: secUidMatch[1]
|
|
2819
|
+
}
|
|
2820
|
+
},
|
|
2821
|
+
itemList: []
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
return null;
|
|
2309
2826
|
}
|
|
2310
2827
|
function parseTikTokItem(item, username) {
|
|
2311
2828
|
const items = [];
|
|
@@ -2368,7 +2885,9 @@ var TikTokExtractor = class {
|
|
|
2368
2885
|
const html = await this.httpClient.fetchText(profileUrl, {
|
|
2369
2886
|
cookiePath: options.cookiePath,
|
|
2370
2887
|
cookieDomain: "tiktok.com",
|
|
2371
|
-
signal: options.signal
|
|
2888
|
+
signal: options.signal,
|
|
2889
|
+
retries: 0,
|
|
2890
|
+
timeoutMs: 4e3
|
|
2372
2891
|
});
|
|
2373
2892
|
const scope = parseTikTokRehydrationJson(html);
|
|
2374
2893
|
const userDetail = scope?.["webapp.user-detail"];
|
|
@@ -2400,13 +2919,28 @@ var TikTokExtractor = class {
|
|
|
2400
2919
|
if (options.signal?.aborted) break;
|
|
2401
2920
|
if (visitedCursors.has(cursor)) break;
|
|
2402
2921
|
visitedCursors.add(cursor);
|
|
2403
|
-
const
|
|
2922
|
+
const deviceId = `${Math.floor(725e16 + Math.random() * 75e15)}`;
|
|
2923
|
+
const queryParams = new URLSearchParams({
|
|
2924
|
+
aid: "1988",
|
|
2925
|
+
app_name: "tiktok_web",
|
|
2926
|
+
device_platform: "web_pc",
|
|
2927
|
+
channel: "tiktok_web",
|
|
2928
|
+
device_id: deviceId,
|
|
2929
|
+
secUid,
|
|
2930
|
+
count: "30",
|
|
2931
|
+
cursor,
|
|
2932
|
+
post_item_list_request_type: "0"
|
|
2933
|
+
});
|
|
2934
|
+
const apiUrl = `https://www.tiktok.com/api/post/item_list/?${queryParams.toString()}`;
|
|
2404
2935
|
let res;
|
|
2405
2936
|
try {
|
|
2406
2937
|
res = await this.httpClient.fetchJson(apiUrl, {
|
|
2407
2938
|
cookiePath: options.cookiePath,
|
|
2408
2939
|
cookieDomain: "tiktok.com",
|
|
2409
|
-
signal: options.signal
|
|
2940
|
+
signal: options.signal,
|
|
2941
|
+
headers: {
|
|
2942
|
+
"Referer": profileUrl
|
|
2943
|
+
}
|
|
2410
2944
|
});
|
|
2411
2945
|
} catch {
|
|
2412
2946
|
break;
|
|
@@ -2435,54 +2969,95 @@ var TikTokExtractor = class {
|
|
|
2435
2969
|
};
|
|
2436
2970
|
|
|
2437
2971
|
// src/lib/engines/extractors/facebook.ts
|
|
2972
|
+
function decodeEscapedUrl(raw) {
|
|
2973
|
+
const sanitized = raw.replace(/[\r\n\t]/g, "").trim();
|
|
2974
|
+
try {
|
|
2975
|
+
return JSON.parse(`"${sanitized.replace(/"/g, '\\"')}"`);
|
|
2976
|
+
} catch {
|
|
2977
|
+
return sanitized.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\(["\\\/bfnrt])/g, (_, ch) => {
|
|
2978
|
+
const escapes = {
|
|
2979
|
+
'"': '"',
|
|
2980
|
+
"\\": "\\",
|
|
2981
|
+
"/": "/",
|
|
2982
|
+
b: "\b",
|
|
2983
|
+
f: "\f",
|
|
2984
|
+
n: "\n",
|
|
2985
|
+
r: "\r",
|
|
2986
|
+
t: " "
|
|
2987
|
+
};
|
|
2988
|
+
return escapes[ch] || ch;
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2438
2992
|
function extractFacebookMediaFromHtml(html, username) {
|
|
2439
2993
|
const items = [];
|
|
2440
2994
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
2441
|
-
const
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2995
|
+
const videoPatterns = [
|
|
2996
|
+
/"browser_native_hd_url":"([^"]+)"/g,
|
|
2997
|
+
/"browser_native_sd_url":"([^"]+)"/g,
|
|
2998
|
+
/"playable_url_quality_hd":"([^"]+)"/g,
|
|
2999
|
+
/"playable_url":"([^"]+)"/g,
|
|
3000
|
+
/<video[^>]+src=["'](https?:[^"']+)["']/gi
|
|
3001
|
+
];
|
|
3002
|
+
let videoIndex = 1;
|
|
3003
|
+
for (const regex of videoPatterns) {
|
|
3004
|
+
let vMatch;
|
|
3005
|
+
while ((vMatch = regex.exec(html)) !== null) {
|
|
3006
|
+
const rawUrl = vMatch[1];
|
|
3007
|
+
const videoUrl = decodeEscapedUrl(rawUrl).replace(/&/g, "&");
|
|
3008
|
+
if (videoUrl.startsWith("http") && !seenUrls.has(videoUrl)) {
|
|
2448
3009
|
seenUrls.add(videoUrl);
|
|
2449
3010
|
items.push({
|
|
2450
|
-
id: `fb_video_${
|
|
3011
|
+
id: `fb_video_${videoIndex}`,
|
|
2451
3012
|
url: videoUrl,
|
|
2452
|
-
filename: `${username}_video_${
|
|
3013
|
+
filename: `${username}_video_${videoIndex}`,
|
|
2453
3014
|
extension: "mp4",
|
|
2454
3015
|
type: "video"
|
|
2455
3016
|
});
|
|
3017
|
+
videoIndex++;
|
|
2456
3018
|
}
|
|
2457
|
-
} catch {
|
|
2458
3019
|
}
|
|
2459
3020
|
}
|
|
2460
|
-
const photoRegex = /,"image":\{"uri":"([^"]+)"/g;
|
|
3021
|
+
const photoRegex = /,"(?:image|viewer_image|full_size_image|display_image)":\{"uri":"([^"]+)"/g;
|
|
2461
3022
|
let match;
|
|
2462
3023
|
let photoIndex = 1;
|
|
2463
3024
|
while ((match = photoRegex.exec(html)) !== null) {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
3025
|
+
const rawUrl = match[1];
|
|
3026
|
+
const photoUrl = decodeEscapedUrl(rawUrl).replace(/&/g, "&");
|
|
3027
|
+
if (!seenUrls.has(photoUrl)) {
|
|
3028
|
+
seenUrls.add(photoUrl);
|
|
3029
|
+
items.push({
|
|
3030
|
+
id: `fb_photo_${photoIndex}`,
|
|
3031
|
+
url: photoUrl,
|
|
3032
|
+
filename: `${username}_photo_${photoIndex}`,
|
|
3033
|
+
extension: "jpg",
|
|
3034
|
+
type: "photo"
|
|
3035
|
+
});
|
|
3036
|
+
photoIndex++;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
const mbasicImgRegex = /<img[^>]+src=["'](https:\/\/[^"'\s]*scontent[^"'\s]*\.(?:jpg|png|webp)[^"'\s]*)["']/gi;
|
|
3040
|
+
let mMatch;
|
|
3041
|
+
while ((mMatch = mbasicImgRegex.exec(html)) !== null) {
|
|
3042
|
+
const rawUrl = mMatch[1];
|
|
3043
|
+
const photoUrl = decodeEscapedUrl(rawUrl).replace(/&/g, "&");
|
|
3044
|
+
if (!seenUrls.has(photoUrl)) {
|
|
3045
|
+
seenUrls.add(photoUrl);
|
|
3046
|
+
items.push({
|
|
3047
|
+
id: `fb_photo_${photoIndex}`,
|
|
3048
|
+
url: photoUrl,
|
|
3049
|
+
filename: `${username}_photo_${photoIndex}`,
|
|
3050
|
+
extension: "jpg",
|
|
3051
|
+
type: "photo"
|
|
3052
|
+
});
|
|
3053
|
+
photoIndex++;
|
|
2479
3054
|
}
|
|
2480
3055
|
}
|
|
2481
3056
|
if (items.length === 0) {
|
|
2482
|
-
const scontentRegex = /https:\/\/[^"'\s]*scontent[^"'\s]*\.(?:jpg|png|webp)[^"'\s]*/gi;
|
|
3057
|
+
const scontentRegex = /https:\/\/[^"'\s<>]*scontent[^"'\s<>]*\.(?:jpg|png|webp)[^"'\s<>]*/gi;
|
|
2483
3058
|
let sMatch;
|
|
2484
3059
|
while ((sMatch = scontentRegex.exec(html)) !== null) {
|
|
2485
|
-
const cleanUrl = sMatch[0].replace(/\\/g, "");
|
|
3060
|
+
const cleanUrl = sMatch[0].replace(/\\/g, "").replace(/&/g, "&");
|
|
2486
3061
|
if (!seenUrls.has(cleanUrl)) {
|
|
2487
3062
|
seenUrls.add(cleanUrl);
|
|
2488
3063
|
items.push({
|
|
@@ -2498,6 +3073,59 @@ function extractFacebookMediaFromHtml(html, username) {
|
|
|
2498
3073
|
}
|
|
2499
3074
|
return items;
|
|
2500
3075
|
}
|
|
3076
|
+
function extractFacebookPaginationCursor(html) {
|
|
3077
|
+
const jsonCursorMatch = html.match(/"end_cursor":"([^"]+)"/) || html.match(/"cursor":"([^"]+)"/);
|
|
3078
|
+
if (jsonCursorMatch && jsonCursorMatch[1]) {
|
|
3079
|
+
return jsonCursorMatch[1];
|
|
3080
|
+
}
|
|
3081
|
+
const linkCursorMatch = html.match(/[?&](?:cursor|after)=([^"'\s&<>]+)/i);
|
|
3082
|
+
if (linkCursorMatch && linkCursorMatch[1]) {
|
|
3083
|
+
try {
|
|
3084
|
+
return decodeURIComponent(linkCursorMatch[1]);
|
|
3085
|
+
} catch {
|
|
3086
|
+
return linkCursorMatch[1];
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
function resolveFacebookUrl(href, currentUrl) {
|
|
3092
|
+
const cleanHref = href.replace(/\\/g, "").replace(/&/g, "&");
|
|
3093
|
+
try {
|
|
3094
|
+
return new URL(cleanHref, currentUrl).toString();
|
|
3095
|
+
} catch {
|
|
3096
|
+
return cleanHref;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function extractFacebookNextPageUrl(html, currentUrl) {
|
|
3100
|
+
const moreItemMatch = html.match(/<div[^>]*id=["'](?:m_more_item|m_more_posts)["'][^>]*>\s*<a[^>]+href=["']([^"']+)["']/i);
|
|
3101
|
+
if (moreItemMatch && moreItemMatch[1]) {
|
|
3102
|
+
return resolveFacebookUrl(moreItemMatch[1], currentUrl);
|
|
3103
|
+
}
|
|
3104
|
+
const cursorLabelMatch = html.match(/<a[^>]+href=["']([^"']*(?:cursor=|after=)[^"']+)["'][^>]*>[\s\S]*?(?:See [Mm]ore|More [Pp]hotos|More|Next|Older [Pp]osts)[\s\S]*?<\/a>/i);
|
|
3105
|
+
if (cursorLabelMatch && cursorLabelMatch[1]) {
|
|
3106
|
+
return resolveFacebookUrl(cursorLabelMatch[1], currentUrl);
|
|
3107
|
+
}
|
|
3108
|
+
const genericCursorMatch = html.match(/<a[^>]+href=["']([^"']*(?:[?&]|&)(?:cursor|after)=[^"']+)["']/i);
|
|
3109
|
+
if (genericCursorMatch && genericCursorMatch[1]) {
|
|
3110
|
+
return resolveFacebookUrl(genericCursorMatch[1], currentUrl);
|
|
3111
|
+
}
|
|
3112
|
+
const cursor = extractFacebookPaginationCursor(html);
|
|
3113
|
+
if (cursor) {
|
|
3114
|
+
try {
|
|
3115
|
+
const u = new URL(currentUrl);
|
|
3116
|
+
if (u.searchParams.get("cursor") !== cursor) {
|
|
3117
|
+
u.searchParams.set("cursor", cursor);
|
|
3118
|
+
return u.toString();
|
|
3119
|
+
}
|
|
3120
|
+
} catch {
|
|
3121
|
+
if (!currentUrl.includes(cursor)) {
|
|
3122
|
+
const sep = currentUrl.includes("?") ? "&" : "?";
|
|
3123
|
+
return `${currentUrl}${sep}cursor=${encodeURIComponent(cursor)}`;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
2501
3129
|
var FacebookExtractor = class {
|
|
2502
3130
|
platform = "facebook";
|
|
2503
3131
|
httpClient;
|
|
@@ -2514,24 +3142,120 @@ var FacebookExtractor = class {
|
|
|
2514
3142
|
found: 0,
|
|
2515
3143
|
message: `Fetching Facebook profile @${username}...`
|
|
2516
3144
|
});
|
|
2517
|
-
const
|
|
2518
|
-
const
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
3145
|
+
const targetPath = subDir === "Photos" ? "photos" : "videos";
|
|
3146
|
+
const desktopUrl = `https://www.facebook.com/${encodeURIComponent(username)}/${targetPath}`;
|
|
3147
|
+
const mbasicUrl = `https://mbasic.facebook.com/${encodeURIComponent(username)}/${targetPath}`;
|
|
3148
|
+
const mobileUrl = `https://m.facebook.com/${encodeURIComponent(username)}/${targetPath}`;
|
|
3149
|
+
let currentUrl = desktopUrl;
|
|
3150
|
+
let html = "";
|
|
3151
|
+
try {
|
|
3152
|
+
html = await this.httpClient.fetchText(desktopUrl, {
|
|
3153
|
+
cookiePath: options.cookiePath,
|
|
3154
|
+
cookieDomain: "facebook.com",
|
|
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) {
|
|
2529
3164
|
options.onProgress?.({
|
|
2530
|
-
stage:
|
|
2531
|
-
found,
|
|
2532
|
-
message: `
|
|
3165
|
+
stage: "Profile",
|
|
3166
|
+
found: 0,
|
|
3167
|
+
message: `Desktop yielded 0 items. Falling back to mbasic.facebook.com for @${username}...`
|
|
2533
3168
|
});
|
|
2534
|
-
|
|
3169
|
+
try {
|
|
3170
|
+
const mbasicHtml = await this.httpClient.fetchText(mbasicUrl, {
|
|
3171
|
+
cookiePath: options.cookiePath,
|
|
3172
|
+
cookieDomain: "facebook.com",
|
|
3173
|
+
signal: options.signal,
|
|
3174
|
+
retries: 1,
|
|
3175
|
+
timeoutMs: 5e3,
|
|
3176
|
+
headers: {
|
|
3177
|
+
"Referer": "https://mbasic.facebook.com/"
|
|
3178
|
+
}
|
|
3179
|
+
});
|
|
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
|
+
} catch {
|
|
3187
|
+
}
|
|
3188
|
+
if (initialItems.length === 0) {
|
|
3189
|
+
try {
|
|
3190
|
+
const mobileHtml = await this.httpClient.fetchText(mobileUrl, {
|
|
3191
|
+
cookiePath: options.cookiePath,
|
|
3192
|
+
cookieDomain: "facebook.com",
|
|
3193
|
+
signal: options.signal,
|
|
3194
|
+
retries: 0,
|
|
3195
|
+
timeoutMs: 5e3,
|
|
3196
|
+
headers: {
|
|
3197
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
|
3198
|
+
"Referer": "https://m.facebook.com/"
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
const mobileItems = extractFacebookMediaFromHtml(mobileHtml, username);
|
|
3202
|
+
if (mobileItems.length > 0) {
|
|
3203
|
+
html = mobileHtml;
|
|
3204
|
+
currentUrl = mobileUrl;
|
|
3205
|
+
initialItems = mobileItems;
|
|
3206
|
+
}
|
|
3207
|
+
} catch {
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
3212
|
+
let found = 0;
|
|
3213
|
+
let page = 1;
|
|
3214
|
+
const maxPages = 5;
|
|
3215
|
+
while (true) {
|
|
3216
|
+
const items = extractFacebookMediaFromHtml(html, username);
|
|
3217
|
+
let pageNewCount = 0;
|
|
3218
|
+
for (const item of items) {
|
|
3219
|
+
if (subDir === "Photos" && item.type !== "photo") continue;
|
|
3220
|
+
if (subDir === "Videos" && item.type !== "video") continue;
|
|
3221
|
+
if (seenUrls.has(item.url)) continue;
|
|
3222
|
+
seenUrls.add(item.url);
|
|
3223
|
+
found++;
|
|
3224
|
+
pageNewCount++;
|
|
3225
|
+
options.onProgress?.({
|
|
3226
|
+
stage: subDir,
|
|
3227
|
+
found,
|
|
3228
|
+
message: `Found item: ${item.filename}`
|
|
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/"
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
page++;
|
|
3256
|
+
} catch {
|
|
3257
|
+
break;
|
|
3258
|
+
}
|
|
2535
3259
|
}
|
|
2536
3260
|
}
|
|
2537
3261
|
};
|
|
@@ -2559,17 +3283,48 @@ import path5 from "path";
|
|
|
2559
3283
|
import { Readable } from "stream";
|
|
2560
3284
|
import { pipeline } from "stream/promises";
|
|
2561
3285
|
var ARCHIVE_FILENAME = ".open-nami-archive.json";
|
|
3286
|
+
var YTDLP_ARCHIVE_FILENAME = "archive_yt-dlp.txt";
|
|
3287
|
+
var TEXT_ARCHIVE_FILENAME = "archive.txt";
|
|
2562
3288
|
async function loadArchive(destDir) {
|
|
2563
|
-
const
|
|
3289
|
+
const archive = /* @__PURE__ */ new Set();
|
|
3290
|
+
const jsonPath = path5.join(destDir, ARCHIVE_FILENAME);
|
|
2564
3291
|
try {
|
|
2565
|
-
const raw = await fsPromises2.readFile(
|
|
3292
|
+
const raw = await fsPromises2.readFile(jsonPath, "utf-8");
|
|
2566
3293
|
const parsed = JSON.parse(raw);
|
|
2567
3294
|
if (Array.isArray(parsed)) {
|
|
2568
|
-
|
|
3295
|
+
for (const id of parsed) {
|
|
3296
|
+
if (id) archive.add(String(id));
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
} catch {
|
|
3300
|
+
}
|
|
3301
|
+
const ytdlpPath = path5.join(destDir, YTDLP_ARCHIVE_FILENAME);
|
|
3302
|
+
try {
|
|
3303
|
+
const raw = await fsPromises2.readFile(ytdlpPath, "utf-8");
|
|
3304
|
+
const lines = raw.split(/\r?\n/);
|
|
3305
|
+
for (const rawLine of lines) {
|
|
3306
|
+
const line = rawLine.trim();
|
|
3307
|
+
if (!line || line.startsWith("#")) continue;
|
|
3308
|
+
const parts = line.split(/\s+/);
|
|
3309
|
+
const id = parts[parts.length - 1];
|
|
3310
|
+
if (id) archive.add(id);
|
|
2569
3311
|
}
|
|
2570
3312
|
} catch {
|
|
2571
3313
|
}
|
|
2572
|
-
|
|
3314
|
+
const textPath = path5.join(destDir, TEXT_ARCHIVE_FILENAME);
|
|
3315
|
+
try {
|
|
3316
|
+
const raw = await fsPromises2.readFile(textPath, "utf-8");
|
|
3317
|
+
const lines = raw.split(/\r?\n/);
|
|
3318
|
+
for (const rawLine of lines) {
|
|
3319
|
+
const line = rawLine.trim();
|
|
3320
|
+
if (!line || line.startsWith("#")) continue;
|
|
3321
|
+
const parts = line.split(/\s+/);
|
|
3322
|
+
const id = parts[parts.length - 1];
|
|
3323
|
+
if (id) archive.add(id);
|
|
3324
|
+
}
|
|
3325
|
+
} catch {
|
|
3326
|
+
}
|
|
3327
|
+
return archive;
|
|
2573
3328
|
}
|
|
2574
3329
|
async function saveArchive(destDir, archive) {
|
|
2575
3330
|
const archivePath = path5.join(destDir, ARCHIVE_FILENAME);
|
|
@@ -2579,6 +3334,18 @@ async function saveArchive(destDir, archive) {
|
|
|
2579
3334
|
await fsPromises2.rename(tmpPath, archivePath);
|
|
2580
3335
|
} catch {
|
|
2581
3336
|
}
|
|
3337
|
+
const textPaths = [
|
|
3338
|
+
path5.join(destDir, YTDLP_ARCHIVE_FILENAME),
|
|
3339
|
+
path5.join(destDir, TEXT_ARCHIVE_FILENAME)
|
|
3340
|
+
];
|
|
3341
|
+
const content = [...archive].map((id) => `${id}
|
|
3342
|
+
`).join("");
|
|
3343
|
+
for (const tPath of textPaths) {
|
|
3344
|
+
try {
|
|
3345
|
+
await fsPromises2.writeFile(tPath, content, "utf-8");
|
|
3346
|
+
} catch {
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
2582
3349
|
}
|
|
2583
3350
|
function sanitizeFilename(rawFilename, fallbackId, ext) {
|
|
2584
3351
|
const cleanExt = ext.replace(/^\./, "");
|
|
@@ -2595,90 +3362,138 @@ function sanitizeFilename(rawFilename, fallbackId, ext) {
|
|
|
2595
3362
|
async function downloadMediaStream(items, options) {
|
|
2596
3363
|
const { jobName, destDir, signal, onProgress } = options;
|
|
2597
3364
|
const httpClient = options.httpClient ?? new HttpClient();
|
|
3365
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
2598
3366
|
await fsPromises2.mkdir(destDir, { recursive: true });
|
|
2599
3367
|
const archive = await loadArchive(destDir);
|
|
2600
3368
|
let downloaded = 0;
|
|
2601
3369
|
let skipped = 0;
|
|
2602
3370
|
const errors = [];
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
downloadedCount: downloaded,
|
|
2615
|
-
skippedCount: skipped,
|
|
2616
|
-
currentFile: finalFilename,
|
|
2617
|
-
statusText: `Skipping already downloaded: ${finalFilename}`
|
|
2618
|
-
});
|
|
2619
|
-
continue;
|
|
2620
|
-
}
|
|
2621
|
-
onProgress?.({
|
|
2622
|
-
job: jobName,
|
|
2623
|
-
downloadedCount: downloaded,
|
|
2624
|
-
skippedCount: skipped,
|
|
2625
|
-
currentFile: finalFilename,
|
|
2626
|
-
statusText: `Downloading: ${finalFilename}`
|
|
2627
|
-
});
|
|
2628
|
-
const tmpPath = `${finalPath}.download`;
|
|
2629
|
-
try {
|
|
2630
|
-
const reqHeaders = {};
|
|
2631
|
-
if (options.referer) {
|
|
2632
|
-
reqHeaders["Referer"] = options.referer;
|
|
2633
|
-
}
|
|
2634
|
-
const response = await httpClient.request(item.url, {
|
|
2635
|
-
signal,
|
|
2636
|
-
cookiePath: options.cookiePath,
|
|
2637
|
-
headers: reqHeaders
|
|
2638
|
-
});
|
|
2639
|
-
if (!response.ok || !response.body) {
|
|
2640
|
-
throw new Error(`HTTP ${response.status} (${response.statusText}) for ${item.url}`);
|
|
3371
|
+
const iterator = items[Symbol.asyncIterator]();
|
|
3372
|
+
let queueLock = Promise.resolve();
|
|
3373
|
+
let isDone = false;
|
|
3374
|
+
let consecutiveErrors = 0;
|
|
3375
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
3376
|
+
function getNextItem() {
|
|
3377
|
+
const nextItem = queueLock.then(() => iterator.next());
|
|
3378
|
+
queueLock = nextItem.then(
|
|
3379
|
+
() => {
|
|
3380
|
+
},
|
|
3381
|
+
() => {
|
|
2641
3382
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
3383
|
+
);
|
|
3384
|
+
return nextItem;
|
|
3385
|
+
}
|
|
3386
|
+
async function downloadWorker() {
|
|
3387
|
+
while (!isDone && !signal?.aborted) {
|
|
3388
|
+
let item;
|
|
3389
|
+
try {
|
|
3390
|
+
const next = await getNextItem();
|
|
3391
|
+
if (next.done) {
|
|
3392
|
+
isDone = true;
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
consecutiveErrors = 0;
|
|
3396
|
+
item = next.value;
|
|
3397
|
+
} catch (err) {
|
|
3398
|
+
consecutiveErrors++;
|
|
3399
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3400
|
+
errors.push(`Extraction stream error: ${errMsg}`);
|
|
3401
|
+
const cleanErr = errMsg.includes("429") ? "rate limited by platform (HTTP 429)" : errMsg.length > 50 ? `${errMsg.slice(0, 47)}\u2026` : errMsg;
|
|
3402
|
+
onProgress?.({
|
|
3403
|
+
job: jobName,
|
|
3404
|
+
downloadedCount: downloaded,
|
|
3405
|
+
skippedCount: skipped,
|
|
3406
|
+
currentFile: "",
|
|
3407
|
+
statusText: `Extractor error: ${cleanErr}`
|
|
3408
|
+
});
|
|
3409
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS || signal?.aborted) {
|
|
3410
|
+
isDone = true;
|
|
3411
|
+
break;
|
|
2649
3412
|
}
|
|
3413
|
+
continue;
|
|
3414
|
+
}
|
|
3415
|
+
const ext = item.extension.replace(/^\./, "") || (item.type === "video" ? "mp4" : "jpg");
|
|
3416
|
+
const finalFilename = sanitizeFilename(item.filename, item.id, ext);
|
|
3417
|
+
const finalPath = path5.join(destDir, finalFilename);
|
|
3418
|
+
if (archive.has(item.id) && fs6.existsSync(finalPath)) {
|
|
3419
|
+
skipped++;
|
|
3420
|
+
onProgress?.({
|
|
3421
|
+
job: jobName,
|
|
3422
|
+
downloadedCount: downloaded,
|
|
3423
|
+
skippedCount: skipped,
|
|
3424
|
+
currentFile: finalFilename,
|
|
3425
|
+
statusText: `Skipping already downloaded: ${finalFilename}`
|
|
3426
|
+
});
|
|
3427
|
+
continue;
|
|
2650
3428
|
}
|
|
2651
|
-
await fsPromises2.rename(tmpPath, finalPath);
|
|
2652
|
-
archive.add(item.id);
|
|
2653
|
-
downloaded++;
|
|
2654
3429
|
onProgress?.({
|
|
2655
3430
|
job: jobName,
|
|
2656
3431
|
downloadedCount: downloaded,
|
|
2657
3432
|
skippedCount: skipped,
|
|
2658
3433
|
currentFile: finalFilename,
|
|
2659
|
-
statusText: `
|
|
3434
|
+
statusText: `Downloading: ${finalFilename}`
|
|
2660
3435
|
});
|
|
2661
|
-
|
|
3436
|
+
const tmpPath = `${finalPath}.download`;
|
|
2662
3437
|
try {
|
|
2663
|
-
|
|
2664
|
-
|
|
3438
|
+
const reqHeaders = {};
|
|
3439
|
+
if (options.referer) {
|
|
3440
|
+
reqHeaders["Referer"] = options.referer;
|
|
2665
3441
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
3442
|
+
const response = await httpClient.request(item.url, {
|
|
3443
|
+
signal,
|
|
3444
|
+
cookiePath: options.cookiePath,
|
|
3445
|
+
headers: reqHeaders
|
|
3446
|
+
});
|
|
3447
|
+
if (!response.ok || !response.body) {
|
|
3448
|
+
try {
|
|
3449
|
+
await response.body?.cancel();
|
|
3450
|
+
} catch {
|
|
3451
|
+
}
|
|
3452
|
+
throw new Error(`HTTP ${response.status} (${response.statusText}) for ${item.url}`);
|
|
3453
|
+
}
|
|
3454
|
+
const fileStream = fs6.createWriteStream(tmpPath);
|
|
3455
|
+
await pipeline(Readable.fromWeb(response.body), fileStream, { signal });
|
|
3456
|
+
if (item.date) {
|
|
3457
|
+
try {
|
|
3458
|
+
const mtime = new Date(item.date * 1e3);
|
|
3459
|
+
await fsPromises2.utimes(tmpPath, mtime, mtime);
|
|
3460
|
+
} catch {
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
await fsPromises2.rename(tmpPath, finalPath);
|
|
3464
|
+
archive.add(item.id);
|
|
3465
|
+
downloaded++;
|
|
3466
|
+
onProgress?.({
|
|
3467
|
+
job: jobName,
|
|
3468
|
+
downloadedCount: downloaded,
|
|
3469
|
+
skippedCount: skipped,
|
|
3470
|
+
currentFile: finalFilename,
|
|
3471
|
+
statusText: `Saved: ${finalFilename}`
|
|
3472
|
+
});
|
|
3473
|
+
} catch (err) {
|
|
3474
|
+
try {
|
|
3475
|
+
if (fs6.existsSync(tmpPath)) {
|
|
3476
|
+
await fsPromises2.unlink(tmpPath);
|
|
3477
|
+
}
|
|
3478
|
+
} catch {
|
|
3479
|
+
}
|
|
3480
|
+
if (signal?.aborted) {
|
|
3481
|
+
break;
|
|
3482
|
+
}
|
|
3483
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3484
|
+
errors.push(`${finalFilename}: ${errMsg}`);
|
|
3485
|
+
onProgress?.({
|
|
3486
|
+
job: jobName,
|
|
3487
|
+
downloadedCount: downloaded,
|
|
3488
|
+
skippedCount: skipped,
|
|
3489
|
+
currentFile: finalFilename,
|
|
3490
|
+
statusText: `Failed: ${finalFilename} (${errMsg})`
|
|
3491
|
+
});
|
|
2670
3492
|
}
|
|
2671
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2672
|
-
errors.push(`${finalFilename}: ${errMsg}`);
|
|
2673
|
-
onProgress?.({
|
|
2674
|
-
job: jobName,
|
|
2675
|
-
downloadedCount: downloaded,
|
|
2676
|
-
skippedCount: skipped,
|
|
2677
|
-
currentFile: finalFilename,
|
|
2678
|
-
statusText: `Failed: ${finalFilename} (${errMsg})`
|
|
2679
|
-
});
|
|
2680
3493
|
}
|
|
2681
3494
|
}
|
|
3495
|
+
const workers = Array.from({ length: concurrency }, () => downloadWorker());
|
|
3496
|
+
await Promise.all(workers);
|
|
2682
3497
|
await saveArchive(destDir, archive);
|
|
2683
3498
|
return { downloaded, skipped, errors };
|
|
2684
3499
|
}
|
|
@@ -2989,8 +3804,8 @@ var GDL_ARCHIVE = "archive_gallery-dl.sqlite3";
|
|
|
2989
3804
|
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
2990
3805
|
var PLATFORM_SLEEP = {
|
|
2991
3806
|
tiktok: "0.6-1.2",
|
|
2992
|
-
instagram: "
|
|
2993
|
-
facebook: "1.
|
|
3807
|
+
instagram: "1.0-1.8",
|
|
3808
|
+
facebook: "1.0-1.8"
|
|
2994
3809
|
};
|
|
2995
3810
|
function buildGalleryDlArgs(options) {
|
|
2996
3811
|
const { destDir, subDir, platform, cleanUrl, username, cookiePath } = options;
|
|
@@ -3030,12 +3845,12 @@ function buildGalleryDlArgs(options) {
|
|
|
3030
3845
|
} else if (subDir === "Stories") {
|
|
3031
3846
|
if (platform === "instagram") {
|
|
3032
3847
|
targetUrl = `https://www.instagram.com/stories/${encodeURIComponent(username)}/`;
|
|
3033
|
-
args.push("--sleep-request", "
|
|
3848
|
+
args.push("--sleep-request", "1.0-1.8");
|
|
3034
3849
|
}
|
|
3035
3850
|
} else if (subDir === "Highlights") {
|
|
3036
3851
|
if (platform === "instagram") {
|
|
3037
3852
|
targetUrl = `https://www.instagram.com/${encodeURIComponent(username)}/highlights/`;
|
|
3038
|
-
args.push("--sleep-request", "
|
|
3853
|
+
args.push("--sleep-request", "1.0-1.8");
|
|
3039
3854
|
}
|
|
3040
3855
|
}
|
|
3041
3856
|
if (cookiePath) {
|
|
@@ -3059,8 +3874,9 @@ async function runGalleryDl(options) {
|
|
|
3059
3874
|
let downloadedCount = 0;
|
|
3060
3875
|
let skippedCount = 0;
|
|
3061
3876
|
let errorOutput = "";
|
|
3877
|
+
let engineCmdStr = "gallery-dl";
|
|
3062
3878
|
try {
|
|
3063
|
-
await ensureGalleryDl((status) => {
|
|
3879
|
+
engineCmdStr = await ensureGalleryDl((status) => {
|
|
3064
3880
|
onProgress?.({
|
|
3065
3881
|
job: subDir,
|
|
3066
3882
|
downloadedCount: 0,
|
|
@@ -3076,9 +3892,15 @@ async function runGalleryDl(options) {
|
|
|
3076
3892
|
error: err.message || String(err)
|
|
3077
3893
|
};
|
|
3078
3894
|
}
|
|
3079
|
-
const engineCmd = resolveEngineCommand("gallery-dl");
|
|
3895
|
+
const engineCmd = engineCmdStr.includes(path8.sep) ? { command: engineCmdStr, baseArgs: [] } : resolveEngineCommand("gallery-dl");
|
|
3080
3896
|
const fullArgs = [...engineCmd.baseArgs, ...args];
|
|
3081
3897
|
return new Promise((resolve) => {
|
|
3898
|
+
onProgress?.({
|
|
3899
|
+
job: subDir,
|
|
3900
|
+
downloadedCount,
|
|
3901
|
+
skippedCount,
|
|
3902
|
+
statusText: "downloading with gallery-dl\u2026"
|
|
3903
|
+
});
|
|
3082
3904
|
const child = spawn2(engineCmd.command, fullArgs, {
|
|
3083
3905
|
windowsHide: true,
|
|
3084
3906
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -3202,8 +4024,9 @@ async function runYtDlp(options) {
|
|
|
3202
4024
|
let downloadedCount = 0;
|
|
3203
4025
|
let skippedCount = 0;
|
|
3204
4026
|
let errorOutput = "";
|
|
4027
|
+
let engineCmdStr = "yt-dlp";
|
|
3205
4028
|
try {
|
|
3206
|
-
await ensureYtDlp((status) => {
|
|
4029
|
+
engineCmdStr = await ensureYtDlp((status) => {
|
|
3207
4030
|
onProgress?.({
|
|
3208
4031
|
job: "Videos",
|
|
3209
4032
|
downloadedCount: 0,
|
|
@@ -3219,9 +4042,15 @@ async function runYtDlp(options) {
|
|
|
3219
4042
|
error: err.message || String(err)
|
|
3220
4043
|
};
|
|
3221
4044
|
}
|
|
3222
|
-
const engineCmd = resolveEngineCommand("yt-dlp");
|
|
4045
|
+
const engineCmd = engineCmdStr.includes(path9.sep) ? { command: engineCmdStr, baseArgs: [] } : resolveEngineCommand("yt-dlp");
|
|
3223
4046
|
const fullArgs = [...engineCmd.baseArgs, ...args];
|
|
3224
4047
|
return new Promise((resolve) => {
|
|
4048
|
+
onProgress?.({
|
|
4049
|
+
job: "Videos",
|
|
4050
|
+
downloadedCount,
|
|
4051
|
+
skippedCount,
|
|
4052
|
+
statusText: "downloading with yt-dlp\u2026"
|
|
4053
|
+
});
|
|
3225
4054
|
const child = spawn3(engineCmd.command, fullArgs, {
|
|
3226
4055
|
windowsHide: true,
|
|
3227
4056
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -3313,14 +4142,14 @@ function resolveDownloadBaseDir(customBaseDir) {
|
|
|
3313
4142
|
if (customBaseDir && customBaseDir.trim()) {
|
|
3314
4143
|
return path10.resolve(expandPath(customBaseDir.trim()));
|
|
3315
4144
|
}
|
|
3316
|
-
const configDir = loadConfig().downloadDir;
|
|
3317
|
-
if (configDir && configDir.trim()) {
|
|
3318
|
-
return path10.resolve(expandPath(configDir.trim()));
|
|
3319
|
-
}
|
|
3320
4145
|
const envDir = process.env.OPEN_NAMI_DIR || process.env.NAMI_DIR;
|
|
3321
4146
|
if (envDir && envDir.trim()) {
|
|
3322
4147
|
return path10.resolve(expandPath(envDir.trim()));
|
|
3323
4148
|
}
|
|
4149
|
+
const configDir = loadConfig().downloadDir;
|
|
4150
|
+
if (configDir && configDir.trim()) {
|
|
4151
|
+
return path10.resolve(expandPath(configDir.trim()));
|
|
4152
|
+
}
|
|
3324
4153
|
if (process.env.NAMI_BASE_DIR && process.env.NAMI_BASE_DIR.trim()) {
|
|
3325
4154
|
const raw = process.env.NAMI_BASE_DIR.trim();
|
|
3326
4155
|
return path10.resolve(expandPath(raw.endsWith("downloads") ? raw : path10.join(raw, "downloads")));
|
|
@@ -3358,7 +4187,7 @@ async function dispatchDownload(options) {
|
|
|
3358
4187
|
jobs.push({ type: "highlights" });
|
|
3359
4188
|
}
|
|
3360
4189
|
}
|
|
3361
|
-
const
|
|
4190
|
+
const defaultRunNative = async (subDir) => {
|
|
3362
4191
|
const dest = path10.join(targetDir, subDir);
|
|
3363
4192
|
try {
|
|
3364
4193
|
const extractor = getExtractor(profile.platform);
|
|
@@ -3391,27 +4220,34 @@ async function dispatchDownload(options) {
|
|
|
3391
4220
|
};
|
|
3392
4221
|
}
|
|
3393
4222
|
};
|
|
3394
|
-
const
|
|
4223
|
+
const runNative = options.runnerOverrides?.nativeRunner ?? defaultRunNative;
|
|
4224
|
+
const runGdl = options.runnerOverrides?.gdlRunner ?? ((subDir) => runGalleryDl({
|
|
3395
4225
|
profile,
|
|
3396
4226
|
targetDir,
|
|
3397
4227
|
subDir,
|
|
3398
4228
|
cookiePath: cookieCopy?.filePath,
|
|
3399
4229
|
signal: options.signal,
|
|
3400
4230
|
onProgress
|
|
3401
|
-
});
|
|
3402
|
-
const runYt = () => runYtDlp({
|
|
4231
|
+
}));
|
|
4232
|
+
const runYt = options.runnerOverrides?.ytRunner ?? (() => runYtDlp({
|
|
3403
4233
|
profile,
|
|
3404
4234
|
targetDir,
|
|
3405
4235
|
cookiePath: cookieCopy?.filePath,
|
|
3406
4236
|
signal: options.signal,
|
|
3407
4237
|
onProgress
|
|
3408
|
-
});
|
|
4238
|
+
}));
|
|
3409
4239
|
for (const job of jobs) {
|
|
3410
4240
|
if (options.signal?.aborted) break;
|
|
3411
4241
|
if (job.type === "photos" || job.type === "stories" || job.type === "highlights") {
|
|
3412
4242
|
const subDir = job.type === "photos" ? "Photos" : job.type === "stories" ? "Stories" : "Highlights";
|
|
3413
4243
|
let res = await runNative(subDir);
|
|
3414
4244
|
if ((!res.success || res.downloaded === 0 && res.skipped === 0) && !options.signal?.aborted) {
|
|
4245
|
+
onProgress?.({
|
|
4246
|
+
job: subDir,
|
|
4247
|
+
downloadedCount: totalDownloaded,
|
|
4248
|
+
skippedCount: totalSkipped,
|
|
4249
|
+
statusText: "switching to gallery-dl engine\u2026"
|
|
4250
|
+
});
|
|
3415
4251
|
const gdlRes = await runGdl(subDir);
|
|
3416
4252
|
if (gdlRes.downloaded > 0 || gdlRes.skipped > 0 || gdlRes.success) {
|
|
3417
4253
|
res = gdlRes;
|
|
@@ -3426,12 +4262,24 @@ async function dispatchDownload(options) {
|
|
|
3426
4262
|
totalDownloaded += res.downloaded;
|
|
3427
4263
|
totalSkipped += res.skipped;
|
|
3428
4264
|
} else if (!options.signal?.aborted) {
|
|
4265
|
+
onProgress?.({
|
|
4266
|
+
job: "Videos",
|
|
4267
|
+
downloadedCount: totalDownloaded,
|
|
4268
|
+
skippedCount: totalSkipped,
|
|
4269
|
+
statusText: "switching to yt-dlp engine\u2026"
|
|
4270
|
+
});
|
|
3429
4271
|
const fbRes = await runYt();
|
|
3430
4272
|
if (fbRes.downloaded > 0 || fbRes.skipped > 0 || fbRes.success) {
|
|
3431
4273
|
totalDownloaded += fbRes.downloaded;
|
|
3432
4274
|
totalSkipped += fbRes.skipped;
|
|
3433
4275
|
if (!fbRes.success && fbRes.error) errors.push(`[Videos] ${fbRes.error}`);
|
|
3434
4276
|
} else if (!options.signal?.aborted) {
|
|
4277
|
+
onProgress?.({
|
|
4278
|
+
job: "Videos",
|
|
4279
|
+
downloadedCount: totalDownloaded,
|
|
4280
|
+
skippedCount: totalSkipped,
|
|
4281
|
+
statusText: "switching to gallery-dl engine\u2026"
|
|
4282
|
+
});
|
|
3435
4283
|
const gdlRes = await runGdl("Videos");
|
|
3436
4284
|
totalDownloaded += gdlRes.downloaded;
|
|
3437
4285
|
totalSkipped += gdlRes.skipped;
|