headless-youtube-captions 1.2.0 → 1.3.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/CHANGELOG.md +18 -0
- package/README.md +64 -0
- package/package.json +5 -2
- package/src/index.d.ts +56 -1
- package/src/index.js +2 -1
- package/src/search.js +171 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2025-01-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **NEW**: `searchYouTubeGlobal()` function for searching across all of YouTube
|
|
7
|
+
- Search for videos and channels globally using any search term
|
|
8
|
+
- Configurable result types: videos, channels, or both
|
|
9
|
+
- Adjustable result limits (1-20 results)
|
|
10
|
+
- Returns structured data with video metadata (title, channel, views, duration, etc.)
|
|
11
|
+
- Returns structured data with channel metadata (title, subscribers, video count, etc.)
|
|
12
|
+
- Uses validated DOM selectors for reliable extraction
|
|
13
|
+
- Consistent API design matching existing functions
|
|
14
|
+
|
|
15
|
+
### Technical Details
|
|
16
|
+
- Added `src/search.js` with comprehensive YouTube search automation
|
|
17
|
+
- Updated TypeScript definitions with new search interfaces
|
|
18
|
+
- Enhanced exports in main index file
|
|
19
|
+
- Maintains existing browser management and error handling patterns
|
|
20
|
+
|
|
3
21
|
## [1.2.0] - 2025-07-06
|
|
4
22
|
|
|
5
23
|
### Added
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- 🎯 Extract video transcripts/captions in multiple languages
|
|
12
12
|
- 📺 Get channel videos with pagination support
|
|
13
13
|
- 🔍 Search videos within a specific channel
|
|
14
|
+
- 🌍 **NEW**: Search across all of YouTube globally
|
|
14
15
|
- 💬 Extract video comments with sorting options
|
|
15
16
|
- 🐳 Docker support with configurable Chrome executable path
|
|
16
17
|
- 📦 Zero build dependencies - runs directly from source
|
|
@@ -64,6 +65,19 @@ const result = await searchChannelVideos({
|
|
|
64
65
|
console.log(result.results);
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
### Search YouTube Globally
|
|
69
|
+
```js
|
|
70
|
+
import { searchYouTubeGlobal } from 'headless-youtube-captions';
|
|
71
|
+
|
|
72
|
+
const result = await searchYouTubeGlobal({
|
|
73
|
+
query: 'javascript tutorial',
|
|
74
|
+
maxResults: 10, // Optional, 1-20, default: 10
|
|
75
|
+
resultTypes: ['videos'] // Optional, ['videos', 'channels', 'all'], default: ['all']
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(result.results);
|
|
79
|
+
```
|
|
80
|
+
|
|
67
81
|
### Get Video Comments
|
|
68
82
|
```js
|
|
69
83
|
import { getVideoComments } from 'headless-youtube-captions';
|
|
@@ -332,6 +346,56 @@ Extract comments from a YouTube video with pagination support.
|
|
|
332
346
|
}
|
|
333
347
|
```
|
|
334
348
|
|
|
349
|
+
### `searchYouTubeGlobal(options)`
|
|
350
|
+
|
|
351
|
+
Search across all of YouTube for videos and channels.
|
|
352
|
+
|
|
353
|
+
#### Parameters
|
|
354
|
+
|
|
355
|
+
- `options` (Object):
|
|
356
|
+
- `query` (String, required): Search term to find content
|
|
357
|
+
- `maxResults` (Number, optional): Maximum results to return (1-20). Default: `10`
|
|
358
|
+
- `resultTypes` (Array, optional): Types of results to include. Options: `['videos']`, `['channels']`, `['all']`. Default: `['all']`
|
|
359
|
+
|
|
360
|
+
#### Returns
|
|
361
|
+
|
|
362
|
+
```js
|
|
363
|
+
{
|
|
364
|
+
query: "javascript tutorial",
|
|
365
|
+
resultTypes: ["videos"],
|
|
366
|
+
maxResults: 10,
|
|
367
|
+
totalFound: 8,
|
|
368
|
+
results: [
|
|
369
|
+
{
|
|
370
|
+
id: "videoId123",
|
|
371
|
+
type: "video",
|
|
372
|
+
title: "JavaScript Tutorial for Beginners",
|
|
373
|
+
url: "https://youtube.com/watch?v=videoId123",
|
|
374
|
+
channel: "Code Academy",
|
|
375
|
+
views: "2.1M views",
|
|
376
|
+
uploadTime: "1 year ago",
|
|
377
|
+
duration: "1:23:45",
|
|
378
|
+
thumbnail: "https://i.ytimg.com/vi/..."
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "channelId456",
|
|
382
|
+
type: "channel",
|
|
383
|
+
title: "JavaScript Mastery",
|
|
384
|
+
url: "https://youtube.com/@javascriptmastery",
|
|
385
|
+
subscribers: "1.2M subscribers",
|
|
386
|
+
videoCount: "200 videos",
|
|
387
|
+
thumbnail: "https://yt3.ggpht.com/..."
|
|
388
|
+
}
|
|
389
|
+
// ... more results
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Result Types
|
|
395
|
+
|
|
396
|
+
- **Video Results** include: `id`, `type`, `title`, `url`, `channel`, `views`, `uploadTime`, `duration`, `thumbnail`
|
|
397
|
+
- **Channel Results** include: `id`, `type`, `title`, `url`, `subscribers`, `videoCount`, `thumbnail`
|
|
398
|
+
|
|
335
399
|
## License
|
|
336
400
|
|
|
337
401
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "headless-youtube-captions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Extract YouTube video transcripts, channel videos, and comments using headless browser automation",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"youtube",
|
|
35
|
+
"search",
|
|
35
36
|
"captions",
|
|
36
37
|
"subtitles",
|
|
37
38
|
"transcript",
|
|
@@ -41,6 +42,8 @@
|
|
|
41
42
|
"puppeteer",
|
|
42
43
|
"headless",
|
|
43
44
|
"scraper",
|
|
44
|
-
"api"
|
|
45
|
+
"api",
|
|
46
|
+
"automation",
|
|
47
|
+
"global-search"
|
|
45
48
|
]
|
|
46
49
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -163,4 +163,59 @@ export interface VideoCommentsResult {
|
|
|
163
163
|
* @param options - Configuration options
|
|
164
164
|
* @returns Promise that resolves to video comments result
|
|
165
165
|
*/
|
|
166
|
-
export function getVideoComments(options: GetVideoCommentsOptions): Promise<VideoCommentsResult>;
|
|
166
|
+
export function getVideoComments(options: GetVideoCommentsOptions): Promise<VideoCommentsResult>;
|
|
167
|
+
|
|
168
|
+
// Types for global YouTube search
|
|
169
|
+
export interface SearchResult {
|
|
170
|
+
/** Result ID (video ID or channel ID) */
|
|
171
|
+
id: string;
|
|
172
|
+
/** Result type */
|
|
173
|
+
type: 'video' | 'channel';
|
|
174
|
+
/** Title of the video or channel */
|
|
175
|
+
title: string;
|
|
176
|
+
/** Full YouTube URL */
|
|
177
|
+
url: string;
|
|
178
|
+
/** Thumbnail URL */
|
|
179
|
+
thumbnail: string;
|
|
180
|
+
/** Channel name (for videos) */
|
|
181
|
+
channel?: string;
|
|
182
|
+
/** View count (for videos) */
|
|
183
|
+
views?: string;
|
|
184
|
+
/** Upload time (for videos) */
|
|
185
|
+
uploadTime?: string;
|
|
186
|
+
/** Duration (for videos) */
|
|
187
|
+
duration?: string;
|
|
188
|
+
/** Subscriber count (for channels) */
|
|
189
|
+
subscribers?: string;
|
|
190
|
+
/** Video count (for channels) */
|
|
191
|
+
videoCount?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface SearchYouTubeGlobalOptions {
|
|
195
|
+
/** Search query */
|
|
196
|
+
query: string;
|
|
197
|
+
/** Maximum number of results to return (1-20, default: 10) */
|
|
198
|
+
maxResults?: number;
|
|
199
|
+
/** Types of results to include (default: ['all']) */
|
|
200
|
+
resultTypes?: ('videos' | 'channels' | 'all')[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface SearchYouTubeGlobalResult {
|
|
204
|
+
/** Search query used */
|
|
205
|
+
query: string;
|
|
206
|
+
/** Result types requested */
|
|
207
|
+
resultTypes: ('videos' | 'channels' | 'all')[];
|
|
208
|
+
/** Maximum results requested */
|
|
209
|
+
maxResults: number;
|
|
210
|
+
/** Total results found */
|
|
211
|
+
totalFound: number;
|
|
212
|
+
/** Array of search results */
|
|
213
|
+
results: SearchResult[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Search across all of YouTube for videos and channels
|
|
218
|
+
* @param options - Configuration options
|
|
219
|
+
* @returns Promise that resolves to global search results
|
|
220
|
+
*/
|
|
221
|
+
export function searchYouTubeGlobal(options: SearchYouTubeGlobalOptions): Promise<SearchYouTubeGlobalResult>;
|
package/src/index.js
CHANGED
|
@@ -258,4 +258,5 @@ export async function getSubtitles({ videoID, lang = 'en' }) {
|
|
|
258
258
|
|
|
259
259
|
// Export new functions
|
|
260
260
|
export { getChannelVideos, searchChannelVideos } from './channel.js';
|
|
261
|
-
export { getVideoComments } from './comments.js';
|
|
261
|
+
export { getVideoComments } from './comments.js';
|
|
262
|
+
export { searchYouTubeGlobal } from './search.js';
|
package/src/search.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createBrowser, createPage, handleCookieConsent } from './utils/browser.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Search across all of YouTube for videos and channels
|
|
5
|
+
* @param {Object} options - Search options
|
|
6
|
+
* @param {string} options.query - Search term
|
|
7
|
+
* @param {number} [options.maxResults=10] - Maximum number of results (1-20)
|
|
8
|
+
* @param {string[]} [options.resultTypes=['all']] - Types of results to include ('videos', 'channels', 'all')
|
|
9
|
+
* @returns {Promise<Object>} Search results with videos and channels
|
|
10
|
+
*/
|
|
11
|
+
export async function searchYouTubeGlobal({ query, maxResults = 10, resultTypes = ['all'] }) {
|
|
12
|
+
// Validate inputs
|
|
13
|
+
if (!query || !query.trim()) {
|
|
14
|
+
throw new Error('Search query cannot be empty');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (maxResults < 1 || maxResults > 20) {
|
|
18
|
+
throw new Error('maxResults must be between 1 and 20');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const browser = await createBrowser();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const page = await createPage(browser);
|
|
25
|
+
|
|
26
|
+
// Navigate to YouTube search results
|
|
27
|
+
const searchUrl = `https://www.youtube.com/results?search_query=${encodeURIComponent(query.trim())}`;
|
|
28
|
+
console.error(`Navigating to ${searchUrl}`);
|
|
29
|
+
|
|
30
|
+
await page.goto(searchUrl, {
|
|
31
|
+
waitUntil: 'networkidle2',
|
|
32
|
+
timeout: 60000
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Handle cookie consent
|
|
36
|
+
await handleCookieConsent(page);
|
|
37
|
+
|
|
38
|
+
// Wait for search results to load
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
40
|
+
await page.waitForSelector('#contents', { timeout: 30000 });
|
|
41
|
+
console.error('Search results page loaded');
|
|
42
|
+
|
|
43
|
+
// Extract search results using validated selectors from discovery work
|
|
44
|
+
const searchResults = await page.evaluate((maxResults, resultTypes) => {
|
|
45
|
+
const results = [];
|
|
46
|
+
|
|
47
|
+
// Use validated selectors from BMAD discovery work
|
|
48
|
+
const videoElements = document.querySelectorAll('#contents ytd-video-renderer');
|
|
49
|
+
const channelElements = document.querySelectorAll('#contents ytd-channel-renderer');
|
|
50
|
+
|
|
51
|
+
// Extract video results
|
|
52
|
+
if (resultTypes.includes('all') || resultTypes.includes('videos')) {
|
|
53
|
+
Array.from(videoElements).forEach((element, index) => {
|
|
54
|
+
if (results.length >= maxResults) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Use validated selectors: h3 a for video titles
|
|
58
|
+
const titleElement = element.querySelector('h3 a');
|
|
59
|
+
const title = titleElement ? titleElement.textContent.trim() : '';
|
|
60
|
+
const url = titleElement ? titleElement.href : '';
|
|
61
|
+
|
|
62
|
+
// Extract video ID from URL
|
|
63
|
+
const videoId = url.match(/watch\?v=([^&]+)/)?.[1] || '';
|
|
64
|
+
|
|
65
|
+
// Channel name using validated selector
|
|
66
|
+
const channelElement = element.querySelector('#text a[href*="/channel/"], #text a[href*="/@"]');
|
|
67
|
+
const channel = channelElement ? channelElement.textContent.trim() : '';
|
|
68
|
+
|
|
69
|
+
// Metadata extraction
|
|
70
|
+
const metadataElement = element.querySelector('#metadata-line');
|
|
71
|
+
let views = '';
|
|
72
|
+
let uploadTime = '';
|
|
73
|
+
|
|
74
|
+
if (metadataElement) {
|
|
75
|
+
const spans = metadataElement.querySelectorAll('span');
|
|
76
|
+
if (spans.length >= 2) {
|
|
77
|
+
views = spans[0].textContent.trim();
|
|
78
|
+
uploadTime = spans[1].textContent.trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Duration
|
|
83
|
+
const durationElement = element.querySelector('ytd-thumbnail-overlay-time-status-renderer span');
|
|
84
|
+
const duration = durationElement ? durationElement.textContent.trim() : '';
|
|
85
|
+
|
|
86
|
+
// Thumbnail
|
|
87
|
+
const thumbnailElement = element.querySelector('img');
|
|
88
|
+
const thumbnail = thumbnailElement ? thumbnailElement.src : '';
|
|
89
|
+
|
|
90
|
+
if (title && url && videoId) {
|
|
91
|
+
results.push({
|
|
92
|
+
id: videoId,
|
|
93
|
+
type: 'video',
|
|
94
|
+
title: title,
|
|
95
|
+
url: url,
|
|
96
|
+
channel: channel,
|
|
97
|
+
views: views,
|
|
98
|
+
uploadTime: uploadTime,
|
|
99
|
+
duration: duration,
|
|
100
|
+
thumbnail: thumbnail
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error('Error extracting video result:', e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Extract channel results
|
|
110
|
+
if (resultTypes.includes('all') || resultTypes.includes('channels')) {
|
|
111
|
+
Array.from(channelElements).forEach((element, index) => {
|
|
112
|
+
if (results.length >= maxResults) return;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Channel title and URL
|
|
116
|
+
const titleElement = element.querySelector('#text a');
|
|
117
|
+
const title = titleElement ? titleElement.textContent.trim() : '';
|
|
118
|
+
const url = titleElement ? titleElement.href : '';
|
|
119
|
+
|
|
120
|
+
// Extract channel ID from URL
|
|
121
|
+
const channelId = url.match(/channel\/([^/]+)/)?.[1] || url.match(/@([^/]+)/)?.[1] || '';
|
|
122
|
+
|
|
123
|
+
// Subscriber count
|
|
124
|
+
const subsElement = element.querySelector('#subscribers');
|
|
125
|
+
const subscribers = subsElement ? subsElement.textContent.trim() : '';
|
|
126
|
+
|
|
127
|
+
// Video count
|
|
128
|
+
const videoCountElement = element.querySelector('#video-count');
|
|
129
|
+
const videoCount = videoCountElement ? videoCountElement.textContent.trim() : '';
|
|
130
|
+
|
|
131
|
+
// Thumbnail
|
|
132
|
+
const thumbnailElement = element.querySelector('img');
|
|
133
|
+
const thumbnail = thumbnailElement ? thumbnailElement.src : '';
|
|
134
|
+
|
|
135
|
+
if (title && url && channelId) {
|
|
136
|
+
results.push({
|
|
137
|
+
id: channelId,
|
|
138
|
+
type: 'channel',
|
|
139
|
+
title: title,
|
|
140
|
+
url: url,
|
|
141
|
+
subscribers: subscribers,
|
|
142
|
+
videoCount: videoCount,
|
|
143
|
+
thumbnail: thumbnail
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.error('Error extracting channel result:', e);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}, maxResults, resultTypes);
|
|
154
|
+
|
|
155
|
+
console.error(`Successfully extracted ${searchResults.length} search results`);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
query: query.trim(),
|
|
159
|
+
resultTypes: resultTypes,
|
|
160
|
+
maxResults: maxResults,
|
|
161
|
+
totalFound: searchResults.length,
|
|
162
|
+
results: searchResults
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Error performing YouTube search:', error);
|
|
167
|
+
throw error;
|
|
168
|
+
} finally {
|
|
169
|
+
await browser.close();
|
|
170
|
+
}
|
|
171
|
+
}
|