youtube全部通过oauth下载

This commit is contained in:
celebrateyang 2025-06-12 15:54:50 +08:00
parent d648b69222
commit f464e62474
9 changed files with 333 additions and 21 deletions

108
YOUTUBE_AUTH_SETUP.md Normal file
View File

@ -0,0 +1,108 @@
# YouTube认证开发环境配置说明
## 概述
现在所有的YouTube下载都要求使用cookies.json中的认证信息。本指南将帮你在开发环境中配置YouTube OAuth认证。
## 快速开始
### 1. 设置YouTube OAuth认证
```powershell
cd d:\code\cobalt_bam\api
npm run setup-youtube
```
这个命令会:
- ✅ 创建cookies.json文件如果不存在
- ✅ 运行YouTube OAuth token生成器
- ✅ 提供详细的认证步骤
### 2. 验证认证配置
```powershell
npm run verify-cookies
```
这个命令会检查:
- ✅ cookies.json是否存在
- ✅ YouTube认证是否配置正确
- ✅ 认证信息是否有效
### 3. 启动开发服务器
```powershell
npm run dev
```
这个命令会:
- ✅ 自动设置COOKIE_PATH环境变量
- ✅ 强制所有YouTube下载使用认证
- ✅ 提供详细的认证日志
## 认证日志说明
所有关键的认证调用都会有特殊的日志标记,以`======>`开头:
```
======> [getCookie] Requesting cookie for service: youtube_oauth
======> [getCookie] Found 1 cookies for youtube_oauth, using index 0
======> [cloneInnertube] Starting Innertube creation, useSession: false
======> [cloneInnertube] Cookie available: true
======> [youtube] Starting YouTube video processing for URL: https://youtube.com/watch?v=...
======> [youtube] Cookie availability check - OAuth: true, Regular: false, Has any: true
======> [getHeaders] Getting headers for service: youtube
======> [getHeaders] YouTube service detected, checking for authentication cookies
======> [getHeaders] Added authentication cookie for YouTube: access_token=ya29...
```
## 故障排除
### 问题1: "No YouTube authentication cookies found!"
**解决方案:**
```powershell
npm run setup-youtube
# 按照提示完成OAuth认证
# 将生成的token添加到cookies.json
npm run verify-cookies
```
### 问题2: "youtube.auth_required"错误
这意味着没有找到有效的YouTube认证。检查
1. cookies.json文件存在
2. youtube_oauth字段有有效的token
3. token没有过期
### 问题3: 403错误持续出现
可能的原因:
1. OAuth token已过期
2. Google账户被限制
3. 需要重新认证
**解决方案:**
```powershell
# 重新生成token
npm run setup-youtube
```
## 文件结构
```
d:\code\cobalt_bam\
├── cookies.json # 认证配置文件
├── api/
│ ├── dev-start.js # 开发环境启动脚本
│ ├── setup-youtube-auth.js # YouTube OAuth设置脚本
│ ├── verify-cookies.js # Cookie验证脚本
│ └── src/
│ ├── cookies.json # API目录下的cookies生产用
│ └── ...
```
## 安全注意事项
⚠️ **重要**:
- 不要提交包含真实token的cookies.json到Git
- 不要在公共场所分享你的OAuth token
- 定期更新和轮换认证token
- 使用专门的测试Google账户不要使用个人账户
## 生产环境部署
生产环境中cookies.json的路径通过deployment配置管理你不需要关心生产环境的配置。

20
api/dev-start.js Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
// 开发环境启动脚本 - 设置cookies.json路径
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cookiesPath = resolve(__dirname, '../cookies.json');
// 设置环境变量
process.env.COOKIE_PATH = cookiesPath;
console.log(`======> [dev-start] Setting COOKIE_PATH to: ${cookiesPath}`);
console.log(`======> [dev-start] YouTube authentication will be required for all downloads`);
// 导入并启动主应用
import('./src/cobalt.js').catch(err => {
console.error('Failed to start application:', err);
process.exit(1);
});

View File

@ -7,9 +7,11 @@
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, }, "scripts": {
"scripts": {
"start": "node src/cobalt", "start": "node src/cobalt",
"dev": "node dev-start.js",
"setup-youtube": "node setup-youtube-auth.js",
"verify-cookies": "node verify-cookies.js",
"test": "node src/util/test", "test": "node src/util/test",
"token:jwt": "node src/util/generate-jwt-secret" "token:jwt": "node src/util/generate-jwt-secret"
}, },

38
api/setup-youtube-auth.js Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env node
// 生成YouTube OAuth token并自动添加到cookies.json
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cookiesPath = resolve(__dirname, '../cookies.json');
console.log(`======> [setup-youtube-auth] Starting YouTube OAuth setup`);
console.log(`======> [setup-youtube-auth] Cookies file: ${cookiesPath}`);
// 检查cookies.json是否存在
if (!existsSync(cookiesPath)) {
console.log(`======> [setup-youtube-auth] Creating new cookies.json file`);
const defaultCookies = {
"instagram": ["mid=<replace>; ig_did=<with>; csrftoken=<your>; ds_user_id=<own>; sessionid=<cookies>"],
"instagram_bearer": ["token=<token_with_no_bearer_in_front>", "token=IGT:2:<looks_like_this>"],
"reddit": ["client_id=<replace_this>; client_secret=<replace_this>; refresh_token=<replace_this>"],
"twitter": ["auth_token=<replace_this>; ct0=<replace_this>"],
"youtube_oauth": ["access_token=<your_oauth_token>; refresh_token=<your_refresh_token>; expires_in=<expiry>"]
};
writeFileSync(cookiesPath, JSON.stringify(defaultCookies, null, 2));
}
console.log(`======> [setup-youtube-auth] Running YouTube token generator...`);
console.log(`======> [setup-youtube-auth] IMPORTANT: Please follow the authentication instructions carefully`);
// 导入并运行YouTube token生成器
import('./src/util/generate-youtube-tokens.js').then(() => {
console.log(`======> [setup-youtube-auth] OAuth token generation completed`);
console.log(`======> [setup-youtube-auth] Please copy the generated token to ${cookiesPath}`);
console.log(`======> [setup-youtube-auth] Replace the youtube_oauth array content with the generated token`);
}).catch(err => {
console.error(`======> [setup-youtube-auth] Failed to generate token:`, err);
process.exit(1);
});

View File

@ -13,6 +13,7 @@ const VALID_SERVICES = new Set([
'reddit', 'reddit',
'twitter', 'twitter',
'youtube', 'youtube',
'youtube_oauth',
]); ]);
const invalidCookies = {}; const invalidCookies = {};
@ -102,24 +103,36 @@ export const setup = async (path) => {
} }
export function getCookie(service) { export function getCookie(service) {
console.log(`======> [getCookie] Requesting cookie for service: ${service}`);
if (!VALID_SERVICES.has(service)) { if (!VALID_SERVICES.has(service)) {
console.error( console.error(
`${Red('[!]')} ${service} not in allowed services list for cookies.` `${Red('[!]')} ${service} not in allowed services list for cookies.`
+ ' if adding a new cookie type, include it there.' + ' if adding a new cookie type, include it there.'
); );
console.log(`======> [getCookie] Service ${service} not in valid services list`);
return; return;
} }
if (!cookies[service] || !cookies[service].length) return; if (!cookies[service] || !cookies[service].length) {
console.log(`======> [getCookie] No cookies found for service: ${service}`);
return;
}
const idx = Math.floor(Math.random() * cookies[service].length); const idx = Math.floor(Math.random() * cookies[service].length);
console.log(`======> [getCookie] Found ${cookies[service].length} cookies for ${service}, using index ${idx}`);
const cookie = cookies[service][idx]; const cookie = cookies[service][idx];
if (typeof cookie === 'string') { if (typeof cookie === 'string') {
console.log(`======> [getCookie] Converting string cookie to Cookie object for ${service}`);
cookies[service][idx] = Cookie.fromString(cookie); cookies[service][idx] = Cookie.fromString(cookie);
} }
cookies[service][idx].meta = { service, idx }; cookies[service][idx].meta = { service, idx };
const cookieStr = cookies[service][idx].toString();
console.log(`======> [getCookie] Returning cookie for ${service}: ${cookieStr.substring(0, 50)}${cookieStr.length > 50 ? '...' : ''}`);
return cookies[service][idx]; return cookies[service][idx];
} }

View File

@ -47,19 +47,37 @@ const clientsWithNoCipher = ['IOS', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDR
const videoQualities = [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320]; const videoQualities = [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320];
const cloneInnertube = async (customFetch, useSession) => { const cloneInnertube = async (customFetch, useSession) => {
const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date(); console.log(`======> [cloneInnertube] Starting Innertube creation, useSession: ${useSession}`);
const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date();
console.log(`======> [cloneInnertube] Should refresh player: ${shouldRefreshPlayer}`);
// First try youtube_oauth cookies
let rawCookie = getCookie('youtube_oauth');
if (!rawCookie) {
console.log(`======> [cloneInnertube] No youtube_oauth cookies found, trying regular youtube cookies`);
rawCookie = getCookie('youtube');
}
const rawCookie = getCookie('youtube');
const cookie = rawCookie?.toString(); const cookie = rawCookie?.toString();
console.log(`======> [cloneInnertube] Cookie available: ${!!cookie}`);
if (cookie) {
console.log(`======> [cloneInnertube] Cookie length: ${cookie.length}, preview: ${cookie.substring(0, 100)}...`);
}
const sessionTokens = getYouTubeSession(); const sessionTokens = getYouTubeSession();
console.log(`======> [cloneInnertube] Session tokens available: ${!!sessionTokens}`);
const retrieve_player = Boolean(sessionTokens || cookie); const retrieve_player = Boolean(sessionTokens || cookie);
console.log(`======> [cloneInnertube] Will retrieve player: ${retrieve_player}`);
if (useSession && env.ytSessionServer && !sessionTokens?.potoken) { if (useSession && env.ytSessionServer && !sessionTokens?.potoken) {
console.log(`======> [cloneInnertube] Throwing no_session_tokens error`);
throw "no_session_tokens"; throw "no_session_tokens";
} }
if (!innertube || shouldRefreshPlayer) { if (!innertube || shouldRefreshPlayer) {
console.log(`======> [cloneInnertube] Creating new Innertube instance with cookie authentication`);
innertube = await Innertube.create({ innertube = await Innertube.create({
fetch: customFetch, fetch: customFetch,
retrieve_player, retrieve_player,
@ -68,6 +86,7 @@ const cloneInnertube = async (customFetch, useSession) => {
visitor_data: useSession ? sessionTokens?.visitor_data : undefined, visitor_data: useSession ? sessionTokens?.visitor_data : undefined,
}); });
lastRefreshedAt = +new Date(); lastRefreshedAt = +new Date();
console.log(`======> [cloneInnertube] Innertube instance created successfully`);
} }
const session = new Session( const session = new Session(
@ -88,18 +107,34 @@ const cloneInnertube = async (customFetch, useSession) => {
} }
export default async function (o) { export default async function (o) {
console.log(`======> [youtube] Starting YouTube video processing for URL: ${o.url || o.id}`);
// Check if cookies are available before proceeding
const oauthCookie = getCookie('youtube_oauth');
const regularCookie = getCookie('youtube');
const hasCookies = !!(oauthCookie || regularCookie);
console.log(`======> [youtube] Cookie availability check - OAuth: ${!!oauthCookie}, Regular: ${!!regularCookie}, Has any: ${hasCookies}`);
if (!hasCookies) {
console.log(`======> [youtube] ERROR: No YouTube authentication cookies found! All YouTube downloads require authentication.`);
return { error: "youtube.auth_required" };
}
const quality = o.quality === "max" ? 9000 : Number(o.quality); const quality = o.quality === "max" ? 9000 : Number(o.quality);
console.log(`======> [youtube] Processing with quality: ${quality}`);
let useHLS = o.youtubeHLS; let useHLS = o.youtubeHLS;
let innertubeClient = o.innertubeClient || env.customInnertubeClient || "IOS"; let innertubeClient = o.innertubeClient || env.customInnertubeClient || "IOS";
console.log(`======> [youtube] Using HLS: ${useHLS}, Client: ${innertubeClient}`);
// HLS playlists from the iOS client don't contain the av1 video format. // HLS playlists from the iOS client don't contain the av1 video format.
if (useHLS && o.format === "av1") { if (useHLS && o.format === "av1") {
useHLS = false; useHLS = false;
} console.log(`======> [youtube] Disabled HLS due to av1 format`);
} if (useHLS) {
if (useHLS) {
innertubeClient = "IOS"; innertubeClient = "IOS";
console.log(`======> [youtube] Set client to IOS for HLS`);
} }
// iOS client doesn't have adaptive formats of resolution >1080p, // iOS client doesn't have adaptive formats of resolution >1080p,
@ -114,14 +149,14 @@ export default async function (o) {
|| (quality > 1080 && o.format !== "vp9") || (quality > 1080 && o.format !== "vp9")
) )
) )
); ); if (useSession) {
if (useSession) {
innertubeClient = env.ytSessionInnertubeClient || "WEB_EMBEDDED"; innertubeClient = env.ytSessionInnertubeClient || "WEB_EMBEDDED";
console.log(`======> [youtube] Using session client: ${innertubeClient}`);
} }
let yt; let yt;
try { try {
console.log(`======> [youtube] Creating Innertube instance with authentication`);
yt = await cloneInnertube( yt = await cloneInnertube(
(input, init) => fetch(input, { (input, init) => fetch(input, {
...init, ...init,
@ -129,7 +164,9 @@ export default async function (o) {
}), }),
useSession useSession
); );
console.log(`======> [youtube] Innertube instance created successfully with authentication`);
} catch (e) { } catch (e) {
console.log(`======> [youtube] Innertube creation failed: ${e}`);
if (e === "no_session_tokens") { if (e === "no_session_tokens") {
return { error: "youtube.no_session_tokens" }; return { error: "youtube.no_session_tokens" };
} else if (e.message?.endsWith("decipher algorithm")) { } else if (e.message?.endsWith("decipher algorithm")) {
@ -141,8 +178,11 @@ export default async function (o) {
let info; let info;
try { try {
console.log(`======> [youtube] Getting basic video info for ID: ${o.id} with client: ${innertubeClient}`);
info = await yt.getBasicInfo(o.id, innertubeClient); info = await yt.getBasicInfo(o.id, innertubeClient);
console.log(`======> [youtube] Successfully retrieved video info with authentication`);
} catch (e) { } catch (e) {
console.log(`======> [youtube] Failed to get video info: ${e.message}`);
if (e?.info) { if (e?.info) {
let errorInfo; let errorInfo;
try { errorInfo = JSON.parse(e?.info); } catch {} try { errorInfo = JSON.parse(e?.info); } catch {}

View File

@ -7,8 +7,8 @@ const CHUNK_SIZE = BigInt(8e6); // 8 MB
const min = (a, b) => a < b ? a : b; const min = (a, b) => a < b ? a : b;
async function* readChunks(streamInfo, size) { async function* readChunks(streamInfo, size) {
let read = 0n, chunksSinceTransplant = 0; let read = 0n, chunksSinceTransplant = 0; console.log(`[readChunks] Starting chunk download - Total size: ${size}, URL: ${streamInfo.url}`);
console.log(`[readChunks] Starting chunk download - Total size: ${size}, URL: ${streamInfo.url}`); console.log(`======> [readChunks] YouTube chunk download with authentication started`);
while (read < size) { while (read < size) {
if (streamInfo.controller.signal.aborted) { if (streamInfo.controller.signal.aborted) {
@ -20,19 +20,26 @@ async function* readChunks(streamInfo, size) {
const rangeEnd = read + CHUNK_SIZE; const rangeEnd = read + CHUNK_SIZE;
console.log(`[readChunks] Requesting chunk: bytes=${rangeStart}-${rangeEnd}, read=${read}/${size}`); console.log(`[readChunks] Requesting chunk: bytes=${rangeStart}-${rangeEnd}, read=${read}/${size}`);
const headers = {
...getHeaders('youtube'),
Range: `bytes=${read}-${read + CHUNK_SIZE}`
};
console.log(`======> [readChunks] Chunk request using authenticated headers: ${!!headers.Cookie}`);
const chunk = await request(streamInfo.url, { const chunk = await request(streamInfo.url, {
headers: { headers,
...getHeaders('youtube'),
Range: `bytes=${read}-${read + CHUNK_SIZE}`
},
dispatcher: streamInfo.dispatcher, dispatcher: streamInfo.dispatcher,
signal: streamInfo.controller.signal, signal: streamInfo.controller.signal,
maxRedirections: 4 maxRedirections: 4
}); });
console.log(`[readChunks] Chunk response: status=${chunk.statusCode}, content-length=${chunk.headers['content-length']}`); if (chunk.statusCode === 403 && chunksSinceTransplant >= 3 && streamInfo.originalRequest) { console.log(`[readChunks] Chunk response: status=${chunk.statusCode}, content-length=${chunk.headers['content-length']}`);
console.log(`======> [readChunks] Authenticated chunk request result: status=${chunk.statusCode}`);
if (chunk.statusCode === 403 && chunksSinceTransplant >= 3 && streamInfo.originalRequest) {
chunksSinceTransplant = 0; chunksSinceTransplant = 0;
console.log(`[readChunks] 403 error after 3+ chunks, attempting fresh YouTube API call`); console.log(`[readChunks] 403 error after 3+ chunks, attempting fresh YouTube API call`);
console.log(`======> [readChunks] 403 error detected, attempting fresh authenticated API call`);
try { try {
// Import YouTube service dynamically // Import YouTube service dynamically
const handler = await import(`../processing/services/youtube.js`); const handler = await import(`../processing/services/youtube.js`);
@ -49,6 +56,7 @@ async function* readChunks(streamInfo, size) {
error: response.error, error: response.error,
type: response.type type: response.type
}); });
console.log(`======> [readChunks] Fresh authenticated API call result:`, { hasUrls: !!response.urls, error: response.error });
if (response.urls) { if (response.urls) {
response.urls = [response.urls].flat(); response.urls = [response.urls].flat();
@ -124,22 +132,28 @@ async function handleYoutubeStream(streamInfo, res) {
}; };
console.log(`[handleYoutubeStream] Starting YouTube stream for URL: ${streamInfo.url}`); console.log(`[handleYoutubeStream] Starting YouTube stream for URL: ${streamInfo.url}`);
console.log(`======> [handleYoutubeStream] YouTube stream processing initiated with authentication`);
try { try {
let req, attempts = 3; let req, attempts = 3;
console.log(`[handleYoutubeStream] Starting HEAD request with ${attempts} attempts`); console.log(`[handleYoutubeStream] Starting HEAD request with ${attempts} attempts`);
console.log(`======> [handleYoutubeStream] Using authenticated headers for HEAD request`);
while (attempts--) { while (attempts--) {
const headers = getHeaders('youtube');
console.log(`======> [handleYoutubeStream] HEAD request headers prepared with auth: ${!!headers.Cookie}`);
req = await fetch(streamInfo.url, { req = await fetch(streamInfo.url, {
headers: getHeaders('youtube'), headers,
method: 'HEAD', method: 'HEAD',
dispatcher: streamInfo.dispatcher, dispatcher: streamInfo.dispatcher,
signal signal
}); });
console.log(`[handleYoutubeStream] HEAD response: status=${req.status}, url=${req.url}`); console.log(`[handleYoutubeStream] HEAD response: status=${req.status}, url=${req.url}`);
console.log(`======> [handleYoutubeStream] Authenticated HEAD request completed: status=${req.status}`);
streamInfo.url = req.url; if (req.status === 403 && streamInfo.originalRequest && attempts > 0) { streamInfo.url = req.url;if (req.status === 403 && streamInfo.originalRequest && attempts > 0) {
console.log(`[handleYoutubeStream] Got 403, attempting fresh YouTube API call (attempts left: ${attempts})`); console.log(`[handleYoutubeStream] Got 403, attempting fresh YouTube API call (attempts left: ${attempts})`);
try { try {
// Import YouTube service dynamically // Import YouTube service dynamically

View File

@ -1,5 +1,6 @@
import { genericUserAgent } from "../config.js"; import { genericUserAgent } from "../config.js";
import { vkClientAgent } from "../processing/services/vk.js"; import { vkClientAgent } from "../processing/services/vk.js";
import { getCookie } from "../processing/cookie/manager.js";
import { getInternalTunnelFromURL } from "./manage.js"; import { getInternalTunnelFromURL } from "./manage.js";
import { probeInternalTunnel } from "./internal.js"; import { probeInternalTunnel } from "./internal.js";
@ -44,9 +45,34 @@ export function closeResponse(res) {
} }
export function getHeaders(service) { export function getHeaders(service) {
console.log(`======> [getHeaders] Getting headers for service: ${service}`);
// Converting all header values to strings // Converting all header values to strings
return Object.entries({ ...defaultHeaders, ...serviceHeaders[service] }) const baseHeaders = Object.entries({ ...defaultHeaders, ...serviceHeaders[service] })
.reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {}) .reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {});
// For YouTube, always try to add authentication cookies
if (service === 'youtube') {
console.log(`======> [getHeaders] YouTube service detected, checking for authentication cookies`);
// First try OAuth cookies, then regular cookies
let cookie = getCookie('youtube_oauth');
if (!cookie) {
console.log(`======> [getHeaders] No OAuth cookies found, trying regular youtube cookies`);
cookie = getCookie('youtube');
}
if (cookie) {
const cookieStr = cookie.toString();
baseHeaders.Cookie = cookieStr;
console.log(`======> [getHeaders] Added authentication cookie for YouTube: ${cookieStr.substring(0, 50)}${cookieStr.length > 50 ? '...' : ''}`);
} else {
console.log(`======> [getHeaders] WARNING: No YouTube authentication cookies found! Requests may fail.`);
}
}
console.log(`======> [getHeaders] Final headers for ${service}:`, Object.keys(baseHeaders));
return baseHeaders;
} }
export function pipe(from, to, done) { export function pipe(from, to, done) {

51
api/verify-cookies.js Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env node
// Cookie验证工具 - 检查cookies.json的有效性
import { readFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cookiesPath = process.argv[2] || resolve(__dirname, '../cookies.json');
console.log(`======> [verify-cookies] Verifying cookies at: ${cookiesPath}`);
if (!existsSync(cookiesPath)) {
console.log(`======> [verify-cookies] ❌ cookies.json not found at: ${cookiesPath}`);
process.exit(1);
}
try {
const cookies = JSON.parse(readFileSync(cookiesPath, 'utf8'));
console.log(`======> [verify-cookies] ✅ cookies.json loaded successfully`);
// 检查YouTube相关cookies
const hasYouTubeOAuth = cookies.youtube_oauth && cookies.youtube_oauth.length > 0;
const hasYouTubeRegular = cookies.youtube && cookies.youtube.length > 0;
console.log(`======> [verify-cookies] YouTube OAuth cookies: ${hasYouTubeOAuth ? '✅ Found' : '❌ Missing'}`);
console.log(`======> [verify-cookies] YouTube regular cookies: ${hasYouTubeRegular ? '✅ Found' : '❌ Missing'}`);
if (hasYouTubeOAuth) {
const oauthCookie = cookies.youtube_oauth[0];
if (oauthCookie.includes('<your_oauth_token>')) {
console.log(`======> [verify-cookies] ⚠️ YouTube OAuth contains placeholder values`);
console.log(`======> [verify-cookies] Please run: npm run setup-youtube`);
} else {
console.log(`======> [verify-cookies] ✅ YouTube OAuth appears to be configured`);
}
}
if (!hasYouTubeOAuth && !hasYouTubeRegular) {
console.log(`======> [verify-cookies] ❌ No YouTube authentication found!`);
console.log(`======> [verify-cookies] YouTube downloads will fail without authentication`);
console.log(`======> [verify-cookies] Please run: npm run setup-youtube`);
process.exit(1);
}
console.log(`======> [verify-cookies] ✅ Cookie verification completed`);
} catch (error) {
console.log(`======> [verify-cookies] ❌ Error reading cookies.json: ${error.message}`);
process.exit(1);
}