getraw 0.1.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.
Files changed (105) hide show
  1. package/.gitattributes +4 -0
  2. package/CLAUDE.md +57 -0
  3. package/README.md +166 -0
  4. package/RESEARCH.md +109 -0
  5. package/STATUS.md +23 -0
  6. package/bun.lock +50 -0
  7. package/bunfig.toml +3 -0
  8. package/docs/plugin-guide.md +166 -0
  9. package/docs/supported-sites.md +41 -0
  10. package/package.json +30 -0
  11. package/src/cli/index.ts +52 -0
  12. package/src/cli/options.ts +97 -0
  13. package/src/core/format-sorter.ts +208 -0
  14. package/src/core/logger.ts +101 -0
  15. package/src/core/orchestrator.ts +140 -0
  16. package/src/core/output-template.ts +58 -0
  17. package/src/core/types.ts +237 -0
  18. package/src/downloaders/base.ts +25 -0
  19. package/src/downloaders/dash.ts +287 -0
  20. package/src/downloaders/fragment.ts +226 -0
  21. package/src/downloaders/hls.ts +170 -0
  22. package/src/downloaders/http.ts +260 -0
  23. package/src/extractors/archive-org.ts +126 -0
  24. package/src/extractors/bandcamp.ts +130 -0
  25. package/src/extractors/base.ts +29 -0
  26. package/src/extractors/bilibili/bangumi.ts +205 -0
  27. package/src/extractors/bilibili/index.ts +233 -0
  28. package/src/extractors/bilibili/wbi.ts +60 -0
  29. package/src/extractors/coub.ts +137 -0
  30. package/src/extractors/dailymotion.ts +99 -0
  31. package/src/extractors/dropbox.ts +52 -0
  32. package/src/extractors/generic.ts +118 -0
  33. package/src/extractors/google-drive.ts +106 -0
  34. package/src/extractors/imgur.ts +156 -0
  35. package/src/extractors/instagram/index.ts +263 -0
  36. package/src/extractors/instagram/reels.ts +166 -0
  37. package/src/extractors/kick/clips.ts +91 -0
  38. package/src/extractors/kick/index.ts +118 -0
  39. package/src/extractors/kick/live.ts +89 -0
  40. package/src/extractors/niconico/index.ts +209 -0
  41. package/src/extractors/odysee.ts +126 -0
  42. package/src/extractors/peertube.ts +143 -0
  43. package/src/extractors/reddit/gallery.ts +124 -0
  44. package/src/extractors/reddit/index.ts +203 -0
  45. package/src/extractors/rumble.ts +127 -0
  46. package/src/extractors/soundcloud/index.ts +161 -0
  47. package/src/extractors/soundcloud/playlist.ts +129 -0
  48. package/src/extractors/spotify.ts +97 -0
  49. package/src/extractors/streamable.ts +121 -0
  50. package/src/extractors/ted.ts +151 -0
  51. package/src/extractors/tiktok/index.ts +207 -0
  52. package/src/extractors/tiktok/user.ts +176 -0
  53. package/src/extractors/twitch/clips.ts +125 -0
  54. package/src/extractors/twitch/index.ts +136 -0
  55. package/src/extractors/twitch/live.ts +132 -0
  56. package/src/extractors/twitter/index.ts +140 -0
  57. package/src/extractors/twitter/spaces.ts +200 -0
  58. package/src/extractors/vimeo/index.ts +187 -0
  59. package/src/extractors/youtube/captions.ts +111 -0
  60. package/src/extractors/youtube/index.ts +252 -0
  61. package/src/extractors/youtube/innertube.ts +364 -0
  62. package/src/extractors/youtube/nsig.ts +105 -0
  63. package/src/extractors/youtube/playlist.ts +227 -0
  64. package/src/extractors/youtube/signature.ts +163 -0
  65. package/src/networking/client.ts +311 -0
  66. package/src/networking/cookies.ts +138 -0
  67. package/src/networking/proxy.ts +132 -0
  68. package/src/networking/tls.ts +67 -0
  69. package/src/networking/user-agents.ts +88 -0
  70. package/src/postprocessors/base.ts +44 -0
  71. package/src/postprocessors/extract-audio.ts +98 -0
  72. package/src/postprocessors/ffmpeg.ts +146 -0
  73. package/src/postprocessors/merge.ts +102 -0
  74. package/src/postprocessors/metadata.ts +73 -0
  75. package/src/postprocessors/sponsorblock.ts +162 -0
  76. package/src/postprocessors/subtitles.ts +285 -0
  77. package/src/postprocessors/thumbnails.ts +194 -0
  78. package/src/utils/sanitize.ts +36 -0
  79. package/src/utils/traverse.ts +68 -0
  80. package/tests/core/format-sorter.test.ts +96 -0
  81. package/tests/core/output-template.test.ts +56 -0
  82. package/tests/core/types.test.ts +79 -0
  83. package/tests/unit/downloaders/dash.test.ts +57 -0
  84. package/tests/unit/downloaders/hls.test.ts +120 -0
  85. package/tests/unit/downloaders/http.test.ts +114 -0
  86. package/tests/unit/extractors/bilibili.test.ts +83 -0
  87. package/tests/unit/extractors/instagram.test.ts +273 -0
  88. package/tests/unit/extractors/kick.test.ts +85 -0
  89. package/tests/unit/extractors/misc.test.ts +942 -0
  90. package/tests/unit/extractors/niconico.test.ts +61 -0
  91. package/tests/unit/extractors/reddit.test.ts +222 -0
  92. package/tests/unit/extractors/soundcloud.test.ts +299 -0
  93. package/tests/unit/extractors/tiktok.test.ts +260 -0
  94. package/tests/unit/extractors/twitch.test.ts +250 -0
  95. package/tests/unit/extractors/twitter.test.ts +181 -0
  96. package/tests/unit/extractors/vimeo.test.ts +253 -0
  97. package/tests/unit/extractors/youtube.test.ts +259 -0
  98. package/tests/unit/networking/client.test.ts +272 -0
  99. package/tests/unit/networking/cookies.test.ts +256 -0
  100. package/tests/unit/networking/proxy.test.ts +137 -0
  101. package/tests/unit/postprocessors/extract-audio.test.ts +63 -0
  102. package/tests/unit/postprocessors/merge.test.ts +61 -0
  103. package/tests/unit/postprocessors/subtitles.test.ts +89 -0
  104. package/tools/dashboard.ts +112 -0
  105. package/tsconfig.json +17 -0
@@ -0,0 +1,253 @@
1
+ import { describe, test, expect, mock } from "bun:test";
2
+ import { VimeoExtractor } from "../../../src/extractors/vimeo/index";
3
+ import { ExtractorError } from "../../../src/core/types";
4
+
5
+ const extractor = new VimeoExtractor();
6
+
7
+ const mockVimeoConfig = {
8
+ video: {
9
+ id: 123456789,
10
+ title: "My Vimeo Video",
11
+ description: "A test video",
12
+ duration: 120,
13
+ owner: {
14
+ name: "Video Creator",
15
+ url: "https://vimeo.com/creator",
16
+ },
17
+ thumbs: {
18
+ "640": "https://i.vimeocdn.com/video/thumb_640.jpg",
19
+ "1280": "https://i.vimeocdn.com/video/thumb_1280.jpg",
20
+ },
21
+ width: 1920,
22
+ height: 1080,
23
+ },
24
+ request: {
25
+ files: {
26
+ hls: {
27
+ default_cdn: "fastly_skyfire",
28
+ cdns: {
29
+ fastly_skyfire: {
30
+ url: "https://skyfire.vimeocdn.com/playlist.m3u8",
31
+ avc_url: "https://skyfire.vimeocdn.com/playlist_avc.m3u8",
32
+ },
33
+ akfire_interconnect_quic: {
34
+ url: "https://akfire.vimeocdn.com/playlist.m3u8",
35
+ },
36
+ },
37
+ },
38
+ dash: {
39
+ default_cdn: "fastly_skyfire",
40
+ cdns: {
41
+ fastly_skyfire: {
42
+ url: "https://skyfire.vimeocdn.com/manifest.mpd",
43
+ },
44
+ },
45
+ },
46
+ progressive: [
47
+ {
48
+ quality: "1080p",
49
+ mime: "video/mp4",
50
+ width: 1920,
51
+ height: 1080,
52
+ fps: 30,
53
+ url: "https://vod-progressive.akamaized.net/video1080.mp4",
54
+ size: 524288000,
55
+ },
56
+ {
57
+ quality: "720p",
58
+ mime: "video/mp4",
59
+ width: 1280,
60
+ height: 720,
61
+ fps: 30,
62
+ url: "https://vod-progressive.akamaized.net/video720.mp4",
63
+ size: 262144000,
64
+ },
65
+ {
66
+ quality: "360p",
67
+ mime: "video/mp4",
68
+ width: 640,
69
+ height: 360,
70
+ fps: 30,
71
+ url: "https://vod-progressive.akamaized.net/video360.mp4",
72
+ size: 52428800,
73
+ },
74
+ ],
75
+ },
76
+ },
77
+ };
78
+
79
+ describe("VimeoExtractor", () => {
80
+ test("canHandle matches standard vimeo.com URL", () => {
81
+ expect(extractor.canHandle("https://vimeo.com/123456789")).toBe(true);
82
+ });
83
+
84
+ test("canHandle matches player.vimeo.com URL", () => {
85
+ expect(extractor.canHandle("https://player.vimeo.com/video/123456789")).toBe(true);
86
+ });
87
+
88
+ test("canHandle matches channel video URL", () => {
89
+ expect(extractor.canHandle("https://vimeo.com/channels/mychannel/123456789")).toBe(true);
90
+ });
91
+
92
+ test("canHandle rejects non-vimeo URL", () => {
93
+ expect(extractor.canHandle("https://youtube.com/watch?v=abc")).toBe(false);
94
+ });
95
+
96
+ test("canHandle rejects vimeo homepage", () => {
97
+ expect(extractor.canHandle("https://vimeo.com/")).toBe(false);
98
+ });
99
+
100
+ test("_NAME is vimeo", () => {
101
+ expect(extractor._NAME).toBe("vimeo");
102
+ });
103
+
104
+ test("extract parses all format types from config", async () => {
105
+ const originalFetch = globalThis.fetch;
106
+
107
+ globalThis.fetch = mock(() =>
108
+ Promise.resolve(
109
+ new Response(JSON.stringify(mockVimeoConfig), {
110
+ status: 200,
111
+ headers: { "Content-Type": "application/json" },
112
+ })
113
+ )
114
+ );
115
+
116
+ const info = await extractor.extract("https://vimeo.com/123456789");
117
+
118
+ expect(info.id).toBe("123456789");
119
+ expect(info.title).toBe("My Vimeo Video");
120
+ expect(info.description).toBe("A test video");
121
+ expect(info.duration).toBe(120);
122
+ expect(info.uploader).toBe("Video Creator");
123
+ expect(info.formats).toBeDefined();
124
+
125
+ const progressive = info.formats!.filter((f) => f.format_id.startsWith("http-"));
126
+ const hls = info.formats!.filter((f) => f.protocol === "m3u8");
127
+ const dash = info.formats!.filter((f) => f.protocol === "dash");
128
+
129
+ expect(progressive.length).toBe(3);
130
+ expect(hls.length).toBeGreaterThan(0);
131
+ expect(dash.length).toBeGreaterThan(0);
132
+
133
+ const p1080 = progressive.find((f) => f.height === 1080);
134
+ expect(p1080).toBeDefined();
135
+ expect(p1080!.width).toBe(1920);
136
+ expect(p1080!.filesize).toBe(524288000);
137
+ expect(p1080!.vcodec).toBe("h264");
138
+
139
+ const hlsDefault = hls.find((f) => f.source_preference === 1);
140
+ expect(hlsDefault).toBeDefined();
141
+ expect(hlsDefault!.url).toContain("skyfire");
142
+ expect(hlsDefault!.url).toContain("_avc.m3u8");
143
+
144
+ globalThis.fetch = originalFetch;
145
+ });
146
+
147
+ test("extract parses thumbnails from config", async () => {
148
+ const originalFetch = globalThis.fetch;
149
+
150
+ globalThis.fetch = mock(() =>
151
+ Promise.resolve(
152
+ new Response(JSON.stringify(mockVimeoConfig), {
153
+ status: 200,
154
+ headers: { "Content-Type": "application/json" },
155
+ })
156
+ )
157
+ );
158
+
159
+ const info = await extractor.extract("https://vimeo.com/123456789");
160
+
161
+ expect(info.thumbnails).toBeDefined();
162
+ expect(info.thumbnails!.length).toBe(2);
163
+ const thumb640 = info.thumbnails!.find((t) => t.id === "640");
164
+ expect(thumb640).toBeDefined();
165
+ expect(thumb640!.width).toBe(640);
166
+
167
+ globalThis.fetch = originalFetch;
168
+ });
169
+
170
+ test("extract throws ExtractorError when config fetch fails", async () => {
171
+ const originalFetch = globalThis.fetch;
172
+
173
+ globalThis.fetch = mock(() =>
174
+ Promise.resolve(new Response(null, { status: 403 }))
175
+ );
176
+
177
+ await expect(
178
+ extractor.extract("https://vimeo.com/999999999")
179
+ ).rejects.toThrow(ExtractorError);
180
+
181
+ globalThis.fetch = originalFetch;
182
+ });
183
+
184
+ test("extract throws ExtractorError when no files in config", async () => {
185
+ const originalFetch = globalThis.fetch;
186
+
187
+ const emptyConfig = {
188
+ video: { id: 1, title: "Test" },
189
+ request: {},
190
+ };
191
+
192
+ globalThis.fetch = mock(() =>
193
+ Promise.resolve(
194
+ new Response(JSON.stringify(emptyConfig), {
195
+ status: 200,
196
+ headers: { "Content-Type": "application/json" },
197
+ })
198
+ )
199
+ );
200
+
201
+ await expect(
202
+ extractor.extract("https://vimeo.com/123456789")
203
+ ).rejects.toThrow(ExtractorError);
204
+
205
+ globalThis.fetch = originalFetch;
206
+ });
207
+
208
+ test("extract handles HLS only (no progressive)", async () => {
209
+ const originalFetch = globalThis.fetch;
210
+
211
+ const hlsOnlyConfig = {
212
+ ...mockVimeoConfig,
213
+ request: {
214
+ files: {
215
+ hls: mockVimeoConfig.request.files.hls,
216
+ },
217
+ },
218
+ };
219
+
220
+ globalThis.fetch = mock(() =>
221
+ Promise.resolve(
222
+ new Response(JSON.stringify(hlsOnlyConfig), {
223
+ status: 200,
224
+ headers: { "Content-Type": "application/json" },
225
+ })
226
+ )
227
+ );
228
+
229
+ const info = await extractor.extract("https://vimeo.com/123456789");
230
+ expect(info.formats!.length).toBeGreaterThan(0);
231
+ expect(info.formats!.every((f) => f.protocol === "m3u8")).toBe(true);
232
+
233
+ globalThis.fetch = originalFetch;
234
+ });
235
+
236
+ test("webpage_url is always canonical vimeo URL", async () => {
237
+ const originalFetch = globalThis.fetch;
238
+
239
+ globalThis.fetch = mock(() =>
240
+ Promise.resolve(
241
+ new Response(JSON.stringify(mockVimeoConfig), {
242
+ status: 200,
243
+ headers: { "Content-Type": "application/json" },
244
+ })
245
+ )
246
+ );
247
+
248
+ const info = await extractor.extract("https://player.vimeo.com/video/123456789");
249
+ expect(info.webpage_url).toBe("https://vimeo.com/123456789");
250
+
251
+ globalThis.fetch = originalFetch;
252
+ });
253
+ });
@@ -0,0 +1,259 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { YouTubeExtractor } from "../../../src/extractors/youtube/index";
3
+ import { InnerTubeClient } from "../../../src/extractors/youtube/innertube";
4
+ import type { StreamingData, CaptionTrack } from "../../../src/extractors/youtube/innertube";
5
+ import { parseCaptionTracks, convertToSrt, convertToVtt } from "../../../src/extractors/youtube/captions";
6
+ import type { TimedTextEvent } from "../../../src/extractors/youtube/captions";
7
+
8
+ describe("YouTubeExtractor URL matching", () => {
9
+ const extractor = new YouTubeExtractor();
10
+
11
+ const validUrls = [
12
+ "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
13
+ "https://youtube.com/watch?v=dQw4w9WgXcQ",
14
+ "https://m.youtube.com/watch?v=dQw4w9WgXcQ",
15
+ "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLtest",
16
+ "https://youtu.be/dQw4w9WgXcQ",
17
+ "https://www.youtube.com/shorts/dQw4w9WgXcQ",
18
+ "https://www.youtube.com/live/dQw4w9WgXcQ",
19
+ "https://www.youtube.com/embed/dQw4w9WgXcQ",
20
+ "https://www.youtube.com/v/dQw4w9WgXcQ",
21
+ "https://music.youtube.com/watch?v=dQw4w9WgXcQ",
22
+ "https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf",
23
+ "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
24
+ "https://www.youtube.com/@username",
25
+ ];
26
+
27
+ const invalidUrls = [
28
+ "https://www.example.com/watch?v=dQw4w9WgXcQ",
29
+ "https://vimeo.com/123456",
30
+ "https://www.youtube.com/",
31
+ "https://www.youtube.com/results?search_query=test",
32
+ ];
33
+
34
+ for (const url of validUrls) {
35
+ test(`matches: ${url}`, () => {
36
+ expect(extractor.canHandle(url)).toBe(true);
37
+ });
38
+ }
39
+
40
+ for (const url of invalidUrls) {
41
+ test(`rejects: ${url}`, () => {
42
+ expect(extractor.canHandle(url)).toBe(false);
43
+ });
44
+ }
45
+ });
46
+
47
+ describe("InnerTubeClient format parsing", () => {
48
+ const client = new InnerTubeClient();
49
+
50
+ test("parses muxed and adaptive formats", () => {
51
+ const streamingData: StreamingData = {
52
+ formats: [
53
+ {
54
+ itag: 18,
55
+ url: "https://rr.googlevideo.com/videoplayback?itag=18",
56
+ mimeType: 'video/mp4; codecs="avc1.42001E, mp4a.40.2"',
57
+ bitrate: 500000,
58
+ width: 640,
59
+ height: 360,
60
+ contentLength: "15000000",
61
+ quality: "medium",
62
+ qualityLabel: "360p",
63
+ fps: 30,
64
+ audioChannels: 2,
65
+ },
66
+ ],
67
+ adaptiveFormats: [
68
+ {
69
+ itag: 137,
70
+ url: "https://rr.googlevideo.com/videoplayback?itag=137",
71
+ mimeType: 'video/mp4; codecs="avc1.640028"',
72
+ bitrate: 4000000,
73
+ width: 1920,
74
+ height: 1080,
75
+ contentLength: "50000000",
76
+ quality: "hd1080",
77
+ qualityLabel: "1080p",
78
+ fps: 30,
79
+ averageBitrate: 3500000,
80
+ },
81
+ {
82
+ itag: 251,
83
+ url: "https://rr.googlevideo.com/videoplayback?itag=251",
84
+ mimeType: 'audio/webm; codecs="opus"',
85
+ bitrate: 160000,
86
+ contentLength: "5000000",
87
+ audioQuality: "AUDIO_QUALITY_HIGH",
88
+ audioSampleRate: "48000",
89
+ audioChannels: 2,
90
+ averageBitrate: 130000,
91
+ },
92
+ ],
93
+ };
94
+
95
+ const formats = client.parseFormats(streamingData);
96
+
97
+ expect(formats).toHaveLength(3);
98
+
99
+ const muxed = formats[0];
100
+ expect(muxed.format_id).toBe("18");
101
+ expect(muxed.ext).toBe("mp4");
102
+ expect(muxed.width).toBe(640);
103
+ expect(muxed.height).toBe(360);
104
+ expect(muxed.vcodec).toBe("avc1.42001E");
105
+ expect(muxed.acodec).toBe("mp4a.40.2");
106
+ expect(muxed.resolution).toBe("640x360");
107
+
108
+ const videoOnly = formats[1];
109
+ expect(videoOnly.format_id).toBe("137");
110
+ expect(videoOnly.ext).toBe("mp4");
111
+ expect(videoOnly.width).toBe(1920);
112
+ expect(videoOnly.height).toBe(1080);
113
+ expect(videoOnly.vbr).toBe(3500);
114
+
115
+ const audioOnly = formats[2];
116
+ expect(audioOnly.format_id).toBe("251");
117
+ expect(audioOnly.ext).toBe("webm");
118
+ expect(audioOnly.vcodec).toBe("none");
119
+ expect(audioOnly.acodec).toBe("opus");
120
+ expect(audioOnly.abr).toBe(130);
121
+ expect(audioOnly.audio_channels).toBe(2);
122
+ });
123
+
124
+ test("skips formats without url or signatureCipher", () => {
125
+ const streamingData: StreamingData = {
126
+ formats: [
127
+ {
128
+ itag: 18,
129
+ mimeType: 'video/mp4; codecs="avc1.42001E"',
130
+ bitrate: 500000,
131
+ },
132
+ ],
133
+ adaptiveFormats: [],
134
+ };
135
+
136
+ const formats = client.parseFormats(streamingData);
137
+ expect(formats).toHaveLength(0);
138
+ });
139
+
140
+ test("parses format with signatureCipher as having empty url", () => {
141
+ const streamingData: StreamingData = {
142
+ formats: [
143
+ {
144
+ itag: 18,
145
+ signatureCipher: "s=test&sp=sig&url=https%3A%2F%2Fexample.com",
146
+ mimeType: 'video/mp4; codecs="avc1.42001E"',
147
+ bitrate: 500000,
148
+ width: 640,
149
+ height: 360,
150
+ },
151
+ ],
152
+ adaptiveFormats: [],
153
+ };
154
+
155
+ const formats = client.parseFormats(streamingData);
156
+ expect(formats).toHaveLength(1);
157
+ expect(formats[0].url).toBe("");
158
+ });
159
+ });
160
+
161
+ describe("Caption parsing", () => {
162
+ test("separates manual and auto-generated captions", () => {
163
+ const tracks: CaptionTrack[] = [
164
+ {
165
+ baseUrl: "https://www.youtube.com/api/timedtext?v=test&lang=en",
166
+ name: { simpleText: "English" },
167
+ vssId: ".en",
168
+ languageCode: "en",
169
+ isTranslatable: true,
170
+ },
171
+ {
172
+ baseUrl: "https://www.youtube.com/api/timedtext?v=test&lang=en&kind=asr",
173
+ name: { simpleText: "English (auto-generated)" },
174
+ vssId: "a.en",
175
+ languageCode: "en",
176
+ kind: "asr",
177
+ isTranslatable: true,
178
+ },
179
+ {
180
+ baseUrl: "https://www.youtube.com/api/timedtext?v=test&lang=es",
181
+ name: { simpleText: "Spanish" },
182
+ vssId: ".es",
183
+ languageCode: "es",
184
+ isTranslatable: true,
185
+ },
186
+ ];
187
+
188
+ const { subtitles, automatic_captions } = parseCaptionTracks(tracks);
189
+
190
+ expect(Object.keys(subtitles)).toEqual(["en", "es"]);
191
+ expect(Object.keys(automatic_captions)).toEqual(["en"]);
192
+
193
+ expect(subtitles.en).toHaveLength(3);
194
+ expect(subtitles.en[0].ext).toBe("json3");
195
+ expect(subtitles.en[1].ext).toBe("vtt");
196
+ expect(subtitles.en[2].ext).toBe("srv1");
197
+
198
+ expect(automatic_captions.en).toHaveLength(3);
199
+ expect(automatic_captions.en[0].name).toBe("English (auto-generated)");
200
+ });
201
+ });
202
+
203
+ describe("Caption format conversion", () => {
204
+ const events: TimedTextEvent[] = [
205
+ {
206
+ tStartMs: 0,
207
+ dDurationMs: 2000,
208
+ segs: [{ utf8: "Hello " }, { utf8: "world" }],
209
+ },
210
+ {
211
+ tStartMs: 2500,
212
+ dDurationMs: 3000,
213
+ segs: [{ utf8: "This is a test" }],
214
+ },
215
+ {
216
+ tStartMs: 6000,
217
+ dDurationMs: 1000,
218
+ segs: [],
219
+ },
220
+ ];
221
+
222
+ test("converts to SRT format", () => {
223
+ const srt = convertToSrt(events);
224
+ const lines = srt.split("\n");
225
+
226
+ expect(lines[0]).toBe("1");
227
+ expect(lines[1]).toBe("00:00:00,000 --> 00:00:02,000");
228
+ expect(lines[2]).toBe("Hello world");
229
+ expect(lines[3]).toBe("");
230
+ expect(lines[4]).toBe("2");
231
+ expect(lines[5]).toBe("00:00:02,500 --> 00:00:05,500");
232
+ expect(lines[6]).toBe("This is a test");
233
+ });
234
+
235
+ test("converts to VTT format", () => {
236
+ const vtt = convertToVtt(events);
237
+ const lines = vtt.split("\n");
238
+
239
+ expect(lines[0]).toBe("WEBVTT");
240
+ expect(lines[1]).toBe("");
241
+ expect(lines[2]).toBe("00:00:00.000 --> 00:00:02.000");
242
+ expect(lines[3]).toBe("Hello world");
243
+ expect(lines[4]).toBe("");
244
+ expect(lines[5]).toBe("00:00:02.500 --> 00:00:05.500");
245
+ expect(lines[6]).toBe("This is a test");
246
+ });
247
+
248
+ test("skips events with empty segments", () => {
249
+ const srt = convertToSrt(events);
250
+ expect(srt).not.toContain("3\n");
251
+ });
252
+ });
253
+
254
+ describe("YouTubeExtractor metadata", () => {
255
+ test("has correct extractor name", () => {
256
+ const extractor = new YouTubeExtractor();
257
+ expect(extractor._NAME).toBe("youtube");
258
+ });
259
+ });