Skip to content

Commit bf5500a

Browse files
author
Transcript-Fetch-Admin
committed
feat: transcripts resource with auto-pagination
1 parent 62918cc commit bf5500a

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

‎src/resources/transcripts.ts‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* The `transcripts` resource: video / channel / playlist / search / batch,
3+
* plus auto-paginating async iterators.
4+
*/
5+
6+
import type { TranscriptFetch } from "../client";
7+
import {
8+
normalizeBatch,
9+
normalizeTranscript,
10+
normalizeVideoList,
11+
type BatchResponse,
12+
type Transcript,
13+
type Video,
14+
type VideoList,
15+
} from "../models";
16+
17+
const VIDEO = "/api/v1/transcripts/video";
18+
const CHANNEL = "/api/v1/transcripts/channel";
19+
const PLAYLIST = "/api/v1/transcripts/playlist";
20+
const SEARCH = "/api/v1/transcripts/search";
21+
const BATCH = "/api/v1/transcripts/batch";
22+
23+
/** Options for the paginated list endpoints (channel, playlist, search). */
24+
export interface ListOptions {
25+
/** Page size. */
26+
limit?: number;
27+
/** Opaque cursor from a previous page's `nextCursor`. */
28+
cursor?: string;
29+
/** Override the auto-generated Idempotency-Key. */
30+
idempotencyKey?: string;
31+
}
32+
33+
function listBody(key: string, value: string, options: ListOptions): Record<string, unknown> {
34+
const body: Record<string, unknown> = { [key]: value };
35+
if (options.limit != null) body["limit"] = options.limit;
36+
if (options.cursor != null) body["cursor"] = options.cursor;
37+
return body;
38+
}
39+
40+
export class Transcripts {
41+
constructor(private readonly client: TranscriptFetch) {}
42+
43+
/** Fetch a single video's transcript (text + timestamped segments). */
44+
async video(video: string, options: { idempotencyKey?: string } = {}): Promise<Transcript> {
45+
const env = await this.client.request("POST", VIDEO, {
46+
body: { video },
47+
idempotent: true,
48+
idempotencyKey: options.idempotencyKey,
49+
});
50+
return normalizeTranscript(env);
51+
}
52+
53+
/** List a channel's videos (metadata only), one page. */
54+
async channel(channel: string, options: ListOptions = {}): Promise<VideoList> {
55+
const env = await this.client.request("POST", CHANNEL, {
56+
body: listBody("channel", channel, options),
57+
idempotent: true,
58+
idempotencyKey: options.idempotencyKey,
59+
});
60+
return normalizeVideoList(env);
61+
}
62+
63+
/** List a playlist's videos (metadata only), one page. */
64+
async playlist(playlist: string, options: ListOptions = {}): Promise<VideoList> {
65+
const env = await this.client.request("POST", PLAYLIST, {
66+
body: listBody("playlist", playlist, options),
67+
idempotent: true,
68+
idempotencyKey: options.idempotencyKey,
69+
});
70+
return normalizeVideoList(env);
71+
}
72+
73+
/** Search YouTube and return matching videos (metadata only), one page. */
74+
async search(query: string, options: ListOptions = {}): Promise<VideoList> {
75+
const env = await this.client.request("POST", SEARCH, {
76+
body: listBody("query", query, options),
77+
idempotent: true,
78+
idempotencyKey: options.idempotencyKey,
79+
});
80+
return normalizeVideoList(env);
81+
}
82+
83+
/** Fetch up to 50 transcripts in one call. */
84+
async batch(videoIds: string[], options: { idempotencyKey?: string } = {}): Promise<BatchResponse> {
85+
const env = await this.client.request("POST", BATCH, {
86+
body: { videoIds },
87+
idempotent: true,
88+
idempotencyKey: options.idempotencyKey,
89+
});
90+
return normalizeBatch(env);
91+
}
92+
93+
// ── Auto-paginating iterators ───────────────────────────────────────────────
94+
95+
/** Iterate every video in a channel, transparently following cursors. */
96+
iterChannel(channel: string, options: { limit?: number } = {}): AsyncGenerator<Video> {
97+
return this.paginate((cursor) => this.channel(channel, { limit: options.limit, cursor }));
98+
}
99+
100+
/** Iterate every video in a playlist, transparently following cursors. */
101+
iterPlaylist(playlist: string, options: { limit?: number } = {}): AsyncGenerator<Video> {
102+
return this.paginate((cursor) => this.playlist(playlist, { limit: options.limit, cursor }));
103+
}
104+
105+
/** Iterate every search result, transparently following cursors. */
106+
iterSearch(query: string, options: { limit?: number } = {}): AsyncGenerator<Video> {
107+
return this.paginate((cursor) => this.search(query, { limit: options.limit, cursor }));
108+
}
109+
110+
private async *paginate(
111+
fetchPage: (cursor: string | undefined) => Promise<VideoList>,
112+
): AsyncGenerator<Video> {
113+
let cursor: string | undefined;
114+
for (;;) {
115+
const page = await fetchPage(cursor);
116+
for (const video of page.videos) yield video;
117+
if (!page.nextCursor) return;
118+
cursor = page.nextCursor;
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)