import type { APIRoute } from "astro"; import { getSession, isLoginEnabled } from "../../lib/session"; import { tApi } from "../../lib/i18n"; import { mkdir, unlink, readFile, readdir, writeFile } 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" }, } ); } } // Cookie-Unterstützung: Temporäre Datei außerhalb des try-Blocks definieren let tempCookiesFile: string | null = null; try { const { url, cookies } = 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" }, } ); } // Cookie-Unterstützung: Wenn Cookies im Request sind, temporäre Datei erstellen if (cookies && typeof cookies === "string" && cookies.trim()) { try { tempCookiesFile = path.join(tmpdir(), `cookies-${Date.now()}.txt`); const cookiesContent = cookies.trim(); await writeFile(tempCookiesFile, cookiesContent, "utf-8"); console.log( `Cookie-Datei erstellt: ${tempCookiesFile}, Größe: ${cookiesContent.length} Zeichen` ); // Prüfe ob Datei existiert und lesbar ist if (existsSync(tempCookiesFile)) { const fileContent = await readFile(tempCookiesFile, "utf-8"); console.log( `Cookie-Datei gelesen: ${ fileContent.length } Zeichen, erste Zeile: ${fileContent.split("\n")[0]}` ); } } catch (error) { console.error( "Fehler beim Erstellen der temporären Cookie-Datei:", error ); // Weiter ohne Cookies } } // 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 = process.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"; // Cookie-Unterstützung für YouTube Bot-Erkennung // Priorität: 1. Cookies aus Request, 2. Umgebungsvariable, 3. Browser-Cookies const cookiesFile = tempCookiesFile || process.env.YT_DLP_COOKIES; const cookiesFromBrowser = process.env.YT_DLP_COOKIES_FROM_BROWSER; // JavaScript Runtime für yt-dlp (Standard: deno) const jsRuntime = process.env.YT_DLP_JS_RUNTIME || "deno"; // Zuerst Video-Informationen abrufen, um den Dateinamen zu erhalten // getVideoInfo verwendet intern yt-dlp mit --dump-json // Deno wird beim eigentlichen Download verwendet const videoInfoOptions: string[] = [ "--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "--extractor-args", "youtube:player_client=ios,tv_embedded,android,web", ]; if (cookiesFile) { videoInfoOptions.push("--cookies", cookiesFile); console.log(`Verwende Cookie-Datei für getVideoInfo: ${cookiesFile}`); } else if (cookiesFromBrowser) { videoInfoOptions.push("--cookies-from-browser", cookiesFromBrowser); console.log( `Verwende Browser-Cookies für getVideoInfo: ${cookiesFromBrowser}` ); } else { console.log("Keine Cookies für getVideoInfo verfügbar"); } console.log(`getVideoInfo Optionen: ${JSON.stringify(videoInfoOptions)}`); const videoInfo = await ytDlpWrap.getVideoInfo(url, videoInfoOptions); 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 // Für "best": Kein Format-String = yt-dlp wählt automatisch bestes Format und merged 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" : format === "best" ? undefined // Kein Format = yt-dlp wählt automatisch bestes Format : undefined; // Fallback: yt-dlp wählt automatisch 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, "--no-mtime", "--no-playlist", "--js-runtimes", jsRuntime, // Zusätzliche Optionen zur Umgehung der Bot-Erkennung "--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "--extractor-args", "youtube:player_client=android,web", ]; // Cookie-Unterstützung hinzufügen if (cookiesFile) { execArgs.push("--cookies", cookiesFile); } else if (cookiesFromBrowser) { execArgs.push("--cookies-from-browser", cookiesFromBrowser); } // Format nur hinzufügen wenn definiert if (formatString) { execArgs.push("-f", formatString); } 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"); } else if (format === "best") { // Für "best": Keine zusätzlichen Optionen, yt-dlp wählt automatisch // Optional: Merge-Format setzen falls Video+Audio getrennt sind execArgs.push("--merge-output-format", "mp4"); } 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 = process.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, "--no-mtime", "--no-playlist", "--js-runtimes", jsRuntime, // Zusätzliche Optionen zur Umgehung der Bot-Erkennung "--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "--extractor-args", "youtube:player_client=ios,tv_embedded,android,web", ]; // Cookie-Unterstützung hinzufügen if (cookiesFile) { execArgs.push("--cookies", cookiesFile); } else if (cookiesFromBrowser) { execArgs.push("--cookies-from-browser", cookiesFromBrowser); } // Format nur hinzufügen wenn definiert if (formatString) { execArgs.push("-f", formatString); } 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"); } else if (format === "best") { // Für "best": Keine zusätzlichen Optionen, yt-dlp wählt automatisch // Optional: Merge-Format setzen falls Video+Audio getrennt sind execArgs.push("--merge-output-format", "mp4"); } 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" } } ); } finally { // Temporäre Cookie-Datei löschen falls vorhanden if (tempCookiesFile && existsSync(tempCookiesFile)) { await unlink(tempCookiesFile).catch((err) => { console.error("Fehler beim Löschen der temporären Cookie-Datei:", err); }); } } };