Files
yt/src/pages/api/download.ts

262 lines
8.0 KiB
TypeScript

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<void>((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<void>((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" } }
);
}
};