offcourse 0.0.2 → 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 (139) hide show
  1. package/README.md +255 -20
  2. package/dist/cli/commands/config.d.ts +13 -0
  3. package/dist/cli/commands/config.d.ts.map +1 -0
  4. package/dist/cli/commands/config.js +66 -0
  5. package/dist/cli/commands/config.js.map +1 -0
  6. package/dist/cli/commands/inspect.d.ts +11 -0
  7. package/dist/cli/commands/inspect.d.ts.map +1 -0
  8. package/dist/cli/commands/inspect.js +365 -0
  9. package/dist/cli/commands/inspect.js.map +1 -0
  10. package/dist/cli/commands/login.d.ts +12 -0
  11. package/dist/cli/commands/login.d.ts.map +1 -0
  12. package/dist/cli/commands/login.js +55 -0
  13. package/dist/cli/commands/login.js.map +1 -0
  14. package/dist/cli/commands/status.d.ts +15 -0
  15. package/dist/cli/commands/status.d.ts.map +1 -0
  16. package/dist/cli/commands/status.js +118 -0
  17. package/dist/cli/commands/status.js.map +1 -0
  18. package/dist/cli/commands/sync.d.ts +15 -0
  19. package/dist/cli/commands/sync.d.ts.map +1 -0
  20. package/dist/cli/commands/sync.js +921 -0
  21. package/dist/cli/commands/sync.js.map +1 -0
  22. package/dist/cli/commands/syncHighLevel.d.ts +23 -0
  23. package/dist/cli/commands/syncHighLevel.d.ts.map +1 -0
  24. package/dist/cli/commands/syncHighLevel.js +479 -0
  25. package/dist/cli/commands/syncHighLevel.js.map +1 -0
  26. package/dist/cli/index.d.ts +3 -0
  27. package/dist/cli/index.d.ts.map +1 -0
  28. package/dist/cli/index.js +106 -0
  29. package/dist/cli/index.js.map +1 -0
  30. package/dist/config/configManager.d.ts +31 -0
  31. package/dist/config/configManager.d.ts.map +1 -0
  32. package/dist/config/configManager.js +68 -0
  33. package/dist/config/configManager.js.map +1 -0
  34. package/dist/config/paths.d.ts +21 -0
  35. package/dist/config/paths.d.ts.map +1 -0
  36. package/dist/config/paths.js +33 -0
  37. package/dist/config/paths.js.map +1 -0
  38. package/dist/config/schema.d.ts +60 -0
  39. package/dist/config/schema.d.ts.map +1 -0
  40. package/dist/config/schema.js +50 -0
  41. package/dist/config/schema.js.map +1 -0
  42. package/dist/downloader/hlsDownloader.d.ts +58 -0
  43. package/dist/downloader/hlsDownloader.d.ts.map +1 -0
  44. package/dist/downloader/hlsDownloader.js +263 -0
  45. package/dist/downloader/hlsDownloader.js.map +1 -0
  46. package/dist/downloader/hlsValidator.d.ts +35 -0
  47. package/dist/downloader/hlsValidator.d.ts.map +1 -0
  48. package/dist/downloader/hlsValidator.js +152 -0
  49. package/dist/downloader/hlsValidator.js.map +1 -0
  50. package/dist/downloader/index.d.ts +29 -0
  51. package/dist/downloader/index.d.ts.map +1 -0
  52. package/dist/downloader/index.js +55 -0
  53. package/dist/downloader/index.js.map +1 -0
  54. package/dist/downloader/loomDownloader.d.ts +56 -0
  55. package/dist/downloader/loomDownloader.d.ts.map +1 -0
  56. package/dist/downloader/loomDownloader.js +562 -0
  57. package/dist/downloader/loomDownloader.js.map +1 -0
  58. package/dist/downloader/queue.d.ts +56 -0
  59. package/dist/downloader/queue.d.ts.map +1 -0
  60. package/dist/downloader/queue.js +88 -0
  61. package/dist/downloader/queue.js.map +1 -0
  62. package/dist/downloader/vimeoDownloader.d.ts +52 -0
  63. package/dist/downloader/vimeoDownloader.d.ts.map +1 -0
  64. package/dist/downloader/vimeoDownloader.js +569 -0
  65. package/dist/downloader/vimeoDownloader.js.map +1 -0
  66. package/dist/scraper/extractor.d.ts +53 -0
  67. package/dist/scraper/extractor.d.ts.map +1 -0
  68. package/dist/scraper/extractor.js +627 -0
  69. package/dist/scraper/extractor.js.map +1 -0
  70. package/dist/scraper/highlevel/extractor.d.ts +89 -0
  71. package/dist/scraper/highlevel/extractor.d.ts.map +1 -0
  72. package/dist/scraper/highlevel/extractor.js +373 -0
  73. package/dist/scraper/highlevel/extractor.js.map +1 -0
  74. package/dist/scraper/highlevel/index.d.ts +3 -0
  75. package/dist/scraper/highlevel/index.d.ts.map +1 -0
  76. package/dist/scraper/highlevel/index.js +3 -0
  77. package/dist/scraper/highlevel/index.js.map +1 -0
  78. package/dist/scraper/highlevel/navigator.d.ts +86 -0
  79. package/dist/scraper/highlevel/navigator.d.ts.map +1 -0
  80. package/dist/scraper/highlevel/navigator.js +505 -0
  81. package/dist/scraper/highlevel/navigator.js.map +1 -0
  82. package/dist/scraper/highlevel/schemas.d.ts +188 -0
  83. package/dist/scraper/highlevel/schemas.d.ts.map +1 -0
  84. package/dist/scraper/highlevel/schemas.js +139 -0
  85. package/dist/scraper/highlevel/schemas.js.map +1 -0
  86. package/dist/scraper/navigator.d.ts +68 -0
  87. package/dist/scraper/navigator.d.ts.map +1 -0
  88. package/dist/scraper/navigator.js +257 -0
  89. package/dist/scraper/navigator.js.map +1 -0
  90. package/dist/scraper/schemas.d.ts +57 -0
  91. package/dist/scraper/schemas.d.ts.map +1 -0
  92. package/dist/scraper/schemas.js +135 -0
  93. package/dist/scraper/schemas.js.map +1 -0
  94. package/dist/scraper/videoInterceptor.d.ts +23 -0
  95. package/dist/scraper/videoInterceptor.d.ts.map +1 -0
  96. package/dist/scraper/videoInterceptor.js +330 -0
  97. package/dist/scraper/videoInterceptor.js.map +1 -0
  98. package/dist/shared/auth.d.ts +58 -0
  99. package/dist/shared/auth.d.ts.map +1 -0
  100. package/dist/shared/auth.js +197 -0
  101. package/dist/shared/auth.js.map +1 -0
  102. package/dist/shared/firebase.d.ts +60 -0
  103. package/dist/shared/firebase.d.ts.map +1 -0
  104. package/dist/shared/firebase.js +102 -0
  105. package/dist/shared/firebase.js.map +1 -0
  106. package/dist/shared/fs.d.ts +31 -0
  107. package/dist/shared/fs.d.ts.map +1 -0
  108. package/dist/shared/fs.js +77 -0
  109. package/dist/shared/fs.js.map +1 -0
  110. package/dist/shared/http.d.ts +15 -0
  111. package/dist/shared/http.d.ts.map +1 -0
  112. package/dist/shared/http.js +31 -0
  113. package/dist/shared/http.js.map +1 -0
  114. package/dist/shared/index.d.ts +7 -0
  115. package/dist/shared/index.d.ts.map +1 -0
  116. package/dist/shared/index.js +7 -0
  117. package/dist/shared/index.js.map +1 -0
  118. package/dist/shared/slug.d.ts +11 -0
  119. package/dist/shared/slug.d.ts.map +1 -0
  120. package/dist/shared/slug.js +25 -0
  121. package/dist/shared/slug.js.map +1 -0
  122. package/dist/shared/url.d.ts +43 -0
  123. package/dist/shared/url.d.ts.map +1 -0
  124. package/dist/shared/url.js +54 -0
  125. package/dist/shared/url.js.map +1 -0
  126. package/dist/state/database.d.ts +246 -0
  127. package/dist/state/database.d.ts.map +1 -0
  128. package/dist/state/database.js +679 -0
  129. package/dist/state/database.js.map +1 -0
  130. package/dist/state/index.d.ts +2 -0
  131. package/dist/state/index.d.ts.map +1 -0
  132. package/dist/state/index.js +2 -0
  133. package/dist/state/index.js.map +1 -0
  134. package/dist/storage/fileSystem.d.ts +56 -0
  135. package/dist/storage/fileSystem.d.ts.map +1 -0
  136. package/dist/storage/fileSystem.js +129 -0
  137. package/dist/storage/fileSystem.js.map +1 -0
  138. package/package.json +71 -11
  139. package/cli.js +0 -45
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Zod schemas for HighLevel API responses.
3
+ * These provide runtime validation and type inference.
4
+ */
5
+ import { z } from "zod";
6
+ export { FirebaseAuthTokenSchema, type FirebaseAuthToken, type FirebaseAuthRaw, } from "../../shared/firebase.js";
7
+ export declare const PortalSettingsResponseSchema: z.ZodObject<{
8
+ locationId: z.ZodString;
9
+ portalName: z.ZodOptional<z.ZodString>;
10
+ name: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ export type PortalSettingsResponse = z.infer<typeof PortalSettingsResponseSchema>;
13
+ export declare const VideoLicenseResponseSchema: z.ZodObject<{
14
+ url: z.ZodString;
15
+ token: z.ZodString;
16
+ }, z.core.$strip>;
17
+ export type VideoLicenseResponse = z.infer<typeof VideoLicenseResponseSchema>;
18
+ export declare const PostDetailsSchema: z.ZodObject<{
19
+ title: z.ZodOptional<z.ZodString>;
20
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
21
+ video: z.ZodOptional<z.ZodNullable<z.ZodObject<{
22
+ id: z.ZodOptional<z.ZodString>;
23
+ assetId: z.ZodOptional<z.ZodString>;
24
+ assetsLicenseId: z.ZodOptional<z.ZodString>;
25
+ url: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>>;
27
+ posterImage: z.ZodOptional<z.ZodNullable<z.ZodObject<{
28
+ assetId: z.ZodOptional<z.ZodString>;
29
+ url: z.ZodOptional<z.ZodString>;
30
+ }, z.core.$strip>>>;
31
+ contentBlock: z.ZodOptional<z.ZodArray<z.ZodObject<{
32
+ type: z.ZodString;
33
+ id: z.ZodOptional<z.ZodString>;
34
+ assetId: z.ZodOptional<z.ZodString>;
35
+ assetsLicenseId: z.ZodOptional<z.ZodString>;
36
+ url: z.ZodOptional<z.ZodString>;
37
+ }, z.core.$strip>>>;
38
+ materials: z.ZodOptional<z.ZodArray<z.ZodObject<{
39
+ id: z.ZodOptional<z.ZodString>;
40
+ name: z.ZodOptional<z.ZodString>;
41
+ url: z.ZodOptional<z.ZodString>;
42
+ type: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strip>>>;
44
+ post_materials: z.ZodOptional<z.ZodArray<z.ZodObject<{
45
+ id: z.ZodOptional<z.ZodString>;
46
+ name: z.ZodOptional<z.ZodString>;
47
+ url: z.ZodOptional<z.ZodString>;
48
+ type: z.ZodOptional<z.ZodString>;
49
+ }, z.core.$strip>>>;
50
+ }, z.core.$strip>;
51
+ export declare const PostDetailsResponseSchema: z.ZodObject<{
52
+ post: z.ZodOptional<z.ZodObject<{
53
+ title: z.ZodOptional<z.ZodString>;
54
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
+ video: z.ZodOptional<z.ZodNullable<z.ZodObject<{
56
+ id: z.ZodOptional<z.ZodString>;
57
+ assetId: z.ZodOptional<z.ZodString>;
58
+ assetsLicenseId: z.ZodOptional<z.ZodString>;
59
+ url: z.ZodOptional<z.ZodString>;
60
+ }, z.core.$strip>>>;
61
+ posterImage: z.ZodOptional<z.ZodNullable<z.ZodObject<{
62
+ assetId: z.ZodOptional<z.ZodString>;
63
+ url: z.ZodOptional<z.ZodString>;
64
+ }, z.core.$strip>>>;
65
+ contentBlock: z.ZodOptional<z.ZodArray<z.ZodObject<{
66
+ type: z.ZodString;
67
+ id: z.ZodOptional<z.ZodString>;
68
+ assetId: z.ZodOptional<z.ZodString>;
69
+ assetsLicenseId: z.ZodOptional<z.ZodString>;
70
+ url: z.ZodOptional<z.ZodString>;
71
+ }, z.core.$strip>>>;
72
+ materials: z.ZodOptional<z.ZodArray<z.ZodObject<{
73
+ id: z.ZodOptional<z.ZodString>;
74
+ name: z.ZodOptional<z.ZodString>;
75
+ url: z.ZodOptional<z.ZodString>;
76
+ type: z.ZodOptional<z.ZodString>;
77
+ }, z.core.$strip>>>;
78
+ post_materials: z.ZodOptional<z.ZodArray<z.ZodObject<{
79
+ id: z.ZodOptional<z.ZodString>;
80
+ name: z.ZodOptional<z.ZodString>;
81
+ url: z.ZodOptional<z.ZodString>;
82
+ type: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strip>>>;
84
+ }, z.core.$strip>>;
85
+ title: z.ZodOptional<z.ZodString>;
86
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
87
+ video: z.ZodOptional<z.ZodNullable<z.ZodObject<{
88
+ id: z.ZodOptional<z.ZodString>;
89
+ assetId: z.ZodOptional<z.ZodString>;
90
+ assetsLicenseId: z.ZodOptional<z.ZodString>;
91
+ url: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>>>;
93
+ posterImage: z.ZodOptional<z.ZodNullable<z.ZodObject<{
94
+ assetId: z.ZodOptional<z.ZodString>;
95
+ url: z.ZodOptional<z.ZodString>;
96
+ }, z.core.$strip>>>;
97
+ contentBlock: z.ZodOptional<z.ZodArray<z.ZodObject<{
98
+ type: z.ZodString;
99
+ id: z.ZodOptional<z.ZodString>;
100
+ assetId: z.ZodOptional<z.ZodString>;
101
+ assetsLicenseId: z.ZodOptional<z.ZodString>;
102
+ url: z.ZodOptional<z.ZodString>;
103
+ }, z.core.$strip>>>;
104
+ materials: z.ZodOptional<z.ZodArray<z.ZodObject<{
105
+ id: z.ZodOptional<z.ZodString>;
106
+ name: z.ZodOptional<z.ZodString>;
107
+ url: z.ZodOptional<z.ZodString>;
108
+ type: z.ZodOptional<z.ZodString>;
109
+ }, z.core.$strip>>>;
110
+ post_materials: z.ZodOptional<z.ZodArray<z.ZodObject<{
111
+ id: z.ZodOptional<z.ZodString>;
112
+ name: z.ZodOptional<z.ZodString>;
113
+ url: z.ZodOptional<z.ZodString>;
114
+ type: z.ZodOptional<z.ZodString>;
115
+ }, z.core.$strip>>>;
116
+ }, z.core.$strip>;
117
+ export type PostDetailsResponse = z.infer<typeof PostDetailsResponseSchema>;
118
+ export declare const CategorySchema: z.ZodObject<{
119
+ id: z.ZodString;
120
+ title: z.ZodString;
121
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
122
+ position: z.ZodOptional<z.ZodNumber>;
123
+ postCount: z.ZodOptional<z.ZodNumber>;
124
+ visibility: z.ZodOptional<z.ZodString>;
125
+ }, z.core.$strip>;
126
+ export declare const CategoriesResponseSchema: z.ZodObject<{
127
+ categories: z.ZodArray<z.ZodObject<{
128
+ id: z.ZodString;
129
+ title: z.ZodString;
130
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
+ position: z.ZodOptional<z.ZodNumber>;
132
+ postCount: z.ZodOptional<z.ZodNumber>;
133
+ visibility: z.ZodOptional<z.ZodString>;
134
+ }, z.core.$strip>>;
135
+ }, z.core.$strip>;
136
+ export type CategoriesResponse = z.infer<typeof CategoriesResponseSchema>;
137
+ export type Category = z.infer<typeof CategorySchema>;
138
+ export declare const PostSchema: z.ZodObject<{
139
+ id: z.ZodString;
140
+ title: z.ZodString;
141
+ indexPosition: z.ZodOptional<z.ZodNumber>;
142
+ visibility: z.ZodOptional<z.ZodString>;
143
+ }, z.core.$strip>;
144
+ export declare const PostsResponseSchema: z.ZodObject<{
145
+ category: z.ZodOptional<z.ZodObject<{
146
+ posts: z.ZodArray<z.ZodObject<{
147
+ id: z.ZodString;
148
+ title: z.ZodString;
149
+ indexPosition: z.ZodOptional<z.ZodNumber>;
150
+ visibility: z.ZodOptional<z.ZodString>;
151
+ }, z.core.$strip>>;
152
+ }, z.core.$strip>>;
153
+ }, z.core.$strip>;
154
+ export type PostsResponse = z.infer<typeof PostsResponseSchema>;
155
+ export type Post = z.infer<typeof PostSchema>;
156
+ export declare const ProductSchema: z.ZodObject<{
157
+ id: z.ZodOptional<z.ZodString>;
158
+ title: z.ZodString;
159
+ description: z.ZodOptional<z.ZodString>;
160
+ posterImage: z.ZodOptional<z.ZodNullable<z.ZodString>>;
161
+ instructor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
162
+ postCount: z.ZodOptional<z.ZodNumber>;
163
+ }, z.core.$strip>;
164
+ export declare const ProductResponseSchema: z.ZodObject<{
165
+ product: z.ZodOptional<z.ZodObject<{
166
+ id: z.ZodOptional<z.ZodString>;
167
+ title: z.ZodString;
168
+ description: z.ZodOptional<z.ZodString>;
169
+ posterImage: z.ZodOptional<z.ZodNullable<z.ZodString>>;
170
+ instructor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
171
+ postCount: z.ZodOptional<z.ZodNumber>;
172
+ }, z.core.$strip>>;
173
+ id: z.ZodOptional<z.ZodString>;
174
+ title: z.ZodOptional<z.ZodString>;
175
+ description: z.ZodOptional<z.ZodString>;
176
+ posterImage: z.ZodOptional<z.ZodNullable<z.ZodString>>;
177
+ instructor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
178
+ postCount: z.ZodOptional<z.ZodNumber>;
179
+ }, z.core.$strip>;
180
+ export type ProductResponse = z.infer<typeof ProductResponseSchema>;
181
+ export type Product = z.infer<typeof ProductSchema>;
182
+ /**
183
+ * Safely parses data with a Zod schema.
184
+ * Returns the parsed data or null if validation fails.
185
+ * Logs validation errors for debugging.
186
+ */
187
+ export declare function safeParse<T>(schema: z.ZodType<T>, data: unknown, context?: string): T | null;
188
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../src/scraper/highlevel/schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,0BAA0B,CAAC;AAMlC,eAAO,MAAM,4BAA4B;;;;iBAIvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAMlF,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAiC9E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ5B,CAAC;AAGH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAUpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAM5E,eAAO,MAAM,cAAc;;;;;;;iBAOzB,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAMtD,eAAO,MAAM,UAAU;;;;;iBAKrB,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;iBAM9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAM9C,eAAO,MAAM,aAAa;;;;;;;iBAOxB,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;iBAShC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAMpD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAW5F"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Zod schemas for HighLevel API responses.
3
+ * These provide runtime validation and type inference.
4
+ */
5
+ import { z } from "zod";
6
+ // Re-export Firebase auth types (Firebase is used by HighLevel for auth)
7
+ export { FirebaseAuthTokenSchema, } from "../../shared/firebase.js";
8
+ // ============================================================================
9
+ // Portal Settings API
10
+ // ============================================================================
11
+ export const PortalSettingsResponseSchema = z.object({
12
+ locationId: z.string(),
13
+ portalName: z.string().optional(),
14
+ name: z.string().optional(),
15
+ });
16
+ // ============================================================================
17
+ // Video License API
18
+ // ============================================================================
19
+ export const VideoLicenseResponseSchema = z.object({
20
+ url: z.string(),
21
+ token: z.string(),
22
+ });
23
+ // ============================================================================
24
+ // Post Details API
25
+ // ============================================================================
26
+ const VideoAssetSchema = z.object({
27
+ id: z.string().optional(),
28
+ assetId: z.string().optional(),
29
+ assetsLicenseId: z.string().optional(),
30
+ url: z.string().optional(),
31
+ });
32
+ const PosterImageSchema = z.object({
33
+ assetId: z.string().optional(),
34
+ url: z.string().optional(),
35
+ });
36
+ const ContentBlockSchema = z.object({
37
+ type: z.string(),
38
+ id: z.string().optional(),
39
+ assetId: z.string().optional(),
40
+ assetsLicenseId: z.string().optional(),
41
+ url: z.string().optional(),
42
+ });
43
+ const MaterialSchema = z.object({
44
+ id: z.string().optional(),
45
+ name: z.string().optional(),
46
+ url: z.string().optional(),
47
+ type: z.string().optional(),
48
+ });
49
+ export const PostDetailsSchema = z.object({
50
+ title: z.string().optional(),
51
+ description: z.string().nullable().optional(),
52
+ video: VideoAssetSchema.nullable().optional(),
53
+ posterImage: PosterImageSchema.nullable().optional(),
54
+ contentBlock: z.array(ContentBlockSchema).optional(),
55
+ materials: z.array(MaterialSchema).optional(),
56
+ post_materials: z.array(MaterialSchema).optional(),
57
+ });
58
+ // Response can have data directly or wrapped in "post"
59
+ export const PostDetailsResponseSchema = z.object({
60
+ post: PostDetailsSchema.optional(),
61
+ // Also allow all post fields directly on root
62
+ title: z.string().optional(),
63
+ description: z.string().nullable().optional(),
64
+ video: VideoAssetSchema.nullable().optional(),
65
+ posterImage: PosterImageSchema.nullable().optional(),
66
+ contentBlock: z.array(ContentBlockSchema).optional(),
67
+ materials: z.array(MaterialSchema).optional(),
68
+ post_materials: z.array(MaterialSchema).optional(),
69
+ });
70
+ // ============================================================================
71
+ // Categories API
72
+ // ============================================================================
73
+ export const CategorySchema = z.object({
74
+ id: z.string(),
75
+ title: z.string(),
76
+ description: z.string().nullable().optional(),
77
+ position: z.number().optional(),
78
+ postCount: z.number().optional(),
79
+ visibility: z.string().optional(),
80
+ });
81
+ export const CategoriesResponseSchema = z.object({
82
+ categories: z.array(CategorySchema),
83
+ });
84
+ // ============================================================================
85
+ // Posts (Lessons) API
86
+ // ============================================================================
87
+ export const PostSchema = z.object({
88
+ id: z.string(),
89
+ title: z.string(),
90
+ indexPosition: z.number().optional(),
91
+ visibility: z.string().optional(),
92
+ });
93
+ export const PostsResponseSchema = z.object({
94
+ category: z
95
+ .object({
96
+ posts: z.array(PostSchema),
97
+ })
98
+ .optional(),
99
+ });
100
+ // ============================================================================
101
+ // Product (Course) API
102
+ // ============================================================================
103
+ export const ProductSchema = z.object({
104
+ id: z.string().optional(),
105
+ title: z.string(),
106
+ description: z.string().optional(),
107
+ posterImage: z.string().nullable().optional(),
108
+ instructor: z.string().nullable().optional(),
109
+ postCount: z.number().optional(),
110
+ });
111
+ export const ProductResponseSchema = z.object({
112
+ product: ProductSchema.optional(),
113
+ // Also allow fields directly on root
114
+ id: z.string().optional(),
115
+ title: z.string().optional(),
116
+ description: z.string().optional(),
117
+ posterImage: z.string().nullable().optional(),
118
+ instructor: z.string().nullable().optional(),
119
+ postCount: z.number().optional(),
120
+ });
121
+ // ============================================================================
122
+ // Helper: Safe parse with logging
123
+ // ============================================================================
124
+ /**
125
+ * Safely parses data with a Zod schema.
126
+ * Returns the parsed data or null if validation fails.
127
+ * Logs validation errors for debugging.
128
+ */
129
+ export function safeParse(schema, data, context) {
130
+ const result = schema.safeParse(data);
131
+ if (result.success) {
132
+ return result.data;
133
+ }
134
+ if (context) {
135
+ console.warn(`[${context}] Validation failed:`, z.treeifyError(result.error));
136
+ }
137
+ return null;
138
+ }
139
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../../src/scraper/highlevel/schemas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { Page } from "playwright";
2
+ export interface CourseModule {
3
+ name: string;
4
+ slug: string;
5
+ url: string;
6
+ isLocked: boolean;
7
+ }
8
+ export interface Lesson {
9
+ name: string;
10
+ slug: string;
11
+ url: string;
12
+ index: number;
13
+ isLocked: boolean;
14
+ }
15
+ export interface CourseStructure {
16
+ name: string;
17
+ url: string;
18
+ modules: (CourseModule & {
19
+ lessons: Lesson[];
20
+ })[];
21
+ }
22
+ /**
23
+ * Extracts the course/community name from page data.
24
+ */
25
+ export declare function extractCourseName(page: Page): Promise<string>;
26
+ /**
27
+ * Extracts module data from the embedded JSON in the page.
28
+ * Skool embeds course structure as JSON in a script tag.
29
+ */
30
+ export declare function extractModulesFromJson(page: Page): Promise<CourseModule[]>;
31
+ /**
32
+ * Extracts lessons from a module page.
33
+ * Lessons are listed in the sidebar with links.
34
+ */
35
+ export declare function extractLessons(page: Page, moduleUrl: string): Promise<Lesson[]>;
36
+ /**
37
+ * Alternative: Extract modules from the classroom overview page links.
38
+ */
39
+ export declare function extractModulesFromPage(page: Page): Promise<CourseModule[]>;
40
+ /**
41
+ * Checks if a URL points to a specific module (has 8-char hex slug).
42
+ */
43
+ export declare function isModuleUrl(url: string): {
44
+ isModule: boolean;
45
+ moduleSlug: string | null;
46
+ };
47
+ /**
48
+ * Gets the classroom base URL (without module slug).
49
+ */
50
+ export declare function getClassroomBaseUrl(url: string): string;
51
+ /**
52
+ * Progress callback for buildCourseStructure.
53
+ */
54
+ export interface ScanProgress {
55
+ phase: "init" | "modules" | "lessons" | "done";
56
+ courseName?: string;
57
+ totalModules?: number;
58
+ currentModule?: string;
59
+ currentModuleIndex?: number;
60
+ lessonsFound?: number;
61
+ skippedLocked?: boolean;
62
+ }
63
+ /**
64
+ * Builds the complete course structure by crawling all modules and lessons.
65
+ */
66
+ export declare function buildCourseStructure(page: Page, classroomUrl: string, onProgress?: (progress: ScanProgress) => void): Promise<CourseStructure>;
67
+ export { slugify, createFolderName } from "../shared/slug.js";
68
+ //# sourceMappingURL=navigator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigator.d.ts","sourceRoot":"","sources":["../../src/scraper/navigator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAOvC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,CAAC,YAAY,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,EAAE,CAAC;CACnD;AAKD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAKnE;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAqEhF;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAkErF;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0ChF;AAGD;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAMzF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAGD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,GAC5C,OAAO,CAAC,eAAe,CAAC,CA0E1B;AAID,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,257 @@
1
+ import { parseNextData, extractModulesFromNextData, extractLessonAccessFromNextData, } from "./schemas.js";
2
+ // Browser automation - requires Playwright
3
+ /* v8 ignore start */
4
+ /**
5
+ * Extracts the course/community name from page data.
6
+ */
7
+ export async function extractCourseName(page) {
8
+ const title = await page.title();
9
+ // Title format: "Classroom · Community Name"
10
+ const match = /·\s*(.+)$/.exec(title);
11
+ return (match?.[1]?.trim() ?? title.replace("Classroom", "").trim()) || "Unknown Course";
12
+ }
13
+ /**
14
+ * Extracts module data from the embedded JSON in the page.
15
+ * Skool embeds course structure as JSON in a script tag.
16
+ */
17
+ export async function extractModulesFromJson(page) {
18
+ // Get the raw JSON from the page
19
+ const nextDataJson = await page.evaluate(() => {
20
+ const nextDataScript = document.getElementById("__NEXT_DATA__");
21
+ return nextDataScript?.textContent ?? null;
22
+ });
23
+ // Parse and validate with Zod schema (in Node context)
24
+ if (nextDataJson) {
25
+ const parsed = parseNextData(nextDataJson);
26
+ if (parsed) {
27
+ const skoolModules = extractModulesFromNextData(parsed);
28
+ if (skoolModules.length > 0) {
29
+ const baseUrl = page.url().split("/classroom")[0];
30
+ return skoolModules.map((m) => ({
31
+ name: m.title,
32
+ slug: m.slug,
33
+ url: `${baseUrl}/classroom/${m.slug}`,
34
+ isLocked: !m.hasAccess,
35
+ }));
36
+ }
37
+ }
38
+ }
39
+ // Fallback: Find script tags that contain course data (regex approach)
40
+ const modules = await page.evaluate(() => {
41
+ const scripts = Array.from(document.querySelectorAll("script"));
42
+ const results = [];
43
+ for (const script of scripts) {
44
+ const content = script.textContent ?? "";
45
+ // Look for module data pattern in the JSON
46
+ // Structure: "id":"...","name":"SLUG","metadata":{..."title":"TITLE"...}
47
+ // Pattern: "name":"8-char-hex" followed by "title":"..." within metadata
48
+ const modulePattern = /"name":"([a-f0-9]{8})","metadata":\{[^}]*"title":"([^"]+)"/g;
49
+ let match;
50
+ while ((match = modulePattern.exec(content)) !== null) {
51
+ const slug = match[1];
52
+ const title = match[2];
53
+ // Skip if already added
54
+ if (slug && title && !results.some((m) => m.slug === slug)) {
55
+ // Decode unicode escapes (e.g., \u0026 -> &)
56
+ const decodedTitle = title.replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
57
+ results.push({
58
+ name: decodedTitle,
59
+ slug,
60
+ url: "", // Will be set later
61
+ isLocked: false,
62
+ });
63
+ }
64
+ }
65
+ }
66
+ return results;
67
+ });
68
+ // Build URLs for each module
69
+ const baseUrl = page.url().split("/classroom")[0];
70
+ return modules.map((module) => ({
71
+ ...module,
72
+ url: `${baseUrl}/classroom/${module.slug}`,
73
+ }));
74
+ }
75
+ /**
76
+ * Extracts lessons from a module page.
77
+ * Lessons are listed in the sidebar with links.
78
+ */
79
+ export async function extractLessons(page, moduleUrl) {
80
+ const currentUrl = page.url();
81
+ const moduleBasePath = moduleUrl.split("?")[0] ?? moduleUrl;
82
+ if (!currentUrl.includes(moduleBasePath)) {
83
+ await page.goto(moduleUrl, { timeout: 30000 });
84
+ await page.waitForLoadState("domcontentloaded");
85
+ await page.waitForTimeout(2000);
86
+ }
87
+ // Get __NEXT_DATA__ and parse it in Node context
88
+ const nextDataJson = await page.evaluate(() => {
89
+ const nextDataScript = document.getElementById("__NEXT_DATA__");
90
+ return nextDataScript?.textContent ?? null;
91
+ });
92
+ // Build access map from validated data
93
+ let accessMap = new Map();
94
+ if (nextDataJson) {
95
+ const parsed = parseNextData(nextDataJson);
96
+ if (parsed) {
97
+ accessMap = extractLessonAccessFromNextData(parsed);
98
+ }
99
+ }
100
+ // Extract lesson links from DOM
101
+ const lessonData = await page.evaluate(() => {
102
+ const results = [];
103
+ // Skool uses styled-components with "ChildrenLink" in the class name
104
+ const lessonLinks = document.querySelectorAll('a[class*="ChildrenLink"]');
105
+ lessonLinks.forEach((link, index) => {
106
+ const anchor = link;
107
+ const href = anchor.href;
108
+ const name = anchor.textContent?.trim() ?? `Lesson ${index + 1}`;
109
+ // Extract lesson ID from URL (?md=...)
110
+ const urlParams = new URL(href).searchParams;
111
+ const lessonId = urlParams.get("md") ?? "";
112
+ if (lessonId && !results.some((l) => l.slug === lessonId)) {
113
+ results.push({ name, slug: lessonId, href });
114
+ }
115
+ });
116
+ return results;
117
+ });
118
+ // Build final lesson list with access info
119
+ return lessonData.map((lesson, index) => {
120
+ let isLocked = false;
121
+ // Check access map from __NEXT_DATA__
122
+ if (accessMap.has(lesson.slug)) {
123
+ isLocked = !accessMap.get(lesson.slug);
124
+ }
125
+ return {
126
+ name: lesson.name,
127
+ slug: lesson.slug,
128
+ url: lesson.href,
129
+ index,
130
+ isLocked,
131
+ };
132
+ });
133
+ }
134
+ /**
135
+ * Alternative: Extract modules from the classroom overview page links.
136
+ */
137
+ export async function extractModulesFromPage(page) {
138
+ await page.waitForTimeout(1000);
139
+ const modules = await page.evaluate(() => {
140
+ // Look for module cards - they're usually divs/links with course images
141
+ const moduleCards = document.querySelectorAll('a[href*="/classroom/"]');
142
+ const results = [];
143
+ const seen = new Set();
144
+ moduleCards.forEach((card) => {
145
+ const anchor = card;
146
+ const href = anchor.href;
147
+ // Extract slug from URL (8 character hex string)
148
+ const slugMatch = /\/classroom\/([a-f0-9]{8})(?:\?|$)/.exec(href);
149
+ if (!slugMatch?.[1])
150
+ return;
151
+ const slug = slugMatch[1];
152
+ if (seen.has(slug))
153
+ return;
154
+ seen.add(slug);
155
+ // Find title - could be in various child elements
156
+ const titleEl = card.querySelector("h3, h4, [class*='title'], [class*='Title']") ??
157
+ card.querySelector("div > div > div");
158
+ const name = titleEl?.textContent?.trim() ?? `Module ${results.length + 1}`;
159
+ // Check for lock icon
160
+ const isLocked = card.querySelector('[class*="lock"], [class*="Lock"]') !== null;
161
+ results.push({
162
+ name,
163
+ slug,
164
+ url: href,
165
+ isLocked,
166
+ });
167
+ });
168
+ return results;
169
+ });
170
+ return modules;
171
+ }
172
+ /* v8 ignore stop */
173
+ /**
174
+ * Checks if a URL points to a specific module (has 8-char hex slug).
175
+ */
176
+ export function isModuleUrl(url) {
177
+ const match = /\/classroom\/([a-f0-9]{8})(?:\?|$)/.exec(url);
178
+ return {
179
+ isModule: !!match,
180
+ moduleSlug: match?.[1] ?? null,
181
+ };
182
+ }
183
+ /**
184
+ * Gets the classroom base URL (without module slug).
185
+ */
186
+ export function getClassroomBaseUrl(url) {
187
+ // Remove module slug and query params
188
+ return url.replace(/\/classroom\/[a-f0-9]{8}.*$/, "/classroom");
189
+ }
190
+ /* v8 ignore start */
191
+ /**
192
+ * Builds the complete course structure by crawling all modules and lessons.
193
+ */
194
+ export async function buildCourseStructure(page, classroomUrl, onProgress) {
195
+ const { isModule, moduleSlug } = isModuleUrl(classroomUrl);
196
+ // If URL points to a specific module, get the base classroom URL first
197
+ const baseClassroomUrl = isModule ? getClassroomBaseUrl(classroomUrl) : classroomUrl;
198
+ // Navigate to the classroom overview to get all modules
199
+ await page.goto(baseClassroomUrl, { timeout: 30000 });
200
+ await page.waitForLoadState("domcontentloaded");
201
+ await page.waitForTimeout(2000);
202
+ const courseName = await extractCourseName(page);
203
+ onProgress?.({ phase: "init", courseName });
204
+ // Try JSON extraction first (more reliable), fall back to page scraping
205
+ let modules = await extractModulesFromJson(page);
206
+ if (modules.length === 0) {
207
+ modules = await extractModulesFromPage(page);
208
+ }
209
+ // If user specified a specific module, filter to just that one
210
+ if (isModule && moduleSlug) {
211
+ const targetModule = modules.find((m) => m.slug === moduleSlug);
212
+ if (targetModule) {
213
+ modules = [targetModule];
214
+ }
215
+ }
216
+ onProgress?.({ phase: "modules", totalModules: modules.length });
217
+ const modulesWithLessons = [];
218
+ for (const [i, module] of modules.entries()) {
219
+ if (module.isLocked) {
220
+ onProgress?.({
221
+ phase: "lessons",
222
+ currentModule: module.name,
223
+ currentModuleIndex: i,
224
+ skippedLocked: true,
225
+ });
226
+ continue;
227
+ }
228
+ onProgress?.({
229
+ phase: "lessons",
230
+ currentModule: module.name,
231
+ currentModuleIndex: i,
232
+ });
233
+ if (module.url) {
234
+ const lessons = await extractLessons(page, module.url);
235
+ onProgress?.({
236
+ phase: "lessons",
237
+ currentModule: module.name,
238
+ currentModuleIndex: i,
239
+ lessonsFound: lessons.length,
240
+ });
241
+ modulesWithLessons.push({
242
+ ...module,
243
+ lessons,
244
+ });
245
+ }
246
+ }
247
+ onProgress?.({ phase: "done" });
248
+ return {
249
+ name: courseName,
250
+ url: baseClassroomUrl,
251
+ modules: modulesWithLessons,
252
+ };
253
+ }
254
+ /* v8 ignore stop */
255
+ // Re-export shared utilities for backwards compatibility
256
+ export { slugify, createFolderName } from "../shared/slug.js";
257
+ //# sourceMappingURL=navigator.js.map