243 lines
7.3 KiB
JavaScript
243 lines
7.3 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const sqlite3 = require('sqlite3').verbose();
|
|
const { exec } = require('child_process');
|
|
const util = require('util');
|
|
const execPromise = util.promisify(exec);
|
|
|
|
// 数据库连接
|
|
const db = new sqlite3.Database('goguryeo_video.db');
|
|
|
|
// 视频文件目录
|
|
const VIDEO_DIR = './video';
|
|
const UPLOAD_VIDEO_DIR = './uploads/videos';
|
|
const UPLOAD_COVER_DIR = './uploads/covers';
|
|
|
|
// 确保上传目录存在
|
|
if (!fs.existsSync(UPLOAD_VIDEO_DIR)) {
|
|
fs.mkdirSync(UPLOAD_VIDEO_DIR, { recursive: true });
|
|
}
|
|
if (!fs.existsSync(UPLOAD_COVER_DIR)) {
|
|
fs.mkdirSync(UPLOAD_COVER_DIR, { recursive: true });
|
|
}
|
|
|
|
// 动物分类映射
|
|
const animalCategoryMap = {
|
|
'羊': { category: 5, tags: ['萌宠', '小羊', '可爱', '动物'] },
|
|
'老虎': { category: 5, tags: ['萌宠', '老虎', '动物园', '可爱'] },
|
|
'荷兰猪': { category: 5, tags: ['萌宠', '荷兰猪', '可爱'] },
|
|
'兔子': { category: 5, tags: ['萌宠', '兔子', '可爱'] },
|
|
'企鹅': { category: 5, tags: ['萌宠', '企鹅', '可爱'] },
|
|
'牛': { category: 5, tags: ['动物', '小牛', '农村生活', '可爱'] },
|
|
'猴子': { category: 5, tags: ['猴子', '可爱', '动物'] },
|
|
'马': { category: 5, tags: ['萌宠', '矮马', '可爱'] },
|
|
'水獭': { category: 5, tags: ['水獭', '萌宠', '治愈', '可爱'] },
|
|
'鸟': { category: 5, tags: ['可爱', '治愈', '萌宠'] },
|
|
'狗': { category: 5, tags: ['宠物', '狗子', '萌宠'] },
|
|
'象': { category: 5, tags: ['小象', '萌宠', '可爱'] },
|
|
'猪': { category: 5, tags: ['小香猪', '小猪', '可爱'] },
|
|
'猫': { category: 5, tags: ['小猫咪', '萌宠', '可爱', '治愈'] }
|
|
};
|
|
|
|
// 根据文件名识别动物类型
|
|
function identifyAnimalType(filename) {
|
|
for (const [animal, config] of Object.entries(animalCategoryMap)) {
|
|
if (filename.includes(animal)) {
|
|
return config;
|
|
}
|
|
}
|
|
// 默认分类为民俗风情
|
|
return { category: 5, tags: ['萌宠', '可爱', '动物'] };
|
|
}
|
|
|
|
// 生成唯一文件名
|
|
function generateUniqueFilename(originalName) {
|
|
const timestamp = Date.now();
|
|
const randomStr = Math.random().toString(36).substring(2, 15);
|
|
const ext = path.extname(originalName);
|
|
return `${timestamp}_${randomStr}${ext}`;
|
|
}
|
|
|
|
// 获取视频时长
|
|
async function getVideoDuration(videoPath) {
|
|
try {
|
|
const { stdout } = await execPromise(`ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${videoPath}"`);
|
|
return Math.round(parseFloat(stdout.trim()));
|
|
} catch (error) {
|
|
console.log(`无法获取视频时长: ${videoPath}`);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// 生成视频封面
|
|
async function generateThumbnail(videoPath, outputPath) {
|
|
try {
|
|
await execPromise(`ffmpeg -i "${videoPath}" -ss 00:00:01 -vframes 1 -y "${outputPath}"`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(`生成封面失败: ${videoPath}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 插入视频到数据库
|
|
function insertVideo(videoData) {
|
|
return new Promise((resolve, reject) => {
|
|
const sql = `
|
|
INSERT INTO videos (
|
|
title, description, video_url, file_path, cover_url, cover_image,
|
|
duration, file_size, category, tags, user_id
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`;
|
|
|
|
db.run(sql, [
|
|
videoData.title,
|
|
videoData.description,
|
|
videoData.video_url,
|
|
videoData.file_path,
|
|
videoData.cover_url,
|
|
videoData.cover_image,
|
|
videoData.duration,
|
|
videoData.file_size,
|
|
videoData.category,
|
|
videoData.tags,
|
|
1 // admin user_id
|
|
], function(err) {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(this.lastID);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// 处理单个视频文件
|
|
async function processVideo(filename) {
|
|
try {
|
|
const originalPath = path.join(VIDEO_DIR, filename);
|
|
const stats = fs.statSync(originalPath);
|
|
|
|
// 生成新文件名
|
|
const newVideoName = generateUniqueFilename(filename);
|
|
const newCoverName = newVideoName.replace('.mp4', '_cover.jpg');
|
|
|
|
const newVideoPath = path.join(UPLOAD_VIDEO_DIR, newVideoName);
|
|
const newCoverPath = path.join(UPLOAD_COVER_DIR, newCoverName);
|
|
|
|
// 复制视频文件
|
|
fs.copyFileSync(originalPath, newVideoPath);
|
|
console.log(`复制视频: ${filename} -> ${newVideoName}`);
|
|
|
|
// 生成封面
|
|
const thumbnailGenerated = await generateThumbnail(newVideoPath, newCoverPath);
|
|
|
|
// 获取视频时长
|
|
const duration = await getVideoDuration(newVideoPath);
|
|
|
|
// 识别动物类型和分类
|
|
const animalConfig = identifyAnimalType(filename);
|
|
|
|
// 清理标题(移除文件扩展名和平台标识)
|
|
const title = filename
|
|
.replace('.mp4', '')
|
|
.replace('-快手', '')
|
|
.trim();
|
|
|
|
// 准备视频数据
|
|
const videoData = {
|
|
title: title,
|
|
description: `可爱的动物视频:${title}`,
|
|
video_url: `/uploads/videos/${newVideoName}`,
|
|
file_path: `/uploads/videos/${newVideoName}`,
|
|
cover_url: thumbnailGenerated ? `/uploads/covers/${newCoverName}` : null,
|
|
cover_image: thumbnailGenerated ? `/uploads/covers/${newCoverName}` : null,
|
|
duration: duration,
|
|
file_size: stats.size,
|
|
category: animalConfig.category,
|
|
tags: animalConfig.tags.join(','),
|
|
};
|
|
|
|
// 插入数据库
|
|
const videoId = await insertVideo(videoData);
|
|
console.log(`视频上传成功: ${title} (ID: ${videoId})`);
|
|
|
|
return {
|
|
success: true,
|
|
videoId: videoId,
|
|
title: title,
|
|
category: animalConfig.category
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error(`处理视频失败 ${filename}:`, error.message);
|
|
return {
|
|
success: false,
|
|
filename: filename,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
// 主函数
|
|
async function main() {
|
|
try {
|
|
console.log('开始批量上传动物视频...');
|
|
|
|
// 读取视频目录
|
|
const files = fs.readdirSync(VIDEO_DIR)
|
|
.filter(file => file.endsWith('.mp4'));
|
|
|
|
console.log(`找到 ${files.length} 个视频文件`);
|
|
|
|
const results = [];
|
|
const categoryStats = {};
|
|
|
|
// 处理每个视频文件
|
|
for (const file of files) {
|
|
console.log(`\n处理: ${file}`);
|
|
const result = await processVideo(file);
|
|
results.push(result);
|
|
|
|
if (result.success) {
|
|
const category = result.category;
|
|
categoryStats[category] = (categoryStats[category] || 0) + 1;
|
|
}
|
|
|
|
// 添加延迟避免系统过载
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
// 统计结果
|
|
const successful = results.filter(r => r.success).length;
|
|
const failed = results.filter(r => !r.success).length;
|
|
|
|
console.log('\n=== 上传完成 ===');
|
|
console.log(`成功: ${successful} 个`);
|
|
console.log(`失败: ${failed} 个`);
|
|
|
|
console.log('\n=== 分类统计 ===');
|
|
for (const [categoryId, count] of Object.entries(categoryStats)) {
|
|
console.log(`分类 ${categoryId}: ${count} 个视频`);
|
|
}
|
|
|
|
if (failed > 0) {
|
|
console.log('\n=== 失败的文件 ===');
|
|
results.filter(r => !r.success).forEach(r => {
|
|
console.log(`${r.filename}: ${r.error}`);
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('批量上传失败:', error);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
// 运行脚本
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = { main, processVideo }; |