mcp-scraper 0.40.1 → 0.40.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/api-server.cjs +60220 -0
- package/dist/bin/api-server.cjs.map +1 -0
- package/dist/bin/api-server.d.cts +1 -0
- package/dist/bin/api-server.d.ts +1 -0
- package/dist/bin/api-server.js +38 -0
- package/dist/bin/api-server.js.map +1 -0
- package/dist/bin/mcp-scraper-cli.cjs +2671 -0
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -0
- package/dist/bin/mcp-scraper-cli.d.cts +1 -0
- package/dist/bin/mcp-scraper-cli.d.ts +1 -0
- package/dist/bin/mcp-scraper-cli.js +742 -0
- package/dist/bin/mcp-scraper-cli.js.map +1 -0
- package/dist/bin/mcp-scraper-install.cjs +129 -0
- package/dist/bin/mcp-scraper-install.cjs.map +1 -0
- package/dist/bin/mcp-scraper-install.d.cts +1 -0
- package/dist/bin/mcp-scraper-install.d.ts +1 -0
- package/dist/bin/mcp-scraper-install.js +27 -0
- package/dist/bin/mcp-scraper-install.js.map +1 -0
- package/dist/bin/mcp-stdio-server.cjs +12532 -0
- package/dist/bin/mcp-stdio-server.cjs.map +1 -0
- package/dist/bin/mcp-stdio-server.d.cts +1 -0
- package/dist/bin/mcp-stdio-server.d.ts +1 -0
- package/dist/bin/mcp-stdio-server.js +135 -0
- package/dist/bin/mcp-stdio-server.js.map +1 -0
- package/dist/bin/paa-harvest.cjs +3808 -0
- package/dist/bin/paa-harvest.cjs.map +1 -0
- package/dist/bin/paa-harvest.d.cts +1 -0
- package/dist/bin/paa-harvest.d.ts +1 -0
- package/dist/bin/paa-harvest.js +44 -0
- package/dist/bin/paa-harvest.js.map +1 -0
- package/dist/chunk-345BQXZH.js +712 -0
- package/dist/chunk-345BQXZH.js.map +1 -0
- package/dist/chunk-3PIWJS6Y.js +276 -0
- package/dist/chunk-3PIWJS6Y.js.map +1 -0
- package/dist/chunk-44HZLHDV.js +52 -0
- package/dist/chunk-44HZLHDV.js.map +1 -0
- package/dist/chunk-7XBBFBYY.js +3410 -0
- package/dist/chunk-7XBBFBYY.js.map +1 -0
- package/dist/chunk-CB5C3BPB.js +135 -0
- package/dist/chunk-CB5C3BPB.js.map +1 -0
- package/dist/chunk-FQI5PFE7.js +1866 -0
- package/dist/chunk-FQI5PFE7.js.map +1 -0
- package/dist/chunk-G3P3ZDB4.js +69 -0
- package/dist/chunk-G3P3ZDB4.js.map +1 -0
- package/dist/chunk-GUVKHCKE.js +3050 -0
- package/dist/chunk-GUVKHCKE.js.map +1 -0
- package/dist/chunk-K443GQY5.js +24 -0
- package/dist/chunk-K443GQY5.js.map +1 -0
- package/dist/chunk-LP6E462I.js +11555 -0
- package/dist/chunk-LP6E462I.js.map +1 -0
- package/dist/chunk-NVXNEOUQ.js +158 -0
- package/dist/chunk-NVXNEOUQ.js.map +1 -0
- package/dist/chunk-QZXKQB7Y.js +414 -0
- package/dist/chunk-QZXKQB7Y.js.map +1 -0
- package/dist/chunk-SBLGBZZB.js +647 -0
- package/dist/chunk-SBLGBZZB.js.map +1 -0
- package/dist/chunk-X2LKCX6H.js +499 -0
- package/dist/chunk-X2LKCX6H.js.map +1 -0
- package/dist/chunk-XELSA2MS.js +108 -0
- package/dist/chunk-XELSA2MS.js.map +1 -0
- package/dist/chunk-XORPNO3Z.js +684 -0
- package/dist/chunk-XORPNO3Z.js.map +1 -0
- package/dist/chunk-ZUJLSICT.js +7 -0
- package/dist/chunk-ZUJLSICT.js.map +1 -0
- package/dist/db-W3CP562I.js +239 -0
- package/dist/db-W3CP562I.js.map +1 -0
- package/dist/editorial-reading-room/assets/app.js +335 -0
- package/dist/editorial-reading-room/assets/index.html +131 -0
- package/dist/editorial-reading-room/assets/styles.css +1052 -0
- package/dist/extract-bundle-K4PG3RZJ.js +568 -0
- package/dist/extract-bundle-K4PG3RZJ.js.map +1 -0
- package/dist/index.cjs +4160 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +413 -0
- package/dist/index.d.ts +413 -0
- package/dist/index.js +338 -0
- package/dist/index.js.map +1 -0
- package/dist/location-data-repository-RLQX6SNM.js +35 -0
- package/dist/location-data-repository-RLQX6SNM.js.map +1 -0
- package/dist/server-KM3CFWCF.js +34674 -0
- package/dist/server-KM3CFWCF.js.map +1 -0
- package/dist/site-extract-repository-2SMMFKKL.js +62 -0
- package/dist/site-extract-repository-2SMMFKKL.js.map +1 -0
- package/dist/worker-FXAGFYOE.js +142 -0
- package/dist/worker-FXAGFYOE.js.map +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CaptchaError,
|
|
3
|
+
LocationMismatchError,
|
|
4
|
+
RequestAbortedError
|
|
5
|
+
} from "./chunk-44HZLHDV.js";
|
|
6
|
+
import {
|
|
7
|
+
finishHarvestAttempt,
|
|
8
|
+
startHarvestAttempt
|
|
9
|
+
} from "./chunk-GUVKHCKE.js";
|
|
10
|
+
|
|
11
|
+
// src/api/harvest-problems.ts
|
|
12
|
+
function errorMessage(err) {
|
|
13
|
+
return err instanceof Error ? err.message : String(err);
|
|
14
|
+
}
|
|
15
|
+
function looksLikeTimeout(err, message) {
|
|
16
|
+
if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) return true;
|
|
17
|
+
return /timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message);
|
|
18
|
+
}
|
|
19
|
+
function looksLikeCaptcha(message) {
|
|
20
|
+
return /captcha|recaptcha|unusual traffic|google\.com\/sorry|blocked/i.test(message);
|
|
21
|
+
}
|
|
22
|
+
function looksLikeProxyTunnelFailure(message) {
|
|
23
|
+
return /ERR_TUNNEL_CONNECTION_FAILED|ERR_PROXY_CONNECTION_FAILED|ERR_SOCKS_CONNECTION_FAILED|tunnel connection failed|proxy connection failed|transport error: proxy/i.test(message);
|
|
24
|
+
}
|
|
25
|
+
function looksLikeProxyUnavailable(message) {
|
|
26
|
+
return /proxy unavailable|proxy_unavailable|connection_test_failed|did not return a proxy id|configured fallback/i.test(message);
|
|
27
|
+
}
|
|
28
|
+
function classifyHarvestProblem(err) {
|
|
29
|
+
const message = errorMessage(err);
|
|
30
|
+
if (err instanceof RequestAbortedError) {
|
|
31
|
+
return {
|
|
32
|
+
error_code: "request_aborted",
|
|
33
|
+
error_type: "request_aborted",
|
|
34
|
+
message,
|
|
35
|
+
retryable: true,
|
|
36
|
+
httpStatus: 408,
|
|
37
|
+
terminalStatus: "cancelled"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (err instanceof CaptchaError || looksLikeCaptcha(message)) {
|
|
41
|
+
return {
|
|
42
|
+
error_code: "captcha_exhausted",
|
|
43
|
+
error_type: "captcha",
|
|
44
|
+
message,
|
|
45
|
+
retryable: true,
|
|
46
|
+
httpStatus: 503,
|
|
47
|
+
terminalStatus: "failed"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (err instanceof LocationMismatchError) {
|
|
51
|
+
return {
|
|
52
|
+
error_code: "location_mismatch",
|
|
53
|
+
error_type: "location_mismatch",
|
|
54
|
+
message,
|
|
55
|
+
retryable: true,
|
|
56
|
+
httpStatus: 503,
|
|
57
|
+
terminalStatus: "failed"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (looksLikeProxyTunnelFailure(message)) {
|
|
61
|
+
return {
|
|
62
|
+
error_code: "proxy_tunnel_failed",
|
|
63
|
+
error_type: "proxy_tunnel_failed",
|
|
64
|
+
message,
|
|
65
|
+
retryable: true,
|
|
66
|
+
httpStatus: 503,
|
|
67
|
+
terminalStatus: "failed"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (looksLikeProxyUnavailable(message)) {
|
|
71
|
+
return {
|
|
72
|
+
error_code: "proxy_unavailable",
|
|
73
|
+
error_type: "proxy_unavailable",
|
|
74
|
+
message,
|
|
75
|
+
retryable: true,
|
|
76
|
+
httpStatus: 503,
|
|
77
|
+
terminalStatus: "failed"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (looksLikeTimeout(err, message)) {
|
|
81
|
+
return {
|
|
82
|
+
error_code: "harvest_timeout",
|
|
83
|
+
error_type: "timeout",
|
|
84
|
+
message,
|
|
85
|
+
retryable: true,
|
|
86
|
+
httpStatus: 504,
|
|
87
|
+
terminalStatus: "failed"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
error_code: "extraction_failed",
|
|
92
|
+
error_type: "extraction",
|
|
93
|
+
message,
|
|
94
|
+
retryable: false,
|
|
95
|
+
httpStatus: 500,
|
|
96
|
+
terminalStatus: "failed"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function serializeHarvestProblem(problem) {
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
error_code: problem.error_code,
|
|
102
|
+
error_type: problem.error_type,
|
|
103
|
+
message: problem.message,
|
|
104
|
+
retryable: problem.retryable
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function harvestProblemResponse(problem) {
|
|
108
|
+
return {
|
|
109
|
+
error: problem.message,
|
|
110
|
+
error_code: problem.error_code,
|
|
111
|
+
error_type: problem.error_type,
|
|
112
|
+
retryable: problem.retryable
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/api/harvest-attempt-events.ts
|
|
117
|
+
function createHarvestAttemptRecorder(jobId, userId) {
|
|
118
|
+
return async (event) => {
|
|
119
|
+
if (event.type === "started") {
|
|
120
|
+
await startHarvestAttempt({
|
|
121
|
+
jobId,
|
|
122
|
+
userId,
|
|
123
|
+
attemptNumber: event.attemptNumber,
|
|
124
|
+
maxAttempts: event.maxAttempts,
|
|
125
|
+
query: event.query,
|
|
126
|
+
location: event.location,
|
|
127
|
+
maxQuestions: event.maxQuestions,
|
|
128
|
+
startedAt: event.startedAt
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await finishHarvestAttempt({
|
|
133
|
+
jobId,
|
|
134
|
+
attemptNumber: event.attemptNumber,
|
|
135
|
+
outcome: event.outcome,
|
|
136
|
+
kernelSessionId: event.kernelSessionId,
|
|
137
|
+
questionCount: event.questionCount,
|
|
138
|
+
durationMs: event.durationMs,
|
|
139
|
+
error: event.error,
|
|
140
|
+
willRetry: event.willRetry,
|
|
141
|
+
kernelDeleteStarted: event.cleanup.kernelDeleteStarted,
|
|
142
|
+
kernelDeleteSucceeded: event.cleanup.kernelDeleteSucceeded,
|
|
143
|
+
kernelDeleteError: event.cleanup.kernelDeleteError,
|
|
144
|
+
browserCloseSucceeded: event.cleanup.browserCloseSucceeded,
|
|
145
|
+
browserCloseError: event.cleanup.browserCloseError,
|
|
146
|
+
debug: event.debug,
|
|
147
|
+
completedAt: event.completedAt
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
classifyHarvestProblem,
|
|
154
|
+
serializeHarvestProblem,
|
|
155
|
+
harvestProblemResponse,
|
|
156
|
+
createHarvestAttemptRecorder
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=chunk-NVXNEOUQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api/harvest-problems.ts","../src/api/harvest-attempt-events.ts"],"sourcesContent":["import { CaptchaError, LocationMismatchError, RequestAbortedError } from '../errors.js'\n\nexport type HarvestProblemCode =\n | 'request_aborted'\n | 'captcha_exhausted'\n | 'location_mismatch'\n | 'proxy_tunnel_failed'\n | 'proxy_unavailable'\n | 'harvest_timeout'\n | 'extraction_failed'\n\nexport interface HarvestProblem {\n error_code: HarvestProblemCode\n error_type: string\n message: string\n retryable: boolean\n httpStatus: number\n terminalStatus: 'cancelled' | 'failed'\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nfunction looksLikeTimeout(err: unknown, message: string): boolean {\n if (err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError')) return true\n return /timeout|timed out|Timeout \\d+ms exceeded|deadline/i.test(message)\n}\n\nfunction looksLikeCaptcha(message: string): boolean {\n return /captcha|recaptcha|unusual traffic|google\\.com\\/sorry|blocked/i.test(message)\n}\n\nfunction looksLikeProxyTunnelFailure(message: string): boolean {\n return /ERR_TUNNEL_CONNECTION_FAILED|ERR_PROXY_CONNECTION_FAILED|ERR_SOCKS_CONNECTION_FAILED|tunnel connection failed|proxy connection failed|transport error: proxy/i.test(message)\n}\n\nfunction looksLikeProxyUnavailable(message: string): boolean {\n return /proxy unavailable|proxy_unavailable|connection_test_failed|did not return a proxy id|configured fallback/i.test(message)\n}\n\nexport function classifyHarvestProblem(err: unknown): HarvestProblem {\n const message = errorMessage(err)\n\n if (err instanceof RequestAbortedError) {\n return {\n error_code: 'request_aborted',\n error_type: 'request_aborted',\n message,\n retryable: true,\n httpStatus: 408,\n terminalStatus: 'cancelled',\n }\n }\n\n if (err instanceof CaptchaError || looksLikeCaptcha(message)) {\n return {\n error_code: 'captcha_exhausted',\n error_type: 'captcha',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (err instanceof LocationMismatchError) {\n return {\n error_code: 'location_mismatch',\n error_type: 'location_mismatch',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (looksLikeProxyTunnelFailure(message)) {\n return {\n error_code: 'proxy_tunnel_failed',\n error_type: 'proxy_tunnel_failed',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (looksLikeProxyUnavailable(message)) {\n return {\n error_code: 'proxy_unavailable',\n error_type: 'proxy_unavailable',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (looksLikeTimeout(err, message)) {\n return {\n error_code: 'harvest_timeout',\n error_type: 'timeout',\n message,\n retryable: true,\n httpStatus: 504,\n terminalStatus: 'failed',\n }\n }\n\n return {\n error_code: 'extraction_failed',\n error_type: 'extraction',\n message,\n retryable: false,\n httpStatus: 500,\n terminalStatus: 'failed',\n }\n}\n\nexport function serializeHarvestProblem(problem: HarvestProblem): string {\n return JSON.stringify({\n error_code: problem.error_code,\n error_type: problem.error_type,\n message: problem.message,\n retryable: problem.retryable,\n })\n}\n\nexport function harvestProblemResponse(problem: HarvestProblem): {\n error: string\n error_code: HarvestProblemCode\n error_type: string\n retryable: boolean\n} {\n return {\n error: problem.message,\n error_code: problem.error_code,\n error_type: problem.error_type,\n retryable: problem.retryable,\n }\n}\n","import type { HarvestAttemptLogEvent } from '../harvest.js'\nimport { finishHarvestAttempt, startHarvestAttempt } from './db.js'\n\nexport function createHarvestAttemptRecorder(jobId: string, userId: number | bigint) {\n return async (event: HarvestAttemptLogEvent): Promise<void> => {\n if (event.type === 'started') {\n await startHarvestAttempt({\n jobId,\n userId,\n attemptNumber: event.attemptNumber,\n maxAttempts: event.maxAttempts,\n query: event.query,\n location: event.location,\n maxQuestions: event.maxQuestions,\n startedAt: event.startedAt,\n })\n return\n }\n\n await finishHarvestAttempt({\n jobId,\n attemptNumber: event.attemptNumber,\n outcome: event.outcome,\n kernelSessionId: event.kernelSessionId,\n questionCount: event.questionCount,\n durationMs: event.durationMs,\n error: event.error,\n willRetry: event.willRetry,\n kernelDeleteStarted: event.cleanup.kernelDeleteStarted,\n kernelDeleteSucceeded: event.cleanup.kernelDeleteSucceeded,\n kernelDeleteError: event.cleanup.kernelDeleteError,\n browserCloseSucceeded: event.cleanup.browserCloseSucceeded,\n browserCloseError: event.cleanup.browserCloseError,\n debug: event.debug,\n completedAt: event.completedAt,\n })\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPrivateArtifactFromStream,
|
|
3
|
+
renewPrivateArtifactDownload,
|
|
4
|
+
validatePublicHttpUrl
|
|
5
|
+
} from "./chunk-345BQXZH.js";
|
|
6
|
+
|
|
7
|
+
// src/lib/media-extractor.ts
|
|
8
|
+
import { createWriteStream, mkdirSync, rmSync } from "fs";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { join, extname, basename } from "path";
|
|
11
|
+
import { pipeline } from "stream/promises";
|
|
12
|
+
import { Readable, Transform } from "stream";
|
|
13
|
+
var AD_PATTERNS = [
|
|
14
|
+
"doubleclick.net",
|
|
15
|
+
"googlesyndication.com",
|
|
16
|
+
"googletagmanager.com",
|
|
17
|
+
"google-analytics.com",
|
|
18
|
+
"googletagservices.com",
|
|
19
|
+
"adservice.google",
|
|
20
|
+
"googletag.",
|
|
21
|
+
"pagead2.googlesyndication",
|
|
22
|
+
"facebook.net/tr",
|
|
23
|
+
"connect.facebook.net",
|
|
24
|
+
"fbcdn.net/rsrc",
|
|
25
|
+
"analytics.twitter.com",
|
|
26
|
+
"static.ads-twitter.com",
|
|
27
|
+
"ads.twitter.com",
|
|
28
|
+
"t.co/i/adsct",
|
|
29
|
+
"pixel.advertising.com",
|
|
30
|
+
"hotjar.com",
|
|
31
|
+
"clarity.ms",
|
|
32
|
+
"quantserve.com",
|
|
33
|
+
"scorecardresearch.com",
|
|
34
|
+
"newrelic.com",
|
|
35
|
+
"nr-data.net",
|
|
36
|
+
"segment.io",
|
|
37
|
+
"segment.com",
|
|
38
|
+
"amplitude.com",
|
|
39
|
+
"mixpanel.com",
|
|
40
|
+
"heap.io",
|
|
41
|
+
"fullstory.com",
|
|
42
|
+
"moatads.com",
|
|
43
|
+
"criteo.com",
|
|
44
|
+
"adsrvr.org",
|
|
45
|
+
"rubiconproject.com",
|
|
46
|
+
"pubmatic.com",
|
|
47
|
+
"openx.net",
|
|
48
|
+
"appnexus.com",
|
|
49
|
+
"amazon-adsystem.com",
|
|
50
|
+
"media.net",
|
|
51
|
+
"yieldmo.com",
|
|
52
|
+
"triplelift.com",
|
|
53
|
+
"sharethrough.com",
|
|
54
|
+
"prebid.",
|
|
55
|
+
"smaato.net",
|
|
56
|
+
"indexworm.com",
|
|
57
|
+
"casalemedia.com",
|
|
58
|
+
"outbrain.com",
|
|
59
|
+
"taboola.com",
|
|
60
|
+
"revcontent.com",
|
|
61
|
+
"mgid.com",
|
|
62
|
+
"tawk.to",
|
|
63
|
+
"intercom.io",
|
|
64
|
+
"drift.com",
|
|
65
|
+
"hs-scripts.com",
|
|
66
|
+
"zopim.com",
|
|
67
|
+
"livechatinc.com",
|
|
68
|
+
"userlike.com",
|
|
69
|
+
"onetrust.com",
|
|
70
|
+
"cookielaw.org",
|
|
71
|
+
"cookieinformation.com",
|
|
72
|
+
"trustarc.com",
|
|
73
|
+
"/ads/",
|
|
74
|
+
"/ad/",
|
|
75
|
+
"/banner/",
|
|
76
|
+
"/banners/",
|
|
77
|
+
"/pixel/",
|
|
78
|
+
"/beacon/",
|
|
79
|
+
"/tracking/",
|
|
80
|
+
"/tracker/",
|
|
81
|
+
"/remarketing/",
|
|
82
|
+
"/conversion/",
|
|
83
|
+
"1x1.gif",
|
|
84
|
+
"spacer.gif",
|
|
85
|
+
"blank.gif",
|
|
86
|
+
"transparent.gif"
|
|
87
|
+
];
|
|
88
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif", ".svg", ".tiff"]);
|
|
89
|
+
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".avi", ".m4v", ".ogv", ".mkv"]);
|
|
90
|
+
var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".aac", ".m4a", ".flac", ".opus"]);
|
|
91
|
+
function isAdUrl(url) {
|
|
92
|
+
const lower = url.toLowerCase();
|
|
93
|
+
return AD_PATTERNS.some((p) => lower.includes(p));
|
|
94
|
+
}
|
|
95
|
+
function isDataUri(url) {
|
|
96
|
+
return url.startsWith("data:");
|
|
97
|
+
}
|
|
98
|
+
function typeFromUrl(url) {
|
|
99
|
+
try {
|
|
100
|
+
const ext = extname(new URL(url).pathname).toLowerCase();
|
|
101
|
+
if (IMAGE_EXTS.has(ext)) return "image";
|
|
102
|
+
if (VIDEO_EXTS.has(ext)) return "video";
|
|
103
|
+
if (AUDIO_EXTS.has(ext)) return "audio";
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function typeFromMime(mime) {
|
|
109
|
+
const lower = mime.toLowerCase();
|
|
110
|
+
if (lower.startsWith("image/")) return "image";
|
|
111
|
+
if (lower.startsWith("video/")) return "video";
|
|
112
|
+
if (lower.startsWith("audio/")) return "audio";
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function resolveUrl(raw, base) {
|
|
116
|
+
if (!raw || isDataUri(raw)) return null;
|
|
117
|
+
try {
|
|
118
|
+
return new URL(raw, base).href;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function safeFilename(url, index) {
|
|
124
|
+
try {
|
|
125
|
+
const u = new URL(url);
|
|
126
|
+
const base = basename(u.pathname).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
127
|
+
return base || `asset-${index}`;
|
|
128
|
+
} catch {
|
|
129
|
+
return `asset-${index}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function extractMediaUrls(html, baseUrl) {
|
|
133
|
+
const seen = /* @__PURE__ */ new Set();
|
|
134
|
+
const urls = [];
|
|
135
|
+
const add = (raw) => {
|
|
136
|
+
const resolved = resolveUrl(raw.trim(), baseUrl);
|
|
137
|
+
if (!resolved || seen.has(resolved) || isAdUrl(resolved)) return;
|
|
138
|
+
seen.add(resolved);
|
|
139
|
+
urls.push(resolved);
|
|
140
|
+
};
|
|
141
|
+
for (const m of html.matchAll(/<img\s[^>]*>/gi)) {
|
|
142
|
+
const tag = m[0];
|
|
143
|
+
for (const attr of ["src", "data-src", "data-lazy-src", "data-original"]) {
|
|
144
|
+
const v = (tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`, "i")) ?? [])[1];
|
|
145
|
+
if (v) add(v);
|
|
146
|
+
}
|
|
147
|
+
const srcset = (tag.match(/\bsrcset\s*=\s*["']([^"']+)["']/i) ?? [])[1];
|
|
148
|
+
if (srcset) {
|
|
149
|
+
for (const part of srcset.split(",")) {
|
|
150
|
+
const u = part.trim().split(/\s+/)[0];
|
|
151
|
+
if (u) add(u);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const m of html.matchAll(/<source\s[^>]*>/gi)) {
|
|
156
|
+
const tag = m[0];
|
|
157
|
+
const src = (tag.match(/\bsrc\s*=\s*["']([^"']+)["']/i) ?? [])[1];
|
|
158
|
+
if (src) add(src);
|
|
159
|
+
const srcset = (tag.match(/\bsrcset\s*=\s*["']([^"']+)["']/i) ?? [])[1];
|
|
160
|
+
if (srcset) {
|
|
161
|
+
for (const part of srcset.split(",")) {
|
|
162
|
+
const u = part.trim().split(/\s+/)[0];
|
|
163
|
+
if (u) add(u);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const m of html.matchAll(/<video\s[^>]*>/gi)) {
|
|
168
|
+
const tag = m[0];
|
|
169
|
+
for (const attr of ["src", "poster"]) {
|
|
170
|
+
const v = (tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`, "i")) ?? [])[1];
|
|
171
|
+
if (v) add(v);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const m of html.matchAll(/<audio\s[^>]*>/gi)) {
|
|
175
|
+
const tag = m[0];
|
|
176
|
+
const v = (tag.match(/\bsrc\s*=\s*["']([^"']+)["']/i) ?? [])[1];
|
|
177
|
+
if (v) add(v);
|
|
178
|
+
}
|
|
179
|
+
for (const m of html.matchAll(/<meta\s[^>]*>/gi)) {
|
|
180
|
+
const tag = m[0];
|
|
181
|
+
const prop = (tag.match(/\b(?:property|name)\s*=\s*["']([^"']+)["']/i) ?? [])[1]?.toLowerCase();
|
|
182
|
+
if (prop === "og:image" || prop === "twitter:image") {
|
|
183
|
+
const content = (tag.match(/\bcontent\s*=\s*["']([^"']+)["']/i) ?? [])[1];
|
|
184
|
+
if (content) add(content);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const m of html.matchAll(/background(?:-image)?\s*:\s*url\(\s*["']?([^"')]+)["']?\s*\)/gi)) {
|
|
188
|
+
add(m[1]);
|
|
189
|
+
}
|
|
190
|
+
return urls;
|
|
191
|
+
}
|
|
192
|
+
async function downloadAsset(url, destDir, filename, options = {}) {
|
|
193
|
+
const maxBytes = Math.max(1, Math.min(options.maxBytes ?? 50 * 1024 * 1024, 100 * 1024 * 1024));
|
|
194
|
+
let target = url;
|
|
195
|
+
let res = null;
|
|
196
|
+
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
197
|
+
const checked = await validatePublicHttpUrl(target, { field: "media URL" });
|
|
198
|
+
if (checked.error || !checked.parsed) throw new Error(checked.error ?? "Media URL was rejected");
|
|
199
|
+
res = await fetch(checked.parsed.href, {
|
|
200
|
+
headers: { "User-Agent": "Mozilla/5.0 (compatible; ThorbitBot/1.0)" },
|
|
201
|
+
signal: AbortSignal.timeout(15e3),
|
|
202
|
+
redirect: "manual"
|
|
203
|
+
});
|
|
204
|
+
if (res.status >= 300 && res.status < 400) {
|
|
205
|
+
const location = res.headers.get("location");
|
|
206
|
+
if (!location) throw new Error(`HTTP ${res.status} redirect did not include Location`);
|
|
207
|
+
target = new URL(location, checked.parsed.href).href;
|
|
208
|
+
res = null;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
if (!res) throw new Error("Media download exceeded five redirects");
|
|
214
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
215
|
+
if (!res.body) throw new Error("Empty response body");
|
|
216
|
+
const mimeType = res.headers.get("content-type")?.split(";")[0].trim() ?? null;
|
|
217
|
+
if (options.expectedType && (!mimeType || typeFromMime(mimeType) !== options.expectedType)) {
|
|
218
|
+
throw new Error(`Expected ${options.expectedType} content but received ${mimeType ?? "no content-type"}`);
|
|
219
|
+
}
|
|
220
|
+
const declaredBytes = Number(res.headers.get("content-length") ?? 0);
|
|
221
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
222
|
+
throw new Error(`Media asset exceeds ${maxBytes} byte limit`);
|
|
223
|
+
}
|
|
224
|
+
let dest = join(destDir, filename);
|
|
225
|
+
if (mimeType && !extname(filename)) {
|
|
226
|
+
const mimeExt = {
|
|
227
|
+
"image/jpeg": ".jpg",
|
|
228
|
+
"image/png": ".png",
|
|
229
|
+
"image/webp": ".webp",
|
|
230
|
+
"image/gif": ".gif",
|
|
231
|
+
"image/svg+xml": ".svg",
|
|
232
|
+
"image/avif": ".avif",
|
|
233
|
+
"video/mp4": ".mp4",
|
|
234
|
+
"video/webm": ".webm",
|
|
235
|
+
"audio/mpeg": ".mp3",
|
|
236
|
+
"audio/ogg": ".ogg",
|
|
237
|
+
"audio/wav": ".wav"
|
|
238
|
+
};
|
|
239
|
+
const ext = mimeExt[mimeType];
|
|
240
|
+
if (ext) dest = dest + ext;
|
|
241
|
+
}
|
|
242
|
+
const writer = createWriteStream(dest);
|
|
243
|
+
await new Promise((resolve, reject) => {
|
|
244
|
+
writer.once("open", () => resolve());
|
|
245
|
+
writer.once("error", reject);
|
|
246
|
+
});
|
|
247
|
+
let streamedBytes = 0;
|
|
248
|
+
const limiter = new Transform({
|
|
249
|
+
transform(chunk, _encoding, callback) {
|
|
250
|
+
const bytes = chunk.length;
|
|
251
|
+
if (streamedBytes + bytes > maxBytes) {
|
|
252
|
+
callback(new Error(`Media asset exceeds ${maxBytes} byte limit`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (options.consumeBytes && !options.consumeBytes(bytes)) {
|
|
256
|
+
callback(new Error("Media export total byte limit exceeded"));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
streamedBytes += bytes;
|
|
260
|
+
callback(null, chunk);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
await pipeline(Readable.fromWeb(res.body), limiter, writer);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
writer.destroy();
|
|
267
|
+
rmSync(dest, { force: true });
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
const { statSync } = await import("fs");
|
|
271
|
+
const sizeBytes = statSync(dest).size;
|
|
272
|
+
return { savedPath: dest, sizeBytes, mimeType };
|
|
273
|
+
}
|
|
274
|
+
async function harvestPageMedia(html, pageUrl, options = {}) {
|
|
275
|
+
const types = options.types ?? ["image", "video", "audio"];
|
|
276
|
+
const typesSet = new Set(types);
|
|
277
|
+
const rawUrls = extractMediaUrls(html, pageUrl);
|
|
278
|
+
const totalFound = rawUrls.length;
|
|
279
|
+
const filteredUrls = [];
|
|
280
|
+
const kept = [];
|
|
281
|
+
for (const url of rawUrls) {
|
|
282
|
+
const type = typeFromUrl(url);
|
|
283
|
+
if (!type || !typesSet.has(type)) {
|
|
284
|
+
filteredUrls.push(url);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
kept.push({ url, type });
|
|
288
|
+
}
|
|
289
|
+
if (options.outputDir === null) {
|
|
290
|
+
return {
|
|
291
|
+
pageUrl,
|
|
292
|
+
outputDir: null,
|
|
293
|
+
assets: kept.map(({ url, type }, i) => ({
|
|
294
|
+
url,
|
|
295
|
+
type,
|
|
296
|
+
mimeType: null,
|
|
297
|
+
filename: safeFilename(url, i),
|
|
298
|
+
savedPath: null,
|
|
299
|
+
sizeBytes: null
|
|
300
|
+
})),
|
|
301
|
+
filteredCount: filteredUrls.length,
|
|
302
|
+
totalFound
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const domain = (() => {
|
|
306
|
+
try {
|
|
307
|
+
return new URL(pageUrl).hostname.replace(/^www\./, "");
|
|
308
|
+
} catch {
|
|
309
|
+
return "unknown";
|
|
310
|
+
}
|
|
311
|
+
})();
|
|
312
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
313
|
+
const outDir = options.outputDir ?? join(homedir(), "Downloads", "mcp-scraper", "media", `${stamp}-${domain}`);
|
|
314
|
+
mkdirSync(outDir, { recursive: true });
|
|
315
|
+
const assets = [];
|
|
316
|
+
await Promise.allSettled(
|
|
317
|
+
kept.map(async ({ url, type }, i) => {
|
|
318
|
+
const filename = safeFilename(url, i);
|
|
319
|
+
try {
|
|
320
|
+
const { savedPath, sizeBytes, mimeType } = await downloadAsset(url, outDir, filename);
|
|
321
|
+
const resolvedType = mimeType ? typeFromMime(mimeType) ?? type : type;
|
|
322
|
+
assets.push({ url, type: resolvedType, mimeType, filename: basename(savedPath), savedPath, sizeBytes });
|
|
323
|
+
} catch {
|
|
324
|
+
assets.push({ url, type, mimeType: null, filename, savedPath: null, sizeBytes: null });
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
assets.sort((a, b) => {
|
|
329
|
+
if (a.savedPath && !b.savedPath) return -1;
|
|
330
|
+
if (!a.savedPath && b.savedPath) return 1;
|
|
331
|
+
return a.url.localeCompare(b.url);
|
|
332
|
+
});
|
|
333
|
+
return { pageUrl, outputDir: outDir, assets, filteredCount: filteredUrls.length, totalFound };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/api/site-extract-artifacts.ts
|
|
337
|
+
var SITE_EXTRACT_ARTIFACT_PREFIX = "site-extracts/";
|
|
338
|
+
var SITE_EXTRACT_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
339
|
+
var SITE_EXTRACT_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
|
|
340
|
+
function siteExtractArtifactToken() {
|
|
341
|
+
return process.env.SITE_EXTRACT_ARTIFACT_READ_WRITE_TOKEN?.trim() || process.env.PRIVATE_ARTIFACT_READ_WRITE_TOKEN?.trim() || process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim() || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim() || null;
|
|
342
|
+
}
|
|
343
|
+
function hostedByEnvironment() {
|
|
344
|
+
return process.env.VERCEL === "1" || process.env.NODE_ENV === "production";
|
|
345
|
+
}
|
|
346
|
+
function policy() {
|
|
347
|
+
return {
|
|
348
|
+
prefix: SITE_EXTRACT_ARTIFACT_PREFIX,
|
|
349
|
+
artifactTtlMs: SITE_EXTRACT_ARTIFACT_TTL_MS,
|
|
350
|
+
downloadTtlMs: SITE_EXTRACT_DOWNLOAD_TTL_MS,
|
|
351
|
+
token: siteExtractArtifactToken()
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async function createSiteExtractBundleArtifactStream(args) {
|
|
355
|
+
const pointer = await createPrivateArtifactFromStream({
|
|
356
|
+
policy: policy(),
|
|
357
|
+
ownerId: args.ownerId,
|
|
358
|
+
artifactKey: `${args.jobId}.zip`,
|
|
359
|
+
createdAt: args.createdAt,
|
|
360
|
+
filename: `${args.jobId}-site-export.zip`,
|
|
361
|
+
contentType: "application/zip",
|
|
362
|
+
content: args.content
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
key: pointer.artifactId,
|
|
366
|
+
url: pointer.downloadUrl ?? "",
|
|
367
|
+
bytes: pointer.bytes,
|
|
368
|
+
contentType: pointer.contentType,
|
|
369
|
+
filename: pointer.filename,
|
|
370
|
+
sha256: pointer.sha256,
|
|
371
|
+
expiresAt: pointer.expiresAt,
|
|
372
|
+
downloadUrlExpiresAt: pointer.downloadUrlExpiresAt
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function renewSiteExtractArtifactDownload(args) {
|
|
376
|
+
return renewPrivateArtifactDownload({
|
|
377
|
+
policy: policy(),
|
|
378
|
+
artifactId: args.artifactId,
|
|
379
|
+
ownerId: args.ownerId
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function cleanupExpiredSiteExtractArtifacts(args = {}) {
|
|
383
|
+
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
384
|
+
const token = siteExtractArtifactToken();
|
|
385
|
+
if (!token) return { deleted: 0, store: hostedByEnvironment() ? "none" : "local" };
|
|
386
|
+
if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 21)) {
|
|
387
|
+
return { deleted: 0, store: "private-vercel-blob", skipped: true };
|
|
388
|
+
}
|
|
389
|
+
const cutoff = now.getTime() - SITE_EXTRACT_ARTIFACT_TTL_MS;
|
|
390
|
+
const { list, del } = await import("@vercel/blob");
|
|
391
|
+
let cursor;
|
|
392
|
+
let deleted = 0;
|
|
393
|
+
for (let page = 0; page < 20; page += 1) {
|
|
394
|
+
const result = await list({ prefix: SITE_EXTRACT_ARTIFACT_PREFIX, token, limit: 1e3, cursor });
|
|
395
|
+
const expired = result.blobs.filter((blob) => new Date(blob.uploadedAt).getTime() <= cutoff);
|
|
396
|
+
if (expired.length > 0) {
|
|
397
|
+
await del(expired.map((blob) => blob.pathname), { token });
|
|
398
|
+
deleted += expired.length;
|
|
399
|
+
}
|
|
400
|
+
if (!result.hasMore || !result.cursor) break;
|
|
401
|
+
cursor = result.cursor;
|
|
402
|
+
}
|
|
403
|
+
return { deleted, store: "private-vercel-blob" };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export {
|
|
407
|
+
downloadAsset,
|
|
408
|
+
harvestPageMedia,
|
|
409
|
+
SITE_EXTRACT_ARTIFACT_PREFIX,
|
|
410
|
+
createSiteExtractBundleArtifactStream,
|
|
411
|
+
renewSiteExtractArtifactDownload,
|
|
412
|
+
cleanupExpiredSiteExtractArtifacts
|
|
413
|
+
};
|
|
414
|
+
//# sourceMappingURL=chunk-QZXKQB7Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/media-extractor.ts","../src/api/site-extract-artifacts.ts"],"sourcesContent":["import { createWriteStream, mkdirSync, rmSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join, extname, basename } from 'node:path'\nimport { pipeline } from 'node:stream/promises'\nimport { Readable, Transform } from 'node:stream'\nimport { validatePublicHttpUrl } from '../api/url-utils.js'\n\nconst AD_PATTERNS: string[] = [\n 'doubleclick.net', 'googlesyndication.com', 'googletagmanager.com',\n 'google-analytics.com', 'googletagservices.com', 'adservice.google',\n 'googletag.', 'pagead2.googlesyndication',\n 'facebook.net/tr', 'connect.facebook.net', 'fbcdn.net/rsrc',\n 'analytics.twitter.com', 'static.ads-twitter.com', 'ads.twitter.com',\n 't.co/i/adsct', 'pixel.advertising.com',\n 'hotjar.com', 'clarity.ms', 'quantserve.com', 'scorecardresearch.com',\n 'newrelic.com', 'nr-data.net', 'segment.io', 'segment.com',\n 'amplitude.com', 'mixpanel.com', 'heap.io', 'fullstory.com',\n 'moatads.com', 'criteo.com', 'adsrvr.org', 'rubiconproject.com',\n 'pubmatic.com', 'openx.net', 'appnexus.com', 'amazon-adsystem.com',\n 'media.net', 'yieldmo.com', 'triplelift.com', 'sharethrough.com',\n 'prebid.', 'smaato.net', 'indexworm.com', 'casalemedia.com',\n 'outbrain.com', 'taboola.com', 'revcontent.com', 'mgid.com',\n 'tawk.to', 'intercom.io', 'drift.com', 'hs-scripts.com',\n 'zopim.com', 'livechatinc.com', 'userlike.com',\n 'onetrust.com', 'cookielaw.org', 'cookieinformation.com', 'trustarc.com',\n '/ads/', '/ad/', '/banner/', '/banners/', '/pixel/', '/beacon/',\n '/tracking/', '/tracker/', '/remarketing/', '/conversion/',\n '1x1.gif', 'spacer.gif', 'blank.gif', 'transparent.gif',\n]\n\nconst IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.avif', '.svg', '.tiff'])\nconst VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi', '.m4v', '.ogv', '.mkv'])\nconst AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.flac', '.opus'])\n\nexport type MediaType = 'image' | 'video' | 'audio'\n\nexport interface MediaAsset {\n url: string\n type: MediaType\n mimeType: string | null\n filename: string\n savedPath: string | null\n sizeBytes: number | null\n}\n\nexport interface MediaManifest {\n pageUrl: string\n outputDir: string | null\n assets: MediaAsset[]\n filteredCount: number\n totalFound: number\n}\n\nexport interface MediaExtractOptions {\n types?: MediaType[]\n outputDir?: string | null\n}\n\nfunction isAdUrl(url: string): boolean {\n const lower = url.toLowerCase()\n return AD_PATTERNS.some(p => lower.includes(p))\n}\n\nfunction isDataUri(url: string): boolean {\n return url.startsWith('data:')\n}\n\nfunction typeFromUrl(url: string): MediaType | null {\n try {\n const ext = extname(new URL(url).pathname).toLowerCase()\n if (IMAGE_EXTS.has(ext)) return 'image'\n if (VIDEO_EXTS.has(ext)) return 'video'\n if (AUDIO_EXTS.has(ext)) return 'audio'\n } catch { /* non-parseable URL */ }\n return null\n}\n\nfunction typeFromMime(mime: string): MediaType | null {\n const lower = mime.toLowerCase()\n if (lower.startsWith('image/')) return 'image'\n if (lower.startsWith('video/')) return 'video'\n if (lower.startsWith('audio/')) return 'audio'\n return null\n}\n\nfunction resolveUrl(raw: string, base: string): string | null {\n if (!raw || isDataUri(raw)) return null\n try { return new URL(raw, base).href } catch { return null }\n}\n\nfunction safeFilename(url: string, index: number): string {\n try {\n const u = new URL(url)\n const base = basename(u.pathname).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80)\n return base || `asset-${index}`\n } catch {\n return `asset-${index}`\n }\n}\n\nexport function extractMediaUrls(html: string, baseUrl: string): string[] {\n const seen = new Set<string>()\n const urls: string[] = []\n\n const add = (raw: string) => {\n const resolved = resolveUrl(raw.trim(), baseUrl)\n if (!resolved || seen.has(resolved) || isAdUrl(resolved)) return\n seen.add(resolved)\n urls.push(resolved)\n }\n\n for (const m of html.matchAll(/<img\\s[^>]*>/gi)) {\n const tag = m[0]\n for (const attr of ['src', 'data-src', 'data-lazy-src', 'data-original']) {\n const v = (tag.match(new RegExp(`\\\\b${attr}\\\\s*=\\\\s*[\"']([^\"']+)[\"']`, 'i')) ?? [])[1]\n if (v) add(v)\n }\n const srcset = (tag.match(/\\bsrcset\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (srcset) {\n for (const part of srcset.split(',')) {\n const u = part.trim().split(/\\s+/)[0]\n if (u) add(u)\n }\n }\n }\n\n for (const m of html.matchAll(/<source\\s[^>]*>/gi)) {\n const tag = m[0]\n const src = (tag.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (src) add(src)\n const srcset = (tag.match(/\\bsrcset\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (srcset) {\n for (const part of srcset.split(',')) {\n const u = part.trim().split(/\\s+/)[0]\n if (u) add(u)\n }\n }\n }\n\n for (const m of html.matchAll(/<video\\s[^>]*>/gi)) {\n const tag = m[0]\n for (const attr of ['src', 'poster']) {\n const v = (tag.match(new RegExp(`\\\\b${attr}\\\\s*=\\\\s*[\"']([^\"']+)[\"']`, 'i')) ?? [])[1]\n if (v) add(v)\n }\n }\n\n for (const m of html.matchAll(/<audio\\s[^>]*>/gi)) {\n const tag = m[0]\n const v = (tag.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (v) add(v)\n }\n\n for (const m of html.matchAll(/<meta\\s[^>]*>/gi)) {\n const tag = m[0]\n const prop = (tag.match(/\\b(?:property|name)\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]?.toLowerCase()\n if (prop === 'og:image' || prop === 'twitter:image') {\n const content = (tag.match(/\\bcontent\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (content) add(content)\n }\n }\n\n for (const m of html.matchAll(/background(?:-image)?\\s*:\\s*url\\(\\s*[\"']?([^\"')]+)[\"']?\\s*\\)/gi)) {\n add(m[1])\n }\n\n return urls\n}\n\nexport async function downloadAsset(\n url: string,\n destDir: string,\n filename: string,\n options: {\n expectedType?: MediaType\n maxBytes?: number\n consumeBytes?: (bytes: number) => boolean\n } = {},\n): Promise<{ savedPath: string; sizeBytes: number; mimeType: string | null }> {\n const maxBytes = Math.max(1, Math.min(options.maxBytes ?? 50 * 1024 * 1024, 100 * 1024 * 1024))\n let target = url\n let res: Response | null = null\n for (let redirects = 0; redirects <= 5; redirects++) {\n const checked = await validatePublicHttpUrl(target, { field: 'media URL' })\n if (checked.error || !checked.parsed) throw new Error(checked.error ?? 'Media URL was rejected')\n res = await fetch(checked.parsed.href, {\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ThorbitBot/1.0)' },\n signal: AbortSignal.timeout(15_000),\n redirect: 'manual',\n })\n if (res.status >= 300 && res.status < 400) {\n const location = res.headers.get('location')\n if (!location) throw new Error(`HTTP ${res.status} redirect did not include Location`)\n target = new URL(location, checked.parsed.href).href\n res = null\n continue\n }\n break\n }\n if (!res) throw new Error('Media download exceeded five redirects')\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n if (!res.body) throw new Error('Empty response body')\n\n const mimeType = res.headers.get('content-type')?.split(';')[0].trim() ?? null\n if (options.expectedType && (!mimeType || typeFromMime(mimeType) !== options.expectedType)) {\n throw new Error(`Expected ${options.expectedType} content but received ${mimeType ?? 'no content-type'}`)\n }\n const declaredBytes = Number(res.headers.get('content-length') ?? 0)\n if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {\n throw new Error(`Media asset exceeds ${maxBytes} byte limit`)\n }\n\n let dest = join(destDir, filename)\n if (mimeType && !extname(filename)) {\n const mimeExt: Record<string, string> = {\n 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp',\n 'image/gif': '.gif', 'image/svg+xml': '.svg', 'image/avif': '.avif',\n 'video/mp4': '.mp4', 'video/webm': '.webm',\n 'audio/mpeg': '.mp3', 'audio/ogg': '.ogg', 'audio/wav': '.wav',\n }\n const ext = mimeExt[mimeType]\n if (ext) dest = dest + ext\n }\n\n const writer = createWriteStream(dest)\n await new Promise<void>((resolve, reject) => {\n writer.once('open', () => resolve())\n writer.once('error', reject)\n })\n let streamedBytes = 0\n const limiter = new Transform({\n transform(chunk: Buffer, _encoding, callback) {\n const bytes = chunk.length\n if (streamedBytes + bytes > maxBytes) {\n callback(new Error(`Media asset exceeds ${maxBytes} byte limit`))\n return\n }\n if (options.consumeBytes && !options.consumeBytes(bytes)) {\n callback(new Error('Media export total byte limit exceeded'))\n return\n }\n streamedBytes += bytes\n callback(null, chunk)\n },\n })\n try {\n await pipeline(Readable.fromWeb(res.body as import('stream/web').ReadableStream), limiter, writer)\n } catch (error) {\n writer.destroy()\n rmSync(dest, { force: true })\n throw error\n }\n\n const { statSync } = await import('node:fs')\n const sizeBytes = statSync(dest).size\n\n return { savedPath: dest, sizeBytes, mimeType }\n}\n\nexport async function harvestPageMedia(\n html: string,\n pageUrl: string,\n options: MediaExtractOptions = {},\n): Promise<MediaManifest> {\n const types = options.types ?? ['image', 'video', 'audio']\n const typesSet = new Set(types)\n const rawUrls = extractMediaUrls(html, pageUrl)\n const totalFound = rawUrls.length\n\n const filteredUrls: string[] = []\n const kept: Array<{ url: string; type: MediaType }> = []\n\n for (const url of rawUrls) {\n const type = typeFromUrl(url)\n if (!type || !typesSet.has(type)) { filteredUrls.push(url); continue }\n kept.push({ url, type })\n }\n\n if (options.outputDir === null) {\n return {\n pageUrl,\n outputDir: null,\n assets: kept.map(({ url, type }, i) => ({\n url, type, mimeType: null, filename: safeFilename(url, i), savedPath: null, sizeBytes: null,\n })),\n filteredCount: filteredUrls.length,\n totalFound,\n }\n }\n\n const domain = (() => { try { return new URL(pageUrl).hostname.replace(/^www\\./, '') } catch { return 'unknown' } })()\n const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)\n const outDir = options.outputDir ?? join(homedir(), 'Downloads', 'mcp-scraper', 'media', `${stamp}-${domain}`)\n mkdirSync(outDir, { recursive: true })\n\n const assets: MediaAsset[] = []\n\n await Promise.allSettled(\n kept.map(async ({ url, type }, i) => {\n const filename = safeFilename(url, i)\n try {\n const { savedPath, sizeBytes, mimeType } = await downloadAsset(url, outDir, filename)\n const resolvedType = mimeType ? (typeFromMime(mimeType) ?? type) : type\n assets.push({ url, type: resolvedType, mimeType, filename: basename(savedPath), savedPath, sizeBytes })\n } catch {\n assets.push({ url, type, mimeType: null, filename, savedPath: null, sizeBytes: null })\n }\n })\n )\n\n assets.sort((a, b) => {\n if (a.savedPath && !b.savedPath) return -1\n if (!a.savedPath && b.savedPath) return 1\n return a.url.localeCompare(b.url)\n })\n\n return { pageUrl, outputDir: outDir, assets, filteredCount: filteredUrls.length, totalFound }\n}\n","import {\n createPrivateArtifact,\n createPrivateArtifactFromStream,\n privateArtifactOwnerId,\n renewPrivateArtifactDownload,\n type PrivateArtifactPolicy,\n} from './private-artifacts.js'\nimport type { ExtractJobArtifact } from './site-extract-repository.js'\nimport type { Readable } from 'node:stream'\n\nexport const SITE_EXTRACT_ARTIFACT_PREFIX = 'site-extracts/'\nexport const SITE_EXTRACT_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1_000\nexport const SITE_EXTRACT_DOWNLOAD_TTL_MS = 15 * 60 * 1_000\n\nfunction siteExtractArtifactToken(): string | null {\n return process.env.SITE_EXTRACT_ARTIFACT_READ_WRITE_TOKEN?.trim()\n || process.env.PRIVATE_ARTIFACT_READ_WRITE_TOKEN?.trim()\n || process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim()\n || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim()\n || null\n}\n\nfunction hostedByEnvironment(): boolean {\n return process.env.VERCEL === '1' || process.env.NODE_ENV === 'production'\n}\n\nfunction policy(): PrivateArtifactPolicy {\n return {\n prefix: SITE_EXTRACT_ARTIFACT_PREFIX,\n artifactTtlMs: SITE_EXTRACT_ARTIFACT_TTL_MS,\n downloadTtlMs: SITE_EXTRACT_DOWNLOAD_TTL_MS,\n token: siteExtractArtifactToken(),\n }\n}\n\nexport function siteExtractArtifactOwnerId(artifactId: string): string | null {\n return privateArtifactOwnerId(artifactId, SITE_EXTRACT_ARTIFACT_PREFIX)\n}\n\nexport async function createSiteExtractBundleArtifact(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n content: Buffer\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifact({\n policy: policy(),\n ownerId: args.ownerId,\n artifactKey: `${args.jobId}.zip`,\n createdAt: args.createdAt,\n filename: `${args.jobId}-site-export.zip`,\n contentType: 'application/zip',\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n }\n}\n\nexport async function createSiteExtractBundleArtifactStream(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n content: Readable\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifactFromStream({\n policy: policy(),\n ownerId: args.ownerId,\n artifactKey: `${args.jobId}.zip`,\n createdAt: args.createdAt,\n filename: `${args.jobId}-site-export.zip`,\n contentType: 'application/zip',\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n }\n}\n\nexport async function renewSiteExtractArtifactDownload(args: {\n artifactId: string\n ownerId: string\n}): Promise<{ downloadUrl: string; downloadUrlExpiresAt: string; expiresAt: string } | null> {\n return renewPrivateArtifactDownload({\n policy: policy(),\n artifactId: args.artifactId,\n ownerId: args.ownerId,\n })\n}\n\nexport async function cleanupExpiredSiteExtractArtifacts(args: {\n now?: Date\n force?: boolean\n} = {}): Promise<{ deleted: number; store: 'private-vercel-blob' | 'local' | 'none'; skipped?: boolean }> {\n const now = args.now ?? new Date()\n const token = siteExtractArtifactToken()\n if (!token) return { deleted: 0, store: hostedByEnvironment() ? 'none' : 'local' }\n if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 21)) {\n return { deleted: 0, store: 'private-vercel-blob', skipped: true }\n }\n const cutoff = now.getTime() - SITE_EXTRACT_ARTIFACT_TTL_MS\n const { list, del } = await import('@vercel/blob')\n let cursor: string | undefined\n let deleted = 0\n for (let page = 0; page < 20; page += 1) {\n const result = await list({ prefix: SITE_EXTRACT_ARTIFACT_PREFIX, token, limit: 1_000, cursor })\n const expired = result.blobs.filter(blob => new Date(blob.uploadedAt).getTime() <= cutoff)\n if (expired.length > 0) {\n await del(expired.map(blob => blob.pathname), { token })\n deleted += expired.length\n }\n if (!result.hasMore || !result.cursor) break\n cursor = result.cursor\n }\n return { deleted, store: 'private-vercel-blob' }\n}\n"],"mappings":"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","names":[]}
|