@thenavidm/facebook-ad-library-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/SKILL.md +115 -0
- package/dist/adlibrary/harvest.d.ts +35 -0
- package/dist/adlibrary/harvest.js +153 -0
- package/dist/adlibrary/harvest.js.map +1 -0
- package/dist/adlibrary/normalize.d.ts +25 -0
- package/dist/adlibrary/normalize.js +221 -0
- package/dist/adlibrary/normalize.js.map +1 -0
- package/dist/adlibrary/types.d.ts +101 -0
- package/dist/adlibrary/types.js +17 -0
- package/dist/adlibrary/types.js.map +1 -0
- package/dist/adlibrary/url.d.ts +10 -0
- package/dist/adlibrary/url.js +29 -0
- package/dist/adlibrary/url.js.map +1 -0
- package/dist/archive/client.d.ts +45 -0
- package/dist/archive/client.js +124 -0
- package/dist/archive/client.js.map +1 -0
- package/dist/backends/apify.d.ts +37 -0
- package/dist/backends/apify.js +152 -0
- package/dist/backends/apify.js.map +1 -0
- package/dist/backends/browser.d.ts +51 -0
- package/dist/backends/browser.js +202 -0
- package/dist/backends/browser.js.map +1 -0
- package/dist/backends/index.d.ts +8 -0
- package/dist/backends/index.js +25 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/scrapecreators.d.ts +29 -0
- package/dist/backends/scrapecreators.js +185 -0
- package/dist/backends/scrapecreators.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +131 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/format/ads.d.ts +16 -0
- package/dist/format/ads.js +97 -0
- package/dist/format/ads.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.js +156 -0
- package/dist/server.js.map +1 -0
- package/dist/store/snapshots.d.ts +29 -0
- package/dist/store/snapshots.js +83 -0
- package/dist/store/snapshots.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/kit.d.ts +70 -0
- package/dist/tools/kit.js +90 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/search.d.ts +5 -0
- package/dist/tools/search.js +165 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/session.d.ts +3 -0
- package/dist/tools/session.js +23 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/track.d.ts +9 -0
- package/dist/tools/track.js +68 -0
- package/dist/tools/track.js.map +1 -0
- package/dist/tools/transparency.d.ts +5 -0
- package/dist/tools/transparency.js +53 -0
- package/dist/tools/transparency.js.map +1 -0
- package/dist/transport/http.d.ts +23 -0
- package/dist/transport/http.js +88 -0
- package/dist/transport/http.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apify backend. Needs a token, works serverless, about $3.40 to $5.80 per 1,000 ads.
|
|
3
|
+
*
|
|
4
|
+
* Two to three times the price of ScrapeCreators, and an actor run to wait on
|
|
5
|
+
* rather than a plain request, so it is not the default provider. It is here
|
|
6
|
+
* because plenty of people already have an Apify account and a token, and
|
|
7
|
+
* because its e-commerce enrichment has no equivalent elsewhere.
|
|
8
|
+
*
|
|
9
|
+
* The actor takes an Ad Library URL as `startUrls`, which is the same URL the
|
|
10
|
+
* browser backend navigates to, so both share `adlibrary/url.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Actor input schema read from the live API on 2026-08-31.
|
|
13
|
+
*/
|
|
14
|
+
import type { Ad, Advertiser, Backend, SearchParams, SearchResult } from "../adlibrary/types.js";
|
|
15
|
+
export declare class ApifyBackend implements Backend {
|
|
16
|
+
private readonly token;
|
|
17
|
+
private readonly options;
|
|
18
|
+
readonly name: "apify";
|
|
19
|
+
readonly needsKey = true;
|
|
20
|
+
constructor(token: string, options?: {
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
ecommerce?: boolean;
|
|
23
|
+
});
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* One synchronous actor run, returning its dataset items.
|
|
27
|
+
*
|
|
28
|
+
* `run-sync-get-dataset-items` blocks until the run finishes, which is why the
|
|
29
|
+
* default timeout is generous. Apify caps a sync run at five minutes, so a very
|
|
30
|
+
* large limit times out rather than returning partial data.
|
|
31
|
+
*/
|
|
32
|
+
private run;
|
|
33
|
+
search(params: SearchParams): Promise<SearchResult>;
|
|
34
|
+
/** No company-search endpoint on this actor, so derive advertisers from results. */
|
|
35
|
+
listAdvertisers(query: string, country?: string): Promise<Advertiser[]>;
|
|
36
|
+
getAd(libraryId: string): Promise<Ad | undefined>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apify backend. Needs a token, works serverless, about $3.40 to $5.80 per 1,000 ads.
|
|
3
|
+
*
|
|
4
|
+
* Two to three times the price of ScrapeCreators, and an actor run to wait on
|
|
5
|
+
* rather than a plain request, so it is not the default provider. It is here
|
|
6
|
+
* because plenty of people already have an Apify account and a token, and
|
|
7
|
+
* because its e-commerce enrichment has no equivalent elsewhere.
|
|
8
|
+
*
|
|
9
|
+
* The actor takes an Ad Library URL as `startUrls`, which is the same URL the
|
|
10
|
+
* browser backend navigates to, so both share `adlibrary/url.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Actor input schema read from the live API on 2026-08-31.
|
|
13
|
+
*/
|
|
14
|
+
import { AdLibraryError } from "../errors.js";
|
|
15
|
+
import { adFromApify } from "../adlibrary/normalize.js";
|
|
16
|
+
import { adDetailsUrl, buildUrl } from "../adlibrary/url.js";
|
|
17
|
+
const ACTOR = "apify~facebook-ads-scraper";
|
|
18
|
+
const BASE = "https://api.apify.com/v2";
|
|
19
|
+
export class ApifyBackend {
|
|
20
|
+
token;
|
|
21
|
+
options;
|
|
22
|
+
name = "apify";
|
|
23
|
+
needsKey = true;
|
|
24
|
+
constructor(token, options = {}) {
|
|
25
|
+
this.token = token;
|
|
26
|
+
this.options = options;
|
|
27
|
+
if (!token) {
|
|
28
|
+
throw new AdLibraryError("The apify backend needs a token.", {
|
|
29
|
+
backend: this.name,
|
|
30
|
+
hint: "Set APIFY_TOKEN, or use the free browser backend with FBADS_BACKEND=browser.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async close() {
|
|
35
|
+
// Nothing to release: this backend is stateless fetch calls.
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One synchronous actor run, returning its dataset items.
|
|
39
|
+
*
|
|
40
|
+
* `run-sync-get-dataset-items` blocks until the run finishes, which is why the
|
|
41
|
+
* default timeout is generous. Apify caps a sync run at five minutes, so a very
|
|
42
|
+
* large limit times out rather than returning partial data.
|
|
43
|
+
*/
|
|
44
|
+
async run(url, limit, details = true) {
|
|
45
|
+
const endpoint = new URL(`/v2/acts/${ACTOR}/run-sync-get-dataset-items`, BASE);
|
|
46
|
+
endpoint.searchParams.set("token", this.token);
|
|
47
|
+
const response = await fetch(endpoint, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json" },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
startUrls: [{ url }],
|
|
52
|
+
resultsLimit: limit,
|
|
53
|
+
isDetailsPerAd: details,
|
|
54
|
+
enrichWithEcommerceData: this.options.ecommerce ?? false,
|
|
55
|
+
}),
|
|
56
|
+
signal: AbortSignal.timeout(this.options.timeoutMs ?? 300_000),
|
|
57
|
+
});
|
|
58
|
+
if (response.status === 401) {
|
|
59
|
+
throw new AdLibraryError("Apify rejected the token (401).", {
|
|
60
|
+
backend: this.name,
|
|
61
|
+
hint: "Check APIFY_TOKEN.",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (response.status === 402) {
|
|
65
|
+
throw new AdLibraryError("Apify reports insufficient credit (402).", {
|
|
66
|
+
backend: this.name,
|
|
67
|
+
hint: "Top up the account, or switch to the free browser backend with FBADS_BACKEND=browser.",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (response.status === 408) {
|
|
71
|
+
throw new AdLibraryError("The Apify run exceeded the five-minute synchronous limit.", {
|
|
72
|
+
backend: this.name,
|
|
73
|
+
hint: "Lower `limit` and make several calls instead of one large one.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const body = (await response.text()).slice(0, 300);
|
|
78
|
+
throw new AdLibraryError(`Apify error ${response.status}: ${body}`, { backend: this.name });
|
|
79
|
+
}
|
|
80
|
+
const items = await response.json();
|
|
81
|
+
return Array.isArray(items) ? items : [];
|
|
82
|
+
}
|
|
83
|
+
async search(params) {
|
|
84
|
+
if (!params.query && !params.pageId) {
|
|
85
|
+
throw new AdLibraryError("Pass either query or page_id.", { backend: this.name });
|
|
86
|
+
}
|
|
87
|
+
const limit = params.limit ?? 30;
|
|
88
|
+
const url = buildUrl(params);
|
|
89
|
+
const items = await this.run(url, limit);
|
|
90
|
+
const ads = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
if (typeof item !== "object" || item === null)
|
|
94
|
+
continue;
|
|
95
|
+
const ad = adFromApify(item);
|
|
96
|
+
if (!ad.libraryId || seen.has(ad.libraryId))
|
|
97
|
+
continue;
|
|
98
|
+
seen.add(ad.libraryId);
|
|
99
|
+
ads.push(ad);
|
|
100
|
+
}
|
|
101
|
+
const kept = ads.slice(0, limit);
|
|
102
|
+
return {
|
|
103
|
+
backend: this.name,
|
|
104
|
+
count: kept.length,
|
|
105
|
+
hasMore: false,
|
|
106
|
+
// The actor paginates internally against resultsLimit and exposes no
|
|
107
|
+
// cursor, so raise `limit` rather than paging. Said plainly here so
|
|
108
|
+
// nobody goes hunting for one.
|
|
109
|
+
cursor: undefined,
|
|
110
|
+
url,
|
|
111
|
+
note: kept.length > 0
|
|
112
|
+
? "This backend has no cursor. Raise `limit` to get more rather than paging."
|
|
113
|
+
: "The Apify actor returned no ads for these filters.",
|
|
114
|
+
ads: kept,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** No company-search endpoint on this actor, so derive advertisers from results. */
|
|
118
|
+
async listAdvertisers(query, country = "US") {
|
|
119
|
+
const result = await this.search({ query, country, activeStatus: "all", limit: 60 });
|
|
120
|
+
const byPage = new Map();
|
|
121
|
+
for (const ad of result.ads) {
|
|
122
|
+
if (!ad.pageId)
|
|
123
|
+
continue;
|
|
124
|
+
const existing = byPage.get(ad.pageId);
|
|
125
|
+
if (existing) {
|
|
126
|
+
existing.adCount = (existing.adCount ?? 0) + 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
byPage.set(ad.pageId, {
|
|
130
|
+
pageId: ad.pageId,
|
|
131
|
+
pageName: ad.pageName,
|
|
132
|
+
pageUrl: ad.pageUrl,
|
|
133
|
+
pageLikes: ad.pageLikes,
|
|
134
|
+
profilePictureUrl: ad.pageProfilePictureUrl,
|
|
135
|
+
adCount: 1,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return [...byPage.values()].sort((a, b) => (b.adCount ?? 0) - (a.adCount ?? 0));
|
|
139
|
+
}
|
|
140
|
+
async getAd(libraryId) {
|
|
141
|
+
const items = await this.run(adDetailsUrl(libraryId), 1);
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
if (typeof item !== "object" || item === null)
|
|
144
|
+
continue;
|
|
145
|
+
const ad = adFromApify(item);
|
|
146
|
+
if (ad.libraryId)
|
|
147
|
+
return ad;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=apify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apify.js","sourceRoot":"","sources":["../../src/backends/apify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The free backend. No key, no account, no provider.
|
|
3
|
+
*
|
|
4
|
+
* How it works, and why it is not what the other Ad Library scrapers do:
|
|
5
|
+
*
|
|
6
|
+
* The Ad Library is a React app that receives every ad from Meta as JSON over
|
|
7
|
+
* GraphQL. The common approach is to let it render, flatten the DOM to markdown
|
|
8
|
+
* and regex the fields back out. That round trip is lossy: it recovers one
|
|
9
|
+
* creative per ad, loses the platform list, hands back an l.facebook.com
|
|
10
|
+
* redirect instead of the real landing page, and breaks whenever Meta ships
|
|
11
|
+
* markup.
|
|
12
|
+
*
|
|
13
|
+
* This backend reads the payload the page was already given. Same browser, same
|
|
14
|
+
* cost, no parsing.
|
|
15
|
+
*
|
|
16
|
+
* Two sources, because Meta uses two. The first page of results is inlined in
|
|
17
|
+
* the document as a JSON script block and no GraphQL call is made for it; every
|
|
18
|
+
* page after that arrives as a GraphQL response triggered by scrolling. Reading
|
|
19
|
+
* only the XHRs makes the first search depend on a scroll landing in time.
|
|
20
|
+
*
|
|
21
|
+
* Verified against a live capture on 2026-08-31.
|
|
22
|
+
*/
|
|
23
|
+
import type { Ad, Advertiser, Backend, SearchParams, SearchResult } from "../adlibrary/types.js";
|
|
24
|
+
export type BrowserOptions = {
|
|
25
|
+
headless?: boolean;
|
|
26
|
+
hydrateMs?: number;
|
|
27
|
+
scrollWaitMs?: number;
|
|
28
|
+
};
|
|
29
|
+
export declare class BrowserBackend implements Backend {
|
|
30
|
+
readonly name: "browser";
|
|
31
|
+
readonly needsKey = false;
|
|
32
|
+
private readonly headless;
|
|
33
|
+
private readonly hydrateMs;
|
|
34
|
+
private readonly scrollWaitMs;
|
|
35
|
+
private browser;
|
|
36
|
+
private queue;
|
|
37
|
+
constructor(options?: BrowserOptions);
|
|
38
|
+
private launch;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
/** Run one page job at a time, whatever the caller does. */
|
|
41
|
+
private serialise;
|
|
42
|
+
private collect;
|
|
43
|
+
search(params: SearchParams): Promise<SearchResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Meta publishes no advertiser-search payload we can rely on, so derive the
|
|
46
|
+
* list from who is actually running ads for the term. Fewer false hits than a
|
|
47
|
+
* name lookup, and it only surfaces pages with live creative.
|
|
48
|
+
*/
|
|
49
|
+
listAdvertisers(query: string, country?: string): Promise<Advertiser[]>;
|
|
50
|
+
getAd(libraryId: string): Promise<Ad | undefined>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The free backend. No key, no account, no provider.
|
|
3
|
+
*
|
|
4
|
+
* How it works, and why it is not what the other Ad Library scrapers do:
|
|
5
|
+
*
|
|
6
|
+
* The Ad Library is a React app that receives every ad from Meta as JSON over
|
|
7
|
+
* GraphQL. The common approach is to let it render, flatten the DOM to markdown
|
|
8
|
+
* and regex the fields back out. That round trip is lossy: it recovers one
|
|
9
|
+
* creative per ad, loses the platform list, hands back an l.facebook.com
|
|
10
|
+
* redirect instead of the real landing page, and breaks whenever Meta ships
|
|
11
|
+
* markup.
|
|
12
|
+
*
|
|
13
|
+
* This backend reads the payload the page was already given. Same browser, same
|
|
14
|
+
* cost, no parsing.
|
|
15
|
+
*
|
|
16
|
+
* Two sources, because Meta uses two. The first page of results is inlined in
|
|
17
|
+
* the document as a JSON script block and no GraphQL call is made for it; every
|
|
18
|
+
* page after that arrives as a GraphQL response triggered by scrolling. Reading
|
|
19
|
+
* only the XHRs makes the first search depend on a scroll landing in time.
|
|
20
|
+
*
|
|
21
|
+
* Verified against a live capture on 2026-08-31.
|
|
22
|
+
*/
|
|
23
|
+
import { AdLibraryError } from "../errors.js";
|
|
24
|
+
import { inlinePayloads, parsePayloads } from "../adlibrary/harvest.js";
|
|
25
|
+
import { adFromGraphql } from "../adlibrary/normalize.js";
|
|
26
|
+
import { adDetailsUrl, buildUrl } from "../adlibrary/url.js";
|
|
27
|
+
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
|
28
|
+
"(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
29
|
+
export class BrowserBackend {
|
|
30
|
+
name = "browser";
|
|
31
|
+
needsKey = false;
|
|
32
|
+
headless;
|
|
33
|
+
hydrateMs;
|
|
34
|
+
scrollWaitMs;
|
|
35
|
+
// Launching Chromium costs seconds, so one instance is kept warm across calls.
|
|
36
|
+
browser;
|
|
37
|
+
// Meta rate-limits hard on parallel pages, so calls are serialised.
|
|
38
|
+
queue = Promise.resolve();
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.headless = options.headless ?? true;
|
|
41
|
+
this.hydrateMs = options.hydrateMs ?? 9000;
|
|
42
|
+
this.scrollWaitMs = options.scrollWaitMs ?? 4000;
|
|
43
|
+
}
|
|
44
|
+
async launch() {
|
|
45
|
+
if (this.browser)
|
|
46
|
+
return this.browser;
|
|
47
|
+
let chromium;
|
|
48
|
+
try {
|
|
49
|
+
({ chromium } = await import("playwright"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new AdLibraryError("The browser backend needs Playwright, which is not installed.", {
|
|
53
|
+
backend: this.name,
|
|
54
|
+
hint: "Run `npx playwright install chromium`, or switch to a provider backend " +
|
|
55
|
+
"by setting FBADS_BACKEND=scrapecreators and SCRAPECREATORS_API_KEY.",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
this.browser = await chromium.launch({ headless: this.headless });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new AdLibraryError(`Chromium failed to launch: ${error.message}`, { backend: this.name, hint: "Run `npx playwright install chromium` to install the browser." });
|
|
63
|
+
}
|
|
64
|
+
return this.browser;
|
|
65
|
+
}
|
|
66
|
+
async close() {
|
|
67
|
+
if (this.browser) {
|
|
68
|
+
await this.browser.close().catch(() => undefined);
|
|
69
|
+
this.browser = undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Run one page job at a time, whatever the caller does. */
|
|
73
|
+
serialise(job) {
|
|
74
|
+
const result = this.queue.then(job, job);
|
|
75
|
+
this.queue = result.then(() => undefined, () => undefined);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
async collect(url, limit) {
|
|
79
|
+
const browser = await this.launch();
|
|
80
|
+
const context = await browser.newContext({
|
|
81
|
+
viewport: { width: 1440, height: 1200 },
|
|
82
|
+
locale: "en-US",
|
|
83
|
+
userAgent: USER_AGENT,
|
|
84
|
+
});
|
|
85
|
+
const bodies = [];
|
|
86
|
+
try {
|
|
87
|
+
const page = await context.newPage();
|
|
88
|
+
page.on("response", (response) => {
|
|
89
|
+
if (!String(response.url()).includes("/api/graphql"))
|
|
90
|
+
return;
|
|
91
|
+
// A body can vanish when the page navigates. Losing one is survivable;
|
|
92
|
+
// an unhandled rejection here would take the whole call down.
|
|
93
|
+
void response
|
|
94
|
+
.text()
|
|
95
|
+
.then((body) => bodies.push(body))
|
|
96
|
+
.catch(() => undefined);
|
|
97
|
+
});
|
|
98
|
+
// Meta answers a headless browser with HTTP 403 on the document but still
|
|
99
|
+
// serves the app and every payload. Judge success by ads parsed, never by status.
|
|
100
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
101
|
+
await page.waitForTimeout(this.hydrateMs);
|
|
102
|
+
// The inlined first page. Read once: it does not change as we scroll.
|
|
103
|
+
bodies.push(...inlinePayloads(await page.content()));
|
|
104
|
+
let seen = new Set();
|
|
105
|
+
let stalls = 0;
|
|
106
|
+
while (stalls < 3) {
|
|
107
|
+
const harvest = parsePayloads(bodies);
|
|
108
|
+
const ids = new Set(harvest.nodes.map((node) => String(node["ad_archive_id"])));
|
|
109
|
+
const sameAsLast = ids.size === seen.size && [...ids].every((id) => seen.has(id));
|
|
110
|
+
if (ids.size >= limit || (!harvest.hasMore && ids.size > 0 && sameAsLast))
|
|
111
|
+
break;
|
|
112
|
+
stalls = sameAsLast ? stalls + 1 : 0;
|
|
113
|
+
seen = ids;
|
|
114
|
+
await page.mouse.wheel(0, 8000);
|
|
115
|
+
await page.waitForTimeout(this.scrollWaitMs);
|
|
116
|
+
}
|
|
117
|
+
await page.close();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await context.close().catch(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
return parsePayloads(bodies);
|
|
123
|
+
}
|
|
124
|
+
async search(params) {
|
|
125
|
+
if (!params.query && !params.pageId) {
|
|
126
|
+
throw new AdLibraryError("Pass either query or page_id.", { backend: this.name });
|
|
127
|
+
}
|
|
128
|
+
const limit = params.limit ?? 30;
|
|
129
|
+
const url = buildUrl(params);
|
|
130
|
+
const harvest = await this.serialise(() => this.collect(url, limit));
|
|
131
|
+
const ads = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const node of harvest.nodes) {
|
|
134
|
+
const ad = adFromGraphql(node);
|
|
135
|
+
if (!ad.libraryId || seen.has(ad.libraryId))
|
|
136
|
+
continue;
|
|
137
|
+
seen.add(ad.libraryId);
|
|
138
|
+
ads.push(ad);
|
|
139
|
+
}
|
|
140
|
+
let note;
|
|
141
|
+
if (harvest.captcha) {
|
|
142
|
+
note =
|
|
143
|
+
"Meta served a captcha instead of results, so nothing could be read. Wait a few " +
|
|
144
|
+
"minutes, or use a provider backend, which is not affected.";
|
|
145
|
+
}
|
|
146
|
+
else if (ads.length === 0) {
|
|
147
|
+
note = harvest.total
|
|
148
|
+
? `No ads captured even though Meta reports ${harvest.total} matching. Retry in a minute.`
|
|
149
|
+
: "No ads captured. Either this search genuinely has no results, or Meta declined " +
|
|
150
|
+
"to serve them. Open the `url` in a browser to tell the two apart.";
|
|
151
|
+
}
|
|
152
|
+
else if (harvest.total && harvest.total > ads.length) {
|
|
153
|
+
note = `Meta reports ${harvest.total} matching ads. Raise \`limit\` for more.`;
|
|
154
|
+
}
|
|
155
|
+
const kept = ads.slice(0, limit);
|
|
156
|
+
return {
|
|
157
|
+
backend: this.name,
|
|
158
|
+
count: kept.length,
|
|
159
|
+
hasMore: harvest.hasMore || ads.length > limit,
|
|
160
|
+
cursor: harvest.cursor,
|
|
161
|
+
totalAvailable: harvest.total,
|
|
162
|
+
url,
|
|
163
|
+
note,
|
|
164
|
+
ads: kept,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Meta publishes no advertiser-search payload we can rely on, so derive the
|
|
169
|
+
* list from who is actually running ads for the term. Fewer false hits than a
|
|
170
|
+
* name lookup, and it only surfaces pages with live creative.
|
|
171
|
+
*/
|
|
172
|
+
async listAdvertisers(query, country = "US") {
|
|
173
|
+
const result = await this.search({ query, country, activeStatus: "all", limit: 60 });
|
|
174
|
+
const byPage = new Map();
|
|
175
|
+
for (const ad of result.ads) {
|
|
176
|
+
if (!ad.pageId)
|
|
177
|
+
continue;
|
|
178
|
+
const existing = byPage.get(ad.pageId);
|
|
179
|
+
if (existing) {
|
|
180
|
+
existing.adCount = (existing.adCount ?? 0) + 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
byPage.set(ad.pageId, {
|
|
184
|
+
pageId: ad.pageId,
|
|
185
|
+
pageName: ad.pageName,
|
|
186
|
+
pageUrl: ad.pageUrl,
|
|
187
|
+
pageLikes: ad.pageLikes,
|
|
188
|
+
category: ad.pageCategories[0],
|
|
189
|
+
profilePictureUrl: ad.pageProfilePictureUrl,
|
|
190
|
+
adCount: 1,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return [...byPage.values()].sort((a, b) => (b.adCount ?? 0) - (a.adCount ?? 0));
|
|
194
|
+
}
|
|
195
|
+
async getAd(libraryId) {
|
|
196
|
+
const harvest = await this.serialise(() => this.collect(adDetailsUrl(libraryId), 1));
|
|
197
|
+
const match = harvest.nodes.find((node) => String(node["ad_archive_id"]) === String(libraryId));
|
|
198
|
+
const node = match ?? harvest.nodes[0];
|
|
199
|
+
return node ? adFromGraphql(node) : undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/backends/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Picking a backend, failing with a message that names the fix. */
|
|
2
|
+
import type { Backend } from "../adlibrary/types.js";
|
|
3
|
+
import type { Config } from "../config.js";
|
|
4
|
+
import { ApifyBackend } from "./apify.js";
|
|
5
|
+
import { BrowserBackend } from "./browser.js";
|
|
6
|
+
import { ScrapeCreatorsBackend } from "./scrapecreators.js";
|
|
7
|
+
export declare function createBackend(config: Config): Backend;
|
|
8
|
+
export { ApifyBackend, BrowserBackend, ScrapeCreatorsBackend };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Picking a backend, failing with a message that names the fix. */
|
|
2
|
+
import { AdLibraryError } from "../errors.js";
|
|
3
|
+
import { ApifyBackend } from "./apify.js";
|
|
4
|
+
import { BrowserBackend } from "./browser.js";
|
|
5
|
+
import { ScrapeCreatorsBackend } from "./scrapecreators.js";
|
|
6
|
+
export function createBackend(config) {
|
|
7
|
+
switch (config.backend) {
|
|
8
|
+
case "browser":
|
|
9
|
+
return new BrowserBackend({
|
|
10
|
+
headless: config.headless,
|
|
11
|
+
hydrateMs: config.hydrateMs,
|
|
12
|
+
scrollWaitMs: config.scrollWaitMs,
|
|
13
|
+
});
|
|
14
|
+
case "scrapecreators":
|
|
15
|
+
return new ScrapeCreatorsBackend(config.scrapeCreatorsKey ?? "");
|
|
16
|
+
case "apify":
|
|
17
|
+
return new ApifyBackend(config.apifyToken ?? "");
|
|
18
|
+
default:
|
|
19
|
+
throw new AdLibraryError(`Unknown backend ${String(config.backend)}.`, {
|
|
20
|
+
hint: "FBADS_BACKEND must be browser, scrapecreators or apify.",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export { ApifyBackend, BrowserBackend, ScrapeCreatorsBackend };
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backends/index.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,QAAQ,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,KAAK,SAAS;YACZ,OAAO,IAAI,cAAc,CAAC;gBACxB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,YAAY,EAAE,MAAM,CAAC,YAAY;aAClC,CAAC,CAAC;QACL,KAAK,gBAAgB;YACnB,OAAO,IAAI,qBAAqB,CAAC,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;QACnE,KAAK,OAAO;YACV,OAAO,IAAI,YAAY,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;QACnD;YACE,MAAM,IAAI,cAAc,CAAC,mBAAmB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;gBACrE,IAAI,EAAE,yDAAyD;aAChE,CAAC,CAAC;IACP,CAAC;AACH,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrapeCreators backend. Needs a key, works serverless, about $1.88 per 1,000 ads.
|
|
3
|
+
*
|
|
4
|
+
* Use this instead of the browser when Chromium cannot run (a serverless
|
|
5
|
+
* function, a container without a browser) or when the volume makes babysitting
|
|
6
|
+
* Meta's rate limits not worth it.
|
|
7
|
+
*
|
|
8
|
+
* Request parameters are taken from their published OpenAPI spec, read
|
|
9
|
+
* 2026-08-31. Their *response* envelope is not published, so nothing here
|
|
10
|
+
* assumes a path into it: we walk the payload for ad-shaped objects. See
|
|
11
|
+
* `adlibrary/harvest.ts`.
|
|
12
|
+
*
|
|
13
|
+
* One thing this backend can do that the browser cannot: `transcribe`, which is
|
|
14
|
+
* server-side speech to text on a video ad.
|
|
15
|
+
*/
|
|
16
|
+
import type { Ad, Advertiser, Backend, SearchParams, SearchResult } from "../adlibrary/types.js";
|
|
17
|
+
export declare class ScrapeCreatorsBackend implements Backend {
|
|
18
|
+
private readonly apiKey;
|
|
19
|
+
private readonly timeoutMs;
|
|
20
|
+
readonly name: "scrapecreators";
|
|
21
|
+
readonly needsKey = true;
|
|
22
|
+
constructor(apiKey: string, timeoutMs?: number);
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
private get;
|
|
25
|
+
search(params: SearchParams): Promise<SearchResult>;
|
|
26
|
+
listAdvertisers(query: string): Promise<Advertiser[]>;
|
|
27
|
+
getAd(libraryId: string): Promise<Ad | undefined>;
|
|
28
|
+
transcribe(libraryId: string): Promise<string | undefined>;
|
|
29
|
+
}
|