listener-ai 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configService.js +4 -0
- package/dist/main.js +75 -0
- package/package.json +1 -1
package/dist/configService.js
CHANGED
|
@@ -194,6 +194,9 @@ class ConfigService {
|
|
|
194
194
|
this.config.minRecordingSeconds = Math.max(0, Math.floor(seconds));
|
|
195
195
|
this.saveConfig();
|
|
196
196
|
}
|
|
197
|
+
getAudioDeviceId() {
|
|
198
|
+
return this.config.audioDeviceId;
|
|
199
|
+
}
|
|
197
200
|
getLastSeenVersion() {
|
|
198
201
|
return this.config.lastSeenVersion;
|
|
199
202
|
}
|
|
@@ -232,6 +235,7 @@ class ConfigService {
|
|
|
232
235
|
maxRecordingMinutes: this.getMaxRecordingMinutes(),
|
|
233
236
|
recordingReminderMinutes: this.getRecordingReminderMinutes(),
|
|
234
237
|
minRecordingSeconds: this.getMinRecordingSeconds(),
|
|
238
|
+
audioDeviceId: this.getAudioDeviceId(),
|
|
235
239
|
lastSeenVersion: this.getLastSeenVersion()
|
|
236
240
|
};
|
|
237
241
|
}
|
package/dist/main.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const electron_1 = require("electron");
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const util_1 = require("util");
|
|
37
39
|
const path = __importStar(require("path"));
|
|
38
40
|
const fs = __importStar(require("fs"));
|
|
39
41
|
const simpleAudioRecorder_1 = require("./simpleAudioRecorder");
|
|
@@ -504,6 +506,79 @@ electron_1.ipcMain.handle('cancel-ffmpeg-download', async () => {
|
|
|
504
506
|
ffmpegManager.cancelDownload();
|
|
505
507
|
return { success: true };
|
|
506
508
|
});
|
|
509
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
510
|
+
// Returns `code: 'ffmpeg-missing'` so the renderer can route users into the
|
|
511
|
+
// existing ffmpeg-download UI (triggered by transcription) rather than
|
|
512
|
+
// duplicating that flow here.
|
|
513
|
+
electron_1.ipcMain.handle('export-recording-m4a', async (_, srcPath) => {
|
|
514
|
+
try {
|
|
515
|
+
if (!srcPath || typeof srcPath !== 'string') {
|
|
516
|
+
return { success: false, error: 'Invalid source path' };
|
|
517
|
+
}
|
|
518
|
+
// Containment: the renderer is trusted today, but bound srcPath to the
|
|
519
|
+
// recordings directory so a future renderer bug can't transcode arbitrary
|
|
520
|
+
// local files. realpath resolves symlinks before the prefix check.
|
|
521
|
+
const recordingsDir = path.join(electron_1.app.getPath('userData'), 'recordings');
|
|
522
|
+
let resolvedSrc;
|
|
523
|
+
try {
|
|
524
|
+
resolvedSrc = await fs.promises.realpath(srcPath);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return { success: false, error: 'Source recording not found' };
|
|
528
|
+
}
|
|
529
|
+
const resolvedRoot = await fs.promises.realpath(recordingsDir).catch(() => recordingsDir);
|
|
530
|
+
if (!resolvedSrc.startsWith(resolvedRoot + path.sep)) {
|
|
531
|
+
return { success: false, error: 'Source path is outside the recordings directory' };
|
|
532
|
+
}
|
|
533
|
+
const ffmpegPath = await ffmpegManager.ensureFFmpeg();
|
|
534
|
+
if (!ffmpegPath) {
|
|
535
|
+
return { success: false, code: 'ffmpeg-missing', error: 'FFmpeg is required for M4A export.' };
|
|
536
|
+
}
|
|
537
|
+
const baseName = path.basename(resolvedSrc, path.extname(resolvedSrc));
|
|
538
|
+
const dialogOptions = {
|
|
539
|
+
title: 'Export recording as M4A',
|
|
540
|
+
defaultPath: `${baseName}.m4a`,
|
|
541
|
+
filters: [{ name: 'M4A Audio', extensions: ['m4a'] }],
|
|
542
|
+
};
|
|
543
|
+
const saveResult = mainWindow
|
|
544
|
+
? await electron_1.dialog.showSaveDialog(mainWindow, dialogOptions)
|
|
545
|
+
: await electron_1.dialog.showSaveDialog(dialogOptions);
|
|
546
|
+
if (saveResult.canceled || !saveResult.filePath) {
|
|
547
|
+
return { canceled: true };
|
|
548
|
+
}
|
|
549
|
+
const destPath = saveResult.filePath;
|
|
550
|
+
// Write to a sibling temp file and atomically rename on success so a
|
|
551
|
+
// failed encode never overwrites the user's picked path with a partial.
|
|
552
|
+
const tmpPath = `${destPath}.partial`;
|
|
553
|
+
try {
|
|
554
|
+
// Force -f ipod (M4A muxer) because the `.partial` extension defeats
|
|
555
|
+
// ffmpeg's format-by-extension detection otherwise.
|
|
556
|
+
await execFileAsync(ffmpegPath, [
|
|
557
|
+
'-y', '-loglevel', 'error',
|
|
558
|
+
'-i', resolvedSrc,
|
|
559
|
+
'-vn', '-c:a', 'aac', '-b:a', '128k',
|
|
560
|
+
'-movflags', '+faststart',
|
|
561
|
+
'-f', 'ipod',
|
|
562
|
+
tmpPath,
|
|
563
|
+
]);
|
|
564
|
+
await fs.promises.rename(tmpPath, destPath);
|
|
565
|
+
}
|
|
566
|
+
catch (encodeError) {
|
|
567
|
+
await fs.promises.unlink(tmpPath).catch(() => { });
|
|
568
|
+
throw encodeError;
|
|
569
|
+
}
|
|
570
|
+
return { success: true, path: destPath };
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.error('Error exporting M4A:', error);
|
|
574
|
+
// execFileAsync rejections carry stderr on the error object — surface it
|
|
575
|
+
// so renderer-side toasts aren't reduced to "Command failed".
|
|
576
|
+
const stderr = error?.stderr;
|
|
577
|
+
const baseMessage = error instanceof Error ? error.message : String(error);
|
|
578
|
+
const message = stderr ? `${baseMessage.split('\n')[0]} — ${stderr.trim()}` : baseMessage;
|
|
579
|
+
return { success: false, error: message };
|
|
580
|
+
}
|
|
581
|
+
});
|
|
507
582
|
// Audio capture runs in the renderer via MediaRecorder; main only tracks state,
|
|
508
583
|
// runs max/reminder timers, updates the menu bar, and writes the final blob to disk.
|
|
509
584
|
electron_1.ipcMain.handle('start-recording', async (_, meetingTitle) => {
|