offcourse 0.0.1 → 1.0.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.
- package/.github/workflows/ci.yml +50 -0
- package/.husky/commit-msg +2 -0
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +3 -0
- package/.prettierrc +8 -0
- package/.release-it.json +23 -0
- package/ARCHITECTURE.md +233 -0
- package/CHANGELOG.md +78 -0
- package/README.md +256 -16
- package/commitlint.config.js +4 -0
- package/dist/ai/openRouter.d.ts +47 -0
- package/dist/ai/openRouter.d.ts.map +1 -0
- package/dist/ai/openRouter.js +116 -0
- package/dist/ai/openRouter.js.map +1 -0
- package/dist/ai/transcriptPolisher.d.ts +24 -0
- package/dist/ai/transcriptPolisher.d.ts.map +1 -0
- package/dist/ai/transcriptPolisher.js +89 -0
- package/dist/ai/transcriptPolisher.js.map +1 -0
- package/dist/cli/commands/config.d.ts +13 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +66 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/enrich.d.ts +14 -0
- package/dist/cli/commands/enrich.d.ts.map +1 -0
- package/dist/cli/commands/enrich.js +271 -0
- package/dist/cli/commands/enrich.js.map +1 -0
- package/dist/cli/commands/inspect.d.ts +11 -0
- package/dist/cli/commands/inspect.d.ts.map +1 -0
- package/dist/cli/commands/inspect.js +365 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/login.d.ts +12 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +55 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/status.d.ts +15 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +118 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +16 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +922 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/syncGhl.d.ts +20 -0
- package/dist/cli/commands/syncGhl.d.ts.map +1 -0
- package/dist/cli/commands/syncGhl.js +483 -0
- package/dist/cli/commands/syncGhl.js.map +1 -0
- package/dist/cli/commands/syncHighLevel.d.ts +24 -0
- package/dist/cli/commands/syncHighLevel.d.ts.map +1 -0
- package/dist/cli/commands/syncHighLevel.js +483 -0
- package/dist/cli/commands/syncHighLevel.js.map +1 -0
- package/dist/cli/commands/syncHighLevel.test.d.ts +2 -0
- package/dist/cli/commands/syncHighLevel.test.d.ts.map +1 -0
- package/dist/cli/commands/syncHighLevel.test.js +102 -0
- package/dist/cli/commands/syncHighLevel.test.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/configManager.d.ts +31 -0
- package/dist/config/configManager.d.ts.map +1 -0
- package/dist/config/configManager.js +64 -0
- package/dist/config/configManager.js.map +1 -0
- package/dist/config/paths.d.ts +21 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/paths.test.d.ts +2 -0
- package/dist/config/paths.test.d.ts.map +1 -0
- package/dist/config/paths.test.js +70 -0
- package/dist/config/paths.test.js.map +1 -0
- package/dist/config/schema.d.ts +60 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +50 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/schema.test.d.ts +2 -0
- package/dist/config/schema.test.d.ts.map +1 -0
- package/dist/config/schema.test.js +151 -0
- package/dist/config/schema.test.js.map +1 -0
- package/dist/downloader/hlsDownloader.d.ts +58 -0
- package/dist/downloader/hlsDownloader.d.ts.map +1 -0
- package/dist/downloader/hlsDownloader.js +254 -0
- package/dist/downloader/hlsDownloader.js.map +1 -0
- package/dist/downloader/hlsDownloader.test.d.ts +2 -0
- package/dist/downloader/hlsDownloader.test.d.ts.map +1 -0
- package/dist/downloader/hlsDownloader.test.js +116 -0
- package/dist/downloader/hlsDownloader.test.js.map +1 -0
- package/dist/downloader/hlsValidator.d.ts +35 -0
- package/dist/downloader/hlsValidator.d.ts.map +1 -0
- package/dist/downloader/hlsValidator.js +148 -0
- package/dist/downloader/hlsValidator.js.map +1 -0
- package/dist/downloader/index.d.ts +26 -0
- package/dist/downloader/index.d.ts.map +1 -0
- package/dist/downloader/index.js +52 -0
- package/dist/downloader/index.js.map +1 -0
- package/dist/downloader/loomDownloader.d.ts +56 -0
- package/dist/downloader/loomDownloader.d.ts.map +1 -0
- package/dist/downloader/loomDownloader.js +559 -0
- package/dist/downloader/loomDownloader.js.map +1 -0
- package/dist/downloader/loomDownloader.test.d.ts +2 -0
- package/dist/downloader/loomDownloader.test.d.ts.map +1 -0
- package/dist/downloader/loomDownloader.test.js +36 -0
- package/dist/downloader/loomDownloader.test.js.map +1 -0
- package/dist/downloader/queue.d.ts +56 -0
- package/dist/downloader/queue.d.ts.map +1 -0
- package/dist/downloader/queue.js +88 -0
- package/dist/downloader/queue.js.map +1 -0
- package/dist/downloader/queue.test.d.ts +2 -0
- package/dist/downloader/queue.test.d.ts.map +1 -0
- package/dist/downloader/queue.test.js +158 -0
- package/dist/downloader/queue.test.js.map +1 -0
- package/dist/downloader/videoDownloader.d.ts +32 -0
- package/dist/downloader/videoDownloader.d.ts.map +1 -0
- package/dist/downloader/videoDownloader.js +173 -0
- package/dist/downloader/videoDownloader.js.map +1 -0
- package/dist/downloader/vimeoDownloader.d.ts +52 -0
- package/dist/downloader/vimeoDownloader.d.ts.map +1 -0
- package/dist/downloader/vimeoDownloader.js +565 -0
- package/dist/downloader/vimeoDownloader.js.map +1 -0
- package/dist/downloader/vimeoDownloader.test.d.ts +2 -0
- package/dist/downloader/vimeoDownloader.test.d.ts.map +1 -0
- package/dist/downloader/vimeoDownloader.test.js +51 -0
- package/dist/downloader/vimeoDownloader.test.js.map +1 -0
- package/dist/scraper/auth.d.ts +29 -0
- package/dist/scraper/auth.d.ts.map +1 -0
- package/dist/scraper/auth.js +115 -0
- package/dist/scraper/auth.js.map +1 -0
- package/dist/scraper/extractor.d.ts +49 -0
- package/dist/scraper/extractor.d.ts.map +1 -0
- package/dist/scraper/extractor.js +627 -0
- package/dist/scraper/extractor.js.map +1 -0
- package/dist/scraper/extractor.test.d.ts +2 -0
- package/dist/scraper/extractor.test.d.ts.map +1 -0
- package/dist/scraper/extractor.test.js +65 -0
- package/dist/scraper/extractor.test.js.map +1 -0
- package/dist/scraper/ghl/auth.d.ts +25 -0
- package/dist/scraper/ghl/auth.d.ts.map +1 -0
- package/dist/scraper/ghl/auth.js +187 -0
- package/dist/scraper/ghl/auth.js.map +1 -0
- package/dist/scraper/ghl/extractor.d.ts +96 -0
- package/dist/scraper/ghl/extractor.d.ts.map +1 -0
- package/dist/scraper/ghl/extractor.js +345 -0
- package/dist/scraper/ghl/extractor.js.map +1 -0
- package/dist/scraper/ghl/index.d.ts +4 -0
- package/dist/scraper/ghl/index.d.ts.map +1 -0
- package/dist/scraper/ghl/index.js +4 -0
- package/dist/scraper/ghl/index.js.map +1 -0
- package/dist/scraper/ghl/navigator.d.ts +93 -0
- package/dist/scraper/ghl/navigator.d.ts.map +1 -0
- package/dist/scraper/ghl/navigator.js +447 -0
- package/dist/scraper/ghl/navigator.js.map +1 -0
- package/dist/scraper/highlevel/auth.d.ts +25 -0
- package/dist/scraper/highlevel/auth.d.ts.map +1 -0
- package/dist/scraper/highlevel/auth.js +189 -0
- package/dist/scraper/highlevel/auth.js.map +1 -0
- package/dist/scraper/highlevel/extractor.d.ts +97 -0
- package/dist/scraper/highlevel/extractor.d.ts.map +1 -0
- package/dist/scraper/highlevel/extractor.js +386 -0
- package/dist/scraper/highlevel/extractor.js.map +1 -0
- package/dist/scraper/highlevel/extractor.test.d.ts +2 -0
- package/dist/scraper/highlevel/extractor.test.d.ts.map +1 -0
- package/dist/scraper/highlevel/extractor.test.js +101 -0
- package/dist/scraper/highlevel/extractor.test.js.map +1 -0
- package/dist/scraper/highlevel/index.d.ts +3 -0
- package/dist/scraper/highlevel/index.d.ts.map +1 -0
- package/dist/scraper/highlevel/index.js +3 -0
- package/dist/scraper/highlevel/index.js.map +1 -0
- package/dist/scraper/highlevel/navigator.d.ts +93 -0
- package/dist/scraper/highlevel/navigator.d.ts.map +1 -0
- package/dist/scraper/highlevel/navigator.js +492 -0
- package/dist/scraper/highlevel/navigator.js.map +1 -0
- package/dist/scraper/highlevel/navigator.test.d.ts +2 -0
- package/dist/scraper/highlevel/navigator.test.d.ts.map +1 -0
- package/dist/scraper/highlevel/navigator.test.js +78 -0
- package/dist/scraper/highlevel/navigator.test.js.map +1 -0
- package/dist/scraper/navigator.d.ts +65 -0
- package/dist/scraper/navigator.d.ts.map +1 -0
- package/dist/scraper/navigator.js +300 -0
- package/dist/scraper/navigator.js.map +1 -0
- package/dist/scraper/navigator.test.d.ts +2 -0
- package/dist/scraper/navigator.test.d.ts.map +1 -0
- package/dist/scraper/navigator.test.js +63 -0
- package/dist/scraper/navigator.test.js.map +1 -0
- package/dist/scraper/skoolApi.d.ts +17 -0
- package/dist/scraper/skoolApi.d.ts.map +1 -0
- package/dist/scraper/skoolApi.js +72 -0
- package/dist/scraper/skoolApi.js.map +1 -0
- package/dist/scraper/videoInterceptor.d.ts +19 -0
- package/dist/scraper/videoInterceptor.d.ts.map +1 -0
- package/dist/scraper/videoInterceptor.js +315 -0
- package/dist/scraper/videoInterceptor.js.map +1 -0
- package/dist/shared/auth.d.ts +58 -0
- package/dist/shared/auth.d.ts.map +1 -0
- package/dist/shared/auth.js +211 -0
- package/dist/shared/auth.js.map +1 -0
- package/dist/shared/fs.d.ts +31 -0
- package/dist/shared/fs.d.ts.map +1 -0
- package/dist/shared/fs.js +73 -0
- package/dist/shared/fs.js.map +1 -0
- package/dist/shared/http.d.ts +15 -0
- package/dist/shared/http.d.ts.map +1 -0
- package/dist/shared/http.js +31 -0
- package/dist/shared/http.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/state/database.d.ts +245 -0
- package/dist/state/database.d.ts.map +1 -0
- package/dist/state/database.js +676 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/database.test.d.ts +2 -0
- package/dist/state/database.test.d.ts.map +1 -0
- package/dist/state/database.test.js +34 -0
- package/dist/state/database.test.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +2 -0
- package/dist/state/index.js.map +1 -0
- package/dist/storage/fileSystem.d.ts +56 -0
- package/dist/storage/fileSystem.d.ts.map +1 -0
- package/dist/storage/fileSystem.js +121 -0
- package/dist/storage/fileSystem.js.map +1 -0
- package/dist/transcription/whisperService.d.ts +27 -0
- package/dist/transcription/whisperService.d.ts.map +1 -0
- package/dist/transcription/whisperService.js +102 -0
- package/dist/transcription/whisperService.js.map +1 -0
- package/eslint.config.js +55 -0
- package/package.json +68 -11
- package/src/__fixtures__/highlevel-post-response.json +68 -0
- package/src/__fixtures__/hls-master-playlist.m3u8 +24 -0
- package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +38 -0
- package/src/cli/commands/config.ts +74 -0
- package/src/cli/commands/inspect.ts +441 -0
- package/src/cli/commands/login.ts +68 -0
- package/src/cli/commands/status.ts +147 -0
- package/src/cli/commands/sync.ts +1235 -0
- package/src/cli/commands/syncHighLevel.test.ts +144 -0
- package/src/cli/commands/syncHighLevel.ts +639 -0
- package/src/cli/index.ts +121 -0
- package/src/config/configManager.ts +75 -0
- package/src/config/paths.test.ts +83 -0
- package/src/config/paths.ts +36 -0
- package/src/config/schema.test.ts +173 -0
- package/src/config/schema.ts +65 -0
- package/src/downloader/hlsDownloader.test.ts +148 -0
- package/src/downloader/hlsDownloader.ts +327 -0
- package/src/downloader/hlsValidator.ts +196 -0
- package/src/downloader/index.ts +122 -0
- package/src/downloader/loomDownloader.test.ts +43 -0
- package/src/downloader/loomDownloader.ts +742 -0
- package/src/downloader/queue.test.ts +199 -0
- package/src/downloader/queue.ts +118 -0
- package/src/downloader/vimeoDownloader.test.ts +62 -0
- package/src/downloader/vimeoDownloader.ts +722 -0
- package/src/scraper/extractor.test.ts +124 -0
- package/src/scraper/extractor.ts +757 -0
- package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +41 -0
- package/src/scraper/highlevel/extractor.test.ts +134 -0
- package/src/scraper/highlevel/extractor.ts +537 -0
- package/src/scraper/highlevel/index.ts +2 -0
- package/src/scraper/highlevel/navigator.test.ts +110 -0
- package/src/scraper/highlevel/navigator.ts +668 -0
- package/src/scraper/highlevel/schemas.ts +183 -0
- package/src/scraper/navigator.test.ts +122 -0
- package/src/scraper/navigator.ts +355 -0
- package/src/scraper/schemas.ts +177 -0
- package/src/scraper/videoInterceptor.ts +435 -0
- package/src/shared/auth.test.ts +58 -0
- package/src/shared/auth.ts +251 -0
- package/src/shared/firebase.ts +151 -0
- package/src/shared/fs.ts +80 -0
- package/src/shared/http.ts +34 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/slug.ts +26 -0
- package/src/shared/url.test.ts +122 -0
- package/src/shared/url.ts +57 -0
- package/src/state/database.test.ts +49 -0
- package/src/state/database.ts +919 -0
- package/src/state/index.ts +14 -0
- package/src/storage/fileSystem.test.ts +64 -0
- package/src/storage/fileSystem.ts +175 -0
- package/tsconfig.json +28 -0
- package/vitest.config.ts +29 -0
- package/cli.js +0 -45
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
import { getSessionPath, SESSIONS_DIR } from "../../config/paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a valid HighLevel session exists for the given domain.
|
|
7
|
+
*/
|
|
8
|
+
export function hasValidHighLevelSession(domain) {
|
|
9
|
+
const sessionPath = getSessionPath(domain);
|
|
10
|
+
return existsSync(sessionPath);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Loads an existing session from disk.
|
|
14
|
+
*/
|
|
15
|
+
async function loadSession(browser, domain) {
|
|
16
|
+
const sessionPath = getSessionPath(domain);
|
|
17
|
+
const storageState = JSON.parse(readFileSync(sessionPath, "utf-8"));
|
|
18
|
+
return browser.newContext({ storageState });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Saves the current session to disk.
|
|
22
|
+
*/
|
|
23
|
+
async function saveSession(context, domain) {
|
|
24
|
+
const sessionPath = getSessionPath(domain);
|
|
25
|
+
const dir = dirname(sessionPath);
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
const storageState = await context.storageState();
|
|
30
|
+
writeFileSync(sessionPath, JSON.stringify(storageState, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks if the current page is on a HighLevel login page.
|
|
34
|
+
*/
|
|
35
|
+
function isHighLevelLoginPage(url) {
|
|
36
|
+
const loginPatterns = [
|
|
37
|
+
/sso\.clientclub\.net/,
|
|
38
|
+
/\/login/,
|
|
39
|
+
/\/signin/,
|
|
40
|
+
/\/auth/,
|
|
41
|
+
/accounts\.google\.com/,
|
|
42
|
+
/firebaseapp\.com/,
|
|
43
|
+
];
|
|
44
|
+
return loginPatterns.some((p) => p.test(url));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks if the page has a valid Firebase auth token.
|
|
48
|
+
*/
|
|
49
|
+
async function hasValidFirebaseToken(page) {
|
|
50
|
+
try {
|
|
51
|
+
const hasToken = await page.evaluate(() => {
|
|
52
|
+
const tokenKey = Object.keys(localStorage).find((k) => k.includes("firebase:authUser"));
|
|
53
|
+
if (!tokenKey)
|
|
54
|
+
return false;
|
|
55
|
+
const tokenData = JSON.parse(localStorage.getItem(tokenKey) ?? "{}");
|
|
56
|
+
const expirationTime = tokenData?.stsTokenManager?.expirationTime;
|
|
57
|
+
// Check if token exists and is not expired
|
|
58
|
+
if (expirationTime) {
|
|
59
|
+
return Date.now() < expirationTime;
|
|
60
|
+
}
|
|
61
|
+
return !!tokenData?.stsTokenManager?.accessToken;
|
|
62
|
+
});
|
|
63
|
+
return hasToken;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Performs interactive login for HighLevel by opening a browser window.
|
|
71
|
+
* The user logs in manually, and we capture the session.
|
|
72
|
+
*/
|
|
73
|
+
export async function performHighLevelInteractiveLogin(domain, portalUrl) {
|
|
74
|
+
// Ensure sessions directory exists
|
|
75
|
+
if (!existsSync(SESSIONS_DIR)) {
|
|
76
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
const browser = await chromium.launch({
|
|
79
|
+
headless: false, // Must be visible for user interaction
|
|
80
|
+
});
|
|
81
|
+
const context = await browser.newContext({
|
|
82
|
+
viewport: { width: 1280, height: 800 },
|
|
83
|
+
});
|
|
84
|
+
const page = await context.newPage();
|
|
85
|
+
await page.goto(portalUrl);
|
|
86
|
+
console.log("\n🔐 Browser opened. Please log in manually.");
|
|
87
|
+
console.log(" The window will close automatically after successful login.\n");
|
|
88
|
+
// Wait for either:
|
|
89
|
+
// 1. Navigation away from login page
|
|
90
|
+
// 2. Firebase token to appear in localStorage
|
|
91
|
+
let loggedIn = false;
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
const timeout = 300000; // 5 minutes
|
|
94
|
+
while (!loggedIn && Date.now() - startTime < timeout) {
|
|
95
|
+
await page.waitForTimeout(1000);
|
|
96
|
+
const currentUrl = page.url();
|
|
97
|
+
// Check if we're still on a login page
|
|
98
|
+
if (!isHighLevelLoginPage(currentUrl)) {
|
|
99
|
+
// Might be logged in, check for Firebase token
|
|
100
|
+
const hasToken = await hasValidFirebaseToken(page);
|
|
101
|
+
if (hasToken) {
|
|
102
|
+
loggedIn = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
// Also check if we're on a course page (successful login)
|
|
106
|
+
if (currentUrl.includes("/courses/") || currentUrl.includes("/library")) {
|
|
107
|
+
loggedIn = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!loggedIn) {
|
|
113
|
+
await browser.close();
|
|
114
|
+
throw new Error("Login timed out after 5 minutes");
|
|
115
|
+
}
|
|
116
|
+
// Give the page a moment to fully load after login
|
|
117
|
+
await page.waitForLoadState("networkidle").catch(() => { });
|
|
118
|
+
await page.waitForTimeout(2000);
|
|
119
|
+
// Save the session
|
|
120
|
+
await saveSession(context, domain);
|
|
121
|
+
console.log("✅ Login successful! Session saved.\n");
|
|
122
|
+
return { context, page };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Gets an authenticated HighLevel session, either from cache or via interactive login.
|
|
126
|
+
*/
|
|
127
|
+
export async function getHighLevelAuthenticatedSession(domain, portalUrl, options = {}) {
|
|
128
|
+
// Default to headless mode (true) unless explicitly set to false
|
|
129
|
+
const useHeadless = options.headless !== false;
|
|
130
|
+
const browser = await chromium.launch({
|
|
131
|
+
headless: useHeadless,
|
|
132
|
+
});
|
|
133
|
+
// Try to use existing session
|
|
134
|
+
if (!options.forceLogin && hasValidHighLevelSession(domain)) {
|
|
135
|
+
try {
|
|
136
|
+
const context = await loadSession(browser, domain);
|
|
137
|
+
const page = await context.newPage();
|
|
138
|
+
// Navigate to portal
|
|
139
|
+
await page.goto(portalUrl);
|
|
140
|
+
await page.waitForLoadState("domcontentloaded");
|
|
141
|
+
await page.waitForTimeout(2000);
|
|
142
|
+
const currentUrl = page.url();
|
|
143
|
+
// Check if we got redirected to login or SSO
|
|
144
|
+
if (isHighLevelLoginPage(currentUrl)) {
|
|
145
|
+
console.log("⚠️ Session expired, need to re-login...");
|
|
146
|
+
await context.close();
|
|
147
|
+
await browser.close();
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Verify we have a valid Firebase token
|
|
151
|
+
const hasToken = await hasValidFirebaseToken(page);
|
|
152
|
+
if (hasToken) {
|
|
153
|
+
console.log("✅ Using cached session");
|
|
154
|
+
return { browser, session: { context, page } };
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log("⚠️ No valid auth token, need to re-login...");
|
|
158
|
+
await context.close();
|
|
159
|
+
await browser.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.log("⚠️ Failed to load session, need to re-login...", error);
|
|
165
|
+
await browser.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
await browser.close();
|
|
170
|
+
}
|
|
171
|
+
// Need fresh login - always visible for interactive login
|
|
172
|
+
const session = await performHighLevelInteractiveLogin(domain, portalUrl);
|
|
173
|
+
// Get the browser from the session context
|
|
174
|
+
const sessionBrowser = session.context.browser();
|
|
175
|
+
if (!sessionBrowser) {
|
|
176
|
+
throw new Error("Failed to get browser from session");
|
|
177
|
+
}
|
|
178
|
+
// After login, reopen with headless browser (unless explicitly set to false)
|
|
179
|
+
if (useHeadless) {
|
|
180
|
+
const newBrowser = await chromium.launch({ headless: true });
|
|
181
|
+
const context = await loadSession(newBrowser, domain);
|
|
182
|
+
const page = await context.newPage();
|
|
183
|
+
// Close the interactive session
|
|
184
|
+
await sessionBrowser.close();
|
|
185
|
+
return { browser: newBrowser, session: { context, page } };
|
|
186
|
+
}
|
|
187
|
+
return { browser: sessionBrowser, session };
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/scraper/highlevel/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAOrE;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAc;IACrD,MAAM,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC;AACjC,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,WAAW,CAAC,OAAgB,EAAE,MAAc;IACzD,MAAM,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;IACpE,OAAO,OAAO,CAAC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,WAAW,CAAC,OAAuB,EAAE,MAAc;IAChE,MAAM,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAEjC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IAClD,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC7E,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,GAAW;IACvC,MAAM,aAAa,GAAG;QACpB,sBAAsB;QACtB,SAAS;QACT,UAAU;QACV,QAAQ;QACR,uBAAuB;QACvB,kBAAkB;KACnB,CAAC;IACF,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,qBAAqB,CAAC,IAAU;IAC7C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC;YACxF,IAAI,CAAC,QAAQ;gBAAE,OAAO,KAAK,CAAC;YAE5B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;YACrE,MAAM,cAAc,GAAG,SAAS,EAAE,eAAe,EAAE,cAAc,CAAC;YAElE,2CAA2C;YAC3C,IAAI,cAAc,EAAE,CAAC;gBACnB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC;YACrC,CAAC;YAED,OAAO,CAAC,CAAC,SAAS,EAAE,eAAe,EAAE,WAAW,CAAC;QACnD,CAAC,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,MAAc,EACd,SAAiB;IAEjB,mCAAmC;IACnC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;QACpC,QAAQ,EAAE,KAAK,EAAE,uCAAuC;KACzD,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;QACvC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;KACvC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IACrC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAE3B,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;IAC5D,OAAO,CAAC,GAAG,CAAC,kEAAkE,CAAC,CAAC;IAEhF,mBAAmB;IACnB,qCAAqC;IACrC,8CAA8C;IAC9C,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,YAAY;IAEpC,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,OAAO,EAAE,CAAC;QACrD,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAEhC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAE9B,uCAAuC;QACvC,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,EAAE,CAAC;YACtC,+CAA+C;YAC/C,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACnD,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,GAAG,IAAI,CAAC;gBAChB,MAAM;YACR,CAAC;YAED,0DAA0D;YAC1D,IAAI,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBACxE,QAAQ,GAAG,IAAI,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IAED,mDAAmD;IACnD,MAAM,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC3D,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAEhC,mBAAmB;IACnB,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;IAEpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,MAAc,EACd,SAAiB,EACjB,UAAwD,EAAE;IAE1D,iEAAiE;IACjE,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,KAAK,KAAK,CAAC;IAE/C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;QACpC,QAAQ,EAAE,WAAW;KACtB,CAAC,CAAC;IAEH,8BAA8B;IAC9B,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,wBAAwB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5D,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACnD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YAErC,qBAAqB;YACrB,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3B,MAAM,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;YAChD,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YAEhC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE9B,6CAA6C;YAC7C,IAAI,oBAAoB,CAAC,UAAU,CAAC,EAAE,CAAC;gBACrC,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;gBACxD,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;gBACtB,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACxB,CAAC;iBAAM,CAAC;gBACN,wCAAwC;gBACxC,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,IAAI,CAAC,CAAC;gBACnD,IAAI,QAAQ,EAAE,CAAC;oBACb,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;oBACtC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;gBACjD,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;oBAC5D,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;oBACtB,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;gBACxB,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,iDAAiD,EAAE,KAAK,CAAC,CAAC;YACtE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED,0DAA0D;IAC1D,MAAM,OAAO,GAAG,MAAM,gCAAgC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAE1E,2CAA2C;IAC3C,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IACjD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;IAED,6EAA6E;IAC7E,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAErC,gCAAgC;QAChC,MAAM,cAAc,CAAC,KAAK,EAAE,CAAC;QAE7B,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;IAC7D,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
export interface HighLevelVideoInfo {
|
|
3
|
+
type: "hls" | "vimeo" | "loom" | "youtube" | "custom";
|
|
4
|
+
url: string;
|
|
5
|
+
masterPlaylistUrl?: string;
|
|
6
|
+
qualities?: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
url: string;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
}>;
|
|
12
|
+
duration?: number;
|
|
13
|
+
thumbnailUrl?: string;
|
|
14
|
+
token?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface HighLevelPostContent {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string | null;
|
|
20
|
+
htmlContent: string | null;
|
|
21
|
+
video: HighLevelVideoInfo | null;
|
|
22
|
+
attachments: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
url: string;
|
|
26
|
+
type: string;
|
|
27
|
+
size?: number;
|
|
28
|
+
}>;
|
|
29
|
+
categoryId: string;
|
|
30
|
+
productId: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extracts the Firebase auth token from the page.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getAuthToken(page: Page): Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Extracts video info from a HighLevel post page by intercepting network requests.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractVideoFromPage(page: Page): Promise<HighLevelVideoInfo | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Extracts video info by intercepting network requests during page load.
|
|
42
|
+
*/
|
|
43
|
+
export declare function interceptVideoRequests(page: Page, postUrl: string): Promise<HighLevelVideoInfo | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Fetches post details from the API.
|
|
46
|
+
*/
|
|
47
|
+
export declare function fetchPostDetails(page: Page, locationId: string, postId: string): Promise<{
|
|
48
|
+
title: string;
|
|
49
|
+
description: string | null;
|
|
50
|
+
video: {
|
|
51
|
+
assetId: string;
|
|
52
|
+
url: string;
|
|
53
|
+
} | null;
|
|
54
|
+
materials: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
url: string;
|
|
58
|
+
type: string;
|
|
59
|
+
}>;
|
|
60
|
+
} | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Fetches the DRM license (HLS token) for a video asset.
|
|
63
|
+
*/
|
|
64
|
+
export declare function fetchVideoLicense(page: Page, assetId: string): Promise<{
|
|
65
|
+
url: string;
|
|
66
|
+
token: string;
|
|
67
|
+
} | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Extracts complete post content including video and attachments.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractHighLevelPostContent(page: Page, postUrl: string, locationId: string, productId: string, postId: string, categoryId: string): Promise<HighLevelPostContent | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Parses an HLS master playlist to extract quality variants.
|
|
74
|
+
* Uses hls-parser for robust parsing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseHLSMasterPlaylist(content: string, baseUrl: string): Array<{
|
|
77
|
+
label: string;
|
|
78
|
+
url: string;
|
|
79
|
+
bandwidth: number;
|
|
80
|
+
width?: number | undefined;
|
|
81
|
+
height?: number | undefined;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Fetches and parses HLS playlist to get quality options.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getHLSQualities(page: Page, masterPlaylistUrl: string): Promise<Array<{
|
|
87
|
+
label: string;
|
|
88
|
+
url: string;
|
|
89
|
+
bandwidth: number;
|
|
90
|
+
width?: number | undefined;
|
|
91
|
+
height?: number | undefined;
|
|
92
|
+
}>>;
|
|
93
|
+
/**
|
|
94
|
+
* Gets the best quality URL from an HLS master playlist.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getBestHLSQuality(page: Page, masterPlaylistUrl: string): Promise<string | null>;
|
|
97
|
+
//# sourceMappingURL=extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/scraper/highlevel/extractor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import * as HLS from "hls-parser";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the Firebase auth token from the page.
|
|
4
|
+
*/
|
|
5
|
+
export async function getAuthToken(page) {
|
|
6
|
+
return page.evaluate(() => {
|
|
7
|
+
const tokenKey = Object.keys(localStorage).find((k) => k.includes("firebase:authUser"));
|
|
8
|
+
if (!tokenKey)
|
|
9
|
+
return null;
|
|
10
|
+
const tokenData = JSON.parse(localStorage.getItem(tokenKey) ?? "{}");
|
|
11
|
+
return tokenData?.stsTokenManager?.accessToken ?? null;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extracts video info from a HighLevel post page by intercepting network requests.
|
|
16
|
+
*/
|
|
17
|
+
export async function extractVideoFromPage(page) {
|
|
18
|
+
// First, check if there's an HLS video on the page
|
|
19
|
+
const hlsUrl = await page.evaluate(() => {
|
|
20
|
+
// Look for HLS master playlist URLs in the DOM
|
|
21
|
+
const videoElements = Array.from(document.querySelectorAll("video"));
|
|
22
|
+
for (const video of videoElements) {
|
|
23
|
+
const src = video.currentSrc || video.src;
|
|
24
|
+
if (src && src.includes(".m3u8")) {
|
|
25
|
+
return src;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Check for plyr or other players
|
|
29
|
+
const sources = Array.from(document.querySelectorAll('source[type*="m3u8"], source[src*=".m3u8"]'));
|
|
30
|
+
for (const source of sources) {
|
|
31
|
+
const src = source.src;
|
|
32
|
+
if (src)
|
|
33
|
+
return src;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
});
|
|
37
|
+
if (hlsUrl) {
|
|
38
|
+
return {
|
|
39
|
+
type: "hls",
|
|
40
|
+
url: hlsUrl,
|
|
41
|
+
masterPlaylistUrl: hlsUrl,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Check for Vimeo embed
|
|
45
|
+
const vimeoUrl = await page.evaluate(() => {
|
|
46
|
+
const iframe = document.querySelector('iframe[src*="vimeo.com"], iframe[src*="player.vimeo"]');
|
|
47
|
+
if (iframe) {
|
|
48
|
+
return iframe.src;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
});
|
|
52
|
+
if (vimeoUrl) {
|
|
53
|
+
return {
|
|
54
|
+
type: "vimeo",
|
|
55
|
+
url: vimeoUrl,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Check for Loom embed
|
|
59
|
+
const loomUrl = await page.evaluate(() => {
|
|
60
|
+
const iframe = document.querySelector('iframe[src*="loom.com"]');
|
|
61
|
+
if (iframe) {
|
|
62
|
+
return iframe.src;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
});
|
|
66
|
+
if (loomUrl) {
|
|
67
|
+
return {
|
|
68
|
+
type: "loom",
|
|
69
|
+
url: loomUrl,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Check for YouTube embed
|
|
73
|
+
const youtubeUrl = await page.evaluate(() => {
|
|
74
|
+
const iframe = document.querySelector('iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"], iframe[src*="youtu.be"]');
|
|
75
|
+
if (iframe) {
|
|
76
|
+
return iframe.src;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
});
|
|
80
|
+
if (youtubeUrl) {
|
|
81
|
+
return {
|
|
82
|
+
type: "youtube",
|
|
83
|
+
url: youtubeUrl,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Extracts video info by intercepting network requests during page load.
|
|
90
|
+
*/
|
|
91
|
+
export async function interceptVideoRequests(page, postUrl) {
|
|
92
|
+
const hlsUrls = [];
|
|
93
|
+
const drmUrls = [];
|
|
94
|
+
// Set up request interception
|
|
95
|
+
const requestHandler = (request) => {
|
|
96
|
+
const url = request.url();
|
|
97
|
+
// Capture HLS master playlist requests
|
|
98
|
+
if (url.includes(".m3u8") || url.includes("master.m3u8")) {
|
|
99
|
+
hlsUrls.push(url);
|
|
100
|
+
}
|
|
101
|
+
// Capture DRM license requests
|
|
102
|
+
if (url.includes("assets-drm/assets-license")) {
|
|
103
|
+
drmUrls.push(url);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
page.on("request", requestHandler);
|
|
107
|
+
// Navigate to the post page
|
|
108
|
+
await page.goto(postUrl, { timeout: 30000 });
|
|
109
|
+
await page.waitForLoadState("domcontentloaded");
|
|
110
|
+
await page.waitForTimeout(3000);
|
|
111
|
+
// Remove the handler
|
|
112
|
+
page.off("request", requestHandler);
|
|
113
|
+
// Get the HLS master playlist URL
|
|
114
|
+
const masterPlaylistUrl = hlsUrls.find((url) => url.includes("master.m3u8"));
|
|
115
|
+
if (masterPlaylistUrl) {
|
|
116
|
+
return {
|
|
117
|
+
type: "hls",
|
|
118
|
+
url: masterPlaylistUrl,
|
|
119
|
+
masterPlaylistUrl,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Fallback to DOM extraction
|
|
123
|
+
return extractVideoFromPage(page);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetches post details from the API.
|
|
127
|
+
*/
|
|
128
|
+
export async function fetchPostDetails(page, locationId, postId) {
|
|
129
|
+
// Fetch raw data from browser context
|
|
130
|
+
const rawData = await page.evaluate(async ({ locationId, postId }) => {
|
|
131
|
+
try {
|
|
132
|
+
const tokenKey = Object.keys(localStorage).find((k) => k.includes("firebase:authUser"));
|
|
133
|
+
const tokenData = tokenKey ? JSON.parse(localStorage.getItem(tokenKey) ?? "{}") : null;
|
|
134
|
+
const token = tokenData?.stsTokenManager?.accessToken;
|
|
135
|
+
if (!token) {
|
|
136
|
+
return { error: "No auth token" };
|
|
137
|
+
}
|
|
138
|
+
const res = await fetch(`https://services.leadconnectorhq.com/membership/locations/${locationId}/posts/${postId}?source=courses`, {
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${token}`,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
return { error: `HTTP ${res.status}`, status: res.status };
|
|
145
|
+
}
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
return { data };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return { error: String(error) };
|
|
151
|
+
}
|
|
152
|
+
}, { locationId, postId });
|
|
153
|
+
// Debug: Log raw response in Node context
|
|
154
|
+
if (rawData?.error) {
|
|
155
|
+
console.log(`[DEBUG] API Error: ${rawData.error}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const data = rawData?.data;
|
|
159
|
+
if (!data) {
|
|
160
|
+
console.log("[DEBUG] No data in response");
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// The API returns data directly (not nested under .post)
|
|
164
|
+
// Check both for backwards compatibility
|
|
165
|
+
const post = data.post ?? data;
|
|
166
|
+
let video = null;
|
|
167
|
+
// Check for video directly on post
|
|
168
|
+
// Video can have: id, assetId, assetsLicenseId, or direct url
|
|
169
|
+
if (post.video) {
|
|
170
|
+
const videoAssetId = post.video.assetsLicenseId ?? post.video.assetId ?? post.video.id;
|
|
171
|
+
if (videoAssetId || post.video.url) {
|
|
172
|
+
video = {
|
|
173
|
+
assetId: videoAssetId ?? "",
|
|
174
|
+
url: post.video.url ?? "",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Check posterImage for video asset (older format)
|
|
179
|
+
if (!video && post.posterImage?.assetId) {
|
|
180
|
+
video = {
|
|
181
|
+
assetId: post.posterImage.assetId,
|
|
182
|
+
url: post.posterImage.url ?? "",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Check for video in contentBlock
|
|
186
|
+
if (!video && post.contentBlock) {
|
|
187
|
+
for (const block of post.contentBlock) {
|
|
188
|
+
if (block.type === "video") {
|
|
189
|
+
const blockAssetId = block.assetsLicenseId ?? block.assetId ?? block.id;
|
|
190
|
+
if (blockAssetId || block.url) {
|
|
191
|
+
video = {
|
|
192
|
+
assetId: blockAssetId ?? "",
|
|
193
|
+
url: block.url ?? "",
|
|
194
|
+
};
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const materials = [];
|
|
201
|
+
// Materials can be under 'materials' or 'post_materials'
|
|
202
|
+
const materialsList = post.materials ?? post.post_materials ?? [];
|
|
203
|
+
if (Array.isArray(materialsList)) {
|
|
204
|
+
for (const material of materialsList) {
|
|
205
|
+
materials.push({
|
|
206
|
+
id: material.id ?? crypto.randomUUID(),
|
|
207
|
+
name: material.name ?? "Attachment",
|
|
208
|
+
url: material.url ?? "",
|
|
209
|
+
type: material.type ?? "file",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
title: post.title ?? "",
|
|
215
|
+
description: post.description ?? null,
|
|
216
|
+
video,
|
|
217
|
+
materials,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Fetches the DRM license (HLS token) for a video asset.
|
|
222
|
+
*/
|
|
223
|
+
export async function fetchVideoLicense(page, assetId) {
|
|
224
|
+
return page.evaluate(async (assetId) => {
|
|
225
|
+
try {
|
|
226
|
+
const tokenKey = Object.keys(localStorage).find((k) => k.includes("firebase:authUser"));
|
|
227
|
+
const tokenData = tokenKey ? JSON.parse(localStorage.getItem(tokenKey) ?? "{}") : null;
|
|
228
|
+
const token = tokenData?.stsTokenManager?.accessToken;
|
|
229
|
+
if (!token) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const res = await fetch(`https://backend.leadconnectorhq.com/assets-drm/assets-license/${assetId}`, {
|
|
233
|
+
headers: {
|
|
234
|
+
Authorization: `Bearer ${token}`,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const data = await res.json();
|
|
241
|
+
return {
|
|
242
|
+
url: data.url ?? "",
|
|
243
|
+
token: data.token ?? "",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error("Failed to fetch video license:", error);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}, assetId);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Extracts complete post content including video and attachments.
|
|
254
|
+
*/
|
|
255
|
+
export async function extractHighLevelPostContent(page, postUrl, locationId, productId, postId, categoryId) {
|
|
256
|
+
// Navigate to post page
|
|
257
|
+
await page.goto(postUrl, { timeout: 30000 });
|
|
258
|
+
await page.waitForLoadState("domcontentloaded");
|
|
259
|
+
await page.waitForTimeout(3000);
|
|
260
|
+
// Fetch post details from API
|
|
261
|
+
const postDetails = await fetchPostDetails(page, locationId, postId);
|
|
262
|
+
if (!postDetails) {
|
|
263
|
+
console.error("Could not fetch post details");
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
let video = null;
|
|
267
|
+
// Check if we have video data
|
|
268
|
+
if (postDetails.video) {
|
|
269
|
+
// Option 1: Direct MP4 URL (preferred - no DRM)
|
|
270
|
+
if (postDetails.video.url && postDetails.video.url.endsWith(".mp4")) {
|
|
271
|
+
video = {
|
|
272
|
+
type: "custom", // Direct download, not HLS
|
|
273
|
+
url: postDetails.video.url,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Option 2: Get HLS license URL via assetId
|
|
277
|
+
else if (postDetails.video.assetId) {
|
|
278
|
+
const license = await fetchVideoLicense(page, postDetails.video.assetId);
|
|
279
|
+
if (license?.url) {
|
|
280
|
+
video = {
|
|
281
|
+
type: "hls",
|
|
282
|
+
url: license.url,
|
|
283
|
+
masterPlaylistUrl: license.url,
|
|
284
|
+
token: license.token,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Fallback: try to extract video from page DOM
|
|
290
|
+
if (!video) {
|
|
291
|
+
video = await extractVideoFromPage(page);
|
|
292
|
+
}
|
|
293
|
+
// Extract HTML content
|
|
294
|
+
const htmlContent = await page.evaluate(() => {
|
|
295
|
+
const contentEl = document.querySelector("[class*='post-content'], [class*='PostContent'], [class*='lesson-content'], article");
|
|
296
|
+
return contentEl?.innerHTML ?? null;
|
|
297
|
+
});
|
|
298
|
+
// Extract text description
|
|
299
|
+
const description = await page.evaluate(() => {
|
|
300
|
+
const descEl = document.querySelector("[class*='description'], [class*='Description'], p:first-of-type");
|
|
301
|
+
return descEl?.textContent?.trim() ?? null;
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
id: postId,
|
|
305
|
+
title: postDetails.title,
|
|
306
|
+
description: description ?? postDetails.description,
|
|
307
|
+
htmlContent,
|
|
308
|
+
video,
|
|
309
|
+
attachments: postDetails.materials.map((m) => ({
|
|
310
|
+
id: m.id,
|
|
311
|
+
name: m.name,
|
|
312
|
+
url: m.url,
|
|
313
|
+
type: m.type,
|
|
314
|
+
})),
|
|
315
|
+
categoryId,
|
|
316
|
+
productId,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Parses an HLS master playlist to extract quality variants.
|
|
321
|
+
* Uses hls-parser for robust parsing.
|
|
322
|
+
*/
|
|
323
|
+
export function parseHLSMasterPlaylist(content, baseUrl) {
|
|
324
|
+
try {
|
|
325
|
+
const playlist = HLS.parse(content);
|
|
326
|
+
// Check if it's a master playlist with variants
|
|
327
|
+
if (!("variants" in playlist) || !playlist.variants) {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
const variants = playlist.variants.map((variant) => {
|
|
331
|
+
const bandwidth = variant.bandwidth ?? 0;
|
|
332
|
+
const resolution = variant.resolution;
|
|
333
|
+
const width = resolution?.width;
|
|
334
|
+
const height = resolution?.height;
|
|
335
|
+
// Build absolute URL
|
|
336
|
+
const variantUrl = variant.uri.startsWith("http")
|
|
337
|
+
? variant.uri
|
|
338
|
+
: new URL(variant.uri, baseUrl).href;
|
|
339
|
+
const label = height ? `${height}p` : `${Math.round(bandwidth / 1000)}k`;
|
|
340
|
+
return {
|
|
341
|
+
label,
|
|
342
|
+
url: variantUrl,
|
|
343
|
+
bandwidth,
|
|
344
|
+
width,
|
|
345
|
+
height,
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
// Sort by bandwidth (highest first)
|
|
349
|
+
variants.sort((a, b) => b.bandwidth - a.bandwidth);
|
|
350
|
+
return variants;
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Fetches and parses HLS playlist to get quality options.
|
|
358
|
+
*/
|
|
359
|
+
export async function getHLSQualities(page, masterPlaylistUrl) {
|
|
360
|
+
try {
|
|
361
|
+
const content = await page.evaluate(async (url) => {
|
|
362
|
+
const res = await fetch(url);
|
|
363
|
+
if (!res.ok)
|
|
364
|
+
return null;
|
|
365
|
+
return res.text();
|
|
366
|
+
}, masterPlaylistUrl);
|
|
367
|
+
if (!content)
|
|
368
|
+
return [];
|
|
369
|
+
return parseHLSMasterPlaylist(content, masterPlaylistUrl);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Gets the best quality URL from an HLS master playlist.
|
|
377
|
+
*/
|
|
378
|
+
export async function getBestHLSQuality(page, masterPlaylistUrl) {
|
|
379
|
+
const qualities = await getHLSQualities(page, masterPlaylistUrl);
|
|
380
|
+
if (qualities.length === 0) {
|
|
381
|
+
return masterPlaylistUrl;
|
|
382
|
+
}
|
|
383
|
+
// Return highest quality
|
|
384
|
+
return qualities[0]?.url ?? null;
|
|
385
|
+
}
|
|
386
|
+
//# sourceMappingURL=extractor.js.map
|