Add image optimization module using sharp to automatically convert uploaded images to WebP format, generate thumbnails, and delete originals. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 3972346d-247c-4913-af41-cab40bf9aec3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/drGbo1a Replit-Helium-Checkpoint-Created: true
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
import type { Express } from "express";
|
|
import { createServer, type Server } from "http";
|
|
import { storage } from "./storage";
|
|
import { insertArticleSchema } from "@shared/schema";
|
|
import { seedDatabase } from "./seed";
|
|
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator";
|
|
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
|
|
import { optimizeImage } from "./image-optimizer";
|
|
import multer from "multer";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import https from "https";
|
|
|
|
const uploadDir = path.join(process.cwd(), "client/public/uploads");
|
|
if (!fs.existsSync(uploadDir)) {
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
}
|
|
|
|
const upload = multer({
|
|
storage: multer.diskStorage({
|
|
destination: (_req, _file, cb) => cb(null, uploadDir),
|
|
filename: (_req, file, cb) => {
|
|
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
const ext = path.extname(file.originalname);
|
|
cb(null, uniqueSuffix + ext);
|
|
},
|
|
}),
|
|
limits: { fileSize: 10 * 1024 * 1024 },
|
|
fileFilter: (_req, file, cb) => {
|
|
const allowed = /jpeg|jpg|png|gif|webp/;
|
|
const ext = allowed.test(path.extname(file.originalname).toLowerCase());
|
|
const mime = allowed.test(file.mimetype);
|
|
cb(null, ext && mime);
|
|
},
|
|
});
|
|
|
|
export async function registerRoutes(
|
|
httpServer: Server,
|
|
app: Express
|
|
): Promise<Server> {
|
|
await seedDatabase();
|
|
|
|
generateDailyHoroscopes().catch((err) =>
|
|
console.error("Background horoscope generation failed:", err.message)
|
|
);
|
|
|
|
storage.getArticles().then((articles) => {
|
|
analyzeAllArticleImages(articles).catch(err =>
|
|
console.log("[focal-point] Analysis skipped:", err.message)
|
|
);
|
|
});
|
|
|
|
app.get("/api/focal-points", (_req, res) => {
|
|
res.json(getCachedFocalPoints());
|
|
});
|
|
|
|
app.get("/api/articles", async (_req, res) => {
|
|
const articles = await storage.getArticles();
|
|
res.json(articles);
|
|
});
|
|
|
|
app.get("/api/articles/featured", async (_req, res) => {
|
|
const articles = await storage.getFeaturedArticles();
|
|
res.json(articles);
|
|
});
|
|
|
|
app.get("/api/articles/popular", async (req, res) => {
|
|
const limit = parseInt(req.query.limit as string) || 6;
|
|
const articles = await storage.getPopularArticles(limit);
|
|
res.json(articles);
|
|
});
|
|
|
|
app.get("/api/articles/category/:category", async (req, res) => {
|
|
const articles = await storage.getArticlesByCategory(req.params.category);
|
|
res.json(articles);
|
|
});
|
|
|
|
app.get("/api/articles/:slug", async (req, res) => {
|
|
const article = await storage.getArticleBySlug(req.params.slug);
|
|
if (!article) {
|
|
return res.status(404).json({ message: "Article not found" });
|
|
}
|
|
await storage.incrementViews(article.id);
|
|
res.json({ ...article, views: article.views + 1 });
|
|
});
|
|
|
|
app.post("/api/articles", async (req, res) => {
|
|
const parsed = insertArticleSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ message: "Invalid article data", errors: parsed.error.flatten() });
|
|
}
|
|
const existing = await storage.getArticleBySlug(parsed.data.slug);
|
|
if (existing) {
|
|
return res.status(409).json({ message: `Artikel "${existing.title}" existiert bereits (ID: ${existing.id})` });
|
|
}
|
|
const article = await storage.createArticle(parsed.data);
|
|
res.status(201).json(article);
|
|
});
|
|
|
|
app.patch("/api/articles/:id", async (req, res) => {
|
|
const id = parseInt(req.params.id);
|
|
const partial = insertArticleSchema.partial().safeParse(req.body);
|
|
if (!partial.success) {
|
|
return res.status(400).json({ message: "Invalid article data", errors: partial.error.flatten() });
|
|
}
|
|
const article = await storage.updateArticle(id, partial.data);
|
|
if (!article) {
|
|
return res.status(404).json({ message: "Article not found" });
|
|
}
|
|
res.json(article);
|
|
});
|
|
|
|
app.delete("/api/articles/:id", async (req, res) => {
|
|
const id = parseInt(req.params.id);
|
|
await storage.deleteArticle(id);
|
|
res.status(204).send();
|
|
});
|
|
|
|
app.post("/api/upload", upload.single("image"), async (req, res) => {
|
|
if (!req.file) {
|
|
return res.status(400).json({ message: "No file uploaded" });
|
|
}
|
|
try {
|
|
const result = await optimizeImage(req.file.path);
|
|
res.json({ url: result.webp, thumb: result.thumb });
|
|
} catch (err) {
|
|
const url = `/uploads/${req.file.filename}`;
|
|
console.log("[image-optimizer] Optimization failed, using original:", (err as Error).message);
|
|
res.json({ url });
|
|
}
|
|
});
|
|
|
|
function bunnyFetch(path: string): Promise<any> {
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
hostname: "video.bunnycdn.com",
|
|
path,
|
|
method: "GET",
|
|
headers: { "AccessKey": process.env.BUNNY_API_KEY || "", "Accept": "application/json" },
|
|
};
|
|
const req = https.request(options, (res) => {
|
|
let body = "";
|
|
res.on("data", (c: string) => (body += c));
|
|
res.on("end", () => {
|
|
try { resolve(JSON.parse(body)); } catch { reject(new Error(body)); }
|
|
});
|
|
});
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
const LIBRARY_ID = process.env.BUNNY_LIBRARY_ID || "476412";
|
|
const CDN_HOST = process.env.BUNNY_CDN_HOST || "vz-7982dfc4-cc8.b-cdn.net";
|
|
|
|
app.get("/api/videos", async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const perPage = parseInt(req.query.perPage as string) || 20;
|
|
const search = req.query.search as string || "";
|
|
let path = `/library/${LIBRARY_ID}/videos?page=${page}&itemsPerPage=${perPage}&orderBy=date`;
|
|
if (search) path += `&search=${encodeURIComponent(search)}`;
|
|
const data = await bunnyFetch(path);
|
|
const videos = (data.items || []).map((v: any) => ({
|
|
guid: v.guid,
|
|
title: (v.title || "").replace(/\.mp4$/i, ""),
|
|
length: v.length,
|
|
dateUploaded: v.dateUploaded,
|
|
thumbnail: `https://${CDN_HOST}/${v.guid}/${v.thumbnailFileName || "thumbnail.jpg"}`,
|
|
embedUrl: `https://player.mediadelivery.net/embed/${LIBRARY_ID}/${v.guid}`,
|
|
hlsUrl: `https://${CDN_HOST}/${v.guid}/playlist.m3u8`,
|
|
status: v.status,
|
|
}));
|
|
res.json({ items: videos, totalItems: data.totalItems, currentPage: data.currentPage });
|
|
} catch (err: any) {
|
|
res.status(500).json({ message: err.message });
|
|
}
|
|
});
|
|
|
|
app.get("/api/videos/:guid", async (req, res) => {
|
|
try {
|
|
const data = await bunnyFetch(`/library/${LIBRARY_ID}/videos/${req.params.guid}`);
|
|
res.json({
|
|
guid: data.guid,
|
|
title: (data.title || "").replace(/\.mp4$/i, ""),
|
|
length: data.length,
|
|
dateUploaded: data.dateUploaded,
|
|
thumbnail: `https://${CDN_HOST}/${data.guid}/${data.thumbnailFileName || "thumbnail.jpg"}`,
|
|
embedUrl: `https://player.mediadelivery.net/embed/${LIBRARY_ID}/${data.guid}`,
|
|
hlsUrl: `https://${CDN_HOST}/${data.guid}/playlist.m3u8`,
|
|
status: data.status,
|
|
});
|
|
} catch (err: any) {
|
|
res.status(500).json({ message: err.message });
|
|
}
|
|
});
|
|
|
|
// Gallery API - serves shuffled photos from Dropbox
|
|
app.get("/api/gallery", (_req, res) => {
|
|
try {
|
|
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
|
|
const data = JSON.parse(fs.readFileSync(galleryPath, "utf-8"));
|
|
// Shuffle using Fisher-Yates
|
|
const shuffled = [...data];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
res.json(shuffled);
|
|
} catch (err: any) {
|
|
res.json([]);
|
|
}
|
|
});
|
|
|
|
// Horoscope API
|
|
app.get("/api/horoscopes/today", async (_req, res) => {
|
|
try {
|
|
const horoscopes = await getHoroscopesForToday();
|
|
res.json(horoscopes);
|
|
} catch (err: any) {
|
|
res.status(500).json({ message: err.message });
|
|
}
|
|
});
|
|
|
|
app.get("/api/horoscopes/sign/:index", async (req, res) => {
|
|
try {
|
|
const signIndex = parseInt(req.params.index);
|
|
if (isNaN(signIndex) || signIndex < 0 || signIndex > 11) {
|
|
return res.status(400).json({ message: "Invalid sign index" });
|
|
}
|
|
const horoscope = await getOrGenerateHoroscope(signIndex);
|
|
res.json(horoscope);
|
|
} catch (err: any) {
|
|
res.status(500).json({ message: err.message });
|
|
}
|
|
});
|
|
|
|
app.post("/api/horoscopes/generate", async (_req, res) => {
|
|
try {
|
|
await generateDailyHoroscopes();
|
|
const horoscopes = await getHoroscopesForToday();
|
|
res.json({ generated: horoscopes.length, horoscopes });
|
|
} catch (err: any) {
|
|
res.status(500).json({ message: err.message });
|
|
}
|
|
});
|
|
|
|
// News feed - Volksmusik/Schlager news from Google News RSS
|
|
app.get("/api/news-feed", async (_req, res) => {
|
|
try {
|
|
const topics = ["Volksmusik", "Schlager+Musik", "Oberkrainer"];
|
|
const topic = topics[Math.floor(Date.now() / 3600000) % topics.length];
|
|
const rssUrl = `https://news.google.com/rss/search?q=${topic}&hl=de&gl=DE&ceid=DE:de`;
|
|
|
|
const response = await new Promise<string>((resolve, reject) => {
|
|
https.get(rssUrl, (resp) => {
|
|
let data = "";
|
|
resp.on("data", (chunk: Buffer) => (data += chunk.toString()));
|
|
resp.on("end", () => resolve(data));
|
|
resp.on("error", reject);
|
|
}).on("error", reject);
|
|
});
|
|
|
|
const items: { title: string; link: string; source: string; pubDate: string }[] = [];
|
|
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
let match;
|
|
while ((match = itemRegex.exec(response)) !== null && items.length < 10) {
|
|
const block = match[1];
|
|
const title = block.match(/<title>(.*?)<\/title>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "") || "";
|
|
const link = block.match(/<link>(.*?)<\/link>/)?.[1] || "";
|
|
const source = block.match(/<source[^>]*>(.*?)<\/source>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "") || "";
|
|
const pubDateRaw = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1] || "";
|
|
|
|
let pubDate = "";
|
|
try {
|
|
const d = new Date(pubDateRaw);
|
|
const diffH = Math.floor((Date.now() - d.getTime()) / 3600000);
|
|
if (diffH < 1) pubDate = "Gerade eben";
|
|
else if (diffH < 24) pubDate = `vor ${diffH} Std.`;
|
|
else pubDate = `vor ${Math.floor(diffH / 24)} T.`;
|
|
} catch {
|
|
pubDate = "";
|
|
}
|
|
|
|
if (title && link) {
|
|
items.push({ title, link, source, pubDate });
|
|
}
|
|
}
|
|
|
|
res.json(items);
|
|
} catch (err: any) {
|
|
res.json([]);
|
|
}
|
|
});
|
|
|
|
return httpServer;
|
|
}
|