Initial commit of YouTube Downloader application, including core functionality for downloading videos, user authentication, and Docker support. Added configuration files, environment setup, and basic UI components using Astro.js and Tailwind CSS.
This commit is contained in:
227
src/pages/api/download.ts
Normal file
227
src/pages/api/download.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getSession, isLoginEnabled } from "../../lib/session";
|
||||
import { tApi } from "../../lib/i18n";
|
||||
import { mkdir, unlink, readFile } 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";
|
||||
const ext = format === "mp4" ? "mp4" : 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"
|
||||
: "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");
|
||||
}
|
||||
|
||||
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
|
||||
return new Response(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type":
|
||||
format === "mp4" ? "video/mp4" : "application/octet-stream",
|
||||
"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");
|
||||
}
|
||||
|
||||
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
|
||||
const actualFilename = path.join(downloadDir, `${title}.${ext}`);
|
||||
let finalFilename = filename;
|
||||
if (existsSync(actualFilename)) {
|
||||
finalFilename = path.basename(actualFilename);
|
||||
}
|
||||
|
||||
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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user