diff --git a/scripts/setup-dev-binaries.js b/scripts/setup-dev-binaries.js
index b648be9..4d36d52 100755
--- a/scripts/setup-dev-binaries.js
+++ b/scripts/setup-dev-binaries.js
@@ -8,7 +8,7 @@
const fs = require('node:fs')
const path = require('node:path')
const os = require('node:os')
-const { execSync } = require('node:child_process')
+const { execSync, spawnSync } = require('node:child_process')
const https = require('node:https')
const http = require('node:http')
@@ -33,7 +33,7 @@ const PLATFORM_CONFIG = {
extract: 'unzip',
release: {
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
- assetPattern: /win64.*gpl.*\.zip$/i,
+ assetPattern: /ffmpeg-master-latest-win64-gpl\.zip$/i,
binaryName: 'ffmpeg.exe'
}
}
@@ -79,7 +79,7 @@ const PLATFORM_CONFIG = {
extract: 'tar',
release: {
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
- assetPattern: /linux64.*gpl.*\.tar\.xz$/i,
+ assetPattern: /ffmpeg-master-latest-linux64-gpl\.tar\.xz$/i,
binaryName: 'ffmpeg'
}
}
@@ -125,24 +125,43 @@ function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http
const file = fs.createWriteStream(dest)
+ let downloadedBytes = 0
const request = protocol.get(url, { headers: getDownloadHeaders(url) }, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirect
file.close()
safeUnlink(dest)
- return downloadFile(response.headers.location, dest).then(resolve).catch(reject)
+ const redirectUrl = response.headers.location
+ if (!redirectUrl) {
+ return reject(new Error(`Redirect without location for ${url}`))
+ }
+ log(`Redirected to ${redirectUrl}`, 'info')
+ return downloadFile(redirectUrl, dest).then(resolve).catch(reject)
}
+ const contentLength = response.headers['content-length']
if (response.statusCode !== 200) {
file.close()
safeUnlink(dest)
- return reject(new Error(`Failed to download: ${response.statusCode}`))
+ return reject(
+ new Error(
+ `Failed to download ${url}: ${response.statusCode} (length: ${contentLength || 'unknown'})`
+ )
+ )
}
+ response.on('data', (chunk) => {
+ downloadedBytes += chunk.length
+ })
+
response.pipe(file)
file.on('finish', () => {
file.close()
+ log(
+ `Downloaded ${formatBytes(downloadedBytes)} from ${url}`,
+ downloadedBytes ? 'success' : 'warn'
+ )
resolve()
})
})
@@ -154,6 +173,7 @@ function downloadFile(url, dest) {
request.on('error', (err) => {
file.close()
safeUnlink(dest)
+ log(`Download error for ${url}: ${err.message}`, 'error')
reject(err)
})
})
@@ -163,6 +183,7 @@ async function downloadFileWithRetry(url, dest, retries = 3, delayMs = 2000) {
let lastError
for (let attempt = 1; attempt <= retries; attempt += 1) {
try {
+ log(`Downloading ${url} (attempt ${attempt}/${retries})...`, 'download')
await downloadFile(url, dest)
return
} catch (error) {
@@ -170,7 +191,7 @@ async function downloadFileWithRetry(url, dest, retries = 3, delayMs = 2000) {
safeUnlink(dest)
if (attempt < retries) {
const backoff = delayMs * attempt
- log(`Download failed (attempt ${attempt}/${retries}): ${error.message}`, 'warn')
+ log(`Download failed for ${url} (attempt ${attempt}/${retries}): ${error.message}`, 'warn')
await new Promise((resolve) => setTimeout(resolve, backoff))
}
}
@@ -298,6 +319,37 @@ function fileExists(filePath) {
return fs.existsSync(filePath)
}
+function formatBytes(bytes) {
+ if (!bytes || bytes <= 0) {
+ return 'unknown size'
+ }
+ if (bytes >= 1024 * 1024) {
+ return `${Math.round(bytes / (1024 * 1024))} MB`
+ }
+ return `${Math.round(bytes / 1024)} KB`
+}
+
+function checkBinary(filePath, args, label) {
+ const result = spawnSync(filePath, args, {
+ encoding: 'utf8',
+ timeout: 8000,
+ windowsHide: true
+ })
+
+ if (result.error) {
+ return { ok: false, message: result.error.message }
+ }
+
+ if (result.status !== 0) {
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim()
+ return { ok: false, message: output || `exit code ${result.status}` }
+ }
+
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim()
+ const firstLine = output.split(/\r?\n/).find((line) => line.trim())
+ return { ok: true, message: firstLine ? firstLine.trim() : `${label} version check ok` }
+}
+
function getDenoAssetName(platform, arch) {
if (platform === 'win32') {
if (arch === 'arm64') {
@@ -330,6 +382,10 @@ async function downloadYtDlp(config) {
const outputPath = path.join(RESOURCES_DIR, output)
if (fileExists(outputPath)) {
+ const validation = checkBinary(outputPath, ['--version'], 'yt-dlp')
+ if (!validation.ok) {
+ log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
+ }
log(`${output} already exists, skipping download`, 'info')
return
}
@@ -342,6 +398,11 @@ async function downloadYtDlp(config) {
await downloadFileWithRetry(url, tempPath)
fs.renameSync(tempPath, outputPath)
setExecutable(outputPath)
+ const validation = checkBinary(outputPath, ['--version'], 'yt-dlp')
+ if (!validation.ok) {
+ safeUnlink(outputPath)
+ throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
+ }
log(`Downloaded ${output} successfully`, 'success')
} catch (error) {
if (fs.existsSync(tempPath)) {
@@ -356,6 +417,10 @@ async function downloadFfmpegWindows(config) {
const outputPath = path.join(RESOURCES_DIR, output)
if (fileExists(outputPath)) {
+ const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
+ if (!validation.ok) {
+ log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
+ }
log(`${output} already exists, skipping download`, 'info')
return
}
@@ -392,6 +457,11 @@ async function downloadFfmpegWindows(config) {
}
fs.copyFileSync(sourcePath, outputPath)
+ const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
+ if (!validation.ok) {
+ safeUnlink(outputPath)
+ throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
+ }
log(`Downloaded ${output} successfully`, 'success')
// Cleanup
@@ -416,6 +486,10 @@ async function downloadFfmpegMac(config) {
const outputPath = path.join(RESOURCES_DIR, output)
if (fileExists(outputPath)) {
+ const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
+ if (!validation.ok) {
+ log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
+ }
log(`${output} already exists, skipping download`, 'info')
return
}
@@ -448,6 +522,11 @@ async function downloadFfmpegMac(config) {
fs.copyFileSync(sourcePath, outputPath)
setExecutable(outputPath)
+ const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
+ if (!validation.ok) {
+ safeUnlink(outputPath)
+ throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
+ }
log(`Downloaded ${output} successfully`, 'success')
// Cleanup
@@ -465,6 +544,10 @@ async function downloadFfmpegLinux(config) {
const outputPath = path.join(RESOURCES_DIR, output)
if (fileExists(outputPath)) {
+ const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
+ if (!validation.ok) {
+ log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
+ }
log(`${output} already exists, skipping download`, 'info')
return
}
@@ -502,6 +585,11 @@ async function downloadFfmpegLinux(config) {
fs.copyFileSync(sourcePath, outputPath)
setExecutable(outputPath)
+ const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
+ if (!validation.ok) {
+ safeUnlink(outputPath)
+ throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
+ }
log(`Downloaded ${output} successfully`, 'success')
// Cleanup
@@ -528,6 +616,10 @@ async function downloadDenoRuntime() {
const outputPath = path.join(RESOURCES_DIR, outputName)
if (fileExists(outputPath)) {
+ const validation = checkBinary(outputPath, ['--version'], 'deno')
+ if (!validation.ok) {
+ log(`Existing ${outputName} failed version check: ${validation.message}`, 'warn')
+ }
log(`${outputName} already exists, skipping download`, 'info')
return
}
@@ -549,6 +641,11 @@ async function downloadDenoRuntime() {
fs.copyFileSync(sourcePath, outputPath)
setExecutable(outputPath)
+ const validation = checkBinary(outputPath, ['--version'], 'deno')
+ if (!validation.ok) {
+ safeUnlink(outputPath)
+ throw new Error(`Downloaded ${outputName} failed version check: ${validation.message}`)
+ }
log(`Downloaded ${outputName} successfully`, 'success')
fs.unlinkSync(tempZip)
diff --git a/src/main/download-engine/args-builder.ts b/src/main/download-engine/args-builder.ts
index 4cfc0cb..355ff0c 100644
--- a/src/main/download-engine/args-builder.ts
+++ b/src/main/download-engine/args-builder.ts
@@ -68,7 +68,7 @@ export const buildDownloadArgs = (
settings: AppSettings,
jsRuntimeArgs: string[] = []
): string[] => {
- const args: string[] = ['--no-playlist', '--embed-chapters', '--no-mtime']
+ const args: string[] = ['--no-playlist', '--no-mtime']
// Add encoding support for proper handling of non-ASCII characters
args.push('--encoding', 'utf-8')
@@ -95,11 +95,26 @@ export const buildDownloadArgs = (
args.push('--download-sections', `*${start}-${end || ''}`)
}
+ const embedSubs = settings.embedSubs
+ const embedMetadata = settings.embedMetadata
+ const embedChapters = settings.embedChapters
+
// Subtitles
- if (options.downloadSubs) {
- args.push('--write-subs', '--sub-langs', 'all')
+ if (options.downloadSubs || embedSubs) {
+ args.push('--sub-langs', 'all')
}
+ if (options.downloadSubs) {
+ args.push('--write-subs')
+ }
+
+ args.push(embedSubs ? '--embed-subs' : '--no-embed-subs')
+ if (process.platform !== 'darwin') {
+ args.push(settings.embedThumbnail ? '--embed-thumbnail' : '--no-embed-thumbnail')
+ }
+ args.push(embedMetadata ? '--embed-metadata' : '--no-embed-metadata')
+ args.push(embedChapters ? '--embed-chapters' : '--no-embed-chapters')
+
// Output path with proper encoding handling
const baseDownloadPath = options.customDownloadPath?.trim() || downloadPath
const filenameTemplate = sanitizeFilenameTemplate(
diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json
index 5a8e2bb..5b407a2 100644
--- a/src/renderer/src/locales/en.json
+++ b/src/renderer/src/locales/en.json
@@ -89,6 +89,7 @@
"currentLocation": "Current download location - ",
"downloadLocation": "Download location",
"downloadSubs": "Download subtitles if available",
+ "downloadSubsHint": "Save subtitles as separate files when available",
"end": "End",
"endHint": "If kept empty, it will be downloaded to the end",
"endPlaceholder": "10:00",
@@ -420,6 +421,14 @@
"launchAtLoginUnsupported": "Auto launch is only available on macOS and Windows.",
"enableAnalytics": "Help improve VidBee",
"enableAnalyticsDescription": "Share anonymous usage data to help us understand how the app is used and prioritize improvements.",
+ "embedChapters": "Embed chapters",
+ "embedChaptersDescription": "Add chapter markers to the file when available",
+ "embedMetadata": "Embed metadata",
+ "embedMetadataDescription": "Write title, artist, and other metadata when available",
+ "embedSubs": "Embed subtitles",
+ "embedSubsDescription": "Embed subtitles into the video file (mp4, webm, mkv)",
+ "embedThumbnail": "Embed thumbnail",
+ "embedThumbnailDescription": "Add the thumbnail as cover art",
"maxConcurrentDownloads": "Maximum number of active downloads",
"maxConcurrentDownloadsDescription": "Maximum number of simultaneous downloads",
"none": "None",
diff --git a/src/renderer/src/pages/Settings.tsx b/src/renderer/src/pages/Settings.tsx
index 845dc87..80cd06f 100644
--- a/src/renderer/src/pages/Settings.tsx
+++ b/src/renderer/src/pages/Settings.tsx
@@ -519,6 +519,94 @@ export function Settings() {
+
+ -
+
+ {t('settings.embedSubs')}
+ {t('settings.embedSubsDescription')}
+
+
+ {
+ try {
+ handleSettingChange('embedSubs', value)
+ } catch (error) {
+ logger.error('[Settings] Error toggling embedSubs:', error)
+ }
+ }}
+ />
+
+
+
+
+
+ {platform !== 'darwin' && (
+ <>
+ -
+
+ {t('settings.embedThumbnail')}
+ {t('settings.embedThumbnailDescription')}
+
+
+ {
+ try {
+ handleSettingChange('embedThumbnail', value)
+ } catch (error) {
+ logger.error('[Settings] Error toggling embedThumbnail:', error)
+ }
+ }}
+ />
+
+
+
+
+ >
+ )}
+
+ -
+
+ {t('settings.embedMetadata')}
+ {t('settings.embedMetadataDescription')}
+
+
+ {
+ try {
+ handleSettingChange('embedMetadata', value)
+ } catch (error) {
+ logger.error('[Settings] Error toggling embedMetadata:', error)
+ }
+ }}
+ />
+
+
+
+
+
+ -
+
+ {t('settings.embedChapters')}
+ {t('settings.embedChaptersDescription')}
+
+
+ {
+ try {
+ handleSettingChange('embedChapters', value)
+ } catch (error) {
+ logger.error('[Settings] Error toggling embedChapters:', error)
+ }
+ }}
+ />
+
+
+
+
-
diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts
index 76185a5..d943718 100644
--- a/src/shared/types/index.ts
+++ b/src/shared/types/index.ts
@@ -271,6 +271,10 @@ export interface AppSettings {
autoUpdate: boolean
subscriptionOnlyLatestDefault: boolean
enableAnalytics: boolean
+ embedSubs: boolean
+ embedThumbnail: boolean
+ embedMetadata: boolean
+ embedChapters: boolean
}
export const DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE = '%(uploader)s/%(title)s.%(ext)s'
@@ -294,5 +298,9 @@ export const defaultSettings: AppSettings = {
launchAtLogin: false,
autoUpdate: true,
subscriptionOnlyLatestDefault: true,
- enableAnalytics: true
+ enableAnalytics: true,
+ embedSubs: true,
+ embedThumbnail: true,
+ embedMetadata: true,
+ embedChapters: true
}