diff --git a/src/handlers/api/asset.handler.ts b/src/handlers/api/asset.handler.ts index 74b6af0..f0acf6d 100644 --- a/src/handlers/api/asset.handler.ts +++ b/src/handlers/api/asset.handler.ts @@ -160,6 +160,7 @@ export interface ICompressionResult { assetId: string; originalSize: number; compressedSize: number; + outputFileName?: string; resultUrl: string; } diff --git a/src/helpers/compression.server.ts b/src/helpers/compression.server.ts index ddfb70e..b688d28 100644 --- a/src/helpers/compression.server.ts +++ b/src/helpers/compression.server.ts @@ -14,11 +14,41 @@ const COMPRESSION_ROOT = "/app/data/compression"; export const getCompressionDirectory = (assetId: string) => path.join(COMPRESSION_ROOT, assetId); -export const getCompressionInputPath = (assetId: string) => - path.join(getCompressionDirectory(assetId), "input"); +const getSafeOriginalFileName = (assetId: string, originalFileName: string) => { + const fileName = path.basename(originalFileName || ""); + return fileName || `${assetId}.mp4`; +}; -export const getCompressionOutputPath = (assetId: string) => - path.join(getCompressionDirectory(assetId), "output.mp4"); +export const getCompressionInputPath = (assetId: string, originalFileName: string) => + path.join(getCompressionDirectory(assetId), getSafeOriginalFileName(assetId, originalFileName)); + +export const getCompressionOutputFileName = (assetId: string, originalFileName: string) => { + const inputFileName = getSafeOriginalFileName(assetId, originalFileName); + const extension = path.extname(inputFileName); + const baseName = extension + ? inputFileName.slice(0, -extension.length) + : inputFileName; + + return `${baseName}-compressed${extension || ".mp4"}`; +}; + +export const getCompressionOutputPath = (assetId: string, originalFileName: string) => + path.join( + getCompressionDirectory(assetId), + getCompressionOutputFileName(assetId, originalFileName) + ); + +export const getVideoContentType = (fileName: string) => { + switch (path.extname(fileName).toLowerCase()) { + case ".mov": return "video/quicktime"; + case ".mkv": return "video/x-matroska"; + case ".webm": return "video/webm"; + case ".avi": return "video/x-msvideo"; + case ".m4v": return "video/x-m4v"; + case ".mp4": return "video/mp4"; + default: return "application/octet-stream"; + } +}; export const ensureCompressionDirectory = async (assetId: string) => { await mkdir(getCompressionDirectory(assetId), { recursive: true }); diff --git a/src/pages/api/assets/compression/replace.ts b/src/pages/api/assets/compression/replace.ts index 80984ff..a52f18a 100644 --- a/src/pages/api/assets/compression/replace.ts +++ b/src/pages/api/assets/compression/replace.ts @@ -2,7 +2,7 @@ import { ENV } from "@/config/environment"; import { db } from "@/config/db"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { getUserHeaders } from "@/helpers/user.helper"; -import { clearCompressionDirectory, getCompressionOutputPath } from "@/helpers/compression.server"; +import { clearCompressionDirectory, getCompressionOutputPath, getVideoContentType } from "@/helpers/compression.server"; import { assets } from "@/schema"; import { and, eq } from "drizzle-orm"; import { openAsBlob } from "node:fs"; @@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const rawAssetId = req.body?.assetId as string; const [dbAsset] = await db - .select({ id: assets.id, type: assets.type }) + .select({ id: assets.id, type: assets.type, originalFileName: assets.originalFileName }) .from(assets) .where(and(eq(assets.id, rawAssetId), eq(assets.ownerId, currentUser.id))); @@ -63,15 +63,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const customMetadata = metadataResponse.ok ? await metadataResponse.json() : []; - const originalExtension = path.extname(source.originalFileName || ""); - const originalBase = originalExtension - ? path.basename(source.originalFileName, originalExtension) - : (source.originalFileName || dbAsset.id); - const replacementFileName = originalExtension.toLowerCase() === ".mp4" - ? source.originalFileName - : `${originalBase}.mp4`; - - const fileBlob = await openAsBlob(getCompressionOutputPath(dbAsset.id), { type: "video/mp4" }); + const replacementFileName = path.basename(source.originalFileName || dbAsset.originalFileName || `${dbAsset.id}.mp4`); + const outputPath = getCompressionOutputPath(dbAsset.id, dbAsset.originalFileName); + const fileBlob = await openAsBlob(outputPath, { type: getVideoContentType(outputPath) }); const formData = new FormData(); formData.set("fileCreatedAt", source.fileCreatedAt); formData.set("fileModifiedAt", source.fileModifiedAt); diff --git a/src/pages/api/assets/compression/result/[assetId].ts b/src/pages/api/assets/compression/result/[assetId].ts index d789270..e288b08 100644 --- a/src/pages/api/assets/compression/result/[assetId].ts +++ b/src/pages/api/assets/compression/result/[assetId].ts @@ -1,6 +1,6 @@ import { db } from "@/config/db"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; -import { clearCompressionDirectory, getCompressionOutputPath } from "@/helpers/compression.server"; +import { clearCompressionDirectory, getCompressionOutputPath, getVideoContentType } from "@/helpers/compression.server"; import { assets } from "@/schema"; import { and, eq } from "drizzle-orm"; import { createReadStream } from "node:fs"; @@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const rawAssetId = req.query.assetId as string; const [asset] = await db - .select({ id: assets.id }) + .select({ id: assets.id, originalFileName: assets.originalFileName }) .from(assets) .where(and(eq(assets.id, rawAssetId), eq(assets.ownerId, currentUser.id))); @@ -26,13 +26,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (req.method !== "GET") return res.status(405).json({ message: "Method not allowed" }); - const filePath = getCompressionOutputPath(asset.id); + const filePath = getCompressionOutputPath(asset.id, asset.originalFileName); try { const fileStat = await stat(filePath); const range = req.headers.range; - res.setHeader("Content-Type", "video/mp4"); + res.setHeader("Content-Type", getVideoContentType(filePath)); res.setHeader("Accept-Ranges", "bytes"); res.setHeader("Cache-Control", "no-store"); diff --git a/src/pages/api/assets/compression/results.ts b/src/pages/api/assets/compression/results.ts index 47fe5f7..d5ff65f 100644 --- a/src/pages/api/assets/compression/results.ts +++ b/src/pages/api/assets/compression/results.ts @@ -1,6 +1,6 @@ import { db } from "@/config/db"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; -import { getCompressionOutputPath } from "@/helpers/compression.server"; +import { getCompressionOutputFileName, getCompressionOutputPath } from "@/helpers/compression.server"; import { assets, exif } from "@/schema"; import { and, eq, inArray } from "drizzle-orm"; import { readdir, stat } from "node:fs/promises"; @@ -37,13 +37,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const results = await Promise.all(rows.map(async (asset) => { try { - const outputPath = getCompressionOutputPath(asset.id); + const outputPath = getCompressionOutputPath(asset.id, asset.originalFileName); const outputStat = await stat(outputPath); if (!outputStat.isFile()) return null; return { assetId: asset.id, originalFileName: asset.originalFileName, + outputFileName: getCompressionOutputFileName(asset.id, asset.originalFileName), localDateTime: asset.localDateTime, originalSize: Number(asset.originalSize ?? 0), compressedSize: outputStat.size, diff --git a/src/pages/api/assets/compression/transcode.ts b/src/pages/api/assets/compression/transcode.ts index c868b17..85024af 100644 --- a/src/pages/api/assets/compression/transcode.ts +++ b/src/pages/api/assets/compression/transcode.ts @@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) .select({ id: assets.id, type: assets.type, + originalFileName: assets.originalFileName, }) .from(assets) .where(and(eq(assets.id, assetId), eq(assets.ownerId, currentUser.id))); @@ -35,8 +36,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(404).json({ message: "Video not found" }); } - const inputPath = getCompressionInputPath(asset.id); - const outputPath = getCompressionOutputPath(asset.id); + const inputPath = getCompressionInputPath(asset.id, asset.originalFileName); + const outputPath = getCompressionOutputPath(asset.id, asset.originalFileName); try { await clearCompressionDirectory(asset.id); @@ -65,6 +66,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) assetId: asset.id, originalSize: originalStat.size, compressedSize: outputStat.size, + outputFileName: outputPath.split("/").pop(), resultUrl: `/api/assets/compression/result/${asset.id}`, }); } catch (error: any) { diff --git a/src/pages/assets/compress-videos.tsx b/src/pages/assets/compress-videos.tsx index 489a0ef..36c48f5 100644 --- a/src/pages/assets/compress-videos.tsx +++ b/src/pages/assets/compress-videos.tsx @@ -432,7 +432,7 @@ export default function CompressVideosPage() {
-
{item.originalFileName}
+
{item.outputFileName ?? item.originalFileName}
{formatMB(item.originalSize)} → {formatMB(item.compressedSize)} · saved {formatMB(savedBytes)} ({savedPercent}%)