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 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.2.0",
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
+ }