Files
video-platform/app.js

376 lines
11 KiB
JavaScript

const express = require('express');
const session = require('express-session');
const path = require('path');
const fs = require('fs');
const WebSocket = require('ws');
const app = express();
const PORT = 16035;
// 中间件配置
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// Session配置
app.use(session({
secret: 'video-platform-secret-key-2024',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 }
}));
// 数据存储路径
const DATA_DIR = path.join(__dirname, 'data');
const VIDEOS_FILE = path.join(DATA_DIR, 'videos.json');
const USERS_FILE = path.join(DATA_DIR, 'users.json');
const COMMENTS_FILE = path.join(DATA_DIR, 'comments.json');
// 确保数据目录和文件存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
// 初始化数据文件
function initDataFile(filePath, defaultData) {
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, JSON.stringify(defaultData, null, 2), 'utf8');
}
}
// 初始化默认数据
initDataFile(VIDEOS_FILE, [
{
id: 1,
title: "精彩短视频演示",
description: "这是一个示例视频,展示了平台的播放功能",
url: "https://www.w3schools.com/html/mov_bbb.mp4",
cover: "https://picsum.photos/400/300?random=1",
duration: 10,
views: 1520,
likes: 128,
category: "推荐",
author: "管理员",
createTime: "2024-01-15"
},
{
id: 2,
title: "城市夜景延时摄影",
description: "美丽的城市夜景,灯光璀璨",
url: "https://www.w3schools.com/html/movie.mp4",
cover: "https://picsum.photos/400/300?random=2",
duration: 12,
views: 2340,
likes: 256,
category: "风景",
author: "摄影师小王",
createTime: "2024-01-14"
},
{
id: 3,
title: "可爱猫咪日常",
description: "萌宠猫咪的有趣日常",
url: "https://www.w3schools.com/html/mov_bbb.mp4",
cover: "https://picsum.photos/400/300?random=3",
duration: 8,
views: 5670,
likes: 892,
category: "萌宠",
author: "铲屎官",
createTime: "2024-01-13"
},
{
id: 4,
title: "美食制作教程",
description: "教你做出美味的家常菜",
url: "https://www.w3schools.com/html/movie.mp4",
cover: "https://picsum.photos/400/300?random=4",
duration: 15,
views: 3210,
likes: 445,
category: "美食",
author: "美食达人",
createTime: "2024-01-12"
},
{
id: 5,
title: "科技产品开箱",
description: "最新科技产品开箱体验",
url: "https://www.w3schools.com/html/mov_bbb.mp4",
cover: "https://picsum.photos/400/300?random=5",
duration: 20,
views: 4560,
likes: 523,
category: "科技",
author: "数码博主",
createTime: "2024-01-11"
},
{
id: 6,
title: "旅行vlog记录",
description: "美丽的旅行风景记录",
url: "https://www.w3schools.com/html/movie.mp4",
cover: "https://picsum.photos/400/300?random=6",
duration: 18,
views: 2890,
likes: 367,
category: "旅行",
author: "旅行家",
createTime: "2024-01-10"
}
]);
initDataFile(USERS_FILE, {
admin: { password: 'admin123', role: 'admin' }
});
initDataFile(COMMENTS_FILE, {});
// 辅助函数
function readJSON(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function writeJSON(filePath, data) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
}
// ============ 前端路由 ============
// 首页
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 视频播放页
app.get('/video/:id', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'video.html'));
});
// 后台管理登录页
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'admin-login.html'));
});
// 后台管理面板
app.get('/admin/panel', (req, res) => {
if (req.session.user && req.session.user.role === 'admin') {
res.sendFile(path.join(__dirname, 'public', 'admin-panel.html'));
} else {
res.redirect('/admin');
}
});
// ============ API路由 ============
// 获取视频列表
app.get('/api/videos', (req, res) => {
const videos = readJSON(VIDEOS_FILE) || [];
res.json({ success: true, data: videos });
});
// 获取单个视频
app.get('/api/videos/:id', (req, res) => {
const videos = readJSON(VIDEOS_FILE) || [];
const video = videos.find(v => v.id === parseInt(req.params.id));
if (video) {
// 增加观看次数
video.views = (video.views || 0) + 1;
writeJSON(VIDEOS_FILE, videos);
res.json({ success: true, data: video });
} else {
res.status(404).json({ success: false, message: '视频不存在' });
}
});
// 点赞视频
app.post('/api/videos/:id/like', (req, res) => {
const videos = readJSON(VIDEOS_FILE) || [];
const video = videos.find(v => v.id === parseInt(req.params.id));
if (video) {
video.likes = (video.likes || 0) + 1;
writeJSON(VIDEOS_FILE, videos);
res.json({ success: true, likes: video.likes });
} else {
res.status(404).json({ success: false, message: '视频不存在' });
}
});
// 获取弹幕
app.get('/api/videos/:id/danmaku', (req, res) => {
const comments = readJSON(COMMENTS_FILE) || {};
const danmaku = comments[req.params.id] || [];
res.json({ success: true, data: danmaku });
});
// 发送弹幕
app.post('/api/videos/:id/danmaku', (req, res) => {
const { time, text, color } = req.body;
if (!text || text.trim() === '') {
return res.status(400).json({ success: false, message: '弹幕内容不能为空' });
}
const comments = readJSON(COMMENTS_FILE) || {};
if (!comments[req.params.id]) {
comments[req.params.id] = [];
}
const danmaku = {
id: Date.now(),
time: parseFloat(time) || 0,
text: text.trim().substring(0, 100),
color: color || '#ffffff',
createTime: new Date().toISOString()
};
comments[req.params.id].push(danmaku);
writeJSON(COMMENTS_FILE, comments);
// 广播弹幕
broadcastDanmaku(req.params.id, danmaku);
res.json({ success: true, data: danmaku });
});
// 管理员登录
app.post('/api/admin/login', (req, res) => {
const { username, password } = req.body;
const users = readJSON(USERS_FILE) || {};
if (users[username] && users[username].password === password) {
req.session.user = {
username,
role: users[username].role
};
res.json({ success: true, message: '登录成功' });
} else {
res.status(401).json({ success: false, message: '用户名或密码错误' });
}
});
// 管理员登出
app.post('/api/admin/logout', (req, res) => {
req.session.destroy();
res.json({ success: true });
});
// 检查登录状态
app.get('/api/admin/check', (req, res) => {
if (req.session.user && req.session.user.role === 'admin') {
res.json({ success: true, user: req.session.user });
} else {
res.status(401).json({ success: false });
}
});
// 添加视频
app.post('/api/admin/videos', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '未授权' });
}
const videos = readJSON(VIDEOS_FILE) || [];
const newVideo = {
id: Date.now(),
...req.body,
views: 0,
likes: 0,
createTime: new Date().toISOString().split('T')[0]
};
videos.push(newVideo);
writeJSON(VIDEOS_FILE, videos);
res.json({ success: true, data: newVideo });
});
// 更新视频
app.put('/api/admin/videos/:id', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '未授权' });
}
const videos = readJSON(VIDEOS_FILE) || [];
const index = videos.findIndex(v => v.id === parseInt(req.params.id));
if (index !== -1) {
videos[index] = { ...videos[index], ...req.body };
writeJSON(VIDEOS_FILE, videos);
res.json({ success: true, data: videos[index] });
} else {
res.status(404).json({ success: false, message: '视频不存在' });
}
});
// 删除视频
app.delete('/api/admin/videos/:id', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '未授权' });
}
const videos = readJSON(VIDEOS_FILE) || [];
const index = videos.findIndex(v => v.id === parseInt(req.params.id));
if (index !== -1) {
videos.splice(index, 1);
writeJSON(VIDEOS_FILE, videos);
res.json({ success: true });
} else {
res.status(404).json({ success: false, message: '视频不存在' });
}
});
// ============ WebSocket 弹幕实时推送 ============
const server = app.listen(PORT, () => {
console.log(`短视频平台已启动: http://localhost:${PORT}`);
console.log(`后台管理地址: http://localhost:${PORT}/admin`);
});
const wss = new WebSocket.Server({ server });
const videoClients = {};
wss.on('connection', (ws) => {
let currentVideoId = null;
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'join' && data.videoId) {
currentVideoId = data.videoId;
if (!videoClients[currentVideoId]) {
videoClients[currentVideoId] = new Set();
}
videoClients[currentVideoId].add(ws);
}
} catch (e) {
console.error('WebSocket消息解析错误:', e);
}
});
ws.on('close', () => {
if (currentVideoId && videoClients[currentVideoId]) {
videoClients[currentVideoId].delete(ws);
}
});
});
function broadcastDanmaku(videoId, danmaku) {
if (videoClients[videoId]) {
const message = JSON.stringify({
type: 'danmaku',
data: danmaku
});
videoClients[videoId].forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
}
module.exports = app;