videofolxtv/server/routes.ts
sebastjanartic e40b738478 Improve platform speed and search responsiveness
Implement performance optimizations including HTTP compression, enhanced caching strategies, reduced search debounce time, and optimized thumbnail generation.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/UCWygK2
2025-08-28 15:04:47 +00:00

494 lines
15 KiB
TypeScript

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<Server> {
// 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" });
}
});
// 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" });
}
});
// 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;
}