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