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,441 +0,0 @@
1
- import chalk from "chalk";
2
- import ora from "ora";
3
- import { join } from "node:path";
4
- import { loadConfig } from "../../config/configManager.js";
5
- import { expandPath } from "../../config/paths.js";
6
- import { getAuthenticatedSession, isSkoolLoginPage } from "../../shared/auth.js";
7
- import { ensureDir, outputFile } from "../../shared/fs.js";
8
-
9
- const SKOOL_DOMAIN = "www.skool.com";
10
- const SKOOL_LOGIN_URL = "https://www.skool.com/login";
11
-
12
- interface InspectOptions {
13
- output?: string;
14
- full?: boolean;
15
- click?: boolean;
16
- }
17
-
18
- /**
19
- * Inspects the page structure and logs useful debugging info.
20
- */
21
- export async function inspectCommand(url: string, options: InspectOptions): Promise<void> {
22
- console.log(chalk.blue("\n🔍 Page Inspector\n"));
23
-
24
- const config = loadConfig();
25
- const spinner = ora("Connecting...").start();
26
-
27
- let browser;
28
- let session;
29
-
30
- try {
31
- const result = await getAuthenticatedSession(
32
- {
33
- domain: SKOOL_DOMAIN,
34
- loginUrl: SKOOL_LOGIN_URL,
35
- isLoginPage: isSkoolLoginPage,
36
- },
37
- { headless: false } // Always visible for inspection
38
- );
39
- browser = result.browser;
40
- session = result.session;
41
- spinner.succeed("Connected");
42
- } catch {
43
- spinner.fail("Failed to connect");
44
- console.log(chalk.red("\n❌ Please run: course-grab login\n"));
45
- process.exit(1);
46
- }
47
-
48
- try {
49
- // Collect network requests to find video URLs
50
- const videoRequests: { url: string; resourceType: string }[] = [];
51
- session.page.on("request", (request) => {
52
- const reqUrl = request.url();
53
- const resourceType = request.resourceType();
54
- if (
55
- resourceType === "media" ||
56
- reqUrl.includes(".mp4") ||
57
- reqUrl.includes(".m3u8") ||
58
- reqUrl.includes(".webm") ||
59
- reqUrl.includes("vimeo") ||
60
- reqUrl.includes("wistia") ||
61
- reqUrl.includes("mux.com") ||
62
- reqUrl.includes("cloudflare") ||
63
- reqUrl.includes("stream")
64
- ) {
65
- videoRequests.push({ url: reqUrl, resourceType });
66
- }
67
- });
68
-
69
- const pageSpinner = ora("Loading page...").start();
70
- await session.page.goto(url, { timeout: 60000 });
71
- // Use domcontentloaded instead of networkidle - some pages never stop loading
72
- await session.page.waitForLoadState("domcontentloaded");
73
- // Give it a moment for JS to render
74
- await session.page.waitForTimeout(2000);
75
- pageSpinner.succeed("Page loaded");
76
-
77
- console.log(chalk.cyan("\n📄 Page Info:\n"));
78
- console.log(` URL: ${session.page.url()}`);
79
- console.log(` Title: ${await session.page.title()}`);
80
-
81
- // Look for video preview/placeholder elements
82
- const previewInfo = await session.page.evaluate(() => {
83
- const previews: {
84
- selector: string;
85
- description: string;
86
- element: string;
87
- }[] = [];
88
-
89
- // Common video preview patterns
90
- const previewSelectors = [
91
- // Play buttons
92
- '[class*="play"]',
93
- '[class*="Play"]',
94
- 'button[class*="video"]',
95
- '[aria-label*="play" i]',
96
- '[aria-label*="Play" i]',
97
- // Thumbnail overlays
98
- '[class*="thumbnail"]',
99
- '[class*="poster"]',
100
- '[class*="preview"]',
101
- '[class*="cover"]',
102
- // SVG play icons
103
- 'svg[class*="play"]',
104
- // Clickable video containers
105
- '[class*="video-container"]',
106
- '[class*="player-container"]',
107
- '[class*="video-wrapper"]',
108
- // Data attributes
109
- "[data-video]",
110
- "[data-video-id]",
111
- "[data-src]",
112
- ];
113
-
114
- for (const selector of previewSelectors) {
115
- const elements = document.querySelectorAll(selector);
116
- elements.forEach((el) => {
117
- const rect = el.getBoundingClientRect();
118
- // Only consider visible, reasonably sized elements
119
- if (rect.width > 50 && rect.height > 50) {
120
- previews.push({
121
- selector,
122
- description: `<${el.tagName.toLowerCase()}> class="${el.className}" (${Math.round(rect.width)}x${Math.round(rect.height)})`,
123
- element: el.outerHTML.substring(0, 200),
124
- });
125
- }
126
- });
127
- }
128
-
129
- return previews;
130
- });
131
-
132
- if (previewInfo.length > 0) {
133
- console.log(chalk.yellow("\n🎬 Potential Video Previews/Placeholders:\n"));
134
- const seen = new Set<string>();
135
- for (const preview of previewInfo) {
136
- if (seen.has(preview.description)) continue;
137
- seen.add(preview.description);
138
- console.log(` ${preview.description}`);
139
- console.log(chalk.gray(` selector: ${preview.selector}`));
140
- console.log(chalk.gray(` html: ${preview.element.substring(0, 100)}...`));
141
- }
142
- }
143
-
144
- // If --click flag, try to click on video preview
145
- if (options.click) {
146
- console.log(chalk.cyan("\n👆 Attempting to click video preview...\n"));
147
-
148
- const clicked = await session.page.evaluate(() => {
149
- // Try various selectors for play button
150
- const playSelectors = [
151
- '[class*="play"]',
152
- '[class*="Play"]',
153
- 'button[class*="video"]',
154
- '[class*="poster"]',
155
- '[class*="thumbnail"]',
156
- '[class*="video-container"]',
157
- '[class*="player"]',
158
- ];
159
-
160
- for (const selector of playSelectors) {
161
- const el = document.querySelector(selector);
162
- if (el && el instanceof HTMLElement) {
163
- const rect = el.getBoundingClientRect();
164
- if (rect.width > 50 && rect.height > 50) {
165
- el.click();
166
- return { clicked: true, selector, element: el.outerHTML.substring(0, 100) };
167
- }
168
- }
169
- }
170
- return { clicked: false };
171
- });
172
-
173
- if (clicked.clicked) {
174
- console.log(chalk.green(` ✓ Clicked on: ${clicked.selector}`));
175
- console.log(chalk.gray(` ${clicked.element}...`));
176
-
177
- // Wait for video element to appear
178
- console.log(chalk.gray(" Waiting for video element..."));
179
- try {
180
- await session.page.waitForSelector(
181
- "video, iframe[src*='vimeo'], iframe[src*='wistia'], iframe[src*='youtube']",
182
- {
183
- timeout: 5000,
184
- }
185
- );
186
- console.log(chalk.green(" ✓ Video element appeared!"));
187
- } catch {
188
- console.log(chalk.yellow(" ⚠ No video element detected after click"));
189
- }
190
-
191
- // Small delay for any animations/loading
192
- await session.page.waitForTimeout(1000);
193
- } else {
194
- console.log(chalk.yellow(" ⚠ No clickable preview found"));
195
- }
196
- }
197
-
198
- // Analyze page structure
199
- const analysis = await session.page.evaluate(() => {
200
- const result: Record<string, unknown> = {};
201
-
202
- // Find all iframes (potential video embeds)
203
- const iframes = document.querySelectorAll("iframe");
204
- result.iframes = Array.from(iframes).map((iframe) => ({
205
- src: iframe.src,
206
- id: iframe.id,
207
- className: iframe.className,
208
- width: iframe.width,
209
- height: iframe.height,
210
- }));
211
-
212
- // Find all video elements
213
- const videos = document.querySelectorAll("video");
214
- result.videos = Array.from(videos).map((video) => ({
215
- src: video.src,
216
- poster: video.poster,
217
- className: video.className,
218
- sources: Array.from(video.querySelectorAll("source")).map((s) => ({
219
- src: s.src,
220
- type: s.type,
221
- })),
222
- }));
223
-
224
- // Find elements with video-related classes
225
- const videoRelated = document.querySelectorAll(
226
- '[class*="video"], [class*="player"], [class*="wistia"], [class*="vimeo"], [class*="embed"], [class*="media"]'
227
- );
228
- result.videoRelatedElements = Array.from(videoRelated).map((el) => ({
229
- tagName: el.tagName,
230
- className: el.className,
231
- id: el.id,
232
- dataAttributes: Object.fromEntries(
233
- Array.from(el.attributes)
234
- .filter((attr) => attr.name.startsWith("data-"))
235
- .map((attr) => [attr.name, attr.value])
236
- ),
237
- // Include src attributes that might have video URLs
238
- src: el.getAttribute("src"),
239
- href: el.getAttribute("href"),
240
- }));
241
-
242
- // Look for any script tags that might contain video configuration
243
- const scripts = document.querySelectorAll("script");
244
- const videoScripts: string[] = [];
245
- scripts.forEach((script) => {
246
- const content = script.textContent || "";
247
- if (
248
- content.includes("vimeo") ||
249
- content.includes("wistia") ||
250
- content.includes("video") ||
251
- content.includes("player") ||
252
- content.includes("mux") ||
253
- content.includes("cloudflare")
254
- ) {
255
- // Extract just the relevant parts
256
- const matches = content.match(/(https?:\/\/[^\s"']+\.(mp4|m3u8|webm|mov)[^\s"']*)/gi);
257
- if (matches) {
258
- videoScripts.push(...matches);
259
- }
260
- // Also look for video IDs
261
- const idMatches = content.match(/"(video[_-]?id|videoId|id)":\s*"([^"]+)"/gi);
262
- if (idMatches) {
263
- videoScripts.push(...idMatches);
264
- }
265
- }
266
- });
267
- result.videoScripts = [...new Set(videoScripts)];
268
-
269
- // Find navigation/sidebar elements (for lessons)
270
- const navElements = document.querySelectorAll(
271
- 'nav, [class*="sidebar"], [class*="nav"], [class*="menu"], [class*="lesson"]'
272
- );
273
- result.navigationElements = Array.from(navElements)
274
- .slice(0, 10)
275
- .map((el) => ({
276
- tagName: el.tagName,
277
- className: el.className,
278
- id: el.id,
279
- childCount: el.children.length,
280
- links: Array.from(el.querySelectorAll("a")).map((a) => ({
281
- href: a.href,
282
- text: a.textContent?.trim().substring(0, 50),
283
- })),
284
- }));
285
-
286
- // Find main content area
287
- const contentAreas = document.querySelectorAll(
288
- 'main, article, [class*="content"], [class*="post"], [class*="body"]'
289
- );
290
- result.contentAreas = Array.from(contentAreas)
291
- .slice(0, 5)
292
- .map((el) => ({
293
- tagName: el.tagName,
294
- className: el.className,
295
- id: el.id,
296
- textLength: el.textContent?.length ?? 0,
297
- hasVideo: el.querySelector("video, iframe") !== null,
298
- }));
299
-
300
- // Find all links to /classroom/
301
- const classroomLinks = document.querySelectorAll('a[href*="/classroom/"]');
302
- result.classroomLinks = Array.from(classroomLinks).map((a) => ({
303
- href: (a as HTMLAnchorElement).href,
304
- text: a.textContent?.trim().substring(0, 100),
305
- className: a.className,
306
- parentClass: a.parentElement?.className,
307
- }));
308
-
309
- // Get page structure overview
310
- const getAllClasses = (el: Element, depth = 0): string[] => {
311
- if (depth > 3) return [];
312
- const classes: string[] = [];
313
- if (el.className && typeof el.className === "string") {
314
- classes.push(`${" ".repeat(depth)}${el.tagName}.${el.className.split(" ").join(".")}`);
315
- }
316
- Array.from(el.children).forEach((child) => {
317
- classes.push(...getAllClasses(child, depth + 1));
318
- });
319
- return classes;
320
- };
321
-
322
- result.bodyStructure = getAllClasses(document.body).slice(0, 100);
323
-
324
- return result;
325
- });
326
-
327
- // Output analysis
328
- console.log(chalk.cyan("\n🎬 Video Sources:\n"));
329
- if ((analysis.iframes as { src: string }[]).length > 0) {
330
- console.log(chalk.yellow(" Iframes:"));
331
- for (const iframe of analysis.iframes as { src: string; className: string }[]) {
332
- console.log(` - ${iframe.src}`);
333
- console.log(chalk.gray(` class: ${iframe.className}`));
334
- }
335
- }
336
- if ((analysis.videos as { src: string }[]).length > 0) {
337
- console.log(chalk.yellow("\n Video elements:"));
338
- for (const video of analysis.videos as {
339
- src: string;
340
- sources: { src: string }[];
341
- }[]) {
342
- console.log(` - ${video.src || "(no src)"}`);
343
- for (const source of video.sources) {
344
- console.log(` source: ${source.src}`);
345
- }
346
- }
347
- }
348
-
349
- // Show video URLs found in scripts
350
- const videoScripts = analysis.videoScripts as string[];
351
- if (videoScripts.length > 0) {
352
- console.log(chalk.yellow("\n Video URLs from scripts:"));
353
- for (const url of videoScripts.slice(0, 10)) {
354
- console.log(chalk.green(` - ${url}`));
355
- }
356
- }
357
-
358
- if ((analysis.videoRelatedElements as { className: string }[]).length > 0) {
359
- console.log(chalk.yellow("\n Video-related elements:"));
360
- for (const el of (
361
- analysis.videoRelatedElements as {
362
- tagName: string;
363
- className: string;
364
- dataAttributes: Record<string, string>;
365
- src?: string;
366
- }[]
367
- ).slice(0, 10)) {
368
- console.log(` - <${el.tagName.toLowerCase()}> class="${el.className}"`);
369
- if (el.src) {
370
- console.log(chalk.green(` src: ${el.src}`));
371
- }
372
- if (Object.keys(el.dataAttributes).length > 0) {
373
- console.log(chalk.gray(` data: ${JSON.stringify(el.dataAttributes)}`));
374
- }
375
- }
376
- }
377
-
378
- console.log(chalk.cyan("\n📚 Classroom Links:\n"));
379
- for (const link of (
380
- analysis.classroomLinks as { href: string; text: string; className: string }[]
381
- ).slice(0, 15)) {
382
- console.log(` - ${link.text || "(no text)"}`);
383
- console.log(chalk.gray(` ${link.href}`));
384
- console.log(chalk.gray(` class: ${link.className}`));
385
- }
386
-
387
- console.log(chalk.cyan("\n🧭 Navigation Elements:\n"));
388
- for (const nav of analysis.navigationElements as {
389
- tagName: string;
390
- className: string;
391
- links: { href: string; text: string }[];
392
- }[]) {
393
- console.log(` <${nav.tagName.toLowerCase()}> class="${nav.className}"`);
394
- if (nav.links.length > 0) {
395
- console.log(chalk.gray(` Links: ${nav.links.length}`));
396
- for (const link of nav.links.slice(0, 5)) {
397
- console.log(chalk.gray(` - ${link.text}: ${link.href}`));
398
- }
399
- }
400
- }
401
-
402
- // Save full analysis to file if requested
403
- if (options.output || options.full) {
404
- const outputDir = expandPath(options.output ?? config.outputDir);
405
- await ensureDir(outputDir);
406
-
407
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
408
- const filename = `inspect-${timestamp}.json`;
409
- const filepath = join(outputDir, filename);
410
-
411
- await outputFile(filepath, JSON.stringify(analysis, null, 2));
412
- console.log(chalk.green(`\n📁 Full analysis saved to: ${filepath}\n`));
413
-
414
- // Also save HTML
415
- if (options.full) {
416
- const html = await session.page.content();
417
- const htmlPath = join(outputDir, `inspect-${timestamp}.html`);
418
- await outputFile(htmlPath, html);
419
- console.log(chalk.green(`📁 HTML saved to: ${htmlPath}\n`));
420
- }
421
- }
422
-
423
- // Show network requests that looked like video
424
- if (videoRequests.length > 0) {
425
- console.log(chalk.cyan("\n📡 Video-related Network Requests:\n"));
426
- const seen = new Set<string>();
427
- for (const req of videoRequests) {
428
- if (seen.has(req.url)) continue;
429
- seen.add(req.url);
430
- console.log(chalk.green(` - ${req.url.substring(0, 120)}`));
431
- console.log(chalk.gray(` type: ${req.resourceType}`));
432
- }
433
- }
434
-
435
- console.log(chalk.gray("\n💡 Tips:"));
436
- console.log(chalk.gray(" - Use --click to trigger lazy-loaded video players"));
437
- console.log(chalk.gray(" - Use --full to save complete HTML for offline analysis\n"));
438
- } finally {
439
- await browser.close();
440
- }
441
- }
@@ -1,68 +0,0 @@
1
- import chalk from "chalk";
2
- import {
3
- clearSession,
4
- getAuthenticatedSession,
5
- hasValidSession,
6
- isSkoolLoginPage,
7
- } from "../../shared/auth.js";
8
-
9
- const SKOOL_DOMAIN = "www.skool.com";
10
- const SKOOL_LOGIN_URL = "https://www.skool.com/login";
11
-
12
- /**
13
- * Handles the login command.
14
- * Opens a browser for the user to log in manually.
15
- */
16
- export async function loginCommand(options: { force?: boolean }): Promise<void> {
17
- console.log(chalk.blue("\n🔐 Skool.com Login\n"));
18
-
19
- if ((await hasValidSession(SKOOL_DOMAIN)) && !options.force) {
20
- console.log(chalk.yellow("⚠️ You already have an active session."));
21
- console.log(chalk.gray(" Use --force to re-login anyway.\n"));
22
- return;
23
- }
24
-
25
- if (options.force) {
26
- await clearSession(SKOOL_DOMAIN);
27
- console.log(chalk.gray(" Cleared existing session.\n"));
28
- }
29
-
30
- try {
31
- const { browser } = await getAuthenticatedSession(
32
- {
33
- domain: SKOOL_DOMAIN,
34
- loginUrl: SKOOL_LOGIN_URL,
35
- isLoginPage: isSkoolLoginPage,
36
- },
37
- { headless: false }
38
- );
39
-
40
- // Close the browser after successful login
41
- await browser.close();
42
-
43
- console.log(chalk.green("✅ Login successful!"));
44
- console.log(chalk.gray(" Your session has been saved.\n"));
45
- console.log(chalk.gray(" You can now use: offcourse sync <url>\n"));
46
- } catch (error) {
47
- if (error instanceof Error && error.message.includes("Timeout")) {
48
- console.log(chalk.red("\n❌ Login timed out."));
49
- console.log(chalk.gray(" Please try again and complete the login within 5 minutes.\n"));
50
- } else {
51
- console.log(chalk.red("\n❌ Login failed:"), error);
52
- }
53
- process.exit(1);
54
- }
55
- }
56
-
57
- /**
58
- * Handles the logout command.
59
- */
60
- export async function logoutCommand(): Promise<void> {
61
- console.log(chalk.blue("\n🔓 Logging out...\n"));
62
-
63
- if (await clearSession(SKOOL_DOMAIN)) {
64
- console.log(chalk.green("✅ Session cleared successfully.\n"));
65
- } else {
66
- console.log(chalk.yellow("⚠️ No active session found.\n"));
67
- }
68
- }
@@ -1,147 +0,0 @@
1
- import chalk from "chalk";
2
- import {
3
- CourseDatabase,
4
- extractCommunitySlug,
5
- LessonStatus,
6
- getDbPath,
7
- } from "../../state/index.js";
8
- import { existsSync } from "node:fs";
9
-
10
- export interface StatusOptions {
11
- errors?: boolean;
12
- pending?: boolean;
13
- all?: boolean;
14
- }
15
-
16
- /**
17
- * Handles the status command.
18
- * Shows the current sync state for a course.
19
- */
20
- export function statusCommand(url: string, options: StatusOptions): void {
21
- console.log(chalk.blue("\n📊 Course Status\n"));
22
-
23
- // Validate URL
24
- if (!url.includes("skool.com")) {
25
- console.log(chalk.red("❌ Invalid URL. Please provide a Skool URL."));
26
- process.exit(1);
27
- }
28
-
29
- const communitySlug = extractCommunitySlug(url);
30
- const dbPath = getDbPath(communitySlug);
31
-
32
- if (!existsSync(dbPath)) {
33
- console.log(chalk.yellow(` No sync state found for: ${communitySlug}`));
34
- console.log(chalk.gray(` Run 'offcourse sync ${url}' to start syncing.\n`));
35
- return;
36
- }
37
-
38
- const db = new CourseDatabase(communitySlug);
39
-
40
- try {
41
- const meta = db.getCourseMetadata();
42
- const summary = db.getStatusSummary();
43
-
44
- console.log(chalk.white(` Course: ${meta.name}`));
45
- console.log(chalk.gray(` URL: ${meta.url}`));
46
- console.log(chalk.gray(` Last sync: ${meta.lastSyncAt ?? "never"}`));
47
- console.log();
48
- console.log(chalk.gray(` Modules: ${meta.totalModules}`));
49
- console.log(chalk.gray(` Lessons: ${meta.totalLessons}`));
50
- console.log();
51
- console.log(chalk.green(` ✅ Downloaded: ${summary.downloaded}`));
52
- if (summary.validated > 0) {
53
- console.log(chalk.blue(` ⬇️ Ready to download: ${summary.validated}`));
54
- }
55
- if (summary.pending > 0) {
56
- console.log(chalk.gray(` 🔍 Not scanned yet: ${summary.pending}`));
57
- }
58
- if (summary.skipped > 0) {
59
- console.log(chalk.gray(` ➖ No video: ${summary.skipped}`));
60
- }
61
- if (summary.error > 0) {
62
- console.log(chalk.red(` ❌ Failed: ${summary.error}`));
63
- }
64
-
65
- // Show error details if requested
66
- if (options.errors || options.all) {
67
- const errorLessons = db.getLessonsByStatus(LessonStatus.ERROR);
68
- if (errorLessons.length > 0) {
69
- console.log(chalk.red("\n ❌ Failed Lessons:\n"));
70
- for (const lesson of errorLessons) {
71
- console.log(chalk.red(` • ${lesson.moduleName} > ${lesson.name}`));
72
- if (lesson.errorMessage) {
73
- console.log(chalk.gray(` ${lesson.errorMessage}`));
74
- }
75
- if (lesson.errorCode) {
76
- console.log(chalk.gray(` Code: ${lesson.errorCode}`));
77
- }
78
- }
79
- }
80
- }
81
-
82
- // Show not-scanned details if requested
83
- if (options.pending || options.all) {
84
- const pendingLessons = db.getLessonsByStatus(LessonStatus.PENDING);
85
- if (pendingLessons.length > 0) {
86
- console.log(chalk.yellow("\n 🔍 Not Yet Scanned:\n"));
87
- let currentModule = "";
88
- for (const lesson of pendingLessons) {
89
- if (lesson.moduleName !== currentModule) {
90
- currentModule = lesson.moduleName;
91
- console.log(chalk.blue(`\n 📖 ${currentModule}`));
92
- }
93
- console.log(chalk.gray(` • ${lesson.name}`));
94
- }
95
- }
96
- }
97
-
98
- console.log();
99
- } finally {
100
- db.close();
101
- }
102
- }
103
-
104
- /**
105
- * List all synced courses.
106
- */
107
- export async function statusListCommand(): Promise<void> {
108
- console.log(chalk.blue("\n📚 Synced Courses\n"));
109
-
110
- const { getDbDir } = await import("../../state/index.js");
111
- const { readdirSync } = await import("node:fs");
112
-
113
- const dbDir = getDbDir();
114
-
115
- if (!existsSync(dbDir)) {
116
- console.log(chalk.gray(" No courses synced yet.\n"));
117
- return;
118
- }
119
-
120
- const files = readdirSync(dbDir).filter((f) => f.endsWith(".db"));
121
-
122
- if (files.length === 0) {
123
- console.log(chalk.gray(" No courses synced yet.\n"));
124
- return;
125
- }
126
-
127
- for (const file of files) {
128
- const slug = file.replace(".db", "");
129
- const db = new CourseDatabase(slug);
130
-
131
- try {
132
- const meta = db.getCourseMetadata();
133
- const summary = db.getStatusSummary();
134
-
135
- console.log(chalk.white(` ${meta.name || slug}`));
136
- console.log(chalk.gray(` └─ ${summary.downloaded}/${meta.totalLessons} downloaded`));
137
-
138
- if (summary.error > 0) {
139
- console.log(chalk.red(` ${summary.error} errors`));
140
- }
141
- console.log();
142
- } finally {
143
- db.close();
144
- }
145
- }
146
- }
147
-