import type { APIRoute } from "astro"; import { getSession, isLoginEnabled } from "../../lib/session"; import { tApi } from "../../lib/i18n"; import { mkdir, unlink, readFile, readdir } from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; import { tmpdir } from "node:os"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const YTDlpWrap = require("yt-dlp-wrap").default || require("yt-dlp-wrap"); export const POST: APIRoute = async ({ request }) => { // Session prüfen (nur wenn Login aktiviert ist) const loginEnabled = isLoginEnabled(); if (loginEnabled) { const session = await getSession(request); if (!session) { return new Response( JSON.stringify({ error: tApi(request, "api.notAuthenticated") }), { status: 401, headers: { "Content-Type": "application/json" }, } ); } } try { const { url } = await request.json(); if (!url || typeof url !== "string") { return new Response( JSON.stringify({ error: tApi(request, "api.invalidUrl") }), { status: 400, headers: { "Content-Type": "application/json" }, } ); } // YouTube URL validieren if (!url.includes("youtube.com") && !url.includes("youtu.be")) { return new Response( JSON.stringify({ error: tApi(request, "api.invalidYouTubeUrl") }), { status: 400, headers: { "Content-Type": "application/json" } } ); } // Prüfe ob Stream-Modus aktiviert ist const streamOnly = import.meta.env.STREAM_ONLY === "true"; // yt-dlp-wrap Instanz erstellen const ytDlpWrap = new YTDlpWrap(); // Format aus Request Header (optional, Standard: MP4) const format = request.headers.get("x-format") || "mp4"; // Zuerst Video-Informationen abrufen, um den Dateinamen zu erhalten const videoInfo = await ytDlpWrap.getVideoInfo(url); const title = videoInfo.title || "Video"; // Dateiendung bestimmen let ext = "mp4"; if (format === "audio") { ext = "mp3"; // yt-dlp wird zu MP3 konvertieren } else if (format === "mp4") { ext = "mp4"; } else { ext = videoInfo.ext || "mp4"; } const filename = `${title}.${ext}`; // Format-String für yt-dlp const formatString = format === "mp4" ? "bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio/best[ext=mp4]/best" : format === "audio" ? "bestaudio[ext=m4a]/bestaudio[ext=mp3]/bestaudio" : "best"; if (streamOnly) { // STREAM-MODUS: Datei temporär speichern, streamen, dann löschen const tempDir = tmpdir(); const tempFilePath = path.join(tempDir, `${Date.now()}-${filename}`); try { // Download in temporäres Verzeichnis await new Promise((resolve, reject) => { const execArgs = [ url, "-o", tempFilePath, "-f", formatString, "--no-mtime", "--no-playlist", ]; if (format === "mp4") { execArgs.push("--merge-output-format", "mp4"); } else if (format === "audio") { // Für Audio: Konvertiere zu MP3 execArgs.push("--extract-audio", "--audio-format", "mp3"); } ytDlpWrap .exec(execArgs) .on("error", (error: Error) => { reject(error); }) .on("close", (code: number) => { if (code === 0) { resolve(); } else { reject(new Error(`yt-dlp exited with code ${code}`)); } }); }); // Datei als Stream zurückgeben const fileBuffer = await readFile(tempFilePath); // Temporäre Datei löschen await unlink(tempFilePath).catch((err) => { console.error("Fehler beim Löschen der temporären Datei:", err); }); // Stream als Response zurückgeben let contentType = "application/octet-stream"; if (format === "mp4") { contentType = "video/mp4"; } else if (format === "audio") { contentType = "audio/mpeg"; // MP3 } return new Response(fileBuffer, { status: 200, headers: { "Content-Type": contentType, "Content-Disposition": `attachment; filename="${encodeURIComponent( filename )}"`, "Content-Length": fileBuffer.length.toString(), }, }); } catch (error) { // Aufräumen bei Fehler if (existsSync(tempFilePath)) { await unlink(tempFilePath).catch(() => {}); } throw error; } } else { // NORMALER MODUS: Datei speichern wie bisher const downloadDir = import.meta.env.DOWNLOAD_DIR || path.join(process.cwd(), "downloaded"); // Verzeichnis erstellen falls nicht vorhanden try { if (!existsSync(downloadDir)) { await mkdir(downloadDir, { recursive: true }); } } catch (mkdirError) { console.error( "Fehler beim Erstellen des Download-Verzeichnisses:", mkdirError ); return new Response( JSON.stringify({ error: tApi(request, "api.couldNotCreateDirectory", { dir: downloadDir, }), }), { status: 500, headers: { "Content-Type": "application/json" } } ); } const outputPath = path.join(downloadDir, "%(title)s.%(ext)s"); // Download durchführen mit yt-dlp-wrap await new Promise((resolve, reject) => { const execArgs = [ url, "-o", outputPath, "-f", formatString, "--no-mtime", "--no-playlist", ]; if (format === "mp4") { execArgs.push("--merge-output-format", "mp4"); } else if (format === "audio") { // Für Audio: Konvertiere zu MP3 execArgs.push("--extract-audio", "--audio-format", "mp3"); } ytDlpWrap .exec(execArgs) .on("progress", (progress: any) => { // Progress-Events können hier verarbeitet werden }) .on("ytDlpEvent", (eventType: string, eventData: any) => { if (eventType === "download") { // Download-Event verarbeiten } }) .on("error", (error: Error) => { reject(error); }) .on("close", (code: number) => { if (code === 0) { resolve(); } else { reject(new Error(`yt-dlp exited with code ${code}`)); } }); }); // Dateiname aus dem tatsächlichen Output-Pfad extrahieren // Für Audio: yt-dlp erstellt .mp3 Datei const actualExt = format === "audio" ? "mp3" : ext; const actualFilename = path.join(downloadDir, `${title}.${actualExt}`); let finalFilename = filename; if (existsSync(actualFilename)) { finalFilename = path.basename(actualFilename); } else if (format === "audio") { // Fallback: Suche nach .mp3 Datei mit möglicherweise anderem Namen const files = await readdir(downloadDir); const mp3File = files.find( (f: string) => f.endsWith(".mp3") && f.includes(title) ); if (mp3File) { finalFilename = mp3File; } } return new Response( JSON.stringify({ success: true, message: tApi(request, "api.downloadSuccessful"), filename: finalFilename, }), { status: 200, headers: { "Content-Type": "application/json" } } ); } } catch (error) { console.error("Download-Fehler:", error); return new Response( JSON.stringify({ error: error instanceof Error ? error.message : tApi(request, "api.downloadFailed"), }), { status: 500, headers: { "Content-Type": "application/json" } } ); } };