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,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
-