podscan 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Podscan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # podscan
2
+
3
+ Lightweight, zero-dependency TypeScript SDK for the [Podscan REST API](https://podscan.fm/rest-api). Optimized for AWS Lambda and serverless environments.
4
+
5
+ - **Zero runtime dependencies** -- uses native `fetch` (Node 18+)
6
+ - **Dual format** -- ESM and CommonJS
7
+ - **Fully typed** -- complete TypeScript definitions including episode metadata (hosts, guests, speakers)
8
+ - **Auto-pagination** -- `searchAll()` async iterators walk all pages automatically
9
+ - **Time period helpers** -- `periods.thisWeek()`, `periods.lastMonth()`, etc.
10
+ - **Delta sync** -- checkpoint-based tracking for incremental data pulls
11
+ - **Tiny** -- under 10 KB minified (ESM)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install podscan
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { PodscanClient } from 'podscan';
23
+
24
+ const client = new PodscanClient({
25
+ apiKey: process.env.PODSCAN_API_KEY!,
26
+ });
27
+
28
+ // Search episodes
29
+ const results = await client.episodes.search({
30
+ query: 'AI marketing',
31
+ language: 'en',
32
+ per_page: 10,
33
+ });
34
+
35
+ console.log(`Found ${results.pagination.total} episodes`);
36
+
37
+ for (const episode of results.episodes) {
38
+ console.log(`${episode.episode_title} — ${episode.podcast?.podcast_name}`);
39
+ }
40
+ ```
41
+
42
+ ## Lambda Usage
43
+
44
+ The SDK uses native `fetch` with no external dependencies, so cold starts are fast and bundle size is minimal.
45
+
46
+ ```typescript
47
+ import { PodscanClient } from 'podscan';
48
+
49
+ const client = new PodscanClient({
50
+ apiKey: process.env.PODSCAN_API_KEY!,
51
+ timeout: 10_000, // tighter timeout for Lambda
52
+ });
53
+
54
+ export const handler = async (event: any) => {
55
+ const results = await client.episodes.search({
56
+ query: event.queryStringParameters?.q ?? 'tech',
57
+ });
58
+
59
+ return {
60
+ statusCode: 200,
61
+ body: JSON.stringify(results.episodes),
62
+ };
63
+ };
64
+ ```
65
+
66
+ ## Configuration
67
+
68
+ ```typescript
69
+ const client = new PodscanClient({
70
+ apiKey: 'your-api-key', // Required. Bearer token for Podscan API.
71
+ baseUrl: 'https://...', // Optional. Override API base URL.
72
+ timeout: 30_000, // Optional. Request timeout in ms (default: 30000).
73
+ });
74
+ ```
75
+
76
+ ## API Reference
77
+
78
+ All methods return typed promises. Parameters mirror the [Podscan API docs](https://podscan.fm/docs/api).
79
+
80
+ ### `client.episodes`
81
+
82
+ | Method | Description |
83
+ |---|---|
84
+ | `search(params)` | Full-text search across episode transcripts, titles, and descriptions |
85
+ | `get(params)` | Get detailed info about a specific episode |
86
+ | `getRecent(params?)` | Get the most recently published episodes |
87
+ | `getByPodcast(params)` | List all episodes for a specific podcast |
88
+
89
+ ```typescript
90
+ // Search episodes with filters
91
+ const results = await client.episodes.search({
92
+ query: 'machine learning',
93
+ language: 'en',
94
+ has_guests: true,
95
+ since: '2026-01-01',
96
+ order_by: 'relevance',
97
+ per_page: 25,
98
+ });
99
+
100
+ // Get episode details with transcript
101
+ const episode = await client.episodes.get({
102
+ episode_id: 'ep_m9v2x7kq4pn8rjsw',
103
+ include_transcript: true,
104
+ include_entities: true,
105
+ });
106
+
107
+ // Get recent episodes
108
+ const recent = await client.episodes.getRecent({ limit: 10, language: 'en' });
109
+
110
+ // List episodes for a podcast
111
+ const podcastEpisodes = await client.episodes.getByPodcast({
112
+ podcast_id: 'pd_ka86x53ynan9wgdv',
113
+ order_by: 'posted_at',
114
+ per_page: 50,
115
+ });
116
+ ```
117
+
118
+ ### `client.podcasts`
119
+
120
+ | Method | Description |
121
+ |---|---|
122
+ | `search(params)` | Search podcasts by name, topic, or characteristics |
123
+ | `get(params)` | Get detailed info about a specific podcast |
124
+
125
+ ```typescript
126
+ // Search podcasts
127
+ const podcasts = await client.podcasts.search({
128
+ query: 'business',
129
+ has_guests: true,
130
+ min_episode_count: 50,
131
+ order_by: 'audience_size',
132
+ });
133
+
134
+ // Get podcast details
135
+ const podcast = await client.podcasts.get({
136
+ podcast_id: 'pd_ka86x53ynan9wgdv',
137
+ include_episodes: true,
138
+ episode_limit: 5,
139
+ });
140
+ ```
141
+
142
+ ### `client.alerts`
143
+
144
+ | Method | Description |
145
+ |---|---|
146
+ | `list(params?)` | List your team's content monitoring alerts |
147
+ | `getMentions(params)` | Get mentions found by a specific alert |
148
+ | `create(params)` | Create a new content monitoring alert |
149
+
150
+ ```typescript
151
+ // List alerts
152
+ const alerts = await client.alerts.list({ enabled_only: true });
153
+
154
+ // Get mentions for an alert
155
+ const mentions = await client.alerts.getMentions({
156
+ alert_id: 'al_h3f5g8k2m7n4p9q6',
157
+ since: '2026-02-01',
158
+ });
159
+
160
+ // Create an alert with filter expressions
161
+ const alert = await client.alerts.create({
162
+ name: 'Brand Monitor',
163
+ filters: '"Acme Corp"\nAcme AND (product OR service)',
164
+ webhook_url: 'https://example.com/webhook',
165
+ webhook_active: true,
166
+ });
167
+ ```
168
+
169
+ ### `client.topics`
170
+
171
+ | Method | Description |
172
+ |---|---|
173
+ | `search(params)` | Discover topics discussed across podcasts |
174
+ | `get(params)` | Get detailed info about a specific topic |
175
+ | `getEpisodes(params)` | Get episodes where a topic was mentioned |
176
+ | `getTrending(params?)` | Get currently trending topics |
177
+
178
+ ```typescript
179
+ // Search topics
180
+ const topics = await client.topics.search({
181
+ query: 'cryptocurrency',
182
+ min_episodes: 100,
183
+ });
184
+
185
+ // Get topic with history
186
+ const topic = await client.topics.get({
187
+ topic_id: 'tp_z8x6c4v2b0n9m7k5',
188
+ with_history: true,
189
+ });
190
+
191
+ // Get episodes for a topic
192
+ const topicEpisodes = await client.topics.getEpisodes({
193
+ topic_id: 'tp_z8x6c4v2b0n9m7k5',
194
+ podcast_audience_min: 10000,
195
+ per_page: 25,
196
+ });
197
+
198
+ // Get trending topics
199
+ const trending = await client.topics.getTrending({ period: '7d', limit: 20 });
200
+ ```
201
+
202
+ ### `client.entities`
203
+
204
+ | Method | Description |
205
+ |---|---|
206
+ | `search(params)` | Search for people and organizations mentioned in podcasts |
207
+ | `get(params)` | Get detailed info about a person or organization |
208
+ | `getAppearances(params)` | Get all podcast appearances for an entity |
209
+
210
+ ```typescript
211
+ // Search entities
212
+ const entities = await client.entities.search({
213
+ query: 'Elon',
214
+ entity_type: 'person',
215
+ min_appearances: 100,
216
+ });
217
+
218
+ // Get entity with recent appearances
219
+ const entity = await client.entities.get({
220
+ entity_id: 'en_p4o2i8u6y3t1r5e9',
221
+ with_appearances: true,
222
+ appearances_limit: 10,
223
+ });
224
+
225
+ // Get all appearances filtered by role
226
+ const appearances = await client.entities.getAppearances({
227
+ entity_id: 'en_p4o2i8u6y3t1r5e9',
228
+ role: 'guest',
229
+ order_dir: 'desc',
230
+ });
231
+ ```
232
+
233
+ ### `client.lists`
234
+
235
+ | Method | Description |
236
+ |---|---|
237
+ | `list(params?)` | Get all lists/collections for your team |
238
+ | `getItems(params)` | Get contents of a specific list |
239
+ | `addItems(params)` | Add items to a list (podcasts, episodes, entities, topics) |
240
+
241
+ ```typescript
242
+ // List all collections
243
+ const lists = await client.lists.list();
244
+
245
+ // Get list items filtered by type
246
+ const items = await client.lists.getItems({
247
+ list_id: 'cl_q9w3e5r7t1y4u8i2',
248
+ item_type: 'podcasts',
249
+ });
250
+
251
+ // Add items to a list
252
+ const result = await client.lists.addItems({
253
+ list_id: 'cl_q9w3e5r7t1y4u8i2',
254
+ item_ids: 'pd_ka86x53ynan9wgdv,ep_m9v2x7kq4pn8rjsw',
255
+ });
256
+ ```
257
+
258
+ ### `client.publishers`
259
+
260
+ | Method | Description |
261
+ |---|---|
262
+ | `get(params)` | Get publisher info with their podcast portfolio |
263
+
264
+ ```typescript
265
+ const publisher = await client.publishers.get({
266
+ publisher_id: 'pb_l7k5j3h1g9f6d4s2',
267
+ include_podcasts: true,
268
+ podcast_limit: 20,
269
+ });
270
+ ```
271
+
272
+ ## Time Periods
273
+
274
+ The `periods` helper computes date ranges you can spread into any search call:
275
+
276
+ ```typescript
277
+ import { PodscanClient, periods } from 'podscan';
278
+
279
+ const client = new PodscanClient({ apiKey: process.env.PODSCAN_API_KEY! });
280
+
281
+ // This week's AI episodes
282
+ const results = await client.episodes.search({
283
+ query: 'AI',
284
+ ...periods.thisWeek(),
285
+ per_page: 50,
286
+ });
287
+ ```
288
+
289
+ Available presets:
290
+
291
+ | Method | Range |
292
+ |---|---|
293
+ | `periods.today()` | Midnight UTC today to now |
294
+ | `periods.yesterday()` | Yesterday midnight to today midnight |
295
+ | `periods.last24Hours()` | Rolling 24-hour window |
296
+ | `periods.thisWeek()` | Monday 00:00 UTC to now |
297
+ | `periods.lastWeek()` | Previous Monday to this Monday |
298
+ | `periods.thisMonth()` | 1st of month to now |
299
+ | `periods.lastMonth()` | 1st of last month to 1st of this month |
300
+ | `periods.lastNDays(n)` | Rolling N-day window |
301
+ | `periods.lastNHours(n)` | Rolling N-hour window |
302
+ | `periods.since(date)` | Everything after a Date or ISO string |
303
+
304
+ All dates are UTC ISO 8601 strings. Each returns `{ since, before? }`.
305
+
306
+ ## Auto-Pagination
307
+
308
+ Every paginated resource has a `searchAll()` method that returns an async iterator, walking all pages automatically:
309
+
310
+ ```typescript
311
+ // Iterate every matching episode across all pages
312
+ for await (const episode of client.episodes.searchAll({
313
+ query: 'artificial intelligence',
314
+ ...periods.thisWeek(),
315
+ has_guests: true,
316
+ })) {
317
+ console.log(episode.episode_title);
318
+ console.log(' Guests:', episode.metadata?.guests.map(g => g.guest_name).join(', '));
319
+ }
320
+ ```
321
+
322
+ Available auto-paginating methods:
323
+
324
+ | Resource | Method | Yields |
325
+ |---|---|---|
326
+ | `client.episodes` | `searchAll(params)` | `Episode` |
327
+ | `client.episodes` | `getByPodcastAll(params)` | `Episode` |
328
+ | `client.podcasts` | `searchAll(params)` | `Podcast` |
329
+ | `client.topics` | `searchAll(params)` | `TopicSummary` |
330
+ | `client.topics` | `getEpisodesAll(params)` | `Episode` |
331
+ | `client.entities` | `searchAll(params)` | `Entity` |
332
+
333
+ ## Delta Sync (Checkpoints)
334
+
335
+ Track what you've already pulled so the next run only fetches new content:
336
+
337
+ ```typescript
338
+ import { PodscanClient, periods } from 'podscan';
339
+
340
+ const client = new PodscanClient({ apiKey: process.env.PODSCAN_API_KEY! });
341
+
342
+ // First run: pull this week's episodes
343
+ const paginator = client.episodes.searchAll({
344
+ query: 'AI',
345
+ ...periods.thisWeek(),
346
+ });
347
+
348
+ for await (const episode of paginator) {
349
+ await saveToDatabase(episode);
350
+ }
351
+
352
+ // Save checkpoint
353
+ const checkpoint = paginator.checkpoint();
354
+ // { lastSeenAt: '2026-02-16T14:30:00Z', lastSeenId: 'ep_abc123', totalSeen: 142 }
355
+ await saveCheckpoint(checkpoint);
356
+
357
+ // Next run: only get new episodes since last checkpoint
358
+ const lastCheckpoint = await loadCheckpoint();
359
+ const newEpisodes = client.episodes.searchAll({
360
+ query: 'AI',
361
+ since: lastCheckpoint.lastSeenAt,
362
+ });
363
+
364
+ for await (const episode of newEpisodes) {
365
+ await saveToDatabase(episode);
366
+ }
367
+ ```
368
+
369
+ ## Transcripts and Guest Data
370
+
371
+ Every episode includes full transcript and structured metadata with host/guest/speaker info:
372
+
373
+ ```typescript
374
+ const results = await client.episodes.search({
375
+ query: 'machine learning',
376
+ has_guests: true,
377
+ ...periods.thisWeek(),
378
+ per_page: 10,
379
+ });
380
+
381
+ for (const ep of results.episodes) {
382
+ // Full transcript with timestamps and speaker labels
383
+ console.log(ep.episode_transcript);
384
+ // "[00:00:08] [SPEAKER_01] Welcome to the show..."
385
+
386
+ // Structured metadata
387
+ const meta = ep.metadata;
388
+ if (meta) {
389
+ // Hosts
390
+ for (const host of meta.hosts) {
391
+ console.log(`Host: ${host.host_name} (${host.host_company})`);
392
+ }
393
+
394
+ // Guests with social links and occupation
395
+ for (const guest of meta.guests) {
396
+ console.log(`Guest: ${guest.guest_name}`);
397
+ console.log(` Occupation: ${guest.guest_occupation}`);
398
+ console.log(` Social: ${guest.guest_social_media_links?.join(', ')}`);
399
+ }
400
+
401
+ // Speaker label to name mapping
402
+ console.log('Speakers:', meta.speakers);
403
+ // { "SPEAKER_01": "John Smith", "SPEAKER_02": "Jane Doe" }
404
+
405
+ // AI-generated summaries
406
+ console.log('Summary:', meta.summary_short);
407
+ console.log('Keywords:', meta.summary_keywords);
408
+ }
409
+ }
410
+ ```
411
+
412
+ Transcript formatting options:
413
+
414
+ ```typescript
415
+ // Clean text without timestamps
416
+ const clean = await client.episodes.search({
417
+ query: 'AI',
418
+ remove_timestamps: true,
419
+ remove_speaker_labels: true,
420
+ });
421
+
422
+ // Paragraphs (merges segments)
423
+ const paragraphs = await client.episodes.search({
424
+ query: 'AI',
425
+ transcript_formatter: 'paragraphs',
426
+ });
427
+
428
+ // Exclude transcript entirely (saves bandwidth)
429
+ const noTranscript = await client.episodes.search({
430
+ query: 'AI',
431
+ exclude_transcript: true,
432
+ });
433
+ ```
434
+
435
+ ## Error Handling
436
+
437
+ All API errors throw a `PodscanError` with structured details:
438
+
439
+ ```typescript
440
+ import { PodscanClient, PodscanError } from 'podscan';
441
+
442
+ try {
443
+ const results = await client.episodes.search({ query: 'test' });
444
+ } catch (error) {
445
+ if (error instanceof PodscanError) {
446
+ console.error(error.code); // 'quota_exceeded', 'not_found', etc.
447
+ console.error(error.message); // Human-readable message
448
+ console.error(error.status); // HTTP status code (0 for network/timeout errors)
449
+ console.error(error.details); // Additional context from the API
450
+ }
451
+ }
452
+ ```
453
+
454
+ ### Error Codes
455
+
456
+ | Code | Description |
457
+ |---|---|
458
+ | `api_error` | Generic API error |
459
+ | `not_found` | Resource does not exist |
460
+ | `quota_exceeded` | Daily request limit reached |
461
+ | `access_denied` | Resource belongs to another team |
462
+ | `validation_error` | Invalid parameter value |
463
+ | `timeout` | Request timed out |
464
+ | `network_error` | Network connectivity issue |
465
+
466
+ ## Rate Limits
467
+
468
+ Rate-limit info is available after each request:
469
+
470
+ ```typescript
471
+ await client.episodes.search({ query: 'test' });
472
+
473
+ console.log(client.rateLimit);
474
+ // { limit: 2000, remaining: 1999, used: 1, resetsAt: '2026-02-17T00:00:00Z' }
475
+ ```
476
+
477
+ ## Requirements
478
+
479
+ - Node.js >= 18.0.0 (uses native `fetch`)
480
+ - A [Podscan](https://podscan.fm) account with API access
481
+
482
+ ## License
483
+
484
+ MIT