diff --git a/README.md b/README.md index 0cc7664..bf1b82c 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@

VidBee

- - - - - + + + + + + Ask DeepWiki

VidBee Desktop diff --git a/package.json b/package.json index 6c856ae..1267114 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "build": "electron-vite build", "setup": "node scripts/setup-dev-binaries.js", "postinstall": "node scripts/setup-dev-binaries.js && electron-builder install-app-deps", - "build:unpack": "pnpm run build && electron-builder --dir", - "build:win": "node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win", - "build:mac": "node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64", - "build:linux": "node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux", + "build:unpack": "pnpm run setup && pnpm run build && electron-builder --dir", + "build:win": "pnpm run setup && node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win", + "build:mac": "pnpm run setup && node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64", + "build:linux": "pnpm run setup && node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux", "release": "git checkout main && git pull && pnpm run check && bumpp", "db:generate": "drizzle-kit generate" }, diff --git a/resources/.gitignore b/resources/.gitignore index 6d02c3a..6360655 100644 --- a/resources/.gitignore +++ b/resources/.gitignore @@ -6,6 +6,8 @@ ffmpeg.exe ffmpeg_macos ffmpeg_linux ffmpeg +deno.exe +deno # But keep the README !README.md diff --git a/resources/README.md b/resources/README.md index 74d2506..3d8c736 100644 --- a/resources/README.md +++ b/resources/README.md @@ -69,3 +69,23 @@ ffmpeg is required for merging audio/video streams and audio extraction. Bundle - Bundled binaries are required for Windows builds. On macOS/Linux the app can also use ffmpeg/yt-dlp from the system PATH. - You can override the lookup paths via the `YTDLP_PATH` or `FFMPEG_PATH` environment variables if you prefer custom locations. - File sizes: ~10-15 MB per yt-dlp binary, ~40-80 MB per ffmpeg binary + +## JS Runtime (Deno) + +yt-dlp uses an external JS runtime (Deno by default) for some extractors. Bundle a Deno binary so the app can run without system dependencies. + +### Required Files + +1. **Windows**: `deno.exe` +2. **macOS**: `deno` +3. **Linux**: `deno` + +### How to Download + +- Visit: +- Download the matching platform archive and extract the `deno` (or `deno.exe`) binary into `resources/`. +- On macOS/Linux ensure the file is executable: `chmod +x resources/deno` + +### Note + +- You can override the runtime path via `YTDLP_JS_RUNTIME_PATH` if needed. diff --git a/scripts/check-ytdlp.js b/scripts/check-ytdlp.js index ee9882f..4c966df 100644 --- a/scripts/check-ytdlp.js +++ b/scripts/check-ytdlp.js @@ -44,6 +44,17 @@ const binaries = [ linux: 'https://ffmpeg.org/download.html', mac: 'https://github.com/eko5624/mpv-mac/releases/latest' } + }, + { + label: 'deno', + filenameMap: { + win: 'deno.exe', + mac: 'deno', + linux: 'deno' + }, + help: { + default: 'https://github.com/denoland/deno/releases/latest' + } } ] diff --git a/scripts/setup-dev-binaries.js b/scripts/setup-dev-binaries.js index 718f870..776a6f8 100755 --- a/scripts/setup-dev-binaries.js +++ b/scripts/setup-dev-binaries.js @@ -15,6 +15,7 @@ const http = require('node:http') // Configuration const RESOURCES_DIR = path.join(__dirname, '..', 'resources') const YTDLP_BASE_URL = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download' +const DENO_BASE_URL = 'https://github.com/denoland/deno/releases/latest/download' const GITHUB_TOKEN = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_API_TOKEN @@ -257,6 +258,32 @@ function fileExists(filePath) { return fs.existsSync(filePath) } +function getDenoAssetName(platform, arch) { + if (platform === 'win32') { + if (arch === 'arm64') { + return 'deno-aarch64-pc-windows-msvc.zip' + } + return 'deno-x86_64-pc-windows-msvc.zip' + } + if (platform === 'darwin') { + if (arch === 'arm64') { + return 'deno-aarch64-apple-darwin.zip' + } + return 'deno-x86_64-apple-darwin.zip' + } + if (platform === 'linux') { + if (arch === 'arm64') { + return 'deno-aarch64-unknown-linux-gnu.zip' + } + return 'deno-x86_64-unknown-linux-gnu.zip' + } + return null +} + +function getDenoOutputName(platform) { + return platform === 'win32' ? 'deno.exe' : 'deno' +} + // Main download functions async function downloadYtDlp(config) { const { asset, output } = config.ytdlp @@ -447,6 +474,52 @@ async function downloadFfmpegLinux(config) { } } +async function downloadDenoRuntime() { + const platform = os.platform() + const arch = os.arch() + const assetName = getDenoAssetName(platform, arch) + + if (!assetName) { + log(`Skipping Deno runtime: unsupported platform/arch ${platform}/${arch}`, 'warn') + return + } + + const outputName = getDenoOutputName(platform) + const outputPath = path.join(RESOURCES_DIR, outputName) + + if (fileExists(outputPath)) { + log(`${outputName} already exists, skipping download`, 'info') + return + } + + log(`Downloading Deno runtime (${platform}/${arch})...`, 'download') + const tempZip = path.join(RESOURCES_DIR, 'deno-temp.zip') + const extractDir = path.join(RESOURCES_DIR, 'deno-temp') + const downloadUrl = `${DENO_BASE_URL}/${assetName}` + + try { + await downloadFile(downloadUrl, tempZip) + log('Extracting Deno runtime...', 'info') + extractZip(tempZip, extractDir) + + const sourcePath = path.join(extractDir, outputName) + if (!fileExists(sourcePath)) { + throw new Error(`Deno binary not found at ${sourcePath}`) + } + + fs.copyFileSync(sourcePath, outputPath) + setExecutable(outputPath) + log(`Downloaded ${outputName} successfully`, 'success') + + fs.unlinkSync(tempZip) + fs.rmSync(extractDir, { recursive: true, force: true }) + } catch (error) { + if (fs.existsSync(tempZip)) fs.unlinkSync(tempZip) + if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true }) + throw error + } +} + // Main setup function async function setup() { const platform = os.platform() @@ -464,6 +537,9 @@ async function setup() { // Download yt-dlp await downloadYtDlp(config) + // Download JS runtime (Deno) + await downloadDenoRuntime() + // Download ffmpeg if (platform === 'win32') { await downloadFfmpegWindows(config) diff --git a/src/main/config/logger-config.ts b/src/main/config/logger-config.ts index 2ccd69d..341b2e4 100644 --- a/src/main/config/logger-config.ts +++ b/src/main/config/logger-config.ts @@ -5,15 +5,6 @@ import log from 'electron-log/main' * Set log format, file path, transport methods, etc. */ export function configureLogger() { - // Configure console output format - support colors and scope, time in gray - log.transports.console.format = '%c{h}:{i}:{s}%c [{level}]{scope} {text}' - - // Enable console colors - log.transports.console.useStyles = true - - // Configure file output format - include scope information - log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {scope} {text}' - // Set log levels // Development: show all logs // Production: show info level and above only diff --git a/src/main/download-engine/args-builder.ts b/src/main/download-engine/args-builder.ts index e9a33b5..6dd033d 100644 --- a/src/main/download-engine/args-builder.ts +++ b/src/main/download-engine/args-builder.ts @@ -61,7 +61,8 @@ export const resolveAudioFormatSelector = (options: DownloadOptions): string => export const buildDownloadArgs = ( options: DownloadOptions, downloadPath: string, - settings: AppSettings + settings: AppSettings, + jsRuntimeArgs: string[] = [] ): string[] => { const args: string[] = ['--no-playlist', '--embed-chapters', '--no-mtime'] @@ -130,6 +131,10 @@ export const buildDownloadArgs = ( args.push('--config-location', configPath) } + if (jsRuntimeArgs.length > 0) { + args.push(...jsRuntimeArgs) + } + args.push(options.url) return args diff --git a/src/main/ipc/services/app-service.ts b/src/main/ipc/services/app-service.ts index 5325737..6b418ac 100644 --- a/src/main/ipc/services/app-service.ts +++ b/src/main/ipc/services/app-service.ts @@ -1,6 +1,7 @@ import os from 'node:os' import { app, BrowserWindow, dialog } from 'electron' import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator' +import { scopedLoggers } from '../../utils/logger' class AppService extends IpcService { static readonly groupName = 'app' @@ -48,7 +49,7 @@ class AppService extends IpcService { const base64 = buffer.toString('base64') return `data:${contentType};base64,${base64}` } catch (error) { - console.error('Failed to fetch site icon:', error) + scopedLoggers.system.error('Failed to fetch site icon:', error) return null } } diff --git a/src/main/ipc/services/file-system-service.ts b/src/main/ipc/services/file-system-service.ts index 2f47464..b345f76 100644 --- a/src/main/ipc/services/file-system-service.ts +++ b/src/main/ipc/services/file-system-service.ts @@ -6,6 +6,7 @@ import { pathToFileURL } from 'node:url' import { promisify } from 'node:util' import { clipboard, dialog, shell } from 'electron' import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator' +import { scopedLoggers } from '../../utils/logger' const execFileAsync = promisify(execFile) @@ -49,7 +50,10 @@ class FileSystemService extends IpcService { return xdgPath } } catch (error) { - console.warn('Unable to resolve XDG download directory, falling back to default:', error) + scopedLoggers.system.warn( + 'Unable to resolve XDG download directory, falling back to default:', + error + ) } } @@ -75,7 +79,7 @@ class FileSystemService extends IpcService { if (stats?.isDirectory()) { const result = await shell.openPath(normalizedPath) if (result) { - console.error('Failed to open directory:', result) + scopedLoggers.system.error('Failed to open directory:', result) return false } return true @@ -88,16 +92,16 @@ class FileSystemService extends IpcService { if (parentStats?.isDirectory()) { const result = await shell.openPath(parentDirectory) if (result) { - console.error('Failed to open parent directory:', result) + scopedLoggers.system.error('Failed to open parent directory:', result) return false } return true } - console.error('File or directory does not exist:', normalizedPath) + scopedLoggers.system.error('File or directory does not exist:', normalizedPath) return false } catch (error) { - console.error('Failed to open file location:', error) + scopedLoggers.system.error('Failed to open file location:', error) return false } } @@ -122,7 +126,7 @@ class FileSystemService extends IpcService { return true } catch (error) { - console.error('Failed to copy file to clipboard:', error) + scopedLoggers.system.error('Failed to copy file to clipboard:', error) return false } } @@ -150,7 +154,7 @@ class FileSystemService extends IpcService { await shell.openExternal(url) return true } catch (error) { - console.error('Failed to open external URL:', error) + scopedLoggers.system.error('Failed to open external URL:', error) return false } } @@ -166,7 +170,10 @@ class FileSystemService extends IpcService { ]) return } catch (error) { - console.error('PowerShell clipboard copy failed, falling back to manual buffer:', error) + scopedLoggers.system.error( + 'PowerShell clipboard copy failed, falling back to manual buffer:', + error + ) } const winPath = resolvedPath.replace(/\//g, '\\') @@ -194,7 +201,10 @@ class FileSystemService extends IpcService { await execFileAsync('osascript', ['-e', `set the clipboard to (POSIX file "${escaped}")`]) return } catch (error) { - console.error('osascript clipboard copy failed, falling back to manual buffer:', error) + scopedLoggers.system.error( + 'osascript clipboard copy failed, falling back to manual buffer:', + error + ) } const entries = [ @@ -243,7 +253,7 @@ class FileSystemService extends IpcService { return stats?.isFile() ?? false } catch (error) { - console.error('Failed to check file existence:', error) + scopedLoggers.system.error('Failed to check file existence:', error) return false } } @@ -295,7 +305,7 @@ class FileSystemService extends IpcService { return false } catch (error) { - console.error('Failed to delete file:', error) + scopedLoggers.system.error('Failed to delete file:', error) return false } } diff --git a/src/main/lib/download-engine.ts b/src/main/lib/download-engine.ts index 0b54e33..7e57370 100644 --- a/src/main/lib/download-engine.ts +++ b/src/main/lib/download-engine.ts @@ -145,6 +145,13 @@ const resolveHistoryDownloadPath = ( return path.join(basePath, templateDir) } +const appendJsRuntimeArgs = (args: string[]): void => { + const runtimeArgs = ytdlpManager.getJsRuntimeArgs() + if (runtimeArgs.length > 0) { + args.push(...runtimeArgs) + } +} + class DownloadEngine extends EventEmitter { private activeDownloads: Map = new Map() private queue: DownloadQueue @@ -194,6 +201,7 @@ class DownloadEngine extends EventEmitter { args.push('--config-location', configPath) } + appendJsRuntimeArgs(args) args.push(url) return new Promise((resolve, reject) => { @@ -290,6 +298,7 @@ class DownloadEngine extends EventEmitter { args.push('--config-location', configPath) } + appendJsRuntimeArgs(args) args.push(url) type RawPlaylistEntry = { @@ -511,7 +520,7 @@ class DownloadEngine extends EventEmitter { startDownload(id: string, options: DownloadOptions): void { if (this.activeDownloads.has(id)) { - console.warn(`Download ${id} is already active`) + scopedLoggers.engine.warn(`Download ${id} is already active`) return } @@ -644,7 +653,12 @@ class DownloadEngine extends EventEmitter { return true } - const args = buildDownloadArgs(options, resolvedDownloadPath, settings) + const args = buildDownloadArgs( + options, + resolvedDownloadPath, + settings, + ytdlpManager.getJsRuntimeArgs() + ) const captureOutputPath = (rawPath: string | undefined): void => { if (!rawPath) { diff --git a/src/main/lib/ffmpeg-manager.ts b/src/main/lib/ffmpeg-manager.ts index 08396c6..4fd181f 100644 --- a/src/main/lib/ffmpeg-manager.ts +++ b/src/main/lib/ffmpeg-manager.ts @@ -2,13 +2,14 @@ import { execSync } from 'node:child_process' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' +import { scopedLoggers } from '../utils/logger' class FfmpegManager { private ffmpegPath: string | null = null async initialize(): Promise { this.ffmpegPath = await this.findFfmpegBinary() - console.log('ffmpeg initialized at:', this.ffmpegPath) + scopedLoggers.engine.info('ffmpeg initialized at:', this.ffmpegPath) } getPath(): string { @@ -30,7 +31,7 @@ class FfmpegManager { const resourceCandidates: string[] = [] if (process.env.FFMPEG_PATH && fs.existsSync(process.env.FFMPEG_PATH)) { - console.log('Using ffmpeg from FFMPEG_PATH:', process.env.FFMPEG_PATH) + scopedLoggers.engine.info('Using ffmpeg from FFMPEG_PATH:', process.env.FFMPEG_PATH) return process.env.FFMPEG_PATH } @@ -50,10 +51,13 @@ class FfmpegManager { try { fs.chmodSync(fullPath, 0o755) } catch (error) { - console.warn('Failed to set executable permission on ffmpeg binary:', error) + scopedLoggers.engine.warn( + 'Failed to set executable permission on ffmpeg binary:', + error + ) } } - console.log('Using bundled ffmpeg:', fullPath) + scopedLoggers.engine.info('Using bundled ffmpeg:', fullPath) return fullPath } } @@ -62,7 +66,7 @@ class FfmpegManager { const commonPaths = ['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg'] for (const candidate of commonPaths) { if (fs.existsSync(candidate)) { - console.log('Using system ffmpeg:', candidate) + scopedLoggers.engine.info('Using system ffmpeg:', candidate) return candidate } } @@ -72,7 +76,7 @@ class FfmpegManager { try { const systemPath = execSync('which ffmpeg').toString().trim() if (systemPath && fs.existsSync(systemPath)) { - console.log('Using system ffmpeg:', systemPath) + scopedLoggers.engine.info('Using system ffmpeg:', systemPath) return systemPath } } catch (_error) { @@ -84,7 +88,7 @@ class FfmpegManager { try { const output = execSync('where ffmpeg').toString().split(/\r?\n/)[0] if (output && fs.existsSync(output)) { - console.log('Using system ffmpeg:', output) + scopedLoggers.engine.info('Using system ffmpeg:', output) return output } } catch (_error) { diff --git a/src/main/lib/thumbnail-cache.ts b/src/main/lib/thumbnail-cache.ts index 0a646b7..93e0e60 100644 --- a/src/main/lib/thumbnail-cache.ts +++ b/src/main/lib/thumbnail-cache.ts @@ -4,6 +4,7 @@ import fsPromises from 'node:fs/promises' import path from 'node:path' import { APP_PROTOCOL_SCHEME } from '@shared/constants' import { app } from 'electron' +import { scopedLoggers } from '../utils/logger' const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']) @@ -81,7 +82,7 @@ export class ThumbnailCache { await fsPromises.writeFile(finalPath, buffer) return this.toAppProtocolUrl(finalPath) } catch (error) { - console.error('Failed to cache thumbnail:', error) + scopedLoggers.thumbnail.error('Failed to cache thumbnail:', error) return null } } diff --git a/src/main/lib/ytdlp-manager.ts b/src/main/lib/ytdlp-manager.ts index 62ed336..3e04b14 100644 --- a/src/main/lib/ytdlp-manager.ts +++ b/src/main/lib/ytdlp-manager.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import type YTDlpWrap from 'yt-dlp-wrap-plus' +import { scopedLoggers } from '../utils/logger' // Use require for yt-dlp-wrap-plus to handle CommonJS/ESM compatibility const YTDlpWrapModule = require('yt-dlp-wrap-plus') @@ -12,11 +13,13 @@ type YTDlpWrapInstance = InstanceType class YtDlpManager { private ytdlpPath: string | null = null private ytdlpInstance: YTDlpWrapInstance | null = null + private jsRuntimeArgs: string[] = [] async initialize(): Promise { this.ytdlpPath = await this.findOrDownloadYtDlp() this.ytdlpInstance = new YTDlpWrapCtor(this.ytdlpPath) - console.log('yt-dlp initialized at:', this.ytdlpPath) + this.jsRuntimeArgs = this.resolveJsRuntimeArgs() + scopedLoggers.engine.info('yt-dlp initialized at:', this.ytdlpPath) } getInstance(): YTDlpWrapInstance { @@ -33,6 +36,10 @@ class YtDlpManager { return this.ytdlpPath } + getJsRuntimeArgs(): string[] { + return [...this.jsRuntimeArgs] + } + private getResourcesPath(): string { // In development, read from project root's resources if (process.env.NODE_ENV === 'development') { @@ -57,7 +64,7 @@ class YtDlpManager { // Check environment variable first if (process.env.YTDLP_PATH && fs.existsSync(process.env.YTDLP_PATH)) { - console.log('Using yt-dlp from YTDLP_PATH:', process.env.YTDLP_PATH) + scopedLoggers.engine.info('Using yt-dlp from YTDLP_PATH:', process.env.YTDLP_PATH) return process.env.YTDLP_PATH } @@ -65,13 +72,13 @@ class YtDlpManager { const resourcesPath = this.getResourcesPath() const bundledPath = path.join(resourcesPath, bundledName) if (fs.existsSync(bundledPath)) { - console.log('Using bundled yt-dlp:', bundledPath) + scopedLoggers.engine.info('Using bundled yt-dlp:', bundledPath) // Make executable on Unix-like systems if needed if (platform !== 'win32') { try { fs.chmodSync(bundledPath, 0o755) } catch (error) { - console.warn('Failed to set executable permission:', error) + scopedLoggers.engine.warn('Failed to set executable permission:', error) } } return bundledPath @@ -82,7 +89,7 @@ class YtDlpManager { const possiblePaths = ['/opt/homebrew/bin/yt-dlp', '/usr/local/bin/yt-dlp'] for (const p of possiblePaths) { if (fs.existsSync(p)) { - console.log('Using system yt-dlp:', p) + scopedLoggers.engine.info('Using system yt-dlp:', p) return p } } @@ -93,7 +100,7 @@ class YtDlpManager { try { const systemPath = execSync('which yt-dlp').toString().trim() if (systemPath && fs.existsSync(systemPath)) { - console.log('Using system yt-dlp:', systemPath) + scopedLoggers.engine.info('Using system yt-dlp:', systemPath) return systemPath } } catch (_error) { @@ -115,6 +122,92 @@ class YtDlpManager { ) } + private resolveJsRuntimeArgs(): string[] { + const runtime = (process.env.YTDLP_JS_RUNTIME || 'deno').trim() + if (!runtime || runtime === 'none') { + return [] + } + + const runtimePath = this.resolveJsRuntimePath(runtime) + if (runtimePath) { + return ['--js-runtimes', `${runtime}:${runtimePath}`] + } + + if (process.env.YTDLP_JS_RUNTIME) { + scopedLoggers.engine.warn( + `Requested JS runtime "${runtime}" was not found. Falling back to yt-dlp default detection.` + ) + } else { + scopedLoggers.engine.warn( + 'JS runtime not found. YouTube support may be limited without an external JS runtime.' + ) + } + + return process.env.YTDLP_JS_RUNTIME ? ['--js-runtimes', runtime] : [] + } + + private resolveJsRuntimePath(runtime: string): string | null { + const envPath = process.env.YTDLP_JS_RUNTIME_PATH?.trim() + if (envPath && fs.existsSync(envPath)) { + scopedLoggers.engine.info('Using JS runtime from YTDLP_JS_RUNTIME_PATH:', envPath) + return envPath + } + + const platform = os.platform() + const resourcesPath = this.getResourcesPath() + const resourceCandidates: string[] = [] + + if (runtime === 'deno') { + resourceCandidates.push(platform === 'win32' ? 'deno.exe' : 'deno') + } else if (runtime === 'node') { + resourceCandidates.push(platform === 'win32' ? 'node.exe' : 'node') + } else if (runtime === 'bun') { + resourceCandidates.push(platform === 'win32' ? 'bun.exe' : 'bun') + } else if (runtime === 'quickjs') { + resourceCandidates.push(platform === 'win32' ? 'qjs.exe' : 'qjs') + } else { + resourceCandidates.push(runtime) + if (platform === 'win32' && !runtime.endsWith('.exe')) { + resourceCandidates.push(`${runtime}.exe`) + } + } + + for (const candidate of resourceCandidates) { + const fullPath = path.join(resourcesPath, candidate) + if (fs.existsSync(fullPath)) { + if (platform !== 'win32') { + try { + fs.chmodSync(fullPath, 0o755) + } catch (error) { + scopedLoggers.engine.warn('Failed to set executable permission on JS runtime:', error) + } + } + scopedLoggers.engine.info('Using bundled JS runtime:', fullPath) + return fullPath + } + } + + try { + if (platform === 'win32') { + const output = execSync(`where ${runtime}`).toString().split(/\r?\n/)[0] + if (output && fs.existsSync(output)) { + scopedLoggers.engine.info('Using system JS runtime:', output) + return output + } + } else { + const systemPath = execSync(`which ${runtime}`).toString().trim() + if (systemPath && fs.existsSync(systemPath)) { + scopedLoggers.engine.info('Using system JS runtime:', systemPath) + return systemPath + } + } + } catch (_error) { + // Runtime not found in PATH + } + + return null + } + // Removed runtime download/update to avoid network dependency in production builds } diff --git a/src/main/settings.ts b/src/main/settings.ts index ebb5ee6..cebf689 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -3,6 +3,7 @@ import os from 'node:os' import path from 'node:path' import type { AppSettings } from '../shared/types' import { defaultSettings } from '../shared/types' +import { scopedLoggers } from './utils/logger' // Use require for electron-store to avoid CommonJS/ESM issues const ElectronStore = require('electron-store') @@ -14,7 +15,7 @@ const ensureDirectoryExists = (dir: string) => { try { fs.mkdirSync(dir, { recursive: true }) } catch (error) { - console.error('Failed to ensure download directory:', error) + scopedLoggers.system.error('Failed to ensure download directory:', error) } } @@ -85,7 +86,7 @@ class SettingsManager { } ensureDirectoryExists(normalizedDownloadPath) } catch (error) { - console.error('Failed to verify download directory:', error) + scopedLoggers.system.error('Failed to verify download directory:', error) } } }