offcourse 1.0.0 → 1.0.1

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 (258) hide show
  1. package/dist/cli/commands/config.js.map +1 -1
  2. package/dist/cli/commands/inspect.js +1 -1
  3. package/dist/cli/commands/inspect.js.map +1 -1
  4. package/dist/cli/commands/sync.d.ts +1 -2
  5. package/dist/cli/commands/sync.d.ts.map +1 -1
  6. package/dist/cli/commands/sync.js +13 -14
  7. package/dist/cli/commands/sync.js.map +1 -1
  8. package/dist/cli/commands/syncHighLevel.d.ts +1 -2
  9. package/dist/cli/commands/syncHighLevel.d.ts.map +1 -1
  10. package/dist/cli/commands/syncHighLevel.js +4 -8
  11. package/dist/cli/commands/syncHighLevel.js.map +1 -1
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/config/configManager.d.ts.map +1 -1
  15. package/dist/config/configManager.js +4 -0
  16. package/dist/config/configManager.js.map +1 -1
  17. package/dist/downloader/hlsDownloader.d.ts.map +1 -1
  18. package/dist/downloader/hlsDownloader.js +23 -14
  19. package/dist/downloader/hlsDownloader.js.map +1 -1
  20. package/dist/downloader/hlsValidator.d.ts.map +1 -1
  21. package/dist/downloader/hlsValidator.js +6 -2
  22. package/dist/downloader/hlsValidator.js.map +1 -1
  23. package/dist/downloader/index.d.ts +3 -0
  24. package/dist/downloader/index.d.ts.map +1 -1
  25. package/dist/downloader/index.js +3 -0
  26. package/dist/downloader/index.js.map +1 -1
  27. package/dist/downloader/loomDownloader.d.ts.map +1 -1
  28. package/dist/downloader/loomDownloader.js +23 -20
  29. package/dist/downloader/loomDownloader.js.map +1 -1
  30. package/dist/downloader/queue.d.ts +4 -4
  31. package/dist/downloader/queue.d.ts.map +1 -1
  32. package/dist/downloader/queue.js.map +1 -1
  33. package/dist/downloader/vimeoDownloader.d.ts.map +1 -1
  34. package/dist/downloader/vimeoDownloader.js +7 -3
  35. package/dist/downloader/vimeoDownloader.js.map +1 -1
  36. package/dist/scraper/extractor.d.ts +4 -0
  37. package/dist/scraper/extractor.d.ts.map +1 -1
  38. package/dist/scraper/extractor.js +79 -79
  39. package/dist/scraper/extractor.js.map +1 -1
  40. package/dist/scraper/highlevel/extractor.d.ts +11 -19
  41. package/dist/scraper/highlevel/extractor.d.ts.map +1 -1
  42. package/dist/scraper/highlevel/extractor.js +72 -85
  43. package/dist/scraper/highlevel/extractor.js.map +1 -1
  44. package/dist/scraper/highlevel/navigator.d.ts +3 -10
  45. package/dist/scraper/highlevel/navigator.d.ts.map +1 -1
  46. package/dist/scraper/highlevel/navigator.js +140 -127
  47. package/dist/scraper/highlevel/navigator.js.map +1 -1
  48. package/dist/scraper/highlevel/schemas.d.ts +188 -0
  49. package/dist/scraper/highlevel/schemas.d.ts.map +1 -0
  50. package/dist/scraper/highlevel/schemas.js +139 -0
  51. package/dist/scraper/highlevel/schemas.js.map +1 -0
  52. package/dist/scraper/navigator.d.ts +14 -11
  53. package/dist/scraper/navigator.d.ts.map +1 -1
  54. package/dist/scraper/navigator.js +61 -104
  55. package/dist/scraper/navigator.js.map +1 -1
  56. package/dist/scraper/schemas.d.ts +57 -0
  57. package/dist/scraper/schemas.d.ts.map +1 -0
  58. package/dist/scraper/schemas.js +135 -0
  59. package/dist/scraper/schemas.js.map +1 -0
  60. package/dist/scraper/videoInterceptor.d.ts +4 -0
  61. package/dist/scraper/videoInterceptor.d.ts.map +1 -1
  62. package/dist/scraper/videoInterceptor.js +66 -51
  63. package/dist/scraper/videoInterceptor.js.map +1 -1
  64. package/dist/shared/auth.d.ts +9 -9
  65. package/dist/shared/auth.d.ts.map +1 -1
  66. package/dist/shared/auth.js +24 -38
  67. package/dist/shared/auth.js.map +1 -1
  68. package/dist/shared/firebase.d.ts +60 -0
  69. package/dist/shared/firebase.d.ts.map +1 -0
  70. package/dist/shared/firebase.js +102 -0
  71. package/dist/shared/firebase.js.map +1 -0
  72. package/dist/shared/fs.d.ts.map +1 -1
  73. package/dist/shared/fs.js +4 -0
  74. package/dist/shared/fs.js.map +1 -1
  75. package/dist/shared/index.d.ts +3 -0
  76. package/dist/shared/index.d.ts.map +1 -1
  77. package/dist/shared/index.js +3 -0
  78. package/dist/shared/index.js.map +1 -1
  79. package/dist/shared/slug.d.ts +11 -0
  80. package/dist/shared/slug.d.ts.map +1 -0
  81. package/{src/shared/slug.ts → dist/shared/slug.js} +10 -11
  82. package/dist/shared/slug.js.map +1 -0
  83. package/dist/shared/url.d.ts +43 -0
  84. package/dist/shared/url.d.ts.map +1 -0
  85. package/{src/shared/url.ts → dist/shared/url.js} +12 -15
  86. package/dist/shared/url.js.map +1 -0
  87. package/dist/state/database.d.ts +1 -0
  88. package/dist/state/database.d.ts.map +1 -1
  89. package/dist/state/database.js +3 -0
  90. package/dist/state/database.js.map +1 -1
  91. package/dist/storage/fileSystem.d.ts +17 -17
  92. package/dist/storage/fileSystem.d.ts.map +1 -1
  93. package/dist/storage/fileSystem.js +39 -31
  94. package/dist/storage/fileSystem.js.map +1 -1
  95. package/package.json +5 -2
  96. package/.github/workflows/ci.yml +0 -50
  97. package/.husky/commit-msg +0 -2
  98. package/.husky/pre-commit +0 -1
  99. package/.husky/pre-push +0 -3
  100. package/.prettierrc +0 -8
  101. package/.release-it.json +0 -23
  102. package/ARCHITECTURE.md +0 -233
  103. package/CHANGELOG.md +0 -78
  104. package/commitlint.config.js +0 -4
  105. package/dist/ai/openRouter.d.ts +0 -47
  106. package/dist/ai/openRouter.d.ts.map +0 -1
  107. package/dist/ai/openRouter.js +0 -116
  108. package/dist/ai/openRouter.js.map +0 -1
  109. package/dist/ai/transcriptPolisher.d.ts +0 -24
  110. package/dist/ai/transcriptPolisher.d.ts.map +0 -1
  111. package/dist/ai/transcriptPolisher.js +0 -89
  112. package/dist/ai/transcriptPolisher.js.map +0 -1
  113. package/dist/cli/commands/enrich.d.ts +0 -14
  114. package/dist/cli/commands/enrich.d.ts.map +0 -1
  115. package/dist/cli/commands/enrich.js +0 -271
  116. package/dist/cli/commands/enrich.js.map +0 -1
  117. package/dist/cli/commands/syncGhl.d.ts +0 -20
  118. package/dist/cli/commands/syncGhl.d.ts.map +0 -1
  119. package/dist/cli/commands/syncGhl.js +0 -483
  120. package/dist/cli/commands/syncGhl.js.map +0 -1
  121. package/dist/cli/commands/syncHighLevel.test.d.ts +0 -2
  122. package/dist/cli/commands/syncHighLevel.test.d.ts.map +0 -1
  123. package/dist/cli/commands/syncHighLevel.test.js +0 -102
  124. package/dist/cli/commands/syncHighLevel.test.js.map +0 -1
  125. package/dist/config/paths.test.d.ts +0 -2
  126. package/dist/config/paths.test.d.ts.map +0 -1
  127. package/dist/config/paths.test.js +0 -70
  128. package/dist/config/paths.test.js.map +0 -1
  129. package/dist/config/schema.test.d.ts +0 -2
  130. package/dist/config/schema.test.d.ts.map +0 -1
  131. package/dist/config/schema.test.js +0 -151
  132. package/dist/config/schema.test.js.map +0 -1
  133. package/dist/downloader/hlsDownloader.test.d.ts +0 -2
  134. package/dist/downloader/hlsDownloader.test.d.ts.map +0 -1
  135. package/dist/downloader/hlsDownloader.test.js +0 -116
  136. package/dist/downloader/hlsDownloader.test.js.map +0 -1
  137. package/dist/downloader/loomDownloader.test.d.ts +0 -2
  138. package/dist/downloader/loomDownloader.test.d.ts.map +0 -1
  139. package/dist/downloader/loomDownloader.test.js +0 -36
  140. package/dist/downloader/loomDownloader.test.js.map +0 -1
  141. package/dist/downloader/queue.test.d.ts +0 -2
  142. package/dist/downloader/queue.test.d.ts.map +0 -1
  143. package/dist/downloader/queue.test.js +0 -158
  144. package/dist/downloader/queue.test.js.map +0 -1
  145. package/dist/downloader/videoDownloader.d.ts +0 -32
  146. package/dist/downloader/videoDownloader.d.ts.map +0 -1
  147. package/dist/downloader/videoDownloader.js +0 -173
  148. package/dist/downloader/videoDownloader.js.map +0 -1
  149. package/dist/downloader/vimeoDownloader.test.d.ts +0 -2
  150. package/dist/downloader/vimeoDownloader.test.d.ts.map +0 -1
  151. package/dist/downloader/vimeoDownloader.test.js +0 -51
  152. package/dist/downloader/vimeoDownloader.test.js.map +0 -1
  153. package/dist/scraper/auth.d.ts +0 -29
  154. package/dist/scraper/auth.d.ts.map +0 -1
  155. package/dist/scraper/auth.js +0 -115
  156. package/dist/scraper/auth.js.map +0 -1
  157. package/dist/scraper/extractor.test.d.ts +0 -2
  158. package/dist/scraper/extractor.test.d.ts.map +0 -1
  159. package/dist/scraper/extractor.test.js +0 -65
  160. package/dist/scraper/extractor.test.js.map +0 -1
  161. package/dist/scraper/ghl/auth.d.ts +0 -25
  162. package/dist/scraper/ghl/auth.d.ts.map +0 -1
  163. package/dist/scraper/ghl/auth.js +0 -187
  164. package/dist/scraper/ghl/auth.js.map +0 -1
  165. package/dist/scraper/ghl/extractor.d.ts +0 -96
  166. package/dist/scraper/ghl/extractor.d.ts.map +0 -1
  167. package/dist/scraper/ghl/extractor.js +0 -345
  168. package/dist/scraper/ghl/extractor.js.map +0 -1
  169. package/dist/scraper/ghl/index.d.ts +0 -4
  170. package/dist/scraper/ghl/index.d.ts.map +0 -1
  171. package/dist/scraper/ghl/index.js +0 -4
  172. package/dist/scraper/ghl/index.js.map +0 -1
  173. package/dist/scraper/ghl/navigator.d.ts +0 -93
  174. package/dist/scraper/ghl/navigator.d.ts.map +0 -1
  175. package/dist/scraper/ghl/navigator.js +0 -447
  176. package/dist/scraper/ghl/navigator.js.map +0 -1
  177. package/dist/scraper/highlevel/auth.d.ts +0 -25
  178. package/dist/scraper/highlevel/auth.d.ts.map +0 -1
  179. package/dist/scraper/highlevel/auth.js +0 -189
  180. package/dist/scraper/highlevel/auth.js.map +0 -1
  181. package/dist/scraper/highlevel/extractor.test.d.ts +0 -2
  182. package/dist/scraper/highlevel/extractor.test.d.ts.map +0 -1
  183. package/dist/scraper/highlevel/extractor.test.js +0 -101
  184. package/dist/scraper/highlevel/extractor.test.js.map +0 -1
  185. package/dist/scraper/highlevel/navigator.test.d.ts +0 -2
  186. package/dist/scraper/highlevel/navigator.test.d.ts.map +0 -1
  187. package/dist/scraper/highlevel/navigator.test.js +0 -78
  188. package/dist/scraper/highlevel/navigator.test.js.map +0 -1
  189. package/dist/scraper/navigator.test.d.ts +0 -2
  190. package/dist/scraper/navigator.test.d.ts.map +0 -1
  191. package/dist/scraper/navigator.test.js +0 -63
  192. package/dist/scraper/navigator.test.js.map +0 -1
  193. package/dist/scraper/skoolApi.d.ts +0 -17
  194. package/dist/scraper/skoolApi.d.ts.map +0 -1
  195. package/dist/scraper/skoolApi.js +0 -72
  196. package/dist/scraper/skoolApi.js.map +0 -1
  197. package/dist/state/database.test.d.ts +0 -2
  198. package/dist/state/database.test.d.ts.map +0 -1
  199. package/dist/state/database.test.js +0 -34
  200. package/dist/state/database.test.js.map +0 -1
  201. package/dist/transcription/whisperService.d.ts +0 -27
  202. package/dist/transcription/whisperService.d.ts.map +0 -1
  203. package/dist/transcription/whisperService.js +0 -102
  204. package/dist/transcription/whisperService.js.map +0 -1
  205. package/eslint.config.js +0 -55
  206. package/src/__fixtures__/highlevel-post-response.json +0 -68
  207. package/src/__fixtures__/hls-master-playlist.m3u8 +0 -24
  208. package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +0 -38
  209. package/src/cli/commands/config.ts +0 -74
  210. package/src/cli/commands/inspect.ts +0 -441
  211. package/src/cli/commands/login.ts +0 -68
  212. package/src/cli/commands/status.ts +0 -147
  213. package/src/cli/commands/sync.ts +0 -1235
  214. package/src/cli/commands/syncHighLevel.test.ts +0 -144
  215. package/src/cli/commands/syncHighLevel.ts +0 -639
  216. package/src/cli/index.ts +0 -121
  217. package/src/config/configManager.ts +0 -75
  218. package/src/config/paths.test.ts +0 -83
  219. package/src/config/paths.ts +0 -36
  220. package/src/config/schema.test.ts +0 -173
  221. package/src/config/schema.ts +0 -65
  222. package/src/downloader/hlsDownloader.test.ts +0 -148
  223. package/src/downloader/hlsDownloader.ts +0 -327
  224. package/src/downloader/hlsValidator.ts +0 -196
  225. package/src/downloader/index.ts +0 -122
  226. package/src/downloader/loomDownloader.test.ts +0 -43
  227. package/src/downloader/loomDownloader.ts +0 -742
  228. package/src/downloader/queue.test.ts +0 -199
  229. package/src/downloader/queue.ts +0 -118
  230. package/src/downloader/vimeoDownloader.test.ts +0 -62
  231. package/src/downloader/vimeoDownloader.ts +0 -722
  232. package/src/scraper/extractor.test.ts +0 -124
  233. package/src/scraper/extractor.ts +0 -757
  234. package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +0 -41
  235. package/src/scraper/highlevel/extractor.test.ts +0 -134
  236. package/src/scraper/highlevel/extractor.ts +0 -537
  237. package/src/scraper/highlevel/index.ts +0 -2
  238. package/src/scraper/highlevel/navigator.test.ts +0 -110
  239. package/src/scraper/highlevel/navigator.ts +0 -668
  240. package/src/scraper/highlevel/schemas.ts +0 -183
  241. package/src/scraper/navigator.test.ts +0 -122
  242. package/src/scraper/navigator.ts +0 -355
  243. package/src/scraper/schemas.ts +0 -177
  244. package/src/scraper/videoInterceptor.ts +0 -435
  245. package/src/shared/auth.test.ts +0 -58
  246. package/src/shared/auth.ts +0 -251
  247. package/src/shared/firebase.ts +0 -151
  248. package/src/shared/fs.ts +0 -80
  249. package/src/shared/http.ts +0 -34
  250. package/src/shared/index.ts +0 -6
  251. package/src/shared/url.test.ts +0 -122
  252. package/src/state/database.test.ts +0 -49
  253. package/src/state/database.ts +0 -919
  254. package/src/state/index.ts +0 -14
  255. package/src/storage/fileSystem.test.ts +0 -64
  256. package/src/storage/fileSystem.ts +0 -175
  257. package/tsconfig.json +0 -28
  258. 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 */
@@ -1,2 +0,0 @@
1
- export * from "./navigator.js";
2
- export * from "./extractor.js";