376 lines
11 KiB
JavaScript
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; |