scavio 0.21.1 → 0.23.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/README.md +181 -4
- package/dist/index.cjs +398 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -28
- package/dist/index.d.ts +601 -28
- package/dist/index.js +384 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -30,6 +40,7 @@ __export(index_exports, {
|
|
|
30
40
|
CostcoNamespace: () => CostcoNamespace,
|
|
31
41
|
DEFAULT_MAX_REQUESTS_PER_SECOND: () => DEFAULT_MAX_REQUESTS_PER_SECOND,
|
|
32
42
|
EbayNamespace: () => EbayNamespace,
|
|
43
|
+
ExtractNamespace: () => ExtractNamespace,
|
|
33
44
|
G2Namespace: () => G2Namespace,
|
|
34
45
|
GlassdoorNamespace: () => GlassdoorNamespace,
|
|
35
46
|
GoogleAdsNamespace: () => GoogleAdsNamespace,
|
|
@@ -57,6 +68,7 @@ __export(index_exports, {
|
|
|
57
68
|
ScavioAPIError: () => ScavioAPIError,
|
|
58
69
|
ScavioConnectionError: () => ScavioConnectionError,
|
|
59
70
|
ScavioError: () => ScavioError,
|
|
71
|
+
ScavioJobTimeoutError: () => ScavioJobTimeoutError,
|
|
60
72
|
ScavioTimeoutError: () => ScavioTimeoutError,
|
|
61
73
|
TargetNamespace: () => TargetNamespace,
|
|
62
74
|
ThreadsNamespace: () => ThreadsNamespace,
|
|
@@ -68,7 +80,8 @@ __export(index_exports, {
|
|
|
68
80
|
XNamespace: () => XNamespace,
|
|
69
81
|
YelpNamespace: () => YelpNamespace,
|
|
70
82
|
YouTubeNamespace: () => YouTubeNamespace,
|
|
71
|
-
ZillowNamespace: () => ZillowNamespace
|
|
83
|
+
ZillowNamespace: () => ZillowNamespace,
|
|
84
|
+
verifyWebhook: () => verifyWebhook
|
|
72
85
|
});
|
|
73
86
|
module.exports = __toCommonJS(index_exports);
|
|
74
87
|
|
|
@@ -155,6 +168,22 @@ var ScavioAPIError = class extends ScavioError {
|
|
|
155
168
|
this.responseBody = responseBody;
|
|
156
169
|
}
|
|
157
170
|
};
|
|
171
|
+
var ScavioJobTimeoutError = class extends ScavioError {
|
|
172
|
+
constructor(jobType, jobId, statusUrl, lastStatus) {
|
|
173
|
+
super(
|
|
174
|
+
`${jobType} ${jobId} was still ${lastStatus} when the wait timed out. It keeps running: poll extract.${jobType}Status("${jobId}") or cancel it.`
|
|
175
|
+
);
|
|
176
|
+
this.jobType = jobType;
|
|
177
|
+
this.jobId = jobId;
|
|
178
|
+
this.statusUrl = statusUrl;
|
|
179
|
+
this.lastStatus = lastStatus;
|
|
180
|
+
this.name = "ScavioJobTimeoutError";
|
|
181
|
+
}
|
|
182
|
+
jobType;
|
|
183
|
+
jobId;
|
|
184
|
+
statusUrl;
|
|
185
|
+
lastStatus;
|
|
186
|
+
};
|
|
158
187
|
|
|
159
188
|
// src/retry.ts
|
|
160
189
|
var DEFAULT_RETRY_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -203,7 +232,7 @@ function parseRetryAfter(header) {
|
|
|
203
232
|
var BASE_URL = "https://api.scavio.dev";
|
|
204
233
|
var DEFAULT_TIMEOUT = 3e4;
|
|
205
234
|
var DEFAULT_MAX_RETRIES = 2;
|
|
206
|
-
var SDK_VERSION = true ? "0.
|
|
235
|
+
var SDK_VERSION = true ? "0.23.0" : "0.0.0";
|
|
207
236
|
var USER_AGENT = `scavio-js/${SDK_VERSION}`;
|
|
208
237
|
function isServerRuntime() {
|
|
209
238
|
const g = globalThis;
|
|
@@ -261,9 +290,18 @@ function getHeader(response, name) {
|
|
|
261
290
|
}
|
|
262
291
|
return null;
|
|
263
292
|
}
|
|
293
|
+
function buildQuery(query) {
|
|
294
|
+
if (!query) return "";
|
|
295
|
+
const params = new URLSearchParams();
|
|
296
|
+
for (const [key, value] of Object.entries(query)) {
|
|
297
|
+
if (value !== void 0) params.append(key, String(value));
|
|
298
|
+
}
|
|
299
|
+
const qs = params.toString();
|
|
300
|
+
return qs ? `?${qs}` : "";
|
|
301
|
+
}
|
|
264
302
|
async function request(options) {
|
|
265
|
-
const url = `${options.baseUrl}${options.path}`;
|
|
266
|
-
const headers = buildHeaders(options.apiKey);
|
|
303
|
+
const url = `${options.baseUrl}${options.path}${buildQuery(options.query)}`;
|
|
304
|
+
const headers = { ...buildHeaders(options.apiKey), ...options.headers };
|
|
267
305
|
const retry = makeRetryConfig(
|
|
268
306
|
options.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
269
307
|
);
|
|
@@ -493,6 +531,21 @@ var RedditNamespace = class {
|
|
|
493
531
|
async trending() {
|
|
494
532
|
return this.client._post("/api/v1/reddit/trending", {});
|
|
495
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Search Reddit comments by keyword. Each result carries the comment text,
|
|
536
|
+
* author, score and date, plus the post and subreddit it was left on.
|
|
537
|
+
* Costs 1 credit.
|
|
538
|
+
*/
|
|
539
|
+
async searchComments(options) {
|
|
540
|
+
return this.client._post("/api/v1/reddit/search/comments", options);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Find subreddits by keyword, with subscriber counts, descriptions and NSFW
|
|
544
|
+
* flags. Costs 1 credit.
|
|
545
|
+
*/
|
|
546
|
+
async searchSubreddits(options) {
|
|
547
|
+
return this.client._post("/api/v1/reddit/search/subreddits", options);
|
|
548
|
+
}
|
|
496
549
|
};
|
|
497
550
|
|
|
498
551
|
// src/namespaces/tiktok.ts
|
|
@@ -510,6 +563,14 @@ var TikTokNamespace = class {
|
|
|
510
563
|
async video(options) {
|
|
511
564
|
return this.client._post("/api/v1/tiktok/video", options);
|
|
512
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Spoken-word transcript of a TikTok video, as timestamped segments and
|
|
568
|
+
* full text. Pass `video_id` or `url`. Videos with no spoken captions
|
|
569
|
+
* (music, dance, muted clips) return `available: false`. 1 credit.
|
|
570
|
+
*/
|
|
571
|
+
async videoTranscript(options) {
|
|
572
|
+
return this.client._post("/api/v1/tiktok/video/transcript", options);
|
|
573
|
+
}
|
|
513
574
|
async videoComments(options) {
|
|
514
575
|
return this.client._post("/api/v1/tiktok/video/comments", options);
|
|
515
576
|
}
|
|
@@ -534,6 +595,107 @@ var TikTokNamespace = class {
|
|
|
534
595
|
async userFollowings(options) {
|
|
535
596
|
return this.client._post("/api/v1/tiktok/user/followings", options);
|
|
536
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Search terms trending on TikTok now (about 90 per call, US-weighted, no
|
|
600
|
+
* region filter). Costs 1 credit.
|
|
601
|
+
*/
|
|
602
|
+
async trendingSearches() {
|
|
603
|
+
return this.client._post("/api/v1/tiktok/trending/searches", {});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* A sample of videos trending on TikTok now, with author, stats, sound and
|
|
607
|
+
* hashtags. Each call returns about 6 videos whatever `count` is set to;
|
|
608
|
+
* call again for a fresh sample. No region filter. Costs 1 credit.
|
|
609
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
610
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
611
|
+
*/
|
|
612
|
+
async trendingVideos(options = {}) {
|
|
613
|
+
return this.client._post("/api/v1/tiktok/trending/videos", options);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* TikTok search autocomplete suggestions for a keyword. Long-tail keywords
|
|
617
|
+
* can return none, which is still a billed answer. Costs 1 credit.
|
|
618
|
+
*/
|
|
619
|
+
async searchSuggestions(options) {
|
|
620
|
+
return this.client._post("/api/v1/tiktok/search/suggestions", options);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* A TikTok sound by id: title, artist, album, duration, how many videos use
|
|
624
|
+
* it, original vs commercial, play URL, cover, and chart rank when charting.
|
|
625
|
+
* Costs 1 credit.
|
|
626
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
627
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
628
|
+
*/
|
|
629
|
+
async music(options) {
|
|
630
|
+
return this.client._post("/api/v1/tiktok/music", options);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Videos that use a TikTok sound, about 14 per page (`count` is ignored
|
|
634
|
+
* upstream). Costs 1 credit.
|
|
635
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
636
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
637
|
+
*/
|
|
638
|
+
async musicVideos(options) {
|
|
639
|
+
return this.client._post("/api/v1/tiktok/music/videos", options);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Search TikTok sounds by keyword: id, title, artist, duration, usage count
|
|
643
|
+
* and play URL. Costs 1 credit.
|
|
644
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
645
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
646
|
+
*/
|
|
647
|
+
async searchMusic(options) {
|
|
648
|
+
return this.client._post("/api/v1/tiktok/search/music", options);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* TikTok's Top 50 or Viral 50 sound chart, with each sound's details and
|
|
652
|
+
* trend. One global chart, no region filter. Costs 1 credit.
|
|
653
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
654
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
655
|
+
*/
|
|
656
|
+
async musicChart(options = {}) {
|
|
657
|
+
return this.client._post("/api/v1/tiktok/music/chart", options);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Creators similar to a TikTok user (about 30 per page), with follower,
|
|
661
|
+
* following, video and like counts, bio and verification. Costs 1 credit.
|
|
662
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
663
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
664
|
+
*/
|
|
665
|
+
async userSimilar(options) {
|
|
666
|
+
return this.client._post("/api/v1/tiktok/user/similar", options);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* A TikTok user's playlists (series): id, name, video count and cover.
|
|
670
|
+
* Accounts without playlists return an empty list. Costs 1 credit.
|
|
671
|
+
*/
|
|
672
|
+
async userPlaylists(options) {
|
|
673
|
+
return this.client._post("/api/v1/tiktok/user/playlists", options);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Videos in a TikTok playlist, with pagination. Costs 1 credit.
|
|
677
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
678
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
679
|
+
*/
|
|
680
|
+
async playlistVideos(options) {
|
|
681
|
+
return this.client._post("/api/v1/tiktok/playlist/videos", options);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Whether a TikTok user is live now: the user, live room status (2 = live,
|
|
685
|
+
* 4 = not live), title, start time and viewer counts. An unknown username is
|
|
686
|
+
* a billed 404. Costs 1 credit.
|
|
687
|
+
*/
|
|
688
|
+
async userLive(options) {
|
|
689
|
+
return this.client._post("/api/v1/tiktok/user/live", options);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* The region a TikTok account is registered in (ISO country code, not the
|
|
693
|
+
* creator's nationality), plus its user id and sec_user_id. An unknown
|
|
694
|
+
* username is a 404 and is not billed. Costs 1 credit.
|
|
695
|
+
*/
|
|
696
|
+
async userRegion(options) {
|
|
697
|
+
return this.client._post("/api/v1/tiktok/user/region", options);
|
|
698
|
+
}
|
|
537
699
|
};
|
|
538
700
|
|
|
539
701
|
// src/namespaces/tiktok-shop.ts
|
|
@@ -826,6 +988,7 @@ var YouTubeNamespace = class {
|
|
|
826
988
|
async commentReplies(options) {
|
|
827
989
|
return this.client._post("/api/v1/youtube/comments/replies", options);
|
|
828
990
|
}
|
|
991
|
+
/** Transcript / captions for a YouTube video. Costs 5 credits. */
|
|
829
992
|
async transcript(options) {
|
|
830
993
|
return this.client._post("/api/v1/youtube/transcript", options);
|
|
831
994
|
}
|
|
@@ -857,6 +1020,21 @@ var YouTubeNamespace = class {
|
|
|
857
1020
|
async streams(options) {
|
|
858
1021
|
return this.client._post("/api/v1/youtube/streams", options);
|
|
859
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* A single YouTube community post: text, author, likes, comment count,
|
|
1025
|
+
* attached images, and a comments_cursor for communityPostComments.
|
|
1026
|
+
* Costs 1 credit.
|
|
1027
|
+
*/
|
|
1028
|
+
async communityPost(options) {
|
|
1029
|
+
return this.client._post("/api/v1/youtube/community/post", options);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Comments on a YouTube community post, with likes, reply counts and
|
|
1033
|
+
* pagination. Costs 1 credit.
|
|
1034
|
+
*/
|
|
1035
|
+
async communityPostComments(options) {
|
|
1036
|
+
return this.client._post("/api/v1/youtube/community/post/comments", options);
|
|
1037
|
+
}
|
|
860
1038
|
};
|
|
861
1039
|
|
|
862
1040
|
// src/namespaces/x.ts
|
|
@@ -898,6 +1076,27 @@ var XNamespace = class {
|
|
|
898
1076
|
async trending(options = {}) {
|
|
899
1077
|
return this.client._post("/api/v1/x/trending", options);
|
|
900
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Find X Communities by keyword, with member counts, topic and NSFW flag.
|
|
1081
|
+
* Costs 1 credit.
|
|
1082
|
+
*/
|
|
1083
|
+
async searchCommunities(options) {
|
|
1084
|
+
return this.client._post("/api/v1/x/search/communities", options);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* An X Community: name, description, creation date, member count, topic and
|
|
1088
|
+
* rules. Costs 1 credit.
|
|
1089
|
+
*/
|
|
1090
|
+
async community(options) {
|
|
1091
|
+
return this.client._post("/api/v1/x/community", options);
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Posts in an X Community, by relevance or recency, with pagination.
|
|
1095
|
+
* Costs 1 credit.
|
|
1096
|
+
*/
|
|
1097
|
+
async communityTweets(options) {
|
|
1098
|
+
return this.client._post("/api/v1/x/community/tweets", options);
|
|
1099
|
+
}
|
|
901
1100
|
};
|
|
902
1101
|
|
|
903
1102
|
// src/namespaces/linkedin.ts
|
|
@@ -2792,10 +2991,128 @@ var MercadoLibreNamespace = class {
|
|
|
2792
2991
|
}
|
|
2793
2992
|
};
|
|
2794
2993
|
|
|
2994
|
+
// src/namespaces/extract.ts
|
|
2995
|
+
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2996
|
+
var DEFAULT_POLL_INTERVAL = 2e3;
|
|
2997
|
+
var DEFAULT_WAIT_TIMEOUT = 6e5;
|
|
2998
|
+
var COLLECT_PAGE_SIZE = 100;
|
|
2999
|
+
function sleep2(ms) {
|
|
3000
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3001
|
+
}
|
|
3002
|
+
var ExtractNamespace = class {
|
|
3003
|
+
constructor(client) {
|
|
3004
|
+
this.client = client;
|
|
3005
|
+
}
|
|
3006
|
+
client;
|
|
3007
|
+
/**
|
|
3008
|
+
* List a site's page URLs, gathered from its sitemaps and its start page,
|
|
3009
|
+
* deduplicated, filtered by path and limited. Pages disallowed by the
|
|
3010
|
+
* site's robots.txt are left out. 1 credit on the normal tier.
|
|
3011
|
+
*/
|
|
3012
|
+
async map(options) {
|
|
3013
|
+
return await this.client._post("/api/v1/extract/map", options);
|
|
3014
|
+
}
|
|
3015
|
+
async crawl(options, request2 = {}) {
|
|
3016
|
+
return this.start("crawl", options, request2);
|
|
3017
|
+
}
|
|
3018
|
+
/** A crawl's progress and its page results, paged by cursor. Free. */
|
|
3019
|
+
async crawlStatus(id, query) {
|
|
3020
|
+
return this.status("crawl", id, query);
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Stop a queued or running crawl. No further pages are fetched or billed;
|
|
3024
|
+
* results so far stay available. A finished crawl answers 409. Free.
|
|
3025
|
+
*/
|
|
3026
|
+
async cancelCrawl(id) {
|
|
3027
|
+
return this.cancel("crawl", id);
|
|
3028
|
+
}
|
|
3029
|
+
async batch(options, request2 = {}) {
|
|
3030
|
+
return this.start("batch", options, request2);
|
|
3031
|
+
}
|
|
3032
|
+
/** A batch's progress and its page results in input order, paged by cursor. Free. */
|
|
3033
|
+
async batchStatus(id, query) {
|
|
3034
|
+
return this.status("batch", id, query);
|
|
3035
|
+
}
|
|
3036
|
+
/** Stop a queued or running batch. A finished batch answers 409. Free. */
|
|
3037
|
+
async cancelBatch(id) {
|
|
3038
|
+
return this.cancel("batch", id);
|
|
3039
|
+
}
|
|
3040
|
+
async start(type, body, request2) {
|
|
3041
|
+
const headers = request2.idempotencyKey ? { "Idempotency-Key": request2.idempotencyKey } : void 0;
|
|
3042
|
+
const created = await this.client._post(
|
|
3043
|
+
`/api/v1/extract/${type}`,
|
|
3044
|
+
body,
|
|
3045
|
+
headers
|
|
3046
|
+
);
|
|
3047
|
+
if (!request2.wait) return created;
|
|
3048
|
+
return this.waitFor(type, created.data.id, created.data.status_url, request2);
|
|
3049
|
+
}
|
|
3050
|
+
async status(type, id, query = {}) {
|
|
3051
|
+
return await this.client._get(`/api/v1/extract/${type}/${encodeURIComponent(id)}`, {
|
|
3052
|
+
cursor: query.cursor,
|
|
3053
|
+
limit: query.limit,
|
|
3054
|
+
status: query.status,
|
|
3055
|
+
include_content: query.include_content
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
async cancel(type, id) {
|
|
3059
|
+
return await this.client._delete(
|
|
3060
|
+
`/api/v1/extract/${type}/${encodeURIComponent(id)}`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
async waitFor(type, id, statusUrl, request2) {
|
|
3064
|
+
const interval = request2.pollInterval ?? DEFAULT_POLL_INTERVAL;
|
|
3065
|
+
const deadline = Date.now() + (request2.waitTimeout ?? DEFAULT_WAIT_TIMEOUT);
|
|
3066
|
+
for (; ; ) {
|
|
3067
|
+
const probe = await this.status(type, id, { limit: 1, include_content: false });
|
|
3068
|
+
if (TERMINAL.has(probe.data.status)) break;
|
|
3069
|
+
if (Date.now() + interval > deadline) {
|
|
3070
|
+
throw new ScavioJobTimeoutError(type, id, statusUrl, probe.data.status);
|
|
3071
|
+
}
|
|
3072
|
+
await sleep2(interval);
|
|
3073
|
+
}
|
|
3074
|
+
const results = [];
|
|
3075
|
+
let cursor;
|
|
3076
|
+
let last;
|
|
3077
|
+
do {
|
|
3078
|
+
last = await this.status(type, id, { cursor, limit: COLLECT_PAGE_SIZE });
|
|
3079
|
+
results.push(...last.data.results);
|
|
3080
|
+
cursor = last.data.next_cursor ?? void 0;
|
|
3081
|
+
} while (cursor);
|
|
3082
|
+
return { data: { ...last.data, results, next_cursor: null } };
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
|
|
2795
3086
|
// src/client.ts
|
|
2796
3087
|
var DEFAULT_MAX_REQUESTS_PER_SECOND = 1;
|
|
2797
3088
|
var MAX_REQUESTS_PER_SECOND = 50;
|
|
2798
|
-
var
|
|
3089
|
+
var ScavioCore = class {
|
|
3090
|
+
/**
|
|
3091
|
+
* Read ANY web page and get it back as readability Markdown (the default),
|
|
3092
|
+
* plain text, or raw HTML. Returns `{ url, format, mode, content,
|
|
3093
|
+
* content_length }`.
|
|
3094
|
+
*
|
|
3095
|
+
* Extract is a core endpoint, not a platform: `scavio.extract({ url })`
|
|
3096
|
+
* reads one page, and `scavio.extract.map()` / `.crawl()` / `.batch()` work
|
|
3097
|
+
* on whole sites and URL lists.
|
|
3098
|
+
*
|
|
3099
|
+
* Credits are a function of `mode`, not a flat per-call constant:
|
|
3100
|
+
* "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
|
|
3101
|
+
* only on a successful extraction - a dead link, bot wall or timeout costs
|
|
3102
|
+
* nothing.
|
|
3103
|
+
*
|
|
3104
|
+
* Start on "normal". Move to "advanced" when the page builds its content in
|
|
3105
|
+
* the browser, and to "ultra" only when a bot wall blocks the other two.
|
|
3106
|
+
*
|
|
3107
|
+
* @example
|
|
3108
|
+
* const page = await scavio.extract({ url: "https://example.com/pricing" });
|
|
3109
|
+
* console.log(page.content);
|
|
3110
|
+
*/
|
|
3111
|
+
async extract(options) {
|
|
3112
|
+
return this._post("/api/v1/extract", options);
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
var Scavio = class extends ScavioCore {
|
|
2799
3116
|
google;
|
|
2800
3117
|
amazon;
|
|
2801
3118
|
walmart;
|
|
@@ -2838,6 +3155,7 @@ var Scavio = class {
|
|
|
2838
3155
|
maxRetries;
|
|
2839
3156
|
rateLimiter;
|
|
2840
3157
|
constructor(config) {
|
|
3158
|
+
super();
|
|
2841
3159
|
this.apiKey = config?.apiKey ?? process.env.SCAVIO_API_KEY ?? "";
|
|
2842
3160
|
if (!this.apiKey) {
|
|
2843
3161
|
throw new MissingAPIKeyError();
|
|
@@ -2888,9 +3206,19 @@ var Scavio = class {
|
|
|
2888
3206
|
this.realtor = new RealtorNamespace(this);
|
|
2889
3207
|
this.rightmove = new RightmoveNamespace(this);
|
|
2890
3208
|
this.mercadolibre = new MercadoLibreNamespace(this);
|
|
3209
|
+
const extractNs = new ExtractNamespace(this);
|
|
3210
|
+
this.extract = Object.assign(ScavioCore.prototype.extract.bind(this), {
|
|
3211
|
+
map: extractNs.map.bind(extractNs),
|
|
3212
|
+
crawl: extractNs.crawl.bind(extractNs),
|
|
3213
|
+
crawlStatus: extractNs.crawlStatus.bind(extractNs),
|
|
3214
|
+
cancelCrawl: extractNs.cancelCrawl.bind(extractNs),
|
|
3215
|
+
batch: extractNs.batch.bind(extractNs),
|
|
3216
|
+
batchStatus: extractNs.batchStatus.bind(extractNs),
|
|
3217
|
+
cancelBatch: extractNs.cancelBatch.bind(extractNs)
|
|
3218
|
+
});
|
|
2891
3219
|
}
|
|
2892
3220
|
/** @internal */
|
|
2893
|
-
async _post(path, body) {
|
|
3221
|
+
async _post(path, body, headers) {
|
|
2894
3222
|
return request({
|
|
2895
3223
|
method: "POST",
|
|
2896
3224
|
path,
|
|
@@ -2899,11 +3227,12 @@ var Scavio = class {
|
|
|
2899
3227
|
timeout: this.timeout,
|
|
2900
3228
|
maxRetries: this.maxRetries,
|
|
2901
3229
|
rateLimiter: this.rateLimiter,
|
|
2902
|
-
body
|
|
3230
|
+
body,
|
|
3231
|
+
headers
|
|
2903
3232
|
});
|
|
2904
3233
|
}
|
|
2905
3234
|
/** @internal */
|
|
2906
|
-
async _get(path) {
|
|
3235
|
+
async _get(path, query) {
|
|
2907
3236
|
return request({
|
|
2908
3237
|
method: "GET",
|
|
2909
3238
|
path,
|
|
@@ -2911,39 +3240,73 @@ var Scavio = class {
|
|
|
2911
3240
|
baseUrl: this.baseUrl,
|
|
2912
3241
|
timeout: this.timeout,
|
|
2913
3242
|
maxRetries: this.maxRetries,
|
|
3243
|
+
rateLimiter: this.rateLimiter,
|
|
3244
|
+
query
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
/** @internal */
|
|
3248
|
+
async _delete(path) {
|
|
3249
|
+
return request({
|
|
3250
|
+
method: "DELETE",
|
|
3251
|
+
path,
|
|
3252
|
+
apiKey: this.apiKey,
|
|
3253
|
+
baseUrl: this.baseUrl,
|
|
3254
|
+
timeout: this.timeout,
|
|
3255
|
+
maxRetries: this.maxRetries,
|
|
2914
3256
|
rateLimiter: this.rateLimiter
|
|
2915
3257
|
});
|
|
2916
3258
|
}
|
|
2917
3259
|
async search(options) {
|
|
2918
3260
|
return this.google.search(options);
|
|
2919
3261
|
}
|
|
2920
|
-
/**
|
|
2921
|
-
* Read ANY web page and get it back as readability Markdown (the default),
|
|
2922
|
-
* plain text, or raw HTML. Returns `{ url, format, mode, content,
|
|
2923
|
-
* content_length }`.
|
|
2924
|
-
*
|
|
2925
|
-
* This is a core endpoint, not a platform, so it lives on the client itself:
|
|
2926
|
-
* `scavio.extract({ url })`, never `scavio.extract.extract()`.
|
|
2927
|
-
*
|
|
2928
|
-
* Credits are a function of `mode`, not a flat per-call constant:
|
|
2929
|
-
* "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
|
|
2930
|
-
* only on a successful extraction - a dead link, bot wall or timeout costs
|
|
2931
|
-
* nothing.
|
|
2932
|
-
*
|
|
2933
|
-
* Start on "normal". Move to "advanced" when the page builds its content in
|
|
2934
|
-
* the browser, and to "ultra" only when a bot wall blocks the other two.
|
|
2935
|
-
*
|
|
2936
|
-
* @example
|
|
2937
|
-
* const page = await scavio.extract({ url: "https://example.com/pricing" });
|
|
2938
|
-
* console.log(page.content);
|
|
2939
|
-
*/
|
|
2940
|
-
async extract(options) {
|
|
2941
|
-
return this._post("/api/v1/extract", options);
|
|
2942
|
-
}
|
|
2943
3262
|
async getUsage() {
|
|
2944
3263
|
return this._get("/api/v1/usage");
|
|
2945
3264
|
}
|
|
2946
3265
|
};
|
|
3266
|
+
|
|
3267
|
+
// src/webhook.ts
|
|
3268
|
+
async function verifyWebhook(secret, header, body, toleranceSec = 300) {
|
|
3269
|
+
if (!secret || !header) return false;
|
|
3270
|
+
const parts = {};
|
|
3271
|
+
for (const piece of header.split(",")) {
|
|
3272
|
+
const i = piece.indexOf("=");
|
|
3273
|
+
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
|
|
3274
|
+
}
|
|
3275
|
+
const t = Number(parts.t);
|
|
3276
|
+
const v1 = parts.v1;
|
|
3277
|
+
if (!parts.t || !Number.isFinite(t) || !v1 || !/^[0-9a-f]+$/i.test(v1)) return false;
|
|
3278
|
+
if (Math.abs(Math.floor(Date.now() / 1e3) - t) > toleranceSec) return false;
|
|
3279
|
+
const subtle = await getSubtle();
|
|
3280
|
+
const enc = new TextEncoder();
|
|
3281
|
+
const key = await subtle.importKey(
|
|
3282
|
+
"raw",
|
|
3283
|
+
enc.encode(secret),
|
|
3284
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
3285
|
+
false,
|
|
3286
|
+
["sign"]
|
|
3287
|
+
);
|
|
3288
|
+
const bodyBytes = typeof body === "string" ? enc.encode(body) : body;
|
|
3289
|
+
const prefix = enc.encode(`${t}.`);
|
|
3290
|
+
const message = new Uint8Array(prefix.length + bodyBytes.length);
|
|
3291
|
+
message.set(prefix, 0);
|
|
3292
|
+
message.set(bodyBytes, prefix.length);
|
|
3293
|
+
const mac = new Uint8Array(await subtle.sign("HMAC", key, message));
|
|
3294
|
+
let expected = "";
|
|
3295
|
+
for (const b of mac) expected += b.toString(16).padStart(2, "0");
|
|
3296
|
+
return timingSafeEqualHex(expected, v1.toLowerCase());
|
|
3297
|
+
}
|
|
3298
|
+
function timingSafeEqualHex(a, b) {
|
|
3299
|
+
if (a.length !== b.length) return false;
|
|
3300
|
+
let diff = 0;
|
|
3301
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3302
|
+
return diff === 0;
|
|
3303
|
+
}
|
|
3304
|
+
async function getSubtle() {
|
|
3305
|
+
const g = globalThis;
|
|
3306
|
+
if (g.crypto?.subtle) return g.crypto.subtle;
|
|
3307
|
+
const nodeCrypto = await import("crypto");
|
|
3308
|
+
return nodeCrypto.webcrypto.subtle;
|
|
3309
|
+
}
|
|
2947
3310
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2948
3311
|
0 && (module.exports = {
|
|
2949
3312
|
AirbnbNamespace,
|
|
@@ -2956,6 +3319,7 @@ var Scavio = class {
|
|
|
2956
3319
|
CostcoNamespace,
|
|
2957
3320
|
DEFAULT_MAX_REQUESTS_PER_SECOND,
|
|
2958
3321
|
EbayNamespace,
|
|
3322
|
+
ExtractNamespace,
|
|
2959
3323
|
G2Namespace,
|
|
2960
3324
|
GlassdoorNamespace,
|
|
2961
3325
|
GoogleAdsNamespace,
|
|
@@ -2983,6 +3347,7 @@ var Scavio = class {
|
|
|
2983
3347
|
ScavioAPIError,
|
|
2984
3348
|
ScavioConnectionError,
|
|
2985
3349
|
ScavioError,
|
|
3350
|
+
ScavioJobTimeoutError,
|
|
2986
3351
|
ScavioTimeoutError,
|
|
2987
3352
|
TargetNamespace,
|
|
2988
3353
|
ThreadsNamespace,
|
|
@@ -2994,6 +3359,7 @@ var Scavio = class {
|
|
|
2994
3359
|
XNamespace,
|
|
2995
3360
|
YelpNamespace,
|
|
2996
3361
|
YouTubeNamespace,
|
|
2997
|
-
ZillowNamespace
|
|
3362
|
+
ZillowNamespace,
|
|
3363
|
+
verifyWebhook
|
|
2998
3364
|
});
|
|
2999
3365
|
//# sourceMappingURL=index.cjs.map
|