offcourse 1.0.0 → 1.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.
- package/README.md +107 -8
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/inspect.js +1 -1
- package/dist/cli/commands/inspect.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +1 -2
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +17 -15
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/commands/syncHighLevel.d.ts +1 -2
- package/dist/cli/commands/syncHighLevel.d.ts.map +1 -1
- package/dist/cli/commands/syncHighLevel.js +8 -9
- package/dist/cli/commands/syncHighLevel.js.map +1 -1
- package/dist/cli/commands/syncLearningSuite.d.ts +35 -0
- package/dist/cli/commands/syncLearningSuite.d.ts.map +1 -0
- package/dist/cli/commands/syncLearningSuite.js +765 -0
- package/dist/cli/commands/syncLearningSuite.js.map +1 -0
- package/dist/cli/index.js +39 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/config/configManager.d.ts.map +1 -1
- package/dist/config/configManager.js +4 -0
- package/dist/config/configManager.js.map +1 -1
- package/dist/downloader/hlsDownloader.d.ts +10 -4
- package/dist/downloader/hlsDownloader.d.ts.map +1 -1
- package/dist/downloader/hlsDownloader.js +60 -29
- package/dist/downloader/hlsDownloader.js.map +1 -1
- package/dist/downloader/hlsValidator.d.ts.map +1 -1
- package/dist/downloader/hlsValidator.js +6 -2
- package/dist/downloader/hlsValidator.js.map +1 -1
- package/dist/downloader/index.d.ts +7 -0
- package/dist/downloader/index.d.ts.map +1 -1
- package/dist/downloader/index.js +9 -6
- package/dist/downloader/index.js.map +1 -1
- package/dist/downloader/loomDownloader.d.ts +1 -1
- package/dist/downloader/loomDownloader.d.ts.map +1 -1
- package/dist/downloader/loomDownloader.js +32 -27
- package/dist/downloader/loomDownloader.js.map +1 -1
- package/dist/downloader/queue.d.ts +4 -4
- package/dist/downloader/queue.d.ts.map +1 -1
- package/dist/downloader/queue.js.map +1 -1
- package/dist/downloader/vimeoDownloader.d.ts.map +1 -1
- package/dist/downloader/vimeoDownloader.js +7 -3
- package/dist/downloader/vimeoDownloader.js.map +1 -1
- package/dist/scraper/extractor.d.ts +4 -0
- package/dist/scraper/extractor.d.ts.map +1 -1
- package/dist/scraper/extractor.js +79 -79
- package/dist/scraper/extractor.js.map +1 -1
- package/dist/scraper/highlevel/extractor.d.ts +11 -19
- package/dist/scraper/highlevel/extractor.d.ts.map +1 -1
- package/dist/scraper/highlevel/extractor.js +72 -85
- package/dist/scraper/highlevel/extractor.js.map +1 -1
- package/dist/scraper/highlevel/navigator.d.ts +3 -10
- package/dist/scraper/highlevel/navigator.d.ts.map +1 -1
- package/dist/scraper/highlevel/navigator.js +140 -127
- package/dist/scraper/highlevel/navigator.js.map +1 -1
- package/dist/scraper/highlevel/schemas.d.ts +188 -0
- package/dist/scraper/highlevel/schemas.d.ts.map +1 -0
- package/dist/scraper/highlevel/schemas.js +139 -0
- package/dist/scraper/highlevel/schemas.js.map +1 -0
- package/dist/scraper/learningsuite/extractor.d.ts +50 -0
- package/dist/scraper/learningsuite/extractor.d.ts.map +1 -0
- package/dist/scraper/learningsuite/extractor.js +429 -0
- package/dist/scraper/learningsuite/extractor.js.map +1 -0
- package/dist/scraper/learningsuite/index.d.ts +4 -0
- package/dist/scraper/learningsuite/index.d.ts.map +1 -0
- package/dist/scraper/{ghl → learningsuite}/index.js +1 -1
- package/dist/scraper/learningsuite/index.js.map +1 -0
- package/dist/scraper/learningsuite/navigator.d.ts +122 -0
- package/dist/scraper/learningsuite/navigator.d.ts.map +1 -0
- package/dist/scraper/learningsuite/navigator.js +736 -0
- package/dist/scraper/learningsuite/navigator.js.map +1 -0
- package/dist/scraper/learningsuite/schemas.d.ts +270 -0
- package/dist/scraper/learningsuite/schemas.d.ts.map +1 -0
- package/dist/scraper/learningsuite/schemas.js +147 -0
- package/dist/scraper/learningsuite/schemas.js.map +1 -0
- package/dist/scraper/navigator.d.ts +14 -11
- package/dist/scraper/navigator.d.ts.map +1 -1
- package/dist/scraper/navigator.js +61 -104
- package/dist/scraper/navigator.js.map +1 -1
- package/dist/scraper/schemas.d.ts +57 -0
- package/dist/scraper/schemas.d.ts.map +1 -0
- package/dist/scraper/schemas.js +135 -0
- package/dist/scraper/schemas.js.map +1 -0
- package/dist/scraper/videoInterceptor.d.ts +4 -0
- package/dist/scraper/videoInterceptor.d.ts.map +1 -1
- package/dist/scraper/videoInterceptor.js +66 -51
- package/dist/scraper/videoInterceptor.js.map +1 -1
- package/dist/shared/auth.d.ts +9 -9
- package/dist/shared/auth.d.ts.map +1 -1
- package/dist/shared/auth.js +24 -38
- package/dist/shared/auth.js.map +1 -1
- package/dist/shared/firebase.d.ts +60 -0
- package/dist/shared/firebase.d.ts.map +1 -0
- package/dist/shared/firebase.js +102 -0
- package/dist/shared/firebase.js.map +1 -0
- package/dist/shared/fs.d.ts.map +1 -1
- package/dist/shared/fs.js +4 -0
- package/dist/shared/fs.js.map +1 -1
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +3 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/slug.d.ts +11 -0
- package/dist/shared/slug.d.ts.map +1 -0
- package/{src/shared/slug.ts → dist/shared/slug.js} +10 -11
- package/dist/shared/slug.js.map +1 -0
- package/dist/shared/url.d.ts +43 -0
- package/dist/shared/url.d.ts.map +1 -0
- package/{src/shared/url.ts → dist/shared/url.js} +12 -15
- package/dist/shared/url.js.map +1 -0
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.d.ts.map +1 -1
- package/dist/state/database.js +3 -0
- package/dist/state/database.js.map +1 -1
- package/dist/storage/fileSystem.d.ts +17 -17
- package/dist/storage/fileSystem.d.ts.map +1 -1
- package/dist/storage/fileSystem.js +39 -31
- package/dist/storage/fileSystem.js.map +1 -1
- package/package.json +5 -2
- package/.github/workflows/ci.yml +0 -50
- package/.husky/commit-msg +0 -2
- package/.husky/pre-commit +0 -1
- package/.husky/pre-push +0 -3
- package/.prettierrc +0 -8
- package/.release-it.json +0 -23
- package/ARCHITECTURE.md +0 -233
- package/CHANGELOG.md +0 -78
- package/commitlint.config.js +0 -4
- package/dist/ai/openRouter.d.ts +0 -47
- package/dist/ai/openRouter.d.ts.map +0 -1
- package/dist/ai/openRouter.js +0 -116
- package/dist/ai/openRouter.js.map +0 -1
- package/dist/ai/transcriptPolisher.d.ts +0 -24
- package/dist/ai/transcriptPolisher.d.ts.map +0 -1
- package/dist/ai/transcriptPolisher.js +0 -89
- package/dist/ai/transcriptPolisher.js.map +0 -1
- package/dist/cli/commands/enrich.d.ts +0 -14
- package/dist/cli/commands/enrich.d.ts.map +0 -1
- package/dist/cli/commands/enrich.js +0 -271
- package/dist/cli/commands/enrich.js.map +0 -1
- package/dist/cli/commands/syncGhl.d.ts +0 -20
- package/dist/cli/commands/syncGhl.d.ts.map +0 -1
- package/dist/cli/commands/syncGhl.js +0 -483
- package/dist/cli/commands/syncGhl.js.map +0 -1
- package/dist/cli/commands/syncHighLevel.test.d.ts +0 -2
- package/dist/cli/commands/syncHighLevel.test.d.ts.map +0 -1
- package/dist/cli/commands/syncHighLevel.test.js +0 -102
- package/dist/cli/commands/syncHighLevel.test.js.map +0 -1
- package/dist/config/paths.test.d.ts +0 -2
- package/dist/config/paths.test.d.ts.map +0 -1
- package/dist/config/paths.test.js +0 -70
- package/dist/config/paths.test.js.map +0 -1
- package/dist/config/schema.test.d.ts +0 -2
- package/dist/config/schema.test.d.ts.map +0 -1
- package/dist/config/schema.test.js +0 -151
- package/dist/config/schema.test.js.map +0 -1
- package/dist/downloader/hlsDownloader.test.d.ts +0 -2
- package/dist/downloader/hlsDownloader.test.d.ts.map +0 -1
- package/dist/downloader/hlsDownloader.test.js +0 -116
- package/dist/downloader/hlsDownloader.test.js.map +0 -1
- package/dist/downloader/loomDownloader.test.d.ts +0 -2
- package/dist/downloader/loomDownloader.test.d.ts.map +0 -1
- package/dist/downloader/loomDownloader.test.js +0 -36
- package/dist/downloader/loomDownloader.test.js.map +0 -1
- package/dist/downloader/queue.test.d.ts +0 -2
- package/dist/downloader/queue.test.d.ts.map +0 -1
- package/dist/downloader/queue.test.js +0 -158
- package/dist/downloader/queue.test.js.map +0 -1
- package/dist/downloader/videoDownloader.d.ts +0 -32
- package/dist/downloader/videoDownloader.d.ts.map +0 -1
- package/dist/downloader/videoDownloader.js +0 -173
- package/dist/downloader/videoDownloader.js.map +0 -1
- package/dist/downloader/vimeoDownloader.test.d.ts +0 -2
- package/dist/downloader/vimeoDownloader.test.d.ts.map +0 -1
- package/dist/downloader/vimeoDownloader.test.js +0 -51
- package/dist/downloader/vimeoDownloader.test.js.map +0 -1
- package/dist/scraper/auth.d.ts +0 -29
- package/dist/scraper/auth.d.ts.map +0 -1
- package/dist/scraper/auth.js +0 -115
- package/dist/scraper/auth.js.map +0 -1
- package/dist/scraper/extractor.test.d.ts +0 -2
- package/dist/scraper/extractor.test.d.ts.map +0 -1
- package/dist/scraper/extractor.test.js +0 -65
- package/dist/scraper/extractor.test.js.map +0 -1
- package/dist/scraper/ghl/auth.d.ts +0 -25
- package/dist/scraper/ghl/auth.d.ts.map +0 -1
- package/dist/scraper/ghl/auth.js +0 -187
- package/dist/scraper/ghl/auth.js.map +0 -1
- package/dist/scraper/ghl/extractor.d.ts +0 -96
- package/dist/scraper/ghl/extractor.d.ts.map +0 -1
- package/dist/scraper/ghl/extractor.js +0 -345
- package/dist/scraper/ghl/extractor.js.map +0 -1
- package/dist/scraper/ghl/index.d.ts +0 -4
- package/dist/scraper/ghl/index.d.ts.map +0 -1
- package/dist/scraper/ghl/index.js.map +0 -1
- package/dist/scraper/ghl/navigator.d.ts +0 -93
- package/dist/scraper/ghl/navigator.d.ts.map +0 -1
- package/dist/scraper/ghl/navigator.js +0 -447
- package/dist/scraper/ghl/navigator.js.map +0 -1
- package/dist/scraper/highlevel/auth.d.ts +0 -25
- package/dist/scraper/highlevel/auth.d.ts.map +0 -1
- package/dist/scraper/highlevel/auth.js +0 -189
- package/dist/scraper/highlevel/auth.js.map +0 -1
- package/dist/scraper/highlevel/extractor.test.d.ts +0 -2
- package/dist/scraper/highlevel/extractor.test.d.ts.map +0 -1
- package/dist/scraper/highlevel/extractor.test.js +0 -101
- package/dist/scraper/highlevel/extractor.test.js.map +0 -1
- package/dist/scraper/highlevel/navigator.test.d.ts +0 -2
- package/dist/scraper/highlevel/navigator.test.d.ts.map +0 -1
- package/dist/scraper/highlevel/navigator.test.js +0 -78
- package/dist/scraper/highlevel/navigator.test.js.map +0 -1
- package/dist/scraper/navigator.test.d.ts +0 -2
- package/dist/scraper/navigator.test.d.ts.map +0 -1
- package/dist/scraper/navigator.test.js +0 -63
- package/dist/scraper/navigator.test.js.map +0 -1
- package/dist/scraper/skoolApi.d.ts +0 -17
- package/dist/scraper/skoolApi.d.ts.map +0 -1
- package/dist/scraper/skoolApi.js +0 -72
- package/dist/scraper/skoolApi.js.map +0 -1
- package/dist/state/database.test.d.ts +0 -2
- package/dist/state/database.test.d.ts.map +0 -1
- package/dist/state/database.test.js +0 -34
- package/dist/state/database.test.js.map +0 -1
- package/dist/transcription/whisperService.d.ts +0 -27
- package/dist/transcription/whisperService.d.ts.map +0 -1
- package/dist/transcription/whisperService.js +0 -102
- package/dist/transcription/whisperService.js.map +0 -1
- package/eslint.config.js +0 -55
- package/src/__fixtures__/highlevel-post-response.json +0 -68
- package/src/__fixtures__/hls-master-playlist.m3u8 +0 -24
- package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +0 -38
- package/src/cli/commands/config.ts +0 -74
- package/src/cli/commands/inspect.ts +0 -441
- package/src/cli/commands/login.ts +0 -68
- package/src/cli/commands/status.ts +0 -147
- package/src/cli/commands/sync.ts +0 -1235
- package/src/cli/commands/syncHighLevel.test.ts +0 -144
- package/src/cli/commands/syncHighLevel.ts +0 -639
- package/src/cli/index.ts +0 -121
- package/src/config/configManager.ts +0 -75
- package/src/config/paths.test.ts +0 -83
- package/src/config/paths.ts +0 -36
- package/src/config/schema.test.ts +0 -173
- package/src/config/schema.ts +0 -65
- package/src/downloader/hlsDownloader.test.ts +0 -148
- package/src/downloader/hlsDownloader.ts +0 -327
- package/src/downloader/hlsValidator.ts +0 -196
- package/src/downloader/index.ts +0 -122
- package/src/downloader/loomDownloader.test.ts +0 -43
- package/src/downloader/loomDownloader.ts +0 -742
- package/src/downloader/queue.test.ts +0 -199
- package/src/downloader/queue.ts +0 -118
- package/src/downloader/vimeoDownloader.test.ts +0 -62
- package/src/downloader/vimeoDownloader.ts +0 -722
- package/src/scraper/extractor.test.ts +0 -124
- package/src/scraper/extractor.ts +0 -757
- package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +0 -41
- package/src/scraper/highlevel/extractor.test.ts +0 -134
- package/src/scraper/highlevel/extractor.ts +0 -537
- package/src/scraper/highlevel/index.ts +0 -2
- package/src/scraper/highlevel/navigator.test.ts +0 -110
- package/src/scraper/highlevel/navigator.ts +0 -668
- package/src/scraper/highlevel/schemas.ts +0 -183
- package/src/scraper/navigator.test.ts +0 -122
- package/src/scraper/navigator.ts +0 -355
- package/src/scraper/schemas.ts +0 -177
- package/src/scraper/videoInterceptor.ts +0 -435
- package/src/shared/auth.test.ts +0 -58
- package/src/shared/auth.ts +0 -251
- package/src/shared/firebase.ts +0 -151
- package/src/shared/fs.ts +0 -80
- package/src/shared/http.ts +0 -34
- package/src/shared/index.ts +0 -6
- package/src/shared/url.test.ts +0 -122
- package/src/state/database.test.ts +0 -49
- package/src/state/database.ts +0 -919
- package/src/state/index.ts +0 -14
- package/src/storage/fileSystem.test.ts +0 -64
- package/src/storage/fileSystem.ts +0 -175
- package/tsconfig.json +0 -28
- package/vitest.config.ts +0 -29
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { parseHLSPlaylist, parseHighLevelVideoUrl } from "./hlsDownloader.js";
|
|
3
|
-
|
|
4
|
-
describe("parseHLSPlaylist", () => {
|
|
5
|
-
const baseUrl = "https://cdn.example.com/video/";
|
|
6
|
-
|
|
7
|
-
it("parses master playlist with multiple qualities", () => {
|
|
8
|
-
const content = `#EXTM3U
|
|
9
|
-
#EXT-X-VERSION:3
|
|
10
|
-
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
|
|
11
|
-
360p.m3u8
|
|
12
|
-
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=854x480
|
|
13
|
-
480p.m3u8
|
|
14
|
-
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
|
|
15
|
-
720p.m3u8
|
|
16
|
-
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
|
|
17
|
-
1080p.m3u8`;
|
|
18
|
-
|
|
19
|
-
const result = parseHLSPlaylist(content, baseUrl);
|
|
20
|
-
|
|
21
|
-
expect(result).toHaveLength(4);
|
|
22
|
-
// Should be sorted by bandwidth (highest first)
|
|
23
|
-
expect(result[0]!.label).toBe("1080p");
|
|
24
|
-
expect(result[0]!.bandwidth).toBe(5000000);
|
|
25
|
-
expect(result[0]!.height).toBe(1080);
|
|
26
|
-
expect(result[0]!.width).toBe(1920);
|
|
27
|
-
expect(result[0]!.url).toBe("https://cdn.example.com/video/1080p.m3u8");
|
|
28
|
-
|
|
29
|
-
expect(result[3]!.label).toBe("360p");
|
|
30
|
-
expect(result[3]!.bandwidth).toBe(800000);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("handles absolute URLs in playlist", () => {
|
|
34
|
-
const content = `#EXTM3U
|
|
35
|
-
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
|
|
36
|
-
https://other-cdn.com/video/720p.m3u8`;
|
|
37
|
-
|
|
38
|
-
const result = parseHLSPlaylist(content, baseUrl);
|
|
39
|
-
|
|
40
|
-
expect(result).toHaveLength(1);
|
|
41
|
-
expect(result[0]!.url).toBe("https://other-cdn.com/video/720p.m3u8");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("handles playlist without resolution", () => {
|
|
45
|
-
const content = `#EXTM3U
|
|
46
|
-
#EXT-X-STREAM-INF:BANDWIDTH=500000
|
|
47
|
-
audio.m3u8`;
|
|
48
|
-
|
|
49
|
-
const result = parseHLSPlaylist(content, baseUrl);
|
|
50
|
-
|
|
51
|
-
expect(result).toHaveLength(1);
|
|
52
|
-
expect(result[0]!.label).toBe("500k");
|
|
53
|
-
expect(result[0]!.height).toBeUndefined();
|
|
54
|
-
expect(result[0]!.width).toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("handles empty playlist", () => {
|
|
58
|
-
const content = `#EXTM3U
|
|
59
|
-
#EXT-X-VERSION:3`;
|
|
60
|
-
|
|
61
|
-
const result = parseHLSPlaylist(content, baseUrl);
|
|
62
|
-
expect(result).toHaveLength(0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("ignores comments and metadata", () => {
|
|
66
|
-
const content = `#EXTM3U
|
|
67
|
-
#EXT-X-VERSION:3
|
|
68
|
-
# This is a comment
|
|
69
|
-
#EXT-X-INDEPENDENT-SEGMENTS
|
|
70
|
-
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=1280x720
|
|
71
|
-
720p.m3u8`;
|
|
72
|
-
|
|
73
|
-
const result = parseHLSPlaylist(content, baseUrl);
|
|
74
|
-
expect(result).toHaveLength(1);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("handles real-world Vimeo-style playlist", () => {
|
|
78
|
-
const content = `#EXTM3U
|
|
79
|
-
#EXT-X-VERSION:4
|
|
80
|
-
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=246064,BANDWIDTH=326400,CODECS="avc1.4D401E,mp4a.40.2",RESOLUTION=426x240,FRAME-RATE=24.000
|
|
81
|
-
https://vod.example.com/exp=123/~hmac=abc/240p/prog_index.m3u8
|
|
82
|
-
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=602416,BANDWIDTH=796800,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=24.000
|
|
83
|
-
https://vod.example.com/exp=123/~hmac=abc/360p/prog_index.m3u8
|
|
84
|
-
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1270416,BANDWIDTH=1680000,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=854x480,FRAME-RATE=24.000
|
|
85
|
-
https://vod.example.com/exp=123/~hmac=abc/480p/prog_index.m3u8`;
|
|
86
|
-
|
|
87
|
-
const result = parseHLSPlaylist(content, baseUrl);
|
|
88
|
-
|
|
89
|
-
expect(result).toHaveLength(3);
|
|
90
|
-
expect(result[0]!.height).toBe(480);
|
|
91
|
-
expect(result[1]!.height).toBe(360);
|
|
92
|
-
expect(result[2]!.height).toBe(240);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("parseHighLevelVideoUrl", () => {
|
|
97
|
-
it("parses standard HighLevel HLS URL", () => {
|
|
98
|
-
const url =
|
|
99
|
-
"https://backend.leadconnectorhq.com/hls/v2/memberships/ABC123/videos/video-id-456/master.m3u8";
|
|
100
|
-
|
|
101
|
-
const result = parseHighLevelVideoUrl(url);
|
|
102
|
-
|
|
103
|
-
expect(result).toEqual({
|
|
104
|
-
locationId: "ABC123",
|
|
105
|
-
videoId: "video-id-456",
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("parses URL with token", () => {
|
|
110
|
-
const url =
|
|
111
|
-
"https://backend.leadconnectorhq.com/hls/v2/memberships/LOC123/videos/VID456/master.m3u8?token=secret-token";
|
|
112
|
-
|
|
113
|
-
const result = parseHighLevelVideoUrl(url);
|
|
114
|
-
|
|
115
|
-
expect(result).toEqual({
|
|
116
|
-
locationId: "LOC123",
|
|
117
|
-
videoId: "VID456",
|
|
118
|
-
token: "secret-token",
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("handles complex video IDs", () => {
|
|
123
|
-
const url =
|
|
124
|
-
"https://cdn.example.com/hls/memberships/location-abc/videos/cts-184162b5f0747fcd,1080p/master.m3u8";
|
|
125
|
-
|
|
126
|
-
const result = parseHighLevelVideoUrl(url);
|
|
127
|
-
|
|
128
|
-
expect(result).toEqual({
|
|
129
|
-
locationId: "location-abc",
|
|
130
|
-
videoId: "cts-184162b5f0747fcd",
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("returns null for non-HighLevel URLs", () => {
|
|
135
|
-
expect(parseHighLevelVideoUrl("https://vimeo.com/123456")).toBeNull();
|
|
136
|
-
expect(parseHighLevelVideoUrl("https://youtube.com/watch?v=abc")).toBeNull();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("returns null for invalid URLs", () => {
|
|
140
|
-
expect(parseHighLevelVideoUrl("not-a-url")).toBeNull();
|
|
141
|
-
expect(parseHighLevelVideoUrl("")).toBeNull();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("returns null for missing video path", () => {
|
|
145
|
-
const url = "https://backend.leadconnectorhq.com/hls/v2/other/path";
|
|
146
|
-
expect(parseHighLevelVideoUrl(url)).toBeNull();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { execa } from "execa";
|
|
4
|
-
import * as HLS from "hls-parser";
|
|
5
|
-
import type { DownloadProgress } from "./loomDownloader.js";
|
|
6
|
-
|
|
7
|
-
export interface HLSDownloadResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
error?: string;
|
|
10
|
-
errorCode?: string;
|
|
11
|
-
outputPath?: string;
|
|
12
|
-
duration?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface HLSQuality {
|
|
16
|
-
label: string;
|
|
17
|
-
url: string;
|
|
18
|
-
bandwidth: number;
|
|
19
|
-
width?: number | undefined;
|
|
20
|
-
height?: number | undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Checks if ffmpeg is available on the system.
|
|
25
|
-
*/
|
|
26
|
-
/* v8 ignore next 8 */
|
|
27
|
-
export async function checkFfmpeg(): Promise<boolean> {
|
|
28
|
-
try {
|
|
29
|
-
await execa("ffmpeg", ["-version"]);
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Fetches an HLS master playlist and parses quality variants.
|
|
38
|
-
*/
|
|
39
|
-
/* v8 ignore next 14 */
|
|
40
|
-
export async function fetchHLSQualities(masterUrl: string): Promise<HLSQuality[]> {
|
|
41
|
-
try {
|
|
42
|
-
const response = await fetch(masterUrl);
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
throw new Error(`Failed to fetch playlist: ${response.status}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const content = await response.text();
|
|
48
|
-
return parseHLSPlaylist(content, masterUrl);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error("Failed to fetch HLS qualities:", error);
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Parses an HLS master playlist to extract quality variants.
|
|
57
|
-
* Uses hls-parser for robust parsing.
|
|
58
|
-
*/
|
|
59
|
-
export function parseHLSPlaylist(content: string, baseUrl: string): HLSQuality[] {
|
|
60
|
-
try {
|
|
61
|
-
const playlist = HLS.parse(content);
|
|
62
|
-
|
|
63
|
-
// Check if it's a master playlist with variants
|
|
64
|
-
if (!("variants" in playlist) || !playlist.variants) {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const variants: HLSQuality[] = playlist.variants.map((variant) => {
|
|
69
|
-
const bandwidth = variant.bandwidth ?? 0;
|
|
70
|
-
const resolution = variant.resolution;
|
|
71
|
-
const width = resolution?.width;
|
|
72
|
-
const height = resolution?.height;
|
|
73
|
-
|
|
74
|
-
// Build absolute URL
|
|
75
|
-
const variantUrl = variant.uri.startsWith("http")
|
|
76
|
-
? variant.uri
|
|
77
|
-
: new URL(variant.uri, baseUrl).href;
|
|
78
|
-
|
|
79
|
-
const label = height ? `${height}p` : `${Math.round(bandwidth / 1000)}k`;
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
label,
|
|
83
|
-
url: variantUrl,
|
|
84
|
-
bandwidth,
|
|
85
|
-
width,
|
|
86
|
-
height,
|
|
87
|
-
};
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Sort by bandwidth (highest first)
|
|
91
|
-
variants.sort((a, b) => b.bandwidth - a.bandwidth);
|
|
92
|
-
|
|
93
|
-
return variants;
|
|
94
|
-
} catch {
|
|
95
|
-
// Fallback to empty array on parse error
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Gets the best quality URL from a master playlist.
|
|
102
|
-
* @param masterUrl The master playlist URL
|
|
103
|
-
* @param preferredHeight Preferred video height (e.g., 720, 1080)
|
|
104
|
-
*/
|
|
105
|
-
/* v8 ignore start */
|
|
106
|
-
export async function getBestQualityUrl(
|
|
107
|
-
masterUrl: string,
|
|
108
|
-
preferredHeight?: number
|
|
109
|
-
): Promise<string> {
|
|
110
|
-
const qualities = await fetchHLSQualities(masterUrl);
|
|
111
|
-
|
|
112
|
-
if (qualities.length === 0) {
|
|
113
|
-
// Assume it's a direct media playlist
|
|
114
|
-
return masterUrl;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (preferredHeight) {
|
|
118
|
-
// Find closest match to preferred height
|
|
119
|
-
const match = qualities.find((q) => q.height === preferredHeight);
|
|
120
|
-
if (match) return match.url;
|
|
121
|
-
|
|
122
|
-
// Find closest lower quality
|
|
123
|
-
const lower = qualities.filter((q) => q.height && q.height <= preferredHeight);
|
|
124
|
-
const closest = lower[0];
|
|
125
|
-
if (closest) {
|
|
126
|
-
return closest.url;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Return highest quality
|
|
131
|
-
return qualities[0]?.url ?? masterUrl;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Downloads an HLS stream using ffmpeg.
|
|
136
|
-
* @param hlsUrl The HLS playlist URL (master or media)
|
|
137
|
-
* @param outputPath The output file path (should end in .mp4)
|
|
138
|
-
* @param onProgress Progress callback
|
|
139
|
-
*/
|
|
140
|
-
export async function downloadHLSVideo(
|
|
141
|
-
hlsUrl: string,
|
|
142
|
-
outputPath: string,
|
|
143
|
-
onProgress?: (progress: DownloadProgress) => void
|
|
144
|
-
): Promise<HLSDownloadResult> {
|
|
145
|
-
// Check if ffmpeg is available
|
|
146
|
-
const hasFfmpeg = await checkFfmpeg();
|
|
147
|
-
if (!hasFfmpeg) {
|
|
148
|
-
return {
|
|
149
|
-
success: false,
|
|
150
|
-
error: "ffmpeg is not installed. Please install ffmpeg to download HLS videos.",
|
|
151
|
-
errorCode: "FFMPEG_NOT_FOUND",
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Ensure output directory exists
|
|
156
|
-
const outputDir = path.dirname(outputPath);
|
|
157
|
-
if (!fs.existsSync(outputDir)) {
|
|
158
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Build ffmpeg command
|
|
162
|
-
const args = [
|
|
163
|
-
"-y", // Overwrite output
|
|
164
|
-
"-hide_banner",
|
|
165
|
-
"-loglevel",
|
|
166
|
-
"warning",
|
|
167
|
-
"-stats",
|
|
168
|
-
"-i",
|
|
169
|
-
hlsUrl,
|
|
170
|
-
"-c",
|
|
171
|
-
"copy", // Copy streams without re-encoding
|
|
172
|
-
"-bsf:a",
|
|
173
|
-
"aac_adtstoasc", // Fix AAC stream
|
|
174
|
-
outputPath,
|
|
175
|
-
];
|
|
176
|
-
|
|
177
|
-
let duration = 0;
|
|
178
|
-
let currentTime = 0;
|
|
179
|
-
let lastProgressUpdate = 0;
|
|
180
|
-
|
|
181
|
-
const updateProgress = () => {
|
|
182
|
-
if (duration > 0 && onProgress) {
|
|
183
|
-
const percent = Math.min((currentTime / duration) * 100, 100);
|
|
184
|
-
const now = Date.now();
|
|
185
|
-
|
|
186
|
-
// Throttle progress updates to avoid spam
|
|
187
|
-
if (now - lastProgressUpdate > 200 || percent >= 100) {
|
|
188
|
-
lastProgressUpdate = now;
|
|
189
|
-
onProgress({
|
|
190
|
-
phase: "downloading",
|
|
191
|
-
percent: Math.round(percent),
|
|
192
|
-
currentBytes: currentTime,
|
|
193
|
-
totalBytes: duration,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
const subprocess = execa("ffmpeg", args);
|
|
201
|
-
|
|
202
|
-
// Parse stderr for progress info
|
|
203
|
-
subprocess.stderr?.on("data", (data: Buffer) => {
|
|
204
|
-
const output = data.toString();
|
|
205
|
-
|
|
206
|
-
// Parse duration from input info
|
|
207
|
-
const durationMatch = /Duration:\s*(\d{2}):(\d{2}):(\d{2})\.(\d{2})/.exec(output);
|
|
208
|
-
if (durationMatch && duration === 0) {
|
|
209
|
-
const [, hours = "0", mins = "0", secs = "0", centis = "0"] = durationMatch;
|
|
210
|
-
duration =
|
|
211
|
-
parseInt(hours, 10) * 3600 +
|
|
212
|
-
parseInt(mins, 10) * 60 +
|
|
213
|
-
parseInt(secs, 10) +
|
|
214
|
-
parseInt(centis, 10) / 100;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Parse current time from progress
|
|
218
|
-
const timeMatch = /time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})/.exec(output);
|
|
219
|
-
if (timeMatch) {
|
|
220
|
-
const [, hours = "0", mins = "0", secs = "0", centis = "0"] = timeMatch;
|
|
221
|
-
currentTime =
|
|
222
|
-
parseInt(hours, 10) * 3600 +
|
|
223
|
-
parseInt(mins, 10) * 60 +
|
|
224
|
-
parseInt(secs, 10) +
|
|
225
|
-
parseInt(centis, 10) / 100;
|
|
226
|
-
|
|
227
|
-
updateProgress();
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
await subprocess;
|
|
232
|
-
|
|
233
|
-
// Final progress update
|
|
234
|
-
if (onProgress) {
|
|
235
|
-
onProgress({
|
|
236
|
-
phase: "complete",
|
|
237
|
-
percent: 100,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
success: true,
|
|
243
|
-
outputPath,
|
|
244
|
-
duration,
|
|
245
|
-
};
|
|
246
|
-
} catch (error) {
|
|
247
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
248
|
-
return {
|
|
249
|
-
success: false,
|
|
250
|
-
error: `ffmpeg error: ${errorMessage}`,
|
|
251
|
-
errorCode: "FFMPEG_ERROR",
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Downloads a HighLevel HLS video with quality selection.
|
|
258
|
-
* @param masterUrl The master playlist URL (may include token)
|
|
259
|
-
* @param outputPath The output file path
|
|
260
|
-
* @param preferredQuality Preferred quality label (e.g., "720p", "1080p")
|
|
261
|
-
* @param onProgress Progress callback
|
|
262
|
-
*/
|
|
263
|
-
export async function downloadHighLevelVideo(
|
|
264
|
-
masterUrl: string,
|
|
265
|
-
outputPath: string,
|
|
266
|
-
preferredQuality?: string,
|
|
267
|
-
onProgress?: (progress: DownloadProgress) => void
|
|
268
|
-
): Promise<HLSDownloadResult> {
|
|
269
|
-
// Report start
|
|
270
|
-
onProgress?.({
|
|
271
|
-
phase: "preparing",
|
|
272
|
-
percent: 0,
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Parse preferred height from quality string
|
|
276
|
-
let preferredHeight: number | undefined;
|
|
277
|
-
if (preferredQuality) {
|
|
278
|
-
const match = /(\d+)p?/i.exec(preferredQuality);
|
|
279
|
-
if (match?.[1]) {
|
|
280
|
-
preferredHeight = parseInt(match[1], 10);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Get the best quality URL
|
|
285
|
-
let downloadUrl = masterUrl;
|
|
286
|
-
try {
|
|
287
|
-
downloadUrl = await getBestQualityUrl(masterUrl, preferredHeight);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
console.warn("Failed to fetch quality options, using master URL:", error);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Download using ffmpeg
|
|
293
|
-
return downloadHLSVideo(downloadUrl, outputPath, onProgress);
|
|
294
|
-
}
|
|
295
|
-
/* v8 ignore stop */
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Extracts video info from a HighLevel HLS URL.
|
|
299
|
-
*/
|
|
300
|
-
export function parseHighLevelVideoUrl(url: string): {
|
|
301
|
-
locationId: string;
|
|
302
|
-
videoId: string;
|
|
303
|
-
token?: string | undefined;
|
|
304
|
-
} | null {
|
|
305
|
-
try {
|
|
306
|
-
const urlObj = new URL(url);
|
|
307
|
-
|
|
308
|
-
// Pattern: /hls/v2/memberships/{locationId}/videos/{videoId}/...
|
|
309
|
-
const match = /\/memberships\/([^/]+)\/videos\/([^/,]+)/.exec(urlObj.pathname);
|
|
310
|
-
const locationId = match?.[1];
|
|
311
|
-
const videoId = match?.[2];
|
|
312
|
-
|
|
313
|
-
if (!locationId || !videoId) {
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const token = urlObj.searchParams.get("token");
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
locationId,
|
|
321
|
-
videoId,
|
|
322
|
-
...(token ? { token } : {}),
|
|
323
|
-
};
|
|
324
|
-
} catch {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HLS stream validation - requires network access to verify streams.
|
|
3
|
-
* Excluded from coverage via vitest.config.ts.
|
|
4
|
-
*/
|
|
5
|
-
import { extractLoomId, getLoomVideoInfoDetailed } from "./loomDownloader.js";
|
|
6
|
-
import {
|
|
7
|
-
extractVimeoId,
|
|
8
|
-
getVimeoVideoInfo,
|
|
9
|
-
getVimeoVideoInfoFromBrowser,
|
|
10
|
-
} from "./vimeoDownloader.js";
|
|
11
|
-
import { captureLoomHls, captureVimeoConfig } from "../scraper/videoInterceptor.js";
|
|
12
|
-
import type { Page } from "playwright";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Result of HLS validation.
|
|
16
|
-
*/
|
|
17
|
-
export interface HlsValidationResult {
|
|
18
|
-
isValid: boolean;
|
|
19
|
-
hlsUrl: string | null;
|
|
20
|
-
error?: string;
|
|
21
|
-
errorCode?: string;
|
|
22
|
-
details?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Validates that a Loom video has an accessible HLS stream.
|
|
27
|
-
* This should be called during the scanning phase to catch issues early.
|
|
28
|
-
*
|
|
29
|
-
* @param loomUrl - The Loom video URL
|
|
30
|
-
* @param page - Optional Playwright page for network interception fallback
|
|
31
|
-
*/
|
|
32
|
-
export async function validateLoomHls(loomUrl: string, page?: Page): Promise<HlsValidationResult> {
|
|
33
|
-
const videoId = extractLoomId(loomUrl);
|
|
34
|
-
|
|
35
|
-
if (!videoId) {
|
|
36
|
-
return {
|
|
37
|
-
isValid: false,
|
|
38
|
-
hlsUrl: null,
|
|
39
|
-
error: "Invalid Loom URL - could not extract video ID",
|
|
40
|
-
errorCode: "INVALID_URL",
|
|
41
|
-
details: `URL: ${loomUrl}`,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// First try direct API
|
|
46
|
-
const result = await getLoomVideoInfoDetailed(videoId, 2, 500);
|
|
47
|
-
|
|
48
|
-
if (result.success && result.info) {
|
|
49
|
-
return {
|
|
50
|
-
isValid: true,
|
|
51
|
-
hlsUrl: result.info.hlsUrl,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// If direct API failed and we have a page, try network interception
|
|
56
|
-
if (page && result.errorCode === "HLS_NOT_FOUND") {
|
|
57
|
-
const captured = await captureLoomHls(page, videoId, 15000);
|
|
58
|
-
if (captured.hlsUrl) {
|
|
59
|
-
return {
|
|
60
|
-
isValid: true,
|
|
61
|
-
hlsUrl: captured.hlsUrl,
|
|
62
|
-
details: "Captured via network interception",
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Return the original error
|
|
68
|
-
const validation: HlsValidationResult = {
|
|
69
|
-
isValid: false,
|
|
70
|
-
hlsUrl: null,
|
|
71
|
-
error: result.error ?? "Failed to fetch Loom video info",
|
|
72
|
-
};
|
|
73
|
-
if (result.errorCode) {
|
|
74
|
-
validation.errorCode = result.errorCode;
|
|
75
|
-
}
|
|
76
|
-
if (result.details) {
|
|
77
|
-
validation.details = result.details;
|
|
78
|
-
}
|
|
79
|
-
return validation;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Validates a Vimeo video has accessible streams.
|
|
84
|
-
* @param vimeoUrl - The Vimeo video URL
|
|
85
|
-
* @param page - Optional Playwright page for domain-restricted videos
|
|
86
|
-
* @param lessonUrl - Optional lesson URL for referer-based access
|
|
87
|
-
*/
|
|
88
|
-
export async function validateVimeoVideo(
|
|
89
|
-
vimeoUrl: string,
|
|
90
|
-
page?: Page,
|
|
91
|
-
lessonUrl?: string
|
|
92
|
-
): Promise<HlsValidationResult> {
|
|
93
|
-
const videoId = extractVimeoId(vimeoUrl);
|
|
94
|
-
|
|
95
|
-
if (!videoId) {
|
|
96
|
-
return {
|
|
97
|
-
isValid: false,
|
|
98
|
-
hlsUrl: null,
|
|
99
|
-
error: "Invalid Vimeo URL - could not extract video ID",
|
|
100
|
-
errorCode: "INVALID_URL",
|
|
101
|
-
details: `URL: ${vimeoUrl}`,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Extract unlisted hash if present
|
|
106
|
-
const hashMatch =
|
|
107
|
-
/vimeo\.com\/\d+\/([a-f0-9]+)/.exec(vimeoUrl) ?? /[?&]h=([a-f0-9]+)/.exec(vimeoUrl);
|
|
108
|
-
const unlistedHash = hashMatch?.[1] ?? null;
|
|
109
|
-
|
|
110
|
-
// First try direct fetch (works for public videos)
|
|
111
|
-
let result = await getVimeoVideoInfo(videoId, unlistedHash, lessonUrl);
|
|
112
|
-
|
|
113
|
-
// If video is private/restricted and we have a browser context, try browser-based fetch
|
|
114
|
-
if (!result.success && result.errorCode === "PRIVATE_VIDEO" && page) {
|
|
115
|
-
result = await getVimeoVideoInfoFromBrowser(page, videoId, unlistedHash);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// If still failing and we have a page, try extracting from the running player
|
|
119
|
-
if (!result.success && result.errorCode === "PRIVATE_VIDEO" && page) {
|
|
120
|
-
const captured = await captureVimeoConfig(page, videoId, 20000);
|
|
121
|
-
if (captured.hlsUrl || captured.progressiveUrl) {
|
|
122
|
-
return {
|
|
123
|
-
isValid: true,
|
|
124
|
-
hlsUrl: captured.hlsUrl ?? captured.progressiveUrl,
|
|
125
|
-
details: "Extracted from running player",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (!result.success || !result.info) {
|
|
131
|
-
const validation: HlsValidationResult = {
|
|
132
|
-
isValid: false,
|
|
133
|
-
hlsUrl: null,
|
|
134
|
-
error: result.error ?? "Failed to fetch Vimeo video info",
|
|
135
|
-
};
|
|
136
|
-
if (result.errorCode) {
|
|
137
|
-
validation.errorCode = result.errorCode;
|
|
138
|
-
}
|
|
139
|
-
if (result.details) {
|
|
140
|
-
validation.details = result.details;
|
|
141
|
-
}
|
|
142
|
-
return validation;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Return HLS URL if available, or progressive URL as fallback
|
|
146
|
-
return {
|
|
147
|
-
isValid: true,
|
|
148
|
-
hlsUrl: result.info.hlsUrl ?? result.info.progressiveUrl,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Validates HLS availability for a video URL based on its type.
|
|
154
|
-
* @param videoUrl - The video URL to validate
|
|
155
|
-
* @param videoType - The type of video (loom, vimeo, etc.)
|
|
156
|
-
* @param page - Optional Playwright page for network interception fallback
|
|
157
|
-
* @param lessonUrl - Optional lesson URL for referer-based access
|
|
158
|
-
*/
|
|
159
|
-
export async function validateVideoHls(
|
|
160
|
-
videoUrl: string,
|
|
161
|
-
videoType: string,
|
|
162
|
-
page?: Page,
|
|
163
|
-
lessonUrl?: string
|
|
164
|
-
): Promise<HlsValidationResult> {
|
|
165
|
-
switch (videoType) {
|
|
166
|
-
case "loom":
|
|
167
|
-
return validateLoomHls(videoUrl, page);
|
|
168
|
-
|
|
169
|
-
case "vimeo":
|
|
170
|
-
return validateVimeoVideo(videoUrl, page, lessonUrl);
|
|
171
|
-
|
|
172
|
-
case "youtube":
|
|
173
|
-
case "wistia":
|
|
174
|
-
// These require yt-dlp - skip validation, will fail at download
|
|
175
|
-
return {
|
|
176
|
-
isValid: true,
|
|
177
|
-
hlsUrl: null,
|
|
178
|
-
details: `${videoType} requires yt-dlp - will attempt download`,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
case "native":
|
|
182
|
-
// Native videos have direct URLs, no HLS needed
|
|
183
|
-
return {
|
|
184
|
-
isValid: true,
|
|
185
|
-
hlsUrl: videoUrl,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
default:
|
|
189
|
-
return {
|
|
190
|
-
isValid: false,
|
|
191
|
-
hlsUrl: null,
|
|
192
|
-
error: `Unknown video type: ${videoType}`,
|
|
193
|
-
errorCode: "UNKNOWN_TYPE",
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
}
|