import type { Express } from "express"; import { createServer, type Server } from "http"; import { storage } from "./storage"; import { z } from "zod"; import { BunnyService } from "./bunny"; import { ThumbnailGenerator } from "./thumbnail-generator"; export async function registerRoutes(app: Express): Promise { const thumbnailGenerator = new ThumbnailGenerator(); // Get videos with pagination and filtering 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; const category = req.query.category as string; const videos = await storage.getVideos(limit, offset, search, category); const total = await storage.getVideoCount(search, category); res.json({ videos, total, hasMore: offset + limit < total }); } catch (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" }); } }); // Get video categories app.get("/api/categories", async (req, res) => { try { const videos = await storage.getVideos(1000); const categories = Array.from(new Set(videos.map(v => v.category).filter(Boolean))); res.json(categories); } catch (error) { console.error("Error fetching categories:", error); res.status(500).json({ message: "Failed to fetch categories" }); } }); // Serve video page with meta tags for social sharing app.get("/video/:id", async (req, res) => { try { const video = await storage.getVideo(req.params.id); if (!video) { return res.redirect("/"); } const shareUrl = `${req.protocol}://${req.get('host')}/video/${video.id}`; const html = ` ${video.title} - VideoStream
`; res.send(html); } catch (error) { console.error("Error serving video page:", error); res.redirect("/"); } }); // Generate real thumbnail from video frame at specific time app.get("/thumbnail/:videoId", async (req, res) => { const timeStamp = req.query.t as string || "5"; // Default to 5 seconds try { const { videoId } = req.params; // Get video info const video = await storage.getVideo(videoId); if (!video) { return res.status(404).json({ message: "Video not found" }); } // Try to generate real thumbnail from video const thumbnailPath = await thumbnailGenerator.generateThumbnail({ videoId, timeStamp, width: 400, height: 225, quality: 85 }); if (thumbnailPath && require('fs').existsSync(thumbnailPath)) { // Serve the generated thumbnail res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=86400'); res.setHeader('Access-Control-Allow-Origin', '*'); return res.sendFile(require('path').resolve(thumbnailPath)); } else { // Fallback to SVG if thumbnail generation fails const displayTitle = video.title.replace('.mp4', '').substring(0, 40); const duration = video.duration ? Math.floor(video.duration / 60) + ':' + String(video.duration % 60).padStart(2, '0') : ''; const svg = ` ${displayTitle} ${duration ? ` ${duration}` : ''} VIDEO `; res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=86400'); res.setHeader('Access-Control-Allow-Origin', '*'); return res.send(svg); } } catch (error) { console.error("Error creating thumbnail:", error); res.status(500).json({ message: "Error generating thumbnail" }); } }); // API endpoint to generate multiple thumbnails for video preview app.post("/api/thumbnails/:videoId/generate", async (req, res) => { try { const { videoId } = req.params; const { timestamps = ["5", "15", "30", "60"] } = req.body; const video = await storage.getVideo(videoId); if (!video) { return res.status(404).json({ message: "Video not found" }); } const thumbnailPaths = await thumbnailGenerator.generateMultipleThumbnails(videoId, timestamps); const thumbnails = thumbnailPaths.map((path, index) => ({ timestamp: timestamps[index], url: `/thumbnail/${videoId}?t=${timestamps[index]}`, path: path })); res.json({ thumbnails }); } catch (error) { console.error("Error generating thumbnails:", error); res.status(500).json({ message: "Error generating thumbnails" }); } }); // API endpoint to list existing thumbnails for a video app.get("/api/thumbnails/:videoId", async (req, res) => { try { const { videoId } = req.params; const thumbnailPaths = thumbnailGenerator.listThumbnails(videoId); const thumbnails = thumbnailPaths.map(path => { const filename = require('path').basename(path); const match = filename.match(new RegExp(`${videoId}_(.+)_\\d+x\\d+\\.jpg`)); const timestamp = match ? match[1].replace(/-/g, ':') : 'unknown'; return { timestamp, url: `/thumbnail/${videoId}?t=${timestamp}`, path: path }; }); res.json({ thumbnails }); } catch (error) { console.error("Error listing thumbnails:", error); res.status(500).json({ message: "Error listing thumbnails" }); } }); // User routes app.get("/api/users/:id", async (req, res) => { try { const user = await storage.getUser(req.params.id); if (!user) { return res.status(404).json({ message: "User not found" }); } // Don't send password hash const { passwordHash, ...safeUser } = user; res.json(safeUser); } catch (error) { res.status(500).json({ message: "Failed to fetch user" }); } }); app.post("/api/users", async (req, res) => { try { const userData = req.body; const user = await storage.createUser(userData); const { passwordHash, ...safeUser } = user; res.status(201).json(safeUser); } catch (error) { res.status(500).json({ message: "Failed to create user" }); } }); // Playlist routes app.get("/api/playlists", async (req, res) => { try { const userId = req.query.userId as string; const limit = parseInt(req.query.limit as string) || 20; const offset = parseInt(req.query.offset as string) || 0; if (!userId) { return res.status(400).json({ message: "userId is required" }); } const playlists = await storage.getPlaylists(userId, limit, offset); res.json(playlists); } catch (error) { res.status(500).json({ message: "Failed to fetch playlists" }); } }); app.get("/api/playlists/:id", async (req, res) => { try { const userId = req.query.userId as string; const playlist = await storage.getPlaylist(req.params.id, userId); if (!playlist) { return res.status(404).json({ message: "Playlist not found" }); } res.json(playlist); } catch (error) { res.status(500).json({ message: "Failed to fetch playlist" }); } }); app.post("/api/playlists", async (req, res) => { try { const playlist = await storage.createPlaylist(req.body); res.status(201).json(playlist); } catch (error) { res.status(500).json({ message: "Failed to create playlist" }); } }); app.put("/api/playlists/:id", async (req, res) => { try { const userId = req.query.userId as string; if (!userId) { return res.status(400).json({ message: "userId is required" }); } const playlist = await storage.updatePlaylist(req.params.id, req.body, userId); if (!playlist) { return res.status(404).json({ message: "Playlist not found or access denied" }); } res.json(playlist); } catch (error) { res.status(500).json({ message: "Failed to update playlist" }); } }); app.delete("/api/playlists/:id", async (req, res) => { try { const userId = req.query.userId as string; if (!userId) { return res.status(400).json({ message: "userId is required" }); } const success = await storage.deletePlaylist(req.params.id, userId); if (!success) { return res.status(404).json({ message: "Playlist not found or access denied" }); } res.json({ success: true }); } catch (error) { res.status(500).json({ message: "Failed to delete playlist" }); } }); // Playlist videos routes app.get("/api/playlists/:id/videos", async (req, res) => { try { const videos = await storage.getPlaylistVideos(req.params.id); res.json(videos); } catch (error) { res.status(500).json({ message: "Failed to fetch playlist videos" }); } }); app.post("/api/playlists/:id/videos", async (req, res) => { try { const playlistVideo = await storage.addVideoToPlaylist({ playlistId: req.params.id, videoId: req.body.videoId, position: req.body.position || 0 }); res.status(201).json(playlistVideo); } catch (error) { if (error instanceof Error && error.message.includes("already in playlist")) { return res.status(409).json({ message: error.message }); } res.status(500).json({ message: "Failed to add video to playlist" }); } }); app.delete("/api/playlists/:playlistId/videos/:videoId", async (req, res) => { try { const success = await storage.removeVideoFromPlaylist(req.params.playlistId, req.params.videoId); if (!success) { return res.status(404).json({ message: "Video not found in playlist" }); } res.json({ success: true }); } catch (error) { res.status(500).json({ message: "Failed to remove video from playlist" }); } }); // Favorites routes app.get("/api/favorites", async (req, res) => { try { const userId = req.query.userId as string; const limit = parseInt(req.query.limit as string) || 20; const offset = parseInt(req.query.offset as string) || 0; if (!userId) { return res.status(400).json({ message: "userId is required" }); } const favorites = await storage.getUserFavorites(userId, limit, offset); res.json(favorites); } catch (error) { res.status(500).json({ message: "Failed to fetch favorites" }); } }); app.post("/api/favorites", async (req, res) => { try { const favorite = await storage.addToFavorites({ userId: req.body.userId, videoId: req.body.videoId }); res.status(201).json(favorite); } catch (error) { if (error instanceof Error && error.message.includes("already in favorites")) { return res.status(409).json({ message: error.message }); } res.status(500).json({ message: "Failed to add to favorites" }); } }); app.delete("/api/favorites/:userId/:videoId", async (req, res) => { try { const success = await storage.removeFromFavorites(req.params.userId, req.params.videoId); if (!success) { return res.status(404).json({ message: "Favorite not found" }); } res.json({ success: true }); } catch (error) { res.status(500).json({ message: "Failed to remove from favorites" }); } }); app.get("/api/favorites/:userId/:videoId", async (req, res) => { try { const isFavorited = await storage.isVideoFavorited(req.params.userId, req.params.videoId); res.json({ isFavorited }); } catch (error) { res.status(500).json({ message: "Failed to check favorite status" }); } }); const httpServer = createServer(app); return httpServer; }