Improve configuration validation and error handling: translate comments to English, add directory validation, increase filename length limit, add --no-check-certificate flag

This commit is contained in:
ALIHAN DIKEL
2025-08-10 15:58:20 +03:00
parent da7e4666ed
commit 1710659b2a
4 changed files with 50 additions and 29 deletions

View File

@@ -1,3 +1,4 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
@@ -6,31 +7,31 @@ type DeepPartial<T> = {
};
/**
* 配置類型定義
* Configuration type definition
*/
export interface Config {
// 文件相關配置
// File related configuration
file: {
maxFilenameLength: number;
downloadsDir: string;
tempDirPrefix: string;
// 文件名處理相關配置
// Filename sanitization configuration
sanitize: {
// 替換非法字符為此字符
// Character to replace illegal characters with
replaceChar: string;
// 文件名截斷時的後綴
// Suffix when truncating filenames
truncateSuffix: string;
// 非法字符正則表達式
// Regular expression for illegal characters
illegalChars: RegExp;
// 保留字列表
// Reserved names list
reservedNames: readonly string[];
};
};
// 工具相關配置
// Tool related configuration
tools: {
required: readonly string[];
};
// 下載相關配置
// Download related configuration
download: {
defaultResolution: "480p" | "720p" | "1080p" | "best";
defaultAudioFormat: "m4a" | "mp3";
@@ -39,17 +40,17 @@ export interface Config {
}
/**
* 默認配置
* Default configuration
*/
const defaultConfig: Config = {
file: {
maxFilenameLength: 50,
maxFilenameLength: 100, // Increased from 50 for better compatibility
downloadsDir: path.join(os.homedir(), "Downloads"),
tempDirPrefix: "ytdlp-",
sanitize: {
replaceChar: '_',
truncateSuffix: '...',
illegalChars: /[<>:"/\\|?*\x00-\x1F]/g, // Windows 非法字符
illegalChars: /[<>:"/\\|?*\x00-\x1F]/g, // Windows illegal characters
reservedNames: [
'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
@@ -68,12 +69,12 @@ const defaultConfig: Config = {
};
/**
* 從環境變數加載配置
* Load configuration from environment variables
*/
function loadEnvConfig(): DeepPartial<Config> {
const envConfig: DeepPartial<Config> = {};
// 文件配置
// File configuration
const fileConfig: DeepPartial<Config['file']> = {
sanitize: {
replaceChar: process.env.YTDLP_SANITIZE_REPLACE_CHAR,
@@ -97,7 +98,7 @@ function loadEnvConfig(): DeepPartial<Config> {
envConfig.file = fileConfig;
}
// 下載配置
// Download configuration
const downloadConfig: Partial<Config['download']> = {};
if (process.env.YTDLP_DEFAULT_RESOLUTION &&
['480p', '720p', '1080p', 'best'].includes(process.env.YTDLP_DEFAULT_RESOLUTION)) {
@@ -118,42 +119,56 @@ function loadEnvConfig(): DeepPartial<Config> {
}
/**
* 驗證配置
* Validate configuration
*/
function validateConfig(config: Config): void {
// 驗證文件名長度
// Validate filename length
if (config.file.maxFilenameLength < 5) {
throw new Error('maxFilenameLength must be at least 5');
}
// 驗證下載目錄
// Validate downloads directory
if (!config.file.downloadsDir) {
throw new Error('downloadsDir must be specified');
}
// 驗證臨時目錄前綴
// Try to create the downloads directory if it doesn't exist
try {
if (!fs.existsSync(config.file.downloadsDir)) {
fs.mkdirSync(config.file.downloadsDir, { recursive: true });
}
// Test write permissions
const testFile = path.join(config.file.downloadsDir, `.writable-test-${Date.now()}`);
fs.writeFileSync(testFile, '');
fs.unlinkSync(testFile);
} catch (error) {
throw new Error(`Cannot access downloads directory '${config.file.downloadsDir}': ${error.message}`);
}
// Validate temp directory prefix
if (!config.file.tempDirPrefix) {
throw new Error('tempDirPrefix must be specified');
}
// 驗證默認分辨率
// Validate default resolution
if (!['480p', '720p', '1080p', 'best'].includes(config.download.defaultResolution)) {
throw new Error('Invalid defaultResolution');
}
// 驗證默認音頻格式
// Validate default audio format
if (!['m4a', 'mp3'].includes(config.download.defaultAudioFormat)) {
throw new Error('Invalid defaultAudioFormat');
}
// 驗證默認字幕語言
// Validate default subtitle language
if (!/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/i.test(config.download.defaultSubtitleLanguage)) {
throw new Error('Invalid defaultSubtitleLanguage');
}
}
/**
* 合併配置
* Merge configurations
*/
function mergeConfig(base: Config, override: DeepPartial<Config>): Config {
return {
@@ -180,7 +195,7 @@ function mergeConfig(base: Config, override: DeepPartial<Config>): Config {
}
/**
* 加載配置
* Load configuration
*/
export function loadConfig(): Config {
const envConfig = loadEnvConfig();
@@ -190,19 +205,19 @@ export function loadConfig(): Config {
}
/**
* 安全的文件名處理函數
* Safe filename processing function
*/
export function sanitizeFilename(filename: string, config: Config['file']): string {
// 移除非法字符
// Remove illegal characters
let safe = filename.replace(config.sanitize.illegalChars, config.sanitize.replaceChar);
// 檢查保留字
// Check reserved names
const basename = path.parse(safe).name.toUpperCase();
if (config.sanitize.reservedNames.includes(basename)) {
safe = `_${safe}`;
}
// 處理長度限制
// Handle length limitation
if (safe.length > config.maxFilenameLength) {
const ext = path.extname(safe);
const name = safe.slice(0, config.maxFilenameLength - ext.length - config.sanitize.truncateSuffix.length);
@@ -212,5 +227,5 @@ export function sanitizeFilename(filename: string, config: Config['file']): stri
return safe;
}
// 導出當前配置實例
// Export current configuration instance
export const CONFIG = loadConfig();

View File

@@ -48,6 +48,7 @@ export async function downloadAudio(url: string, config: Config): Promise<string
"--progress",
"--newline",
"--no-mtime",
"--no-check-certificate",
"-f", format,
"--output", outputTemplate,
url

View File

@@ -32,6 +32,7 @@ export async function listSubtitles(url: string): Promise<string> {
'--write-auto-sub',
'--skip-download',
'--verbose',
'--no-check-certificate',
url
]);
return output;
@@ -85,6 +86,7 @@ export async function downloadSubtitles(
'--write-auto-sub',
'--sub-lang', language,
'--skip-download',
'--no-check-certificate',
'--output', path.join(tempDir, '%(title)s.%(ext)s'),
url
]);
@@ -145,6 +147,7 @@ export async function downloadTranscript(
'--sub-lang', language,
'--sub-format', 'ttml',
'--convert-subs', 'srt',
'--no-check-certificate',
'--output', path.join(tempDir, 'transcript.%(ext)s'),
url
]);

View File

@@ -92,6 +92,7 @@ export async function downloadVideo(
"--get-filename",
"-f", format,
"--output", outputTemplate,
"--no-check-certificate",
url
]);
expectedFilename = expectedFilename.trim();
@@ -108,6 +109,7 @@ export async function downloadVideo(
"--progress",
"--newline",
"--no-mtime",
"--no-check-certificate",
"-f", format,
"--output", outputTemplate,
url