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:
Peter Meier
2025-12-22 10:59:01 +01:00
parent 79d8a95391
commit 486639aaea
31 changed files with 13078 additions and 132 deletions

View File

@@ -0,0 +1,213 @@
---
import { t } from "../lib/i18n";
const streamOnly = import.meta.env.STREAM_ONLY === "true";
---
<div class="container mx-auto px-4 py-8 max-w-2xl">
<h1 class="text-2xl font-bold mb-6">{t(Astro, "downloadForm.title")}</h1>
<form id="downloadForm" class="space-y-4">
<div class="form-control">
<label class="label" for="url">
<span class="label-text">{t(Astro, "downloadForm.urlLabel")}</span>
</label>
<div class="join w-full">
<input
type="url"
id="url"
name="url"
required
class="input input-bordered join-item flex-1 border border-base-300"
placeholder={t(Astro, "downloadForm.urlPlaceholder")}
/>
<button
type="submit"
id="downloadBtn"
class="btn btn-primary join-item"
>
{t(Astro, "common.download")}
</button>
</div>
</div>
<div class="form-control">
<label class="label" for="format">
<span class="label-text">{t(Astro, "downloadForm.formatLabel")}</span>
</label>
<select id="format" class="select select-bordered w-full">
<option value="mp4" selected
>{t(Astro, "downloadForm.formatMp4")}</option
>
<option value="best">{t(Astro, "downloadForm.formatBest")}</option>
</select>
</div>
</form>
<div id="status" class="mt-4 hidden"></div>
<div id="loading" class="mt-4 hidden">
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-md"></span>
<span>{t(Astro, "downloadForm.downloadInProgress")}</span>
</div>
</div>
<div id="lastFile" class="mt-4 hidden">
<div class="alert alert-success">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<div class="font-bold">{t(Astro, "downloadForm.lastFile")}</div>
<div id="lastFileName" class="text-sm"></div>
</div>
<div>
<a id="lastFileLink" href="#" class="btn btn-sm btn-primary" download>
{t(Astro, "common.download")}
</a>
{
!streamOnly && (
<a href="/files" class="btn btn-sm btn-ghost ml-2">
{t(Astro, "common.allFiles")}
</a>
)
}
</div>
</div>
</div>
</div>
<script>
const form = document.getElementById("downloadForm");
const status = document.getElementById("status");
const loading = document.getElementById("loading");
const downloadBtn = document.getElementById("downloadBtn");
const lastFile = document.getElementById("lastFile");
const lastFileName = document.getElementById("lastFileName");
const lastFileLink = document.getElementById(
"lastFileLink"
) as HTMLAnchorElement;
form?.addEventListener("submit", async (e) => {
e.preventDefault();
const urlInput = document.getElementById("url") as HTMLInputElement;
const formatSelect = document.getElementById("format") as HTMLSelectElement;
const url = urlInput?.value;
const format = formatSelect?.value || "mp4";
if (!url || !downloadBtn || !status || !loading) return;
(downloadBtn as HTMLButtonElement).disabled = true;
status.classList.add("hidden");
loading?.classList.remove("hidden");
try {
const response = await fetch("/api/download", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-format": format,
},
body: JSON.stringify({ url }),
});
// Prüfe ob Response ein Stream ist (Content-Type ist nicht JSON)
const contentType = response.headers.get("content-type");
const isStream = contentType && !contentType.includes("application/json");
if (isStream && response.ok) {
// Stream-Modus: Datei direkt herunterladen
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = downloadUrl;
// Dateiname aus Content-Disposition Header extrahieren
const contentDisposition = response.headers.get("content-disposition");
let filename = "video." + format;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1].replace(/['"]/g, "");
// URL-dekodieren falls nötig
try {
filename = decodeURIComponent(filename);
} catch (e) {
// Falls Dekodierung fehlschlägt, Original verwenden
}
}
}
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
loading?.classList.add("hidden");
if (status) {
status.classList.remove("hidden");
status.className = "alert alert-success";
status.innerHTML = `<span>${document.documentElement.lang === "en" ? "Download successful!" : "Download erfolgreich!"}</span>`;
}
urlInput.value = "";
} else {
// Normaler Modus: JSON Response
const data = await response.json();
loading?.classList.add("hidden");
if (response.ok && status) {
status.classList.remove("hidden");
status.className = "alert alert-success";
status.innerHTML = `<span>${document.documentElement.lang === "en" ? "Download successful!" : "Download erfolgreich!"}</span>`;
urlInput.value = "";
// Show last file
if (data.filename && lastFile && lastFileName && lastFileLink) {
lastFileName.textContent = data.filename;
lastFileLink.href = `/api/download-file?file=${encodeURIComponent(data.filename)}`;
lastFile.classList.remove("hidden");
}
} else if (status) {
status.classList.remove("hidden");
status.className = "alert alert-error";
const errorMsg =
data.error ||
(document.documentElement.lang === "en"
? "Unknown error"
: "Unbekannter Fehler");
status.innerHTML = `<span>${document.documentElement.lang === "en" ? "Error: " : "Fehler: "}${errorMsg}</span>`;
}
}
} catch (error) {
loading?.classList.add("hidden");
if (status) {
status.classList.remove("hidden");
status.className = "alert alert-error";
const errorMsg =
error instanceof Error
? error.message
: document.documentElement.lang === "en"
? "Network error"
: "Netzwerkfehler";
status.innerHTML = `<span>${document.documentElement.lang === "en" ? "Error: " : "Fehler: "}${errorMsg}</span>`;
}
} finally {
if (downloadBtn) {
(downloadBtn as HTMLButtonElement).disabled = false;
}
}
});
</script>