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,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractCommunitySlug } from "./database.js";
|
|
3
|
+
describe("extractCommunitySlug", () => {
|
|
4
|
+
it("extracts slug from standard Skool URL", () => {
|
|
5
|
+
expect(extractCommunitySlug("https://www.skool.com/my-community")).toBe("my-community");
|
|
6
|
+
});
|
|
7
|
+
it("extracts slug from Skool URL without www", () => {
|
|
8
|
+
expect(extractCommunitySlug("https://skool.com/test-group")).toBe("test-group");
|
|
9
|
+
});
|
|
10
|
+
it("extracts slug from URL with path", () => {
|
|
11
|
+
expect(extractCommunitySlug("https://www.skool.com/my-community/classroom")).toBe("my-community");
|
|
12
|
+
expect(extractCommunitySlug("https://www.skool.com/my-community/classroom/lessons/123")).toBe("my-community");
|
|
13
|
+
});
|
|
14
|
+
it("extracts slug from URL with query params (includes params in slug)", () => {
|
|
15
|
+
// Note: current implementation doesn't strip query params
|
|
16
|
+
expect(extractCommunitySlug("https://www.skool.com/my-community?ref=abc")).toBe("my-community?ref=abc");
|
|
17
|
+
});
|
|
18
|
+
it("handles complex community names", () => {
|
|
19
|
+
expect(extractCommunitySlug("https://skool.com/the-best-community-ever-2024")).toBe("the-best-community-ever-2024");
|
|
20
|
+
});
|
|
21
|
+
it("returns 'unknown' for non-Skool URLs", () => {
|
|
22
|
+
expect(extractCommunitySlug("https://example.com/path")).toBe("unknown");
|
|
23
|
+
expect(extractCommunitySlug("https://youtube.com/channel/abc")).toBe("unknown");
|
|
24
|
+
});
|
|
25
|
+
it("returns 'unknown' for invalid URLs", () => {
|
|
26
|
+
expect(extractCommunitySlug("not-a-url")).toBe("unknown");
|
|
27
|
+
expect(extractCommunitySlug("")).toBe("unknown");
|
|
28
|
+
});
|
|
29
|
+
it("returns 'unknown' for Skool root URL", () => {
|
|
30
|
+
expect(extractCommunitySlug("https://www.skool.com/")).toBe("unknown");
|
|
31
|
+
expect(extractCommunitySlug("https://www.skool.com")).toBe("unknown");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=database.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.test.js","sourceRoot":"","sources":["../../src/state/database.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,oBAAoB,CAAC,oCAAoC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,oBAAoB,CAAC,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,oBAAoB,CAAC,8CAA8C,CAAC,CAAC,CAAC,IAAI,CAC/E,cAAc,CACf,CAAC;QACF,MAAM,CAAC,oBAAoB,CAAC,0DAA0D,CAAC,CAAC,CAAC,IAAI,CAC3F,cAAc,CACf,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,0DAA0D;QAC1D,MAAM,CAAC,oBAAoB,CAAC,4CAA4C,CAAC,CAAC,CAAC,IAAI,CAC7E,sBAAsB,CACvB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,oBAAoB,CAAC,gDAAgD,CAAC,CAAC,CAAC,IAAI,CACjF,8BAA8B,CAC/B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,oBAAoB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzE,MAAM,CAAC,oBAAoB,CAAC,iCAAiC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1D,MAAM,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,oBAAoB,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACvE,MAAM,CAAC,oBAAoB,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { CourseDatabase, LessonStatus, VideoType, extractCommunitySlug, getDbDir, getDbPath, type CourseMetadata, type LessonRecord, type LessonStatusType, type LessonWithModule, type ModuleRecord, type VideoTypeValue, } from "./database.js";
|
|
2
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,SAAS,EACT,oBAAoB,EACpB,QAAQ,EACR,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,SAAS,EACT,oBAAoB,EACpB,QAAQ,EACR,SAAS,GAOV,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CourseSyncState } from "../config/schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates the output directory structure for a course.
|
|
4
|
+
*/
|
|
5
|
+
export declare function createCourseDirectory(outputBase: string, courseName: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a module directory within a course.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createModuleDirectory(courseDir: string, moduleIndex: number, moduleName: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Gets the base filename for a lesson (without extension).
|
|
12
|
+
* Format: "01-lesson-name"
|
|
13
|
+
*/
|
|
14
|
+
export declare function getLessonBasename(lessonIndex: number, lessonName: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Saves markdown content to a file.
|
|
17
|
+
*/
|
|
18
|
+
export declare function saveMarkdown(directory: string, filename: string, content: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Gets the video file path for a lesson.
|
|
21
|
+
* Videos are stored directly in the module directory with lesson name.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getVideoPath(moduleDir: string, lessonIndex: number, lessonName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Gets the markdown file path for a lesson.
|
|
26
|
+
* Markdown files are stored directly in the module directory with lesson name.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getMarkdownPath(moduleDir: string, lessonIndex: number, lessonName: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Loads the sync state for a course.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadSyncState(courseSlug: string): Promise<CourseSyncState | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Saves the sync state for a course.
|
|
35
|
+
*/
|
|
36
|
+
export declare function saveSyncState(courseSlug: string, state: CourseSyncState): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a lesson has been fully synced.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isLessonSynced(moduleDir: string, lessonIndex: number, lessonName: string): Promise<{
|
|
41
|
+
video: boolean;
|
|
42
|
+
content: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the path for a downloadable file.
|
|
46
|
+
* Files are stored in the module directory with lesson prefix.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getDownloadFilePath(moduleDir: string, lessonIndex: number, lessonName: string, filename: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Downloads a file from a URL to the specified path.
|
|
51
|
+
*/
|
|
52
|
+
export declare function downloadFile(url: string, outputPath: string): Promise<{
|
|
53
|
+
success: boolean;
|
|
54
|
+
error?: string;
|
|
55
|
+
}>;
|
|
56
|
+
//# sourceMappingURL=fileSystem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSystem.d.ts","sourceRoot":"","sources":["../../src/storage/fileSystem.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAM3D;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/F;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAavF;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAG7F;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAM/C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAKR;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsB/C"}
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { courseSyncStateSchema } from "../config/schema.js";
|
|
7
|
+
import { createFolderName } from "../scraper/navigator.js";
|
|
8
|
+
import { ensureDir, outputFile, pathExists, readJson, outputJson } from "../shared/fs.js";
|
|
9
|
+
import { http } from "../shared/http.js";
|
|
10
|
+
/**
|
|
11
|
+
* Creates the output directory structure for a course.
|
|
12
|
+
*/
|
|
13
|
+
export async function createCourseDirectory(outputBase, courseName) {
|
|
14
|
+
const expanded = expandPath(outputBase);
|
|
15
|
+
const courseDir = join(expanded, createFolderName(0, courseName).replace(/^\d+-/, ""));
|
|
16
|
+
await ensureDir(courseDir);
|
|
17
|
+
return courseDir;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a module directory within a course.
|
|
21
|
+
*/
|
|
22
|
+
export async function createModuleDirectory(courseDir, moduleIndex, moduleName) {
|
|
23
|
+
const moduleDir = join(courseDir, createFolderName(moduleIndex, moduleName));
|
|
24
|
+
await ensureDir(moduleDir);
|
|
25
|
+
return moduleDir;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Gets the base filename for a lesson (without extension).
|
|
29
|
+
* Format: "01-lesson-name"
|
|
30
|
+
*/
|
|
31
|
+
export function getLessonBasename(lessonIndex, lessonName) {
|
|
32
|
+
return createFolderName(lessonIndex, lessonName);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Saves markdown content to a file.
|
|
36
|
+
*/
|
|
37
|
+
export async function saveMarkdown(directory, filename, content) {
|
|
38
|
+
const filePath = join(directory, filename);
|
|
39
|
+
await outputFile(filePath, content);
|
|
40
|
+
return filePath;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Gets the video file path for a lesson.
|
|
44
|
+
* Videos are stored directly in the module directory with lesson name.
|
|
45
|
+
*/
|
|
46
|
+
export function getVideoPath(moduleDir, lessonIndex, lessonName) {
|
|
47
|
+
return join(moduleDir, `${getLessonBasename(lessonIndex, lessonName)}.mp4`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Gets the markdown file path for a lesson.
|
|
51
|
+
* Markdown files are stored directly in the module directory with lesson name.
|
|
52
|
+
*/
|
|
53
|
+
export function getMarkdownPath(moduleDir, lessonIndex, lessonName) {
|
|
54
|
+
return join(moduleDir, `${getLessonBasename(lessonIndex, lessonName)}.md`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Loads the sync state for a course.
|
|
58
|
+
*/
|
|
59
|
+
export async function loadSyncState(courseSlug) {
|
|
60
|
+
const statePath = getSyncStatePath(courseSlug);
|
|
61
|
+
const data = await readJson(statePath);
|
|
62
|
+
if (!data) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return courseSyncStateSchema.parse(data);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Saves the sync state for a course.
|
|
74
|
+
*/
|
|
75
|
+
export async function saveSyncState(courseSlug, state) {
|
|
76
|
+
const statePath = getSyncStatePath(courseSlug);
|
|
77
|
+
await outputJson(statePath, state);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Checks if a lesson has been fully synced.
|
|
81
|
+
*/
|
|
82
|
+
export async function isLessonSynced(moduleDir, lessonIndex, lessonName) {
|
|
83
|
+
const [video, content] = await Promise.all([
|
|
84
|
+
pathExists(getVideoPath(moduleDir, lessonIndex, lessonName)),
|
|
85
|
+
pathExists(getMarkdownPath(moduleDir, lessonIndex, lessonName)),
|
|
86
|
+
]);
|
|
87
|
+
return { video, content };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Gets the path for a downloadable file.
|
|
91
|
+
* Files are stored in the module directory with lesson prefix.
|
|
92
|
+
*/
|
|
93
|
+
export function getDownloadFilePath(moduleDir, lessonIndex, lessonName, filename) {
|
|
94
|
+
const lessonPrefix = getLessonBasename(lessonIndex, lessonName);
|
|
95
|
+
// Sanitize filename
|
|
96
|
+
const safeFilename = filename.replace(/[<>:"/\\|?*]/g, "_");
|
|
97
|
+
return join(moduleDir, `${lessonPrefix}-${safeFilename}`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Downloads a file from a URL to the specified path.
|
|
101
|
+
*/
|
|
102
|
+
export async function downloadFile(url, outputPath) {
|
|
103
|
+
if (await pathExists(outputPath)) {
|
|
104
|
+
return { success: true }; // Already downloaded
|
|
105
|
+
}
|
|
106
|
+
await ensureDir(join(outputPath, ".."));
|
|
107
|
+
try {
|
|
108
|
+
const response = await http.get(url);
|
|
109
|
+
const body = response.body;
|
|
110
|
+
if (!body) {
|
|
111
|
+
return { success: false, error: "No response body" };
|
|
112
|
+
}
|
|
113
|
+
const fileStream = createWriteStream(outputPath);
|
|
114
|
+
await pipeline(Readable.fromWeb(body), fileStream);
|
|
115
|
+
return { success: true };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return { success: false, error: String(error) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=fileSystem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSystem.js","sourceRoot":"","sources":["../../src/storage/fileSystem.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type WhisperModel = "tiny" | "base" | "small" | "medium" | "large";
|
|
2
|
+
export interface TranscriptionOptions {
|
|
3
|
+
model?: WhisperModel;
|
|
4
|
+
language?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TranscriptionResult {
|
|
7
|
+
text: string;
|
|
8
|
+
duration: number;
|
|
9
|
+
processingTime: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract audio from video file using ffmpeg.
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractAudio(videoPath: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Transcribe audio file using Whisper.
|
|
17
|
+
*/
|
|
18
|
+
export declare function transcribeAudio(audioPath: string, options?: TranscriptionOptions): Promise<TranscriptionResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Transcribe a video file (extracts audio first).
|
|
21
|
+
*/
|
|
22
|
+
export declare function transcribeVideo(videoPath: string, options?: TranscriptionOptions): Promise<TranscriptionResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Check if whisper model exists (models are auto-downloaded on first use).
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkModel(_model: WhisperModel): boolean;
|
|
27
|
+
//# sourceMappingURL=whisperService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whisperService.d.ts","sourceRoot":"","sources":["../../src/transcription/whisperService.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB;AAcD;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAoD9B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAkB9B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAIxD"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, unlinkSync, readFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, basename } from "node:path";
|
|
5
|
+
import { nodewhisper } from "nodejs-whisper";
|
|
6
|
+
/**
|
|
7
|
+
* Strip timestamps from Whisper output and return plain text.
|
|
8
|
+
*/
|
|
9
|
+
function stripTimestamps(rawText) {
|
|
10
|
+
return rawText
|
|
11
|
+
.replace(/\[\d{2}:\d{2}:\d{2}\.\d{3}\s*-->\s*\d{2}:\d{2}:\d{2}\.\d{3}\]\s*/g, "")
|
|
12
|
+
.split("\n")
|
|
13
|
+
.map((line) => line.trim())
|
|
14
|
+
.filter((line) => line.length > 0)
|
|
15
|
+
.join(" ");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract audio from video file using ffmpeg.
|
|
19
|
+
*/
|
|
20
|
+
export function extractAudio(videoPath) {
|
|
21
|
+
const tempDir = tmpdir();
|
|
22
|
+
const audioPath = join(tempDir, `${basename(videoPath, ".mp4")}-${Date.now()}.wav`);
|
|
23
|
+
execSync(`ffmpeg -i "${videoPath}" -ar 16000 -ac 1 -c:a pcm_s16le "${audioPath}" -y`, { stdio: "pipe" });
|
|
24
|
+
return audioPath;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Transcribe audio file using Whisper.
|
|
28
|
+
*/
|
|
29
|
+
export async function transcribeAudio(audioPath, options = {}) {
|
|
30
|
+
const model = options.model ?? "small";
|
|
31
|
+
const language = options.language ?? "de";
|
|
32
|
+
const startTime = performance.now();
|
|
33
|
+
// nodejs-whisper writes output to a .txt file alongside the input
|
|
34
|
+
await nodewhisper(audioPath, {
|
|
35
|
+
modelName: model,
|
|
36
|
+
autoDownloadModelName: model,
|
|
37
|
+
removeWavFileAfterTranscription: false,
|
|
38
|
+
whisperOptions: {
|
|
39
|
+
outputInText: true,
|
|
40
|
+
outputInSrt: false,
|
|
41
|
+
outputInVtt: false,
|
|
42
|
+
translateToEnglish: false,
|
|
43
|
+
language: language,
|
|
44
|
+
wordTimestamps: false,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const processingTime = (performance.now() - startTime) / 1000;
|
|
48
|
+
// Read the generated transcript file
|
|
49
|
+
const txtPath = `${audioPath}.txt`;
|
|
50
|
+
let rawText = "";
|
|
51
|
+
if (existsSync(txtPath)) {
|
|
52
|
+
rawText = readFileSync(txtPath, "utf-8").trim();
|
|
53
|
+
// Clean up the txt file
|
|
54
|
+
unlinkSync(txtPath);
|
|
55
|
+
}
|
|
56
|
+
// Strip timestamps, return plain text (LLM will format it)
|
|
57
|
+
const text = stripTimestamps(rawText);
|
|
58
|
+
// Get audio duration
|
|
59
|
+
let duration = 0;
|
|
60
|
+
try {
|
|
61
|
+
const durationOutput = execSync(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audioPath}"`, { encoding: "utf-8" });
|
|
62
|
+
duration = parseFloat(durationOutput.trim()) || 0;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore duration errors
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
text,
|
|
69
|
+
duration,
|
|
70
|
+
processingTime,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Transcribe a video file (extracts audio first).
|
|
75
|
+
*/
|
|
76
|
+
export async function transcribeVideo(videoPath, options = {}) {
|
|
77
|
+
if (!existsSync(videoPath)) {
|
|
78
|
+
throw new Error(`Video file not found: ${videoPath}`);
|
|
79
|
+
}
|
|
80
|
+
// Extract audio
|
|
81
|
+
const audioPath = extractAudio(videoPath);
|
|
82
|
+
try {
|
|
83
|
+
// Transcribe
|
|
84
|
+
const result = await transcribeAudio(audioPath, options);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
// Cleanup temp audio file
|
|
89
|
+
if (existsSync(audioPath)) {
|
|
90
|
+
unlinkSync(audioPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if whisper model exists (models are auto-downloaded on first use).
|
|
96
|
+
*/
|
|
97
|
+
export function checkModel(_model) {
|
|
98
|
+
// Models are stored in nodejs-whisper's models directory
|
|
99
|
+
// They are auto-downloaded on first actual transcription
|
|
100
|
+
return true; // Let nodejs-whisper handle downloads
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=whisperService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whisperService.js","sourceRoot":"","sources":["../../src/transcription/whisperService.ts"],"names":[],"mappings":"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"}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import eslint from "@eslint/js";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
3
|
+
import prettier from "eslint-config-prettier";
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
eslint.configs.recommended,
|
|
7
|
+
...tseslint.configs.strictTypeChecked,
|
|
8
|
+
...tseslint.configs.stylisticTypeChecked,
|
|
9
|
+
{
|
|
10
|
+
languageOptions: {
|
|
11
|
+
parserOptions: {
|
|
12
|
+
projectService: true,
|
|
13
|
+
tsconfigRootDir: import.meta.dirname,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
files: ["src/**/*.ts"],
|
|
19
|
+
rules: {
|
|
20
|
+
// Allow underscore-prefixed unused vars (common pattern)
|
|
21
|
+
"@typescript-eslint/no-unused-vars": [
|
|
22
|
+
"error",
|
|
23
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
24
|
+
],
|
|
25
|
+
// Allow numbers/booleans in template literals
|
|
26
|
+
"@typescript-eslint/restrict-template-expressions": [
|
|
27
|
+
"error",
|
|
28
|
+
{ allowNumber: true, allowBoolean: true },
|
|
29
|
+
],
|
|
30
|
+
// Empty callbacks are fine (e.g., .catch(() => {}))
|
|
31
|
+
"@typescript-eslint/no-empty-function": "off",
|
|
32
|
+
// Defensive coding with optional chains is fine
|
|
33
|
+
"@typescript-eslint/no-unnecessary-condition": "off",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
files: ["src/**/*.test.ts"],
|
|
38
|
+
rules: {
|
|
39
|
+
// Tests need more flexibility with mocks and fixtures
|
|
40
|
+
"@typescript-eslint/require-await": "off",
|
|
41
|
+
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
42
|
+
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
43
|
+
"@typescript-eslint/only-throw-error": "off",
|
|
44
|
+
"@typescript-eslint/no-non-null-assertion": "off",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
files: ["*.config.js", "*.config.ts"],
|
|
49
|
+
...tseslint.configs.disableTypeChecked,
|
|
50
|
+
},
|
|
51
|
+
prettier,
|
|
52
|
+
{
|
|
53
|
+
ignores: ["dist/**", "node_modules/**"],
|
|
54
|
+
},
|
|
55
|
+
);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offcourse",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Download online courses for offline access – of course!
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Download online courses for offline access – of course!",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"offcourse": "./cli.js"
|
|
7
|
+
"offcourse": "./dist/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -13,21 +13,78 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/sebastian-software/offcourse/issues"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://
|
|
16
|
+
"homepage": "https://offcourse.app",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/cli/index.js",
|
|
21
|
+
"lint": "eslint src",
|
|
22
|
+
"lint:fix": "eslint src --fix",
|
|
23
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
24
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"test:coverage": "vitest run --coverage",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"release": "release-it",
|
|
29
|
+
"prepare": "husky"
|
|
30
|
+
},
|
|
31
|
+
"lint-staged": {
|
|
32
|
+
"src/**/*.ts": [
|
|
33
|
+
"prettier --write"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
17
36
|
"keywords": [
|
|
18
37
|
"course",
|
|
19
38
|
"downloader",
|
|
20
39
|
"offline",
|
|
21
40
|
"cli",
|
|
22
41
|
"e-learning",
|
|
23
|
-
"video"
|
|
24
|
-
"skool",
|
|
25
|
-
"loom"
|
|
42
|
+
"video"
|
|
26
43
|
],
|
|
27
|
-
"author": "
|
|
44
|
+
"author": "",
|
|
28
45
|
"license": "MIT",
|
|
29
46
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
31
|
-
}
|
|
47
|
+
"node": ">=22.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@sindresorhus/slugify": "^3.0.0",
|
|
51
|
+
"better-sqlite3": "^12.5.0",
|
|
52
|
+
"chalk": "^5.6.2",
|
|
53
|
+
"cli-progress": "^3.12.0",
|
|
54
|
+
"commander": "^14.0.2",
|
|
55
|
+
"conf": "^15.0.2",
|
|
56
|
+
"delay": "^7.0.0",
|
|
57
|
+
"execa": "^9.6.1",
|
|
58
|
+
"hls-parser": "^0.16.0",
|
|
59
|
+
"ky": "^1.14.1",
|
|
60
|
+
"ora": "^9.0.0",
|
|
61
|
+
"p-queue": "^9.0.1",
|
|
62
|
+
"p-retry": "^7.1.1",
|
|
63
|
+
"playwright": "^1.57.0",
|
|
64
|
+
"turndown": "^7.2.2",
|
|
65
|
+
"untildify": "^6.0.0",
|
|
66
|
+
"zod": "^4.2.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@commitlint/cli": "^20.2.0",
|
|
70
|
+
"@commitlint/config-conventional": "^20.2.0",
|
|
71
|
+
"@release-it/conventional-changelog": "^10.0.4",
|
|
72
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
73
|
+
"@types/cli-progress": "^3.11.6",
|
|
74
|
+
"@types/node": "^22.10.2",
|
|
75
|
+
"@types/turndown": "^5.0.6",
|
|
76
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
77
|
+
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
78
|
+
"eslint": "^9.39.2",
|
|
79
|
+
"eslint-config-prettier": "^10.1.8",
|
|
80
|
+
"husky": "^9.1.7",
|
|
81
|
+
"lint-staged": "^16.2.7",
|
|
82
|
+
"prettier": "^3.7.4",
|
|
83
|
+
"release-it": "^19.1.0",
|
|
84
|
+
"tsx": "^4.21.0",
|
|
85
|
+
"typescript": "^5.9.3",
|
|
86
|
+
"typescript-eslint": "^8.50.1",
|
|
87
|
+
"vitest": "^4.0.16"
|
|
88
|
+
},
|
|
89
|
+
"packageManager": "pnpm@10.26.1+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa"
|
|
32
90
|
}
|
|
33
|
-
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "7a06a12e-3501-42f6-bd60-4144d9d9d7ea",
|
|
3
|
+
"locationId": "EREskxmNprHK9DrsVSDw",
|
|
4
|
+
"originId": null,
|
|
5
|
+
"userId": "internal",
|
|
6
|
+
"title": "Introduction to the Course",
|
|
7
|
+
"description": "Welcome to our comprehensive course! In this lesson, we'll cover the basics.",
|
|
8
|
+
"categoryId": "cat-abc-123",
|
|
9
|
+
"visibility": "published",
|
|
10
|
+
"commentPermission": "enabled",
|
|
11
|
+
"sequenceNo": 1,
|
|
12
|
+
"posterImage": "/memberships/EREskxmNprHK9DrsVSDw/post/thumbnail.png",
|
|
13
|
+
"commentStatus": "open",
|
|
14
|
+
"contentId": null,
|
|
15
|
+
"contentType": "video",
|
|
16
|
+
"productId": "e5f64bf3-9d88-4d02-b10e-516f47866094",
|
|
17
|
+
"lockedByPost": null,
|
|
18
|
+
"lockedByCategory": null,
|
|
19
|
+
"certificateTemplateId": null,
|
|
20
|
+
"metaData": null,
|
|
21
|
+
"createdAt": "2025-08-11T15:33:59.000Z",
|
|
22
|
+
"updatedAt": "2025-08-11T15:47:25.000Z",
|
|
23
|
+
"deletedAt": null,
|
|
24
|
+
"video": {
|
|
25
|
+
"id": "5b47db50-4043-43d2-a4d4-331471b88070",
|
|
26
|
+
"locationId": "EREskxmNprHK9DrsVSDw",
|
|
27
|
+
"originId": null,
|
|
28
|
+
"userId": "internal",
|
|
29
|
+
"title": "Introduction Video",
|
|
30
|
+
"url": "https://storage.googleapis.com/revex-membership-production/memberships/EREskxmNprHK9DrsVSDw/videos/cts-184162b5f0747fcd_1080p.mp4",
|
|
31
|
+
"thumbnail": null,
|
|
32
|
+
"postId": "7a06a12e-3501-42f6-bd60-4144d9d9d7ea",
|
|
33
|
+
"transcodingStatus": "completed",
|
|
34
|
+
"hdTranscoded": null,
|
|
35
|
+
"videoFormats": ["360", "1080", "480", "720"],
|
|
36
|
+
"rawSize": null,
|
|
37
|
+
"transcodedSize": "18463027",
|
|
38
|
+
"assetsLicenseId": "689a0d672ea246086539f453",
|
|
39
|
+
"transcoderWorkerVideoId": "184162b5f0747fcd",
|
|
40
|
+
"transcodingEventHistory": [
|
|
41
|
+
{
|
|
42
|
+
"event": "VIDEO_TRANSCODED",
|
|
43
|
+
"timestamp": "2025-08-11T15:47:25.705Z"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"metaData": null,
|
|
47
|
+
"createdAt": "2025-08-11T15:33:59.000Z",
|
|
48
|
+
"updatedAt": "2025-08-11T15:47:25.000Z",
|
|
49
|
+
"deletedAt": null
|
|
50
|
+
},
|
|
51
|
+
"audio": null,
|
|
52
|
+
"post_materials": [
|
|
53
|
+
{
|
|
54
|
+
"id": "mat-001",
|
|
55
|
+
"name": "Course Workbook.pdf",
|
|
56
|
+
"url": "https://storage.example.com/materials/workbook.pdf",
|
|
57
|
+
"type": "pdf"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "mat-002",
|
|
61
|
+
"name": "Resource Links.txt",
|
|
62
|
+
"url": "https://storage.example.com/materials/links.txt",
|
|
63
|
+
"type": "text"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"asset_urls": {}
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#EXTM3U
|
|
2
|
+
#EXT-X-VERSION:4
|
|
3
|
+
#EXT-X-INDEPENDENT-SEGMENTS
|
|
4
|
+
|
|
5
|
+
# 240p quality
|
|
6
|
+
#EXT-X-STREAM-INF:BANDWIDTH=326400,AVERAGE-BANDWIDTH=246064,CODECS="avc1.4D401E,mp4a.40.2",RESOLUTION=426x240,FRAME-RATE=24.000
|
|
7
|
+
https://cdn.example.com/video/240p/index.m3u8
|
|
8
|
+
|
|
9
|
+
# 360p quality
|
|
10
|
+
#EXT-X-STREAM-INF:BANDWIDTH=796800,AVERAGE-BANDWIDTH=602416,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=24.000
|
|
11
|
+
https://cdn.example.com/video/360p/index.m3u8
|
|
12
|
+
|
|
13
|
+
# 480p quality
|
|
14
|
+
#EXT-X-STREAM-INF:BANDWIDTH=1680000,AVERAGE-BANDWIDTH=1270416,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=854x480,FRAME-RATE=24.000
|
|
15
|
+
https://cdn.example.com/video/480p/index.m3u8
|
|
16
|
+
|
|
17
|
+
# 720p quality
|
|
18
|
+
#EXT-X-STREAM-INF:BANDWIDTH=3200000,AVERAGE-BANDWIDTH=2400000,CODECS="avc1.4D4020,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=24.000
|
|
19
|
+
https://cdn.example.com/video/720p/index.m3u8
|
|
20
|
+
|
|
21
|
+
# 1080p quality
|
|
22
|
+
#EXT-X-STREAM-INF:BANDWIDTH=6000000,AVERAGE-BANDWIDTH=4500000,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=24.000
|
|
23
|
+
https://cdn.example.com/video/1080p/index.m3u8
|
|
24
|
+
|