@mkterswingman/5mghost-yonder 0.0.26 → 0.0.27
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/package.json +1 -1
- package/dist/auth/oauthFlow.d.ts +0 -9
- package/dist/auth/oauthFlow.js +0 -151
- package/dist/auth/sharedAuth.d.ts +0 -10
- package/dist/auth/sharedAuth.js +0 -31
- package/dist/auth/tokenManager.d.ts +0 -18
- package/dist/auth/tokenManager.js +0 -92
- package/dist/cli/check.d.ts +0 -4
- package/dist/cli/check.js +0 -90
- package/dist/cli/index.d.ts +0 -18
- package/dist/cli/index.js +0 -166
- package/dist/cli/installSkills.d.ts +0 -1
- package/dist/cli/installSkills.js +0 -39
- package/dist/cli/runtime.d.ts +0 -9
- package/dist/cli/runtime.js +0 -35
- package/dist/cli/serve.d.ts +0 -1
- package/dist/cli/serve.js +0 -26
- package/dist/cli/setup.d.ts +0 -33
- package/dist/cli/setup.js +0 -450
- package/dist/cli/setupCookies.d.ts +0 -88
- package/dist/cli/setupCookies.js +0 -431
- package/dist/cli/smoke.d.ts +0 -27
- package/dist/cli/smoke.js +0 -108
- package/dist/cli/uninstall.d.ts +0 -16
- package/dist/cli/uninstall.js +0 -99
- package/dist/download/downloader.d.ts +0 -67
- package/dist/download/downloader.js +0 -309
- package/dist/download/jobManager.d.ts +0 -22
- package/dist/download/jobManager.js +0 -211
- package/dist/download/types.d.ts +0 -44
- package/dist/download/types.js +0 -1
- package/dist/runtime/ffmpegRuntime.d.ts +0 -13
- package/dist/runtime/ffmpegRuntime.js +0 -51
- package/dist/runtime/installers.d.ts +0 -12
- package/dist/runtime/installers.js +0 -45
- package/dist/runtime/manifest.d.ts +0 -18
- package/dist/runtime/manifest.js +0 -43
- package/dist/runtime/playwrightRuntime.d.ts +0 -17
- package/dist/runtime/playwrightRuntime.js +0 -49
- package/dist/runtime/systemDeps.d.ts +0 -3
- package/dist/runtime/systemDeps.js +0 -30
- package/dist/runtime/ytdlpRuntime.d.ts +0 -14
- package/dist/runtime/ytdlpRuntime.js +0 -58
- package/dist/server.d.ts +0 -23
- package/dist/server.js +0 -81
- package/dist/tools/downloads.d.ts +0 -11
- package/dist/tools/downloads.js +0 -220
- package/dist/tools/remote.d.ts +0 -4
- package/dist/tools/remote.js +0 -239
- package/dist/tools/subtitles.d.ts +0 -29
- package/dist/tools/subtitles.js +0 -713
- package/dist/utils/browserLaunch.d.ts +0 -5
- package/dist/utils/browserLaunch.js +0 -22
- package/dist/utils/browserProfileImport.d.ts +0 -49
- package/dist/utils/browserProfileImport.js +0 -163
- package/dist/utils/codexInternal.d.ts +0 -9
- package/dist/utils/codexInternal.js +0 -60
- package/dist/utils/config.d.ts +0 -53
- package/dist/utils/config.js +0 -77
- package/dist/utils/cookieRefresh.d.ts +0 -18
- package/dist/utils/cookieRefresh.js +0 -70
- package/dist/utils/cookies.d.ts +0 -18
- package/dist/utils/cookies.js +0 -100
- package/dist/utils/ffmpeg.d.ts +0 -5
- package/dist/utils/ffmpeg.js +0 -16
- package/dist/utils/ffmpegPath.d.ts +0 -8
- package/dist/utils/ffmpegPath.js +0 -21
- package/dist/utils/formatters.d.ts +0 -4
- package/dist/utils/formatters.js +0 -42
- package/dist/utils/launcher.d.ts +0 -12
- package/dist/utils/launcher.js +0 -90
- package/dist/utils/mcpRegistration.d.ts +0 -7
- package/dist/utils/mcpRegistration.js +0 -23
- package/dist/utils/mediaPaths.d.ts +0 -7
- package/dist/utils/mediaPaths.js +0 -10
- package/dist/utils/openClaw.d.ts +0 -18
- package/dist/utils/openClaw.js +0 -82
- package/dist/utils/skills.d.ts +0 -16
- package/dist/utils/skills.js +0 -56
- package/dist/utils/videoInput.d.ts +0 -5
- package/dist/utils/videoInput.js +0 -58
- package/dist/utils/videoMetadata.d.ts +0 -11
- package/dist/utils/videoMetadata.js +0 -1
- package/dist/utils/ytdlp.d.ts +0 -28
- package/dist/utils/ytdlp.js +0 -205
- package/dist/utils/ytdlpFailures.d.ts +0 -3
- package/dist/utils/ytdlpFailures.js +0 -27
- package/dist/utils/ytdlpPath.d.ts +0 -33
- package/dist/utils/ytdlpPath.js +0 -77
- package/dist/utils/ytdlpProgress.d.ts +0 -13
- package/dist/utils/ytdlpProgress.js +0 -77
- package/dist/utils/ytdlpScheduler.d.ts +0 -25
- package/dist/utils/ytdlpScheduler.js +0 -78
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { DownloadJobManager } from "./jobManager.js";
|
|
2
|
-
import type { DownloadJobItemSnapshot, DownloadMode } from "./types.js";
|
|
3
|
-
import type { PreflightVideoMetadata } from "../utils/videoMetadata.js";
|
|
4
|
-
import { type YtDlpSchedulerState } from "../utils/ytdlpScheduler.js";
|
|
5
|
-
type DownloadingStep = "download_video" | "download_subtitles" | "merge";
|
|
6
|
-
export interface DownloadOneItemProgressUpdate {
|
|
7
|
-
step: DownloadingStep;
|
|
8
|
-
progressPercent: number | null;
|
|
9
|
-
}
|
|
10
|
-
export interface DownloadOneItemResult {
|
|
11
|
-
video_file: string | null;
|
|
12
|
-
metadata_file: string;
|
|
13
|
-
final_file_size: string | null;
|
|
14
|
-
subtitle_files: Record<string, string[]>;
|
|
15
|
-
metadata: PreflightVideoMetadata;
|
|
16
|
-
}
|
|
17
|
-
export interface DownloadOneItemInput {
|
|
18
|
-
item: Pick<DownloadJobItemSnapshot, "video_id" | "source_url" | "title" | "output_dir">;
|
|
19
|
-
mode: DownloadMode;
|
|
20
|
-
videoQuality?: string | null;
|
|
21
|
-
subtitleFormats: string[];
|
|
22
|
-
subtitleLanguages?: string[];
|
|
23
|
-
defaultSubtitleLanguages?: string[];
|
|
24
|
-
jobManager?: Pick<DownloadJobManager, "startItem" | "updateItemProgress" | "completeItem" | "failItem" | "setItemQueuedByGlobalRateLimit">;
|
|
25
|
-
jobId?: string;
|
|
26
|
-
deps?: Partial<DownloadOneItemDeps>;
|
|
27
|
-
}
|
|
28
|
-
interface DownloadOneItemDeps {
|
|
29
|
-
hasYtDlp: () => boolean;
|
|
30
|
-
hasFfmpeg: () => boolean;
|
|
31
|
-
fetchMetadata: (input: {
|
|
32
|
-
sourceUrl: string;
|
|
33
|
-
videoId: string;
|
|
34
|
-
title: string;
|
|
35
|
-
}) => Promise<PreflightVideoMetadata>;
|
|
36
|
-
downloadVideo: (input: {
|
|
37
|
-
sourceUrl: string;
|
|
38
|
-
outputFile: string;
|
|
39
|
-
videoQuality: string;
|
|
40
|
-
onProgress: (update: DownloadOneItemProgressUpdate) => void;
|
|
41
|
-
}) => Promise<string>;
|
|
42
|
-
refreshCookies: () => Promise<boolean>;
|
|
43
|
-
getYtDlpSchedulerState: () => YtDlpSchedulerState;
|
|
44
|
-
downloadSubtitles: (input: {
|
|
45
|
-
videoId: string;
|
|
46
|
-
sourceUrl: string;
|
|
47
|
-
subtitlesDir: string;
|
|
48
|
-
formats: string[];
|
|
49
|
-
languages: string[];
|
|
50
|
-
skipMissingLanguages: boolean;
|
|
51
|
-
onProgress: (update: DownloadOneItemProgressUpdate) => void;
|
|
52
|
-
}) => Promise<Record<string, string[]>>;
|
|
53
|
-
mkdir: (path: string, options: {
|
|
54
|
-
recursive: true;
|
|
55
|
-
}) => Promise<void>;
|
|
56
|
-
rm: (path: string, options: {
|
|
57
|
-
recursive: true;
|
|
58
|
-
force: true;
|
|
59
|
-
}) => Promise<void>;
|
|
60
|
-
stat: (path: string) => Promise<{
|
|
61
|
-
size: number;
|
|
62
|
-
}>;
|
|
63
|
-
writeMetadata: (path: string, contents: string) => Promise<void>;
|
|
64
|
-
}
|
|
65
|
-
export declare function downloadOneItem(input: DownloadOneItemInput): Promise<DownloadOneItemResult>;
|
|
66
|
-
export declare function resolveYtDlpOutputPath(expectedPath: string, stdout: string): string;
|
|
67
|
-
export {};
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { downloadSubtitlesForLanguages, toReadableSubtitleJobError } from "../tools/subtitles.js";
|
|
4
|
-
import { loadConfig } from "../utils/config.js";
|
|
5
|
-
import { tryHeadlessRefresh } from "../utils/cookieRefresh.js";
|
|
6
|
-
import { hasFfmpeg, isRuntimeMissingMessage } from "../utils/ffmpeg.js";
|
|
7
|
-
import { formatBytes } from "../utils/formatters.js";
|
|
8
|
-
import { hasYtDlp, runYtDlp, runYtDlpJson } from "../utils/ytdlp.js";
|
|
9
|
-
import { appendDiagnosticLog, classifyYtDlpFailure } from "../utils/ytdlpFailures.js";
|
|
10
|
-
import { parseYtDlpProgressLine } from "../utils/ytdlpProgress.js";
|
|
11
|
-
import { getGlobalYtDlpSchedulerState } from "../utils/ytdlpScheduler.js";
|
|
12
|
-
export async function downloadOneItem(input) {
|
|
13
|
-
const deps = resolveDeps(input.deps);
|
|
14
|
-
const manager = input.jobManager;
|
|
15
|
-
const jobId = input.jobId;
|
|
16
|
-
const videoQuality = normalizeVideoQuality(input.videoQuality);
|
|
17
|
-
const subtitleFormats = [...input.subtitleFormats];
|
|
18
|
-
const usingDefaultSubtitleLanguages = input.subtitleLanguages == null;
|
|
19
|
-
const subtitleLanguages = [...(input.subtitleLanguages ?? input.defaultSubtitleLanguages ?? loadConfig().default_languages)];
|
|
20
|
-
const metadataFile = join(input.item.output_dir, "metadata.json");
|
|
21
|
-
const subtitlesDir = join(input.item.output_dir, "subtitles");
|
|
22
|
-
const videoFile = join(input.item.output_dir, "video.mp4");
|
|
23
|
-
let currentStep = "metadata";
|
|
24
|
-
try {
|
|
25
|
-
assertManagerInput(manager, jobId);
|
|
26
|
-
manager?.startItem(jobId, input.item.video_id, currentStep);
|
|
27
|
-
if (!deps.hasYtDlp()) {
|
|
28
|
-
throw new Error(isRuntimeMissingMessage("yt-dlp"));
|
|
29
|
-
}
|
|
30
|
-
if (needsVideo(input.mode) && !deps.hasFfmpeg()) {
|
|
31
|
-
throw new Error(isRuntimeMissingMessage("ffmpeg"));
|
|
32
|
-
}
|
|
33
|
-
// Why: reruns intentionally reuse the same YYYY-MM-DD_<video_id> directory, so stale artifacts must be cleared first.
|
|
34
|
-
await deps.rm(input.item.output_dir, { recursive: true, force: true });
|
|
35
|
-
await deps.mkdir(input.item.output_dir, { recursive: true });
|
|
36
|
-
await deps.mkdir(subtitlesDir, { recursive: true });
|
|
37
|
-
const metadata = await deps.fetchMetadata({
|
|
38
|
-
sourceUrl: input.item.source_url,
|
|
39
|
-
videoId: input.item.video_id,
|
|
40
|
-
title: input.item.title,
|
|
41
|
-
});
|
|
42
|
-
const result = {
|
|
43
|
-
video_file: null,
|
|
44
|
-
metadata_file: metadataFile,
|
|
45
|
-
final_file_size: null,
|
|
46
|
-
subtitle_files: {},
|
|
47
|
-
metadata,
|
|
48
|
-
};
|
|
49
|
-
const onProgress = (update) => {
|
|
50
|
-
currentStep = update.step;
|
|
51
|
-
manager?.updateItemProgress(jobId, input.item.video_id, update.step, update.progressPercent);
|
|
52
|
-
};
|
|
53
|
-
if (needsVideo(input.mode)) {
|
|
54
|
-
currentStep = "download_video";
|
|
55
|
-
manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 0);
|
|
56
|
-
manager?.setItemQueuedByGlobalRateLimit(jobId, input.item.video_id, deps.getYtDlpSchedulerState().wouldThrottle);
|
|
57
|
-
try {
|
|
58
|
-
result.video_file = await deps.downloadVideo({
|
|
59
|
-
sourceUrl: input.item.source_url,
|
|
60
|
-
outputFile: videoFile,
|
|
61
|
-
videoQuality,
|
|
62
|
-
onProgress,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
const failureKind = classifyVideoDownloadError(error);
|
|
67
|
-
if (failureKind != null && failureKind !== "RATE_LIMITED" && await deps.refreshCookies()) {
|
|
68
|
-
result.video_file = await deps.downloadVideo({
|
|
69
|
-
sourceUrl: input.item.source_url,
|
|
70
|
-
outputFile: videoFile,
|
|
71
|
-
videoQuality,
|
|
72
|
-
onProgress,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
finally {
|
|
80
|
-
manager?.setItemQueuedByGlobalRateLimit(jobId, input.item.video_id, false);
|
|
81
|
-
}
|
|
82
|
-
result.final_file_size = formatBytes((await deps.stat(result.video_file)).size);
|
|
83
|
-
}
|
|
84
|
-
if (needsSubtitles(input.mode)) {
|
|
85
|
-
currentStep = "download_subtitles";
|
|
86
|
-
manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 0);
|
|
87
|
-
manager?.setItemQueuedByGlobalRateLimit(jobId, input.item.video_id, deps.getYtDlpSchedulerState().wouldThrottle);
|
|
88
|
-
try {
|
|
89
|
-
result.subtitle_files = await deps.downloadSubtitles({
|
|
90
|
-
videoId: input.item.video_id,
|
|
91
|
-
sourceUrl: input.item.source_url,
|
|
92
|
-
subtitlesDir,
|
|
93
|
-
formats: subtitleFormats,
|
|
94
|
-
languages: subtitleLanguages,
|
|
95
|
-
skipMissingLanguages: usingDefaultSubtitleLanguages,
|
|
96
|
-
onProgress,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
manager?.setItemQueuedByGlobalRateLimit(jobId, input.item.video_id, false);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
currentStep = "write_metadata";
|
|
104
|
-
manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 100);
|
|
105
|
-
await deps.writeMetadata(metadataFile, JSON.stringify(buildMetadataDocument({
|
|
106
|
-
item: input.item,
|
|
107
|
-
mode: input.mode,
|
|
108
|
-
videoQuality,
|
|
109
|
-
subtitleFormats,
|
|
110
|
-
metadata,
|
|
111
|
-
result,
|
|
112
|
-
}), null, 2));
|
|
113
|
-
manager?.completeItem(jobId, input.item.video_id, {
|
|
114
|
-
video_file: result.video_file,
|
|
115
|
-
metadata_file: result.metadata_file,
|
|
116
|
-
final_file_size: result.final_file_size,
|
|
117
|
-
subtitle_files: result.subtitle_files,
|
|
118
|
-
});
|
|
119
|
-
return result;
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const readableError = toErrorMessage(error);
|
|
123
|
-
if (manager && jobId) {
|
|
124
|
-
manager.failItem(jobId, input.item.video_id, readableError, currentStep);
|
|
125
|
-
}
|
|
126
|
-
throw new Error(readableError);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function resolveDeps(overrides) {
|
|
130
|
-
return {
|
|
131
|
-
hasYtDlp,
|
|
132
|
-
hasFfmpeg,
|
|
133
|
-
fetchMetadata: fetchMetadataWithYtDlp,
|
|
134
|
-
downloadVideo: downloadVideoWithYtDlp,
|
|
135
|
-
refreshCookies: tryHeadlessRefresh,
|
|
136
|
-
getYtDlpSchedulerState: getGlobalYtDlpSchedulerState,
|
|
137
|
-
downloadSubtitles: downloadSubtitlesWithYtDlp,
|
|
138
|
-
mkdir: async (path, options) => {
|
|
139
|
-
await mkdir(path, options);
|
|
140
|
-
},
|
|
141
|
-
rm: async (path, options) => {
|
|
142
|
-
await rm(path, options);
|
|
143
|
-
},
|
|
144
|
-
stat,
|
|
145
|
-
writeMetadata: async (path, contents) => {
|
|
146
|
-
await writeFile(path, contents, "utf8");
|
|
147
|
-
},
|
|
148
|
-
...overrides,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
async function fetchMetadataWithYtDlp(input) {
|
|
152
|
-
const result = await runYtDlpJson([
|
|
153
|
-
"--dump-single-json",
|
|
154
|
-
"--skip-download",
|
|
155
|
-
"--no-playlist",
|
|
156
|
-
input.sourceUrl,
|
|
157
|
-
]);
|
|
158
|
-
if (!result.ok) {
|
|
159
|
-
throw new Error(result.error);
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
video_id: result.value.id ?? input.videoId,
|
|
163
|
-
source_url: result.value.webpage_url ?? input.sourceUrl,
|
|
164
|
-
title: result.value.title ?? input.title,
|
|
165
|
-
description: result.value.description,
|
|
166
|
-
channel_id: result.value.channel_id ?? result.value.uploader_id,
|
|
167
|
-
channel_title: result.value.channel ?? result.value.uploader,
|
|
168
|
-
published_at: normalizeUploadDate(result.value.upload_date),
|
|
169
|
-
duration: result.value.duration_string ?? formatDuration(result.value.duration),
|
|
170
|
-
thumbnail_url: result.value.thumbnail,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
async function downloadVideoWithYtDlp(input) {
|
|
174
|
-
let lastProgress = 0;
|
|
175
|
-
const result = await runYtDlp([
|
|
176
|
-
"--no-playlist",
|
|
177
|
-
"--newline",
|
|
178
|
-
"--progress",
|
|
179
|
-
"--format",
|
|
180
|
-
resolveVideoFormat(input.videoQuality),
|
|
181
|
-
"--merge-output-format",
|
|
182
|
-
"mp4",
|
|
183
|
-
"--output",
|
|
184
|
-
input.outputFile.replace(/\.mp4$/u, ".%(ext)s"),
|
|
185
|
-
"--print",
|
|
186
|
-
"after_move:filepath",
|
|
187
|
-
input.sourceUrl,
|
|
188
|
-
], 600_000, (line) => {
|
|
189
|
-
const parsed = parseYtDlpProgressLine(line);
|
|
190
|
-
if (parsed) {
|
|
191
|
-
lastProgress = Number(parsed.progress.replace(/%$/u, ""));
|
|
192
|
-
input.onProgress({ step: "download_video", progressPercent: lastProgress });
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (line.includes("[Merger]")) {
|
|
196
|
-
input.onProgress({ step: "merge", progressPercent: Math.max(lastProgress, 99) });
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
if (result.exitCode !== 0) {
|
|
200
|
-
const failureKind = classifyYtDlpFailure(result.stderr);
|
|
201
|
-
if (failureKind === "RATE_LIMITED") {
|
|
202
|
-
throw new Error(appendDiagnosticLog("The current YouTube session has been rate-limited. Wait and retry later.", result.failureLogPath));
|
|
203
|
-
}
|
|
204
|
-
if (failureKind === "SIGN_IN_REQUIRED") {
|
|
205
|
-
throw new Error(appendDiagnosticLog("YouTube requested an additional sign-in confirmation for this video.", result.failureLogPath));
|
|
206
|
-
}
|
|
207
|
-
if (failureKind === "COOKIES_INVALID" || failureKind === "COOKIES_EXPIRED") {
|
|
208
|
-
throw new Error(appendDiagnosticLog("YouTube rejected the current cookies for video download.", result.failureLogPath));
|
|
209
|
-
}
|
|
210
|
-
throw new Error(appendDiagnosticLog(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`, result.failureLogPath));
|
|
211
|
-
}
|
|
212
|
-
return resolveYtDlpOutputPath(input.outputFile, result.stdout);
|
|
213
|
-
}
|
|
214
|
-
async function downloadSubtitlesWithYtDlp(input) {
|
|
215
|
-
input.onProgress({ step: "download_subtitles", progressPercent: 0 });
|
|
216
|
-
return downloadSubtitlesForLanguages({
|
|
217
|
-
videoId: input.videoId,
|
|
218
|
-
sourceUrl: input.sourceUrl,
|
|
219
|
-
formats: input.formats,
|
|
220
|
-
languages: input.languages,
|
|
221
|
-
subtitlesDir: input.subtitlesDir,
|
|
222
|
-
skipMissingLanguages: input.skipMissingLanguages,
|
|
223
|
-
onProgress: (completed, total) => {
|
|
224
|
-
input.onProgress({
|
|
225
|
-
step: "download_subtitles",
|
|
226
|
-
progressPercent: total === 0 ? 100 : Math.round((completed / total) * 100),
|
|
227
|
-
});
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
function buildMetadataDocument(input) {
|
|
232
|
-
return {
|
|
233
|
-
video_id: input.metadata.video_id,
|
|
234
|
-
source_url: input.metadata.source_url,
|
|
235
|
-
title: input.metadata.title,
|
|
236
|
-
description: input.metadata.description ?? null,
|
|
237
|
-
channel_id: input.metadata.channel_id ?? null,
|
|
238
|
-
channel_title: input.metadata.channel_title ?? null,
|
|
239
|
-
published_at: input.metadata.published_at ?? null,
|
|
240
|
-
duration: input.metadata.duration ?? null,
|
|
241
|
-
thumbnail_url: input.metadata.thumbnail_url ?? null,
|
|
242
|
-
mode: input.mode,
|
|
243
|
-
video_quality: needsVideo(input.mode) ? input.videoQuality : null,
|
|
244
|
-
subtitle_formats: [...input.subtitleFormats],
|
|
245
|
-
output_dir: input.item.output_dir,
|
|
246
|
-
video_file: input.result.video_file,
|
|
247
|
-
metadata_file: input.result.metadata_file,
|
|
248
|
-
final_file_size: input.result.final_file_size,
|
|
249
|
-
subtitle_files: sortSubtitleFiles(input.result.subtitle_files),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
export function resolveYtDlpOutputPath(expectedPath, stdout) {
|
|
253
|
-
const lines = stdout
|
|
254
|
-
.split(/\r?\n/u)
|
|
255
|
-
.map((line) => line.trim())
|
|
256
|
-
.filter(Boolean);
|
|
257
|
-
return lines.at(-1) ?? expectedPath;
|
|
258
|
-
}
|
|
259
|
-
function assertManagerInput(manager, jobId) {
|
|
260
|
-
if ((manager == null) !== (jobId == null)) {
|
|
261
|
-
throw new Error("jobManager and jobId must be provided together.");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function normalizeVideoQuality(videoQuality) {
|
|
265
|
-
return videoQuality === "max" ? "max" : "1080p";
|
|
266
|
-
}
|
|
267
|
-
function resolveVideoFormat(videoQuality) {
|
|
268
|
-
if (videoQuality === "max") {
|
|
269
|
-
return "bv*+ba/b";
|
|
270
|
-
}
|
|
271
|
-
return "bv*[height<=1080]+ba/b[height<=1080]/b";
|
|
272
|
-
}
|
|
273
|
-
function needsVideo(mode) {
|
|
274
|
-
return mode === "video" || mode === "both";
|
|
275
|
-
}
|
|
276
|
-
function needsSubtitles(mode) {
|
|
277
|
-
return mode === "subtitles" || mode === "both";
|
|
278
|
-
}
|
|
279
|
-
function normalizeUploadDate(uploadDate) {
|
|
280
|
-
if (!uploadDate || !/^\d{8}$/u.test(uploadDate)) {
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
return `${uploadDate.slice(0, 4)}-${uploadDate.slice(4, 6)}-${uploadDate.slice(6, 8)}`;
|
|
284
|
-
}
|
|
285
|
-
function formatDuration(durationSeconds) {
|
|
286
|
-
if (!Number.isFinite(durationSeconds) || durationSeconds == null || durationSeconds < 0) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
const wholeSeconds = Math.floor(durationSeconds);
|
|
290
|
-
const hours = Math.floor(wholeSeconds / 3600);
|
|
291
|
-
const minutes = Math.floor((wholeSeconds % 3600) / 60);
|
|
292
|
-
const seconds = wholeSeconds % 60;
|
|
293
|
-
if (hours > 0) {
|
|
294
|
-
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
295
|
-
}
|
|
296
|
-
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
297
|
-
}
|
|
298
|
-
function sortSubtitleFiles(files) {
|
|
299
|
-
return Object.fromEntries(Object.entries(files)
|
|
300
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
301
|
-
.map(([format, paths]) => [format, [...paths].sort()]));
|
|
302
|
-
}
|
|
303
|
-
function classifyVideoDownloadError(error) {
|
|
304
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
305
|
-
return classifyYtDlpFailure(message);
|
|
306
|
-
}
|
|
307
|
-
function toErrorMessage(error) {
|
|
308
|
-
return toReadableSubtitleJobError(error);
|
|
309
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { CreateDownloadJobInput, DownloadJobItemSnapshot, DownloadJobSnapshot, DownloadStep } from "./types.js";
|
|
2
|
-
interface DownloadJobManagerOptions {
|
|
3
|
-
now?: () => string;
|
|
4
|
-
}
|
|
5
|
-
export declare class DownloadJobManager {
|
|
6
|
-
private readonly jobs;
|
|
7
|
-
private readonly now;
|
|
8
|
-
constructor(options?: DownloadJobManagerOptions);
|
|
9
|
-
createJob(input: CreateDownloadJobInput): DownloadJobSnapshot;
|
|
10
|
-
pollJob(jobId: string): DownloadJobSnapshot;
|
|
11
|
-
startItem(jobId: string, videoId: string, step?: DownloadStep): DownloadJobSnapshot;
|
|
12
|
-
updateItemProgress(jobId: string, videoId: string, step: DownloadStep, progressPercent: number | null): DownloadJobSnapshot;
|
|
13
|
-
setItemQueuedByGlobalRateLimit(jobId: string, videoId: string, queued: boolean): DownloadJobSnapshot;
|
|
14
|
-
completeItem(jobId: string, videoId: string, outputs?: Pick<DownloadJobItemSnapshot, "video_file" | "metadata_file" | "final_file_size" | "subtitle_files">): DownloadJobSnapshot;
|
|
15
|
-
failItem(jobId: string, videoId: string, error: string, step?: DownloadStep): DownloadJobSnapshot;
|
|
16
|
-
private applyItemMutation;
|
|
17
|
-
private refreshJobStatus;
|
|
18
|
-
private getJob;
|
|
19
|
-
private getItem;
|
|
20
|
-
private toSnapshot;
|
|
21
|
-
}
|
|
22
|
-
export {};
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
export class DownloadJobManager {
|
|
3
|
-
jobs = new Map();
|
|
4
|
-
now;
|
|
5
|
-
constructor(options = {}) {
|
|
6
|
-
this.now = options.now ?? (() => new Date().toISOString());
|
|
7
|
-
}
|
|
8
|
-
createJob(input) {
|
|
9
|
-
assertNonEmptyVideos(input);
|
|
10
|
-
const createdAt = this.now();
|
|
11
|
-
const jobId = randomUUID();
|
|
12
|
-
assertUniqueVideoIds(input);
|
|
13
|
-
const job = {
|
|
14
|
-
job_id: jobId,
|
|
15
|
-
status: "queued",
|
|
16
|
-
mode: input.mode,
|
|
17
|
-
video_quality: input.video_quality ?? null,
|
|
18
|
-
subtitle_formats: [...input.subtitle_formats],
|
|
19
|
-
created_at: createdAt,
|
|
20
|
-
started_at: null,
|
|
21
|
-
completed_at: null,
|
|
22
|
-
total: input.videos.length,
|
|
23
|
-
items: input.videos.map((video) => ({
|
|
24
|
-
video_id: video.video_id,
|
|
25
|
-
source_url: video.source_url,
|
|
26
|
-
title: video.title,
|
|
27
|
-
output_dir: video.output_dir,
|
|
28
|
-
status: "queued",
|
|
29
|
-
queued_by_global_rate_limit: false,
|
|
30
|
-
step: null,
|
|
31
|
-
progress_percent: null,
|
|
32
|
-
error: null,
|
|
33
|
-
started_at: null,
|
|
34
|
-
completed_at: null,
|
|
35
|
-
video_file: null,
|
|
36
|
-
metadata_file: null,
|
|
37
|
-
final_file_size: null,
|
|
38
|
-
subtitle_files: {},
|
|
39
|
-
})),
|
|
40
|
-
};
|
|
41
|
-
this.jobs.set(jobId, job);
|
|
42
|
-
return this.toSnapshot(job);
|
|
43
|
-
}
|
|
44
|
-
pollJob(jobId) {
|
|
45
|
-
return this.toSnapshot(this.getJob(jobId));
|
|
46
|
-
}
|
|
47
|
-
startItem(jobId, videoId, step = "metadata") {
|
|
48
|
-
const job = this.getJob(jobId);
|
|
49
|
-
const item = this.getItem(job, videoId);
|
|
50
|
-
this.applyItemMutation(job, item, {
|
|
51
|
-
status: "running",
|
|
52
|
-
step,
|
|
53
|
-
progress_percent: 0,
|
|
54
|
-
error: null,
|
|
55
|
-
});
|
|
56
|
-
return this.toSnapshot(job);
|
|
57
|
-
}
|
|
58
|
-
updateItemProgress(jobId, videoId, step, progressPercent) {
|
|
59
|
-
const job = this.getJob(jobId);
|
|
60
|
-
const item = this.getItem(job, videoId);
|
|
61
|
-
this.applyItemMutation(job, item, {
|
|
62
|
-
status: "running",
|
|
63
|
-
step,
|
|
64
|
-
progress_percent: progressPercent == null ? null : clampProgress(progressPercent),
|
|
65
|
-
error: null,
|
|
66
|
-
});
|
|
67
|
-
return this.toSnapshot(job);
|
|
68
|
-
}
|
|
69
|
-
setItemQueuedByGlobalRateLimit(jobId, videoId, queued) {
|
|
70
|
-
const job = this.getJob(jobId);
|
|
71
|
-
const item = this.getItem(job, videoId);
|
|
72
|
-
item.queued_by_global_rate_limit = queued;
|
|
73
|
-
return this.toSnapshot(job);
|
|
74
|
-
}
|
|
75
|
-
completeItem(jobId, videoId, outputs) {
|
|
76
|
-
const job = this.getJob(jobId);
|
|
77
|
-
const item = this.getItem(job, videoId);
|
|
78
|
-
this.applyItemMutation(job, item, {
|
|
79
|
-
status: "completed",
|
|
80
|
-
step: "done",
|
|
81
|
-
progress_percent: 100,
|
|
82
|
-
error: null,
|
|
83
|
-
video_file: outputs?.video_file,
|
|
84
|
-
metadata_file: outputs?.metadata_file,
|
|
85
|
-
final_file_size: outputs?.final_file_size,
|
|
86
|
-
subtitle_files: outputs?.subtitle_files,
|
|
87
|
-
});
|
|
88
|
-
return this.toSnapshot(job);
|
|
89
|
-
}
|
|
90
|
-
failItem(jobId, videoId, error, step) {
|
|
91
|
-
const job = this.getJob(jobId);
|
|
92
|
-
const item = this.getItem(job, videoId);
|
|
93
|
-
this.applyItemMutation(job, item, {
|
|
94
|
-
status: "failed",
|
|
95
|
-
step: step ?? item.step,
|
|
96
|
-
error,
|
|
97
|
-
});
|
|
98
|
-
return this.toSnapshot(job);
|
|
99
|
-
}
|
|
100
|
-
applyItemMutation(job, item, mutation) {
|
|
101
|
-
const now = this.now();
|
|
102
|
-
const nextStatus = mutation.status;
|
|
103
|
-
const nextStep = mutation.step === undefined ? item.step : mutation.step;
|
|
104
|
-
const previousStatus = item.status;
|
|
105
|
-
item.status = nextStatus;
|
|
106
|
-
item.queued_by_global_rate_limit =
|
|
107
|
-
mutation.queued_by_global_rate_limit === undefined
|
|
108
|
-
? item.queued_by_global_rate_limit
|
|
109
|
-
: mutation.queued_by_global_rate_limit;
|
|
110
|
-
item.step = nextStep;
|
|
111
|
-
item.video_file = mutation.video_file === undefined ? item.video_file : mutation.video_file;
|
|
112
|
-
item.metadata_file = mutation.metadata_file === undefined ? item.metadata_file : mutation.metadata_file;
|
|
113
|
-
item.final_file_size =
|
|
114
|
-
mutation.final_file_size === undefined ? item.final_file_size : mutation.final_file_size;
|
|
115
|
-
item.subtitle_files = mutation.subtitle_files === undefined ? item.subtitle_files : cloneSubtitleFiles(mutation.subtitle_files);
|
|
116
|
-
if (nextStatus === "running") {
|
|
117
|
-
item.started_at = item.started_at ?? now;
|
|
118
|
-
job.started_at = job.started_at ?? now;
|
|
119
|
-
item.completed_at = null;
|
|
120
|
-
item.error = null;
|
|
121
|
-
item.queued_by_global_rate_limit = false;
|
|
122
|
-
item.progress_percent = resolveRunningProgress(item.progress_percent, mutation.progress_percent, previousStatus);
|
|
123
|
-
}
|
|
124
|
-
if (nextStatus === "completed" || nextStatus === "failed") {
|
|
125
|
-
item.started_at = item.started_at ?? now;
|
|
126
|
-
job.started_at = job.started_at ?? now;
|
|
127
|
-
item.completed_at = now;
|
|
128
|
-
item.error = mutation.error === undefined ? item.error : mutation.error;
|
|
129
|
-
item.queued_by_global_rate_limit = false;
|
|
130
|
-
item.progress_percent =
|
|
131
|
-
mutation.progress_percent === undefined ? item.progress_percent : clampProgressOrNull(mutation.progress_percent);
|
|
132
|
-
}
|
|
133
|
-
this.refreshJobStatus(job, now);
|
|
134
|
-
}
|
|
135
|
-
refreshJobStatus(job, now = this.now()) {
|
|
136
|
-
const statuses = job.items.map((item) => item.status);
|
|
137
|
-
if (statuses.every((status) => status === "completed")) {
|
|
138
|
-
job.status = "completed";
|
|
139
|
-
job.completed_at = job.completed_at ?? now;
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (statuses.every((status) => status === "completed" || status === "failed")) {
|
|
143
|
-
job.status = "failed";
|
|
144
|
-
job.completed_at = job.completed_at ?? now;
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (job.started_at !== null || statuses.some((status) => status !== "queued")) {
|
|
148
|
-
job.status = "running";
|
|
149
|
-
job.completed_at = null;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
job.status = "queued";
|
|
153
|
-
job.completed_at = null;
|
|
154
|
-
}
|
|
155
|
-
getJob(jobId) {
|
|
156
|
-
const job = this.jobs.get(jobId);
|
|
157
|
-
if (!job) {
|
|
158
|
-
throw new Error(`Unknown download job: ${jobId}`);
|
|
159
|
-
}
|
|
160
|
-
return job;
|
|
161
|
-
}
|
|
162
|
-
getItem(job, videoId) {
|
|
163
|
-
const item = job.items.find((entry) => entry.video_id === videoId);
|
|
164
|
-
if (!item) {
|
|
165
|
-
throw new Error(`Unknown download item: ${videoId}`);
|
|
166
|
-
}
|
|
167
|
-
return item;
|
|
168
|
-
}
|
|
169
|
-
toSnapshot(job) {
|
|
170
|
-
return {
|
|
171
|
-
...job,
|
|
172
|
-
subtitle_formats: [...job.subtitle_formats],
|
|
173
|
-
items: job.items.map((item) => ({
|
|
174
|
-
...item,
|
|
175
|
-
subtitle_files: cloneSubtitleFiles(item.subtitle_files),
|
|
176
|
-
})),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function clampProgress(value) {
|
|
181
|
-
return Math.max(0, Math.min(100, value));
|
|
182
|
-
}
|
|
183
|
-
function clampProgressOrNull(value) {
|
|
184
|
-
return value == null ? null : clampProgress(value);
|
|
185
|
-
}
|
|
186
|
-
function resolveRunningProgress(currentProgress, nextProgress, previousStatus) {
|
|
187
|
-
if (nextProgress !== undefined) {
|
|
188
|
-
return clampProgressOrNull(nextProgress);
|
|
189
|
-
}
|
|
190
|
-
if (previousStatus === "completed" || previousStatus === "failed") {
|
|
191
|
-
return 0;
|
|
192
|
-
}
|
|
193
|
-
return currentProgress ?? 0;
|
|
194
|
-
}
|
|
195
|
-
function assertUniqueVideoIds(input) {
|
|
196
|
-
const seen = new Set();
|
|
197
|
-
for (const video of input.videos) {
|
|
198
|
-
if (seen.has(video.video_id)) {
|
|
199
|
-
throw new Error(`Duplicate video_id in download job: ${video.video_id}`);
|
|
200
|
-
}
|
|
201
|
-
seen.add(video.video_id);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
function assertNonEmptyVideos(input) {
|
|
205
|
-
if (input.videos.length === 0) {
|
|
206
|
-
throw new Error("Download job must include at least one video");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function cloneSubtitleFiles(files) {
|
|
210
|
-
return Object.fromEntries(Object.entries(files).map(([format, paths]) => [format, [...paths]]));
|
|
211
|
-
}
|
package/dist/download/types.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export type DownloadMode = "video" | "subtitles" | "both";
|
|
2
|
-
export type DownloadItemStatus = "queued" | "running" | "completed" | "failed";
|
|
3
|
-
export type DownloadStep = "metadata" | "download_video" | "download_subtitles" | "merge" | "write_metadata" | "done";
|
|
4
|
-
export interface DownloadJobVideoInput {
|
|
5
|
-
video_id: string;
|
|
6
|
-
source_url: string;
|
|
7
|
-
title: string;
|
|
8
|
-
output_dir: string;
|
|
9
|
-
}
|
|
10
|
-
export interface CreateDownloadJobInput {
|
|
11
|
-
videos: DownloadJobVideoInput[];
|
|
12
|
-
mode: DownloadMode;
|
|
13
|
-
video_quality?: string;
|
|
14
|
-
subtitle_formats: string[];
|
|
15
|
-
}
|
|
16
|
-
export interface DownloadJobItemSnapshot {
|
|
17
|
-
video_id: string;
|
|
18
|
-
source_url: string;
|
|
19
|
-
title: string;
|
|
20
|
-
output_dir: string;
|
|
21
|
-
status: DownloadItemStatus;
|
|
22
|
-
queued_by_global_rate_limit: boolean;
|
|
23
|
-
step: DownloadStep | null;
|
|
24
|
-
progress_percent: number | null;
|
|
25
|
-
error: string | null;
|
|
26
|
-
started_at: string | null;
|
|
27
|
-
completed_at: string | null;
|
|
28
|
-
video_file: string | null;
|
|
29
|
-
metadata_file: string | null;
|
|
30
|
-
final_file_size: string | null;
|
|
31
|
-
subtitle_files: Record<string, string[]>;
|
|
32
|
-
}
|
|
33
|
-
export interface DownloadJobSnapshot {
|
|
34
|
-
job_id: string;
|
|
35
|
-
status: DownloadItemStatus;
|
|
36
|
-
mode: DownloadMode;
|
|
37
|
-
video_quality: string | null;
|
|
38
|
-
subtitle_formats: string[];
|
|
39
|
-
created_at: string;
|
|
40
|
-
started_at: string | null;
|
|
41
|
-
completed_at: string | null;
|
|
42
|
-
total: number;
|
|
43
|
-
items: DownloadJobItemSnapshot[];
|
|
44
|
-
}
|
package/dist/download/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { RuntimeComponentState } from "./manifest.js";
|
|
2
|
-
export declare function checkFfmpegRuntime(): RuntimeComponentState & {
|
|
3
|
-
name: "ffmpeg";
|
|
4
|
-
message?: string;
|
|
5
|
-
};
|
|
6
|
-
export declare function installFfmpegRuntime(): Promise<RuntimeComponentState & {
|
|
7
|
-
name: "ffmpeg";
|
|
8
|
-
message?: string;
|
|
9
|
-
}>;
|
|
10
|
-
export declare function updateFfmpegRuntime(): Promise<RuntimeComponentState & {
|
|
11
|
-
name: "ffmpeg";
|
|
12
|
-
message?: string;
|
|
13
|
-
}>;
|