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 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 { await seedDatabase(); 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) || 5; 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"), (req, res) => { if (!req.file) { return res.status(400).json({ message: "No file uploaded" }); } const url = `/uploads/${req.file.filename}`; res.json({ url }); }); function bunnyFetch(path: string): Promise { 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 }); } }); return httpServer; }