Restored to '551aaee106c3dc6c34e10193d8683fa67e7f9bea'
Replit-Restored-To: 551aaee106
This commit is contained in:
@@ -2,11 +2,8 @@ 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 {
|
||||
@@ -120,338 +117,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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:#374151;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1f2937;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow">
|
||||
<feDropShadow dx="2" dy="2" stdDeviation="3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="400" height="225" fill="url(#grad)" />
|
||||
|
||||
<!-- Video camera icon background -->
|
||||
<circle cx="120" cy="80" r="25" fill="rgba(255,255,255,0.1)" />
|
||||
<!-- Camera body -->
|
||||
<rect x="108" y="72" width="18" height="12" rx="2" fill="rgba(255,255,255,0.6)" />
|
||||
<!-- Camera lens -->
|
||||
<circle cx="114" cy="78" r="3" fill="#374151" />
|
||||
<!-- Camera viewfinder -->
|
||||
<rect x="126" y="75" width="6" height="4" fill="rgba(255,255,255,0.6)" />
|
||||
|
||||
<!-- Play button circle -->
|
||||
<circle cx="200" cy="112" r="35" fill="rgba(255,255,255,0.9)" filter="url(#shadow)" />
|
||||
<!-- Play button triangle -->
|
||||
<polygon points="188,97 188,127 218,112" fill="#374151" />
|
||||
|
||||
<!-- Title background -->
|
||||
<rect x="0" y="175" width="400" height="50" fill="rgba(0,0,0,0.8)" />
|
||||
|
||||
<!-- Title text -->
|
||||
<text x="20" y="200" fill="white" font-family="Arial, sans-serif" font-size="14" font-weight="bold">${displayTitle}</text>
|
||||
|
||||
<!-- Duration -->
|
||||
${duration ? `<rect x="350" y="185" width="35" height="20" rx="3" fill="rgba(0,0,0,0.8)"/>
|
||||
<text x="367" y="198" fill="white" font-family="Arial, sans-serif" font-size="11" text-anchor="middle">${duration}</text>` : ''}
|
||||
|
||||
<!-- Video type indicator -->
|
||||
<text x="20" y="25" fill="rgba(255,255,255,0.7)" font-family="Arial, sans-serif" font-size="11">VIDEO</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" });
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user