fix: 3 bugs in Electron export pipeline

Bug 1: RENDERS_DIR mismatch (root cause)
- server.ts had hardcoded renders dir, now reads BRADLY_RENDERS_DIR env
- renderQueue.ts saves to ~/Library/.../Bradly/renders/
- server.ts now serves from the same directory

Bug 2: Upload origin pointing to wrong port
- uploadBlobContent.ts used window.location.origin (Vite 5173)
- Remotion bundler needs Express origin (3000) to access media
- Added getExpressOrigin() helper that detects Electron

Bug 3: Batch ZIP export using file-saver (doesn't work in Electron)
- Added saveBlobFile IPC method (preload + main)
- batchExporter.ts now uses native save dialog in Electron
- Web mode falls back to file-saver
This commit is contained in:
2026-06-02 04:44:25 -05:00
parent 25587ab07f
commit 7c4196475c
5 changed files with 61 additions and 4 deletions
+1 -1
View File
@@ -190,7 +190,7 @@ export async function createExpressApp() {
});
// ═══ Render Queue ═══
const RENDERS_DIR = path.join(process.cwd(), "renders");
const RENDERS_DIR = process.env.BRADLY_RENDERS_DIR || path.join(process.cwd(), "renders");
if (!fs.existsSync(RENDERS_DIR)) {
fs.mkdirSync(RENDERS_DIR, { recursive: true });
}
+22
View File
@@ -250,6 +250,28 @@ function setupIPC() {
return null;
}
});
// Save raw blob data to user-chosen location (for ZIP exports etc.)
ipcMain.handle('file:saveBlob', async (_event, data: Uint8Array, defaultName: string, filters?: { name: string; extensions: string[] }[]) => {
if (!mainWindow) return null;
const result = await dialog.showSaveDialog(mainWindow, {
title: 'Guardar archivo',
defaultPath: path.join(app.getPath('downloads'), defaultName),
filters: filters || [{ name: 'Archivo', extensions: ['*'] }],
});
if (result.canceled || !result.filePath) return null;
try {
await fs.promises.writeFile(result.filePath, Buffer.from(data));
console.log(`✅ Blob saved to: ${result.filePath}`);
return result.filePath;
} catch (err) {
console.error('❌ Failed to save blob:', err);
return null;
}
});
}
// ═══ Remotion Browser Pre-download ═══
+7
View File
@@ -37,6 +37,13 @@ const electronAPI = {
saveRenderedFile: (renderUrl: string, defaultName: string) =>
ipcRenderer.invoke('file:saveRendered', renderUrl, defaultName) as Promise<string | null>,
/**
* Save raw binary data (e.g. a ZIP blob) to a user-chosen location.
* Shows native save dialog, then writes the bytes.
*/
saveBlobFile: (data: Uint8Array, defaultName: string, filters?: { name: string; extensions: string[] }[]) =>
ipcRenderer.invoke('file:saveBlob', data, defaultName, filters) as Promise<string | null>,
// ─── App Info ───
getAppInfo: () =>
ipcRenderer.invoke('app:info') as Promise<{
+13 -1
View File
@@ -198,7 +198,19 @@ export async function exportBatchAsZip(
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9._-]/g, '');
saveAs(zipBlob, zipName);
// In Electron, use native save dialog
const electronAPI = (typeof window !== 'undefined') ? (window as any).electronAPI : null;
if (electronAPI?.saveBlobFile) {
const arrayBuffer = await zipBlob.arrayBuffer();
await electronAPI.saveBlobFile(
new Uint8Array(arrayBuffer),
zipName,
[{ name: 'ZIP Archive', extensions: ['zip'] }],
);
} else {
// Web fallback
saveAs(zipBlob, zipName);
}
onProgress?.({ current: total, total, status: 'done' });
}
+18 -2
View File
@@ -27,6 +27,23 @@ async function blobUrlToFile(blobUrl: string, fallbackName: string): Promise<Fil
return new File([blob], `${fallbackName}${ext}`, { type: blob.type });
}
/**
* Get the Express server origin for building absolute media URLs.
* In Electron dev mode, the renderer runs on Vite (5173) but media
* is served by Express (3000). Remotion's bundler needs Express URLs.
*/
function getExpressOrigin(): string {
if (typeof window !== 'undefined' && (window as any).electronAPI?.isElectron) {
// In Electron, Express is always on 127.0.0.1:3000
return 'http://127.0.0.1:3000';
}
// In web mode, Express IS the origin
if (typeof window !== 'undefined') {
return window.location.origin;
}
return 'http://localhost:3000';
}
/**
* Upload a single File to the server and return an absolute persistent URL.
* Must be absolute because Remotion's server-side bundler runs on a different
@@ -44,8 +61,7 @@ async function uploadFile(file: File): Promise<string> {
}
const data = await res.json();
// Return absolute URL so Remotion's bundler (different port) can reach it
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
return `${origin}${data.url}`;
return `${getExpressOrigin()}${data.url}`;
}
/**