Add audio format option to download form and update API for audio downloads

This commit is contained in:
Peter Meier
2025-12-22 11:06:14 +01:00
parent 486639aaea
commit 4fd9d3f400
4 changed files with 42 additions and 5 deletions

View File

@@ -38,6 +38,7 @@ const streamOnly = import.meta.env.STREAM_ONLY === "true";
<option value="mp4" selected <option value="mp4" selected
>{t(Astro, "downloadForm.formatMp4")}</option >{t(Astro, "downloadForm.formatMp4")}</option
> >
<option value="audio">{t(Astro, "downloadForm.formatAudio")}</option>
<option value="best">{t(Astro, "downloadForm.formatBest")}</option> <option value="best">{t(Astro, "downloadForm.formatBest")}</option>
</select> </select>
</div> </div>

View File

@@ -20,6 +20,7 @@
"urlPlaceholder": "https://www.youtube.com/watch?v=...", "urlPlaceholder": "https://www.youtube.com/watch?v=...",
"formatLabel": "Format", "formatLabel": "Format",
"formatMp4": "MP4 (empfohlen - beste Kompatibilität)", "formatMp4": "MP4 (empfohlen - beste Kompatibilität)",
"formatAudio": "Nur Audio (MP3/M4A)",
"formatBest": "Bestes verfügbares Format", "formatBest": "Bestes verfügbares Format",
"downloadInProgress": "Download läuft...", "downloadInProgress": "Download läuft...",
"downloadSuccessful": "Download erfolgreich!", "downloadSuccessful": "Download erfolgreich!",

View File

@@ -20,6 +20,7 @@
"urlPlaceholder": "https://www.youtube.com/watch?v=...", "urlPlaceholder": "https://www.youtube.com/watch?v=...",
"formatLabel": "Format", "formatLabel": "Format",
"formatMp4": "MP4 (recommended - best compatibility)", "formatMp4": "MP4 (recommended - best compatibility)",
"formatAudio": "Audio only (MP3/M4A)",
"formatBest": "Best available format", "formatBest": "Best available format",
"downloadInProgress": "Download in progress...", "downloadInProgress": "Download in progress...",
"downloadSuccessful": "Download successful!", "downloadSuccessful": "Download successful!",

View File

@@ -1,7 +1,7 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import { getSession, isLoginEnabled } from "../../lib/session"; import { getSession, isLoginEnabled } from "../../lib/session";
import { tApi } from "../../lib/i18n"; import { tApi } from "../../lib/i18n";
import { mkdir, unlink, readFile } from "node:fs/promises"; import { mkdir, unlink, readFile, readdir } from "node:fs/promises";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
@@ -59,13 +59,24 @@ export const POST: APIRoute = async ({ request }) => {
// Zuerst Video-Informationen abrufen, um den Dateinamen zu erhalten // Zuerst Video-Informationen abrufen, um den Dateinamen zu erhalten
const videoInfo = await ytDlpWrap.getVideoInfo(url); const videoInfo = await ytDlpWrap.getVideoInfo(url);
const title = videoInfo.title || "Video"; const title = videoInfo.title || "Video";
const ext = format === "mp4" ? "mp4" : videoInfo.ext || "mp4";
// 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}`; const filename = `${title}.${ext}`;
// Format-String für yt-dlp // Format-String für yt-dlp
const formatString = const formatString =
format === "mp4" format === "mp4"
? "bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio/best[ext=mp4]/best" ? "bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio/best[ext=mp4]/best"
: format === "audio"
? "bestaudio[ext=m4a]/bestaudio[ext=mp3]/bestaudio"
: "best"; : "best";
if (streamOnly) { if (streamOnly) {
@@ -88,6 +99,9 @@ export const POST: APIRoute = async ({ request }) => {
if (format === "mp4") { if (format === "mp4") {
execArgs.push("--merge-output-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 ytDlpWrap
@@ -113,11 +127,17 @@ export const POST: APIRoute = async ({ request }) => {
}); });
// Stream als Response zurückgeben // 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, { return new Response(fileBuffer, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "Content-Type": contentType,
format === "mp4" ? "video/mp4" : "application/octet-stream",
"Content-Disposition": `attachment; filename="${encodeURIComponent( "Content-Disposition": `attachment; filename="${encodeURIComponent(
filename filename
)}"`, )}"`,
@@ -172,6 +192,9 @@ export const POST: APIRoute = async ({ request }) => {
if (format === "mp4") { if (format === "mp4") {
execArgs.push("--merge-output-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 ytDlpWrap
@@ -197,10 +220,21 @@ export const POST: APIRoute = async ({ request }) => {
}); });
// Dateiname aus dem tatsächlichen Output-Pfad extrahieren // Dateiname aus dem tatsächlichen Output-Pfad extrahieren
const actualFilename = path.join(downloadDir, `${title}.${ext}`); // Für Audio: yt-dlp erstellt .mp3 Datei
const actualExt = format === "audio" ? "mp3" : ext;
const actualFilename = path.join(downloadDir, `${title}.${actualExt}`);
let finalFilename = filename; let finalFilename = filename;
if (existsSync(actualFilename)) { if (existsSync(actualFilename)) {
finalFilename = path.basename(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( return new Response(