first commit

This commit is contained in:
2025-09-23 07:35:11 +00:00
commit a5dd3f1335
110 changed files with 46108 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
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 };

262
scripts/batch-upload-videos.cjs Executable file
View File

@@ -0,0 +1,262 @@
/**
* 批量上传视频脚本
* 从指定目录批量上传视频文件到服务器
*/
const fs = require('fs');
const path = require('path');
const FormData = require('form-data');
const axios = require('axios');
const sqlite3 = require('sqlite3').verbose();
// 配置
const VIDEO_SOURCE_DIR = '/www/wwwroot/ggl/video';
const SERVER_URL = 'http://localhost:3001';
const UPLOAD_ENDPOINT = '/api/videos/upload';
const LOGIN_ENDPOINT = '/api/auth/login';
const DB_PATH = path.join(process.cwd(), 'database', 'goguryeo_video.db');
// 测试用户
const TEST_USER = {
username: 'admin',
password: 'admin123'
};
// 全局token
let authToken = null;
// 支持的视频格式
const SUPPORTED_FORMATS = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'];
// 默认专题映射(根据文件名关键词)
const TOPIC_MAPPING = {
'可爱': ['文化艺术', '民俗传统'],
'小': ['文化艺术'],
'动物': ['民俗传统'],
'萌': ['文化艺术'],
'治愈': ['文化艺术'],
'成长': ['文化艺术', '民俗传统']
};
// 登录获取token
async function login() {
try {
console.log('正在登录获取认证token...');
const response = await axios.post(`${SERVER_URL}${LOGIN_ENDPOINT}`, TEST_USER);
if (response.data.success && response.data.data && response.data.data.token) {
authToken = response.data.data.token;
console.log('✅ 登录成功获取到token');
return true;
} else {
console.error('❌ 登录失败:', response.data.message || '未知错误');
return false;
}
} catch (error) {
console.error('❌ 登录请求异常:', error.message);
return false;
}
}
// 获取专题ID
async function getTopicIds() {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
reject(err);
return;
}
});
db.all('SELECT id, name FROM topics WHERE status = "active"', (err, rows) => {
if (err) {
reject(err);
} else {
const topicMap = {};
rows.forEach(row => {
topicMap[row.name] = row.id;
});
resolve(topicMap);
}
db.close();
});
});
}
// 根据文件名推断专题
function inferTopics(filename, topicMap) {
const topicIds = [];
const lowerFilename = filename.toLowerCase();
for (const [keyword, topics] of Object.entries(TOPIC_MAPPING)) {
if (lowerFilename.includes(keyword)) {
topics.forEach(topicName => {
if (topicMap[topicName] && !topicIds.includes(topicMap[topicName])) {
topicIds.push(topicMap[topicName]);
}
});
}
}
// 如果没有匹配到专题,默认分配到"文化艺术"
if (topicIds.length === 0 && topicMap['文化艺术']) {
topicIds.push(topicMap['文化艺术']);
}
return topicIds;
}
// 生成视频标题和描述
function generateVideoInfo(filename) {
// 移除文件扩展名和特殊字符
let title = path.parse(filename).name;
title = title.replace(/[\-快手]/g, '').trim();
// 如果标题太长截取前50个字符
if (title.length > 50) {
title = title.substring(0, 50) + '...';
}
const description = `精彩视频内容:${title}`;
return { title, description };
}
// 上传单个视频文件
async function uploadVideo(filePath, topicMap) {
const filename = path.basename(filePath);
const { title, description } = generateVideoInfo(filename);
const topicIds = inferTopics(filename, topicMap);
console.log(`正在上传: ${filename}`);
console.log(`标题: ${title}`);
console.log(`专题: ${topicIds.map(id => Object.keys(topicMap).find(key => topicMap[key] === id)).join(', ')}`);
try {
const formData = new FormData();
formData.append('video', fs.createReadStream(filePath));
formData.append('title', title);
formData.append('description', description);
formData.append('category', '文化艺术');
formData.append('topicIds', JSON.stringify(topicIds));
if (!authToken) {
throw new Error('未获取到认证token请先登录');
}
const response = await axios.post(`${SERVER_URL}${UPLOAD_ENDPOINT}`, formData, {
headers: {
...formData.getHeaders(),
'Authorization': `Bearer ${authToken}`
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 300000 // 5分钟超时
});
if (response.data.code === 201 || response.data.code === 200) {
console.log(`✅ 上传成功: ${filename}`);
return { success: true, filename, videoId: response.data.data.id };
} else {
console.error(`❌ 上传失败: ${filename} - ${response.data.message}`);
return { success: false, filename, error: response.data.message };
}
} catch (error) {
console.error(`❌ 上传异常: ${filename} - ${error.message}`);
return { success: false, filename, error: error.message };
}
}
// 获取视频文件列表
function getVideoFiles(directory) {
if (!fs.existsSync(directory)) {
throw new Error(`目录不存在: ${directory}`);
}
const files = fs.readdirSync(directory);
const videoFiles = files.filter(file => {
const ext = path.extname(file).toLowerCase();
return SUPPORTED_FORMATS.includes(ext);
});
return videoFiles.map(file => path.join(directory, file));
}
// 主函数
async function main() {
try {
console.log('开始批量上传视频...');
console.log(`源目录: ${VIDEO_SOURCE_DIR}`);
// 先登录获取token
console.log('\n=== 用户认证 ===');
const loginSuccess = await login();
if (!loginSuccess) {
console.log('❌ 登录失败,无法继续上传');
return;
}
// 获取专题映射
console.log('\n=== 获取专题信息 ===');
const topicMap = await getTopicIds();
console.log('可用专题:', Object.keys(topicMap));
// 获取视频文件列表
console.log('\n=== 扫描视频文件 ===');
const videoFiles = getVideoFiles(VIDEO_SOURCE_DIR);
console.log(`发现 ${videoFiles.length} 个视频文件`);
if (videoFiles.length === 0) {
console.log('没有找到视频文件,退出程序');
return;
}
// 批量上传
console.log('\n=== 开始上传 ===');
const results = [];
for (let i = 0; i < videoFiles.length; i++) {
const filePath = videoFiles[i];
console.log(`\n[${i + 1}/${videoFiles.length}]`);
const result = await uploadVideo(filePath, topicMap);
results.push(result);
// 添加延迟避免服务器压力
if (i < videoFiles.length - 1) {
console.log('等待 2 秒...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
// 统计结果
console.log('\n=== 上传结果统计 ===');
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`总计: ${results.length} 个文件`);
console.log(`成功: ${successful.length}`);
console.log(`失败: ${failed.length}`);
if (failed.length > 0) {
console.log('\n失败的文件:');
failed.forEach(f => {
console.log(`- ${f.filename}: ${f.error}`);
});
}
console.log('\n✅ 批量上传完成!');
} catch (error) {
console.error('❌ 批量上传过程中出现错误:', error);
process.exit(1);
}
}
// 如果直接运行此脚本
if (require.main === module) {
main();
}
module.exports = { uploadVideo, getVideoFiles, getTopicIds };

133
scripts/clean-test-data.cjs Executable file
View File

@@ -0,0 +1,133 @@
/**
* 清理测试视频数据脚本
* 删除所有视频记录、相关文件和数据库记录
*/
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
// 数据库路径
const DB_PATH = path.join(process.cwd(), 'database', 'goguryeo_video.db');
// 视频文件存储目录
const VIDEOS_DIR = path.join(process.cwd(), 'uploads', 'videos');
const COVERS_DIR = path.join(process.cwd(), 'uploads', 'covers');
const THUMBNAILS_DIR = path.join(process.cwd(), 'uploads', 'thumbnails');
async function cleanDatabase() {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
console.error('数据库连接失败:', err);
reject(err);
return;
}
console.log('已连接到数据库');
});
// 开始事务
db.serialize(() => {
db.run('BEGIN TRANSACTION');
// 删除视频相关的所有数据
const queries = [
'DELETE FROM play_history',
'DELETE FROM user_likes',
'DELETE FROM video_stats',
'DELETE FROM video_tags',
'DELETE FROM videos',
'DELETE FROM tags WHERE id > 10', // 保留默认标签
// 重置自增ID
'DELETE FROM sqlite_sequence WHERE name IN ("videos", "video_tags", "play_history", "user_likes", "video_stats")'
];
let completed = 0;
const total = queries.length;
queries.forEach((query, index) => {
db.run(query, (err) => {
if (err) {
console.error(`执行查询失败 [${index}]:`, err);
db.run('ROLLBACK');
db.close();
reject(err);
return;
}
completed++;
console.log(`已完成查询 ${completed}/${total}: ${query.split(' ')[0]} ${query.split(' ')[2]}`);
if (completed === total) {
db.run('COMMIT', (err) => {
if (err) {
console.error('提交事务失败:', err);
reject(err);
} else {
console.log('数据库清理完成');
resolve();
}
db.close();
});
}
});
});
});
});
}
async function cleanFiles() {
const directories = [VIDEOS_DIR, COVERS_DIR, THUMBNAILS_DIR];
for (const dir of directories) {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir);
for (const file of files) {
// 跳过示例文件和隐藏文件
if (file.startsWith('.') || file.startsWith('sample')) {
continue;
}
const filePath = path.join(dir, file);
try {
fs.unlinkSync(filePath);
console.log(`已删除文件: ${filePath}`);
} catch (err) {
console.error(`删除文件失败 ${filePath}:`, err);
}
}
console.log(`已清理目录: ${dir}`);
} else {
console.log(`目录不存在: ${dir}`);
}
}
}
async function main() {
try {
console.log('开始清理测试数据...');
// 清理数据库
console.log('\n=== 清理数据库 ===');
await cleanDatabase();
// 清理文件
console.log('\n=== 清理文件 ===');
await cleanFiles();
console.log('\n✅ 测试数据清理完成!');
} catch (error) {
console.error('❌ 清理过程中出现错误:', error);
process.exit(1);
}
}
// 如果直接运行此脚本
if (require.main === module) {
main();
}
module.exports = { cleanDatabase, cleanFiles };

View File

@@ -0,0 +1,124 @@
const sqlite3 = require('sqlite3').verbose();
// 数据库连接
const db = new sqlite3.Database('goguryeo_video.db');
// 重新分配视频到不同分类
const redistributionPlan = [
// 将一些动物视频重新分类为传统艺术(表演类)
{
videoIds: [17], // 巨肺狗子(表演性质)
newCategory: 2, // 传统艺术
reason: '表演性质的内容'
},
// 将一些动物视频重新分类为语言学习(教育性质)
{
videoIds: [11, 15], // 小牛叫妈妈、水獭
newCategory: 3, // 语言学习
reason: '教育和学习性质的内容'
},
// 将一些动物视频重新分类为考古发现(自然探索)
{
videoIds: [9, 18], // 小老虎、小象
newCategory: 4, // 考古发现
reason: '自然探索和发现类内容'
},
// 将一些动物视频重新分类为其他
{
videoIds: [16, 20], // 小肥啾、小猫咪
newCategory: 6, // 其他
reason: '综合性内容'
}
];
// 更新视频分类
function updateVideoCategory(videoId, newCategory) {
return new Promise((resolve, reject) => {
const sql = 'UPDATE videos SET category = ? WHERE id = ?';
db.run(sql, [newCategory, videoId], function(err) {
if (err) {
reject(err);
} else {
resolve(this.changes);
}
});
});
}
// 获取视频信息
function getVideoInfo(videoId) {
return new Promise((resolve, reject) => {
const sql = 'SELECT id, title, category FROM videos WHERE id = ?';
db.get(sql, [videoId], (err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
}
// 主函数
async function main() {
try {
console.log('开始重新分配视频分类...');
for (const plan of redistributionPlan) {
console.log(`\n重新分配到分类 ${plan.newCategory}: ${plan.reason}`);
for (const videoId of plan.videoIds) {
try {
// 获取视频信息
const videoInfo = await getVideoInfo(videoId);
if (!videoInfo) {
console.log(`视频 ID ${videoId} 不存在`);
continue;
}
console.log(` ${videoInfo.title} (ID: ${videoId}) 从分类 ${videoInfo.category} -> ${plan.newCategory}`);
// 更新分类
await updateVideoCategory(videoId, plan.newCategory);
} catch (error) {
console.error(`更新视频 ${videoId} 失败:`, error.message);
}
}
}
console.log('\n重新分配完成');
// 显示最终的分类统计
console.log('\n=== 最终分类统计 ===');
const sql = `
SELECT c.name, COUNT(v.id) as video_count
FROM categories c
LEFT JOIN videos v ON c.id = v.category
GROUP BY c.id, c.name
ORDER BY c.id
`;
db.all(sql, [], (err, rows) => {
if (err) {
console.error('查询统计失败:', err);
} else {
rows.forEach(row => {
console.log(`${row.name}: ${row.video_count} 个视频`);
});
}
db.close();
});
} catch (error) {
console.error('重新分配失败:', error);
db.close();
}
}
// 运行脚本
if (require.main === module) {
main();
}
module.exports = { main };