offcourse 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/inspect.js +1 -1
- package/dist/cli/commands/inspect.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +1 -2
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +13 -14
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/commands/syncHighLevel.d.ts +1 -2
- package/dist/cli/commands/syncHighLevel.d.ts.map +1 -1
- package/dist/cli/commands/syncHighLevel.js +4 -8
- package/dist/cli/commands/syncHighLevel.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/config/configManager.d.ts.map +1 -1
- package/dist/config/configManager.js +4 -0
- package/dist/config/configManager.js.map +1 -1
- package/dist/downloader/hlsDownloader.d.ts.map +1 -1
- package/dist/downloader/hlsDownloader.js +23 -14
- package/dist/downloader/hlsDownloader.js.map +1 -1
- package/dist/downloader/hlsValidator.d.ts.map +1 -1
- package/dist/downloader/hlsValidator.js +6 -2
- package/dist/downloader/hlsValidator.js.map +1 -1
- package/dist/downloader/index.d.ts +3 -0
- package/dist/downloader/index.d.ts.map +1 -1
- package/dist/downloader/index.js +3 -0
- package/dist/downloader/index.js.map +1 -1
- package/dist/downloader/loomDownloader.d.ts.map +1 -1
- package/dist/downloader/loomDownloader.js +23 -20
- package/dist/downloader/loomDownloader.js.map +1 -1
- package/dist/downloader/queue.d.ts +4 -4
- package/dist/downloader/queue.d.ts.map +1 -1
- package/dist/downloader/queue.js.map +1 -1
- package/dist/downloader/vimeoDownloader.d.ts.map +1 -1
- package/dist/downloader/vimeoDownloader.js +7 -3
- package/dist/downloader/vimeoDownloader.js.map +1 -1
- package/dist/scraper/extractor.d.ts +4 -0
- package/dist/scraper/extractor.d.ts.map +1 -1
- package/dist/scraper/extractor.js +79 -79
- package/dist/scraper/extractor.js.map +1 -1
- package/dist/scraper/highlevel/extractor.d.ts +11 -19
- package/dist/scraper/highlevel/extractor.d.ts.map +1 -1
- package/dist/scraper/highlevel/extractor.js +72 -85
- package/dist/scraper/highlevel/extractor.js.map +1 -1
- package/dist/scraper/highlevel/navigator.d.ts +3 -10
- package/dist/scraper/highlevel/navigator.d.ts.map +1 -1
- package/dist/scraper/highlevel/navigator.js +140 -127
- package/dist/scraper/highlevel/navigator.js.map +1 -1
- package/dist/scraper/highlevel/schemas.d.ts +188 -0
- package/dist/scraper/highlevel/schemas.d.ts.map +1 -0
- package/dist/scraper/highlevel/schemas.js +139 -0
- package/dist/scraper/highlevel/schemas.js.map +1 -0
- package/dist/scraper/navigator.d.ts +14 -11
- package/dist/scraper/navigator.d.ts.map +1 -1
- package/dist/scraper/navigator.js +61 -104
- package/dist/scraper/navigator.js.map +1 -1
- package/dist/scraper/schemas.d.ts +57 -0
- package/dist/scraper/schemas.d.ts.map +1 -0
- package/dist/scraper/schemas.js +135 -0
- package/dist/scraper/schemas.js.map +1 -0
- package/dist/scraper/videoInterceptor.d.ts +4 -0
- package/dist/scraper/videoInterceptor.d.ts.map +1 -1
- package/dist/scraper/videoInterceptor.js +66 -51
- package/dist/scraper/videoInterceptor.js.map +1 -1
- package/dist/shared/auth.d.ts +9 -9
- package/dist/shared/auth.d.ts.map +1 -1
- package/dist/shared/auth.js +24 -38
- package/dist/shared/auth.js.map +1 -1
- package/dist/shared/firebase.d.ts +60 -0
- package/dist/shared/firebase.d.ts.map +1 -0
- package/dist/shared/firebase.js +102 -0
- package/dist/shared/firebase.js.map +1 -0
- package/dist/shared/fs.d.ts.map +1 -1
- package/dist/shared/fs.js +4 -0
- package/dist/shared/fs.js.map +1 -1
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +3 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/slug.d.ts +11 -0
- package/dist/shared/slug.d.ts.map +1 -0
- package/{src/shared/slug.ts → dist/shared/slug.js} +10 -11
- package/dist/shared/slug.js.map +1 -0
- package/dist/shared/url.d.ts +43 -0
- package/dist/shared/url.d.ts.map +1 -0
- package/{src/shared/url.ts → dist/shared/url.js} +12 -15
- package/dist/shared/url.js.map +1 -0
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.d.ts.map +1 -1
- package/dist/state/database.js +3 -0
- package/dist/state/database.js.map +1 -1
- package/dist/storage/fileSystem.d.ts +17 -17
- package/dist/storage/fileSystem.d.ts.map +1 -1
- package/dist/storage/fileSystem.js +39 -31
- package/dist/storage/fileSystem.js.map +1 -1
- package/package.json +5 -2
- package/.github/workflows/ci.yml +0 -50
- package/.husky/commit-msg +0 -2
- package/.husky/pre-commit +0 -1
- package/.husky/pre-push +0 -3
- package/.prettierrc +0 -8
- package/.release-it.json +0 -23
- package/ARCHITECTURE.md +0 -233
- package/CHANGELOG.md +0 -78
- package/commitlint.config.js +0 -4
- package/dist/ai/openRouter.d.ts +0 -47
- package/dist/ai/openRouter.d.ts.map +0 -1
- package/dist/ai/openRouter.js +0 -116
- package/dist/ai/openRouter.js.map +0 -1
- package/dist/ai/transcriptPolisher.d.ts +0 -24
- package/dist/ai/transcriptPolisher.d.ts.map +0 -1
- package/dist/ai/transcriptPolisher.js +0 -89
- package/dist/ai/transcriptPolisher.js.map +0 -1
- package/dist/cli/commands/enrich.d.ts +0 -14
- package/dist/cli/commands/enrich.d.ts.map +0 -1
- package/dist/cli/commands/enrich.js +0 -271
- package/dist/cli/commands/enrich.js.map +0 -1
- package/dist/cli/commands/syncGhl.d.ts +0 -20
- package/dist/cli/commands/syncGhl.d.ts.map +0 -1
- package/dist/cli/commands/syncGhl.js +0 -483
- package/dist/cli/commands/syncGhl.js.map +0 -1
- package/dist/cli/commands/syncHighLevel.test.d.ts +0 -2
- package/dist/cli/commands/syncHighLevel.test.d.ts.map +0 -1
- package/dist/cli/commands/syncHighLevel.test.js +0 -102
- package/dist/cli/commands/syncHighLevel.test.js.map +0 -1
- package/dist/config/paths.test.d.ts +0 -2
- package/dist/config/paths.test.d.ts.map +0 -1
- package/dist/config/paths.test.js +0 -70
- package/dist/config/paths.test.js.map +0 -1
- package/dist/config/schema.test.d.ts +0 -2
- package/dist/config/schema.test.d.ts.map +0 -1
- package/dist/config/schema.test.js +0 -151
- package/dist/config/schema.test.js.map +0 -1
- package/dist/downloader/hlsDownloader.test.d.ts +0 -2
- package/dist/downloader/hlsDownloader.test.d.ts.map +0 -1
- package/dist/downloader/hlsDownloader.test.js +0 -116
- package/dist/downloader/hlsDownloader.test.js.map +0 -1
- package/dist/downloader/loomDownloader.test.d.ts +0 -2
- package/dist/downloader/loomDownloader.test.d.ts.map +0 -1
- package/dist/downloader/loomDownloader.test.js +0 -36
- package/dist/downloader/loomDownloader.test.js.map +0 -1
- package/dist/downloader/queue.test.d.ts +0 -2
- package/dist/downloader/queue.test.d.ts.map +0 -1
- package/dist/downloader/queue.test.js +0 -158
- package/dist/downloader/queue.test.js.map +0 -1
- package/dist/downloader/videoDownloader.d.ts +0 -32
- package/dist/downloader/videoDownloader.d.ts.map +0 -1
- package/dist/downloader/videoDownloader.js +0 -173
- package/dist/downloader/videoDownloader.js.map +0 -1
- package/dist/downloader/vimeoDownloader.test.d.ts +0 -2
- package/dist/downloader/vimeoDownloader.test.d.ts.map +0 -1
- package/dist/downloader/vimeoDownloader.test.js +0 -51
- package/dist/downloader/vimeoDownloader.test.js.map +0 -1
- package/dist/scraper/auth.d.ts +0 -29
- package/dist/scraper/auth.d.ts.map +0 -1
- package/dist/scraper/auth.js +0 -115
- package/dist/scraper/auth.js.map +0 -1
- package/dist/scraper/extractor.test.d.ts +0 -2
- package/dist/scraper/extractor.test.d.ts.map +0 -1
- package/dist/scraper/extractor.test.js +0 -65
- package/dist/scraper/extractor.test.js.map +0 -1
- package/dist/scraper/ghl/auth.d.ts +0 -25
- package/dist/scraper/ghl/auth.d.ts.map +0 -1
- package/dist/scraper/ghl/auth.js +0 -187
- package/dist/scraper/ghl/auth.js.map +0 -1
- package/dist/scraper/ghl/extractor.d.ts +0 -96
- package/dist/scraper/ghl/extractor.d.ts.map +0 -1
- package/dist/scraper/ghl/extractor.js +0 -345
- package/dist/scraper/ghl/extractor.js.map +0 -1
- package/dist/scraper/ghl/index.d.ts +0 -4
- package/dist/scraper/ghl/index.d.ts.map +0 -1
- package/dist/scraper/ghl/index.js +0 -4
- package/dist/scraper/ghl/index.js.map +0 -1
- package/dist/scraper/ghl/navigator.d.ts +0 -93
- package/dist/scraper/ghl/navigator.d.ts.map +0 -1
- package/dist/scraper/ghl/navigator.js +0 -447
- package/dist/scraper/ghl/navigator.js.map +0 -1
- package/dist/scraper/highlevel/auth.d.ts +0 -25
- package/dist/scraper/highlevel/auth.d.ts.map +0 -1
- package/dist/scraper/highlevel/auth.js +0 -189
- package/dist/scraper/highlevel/auth.js.map +0 -1
- package/dist/scraper/highlevel/extractor.test.d.ts +0 -2
- package/dist/scraper/highlevel/extractor.test.d.ts.map +0 -1
- package/dist/scraper/highlevel/extractor.test.js +0 -101
- package/dist/scraper/highlevel/extractor.test.js.map +0 -1
- package/dist/scraper/highlevel/navigator.test.d.ts +0 -2
- package/dist/scraper/highlevel/navigator.test.d.ts.map +0 -1
- package/dist/scraper/highlevel/navigator.test.js +0 -78
- package/dist/scraper/highlevel/navigator.test.js.map +0 -1
- package/dist/scraper/navigator.test.d.ts +0 -2
- package/dist/scraper/navigator.test.d.ts.map +0 -1
- package/dist/scraper/navigator.test.js +0 -63
- package/dist/scraper/navigator.test.js.map +0 -1
- package/dist/scraper/skoolApi.d.ts +0 -17
- package/dist/scraper/skoolApi.d.ts.map +0 -1
- package/dist/scraper/skoolApi.js +0 -72
- package/dist/scraper/skoolApi.js.map +0 -1
- package/dist/state/database.test.d.ts +0 -2
- package/dist/state/database.test.d.ts.map +0 -1
- package/dist/state/database.test.js +0 -34
- package/dist/state/database.test.js.map +0 -1
- package/dist/transcription/whisperService.d.ts +0 -27
- package/dist/transcription/whisperService.d.ts.map +0 -1
- package/dist/transcription/whisperService.js +0 -102
- package/dist/transcription/whisperService.js.map +0 -1
- package/eslint.config.js +0 -55
- package/src/__fixtures__/highlevel-post-response.json +0 -68
- package/src/__fixtures__/hls-master-playlist.m3u8 +0 -24
- package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +0 -38
- package/src/cli/commands/config.ts +0 -74
- package/src/cli/commands/inspect.ts +0 -441
- package/src/cli/commands/login.ts +0 -68
- package/src/cli/commands/status.ts +0 -147
- package/src/cli/commands/sync.ts +0 -1235
- package/src/cli/commands/syncHighLevel.test.ts +0 -144
- package/src/cli/commands/syncHighLevel.ts +0 -639
- package/src/cli/index.ts +0 -121
- package/src/config/configManager.ts +0 -75
- package/src/config/paths.test.ts +0 -83
- package/src/config/paths.ts +0 -36
- package/src/config/schema.test.ts +0 -173
- package/src/config/schema.ts +0 -65
- package/src/downloader/hlsDownloader.test.ts +0 -148
- package/src/downloader/hlsDownloader.ts +0 -327
- package/src/downloader/hlsValidator.ts +0 -196
- package/src/downloader/index.ts +0 -122
- package/src/downloader/loomDownloader.test.ts +0 -43
- package/src/downloader/loomDownloader.ts +0 -742
- package/src/downloader/queue.test.ts +0 -199
- package/src/downloader/queue.ts +0 -118
- package/src/downloader/vimeoDownloader.test.ts +0 -62
- package/src/downloader/vimeoDownloader.ts +0 -722
- package/src/scraper/extractor.test.ts +0 -124
- package/src/scraper/extractor.ts +0 -757
- package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +0 -41
- package/src/scraper/highlevel/extractor.test.ts +0 -134
- package/src/scraper/highlevel/extractor.ts +0 -537
- package/src/scraper/highlevel/index.ts +0 -2
- package/src/scraper/highlevel/navigator.test.ts +0 -110
- package/src/scraper/highlevel/navigator.ts +0 -668
- package/src/scraper/highlevel/schemas.ts +0 -183
- package/src/scraper/navigator.test.ts +0 -122
- package/src/scraper/navigator.ts +0 -355
- package/src/scraper/schemas.ts +0 -177
- package/src/scraper/videoInterceptor.ts +0 -435
- package/src/shared/auth.test.ts +0 -58
- package/src/shared/auth.ts +0 -251
- package/src/shared/firebase.ts +0 -151
- package/src/shared/fs.ts +0 -80
- package/src/shared/http.ts +0 -34
- package/src/shared/index.ts +0 -6
- package/src/shared/url.test.ts +0 -122
- package/src/state/database.test.ts +0 -49
- package/src/state/database.ts +0 -919
- package/src/state/index.ts +0 -14
- package/src/storage/fileSystem.test.ts +0 -64
- package/src/storage/fileSystem.ts +0 -175
- package/tsconfig.json +0 -28
- package/vitest.config.ts +0 -29
|
@@ -1,722 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, existsSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { USER_AGENT } from "../shared/http.js";
|
|
4
|
-
import { getBaseUrl } from "../shared/url.js";
|
|
5
|
-
|
|
6
|
-
export interface VimeoVideoInfo {
|
|
7
|
-
id: string;
|
|
8
|
-
title: string;
|
|
9
|
-
duration: number;
|
|
10
|
-
width: number;
|
|
11
|
-
height: number;
|
|
12
|
-
hlsUrl: string | null;
|
|
13
|
-
progressiveUrl: string | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface VimeoFetchResult {
|
|
17
|
-
success: boolean;
|
|
18
|
-
info?: VimeoVideoInfo;
|
|
19
|
-
error?: string;
|
|
20
|
-
errorCode?:
|
|
21
|
-
| "VIDEO_NOT_FOUND"
|
|
22
|
-
| "DRM_PROTECTED"
|
|
23
|
-
| "PRIVATE_VIDEO"
|
|
24
|
-
| "RATE_LIMITED"
|
|
25
|
-
| "NETWORK_ERROR"
|
|
26
|
-
| "PARSE_ERROR";
|
|
27
|
-
details?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface DownloadProgress {
|
|
31
|
-
percent: number;
|
|
32
|
-
downloaded: number;
|
|
33
|
-
total: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface VimeoDownloadResult {
|
|
37
|
-
success: boolean;
|
|
38
|
-
error?: string;
|
|
39
|
-
errorCode?: VimeoFetchResult["errorCode"] | "INVALID_URL" | "NO_STREAM" | "DOWNLOAD_FAILED";
|
|
40
|
-
details?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Extracts the Vimeo video ID from various URL formats.
|
|
45
|
-
*/
|
|
46
|
-
export function extractVimeoId(url: string): string | null {
|
|
47
|
-
// Handle various Vimeo URL formats:
|
|
48
|
-
// https://vimeo.com/123456789
|
|
49
|
-
// https://vimeo.com/123456789?share=copy
|
|
50
|
-
// https://player.vimeo.com/video/123456789
|
|
51
|
-
// https://vimeo.com/channels/xxx/123456789
|
|
52
|
-
const patterns = [
|
|
53
|
-
/vimeo\.com\/(?:video\/)?(\d+)/,
|
|
54
|
-
/player\.vimeo\.com\/video\/(\d+)/,
|
|
55
|
-
/vimeo\.com\/channels\/[^/]+\/(\d+)/,
|
|
56
|
-
/vimeo\.com\/groups\/[^/]+\/videos\/(\d+)/,
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
for (const pattern of patterns) {
|
|
60
|
-
const match = url.match(pattern);
|
|
61
|
-
if (match?.[1]) {
|
|
62
|
-
return match[1];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Network I/O and file operations - excluded from coverage
|
|
70
|
-
/* v8 ignore start */
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Extracts the unlisted hash from a Vimeo URL if present.
|
|
74
|
-
* Unlisted videos require this hash to access.
|
|
75
|
-
*/
|
|
76
|
-
function extractUnlistedHash(url: string): string | null {
|
|
77
|
-
// Format: https://vimeo.com/123456789/abcdef1234
|
|
78
|
-
// Or in player: https://player.vimeo.com/video/123456789?h=abcdef1234
|
|
79
|
-
const pathMatch = /vimeo\.com\/\d+\/([a-f0-9]+)/.exec(url);
|
|
80
|
-
if (pathMatch?.[1]) {
|
|
81
|
-
return pathMatch[1];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const paramMatch = /[?&]h=([a-f0-9]+)/.exec(url);
|
|
85
|
-
if (paramMatch?.[1]) {
|
|
86
|
-
return paramMatch[1];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Fetches video information from Vimeo's player config.
|
|
94
|
-
* @param referer - Optional referer URL (e.g., the Skool page URL) for domain-restricted videos
|
|
95
|
-
*/
|
|
96
|
-
export async function getVimeoVideoInfo(
|
|
97
|
-
videoId: string,
|
|
98
|
-
unlistedHash?: string | null,
|
|
99
|
-
referer?: string
|
|
100
|
-
): Promise<VimeoFetchResult> {
|
|
101
|
-
// Try the config endpoint first
|
|
102
|
-
let configUrl = `https://player.vimeo.com/video/${videoId}/config`;
|
|
103
|
-
if (unlistedHash) {
|
|
104
|
-
configUrl += `?h=${unlistedHash}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Build headers - use provided referer for domain-restricted videos
|
|
108
|
-
const headers: Record<string, string> = {
|
|
109
|
-
"User-Agent": USER_AGENT,
|
|
110
|
-
Accept: "application/json",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Try with Skool referer first if provided, otherwise use Vimeo's player
|
|
114
|
-
if (referer) {
|
|
115
|
-
headers.Referer = referer;
|
|
116
|
-
headers.Origin = new URL(referer).origin;
|
|
117
|
-
} else {
|
|
118
|
-
headers.Referer = "https://player.vimeo.com/";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
let response = await fetch(configUrl, { headers });
|
|
123
|
-
|
|
124
|
-
// If we got 403 with a custom referer, the video might be strictly domain-locked
|
|
125
|
-
// Try with the embed page URL as referer
|
|
126
|
-
if (response.status === 403 && referer) {
|
|
127
|
-
headers.Referer = `https://player.vimeo.com/video/${videoId}`;
|
|
128
|
-
headers.Origin = "https://player.vimeo.com";
|
|
129
|
-
response = await fetch(configUrl, { headers });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (response.status === 404) {
|
|
133
|
-
return {
|
|
134
|
-
success: false,
|
|
135
|
-
error: "Video not found",
|
|
136
|
-
errorCode: "VIDEO_NOT_FOUND",
|
|
137
|
-
details: `Video ID: ${videoId}`,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (response.status === 403) {
|
|
142
|
-
return {
|
|
143
|
-
success: false,
|
|
144
|
-
error: "Video is private or requires authentication",
|
|
145
|
-
errorCode: "PRIVATE_VIDEO",
|
|
146
|
-
details: `Video ID: ${videoId}. This video may require login or is restricted.`,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (response.status === 429) {
|
|
151
|
-
return {
|
|
152
|
-
success: false,
|
|
153
|
-
error: "Rate limited by Vimeo",
|
|
154
|
-
errorCode: "RATE_LIMITED",
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!response.ok) {
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
error: `Vimeo returned HTTP ${response.status}`,
|
|
162
|
-
errorCode: "NETWORK_ERROR",
|
|
163
|
-
details: `Config URL: ${configUrl}`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const config = (await response.json()) as VimeoConfig;
|
|
168
|
-
|
|
169
|
-
// Check for DRM
|
|
170
|
-
if (
|
|
171
|
-
config.request?.files?.dash?.cdns &&
|
|
172
|
-
!config.request?.files?.hls &&
|
|
173
|
-
!config.request?.files?.progressive
|
|
174
|
-
) {
|
|
175
|
-
// Only DASH with no HLS/progressive usually means DRM
|
|
176
|
-
const hasDrm = config.video?.drm ?? config.request?.drm;
|
|
177
|
-
if (hasDrm) {
|
|
178
|
-
return {
|
|
179
|
-
success: false,
|
|
180
|
-
error: "Video is DRM protected and cannot be downloaded",
|
|
181
|
-
errorCode: "DRM_PROTECTED",
|
|
182
|
-
details: `Video "${config.video?.title ?? videoId}" uses DRM protection. This video cannot be downloaded without the content provider's authorization.`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Extract HLS URL
|
|
188
|
-
let hlsUrl: string | null = null;
|
|
189
|
-
const hlsCdns = config.request?.files?.hls?.cdns;
|
|
190
|
-
if (hlsCdns) {
|
|
191
|
-
// Prefer akamai_live, then fastly, then any available CDN
|
|
192
|
-
const preferredCdns = ["akfire_interconnect_quic", "akamai_live", "fastly_skyfire", "fastly"];
|
|
193
|
-
for (const cdn of preferredCdns) {
|
|
194
|
-
if (hlsCdns[cdn]?.url) {
|
|
195
|
-
hlsUrl = hlsCdns[cdn].url;
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Fallback to any CDN
|
|
200
|
-
if (!hlsUrl) {
|
|
201
|
-
const cdnKeys = Object.keys(hlsCdns);
|
|
202
|
-
const firstCdn = cdnKeys[0];
|
|
203
|
-
if (firstCdn) {
|
|
204
|
-
const cdnUrl = hlsCdns[firstCdn]?.url;
|
|
205
|
-
if (cdnUrl) {
|
|
206
|
-
hlsUrl = cdnUrl;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Extract progressive (direct MP4) URL - prefer highest quality
|
|
213
|
-
let progressiveUrl: string | null = null;
|
|
214
|
-
const progressive = config.request?.files?.progressive;
|
|
215
|
-
if (progressive && Array.isArray(progressive) && progressive.length > 0) {
|
|
216
|
-
// Sort by height descending to get best quality
|
|
217
|
-
const sorted = [...progressive].sort((a, b) => (b.height ?? 0) - (a.height ?? 0));
|
|
218
|
-
progressiveUrl = sorted[0]?.url ?? null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!hlsUrl && !progressiveUrl) {
|
|
222
|
-
// Check if this is a DRM-only video
|
|
223
|
-
if (config.request?.files?.dash) {
|
|
224
|
-
return {
|
|
225
|
-
success: false,
|
|
226
|
-
error: "Video only has DRM-protected DASH streams",
|
|
227
|
-
errorCode: "DRM_PROTECTED",
|
|
228
|
-
details: `Video "${config.video?.title ?? videoId}" appears to use DRM protection. No downloadable streams available.`,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
success: false,
|
|
234
|
-
error: "No downloadable video streams found",
|
|
235
|
-
errorCode: "PARSE_ERROR",
|
|
236
|
-
details: "Could not find HLS or progressive download URLs in config",
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
success: true,
|
|
242
|
-
info: {
|
|
243
|
-
id: videoId,
|
|
244
|
-
title: config.video?.title ?? "Vimeo Video",
|
|
245
|
-
duration: config.video?.duration ?? 0,
|
|
246
|
-
width: config.video?.width ?? 1920,
|
|
247
|
-
height: config.video?.height ?? 1080,
|
|
248
|
-
hlsUrl,
|
|
249
|
-
progressiveUrl,
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
} catch (error) {
|
|
253
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
254
|
-
return {
|
|
255
|
-
success: false,
|
|
256
|
-
error: `Network error: ${errorMessage}`,
|
|
257
|
-
errorCode: "NETWORK_ERROR",
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Fetches Vimeo config from within a Playwright browser context.
|
|
264
|
-
* Uses page.request API which runs at browser level (no CORS restrictions)
|
|
265
|
-
* and includes the browser's cookies/session.
|
|
266
|
-
*
|
|
267
|
-
* @param page - Playwright page (should be on the Skool lesson page)
|
|
268
|
-
* @param videoId - Vimeo video ID
|
|
269
|
-
* @param unlistedHash - Optional hash for unlisted videos
|
|
270
|
-
*/
|
|
271
|
-
export async function getVimeoVideoInfoFromBrowser(
|
|
272
|
-
page: import("playwright").Page,
|
|
273
|
-
videoId: string,
|
|
274
|
-
unlistedHash?: string | null
|
|
275
|
-
): Promise<VimeoFetchResult> {
|
|
276
|
-
let configUrl = `https://player.vimeo.com/video/${videoId}/config`;
|
|
277
|
-
if (unlistedHash) {
|
|
278
|
-
configUrl += `?h=${unlistedHash}`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
// Use page.request (Playwright API) - runs at browser level, no CORS issues
|
|
283
|
-
// and includes the browser's cookies/session
|
|
284
|
-
const currentUrl = page.url();
|
|
285
|
-
const response = await page.request.get(configUrl, {
|
|
286
|
-
headers: {
|
|
287
|
-
Accept: "application/json",
|
|
288
|
-
Referer: currentUrl,
|
|
289
|
-
Origin: new URL(currentUrl).origin,
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (response.status() === 403) {
|
|
294
|
-
return {
|
|
295
|
-
success: false,
|
|
296
|
-
error: "Video is private or requires authentication",
|
|
297
|
-
errorCode: "PRIVATE_VIDEO",
|
|
298
|
-
details: `Video ID: ${videoId}. Domain-restricted even with browser session.`,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (response.status() === 404) {
|
|
303
|
-
return {
|
|
304
|
-
success: false,
|
|
305
|
-
error: "Video not found",
|
|
306
|
-
errorCode: "VIDEO_NOT_FOUND",
|
|
307
|
-
details: `Video ID: ${videoId}`,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (!response.ok()) {
|
|
312
|
-
return {
|
|
313
|
-
success: false,
|
|
314
|
-
error: `Vimeo returned HTTP ${response.status()}`,
|
|
315
|
-
errorCode: "NETWORK_ERROR",
|
|
316
|
-
details: `Config URL: ${configUrl}`,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const config = (await response.json()) as VimeoConfig;
|
|
321
|
-
|
|
322
|
-
// Extract HLS URL (same logic as getVimeoVideoInfo)
|
|
323
|
-
let hlsUrl: string | null = null;
|
|
324
|
-
const hlsCdns = config.request?.files?.hls?.cdns;
|
|
325
|
-
if (hlsCdns) {
|
|
326
|
-
const preferredCdns = ["akfire_interconnect_quic", "akamai_live", "fastly_skyfire", "fastly"];
|
|
327
|
-
for (const cdn of preferredCdns) {
|
|
328
|
-
if (hlsCdns[cdn]?.url) {
|
|
329
|
-
hlsUrl = hlsCdns[cdn].url;
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (!hlsUrl) {
|
|
334
|
-
const cdnKeys = Object.keys(hlsCdns);
|
|
335
|
-
const firstCdn = cdnKeys[0];
|
|
336
|
-
if (firstCdn) {
|
|
337
|
-
const cdnUrl = hlsCdns[firstCdn]?.url;
|
|
338
|
-
if (cdnUrl) {
|
|
339
|
-
hlsUrl = cdnUrl;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Extract progressive URL
|
|
346
|
-
let progressiveUrl: string | null = null;
|
|
347
|
-
const progressive = config.request?.files?.progressive;
|
|
348
|
-
if (progressive && Array.isArray(progressive) && progressive.length > 0) {
|
|
349
|
-
const sorted = [...progressive].sort((a, b) => (b.height ?? 0) - (a.height ?? 0));
|
|
350
|
-
progressiveUrl = sorted[0]?.url ?? null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (!hlsUrl && !progressiveUrl) {
|
|
354
|
-
if (config.request?.files?.dash) {
|
|
355
|
-
return {
|
|
356
|
-
success: false,
|
|
357
|
-
error: "Video only has DRM-protected DASH streams",
|
|
358
|
-
errorCode: "DRM_PROTECTED",
|
|
359
|
-
details: `Video "${config.video?.title ?? videoId}" uses DRM. Cannot download.`,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
return {
|
|
363
|
-
success: false,
|
|
364
|
-
error: "No downloadable streams found",
|
|
365
|
-
errorCode: "PARSE_ERROR",
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
success: true,
|
|
371
|
-
info: {
|
|
372
|
-
id: videoId,
|
|
373
|
-
title: config.video?.title ?? "Vimeo Video",
|
|
374
|
-
duration: config.video?.duration ?? 0,
|
|
375
|
-
width: config.video?.width ?? 1920,
|
|
376
|
-
height: config.video?.height ?? 1080,
|
|
377
|
-
hlsUrl,
|
|
378
|
-
progressiveUrl,
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
} catch (error) {
|
|
382
|
-
return {
|
|
383
|
-
success: false,
|
|
384
|
-
error: error instanceof Error ? error.message : String(error),
|
|
385
|
-
errorCode: "NETWORK_ERROR",
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Downloads a Vimeo video.
|
|
392
|
-
* Prefers progressive (direct MP4) download, falls back to HLS.
|
|
393
|
-
*/
|
|
394
|
-
export async function downloadVimeoVideo(
|
|
395
|
-
url: string,
|
|
396
|
-
outputPath: string,
|
|
397
|
-
onProgress?: (progress: DownloadProgress) => void
|
|
398
|
-
): Promise<VimeoDownloadResult> {
|
|
399
|
-
if (existsSync(outputPath)) {
|
|
400
|
-
return { success: true };
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Check if this is already a direct HLS/CDN URL (from previous validation)
|
|
404
|
-
if (url.includes("vimeocdn.com") && url.includes(".m3u8")) {
|
|
405
|
-
// Direct HLS download
|
|
406
|
-
const dir = dirname(outputPath);
|
|
407
|
-
if (!existsSync(dir)) {
|
|
408
|
-
mkdirSync(dir, { recursive: true });
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return downloadHlsVideo(url, outputPath, onProgress);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const videoId = extractVimeoId(url);
|
|
415
|
-
if (!videoId) {
|
|
416
|
-
return {
|
|
417
|
-
success: false,
|
|
418
|
-
error: "Invalid Vimeo URL",
|
|
419
|
-
errorCode: "INVALID_URL",
|
|
420
|
-
details: `Could not extract video ID from: ${url}`,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const unlistedHash = extractUnlistedHash(url);
|
|
425
|
-
const fetchResult = await getVimeoVideoInfo(videoId, unlistedHash);
|
|
426
|
-
|
|
427
|
-
if (!fetchResult.success || !fetchResult.info) {
|
|
428
|
-
const result: VimeoDownloadResult = {
|
|
429
|
-
success: false,
|
|
430
|
-
error: fetchResult.error ?? "Could not fetch video info",
|
|
431
|
-
};
|
|
432
|
-
if (fetchResult.errorCode) {
|
|
433
|
-
result.errorCode = fetchResult.errorCode;
|
|
434
|
-
}
|
|
435
|
-
if (fetchResult.details) {
|
|
436
|
-
result.details = fetchResult.details;
|
|
437
|
-
}
|
|
438
|
-
return result;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const info = fetchResult.info;
|
|
442
|
-
|
|
443
|
-
// Ensure output directory exists
|
|
444
|
-
const dir = dirname(outputPath);
|
|
445
|
-
if (!existsSync(dir)) {
|
|
446
|
-
mkdirSync(dir, { recursive: true });
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Prefer progressive (direct MP4) download - simpler and often better quality
|
|
450
|
-
if (info.progressiveUrl) {
|
|
451
|
-
const result = await downloadProgressiveVideo(info.progressiveUrl, outputPath, onProgress);
|
|
452
|
-
if (result.success) {
|
|
453
|
-
return result;
|
|
454
|
-
}
|
|
455
|
-
// Fall through to HLS if progressive fails
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Try HLS download
|
|
459
|
-
if (info.hlsUrl) {
|
|
460
|
-
return downloadHlsVideo(info.hlsUrl, outputPath, onProgress);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return {
|
|
464
|
-
success: false,
|
|
465
|
-
error: "No downloadable streams available",
|
|
466
|
-
errorCode: "NO_STREAM",
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Downloads a progressive (direct) video file.
|
|
472
|
-
*/
|
|
473
|
-
async function downloadProgressiveVideo(
|
|
474
|
-
url: string,
|
|
475
|
-
outputPath: string,
|
|
476
|
-
onProgress?: (progress: DownloadProgress) => void
|
|
477
|
-
): Promise<VimeoDownloadResult> {
|
|
478
|
-
const tempPath = `${outputPath}.tmp`;
|
|
479
|
-
|
|
480
|
-
try {
|
|
481
|
-
const response = await fetch(url, {
|
|
482
|
-
headers: {
|
|
483
|
-
"User-Agent": USER_AGENT,
|
|
484
|
-
Referer: "https://player.vimeo.com/",
|
|
485
|
-
},
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
if (!response.ok) {
|
|
489
|
-
return {
|
|
490
|
-
success: false,
|
|
491
|
-
error: `Download failed: HTTP ${response.status}`,
|
|
492
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const contentLength = response.headers.get("content-length");
|
|
497
|
-
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
498
|
-
|
|
499
|
-
if (!response.body) {
|
|
500
|
-
return {
|
|
501
|
-
success: false,
|
|
502
|
-
error: "No response body",
|
|
503
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const fileStream = createWriteStream(tempPath);
|
|
508
|
-
const reader = response.body.getReader();
|
|
509
|
-
let downloaded = 0;
|
|
510
|
-
|
|
511
|
-
while (true) {
|
|
512
|
-
const { done, value } = await reader.read();
|
|
513
|
-
if (done) break;
|
|
514
|
-
|
|
515
|
-
fileStream.write(Buffer.from(value));
|
|
516
|
-
downloaded += value.length;
|
|
517
|
-
|
|
518
|
-
if (onProgress && total > 0) {
|
|
519
|
-
onProgress({
|
|
520
|
-
percent: (downloaded / total) * 100,
|
|
521
|
-
downloaded,
|
|
522
|
-
total,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
await new Promise<void>((resolve, reject) => {
|
|
528
|
-
fileStream.end((err: Error | null) => {
|
|
529
|
-
if (err) {
|
|
530
|
-
reject(err);
|
|
531
|
-
} else {
|
|
532
|
-
resolve();
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
renameSync(tempPath, outputPath);
|
|
538
|
-
return { success: true };
|
|
539
|
-
} catch (error) {
|
|
540
|
-
if (existsSync(tempPath)) {
|
|
541
|
-
unlinkSync(tempPath);
|
|
542
|
-
}
|
|
543
|
-
return {
|
|
544
|
-
success: false,
|
|
545
|
-
error: error instanceof Error ? error.message : String(error),
|
|
546
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Downloads an HLS video stream.
|
|
553
|
-
*/
|
|
554
|
-
async function downloadHlsVideo(
|
|
555
|
-
masterUrl: string,
|
|
556
|
-
outputPath: string,
|
|
557
|
-
onProgress?: (progress: DownloadProgress) => void
|
|
558
|
-
): Promise<VimeoDownloadResult> {
|
|
559
|
-
try {
|
|
560
|
-
// Fetch master playlist
|
|
561
|
-
const masterResponse = await fetch(masterUrl, {
|
|
562
|
-
headers: {
|
|
563
|
-
"User-Agent": USER_AGENT,
|
|
564
|
-
Referer: "https://player.vimeo.com/",
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
if (!masterResponse.ok) {
|
|
569
|
-
return {
|
|
570
|
-
success: false,
|
|
571
|
-
error: `Failed to fetch HLS playlist: HTTP ${masterResponse.status}`,
|
|
572
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const masterPlaylist = await masterResponse.text();
|
|
577
|
-
const lines = masterPlaylist.split("\n");
|
|
578
|
-
const baseUrl = getBaseUrl(masterUrl);
|
|
579
|
-
|
|
580
|
-
// Find best quality video stream
|
|
581
|
-
let bestBandwidth = 0;
|
|
582
|
-
let videoPlaylistUrl: string | null = null;
|
|
583
|
-
|
|
584
|
-
for (let i = 0; i < lines.length; i++) {
|
|
585
|
-
const line = lines[i]?.trim();
|
|
586
|
-
if (!line) continue;
|
|
587
|
-
|
|
588
|
-
if (line.startsWith("#EXT-X-STREAM-INF:")) {
|
|
589
|
-
const bandwidthMatch = /BANDWIDTH=(\d+)/.exec(line);
|
|
590
|
-
const bandwidth = bandwidthMatch?.[1] ? parseInt(bandwidthMatch[1], 10) : 0;
|
|
591
|
-
|
|
592
|
-
const nextLine = lines[i + 1]?.trim();
|
|
593
|
-
if (nextLine && !nextLine.startsWith("#") && bandwidth > bestBandwidth) {
|
|
594
|
-
bestBandwidth = bandwidth;
|
|
595
|
-
videoPlaylistUrl = nextLine.startsWith("http") ? nextLine : baseUrl + nextLine;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (!videoPlaylistUrl) {
|
|
601
|
-
return {
|
|
602
|
-
success: false,
|
|
603
|
-
error: "Could not find video stream in HLS playlist",
|
|
604
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Fetch video playlist and get segments
|
|
609
|
-
const videoResponse = await fetch(videoPlaylistUrl, {
|
|
610
|
-
headers: { "User-Agent": USER_AGENT },
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
if (!videoResponse.ok) {
|
|
614
|
-
return {
|
|
615
|
-
success: false,
|
|
616
|
-
error: `Failed to fetch video playlist: HTTP ${videoResponse.status}`,
|
|
617
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const videoPlaylist = await videoResponse.text();
|
|
622
|
-
const videoBaseUrl = getBaseUrl(videoPlaylistUrl);
|
|
623
|
-
const segments: string[] = [];
|
|
624
|
-
|
|
625
|
-
for (const line of videoPlaylist.split("\n")) {
|
|
626
|
-
const trimmed = line.trim();
|
|
627
|
-
if (trimmed && !trimmed.startsWith("#")) {
|
|
628
|
-
const segmentUrl = trimmed.startsWith("http") ? trimmed : videoBaseUrl + trimmed;
|
|
629
|
-
segments.push(segmentUrl);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (segments.length === 0) {
|
|
634
|
-
return {
|
|
635
|
-
success: false,
|
|
636
|
-
error: "No segments found in video playlist",
|
|
637
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Download all segments
|
|
642
|
-
const tempPath = `${outputPath}.tmp`;
|
|
643
|
-
const fileStream = createWriteStream(tempPath);
|
|
644
|
-
|
|
645
|
-
for (let i = 0; i < segments.length; i++) {
|
|
646
|
-
const segmentUrl = segments[i];
|
|
647
|
-
if (!segmentUrl) continue;
|
|
648
|
-
|
|
649
|
-
const segResponse = await fetch(segmentUrl, {
|
|
650
|
-
headers: { "User-Agent": USER_AGENT },
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
if (!segResponse.ok || !segResponse.body) continue;
|
|
654
|
-
|
|
655
|
-
const reader = segResponse.body.getReader();
|
|
656
|
-
while (true) {
|
|
657
|
-
const { done, value } = await reader.read();
|
|
658
|
-
if (done) break;
|
|
659
|
-
fileStream.write(Buffer.from(value));
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (onProgress) {
|
|
663
|
-
onProgress({
|
|
664
|
-
percent: ((i + 1) / segments.length) * 100,
|
|
665
|
-
downloaded: i + 1,
|
|
666
|
-
total: segments.length,
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
await new Promise<void>((resolve, reject) => {
|
|
672
|
-
fileStream.end((err: Error | null) => {
|
|
673
|
-
if (err) {
|
|
674
|
-
reject(err);
|
|
675
|
-
} else {
|
|
676
|
-
resolve();
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
renameSync(tempPath, outputPath);
|
|
682
|
-
return { success: true };
|
|
683
|
-
} catch (error) {
|
|
684
|
-
return {
|
|
685
|
-
success: false,
|
|
686
|
-
error: error instanceof Error ? error.message : String(error),
|
|
687
|
-
errorCode: "DOWNLOAD_FAILED",
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Vimeo config response type (partial).
|
|
694
|
-
*/
|
|
695
|
-
interface VimeoConfig {
|
|
696
|
-
video?: {
|
|
697
|
-
id?: number;
|
|
698
|
-
title?: string;
|
|
699
|
-
duration?: number;
|
|
700
|
-
width?: number;
|
|
701
|
-
height?: number;
|
|
702
|
-
drm?: boolean;
|
|
703
|
-
};
|
|
704
|
-
request?: {
|
|
705
|
-
drm?: boolean;
|
|
706
|
-
files?: {
|
|
707
|
-
hls?: {
|
|
708
|
-
cdns?: Record<string, { url?: string }>;
|
|
709
|
-
};
|
|
710
|
-
dash?: {
|
|
711
|
-
cdns?: Record<string, { url?: string }>;
|
|
712
|
-
};
|
|
713
|
-
progressive?: {
|
|
714
|
-
url?: string;
|
|
715
|
-
quality?: string;
|
|
716
|
-
width?: number;
|
|
717
|
-
height?: number;
|
|
718
|
-
}[];
|
|
719
|
-
};
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
/* v8 ignore stop */
|