Skip to content

Commit 4f4f963

Browse files
committed
fix(learningsuite): add auth token (APIKEY) to video downloads
- Extract auth token from browser session (localStorage/sessionStorage) - Pass authToken through VideoDownloadTask interface - Add APIKEY and Authorization headers to HLS fetcher and downloader - Fix 401 'Missing APIKEY' error for LearningSuite video API
1 parent 6301013 commit 4f4f963

File tree

3 files changed

+41
-16
lines changed

3 files changed

+41
-16
lines changed

src/cli/commands/syncLearningSuite.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
buildLearningSuiteCourseStructure,
1010
createFolderName,
1111
extractLearningSuitePostContent,
12+
getAuthToken,
1213
getLearningSuiteLessonUrl,
1314
slugify,
1415
type LearningSuiteCourseStructure,
@@ -518,15 +519,19 @@ export async function syncLearningSuiteCommand(
518519

519520
// Phase 3: Download videos
520521
if (!options.skipVideos && videoTasks.length > 0) {
521-
// Extract cookies from session for authenticated video downloads
522+
// Extract cookies and auth token from session for authenticated video downloads
522523
const browserCookies = await session.page.context().cookies();
523524
const cookieString = browserCookies.map((c) => `${c.name}=${c.value}`).join("; ");
524525
const refererUrl = `https://${courseStructure.domain}/`;
526+
const authToken = await getAuthToken(session.page);
525527

526-
// Add cookies and referer to all video tasks
528+
// Add cookies, referer, and auth token to all video tasks
527529
for (const task of videoTasks) {
528530
task.cookies = cookieString;
529531
task.referer = refererUrl;
532+
if (authToken) {
533+
task.authToken = authToken;
534+
}
530535
}
531536

532537
await downloadVideos(videoTasks, config);

src/downloader/hlsDownloader.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export async function checkFfmpeg(): Promise<boolean> {
4040
export async function fetchHLSQualities(
4141
masterUrl: string,
4242
cookies?: string,
43-
referer?: string
43+
referer?: string,
44+
authToken?: string
4445
): Promise<HLSQuality[]> {
4546
try {
4647
// Use provided referer or extract origin from URL
@@ -55,6 +56,11 @@ export async function fetchHLSQualities(
5556
if (cookies) {
5657
headers.Cookie = cookies;
5758
}
59+
// Add auth token as APIKEY header (used by LearningSuite)
60+
if (authToken) {
61+
headers.APIKEY = authToken;
62+
headers.Authorization = `Bearer ${authToken}`;
63+
}
5864

5965
// Follow redirects manually to capture the final URL
6066
const response = await fetch(masterUrl, {
@@ -86,14 +92,14 @@ export async function fetchHLSQualities(
8692
(json.source as string | undefined);
8793
if (playlistUrl && typeof playlistUrl === "string") {
8894
// Recursively fetch the actual playlist
89-
return await fetchHLSQualities(playlistUrl, cookies, referer);
95+
return await fetchHLSQualities(playlistUrl, cookies, referer, authToken);
9096
}
9197
// Look for CDN URL anywhere in the JSON string
9298
const jsonStr = JSON.stringify(json);
9399
const cdnMatch =
94100
/(https?:\/\/[^"'\s]*(?:b-cdn\.net|mediadelivery\.net|vz-)[^"'\s]*)/i.exec(jsonStr);
95101
if (cdnMatch?.[1]) {
96-
return await fetchHLSQualities(cdnMatch[1], cookies, referer);
102+
return await fetchHLSQualities(cdnMatch[1], cookies, referer, authToken);
97103
}
98104
} catch {
99105
// Not valid JSON
@@ -106,7 +112,7 @@ export async function fetchHLSQualities(
106112
content
107113
);
108114
if (cdnMatch?.[1]) {
109-
return await fetchHLSQualities(cdnMatch[1], cookies, referer);
115+
return await fetchHLSQualities(cdnMatch[1], cookies, referer, authToken);
110116
}
111117

112118
console.error(`[HLS] Invalid playlist (starts with: ${content.substring(0, 50)}...)`);
@@ -177,9 +183,10 @@ export async function getBestQualityUrl(
177183
masterUrl: string,
178184
preferredHeight?: number,
179185
cookies?: string,
180-
referer?: string
186+
referer?: string,
187+
authToken?: string
181188
): Promise<string> {
182-
const qualities = await fetchHLSQualities(masterUrl, cookies, referer);
189+
const qualities = await fetchHLSQualities(masterUrl, cookies, referer, authToken);
183190

184191
if (qualities.length === 0) {
185192
// Assume it's a direct media playlist
@@ -216,7 +223,8 @@ export async function downloadHLSVideo(
216223
outputPath: string,
217224
onProgress?: (progress: DownloadProgress) => void,
218225
cookies?: string,
219-
referer?: string
226+
referer?: string,
227+
authToken?: string
220228
): Promise<HLSDownloadResult> {
221229
// Check if ffmpeg is available
222230
const hasFfmpeg = await checkFfmpeg();
@@ -240,6 +248,10 @@ export async function downloadHLSVideo(
240248
if (cookies) {
241249
headers.Cookie = cookies;
242250
}
251+
if (authToken) {
252+
headers.APIKEY = authToken;
253+
headers.Authorization = `Bearer ${authToken}`;
254+
}
243255

244256
const testResponse = await fetch(hlsUrl, { headers, method: "HEAD" });
245257
if (!testResponse.ok) {
@@ -282,6 +294,10 @@ export async function downloadHLSVideo(
282294
if (cookies) {
283295
headerParts.push(`Cookie: ${cookies}`);
284296
}
297+
if (authToken) {
298+
headerParts.push(`APIKEY: ${authToken}`);
299+
headerParts.push(`Authorization: Bearer ${authToken}`);
300+
}
285301
args.push("-headers", headerParts.join("\r\n") + "\r\n");
286302

287303
args.push(
@@ -388,7 +404,8 @@ export async function downloadHighLevelVideo(
388404
preferredQuality?: string,
389405
onProgress?: (progress: DownloadProgress) => void,
390406
cookies?: string,
391-
referer?: string
407+
referer?: string,
408+
authToken?: string
392409
): Promise<HLSDownloadResult> {
393410
// Report start
394411
onProgress?.({
@@ -408,13 +425,13 @@ export async function downloadHighLevelVideo(
408425
// Get the best quality URL
409426
let downloadUrl = masterUrl;
410427
try {
411-
downloadUrl = await getBestQualityUrl(masterUrl, preferredHeight, cookies, referer);
428+
downloadUrl = await getBestQualityUrl(masterUrl, preferredHeight, cookies, referer, authToken);
412429
} catch (error) {
413430
console.warn("Failed to fetch quality options, using master URL:", error);
414431
}
415432

416433
// Download using ffmpeg
417-
return downloadHLSVideo(downloadUrl, outputPath, onProgress, cookies, referer);
434+
return downloadHLSVideo(downloadUrl, outputPath, onProgress, cookies, referer, authToken);
418435
}
419436
/* v8 ignore stop */
420437

src/downloader/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface VideoDownloadTask {
2626
cookies?: string | undefined;
2727
/** Optional referer URL for authenticated downloads */
2828
referer?: string | undefined;
29+
/** Optional auth token (API key) for authenticated downloads */
30+
authToken?: string | undefined;
2931
}
3032

3133
export interface DownloadResult {
@@ -42,7 +44,7 @@ export async function downloadVideo(
4244
task: VideoDownloadTask,
4345
onProgress?: (progress: DownloadProgress) => void
4446
): Promise<DownloadResult> {
45-
const { videoUrl, videoType, outputPath, preferredQuality, cookies, referer } = task;
47+
const { videoUrl, videoType, outputPath, preferredQuality, cookies, referer, authToken } = task;
4648

4749
switch (videoType) {
4850
case "loom":
@@ -57,7 +59,7 @@ export async function downloadVideo(
5759

5860
case "hls":
5961
// Generic HLS stream
60-
return downloadHLSVideo(videoUrl, outputPath, onProgress, cookies, referer);
62+
return downloadHLSVideo(videoUrl, outputPath, onProgress, cookies, referer, authToken);
6163

6264
case "highlevel":
6365
// HighLevel HLS video with quality selection
@@ -67,7 +69,8 @@ export async function downloadVideo(
6769
preferredQuality,
6870
onProgress,
6971
cookies,
70-
referer
72+
referer,
73+
authToken
7174
);
7275

7376
case "youtube":
@@ -87,7 +90,7 @@ export async function downloadVideo(
8790
}
8891
// Try HLS if it looks like a playlist
8992
if (/\.m3u8(\?|$)/i.exec(videoUrl)) {
90-
return downloadHLSVideo(videoUrl, outputPath, onProgress, cookies, referer);
93+
return downloadHLSVideo(videoUrl, outputPath, onProgress, cookies, referer, authToken);
9194
}
9295
return {
9396
success: false,

0 commit comments

Comments
 (0)