extract-youtube 1.0.2 → 1.0.3

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.
Files changed (47) hide show
  1. package/README.md +61 -20
  2. package/dist/archiver-web.es-3EQgnnex.js +2 -0
  3. package/dist/archiver-web.es-3EQgnnex.js.map +1 -0
  4. package/dist/archiver-web.es-CO0IfW41.mjs +2 -0
  5. package/dist/archiver-web.es-CO0IfW41.mjs.map +1 -0
  6. package/dist/archiver-web.es-eh2cNrlm.js +3140 -0
  7. package/dist/archiver-web.es-eh2cNrlm.js.map +1 -0
  8. package/dist/cli.js +455 -56
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index-CLlAnStL-BKslXZFs.mjs +2 -0
  11. package/dist/index-CLlAnStL-BKslXZFs.mjs.map +1 -0
  12. package/dist/index-CLlAnStL-BNm8mEFS.js +6524 -0
  13. package/dist/index-CLlAnStL-BNm8mEFS.js.map +1 -0
  14. package/dist/index-CLlAnStL-DDV_Pgc5.js +2 -0
  15. package/dist/index-CLlAnStL-DDV_Pgc5.js.map +1 -0
  16. package/dist/index.cjs +2 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.mjs +2 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/dist/rolldown-runtime-Bd6kNlEP.js +71 -0
  21. package/jest.config.js +0 -0
  22. package/package.json +13 -10
  23. package/src/cli.ts +11 -2
  24. package/src/{errors.ts → errors/index.ts} +1 -1
  25. package/src/fetchers/transcript-list-fetcher.ts +0 -0
  26. package/src/fetchers/youtube-data-extractor.ts +0 -0
  27. package/src/{formatters.ts → formatters/index.ts} +82 -3
  28. package/src/http/fetch-http-client.ts +9 -9
  29. package/src/index.ts +4 -0
  30. package/src/models/fetched-transcript.ts +0 -0
  31. package/src/models/index.ts +0 -0
  32. package/src/models/transcript-list.ts +0 -0
  33. package/src/models/transcript.ts +0 -0
  34. package/src/parsers/transcript-parser.ts +4 -0
  35. package/src/{proxies.ts → proxies/index.ts} +1 -1
  36. package/src/{types.ts → types/index.ts} +0 -2
  37. package/src/utils/http-error-handler.ts +0 -1
  38. package/src/utils/transcript-utils.ts +107 -0
  39. package/src/youtube-transcript-api.ts +0 -0
  40. package/{__tests__ → test}/api.test.ts +0 -0
  41. package/{__tests__/basic-usage.ts → test/basic.test.ts} +0 -0
  42. package/{__tests__ → test}/formatters.test.ts +0 -0
  43. package/{__tests__ → test}/integration.test.ts +0 -0
  44. package/tsconfig.json +5 -5
  45. package/tsconfig.test.json +10 -0
  46. package/vite.config.ts +8 -3
  47. package/CHANGELOG.md +0 -1
@@ -2,8 +2,8 @@
2
2
  * @fileoverview All formatter classes for transcript formatting
3
3
  */
4
4
 
5
- import type { FetchedTranscript } from './models/fetched-transcript';
6
- import type { FetchedTranscriptSnippet } from './types';
5
+ import type { FetchedTranscript } from '../models/fetched-transcript';
6
+ import type { FetchedTranscriptSnippet } from '../types';
7
7
 
8
8
  /**
9
9
  * Base abstract class for all transcript formatters.
@@ -52,6 +52,83 @@ export class TextFormatter extends Formatter {
52
52
  }
53
53
  }
54
54
 
55
+ /**
56
+ * Formatter that converts transcripts to article format with character-to-timestamp mappings.
57
+ * Returns full text as a string along with character position to time pairings.
58
+ */
59
+ export class ArticleFormatter extends Formatter {
60
+ formatTranscript(transcript: FetchedTranscript): string {
61
+ // Build full text content
62
+ const fullText = transcript.snippets.map(snippet => snippet.text).join(' ');
63
+
64
+ // Build timestamp mappings - track which character position corresponds to which time
65
+ const timestamps: Array<[number, number]> = [];
66
+ let charPosition = 0;
67
+
68
+ for (const snippet of transcript.snippets) {
69
+ // Record the character position and start time for this snippet
70
+ timestamps.push([charPosition, snippet.start]);
71
+ charPosition += snippet.text.length + 1; // +1 for the space separator
72
+ }
73
+
74
+ // Calculate character-per-second speeds at intervals
75
+ const speedsEveryCharPeriod: { [key: number]: number } = {};
76
+ const valueCharPeriod = 100;
77
+
78
+ for (const [char, time] of timestamps) {
79
+ const speed = Math.floor(char / time) - 10;
80
+ speedsEveryCharPeriod[Math.floor(char / valueCharPeriod)] = speed;
81
+ }
82
+
83
+ // Compress the speed data
84
+ const speeds = Object.keys(speedsEveryCharPeriod).map(
85
+ (timeKey) => speedsEveryCharPeriod[parseInt(timeKey)]
86
+ );
87
+
88
+ const compressed: number[] = [];
89
+ const compressedCount: number[] = [];
90
+ let currentNum = speeds[0];
91
+ let count = 1;
92
+
93
+ for (let i = 1; i < speeds.length; i++) {
94
+ if (speeds[i] === currentNum) {
95
+ count++;
96
+ } else {
97
+ compressed.push(currentNum);
98
+ compressedCount.push(count);
99
+ currentNum = speeds[i];
100
+ count = 1;
101
+ }
102
+ }
103
+ compressed.push(currentNum);
104
+ compressedCount.push(count);
105
+
106
+ // Convert counts to cumulative positions
107
+ let total = 0;
108
+ const cumulativeCounts = compressedCount.map((c) => {
109
+ total += c;
110
+ return total;
111
+ });
112
+
113
+ // Format output with speeds and timestamps
114
+ const speedsData = compressed.join(',') + ' ' + cumulativeCounts.join(',');
115
+
116
+ // Remove extra spaces from full text
117
+ const cleanedText = fullText.replace(/\s+/g, ' ');
118
+
119
+ return JSON.stringify({
120
+ text: cleanedText,
121
+ timestamps: speedsData,
122
+ wordCount: cleanedText.split(/\s+/).length,
123
+ charCount: cleanedText.length
124
+ }, null, 2);
125
+ }
126
+
127
+ formatTranscripts(transcripts: FetchedTranscript[]): string {
128
+ return transcripts.map(t => this.formatTranscript(t)).join('\n\n');
129
+ }
130
+ }
131
+
55
132
  /**
56
133
  * Base class for subtitle formatters (SRT, WebVTT).
57
134
  */
@@ -132,7 +209,7 @@ export class WebVTTFormatter extends SubtitleFormatterBase {
132
209
  */
133
210
  export class UnknownFormatterType extends Error {
134
211
  constructor(formatterType: string) {
135
- super(`Unknown formatter type: ${formatterType}. Available types: json, text, srt, webvtt, pretty`);
212
+ super(`Unknown formatter type: ${formatterType}. Available types: json, text, srt, webvtt, pretty, article`);
136
213
  this.name = 'UnknownFormatterType';
137
214
  Object.setPrototypeOf(this, UnknownFormatterType.prototype);
138
215
  }
@@ -150,6 +227,8 @@ export class FormatterLoader {
150
227
  return new JSONFormatter();
151
228
  case 'text':
152
229
  return new TextFormatter();
230
+ case 'article':
231
+ return new ArticleFormatter();
153
232
  case 'srt':
154
233
  return new SRTFormatter();
155
234
  case 'webvtt':
@@ -1,13 +1,13 @@
1
1
  /**
2
- * @fileoverview HTTP client implementation using node-fetch
2
+ * @fileoverview HTTP client implementation using grab-url
3
3
  */
4
4
 
5
- import fetch, { Response, RequestInit } from 'node-fetch';
5
+ import grab from 'grab-url';
6
6
  import { HttpsProxyAgent } from 'https-proxy-agent';
7
7
  import { HttpClient, RequestsProxyConfigDict } from '../types';
8
8
 
9
9
  /**
10
- * Simple HTTP client implementation using node-fetch with proxy support.
10
+ * Simple HTTP client implementation using grab-url with proxy support.
11
11
  * This client handles HTTP/HTTPS requests with configurable headers and proxy settings.
12
12
  *
13
13
  * @internal
@@ -52,12 +52,12 @@ export class FetchHttpClient implements HttpClient {
52
52
  */
53
53
  async get(url: string, options?: RequestInit): Promise<Response> {
54
54
  const proxyAgent = this.getProxyAgentForUrl(url);
55
- return fetch(url, {
55
+ return grab(url, {
56
56
  ...options,
57
57
  method: 'GET',
58
- headers: { ...this.requestHeaders, ...options?.headers },
58
+ headers: { ...this.requestHeaders, ...(options?.headers as Record<string, string>) },
59
59
  agent: proxyAgent
60
- });
60
+ } as any);
61
61
  }
62
62
 
63
63
  /**
@@ -69,12 +69,12 @@ export class FetchHttpClient implements HttpClient {
69
69
  */
70
70
  async post(url: string, options?: RequestInit): Promise<Response> {
71
71
  const proxyAgent = this.getProxyAgentForUrl(url);
72
- return fetch(url, {
72
+ return grab(url, {
73
73
  ...options,
74
74
  method: 'POST',
75
- headers: { ...this.requestHeaders, ...options?.headers },
75
+ headers: { ...this.requestHeaders, ...(options?.headers as Record<string, string>) },
76
76
  agent: proxyAgent
77
- });
77
+ } as any);
78
78
  }
79
79
 
80
80
  /**
package/src/index.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  // Main API
7
7
  export { YouTubeTranscriptApi } from './youtube-transcript-api';
8
8
 
9
+ // Transcript utilities
10
+ export { encodeTranscriptSpeeds, getTimestampAtChar, decompressTimestampsArray } from './utils/transcript-utils';
11
+
9
12
  // Transcript model classes
10
13
  export {
11
14
  FetchedTranscript,
@@ -30,6 +33,7 @@ export {
30
33
  JSONFormatter,
31
34
  PrettyPrintFormatter,
32
35
  TextFormatter,
36
+ ArticleFormatter,
33
37
  SRTFormatter,
34
38
  WebVTTFormatter,
35
39
  FormatterLoader,
File without changes
File without changes
File without changes
File without changes
@@ -129,6 +129,10 @@ export class TranscriptParser {
129
129
  * @returns {string} The cleaned text
130
130
  */
131
131
  private cleanTranscriptText(text: string): string {
132
+ // Ensure text is a string
133
+ if (typeof text !== 'string') {
134
+ text = String(text || '');
135
+ }
132
136
  const decodedText = decode(text);
133
137
  return decodedText.replace(this.htmlRemovalRegex, '');
134
138
  }
@@ -2,7 +2,7 @@
2
2
  * @fileoverview Proxy configuration classes for the YouTube Transcript API
3
3
  */
4
4
 
5
- import { RequestsProxyConfigDict } from './types';
5
+ import { RequestsProxyConfigDict } from '../types';
6
6
 
7
7
  /**
8
8
  * Exception for invalid proxy configurations
@@ -2,8 +2,6 @@
2
2
  * @fileoverview Type definitions for the YouTube Transcript API
3
3
  */
4
4
 
5
- import { Response, RequestInit } from 'node-fetch';
6
-
7
5
  /**
8
6
  * Represents a single snippet/cue of a transcript
9
7
  */
@@ -2,7 +2,6 @@
2
2
  * @fileoverview HTTP error handling utilities for YouTube API requests
3
3
  */
4
4
 
5
- import { Response } from 'node-fetch';
6
5
  import { IpBlocked, YouTubeRequestFailed } from '../errors';
7
6
 
8
7
  /**
@@ -0,0 +1,107 @@
1
+ import { FetchedTranscript } from '../models';
2
+
3
+ /**
4
+ * Encodes speech speed from a fetched transcript into a compact run-length string.
5
+ * Optionally prepends a YouTube embed iframe with data-timestamps for player sync.
6
+ * @param transcript - A FetchedTranscript returned by fetchTranscript()
7
+ * @param addPlayer - If true, prepends an iframe embed with data-timestamps attribute
8
+ * @returns html content, word count, run-length encoded speed string, and raw timestamps
9
+ */
10
+ export function encodeTranscriptSpeeds(
11
+ transcript: FetchedTranscript,
12
+ addPlayer = false
13
+ ): {
14
+ html: string;
15
+ word_count: number;
16
+ speeds: string;
17
+ } {
18
+ let charCount = 0;
19
+ const timestamps: [number, number][] = [];
20
+ const textParts: string[] = [];
21
+
22
+ for (const snippet of transcript.snippets) {
23
+ textParts.push(snippet.text);
24
+ charCount += snippet.text.length;
25
+ if (snippet.start > 0) {
26
+ timestamps.push([charCount, snippet.start]);
27
+ }
28
+ }
29
+
30
+ const content = textParts.join(' ').replace(/\s+/g, ' ');
31
+ const word_count = content.split(/\s+/).filter(Boolean).length;
32
+
33
+ const speedValues = timestamps.map(([char, time]) => Math.floor(char / time) - 10);
34
+ if (speedValues.length === 0) {
35
+ return { html: content, word_count, speeds: '' };
36
+ }
37
+
38
+ const compressed: number[] = [];
39
+ const compressedCount: number[] = [];
40
+ let currentNum = speedValues[0];
41
+ let count = 1;
42
+
43
+ for (let i = 1; i < speedValues.length; i++) {
44
+ if (speedValues[i] === currentNum) {
45
+ count++;
46
+ } else {
47
+ compressed.push(currentNum);
48
+ compressedCount.push(count);
49
+ currentNum = speedValues[i];
50
+ count = 1;
51
+ }
52
+ }
53
+ compressed.push(currentNum);
54
+ compressedCount.push(count);
55
+
56
+ const speeds = compressed.map((val, i) => `${val}x${compressedCount[i]}`).join(',');
57
+
58
+ const html = addPlayer
59
+ ? `<iframe width="100%" height="315px" data-timestamps="${speeds}" ` +
60
+ `src="https://www.youtube.com/embed/${transcript.videoId}" frameborder="0" ` +
61
+ `allow="accelerometer; autoplay; clipboard-write; encrypted-media; ` +
62
+ `gyroscope; picture-in-picture" allowfullscreen></iframe>${content}`
63
+ : content;
64
+
65
+ return { html, word_count, speeds };
66
+ }
67
+
68
+ /**
69
+ * Given a character index in the joined transcript text, returns the interpolated
70
+ * video timestamp in seconds. Uses the nearest snippet boundaries to interpolate.
71
+ */
72
+ export function getTimestampAtChar(transcript: FetchedTranscript, charIndex: number): number {
73
+ let charCount = 0;
74
+ const points: [number, number][] = [[0, 0]]; // [cumulativeChars, seconds]
75
+
76
+ for (const snippet of transcript.snippets) {
77
+ charCount += snippet.text.length + 1; // +1 for the join space
78
+ points.push([charCount, snippet.start]);
79
+ }
80
+
81
+ if (charIndex <= 0) return 0;
82
+ if (charIndex >= points[points.length - 1][0]) return points[points.length - 1][1];
83
+
84
+ for (let i = 1; i < points.length; i++) {
85
+ if (points[i][0] >= charIndex) {
86
+ const [c0, t0] = points[i - 1];
87
+ const [c1, t1] = points[i];
88
+ const ratio = (charIndex - c0) / (c1 - c0);
89
+ return t0 + ratio * (t1 - t0);
90
+ }
91
+ }
92
+
93
+ return points[points.length - 1][1];
94
+ }
95
+
96
+ /**
97
+ * Decompresses a speeds string produced by encodeTranscriptSpeeds back into a flat array.
98
+ * Format: "12x3,8x7,15x1" → [12,12,12, 8,8,8,8,8,8,8, 15]
99
+ */
100
+ export function decompressTimestampsArray(speeds: string): number[] {
101
+ const decompressed: number[] = [];
102
+ for (const part of speeds.split(',')) {
103
+ const [num, count] = part.split('x');
104
+ decompressed.push(...Array(parseInt(count)).fill(parseInt(num)));
105
+ }
106
+ return decompressed;
107
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
package/tsconfig.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
4
  "module": "commonjs",
5
- "lib": ["ES2020"],
5
+ "lib": ["ES2020", "DOM"],
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "declaration": true,
@@ -12,9 +12,9 @@
12
12
  "esModuleInterop": true,
13
13
  "skipLibCheck": true,
14
14
  "forceConsistentCasingInFileNames": true,
15
- "moduleResolution": "node",
16
- "resolveJsonModule": true
15
+ "resolveJsonModule": true,
16
+ "types": ["node"]
17
17
  },
18
- "include": ["src/**/*", "__tests__"],
19
- "exclude": ["node_modules", "dist", "src/**/*.test.ts", "__tests__"]
18
+ "include": ["src/**/*", "test"],
19
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts", "test"]
20
20
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "types": ["node", "jest"],
6
+ "noEmit": true
7
+ },
8
+ "include": ["src/**/*", "test/**/*"],
9
+ "exclude": ["node_modules", "dist"]
10
+ }
package/vite.config.ts CHANGED
@@ -10,6 +10,8 @@ const buildCli = process.env.BUILD_CLI === 'true';
10
10
 
11
11
  export default defineConfig({
12
12
  build: {
13
+ // Don't empty output dir for CLI build to preserve library files
14
+ emptyOutDir: !buildCli,
13
15
  lib: buildCli ? {
14
16
  entry: resolve(__dirname, 'src/cli.ts'),
15
17
  name: 'ExtractYoutubeCli',
@@ -22,9 +24,12 @@ export default defineConfig({
22
24
  fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
23
25
  },
24
26
  rollupOptions: {
25
- // Externalize dependencies to avoid bundling them
27
+ // Externalize dependencies to avoid bundling them.
28
+ // grab-url is deliberately bundled: its published CJS entry resolves
29
+ // its internal chunks relative to the consumer's cwd and fails to load
30
+ // under require(); bundling its ESM source avoids the broken entry.
26
31
  external: [
27
- ...Object.keys(pkg.dependencies || {}),
32
+ ...Object.keys(pkg.dependencies || {}).filter((d) => d !== 'grab-url'),
28
33
  'node:http',
29
34
  'node:https',
30
35
  'node:url',
@@ -38,7 +43,7 @@ export default defineConfig({
38
43
  exports: 'named',
39
44
  // Provide global names for UMD/IIFE builds
40
45
  globals: {
41
- 'node-fetch': 'fetch',
46
+ 'grab-url': 'grab',
42
47
  'fast-xml-parser': 'XMLParser',
43
48
  'html-entities': 'htmlEntities',
44
49
  'https-proxy-agent': 'HttpsProxyAgent',
package/CHANGELOG.md DELETED
@@ -1 +0,0 @@
1
-