renderizando projetos com sample tracks. basslines ok

This commit is contained in:
JotaChina committed 2025-12-28 16:56:31 -03:00
1 parent be5b12e845
commit 43a9aff447
3 files changed
+130 -32

No files matched your search

+82 -27
View File
@@ -633,7 +633,7 @@ io.on("connection", (socket) => {
// Conversão de projeto em áudio no mmpCreator
app.use(express.json({ limit: "25mb" }));
app.use(express.json({ limit: "250mb" }));
app.use((req, res, next) => {
const origin = req.headers.origin;
@@ -681,9 +681,47 @@ function sanitizeFileName(name) {
.slice(0, 120) || "projeto";
}
function run(cmd, args, { timeoutMs } = {}) {
function isSafeZipEntry(name) {
const s = String(name || "").replace(/\\/g, "/");
if (!s) return false;
if (s.startsWith("/") || /^[a-zA-Z]:\//.test(s)) return false; // absoluto
if (s.includes("..")) return false; // zip slip básico
return true;
}
async function safeUnzip(zipPath, destDir) {
// lista entradas
const list = await run("unzip", ["-Z1", zipPath], { timeoutMs: 60_000 });
const entries = String(list.out || "").split("\n").map(x => x.trim()).filter(Boolean);
if (!entries.length) throw new Error("zip sem entradas (ou unzip falhou ao listar)");
for (const e of entries) {
if (!isSafeZipEntry(e)) throw new Error(`zip contém caminho inseguro: ${e}`);
}
// extrai
await run("unzip", ["-q", zipPath, "-d", destDir], { timeoutMs: 120_000 });
}
async function findFirstMmp(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
const found = await findFirstMmp(p);
if (found) return found;
} else if (e.isFile() && e.name.toLowerCase().endsWith(".mmp")) {
return p;
}
}
return null;
}
function run(cmd, args, { timeoutMs, cwd } = {}) {
return new Promise((resolve, reject) => {
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
let out = "";
let err = "";
@@ -692,7 +730,7 @@ function run(cmd, args, { timeoutMs } = {}) {
const t = setTimeout(() => {
try { p.kill("SIGKILL"); } catch {}
reject(new Error(`LMMS timeout (${timeoutMs}ms)\n${err || out}`));
reject(new Error(`${cmd} timeout (${timeoutMs}ms)\n${err || out}`));
}, timeoutMs || LMMS_TIMEOUT_MS);
p.on("error", (e) => {
@@ -703,7 +741,7 @@ function run(cmd, args, { timeoutMs } = {}) {
p.on("close", (code) => {
clearTimeout(t);
if (code === 0) return resolve({ out, err });
reject(new Error(`LMMS exit code=${code}\n${err || out}`));
reject(new Error(`${cmd} exit code=${code}\n${err || out}`));
});
});
}
@@ -712,7 +750,7 @@ function run(cmd, args, { timeoutMs } = {}) {
const renderLocks = new Map();
app.post("/render", async (req, res) => {
const { roomName, xml, format, name } = req.body || {};
const { roomName, xml, format, name, mmpzBase64, mmpzName } = req.body || {};
const ext = String(format || "wav").toLowerCase();
const allowed = new Set(["wav", "ogg", "flac", "mp3"]);
@@ -720,36 +758,53 @@ app.post("/render", async (req, res) => {
return res.status(400).json({ ok: false, error: "invalid_format", allowed: [...allowed] });
}
// prioridade: sala -> pega do estado autoritativo
let projectXml = null;
if (xml && String(xml).trim().length > 0) {
projectXml = xml;
} else if (roomName) {
if (renderLocks.get(roomName)) {
return res.status(429).json({ ok: false, error: "render_in_progress" });
}
projectXml = ensureRoom(roomName)?.projectXml || null;
}
// fallback: se mandou xml direto
if (!projectXml) projectXml = xml;
if (!projectXml || String(projectXml).trim().length === 0) {
return res.status(400).json({ ok: false, error: "missing_xml_or_room" });
// trava sala (se aplicável)
if (roomName && renderLocks.get(roomName)) {
return res.status(429).json({ ok: false, error: "render_in_progress" });
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lmms-render-"));
const inputPath = path.join(tmpDir, "project.mmp");
const outputPath = path.join(tmpDir, `render.${ext}`);
if (roomName) renderLocks.set(roomName, true);
try {
fs.writeFileSync(inputPath, projectXml, "utf8");
let inputMmpPath = null;
let cwdForLmms = tmpDir;
const { cmd, args } = buildLmmsCommand(inputPath, outputPath, ext);
await run(cmd, args, { timeoutMs: LMMS_TIMEOUT_MS });
// ✅ caminho novo: veio pacote (mmpz/zip) com samples/
if (mmpzBase64 && String(mmpzBase64).trim().length > 0) {
const safeName = sanitizeFileName(mmpzName || "project.mmpz");
const zipPath = path.join(tmpDir, safeName);
fs.writeFileSync(zipPath, Buffer.from(mmpzBase64, "base64"));
await safeUnzip(zipPath, tmpDir);
inputMmpPath = await findFirstMmp(tmpDir);
if (!inputMmpPath) throw new Error("nenhum .mmp encontrado após extrair o pacote");
cwdForLmms = path.dirname(inputMmpPath); // 🔥 isso faz o LMMS resolver samples/...
} else {
// fallback antigo: xml puro (patterns ok, samples não)
let projectXml = null;
if (xml && String(xml).trim().length > 0) {
projectXml = xml;
} else if (roomName) {
projectXml = ensureRoom(roomName)?.projectXml || null;
}
if (!projectXml || String(projectXml).trim().length === 0) {
return res.status(400).json({ ok: false, error: "missing_xml_or_room" });
}
inputMmpPath = path.join(tmpDir, "project.mmp");
fs.writeFileSync(inputMmpPath, projectXml, "utf8");
cwdForLmms = tmpDir;
}
const { cmd, args } = buildLmmsCommand(inputMmpPath, outputPath, ext);
await run(cmd, args, { timeoutMs: LMMS_TIMEOUT_MS, cwd: cwdForLmms });
const fileBase = sanitizeFileName(name || roomName || "projeto");
const downloadName = `${fileBase}.${ext}`;