217 lines
7.4 KiB
JavaScript
217 lines
7.4 KiB
JavaScript
/**
|
||
* 批量转码所有视频素材脚本
|
||
* 用途:一次性任务,将所有视频素材重新转码为新标准(720P + H.265)
|
||
*
|
||
* 使用方法:
|
||
* 1. 确保服务器正在运行
|
||
* 2. 在浏览器中登录管理员账号,获取 session cookie
|
||
* 3. 运行: node scripts/retranscode-all-videos.js
|
||
*
|
||
* 或者直接在管理员页面使用批量转码功能
|
||
*/
|
||
|
||
const https = require('https');
|
||
const http = require('http');
|
||
|
||
// 配置
|
||
const config = {
|
||
baseUrl: process.env.API_BASE_URL || 'http://localhost:3000',
|
||
// 如果需要认证,请设置 session cookie
|
||
// cookie: 'next-auth.session-token=YOUR_SESSION_TOKEN',
|
||
cookie: process.env.ADMIN_SESSION_COOKIE || '',
|
||
batchSize: 1000, // 每批处理数量
|
||
};
|
||
|
||
/**
|
||
* 发送 HTTP 请求
|
||
*/
|
||
function request(options, data) {
|
||
return new Promise((resolve, reject) => {
|
||
const url = new URL(options.url || config.baseUrl + options.path);
|
||
const isHttps = url.protocol === 'https:';
|
||
const client = isHttps ? https : http;
|
||
|
||
const reqOptions = {
|
||
hostname: url.hostname,
|
||
port: url.port || (isHttps ? 443 : 80),
|
||
path: url.pathname + url.search,
|
||
method: options.method || 'GET',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(config.cookie ? { 'Cookie': config.cookie } : {}),
|
||
...(options.headers || {}),
|
||
},
|
||
};
|
||
|
||
const req = client.request(reqOptions, (res) => {
|
||
let body = '';
|
||
res.on('data', (chunk) => { body += chunk; });
|
||
res.on('end', () => {
|
||
try {
|
||
const json = JSON.parse(body);
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
resolve({ status: res.statusCode, data: json });
|
||
} else {
|
||
reject(new Error(`HTTP ${res.statusCode}: ${json.error || body}`));
|
||
}
|
||
} catch (e) {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
resolve({ status: res.statusCode, data: body });
|
||
} else {
|
||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
|
||
if (data) {
|
||
req.write(JSON.stringify(data));
|
||
}
|
||
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 主函数
|
||
*/
|
||
async function main() {
|
||
console.log('==========================================');
|
||
console.log('批量转码所有视频素材');
|
||
console.log('==========================================');
|
||
console.log(`API URL: ${config.baseUrl}`);
|
||
console.log(`批次大小: ${config.batchSize}`);
|
||
console.log('');
|
||
|
||
// 第一步:统计需要转码的素材数量
|
||
console.log('📊 第一步:统计需要转码的素材数量...');
|
||
try {
|
||
const dryRunResponse = await request({
|
||
method: 'POST',
|
||
path: '/api/admin/video-retranscode',
|
||
}, {
|
||
limit: 0, // 0 表示不限制,获取所有匹配的素材
|
||
status: 'any', // 转码所有状态的视频
|
||
dryRun: true,
|
||
});
|
||
|
||
const totalCount = dryRunResponse.data.matched || 0;
|
||
console.log(`✓ 找到 ${totalCount} 个视频素材需要转码`);
|
||
|
||
if (totalCount === 0) {
|
||
console.log('没有找到需要转码的视频素材');
|
||
return;
|
||
}
|
||
|
||
if (dryRunResponse.data.sample && dryRunResponse.data.sample.length > 0) {
|
||
console.log('\n前10个素材预览:');
|
||
dryRunResponse.data.sample.forEach((m, i) => {
|
||
console.log(` ${i + 1}. ID: ${m.id.substring(0, 20)}..., 产品: ${m.productModel || 'N/A'}, 状态: ${m.status || 'null'}`);
|
||
});
|
||
}
|
||
|
||
// 确认执行
|
||
console.log('\n⚠️ 警告:即将开始批量转码所有视频素材');
|
||
console.log(' 转码是异步执行的,会进入队列处理');
|
||
console.log(' 转码完成后,旧的预览文件会自动清理');
|
||
console.log('\n按 Ctrl+C 取消,或等待 5 秒后自动开始...');
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||
|
||
// 第二步:分批执行转码
|
||
console.log('\n🚀 开始批量转码...');
|
||
const totalBatches = Math.ceil(totalCount / config.batchSize);
|
||
console.log(`预计需要 ${totalBatches} 个批次\n`);
|
||
|
||
let totalQueued = 0;
|
||
let totalFailed = 0;
|
||
let processedCount = 0;
|
||
|
||
for (let batch = 1; batch <= totalBatches; batch++) {
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
console.log(`批次 ${batch}/${totalBatches}`);
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
||
try {
|
||
const response = await request({
|
||
method: 'POST',
|
||
path: '/api/admin/video-retranscode',
|
||
}, {
|
||
limit: config.batchSize,
|
||
status: 'any',
|
||
dryRun: false,
|
||
});
|
||
|
||
const queued = response.data.queued || 0;
|
||
const failed = response.data.failed || 0;
|
||
const matched = response.data.matched || 0;
|
||
|
||
totalQueued += queued;
|
||
totalFailed += failed;
|
||
processedCount += matched;
|
||
|
||
console.log(`✓ 批次 ${batch} 完成: 已排队 ${queued} 个,失败 ${failed} 个`);
|
||
|
||
if (response.data.details && response.data.details.length > 0) {
|
||
const failedDetails = response.data.details.filter(d => !d.ok).slice(0, 5);
|
||
if (failedDetails.length > 0) {
|
||
console.log('\n失败详情(前5个):');
|
||
failedDetails.forEach(d => {
|
||
console.log(` - ID: ${d.id.substring(0, 20)}..., 状态: ${d.status}, 错误: ${d.message || 'N/A'}`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 如果这一批匹配的数量少于批次大小,说明已经处理完所有素材
|
||
if (matched < config.batchSize) {
|
||
console.log('\n已处理完所有素材');
|
||
break;
|
||
}
|
||
|
||
// 批次间稍作延迟,避免请求过快
|
||
if (batch < totalBatches) {
|
||
console.log('\n等待 2 秒后继续下一批次...\n');
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`❌ 批次 ${batch} 失败:`, error.message);
|
||
// 继续处理下一批次
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 总结
|
||
console.log('\n==========================================');
|
||
console.log('批量转码完成');
|
||
console.log('==========================================');
|
||
console.log(`✓ 总计已排队: ${totalQueued} 个`);
|
||
if (totalFailed > 0) {
|
||
console.log(`⚠️ 总计失败: ${totalFailed} 个`);
|
||
}
|
||
console.log('\n📝 说明:');
|
||
console.log(' - 转码是异步执行的,会进入队列处理');
|
||
console.log(' - 实际并发数由服务器配置控制(VIDEO_TRANSCODE_MAX_CONCURRENT)');
|
||
console.log(' - 转码成功后,旧的预览文件会自动清理');
|
||
console.log(' - 可以在管理员页面查看转码进度和状态');
|
||
console.log('');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 执行失败:', error.message);
|
||
if (error.message.includes('403') || error.message.includes('无权限')) {
|
||
console.error('\n提示:需要管理员权限。请:');
|
||
console.error(' 1. 在浏览器中登录管理员账号');
|
||
console.error(' 2. 获取 session cookie');
|
||
console.error(' 3. 设置环境变量: ADMIN_SESSION_COOKIE="next-auth.session-token=YOUR_TOKEN"');
|
||
console.error(' 或者直接在管理员页面使用批量转码功能');
|
||
}
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// 运行主函数
|
||
main().catch(console.error);
|
||
|