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:
@@ -190,7 +190,7 @@ export async function createExpressApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ═══ Render Queue ═══
|
// ═══ 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)) {
|
if (!fs.existsSync(RENDERS_DIR)) {
|
||||||
fs.mkdirSync(RENDERS_DIR, { recursive: true });
|
fs.mkdirSync(RENDERS_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,6 +250,28 @@ function setupIPC() {
|
|||||||
return null;
|
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 ═══
|
// ═══ Remotion Browser Pre-download ═══
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ const electronAPI = {
|
|||||||
saveRenderedFile: (renderUrl: string, defaultName: string) =>
|
saveRenderedFile: (renderUrl: string, defaultName: string) =>
|
||||||
ipcRenderer.invoke('file:saveRendered', renderUrl, defaultName) as Promise<string | null>,
|
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 ───
|
// ─── App Info ───
|
||||||
getAppInfo: () =>
|
getAppInfo: () =>
|
||||||
ipcRenderer.invoke('app:info') as Promise<{
|
ipcRenderer.invoke('app:info') as Promise<{
|
||||||
|
|||||||
@@ -198,7 +198,19 @@ export async function exportBatchAsZip(
|
|||||||
.replace(/\s+/g, '_')
|
.replace(/\s+/g, '_')
|
||||||
.replace(/[^a-zA-Z0-9._-]/g, '');
|
.replace(/[^a-zA-Z0-9._-]/g, '');
|
||||||
|
|
||||||
|
// 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);
|
saveAs(zipBlob, zipName);
|
||||||
|
}
|
||||||
|
|
||||||
onProgress?.({ current: total, total, status: 'done' });
|
onProgress?.({ current: total, total, status: 'done' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,23 @@ async function blobUrlToFile(blobUrl: string, fallbackName: string): Promise<Fil
|
|||||||
return new File([blob], `${fallbackName}${ext}`, { type: blob.type });
|
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.
|
* 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
|
* 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();
|
const data = await res.json();
|
||||||
// Return absolute URL so Remotion's bundler (different port) can reach it
|
// Return absolute URL so Remotion's bundler (different port) can reach it
|
||||||
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
|
return `${getExpressOrigin()}${data.url}`;
|
||||||
return `${origin}${data.url}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user