import type { Express, Request, Response } from "express"; import { createServer, type Server } from "http"; import express from "express"; import compression from "compression"; import { storage } from "./storage"; import { z } from "zod"; import { updateVideoSchema, insertVideoSchema, insertUserSchema, insertVideoUploadSchema, insertCategorySchema, insertTagSchema, type User, type VideoUpload } from "@shared/schema"; import multer from "multer"; import { randomUUID } from "crypto"; import path from "path"; import session from "express-session"; // Extend express session declare module "express-session" { interface SessionData { userId: string; } } // Configure multer for video uploads const upload = multer({ storage: multer.diskStorage({ destination: './uploads/videos', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }), limits: { fileSize: 500 * 1024 * 1024, // 500MB max file size }, fileFilter: (req, file, cb) => { // Allow video files only if (file.mimetype.startsWith('video/')) { cb(null, true); } else { cb(new Error('Only video files are allowed')); } } }); // Simple session-based authentication middleware const authenticate = (req: Request, res: Response, next: any) => { if (req.session?.userId) { next(); } else { res.status(401).json({ message: "Authentication required" }); } }; export async function registerRoutes(app: Express): Promise { // Add compression middleware for better performance app.use(compression({ level: 6, threshold: 1024, // Only compress responses larger than 1KB filter: (req, res) => { // Don't compress video files if (req.headers['accept']?.includes('video/')) { return false; } return compression.filter(req, res); } })); // Configure session middleware app.use(session({ secret: process.env.SESSION_SECRET || 'dev-secret-key', resave: false, saveUninitialized: false, cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24 hours })); // Authentication Routes app.post("/api/auth/register", async (req, res) => { try { const userData = insertUserSchema.parse(req.body); // Check if user already exists const existingUser = await storage.getUserByEmail(userData.email); if (existingUser) { return res.status(400).json({ message: "User already exists with this email" }); } const user = await storage.createUser(userData); // Remove password from response const { password, ...userResponse } = user; res.status(201).json(userResponse); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid user data", errors: error.errors }); } res.status(500).json({ message: "Failed to create user" }); } }); app.post("/api/auth/login", async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: "Email and password are required" }); } const user = await storage.validateUserPassword(email, password); if (!user) { return res.status(401).json({ message: "Invalid credentials" }); } // Set session req.session.userId = user.id; // Remove password from response const { password: _, ...userResponse } = user; res.json(userResponse); } catch (error) { res.status(500).json({ message: "Failed to login" }); } }); app.post("/api/auth/logout", (req, res) => { req.session?.destroy(() => { res.json({ message: "Logged out successfully" }); }); }); app.get("/api/auth/me", authenticate, async (req, res) => { try { const user = await storage.getUser(req.session.userId!); if (!user) { return res.status(404).json({ message: "User not found" }); } const { password, ...userResponse } = user; res.json(userResponse); } catch (error) { res.status(500).json({ message: "Failed to fetch user" }); } }); // Video Routes // Bunny.net administration routes app.get("/api/bunny/stats", async (req, res) => { try { const videos = await storage.getVideos(1, 1000); const totalViews = videos.length > 0 ? videos.reduce((sum: number, video: any) => sum + video.views, 0) : 0; const stats = { totalVideos: videos.length, totalViews, totalStorage: 0, // Would need separate Bunny API call bandwidth: 0 // Would need separate Bunny API call }; res.json(stats); } catch (error) { console.error("Error fetching Bunny stats:", error); res.status(500).json({ message: "Failed to fetch statistics" }); } }); app.delete("/api/bunny/videos/:id", async (req, res) => { try { const { id } = req.params; // Note: This would need implementation in BunnyService // For now, return success (deletion would happen in Bunny dashboard) res.json({ message: "Video deletion initiated" }); } catch (error) { console.error("Error deleting video:", error); res.status(500).json({ message: "Failed to delete video" }); } }); app.get("/api/videos", async (req, res) => { try { const limit = parseInt(req.query.limit as string) || 20; const offset = parseInt(req.query.offset as string) || 0; const search = req.query.search as string; // Skip search for queries shorter than 2 characters for performance const searchQuery = search && search.length >= 2 ? search : undefined; // Create cache key for ETag const cacheKey = `videos-${limit}-${offset}-${searchQuery || 'all'}`; const etag = `"${cacheKey}"`; // Check if client has cached version if (req.headers['if-none-match'] === etag) { return res.status(304).end(); } // Set optimized cache headers res.set({ 'Cache-Control': 'public, max-age=120, stale-while-revalidate=300', // 2 min cache, 5 min stale 'ETag': etag, 'X-Content-Type-Options': 'nosniff', 'Vary': 'Accept-Encoding' }); console.log(`Fetching videos: limit=${limit}, offset=${offset}, search=${searchQuery}`); const videos = await storage.getVideos(limit, offset, searchQuery); const total = await storage.getVideoCount(searchQuery); console.log(`Returning ${videos.length} videos`); res.json({ videos, total, hasMore: offset + limit < total }); } catch (error) { console.error("Error fetching videos:", error); res.status(500).json({ message: "Failed to fetch videos" }); } }); // Face detection endpoint for thumbnails app.post("/api/videos/:id/analyze-face", async (req, res) => { try { const { id } = req.params; const video = await storage.getVideo(id); if (!video) { return res.status(404).json({ message: "Video not found" }); } if (!video.thumbnailUrl) { return res.status(400).json({ message: "Video has no thumbnail" }); } // Import smart thumbnail service (no external dependencies) const { smartThumbnailService } = await import("./smart-thumbnail-service"); const result = await smartThumbnailService.getOptimizedThumbnailInfo(video.thumbnailUrl); // Update video with face position data await storage.updateVideo(id, { faceCenterPosition: result.faceCenterPosition, facesDetected: result.facesDetected, faceConfidence: result.confidence }); res.json({ videoId: id, ...result }); } catch (error) { console.error("Error analyzing face in thumbnail:", error); res.status(500).json({ message: "Failed to analyze face" }); } }); // Batch face analysis for all videos app.post("/api/videos/batch-analyze-faces", async (req, res) => { try { // Get all videos that don't have face analysis yet const allVideos = await storage.getVideos(1000, 0); // Get up to 1000 videos const videosToProcess = allVideos.filter(video => !video.faceCenterPosition && video.thumbnailUrl); if (videosToProcess.length === 0) { return res.json({ message: "All videos already processed or no thumbnails available", processed: 0, total: allVideos.length }); } console.log(`Starting batch face analysis for ${videosToProcess.length} videos...`); // Import smart thumbnail service (no external dependencies) const { smartThumbnailService } = await import("./smart-thumbnail-service"); let processed = 0; let failed = 0; // Process videos in batches to avoid overwhelming the system const batchSize = 5; for (let i = 0; i < videosToProcess.length; i += batchSize) { const batch = videosToProcess.slice(i, i + batchSize); await Promise.all(batch.map(async (video) => { try { console.log(`Processing face detection for video: ${video.title} (${video.id})`); const result = await smartThumbnailService.getOptimizedThumbnailInfo(video.thumbnailUrl); await storage.updateVideo(video.id, { faceCenterPosition: result.faceCenterPosition, facesDetected: result.facesDetected, faceConfidence: result.confidence }); processed++; console.log(`✅ Face analysis completed for ${video.title} - Faces detected: ${result.facesDetected}`); } catch (error) { console.error(`❌ Face analysis failed for ${video.title}:`, error); failed++; } })); // Small delay between batches to be respectful if (i + batchSize < videosToProcess.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } console.log(`Batch face analysis completed: ${processed} processed, ${failed} failed`); res.json({ message: "Batch face analysis completed", processed, failed, total: videosToProcess.length }); } catch (error) { console.error("Error in batch face analysis:", error); res.status(500).json({ message: "Failed to perform batch face analysis" }); } }); // Get single video by ID app.get("/api/videos/:id", async (req, res) => { try { const video = await storage.getVideo(req.params.id); if (!video) { return res.status(404).json({ message: "Video not found" }); } res.json(video); } catch (error) { res.status(500).json({ message: "Failed to fetch video" }); } }); // Get video ads/spots metadata from Bunny.net app.get("/api/videos/:id/ads", async (req, res) => { try { const { id } = req.params; // Check if video exists first const video = await storage.getVideo(id); if (!video) { return res.status(404).json({ message: "Video not found" }); } // Get ads from Bunny.net API let ads = []; try { const { BunnyService } = await import("./bunny"); const bunnyService = new BunnyService(); ads = await bunnyService.getVideoAds(id); console.log(`Retrieved ${ads.length} ad spots for video ${id}`); } catch (error) { console.error(`Failed to get ads from Bunny.net for video ${id}:`, error); // Return empty array if Bunny service fails ads = []; } res.json({ videoId: id, ads: ads, totalAds: ads.length }); } catch (error) { console.error(`Error fetching ads for video ${id}:`, error); res.status(500).json({ message: "Failed to fetch video ads" }); } }); // Update video views app.post("/api/videos/:id/view", async (req, res) => { try { await storage.updateVideoViews(req.params.id); res.json({ success: true }); } catch (error) { res.status(500).json({ message: "Failed to update views" }); } }); // Create new video app.post("/api/videos", authenticate, async (req, res) => { try { const videoData = insertVideoSchema.parse(req.body); const video = await storage.createVideo(videoData); res.status(201).json(video); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid video data", errors: error.errors }); } res.status(500).json({ message: "Failed to create video" }); } }); // Update video metadata (title, description, etc.) app.patch("/api/videos/:id", authenticate, async (req, res) => { try { const updates = updateVideoSchema.parse(req.body); const video = await storage.updateVideo(req.params.id, updates); if (!video) { return res.status(404).json({ message: "Video not found" }); } res.json(video); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid request data", errors: error.errors }); } res.status(500).json({ message: "Failed to update video" }); } }); // Delete video app.delete("/api/videos/:id", authenticate, async (req, res) => { try { const success = await storage.deleteVideo(req.params.id); if (!success) { return res.status(404).json({ message: "Video not found" }); } res.json({ success: true, message: "Video deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete video" }); } }); // Video Upload Routes app.post("/api/uploads/start", authenticate, async (req, res) => { try { const { originalFileName, fileSize, mimeType } = req.body; if (!originalFileName || !fileSize || !mimeType) { return res.status(400).json({ message: "originalFileName, fileSize, and mimeType are required" }); } const uploadData = { userId: req.session.userId!, originalFileName, fileSize: parseInt(fileSize), mimeType, uploadStatus: "uploading" as const, uploadProgress: 0 }; const upload = await storage.createVideoUpload(uploadData); res.status(201).json(upload); } catch (error) { res.status(500).json({ message: "Failed to initialize upload" }); } }); app.post("/api/uploads/:id/video", authenticate, upload.single('video'), async (req, res) => { try { const uploadId = req.params.id; const file = req.file; if (!file) { return res.status(400).json({ message: "No video file provided" }); } // Update upload with file information await storage.updateVideoUpload(uploadId, { uploadStatus: "processing", uploadProgress: 1.0 }); // Create video record const videoData = { title: req.body.title || path.parse(file.originalname).name, description: req.body.description || "", thumbnailUrl: req.body.thumbnailUrl || "https://via.placeholder.com/800x450", videoUrl: `/uploads/videos/${file.filename}`, duration: parseInt(req.body.duration) || 0, views: 0, category: req.body.category || "", tags: req.body.tags ? JSON.parse(req.body.tags) : [], isPublic: req.body.isPublic !== "false", uploadStatus: "completed", originalFileName: file.originalname, fileSize: file.size, format: path.extname(file.originalname).slice(1) }; const video = await storage.createVideo(videoData); // Link video to upload await storage.updateVideoUpload(uploadId, { videoId: video.id, uploadStatus: "completed" }); res.json({ video, upload: { id: uploadId, status: "completed" } }); } catch (error) { console.error("Upload error:", error); // Update upload status to failed if (req.params.id) { await storage.updateVideoUpload(req.params.id, { uploadStatus: "failed", errorMessage: error instanceof Error ? error.message : "Upload failed" }); } res.status(500).json({ message: "Failed to upload video" }); } }); app.get("/api/uploads/:id/status", authenticate, async (req, res) => { try { const upload = await storage.getVideoUpload(req.params.id); if (!upload) { return res.status(404).json({ message: "Upload not found" }); } res.json(upload); } catch (error) { res.status(500).json({ message: "Failed to get upload status" }); } }); app.get("/api/uploads/user", authenticate, async (req, res) => { try { const uploads = await storage.getUserVideoUploads(req.session.userId!); res.json(uploads); } catch (error) { res.status(500).json({ message: "Failed to fetch user uploads" }); } }); // Category Routes app.get("/api/categories", async (req, res) => { try { const categories = await storage.getCategories(); res.json(categories); } catch (error) { res.status(500).json({ message: "Failed to fetch categories" }); } }); app.post("/api/categories", authenticate, async (req, res) => { try { const categoryData = insertCategorySchema.parse(req.body); const category = await storage.createCategory(categoryData); res.status(201).json(category); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid category data", errors: error.errors }); } res.status(500).json({ message: "Failed to create category" }); } }); app.patch("/api/categories/:id", authenticate, async (req, res) => { try { const updates = insertCategorySchema.partial().parse(req.body); const category = await storage.updateCategory(req.params.id, updates); if (!category) { return res.status(404).json({ message: "Category not found" }); } res.json(category); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid category data", errors: error.errors }); } res.status(500).json({ message: "Failed to update category" }); } }); app.delete("/api/categories/:id", authenticate, async (req, res) => { try { const success = await storage.deleteCategory(req.params.id); if (!success) { return res.status(404).json({ message: "Category not found" }); } res.json({ success: true, message: "Category deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete category" }); } }); // Tag Routes app.get("/api/tags", async (req, res) => { try { const limit = parseInt(req.query.limit as string) || 50; const popular = req.query.popular === "true"; const tags = popular ? await storage.getPopularTags(limit) : await storage.getTags(); res.json(tags); } catch (error) { res.status(500).json({ message: "Failed to fetch tags" }); } }); app.post("/api/tags", authenticate, async (req, res) => { try { const tagData = insertTagSchema.parse(req.body); const tag = await storage.createTag(tagData); res.status(201).json(tag); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid tag data", errors: error.errors }); } res.status(500).json({ message: "Failed to create tag" }); } }); // Serve uploaded videos app.use('/uploads', express.static('uploads')); // Serve ads.txt file specifically app.get("/ads.txt", (req, res) => { res.type("text/plain"); res.sendFile(path.join(__dirname, "../client/public/ads.txt")); }); const httpServer = createServer(app); return httpServer; }