From f464e62474673715e742a5696c1c82ea1e6c460b Mon Sep 17 00:00:00 2001 From: celebrateyang Date: Thu, 12 Jun 2025 15:54:50 +0800 Subject: [PATCH] =?UTF-8?q?youtube=E5=85=A8=E9=83=A8=E9=80=9A=E8=BF=87oaut?= =?UTF-8?q?h=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YOUTUBE_AUTH_SETUP.md | 108 +++++++++++++++++++++++++ api/dev-start.js | 20 +++++ api/package.json | 6 +- api/setup-youtube-auth.js | 38 +++++++++ api/src/processing/cookie/manager.js | 15 +++- api/src/processing/services/youtube.js | 54 +++++++++++-- api/src/stream/internal.js | 32 +++++--- api/src/stream/shared.js | 30 ++++++- api/verify-cookies.js | 51 ++++++++++++ 9 files changed, 333 insertions(+), 21 deletions(-) create mode 100644 YOUTUBE_AUTH_SETUP.md create mode 100644 api/dev-start.js create mode 100644 api/setup-youtube-auth.js create mode 100644 api/verify-cookies.js diff --git a/YOUTUBE_AUTH_SETUP.md b/YOUTUBE_AUTH_SETUP.md new file mode 100644 index 00000000..3b3078e7 --- /dev/null +++ b/YOUTUBE_AUTH_SETUP.md @@ -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配置管理,你不需要关心生产环境的配置。 diff --git a/api/dev-start.js b/api/dev-start.js new file mode 100644 index 00000000..c30c8bd8 --- /dev/null +++ b/api/dev-start.js @@ -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); +}); diff --git a/api/package.json b/api/package.json index 9638712c..903c8dd2 100644 --- a/api/package.json +++ b/api/package.json @@ -7,9 +7,11 @@ "type": "module", "engines": { "node": ">=18" - }, - "scripts": { + }, "scripts": { "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", "token:jwt": "node src/util/generate-jwt-secret" }, diff --git a/api/setup-youtube-auth.js b/api/setup-youtube-auth.js new file mode 100644 index 00000000..ddd2c8dc --- /dev/null +++ b/api/setup-youtube-auth.js @@ -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=; ig_did=; csrftoken=; ds_user_id=; sessionid="], + "instagram_bearer": ["token=", "token=IGT:2:"], + "reddit": ["client_id=; client_secret=; refresh_token="], + "twitter": ["auth_token=; ct0="], + "youtube_oauth": ["access_token=; refresh_token=; expires_in="] + }; + 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); +}); diff --git a/api/src/processing/cookie/manager.js b/api/src/processing/cookie/manager.js index 9e23374b..76635899 100644 --- a/api/src/processing/cookie/manager.js +++ b/api/src/processing/cookie/manager.js @@ -13,6 +13,7 @@ const VALID_SERVICES = new Set([ 'reddit', 'twitter', 'youtube', + 'youtube_oauth', ]); const invalidCookies = {}; @@ -102,24 +103,36 @@ export const setup = async (path) => { } export function getCookie(service) { + console.log(`======> [getCookie] Requesting cookie for service: ${service}`); + if (!VALID_SERVICES.has(service)) { console.error( `${Red('[!]')} ${service} not in allowed services list for cookies.` + ' if adding a new cookie type, include it there.' ); + console.log(`======> [getCookie] Service ${service} not in valid services list`); 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); + console.log(`======> [getCookie] Found ${cookies[service].length} cookies for ${service}, using index ${idx}`); const cookie = cookies[service][idx]; if (typeof cookie === 'string') { + console.log(`======> [getCookie] Converting string cookie to Cookie object for ${service}`); cookies[service][idx] = Cookie.fromString(cookie); } 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]; } diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index e184ff1f..b30baeda 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -47,19 +47,37 @@ const clientsWithNoCipher = ['IOS', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDR const videoQualities = [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320]; const cloneInnertube = async (customFetch, useSession) => { + console.log(`======> [cloneInnertube] Starting Innertube creation, useSession: ${useSession}`); + const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date(); + console.log(`======> [cloneInnertube] Should refresh player: ${shouldRefreshPlayer}`); - const rawCookie = getCookie('youtube'); + // 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 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(); + console.log(`======> [cloneInnertube] Session tokens available: ${!!sessionTokens}`); + const retrieve_player = Boolean(sessionTokens || cookie); + console.log(`======> [cloneInnertube] Will retrieve player: ${retrieve_player}`); if (useSession && env.ytSessionServer && !sessionTokens?.potoken) { + console.log(`======> [cloneInnertube] Throwing no_session_tokens error`); throw "no_session_tokens"; } if (!innertube || shouldRefreshPlayer) { + console.log(`======> [cloneInnertube] Creating new Innertube instance with cookie authentication`); innertube = await Innertube.create({ fetch: customFetch, retrieve_player, @@ -68,6 +86,7 @@ const cloneInnertube = async (customFetch, useSession) => { visitor_data: useSession ? sessionTokens?.visitor_data : undefined, }); lastRefreshedAt = +new Date(); + console.log(`======> [cloneInnertube] Innertube instance created successfully`); } const session = new Session( @@ -88,18 +107,34 @@ const cloneInnertube = async (customFetch, useSession) => { } 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); + console.log(`======> [youtube] Processing with quality: ${quality}`); let useHLS = o.youtubeHLS; 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. if (useHLS && o.format === "av1") { useHLS = false; - } - - if (useHLS) { + console.log(`======> [youtube] Disabled HLS due to av1 format`); + } if (useHLS) { innertubeClient = "IOS"; + console.log(`======> [youtube] Set client to IOS for HLS`); } // iOS client doesn't have adaptive formats of resolution >1080p, @@ -114,14 +149,14 @@ export default async function (o) { || (quality > 1080 && o.format !== "vp9") ) ) - ); - - if (useSession) { + ); if (useSession) { innertubeClient = env.ytSessionInnertubeClient || "WEB_EMBEDDED"; + console.log(`======> [youtube] Using session client: ${innertubeClient}`); } let yt; try { + console.log(`======> [youtube] Creating Innertube instance with authentication`); yt = await cloneInnertube( (input, init) => fetch(input, { ...init, @@ -129,7 +164,9 @@ export default async function (o) { }), useSession ); + console.log(`======> [youtube] Innertube instance created successfully with authentication`); } catch (e) { + console.log(`======> [youtube] Innertube creation failed: ${e}`); if (e === "no_session_tokens") { return { error: "youtube.no_session_tokens" }; } else if (e.message?.endsWith("decipher algorithm")) { @@ -141,8 +178,11 @@ export default async function (o) { let info; try { + console.log(`======> [youtube] Getting basic video info for ID: ${o.id} with client: ${innertubeClient}`); info = await yt.getBasicInfo(o.id, innertubeClient); + console.log(`======> [youtube] Successfully retrieved video info with authentication`); } catch (e) { + console.log(`======> [youtube] Failed to get video info: ${e.message}`); if (e?.info) { let errorInfo; try { errorInfo = JSON.parse(e?.info); } catch {} diff --git a/api/src/stream/internal.js b/api/src/stream/internal.js index a7b831c5..b7863646 100644 --- a/api/src/stream/internal.js +++ b/api/src/stream/internal.js @@ -7,8 +7,8 @@ const CHUNK_SIZE = BigInt(8e6); // 8 MB const min = (a, b) => a < b ? a : b; async function* readChunks(streamInfo, size) { - let read = 0n, chunksSinceTransplant = 0; - console.log(`[readChunks] Starting chunk download - Total size: ${size}, URL: ${streamInfo.url}`); + let read = 0n, chunksSinceTransplant = 0; console.log(`[readChunks] Starting chunk download - Total size: ${size}, URL: ${streamInfo.url}`); + console.log(`======> [readChunks] YouTube chunk download with authentication started`); while (read < size) { if (streamInfo.controller.signal.aborted) { @@ -20,19 +20,26 @@ async function* readChunks(streamInfo, size) { const rangeEnd = read + CHUNK_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, { - headers: { - ...getHeaders('youtube'), - Range: `bytes=${read}-${read + CHUNK_SIZE}` - }, + headers, dispatcher: streamInfo.dispatcher, signal: streamInfo.controller.signal, 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; 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 { // Import YouTube service dynamically const handler = await import(`../processing/services/youtube.js`); @@ -49,6 +56,7 @@ async function* readChunks(streamInfo, size) { error: response.error, type: response.type }); + console.log(`======> [readChunks] Fresh authenticated API call result:`, { hasUrls: !!response.urls, error: response.error }); if (response.urls) { 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] YouTube stream processing initiated with authentication`); try { let req, attempts = 3; console.log(`[handleYoutubeStream] Starting HEAD request with ${attempts} attempts`); + console.log(`======> [handleYoutubeStream] Using authenticated headers for HEAD request`); while (attempts--) { + const headers = getHeaders('youtube'); + console.log(`======> [handleYoutubeStream] HEAD request headers prepared with auth: ${!!headers.Cookie}`); + req = await fetch(streamInfo.url, { - headers: getHeaders('youtube'), + headers, method: 'HEAD', dispatcher: streamInfo.dispatcher, signal }); 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})`); try { // Import YouTube service dynamically diff --git a/api/src/stream/shared.js b/api/src/stream/shared.js index 714d5055..2d46dee1 100644 --- a/api/src/stream/shared.js +++ b/api/src/stream/shared.js @@ -1,5 +1,6 @@ import { genericUserAgent } from "../config.js"; import { vkClientAgent } from "../processing/services/vk.js"; +import { getCookie } from "../processing/cookie/manager.js"; import { getInternalTunnelFromURL } from "./manage.js"; import { probeInternalTunnel } from "./internal.js"; @@ -44,9 +45,34 @@ export function closeResponse(res) { } export function getHeaders(service) { + console.log(`======> [getHeaders] Getting headers for service: ${service}`); + // Converting all header values to strings - return Object.entries({ ...defaultHeaders, ...serviceHeaders[service] }) - .reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {}) + const baseHeaders = Object.entries({ ...defaultHeaders, ...serviceHeaders[service] }) + .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) { diff --git a/api/verify-cookies.js b/api/verify-cookies.js new file mode 100644 index 00000000..f8ad74f8 --- /dev/null +++ b/api/verify-cookies.js @@ -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('')) { + 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); +}