From 324156fd036b397c585c5e39616eb6e400e0cc90 Mon Sep 17 00:00:00 2001 From: Domenik Date: Sat, 22 Feb 2025 14:25:05 +0100 Subject: [PATCH] feat: video-to-audio converter implemented without logic --- backend/package-lock.json | 9 +- backend/package.json | 3 +- backend/server.ts | 2 + backend/src/routes/videoconvert.route.ts | 70 ++++++++ frontend/src/app/video-to-audio/layout.tsx | 20 +++ frontend/src/app/video-to-audio/page.tsx | 186 +++++++++++++++++++++ frontend/src/constants/index.ts | 30 ++++ 7 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 backend/src/routes/videoconvert.route.ts create mode 100644 frontend/src/app/video-to-audio/layout.tsx create mode 100644 frontend/src/app/video-to-audio/page.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index b6705e9..7b41736 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,7 +17,8 @@ "fastify-multipart": "^5.3.1", "libreoffice-convert": "^1.6.0", "luxon": "^3.5.0", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "video-to-audio": "^1.0.1" }, "devDependencies": { "@types/luxon": "^3.4.2", @@ -1317,6 +1318,12 @@ "node": ">= 0.8" } }, + "node_modules/video-to-audio": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/video-to-audio/-/video-to-audio-1.0.1.tgz", + "integrity": "sha512-6+h79x2kSO60PYTT2cXcgXIsqw/3X8a5qshsZanxH0cxbGUVxVllwQdNOWr/8ywDNcgGgecSmk8Q6TJ79gtFjQ==", + "license": "MIT" + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index ab5c3e8..fb60513 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,7 +20,8 @@ "fastify-multipart": "^5.3.1", "libreoffice-convert": "^1.6.0", "luxon": "^3.5.0", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "video-to-audio": "^1.0.1" }, "devDependencies": { "@types/luxon": "^3.4.2", diff --git a/backend/server.ts b/backend/server.ts index 7593215..bffc6aa 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -8,6 +8,7 @@ import { regexTest } from "./src/routes/regextest.route"; import { tmzConvert } from "./src/routes/tmzconvert.route"; import { generateQRCode } from "./src/routes/generateqrcode.route"; import { wordCounter } from "./src/routes/wordcounter.route"; +import { videoConvert } from "./src/routes/videoconvert.route"; const app = Fastify({ logger: true }); @@ -25,6 +26,7 @@ app.register(regexTest); app.register(tmzConvert); app.register(generateQRCode); app.register(wordCounter); +app.register(videoConvert); const PORT = process.env.PORT || 4000; app.listen({ port: Number(PORT), host: "0.0.0.0" }, () => { diff --git a/backend/src/routes/videoconvert.route.ts b/backend/src/routes/videoconvert.route.ts new file mode 100644 index 0000000..5daca9b --- /dev/null +++ b/backend/src/routes/videoconvert.route.ts @@ -0,0 +1,70 @@ +import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import * as libre from "libreoffice-convert"; +import { promisify } from "util"; +import { MultipartValue } from "@fastify/multipart"; + +const libreConvertAsync = promisify(libre.convert); + +const mimeTypes: { [key: string]: string } = { + mp4: "video/mp4", + avi: "video/x-msvideo", + mkv: "video/x-matroska", +}; + +export async function videoConvert(app: FastifyInstance) { + app.post( + "/api/video-convert", + async (request: FastifyRequest, reply: FastifyReply) => { + try { + const parts = request.parts(); + + let fileBuffer: Buffer | null = null; + let outputFileExt = ""; + + for await (const part of parts) { + if (part.type === "file") { + fileBuffer = await part.toBuffer(); + } else if ( + part.fieldname === "outputFormat" && + part.type === "field" + ) { + outputFileExt = (part as MultipartValue).value; + } + } + + if (!fileBuffer) { + return reply.status(400).send({ error: "No file uploaded!" }); + } + if (!outputFileExt) { + return reply + .status(400) + .send({ error: "No output format provided!" }); + } + if (!outputFileExt.startsWith(".")) { + outputFileExt = "." + outputFileExt; + } + + const format = outputFileExt.substring(1); + const mimeType = mimeTypes[format] || "application/octet-stream"; + + const convertedBuffer = await libreConvertAsync( + fileBuffer, + outputFileExt, + undefined, + ); + + reply + .header("Content-Type", mimeType) + .header( + "Content-Disposition", + `attachment; filename="converted${outputFileExt}"`, + ) + .status(200) + .send(convertedBuffer); + } catch (error) { + console.error("Convert error:", error); + reply.status(500).send({ error: "Error while converting!" }); + } + }, + ); +} diff --git a/frontend/src/app/video-to-audio/layout.tsx b/frontend/src/app/video-to-audio/layout.tsx new file mode 100644 index 0000000..76ac270 --- /dev/null +++ b/frontend/src/app/video-to-audio/layout.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import type { Metadata } from "next"; +import { toolLinks } from "@/constants"; + +export const metadata: Metadata = { + title: toolLinks[8].title, + description: "MP4 to MP3 converter!", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/video-to-audio/page.tsx b/frontend/src/app/video-to-audio/page.tsx new file mode 100644 index 0000000..e8de4f1 --- /dev/null +++ b/frontend/src/app/video-to-audio/page.tsx @@ -0,0 +1,186 @@ +"use client"; +import React, { useState } from "react"; +import Navbar from "../../components/Navbar"; +import Footer from "../../components/Footer"; +import Button from "../../components/Button"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import Dropdown from "@/components/Dropdown"; +import { + FileFormatsTable, + outputFileFormats, + videoAudioFormats, + videoAudioFormatsTable, +} from "@/constants"; + +export default function DocConverter() { + const [file, setFile] = useState(null); + const [loading, setLoading] = useState(false); + const [tableOpen, setTableOpen] = useState(false); + const [filteredOptions, setFilteredOptions] = useState([]); + const [selectedOutputFormat, setSelectedOutputFormat] = useState( + "", + ); + + const handleFileChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + if (event.target.files && event.target.files.length > 0) { + const selectedFile = event.target.files[0]; + const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase(); + + const isSupported = videoAudioFormats.some((format) => + format.input.toLowerCase().includes(fileExtension || ""), + ); + + if (!isSupported) { + alert("File format not supported!"); + event.target.value = ""; + return; + } + + setFile(selectedFile); + setSelectedOutputFormat(""); + + const matchedFormat = videoAudioFormats.find((format) => + format.input.toLowerCase().includes(fileExtension || ""), + ); + setFilteredOptions(matchedFormat ? matchedFormat.output : []); + } + }; + + const convertDoc = async () => { + if (!file) { + alert("No file selected"); + return; + } + + const formData = new FormData(); + formData.append("file", file); + formData.append("outputFormat", selectedOutputFormat); + + setLoading(true); + + try { + const response = await fetch( + process.env.backend_url + "/api/video-convert", + { + method: "POST", + body: formData, + }, + ); + + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const filename = file.name.split(".")[0]; + + const a = document.createElement("a"); + a.href = url; + a.download = `${filename}${selectedOutputFormat}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + setTimeout(() => { + URL.revokeObjectURL(url); + }, 5000); + } catch (error) { + console.error("Error while converting:", error); + alert("Error while converting!"); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+

doc-converter

+
+ + 0 ? filteredOptions : [""]} + onClick={(event) => { + const selectedFormat = + event.currentTarget.textContent?.trim() || ""; + setSelectedOutputFormat(selectedFormat); + }} + /> +
+
+
+
+
setTableOpen(!tableOpen)} + > + + + + + + + +
+ 📥 Input Format + +

📤 Output Format

+
+ {tableOpen ? ( + + ) : ( + + )} +
+
+
+ +
+ + + {videoAudioFormatsTable.map((format) => ( + + + + + ))} + +
+ {format.input} + + {format.output.join(", ")} +
+
+
+
+
+ ); +} diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 13db40f..792dbce 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -154,3 +154,33 @@ export const FileFormatsTable = [ output: [".pdf", ".ppt", ".pptx"], }, ]; + +export const videoAudioFormats = [ + { + input: ".mp4", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".avi", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".mov", + output: [".mp3", ".wav", ".aac"], + }, +]; + +export const videoAudioFormatsTable = [ + { + input: ".mp4", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".avi", + output: [".mp3", ".wav", ".aac"], + }, + { + input: ".mov", + output: [".mp3", ".wav", ".aac"], + }, +];