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,41 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`parseHLSMasterPlaylist > with fixture file > snapshot: parsed playlist structure 1`] = `
|
|
4
|
-
[
|
|
5
|
-
{
|
|
6
|
-
"bandwidth": 6000000,
|
|
7
|
-
"height": 1080,
|
|
8
|
-
"label": "1080p",
|
|
9
|
-
"url": "https://cdn.example.com/video/1080p/index.m3u8",
|
|
10
|
-
"width": 1920,
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"bandwidth": 3200000,
|
|
14
|
-
"height": 720,
|
|
15
|
-
"label": "720p",
|
|
16
|
-
"url": "https://cdn.example.com/video/720p/index.m3u8",
|
|
17
|
-
"width": 1280,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"bandwidth": 1680000,
|
|
21
|
-
"height": 480,
|
|
22
|
-
"label": "480p",
|
|
23
|
-
"url": "https://cdn.example.com/video/480p/index.m3u8",
|
|
24
|
-
"width": 854,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"bandwidth": 796800,
|
|
28
|
-
"height": 360,
|
|
29
|
-
"label": "360p",
|
|
30
|
-
"url": "https://cdn.example.com/video/360p/index.m3u8",
|
|
31
|
-
"width": 640,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"bandwidth": 326400,
|
|
35
|
-
"height": 240,
|
|
36
|
-
"label": "240p",
|
|
37
|
-
"url": "https://cdn.example.com/video/240p/index.m3u8",
|
|
38
|
-
"width": 426,
|
|
39
|
-
},
|
|
40
|
-
]
|
|
41
|
-
`;
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { parseHLSMasterPlaylist } from "./extractor.js";
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const fixturesDir = join(__dirname, "../../__fixtures__");
|
|
9
|
-
|
|
10
|
-
describe("parseHLSMasterPlaylist", () => {
|
|
11
|
-
const baseUrl = "https://cdn.example.com/video/";
|
|
12
|
-
|
|
13
|
-
it("parses master playlist with multiple qualities", () => {
|
|
14
|
-
const content = `#EXTM3U
|
|
15
|
-
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
|
|
16
|
-
360p.m3u8
|
|
17
|
-
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
|
|
18
|
-
720p.m3u8
|
|
19
|
-
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
|
|
20
|
-
1080p.m3u8`;
|
|
21
|
-
|
|
22
|
-
const result = parseHLSMasterPlaylist(content, baseUrl);
|
|
23
|
-
|
|
24
|
-
expect(result).toHaveLength(3);
|
|
25
|
-
// Should be sorted by bandwidth (highest first)
|
|
26
|
-
expect(result[0]!.label).toBe("1080p");
|
|
27
|
-
expect(result[0]!.bandwidth).toBe(5000000);
|
|
28
|
-
expect(result[0]!.height).toBe(1080);
|
|
29
|
-
expect(result[0]!.width).toBe(1920);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("resolves relative URLs correctly", () => {
|
|
33
|
-
const content = `#EXTM3U
|
|
34
|
-
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
|
|
35
|
-
720p/index.m3u8`;
|
|
36
|
-
|
|
37
|
-
const result = parseHLSMasterPlaylist(content, baseUrl);
|
|
38
|
-
|
|
39
|
-
expect(result[0]!.url).toBe("https://cdn.example.com/video/720p/index.m3u8");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("preserves absolute URLs", () => {
|
|
43
|
-
const content = `#EXTM3U
|
|
44
|
-
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
|
|
45
|
-
https://other-cdn.com/720p.m3u8`;
|
|
46
|
-
|
|
47
|
-
const result = parseHLSMasterPlaylist(content, baseUrl);
|
|
48
|
-
|
|
49
|
-
expect(result[0]!.url).toBe("https://other-cdn.com/720p.m3u8");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("handles playlist without resolution (audio only)", () => {
|
|
53
|
-
const content = `#EXTM3U
|
|
54
|
-
#EXT-X-STREAM-INF:BANDWIDTH=128000
|
|
55
|
-
audio.m3u8`;
|
|
56
|
-
|
|
57
|
-
const result = parseHLSMasterPlaylist(content, baseUrl);
|
|
58
|
-
|
|
59
|
-
expect(result).toHaveLength(1);
|
|
60
|
-
expect(result[0]!.label).toBe("128k");
|
|
61
|
-
expect(result[0]!.height).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("returns empty array for empty playlist", () => {
|
|
65
|
-
const content = `#EXTM3U
|
|
66
|
-
#EXT-X-VERSION:3`;
|
|
67
|
-
|
|
68
|
-
const result = parseHLSMasterPlaylist(content, baseUrl);
|
|
69
|
-
expect(result).toHaveLength(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("with fixture file", () => {
|
|
73
|
-
it("parses real-world HLS master playlist", () => {
|
|
74
|
-
const content = readFileSync(join(fixturesDir, "hls-master-playlist.m3u8"), "utf-8");
|
|
75
|
-
|
|
76
|
-
const result = parseHLSMasterPlaylist(content, "https://cdn.example.com/video/");
|
|
77
|
-
|
|
78
|
-
expect(result).toHaveLength(5);
|
|
79
|
-
|
|
80
|
-
// Sorted by bandwidth (highest first)
|
|
81
|
-
expect(result[0]!.label).toBe("1080p");
|
|
82
|
-
expect(result[0]!.height).toBe(1080);
|
|
83
|
-
expect(result[0]!.bandwidth).toBe(6000000);
|
|
84
|
-
|
|
85
|
-
expect(result[4]!.label).toBe("240p");
|
|
86
|
-
expect(result[4]!.height).toBe(240);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("snapshot: parsed playlist structure", () => {
|
|
90
|
-
const content = readFileSync(join(fixturesDir, "hls-master-playlist.m3u8"), "utf-8");
|
|
91
|
-
const result = parseHLSMasterPlaylist(content, "https://cdn.example.com/video/");
|
|
92
|
-
|
|
93
|
-
expect(result).toMatchSnapshot();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("HighLevel API Response parsing", () => {
|
|
99
|
-
it("snapshot: fixture structure", () => {
|
|
100
|
-
const response = JSON.parse(
|
|
101
|
-
readFileSync(join(fixturesDir, "highlevel-post-response.json"), "utf-8")
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Verify the structure we depend on
|
|
105
|
-
expect(response).toHaveProperty("id");
|
|
106
|
-
expect(response).toHaveProperty("title");
|
|
107
|
-
expect(response).toHaveProperty("video");
|
|
108
|
-
expect(response).toHaveProperty("post_materials");
|
|
109
|
-
|
|
110
|
-
// Video structure
|
|
111
|
-
expect(response.video).toHaveProperty("url");
|
|
112
|
-
expect(response.video).toHaveProperty("assetsLicenseId");
|
|
113
|
-
expect(response.video.url).toMatch(/\.mp4$/);
|
|
114
|
-
|
|
115
|
-
// Materials structure
|
|
116
|
-
expect(response.post_materials).toHaveLength(2);
|
|
117
|
-
expect(response.post_materials[0]).toHaveProperty("name");
|
|
118
|
-
expect(response.post_materials[0]).toHaveProperty("url");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("extracts video info from response", () => {
|
|
122
|
-
const response = JSON.parse(
|
|
123
|
-
readFileSync(join(fixturesDir, "highlevel-post-response.json"), "utf-8")
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// Simulate the extraction logic
|
|
127
|
-
const video = response.video;
|
|
128
|
-
const assetId = video?.assetsLicenseId ?? video?.assetId ?? video?.id;
|
|
129
|
-
const directUrl = video?.url;
|
|
130
|
-
|
|
131
|
-
expect(assetId).toBe("689a0d672ea246086539f453");
|
|
132
|
-
expect(directUrl).toContain("1080p.mp4");
|
|
133
|
-
});
|
|
134
|
-
});
|
|
@@ -1,537 +0,0 @@
|
|
|
1
|
-
import type { Page } from "playwright";
|
|
2
|
-
import { parseHLSPlaylist } from "../../downloader/hlsDownloader.js";
|
|
3
|
-
import {
|
|
4
|
-
FirebaseAuthTokenSchema,
|
|
5
|
-
PostDetailsResponseSchema,
|
|
6
|
-
VideoLicenseResponseSchema,
|
|
7
|
-
safeParse,
|
|
8
|
-
type FirebaseAuthRaw,
|
|
9
|
-
} from "./schemas.js";
|
|
10
|
-
|
|
11
|
-
// Alias for backwards compatibility and internal use
|
|
12
|
-
const parseHLSMasterPlaylist = parseHLSPlaylist;
|
|
13
|
-
|
|
14
|
-
export interface HighLevelVideoInfo {
|
|
15
|
-
type: "hls" | "vimeo" | "loom" | "youtube" | "custom";
|
|
16
|
-
url: string;
|
|
17
|
-
masterPlaylistUrl?: string;
|
|
18
|
-
qualities?: {
|
|
19
|
-
label: string;
|
|
20
|
-
url: string;
|
|
21
|
-
width?: number;
|
|
22
|
-
height?: number;
|
|
23
|
-
}[];
|
|
24
|
-
duration?: number;
|
|
25
|
-
thumbnailUrl?: string;
|
|
26
|
-
token?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface HighLevelPostContent {
|
|
30
|
-
id: string;
|
|
31
|
-
title: string;
|
|
32
|
-
description: string | null;
|
|
33
|
-
htmlContent: string | null;
|
|
34
|
-
video: HighLevelVideoInfo | null;
|
|
35
|
-
attachments: {
|
|
36
|
-
id: string;
|
|
37
|
-
name: string;
|
|
38
|
-
url: string;
|
|
39
|
-
type: string;
|
|
40
|
-
size?: number;
|
|
41
|
-
}[];
|
|
42
|
-
categoryId: string;
|
|
43
|
-
productId: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Browser/API automation - requires Playwright
|
|
47
|
-
/* v8 ignore start */
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Extracts the Firebase auth token from the page.
|
|
51
|
-
*/
|
|
52
|
-
export async function getAuthToken(page: Page): Promise<string | null> {
|
|
53
|
-
const rawData = await page.evaluate((): FirebaseAuthRaw | null => {
|
|
54
|
-
const tokenKey = Object.keys(localStorage).find((k) => k.includes("firebase:authUser"));
|
|
55
|
-
if (!tokenKey) return null;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
return JSON.parse(localStorage.getItem(tokenKey) ?? "{}") as FirebaseAuthRaw;
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (!rawData) return null;
|
|
65
|
-
|
|
66
|
-
const parsed = safeParse(FirebaseAuthTokenSchema, rawData, "getAuthToken");
|
|
67
|
-
return parsed?.stsTokenManager.accessToken ?? null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Extracts video info from a HighLevel post page by intercepting network requests.
|
|
72
|
-
*/
|
|
73
|
-
export async function extractVideoFromPage(page: Page): Promise<HighLevelVideoInfo | null> {
|
|
74
|
-
// First, check if there's an HLS video on the page
|
|
75
|
-
const hlsUrl = await page.evaluate(() => {
|
|
76
|
-
// Look for HLS master playlist URLs in the DOM
|
|
77
|
-
const videoElements = Array.from(document.querySelectorAll("video"));
|
|
78
|
-
for (const video of videoElements) {
|
|
79
|
-
const src = video.currentSrc ?? video.src;
|
|
80
|
-
if (src?.includes(".m3u8")) {
|
|
81
|
-
return src;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Check for plyr or other players
|
|
86
|
-
const sources = Array.from(
|
|
87
|
-
document.querySelectorAll('source[type*="m3u8"], source[src*=".m3u8"]')
|
|
88
|
-
);
|
|
89
|
-
for (const source of sources) {
|
|
90
|
-
const src = (source as HTMLSourceElement).src;
|
|
91
|
-
if (src) return src;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return null;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
if (hlsUrl) {
|
|
98
|
-
return {
|
|
99
|
-
type: "hls",
|
|
100
|
-
url: hlsUrl,
|
|
101
|
-
masterPlaylistUrl: hlsUrl,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check for Vimeo embed
|
|
106
|
-
const vimeoUrl = await page.evaluate(() => {
|
|
107
|
-
const iframe = document.querySelector('iframe[src*="vimeo.com"], iframe[src*="player.vimeo"]');
|
|
108
|
-
if (iframe) {
|
|
109
|
-
return (iframe as HTMLIFrameElement).src;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (vimeoUrl) {
|
|
115
|
-
return {
|
|
116
|
-
type: "vimeo",
|
|
117
|
-
url: vimeoUrl,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Check for Loom embed
|
|
122
|
-
const loomUrl = await page.evaluate(() => {
|
|
123
|
-
const iframe = document.querySelector('iframe[src*="loom.com"]');
|
|
124
|
-
if (iframe) {
|
|
125
|
-
return (iframe as HTMLIFrameElement).src;
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (loomUrl) {
|
|
131
|
-
return {
|
|
132
|
-
type: "loom",
|
|
133
|
-
url: loomUrl,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check for YouTube embed
|
|
138
|
-
const youtubeUrl = await page.evaluate(() => {
|
|
139
|
-
const iframe = document.querySelector(
|
|
140
|
-
'iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"], iframe[src*="youtu.be"]'
|
|
141
|
-
);
|
|
142
|
-
if (iframe) {
|
|
143
|
-
return (iframe as HTMLIFrameElement).src;
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
if (youtubeUrl) {
|
|
149
|
-
return {
|
|
150
|
-
type: "youtube",
|
|
151
|
-
url: youtubeUrl,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Extracts video info by intercepting network requests during page load.
|
|
160
|
-
*/
|
|
161
|
-
export async function interceptVideoRequests(
|
|
162
|
-
page: Page,
|
|
163
|
-
postUrl: string
|
|
164
|
-
): Promise<HighLevelVideoInfo | null> {
|
|
165
|
-
const hlsUrls: string[] = [];
|
|
166
|
-
const drmUrls: string[] = [];
|
|
167
|
-
|
|
168
|
-
// Set up request interception
|
|
169
|
-
const requestHandler = (request: { url: () => string }) => {
|
|
170
|
-
const url = request.url();
|
|
171
|
-
|
|
172
|
-
// Capture HLS master playlist requests
|
|
173
|
-
if (url.includes(".m3u8") || url.includes("master.m3u8")) {
|
|
174
|
-
hlsUrls.push(url);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Capture DRM license requests
|
|
178
|
-
if (url.includes("assets-drm/assets-license")) {
|
|
179
|
-
drmUrls.push(url);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
page.on("request", requestHandler);
|
|
184
|
-
|
|
185
|
-
// Navigate to the post page
|
|
186
|
-
await page.goto(postUrl, { timeout: 30000 });
|
|
187
|
-
await page.waitForLoadState("domcontentloaded");
|
|
188
|
-
await page.waitForTimeout(3000);
|
|
189
|
-
|
|
190
|
-
// Remove the handler
|
|
191
|
-
page.off("request", requestHandler);
|
|
192
|
-
|
|
193
|
-
// Get the HLS master playlist URL
|
|
194
|
-
const masterPlaylistUrl = hlsUrls.find((url) => url.includes("master.m3u8"));
|
|
195
|
-
|
|
196
|
-
if (masterPlaylistUrl) {
|
|
197
|
-
return {
|
|
198
|
-
type: "hls",
|
|
199
|
-
url: masterPlaylistUrl,
|
|
200
|
-
masterPlaylistUrl,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Fallback to DOM extraction
|
|
205
|
-
return extractVideoFromPage(page);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Fetches post details from the API.
|
|
210
|
-
*/
|
|
211
|
-
export async function fetchPostDetails(
|
|
212
|
-
page: Page,
|
|
213
|
-
locationId: string,
|
|
214
|
-
postId: string
|
|
215
|
-
): Promise<{
|
|
216
|
-
title: string;
|
|
217
|
-
description: string | null;
|
|
218
|
-
video: {
|
|
219
|
-
assetId: string;
|
|
220
|
-
url: string;
|
|
221
|
-
} | null;
|
|
222
|
-
materials: {
|
|
223
|
-
id: string;
|
|
224
|
-
name: string;
|
|
225
|
-
url: string;
|
|
226
|
-
type: string;
|
|
227
|
-
}[];
|
|
228
|
-
} | null> {
|
|
229
|
-
// Fetch raw data from browser context
|
|
230
|
-
type FetchResult = { error: string; status?: number } | { data: unknown };
|
|
231
|
-
|
|
232
|
-
const rawData = await page.evaluate(
|
|
233
|
-
async ({ locationId, postId }): Promise<FetchResult> => {
|
|
234
|
-
try {
|
|
235
|
-
const tokenKey = Object.keys(localStorage).find((k) => k.includes("firebase:authUser"));
|
|
236
|
-
const tokenData = tokenKey
|
|
237
|
-
? (JSON.parse(localStorage.getItem(tokenKey) ?? "{}") as FirebaseAuthRaw)
|
|
238
|
-
: null;
|
|
239
|
-
const token = tokenData?.stsTokenManager?.accessToken;
|
|
240
|
-
|
|
241
|
-
if (!token) {
|
|
242
|
-
return { error: "No auth token" };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const res = await fetch(
|
|
246
|
-
`https://services.leadconnectorhq.com/membership/locations/${locationId}/posts/${postId}?source=courses`,
|
|
247
|
-
{
|
|
248
|
-
headers: {
|
|
249
|
-
Authorization: `Bearer ${token}`,
|
|
250
|
-
},
|
|
251
|
-
}
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
if (!res.ok) {
|
|
255
|
-
return { error: `HTTP ${res.status}`, status: res.status };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const data: unknown = await res.json();
|
|
259
|
-
return { data };
|
|
260
|
-
} catch (error) {
|
|
261
|
-
return { error: String(error) };
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
{ locationId, postId }
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
// Debug: Log raw response in Node context
|
|
268
|
-
if ("error" in rawData) {
|
|
269
|
-
console.log(`[DEBUG] API Error: ${rawData.error}`);
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const data = rawData.data;
|
|
274
|
-
if (!data) {
|
|
275
|
-
console.log("[DEBUG] No data in response");
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Validate response with Zod schema
|
|
280
|
-
const parsed = safeParse(PostDetailsResponseSchema, data, "fetchPostDetails");
|
|
281
|
-
if (!parsed) {
|
|
282
|
-
console.log("[DEBUG] Response validation failed");
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// The API returns data directly (not nested under .post)
|
|
287
|
-
// Check both for backwards compatibility
|
|
288
|
-
const post = parsed.post ?? parsed;
|
|
289
|
-
|
|
290
|
-
let video: { assetId: string; url: string } | null = null;
|
|
291
|
-
|
|
292
|
-
// Check for video directly on post
|
|
293
|
-
// Video can have: id, assetId, assetsLicenseId, or direct url
|
|
294
|
-
if (post.video) {
|
|
295
|
-
const videoAssetId = post.video.assetsLicenseId ?? post.video.assetId ?? post.video.id;
|
|
296
|
-
if (videoAssetId || post.video.url) {
|
|
297
|
-
video = {
|
|
298
|
-
assetId: videoAssetId ?? "",
|
|
299
|
-
url: post.video.url ?? "",
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Check posterImage for video asset (older format)
|
|
305
|
-
if (!video && post.posterImage?.assetId) {
|
|
306
|
-
video = {
|
|
307
|
-
assetId: post.posterImage.assetId,
|
|
308
|
-
url: post.posterImage.url ?? "",
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check for video in contentBlock
|
|
313
|
-
if (!video && post.contentBlock) {
|
|
314
|
-
for (const block of post.contentBlock) {
|
|
315
|
-
if (block.type === "video") {
|
|
316
|
-
const blockAssetId = block.assetsLicenseId ?? block.assetId ?? block.id;
|
|
317
|
-
if (blockAssetId || block.url) {
|
|
318
|
-
video = {
|
|
319
|
-
assetId: blockAssetId ?? "",
|
|
320
|
-
url: block.url ?? "",
|
|
321
|
-
};
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const materials: {
|
|
329
|
-
id: string;
|
|
330
|
-
name: string;
|
|
331
|
-
url: string;
|
|
332
|
-
type: string;
|
|
333
|
-
}[] = [];
|
|
334
|
-
|
|
335
|
-
// Materials can be under 'materials' or 'post_materials'
|
|
336
|
-
const materialsList = post.materials ?? post.post_materials ?? [];
|
|
337
|
-
for (const material of materialsList) {
|
|
338
|
-
materials.push({
|
|
339
|
-
id: material.id ?? crypto.randomUUID(),
|
|
340
|
-
name: material.name ?? "Attachment",
|
|
341
|
-
url: material.url ?? "",
|
|
342
|
-
type: material.type ?? "file",
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
title: post.title ?? "",
|
|
348
|
-
description: post.description ?? null,
|
|
349
|
-
video,
|
|
350
|
-
materials,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Fetches the DRM license (HLS token) for a video asset.
|
|
356
|
-
*/
|
|
357
|
-
export async function fetchVideoLicense(
|
|
358
|
-
page: Page,
|
|
359
|
-
assetId: string
|
|
360
|
-
): Promise<{ url: string; token: string } | null> {
|
|
361
|
-
// Get auth token first
|
|
362
|
-
const authToken = await getAuthToken(page);
|
|
363
|
-
if (!authToken) {
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
// Use page.request to make the API call (bypasses CORS)
|
|
369
|
-
const response = await page.request.get(
|
|
370
|
-
`https://backend.leadconnectorhq.com/assets-drm/assets-license/${assetId}`,
|
|
371
|
-
{
|
|
372
|
-
headers: {
|
|
373
|
-
Authorization: `Bearer ${authToken}`,
|
|
374
|
-
},
|
|
375
|
-
}
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
if (!response.ok()) {
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const data: unknown = await response.json();
|
|
383
|
-
|
|
384
|
-
// Validate response with Zod schema
|
|
385
|
-
const parsed = safeParse(VideoLicenseResponseSchema, data, "fetchVideoLicense");
|
|
386
|
-
if (!parsed) {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
url: parsed.url,
|
|
392
|
-
token: parsed.token,
|
|
393
|
-
};
|
|
394
|
-
} catch (error) {
|
|
395
|
-
console.error("Failed to fetch video license:", error);
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Extracts complete post content including video and attachments.
|
|
402
|
-
*/
|
|
403
|
-
export async function extractHighLevelPostContent(
|
|
404
|
-
page: Page,
|
|
405
|
-
postUrl: string,
|
|
406
|
-
locationId: string,
|
|
407
|
-
productId: string,
|
|
408
|
-
postId: string,
|
|
409
|
-
categoryId: string
|
|
410
|
-
): Promise<HighLevelPostContent | null> {
|
|
411
|
-
// Navigate to post page
|
|
412
|
-
await page.goto(postUrl, { timeout: 30000 });
|
|
413
|
-
await page.waitForLoadState("domcontentloaded");
|
|
414
|
-
await page.waitForTimeout(3000);
|
|
415
|
-
|
|
416
|
-
// Fetch post details from API
|
|
417
|
-
const postDetails = await fetchPostDetails(page, locationId, postId);
|
|
418
|
-
|
|
419
|
-
if (!postDetails) {
|
|
420
|
-
console.error("Could not fetch post details");
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
let video: HighLevelVideoInfo | null = null;
|
|
425
|
-
|
|
426
|
-
// Check if we have video data
|
|
427
|
-
if (postDetails.video) {
|
|
428
|
-
// Option 1: Direct MP4 URL (preferred - no DRM)
|
|
429
|
-
if (postDetails.video.url?.endsWith(".mp4")) {
|
|
430
|
-
video = {
|
|
431
|
-
type: "custom", // Direct download, not HLS
|
|
432
|
-
url: postDetails.video.url,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
// Option 2: Get HLS license URL via assetId
|
|
436
|
-
else if (postDetails.video.assetId) {
|
|
437
|
-
const license = await fetchVideoLicense(page, postDetails.video.assetId);
|
|
438
|
-
|
|
439
|
-
if (license?.url) {
|
|
440
|
-
video = {
|
|
441
|
-
type: "hls",
|
|
442
|
-
url: license.url,
|
|
443
|
-
masterPlaylistUrl: license.url,
|
|
444
|
-
token: license.token,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Fallback: try to extract video from page DOM
|
|
451
|
-
video ??= await extractVideoFromPage(page);
|
|
452
|
-
|
|
453
|
-
// Extract HTML content
|
|
454
|
-
const htmlContent = await page.evaluate(() => {
|
|
455
|
-
const contentEl = document.querySelector(
|
|
456
|
-
"[class*='post-content'], [class*='PostContent'], [class*='lesson-content'], article"
|
|
457
|
-
);
|
|
458
|
-
return contentEl?.innerHTML ?? null;
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// Extract text description
|
|
462
|
-
const description = await page.evaluate(() => {
|
|
463
|
-
const descEl = document.querySelector(
|
|
464
|
-
"[class*='description'], [class*='Description'], p:first-of-type"
|
|
465
|
-
);
|
|
466
|
-
return descEl?.textContent?.trim() ?? null;
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
return {
|
|
470
|
-
id: postId,
|
|
471
|
-
title: postDetails.title,
|
|
472
|
-
description: description ?? postDetails.description,
|
|
473
|
-
htmlContent,
|
|
474
|
-
video,
|
|
475
|
-
attachments: postDetails.materials.map((m) => ({
|
|
476
|
-
id: m.id,
|
|
477
|
-
name: m.name,
|
|
478
|
-
url: m.url,
|
|
479
|
-
type: m.type,
|
|
480
|
-
})),
|
|
481
|
-
categoryId,
|
|
482
|
-
productId,
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
/* v8 ignore stop */
|
|
486
|
-
|
|
487
|
-
// Re-export for backwards compatibility
|
|
488
|
-
export { parseHLSMasterPlaylist };
|
|
489
|
-
|
|
490
|
-
/* v8 ignore start */
|
|
491
|
-
/**
|
|
492
|
-
* Fetches and parses HLS playlist to get quality options.
|
|
493
|
-
*/
|
|
494
|
-
export async function getHLSQualities(
|
|
495
|
-
page: Page,
|
|
496
|
-
masterPlaylistUrl: string
|
|
497
|
-
): Promise<
|
|
498
|
-
{
|
|
499
|
-
label: string;
|
|
500
|
-
url: string;
|
|
501
|
-
bandwidth: number;
|
|
502
|
-
width?: number | undefined;
|
|
503
|
-
height?: number | undefined;
|
|
504
|
-
}[]
|
|
505
|
-
> {
|
|
506
|
-
try {
|
|
507
|
-
const content = await page.evaluate(async (url) => {
|
|
508
|
-
const res = await fetch(url);
|
|
509
|
-
if (!res.ok) return null;
|
|
510
|
-
return res.text();
|
|
511
|
-
}, masterPlaylistUrl);
|
|
512
|
-
|
|
513
|
-
if (!content) return [];
|
|
514
|
-
|
|
515
|
-
return parseHLSMasterPlaylist(content, masterPlaylistUrl);
|
|
516
|
-
} catch {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Gets the best quality URL from an HLS master playlist.
|
|
523
|
-
*/
|
|
524
|
-
export async function getBestHLSQuality(
|
|
525
|
-
page: Page,
|
|
526
|
-
masterPlaylistUrl: string
|
|
527
|
-
): Promise<string | null> {
|
|
528
|
-
const qualities = await getHLSQualities(page, masterPlaylistUrl);
|
|
529
|
-
|
|
530
|
-
if (qualities.length === 0) {
|
|
531
|
-
return masterPlaylistUrl;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Return highest quality
|
|
535
|
-
return qualities[0]?.url ?? null;
|
|
536
|
-
}
|
|
537
|
-
/* v8 ignore stop */
|