ytdlp-cookie-keeper 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bryan Kuang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # ytdlp-cookie-keeper
2
+
3
+ Keep your yt-dlp YouTube cookies fresh on a headless server — export from a persistent browser profile, validate against real videos, swap atomically. **Never clobber a working cookie file with a broken one.**
4
+
5
+ ## The problem
6
+
7
+ Running yt-dlp on a server, you've probably seen this:
8
+
9
+ ```
10
+ ERROR: Sign in to confirm you're not a bot. Use --cookies-from-browser or --cookies ...
11
+ ```
12
+
13
+ So you export cookies from a logged-in browser and pass `--cookies cookies.txt`. It works — until YouTube rotates the session and the file goes stale, usually within days. The obvious fix, re-exporting on a cron with `--cookies-from-browser` pointed at a browser profile on the server, has a nasty failure mode: **the moment the browser session dies, the export silently produces logged-out cookies and overwrites the working file you had.** Playback breaks at the worst possible time and you don't find out until it does.
14
+
15
+ `ytdlp-cookie-keeper` closes that gap. Every refresh cycle:
16
+
17
+ 1. **Exports** cookies from a persistent browser profile into a temp candidate file (`yt-dlp --cookies-from-browser`).
18
+ 2. **Validates** the candidate end-to-end: yt-dlp must successfully extract real watch URLs using *only the candidate*.
19
+ 3. **Swaps** the live file in place (same path, same inode, `0600` perms) — only after validation passes.
20
+
21
+ A candidate that fails export or validation is discarded and your previous cookie file stays untouched. A stale-but-working file beats a fresh-but-broken one.
22
+
23
+ Extras you'd otherwise reimplement: a cooldown window so error-triggered refreshes can't stampede, in-flight deduplication so concurrent triggers join one refresh, and log/error redaction so cookie values and signed CDN URLs never leak into your logs.
24
+
25
+ ## Requirements
26
+
27
+ - Node.js ≥ 20 (or [Bun](https://bun.sh))
28
+ - A recent [yt-dlp](https://github.com/yt-dlp/yt-dlp) on PATH (YouTube extraction increasingly needs a JS runtime — install `yt-dlp[default]` via pip, or have Deno/Node available for its challenge solver)
29
+ - A browser installed on the server with a **logged-in YouTube session** in a persistent profile (see [setup](#setting-up-the-persistent-browser-profile))
30
+
31
+ ## CLI
32
+
33
+ ```bash
34
+ npm install -g ytdlp-cookie-keeper # or: npx ytdlp-cookie-keeper ...
35
+ ```
36
+
37
+ ### One-shot refresh (cron-friendly)
38
+
39
+ ```bash
40
+ ytdlp-cookie-keeper refresh \
41
+ --cookies /srv/secrets/youtube_cookies.txt \
42
+ --browser chrome+basictext:/srv/youtube-browser-profile
43
+ ```
44
+
45
+ Exit `0` on success, `1` on failure. The `--browser` value is passed straight to yt-dlp's `--cookies-from-browser` — the `+basictext` selects Chrome's plaintext keyring, typical for a dedicated headless-server profile.
46
+
47
+ ### Daemon
48
+
49
+ ```bash
50
+ ytdlp-cookie-keeper daemon \
51
+ --cookies /srv/secrets/youtube_cookies.txt \
52
+ --browser chrome+basictext:/srv/youtube-browser-profile \
53
+ --interval 6h --refresh-on-start
54
+ ```
55
+
56
+ ### Freshness check (alerting)
57
+
58
+ The refresh loop only rewrites the file on success — so if the browser session dies, failures are silent and the file just quietly ages. Alert on that from anywhere (cron, CI, a monitoring box):
59
+
60
+ ```bash
61
+ ytdlp-cookie-keeper check --cookies /srv/secrets/youtube_cookies.txt --max-age 13h
62
+ ```
63
+
64
+ | Exit code | Meaning |
65
+ |---|---|
66
+ | `0` | fresh |
67
+ | `2` | file missing |
68
+ | `3` | stale — refresh loop is failing; the browser profile likely needs a re-login |
69
+
70
+ A `13h` threshold on a `6h` interval tolerates one missed cycle without flapping.
71
+
72
+ Durations everywhere accept `90s`, `5m`, `6h`; bare numbers mean seconds.
73
+
74
+ ## Library
75
+
76
+ ```ts
77
+ import { CookieKeeper } from 'ytdlp-cookie-keeper';
78
+
79
+ const keeper = new CookieKeeper({
80
+ cookiesFile: '/srv/secrets/youtube_cookies.txt',
81
+ browserSpec: 'chrome+basictext:/srv/youtube-browser-profile',
82
+ refreshIntervalMs: 6 * 60 * 60 * 1000,
83
+ });
84
+
85
+ keeper.on('refresh:success', (e) => metrics.gauge('cookie_last_refresh', e.timestampMs / 1000));
86
+ keeper.on('refresh:failure', (e) => logger.warn('cookie refresh failed', { error: e.error }));
87
+
88
+ keeper.start(); // background loop
89
+
90
+ // Or trigger on demand, e.g. when playback hits a 403:
91
+ const result = await keeper.refreshNow({ reason: 'playback-403' });
92
+ ```
93
+
94
+ Concurrent `refreshNow()` calls join the in-flight refresh; calls inside the cooldown window (default 5m) are skipped unless `{ force: true }`. Failure messages are redacted (no URLs, no cookie values) and safe to log.
95
+
96
+ ## Setting up the persistent browser profile
97
+
98
+ The keeper refreshes cookies from a browser profile; keeping that profile's Google session alive is the part you own. A recipe for a Linux VPS:
99
+
100
+ ```bash
101
+ # 1. Virtual display + Chrome with a dedicated profile dir
102
+ apt install xvfb x11vnc chromium xdotool
103
+ Xvfb :94 -screen 0 1280x800x24 &
104
+ DISPLAY=:94 chromium --user-data-dir=/srv/youtube-browser-profile \
105
+ --password-store=basic --no-first-run &
106
+
107
+ # 2. Log in interactively, once, over VNC
108
+ x11vnc -display :94 -localhost -once
109
+ # tunnel: ssh -L 5900:localhost:5900 you@server, then VNC to localhost:5900
110
+ # and sign in to YouTube in the Chrome window
111
+
112
+ # 3. Point the keeper at the profile
113
+ ytdlp-cookie-keeper refresh \
114
+ --cookies /srv/secrets/youtube_cookies.txt \
115
+ --browser chrome+basictext:/srv/youtube-browser-profile
116
+ ```
117
+
118
+ Tips that make the session last:
119
+
120
+ - **Use a throwaway Google account**, not your own — automated-looking activity can trip account security.
121
+ - **Keep Chrome running** (or restart it periodically) so its background token refresh keeps the session warm; a profile that's never opened goes stale faster.
122
+ - `--password-store=basic` at first launch matches the `+basictext` keyring in the browser spec — cookies stay decryptable without a desktop keyring.
123
+ - When Google eventually forces a re-login (weeks to months), **log in manually over VNC again**. Don't script the login: Google actively detects automated sign-ins, and CAPTCHA/2FA challenges can't be bypassed — a typed-credentials script fails exactly when you need it and puts the account at risk.
124
+
125
+ ## Recipes
126
+
127
+ ### systemd
128
+
129
+ ```ini
130
+ # /etc/systemd/system/cookie-keeper.service
131
+ [Unit]
132
+ Description=Keep yt-dlp YouTube cookies fresh
133
+ After=network-online.target
134
+
135
+ [Service]
136
+ ExecStart=/usr/bin/ytdlp-cookie-keeper daemon \
137
+ --cookies /srv/secrets/youtube_cookies.txt \
138
+ --browser chrome+basictext:/srv/youtube-browser-profile \
139
+ --interval 6h --refresh-on-start
140
+ Restart=on-failure
141
+ User=ytdlp
142
+
143
+ [Install]
144
+ WantedBy=multi-user.target
145
+ ```
146
+
147
+ ### GitHub Actions staleness alert
148
+
149
+ ```yaml
150
+ name: Cookie Health
151
+ on:
152
+ schedule: [{ cron: '0 */6 * * *' }]
153
+ workflow_dispatch:
154
+ jobs:
155
+ check:
156
+ runs-on: ubuntu-latest
157
+ steps:
158
+ - name: Check cookie freshness on the server
159
+ uses: appleboy/ssh-action@v1
160
+ with:
161
+ host: ${{ secrets.HOST }}
162
+ username: ${{ secrets.USER }}
163
+ key: ${{ secrets.SSH_KEY }}
164
+ script: npx ytdlp-cookie-keeper check --cookies /srv/secrets/youtube_cookies.txt --max-age 13h
165
+ - name: Alert on stale cookies
166
+ if: failure()
167
+ run: |
168
+ curl -s -X POST "${{ secrets.WEBHOOK_URL }}" -H 'Content-Type: application/json' \
169
+ -d '{"text":"YouTube cookies are stale — the browser profile likely needs a manual re-login."}'
170
+ ```
171
+
172
+ ### Docker
173
+
174
+ Run the daemon as a sidecar sharing a `secrets/` volume with whatever consumes the cookie file; mount the browser profile read-only into the sidecar. The consumer only ever sees a validated file.
175
+
176
+ ## Scope and non-goals
177
+
178
+ - **No login automation.** This tool assumes a working browser session and keeps cookies flowing from it; establishing the session is a manual, interactive step by design (see above).
179
+ - **Not a bypass.** If YouTube challenges the account, the validation step fails closed and your previous cookie file is preserved — nothing here evades bot detection; it just stops session rot from breaking you silently.
180
+ - YouTube-flavored defaults, but the export→validate→swap pattern is site-agnostic: point `--validate-url` at any site yt-dlp supports.
181
+
182
+ ## Provenance
183
+
184
+ Extracted from [FM-Hachimi](https://github.com/Bryan-Kuang/FM-Hachimi), a Discord music bot that has run this refresh loop unattended in production. MIT.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pure helpers backing the CLI: duration parsing and cookie-file freshness.
3
+ * Kept free of process/argv side effects so they are directly testable.
4
+ */
5
+ /** Parse '90s' / '5m' / '6h' (bare numbers mean seconds) into milliseconds. */
6
+ export declare function parseDuration(value: string): number;
7
+ export interface FreshnessResult {
8
+ status: 'ok' | 'stale' | 'missing';
9
+ ageMs?: number;
10
+ }
11
+ /** Compare a cookie file's mtime against a maximum allowed age. */
12
+ export declare function checkFreshness(cookiesFile: string, maxAgeMs: number): FreshnessResult;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Pure helpers backing the CLI: duration parsing and cookie-file freshness.
4
+ * Kept free of process/argv side effects so they are directly testable.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.parseDuration = parseDuration;
41
+ exports.checkFreshness = checkFreshness;
42
+ const fs = __importStar(require("fs"));
43
+ const DURATION_PATTERN = /^(\d+(?:\.\d+)?)([smh]?)$/;
44
+ const UNIT_MS = {
45
+ s: 1000,
46
+ m: 60 * 1000,
47
+ h: 60 * 60 * 1000,
48
+ };
49
+ /** Parse '90s' / '5m' / '6h' (bare numbers mean seconds) into milliseconds. */
50
+ function parseDuration(value) {
51
+ const match = DURATION_PATTERN.exec(value.trim());
52
+ if (!match) {
53
+ throw new Error(`Invalid duration '${value}' — use forms like 90s, 5m, 6h`);
54
+ }
55
+ const amount = Number(match[1]);
56
+ const unit = match[2] || 's';
57
+ return amount * UNIT_MS[unit];
58
+ }
59
+ /** Compare a cookie file's mtime against a maximum allowed age. */
60
+ function checkFreshness(cookiesFile, maxAgeMs) {
61
+ let stat;
62
+ try {
63
+ stat = fs.statSync(cookiesFile);
64
+ }
65
+ catch {
66
+ return { status: 'missing' };
67
+ }
68
+ // Filesystem timestamps can be fractionally ahead of Date.now(); never report a negative age.
69
+ const ageMs = Math.max(0, Date.now() - stat.mtimeMs);
70
+ return { status: ageMs > maxAgeMs ? 'stale' : 'ok', ageMs };
71
+ }
72
+ //# sourceMappingURL=cli-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-support.js","sourceRoot":"","sources":["../src/cli-support.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaH,sCAQC;AAQD,wCAUC;AArCD,uCAAyB;AAEzB,MAAM,gBAAgB,GAAG,2BAA2B,CAAC;AAErD,MAAM,OAAO,GAA2B;IACtC,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,EAAE,GAAG,IAAI;IACZ,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;CAClB,CAAC;AAEF,+EAA+E;AAC/E,SAAgB,aAAa,CAAC,KAAa;IACzC,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,gCAAgC,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;IAC7B,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAOD,mEAAmE;AACnE,SAAgB,cAAc,CAAC,WAAmB,EAAE,QAAgB;IAClE,IAAI,IAAc,CAAC;IACnB,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;IACD,8FAA8F;IAC9F,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IACrD,OAAO,EAAE,MAAM,EAAE,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC9D,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ytdlp-cookie-keeper CLI
4
+ *
5
+ * refresh One-shot export + validate + swap. Exit 0 on success, 1 on failure.
6
+ * daemon Refresh on an interval until terminated.
7
+ * check Freshness probe for cron/CI alerting.
8
+ * Exit 0 = fresh, 2 = file missing, 3 = stale.
9
+ */
10
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * ytdlp-cookie-keeper CLI
5
+ *
6
+ * refresh One-shot export + validate + swap. Exit 0 on success, 1 on failure.
7
+ * daemon Refresh on an interval until terminated.
8
+ * check Freshness probe for cron/CI alerting.
9
+ * Exit 0 = fresh, 2 = file missing, 3 = stale.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const cookie_keeper_1 = require("./cookie-keeper");
13
+ const cli_support_1 = require("./cli-support");
14
+ const USAGE = `Usage: ytdlp-cookie-keeper <command> [options]
15
+
16
+ Commands:
17
+ refresh Export cookies from the browser profile, validate, swap. One-shot.
18
+ daemon Run refresh on an interval until terminated.
19
+ check Verify the cookie file is fresh (for cron/CI alerting).
20
+
21
+ Options (refresh, daemon):
22
+ --cookies <file> Live cookie file to keep fresh (required)
23
+ --browser <spec> yt-dlp --cookies-from-browser spec, e.g.
24
+ chrome+basictext:/srv/youtube-profile (required)
25
+ --validate-url <url> Watch URL the candidate must extract (repeatable;
26
+ defaults to two evergreen videos)
27
+ --yt-dlp <path> yt-dlp executable (default: yt-dlp on PATH)
28
+ --timeout <duration> Per-invocation timeout (default: 90s)
29
+
30
+ Options (daemon):
31
+ --interval <duration> Time between refreshes (default: 6h)
32
+ --cooldown <duration> Minimum gap between attempts (default: 5m)
33
+ --refresh-on-start Also refresh immediately on startup
34
+
35
+ Options (check):
36
+ --cookies <file> Cookie file to inspect (required)
37
+ --max-age <duration> Maximum acceptable age (default: 13h)
38
+
39
+ Durations accept 90s, 5m, 6h; bare numbers mean seconds.
40
+
41
+ Exit codes:
42
+ refresh/daemon: 0 success, 1 failure, 64 usage error
43
+ check: 0 fresh, 2 missing, 3 stale, 64 usage error
44
+ `;
45
+ const VALUE_FLAGS = new Set(['--cookies', '--browser', '--validate-url', '--yt-dlp', '--timeout', '--interval', '--cooldown', '--max-age']);
46
+ const BOOLEAN_FLAGS = new Set(['--refresh-on-start', '--quiet']);
47
+ function parseArgs(argv) {
48
+ const [command, ...rest] = argv;
49
+ if (!command)
50
+ usageError('missing command');
51
+ const flags = new Map();
52
+ const booleans = new Set();
53
+ for (let i = 0; i < rest.length; i++) {
54
+ const arg = rest[i];
55
+ if (BOOLEAN_FLAGS.has(arg)) {
56
+ booleans.add(arg);
57
+ }
58
+ else if (VALUE_FLAGS.has(arg)) {
59
+ const value = rest[++i];
60
+ if (value === undefined)
61
+ usageError(`${arg} requires a value`);
62
+ const list = flags.get(arg) ?? [];
63
+ list.push(value);
64
+ flags.set(arg, list);
65
+ }
66
+ else {
67
+ usageError(`unknown option '${arg}'`);
68
+ }
69
+ }
70
+ return { command, flags, booleans };
71
+ }
72
+ function usageError(message) {
73
+ process.stderr.write(`error: ${message}\n\n${USAGE}`);
74
+ process.exit(64);
75
+ }
76
+ function required(args, flag) {
77
+ const values = args.flags.get(flag);
78
+ if (!values?.length)
79
+ usageError(`${flag} is required for '${args.command}'`);
80
+ return values[values.length - 1];
81
+ }
82
+ function optional(args, flag) {
83
+ const values = args.flags.get(flag);
84
+ return values?.[values.length - 1];
85
+ }
86
+ function duration(args, flag, fallbackMs) {
87
+ const raw = optional(args, flag);
88
+ if (raw === undefined)
89
+ return fallbackMs;
90
+ try {
91
+ return (0, cli_support_1.parseDuration)(raw);
92
+ }
93
+ catch (error) {
94
+ usageError(error.message);
95
+ }
96
+ }
97
+ function log(message) {
98
+ process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
99
+ }
100
+ function buildKeeper(args, intervalMs, cooldownMs) {
101
+ const keeper = new cookie_keeper_1.CookieKeeper({
102
+ cookiesFile: required(args, '--cookies'),
103
+ browserSpec: required(args, '--browser'),
104
+ validateUrls: args.flags.get('--validate-url'),
105
+ ytDlpPath: optional(args, '--yt-dlp'),
106
+ processTimeoutMs: duration(args, '--timeout', 90_000),
107
+ refreshIntervalMs: intervalMs,
108
+ cooldownMs,
109
+ });
110
+ keeper.on('refresh:start', (e) => log(`refresh started (browser: ${e.browserSpec})`));
111
+ keeper.on('refresh:success', (e) => log(`refresh succeeded — validated against ${e.validateUrlCount} URL(s)`));
112
+ keeper.on('refresh:failure', (e) => log(`refresh failed: ${e.error}`));
113
+ keeper.on('refresh:skipped', (e) => log(`refresh skipped (cooldown, retry in ${Math.ceil(e.retryInMs / 1000)}s)`));
114
+ return keeper;
115
+ }
116
+ async function runRefresh(args) {
117
+ const keeper = buildKeeper(args, 0, 0);
118
+ const result = await keeper.refreshNow({ reason: 'cli', force: true });
119
+ process.exit(result.success ? 0 : 1);
120
+ }
121
+ async function runDaemon(args) {
122
+ const intervalMs = duration(args, '--interval', 6 * 60 * 60 * 1000);
123
+ const cooldownMs = duration(args, '--cooldown', 5 * 60 * 1000);
124
+ const keeper = buildKeeper(args, intervalMs, cooldownMs);
125
+ const shutdown = (signal) => {
126
+ log(`received ${signal}, stopping`);
127
+ keeper.stop();
128
+ process.exit(0);
129
+ };
130
+ process.on('SIGINT', () => shutdown('SIGINT'));
131
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
132
+ keeper.start();
133
+ log(`daemon started (interval ${Math.round(intervalMs / 1000)}s)`);
134
+ if (args.booleans.has('--refresh-on-start')) {
135
+ keeper.refreshInBackground({ reason: 'startup' });
136
+ }
137
+ // Keep the process alive; the keeper's own interval is unref'd by design.
138
+ setInterval(() => { }, 1 << 30);
139
+ }
140
+ function runCheck(args) {
141
+ const cookiesFile = required(args, '--cookies');
142
+ const maxAgeMs = duration(args, '--max-age', 13 * 60 * 60 * 1000);
143
+ const result = (0, cli_support_1.checkFreshness)(cookiesFile, maxAgeMs);
144
+ if (result.status === 'missing') {
145
+ process.stdout.write(`MISSING: ${cookiesFile} does not exist\n`);
146
+ process.exit(2);
147
+ }
148
+ const ageSeconds = Math.round((result.ageMs ?? 0) / 1000);
149
+ const thresholdSeconds = Math.round(maxAgeMs / 1000);
150
+ if (result.status === 'stale') {
151
+ process.stdout.write(`STALE: cookie age ${ageSeconds}s exceeds threshold ${thresholdSeconds}s — the refresh loop is likely failing (browser profile may need re-login)\n`);
152
+ process.exit(3);
153
+ }
154
+ process.stdout.write(`OK: cookie age ${ageSeconds}s (threshold ${thresholdSeconds}s)\n`);
155
+ process.exit(0);
156
+ }
157
+ async function main() {
158
+ const args = parseArgs(process.argv.slice(2));
159
+ switch (args.command) {
160
+ case 'refresh':
161
+ return runRefresh(args);
162
+ case 'daemon':
163
+ return runDaemon(args);
164
+ case 'check':
165
+ return runCheck(args);
166
+ case 'help':
167
+ case '--help':
168
+ case '-h':
169
+ process.stdout.write(USAGE);
170
+ return;
171
+ default:
172
+ usageError(`unknown command '${args.command}'`);
173
+ }
174
+ }
175
+ void main().catch((error) => {
176
+ process.stderr.write(`fatal: ${error.message}\n`);
177
+ process.exit(1);
178
+ });
179
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * CookieKeeper
3
+ * Keeps a yt-dlp YouTube cookie file fresh on a headless server.
4
+ *
5
+ * The refresh cycle never risks the live cookie file:
6
+ * 1. Export cookies from a persistent browser profile into a temp candidate
7
+ * file (`yt-dlp --cookies-from-browser`).
8
+ * 2. Validate the candidate end-to-end against real watch URLs.
9
+ * 3. Only then overwrite the live file, in place (same inode, 0600 perms),
10
+ * so anything holding the path open keeps working.
11
+ * A candidate that fails export or validation is discarded and the previous
12
+ * live file stays untouched.
13
+ *
14
+ * Concurrency: a refresh already in flight is joined, not duplicated, and a
15
+ * cooldown window absorbs bursts of retry-triggered refresh requests.
16
+ */
17
+ import { EventEmitter } from 'events';
18
+ export interface RefreshContext {
19
+ /** Why this refresh was requested, e.g. 'scheduled' or 'playback-403'. */
20
+ reason?: string;
21
+ /** Which component requested it. */
22
+ source?: string;
23
+ /** Bypass the cooldown window. */
24
+ force?: boolean;
25
+ }
26
+ export interface RefreshResult {
27
+ success: boolean;
28
+ refreshed: boolean;
29
+ skipped?: boolean;
30
+ error?: string;
31
+ }
32
+ export interface RefreshStartEvent {
33
+ reason?: string;
34
+ source?: string;
35
+ /** Browser spec with the profile path redacted. */
36
+ browserSpec: string;
37
+ }
38
+ export interface RefreshSuccessEvent {
39
+ reason?: string;
40
+ source?: string;
41
+ validateUrlCount: number;
42
+ /** Unix time (ms) of the successful swap — feed this to your metrics/alerts. */
43
+ timestampMs: number;
44
+ }
45
+ export interface RefreshFailureEvent {
46
+ reason?: string;
47
+ source?: string;
48
+ /** Human-readable failure, with URLs and cookie values redacted. */
49
+ error: string;
50
+ }
51
+ export interface RefreshSkippedEvent {
52
+ reason?: string;
53
+ source?: string;
54
+ why: 'cooldown';
55
+ /** Milliseconds until the cooldown window ends. */
56
+ retryInMs: number;
57
+ }
58
+ /** Minimal surface of child_process.spawn output that CookieKeeper needs. */
59
+ export interface SpawnedProcess {
60
+ stdout?: {
61
+ on(event: 'data', listener: (data: Buffer) => void): unknown;
62
+ } | null;
63
+ stderr?: {
64
+ on(event: 'data', listener: (data: Buffer) => void): unknown;
65
+ } | null;
66
+ killed: boolean;
67
+ kill(signal?: NodeJS.Signals | number): unknown;
68
+ on(event: 'close', listener: (code: number | null) => void): unknown;
69
+ on(event: 'error', listener: (error: Error) => void): unknown;
70
+ }
71
+ export type SpawnFunction = (command: string, args: readonly string[]) => SpawnedProcess;
72
+ export interface CookieKeeperOptions {
73
+ /** Live cookie file consumed by yt-dlp (`--cookies <file>`). Overwritten in place only after validation. */
74
+ cookiesFile: string;
75
+ /** `--cookies-from-browser` spec, e.g. `chrome+basictext:/srv/youtube-profile`. */
76
+ browserSpec: string;
77
+ /** Watch URLs the candidate must extract successfully before it goes live. */
78
+ validateUrls?: string[];
79
+ /** Interval for start()'s background refresh loop. 0 disables the loop. Default 6h. */
80
+ refreshIntervalMs?: number;
81
+ /** Minimum time between refresh attempts unless forced. Default 5m. */
82
+ cooldownMs?: number;
83
+ /** Where candidate files are staged. Default os.tmpdir(). */
84
+ tmpDir?: string;
85
+ /** yt-dlp executable. Default 'yt-dlp' from PATH. */
86
+ ytDlpPath?: string;
87
+ /** Timeout per yt-dlp invocation. Default 90s. */
88
+ processTimeoutMs?: number;
89
+ /** Injectable spawn, for testing. Default child_process.spawn. */
90
+ spawnFn?: SpawnFunction;
91
+ }
92
+ export declare interface CookieKeeper {
93
+ on(event: 'refresh:start', listener: (event: RefreshStartEvent) => void): this;
94
+ on(event: 'refresh:success', listener: (event: RefreshSuccessEvent) => void): this;
95
+ on(event: 'refresh:failure', listener: (event: RefreshFailureEvent) => void): this;
96
+ on(event: 'refresh:skipped', listener: (event: RefreshSkippedEvent) => void): this;
97
+ once(event: 'refresh:start', listener: (event: RefreshStartEvent) => void): this;
98
+ once(event: 'refresh:success', listener: (event: RefreshSuccessEvent) => void): this;
99
+ once(event: 'refresh:failure', listener: (event: RefreshFailureEvent) => void): this;
100
+ once(event: 'refresh:skipped', listener: (event: RefreshSkippedEvent) => void): this;
101
+ }
102
+ export declare class CookieKeeper extends EventEmitter {
103
+ private readonly cookiesFile;
104
+ private readonly browserSpec;
105
+ private readonly validateUrls;
106
+ private readonly refreshIntervalMs;
107
+ private readonly cooldownMs;
108
+ private readonly tmpDir;
109
+ private readonly ytDlpPath;
110
+ private readonly processTimeoutMs;
111
+ private readonly spawnFn;
112
+ private interval;
113
+ private inFlight;
114
+ private lastAttemptAt;
115
+ constructor(options: CookieKeeperOptions);
116
+ /** Start the background refresh loop (no-op if refreshIntervalMs is 0). */
117
+ start(): void;
118
+ stop(): void;
119
+ /** Fire-and-forget refresh; failures surface via the 'refresh:failure' event. */
120
+ refreshInBackground(context?: RefreshContext): void;
121
+ refreshNow(context?: RefreshContext): Promise<RefreshResult>;
122
+ private doRefresh;
123
+ private hasCookiePayload;
124
+ private failure;
125
+ private runYtDlp;
126
+ private redactBrowserSpec;
127
+ /** Strip URLs, signed query params, and cookie values from error details. */
128
+ private redact;
129
+ }
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ /**
3
+ * CookieKeeper
4
+ * Keeps a yt-dlp YouTube cookie file fresh on a headless server.
5
+ *
6
+ * The refresh cycle never risks the live cookie file:
7
+ * 1. Export cookies from a persistent browser profile into a temp candidate
8
+ * file (`yt-dlp --cookies-from-browser`).
9
+ * 2. Validate the candidate end-to-end against real watch URLs.
10
+ * 3. Only then overwrite the live file, in place (same inode, 0600 perms),
11
+ * so anything holding the path open keeps working.
12
+ * A candidate that fails export or validation is discarded and the previous
13
+ * live file stays untouched.
14
+ *
15
+ * Concurrency: a refresh already in flight is joined, not duplicated, and a
16
+ * cooldown window absorbs bursts of retry-triggered refresh requests.
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.CookieKeeper = void 0;
53
+ const child_process_1 = require("child_process");
54
+ const events_1 = require("events");
55
+ const fs = __importStar(require("fs"));
56
+ const os = __importStar(require("os"));
57
+ const path = __importStar(require("path"));
58
+ const DEFAULT_VALIDATE_URLS = [
59
+ // Evergreen videos that are very unlikely to ever be removed or region-locked.
60
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', // Rick Astley — Never Gonna Give You Up
61
+ 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // "Me at the zoo" — the first YouTube video
62
+ ];
63
+ const DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1000;
64
+ const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
65
+ const DEFAULT_PROCESS_TIMEOUT_MS = 90_000;
66
+ const NETSCAPE_HEADER = '# Netscape HTTP Cookie File\n';
67
+ class CookieKeeper extends events_1.EventEmitter {
68
+ cookiesFile;
69
+ browserSpec;
70
+ validateUrls;
71
+ refreshIntervalMs;
72
+ cooldownMs;
73
+ tmpDir;
74
+ ytDlpPath;
75
+ processTimeoutMs;
76
+ spawnFn;
77
+ interval = null;
78
+ inFlight = null;
79
+ lastAttemptAt = 0;
80
+ constructor(options) {
81
+ super();
82
+ if (!options.cookiesFile)
83
+ throw new Error('cookiesFile is required');
84
+ if (!options.browserSpec)
85
+ throw new Error('browserSpec is required');
86
+ this.cookiesFile = options.cookiesFile;
87
+ this.browserSpec = options.browserSpec;
88
+ this.validateUrls = options.validateUrls?.length ? options.validateUrls : DEFAULT_VALIDATE_URLS;
89
+ this.refreshIntervalMs = Math.max(0, options.refreshIntervalMs ?? DEFAULT_INTERVAL_MS);
90
+ this.cooldownMs = Math.max(0, options.cooldownMs ?? DEFAULT_COOLDOWN_MS);
91
+ this.tmpDir = options.tmpDir || os.tmpdir();
92
+ this.ytDlpPath = options.ytDlpPath || 'yt-dlp';
93
+ this.processTimeoutMs = options.processTimeoutMs ?? DEFAULT_PROCESS_TIMEOUT_MS;
94
+ this.spawnFn = options.spawnFn || child_process_1.spawn;
95
+ }
96
+ /** Start the background refresh loop (no-op if refreshIntervalMs is 0). */
97
+ start() {
98
+ if (this.refreshIntervalMs <= 0 || this.interval)
99
+ return;
100
+ this.interval = setInterval(() => {
101
+ this.refreshInBackground({ reason: 'scheduled' });
102
+ }, this.refreshIntervalMs);
103
+ this.interval.unref();
104
+ }
105
+ stop() {
106
+ if (!this.interval)
107
+ return;
108
+ clearInterval(this.interval);
109
+ this.interval = null;
110
+ }
111
+ /** Fire-and-forget refresh; failures surface via the 'refresh:failure' event. */
112
+ refreshInBackground(context = {}) {
113
+ void this.refreshNow(context).catch((error) => {
114
+ this.emit('refresh:failure', {
115
+ reason: context.reason,
116
+ source: context.source,
117
+ error: this.redact(error.message),
118
+ });
119
+ });
120
+ }
121
+ async refreshNow(context = {}) {
122
+ if (this.inFlight)
123
+ return this.inFlight;
124
+ const now = Date.now();
125
+ if (!context.force && this.lastAttemptAt > 0 && now - this.lastAttemptAt < this.cooldownMs) {
126
+ const retryInMs = this.cooldownMs - (now - this.lastAttemptAt);
127
+ this.emit('refresh:skipped', {
128
+ reason: context.reason,
129
+ source: context.source,
130
+ why: 'cooldown',
131
+ retryInMs,
132
+ });
133
+ return {
134
+ success: false,
135
+ refreshed: false,
136
+ skipped: true,
137
+ error: `refresh is cooling down for another ${retryInMs}ms`,
138
+ };
139
+ }
140
+ this.lastAttemptAt = now;
141
+ this.inFlight = this.doRefresh(context).finally(() => {
142
+ this.inFlight = null;
143
+ });
144
+ return this.inFlight;
145
+ }
146
+ async doRefresh(context) {
147
+ let workDir = null;
148
+ try {
149
+ fs.mkdirSync(path.dirname(this.cookiesFile), { recursive: true });
150
+ fs.mkdirSync(this.tmpDir, { recursive: true });
151
+ workDir = fs.mkdtempSync(path.join(this.tmpDir, 'cookie-keeper-'));
152
+ const candidateFile = path.join(workDir, 'candidate_cookies.txt');
153
+ fs.writeFileSync(candidateFile, NETSCAPE_HEADER, { mode: 0o600 });
154
+ this.emit('refresh:start', {
155
+ reason: context.reason,
156
+ source: context.source,
157
+ browserSpec: this.redactBrowserSpec(this.browserSpec),
158
+ });
159
+ const exportResult = await this.runYtDlp([
160
+ '--cookies-from-browser', this.browserSpec,
161
+ '--cookies', candidateFile,
162
+ '--skip-download',
163
+ '--flat-playlist',
164
+ '--playlist-items', '0',
165
+ '--extractor-args', 'youtubetab:skip=authcheck',
166
+ 'https://www.youtube.com/',
167
+ ]);
168
+ if (exportResult.code !== 0) {
169
+ return this.failure(context, 'cookie export failed', exportResult.stderr);
170
+ }
171
+ if (!this.hasCookiePayload(candidateFile)) {
172
+ return this.failure(context, 'cookie export produced no usable cookies');
173
+ }
174
+ for (const validateUrl of this.validateUrls) {
175
+ const validateResult = await this.runYtDlp([
176
+ '--cookies', candidateFile,
177
+ '--skip-download',
178
+ '--no-playlist',
179
+ '--no-warnings',
180
+ '--format', 'bestaudio[vcodec=none][acodec!=none]/best[height<=360][acodec!=none]/worst[acodec!=none]',
181
+ '--print', 'id',
182
+ validateUrl,
183
+ ]);
184
+ if (validateResult.code !== 0) {
185
+ return this.failure(context, 'exported cookies failed validation', validateResult.stderr);
186
+ }
187
+ }
188
+ const candidate = fs.readFileSync(candidateFile);
189
+ fs.writeFileSync(this.cookiesFile, candidate, { mode: 0o600 });
190
+ fs.chmodSync(this.cookiesFile, 0o600);
191
+ this.emit('refresh:success', {
192
+ reason: context.reason,
193
+ source: context.source,
194
+ validateUrlCount: this.validateUrls.length,
195
+ timestampMs: Date.now(),
196
+ });
197
+ return { success: true, refreshed: true };
198
+ }
199
+ catch (error) {
200
+ return this.failure(context, 'refresh failed', error.message);
201
+ }
202
+ finally {
203
+ if (workDir) {
204
+ fs.rmSync(workDir, { recursive: true, force: true });
205
+ }
206
+ }
207
+ }
208
+ hasCookiePayload(cookieFile) {
209
+ if (!fs.existsSync(cookieFile))
210
+ return false;
211
+ const raw = fs.readFileSync(cookieFile, 'utf8');
212
+ const dataLines = raw
213
+ .split(/\r?\n/)
214
+ .map((line) => line.trim())
215
+ .map((line) => (line.startsWith('#HttpOnly_') ? line.replace(/^#HttpOnly_/, '') : line))
216
+ .filter((line) => line && !line.startsWith('#'));
217
+ return dataLines.some((line) => line.includes('youtube.com') || line.includes('\tSID\t'));
218
+ }
219
+ failure(context, message, rawDetails = '') {
220
+ const error = rawDetails ? `${message}: ${this.redact(rawDetails)}` : message;
221
+ this.emit('refresh:failure', {
222
+ reason: context.reason,
223
+ source: context.source,
224
+ error,
225
+ });
226
+ return { success: false, refreshed: false, error };
227
+ }
228
+ runYtDlp(args) {
229
+ return new Promise((resolve, reject) => {
230
+ const child = this.spawnFn(this.ytDlpPath, args);
231
+ let stdout = '';
232
+ let stderr = '';
233
+ let settled = false;
234
+ child.stdout?.on('data', (data) => {
235
+ stdout += data.toString();
236
+ });
237
+ child.stderr?.on('data', (data) => {
238
+ stderr += data.toString();
239
+ });
240
+ const timeoutId = setTimeout(() => {
241
+ if (settled)
242
+ return;
243
+ child.kill('SIGTERM');
244
+ const killTimer = setTimeout(() => {
245
+ if (!child.killed)
246
+ child.kill('SIGKILL');
247
+ }, 2000);
248
+ killTimer.unref();
249
+ settled = true;
250
+ resolve({ code: 143, stdout, stderr: `${stderr}\nTimed out` });
251
+ }, this.processTimeoutMs);
252
+ timeoutId.unref();
253
+ child.on('close', (code) => {
254
+ if (settled)
255
+ return;
256
+ settled = true;
257
+ clearTimeout(timeoutId);
258
+ resolve({ code, stdout, stderr });
259
+ });
260
+ child.on('error', (error) => {
261
+ if (settled)
262
+ return;
263
+ settled = true;
264
+ clearTimeout(timeoutId);
265
+ reject(error);
266
+ });
267
+ });
268
+ }
269
+ redactBrowserSpec(value) {
270
+ return value.replace(/:(\/[^:\s]+)/, ':<profile>');
271
+ }
272
+ /** Strip URLs, signed query params, and cookie values from error details. */
273
+ redact(value) {
274
+ return value
275
+ .replace(/https?:\/\/\S+/g, '<redacted-url>')
276
+ .replace(/([?&](?:sig|signature|expire|ei|spc|lsig|n)=)[^&\s]+/gi, '$1<redacted>')
277
+ .replace(/(SID|HSID|SSID|SAPISID|APISID|LOGIN_INFO)\t[^\s]+/g, '$1\t<redacted>');
278
+ }
279
+ }
280
+ exports.CookieKeeper = CookieKeeper;
281
+ //# sourceMappingURL=cookie-keeper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie-keeper.js","sourceRoot":"","sources":["../src/cookie-keeper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { CookieKeeper, CookieKeeperOptions, RefreshContext, RefreshResult, RefreshStartEvent, RefreshSuccessEvent, RefreshFailureEvent, RefreshSkippedEvent, SpawnedProcess, SpawnFunction, } from './cookie-keeper';
2
+ export { parseDuration, checkFreshness, FreshnessResult } from './cli-support';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkFreshness = exports.parseDuration = exports.CookieKeeper = void 0;
4
+ var cookie_keeper_1 = require("./cookie-keeper");
5
+ Object.defineProperty(exports, "CookieKeeper", { enumerable: true, get: function () { return cookie_keeper_1.CookieKeeper; } });
6
+ var cli_support_1 = require("./cli-support");
7
+ Object.defineProperty(exports, "parseDuration", { enumerable: true, get: function () { return cli_support_1.parseDuration; } });
8
+ Object.defineProperty(exports, "checkFreshness", { enumerable: true, get: function () { return cli_support_1.checkFreshness; } });
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAWyB;AAVvB,6GAAA,YAAY,OAAA;AAYd,6CAA+E;AAAtE,4GAAA,aAAa,OAAA;AAAE,6GAAA,cAAc,OAAA"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "ytdlp-cookie-keeper",
3
+ "version": "0.1.0",
4
+ "description": "Keep your yt-dlp YouTube cookies fresh on a headless server: export from a persistent browser profile, validate against real videos, atomically swap — never clobber a working cookie file with a broken one.",
5
+ "license": "MIT",
6
+ "author": "Bryan Kuang",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "ytdlp-cookie-keeper": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "test": "jest",
20
+ "prepublishOnly": "npm test && npm run build"
21
+ },
22
+ "keywords": [
23
+ "yt-dlp",
24
+ "ytdlp",
25
+ "youtube",
26
+ "cookies",
27
+ "cookies-from-browser",
28
+ "headless",
29
+ "bot-detection",
30
+ "sign-in-to-confirm"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@jest/globals": "^29.7.0",
37
+ "@types/node": "^20.12.12",
38
+ "jest": "^29.7.0",
39
+ "ts-jest": "^29.1.5",
40
+ "typescript": "^5.4.5"
41
+ }
42
+ }