mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
added and fix some of things
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
// controllers/admin/assets.controller.js
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const sequelize = require("../../config/db.config");
|
||||
const Asset = require("../../models/assets/assets.mdl");
|
||||
const chibi = require("../../services/chibisafe.service");
|
||||
const s3 = require("../../services/s3.service");
|
||||
const mediaToken = require("../../services/mediaToken.service");
|
||||
const uploadProgress = require("../../services/uploadProgress.service");
|
||||
const { extractVideoMeta } = require("../../services/ffprobe.service");
|
||||
const ffmpegSvc = require("../../services/ffmpeg.service");
|
||||
const assetTranscode = require("../../services/assetTranscode.service");
|
||||
const documentConversion = require("../../services/documentConversion.service");
|
||||
const R = require('../../utils/response.util');
|
||||
const { paginate } = require("../../utils/paginate.util");
|
||||
const { adminExclude, jsonbSchemas, computedAttributes } = require("../../models/assets/assets.attributes");
|
||||
@@ -58,15 +57,6 @@ function resolveExtension(originalName = "") {
|
||||
return path.extname(originalName).replace(".", "").toLowerCase() || null;
|
||||
}
|
||||
|
||||
function streamToBuffer(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream.on("data", (chunk) => chunks.push(chunk));
|
||||
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveResolution(width, height) {
|
||||
if (!width || !height) return null;
|
||||
const h = Math.min(width, height);
|
||||
@@ -398,7 +388,31 @@ async function finalizeAssetFromStorage({ storage_key, thumbnail_storage_key, or
|
||||
video_codec = videoMeta.video_codec;
|
||||
audio_codec = videoMeta.audio_codec;
|
||||
|
||||
if (thumbnail_storage_key) thumbnail_url = await svc.buildPublicUrl(thumbnail_storage_key);
|
||||
if (thumbnail_storage_key) {
|
||||
thumbnail_url = await svc.buildPublicUrl(thumbnail_storage_key);
|
||||
} else if (file_type === "video") {
|
||||
// No client-provided thumbnail — grab a frame from the video itself so
|
||||
// the asset doesn't sit with no preview at all in every picker/library
|
||||
// grid. Best-effort: a failure here must not fail the whole upload.
|
||||
let framePath = null;
|
||||
try {
|
||||
framePath = await ffmpegSvc.extractFrameThumbnail(probeUrl, duration);
|
||||
const uploaded = await svc.uploadStream({
|
||||
stream: fs.createReadStream(framePath),
|
||||
originalname: `${(original_name || "thumb").replace(/\.[^.]+$/, "")}.jpg`,
|
||||
mimetype: "image/jpeg",
|
||||
ownerType: "thumbnail", // → thumbnails/ prefix, same as manually-uploaded thumbnails
|
||||
});
|
||||
thumbnail_storage_key = uploaded.uuid;
|
||||
thumbnail_url = uploaded.url;
|
||||
uploadedFiles.push({ key: thumbnail_storage_key, provider: storage_provider }); // rollback cleanup on later failure
|
||||
} catch (err) {
|
||||
console.warn(`[ASSET][THUMBNAIL] Auto-generate failed for "${storage_key}":`, err.message);
|
||||
// leave thumbnail_url null — same fallback as before, admin can add one manually later
|
||||
} finally {
|
||||
if (framePath) fs.promises.unlink(framePath).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
const parsedWidth = body.width ? parseInt(body.width) : null;
|
||||
@@ -435,6 +449,7 @@ async function finalizeAssetFromStorage({ storage_key, thumbnail_storage_key, or
|
||||
video_codec,
|
||||
audio_codec,
|
||||
thumbnail_url,
|
||||
thumbnail_storage_key,
|
||||
description,
|
||||
storage_provider,
|
||||
storage_bucket: storage_bucket || (storage_provider === "s3" ? process.env.S3_BUCKET : null) || null,
|
||||
@@ -572,90 +587,6 @@ exports.uploadAsset = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Same broadcaster, reused as-is for document-conversion job progress — it's
|
||||
// just a generic string-keyed SSE channel, nothing upload-specific about it.
|
||||
exports.streamConvertProgress = (req, res) => {
|
||||
uploadProgress.subscribe(req.params.jobId, res);
|
||||
};
|
||||
|
||||
// ─── CONVERT TO MARKDOWN ────────────────────────────────────────────────────
|
||||
//
|
||||
// PDF/PPTX -> Markdown, text only (see services/documentConversion.service.js
|
||||
// for the compile/validate/automate stages and why OCR/images are out of
|
||||
// scope). Nothing here is written to the database — the caller (the Lesson
|
||||
// block builder) treats the response as a draft and only persists it if/when
|
||||
// the admin explicitly inserts it into a block and saves the lesson page.
|
||||
//
|
||||
// Mirrors the upload flow's SSE progress pattern exactly: the actual work
|
||||
// runs synchronously inside this request (uploadProgress.service.js is
|
||||
// reused as-is, keyed by a client-generated jobId) while stage transitions
|
||||
// are published for a live "Compiling / Validating / Generating" UI, same as
|
||||
// AddAsset.jsx already renders for uploads.
|
||||
//
|
||||
const CONVERTIBLE_EXTENSIONS = new Set(["pdf", "pptx"]);
|
||||
const MAX_CONVERT_SIZE_BYTES = 25 * 1024 * 1024; // 25MB — keeps this comfortably synchronous
|
||||
// compile() calls the MarkItDown sidecar over HTTP now instead of running
|
||||
// officeparser in-process, so this leaves a bit more room than the original
|
||||
// 45s for network/queueing overhead — MarkItDown's own parsing is plain
|
||||
// CPU-bound work, not ML inference, so it doesn't need much more than that.
|
||||
const CONVERT_TIMEOUT_MS = 60_000;
|
||||
|
||||
exports.convertAssetToMarkdown = async (req, res) => {
|
||||
const { jobId } = req.body;
|
||||
|
||||
try {
|
||||
const { assetId } = req.params;
|
||||
if (!assetId || assetId === "undefined") return R.error(res, "Invalid asset ID.", 400);
|
||||
|
||||
const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted } });
|
||||
if (!asset) return R.error(res, "Asset not found.", 404);
|
||||
|
||||
const extension = (asset.extension || "").toLowerCase();
|
||||
if (!CONVERTIBLE_EXTENSIONS.has(extension)) {
|
||||
return R.error(res, "Only PDF and PPTX documents can be converted to Markdown.", 400);
|
||||
}
|
||||
if (asset.storage_provider !== "s3" || !asset.storage_key) {
|
||||
return R.error(res, "This asset has no stored file to convert.", 400);
|
||||
}
|
||||
if (Number(asset.file_size) > MAX_CONVERT_SIZE_BYTES) {
|
||||
return R.error(res, "File is too large to convert (25MB max).", 400);
|
||||
}
|
||||
|
||||
const publish = (phase) => { if (jobId) uploadProgress.publish(jobId, { phase }); };
|
||||
const abortController = new AbortController();
|
||||
const timeout = setTimeout(() => abortController.abort(), CONVERT_TIMEOUT_MS);
|
||||
|
||||
let markdown, warnings, stats;
|
||||
try {
|
||||
publish("compiling");
|
||||
const { stream } = await s3.getObjectStream(asset.storage_key);
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const ast = await documentConversion.compile(buffer, extension, { signal: abortController.signal });
|
||||
|
||||
publish("validating");
|
||||
const validated = documentConversion.validate(ast);
|
||||
|
||||
publish("generating");
|
||||
const generated = await documentConversion.automate(ast, { signal: abortController.signal });
|
||||
|
||||
markdown = generated.markdown;
|
||||
warnings = [...validated.warnings, ...generated.messages];
|
||||
stats = validated.stats;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
if (jobId) uploadProgress.complete(jobId, { phase: "done" });
|
||||
return R.success(res, "Document converted.", { markdown, warnings, stats });
|
||||
|
||||
} catch (err) {
|
||||
if (jobId) uploadProgress.complete(jobId, { phase: "error", message: err.message });
|
||||
console.error("[ASSET][CONVERT TO MARKDOWN]", err);
|
||||
if (err.status) return R.error(res, err.message, err.status);
|
||||
return R.error(res, "Internal server error.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.updateAsset = async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user