@mkterswingman/5mghost-yonder 0.0.13 → 0.0.14
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/download/downloader.d.ts +3 -1
- package/dist/download/downloader.js +21 -9
- package/dist/download/jobManager.d.ts +1 -0
- package/dist/download/jobManager.js +13 -0
- package/dist/download/types.d.ts +1 -0
- package/dist/utils/ytdlp.js +7 -2
- package/dist/utils/ytdlpScheduler.d.ts +22 -0
- package/dist/utils/ytdlpScheduler.js +66 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DownloadJobManager } from "./jobManager.js";
|
|
2
2
|
import type { DownloadJobItemSnapshot, DownloadMode } from "./types.js";
|
|
3
3
|
import type { PreflightVideoMetadata } from "../utils/videoMetadata.js";
|
|
4
|
+
import { type YtDlpSchedulerState } from "../utils/ytdlpScheduler.js";
|
|
4
5
|
type DownloadingStep = "download_video" | "download_subtitles" | "merge";
|
|
5
6
|
export interface DownloadOneItemProgressUpdate {
|
|
6
7
|
step: DownloadingStep;
|
|
@@ -20,7 +21,7 @@ export interface DownloadOneItemInput {
|
|
|
20
21
|
subtitleFormats: string[];
|
|
21
22
|
subtitleLanguages?: string[];
|
|
22
23
|
defaultSubtitleLanguages?: string[];
|
|
23
|
-
jobManager?: Pick<DownloadJobManager, "startItem" | "updateItemProgress" | "completeItem" | "failItem">;
|
|
24
|
+
jobManager?: Pick<DownloadJobManager, "startItem" | "updateItemProgress" | "completeItem" | "failItem" | "setItemQueuedByGlobalRateLimit">;
|
|
24
25
|
jobId?: string;
|
|
25
26
|
deps?: Partial<DownloadOneItemDeps>;
|
|
26
27
|
}
|
|
@@ -39,6 +40,7 @@ interface DownloadOneItemDeps {
|
|
|
39
40
|
onProgress: (update: DownloadOneItemProgressUpdate) => void;
|
|
40
41
|
}) => Promise<string>;
|
|
41
42
|
refreshCookies: () => Promise<boolean>;
|
|
43
|
+
getYtDlpSchedulerState: () => YtDlpSchedulerState;
|
|
42
44
|
downloadSubtitles: (input: {
|
|
43
45
|
videoId: string;
|
|
44
46
|
sourceUrl: string;
|
|
@@ -8,6 +8,7 @@ import { formatBytes } from "../utils/formatters.js";
|
|
|
8
8
|
import { hasYtDlp, runYtDlp, runYtDlpJson } from "../utils/ytdlp.js";
|
|
9
9
|
import { appendDiagnosticLog, classifyYtDlpFailure } from "../utils/ytdlpFailures.js";
|
|
10
10
|
import { parseYtDlpProgressLine } from "../utils/ytdlpProgress.js";
|
|
11
|
+
import { getGlobalYtDlpSchedulerState } from "../utils/ytdlpScheduler.js";
|
|
11
12
|
export async function downloadOneItem(input) {
|
|
12
13
|
const deps = resolveDeps(input.deps);
|
|
13
14
|
const manager = input.jobManager;
|
|
@@ -52,6 +53,7 @@ export async function downloadOneItem(input) {
|
|
|
52
53
|
if (needsVideo(input.mode)) {
|
|
53
54
|
currentStep = "download_video";
|
|
54
55
|
manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 0);
|
|
56
|
+
manager?.setItemQueuedByGlobalRateLimit(jobId, input.item.video_id, deps.getYtDlpSchedulerState().wouldThrottle);
|
|
55
57
|
try {
|
|
56
58
|
result.video_file = await deps.downloadVideo({
|
|
57
59
|
sourceUrl: input.item.source_url,
|
|
@@ -74,20 +76,29 @@ export async function downloadOneItem(input) {
|
|
|
74
76
|
throw error;
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
finally {
|
|
80
|
+
manager?.setItemQueuedByGlobalRateLimit(jobId, input.item.video_id, false);
|
|
81
|
+
}
|
|
77
82
|
result.final_file_size = formatBytes((await deps.stat(result.video_file)).size);
|
|
78
83
|
}
|
|
79
84
|
if (needsSubtitles(input.mode)) {
|
|
80
85
|
currentStep = "download_subtitles";
|
|
81
86
|
manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 0);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
102
|
}
|
|
92
103
|
currentStep = "write_metadata";
|
|
93
104
|
manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 100);
|
|
@@ -122,6 +133,7 @@ function resolveDeps(overrides) {
|
|
|
122
133
|
fetchMetadata: fetchMetadataWithYtDlp,
|
|
123
134
|
downloadVideo: downloadVideoWithYtDlp,
|
|
124
135
|
refreshCookies: tryHeadlessRefresh,
|
|
136
|
+
getYtDlpSchedulerState: getGlobalYtDlpSchedulerState,
|
|
125
137
|
downloadSubtitles: downloadSubtitlesWithYtDlp,
|
|
126
138
|
mkdir: async (path, options) => {
|
|
127
139
|
await mkdir(path, options);
|
|
@@ -10,6 +10,7 @@ export declare class DownloadJobManager {
|
|
|
10
10
|
pollJob(jobId: string): DownloadJobSnapshot;
|
|
11
11
|
startItem(jobId: string, videoId: string, step?: DownloadStep): DownloadJobSnapshot;
|
|
12
12
|
updateItemProgress(jobId: string, videoId: string, step: DownloadStep, progressPercent: number | null): DownloadJobSnapshot;
|
|
13
|
+
setItemQueuedByGlobalRateLimit(jobId: string, videoId: string, queued: boolean): DownloadJobSnapshot;
|
|
13
14
|
completeItem(jobId: string, videoId: string, outputs?: Pick<DownloadJobItemSnapshot, "video_file" | "metadata_file" | "final_file_size" | "subtitle_files">): DownloadJobSnapshot;
|
|
14
15
|
failItem(jobId: string, videoId: string, error: string, step?: DownloadStep): DownloadJobSnapshot;
|
|
15
16
|
private applyItemMutation;
|
|
@@ -26,6 +26,7 @@ export class DownloadJobManager {
|
|
|
26
26
|
title: video.title,
|
|
27
27
|
output_dir: video.output_dir,
|
|
28
28
|
status: "queued",
|
|
29
|
+
queued_by_global_rate_limit: false,
|
|
29
30
|
step: null,
|
|
30
31
|
progress_percent: null,
|
|
31
32
|
error: null,
|
|
@@ -65,6 +66,12 @@ export class DownloadJobManager {
|
|
|
65
66
|
});
|
|
66
67
|
return this.toSnapshot(job);
|
|
67
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
|
+
}
|
|
68
75
|
completeItem(jobId, videoId, outputs) {
|
|
69
76
|
const job = this.getJob(jobId);
|
|
70
77
|
const item = this.getItem(job, videoId);
|
|
@@ -96,6 +103,10 @@ export class DownloadJobManager {
|
|
|
96
103
|
const nextStep = mutation.step === undefined ? item.step : mutation.step;
|
|
97
104
|
const previousStatus = item.status;
|
|
98
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;
|
|
99
110
|
item.step = nextStep;
|
|
100
111
|
item.video_file = mutation.video_file === undefined ? item.video_file : mutation.video_file;
|
|
101
112
|
item.metadata_file = mutation.metadata_file === undefined ? item.metadata_file : mutation.metadata_file;
|
|
@@ -107,6 +118,7 @@ export class DownloadJobManager {
|
|
|
107
118
|
job.started_at = job.started_at ?? now;
|
|
108
119
|
item.completed_at = null;
|
|
109
120
|
item.error = null;
|
|
121
|
+
item.queued_by_global_rate_limit = false;
|
|
110
122
|
item.progress_percent = resolveRunningProgress(item.progress_percent, mutation.progress_percent, previousStatus);
|
|
111
123
|
}
|
|
112
124
|
if (nextStatus === "completed" || nextStatus === "failed") {
|
|
@@ -114,6 +126,7 @@ export class DownloadJobManager {
|
|
|
114
126
|
job.started_at = job.started_at ?? now;
|
|
115
127
|
item.completed_at = now;
|
|
116
128
|
item.error = mutation.error === undefined ? item.error : mutation.error;
|
|
129
|
+
item.queued_by_global_rate_limit = false;
|
|
117
130
|
item.progress_percent =
|
|
118
131
|
mutation.progress_percent === undefined ? item.progress_percent : clampProgressOrNull(mutation.progress_percent);
|
|
119
132
|
}
|
package/dist/download/types.d.ts
CHANGED
package/dist/utils/ytdlp.js
CHANGED
|
@@ -4,6 +4,8 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { PATHS } from "./config.js";
|
|
6
6
|
import { getYtDlpPath } from "./ytdlpPath.js";
|
|
7
|
+
import { markGlobalYtDlpRateLimited, scheduleYtDlp } from "./ytdlpScheduler.js";
|
|
8
|
+
import { classifyYtDlpFailure } from "./ytdlpFailures.js";
|
|
7
9
|
function writeYtDlpFailureLog(payload) {
|
|
8
10
|
try {
|
|
9
11
|
mkdirSync(PATHS.logsDir, { recursive: true });
|
|
@@ -84,7 +86,7 @@ export function buildYtDlpArgs(args, options = {}) {
|
|
|
84
86
|
return finalArgs;
|
|
85
87
|
}
|
|
86
88
|
export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
87
|
-
return new Promise((resolve, reject) => {
|
|
89
|
+
return scheduleYtDlp(() => new Promise((resolve, reject) => {
|
|
88
90
|
const start = Date.now();
|
|
89
91
|
const finalArgs = buildYtDlpArgs(args);
|
|
90
92
|
const proc = spawn(getYtDlpPath(), finalArgs, {
|
|
@@ -153,6 +155,9 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
|
153
155
|
trigger: "exit",
|
|
154
156
|
}),
|
|
155
157
|
});
|
|
158
|
+
if (exitCode !== 0 && classifyYtDlpFailure(stderr) === "RATE_LIMITED") {
|
|
159
|
+
markGlobalYtDlpRateLimited();
|
|
160
|
+
}
|
|
156
161
|
}
|
|
157
162
|
});
|
|
158
163
|
proc.on("error", (err) => {
|
|
@@ -175,7 +180,7 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
|
|
|
175
180
|
}));
|
|
176
181
|
}
|
|
177
182
|
});
|
|
178
|
-
});
|
|
183
|
+
}));
|
|
179
184
|
}
|
|
180
185
|
export async function runYtDlpJson(args, timeoutMs = 45_000) {
|
|
181
186
|
const result = await runYtDlp(args, timeoutMs);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface YtDlpSchedulerState {
|
|
2
|
+
running: boolean;
|
|
3
|
+
pendingCount: number;
|
|
4
|
+
nextAvailableAt: number | null;
|
|
5
|
+
wouldThrottle: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface YtDlpSchedulerOptions {
|
|
8
|
+
minIntervalMs?: number;
|
|
9
|
+
rateLimitBackoffMs?: number;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
sleep?: (ms: number) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface YtDlpScheduler {
|
|
14
|
+
schedule<T>(task: () => Promise<T>): Promise<T>;
|
|
15
|
+
markRateLimited(): void;
|
|
16
|
+
getState(): YtDlpSchedulerState;
|
|
17
|
+
}
|
|
18
|
+
export declare function createYtDlpScheduler(options?: YtDlpSchedulerOptions): YtDlpScheduler;
|
|
19
|
+
export declare function scheduleYtDlp<T>(task: () => Promise<T>): Promise<T>;
|
|
20
|
+
export declare function markGlobalYtDlpRateLimited(): void;
|
|
21
|
+
export declare function getGlobalYtDlpSchedulerState(): YtDlpSchedulerState;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const DEFAULT_MIN_INTERVAL_MS = 5_000;
|
|
2
|
+
const DEFAULT_RATE_LIMIT_BACKOFF_MS = 10 * 60_000;
|
|
3
|
+
export function createYtDlpScheduler(options = {}) {
|
|
4
|
+
const now = options.now ?? Date.now;
|
|
5
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6
|
+
const minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
7
|
+
const rateLimitBackoffMs = options.rateLimitBackoffMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS;
|
|
8
|
+
let running = false;
|
|
9
|
+
let pendingCount = 0;
|
|
10
|
+
let nextAvailableAt = null;
|
|
11
|
+
let tail = Promise.resolve();
|
|
12
|
+
async function waitForAvailability() {
|
|
13
|
+
const currentNext = nextAvailableAt;
|
|
14
|
+
if (currentNext == null) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const delayMs = currentNext - now();
|
|
18
|
+
if (delayMs > 0) {
|
|
19
|
+
await sleep(delayMs);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
async schedule(task) {
|
|
24
|
+
pendingCount += 1;
|
|
25
|
+
const previousTail = tail;
|
|
26
|
+
let release;
|
|
27
|
+
tail = new Promise((resolve) => {
|
|
28
|
+
release = resolve;
|
|
29
|
+
});
|
|
30
|
+
await previousTail;
|
|
31
|
+
pendingCount -= 1;
|
|
32
|
+
await waitForAvailability();
|
|
33
|
+
running = true;
|
|
34
|
+
try {
|
|
35
|
+
return await task();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
running = false;
|
|
39
|
+
nextAvailableAt = Math.max(nextAvailableAt ?? 0, now() + minIntervalMs);
|
|
40
|
+
release();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
markRateLimited() {
|
|
44
|
+
nextAvailableAt = Math.max(nextAvailableAt ?? 0, now() + rateLimitBackoffMs);
|
|
45
|
+
},
|
|
46
|
+
getState() {
|
|
47
|
+
const next = nextAvailableAt;
|
|
48
|
+
return {
|
|
49
|
+
running,
|
|
50
|
+
pendingCount,
|
|
51
|
+
nextAvailableAt: next,
|
|
52
|
+
wouldThrottle: running || pendingCount > 0 || (next != null && next > now()),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const globalYtDlpScheduler = createYtDlpScheduler();
|
|
58
|
+
export function scheduleYtDlp(task) {
|
|
59
|
+
return globalYtDlpScheduler.schedule(task);
|
|
60
|
+
}
|
|
61
|
+
export function markGlobalYtDlpRateLimited() {
|
|
62
|
+
globalYtDlpScheduler.markRateLimited();
|
|
63
|
+
}
|
|
64
|
+
export function getGlobalYtDlpSchedulerState() {
|
|
65
|
+
return globalYtDlpScheduler.getState();
|
|
66
|
+
}
|