* Add yt-dlp JS runtime support * Update build scripts and logging * fix: update README to include DeepWiki badge and correct contributor badge logo
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, DownloadProcess> = 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) {
|
||||
|
||||
@@ -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<void> {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof YTDlpWrapCtor>
|
||||
class YtDlpManager {
|
||||
private ytdlpPath: string | null = null
|
||||
private ytdlpInstance: YTDlpWrapInstance | null = null
|
||||
private jsRuntimeArgs: string[] = []
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user