diff --git a/Dockerfile b/Dockerfile index cacdc32..c58ef96 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,25 @@ FROM node:20-alpine # Set default locale to German ENV LOCALE=de -# yt-dlp und ffmpeg installieren +# yt-dlp, ffmpeg und deno (JavaScript Runtime für YouTube) installieren RUN apk add --no-cache \ python3 \ py3-pip \ ffmpeg \ - && pip3 install --no-cache-dir --break-system-packages yt-dlp + curl \ + unzip \ + && pip3 install --no-cache-dir --break-system-packages yt-dlp \ + && curl -fsSL https://deno.land/install.sh | sh \ + && mv /root/.deno/bin/deno /usr/local/bin/deno \ + && chmod +x /usr/local/bin/deno WORKDIR /app +# yt-dlp Konfiguration erstellen (für deno als JS-Runtime) +RUN mkdir -p /root/.config/yt-dlp && \ + echo "--js-runtimes deno" > /root/.config/yt-dlp/config && \ + echo "--no-warnings" >> /root/.config/yt-dlp/config + # Package-Dateien kopieren und Dependencies installieren COPY package*.json ./ RUN npm ci diff --git a/README.md b/README.md index a8686a9..51cc17e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ Siehe [BUILD-MACOS.md](./BUILD-MACOS.md) für detaillierte Anleitung. - `SESSION_SECRET`: Secret für Session-Cookies (Standard: `default-secret-change-in-production`) - `STREAM_ONLY`: Wenn auf `true` gesetzt, werden Dateien nicht physisch gespeichert, sondern direkt als Download-Stream angeboten (Standard: `false`) - `LOCALE`: Sprache der Anwendung - `de` für Deutsch oder `en` für Englisch (Standard: `de`) +- `YT_DLP_COOKIES`: Pfad zu einer Cookie-Datei für yt-dlp (z.B. `./cookies.txt`). Wird verwendet, um YouTube Bot-Erkennung zu umgehen. Siehe [yt-dlp Cookie-Dokumentation](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp) +- `YT_DLP_COOKIES_FROM_BROWSER`: Browser-Name, aus dem Cookies geladen werden sollen (z.B. `chrome`, `firefox`, `edge`, `safari`). Alternative zu `YT_DLP_COOKIES`. Siehe [yt-dlp Cookie-Dokumentation](https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies) +- `YT_DLP_JS_RUNTIME`: JavaScript Runtime für yt-dlp (Standard: `deno`). Andere Optionen: `node`, `d8`, etc. **Beispiel `.env` Datei mit Login:** ``` @@ -111,6 +114,7 @@ DOWNLOAD_DIR=./downloaded SESSION_SECRET=mein-geheimes-session-secret STREAM_ONLY=false LOCALE=de +YT_DLP_COOKIES_FROM_BROWSER=chrome ``` **Beispiel `.env` Datei ohne Login:** @@ -119,8 +123,15 @@ LOGIN=false DOWNLOAD_DIR=./downloaded STREAM_ONLY=false LOCALE=de +YT_DLP_COOKIES_FROM_BROWSER=chrome ``` +**Hinweis zu Cookies:** +- Wenn YouTube Bot-Erkennung auftritt, müssen Cookies verwendet werden +- `YT_DLP_COOKIES_FROM_BROWSER` ist die einfachste Option: Gibt den Browser-Namen an (z.B. `chrome`, `firefox`, `edge`, `safari`) +- Alternativ kann `YT_DLP_COOKIES` mit dem Pfad zu einer Cookie-Datei verwendet werden +- Cookie-Dateien können mit Browser-Erweiterungen wie "Get cookies.txt LOCALLY" oder "cookies.txt" exportiert werden + **Hinweis zu `LOGIN`:** - Wenn `LOGIN=false`: Keine Login-Seite, alle Seiten sind öffentlich zugänglich. `LOGIN_USERNAME` und `LOGIN_PASSWORD` werden ignoriert. - Wenn `LOGIN=true` oder nicht gesetzt: Login ist aktiviert. `LOGIN_USERNAME` und `LOGIN_PASSWORD` müssen gesetzt sein. diff --git a/src/pages/api/download.ts b/src/pages/api/download.ts index 3d53d23..7ce3624 100644 --- a/src/pages/api/download.ts +++ b/src/pages/api/download.ts @@ -56,8 +56,23 @@ export const POST: APIRoute = async ({ request }) => { // Format aus Request Header (optional, Standard: MP4) const format = request.headers.get("x-format") || "mp4"; + // Cookie-Unterstützung für YouTube Bot-Erkennung + const cookiesFile = 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 - const videoInfo = await ytDlpWrap.getVideoInfo(url); + // getVideoInfo verwendet intern yt-dlp mit --dump-json + // Deno wird beim eigentlichen Download verwendet + const videoInfoOptions: string[] = []; + if (cookiesFile) { + videoInfoOptions.push("--cookies", cookiesFile); + } else if (cookiesFromBrowser) { + videoInfoOptions.push("--cookies-from-browser", cookiesFromBrowser); + } + const videoInfo = await ytDlpWrap.getVideoInfo(url, videoInfoOptions); const title = videoInfo.title || "Video"; // Dateiendung bestimmen @@ -72,12 +87,13 @@ export const POST: APIRoute = async ({ request }) => { 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" - : "best"; + : undefined; // undefined = yt-dlp wählt automatisch bestes Format if (streamOnly) { // STREAM-MODUS: Datei temporär speichern, streamen, dann löschen @@ -91,12 +107,24 @@ export const POST: APIRoute = async ({ request }) => { url, "-o", tempFilePath, - "-f", - formatString, "--no-mtime", "--no-playlist", + "--js-runtimes", + jsRuntime, ]; + // 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") { @@ -184,12 +212,24 @@ export const POST: APIRoute = async ({ request }) => { url, "-o", outputPath, - "-f", - formatString, "--no-mtime", "--no-playlist", + "--js-runtimes", + jsRuntime, ]; + // 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") {