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,177 +0,0 @@
1
- /**
2
- * Zod schemas for Skool/Next.js __NEXT_DATA__ responses.
3
- * These validate only the fields we actually use, ignoring everything else.
4
- */
5
-
6
- import { z } from "zod";
7
-
8
- // ============================================================================
9
- // Skool Course Child (Module/Lesson)
10
- // ============================================================================
11
-
12
- const CourseMetadataSchema = z.looseObject({
13
- title: z.string().optional(),
14
- videoLink: z.string().optional(),
15
- });
16
-
17
- const CourseInfoSchema = z.looseObject({
18
- id: z.string().optional(),
19
- name: z.string().optional(), // 8-char hex slug
20
- metadata: CourseMetadataSchema.optional(),
21
- });
22
-
23
- const CourseChildSchema = z.looseObject({
24
- course: CourseInfoSchema.optional(),
25
- hasAccess: z.boolean().optional(),
26
- });
27
-
28
- // ============================================================================
29
- // Skool __NEXT_DATA__ PageProps
30
- // ============================================================================
31
-
32
- const SkoolCourseSchema = z.looseObject({
33
- children: z.array(CourseChildSchema).optional(),
34
- });
35
-
36
- const SkoolPagePropsSchema = z.looseObject({
37
- course: SkoolCourseSchema.optional(),
38
- selectedModule: z.string().optional(),
39
- });
40
-
41
- export const SkoolNextDataSchema = z.looseObject({
42
- props: z
43
- .looseObject({
44
- pageProps: SkoolPagePropsSchema.optional(),
45
- })
46
- .optional(),
47
- });
48
-
49
- export type SkoolNextData = z.infer<typeof SkoolNextDataSchema>;
50
-
51
- // ============================================================================
52
- // Extracted Types (clean types for use in the app)
53
- // ============================================================================
54
-
55
- export interface SkoolModule {
56
- slug: string; // 8-char hex
57
- title: string;
58
- hasAccess: boolean;
59
- }
60
-
61
- export interface SkoolLesson {
62
- id: string;
63
- hasAccess: boolean;
64
- }
65
-
66
- export interface SkoolVideoInfo {
67
- url: string;
68
- type: "loom" | "vimeo" | "youtube" | "wistia" | "unknown";
69
- }
70
-
71
- // ============================================================================
72
- // Helper Functions
73
- // ============================================================================
74
-
75
- /**
76
- * Safely parses __NEXT_DATA__ JSON from a script element.
77
- * Returns null if parsing fails.
78
- */
79
- export function parseNextData(json: string): SkoolNextData | null {
80
- try {
81
- const data: unknown = JSON.parse(json);
82
- const result = SkoolNextDataSchema.safeParse(data);
83
- if (result.success) {
84
- return result.data;
85
- }
86
- console.warn("[parseNextData] Validation failed:", z.treeifyError(result.error));
87
- return null;
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- /**
94
- * Extracts modules from parsed __NEXT_DATA__.
95
- */
96
- export function extractModulesFromNextData(data: SkoolNextData): SkoolModule[] {
97
- const children = data.props?.pageProps?.course?.children;
98
- if (!Array.isArray(children)) return [];
99
-
100
- const modules: SkoolModule[] = [];
101
- const seen = new Set<string>();
102
-
103
- for (const child of children) {
104
- const course = child.course;
105
- if (!course?.name) continue;
106
-
107
- // Skool module slugs are 8-char hex strings
108
- if (!/^[a-f0-9]{8}$/.test(course.name)) continue;
109
-
110
- if (seen.has(course.name)) continue;
111
- seen.add(course.name);
112
-
113
- modules.push({
114
- slug: course.name,
115
- title: course.metadata?.title ?? `Module ${modules.length + 1}`,
116
- hasAccess: child.hasAccess !== false,
117
- });
118
- }
119
-
120
- return modules;
121
- }
122
-
123
- /**
124
- * Extracts lesson access info from parsed __NEXT_DATA__.
125
- */
126
- export function extractLessonAccessFromNextData(data: SkoolNextData): Map<string, boolean> {
127
- const accessMap = new Map<string, boolean>();
128
- const children = data.props?.pageProps?.course?.children;
129
-
130
- if (!Array.isArray(children)) return accessMap;
131
-
132
- for (const child of children) {
133
- const id = child.course?.id;
134
- const hasAccess = child.hasAccess;
135
- if (id && typeof hasAccess === "boolean") {
136
- accessMap.set(id, hasAccess);
137
- }
138
- }
139
-
140
- return accessMap;
141
- }
142
-
143
- /**
144
- * Extracts video URL from parsed __NEXT_DATA__ for a specific module.
145
- */
146
- export function extractVideoFromNextData(
147
- data: SkoolNextData,
148
- selectedModuleId: string
149
- ): SkoolVideoInfo | null {
150
- const children = data.props?.pageProps?.course?.children;
151
- if (!Array.isArray(children)) return null;
152
-
153
- for (const child of children) {
154
- if (child.course?.id === selectedModuleId) {
155
- const videoLink = child.course.metadata?.videoLink;
156
- if (!videoLink) return null;
157
-
158
- // Determine video type
159
- if (videoLink.includes("loom.com")) {
160
- const embedUrl = videoLink.replace("/share/", "/embed/").split("?")[0];
161
- return { url: embedUrl ?? videoLink, type: "loom" };
162
- }
163
- if (videoLink.includes("vimeo.com")) {
164
- return { url: videoLink, type: "vimeo" };
165
- }
166
- if (videoLink.includes("youtube.com") || videoLink.includes("youtu.be")) {
167
- return { url: videoLink, type: "youtube" };
168
- }
169
- if (videoLink.includes("wistia")) {
170
- return { url: videoLink, type: "wistia" };
171
- }
172
- return { url: videoLink, type: "unknown" };
173
- }
174
- }
175
-
176
- return null;
177
- }
@@ -1,435 +0,0 @@
1
- /**
2
- * Browser-based video URL interception - requires Playwright.
3
- * Excluded from coverage via vitest.config.ts.
4
- */
5
- import type { Page } from "playwright";
6
-
7
- // ============================================================================
8
- // Type definitions for external browser APIs
9
- // ============================================================================
10
-
11
- /** Vimeo player configuration embedded in the page */
12
- interface VimeoPlayerConfig {
13
- request?: {
14
- files?: {
15
- hls?: {
16
- cdns?: Record<string, { url?: string }>;
17
- };
18
- progressive?: { url?: string; height?: number }[];
19
- };
20
- };
21
- }
22
-
23
- /** Vimeo-related window properties */
24
- interface VimeoWindow {
25
- playerConfig?: VimeoPlayerConfig;
26
- vimeo?: { config?: VimeoPlayerConfig };
27
- __vimeo_player__?: { config?: VimeoPlayerConfig };
28
- }
29
-
30
- /** Loom video asset URLs */
31
- interface LoomAssetUrls {
32
- hls_url?: string;
33
- }
34
-
35
- /** Loom video data */
36
- interface LoomVideo {
37
- asset_urls?: LoomAssetUrls;
38
- }
39
-
40
- /** Loom SSR state embedded in the page */
41
- interface LoomSSRState {
42
- video?: LoomVideo;
43
- }
44
-
45
- /** Loom-related window properties */
46
- interface LoomWindow {
47
- __LOOM_SSR_STATE__?: LoomSSRState;
48
- }
49
-
50
- /** Next.js data for Loom pages */
51
- interface LoomNextData {
52
- props?: {
53
- pageProps?: {
54
- video?: LoomVideo;
55
- };
56
- };
57
- }
58
-
59
- /**
60
- * Captures Vimeo video URL by extracting it from the running player.
61
- * The key insight: the video is ALREADY playing in the iframe - we just need to get the URL.
62
- */
63
- export async function captureVimeoConfig(
64
- page: Page,
65
- _videoId: string,
66
- timeoutMs = 20000
67
- ): Promise<{ hlsUrl: string | null; progressiveUrl: string | null; error?: string }> {
68
- try {
69
- // Step 1: Make sure we have a Vimeo iframe or video wrapper
70
- // Skool wraps videos in a VideoPlayerWrapper - click it to ensure video loads
71
- const videoWrapper = await page.$(
72
- '[class*="VideoPlayerWrapper"], [class*="video-wrapper"], [class*="VideoPlayer"]'
73
- );
74
- if (videoWrapper) {
75
- await videoWrapper.click().catch(() => {});
76
- await page.waitForTimeout(1000);
77
- }
78
-
79
- // Step 2: Wait for Vimeo iframe to appear
80
- let vimeoFrame = null;
81
- const startTime = Date.now();
82
-
83
- while (!vimeoFrame && Date.now() - startTime < timeoutMs) {
84
- // Try to find the iframe
85
- const iframe = await page.$('iframe[src*="vimeo.com"], iframe[src*="player.vimeo"]');
86
- if (iframe) {
87
- vimeoFrame = await iframe.contentFrame();
88
- if (vimeoFrame) break;
89
- }
90
- await page.waitForTimeout(500);
91
- }
92
-
93
- if (!vimeoFrame) {
94
- return { hlsUrl: null, progressiveUrl: null, error: "Vimeo iframe not found after waiting" };
95
- }
96
-
97
- // Step 3: Mute the video before playing (we don't want audio!)
98
- await vimeoFrame
99
- .evaluate(() => {
100
- const video = document.querySelector("video");
101
- if (video) {
102
- video.muted = true;
103
- video.volume = 0;
104
- }
105
- })
106
- .catch(() => {});
107
-
108
- // Step 4: Click play button in the iframe to start video
109
- try {
110
- // Multiple selectors for Vimeo's play button
111
- await vimeoFrame
112
- .click(
113
- '.vp-controls button, .play-icon, [aria-label="Play"], .vp-big-play-button, button',
114
- {
115
- timeout: 2000,
116
- }
117
- )
118
- .catch(() => {});
119
- } catch {
120
- // Video might auto-play
121
- }
122
-
123
- // Ensure video stays muted
124
- await vimeoFrame
125
- .evaluate(() => {
126
- const video = document.querySelector("video");
127
- if (video) {
128
- video.muted = true;
129
- video.volume = 0;
130
- }
131
- })
132
- .catch(() => {});
133
-
134
- // Step 4: Wait for video to actually start playing and get the URL
135
- let hlsUrl: string | null = null;
136
- let progressiveUrl: string | null = null;
137
-
138
- const extractionStart = Date.now();
139
- while (!hlsUrl && !progressiveUrl && Date.now() - extractionStart < timeoutMs - 5000) {
140
- const urls = await vimeoFrame.evaluate(() => {
141
- const result = {
142
- hlsUrl: null as string | null,
143
- progressiveUrl: null as string | null,
144
- debug: [] as string[],
145
- };
146
-
147
- // Method 1: Get URL directly from video element
148
- const video = document.querySelector("video");
149
- if (video) {
150
- result.debug.push(`Video element found, src length: ${video.src?.length ?? 0}`);
151
-
152
- // Check currentSrc (what's actually playing)
153
- if (video.currentSrc) {
154
- result.debug.push(`currentSrc: ${video.currentSrc.substring(0, 80)}`);
155
- if (video.currentSrc.includes(".m3u8")) {
156
- result.hlsUrl = video.currentSrc;
157
- } else if (video.currentSrc.includes(".mp4")) {
158
- result.progressiveUrl = video.currentSrc;
159
- }
160
- }
161
-
162
- // Also check src attribute
163
- if (!result.hlsUrl && !result.progressiveUrl && video.src) {
164
- if (video.src.includes(".m3u8")) {
165
- result.hlsUrl = video.src;
166
- } else if (video.src.includes(".mp4")) {
167
- result.progressiveUrl = video.src;
168
- }
169
- }
170
- }
171
-
172
- // Method 2: Check source elements
173
- if (!result.hlsUrl && !result.progressiveUrl) {
174
- const sources = document.querySelectorAll("video source");
175
- result.debug.push(`Found ${sources.length} source elements`);
176
- for (const source of Array.from(sources)) {
177
- const src = (source as HTMLSourceElement).src;
178
- if (src?.includes(".m3u8")) {
179
- result.hlsUrl = src;
180
- break;
181
- } else if (src?.includes(".mp4") && !result.progressiveUrl) {
182
- result.progressiveUrl = src;
183
- }
184
- }
185
- }
186
-
187
- // Method 3: Extract from Vimeo's internal player state
188
- if (!result.hlsUrl && !result.progressiveUrl) {
189
- try {
190
- const win = window as unknown as VimeoWindow;
191
-
192
- // Try various Vimeo internal variables
193
- const configPaths = [
194
- win.playerConfig?.request?.files,
195
- win.vimeo?.config?.request?.files,
196
- win.__vimeo_player__?.config?.request?.files,
197
- ];
198
-
199
- for (const files of configPaths) {
200
- if (!files) continue;
201
-
202
- // HLS
203
- if (files.hls?.cdns) {
204
- const cdns = files.hls.cdns;
205
- for (const cdn of Object.keys(cdns)) {
206
- const cdnEntry = cdns[cdn];
207
- if (cdnEntry?.url) {
208
- result.hlsUrl = cdnEntry.url;
209
- result.debug.push(`Found HLS in playerConfig.${cdn}`);
210
- break;
211
- }
212
- }
213
- }
214
-
215
- // Progressive MP4
216
- if (!result.progressiveUrl && files.progressive && files.progressive.length > 0) {
217
- const sorted = [...files.progressive].sort(
218
- (a, b) => (b.height ?? 0) - (a.height ?? 0)
219
- );
220
- result.progressiveUrl = sorted[0]?.url ?? null;
221
- if (result.progressiveUrl) {
222
- result.debug.push("Found progressive in playerConfig");
223
- }
224
- }
225
-
226
- if (result.hlsUrl || result.progressiveUrl) break;
227
- }
228
- } catch (e) {
229
- result.debug.push(
230
- `Config extraction error: ${e instanceof Error ? e.message : String(e)}`
231
- );
232
- }
233
- }
234
-
235
- // Method 4: Network request URLs might be in DOM attributes
236
- if (!result.hlsUrl && !result.progressiveUrl) {
237
- const allElements = document.querySelectorAll("*");
238
- for (const el of Array.from(allElements)) {
239
- for (const attr of Array.from(el.attributes)) {
240
- if (attr.value.includes("vimeocdn.com") && attr.value.includes(".m3u8")) {
241
- result.hlsUrl = /https:\/\/[^\s"']+\.m3u8[^\s"']*/.exec(attr.value)?.[0] ?? null;
242
- if (result.hlsUrl) {
243
- result.debug.push("Found HLS in element attribute");
244
- break;
245
- }
246
- }
247
- }
248
- if (result.hlsUrl) break;
249
- }
250
- }
251
-
252
- return result;
253
- });
254
-
255
- hlsUrl = urls.hlsUrl;
256
- progressiveUrl = urls.progressiveUrl;
257
-
258
- if (!hlsUrl && !progressiveUrl) {
259
- // Wait and try again
260
- await new Promise((r) => setTimeout(r, 1000));
261
- }
262
- }
263
-
264
- if (hlsUrl || progressiveUrl) {
265
- return { hlsUrl, progressiveUrl };
266
- }
267
-
268
- return {
269
- hlsUrl: null,
270
- progressiveUrl: null,
271
- error: "Could not extract video URL from Vimeo player",
272
- };
273
- } catch (error) {
274
- return {
275
- hlsUrl: null,
276
- progressiveUrl: null,
277
- error: `Vimeo extraction failed: ${error instanceof Error ? error.message : String(error)}`,
278
- };
279
- }
280
- }
281
-
282
- /**
283
- * Captures Loom HLS URL by navigating directly to the embed page.
284
- * This works better than CDP because we can intercept all requests on that page.
285
- */
286
- export async function captureLoomHls(
287
- page: Page,
288
- videoId: string,
289
- timeoutMs = 15000
290
- ): Promise<{ hlsUrl: string | null; error?: string }> {
291
- let capturedUrl: string | null = null;
292
- const originalUrl = page.url();
293
-
294
- try {
295
- // Use CDP to intercept network responses
296
- const client = await page.context().newCDPSession(page);
297
- await client.send("Network.enable");
298
-
299
- // Match HLS playlists from Loom's CDN
300
- // Prefer master playlist (playlist.m3u8) over media playlists (mediaplaylist-*.m3u8)
301
- const masterPattern = /luna\.loom\.com.*\/playlist\.m3u8/;
302
- const anyHlsPattern = /luna\.loom\.com.*\.m3u8/;
303
-
304
- // Set up listener before navigation
305
- const responsePromise = new Promise<void>((resolve) => {
306
- const timeout = setTimeout(() => {
307
- resolve();
308
- }, timeoutMs);
309
- let hasMasterPlaylist = false;
310
-
311
- client.on("Network.responseReceived", (event) => {
312
- const url = event.response.url;
313
-
314
- // Always prefer master playlist
315
- if (masterPattern.test(url)) {
316
- capturedUrl = url;
317
- hasMasterPlaylist = true;
318
- clearTimeout(timeout);
319
- resolve();
320
- } else if (!hasMasterPlaylist && anyHlsPattern.test(url)) {
321
- // Capture any HLS as fallback, but keep listening for master
322
- capturedUrl = url;
323
- }
324
- });
325
- });
326
-
327
- // Navigate directly to Loom embed with autoplay (muted)
328
- const embedUrl = `https://www.loom.com/embed/${videoId}?autoplay=1`;
329
- await page.goto(embedUrl, { waitUntil: "domcontentloaded", timeout: 10000 });
330
-
331
- // Mute the video immediately
332
- await page
333
- .evaluate(() => {
334
- const video = document.querySelector("video");
335
- if (video) {
336
- video.muted = true;
337
- video.volume = 0;
338
- }
339
- })
340
- .catch(() => {});
341
-
342
- // Try to click play button if video doesn't autoplay
343
- try {
344
- await page.waitForTimeout(2000);
345
- const playButton = await page.$(
346
- '[data-testid="play-button"], .PlayButton, [aria-label="Play"], button[class*="play"]'
347
- );
348
- if (playButton) {
349
- // Mute again before clicking play
350
- await page
351
- .evaluate(() => {
352
- const video = document.querySelector("video");
353
- if (video) {
354
- video.muted = true;
355
- video.volume = 0;
356
- }
357
- })
358
- .catch(() => {});
359
- await playButton.click();
360
- }
361
- } catch {
362
- // No play button or click failed
363
- }
364
-
365
- // Ensure video stays muted after play
366
- await page
367
- .evaluate(() => {
368
- const video = document.querySelector("video");
369
- if (video) {
370
- video.muted = true;
371
- video.volume = 0;
372
- }
373
- })
374
- .catch(() => {});
375
-
376
- // Wait for HLS to be captured
377
- await responsePromise;
378
-
379
- // Also try to extract from page JS if not found via network
380
- if (!capturedUrl) {
381
- const jsUrl = await page.evaluate(() => {
382
- const win = window as unknown as LoomWindow;
383
-
384
- // Check __LOOM_SSR_STATE__
385
- if (win.__LOOM_SSR_STATE__?.video?.asset_urls?.hls_url) {
386
- return win.__LOOM_SSR_STATE__.video.asset_urls.hls_url;
387
- }
388
-
389
- // Check for Next.js data
390
- const nextData = document.getElementById("__NEXT_DATA__");
391
- if (nextData?.textContent) {
392
- try {
393
- const data = JSON.parse(nextData.textContent) as LoomNextData;
394
- const hlsUrl = data?.props?.pageProps?.video?.asset_urls?.hls_url;
395
- if (hlsUrl) return hlsUrl;
396
-
397
- // Try regex match in full data
398
- const videoData = /hls_url['":\s]+['"]([^'"]+)['"]/.exec(JSON.stringify(data));
399
- if (videoData?.[1]) return videoData[1];
400
- } catch {
401
- /* ignore parse errors */
402
- }
403
- }
404
-
405
- // Scan scripts for HLS URL
406
- const scripts = Array.from(document.querySelectorAll("script"));
407
- for (const script of scripts) {
408
- const match = /https:\/\/luna\.loom\.com[^"'\s]+\.m3u8[^"'\s]*/.exec(
409
- script.textContent ?? ""
410
- );
411
- if (match) return match[0];
412
- }
413
-
414
- return null;
415
- });
416
-
417
- if (jsUrl) {
418
- capturedUrl = jsUrl;
419
- }
420
- }
421
-
422
- await client.detach();
423
- } catch {
424
- // Error during capture
425
- }
426
-
427
- // Navigate back to original page
428
- try {
429
- await page.goto(originalUrl, { waitUntil: "domcontentloaded", timeout: 10000 });
430
- } catch {
431
- // Failed to navigate back
432
- }
433
-
434
- return capturedUrl ? { hlsUrl: capturedUrl } : { hlsUrl: null, error: "HLS URL not captured" };
435
- }
@@ -1,58 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createLoginChecker, isSkoolLoginPage, isHighLevelLoginPage } from "./auth.js";
3
-
4
- describe("auth", () => {
5
- describe("createLoginChecker", () => {
6
- it("creates checker that matches patterns", () => {
7
- const checker = createLoginChecker([/\/login/, /\/signin/]);
8
-
9
- expect(checker("https://example.com/login")).toBe(true);
10
- expect(checker("https://example.com/signin")).toBe(true);
11
- expect(checker("https://example.com/dashboard")).toBe(false);
12
- });
13
-
14
- it("uses default patterns when none provided", () => {
15
- const checker = createLoginChecker();
16
-
17
- expect(checker("https://example.com/login")).toBe(true);
18
- expect(checker("https://accounts.google.com/auth")).toBe(true);
19
- expect(checker("https://example.com/dashboard")).toBe(false);
20
- });
21
- });
22
-
23
- describe("isSkoolLoginPage", () => {
24
- it("detects Skool login page", () => {
25
- expect(isSkoolLoginPage("https://www.skool.com/login")).toBe(true);
26
- });
27
-
28
- it("detects Google OAuth redirect", () => {
29
- expect(isSkoolLoginPage("https://accounts.google.com/o/oauth2/auth?...")).toBe(true);
30
- });
31
-
32
- it("returns false for non-login pages", () => {
33
- expect(isSkoolLoginPage("https://www.skool.com/my-community")).toBe(false);
34
- expect(isSkoolLoginPage("https://www.skool.com/my-community/classroom")).toBe(false);
35
- });
36
- });
37
-
38
- describe("isHighLevelLoginPage", () => {
39
- it("detects HighLevel SSO page", () => {
40
- expect(isHighLevelLoginPage("https://sso.clientclub.net/login")).toBe(true);
41
- });
42
-
43
- it("detects various login paths", () => {
44
- expect(isHighLevelLoginPage("https://portal.example.com/login")).toBe(true);
45
- expect(isHighLevelLoginPage("https://portal.example.com/signin")).toBe(true);
46
- expect(isHighLevelLoginPage("https://portal.example.com/auth/callback")).toBe(true);
47
- });
48
-
49
- it("detects Firebase auth", () => {
50
- expect(isHighLevelLoginPage("https://example.firebaseapp.com/auth")).toBe(true);
51
- });
52
-
53
- it("returns false for content pages", () => {
54
- expect(isHighLevelLoginPage("https://portal.example.com/courses")).toBe(false);
55
- expect(isHighLevelLoginPage("https://portal.example.com/dashboard")).toBe(false);
56
- });
57
- });
58
- });