// controllers/admin/assets.controller.js const path = require("path"); const crypto = require("crypto"); const { Op } = require("sequelize"); const Asset = require("../../models/assets/assets.mdl"); // ─── Helpers ────────────────────────────────────────────────────────────────── const notDeleted = { deletedAt: null }; function resolveFileType(mimeType = "") { if (mimeType.startsWith("image/")) return "image"; if (mimeType.startsWith("video/")) return "video"; if (mimeType.startsWith("application/") || mimeType.startsWith("text/")) return "document"; return "other"; } function resolveExtension(originalName = "") { return path.extname(originalName).replace(".", "").toLowerCase() || null; } function resolveChecksum(buffer) { return crypto.createHash("sha256").update(buffer).digest("hex"); } function resolveResolution(width, height) { if (!width || !height) return null; const h = Math.min(width, height); if (h >= 2160) return "4K"; if (h >= 1440) return "1440p"; if (h >= 1080) return "1080p"; if (h >= 720) return "720p"; if (h >= 480) return "480p"; if (h >= 360) return "360p"; if (h >= 240) return "240p"; return `${width}x${height}`; } // ─── GET ALL ────────────────────────────────────────────────────────────────── exports.getAssets = async (req, res) => { try { const { page = 1, limit = 20, file_type, owner_type, owner_id, uploadedBy, is_public, resolution, search, sort_by = "createdAt", sort_dir = "DESC", } = req.query; const where = { ...notDeleted }; if (file_type) where.file_type = file_type; if (owner_type) where.owner_type = owner_type; if (owner_id) where.owner_id = owner_id; if (uploadedBy) where.uploadedBy = uploadedBy; if (resolution) where.resolution = resolution; if (is_public !== undefined) where.is_public = is_public === "true"; if (search) { where[Op.or] = [ { display_name: { [Op.iLike]: `%${search}%` } }, { original_name: { [Op.iLike]: `%${search}%` } }, { description: { [Op.iLike]: `%${search}%` } }, ]; } const offset = (parseInt(page) - 1) * parseInt(limit); const { count, rows } = await Asset.findAndCountAll({ where, order: [[sort_by, sort_dir.toUpperCase()]], limit: parseInt(limit), offset, }); return res.status(200).json({ data: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / parseInt(limit)), }, }); } catch (err) { console.error("[ASSET][GET ALL]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── GET ONE ────────────────────────────────────────────────────────────────── exports.getAsset = async (req, res) => { try { const { assetId } = req.params; if (!assetId || assetId === "undefined") { return res.status(400).json({ message: "Invalid asset ID." }); } const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted }, }); if (!asset) return res.status(404).json({ message: "Asset not found." }); return res.status(200).json({ data: asset }); } catch (err) { console.error("[ASSET][GET ONE]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── UPLOAD ─────────────────────────────────────────────────────────────────── exports.uploadAsset = async (req, res) => { try { const file = req.file; if (!file) return res.status(400).json({ message: "No file uploaded." }); const { display_name, description, owner_type, owner_id, is_public = false, access_level = "private", storage_provider = "local", storage_bucket, storage_key, uploadedBy, file_url: bodyFileUrl, // video metadata — from ffprobe pipeline or client width, height, duration, frame_rate, bitrate, video_codec, audio_codec, thumbnail_url, } = req.body; if (!uploadedBy) { return res.status(400).json({ message: "uploadedBy is required." }); } const mime_type = file.mimetype; const file_type = resolveFileType(mime_type); const extension = resolveExtension(file.originalname); const checksum = file.buffer ? resolveChecksum(file.buffer) : null; const parsedWidth = width ? parseInt(width) : null; const parsedHeight = height ? parseInt(height) : null; const resolution = file_type === "video" ? resolveResolution(parsedWidth, parsedHeight) : null; const file_url = storage_provider === "local" ? `/uploads/${file.filename}` : bodyFileUrl; if (!file_url) { return res.status(400).json({ message: "file_url is required for non-local storage." }); } const asset = await Asset.create({ original_name: file.originalname, display_name: display_name || file.originalname, file_url, file_size: file.size, mime_type, extension, checksum, file_type, width: parsedWidth, height: parsedHeight, duration: duration ? parseFloat(duration) : null, frame_rate: frame_rate ? parseFloat(frame_rate) : null, bitrate: bitrate ? parseInt(bitrate) : null, video_codec: video_codec || null, audio_codec: audio_codec || null, thumbnail_url: thumbnail_url || null, resolution, description, storage_provider, storage_bucket: storage_bucket || null, storage_key: storage_key || file.filename, is_public, access_level, owner_type: owner_type || null, owner_id: owner_id || null, uploadedBy, }); return res.status(201).json({ data: asset }); } catch (err) { console.error("[ASSET][UPLOAD]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── UPDATE THUMBNAIL ───────────────────────────────────────────────────────── exports.updateThumbnail = async (req, res) => { try { const { assetId } = req.params; const { thumbnail_url } = req.body; if (!thumbnail_url) { return res.status(400).json({ message: "thumbnail_url is required." }); } const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted }, }); if (!asset) return res.status(404).json({ message: "Asset not found." }); asset.thumbnail_url = thumbnail_url; await asset.save(); return res.status(200).json({ data: asset }); } catch (err) { console.error("[ASSET][UPDATE THUMBNAIL]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── UPDATE METADATA ────────────────────────────────────────────────────────── exports.updateAsset = async (req, res) => { try { const { assetId } = req.params; if (!assetId || assetId === "undefined") { return res.status(400).json({ message: "Invalid asset ID." }); } const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted }, }); if (!asset) return res.status(404).json({ message: "Asset not found." }); const allowed = [ "display_name", "description", "owner_type", "owner_id", "is_public", "access_level", "thumbnail_url", "width", "height", "duration", "frame_rate", "bitrate", "video_codec", "audio_codec", ]; allowed.forEach((field) => { if (req.body[field] !== undefined) asset[field] = req.body[field]; }); // Re-derive resolution if dimensions were updated if (req.body.width || req.body.height) { asset.resolution = resolveResolution(asset.width, asset.height); } await asset.save(); return res.status(200).json({ data: asset }); } catch (err) { console.error("[ASSET][UPDATE]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── SOFT DELETE (single) ───────────────────────────────────────────────────── exports.deleteAsset = async (req, res) => { try { const { assetId } = req.params; if (!assetId || assetId === "undefined") { return res.status(400).json({ message: "Invalid asset ID." }); } const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted }, }); if (!asset) return res.status(404).json({ message: "Asset not found." }); asset.deletedAt = new Date(); asset.deletedBy = req.body.deletedBy ?? null; await asset.save(); return res.status(200).json({ message: "Asset deleted." }); } catch (err) { console.error("[ASSET][DELETE]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── SOFT DELETE (bulk) ─────────────────────────────────────────────────────── exports.deleteAssets = async (req, res) => { try { const { ids, deletedBy } = req.body; if (!Array.isArray(ids) || !ids.length) { return res.status(400).json({ message: "ids must be a non-empty array." }); } const [count] = await Asset.update( { deletedAt: new Date(), deletedBy: deletedBy ?? null }, { where: { asset_id: { [Op.in]: ids }, ...notDeleted } }, ); return res.status(200).json({ message: `${count} asset(s) deleted.` }); } catch (err) { console.error("[ASSET][BULK DELETE]", err); return res.status(500).json({ message: "Internal server error." }); } }; // ─── RESTORE (single) ───────────────────────────────────────────────────────── exports.restoreAsset = async (req, res) => { try { const { assetId } = req.params; const asset = await Asset.findOne({ where: { asset_id: assetId, deletedAt: { [Op.not]: null } }, }); if (!asset) { return res.status(404).json({ message: "Asset not found or not deleted." }); } asset.deletedAt = null; asset.deletedBy = null; await asset.save(); return res.status(200).json({ data: asset, message: "Asset restored." }); } catch (err) { console.error("[ASSET][RESTORE]", err); return res.status(500).json({ message: "Internal server error." }); } };