scavio 0.4.0 → 0.6.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 CHANGED
@@ -104,15 +104,45 @@ await client.walmart.product({
104
104
  // Search videos
105
105
  await client.youtube.search({
106
106
  query: "typescript tutorial",
107
- upload_date: "week", // optional
108
- sort_by: "relevance", // optional
109
- hd: true, // optional
107
+ upload_date: "this_week", // optional
108
+ sort_by: "relevance", // optional
109
+ features: ["hd", "4k"], // optional
110
+ cursor: "...", // optional (pagination)
110
111
  });
111
112
 
112
- // Get video metadata
113
- await client.youtube.metadata({
113
+ // Search Shorts
114
+ await client.youtube.shorts({ query: "cooking" });
115
+
116
+ // Search-as-you-type suggestions
117
+ await client.youtube.suggestions({ query: "how to" });
118
+
119
+ // Get video details (accepts an id or a full watch URL)
120
+ await client.youtube.video({ video_id: "dQw4w9WgXcQ" });
121
+ // youtube.metadata() is a deprecated alias of youtube.video()
122
+
123
+ // Video comments and threaded replies
124
+ await client.youtube.comments({ video_id: "dQw4w9WgXcQ" });
125
+ await client.youtube.commentReplies({
114
126
  video_id: "dQw4w9WgXcQ",
127
+ reply_cursor: "...", // from a comment's reply_cursor
115
128
  });
129
+
130
+ // Transcript / subtitles (format: 'text' or 'srt')
131
+ await client.youtube.transcript({ video_id: "dQw4w9WgXcQ", format: "srt" });
132
+
133
+ // Related videos
134
+ await client.youtube.related({ video_id: "dQw4w9WgXcQ" });
135
+
136
+ // Playable / downloadable stream URLs
137
+ await client.youtube.streams({ video_id: "dQw4w9WgXcQ" });
138
+
139
+ // Channels
140
+ await client.youtube.channelSearch({ query: "mkbhd" });
141
+ await client.youtube.channel({ channel_id: "@mkbhd" }); // id, @handle, or URL
142
+ await client.youtube.channelVideos({ channel_id: "UC..." });
143
+ await client.youtube.channelShorts({ channel_id: "UC..." });
144
+ await client.youtube.channelCommunity({ channel_id: "UC..." });
145
+ await client.youtube.channelResolve({ channel: "@mkbhd" }); // handle/URL -> id
116
146
  ```
117
147
 
118
148
  ### Reddit
@@ -246,11 +276,13 @@ MIT
246
276
 
247
277
  ## About Scavio
248
278
 
249
- [Scavio](https://scavio.dev) is a unified [search API](https://scavio.dev/docs/search-api) built for AI agents — one API key, structured JSON, no scraping or proxies. A real-time [Tavily alternative](https://scavio.dev) and [SerpAPI alternative](https://scavio.dev) with data from:
279
+ [Scavio](https://scavio.dev) is a unified [search API for AI agents](https://scavio.dev/search-api-for-ai-agents) — one API key, structured JSON, no scraping or proxies. A real-time [Tavily alternative](https://scavio.dev/alternatives/tavily) and [SerpAPI alternative](https://scavio.dev/alternatives/serpapi) with data from:
280
+
281
+ - [Google Search API](https://scavio.dev/google-search-api) — SERP results, news, images, maps, and knowledge graph
282
+ - [Amazon Product API](https://scavio.dev/amazon-product-api) and [Walmart Product API](https://scavio.dev/walmart-product-api) — product search and details
283
+ - [YouTube API](https://scavio.dev/youtube-transcript-api), [TikTok API](https://scavio.dev/tiktok-api), and [Instagram API](https://scavio.dev/instagram-api) — video and social media data
284
+ - [Reddit API](https://scavio.dev/reddit-api) — posts and threaded comments
250
285
 
251
- - [Google Search API](https://scavio.dev/docs/search-api) — SERP results, news, images, maps, and knowledge graph
252
- - [Amazon Product API](https://scavio.dev/docs/amazon-api) and [Walmart API](https://scavio.dev/docs/walmart-api) — product search and details
253
- - [YouTube API](https://scavio.dev/docs/youtube-api), [TikTok API](https://scavio.dev/docs/tiktok-api), and [Instagram API](https://scavio.dev/docs/instagram-api) — video and social media data
254
- - [Reddit API](https://scavio.dev/docs/reddit-api) — posts and threaded comments
286
+ Teams choosing between providers can [compare Scavio vs alternatives](https://scavio.dev/compare) side by side.
255
287
 
256
288
  Get a free [API key](https://dashboard.scavio.dev) and explore the [documentation](https://scavio.dev/docs/introduction).
package/dist/index.cjs CHANGED
@@ -24,10 +24,13 @@ __export(index_exports, {
24
24
  InsufficientCreditsError: () => InsufficientCreditsError,
25
25
  InvalidAPIKeyError: () => InvalidAPIKeyError,
26
26
  MissingAPIKeyError: () => MissingAPIKeyError,
27
+ NotFoundError: () => NotFoundError,
27
28
  RateLimitError: () => RateLimitError,
28
29
  Scavio: () => Scavio,
29
30
  ScavioAPIError: () => ScavioAPIError,
30
- ScavioError: () => ScavioError
31
+ ScavioConnectionError: () => ScavioConnectionError,
32
+ ScavioError: () => ScavioError,
33
+ ScavioTimeoutError: () => ScavioTimeoutError
31
34
  });
32
35
  module.exports = __toCommonJS(index_exports);
33
36
 
@@ -46,42 +49,121 @@ var MissingAPIKeyError = class extends ScavioError {
46
49
  this.name = "MissingAPIKeyError";
47
50
  }
48
51
  };
52
+ var ScavioConnectionError = class extends ScavioError {
53
+ constructor(message = "Connection error") {
54
+ super(message);
55
+ this.name = "ScavioConnectionError";
56
+ }
57
+ };
58
+ var ScavioTimeoutError = class extends ScavioError {
59
+ constructor(message = "Request timed out") {
60
+ super(message);
61
+ this.name = "ScavioTimeoutError";
62
+ }
63
+ };
49
64
  var InvalidAPIKeyError = class extends ScavioError {
50
- constructor(message = "Invalid API key") {
65
+ statusCode = 401;
66
+ responseBody;
67
+ constructor(message = "Invalid API key", responseBody) {
51
68
  super(message);
52
69
  this.name = "InvalidAPIKeyError";
70
+ this.responseBody = responseBody;
53
71
  }
54
72
  };
55
73
  var InsufficientCreditsError = class extends ScavioError {
56
- constructor(message = "Insufficient credits") {
74
+ statusCode = 402;
75
+ responseBody;
76
+ constructor(message = "Insufficient credits", responseBody) {
57
77
  super(message);
58
78
  this.name = "InsufficientCreditsError";
79
+ this.responseBody = responseBody;
59
80
  }
60
81
  };
61
82
  var BadRequestError = class extends ScavioError {
62
- constructor(message = "Bad request") {
83
+ statusCode = 400;
84
+ responseBody;
85
+ constructor(message = "Bad request", responseBody) {
63
86
  super(message);
64
87
  this.name = "BadRequestError";
88
+ this.responseBody = responseBody;
89
+ }
90
+ };
91
+ var NotFoundError = class extends ScavioError {
92
+ statusCode = 404;
93
+ responseBody;
94
+ constructor(message = "Not found", responseBody) {
95
+ super(message);
96
+ this.name = "NotFoundError";
97
+ this.responseBody = responseBody;
65
98
  }
66
99
  };
67
100
  var RateLimitError = class extends ScavioError {
68
- constructor(message = "Rate limit exceeded") {
101
+ statusCode = 429;
102
+ responseBody;
103
+ constructor(message = "Rate limit exceeded", responseBody) {
69
104
  super(message);
70
105
  this.name = "RateLimitError";
106
+ this.responseBody = responseBody;
71
107
  }
72
108
  };
73
109
  var ScavioAPIError = class extends ScavioError {
74
110
  statusCode;
75
- constructor(statusCode, message) {
111
+ responseBody;
112
+ constructor(statusCode, message, responseBody) {
76
113
  super(`API error ${statusCode}: ${message}`);
77
114
  this.name = "ScavioAPIError";
78
115
  this.statusCode = statusCode;
116
+ this.responseBody = responseBody;
79
117
  }
80
118
  };
81
119
 
120
+ // src/retry.ts
121
+ var DEFAULT_RETRY_STATUSES = /* @__PURE__ */ new Set([
122
+ 429,
123
+ 500,
124
+ 502,
125
+ 503,
126
+ 504
127
+ ]);
128
+ function makeRetryConfig(maxRetries) {
129
+ return {
130
+ maxRetries,
131
+ baseDelay: 0.5,
132
+ maxDelay: 8,
133
+ retryStatuses: DEFAULT_RETRY_STATUSES
134
+ };
135
+ }
136
+ function shouldRetryStatus(config, statusCode, attempt) {
137
+ return attempt < config.maxRetries && config.retryStatuses.has(statusCode);
138
+ }
139
+ function shouldRetryException(config, attempt) {
140
+ return attempt < config.maxRetries;
141
+ }
142
+ function backoff(config, attempt, retryAfter) {
143
+ if (retryAfter !== void 0) {
144
+ return Math.min(Math.max(retryAfter, 0), config.maxDelay);
145
+ }
146
+ const capped = Math.min(config.maxDelay, config.baseDelay * 2 ** attempt);
147
+ return Math.random() * capped;
148
+ }
149
+ function parseRetryAfter(header) {
150
+ if (!header) return void 0;
151
+ const trimmed = header.trim();
152
+ const asNumber = Number(trimmed);
153
+ if (!Number.isNaN(asNumber) && trimmed !== "") {
154
+ return asNumber;
155
+ }
156
+ const asDate = Date.parse(trimmed);
157
+ if (!Number.isNaN(asDate)) {
158
+ return Math.max((asDate - Date.now()) / 1e3, 0);
159
+ }
160
+ return void 0;
161
+ }
162
+
82
163
  // src/http.ts
83
164
  var BASE_URL = "https://api.scavio.dev";
84
165
  var DEFAULT_TIMEOUT = 3e4;
166
+ var DEFAULT_MAX_RETRIES = 2;
85
167
  function buildHeaders(apiKey) {
86
168
  return {
87
169
  Authorization: `Bearer ${apiKey}`,
@@ -104,39 +186,80 @@ function handleError(statusCode, body) {
104
186
  error = error.message;
105
187
  }
106
188
  const msg = String(error);
107
- if (statusCode === 400) throw new BadRequestError(msg);
108
- if (statusCode === 401) throw new InvalidAPIKeyError(msg);
109
- if (statusCode === 402) throw new InsufficientCreditsError(msg);
110
- if (statusCode === 429) throw new RateLimitError(msg);
111
- throw new ScavioAPIError(statusCode, msg);
189
+ const responseBody = Object.keys(body).length > 0 ? body : void 0;
190
+ if (statusCode === 400) throw new BadRequestError(msg, responseBody);
191
+ if (statusCode === 401) throw new InvalidAPIKeyError(msg, responseBody);
192
+ if (statusCode === 402) throw new InsufficientCreditsError(msg, responseBody);
193
+ if (statusCode === 404) throw new NotFoundError(msg, responseBody);
194
+ if (statusCode === 429) throw new RateLimitError(msg, responseBody);
195
+ throw new ScavioAPIError(statusCode, msg, responseBody);
196
+ }
197
+ function sleep(seconds) {
198
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
199
+ }
200
+ function isAbortError(err) {
201
+ return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
202
+ }
203
+ function getHeader(response, name) {
204
+ const headers = response.headers;
205
+ if (headers && typeof headers.get === "function") {
206
+ return headers.get(name);
207
+ }
208
+ return null;
112
209
  }
113
210
  async function request(options) {
114
- await options.rateLimiter.wait();
115
211
  const url = `${options.baseUrl}${options.path}`;
116
212
  const headers = buildHeaders(options.apiKey);
117
- const controller = new AbortController();
118
- const timeoutId = setTimeout(() => controller.abort(), options.timeout);
119
- try {
120
- const fetchOptions = {
121
- method: options.method,
122
- headers,
123
- signal: controller.signal
124
- };
125
- if (options.method === "POST" && options.body) {
126
- fetchOptions.body = JSON.stringify(stripUndefined(options.body));
127
- }
128
- const response = await fetch(url, fetchOptions);
129
- if (!response.ok) {
130
- let body = {};
131
- try {
132
- body = await response.json();
133
- } catch {
213
+ const retry = makeRetryConfig(
214
+ options.maxRetries ?? DEFAULT_MAX_RETRIES
215
+ );
216
+ let attempt = 0;
217
+ for (; ; ) {
218
+ await options.rateLimiter.wait();
219
+ const controller = new AbortController();
220
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout);
221
+ let response;
222
+ try {
223
+ const fetchOptions = {
224
+ method: options.method,
225
+ headers,
226
+ signal: controller.signal
227
+ };
228
+ if (options.method === "POST" && options.body) {
229
+ fetchOptions.body = JSON.stringify(stripUndefined(options.body));
230
+ }
231
+ response = await fetch(url, fetchOptions);
232
+ } catch (err) {
233
+ clearTimeout(timeoutId);
234
+ const timedOut = isAbortError(err);
235
+ if (shouldRetryException(retry, attempt)) {
236
+ await sleep(backoff(retry, attempt));
237
+ attempt += 1;
238
+ continue;
239
+ }
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ if (timedOut) {
242
+ throw new ScavioTimeoutError(msg);
134
243
  }
135
- handleError(response.status, body);
244
+ throw new ScavioConnectionError(msg);
245
+ } finally {
246
+ clearTimeout(timeoutId);
247
+ }
248
+ if (response.ok) {
249
+ return await response.json();
250
+ }
251
+ if (shouldRetryStatus(retry, response.status, attempt)) {
252
+ const retryAfter = parseRetryAfter(getHeader(response, "Retry-After"));
253
+ await sleep(backoff(retry, attempt, retryAfter));
254
+ attempt += 1;
255
+ continue;
256
+ }
257
+ let body = {};
258
+ try {
259
+ body = await response.json();
260
+ } catch {
136
261
  }
137
- return await response.json();
138
- } finally {
139
- clearTimeout(timeoutId);
262
+ handleError(response.status, body);
140
263
  }
141
264
  }
142
265
 
@@ -188,6 +311,10 @@ var AmazonNamespace = class {
188
311
  ...rest
189
312
  });
190
313
  }
314
+ /** Supported Amazon domains, languages, currencies, and countries. */
315
+ async options() {
316
+ return this.client._get("/api/v1/amazon/options");
317
+ }
191
318
  };
192
319
 
193
320
  // src/namespaces/google.ts
@@ -374,14 +501,73 @@ var YouTubeNamespace = class {
374
501
  }
375
502
  client;
376
503
  async search(options) {
504
+ const { query, fourK, video_360, video_3d, ...rest } = options;
505
+ const body = {
506
+ search: query,
507
+ ...rest
508
+ };
509
+ if (fourK !== void 0) body["4k"] = fourK;
510
+ if (video_360 !== void 0) body["360"] = video_360;
511
+ if (video_3d !== void 0) body["3d"] = video_3d;
512
+ return this.client._post("/api/v1/youtube/search", body);
513
+ }
514
+ async shorts(options) {
515
+ const { query, ...rest } = options;
516
+ return this.client._post("/api/v1/youtube/shorts", {
517
+ search: query,
518
+ ...rest
519
+ });
520
+ }
521
+ async suggestions(options) {
377
522
  const { query, ...rest } = options;
378
- return this.client._post("/api/v1/youtube/search", {
523
+ return this.client._post("/api/v1/youtube/suggestions", {
379
524
  search: query,
380
525
  ...rest
381
526
  });
382
527
  }
528
+ async video(options) {
529
+ return this.client._post("/api/v1/youtube/video", options);
530
+ }
531
+ /** @deprecated Use youtube.video(). Alias kept for backward compatibility. */
383
532
  async metadata(options) {
384
- return this.client._post("/api/v1/youtube/metadata", options);
533
+ return this.video(options);
534
+ }
535
+ async comments(options) {
536
+ return this.client._post("/api/v1/youtube/comments", options);
537
+ }
538
+ async commentReplies(options) {
539
+ return this.client._post("/api/v1/youtube/comments/replies", options);
540
+ }
541
+ async transcript(options) {
542
+ return this.client._post("/api/v1/youtube/transcript", options);
543
+ }
544
+ async related(options) {
545
+ return this.client._post("/api/v1/youtube/related", options);
546
+ }
547
+ async channelSearch(options) {
548
+ const { query, ...rest } = options;
549
+ return this.client._post("/api/v1/youtube/channel/search", {
550
+ search: query,
551
+ ...rest
552
+ });
553
+ }
554
+ async channel(options) {
555
+ return this.client._post("/api/v1/youtube/channel", options);
556
+ }
557
+ async channelVideos(options) {
558
+ return this.client._post("/api/v1/youtube/channel/videos", options);
559
+ }
560
+ async channelShorts(options) {
561
+ return this.client._post("/api/v1/youtube/channel/shorts", options);
562
+ }
563
+ async channelCommunity(options) {
564
+ return this.client._post("/api/v1/youtube/channel/community", options);
565
+ }
566
+ async channelResolve(options) {
567
+ return this.client._post("/api/v1/youtube/channel/resolve", options);
568
+ }
569
+ async streams(options) {
570
+ return this.client._post("/api/v1/youtube/streams", options);
385
571
  }
386
572
  };
387
573
 
@@ -397,6 +583,7 @@ var Scavio = class {
397
583
  apiKey;
398
584
  baseUrl;
399
585
  timeout;
586
+ maxRetries;
400
587
  rateLimiter;
401
588
  constructor(config) {
402
589
  this.apiKey = config?.apiKey ?? process.env.SCAVIO_API_KEY ?? "";
@@ -405,6 +592,7 @@ var Scavio = class {
405
592
  }
406
593
  this.baseUrl = (config?.baseUrl ?? BASE_URL).replace(/\/+$/, "");
407
594
  this.timeout = config?.timeout ?? DEFAULT_TIMEOUT;
595
+ this.maxRetries = config?.maxRetries ?? DEFAULT_MAX_RETRIES;
408
596
  const rps = config?.maxRequestsPerSecond ?? 1;
409
597
  if (rps < 1 || rps > 10) {
410
598
  throw new ScavioError("maxRequestsPerSecond must be between 1 and 10");
@@ -426,6 +614,7 @@ var Scavio = class {
426
614
  apiKey: this.apiKey,
427
615
  baseUrl: this.baseUrl,
428
616
  timeout: this.timeout,
617
+ maxRetries: this.maxRetries,
429
618
  rateLimiter: this.rateLimiter,
430
619
  body
431
620
  });
@@ -438,6 +627,7 @@ var Scavio = class {
438
627
  apiKey: this.apiKey,
439
628
  baseUrl: this.baseUrl,
440
629
  timeout: this.timeout,
630
+ maxRetries: this.maxRetries,
441
631
  rateLimiter: this.rateLimiter
442
632
  });
443
633
  }
@@ -454,9 +644,12 @@ var Scavio = class {
454
644
  InsufficientCreditsError,
455
645
  InvalidAPIKeyError,
456
646
  MissingAPIKeyError,
647
+ NotFoundError,
457
648
  RateLimitError,
458
649
  Scavio,
459
650
  ScavioAPIError,
460
- ScavioError
651
+ ScavioConnectionError,
652
+ ScavioError,
653
+ ScavioTimeoutError
461
654
  });
462
655
  //# sourceMappingURL=index.cjs.map