Compare commits

..

4 Commits

Author SHA1 Message Date
Nexmoe
7074f698e5 chore: release v1.1.7 2026-01-11 15:46:37 +08:00
Nexmoe
b254bb6dba chore(build): reduce packaged artifacts (#79) 2026-01-11 15:44:20 +08:00
Nexmoe
1caca0ea9a feat(settings): add embed features (#76)
* feat(settings): add embed options

* feat(settings): default embed on

* refactor(setup-dev-binaries): enhance download error handling and version checks for binaries
2026-01-11 15:24:33 +08:00
Nexmoe
12ec142a77 ci(release): notify Cloudflare Pages (#78) 2026-01-11 15:21:40 +08:00
9 changed files with 242 additions and 11 deletions

View File

@@ -68,6 +68,8 @@ jobs:
$source = Join-Path 'ffmpeg' '${{ matrix.ffmpeg_inner_path }}'
$destination = Join-Path 'resources' '${{ matrix.ffmpeg_output }}'
Copy-Item -Path $source -Destination $destination -Force
Remove-Item ffmpeg.zip -Force
Remove-Item ffmpeg -Recurse -Force
- name: Download ffmpeg binary (macOS)
if: matrix.platform == 'macos'
@@ -112,6 +114,7 @@ jobs:
tar -xf ffmpeg.tar.xz -C ffmpeg
cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/${{ matrix.ffmpeg_output }}"
chmod +x "resources/${{ matrix.ffmpeg_output }}"
rm -rf ffmpeg.tar.xz ffmpeg
- name: Download yt-dlp binary
shell: bash

View File

@@ -42,3 +42,9 @@ jobs:
dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- name: Notify Cloudflare Pages
env:
CLOUDFLARE_WEBHOOK_URL: ${{ secrets.CLOUDFLARE_WEBHOOK_URL }}
run: |
curl -X POST "$CLOUDFLARE_WEBHOOK_URL"

View File

@@ -8,11 +8,16 @@ protocols:
- vidbee
files:
- '!**/.vscode/*'
- '!**/.context/**'
- '!**/.github/**'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintcache,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- '!extension/**'
- '!monkey/**'
- '!screenshots/**'
asarUnpack:
- resources/**
win:

View File

@@ -1,6 +1,6 @@
{
"name": "vidbee",
"version": "1.1.6",
"version": "1.1.7",
"description": "A modern Electron application for downloading videos and audios",
"main": "./out/main/index.js",
"author": "VidBee",

View File

@@ -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)

View File

@@ -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(

View File

@@ -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",

View File

@@ -519,6 +519,94 @@ export function Settings() {
</Item>
</ItemGroup>
<ItemGroup>
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.embedSubs')}</ItemTitle>
<ItemDescription>{t('settings.embedSubsDescription')}</ItemDescription>
</ItemContent>
<ItemActions>
<Switch
checked={settings.embedSubs ?? false}
onCheckedChange={(value) => {
try {
handleSettingChange('embedSubs', value)
} catch (error) {
logger.error('[Settings] Error toggling embedSubs:', error)
}
}}
/>
</ItemActions>
</Item>
<ItemSeparator />
{platform !== 'darwin' && (
<>
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.embedThumbnail')}</ItemTitle>
<ItemDescription>{t('settings.embedThumbnailDescription')}</ItemDescription>
</ItemContent>
<ItemActions>
<Switch
checked={settings.embedThumbnail ?? false}
onCheckedChange={(value) => {
try {
handleSettingChange('embedThumbnail', value)
} catch (error) {
logger.error('[Settings] Error toggling embedThumbnail:', error)
}
}}
/>
</ItemActions>
</Item>
<ItemSeparator />
</>
)}
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.embedMetadata')}</ItemTitle>
<ItemDescription>{t('settings.embedMetadataDescription')}</ItemDescription>
</ItemContent>
<ItemActions>
<Switch
checked={settings.embedMetadata ?? false}
onCheckedChange={(value) => {
try {
handleSettingChange('embedMetadata', value)
} catch (error) {
logger.error('[Settings] Error toggling embedMetadata:', error)
}
}}
/>
</ItemActions>
</Item>
<ItemSeparator />
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.embedChapters')}</ItemTitle>
<ItemDescription>{t('settings.embedChaptersDescription')}</ItemDescription>
</ItemContent>
<ItemActions>
<Switch
checked={settings.embedChapters ?? true}
onCheckedChange={(value) => {
try {
handleSettingChange('embedChapters', value)
} catch (error) {
logger.error('[Settings] Error toggling embedChapters:', error)
}
}}
/>
</ItemActions>
</Item>
</ItemGroup>
<ItemGroup>
<Item variant="muted">
<ItemContent>

View File

@@ -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
}