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
This commit is contained in:
+113
-51
@@ -3,8 +3,10 @@ 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 {
|
||||
@@ -118,70 +120,130 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Create SVG thumbnails with video title and play button
|
||||
// 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) {
|
||||
const fallbackSvg = `
|
||||
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="225" fill="#1a1a1a"/>
|
||||
<text x="200" y="112" text-anchor="middle" fill="white" font-family="Arial" font-size="16">Video not found</text>
|
||||
</svg>
|
||||
`;
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
return res.send(fallbackSvg);
|
||||
return res.status(404).json({ message: "Video not found" });
|
||||
}
|
||||
|
||||
// Clean up title for display
|
||||
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') : '';
|
||||
// Try to generate real thumbnail from video
|
||||
const thumbnailPath = await thumbnailGenerator.generateThumbnail({
|
||||
videoId,
|
||||
timeStamp,
|
||||
width: 400,
|
||||
height: 225,
|
||||
quality: 85
|
||||
});
|
||||
|
||||
// Create SVG thumbnail with play button
|
||||
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>` : ''}
|
||||
</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', '*');
|
||||
res.send(svg);
|
||||
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);
|
||||
const errorSvg = `
|
||||
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="225" fill="#dc2626"/>
|
||||
<text x="200" y="112" text-anchor="middle" fill="white" font-family="Arial" font-size="16">Error loading thumbnail</text>
|
||||
</svg>
|
||||
`;
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
res.send(errorSvg);
|
||||
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" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user