Implement FFmpeg to generate video thumbnails and add thumbnail caching. 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/1majdC5
182 lines
6.6 KiB
TypeScript
182 lines
6.6 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";
|
|
|
|
export async function registerRoutes(app: Express): Promise<Server> {
|
|
// 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 thumbnails from Bunny.net videos using FFmpeg
|
|
app.get("/thumbnail/:videoId", async (req, res) => {
|
|
try {
|
|
const { videoId } = req.params;
|
|
const { exec } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Get video URL from Bunny.net
|
|
const video = await storage.getVideo(videoId);
|
|
if (!video) {
|
|
return res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
|
|
}
|
|
|
|
// Create thumbnails directory if it doesn't exist
|
|
const thumbnailDir = path.join(process.cwd(), 'thumbnails');
|
|
if (!fs.existsSync(thumbnailDir)) {
|
|
fs.mkdirSync(thumbnailDir, { recursive: true });
|
|
}
|
|
|
|
const thumbnailPath = path.join(thumbnailDir, `${videoId}.jpg`);
|
|
|
|
// Check if thumbnail already exists
|
|
if (fs.existsSync(thumbnailPath)) {
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
return res.sendFile(thumbnailPath);
|
|
}
|
|
|
|
// Generate video URL for FFmpeg (use direct CDN URL without iframe)
|
|
const directVideoUrl = `https://vz-7982dfc4-cc8.b-cdn.net/${videoId}/playlist.m3u8`;
|
|
|
|
// Generate thumbnail using FFmpeg
|
|
const ffmpegCommand = `ffmpeg -i "${directVideoUrl}" -ss 00:00:05 -vframes 1 -vf "scale=400:225" "${thumbnailPath}" -y`;
|
|
|
|
exec(ffmpegCommand, (error: any, stdout: any, stderr: any) => {
|
|
if (error) {
|
|
console.error(`FFmpeg error: ${error.message}`);
|
|
// Fallback to placeholder
|
|
return res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
|
|
}
|
|
|
|
if (fs.existsSync(thumbnailPath)) {
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
res.sendFile(thumbnailPath);
|
|
} else {
|
|
// Fallback if thumbnail generation failed
|
|
res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("Error generating thumbnail:", error);
|
|
res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
|
|
}
|
|
});
|
|
|
|
const httpServer = createServer(app);
|
|
return httpServer;
|
|
}
|