146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
import path from 'node:path'
|
|
import type { AppSettings, DownloadOptions } from '../../shared/types'
|
|
import { resolvePathWithHome } from '../utils/path-helpers'
|
|
|
|
export const sanitizeFilenameTemplate = (template: string): string => {
|
|
const trimmed = template.trim()
|
|
if (!trimmed) {
|
|
return '%(title)s via VidBee.%(ext)s'
|
|
}
|
|
const normalized = trimmed.replace(/\\/g, '/')
|
|
const safeParts = normalized
|
|
.split('/')
|
|
.map((part) => part.trim())
|
|
.filter((part) => part !== '' && part !== '.' && part !== '..')
|
|
.map((part) => part.replace(/[<>:"|?*]/g, '-').replace(/[. ]+$/g, ''))
|
|
.filter((part) => part !== '')
|
|
return safeParts.length === 0 ? '%(title)s via VidBee.%(ext)s' : safeParts.join('/')
|
|
}
|
|
|
|
export const resolveVideoFormatSelector = (options: DownloadOptions): string => {
|
|
const format = options.format
|
|
const audioFormat = options.audioFormat
|
|
|
|
if (format && audioFormat === '') {
|
|
return format
|
|
}
|
|
|
|
if (format && (format.includes('/') || (audioFormat === undefined && format.includes('+')))) {
|
|
return format
|
|
}
|
|
|
|
if (!format || format === 'best') {
|
|
if (audioFormat === 'none') {
|
|
return 'bestvideo+none'
|
|
}
|
|
if (!audioFormat || audioFormat === 'best') {
|
|
// Use bestvideo+bestaudio to ensure video and audio are merged into a single file
|
|
return 'bestvideo+bestaudio'
|
|
}
|
|
return `bestvideo+${audioFormat}`
|
|
}
|
|
|
|
if (audioFormat === 'none') {
|
|
return `${format}+none`
|
|
}
|
|
|
|
const audio = audioFormat && audioFormat !== 'best' ? audioFormat : 'bestaudio'
|
|
return `${format}+${audio}`
|
|
}
|
|
|
|
export const resolveAudioFormatSelector = (options: DownloadOptions): string => {
|
|
const format = options.format
|
|
|
|
if (!format) {
|
|
return 'bestaudio'
|
|
}
|
|
|
|
if (format.includes('/') || format.includes('+') || format.includes('[')) {
|
|
return format
|
|
}
|
|
|
|
return format
|
|
}
|
|
|
|
export const buildDownloadArgs = (
|
|
options: DownloadOptions,
|
|
downloadPath: string,
|
|
settings: AppSettings,
|
|
jsRuntimeArgs: string[] = []
|
|
): string[] => {
|
|
const args: string[] = ['--no-playlist', '--embed-chapters', '--no-mtime']
|
|
|
|
// Add encoding support for proper handling of non-ASCII characters
|
|
args.push('--encoding', 'utf-8')
|
|
|
|
// Format selection
|
|
if (options.type === 'video') {
|
|
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') {
|
|
args.push('-x')
|
|
args.push('--audio-format', options.extractFormat || 'mp3')
|
|
args.push('--audio-quality', options.extractQuality || '5')
|
|
}
|
|
|
|
// Time range
|
|
if (options.startTime || options.endTime) {
|
|
const start = options.startTime || '0'
|
|
const end = options.endTime || ''
|
|
args.push('--download-sections', `*${start}-${end || ''}`)
|
|
}
|
|
|
|
// Subtitles
|
|
if (options.downloadSubs) {
|
|
args.push('--write-subs', '--sub-langs', 'all')
|
|
}
|
|
|
|
// Output path with proper encoding handling
|
|
const baseDownloadPath = options.customDownloadPath?.trim() || downloadPath
|
|
const filenameTemplate = sanitizeFilenameTemplate(
|
|
options.customFilenameTemplate ?? '%(title)s via VidBee.%(ext)s'
|
|
)
|
|
const safeTemplate = filenameTemplate.replace(/^[\\/]+/, '')
|
|
const outputTemplate = path.join(baseDownloadPath, safeTemplate)
|
|
args.push('-o', outputTemplate)
|
|
|
|
// Add options for better filename handling
|
|
args.push('--no-part')
|
|
args.push('--no-playlist-reverse')
|
|
|
|
if (process.platform === 'win32') {
|
|
args.push('--windows-filenames')
|
|
}
|
|
|
|
if (settings.browserForCookies && settings.browserForCookies !== 'none') {
|
|
args.push('--cookies-from-browser', settings.browserForCookies)
|
|
}
|
|
|
|
const cookiesPath = settings.cookiesPath?.trim()
|
|
if (cookiesPath) {
|
|
args.push('--cookies', cookiesPath)
|
|
}
|
|
|
|
if (settings.proxy) {
|
|
args.push('--proxy', settings.proxy)
|
|
}
|
|
|
|
const configPath = resolvePathWithHome(settings.configPath)
|
|
if (configPath) {
|
|
args.push('--config-location', configPath)
|
|
}
|
|
|
|
if (jsRuntimeArgs.length > 0) {
|
|
args.push(...jsRuntimeArgs)
|
|
}
|
|
|
|
args.push(options.url)
|
|
|
|
return args
|
|
}
|