import type { Express, Request, Response } from "express"; import { createServer, type Server } from "http"; import express from "express"; import compression from "compression"; import { storage } from "./storage"; import { z } from "zod"; import { updateVideoSchema, insertVideoSchema, insertUserSchema, insertVideoUploadSchema, insertCategorySchema, insertTagSchema, type User, type VideoUpload } from "@shared/schema"; import multer from "multer"; import { randomUUID } from "crypto"; import path from "path"; import fs from "fs"; import session from "express-session"; import sharp from "sharp"; import fetch from "node-fetch"; import { setupAuth, isAuthenticated, isAdmin } from "./replitAuth"; import { ObjectStorageService, ObjectNotFoundError } from "./objectStorage"; import { generateVideoDescription, generateBulkDescriptions } from "./aiService"; // Extract unique artist names from video titles function extractArtists(videos: any[]): { name: string; videoCount: number; videos: any[] }[] { const artistMap = new Map(); for (const v of videos) { const title = v.title || ''; let artist = ''; if (title.includes('–')) { artist = title.split('–')[0].trim(); } else if (title.includes(' - ')) { artist = title.split(' - ')[0].trim(); } if (artist && !artist.toUpperCase().startsWith('FOLX')) { const normalized = artist.replace(/\s+/g, ' ').trim(); if (!artistMap.has(normalized)) { artistMap.set(normalized, []); } artistMap.get(normalized)!.push(v); } } return Array.from(artistMap.entries()) .map(([name, vids]) => ({ name, videoCount: vids.length, videos: vids })) .sort((a, b) => a.name.localeCompare(b.name, 'de')); } // Find video by short or long ID - moved to top level for export export async function findVideoByAnyId(id: string) { try { // If it's already a full UUID, use it directly if (id.length === 36 && id.includes('-')) { return await storage.getVideo(id); } // If it's an 8-character short ID, find by short ID if (id.length === 8) { const allVideos = await storage.getVideos(600, 0); return allVideos.find(v => v.id.replace(/-/g, '').substring(0, 8) === id); } return undefined; } catch (error) { console.error(`Error finding video by ID ${id}:`, error); return undefined; } } // Extend express session declare module "express-session" { interface SessionData { userId: string; } } // Configure multer for video uploads const upload = multer({ storage: multer.diskStorage({ destination: './uploads/videos', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }), limits: { fileSize: 500 * 1024 * 1024, // 500MB max file size }, fileFilter: (req, file, cb) => { // Allow video files only if (file.mimetype.startsWith('video/')) { cb(null, true); } else { cb(new Error('Only video files are allowed')); } } }); // Simple session-based authentication middleware const authenticate = (req: Request, res: Response, next: any) => { if (req.session?.userId) { next(); } else { res.status(401).json({ message: "Authentication required" }); } }; export async function registerRoutes(app: Express): Promise { // Setup Replit Auth first await setupAuth(app); // Social media crawler detection middleware for /video/:id routes // This serves proper OG meta tags to crawlers while letting regular users get the SPA app.get('/video/:id', async (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; // List of social media crawler user agents const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) { // Not a crawler, let the SPA handle it return next(); } try { const { id } = req.params; // Find video from cache const allVideos = await storage.getVideos(600, 0); let video; if (id.length === 36 && id.includes('-')) { video = allVideos.find(v => v.id === id); } else if (id.length === 8) { video = allVideos.find(v => v.id.replace(/-/g, '').substring(0, 8) === id); } else { video = allVideos.find(v => v.id.includes(id)); } if (!video) { return next(); // Let SPA handle 404 } const baseUrl = 'https://video.folx.tv'; const videoUrl = `${baseUrl}/video/${video.id}`; // Get high-quality thumbnail for sharing (1200x630 is ideal for Facebook) let thumbnailUrl = `${baseUrl}/api/social-image`; if (video.thumbnailUrl) { thumbnailUrl = video.thumbnailUrl .replace(/width=\d+/gi, 'width=1200') .replace(/height=\d+/gi, 'height=630') .replace(/format=webp/gi, 'format=jpg'); } // Extract artist name from title (format: "Artist – Song" or "Artist - Song") const titleStr = video.title || ''; let artistName = ''; let songName = titleStr; if (titleStr.includes('–')) { const parts = titleStr.split('–'); artistName = parts[0].trim(); songName = parts.slice(1).join('–').trim(); } else if (titleStr.includes(' - ')) { const parts = titleStr.split(' - '); artistName = parts[0].trim(); songName = parts.slice(1).join(' - ').trim(); } // Clean description for meta tags const description = video.description ? video.description.substring(0, 200).replace(/[<>"']/g, '') : artistName ? `${artistName} – ${songName}. Jetzt ansehen auf Folx TV - Nummer 1 in Europa für Volksmusik und Schlager.` : `${titleStr} auf Folx TV ansehen - Nummer 1 in Europa für Volksmusik und Schlager.`; const title = video.title || 'Folx TV - Video'; const keywords = artistName ? `${artistName}, ${songName}, Volksmusik, Schlager, Folx TV, Musikvideo` : `${titleStr}, Volksmusik, Schlager, Folx TV`; // JSON-LD structured data for video with artist const jsonLd: any = { "@context": "https://schema.org", "@type": "VideoObject", "name": title, "description": description, "thumbnailUrl": thumbnailUrl, "uploadDate": video.createdAt ? new Date(video.createdAt).toISOString() : new Date().toISOString(), "duration": video.duration ? `PT${Math.floor(video.duration / 60)}M${video.duration % 60}S` : undefined, "contentUrl": videoUrl, "embedUrl": videoUrl, "interactionStatistic": { "@type": "InteractionCounter", "interactionType": "https://schema.org/WatchAction", "userInteractionCount": video.views || 0 }, "publisher": { "@type": "Organization", "name": "FOLX.TV", "url": "https://folx.tv" }, "inLanguage": "de" }; if (artistName) { jsonLd["byArtist"] = { "@type": "MusicGroup", "name": artistName }; jsonLd["@type"] = "MusicVideoObject"; } // Return HTML page with OG tags for crawlers const html = ` ${title} | Folx TV - Video

${title}

${artistName ? `

Interpret: ${artistName}

` : ''}

${description}

Jetzt ansehen auf Folx TV

`; res.set('Content-Type', 'text/html'); res.send(html); } catch (error) { console.error('Error generating OG tags for crawler:', error); next(); // Let SPA handle errors } }); // Add compression middleware for better performance app.use(compression({ level: 6, threshold: 1024, // Only compress responses larger than 1KB filter: (req: any, res: any) => { // Don't compress video files if (req.headers['accept']?.includes('video/')) { return false; } return compression.filter(req, res); } })); // Configure session middleware app.use(session({ secret: process.env.SESSION_SECRET || 'dev-secret-key', resave: false, saveUninitialized: false, cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24 hours })); // Authentication Routes app.post("/api/auth/register", async (req, res) => { try { const userData = insertUserSchema.parse(req.body); // Check if user already exists const existingUser = await storage.getUserByEmail(userData.email); if (existingUser) { return res.status(400).json({ message: "User already exists with this email" }); } const user = await storage.createUser(userData); // Remove password from response const { passwordHash, ...userResponse } = user; res.status(201).json(userResponse); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid user data", errors: error.errors }); } res.status(500).json({ message: "Failed to create user" }); } }); app.post("/api/auth/login", async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: "Email and password are required" }); } const user = await storage.validateUserPassword(email, password); if (!user) { return res.status(401).json({ message: "Invalid credentials" }); } // Set session req.session.userId = user.id; // Remove password from response const { passwordHash, ...userResponse } = user; res.json(userResponse); } catch (error) { res.status(500).json({ message: "Failed to login" }); } }); app.post("/api/auth/logout", (req, res) => { req.session?.destroy(() => { res.json({ message: "Logged out successfully" }); }); }); // Server-side meta tags for /player page (crawlers + SEO) app.get('/player', (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) { return next(); } const baseUrl = 'https://video.folx.tv'; const pageUrl = `${baseUrl}/player`; const title = 'Professional Player - Folx TV - Video'; const description = 'Professioneller Video Player mit Overlay Graphics und Streaming-Funktionen. Folx TV - Nummer 1 in Europa für Volksmusik und Schlager.'; const imageUrl = `${baseUrl}/images/logo.svg`; const html = ` ${title}

${title}

${description}

Zum Player auf video.folx.tv

`; res.set('Content-Type', 'text/html'); res.send(html); }); // Server-side meta tags for /live page (crawlers + SEO) app.get('/live', (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) { return next(); } const baseUrl = 'https://video.folx.tv'; const pageUrl = `${baseUrl}/live`; const title = 'LIVE Stream - Folx TV - Video'; const description = 'Live Stream auf video.folx.tv - Schauen Sie exklusive Inhalte in Echtzeit. Folx TV - Nummer 1 in Europa für Volksmusik und Schlager.'; const imageUrl = `${baseUrl}/images/logo.svg`; const html = ` ${title}

${title}

${description}

Zum Live Stream auf video.folx.tv

`; res.set('Content-Type', 'text/html'); res.send(html); }); // API endpoint for artists list app.get('/api/artists', async (req, res) => { try { const allVideos = await storage.getVideos(600, 0); const artists = extractArtists(allVideos); res.json({ artists: artists.map(a => ({ name: a.name, videoCount: a.videoCount })), total: artists.length }); } catch (error) { res.status(500).json({ message: 'Error fetching artists' }); } }); // Server-side meta tags for /kuenstler page (crawlers + SEO) app.get('/kuenstler', async (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) return next(); const baseUrl = 'https://video.folx.tv'; const pageUrl = `${baseUrl}/kuenstler`; const allVideos = await storage.getVideos(600, 0); const artists = extractArtists(allVideos); const topArtists = artists.slice(0, 30).map(a => a.name).join(', '); const title = `Alle Künstler & Interpreten (${artists.length}) | Folx TV - Video`; const description = `Entdecken Sie ${artists.length} Volksmusik- und Schlager-Künstler auf Folx TV: ${topArtists} und viele mehr. Nummer 1 in Europa für Volksmusik und Schlager.`; const keywords = artists.map(a => a.name).join(', ') + ', Volksmusik, Schlager, Folx TV'; const escapeHtml = (s: string) => s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const jsonLd = { "@context": "https://schema.org", "@type": "CollectionPage", "name": title, "description": description, "url": pageUrl, "numberOfItems": artists.length, "publisher": { "@type": "Organization", "name": "FOLX.TV", "url": "https://folx.tv" }, "inLanguage": "de", "mainEntity": { "@type": "ItemList", "numberOfItems": artists.length, "itemListElement": artists.map((a, i) => ({ "@type": "ListItem", "position": i + 1, "item": { "@type": "MusicGroup", "name": a.name, "url": `${pageUrl}#letter-${a.name.charAt(0).toUpperCase()}` } })) } }; const html = ` ${escapeHtml(title)}

Alle Künstler & Interpreten auf Folx TV

${escapeHtml(description)}

Insgesamt ${artists.length} Künstler mit ${allVideos.length} Musikvideos.

`; res.set('Content-Type', 'text/html'); res.send(html); }); // Server-side meta tags for /folx-stadl page (crawlers + SEO) app.get('/folx-stadl', async (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) return next(); const baseUrl = 'https://video.folx.tv'; const pageUrl = `${baseUrl}/folx-stadl`; const title = 'FOLX STADL - Die große Volksmusik Show | Folx TV - Video'; const description = 'FOLX STADL - Die beliebte Volksmusik- und Schlagershow auf Folx TV. Mit Stars wie Angela Wiedl, Oswald Sattler, Die Grubertaler, Kastelruther Spatzen und vielen mehr. Jetzt alle Folgen ansehen!'; const keywords = 'FOLX STADL, Volksmusik Show, Schlager, Angela Wiedl, Oswald Sattler, Die Grubertaler, Kastelruther Spatzen, Volksmusik TV, Folx TV'; const allVideos = await storage.getVideos(600, 0); const stadlVideos = allVideos.filter(v => v.title.includes('FOLX STADL') || v.title.includes('FOLXSTADL')); const html = ` ${title}

${title}

${description}

Alle FOLX STADL Folgen (${stadlVideos.length} Videos)

`; res.set('Content-Type', 'text/html'); res.send(html); }); // Server-side meta tags for /geschichte-lied page (crawlers + SEO) app.get('/geschichte-lied', async (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) return next(); const baseUrl = 'https://video.folx.tv'; const pageUrl = `${baseUrl}/geschichte-lied`; const title = 'Die Geschichte des Liedes - Musikdokumentation | Folx TV - Video'; const description = 'Die Geschichte des Liedes - Entdecken Sie die Entstehung und Hintergründe der beliebtesten Volksmusik- und Schlagerlieder. Eine einzigartige Musikdokumentation auf Folx TV.'; const keywords = 'Geschichte des Liedes, Musikdokumentation, Volksmusik Geschichte, Schlager Historie, Liedgeschichte, Folx TV, Musiksendung, Kastelruther Spatzen, Monika Martin, Oswald Sattler, Angela Wiedl, Die Grubertaler'; const allVideos = await storage.getVideos(600, 0); const geschichteVideos = allVideos.filter(v => v.title.includes('Geschichte des Liedes')); const html = ` ${title}

${title}

${description}

Alle Folgen (${geschichteVideos.length} Videos)

`; res.set('Content-Type', 'text/html'); res.send(html); }); // Server-side meta tags for /gipfelstammtisch page (crawlers + SEO) app.get('/gipfelstammtisch', async (req, res, next) => { const userAgent = req.headers['user-agent']?.toLowerCase() || ''; const crawlers = [ 'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp', 'telegrambot', 'linkedinbot', 'pinterest', 'slackbot', 'viberbot', 'discordbot', 'applebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot' ]; const isCrawler = crawlers.some(crawler => userAgent.includes(crawler)); if (!isCrawler) return next(); const baseUrl = 'https://video.folx.tv'; const pageUrl = `${baseUrl}/gipfelstammtisch`; const title = 'Gipfelstammtisch - Volksmusik Talkshow | Folx TV - Video'; const description = 'Gipfelstammtisch - Die gemütliche Volksmusik-Talkshow auf Folx TV. Gespräche mit Stars der Volksmusik- und Schlagerszene in einzigartiger Alpenatmosphäre. Jetzt alle Folgen ansehen!'; const keywords = 'Gipfelstammtisch, Volksmusik Talkshow, Schlager Talk, Alpen, Volksmusik Stars, Folx TV, Musiksendung, Kastelruther Spatzen, Monika Martin, Oswald Sattler, Angela Wiedl, Michael Hirte'; const allVideos = await storage.getVideos(600, 0); const gipfelVideos = allVideos.filter(v => v.title.toLowerCase().includes('gipfelstammtisch')); const html = ` ${title}

${title}

${description}

Alle Folgen (${gipfelVideos.length} Videos)

`; res.set('Content-Type', 'text/html'); res.send(html); }); // Sitemap.xml for SEO app.get("/sitemap.xml", async (req, res) => { try { const baseUrl = "https://video.folx.tv"; const videos = await storage.getVideos(1000, 0); // Helper to escape XML special characters const escapeXml = (str: string) => { if (!str) return ''; return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; let xml = ` ${baseUrl}/ daily 1.0 ${baseUrl}/folx-stadl weekly 0.7 ${baseUrl}/geschichte-lied weekly 0.7 ${baseUrl}/gipfelstammtisch weekly 0.7 ${baseUrl}/kuenstler weekly 0.8 `; // Add artist letter-index anchors to sitemap const artists = extractArtists(videos); const artistLetters = Array.from(new Set(artists.map(a => a.name.charAt(0).toUpperCase()))).sort(); for (const letter of artistLetters) { xml += ` ${baseUrl}/kuenstler#letter-${escapeXml(letter)} weekly 0.6 `; } for (const video of videos) { const shortId = video.id.replace(/-/g, '').substring(0, 8); const lastmod = video.createdAt ? new Date(video.createdAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]; const safeTitle = escapeXml(video.title); const safeThumbnail = escapeXml(video.thumbnailUrl || ''); // Extract artist for richer description const titleStr = video.title || ''; let artist = ''; let song = titleStr; if (titleStr.includes('–')) { artist = titleStr.split('–')[0].trim(); song = titleStr.split('–').slice(1).join('–').trim(); } else if (titleStr.includes(' - ')) { artist = titleStr.split(' - ')[0].trim(); song = titleStr.split(' - ').slice(1).join(' - ').trim(); } const safeDescription = escapeXml( video.description || (artist ? `${artist} – ${song}. Volksmusik und Schlager auf Folx TV - Nummer 1 in Europa.` : `${titleStr} auf Folx TV ansehen.`) ); const safeTags = artist ? `${escapeXml(artist)}VolksmusikSchlagerFolx TV` : 'VolksmusikSchlagerFolx TV'; xml += ` ${baseUrl}/video/${shortId} ${lastmod} weekly 0.8 ${safeThumbnail} ${safeTitle} ${safeDescription} ${video.duration || 0} yes ${safeTags} `; } xml += ``; res.setHeader('Content-Type', 'application/xml'); res.setHeader('Cache-Control', 'public, max-age=3600'); res.send(xml); } catch (error) { console.error('Error generating sitemap:', error); res.status(500).send('Error generating sitemap'); } }); // Robots.txt app.get("/robots.txt", (req, res) => { const baseUrl = "https://video.folx.tv"; const robots = `User-agent: * Allow: / Disallow: /admin Disallow: /api/ Sitemap: ${baseUrl}/sitemap.xml `; res.setHeader('Content-Type', 'text/plain'); res.send(robots); }); app.get("/api/auth/me", authenticate, async (req, res) => { try { const user = await storage.getUser(req.session.userId!); if (!user) { return res.status(404).json({ message: "User not found" }); } const { passwordHash, ...userResponse } = user; res.json(userResponse); } catch (error) { res.status(500).json({ message: "Failed to fetch user" }); } }); // Video Routes // Bunny.net administration routes app.get("/api/bunny/stats", async (req, res) => { try { const videos = await storage.getVideos(1, 1000); const totalViews = videos.length > 0 ? videos.reduce((sum: number, video: any) => sum + video.views, 0) : 0; const stats = { totalVideos: videos.length, totalViews, totalStorage: 0, // Would need separate Bunny API call bandwidth: 0 // Would need separate Bunny API call }; res.json(stats); } catch (error) { console.error("Error fetching Bunny stats:", error); res.status(500).json({ message: "Failed to fetch statistics" }); } }); app.delete("/api/bunny/videos/:id", async (req, res) => { try { const { id } = req.params; // Note: This would need implementation in BunnyService // For now, return success (deletion would happen in Bunny dashboard) res.json({ message: "Video deletion initiated" }); } catch (error) { console.error("Error deleting video:", error); res.status(500).json({ message: "Failed to delete video" }); } }); // Manual sync endpoint to force refresh from Bunny.net app.post("/api/bunny/sync", async (req, res) => { try { console.log('🔄 Manual video sync requested...'); // Import videoSyncService to trigger manual sync const { videoSyncService } = await import('./videoSync'); await videoSyncService.initialize(); const videos = await storage.getVideos(1, 1000); res.json({ message: "Video sync completed successfully", totalVideos: videos.length, timestamp: new Date().toISOString() }); } catch (error) { console.error("Error during manual sync:", error); res.status(500).json({ message: "Failed to sync videos from Bunny.net" }); } }); 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; // Skip search for queries shorter than 2 characters for performance const searchQuery = search && search.length >= 2 ? search : undefined; // Create cache key for ETag const cacheKey = `videos-${limit}-${offset}-${searchQuery || 'all'}`; const etag = `"${cacheKey}"`; // Check if client has cached version if (req.headers['if-none-match'] === etag) { return res.status(304).end(); } // Set optimized cache headers res.set({ 'Cache-Control': 'public, max-age=120, stale-while-revalidate=300', // 2 min cache, 5 min stale 'ETag': etag, 'X-Content-Type-Options': 'nosniff', 'Vary': 'Accept-Encoding' }); console.log(`Fetching videos: limit=${limit}, offset=${offset}, search=${searchQuery}`); const videos = await storage.getVideos(limit, offset, searchQuery); const total = await storage.getVideoCount(searchQuery); console.log(`Returning ${videos.length} videos`); res.json({ videos, total, hasMore: offset + limit < total }); } catch (error) { console.error("Error fetching videos:", error); res.status(500).json({ message: "Failed to fetch videos" }); } }); // Face detection endpoint for thumbnails app.post("/api/videos/:id/analyze-face", async (req, res) => { try { const { id } = req.params; const video = await storage.getVideo(id); if (!video) { return res.status(404).json({ message: "Video not found" }); } if (!video.thumbnailUrl) { return res.status(400).json({ message: "Video has no thumbnail" }); } // Import smart thumbnail service (no external dependencies) const { smartThumbnailService } = await import("./smart-thumbnail-service"); const result = await smartThumbnailService.getOptimizedThumbnailInfo(video.thumbnailUrl); // Update video with face position data await storage.updateVideo(id, { faceCenterPosition: result.faceCenterPosition, facesDetected: result.facesDetected, faceConfidence: result.confidence }); res.json({ videoId: id, ...result }); } catch (error) { console.error("Error analyzing face in thumbnail:", error); res.status(500).json({ message: "Failed to analyze face" }); } }); // Batch face analysis for all videos app.post("/api/videos/batch-analyze-faces", async (req, res) => { try { // Get all videos that don't have face analysis yet const allVideos = await storage.getVideos(1000, 0); // Get up to 1000 videos const videosToProcess = allVideos.filter(video => !video.faceCenterPosition && video.thumbnailUrl); if (videosToProcess.length === 0) { return res.json({ message: "All videos already processed or no thumbnails available", processed: 0, total: allVideos.length }); } console.log(`Starting batch face analysis for ${videosToProcess.length} videos...`); // Import smart thumbnail service (no external dependencies) const { smartThumbnailService } = await import("./smart-thumbnail-service"); let processed = 0; let failed = 0; // Process videos in batches to avoid overwhelming the system const batchSize = 5; for (let i = 0; i < videosToProcess.length; i += batchSize) { const batch = videosToProcess.slice(i, i + batchSize); await Promise.all(batch.map(async (video) => { try { console.log(`Processing face detection for video: ${video.title} (${video.id})`); const result = await smartThumbnailService.getOptimizedThumbnailInfo(video.thumbnailUrl); await storage.updateVideo(video.id, { faceCenterPosition: result.faceCenterPosition, facesDetected: result.facesDetected, faceConfidence: result.confidence }); processed++; console.log(`✅ Face analysis completed for ${video.title} - Faces detected: ${result.facesDetected}`); } catch (error) { console.error(`❌ Face analysis failed for ${video.title}:`, error); failed++; } })); // Small delay between batches to be respectful if (i + batchSize < videosToProcess.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } console.log(`Batch face analysis completed: ${processed} processed, ${failed} failed`); res.json({ message: "Batch face analysis completed", processed, failed, total: videosToProcess.length }); } catch (error) { console.error("Error in batch face analysis:", error); res.status(500).json({ message: "Failed to perform batch face analysis" }); } }); // Generate short ID from long UUID function generateShortId(longId: string): string { // Take first 8 characters and remove dashes for shorter, cleaner URLs return longId.replace(/-/g, '').substring(0, 8); } // Get single video by ID (supports both short and long IDs) app.get("/api/videos/:id", async (req, res) => { try { const video = await findVideoByAnyId(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" }); } }); // Get video ads/spots metadata from Bunny.net app.get("/api/videos/:id/ads", async (req, res) => { try { const { id } = req.params; // Check if video exists first (supports short and long IDs) const video = await findVideoByAnyId(id); if (!video) { return res.status(404).json({ message: "Video not found" }); } // Get ads from Bunny.net API let ads: any[] = []; try { const { BunnyService } = await import("./bunny"); const bunnyService = new BunnyService(); ads = await bunnyService.getVideoAds(video.id); // Use full ID for API calls console.log(`Retrieved ${ads.length} ad spots for video ${id}`); } catch (error) { console.error(`Failed to get ads from Bunny.net for video ${id}:`, error); // Return empty array if Bunny service fails ads = []; } res.json({ videoId: id, ads: ads, totalAds: ads.length }); } catch (error) { console.error(`Error fetching ads for video ${req.params.id}:`, error); res.status(500).json({ message: "Failed to fetch video ads" }); } }); // Update video views (supports short and long IDs) app.post("/api/videos/:id/view", async (req, res) => { try { const video = await findVideoByAnyId(req.params.id); if (!video) { return res.status(404).json({ message: "Video not found" }); } // Use the full video ID for storage operations await storage.updateVideoViews(video.id); res.json({ success: true }); } catch (error) { res.status(500).json({ message: "Failed to update views" }); } }); // Create new video app.post("/api/videos", authenticate, async (req, res) => { try { const videoData = insertVideoSchema.parse(req.body); const video = await storage.createVideo(videoData); res.status(201).json(video); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid video data", errors: error.errors }); } res.status(500).json({ message: "Failed to create video" }); } }); // Update video metadata (title, description, etc.) app.patch("/api/videos/:id", authenticate, async (req, res) => { try { const updates = updateVideoSchema.parse(req.body); const video = await storage.updateVideo(req.params.id, updates); if (!video) { return res.status(404).json({ message: "Video not found" }); } res.json(video); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid request data", errors: error.errors }); } res.status(500).json({ message: "Failed to update video" }); } }); // Delete video app.delete("/api/videos/:id", authenticate, async (req, res) => { try { const success = await storage.deleteVideo(req.params.id); if (!success) { return res.status(404).json({ message: "Video not found" }); } res.json({ success: true, message: "Video deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete video" }); } }); // Video Upload Routes app.post("/api/uploads/start", authenticate, async (req, res) => { try { const { originalFileName, fileSize, mimeType } = req.body; if (!originalFileName || !fileSize || !mimeType) { return res.status(400).json({ message: "originalFileName, fileSize, and mimeType are required" }); } const uploadData = { userId: req.session.userId!, originalFileName, fileSize: parseInt(fileSize), mimeType, uploadStatus: "uploading" as const, uploadProgress: 0 }; const upload = await storage.createVideoUpload(uploadData); res.status(201).json(upload); } catch (error) { res.status(500).json({ message: "Failed to initialize upload" }); } }); app.post("/api/uploads/:id/video", authenticate, upload.single('video'), async (req, res) => { try { const uploadId = req.params.id; const file = req.file; if (!file) { return res.status(400).json({ message: "No video file provided" }); } // Update upload with file information await storage.updateVideoUpload(uploadId, { uploadStatus: "processing", uploadProgress: 1.0 }); // Create video record const videoData = { title: req.body.title || path.parse(file.originalname).name, description: req.body.description || "", thumbnailUrl: req.body.thumbnailUrl || "https://via.placeholder.com/800x450", videoUrl: `/uploads/videos/${file.filename}`, duration: parseInt(req.body.duration) || 0, views: 0, category: req.body.category || "", tags: req.body.tags ? JSON.parse(req.body.tags) : [], isPublic: req.body.isPublic !== "false", uploadStatus: "completed", originalFileName: file.originalname, fileSize: file.size, format: path.extname(file.originalname).slice(1) }; const video = await storage.createVideo(videoData); // Link video to upload await storage.updateVideoUpload(uploadId, { videoId: video.id, uploadStatus: "completed" }); res.json({ video, upload: { id: uploadId, status: "completed" } }); } catch (error) { console.error("Upload error:", error); // Update upload status to failed if (req.params.id) { await storage.updateVideoUpload(req.params.id, { uploadStatus: "failed", errorMessage: error instanceof Error ? error.message : "Upload failed" }); } res.status(500).json({ message: "Failed to upload video" }); } }); app.get("/api/uploads/:id/status", authenticate, async (req, res) => { try { const upload = await storage.getVideoUpload(req.params.id); if (!upload) { return res.status(404).json({ message: "Upload not found" }); } res.json(upload); } catch (error) { res.status(500).json({ message: "Failed to get upload status" }); } }); app.get("/api/uploads/user", authenticate, async (req, res) => { try { const uploads = await storage.getUserVideoUploads(req.session.userId!); res.json(uploads); } catch (error) { res.status(500).json({ message: "Failed to fetch user uploads" }); } }); // Category Routes app.get("/api/categories", async (req, res) => { try { const categories = await storage.getCategories(); res.json(categories); } catch (error) { res.status(500).json({ message: "Failed to fetch categories" }); } }); app.post("/api/categories", authenticate, async (req, res) => { try { const categoryData = insertCategorySchema.parse(req.body); const category = await storage.createCategory(categoryData); res.status(201).json(category); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid category data", errors: error.errors }); } res.status(500).json({ message: "Failed to create category" }); } }); app.patch("/api/categories/:id", authenticate, async (req, res) => { try { const updates = insertCategorySchema.partial().parse(req.body); const category = await storage.updateCategory(req.params.id, updates); if (!category) { return res.status(404).json({ message: "Category not found" }); } res.json(category); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid category data", errors: error.errors }); } res.status(500).json({ message: "Failed to update category" }); } }); app.delete("/api/categories/:id", authenticate, async (req, res) => { try { const success = await storage.deleteCategory(req.params.id); if (!success) { return res.status(404).json({ message: "Category not found" }); } res.json({ success: true, message: "Category deleted successfully" }); } catch (error) { res.status(500).json({ message: "Failed to delete category" }); } }); // Tag Routes app.get("/api/tags", async (req, res) => { try { const limit = parseInt(req.query.limit as string) || 50; const popular = req.query.popular === "true"; const tags = popular ? await storage.getPopularTags(limit) : await storage.getTags(); res.json(tags); } catch (error) { res.status(500).json({ message: "Failed to fetch tags" }); } }); app.post("/api/tags", authenticate, async (req, res) => { try { const tagData = insertTagSchema.parse(req.body); const tag = await storage.createTag(tagData); res.status(201).json(tag); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ message: "Invalid tag data", errors: error.errors }); } res.status(500).json({ message: "Failed to create tag" }); } }); // Serve uploaded videos app.use('/uploads', express.static('uploads')); // Serve ads.txt file specifically app.get("/ads.txt", (req, res) => { res.type("text/plain"); res.sendFile(path.join(__dirname, "../client/public/ads.txt")); }); // Open Graph image generation endpoint app.get('/api/og-image', (req, res) => { try { // Generate SVG-based image for Open Graph const svg = ` FOLX.TV Professional Video Streaming Platform Geschichte des Liedes • FOLX STADL • Premium Content `; res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours res.send(svg); } catch (error) { console.error('Error generating OG image:', error); res.status(500).send('Error generating image'); } }); // Custom social image endpoint - directly serve the beautiful triangular image app.get('/api/social-image', async (req, res) => { try { console.log('📸 Generating beautiful social image...'); // Create the beautiful triangular design directly const width = 1200; const height = 630; const svg = ` FOLX.TV video.folx.tv Amazing Content • Premium Streaming Platform `; const buffer = await sharp(Buffer.from(svg)) .png({ quality: 95, compressionLevel: 6, progressive: true }) .toBuffer(); console.log(`📸 Beautiful social image generated: ${buffer.length} bytes`); // Set aggressive cache-busting headers for social media platforms res.set({ 'Content-Type': 'image/png', 'Content-Length': buffer.length.toString(), 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', 'Pragma': 'no-cache', 'Expires': '0', 'Last-Modified': new Date().toUTCString(), 'ETag': `W/"${buffer.length}-${Date.now()}"`, 'Access-Control-Allow-Origin': '*', 'Vary': 'User-Agent' }); res.send(buffer); } catch (error) { console.error('❌ Error generating social image:', error); res.status(500).send('Error generating social image'); } }); // Favicon generation endpoint app.get('/api/favicon', async (req, res) => { try { const size = req.query.size ? parseInt(req.query.size as string) : 32; const format = req.query.format as string || 'svg'; const padding = Math.max(2, size * 0.1); const logoSize = size - (padding * 2); const cornerRadius = Math.max(2, size * 0.15); if (format === 'png') { // Ustvarimo PNG ikono za iOS PWA // Ustvarimo SVG za pretvorbo v PNG const svg = ` `; // Pretvorimo SVG v PNG z Sharp const pngBuffer = await sharp(Buffer.from(svg)) .png({ quality: 100, compressionLevel: 0 }) .toBuffer(); res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours res.send(pngBuffer); } else { // Originalni SVG favicon const svg = ` `; res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours res.send(svg); } } catch (error) { console.error('Error generating favicon:', error); res.status(500).send('Error generating favicon'); } }); // Social media thumbnail generation endpoint app.get('/api/video-thumbnail/:videoId', async (req, res) => { try { const { videoId } = req.params; const video = await storage.getVideo(videoId); if (!video) { console.log(`❌ Video not found: ${videoId}`); return res.status(404).send('Video not found'); } console.log(`🖼️ Generating thumbnail for: ${video.title}`); // Uporabimo čisti thumbnail iz Bunny.net - prioriteta if (video.thumbnailUrl) { try { console.log(`📥 Fetching thumbnail from Bunny.net: ${video.thumbnailUrl}`); const response = await fetch(video.thumbnailUrl); if (response.ok) { const thumbnailBuffer = await response.arrayBuffer(); // Optimiziramo thumbnail za social media brez overlay-a const resizedBuffer = await sharp(Buffer.from(thumbnailBuffer)) .resize(1200, 630, { fit: 'cover', position: 'center' }) .jpeg({ quality: 85, progressive: true }) // JPEG je bolje za photo thumbnails .toBuffer(); console.log(`✅ Real thumbnail processed: ${resizedBuffer.length} bytes`); res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=7200'); // Cache za 2 uri res.setHeader('Access-Control-Allow-Origin', '*'); return res.send(resizedBuffer); } } catch (fetchError) { console.log('⚠️ Failed to fetch real thumbnail, falling back to generated:', fetchError); } } // Fallback: ustvarimo generirani thumbnail z video informacijami const width = 1200; const height = 630; // Facebook/Twitter optimalne dimenzije const svg = ` ${video.title.length > 35 ? video.title.substring(0, 32) + '...' : video.title} ${video.description && video.description.length > 60 ? video.description.substring(0, 57) + '...' : video.description || 'Watch this video on video.folx.tv'} FOLX.TV Professional Video Streaming Platform ${Math.floor(video.duration / 60)}:${(video.duration % 60).toString().padStart(2, '0')} `; // Pretvorimo SVG v PNG const pngBuffer = await sharp(Buffer.from(svg)) .png({ quality: 90, compressionLevel: 6 }) .toBuffer(); res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache za 1 uro res.send(pngBuffer); } catch (error) { console.error('Error generating video thumbnail:', error); res.status(500).send('Error generating thumbnail'); } }); // ===== ADMIN ROUTES ===== // Auth route to get current user app.get('/api/auth/user', isAuthenticated, async (req: any, res) => { try { // Import the generateDeterministicUUID function from replitAuth const { createHash } = await import('crypto'); const generateDeterministicUUID = (replitId: string): string => { const hash = createHash('sha256').update(`replit_${replitId}`).digest('hex'); return [ hash.substring(0, 8), hash.substring(8, 12), '4' + hash.substring(13, 16), (parseInt(hash.substring(16, 17), 16) & 0x3 | 0x8).toString(16) + hash.substring(17, 20), hash.substring(20, 32) ].join('-'); }; const userId = generateDeterministicUUID(req.user.claims.sub); const user = await storage.getUser(userId); if (!user) { return res.status(404).json({ message: "User not found" }); } // Remove sensitive data const { passwordHash, ...userResponse } = user; res.json(userResponse); } catch (error) { console.error("Error fetching user:", error); res.status(500).json({ message: "Failed to fetch user" }); } }); // Admin video management app.get('/api/admin/videos', isAdmin, async (req, res) => { try { const limit = parseInt(req.query.limit as string) || 500; // Increased default limit for admin const offset = parseInt(req.query.offset as string) || 0; const search = req.query.search as string; const videos = await storage.getVideos(limit, offset, search); const total = await storage.getVideoCount(search); res.json({ videos, total, limit, offset }); } catch (error) { console.error("Error fetching admin videos:", error); res.status(500).json({ message: "Failed to fetch videos" }); } }); app.patch('/api/admin/videos/:id', isAdmin, async (req, res) => { try { const videoId = req.params.id; console.log(`PATCH request for video ${videoId} with body:`, JSON.stringify(req.body, null, 2)); const updateData = updateVideoSchema.parse(req.body); console.log(`Parsed update data:`, JSON.stringify(updateData, null, 2)); const updatedVideo = await storage.updateVideo(videoId, updateData); if (!updatedVideo) { return res.status(404).json({ message: "Video not found" }); } res.json(updatedVideo); } catch (error) { console.error("Error updating video:", error); if (error instanceof z.ZodError) { res.status(400).json({ message: "Invalid video data", errors: error.errors }); } else { res.status(500).json({ message: "Failed to update video" }); } } }); // Thumbnail upload app.post('/api/admin/thumbnails/upload', isAdmin, async (req, res) => { try { const objectStorageService = new ObjectStorageService(); const uploadURL = await objectStorageService.getThumbnailUploadURL(); res.json({ uploadURL }); } catch (error) { console.error("Error getting thumbnail upload URL:", error); res.status(500).json({ message: "Failed to get upload URL" }); } }); // Serve uploaded objects app.get("/objects/:objectPath(*)", async (req, res) => { const objectStorageService = new ObjectStorageService(); try { const objectFile = await objectStorageService.getObjectFile(req.path); objectStorageService.downloadObject(objectFile, res); } catch (error) { console.error("Error serving object:", error); if (error instanceof ObjectNotFoundError) { return res.sendStatus(404); } return res.sendStatus(500); } }); // ===== USER MANAGEMENT ROUTES (ADMIN) ===== // Get all users (admin only) app.get('/api/admin/users', isAdmin, async (req, res) => { try { const limit = parseInt(req.query.limit as string) || 50; const offset = parseInt(req.query.offset as string) || 0; const search = req.query.search as string; // For now, get all users - in production you'd want pagination const users = await storage.getAllUsers ? await storage.getAllUsers(limit, offset, search) : []; // Remove password hashes from response const safeUsers = users.map(({ passwordHash, ...user }) => user); res.json({ users: safeUsers, total: users.length, limit, offset }); } catch (error) { console.error("Error fetching users:", error); res.status(500).json({ message: "Failed to fetch users" }); } }); // Get user by ID (admin only) app.get('/api/admin/users/:id', isAdmin, async (req, res) => { try { const user = await storage.getUser(req.params.id); if (!user) { return res.status(404).json({ message: "User not found" }); } // Remove password hash from response const { passwordHash, ...safeUser } = user; res.json(safeUser); } catch (error) { console.error("Error fetching user:", error); res.status(500).json({ message: "Failed to fetch user" }); } }); // Update user admin status (super admin only) app.patch('/api/admin/users/:id/admin', isAuthenticated, async (req: any, res) => { try { const currentUserId = `replit_${req.user.claims.sub}`; const currentUser = await storage.getUser(currentUserId); if (!currentUser?.isSuperAdmin) { return res.status(403).json({ message: "Super admin access required" }); } const userId = req.params.id; const { isAdmin } = req.body; const updatedUser = await storage.updateUser(userId, { isAdmin }); if (!updatedUser) { return res.status(404).json({ message: "User not found" }); } // Remove password hash from response const { passwordHash, ...safeUser } = updatedUser; res.json(safeUser); } catch (error) { console.error("Error updating user admin status:", error); res.status(500).json({ message: "Failed to update user" }); } }); // Update user active status (admin only) app.patch('/api/admin/users/:id/status', isAdmin, async (req, res) => { try { const userId = req.params.id; const { isActive } = req.body; if (typeof isActive !== 'boolean') { return res.status(400).json({ message: "isActive must be a boolean" }); } const updatedUser = await storage.updateUser(userId, { isActive }); if (!updatedUser) { return res.status(404).json({ message: "User not found" }); } // Remove password hash from response const { passwordHash, ...safeUser } = updatedUser; res.json(safeUser); } catch (error) { console.error("Error updating user status:", error); res.status(500).json({ message: "Failed to update user status" }); } }); // Get admin statistics (admin only) app.get('/api/admin/stats', isAdmin, async (req, res) => { try { const videoCount = await storage.getVideoCount(); const userCount = await storage.getUserCount ? await storage.getUserCount() : 0; // Get recent videos const recentVideos = await storage.getVideos(5, 0); // Basic stats - in production you might want more sophisticated analytics const stats = { videos: { total: videoCount, recent: recentVideos.length }, users: { total: userCount, // Could add more user analytics here }, platform: { uptime: process.uptime(), nodeVersion: process.version } }; res.json(stats); } catch (error) { console.error("Error fetching admin stats:", error); res.status(500).json({ message: "Failed to fetch statistics" }); } }); // ===== AI DESCRIPTION ROUTES ===== // Generate AI description for single video (admin only) app.post('/api/admin/videos/:id/generate-description', isAdmin, async (req, res) => { try { const videoId = req.params.id; const { maxCharacters = 500, includeArtistInfo = true, includeLabelInfo = true, customInstructions } = req.body; console.log("AI description request for video:", videoId); // Debug log // Get video details const video = await storage.getVideo(videoId); if (!video) { console.log("Video not found:", videoId); // Debug log return res.status(404).json({ message: "Video not found" }); } console.log("Generating AI description for title:", video.title); // Debug log // Generate description using AI const description = await generateVideoDescription(video.title, { maxCharacters, language: "german", includeArtistInfo, includeLabelInfo, customInstructions, contentType: video.contentType || 'music_video' }); console.log("Generated description:", description); // Debug log const result = { description, title: video.title, characterCount: description.length, maxCharacters }; console.log("Sending AI response:", result); // Debug log res.json(result); } catch (error) { console.error("Error generating AI description:", error); res.status(500).json({ message: error instanceof Error ? error.message : "Failed to generate description" }); } }); // Generate AI descriptions for multiple videos (admin only) app.post('/api/admin/videos/generate-descriptions-bulk', isAdmin, async (req, res) => { try { const { videoIds, maxCharacters = 500, includeArtistInfo = true, includeLabelInfo = true } = req.body; if (!Array.isArray(videoIds) || videoIds.length === 0) { return res.status(400).json({ message: "Video IDs array is required" }); } if (videoIds.length > 50) { return res.status(400).json({ message: "Maximum 50 videos can be processed at once" }); } // Get video details for all requested videos const videos = []; for (const id of videoIds) { const video = await storage.getVideo(id); if (video) { videos.push({ id: video.id, title: video.title }); } } // Generate descriptions using AI const results = await generateBulkDescriptions(videos, { maxCharacters, language: "slovenian", includeArtistInfo, includeLabelInfo }); res.json({ results, processed: results.length, successful: results.filter(r => !r.error).length, failed: results.filter(r => r.error).length }); } catch (error) { console.error("Error generating bulk AI descriptions:", error); res.status(500).json({ message: error instanceof Error ? error.message : "Failed to generate descriptions" }); } }); // Facebook/Social Media Share Page - Returns HTML with proper OG tags for video sharing app.get('/share/video/:id', async (req, res) => { try { const { id } = req.params; // Find video from cache (not direct Bunny API to avoid 404 errors) const allVideos = await storage.getVideos(600, 0); let video; // Check if it's a full UUID or short ID if (id.length === 36 && id.includes('-')) { video = allVideos.find(v => v.id === id); } else if (id.length === 8) { video = allVideos.find(v => v.id.replace(/-/g, '').substring(0, 8) === id); } else { video = allVideos.find(v => v.id.includes(id)); } if (!video) { return res.status(404).send('Video not found'); } const baseUrl = 'https://video.folx.tv'; const videoUrl = `${baseUrl}/video/${video.id}`; // Get high-quality thumbnail for sharing (1200x630 is ideal for Facebook) let thumbnailUrl = `${baseUrl}/api/social-image`; if (video.thumbnailUrl) { thumbnailUrl = video.thumbnailUrl .replace(/width=\d+/gi, 'width=1200') .replace(/height=\d+/gi, 'height=630') .replace(/format=webp/gi, 'format=jpg'); } // Clean description for meta tags const description = video.description ? video.description.substring(0, 200).replace(/[<>"']/g, '') : `Jetzt ${video.title} auf video.folx.tv ansehen - Die beste Volksmusik`; const title = video.title || 'video.folx.tv'; // Return HTML page with OG tags that redirects to actual video const html = ` ${title} - video.folx.tv

Weiterleitung...

Falls keine automatische Weiterleitung erfolgt, hier klicken.

`; res.set('Content-Type', 'text/html'); res.send(html); } catch (error) { console.error('Error generating share page:', error); res.status(500).send('Error generating share page'); } }); // SEO - Sitemap XML with all video pages app.get('/sitemap.xml', async (req, res) => { try { const videos = await storage.getVideos(1, 1000); // Get all videos const baseUrl = 'https://video.folx.tv'; let sitemap = ` ${baseUrl}/ daily 1.0 ${new Date().toISOString().split('T')[0]} `; // Add all video pages videos.forEach(video => { sitemap += ` ${baseUrl}/video/${video.id} weekly 0.8 ${video.updatedAt ? video.updatedAt.toISOString().split('T')[0] : new Date().toISOString().split('T')[0]} `; }); sitemap += '\n'; res.set('Content-Type', 'application/xml'); res.send(sitemap); } catch (error) { console.error('Error generating sitemap:', error); res.status(500).send('Error generating sitemap'); } }); const httpServer = createServer(app); return httpServer; }