feat: compression 7
This commit is contained in:
1 parent
1b63992d0c
commit
5817752bc4
7 files changed
+52
-24
No files matched your search
@@ -160,6 +160,7 @@ export interface ICompressionResult {
|
||||
assetId: string;
|
||||
originalSize: number;
|
||||
compressedSize: number;
|
||||
outputFileName?: string;
|
||||
resultUrl: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -432,7 +432,7 @@ export default function CompressVideosPage() {
|
||||
<div key={item.assetId} className="border-b last:border-b-0 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{item.originalFileName}</div>
|
||||
<div className="truncate text-sm font-medium">{item.outputFileName ?? item.originalFileName}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{formatMB(item.originalSize)} → {formatMB(item.compressedSize)} · saved {formatMB(savedBytes)} ({savedPercent}%)
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user