// ...existing code... const express = require("express"); const multer = require("multer"); const fs = require("fs"); const path = require("path"); const session = require("express-session"); const bcrypt = require("bcryptjs"); const ffmpeg = require("fluent-ffmpeg"); const app = express(); const PORT = 3000; // Data storage const userDbFile = "./data/users.json"; const videoDbFile = "./data/videos.json"; let users = fs.existsSync(userDbFile) ? JSON.parse(fs.readFileSync(userDbFile)) : []; let videos = fs.existsSync(videoDbFile) ? JSON.parse(fs.readFileSync(videoDbFile)) : []; // Ensure all users have a watchHistory array users.forEach(u => { if (!u.watchHistory) u.watchHistory = []; }); // Middleware app.use(express.static("public")); app.use("/videos", express.static("videos")); app.use("/thumbnails", express.static("thumbnails")); app.use(express.json()); app.use(session({ secret: "minitube_secret_key", resave: false, saveUninitialized: false, })); // Helpers function requireLogin(req, res, next) { if (!req.session.userId) return res.status(403).json({ error: "Login required" }); next(); } // ----------------- Multer for Videos ----------------- const videoStorage = multer.diskStorage({ destination: "videos/", filename: (req, file, cb) => { cb(null, Date.now() + path.extname(file.originalname)); }, }); const uploadVideo = multer({ storage: videoStorage, fileFilter: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); const allowed = [".mp4", ".webm"]; if (!allowed.includes(ext)) { return cb(new Error("Only .mp4 and .webm allowed")); } cb(null, true); } }); // ----------------- Multer for Thumbnails ----------------- const thumbStorage = multer.diskStorage({ destination: "thumbnails/", filename: (req, file, cb) => { cb(null, Date.now() + path.extname(file.originalname)); }, }); const uploadThumb = multer({ storage: thumbStorage, fileFilter: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); const allowed = [".png", ".jpg", ".jpeg"]; if (!allowed.includes(ext)) { return cb(new Error("Only .png, .jpg, .jpeg thumbnails allowed")); } cb(null, true); } }); // ----------------- Auth ----------------- app.post("/api/register", (req, res) => { const { username, password } = req.body; if (users.find(u => u.username === username)) { return res.status(400).json({ error: "Username taken" }); } const hash = bcrypt.hashSync(password, 10); const user = { id: Date.now(), username, password: hash, subscribers: [] }; users.push(user); fs.writeFileSync(userDbFile, JSON.stringify(users, null, 2)); res.json({ success: true }); }); app.post("/api/login", (req, res) => { const { username, password } = req.body; const user = users.find(u => u.username === username); if (!user || !bcrypt.compareSync(password, user.password)) { return res.status(400).json({ error: "Invalid login" }); } req.session.userId = user.id; res.json({ success: true, username: user.username }); }); app.post("/api/logout", (req, res) => { req.session.destroy(() => res.json({ success: true })); }); // ----------------- Videos ----------------- app.get("/api/videos", (req, res) => res.json(videos)); // Support tags, title, and custom thumbnail const uploadFields = uploadVideo.fields([ { name: 'video', maxCount: 1 }, { name: 'thumbnail', maxCount: 1 } ]); app.post("/api/upload", requireLogin, (req, res) => { uploadFields(req, res, function (err) { if (err) return res.status(400).json({ error: err.message }); const user = users.find(u => u.id === req.session.userId); if (!req.files || !req.files['video'] || !req.files['video'][0]) { return res.status(400).json({ error: "No video uploaded" }); } const videoFile = req.files['video'][0]; const title = req.body.title || videoFile.originalname; const tags = (req.body.tags || "").split(",").map(t => t.trim()).filter(Boolean); const videoId = Date.now(); // If custom thumbnail provided, use it let thumbFilename = null; if (req.files['thumbnail'] && req.files['thumbnail'][0]) { thumbFilename = req.files['thumbnail'][0].filename; finishUpload(); } else { // Auto-generate thumbnail thumbFilename = `${videoId}.png`; const videoPath = videoFile.path; ffmpeg(videoPath) .screenshots({ timestamps: ['2'], filename: thumbFilename, folder: 'thumbnails', size: '320x240' }) .on('end', finishUpload) .on('error', err => { console.error("Thumbnail generation error:", err); finishUpload("Video uploaded but thumbnail generation failed."); }); } function finishUpload(thumbnailError) { const newVideo = { id: videoId, title, filename: videoFile.filename, uploader: user.username, likes: [], comments: [], thumbnail: thumbFilename, tags }; videos.push(newVideo); fs.writeFileSync(videoDbFile, JSON.stringify(videos, null, 2)); if (thumbnailError) { res.status(500).json({ error: thumbnailError, video: newVideo }); } else { res.json({ success: true, video: newVideo }); } } }); }); // Get video and record watch history if logged in app.get("/api/videos/:id", (req, res) => { const video = videos.find(v => v.id == req.params.id); if (!video) return res.status(404).json({ error: "Not found" }); // Record watch history for logged-in user if (req.session.userId) { const user = users.find(u => u.id === req.session.userId); if (user && !user.watchHistory.includes(video.id)) { user.watchHistory.push(video.id); fs.writeFileSync(userDbFile, JSON.stringify(users, null, 2)); } } res.json(video); }); // Personalized recommendations based on tags and watch history app.get("/api/recommended", requireLogin, (req, res) => { const user = users.find(u => u.id === req.session.userId); if (!user) return res.status(401).json([]); // Get videos not watched yet let unwatched = videos.filter(v => !user.watchHistory.includes(v.id)); // Get tags from recently watched videos (last 5) let recent = user.watchHistory.slice(-5).map(id => videos.find(v => v.id === id)).filter(Boolean); let tagCounts = {}; recent.forEach(v => { if (Array.isArray(v.tags)) { v.tags.forEach(tag => { tagCounts[tag] = (tagCounts[tag] || 0) + 1; }); } }); // Score unwatched videos by tag overlap unwatched.forEach(v => { v._score = 0; if (Array.isArray(v.tags)) { v.tags.forEach(tag => { v._score += tagCounts[tag] || 0; }); } // Boost by likes v._score += (v.likes?.length || 0) * 0.2; }); // Sort by score, then most recent unwatched.sort((a, b) => b._score - a._score || b.id - a.id); res.json(unwatched.slice(0, 8)); }); // Change thumbnail app.post("/api/videos/:id/thumbnail", requireLogin, uploadThumb.single("thumbnail"), (req, res) => { const video = videos.find(v => v.id == req.params.id); if (!video) return res.status(404).json({ error: "Video not found" }); const user = users.find(u => u.id === req.session.userId); if (video.uploader !== user.username) { return res.status(403).json({ error: "Not your video" }); } video.thumbnail = req.file.filename; fs.writeFileSync(videoDbFile, JSON.stringify(videos, null, 2)); res.json({ success: true, thumbnail: video.thumbnail }); }); // Like app.post("/api/videos/:id/like", requireLogin, (req, res) => { const video = videos.find(v => v.id == req.params.id); if (!video) return res.status(404).json({ error: "Not found" }); const user = users.find(u => u.id === req.session.userId); if (video.likes.includes(user.username)) { video.likes = video.likes.filter(u => u !== user.username); } else { video.likes.push(user.username); } fs.writeFileSync(videoDbFile, JSON.stringify(videos, null, 2)); res.json({ likes: video.likes.length }); }); // Comment app.post("/api/videos/:id/comment", requireLogin, (req, res) => { const video = videos.find(v => v.id == req.params.id); if (!video) return res.status(404).json({ error: "Not found" }); const user = users.find(u => u.id === req.session.userId); const comment = { user: user.username, text: req.body.text }; video.comments.push(comment); fs.writeFileSync(videoDbFile, JSON.stringify(videos, null, 2)); res.json(comment); }); // ----------------- User Pages ----------------- app.get("/api/user/:username", (req, res) => { const user = users.find(u => u.username === req.params.username); if (!user) return res.status(404).json({ error: "User not found" }); const userVideos = videos.filter(v => v.uploader === user.username); res.json({ username: user.username, subscribers: user.subscribers.length, videos: userVideos }); }); app.post("/api/subscribe/:username", requireLogin, (req, res) => { const target = users.find(u => u.username === req.params.username); if (!target) return res.status(404).json({ error: "User not found" }); const me = users.find(u => u.id === req.session.userId); if (target.subscribers.includes(me.username)) { target.subscribers = target.subscribers.filter(s => s !== me.username); } else { target.subscribers.push(me.username); } fs.writeFileSync(userDbFile, JSON.stringify(users, null, 2)); res.json({ success: true, subscribers: target.subscribers.length }); }); // ----------------- Start ----------------- app.listen(PORT, () => console.log(`MiniTube running at http://localhost:${PORT}`) ); // ----------------- Admin Panel (MUST be at the very end) ----------------- function requireAdmin(req, res, next) { const user = users.find(u => u.id === req.session.userId); if (!user || !user.isAdmin) { return res.status(403).json({ error: 'Admin only' }); } next(); } app.post('/api/admin/delete-user', requireLogin, requireAdmin, (req, res) => { const { username } = req.body; if (!username) return res.status(400).json({ error: 'Username required' }); const idx = users.findIndex(u => u.username === username); if (idx === -1) return res.status(404).json({ error: 'User not found' }); users.splice(idx, 1); fs.writeFileSync(userDbFile, JSON.stringify(users, null, 2)); res.json({ success: true }); }); app.post('/api/admin/delete-video', requireLogin, requireAdmin, (req, res) => { const { id } = req.body; const idx = videos.findIndex(v => String(v.id) === String(id)); if (idx === -1) return res.status(404).json({ error: 'Video not found' }); videos.splice(idx, 1); fs.writeFileSync(videoDbFile, JSON.stringify(videos, null, 2)); res.json({ success: true }); });