offcourse 0.0.2 → 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 +255 -20
- 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,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CourseDatabase,
|
|
3
|
+
LessonStatus,
|
|
4
|
+
VideoType,
|
|
5
|
+
extractCommunitySlug,
|
|
6
|
+
getDbDir,
|
|
7
|
+
getDbPath,
|
|
8
|
+
type CourseMetadata,
|
|
9
|
+
type LessonRecord,
|
|
10
|
+
type LessonStatusType,
|
|
11
|
+
type LessonWithModule,
|
|
12
|
+
type ModuleRecord,
|
|
13
|
+
type VideoTypeValue,
|
|
14
|
+
} from "./database.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getLessonBasename,
|
|
4
|
+
getVideoPath,
|
|
5
|
+
getMarkdownPath,
|
|
6
|
+
getDownloadFilePath,
|
|
7
|
+
} from "./fileSystem.js";
|
|
8
|
+
|
|
9
|
+
describe("fileSystem", () => {
|
|
10
|
+
describe("getLessonBasename", () => {
|
|
11
|
+
it("creates numbered filename from lesson name", () => {
|
|
12
|
+
expect(getLessonBasename(0, "Introduction")).toBe("01-introduction");
|
|
13
|
+
expect(getLessonBasename(9, "Final Lesson")).toBe("10-final-lesson");
|
|
14
|
+
expect(getLessonBasename(99, "Bonus")).toBe("100-bonus");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("handles special characters", () => {
|
|
18
|
+
expect(getLessonBasename(0, "What's Next?")).toBe("01-whats-next");
|
|
19
|
+
expect(getLessonBasename(0, "Module 1: Basics")).toBe("01-module-1-basics");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("getVideoPath", () => {
|
|
24
|
+
it("creates video path with .mp4 extension", () => {
|
|
25
|
+
const path = getVideoPath("/courses/my-course/01-intro", 0, "Welcome");
|
|
26
|
+
expect(path).toBe("/courses/my-course/01-intro/01-welcome.mp4");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("handles nested paths", () => {
|
|
30
|
+
const path = getVideoPath("/home/user/Downloads/courses/test", 5, "Lesson Six");
|
|
31
|
+
expect(path).toBe("/home/user/Downloads/courses/test/06-lesson-six.mp4");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("getMarkdownPath", () => {
|
|
36
|
+
it("creates markdown path with .md extension", () => {
|
|
37
|
+
const path = getMarkdownPath("/courses/my-course/01-intro", 0, "Welcome");
|
|
38
|
+
expect(path).toBe("/courses/my-course/01-intro/01-welcome.md");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("getDownloadFilePath", () => {
|
|
43
|
+
it("prefixes filename with lesson basename", () => {
|
|
44
|
+
const path = getDownloadFilePath("/module", 2, "Resources", "workbook.pdf");
|
|
45
|
+
expect(path).toBe("/module/03-resources-workbook.pdf");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("sanitizes dangerous filename characters", () => {
|
|
49
|
+
// Characters that are invalid in filenames on various OS
|
|
50
|
+
const path = getDownloadFilePath("/module", 0, "Intro", 'file<>:"/\\|?*.pdf');
|
|
51
|
+
expect(path).toBe("/module/01-intro-file_________.pdf");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("preserves safe special characters", () => {
|
|
55
|
+
const path = getDownloadFilePath("/module", 0, "Intro", "my-file_v2 (1).pdf");
|
|
56
|
+
expect(path).toBe("/module/01-intro-my-file_v2 (1).pdf");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("handles filenames with multiple extensions", () => {
|
|
60
|
+
const path = getDownloadFilePath("/module", 0, "Intro", "archive.tar.gz");
|
|
61
|
+
expect(path).toBe("/module/01-intro-archive.tar.gz");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { expandPath, getSyncStatePath } from "../config/paths.js";
|
|
6
|
+
import type { CourseSyncState } from "../config/schema.js";
|
|
7
|
+
import { courseSyncStateSchema } from "../config/schema.js";
|
|
8
|
+
import { createFolderName } from "../scraper/navigator.js";
|
|
9
|
+
import { ensureDir, outputFile, pathExists, readJson, outputJson } from "../shared/fs.js";
|
|
10
|
+
import { http } from "../shared/http.js";
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// Pure functions - testable without mocking
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Gets the base filename for a lesson (without extension).
|
|
18
|
+
* Format: "01-lesson-name"
|
|
19
|
+
*/
|
|
20
|
+
export function getLessonBasename(lessonIndex: number, lessonName: string): string {
|
|
21
|
+
return createFolderName(lessonIndex, lessonName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the video file path for a lesson.
|
|
26
|
+
* Videos are stored directly in the module directory with lesson name.
|
|
27
|
+
*/
|
|
28
|
+
export function getVideoPath(moduleDir: string, lessonIndex: number, lessonName: string): string {
|
|
29
|
+
return join(moduleDir, `${getLessonBasename(lessonIndex, lessonName)}.mp4`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the markdown file path for a lesson.
|
|
34
|
+
* Markdown files are stored directly in the module directory with lesson name.
|
|
35
|
+
*/
|
|
36
|
+
export function getMarkdownPath(
|
|
37
|
+
moduleDir: string,
|
|
38
|
+
lessonIndex: number,
|
|
39
|
+
lessonName: string
|
|
40
|
+
): string {
|
|
41
|
+
return join(moduleDir, `${getLessonBasename(lessonIndex, lessonName)}.md`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets the path for a downloadable file.
|
|
46
|
+
* Files are stored in the module directory with lesson prefix.
|
|
47
|
+
*/
|
|
48
|
+
export function getDownloadFilePath(
|
|
49
|
+
moduleDir: string,
|
|
50
|
+
lessonIndex: number,
|
|
51
|
+
lessonName: string,
|
|
52
|
+
filename: string
|
|
53
|
+
): string {
|
|
54
|
+
const lessonPrefix = getLessonBasename(lessonIndex, lessonName);
|
|
55
|
+
// Sanitize filename
|
|
56
|
+
const safeFilename = filename.replace(/[<>:"/\\|?*]/g, "_");
|
|
57
|
+
return join(moduleDir, `${lessonPrefix}-${safeFilename}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================
|
|
61
|
+
// I/O functions - require filesystem access
|
|
62
|
+
// ============================================
|
|
63
|
+
/* v8 ignore start */
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates the output directory structure for a course.
|
|
67
|
+
*/
|
|
68
|
+
export async function createCourseDirectory(
|
|
69
|
+
outputBase: string,
|
|
70
|
+
courseName: string
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const expanded = expandPath(outputBase);
|
|
73
|
+
const courseDir = join(expanded, createFolderName(0, courseName).replace(/^\d+-/, ""));
|
|
74
|
+
await ensureDir(courseDir);
|
|
75
|
+
return courseDir;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a module directory within a course.
|
|
80
|
+
*/
|
|
81
|
+
export async function createModuleDirectory(
|
|
82
|
+
courseDir: string,
|
|
83
|
+
moduleIndex: number,
|
|
84
|
+
moduleName: string
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
const moduleDir = join(courseDir, createFolderName(moduleIndex, moduleName));
|
|
87
|
+
await ensureDir(moduleDir);
|
|
88
|
+
return moduleDir;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Saves markdown content to a file.
|
|
93
|
+
*/
|
|
94
|
+
export async function saveMarkdown(
|
|
95
|
+
directory: string,
|
|
96
|
+
filename: string,
|
|
97
|
+
content: string
|
|
98
|
+
): Promise<string> {
|
|
99
|
+
const filePath = join(directory, filename);
|
|
100
|
+
await outputFile(filePath, content);
|
|
101
|
+
return filePath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Loads the sync state for a course.
|
|
106
|
+
*/
|
|
107
|
+
export async function loadSyncState(courseSlug: string): Promise<CourseSyncState | null> {
|
|
108
|
+
const statePath = getSyncStatePath(courseSlug);
|
|
109
|
+
const data = await readJson(statePath);
|
|
110
|
+
|
|
111
|
+
if (!data) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
return courseSyncStateSchema.parse(data);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Saves the sync state for a course.
|
|
124
|
+
*/
|
|
125
|
+
export async function saveSyncState(courseSlug: string, state: CourseSyncState): Promise<void> {
|
|
126
|
+
const statePath = getSyncStatePath(courseSlug);
|
|
127
|
+
await outputJson(statePath, state);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Checks if a lesson has been fully synced.
|
|
132
|
+
*/
|
|
133
|
+
export async function isLessonSynced(
|
|
134
|
+
moduleDir: string,
|
|
135
|
+
lessonIndex: number,
|
|
136
|
+
lessonName: string
|
|
137
|
+
): Promise<{ video: boolean; content: boolean }> {
|
|
138
|
+
const [video, content] = await Promise.all([
|
|
139
|
+
pathExists(getVideoPath(moduleDir, lessonIndex, lessonName)),
|
|
140
|
+
pathExists(getMarkdownPath(moduleDir, lessonIndex, lessonName)),
|
|
141
|
+
]);
|
|
142
|
+
return { video, content };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Downloads a file from a URL to the specified path.
|
|
147
|
+
*/
|
|
148
|
+
export async function downloadFile(
|
|
149
|
+
url: string,
|
|
150
|
+
outputPath: string
|
|
151
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
152
|
+
if (await pathExists(outputPath)) {
|
|
153
|
+
return { success: true }; // Already downloaded
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await ensureDir(join(outputPath, ".."));
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const response = await http.get(url);
|
|
160
|
+
const body = response.body;
|
|
161
|
+
|
|
162
|
+
if (!body) {
|
|
163
|
+
return { success: false, error: "No response body" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fileStream = createWriteStream(outputPath);
|
|
167
|
+
await pipeline(Readable.fromWeb(body as import("stream/web").ReadableStream), fileStream);
|
|
168
|
+
|
|
169
|
+
return { success: true };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return { success: false, error: String(error) };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* v8 ignore stop */
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2024", "DOM"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"noUncheckedIndexedAccess": true,
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"exactOptionalPropertyTypes": true,
|
|
23
|
+
"isolatedModules": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*"],
|
|
26
|
+
"exclude": ["node_modules", "dist"]
|
|
27
|
+
}
|
|
28
|
+
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
include: ["src/**/*.test.ts"],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: "v8",
|
|
10
|
+
reporter: ["text", "html", "lcov"],
|
|
11
|
+
include: ["src/**/*.ts"],
|
|
12
|
+
exclude: [
|
|
13
|
+
// Test files
|
|
14
|
+
"src/**/*.test.ts",
|
|
15
|
+
// CLI commands (interactive, hard to test)
|
|
16
|
+
"src/cli/**",
|
|
17
|
+
// Re-export index files
|
|
18
|
+
"src/**/index.ts",
|
|
19
|
+
// Pure I/O wrappers (testing would just test Node.js/packages)
|
|
20
|
+
"src/shared/fs.ts",
|
|
21
|
+
"src/config/configManager.ts",
|
|
22
|
+
// Browser automation (requires Playwright, not unit testable)
|
|
23
|
+
"src/scraper/videoInterceptor.ts",
|
|
24
|
+
// Network validation (requires live connections)
|
|
25
|
+
"src/downloader/hlsValidator.ts",
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
package/cli.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const VERSION = "0.0.2";
|
|
4
|
-
|
|
5
|
-
const banner = `
|
|
6
|
-
╔═══════════════════════════════════════════════════════════════╗
|
|
7
|
-
║ ║
|
|
8
|
-
║ ┌─┐┌─┐┌─┐┌─┐┌─┐┬ ┬┬─┐┌─┐┌─┐ ║
|
|
9
|
-
║ │ │├┤ ├┤ │ │ ││ │├┬┘└─┐├┤ ║
|
|
10
|
-
║ └─┘└ └ └─┘└─┘└─┘┴└─└─┘└─┘ ║
|
|
11
|
-
║ ║
|
|
12
|
-
║ Download online courses for offline access – of course! 📚 ║
|
|
13
|
-
║ ║
|
|
14
|
-
╚═══════════════════════════════════════════════════════════════╝
|
|
15
|
-
`;
|
|
16
|
-
|
|
17
|
-
const comingSoon = `
|
|
18
|
-
🚧 Coming Soon!
|
|
19
|
-
|
|
20
|
-
This package is currently in private development.
|
|
21
|
-
The full release will include:
|
|
22
|
-
|
|
23
|
-
• 🔐 Browser-based authentication with session caching
|
|
24
|
-
• 📚 Course structure preservation (module/lesson hierarchy)
|
|
25
|
-
• 🎬 Video downloads (Loom, Vimeo, YouTube, Wistia)
|
|
26
|
-
• 📝 Content extraction to clean Markdown
|
|
27
|
-
• ⏸️ Resumable syncs
|
|
28
|
-
• ⚡ Concurrent downloads
|
|
29
|
-
|
|
30
|
-
Supported platforms:
|
|
31
|
-
• Skool.com (ready)
|
|
32
|
-
• LearningSuite.io (planned)
|
|
33
|
-
|
|
34
|
-
Follow the project:
|
|
35
|
-
→ https://github.com/sebastian-software/offcourse
|
|
36
|
-
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
console.log(banner);
|
|
40
|
-
console.log(comingSoon);
|
|
41
|
-
|
|
42
|
-
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
43
|
-
console.log(`v${VERSION}`);
|
|
44
|
-
}
|
|
45
|
-
|