videofolxtv/server/index.ts
sebastjanartic bd01ca5e3c Update interface text to be displayed in German
Translate Slovenian text to German across various components, pages, and API error messages, including video cards, modals, live page error handling, and AI service responses.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 401e2ec0-e00d-4f10-9d0e-60f3d479f9a5
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1f3f88aa-7796-4167-865f-faff8fe770e1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/401e2ec0-e00d-4f10-9d0e-60f3d479f9a5/g0TFDck
2026-01-12 10:50:01 +00:00

267 lines
9.6 KiB
TypeScript

import express, { type Request, Response, NextFunction } from "express";
import compression from "compression";
import { registerRoutes, findVideoByAnyId } from "./routes";
import { videoSyncService } from "./videoSync";
import { setupVite, serveStatic, log } from "./vite";
import { storage } from "./storage";
import fs from "fs";
import path from "path";
const app = express();
// Enable gzip compression for faster responses
app.use(compression());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Performance and caching middleware
app.use((req, res, next) => {
// Set no-cache headers only for HTML pages to prevent blank screen issues
if (req.path === '/' || req.path.endsWith('.html') || !req.path.includes('.')) {
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
}
// Cache static assets for better performance
else if (req.path.includes('/assets/') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
res.set({
'Cache-Control': 'public, max-age=31536000', // 1 year
'ETag': `"${Date.now()}"` // Simple ETag
});
}
// Cache API responses for 30 seconds
else if (req.path.startsWith('/api/videos') && req.method === 'GET') {
res.set({
'Cache-Control': 'public, max-age=30',
'ETag': `"videos-${Date.now()}"`
});
}
next();
});
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: Record<string, any> | undefined = undefined;
const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
// Initialize video sync service for automatic Bunny.net updates
await videoSyncService.initialize();
// Social media prerendering middleware - samo za social bot-e
app.get('/video/:videoId', async (req, res, next) => {
const { videoId } = req.params;
const userAgent = req.get('User-Agent') || '';
// Če je to Facebook, Twitter, WhatsApp ali podoben scraper
const isSocialBot = /facebookexternalhit|twitterbot|whatsapp|telegrambot|discordbot|slackbot|linkedinbot/i.test(userAgent);
// Če NI social bot, pustimo da React routing prevzame
if (!isSocialBot) {
return next();
}
if (isSocialBot) {
try {
console.log(`🤖 Social bot detected: ${userAgent}, looking for video: ${videoId}`);
// Use the same findVideoByAnyId function as in routes
const video = await findVideoByAnyId(videoId);
if (!video) {
console.log(`❌ Video not found for social bot: ${videoId}`);
return next(); // Če video ne obstaja, preusmerimo na običajno SPA
}
console.log(`✅ Video found for social bot: ${video.title}`);
console.log(`📷 Original thumbnail: ${video.thumbnailUrl}`);
// Preberemo osnovni HTML template
const clientTemplate = path.resolve(import.meta.dirname, "..", "client", "index.html");
let template = await fs.promises.readFile(clientTemplate, "utf-8");
// Escape special characters v naslovih in opisih
const escapeHtml = (text: string) => text.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
// Zamenimo meta oznake z video specifičnimi
const baseUrl = req.protocol + '://' + req.get('host');
// Use short ID for sharing URLs
const shortId = video.id.replace(/-/g, '').substring(0, 8);
const videoUrl = `${baseUrl}/video/${shortId}`;
// Optimize thumbnail for social media sharing - larger size and JPEG format
let thumbnailUrl = video.thumbnailUrl;
if (thumbnailUrl) {
// Replace small WebP with larger JPEG for better social media compatibility
thumbnailUrl = thumbnailUrl
.replace('width=400&height=225', 'width=1200&height=630')
.replace('format=webp', 'format=jpg');
} else {
thumbnailUrl = `${baseUrl}/api/video-thumbnail/${video.id}`;
}
// Zamenjamo osnovne meta oznake
template = template.replace(
/<title>.*<\/title>/,
`<title>${escapeHtml(video.title)} | go4.video</title>`
);
template = template.replace(
/<meta name="description" content="[^"]*"/,
`<meta name="description" content="${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}"`
);
// Zamenjamo Open Graph meta oznake
template = template.replace(
/<meta property="og:title" content="[^"]*"/,
`<meta property="og:title" content="${escapeHtml(video.title)}"`
);
template = template.replace(
/<meta property="og:description" content="[^"]*"/,
`<meta property="og:description" content="${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}"`
);
// Replace og:image - handle both with and without id attribute
template = template.replace(
/<meta property="og:image"[^>]*>/,
`<meta property="og:image" id="og-image" content="${thumbnailUrl}"`
);
// Zamenjamo tudi Twitter image
// Replace Twitter image - handle both with and without id attribute
template = template.replace(
/<meta name="twitter:image"[^>]*>/,
`<meta name="twitter:image" id="twitter-image" content="${thumbnailUrl}"`
);
template = template.replace(
/<meta name="twitter:title" content="[^"]*"/,
`<meta name="twitter:title" content="${escapeHtml(video.title)}"`
);
template = template.replace(
/<meta name="twitter:description" content="[^"]*"/,
`<meta name="twitter:description" content="${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}"`
);
// Also update URL and secure image tags that have id attributes
template = template.replace(
/<meta property="og:url"[^>]*>/,
`<meta property="og:url" id="og-url" content="${videoUrl}">`
);
template = template.replace(
/<meta property="og:image:secure_url"[^>]*>/,
`<meta property="og:image:secure_url" id="og-image-secure" content="${thumbnailUrl}">`
);
// Dodamo dodatne OG oznake za video z detajlnimi informacijami
const duration = Math.floor(video.duration / 60) + ':' + (video.duration % 60).toString().padStart(2, '0');
const additionalMeta = `
<meta property="og:type" content="video.other">
<meta property="og:video:duration" content="${video.duration}">
<meta property="video:duration" content="${video.duration}">
<meta property="video:release_date" content="${video.createdAt?.toISOString()}">
<!-- Dodatni Twitter Card meta podatki -->
<meta name="twitter:player:width" content="1200">
<meta name="twitter:player:height" content="630">
<meta name="twitter:creator" content="@go4video">
<!-- Structured data for Google -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "${escapeHtml(video.title)}",
"description": "${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}",
"thumbnailUrl": "${thumbnailUrl}",
"uploadDate": "${video.createdAt?.toISOString()}",
"duration": "PT${Math.floor(video.duration / 60)}M${video.duration % 60}S",
"contentUrl": "${videoUrl}",
"embedUrl": "${videoUrl}",
"publisher": {
"@type": "Organization",
"name": "go4.video",
"url": "${baseUrl}"
}
}
</script>`;
template = template.replace('</head>', `${additionalMeta}\n</head>`);
res.setHeader('Content-Type', 'text/html');
res.send(template);
return;
} catch (error) {
console.error('Error rendering video page for social bot:', error);
// If an error occurs, continue with normal SPA
return next();
}
}
// For regular users, continue with SPA
next();
});
const server = await registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on the port specified in the environment variable PORT
// Other ports are firewalled. Default to 5000 if not specified.
// this serves both the API and the client.
// It is the only port that is not firewalled.
const port = parseInt(process.env.PORT || '5000', 10);
server.listen({
port,
host: "0.0.0.0",
reusePort: true,
}, () => {
log(`serving on port ${port}`);
});
})();