@vitkuz/youtube-adapter 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/README.md +44 -0
- package/dist/index.d.mts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +215 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +209 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @vitkuz/youtube-adapter
|
|
2
|
+
|
|
3
|
+
Functional YouTube Data API adapter for AWS/Node.js environments.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vitkuz/youtube-adapter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createAdapter } from '@vitkuz/youtube-adapter';
|
|
15
|
+
|
|
16
|
+
const adapter = createAdapter({
|
|
17
|
+
apiKey: process.env.YOUTUBE_API_KEY,
|
|
18
|
+
logger: console, // Optional logger
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Search
|
|
22
|
+
const results = await adapter.search({
|
|
23
|
+
q: 'Node.js tutorial',
|
|
24
|
+
maxResults: 5
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Get Video Details
|
|
28
|
+
if (results.items?.length) {
|
|
29
|
+
const videoId = results.items[0].id.videoId;
|
|
30
|
+
const details = await adapter.videoDetails({
|
|
31
|
+
id: [videoId]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Operations
|
|
37
|
+
|
|
38
|
+
- `search(input: SearchInput)`: Search for videos, channels, playlists.
|
|
39
|
+
- `videoDetails(input: VideoDetailsInput)`: Get detailed information about videos.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
Required environment variables:
|
|
44
|
+
- `YOUTUBE_API_KEY` (if using env vars, otherwise pass to constructor)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { youtube_v3 } from 'googleapis';
|
|
2
|
+
import { Adapter as Adapter$1 } from '@vitkuz/firecrawl-adapter';
|
|
3
|
+
|
|
4
|
+
type YoutubeClient = youtube_v3.Youtube;
|
|
5
|
+
declare const createClient: (apiKey: string) => YoutubeClient;
|
|
6
|
+
|
|
7
|
+
interface Logger {
|
|
8
|
+
debug: (message: string, context?: {
|
|
9
|
+
error?: any;
|
|
10
|
+
data?: any;
|
|
11
|
+
}) => void;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
interface Context {
|
|
15
|
+
client: YoutubeClient;
|
|
16
|
+
firecrawlAdapter?: Adapter$1;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SearchInput = youtube_v3.Params$Resource$Search$List;
|
|
21
|
+
type SearchOutput = youtube_v3.Schema$SearchListResponse;
|
|
22
|
+
declare const search: (context: Context) => (input: SearchInput) => Promise<SearchOutput>;
|
|
23
|
+
|
|
24
|
+
type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;
|
|
25
|
+
type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;
|
|
26
|
+
declare const videoDetails: (context: Context) => (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;
|
|
27
|
+
|
|
28
|
+
interface GetAllChannelVideosInput {
|
|
29
|
+
channelId: string;
|
|
30
|
+
}
|
|
31
|
+
interface GetAllChannelVideosOutput {
|
|
32
|
+
items: youtube_v3.Schema$Video[];
|
|
33
|
+
totalCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GetTranscriptInput {
|
|
37
|
+
videoId: string;
|
|
38
|
+
lang?: string;
|
|
39
|
+
}
|
|
40
|
+
interface TranscriptItem {
|
|
41
|
+
timestamp: string;
|
|
42
|
+
text: string;
|
|
43
|
+
}
|
|
44
|
+
type GetTranscriptOutput = TranscriptItem[];
|
|
45
|
+
declare const getTranscript: (context: Context) => (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
|
|
46
|
+
|
|
47
|
+
interface AdapterConfig {
|
|
48
|
+
apiKey: string;
|
|
49
|
+
firecrawlApiKey?: string;
|
|
50
|
+
logger?: Logger;
|
|
51
|
+
}
|
|
52
|
+
interface Adapter {
|
|
53
|
+
client: YoutubeClient;
|
|
54
|
+
search: (input: SearchInput) => Promise<SearchOutput>;
|
|
55
|
+
videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;
|
|
56
|
+
getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;
|
|
57
|
+
getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
|
|
58
|
+
}
|
|
59
|
+
declare const createAdapter: (config: AdapterConfig) => Adapter;
|
|
60
|
+
|
|
61
|
+
export { type Adapter, type AdapterConfig, type Context, type GetTranscriptInput, type GetTranscriptOutput, type Logger, type SearchInput, type SearchOutput, type TranscriptItem, type VideoDetailsInput, type VideoDetailsOutput, type YoutubeClient, createAdapter, createClient, getTranscript, search, videoDetails };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { youtube_v3 } from 'googleapis';
|
|
2
|
+
import { Adapter as Adapter$1 } from '@vitkuz/firecrawl-adapter';
|
|
3
|
+
|
|
4
|
+
type YoutubeClient = youtube_v3.Youtube;
|
|
5
|
+
declare const createClient: (apiKey: string) => YoutubeClient;
|
|
6
|
+
|
|
7
|
+
interface Logger {
|
|
8
|
+
debug: (message: string, context?: {
|
|
9
|
+
error?: any;
|
|
10
|
+
data?: any;
|
|
11
|
+
}) => void;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
interface Context {
|
|
15
|
+
client: YoutubeClient;
|
|
16
|
+
firecrawlAdapter?: Adapter$1;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SearchInput = youtube_v3.Params$Resource$Search$List;
|
|
21
|
+
type SearchOutput = youtube_v3.Schema$SearchListResponse;
|
|
22
|
+
declare const search: (context: Context) => (input: SearchInput) => Promise<SearchOutput>;
|
|
23
|
+
|
|
24
|
+
type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;
|
|
25
|
+
type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;
|
|
26
|
+
declare const videoDetails: (context: Context) => (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;
|
|
27
|
+
|
|
28
|
+
interface GetAllChannelVideosInput {
|
|
29
|
+
channelId: string;
|
|
30
|
+
}
|
|
31
|
+
interface GetAllChannelVideosOutput {
|
|
32
|
+
items: youtube_v3.Schema$Video[];
|
|
33
|
+
totalCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GetTranscriptInput {
|
|
37
|
+
videoId: string;
|
|
38
|
+
lang?: string;
|
|
39
|
+
}
|
|
40
|
+
interface TranscriptItem {
|
|
41
|
+
timestamp: string;
|
|
42
|
+
text: string;
|
|
43
|
+
}
|
|
44
|
+
type GetTranscriptOutput = TranscriptItem[];
|
|
45
|
+
declare const getTranscript: (context: Context) => (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
|
|
46
|
+
|
|
47
|
+
interface AdapterConfig {
|
|
48
|
+
apiKey: string;
|
|
49
|
+
firecrawlApiKey?: string;
|
|
50
|
+
logger?: Logger;
|
|
51
|
+
}
|
|
52
|
+
interface Adapter {
|
|
53
|
+
client: YoutubeClient;
|
|
54
|
+
search: (input: SearchInput) => Promise<SearchOutput>;
|
|
55
|
+
videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;
|
|
56
|
+
getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;
|
|
57
|
+
getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
|
|
58
|
+
}
|
|
59
|
+
declare const createAdapter: (config: AdapterConfig) => Adapter;
|
|
60
|
+
|
|
61
|
+
export { type Adapter, type AdapterConfig, type Context, type GetTranscriptInput, type GetTranscriptOutput, type Logger, type SearchInput, type SearchOutput, type TranscriptItem, type VideoDetailsInput, type VideoDetailsOutput, type YoutubeClient, createAdapter, createClient, getTranscript, search, videoDetails };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var googleapis = require('googleapis');
|
|
4
|
+
var firecrawlAdapter = require('@vitkuz/firecrawl-adapter');
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
var createClient = (apiKey) => {
|
|
8
|
+
return googleapis.google.youtube({
|
|
9
|
+
version: "v3",
|
|
10
|
+
auth: apiKey
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/operations/search.ts
|
|
15
|
+
var search = (context) => async (input) => {
|
|
16
|
+
const { client, logger } = context;
|
|
17
|
+
logger?.debug("search:start", { data: input });
|
|
18
|
+
try {
|
|
19
|
+
const response = await client.search.list({
|
|
20
|
+
part: ["snippet"],
|
|
21
|
+
...input
|
|
22
|
+
});
|
|
23
|
+
logger?.debug("search:success");
|
|
24
|
+
return response.data;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger?.debug("search:error", { error });
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/operations/video-details.ts
|
|
32
|
+
var videoDetails = (context) => async (input) => {
|
|
33
|
+
const { client, logger } = context;
|
|
34
|
+
logger?.debug("videoDetails:start", { data: input });
|
|
35
|
+
try {
|
|
36
|
+
const response = await client.videos.list({
|
|
37
|
+
part: ["snippet", "contentDetails", "statistics"],
|
|
38
|
+
...input
|
|
39
|
+
});
|
|
40
|
+
logger?.debug("videoDetails:success");
|
|
41
|
+
return response.data;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger?.debug("videoDetails:error", { error });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/operations/get-all-channel-videos.ts
|
|
49
|
+
var getAllChannelVideos = (context) => async (input) => {
|
|
50
|
+
const { client, logger } = context;
|
|
51
|
+
logger?.debug("getAllChannelVideos:start", { data: input });
|
|
52
|
+
try {
|
|
53
|
+
const channelResponse = await client.channels.list({
|
|
54
|
+
id: [input.channelId],
|
|
55
|
+
part: ["contentDetails"]
|
|
56
|
+
});
|
|
57
|
+
const uploadsPlaylistId = channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;
|
|
58
|
+
if (!uploadsPlaylistId) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Could not find uploads playlist for channel ID: ${input.channelId}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
logger?.debug("getAllChannelVideos:found_playlist", { data: { uploadsPlaylistId } });
|
|
64
|
+
let items = [];
|
|
65
|
+
let nextPageToken = void 0;
|
|
66
|
+
do {
|
|
67
|
+
const playlistResponse = await client.playlistItems.list({
|
|
68
|
+
playlistId: uploadsPlaylistId,
|
|
69
|
+
part: ["contentDetails"],
|
|
70
|
+
maxResults: 50,
|
|
71
|
+
pageToken: nextPageToken
|
|
72
|
+
});
|
|
73
|
+
const playlistItems = playlistResponse.data.items || [];
|
|
74
|
+
if (playlistItems.length > 0) {
|
|
75
|
+
const videoIds = playlistItems.map((item) => item.contentDetails?.videoId).filter((id) => !!id);
|
|
76
|
+
if (videoIds.length > 0) {
|
|
77
|
+
const videosResponse = await client.videos.list({
|
|
78
|
+
id: videoIds,
|
|
79
|
+
part: ["snippet", "contentDetails", "statistics"]
|
|
80
|
+
});
|
|
81
|
+
const videoItems = videosResponse.data.items || [];
|
|
82
|
+
items = items.concat(videoItems);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
nextPageToken = playlistResponse.data.nextPageToken || void 0;
|
|
86
|
+
logger?.debug("getAllChannelVideos:fetched_page", {
|
|
87
|
+
data: {
|
|
88
|
+
fetched: playlistItems.length,
|
|
89
|
+
totalVideosSoFar: items.length,
|
|
90
|
+
hasNextPage: !!nextPageToken
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
} while (nextPageToken);
|
|
94
|
+
logger?.debug("getAllChannelVideos:success", { data: { totalCount: items.length } });
|
|
95
|
+
return {
|
|
96
|
+
items,
|
|
97
|
+
totalCount: items.length
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger?.debug("getAllChannelVideos:error", { error });
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/operations/get-transcript.ts
|
|
106
|
+
var getTranscript = (context) => async (input) => {
|
|
107
|
+
const { logger, firecrawlAdapter } = context;
|
|
108
|
+
logger?.debug("getTranscript:start", { data: input });
|
|
109
|
+
if (!firecrawlAdapter) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"Firecrawl adapter is not initialized. Provide firecrawlApiKey in config."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const url = `https://www.youtube.com/watch?v=${input.videoId}`;
|
|
115
|
+
let markdown;
|
|
116
|
+
try {
|
|
117
|
+
const response = await firecrawlAdapter.scrape({
|
|
118
|
+
url,
|
|
119
|
+
params: {
|
|
120
|
+
formats: ["markdown"]
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
if (!response.success || !response.data || !response.data.markdown) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Failed to scrape YouTube page: ${response.error || "No data returned"}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
markdown = response.data.markdown;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger?.debug("getTranscript:error", { error });
|
|
131
|
+
throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);
|
|
132
|
+
}
|
|
133
|
+
const segments = [];
|
|
134
|
+
const transcriptStartValues = markdown.split("## Transcript");
|
|
135
|
+
if (transcriptStartValues.length < 2) {
|
|
136
|
+
if (markdown.length < 500) {
|
|
137
|
+
logger?.debug("getTranscript:markdown-too-short", { data: { markdown } });
|
|
138
|
+
} else {
|
|
139
|
+
logger?.debug("getTranscript:no-transcript-found", {
|
|
140
|
+
data: { preview: markdown.slice(0, 1e3) }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
throw new Error("Transcript section not found in scraped content");
|
|
144
|
+
}
|
|
145
|
+
const transcriptContent = transcriptStartValues[1];
|
|
146
|
+
const lines = transcriptContent.split("\n");
|
|
147
|
+
let currentTimestamp = "";
|
|
148
|
+
let currentText = [];
|
|
149
|
+
const timestampRegex = /^(\d{1,2}:)?\d{1,2}:\d{2}$/;
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (!trimmed) continue;
|
|
153
|
+
if (timestampRegex.test(trimmed)) {
|
|
154
|
+
if (currentTimestamp) {
|
|
155
|
+
segments.push({
|
|
156
|
+
timestamp: currentTimestamp,
|
|
157
|
+
text: currentText.join(" ").trim()
|
|
158
|
+
});
|
|
159
|
+
currentText = [];
|
|
160
|
+
}
|
|
161
|
+
currentTimestamp = trimmed;
|
|
162
|
+
} else {
|
|
163
|
+
if (trimmed.startsWith("##")) continue;
|
|
164
|
+
if (trimmed.startsWith("[) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (trimmed === "English" || trimmed === "German") {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (currentTimestamp) {
|
|
171
|
+
currentText.push(trimmed);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (currentTimestamp && currentText.length > 0) {
|
|
176
|
+
segments.push({
|
|
177
|
+
timestamp: currentTimestamp,
|
|
178
|
+
text: currentText.join(" ").trim()
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
logger?.debug("getTranscript:success", { data: { count: segments.length } });
|
|
182
|
+
return segments;
|
|
183
|
+
};
|
|
184
|
+
var createAdapter = (config) => {
|
|
185
|
+
const client = createClient(config.apiKey);
|
|
186
|
+
let firecrawlAdapter$1;
|
|
187
|
+
if (config.firecrawlApiKey) {
|
|
188
|
+
firecrawlAdapter$1 = firecrawlAdapter.createAdapter({
|
|
189
|
+
apiKey: config.firecrawlApiKey,
|
|
190
|
+
plan: firecrawlAdapter.FirecrawlPlan.FREE,
|
|
191
|
+
// Default to free, could be configurable
|
|
192
|
+
logger: config.logger
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const context = {
|
|
196
|
+
client,
|
|
197
|
+
firecrawlAdapter: firecrawlAdapter$1,
|
|
198
|
+
logger: config.logger
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
client,
|
|
202
|
+
search: search(context),
|
|
203
|
+
videoDetails: videoDetails(context),
|
|
204
|
+
getAllChannelVideos: getAllChannelVideos(context),
|
|
205
|
+
getTranscript: getTranscript(context)
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
exports.createAdapter = createAdapter;
|
|
210
|
+
exports.createClient = createClient;
|
|
211
|
+
exports.getTranscript = getTranscript;
|
|
212
|
+
exports.search = search;
|
|
213
|
+
exports.videoDetails = videoDetails;
|
|
214
|
+
//# sourceMappingURL=index.js.map
|
|
215
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/operations/search.ts","../src/operations/video-details.ts","../src/operations/get-all-channel-videos.ts","../src/operations/get-transcript.ts","../src/adapter.ts"],"names":["google","firecrawlAdapter","createFirecrawlAdapter","FirecrawlPlan"],"mappings":"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","file":"index.js","sourcesContent":["import { google, youtube_v3 } from 'googleapis';\n\nexport type YoutubeClient = youtube_v3.Youtube;\n\nexport const createClient = (apiKey: string): YoutubeClient => {\n return google.youtube({\n version: 'v3',\n auth: apiKey,\n });\n};\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type SearchInput = youtube_v3.Params$Resource$Search$List;\nexport type SearchOutput = youtube_v3.Schema$SearchListResponse;\n\nexport const search =\n (context: Context) =>\n async (input: SearchInput): Promise<SearchOutput> => {\n const { client, logger } = context;\n\n logger?.debug('search:start', { data: input });\n\n try {\n const response = await client.search.list({\n part: ['snippet'],\n ...input,\n });\n\n logger?.debug('search:success');\n return response.data;\n } catch (error) {\n logger?.debug('search:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;\nexport type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;\n\nexport const videoDetails =\n (context: Context) =>\n async (input: VideoDetailsInput): Promise<VideoDetailsOutput> => {\n const { client, logger } = context;\n\n logger?.debug('videoDetails:start', { data: input });\n\n try {\n const response = await client.videos.list({\n part: ['snippet', 'contentDetails', 'statistics'],\n ...input,\n });\n\n logger?.debug('videoDetails:success');\n return response.data;\n } catch (error) {\n logger?.debug('videoDetails:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport interface GetAllChannelVideosInput {\n channelId: string;\n}\n\nexport interface GetAllChannelVideosOutput {\n items: youtube_v3.Schema$Video[];\n totalCount: number;\n}\n\nexport const getAllChannelVideos =\n (context: Context) =>\n async (input: GetAllChannelVideosInput): Promise<GetAllChannelVideosOutput> => {\n const { client, logger } = context;\n\n logger?.debug('getAllChannelVideos:start', { data: input });\n\n try {\n // Step 1: Get the Uploads playlist ID\n const channelResponse = await client.channels.list({\n id: [input.channelId],\n part: ['contentDetails'],\n });\n\n const uploadsPlaylistId =\n channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;\n\n if (!uploadsPlaylistId) {\n throw new Error(\n `Could not find uploads playlist for channel ID: ${input.channelId}`,\n );\n }\n\n logger?.debug('getAllChannelVideos:found_playlist', { data: { uploadsPlaylistId } });\n\n // Step 2: Recursively fetch all playlist items and their video details\n let items: youtube_v3.Schema$Video[] = [];\n let nextPageToken: string | undefined = undefined;\n\n do {\n // Get playlist items (Video IDs)\n const playlistResponse: { data: youtube_v3.Schema$PlaylistItemListResponse } =\n (await client.playlistItems.list({\n playlistId: uploadsPlaylistId,\n part: ['contentDetails'],\n maxResults: 50,\n pageToken: nextPageToken,\n })) as any;\n\n const playlistItems = playlistResponse.data.items || [];\n\n if (playlistItems.length > 0) {\n const videoIds = playlistItems\n .map((item) => item.contentDetails?.videoId)\n .filter((id): id is string => !!id);\n\n if (videoIds.length > 0) {\n // Fetch full video details\n const videosResponse = await client.videos.list({\n id: videoIds,\n part: ['snippet', 'contentDetails', 'statistics'],\n });\n\n const videoItems = videosResponse.data.items || [];\n items = items.concat(videoItems);\n }\n }\n\n nextPageToken = playlistResponse.data.nextPageToken || undefined;\n\n logger?.debug('getAllChannelVideos:fetched_page', {\n data: {\n fetched: playlistItems.length,\n totalVideosSoFar: items.length,\n hasNextPage: !!nextPageToken,\n },\n });\n } while (nextPageToken);\n\n logger?.debug('getAllChannelVideos:success', { data: { totalCount: items.length } });\n\n return {\n items,\n totalCount: items.length,\n };\n } catch (error) {\n logger?.debug('getAllChannelVideos:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\n\nexport interface GetTranscriptInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface TranscriptItem {\n timestamp: string;\n text: string;\n}\n\nexport type GetTranscriptOutput = TranscriptItem[];\n\nexport const getTranscript =\n (context: Context) =>\n async (input: GetTranscriptInput): Promise<GetTranscriptOutput> => {\n const { logger, firecrawlAdapter } = context;\n logger?.debug('getTranscript:start', { data: input });\n\n if (!firecrawlAdapter) {\n throw new Error(\n 'Firecrawl adapter is not initialized. Provide firecrawlApiKey in config.',\n );\n }\n\n const url = `https://www.youtube.com/watch?v=${input.videoId}`;\n let markdown: string;\n\n try {\n // Use firecrawlAdapter.scrape directly\n const response = await firecrawlAdapter.scrape({\n url,\n params: {\n formats: ['markdown'],\n },\n });\n\n if (!response.success || !response.data || !response.data.markdown) {\n throw new Error(\n `Failed to scrape YouTube page: ${response.error || 'No data returned'}`,\n );\n }\n markdown = response.data.markdown;\n } catch (error: any) {\n logger?.debug('getTranscript:error', { error });\n throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);\n }\n\n // Parsing logic reused from firecrawl adapter implementation\n const segments: TranscriptItem[] = [];\n\n const transcriptStartValues = markdown.split('## Transcript');\n if (transcriptStartValues.length < 2) {\n if (markdown.length < 500) {\n logger?.debug('getTranscript:markdown-too-short', { data: { markdown } });\n } else {\n logger?.debug('getTranscript:no-transcript-found', {\n data: { preview: markdown.slice(0, 1000) },\n });\n }\n throw new Error('Transcript section not found in scraped content');\n }\n\n const transcriptContent = transcriptStartValues[1];\n\n const lines = transcriptContent.split('\\n');\n let currentTimestamp = '';\n let currentText: string[] = [];\n\n const timestampRegex = /^(\\d{1,2}:)?\\d{1,2}:\\d{2}$/; // Matches 0:01, 10:05, 1:00:00\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check if it's a timestamp\n if (timestampRegex.test(trimmed)) {\n // If we have a previous segment accumulating, push it\n if (currentTimestamp) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n currentText = [];\n }\n currentTimestamp = trimmed;\n } else {\n // It's text or a header?\n if (trimmed.startsWith('##')) continue;\n\n // Stop if we hit video thumbnails (footer)\n if (trimmed.startsWith('[) {\n break;\n }\n\n // Stop if we hit language options (English/German) which usually signify end of transcript\n if (trimmed === 'English' || trimmed === 'German') {\n continue;\n }\n\n if (currentTimestamp) {\n currentText.push(trimmed);\n }\n }\n }\n\n // Push last segment\n if (currentTimestamp && currentText.length > 0) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n }\n\n logger?.debug('getTranscript:success', { data: { count: segments.length } });\n return segments;\n };\n","import { createClient, YoutubeClient } from './client';\nimport { Context, Logger } from './types';\nimport { search, SearchInput, SearchOutput } from './operations/search';\nimport { videoDetails, VideoDetailsInput, VideoDetailsOutput } from './operations/video-details';\nimport {\n getAllChannelVideos,\n GetAllChannelVideosInput,\n GetAllChannelVideosOutput,\n} from './operations/get-all-channel-videos';\nimport {\n getTranscript,\n GetTranscriptInput,\n GetTranscriptOutput,\n} from './operations/get-transcript';\n\nexport interface AdapterConfig {\n apiKey: string;\n firecrawlApiKey?: string;\n logger?: Logger;\n}\n\nexport interface Adapter {\n client: YoutubeClient;\n search: (input: SearchInput) => Promise<SearchOutput>;\n videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;\n getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;\n getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;\n}\n\nimport { createAdapter as createFirecrawlAdapter, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';\n\nexport const createAdapter = (config: AdapterConfig): Adapter => {\n const client = createClient(config.apiKey);\n\n let firecrawlAdapter;\n if (config.firecrawlApiKey) {\n firecrawlAdapter = createFirecrawlAdapter({\n apiKey: config.firecrawlApiKey,\n plan: FirecrawlPlan.FREE, // Default to free, could be configurable\n logger: config.logger,\n });\n }\n\n const context: Context = {\n client,\n firecrawlAdapter,\n logger: config.logger,\n };\n\n return {\n client,\n search: search(context),\n videoDetails: videoDetails(context),\n getAllChannelVideos: getAllChannelVideos(context),\n getTranscript: getTranscript(context),\n };\n};\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { createAdapter as createAdapter$1, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';
|
|
3
|
+
|
|
4
|
+
// src/client.ts
|
|
5
|
+
var createClient = (apiKey) => {
|
|
6
|
+
return google.youtube({
|
|
7
|
+
version: "v3",
|
|
8
|
+
auth: apiKey
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/operations/search.ts
|
|
13
|
+
var search = (context) => async (input) => {
|
|
14
|
+
const { client, logger } = context;
|
|
15
|
+
logger?.debug("search:start", { data: input });
|
|
16
|
+
try {
|
|
17
|
+
const response = await client.search.list({
|
|
18
|
+
part: ["snippet"],
|
|
19
|
+
...input
|
|
20
|
+
});
|
|
21
|
+
logger?.debug("search:success");
|
|
22
|
+
return response.data;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logger?.debug("search:error", { error });
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/operations/video-details.ts
|
|
30
|
+
var videoDetails = (context) => async (input) => {
|
|
31
|
+
const { client, logger } = context;
|
|
32
|
+
logger?.debug("videoDetails:start", { data: input });
|
|
33
|
+
try {
|
|
34
|
+
const response = await client.videos.list({
|
|
35
|
+
part: ["snippet", "contentDetails", "statistics"],
|
|
36
|
+
...input
|
|
37
|
+
});
|
|
38
|
+
logger?.debug("videoDetails:success");
|
|
39
|
+
return response.data;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger?.debug("videoDetails:error", { error });
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/operations/get-all-channel-videos.ts
|
|
47
|
+
var getAllChannelVideos = (context) => async (input) => {
|
|
48
|
+
const { client, logger } = context;
|
|
49
|
+
logger?.debug("getAllChannelVideos:start", { data: input });
|
|
50
|
+
try {
|
|
51
|
+
const channelResponse = await client.channels.list({
|
|
52
|
+
id: [input.channelId],
|
|
53
|
+
part: ["contentDetails"]
|
|
54
|
+
});
|
|
55
|
+
const uploadsPlaylistId = channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;
|
|
56
|
+
if (!uploadsPlaylistId) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Could not find uploads playlist for channel ID: ${input.channelId}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
logger?.debug("getAllChannelVideos:found_playlist", { data: { uploadsPlaylistId } });
|
|
62
|
+
let items = [];
|
|
63
|
+
let nextPageToken = void 0;
|
|
64
|
+
do {
|
|
65
|
+
const playlistResponse = await client.playlistItems.list({
|
|
66
|
+
playlistId: uploadsPlaylistId,
|
|
67
|
+
part: ["contentDetails"],
|
|
68
|
+
maxResults: 50,
|
|
69
|
+
pageToken: nextPageToken
|
|
70
|
+
});
|
|
71
|
+
const playlistItems = playlistResponse.data.items || [];
|
|
72
|
+
if (playlistItems.length > 0) {
|
|
73
|
+
const videoIds = playlistItems.map((item) => item.contentDetails?.videoId).filter((id) => !!id);
|
|
74
|
+
if (videoIds.length > 0) {
|
|
75
|
+
const videosResponse = await client.videos.list({
|
|
76
|
+
id: videoIds,
|
|
77
|
+
part: ["snippet", "contentDetails", "statistics"]
|
|
78
|
+
});
|
|
79
|
+
const videoItems = videosResponse.data.items || [];
|
|
80
|
+
items = items.concat(videoItems);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
nextPageToken = playlistResponse.data.nextPageToken || void 0;
|
|
84
|
+
logger?.debug("getAllChannelVideos:fetched_page", {
|
|
85
|
+
data: {
|
|
86
|
+
fetched: playlistItems.length,
|
|
87
|
+
totalVideosSoFar: items.length,
|
|
88
|
+
hasNextPage: !!nextPageToken
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} while (nextPageToken);
|
|
92
|
+
logger?.debug("getAllChannelVideos:success", { data: { totalCount: items.length } });
|
|
93
|
+
return {
|
|
94
|
+
items,
|
|
95
|
+
totalCount: items.length
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logger?.debug("getAllChannelVideos:error", { error });
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/operations/get-transcript.ts
|
|
104
|
+
var getTranscript = (context) => async (input) => {
|
|
105
|
+
const { logger, firecrawlAdapter } = context;
|
|
106
|
+
logger?.debug("getTranscript:start", { data: input });
|
|
107
|
+
if (!firecrawlAdapter) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Firecrawl adapter is not initialized. Provide firecrawlApiKey in config."
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const url = `https://www.youtube.com/watch?v=${input.videoId}`;
|
|
113
|
+
let markdown;
|
|
114
|
+
try {
|
|
115
|
+
const response = await firecrawlAdapter.scrape({
|
|
116
|
+
url,
|
|
117
|
+
params: {
|
|
118
|
+
formats: ["markdown"]
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (!response.success || !response.data || !response.data.markdown) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Failed to scrape YouTube page: ${response.error || "No data returned"}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
markdown = response.data.markdown;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger?.debug("getTranscript:error", { error });
|
|
129
|
+
throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
const segments = [];
|
|
132
|
+
const transcriptStartValues = markdown.split("## Transcript");
|
|
133
|
+
if (transcriptStartValues.length < 2) {
|
|
134
|
+
if (markdown.length < 500) {
|
|
135
|
+
logger?.debug("getTranscript:markdown-too-short", { data: { markdown } });
|
|
136
|
+
} else {
|
|
137
|
+
logger?.debug("getTranscript:no-transcript-found", {
|
|
138
|
+
data: { preview: markdown.slice(0, 1e3) }
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
throw new Error("Transcript section not found in scraped content");
|
|
142
|
+
}
|
|
143
|
+
const transcriptContent = transcriptStartValues[1];
|
|
144
|
+
const lines = transcriptContent.split("\n");
|
|
145
|
+
let currentTimestamp = "";
|
|
146
|
+
let currentText = [];
|
|
147
|
+
const timestampRegex = /^(\d{1,2}:)?\d{1,2}:\d{2}$/;
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
const trimmed = line.trim();
|
|
150
|
+
if (!trimmed) continue;
|
|
151
|
+
if (timestampRegex.test(trimmed)) {
|
|
152
|
+
if (currentTimestamp) {
|
|
153
|
+
segments.push({
|
|
154
|
+
timestamp: currentTimestamp,
|
|
155
|
+
text: currentText.join(" ").trim()
|
|
156
|
+
});
|
|
157
|
+
currentText = [];
|
|
158
|
+
}
|
|
159
|
+
currentTimestamp = trimmed;
|
|
160
|
+
} else {
|
|
161
|
+
if (trimmed.startsWith("##")) continue;
|
|
162
|
+
if (trimmed.startsWith("[) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (trimmed === "English" || trimmed === "German") {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (currentTimestamp) {
|
|
169
|
+
currentText.push(trimmed);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (currentTimestamp && currentText.length > 0) {
|
|
174
|
+
segments.push({
|
|
175
|
+
timestamp: currentTimestamp,
|
|
176
|
+
text: currentText.join(" ").trim()
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
logger?.debug("getTranscript:success", { data: { count: segments.length } });
|
|
180
|
+
return segments;
|
|
181
|
+
};
|
|
182
|
+
var createAdapter = (config) => {
|
|
183
|
+
const client = createClient(config.apiKey);
|
|
184
|
+
let firecrawlAdapter;
|
|
185
|
+
if (config.firecrawlApiKey) {
|
|
186
|
+
firecrawlAdapter = createAdapter$1({
|
|
187
|
+
apiKey: config.firecrawlApiKey,
|
|
188
|
+
plan: FirecrawlPlan.FREE,
|
|
189
|
+
// Default to free, could be configurable
|
|
190
|
+
logger: config.logger
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const context = {
|
|
194
|
+
client,
|
|
195
|
+
firecrawlAdapter,
|
|
196
|
+
logger: config.logger
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
client,
|
|
200
|
+
search: search(context),
|
|
201
|
+
videoDetails: videoDetails(context),
|
|
202
|
+
getAllChannelVideos: getAllChannelVideos(context),
|
|
203
|
+
getTranscript: getTranscript(context)
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export { createAdapter, createClient, getTranscript, search, videoDetails };
|
|
208
|
+
//# sourceMappingURL=index.mjs.map
|
|
209
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/operations/search.ts","../src/operations/video-details.ts","../src/operations/get-all-channel-videos.ts","../src/operations/get-transcript.ts","../src/adapter.ts"],"names":["createFirecrawlAdapter"],"mappings":"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","file":"index.mjs","sourcesContent":["import { google, youtube_v3 } from 'googleapis';\n\nexport type YoutubeClient = youtube_v3.Youtube;\n\nexport const createClient = (apiKey: string): YoutubeClient => {\n return google.youtube({\n version: 'v3',\n auth: apiKey,\n });\n};\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type SearchInput = youtube_v3.Params$Resource$Search$List;\nexport type SearchOutput = youtube_v3.Schema$SearchListResponse;\n\nexport const search =\n (context: Context) =>\n async (input: SearchInput): Promise<SearchOutput> => {\n const { client, logger } = context;\n\n logger?.debug('search:start', { data: input });\n\n try {\n const response = await client.search.list({\n part: ['snippet'],\n ...input,\n });\n\n logger?.debug('search:success');\n return response.data;\n } catch (error) {\n logger?.debug('search:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;\nexport type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;\n\nexport const videoDetails =\n (context: Context) =>\n async (input: VideoDetailsInput): Promise<VideoDetailsOutput> => {\n const { client, logger } = context;\n\n logger?.debug('videoDetails:start', { data: input });\n\n try {\n const response = await client.videos.list({\n part: ['snippet', 'contentDetails', 'statistics'],\n ...input,\n });\n\n logger?.debug('videoDetails:success');\n return response.data;\n } catch (error) {\n logger?.debug('videoDetails:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport interface GetAllChannelVideosInput {\n channelId: string;\n}\n\nexport interface GetAllChannelVideosOutput {\n items: youtube_v3.Schema$Video[];\n totalCount: number;\n}\n\nexport const getAllChannelVideos =\n (context: Context) =>\n async (input: GetAllChannelVideosInput): Promise<GetAllChannelVideosOutput> => {\n const { client, logger } = context;\n\n logger?.debug('getAllChannelVideos:start', { data: input });\n\n try {\n // Step 1: Get the Uploads playlist ID\n const channelResponse = await client.channels.list({\n id: [input.channelId],\n part: ['contentDetails'],\n });\n\n const uploadsPlaylistId =\n channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;\n\n if (!uploadsPlaylistId) {\n throw new Error(\n `Could not find uploads playlist for channel ID: ${input.channelId}`,\n );\n }\n\n logger?.debug('getAllChannelVideos:found_playlist', { data: { uploadsPlaylistId } });\n\n // Step 2: Recursively fetch all playlist items and their video details\n let items: youtube_v3.Schema$Video[] = [];\n let nextPageToken: string | undefined = undefined;\n\n do {\n // Get playlist items (Video IDs)\n const playlistResponse: { data: youtube_v3.Schema$PlaylistItemListResponse } =\n (await client.playlistItems.list({\n playlistId: uploadsPlaylistId,\n part: ['contentDetails'],\n maxResults: 50,\n pageToken: nextPageToken,\n })) as any;\n\n const playlistItems = playlistResponse.data.items || [];\n\n if (playlistItems.length > 0) {\n const videoIds = playlistItems\n .map((item) => item.contentDetails?.videoId)\n .filter((id): id is string => !!id);\n\n if (videoIds.length > 0) {\n // Fetch full video details\n const videosResponse = await client.videos.list({\n id: videoIds,\n part: ['snippet', 'contentDetails', 'statistics'],\n });\n\n const videoItems = videosResponse.data.items || [];\n items = items.concat(videoItems);\n }\n }\n\n nextPageToken = playlistResponse.data.nextPageToken || undefined;\n\n logger?.debug('getAllChannelVideos:fetched_page', {\n data: {\n fetched: playlistItems.length,\n totalVideosSoFar: items.length,\n hasNextPage: !!nextPageToken,\n },\n });\n } while (nextPageToken);\n\n logger?.debug('getAllChannelVideos:success', { data: { totalCount: items.length } });\n\n return {\n items,\n totalCount: items.length,\n };\n } catch (error) {\n logger?.debug('getAllChannelVideos:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\n\nexport interface GetTranscriptInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface TranscriptItem {\n timestamp: string;\n text: string;\n}\n\nexport type GetTranscriptOutput = TranscriptItem[];\n\nexport const getTranscript =\n (context: Context) =>\n async (input: GetTranscriptInput): Promise<GetTranscriptOutput> => {\n const { logger, firecrawlAdapter } = context;\n logger?.debug('getTranscript:start', { data: input });\n\n if (!firecrawlAdapter) {\n throw new Error(\n 'Firecrawl adapter is not initialized. Provide firecrawlApiKey in config.',\n );\n }\n\n const url = `https://www.youtube.com/watch?v=${input.videoId}`;\n let markdown: string;\n\n try {\n // Use firecrawlAdapter.scrape directly\n const response = await firecrawlAdapter.scrape({\n url,\n params: {\n formats: ['markdown'],\n },\n });\n\n if (!response.success || !response.data || !response.data.markdown) {\n throw new Error(\n `Failed to scrape YouTube page: ${response.error || 'No data returned'}`,\n );\n }\n markdown = response.data.markdown;\n } catch (error: any) {\n logger?.debug('getTranscript:error', { error });\n throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);\n }\n\n // Parsing logic reused from firecrawl adapter implementation\n const segments: TranscriptItem[] = [];\n\n const transcriptStartValues = markdown.split('## Transcript');\n if (transcriptStartValues.length < 2) {\n if (markdown.length < 500) {\n logger?.debug('getTranscript:markdown-too-short', { data: { markdown } });\n } else {\n logger?.debug('getTranscript:no-transcript-found', {\n data: { preview: markdown.slice(0, 1000) },\n });\n }\n throw new Error('Transcript section not found in scraped content');\n }\n\n const transcriptContent = transcriptStartValues[1];\n\n const lines = transcriptContent.split('\\n');\n let currentTimestamp = '';\n let currentText: string[] = [];\n\n const timestampRegex = /^(\\d{1,2}:)?\\d{1,2}:\\d{2}$/; // Matches 0:01, 10:05, 1:00:00\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check if it's a timestamp\n if (timestampRegex.test(trimmed)) {\n // If we have a previous segment accumulating, push it\n if (currentTimestamp) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n currentText = [];\n }\n currentTimestamp = trimmed;\n } else {\n // It's text or a header?\n if (trimmed.startsWith('##')) continue;\n\n // Stop if we hit video thumbnails (footer)\n if (trimmed.startsWith('[) {\n break;\n }\n\n // Stop if we hit language options (English/German) which usually signify end of transcript\n if (trimmed === 'English' || trimmed === 'German') {\n continue;\n }\n\n if (currentTimestamp) {\n currentText.push(trimmed);\n }\n }\n }\n\n // Push last segment\n if (currentTimestamp && currentText.length > 0) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n }\n\n logger?.debug('getTranscript:success', { data: { count: segments.length } });\n return segments;\n };\n","import { createClient, YoutubeClient } from './client';\nimport { Context, Logger } from './types';\nimport { search, SearchInput, SearchOutput } from './operations/search';\nimport { videoDetails, VideoDetailsInput, VideoDetailsOutput } from './operations/video-details';\nimport {\n getAllChannelVideos,\n GetAllChannelVideosInput,\n GetAllChannelVideosOutput,\n} from './operations/get-all-channel-videos';\nimport {\n getTranscript,\n GetTranscriptInput,\n GetTranscriptOutput,\n} from './operations/get-transcript';\n\nexport interface AdapterConfig {\n apiKey: string;\n firecrawlApiKey?: string;\n logger?: Logger;\n}\n\nexport interface Adapter {\n client: YoutubeClient;\n search: (input: SearchInput) => Promise<SearchOutput>;\n videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;\n getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;\n getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;\n}\n\nimport { createAdapter as createFirecrawlAdapter, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';\n\nexport const createAdapter = (config: AdapterConfig): Adapter => {\n const client = createClient(config.apiKey);\n\n let firecrawlAdapter;\n if (config.firecrawlApiKey) {\n firecrawlAdapter = createFirecrawlAdapter({\n apiKey: config.firecrawlApiKey,\n plan: FirecrawlPlan.FREE, // Default to free, could be configurable\n logger: config.logger,\n });\n }\n\n const context: Context = {\n client,\n firecrawlAdapter,\n logger: config.logger,\n };\n\n return {\n client,\n search: search(context),\n videoDetails: videoDetails(context),\n getAllChannelVideos: getAllChannelVideos(context),\n getTranscript: getTranscript(context),\n };\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitkuz/youtube-adapter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Functional YouTube adapter",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
22
|
+
"prepublishOnly": "npm run format && npm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@vitkuz/firecrawl-adapter": "file:../vitkuz-firecrawl-adapter",
|
|
26
|
+
"dotenv": "^16.4.5",
|
|
27
|
+
"googleapis": "^144.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"prettier": "^3.7.4",
|
|
32
|
+
"rimraf": "^6.0.0",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|