mirror of
https://github.com/imputnet/cobalt.git
synced 2025-06-28 09:28:29 +00:00
youtube全部通过oauth下载
This commit is contained in:
parent
d648b69222
commit
f464e62474
108
YOUTUBE_AUTH_SETUP.md
Normal file
108
YOUTUBE_AUTH_SETUP.md
Normal 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
20
api/dev-start.js
Normal 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);
|
||||||
|
});
|
@ -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
38
api/setup-youtube-auth.js
Normal 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);
|
||||||
|
});
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
console.log(`======> [cloneInnertube] Starting Innertube creation, useSession: ${useSession}`);
|
||||||
|
|
||||||
const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date();
|
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();
|
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 {}
|
||||||
|
@ -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
|
||||||
|
@ -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
51
api/verify-cookies.js
Normal 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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user