Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bcddddc37 | ||
|
|
99163865e2 | ||
|
|
40113a2b53 | ||
|
|
75c8b19f31 | ||
|
|
2a28b0db85 | ||
|
|
ca4b851ba4 |
133
.github/workflows/build.yml
vendored
Normal file
133
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
upload_artifacts:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
description: 'Whether to upload build artifacts'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: windows
|
||||
os: windows-latest
|
||||
build_script: pnpm run build:win
|
||||
ytdlp_asset: yt-dlp.exe
|
||||
ytdlp_output: yt-dlp.exe
|
||||
ffmpeg_url: https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-win64-gpl.zip
|
||||
ffmpeg_inner_path: ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe
|
||||
ffmpeg_output: ffmpeg.exe
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
build_script: pnpm run build:mac
|
||||
ytdlp_asset: yt-dlp_macos
|
||||
ytdlp_output: yt-dlp_macos
|
||||
ffmpeg_arm_url: https://github.com/eko5624/mpv-mac/releases/download/2025-10-25/ffmpeg-arm64-defd5f3f64.zip
|
||||
ffmpeg_x86_url: https://github.com/eko5624/mpv-mac/releases/download/2025-10-25/ffmpeg-x86_64-defd5f3f64.zip
|
||||
ffmpeg_inner_path: ffmpeg/ffmpeg
|
||||
ffmpeg_output: ffmpeg_macos
|
||||
- platform: linux
|
||||
os: ubuntu-latest
|
||||
build_script: pnpm run build:linux
|
||||
ytdlp_asset: yt-dlp
|
||||
ytdlp_output: yt-dlp_linux
|
||||
ffmpeg_url: https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-linux64-gpl.tar.xz
|
||||
ffmpeg_inner_path: ffmpeg-master-latest-linux64-gpl/bin/ffmpeg
|
||||
ffmpeg_output: ffmpeg_linux
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download ffmpeg binary (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ffmpegUrl = '${{ matrix.ffmpeg_url }}'
|
||||
Invoke-WebRequest -Uri $ffmpegUrl -OutFile ffmpeg.zip
|
||||
Expand-Archive ffmpeg.zip -DestinationPath ffmpeg -Force
|
||||
$source = Join-Path 'ffmpeg' '${{ matrix.ffmpeg_inner_path }}'
|
||||
$destination = Join-Path 'resources' '${{ matrix.ffmpeg_output }}'
|
||||
Copy-Item -Path $source -Destination $destination -Force
|
||||
|
||||
- name: Download ffmpeg binary (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
shell: bash
|
||||
env:
|
||||
FFMPEG_OUTPUT: ${{ matrix.ffmpeg_output }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -L "${{ matrix.ffmpeg_arm_url }}" -o ffmpeg-arm.zip
|
||||
unzip -q ffmpeg-arm.zip -d ffmpeg-arm
|
||||
|
||||
curl -L "${{ matrix.ffmpeg_x86_url }}" -o ffmpeg-x86.zip
|
||||
unzip -q ffmpeg-x86.zip -d ffmpeg-x86
|
||||
|
||||
arm_bin="ffmpeg-arm/${{ matrix.ffmpeg_inner_path }}"
|
||||
x86_bin="ffmpeg-x86/${{ matrix.ffmpeg_inner_path }}"
|
||||
|
||||
if [[ ! -f "$arm_bin" ]]; then
|
||||
echo "::error::Missing arm64 ffmpeg binary at $arm_bin"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$x86_bin" ]]; then
|
||||
echo "::error::Missing x86_64 ffmpeg binary at $x86_bin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lipo -create "$arm_bin" "$x86_bin" -output "resources/$FFMPEG_OUTPUT"
|
||||
chmod +x "resources/$FFMPEG_OUTPUT"
|
||||
rm -rf ffmpeg-arm ffmpeg-x86 ffmpeg-arm.zip ffmpeg-x86.zip
|
||||
|
||||
- name: Download ffmpeg binary (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -L "${{ matrix.ffmpeg_url }}" -o ffmpeg.tar.xz
|
||||
mkdir ffmpeg
|
||||
tar -xf ffmpeg.tar.xz -C ffmpeg
|
||||
cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/${{ matrix.ffmpeg_output }}"
|
||||
chmod +x "resources/${{ matrix.ffmpeg_output }}"
|
||||
|
||||
- name: Download yt-dlp binary
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}"
|
||||
if [[ "${{ matrix.platform }}" == "linux" ]] || [[ "${{ matrix.platform }}" == "macos" ]]; then
|
||||
chmod +x "resources/${{ matrix.ytdlp_output }}"
|
||||
fi
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check && pnpm run typecheck
|
||||
|
||||
- name: Build application
|
||||
run: ${{ matrix.build_script }}
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: inputs.upload_artifacts == true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ matrix.os }}
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@@ -5,48 +5,5 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
build-script: pnpm run build:win
|
||||
ytdlp-asset: yt-dlp.exe
|
||||
ytdlp-output: yt-dlp.exe
|
||||
- os: ubuntu-latest
|
||||
build-script: pnpm run build:linux
|
||||
ytdlp-asset: yt-dlp
|
||||
ytdlp-output: yt-dlp_linux
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download yt-dlp binary
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp-asset }}" -o "resources/${{ matrix.ytdlp-output }}"
|
||||
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
chmod +x "resources/${{ matrix.ytdlp-output }}"
|
||||
fi
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check
|
||||
|
||||
- name: Build application
|
||||
run: ${{ matrix.build-script }}
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
|
||||
58
.github/workflows/release.yml
vendored
58
.github/workflows/release.yml
vendored
@@ -6,57 +6,26 @@ on:
|
||||
- v*.*.*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
upload_artifacts: true
|
||||
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
build_script: pnpm run build:win
|
||||
ytdlp_asset: yt-dlp.exe
|
||||
ytdlp_output: yt-dlp.exe
|
||||
- os: macos-latest
|
||||
build_script: pnpm run build:mac
|
||||
ytdlp_asset: yt-dlp_macos
|
||||
ytdlp_output: yt-dlp_macos
|
||||
- os: ubuntu-latest
|
||||
build_script: pnpm run build:linux
|
||||
ytdlp_asset: yt-dlp
|
||||
ytdlp_output: yt-dlp_linux
|
||||
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
node-version: 20
|
||||
pattern: dist-*
|
||||
merge-multiple: true
|
||||
path: dist/
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download yt-dlp binary
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}"
|
||||
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
chmod +x "resources/${{ matrix.ytdlp_output }}"
|
||||
fi
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check
|
||||
|
||||
- name: Build application
|
||||
run: ${{ matrix.build_script }}
|
||||
|
||||
- name: release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
@@ -73,4 +42,3 @@ jobs:
|
||||
dist/*.blockmap
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
|
||||
29
.github/workflows/translator.yaml
vendored
Normal file
29
.github/workflows/translator.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'translator'
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
discussion:
|
||||
types: [created, edited]
|
||||
discussion_comment:
|
||||
types: [created, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: lizheming/github-translate-action@c55aac477e98562d4faed9f77c54ab8306ae6ebf
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
@@ -58,6 +58,7 @@ src/
|
||||
- Build production bundles with `pnpm build`.
|
||||
- Create platform-specific artifacts with `pnpm build:win`, `pnpm build:mac`, or `pnpm build:linux`.
|
||||
- Use `pnpm build:unpack` to generate unpacked directories under `dist/` for manual inspection.
|
||||
- Bundle platform binaries of `yt-dlp` and `ffmpeg` under `resources/` (or set `YTDLP_PATH`/`FFMPEG_PATH`) before packaging so merges and audio extraction work out of the box.
|
||||
|
||||
## Working on Changes
|
||||
- Keep each pull request focused on a single problem or feature.
|
||||
|
||||
@@ -21,13 +21,12 @@ nsis:
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
artifactName: ${name}-${version}-${arch}.${ext}
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
|
||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vidbee",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.3",
|
||||
"description": "A modern Electron application for downloading videos and audios",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "VidBee",
|
||||
@@ -12,13 +12,14 @@
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "node scripts/set-console-encoding.js && electron-vite dev",
|
||||
"build": "pnpm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"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",
|
||||
"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",
|
||||
"release": "pnpm run check && bumpp"
|
||||
"release": "git checkout main && git pull && pnpm run check && bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
|
||||
5
resources/.gitignore
vendored
5
resources/.gitignore
vendored
@@ -2,8 +2,11 @@
|
||||
yt-dlp.exe
|
||||
yt-dlp_macos
|
||||
yt-dlp_linux
|
||||
ffmpeg.exe
|
||||
ffmpeg_macos
|
||||
ffmpeg_linux
|
||||
ffmpeg
|
||||
|
||||
# But keep the README
|
||||
!README.md
|
||||
!.gitignore
|
||||
|
||||
|
||||
@@ -48,8 +48,24 @@ Invoke-WebRequest -Uri "https://github.com/yt-dlp/yt-dlp/releases/latest/downloa
|
||||
Invoke-WebRequest -Uri "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" -OutFile "resources/yt-dlp_linux"
|
||||
```
|
||||
|
||||
## ffmpeg Binaries
|
||||
|
||||
ffmpeg is required for merging audio/video streams and audio extraction. Bundle the matching binary for each target platform:
|
||||
|
||||
### Required Files
|
||||
|
||||
1. **Windows**: `ffmpeg.exe`
|
||||
2. **macOS**: `ffmpeg_macos`
|
||||
3. **Linux**: `ffmpeg_linux`
|
||||
|
||||
### How to Download
|
||||
|
||||
- **Windows / Linux**: Grab static builds from <https://ffmpeg.org/download.html> (or <https://github.com/yt-dlp/FFmpeg-Builds/releases>) and rename the binary to match the filenames above.
|
||||
- **macOS**: Download the `ffmpeg-arm64*.zip` and `ffmpeg-x86_64*.zip` assets from <https://github.com/eko5624/mpv-mac/releases/latest>. Extract them and merge into a universal binary with `lipo -create`, then save the result as `resources/ffmpeg_macos`.
|
||||
- On macOS/Linux ensure the final binary is executable: `chmod +x resources/ffmpeg_macos` (or `ffmpeg_linux`).
|
||||
|
||||
### Note
|
||||
|
||||
- If you don't place binaries here, the app will attempt to download them at runtime
|
||||
- The app will automatically use the bundled version if available
|
||||
- File sizes: ~10-15 MB per binary
|
||||
- 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
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
/**
|
||||
* Check if yt-dlp binary exists in resources directory
|
||||
* Usage: node scripts/check-ytdlp.js [platform]
|
||||
* Platform options: win, mac, linux
|
||||
* Exit with error code 1 if not found
|
||||
*/
|
||||
function checkYtDlpExists(platform) {
|
||||
const platformMap = {
|
||||
win: 'yt-dlp.exe',
|
||||
mac: 'yt-dlp_macos',
|
||||
linux: 'yt-dlp_linux'
|
||||
}
|
||||
|
||||
const filename = platformMap[platform]
|
||||
if (!filename) {
|
||||
console.error('❌ Error: Invalid platform specified!')
|
||||
console.error('Usage: node scripts/check-ytdlp.js [win|mac|linux]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const ytdlpPath = path.join(__dirname, '..', 'resources', filename)
|
||||
|
||||
if (!fs.existsSync(ytdlpPath)) {
|
||||
console.error(`❌ Error: resources/${filename} not found!`)
|
||||
console.error(`Please download ${filename} to the resources/ directory first.`)
|
||||
console.error('You can download it from: https://github.com/yt-dlp/yt-dlp/releases/latest')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`✅ ${filename} found in resources/ directory`)
|
||||
}
|
||||
|
||||
// Get platform from command line arguments
|
||||
const platform = process.argv[2]
|
||||
|
||||
@@ -44,5 +12,61 @@ if (!platform) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkYtDlpExists(platform)
|
||||
const supportedPlatforms = ['win', 'mac', 'linux']
|
||||
|
||||
if (!supportedPlatforms.includes(platform)) {
|
||||
console.error('❌ Error: Invalid platform specified!')
|
||||
console.error('Usage: node scripts/check-ytdlp.js [win|mac|linux]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const binaries = [
|
||||
{
|
||||
label: 'yt-dlp',
|
||||
filenameMap: {
|
||||
win: 'yt-dlp.exe',
|
||||
mac: 'yt-dlp_macos',
|
||||
linux: 'yt-dlp_linux'
|
||||
},
|
||||
help: {
|
||||
default: 'https://github.com/yt-dlp/yt-dlp/releases/latest'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'ffmpeg',
|
||||
filenameMap: {
|
||||
win: 'ffmpeg.exe',
|
||||
mac: 'ffmpeg_macos',
|
||||
linux: 'ffmpeg_linux'
|
||||
},
|
||||
help: {
|
||||
win: 'https://ffmpeg.org/download.html',
|
||||
linux: 'https://ffmpeg.org/download.html',
|
||||
mac: 'https://github.com/eko5624/mpv-mac/releases/latest'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let hasMissingBinary = false
|
||||
|
||||
for (const binary of binaries) {
|
||||
const filename = binary.filenameMap[platform]
|
||||
const binaryPath = path.join(__dirname, '..', 'resources', filename)
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
console.error(`❌ Error: resources/${filename} not found!`)
|
||||
console.error(`Please download ${filename} to the resources/ directory first.`)
|
||||
const help =
|
||||
typeof binary.help === 'string' ? binary.help : binary.help[platform] || binary.help.default
|
||||
if (help) {
|
||||
console.error(`See ${help}`)
|
||||
}
|
||||
hasMissingBinary = true
|
||||
} else {
|
||||
console.log(`✅ ${filename} found in resources/ directory`)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMissingBinary) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
343
scripts/setup-dev-binaries.js
Executable file
343
scripts/setup-dev-binaries.js
Executable file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Development environment setup script
|
||||
* Automatically downloads yt-dlp and ffmpeg binaries based on the current system
|
||||
*/
|
||||
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const os = require('node:os')
|
||||
const { execSync } = require('node:child_process')
|
||||
const https = require('node:https')
|
||||
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'
|
||||
|
||||
// Platform configuration
|
||||
const PLATFORM_CONFIG = {
|
||||
win32: {
|
||||
ytdlp: {
|
||||
asset: 'yt-dlp.exe',
|
||||
output: 'yt-dlp.exe'
|
||||
},
|
||||
ffmpeg: {
|
||||
url: 'https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-win64-gpl.zip',
|
||||
innerPath: 'ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe',
|
||||
output: 'ffmpeg.exe',
|
||||
extract: 'unzip'
|
||||
}
|
||||
},
|
||||
darwin: {
|
||||
ytdlp: {
|
||||
asset: 'yt-dlp_macos',
|
||||
output: 'yt-dlp_macos'
|
||||
},
|
||||
ffmpeg: {
|
||||
// For development, download only the architecture matching current system
|
||||
arm64: {
|
||||
url: 'https://github.com/eko5624/mpv-mac/releases/download/2025-10-25/ffmpeg-arm64-defd5f3f64.zip',
|
||||
innerPath: 'ffmpeg/ffmpeg',
|
||||
output: 'ffmpeg_macos',
|
||||
extract: 'unzip'
|
||||
},
|
||||
x64: {
|
||||
url: 'https://github.com/eko5624/mpv-mac/releases/download/2025-10-25/ffmpeg-x86_64-defd5f3f64.zip',
|
||||
innerPath: 'ffmpeg/ffmpeg',
|
||||
output: 'ffmpeg_macos',
|
||||
extract: 'unzip'
|
||||
}
|
||||
}
|
||||
},
|
||||
linux: {
|
||||
ytdlp: {
|
||||
asset: 'yt-dlp',
|
||||
output: 'yt-dlp_linux'
|
||||
},
|
||||
ffmpeg: {
|
||||
url: 'https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-linux64-gpl.tar.xz',
|
||||
innerPath: 'ffmpeg-master-latest-linux64-gpl/bin/ffmpeg',
|
||||
output: 'ffmpeg_linux',
|
||||
extract: 'tar'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function log(message, type = 'info') {
|
||||
const icons = {
|
||||
info: '📦',
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warn: '⚠️',
|
||||
download: '⬇️'
|
||||
}
|
||||
console.log(`${icons[type] || 'ℹ️'} ${message}`)
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http
|
||||
const file = fs.createWriteStream(dest)
|
||||
|
||||
protocol
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
// Handle redirect
|
||||
file.close()
|
||||
fs.unlinkSync(dest)
|
||||
return downloadFile(response.headers.location, dest).then(resolve).catch(reject)
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
file.close()
|
||||
fs.unlinkSync(dest)
|
||||
return reject(new Error(`Failed to download: ${response.statusCode}`))
|
||||
}
|
||||
|
||||
response.pipe(file)
|
||||
file.on('finish', () => {
|
||||
file.close()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
.on('error', (err) => {
|
||||
file.close()
|
||||
fs.unlinkSync(dest)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function extractZip(zipPath, extractDir) {
|
||||
const platform = os.platform()
|
||||
ensureDir(extractDir)
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Use PowerShell Expand-Archive on Windows
|
||||
try {
|
||||
const zipAbsPath = path.resolve(zipPath)
|
||||
const extractAbsDir = path.resolve(extractDir)
|
||||
execSync(
|
||||
`powershell -NoProfile -Command "Expand-Archive -Path '${zipAbsPath.replace(/'/g, "''")}' -DestinationPath '${extractAbsDir.replace(/'/g, "''")}' -Force"`,
|
||||
{ stdio: 'inherit' }
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to extract zip: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
// Use unzip command on macOS/Linux
|
||||
try {
|
||||
execSync(`unzip -q "${zipPath}" -d "${extractDir}"`, { stdio: 'inherit' })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to extract zip: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractTarXz(tarPath, extractDir) {
|
||||
ensureDir(extractDir)
|
||||
execSync(`tar -xf "${tarPath}" -C "${extractDir}"`, { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
function setExecutable(filePath) {
|
||||
if (os.platform() !== 'win32') {
|
||||
fs.chmodSync(filePath, 0o755)
|
||||
}
|
||||
}
|
||||
|
||||
function fileExists(filePath) {
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
|
||||
// Main download functions
|
||||
async function downloadYtDlp(config) {
|
||||
const { asset, output } = config.ytdlp
|
||||
const outputPath = path.join(RESOURCES_DIR, output)
|
||||
|
||||
if (fileExists(outputPath)) {
|
||||
log(`${output} already exists, skipping download`, 'info')
|
||||
return
|
||||
}
|
||||
|
||||
log(`Downloading ${asset}...`, 'download')
|
||||
const url = `${YTDLP_BASE_URL}/${asset}`
|
||||
const tempPath = path.join(RESOURCES_DIR, `.${asset}.tmp`)
|
||||
|
||||
try {
|
||||
await downloadFile(url, tempPath)
|
||||
fs.renameSync(tempPath, outputPath)
|
||||
setExecutable(outputPath)
|
||||
log(`Downloaded ${output} successfully`, 'success')
|
||||
} catch (error) {
|
||||
if (fs.existsSync(tempPath)) {
|
||||
fs.unlinkSync(tempPath)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFfmpegWindows(config) {
|
||||
const { url, innerPath, output } = config.ffmpeg
|
||||
const outputPath = path.join(RESOURCES_DIR, output)
|
||||
|
||||
if (fileExists(outputPath)) {
|
||||
log(`${output} already exists, skipping download`, 'info')
|
||||
return
|
||||
}
|
||||
|
||||
log(`Downloading ffmpeg for Windows...`, 'download')
|
||||
const tempZip = path.join(RESOURCES_DIR, 'ffmpeg-temp.zip')
|
||||
const extractDir = path.join(RESOURCES_DIR, 'ffmpeg-temp')
|
||||
|
||||
try {
|
||||
await downloadFile(url, tempZip)
|
||||
log('Extracting ffmpeg...', 'info')
|
||||
extractZip(tempZip, extractDir)
|
||||
|
||||
const sourcePath = path.join(extractDir, innerPath.replace(/\\/g, path.sep))
|
||||
if (!fileExists(sourcePath)) {
|
||||
throw new Error(`ffmpeg binary not found at ${sourcePath}`)
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, outputPath)
|
||||
log(`Downloaded ${output} successfully`, 'success')
|
||||
|
||||
// Cleanup
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFfmpegMac(config) {
|
||||
const arch = os.arch()
|
||||
const ffmpegConfig = config.ffmpeg[arch === 'arm64' ? 'arm64' : 'x64']
|
||||
|
||||
if (!ffmpegConfig) {
|
||||
throw new Error(`Unsupported architecture: ${arch}`)
|
||||
}
|
||||
|
||||
const { url, innerPath, output } = ffmpegConfig
|
||||
const outputPath = path.join(RESOURCES_DIR, output)
|
||||
|
||||
if (fileExists(outputPath)) {
|
||||
log(`${output} already exists, skipping download`, 'info')
|
||||
return
|
||||
}
|
||||
|
||||
log(`Downloading ffmpeg for macOS (${arch})...`, 'download')
|
||||
const tempZip = path.join(RESOURCES_DIR, 'ffmpeg-temp.zip')
|
||||
const extractDir = path.join(RESOURCES_DIR, 'ffmpeg-temp')
|
||||
|
||||
try {
|
||||
await downloadFile(url, tempZip)
|
||||
log('Extracting ffmpeg...', 'info')
|
||||
extractZip(tempZip, extractDir)
|
||||
|
||||
const sourcePath = path.join(extractDir, innerPath)
|
||||
if (!fileExists(sourcePath)) {
|
||||
throw new Error(`ffmpeg binary not found at ${sourcePath}`)
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, outputPath)
|
||||
setExecutable(outputPath)
|
||||
log(`Downloaded ${output} successfully`, 'success')
|
||||
|
||||
// Cleanup
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFfmpegLinux(config) {
|
||||
const { url, innerPath, output } = config.ffmpeg
|
||||
const outputPath = path.join(RESOURCES_DIR, output)
|
||||
|
||||
if (fileExists(outputPath)) {
|
||||
log(`${output} already exists, skipping download`, 'info')
|
||||
return
|
||||
}
|
||||
|
||||
log(`Downloading ffmpeg for Linux...`, 'download')
|
||||
const tempTar = path.join(RESOURCES_DIR, 'ffmpeg-temp.tar.xz')
|
||||
const extractDir = path.join(RESOURCES_DIR, 'ffmpeg-temp')
|
||||
|
||||
try {
|
||||
await downloadFile(url, tempTar)
|
||||
log('Extracting ffmpeg...', 'info')
|
||||
extractTarXz(tempTar, extractDir)
|
||||
|
||||
const sourcePath = path.join(extractDir, innerPath)
|
||||
if (!fileExists(sourcePath)) {
|
||||
throw new Error(`ffmpeg binary not found at ${sourcePath}`)
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, outputPath)
|
||||
setExecutable(outputPath)
|
||||
log(`Downloaded ${output} successfully`, 'success')
|
||||
|
||||
// Cleanup
|
||||
fs.unlinkSync(tempTar)
|
||||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
if (fs.existsSync(tempTar)) fs.unlinkSync(tempTar)
|
||||
if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true, force: true })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Main setup function
|
||||
async function setup() {
|
||||
const platform = os.platform()
|
||||
const config = PLATFORM_CONFIG[platform]
|
||||
|
||||
if (!config) {
|
||||
log(`Unsupported platform: ${platform}`, 'error')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log(`Setting up development binaries for ${platform}...`, 'info')
|
||||
ensureDir(RESOURCES_DIR)
|
||||
|
||||
try {
|
||||
// Download yt-dlp
|
||||
await downloadYtDlp(config)
|
||||
|
||||
// Download ffmpeg
|
||||
if (platform === 'win32') {
|
||||
await downloadFfmpegWindows(config)
|
||||
} else if (platform === 'darwin') {
|
||||
await downloadFfmpegMac(config)
|
||||
} else if (platform === 'linux') {
|
||||
await downloadFfmpegLinux(config)
|
||||
}
|
||||
|
||||
log('Development environment setup completed!', 'success')
|
||||
} catch (error) {
|
||||
log(`Setup failed: ${error.message}`, 'error')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run setup
|
||||
if (require.main === module) {
|
||||
setup()
|
||||
}
|
||||
|
||||
module.exports = { setup }
|
||||
@@ -14,7 +14,8 @@ export const resolveVideoFormatSelector = (options: DownloadOptions): string =>
|
||||
return 'bestvideo+none'
|
||||
}
|
||||
if (!audioFormat || audioFormat === 'best') {
|
||||
return 'best'
|
||||
// Use bestvideo+bestaudio to ensure video and audio are merged into a single file
|
||||
return 'bestvideo+bestaudio'
|
||||
}
|
||||
return `bestvideo+${audioFormat}`
|
||||
}
|
||||
@@ -53,7 +54,11 @@ export const buildDownloadArgs = (
|
||||
|
||||
// Format selection
|
||||
if (options.type === 'video') {
|
||||
args.push('-f', resolveVideoFormatSelector(options))
|
||||
const formatSelector = resolveVideoFormatSelector(options)
|
||||
args.push('-f', formatSelector)
|
||||
// Let yt-dlp automatically choose the best merge format (mkv/webm/mp4)
|
||||
// based on codec compatibility. Forcing MP4 can cause failures
|
||||
// when codecs are incompatible (e.g., VP9+Opus requires mkv/webm)
|
||||
} else if (options.type === 'audio') {
|
||||
args.push('-f', resolveAudioFormatSelector(options))
|
||||
} else if (options.type === 'extract') {
|
||||
|
||||
@@ -7,6 +7,7 @@ import appIcon from '../../build/icon.png?asset'
|
||||
import { configureLogger } from './config/logger-config'
|
||||
import { services } from './ipc'
|
||||
import { downloadEngine } from './lib/download-engine'
|
||||
import { ffmpegManager } from './lib/ffmpeg-manager'
|
||||
import { ytdlpManager } from './lib/ytdlp-manager'
|
||||
import { settingsManager } from './settings'
|
||||
import { createTray, destroyTray } from './tray'
|
||||
@@ -179,6 +180,15 @@ app.whenReady().then(async () => {
|
||||
// IPC services are automatically registered by electron-ipc-decorator when imported
|
||||
log.info('IPC services available:', Object.keys(services))
|
||||
|
||||
// Initialize ffmpeg
|
||||
try {
|
||||
log.info('Initializing ffmpeg...')
|
||||
await ffmpegManager.initialize()
|
||||
log.info('ffmpeg initialized successfully')
|
||||
} catch (error) {
|
||||
log.error('Failed to initialize ffmpeg:', error)
|
||||
}
|
||||
|
||||
// Initialize yt-dlp
|
||||
try {
|
||||
log.info('Initializing yt-dlp...')
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
VideoFormat,
|
||||
VideoInfo
|
||||
} from '../../shared/types'
|
||||
import { buildDownloadArgs } from '../download-engine/args-builder'
|
||||
import { buildDownloadArgs, resolveVideoFormatSelector } from '../download-engine/args-builder'
|
||||
import {
|
||||
findFormatByIdCandidates,
|
||||
parseSizeToBytes,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { settingsManager } from '../settings'
|
||||
import { scopedLoggers } from '../utils/logger'
|
||||
import { DownloadQueue } from './download-queue'
|
||||
import { ffmpegManager } from './ffmpeg-manager'
|
||||
import { historyManager } from './history-manager'
|
||||
import { ytdlpManager } from './ytdlp-manager'
|
||||
|
||||
@@ -496,6 +497,46 @@ class DownloadEngine extends EventEmitter {
|
||||
|
||||
const args = buildDownloadArgs(options, downloadPath, settings)
|
||||
|
||||
// Check if format selector contains '+' which means video and audio will be merged
|
||||
const formatSelector =
|
||||
options.type === 'video' ? resolveVideoFormatSelector(options) : undefined
|
||||
const willMerge = formatSelector?.includes('+') ?? false
|
||||
|
||||
const urlArg = args.pop()
|
||||
if (!urlArg) {
|
||||
const missingUrlError = new Error('Download arguments missing URL.')
|
||||
scopedLoggers.download.error('Missing URL argument for download ID:', id)
|
||||
this.updateDownloadInfo(id, {
|
||||
status: 'error',
|
||||
completedAt: Date.now(),
|
||||
error: missingUrlError.message
|
||||
})
|
||||
this.queue.downloadCompleted(id)
|
||||
this.emit('download-error', id, missingUrlError)
|
||||
this.addToHistory(id, options, 'error', missingUrlError.message)
|
||||
return
|
||||
}
|
||||
|
||||
let ffmpegPath: string
|
||||
try {
|
||||
ffmpegPath = ffmpegManager.getPath()
|
||||
} catch (error) {
|
||||
const ffmpegError = error instanceof Error ? error : new Error(String(error))
|
||||
scopedLoggers.download.error('Failed to resolve ffmpeg for download ID:', id, ffmpegError)
|
||||
this.updateDownloadInfo(id, {
|
||||
status: 'error',
|
||||
completedAt: Date.now(),
|
||||
error: ffmpegError.message
|
||||
})
|
||||
this.queue.downloadCompleted(id)
|
||||
this.emit('download-error', id, ffmpegError)
|
||||
this.addToHistory(id, options, 'error', ffmpegError.message)
|
||||
return
|
||||
}
|
||||
|
||||
args.push('--ffmpeg-location', ffmpegPath)
|
||||
args.push(urlArg)
|
||||
|
||||
const controller = new AbortController()
|
||||
const ytdlpProcess = ytdlp.exec(args, {
|
||||
signal: controller.signal
|
||||
@@ -593,23 +634,81 @@ class DownloadEngine extends EventEmitter {
|
||||
// Generate file path using downloadPath + title + ext
|
||||
const title = videoInfo?.title || 'Unknown'
|
||||
const sanitizedTitle = title.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50)
|
||||
const extension =
|
||||
options.type === 'audio' ? options.extractFormat || 'mp3' : actualFormat || 'mp4'
|
||||
|
||||
// Determine file extension based on download type and format
|
||||
// yt-dlp automatically chooses the best merge format (mkv/webm/mp4)
|
||||
// based on codec compatibility, so we should use actualFormat when available
|
||||
let extension: string
|
||||
if (options.type === 'audio') {
|
||||
extension = options.extractFormat || 'mp3'
|
||||
} else if (willMerge) {
|
||||
// For merged files, yt-dlp auto-selects format (mkv/webm/mp4)
|
||||
// Use actualFormat if available, otherwise default to mkv (most compatible)
|
||||
extension = actualFormat || 'mkv'
|
||||
} else {
|
||||
extension = actualFormat || 'mp4'
|
||||
}
|
||||
|
||||
const fileName = `${sanitizedTitle}.${extension}`
|
||||
const finalOutputPath = path.join(downloadPath, fileName)
|
||||
|
||||
scopedLoggers.download.info('Generated file path for ID:', id, 'Path:', finalOutputPath)
|
||||
scopedLoggers.download.info(
|
||||
'Generated file path for ID:',
|
||||
id,
|
||||
'Path:',
|
||||
finalOutputPath,
|
||||
'Will merge:',
|
||||
willMerge
|
||||
)
|
||||
|
||||
let fileSize: number | undefined
|
||||
let actualFilePath = finalOutputPath
|
||||
try {
|
||||
const fs = await import('node:fs/promises')
|
||||
// Try to find the actual file - yt-dlp may generate files with slightly different names
|
||||
const stats = await fs.stat(finalOutputPath)
|
||||
fileSize = stats.size
|
||||
actualFilePath = finalOutputPath
|
||||
} catch (error) {
|
||||
if (latestKnownSizeBytes !== undefined) {
|
||||
fileSize = latestKnownSizeBytes
|
||||
} else {
|
||||
scopedLoggers.download.warn('Failed to get file size for ID:', id, error)
|
||||
// If the expected file doesn't exist, try to find it by scanning the directory
|
||||
try {
|
||||
const fs = await import('node:fs/promises')
|
||||
const files = await fs.readdir(downloadPath)
|
||||
// Look for files matching the title pattern with the correct extension
|
||||
const matchingFiles = files.filter((file) => {
|
||||
const baseName = file.replace(/\.[^.]+$/, '')
|
||||
const fileExt = file.split('.').pop()?.toLowerCase()
|
||||
return (
|
||||
(baseName === sanitizedTitle || baseName.startsWith(sanitizedTitle)) &&
|
||||
fileExt === extension.toLowerCase()
|
||||
)
|
||||
})
|
||||
|
||||
if (matchingFiles.length > 0) {
|
||||
// Use the most recently modified file if multiple matches
|
||||
const fileStats = await Promise.all(
|
||||
matchingFiles.map(async (file) => {
|
||||
const filePath = path.join(downloadPath, file)
|
||||
const stats = await fs.stat(filePath)
|
||||
return { file, path: filePath, mtime: stats.mtime, size: stats.size }
|
||||
})
|
||||
)
|
||||
const mostRecent = fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0]
|
||||
actualFilePath = mostRecent.path
|
||||
fileSize = mostRecent.size
|
||||
scopedLoggers.download.info('Found actual file:', actualFilePath, 'Size:', fileSize)
|
||||
} else if (latestKnownSizeBytes !== undefined) {
|
||||
fileSize = latestKnownSizeBytes
|
||||
scopedLoggers.download.warn('File not found, using estimated size:', fileSize)
|
||||
} else {
|
||||
scopedLoggers.download.warn('Failed to find file for ID:', id, error)
|
||||
}
|
||||
} catch (scanError) {
|
||||
if (latestKnownSizeBytes !== undefined) {
|
||||
fileSize = latestKnownSizeBytes
|
||||
} else {
|
||||
scopedLoggers.download.warn('Failed to get file size for ID:', id, scanError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,7 +720,7 @@ class DownloadEngine extends EventEmitter {
|
||||
status: 'completed',
|
||||
completedAt: Date.now(),
|
||||
fileSize,
|
||||
format: actualFormat || undefined,
|
||||
format: willMerge ? 'mp4' : actualFormat || undefined,
|
||||
quality: actualQuality || undefined,
|
||||
codec: actualCodec || undefined
|
||||
})
|
||||
|
||||
101
src/main/lib/ffmpeg-manager.ts
Normal file
101
src/main/lib/ffmpeg-manager.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
class FfmpegManager {
|
||||
private ffmpegPath: string | null = null
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.ffmpegPath = await this.findFfmpegBinary()
|
||||
console.log('ffmpeg initialized at:', this.ffmpegPath)
|
||||
}
|
||||
|
||||
getPath(): string {
|
||||
if (!this.ffmpegPath) {
|
||||
throw new Error('ffmpeg not initialized. Call initialize() first.')
|
||||
}
|
||||
return this.ffmpegPath
|
||||
}
|
||||
|
||||
private getResourcesPath(): string {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return path.join(process.cwd(), 'resources')
|
||||
}
|
||||
return path.join(process.resourcesPath, 'app.asar.unpacked', 'resources')
|
||||
}
|
||||
|
||||
private async findFfmpegBinary(): Promise<string> {
|
||||
const platform = os.platform()
|
||||
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)
|
||||
return process.env.FFMPEG_PATH
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
resourceCandidates.push('ffmpeg.exe')
|
||||
} else if (platform === 'darwin') {
|
||||
resourceCandidates.push('ffmpeg_macos', 'ffmpeg')
|
||||
} else {
|
||||
resourceCandidates.push('ffmpeg_linux', 'ffmpeg')
|
||||
}
|
||||
|
||||
const resourcesPath = this.getResourcesPath()
|
||||
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) {
|
||||
console.warn('Failed to set executable permission on ffmpeg binary:', error)
|
||||
}
|
||||
}
|
||||
console.log('Using bundled ffmpeg:', fullPath)
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
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)
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'linux' || platform === 'freebsd') {
|
||||
try {
|
||||
const systemPath = execSync('which ffmpeg').toString().trim()
|
||||
if (systemPath && fs.existsSync(systemPath)) {
|
||||
console.log('Using system ffmpeg:', systemPath)
|
||||
return systemPath
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore error and continue
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
try {
|
||||
const output = execSync('where ffmpeg').toString().split(/\r?\n/)[0]
|
||||
if (output && fs.existsSync(output)) {
|
||||
console.log('Using system ffmpeg:', output)
|
||||
return output
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore error and continue
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'ffmpeg not found. Bundle it under resources/ (asarUnpack) or set the FFMPEG_PATH environment variable.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const ffmpegManager = new FfmpegManager()
|
||||
@@ -76,18 +76,20 @@ const getQualityPreset = (settings: AppSettings): OneClickQualityPreset =>
|
||||
|
||||
const buildAudioSelectors = (preset: OneClickQualityPreset): string[] => {
|
||||
if (preset === 'worst') {
|
||||
return ['worstaudio', 'worst']
|
||||
return ['worstaudio']
|
||||
}
|
||||
|
||||
const abrLimit = qualityPresetToAudioAbr[preset]
|
||||
return dedupe([abrLimit ? `bestaudio[abr<=${abrLimit}]` : undefined, 'bestaudio', 'best'])
|
||||
// Remove 'best' fallback to ensure merging - only use 'bestaudio' variants
|
||||
return dedupe([abrLimit ? `bestaudio[abr<=${abrLimit}]` : undefined, 'bestaudio'])
|
||||
}
|
||||
|
||||
const buildVideoFormatPreference = (settings: AppSettings): string => {
|
||||
const preset = getQualityPreset(settings)
|
||||
|
||||
if (preset === 'worst') {
|
||||
return 'worstvideo+worstaudio/worst'
|
||||
// Use worstvideo+worstaudio as fallback instead of 'worst' to ensure merging
|
||||
return 'worstvideo+worstaudio'
|
||||
}
|
||||
|
||||
const maxHeight = qualityPresetToVideoHeight[preset]
|
||||
@@ -110,7 +112,8 @@ const buildVideoFormatPreference = (settings: AppSettings): string => {
|
||||
combinations.push(video)
|
||||
}
|
||||
} else {
|
||||
combinations.push('best')
|
||||
// Use bestvideo+bestaudio as fallback instead of 'best' to ensure merging
|
||||
combinations.push('bestvideo+bestaudio')
|
||||
}
|
||||
|
||||
return dedupe(combinations).join('/')
|
||||
|
||||
Reference in New Issue
Block a user