900 lines
28 KiB
JavaScript
900 lines
28 KiB
JavaScript
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
|
const { join } = require('path');
|
|
const { writeFile, mkdir, access, unlink, readFile } = require('fs').promises;
|
|
const { constants } = require('fs');
|
|
const Store = require('electron-store');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const https = require('https');
|
|
const FormData = require('form-data');
|
|
const { exec } = require('child_process');
|
|
const { promisify } = require('util');
|
|
const execAsync = promisify(exec);
|
|
const { spawn } = require('child_process');
|
|
|
|
const store = new Store();
|
|
|
|
// Log file path
|
|
const LOG_FILE_PATH = path.join(app.getPath('userData'), 'app.log');
|
|
|
|
// Ensure log file exists
|
|
if (!fs.existsSync(LOG_FILE_PATH)) {
|
|
fs.writeFileSync(LOG_FILE_PATH, '');
|
|
}
|
|
|
|
// Log file management functions
|
|
function saveLog(logEntry) {
|
|
const logLine = JSON.stringify(logEntry) + '\n';
|
|
fs.appendFileSync(LOG_FILE_PATH, logLine);
|
|
}
|
|
|
|
function getLogs() {
|
|
try {
|
|
const logContent = fs.readFileSync(LOG_FILE_PATH, 'utf8');
|
|
return logContent
|
|
.split('\n')
|
|
.filter(line => line.trim())
|
|
.map(line => JSON.parse(line));
|
|
} catch (error) {
|
|
console.error('Error reading logs:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function clearLogs() {
|
|
fs.writeFileSync(LOG_FILE_PATH, '');
|
|
}
|
|
|
|
// Check if ffmpeg is installed
|
|
async function checkFFmpeg() {
|
|
try {
|
|
// Look in extraResources path in packaged app, otherwise use relative dev path
|
|
const basePath = app.isPackaged
|
|
? process.resourcesPath
|
|
: join(__dirname, '..');
|
|
|
|
// Determine the correct subdirectory based on platform
|
|
const platformBinDir = process.platform === 'win32' ? 'bin' : 'linux_bin';
|
|
const ffmpegBinaryName = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
|
|
|
const ffmpegPath = join(basePath, 'ffmpeg', platformBinDir, ffmpegBinaryName);
|
|
|
|
console.log(`[Main] Checking for FFmpeg at: ${ffmpegPath}`);
|
|
await fs.promises.access(ffmpegPath);
|
|
console.log('[Main] FFmpeg found.');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[Main] FFmpeg check failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Install FFmpeg
|
|
async function installFFmpeg() {
|
|
try {
|
|
console.log('Installing FFmpeg...');
|
|
|
|
// Create temp directory for download
|
|
const tempDir = join(app.getPath('temp'), 'ffmpeg-install');
|
|
await mkdir(tempDir, { recursive: true });
|
|
|
|
let downloadUrl;
|
|
let extractPath;
|
|
let ffmpegDir = join(app.getPath('userData'), 'ffmpeg');
|
|
|
|
if (process.platform === 'win32') {
|
|
downloadUrl = 'https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.zip';
|
|
const zipPath = join(tempDir, 'ffmpeg.zip');
|
|
|
|
console.log('Downloading FFmpeg from:', downloadUrl);
|
|
await new Promise((resolve, reject) => {
|
|
const file = fs.createWriteStream(zipPath);
|
|
https.get(downloadUrl, (response) => {
|
|
response.pipe(file);
|
|
file.on('finish', () => {
|
|
file.close();
|
|
resolve(null);
|
|
});
|
|
}).on('error', (err) => {
|
|
fs.unlink(zipPath, () => {});
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
// Extract the zip file
|
|
console.log('Extracting FFmpeg...');
|
|
extractPath = join(tempDir, 'extracted');
|
|
await mkdir(extractPath, { recursive: true });
|
|
|
|
const { stdout: extractOutput } = await execAsync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractPath}' -Force"`);
|
|
console.log('Extraction output:', extractOutput);
|
|
|
|
// Copy FFmpeg files to a permanent location
|
|
await mkdir(ffmpegDir, { recursive: true });
|
|
|
|
const ffmpegFiles = ['ffmpeg.exe', 'ffprobe.exe', 'ffplay.exe'];
|
|
for (const file of ffmpegFiles) {
|
|
const sourcePath = join(extractPath, 'ffmpeg-7.1.1-full_build', 'bin', file);
|
|
const targetPath = join(ffmpegDir, file);
|
|
await fs.promises.copyFile(sourcePath, targetPath);
|
|
}
|
|
} else if (process.platform === 'darwin') {
|
|
// For macOS, we'll use Homebrew
|
|
try {
|
|
await execAsync('brew install ffmpeg');
|
|
ffmpegDir = '/usr/local/bin';
|
|
} catch (error) {
|
|
console.error('Error installing FFmpeg via Homebrew:', error);
|
|
throw new Error('Failed to install FFmpeg via Homebrew. Please install it manually.');
|
|
}
|
|
} else {
|
|
// For Linux, we'll use apt-get
|
|
try {
|
|
await execAsync('sudo apt-get update && sudo apt-get install -y ffmpeg');
|
|
ffmpegDir = '/usr/bin';
|
|
} catch (error) {
|
|
console.error('Error installing FFmpeg via apt-get:', error);
|
|
throw new Error('Failed to install FFmpeg via apt-get. Please install it manually.');
|
|
}
|
|
}
|
|
|
|
// Add FFmpeg directory to PATH
|
|
const userProfile = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
|
const pathFile = join(userProfile, '.ffmpeg-path');
|
|
await fs.promises.writeFile(pathFile, ffmpegDir);
|
|
|
|
// Clean up
|
|
if (process.platform === 'win32') {
|
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
|
|
console.log('FFmpeg installation completed successfully');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error installing FFmpeg:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Get FFmpeg download URL based on platform
|
|
function getFFmpegDownloadUrl() {
|
|
return process.platform === 'win32'
|
|
? 'https://www.gyan.dev/ffmpeg/builds/'
|
|
: process.platform === 'darwin'
|
|
? 'https://evermeet.cx/ffmpeg/'
|
|
: 'https://ffmpeg.org/download.html';
|
|
}
|
|
|
|
// Initialize default settings if they don't exist
|
|
async function initializeDefaultSettings() {
|
|
const defaultSaveDir = join(app.getPath('music'), 'PodcastUpptokur');
|
|
console.log('Initializing default save directory:', defaultSaveDir);
|
|
|
|
// Ensure the base directory exists
|
|
try {
|
|
await access(defaultSaveDir, constants.F_OK);
|
|
} catch {
|
|
console.log('Creating default directory:', defaultSaveDir);
|
|
await mkdir(defaultSaveDir, { recursive: true });
|
|
}
|
|
|
|
// Initialize settings with default values
|
|
const defaultSettings = {
|
|
saveDirectory: defaultSaveDir,
|
|
borgEnvironment: 'borg.unak.is',
|
|
audioSettings: {
|
|
bitrate: 256000,
|
|
sampleRate: 48000,
|
|
channels: 2
|
|
},
|
|
userSettings: {} // Store user-specific settings
|
|
};
|
|
|
|
// Get current settings or use defaults
|
|
const currentSettings = store.get('settings') || defaultSettings;
|
|
|
|
// Merge current settings with defaults, preserving user-specific settings
|
|
const mergedSettings = {
|
|
...defaultSettings,
|
|
...currentSettings,
|
|
userSettings: {
|
|
...defaultSettings.userSettings,
|
|
...(currentSettings.userSettings || {})
|
|
}
|
|
};
|
|
|
|
// Save the merged settings
|
|
store.set('settings', mergedSettings);
|
|
console.log('Settings initialized:', mergedSettings);
|
|
}
|
|
|
|
// Get FFmpeg path
|
|
async function getFFmpegPath() {
|
|
try {
|
|
// Look in extraResources path in packaged app, otherwise use relative dev path
|
|
const basePath = app.isPackaged
|
|
? process.resourcesPath
|
|
: join(__dirname, '..');
|
|
|
|
// Determine the correct subdirectory and binary name based on platform
|
|
const platformBinDir = process.platform === 'win32' ? 'bin' : 'linux_bin';
|
|
const ffmpegBinaryName = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
|
const ffprobeBinaryName = process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe'; // Also need ffprobe path
|
|
|
|
const ffmpegPath = join(basePath, 'ffmpeg', platformBinDir, ffmpegBinaryName);
|
|
|
|
// Check access again to be sure
|
|
try {
|
|
await fs.promises.access(ffmpegPath);
|
|
console.log('[Main] Found FFmpeg path for execution:', ffmpegPath);
|
|
// Return object with both paths, or just ffmpeg path?
|
|
// Let's return just ffmpeg path for now, processAudioFile needs update
|
|
return ffmpegPath;
|
|
} catch (accessError) {
|
|
console.error('[Main] FFmpeg not accessible at expected path:', ffmpegPath, accessError);
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error('[Main] Error getting FFmpeg path:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Process audio file with ffmpeg to ensure proper metadata
|
|
async function processAudioFile(inputPath, outputPath, email) {
|
|
try {
|
|
// Check if FFmpeg is installed (this implicitly gets the path now)
|
|
const ffmpegPath = await getFFmpegPath();
|
|
if (!ffmpegPath) {
|
|
throw new Error('FFmpeg executable not found or not accessible.');
|
|
}
|
|
|
|
// Determine platform-specific paths for ffprobe
|
|
const platformBinDir = process.platform === 'win32' ? 'bin' : 'linux_bin';
|
|
const ffprobeBinaryName = process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe';
|
|
// Construct ffprobe path relative to ffmpeg path or base path
|
|
const ffprobePath = join(path.dirname(ffmpegPath), ffprobeBinaryName);
|
|
// Verify ffprobe exists too
|
|
try {
|
|
await fs.promises.access(ffprobePath);
|
|
console.log('[Main] Found FFprobe path for execution:', ffprobePath);
|
|
} catch (ffprobeAccessError) {
|
|
console.error('[Main] FFprobe not accessible at expected path:', ffprobePath, ffprobeAccessError);
|
|
throw new Error('FFprobe executable not found or not accessible.');
|
|
}
|
|
|
|
// Get current date in ISO format
|
|
const currentDate = new Date().toISOString();
|
|
|
|
// Create FFmpeg command with explicit metadata
|
|
const ffmpegCommand = [
|
|
'-i', inputPath,
|
|
'-c:a', 'libopus', // Use Opus codec for WebM
|
|
'-b:a', '256k', // Set bitrate to 256k
|
|
'-ar', '48000', // Set sample rate to 48kHz
|
|
'-ac', '2', // Set to stereo
|
|
'-metadata', 'title=Podcast Recording',
|
|
'-metadata', `artist=${email}`,
|
|
'-metadata', 'album=PodcastUpptokur',
|
|
'-metadata', `date=${currentDate}`,
|
|
'-metadata', 'encoder=PodcastUpptokur',
|
|
'-metadata', 'copyright=© 2025 PodcastUpptokur',
|
|
'-metadata', 'genre=Podcast',
|
|
'-metadata', 'language=is', // Icelandic language code
|
|
'-y', // Overwrite output file if it exists
|
|
outputPath
|
|
];
|
|
|
|
console.log('Running FFmpeg command:', ffmpegCommand.join(' '));
|
|
|
|
// Run FFmpeg command using the found path
|
|
await new Promise((resolve, reject) => {
|
|
const ffmpeg = spawn(ffmpegPath, ffmpegCommand);
|
|
|
|
let errorOutput = '';
|
|
|
|
ffmpeg.stderr.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
console.log('FFmpeg progress:', data.toString());
|
|
});
|
|
|
|
ffmpeg.on('close', (code) => {
|
|
if (code === 0) {
|
|
console.log('FFmpeg processing completed successfully');
|
|
resolve();
|
|
} else {
|
|
console.error('FFmpeg error output:', errorOutput);
|
|
reject(new Error(`FFmpeg process exited with code ${code}`));
|
|
}
|
|
});
|
|
|
|
ffmpeg.on('error', (err) => {
|
|
console.error('FFmpeg process error:', err);
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
// Verify the output file exists and has content
|
|
const stats = await fs.promises.stat(outputPath);
|
|
if (stats.size === 0) {
|
|
throw new Error('FFmpeg produced an empty file');
|
|
}
|
|
|
|
// Verify metadata using FFprobe using the found path
|
|
const ffprobeCommand = [
|
|
'-v', 'error',
|
|
'-show_entries', 'format_tags',
|
|
'-of', 'json',
|
|
outputPath
|
|
];
|
|
|
|
const metadata = await new Promise((resolve, reject) => {
|
|
const ffprobe = spawn(ffprobePath, ffprobeCommand);
|
|
let output = '';
|
|
|
|
ffprobe.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
ffprobe.on('close', (code) => {
|
|
if (code === 0) {
|
|
try {
|
|
const metadata = JSON.parse(output);
|
|
resolve(metadata);
|
|
} catch (err) {
|
|
reject(new Error('Failed to parse FFprobe output'));
|
|
}
|
|
} else {
|
|
reject(new Error(`FFprobe process exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
console.log('File metadata:', metadata);
|
|
|
|
// Verify essential metadata fields
|
|
const tags = metadata.format?.tags || {};
|
|
const tagsLower = {};
|
|
for (const [key, value] of Object.entries(tags)) {
|
|
tagsLower[key.toLowerCase()] = value;
|
|
}
|
|
const requiredFields = ['title', 'artist', 'date'];
|
|
const missingFields = requiredFields.filter(field => !tagsLower[field]);
|
|
if (missingFields.length > 0) {
|
|
throw new Error(`Missing required metadata fields: ${missingFields.join(', ')}`);
|
|
}
|
|
|
|
return outputPath;
|
|
} catch (error) {
|
|
console.error('Error in processAudioFile:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
let mainWindow;
|
|
|
|
async function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
frame: false,
|
|
webPreferences: {
|
|
preload: join(__dirname, 'preload.cjs'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false
|
|
}
|
|
});
|
|
|
|
// In development, use Vite's dev server
|
|
if (!app.isPackaged) {
|
|
try {
|
|
await mainWindow.loadURL('http://127.0.0.1:5173');
|
|
mainWindow.webContents.openDevTools();
|
|
} catch (error) {
|
|
console.error('Failed to load dev server:', error);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
// In production, load the built files
|
|
mainWindow.loadFile(join(__dirname, '../dist/index.html'));
|
|
}
|
|
|
|
// Add CSS to make the top bar draggable
|
|
mainWindow.webContents.insertCSS(`
|
|
.window-drag {
|
|
-webkit-app-region: drag;
|
|
}
|
|
.window-no-drag {
|
|
-webkit-app-region: no-drag;
|
|
}
|
|
`);
|
|
|
|
mainWindow.on('closed', () => {
|
|
mainWindow = null;
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(async () => {
|
|
// Initialize default settings
|
|
await initializeDefaultSettings();
|
|
|
|
// Ensure the default directory exists
|
|
const settings = store.get('settings');
|
|
console.log('Current settings:', settings);
|
|
|
|
try {
|
|
await access(settings.saveDirectory, constants.F_OK);
|
|
console.log('Directory exists:', settings.saveDirectory);
|
|
} catch {
|
|
console.log('Creating directory:', settings.saveDirectory);
|
|
await mkdir(settings.saveDirectory, { recursive: true });
|
|
}
|
|
|
|
await createWindow();
|
|
|
|
// Check for ffmpeg after window is created
|
|
const hasFFmpeg = await checkFFmpeg();
|
|
// Notify renderer about ffmpeg status
|
|
mainWindow.webContents.send('ffmpeg-status', hasFFmpeg);
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
// IPC handlers
|
|
ipcMain.handle('get-default-directory', async () => {
|
|
const settings = store.get('settings');
|
|
console.log('Getting default directory:', settings.saveDirectory);
|
|
|
|
// Ensure the directory exists
|
|
try {
|
|
await access(settings.saveDirectory, constants.F_OK);
|
|
} catch {
|
|
console.log('Creating default directory:', settings.saveDirectory);
|
|
await mkdir(settings.saveDirectory, { recursive: true });
|
|
}
|
|
|
|
return settings.saveDirectory;
|
|
});
|
|
|
|
ipcMain.handle('select-directory', async () => {
|
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
properties: ['openDirectory']
|
|
});
|
|
|
|
if (!result.canceled && result.filePaths.length > 0) {
|
|
const newDir = result.filePaths[0];
|
|
console.log('Selected new directory:', newDir);
|
|
store.set('settings.saveDirectory', newDir);
|
|
return newDir;
|
|
}
|
|
|
|
throw new Error('No directory selected');
|
|
});
|
|
|
|
ipcMain.handle('save-recording', async (event, fileName, data, email) => {
|
|
try {
|
|
const settings = store.get('settings');
|
|
if (!settings || !settings.saveDirectory) {
|
|
throw new Error('No save directory configured');
|
|
}
|
|
|
|
// Defensive check for email
|
|
if (!email || typeof email !== 'string' || email.trim() === '') {
|
|
console.error('No email provided to save-recording!');
|
|
throw new Error('Email is required to save a recording.');
|
|
}
|
|
|
|
// Create email-specific subfolder
|
|
const userDir = join(settings.saveDirectory, email);
|
|
console.log('User directory:', userDir);
|
|
|
|
// Ensure the user directory exists
|
|
try {
|
|
await access(userDir, constants.F_OK);
|
|
console.log('User directory exists:', userDir);
|
|
} catch {
|
|
console.log('Creating user directory:', userDir);
|
|
await mkdir(userDir, { recursive: true });
|
|
}
|
|
|
|
// Use the user directory for both temp and final files
|
|
const tempFilePath = join(userDir, `temp_${fileName}`);
|
|
const finalFilePath = join(userDir, fileName);
|
|
console.log('Saving recording to:', tempFilePath);
|
|
|
|
// Validate base64 data
|
|
if (!data || typeof data !== 'string') {
|
|
throw new Error('Invalid recording data');
|
|
}
|
|
|
|
// Write to temporary file first
|
|
const buffer = Buffer.from(data, 'base64');
|
|
await writeFile(tempFilePath, buffer);
|
|
console.log('Temporary file written successfully');
|
|
|
|
// Process the audio file with FFmpeg
|
|
try {
|
|
await processAudioFile(tempFilePath, finalFilePath, email);
|
|
console.log('Audio processing completed');
|
|
} catch (error) {
|
|
console.error('Error processing audio:', error);
|
|
throw new Error('Failed to process audio file');
|
|
}
|
|
|
|
// Clean up temporary file
|
|
try {
|
|
await unlink(tempFilePath);
|
|
console.log('Temporary file cleaned up');
|
|
} catch (error) {
|
|
console.warn('Could not delete temporary file:', error);
|
|
}
|
|
|
|
return finalFilePath;
|
|
} catch (error) {
|
|
console.error('Error saving recording:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('get-recording-path', async (event, fileName, email) => {
|
|
try {
|
|
const settings = store.get('settings');
|
|
if (!settings || !settings.saveDirectory) {
|
|
throw new Error('No save directory configured');
|
|
}
|
|
|
|
// Use email-specific subfolder
|
|
const userDir = join(settings.saveDirectory, email);
|
|
const filePath = join(userDir, fileName);
|
|
console.log('Getting recording path:', filePath);
|
|
return filePath;
|
|
} catch (error) {
|
|
console.error('Error getting recording path:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('read-recording-data', async (event, fileName, email) => {
|
|
try {
|
|
const settings = store.get('settings');
|
|
const userDir = path.join(settings.saveDirectory, email);
|
|
const filePath = path.join(userDir, fileName);
|
|
console.log('Reading file:', filePath);
|
|
|
|
// Read file using fs.promises
|
|
const buffer = await fs.promises.readFile(filePath);
|
|
console.log('File read successfully, size:', buffer.length);
|
|
|
|
if (buffer.length === 0) {
|
|
throw new Error('File is empty');
|
|
}
|
|
|
|
// Convert buffer to base64
|
|
const base64 = buffer.toString('base64');
|
|
console.log('Base64 data length:', base64.length);
|
|
|
|
// Create data URL with proper MIME type
|
|
const dataUrl = `data:audio/webm;codecs=opus;base64,${base64}`;
|
|
console.log('Data URL length:', dataUrl.length);
|
|
|
|
return dataUrl;
|
|
} catch (error) {
|
|
console.error('Error reading recording:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('delete-recording', async (event, fileName, email) => {
|
|
try {
|
|
const settings = store.get('settings');
|
|
const userDir = join(settings.saveDirectory, email);
|
|
const filePath = join(userDir, fileName);
|
|
console.log('Deleting recording:', filePath);
|
|
|
|
try {
|
|
await unlink(filePath);
|
|
console.log('Recording deleted successfully');
|
|
} catch (error) {
|
|
// If the file doesn't exist, we can consider this a successful deletion
|
|
if (error.code === 'ENOENT') {
|
|
console.log('File already deleted:', filePath);
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting recording:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('list-recordings', async (event, email) => {
|
|
try {
|
|
const settings = store.get('settings');
|
|
if (!settings || !settings.saveDirectory) {
|
|
throw new Error('No save directory configured');
|
|
}
|
|
|
|
const userDir = join(settings.saveDirectory, email);
|
|
console.log('Listing recordings in:', userDir);
|
|
|
|
// Ensure directory exists
|
|
try {
|
|
await access(userDir, constants.F_OK);
|
|
console.log('User directory exists:', userDir);
|
|
} catch {
|
|
console.log('User directory does not exist, creating:', userDir);
|
|
await mkdir(userDir, { recursive: true });
|
|
console.log('Created user directory, returning empty list');
|
|
return [];
|
|
}
|
|
|
|
const files = await fs.promises.readdir(userDir);
|
|
console.log('Found files:', files);
|
|
|
|
const recordings = await Promise.all(
|
|
files
|
|
.filter(file => file.endsWith('.webm'))
|
|
.map(async file => {
|
|
const filePath = join(userDir, file);
|
|
const stats = await fs.promises.stat(filePath);
|
|
return {
|
|
fileName: file,
|
|
filePath: filePath,
|
|
date: stats.mtime,
|
|
size: stats.size
|
|
};
|
|
})
|
|
);
|
|
|
|
// Sort by date, newest first
|
|
recordings.sort((a, b) => b.date - a.date);
|
|
|
|
console.log('Returning recordings:', recordings);
|
|
return recordings;
|
|
} catch (error) {
|
|
console.error('Error listing recordings:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('upload-recording', async (event, fileName, email, borgEnvironment) => {
|
|
try {
|
|
const settings = store.get('settings');
|
|
const userDir = join(settings.saveDirectory, email);
|
|
const filePath = join(userDir, fileName);
|
|
console.log('Uploading file:', filePath);
|
|
|
|
// Get file stats
|
|
const stats = await fs.promises.stat(filePath);
|
|
console.log('File stats:', {
|
|
size: stats.size,
|
|
created: stats.birthtime,
|
|
modified: stats.mtime
|
|
});
|
|
|
|
// Use the final file directly, do not process again
|
|
// Create form data using form-data package
|
|
const form = new FormData();
|
|
|
|
// Append final file using createReadStream with proper content type
|
|
form.append('audioFile', fs.createReadStream(filePath), {
|
|
filename: fileName,
|
|
contentType: 'audio/webm' // Always use audio/webm for both regular and saved files
|
|
});
|
|
form.append('userEmail', email);
|
|
|
|
// Create request options
|
|
const options = {
|
|
hostname: borgEnvironment || 'borg.unak.is',
|
|
path: '/api/studio/podcasts',
|
|
method: 'POST',
|
|
headers: {
|
|
'X-API-Key': '0f4801033b3622cc2029e5f2159ff76ea0e3e326',
|
|
'Content-Type': 'multipart/form-data',
|
|
...form.getHeaders()
|
|
}
|
|
};
|
|
|
|
console.log('Request options:', {
|
|
hostname: options.hostname,
|
|
path: options.path,
|
|
method: options.method,
|
|
headers: options.headers
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
|
|
console.log('Response headers:', res.headers);
|
|
|
|
res.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
|
|
res.on('end', () => {
|
|
console.log('Server response status:', res.statusCode);
|
|
console.log('Server response:', data);
|
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
console.log('Upload completed successfully');
|
|
resolve();
|
|
} else {
|
|
console.error('Upload failed with status:', res.statusCode);
|
|
console.error('Response:', data);
|
|
reject(new Error(`Upload failed with status ${res.statusCode}: ${data}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', (error) => {
|
|
console.error('Upload error:', error);
|
|
reject(error);
|
|
});
|
|
|
|
// Handle upload progress
|
|
form.on('progress', (bytesUploaded, bytesTotal) => {
|
|
if (bytesTotal) {
|
|
const progress = (bytesUploaded / bytesTotal) * 100;
|
|
console.log('Upload progress:', progress.toFixed(2) + '%');
|
|
event.sender.send('upload-progress', progress);
|
|
}
|
|
});
|
|
|
|
// Pipe the form data to the request
|
|
form.pipe(req);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in upload-recording handler:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Add new IPC handlers for ffmpeg status
|
|
ipcMain.handle('check-ffmpeg', async () => {
|
|
const hasFFmpeg = await checkFFmpeg();
|
|
return hasFFmpeg;
|
|
});
|
|
|
|
// Add new IPC handler for FFmpeg download URL
|
|
ipcMain.handle('get-ffmpeg-download-url', () => {
|
|
return getFFmpegDownloadUrl();
|
|
});
|
|
|
|
// Add new IPC handler for FFmpeg installation
|
|
ipcMain.handle('install-ffmpeg', async () => {
|
|
try {
|
|
await installFFmpeg();
|
|
// Check if installation was successful
|
|
const hasFFmpeg = await checkFFmpeg();
|
|
if (hasFFmpeg) {
|
|
mainWindow.webContents.send('ffmpeg-status', true);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Error in install-ffmpeg handler:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Add new IPC handler for app restart
|
|
ipcMain.handle('restart-app', () => {
|
|
app.relaunch();
|
|
app.exit(0);
|
|
});
|
|
|
|
// Add new IPC handler for getting user settings
|
|
ipcMain.handle('get-user-settings', async (event, email) => {
|
|
try {
|
|
console.log(`[Main] IPC: Handling get-user-settings for ${email}`);
|
|
const settings = store.get('settings');
|
|
// Ensure settings and userSettings exist before access
|
|
const userSpecificSettings = (settings && settings.userSettings && settings.userSettings[email])
|
|
? settings.userSettings[email]
|
|
: {};
|
|
console.log(`[Main] IPC: Returning user settings for ${email}:`, userSpecificSettings);
|
|
return userSpecificSettings;
|
|
} catch (error) {
|
|
console.error(`[Main] IPC: Error in get-user-settings for ${email}:`, error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Add new IPC handler for saving user settings
|
|
ipcMain.handle('save-user-settings', async (event, email, userSettings) => {
|
|
try {
|
|
console.log(`[Main] IPC: Handling save-user-settings for ${email}:`, userSettings);
|
|
const settings = store.get('settings') || {}; // Ensure settings is at least an empty object
|
|
|
|
// Ensure userSettings container exists
|
|
if (!settings.userSettings) {
|
|
settings.userSettings = {};
|
|
}
|
|
|
|
settings.userSettings[email] = {
|
|
...(settings.userSettings[email] || {}), // Ensure existing settings for the user are merged correctly
|
|
...userSettings
|
|
};
|
|
store.set('settings', settings);
|
|
console.log(`[Main] IPC: Saved user settings for ${email}:`, settings.userSettings[email]);
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[Main] IPC: Error saving user settings for ${email}:`, error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Handle renaming recordings
|
|
ipcMain.handle('rename-recording', async (event, oldFileName, newFileName, email) => {
|
|
try {
|
|
console.log('Renaming recording:', { oldFileName, newFileName, email });
|
|
|
|
// Get the save directory from settings
|
|
const settings = store.get('settings');
|
|
if (!settings || !settings.saveDirectory) {
|
|
throw new Error('No save directory configured');
|
|
}
|
|
|
|
// Ensure the new filename has .webm extension
|
|
if (!newFileName.endsWith('.webm')) {
|
|
newFileName = `${newFileName}.webm`;
|
|
}
|
|
|
|
// Construct full paths
|
|
const oldPath = join(settings.saveDirectory, email, oldFileName);
|
|
const newPath = join(settings.saveDirectory, email, newFileName);
|
|
|
|
// Check if old file exists
|
|
try {
|
|
await access(oldPath, constants.F_OK);
|
|
} catch {
|
|
throw new Error('Original file not found');
|
|
}
|
|
|
|
// Check if new file already exists
|
|
try {
|
|
await access(newPath, constants.F_OK);
|
|
throw new Error('A file with this name already exists');
|
|
} catch {
|
|
// File doesn't exist, which is what we want
|
|
}
|
|
|
|
// Perform the rename
|
|
await fs.promises.rename(oldPath, newPath);
|
|
console.log('File renamed successfully');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error renaming recording:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Add new IPC handlers for logging
|
|
ipcMain.handle('saveLog', (event, logEntry) => {
|
|
saveLog(logEntry);
|
|
});
|
|
|
|
ipcMain.handle('getLogs', () => {
|
|
return getLogs();
|
|
});
|
|
|
|
ipcMain.handle('clearLogs', () => {
|
|
clearLogs();
|
|
});
|
|
|
|
// Add IPC handler for app version
|
|
ipcMain.handle('get-app-version', () => {
|
|
return app.getVersion();
|
|
}); |