videofolxtv/server/routes.ts
sebastjanartic 60cf545f79 Provide users with advanced options for video thumbnail generation
Adds FFmpeg/ImageMagick integration for dynamic thumbnail creation with API endpoints.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 50814a1e-92e4-4968-856f-7bc7eedf5e8f
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/50814a1e-92e4-4968-856f-7bc7eedf5e8f/L923Cjb
2025-08-04 19:51:11 +00:00

253 lines
9.1 KiB
TypeScript

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<Server> {
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 = `
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>${video.title} - VideoStream</title>
<meta name="description" content="${video.description || 'Oglej si ta video na VideoStream'}" />
<!-- Open Graph Meta Tags for Facebook -->
<meta property="og:title" content="${video.title}" />
<meta property="og:description" content="${video.description || 'Oglej si ta video na VideoStream'}" />
<meta property="og:type" content="video.other" />
<meta property="og:url" content="${shareUrl}" />
<meta property="og:image" content="${video.thumbnailUrl}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:site_name" content="VideoStream" />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${video.title}" />
<meta name="twitter:description" content="${video.description || 'Oglej si ta video na VideoStream'}" />
<meta name="twitter:image" content="${video.thumbnailUrl}" />
<script>
// Redirect to client-side app
window.location.href = '/';
setTimeout(() => {
window.location.href = '/video/${video.id}';
}, 100);
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js"></script>
</body>
</html>`;
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 = `
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e40af;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1e3a8a;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="400" height="225" fill="url(#grad)" />
<!-- Play button circle -->
<circle cx="200" cy="112" r="35" fill="rgba(255,255,255,0.9)" />
<!-- Play button triangle -->
<polygon points="188,97 188,127 218,112" fill="#1e40af" />
<!-- Title background -->
<rect x="0" y="175" width="400" height="50" fill="rgba(0,0,0,0.7)" />
<!-- Title text -->
<text x="20" y="200" fill="white" font-family="Arial, sans-serif" font-size="14" font-weight="bold">${displayTitle}</text>
<!-- Duration -->
${duration ? `<text x="370" y="200" fill="white" font-family="Arial, sans-serif" font-size="12" text-anchor="end">${duration}</text>` : ''}
<!-- Time indicator -->
<text x="20" y="25" fill="rgba(255,255,255,0.8)" font-family="Arial, sans-serif" font-size="12">t=${timeStamp}s</text>
</svg>
`;
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" });
}
});
const httpServer = createServer(app);
return httpServer;
}